Compare commits

..

214 Commits
0.1.5 ... 0.1.7

Author SHA1 Message Date
Fabian Dill
89984a0d09 Core: don't start threads for 'pass'
Core: print output progress every 10 files (OoT output may take a while, so let's give some user feedback on progress)
Subnautica: remove empty output method
2021-09-03 20:35:40 +02:00
Fabian Dill
2e2ca1665b Core: don't start threads for 'pass'
Core: print output progress every 10 files (OoT output may take a while, so let's give some user feedback on progress)
Subnautica: remove empty output method
2021-09-03 17:30:10 +02:00
Fabian Dill
1b27fc495f Ocarina of Time: reduce memory use by 64 MiB for each OoT world past the first
Ocarina of Time: limit parallel output to 2, to not waste memory that doesn't benefit speed
Ocarina of Time: remove swarm of os.chdir()
2021-09-03 12:50:26 +02:00
espeon65536
51c38fc628 Ocarina of Time (#64)
* first commit (not including OoT data files yet)

* added some basic options

* rule parser works now at least

* make sure to commit everything this time

* temporary change to BaseClasses for oot

* overworld location graph builds mostly correctly

* adding oot data files

* commenting out world options until later since they only existed to make the RuleParser work

* conversion functions between AP ids and OOT ids

* world graph outputs

* set scrub prices

* itempool generates, entrances connected, way too many options added

* fixed set_rules and set_shop_rules

* temp baseclasses changes

* Reaches the fill step now, old event-based system retained in case the new way breaks

* Song placements and misc fixes everywhere

* temporary changes to make oot work

* changed root exits for AP fill framework

* prevent infinite recursion due to OoT sharing usage of the address field

* age reachability works hopefully, songs are broken again

* working spoiler log generation on beatable-only

* Logic tricks implemented

* need this for logic tricks

* fixed map/compass being placed on Serenade location

* kill unreachable events before filling the world

* add a bunch of utility functions to prepare for rom patching

* move OptionList into generic options

* fixed some silly bugs with OptionList

* properly seed all random behavior (so far)

* ROM generation working

* fix hints trying to get alttp dungeon hint texts

* continue fixing hints

* add oot to network data package

* change item and location IDs to 66000 and 67000 range respectively

* push removed items to precollected items

* fixed various issues with cross-contamination with multiple world generation

* reenable glitched logic (hopefully)

* glitched world files age-check fix

* cleaned up some get_locations calls

* added token shuffle and scrub shuffle, modified some options slightly to make the parsing work

* reenable MQ dungeons

* fix forest mq exception

* made targeting style an option for now, will be cosmetic later

* reminder to move targeting to cosmetics

* some oot option maintenance

* enabled starting time of day

* fixed issue breaking shop slots in multiworld generation

* added "off" option for text shuffle and hints

* shopsanity functionality restored

* change patch file extension

* remove unnecessary utility functions + imports

* update MIT license

* change option to "patch_uncompressed_rom" instead of "compress_rom"

* compliance with new AutoWorld systems

* Kill only internal events, remove non-internal big poe event in code

* re-add the big poe event and handle it correctly

* remove extra method in Range option

* fix typo

* Starting items, starting with consumables option

* do not remove nonexistent item

* move set_shop_rules to after shop items are placed

* some cleanup

* add retries for song placement

* flagged Skull Mask and Mask of Truth as advancement items

* update OoT to use LogicMixin

* Fixed trying to assign starting items from the wrong players

* fixed song retry step

* improved option handling, comments, and starting item replacements

* DefaultOnToggle writes Yes or No to spoiler

* enable compression of output if Compress executable is present

* clean up compression

* check whether (de)compressor exists before running the process

* allow specification of rom path in host.yaml

* check if decompressed file already exists before decompressing again

* fix triforce hunt generation

* rename all the oot state functions with prefix

* OoT: mark triforce pieces as completion goal for triforce hunt

* added overworld and any-dungeon shuffle for dungeon items

* Hide most unshuffled locations and events from the list of locations in spoiler

* build oot option ranges with a generic function instead of defining each separately

* move oot output-type control to host.yaml instead of individual yamls

* implement dungeon song shuffle

* minor improvements to overworld dungeon item shuffle

* remove random ice trap names in shops, mostly to avoid maintaining a massive censor list

* always output patch file to folder, remove option to generate ROM in preparation for removal

* re-add the fix for infinite recursion due to not being light or dark world

* change AP-sendable to Ocarina of Time model, since the triforce piece has some extra code apparently

* oot: remove item_names and location_names

* oot: minor fixes

* oot: comment out ROM patching

* oot: only add CollectionState objects on creation if actually needed

* main entrance shuffle method and entrances-based rules

* fix entrances based rules

* disable master quest and big poe count options for client compatibility

* use get_player_name instead of get_player_names

* fix OptionList

* fix oot options for new option system

* new coop section in oot rom: expand player names to 16 bytes, write AP_PLAYER_NAME at end of PLAYER_NAMES

* fill AP player name in oot rom with 0 instead of 0xDF

* encode player name with ASCII for fixed-width

* revert oot player name array to 8 bytes per name

* remove Pierre location if fast scarecrow is on

* check player name length

* "free_scarecrow" not "fast_scarecrow"

* OoT locations now properly store the AP ID instead of the oot internal ID

* oot __version__ updates in lockstep with AP version

* pull in unmodified oot cosmetic files

* also grab JSONDump since it's needed apparently

* gather extra needed methods, modify imports

* delete cosmetics log, replace all instances of SettingsList with OOTWorld

* cosmetic options working, except for sound effects (due to ear-safe issues)

* SFX, Music, and Fanfare randomization reenabled

* move OoT data files into the worlds folder

* move Compress and Decompress into oot data folder

* Replace get_all_state with custom method to avoid the cache

* OoT ROM: increment item counter before setting incoming item/player values to 0, preventing desync issues

* set data_version to 0

* make Kokiri Sword shuffle off by default

* reenable "Random Choice" for various cosmetic options

* kill Ruto's Letter turnin if open fountain
also fix for shopsanity

* place Buy Goron/Zora Tunic first in shop shuffle

* make ice traps appear as other items instead of breaking generation

* managed to break ice traps on non-major-only

* only handle ice traps if they are on

* fix shopsanity for non-oot games, and write player name instead of player number

* light arrows hint uses player name instead of player number

* Reenable "skip child zelda" option

* fix entrances_based_rules

* fix ganondorf hint if starting with light arrows

* fix dungeonitem shuffle and shopsanity interaction

* remove has_all_of, has_any_of, count_of in BaseClasses, replace usage with has_all, has_any, has_group

* force local giveable item on ZL if skip_child_zelda and shuffle_song_items is any

* keep bosses and bombchu bowling chus out of data package

* revert workaround for infinite recursion and fix it properly

* fix shared shop id caches during patching process

* fix shop text box overflows, as much as possible

* add default oot host.yaml option

* add .apz5, .n64, .z64 to gitignore

* Properly document and name all (functioning) OOT options

* clean up some imports

* remove unnecessary files from oot's data

* fix typo in gitignore

* readd the Compress and Decompress utilities, since they are needed for generation

* cleanup of imports and some minor optimizations

* increase shop offset for item IDs to 0xCB

* remove shop item AP ids entirely

* prevent triforce pieces for other players from being received by yourself

* add "excluded" property to Location

* Hint system adapted and reenabled; hints still unseeded

* make hints deterministic with lists instead of sets

* do not allow hints to point to Light Arrows on non-vanilla bridge

* foreign locations hint as their full name in OoT rather than their region

* checkedLocations now stores hint names by player ID, so that the same location in different worlds can have hints associated

* consolidate versioning in Utils

* ice traps appear as major items rather than any progression item

* set prescription and claim check as defaults for adult trade item settings

* add oot options to playerSettings

* allow case-insensitive logic tricks in yaml

* fix oot shopsanity option formatting

* Write OoT override info even if local item, enabling local checks to show up immediately in the client

* implement CollectionState.can_live_dmg for oot glitched logic

* filter item names for invalid characters when patching shops

* make ice traps appear according to the settings of the world they are shuffled into, rather than the original world

* set hidden-spoiler items and locations with Shop items to events

* make GF carpenters, Gerudo Card, Malon, ZL, and Impa events if the relevant settings are enabled, preventing them from appearing in the client on game start

* Fix oot Glitched and No Logic generation

* fix indenting

* Greatly reduce displayed cosmetic options

* Change oot data version to 1

* add apz5 distribution to webhost

* print player name if an ALttP dungeon contains a good item for OoT world

* delete unneeded commented code

* remove OcarinaSongs import to satisfy lint
2021-09-02 14:35:05 +02:00
Fabian Dill
74c30ce09a Fill: remove/delay some LttP imports 2021-09-02 03:45:37 +02:00
Chris Wilson
859316353e Link /games to player-settings pages, add link to template file to player-settings, add markdown style formatting to /templates 2021-09-01 20:47:36 -04:00
Hussein Farran
63c9bea724 Remove total_items option. 2021-09-01 21:47:29 +00:00
espeon65536
c73b994305 use_cache argument to get_all_state 2021-09-01 19:21:03 +00:00
espeon65536
88451d4239 Skip caching get_all_state while setting rules
Since rules have not been set for later worlds, the cache believes the completion condition is freely available if it had been placed previously, which breaks beatable-only key placement.
2021-09-01 19:21:03 +00:00
CaitSith2
f74db254f6 fix typo in default value. 2021-09-01 09:18:43 -07:00
Fabian Dill
3cb0a22e17 LttP: crash on outdated dungeon_items use 2021-09-01 17:56:35 +02:00
Fabian Dill
ca3e01b15e LttPClient: prevent crash when trying to access sys.stdin 2021-09-01 17:56:19 +02:00
espeon65536
e9d1dcc46c set get_all_cache properly 2021-09-01 14:49:29 +00:00
Fabian Dill
7fd0f1a5bf Subnautica: implement create_item and therefore start_inventory 2021-09-01 16:46:44 +02:00
Fabian Dill
2d65fbf798 Merge pull request #58 from Ijwu/main
Risk of Rain 2 support
2021-09-01 11:30:41 +00:00
Fabian Dill
ac915d00fc Merge branch 'main' into main 2021-09-01 11:23:30 +00:00
espeon65536
fbb8d6b132 invalidate state cache so that reachable_regions are recalculated during TR key logic 2021-09-01 11:22:30 +00:00
espeon65536
fb0f70b3e3 make owg entrances in inverted 2021-09-01 11:22:30 +00:00
espeon65536
17929415ee actually set owg rules 2021-09-01 11:22:30 +00:00
espeon65536
631b6788c6 remove keys option for get_all_state, collect dungeon-local keys, and fix all uses of the state 2021-09-01 11:22:30 +00:00
espeon65536
7972aa6320 split building owg connections and setting the rules for those connections 2021-09-01 11:22:30 +00:00
espeon65536
138c884684 wipe reachable regions during TR key logic checks to ensure properly finding logic regions 2021-09-01 11:22:30 +00:00
Hussein Farran
f5ef98287a Add docstring to RiskOfRainWorld 2021-08-31 20:45:09 -04:00
Hussein Farran
5188b41ab0 Update RoR2 guide. 2021-08-31 20:42:16 -04:00
Hussein Farran
f83ba6e615 Add YAML options and update slot data.
Add TotalItems YAML option.
Add AllowLunarItems YAML option.
Send along TotalRevivals number with slot data.
2021-08-31 20:38:44 -04:00
Hussein Farran
cc2a72eb82 Locations/Events now None id 2021-08-31 20:21:52 -04:00
Chris Wilson
4fcce66505 Move game names and descriptions into AutoWorld, fix option value names on player-settings pages 2021-08-31 17:28:46 -04:00
Fabian Dill
66627d8a66 Options: match Toggle's get_option_name signature to Choice's 2021-08-31 22:52:14 +02:00
Fabian Dill
adfd68f83c Options: fix get_option_name 2021-08-31 22:14:18 +02:00
Fabian Dill
ddc619f2e7 WebHost: sample yamls: some formatting issues 2021-08-31 19:56:45 +02:00
Fabian Dill
ff2e57705e WebHost: sample yamls now render Range defaults correctly 2021-08-31 19:54:55 +02:00
Fabian Dill
a6a859b272 WebHost: fix sample yamls that have no options.
WebHost: hide hidden games from templates listing
2021-08-31 19:06:24 +02:00
Fabian Dill
88c5ebdd2f WebHost: add per-game yaml file downloads 2021-08-31 18:58:54 +02:00
Hussein Farran
3d578bcc98 Set force_auto_forfeit for RoR2 2021-08-31 10:08:19 -04:00
Hussein Farran
c3290af2bd Merge branch 'ArchipelagoMW:main' into main 2021-08-31 10:07:40 -04:00
Fabian Dill
01f1545b3e AutoWorld: add forced_auto_forfeit and set it for StS 2021-08-31 16:04:54 +02:00
Hussein Farran
fc8e849db5 Remove location id from Victory location. 2021-08-31 10:01:09 -04:00
Hussein Farran
9115e59f15 Add RoR2 to README 2021-08-31 08:37:01 -04:00
Hussein Farran
2f4b248a45 Add more information to the RoR2 docs. 2021-08-31 00:25:48 -04:00
Hussein Farran
2f28afb46e Add RoR2 Docs 2021-08-31 00:17:08 -04:00
Hussein Farran
e960d7b58c Merge branch 'main' of https://github.com/Ijwu/Archipelago into main 2021-08-30 21:43:18 -04:00
Fabian Dill
321569c542 Factorio: Fix random rocket-silo recipe unable to pick ingredients where recipe name != product name 2021-08-31 01:47:00 +02:00
Fabian Dill
df037c54ff LttP: fix dungeon original item rule calling
Found by Espeon
2021-08-30 23:52:40 +02:00
Fabian Dill
d859cecffb Options: use isinstance instead of type for Choice comparison 2021-08-30 23:07:19 +02:00
Fabian Dill
fd6e009c4b Fill: fix placing non_local + non advancement items 2021-08-30 22:20:44 +02:00
Fabian Dill
4520051ec9 Slay the Spire: add to playerSettings.yaml 2021-08-30 22:19:48 +02:00
Fabian Dill
b90b73859a Slay the Spire: add to playerSettings.yaml 2021-08-30 20:07:25 +02:00
Fabian Dill
6c357b61cc LttP: re-remove LttP import in BaseClasses 2021-08-30 19:11:12 +02:00
Fabian Dill
12957db90f Options: implement __eq__ assert for possible checks 2021-08-30 19:08:10 +02:00
CaitSith2
3c74f561d5 LttP: Fix smallkey_shuffle in menu display
use smallkey_shuffle.option_universal from worlds.alttp.Options rather than "universal" for compare operations on universal checking.
2021-08-30 09:59:20 -07:00
Fabian Dill
cc70a6fa26 LttP: make shuffle names consistent 2021-08-30 18:00:39 +02:00
Fabian Dill
1c42564d90 LttP: remove leftover location binding 2021-08-30 16:47:34 +02:00
Fabian Dill
e76c870c09 Unittest: fix TestInvertedBombRules 2021-08-30 16:38:21 +02:00
Fabian Dill
5daadcb2d5 LttP: implement new dungeon_items handling
LttP: move glitch_boots to new options system
WebHost: options.yaml no longer lists aliases
General: remove region.can_fill, it was only used as a hack to make dungeon-specific items to work
2021-08-30 16:31:56 +02:00
espeon65536
a124a7a82a Create event Blaze Spawner containing Blaze Rods, preventing scenarios where the only progression in a sphere is to gain access to a fortress, which crashes playthrough generation 2021-08-30 08:15:21 +00:00
espeon65536
a65bf60cea add structure compasses to itempool in a fixed order 2021-08-30 08:15:21 +00:00
Fabian Dill
3fa28a3fdb LttP: fix import mistake 2021-08-30 01:18:30 +02:00
Fabian Dill
baa7992a7a AutoWorld: add post_fill
LttP: Move ShopSlotFill to post_fill
2021-08-30 01:16:04 +02:00
Fabian Dill
7ba4bfc0d5 Generate: make sure no None items make it into multidata. 2021-08-30 00:52:57 +02:00
Fabian Dill
11fedef2f5 Generate: turn off interpret_on_off for newstyle options 2021-08-29 20:21:49 +02:00
Hussein Farran
944347a2b3 Risk of Rain 2 implementation 2021-08-29 14:02:02 -04:00
Fabian Dill
8c72b0a6c4 AutoYAML: proper multi-line comments 2021-08-29 18:13:38 +02:00
Fabian Dill
5d62d4e063 Clients: logging fixes 2021-08-29 17:38:35 +02:00
Adam Ziegler
9b05537a0e fix argument, logger name 2021-08-29 15:31:02 +00:00
Adam Ziegler
fd0a87626e list connected SNESes if more than one; allow connecting to specific one 2021-08-29 15:31:02 +00:00
KonoTyran
9402d82405 Slay the Spire (#54)
Add Slay the Spire
2021-08-29 17:30:44 +02:00
Fabian Dill
da6674760c LttP: convert MultiWorld.dungeons to dict for faster lookup 2021-08-29 16:02:28 +02:00
Fabian Dill
ee03371dd0 LttP: make heartbeep off functional again 2021-08-29 15:43:16 +02:00
Fabian Dill
a975c8fd00 LttP: Format non-native Location hints better 2021-08-28 23:18:45 +02:00
Fabian Dill
60840da740 LttP: fix dungeon local items to be local to their own dungeon 2021-08-28 22:58:23 +02:00
Fabian Dill
de567cc701 LttP: Move more functionality into ALttPItem from Item
LttP: More efficiently build !hint entrance info
LttP: More efficiently check for and build Big Bomb Shop playthrough path
2021-08-28 12:56:52 +02:00
Fabian Dill
de4775b0c8 LttP: Move difficulties and er seed sharing to generate_early 2021-08-28 00:26:02 +02:00
Fabian Dill
104cc0ea83 document World.hidden 2021-08-27 20:46:33 +02:00
CaitSith2
5bb8de500a Fix issue with syncing tech tree post-forfeit. 2021-08-27 10:41:29 -07:00
Fabian Dill
21255b3b46 LttP: Rename Shop Slot 1, 2, 3 to Shop Slot Left, Center, Right
General: Move generic IDs from LttP to new Generic World
Generate: ensure thread errors are collected before data from their completion may be referenced in playthrough/spoiler
2021-08-27 14:52:33 +02:00
espeon65536
e8da9924c6 allow collecting silver bow if noglitches or swordless, even if the limit is under 2 2021-08-27 07:44:05 +00:00
espeon65536
96b38aba04 mark TRBK as impassable during initial pass for TR key logic, so that crystaroller can be marked as front-locked 2021-08-27 07:44:05 +00:00
espeon65536
b8b51965d2 skip first sweep_for_events in playthrough computation, so keys are no longer treated as special 2021-08-27 07:44:05 +00:00
espeon65536
be46d128bc do not double-collect keys during playthrough computation, since they are progression items now 2021-08-27 07:44:05 +00:00
Fabian Dill
c05f1ed24f to be or not to be 2021-08-26 18:25:15 +02:00
Fabian Dill
99775ec1bd Generate: require that player names be unique again 2021-08-26 17:22:55 +02:00
Fabian Dill
f4f043ac87 MultiServer: categorize methods 2021-08-26 16:19:37 +02:00
Fabian Dill
acbca78e2d update Prompt Toolkit 2021-08-24 09:52:45 +02:00
Fabian Dill
30ac7baa2c FactorioClient: Batch-Send RCON commands when receiving catch-up locations and multiple items. 2021-08-24 09:52:12 +02:00
espeon65536
21a5170337 remove double negative in apmc file check 2021-08-24 04:02:28 +00:00
espeon65536
3a5a6a096b add .apmc and Forge server to gitignore 2021-08-24 04:02:28 +00:00
espeon65536
578ae70150 update playerSettings.yaml 2021-08-24 04:02:28 +00:00
espeon65536
57282e76a4 add send_defeated_mobs as option 2021-08-24 04:02:28 +00:00
espeon65536
7aaa652ef5 Give docstrings and display names to Minecraft options 2021-08-24 04:02:28 +00:00
espeon65536
81da0d2ba4 Minecraft client: skip deleting and recopying an apmc file that is already in APData 2021-08-24 04:02:28 +00:00
espeon65536
ce6cdcaf92 Minecraft client: prevent options.yaml/host.yaml contamination from non-install directories 2021-08-24 04:02:28 +00:00
espeon65536
4730a928b5 Minecraft client: fix NoneType-related error if run without apmc file 2021-08-24 04:02:28 +00:00
Chris Wilson
4c0f0a16c9 Updates to WebHost
- Support displayname option for Options module
- Improvements to landing page
- Added multi-language capable FAQ page
- Removed weighted-settings page
- Removed references to weighted-settings page
2021-08-22 20:01:58 -04:00
Fabian Dill
b07fc80f3f AutoWorld: if any world data_version is set to 0, set it for the main datapackage 2021-08-22 04:22:34 +02:00
Fabian Dill
6a3d1fcaf4 LttP & Factorio: fix item state removal for progressive items. 2021-08-21 06:55:08 +02:00
Fabian Dill
4aeb3cd3dc WebHost: allow /tutorial and /tutorial/ 2021-08-20 22:41:23 +02:00
Fabian Dill
6dc2000638 CommonClient.py: move in gui_enabled 2021-08-20 22:31:17 +02:00
Fabian Dill
72610d8c2f Core: log world ID ranges 2021-08-16 18:40:26 +02:00
Fabian Dill
0f55fa4f45 FactorioClient: allow setting a folder and find the executable in it, instead of trying to run a folder. 2021-08-15 13:46:58 +02:00
Fabian Dill
aec39c919c Minecraft: add missing minecraft defaults 2021-08-15 02:32:36 +02:00
Kono Tyran
a0849f9416 fixed error if destination folder did not exist already. 2021-08-15 00:05:54 +00:00
Kono Tyran
0668f94461 - change minecraft clients icon. 2021-08-14 23:05:15 +00:00
Chris Wilson
953ccc55d9 Update factorio icons to make progression items more distinct 2021-08-14 17:47:32 -04:00
espeon65536
fbaa8226c4 Minecraft tracker: only lookup recognized item ids 2021-08-14 19:58:23 +00:00
Fabian Dill
8abfd14569 LttP: fix missing music 2021-08-14 01:00:36 +02:00
Fabian Dill
f2f4d6a133 remove leftover debug log 2021-08-14 00:51:35 +02:00
Fabian Dill
3ed7092af5 LttP: make sure Hyrule Castle Small Key in Standard + keyshuffle is reachable in first sphere of any such players 2021-08-14 00:51:35 +02:00
Fabian Dill
9d6fa855d8 Multidata: fix accidental format change 2021-08-12 04:23:07 +02:00
Fabian Dill
8c7404edf9 Spoiler: fix built-in variable name shadowing 2021-08-11 12:45:03 +02:00
espeon65536
3f6a9e5dc7 MC client: only log removal of .apmc files 2021-08-10 18:42:48 +00:00
espeon65536
9e1748bf67 check_eula function 2021-08-10 18:42:48 +00:00
espeon65536
527a9b49e2 change to executable's working directory to find forge directory 2021-08-10 18:42:48 +00:00
espeon65536
b187223162 streamline function calls 2021-08-10 18:42:48 +00:00
espeon65536
2c5e99efed make apmc_file argument optional 2021-08-10 18:42:48 +00:00
espeon65536
fa8531022d reorganize Minecraft client internal structure, add missing error handling in update_mod 2021-08-10 18:42:48 +00:00
espeon65536
8d4be10fd7 Minecraft client first pass 2021-08-10 18:42:48 +00:00
Kono Tyran
285b9e12eb - Add Minecraft to inno_setup_38.iss, this will download java and forge and install them. 2021-08-10 18:42:25 +00:00
Fabian Dill
53fcb86174 Spoiler: remove Progressive from old system to prevent crashes when no LttP is present 2021-08-10 20:40:44 +02:00
Fabian Dill
a532ceeb0a AutoWorld: Should no longer need to overwrite collect, collect_item should be used instead
AutoWorld: Now correctly automatically applies State.remove if collect_item is also correct
LttP: Make keys advancement items

This feels like it improved generation chance. Might not be the case.
2021-08-10 09:47:28 +02:00
Fabian Dill
9ec0680ce5 LttP: move game specific fill to new AutoWorld fill_hook 2021-08-10 09:03:44 +02:00
Fabian Dill
299036ecca LttP: move some LttP specific things more towards locations where they belong. 2021-08-10 08:00:53 +02:00
Fabian Dill
4bfeb77a3a CommonClient: fix /missing
found by lordlou
2021-08-10 04:38:29 +02:00
Fabian Dill
ab7a5b07eb YAML: Make player pick a game, error out if step is skipped. 2021-08-09 23:00:28 +02:00
Fabian Dill
50ad661796 Put in support for old Progressive item key
I will probably regret this.
2021-08-09 10:07:25 +02:00
Fabian Dill
d3e71ecb9b Install all modules for unittests.yml 2021-08-09 07:29:21 +00:00
Fabian Dill
ba3bb201cd Multiple: Followed a rabbit hole of moving LttP Rom generation to AutoWorld
Generator: Re-allow names with spaces (and see what breaks)
Generator: Removed teams (Note that teams are intended to move from a generation step feature to a server runtime feature, allowing dynamic creation of an already generated MW)
LttP: All Rom Options are now on the new system
LttP: palette option "random" is now called "good"
LttP: Roms are now created as part of the general output file creation step
LttP: disable Music is now Music, removing potential double negatives
LttP & Factorio: Progressive option random is now grouped_random
LttP: Enemy damage option random is now Enemy damage: chaos
2021-08-09 09:15:41 +02:00
Fabian Dill
01d88c362a AutoWorld: Add "stage" methods and implement LttP Dungeon fill as an example. 2021-08-09 06:50:11 +02:00
Fabian Dill
95350a1fa9 Fill: Cache get_all_State 2021-08-09 06:33:26 +02:00
Fabian Dill
cc458ca5b1 LttP: Remove no longer reachable code 2021-08-09 06:19:01 +02:00
Fabian Dill
f19878fcb8 LttP: Remove calling the player Idiot 2021-08-09 03:51:33 +02:00
black-sliver
eb8e8691e9 Factorio: avoid ores when spawning silo
and minor code clean-up
2021-08-08 00:40:09 +00:00
Fabian Dill
0423c22d7f DataPackage: bring back compatibility layer for datapackage - for now. Mark removal version. 2021-08-07 09:18:42 +02:00
Fabian Dill
3441c390bd Setup: Fix crash if ROM was present. 2021-08-07 08:05:01 +02:00
Fabian Dill
a0fb9bc4ab Setup: Skip LttP Rom Selection if the Rom is not needed. 2021-08-07 06:57:33 +02:00
Fabian Dill
a7bb6f6a95 CommonClient: make entrances blue in console 2021-08-07 05:40:18 +02:00
Fabian Dill
f1bef73757 Setup: Fix subprogram paths 2021-08-07 03:16:30 +02:00
Fabian Dill
4598dd1a0f Factorio: syntax~ 2021-08-07 02:57:47 +02:00
espeon65536
0241d6f443 fix minecraft tests for egg shards 2021-08-07 00:44:57 +00:00
espeon65536
72acb5509a Minecraft: dragon egg shards 2021-08-07 00:44:57 +00:00
espeon65536
b43e99fa20 better check for completion in MC webtracker 2021-08-07 00:44:57 +00:00
espeon65536
b5083ddb1b update playerSettings: new minecraft bee_traps format 2021-08-07 00:44:57 +00:00
espeon65536
f62e8b7be9 Minecraft: write server and port to apmc on download 2021-08-07 00:44:57 +00:00
espeon65536
f655dc0dbc Minecraft tracker: formatting fix 2021-08-07 00:44:57 +00:00
espeon65536
95e0fa2672 Minecraft tracker: add progressive resource crafting 2021-08-07 00:44:57 +00:00
espeon65536
4b7c8f757e Minecraft: increment data version and client version 2021-08-07 00:44:57 +00:00
espeon65536
381e9c744a fix tests for progressive resource crafting 2021-08-07 00:44:57 +00:00
espeon65536
9aa4bb3f4b fix tests for bee traps 2021-08-07 00:44:57 +00:00
espeon65536
63617edfef Minecraft: merge ingot crafting and resource blocks into Progressive Resource Crafting 2021-08-07 00:44:57 +00:00
espeon65536
72de0450e0 Minecraft: refactored bee trap to percentage of junk item pool 2021-08-07 00:44:57 +00:00
espeon65536
306bdd322f Minecraft tracker: fix incorrect bold css 2021-08-07 00:44:57 +00:00
espeon65536
231613cb3b Minecraft tracker: automated location tracking and dropdown tabs 2021-08-07 00:44:57 +00:00
espeon65536
2af5739592 Minecraft tracker v2
group advancements by category
update font to Minecraft font
always display pearl/scrap counter
2021-08-07 00:44:57 +00:00
espeon65536
b38f7c8f2a Minecraft web tracker, built as a mix of the LttP tracker and the generic tracker 2021-08-07 00:44:57 +00:00
espeon65536
e3a81c1bed Minecraft: randomly determine junk items filling the itempool 2021-08-07 00:44:57 +00:00
Fabian Dill
cd8452d839 Factorio: sync already cleared locations to local world 2021-08-07 01:01:56 +02:00
Fabian Dill
4b38cb4c2e Setup: various small adjustments and fixes 2021-08-06 19:33:17 +02:00
Fabian Dill
eda8c6f263 add the forgotten progressive persoanl roboport equipment 2021-08-06 08:14:16 +02:00
Hussein Farran
a8cf67c94d Fix type annotation for a key under GameData 2021-08-04 22:04:53 +00:00
Hussein Farran
928b341fb3 Make data package contents more descriptive 2021-08-04 22:04:53 +00:00
Hussein Farran
6e51b1d50c Change BouncePacket and BouncedPacket docs to add key for extra data. 2021-08-04 19:54:06 +00:00
Fabian Dill
78aaa65b45 explain !hint a bit better 2021-08-04 18:38:49 +02:00
Fabian Dill
3627d8f1ae DataPackage: remove legacy format 2021-08-04 16:01:08 +02:00
Fabian Dill
1e64b817f6 CommonClient: implement new DataPackage format 2021-08-04 15:54:42 +02:00
CaitSith2
37e999652d Return of the warning for the backwards compatibility layer.
Mainly, make sure the backwards compatible /sc game.print works 100% of the time, instead of being silent, since commands that disable achievemets need to be executed twice at least once, within a certain period of time.
2021-08-03 23:24:32 -07:00
Fabian Dill
9408557f03 Factorio: add Traps 2021-08-04 05:40:51 +02:00
Fabian Dill
16701249b4 Minecraft: fix combat difficulty rules 2021-08-03 19:21:59 +02:00
Fabian Dill
3c1ac134f2 Options: add a way to get all option names (for selection menus or such) 2021-08-03 19:09:37 +02:00
Fabian Dill
230d9d993e clean up some spoiler display names 2021-08-03 19:03:41 +02:00
CaitSith2
d1c83ffc09 Make /factorio {factorio_command} no longer silent, even if /sc is used. 2021-08-03 09:48:07 -07:00
CaitSith2
a52f991543 Fix backwards compatibility check for cases where AP mod is NOT loaded last. 2021-08-03 08:51:12 -07:00
CaitSith2
dfc56a3272 Implement random progressive techs. 2021-08-02 19:33:14 -07:00
Fabian Dill
41037ce599 remove debug prints from a3924ed40a 2021-08-03 03:55:02 +02:00
CaitSith2
a3924ed40a Fix progressive items toggle 2021-08-02 18:50:56 -07:00
Fabian Dill
361bd4e5f6 Factorio: fix progressive flamethrower ordering 2021-08-03 01:14:20 +02:00
Fabian Dill
8cc245ac11 Technologies.py: add some missing types 2021-08-02 19:27:43 +02:00
Fabian Dill
2d8a6e84c1 Factorio: generalize merging of progressive technologies
use it for:
train network + braking force
flamethrower + refined flammables
inserters + inserter capacity
2021-08-02 19:12:42 +02:00
Fabian Dill
d2add54cd6 Factorio: implement decent option display names for Spoiler 2021-08-02 04:57:57 +02:00
Fabian Dill
40044ac5a6 Generate: wait for user close 2021-08-02 01:36:04 +02:00
Fabian Dill
bb15d0636e Network: implement Bounce and Bounced 2021-08-02 01:35:24 +02:00
Fabian Dill
2cc7d8395b MultiServer: fix loading old savegames 2021-08-01 22:47:56 +02:00
Fabian Dill
2f2e039356 MultiServer: Limit !hint to a single new result if costs are on. 2021-08-01 17:09:10 +02:00
Fabian Dill
0cd388ca66 MultiServer: seeded !hint selected 2021-08-01 17:02:38 +02:00
Fabian Dill
7ef1fe81f6 MultiServer: move !hint point counting to end of message 2021-08-01 16:48:25 +02:00
Fabian Dill
774610de7b Factorio: add progressive turret 2021-08-01 06:15:50 +02:00
Fabian Dill
f6c85e17d5 roll braking force into progressive train network 2021-08-01 02:51:20 +02:00
Fabian Dill
8142306562 Factorio: move adjust_energy over to "flop_random", giving half and half in each random direction, but no particular average. 2021-07-31 20:20:59 +02:00
Fabian Dill
2d84245103 Factorio: fix adjust_energy to hit special cases with implied energy cost 2021-07-31 20:19:05 +02:00
Fabian Dill
1d954b192c Factorio: display required rocket-silo ingredients ahead of time. 2021-07-31 19:45:17 +02:00
black-sliver
db0604f585 Factorio: add silo 'spawn' option 2021-07-31 16:27:53 +00:00
black-sliver
08beb5fbe6 Factorio: option to randomize silo recipe 2021-07-31 16:27:53 +00:00
alwaysintreble
7df06b87a5 reclarified some text (#38)
* reclarified some setup text
2021-07-31 16:02:38 +02:00
Fabian Dill
abf4e82737 Move Factorio data from /data/factorio to /worlds/factorio/data, to contain it in its world folder 2021-07-31 15:13:55 +02:00
Fabian Dill
7f8617d639 move ctx.ui to CommonClient.py 2021-07-31 01:53:06 +02:00
Fabian Dill
f5c62a82ac some post unification cleanup. 2021-07-31 01:40:27 +02:00
Fabian Dill
66514ec607 unify clients and setup 2021-07-31 00:03:48 +02:00
Fabian Dill
096e682b18 FactorioClient: implement JSONPrint in the client 2021-07-30 20:18:03 +02:00
Fabian Dill
e098b3c504 AutoWorld: automate item_names and location_names 2021-07-29 20:27:41 +02:00
Fabian Dill
4dde466364 MultiServer: print which game is being played. 2021-07-29 16:21:11 +02:00
Fabian Dill
6d6fc52481 Factorio: implement backwards compatible printing 2021-07-29 15:26:13 +02:00
Fabian Dill
eaae4af832 Factorio: fix reconnect 2021-07-29 15:25:45 +02:00
Fabian Dill
7f5afddb38 Docs: update network graph 2021-07-29 15:23:44 +02:00
Fabian Dill
36a981aaa2 Freeze: don't discard docstrings as Archipelago makes use of them during runtime. 2021-07-28 13:31:27 +02:00
Hussein Farran
fdcf093be0 Update README.md 2021-07-28 11:29:51 +00:00
black-sliver
1bd55b4572 ModuleUpdater: add -f and -y switches
-f: force update
-y: skip question to run pip
2021-07-27 16:02:09 +00:00
black-sliver
eb0e5b7438 MultiServer: don't extract .zip 2021-07-27 16:01:55 +00:00
Fabian Dill
884dece54c Factorio: move prints from /sc (silent command) to /ap-print, to prevent two warnings getting printed by Factorio 2021-07-27 14:59:24 +02:00
Fabian Dill
3759f4c644 FactorioClient: add /resync to trigger resending/reauth 2021-07-27 14:59:24 +02:00
Fabian Dill
f232f74246 Version: 0.1.6 start 2021-07-27 14:59:24 +02:00
Chris Wilson
a9ecab35d8 Add Subnautica to games list on WebHost 2021-07-26 17:21:00 -04:00
Chris Wilson
e1e25d0eae Add range options to player-settings pages 2021-07-25 19:04:08 -04:00
Chris Wilson
f45c042351 Include range options in generated JSON files for player-settings 2021-07-25 18:18:15 -04:00
Chris Wilson
f15bb9dbd7 Fix player-settings not defaulting the select options to their proper values. Also fix tab title. 2021-07-25 18:07:03 -04:00
Chris Wilson
610871c61b Template gameName into player-settings as a data attribute to avoid potential security risks. 2021-07-25 15:49:51 -04:00
Daivuk
35b9e4768a Adjust radiation rules to match code 2021-07-25 17:58:53 +00:00
256 changed files with 85783 additions and 3783 deletions

View File

@@ -20,7 +20,7 @@ jobs:
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install flake8 pytest pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi python ModuleUpdate.py --yes --force
- name: Unittests - name: Unittests
run: | run: |
pytest test pytest test

6
.gitignore vendored
View File

@@ -4,9 +4,13 @@
*_Spoiler.txt *_Spoiler.txt
*.bmbp *.bmbp
*.apbp *.apbp
*.apmc
*.apz5
*.pyc *.pyc
*.pyd *.pyd
*.sfc *.sfc
*.z64
*.n64
*.wixobj *.wixobj
*.lck *.lck
*.db3 *.db3
@@ -16,7 +20,6 @@
*.apsave *.apsave
build build
/build_factorio/
bundle/components.wxs bundle/components.wxs
dist dist
README.html README.html
@@ -38,6 +41,7 @@ success.txt
output/ output/
Output Logs/ Output Logs/
/factorio/ /factorio/
/Minecraft Forge Server/
/WebHostLib/static/generated /WebHostLib/static/generated
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files

View File

@@ -6,14 +6,14 @@ import logging
import json import json
import functools import functools
from collections import OrderedDict, Counter, deque from collections import OrderedDict, Counter, deque
from typing import List, Dict, Optional, Set, Iterable, Union, Any from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple
import secrets import secrets
import random import random
class MultiWorld(): class MultiWorld():
debug_types = False debug_types = False
player_names: Dict[int, List[str]] player_name: Dict[int, str]
_region_cache: Dict[int, Dict[str, Region]] _region_cache: Dict[int, Dict[str, Region]]
difficulty_requirements: dict difficulty_requirements: dict
required_medallions: dict required_medallions: dict
@@ -36,10 +36,9 @@ class MultiWorld():
def __init__(self, players: int): def __init__(self, players: int):
self.random = random.Random() # world-local random state is saved for multiple generations running concurrently self.random = random.Random() # world-local random state is saved for multiple generations running concurrently
self.players = players self.players = players
self.teams = 1
self.glitch_triforce = False self.glitch_triforce = False
self.algorithm = 'balanced' self.algorithm = 'balanced'
self.dungeons = [] self.dungeons: Dict[Tuple[str, int], Dungeon] = {}
self.regions = [] self.regions = []
self.shops = [] self.shops = []
self.itempool = [] self.itempool = []
@@ -83,11 +82,9 @@ class MultiWorld():
set_player_attr('item_functionality', 'normal') set_player_attr('item_functionality', 'normal')
set_player_attr('timer', False) set_player_attr('timer', False)
set_player_attr('goal', 'ganon') set_player_attr('goal', 'ganon')
set_player_attr('progressive', 'on')
set_player_attr('accessibility', 'items') set_player_attr('accessibility', 'items')
set_player_attr('retro', False) set_player_attr('retro', False)
set_player_attr('hints', True) set_player_attr('hints', True)
set_player_attr('player_names', [])
set_player_attr('required_medallions', ['Ether', 'Quake']) set_player_attr('required_medallions', ['Ether', 'Quake'])
set_player_attr('swamp_patch_required', False) set_player_attr('swamp_patch_required', False)
set_player_attr('powder_patch_required', False) set_player_attr('powder_patch_required', False)
@@ -98,10 +95,6 @@ class MultiWorld():
set_player_attr('can_access_trock_big_chest', None) set_player_attr('can_access_trock_big_chest', None)
set_player_attr('can_access_trock_middle', None) set_player_attr('can_access_trock_middle', None)
set_player_attr('fix_fake_world', True) set_player_attr('fix_fake_world', True)
set_player_attr('mapshuffle', False)
set_player_attr('compassshuffle', False)
set_player_attr('keyshuffle', False)
set_player_attr('bigkeyshuffle', False)
set_player_attr('difficulty_requirements', None) set_player_attr('difficulty_requirements', None)
set_player_attr('boss_shuffle', 'none') set_player_attr('boss_shuffle', 'none')
set_player_attr('enemy_shuffle', False) set_player_attr('enemy_shuffle', False)
@@ -121,7 +114,6 @@ class MultiWorld():
set_player_attr('blue_clock_time', 2) set_player_attr('blue_clock_time', 2)
set_player_attr('green_clock_time', 4) set_player_attr('green_clock_time', 4)
set_player_attr('can_take_damage', True) set_player_attr('can_take_damage', True)
set_player_attr('glitch_boots', True)
set_player_attr('progression_balancing', True) set_player_attr('progression_balancing', True)
set_player_attr('local_items', set()) set_player_attr('local_items', set())
set_player_attr('non_local_items', set()) set_player_attr('non_local_items', set())
@@ -161,11 +153,15 @@ class MultiWorld():
def get_game_players(self, game_name: str): def get_game_players(self, game_name: str):
return tuple(player for player in self.player_ids if self.game[player] == game_name) return tuple(player for player in self.player_ids if self.game[player] == game_name)
def get_name_string_for_object(self, obj) -> str: @functools.lru_cache()
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_names(obj.player)})' def get_game_worlds(self, game_name: str):
return tuple(world for player, world in self.worlds.items() if self.game[player] == game_name)
def get_player_names(self, player: int) -> str: def get_name_string_for_object(self, obj) -> str:
return ", ".join(self.player_names[player]) return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_name(obj.player)})'
def get_player_name(self, player: int) -> str:
return self.player_name[player]
def initialize_regions(self, regions=None): def initialize_regions(self, regions=None):
for region in regions if regions else self.regions: for region in regions if regions else self.regions:
@@ -174,7 +170,7 @@ class MultiWorld():
@functools.cached_property @functools.cached_property
def world_name_lookup(self): def world_name_lookup(self):
return {self.player_names[player_id][0]: player_id for player_id in self.player_ids} return {self.player_name[player_id]: player_id for player_id in self.player_ids}
def _recache(self): def _recache(self):
"""Rebuild world cache""" """Rebuild world cache"""
@@ -197,7 +193,6 @@ class MultiWorld():
self._recache() self._recache()
return self._region_cache[player][regionname] return self._region_cache[player][regionname]
def get_entrance(self, entrance: str, player: int) -> Entrance: def get_entrance(self, entrance: str, player: int) -> Entrance:
try: try:
return self._entrance_cache[entrance, player] return self._entrance_cache[entrance, player]
@@ -205,7 +200,6 @@ class MultiWorld():
self._recache() self._recache()
return self._entrance_cache[entrance, player] return self._entrance_cache[entrance, player]
def get_location(self, location: str, player: int) -> Location: def get_location(self, location: str, player: int) -> Location:
try: try:
return self._location_cache[location, player] return self._location_cache[location, player]
@@ -213,39 +207,30 @@ class MultiWorld():
self._recache() self._recache()
return self._location_cache[location, player] return self._location_cache[location, player]
def get_dungeon(self, dungeonname: str, player: int) -> Dungeon: def get_dungeon(self, dungeonname: str, player: int) -> Dungeon:
for dungeon in self.dungeons: try:
if dungeon.name == dungeonname and dungeon.player == player: return self.dungeons[dungeonname, player]
return dungeon except KeyError as e:
raise KeyError('No such dungeon %s for player %d' % (dungeonname, player)) raise KeyError('No such dungeon %s for player %d' % (dungeonname, player)) from e
def get_all_state(self, use_cache: bool) -> CollectionState:
cached = getattr(self, "_all_state", None)
if use_cache and cached:
return cached.copy()
def get_all_state(self, keys=False) -> CollectionState:
ret = CollectionState(self) ret = CollectionState(self)
for item in self.itempool: for item in self.itempool:
self.worlds[item.player].collect(ret, item) self.worlds[item.player].collect(ret, item)
from worlds.alttp.Dungeons import get_dungeon_item_pool
if keys: for item in get_dungeon_item_pool(self):
for p in self.get_game_players("A Link to the Past"): subworld = self.worlds[item.player]
world = self.worlds[p] if item.name in subworld.dungeon_local_item_names:
from worlds.alttp.Items import ItemFactory subworld.collect(ret, item)
for item in ItemFactory(
['Small Key (Hyrule Castle)', 'Big Key (Eastern Palace)', 'Big Key (Desert Palace)',
'Small Key (Desert Palace)', 'Big Key (Tower of Hera)', 'Small Key (Tower of Hera)',
'Small Key (Agahnims Tower)', 'Small Key (Agahnims Tower)',
'Big Key (Palace of Darkness)'] + ['Small Key (Palace of Darkness)'] * 6 + [
'Big Key (Thieves Town)', 'Small Key (Thieves Town)', 'Big Key (Skull Woods)'] + [
'Small Key (Skull Woods)'] * 3 + ['Big Key (Swamp Palace)',
'Small Key (Swamp Palace)', 'Big Key (Ice Palace)'] + [
'Small Key (Ice Palace)'] * 2 + ['Big Key (Misery Mire)', 'Big Key (Turtle Rock)',
'Big Key (Ganons Tower)'] + [
'Small Key (Misery Mire)'] * 3 + ['Small Key (Turtle Rock)'] * 4 + [
'Small Key (Ganons Tower)'] * 4,
p):
world.collect(ret, item)
ret.sweep_for_events() ret.sweep_for_events()
if use_cache:
self._all_state = ret
return ret return ret
def get_items(self) -> list: def get_items(self) -> list:
@@ -264,8 +249,6 @@ class MultiWorld():
def push_precollected(self, item: Item): def push_precollected(self, item: Item):
item.world = self item.world = self
if (item.smallkey and self.keyshuffle[item.player]) or (item.bigkey and self.bigkeyshuffle[item.player]):
item.advancement = True
self.precollected_items.append(item) self.precollected_items.append(item)
self.state.collect(item, True) self.state.collect(item, True)
@@ -540,9 +523,7 @@ class CollectionState(object):
locations = {location for location in locations if location.event} locations = {location for location in locations if location.event}
while new_locations: while new_locations:
reachable_events = {location for location in locations if reachable_events = {location for location in locations if
(not key_only or (not key_only or getattr(location.item, "locked_dungeon_item", False))
(not self.world.keyshuffle[location.item.player] and location.item.smallkey)
or (not self.world.bigkeyshuffle[location.item.player] and location.item.bigkey))
and location.can_reach(self)} and location.can_reach(self)}
new_locations = reachable_events - self.events new_locations = reachable_events - self.events
for event in new_locations: for event in new_locations:
@@ -572,13 +553,6 @@ class CollectionState(object):
found += self.prog_items[item_name, player] found += self.prog_items[item_name, player]
return found return found
def has_key(self, item, player, count: int = 1):
if self.world.logic[player] == 'nologic':
return True
if self.world.keyshuffle[player] == "universal":
return self.can_buy_unlimited('Small Key (Universal)', player)
return self.prog_items[item, player] >= count
def can_buy_unlimited(self, item: str, player: int) -> bool: def can_buy_unlimited(self, item: str, player: int) -> bool:
return any(shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(self) for return any(shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(self) for
shop in self.world.shops) shop in self.world.shops)
@@ -759,53 +733,12 @@ class CollectionState(object):
return changed return changed
def remove(self, item): def remove(self, item):
if item.advancement: changed = self.world.worlds[item.player].remove(self, item)
to_remove = item.name if changed:
if item.game == "A Link to the Past" and to_remove.startswith('Progressive '): # invalidate caches, nothing can be trusted anymore now
if 'Sword' in to_remove: self.reachable_regions[item.player] = set()
if self.has('Golden Sword', item.player): self.blocked_connections[item.player] = set()
to_remove = 'Golden Sword' self.stale[item.player] = True
elif self.has('Tempered Sword', item.player):
to_remove = 'Tempered Sword'
elif self.has('Master Sword', item.player):
to_remove = 'Master Sword'
elif self.has('Fighter Sword', item.player):
to_remove = 'Fighter Sword'
else:
to_remove = None
elif 'Glove' in item.name:
if self.has('Titans Mitts', item.player):
to_remove = 'Titans Mitts'
elif self.has('Power Glove', item.player):
to_remove = 'Power Glove'
else:
to_remove = None
elif 'Shield' in item.name:
if self.has('Mirror Shield', item.player):
to_remove = 'Mirror Shield'
elif self.has('Red Shield', item.player):
to_remove = 'Red Shield'
elif self.has('Blue Shield', item.player):
to_remove = 'Blue Shield'
else:
to_remove = None
elif 'Bow' in item.name:
if self.has('Silver Bow', item.player):
to_remove = 'Silver Bow'
elif self.has('Bow', item.player):
to_remove = 'Bow'
else:
to_remove = None
if to_remove:
self.prog_items[to_remove, item.player] -= 1
if self.prog_items[to_remove, item.player] < 1:
del (self.prog_items[to_remove, item.player])
# invalidate caches, nothing can be trusted anymore now
self.reachable_regions[item.player] = set()
self.blocked_connections[item.player] = set()
self.stale[item.player] = True
@unique @unique
class RegionType(int, Enum): class RegionType(int, Enum):
@@ -851,14 +784,6 @@ class Region(object):
return True return True
return False return False
def can_fill(self, item: Item):
inside_dungeon_item = item.locked_dungeon_item
sewer_hack = self.world.mode[item.player] == 'standard' and item.name == 'Small Key (Hyrule Castle)'
if sewer_hack or inside_dungeon_item:
return self.dungeon and self.dungeon.is_dungeon_item(item) and item.player == self.player
return True
def __repr__(self): def __repr__(self):
return self.__str__() return self.__str__()
@@ -900,8 +825,8 @@ class Entrance(object):
world = self.parent_region.world if self.parent_region else None world = self.parent_region.world if self.parent_region else None
return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})' return world.get_name_string_for_object(self) if world else f'{self.name} (Player {self.player})'
class Dungeon(object):
class Dungeon(object):
def __init__(self, name: str, regions, big_key, small_keys, dungeon_items, player: int): def __init__(self, name: str, regions, big_key, small_keys, dungeon_items, player: int):
self.name = name self.name = name
self.regions = regions self.regions = regions
@@ -956,12 +881,15 @@ class Boss():
return f"Boss({self.name})" return f"Boss({self.name})"
class Location(): class Location():
shop_slot: bool = False # If given as integer, then this is the shop's inventory index
shop_slot: Optional[int] = None
shop_slot_disabled: bool = False shop_slot_disabled: bool = False
event: bool = False event: bool = False
locked: bool = False locked: bool = False
spot_type = 'Location' spot_type = 'Location'
game: str = "Generic" game: str = "Generic"
show_in_spoiler: bool = True
excluded: bool = False
crystal: bool = False crystal: bool = False
always_allow = staticmethod(lambda item, state: False) always_allow = staticmethod(lambda item, state: False)
access_rule = staticmethod(lambda state: True) access_rule = staticmethod(lambda state: True)
@@ -975,7 +903,7 @@ class Location():
self.item: Optional[Item] = None self.item: Optional[Item] = None
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool: def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
return self.always_allow(state, item) or (self.parent_region.can_fill(item) and self.item_rule(item) and (not check_access or self.can_reach(state))) return self.always_allow(state, item) or (self.item_rule(item) and (not check_access or self.can_reach(state)))
def can_reach(self, state: CollectionState) -> bool: def can_reach(self, state: CollectionState) -> bool:
# self.access_rule computes faster on average, so placing it first for faster abort # self.access_rule computes faster on average, so placing it first for faster abort
@@ -1011,21 +939,33 @@ class Location():
@property @property
def hint_text(self): def hint_text(self):
return getattr(self, "_hint_text", self.name.replace("_", " ").replace("-", " ")) hint_text = getattr(self, "_hint_text", None)
if hint_text:
return hint_text
return "at " + self.name.replace("_", " ").replace("-", " ")
class Item(): class Item():
location: Optional[Location] = None location: Optional[Location] = None
world: Optional[MultiWorld] = None world: Optional[MultiWorld] = None
code: Optional[str] = None # an item with ID None is called an Event, and does not get written to multidata
game: str = "Generic" game: str = "Generic"
type: str = None type: str = None
never_exclude = False # change manually to ensure that a specific nonprogression item never goes on an excluded location # change manually to ensure that a specific non-progression item never goes on an excluded location
never_exclude = False
# need to find a decent place for these to live and to allow other games to register texts if they want.
pedestal_credit_text: str = "and the Unknown Item" pedestal_credit_text: str = "and the Unknown Item"
sickkid_credit_text: Optional[str] = None sickkid_credit_text: Optional[str] = None
magicshop_credit_text: Optional[str] = None magicshop_credit_text: Optional[str] = None
zora_credit_text: Optional[str] = None zora_credit_text: Optional[str] = None
fluteboy_credit_text: Optional[str] = None fluteboy_credit_text: Optional[str] = None
code: Optional[str] = None # an item with ID None is called an Event, and does not get written to multidata
# hopefully temporary attributes to satisfy legacy LttP code, proper implementation in subclass ALttPItem
smallkey: bool = False
bigkey: bool = False
map: bool = False
compass: bool = False
def __init__(self, name: str, advancement: bool, code: Optional[int], player: int): def __init__(self, name: str, advancement: bool, code: Optional[int], player: int):
self.name = name self.name = name
@@ -1052,51 +992,6 @@ class Item():
def __hash__(self): def __hash__(self):
return hash((self.name, self.player)) return hash((self.name, self.player))
@property
def crystal(self) -> bool:
return self.type == 'Crystal'
@property
def smallkey(self) -> bool:
return self.type == 'SmallKey'
@property
def bigkey(self) -> bool:
return self.type == 'BigKey'
@property
def map(self) -> bool:
return self.type == 'Map'
@property
def compass(self) -> bool:
return self.type == 'Compass'
@property
def dungeon_item(self) -> Optional[str]:
if self.game == "A Link to the Past" and self.type in {"SmallKey", "BigKey", "Map", "Compass"}:
return self.type
@property
def shuffled_dungeon_item(self) -> bool:
dungeon_item_type = self.dungeon_item
if dungeon_item_type:
return {"SmallKey" : self.world.keyshuffle,
"BigKey": self.world.bigkeyshuffle,
"Map": self.world.mapshuffle,
"Compass": self.world.compassshuffle}[dungeon_item_type][self.player]
return False
@property
def locked_dungeon_item(self) -> bool:
dungeon_item_type = self.dungeon_item
if dungeon_item_type:
return not {"SmallKey" : self.world.keyshuffle,
"BigKey": self.world.bigkeyshuffle,
"Map": self.world.mapshuffle,
"Compass": self.world.compassshuffle}[dungeon_item_type][self.player]
return False
def __repr__(self): def __repr__(self):
return self.__str__() return self.__str__()
@@ -1104,7 +999,7 @@ class Item():
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})' return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
class Spoiler(object): class Spoiler():
world: MultiWorld world: MultiWorld
def __init__(self, world): def __init__(self, world):
@@ -1130,32 +1025,32 @@ class Spoiler(object):
def parse_data(self): def parse_data(self):
self.medallions = OrderedDict() self.medallions = OrderedDict()
for player in self.world.get_game_players("A Link to the Past"): for player in self.world.get_game_players("A Link to the Past"):
self.medallions[f'Misery Mire ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][0] self.medallions[f'Misery Mire ({self.world.get_player_name(player)})'] = self.world.required_medallions[player][0]
self.medallions[f'Turtle Rock ({self.world.get_player_names(player)})'] = self.world.required_medallions[player][1] self.medallions[f'Turtle Rock ({self.world.get_player_name(player)})'] = self.world.required_medallions[player][1]
self.startinventory = list(map(str, self.world.precollected_items)) self.startinventory = list(map(str, self.world.precollected_items))
self.locations = OrderedDict() self.locations = OrderedDict()
listed_locations = set() listed_locations = set()
lw_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.LightWorld] lw_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.LightWorld and loc.show_in_spoiler]
self.locations['Light World'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in lw_locations]) self.locations['Light World'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in lw_locations])
listed_locations.update(lw_locations) listed_locations.update(lw_locations)
dw_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.DarkWorld] dw_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.DarkWorld and loc.show_in_spoiler]
self.locations['Dark World'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in dw_locations]) self.locations['Dark World'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in dw_locations])
listed_locations.update(dw_locations) listed_locations.update(dw_locations)
cave_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.Cave] cave_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.Cave and loc.show_in_spoiler]
self.locations['Caves'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in cave_locations]) self.locations['Caves'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in cave_locations])
listed_locations.update(cave_locations) listed_locations.update(cave_locations)
for dungeon in self.world.dungeons: for dungeon in self.world.dungeons.values():
dungeon_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.dungeon == dungeon] dungeon_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.dungeon == dungeon and loc.show_in_spoiler]
self.locations[str(dungeon)] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in dungeon_locations]) self.locations[str(dungeon)] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in dungeon_locations])
listed_locations.update(dungeon_locations) listed_locations.update(dungeon_locations)
other_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations] other_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.show_in_spoiler]
if other_locations: if other_locations:
self.locations['Other Locations'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in other_locations]) self.locations['Other Locations'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in other_locations])
listed_locations.update(other_locations) listed_locations.update(other_locations)
@@ -1224,10 +1119,6 @@ class Spoiler(object):
'open_pyramid': self.world.open_pyramid, 'open_pyramid': self.world.open_pyramid,
'accessibility': self.world.accessibility, 'accessibility': self.world.accessibility,
'hints': self.world.hints, 'hints': self.world.hints,
'mapshuffle': self.world.mapshuffle,
'compassshuffle': self.world.compassshuffle,
'keyshuffle': self.world.keyshuffle,
'bigkeyshuffle': self.world.bigkeyshuffle,
'boss_shuffle': self.world.boss_shuffle, 'boss_shuffle': self.world.boss_shuffle,
'enemy_shuffle': self.world.enemy_shuffle, 'enemy_shuffle': self.world.enemy_shuffle,
'enemy_health': self.world.enemy_health, 'enemy_health': self.world.enemy_health,
@@ -1236,10 +1127,8 @@ class Spoiler(object):
'tile_shuffle': self.world.tile_shuffle, 'tile_shuffle': self.world.tile_shuffle,
'bush_shuffle': self.world.bush_shuffle, 'bush_shuffle': self.world.bush_shuffle,
'beemizer': self.world.beemizer, 'beemizer': self.world.beemizer,
'progressive': self.world.progressive,
'shufflepots': self.world.shufflepots, 'shufflepots': self.world.shufflepots,
'players': self.world.players, 'players': self.world.players,
'teams': self.world.teams,
'progression_balancing': self.world.progression_balancing, 'progression_balancing': self.world.progression_balancing,
'triforce_pieces_available': self.world.triforce_pieces_available, 'triforce_pieces_available': self.world.triforce_pieces_available,
'triforce_pieces_required': self.world.triforce_pieces_required, 'triforce_pieces_required': self.world.triforce_pieces_required,
@@ -1259,7 +1148,7 @@ class Spoiler(object):
out['Starting Inventory'] = self.startinventory out['Starting Inventory'] = self.startinventory
out['Special'] = self.medallions out['Special'] = self.medallions
if self.hashes: if self.hashes:
out['Hashes'] = {f"{self.world.player_names[player][team]} (Team {team+1})": hash for (player, team), hash in self.hashes.items()} out['Hashes'] = self.hashes
if self.shops: if self.shops:
out['Shops'] = self.shops out['Shops'] = self.shops
out['playthrough'] = self.playthrough out['playthrough'] = self.playthrough
@@ -1270,7 +1159,6 @@ class Spoiler(object):
return json.dumps(out) return json.dumps(out)
def to_file(self, filename): def to_file(self, filename):
import Options
self.parse_data() self.parse_data()
def bool_to_text(variable: Union[bool, str]) -> str: def bool_to_text(variable: Union[bool, str]) -> str:
@@ -1284,27 +1172,24 @@ class Spoiler(object):
self.metadata['version'], self.world.seed)) self.metadata['version'], self.world.seed))
outfile.write('Filling Algorithm: %s\n' % self.world.algorithm) outfile.write('Filling Algorithm: %s\n' % self.world.algorithm)
outfile.write('Players: %d\n' % self.world.players) outfile.write('Players: %d\n' % self.world.players)
outfile.write('Teams: %d\n' % self.world.teams)
for player in range(1, self.world.players + 1): for player in range(1, self.world.players + 1):
if self.world.players > 1: if self.world.players > 1:
outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_names(player))) outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_name(player)))
outfile.write('Game: %s\n' % self.metadata['game'][player]) outfile.write('Game: %s\n' % self.world.game[player])
if self.world.players > 1: if self.world.players > 1:
outfile.write('Progression Balanced: %s\n' % ( outfile.write('Progression Balanced: %s\n' % (
'Yes' if self.metadata['progression_balancing'][player] else 'No')) 'Yes' if self.metadata['progression_balancing'][player] else 'No'))
outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player]) outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player])
options = self.world.worlds[player].options options = self.world.worlds[player].options
if options: if options:
for f_option in options: for f_option, option in options.items():
res = getattr(self.world, f_option)[player] res = getattr(self.world, f_option)[player]
outfile.write(f'{f_option+":":33}{bool_to_text(res) if type(res) == Options.Toggle else res.get_option_name()}\n') displayname = getattr(option, "displayname", f_option)
outfile.write(f'{displayname + ":":33}{res.get_current_option_name()}\n')
if player in self.world.get_game_players("A Link to the Past"): if player in self.world.get_game_players("A Link to the Past"):
for team in range(self.world.teams): outfile.write('%s%s\n' % ('Hash: ', self.hashes[player]))
outfile.write('%s%s\n' % (
f"Hash - {self.world.player_names[player][team]} (Team {team + 1}): " if
(player in self.world.get_game_players("A Link to the Past") and self.world.teams > 1) else 'Hash: ',
self.hashes[player, team]))
outfile.write('Logic: %s\n' % self.metadata['logic'][player]) outfile.write('Logic: %s\n' % self.metadata['logic'][player])
outfile.write('Dark Room Logic: %s\n' % self.metadata['dark_room_logic'][player]) outfile.write('Dark Room Logic: %s\n' % self.metadata['dark_room_logic'][player])
@@ -1323,21 +1208,11 @@ class Spoiler(object):
self.metadata["triforce_pieces_required"][player]) self.metadata["triforce_pieces_required"][player])
outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player]) outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player])
outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player]) outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player])
outfile.write('Item Progression: %s\n' % self.metadata['progressive'][player])
outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player]) outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player])
if self.metadata['shuffle'][player] != "vanilla": if self.metadata['shuffle'][player] != "vanilla":
outfile.write('Entrance Shuffle Seed %s\n' % self.metadata['er_seeds'][player]) outfile.write('Entrance Shuffle Seed %s\n' % self.metadata['er_seeds'][player])
outfile.write('Pyramid hole pre-opened: %s\n' % ( outfile.write('Pyramid hole pre-opened: %s\n' % (
'Yes' if self.metadata['open_pyramid'][player] else 'No')) 'Yes' if self.metadata['open_pyramid'][player] else 'No'))
outfile.write('Map shuffle: %s\n' %
('Yes' if self.metadata['mapshuffle'][player] else 'No'))
outfile.write('Compass shuffle: %s\n' %
('Yes' if self.metadata['compassshuffle'][player] else 'No'))
outfile.write(
'Small Key shuffle: %s\n' % (bool_to_text(self.metadata['keyshuffle'][player])))
outfile.write('Big Key shuffle: %s\n' % (
'Yes' if self.metadata['bigkeyshuffle'][player] else 'No'))
outfile.write('Shop inventory shuffle: %s\n' % outfile.write('Shop inventory shuffle: %s\n' %
bool_to_text("i" in self.metadata["shop_shuffle"][player])) bool_to_text("i" in self.metadata["shop_shuffle"][player]))
outfile.write('Shop price shuffle: %s\n' % outfile.write('Shop price shuffle: %s\n' %
@@ -1366,7 +1241,7 @@ class Spoiler(object):
self.metadata['shuffle_prizes'][player]) self.metadata['shuffle_prizes'][player])
if self.entrances: if self.entrances:
outfile.write('\n\nEntrances:\n\n') outfile.write('\n\nEntrances:\n\n')
outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_names(entry["player"])}: ' outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_name(entry["player"])}: '
if self.world.players > 1 else '', entry['entrance'], if self.world.players > 1 else '', entry['entrance'],
'<=>' if entry['direction'] == 'both' else '<=>' if entry['direction'] == 'both' else
'<=' if entry['direction'] == 'exit' else '=>', '<=' if entry['direction'] == 'exit' else '=>',
@@ -1380,7 +1255,7 @@ class Spoiler(object):
if factorio_players: if factorio_players:
outfile.write('\n\nRecipes:\n') outfile.write('\n\nRecipes:\n')
for player in factorio_players: for player in factorio_players:
name = self.world.get_player_names(player) name = self.world.get_player_name(player)
for recipe in self.world.worlds[player].custom_recipes.values(): for recipe in self.world.worlds[player].custom_recipes.values():
outfile.write(f"\n{recipe.name} ({name}): {recipe.ingredients} -> {recipe.products}") outfile.write(f"\n{recipe.name} ({name}): {recipe.ingredients} -> {recipe.products}")
@@ -1398,7 +1273,7 @@ class Spoiler(object):
for player in self.world.get_game_players("A Link to the Past"): for player in self.world.get_game_players("A Link to the Past"):
if self.world.boss_shuffle[player] != 'none': if self.world.boss_shuffle[player] != 'none':
bossmap = self.bosses[str(player)] if self.world.players > 1 else self.bosses bossmap = self.bosses[str(player)] if self.world.players > 1 else self.bosses
outfile.write(f'\n\nBosses{(f" ({self.world.get_player_names(player)})" if self.world.players > 1 else "")}:\n') outfile.write(f'\n\nBosses{(f" ({self.world.get_player_name(player)})" if self.world.players > 1 else "")}:\n')
outfile.write(' '+'\n '.join([f'{x}: {y}' for x, y in bossmap.items()])) outfile.write(' '+'\n '.join([f'{x}: {y}' for x, y in bossmap.items()]))
outfile.write('\n\nPlaythrough:\n\n') outfile.write('\n\nPlaythrough:\n\n')
outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join([' %s: %s' % (location, item) for (location, item) in sphere.items()] if sphere_nr != '0' else [f' {item}' for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()])) outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join([' %s: %s' % (location, item) for (location, item) in sphere.items()] if sphere_nr != '0' else [f' {item}' for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()]))

View File

@@ -3,6 +3,7 @@ import logging
import typing import typing
import asyncio import asyncio
import urllib.parse import urllib.parse
import sys
import websockets import websockets
@@ -14,6 +15,8 @@ from worlds import network_data_package, AutoWorldRegister
logger = logging.getLogger("Client") logger = logging.getLogger("Client")
gui_enabled = Utils.is_frozen() or "--nogui" not in sys.argv
class ClientCommandProcessor(CommandProcessor): class ClientCommandProcessor(CommandProcessor):
def __init__(self, ctx: CommonContext): def __init__(self, ctx: CommonContext):
self.ctx = ctx self.ctx = ctx
@@ -49,7 +52,7 @@ class ClientCommandProcessor(CommandProcessor):
"""List all missing location checks, from your local game state""" """List all missing location checks, from your local game state"""
count = 0 count = 0
checked_count = 0 checked_count = 0
for location, location_id in AutoWorldRegister.world_types[self.ctx.game].item_name_to_id.items(): for location, location_id in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id.items():
if location_id < 0: if location_id < 0:
continue continue
if location_id not in self.ctx.locations_checked: if location_id not in self.ctx.locations_checked:
@@ -68,8 +71,6 @@ class ClientCommandProcessor(CommandProcessor):
self.output("No missing location checks found.") self.output("No missing location checks found.")
return True return True
def _cmd_ready(self): def _cmd_ready(self):
self.ctx.ready = not self.ctx.ready self.ctx.ready = not self.ctx.ready
if self.ctx.ready: if self.ctx.ready:
@@ -83,11 +84,13 @@ class ClientCommandProcessor(CommandProcessor):
def default(self, raw: str): def default(self, raw: str):
asyncio.create_task(self.ctx.send_msgs([{"cmd": "Say", "text": raw}])) asyncio.create_task(self.ctx.send_msgs([{"cmd": "Say", "text": raw}]))
class CommonContext(): class CommonContext():
starting_reconnect_delay = 5 starting_reconnect_delay = 5
current_reconnect_delay = starting_reconnect_delay current_reconnect_delay = starting_reconnect_delay
command_processor = ClientCommandProcessor command_processor = ClientCommandProcessor
game: None game: None
ui: None
def __init__(self, server_address, password): def __init__(self, server_address, password):
# server state # server state
@@ -140,16 +143,17 @@ class CommonContext():
if local_package and local_package["version"] > network_data_package["version"]: if local_package and local_package["version"] > network_data_package["version"]:
data_package: dict = local_package data_package: dict = local_package
elif network: # check if data from server is newer elif network: # check if data from server is newer
for key, value in data_package.items():
if type(value) == dict: # convert to int keys
data_package[key] = \
{int(subkey) if subkey.isdigit() else subkey: subvalue for subkey, subvalue in value.items()}
if data_package["version"] > network_data_package["version"]: if data_package["version"] > network_data_package["version"]:
Utils.persistent_store("datapackage", "latest", network_data_package) Utils.persistent_store("datapackage", "latest", network_data_package)
item_lookup: dict = data_package["lookup_any_item_id_to_name"] item_lookup: dict = {}
locations_lookup: dict = data_package["lookup_any_location_id_to_name"] locations_lookup: dict = {}
for game, gamedata in data_package["games"].items():
for item_name, item_id in gamedata["item_name_to_id"].items():
item_lookup[item_id] = item_name
for location_name, location_id in gamedata["location_name_to_id"].items():
locations_lookup[location_id] = location_name
def get_item_name_from_id(code: int): def get_item_name_from_id(code: int):
return item_lookup.get(code, f'Unknown item (ID:{code})') return item_lookup.get(code, f'Unknown item (ID:{code})')
@@ -196,7 +200,7 @@ class CommonContext():
self.input_requests += 1 self.input_requests += 1
return await self.input_queue.get() return await self.input_queue.get()
async def connect(self, address= None): async def connect(self, address=None):
await self.disconnect() await self.disconnect()
self.server_task = asyncio.create_task(server_loop(self, address)) self.server_task = asyncio.create_task(server_loop(self, address))
@@ -204,7 +208,15 @@ class CommonContext():
logger.info(args["text"]) logger.info(args["text"])
def on_print_json(self, args: dict): def on_print_json(self, args: dict):
logger.info(self.jsontotextparser(args["data"])) if self.ui:
self.ui.print_json(args["data"])
else:
text = self.jsontotextparser(args["data"])
logger.info(text)
def on_package(self, cmd: str, args: dict):
"""For custom package handling in subclasses."""
pass
async def server_loop(ctx: CommonContext, address=None): async def server_loop(ctx: CommonContext, address=None):
@@ -284,8 +296,10 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
logger.info('Password required') logger.info('Password required')
logger.info(f"Forfeit setting: {args['forfeit_mode']}") logger.info(f"Forfeit setting: {args['forfeit_mode']}")
logger.info(f"Remaining setting: {args['remaining_mode']}") logger.info(f"Remaining setting: {args['remaining_mode']}")
logger.info(f"A !hint costs {args['hint_cost']}% of checks points and you get {args['location_check_points']}" logger.info(
f" for each location checked. Use !hint for more information.") f"A !hint costs {args['hint_cost']}% of your total location count as points"
f" and you get {args['location_check_points']}"
f" for each location checked. Use !hint for more information.")
ctx.hint_cost = int(args['hint_cost']) ctx.hint_cost = int(args['hint_cost'])
ctx.check_points = int(args['location_check_points']) ctx.check_points = int(args['location_check_points'])
ctx.forfeit_mode = args['forfeit_mode'] ctx.forfeit_mode = args['forfeit_mode']
@@ -388,9 +402,14 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
elif cmd == 'InvalidPacket': elif cmd == 'InvalidPacket':
logger.warning(f"Invalid Packet of {args['type']}: {args['text']}") logger.warning(f"Invalid Packet of {args['type']}: {args['text']}")
elif cmd == "Bounced":
pass
else: else:
logger.debug(f"unknown command {cmd}") logger.debug(f"unknown command {cmd}")
ctx.on_package(cmd, args)
async def console_loop(ctx: CommonContext): async def console_loop(ctx: CommonContext):
import sys import sys
@@ -410,4 +429,4 @@ async def console_loop(ctx: CommonContext):
if input_text: if input_text:
commandprocessor(input_text) commandprocessor(input_text)
except Exception as e: except Exception as e:
logging.exception(e) logger.exception(e)

View File

@@ -4,133 +4,33 @@ import logging
import json import json
import string import string
import copy import copy
import sys
import subprocess import subprocess
import factorio_rcon import factorio_rcon
import colorama import colorama
import asyncio import asyncio
from queue import Queue from queue import Queue
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger, gui_enabled
from MultiServer import mark_raw from MultiServer import mark_raw
import Utils import Utils
import random import random
from NetUtils import RawJSONtoTextParser, NetworkItem, ClientStatus, JSONtoTextParser, JSONMessagePart from NetUtils import NetworkItem, ClientStatus, JSONtoTextParser, JSONMessagePart
from worlds.factorio.Technologies import lookup_id_to_name from worlds.factorio import Factorio
os.makedirs("logs", exist_ok=True) log_folder = Utils.local_path("logs")
# Log to file in gui case os.makedirs(log_folder, exist_ok=True)
if getattr(sys, "frozen", False) and not "--nogui" in sys.argv:
if gui_enabled:
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO, logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO,
filename=os.path.join("logs", "FactorioClient.txt"), filemode="w") filename=os.path.join(log_folder, "FactorioClient.txt"), filemode="w", force=True)
else: else:
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO) logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO, force=True)
logging.getLogger().addHandler(logging.FileHandler(os.path.join("logs", "FactorioClient.txt"), "w")) logging.getLogger().addHandler(logging.FileHandler(os.path.join(log_folder, "FactorioClient.txt"), "w"))
gui_enabled = Utils.is_frozen() or "--nogui" not in sys.argv
def get_kivy_app():
os.environ["KIVY_NO_CONSOLELOG"] = "1"
os.environ["KIVY_NO_FILELOG"] = "1"
os.environ["KIVY_NO_ARGS"] = "1"
from kivy.app import App
from kivy.base import ExceptionHandler, ExceptionManager, Config
from kivy.uix.gridlayout import GridLayout
from kivy.uix.textinput import TextInput
from kivy.uix.recycleview import RecycleView
from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem
from kivy.lang import Builder
class FactorioManager(App):
def __init__(self, ctx):
super(FactorioManager, self).__init__()
self.ctx = ctx
self.commandprocessor = ctx.command_processor(ctx)
self.icon = r"data/icon.png"
def build(self):
self.grid = GridLayout()
self.grid.cols = 1
self.tabs = TabbedPanel()
self.tabs.default_tab_text = "All"
self.title = "Archipelago Factorio Client"
pairs = [
("Client", "Archipelago"),
("FactorioServer", "Factorio Server Log"),
("FactorioWatcher", "Bridge Data Log"),
]
self.tabs.default_tab_content = UILog(*(logging.getLogger(logger_name) for logger_name, name in pairs))
for logger_name, display_name in pairs:
bridge_logger = logging.getLogger(logger_name)
panel = TabbedPanelItem(text=display_name)
panel.content = UILog(bridge_logger)
self.tabs.add_widget(panel)
self.grid.add_widget(self.tabs)
textinput = TextInput(size_hint_y=None, height=30, multiline=False)
textinput.bind(on_text_validate=self.on_message)
self.grid.add_widget(textinput)
self.commandprocessor("/help")
return self.grid
def on_stop(self):
self.ctx.exit_event.set()
def on_message(self, textinput: TextInput):
try:
input_text = textinput.text.strip()
textinput.text = ""
if self.ctx.input_requests > 0:
self.ctx.input_requests -= 1
self.ctx.input_queue.put_nowait(input_text)
elif input_text:
self.commandprocessor(input_text)
except Exception as e:
logger.exception(e)
def on_address(self, text: str):
print(text)
class LogtoUI(logging.Handler):
def __init__(self, on_log):
super(LogtoUI, self).__init__(logging.DEBUG)
self.on_log = on_log
def handle(self, record: logging.LogRecord) -> None:
self.on_log(record)
class UILog(RecycleView):
cols = 1
def __init__(self, *loggers_to_handle, **kwargs):
super(UILog, self).__init__(**kwargs)
self.data = []
for logger in loggers_to_handle:
logger.addHandler(LogtoUI(self.on_log))
def on_log(self, record: logging.LogRecord) -> None:
self.data.append({"text": record.getMessage()})
class E(ExceptionHandler):
def handle_exception(self, inst):
logger.exception(inst)
return ExceptionManager.RAISE
ExceptionManager.add_handler(E())
Config.set("input", "mouse", "mouse,disable_multitouch")
Builder.load_file(Utils.local_path("data", "client.kv"))
return FactorioManager
class FactorioCommandProcessor(ClientCommandProcessor): class FactorioCommandProcessor(ClientCommandProcessor):
ctx: FactorioContext ctx: FactorioContext
@@ -139,38 +39,44 @@ class FactorioCommandProcessor(ClientCommandProcessor):
def _cmd_factorio(self, text: str) -> bool: def _cmd_factorio(self, text: str) -> bool:
"""Send the following command to the bound Factorio Server.""" """Send the following command to the bound Factorio Server."""
if self.ctx.rcon_client: if self.ctx.rcon_client:
# TODO: Print the command non-silently only for race seeds, or otherwise block anything but /factorio /save in race seeds.
self.ctx.print_to_game(f"/factorio {text}")
result = self.ctx.rcon_client.send_command(text) result = self.ctx.rcon_client.send_command(text)
if result: if result:
self.output(result) self.output(result)
return True return True
return False return False
def _cmd_connect(self, address: str = "") -> bool: def _cmd_resync(self):
"""Connect to a MultiWorld Server""" """Manually trigger a resync."""
if not self.ctx.auth: self.ctx.awaiting_bridge = True
if self.ctx.rcon_client:
get_info(self.ctx, self.ctx.rcon_client) # retrieve current auth code
else:
self.output("Cannot connect to a server with unknown own identity, bridge to Factorio first.")
return super(FactorioCommandProcessor, self)._cmd_connect(address)
class FactorioContext(CommonContext): class FactorioContext(CommonContext):
command_processor = FactorioCommandProcessor command_processor = FactorioCommandProcessor
game = "Factorio" game = "Factorio"
# updated by spinup server
mod_version: Utils.Version = Utils.Version(0, 0, 0)
def __init__(self, server_address, password): def __init__(self, server_address, password):
super(FactorioContext, self).__init__(server_address, password) super(FactorioContext, self).__init__(server_address, password)
self.send_index = 0 self.send_index = 0
self.rcon_client = None self.rcon_client = None
self.awaiting_bridge = False self.awaiting_bridge = False
self.raw_json_text_parser = RawJSONtoTextParser(self)
self.factorio_json_text_parser = FactorioJSONtoTextParser(self) self.factorio_json_text_parser = FactorioJSONtoTextParser(self)
async def server_auth(self, password_requested): async def server_auth(self, password_requested):
if password_requested and not self.password: if password_requested and not self.password:
await super(FactorioContext, self).server_auth(password_requested) await super(FactorioContext, self).server_auth(password_requested)
if not self.auth:
if self.rcon_client:
get_info(self, self.rcon_client) # retrieve current auth code
else:
raise Exception("Cannot connect to a server with unknown own identity, "
"bridge to Factorio first.")
await self.send_msgs([{"cmd": 'Connect', await self.send_msgs([{"cmd": 'Connect',
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple, 'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
'tags': ['AP'], 'tags': ['AP'],
@@ -180,23 +86,35 @@ class FactorioContext(CommonContext):
def on_print(self, args: dict): def on_print(self, args: dict):
logger.info(args["text"]) logger.info(args["text"])
if self.rcon_client: if self.rcon_client:
cleaned_text = args['text'].replace('"', '') self.print_to_game(args['text'])
self.rcon_client.send_command(f"/sc game.print(\"[font=default-large-bold]Archipelago:[/font] "
f"{cleaned_text}\")")
def on_print_json(self, args: dict): def on_print_json(self, args: dict):
text = self.raw_json_text_parser(copy.deepcopy(args["data"]))
logger.info(text)
if self.rcon_client: if self.rcon_client:
text = self.factorio_json_text_parser(args["data"]) text = self.factorio_json_text_parser(copy.deepcopy(args["data"]))
cleaned_text = text.replace('"', '') self.print_to_game(text)
self.rcon_client.send_command(f"/sc game.print(\"[font=default-large-bold]Archipelago:[/font] " super(FactorioContext, self).on_print_json(args)
f"{cleaned_text}\")")
@property @property
def savegame_name(self) -> str: def savegame_name(self) -> str:
return f"AP_{self.seed_name}_{self.auth}.zip" return f"AP_{self.seed_name}_{self.auth}.zip"
def print_to_game(self, text):
# TODO: remove around version 0.2
if self.mod_version < Utils.Version(0, 1, 6):
text = text.replace('"', '')
self.rcon_client.send_command(f"/sc game.print(\"[font=default-large-bold]Archipelago:[/font] "
f"{text}\")")
else:
self.rcon_client.send_command(f"/ap-print [font=default-large-bold]Archipelago:[/font] "
f"{text}")
def on_package(self, cmd: str, args: dict):
if cmd == "Connected":
# catch up sync anything that is already cleared.
if args["checked_locations"]:
self.rcon_client.send_commands({item_name: f'/ap-get-technology ap-{item_name}-\t-1' for
item_name in args["checked_locations"]})
async def game_watcher(ctx: FactorioContext): async def game_watcher(ctx: FactorioContext):
bridge_logger = logging.getLogger("FactorioWatcher") bridge_logger = logging.getLogger("FactorioWatcher")
@@ -207,9 +125,9 @@ async def game_watcher(ctx: FactorioContext):
ctx.awaiting_bridge = False ctx.awaiting_bridge = False
data = json.loads(ctx.rcon_client.send_command("/ap-sync")) data = json.loads(ctx.rcon_client.send_command("/ap-sync"))
if data["slot_name"] != ctx.auth: if data["slot_name"] != ctx.auth:
logger.warning(f"Connected World is not the expected one {data['slot_name']} != {ctx.auth}") bridge_logger.warning(f"Connected World is not the expected one {data['slot_name']} != {ctx.auth}")
elif data["seed_name"] != ctx.seed_name: elif data["seed_name"] != ctx.seed_name:
logger.warning( bridge_logger.warning(
f"Connected Multiworld is not the expected one {data['seed_name']} != {ctx.seed_name}") f"Connected Multiworld is not the expected one {data['seed_name']} != {ctx.seed_name}")
else: else:
data = data["info"] data = data["info"]
@@ -276,23 +194,32 @@ async def factorio_server_watcher(ctx: FactorioContext):
factorio_server_logger.info(msg) factorio_server_logger.info(msg)
if not ctx.rcon_client and "Starting RCON interface at IP ADDR:" in msg: if not ctx.rcon_client and "Starting RCON interface at IP ADDR:" in msg:
ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password) ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
# trigger lua interface confirmation # TODO: remove around version 0.2
ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')") if ctx.mod_version < Utils.Version(0, 1, 6):
ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')") ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')")
ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')")
if not ctx.server:
logger.info("Established bridge to Factorio Server. "
"Ready to connect to Archipelago via /connect")
if not ctx.awaiting_bridge and "Archipelago Bridge Data available for game tick " in msg: if not ctx.awaiting_bridge and "Archipelago Bridge Data available for game tick " in msg:
ctx.awaiting_bridge = True ctx.awaiting_bridge = True
if ctx.rcon_client: if ctx.rcon_client:
commands = {}
while ctx.send_index < len(ctx.items_received): while ctx.send_index < len(ctx.items_received):
transfer_item: NetworkItem = ctx.items_received[ctx.send_index] transfer_item: NetworkItem = ctx.items_received[ctx.send_index]
item_id = transfer_item.item item_id = transfer_item.item
player_name = ctx.player_names[transfer_item.player] player_name = ctx.player_names[transfer_item.player]
if item_id not in lookup_id_to_name: if item_id not in Factorio.item_id_to_name:
logging.error(f"Cannot send unknown item ID: {item_id}") factorio_server_logger.error(f"Cannot send unknown item ID: {item_id}")
else: else:
item_name = lookup_id_to_name[item_id] item_name = Factorio.item_id_to_name[item_id]
factorio_server_logger.info(f"Sending {item_name} to Nauvis from {player_name}.") factorio_server_logger.info(f"Sending {item_name} to Nauvis from {player_name}.")
ctx.rcon_client.send_command(f'/ap-get-technology {item_name}\t{ctx.send_index}\t{player_name}') commands[ctx.send_index] = f'/ap-get-technology {item_name}\t{ctx.send_index}\t{player_name}'
ctx.send_index += 1 ctx.send_index += 1
if commands:
ctx.rcon_client.send_commands(commands)
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
except Exception as e: except Exception as e:
@@ -335,19 +262,24 @@ async def factorio_spinup_server(ctx: FactorioContext):
while not factorio_queue.empty(): while not factorio_queue.empty():
msg = factorio_queue.get() msg = factorio_queue.get()
factorio_server_logger.info(msg) factorio_server_logger.info(msg)
if "Loading mod AP-" in msg and msg.endswith("(data.lua)"):
parts = msg.split()
ctx.mod_version = Utils.Version(*(int(number) for number in parts[-2].split(".")))
if not rcon_client and "Starting RCON interface at IP ADDR:" in msg: if not rcon_client and "Starting RCON interface at IP ADDR:" in msg:
rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password) rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
get_info(ctx, rcon_client) get_info(ctx, rcon_client)
await asyncio.sleep(0.01) await asyncio.sleep(0.01)
if ctx.mod_version == ctx.__class__.mod_version:
raise Exception("No Archipelago mod was loaded. Aborting.")
except Exception as e: except Exception as e:
logging.exception(e) logging.exception(e)
logging.error("Aborted Factorio Server Bridge") logging.error("Aborted Factorio Server Bridge")
ctx.exit_event.set() ctx.exit_event.set()
else: else:
logger.info(f"Got World Information from AP Mod for seed {ctx.seed_name} in slot {ctx.auth}") logger.info(f"Got World Information from AP Mod {tuple(ctx.mod_version)} for seed {ctx.seed_name} in slot {ctx.auth}")
finally: finally:
factorio_process.terminate() factorio_process.terminate()
@@ -359,8 +291,9 @@ async def main(args):
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
if gui_enabled: if gui_enabled:
input_task = None input_task = None
ui_app = get_kivy_app()(ctx) from kvui import FactorioManager
ui_task = asyncio.create_task(ui_app.async_run(), name="UI") ctx.ui = FactorioManager(ctx)
ui_task = asyncio.create_task(ctx.ui.async_run(), name="UI")
else: else:
input_task = asyncio.create_task(console_loop(ctx), name="Input") input_task = asyncio.create_task(console_loop(ctx), name="Input")
ui_task = None ui_task = None
@@ -378,7 +311,7 @@ async def main(args):
if ctx.server and not ctx.server.socket.closed: if ctx.server and not ctx.server.socket.closed:
await ctx.server.socket.close() await ctx.server.socket.close()
if ctx.server_task is not None: if ctx.server_task:
await ctx.server_task await ctx.server_task
while ctx.input_requests > 0: while ctx.input_requests > 0:
@@ -411,7 +344,7 @@ if __name__ == '__main__':
parser = argparse.ArgumentParser(description="Optional arguments to FactorioClient follow. " parser = argparse.ArgumentParser(description="Optional arguments to FactorioClient follow. "
"Remaining arguments get passed into bound Factorio instance." "Remaining arguments get passed into bound Factorio instance."
"Refer to factorio --help for those.") "Refer to Factorio --help for those.")
parser.add_argument('--rcon-port', default='24242', type=int, help='Port to use to communicate with Factorio') parser.add_argument('--rcon-port', default='24242', type=int, help='Port to use to communicate with Factorio')
parser.add_argument('--rcon-password', help='Password to authenticate with RCON.') parser.add_argument('--rcon-password', help='Password to authenticate with RCON.')
parser.add_argument('--connect', default=None, help='Address of the multiworld host.') parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
@@ -427,13 +360,13 @@ if __name__ == '__main__':
factorio_server_logger = logging.getLogger("FactorioServer") factorio_server_logger = logging.getLogger("FactorioServer")
options = Utils.get_options() options = Utils.get_options()
executable = options["factorio_options"]["executable"] executable = options["factorio_options"]["executable"]
bin_dir = os.path.dirname(executable)
if not os.path.exists(bin_dir): if not os.path.exists(os.path.dirname(executable)):
raise FileNotFoundError(f"Path {bin_dir} does not exist or could not be accessed.") raise FileNotFoundError(f"Path {os.path.dirname(executable)} does not exist or could not be accessed.")
if not os.path.isdir(bin_dir): if os.path.isdir(executable): # user entered a path to a directory, let's find the executable therein
raise FileNotFoundError(f"Path {bin_dir} is not a directory.") executable = os.path.join(executable, "factorio")
if not os.path.exists(executable): if not os.path.isfile(executable):
if os.path.exists(executable + ".exe"): if os.path.isfile(executable + ".exe"):
executable = executable + ".exe" executable = executable + ".exe"
else: else:
raise FileNotFoundError(f"Path {executable} is not an executable file.") raise FileNotFoundError(f"Path {executable} is not an executable file.")

76
Fill.py
View File

@@ -4,9 +4,8 @@ import collections
import itertools import itertools
from BaseClasses import CollectionState, Location, MultiWorld from BaseClasses import CollectionState, Location, MultiWorld
from worlds.alttp.Items import ItemFactory
from worlds.alttp.Regions import key_drop_data
from worlds.generic import PlandoItem from worlds.generic import PlandoItem
from worlds.AutoWorld import call_all
class FillError(RuntimeError): class FillError(RuntimeError):
@@ -69,7 +68,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations,
itempool.extend(unplaced_items) itempool.extend(unplaced_items)
def distribute_items_restrictive(world: MultiWorld, gftower_trash=False, fill_locations=None): def distribute_items_restrictive(world: MultiWorld, fill_locations=None):
# If not passed in, then get a shuffled list of locations to fill in # If not passed in, then get a shuffled list of locations to fill in
if not fill_locations: if not fill_locations:
fill_locations = world.get_unfilled_locations() fill_locations = world.get_unfilled_locations()
@@ -80,6 +79,7 @@ def distribute_items_restrictive(world: MultiWorld, gftower_trash=False, fill_lo
progitempool = [] progitempool = []
nonexcludeditempool = [] nonexcludeditempool = []
localrestitempool = {player: [] for player in range(1, world.players + 1)} localrestitempool = {player: [] for player in range(1, world.players + 1)}
nonlocalrestitempool = []
restitempool = [] restitempool = []
for item in world.itempool: for item in world.itempool:
@@ -89,55 +89,14 @@ def distribute_items_restrictive(world: MultiWorld, gftower_trash=False, fill_lo
nonexcludeditempool.append(item) nonexcludeditempool.append(item)
elif item.name in world.local_items[item.player]: elif item.name in world.local_items[item.player]:
localrestitempool[item.player].append(item) localrestitempool[item.player].append(item)
elif item.name in world.non_local_items[item.player]:
nonlocalrestitempool.append(item)
else: else:
restitempool.append(item) restitempool.append(item)
standard_keyshuffle_players = set()
# fill in gtower locations with trash first
for player in world.get_game_players("A Link to the Past"):
if not gftower_trash or not world.ganonstower_vanilla[player] or \
world.logic[player] in {'owglitches', 'hybridglitches', "nologic"}:
gtower_trash_count = 0
elif 'triforcehunt' in world.goal[player] and ('local' in world.goal[player] or world.players == 1):
gtower_trash_count = world.random.randint(world.crystals_needed_for_gt[player] * 2,
world.crystals_needed_for_gt[player] * 4)
else:
gtower_trash_count = world.random.randint(0, world.crystals_needed_for_gt[player] * 2)
if gtower_trash_count:
gtower_locations = [location for location in fill_locations if
'Ganons Tower' in location.name and location.player == player]
world.random.shuffle(gtower_locations)
trashcnt = 0
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 trashcnt < gtower_trash_count:
spot_to_fill = gtower_locations.pop()
item_to_place = gt_item_pool.pop()
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)
trashcnt += 1
if world.mode[player] == 'standard' and world.keyshuffle[player] is True:
standard_keyshuffle_players.add(player)
# Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots
if standard_keyshuffle_players:
progitempool.sort(
key=lambda item: 1 if item.name == 'Small Key (Hyrule Castle)' and
item.player in standard_keyshuffle_players else 0)
world.random.shuffle(fill_locations) world.random.shuffle(fill_locations)
call_all(world, "fill_hook", progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool, restitempool, fill_locations)
fill_restrictive(world, world.state, fill_locations, progitempool) fill_restrictive(world, world.state, fill_locations, progitempool)
if nonexcludeditempool: if nonexcludeditempool:
@@ -162,15 +121,20 @@ def distribute_items_restrictive(world: MultiWorld, gftower_trash=False, fill_lo
world.push_item(spot_to_fill, item_to_place, False) world.push_item(spot_to_fill, item_to_place, False)
fill_locations.remove(spot_to_fill) fill_locations.remove(spot_to_fill)
for item_to_place in nonlocalrestitempool:
for i, location in enumerate(fill_locations):
if location.player != item_to_place.player:
world.push_item(fill_locations.pop(i), item_to_place, False)
break
else:
logging.warning(f"Could not place non_local_item {item_to_place} among {fill_locations}, tossing.")
world.random.shuffle(fill_locations) world.random.shuffle(fill_locations)
restitempool, fill_locations = fast_fill(world, restitempool, fill_locations) restitempool, fill_locations = fast_fill(world, restitempool, fill_locations)
unplaced = [item for item in progitempool + restitempool] unplaced = progitempool + restitempool
unfilled = [location.name for location in fill_locations] unfilled = [location.name for location in fill_locations]
for location in fill_locations:
world.push_item(location, ItemFactory('Nothing', location.player), False)
if unplaced or unfilled: if unplaced or unfilled:
logging.warning(f'Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}') logging.warning(f'Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}')
@@ -232,7 +196,7 @@ def flood_items(world: MultiWorld):
location_list = world.get_reachable_locations() location_list = world.get_reachable_locations()
world.random.shuffle(location_list) world.random.shuffle(location_list)
for location in location_list: for location in location_list:
if location.item is not None and not location.item.advancement and not location.item.smallkey and not location.item.bigkey: if location.item is not None and not location.item.advancement:
# safe to replace # safe to replace
replace_item = location.item replace_item = location.item
replace_item.location = None replace_item.location = None
@@ -373,6 +337,8 @@ def swap_location_item(location_1: Location, location_2: Location, check_locked=
def distribute_planned(world: MultiWorld): def distribute_planned(world: MultiWorld):
# TODO: remove. Preferably by implementing key drop
from worlds.alttp.Regions import key_drop_data
world_name_lookup = world.world_name_lookup world_name_lookup = world.world_name_lookup
for player in world.player_ids: for player in world.player_ids:
@@ -383,7 +349,7 @@ def distribute_planned(world: MultiWorld):
placement.warn( placement.warn(
f"Can't place '{placement.item}' at '{placement.location}', as key drop shuffle locations are not supported yet.") f"Can't place '{placement.item}' at '{placement.location}', as key drop shuffle locations are not supported yet.")
continue continue
item = ItemFactory(placement.item, player) item = world.worlds[player].create_item(placement.item)
target_world: int = placement.world target_world: int = placement.world
if target_world is False or world.players == 1: if target_world is False or world.players == 1:
target_world = player # in own world target_world = player # in own world
@@ -444,4 +410,4 @@ def distribute_planned(world: MultiWorld):
except ValueError: except ValueError:
placement.warn(f"Could not remove {item} from pool as it's already missing from it.") placement.warn(f"Could not remove {item} from pool as it's already missing from it.")
except Exception as e: except Exception as e:
raise Exception(f"Error running plando for player {player} ({world.player_names[player]})") from e raise Exception(f"Error running plando for player {player} ({world.player_name[player]})") from e

View File

@@ -19,7 +19,7 @@ from worlds.alttp.EntranceRandomizer import parse_arguments
from Main import main as ERmain from Main import main as ERmain
from Main import get_seed, seeddigits from Main import get_seed, seeddigits
import Options import Options
from worlds.alttp.Items import item_name_groups, item_table from worlds.alttp.Items import item_table
from worlds.alttp import Bosses from worlds.alttp import Bosses
from worlds.alttp.Text import TextTable from worlds.alttp.Text import TextTable
from worlds.alttp.Regions import location_table, key_drop_data from worlds.alttp.Regions import location_table, key_drop_data
@@ -40,7 +40,6 @@ def mystery_argparse():
help="Input directory for player files.") help="Input directory for player files.")
parser.add_argument('--seed', help='Define seed number to generate.', type=int) parser.add_argument('--seed', help='Define seed number to generate.', type=int)
parser.add_argument('--multi', default=defaults["players"], type=lambda value: min(max(int(value), 1), 255)) parser.add_argument('--multi', default=defaults["players"], type=lambda value: min(max(int(value), 1), 255))
parser.add_argument('--teams', default=1, type=lambda value: max(int(value), 1))
parser.add_argument('--spoiler', type=int, default=defaults["spoiler"]) parser.add_argument('--spoiler', type=int, default=defaults["spoiler"])
parser.add_argument('--rom', default=options["lttp_options"]["rom_file"], help="Path to the 1.0 JP LttP Baserom.") parser.add_argument('--rom', default=options["lttp_options"]["rom_file"], help="Path to the 1.0 JP LttP Baserom.")
parser.add_argument('--enemizercli', default=defaults["enemizer_path"]) parser.add_argument('--enemizercli', default=defaults["enemizer_path"])
@@ -121,14 +120,12 @@ def main(args=None, callback=ERmain):
f"A mix is also permitted.") f"A mix is also permitted.")
erargs = parse_arguments(['--multi', str(args.multi)]) erargs = parse_arguments(['--multi', str(args.multi)])
erargs.seed = seed erargs.seed = seed
erargs.name = {x: "" for x in range(1, args.multi + 1)} # only so it can be overwrittin in mystery
erargs.create_spoiler = args.spoiler > 0 erargs.create_spoiler = args.spoiler > 0
erargs.glitch_triforce = options["generator"]["glitch_triforce_room"] erargs.glitch_triforce = options["generator"]["glitch_triforce_room"]
erargs.race = args.race erargs.race = args.race
erargs.skip_playthrough = args.spoiler < 2 erargs.skip_playthrough = args.spoiler < 2
erargs.outputname = seed_name erargs.outputname = seed_name
erargs.outputpath = args.outputpath erargs.outputpath = args.outputpath
erargs.teams = args.teams
# set up logger # set up logger
if args.log_level: if args.log_level:
@@ -179,6 +176,8 @@ def main(args=None, callback=ERmain):
getattr(erargs, k)[player] = v getattr(erargs, k)[player] = v
except AttributeError: except AttributeError:
setattr(erargs, k, {player: v}) setattr(erargs, k, {player: v})
except Exception as e:
raise Exception(f"Error setting {k} to {v} for player {player}") from e
except Exception as e: except Exception as e:
raise ValueError(f"File {path} is destroyed. Please fix your yaml.") from e raise ValueError(f"File {path} is destroyed. Please fix your yaml.") from e
else: else:
@@ -189,8 +188,9 @@ def main(args=None, callback=ERmain):
erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0] erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0]
erargs.name[player] = handle_name(erargs.name[player], player, name_counter) erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
erargs.names = ",".join(erargs.name[i] for i in range(1, args.multi + 1)) if len(set(erargs.name.values())) != len(erargs.name):
del (erargs.name) raise Exception(f"Names have to be unique. Names: {erargs.name}")
if args.yaml_output: if args.yaml_output:
import yaml import yaml
important = {} important = {}
@@ -239,7 +239,7 @@ def convert_to_on_off(value):
return {True: "on", False: "off"}.get(value, value) return {True: "on", False: "off"}.get(value, value)
def get_choice(option, root, value=None) -> typing.Any: def get_choice_legacy(option, root, value=None) -> typing.Any:
if option not in root: if option not in root:
return value return value
if type(root[option]) is list: if type(root[option]) is list:
@@ -254,6 +254,20 @@ def get_choice(option, root, value=None) -> typing.Any:
raise RuntimeError(f"All options specified in \"{option}\" are weighted as zero.") raise RuntimeError(f"All options specified in \"{option}\" are weighted as zero.")
def get_choice(option, root, value=None) -> typing.Any:
if option not in root:
return value
if type(root[option]) is list:
return random.choices(root[option])[0]
if type(root[option]) is not dict:
return root[option]
if not root[option]:
return value
if any(root[option].values()):
return random.choices(list(root[option].keys()), weights=list(map(int, root[option].values())))[0]
raise RuntimeError(f"All options specified in \"{option}\" are weighted as zero.")
class SafeDict(dict): class SafeDict(dict):
def __missing__(self, key): def __missing__(self, key):
return '{' + key + '}' return '{' + key + '}'
@@ -267,7 +281,7 @@ def handle_name(name: str, player: int, name_counter: Counter):
name] > 1 else ''), name] > 1 else ''),
player=player, player=player,
PLAYER=(player if player > 1 else ''))) PLAYER=(player if player > 1 else '')))
new_name = new_name.strip().replace(' ', '_')[:16] new_name = new_name.strip()[:16]
if new_name == "Archipelago": if new_name == "Archipelago":
raise Exception(f"You cannot name yourself \"{new_name}\"") raise Exception(f"You cannot name yourself \"{new_name}\"")
return new_name return new_name
@@ -467,7 +481,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
inventoryweights = game_weights.get('start_inventory', {}) inventoryweights = game_weights.get('start_inventory', {})
startitems = [] startitems = []
for item in inventoryweights.keys(): for item in inventoryweights.keys():
itemvalue = get_choice(item, inventoryweights) itemvalue = get_choice_legacy(item, inventoryweights)
if isinstance(itemvalue, int): if isinstance(itemvalue, int):
for i in range(int(itemvalue)): for i in range(int(itemvalue)):
startitems.append(item) startitems.append(item)
@@ -487,7 +501,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
for option_name, option in AutoWorldRegister.world_types[ret.game].options.items(): for option_name, option in AutoWorldRegister.world_types[ret.game].options.items():
if option_name in game_weights: if option_name in game_weights:
try: try:
if issubclass(option, Options.OptionDict): if issubclass(option, Options.OptionDict) or issubclass(option, Options.OptionList):
setattr(ret, option_name, option.from_any(game_weights[option_name])) setattr(ret, option_name, option.from_any(game_weights[option_name]))
else: else:
setattr(ret, option_name, option.from_any(get_choice(option_name, game_weights))) setattr(ret, option_name, option.from_any(get_choice(option_name, game_weights)))
@@ -515,7 +529,9 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
glitches_required = get_choice('glitches_required', weights) if "dungeon_items" in weights and get_choice_legacy('dungeon_items', weights, "none") != "none":
raise Exception(f"dungeon_items key in A Link to the Past was removed, but is present in these weights as {get_choice_legacy('dungeon_items', weights, False)}.")
glitches_required = get_choice_legacy('glitches_required', weights)
if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'hybrid_major_glitches', 'minor_glitches']: if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'hybrid_major_glitches', 'minor_glitches']:
logging.warning("Only NMG, OWG, HMG and No Logic supported") logging.warning("Only NMG, OWG, HMG and No Logic supported")
glitches_required = 'none' glitches_required = 'none'
@@ -523,7 +539,7 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
'minor_glitches': 'minorglitches', 'hybrid_major_glitches': 'hybridglitches'}[ 'minor_glitches': 'minorglitches', 'hybrid_major_glitches': 'hybridglitches'}[
glitches_required] glitches_required]
ret.dark_room_logic = get_choice("dark_room_logic", weights, "lamp") ret.dark_room_logic = get_choice_legacy("dark_room_logic", weights, "lamp")
if not ret.dark_room_logic: # None/False if not ret.dark_room_logic: # None/False
ret.dark_room_logic = "none" ret.dark_room_logic = "none"
if ret.dark_room_logic == "sconces": if ret.dark_room_logic == "sconces":
@@ -531,93 +547,78 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
if ret.dark_room_logic not in {"lamp", "torches", "none"}: if ret.dark_room_logic not in {"lamp", "torches", "none"}:
raise ValueError(f"Unknown Dark Room Logic: \"{ret.dark_room_logic}\"") raise ValueError(f"Unknown Dark Room Logic: \"{ret.dark_room_logic}\"")
ret.restrict_dungeon_item_on_boss = get_choice('restrict_dungeon_item_on_boss', weights, False) ret.restrict_dungeon_item_on_boss = get_choice_legacy('restrict_dungeon_item_on_boss', weights, False)
dungeon_items = get_choice('dungeon_items', weights) entrance_shuffle = get_choice_legacy('entrance_shuffle', weights, 'vanilla')
if dungeon_items == 'full' or dungeon_items == True:
dungeon_items = 'mcsb'
elif dungeon_items == 'standard':
dungeon_items = ""
elif not dungeon_items:
dungeon_items = ""
if "u" in dungeon_items:
dungeon_items.replace("s", "")
ret.mapshuffle = get_choice('map_shuffle', weights, 'm' in dungeon_items)
ret.compassshuffle = get_choice('compass_shuffle', weights, 'c' in dungeon_items)
ret.keyshuffle = get_choice('smallkey_shuffle', weights,
'universal' if 'u' in dungeon_items else 's' in dungeon_items)
ret.bigkeyshuffle = get_choice('bigkey_shuffle', weights, 'b' in dungeon_items)
entrance_shuffle = get_choice('entrance_shuffle', weights, 'vanilla')
if entrance_shuffle.startswith('none-'): if entrance_shuffle.startswith('none-'):
ret.shuffle = 'vanilla' ret.shuffle = 'vanilla'
else: else:
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla' ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
goal = get_choice('goals', weights, 'ganon') goal = get_choice_legacy('goals', weights, 'ganon')
ret.goal = goals[goal] ret.goal = goals[goal]
# TODO consider moving open_pyramid to an automatic variable in the core roller, set to True when # TODO consider moving open_pyramid to an automatic variable in the core roller, set to True when
# fast ganon + ganon at hole # fast ganon + ganon at hole
ret.open_pyramid = get_choice('open_pyramid', weights, 'goal') ret.open_pyramid = get_choice_legacy('open_pyramid', weights, 'goal')
extra_pieces = get_choice('triforce_pieces_mode', weights, 'available') extra_pieces = get_choice_legacy('triforce_pieces_mode', weights, 'available')
ret.triforce_pieces_required = LttPOptions.TriforcePieces.from_any(get_choice('triforce_pieces_required', weights, 20)) ret.triforce_pieces_required = LttPOptions.TriforcePieces.from_any(get_choice_legacy('triforce_pieces_required', weights, 20))
# sum a percentage to required # sum a percentage to required
if extra_pieces == 'percentage': if extra_pieces == 'percentage':
percentage = max(100, float(get_choice('triforce_pieces_percentage', weights, 150))) / 100 percentage = max(100, float(get_choice_legacy('triforce_pieces_percentage', weights, 150))) / 100
ret.triforce_pieces_available = int(round(ret.triforce_pieces_required * percentage, 0)) ret.triforce_pieces_available = int(round(ret.triforce_pieces_required * percentage, 0))
# vanilla mode (specify how many pieces are) # vanilla mode (specify how many pieces are)
elif extra_pieces == 'available': elif extra_pieces == 'available':
ret.triforce_pieces_available = LttPOptions.TriforcePieces.from_any( ret.triforce_pieces_available = LttPOptions.TriforcePieces.from_any(
get_choice('triforce_pieces_available', weights, 30)) get_choice_legacy('triforce_pieces_available', weights, 30))
# required pieces + fixed extra # required pieces + fixed extra
elif extra_pieces == 'extra': elif extra_pieces == 'extra':
extra_pieces = max(0, int(get_choice('triforce_pieces_extra', weights, 10))) extra_pieces = max(0, int(get_choice_legacy('triforce_pieces_extra', weights, 10)))
ret.triforce_pieces_available = ret.triforce_pieces_required + extra_pieces ret.triforce_pieces_available = ret.triforce_pieces_required + extra_pieces
# change minimum to required pieces to avoid problems # change minimum to required pieces to avoid problems
ret.triforce_pieces_available = min(max(ret.triforce_pieces_required, int(ret.triforce_pieces_available)), 90) ret.triforce_pieces_available = min(max(ret.triforce_pieces_required, int(ret.triforce_pieces_available)), 90)
ret.shop_shuffle = get_choice('shop_shuffle', weights, '') ret.shop_shuffle = get_choice_legacy('shop_shuffle', weights, '')
if not ret.shop_shuffle: if not ret.shop_shuffle:
ret.shop_shuffle = '' ret.shop_shuffle = ''
ret.mode = get_choice("mode", weights) ret.mode = get_choice_legacy("mode", weights)
ret.retro = get_choice("retro", weights) ret.retro = get_choice_legacy("retro", weights)
ret.hints = get_choice('hints', weights) ret.hints = get_choice_legacy('hints', weights)
ret.swordless = get_choice('swordless', weights, False) ret.swordless = get_choice_legacy('swordless', weights, False)
ret.difficulty = get_choice('item_pool', weights) ret.difficulty = get_choice_legacy('item_pool', weights)
ret.item_functionality = get_choice('item_functionality', weights) ret.item_functionality = get_choice_legacy('item_functionality', weights)
boss_shuffle = get_choice('boss_shuffle', weights) boss_shuffle = get_choice_legacy('boss_shuffle', weights)
ret.shufflebosses = get_plando_bosses(boss_shuffle, plando_options) ret.shufflebosses = get_plando_bosses(boss_shuffle, plando_options)
ret.enemy_shuffle = bool(get_choice('enemy_shuffle', weights, False)) ret.enemy_shuffle = bool(get_choice_legacy('enemy_shuffle', weights, False))
ret.killable_thieves = get_choice('killable_thieves', weights, False) ret.killable_thieves = get_choice_legacy('killable_thieves', weights, False)
ret.tile_shuffle = get_choice('tile_shuffle', weights, False) ret.tile_shuffle = get_choice_legacy('tile_shuffle', weights, False)
ret.bush_shuffle = get_choice('bush_shuffle', weights, False) ret.bush_shuffle = get_choice_legacy('bush_shuffle', weights, False)
ret.enemy_damage = {None: 'default', ret.enemy_damage = {None: 'default',
'default': 'default', 'default': 'default',
'shuffled': 'shuffled', 'shuffled': 'shuffled',
'random': 'chaos' 'random': 'chaos', # to be removed
}[get_choice('enemy_damage', weights)] 'chaos': 'chaos',
}[get_choice_legacy('enemy_damage', weights)]
ret.enemy_health = get_choice('enemy_health', weights) ret.enemy_health = get_choice_legacy('enemy_health', weights)
ret.shufflepots = get_choice('pot_shuffle', weights) ret.shufflepots = get_choice_legacy('pot_shuffle', weights)
ret.beemizer = int(get_choice('beemizer', weights, 0)) ret.beemizer = int(get_choice_legacy('beemizer', weights, 0))
ret.timer = {'none': False, ret.timer = {'none': False,
None: False, None: False,
@@ -626,21 +627,19 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
'timed_ohko': 'timed-ohko', 'timed_ohko': 'timed-ohko',
'ohko': 'ohko', 'ohko': 'ohko',
'timed_countdown': 'timed-countdown', 'timed_countdown': 'timed-countdown',
'display': 'display'}[get_choice('timer', weights, False)] 'display': 'display'}[get_choice_legacy('timer', weights, False)]
ret.countdown_start_time = int(get_choice('countdown_start_time', weights, 10)) ret.countdown_start_time = int(get_choice_legacy('countdown_start_time', weights, 10))
ret.red_clock_time = int(get_choice('red_clock_time', weights, -2)) ret.red_clock_time = int(get_choice_legacy('red_clock_time', weights, -2))
ret.blue_clock_time = int(get_choice('blue_clock_time', weights, 2)) ret.blue_clock_time = int(get_choice_legacy('blue_clock_time', weights, 2))
ret.green_clock_time = int(get_choice('green_clock_time', weights, 4)) ret.green_clock_time = int(get_choice_legacy('green_clock_time', weights, 4))
ret.dungeon_counters = get_choice('dungeon_counters', weights, 'default') ret.dungeon_counters = get_choice_legacy('dungeon_counters', weights, 'default')
ret.progressive = convert_to_on_off(get_choice('progressive', weights, 'on')) ret.shuffle_prizes = get_choice_legacy('shuffle_prizes', weights, "g")
ret.shuffle_prizes = get_choice('shuffle_prizes', weights, "g") ret.required_medallions = [get_choice_legacy("misery_mire_medallion", weights, "random"),
get_choice_legacy("turtle_rock_medallion", weights, "random")]
ret.required_medallions = [get_choice("misery_mire_medallion", weights, "random"),
get_choice("turtle_rock_medallion", weights, "random")]
for index, medallion in enumerate(ret.required_medallions): for index, medallion in enumerate(ret.required_medallions):
ret.required_medallions[index] = {"ether": "Ether", "quake": "Quake", "bombos": "Bombos", "random": "random"} \ ret.required_medallions[index] = {"ether": "Ether", "quake": "Quake", "bombos": "Bombos", "random": "random"} \
@@ -648,13 +647,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
if not ret.required_medallions[index]: if not ret.required_medallions[index]:
raise Exception(f"unknown Medallion {medallion} for {'misery mire' if index == 0 else 'turtle rock'}") raise Exception(f"unknown Medallion {medallion} for {'misery mire' if index == 0 else 'turtle rock'}")
ret.glitch_boots = get_choice('glitch_boots', weights, True)
if get_choice("local_keys", weights, "l" in dungeon_items):
# () important for ordering of commands, without them the Big Keys section is part of the Small Key else
ret.local_items |= item_name_groups["Small Keys"] if ret.keyshuffle else set()
ret.local_items |= item_name_groups["Big Keys"] if ret.bigkeyshuffle else set()
ret.plando_items = [] ret.plando_items = []
if "items" in plando_options: if "items" in plando_options:
@@ -668,10 +660,10 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
options = weights.get("plando_items", []) options = weights.get("plando_items", [])
for placement in options: for placement in options:
if roll_percentage(get_choice("percentage", placement, 100)): if roll_percentage(get_choice_legacy("percentage", placement, 100)):
from_pool = get_choice("from_pool", placement, PlandoItem._field_defaults["from_pool"]) from_pool = get_choice_legacy("from_pool", placement, PlandoItem._field_defaults["from_pool"])
location_world = get_choice("world", placement, PlandoItem._field_defaults["world"]) location_world = get_choice_legacy("world", placement, PlandoItem._field_defaults["world"])
force = str(get_choice("force", placement, PlandoItem._field_defaults["force"])).lower() force = str(get_choice_legacy("force", placement, PlandoItem._field_defaults["force"])).lower()
if "items" in placement and "locations" in placement: if "items" in placement and "locations" in placement:
items = placement["items"] items = placement["items"]
locations = placement["locations"] locations = placement["locations"]
@@ -687,8 +679,8 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
for item, location in zip(items, locations): for item, location in zip(items, locations):
add_plando_item(item, location) add_plando_item(item, location)
else: else:
item = get_choice("item", placement, get_choice("items", placement)) item = get_choice_legacy("item", placement, get_choice_legacy("items", placement))
location = get_choice("location", placement) location = get_choice_legacy("location", placement)
add_plando_item(item, location) add_plando_item(item, location)
ret.plando_texts = {} ret.plando_texts = {}
@@ -697,39 +689,39 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
tt.removeUnwantedText() tt.removeUnwantedText()
options = weights.get("plando_texts", []) options = weights.get("plando_texts", [])
for placement in options: for placement in options:
if roll_percentage(get_choice("percentage", placement, 100)): if roll_percentage(get_choice_legacy("percentage", placement, 100)):
at = str(get_choice("at", placement)) at = str(get_choice_legacy("at", placement))
if at not in tt: if at not in tt:
raise Exception(f"No text target \"{at}\" found.") raise Exception(f"No text target \"{at}\" found.")
ret.plando_texts[at] = str(get_choice("text", placement)) ret.plando_texts[at] = str(get_choice_legacy("text", placement))
ret.plando_connections = [] ret.plando_connections = []
if "connections" in plando_options: if "connections" in plando_options:
options = weights.get("plando_connections", []) options = weights.get("plando_connections", [])
for placement in options: for placement in options:
if roll_percentage(get_choice("percentage", placement, 100)): if roll_percentage(get_choice_legacy("percentage", placement, 100)):
ret.plando_connections.append(PlandoConnection( ret.plando_connections.append(PlandoConnection(
get_choice("entrance", placement), get_choice_legacy("entrance", placement),
get_choice("exit", placement), get_choice_legacy("exit", placement),
get_choice("direction", placement, "both") get_choice_legacy("direction", placement, "both")
)) ))
ret.sprite_pool = weights.get('sprite_pool', []) ret.sprite_pool = weights.get('sprite_pool', [])
ret.sprite = get_choice('sprite', weights, "Link") ret.sprite = get_choice_legacy('sprite', weights, "Link")
if 'random_sprite_on_event' in weights: if 'random_sprite_on_event' in weights:
randomoneventweights = weights['random_sprite_on_event'] randomoneventweights = weights['random_sprite_on_event']
if get_choice('enabled', randomoneventweights, False): if get_choice_legacy('enabled', randomoneventweights, False):
ret.sprite = 'randomon' ret.sprite = 'randomon'
ret.sprite += '-hit' if get_choice('on_hit', randomoneventweights, True) else '' ret.sprite += '-hit' if get_choice_legacy('on_hit', randomoneventweights, True) else ''
ret.sprite += '-enter' if get_choice('on_enter', randomoneventweights, False) else '' ret.sprite += '-enter' if get_choice_legacy('on_enter', randomoneventweights, False) else ''
ret.sprite += '-exit' if get_choice('on_exit', randomoneventweights, False) else '' ret.sprite += '-exit' if get_choice_legacy('on_exit', randomoneventweights, False) else ''
ret.sprite += '-slash' if get_choice('on_slash', randomoneventweights, False) else '' ret.sprite += '-slash' if get_choice_legacy('on_slash', randomoneventweights, False) else ''
ret.sprite += '-item' if get_choice('on_item', randomoneventweights, False) else '' ret.sprite += '-item' if get_choice_legacy('on_item', randomoneventweights, False) else ''
ret.sprite += '-bonk' if get_choice('on_bonk', randomoneventweights, False) else '' ret.sprite += '-bonk' if get_choice_legacy('on_bonk', randomoneventweights, False) else ''
ret.sprite = 'randomonall' if get_choice('on_everything', randomoneventweights, False) else ret.sprite ret.sprite = 'randomonall' if get_choice_legacy('on_everything', randomoneventweights, False) else ret.sprite
ret.sprite = 'randomonnone' if ret.sprite == 'randomon' else ret.sprite ret.sprite = 'randomonnone' if ret.sprite == 'randomon' else ret.sprite
if (not ret.sprite_pool or get_choice('use_weighted_sprite_pool', randomoneventweights, False)) \ if (not ret.sprite_pool or get_choice_legacy('use_weighted_sprite_pool', randomoneventweights, False)) \
and 'sprite' in weights: # Use sprite as a weighted sprite pool, if a sprite pool is not already defined. and 'sprite' in weights: # Use sprite as a weighted sprite pool, if a sprite pool is not already defined.
for key, value in weights['sprite'].items(): for key, value in weights['sprite'].items():
if key.startswith('random'): if key.startswith('random'):
@@ -737,20 +729,10 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
else: else:
ret.sprite_pool += [key] * int(value) ret.sprite_pool += [key] * int(value)
ret.disablemusic = get_choice('disablemusic', weights, False)
ret.triforcehud = get_choice('triforcehud', weights, 'hide_goal')
ret.quickswap = get_choice('quickswap', weights, True)
ret.fastmenu = get_choice('menuspeed', weights, "normal")
ret.reduceflashing = get_choice('reduceflashing', weights, False)
ret.heartcolor = get_choice('heartcolor', weights, "red")
ret.heartbeep = convert_to_on_off(get_choice('heartbeep', weights, "normal"))
ret.ow_palettes = get_choice('ow_palettes', weights, "default")
ret.uw_palettes = get_choice('uw_palettes', weights, "default")
ret.hud_palettes = get_choice('hud_palettes', weights, "default")
ret.sword_palettes = get_choice('sword_palettes', weights, "default")
ret.shield_palettes = get_choice('shield_palettes', weights, "default")
ret.link_palettes = get_choice('link_palettes', weights, "default")
if __name__ == '__main__': if __name__ == '__main__':
import atexit
confirmation = atexit.register(input, "Press enter to close.")
main() main()
# in case of error-free exit should not need confirmation
atexit.unregister(confirmation)

View File

@@ -19,7 +19,7 @@ from tkinter import Tk, Frame, Label, StringVar, Entry, filedialog, messagebox,
from urllib.parse import urlparse from urllib.parse import urlparse
from urllib.request import urlopen from urllib.request import urlopen
from worlds.alttp.Rom import Sprite, LocalRom, apply_rom_settings from worlds.alttp.Rom import Sprite, LocalRom, apply_rom_settings, get_base_rom_bytes
from Utils import output_path, local_path, open_file from Utils import output_path, local_path, open_file
@@ -44,7 +44,7 @@ def main():
help='Path to an ALttP JAP(1.0) rom to use as a base.') help='Path to an ALttP JAP(1.0) rom to use as a base.')
parser.add_argument('--loglevel', default='info', const='info', nargs='?', parser.add_argument('--loglevel', default='info', const='info', nargs='?',
choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.') choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
parser.add_argument('--fastmenu', default='normal', const='normal', nargs='?', parser.add_argument('--menuspeed', default='normal', const='normal', nargs='?',
choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'], choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
help='''\ help='''\
Select the rate at which the menu opens and closes. Select the rate at which the menu opens and closes.
@@ -100,6 +100,7 @@ def main():
parser.add_argument('--names', default='', type=str) parser.add_argument('--names', default='', type=str)
parser.add_argument('--update_sprites', action='store_true', help='Update Sprite Database, then exit.') parser.add_argument('--update_sprites', action='store_true', help='Update Sprite Database, then exit.')
args = parser.parse_args() args = parser.parse_args()
args.music = not args.disablemusic
if args.update_sprites: if args.update_sprites:
run_sprite_update() run_sprite_update()
sys.exit() sys.exit()
@@ -150,7 +151,7 @@ def adjust(args):
if hasattr(args, "world"): if hasattr(args, "world"):
world = getattr(args, "world") world = getattr(args, "world")
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.fastmenu, args.disablemusic, apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.menuspeed, args.music,
args.sprite, palettes_options, reduceflashing=args.reduceflashing or racerom, world=world) args.sprite, palettes_options, reduceflashing=args.reduceflashing or racerom, world=world)
path = output_path(f'{os.path.basename(args.rom)[:-4]}_adjusted.sfc') path = output_path(f'{os.path.basename(args.rom)[:-4]}_adjusted.sfc')
rom.write_to_file(path) rom.write_to_file(path)
@@ -195,14 +196,14 @@ def adjustGUI():
guiargs = Namespace() guiargs = Namespace()
guiargs.heartbeep = rom_vars.heartbeepVar.get() guiargs.heartbeep = rom_vars.heartbeepVar.get()
guiargs.heartcolor = rom_vars.heartcolorVar.get() guiargs.heartcolor = rom_vars.heartcolorVar.get()
guiargs.fastmenu = rom_vars.fastMenuVar.get() guiargs.menuspeed = rom_vars.menuspeedVar.get()
guiargs.ow_palettes = rom_vars.owPalettesVar.get() guiargs.ow_palettes = rom_vars.owPalettesVar.get()
guiargs.uw_palettes = rom_vars.uwPalettesVar.get() guiargs.uw_palettes = rom_vars.uwPalettesVar.get()
guiargs.hud_palettes = rom_vars.hudPalettesVar.get() guiargs.hud_palettes = rom_vars.hudPalettesVar.get()
guiargs.sword_palettes = rom_vars.swordPalettesVar.get() guiargs.sword_palettes = rom_vars.swordPalettesVar.get()
guiargs.shield_palettes = rom_vars.shieldPalettesVar.get() guiargs.shield_palettes = rom_vars.shieldPalettesVar.get()
guiargs.quickswap = bool(rom_vars.quickSwapVar.get()) guiargs.quickswap = bool(rom_vars.quickSwapVar.get())
guiargs.disablemusic = bool(rom_vars.disableMusicVar.get()) guiargs.music = bool(rom_vars.MusicVar.get())
guiargs.reduceflashing = bool(rom_vars.disableFlashingVar.get()) guiargs.reduceflashing = bool(rom_vars.disableFlashingVar.get())
guiargs.rom = romVar2.get() guiargs.rom = romVar2.get()
guiargs.baserom = romVar.get() guiargs.baserom = romVar.get()
@@ -221,7 +222,6 @@ def adjustGUI():
else: else:
messagebox.showinfo(title="Success", message=f"Rom patched successfully to {path}") messagebox.showinfo(title="Success", message=f"Rom patched successfully to {path}")
from Utils import persistent_store from Utils import persistent_store
from worlds.alttp.Rom import Sprite
if isinstance(guiargs.sprite, Sprite): if isinstance(guiargs.sprite, Sprite):
guiargs.sprite = guiargs.sprite.name guiargs.sprite = guiargs.sprite.name
persistent_store("adjuster", "last_settings_3", guiargs) persistent_store("adjuster", "last_settings_3", guiargs)
@@ -411,9 +411,8 @@ def get_rom_frame(parent=None):
def RomSelect(): def RomSelect():
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")]) rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc")), ("All Files", "*")])
import Patch
try: try:
Patch.get_base_rom_bytes(rom) # throws error on checksum fail get_base_rom_bytes(rom) # throws error on checksum fail
except Exception as e: except Exception as e:
logging.exception(e) logging.exception(e)
messagebox.showerror(title="Error while reading ROM", message=str(e)) messagebox.showerror(title="Error while reading ROM", message=str(e))
@@ -439,9 +438,10 @@ def get_rom_options_frame(parent=None):
romOptionsFrame.rowconfigure(i, weight=1) romOptionsFrame.rowconfigure(i, weight=1)
vars = Namespace() vars = Namespace()
vars.disableMusicVar = IntVar() vars.MusicVar = IntVar()
disableMusicCheckbutton = Checkbutton(romOptionsFrame, text="Disable music", variable=vars.disableMusicVar) vars.MusicVar.set(1)
disableMusicCheckbutton.grid(row=0, column=0, sticky=E) MusicCheckbutton = Checkbutton(romOptionsFrame, text="Music", variable=vars.MusicVar)
MusicCheckbutton.grid(row=0, column=0, sticky=E)
vars.disableFlashingVar = IntVar(value=1) vars.disableFlashingVar = IntVar(value=1)
disableFlashingCheckbutton = Checkbutton(romOptionsFrame, text="Disable flashing (anti-epilepsy)", variable=vars.disableFlashingVar) disableFlashingCheckbutton = Checkbutton(romOptionsFrame, text="Disable flashing (anti-epilepsy)", variable=vars.disableFlashingVar)
@@ -485,14 +485,14 @@ def get_rom_options_frame(parent=None):
quickSwapCheckbutton = Checkbutton(romOptionsFrame, text="L/R Quickswapping", variable=vars.quickSwapVar) quickSwapCheckbutton = Checkbutton(romOptionsFrame, text="L/R Quickswapping", variable=vars.quickSwapVar)
quickSwapCheckbutton.grid(row=1, column=0, sticky=E) quickSwapCheckbutton.grid(row=1, column=0, sticky=E)
fastMenuFrame = Frame(romOptionsFrame) menuspeedFrame = Frame(romOptionsFrame)
fastMenuFrame.grid(row=1, column=1, sticky=E) menuspeedFrame.grid(row=1, column=1, sticky=E)
fastMenuLabel = Label(fastMenuFrame, text='Menu speed') menuspeedLabel = Label(menuspeedFrame, text='Menu speed')
fastMenuLabel.pack(side=LEFT) menuspeedLabel.pack(side=LEFT)
vars.fastMenuVar = StringVar() vars.menuspeedVar = StringVar()
vars.fastMenuVar.set('normal') vars.menuspeedVar.set('normal')
fastMenuOptionMenu = OptionMenu(fastMenuFrame, vars.fastMenuVar, 'normal', 'instant', 'double', 'triple', 'quadruple', 'half') menuspeedOptionMenu = OptionMenu(menuspeedFrame, vars.menuspeedVar, 'normal', 'instant', 'double', 'triple', 'quadruple', 'half')
fastMenuOptionMenu.pack(side=LEFT) menuspeedOptionMenu.pack(side=LEFT)
heartcolorFrame = Frame(romOptionsFrame) heartcolorFrame = Frame(romOptionsFrame)
heartcolorFrame.grid(row=2, column=0, sticky=E) heartcolorFrame.grid(row=2, column=0, sticky=E)
@@ -518,7 +518,7 @@ def get_rom_options_frame(parent=None):
owPalettesLabel.pack(side=LEFT) owPalettesLabel.pack(side=LEFT)
vars.owPalettesVar = StringVar() vars.owPalettesVar = StringVar()
vars.owPalettesVar.set('default') vars.owPalettesVar.set('default')
owPalettesOptionMenu = OptionMenu(owPalettesFrame, vars.owPalettesVar, 'default', 'random', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke') owPalettesOptionMenu = OptionMenu(owPalettesFrame, vars.owPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
owPalettesOptionMenu.pack(side=LEFT) owPalettesOptionMenu.pack(side=LEFT)
uwPalettesFrame = Frame(romOptionsFrame) uwPalettesFrame = Frame(romOptionsFrame)
@@ -527,7 +527,7 @@ def get_rom_options_frame(parent=None):
uwPalettesLabel.pack(side=LEFT) uwPalettesLabel.pack(side=LEFT)
vars.uwPalettesVar = StringVar() vars.uwPalettesVar = StringVar()
vars.uwPalettesVar.set('default') vars.uwPalettesVar.set('default')
uwPalettesOptionMenu = OptionMenu(uwPalettesFrame, vars.uwPalettesVar, 'default', 'random', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke') uwPalettesOptionMenu = OptionMenu(uwPalettesFrame, vars.uwPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
uwPalettesOptionMenu.pack(side=LEFT) uwPalettesOptionMenu.pack(side=LEFT)
hudPalettesFrame = Frame(romOptionsFrame) hudPalettesFrame = Frame(romOptionsFrame)
@@ -536,7 +536,7 @@ def get_rom_options_frame(parent=None):
hudPalettesLabel.pack(side=LEFT) hudPalettesLabel.pack(side=LEFT)
vars.hudPalettesVar = StringVar() vars.hudPalettesVar = StringVar()
vars.hudPalettesVar.set('default') vars.hudPalettesVar.set('default')
hudPalettesOptionMenu = OptionMenu(hudPalettesFrame, vars.hudPalettesVar, 'default', 'random', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke') hudPalettesOptionMenu = OptionMenu(hudPalettesFrame, vars.hudPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
hudPalettesOptionMenu.pack(side=LEFT) hudPalettesOptionMenu.pack(side=LEFT)
swordPalettesFrame = Frame(romOptionsFrame) swordPalettesFrame = Frame(romOptionsFrame)
@@ -545,7 +545,7 @@ def get_rom_options_frame(parent=None):
swordPalettesLabel.pack(side=LEFT) swordPalettesLabel.pack(side=LEFT)
vars.swordPalettesVar = StringVar() vars.swordPalettesVar = StringVar()
vars.swordPalettesVar.set('default') vars.swordPalettesVar.set('default')
swordPalettesOptionMenu = OptionMenu(swordPalettesFrame, vars.swordPalettesVar, 'default', 'random', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke') swordPalettesOptionMenu = OptionMenu(swordPalettesFrame, vars.swordPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
swordPalettesOptionMenu.pack(side=LEFT) swordPalettesOptionMenu.pack(side=LEFT)
shieldPalettesFrame = Frame(romOptionsFrame) shieldPalettesFrame = Frame(romOptionsFrame)
@@ -554,7 +554,7 @@ def get_rom_options_frame(parent=None):
shieldPalettesLabel.pack(side=LEFT) shieldPalettesLabel.pack(side=LEFT)
vars.shieldPalettesVar = StringVar() vars.shieldPalettesVar = StringVar()
vars.shieldPalettesVar.set('default') vars.shieldPalettesVar.set('default')
shieldPalettesOptionMenu = OptionMenu(shieldPalettesFrame, vars.shieldPalettesVar, 'default', 'random', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke') shieldPalettesOptionMenu = OptionMenu(shieldPalettesFrame, vars.shieldPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
shieldPalettesOptionMenu.pack(side=LEFT) shieldPalettesOptionMenu.pack(side=LEFT)
spritePoolFrame = Frame(romOptionsFrame) spritePoolFrame = Frame(romOptionsFrame)

View File

@@ -1,16 +1,19 @@
import argparse import argparse
import atexit import atexit
exit_func = atexit.register(input, "Press enter to close.")
import threading
import time import time
import multiprocessing import multiprocessing
import os import os
import subprocess import subprocess
import base64 import base64
import shutil import shutil
import logging
import asyncio
from json import loads, dumps from json import loads, dumps
from Utils import get_item_name_from_id from Utils import get_item_name_from_id
exit_func = atexit.register(input, "Press enter to close.")
import ModuleUpdate import ModuleUpdate
@@ -23,11 +26,23 @@ from NetUtils import *
from worlds.alttp import Regions, Shops from worlds.alttp import Regions, Shops
from worlds.alttp import Items from worlds.alttp import Items
import Utils import Utils
from CommonClient import CommonContext, server_loop, logger, console_loop, ClientCommandProcessor from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, gui_enabled
snes_logger = logging.getLogger("SNES")
from MultiServer import mark_raw from MultiServer import mark_raw
log_folder = Utils.local_path("logs")
os.makedirs(log_folder, exist_ok=True)
# Log to file in gui case
if gui_enabled:
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO,
filename=os.path.join(log_folder, "LttPClient.txt"), filemode="w", force=True)
else:
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO, force=True)
logging.getLogger().addHandler(logging.FileHandler(os.path.join(log_folder, "LttPClient.txt"), "w"))
class LttPCommandProcessor(ClientCommandProcessor): class LttPCommandProcessor(ClientCommandProcessor):
def _cmd_slow_mode(self, toggle: str = ""): def _cmd_slow_mode(self, toggle: str = ""):
@@ -40,10 +55,26 @@ class LttPCommandProcessor(ClientCommandProcessor):
self.output(f"Setting slow mode to {self.ctx.slow_mode}") self.output(f"Setting slow mode to {self.ctx.slow_mode}")
@mark_raw @mark_raw
def _cmd_snes(self, snes_address: str = "") -> bool: def _cmd_snes(self, snes_options: str = "") -> bool:
"""Connect to a snes. Optionally include network address of a snes to connect to, otherwise show available devices""" """Connect to a snes. Optionally include network address of a snes to connect to, otherwise show available devices; and a SNES device number if more than one SNES is detected"""
snes_address = self.ctx.snes_address
snes_device_number = -1
options = snes_options.split()
num_options = len(options)
if num_options > 0:
snes_address = options[0]
if num_options > 1:
try:
snes_device_number = int(options[1])
except:
pass
self.ctx.snes_reconnect_address = None self.ctx.snes_reconnect_address = None
asyncio.create_task(snes_connect(self.ctx, snes_address if snes_address else self.ctx.snes_address)) asyncio.create_task(snes_connect(self.ctx, snes_address, snes_device_number))
return True return True
def _cmd_snes_close(self) -> bool: def _cmd_snes_close(self) -> bool:
@@ -71,6 +102,7 @@ class Context(CommonContext):
self.snes_recv_queue = asyncio.Queue() self.snes_recv_queue = asyncio.Queue()
self.snes_request_lock = asyncio.Lock() self.snes_request_lock = asyncio.Lock()
self.snes_write_buffer = [] self.snes_write_buffer = []
self.snes_connector_lock = threading.Lock()
self.awaiting_rom = False self.awaiting_rom = False
self.rom = None self.rom = None
@@ -91,7 +123,7 @@ class Context(CommonContext):
await super(Context, self).server_auth(password_requested) await super(Context, self).server_auth(password_requested)
if self.rom is None: if self.rom is None:
self.awaiting_rom = True self.awaiting_rom = True
logger.info( 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 return
self.awaiting_rom = False self.awaiting_rom = False
@@ -420,19 +452,21 @@ def launch_sni(ctx: Context):
sni_path = os.path.join(sni_path, file) sni_path = os.path.join(sni_path, file)
if os.path.isfile(sni_path): if os.path.isfile(sni_path):
logger.info(f"Attempting to start {sni_path}") snes_logger.info(f"Attempting to start {sni_path}")
import subprocess import subprocess
subprocess.Popen(sni_path, cwd=os.path.dirname(sni_path), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) if Utils.is_frozen(): # if it spawns a visible console, may as well populate it
subprocess.Popen(sni_path, cwd=os.path.dirname(sni_path))
else:
subprocess.Popen(sni_path, cwd=os.path.dirname(sni_path), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
else: else:
logger.info( snes_logger.info(
f"Attempt to start SNI was aborted as path {sni_path} was not found, " f"Attempt to start SNI was aborted as path {sni_path} was not found, "
f"please start it yourself if it is not running") f"please start it yourself if it is not running")
async def _snes_connect(ctx: Context, address: str): async def _snes_connect(ctx: Context, address: str):
address = f"ws://{address}" if "://" not in address else address address = f"ws://{address}" if "://" not in address else address
snes_logger.info("Connecting to SNI at %s ..." % address)
logger.info("Connecting to SNI at %s ..." % address)
seen_problems = set() seen_problems = set()
succesful = False succesful = False
while not succesful: while not succesful:
@@ -444,7 +478,7 @@ async def _snes_connect(ctx: Context, address: str):
# only tell the user about new problems, otherwise silently lay in wait for a working connection # only tell the user about new problems, otherwise silently lay in wait for a working connection
if problem not in seen_problems: if problem not in seen_problems:
seen_problems.add(problem) seen_problems.add(problem)
logger.error(f"Error connecting to SNI ({problem})") snes_logger.error(f"Error connecting to SNI ({problem})")
if len(seen_problems) == 1: if len(seen_problems) == 1:
# this is the first problem. Let's try launching SNI if it isn't already running # this is the first problem. Let's try launching SNI if it isn't already running
@@ -467,7 +501,7 @@ async def get_snes_devices(ctx: Context):
devices = reply['Results'] if 'Results' in reply and len(reply['Results']) > 0 else None devices = reply['Results'] if 'Results' in reply and len(reply['Results']) > 0 else None
if not devices: if not devices:
logger.info('No SNES device found. Please connect a SNES device to SNI.') snes_logger.info('No SNES device found. Please connect a SNES device to SNI.')
while not devices: while not devices:
await asyncio.sleep(1) await asyncio.sleep(1)
await socket.send(dumps(DeviceList_Request)) await socket.send(dumps(DeviceList_Request))
@@ -479,12 +513,16 @@ async def get_snes_devices(ctx: Context):
return devices return devices
async def snes_connect(ctx: Context, address): async def snes_connect(ctx: Context, address, deviceIndex = -1):
global SNES_RECONNECT_DELAY global SNES_RECONNECT_DELAY
if ctx.snes_socket is not None and ctx.snes_state == SNESState.SNES_CONNECTED: if ctx.snes_socket is not None and ctx.snes_state == SNESState.SNES_CONNECTED:
logger.error('Already connected to snes') if ctx.rom:
snes_logger.error('Already connected to SNES, with rom loaded.')
else:
snes_logger.error('Already connected to SNI, likely awaiting a device.')
return return
device = None
recv_task = None recv_task = None
ctx.snes_state = SNESState.SNES_CONNECTING ctx.snes_state = SNESState.SNES_CONNECTING
socket = await _snes_connect(ctx, address) socket = await _snes_connect(ctx, address)
@@ -493,19 +531,33 @@ async def snes_connect(ctx: Context, address):
try: try:
devices = await get_snes_devices(ctx) devices = await get_snes_devices(ctx)
numDevices = len(devices)
if len(devices) == 1: if numDevices == 1:
device = devices[0] device = devices[0]
elif ctx.snes_reconnect_address: elif ctx.snes_reconnect_address:
if ctx.snes_attached_device[1] in devices: if ctx.snes_attached_device[1] in devices:
device = ctx.snes_attached_device[1] device = ctx.snes_attached_device[1]
else: else:
device = devices[ctx.snes_attached_device[0]] device = devices[ctx.snes_attached_device[0]]
else: elif numDevices > 1:
if deviceIndex == -1:
snes_logger.info("Found " + str(numDevices) + " SNES devices; connect to one with /snes <address> <device number>:")
for idx, availableDevice in enumerate(devices):
snes_logger.info(str(idx + 1) + ": " + availableDevice)
elif (deviceIndex < 0) or (deviceIndex - 1) > numDevices:
snes_logger.warning("SNES device number out of range")
else:
device = devices[deviceIndex - 1]
if device is None:
await snes_disconnect(ctx) await snes_disconnect(ctx)
return return
logger.info("Attaching to " + device) snes_logger.info("Attaching to " + device)
Attach_Request = { Attach_Request = {
"Opcode": "Attach", "Opcode": "Attach",
@@ -518,6 +570,7 @@ async def snes_connect(ctx: Context, address):
ctx.snes_reconnect_address = address ctx.snes_reconnect_address = address
recv_task = asyncio.create_task(snes_recv_loop(ctx)) recv_task = asyncio.create_task(snes_recv_loop(ctx))
SNES_RECONNECT_DELAY = ctx.starting_reconnect_delay SNES_RECONNECT_DELAY = ctx.starting_reconnect_delay
snes_logger.info(f"Attached to {device}")
except Exception as e: except Exception as e:
if recv_task is not None: if recv_task is not None:
@@ -530,9 +583,9 @@ async def snes_connect(ctx: Context, address):
ctx.snes_socket = None ctx.snes_socket = None
ctx.snes_state = SNESState.SNES_DISCONNECTED ctx.snes_state = SNESState.SNES_DISCONNECTED
if not ctx.snes_reconnect_address: if not ctx.snes_reconnect_address:
logger.error("Error connecting to snes (%s)" % e) snes_logger.error("Error connecting to snes (%s)" % e)
else: else:
logger.error(f"Error connecting to snes, attempt again in {SNES_RECONNECT_DELAY}s") snes_logger.error(f"Error connecting to snes, attempt again in {SNES_RECONNECT_DELAY}s")
asyncio.create_task(snes_autoreconnect(ctx)) asyncio.create_task(snes_autoreconnect(ctx))
SNES_RECONNECT_DELAY *= 2 SNES_RECONNECT_DELAY *= 2
@@ -559,11 +612,11 @@ async def snes_recv_loop(ctx: Context):
try: try:
async for msg in ctx.snes_socket: async for msg in ctx.snes_socket:
ctx.snes_recv_queue.put_nowait(msg) ctx.snes_recv_queue.put_nowait(msg)
logger.warning("Snes disconnected") snes_logger.warning("Snes disconnected")
except Exception as e: except Exception as e:
if not isinstance(e, websockets.WebSocketException): if not isinstance(e, websockets.WebSocketException):
logger.exception(e) snes_logger.exception(e)
logger.error("Lost connection to the snes, type /snes to reconnect") snes_logger.error("Lost connection to the snes, type /snes to reconnect")
finally: finally:
socket, ctx.snes_socket = ctx.snes_socket, None socket, ctx.snes_socket = ctx.snes_socket, None
if socket is not None and not socket.closed: if socket is not None and not socket.closed:
@@ -576,7 +629,7 @@ async def snes_recv_loop(ctx: Context):
ctx.rom = None ctx.rom = None
if ctx.snes_reconnect_address: if ctx.snes_reconnect_address:
logger.info(f"...reconnecting in {SNES_RECONNECT_DELAY}s") snes_logger.info(f"...reconnecting in {SNES_RECONNECT_DELAY}s")
asyncio.create_task(snes_autoreconnect(ctx)) asyncio.create_task(snes_autoreconnect(ctx))
@@ -605,10 +658,10 @@ async def snes_read(ctx: Context, address, size):
break break
if len(data) != size: if len(data) != size:
logger.error('Error reading %s, requested %d bytes, received %d' % (hex(address), size, len(data))) snes_logger.error('Error reading %s, requested %d bytes, received %d' % (hex(address), size, len(data)))
if len(data): if len(data):
logger.error(str(data)) snes_logger.error(str(data))
logger.warning('Communication Failure with SNI') snes_logger.warning('Communication Failure with SNI')
if ctx.snes_socket is not None and not ctx.snes_socket.closed: if ctx.snes_socket is not None and not ctx.snes_socket.closed:
await ctx.snes_socket.close() await ctx.snes_socket.close()
return None return None
@@ -634,7 +687,7 @@ async def snes_write(ctx: Context, write_list):
await ctx.snes_socket.send(dumps(PutAddress_Request)) await ctx.snes_socket.send(dumps(PutAddress_Request))
await ctx.snes_socket.send(data) await ctx.snes_socket.send(data)
else: else:
logger.warning(f"Could not send data to SNES: {data}") snes_logger.warning(f"Could not send data to SNES: {data}")
except websockets.ConnectionClosed: except websockets.ConnectionClosed:
return False return False
@@ -673,7 +726,7 @@ async def track_locations(ctx: Context, roomid, roomdata):
new_locations.append(location_id) new_locations.append(location_id)
ctx.locations_checked.add(location_id) ctx.locations_checked.add(location_id)
location = ctx.location_name_getter(location_id) location = ctx.location_name_getter(location_id)
logger.info(f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') snes_logger.info(f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
try: try:
if roomid in location_shop_ids: if roomid in location_shop_ids:
@@ -682,7 +735,7 @@ async def track_locations(ctx: Context, roomid, roomdata):
if int(b) and (Shops.SHOP_ID_START + cnt) not in ctx.locations_checked: if int(b) and (Shops.SHOP_ID_START + cnt) not in ctx.locations_checked:
new_check(Shops.SHOP_ID_START + cnt) new_check(Shops.SHOP_ID_START + cnt)
except Exception as e: except Exception as e:
logger.info(f"Exception: {e}") snes_logger.info(f"Exception: {e}")
for location_id, (loc_roomid, loc_mask) in location_table_uw_id.items(): for location_id, (loc_roomid, loc_mask) in location_table_uw_id.items():
try: try:
@@ -691,7 +744,7 @@ async def track_locations(ctx: Context, roomid, roomdata):
roomdata << 4) & loc_mask != 0: roomdata << 4) & loc_mask != 0:
new_check(location_id) new_check(location_id)
except Exception as e: except Exception as e:
logger.exception(f"Exception: {e}") snes_logger.exception(f"Exception: {e}")
uw_begin = 0x129 uw_begin = 0x129
ow_end = uw_end = 0 ow_end = uw_end = 0
@@ -774,7 +827,7 @@ async def game_watcher(ctx: Context):
await ctx.server_auth(False) await ctx.server_auth(False)
if ctx.auth and ctx.auth != ctx.rom: if ctx.auth and ctx.auth != ctx.rom:
logger.warning("ROM change detected, please reconnect to the multiworld server") snes_logger.warning("ROM change detected, please reconnect to the multiworld server")
await ctx.disconnect() await ctx.disconnect()
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1) gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
@@ -857,6 +910,8 @@ async def main():
parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical']) parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
parser.add_argument('--founditems', default=False, action='store_true', parser.add_argument('--founditems', default=False, action='store_true',
help='Show items found by other players for themselves.') help='Show items found by other players for themselves.')
if not Utils.is_frozen(): # Frozen state has no cmd window in the first place
parser.add_argument('--nogui', default=False, action='store_true', help="Turns off Client GUI.")
args = parser.parse_args() args = parser.parse_args()
logging.basicConfig(format='%(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO)) logging.basicConfig(format='%(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
if args.diff_file: if args.diff_file:
@@ -865,7 +920,7 @@ async def main():
meta, romfile = Patch.create_rom_file(args.diff_file) meta, romfile = Patch.create_rom_file(args.diff_file)
args.connect = meta["server"] args.connect = meta["server"]
logging.info(f"Wrote rom file to {romfile}") logging.info(f"Wrote rom file to {romfile}")
adjustedromfile, adjusted = Utils.get_adjuster_settings(romfile) adjustedromfile, adjusted = Utils.get_adjuster_settings(romfile, gui_enabled)
if adjusted: if adjusted:
try: try:
shutil.move(adjustedromfile, romfile) shutil.move(adjustedromfile, romfile)
@@ -875,22 +930,32 @@ async def main():
asyncio.create_task(run_game(adjustedromfile if adjusted else romfile)) asyncio.create_task(run_game(adjustedromfile if adjusted else romfile))
ctx = Context(args.snes, args.connect, args.password) ctx = Context(args.snes, args.connect, args.password)
input_task = asyncio.create_task(console_loop(ctx), name="Input")
if ctx.server_task is None: if ctx.server_task is None:
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
asyncio.create_task(snes_connect(ctx, ctx.snes_address))
if gui_enabled:
input_task = None
from kvui import LttPManager
ctx.ui = LttPManager(ctx)
ui_task = asyncio.create_task(ctx.ui.async_run(), name="UI")
else:
input_task = asyncio.create_task(console_loop(ctx), name="Input")
ui_task = None
snes_connect_task = asyncio.create_task(snes_connect(ctx, ctx.snes_address))
watcher_task = asyncio.create_task(game_watcher(ctx), name="GameWatcher") watcher_task = asyncio.create_task(game_watcher(ctx), name="GameWatcher")
await ctx.exit_event.wait() await ctx.exit_event.wait()
if snes_connect_task:
snes_connect_task.cancel()
ctx.server_address = None ctx.server_address = None
ctx.snes_reconnect_address = None ctx.snes_reconnect_address = None
await watcher_task await watcher_task
if ctx.server is not None and not ctx.server.socket.closed: if ctx.server and not ctx.server.socket.closed:
await ctx.server.socket.close() await ctx.server.socket.close()
if ctx.server_task is not None: if ctx.server_task:
await ctx.server_task await ctx.server_task
if ctx.snes_socket is not None and not ctx.snes_socket.closed: if ctx.snes_socket is not None and not ctx.snes_socket.closed:
@@ -900,7 +965,11 @@ async def main():
ctx.input_queue.put_nowait(None) ctx.input_queue.put_nowait(None)
ctx.input_requests -= 1 ctx.input_requests -= 1
await input_task if ui_task:
await ui_task
if input_task:
input_task.cancel()
if __name__ == '__main__': if __name__ == '__main__':

526
Main.py
View File

@@ -10,18 +10,14 @@ import tempfile
import zipfile import zipfile
from typing import Dict, Tuple from typing import Dict, Tuple
from BaseClasses import MultiWorld, CollectionState, Region, Item from BaseClasses import MultiWorld, CollectionState, Region, RegionType
from worlds.alttp.Items import item_name_groups from worlds.alttp.Items import item_name_groups
from worlds.alttp.Regions import lookup_vanilla_location_to_entrance from worlds.alttp.Regions import lookup_vanilla_location_to_entrance
from worlds.alttp.Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, get_hash_string
from worlds.alttp.Dungeons import fill_dungeons, fill_dungeons_restrictive
from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned
from worlds.alttp.Shops import ShopSlotFill, SHOP_ID_START, total_shop_slots, FillDisabledShopSlots from worlds.alttp.Shops import SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
from worlds.alttp.ItemPool import difficulties, fill_prizes from Utils import output_path, get_options, __version__, version_tuple
from Utils import output_path, parse_player_names, get_options, __version__, version_tuple
from worlds.generic.Rules import locality_rules, exclusion_rules from worlds.generic.Rules import locality_rules, exclusion_rules
from worlds import AutoWorld from worlds import AutoWorld
import Patch
seeddigits = 20 seeddigits = 20
@@ -33,15 +29,6 @@ def get_seed(seed=None):
return seed return seed
def get_same_seed(world: MultiWorld, seed_def: tuple) -> str:
seeds: Dict[tuple, str] = getattr(world, "__named_seeds", {})
if seed_def in seeds:
return seeds[seed_def]
seeds[seed_def] = str(world.random.randint(0, 2 ** 64))
world.__named_seeds = seeds
return seeds[seed_def]
def main(args, seed=None): def main(args, seed=None):
if args.outputpath: if args.outputpath:
os.makedirs(args.outputpath, exist_ok=True) os.makedirs(args.outputpath, exist_ok=True)
@@ -67,7 +54,6 @@ def main(args, seed=None):
world.difficulty = args.difficulty.copy() world.difficulty = args.difficulty.copy()
world.item_functionality = args.item_functionality.copy() world.item_functionality = args.item_functionality.copy()
world.timer = args.timer.copy() world.timer = args.timer.copy()
world.progressive = args.progressive.copy()
world.goal = args.goal.copy() world.goal = args.goal.copy()
world.local_items = args.local_items.copy() world.local_items = args.local_items.copy()
if hasattr(args, "algorithm"): # current GUI options if hasattr(args, "algorithm"): # current GUI options
@@ -80,11 +66,6 @@ def main(args, seed=None):
world.retro = args.retro.copy() world.retro = args.retro.copy()
world.hints = args.hints.copy() world.hints = args.hints.copy()
world.mapshuffle = args.mapshuffle.copy()
world.compassshuffle = args.compassshuffle.copy()
world.keyshuffle = args.keyshuffle.copy()
world.bigkeyshuffle = args.bigkeyshuffle.copy()
world.open_pyramid = args.open_pyramid.copy() world.open_pyramid = args.open_pyramid.copy()
world.boss_shuffle = args.shufflebosses.copy() world.boss_shuffle = args.shufflebosses.copy()
world.enemy_shuffle = args.enemy_shuffle.copy() world.enemy_shuffle = args.enemy_shuffle.copy()
@@ -100,9 +81,7 @@ def main(args, seed=None):
world.blue_clock_time = args.blue_clock_time.copy() world.blue_clock_time = args.blue_clock_time.copy()
world.green_clock_time = args.green_clock_time.copy() world.green_clock_time = args.green_clock_time.copy()
world.shufflepots = args.shufflepots.copy() world.shufflepots = args.shufflepots.copy()
world.progressive = args.progressive.copy()
world.dungeon_counters = args.dungeon_counters.copy() world.dungeon_counters = args.dungeon_counters.copy()
world.glitch_boots = args.glitch_boots.copy()
world.triforce_pieces_available = args.triforce_pieces_available.copy() world.triforce_pieces_available = args.triforce_pieces_available.copy()
world.triforce_pieces_required = args.triforce_pieces_required.copy() world.triforce_pieces_required = args.triforce_pieces_required.copy()
world.shop_shuffle = args.shop_shuffle.copy() world.shop_shuffle = args.shop_shuffle.copy()
@@ -118,48 +97,32 @@ def main(args, seed=None):
world.required_medallions = args.required_medallions.copy() world.required_medallions = args.required_medallions.copy()
world.game = args.game.copy() world.game = args.game.copy()
world.set_options(args) world.set_options(args)
world.player_name = args.name.copy()
world.alttp_rom = args.rom
world.enemizer = args.enemizercli
world.sprite = args.sprite.copy()
world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option. world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
world.slot_seeds = {player: random.Random(world.random.getrandbits(64)) for player in world.slot_seeds = {player: random.Random(world.random.getrandbits(64)) for player in
range(1, world.players + 1)} range(1, world.players + 1)}
AutoWorld.call_all(world, "generate_early")
# system for sharing ER layouts
for player in world.get_game_players("A Link to the Past"):
world.er_seeds[player] = str(world.random.randint(0, 2 ** 64))
if "-" in world.shuffle[player]:
shuffle, seed = world.shuffle[player].split("-", 1)
world.shuffle[player] = shuffle
if shuffle == "vanilla":
world.er_seeds[player] = "vanilla"
elif seed.startswith("group-") or args.race:
world.er_seeds[player] = get_same_seed(world, (
shuffle, seed, world.retro[player], world.mode[player], world.logic[player]))
else: # not a race or group seed, use set seed as is.
world.er_seeds[player] = seed
elif world.shuffle[player] == "vanilla":
world.er_seeds[player] = "vanilla"
logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed) logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed)
logger.info("Found World Types:") logger.info("Found World Types:")
longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types) longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types)
numlength = 8
for name, cls in AutoWorld.AutoWorldRegister.world_types.items(): for name, cls in AutoWorld.AutoWorldRegister.world_types.items():
logger.info(f" {name:{longest_name}}: {len(cls.item_names):3} Items | {len(cls.location_names):3} Locations") if not cls.hidden:
logger.info(f" {name:{longest_name}}: {len(cls.item_names):3} Items | "
f"{len(cls.location_names):3} Locations")
logger.info(f" Item IDs: {min(cls.item_id_to_name):{numlength}} - "
f"{max(cls.item_id_to_name):{numlength}} | "
f"Location IDs: {min(cls.location_id_to_name):{numlength}} - "
f"{max(cls.location_id_to_name):{numlength}}")
parsed_names = parse_player_names(args.names, world.players, args.teams) AutoWorld.call_all(world, "generate_early")
world.teams = len(parsed_names)
for i, team in enumerate(parsed_names, 1):
if world.players > 1:
logger.info('%s%s', 'Team%d: ' % i if world.teams > 1 else 'Players: ', ', '.join(team))
for player, name in enumerate(team, 1):
world.player_names[player].append(name)
logger.info('') logger.info('')
for player in world.get_game_players("A Link to the Past"):
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
for player in world.player_ids: for player in world.player_ids:
for item_name in args.startinventory[player]: for item_name in args.startinventory[player]:
@@ -171,21 +134,6 @@ def main(args, seed=None):
if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]: if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]:
world.local_items[player].add('Triforce Piece') world.local_items[player].add('Triforce Piece')
# dungeon items can't be in non-local if the appropriate dungeon item shuffle setting is not set.
if not world.mapshuffle[player]:
world.non_local_items[player] -= item_name_groups['Maps']
if not world.compassshuffle[player]:
world.non_local_items[player] -= item_name_groups['Compasses']
if not world.keyshuffle[player]:
world.non_local_items[player] -= item_name_groups['Small Keys']
# This could probably use a more elegant solution.
elif world.keyshuffle[player] == True and world.mode[player] == "Standard":
world.local_items[player].add("Small Key (Hyrule Castle)")
if not world.bigkeyshuffle[player]:
world.non_local_items[player] -= item_name_groups['Big Keys']
# Not possible to place pendants/crystals out side of boss prizes yet. # Not possible to place pendants/crystals out side of boss prizes yet.
world.non_local_items[player] -= item_name_groups['Pendants'] world.non_local_items[player] -= item_name_groups['Pendants']
world.non_local_items[player] -= item_name_groups['Crystals'] world.non_local_items[player] -= item_name_groups['Crystals']
@@ -218,309 +166,199 @@ def main(args, seed=None):
distribute_planned(world) distribute_planned(world)
logger.info('Placing Dungeon Prizes.') logger.info('Running Pre Main Fill.')
fill_prizes(world) AutoWorld.call_all(world, "pre_fill")
logger.info('Placing Dungeon Items.')
if world.algorithm in ['balanced', 'vt26'] or any(
list(args.mapshuffle.values()) + list(args.compassshuffle.values()) +
list(args.keyshuffle.values()) + list(args.bigkeyshuffle.values())):
fill_dungeons_restrictive(world)
else:
fill_dungeons(world)
logger.info('Fill the world.') logger.info('Fill the world.')
if world.algorithm == 'flood': if world.algorithm == 'flood':
flood_items(world) # different algo, biased towards early game progress items flood_items(world) # different algo, biased towards early game progress items
elif world.algorithm == 'vt25':
distribute_items_restrictive(world, False)
elif world.algorithm == 'vt26':
distribute_items_restrictive(world, True)
elif world.algorithm == 'balanced': elif world.algorithm == 'balanced':
distribute_items_restrictive(world, True) distribute_items_restrictive(world)
logger.info("Filling Shop Slots") AutoWorld.call_all(world, 'post_fill')
ShopSlotFill(world)
if world.players > 1: if world.players > 1:
balance_multiworld_progression(world) balance_multiworld_progression(world)
logger.info('Generating output files.') logger.info(f'Beginning output...')
outfilebase = 'AP_' + world.seed_name outfilebase = 'AP_' + world.seed_name
rom_names = []
def _gen_rom(team: int, player: int, output_directory:str):
use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player]
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
or world.shufflepots[player] or world.bush_shuffle[player]
or world.killable_thieves[player])
rom = LocalRom(args.rom)
patch_rom(world, rom, player, team, use_enemizer)
if use_enemizer:
patch_enemizer(world, team, player, rom, args.enemizercli, output_directory)
if args.race:
patch_race_rom(rom, world, player)
world.spoiler.hashes[(player, team)] = get_hash_string(rom.hash)
palettes_options = {}
palettes_options['dungeon'] = args.uw_palettes[player]
palettes_options['overworld'] = args.ow_palettes[player]
palettes_options['hud'] = args.hud_palettes[player]
palettes_options['sword'] = args.sword_palettes[player]
palettes_options['shield'] = args.shield_palettes[player]
palettes_options['link'] = args.link_palettes[player]
apply_rom_settings(rom, args.heartbeep[player], args.heartcolor[player], args.quickswap[player],
args.fastmenu[player], args.disablemusic[player], args.sprite[player],
palettes_options, world, player, True,
reduceflashing=args.reduceflashing[player] or args.race,
triforcehud=args.triforcehud[player])
mcsb_name = ''
if all([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player],
world.bigkeyshuffle[player]]):
mcsb_name = '-keysanity'
elif [world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player],
world.bigkeyshuffle[player]].count(True) == 1:
mcsb_name = '-mapshuffle' if world.mapshuffle[player] else \
'-compassshuffle' if world.compassshuffle[player] else \
'-universal_keys' if world.keyshuffle[player] == "universal" else \
'-keyshuffle' if world.keyshuffle[player] else '-bigkeyshuffle'
elif any([world.mapshuffle[player], world.compassshuffle[player], world.keyshuffle[player],
world.bigkeyshuffle[player]]):
mcsb_name = '-%s%s%s%sshuffle' % (
'M' if world.mapshuffle[player] else '', 'C' if world.compassshuffle[player] else '',
'U' if world.keyshuffle[player] == "universal" else 'S' if world.keyshuffle[player] else '',
'B' if world.bigkeyshuffle[player] else '')
outfilepname = f'_P{player}'
outfilepname += f"_{world.player_names[player][team].replace(' ', '_')}" \
if world.player_names[player][team] != 'Player%d' % player else ''
outfilestuffs = {
"logic": world.logic[player], # 0
"difficulty": world.difficulty[player], # 1
"item_functionality": world.item_functionality[player], # 2
"mode": world.mode[player], # 3
"goal": world.goal[player], # 4
"timer": str(world.timer[player]), # 5
"shuffle": world.shuffle[player], # 6
"algorithm": world.algorithm, # 7
"mscb": mcsb_name, # 8
"retro": world.retro[player], # 9
"progressive": world.progressive, # A
"hints": 'True' if world.hints[player] else 'False' # B
}
# 0 1 2 3 4 5 6 7 8 9 A B
outfilesuffix = ('_%s_%s-%s-%s-%s%s_%s-%s%s%s%s%s' % (
# 0 1 2 3 4 5 6 7 8 9 A B C
# _noglitches_normal-normal-open-ganon-ohko_simple-balanced-keysanity-retro-prog_random-nohints
# _noglitches_normal-normal-open-ganon _simple-balanced-keysanity-retro
# _noglitches_normal-normal-open-ganon _simple-balanced-keysanity -prog_random
# _noglitches_normal-normal-open-ganon _simple-balanced-keysanity -nohints
outfilestuffs["logic"], # 0
outfilestuffs["difficulty"], # 1
outfilestuffs["item_functionality"], # 2
outfilestuffs["mode"], # 3
outfilestuffs["goal"], # 4
"" if outfilestuffs["timer"] in ['False', 'none', 'display'] else "-" + outfilestuffs["timer"], # 5
outfilestuffs["shuffle"], # 6
outfilestuffs["algorithm"], # 7
outfilestuffs["mscb"], # 8
"-retro" if outfilestuffs["retro"] == "True" else "", # 9
"-prog_" + outfilestuffs["progressive"] if outfilestuffs["progressive"] in ['off', 'random'] else "", # A
"-nohints" if not outfilestuffs["hints"] == "True" else "") # B
) if not args.outputname else ''
rompath = os.path.join(output_directory, f'{outfilebase}{outfilepname}{outfilesuffix}.sfc')
rom.write_to_file(rompath, hide_enemizer=True)
Patch.create_patch_file(rompath, player=player, player_name=world.player_names[player][team])
os.unlink(rompath)
return player, team, bytes(rom.name)
pool = concurrent.futures.ThreadPoolExecutor()
output = tempfile.TemporaryDirectory() output = tempfile.TemporaryDirectory()
with output as temp_dir: with output as temp_dir:
check_accessibility_task = pool.submit(world.fulfills_accessibility) with concurrent.futures.ThreadPoolExecutor() as pool:
rom_futures = [] check_accessibility_task = pool.submit(world.fulfills_accessibility)
output_file_futures = []
for team in range(world.teams):
for player in world.get_game_players("A Link to the Past"):
rom_futures.append(pool.submit(_gen_rom, team, player, temp_dir))
for player in world.player_ids:
output_file_futures.append(pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
def get_entrance_to_region(region: Region): output_file_futures = []
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 for player in world.player_ids:
er_hint_data = {player: {} for player in world.get_game_players("A Link to the Past") if # skip starting a thread for methods that say "pass".
world.shuffle[player] != "vanilla" or world.retro[player]} if AutoWorld.World.generate_output.__code__ is not world.worlds[player].generate_output.__code__:
from worlds.alttp.Regions import RegionType output_file_futures.append(pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
for region in world.regions: output_file_futures.append(pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir))
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
ordered_areas = ('Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace', def get_entrance_to_region(region: Region):
'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace', for entrance in region.entrances:
'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total") 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)
checks_in_area = {player: {area: list() for area in ordered_areas} # collect ER hint info
for player in range(1, world.players + 1)} er_hint_data = {player: {} for player in world.get_game_players("A Link to the Past") if
world.shuffle[player] != "vanilla" or world.retro[player]}
for player in range(1, world.players + 1): for region in world.regions:
checks_in_area[player]["Total"] = 0 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
for location in [loc for loc in world.get_filled_locations() if type(loc.address) is int]: ordered_areas = ('Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace',
main_entrance = get_entrance_to_region(location.parent_region) 'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace',
if location.game != "A Link to the Past": 'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total")
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 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 = [] checks_in_area = {player: {area: list() for area in ordered_areas}
takeanyregions = ["Old Man Sword Cave", "Take-Any #1", "Take-Any #2", "Take-Any #3", "Take-Any #4"] for player in range(1, world.players + 1)}
for index, take_any in enumerate(takeanyregions):
for region in [world.get_region(take_any, player) for player in range(1, world.players + 1) if
world.retro[player]]:
item = world.create_item(region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'],
region.player)
player = region.player
location_id = SHOP_ID_START + total_shop_slots + index
main_entrance = get_entrance_to_region(region) for player in range(1, world.players + 1):
if main_entrance.parent_region.type == RegionType.LightWorld: checks_in_area[player]["Total"] = 0
checks_in_area[player]["Light World"].append(location_id)
else:
checks_in_area[player]["Dark World"].append(location_id)
checks_in_area[player]["Total"] += 1
er_hint_data[player][location_id] = main_entrance.name
oldmancaves.append(((location_id, player), (item.code, player)))
FillDisabledShopSlots(world)
def write_multidata(roms, outputs):
import base64
import NetUtils
for future in roms:
rom_name = future.result()
rom_names.append(rom_name)
slot_data = {}
client_versions = {}
minimum_versions = {"server": (0, 1, 1), "clients": client_versions}
games = {}
for slot in world.player_ids:
client_versions[slot] = world.worlds[slot].get_required_client_version()
games[slot] = world.game[slot]
connect_names = {base64.b64encode(rom_name).decode(): (team, slot) for
slot, team, rom_name in rom_names}
precollected_items = {player: [] for player in range(1, world.players + 1)}
for item in world.precollected_items:
precollected_items[item.player].append(item.code)
precollected_hints = {player: set() for player in range(1, world.players + 1)}
# for now special case Factorio tech_tree_information
sending_visible_players = set()
for player in world.get_game_players("Factorio"):
if world.tech_tree_information[player].value == 2:
sending_visible_players.add(player)
for i, team in enumerate(parsed_names):
for player, name in enumerate(team, 1):
if player not in world.get_game_players("A Link to the Past"):
connect_names[name] = (i, player)
for slot in world.player_ids:
slot_data[slot] = world.worlds[slot].fill_slot_data()
locations_data: Dict[int, Dict[int, Tuple[int, int]]] = {player: {} for player in world.player_ids}
for location in world.get_filled_locations(): for location in world.get_filled_locations():
if type(location.address) == int: if type(location.address) is int:
locations_data[location.player][location.address] = location.item.code, location.item.player main_entrance = get_entrance_to_region(location.parent_region)
if location.player in sending_visible_players and location.item.player != location.player: if location.game != "A Link to the Past":
hint = NetUtils.Hint(location.item.player, location.player, location.address, checks_in_area[location.player]["Light World"].append(location.address)
location.item.code, False) elif location.parent_region.dungeon:
precollected_hints[location.player].add(hint) dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
precollected_hints[location.item.player].add(hint) 'Inverted Ganons Tower': 'Ganons Tower'} \
elif location.item.name in args.start_hints[location.item.player]: .get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
hint = NetUtils.Hint(location.item.player, location.player, location.address, checks_in_area[location.player][dungeonname].append(location.address)
location.item.code, False, elif main_entrance.parent_region.type == RegionType.LightWorld:
er_hint_data.get(location.player, {}).get(location.address, "")) checks_in_area[location.player]["Light World"].append(location.address)
precollected_hints[location.player].add(hint) elif main_entrance.parent_region.type == RegionType.DarkWorld:
precollected_hints[location.item.player].add(hint) checks_in_area[location.player]["Dark World"].append(location.address)
checks_in_area[location.player]["Total"] += 1
multidata = zlib.compress(pickle.dumps({ oldmancaves = []
"slot_data": slot_data, takeanyregions = ["Old Man Sword Cave", "Take-Any #1", "Take-Any #2", "Take-Any #3", "Take-Any #4"]
"games": games, for index, take_any in enumerate(takeanyregions):
"names": parsed_names, for region in [world.get_region(take_any, player) for player in range(1, world.players + 1) if
"connect_names": connect_names, world.retro[player]]:
"remote_items": {player for player in world.player_ids if item = world.create_item(region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'],
world.worlds[player].remote_items}, region.player)
"locations": locations_data, player = region.player
"checks_in_area": checks_in_area, location_id = SHOP_ID_START + total_shop_slots + index
"server_options": get_options()["server_options"],
"er_hint_data": er_hint_data,
"precollected_items": precollected_items,
"precollected_hints": precollected_hints,
"version": tuple(version_tuple),
"tags": ["AP"],
"minimum_versions": minimum_versions,
"seed_name": world.seed_name
}), 9)
with open(os.path.join(temp_dir, '%s.archipelago' % outfilebase), 'wb') as f: main_entrance = get_entrance_to_region(region)
f.write(bytes([1])) # version of format if main_entrance.parent_region.type == RegionType.LightWorld:
f.write(multidata) checks_in_area[player]["Light World"].append(location_id)
for future in outputs: else:
future.result() # collect errors if they occured checks_in_area[player]["Dark World"].append(location_id)
checks_in_area[player]["Total"] += 1
er_hint_data[player][location_id] = main_entrance.name
oldmancaves.append(((location_id, player), (item.code, player)))
FillDisabledShopSlots(world)
def write_multidata():
import NetUtils
slot_data = {}
client_versions = {}
minimum_versions = {"server": (0, 1, 1), "clients": client_versions}
games = {}
for slot in world.player_ids:
client_versions[slot] = world.worlds[slot].get_required_client_version()
games[slot] = world.game[slot]
precollected_items = {player: [] for player in range(1, world.players + 1)}
for item in world.precollected_items:
precollected_items[item.player].append(item.code)
precollected_hints = {player: set() for player in range(1, world.players + 1)}
# for now special case Factorio tech_tree_information
sending_visible_players = set()
for player in world.get_game_players("Factorio"):
if world.tech_tree_information[player].value == 2:
sending_visible_players.add(player)
for slot in world.player_ids:
slot_data[slot] = world.worlds[slot].fill_slot_data()
locations_data: Dict[int, Dict[int, Tuple[int, int]]] = {player: {} for player in world.player_ids}
for location in world.get_filled_locations():
if type(location.address) == int:
# item code None should be event, location.address should then also be None
assert location.item.code is not None
locations_data[location.player][location.address] = location.item.code, location.item.player
if location.player in sending_visible_players and location.item.player != location.player:
hint = NetUtils.Hint(location.item.player, location.player, location.address,
location.item.code, False)
precollected_hints[location.player].add(hint)
precollected_hints[location.item.player].add(hint)
elif location.item.name in args.start_hints[location.item.player]:
hint = NetUtils.Hint(location.item.player, location.player, location.address,
location.item.code, False,
er_hint_data.get(location.player, {}).get(location.address, ""))
precollected_hints[location.player].add(hint)
precollected_hints[location.item.player].add(hint)
multidata = {
"slot_data": slot_data,
"games": games,
"names": [[name for player, name in sorted(world.player_name.items())]],
"connect_names": {name: (0, player) for player, name in world.player_name.items()},
"remote_items": {player for player in world.player_ids if
world.worlds[player].remote_items},
"locations": locations_data,
"checks_in_area": checks_in_area,
"server_options": get_options()["server_options"],
"er_hint_data": er_hint_data,
"precollected_items": precollected_items,
"precollected_hints": precollected_hints,
"version": tuple(version_tuple),
"tags": ["AP"],
"minimum_versions": minimum_versions,
"seed_name": world.seed_name
}
AutoWorld.call_all(world, "modify_multidata", multidata)
multidata = zlib.compress(pickle.dumps(multidata), 9)
with open(os.path.join(temp_dir, f'{outfilebase}.archipelago'), 'wb') as f:
f.write(bytes([1])) # version of format
f.write(multidata)
multidata_task = pool.submit(write_multidata)
if not check_accessibility_task.result():
if not world.can_beat_game():
raise Exception("Game appears as unbeatable. Aborting.")
else:
logger.warning("Location Accessibility requirements not fulfilled.")
# retrieve exceptions via .result() if they occured.
if multidata_task:
multidata_task.result()
for i, future in enumerate(concurrent.futures.as_completed(output_file_futures)):
if i % 10 == 0:
logger.info(f'Generating output files ({i}/{len(output_file_futures)}).')
future.result()
multidata_task = pool.submit(write_multidata, rom_futures, output_file_futures)
if not check_accessibility_task.result():
if not world.can_beat_game():
raise Exception("Game appears as unbeatable. Aborting.")
else:
logger.warning("Location Accessibility requirements not fulfilled.")
if multidata_task:
multidata_task.result() # retrieve exception if one exists
pool.shutdown() # wait for all queued tasks to complete
if not args.skip_playthrough: if not args.skip_playthrough:
logger.info('Calculating playthrough.') logger.info('Calculating playthrough.')
create_playthrough(world) create_playthrough(world)
if args.create_spoiler: # needs spoiler.hashes to be filled, that depend on rom_futures being done
if args.create_spoiler:
world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase)) world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase))
zipfilename = output_path(f"AP_{world.seed_name}.zip") zipfilename = output_path(f"AP_{world.seed_name}.zip")
logger.info(f'Creating final archive at {zipfilename}.') logger.info(f'Creating final archive at {zipfilename}.')
with zipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED, with zipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED,
compresslevel=9) as zf: compresslevel=9) as zf:
for file in os.scandir(temp_dir): for file in os.scandir(temp_dir):
zf.write(os.path.join(temp_dir, file), arcname=file.name) zf.write(file.path, arcname=file.name)
logger.info('Done. Enjoy. Total Time: %s', time.perf_counter() - start) logger.info('Done. Enjoy. Total Time: %s', time.perf_counter() - start)
return world return world
@@ -536,7 +374,6 @@ def create_playthrough(world):
sphere_candidates = set(prog_locations) sphere_candidates = set(prog_locations)
logging.debug('Building up collection spheres.') logging.debug('Building up collection spheres.')
while sphere_candidates: while sphere_candidates:
state.sweep_for_events(key_only=True)
# build up spheres of collection radius. # build up spheres of collection radius.
# Everything in each sphere is independent from each other in dependencies and only depends on lower spheres # Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
@@ -641,18 +478,15 @@ def create_playthrough(world):
{str(location): get_path(state, location.parent_region) for sphere in collection_spheres for location in {str(location): get_path(state, location.parent_region) for sphere in collection_spheres for location in
sphere if location.player == player}) sphere if location.player == player})
if player in world.get_game_players("A Link to the Past"): if player in world.get_game_players("A Link to the Past"):
for path in dict(world.spoiler.paths).values(): # If Pyramid Fairy Entrance needs to be reached, also path to Big Bomb Shop
if any(exit == 'Pyramid Fairy' for (_, exit) in path): # Maybe move the big bomb over to the Event system instead?
if world.mode[player] != 'inverted': if any(exit_path == 'Pyramid Fairy' for path in world.spoiler.paths.values() for (_, exit_path) in path):
world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = get_path(state, if world.mode[player] != 'inverted':
world.get_region( world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = \
'Big Bomb Shop', get_path(state, world.get_region('Big Bomb Shop', player))
player)) else:
else: world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = \
world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = get_path(state, get_path(state, world.get_region('Inverted Big Bomb Shop', player))
world.get_region(
'Inverted Big Bomb Shop',
player))
# we can finally output our playthrough # we can finally output our playthrough
world.spoiler.playthrough = {"0": sorted([str(item) for item in world.precollected_items if item.advancement])} world.spoiler.playthrough = {"0": sorted([str(item) for item in world.precollected_items if item.advancement])}

185
MinecraftClient.py Normal file
View File

@@ -0,0 +1,185 @@
import argparse
import os, sys
import re
import atexit
from subprocess import Popen
from shutil import copyfile
from base64 import b64decode
from time import strftime
import requests
import Utils
atexit.register(input, "Press enter to exit.")
# 1 or more digits followed by m or g, then optional b
max_heap_re = re.compile(r"^\d+[mMgG][bB]?$")
def prompt_yes_no(prompt):
yes_inputs = {'yes', 'ye', 'y'}
no_inputs = {'no', 'n'}
while True:
choice = input(prompt + " [y/n] ").lower()
if choice in yes_inputs:
return True
elif choice in no_inputs:
return False
else:
print('Please respond with "y" or "n".')
# Find Forge jar file; raise error if not found
def find_forge_jar(forge_dir):
for entry in os.scandir(forge_dir):
if ".jar" in entry.name and "forge" in entry.name:
print(f"Found forge .jar: {entry.name}")
return entry.name
raise FileNotFoundError(f"Could not find forge .jar in {forge_dir}.")
# Create mods folder if needed; find AP randomizer jar; return None if not found.
def find_ap_randomizer_jar(forge_dir):
mods_dir = os.path.join(forge_dir, 'mods')
if os.path.isdir(mods_dir):
ap_mod_re = re.compile(r"^aprandomizer-[\d\.]+\.jar$")
for entry in os.scandir(mods_dir):
match = ap_mod_re.match(entry.name)
if match:
print(f"Found AP randomizer mod: {match.group()}")
return match.group()
return None
else:
os.mkdir(mods_dir)
print(f"Created mods folder in {forge_dir}")
return None
# Create APData folder if needed; clean .apmc files from APData; copy given .apmc into directory.
def replace_apmc_files(forge_dir, apmc_file):
if apmc_file is None:
return
apdata_dir = os.path.join(forge_dir, 'APData')
copy_apmc = True
if not os.path.isdir(apdata_dir):
os.mkdir(apdata_dir)
print(f"Created APData folder in {forge_dir}")
for entry in os.scandir(apdata_dir):
if entry.name.endswith(".apmc") and entry.is_file():
if not os.path.samefile(apmc_file, entry.path):
os.remove(entry.path)
print(f"Removed {entry.name} in {apdata_dir}")
else: # apmc already in apdata
copy_apmc = False
if copy_apmc:
copyfile(apmc_file, os.path.join(apdata_dir, os.path.basename(apmc_file)))
print(f"Copied {os.path.basename(apmc_file)} to {apdata_dir}")
# Check mod version, download new mod from GitHub releases page if needed.
def update_mod(forge_dir):
ap_randomizer = find_ap_randomizer_jar(forge_dir)
client_releases_endpoint = "https://api.github.com/repos/KonoTyran/Minecraft_AP_Randomizer/releases"
resp = requests.get(client_releases_endpoint)
if resp.status_code == 200: # OK
latest_release = resp.json()[0]
if ap_randomizer != latest_release['assets'][0]['name']:
print(f"A new release of the Minecraft AP randomizer mod was found: {latest_release['assets'][0]['name']}")
if ap_randomizer is not None:
print(f"Your current mod is {ap_randomizer}.")
else:
print(f"You do not have the AP randomizer mod installed.")
if prompt_yes_no("Would you like to update?"):
old_ap_mod = os.path.join(forge_dir, 'mods', ap_randomizer) if ap_randomizer is not None else None
new_ap_mod = os.path.join(forge_dir, 'mods', latest_release['assets'][0]['name'])
print("Downloading AP randomizer mod. This may take a moment...")
apmod_resp = requests.get(latest_release['assets'][0]['browser_download_url'])
if apmod_resp.status_code == 200:
with open(new_ap_mod, 'wb') as f:
f.write(apmod_resp.content)
print(f"Wrote new mod file to {new_ap_mod}")
if old_ap_mod is not None:
os.remove(old_ap_mod)
print(f"Removed old mod file from {old_ap_mod}")
else:
print(f"Error retrieving the randomizer mod (status code {apmod_resp.status_code}).")
print(f"Please report this issue on the Archipelago Discord server.")
sys.exit(1)
else:
print(f"Error checking for randomizer mod updates (status code {resp.status_code}).")
print(f"If this was not expected, please report this issue on the Archipelago Discord server.")
if not prompt_yes_no("Continue anyways?"):
sys.exit(0)
# Check if the EULA is agreed to, and prompt the user to read and agree if necessary.
def check_eula(forge_dir):
eula_path = os.path.join(forge_dir, "eula.txt")
if not os.path.isfile(eula_path):
# Create eula.txt
with open(eula_path, 'w') as f:
f.write("#By changing the setting below to TRUE you are indicating your agreement to our EULA (https://account.mojang.com/documents/minecraft_eula).\n")
f.write(f"#{strftime('%a %b %d %X %Z %Y')}\n")
f.write("eula=false\n")
with open(eula_path, 'r+') as f:
text = f.read()
if 'false' in text:
# Prompt user to agree to the EULA
print("You need to agree to the Minecraft EULA in order to run the server.")
print("The EULA can be found at https://account.mojang.com/documents/minecraft_eula")
if prompt_yes_no("Do you agree to the EULA?"):
f.seek(0)
f.write(text.replace('false', 'true'))
f.truncate()
print(f"Set {eula_path} to true")
else:
sys.exit(0)
# Run the Forge server. Return process object
def run_forge_server(forge_dir, heap_arg):
forge_server = find_forge_jar(forge_dir)
java_exe = os.path.abspath(os.path.join('jre8', 'bin', 'java.exe'))
if not os.path.isfile(java_exe):
java_exe = "java" # try to fall back on java in the PATH
heap_arg = max_heap_re.match(max_heap).group()
if heap_arg[-1] in ['b', 'B']:
heap_arg = heap_arg[:-1]
heap_arg = "-Xmx" + heap_arg
argstring = ' '.join([java_exe, heap_arg, "-jar", forge_server, "-nogui"])
print(f"Running Forge server: {argstring}")
os.chdir(forge_dir)
return Popen(argstring)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument("apmc_file", default=None, nargs='?', help="Path to an Archipelago Minecraft data file (.apmc)")
args = parser.parse_args()
apmc_file = os.path.abspath(args.apmc_file) if args.apmc_file else None
# Change to executable's working directory
os.chdir(os.path.abspath(os.path.dirname(sys.argv[0])))
options = Utils.get_options()
forge_dir = options["minecraft_options"]["forge_directory"]
max_heap = options["minecraft_options"]["max_heap_size"]
if apmc_file is not None and not os.path.isfile(apmc_file):
raise FileNotFoundError(f"Path {apmc_file} does not exist or could not be accessed.")
if not os.path.isdir(forge_dir):
raise NotADirectoryError(f"Path {forge_dir} does not exist or could not be accessed.")
if not max_heap_re.match(max_heap):
raise Exception(f"Max heap size {max_heap} in incorrect format. Use a number followed by M or G, e.g. 512M or 2G.")
update_mod(forge_dir)
replace_apmc_files(forge_dir, apmc_file)
check_eula(forge_dir)
server_process = run_forge_server(forge_dir, max_heap)
server_process.wait()

View File

@@ -23,10 +23,13 @@ def update_command():
subprocess.call([sys.executable, '-m', 'pip', 'install', '-r', file, '--upgrade']) subprocess.call([sys.executable, '-m', 'pip', 'install', '-r', file, '--upgrade'])
def update(): def update(yes = False, force = False):
global update_ran global update_ran
if not update_ran: if not update_ran:
update_ran = True update_ran = True
if force:
update_command()
return
for req_file in requirements_files: for req_file in requirements_files:
path = os.path.join(os.path.dirname(sys.argv[0]), req_file) path = os.path.join(os.path.dirname(sys.argv[0]), req_file)
if not os.path.exists(path): if not os.path.exists(path):
@@ -38,12 +41,19 @@ def update():
try: try:
pkg_resources.require(requirement) pkg_resources.require(requirement)
except pkg_resources.ResolutionError: except pkg_resources.ResolutionError:
import traceback if not yes:
traceback.print_exc() import traceback
input(f'Requirement {requirement} is not satisfied, press enter to install it') traceback.print_exc()
input(f'Requirement {requirement} is not satisfied, press enter to install it')
update_command() update_command()
return return
if __name__ == "__main__": if __name__ == "__main__":
update() import argparse
parser = argparse.ArgumentParser(description='Install archipelago requirements')
parser.add_argument('-y', '--yes', dest='yes', action='store_true', help='answer "yes" to all questions')
parser.add_argument('-f', '--force', dest='force', action='store_true', help='force update')
args = parser.parse_args()
update(args.yes, args.force)

View File

@@ -31,10 +31,11 @@ from worlds import network_data_package, lookup_any_item_id_to_name, lookup_any_
import Utils import Utils
from Utils import get_item_name_from_id, get_location_name_from_id, \ from Utils import get_item_name_from_id, get_location_name_from_id, \
version_tuple, restricted_loads, Version version_tuple, restricted_loads, Version
from NetUtils import Node, Endpoint, ClientStatus, NetworkItem, decode, NetworkPlayer from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer
colorama.init() colorama.init()
class Client(Endpoint): class Client(Endpoint):
version = Version(0, 0, 0) version = Version(0, 0, 0)
tags: typing.List[str] = [] tags: typing.List[str] = []
@@ -50,9 +51,14 @@ class Client(Endpoint):
self.messageprocessor = client_message_processor(ctx, self) self.messageprocessor = client_message_processor(ctx, self)
self.ctx = weakref.ref(ctx) self.ctx = weakref.ref(ctx)
team_slot = typing.Tuple[int, int] team_slot = typing.Tuple[int, int]
class Context(Node):
class Context:
dumper = staticmethod(encode)
loader = staticmethod(decode)
simple_options = {"hint_cost": int, simple_options = {"hint_cost": int,
"location_check_points": int, "location_check_points": int,
"server_password": str, "server_password": str,
@@ -64,8 +70,10 @@ class Context(Node):
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int, 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", remaining_mode: str = "disabled", hint_cost: int, item_cheat: bool, forfeit_mode: str = "disabled", remaining_mode: str = "disabled",
auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2): auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2, log_network: bool = False):
super(Context, self).__init__() super(Context, self).__init__()
self.log_network = log_network
self.endpoints = []
self.compatibility: int = compatibility self.compatibility: int = compatibility
self.shutdown_task = None self.shutdown_task = None
self.data_filename = None self.data_filename = None
@@ -111,17 +119,88 @@ class Context(Node):
self.games: typing.Dict[int, str] = {} self.games: typing.Dict[int, str] = {}
self.minimum_client_versions: typing.Dict[int, Utils.Version] = {} self.minimum_client_versions: typing.Dict[int, Utils.Version] = {}
self.seed_name = "" self.seed_name = ""
self.random = random.Random()
def get_hint_cost(self, slot): # General networking
if self.hint_cost:
return max(0, int(self.hint_cost * 0.01 * len(self.locations[slot]))) async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool:
return 0 if not endpoint.socket or not endpoint.socket.open:
return False
msg = self.dumper(msgs)
try:
await endpoint.socket.send(msg)
except websockets.ConnectionClosed:
logging.exception(f"Exception during send_msgs, could not send {msg}")
await self.disconnect(endpoint)
else:
if self.log_network:
logging.info(f"Outgoing message: {msg}")
return True
async def send_encoded_msgs(self, endpoint: Endpoint, msg: str) -> bool:
if not endpoint.socket or not endpoint.socket.open:
return False
try:
await endpoint.socket.send(msg)
except websockets.ConnectionClosed:
logging.exception("Exception during send_encoded_msgs")
await self.disconnect(endpoint)
else:
if self.log_network:
logging.info(f"Outgoing message: {msg}")
return True
def broadcast_all(self, msgs):
msgs = self.dumper(msgs)
for endpoint in self.endpoints:
if endpoint.auth:
asyncio.create_task(self.send_encoded_msgs(endpoint, msgs))
def broadcast_team(self, team, msgs):
msgs = self.dumper(msgs)
for client in self.endpoints:
if client.auth and client.team == team:
asyncio.create_task(self.send_encoded_msgs(client, msgs))
async def disconnect(self, endpoint):
if endpoint in self.endpoints:
self.endpoints.remove(endpoint)
await on_client_disconnected(self, endpoint)
# text
def notify_all(self, text):
logging.info("Notice (all): %s" % text)
self.broadcast_all([{"cmd": "Print", "text": 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}]))
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]))
# loading
def load(self, multidatapath: str, use_embedded_server_options: bool = False): def load(self, multidatapath: str, use_embedded_server_options: bool = False):
with open(multidatapath, 'rb') as f: if multidatapath.lower().endswith(".zip"):
data = f.read() import zipfile
with zipfile.ZipFile(multidatapath) as zf:
for file in zf.namelist():
if file.endswith(".archipelago"):
data = zf.read(file)
break
else:
raise Exception("No .archipelago found in archive.")
else:
with open(multidatapath, 'rb') as f:
data = f.read()
self._load(self._decompress(data), use_embedded_server_options) self._load(self._decompress(data), use_embedded_server_options)
self.data_filename = multidatapath self.data_filename = multidatapath
@staticmethod @staticmethod
@@ -147,6 +226,7 @@ class Context(Node):
self.player_names[team, player] = name self.player_names[team, player] = name
self.player_name_lookup[name] = team, player self.player_name_lookup[name] = team, player
self.seed_name = decoded_obj["seed_name"] self.seed_name = decoded_obj["seed_name"]
self.random.seed(self.seed_name)
self.connect_names = decoded_obj['connect_names'] self.connect_names = decoded_obj['connect_names']
self.remote_items = decoded_obj['remote_items'] self.remote_items = decoded_obj['remote_items']
self.locations = decoded_obj['locations'] self.locations = decoded_obj['locations']
@@ -165,27 +245,7 @@ class Context(Node):
server_options = decoded_obj.get("server_options", {}) server_options = decoded_obj.get("server_options", {})
self._set_options(server_options) self._set_options(server_options)
def get_players_package(self): # saving
return [NetworkPlayer(t, p, self.get_aliased_name(t, p), n) for (t, p), n in self.player_names.items()]
def _set_options(self, server_options: dict):
for key, value in server_options.items():
data_type = self.simple_options.get(key, None)
if data_type is not None:
if value not in {False, True, None}: # some can be boolean OR text, such as password
try:
value = data_type(value)
except Exception as e:
try:
raise Exception(f"Could not set server option {key}, skipping.") from e
except Exception as e:
logging.exception(e)
logging.debug(f"Setting server option {key} to {value} from supplied multidata")
setattr(self, key, value)
elif key == "disable_item_cheat":
self.item_cheat = not bool(value)
else:
logging.debug(f"Unrecognized server option {key}")
def save(self, now=False) -> bool: def save(self, now=False) -> bool:
if self.saving: if self.saving:
@@ -213,8 +273,10 @@ class Context(Node):
self.saving = enabled self.saving = enabled
if self.saving: if self.saving:
if not self.save_filename: if not self.save_filename:
self.save_filename = (self.data_filename[:-11] if self.data_filename.endswith('.archipelago') else ( import os
self.data_filename + '_')) + 'apsave' name, ext = os.path.splitext(self.data_filename)
self.save_filename = name + '.apsave' if ext.lower() in ('.archipelago','.zip') \
else self.data_filename + '_' + 'apsave'
try: try:
with open(self.save_filename, 'rb') as f: with open(self.save_filename, 'rb') as f:
save_data = restricted_loads(zlib.decompress(f.read())) save_data = restricted_loads(zlib.decompress(f.read()))
@@ -242,13 +304,6 @@ class Context(Node):
import atexit import atexit
atexit.register(self._save, True) # make sure we save on exit too atexit.register(self._save, True) # make sure we save on exit too
def recheck_hints(self):
for team, slot in self.hints:
self.hints[team, slot] = {
hint.re_check(self, team) for hint in
self.hints[team, slot]
}
def get_save(self) -> dict: def get_save(self) -> dict:
self.recheck_hints() self.recheck_hints()
d = { d = {
@@ -263,6 +318,7 @@ class Context(Node):
(key, value.timestamp()) for key, value in self.client_activity_timers.items()), (key, value.timestamp()) for key, value in self.client_activity_timers.items()),
"client_connection_timers": tuple( "client_connection_timers": tuple(
(key, value.timestamp()) for key, value in self.client_connection_timers.items()), (key, value.timestamp()) for key, value in self.client_connection_timers.items()),
"random_state": self.random.getstate()
} }
return d return d
@@ -283,47 +339,53 @@ class Context(Node):
{tuple(key): datetime.datetime.fromtimestamp(value, datetime.timezone.utc) for key, value {tuple(key): datetime.datetime.fromtimestamp(value, datetime.timezone.utc) for key, value
in savedata["client_activity_timers"]}) in savedata["client_activity_timers"]})
self.location_checks.update(savedata["location_checks"]) self.location_checks.update(savedata["location_checks"])
if "random_state" in savedata:
self.random.setstate(savedata["random_state"])
logging.info(f'Loaded save file with {sum([len(p) for p in self.received_items.values()])} received items ' logging.info(f'Loaded save file with {sum([len(p) for p in self.received_items.values()])} received items '
f'for {len(self.received_items)} players') f'for {len(self.received_items)} players')
# rest
def get_hint_cost(self, slot):
if self.hint_cost:
return max(0, int(self.hint_cost * 0.01 * len(self.locations[slot])))
return 0
def recheck_hints(self):
for team, slot in self.hints:
self.hints[team, slot] = {
hint.re_check(self, team) for hint in
self.hints[team, slot]
}
def get_players_package(self):
return [NetworkPlayer(t, p, self.get_aliased_name(t, p), n) for (t, p), n in self.player_names.items()]
def _set_options(self, server_options: dict):
for key, value in server_options.items():
data_type = self.simple_options.get(key, None)
if data_type is not None:
if value not in {False, True, None}: # some can be boolean OR text, such as password
try:
value = data_type(value)
except Exception as e:
try:
raise Exception(f"Could not set server option {key}, skipping.") from e
except Exception as e:
logging.exception(e)
logging.debug(f"Setting server option {key} to {value} from supplied multidata")
setattr(self, key, value)
elif key == "disable_item_cheat":
self.item_cheat = not bool(value)
else:
logging.debug(f"Unrecognized server option {key}")
def get_aliased_name(self, team: int, slot: int): def get_aliased_name(self, team: int, slot: int):
if (team, slot) in self.name_aliases: if (team, slot) in self.name_aliases:
return f"{self.name_aliases[team, slot]} ({self.player_names[team, slot]})" return f"{self.name_aliases[team, slot]} ({self.player_names[team, slot]})"
else: else:
return self.player_names[team, slot] return self.player_names[team, slot]
def notify_all(self, text):
logging.info("Notice (all): %s" % text)
self.broadcast_all([{"cmd": "Print", "text": 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}]))
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]))
def broadcast_team(self, team, msgs):
msgs = self.dumper(msgs)
for client in self.endpoints:
if client.auth and client.team == team:
asyncio.create_task(self.send_encoded_msgs(client, msgs))
def broadcast_all(self, msgs):
msgs = self.dumper(msgs)
for endpoint in self.endpoints:
if endpoint.auth:
asyncio.create_task(self.send_encoded_msgs(endpoint, msgs))
async def disconnect(self, endpoint):
await super(Context, self).disconnect(endpoint)
await on_client_disconnected(self, endpoint)
def notify_hints(ctx: Context, team: int, hints: typing.List[NetUtils.Hint]): def notify_hints(ctx: Context, team: int, hints: typing.List[NetUtils.Hint]):
concerns = collections.defaultdict(list) concerns = collections.defaultdict(list)
@@ -409,7 +471,8 @@ async def on_client_joined(ctx: Context, client: Client):
update_client_status(ctx, client, ClientStatus.CLIENT_CONNECTED) update_client_status(ctx, client, ClientStatus.CLIENT_CONNECTED)
version_str = '.'.join(str(x) for x in client.version) version_str = '.'.join(str(x) for x in client.version)
ctx.notify_all( ctx.notify_all(
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has joined the game. " f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) "
f"playing {ctx.games[client.slot]} has joined. "
f"Client({version_str}), {client.tags}).") f"Client({version_str}), {client.tags}).")
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc) ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
@@ -884,15 +947,18 @@ class ClientMessageProcessor(CommonCommandProcessor):
@mark_raw @mark_raw
def _cmd_hint(self, item_or_location: str = "") -> bool: def _cmd_hint(self, item_or_location: str = "") -> bool:
"""Use !hint {item_name/location_name}, for example !hint Lamp or !hint Link's House. """ """Use !hint {item_name/location_name},
for example !hint Lamp or !hint Link's House to get a spoiler peek for that location or item.
If hint costs are on, this will only give you one new result,
you can rerun the command to get more in that case."""
points_available = get_client_points(self.ctx, self.client) points_available = get_client_points(self.ctx, self.client)
if not item_or_location: if not item_or_location:
self.output(f"A hint costs {self.ctx.get_hint_cost(self.client.slot)} points. "
f"You have {points_available} points.")
hints = {hint.re_check(self.ctx, self.client.team) for hint in hints = {hint.re_check(self.ctx, self.client.team) for hint in
self.ctx.hints[self.client.team, self.client.slot]} self.ctx.hints[self.client.team, self.client.slot]}
self.ctx.hints[self.client.team, self.client.slot] = hints self.ctx.hints[self.client.team, self.client.slot] = hints
notify_hints(self.ctx, self.client.team, list(hints)) notify_hints(self.ctx, self.client.team, list(hints))
self.output(f"A hint costs {self.ctx.get_hint_cost(self.client.slot)} points. "
f"You have {points_available} points.")
return True return True
else: else:
world = proxy_worlds[self.ctx.games[self.client.slot]] world = proxy_worlds[self.ctx.games[self.client.slot]]
@@ -924,11 +990,11 @@ class ClientMessageProcessor(CommonCommandProcessor):
if not not_found_hints: # everything's been found, no need to pay if not not_found_hints: # everything's been found, no need to pay
can_pay = 1000 can_pay = 1000
elif cost: elif cost:
can_pay = points_available // cost can_pay = int((points_available // cost) > 0) # limit to 1 new hint per call
else: else:
can_pay = 1000 can_pay = 1000
random.shuffle(not_found_hints) self.ctx.random.shuffle(not_found_hints)
hints = found_hints hints = found_hints
while can_pay > 0: while can_pay > 0:
@@ -946,7 +1012,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
if not_found_hints: if not_found_hints:
if hints: if hints:
self.output( self.output(
"Could not pay for everything. Rerun the hint later with more points to get the remaining hints.") "There may be more hintables, you can rerun the command to find more.")
else: else:
self.output(f"You can't afford the hint. " self.output(f"You can't afford the hint. "
f"You have {points_available} points and need at least " f"You have {points_available} points and need at least "
@@ -1098,13 +1164,26 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
elif cmd == 'StatusUpdate': elif cmd == 'StatusUpdate':
update_client_status(ctx, client, args["status"]) update_client_status(ctx, client, args["status"])
if cmd == 'Say': elif cmd == 'Say':
if "text" not in args or type(args["text"]) is not str or not args["text"].isprintable(): if "text" not in args or type(args["text"]) is not str or not args["text"].isprintable():
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'Say'}]) await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'Say'}])
return return
client.messageprocessor(args["text"]) client.messageprocessor(args["text"])
elif cmd == "Bounce":
games = set(args.get("games", []))
tags = set(args.get("tags", []))
slots = set(args.get("slots", []))
args["cmd"] = "Bounced"
msg = ctx.dumper([args])
for bounceclient in ctx.endpoints:
if client.team == bounceclient.team and (ctx.games[bounceclient.slot] in games or
set(bounceclient.tags) & tags or
bounceclient.slot in slots):
await ctx.send_encoded_msgs(bounceclient, msg)
def update_client_status(ctx: Context, client: Client, new_status: ClientStatus): def update_client_status(ctx: Context, client: Client, new_status: ClientStatus):
current = ctx.client_game_state[client.team, client.slot] current = ctx.client_game_state[client.team, client.slot]
@@ -1114,6 +1193,8 @@ def update_client_status(ctx: Context, client: Client, new_status: ClientStatus)
ctx.notify_all(finished_msg) ctx.notify_all(finished_msg)
if "auto" in ctx.forfeit_mode: if "auto" in ctx.forfeit_mode:
forfeit_player(ctx, client.team, client.slot) forfeit_player(ctx, client.team, client.slot)
elif proxy_worlds[ctx.games[client.slot]].forced_auto_forfeit:
forfeit_player(ctx, client.team, client.slot)
ctx.client_game_state[client.team, client.slot] = new_status ctx.client_game_state[client.team, client.slot] = new_status
@@ -1398,8 +1479,7 @@ async def main(args: argparse.Namespace):
ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points, ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points,
args.hint_cost, not args.disable_item_cheat, args.forfeit_mode, args.remaining_mode, args.hint_cost, not args.disable_item_cheat, args.forfeit_mode, args.remaining_mode,
args.auto_shutdown, args.compatibility) args.auto_shutdown, args.compatibility, args.log_network)
ctx.log_network = args.log_network
data_filename = args.multidata data_filename = args.multidata
try: try:
@@ -1410,19 +1490,6 @@ async def main(args: argparse.Namespace):
root.withdraw() root.withdraw()
data_filename = tkinter.filedialog.askopenfilename(filetypes=(("Multiworld data", "*.archipelago *.zip"),)) data_filename = tkinter.filedialog.askopenfilename(filetypes=(("Multiworld data", "*.archipelago *.zip"),))
if data_filename.endswith(".zip"):
import zipfile
with zipfile.ZipFile(data_filename) as zf:
for file in zf.namelist():
if file.endswith(".archipelago"):
import os
data_filename = os.path.join(os.path.dirname(data_filename), file)
with open(data_filename, "wb") as f:
f.write(zf.read(file))
break
else:
raise Exception("No .archipelago found in archive.")
ctx.load(data_filename, args.use_embedded_options) ctx.load(data_filename, args.use_embedded_options)
except Exception as e: except Exception as e:

View File

@@ -1,6 +1,4 @@
from __future__ import annotations from __future__ import annotations
import asyncio
import logging
import typing import typing
import enum import enum
from json import JSONEncoder, JSONDecoder from json import JSONEncoder, JSONDecoder
@@ -94,52 +92,6 @@ def _object_hook(o: typing.Any) -> typing.Any:
decode = JSONDecoder(object_hook=_object_hook).decode decode = JSONDecoder(object_hook=_object_hook).decode
class Node:
endpoints: typing.List
dumper = staticmethod(encode)
loader = staticmethod(decode)
def __init__(self):
self.endpoints = []
super(Node, self).__init__()
self.log_network = 0
def broadcast_all(self, msgs):
msgs = self.dumper(msgs)
for endpoint in self.endpoints:
asyncio.create_task(self.send_encoded_msgs(endpoint, msgs))
async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool:
if not endpoint.socket or not endpoint.socket.open:
return False
msg = self.dumper(msgs)
try:
await endpoint.socket.send(msg)
except websockets.ConnectionClosed:
logging.exception(f"Exception during send_msgs, could not send {msg}")
await self.disconnect(endpoint)
else:
if self.log_network:
logging.info(f"Outgoing message: {msg}")
return True
async def send_encoded_msgs(self, endpoint: Endpoint, msg: str) -> bool:
if not endpoint.socket or not endpoint.socket.open:
return False
try:
await endpoint.socket.send(msg)
except websockets.ConnectionClosed:
logging.exception("Exception during send_encoded_msgs")
await self.disconnect(endpoint)
else:
if self.log_network:
logging.info(f"Outgoing message: {msg}")
return True
async def disconnect(self, endpoint):
if endpoint in self.endpoints:
self.endpoints.remove(endpoint)
class Endpoint: class Endpoint:
socket: websockets.WebSocketServerProtocol socket: websockets.WebSocketServerProtocol
@@ -244,7 +196,7 @@ class JSONtoTextParser(metaclass=HandlerMeta):
return self._handle_item_name(node) return self._handle_item_name(node)
def _handle_entrance_name(self, node: JSONMessagePart): def _handle_entrance_name(self, node: JSONMessagePart):
node["color"] = 'white_bg;black' node["color"] = 'blue'
return self._handle_color(node) return self._handle_color(node)

View File

@@ -7,12 +7,15 @@ class AssembleOptions(type):
def __new__(mcs, name, bases, attrs): def __new__(mcs, name, bases, attrs):
options = attrs["options"] = {} options = attrs["options"] = {}
name_lookup = attrs["name_lookup"] = {} name_lookup = attrs["name_lookup"] = {}
# merge parent class options
for base in bases: for base in bases:
if hasattr(base, "options"): if getattr(base, "options", None):
options.update(base.options) options.update(base.options)
name_lookup.update(name_lookup) name_lookup.update(base.name_lookup)
new_options = {name[7:].lower(): option_id for name, option_id in attrs.items() if new_options = {name[7:].lower(): option_id for name, option_id in attrs.items() if
name.startswith("option_")} name.startswith("option_")}
if "random" in new_options:
raise Exception("Choice option 'random' cannot be manually assigned.")
attrs["name_lookup"].update({option_id: name for name, option_id in new_options.items()}) attrs["name_lookup"].update({option_id: name for name, option_id in new_options.items()})
options.update(new_options) options.update(new_options)
@@ -30,24 +33,41 @@ class AssembleOptions(type):
attrs["__init__"] = validate_decorator(attrs["__init__"]) attrs["__init__"] = validate_decorator(attrs["__init__"])
return super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs) return super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs)
class Option(metaclass=AssembleOptions): class Option(metaclass=AssembleOptions):
value: int value: int
name_lookup: typing.Dict[int, str] name_lookup: typing.Dict[int, str]
default = 0 default = 0
def __repr__(self): # convert option_name_long into Name Long as displayname, otherwise name_long is the result.
return f"{self.__class__.__name__}({self.get_option_name()})" # Handled in get_option_name()
autodisplayname = False
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.get_current_option_name()})"
def __hash__(self): def __hash__(self):
return hash(self.value) return hash(self.value)
def get_option_name(self): @property
def current_key(self) -> str:
return self.name_lookup[self.value] return self.name_lookup[self.value]
def __int__(self): def get_current_option_name(self) -> str:
"""For display purposes."""
return self.get_option_name(self.value)
@classmethod
def get_option_name(cls, value: typing.Any) -> str:
if cls.autodisplayname:
return cls.name_lookup[value].replace("_", " ").title()
else:
return cls.name_lookup[value]
def __int__(self) -> int:
return self.value return self.value
def __bool__(self): def __bool__(self) -> bool:
return bool(self.value) return bool(self.value)
@classmethod @classmethod
@@ -95,20 +115,29 @@ class Toggle(Option):
def __int__(self): def __int__(self):
return int(self.value) return int(self.value)
def get_option_name(self): @classmethod
return bool(self.value) def get_option_name(cls, value):
return ["No", "Yes"][int(value)]
class DefaultOnToggle(Toggle): class DefaultOnToggle(Toggle):
default = 1 default = 1
class Choice(Option): class Choice(Option):
autodisplayname = True
def __init__(self, value: int): def __init__(self, value: int):
self.value: int = value self.value: int = value
@classmethod @classmethod
def from_text(cls, text: str) -> Choice: def from_text(cls, text: str) -> Choice:
text = text.lower()
# TODO: turn on after most people have adjusted their yamls to no longer have suboptions with "random" in them
# maybe in 0.2?
# if text == "random":
# return cls(random.choice(list(cls.options.values())))
for optionname, value in cls.options.items(): for optionname, value in cls.options.items():
if optionname == text.lower(): if optionname == text:
return cls(value) return cls(value)
raise KeyError( raise KeyError(
f'Could not find option "{text}" for "{cls.__name__}", ' f'Could not find option "{text}" for "{cls.__name__}", '
@@ -120,6 +149,29 @@ class Choice(Option):
return cls(data) return cls(data)
return cls.from_text(str(data)) return cls.from_text(str(data))
def __eq__(self, other):
if isinstance(other, str):
assert other in self.options
return other == self.current_key
elif isinstance(other, int):
assert other in self.name_lookup
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__}")
def __ne__(self, other):
if isinstance(other, str):
assert other in self.options
return other != self.current_key
elif isinstance(other, int):
assert other in self.name_lookup
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(Option, int): class Range(Option, int):
range_start = 0 range_start = 0
@@ -152,7 +204,7 @@ class Range(Option, int):
return cls(data) return cls(data)
return cls.from_text(str(data)) return cls.from_text(str(data))
def get_option_name(self): def get_option_name(self, value):
return str(self.value) return str(self.value)
def __str__(self): def __str__(self):
@@ -189,8 +241,29 @@ class OptionDict(Option):
else: else:
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}") raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
def get_option_name(self): def get_option_name(self, value):
return str(self.value) return str(value)
class OptionList(Option):
default = []
def __init__(self, value: typing.List[str, typing.Any]):
self.value = value
@classmethod
def from_text(cls, text: str):
return cls([option.strip() for option in text.split(",")])
@classmethod
def from_any(cls, data: typing.Any):
if type(data) == list:
return cls(data)
return cls.from_text(str(data))
def get_option_name(self, value):
return str(value)
local_objective = Toggle # local triforce pieces, local dungeon prizes etc. local_objective = Toggle # local triforce pieces, local dungeon prizes etc.
@@ -206,14 +279,14 @@ if __name__ == "__main__":
from worlds.alttp.Options import Logic from worlds.alttp.Options import Logic
import argparse import argparse
mapshuffle = Toggle map_shuffle = Toggle
compassshuffle = Toggle compass_shuffle = Toggle
keyshuffle = Toggle keyshuffle = Toggle
bigkeyshuffle = Toggle bigkey_shuffle = Toggle
hints = Toggle hints = Toggle
test = argparse.Namespace() test = argparse.Namespace()
test.logic = Logic.from_text("no_logic") test.logic = Logic.from_text("no_logic")
test.mapshuffle = mapshuffle.from_text("ON") test.map_shuffle = map_shuffle.from_text("ON")
test.hints = hints.from_text('OFF') test.hints = hints.from_text('OFF')
try: try:
test.logic = Logic.from_text("overworld_glitches_typo") test.logic = Logic.from_text("overworld_glitches_typo")
@@ -223,7 +296,7 @@ if __name__ == "__main__":
test.logic_owg = Logic.from_text("owg") test.logic_owg = Logic.from_text("owg")
except KeyError as e: except KeyError as e:
print(e) print(e)
if test.mapshuffle: if test.map_shuffle:
print("Mapshuffle is on") print("map_shuffle is on")
print(f"Hints are {bool(test.hints)}") print(f"Hints are {bool(test.hints)}")
print(test) print(test)

View File

@@ -2,7 +2,6 @@ import bsdiff4
import yaml import yaml
import os import os
import lzma import lzma
import hashlib
import threading import threading
import concurrent.futures import concurrent.futures
import zipfile import zipfile
@@ -10,37 +9,13 @@ import sys
from typing import Tuple, Optional from typing import Tuple, Optional
import Utils import Utils
from worlds.alttp.Rom import JAP10HASH
current_patch_version = 2 current_patch_version = 2
def get_base_rom_path(file_name: str = "") -> str:
options = Utils.get_options()
if not file_name:
file_name = options["lttp_options"]["rom_file"]
if not os.path.exists(file_name):
file_name = Utils.local_path(file_name)
return file_name
def get_base_rom_bytes(file_name: str = "") -> bytes:
from worlds.alttp.Rom import read_rom
base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None)
if not base_rom_bytes:
file_name = get_base_rom_path(file_name)
base_rom_bytes = bytes(read_rom(open(file_name, "rb")))
basemd5 = hashlib.md5()
basemd5.update(base_rom_bytes)
if JAP10HASH != basemd5.hexdigest():
raise Exception('Supplied Base Rom does not match known MD5 for JAP(1.0) release. '
'Get the correct game and version, then dump it')
get_base_rom_bytes.base_rom_bytes = base_rom_bytes
return base_rom_bytes
def generate_yaml(patch: bytes, metadata: Optional[dict] = None) -> bytes: def generate_yaml(patch: bytes, metadata: Optional[dict] = None) -> bytes:
from worlds.alttp.Rom import JAP10HASH
patch = yaml.dump({"meta": metadata, patch = yaml.dump({"meta": metadata,
"patch": patch, "patch": patch,
"game": "A Link to the Past", "game": "A Link to the Past",
@@ -52,6 +27,7 @@ def generate_yaml(patch: bytes, metadata: Optional[dict] = None) -> bytes:
def generate_patch(rom: bytes, metadata: Optional[dict] = None) -> bytes: def generate_patch(rom: bytes, metadata: Optional[dict] = None) -> bytes:
from worlds.alttp.Rom import get_base_rom_bytes
if metadata is None: if metadata is None:
metadata = {} metadata = {}
patch = bsdiff4.diff(get_base_rom_bytes(), rom) patch = bsdiff4.diff(get_base_rom_bytes(), rom)
@@ -71,6 +47,7 @@ def create_patch_file(rom_file_to_patch: str, server: str = "", destination: str
def create_rom_bytes(patch_file: str, ignore_version: bool = False) -> Tuple[dict, str, bytearray]: def create_rom_bytes(patch_file: str, ignore_version: bool = False) -> Tuple[dict, str, bytearray]:
from worlds.alttp.Rom import get_base_rom_bytes
data = Utils.parse_yaml(lzma.decompress(load_bytes(patch_file)).decode("utf-8-sig")) data = Utils.parse_yaml(lzma.decompress(load_bytes(patch_file)).decode("utf-8-sig"))
if not ignore_version and data["compatible_version"] > current_patch_version: if not ignore_version and data["compatible_version"] > current_patch_version:
raise RuntimeError("Patch file is incompatible with this patcher, likely an update is required.") raise RuntimeError("Patch file is incompatible with this patcher, likely an update is required.")
@@ -184,3 +161,11 @@ if __name__ == "__main__":
traceback.print_exc() traceback.print_exc()
input("Press enter to close.") input("Press enter to close.")
def read_rom(stream, strip_header=True) -> bytearray:
"""Reads rom into bytearray and optionally strips off any smc header"""
buffer = bytearray(stream.read())
if strip_header and len(buffer) % 0x400 == 0x200:
return buffer[0x200:]
return buffer

View File

@@ -6,8 +6,12 @@ Currently, the following games are supported:
* The Legend of Zelda: A Link to the Past * The Legend of Zelda: A Link to the Past
* Factorio * Factorio
* Minecraft * Minecraft
* Subnautica
* Slay the Spire
* Risk of Rain 2
* The Legend of Zelda: Ocarina of Time
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial). For setup and instructions check out our [tutorials page](http://archipelago.gg:48484/tutorial).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
windows binaries. windows binaries.
@@ -36,6 +40,7 @@ This project makes use of multiple other projects. We wouldn't be here without t
* [z3randomizer](https://github.com/CaitSith2/z3randomizer) * [z3randomizer](https://github.com/CaitSith2/z3randomizer)
* [Enemizer](https://github.com/Ijwu/Enemizer) * [Enemizer](https://github.com/Ijwu/Enemizer)
* [Ocarina of Time Randomizer](https://github.com/TestRunnerSRL/OoT-Randomizer)
## Contributing ## Contributing
Contributions are welcome. We have a few asks of any new contributors. Contributions are welcome. We have a few asks of any new contributors.

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import typing import typing
def tuplize_version(version: str) -> typing.Tuple[int, ...]: def tuplize_version(version: str) -> Version:
return Version(*(int(piece, 10) for piece in version.split("."))) return Version(*(int(piece, 10) for piece in version.split(".")))
@@ -13,7 +13,7 @@ class Version(typing.NamedTuple):
build: int build: int
__version__ = "0.1.5" __version__ = "0.1.7"
version_tuple = tuplize_version(__version__) version_tuple = tuplize_version(__version__)
import builtins import builtins
@@ -51,24 +51,6 @@ def snes_to_pc(value):
return ((value & 0x7F0000) >> 1) | (value & 0x7FFF) return ((value & 0x7F0000) >> 1) | (value & 0x7FFF)
def parse_player_names(names, players, teams):
names = tuple(n for n in (n.strip() for n in names.split(",")) if n)
if len(names) != len(set(names)):
name_counter = collections.Counter(names)
raise ValueError(f"Duplicate Player names is not supported, "
f'found multiple "{name_counter.most_common(1)[0][0]}".')
ret = []
while names or len(ret) < teams:
team = [n[:16] for n in names[:players]]
# 16 bytes in rom per player, which will map to more in unicode, but those characters later get filtered
while len(team) != players:
team.append(f"Player{len(team) + 1}")
ret.append(team)
names = names[players:]
return ret
def cache_argsless(function): def cache_argsless(function):
if function.__code__.co_argcount: if function.__code__.co_argcount:
raise Exception("Can only cache 0 argument functions with this cache.") raise Exception("Can only cache 0 argument functions with this cache.")
@@ -137,6 +119,7 @@ def open_file(filename):
parse_yaml = safe_load parse_yaml = safe_load
unsafe_parse_yaml = functools.partial(load, Loader=Loader) unsafe_parse_yaml = functools.partial(load, Loader=Loader)
@cache_argsless @cache_argsless
def get_public_ipv4() -> str: def get_public_ipv4() -> str:
import socket import socket
@@ -153,6 +136,7 @@ def get_public_ipv4() -> str:
pass # we could be offline, in a local game, so no point in erroring out pass # we could be offline, in a local game, so no point in erroring out
return ip return ip
@cache_argsless @cache_argsless
def get_public_ipv6() -> str: def get_public_ipv6() -> str:
import socket import socket
@@ -166,6 +150,7 @@ def get_public_ipv6() -> str:
pass # we could be offline, in a local game, or ipv6 may not be available pass # we could be offline, in a local game, or ipv6 may not be available
return ip return ip
@cache_argsless @cache_argsless
def get_default_options() -> dict: def get_default_options() -> dict:
# Refer to host.yaml for comments as to what all these options mean. # Refer to host.yaml for comments as to what all these options mean.
@@ -211,20 +196,19 @@ def get_default_options() -> dict:
"glitch_triforce_room": 1, "glitch_triforce_room": 1,
"race": 0, "race": 0,
"plando_options": "bosses", "plando_options": "bosses",
},
"minecraft_options": {
"forge_directory": "Minecraft Forge server",
"max_heap_size": "2G"
},
"oot_options": {
"rom_file": "The Legend of Zelda - Ocarina of Time.z64",
} }
} }
return options return options
blacklisted_options = {"multi_mystery_options.cpu_threads",
"multi_mystery_options.max_attempts",
"multi_mystery_options.take_first_working",
"multi_mystery_options.keep_all_seeds",
"multi_mystery_options.log_output_path",
"multi_mystery_options.log_level"}
def update_options(src: dict, dest: dict, filename: str, keys: list) -> dict: def update_options(src: dict, dest: dict, filename: str, keys: list) -> dict:
import logging import logging
for key, value in src.items(): for key, value in src.items():
@@ -233,17 +217,18 @@ def update_options(src: dict, dest: dict, filename: str, keys: list) -> dict:
option_name = '.'.join(new_keys) option_name = '.'.join(new_keys)
if key not in dest: if key not in dest:
dest[key] = value dest[key] = value
if filename.endswith("options.yaml") and option_name not in blacklisted_options: if filename.endswith("options.yaml"):
logging.info(f"Warning: {filename} is missing {option_name}") logging.info(f"Warning: {filename} is missing {option_name}")
elif isinstance(value, dict): elif isinstance(value, dict):
if not isinstance(dest.get(key, None), dict): if not isinstance(dest.get(key, None), dict):
if filename.endswith("options.yaml") and option_name not in blacklisted_options: if filename.endswith("options.yaml"):
logging.info(f"Warning: {filename} has {option_name}, but it is not a dictionary. overwriting.") logging.info(f"Warning: {filename} has {option_name}, but it is not a dictionary. overwriting.")
dest[key] = value dest[key] = value
else: else:
dest[key] = update_options(value, dest[key], filename, new_keys) dest[key] = update_options(value, dest[key], filename, new_keys)
return dest return dest
@cache_argsless @cache_argsless
def get_options() -> dict: def get_options() -> dict:
if not hasattr(get_options, "options"): if not hasattr(get_options, "options"):
@@ -300,7 +285,7 @@ def persistent_load() -> typing.Dict[dict]:
return storage return storage
def get_adjuster_settings(romfile: str) -> typing.Tuple[str, bool]: def get_adjuster_settings(romfile: str, skip_questions: bool = False) -> typing.Tuple[str, bool]:
if hasattr(get_adjuster_settings, "adjuster_settings"): if hasattr(get_adjuster_settings, "adjuster_settings"):
adjuster_settings = getattr(get_adjuster_settings, "adjuster_settings") adjuster_settings = getattr(get_adjuster_settings, "adjuster_settings")
else: else:
@@ -308,11 +293,11 @@ def get_adjuster_settings(romfile: str) -> typing.Tuple[str, bool]:
if adjuster_settings: if adjuster_settings:
import pprint import pprint
import Patch from worlds.alttp.Rom import get_base_rom_path
adjuster_settings.rom = romfile adjuster_settings.rom = romfile
adjuster_settings.baserom = Patch.get_base_rom_path() adjuster_settings.baserom = get_base_rom_path()
adjuster_settings.world = None adjuster_settings.world = None
whitelist = {"disablemusic", "fastmenu", "heartbeep", "heartcolor", "ow_palettes", "quickswap", whitelist = {"music", "menuspeed", "heartbeep", "heartcolor", "ow_palettes", "quickswap",
"uw_palettes", "sprite"} "uw_palettes", "sprite"}
printed_options = {name: value for name, value in vars(adjuster_settings).items() if name in whitelist} printed_options = {name: value for name, value in vars(adjuster_settings).items() if name in whitelist}
if hasattr(adjuster_settings, "sprite_pool"): if hasattr(adjuster_settings, "sprite_pool"):
@@ -330,6 +315,8 @@ def get_adjuster_settings(romfile: str) -> typing.Tuple[str, bool]:
adjust_wanted = getattr(get_adjuster_settings, "adjust_wanted") adjust_wanted = getattr(get_adjuster_settings, "adjust_wanted")
elif persistent_load().get("adjuster", {}).get("never_adjust", False): # never adjust, per user request elif persistent_load().get("adjuster", {}).get("never_adjust", False): # never adjust, per user request
return romfile, False return romfile, False
elif skip_questions:
return romfile, False
else: else:
adjust_wanted = input(f"Last used adjuster settings were found. Would you like to apply these? \n" adjust_wanted = input(f"Last used adjuster settings were found. Would you like to apply these? \n"
f"{pprint.pformat(printed_options)}\n" f"{pprint.pformat(printed_options)}\n"
@@ -358,6 +345,7 @@ def get_adjuster_settings(romfile: str) -> typing.Tuple[str, bool]:
return romfile, adjusted return romfile, adjusted
return romfile, False return romfile, False
@cache_argsless @cache_argsless
def get_unique_identifier(): def get_unique_identifier():
uuid = persistent_load().get("client", {}).get("uuid", None) uuid = persistent_load().get("client", {}).get("uuid", None)

View File

@@ -8,6 +8,7 @@ from pony.flask import Pony
from flask import Flask, request, redirect, url_for, render_template, Response, session, abort, send_from_directory from flask import Flask, request, redirect, url_for, render_template, Response, session, abort, send_from_directory
from flask_caching import Cache from flask_caching import Cache
from flask_compress import Compress from flask_compress import Compress
from worlds.AutoWorld import AutoWorldRegister
from .models import * from .models import *
@@ -81,32 +82,10 @@ def page_not_found(err):
return render_template('404.html'), 404 return render_template('404.html'), 404
games_list = {
"A Link to the Past": ("The Legend of Zelda: A Link to the Past",
"""
The Legend of Zelda: A Link to the Past is an action/adventure game. Take on the role of
Link, a boy who is destined to save the land of Hyrule. Delve through three palaces and nine
dungeons on your quest to rescue the descendents of the seven wise men and defeat the evil
Ganon!"""),
"Factorio": ("Factorio",
"""
Factorio is a game about automation. You play as an engineer who has crash landed on the planet
Nauvis, an inhospitable world filled with dangerous creatures called biters. Build a factory,
research new technologies, and become more efficient in your quest to build a rocket and return home.
"""),
"Minecraft": ("Minecraft",
"""
Minecraft is a game about creativity. In a world made entirely of cubes, you explore, discover, mine,
craft, and try not to explode. Delve deep into the earth and discover abandoned mines, ancient
structures, and materials to create a portal to another world. Defeat the Ender Dragon, and claim
victory!""")
}
# Player settings pages # Player settings pages
@app.route('/games/<string:game>/player-settings') @app.route('/games/<string:game>/player-settings')
def player_settings(game): def player_settings(game):
return render_template(f"player-settings.html") return render_template(f"player-settings.html", game=game)
# Game sub-pages # Game sub-pages
@@ -124,7 +103,11 @@ def game_page(game):
# List of supported games # List of supported games
@app.route('/games') @app.route('/games')
def games(): def games():
return render_template("games/games.html", games_list=games_list) worlds = {}
for game, world in AutoWorldRegister.world_types.items():
if not world.hidden:
worlds[game] = world.__doc__ if world.__doc__ else "No description provided."
return render_template("games/games.html", worlds=worlds)
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>') @app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
@@ -132,14 +115,14 @@ def tutorial(game, file, lang):
return render_template("tutorial.html", game=game, file=file, lang=lang) return render_template("tutorial.html", game=game, file=file, lang=lang)
@app.route('/tutorial') @app.route('/tutorial/')
def tutorial_landing(): def tutorial_landing():
return render_template("tutorialLanding.html") return render_template("tutorialLanding.html")
@app.route('/weighted-settings') @app.route('/faq/<string:lang>/')
def weighted_settings(): def faq(lang):
return render_template("weightedSettings.html") return render_template("faq.html", lang=lang)
@app.route('/seed/<suuid:seed>') @app.route('/seed/<suuid:seed>')
@@ -192,10 +175,12 @@ def hostRoom(room: UUID):
return render_template("hostRoom.html", room=room) return render_template("hostRoom.html", room=room)
@app.route('/hosted/<suuid:room>', methods=['GET', 'POST']) @app.route('/hosted/<suuid:room>', methods=['GET', 'POST'])
def hostRoomRedirect(room: UUID): def hostRoomRedirect(room: UUID):
return redirect(url_for("hostRoom", room=room)) return redirect(url_for("hostRoom", room=room))
@app.route('/favicon.ico') @app.route('/favicon.ico')
def favicon(): def favicon():
return send_from_directory(os.path.join(app.root_path, 'static/static'), return send_from_directory(os.path.join(app.root_path, 'static/static'),

View File

@@ -93,10 +93,10 @@ def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
"sid": generation.id, "sid": generation.id,
"owner": generation.owner}, "owner": generation.owner},
handle_generation_success, handle_generation_failure) handle_generation_success, handle_generation_failure)
except: except Exception as e:
generation.state = STATE_ERROR generation.state = STATE_ERROR
commit() commit()
raise logging.exception(e)
else: else:
generation.state = STATE_STARTED generation.state = STATE_STARTED

View File

@@ -1,8 +1,8 @@
from flask import send_file, Response from flask import send_file, Response, render_template
from pony.orm import select from pony.orm import select
from Patch import update_patch_data from Patch import update_patch_data
from WebHostLib import app, Slot, Room, Seed from WebHostLib import app, Slot, Room, Seed, cache
import zipfile import zipfile
@app.route("/dl_patch/<suuid:room_id>/<int:patch_id>") @app.route("/dl_patch/<suuid:room_id>/<int:patch_id>")
@@ -45,10 +45,10 @@ def download_raw_patch(seed_id, player_id: int):
fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](seed_id)}.apbp" fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](seed_id)}.apbp"
return send_file(patch_data, as_attachment=True, attachment_filename=fname) return send_file(patch_data, as_attachment=True, attachment_filename=fname)
@app.route("/slot_file/<suuid:seed_id>/<int:player_id>") @app.route("/slot_file/<suuid:room_id>/<int:player_id>")
def download_slot_file(seed_id, player_id: int): def download_slot_file(room_id, player_id: int):
seed = Seed.get(id=seed_id) room = Room.get(id=room_id)
slot_data: Slot = select(patch for patch in seed.slots if slot_data: Slot = select(patch for patch in room.seed.slots if
patch.player_id == player_id).first() patch.player_id == player_id).first()
if not slot_data: if not slot_data:
@@ -57,12 +57,27 @@ def download_slot_file(seed_id, player_id: int):
import io import io
if slot_data.game == "Minecraft": if slot_data.game == "Minecraft":
fname = f"AP_{app.jinja_env.filters['suuid'](seed_id)}_P{slot_data.player_id}_{slot_data.player_name}.apmc" from worlds.minecraft import mc_update_output
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apmc"
data = mc_update_output(slot_data.data, server=app.config['PATCH_TARGET'], port=room.last_port)
return send_file(io.BytesIO(data), as_attachment=True, attachment_filename=fname)
elif slot_data.game == "Factorio": elif slot_data.game == "Factorio":
with zipfile.ZipFile(io.BytesIO(slot_data.data)) as zf: with zipfile.ZipFile(io.BytesIO(slot_data.data)) as zf:
for name in zf.namelist(): for name in zf.namelist():
if name.endswith("info.json"): if name.endswith("info.json"):
fname = name.rsplit("/", 1)[0]+".zip" fname = name.rsplit("/", 1)[0]+".zip"
elif slot_data.game == "Ocarina of Time":
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apz5"
else: else:
return "Game download not supported." return "Game download not supported."
return send_file(io.BytesIO(slot_data.data), as_attachment=True, attachment_filename=fname) return send_file(io.BytesIO(slot_data.data), as_attachment=True, attachment_filename=fname)
@app.route("/templates")
@cache.cached()
def list_yaml_templates():
files = []
from worlds.AutoWorld import AutoWorldRegister
for world_name, world in AutoWorldRegister.world_types.items():
if not world.hidden:
files.append(world_name)
return render_template("templates.html", files=files)

View File

@@ -91,8 +91,6 @@ def gen_game(gen_options, race=False, owner=None, sid=None):
erargs.name[player] = os.path.splitext(os.path.split(playerfile)[-1])[0] erargs.name[player] = os.path.splitext(os.path.split(playerfile)[-1])[0]
erargs.name[player] = handle_name(erargs.name[player], player, name_counter) erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
erargs.names = ",".join(erargs.name[i] for i in range(1, playercount + 1))
del (erargs.name)
ERmain(erargs, seed) ERmain(erargs, seed)
return upload_to_db(target.name, owner, sid, race) return upload_to_db(target.name, owner, sid, race)

View File

@@ -10,9 +10,18 @@ target_folder = os.path.join("WebHostLib", "static", "generated")
def create(): def create():
def dictify_range(option):
data = {option.range_start: 0, option.range_end: 0, "random": 0, "random-low": 0, "random-high": 0,
option.default: 50}
notes = {
option.range_start: "minimum value",
option.range_end: "maximum value"
}
return data, notes
for game_name, world in AutoWorldRegister.world_types.items(): for game_name, world in AutoWorldRegister.world_types.items():
res = Template(open(os.path.join("WebHostLib", "templates", "options.yaml")).read()).render( res = Template(open(os.path.join("WebHostLib", "templates", "options.yaml")).read()).render(
options=world.options, __version__=__version__, game=game_name, yaml_dump=yaml.dump options=world.options, __version__=__version__, game=game_name, yaml_dump=yaml.dump,
dictify_range=dictify_range
) )
with open(os.path.join(target_folder, game_name + ".yaml"), "w") as f: with open(os.path.join(target_folder, game_name + ".yaml"), "w") as f:
@@ -31,7 +40,7 @@ def create():
if option.options: if option.options:
this_option = { this_option = {
"type": "select", "type": "select",
"friendlyName": option.friendly_name if hasattr(option, "friendly_name") else option_name, "displayName": option.displayname if hasattr(option, "displayname") else option_name,
"description": option.__doc__ if option.__doc__ else "Please document me!", "description": option.__doc__ if option.__doc__ else "Please document me!",
"defaultValue": None, "defaultValue": None,
"options": [] "options": []
@@ -39,7 +48,7 @@ def create():
for sub_option_name, sub_option_id in option.options.items(): for sub_option_name, sub_option_id in option.options.items():
this_option["options"].append({ this_option["options"].append({
"name": sub_option_name, "name": option.get_option_name(sub_option_id),
"value": sub_option_name, "value": sub_option_name,
}) })
@@ -48,6 +57,16 @@ def create():
game_options[option_name] = this_option game_options[option_name] = this_option
elif hasattr(option, "range_start") and hasattr(option, "range_end"):
game_options[option_name] = {
"type": "range",
"displayName": option.displayname if hasattr(option, "displayname") else option_name,
"description": option.__doc__ if option.__doc__ else "Please document me!",
"defaultValue": option.default if hasattr(option, "default") else option.range_start,
"min": option.range_start,
"max": option.range_end,
}
player_settings["gameOptions"] = game_options player_settings["gameOptions"] = game_options
with open(os.path.join(target_folder, game_name + ".json"), "w") as f: with open(os.path.join(target_folder, game_name + ".json"), "w") as f:

View File

@@ -0,0 +1,50 @@
window.addEventListener('load', () => {
const tutorialWrapper = document.getElementById('faq-wrapper');
new Promise((resolve, reject) => {
const ajax = new XMLHttpRequest();
ajax.onreadystatechange = () => {
if (ajax.readyState !== 4) { return; }
if (ajax.status === 404) {
reject("Sorry, the tutorial is not available in that language yet.");
return;
}
if (ajax.status !== 200) {
reject("Something went wrong while loading the tutorial.");
return;
}
resolve(ajax.responseText);
};
ajax.open('GET', `${window.location.origin}/static/assets/faq/` +
`faq_${tutorialWrapper.getAttribute('data-lang')}.md`, true);
ajax.send();
}).then((results) => {
// Populate page with HTML generated from markdown
tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results);
adjustHeaderWidth();
// Reset the id of all header divs to something nicer
const headers = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6'));
const scrollTargetIndex = window.location.href.search(/#[A-z0-9-_]*$/);
for (let i=0; i < headers.length; i++){
const headerId = headers[i].innerText.replace(/[ ]/g,'-').toLowerCase()
headers[i].setAttribute('id', headerId);
headers[i].addEventListener('click', () =>
window.location.href = window.location.href.substring(0, scrollTargetIndex) + `#${headerId}`);
}
// Manually scroll the user to the appropriate header if anchor navigation is used
if (scrollTargetIndex > -1) {
try{
const scrollTarget = window.location.href.substring(scrollTargetIndex + 1);
document.getElementById(scrollTarget).scrollIntoView({ behavior: "smooth" });
} catch(error) {
console.error(error);
}
}
}).catch((error) => {
console.error(error);
tutorialWrapper.innerHTML =
`<h2>This page is out of logic!</h2>
<h3>Click <a href="${window.location.origin}">here</a> to return to safety.</h3>`;
});
});

View File

@@ -0,0 +1,10 @@
# Frequently Asked Questions
## What is a randomizer?
Who's on first.
## What is a multi-world?
What's on second.
## What does multi-game mean?
I don't know's on third.

View File

@@ -0,0 +1,49 @@
window.addEventListener('load', () => {
// Reload tracker every 15 seconds
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 tracker
document.getElementById('inventory-table').innerHTML = fakeDOM.getElementById('inventory-table').innerHTML;
// Update only counters in the location-table
let counters = document.getElementsByClassName('counter');
const fakeCounters = fakeDOM.getElementsByClassName('counter');
for (let i = 0; i < counters.length; i++) {
counters[i].innerHTML = fakeCounters[i].innerHTML;
}
};
ajax.open('GET', url);
ajax.send();
}, 15000)
// Collapsible advancement sections
const categories = document.getElementsByClassName("location-category");
for (let i = 0; i < categories.length; i++) {
let hide_id = categories[i].id.split('-')[0];
if (hide_id == 'Total') {
continue;
}
categories[i].addEventListener('click', function() {
// Toggle the advancement list
document.getElementById(hide_id).classList.toggle("hide");
// Change text of the header
const tab_header = document.getElementById(hide_id+'-header').children[0];
const orig_text = tab_header.innerHTML;
let new_text;
if (orig_text.includes("▼")) {
new_text = orig_text.replace("▼", "▲");
}
else {
new_text = orig_text.replace("▲", "▼");
}
tab_header.innerHTML = new_text;
});
}
});

View File

@@ -1,8 +1,7 @@
let gameName = null; let gameName = null;
window.addEventListener('load', () => { window.addEventListener('load', () => {
const urlMatches = window.location.href.match(/^.*\/(.*)\/player-settings/); gameName = document.getElementById('player-settings').getAttribute('data-game');
gameName = decodeURIComponent(urlMatches[1]);
// Update game name on page // Update game name on page
document.getElementById('game-name').innerHTML = gameName; document.getElementById('game-name').innerHTML = gameName;
@@ -84,28 +83,68 @@ const buildOptionsTable = (settings, romOpts = false) => {
const label = document.createElement('label'); const label = document.createElement('label');
label.setAttribute('for', setting); label.setAttribute('for', setting);
label.setAttribute('data-tooltip', settings[setting].description); label.setAttribute('data-tooltip', settings[setting].description);
label.innerText = `${settings[setting].friendlyName}:`; label.innerText = `${settings[setting].displayName}:`;
tdl.appendChild(label); tdl.appendChild(label);
tr.appendChild(tdl); tr.appendChild(tdl);
// td Right // td Right
const tdr = document.createElement('td'); const tdr = document.createElement('td');
const select = document.createElement('select'); let element = null;
select.setAttribute('id', setting);
select.setAttribute('data-key', setting); switch(settings[setting].type){
if (romOpts) { select.setAttribute('data-romOpt', '1'); } case 'select':
settings[setting].options.forEach((opt) => { element = document.createElement('div');
const option = document.createElement('option'); element.classList.add('select-container');
option.setAttribute('value', opt.value); let select = document.createElement('select');
option.innerText = opt.name; select.setAttribute('id', setting);
if ((isNaN(currentSettings[setting]) && (parseInt(opt.value, 10) === parseInt(currentSettings[setting]))) || select.setAttribute('data-key', setting);
(opt.value === currentSettings[setting])) { if (romOpts) { select.setAttribute('data-romOpt', '1'); }
option.selected = true; settings[setting].options.forEach((opt) => {
} const option = document.createElement('option');
select.appendChild(option); option.setAttribute('value', opt.value);
}); option.innerText = opt.name;
select.addEventListener('change', (event) => updateGameSetting(event)); if ((isNaN(currentSettings[gameName][setting]) &&
tdr.appendChild(select); (parseInt(opt.value, 10) === parseInt(currentSettings[gameName][setting]))) ||
(opt.value === currentSettings[gameName][setting]))
{
option.selected = true;
}
select.appendChild(option);
});
select.addEventListener('change', (event) => updateGameSetting(event));
element.appendChild(select);
break;
case 'range':
element = document.createElement('div');
element.classList.add('range-container');
let range = document.createElement('input');
range.setAttribute('type', 'range');
range.setAttribute('data-key', setting);
range.setAttribute('min', settings[setting].min);
range.setAttribute('max', settings[setting].max);
range.value = currentSettings[gameName][setting];
range.addEventListener('change', (event) => {
document.getElementById(`${setting}-value`).innerText = event.target.value;
updateGameSetting(event);
});
element.appendChild(range);
let rangeVal = document.createElement('span');
rangeVal.classList.add('range-value');
rangeVal.setAttribute('id', `${setting}-value`);
rangeVal.innerText = currentSettings[gameName][setting] ?? settings[setting].defaultValue;
element.appendChild(rangeVal);
break;
default:
console.error(`Unknown setting type: ${settings[setting].type}`);
console.error(setting);
return;
}
tdr.appendChild(element);
tr.appendChild(tdr); tr.appendChild(tdr);
tbody.appendChild(tr); tbody.appendChild(tr);
}); });

View File

@@ -15,8 +15,8 @@ One Server Host exists per Factorio World in an Archipelago Multiworld, any numb
## Installation Procedures ## Installation Procedures
### Dedicated Server Setup ### Dedicated Server Setup
You need a dedicated isolated Factorio installation that the FactorioClient can take control over, if you intend to both emit a world and play, you need to follow both this setup and the player setup. You need a dedicated isolated Factorio installation that the FactorioClient can take control over. If you intend to both host a world and play on the same device, you will need two separate Factorio installations; one for the FactorioClient to hook into and control, and one for you to play on.
This requires two Factorio installations. The easiest and cheapest way to do so is to either buy or register a Factorio on factorio.com, which allows you to download as many Factorio games as you want. The easiest and cheapest way to do so is to either buy or register a Factorio key on factorio.com, which allows you to download as many Factorio games as you want. If you own a steam copy already you can link your account on the website.
1. Download the latest Factorio from https://factorio.com/download for your system, for Windows the recommendation is "win64-manual". 1. Download the latest Factorio from https://factorio.com/download for your system, for Windows the recommendation is "win64-manual".
2. Make sure the Factorio you play and the Factorio you use for hosting do not share paths. If you downloaded the "manual" version, this is already the case, otherwise, go into the hosting Factorio's folder and put the following text into its `config-path.cfg`: 2. Make sure the Factorio you play and the Factorio you use for hosting do not share paths. If you downloaded the "manual" version, this is already the case, otherwise, go into the hosting Factorio's folder and put the following text into its `config-path.cfg`:
@@ -24,26 +24,29 @@ This requires two Factorio installations. The easiest and cheapest way to do so
config-path=__PATH__executable__/../../config config-path=__PATH__executable__/../../config
use-system-read-write-data-directories=false use-system-read-write-data-directories=false
``` ```
3. Navigate to where you installed Archipelago and open the host.yaml file as text. Find the entry `executable` under `factorio_options` and set it to point to your Factorio. If you put Factorio into your Archipelago folder, this would already match. 3. In this same folder if there are shortcuts named "mods" and "saves" delete these and replace with folders with the same names.
4. Navigate to where you installed ArchipelagoFactorioClient and open the host.yaml file as text. Find the entry `executable` under `factorio_options` and set it to point to your hosting Factorio.exe. If you put Factorio into your Archipelago folder, this would already match.<br>
ex.
```yaml
factorio_options:
executable: C:\\Program Files\\factorio\\bin\\x64\\factorio"
```
### Player Setup ### Player Setup
- Manually install the AP mod for the correct world you want to join, then use Factorio's built-in multiplayer. - Manually install the AP mod for the correct world you want to join, then use Factorio's built-in multiplayer. If you're connecting to a FactorioClient on the same system you will connect to localhost
## Joining a MultiWorld Game ## Joining a MultiWorld Game
1. Install the generated Factorio AP Mod (would be in <Factorio Directory>/Mods after step 2 of Setup) 1. Install the generated Factorio AP Mod (would be in /Mods after step 2 of Setup)
2. Run FactorioClient, it should launch a Factorio server, which you can control with `/factorio <original factorio commands>`, 2. Run FactorioClient, it should launch a Factorio server, which you can control with /factorio <original factorio commands>,
* It should start up, create a world and become ready for Factorio connections.
3. In FactorioClient, do `/connect <Archipelago Server Address>` to join that multiworld. You can find further commands with `/help` as well as `!help` once connected. * It should start up, create a world and become ready for Factorio connections.
3. In FactorioClient, do /connect <Archipelago Server Address> to join that multiworld. You can find further commands with /help as well as !help once connected.
* / commands are run on your local client, ! commands are requests for the AP server * / commands are run on your local client, ! commands are requests for the AP server
* Players should be able to connect to your Factorio Server and begin playing.
4. You can join yourself by connecting to address localhost, other people will need to connect to your IP and you may need to port forward for the Factorio Server for those connections.
* Players should be able to connect to your Factorio Server and begin playing.
4. You can join yourself by connecting to address `localhost`, other people will need to connect to your IP
and you may need to port forward for the Factorio Server for those connections.

View File

@@ -1,54 +1,12 @@
# Minecraft Randomizer Setup Guide # Minecraft Randomizer Setup Guide
#Automatic Hosting Install
- download and install [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) and choose the `Minecraft Client` module
## Required Software ## Required Software
### Server Host
- [Minecraft Forge](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.16.5.html)
- [Minecraft Archipelago Randomizer Mod](https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases)
### Players
- [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-edition) - [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-edition)
## Installation Procedures
### Dedicated Server Setup
Only one person has to do this setup and host a dedicated server for everyone else playing to connect to.
1. Download the 1.16.5 **Minecraft Forge** installer from the link above, making sure to download the most recent recommended version.
2. Run the `forge-1.16.5-xx.x.x-installer.jar` file and choose **install server**.
- On this page you will also choose where to install the server to remember this directory it's important in the next step.
3. Navigate to where you installed the server and open `forge-1.16.5-xx.x.x.jar`
- Upon first launch of the server it will close and ask you to accept Minecraft's EULA. There will be a new file called `eula.txt` that contains a link to Minecraft's EULA, and a line that you need to change to `eula=true` to accept Minecraft's EULA.
- This will create the appropriate directories for you to place the files in the following step.
4. Place the `aprandomizer-x.x.x.jar` from the link above file into the `mods` folder of the above installation of your forge server.
- Once again run the server, it will load up and generate the required directory `APData` for when you are ready to play a game!
### Basic Player Setup
- Purchase and install Minecraft from the above link.
**You're Done**.
Players only need to have a Vanilla unmodified version of Minecraft to play!
### Advanced Player Setup
***This is not required to play a randomized minecraft game.***
however this recommended as it helps make the experience more enjoyable.
#### Recomended Mods
- [JourneyMap](https://www.curseforge.com/minecraft/mc-mods/journeymap) (Minimap)
1. Install and run Minecraft from the link above at least once.
2. Run the `forge-1.16.5-xx.x.x-installer.jar` file and choose **install client**.
- Start Minecraft forge at least once to create the directories needed for the next steps.
3. Navigate to your minecraft install directory and place desired mods `.jar` file the in the `mods` directory.
- The default install directories are as follows.
- Windows `%APPDATA%\.minecraft\mods`
- macOS `~/Library/Application Support/minecraft/mods`
- Linux `~/.minecraft/mods`
## Configuring your YAML file ## Configuring your YAML file
### What is a YAML file and why do I need one? ### What is a YAML file and why do I need one?
@@ -60,10 +18,10 @@ can all have different options.
### Where do I get a YAML file? ### Where do I get a YAML file?
A basic minecraft yaml will look like this. A basic minecraft yaml will look like this.
```yaml ```yaml
description: Template Name description: Basic Minecraft Yaml
# Your name in-game. Spaces will be replaced with underscores and # Your name in-game. Spaces will be replaced with underscores and
# there is a 16 character limit # there is a 16 character limit
name: YourName name: YourName
game: Minecraft game: Minecraft
# Shared Options supported by all games: # Shared Options supported by all games:
@@ -71,44 +29,61 @@ accessibility: locations
progression_balancing: on progression_balancing: on
# Minecraft Specific Options # Minecraft Specific Options
# Number of advancements required (out of 92 total) to spawn the Minecraft:
# Ender Dragon and complete the game. # Number of advancements required (87 max) to spawn the Ender Dragon and complete the game.
advancement_goal: advancement_goal: 50
few: 0 #30
normal: 1 #50
many: 0 #70
# Modifies the level of items logically required for exploring # Number of dragon egg shards to collect (30 max) before the Ender Dragon will spawn.
# dangerous areas and fighting bosses. egg_shards_required: 10
combat_difficulty:
easy: 0
normal: 1
hard: 0
# Junk-fills certain RNG-reliant or tedious advancements with XP rewards. # Number of egg shards available in the pool (30 max).
include_hard_advancements: egg_shards_available: 15
on: 0
off: 1
# Junk-fills extremely difficult advancements; # Modifies the level of items logically required for
# this is only How Did We Get Here? and Adventuring Time. # exploring dangerous areas and fighting bosses.
include_insane_advancements: combat_difficulty:
on: 0 easy: 0
off: 1 normal: 1
hard: 0
# Junk-fills certain RNG-reliant or tedious advancements.
include_hard_advancements:
on: 0
off: 1
# Junk-fills extremely difficult advancements;
# this is only How Did We Get Here? and Adventuring Time.
include_insane_advancements:
on: 0
off: 1
# Some advancements require defeating the Ender Dragon; # Some advancements require defeating the Ender Dragon;
# this will junk-fill them so you won't have to finish to send some items. # this will junk-fill them, so you won't have to finish them to send some items.
include_postgame_advancements: include_postgame_advancements:
on: 0 on: 0
off: 1 off: 1
# Enables shuffling of villages, outposts, fortresses, bastions, and end cities.
shuffle_structures:
on: 0
off: 1
#enables shuffling of villages, outposts, fortresses, bastions, and end cities. # Adds structure compasses to the item pool,
shuffle_structures: # which point to the nearest indicated structure.
on: 1 structure_compasses:
off: 0 on: 0
off: 1
# Replaces a percentage of junk items with bee traps
# which spawn multiple angered bees around every player when received.
bee_traps:
0: 1
25: 0
50: 0
75: 0
100: 0
``` ```
## Joining a MultiWorld Game ## Joining a MultiWorld Game
### Obtain your Minecraft data file ### Obtain your Minecraft data file
@@ -118,8 +93,7 @@ When you join a multiworld game, you will be asked to provide your YAML file to
is done, the host will provide you with either a link to download your data file, or with a zip file containing is done, the host will provide you with either a link to download your data file, or with a zip file containing
everyone's data files. Your data file should have a `.apmc` extension. everyone's data files. Your data file should have a `.apmc` extension.
Put your data file in your forge servers `APData` folder. Make sure to remove any previous data file that was in there double click on your `.apmc` file to have the minecraft client auto-launch the installed forge server.
previously.
### Connect to the MultiServer ### Connect to the MultiServer
After having placed your data file in the `APData` folder, start the Forge server and make sure you have OP After having placed your data file in the `APData` folder, start the Forge server and make sure you have OP
@@ -134,3 +108,23 @@ When the console tells you that you have joined the room, you're ready to begin
on successfully joining a multiworld game! At this point any additional minecraft players may connect to your on successfully joining a multiworld game! At this point any additional minecraft players may connect to your
forge server. forge server.
## Manual Installation Procedures
this is only required if you wish to set up a forge install yourself, its recommended to just use the Archipelago Installer.
###Required Software
- [Minecraft Forge](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.16.5.html)
- [Minecraft Archipelago Randomizer Mod](https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases)
**DO NOT INSTALL THIS ON YOUR CLIENT**
### Dedicated Server Setup
Only one person has to do this setup and host a dedicated server for everyone else playing to connect to.
1. Download the 1.16.5 **Minecraft Forge** installer from the link above, making sure to download the most recent recommended version.
2. Run the `forge-1.16.5-xx.x.x-installer.jar` file and choose **install server**.
- On this page you will also choose where to install the server to remember this directory it's important in the next step.
3. Navigate to where you installed the server and open `forge-1.16.5-xx.x.x.jar`
- Upon first launch of the server it will close and ask you to accept Minecraft's EULA. There will be a new file called `eula.txt` that contains a link to Minecraft's EULA, and a line that you need to change to `eula=true` to accept Minecraft's EULA.
- This will create the appropriate directories for you to place the files in the following step.
4. Place the `aprandomizer-x.x.x.jar` from the link above file into the `mods` folder of the above installation of your forge server.
- Once again run the server, it will load up and generate the required directory `APData` for when you are ready to play a game!

View File

@@ -0,0 +1,69 @@
# Risk of Rain 2 Setup Guide
## Install using r2modman
### Install r2modman
Head on over to the r2modman page on Thunderstore and follow the installation instructions.
https://thunderstore.io/package/ebkr/r2modman/
### Install Archipelago Mod using r2modman
You can install the Archipelago mod using r2modman in one of two ways.
One, you can use the Thunderstore website and click on the "Install with Mod Manager" link.
https://thunderstore.io/package/ArchipelagoMW/Archipelago/
You can also search for the "Archipelago" mod in the r2modman interface.
The mod manager should automatically install all necessary dependencies as well.
### Running the Modded Game
Click on the "Start modded" button in the top left in r2modman to start the game with the
Archipelago mod installed.
## Joining an Archipelago Session
There will be a menu button on the right side of the screen in the character select menu.
Click it in order to bring up the in lobby mod config.
From here you can expand the Archipelago sections and fill in the relevant info.
Keep password blank if there is no password on the server.
Simply check `Enable Archipelago?` and when you start the run it will automatically connect.
## Gameplay
The Risk of Rain 2 players send checks by causing items to spawn in-game. That means opening chests or killing bosses, generally.
An item check is only sent out after a certain number of items are picked up. This count is configurable in the player's YAML.
## YAML Settings
An example YAML would look like this:
```yaml
description: Ijwu-ror2
name: Ijwu
game:
Risk of Rain 2: 1
Risk of Rain 2:
total_locations: 15
total_revivals: 4
start_with_revive: true
item_pickup_step: 1
enable_lunar: true
```
| Name | Description | Allowed values |
| ---- | ----------- | -------------- |
| total_locations | The total number of location checks that will be attributed to the Risk of Rain player. This option is ALSO the total number of items in the item pool for the Risk of Rain player. | 10 - 50 |
| total_revivals | The total number of items in the Risk of Rain player's item pool (items other players pick up for them) replaced with `Dio's Best Friend`. | 0 - 5 |
| start_with_revive | Starts the player off with a `Dio's Best Friend`. Functionally equivalent to putting a `Dio's Best Friend` in your `starting_inventory`. | true/false |
| item_pickup_step | The number of item pickups which you are allowed to claim before they become an Archipelago location check. | 0 - 5 |
| enable_lunar | Allows for lunar items to be shuffled into the item pool on behalf of the Risk of Rain player. | true/false |
Using the example YAML above: the Risk of Rain 2 player will have 15 total items which they can pick up for other players. (total_locations = 15)
They will have 15 items waiting for them in the item pool which will be distributed out to the multiworld. (total_locations = 15)
They will complete a location check every second item. (item_pickup_step = 1)
They will have 4 of the items which other players can grant them replaced with `Dio's Best Friend`. (total_revivals = 4)
The player will also start with a `Dio's Best Friend`. (start_with_revive = true)
The player will have lunar items shuffled into the item pool on their behalf. (enable_lunar = true)

View File

@@ -139,5 +139,24 @@
] ]
} }
] ]
},
{
"gameTitle": "Risk of Rain 2",
"tutorials": [
{
"name": "Multiworld Setup Guide",
"description": "A guide to setting up the Risk of Rain 2 integration for Archipelago multiworld games.",
"files": [
{
"language": "English",
"filename": "ror2/setup_en.md",
"link": "ror2/setup/en",
"authors": [
"Ijwu"
]
}
]
}
]
} }
] ]

View File

@@ -53,22 +53,6 @@ can all have different options.
The [Generate Game](/player-settings) page on the website allows you to configure your personal settings and The [Generate Game](/player-settings) page on the website allows you to configure your personal settings and
export a YAML file from them. export a YAML file from them.
### Advanced YAML configuration
A more advanced version of the YAML file can be created using the [Weighted Settings](/weighted-settings) page,
which allows you to configure up to three presets. The Weighted Settings page has many options which are
primarily represented with sliders. This allows you to choose how likely certain options are to occur relative
to other options within a category.
For example, imagine the generator creates a bucket labeled "Map Shuffle", and places folded pieces of paper
into the bucket for each sub-option. Also imagine your chosen value for "On" is 20, and your value for "Off" is 40.
In this example, sixty pieces of paper are put into the bucket. Twenty for "On" and forty for "Off". When the
generator is deciding whether or not to turn on map shuffle for your game, it reaches into this bucket and pulls
out a piece of paper at random. In this example, you are much more likely to have map shuffle turned off.
If you never want an option to be chosen, simply set its value to zero. Remember that each setting must have at
lease one option set to a number greater than zero.
### Verifying your YAML file ### Verifying your YAML file
If you would like to validate your YAML file to make sure it works, you may do so on the If you would like to validate your YAML file to make sure it works, you may do so on the
[YAML Validator](/mysterycheck) page. [YAML Validator](/mysterycheck) page.

View File

@@ -1,486 +0,0 @@
let spriteData = null;
window.addEventListener('load', () => {
const gameSettings = document.getElementById('weighted-settings');
Promise.all([fetchWeightedSettingsYaml(), fetchWeightedSettingsJson(), fetchSpriteData()]).then((results) => {
// Load YAML into object
const sourceData = jsyaml.safeLoad(results[0], { json: true });
const wsVersion = sourceData.ws_version;
delete sourceData.ws_version; // Do not include the settings version number in the export
// Check if settings exist in localStorage. If no settings are present, this is a first load (or reset to default)
// and the version number should be silently updated
if (!localStorage.getItem('weightedSettings1')) {
localStorage.setItem('wsVersion', wsVersion);
}
// Update localStorage with three settings objects. Preserve original objects if present.
for (let i=1; i<=3; i++) {
const localSettings = JSON.parse(localStorage.getItem(`weightedSettings${i}`));
const updatedObj = localSettings ? Object.assign(sourceData, localSettings) : sourceData;
localStorage.setItem(`weightedSettings${i}`, JSON.stringify(updatedObj));
}
// Build the entire UI
buildUI(JSON.parse(results[1]), JSON.parse(results[2]));
// Populate the UI and add event listeners
populateSettings();
document.getElementById('preset-number').addEventListener('change', populateSettings);
gameSettings.addEventListener('change', handleOptionChange);
gameSettings.addEventListener('keyup', handleOptionChange);
document.getElementById('export-button').addEventListener('click', exportSettings);
document.getElementById('reset-to-default').addEventListener('click', resetToDefaults);
adjustHeaderWidth();
if (localStorage.getItem('wsVersion') !== wsVersion) {
const userWarning = document.getElementById('user-warning');
const messageSpan = document.createElement('span');
messageSpan.innerHTML = "A new version of the weighted settings file is available. Click here to update!" +
"<br />Be aware this will also reset your presets, so you should export them now if you want to save them.";
userWarning.appendChild(messageSpan);
userWarning.style.display = 'block';
userWarning.addEventListener('click', resetToDefaults);
}
}).catch((error) => {
console.error(error);
gameSettings.innerHTML = `
<h2>Something went wrong while loading your game settings page.</h2>
<h2>${error}</h2>
<h2><a href="${window.location.origin}">Click here to return to safety!</a></h2>
`
});
document.getElementById('generate-game').addEventListener('click', () => generateGame());
document.getElementById('generate-race').addEventListener('click', () => generateGame(true));
});
const fetchWeightedSettingsYaml = () => new Promise((resolve, reject) => {
const ajax = new XMLHttpRequest();
ajax.onreadystatechange = () => {
if (ajax.readyState !== 4) { return; }
if (ajax.status !== 200) {
reject("Unable to fetch source yaml file.");
return;
}
resolve(ajax.responseText);
};
ajax.open('GET', `${window.location.origin}/static/static/weightedSettings.yaml` ,true);
ajax.send();
});
const fetchWeightedSettingsJson = () => new Promise((resolve, reject) => {
const ajax = new XMLHttpRequest();
ajax.onreadystatechange = () => {
if (ajax.readyState !== 4) { return; }
if (ajax.status !== 200) {
reject('Unable to fetch JSON schema file');
return;
}
resolve(ajax.responseText);
};
ajax.open('GET', `${window.location.origin}/static/static/weightedSettings.json`, true);
ajax.send();
});
const fetchSpriteData = () => new Promise((resolve, reject) => {
const ajax = new XMLHttpRequest();
ajax.onreadystatechange = () => {
if (ajax.readyState !== 4) { return; }
if (ajax.status !== 200) {
reject('Unable to fetch sprite data.');
return;
}
resolve(ajax.responseText);
};
ajax.open('GET', `${window.location.origin}/static/generated/spriteData.json`, true);
ajax.send();
});
const handleOptionChange = (event) => {
if(!event.target.matches('.setting')) { return; }
const presetNumber = document.getElementById('preset-number').value;
const settings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`))
const settingString = event.target.getAttribute('data-setting');
document.getElementById(settingString).innerText = event.target.value;
if(getSettingValue(settings, settingString) !== false){
const keys = settingString.split('.');
switch (keys.length) {
case 1:
settings[keys[0]] = isNaN(event.target.value) ?
event.target.value : parseInt(event.target.value, 10);
break;
case 2:
settings[keys[0]][keys[1]] = isNaN(event.target.value) ?
event.target.value : parseInt(event.target.value, 10);
break;
case 3:
settings[keys[0]][keys[1]][keys[2]] = isNaN(event.target.value) ?
event.target.value : parseInt(event.target.value, 10);
break;
default:
console.warn(`Unknown setting string received: ${settingString}`)
return;
}
// Save the updated settings object bask to localStorage
localStorage.setItem(`weightedSettings${presetNumber}`, JSON.stringify(settings));
}else{
console.warn(`Unknown setting string received: ${settingString}`)
}
};
const populateSettings = () => {
buildSpriteOptions();
const presetNumber = document.getElementById('preset-number').value;
const settings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`))
const settingsInputs = Array.from(document.querySelectorAll('.setting'));
settingsInputs.forEach((input) => {
const settingString = input.getAttribute('data-setting');
const settingValue = getSettingValue(settings, settingString);
if(settingValue !== false){
input.value = settingValue;
document.getElementById(settingString).innerText = settingValue;
}
});
};
/**
* Returns the value of the settings object, or false if the settings object does not exist
* @param settings
* @param keyString
* @returns {string} | bool
*/
const getSettingValue = (settings, keyString) => {
const keys = keyString.split('.');
let currentVal = settings;
keys.forEach((key) => {
if(typeof(key) === 'string' && currentVal.hasOwnProperty(key)){
currentVal = currentVal[key];
}else{
currentVal = false;
}
});
return currentVal;
};
const exportSettings = () => {
const presetNumber = document.getElementById('preset-number').value;
const settings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`));
const yamlText = jsyaml.safeDump(settings, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`);
download(`${settings.description}.yaml`, yamlText);
};
const resetToDefaults = () => {
[1, 2, 3].forEach((presetNumber) => localStorage.removeItem(`weightedSettings${presetNumber}`));
location.reload();
};
/** Create an anchor and trigger a download of a text file. */
const download = (filename, text) => {
const downloadLink = document.createElement('a');
downloadLink.setAttribute('href','data:text/yaml;charset=utf-8,'+ encodeURIComponent(text))
downloadLink.setAttribute('download', filename);
downloadLink.style.display = 'none';
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
};
const buildUI = (settings, spriteData) => {
const settingsWrapper = document.getElementById('settings-wrapper');
const settingTypes = {
gameOptions: 'Game Options',
romOptions: 'ROM Options',
}
Object.keys(settingTypes).forEach((settingTypeKey) => {
const sectionHeader = document.createElement('h2');
sectionHeader.innerText = settingTypes[settingTypeKey];
settingsWrapper.appendChild(sectionHeader);
Object.values(settings[settingTypeKey]).forEach((setting) => {
if (typeof(setting.inputType) === 'undefined' || !setting.inputType){
console.error(setting);
throw new Error('Setting with no inputType specified.');
}
switch(setting.inputType){
case 'text':
// Currently, all text input is handled manually because there is very little of it
return;
case 'range':
buildRangeSettings(settingsWrapper, setting);
return;
default:
console.error(setting);
throw new Error('Unhandled inputType specified.');
}
});
});
// Build sprite options
const spriteOptionsHeader = document.createElement('h2');
spriteOptionsHeader.innerText = 'Sprite Options';
settingsWrapper.appendChild(spriteOptionsHeader);
const spriteOptionsWrapper = document.createElement('div');
spriteOptionsWrapper.setAttribute('id', 'sprite-options-wrapper');
spriteOptionsWrapper.className = 'setting-wrapper';
settingsWrapper.appendChild(spriteOptionsWrapper);
// Append sprite picker
settingsWrapper.appendChild(buildSpritePicker(spriteData));
};
const buildSpriteOptions = () => {
const spriteOptionsWrapper = document.getElementById('sprite-options-wrapper');
// Clear the contents of the wrapper div
while(spriteOptionsWrapper.firstChild){
spriteOptionsWrapper.removeChild(spriteOptionsWrapper.lastChild);
}
const spriteOptionsTitle = document.createElement('span');
spriteOptionsTitle.className = 'title-span';
spriteOptionsTitle.innerText = 'Alternate Sprites';
spriteOptionsWrapper.appendChild(spriteOptionsTitle);
const spriteOptionsDescription = document.createElement('span');
spriteOptionsDescription.className = 'description-span';
spriteOptionsDescription.innerHTML = 'Choose an alternate sprite to play the game with. Additional randomization ' +
'options are documented in the ' +
'<a href="https://github.com/Berserker66/MultiWorld-Utilities/blob/main/playerSettings.yaml#L374">settings file</a>.';
spriteOptionsWrapper.appendChild(spriteOptionsDescription);
const spriteOptionsTable = document.createElement('table');
spriteOptionsTable.setAttribute('id', 'sprite-options-table');
spriteOptionsTable.className = 'option-set';
const tbody = document.createElement('tbody');
tbody.setAttribute('id', 'sprites-tbody');
const currentPreset = document.getElementById('preset-number').value;
const playerSettings = JSON.parse(localStorage.getItem(`weightedSettings${currentPreset}`));
// Manually add a row for random sprites
addSpriteRow(tbody, playerSettings, 'random');
// Add a row for each sprite currently present in the player's settings
Object.keys(playerSettings.rom.sprite).forEach((spriteName) => {
if(['random'].indexOf(spriteName) > -1) return;
addSpriteRow(tbody, playerSettings, spriteName)
});
spriteOptionsTable.appendChild(tbody);
spriteOptionsWrapper.appendChild(spriteOptionsTable);
};
const buildRangeSettings = (parentElement, settings) => {
// Ensure we are operating on a range-specific setting
if(typeof(settings.inputType) === 'undefined' || settings.inputType !== 'range'){
throw new Error('Invalid input type provided to buildRangeSettings func.');
}
const settingWrapper = document.createElement('div');
settingWrapper.className = 'setting-wrapper';
if(typeof(settings.friendlyName) !== 'undefined' && settings.friendlyName){
const sectionTitle = document.createElement('span');
sectionTitle.className = 'title-span';
sectionTitle.innerText = settings.friendlyName;
settingWrapper.appendChild(sectionTitle);
}
if(settings.description){
const description = document.createElement('span');
description.className = 'description-span';
description.innerText = settings.description;
settingWrapper.appendChild(description);
}
// Create table
const optionSetTable = document.createElement('table');
optionSetTable.className = 'option-set';
// Create table body
const tbody = document.createElement('tbody');
Object.keys(settings.subOptions).forEach((setting) => {
// Overwrite setting key name with real object
setting = settings.subOptions[setting];
const settingId = (Math.random() * 1000000).toString();
// Create rows for each option
const optionRow = document.createElement('tr');
// Option name td
const optionName = document.createElement('td');
optionName.className = 'option-name';
const optionLabel = document.createElement('label');
optionLabel.setAttribute('for', settingId);
optionLabel.setAttribute('data-tooltip', setting.description);
optionLabel.innerText = setting.friendlyName;
optionName.appendChild(optionLabel);
optionRow.appendChild(optionName);
// Option value td
const optionValue = document.createElement('td');
optionValue.className = 'option-value';
const input = document.createElement('input');
input.className = 'setting';
input.setAttribute('id', settingId);
input.setAttribute('type', 'range');
input.setAttribute('min', '0');
input.setAttribute('max', '100');
input.setAttribute('data-setting', setting.keyString);
input.value = setting.defaultValue;
optionValue.appendChild(input);
const valueDisplay = document.createElement('span');
valueDisplay.setAttribute('id', setting.keyString);
valueDisplay.innerText = setting.defaultValue;
optionValue.appendChild(valueDisplay);
optionRow.appendChild(optionValue);
tbody.appendChild(optionRow);
});
optionSetTable.appendChild(tbody);
settingWrapper.appendChild(optionSetTable);
parentElement.appendChild(settingWrapper);
};
const addSpriteRow = (tbody, playerSettings, spriteName) => {
const rowId = (Math.random() * 1000000).toString();
const optionId = (Math.random() * 1000000).toString();
const tr = document.createElement('tr');
tr.setAttribute('id', rowId);
// Option Name
const optionName = document.createElement('td');
optionName.className = 'option-name';
const label = document.createElement('label');
label.htmlFor = optionId;
label.innerText = spriteName;
optionName.appendChild(label);
if(['random', 'random_sprite_on_event'].indexOf(spriteName) === -1) {
const deleteButton = document.createElement('span');
deleteButton.setAttribute('data-sprite', spriteName);
deleteButton.setAttribute('data-row-id', rowId);
deleteButton.innerText = ' (❌)';
deleteButton.className = 'delete-button';
optionName.appendChild(deleteButton);
deleteButton.addEventListener('click', removeSpriteOption);
}
tr.appendChild(optionName);
// Option Value
const optionValue = document.createElement('td');
optionValue.className = 'option-value';
const input = document.createElement('input');
input.className = 'setting';
input.setAttribute('id', optionId);
input.setAttribute('type', 'range');
input.setAttribute('min', '0');
input.setAttribute('max', '100');
input.setAttribute('data-setting', `rom.sprite.${spriteName}`);
input.value = "50";
optionValue.appendChild(input);
// Value display
const valueDisplay = document.createElement('span');
valueDisplay.setAttribute('id', `rom.sprite.${spriteName}`);
valueDisplay.innerText = playerSettings.rom.sprite.hasOwnProperty(spriteName) ?
playerSettings.rom.sprite[spriteName] : '0';
optionValue.appendChild(valueDisplay);
tr.appendChild(optionValue);
tbody.appendChild(tr);
};
const addSpriteOption = (event) => {
const presetNumber = document.getElementById('preset-number').value;
const playerSettings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`));
const spriteName = event.target.getAttribute('data-sprite');
if (Object.keys(playerSettings.rom.sprite).indexOf(spriteName) !== -1) {
// Do not add the same sprite twice
return;
}
// Add option to playerSettings object
playerSettings.rom.sprite[event.target.getAttribute('data-sprite')] = 50;
localStorage.setItem(`weightedSettings${presetNumber}`, JSON.stringify(playerSettings));
// Add <tr> to #sprite-options-table
const tbody = document.getElementById('sprites-tbody');
addSpriteRow(tbody, playerSettings, spriteName);
};
const removeSpriteOption = (event) => {
const presetNumber = document.getElementById('preset-number').value;
const playerSettings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`));
const spriteName = event.target.getAttribute('data-sprite');
// Remove option from playerSettings object
delete playerSettings.rom.sprite[spriteName];
localStorage.setItem(`weightedSettings${presetNumber}`, JSON.stringify(playerSettings));
// Remove <tr> from #sprite-options-table
const tr = document.getElementById(event.target.getAttribute('data-row-id'));
tr.parentNode.removeChild(tr);
};
const buildSpritePicker = (spriteData) => {
const spritePicker = document.createElement('div');
spritePicker.setAttribute('id', 'sprite-picker');
// Build description
const description = document.createElement('span');
description.innerText = 'To add a sprite to your playable list, click the one you want below.';
spritePicker.appendChild(description);
const sprites = document.createElement('div');
sprites.setAttribute('id', 'sprite-picker-sprites');
spriteData.sprites.forEach((sprite) => {
const spriteImg = document.createElement('img');
let spriteGifFile = sprite.file.split('.');
spriteGifFile.pop();
spriteGifFile = spriteGifFile.join('.') + '.gif';
spriteImg.setAttribute('src', `static/generated/sprites/${spriteGifFile}`);
spriteImg.setAttribute('data-sprite', sprite.file.split('.')[0]);
spriteImg.setAttribute('alt', sprite.name);
// Wrap the image in a span to allow for tooltip presence
const imgWrapper = document.createElement('span');
imgWrapper.className = 'sprite-img-wrapper';
imgWrapper.setAttribute('data-tooltip', `${sprite.name}${sprite.author ? `, by ${sprite.author}` : ''}`);
imgWrapper.appendChild(spriteImg);
imgWrapper.setAttribute('data-sprite', sprite.name);
sprites.appendChild(imgWrapper);
imgWrapper.addEventListener('click', addSpriteOption);
});
spritePicker.appendChild(sprites);
return spritePicker;
};
const generateGame = (raceMode = false) => {
const presetNumber = document.getElementById('preset-number').value;
axios.post('/api/generate', {
weights: { player: localStorage.getItem(`weightedSettings${presetNumber}`) },
presetData: { player: localStorage.getItem(`weightedSettings${presetNumber}`) },
playerCount: 1,
race: raceMode ? '1' : '0',
}).then((response) => {
window.location.href = response.data.url;
}).catch((error) => {
const userMessage = document.getElementById('user-message');
userMessage.innerText = 'Something went wrong and your game could not be generated.';
if (error.response.data.text) {
userMessage.innerText += ' ' + error.response.data.text;
}
userMessage.classList.add('visible');
window.scrollTo(0, 0);
console.error(error);
});
};

View File

@@ -1,129 +0,0 @@
html{
background-image: url('../../static/backgrounds/grass/grass-0007-large.png');
background-repeat: repeat;
background-size: 650px 650px;
}
#player-settings{
max-width: 1000px;
margin-left: auto;
margin-right: auto;
background-color: rgba(0, 0, 0, 0.15);
border-radius: 8px;
padding: 1rem;
color: #eeffeb;
}
#player-settings #player-settings-button-row{
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 15px;
}
#player-settings code{
background-color: #d9cd8e;
border-radius: 4px;
padding-left: 0.25rem;
padding-right: 0.25rem;
color: #000000;
}
#player-settings #user-message{
display: none;
width: calc(100% - 8px);
background-color: #ffe86b;
border-radius: 4px;
color: #000000;
padding: 4px;
text-align: center;
}
#player-settings #user-message.visible{
display: block;
}
#player-settings h1{
font-size: 2.5rem;
font-weight: normal;
border-bottom: 1px solid #ffffff;
width: 100%;
margin-bottom: 0.5rem;
color: #ffffff;
text-shadow: 1px 1px 4px #000000;
}
#player-settings h2{
font-size: 2rem;
font-weight: normal;
border-bottom: 1px solid #ffffff;
width: 100%;
margin-bottom: 0.5rem;
color: #ffe993;
text-transform: lowercase;
text-shadow: 1px 1px 2px #000000;
}
#player-settings h3, #player-settings h4, #player-settings h5, #player-settings h6{
color: #ffffff;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
}
#player-settings a{
color: #ffef00;
}
#player-settings input:not([type]){
border: 1px solid #000000;
padding: 3px;
border-radius: 3px;
min-width: 150px;
}
#player-settings input:not([type]):focus{
border: 1px solid #ffffff;
}
#player-settings select{
border: 1px solid #000000;
padding: 3px;
border-radius: 3px;
min-width: 150px;
background-color: #ffffff;
}
#player-settings #game-options, #player-settings #rom-options{
display: flex;
flex-direction: row;
}
#player-settings .left, #player-settings .right{
flex-grow: 1;
}
#player-settings table select{
width: 250px;
}
#player-settings table label{
display: block;
min-width: 200px;
margin-right: 4px;
cursor: default;
}
@media all and (max-width: 1000px), all and (orientation: portrait){
#player-settings #game-options, #player-settings #rom-options{
justify-content: flex-start;
flex-wrap: wrap;
}
#player-settings .left, #player-settings .right{
flex-grow: unset;
}
#game-options table label, #rom-options table label{
display: block;
min-width: 200px;
}
}

View File

@@ -60,7 +60,7 @@ html{
width: 200px; width: 200px;
height: calc(156px - 40px); height: calc(156px - 40px);
padding-top: 40px; padding-top: 40px;
cursor: default; cursor: pointer;
} }
#mid-left-button{ #mid-left-button{

View File

@@ -0,0 +1,120 @@
.markdown{
display: flex;
flex-direction: column;
max-width: 70rem;
margin-left: auto;
margin-right: auto;
background-color: rgba(0, 0, 0, 0.15);
border-radius: 8px;
padding: 1rem 1rem 3rem;
color: #eeffeb;
}
.markdown img{
max-width: 100%;
border-radius: 6px;
}
.markdown p{
margin-top: 0;
}
.markdown a{
color: #ffef00;
}
.markdown h1{
font-size: 2.5rem;
font-weight: normal;
border-bottom: 1px solid #ffffff;
cursor: pointer;
width: 100%;
margin-bottom: 0.5rem;
color: #ffffff;
text-shadow: 1px 1px 4px #000000;
}
.markdown h2{
font-size: 2rem;
font-weight: normal;
border-bottom: 1px solid #ffffff;
cursor: pointer;
width: 100%;
margin-bottom: 0.5rem;
color: #ffe993;
text-transform: lowercase;
text-shadow: 1px 1px 2px #000000;
}
.markdown h3{
font-size: 1.70rem;
font-weight: normal;
text-align: left;
cursor: pointer;
width: 100%;
margin-bottom: 0.5rem;
}
.markdown h4{
font-size: 1.5rem;
font-weight: normal;
cursor: pointer;
margin-bottom: 0.5rem;
}
.markdown h5{
font-size: 1.25rem;
font-weight: normal;
cursor: pointer;
}
.markdown h6{
font-size: 1.25rem;
font-weight: normal;
cursor: pointer;
color: #434343;
}
.markdown h3, .markdown h4, .markdown h5,.markdown h6{
color: #ffffff;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
}
.markdown ul{
}
.markdown ol{
}
.markdown li{
}
.markdown pre{
margin-top: 0;
padding: 0.5rem 0.25rem;
background-color: #ffeeab;
border: 1px solid #9f916a;
border-radius: 6px;
color: #000000;
}
.markdown code{
background-color: #ffeeab;
border-radius: 4px;
padding-left: 0.25rem;
padding-right: 0.25rem;
color: #000000;
}
.markdown #tutorial-video-container{
width: 100%;
text-align: center;
}
.markdown #language-selector-wrapper{
width: 100%;
text-align: right;
}

View File

@@ -1,129 +0,0 @@
html{
background-image: url('../../static/backgrounds/grass/grass-0007-large.png');
background-repeat: repeat;
background-size: 650px 650px;
}
#player-settings{
max-width: 1000px;
margin-left: auto;
margin-right: auto;
background-color: rgba(0, 0, 0, 0.15);
border-radius: 8px;
padding: 1rem;
color: #eeffeb;
}
#player-settings #player-settings-button-row{
display: flex;
flex-direction: row;
justify-content: space-between;
margin-top: 15px;
}
#player-settings code{
background-color: #d9cd8e;
border-radius: 4px;
padding-left: 0.25rem;
padding-right: 0.25rem;
color: #000000;
}
#player-settings #user-message{
display: none;
width: calc(100% - 8px);
background-color: #ffe86b;
border-radius: 4px;
color: #000000;
padding: 4px;
text-align: center;
}
#player-settings #user-message.visible{
display: block;
}
#player-settings h1{
font-size: 2.5rem;
font-weight: normal;
border-bottom: 1px solid #ffffff;
width: 100%;
margin-bottom: 0.5rem;
color: #ffffff;
text-shadow: 1px 1px 4px #000000;
}
#player-settings h2{
font-size: 2rem;
font-weight: normal;
border-bottom: 1px solid #ffffff;
width: 100%;
margin-bottom: 0.5rem;
color: #ffe993;
text-transform: lowercase;
text-shadow: 1px 1px 2px #000000;
}
#player-settings h3, #player-settings h4, #player-settings h5, #player-settings h6{
color: #ffffff;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
}
#player-settings a{
color: #ffef00;
}
#player-settings input:not([type]){
border: 1px solid #000000;
padding: 3px;
border-radius: 3px;
min-width: 150px;
}
#player-settings input:not([type]):focus{
border: 1px solid #ffffff;
}
#player-settings select{
border: 1px solid #000000;
padding: 3px;
border-radius: 3px;
min-width: 150px;
background-color: #ffffff;
}
#player-settings #game-options, #player-settings #rom-options{
display: flex;
flex-direction: row;
}
#player-settings .left, #player-settings .right{
flex-grow: 1;
}
#player-settings table select{
width: 250px;
}
#player-settings table label{
display: block;
min-width: 200px;
margin-right: 4px;
cursor: default;
}
@media all and (max-width: 1000px), all and (orientation: portrait){
#player-settings #game-options, #player-settings #rom-options{
justify-content: flex-start;
flex-wrap: wrap;
}
#player-settings .left, #player-settings .right{
flex-grow: unset;
}
#game-options table label, #rom-options table label{
display: block;
min-width: 200px;
}
}

View File

@@ -0,0 +1,102 @@
#player-tracker-wrapper{
margin: 0;
}
#inventory-table{
border-top: 2px solid #000000;
border-left: 2px solid #000000;
border-right: 2px solid #000000;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
padding: 3px 3px 10px;
width: 384px;
background-color: #42b149;
}
#inventory-table td{
width: 40px;
height: 40px;
text-align: center;
vertical-align: middle;
}
#inventory-table img{
height: 100%;
max-width: 40px;
max-height: 40px;
filter: grayscale(100%) contrast(75%) brightness(30%);
}
#inventory-table img.acquired{
filter: none;
}
#inventory-table div.counted-item {
position: relative;
}
#inventory-table div.item-count {
position: absolute;
color: white;
font-family: "Minecraftia", monospace;
font-weight: bold;
bottom: 0px;
right: 0px;
}
#location-table{
width: 384px;
border-left: 2px solid #000000;
border-right: 2px solid #000000;
border-bottom: 2px solid #000000;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
background-color: #42b149;
padding: 0 3px 3px;
font-family: "Minecraftia", monospace;
font-size: 14px;
cursor: default;
}
#location-table th{
vertical-align: middle;
text-align: left;
padding-right: 10px;
}
#location-table td{
padding-top: 2px;
padding-bottom: 2px;
line-height: 20px;
}
#location-table td.counter {
text-align: right;
font-size: 14px;
}
#location-table td.toggle-arrow {
text-align: right;
}
#location-table tr#Total-header {
font-weight: bold;
}
#location-table img{
height: 100%;
max-width: 30px;
max-height: 30px;
}
#location-table tbody.locations {
font-size: 12px;
}
#location-table td.location-name {
padding-left: 16px;
}
.hide {
display: none;
}

View File

@@ -1,5 +1,5 @@
html{ html{
background-image: url('../../static/backgrounds/grass/grass-0007-large.png'); background-image: url('../static/backgrounds/grass/grass-0007-large.png');
background-repeat: repeat; background-repeat: repeat;
background-size: 650px 650px; background-size: 650px 650px;
} }
@@ -101,8 +101,28 @@ html{
flex-grow: 1; flex-grow: 1;
} }
#player-settings table select{ #player-settings table .select-container{
width: 250px; display: flex;
flex-direction: row;
}
#player-settings table .select-container select{
min-width: 200px;
flex-grow: 1;
}
#player-settings table .range-container{
display: flex;
flex-direction: row;
}
#player-settings table .range-container input[type=range]{
flex-grow: 1;
}
#player-settings table .range-value{
min-width: 20px;
margin-left: 0.25rem;
} }
#player-settings table label{ #player-settings table label{
@@ -113,7 +133,7 @@ html{
} }
@media all and (max-width: 1000px), all and (orientation: portrait){ @media all and (max-width: 1000px), all and (orientation: portrait){
#player-settings #game-options, #player-settings #rom-options{ #player-settings #game-options{
justify-content: flex-start; justify-content: flex-start;
flex-wrap: wrap; flex-wrap: wrap;
} }
@@ -122,7 +142,7 @@ html{
flex-grow: unset; flex-grow: unset;
} }
#game-options table label, #rom-options table label{ #game-options table label{
display: block; display: block;
min-width: 200px; min-width: 200px;
} }

View File

@@ -0,0 +1,3 @@
#subnautica{
margin: 1rem;
}

View File

@@ -1,162 +0,0 @@
#weighted-settings{
width: 60rem;
margin-left: auto;
margin-right: auto;
background-color: rgba(0, 0, 0, 0.15);
border-radius: 8px;
padding: 1rem;
color: #eeffeb;
}
#user-warning, #weighted-settings #user-message{
display: none;
width: calc(100% - 8px);
background-color: #ffe86b;
border-radius: 4px;
color: #000000;
padding: 4px;
text-align: center;
cursor: pointer;
}
#weighted-settings #user-message.visible{
display: block;
}
#weighted-settings code{
background-color: #d9cd8e;
border-radius: 4px;
padding-left: 0.25rem;
padding-right: 0.25rem;
color: #000000;
}
#weighted-settings h1{
font-size: 2.5rem;
font-weight: normal;
border-bottom: 1px solid #ffffff;
width: 100%;
margin-bottom: 0.5rem;
color: #ffffff;
text-shadow: 1px 1px 4px #000000;
}
#weighted-settings h2{
font-size: 2rem;
font-weight: normal;
border-bottom: 1px solid #ffffff;
width: 100%;
margin-bottom: 0.5rem;
color: #ffe993;
text-transform: lowercase;
text-shadow: 1px 1px 2px #000000;
}
#weighted-settings h3, #weighted-settings h4, #weighted-settings h5, #weighted-settings h6{
color: #ffffff;
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
}
#weighted-settings .instructions{
text-align: left;
}
#weighted-settings #settings-wrapper .setting-wrapper{
display: flex;
flex-direction: column;
justify-content: flex-start;
width: 100%;
}
#weighted-settings #settings-wrapper .setting-wrapper .title-span{
font-weight: 600;
font-size: 1.25rem;
}
#weighted-settings #settings-wrapper{
margin-top: 1.5rem;
}
#weighted-settings #settings-wrapper #sprite-picker{
margin-bottom: 2rem;
}
#weighted-settings #settings-wrapper #sprite-picker #sprite-picker-sprites{
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
}
#weighted-settings #settings-wrapper #sprite-picker .sprite-img-wrapper{
cursor: pointer;
margin: 10px;
image-rendering: pixelated;
}
/* Center tooltip text for sprite images */
#weighted-settings #settings-wrapper #sprite-picker .sprite-img-wrapper::after{
text-align: center;
}
#weighted-settings #settings-wrapper #sprite-picker .sprite-img-wrapper img{
width: 32px;
height: 48px;
}
#weighted-settings table.option-set{
width: 100%;
margin-bottom: 1.5rem;
}
#weighted-settings table.option-set td.option-name{
width: 150px;
font-weight: 400;
font-size: 1rem;
line-height: 2rem;
}
#weighted-settings table.option-set td.option-name .delete-button{
cursor: pointer;
}
#weighted-settings table.option-set td.option-value{
line-height: 2rem;
}
#weighted-settings table.option-set td.option-value input[type=range]{
width: 90%;
min-width: 300px;
vertical-align: middle;
}
#weighted-settings #weighted-settings-button-row{
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
}
#weighted-settings a{
color: #ffef00;
}
#weighted-settings input:not([type]){
border: 1px solid #000000;
padding: 3px;
border-radius: 3px;
min-width: 150px;
}
#weighted-settings input:not([type]):focus{
border: 1px solid #ffffff;
}
#weighted-settings select{
border: 1px solid #000000;
padding: 3px;
border-radius: 3px;
min-width: 150px;
background-color: #ffffff;
}

View File

@@ -0,0 +1,17 @@
{% extends 'pageWrapper.html' %}
{% block head %}
{% include 'header/grassHeader.html' %}
<title>Frequently Asked Questions</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.1/showdown.min.js"
integrity="sha512-L03kznCrNOfVxOUovR6ESfCz9Gfny7gihUX/huVbQB9zjODtYpxaVtIaAkpetoiyV2eqWbvxMH9fiSv5enX7bw=="
crossorigin="anonymous"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/faq.js") }}"></script>
{% endblock %}
{% block body %}
<div id="faq-wrapper" data-lang="{{ lang }}" class="markdown">
<!-- Content generated by JavaScript -->
</div>
{% endblock %}

View File

@@ -0,0 +1,15 @@
{% extends 'pageWrapper.html' %}
{% block head %}
<title>Subnautica</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/subnautica/subnautica.css") }}" />
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
{% endblock %}
{% block body %}
{% include 'header/grassHeader.html' %}
<div id="subnautica">
Coming Soon™
</div>
{% endblock %}

View File

@@ -9,9 +9,9 @@
{% include 'header/grassHeader.html' %} {% include 'header/grassHeader.html' %}
<div id="games"> <div id="games">
<h1>Currently Supported Games</h1> <h1>Currently Supported Games</h1>
{% for game, (display_name, description) in games_list.items() %} {% for game, description in worlds.items() %}
<h3><a href="{{ url_for("game_page", game=game) }}">{{ display_name}}</a></h3> <h3><a href="{{ url_for("game_page", game=game) }}/player-settings">{{ game }}</a></h3>
<p>{{ description}}</p> <p>{{ description }}</p>
{% endfor %} {% endfor %}
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -15,8 +15,7 @@
<p> <p>
This page allows you to generate a game by uploading a yaml file or a zip file containing yaml files. This page allows you to generate a game by uploading a yaml file or a zip file containing yaml files.
If you do not have a config (yaml) file yet, you may create one on the If you do not have a config (yaml) file yet, you may create one on the
<a href="/player-settings">Player Settings</a> page. If you would like more advanced options, <a href="/player-settings">Player Settings</a> page.
the <a href="/weighted-settings">Weighted Settings</a> page might be what you're looking for.
</p> </p>
<p> <p>
{% if race -%} {% if race -%}

View File

@@ -11,9 +11,11 @@
<a href="/">archipelago</a> <a href="/">archipelago</a>
</div> </div>
<div id="base-header-right"> <div id="base-header-right">
<a href="/games">games</a> <a href="/games">supported games</a>
<a href="/tutorial">setup guides</a> <a href="/tutorial">setup guides</a>
<a href="https://discord.gg/8Z65BR2">discord</a> <a href="/uploads">start game</a>
<a href="/faq/en">f.a.q.</a>
<a href="https://discord.gg/8Z65BR2" target="_blank">discord</a>
</div> </div>
</header> </header>
{% endblock %} {% endblock %}

View File

@@ -10,14 +10,14 @@
<div id="landing-wrapper"> <div id="landing-wrapper">
<div id="landing-header"> <div id="landing-header">
<h1>ARCHIPELAGO</h1> <h1>ARCHIPELAGO</h1>
<h4>multiworld randomizer ecosystem</h4> <h4>multiworld multi-game randomizer</h4>
</div> </div>
<div id="landing-links"> <div id="landing-links">
<a href="/games" id="mid-button">start<br />playing</a> <a href="/uploads" id="mid-button">start<br />game</a>
<a id="far-left-button"></a> <a href="/games" id="far-left-button">supported<br />games</a>
<a href="/tutorial" id="mid-left-button">setup guide</a> <a href="/tutorial" id="mid-left-button">setup guides</a>
<a href="/uploads" id="far-right-button">Host Game</a> <a href="https://discord.gg/8Z65BR2" id="far-right-button" target="_blank">discord</a>
<a href="https://discord.gg/8Z65BR2" id="mid-right-button">discord</a> <a href="/faq/en/" id="mid-right-button">f.a.q.</a>
</div> </div>
<div id="landing-clouds"> <div id="landing-clouds">
<img id="cloud1" src="/static/static/backgrounds/clouds/cloud-0001.png"/> <img id="cloud1" src="/static/static/backgrounds/clouds/cloud-0001.png"/>

View File

@@ -11,11 +11,14 @@
<ul> <ul>
{% for patch in room.seed.slots|list|sort(attribute="player_id") %} {% for patch in room.seed.slots|list|sort(attribute="player_id") %}
{% if patch.game == "Minecraft" %} {% if patch.game == "Minecraft" %}
<li><a href="{{ url_for("download_slot_file", seed_id=room.seed.id, player_id=patch.player_id) }}"> <li><a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}">
APMC for player {{ patch.player_id }} - {{ patch.player_name }}</a></li> APMC for player {{ patch.player_id }} - {{ patch.player_name }}</a></li>
{% elif patch.game == "Factorio" %} {% elif patch.game == "Factorio" %}
<li><a href="{{ url_for("download_slot_file", seed_id=room.seed.id, player_id=patch.player_id) }}"> <li><a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}">
Mod for player {{ patch.player_id }} - {{ patch.player_name }}</a></li> Mod for player {{ patch.player_id }} - {{ patch.player_name }}</a></li>
{% elif patch.game == "Ocarina of Time" %}
<li><a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}">
APZ5 for player {{ patch.player_id }} - {{ patch.player_name }}</a></li>
{% else %} {% else %}
<li><a href="{{ url_for("download_patch", patch_id=patch.id, room_id=room.id) }}"> <li><a href="{{ url_for("download_patch", patch_id=patch.id, room_id=room.id) }}">
Patch for player {{ patch.player_id }} - {{ patch.player_name }}</a></li> Patch for player {{ patch.player_id }} - {{ patch.player_name }}</a></li>

View File

@@ -0,0 +1,66 @@
<!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/minecraftTracker.css') }}"/>
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/minecraftTracker.js') }}"></script>
<link rel="stylesheet" media="screen" href="https://fontlibrary.org//face/minecraftia" type="text/css"/>
</head>
<body>
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
<table id="inventory-table">
<tr>
<td><img src="{{ tools_url }}" class="{{ 'acquired' }}" title="Progressive Tools" /></td>
<td><img src="{{ weapons_url }}" class="{{ 'acquired' }}" title="Progressive Weapons" /></td>
<td><img src="{{ armor_url }}" class="{{ 'acquired' }}" title="Progressive Armor" /></td>
<td><img src="{{ resource_crafting_url }}" class="{{ 'acquired' if 'Progressive Resource Crafting' in acquired_items }}"
title="Progressive Resource Crafting" /></td>
<td><img src="{{ icons['Brewing Stand'] }}" class="{{ 'acquired' if 'Brewing' in acquired_items }}" title="Brewing" /></td>
<td>
<div class="counted-item">
<img src="{{ icons['Ender Pearl'] }}" class="{{ 'acquired' if '3 Ender Pearls' in acquired_items }}" title="Ender Pearls" />
<div class="item-count">{{ pearls_count }}</div>
</div>
</td>
</tr>
<tr>
<td><img src="{{ icons['Bucket'] }}" class="{{ 'acquired' if 'Bucket' in acquired_items }}" title="Bucket" /></td>
<td><img src="{{ icons['Bow'] }}" class="{{ 'acquired' if 'Archery' in acquired_items }}" title="Archery" /></td>
<td><img src="{{ icons['Shield'] }}" class="{{ 'acquired' if 'Shield' in acquired_items }}" title="Shield" /></td>
<td><img src="{{ icons['Red Bed'] }}" class="{{ 'acquired' if 'Bed' in acquired_items }}" title="Bed" /></td>
<td><img src="{{ icons['Water Bottle'] }}" class="{{ 'acquired' if 'Bottles' in acquired_items }}" title="Bottles" /></td>
<td>
<div class="counted-item">
<img src="{{ icons['Netherite Scrap'] }}" class="{{ 'acquired' if '8 Netherite Scrap' in acquired_items }}" title="Netherite Scrap" />
<div class="item-count">{{ scrap_count }}</div>
</div>
</td>
</tr>
<tr>
<td><img src="{{ icons['Flint and Steel'] }}" class="{{ 'acquired' if 'Flint and Steel' in acquired_items }}" title="Flint and Steel" /></td>
<td><img src="{{ icons['Enchanting Table'] }}" class="{{ 'acquired' if 'Enchanting' in acquired_items }}" title="Enchanting" /></td>
<td><img src="{{ icons['Fishing Rod'] }}" class="{{ 'acquired' if 'Fishing Rod' in acquired_items }}" title="Fishing Rod" /></td>
<td><img src="{{ icons['Campfire'] }}" class="{{ 'acquired' if 'Campfire' in acquired_items }}" title="Campfire" /></td>
<td><img src="{{ icons['Dragon Head'] }}" class="{{ 'acquired' if game_finished }}" title="Ender Dragon" /></td>
</tr>
</table>
<table id="location-table">
{% for area in checks_done %}
<tr class="location-category" id="{{area}}-header">
<td>{{ area }} {{'▼' if area != 'Total'}}</td>
<td class="counter">{{ checks_done[area] }} / {{ checks_in_area[area] }}</td>
</tr>
<tbody class="locations hide" id="{{area}}">
{% for location in location_info[area] %}
<tr>
<td class="location-name">{{ location }}</td>
<td class="counter">{{ '✔' if location_info[area][location] else '' }}</td>
</tr>
{% endfor %}
</tbody>
{% endfor %}
</table>
</div>
</body>
</html>

View File

@@ -18,7 +18,8 @@
# http://www.yamllint.com/ # http://www.yamllint.com/
description: Default {{ game }} Template # Used to describe your yaml. Useful if you have multiple files description: Default {{ game }} Template # Used to describe your yaml. Useful if you have multiple files
name: YourName{number} # Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit # Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit
name: YourName{number}
#{player} will be replaced with the player's slot number. #{player} will be replaced with the player's slot number.
#{PLAYER} will be replaced with the player's slot number if that slot number is greater than 1. #{PLAYER} will be replaced with the player's slot number if that slot number is greater than 1.
#{number} will be replaced with the counter value of the name. #{number} will be replaced with the counter value of the name.
@@ -51,21 +52,24 @@ progression_balancing:
# - "Progressive Weapons" # - "Progressive Weapons"
# exclude_locations: # Force certain locations to never contain progression items, and always be filled with junk. # exclude_locations: # Force certain locations to never contain progression items, and always be filled with junk.
# - "Master Sword Pedestal" # - "Master Sword Pedestal"
{{ game }}: {%- macro range_option(option) %}
{%- for option_name, option in options.items() %}
{{ option_name }}:{% if option.__doc__ %} # {{ option.__doc__ }}{% endif %}
{%- if option.range_start is defined %}
# you can add additional values between minimum and maximum # you can add additional values between minimum and maximum
{{ option.range_start }}: 0 # minimum value {%- set data, notes = dictify_range(option) %}
{{ option.range_end }}: 0 # maximum value {%- for entry, default in data.items() %}
random: 50 {{ entry }}: {{ default }}{% if notes[entry] %} # {{ notes[entry] }}{% endif %}
random-low: 0 {%- endfor -%}
random-high: 0 {% endmacro %}
{{ game }}:
{%- for option_key, option in options.items() %}
{{ option_key }}:{% if option.__doc__ %} # {{ option.__doc__ | replace('\n', '\n#') | indent(4, first=False) }}{% endif %}
{%- if option.range_start is defined %}
{{- range_option(option) -}}
{%- elif option.options -%} {%- elif option.options -%}
{%- for sub_option_name, suboption_option_id in option.options.items() %} {%- for suboption_option_id, sub_option_name in option.name_lookup.items() %}
{{ sub_option_name }}: {% if suboption_option_id == option.default %}50{% else %}0{% endif %} {{ sub_option_name }}: {% if suboption_option_id == option.default %}50{% else %}0{% endif %}
{%- endfor -%} {%- endfor -%}
{%- else %} {%- else %}
{{ yaml_dump(option.default) | indent(4, first=False) }} {{ yaml_dump(option.default) | indent(4, first=False) }}
{%- endif -%} {%- endif -%}
{%- endfor -%} {%- endfor %}
{% if not options %}{}{% endif %}

View File

@@ -1,8 +1,8 @@
{% extends 'pageWrapper.html' %} {% extends 'pageWrapper.html' %}
{% block head %} {% block head %}
<title>A Link to the Past Settings</title> <title>{{ game }} Settings</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/zelda3/player-settings.css") }}" /> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/player-settings.css") }}" />
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> <script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script> <script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/player-settings.js") }}"></script> <script type="application/ecmascript" src="{{ url_for('static', filename="assets/player-settings.js") }}"></script>
@@ -10,15 +10,18 @@
{% block body %} {% block body %}
{% include 'header/grassHeader.html' %} {% include 'header/grassHeader.html' %}
<div id="player-settings"> <div id="player-settings" data-game="{{ game }}">
<div id="user-message"></div> <div id="user-message"></div>
<h1><span id="game-name">Player</span> Settings</h1> <h1><span id="game-name">Player</span> Settings</h1>
<p>Choose the options you would like to play with! You may generate a single-player game from this page, <p>Choose the options you would like to play with! You may generate a single-player game from this page,
or download a settings file you can use to participate in a MultiWorld. If you would like to make or download a settings file you can use to participate in a MultiWorld.</p>
your settings extra random, check out the advanced <a href="/weighted-settings">weighted settings</a>
page.</p>
<p>A list of all games you have generated can be found <a href="/user-content">here</a>.</p> <p>
A list of all games you have generated can be found <a href="/user-content">here</a>.
<br />
Advanced users can download a template file for this game
<a href="/static/generated/{{ game }}.yaml">here</a>.
</p>
<p><label for="player-name">Please enter your player name. This will appear in-game as you send and receive <p><label for="player-name">Please enter your player name. This will appear in-game as you send and receive
items if you are playing in a MultiWorld.</label><br /> items if you are playing in a MultiWorld.</label><br />

View File

@@ -0,0 +1,21 @@
{% extends 'pageWrapper.html' %}
{% block head %}
{% include 'header/grassHeader.html' %}
<title>Option Templates (YAML)</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.1/showdown.min.js"
integrity="sha512-L03kznCrNOfVxOUovR6ESfCz9Gfny7gihUX/huVbQB9zjODtYpxaVtIaAkpetoiyV2eqWbvxMH9fiSv5enX7bw=="
crossorigin="anonymous"></script>
{% endblock %}
{% block body %}
<div class="markdown">
<h1>Option Templates (YAML)</h1>
<ul>
{% for file in files %}
<li><a href="{{ url_for('static', filename="generated/"+file+".yaml") }}">{{ file }}</a></li>
{% endfor %}
</ul>
</div>
{% endblock %}

View File

@@ -1,77 +0,0 @@
{% extends 'pageWrapper.html' %}
{% block head %}
<title>Player Settings</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/weightedSettings.css") }}" />
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/weightedSettings.js") }}"></script>
{% endblock %}
{% block body %}
{% include 'header/grassHeader.html' %}
<div id="weighted-settings">
<header id="user-warning"></header>
<div id="user-message"></div>
<h1>Weighted Settings</h1>
<div id="instructions">
This page is used to configure your weighted settings. You have three presets you can control, which
you can access using the dropdown menu below. These settings will be usable when generating a
single player game, or you can export them to a <code>.yaml</code> file and use them in a multiworld.
If you already have a settings file you would like to validate, you may do so on the
<a href="/mysterycheck">verification page</a>.
</div>
<div id="settings-wrapper">
<div class="setting-wrapper">
Choose a preset and optionally assign it a nickname, which will be used as the file's description if
you download it.
<table class="option-set">
<tbody>
<tr>
<td class="option-name">
<label for="preset-number">Preset Number:</label>
</td>
<td class="option-value">
<select id="preset-number">
<option value="1">Preset 1</option>
<option value="2">Preset 2</option>
<option value="3">Preset 3</option>
</select>
</td>
</tr>
<tr>
<td class="option-name">
<label for="description">Preset Name:</label>
</td>
<td class="option-value">
<input id="description" class="setting" data-setting="description" />
</td>
</tr>
</tbody>
</table>
</div>
Choose a name you want to represent you in-game. This will appear when you send items
to other people in multiworld games.
<table class="option-set">
<tbody>
<tr>
<td class="option-name">
<label for="name">Player Name:</label>
</td>
<td class="option-value">
<input id="name" maxlength="16" class="setting" data-setting="name" />
</td>
</tr>
</tbody>
</table>
</div>
<div id="weighted-settings-button-row">
<button id="reset-to-default">Reset to Defaults</button>
<button id="export-button">Export Settings</button>
<button id="generate-game">Generate Game</button>
<button id="generate-race">Generate Race</button>
</div>
</div>
{% endblock %}

View File

@@ -424,6 +424,101 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
key_locations=player_small_key_locations[tracked_player], key_locations=player_small_key_locations[tracked_player],
big_key_locations=player_big_key_locations[tracked_player], big_key_locations=player_big_key_locations[tracked_player],
**display_data) **display_data)
elif games[tracked_player] == "Minecraft":
minecraft_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/f/fa/Brewing_Stand.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/d/dc/Red_Bed_JE4_BE3.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",
"Dragon Head": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b6/Dragon_Head.png",
}
minecraft_location_ids = {
"Story": [42073, 42080, 42081, 42023, 42082, 42027, 42039, 42085, 42002, 42009, 42010,
42070, 42041, 42049, 42090, 42004, 42031, 42025, 42029, 42051, 42077, 42089],
"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, 42086, 42087, 42050, 42059, 42055, 42072, 42003, 42035, 42016, 42020,
42048, 42054, 42068, 42043, 42074, 42075, 42024, 42026, 42037, 42045, 42056, 42088],
"Husbandry": [42065, 42067, 42078, 42022, 42007, 42079, 42013, 42028,
42036, 42057, 42063, 42053, 42083, 42084, 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"] = minecraft_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((tracked_team, tracked_player), 0)
display_data['game_finished'] = game_state == 30
# Turn location IDs into advancement tab counts
checked_locations = multisave.get("location_checks", {}).get((tracked_team, tracked_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=minecraft_icons, acquired_items={lookup_any_item_id_to_name[id] for id in inventory if id in lookup_any_item_id_to_name},
player=tracked_player, team=tracked_team, room=room, player_name=player_name,
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
**display_data)
else: else:
checked_locations = multisave.get("location_checks", {}).get((tracked_team, tracked_player), set()) checked_locations = multisave.get("location_checks", {}).get((tracked_team, tracked_player), set())
return render_template("genericTracker.html", return render_template("genericTracker.html",

View File

@@ -58,11 +58,17 @@ def uploads():
game="Minecraft")) game="Minecraft"))
elif file.filename.endswith(".zip"): elif file.filename.endswith(".zip"):
# Factorio mods needs a specific name or they do no function # Factorio mods needs a specific name or they do not function
_, seed_name, slot_id, slot_name = file.filename.rsplit("_", 1)[0].split("-") _, seed_name, slot_id, slot_name = file.filename.rsplit("_", 1)[0].split("-")
slots.add(Slot(data=zfile.open(file, "r").read(), player_name=slot_name, slots.add(Slot(data=zfile.open(file, "r").read(), player_name=slot_name,
player_id=int(slot_id[1:]), game="Factorio")) player_id=int(slot_id[1:]), game="Factorio"))
elif file.filename.endswith(".apz5"):
# .apz5 must be named specifically since they don't contain any metadata
_, seed_name, slot_id, slot_name = file.filename.split('.')[0].split('_', 3)
slots.add(Slot(data=zfile.open(file, "r").read(), player_name=slot_name,
player_id=int(slot_id[1:]), game="Ocarina of Time"))
elif file.filename.endswith(".txt"): elif file.filename.endswith(".txt"):
spoiler = zfile.open(file, "r").read().decode("utf-8-sig") spoiler = zfile.open(file, "r").read().decode("utf-8-sig")
elif file.filename.endswith(".archipelago"): elif file.filename.endswith(".archipelago"):

View File

@@ -11,6 +11,7 @@
size_hint_y: None size_hint_y: None
height: self.texture_size[1] height: self.texture_size[1]
font_size: dp(20) font_size: dp(20)
markup: True
<UILog>: <UILog>:
viewclass: 'Row' viewclass: 'Row'
scroll_y: 0 scroll_y: 0

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
data/mcicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -43,6 +43,7 @@ These packets are are sent from the multiworld server to the client. They are no
* [Print](#Print) * [Print](#Print)
* [PrintJSON](#PrintJSON) * [PrintJSON](#PrintJSON)
* [DataPackage](#DataPackage) * [DataPackage](#DataPackage)
* [Bounced](#Bounced)
### RoomInfo ### RoomInfo
Sent to clients when they connect to an Archipelago server. Sent to clients when they connect to an Archipelago server.
@@ -148,6 +149,15 @@ Sent to clients to provide what is known as a 'data package' which contains info
| ---- | ---- | ----- | | ---- | ---- | ----- |
| data | DataPackageObject | The data package as a JSON object. More details on its contents may be found at [Data Package Contents](#Data-Package-Contents) | | data | DataPackageObject | The data package as a JSON object. More details on its contents may be found at [Data Package Contents](#Data-Package-Contents) |
### Bounced
Sent to clients after a client requested this message be sent to them, more info in the Bounce package.
#### Arguments
| Name | Type | Notes |
| ---- | ---- | ----- |
| data | dict | The data in the Bounce package copied |
## (Client -> Server) ## (Client -> Server)
These packets are sent purely from client to server. They are not accepted by clients. These packets are sent purely from client to server. They are not accepted by clients.
@@ -158,6 +168,7 @@ These packets are sent purely from client to server. They are not accepted by cl
* [StatusUpdate](#StatusUpdate) * [StatusUpdate](#StatusUpdate)
* [Say](#Say) * [Say](#Say)
* [GetDataPackage](#GetDataPackage) * [GetDataPackage](#GetDataPackage)
* [Bounce](#Bounce)
### Connect ### Connect
Sent by the client to initiate a connection to an Archipelago game session. Sent by the client to initiate a connection to an Archipelago game session.
@@ -220,6 +231,19 @@ Requests the data package from the server. Does not require client authenticatio
| ------ | ----- | ------ | | ------ | ----- | ------ |
| exlusions | list[str] | Optional. If specified, will not send back the specified data. Such as, ["Factorio"] -> Datapackage without Factorio data.| | exlusions | list[str] | Optional. If specified, will not send back the specified data. Such as, ["Factorio"] -> Datapackage without Factorio data.|
### Bounce
Send this message to the server, tell it which clients should receive the message and
the server will forward the message to all those targets to which any one requirement applies.
#### Arguments
| Name | Type | Notes |
| ------ | ----- | ------ |
| games | list[str] | Optional. Game names that should receive this message |
| slots | list[int] | Optional. Player IDs that should receive this message |
| tags | list[str] | Optional. Client tags that should receive this message |
| data | dict | Any data you want to send |
## Appendix ## Appendix
### NetworkPlayer ### NetworkPlayer
A list of objects. Each object denotes one player. Each object has four fields about the player, in this order: `team`, `slot`, `alias`, and `name`. `team` and `slot` are ints, `alias` and `name` are strs. A list of objects. Each object denotes one player. Each object has four fields about the player, in this order: `team`, `slot`, `alias`, and `name`. `team` and `slot` are ints, `alias` and `name` are strs.
@@ -340,8 +364,13 @@ Note:
#### Contents #### Contents
| Name | Type | Notes | | Name | Type | Notes |
| ------ | ----- | ------ | | ------ | ----- | ------ |
| games | dict[str, dict] | Mapping of all Games and their respective data | | games | dict[str, GameData] | Mapping of all Games and their respective data |
| games[<game_name>]["item_name_to_id"] | dict[int, str] | Mapping of all item names to their respective ID. |
| games[<game_name>]["location_name_to_id"] | dict[str, int] | Mapping of all location names to their respective ID. |
| games[<game_name>]["version"] | int | Version number of this game's data |
| version | int | Sum of all per-game version numbers, for clients that don't bother with per-game caching/updating. | | version | int | Sum of all per-game version numbers, for clients that don't bother with per-game caching/updating. |
#### GameData
GameData is a **dict** but contains these keys and values. It's broken out into another "type" for ease of documentation.
| Name | Type | Notes |
| ---- | ---- | ----- |
| item_name_to_id | dict[str, int] | Mapping of all item names to their respective ID. |
| location_name_to_id | dict[str, int] | Mapping of all location names to their respective ID. |
| version | int | Version number of this game's data |

View File

@@ -17,10 +17,10 @@
<node id="n0"> <node id="n0">
<data key="d6"> <data key="d6">
<y:ShapeNode> <y:ShapeNode>
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="608.5730559999993" y="400.2375040000002"/> <y:Geometry height="50.48000000000002" width="524.7469440000009" x="298.1461119999983" y="400.2375040000002"/>
<y:Fill color="#FFCC00" transparent="false"/> <y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="106.052734375" x="54.13363281249997" xml:space="preserve" y="15.889414062500009">Archipelago Server<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel> <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="106.052734375" x="209.34710481250045" xml:space="preserve" y="15.889414062500009">Archipelago Server<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/> <y:Shape type="rectangle"/>
</y:ShapeNode> </y:ShapeNode>
</data> </data>
@@ -124,7 +124,7 @@
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="608.5730559999993" y="550.637504"/> <y:Geometry height="50.48000000000002" width="214.31999999999994" x="608.5730559999993" y="550.637504"/>
<y:Fill color="#FFCC00" transparent="false"/> <y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="56.025390625" x="79.14730468749997" xml:space="preserve" y="15.889414062500009">LttPClient<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel> <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="104.04296875" x="55.13851562499997" xml:space="preserve" y="15.889414062500009">LttPClient/Z3Client<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/> <y:Shape type="rectangle"/>
</y:ShapeNode> </y:ShapeNode>
</data> </data>
@@ -135,7 +135,7 @@
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="608.5730559999993" y="667.1550080000001"/> <y:Geometry height="50.48000000000002" width="214.31999999999994" x="608.5730559999993" y="667.1550080000001"/>
<y:Fill color="#FFCC00" transparent="false"/> <y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="77.359375" x="68.48031249999997" xml:space="preserve" y="15.889414062500009">QUSB2SNES<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel> <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="24.00390625" x="95.15804687499997" xml:space="preserve" y="15.889414062500009">SNI<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/> <y:Shape type="rectangle"/>
</y:ShapeNode> </y:ShapeNode>
</data> </data>
@@ -234,7 +234,7 @@
<y:ProxyAutoBoundsNode> <y:ProxyAutoBoundsNode>
<y:Realizers active="0"> <y:Realizers active="0">
<y:GroupNode> <y:GroupNode>
<y:Geometry height="211.29396884375012" width="244.31999999999994" x="287.1461119999983" y="362.8610391562502"/> <y:Geometry height="211.29396884375012" width="244.31999999999994" x="298.1461119999983" y="513.26103915625"/>
<y:Fill color="#F5F5F5" transparent="false"/> <y:Fill color="#F5F5F5" transparent="false"/>
<y:BorderStyle color="#000000" type="dashed" width="1.0"/> <y:BorderStyle color="#000000" type="dashed" width="1.0"/>
<y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="244.31999999999994" x="0.0" xml:space="preserve" y="0.0">Minecraft</y:NodeLabel> <y:NodeLabel alignment="right" autoSizePolicy="node_width" backgroundColor="#EBEBEB" borderDistance="0.0" fontFamily="Dialog" fontSize="15" fontStyle="plain" hasLineColor="false" height="22.37646484375" horizontalTextPosition="center" iconTextGap="4" modelName="internal" modelPosition="t" textColor="#000000" verticalTextPosition="bottom" visible="true" width="244.31999999999994" x="0.0" xml:space="preserve" y="0.0">Minecraft</y:NodeLabel>
@@ -260,7 +260,7 @@
<node id="n4::n0"> <node id="n4::n0">
<data key="d6"> <data key="d6">
<y:ShapeNode> <y:ShapeNode>
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="302.1461119999983" y="400.2375040000002"/> <y:Geometry height="50.48000000000002" width="214.31999999999994" x="313.1461119999983" y="550.637504"/>
<y:Fill color="#FFCC00" transparent="false"/> <y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="173.40625" x="20.456874999999968" xml:space="preserve" y="15.889414062500009">Modded Minecraft Forge Server<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel> <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="173.40625" x="20.456874999999968" xml:space="preserve" y="15.889414062500009">Modded Minecraft Forge Server<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
@@ -271,7 +271,7 @@
<node id="n4::n1"> <node id="n4::n1">
<data key="d6"> <data key="d6">
<y:ShapeNode> <y:ShapeNode>
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="302.14611199999837" y="508.6750080000003"/> <y:Geometry height="50.48000000000002" width="214.31999999999994" x="313.14611199999837" y="659.0750080000001"/>
<y:Fill color="#FFCC00" transparent="false"/> <y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/> <y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="117.373046875" x="48.47347656249997" xml:space="preserve" y="15.889414062500009">Any Minecraft Clients<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel> <y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="117.373046875" x="48.47347656249997" xml:space="preserve" y="15.889414062500009">Any Minecraft Clients<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
@@ -281,13 +281,25 @@
</node> </node>
</graph> </graph>
</node> </node>
<node id="n5">
<data key="d6">
<y:ShapeNode>
<y:Geometry height="50.48000000000002" width="214.31999999999994" x="313.1461119999983" y="270.35625600000026"/>
<y:Fill color="#FFCC00" transparent="false"/>
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="110.74609375" x="51.78695312499997" xml:space="preserve" y="15.889414062500009">Modded Subnautica<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
<y:Shape type="rectangle"/>
</y:ShapeNode>
</data>
</node>
<edge id="e0" source="n2::n0" target="n0"> <edge id="e0" source="n2::n0" target="n0">
<data key="d10"> <data key="d10">
<y:PolyLineEdge> <y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> <y:Path sx="0.0" sy="0.0" tx="155.21347200000048" ty="-25.23531650000018">
<y:Point x="715.7330559999992" y="458.5"/>
</y:Path>
<y:LineStyle color="#000000" type="line" width="1.0"/> <y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/> <y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.359375" x="17.160336273436542" xml:space="preserve" y="-59.31059414453114">WebSocket<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="49.83999999999992" distanceToCenter="true" position="right" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/> <y:BendStyle smoothed="false"/>
</y:PolyLineEdge> </y:PolyLineEdge>
</data> </data>
@@ -295,9 +307,10 @@
<edge id="e1" source="n0" target="n2::n0"> <edge id="e1" source="n0" target="n2::n0">
<data key="d10"> <data key="d10">
<y:PolyLineEdge> <y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> <y:Path sx="155.21347200000048" sy="-7.977504000000181" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/> <y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/> <y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.359375" x="28.320336273436396" xml:space="preserve" y="24.612501558593806">WebSocket<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="60.999999999999886" distanceToCenter="true" position="left" ratio="0.2753403078759233" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/> <y:BendStyle smoothed="false"/>
</y:PolyLineEdge> </y:PolyLineEdge>
</data> </data>
@@ -357,7 +370,7 @@
<edge id="e3" source="n1::n0" target="n0"> <edge id="e3" source="n1::n0" target="n0">
<data key="d10"> <data key="d10">
<y:PolyLineEdge> <y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="71.68000000000018" ty="0.0"/> <y:Path sx="0.0" sy="0.0" tx="175.50327055767139" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/> <y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/> <y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.359375" x="-92.86621678125107" xml:space="preserve" y="-39.350590482421694">WebSocket<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel> <y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.359375" x="-92.86621678125107" xml:space="preserve" y="-39.350590482421694">WebSocket<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
@@ -424,7 +437,7 @@
<edge id="e4" source="n3::n2" target="n0"> <edge id="e4" source="n3::n2" target="n0">
<data key="d10"> <data key="d10">
<y:PolyLineEdge> <y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> <y:Path sx="0.0" sy="0.0" tx="155.21347200000048" ty="-3.977504000000181"/>
<y:LineStyle color="#000000" type="line" width="1.0"/> <y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/> <y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="80.04296875" x="27.978539398436737" xml:space="preserve" y="30.350051386718974">Subprocesses<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="68.0" distanceToCenter="true" position="left" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel> <y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="80.04296875" x="27.978539398436737" xml:space="preserve" y="30.350051386718974">Subprocesses<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="68.0" distanceToCenter="true" position="left" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
@@ -477,7 +490,7 @@
<edge id="e5" source="n0" target="n4::n0"> <edge id="e5" source="n0" target="n4::n0">
<data key="d10"> <data key="d10">
<y:PolyLineEdge> <y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> <y:Path sx="-140.21347200000048" sy="-1.977504000000181" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/> <y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/> <y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/> <y:BendStyle smoothed="false"/>
@@ -487,10 +500,10 @@
<edge id="e6" source="n4::n0" target="n0"> <edge id="e6" source="n4::n0" target="n0">
<data key="d10"> <data key="d10">
<y:PolyLineEdge> <y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> <y:Path sx="0.0" sy="0.0" tx="-140.21347200000048" ty="-12.977504000000181"/>
<y:LineStyle color="#000000" type="line" width="1.0"/> <y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/> <y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.359375" x="21.74755551171802" xml:space="preserve" y="-39.350590482421694">WebSocket<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="left" ratio="1.0" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel> <y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.359375" x="32.320302673826404" xml:space="preserve" y="-78.31059414453114">WebSocket<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="65.0" distanceToCenter="true" position="right" ratio="0.7667833843973411" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/> <y:BendStyle smoothed="false"/>
</y:PolyLineEdge> </y:PolyLineEdge>
</data> </data>
@@ -511,7 +524,30 @@
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/> <y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/> <y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/> <y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="28.0" x="15.999990173826404" xml:space="preserve" y="-38.32934824804664">TCP<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel> <y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="28.0" x="15.999990173826461" xml:space="preserve" y="-38.32934214453121">TCP<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="0.0" distance="30.0" distanceToCenter="true" position="right" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e7" source="n5" target="n0">
<data key="d9"/>
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="0.0" sy="0.0" tx="-138.06736000000217" ty="25.21780849999982"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="18.701171875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="65.359375" x="33.223286918485144" xml:space="preserve" y="31.70008944921858">WebSocket<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="65.32870706273742" distanceToCenter="true" position="left" ratio="0.5266279296932641" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
<y:BendStyle smoothed="false"/>
</y:PolyLineEdge>
</data>
</edge>
<edge id="e8" source="n0" target="n5">
<data key="d9"/>
<data key="d10">
<y:PolyLineEdge>
<y:Path sx="-140.21347200000048" sy="4.022495999999819" tx="0.0" ty="0.0"/>
<y:LineStyle color="#000000" type="line" width="1.0"/>
<y:Arrows source="none" target="standard"/>
<y:BendStyle smoothed="false"/> <y:BendStyle smoothed="false"/>
</y:PolyLineEdge> </y:PolyLineEdge>
</data> </data>

View File

@@ -1,80 +0,0 @@
#define sourcepath "build_factorio\exe.win-amd64-3.8\"
#define MyAppName "Archipelago Factorio Client"
#define MyAppExeName "ArchipelagoGraphicalFactorioClient.exe"
#define MyAppIcon "data/icon.ico"
[Setup]
; NOTE: The value of AppId uniquely identifies this application.
; Do not use the same AppId value in installers for other applications.
AppId={{D13CEBD0-F1D5-4435-A4A6-5243F934613F}}
AppName={#MyAppName}
AppVerName={#MyAppName}
DefaultDirName={commonappdata}\{#MyAppName}
DisableProgramGroupPage=yes
DefaultGroupName=Archipelago
OutputDir=setups
OutputBaseFilename=Setup {#MyAppName}
Compression=lzma2
SolidCompression=yes
LZMANumBlockThreads=8
ArchitecturesInstallIn64BitMode=x64
ChangesAssociations=yes
ArchitecturesAllowed=x64
AllowNoIcons=yes
SetupIconFile={#MyAppIcon}
UninstallDisplayIcon={app}\{#MyAppExeName}
SignTool= signtool
LicenseFile= LICENSE
WizardStyle= modern
SetupLogging=yes
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}";
[Dirs]
NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify;
[Files]
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
Source: "{#sourcepath}*"; Excludes: "*.sfc, *.log, data\sprites\alttpr"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons]
Name: "{group}\{#MyAppName} Folder"; Filename: "{app}";
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}";
Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon
Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Run]
Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..."
[UninstallDelete]
Type: dirifempty; Name: "{app}"
[Code]
// See: https://stackoverflow.com/a/51614652/2287576
function IsVCRedist64BitNeeded(): boolean;
var
strVersion: string;
begin
if (RegQueryStringValue(HKEY_LOCAL_MACHINE,
'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64', 'Version', strVersion)) then
begin
// Is the installed version at least the packaged one ?
Log('VC Redist x64 Version : found ' + strVersion);
Result := (CompareStr(strVersion, 'v14.29.30037') < 0);
end
else
begin
// Not even an old version installed
Log('VC Redist x64 is not already installed');
Result := True;
end;
end;

View File

@@ -45,9 +45,6 @@ server_options:
log_network: 0 log_network: 0
# Options for Generation # Options for Generation
generator: generator:
# Teams
# Note that this feature is TODO: to move it to dynamic creation on server, not during generation
teams: 1
# Location of your Enemizer CLI, available here: https://github.com/Ijwu/Enemizer/releases # Location of your Enemizer CLI, available here: https://github.com/Ijwu/Enemizer/releases
enemizer_path: "EnemizerCLI/EnemizerCLI.Core.exe" enemizer_path: "EnemizerCLI/EnemizerCLI.Core.exe"
# Folder from which the player yaml files are pulled from # Folder from which the player yaml files are pulled from
@@ -85,4 +82,10 @@ lttp_options:
# Alternatively, a path to a program to open the .sfc file with # Alternatively, a path to a program to open the .sfc file with
rom_start: true rom_start: true
factorio_options: factorio_options:
executable: "factorio\\bin\\x64\\factorio" executable: "factorio\\bin\\x64\\factorio"
minecraft_options:
forge_directory: "Minecraft Forge server"
max_heap_size: "2G"
oot_options:
# File name of the OoT v1.0 ROM
rom_file: "The Legend of Zelda - Ocarina of Time.z64"

View File

@@ -1,19 +1,25 @@
#define sourcepath "build\exe.win-amd64-3.8\" #define sourcepath "build\exe.win-amd64-3.8"
#define MyAppName "Archipelago" #define MyAppName "Archipelago"
#define MyAppExeName "ArchipelagoLttPClient.exe" #define MyAppExeName "ArchipelagoServer.exe"
#define MyAppIcon "data/icon.ico" #define MyAppIcon "data/icon.ico"
#dim VersionTuple[4]
#define MyAppVersion ParseVersion('build\exe.win-amd64-3.8\ArchipelagoServer.exe', VersionTuple[0], VersionTuple[1], VersionTuple[2], VersionTuple[3])
#define MyAppVersionText Str(VersionTuple[0])+"."+Str(VersionTuple[1])+"."+Str(VersionTuple[2])
[Setup] [Setup]
; NOTE: The value of AppId uniquely identifies this application. ; NOTE: The value of AppId uniquely identifies this application.
; Do not use the same AppId value in installers for other applications. ; Do not use the same AppId value in installers for other applications.
AppId={{918BA46A-FAB8-460C-9DFF-AE691E1C865B}} AppId={{918BA46A-FAB8-460C-9DFF-AE691E1C865B}}
AppName={#MyAppName} AppName={#MyAppName}
AppVerName={#MyAppName} AppCopyright=Distributed under MIT License
AppVerName={#MyAppName} {#MyAppVersionText}
VersionInfoVersion={#MyAppVersion}
DefaultDirName={commonappdata}\{#MyAppName} DefaultDirName={commonappdata}\{#MyAppName}
DisableProgramGroupPage=yes DisableProgramGroupPage=yes
DefaultGroupName=Archipelago DefaultGroupName=Archipelago
OutputDir=setups OutputDir=setups
OutputBaseFilename=Setup {#MyAppName} OutputBaseFilename=Setup {#MyAppName} {#MyAppVersionText}
Compression=lzma2 Compression=lzma2
SolidCompression=yes SolidCompression=yes
LZMANumBlockThreads=8 LZMANumBlockThreads=8
@@ -23,6 +29,7 @@ ArchitecturesAllowed=x64
AllowNoIcons=yes AllowNoIcons=yes
SetupIconFile={#MyAppIcon} SetupIconFile={#MyAppIcon}
UninstallDisplayIcon={app}\{#MyAppExeName} UninstallDisplayIcon={app}\{#MyAppExeName}
; you will likely have to remove the following signtool line when testing/debugging localy. Don't include that change in PRs.
SignTool= signtool SignTool= signtool
LicenseFile= LICENSE LicenseFile= LICENSE
WizardStyle= modern WizardStyle= modern
@@ -34,44 +41,86 @@ Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks] [Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}";
[Types]
Name: "full"; Description: "Full installation"
Name: "hosting"; Description: "Installation for hosting purposes"
Name: "playing"; Description: "Installation for playing purposes"
Name: "custom"; Description: "Custom installation"; Flags: iscustom
[Components]
Name: "core"; Description: "Core Files"; Types: full hosting playing custom; Flags: fixed
Name: "generator"; Description: "Generator"; Types: full hosting
Name: "server"; Description: "Server"; Types: full hosting
Name: "client"; Description: "Clients"; Types: full playing
Name: "client/lttp"; Description: "A Link to the Past"; Types: full playing hosting
Name: "client/factorio"; Description: "Factorio"; Types: full playing
Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
[Dirs] [Dirs]
NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify; NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify;
[Files] [Files]
Source: "{code:GetROMPath}"; DestDir: "{app}"; DestName: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"; Flags: external Source: "{code:GetROMPath}"; DestDir: "{app}"; DestName: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"; Flags: external; Components: client/lttp or generator
Source: "{#sourcepath}*"; Excludes: "*.sfc, *.log, data\sprites\alttpr"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "{#sourcepath}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, *exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "{#sourcepath}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/lttp
Source: "{#sourcepath}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator
Source: "{#sourcepath}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator
Source: "{#sourcepath}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server
Source: "{#sourcepath}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio
Source: "{#sourcepath}\ArchipelagoLttPClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/lttp
Source: "{#sourcepath}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/lttp
Source: "{#sourcepath}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
;minecraft temp files
Source: "{tmp}\forge-installer.jar"; DestDir: "{app}"; Flags: skipifsourcedoesntexist external deleteafterinstall; Components: client/minecraft
[Icons] [Icons]
Name: "{group}\{#MyAppName} Folder"; Filename: "{app}"; Name: "{group}\{#MyAppName} Folder"; Filename: "{app}";
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Name: "{group}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Components: server
Name: "{group}\{#MyAppName} LttP Client"; Filename: "{app}\ArchipelagoLttPClient.exe"; Components: client/lttp
Name: "{group}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Components: client/factorio
Name: "{group}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Components: client/minecraft
Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon
Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Components: server
Name: "{commondesktop}\{#MyAppName} LttP Client"; Filename: "{app}\ArchipelagoLttPClient.exe"; Tasks: desktopicon; Components: client/lttp
Name: "{commondesktop}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Tasks: desktopicon; Components: client/factorio
[Run] [Run]
Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..." Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..."
Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..." Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."; Components: client/lttp or generator
Filename: "{app}\jre8\bin\java.exe"; Parameters: "-jar ""{app}\forge-installer.jar"" --installServer ""{app}\Minecraft Forge server"""; Flags: runhidden; Check: IsForgeNeeded(); StatusMsg: "Installing Forge Server..."; Components: client/minecraft
[UninstallDelete] [UninstallDelete]
Type: dirifempty; Name: "{app}" Type: dirifempty; Name: "{app}"
[Registry] [Registry]
Root: HKCR; Subkey: ".apbp"; ValueData: "{#MyAppName}patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "" Root: HKCR; Subkey: ".apbp"; ValueData: "{#MyAppName}patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/lttp
Root: HKCR; Subkey: "{#MyAppName}patch"; ValueData: "Archipelago Binary Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "" Root: HKCR; Subkey: "{#MyAppName}patch"; ValueData: "Archipelago Binary Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/lttp
Root: HKCR; Subkey: "{#MyAppName}patch\DefaultIcon"; ValueData: "{app}\{#MyAppExeName},0"; ValueType: string; ValueName: "" Root: HKCR; Subkey: "{#MyAppName}patch\DefaultIcon"; ValueData: "{app}\ArchipelagoLttPClient.exe,0"; ValueType: string; ValueName: ""; Components: client/lttp
Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; ValueType: string; ValueName: "" Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\ArchipelagoLttPClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/lttp
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "" Root: HKCR; Subkey: ".apmc"; ValueData: "{#MyAppName}mcdata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/minecraft
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "" Root: HKCR; Subkey: "{#MyAppName}mcdata"; ValueData: "Archipelago Minecraft Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/minecraft
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: "" Root: HKCR; Subkey: "{#MyAppName}mcdata\DefaultIcon"; ValueData: "{app}\ArchipelagoMinecraftClient.exe,0"; ValueType: string; ValueName: ""; Components: client/minecraft
Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{app}\ArchipelagoServer.exe"" ""%1"""; ValueType: string; ValueName: "" Root: HKCR; Subkey: "{#MyAppName}mcdata\shell\open\command"; ValueData: """{app}\ArchipelagoMinecraftClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/minecraft
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: server
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: server
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: ""; Components: server
Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{app}\ArchipelagoServer.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: server
[Code] [Code]
const
SHCONTCH_NOPROGRESSBOX = 4;
SHCONTCH_RESPONDYESTOALL = 16;
FORGE_VERSION = '1.16.5-36.2.0';
// See: https://stackoverflow.com/a/51614652/2287576 // See: https://stackoverflow.com/a/51614652/2287576
function IsVCRedist64BitNeeded(): boolean; function IsVCRedist64BitNeeded(): boolean;
var var
@@ -92,11 +141,54 @@ begin
end; end;
end; end;
function IsForgeNeeded(): boolean;
begin
Result := True;
if (FileExists(ExpandConstant('{app}')+'\Minecraft Forge Server\forge-'+FORGE_VERSION+'.jar')) then
Result := False;
end;
function IsJavaNeeded(): boolean;
begin
Result := True;
if (FileExists(ExpandConstant('{app}')+'\jre8\bin\java.exe')) then
Result := False;
end;
function OnDownloadMinecraftProgress(const Url, FileName: String; const Progress, ProgressMax: Int64): Boolean;
begin
if Progress = ProgressMax then
Log(Format('Successfully downloaded Minecraft additional files to {tmp}: %s', [FileName]));
Result := True;
end;
procedure UnZip(ZipPath, TargetPath: string);
var
Shell: Variant;
ZipFile: Variant;
TargetFolder: Variant;
begin
Shell := CreateOleObject('Shell.Application');
ZipFile := Shell.NameSpace(ZipPath);
if VarIsClear(ZipFile) then
RaiseException(
Format('ZIP file "%s" does not exist or cannot be opened', [ZipPath]));
TargetFolder := Shell.NameSpace(TargetPath);
if VarIsClear(TargetFolder) then
RaiseException(Format('Target path "%s" does not exist', [TargetPath]));
TargetFolder.CopyHere(
ZipFile.Items, SHCONTCH_NOPROGRESSBOX or SHCONTCH_RESPONDYESTOALL);
end;
var ROMFilePage: TInputFileWizardPage; var ROMFilePage: TInputFileWizardPage;
var R : longint; var R : longint;
var rom: string; var rom: string;
var MinecraftDownloadPage: TDownloadWizardPage;
procedure InitializeWizard(); procedure AddRomPage();
begin begin
rom := FileSearch('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', WizardDirValue()); rom := FileSearch('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', WizardDirValue());
if Length(rom) > 0 then if Length(rom) > 0 then
@@ -113,15 +205,65 @@ begin
rom := '' rom := ''
ROMFilePage := ROMFilePage :=
CreateInputFilePage( CreateInputFilePage(
wpLicense, wpSelectComponents,
'Select ROM File', 'Select ROM File',
'Where is your Zelda no Densetsu - Kamigami no Triforce (Japan).sfc located?', 'Where is your Zelda no Densetsu - Kamigami no Triforce (Japan).sfc located?',
'Select the file, then click Next.'); 'Select the file, then click Next.');
ROMFilePage.Add( ROMFilePage.Add(
'Location of ROM file:', 'Location of ROM file:',
'SNES ROM files|*.sfc|All files|*.*', 'SNES ROM files|*.sfc|All files|*.*',
'.sfc'); '.sfc');
end;
procedure AddMinecraftDownloads();
begin
MinecraftDownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), @OnDownloadMinecraftProgress);
end;
function NextButtonClick(CurPageID: Integer): Boolean;
begin
if (CurPageID = wpReady) and (WizardIsComponentSelected('client/minecraft')) then begin
MinecraftDownloadPage.Clear;
if(IsForgeNeeded()) then
MinecraftDownloadPage.Add('https://maven.minecraftforge.net/net/minecraftforge/forge/'+FORGE_VERSION+'/forge-'+FORGE_VERSION+'-installer.jar','forge-installer.jar','');
if(IsJavaNeedeD()) then
MinecraftDownloadPage.Add('https://corretto.aws/downloads/latest/amazon-corretto-8-x64-windows-jre.zip','java.zip','');
MinecraftDownloadPage.Show;
try
try
MinecraftDownloadPage.Download;
Result := True;
except
if MinecraftDownloadPage.AbortedByUser then
Log('Aborted by user.')
else
SuppressibleMsgBox(AddPeriod(GetExceptionMessage), mbCriticalError, MB_OK, IDOK);
Result := False;
end;
finally
if( isJavaNeeded() ) then
if(ForceDirectories(ExpandConstant('{app}'))) then
UnZip(ExpandConstant('{tmp}')+'\java.zip',ExpandConstant('{app}'));
MinecraftDownloadPage.Hide;
end;
Result := True;
end else
Result := True;
end;
procedure InitializeWizard();
begin
AddRomPage();
AddMinecraftDownloads();
end;
function ShouldSkipPage(PageID: Integer): Boolean;
begin
Result := False;
if (assigned(ROMFilePage)) and (PageID = ROMFilePage.ID) then
Result := not (WizardIsComponentSelected('client/lttp') or WizardIsComponentSelected('generator'));
end; end;
function GetROMPath(Param: string): string; function GetROMPath(Param: string): string;

View File

@@ -1,141 +0,0 @@
#define sourcepath "build\exe.win-amd64-3.9\"
#define MyAppName "Archipelago"
#define MyAppExeName "ArchipelagoLttPClient.exe"
#define MyAppIcon "data/icon.ico"
[Setup]
; NOTE: The value of AppId uniquely identifies this application.
; Do not use the same AppId value in installers for other applications.
AppId={{918BA46A-FAB8-460C-9DFF-AE691E1C865B}}
AppName={#MyAppName}
AppVerName={#MyAppName}
DefaultDirName={commonappdata}\{#MyAppName}
DisableProgramGroupPage=yes
DefaultGroupName=Archipelago
OutputDir=setups
OutputBaseFilename=Setup {#MyAppName}
Compression=lzma2
SolidCompression=yes
LZMANumBlockThreads=8
ArchitecturesInstallIn64BitMode=x64
ChangesAssociations=yes
ArchitecturesAllowed=x64
AllowNoIcons=yes
SetupIconFile={#MyAppIcon}
UninstallDisplayIcon={app}\{#MyAppExeName}
SignTool= signtool
LicenseFile= LICENSE
WizardStyle= modern
SetupLogging=yes
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}";
[Dirs]
NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify;
[Files]
Source: "{code:GetROMPath}"; DestDir: "{app}"; DestName: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"; Flags: external
Source: "{#sourcepath}*"; Excludes: "*.sfc, *.log, data\sprites\alttpr"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[Icons]
Name: "{group}\{#MyAppName} Folder"; Filename: "{app}";
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}";
Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon
Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Run]
Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..."
Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."
[UninstallDelete]
Type: dirifempty; Name: "{app}"
[Registry]
Root: HKCR; Subkey: ".apbp"; ValueData: "{#MyAppName}patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""
Root: HKCR; Subkey: "{#MyAppName}patch"; ValueData: "Archipelago Binary Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""
Root: HKCR; Subkey: "{#MyAppName}patch\DefaultIcon"; ValueData: "{app}\{#MyAppExeName},0"; ValueType: string; ValueName: ""
Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; ValueType: string; ValueName: ""
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: ""
Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{app}\ArchipelagoServer.exe"" ""%1"""; ValueType: string; ValueName: ""
[Code]
// See: https://stackoverflow.com/a/51614652/2287576
function IsVCRedist64BitNeeded(): boolean;
var
strVersion: string;
begin
if (RegQueryStringValue(HKEY_LOCAL_MACHINE,
'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64', 'Version', strVersion)) then
begin
// Is the installed version at least the packaged one ?
Log('VC Redist x64 Version : found ' + strVersion);
Result := (CompareStr(strVersion, 'v14.29.30037') < 0);
end
else
begin
// Not even an old version installed
Log('VC Redist x64 is not already installed');
Result := True;
end;
end;
var ROMFilePage: TInputFileWizardPage;
var R : longint;
var rom: string;
procedure InitializeWizard();
begin
rom := FileSearch('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', WizardDirValue());
if Length(rom) > 0 then
begin
log('existing ROM found');
log(IntToStr(CompareStr(GetMD5OfFile(rom), '03a63945398191337e896e5771f77173')));
if CompareStr(GetMD5OfFile(rom), '03a63945398191337e896e5771f77173') = 0 then
begin
log('existing ROM verified');
exit;
end;
log('existing ROM failed verification');
end;
rom := ''
ROMFilePage :=
CreateInputFilePage(
wpLicense,
'Select ROM File',
'Where is your Zelda no Densetsu - Kamigami no Triforce (Japan).sfc located?',
'Select the file, then click Next.');
ROMFilePage.Add(
'Location of ROM file:',
'SNES ROM files|*.sfc|All files|*.*',
'.sfc');
end;
function GetROMPath(Param: string): string;
begin
if Length(rom) > 0 then
Result := rom
else if Assigned(RomFilePage) then
begin
R := CompareStr(GetMD5OfFile(ROMFilePage.Values[0]), '03a63945398191337e896e5771f77173')
if R <> 0 then
MsgBox('ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
Result := ROMFilePage.Values[0]
end
else
Result := '';
end;

153
kvui.py Normal file
View File

@@ -0,0 +1,153 @@
import os
import logging
os.environ["KIVY_NO_CONSOLELOG"] = "1"
os.environ["KIVY_NO_FILELOG"] = "1"
os.environ["KIVY_NO_ARGS"] = "1"
from kivy.app import App
from kivy.base import ExceptionHandler, ExceptionManager, Config
from kivy.uix.gridlayout import GridLayout
from kivy.uix.textinput import TextInput
from kivy.uix.recycleview import RecycleView
from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem
from kivy.utils import escape_markup
from kivy.lang import Builder
import Utils
from NetUtils import JSONtoTextParser, JSONMessagePart
class GameManager(App):
logging_pairs = [
("Client", "Archipelago"),
]
def __init__(self, ctx):
self.ctx = ctx
self.commandprocessor = ctx.command_processor(ctx)
self.icon = r"data/icon.png"
self.json_to_kivy_parser = KivyJSONtoTextParser(ctx)
self.log_panels = {}
super(GameManager, self).__init__()
def build(self):
self.grid = GridLayout()
self.grid.cols = 1
self.tabs = TabbedPanel()
self.tabs.default_tab_text = "All"
self.log_panels["All"] = self.tabs.default_tab_content = UILog(*(logging.getLogger(logger_name)
for logger_name, name in
self.logging_pairs))
for logger_name, display_name in self.logging_pairs:
bridge_logger = logging.getLogger(logger_name)
panel = TabbedPanelItem(text=display_name)
self.log_panels[display_name] = panel.content = UILog(bridge_logger)
self.tabs.add_widget(panel)
self.grid.add_widget(self.tabs)
textinput = TextInput(size_hint_y=None, height=30, multiline=False)
textinput.bind(on_text_validate=self.on_message)
self.grid.add_widget(textinput)
self.commandprocessor("/help")
return self.grid
def on_stop(self):
self.ctx.exit_event.set()
def on_message(self, textinput: TextInput):
try:
input_text = textinput.text.strip()
textinput.text = ""
if self.ctx.input_requests > 0:
self.ctx.input_requests -= 1
self.ctx.input_queue.put_nowait(input_text)
elif input_text:
self.commandprocessor(input_text)
except Exception as e:
logging.getLogger("Client").exception(e)
def print_json(self, data):
text = self.json_to_kivy_parser(data)
self.log_panels["Archipelago"].on_message_markup(text)
self.log_panels["All"].on_message_markup(text)
class FactorioManager(GameManager):
logging_pairs = [
("Client", "Archipelago"),
("FactorioServer", "Factorio Server Log"),
("FactorioWatcher", "Bridge Data Log"),
]
title = "Archipelago Factorio Client"
class LttPManager(GameManager):
logging_pairs = [
("Client", "Archipelago"),
("SNES", "SNES"),
]
title = "Archipelago LttP Client"
class LogtoUI(logging.Handler):
def __init__(self, on_log):
super(LogtoUI, self).__init__(logging.DEBUG)
self.on_log = on_log
def handle(self, record: logging.LogRecord) -> None:
self.on_log(record)
class UILog(RecycleView):
cols = 1
def __init__(self, *loggers_to_handle, **kwargs):
super(UILog, self).__init__(**kwargs)
self.data = []
for logger in loggers_to_handle:
logger.addHandler(LogtoUI(self.on_log))
def on_log(self, record: logging.LogRecord) -> None:
self.data.append({"text": escape_markup(record.getMessage())})
def on_message_markup(self, text):
self.data.append({"text": text})
class E(ExceptionHandler):
logger = logging.getLogger("Client")
def handle_exception(self, inst):
self.logger.exception(inst)
return ExceptionManager.RAISE
class KivyJSONtoTextParser(JSONtoTextParser):
color_codes = {
# not exact color names, close enough but decent looking
"black": "000000",
"red": "EE0000",
"green": "00FF7F",
"yellow": "FAFAD2",
"blue": "6495ED",
"magenta": "EE00EE",
"cyan": "00EEEE",
"white": "FFFFFF"
}
def _handle_color(self, node: JSONMessagePart):
colors = node["color"].split(";")
node["text"] = escape_markup(node["text"])
for color in colors:
color_code = self.color_codes.get(color, None)
if color_code:
node["text"] = f"[color={color_code}]{node['text']}[/color]"
return self._handle_text(node)
return self._handle_text(node)
ExceptionManager.add_handler(E())
Config.set("input", "mouse", "mouse,disable_multitouch")
Builder.load_file(Utils.local_path("data", "client.kv"))

View File

@@ -23,13 +23,14 @@ name: YourName{number} # Your name in-game. Spaces will be replaced with undersc
#{PLAYER} will be replaced with the player's slot number if that slot number is greater than 1. #{PLAYER} will be replaced with the player's slot number if that slot number is greater than 1.
#{number} will be replaced with the counter value of the name. #{number} will be replaced with the counter value of the name.
#{NUMBER} will be replaced with the counter value of the name if the counter value is greater than 1. #{NUMBER} will be replaced with the counter value of the name if the counter value is greater than 1.
game: game: # Pick a game to play
A Link to the Past: 1 A Link to the Past: 0
Factorio: 1 Factorio: 0
Minecraft: 1 Minecraft: 0
Subnautica: 1 Subnautica: 0
Slay the Spire: 0
requires: requires:
version: 0.1.5 # Version of Archipelago required for this yaml to work as expected. version: 0.1.6 # Version of Archipelago required for this yaml to work as expected.
# Shared Options supported by all games: # Shared Options supported by all games:
accessibility: accessibility:
items: 0 # Guarantees you will be able to acquire all items, but you may not be able to access all locations items: 0 # Guarantees you will be able to acquire all items, but you may not be able to access all locations
@@ -56,7 +57,23 @@ progression_balancing:
# - "Master Sword Pedestal" # - "Master Sword Pedestal"
Subnautica: {} Subnautica: {}
Slay the Spire:
character: # Pick What Character you wish to play with.
ironclad: 50
silent: 50
defect: 50
watcher: 50
ascension: # What Ascension do you wish to play with.
# you can add additional values between minimum and maximum
0: 50 # minimum value
20: 0 # maximum value
random: 0
random-low: 0
random-high: 0
heart_run: # Whether or not you will need to collect they 3 keys to unlock the final act
# and beat the heart to finish the game.
false: 50
true: 0
Factorio: Factorio:
tech_tree_layout: tech_tree_layout:
single: 1 single: 1
@@ -94,6 +111,10 @@ Factorio:
hard : 0 hard : 0
very_hard : 0 very_hard : 0
insane : 0 insane : 0
silo:
vanilla: 1
randomize_recipe: 0
spawn: 0 # spawn silo near player spawn point
free_samples: free_samples:
none: 1 none: 1
single_craft: 0 single_craft: 0
@@ -102,6 +123,7 @@ Factorio:
progressive: progressive:
on: 1 on: 1
off: 0 off: 0
grouped_random: 0
tech_tree_information: tech_tree_information:
none: 0 none: 0
advancement: 0 # show which items are a logical advancement advancement: 0 # show which items are a logical advancement
@@ -112,6 +134,40 @@ Factorio:
starting_items: starting_items:
burner-mining-drill: 19 burner-mining-drill: 19
stone-furnace: 19 stone-furnace: 19
# Note: Total amount of traps cannot exceed 4, if the sum of them is higher it will get automatically capped.
evolution_traps:
# Trap items that when received increase the enemy evolution.
0: 1
1: 0
2: 0
3: 0
4: 0
random: 0
random-low: 0
random-middle: 0
random-high: 0
evolution_trap_increase:
# If present, % increase of Evolution with each trap received.
5: 0
10: 1
15: 0
20: 0
100: 0
random: 0
random-low: 0
random-middle: 0
random-high: 0
attack_traps:
# Trap items that when received trigger an attack on your base.
0: 1
1: 0
2: 0
3: 0
4: 0
random: 0
random-low: 0
random-middle: 0
random-high: 0
world_gen: world_gen:
# frequency, size, richness, terrain segmentation, starting area and water are all of https://wiki.factorio.com/Types/MapGenSize # frequency, size, richness, terrain segmentation, starting area and water are all of https://wiki.factorio.com/Types/MapGenSize
# inverse of water scale # inverse of water scale
@@ -184,12 +240,22 @@ Factorio:
min_expansion_cooldown: 14400 # 1 to 60 min in ticks min_expansion_cooldown: 14400 # 1 to 60 min in ticks
max_expansion_cooldown: 216000 # 5 to 180 min in ticks max_expansion_cooldown: 216000 # 5 to 180 min in ticks
Minecraft: Minecraft:
advancement_goal: 50 # Number of advancements required (out of 92 total) to spawn the Ender Dragon and complete the game. advancement_goal: 50 # Number of advancements required (87 max) to spawn the Ender Dragon and complete the game.
egg_shards_required: # Number of dragon egg shards to collect (30 max) before the Ender Dragon will spawn.
0: 1
5: 0
10: 0
20: 0
egg_shards_available: # Number of egg shards available in the pool (30 max).
0: 1
5: 0
10: 0
20: 0
combat_difficulty: # Modifies the level of items logically required for exploring dangerous areas and fighting bosses. combat_difficulty: # Modifies the level of items logically required for exploring dangerous areas and fighting bosses.
easy: 0 easy: 0
normal: 1 normal: 1
hard: 0 hard: 0
include_hard_advancements: # Junk-fills certain RNG-reliant or tedious advancements with XP rewards. include_hard_advancements: # Junk-fills certain RNG-reliant or tedious advancements.
on: 0 on: 0
off: 1 off: 1
include_insane_advancements: # Junk-fills extremely difficult advancements; this is only How Did We Get Here? and Adventuring Time. include_insane_advancements: # Junk-fills extremely difficult advancements; this is only How Did We Get Here? and Adventuring Time.
@@ -204,7 +270,13 @@ Minecraft:
structure_compasses: # Adds structure compasses to the item pool, which point to the nearest indicated structure. structure_compasses: # Adds structure compasses to the item pool, which point to the nearest indicated structure.
on: 0 on: 0
off: 1 off: 1
bee_traps: # Adds bee traps to the item pool, which spawn multiple angered bees around every player when received. bee_traps: # Replaces a percentage of junk items with bee traps, which spawn multiple angered bees around every player when received.
0: 1
25: 0
50: 0
75: 0
100: 0
send_defeated_mobs: # Send killed mobs to other Minecraft worlds which have this option enabled.
on: 0 on: 0
off: 1 off: 1
A Link to the Past: A Link to the Past:
@@ -224,29 +296,31 @@ A Link to the Past:
on: 0 # prevents unshuffled compasses, maps and keys to be boss drops, they can still drop keysanity and other players' items on: 0 # prevents unshuffled compasses, maps and keys to be boss drops, they can still drop keysanity and other players' items
off: 50 off: 50
### End of Logic Section ### ### End of Logic Section ###
map_shuffle: # Shuffle dungeon maps into the world and other dungeons, including other players' worlds bigkey_shuffle: # Big Key Placement
on: 0 original_dungeon: 50
off: 50 own_dungeons: 0
compass_shuffle: # Shuffle compasses into the world and other dungeons, including other players' worlds own_world: 0
on: 0 any_world: 0
off: 50 different_world: 0
smallkey_shuffle: # Shuffle small keys into the world and other dungeons, including other players' worlds smallkey_shuffle: # Small Key Placement
on: 0 original_dungeon: 50
universal: 0 # allows small keys to be used in any dungeon and adds shops to buy more own_dungeons: 0
off: 50 own_world: 0
bigkey_shuffle: # Shuffle big keys into the world and other dungeons, including other players' worlds any_world: 0
on: 0 different_world: 0
off: 50 universal: 0
local_keys: # Keep small keys and big keys local to your world compass_shuffle: # Compass Placement
on: 0 original_dungeon: 50
off: 50 own_dungeons: 0
dungeon_items: # Alternative to the 4 shuffles and local_keys above this, does nothing until the respective 4 shuffles and local_keys above are deleted own_world: 0
mc: 0 # Shuffle maps and compasses any_world: 0
none: 50 # Shuffle none of the 4 different_world: 0
mcsb: 0 # Shuffle all of the 4, any combination of m, c, s and b will shuffle the respective item, or not if it's missing, so you can add more options here map_shuffle: # Map Placement
lmcsb: 0 # Like mcsb above, but with keys kept local to your world. l is what makes your keys local, or not if it's missing original_dungeon: 50
ub: 0 # universal small keys and shuffled big keys own_dungeons: 0
# you can add more combos of these letters here own_world: 0
any_world: 0
different_world: 0
dungeon_counters: dungeon_counters:
on: 0 # Always display amount of items checked in a dungeon on: 0 # Always display amount of items checked in a dungeon
pickup: 50 # Show when compass is picked up pickup: 50 # Show when compass is picked up
@@ -255,7 +329,7 @@ A Link to the Past:
progressive: # Enable or disable progressive items (swords, shields, bow) progressive: # Enable or disable progressive items (swords, shields, bow)
on: 50 # All items are progressive on: 50 # All items are progressive
off: 0 # No items are progressive off: 0 # No items are progressive
random: 0 # Randomly decides for all items. Swords could be progressive, shields might not be grouped_random: 0 # Randomly decides for all items. Swords could be progressive, shields might not be
entrance_shuffle: entrance_shuffle:
none: 50 # Vanilla game map. All entrances and exits lead to their original locations. You probably want this option none: 50 # Vanilla game map. All entrances and exits lead to their original locations. You probably want this option
dungeonssimple: 0 # Shuffle just dungeons amongst each other, swapping dungeons entirely, so Hyrule Castle is always 1 dungeon dungeonssimple: 0 # Shuffle just dungeons amongst each other, swapping dungeons entirely, so Hyrule Castle is always 1 dungeon
@@ -396,7 +470,7 @@ A Link to the Past:
enemy_damage: enemy_damage:
default: 50 # Vanilla enemy damage default: 50 # Vanilla enemy damage
shuffled: 0 # Enemies deal 0 to 4 hearts and armor helps shuffled: 0 # Enemies deal 0 to 4 hearts and armor helps
random: 0 # Enemies deal 0 to 8 hearts and armor just reshuffles the damage chaos: 0 # Enemies deal 0 to 8 hearts and armor just reshuffles the damage
enemy_health: enemy_health:
default: 50 # Vanilla enemy HP default: 50 # Vanilla enemy HP
easy: 0 # Enemies have reduced health easy: 0 # Enemies have reduced health
@@ -509,9 +583,9 @@ A Link to the Past:
# You can combine these events like this. randomonhit-enter-exit if you want it on hit, enter, exit. # You can combine these events like this. randomonhit-enter-exit if you want it on hit, enter, exit.
randomonall: 0 # Random sprite on any and all currently supported events. Refer to above for the supported events. randomonall: 0 # Random sprite on any and all currently supported events. Refer to above for the supported events.
Link: 50 # To add other sprites: open the gui/Creator, go to adjust, select a sprite and write down the name the gui calls it Link: 50 # To add other sprites: open the gui/Creator, go to adjust, select a sprite and write down the name the gui calls it
disablemusic: # If "on", all in-game music will be disabled music: # If "off", all in-game music will be disabled
on: 0 on: 50
off: 50 off: 0
quickswap: # Enable switching items by pressing the L+R shoulder buttons quickswap: # Enable switching items by pressing the L+R shoulder buttons
on: 50 on: 50
off: 0 off: 0
@@ -544,7 +618,7 @@ A Link to the Past:
off: 0 off: 0
ow_palettes: # Change the colors of the overworld ow_palettes: # Change the colors of the overworld
default: 50 # No changes default: 50 # No changes
random: 0 # Shuffle the colors, with harmony in mind good: 0 # Shuffle the colors, with harmony in mind
blackout: 0 # everything black / blind mode blackout: 0 # everything black / blind mode
grayscale: 0 grayscale: 0
negative: 0 negative: 0
@@ -554,7 +628,7 @@ A Link to the Past:
puke: 0 puke: 0
uw_palettes: # Change the colors of caves and dungeons uw_palettes: # Change the colors of caves and dungeons
default: 50 # No changes default: 50 # No changes
random: 0 # Shuffle the colors, with harmony in mind good: 0 # Shuffle the colors, with harmony in mind
blackout: 0 # everything black / blind mode blackout: 0 # everything black / blind mode
grayscale: 0 grayscale: 0
negative: 0 negative: 0
@@ -564,7 +638,7 @@ A Link to the Past:
puke: 0 puke: 0
hud_palettes: # Change the colors of the hud hud_palettes: # Change the colors of the hud
default: 50 # No changes default: 50 # No changes
random: 0 # Shuffle the colors, with harmony in mind good: 0 # Shuffle the colors, with harmony in mind
blackout: 0 # everything black / blind mode blackout: 0 # everything black / blind mode
grayscale: 0 grayscale: 0
negative: 0 negative: 0
@@ -574,7 +648,7 @@ A Link to the Past:
puke: 0 puke: 0
sword_palettes: # Change the colors of swords sword_palettes: # Change the colors of swords
default: 50 # No changes default: 50 # No changes
random: 0 # Shuffle the colors, with harmony in mind good: 0 # Shuffle the colors, with harmony in mind
blackout: 0 # everything black / blind mode blackout: 0 # everything black / blind mode
grayscale: 0 grayscale: 0
negative: 0 negative: 0
@@ -584,7 +658,7 @@ A Link to the Past:
puke: 0 puke: 0
shield_palettes: # Change the colors of shields shield_palettes: # Change the colors of shields
default: 50 # No changes default: 50 # No changes
random: 0 # Shuffle the colors, with harmony in mind good: 0 # Shuffle the colors, with harmony in mind
blackout: 0 # everything black / blind mode blackout: 0 # everything black / blind mode
grayscale: 0 grayscale: 0
negative: 0 negative: 0
@@ -600,6 +674,618 @@ A Link to the Past:
assured: 0 # Begin with a sword, the rest are placed randomly throughout the world assured: 0 # Begin with a sword, the rest are placed randomly throughout the world
vanilla: 0 # Swords are placed in vanilla locations in your own game (Uncle, Pyramid Fairy, Smiths, Pedestal) vanilla: 0 # Swords are placed in vanilla locations in your own game (Uncle, Pyramid Fairy, Smiths, Pedestal)
swordless: 0 # swordless mode swordless: 0 # swordless mode
Ocarina of Time:
logic_rules: # Set the logic used for the generator.
glitchless: 50
glitched: 0
no_logic: 0
logic_no_night_tokens_without_suns_song: # Nighttime skulltulas will logically require Sun's Song.
false: 50
true: 0
open_forest: # Set the state of Kokiri Forest and the path to Deku Tree.
open: 50
closed_deku: 0
closed: 0
open_kakariko: # Set the state of the Kakariko Village gate.
open: 50
zelda: 0
closed: 0
open_door_of_time: # Open the Door of Time by default, without the Song of Time.
false: 0
true: 50
zora_fountain: # Set the state of King Zora, blocking the way to Zora's Fountain.
open: 0
adult: 0
closed: 50
gerudo_fortress: # Set the requirements for access to Gerudo Fortress.
normal: 0
fast: 50
open: 0
bridge: # Set the requirements for the Rainbow Bridge.
open: 0
vanilla: 0
stones: 0
medallions: 50
dungeons: 0
tokens: 0
trials: # Set the number of required trials in Ganon's Castle.
# you can add additional values between minimum and maximum
0: 0 # minimum value
6: 0 # maximum value
random: 50
random-low: 0
random-high: 0
starting_age: # Choose which age Link will start as.
child: 50
adult: 0
triforce_hunt: # Gather pieces of the Triforce scattered around the world to complete the game.
false: 50
true: 0
triforce_goal: # Number of Triforce pieces required to complete the game. Total number placed determined by the Item Pool setting.
# you can add additional values between minimum and maximum
1: 0 # minimum value
50: 0 # maximum value
random: 50
random-low: 0
random-high: 0
bombchus_in_logic: # Bombchus are properly considered in logic. The first found pack will have 20 chus; Kokiri Shop and Bazaar sell refills; bombchus open Bombchu Bowling.
false: 50
true: 0
bridge_stones: # Set the number of Spiritual Stones required for the rainbow bridge.
# you can add additional values between minimum and maximum
0: 0 # minimum value
3: 0 # maximum value
random: 50
random-low: 0
random-high: 0
bridge_medallions: # Set the number of medallions required for the rainbow bridge.
# you can add additional values between minimum and maximum
0: 0 # minimum value
6: 0 # maximum value
random: 50
random-low: 0
random-high: 0
bridge_rewards: # Set the number of dungeon rewards required for the rainbow bridge.
# you can add additional values between minimum and maximum
0: 0 # minimum value
9: 0 # maximum value
random: 50
random-low: 0
random-high: 0
bridge_tokens: # Set the number of Gold Skulltula Tokens required for the rainbow bridge.
# you can add additional values between minimum and maximum
0: 0 # minimum value
100: 0 # maximum value
random: 50
random-low: 0
random-high: 0
shuffle_mapcompass: # Control where to shuffle dungeon maps and compasses.
remove: 0
startwith: 50
vanilla: 0
dungeon: 0
overworld: 0
any_dungeon: 0
keysanity: 0
shuffle_smallkeys: # Control where to shuffle dungeon small keys.
remove: 0
vanilla: 0
dungeon: 50
overworld: 0
any_dungeon: 0
keysanity: 0
shuffle_fortresskeys: # Control where to shuffle the Gerudo Fortress small keys.
vanilla: 50
overworld: 0
any_dungeon: 0
keysanity: 0
shuffle_bosskeys: # Control where to shuffle boss keys, except the Ganon's Castle Boss Key.
remove: 0
vanilla: 0
dungeon: 50
overworld: 0
any_dungeon: 0
keysanity: 0
shuffle_ganon_bosskey: # Control where to shuffle the Ganon's Castle Boss Key.
remove: 50
vanilla: 0
dungeon: 0
overworld: 0
any_dungeon: 0
keysanity: 0
on_lacs: 0
enhance_map_compass: # Map tells if a dungeon is vanilla or MQ. Compass tells what the dungeon reward is.
false: 50
true: 0
lacs_condition: # Set the requirements for the Light Arrow Cutscene in the Temple of Time.
vanilla: 50
stones: 0
medallions: 0
dungeons: 0
tokens: 0
lacs_stones: # Set the number of Spiritual Stones required for LACS.
# you can add additional values between minimum and maximum
0: 0 # minimum value
3: 0 # maximum value
random: 50
random-low: 0
random-high: 0
lacs_medallions: # Set the number of medallions required for LACS.
# you can add additional values between minimum and maximum
0: 0 # minimum value
6: 0 # maximum value
random: 50
random-low: 0
random-high: 0
lacs_rewards: # Set the number of dungeon rewards required for LACS.
# you can add additional values between minimum and maximum
0: 0 # minimum value
9: 0 # maximum value
random: 50
random-low: 0
random-high: 0
lacs_tokens: # Set the number of Gold Skulltula Tokens required for LACS.
# you can add additional values between minimum and maximum
0: 0 # minimum value
100: 0 # maximum value
random: 50
random-low: 0
random-high: 0
shuffle_song_items: # Set where songs can appear.
song: 50
dungeon: 0
any: 0
shopsanity: # Randomizes shop contents. Set to "off" to not shuffle shops; "0" shuffles shops but does not allow multiworld items in shops.
off: 50
"0": 0
"1": 0
"2": 0
"3": 0
"4": 0
random_value: 0
tokensanity: # Token rewards from Gold Skulltulas are shuffled into the pool.
off: 50
dungeons: 0
overworld: 0
all: 0
shuffle_scrubs: # Shuffle the items sold by Business Scrubs, and set the prices.
off: 50
low: 0
affordable: 0
expensive: 0
shuffle_cows: # Cows give items when Epona's Song is played.
false: 50
true: 0
shuffle_kokiri_sword: # Shuffle Kokiri Sword into the item pool.
false: 50
true: 0
shuffle_ocarinas: # Shuffle the Fairy Ocarina and Ocarina of Time into the item pool.
false: 50
true: 0
shuffle_weird_egg: # Shuffle the Weird Egg from Malon at Hyrule Castle.
false: 50
true: 0
shuffle_gerudo_card: # Shuffle the Gerudo Membership Card into the item pool.
false: 50
true: 0
shuffle_beans: # Adds a pack of 10 beans to the item pool and changes the bean salesman to sell one item for 60 rupees.
false: 50
true: 0
shuffle_medigoron_carpet_salesman: # Shuffle the items sold by Medigoron and the Haunted Wasteland Carpet Salesman.
false: 50
true: 0
skip_child_zelda: # Game starts with Zelda's Letter, the item at Zelda's Lullaby, and the relevant events already completed.
false: 50
true: 0
no_escape_sequence: # Skips the tower collapse sequence between the Ganondorf and Ganon fights.
false: 0
true: 50
no_guard_stealth: # The crawlspace into Hyrule Castle skips straight to Zelda.
false: 0
true: 50
no_epona_race: # Epona can always be summoned with Epona's Song.
false: 0
true: 50
skip_some_minigame_phases: # Dampe Race and Horseback Archery give both rewards if the second condition is met on the first attempt.
false: 0
true: 50
complete_mask_quest: # All masks are immediately available to borrow from the Happy Mask Shop.
false: 50
true: 0
useful_cutscenes: # Reenables the Poe cutscene in Forest Temple, Darunia in Fire Temple, and Twinrova introduction. Mostly useful for glitched.
false: 50
true: 0
fast_chests: # All chest animations are fast. If disabled, major items have a slow animation.
false: 0
true: 50
free_scarecrow: # Pulling out the ocarina near a scarecrow spot spawns Pierre without needing the song.
false: 50
true: 0
fast_bunny_hood: # Bunny Hood lets you move 1.5x faster like in Majora's Mask.
false: 50
true: 0
chicken_count: # Controls the number of Cuccos for Anju to give an item as child.
# you can add additional values between minimum and maximum
0: 0 # minimum value
7: 0 # maximum value
random: 50
random-low: 0
random-high: 0
hints: # Gossip Stones can give hints about item locations.
none: 0
mask: 0
agony: 0
always: 50
false: 0
hint_dist: # Choose the hint distribution to use. Affects the frequency of strong hints, which items are always hinted, etc.
balanced: 50
ddr: 0
league: 0
mw2: 0
scrubs: 0
strong: 0
tournament: 0
useless: 0
very_strong: 0
text_shuffle: # Randomizes text in the game for comedic effect.
none: 50
except_hints: 0
complete: 0
damage_multiplier: # Controls the amount of damage Link takes.
half: 0
normal: 50
double: 0
quadruple: 0
ohko: 0
no_collectible_hearts: # Hearts will not drop from enemies or objects.
false: 50
true: 0
starting_tod: # Change the starting time of day.
default: 50
sunrise: 0
morning: 0
noon: 0
afternoon: 0
sunset: 0
evening: 0
midnight: 0
witching_hour: 0
start_with_consumables: # Start the game with full Deku Sticks and Deku Nuts.
false: 50
true: 0
start_with_rupees: # Start with a full wallet. Wallet upgrades will also fill your wallet.
false: 50
true: 0
item_pool_value: # Changes the number of items available in the game.
plentiful: 0
balanced: 50
scarce: 0
minimal: 0
junk_ice_traps: # Adds ice traps to the item pool.
off: 0
normal: 50
extra: 0
mayhem: 0
onslaught: 0
ice_trap_appearance: # Changes the appearance of ice traps as freestanding items.
major_only: 50
junk_only: 0
anything: 0
logic_earliest_adult_trade: # Earliest item that can appear in the adult trade sequence.
pocket_egg: 0
pocket_cucco: 0
cojiro: 0
odd_mushroom: 0
poachers_saw: 0
broken_sword: 0
prescription: 50
eyeball_frog: 0
eyedrops: 0
claim_check: 0
logic_latest_adult_trade: # Latest item that can appear in the adult trade sequence.
pocket_egg: 0
pocket_cucco: 0
cojiro: 0
odd_mushroom: 0
poachers_saw: 0
broken_sword: 0
prescription: 0
eyeball_frog: 0
eyedrops: 0
claim_check: 50
default_targeting: # Default targeting option.
hold: 50
switch: 0
display_dpad: # Show dpad icon on HUD for quick actions (ocarina, hover boots, iron boots).
false: 0
true: 50
correct_model_colors: # Makes in-game models match their HUD element colors.
false: 0
true: 50
background_music: # Randomize or disable background music.
normal: 50
off: 0
randomized: 0
fanfares: # Randomize or disable item fanfares.
normal: 50
off: 0
randomized: 0
ocarina_fanfares: # Enable ocarina songs as fanfares. These are longer than usual fanfares. Does nothing without fanfares randomized.
false: 50
true: 0
kokiri_color: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code.
random_choice: 0
completely_random: 0
kokiri_green: 50
goron_red: 0
zora_blue: 0
black: 0
white: 0
azure_blue: 0
vivid_cyan: 0
light_red: 0
fuchsia: 0
purple: 0
majora_purple: 0
twitch_purple: 0
purple_heart: 0
persian_rose: 0
dirty_yellow: 0
blush_pink: 0
hot_pink: 0
rose_pink: 0
orange: 0
gray: 0
gold: 0
silver: 0
beige: 0
teal: 0
blood_red: 0
blood_orange: 0
royal_blue: 0
sonic_blue: 0
nes_green: 0
dark_green: 0
lumen: 0
goron_color: # Choose a color. Uses the same options as "kokiri_color".
random_choice: 0
completely_random: 0
goron_red: 50
zora_color: # Choose a color. Uses the same options as "kokiri_color".
random_choice: 0
completely_random: 0
zora_blue: 50
silver_gauntlets_color: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code.
random_choice: 0
completely_random: 0
silver: 50
gold: 0
black: 0
green: 0
blue: 0
bronze: 0
red: 0
sky_blue: 0
pink: 0
magenta: 0
orange: 0
lime: 0
purple: 0
golden_gauntlets_color: # Choose a color. Uses the same options as "silver_gauntlets_color".
random_choice: 0
completely_random: 0
gold: 50
mirror_shield_frame_color: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code.
random_choice: 0
completely_random: 0
red: 50
green: 0
blue: 0
yellow: 0
cyan: 0
magenta: 0
orange: 0
gold: 0
purple: 0
pink: 0
navi_color_default_inner: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code.
random_choice: 0
completely_random: 0
rainbow: 0
gold: 0
white: 50
green: 0
light_blue: 0
yellow: 0
red: 0
magenta: 0
black: 0
tatl: 0
tael: 0
fi: 0
ciela: 0
epona: 0
ezlo: 0
king_of_red_lions: 0
linebeck: 0
loftwing: 0
midna: 0
phantom_zelda: 0
navi_color_default_outer: # Choose a color. Uses the same options as "navi_color_default_inner" plus "match_inner".
random_choice: 0
completely_random: 0
match_inner: 50
navi_color_enemy_inner: # Choose a color. Uses the same options as "navi_color_default_inner".
random_choice: 0
completely_random: 0
yellow: 50
navi_color_enemy_outer: # Choose a color. Uses the same options as "navi_color_default_inner" plus "match_inner".
random_choice: 0
completely_random: 0
match_inner: 50
navi_color_npc_inner: # Choose a color. Uses the same options as "navi_color_default_inner".
random_choice: 0
completely_random: 0
light_blue: 50
navi_color_npc_outer: # Choose a color. Uses the same options as "navi_color_default_inner" plus "match_inner".
random_choice: 0
completely_random: 0
match_inner: 50
navi_color_prop_inner: # Choose a color. Uses the same options as "navi_color_default_inner".
random_choice: 0
completely_random: 0
green: 50
navi_color_prop_outer: # Choose a color. Uses the same options as "navi_color_default_inner" plus "match_inner".
random_choice: 0
completely_random: 0
match_inner: 50
sword_trail_duration: # Set the duration for sword trails.
# you can add additional values between minimum and maximum
4: 50 # minimum value
20: 0 # maximum value
random: 0
random-low: 0
random-high: 0
sword_trail_color_inner: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code.
random_choice: 0
completely_random: 0
rainbow: 0
white: 50
red: 0
green: 0
blue: 0
cyan: 0
magenta: 0
orange: 0
gold: 0
purple: 0
pink: 0
sword_trail_color_outer: # Uses the same options as "sword_trail_color_inner" plus "match_inner".
random_choice: 0
completely_random: 0
match_inner: 50
bombchu_trail_color_inner: # Uses the same options as "sword_trail_color_inner".
random_choice: 0
completely_random: 0
red: 50
bombchu_trail_color_outer: # Uses the same options as "sword_trail_color_inner" plus "match_inner".
random_choice: 0
completely_random: 0
match_inner: 50
boomerang_trail_color_inner: # Uses the same options as "sword_trail_color_inner".
random_choice: 0
completely_random: 0
yellow: 50
boomerang_trail_color_outer: # Uses the same options as "sword_trail_color_inner" plus "match_inner".
random_choice: 0
completely_random: 0
match_inner: 50
heart_color: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code.
random_choice: 0
completely_random: 0
red: 50
green: 0
blue: 0
yellow: 0
magic_color: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code.
random_choice: 0
completely_random: 0
green: 50
red: 0
blue: 0
purple: 0
pink: 0
yellow: 0
white: 0
a_button_color: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code.
random_choice: 0
completely_random: 0
n64_blue: 50
n64_green: 0
n64_red: 0
gamecube_green: 0
gamecube_red: 0
gamecube_grey: 0
yellow: 0
black: 0
white: 0
magenta: 0
ruby: 0
sapphire: 0
lime: 0
cyan: 0
purple: 0
orange: 0
b_button_color: # Choose a color. Uses the same options as "a_button_color".
random_choice: 0
completely_random: 0
n64_green: 50
c_button_color: # Choose a color. Uses the same options as "a_button_color".
random_choice: 0
completely_random: 0
yellow: 50
start_button_color: # Choose a color. Uses the same options as "a_button_color".
random_choice: 0
completely_random: 0
n64_red: 50
sfx_navi_overworld: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound.
default: 50
completely_random: 0
random_ear_safe: 0
random_choice: 0
none: 0
sfx_navi_enemy: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound.
default: 50
completely_random: 0
random_ear_safe: 0
random_choice: 0
none: 0
sfx_low_hp: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound.
default: 50
completely_random: 0
random_ear_safe: 0
random_choice: 0
none: 0
sfx_menu_cursor: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound.
default: 50
completely_random: 0
random_ear_safe: 0
random_choice: 0
none: 0
sfx_menu_select: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound.
default: 50
completely_random: 0
random_ear_safe: 0
random_choice: 0
none: 0
sfx_nightfall: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound.
default: 50
completely_random: 0
random_ear_safe: 0
random_choice: 0
none: 0
sfx_horse_neigh: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound.
default: 50
completely_random: 0
random_ear_safe: 0
random_choice: 0
none: 0
sfx_hover_boots: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound.
default: 50
completely_random: 0
random_ear_safe: 0
random_choice: 0
sfx_ocarina: # Change the sound of the ocarina.
ocarina: 50
malon: 0
whistle: 0
harp: 0
grind_organ: 0
flute: 0
logic_tricks:
[]
# meta_ignore, linked_options and triggers work for any game # meta_ignore, linked_options and triggers work for any game
meta_ignore: # Nullify options specified in the meta.yaml file. Adding an option here guarantees it will not occur in your seed, even if the .yaml file specifies it meta_ignore: # Nullify options specified in the meta.yaml file. Adding an option here guarantees it will not occur in your seed, even if the .yaml file specifies it
mode: mode:
@@ -641,7 +1327,7 @@ linked_options:
singularity: 1 singularity: 1
enemy_damage: enemy_damage:
shuffled: 1 shuffled: 1
random: 1 chaos: 1
enemy_health: enemy_health:
easy: 1 easy: 1
hard: 1 hard: 1

View File

@@ -2,7 +2,7 @@ colorama>=0.4.4
websockets>=9.1 websockets>=9.1
PyYAML>=5.4.1 PyYAML>=5.4.1
fuzzywuzzy>=0.18.0 fuzzywuzzy>=0.18.0
prompt_toolkit>=3.0.19 prompt_toolkit>=3.0.20
appdirs>=1.4.4 appdirs>=1.4.4
jinja2>=3.0.1 jinja2>=3.0.1
schema>=0.7.4 schema>=0.7.4

145
setup.py
View File

@@ -4,18 +4,21 @@ import sys
import sysconfig import sysconfig
from pathlib import Path from pathlib import Path
import cx_Freeze import cx_Freeze
from kivy_deps import sdl2, glew
from Utils import version_tuple
is_64bits = sys.maxsize > 2 ** 32 is_64bits = sys.maxsize > 2 ** 32
folder = "exe.{platform}-{version}".format(platform=sysconfig.get_platform(), arch_folder = "exe.{platform}-{version}".format(platform=sysconfig.get_platform(),
version=sysconfig.get_python_version()) version=sysconfig.get_python_version())
buildfolder = Path("build", folder) buildfolder = Path("build", arch_folder)
sbuildfolder = str(buildfolder) sbuildfolder = str(buildfolder)
libfolder = Path(buildfolder, "lib") libfolder = Path(buildfolder, "lib")
library = Path(libfolder, "library.zip") library = Path(libfolder, "library.zip")
print("Outputting to: " + sbuildfolder) print("Outputting to: " + sbuildfolder)
icon = os.path.join("data", "icon.ico") icon = os.path.join("data", "icon.ico")
mcicon = os.path.join("data", "mcicon.ico")
if os.path.exists("X:/pw.txt"): if os.path.exists("X:/pw.txt"):
print("Using signtool") print("Using signtool")
@@ -38,38 +41,56 @@ def _threaded_hash(filepath):
os.makedirs(buildfolder, exist_ok=True) os.makedirs(buildfolder, exist_ok=True)
def manifest_creation(folder): def manifest_creation(folder, create_hashes=False):
# Since the setup is now split into components and the manifest is not,
# it makes most sense to just remove the hashes for now. Not aware of anyone using them.
hashes = {} hashes = {}
manifestpath = os.path.join(folder, "manifest.json") manifestpath = os.path.join(folder, "manifest.json")
from concurrent.futures import ThreadPoolExecutor if create_hashes:
pool = ThreadPoolExecutor() from concurrent.futures import ThreadPoolExecutor
for dirpath, dirnames, filenames in os.walk(folder): pool = ThreadPoolExecutor()
for filename in filenames: for dirpath, dirnames, filenames in os.walk(folder):
path = os.path.join(dirpath, filename) for filename in filenames:
hashes[os.path.relpath(path, start=folder)] = pool.submit(_threaded_hash, path) path = os.path.join(dirpath, filename)
hashes[os.path.relpath(path, start=folder)] = pool.submit(_threaded_hash, path)
import json import json
from Utils import version_tuple manifest = {
manifest = {"buildtime": buildtime.isoformat(sep=" ", timespec="seconds"), "buildtime": buildtime.isoformat(sep=" ", timespec="seconds"),
"hashes": {path: hash.result() for path, hash in hashes.items()}, "hashes": {path: hash.result() for path, hash in hashes.items()},
"version": version_tuple} "version": version_tuple}
json.dump(manifest, open(manifestpath, "wt"), indent=4) json.dump(manifest, open(manifestpath, "wt"), indent=4)
print("Created Manifest") print("Created Manifest")
def remove_sprites_from_folder(folder):
for file in os.listdir(folder):
if file != ".gitignore":
os.remove(folder / file)
scripts = { scripts = {
"LttPClient.py": "ArchipelagoLttPClient", # Core
"MultiServer.py": "ArchipelagoServer", "MultiServer.py": ("ArchipelagoServer", False, icon),
"Generate.py": "ArchipelagoGenerate", "Generate.py": ("ArchipelagoGenerate", False, icon),
"LttPAdjuster.py": "ArchipelagoLttPAdjuster" # LttP
"LttPClient.py": ("ArchipelagoLttPClient", True, icon),
"LttPAdjuster.py": ("ArchipelagoLttPAdjuster", True, icon),
# Factorio
"FactorioClient.py": ("ArchipelagoFactorioClient", True, icon),
# Minecraft
"MinecraftClient.py": ("ArchipelagoMinecraftClient", False, mcicon),
} }
exes = [] exes = []
for script, scriptname in scripts.items(): for script, (scriptname, gui, icon) in scripts.items():
exes.append(cx_Freeze.Executable( exes.append(cx_Freeze.Executable(
script=script, script=script,
target_name=scriptname + ("" if sys.platform == "linux" else ".exe"), target_name=scriptname + ("" if sys.platform == "linux" else ".exe"),
icon=icon, icon=icon,
base="Win32GUI" if sys.platform == "win32" and gui else None
)) ))
import datetime import datetime
@@ -78,21 +99,21 @@ buildtime = datetime.datetime.utcnow()
cx_Freeze.setup( cx_Freeze.setup(
name="Archipelago", name="Archipelago",
version=f"{buildtime.year}.{buildtime.month}.{buildtime.day}.{buildtime.hour}", version=f"{version_tuple.major}.{version_tuple.minor}.{version_tuple.build}",
description="Archipelago", description="Archipelago",
executables=exes, executables=exes,
options={ options={
"build_exe": { "build_exe": {
"packages": ["websockets", "worlds"], "packages": ["websockets", "worlds", "kivy"],
"includes": [], "includes": [],
"excludes": ["numpy", "Cython", "PySide2", "PIL", "excludes": ["numpy", "Cython", "PySide2", "PIL",
"pandas"], "pandas"],
"zip_include_packages": ["*"], "zip_include_packages": ["*"],
"zip_exclude_packages": ["worlds"], "zip_exclude_packages": ["worlds", "kivy"],
"include_files": [], "include_files": [],
"include_msvcr": True, "include_msvcr": False,
"replace_paths": [("*", "")], "replace_paths": [("*", "")],
"optimize": 2, "optimize": 1,
"build_exe": buildfolder "build_exe": buildfolder
}, },
}, },
@@ -113,6 +134,10 @@ def installfile(path, keep_content=False):
print('Warning,', path, 'not found') print('Warning,', path, 'not found')
for folder in sdl2.dep_bins + glew.dep_bins:
shutil.copytree(folder, libfolder, dirs_exist_ok=True)
print('copying', folder, '->', libfolder)
extra_data = ["LICENSE", "data", "EnemizerCLI", "host.yaml", "SNI", "meta.yaml"] extra_data = ["LICENSE", "data", "EnemizerCLI", "host.yaml", "SNI", "meta.yaml"]
for data in extra_data: for data in extra_data:
@@ -138,76 +163,6 @@ if signtool:
print(f"Signing SNI") print(f"Signing SNI")
os.system(signtool + os.path.join(buildfolder, "SNI", "SNI.exe")) os.system(signtool + os.path.join(buildfolder, "SNI", "SNI.exe"))
alttpr_sprites_folder = buildfolder / "data" / "sprites" / "alttpr" remove_sprites_from_folder(buildfolder / "data" / "sprites" / "alttpr")
for file in os.listdir(alttpr_sprites_folder):
if file != ".gitignore":
os.remove(alttpr_sprites_folder / file)
manifest_creation(buildfolder)
buildfolder = Path("build_factorio", folder)
sbuildfolder = str(buildfolder)
libfolder = Path(buildfolder, "lib")
library = Path(libfolder, "library.zip")
print("Outputting Factorio Client to: " + sbuildfolder)
os.makedirs(buildfolder, exist_ok=True)
exes = [
cx_Freeze.Executable(
script="FactorioClient.py",
target_name="ArchipelagoFactorioClient" + ("" if sys.platform == "linux" else ".exe"),
icon=icon,
base="Win32GUI" if sys.platform == "win32" else None
)]
import datetime
buildtime = datetime.datetime.utcnow()
cx_Freeze.setup(
name="Archipelago Factorio Client",
version=f"{buildtime.year}.{buildtime.month}.{buildtime.day}.{buildtime.hour}",
description="Archipelago Factorio Client",
executables=exes,
options={
"build_exe": {
"packages": ["websockets", "kivy", "worlds"],
"includes": [],
"excludes": ["numpy", "Cython", "PySide2", "PIL",
"pandas"],
"zip_include_packages": ["*"],
"zip_exclude_packages": ["kivy", "worlds"],
"include_files": [],
"include_msvcr": True,
"replace_paths": [("*", "")],
"optimize": 2,
"build_exe": buildfolder
},
},
)
extra_data = ["LICENSE", "data", "host.yaml", "meta.yaml"]
from kivy_deps import sdl2, glew
for folder in sdl2.dep_bins+glew.dep_bins:
shutil.copytree(folder, buildfolder, dirs_exist_ok=True)
for data in extra_data:
installfile(Path(data))
os.makedirs(buildfolder / "Players", exist_ok=True)
shutil.copyfile("playerSettings.yaml", buildfolder / "Players" / "weightedSettings.yaml")
if signtool:
for exe in exes:
print(f"Signing {exe.target_name}")
os.system(signtool + os.path.join(buildfolder, exe.target_name))
alttpr_sprites_folder = buildfolder / "data" / "sprites" / "alttpr"
for file in os.listdir(alttpr_sprites_folder):
if file != ".gitignore":
os.remove(alttpr_sprites_folder / file)
manifest_creation(buildfolder) manifest_creation(buildfolder)

View File

@@ -4,11 +4,10 @@ from BaseClasses import MultiWorld
from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool from worlds.alttp.Dungeons import create_dungeons, get_dungeon_item_pool
from worlds.alttp.EntranceShuffle import link_inverted_entrances from worlds.alttp.EntranceShuffle import link_inverted_entrances
from worlds.alttp.InvertedRegions import create_inverted_regions from worlds.alttp.InvertedRegions import create_inverted_regions
from worlds.alttp.ItemPool import generate_itempool, difficulties from worlds.alttp.ItemPool import difficulties
from worlds.alttp.Items import ItemFactory from worlds.alttp.Items import ItemFactory
from worlds.alttp.Regions import mark_light_world_regions from worlds.alttp.Regions import mark_light_world_regions
from worlds.alttp.Shops import create_shops from worlds.alttp.Shops import create_shops
from worlds.alttp.Rules import set_rules
from test.TestBase import TestBase from test.TestBase import TestBase
from worlds import AutoWorld from worlds import AutoWorld

View File

@@ -1,4 +1,5 @@
import unittest import unittest
from argparse import Namespace
from BaseClasses import MultiWorld from BaseClasses import MultiWorld
from worlds.alttp.Dungeons import create_dungeons from worlds.alttp.Dungeons import create_dungeons
@@ -7,6 +8,7 @@ from worlds.alttp.EntranceShuffle import connect_entrance, Inverted_LW_Entrances
from worlds.alttp.InvertedRegions import create_inverted_regions from worlds.alttp.InvertedRegions import create_inverted_regions
from worlds.alttp.ItemPool import difficulties from worlds.alttp.ItemPool import difficulties
from worlds.alttp.Rules import set_inverted_big_bomb_rules from worlds.alttp.Rules import set_inverted_big_bomb_rules
from worlds import AutoWorld
class TestInvertedBombRules(unittest.TestCase): class TestInvertedBombRules(unittest.TestCase):
@@ -14,6 +16,10 @@ class TestInvertedBombRules(unittest.TestCase):
def setUp(self): def setUp(self):
self.world = MultiWorld(1) self.world = MultiWorld(1)
self.world.mode[1] = "inverted" self.world.mode[1] = "inverted"
args = Namespace
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args)
self.world.difficulty_requirements[1] = difficulties['normal'] self.world.difficulty_requirements[1] = difficulties['normal']
create_inverted_regions(self.world, 1) create_inverted_regions(self.world, 1)
create_dungeons(self.world, 1) create_dungeons(self.world, 1)

File diff suppressed because it is too large Load Diff

View File

@@ -6,31 +6,31 @@ class TestEntrances(TestMinecraft):
self.run_entrance_tests([ self.run_entrance_tests([
['Nether Portal', False, []], ['Nether Portal', False, []],
['Nether Portal', False, [], ['Flint and Steel']], ['Nether Portal', False, [], ['Flint and Steel']],
['Nether Portal', False, [], ['Ingot Crafting']], ['Nether Portal', False, [], ['Progressive Resource Crafting']],
['Nether Portal', False, [], ['Progressive Tools']], ['Nether Portal', False, [], ['Progressive Tools']],
['Nether Portal', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], ['Nether Portal', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
['Nether Portal', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Bucket']], ['Nether Portal', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Bucket']],
['Nether Portal', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools']], ['Nether Portal', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools']],
['End Portal', False, []], ['End Portal', False, []],
['End Portal', False, [], ['Brewing']], ['End Portal', False, [], ['Brewing']],
['End Portal', False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']], ['End Portal', False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']],
['End Portal', False, [], ['Flint and Steel']], ['End Portal', False, [], ['Flint and Steel']],
['End Portal', False, [], ['Ingot Crafting']], ['End Portal', False, [], ['Progressive Resource Crafting']],
['End Portal', False, [], ['Progressive Tools']], ['End Portal', False, [], ['Progressive Tools']],
['End Portal', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], ['End Portal', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
['End Portal', False, [], ['Progressive Weapons']], ['End Portal', False, [], ['Progressive Weapons']],
['End Portal', False, [], ['Progressive Armor', 'Shield']], ['End Portal', False, [], ['Progressive Armor', 'Shield']],
['End Portal', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Bucket', ['End Portal', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Bucket',
'Progressive Weapons', 'Progressive Armor', 'Progressive Weapons', 'Progressive Armor',
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
['End Portal', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Bucket', ['End Portal', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Bucket',
'Progressive Weapons', 'Shield', 'Progressive Weapons', 'Shield',
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
['End Portal', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools', ['End Portal', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools',
'Progressive Weapons', 'Progressive Armor', 'Progressive Weapons', 'Progressive Armor',
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
['End Portal', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools', ['End Portal', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools',
'Progressive Weapons', 'Shield', 'Progressive Weapons', 'Shield',
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
]) ])
@@ -39,53 +39,53 @@ class TestEntrances(TestMinecraft):
self.run_entrance_tests([ # Structures 1 and 2 should be logically equivalent self.run_entrance_tests([ # Structures 1 and 2 should be logically equivalent
['Overworld Structure 1', False, []], ['Overworld Structure 1', False, []],
['Overworld Structure 1', False, [], ['Progressive Weapons']], ['Overworld Structure 1', False, [], ['Progressive Weapons']],
['Overworld Structure 1', False, [], ['Ingot Crafting', 'Campfire']], ['Overworld Structure 1', False, [], ['Progressive Resource Crafting', 'Campfire']],
['Overworld Structure 1', True, ['Progressive Weapons', 'Ingot Crafting']], ['Overworld Structure 1', True, ['Progressive Weapons', 'Progressive Resource Crafting']],
['Overworld Structure 1', True, ['Progressive Weapons', 'Campfire']], ['Overworld Structure 1', True, ['Progressive Weapons', 'Campfire']],
['Overworld Structure 2', False, []], ['Overworld Structure 2', False, []],
['Overworld Structure 2', False, [], ['Progressive Weapons']], ['Overworld Structure 2', False, [], ['Progressive Weapons']],
['Overworld Structure 2', False, [], ['Ingot Crafting', 'Campfire']], ['Overworld Structure 2', False, [], ['Progressive Resource Crafting', 'Campfire']],
['Overworld Structure 2', True, ['Progressive Weapons', 'Ingot Crafting']], ['Overworld Structure 2', True, ['Progressive Weapons', 'Progressive Resource Crafting']],
['Overworld Structure 2', True, ['Progressive Weapons', 'Campfire']], ['Overworld Structure 2', True, ['Progressive Weapons', 'Campfire']],
['Nether Structure 1', False, []], ['Nether Structure 1', False, []],
['Nether Structure 1', False, [], ['Flint and Steel']], ['Nether Structure 1', False, [], ['Flint and Steel']],
['Nether Structure 1', False, [], ['Ingot Crafting']], ['Nether Structure 1', False, [], ['Progressive Resource Crafting']],
['Nether Structure 1', False, [], ['Progressive Tools']], ['Nether Structure 1', False, [], ['Progressive Tools']],
['Nether Structure 1', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], ['Nether Structure 1', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
['Nether Structure 1', False, [], ['Progressive Weapons']], ['Nether Structure 1', False, [], ['Progressive Weapons']],
['Nether Structure 1', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Bucket', 'Progressive Weapons']], ['Nether Structure 1', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Bucket', 'Progressive Weapons']],
['Nether Structure 1', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons']], ['Nether Structure 1', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons']],
['Nether Structure 2', False, []], ['Nether Structure 2', False, []],
['Nether Structure 2', False, [], ['Flint and Steel']], ['Nether Structure 2', False, [], ['Flint and Steel']],
['Nether Structure 2', False, [], ['Ingot Crafting']], ['Nether Structure 2', False, [], ['Progressive Resource Crafting']],
['Nether Structure 2', False, [], ['Progressive Tools']], ['Nether Structure 2', False, [], ['Progressive Tools']],
['Nether Structure 2', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], ['Nether Structure 2', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
['Nether Structure 2', False, [], ['Progressive Weapons']], ['Nether Structure 2', False, [], ['Progressive Weapons']],
['Nether Structure 2', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Bucket', 'Progressive Weapons']], ['Nether Structure 2', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Bucket', 'Progressive Weapons']],
['Nether Structure 2', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons']], ['Nether Structure 2', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons']],
['The End Structure', False, []], ['The End Structure', False, []],
['The End Structure', False, [], ['Brewing']], ['The End Structure', False, [], ['Brewing']],
['The End Structure', False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']], ['The End Structure', False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']],
['The End Structure', False, [], ['Flint and Steel']], ['The End Structure', False, [], ['Flint and Steel']],
['The End Structure', False, [], ['Ingot Crafting']], ['The End Structure', False, [], ['Progressive Resource Crafting']],
['The End Structure', False, [], ['Progressive Tools']], ['The End Structure', False, [], ['Progressive Tools']],
['The End Structure', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']], ['The End Structure', False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
['The End Structure', False, [], ['Progressive Weapons']], ['The End Structure', False, [], ['Progressive Weapons']],
['The End Structure', False, [], ['Progressive Armor', 'Shield']], ['The End Structure', False, [], ['Progressive Armor', 'Shield']],
['The End Structure', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Bucket', ['The End Structure', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Bucket',
'Progressive Weapons', 'Progressive Armor', 'Progressive Weapons', 'Progressive Armor',
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
['The End Structure', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Bucket', ['The End Structure', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Bucket',
'Progressive Weapons', 'Shield', 'Progressive Weapons', 'Shield',
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
['The End Structure', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools', ['The End Structure', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools',
'Progressive Weapons', 'Progressive Armor', 'Progressive Weapons', 'Progressive Armor',
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],
['The End Structure', True, ['Flint and Steel', 'Ingot Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools', ['The End Structure', True, ['Flint and Steel', 'Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools',
'Progressive Weapons', 'Shield', 'Progressive Weapons', 'Shield',
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']], 'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls']],

View File

@@ -4,8 +4,8 @@ from BaseClasses import MultiWorld
from worlds import AutoWorld from worlds import AutoWorld
from worlds.minecraft import MinecraftWorld from worlds.minecraft import MinecraftWorld
from worlds.minecraft.Items import MinecraftItem, item_table from worlds.minecraft.Items import MinecraftItem, item_table
from worlds.minecraft.Options import AdvancementGoal, CombatDifficulty from worlds.minecraft.Options import AdvancementGoal, CombatDifficulty, BeeTraps
from Options import Toggle from Options import Toggle, Range
# Converts the name of an item into an item object # Converts the name of an item into an item object
def MCItemFactory(items, player: int): def MCItemFactory(items, player: int):
@@ -36,8 +36,10 @@ class TestMinecraft(TestBase):
setattr(self.world, "advancement_goal", {1: AdvancementGoal(30)}) setattr(self.world, "advancement_goal", {1: AdvancementGoal(30)})
setattr(self.world, "shuffle_structures", {1: Toggle(False)}) setattr(self.world, "shuffle_structures", {1: Toggle(False)})
setattr(self.world, "combat_difficulty", {1: CombatDifficulty(1)}) # normal setattr(self.world, "combat_difficulty", {1: CombatDifficulty(1)}) # normal
setattr(self.world, "bee_traps", {1: Toggle(False)}) setattr(self.world, "bee_traps", {1: BeeTraps(0)})
setattr(self.world, "structure_compasses", {1: Toggle(False)}) setattr(self.world, "structure_compasses", {1: Toggle(False)})
setattr(self.world, "egg_shards_required", {1: Range(0)})
setattr(self.world, "egg_shards_available", {1: Range(0)})
AutoWorld.call_single(self.world, "create_regions", 1) AutoWorld.call_single(self.world, "create_regions", 1)
AutoWorld.call_single(self.world, "generate_basic", 1) AutoWorld.call_single(self.world, "generate_basic", 1)
AutoWorld.call_single(self.world, "set_rules", 1) AutoWorld.call_single(self.world, "set_rules", 1)

View File

@@ -1,20 +1,25 @@
from __future__ import annotations from __future__ import annotations
from typing import Dict, Set, Tuple from typing import Dict, Set, Tuple, List, Optional
from BaseClasses import MultiWorld, Item, CollectionState from BaseClasses import MultiWorld, Item, CollectionState, Location
class AutoWorldRegister(type): class AutoWorldRegister(type):
world_types:Dict[str, World] = {} world_types: Dict[str, World] = {}
def __new__(cls, name, bases, dct): def __new__(cls, name, bases, dct):
dct["all_names"] = dct["item_names"] | dct["location_names"] | set(dct.get("item_name_groups", {}))
# filter out any events # filter out any events
dct["item_name_to_id"] = {name: id for name, id in dct["item_name_to_id"].items() if id} dct["item_name_to_id"] = {name: id for name, id in dct["item_name_to_id"].items() if id}
dct["location_name_to_id"] = {name: id for name, id in dct["location_name_to_id"].items() if id} dct["location_name_to_id"] = {name: id for name, id in dct["location_name_to_id"].items() if id}
# build reverse lookups # build reverse lookups
dct["item_id_to_name"] = {code: name for name, code in dct["item_name_to_id"].items()} dct["item_id_to_name"] = {code: name for name, code in dct["item_name_to_id"].items()}
dct["location_id_to_name"] = {code: name for name, code in dct["location_name_to_id"].items()} dct["location_id_to_name"] = {code: name for name, code in dct["location_name_to_id"].items()}
# build rest
dct["item_names"] = frozenset(dct["item_name_to_id"])
dct["location_names"] = frozenset(dct["location_name_to_id"])
dct["all_names"] = dct["item_names"] | dct["location_names"] | set(dct.get("item_name_groups", {}))
# construct class # construct class
new_class = super().__new__(cls, name, bases, dct) new_class = super().__new__(cls, name, bases, dct)
if "game" in dct: if "game" in dct:
@@ -39,8 +44,22 @@ def call_single(world: MultiWorld, method_name: str, player: int, *args):
def call_all(world: MultiWorld, method_name: str, *args): def call_all(world: MultiWorld, method_name: str, *args):
world_types = set()
for player in world.player_ids: for player in world.player_ids:
world_types.add(world.worlds[player].__class__)
call_single(world, method_name, player, *args) call_single(world, method_name, player, *args)
for world_type in world_types:
stage_callable = getattr(world_type, f"stage_{method_name}", None)
if stage_callable:
stage_callable(world, *args)
def call_stage(world: MultiWorld, method_name: str, *args):
world_types = {world.worlds[player].__class__ for player in world.player_ids}
for world_type in world_types:
stage_callable = getattr(world_type, f"stage_{method_name}", None)
if stage_callable:
stage_callable(world, *args)
class World(metaclass=AutoWorldRegister): class World(metaclass=AutoWorldRegister):
@@ -48,23 +67,21 @@ class World(metaclass=AutoWorldRegister):
A Game should have its own subclass of World in which it defines the required data structures.""" A Game should have its own subclass of World in which it defines the required data structures."""
options: dict = {} # link your Options mapping options: dict = {} # link your Options mapping
game: str # name the game game: str # name the game
topology_present: bool = False # indicate if world type has any meaningful layout/pathing topology_present: bool = False # indicate if world type has any meaningful layout/pathing
item_names: Set[str] = frozenset() # set of all potential item names
# maps item group names to sets of items. Example: "Weapons" -> {"Sword", "Bow"}
item_name_groups: Dict[str, Set[str]] = {}
location_names: Set[str] = frozenset() # set of all potential location names
all_names: Set[str] = frozenset() # gets automatically populated with all item, item group and location names all_names: Set[str] = frozenset() # gets automatically populated with all item, item group and location names
# map names to their IDs # map names to their IDs
item_name_to_id: Dict[str, int] = {} item_name_to_id: Dict[str, int] = {}
location_name_to_id: Dict[str, int] = {} location_name_to_id: Dict[str, int] = {}
# reverse, automatically generated # maps item group names to sets of items. Example: "Weapons" -> {"Sword", "Bow"}
item_id_to_name: Dict[int, str] = {} item_name_groups: Dict[str, Set[str]] = {}
location_id_to_name: Dict[int, str] = {}
data_version = 1 # increment this every time something in your world's names/id mappings changes. # increment this every time something in your world's names/id mappings changes.
# While this is set to 0 in *any* AutoWorld, the entire DataPackage is considered in testing mode and will be
# retrieved by clients on every connection.
data_version = 1
hint_blacklist: Set[str] = frozenset() # any names that should not be hintable hint_blacklist: Set[str] = frozenset() # any names that should not be hintable
@@ -74,15 +91,32 @@ class World(metaclass=AutoWorldRegister):
# the client finds its own items in its own world. # the client finds its own items in its own world.
remote_items: bool = True remote_items: bool = True
# For games where after a victory it is impossible to go back in and get additional/remaining Locations checked.
# this forces forfeit: auto for those games.
forced_auto_forfeit: bool = False
# Hide World Type from various views. Does not remove functionality.
hidden = False
# autoset on creation: # autoset on creation:
world: MultiWorld world: MultiWorld
player: int player: int
# automatically generated
item_id_to_name: Dict[int, str]
location_id_to_name: Dict[int, str]
item_names: Set[str] # set of all potential item names
location_names: Set[str] # set of all potential location names
def __init__(self, world: MultiWorld, player: int): def __init__(self, world: MultiWorld, player: int):
self.world = world self.world = world
self.player = player self.player = player
# overridable methods that get called by Main.py, sorted by execution order # overridable methods that get called by Main.py, sorted by execution order
# can also be implemented as a classmethod and called "stage_<original_name",
# in that case the MultiWorld object is passed as an argument and it gets called once for the entire multiworld.
# An example of this can be found in alttp as stage_pre_fill
def generate_early(self): def generate_early(self):
pass pass
@@ -98,6 +132,20 @@ class World(metaclass=AutoWorldRegister):
def generate_basic(self): def generate_basic(self):
pass pass
def pre_fill(self):
"""Optional method that is supposed to be used for special fill stages. This is run *after* plando."""
pass
def fill_hook(cls, progitempool: List[Item], nonexcludeditempool: List[Item],
localrestitempool: Dict[int, List[Item]], nonlocalrestitempool: Dict[int, List[Item]],
restitempool: List[Item], fill_locations: List[Location]):
"""Special method that gets called as part of distribute_items_restrictive (main fill).
This gets called once per present world type."""
pass
def post_fill(self):
"""Optional Method that is called after regular fill. Can be used to do adjustments before output generation."""
def generate_output(self, output_directory: str): def generate_output(self, output_directory: str):
"""This method gets called from a threadpool, do not use world.random here. """This method gets called from a threadpool, do not use world.random here.
If you need any last-second randomization, use MultiWorld.slot_seeds[slot] instead.""" If you need any last-second randomization, use MultiWorld.slot_seeds[slot] instead."""
@@ -107,23 +155,44 @@ class World(metaclass=AutoWorldRegister):
"""Fill in the slot_data field in the Connected network package.""" """Fill in the slot_data field in the Connected network package."""
return {} return {}
def modify_multidata(self, multidata: dict):
"""For deeper modification of server multidata."""
pass
def get_required_client_version(self) -> Tuple[int, int, int]: def get_required_client_version(self) -> Tuple[int, int, int]:
return 0, 0, 3 return 0, 0, 3
# end of Main.py calls # end of Main.py calls
def collect(self, state: CollectionState, item: Item) -> bool: def collect_item(self, state: CollectionState, item: Item, remove=False) -> Optional[str]:
"""Collect an item into state. For speed reasons items that aren't logically useful get skipped.""" """Collect an item name into state. For speed reasons items that aren't logically useful get skipped.
Collect None to skip item.
:param remove: indicate if this is meant to remove from state instead of adding."""
if item.advancement: if item.advancement:
state.prog_items[item.name, item.player] += 1 return item.name
return True # indicate that a logical state change has occured
return False
def create_item(self, name: str) -> Item: def create_item(self, name: str) -> Item:
"""Create an item for this world type and player. """Create an item for this world type and player.
Warning: this may be called with self.world = None, for example by MultiServer""" Warning: this may be called with self.world = None, for example by MultiServer"""
raise NotImplementedError raise NotImplementedError
# following methods should not need to be overriden.
def collect(self, state: CollectionState, item: Item) -> bool:
name = self.collect_item(state, item)
if name:
state.prog_items[name, item.player] += 1
return True
return False
def remove(self, state: CollectionState, item: Item) -> bool:
name = self.collect_item(state, item, True)
if name:
state.prog_items[name, item.player] -= 1
if state.prog_items[name, item.player] < 1:
del (state.prog_items[name, item.player])
return True
return False
# any methods attached to this can be used as part of CollectionState, # any methods attached to this can be used as part of CollectionState,
# please use a prefix as all of them get clobbered together # please use a prefix as all of them get clobbered together

View File

@@ -27,8 +27,14 @@ for world_name, world in AutoWorldRegister.world_types.items():
lookup_any_location_id_to_name.update(world.location_id_to_name) lookup_any_location_id_to_name.update(world.location_id_to_name)
network_data_package = { network_data_package = {
# Remove with 0.2.0
"lookup_any_location_id_to_name": lookup_any_location_id_to_name, # legacy, to be removed "lookup_any_location_id_to_name": lookup_any_location_id_to_name, # legacy, to be removed
"lookup_any_item_id_to_name": lookup_any_item_id_to_name, # legacy, to be removed "lookup_any_item_id_to_name": lookup_any_item_id_to_name, # legacy, to be removed
"version": sum(world.data_version for world in AutoWorldRegister.world_types.values()), "version": sum(world.data_version for world in AutoWorldRegister.world_types.values()),
"games": games, "games": games,
} }
# Set entire datapackage to version 0 if any of them are set to 0
if any(not world.data_version for world in AutoWorldRegister.world_types.values()):
network_data_package["version"] = 0

View File

@@ -3,12 +3,16 @@ from worlds.alttp.Bosses import BossFactory
from Fill import fill_restrictive from Fill import fill_restrictive
from worlds.alttp.Items import ItemFactory from worlds.alttp.Items import ItemFactory
from worlds.alttp.Regions import lookup_boss_drops from worlds.alttp.Regions import lookup_boss_drops
from worlds.alttp.Options import smallkey_shuffle
def create_dungeons(world, player): def create_dungeons(world, player):
def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dungeon_items): def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dungeon_items):
dungeon = Dungeon(name, dungeon_regions, big_key, [] if world.keyshuffle[player] == "universal" else small_keys, dungeon = Dungeon(name, dungeon_regions, big_key,
[] if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal else small_keys,
dungeon_items, player) dungeon_items, player)
for item in dungeon.all_items:
item.dungeon = dungeon
dungeon.boss = BossFactory(default_boss, player) if default_boss else None dungeon.boss = BossFactory(default_boss, player) if default_boss else None
for region in dungeon.regions: for region in dungeon.regions:
world.get_region(region, player).dungeon = dungeon world.get_region(region, player).dungeon = dungeon
@@ -21,131 +25,141 @@ def create_dungeons(world, player):
EP = make_dungeon('Eastern Palace', 'Armos Knights', ['Eastern Palace'], EP = make_dungeon('Eastern Palace', 'Armos Knights', ['Eastern Palace'],
ItemFactory('Big Key (Eastern Palace)', player), [], ItemFactory('Big Key (Eastern Palace)', player), [],
ItemFactory(['Map (Eastern Palace)', 'Compass (Eastern Palace)'], player)) ItemFactory(['Map (Eastern Palace)', 'Compass (Eastern Palace)'], player))
DP = make_dungeon('Desert Palace', 'Lanmolas', ['Desert Palace North', 'Desert Palace Main (Inner)', 'Desert Palace Main (Outer)', 'Desert Palace East'], ItemFactory('Big Key (Desert Palace)', player), [ItemFactory('Small Key (Desert Palace)', player)], ItemFactory(['Map (Desert Palace)', 'Compass (Desert Palace)'], player)) DP = make_dungeon('Desert Palace', 'Lanmolas',
ToH = make_dungeon('Tower of Hera', 'Moldorm', ['Tower of Hera (Bottom)', 'Tower of Hera (Basement)', 'Tower of Hera (Top)'], ItemFactory('Big Key (Tower of Hera)', player), [ItemFactory('Small Key (Tower of Hera)', player)], ItemFactory(['Map (Tower of Hera)', 'Compass (Tower of Hera)'], player)) ['Desert Palace North', 'Desert Palace Main (Inner)', 'Desert Palace Main (Outer)',
PoD = make_dungeon('Palace of Darkness', 'Helmasaur King', ['Palace of Darkness (Entrance)', 'Palace of Darkness (Center)', 'Palace of Darkness (Big Key Chest)', 'Palace of Darkness (Bonk Section)', 'Palace of Darkness (North)', 'Palace of Darkness (Maze)', 'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness (Final Section)'], ItemFactory('Big Key (Palace of Darkness)', player), ItemFactory(['Small Key (Palace of Darkness)'] * 6, player), ItemFactory(['Map (Palace of Darkness)', 'Compass (Palace of Darkness)'], player)) 'Desert Palace East'], ItemFactory('Big Key (Desert Palace)', player),
TT = make_dungeon('Thieves Town', 'Blind', ['Thieves Town (Entrance)', 'Thieves Town (Deep)', 'Blind Fight'], ItemFactory('Big Key (Thieves Town)', player), [ItemFactory('Small Key (Thieves Town)', player)], ItemFactory(['Map (Thieves Town)', 'Compass (Thieves Town)'], player)) [ItemFactory('Small Key (Desert Palace)', player)],
SW = make_dungeon('Skull Woods', 'Mothula', ['Skull Woods Final Section (Entrance)', 'Skull Woods First Section', 'Skull Woods Second Section', 'Skull Woods Second Section (Drop)', 'Skull Woods Final Section (Mothula)', 'Skull Woods First Section (Right)', 'Skull Woods First Section (Left)', 'Skull Woods First Section (Top)'], ItemFactory('Big Key (Skull Woods)', player), ItemFactory(['Small Key (Skull Woods)'] * 3, player), ItemFactory(['Map (Skull Woods)', 'Compass (Skull Woods)'], player)) ItemFactory(['Map (Desert Palace)', 'Compass (Desert Palace)'], player))
SP = make_dungeon('Swamp Palace', 'Arrghus', ['Swamp Palace (Entrance)', 'Swamp Palace (First Room)', 'Swamp Palace (Starting Area)', 'Swamp Palace (Center)', 'Swamp Palace (North)'], ItemFactory('Big Key (Swamp Palace)', player), [ItemFactory('Small Key (Swamp Palace)', player)], ItemFactory(['Map (Swamp Palace)', 'Compass (Swamp Palace)'], player)) ToH = make_dungeon('Tower of Hera', 'Moldorm',
IP = make_dungeon('Ice Palace', 'Kholdstare', ['Ice Palace (Entrance)', 'Ice Palace (Main)', 'Ice Palace (East)', 'Ice Palace (East Top)', 'Ice Palace (Kholdstare)'], ItemFactory('Big Key (Ice Palace)', player), ItemFactory(['Small Key (Ice Palace)'] * 2, player), ItemFactory(['Map (Ice Palace)', 'Compass (Ice Palace)'], player)) ['Tower of Hera (Bottom)', 'Tower of Hera (Basement)', 'Tower of Hera (Top)'],
MM = make_dungeon('Misery Mire', 'Vitreous', ['Misery Mire (Entrance)', 'Misery Mire (Main)', 'Misery Mire (West)', 'Misery Mire (Final Area)', 'Misery Mire (Vitreous)'], ItemFactory('Big Key (Misery Mire)', player), ItemFactory(['Small Key (Misery Mire)'] * 3, player), ItemFactory(['Map (Misery Mire)', 'Compass (Misery Mire)'], player)) ItemFactory('Big Key (Tower of Hera)', player),
TR = make_dungeon('Turtle Rock', 'Trinexx', ['Turtle Rock (Entrance)', 'Turtle Rock (First Section)', 'Turtle Rock (Chain Chomp Room)', 'Turtle Rock (Second Section)', 'Turtle Rock (Big Chest)', 'Turtle Rock (Crystaroller Room)', 'Turtle Rock (Dark Room)', 'Turtle Rock (Eye Bridge)', 'Turtle Rock (Trinexx)'], ItemFactory('Big Key (Turtle Rock)', player), ItemFactory(['Small Key (Turtle Rock)'] * 4, player), ItemFactory(['Map (Turtle Rock)', 'Compass (Turtle Rock)'], player)) [ItemFactory('Small Key (Tower of Hera)', player)],
ItemFactory(['Map (Tower of Hera)', 'Compass (Tower of Hera)'], player))
PoD = make_dungeon('Palace of Darkness', 'Helmasaur King',
['Palace of Darkness (Entrance)', 'Palace of Darkness (Center)',
'Palace of Darkness (Big Key Chest)', 'Palace of Darkness (Bonk Section)',
'Palace of Darkness (North)', 'Palace of Darkness (Maze)',
'Palace of Darkness (Harmless Hellway)', 'Palace of Darkness (Final Section)'],
ItemFactory('Big Key (Palace of Darkness)', player),
ItemFactory(['Small Key (Palace of Darkness)'] * 6, player),
ItemFactory(['Map (Palace of Darkness)', 'Compass (Palace of Darkness)'], player))
TT = make_dungeon('Thieves Town', 'Blind', ['Thieves Town (Entrance)', 'Thieves Town (Deep)', 'Blind Fight'],
ItemFactory('Big Key (Thieves Town)', player), [ItemFactory('Small Key (Thieves Town)', player)],
ItemFactory(['Map (Thieves Town)', 'Compass (Thieves Town)'], player))
SW = make_dungeon('Skull Woods', 'Mothula', ['Skull Woods Final Section (Entrance)', 'Skull Woods First Section',
'Skull Woods Second Section', 'Skull Woods Second Section (Drop)',
'Skull Woods Final Section (Mothula)',
'Skull Woods First Section (Right)',
'Skull Woods First Section (Left)', 'Skull Woods First Section (Top)'],
ItemFactory('Big Key (Skull Woods)', player),
ItemFactory(['Small Key (Skull Woods)'] * 3, player),
ItemFactory(['Map (Skull Woods)', 'Compass (Skull Woods)'], player))
SP = make_dungeon('Swamp Palace', 'Arrghus',
['Swamp Palace (Entrance)', 'Swamp Palace (First Room)', 'Swamp Palace (Starting Area)',
'Swamp Palace (Center)', 'Swamp Palace (North)'], ItemFactory('Big Key (Swamp Palace)', player),
[ItemFactory('Small Key (Swamp Palace)', player)],
ItemFactory(['Map (Swamp Palace)', 'Compass (Swamp Palace)'], player))
IP = make_dungeon('Ice Palace', 'Kholdstare',
['Ice Palace (Entrance)', 'Ice Palace (Main)', 'Ice Palace (East)', 'Ice Palace (East Top)',
'Ice Palace (Kholdstare)'], ItemFactory('Big Key (Ice Palace)', player),
ItemFactory(['Small Key (Ice Palace)'] * 2, player),
ItemFactory(['Map (Ice Palace)', 'Compass (Ice Palace)'], player))
MM = make_dungeon('Misery Mire', 'Vitreous',
['Misery Mire (Entrance)', 'Misery Mire (Main)', 'Misery Mire (West)', 'Misery Mire (Final Area)',
'Misery Mire (Vitreous)'], ItemFactory('Big Key (Misery Mire)', player),
ItemFactory(['Small Key (Misery Mire)'] * 3, player),
ItemFactory(['Map (Misery Mire)', 'Compass (Misery Mire)'], player))
TR = make_dungeon('Turtle Rock', 'Trinexx',
['Turtle Rock (Entrance)', 'Turtle Rock (First Section)', 'Turtle Rock (Chain Chomp Room)',
'Turtle Rock (Second Section)', 'Turtle Rock (Big Chest)', 'Turtle Rock (Crystaroller Room)',
'Turtle Rock (Dark Room)', 'Turtle Rock (Eye Bridge)', 'Turtle Rock (Trinexx)'],
ItemFactory('Big Key (Turtle Rock)', player),
ItemFactory(['Small Key (Turtle Rock)'] * 4, player),
ItemFactory(['Map (Turtle Rock)', 'Compass (Turtle Rock)'], player))
if world.mode[player] != 'inverted': if world.mode[player] != 'inverted':
AT = make_dungeon('Agahnims Tower', 'Agahnim', ['Agahnims Tower', 'Agahnim 1'], None, ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), []) AT = make_dungeon('Agahnims Tower', 'Agahnim', ['Agahnims Tower', 'Agahnim 1'], None,
GT = make_dungeon('Ganons Tower', 'Agahnim2', ['Ganons Tower (Entrance)', 'Ganons Tower (Tile Room)', 'Ganons Tower (Compass Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower (Map Room)', 'Ganons Tower (Firesnake Room)', 'Ganons Tower (Teleport Room)', 'Ganons Tower (Bottom)', 'Ganons Tower (Top)', 'Ganons Tower (Before Moldorm)', 'Ganons Tower (Moldorm)', 'Agahnim 2'], ItemFactory('Big Key (Ganons Tower)', player), ItemFactory(['Small Key (Ganons Tower)'] * 4, player), ItemFactory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], player)) ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), [])
GT = make_dungeon('Ganons Tower', 'Agahnim2',
['Ganons Tower (Entrance)', 'Ganons Tower (Tile Room)', 'Ganons Tower (Compass Room)',
'Ganons Tower (Hookshot Room)', 'Ganons Tower (Map Room)', 'Ganons Tower (Firesnake Room)',
'Ganons Tower (Teleport Room)', 'Ganons Tower (Bottom)', 'Ganons Tower (Top)',
'Ganons Tower (Before Moldorm)', 'Ganons Tower (Moldorm)', 'Agahnim 2'],
ItemFactory('Big Key (Ganons Tower)', player),
ItemFactory(['Small Key (Ganons Tower)'] * 4, player),
ItemFactory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], player))
else: else:
AT = make_dungeon('Inverted Agahnims Tower', 'Agahnim', ['Inverted Agahnims Tower', 'Agahnim 1'], None, ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), []) AT = make_dungeon('Inverted Agahnims Tower', 'Agahnim', ['Inverted Agahnims Tower', 'Agahnim 1'], None,
GT = make_dungeon('Inverted Ganons Tower', 'Agahnim2', ['Inverted Ganons Tower (Entrance)', 'Ganons Tower (Tile Room)', 'Ganons Tower (Compass Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower (Map Room)', 'Ganons Tower (Firesnake Room)', 'Ganons Tower (Teleport Room)', 'Ganons Tower (Bottom)', 'Ganons Tower (Top)', 'Ganons Tower (Before Moldorm)', 'Ganons Tower (Moldorm)', 'Agahnim 2'], ItemFactory('Big Key (Ganons Tower)', player), ItemFactory(['Small Key (Ganons Tower)'] * 4, player), ItemFactory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], player)) ItemFactory(['Small Key (Agahnims Tower)'] * 2, player), [])
GT = make_dungeon('Inverted Ganons Tower', 'Agahnim2',
['Inverted Ganons Tower (Entrance)', 'Ganons Tower (Tile Room)',
'Ganons Tower (Compass Room)', 'Ganons Tower (Hookshot Room)', 'Ganons Tower (Map Room)',
'Ganons Tower (Firesnake Room)', 'Ganons Tower (Teleport Room)', 'Ganons Tower (Bottom)',
'Ganons Tower (Top)', 'Ganons Tower (Before Moldorm)', 'Ganons Tower (Moldorm)',
'Agahnim 2'], ItemFactory('Big Key (Ganons Tower)', player),
ItemFactory(['Small Key (Ganons Tower)'] * 4, player),
ItemFactory(['Map (Ganons Tower)', 'Compass (Ganons Tower)'], player))
GT.bosses['bottom'] = BossFactory('Armos Knights', player) GT.bosses['bottom'] = BossFactory('Armos Knights', player)
GT.bosses['middle'] = BossFactory('Lanmolas', player) GT.bosses['middle'] = BossFactory('Lanmolas', player)
GT.bosses['top'] = BossFactory('Moldorm', player) GT.bosses['top'] = BossFactory('Moldorm', player)
world.dungeons += [ES, EP, DP, ToH, AT, PoD, TT, SW, SP, IP, MM, TR, GT] for dungeon in [ES, EP, DP, ToH, AT, PoD, TT, SW, SP, IP, MM, TR, GT]:
world.dungeons[dungeon.name, dungeon.player] = dungeon
def fill_dungeons(world):
#All chests on the freebes list locked behind a key in room with no other exit
freebes = ['Ganons Tower - Map Chest', 'Palace of Darkness - Harmless Hellway', 'Palace of Darkness - Big Key Chest', 'Turtle Rock - Big Key Chest']
all_state_base = world.get_all_state()
dungeons = [(list(dungeon.regions), dungeon.big_key, list(dungeon.small_keys), list(dungeon.dungeon_items)) for dungeon in world.dungeons]
loopcnt = 0
while dungeons:
loopcnt += 1
dungeon_regions, big_key, small_keys, dungeon_items = dungeons.pop(0)
# this is what we need to fill
dungeon_locations = [location for location in world.get_unfilled_locations() if location.parent_region.name in dungeon_regions]
world.random.shuffle(dungeon_locations)
all_state = all_state_base.copy()
# first place big key
if big_key is not None:
bk_location = None
for location in dungeon_locations:
if location.item_rule(big_key):
bk_location = location
break
if bk_location is None:
raise RuntimeError('No suitable location for %s' % big_key)
world.push_item(bk_location, big_key, False)
bk_location.event = True
bk_location.locked = True
dungeon_locations.remove(bk_location)
big_key = None
# next place small keys
while small_keys:
small_key = small_keys.pop()
all_state.sweep_for_events()
sk_location = None
for location in dungeon_locations:
if location.name in freebes or (location.can_reach(all_state) and location.item_rule(small_key)):
sk_location = location
break
if sk_location is None:
# need to retry this later
small_keys.append(small_key)
dungeons.append((dungeon_regions, big_key, small_keys, dungeon_items))
# infinite regression protection
if loopcnt < (30 * world.players):
break
else:
raise RuntimeError('No suitable location for %s' % small_key)
world.push_item(sk_location, small_key, False)
sk_location.event = True
sk_location.locked = True
dungeon_locations.remove(sk_location)
if small_keys:
# key placement not finished, loop again
continue
# next place dungeon items
for dungeon_item in dungeon_items:
di_location = dungeon_locations.pop()
world.push_item(di_location, dungeon_item, False)
def get_dungeon_item_pool(world): def get_dungeon_item_pool(world):
items = [item for dungeon in world.dungeons for item in dungeon.all_items] items = [item for dungeon in world.dungeons.values() for item in dungeon.all_items]
for item in items: for item in items:
item.world = world item.world = world
return items return items
def fill_dungeons_restrictive(world):
def get_dungeon_item_pool_player(world, player):
items = [item for dungeon in world.dungeons.values() if dungeon.player == player for item in dungeon.all_items]
for item in items:
item.world = world
return items
def fill_dungeons_restrictive(autoworld, world):
"""Places dungeon-native items into their dungeons, places nothing if everything is shuffled outside.""" """Places dungeon-native items into their dungeons, places nothing if everything is shuffled outside."""
restricted_players = {player for player, restricted in world.restrict_dungeon_item_on_boss.items() if restricted} localized: set = set()
dungeon_specific: set = set()
for subworld in world.get_game_worlds("A Link to the Past"):
player = subworld.player
localized |= {(player, item_name) for item_name in
subworld.dungeon_local_item_names}
dungeon_specific |= {(player, item_name) for item_name in
subworld.dungeon_specific_item_names}
locations = [location for location in world.get_unfilled_dungeon_locations() if localized:
if not (location.player in restricted_players and location.name in lookup_boss_drops)] # filter boss in_dungeon_items = [item for item in get_dungeon_item_pool(world) if (item.player, item.name) in localized]
if in_dungeon_items:
restricted_players = {player for player, restricted in world.restrict_dungeon_item_on_boss.items() if
restricted}
locations = [location for location in world.get_unfilled_dungeon_locations()
# filter boss
if not (location.player in restricted_players and location.name in lookup_boss_drops)]
if dungeon_specific:
for location in locations:
dungeon = location.parent_region.dungeon
orig_rule = location.item_rule
location.item_rule = lambda item, dungeon=dungeon, orig_rule=orig_rule: \
(not (item.player, item.name) in dungeon_specific or item.dungeon is dungeon) and orig_rule(item)
world.random.shuffle(locations) world.random.shuffle(locations)
all_state_base = world.get_all_state() all_state_base = world.get_all_state(use_cache=True)
# Dungeon-locked items have to be placed first, to not run out of spaces for dungeon-locked items
# subsort in the order Big Key, Small Key, Other before placing dungeon items
# with shuffled dungeon items they are distributed as part of the normal item pool sort_order = {"BigKey": 3, "SmallKey": 2}
for item in world.get_items(): in_dungeon_items.sort(
if (item.smallkey and world.keyshuffle[item.player]) or (item.bigkey and world.bigkeyshuffle[item.player]): key=lambda item: sort_order.get(item.type, 1) +
all_state_base.collect(item, True) (5 if (item.player, item.name) in dungeon_specific else 0))
item.advancement = True for item in in_dungeon_items:
all_state_base.remove(item)
dungeon_items = [item for item in get_dungeon_item_pool(world) if (((item.smallkey and not world.keyshuffle[item.player]) fill_restrictive(world, all_state_base, locations, in_dungeon_items, True, True)
or (item.bigkey and not world.bigkeyshuffle[item.player])
or (item.map and not world.mapshuffle[item.player])
or (item.compass and not world.compassshuffle[item.player])
) and world.goal[item.player] != 'icerodhunt')] #
if dungeon_items:
# sort in the order Big Key, Small Key, Other before placing dungeon items
sort_order = {"BigKey": 3, "SmallKey": 2}
dungeon_items.sort(key=lambda item: sort_order.get(item.type, 1))
fill_restrictive(world, all_state_base, locations, dungeon_items, True, True)
dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A], dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A],
@@ -154,7 +168,8 @@ dungeon_music_addresses = {'Eastern Palace - Prize': [0x1559A],
'Palace of Darkness - Prize': [0x155B8], 'Palace of Darkness - Prize': [0x155B8],
'Swamp Palace - Prize': [0x155B7], 'Swamp Palace - Prize': [0x155B7],
'Thieves\' Town - Prize': [0x155C6], 'Thieves\' Town - Prize': [0x155C6],
'Skull Woods - Prize': [0x155BA, 0x155BB, 0x155BC, 0x155BD, 0x15608, 0x15609, 0x1560A, 0x1560B], 'Skull Woods - Prize': [0x155BA, 0x155BB, 0x155BC, 0x155BD, 0x15608, 0x15609, 0x1560A,
0x1560B],
'Ice Palace - Prize': [0x155BF], 'Ice Palace - Prize': [0x155BF],
'Misery Mire - Prize': [0x155B9], 'Misery Mire - Prize': [0x155B9],
'Turtle Rock - Prize': [0x155C7, 0x155A7, 0x155AA, 0x155AB]} 'Turtle Rock - Prize': [0x155C7, 0x155A7, 0x155AA, 0x155AB]}

View File

@@ -143,20 +143,7 @@ def parse_arguments(argv, no_defaults=False):
off. off.
Off: Dungeon counters are never shown. Off: Dungeon counters are never shown.
''') ''')
parser.add_argument('--progressive', default=defval('on'), const='normal', nargs='?', choices=['on', 'off', 'random'],
help='''\
Select progressive equipment setting. Affects available itempool. (default: %(default)s)
On: Swords, Shields, Armor, and Gloves will
all be progressive equipment. Each subsequent
item of the same type the player finds will
upgrade that piece of equipment by one stage.
Off: Swords, Shields, Armor, and Gloves will not
be progressive equipment. Higher level items may
be found at any time. Downgrades are not possible.
Random: Swords, Shields, Armor, and Gloves will, per
category, be randomly progressive or not.
Link will die in one hit.
''')
parser.add_argument('--algorithm', default=defval('balanced'), const='balanced', nargs='?', parser.add_argument('--algorithm', default=defval('balanced'), const='balanced', nargs='?',
choices=['freshness', 'flood', 'vt25', 'vt26', 'balanced'], choices=['freshness', 'flood', 'vt25', 'vt26', 'balanced'],
help='''\ help='''\
@@ -218,42 +205,11 @@ def parse_arguments(argv, no_defaults=False):
--seed given will produce the same 10 (different) roms each --seed given will produce the same 10 (different) roms each
time). time).
''', type=int) ''', type=int)
parser.add_argument('--fastmenu', default=defval('normal'), const='normal', nargs='?',
choices=['normal', 'instant', 'double', 'triple', 'quadruple', 'half'],
help='''\
Select the rate at which the menu opens and closes.
(default: %(default)s)
''')
parser.add_argument('--quickswap', help='Enable quick item swapping with L and R.', action='store_true')
parser.add_argument('--disablemusic', help='Disables game music.', action='store_true')
parser.add_argument('--triforcehud', default='hide_goal', const='hide_goal', nargs='?', choices=['normal', 'hide_goal', 'hide_required', 'hide_both'],
help='''\
Hide the triforce hud in certain circumstances.
hide_goal will hide the hud until finding a triforce piece, hide_required will hide the total amount needed to win
(Both can be revealed when speaking to Murahalda)
(default: %(default)s)
''')
parser.add_argument('--enableflashing', help='Reenable flashing animations (unfriendly to epilepsy, always disabled in race roms)', action='store_false', dest="reduceflashing")
parser.add_argument('--mapshuffle', default=defval(False),
help='Maps are no longer restricted to their dungeons, but can be anywhere',
action='store_true')
parser.add_argument('--compassshuffle', default=defval(False),
help='Compasses are no longer restricted to their dungeons, but can be anywhere',
action='store_true')
parser.add_argument('--keyshuffle', default=defval("off"), help='\
on: Small Keys are no longer restricted to their dungeons, but can be anywhere.\
universal: Makes all Small Keys usable in any dungeon and places shops to buy more keys.',
choices=["on", "universal", "off"])
parser.add_argument('--bigkeyshuffle', default=defval(False),
help='Big Keys are no longer restricted to their dungeons, but can be anywhere',
action='store_true')
parser.add_argument('--keysanity', default=defval(False), help=argparse.SUPPRESS, action='store_true')
parser.add_argument('--retro', default=defval(False), help='''\ parser.add_argument('--retro', default=defval(False), help='''\
Keys are universal, shooting arrows costs rupees, Keys are universal, shooting arrows costs rupees,
and a few other little things make this more like Zelda-1. and a few other little things make this more like Zelda-1.
''', action='store_true') ''', action='store_true')
parser.add_argument('--startinventory', default=defval(''),
help='Specifies a list of items that will be in your starting inventory (separated by commas)')
parser.add_argument('--local_items', default=defval(''), parser.add_argument('--local_items', default=defval(''),
help='Specifies a list of items that will not spread across the multiworld (separated by commas)') help='Specifies a list of items that will not spread across the multiworld (separated by commas)')
parser.add_argument('--non_local_items', default=defval(''), parser.add_argument('--non_local_items', default=defval(''),
@@ -276,19 +232,6 @@ def parse_arguments(argv, no_defaults=False):
If set, the Pyramid Hole and Ganon's Tower are not If set, the Pyramid Hole and Ganon's Tower are not
included entrance shuffle pool. included entrance shuffle pool.
''', action='store_false', dest='shuffleganon') ''', action='store_false', dest='shuffleganon')
parser.add_argument('--heartbeep', default=defval('normal'), const='normal', nargs='?', choices=['double', 'normal', 'half', 'quarter', 'off'],
help='''\
Select the rate at which the heart beep sound is played at
low health. (default: %(default)s)
''')
parser.add_argument('--heartcolor', default=defval('red'), const='red', nargs='?', choices=['red', 'blue', 'green', 'yellow', 'random'],
help='Select the color of Link\'s heart meter. (default: %(default)s)')
parser.add_argument('--ow_palettes', default=defval('default'), choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
parser.add_argument('--uw_palettes', default=defval('default'), choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
parser.add_argument('--hud_palettes', default=defval('default'), choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
parser.add_argument('--shield_palettes', default=defval('default'), choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
parser.add_argument('--sword_palettes', default=defval('default'), choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
parser.add_argument('--link_palettes', default=defval('default'), choices=['default', 'random', 'blackout','puke','classic','grayscale','negative','dizzy','sick'])
parser.add_argument('--sprite', help='''\ parser.add_argument('--sprite', help='''\
Path to a sprite sheet to use for Link. Needs to be in Path to a sprite sheet to use for Link. Needs to be in
@@ -332,13 +275,10 @@ def parse_arguments(argv, no_defaults=False):
parser.add_argument('--restrict_dungeon_item_on_boss', default=defval(False), action="store_true") parser.add_argument('--restrict_dungeon_item_on_boss', default=defval(False), action="store_true")
parser.add_argument('--multi', default=defval(1), type=lambda value: min(max(int(value), 1), 255)) parser.add_argument('--multi', default=defval(1), type=lambda value: min(max(int(value), 1), 255))
parser.add_argument('--names', default=defval('')) parser.add_argument('--names', default=defval(''))
parser.add_argument('--teams', default=defval(1), type=lambda value: max(int(value), 1))
parser.add_argument('--outputpath') parser.add_argument('--outputpath')
parser.add_argument('--game', default="A Link to the Past") parser.add_argument('--game', default="A Link to the Past")
parser.add_argument('--race', default=defval(False), action='store_true') parser.add_argument('--race', default=defval(False), action='store_true')
parser.add_argument('--outputname') parser.add_argument('--outputname')
parser.add_argument('--disable_glitch_boots', default=defval(False), action='store_true', help='''\
turns off starting with Pegasus Boots in glitched modes.''')
parser.add_argument('--start_hints') parser.add_argument('--start_hints')
if multiargs.multi: if multiargs.multi:
for player in range(1, multiargs.multi + 1): for player in range(1, multiargs.multi + 1):
@@ -355,7 +295,6 @@ def parse_arguments(argv, no_defaults=False):
ret.plando_connections = [] ret.plando_connections = []
ret.er_seeds = {} ret.er_seeds = {}
ret.glitch_boots = not ret.disable_glitch_boots
if ret.timer == "none": if ret.timer == "none":
ret.timer = False ret.timer = False
if ret.dungeon_counters == 'on': if ret.dungeon_counters == 'on':
@@ -363,12 +302,6 @@ def parse_arguments(argv, no_defaults=False):
elif ret.dungeon_counters == 'off': elif ret.dungeon_counters == 'off':
ret.dungeon_counters = False ret.dungeon_counters = False
if ret.keysanity:
ret.mapshuffle = ret.compassshuffle = ret.keyshuffle = ret.bigkeyshuffle = True
elif ret.keyshuffle == "on":
ret.keyshuffle = True
elif ret.keyshuffle == "off":
ret.keyshuffle = False
if multiargs.multi: if multiargs.multi:
defaults = copy.deepcopy(ret) defaults = copy.deepcopy(ret)
for player in range(1, multiargs.multi + 1): for player in range(1, multiargs.multi + 1):
@@ -377,18 +310,16 @@ def parse_arguments(argv, no_defaults=False):
for name in ['logic', 'mode', 'swordless', 'goal', 'difficulty', 'item_functionality', for name in ['logic', 'mode', 'swordless', 'goal', 'difficulty', 'item_functionality',
'shuffle', 'open_pyramid', 'timer', 'shuffle', 'open_pyramid', 'timer',
'countdown_start_time', 'red_clock_time', 'blue_clock_time', 'green_clock_time', 'countdown_start_time', 'red_clock_time', 'blue_clock_time', 'green_clock_time',
'mapshuffle', 'compassshuffle', 'keyshuffle', 'bigkeyshuffle', 'startinventory',
'local_items', 'non_local_items', 'retro', 'accessibility', 'hints', 'beemizer', 'local_items', 'non_local_items', 'retro', 'accessibility', 'hints', 'beemizer',
'shufflebosses', 'enemy_shuffle', 'enemy_health', 'enemy_damage', 'shufflepots', 'shufflebosses', 'enemy_shuffle', 'enemy_health', 'enemy_damage', 'shufflepots',
'ow_palettes', 'uw_palettes', 'sprite', 'disablemusic', 'quickswap', 'fastmenu', 'heartcolor', 'sprite',
'heartbeep', "progression_balancing", "triforce_pieces_available", "progression_balancing", "triforce_pieces_available",
"triforce_pieces_required", "shop_shuffle", "triforce_pieces_required", "shop_shuffle",
"required_medallions", "start_hints", "required_medallions", "start_hints",
"plando_items", "plando_texts", "plando_connections", "er_seeds", "plando_items", "plando_texts", "plando_connections", "er_seeds",
'progressive', 'dungeon_counters', 'glitch_boots', 'killable_thieves', 'dungeon_counters', 'killable_thieves',
'tile_shuffle', 'bush_shuffle', 'shuffle_prizes', 'sprite_pool', 'dark_room_logic', 'tile_shuffle', 'bush_shuffle', 'shuffle_prizes', 'sprite_pool', 'dark_room_logic',
'restrict_dungeon_item_on_boss', 'reduceflashing', 'game', 'restrict_dungeon_item_on_boss', 'game']:
'hud_palettes', 'sword_palettes', 'shield_palettes', 'link_palettes', 'triforcehud']:
value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name) value = getattr(defaults, name) if getattr(playerargs, name) is None else getattr(playerargs, name)
if player == 1: if player == 1:
setattr(ret, name, {1: value}) setattr(ret, name, {1: value})

View File

@@ -1,5 +1,6 @@
# ToDo: With shuffle_ganon option, prevent gtower from linking to an exit only location through a 2 entrance cave. # ToDo: With shuffle_ganon option, prevent gtower from linking to an exit only location through a 2 entrance cave.
from collections import defaultdict from collections import defaultdict
from worlds.alttp.OverworldGlitchRules import overworld_glitch_connections
from worlds.alttp.UnderworldGlitchRules import underworld_glitch_connections from worlds.alttp.UnderworldGlitchRules import underworld_glitch_connections
def link_entrances(world, player): def link_entrances(world, player):
@@ -1064,11 +1065,13 @@ def link_entrances(world, player):
connect_doors(world, single_doors, door_targets, player) connect_doors(world, single_doors, door_targets, player)
else: else:
raise NotImplementedError( raise NotImplementedError(
f'{world.shuffle[player]} Shuffling not supported yet. Player {world.get_player_names(player)}') f'{world.shuffle[player]} Shuffling not supported yet. Player {world.get_player_name(player)}')
# mandatory hybrid major glitches connections if world.logic[player] in ['owglitches', 'hybridglitches', 'nologic']:
if world.logic[player] in ['hybridglitches', 'nologic']: overworld_glitch_connections(world, player)
underworld_glitch_connections(world, player) # mandatory hybrid major glitches connections
if world.logic[player] in ['hybridglitches', 'nologic']:
underworld_glitch_connections(world, player)
# check for swamp palace fix # check for swamp palace fix
if world.get_entrance('Dam', player).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)': if world.get_entrance('Dam', player).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)':
@@ -1771,9 +1774,11 @@ def link_inverted_entrances(world, player):
else: else:
raise NotImplementedError('Shuffling not supported yet') raise NotImplementedError('Shuffling not supported yet')
# mandatory hybrid major glitches connections if world.logic[player] in ['owglitches', 'hybridglitches', 'nologic']:
if world.logic[player] in ['hybridglitches', 'nologic']: overworld_glitch_connections(world, player)
underworld_glitch_connections(world, player) # mandatory hybrid major glitches connections
if world.logic[player] in ['hybridglitches', 'nologic']:
underworld_glitch_connections(world, player)
# patch swamp drain # patch swamp drain
if world.get_entrance('Dam', player).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)': if world.get_entrance('Dam', player).connected_region.name != 'Dam' or world.get_entrance('Swamp Palace', player).connected_region.name != 'Swamp Palace (Entrance)':

View File

@@ -5,10 +5,11 @@ from BaseClasses import Region, RegionType
from worlds.alttp.SubClasses import ALttPLocation from worlds.alttp.SubClasses import ALttPLocation
from worlds.alttp.Shops import TakeAny, total_shop_slots, set_up_shops, shuffle_shops from worlds.alttp.Shops import TakeAny, total_shop_slots, set_up_shops, shuffle_shops
from worlds.alttp.Bosses import place_bosses from worlds.alttp.Bosses import place_bosses
from worlds.alttp.Dungeons import get_dungeon_item_pool from worlds.alttp.Dungeons import get_dungeon_item_pool_player
from worlds.alttp.EntranceShuffle import connect_entrance from worlds.alttp.EntranceShuffle import connect_entrance
from Fill import FillError, fill_restrictive from Fill import FillError
from worlds.alttp.Items import ItemFactory, GetBeemizerItem from worlds.alttp.Items import ItemFactory, GetBeemizerItem
from worlds.alttp.Options import smallkey_shuffle
# This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space. # This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space.
# Some basic items that various modes require are placed here, including pendants and crystals. Medallion requirements for the two relevant entrances are also decided. # Some basic items that various modes require are placed here, including pendants and crystals. Medallion requirements for the two relevant entrances are also decided.
@@ -274,7 +275,7 @@ def generate_itempool(world):
itempool.extend(['Rupees (300)'] * 34) itempool.extend(['Rupees (300)'] * 34)
itempool.extend(['Bombs (10)'] * 5) itempool.extend(['Bombs (10)'] * 5)
itempool.extend(['Arrows (10)'] * 7) itempool.extend(['Arrows (10)'] * 7)
if world.keyshuffle[player] == 'universal': if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
itempool.extend(itemdiff.universal_keys) itempool.extend(itemdiff.universal_keys)
itempool.append('Small Key (Universal)') itempool.append('Small Key (Universal)')
@@ -362,12 +363,8 @@ def generate_itempool(world):
if treasure_hunt_icon is not None: if treasure_hunt_icon is not None:
world.treasure_hunt_icon[player] = treasure_hunt_icon world.treasure_hunt_icon[player] = treasure_hunt_icon
dungeon_items = [item for item in get_dungeon_item_pool(world) if item.player == player dungeon_items = [item for item in get_dungeon_item_pool_player(world, player)
and ((item.smallkey and world.keyshuffle[player]) if item.name not in world.worlds[player].dungeon_local_item_names]
or (item.bigkey and world.bigkeyshuffle[player])
or (item.map and world.mapshuffle[player])
or (item.compass and world.compassshuffle[player])
or world.goal[player] == 'icerodhunt')]
if world.goal[player] == 'icerodhunt': if world.goal[player] == 'icerodhunt':
for item in dungeon_items: for item in dungeon_items:
@@ -399,7 +396,7 @@ def generate_itempool(world):
if additional_triforce_pieces: if additional_triforce_pieces:
if additional_triforce_pieces > len(nonprogressionitems): if additional_triforce_pieces > len(nonprogressionitems):
raise FillError(f"Not enough non-progression items to replace with Triforce pieces found for player " raise FillError(f"Not enough non-progression items to replace with Triforce pieces found for player "
f"{world.get_player_names(player)}.") f"{world.get_player_name(player)}.")
progressionitems += [ItemFactory("Triforce Piece", player)] * additional_triforce_pieces progressionitems += [ItemFactory("Triforce Piece", player)] * additional_triforce_pieces
nonprogressionitems.sort(key=lambda item: int("Heart" in item.name)) # try to keep hearts in the pool nonprogressionitems.sort(key=lambda item: int("Heart" in item.name)) # try to keep hearts in the pool
nonprogressionitems = nonprogressionitems[additional_triforce_pieces:] nonprogressionitems = nonprogressionitems[additional_triforce_pieces:]
@@ -492,6 +489,7 @@ def set_up_take_anys(world, player):
world.initialize_regions() world.initialize_regions()
def create_dynamic_shop_locations(world, player): def create_dynamic_shop_locations(world, player):
for shop in world.shops: for shop in world.shops:
if shop.region.player == player: if shop.region.player == player:
@@ -499,47 +497,19 @@ def create_dynamic_shop_locations(world, player):
if item is None: if item is None:
continue continue
if item['create_location']: if item['create_location']:
loc = ALttPLocation(player, "{} Slot {}".format(shop.region.name, i + 1), parent=shop.region) loc = ALttPLocation(player, f"{shop.region.name} {shop.slot_names[i]}", parent=shop.region)
shop.region.locations.append(loc) shop.region.locations.append(loc)
world.dynamic_locations.append(loc) world.dynamic_locations.append(loc)
world.clear_location_cache() world.clear_location_cache()
world.push_item(loc, ItemFactory(item['item'], player), False) world.push_item(loc, ItemFactory(item['item'], player), False)
loc.shop_slot = True loc.shop_slot = i
loc.event = True loc.event = True
loc.locked = True loc.locked = True
def fill_prizes(world, attempts=15):
all_state = world.get_all_state(keys=True)
for player in world.get_game_players("A Link to the Past"):
crystals = ItemFactory(['Red Pendant', 'Blue Pendant', 'Green Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 7', 'Crystal 5', 'Crystal 6'], player)
crystal_locations = [world.get_location('Turtle Rock - Prize', player), world.get_location('Eastern Palace - Prize', player), world.get_location('Desert Palace - Prize', player), world.get_location('Tower of Hera - Prize', player), world.get_location('Palace of Darkness - Prize', player),
world.get_location('Thieves\' Town - Prize', player), world.get_location('Skull Woods - Prize', player), world.get_location('Swamp Palace - Prize', player), world.get_location('Ice Palace - Prize', player),
world.get_location('Misery Mire - Prize', player)]
placed_prizes = {loc.item.name for loc in crystal_locations if loc.item}
unplaced_prizes = [crystal for crystal in crystals if crystal.name not in placed_prizes]
empty_crystal_locations = [loc for loc in crystal_locations if not loc.item]
for attempt in range(attempts):
try:
prizepool = unplaced_prizes.copy()
prize_locs = empty_crystal_locations.copy()
world.random.shuffle(prize_locs)
fill_restrictive(world, all_state, prize_locs, prizepool, True, lock=True)
except FillError as e:
logging.getLogger('').exception("Failed to place dungeon prizes (%s). Will retry %s more times", e,
attempts - attempt)
for location in empty_crystal_locations:
location.item = None
continue
break
else:
raise FillError('Unable to place dungeon prizes')
def get_pool_core(world, player: int): def get_pool_core(world, player: int):
progressive = world.progressive[player]
shuffle = world.shuffle[player] shuffle = world.shuffle[player]
difficulty = world.difficulty[player] difficulty = world.difficulty[player]
timer = world.timer[player] timer = world.timer[player]
@@ -563,16 +533,14 @@ def get_pool_core(world, player: int):
assert loc not in placed_items assert loc not in placed_items
placed_items[loc] = item placed_items[loc] = item
def want_progressives():
return world.random.choice([True, False]) if progressive == 'random' else progressive == 'on'
# provide boots to major glitch dependent seeds # provide boots to major glitch dependent seeds
if logic in {'owglitches', 'hybridglitches', 'nologic'} and world.glitch_boots[player] and goal != 'icerodhunt': if logic in {'owglitches', 'hybridglitches', 'nologic'} and world.glitch_boots[player] and goal != 'icerodhunt':
precollected_items.append('Pegasus Boots') precollected_items.append('Pegasus Boots')
pool.remove('Pegasus Boots') pool.remove('Pegasus Boots')
pool.append('Rupees (20)') pool.append('Rupees (20)')
want_progressives = world.progressive[player].want_progressives
if want_progressives(): if want_progressives(world.random):
pool.extend(diff.progressiveglove) pool.extend(diff.progressiveglove)
else: else:
pool.extend(diff.basicglove) pool.extend(diff.basicglove)
@@ -599,22 +567,22 @@ def get_pool_core(world, player: int):
thisbottle = world.random.choice(diff.bottles) thisbottle = world.random.choice(diff.bottles)
pool.append(thisbottle) pool.append(thisbottle)
if want_progressives(): if want_progressives(world.random):
pool.extend(diff.progressiveshield) pool.extend(diff.progressiveshield)
else: else:
pool.extend(diff.basicshield) pool.extend(diff.basicshield)
if want_progressives(): if want_progressives(world.random):
pool.extend(diff.progressivearmor) pool.extend(diff.progressivearmor)
else: else:
pool.extend(diff.basicarmor) pool.extend(diff.basicarmor)
if want_progressives(): if want_progressives(world.random):
pool.extend(diff.progressivemagic) pool.extend(diff.progressivemagic)
else: else:
pool.extend(diff.basicmagic) pool.extend(diff.basicmagic)
if want_progressives(): if want_progressives(world.random):
pool.extend(diff.progressivebow) pool.extend(diff.progressivebow)
elif (swordless or logic == 'noglitches') and goal != 'icerodhunt': elif (swordless or logic == 'noglitches') and goal != 'icerodhunt':
swordless_bows = ['Bow', 'Silver Bow'] swordless_bows = ['Bow', 'Silver Bow']
@@ -627,7 +595,7 @@ def get_pool_core(world, player: int):
if swordless: if swordless:
pool.extend(diff.swordless) pool.extend(diff.swordless)
else: else:
progressive_swords = want_progressives() progressive_swords = want_progressives(world.random)
pool.extend(diff.progressivesword if progressive_swords else diff.basicsword) pool.extend(diff.progressivesword if progressive_swords else diff.basicsword)
extraitems = total_items_to_place - len(pool) - len(placed_items) extraitems = total_items_to_place - len(pool) - len(placed_items)
@@ -666,7 +634,7 @@ def get_pool_core(world, player: int):
if retro: if retro:
replace = {'Single Arrow', 'Arrows (10)', 'Arrow Upgrade (+5)', 'Arrow Upgrade (+10)'} replace = {'Single Arrow', 'Arrows (10)', 'Arrow Upgrade (+5)', 'Arrow Upgrade (+10)'}
pool = ['Rupees (5)' if item in replace else item for item in pool] pool = ['Rupees (5)' if item in replace else item for item in pool]
if world.keyshuffle[player] == "universal": if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
pool.extend(diff.universal_keys) pool.extend(diff.universal_keys)
item_to_place = 'Small Key (Universal)' if goal != 'icerodhunt' else 'Nothing' item_to_place = 'Small Key (Universal)' if goal != 'icerodhunt' else 'Nothing'
if mode == 'standard': if mode == 'standard':
@@ -803,7 +771,7 @@ def make_custom_item_pool(world, player):
itemtotal = itemtotal + 1 itemtotal = itemtotal + 1
if mode == 'standard': if mode == 'standard':
if world.keyshuffle[player] == "universal": if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
key_location = world.random.choice( key_location = world.random.choice(
['Secret Passage', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest', ['Secret Passage', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest',
'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross']) 'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross'])
@@ -826,7 +794,7 @@ def make_custom_item_pool(world, player):
pool.extend(['Magic Mirror'] * customitemarray[22]) pool.extend(['Magic Mirror'] * customitemarray[22])
pool.extend(['Moon Pearl'] * customitemarray[28]) pool.extend(['Moon Pearl'] * customitemarray[28])
if world.keyshuffle == "universal": if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
itemtotal = itemtotal - 28 # Corrects for small keys not being in item pool in Retro Mode itemtotal = itemtotal - 28 # Corrects for small keys not being in item pool in Retro Mode
if itemtotal < total_items_to_place: if itemtotal < total_items_to_place:
pool.extend(['Nothing'] * (total_items_to_place - itemtotal)) pool.extend(['Nothing'] * (total_items_to_place - itemtotal))

View File

@@ -136,58 +136,58 @@ item_table = {'Bow': ItemData(True, None, 0x0B, 'You have\nchosen the\narcher cl
'Multi RNG': ItemData(False, None, 0x63, 'something you may already have', None, None, None, None, 'unknown boy somethings again', 'a total mystery'), 'Multi RNG': ItemData(False, None, 0x63, 'something you may already have', None, None, None, None, 'unknown boy somethings again', 'a total mystery'),
'Magic Upgrade (1/2)': ItemData(True, None, 0x4E, 'Your magic\npower has been\ndoubled!', 'and the spell power', 'the magic-saving kid', 'wizardry for sale', 'mekalekahi mekahiney ho', 'magic boy saves magic again', 'Half Magic'), # can be required to beat mothula in an open seed in very very rare circumstance 'Magic Upgrade (1/2)': ItemData(True, None, 0x4E, 'Your magic\npower has been\ndoubled!', 'and the spell power', 'the magic-saving kid', 'wizardry for sale', 'mekalekahi mekahiney ho', 'magic boy saves magic again', 'Half Magic'), # can be required to beat mothula in an open seed in very very rare circumstance
'Magic Upgrade (1/4)': ItemData(True, None, 0x4F, 'Your magic\npower has been\nquadrupled!', 'and the spell power', 'the magic-saving kid', 'wizardry for sale', 'mekalekahi mekahiney ho', 'magic boy saves magic again', 'Quarter Magic'), # can be required to beat mothula in an open seed in very very rare circumstance 'Magic Upgrade (1/4)': ItemData(True, None, 0x4F, 'Your magic\npower has been\nquadrupled!', 'and the spell power', 'the magic-saving kid', 'wizardry for sale', 'mekalekahi mekahiney ho', 'magic boy saves magic again', 'Quarter Magic'), # can be required to beat mothula in an open seed in very very rare circumstance
'Small Key (Eastern Palace)': ItemData(False, 'SmallKey', 0xA2, 'A small key to Armos Knights', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Eastern Palace'), 'Small Key (Eastern Palace)': ItemData(True, 'SmallKey', 0xA2, 'A small key to Armos Knights', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Eastern Palace'),
'Big Key (Eastern Palace)': ItemData(False, 'BigKey', 0x9D, 'A big key to Armos Knights', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Eastern Palace'), 'Big Key (Eastern Palace)': ItemData(True, 'BigKey', 0x9D, 'A big key to Armos Knights', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Eastern Palace'),
'Compass (Eastern Palace)': ItemData(False, 'Compass', 0x8D, 'Now you can find the Armos Knights!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Eastern Palace'), 'Compass (Eastern Palace)': ItemData(False, 'Compass', 0x8D, 'Now you can find the Armos Knights!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Eastern Palace'),
'Map (Eastern Palace)': ItemData(False, 'Map', 0x7D, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Eastern Palace'), 'Map (Eastern Palace)': ItemData(False, 'Map', 0x7D, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Eastern Palace'),
'Small Key (Desert Palace)': ItemData(False, 'SmallKey', 0xA3, 'A small key to the desert', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Desert Palace'), 'Small Key (Desert Palace)': ItemData(True, 'SmallKey', 0xA3, 'A small key to the desert', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Desert Palace'),
'Big Key (Desert Palace)': ItemData(False, 'BigKey', 0x9C, 'A big key to the desert', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Desert Palace'), 'Big Key (Desert Palace)': ItemData(True, 'BigKey', 0x9C, 'A big key to the desert', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Desert Palace'),
'Compass (Desert Palace)': ItemData(False, 'Compass', 0x8C, 'Now you can find Lanmolas!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Desert Palace'), 'Compass (Desert Palace)': ItemData(False, 'Compass', 0x8C, 'Now you can find Lanmolas!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Desert Palace'),
'Map (Desert Palace)': ItemData(False, 'Map', 0x7C, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Desert Palace'), 'Map (Desert Palace)': ItemData(False, 'Map', 0x7C, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Desert Palace'),
'Small Key (Tower of Hera)': ItemData(False, 'SmallKey', 0xAA, 'A small key to Hera', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Tower of Hera'), 'Small Key (Tower of Hera)': ItemData(True, 'SmallKey', 0xAA, 'A small key to Hera', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Tower of Hera'),
'Big Key (Tower of Hera)': ItemData(False, 'BigKey', 0x95, 'A big key to Hera', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Tower of Hera'), 'Big Key (Tower of Hera)': ItemData(True, 'BigKey', 0x95, 'A big key to Hera', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Tower of Hera'),
'Compass (Tower of Hera)': ItemData(False, 'Compass', 0x85, 'Now you can find Moldorm!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Tower of Hera'), 'Compass (Tower of Hera)': ItemData(False, 'Compass', 0x85, 'Now you can find Moldorm!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Tower of Hera'),
'Map (Tower of Hera)': ItemData(False, 'Map', 0x75, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Tower of Hera'), 'Map (Tower of Hera)': ItemData(False, 'Map', 0x75, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Tower of Hera'),
'Small Key (Hyrule Castle)': ItemData(False, 'SmallKey', 0xA0, 'A small key to the castle', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Hyrule Castle'), 'Small Key (Hyrule Castle)': ItemData(True, 'SmallKey', 0xA0, 'A small key to the castle', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Hyrule Castle'),
'Big Key (Hyrule Castle)': ItemData(False, 'BigKey', 0x9F, 'A big key to the castle', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Hyrule Castle'), 'Big Key (Hyrule Castle)': ItemData(True, 'BigKey', 0x9F, 'A big key to the castle', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Hyrule Castle'),
'Compass (Hyrule Castle)': ItemData(False, 'Compass', 0x8F, 'Now you can find no boss!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Hyrule Castle'), 'Compass (Hyrule Castle)': ItemData(False, 'Compass', 0x8F, 'Now you can find no boss!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Hyrule Castle'),
'Map (Hyrule Castle)': ItemData(False, 'Map', 0x7F, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Hyrule Castle'), 'Map (Hyrule Castle)': ItemData(False, 'Map', 0x7F, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Hyrule Castle'),
'Small Key (Agahnims Tower)': ItemData(False, 'SmallKey', 0xA4, 'A small key to Agahnim', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Castle Tower'), 'Small Key (Agahnims Tower)': ItemData(True, 'SmallKey', 0xA4, 'A small key to Agahnim', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Castle Tower'),
# doors-specific items, baserom will not be able to understand these # doors-specific items, baserom will not be able to understand these
'Big Key (Agahnims Tower)': ItemData(False, 'BigKey', 0x9B, 'A big key to Agahnim', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Castle Tower'), 'Big Key (Agahnims Tower)': ItemData(True, 'BigKey', 0x9B, 'A big key to Agahnim', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Castle Tower'),
'Compass (Agahnims Tower)': ItemData(False, 'Compass', 0x8B, 'Now you can find Aga1!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds null again', 'a compass to Castle Tower'), 'Compass (Agahnims Tower)': ItemData(False, 'Compass', 0x8B, 'Now you can find Aga1!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds null again', 'a compass to Castle Tower'),
'Map (Agahnims Tower)': ItemData(False, 'Map', 0x7B, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Castle Tower'), 'Map (Agahnims Tower)': ItemData(False, 'Map', 0x7B, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Castle Tower'),
# end of doors-specific items # end of doors-specific items
'Small Key (Palace of Darkness)': ItemData(False, 'SmallKey', 0xA6, 'A small key to darkness', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Palace of Darkness'), 'Small Key (Palace of Darkness)': ItemData(True, 'SmallKey', 0xA6, 'A small key to darkness', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Palace of Darkness'),
'Big Key (Palace of Darkness)': ItemData(False, 'BigKey', 0x99, 'A big key to darkness', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Palace of Darkness'), 'Big Key (Palace of Darkness)': ItemData(True, 'BigKey', 0x99, 'A big key to darkness', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Palace of Darkness'),
'Compass (Palace of Darkness)': ItemData(False, 'Compass', 0x89, 'Now you can find Helmasaur King!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Palace of Darkness'), 'Compass (Palace of Darkness)': ItemData(False, 'Compass', 0x89, 'Now you can find Helmasaur King!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Palace of Darkness'),
'Map (Palace of Darkness)': ItemData(False, 'Map', 0x79, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Palace of Darkness'), 'Map (Palace of Darkness)': ItemData(False, 'Map', 0x79, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Palace of Darkness'),
'Small Key (Thieves Town)': ItemData(False, 'SmallKey', 0xAB, 'A small key to thievery', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Thieves\' Town'), 'Small Key (Thieves Town)': ItemData(True, 'SmallKey', 0xAB, 'A small key to thievery', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Thieves\' Town'),
'Big Key (Thieves Town)': ItemData(False, 'BigKey', 0x94, 'A big key to thievery', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Thieves\' Town'), 'Big Key (Thieves Town)': ItemData(True, 'BigKey', 0x94, 'A big key to thievery', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Thieves\' Town'),
'Compass (Thieves Town)': ItemData(False, 'Compass', 0x84, 'Now you can find Blind!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Thieves\' Town'), 'Compass (Thieves Town)': ItemData(False, 'Compass', 0x84, 'Now you can find Blind!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Thieves\' Town'),
'Map (Thieves Town)': ItemData(False, 'Map', 0x74, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Thieves\' Town'), 'Map (Thieves Town)': ItemData(False, 'Map', 0x74, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Thieves\' Town'),
'Small Key (Skull Woods)': ItemData(False, 'SmallKey', 0xA8, 'A small key to the woods', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Skull Woods'), 'Small Key (Skull Woods)': ItemData(True, 'SmallKey', 0xA8, 'A small key to the woods', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Skull Woods'),
'Big Key (Skull Woods)': ItemData(False, 'BigKey', 0x97, 'A big key to the woods', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Skull Woods'), 'Big Key (Skull Woods)': ItemData(True, 'BigKey', 0x97, 'A big key to the woods', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Skull Woods'),
'Compass (Skull Woods)': ItemData(False, 'Compass', 0x87, 'Now you can find Mothula!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Skull Woods'), 'Compass (Skull Woods)': ItemData(False, 'Compass', 0x87, 'Now you can find Mothula!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Skull Woods'),
'Map (Skull Woods)': ItemData(False, 'Map', 0x77, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Skull Woods'), 'Map (Skull Woods)': ItemData(False, 'Map', 0x77, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Skull Woods'),
'Small Key (Swamp Palace)': ItemData(False, 'SmallKey', 0xA5, 'A small key to the swamp', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Swamp Palace'), 'Small Key (Swamp Palace)': ItemData(True, 'SmallKey', 0xA5, 'A small key to the swamp', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Swamp Palace'),
'Big Key (Swamp Palace)': ItemData(False, 'BigKey', 0x9A, 'A big key to the swamp', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Swamp Palace'), 'Big Key (Swamp Palace)': ItemData(True, 'BigKey', 0x9A, 'A big key to the swamp', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Swamp Palace'),
'Compass (Swamp Palace)': ItemData(False, 'Compass', 0x8A, 'Now you can find Arrghus!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Swamp Palace'), 'Compass (Swamp Palace)': ItemData(False, 'Compass', 0x8A, 'Now you can find Arrghus!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Swamp Palace'),
'Map (Swamp Palace)': ItemData(False, 'Map', 0x7A, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Swamp Palace'), 'Map (Swamp Palace)': ItemData(False, 'Map', 0x7A, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Swamp Palace'),
'Small Key (Ice Palace)': ItemData(False, 'SmallKey', 0xA9, 'A small key to the iceberg', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Ice Palace'), 'Small Key (Ice Palace)': ItemData(True, 'SmallKey', 0xA9, 'A small key to the iceberg', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Ice Palace'),
'Big Key (Ice Palace)': ItemData(False, 'BigKey', 0x96, 'A big key to the iceberg', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Ice Palace'), 'Big Key (Ice Palace)': ItemData(True, 'BigKey', 0x96, 'A big key to the iceberg', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Ice Palace'),
'Compass (Ice Palace)': ItemData(False, 'Compass', 0x86, 'Now you can find Kholdstare!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Ice Palace'), 'Compass (Ice Palace)': ItemData(False, 'Compass', 0x86, 'Now you can find Kholdstare!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Ice Palace'),
'Map (Ice Palace)': ItemData(False, 'Map', 0x76, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Ice Palace'), 'Map (Ice Palace)': ItemData(False, 'Map', 0x76, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Ice Palace'),
'Small Key (Misery Mire)': ItemData(False, 'SmallKey', 0xA7, 'A small key to the mire', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Misery Mire'), 'Small Key (Misery Mire)': ItemData(True, 'SmallKey', 0xA7, 'A small key to the mire', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Misery Mire'),
'Big Key (Misery Mire)': ItemData(False, 'BigKey', 0x98, 'A big key to the mire', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Misery Mire'), 'Big Key (Misery Mire)': ItemData(True, 'BigKey', 0x98, 'A big key to the mire', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Misery Mire'),
'Compass (Misery Mire)': ItemData(False, 'Compass', 0x88, 'Now you can find Vitreous!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Misery Mire'), 'Compass (Misery Mire)': ItemData(False, 'Compass', 0x88, 'Now you can find Vitreous!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Misery Mire'),
'Map (Misery Mire)': ItemData(False, 'Map', 0x78, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Misery Mire'), 'Map (Misery Mire)': ItemData(False, 'Map', 0x78, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Misery Mire'),
'Small Key (Turtle Rock)': ItemData(False, 'SmallKey', 0xAC, 'A small key to the pipe maze', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Turtle Rock'), 'Small Key (Turtle Rock)': ItemData(True, 'SmallKey', 0xAC, 'A small key to the pipe maze', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Turtle Rock'),
'Big Key (Turtle Rock)': ItemData(False, 'BigKey', 0x93, 'A big key to the pipe maze', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Turtle Rock'), 'Big Key (Turtle Rock)': ItemData(True, 'BigKey', 0x93, 'A big key to the pipe maze', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Turtle Rock'),
'Compass (Turtle Rock)': ItemData(False, 'Compass', 0x83, 'Now you can find Trinexx!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Turtle Rock'), 'Compass (Turtle Rock)': ItemData(False, 'Compass', 0x83, 'Now you can find Trinexx!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Turtle Rock'),
'Map (Turtle Rock)': ItemData(False, 'Map', 0x73, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Turtle Rock'), 'Map (Turtle Rock)': ItemData(False, 'Map', 0x73, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Turtle Rock'),
'Small Key (Ganons Tower)': ItemData(False, 'SmallKey', 0xAD, 'A small key to the evil tower', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Ganon\'s Tower'), 'Small Key (Ganons Tower)': ItemData(True, 'SmallKey', 0xAD, 'A small key to the evil tower', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key to Ganon\'s Tower'),
'Big Key (Ganons Tower)': ItemData(False, 'BigKey', 0x92, 'A big key to the evil tower', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Ganon\'s Tower'), 'Big Key (Ganons Tower)': ItemData(True, 'BigKey', 0x92, 'A big key to the evil tower', 'and the big key', 'the big-unlock kid', 'big key for sale', 'face key fungus', 'key boy opens chest again', 'a big key to Ganon\'s Tower'),
'Compass (Ganons Tower)': ItemData(False, 'Compass', 0x82, 'Now you can find Agahnim!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Ganon\'s Tower'), 'Compass (Ganons Tower)': ItemData(False, 'Compass', 0x82, 'Now you can find Agahnim!', 'and the compass', 'the magnetic kid', 'compass for sale', 'magnetic fungus', 'compass boy finds boss again', 'a compass to Ganon\'s Tower'),
'Map (Ganons Tower)': ItemData(False, 'Map', 0x72, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Ganon\'s Tower'), 'Map (Ganons Tower)': ItemData(False, 'Map', 0x72, 'A tightly folded map rests here', 'and the map', 'cartography kid', 'map for sale', 'a map to shrooms', 'map boy navigates again', 'a map to Ganon\'s Tower'),
'Small Key (Universal)': ItemData(False, None, 0xAF, 'A small key for any door', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key'), 'Small Key (Universal)': ItemData(False, None, 0xAF, 'A small key for any door', 'and the key', 'the unlocking kid', 'keys for sale', 'unlock the fungus', 'key boy opens door again', 'a small key'),

View File

@@ -1,6 +1,7 @@
import typing import typing
import random
from Options import Choice, Range, Option from Options import Choice, Range, Option, Toggle, DefaultOnToggle
class Logic(Choice): class Logic(Choice):
@@ -27,6 +28,46 @@ class Goal(Choice):
option_hand_in = 2 option_hand_in = 2
class DungeonItem(Choice):
value: int
option_original_dungeon = 0
option_own_dungeons = 1
option_own_world = 2
option_any_world = 3
option_different_world = 4
alias_true = 3
alias_false = 0
@property
def in_dungeon(self):
return self.value in {0, 1}
class bigkey_shuffle(DungeonItem):
"""Big Key Placement"""
item_name_group = "Big Keys"
displayname = "Big Key Shuffle"
class smallkey_shuffle(DungeonItem):
"""Small Key Placement"""
option_universal = 5
item_name_group = "Small Keys"
displayname = "Small Key Shuffle"
class compass_shuffle(DungeonItem):
"""Compass Placement"""
item_name_group = "Compasses"
displayname = "Compass Shuffle"
class map_shuffle(DungeonItem):
"""Map Placement"""
item_name_group = "Maps"
displayname = "Map Shuffle"
class Crystals(Range): class Crystals(Range):
range_start = 0 range_start = 0
range_end = 7 range_end = 7
@@ -70,8 +111,135 @@ class Enemies(Choice):
option_shuffled = 1 option_shuffled = 1
option_chaos = 2 option_chaos = 2
class Progressive(Choice):
displayname = "Progressive Items"
option_off = 0
option_grouped_random = 1
option_on = 2
alias_false = 0
alias_true = 2
default = 2
alias_random = 1
def want_progressives(self, random):
return random.choice([True, False]) if self.value == self.option_grouped_random else bool(self.value)
class Palette(Choice):
option_default = 0
option_good = 1
option_blackout = 2
option_puke = 3
option_classic = 4
option_grayscale = 5
option_negative = 6
option_dizzy = 7
option_sick = 8
alias_random = 1
class OWPalette(Palette):
displayname = "Overworld Palette"
class UWPalette(Palette):
displayname = "Underworld Palette"
class HUDPalette(Palette):
displayname = "Menu Palette"
class SwordPalette(Palette):
displayname = "Sword Palette"
class ShieldPalette(Palette):
displayname = "Shield Palette"
class LinkPalette(Palette):
displayname = "Link Palette"
class HeartBeep(Choice):
displayname = "Heart Beep Rate"
option_normal = 0
option_double = 1
option_half = 2
option_quarter = 3
option_off = 4
alias_false = 4
class HeartColor(Choice):
displayname = "Heart Color"
option_red = 0
option_blue = 1
option_green = 2
option_yellow = 3
@classmethod
def from_text(cls, text: str) -> Choice:
# remove when this becomes a base Choice feature
if text == "random":
return cls(random.randint(0, 3))
return super(HeartColor, cls).from_text(text)
class QuickSwap(DefaultOnToggle):
displayname = "L/R Quickswapping"
class MenuSpeed(Choice):
displayname = "Menu Speed"
option_normal = 0
option_instant = 1,
option_double = 2
option_triple = 3
option_quadruple = 4
option_half = 5
class Music(DefaultOnToggle):
displayname = "Play music"
class ReduceFlashing(DefaultOnToggle):
displayname = "Reduce Screen Flashes"
class TriforceHud(Choice):
displayname = "Display Method for Triforce Hunt"
option_normal = 0
option_hide_goal = 1
option_hide_required = 2
option_hide_both = 3
alttp_options: typing.Dict[str, type(Option)] = { alttp_options: typing.Dict[str, type(Option)] = {
"crystals_needed_for_gt": CrystalsTower, "crystals_needed_for_gt": CrystalsTower,
"crystals_needed_for_ganon": CrystalsGanon, "crystals_needed_for_ganon": CrystalsGanon,
"bigkey_shuffle": bigkey_shuffle,
"smallkey_shuffle": smallkey_shuffle,
"compass_shuffle": compass_shuffle,
"map_shuffle": map_shuffle,
"progressive": Progressive,
"shop_item_slots": ShopItemSlots, "shop_item_slots": ShopItemSlots,
} "ow_palettes": OWPalette,
"uw_palettes": UWPalette,
"hud_palettes": HUDPalette,
"sword_palettes": SwordPalette,
"shield_palettes": ShieldPalette,
"link_palettes": LinkPalette,
"heartbeep": HeartBeep,
"heartcolor": HeartColor,
"quickswap": QuickSwap,
"menuspeed": MenuSpeed,
"music": Music,
"reduceflashing": ReduceFlashing,
"triforcehud": TriforceHud,
"glitch_boots": DefaultOnToggle
}

View File

@@ -235,24 +235,41 @@ def no_logic_rules(world, player):
create_no_logic_connections(player, world, get_mirror_offset_spots_lw(player)) create_no_logic_connections(player, world, get_mirror_offset_spots_lw(player))
def overworld_glitch_connections(world, player):
# Boots-accessible locations.
create_owg_connections(player, world, get_boots_clip_exits_lw(world.mode[player] == 'inverted'))
create_owg_connections(player, world, get_boots_clip_exits_dw(world.mode[player] == 'inverted', player))
# Glitched speed drops.
create_owg_connections(player, world, get_glitched_speed_drops_dw(world.mode[player] == 'inverted'))
# Mirror clip spots.
if world.mode[player] != 'inverted':
create_owg_connections(player, world, get_mirror_clip_spots_dw())
create_owg_connections(player, world, get_mirror_offset_spots_dw())
else:
create_owg_connections(player, world, get_mirror_offset_spots_lw(player))
def overworld_glitches_rules(world, player): def overworld_glitches_rules(world, player):
# Boots-accessible locations. # Boots-accessible locations.
create_owg_connections(player, world, get_boots_clip_exits_lw(world.mode[player] == 'inverted'), lambda state: state.can_boots_clip_lw(player)) set_owg_connection_rules(player, world, get_boots_clip_exits_lw(world.mode[player] == 'inverted'), lambda state: state.can_boots_clip_lw(player))
create_owg_connections(player, world, get_boots_clip_exits_dw(world.mode[player] == 'inverted', player), lambda state: state.can_boots_clip_dw(player)) set_owg_connection_rules(player, world, get_boots_clip_exits_dw(world.mode[player] == 'inverted', player), lambda state: state.can_boots_clip_dw(player))
# Glitched speed drops. # Glitched speed drops.
create_owg_connections(player, world, get_glitched_speed_drops_dw(world.mode[player] == 'inverted'), lambda state: state.can_get_glitched_speed_dw(player)) set_owg_connection_rules(player, world, get_glitched_speed_drops_dw(world.mode[player] == 'inverted'), lambda state: state.can_get_glitched_speed_dw(player))
# Dark Death Mountain Ledge Clip Spot also accessible with mirror. # Dark Death Mountain Ledge Clip Spot also accessible with mirror.
if world.mode[player] != 'inverted': if world.mode[player] != 'inverted':
add_alternate_rule(world.get_entrance('Dark Death Mountain Ledge Clip Spot', player), lambda state: state.has('Magic Mirror', player)) add_alternate_rule(world.get_entrance('Dark Death Mountain Ledge Clip Spot', player), lambda state: state.has('Magic Mirror', player))
# Mirror clip spots. # Mirror clip spots.
if world.mode[player] != 'inverted': if world.mode[player] != 'inverted':
create_owg_connections(player, world, get_mirror_clip_spots_dw(), lambda state: state.has('Magic Mirror', player)) set_owg_connection_rules(player, world, get_mirror_clip_spots_dw(), lambda state: state.has('Magic Mirror', player))
create_owg_connections(player, world, get_mirror_offset_spots_dw(), lambda state: state.has('Magic Mirror', player) and state.can_boots_clip_lw(player)) set_owg_connection_rules(player, world, get_mirror_offset_spots_dw(), lambda state: state.has('Magic Mirror', player) and state.can_boots_clip_lw(player))
else: else:
create_owg_connections(player, world, get_mirror_offset_spots_lw(player), lambda state: state.has('Magic Mirror', player) and state.can_boots_clip_dw(player)) set_owg_connection_rules(player, world, get_mirror_offset_spots_lw(player), lambda state: state.has('Magic Mirror', player) and state.can_boots_clip_dw(player))
# Regions that require the boots and some other stuff. # Regions that require the boots and some other stuff.
if world.mode[player] != 'inverted': if world.mode[player] != 'inverted':
@@ -282,12 +299,16 @@ def create_no_logic_connections(player, world, connections):
parent.exits.append(connection) parent.exits.append(connection)
connection.connect(target) connection.connect(target)
def create_owg_connections(player, world, connections, default_rule): def create_owg_connections(player, world, connections):
for entrance, parent_region, target_region, *rule_override in connections: for entrance, parent_region, target_region, *rule_override in connections:
parent = world.get_region(parent_region, player) parent = world.get_region(parent_region, player)
target = world.get_region(target_region, player) target = world.get_region(target_region, player)
connection = Entrance(player, entrance, parent) connection = Entrance(player, entrance, parent)
parent.exits.append(connection) parent.exits.append(connection)
connection.connect(target) connection.connect(target)
def set_owg_connection_rules(player, world, connections, default_rule):
for entrance, _, _, *rule_override in connections:
connection = world.get_entrance(entrance, player)
rule = rule_override[0] if len(rule_override) > 0 else default_rule rule = rule_override[0] if len(rule_override) > 0 else default_rule
connection.access_rule = rule connection.access_rule = rule

View File

@@ -676,12 +676,10 @@ location_table: typing.Dict[str,
from worlds.alttp.Shops import shop_table_by_location_id, shop_table_by_location from worlds.alttp.Shops import shop_table_by_location_id, shop_table_by_location
lookup_id_to_name = {data[0]: name for name, data in location_table.items() if type(data[0]) == int} lookup_id_to_name = {data[0]: name for name, data in location_table.items() if type(data[0]) == int}
lookup_id_to_name = {**lookup_id_to_name, **{data[1]: name for name, data in key_drop_data.items()}, lookup_id_to_name = {**lookup_id_to_name, **{data[1]: name for name, data in key_drop_data.items()}}
-1: "Cheat Console", -2: "Server"}
lookup_id_to_name.update(shop_table_by_location_id) lookup_id_to_name.update(shop_table_by_location_id)
lookup_name_to_id = {name: data[0] for name, data in location_table.items() if type(data[0]) == int} lookup_name_to_id = {name: data[0] for name, data in location_table.items() if type(data[0]) == int}
lookup_name_to_id = {**lookup_name_to_id, **{name: data[1] for name, data in key_drop_data.items()}, lookup_name_to_id = {**lookup_name_to_id, **{name: data[1] for name, data in key_drop_data.items()}}
"Cheat Console": -1, "Server": -2}
lookup_name_to_id.update(shop_table_by_location) lookup_name_to_id.update(shop_table_by_location)
lookup_vanilla_location_to_entrance = {1572883: 'Kings Grave Inner Rocks', 191256: 'Kings Grave Inner Rocks', lookup_vanilla_location_to_entrance = {1572883: 'Kings Grave Inner Rocks', 191256: 'Kings Grave Inner Rocks',

View File

@@ -1,5 +1,8 @@
from __future__ import annotations from __future__ import annotations
import Utils
from Patch import read_rom
JAP10HASH = '03a63945398191337e896e5771f77173' JAP10HASH = '03a63945398191337e896e5771f77173'
RANDOMIZERBASEHASH = '13a75c5dd28055fbcf8f69bd8161871d' RANDOMIZERBASEHASH = '13a75c5dd28055fbcf8f69bd8161871d'
@@ -31,9 +34,10 @@ from worlds.alttp.Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts
DeathMountain_texts, \ DeathMountain_texts, \
LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, \ LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, \
SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names
from Utils import output_path, local_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen from Utils import local_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen
from worlds.alttp.Items import ItemFactory, item_table from worlds.alttp.Items import ItemFactory, item_table
from worlds.alttp.EntranceShuffle import door_addresses from worlds.alttp.EntranceShuffle import door_addresses
from worlds.alttp.Options import smallkey_shuffle
import Patch import Patch
try: try:
@@ -168,14 +172,6 @@ class LocalRom(object):
self.write_int32(startaddress + (i * 4), value) self.write_int32(startaddress + (i * 4), value)
def read_rom(stream) -> bytearray:
"Reads rom into bytearray and strips off any smc header"
buffer = bytearray(stream.read())
if len(buffer) % 0x400 == 0x200:
buffer = buffer[0x200:]
return buffer
check_lock = threading.Lock() check_lock = threading.Lock()
@@ -279,11 +275,11 @@ def apply_random_sprite_on_event(rom: LocalRom, sprite, local_random, allow_rand
rom.write_bytes(0x307078 + (i * 0x8000), sprite.glove_palette) rom.write_bytes(0x307078 + (i * 0x8000), sprite.glove_palette)
def patch_enemizer(world, team: int, player: int, rom: LocalRom, enemizercli, output_directory): def patch_enemizer(world, player: int, rom: LocalRom, enemizercli, output_directory):
check_enemizer(enemizercli) check_enemizer(enemizercli)
randopatch_path = os.path.abspath(os.path.join(output_directory, f'enemizer_randopatch_{team}_{player}.sfc')) randopatch_path = os.path.abspath(os.path.join(output_directory, f'enemizer_randopatch_{player}.sfc'))
options_path = os.path.abspath(os.path.join(output_directory, f'enemizer_options_{team}_{player}.json')) options_path = os.path.abspath(os.path.join(output_directory, f'enemizer_options_{player}.json'))
enemizer_output_path = os.path.abspath(os.path.join(output_directory, f'enemizer_output_{team}_{player}.sfc')) enemizer_output_path = os.path.abspath(os.path.join(output_directory, f'enemizer_output_{player}.sfc'))
# write options file for enemizer # write options file for enemizer
options = { options = {
@@ -546,7 +542,6 @@ class Sprite():
self.valid = False self.valid = False
def get_vanilla_sprite_data(self): def get_vanilla_sprite_data(self):
from Patch import get_base_rom_path
file_name = get_base_rom_path() file_name = get_base_rom_path()
base_rom_bytes = bytes(read_rom(open(file_name, "rb"))) base_rom_bytes = bytes(read_rom(open(file_name, "rb")))
Sprite.sprite = base_rom_bytes[0x80000:0x87000] Sprite.sprite = base_rom_bytes[0x80000:0x87000]
@@ -756,7 +751,7 @@ def get_nonnative_item_sprite(item: str) -> int:
# https://discord.com/channels/731205301247803413/827141303330406408/852102450822905886 # https://discord.com/channels/731205301247803413/827141303330406408/852102450822905886
def patch_rom(world, rom, player, team, enemized): def patch_rom(world, rom, player, enemized):
local_random = world.slot_seeds[player] local_random = world.slot_seeds[player]
# progressive bow silver arrow hint hack # progressive bow silver arrow hint hack
@@ -769,7 +764,7 @@ def patch_rom(world, rom, player, team, enemized):
# patch items # patch items
for location in world.get_locations(): for location in world.get_locations():
if location.player != player or location.address is None or location.shop_slot: if location.player != player or location.address is None or location.shop_slot is not None:
continue continue
itemid = location.item.code if location.item is not None else 0x5A itemid = location.item.code if location.item is not None else 0x5A
@@ -808,14 +803,14 @@ def patch_rom(world, rom, player, team, enemized):
# patch music # patch music
music_addresses = dungeon_music_addresses[location.name] music_addresses = dungeon_music_addresses[location.name]
if world.mapshuffle[player]: if world.map_shuffle[player]:
music = local_random.choice([0x11, 0x16]) music = local_random.choice([0x11, 0x16])
else: else:
music = 0x11 if 'Pendant' in location.item.name else 0x16 music = 0x11 if 'Pendant' in location.item.name else 0x16
for music_address in music_addresses: for music_address in music_addresses:
rom.write_byte(music_address, music) rom.write_byte(music_address, music)
if world.mapshuffle[player]: if world.map_shuffle[player]:
rom.write_byte(0x155C9, local_random.choice([0x11, 0x16])) # Randomize GT music too with map shuffle rom.write_byte(0x155C9, local_random.choice([0x11, 0x16])) # Randomize GT music too with map shuffle
# patch entrance/exits/holes # patch entrance/exits/holes
@@ -1497,18 +1492,18 @@ def patch_rom(world, rom, player, team, enemized):
# block HC upstairs doors in rain state in standard mode # block HC upstairs doors in rain state in standard mode
rom.write_byte(0x18008A, 0x01 if world.mode[player] == "standard" and world.shuffle[player] != 'vanilla' else 0x00) rom.write_byte(0x18008A, 0x01 if world.mode[player] == "standard" and world.shuffle[player] != 'vanilla' else 0x00)
rom.write_byte(0x18016A, 0x10 | ((0x01 if world.keyshuffle[player] is True else 0x00) rom.write_byte(0x18016A, 0x10 | ((0x01 if world.smallkey_shuffle[player] else 0x00)
| (0x02 if world.compassshuffle[player] else 0x00) | (0x02 if world.compass_shuffle[player] else 0x00)
| (0x04 if world.mapshuffle[player] else 0x00) | (0x04 if world.map_shuffle[player] else 0x00)
| (0x08 if world.bigkeyshuffle[player] else 0x00))) # free roaming item text boxes | (0x08 if world.bigkey_shuffle[player] else 0x00))) # free roaming item text boxes
rom.write_byte(0x18003B, 0x01 if world.mapshuffle[player] else 0x00) # maps showing crystals on overworld rom.write_byte(0x18003B, 0x01 if world.map_shuffle[player] else 0x00) # maps showing crystals on overworld
# compasses showing dungeon count # compasses showing dungeon count
if world.clock_mode[player] or not world.dungeon_counters[player]: if world.clock_mode[player] or not world.dungeon_counters[player]:
rom.write_byte(0x18003C, 0x00) # Currently must be off if timer is on, because they use same HUD location rom.write_byte(0x18003C, 0x00) # Currently must be off if timer is on, because they use same HUD location
elif world.dungeon_counters[player] is True: elif world.dungeon_counters[player] is True:
rom.write_byte(0x18003C, 0x02) # always on rom.write_byte(0x18003C, 0x02) # always on
elif world.compassshuffle[player] or world.dungeon_counters[player] == 'pickup': elif world.compass_shuffle[player] or world.dungeon_counters[player] == 'pickup':
rom.write_byte(0x18003C, 0x01) # show on pickup rom.write_byte(0x18003C, 0x01) # show on pickup
else: else:
rom.write_byte(0x18003C, 0x00) rom.write_byte(0x18003C, 0x00)
@@ -1521,10 +1516,11 @@ def patch_rom(world, rom, player, team, enemized):
# b - Big Key # b - Big Key
# a - Small Key # a - Small Key
# #
rom.write_byte(0x180045, ((0x01 if world.keyshuffle[player] is True else 0x00) rom.write_byte(0x180045, ((0x00 if (world.smallkey_shuffle[player] == smallkey_shuffle.option_original_dungeon or
| (0x02 if world.bigkeyshuffle[player] else 0x00) world.smallkey_shuffle[player] == smallkey_shuffle.option_universal) else 0x01)
| (0x04 if world.mapshuffle[player] else 0x00) | (0x02 if world.bigkey_shuffle[player] else 0x00)
| (0x08 if world.compassshuffle[player] else 0x00))) # free roaming items in menu | (0x04 if world.map_shuffle[player] else 0x00)
| (0x08 if world.compass_shuffle[player] else 0x00))) # free roaming items in menu
# Map reveals # Map reveals
reveal_bytes = { reveal_bytes = {
@@ -1550,11 +1546,11 @@ def patch_rom(world, rom, player, team, enemized):
return 0x0000 return 0x0000
rom.write_int16(0x18017A, rom.write_int16(0x18017A,
get_reveal_bytes('Green Pendant') if world.mapshuffle[player] else 0x0000) # Sahasrahla reveal get_reveal_bytes('Green Pendant') if world.map_shuffle[player] else 0x0000) # Sahasrahla reveal
rom.write_int16(0x18017C, get_reveal_bytes('Crystal 5') | get_reveal_bytes('Crystal 6') if world.mapshuffle[ rom.write_int16(0x18017C, get_reveal_bytes('Crystal 5') | get_reveal_bytes('Crystal 6') if world.map_shuffle[
player] else 0x0000) # Bomb Shop Reveal player] else 0x0000) # Bomb Shop Reveal
rom.write_byte(0x180172, 0x01 if world.keyshuffle[player] == "universal" else 0x00) # universal keys rom.write_byte(0x180172, 0x01 if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal else 0x00) # universal keys
rom.write_byte(0x18637E, 0x01 if world.retro[player] else 0x00) # Skip quiver in item shops once bought rom.write_byte(0x18637E, 0x01 if world.retro[player] else 0x00) # Skip quiver in item shops once bought
rom.write_byte(0x180175, 0x01 if world.retro[player] else 0x00) # rupee bow rom.write_byte(0x180175, 0x01 if world.retro[player] else 0x00) # rupee bow
rom.write_byte(0x180176, 0x0A if world.retro[player] else 0x00) # wood arrow cost rom.write_byte(0x180176, 0x0A if world.retro[player] else 0x00) # wood arrow cost
@@ -1645,7 +1641,7 @@ def patch_rom(world, rom, player, team, enemized):
rom.write_byte(0x4BA1D, tile_set.get_len()) rom.write_byte(0x4BA1D, tile_set.get_len())
rom.write_bytes(0x4BA2A, tile_set.get_bytes()) rom.write_bytes(0x4BA2A, tile_set.get_bytes())
write_strings(rom, world, player, team) write_strings(rom, world, player)
# remote items flag, does not currently work # remote items flag, does not currently work
rom.write_byte(0x18637C, int(world.worlds[player].remote_items)) rom.write_byte(0x18637C, int(world.worlds[player].remote_items))
@@ -1654,13 +1650,13 @@ def patch_rom(world, rom, player, team, enemized):
# 21 bytes # 21 bytes
from Main import __version__ from Main import __version__
# TODO: Adjust Enemizer to accept AP and AD # TODO: Adjust Enemizer to accept AP and AD
rom.name = bytearray(f'BM{__version__.replace(".", "")[0:3]}_{team + 1}_{player}_{world.seed:09}\0', 'utf8')[:21] rom.name = bytearray(f'BM{__version__.replace(".", "")[0:3]}_{player}_{world.seed:11}\0', 'utf8')[:21]
rom.name.extend([0] * (21 - len(rom.name))) rom.name.extend([0] * (21 - len(rom.name)))
rom.write_bytes(0x7FC0, rom.name) rom.write_bytes(0x7FC0, rom.name)
# set player names # set player names
for p in range(1, min(world.players, 255) + 1): for p in range(1, min(world.players, 255) + 1):
rom.write_bytes(0x195FFC + ((p - 1) * 32), hud_format_text(world.player_names[p][team])) rom.write_bytes(0x195FFC + ((p - 1) * 32), hud_format_text(world.player_name[p]))
# Write title screen Code # Write title screen Code
hashint = int(rom.get_hash(), 16) hashint = int(rom.get_hash(), 16)
@@ -1756,13 +1752,13 @@ def hud_format_text(text):
return output[:32] return output[:32]
def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, sprite: str, palettes_options, def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, sprite: str, palettes_options,
world=None, player=1, allow_random_on_event=False, reduceflashing=False, world=None, player=1, allow_random_on_event=False, reduceflashing=False,
triforcehud: str = None): triforcehud: str = None):
local_random = random if not world else world.slot_seeds[player] local_random = random if not world else world.slot_seeds[player]
disable_music: bool = not music
# enable instant item menu # enable instant item menu
if fastmenu == 'instant': if menuspeed == 'instant':
rom.write_byte(0x6DD9A, 0x20) rom.write_byte(0x6DD9A, 0x20)
rom.write_byte(0x6DF2A, 0x20) rom.write_byte(0x6DF2A, 0x20)
rom.write_byte(0x6E0E9, 0x20) rom.write_byte(0x6E0E9, 0x20)
@@ -1770,15 +1766,15 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr
rom.write_byte(0x6DD9A, 0x11) rom.write_byte(0x6DD9A, 0x11)
rom.write_byte(0x6DF2A, 0x12) rom.write_byte(0x6DF2A, 0x12)
rom.write_byte(0x6E0E9, 0x12) rom.write_byte(0x6E0E9, 0x12)
if fastmenu == 'instant': if menuspeed == 'instant':
rom.write_byte(0x180048, 0xE8) rom.write_byte(0x180048, 0xE8)
elif fastmenu == 'double': elif menuspeed == 'double':
rom.write_byte(0x180048, 0x10) rom.write_byte(0x180048, 0x10)
elif fastmenu == 'triple': elif menuspeed == 'triple':
rom.write_byte(0x180048, 0x18) rom.write_byte(0x180048, 0x18)
elif fastmenu == 'quadruple': elif menuspeed == 'quadruple':
rom.write_byte(0x180048, 0x20) rom.write_byte(0x180048, 0x20)
elif fastmenu == 'half': elif menuspeed == 'half':
rom.write_byte(0x180048, 0x04) rom.write_byte(0x180048, 0x04)
else: else:
rom.write_byte(0x180048, 0x08) rom.write_byte(0x180048, 0x08)
@@ -1854,7 +1850,7 @@ def apply_rom_settings(rom, beep, color, quickswap, fastmenu, disable_music, spr
while True: while True:
yield ColorF(local_random.random(), local_random.random(), local_random.random()) yield ColorF(local_random.random(), local_random.random(), local_random.random())
if mode == 'random': if mode == 'good':
mode = 'maseya' mode = 'maseya'
z3pr.randomize(rom.buffer, mode, offset_collections=offsets_array, random_colors=next_color_generator()) z3pr.randomize(rom.buffer, mode, offset_collections=offsets_array, random_colors=next_color_generator())
@@ -2075,7 +2071,7 @@ def write_string_to_rom(rom, target, string):
rom.write_bytes(address, MultiByteTextMapper.convert(string, maxbytes)) rom.write_bytes(address, MultiByteTextMapper.convert(string, maxbytes))
def write_strings(rom, world, player, team): def write_strings(rom, world, player):
local_random = world.slot_seeds[player] local_random = world.slot_seeds[player]
tt = TextTable() tt = TextTable()
@@ -2093,16 +2089,16 @@ def write_strings(rom, world, player, team):
if not dest: if not dest:
return "nothing" return "nothing"
if ped_hint: if ped_hint:
hint = dest.pedestal_hint_text if dest.pedestal_hint_text else "unknown item" hint = dest.pedestal_hint_text
else: else:
hint = dest.hint_text if dest.hint_text else "something" hint = dest.hint_text
if dest.player != player: if dest.player != player:
if ped_hint: if ped_hint:
hint += f" for {world.player_names[dest.player][team]}!" hint += f" for {world.player_name[dest.player]}!"
elif type(dest) in [Region, ALttPLocation]: elif type(dest) in [Region, ALttPLocation]:
hint += f" in {world.player_names[dest.player][team]}'s world" hint += f" in {world.player_name[dest.player]}'s world"
else: else:
hint += f" for {world.player_names[dest.player][team]}" hint += f" for {world.player_name[dest.player]}"
return hint return hint
# For hints, first we write hints about entrances, some from the inconvenient list others from all reasonable entrances. # For hints, first we write hints about entrances, some from the inconvenient list others from all reasonable entrances.
@@ -2253,9 +2249,9 @@ def write_strings(rom, world, player, team):
# Lastly we write hints to show where certain interesting items are. It is done the way it is to re-use the silver code and also to give one hint per each type of item regardless of how many exist. This supports many settings well. # Lastly we write hints to show where certain interesting items are. It is done the way it is to re-use the silver code and also to give one hint per each type of item regardless of how many exist. This supports many settings well.
items_to_hint = RelevantItems.copy() items_to_hint = RelevantItems.copy()
if world.keyshuffle[player]: if world.smallkey_shuffle[player]:
items_to_hint.extend(SmallKeys) items_to_hint.extend(SmallKeys)
if world.bigkeyshuffle[player]: if world.bigkey_shuffle[player]:
items_to_hint.extend(BigKeys) items_to_hint.extend(BigKeys)
local_random.shuffle(items_to_hint) local_random.shuffle(items_to_hint)
hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed'] else 8 hint_count = 5 if world.shuffle[player] not in ['vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed'] else 8
@@ -2934,3 +2930,27 @@ hash_alphabet = [
"Lamp", "Hammer", "Shovel", "Flute", "Bug Net", "Book", "Bottle", "Potion", "Cane", "Cape", "Mirror", "Boots", "Lamp", "Hammer", "Shovel", "Flute", "Bug Net", "Book", "Bottle", "Potion", "Cane", "Cape", "Mirror", "Boots",
"Gloves", "Flippers", "Pearl", "Shield", "Tunic", "Heart", "Map", "Compass", "Key" "Gloves", "Flippers", "Pearl", "Shield", "Tunic", "Heart", "Map", "Compass", "Key"
] ]
def get_base_rom_bytes(file_name: str = "") -> bytes:
base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None)
if not base_rom_bytes:
file_name = get_base_rom_path(file_name)
base_rom_bytes = bytes(read_rom(open(file_name, "rb")))
basemd5 = hashlib.md5()
basemd5.update(base_rom_bytes)
if JAP10HASH != basemd5.hexdigest():
raise Exception('Supplied Base Rom does not match known MD5 for JAP(1.0) release. '
'Get the correct game and version, then dump it')
get_base_rom_bytes.base_rom_bytes = base_rom_bytes
return base_rom_bytes
def get_base_rom_path(file_name: str = "") -> str:
options = Utils.get_options()
if not file_name:
file_name = options["lttp_options"]["rom_file"]
if not os.path.exists(file_name):
file_name = Utils.local_path(file_name)
return file_name

View File

@@ -8,6 +8,7 @@ from worlds.alttp.UnderworldGlitchRules import underworld_glitches_rules
from worlds.alttp.Bosses import GanonDefeatRule from worlds.alttp.Bosses import GanonDefeatRule
from worlds.generic.Rules import set_rule, add_rule, forbid_item, add_item_rule, item_in_locations, \ from worlds.generic.Rules import set_rule, add_rule, forbid_item, add_item_rule, item_in_locations, \
item_name item_name
from worlds.alttp.Options import smallkey_shuffle
def set_rules(world): def set_rules(world):
@@ -99,7 +100,7 @@ def set_rules(world):
add_rule(world.get_entrance('Ganons Tower', player), lambda state: state.world.get_entrance('Ganons Tower Ascent', player).can_reach(state), 'or') add_rule(world.get_entrance('Ganons Tower', player), lambda state: state.world.get_entrance('Ganons Tower Ascent', player).can_reach(state), 'or')
set_bunny_rules(world, player, world.mode[player] == 'inverted') set_bunny_rules(world, player, world.mode[player] == 'inverted')
def mirrorless_path_to_castle_courtyard(world, player): def mirrorless_path_to_castle_courtyard(world, player):
# If Agahnim is defeated then the courtyard needs to be accessible without using the mirror for the mirror offset glitch. # If Agahnim is defeated then the courtyard needs to be accessible without using the mirror for the mirror offset glitch.
@@ -118,7 +119,7 @@ def mirrorless_path_to_castle_courtyard(world, player):
else: else:
queue.append((entrance.connected_region, new_path)) queue.append((entrance.connected_region, new_path))
raise Exception(f"Could not find mirrorless path to castle courtyard for Player {player} ({world.get_player_names(player)})") raise Exception(f"Could not find mirrorless path to castle courtyard for Player {player} ({world.get_player_name(player)})")
def set_defeat_dungeon_boss_rule(location): def set_defeat_dungeon_boss_rule(location):
@@ -211,17 +212,17 @@ def global_rules(world, player):
set_rule(world.get_location('Hookshot Cave - Bottom Left', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_location('Hookshot Cave - Bottom Left', player), lambda state: state.has('Hookshot', player))
set_rule(world.get_entrance('Sewers Door', player), set_rule(world.get_entrance('Sewers Door', player),
lambda state: state.has_key('Small Key (Hyrule Castle)', player) or ( lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player) or (
world.keyshuffle[player] == "universal" and world.mode[ world.smallkey_shuffle[player] == smallkey_shuffle.option_universal and world.mode[
player] == 'standard')) # standard universal small keys cannot access the shop player] == 'standard')) # standard universal small keys cannot access the shop
set_rule(world.get_entrance('Sewers Back Door', player), set_rule(world.get_entrance('Sewers Back Door', player),
lambda state: state.has_key('Small Key (Hyrule Castle)', player)) lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player))
set_rule(world.get_entrance('Agahnim 1', player), set_rule(world.get_entrance('Agahnim 1', player),
lambda state: state.has_sword(player) and state.has_key('Small Key (Agahnims Tower)', player, 2)) lambda state: state.has_sword(player) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2))
set_rule(world.get_location('Castle Tower - Room 03', player), lambda state: state.can_kill_most_things(player, 8)) set_rule(world.get_location('Castle Tower - Room 03', player), lambda state: state.can_kill_most_things(player, 8))
set_rule(world.get_location('Castle Tower - Dark Maze', player), set_rule(world.get_location('Castle Tower - Dark Maze', player),
lambda state: state.can_kill_most_things(player, 8) and state.has_key('Small Key (Agahnims Tower)', lambda state: state.can_kill_most_things(player, 8) and state._lttp_has_key('Small Key (Agahnims Tower)',
player)) player))
set_rule(world.get_location('Eastern Palace - Big Chest', player), set_rule(world.get_location('Eastern Palace - Big Chest', player),
@@ -238,15 +239,15 @@ def global_rules(world, player):
set_rule(world.get_location('Desert Palace - Big Chest', player), lambda state: state.has('Big Key (Desert Palace)', player)) set_rule(world.get_location('Desert Palace - Big Chest', player), lambda state: state.has('Big Key (Desert Palace)', player))
set_rule(world.get_location('Desert Palace - Torch', player), lambda state: state.has('Pegasus Boots', player)) set_rule(world.get_location('Desert Palace - Torch', player), lambda state: state.has('Pegasus Boots', player))
set_rule(world.get_entrance('Desert Palace East Wing', player), lambda state: state.has_key('Small Key (Desert Palace)', player)) set_rule(world.get_entrance('Desert Palace East Wing', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player))
set_rule(world.get_location('Desert Palace - Prize', player), lambda state: state.has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and state.has_fire_source(player) and state.world.get_location('Desert Palace - Prize', player).parent_region.dungeon.boss.can_defeat(state)) set_rule(world.get_location('Desert Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and state.has_fire_source(player) and state.world.get_location('Desert Palace - Prize', player).parent_region.dungeon.boss.can_defeat(state))
set_rule(world.get_location('Desert Palace - Boss', player), lambda state: state.has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and state.has_fire_source(player) and state.world.get_location('Desert Palace - Boss', player).parent_region.dungeon.boss.can_defeat(state)) set_rule(world.get_location('Desert Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player) and state.has('Big Key (Desert Palace)', player) and state.has_fire_source(player) and state.world.get_location('Desert Palace - Boss', player).parent_region.dungeon.boss.can_defeat(state))
# logic patch to prevent placing a crystal in Desert that's required to reach the required keys # logic patch to prevent placing a crystal in Desert that's required to reach the required keys
if not (world.keyshuffle[player] and world.bigkeyshuffle[player]): if not (world.smallkey_shuffle[player] and world.bigkey_shuffle[player]):
add_rule(world.get_location('Desert Palace - Prize', player), lambda state: state.world.get_region('Desert Palace Main (Outer)', player).can_reach(state)) add_rule(world.get_location('Desert Palace - Prize', player), lambda state: state.world.get_region('Desert Palace Main (Outer)', player).can_reach(state))
set_rule(world.get_entrance('Tower of Hera Small Key Door', player), lambda state: state.has_key('Small Key (Tower of Hera)', player) or item_name(state, 'Tower of Hera - Big Key Chest', player) == ('Small Key (Tower of Hera)', player)) set_rule(world.get_entrance('Tower of Hera Small Key Door', player), lambda state: state._lttp_has_key('Small Key (Tower of Hera)', player) or item_name(state, 'Tower of Hera - Big Key Chest', player) == ('Small Key (Tower of Hera)', player))
set_rule(world.get_entrance('Tower of Hera Big Key Door', player), lambda state: state.has('Big Key (Tower of Hera)', player)) set_rule(world.get_entrance('Tower of Hera Big Key Door', player), lambda state: state.has('Big Key (Tower of Hera)', player))
set_rule(world.get_location('Tower of Hera - Big Chest', player), lambda state: state.has('Big Key (Tower of Hera)', player)) set_rule(world.get_location('Tower of Hera - Big Chest', player), lambda state: state.has('Big Key (Tower of Hera)', player))
set_rule(world.get_location('Tower of Hera - Big Key Chest', player), lambda state: state.has_fire_source(player)) set_rule(world.get_location('Tower of Hera - Big Key Chest', player), lambda state: state.has_fire_source(player))
@@ -254,36 +255,36 @@ def global_rules(world, player):
set_always_allow(world.get_location('Tower of Hera - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Tower of Hera)' and item.player == player) set_always_allow(world.get_location('Tower of Hera - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Tower of Hera)' and item.player == player)
set_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Flippers', player) and state.has('Open Floodgate', player)) set_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Flippers', player) and state.has('Open Floodgate', player))
set_rule(world.get_entrance('Swamp Palace Small Key Door', player), lambda state: state.has_key('Small Key (Swamp Palace)', player)) set_rule(world.get_entrance('Swamp Palace Small Key Door', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player))
set_rule(world.get_entrance('Swamp Palace (Center)', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('Swamp Palace (Center)', player), lambda state: state.has('Hammer', player))
set_rule(world.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player) or item_name(state, 'Swamp Palace - Big Chest', player) == ('Big Key (Swamp Palace)', player)) set_rule(world.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player) or item_name(state, 'Swamp Palace - Big Chest', player) == ('Big Key (Swamp Palace)', player))
if world.accessibility[player] != 'locations': if world.accessibility[player] != 'locations':
set_always_allow(world.get_location('Swamp Palace - Big Chest', player), lambda state, item: item.name == 'Big Key (Swamp Palace)' and item.player == player) set_always_allow(world.get_location('Swamp Palace - Big Chest', player), lambda state, item: item.name == 'Big Key (Swamp Palace)' and item.player == player)
set_rule(world.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player)) set_rule(world.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player))
if not world.keyshuffle[player] and world.logic[player] != 'nologic': if not world.smallkey_shuffle[player] and world.logic[player] != 'nologic':
forbid_item(world.get_location('Swamp Palace - Entrance', player), 'Big Key (Swamp Palace)', player) forbid_item(world.get_location('Swamp Palace - Entrance', player), 'Big Key (Swamp Palace)', player)
set_rule(world.get_entrance('Thieves Town Big Key Door', player), lambda state: state.has('Big Key (Thieves Town)', player)) set_rule(world.get_entrance('Thieves Town Big Key Door', player), lambda state: state.has('Big Key (Thieves Town)', player))
set_rule(world.get_entrance('Blind Fight', player), lambda state: state.has_key('Small Key (Thieves Town)', player)) set_rule(world.get_entrance('Blind Fight', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
set_rule(world.get_location('Thieves\' Town - Big Chest', player), lambda state: (state.has_key('Small Key (Thieves Town)', player) or item_name(state, 'Thieves\' Town - Big Chest', player) == ('Small Key (Thieves Town)', player)) and state.has('Hammer', player)) set_rule(world.get_location('Thieves\' Town - Big Chest', player), lambda state: (state._lttp_has_key('Small Key (Thieves Town)', player) or item_name(state, 'Thieves\' Town - Big Chest', player) == ('Small Key (Thieves Town)', player)) and state.has('Hammer', player))
if world.accessibility[player] != 'locations': if world.accessibility[player] != 'locations':
set_always_allow(world.get_location('Thieves\' Town - Big Chest', player), lambda state, item: item.name == 'Small Key (Thieves Town)' and item.player == player and state.has('Hammer', player)) set_always_allow(world.get_location('Thieves\' Town - Big Chest', player), lambda state, item: item.name == 'Small Key (Thieves Town)' and item.player == player and state.has('Hammer', player))
set_rule(world.get_location('Thieves\' Town - Attic', player), lambda state: state.has_key('Small Key (Thieves Town)', player)) set_rule(world.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
set_rule(world.get_entrance('Skull Woods First Section South Door', player), lambda state: state.has_key('Small Key (Skull Woods)', player)) set_rule(world.get_entrance('Skull Woods First Section South Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player))
set_rule(world.get_entrance('Skull Woods First Section (Right) North Door', player), lambda state: state.has_key('Small Key (Skull Woods)', player)) set_rule(world.get_entrance('Skull Woods First Section (Right) North Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player))
set_rule(world.get_entrance('Skull Woods First Section West Door', player), lambda state: state.has_key('Small Key (Skull Woods)', player, 2)) # ideally would only be one key, but we may have spent thst key already on escaping the right section set_rule(world.get_entrance('Skull Woods First Section West Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 2)) # ideally would only be one key, but we may have spent thst key already on escaping the right section
set_rule(world.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state.has_key('Small Key (Skull Woods)', player, 2)) set_rule(world.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 2))
set_rule(world.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player) or item_name(state, 'Skull Woods - Big Chest', player) == ('Big Key (Skull Woods)', player)) set_rule(world.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player) or item_name(state, 'Skull Woods - Big Chest', player) == ('Big Key (Skull Woods)', player))
if world.accessibility[player] != 'locations': if world.accessibility[player] != 'locations':
set_always_allow(world.get_location('Skull Woods - Big Chest', player), lambda state, item: item.name == 'Big Key (Skull Woods)' and item.player == player) set_always_allow(world.get_location('Skull Woods - Big Chest', player), lambda state, item: item.name == 'Big Key (Skull Woods)' and item.player == player)
set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state.has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player) and state.has_sword(player)) # sword required for curtain set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player) and state.has_sword(player)) # sword required for curtain
set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: state.can_melt_things(player)) set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: state.can_melt_things(player))
set_rule(world.get_location('Ice Palace - Big Chest', player), lambda state: state.has('Big Key (Ice Palace)', player)) set_rule(world.get_location('Ice Palace - Big Chest', player), lambda state: state.has('Big Key (Ice Palace)', player))
set_rule(world.get_entrance('Ice Palace (Kholdstare)', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player) and state.has('Big Key (Ice Palace)', player) and (state.has_key('Small Key (Ice Palace)', player, 2) or (state.has('Cane of Somaria', player) and state.has_key('Small Key (Ice Palace)', player, 1)))) set_rule(world.get_entrance('Ice Palace (Kholdstare)', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player) and state.has('Big Key (Ice Palace)', player) and (state._lttp_has_key('Small Key (Ice Palace)', player, 2) or (state.has('Cane of Somaria', player) and state._lttp_has_key('Small Key (Ice Palace)', player, 1))))
set_rule(world.get_entrance('Ice Palace (East)', player), lambda state: (state.has('Hookshot', player) or ( set_rule(world.get_entrance('Ice Palace (East)', player), lambda state: (state.has('Hookshot', player) or (
item_in_locations(state, 'Big Key (Ice Palace)', player, [('Ice Palace - Spike Room', player), ('Ice Palace - Big Key Chest', player), ('Ice Palace - Map Chest', player)]) and state.has_key('Small Key (Ice Palace)', player))) and (state.world.can_take_damage[player] or state.has('Hookshot', player) or state.has('Cape', player) or state.has('Cane of Byrna', player))) item_in_locations(state, 'Big Key (Ice Palace)', player, [('Ice Palace - Spike Room', player), ('Ice Palace - Big Key Chest', player), ('Ice Palace - Map Chest', player)]) and state._lttp_has_key('Small Key (Ice Palace)', player))) and (state.world.can_take_damage[player] or state.has('Hookshot', player) or state.has('Cape', player) or state.has('Cane of Byrna', player)))
set_rule(world.get_entrance('Ice Palace (East Top)', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player)) set_rule(world.get_entrance('Ice Palace (East Top)', player), lambda state: state.can_lift_rocks(player) and state.has('Hammer', player))
set_rule(world.get_entrance('Misery Mire Entrance Gap', player), lambda state: (state.has('Pegasus Boots', player) or state.has('Hookshot', player)) and (state.has_sword(player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Hammer', player) or state.has('Cane of Somaria', player) or state.can_shoot_arrows(player))) # need to defeat wizzrobes, bombs don't work ... set_rule(world.get_entrance('Misery Mire Entrance Gap', player), lambda state: (state.has('Pegasus Boots', player) or state.has('Hookshot', player)) and (state.has_sword(player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Hammer', player) or state.has('Cane of Somaria', player) or state.can_shoot_arrows(player))) # need to defeat wizzrobes, bombs don't work ...
@@ -292,13 +293,13 @@ def global_rules(world, player):
set_rule(world.get_entrance('Misery Mire Big Key Door', player), lambda state: state.has('Big Key (Misery Mire)', player)) set_rule(world.get_entrance('Misery Mire Big Key Door', player), lambda state: state.has('Big Key (Misery Mire)', player))
# you can squander the free small key from the pot by opening the south door to the north west switch room, locking you out of accessing a color switch ... # you can squander the free small key from the pot by opening the south door to the north west switch room, locking you out of accessing a color switch ...
# big key gives backdoor access to that from the teleporter in the north west # big key gives backdoor access to that from the teleporter in the north west
set_rule(world.get_location('Misery Mire - Map Chest', player), lambda state: state.has_key('Small Key (Misery Mire)', player, 1) or state.has('Big Key (Misery Mire)', player)) set_rule(world.get_location('Misery Mire - Map Chest', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 1) or state.has('Big Key (Misery Mire)', player))
set_rule(world.get_location('Misery Mire - Main Lobby', player), lambda state: state.has_key('Small Key (Misery Mire)', player, 1) or state.has_key('Big Key (Misery Mire)', player)) set_rule(world.get_location('Misery Mire - Main Lobby', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 1) or state._lttp_has_key('Big Key (Misery Mire)', player))
# we can place a small key in the West wing iff it also contains/blocks the Big Key, as we cannot reach and softlock with the basement key door yet # we can place a small key in the West wing iff it also contains/blocks the Big Key, as we cannot reach and softlock with the basement key door yet
set_rule(world.get_entrance('Misery Mire (West)', player), lambda state: state.has_key('Small Key (Misery Mire)', player, 2) if (( set_rule(world.get_entrance('Misery Mire (West)', player), lambda state: state._lttp_has_key('Small Key (Misery Mire)', player, 2) if ((
item_name(state, 'Misery Mire - Compass Chest', player) in [('Big Key (Misery Mire)', player)]) or item_name(state, 'Misery Mire - Compass Chest', player) in [('Big Key (Misery Mire)', player)]) or
( (
item_name(state, 'Misery Mire - Big Key Chest', player) in [('Big Key (Misery Mire)', player)])) else state.has_key('Small Key (Misery Mire)', player, 3)) item_name(state, 'Misery Mire - Big Key Chest', player) in [('Big Key (Misery Mire)', player)])) else state._lttp_has_key('Small Key (Misery Mire)', player, 3))
set_rule(world.get_location('Misery Mire - Compass Chest', player), lambda state: state.has_fire_source(player)) set_rule(world.get_location('Misery Mire - Compass Chest', player), lambda state: state.has_fire_source(player))
set_rule(world.get_location('Misery Mire - Big Key Chest', player), lambda state: state.has_fire_source(player)) set_rule(world.get_location('Misery Mire - Big Key Chest', player), lambda state: state.has_fire_source(player))
set_rule(world.get_entrance('Misery Mire (Vitreous)', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('Misery Mire (Vitreous)', player), lambda state: state.has('Cane of Somaria', player))
@@ -317,27 +318,27 @@ def global_rules(world, player):
set_rule(world.get_location('Turtle Rock - Eye Bridge - Bottom Right', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player)) set_rule(world.get_location('Turtle Rock - Eye Bridge - Bottom Right', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Left', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player)) set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Left', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Right', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player)) set_rule(world.get_location('Turtle Rock - Eye Bridge - Top Right', player), lambda state: state.has('Cane of Byrna', player) or state.has('Cape', player) or state.has('Mirror Shield', player))
set_rule(world.get_entrance('Turtle Rock (Trinexx)', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 4) and state.has('Big Key (Turtle Rock)', player) and state.has('Cane of Somaria', player)) set_rule(world.get_entrance('Turtle Rock (Trinexx)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 4) and state.has('Big Key (Turtle Rock)', player) and state.has('Cane of Somaria', player))
if not world.enemy_shuffle[player]: if not world.enemy_shuffle[player]:
set_rule(world.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: state.can_shoot_arrows(player)) set_rule(world.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: state.can_shoot_arrows(player))
set_rule(world.get_entrance('Palace of Darkness Hammer Peg Drop', player), lambda state: state.has('Hammer', player)) set_rule(world.get_entrance('Palace of Darkness Hammer Peg Drop', player), lambda state: state.has('Hammer', player))
set_rule(world.get_entrance('Palace of Darkness Bridge Room', player), lambda state: state.has_key('Small Key (Palace of Darkness)', player, 1)) # If we can reach any other small key door, we already have back door access to this area set_rule(world.get_entrance('Palace of Darkness Bridge Room', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 1)) # If we can reach any other small key door, we already have back door access to this area
set_rule(world.get_entrance('Palace of Darkness Big Key Door', player), lambda state: state.has_key('Small Key (Palace of Darkness)', player, 6) and state.has('Big Key (Palace of Darkness)', player) and state.can_shoot_arrows(player) and state.has('Hammer', player)) set_rule(world.get_entrance('Palace of Darkness Big Key Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) and state.has('Big Key (Palace of Darkness)', player) and state.can_shoot_arrows(player) and state.has('Hammer', player))
set_rule(world.get_entrance('Palace of Darkness (North)', player), lambda state: state.has_key('Small Key (Palace of Darkness)', player, 4)) set_rule(world.get_entrance('Palace of Darkness (North)', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 4))
set_rule(world.get_location('Palace of Darkness - Big Chest', player), lambda state: state.has('Big Key (Palace of Darkness)', player)) set_rule(world.get_location('Palace of Darkness - Big Chest', player), lambda state: state.has('Big Key (Palace of Darkness)', player))
set_rule(world.get_entrance('Palace of Darkness Big Key Chest Staircase', player), lambda state: state.has_key('Small Key (Palace of Darkness)', player, 6) or ( set_rule(world.get_entrance('Palace of Darkness Big Key Chest Staircase', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
item_name(state, 'Palace of Darkness - Big Key Chest', player) in [('Small Key (Palace of Darkness)', player)] and state.has_key('Small Key (Palace of Darkness)', player, 3))) item_name(state, 'Palace of Darkness - Big Key Chest', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 3)))
if world.accessibility[player] != 'locations': if world.accessibility[player] != 'locations':
set_always_allow(world.get_location('Palace of Darkness - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state.has_key('Small Key (Palace of Darkness)', player, 5)) set_always_allow(world.get_location('Palace of Darkness - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
set_rule(world.get_entrance('Palace of Darkness Spike Statue Room Door', player), lambda state: state.has_key('Small Key (Palace of Darkness)', player, 6) or ( set_rule(world.get_entrance('Palace of Darkness Spike Statue Room Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
item_name(state, 'Palace of Darkness - Harmless Hellway', player) in [('Small Key (Palace of Darkness)', player)] and state.has_key('Small Key (Palace of Darkness)', player, 4))) item_name(state, 'Palace of Darkness - Harmless Hellway', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 4)))
if world.accessibility[player] != 'locations': if world.accessibility[player] != 'locations':
set_always_allow(world.get_location('Palace of Darkness - Harmless Hellway', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state.has_key('Small Key (Palace of Darkness)', player, 5)) set_always_allow(world.get_location('Palace of Darkness - Harmless Hellway', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
set_rule(world.get_entrance('Palace of Darkness Maze Door', player), lambda state: state.has_key('Small Key (Palace of Darkness)', player, 6)) set_rule(world.get_entrance('Palace of Darkness Maze Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6))
# these key rules are conservative, you might be able to get away with more lenient rules # these key rules are conservative, you might be able to get away with more lenient rules
randomizer_room_chests = ['Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right', 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right'] randomizer_room_chests = ['Ganons Tower - Randomizer Room - Top Left', 'Ganons Tower - Randomizer Room - Top Right', 'Ganons Tower - Randomizer Room - Bottom Left', 'Ganons Tower - Randomizer Room - Bottom Right']
@@ -346,30 +347,30 @@ def global_rules(world, player):
set_rule(world.get_location('Ganons Tower - Bob\'s Torch', player), lambda state: state.has('Pegasus Boots', player)) set_rule(world.get_location('Ganons Tower - Bob\'s Torch', player), lambda state: state.has('Pegasus Boots', player))
set_rule(world.get_entrance('Ganons Tower (Tile Room)', player), lambda state: state.has('Cane of Somaria', player)) set_rule(world.get_entrance('Ganons Tower (Tile Room)', player), lambda state: state.has('Cane of Somaria', player))
set_rule(world.get_entrance('Ganons Tower (Hookshot Room)', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has('Pegasus Boots', player))) set_rule(world.get_entrance('Ganons Tower (Hookshot Room)', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has('Pegasus Boots', player)))
set_rule(world.get_entrance('Ganons Tower (Map Room)', player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 4) or ( set_rule(world.get_entrance('Ganons Tower (Map Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or (
item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player), ('Small Key (Ganons Tower)', player)] and state.has_key('Small Key (Ganons Tower)', player, 3))) item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player), ('Small Key (Ganons Tower)', player)] and state._lttp_has_key('Small Key (Ganons Tower)', player, 3)))
if world.accessibility[player] != 'locations': if world.accessibility[player] != 'locations':
set_always_allow(world.get_location('Ganons Tower - Map Chest', player), lambda state, item: item.name == 'Small Key (Ganons Tower)' and item.player == player and state.has_key('Small Key (Ganons Tower)', player, 3) and state.can_reach('Ganons Tower (Hookshot Room)', 'region', player)) set_always_allow(world.get_location('Ganons Tower - Map Chest', player), lambda state, item: item.name == 'Small Key (Ganons Tower)' and item.player == player and state._lttp_has_key('Small Key (Ganons Tower)', player, 3) and state.can_reach('Ganons Tower (Hookshot Room)', 'region', player))
# It is possible to need more than 2 keys to get through this entrance if you spend keys elsewhere. We reflect this in the chest requirements. # It is possible to need more than 2 keys to get through this entrance if you spend keys elsewhere. We reflect this in the chest requirements.
# However we need to leave these at the lower values to derive that with 3 keys it is always possible to reach Bob and Ice Armos. # However we need to leave these at the lower values to derive that with 3 keys it is always possible to reach Bob and Ice Armos.
set_rule(world.get_entrance('Ganons Tower (Double Switch Room)', player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 2)) set_rule(world.get_entrance('Ganons Tower (Double Switch Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 2))
# It is possible to need more than 3 keys .... # It is possible to need more than 3 keys ....
set_rule(world.get_entrance('Ganons Tower (Firesnake Room)', player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 3)) set_rule(world.get_entrance('Ganons Tower (Firesnake Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3))
#The actual requirements for these rooms to avoid key-lock #The actual requirements for these rooms to avoid key-lock
set_rule(world.get_location('Ganons Tower - Firesnake Room', player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 3) or (( set_rule(world.get_location('Ganons Tower - Firesnake Room', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3) or ((
item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) or item_in_locations(state, 'Small Key (Ganons Tower)', player, [('Ganons Tower - Firesnake Room', player)])) and state.has_key('Small Key (Ganons Tower)', player, 2))) item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) or item_in_locations(state, 'Small Key (Ganons Tower)', player, [('Ganons Tower - Firesnake Room', player)])) and state._lttp_has_key('Small Key (Ganons Tower)', player, 2)))
for location in randomizer_room_chests: for location in randomizer_room_chests:
set_rule(world.get_location(location, player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 4) or ( set_rule(world.get_location(location, player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or (
item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) and state.has_key('Small Key (Ganons Tower)', player, 3))) item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 3)))
# Once again it is possible to need more than 3 keys... # Once again it is possible to need more than 3 keys...
set_rule(world.get_entrance('Ganons Tower (Tile Room) Key Door', player), lambda state: state.has_key('Small Key (Ganons Tower)', player, 3) and state.has('Fire Rod', player)) set_rule(world.get_entrance('Ganons Tower (Tile Room) Key Door', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3) and state.has('Fire Rod', player))
# Actual requirements # Actual requirements
for location in compass_room_chests: for location in compass_room_chests:
set_rule(world.get_location(location, player), lambda state: state.has('Fire Rod', player) and (state.has_key('Small Key (Ganons Tower)', player, 4) or ( set_rule(world.get_location(location, player), lambda state: state.has('Fire Rod', player) and (state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or (
item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(compass_room_chests, [player] * len(compass_room_chests))) and state.has_key('Small Key (Ganons Tower)', player, 3)))) item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(compass_room_chests, [player] * len(compass_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 3))))
set_rule(world.get_location('Ganons Tower - Big Chest', player), lambda state: state.has('Big Key (Ganons Tower)', player)) set_rule(world.get_location('Ganons Tower - Big Chest', player), lambda state: state.has('Big Key (Ganons Tower)', player))
@@ -388,9 +389,9 @@ def global_rules(world, player):
set_rule(world.get_entrance('Ganons Tower Torch Rooms', player), set_rule(world.get_entrance('Ganons Tower Torch Rooms', player),
lambda state: state.has_fire_source(player) and state.world.get_entrance('Ganons Tower Torch Rooms', player).parent_region.dungeon.bosses['middle'].can_defeat(state)) lambda state: state.has_fire_source(player) and state.world.get_entrance('Ganons Tower Torch Rooms', player).parent_region.dungeon.bosses['middle'].can_defeat(state))
set_rule(world.get_location('Ganons Tower - Pre-Moldorm Chest', player), set_rule(world.get_location('Ganons Tower - Pre-Moldorm Chest', player),
lambda state: state.has_key('Small Key (Ganons Tower)', player, 3)) lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3))
set_rule(world.get_entrance('Ganons Tower Moldorm Door', player), set_rule(world.get_entrance('Ganons Tower Moldorm Door', player),
lambda state: state.has_key('Small Key (Ganons Tower)', player, 4)) lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 4))
set_rule(world.get_entrance('Ganons Tower Moldorm Gap', player), set_rule(world.get_entrance('Ganons Tower Moldorm Gap', player),
lambda state: state.has('Hookshot', player) and state.world.get_entrance('Ganons Tower Moldorm Gap', player).parent_region.dungeon.bosses['top'].can_defeat(state)) lambda state: state.has('Hookshot', player) and state.world.get_entrance('Ganons Tower Moldorm Gap', player).parent_region.dungeon.bosses['top'].can_defeat(state))
set_defeat_dungeon_boss_rule(world.get_location('Agahnim 2', player)) set_defeat_dungeon_boss_rule(world.get_location('Agahnim 2', player))
@@ -799,14 +800,14 @@ def add_conditional_lamps(world, player):
def open_rules(world, player): def open_rules(world, player):
# softlock protection as you can reach the sewers small key door with a guard drop key # softlock protection as you can reach the sewers small key door with a guard drop key
set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player), set_rule(world.get_location('Hyrule Castle - Boomerang Chest', player),
lambda state: state.has_key('Small Key (Hyrule Castle)', player)) lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player))
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player), set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
lambda state: state.has_key('Small Key (Hyrule Castle)', player)) lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player))
def swordless_rules(world, player): def swordless_rules(world, player):
set_rule(world.get_entrance('Agahnim 1', player), lambda state: (state.has('Hammer', player) or state.has('Fire Rod', player) or state.can_shoot_arrows(player) or state.has('Cane of Somaria', player)) and state.has_key('Small Key (Agahnims Tower)', player, 2)) set_rule(world.get_entrance('Agahnim 1', player), lambda state: (state.has('Hammer', player) or state.has('Fire Rod', player) or state.can_shoot_arrows(player) or state.has('Cane of Somaria', player)) and state._lttp_has_key('Small Key (Agahnims Tower)', player, 2))
set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state.has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player)) # no curtain set_rule(world.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 3) and state.has('Fire Rod', player)) # no curtain
set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player)) #in swordless mode bombos pads are present in the relevant parts of ice palace set_rule(world.get_entrance('Ice Palace Entrance Room', player), lambda state: state.has('Fire Rod', player) or state.has('Bombos', player)) #in swordless mode bombos pads are present in the relevant parts of ice palace
set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has('Hammer', player)) # need to damage ganon to get tiles to drop set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has('Hammer', player)) # need to damage ganon to get tiles to drop
@@ -849,10 +850,12 @@ def toss_junk_item(world, player):
def set_trock_key_rules(world, player): def set_trock_key_rules(world, player):
# First set all relevant locked doors to impassible. # First set all relevant locked doors to impassible.
for entrance in ['Turtle Rock Dark Room Staircase', 'Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock Pokey Room']: for entrance in ['Turtle Rock Dark Room Staircase', 'Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock Pokey Room', 'Turtle Rock Big Key Door']:
set_rule(world.get_entrance(entrance, player), lambda state: False) set_rule(world.get_entrance(entrance, player), lambda state: False)
all_state = world.get_all_state(True) all_state = world.get_all_state(use_cache=False)
all_state.reachable_regions[player] = set() # wipe reachable regions so that the locked doors actually work
all_state.stale[player] = True
# Check if each of the four main regions of the dungoen can be reached. The previous code section prevents key-costing moves within the dungeon. # Check if each of the four main regions of the dungoen can be reached. The previous code section prevents key-costing moves within the dungeon.
can_reach_back = all_state.can_reach(world.get_region('Turtle Rock (Eye Bridge)', player)) if world.can_access_trock_eyebridge[player] is None else world.can_access_trock_eyebridge[player] can_reach_back = all_state.can_reach(world.get_region('Turtle Rock (Eye Bridge)', player)) if world.can_access_trock_eyebridge[player] is None else world.can_access_trock_eyebridge[player]
@@ -877,28 +880,32 @@ def set_trock_key_rules(world, player):
# The following represent the common key rules. # The following represent the common key rules.
# Big key door requires the big key, obviously. We removed this rule in the previous section to flag front_locked_locations correctly,
# otherwise crystaroller room might not be properly marked as reachable through the back.
set_rule(world.get_entrance('Turtle Rock Big Key Door', player), lambda state: state.has('Big Key (Turtle Rock)', player))
# No matter what, the key requirement for going from the middle to the bottom should be three keys. # No matter what, the key requirement for going from the middle to the bottom should be three keys.
set_rule(world.get_entrance('Turtle Rock Dark Room Staircase', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 3)) set_rule(world.get_entrance('Turtle Rock Dark Room Staircase', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3))
# Now we need to set rules based on which entrances we have access to. The most important point is whether we have back access. If we have back access, we # Now we need to set rules based on which entrances we have access to. The most important point is whether we have back access. If we have back access, we
# might open all the locked doors in any order so we need maximally restrictive rules. # might open all the locked doors in any order so we need maximally restrictive rules.
if can_reach_back: if can_reach_back:
set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: (state.has_key('Small Key (Turtle Rock)', player, 4) or item_name(state, 'Turtle Rock - Big Key Chest', player) == ('Small Key (Turtle Rock)', player))) set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: (state._lttp_has_key('Small Key (Turtle Rock)', player, 4) or item_name(state, 'Turtle Rock - Big Key Chest', player) == ('Small Key (Turtle Rock)', player)))
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 4)) set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 4))
# Only consider wasting the key on the Trinexx door for going from the front entrance to middle section. If other key doors are accessible, then these doors can be avoided # Only consider wasting the key on the Trinexx door for going from the front entrance to middle section. If other key doors are accessible, then these doors can be avoided
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 3)) set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3))
set_rule(world.get_entrance('Turtle Rock Pokey Room', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 2)) set_rule(world.get_entrance('Turtle Rock Pokey Room', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2))
else: else:
# Middle to front requires 2 keys if the back is locked, otherwise 4 # Middle to front requires 2 keys if the back is locked, otherwise 4
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 2) set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2)
if item_in_locations(state, 'Big Key (Turtle Rock)', player, front_locked_locations) if item_in_locations(state, 'Big Key (Turtle Rock)', player, front_locked_locations)
else state.has_key('Small Key (Turtle Rock)', player, 4)) else state._lttp_has_key('Small Key (Turtle Rock)', player, 4))
# Front to middle requires 2 keys (if the middle is accessible then these doors can be avoided, otherwise no keys can be wasted) # Front to middle requires 2 keys (if the middle is accessible then these doors can be avoided, otherwise no keys can be wasted)
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 2)) set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2))
set_rule(world.get_entrance('Turtle Rock Pokey Room', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, 1)) set_rule(world.get_entrance('Turtle Rock Pokey Room', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 1))
set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: state.has_key('Small Key (Turtle Rock)', player, tr_big_key_chest_keys_needed(state))) set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, tr_big_key_chest_keys_needed(state)))
def tr_big_key_chest_keys_needed(state): def tr_big_key_chest_keys_needed(state):
# This function handles the key requirements for the TR Big Chest in the situations it having the Big Key should logically require 2 keys, small key # This function handles the key requirements for the TR Big Chest in the situations it having the Big Key should logically require 2 keys, small key
@@ -911,14 +918,14 @@ def set_trock_key_rules(world, player):
return 4 return 4
# If TR is only accessible from the middle, the big key must be further restricted to prevent softlock potential # If TR is only accessible from the middle, the big key must be further restricted to prevent softlock potential
if not can_reach_front and not world.keyshuffle[player]: if not can_reach_front and not world.smallkey_shuffle[player]:
# Must not go in the Big Key Chest - only 1 other chest available and 2+ keys required for all other chests # Must not go in the Big Key Chest - only 1 other chest available and 2+ keys required for all other chests
forbid_item(world.get_location('Turtle Rock - Big Key Chest', player), 'Big Key (Turtle Rock)', player) forbid_item(world.get_location('Turtle Rock - Big Key Chest', player), 'Big Key (Turtle Rock)', player)
if not can_reach_big_chest: if not can_reach_big_chest:
# Must not go in the Chain Chomps chest - only 2 other chests available and 3+ keys required for all other chests # Must not go in the Chain Chomps chest - only 2 other chests available and 3+ keys required for all other chests
forbid_item(world.get_location('Turtle Rock - Chain Chomps', player), 'Big Key (Turtle Rock)', player) forbid_item(world.get_location('Turtle Rock - Chain Chomps', player), 'Big Key (Turtle Rock)', player)
if world.accessibility[player] == 'locations' and world.goal[player] != 'icerodhunt': if world.accessibility[player] == 'locations' and world.goal[player] != 'icerodhunt':
if world.bigkeyshuffle[player] and can_reach_big_chest: if world.bigkey_shuffle[player] and can_reach_big_chest:
# Must not go in the dungeon - all 3 available chests (Chomps, Big Chest, Crystaroller) must be keys to access laser bridge, and the big key is required first # Must not go in the dungeon - all 3 available chests (Chomps, Big Chest, Crystaroller) must be keys to access laser bridge, and the big key is required first
for location in ['Turtle Rock - Chain Chomps', 'Turtle Rock - Compass Chest', for location in ['Turtle Rock - Chain Chomps', 'Turtle Rock - Compass Chest',
'Turtle Rock - Roller Room - Left', 'Turtle Rock - Roller Room - Right']: 'Turtle Rock - Roller Room - Left', 'Turtle Rock - Roller Room - Right']:

View File

@@ -6,6 +6,7 @@ import logging
from worlds.alttp.SubClasses import ALttPLocation from worlds.alttp.SubClasses import ALttPLocation
from worlds.alttp.EntranceShuffle import door_addresses from worlds.alttp.EntranceShuffle import door_addresses
from worlds.alttp.Items import item_name_groups, item_table, ItemFactory, trap_replaceable, GetBeemizerItem from worlds.alttp.Items import item_name_groups, item_table, ItemFactory, trap_replaceable, GetBeemizerItem
from worlds.alttp.Options import smallkey_shuffle
from Utils import int16_as_bytes from Utils import int16_as_bytes
logger = logging.getLogger("Shops") logger = logging.getLogger("Shops")
@@ -22,6 +23,11 @@ class Shop():
slots: int = 3 # slot count is not dynamic in asm, however inventory can have None as empty slots slots: int = 3 # slot count is not dynamic in asm, however inventory can have None as empty slots
blacklist: Set[str] = set() # items that don't work, todo: actually check against this blacklist: Set[str] = set() # items that don't work, todo: actually check against this
type = ShopType.Shop type = ShopType.Shop
slot_names: Dict[int, str] = {
0: "Left",
1: "Center",
2: "Right"
}
def __init__(self, region, room_id: int, shopkeeper_config: int, custom: bool, locked: bool, sram_offset: int): def __init__(self, region, room_id: int, shopkeeper_config: int, custom: bool, locked: bool, sram_offset: int):
self.region = region self.region = region
@@ -131,23 +137,22 @@ shop_class_mapping = {ShopType.UpgradeShop: UpgradeShop,
def FillDisabledShopSlots(world): def FillDisabledShopSlots(world):
shop_slots: Set[ALttPLocation] = {location for shop_locations in (shop.region.locations for shop in world.shops) shop_slots: Set[ALttPLocation] = {location for shop_locations in (shop.region.locations for shop in world.shops)
for location in shop_locations if location.shop_slot and location.shop_slot_disabled} for location in shop_locations
if location.shop_slot is not None and location.shop_slot_disabled}
for location in shop_slots: for location in shop_slots:
location.shop_slot_disabled = True location.shop_slot_disabled = True
slot_num = int(location.name[-1]) - 1
shop: Shop = location.parent_region.shop shop: Shop = location.parent_region.shop
location.item = ItemFactory(shop.inventory[slot_num]['item'], location.player) location.item = ItemFactory(shop.inventory[location.shop_slot]['item'], location.player)
location.item_rule = lambda item: item.name == location.item.name and item.player == location.player location.item_rule = lambda item: item.name == location.item.name and item.player == location.player
def ShopSlotFill(world): def ShopSlotFill(world):
shop_slots: Set[ALttPLocation] = {location for shop_locations in (shop.region.locations for shop in world.shops) shop_slots: Set[ALttPLocation] = {location for shop_locations in (shop.region.locations for shop in world.shops)
for location in shop_locations if location.shop_slot} for location in shop_locations if location.shop_slot is not None}
removed = set() removed = set()
for location in shop_slots: for location in shop_slots:
slot_num = int(location.name[-1]) - 1
shop: Shop = location.parent_region.shop shop: Shop = location.parent_region.shop
if not shop.can_push_inventory(slot_num) or location.shop_slot_disabled: if not shop.can_push_inventory(location.shop_slot) or location.shop_slot_disabled:
location.shop_slot_disabled = True location.shop_slot_disabled = True
removed.add(location) removed.add(location)
@@ -155,6 +160,7 @@ def ShopSlotFill(world):
shop_slots -= removed shop_slots -= removed
if shop_slots: if shop_slots:
logger.info("Filling LttP Shop Slots")
del shop_slots del shop_slots
from Fill import swap_location_item from Fill import swap_location_item
@@ -179,7 +185,7 @@ def ShopSlotFill(world):
shops_per_sphere.append(current_shops_slots) shops_per_sphere.append(current_shops_slots)
candidates_per_sphere.append(current_candidates) candidates_per_sphere.append(current_candidates)
for location in sphere: for location in sphere:
if location.shop_slot: if location.shop_slot is not None:
if not location.shop_slot_disabled: if not location.shop_slot_disabled:
current_shops_slots.append(location) current_shops_slots.append(location)
elif not location.locked and not location.item.name in blacklist_words: elif not location.locked and not location.item.name in blacklist_words:
@@ -229,7 +235,7 @@ def ShopSlotFill(world):
else: else:
price = world.random.randrange(8, 56) price = world.random.randrange(8, 56)
shop.push_inventory(int(location.name[-1]) - 1, item_name, price * 5, 1, shop.push_inventory(location.shop_slot, item_name, price * 5, 1,
location.item.player if location.item.player != location.player else 0) location.item.player if location.item.player != location.player else 0)
@@ -266,7 +272,7 @@ def create_shops(world, player: int):
# make sure that blue potion is available in inverted, special case locked = None; lock when done. # make sure that blue potion is available in inverted, special case locked = None; lock when done.
player_shop_table["Dark Lake Hylia Shop"] = \ player_shop_table["Dark Lake Hylia Shop"] = \
player_shop_table["Dark Lake Hylia Shop"]._replace(items=_inverted_hylia_shop_defaults, locked=None) player_shop_table["Dark Lake Hylia Shop"]._replace(items=_inverted_hylia_shop_defaults, locked=None)
chance_100 = int(world.retro[player])*0.25+int(world.keyshuffle[player] == "universal") * 0.5 chance_100 = int(world.retro[player])*0.25+int(world.smallkey_shuffle[player] == smallkey_shuffle.option_universal) * 0.5
for region_name, (room_id, type, shopkeeper, custom, locked, inventory, sram_offset) in player_shop_table.items(): for region_name, (room_id, type, shopkeeper, custom, locked, inventory, sram_offset) in player_shop_table.items():
region = world.get_region(region_name, player) region = world.get_region(region_name, player)
shop: Shop = shop_class_mapping[type](region, room_id, shopkeeper, custom, locked, sram_offset) shop: Shop = shop_class_mapping[type](region, room_id, shopkeeper, custom, locked, sram_offset)
@@ -278,10 +284,10 @@ def create_shops(world, player: int):
for index, item in enumerate(inventory): for index, item in enumerate(inventory):
shop.add_inventory(index, *item) shop.add_inventory(index, *item)
if not locked and num_slots: if not locked and num_slots:
slot_name = "{} Slot {}".format(region.name, index + 1) slot_name = f"{region.name} {shop.slot_names[index]}"
loc = ALttPLocation(player, slot_name, address=shop_table_by_location[slot_name], loc = ALttPLocation(player, slot_name, address=shop_table_by_location[slot_name],
parent=region, hint_text="for sale") parent=region, hint_text="for sale")
loc.shop_slot = True loc.shop_slot = index
loc.locked = True loc.locked = True
if single_purchase_slots.pop(): if single_purchase_slots.pop():
if world.goal[player] != 'icerodhunt': if world.goal[player] != 'icerodhunt':
@@ -337,9 +343,10 @@ total_shop_slots = len(shop_table) * 3
total_dynamic_shop_slots = sum(3 for shopname, data in shop_table.items() if not data[4]) # data[4] -> locked total_dynamic_shop_slots = sum(3 for shopname, data in shop_table.items() if not data[4]) # data[4] -> locked
SHOP_ID_START = 0x400000 SHOP_ID_START = 0x400000
shop_table_by_location_id = {cnt: s for cnt, s in enumerate( shop_table_by_location_id = dict(enumerate(
(f"{name} Slot {num}" for name in [key for key, value in sorted(shop_table.items(), key=lambda item: item[1].sram_offset)] (f"{name} {Shop.slot_names[num]}" for name, shop_data in sorted(shop_table.items(), key=lambda item: item[1].sram_offset)
for num in range(1, 4)), start=SHOP_ID_START)} for num in range(3)), start=SHOP_ID_START))
shop_table_by_location_id[(SHOP_ID_START + total_shop_slots)] = "Old Man Sword Cave" shop_table_by_location_id[(SHOP_ID_START + total_shop_slots)] = "Old Man Sword Cave"
shop_table_by_location_id[(SHOP_ID_START + total_shop_slots + 1)] = "Take-Any #1" shop_table_by_location_id[(SHOP_ID_START + total_shop_slots + 1)] = "Take-Any #1"
shop_table_by_location_id[(SHOP_ID_START + total_shop_slots + 2)] = "Take-Any #2" shop_table_by_location_id[(SHOP_ID_START + total_shop_slots + 2)] = "Take-Any #2"
@@ -365,13 +372,13 @@ def set_up_shops(world, player: int):
rss = world.get_region('Red Shield Shop', player).shop rss = world.get_region('Red Shield Shop', player).shop
replacement_items = [['Red Potion', 150], ['Green Potion', 75], ['Blue Potion', 200], ['Bombs (10)', 50], replacement_items = [['Red Potion', 150], ['Green Potion', 75], ['Blue Potion', 200], ['Bombs (10)', 50],
['Blue Shield', 50], ['Small Heart', 10]] # Can't just replace the single arrow with 10 arrows as retro doesn't need them. ['Blue Shield', 50], ['Small Heart', 10]] # Can't just replace the single arrow with 10 arrows as retro doesn't need them.
if world.keyshuffle[player] == "universal": if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
replacement_items.append(['Small Key (Universal)', 100]) replacement_items.append(['Small Key (Universal)', 100])
replacement_item = world.random.choice(replacement_items) replacement_item = world.random.choice(replacement_items)
rss.add_inventory(2, 'Single Arrow', 80, 1, replacement_item[0], replacement_item[1]) rss.add_inventory(2, 'Single Arrow', 80, 1, replacement_item[0], replacement_item[1])
rss.locked = True rss.locked = True
if world.keyshuffle[player] == "universal" or world.retro[player]: if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal or world.retro[player]:
for shop in world.random.sample([s for s in world.shops if for shop in world.random.sample([s for s in world.shops if
s.custom and not s.locked and s.type == ShopType.Shop and s.region.player == player], s.custom and not s.locked and s.type == ShopType.Shop and s.region.player == player],
5): 5):
@@ -379,7 +386,7 @@ def set_up_shops(world, player: int):
slots = [0, 1, 2] slots = [0, 1, 2]
world.random.shuffle(slots) world.random.shuffle(slots)
slots = iter(slots) slots = iter(slots)
if world.keyshuffle[player] == "universal": if world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
shop.add_inventory(next(slots), 'Small Key (Universal)', 100) shop.add_inventory(next(slots), 'Small Key (Universal)', 100)
if world.retro[player]: if world.retro[player]:
shop.push_inventory(next(slots), 'Single Arrow', 80) shop.push_inventory(next(slots), 'Single Arrow', 80)

View File

@@ -18,6 +18,7 @@ class ALttPLocation(Location):
class ALttPItem(Item): class ALttPItem(Item):
game: str = "A Link to the Past" game: str = "A Link to the Past"
dungeon = None
def __init__(self, name, player, advancement=False, type=None, item_code=None, pedestal_hint=None, pedestal_credit=None, def __init__(self, name, player, advancement=False, type=None, item_code=None, pedestal_hint=None, pedestal_credit=None,
sick_kid_credit=None, zora_credit=None, witch_credit=None, flute_boy_credit=None, hint_text=None): sick_kid_credit=None, zora_credit=None, witch_credit=None, flute_boy_credit=None, hint_text=None):
@@ -29,4 +30,33 @@ class ALttPItem(Item):
self.zora_credit_text = zora_credit self.zora_credit_text = zora_credit
self.magicshop_credit_text = witch_credit self.magicshop_credit_text = witch_credit
self.fluteboy_credit_text = flute_boy_credit self.fluteboy_credit_text = flute_boy_credit
self._hint_text = hint_text self._hint_text = hint_text
@property
def crystal(self) -> bool:
return self.type == 'Crystal'
@property
def smallkey(self) -> bool:
return self.type == 'SmallKey'
@property
def bigkey(self) -> bool:
return self.type == 'BigKey'
@property
def map(self) -> bool:
return self.type == 'Map'
@property
def compass(self) -> bool:
return self.type == 'Compass'
@property
def dungeon_item(self) -> Optional[str]:
if self.type in {"SmallKey", "BigKey", "Map", "Compass"}:
return self.type
@property
def locked_dungeon_item(self):
return self.location.locked and self.dungeon_item

View File

@@ -127,7 +127,7 @@ Triforce_texts = [
"\n Honk.", "\n Honk.",
] ]
BombShop2_texts = ['Bombs!\nBombs!\nBiggest!\nBestest!\nGreatest!\nBoomiest!'] BombShop2_texts = ['Bombs!\nBombs!\nBiggest!\nBestest!\nGreatest!\nBoomiest!']
Sahasrahla2_texts = ['You already got my item, idiot.', 'Why are you still talking to me?', 'Have you met my brother, Hasarahshla?'] Sahasrahla2_texts = ['You already have my item.', 'Why are you still talking to me?', 'Have you met my brother, Hasarahshla?']
Blind_texts = [ Blind_texts = [
"I hate insect\npuns, they\nreally bug me.", "I hate insect\npuns, they\nreally bug me.",
"I haven't seen\nthe eye doctor\nin years.", "I haven't seen\nthe eye doctor\nin years.",

View File

@@ -1,38 +1,89 @@
import random import random
import logging
import os
import threading
import typing
from BaseClasses import Item, CollectionState from BaseClasses import Item, CollectionState
from .SubClasses import ALttPItem from .SubClasses import ALttPItem
from ..AutoWorld import World from ..AutoWorld import World, LogicMixin
from .Options import alttp_options from .Options import alttp_options, smallkey_shuffle
from .Items import as_dict_item_table, item_name_groups, item_table from .Items import as_dict_item_table, item_name_groups, item_table
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
from .Rules import set_rules from .Rules import set_rules
from .ItemPool import generate_itempool from .ItemPool import generate_itempool, difficulties
from .Shops import create_shops from .Shops import create_shops, ShopSlotFill
from .Dungeons import create_dungeons from .Dungeons import create_dungeons
from .Rom import LocalRom, patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, get_hash_string
import Patch
from .InvertedRegions import create_inverted_regions, mark_dark_world_regions from .InvertedRegions import create_inverted_regions, mark_dark_world_regions
from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect
lttp_logger = logging.getLogger("A Link to the Past")
class ALTTPWorld(World): class ALTTPWorld(World):
"""
The Legend of Zelda: A Link to the Past is an action/adventure game. Take on the role of
Link, a boy who is destined to save the land of Hyrule. Delve through three palaces and nine
dungeons on your quest to rescue the descendents of the seven wise men and defeat the evil
Ganon!
"""
game: str = "A Link to the Past" game: str = "A Link to the Past"
options = alttp_options options = alttp_options
topology_present = True topology_present = True
item_name_groups = item_name_groups item_name_groups = item_name_groups
item_names = frozenset(item_table)
location_names = frozenset(lookup_name_to_id)
hint_blacklist = {"Triforce"} hint_blacklist = {"Triforce"}
item_name_to_id = {name: data.item_code for name, data in item_table.items() if type(data.item_code) == int} item_name_to_id = {name: data.item_code for name, data in item_table.items() if type(data.item_code) == int}
location_name_to_id = lookup_name_to_id location_name_to_id = lookup_name_to_id
data_version = 7 data_version = 8
remote_items: bool = False remote_items: bool = False
set_rules = set_rules set_rules = set_rules
create_items = generate_itempool create_items = generate_itempool
def __init__(self, *args, **kwargs):
self.dungeon_local_item_names = set()
self.dungeon_specific_item_names = set()
self.rom_name_available_event = threading.Event()
super(ALTTPWorld, self).__init__(*args, **kwargs)
def generate_early(self):
player = self.player
world = self.world
# system for sharing ER layouts
world.er_seeds[player] = str(world.random.randint(0, 2 ** 64))
if "-" in world.shuffle[player]:
shuffle, seed = world.shuffle[player].split("-", 1)
world.shuffle[player] = shuffle
if shuffle == "vanilla":
world.er_seeds[player] = "vanilla"
elif seed.startswith("group-") or world.is_race:
world.er_seeds[player] = get_same_seed(world, (
shuffle, seed, world.retro[player], world.mode[player], world.logic[player]))
else: # not a race or group seed, use set seed as is.
world.er_seeds[player] = seed
elif world.shuffle[player] == "vanilla":
world.er_seeds[player] = "vanilla"
for dungeon_item in ["smallkey_shuffle", "bigkey_shuffle", "compass_shuffle", "map_shuffle"]:
option = getattr(world, dungeon_item)[player]
if option == "own_world":
world.local_items[player] |= self.item_name_groups[option.item_name_group]
elif option == "different_world":
world.non_local_items[player] |= self.item_name_groups[option.item_name_group]
elif option.in_dungeon:
self.dungeon_local_item_names |= self.item_name_groups[option.item_name_group]
if option == "original_dungeon":
self.dungeon_specific_item_names |= self.item_name_groups[option.item_name_group]
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
def create_regions(self): def create_regions(self):
player = self.player player = self.player
world = self.world world = self.world
@@ -58,7 +109,6 @@ class ALTTPWorld(World):
create_shops(world, player) create_shops(world, player)
create_dungeons(world, player) create_dungeons(world, player)
if world.logic[player] not in ["noglitches", "minorglitches"] and world.shuffle[player] in \ if world.logic[player] not in ["noglitches", "minorglitches"] and world.shuffle[player] in \
{"vanilla", "dungeonssimple", "dungeonsfull", "simple", "restricted", "full"}: {"vanilla", "dungeonssimple", "dungeonsfull", "simple", "restricted", "full"}:
world.fix_fake_world[player] = False world.fix_fake_world[player] = False
@@ -77,59 +127,197 @@ class ALTTPWorld(World):
world.random = old_random world.random = old_random
plando_connect(world, player) plando_connect(world, player)
def collect(self, state: CollectionState, item: Item) -> bool: def collect_item(self, state: CollectionState, item: Item, remove=False):
if item.name.startswith('Progressive '): item_name = item.name
if 'Sword' in item.name: if item_name.startswith('Progressive '):
if state.has('Golden Sword', item.player): if remove:
pass if 'Sword' in item_name:
elif state.has('Tempered Sword', item.player) and self.world.difficulty_requirements[ if state.has('Golden Sword', item.player):
item.player].progressive_sword_limit >= 4: return 'Golden Sword'
state.prog_items['Golden Sword', item.player] += 1 elif state.has('Tempered Sword', item.player):
return True return 'Tempered Sword'
elif state.has('Master Sword', item.player) and self.world.difficulty_requirements[ elif state.has('Master Sword', item.player):
item.player].progressive_sword_limit >= 3: return 'Master Sword'
state.prog_items['Tempered Sword', item.player] += 1 elif state.has('Fighter Sword', item.player):
return True return 'Fighter Sword'
elif state.has('Fighter Sword', item.player) and self.world.difficulty_requirements[item.player].progressive_sword_limit >= 2: else:
state.prog_items['Master Sword', item.player] += 1 return None
return True elif 'Glove' in item.name:
elif self.world.difficulty_requirements[item.player].progressive_sword_limit >= 1: if state.has('Titans Mitts', item.player):
state.prog_items['Fighter Sword', item.player] += 1 return 'Titans Mitts'
return True elif state.has('Power Glove', item.player):
elif 'Glove' in item.name: return 'Power Glove'
if state.has('Titans Mitts', item.player): else:
pass return None
elif state.has('Power Glove', item.player): elif 'Shield' in item_name:
state.prog_items['Titans Mitts', item.player] += 1 if state.has('Mirror Shield', item.player):
return True return 'Mirror Shield'
else: elif state.has('Red Shield', item.player):
state.prog_items['Power Glove', item.player] += 1 return 'Red Shield'
return True elif state.has('Blue Shield', item.player):
elif 'Shield' in item.name: return 'Blue Shield'
if state.has('Mirror Shield', item.player): else:
pass return None
elif state.has('Red Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 3: elif 'Bow' in item_name:
state.prog_items['Mirror Shield', item.player] += 1 if state.has('Silver Bow', item.player):
return True return 'Silver Bow'
elif state.has('Blue Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 2: elif state.has('Bow', item.player):
state.prog_items['Red Shield', item.player] += 1 return 'Bow'
return True else:
elif self.world.difficulty_requirements[item.player].progressive_shield_limit >= 1: return None
state.prog_items['Blue Shield', item.player] += 1 else:
return True if 'Sword' in item_name:
elif 'Bow' in item.name: if state.has('Golden Sword', item.player):
if state.has('Silver', item.player): pass
pass elif state.has('Tempered Sword', item.player) and self.world.difficulty_requirements[
elif state.has('Bow', item.player) and self.world.difficulty_requirements[item.player].progressive_bow_limit >= 2: item.player].progressive_sword_limit >= 4:
state.prog_items['Silver Bow', item.player] += 1 return 'Golden Sword'
return True elif state.has('Master Sword', item.player) and self.world.difficulty_requirements[
elif self.world.difficulty_requirements[item.player].progressive_bow_limit >= 1: item.player].progressive_sword_limit >= 3:
state.prog_items['Bow', item.player] += 1 return 'Tempered Sword'
return True elif state.has('Fighter Sword', item.player) and self.world.difficulty_requirements[item.player].progressive_sword_limit >= 2:
elif item.advancement or item.smallkey or item.bigkey: return 'Master Sword'
state.prog_items[item.name, item.player] += 1 elif self.world.difficulty_requirements[item.player].progressive_sword_limit >= 1:
return True return 'Fighter Sword'
return False elif 'Glove' in item_name:
if state.has('Titans Mitts', item.player):
return
elif state.has('Power Glove', item.player):
return 'Titans Mitts'
else:
return 'Power Glove'
elif 'Shield' in item_name:
if state.has('Mirror Shield', item.player):
return
elif state.has('Red Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 3:
return 'Mirror Shield'
elif state.has('Blue Shield', item.player) and self.world.difficulty_requirements[item.player].progressive_shield_limit >= 2:
return 'Red Shield'
elif self.world.difficulty_requirements[item.player].progressive_shield_limit >= 1:
return 'Blue Shield'
elif 'Bow' in item_name:
if state.has('Silver Bow', item.player):
return
elif state.has('Bow', item.player) and (self.world.difficulty_requirements[item.player].progressive_bow_limit >= 2
or self.world.logic[item.player] == 'noglitches'
or self.world.swordless[item.player]): # modes where silver bow is always required for ganon
return 'Silver Bow'
elif self.world.difficulty_requirements[item.player].progressive_bow_limit >= 1:
return 'Bow'
elif item.advancement:
return item_name
def pre_fill(self):
from Fill import fill_restrictive, FillError
attempts = 5
world = self.world
player = self.player
all_state = world.get_all_state(use_cache=True)
crystals = [self.create_item(name) for name in ['Red Pendant', 'Blue Pendant', 'Green Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 7', 'Crystal 5', 'Crystal 6']]
crystal_locations = [world.get_location('Turtle Rock - Prize', player),
world.get_location('Eastern Palace - Prize', player),
world.get_location('Desert Palace - Prize', player),
world.get_location('Tower of Hera - Prize', player),
world.get_location('Palace of Darkness - Prize', player),
world.get_location('Thieves\' Town - Prize', player),
world.get_location('Skull Woods - Prize', player),
world.get_location('Swamp Palace - Prize', player),
world.get_location('Ice Palace - Prize', player),
world.get_location('Misery Mire - Prize', player)]
placed_prizes = {loc.item.name for loc in crystal_locations if loc.item}
unplaced_prizes = [crystal for crystal in crystals if crystal.name not in placed_prizes]
empty_crystal_locations = [loc for loc in crystal_locations if not loc.item]
for attempt in range(attempts):
try:
prizepool = unplaced_prizes.copy()
prize_locs = empty_crystal_locations.copy()
world.random.shuffle(prize_locs)
fill_restrictive(world, all_state, prize_locs, prizepool, True, lock=True)
except FillError as e:
lttp_logger.exception("Failed to place dungeon prizes (%s). Will retry %s more times", e,
attempts - attempt)
for location in empty_crystal_locations:
location.item = None
continue
break
else:
raise FillError('Unable to place dungeon prizes')
@classmethod
def stage_pre_fill(cls, world):
from .Dungeons import fill_dungeons_restrictive
fill_dungeons_restrictive(cls, world)
@classmethod
def stage_post_fill(cls, world):
ShopSlotFill(world)
def generate_output(self, output_directory: str):
world = self.world
player = self.player
try:
use_enemizer = (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player]
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
or world.shufflepots[player] or world.bush_shuffle[player]
or world.killable_thieves[player])
rom = LocalRom(world.alttp_rom)
patch_rom(world, rom, player, use_enemizer)
if use_enemizer:
patch_enemizer(world, player, rom, world.enemizer, output_directory)
if world.is_race:
patch_race_rom(rom, world, player)
world.spoiler.hashes[player] = get_hash_string(rom.hash)
palettes_options = {
'dungeon': world.uw_palettes[player],
'overworld': world.ow_palettes[player],
'hud': world.hud_palettes[player],
'sword': world.sword_palettes[player],
'shield': world.shield_palettes[player],
'link': world.link_palettes[player]
}
palettes_options = {key: option.current_key for key, option in palettes_options.items()}
apply_rom_settings(rom, world.heartbeep[player].current_key,
world.heartcolor[player].current_key,
world.quickswap[player],
world.menuspeed[player].current_key,
world.music[player],
world.sprite[player],
palettes_options, world, player, True,
reduceflashing=world.reduceflashing[player] or world.is_race,
triforcehud=world.triforcehud[player].current_key)
outfilepname = f'_P{player}'
outfilepname += f"_{world.player_name[player].replace(' ', '_')}" \
if world.player_name[player] != 'Player%d' % player else ''
rompath = os.path.join(output_directory, f'AP_{world.seed_name}{outfilepname}.sfc')
rom.write_to_file(rompath, hide_enemizer=True)
Patch.create_patch_file(rompath, player=player, player_name=world.player_name[player])
os.unlink(rompath)
self.rom_name = rom.name
except:
raise
finally:
self.rom_name_available_event.set() # make sure threading continues and errors are collected
def modify_multidata(self, multidata: dict):
import base64
# wait for self.rom_name to be available.
self.rom_name_available_event.wait()
rom_name = getattr(self, "rom_name", None)
# we skip in case of error, so that the original error in the output thread is the one that gets raised
if rom_name:
new_name = base64.b64encode(bytes(self.rom_name)).decode()
payload = multidata["connect_names"][self.world.player_name[self.player]]
multidata["connect_names"][new_name] = payload
del (multidata["connect_names"][self.world.player_name[self.player]])
def get_required_client_version(self) -> tuple: def get_required_client_version(self) -> tuple:
return max((0, 1, 4), super(ALTTPWorld, self).get_required_client_version()) return max((0, 1, 4), super(ALTTPWorld, self).get_required_client_version())
@@ -137,4 +325,86 @@ class ALTTPWorld(World):
def create_item(self, name: str) -> Item: def create_item(self, name: str) -> Item:
return ALttPItem(name, self.player, **as_dict_item_table[name]) return ALttPItem(name, self.player, **as_dict_item_table[name])
@classmethod
def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool,
restitempool, fill_locations):
trash_counts = {}
standard_keyshuffle_players = set()
for player in world.get_game_players("A Link to the Past"):
if world.mode[player] == 'standard' and world.smallkey_shuffle[player] \
and world.smallkey_shuffle[player] != smallkey_shuffle.option_universal:
standard_keyshuffle_players.add(player)
if not world.ganonstower_vanilla[player] or \
world.logic[player] in {'owglitches', 'hybridglitches', "nologic"}:
pass
elif 'triforcehunt' in world.goal[player] and ('local' in world.goal[player] or world.players == 1):
trash_counts[player] = world.random.randint(world.crystals_needed_for_gt[player] * 2,
world.crystals_needed_for_gt[player] * 4)
else:
trash_counts[player] = world.random.randint(0, world.crystals_needed_for_gt[player] * 2)
# Make sure the escape small key is placed first in standard with key shuffle to prevent running out of spots
# TODO: this might be worthwhile to introduce as generic option for various games and then optimize it
if standard_keyshuffle_players:
viable = []
for location in world.get_locations():
if location.player in standard_keyshuffle_players \
and location.item is None \
and location.can_reach(world.state):
viable.append(location)
world.random.shuffle(viable)
for player in standard_keyshuffle_players:
key = world.create_item("Small Key (Hyrule Castle)", player)
loc = viable.pop()
loc.place_locked_item(key)
fill_locations.remove(loc)
world.random.shuffle(fill_locations)
# TODO: investigate not creating the key in the first place
progitempool[:] = [item for item in progitempool if
item.player not in standard_keyshuffle_players or
item.name != "Small Key (Hyrule Castle)"]
if trash_counts:
locations_mapping = {player: [] for player in trash_counts}
for location in fill_locations:
if 'Ganons Tower' in location.name and location.player in locations_mapping:
locations_mapping[location.player].append(location)
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:
spot_to_fill = gtower_locations.pop()
item_to_place = gt_item_pool.pop()
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_same_seed(world, seed_def: tuple) -> str:
seeds: typing.Dict[tuple, str] = getattr(world, "__named_seeds", {})
if seed_def in seeds:
return seeds[seed_def]
seeds[seed_def] = str(world.random.randint(0, 2 ** 64))
world.__named_seeds = seeds
return seeds[seed_def]
class ALttPLogic(LogicMixin):
def _lttp_has_key(self, item, player, count: int = 1):
if self.world.logic[player] == 'nologic':
return True
if self.world.smallkey_shuffle[player] == smallkey_shuffle.option_universal:
return self.can_buy_unlimited('Small Key (Universal)', player)
return self.prog_items[item, player] >= count

View File

@@ -49,7 +49,7 @@ def generate_mod(world, output_directory: str):
global data_final_template, locale_template, control_template, data_template global data_final_template, locale_template, control_template, data_template
with template_load_lock: with template_load_lock:
if not data_final_template: if not data_final_template:
mod_template_folder = Utils.local_path("data", "factorio", "mod_template") mod_template_folder = os.path.join(os.path.dirname(__file__), "data", "mod_template")
template_env: Optional[jinja2.Environment] = \ template_env: Optional[jinja2.Environment] = \
jinja2.Environment(loader=jinja2.FileSystemLoader([mod_template_folder])) jinja2.Environment(loader=jinja2.FileSystemLoader([mod_template_folder]))
data_template = template_env.get_template("data.lua") data_template = template_env.get_template("data.lua")
@@ -57,12 +57,12 @@ def generate_mod(world, output_directory: str):
locale_template = template_env.get_template(r"locale/en/locale.cfg") locale_template = template_env.get_template(r"locale/en/locale.cfg")
control_template = template_env.get_template("control.lua") control_template = template_env.get_template("control.lua")
# get data for templates # get data for templates
player_names = {x: multiworld.player_names[x][0] for x in multiworld.player_ids} player_names = {x: multiworld.player_name[x] for x in multiworld.player_ids}
locations = [] locations = []
for location in multiworld.get_filled_locations(player): for location in multiworld.get_filled_locations(player):
if location.address: if location.address:
locations.append((location.name, location.item.name, location.item.player, location.item.advancement)) locations.append((location.name, location.item.name, location.item.player, location.item.advancement))
mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.player_names[player][0]}" mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.player_name[player]}"
tech_cost_scale = {0: 0.1, tech_cost_scale = {0: 0.1,
1: 0.25, 1: 0.25,
2: 0.5, 2: 0.5,
@@ -70,15 +70,27 @@ def generate_mod(world, output_directory: str):
4: 2, 4: 2,
5: 5, 5: 5,
6: 10}[multiworld.tech_cost[player].value] 6: 10}[multiworld.tech_cost[player].value]
random = multiworld.slot_seeds[player]
def flop_random(low, high, base=None):
"""Guarentees 50% bwlo base and 50% above base, uniform distribution in each direction."""
if base:
distance = random.random()
if random.randint(0, 1):
return base + (high-base) * distance
else:
return base - (base-low) * distance
return random.uniform(low, high)
template_data = {"locations": locations, "player_names": player_names, "tech_table": tech_table, template_data = {"locations": locations, "player_names": player_names, "tech_table": tech_table,
"base_tech_table": base_tech_table, "tech_to_progressive_lookup": tech_to_progressive_lookup, "base_tech_table": base_tech_table, "tech_to_progressive_lookup": tech_to_progressive_lookup,
"mod_name": mod_name, "allowed_science_packs": multiworld.max_science_pack[player].get_allowed_packs(), "mod_name": mod_name, "allowed_science_packs": multiworld.max_science_pack[player].get_allowed_packs(),
"tech_cost_scale": tech_cost_scale, "custom_technologies": multiworld.worlds[player].custom_technologies, "tech_cost_scale": tech_cost_scale, "custom_technologies": multiworld.worlds[player].custom_technologies,
"tech_tree_layout_prerequisites": multiworld.tech_tree_layout_prerequisites[player], "tech_tree_layout_prerequisites": multiworld.tech_tree_layout_prerequisites[player],
"slot_name": multiworld.player_names[player][0], "seed_name": multiworld.seed_name, "slot_name": multiworld.player_name[player], "seed_name": multiworld.seed_name,
"starting_items": multiworld.starting_items[player], "recipes": recipes, "starting_items": multiworld.starting_items[player], "recipes": recipes,
"random": multiworld.slot_seeds[player], "static_nodes": multiworld.worlds[player].static_nodes, "random": random, "flop_random": flop_random,
"static_nodes": multiworld.worlds[player].static_nodes,
"recipe_time_scale": recipe_time_scales[multiworld.recipe_time[player].value], "recipe_time_scale": recipe_time_scales[multiworld.recipe_time[player].value],
"free_sample_blacklist": {item : 1 for item in free_sample_blacklist}, "free_sample_blacklist": {item : 1 for item in free_sample_blacklist},
"progressive_technology_table": {tech.name : tech.progressive for tech in "progressive_technology_table": {tech.name : tech.progressive for tech in
@@ -95,7 +107,7 @@ def generate_mod(world, output_directory: str):
mod_dir = os.path.join(output_directory, mod_name + "_" + Utils.__version__) mod_dir = os.path.join(output_directory, mod_name + "_" + Utils.__version__)
en_locale_dir = os.path.join(mod_dir, "locale", "en") en_locale_dir = os.path.join(mod_dir, "locale", "en")
os.makedirs(en_locale_dir, exist_ok=True) os.makedirs(en_locale_dir, exist_ok=True)
shutil.copytree(Utils.local_path("data", "factorio", "mod"), mod_dir, dirs_exist_ok=True) shutil.copytree(os.path.join(os.path.dirname(__file__), "data", "mod"), mod_dir, dirs_exist_ok=True)
with open(os.path.join(mod_dir, "data.lua"), "wt") as f: with open(os.path.join(mod_dir, "data.lua"), "wt") as f:
f.write(data_template_code) f.write(data_template_code)
with open(os.path.join(mod_dir, "data-final-fixes.lua"), "wt") as f: with open(os.path.join(mod_dir, "data-final-fixes.lua"), "wt") as f:

View File

@@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
import typing import typing
from Options import Choice, OptionDict, Option, DefaultOnToggle from Options import Choice, OptionDict, Option, DefaultOnToggle, Range
from schema import Schema, Optional, And, Or from schema import Schema, Optional, And, Or
# schema helpers # schema helpers
@@ -11,6 +11,7 @@ LuaBool = Or(bool, And(int, lambda n: n in (0, 1)))
class MaxSciencePack(Choice): class MaxSciencePack(Choice):
"""Maximum level of science pack required to complete the game.""" """Maximum level of science pack required to complete the game."""
displayname = "Maximum Required Science Pack"
option_automation_science_pack = 0 option_automation_science_pack = 0
option_logistic_science_pack = 1 option_logistic_science_pack = 1
option_military_science_pack = 2 option_military_science_pack = 2
@@ -34,6 +35,7 @@ class MaxSciencePack(Choice):
class TechCost(Choice): class TechCost(Choice):
"""How expensive are the technologies.""" """How expensive are the technologies."""
displayname = "Technology Cost Scale"
option_very_easy = 0 option_very_easy = 0
option_easy = 1 option_easy = 1
option_kind = 2 option_kind = 2
@@ -44,8 +46,18 @@ class TechCost(Choice):
default = 3 default = 3
class Silo(Choice):
"""Ingredients to craft rocket silo or auto-place if set to spawn."""
displayname = "Rocket Silo"
option_vanilla = 0
option_randomize_recipe = 1
option_spawn = 2
default = 0
class FreeSamples(Choice): class FreeSamples(Choice):
"""Get free items with your technologies.""" """Get free items with your technologies."""
displayname = "Free Samples"
option_none = 0 option_none = 0
option_single_craft = 1 option_single_craft = 1
option_half_stack = 2 option_half_stack = 2
@@ -55,6 +67,7 @@ class FreeSamples(Choice):
class TechTreeLayout(Choice): class TechTreeLayout(Choice):
"""Selects how the tech tree nodes are interwoven.""" """Selects how the tech tree nodes are interwoven."""
displayname = "Technology Tree Layout"
option_single = 0 option_single = 0
option_small_diamonds = 1 option_small_diamonds = 1
option_medium_diamonds = 2 option_medium_diamonds = 2
@@ -70,6 +83,7 @@ class TechTreeLayout(Choice):
class TechTreeInformation(Choice): class TechTreeInformation(Choice):
"""How much information should be displayed in the tech tree.""" """How much information should be displayed in the tech tree."""
displayname = "Technology Tree Information"
option_none = 0 option_none = 0
option_advancement = 1 option_advancement = 1
option_full = 2 option_full = 2
@@ -78,6 +92,7 @@ class TechTreeInformation(Choice):
class RecipeTime(Choice): class RecipeTime(Choice):
"""randomize the time it takes for any recipe to craft, this includes smelting, chemical lab, hand crafting etc.""" """randomize the time it takes for any recipe to craft, this includes smelting, chemical lab, hand crafting etc."""
displayname = "Recipe Time"
option_vanilla = 0 option_vanilla = 0
option_fast = 1 option_fast = 1
option_normal = 2 option_normal = 2
@@ -85,28 +100,55 @@ class RecipeTime(Choice):
option_chaos = 5 option_chaos = 5
# TODO: implement random
class Progressive(Choice): class Progressive(Choice):
displayname = "Progressive Technologies"
option_off = 0 option_off = 0
option_random = 1 option_grouped_random = 1
option_on = 2 option_on = 2
alias_false = 0
alias_true = 2
default = 2 default = 2
alias_random = 1
def want_progressives(self, random): def want_progressives(self, random):
return random.choice([True, False]) if self.value == self.option_random else int(self.value) return random.choice([True, False]) if self.value == self.option_grouped_random else bool(self.value)
class RecipeIngredients(Choice): class RecipeIngredients(Choice):
"""Select if rocket, or rocket + science pack ingredients should be random.""" """Select if rocket, or rocket + science pack ingredients should be random."""
displayname = "Random Recipe Ingredients Level"
option_rocket = 0 option_rocket = 0
option_science_pack = 1 option_science_pack = 1
class FactorioStartItems(OptionDict): class FactorioStartItems(OptionDict):
displayname = "Starting Items"
default = {"burner-mining-drill": 19, "stone-furnace": 19} default = {"burner-mining-drill": 19, "stone-furnace": 19}
class TrapCount(Range):
range_end = 4
class AttackTrapCount(TrapCount):
"""Trap items that when received trigger an attack on your base."""
displayname = "Attack Traps"
class EvolutionTrapCount(TrapCount):
"""Trap items that when received increase the enemy evolution."""
displayname = "Evolution Traps"
class EvolutionTrapIncrease(Range):
displayname = "Evolution Trap % Effect"
range_start = 1
default = 10
range_end = 100
class FactorioWorldGen(OptionDict): class FactorioWorldGen(OptionDict):
displayname = "World Generation"
# FIXME: do we want default be a rando-optimized default or in-game DS? # FIXME: do we want default be a rando-optimized default or in-game DS?
value: typing.Dict[str, typing.Dict[str, typing.Any]] value: typing.Dict[str, typing.Dict[str, typing.Any]]
default = { default = {
@@ -238,16 +280,24 @@ class FactorioWorldGen(OptionDict):
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}") raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
class ImportedBlueprint(DefaultOnToggle):
displayname = "Blueprints"
factorio_options: typing.Dict[str, type(Option)] = { factorio_options: typing.Dict[str, type(Option)] = {
"max_science_pack": MaxSciencePack, "max_science_pack": MaxSciencePack,
"tech_tree_layout": TechTreeLayout, "tech_tree_layout": TechTreeLayout,
"tech_cost": TechCost, "tech_cost": TechCost,
"silo": Silo,
"free_samples": FreeSamples, "free_samples": FreeSamples,
"tech_tree_information": TechTreeInformation, "tech_tree_information": TechTreeInformation,
"starting_items": FactorioStartItems, "starting_items": FactorioStartItems,
"recipe_time": RecipeTime, "recipe_time": RecipeTime,
"recipe_ingredients": RecipeIngredients, "recipe_ingredients": RecipeIngredients,
"imported_blueprints": DefaultOnToggle, "imported_blueprints": ImportedBlueprint,
"world_gen": FactorioWorldGen, "world_gen": FactorioWorldGen,
"progressive": DefaultOnToggle "progressive": Progressive,
"evolution_traps": EvolutionTrapCount,
"attack_traps": AttackTrapCount,
"evolution_trap_increase": EvolutionTrapIncrease,
} }

View File

@@ -1,19 +1,18 @@
from __future__ import annotations from __future__ import annotations
# Factorio technologies are imported from a .json document in /data # Factorio technologies are imported from a .json document in /data
from typing import Dict, Set, FrozenSet, Tuple from typing import Dict, Set, FrozenSet, Tuple, Union, List
from collections import Counter, defaultdict from collections import Counter
import os import os
import json import json
import string import string
import Utils import Utils
import logging import logging
import functools
from . import Options from . import Options
factorio_id = 2 ** 17 factorio_id = factorio_base_id = 2 ** 17
source_folder = Utils.local_path("data", "factorio") source_folder = os.path.join(os.path.dirname(__file__), "data")
with open(os.path.join(source_folder, "techs.json")) as f: with open(os.path.join(source_folder, "techs.json")) as f:
raw = json.load(f) raw = json.load(f)
@@ -38,11 +37,24 @@ class FactorioElement():
class Technology(FactorioElement): # maybe make subclass of Location? class Technology(FactorioElement): # maybe make subclass of Location?
def __init__(self, name: str, ingredients: Set[str], factorio_id: int, progressive: Tuple[str] = ()): has_modifier: bool
factorio_id: int
name: str
ingredients: Set[str]
progressive: Tuple[str]
unlocks: Union[Set[str], bool] # bool case is for progressive technologies
def __init__(self, name: str, ingredients: Set[str], factorio_id: int, progressive: Tuple[str] = (),
has_modifier: bool = False, unlocks: Union[Set[str], bool] = None):
self.name = name self.name = name
self.factorio_id = factorio_id self.factorio_id = factorio_id
self.ingredients = ingredients self.ingredients = ingredients
self.progressive = progressive self.progressive = progressive
self.has_modifier = has_modifier
if unlocks:
self.unlocks = unlocks
else:
self.unlocks = set()
def build_rule(self, player: int): def build_rule(self, player: int):
logging.debug(f"Building rules for {self.name}") logging.debug(f"Building rules for {self.name}")
@@ -63,6 +75,9 @@ class Technology(FactorioElement): # maybe make subclass of Location?
def get_custom(self, world, allowed_packs: Set[str], player: int) -> CustomTechnology: def get_custom(self, world, allowed_packs: Set[str], player: int) -> CustomTechnology:
return CustomTechnology(self, world, allowed_packs, player) return CustomTechnology(self, world, allowed_packs, player)
def useful(self) -> bool:
return self.has_modifier or self.unlocks
class CustomTechnology(Technology): class CustomTechnology(Technology):
"""A particularly configured Technology for a world.""" """A particularly configured Technology for a world."""
@@ -82,12 +97,14 @@ class Recipe(FactorioElement):
category: str category: str
ingredients: Dict[str, int] ingredients: Dict[str, int]
products: Dict[str, int] products: Dict[str, int]
energy: float
def __init__(self, name: str, category: str, ingredients: Dict[str, int], products: Dict[str, int]): def __init__(self, name: str, category: str, ingredients: Dict[str, int], products: Dict[str, int], energy: float):
self.name = name self.name = name
self.category = category self.category = category
self.ingredients = ingredients self.ingredients = ingredients
self.products = products self.products = products
self.energy = energy
def __repr__(self): def __repr__(self):
return f"{self.__class__.__name__}({self.name})" return f"{self.__class__.__name__}({self.name})"
@@ -112,7 +129,7 @@ class Recipe(FactorioElement):
@property @property
def rel_cost(self) -> float: def rel_cost(self) -> float:
ingredients = sum(self.ingredients.values()) ingredients = sum(self.ingredients.values())
return min(ingredients/amount for product, amount in self.products.items()) return min(ingredients / amount for product, amount in self.products.items())
@property @property
def base_cost(self) -> Dict[str, int]: def base_cost(self) -> Dict[str, int]:
@@ -120,44 +137,60 @@ class Recipe(FactorioElement):
for ingredient, cost in self.ingredients.items(): for ingredient, cost in self.ingredients.items():
if ingredient in all_product_sources: if ingredient in all_product_sources:
for recipe in all_product_sources[ingredient]: for recipe in all_product_sources[ingredient]:
ingredients.update({name: amount*cost/recipe.products[ingredient] for name, amount in recipe.base_cost.items()}) ingredients.update({name: amount * cost / recipe.products[ingredient] for name, amount in
recipe.base_cost.items()})
else: else:
ingredients[ingredient] += cost ingredients[ingredient] += cost
return ingredients return ingredients
@property
def total_energy(self) -> float:
"""Total required energy (crafting time) for single craft"""
# TODO: multiply mining energy by 2 since drill has 0.5 speed
total_energy = self.energy
for ingredient, cost in self.ingredients.items():
if ingredient in all_product_sources:
for ingredient_recipe in all_product_sources[ingredient]: # FIXME: this may select the wrong recipe
craft_count = max((n for name, n in ingredient_recipe.products.items() if name == ingredient))
total_energy += ingredient_recipe.total_energy / craft_count * cost
break
return total_energy
class Machine(FactorioElement): class Machine(FactorioElement):
def __init__(self, name, categories): def __init__(self, name, categories):
self.name: str = name self.name: str = name
self.categories: set = categories self.categories: set = categories
recipe_sources: Dict[str, Set[str]] = {} # recipe_name -> technology source
# recipes and technologies can share names in Factorio # recipes and technologies can share names in Factorio
for technology_name in sorted(raw): for technology_name in sorted(raw):
data = raw[technology_name] data = raw[technology_name]
current_ingredients = set(data["ingredients"]) current_ingredients = set(data["ingredients"])
technology = Technology(technology_name, current_ingredients, factorio_id) technology = Technology(technology_name, current_ingredients, factorio_id,
has_modifier=data["has_modifier"], unlocks=set(data["unlocks"]))
factorio_id += 1 factorio_id += 1
tech_table[technology_name] = technology.factorio_id tech_table[technology_name] = technology.factorio_id
technology_table[technology_name] = technology technology_table[technology_name] = technology
for recipe_name in technology.unlocks:
recipe_sources: Dict[str, str] = {} # recipe_name -> technology source recipe_sources.setdefault(recipe_name, set()).add(technology_name)
for technology, data in raw.items():
for recipe_name in data["unlocks"]:
recipe_sources.setdefault(recipe_name, set()).add(technology)
del (raw) del (raw)
recipes = {} recipes = {}
all_product_sources: Dict[str, Set[Recipe]] = {"character": set()} all_product_sources: Dict[str, Set[Recipe]] = {"character": set()}
# add uranium mining to logic graph. TODO: add to automatic extractor for mod support # add uranium mining to logic graph. TODO: add to automatic extractor for mod support
raw_recipes["uranium-ore"] = {"ingredients": {"sulfuric-acid": 1}, "products": {"uranium-ore": 1}, "category": "mining"} raw_recipes["uranium-ore"] = {"ingredients": {"sulfuric-acid": 1}, "products": {"uranium-ore": 1}, "category": "mining",
"energy": 2}
for recipe_name, recipe_data in raw_recipes.items(): for recipe_name, recipe_data in raw_recipes.items():
# example: # example:
# "accumulator":{"ingredients":{"iron-plate":2,"battery":5},"products":{"accumulator":1},"category":"crafting"} # "accumulator":{"ingredients":{"iron-plate":2,"battery":5},"products":{"accumulator":1},"category":"crafting"}
# FIXME: add mining?
recipe = Recipe(recipe_name, recipe_data["category"], recipe_data["ingredients"], recipe_data["products"]) recipe = Recipe(recipe_name, recipe_data["category"], recipe_data["ingredients"],
recipe_data["products"], recipe_data["energy"] if "energy" in recipe_data else 0)
recipes[recipe_name] = recipe recipes[recipe_name] = recipe
if set(recipe.products).isdisjoint( if set(recipe.products).isdisjoint(
# prevents loop recipes like uranium centrifuging # prevents loop recipes like uranium centrifuging
@@ -177,7 +210,7 @@ for name, categories in raw_machines.items():
# add electric mining drill as a crafting machine to resolve uranium-ore # add electric mining drill as a crafting machine to resolve uranium-ore
machines["electric-mining-drill"] = Machine("electric-mining-drill", {"mining"}) machines["electric-mining-drill"] = Machine("electric-mining-drill", {"mining"})
machines["assembling-machine-1"].categories.add("crafting-with-fluid") # mod enables this machines["assembling-machine-1"].categories.add("crafting-with-fluid") # mod enables this
machines["character"].categories.add("basic-crafting") # somehow this is implied and not exported machines["character"].categories.add("basic-crafting") # somehow this is implied and not exported
del (raw_machines) del (raw_machines)
# build requirements graph for all technology ingredients # build requirements graph for all technology ingredients
@@ -249,25 +282,26 @@ for category_name, machine_name in machine_per_category.items():
techs |= recursively_get_unlocking_technologies(machine_name) techs |= recursively_get_unlocking_technologies(machine_name)
required_category_technologies[category_name] = frozenset(techs) required_category_technologies[category_name] = frozenset(techs)
required_technologies: Dict[str, FrozenSet[Technology]] = Utils.KeyedDefaultDict(lambda ingredient_name : frozenset( required_technologies: Dict[str, FrozenSet[Technology]] = Utils.KeyedDefaultDict(lambda ingredient_name: frozenset(
recursively_get_unlocking_technologies(ingredient_name, unlock_func=unlock))) recursively_get_unlocking_technologies(ingredient_name, unlock_func=unlock)))
advancement_technologies: Set[str] = set() advancement_technologies: Set[str] = set()
for ingredient_name in all_ingredient_names: for ingredient_name in all_ingredient_names:
technologies = required_technologies[ingredient_name] technologies = required_technologies[ingredient_name]
advancement_technologies |= {technology.name for technology in technologies} advancement_technologies |= {technology.name for technology in technologies}
@functools.lru_cache(10) def get_rocket_requirements(silo_recipe: Recipe, part_recipe: Recipe) -> Set[str]:
def get_rocket_requirements(recipe: Recipe) -> Set[str]: techs = set()
techs = recursively_get_unlocking_technologies("rocket-silo") if silo_recipe:
for ingredient in recipe.ingredients: for ingredient in silo_recipe.ingredients:
techs |= recursively_get_unlocking_technologies(ingredient)
for ingredient in part_recipe.ingredients:
techs |= recursively_get_unlocking_technologies(ingredient) techs |= recursively_get_unlocking_technologies(ingredient)
return {tech.name for tech in techs} return {tech.name for tech in techs}
free_sample_blacklist = all_ingredient_names | {"rocket-part"} free_sample_blacklist: Set[str] = all_ingredient_names | {"rocket-part"}
rocket_recipes = { rocket_recipes = {
Options.MaxSciencePack.option_space_science_pack: Options.MaxSciencePack.option_space_science_pack:
@@ -290,7 +324,7 @@ advancement_technologies |= {tech.name for tech in required_technologies["rocket
# progressive technologies # progressive technologies
# auto-progressive # auto-progressive
progressive_rows = {} progressive_rows: Dict[str, Union[List[str], Tuple[str, ...]]] = {}
progressive_incs = set() progressive_incs = set()
for tech_name in tech_table: for tech_name in tech_table:
if tech_name.endswith("-1"): if tech_name.endswith("-1"):
@@ -299,17 +333,17 @@ for tech_name in tech_table:
progressive_incs.add(tech_name) progressive_incs.add(tech_name)
for root, progressive in progressive_rows.items(): for root, progressive in progressive_rows.items():
seeking = root[:-1]+str(int(root[-1])+1) seeking = root[:-1] + str(int(root[-1]) + 1)
while seeking in progressive_incs: while seeking in progressive_incs:
progressive.append(seeking) progressive.append(seeking)
progressive_incs.remove(seeking) progressive_incs.remove(seeking)
seeking = seeking[:-1]+str(int(seeking[-1])+1) seeking = seeking[:-1] + str(int(seeking[-1]) + 1)
# make root entry the progressive name # make root entry the progressive name
for old_name in set(progressive_rows): for old_name in set(progressive_rows):
prog_name = "progressive-" + old_name.rsplit("-", 1)[0] prog_name = "progressive-" + old_name.rsplit("-", 1)[0]
progressive_rows[prog_name] = tuple([old_name] + progressive_rows[old_name]) progressive_rows[prog_name] = tuple([old_name] + progressive_rows[old_name])
del(progressive_rows[old_name]) del (progressive_rows[old_name])
# no -1 start # no -1 start
base_starts = set() base_starts = set()
@@ -318,17 +352,16 @@ for remnant in progressive_incs:
base_starts.add(remnant[:-2]) base_starts.add(remnant[:-2])
for root in base_starts: for root in base_starts:
seeking = root+"-2" seeking = root + "-2"
progressive = [root] progressive = [root]
while seeking in progressive_incs: while seeking in progressive_incs:
progressive.append(seeking) progressive.append(seeking)
seeking = seeking[:-1]+str(int(seeking[-1])+1) seeking = seeking[:-1] + str(int(seeking[-1]) + 1)
progressive_rows["progressive-"+root] = tuple(progressive) progressive_rows["progressive-" + root] = tuple(progressive)
# science packs # science packs
progressive_rows["progressive-science-pack"] = tuple(Options.MaxSciencePack.get_ordered_science_packs())[1:] progressive_rows["progressive-science-pack"] = tuple(Options.MaxSciencePack.get_ordered_science_packs())[1:]
# manual progressive # manual progressive
progressive_rows["progressive-processing"] = ( progressive_rows["progressive-processing"] = (
"steel-processing", "steel-processing",
@@ -336,7 +369,8 @@ progressive_rows["progressive-processing"] = (
"uranium-processing", "kovarex-enrichment-process", "nuclear-fuel-reprocessing") "uranium-processing", "kovarex-enrichment-process", "nuclear-fuel-reprocessing")
progressive_rows["progressive-rocketry"] = ("rocketry", "explosive-rocketry", "atomic-bomb") progressive_rows["progressive-rocketry"] = ("rocketry", "explosive-rocketry", "atomic-bomb")
progressive_rows["progressive-vehicle"] = ("automobilism", "tank", "spidertron") progressive_rows["progressive-vehicle"] = ("automobilism", "tank", "spidertron")
progressive_rows["progressive-train-network"] = ("railway", "fluid-wagon", "automated-rail-transportation", "rail-signals") progressive_rows["progressive-train-network"] = ("railway", "fluid-wagon",
"automated-rail-transportation", "rail-signals")
progressive_rows["progressive-engine"] = ("engine", "electric-engine") progressive_rows["progressive-engine"] = ("engine", "electric-engine")
progressive_rows["progressive-armor"] = ("heavy-armor", "modular-armor", "power-armor", "power-armor-mk2") progressive_rows["progressive-armor"] = ("heavy-armor", "modular-armor", "power-armor", "power-armor-mk2")
progressive_rows["progressive-personal-battery"] = ("battery-equipment", "battery-mk2-equipment") progressive_rows["progressive-personal-battery"] = ("battery-equipment", "battery-mk2-equipment")
@@ -345,18 +379,40 @@ progressive_rows["progressive-wall"] = ("stone-wall", "gate")
progressive_rows["progressive-follower"] = ("defender", "distractor", "destroyer") progressive_rows["progressive-follower"] = ("defender", "distractor", "destroyer")
progressive_rows["progressive-inserter"] = ("fast-inserter", "stack-inserter") progressive_rows["progressive-inserter"] = ("fast-inserter", "stack-inserter")
sorted_rows = sorted(progressive_rows)
# to keep ID mappings the same.
# If there's a breaking change at some point, then this should be moved in with the sorted ordering
progressive_rows["progressive-turret"] = ("gun-turret", "laser-turret")
sorted_rows.append("progressive-turret")
progressive_rows["progressive-flamethrower"] = ("flamethrower",) # leaving out flammables, as they do nothing
sorted_rows.append("progressive-flamethrower")
progressive_rows["progressive-personal-roboport-equipment"] = ("personal-roboport-equipment",
"personal-roboport-mk2-equipment")
sorted_rows.append("progressive-personal-roboport-equipment")
# integrate into
source_target_mapping: Dict[str, str] = {
"progressive-braking-force": "progressive-train-network",
"progressive-inserter-capacity-bonus": "progressive-inserter",
"progressive-refined-flammables": "progressive-flamethrower"
}
for source, target in source_target_mapping.items():
progressive_rows[target] += progressive_rows[source]
base_tech_table = tech_table.copy() # without progressive techs base_tech_table = tech_table.copy() # without progressive techs
base_technology_table = technology_table.copy() base_technology_table = technology_table.copy()
progressive_tech_table: Dict[str, int] = {} progressive_tech_table: Dict[str, int] = {}
progressive_technology_table: Dict[str, Technology] = {} progressive_technology_table: Dict[str, Technology] = {}
for root in sorted(progressive_rows): for root in sorted_rows:
progressive = progressive_rows[root] progressive = progressive_rows[root]
assert all(tech in tech_table for tech in progressive) assert all(tech in tech_table for tech in progressive)
factorio_id += 1 factorio_id += 1
progressive_technology = Technology(root, technology_table[progressive_rows[root][0]].ingredients, factorio_id, progressive_technology = Technology(root, technology_table[progressive_rows[root][0]].ingredients, factorio_id,
progressive) progressive,
has_modifier=any(technology_table[tech].has_modifier for tech in progressive),
unlocks=any(technology_table[tech].unlocks for tech in progressive))
progressive_tech_table[root] = progressive_technology.factorio_id progressive_tech_table[root] = progressive_technology.factorio_id
progressive_technology_table[root] = progressive_technology progressive_technology_table[root] = progressive_technology
if any(tech in advancement_technologies for tech in progressive): if any(tech in advancement_technologies for tech in progressive):
@@ -364,8 +420,9 @@ for root in sorted(progressive_rows):
tech_to_progressive_lookup: Dict[str, str] = {} tech_to_progressive_lookup: Dict[str, str] = {}
for technology in progressive_technology_table.values(): for technology in progressive_technology_table.values():
for progressive in technology.progressive: if technology.name not in source_target_mapping:
tech_to_progressive_lookup[progressive] = technology.name for progressive in technology.progressive:
tech_to_progressive_lookup[progressive] = technology.name
tech_table.update(progressive_tech_table) tech_table.update(progressive_tech_table)
technology_table.update(progressive_technology_table) technology_table.update(progressive_technology_table)
@@ -374,10 +431,13 @@ technology_table.update(progressive_technology_table)
common_tech_table: Dict[str, int] = {tech_name: tech_id for tech_name, tech_id in base_tech_table.items() common_tech_table: Dict[str, int] = {tech_name: tech_id for tech_name, tech_id in base_tech_table.items()
if tech_name not in progressive_tech_table} if tech_name not in progressive_tech_table}
useless_technologies: Set[str] = {tech_name for tech_name in common_tech_table
if not technology_table[tech_name].useful()}
lookup_id_to_name: Dict[int, str] = {item_id: item_name for item_name, item_id in tech_table.items()} lookup_id_to_name: Dict[int, str] = {item_id: item_name for item_name, item_id in tech_table.items()}
rel_cost = { rel_cost = {
"wood" : 10000, "wood": 10000,
"iron-ore": 1, "iron-ore": 1,
"copper-ore": 1, "copper-ore": 1,
"stone": 1, "stone": 1,
@@ -390,8 +450,9 @@ rel_cost = {
} }
# forbid liquids for now, TODO: allow a single liquid per assembler # forbid liquids for now, TODO: allow a single liquid per assembler
blacklist = all_ingredient_names | {"rocket-part", "crude-oil", "water", "sulfuric-acid", "petroleum-gas", "light-oil", blacklist: Set[str] = all_ingredient_names | {"rocket-part", "crude-oil", "water", "sulfuric-acid", "petroleum-gas",
"heavy-oil", "lubricant", "steam"} "light-oil", "heavy-oil", "lubricant", "steam"}
@Utils.cache_argsless @Utils.cache_argsless
def get_science_pack_pools() -> Dict[str, Set[str]]: def get_science_pack_pools() -> Dict[str, Set[str]]:
@@ -403,7 +464,6 @@ def get_science_pack_pools() -> Dict[str, Set[str]]:
cost += rel_cost.get(ingredient_name, 1) * amount cost += rel_cost.get(ingredient_name, 1) * amount
return cost return cost
science_pack_pools = {} science_pack_pools = {}
already_taken = blacklist.copy() already_taken = blacklist.copy()
current_difficulty = 5 current_difficulty = 5
@@ -416,4 +476,4 @@ def get_science_pack_pools() -> Dict[str, Set[str]]:
current -= already_taken current -= already_taken
already_taken |= current already_taken |= current
current_difficulty *= 2 current_difficulty *= 2
return science_pack_pools return science_pack_pools

View File

@@ -1,44 +1,70 @@
import collections
from ..AutoWorld import World from ..AutoWorld import World
from BaseClasses import Region, Entrance, Location, Item from BaseClasses import Region, Entrance, Location, Item
from .Technologies import base_tech_table, recipe_sources, base_technology_table, advancement_technologies, \ from .Technologies import base_tech_table, recipe_sources, base_technology_table, advancement_technologies, \
all_ingredient_names, required_technologies, get_rocket_requirements, rocket_recipes, \ all_ingredient_names, all_product_sources, required_technologies, get_rocket_requirements, rocket_recipes, \
progressive_technology_table, common_tech_table, tech_to_progressive_lookup, progressive_tech_table, \ progressive_technology_table, common_tech_table, tech_to_progressive_lookup, progressive_tech_table, \
get_science_pack_pools, Recipe, recipes, technology_table, tech_table get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies
from .Shapes import get_shapes from .Shapes import get_shapes
from .Mod import generate_mod from .Mod import generate_mod
from .Options import factorio_options from .Options import factorio_options, Silo
import logging
class FactorioItem(Item): class FactorioItem(Item):
game = "Factorio" game = "Factorio"
all_items = tech_table.copy()
all_items["Attack Trap"] = factorio_base_id - 1
all_items["Evolution Trap"] = factorio_base_id - 2
class Factorio(World): class Factorio(World):
"""
Factorio is a game about automation. You play as an engineer who has crash landed on the planet
Nauvis, an inhospitable world filled with dangerous creatures called biters. Build a factory,
research new technologies, and become more efficient in your quest to build a rocket and return home.
"""
game: str = "Factorio" game: str = "Factorio"
static_nodes = {"automation", "logistics", "rocket-silo"} static_nodes = {"automation", "logistics", "rocket-silo"}
custom_recipes = {} custom_recipes = {}
additional_advancement_technologies = set() additional_advancement_technologies = set()
item_names = frozenset(tech_table)
location_names = frozenset(base_tech_table)
item_name_to_id = tech_table item_name_to_id = all_items
location_name_to_id = base_tech_table location_name_to_id = base_tech_table
data_version = 5
def generate_basic(self): def generate_basic(self):
player = self.player
want_progressives = collections.defaultdict(lambda: self.world.progressive[player].
want_progressives(self.world.random))
skip_silo = self.world.silo[player].value == Silo.option_spawn
evolution_traps_wanted = self.world.evolution_traps[player].value
attack_traps_wanted = self.world.attack_traps[player].value
traps_wanted = ["Evolution Trap"] * evolution_traps_wanted + ["Attack Trap"] * attack_traps_wanted
self.world.random.shuffle(traps_wanted)
for tech_name in base_tech_table: for tech_name in base_tech_table:
if self.world.progressive: if traps_wanted and tech_name in useless_technologies:
item_name = tech_to_progressive_lookup.get(tech_name, tech_name) self.world.itempool.append(self.create_item(traps_wanted.pop()))
elif skip_silo and tech_name == "rocket-silo":
pass
else: else:
item_name = item_name progressive_item_name = tech_to_progressive_lookup.get(tech_name, tech_name)
tech_item = self.create_item(item_name) want_progressive = want_progressives[progressive_item_name]
if tech_name in self.static_nodes: item_name = progressive_item_name if want_progressive else tech_name
self.world.get_location(tech_name, self.player).place_locked_item(tech_item) tech_item = self.create_item(item_name)
else: if tech_name in self.static_nodes:
self.world.itempool.append(tech_item) self.world.get_location(tech_name, player).place_locked_item(tech_item)
map_basic_settings = self.world.world_gen[self.player].value["basic"] else:
self.world.itempool.append(tech_item)
map_basic_settings = self.world.world_gen[player].value["basic"]
if map_basic_settings.get("seed", None) is None: # allow seed 0 if map_basic_settings.get("seed", None) is None: # allow seed 0
map_basic_settings["seed"] = self.world.slot_seeds[self.player].randint(0, 2**32-1) # 32 bit uint map_basic_settings["seed"] = self.world.slot_seeds[player].randint(0, 2 ** 32 - 1) # 32 bit uint
generate_output = generate_mod generate_output = generate_mod
@@ -49,7 +75,10 @@ class Factorio(World):
menu.exits.append(crash) menu.exits.append(crash)
nauvis = Region("Nauvis", None, "Nauvis", player, self.world) nauvis = Region("Nauvis", None, "Nauvis", player, self.world)
skip_silo = self.world.silo[self.player].value == Silo.option_spawn
for tech_name, tech_id in base_tech_table.items(): for tech_name, tech_id in base_tech_table.items():
if skip_silo and tech_name == "rocket-silo":
continue
tech = Location(player, tech_name, tech_id, nauvis) tech = Location(player, tech_name, tech_id, nauvis)
nauvis.locations.append(tech) nauvis.locations.append(tech)
tech.game = "Factorio" tech.game = "Factorio"
@@ -92,7 +121,10 @@ class Factorio(World):
location.access_rule = lambda state, ingredient=ingredient: \ location.access_rule = lambda state, ingredient=ingredient: \
all(state.has(technology.name, player) for technology in required_technologies[ingredient]) all(state.has(technology.name, player) for technology in required_technologies[ingredient])
skip_silo = self.world.silo[self.player].value == Silo.option_spawn
for tech_name, technology in self.custom_technologies.items(): for tech_name, technology in self.custom_technologies.items():
if skip_silo and tech_name == "rocket-silo":
continue
location = world.get_location(tech_name, player) location = world.get_location(tech_name, player)
Rules.set_rule(location, technology.build_rule(player)) Rules.set_rule(location, technology.build_rule(player))
prequisites = shapes.get(tech_name) prequisites = shapes.get(tech_name)
@@ -101,27 +133,104 @@ class Factorio(World):
Rules.add_rule(location, lambda state, Rules.add_rule(location, lambda state,
locations=locations: all(state.can_reach(loc) for loc in locations)) locations=locations: all(state.can_reach(loc) for loc in locations))
victory_tech_names = get_rocket_requirements(self.custom_recipes["rocket-part"]) silo_recipe = None if self.world.silo[self.player].value == Silo.option_spawn \
else self.custom_recipes["rocket-silo"] \
if "rocket-silo" in self.custom_recipes \
else next(iter(all_product_sources.get("rocket-silo")))
part_recipe = self.custom_recipes["rocket-part"]
victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe)
world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player) world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player)
for technology in for technology in
victory_tech_names) victory_tech_names)
world.completion_condition[player] = lambda state: state.has('Victory', player) world.completion_condition[player] = lambda state: state.has('Victory', player)
def collect(self, state, item) -> bool: def collect_item(self, state, item, remove=False):
if item.advancement and item.name in progressive_technology_table: if item.advancement and item.name in progressive_technology_table:
prog_table = progressive_technology_table[item.name].progressive prog_table = progressive_technology_table[item.name].progressive
for item_name in prog_table: if remove:
if not state.has(item_name, item.player): for item_name in reversed(prog_table):
state.prog_items[item_name, item.player] += 1 if state.has(item_name, item.player):
return True return item_name
return super(Factorio, self).collect(state, item) else:
for item_name in prog_table:
if not state.has(item_name, item.player):
return item_name
return super(Factorio, self).collect_item(state, item, remove)
def get_required_client_version(self) -> tuple: def get_required_client_version(self) -> tuple:
return max((0, 1, 5), super(Factorio, self).get_required_client_version()) return max((0, 1, 6), super(Factorio, self).get_required_client_version())
options = factorio_options options = factorio_options
def make_balanced_recipe(self, original: Recipe, pool: list, factor: float = 1) -> Recipe:
"""Generate a recipe from pool with time and cost similar to original * factor"""
new_ingredients = {}
self.world.random.shuffle(pool)
target_raw = int(sum((count for ingredient, count in original.base_cost.items())) * factor)
target_energy = original.total_energy * factor
target_num_ingredients = len(original.ingredients)
remaining_raw = target_raw
remaining_energy = target_energy
remaining_num_ingredients = target_num_ingredients
fallback_pool = []
# fill all but one slot with random ingredients, last with a good match
while remaining_num_ingredients > 0 and len(pool) > 0:
if remaining_num_ingredients == 1:
max_raw = 1.1 * remaining_raw
min_raw = 0.9 * remaining_raw
max_energy = 1.1 * remaining_energy
min_energy = 1.1 * remaining_energy
else:
max_raw = remaining_raw * 0.75
min_raw = (remaining_raw - max_raw) / remaining_num_ingredients
max_energy = remaining_energy * 0.75
min_energy = (remaining_energy - max_energy) / remaining_num_ingredients
ingredient = pool.pop()
ingredient_recipe = min(all_product_sources[ingredient], key=lambda recipe: recipe.rel_cost)
ingredient_raw = sum((count for ingredient, count in ingredient_recipe.base_cost.items()))
ingredient_energy = ingredient_recipe.total_energy
min_num_raw = min_raw/ingredient_raw
max_num_raw = max_raw/ingredient_raw
min_num_energy = min_energy/ingredient_energy
max_num_energy = max_energy/ingredient_energy
min_num = int(max(1, min_num_raw, min_num_energy))
max_num = int(min(1000, max_num_raw, max_num_energy))
if min_num > max_num:
fallback_pool.append(ingredient)
continue # can't use that ingredient
num = self.world.random.randint(min_num,max_num)
new_ingredients[ingredient] = num
remaining_raw -= num * ingredient_raw
remaining_energy -= num * ingredient_energy
remaining_num_ingredients -= 1
# fill failed slots with whatever we got
pool = fallback_pool
while remaining_num_ingredients > 0 and len(pool) > 0:
ingredient = pool.pop()
if ingredient not in recipes:
logging.warning(f"missing recipe for {ingredient}")
continue
ingredient_recipe = recipes[ingredient]
ingredient_raw = sum((count for ingredient, count in ingredient_recipe.base_cost.items()))
ingredient_energy = ingredient_recipe.total_energy
num_raw = remaining_raw/ingredient_raw/remaining_num_ingredients
num_energy = remaining_energy/ingredient_energy/remaining_num_ingredients
num = int(min(num_raw, num_energy))
if num < 1: continue
new_ingredients[ingredient] = num
remaining_raw -= num * ingredient_raw
remaining_energy -= num * ingredient_energy
remaining_num_ingredients -= 1
if remaining_num_ingredients > 1:
logging.warning("could not randomize recipe")
return Recipe(original.name, original.category, new_ingredients, original.products, original.energy)
def set_custom_technologies(self): def set_custom_technologies(self):
custom_technologies = {} custom_technologies = {}
allowed_packs = self.world.max_science_pack[self.player].get_allowed_packs() allowed_packs = self.world.max_science_pack[self.player].get_allowed_packs()
@@ -136,7 +245,8 @@ class Factorio(World):
self.world.random.shuffle(valid_pool) self.world.random.shuffle(valid_pool)
self.custom_recipes = {"rocket-part": Recipe("rocket-part", original_rocket_part.category, self.custom_recipes = {"rocket-part": Recipe("rocket-part", original_rocket_part.category,
{valid_pool[x] : 10 for x in range(3)}, {valid_pool[x] : 10 for x in range(3)},
original_rocket_part.products)} original_rocket_part.products,
original_rocket_part.energy)}
self.additional_advancement_technologies = {tech.name for tech in self.additional_advancement_technologies = {tech.name for tech in
self.custom_recipes["rocket-part"].recursive_unlocking_technologies} self.custom_recipes["rocket-part"].recursive_unlocking_technologies}
@@ -150,11 +260,21 @@ class Factorio(World):
new_ingredients = {} new_ingredients = {}
for _ in original.ingredients: for _ in original.ingredients:
new_ingredients[valid_pool.pop()] = 1 new_ingredients[valid_pool.pop()] = 1
new_recipe = Recipe(pack, original.category, new_ingredients, original.products) new_recipe = Recipe(pack, original.category, new_ingredients, original.products, original.energy)
self.additional_advancement_technologies |= {tech.name for tech in self.additional_advancement_technologies |= {tech.name for tech in
new_recipe.recursive_unlocking_technologies} new_recipe.recursive_unlocking_technologies}
self.custom_recipes[pack] = new_recipe self.custom_recipes[pack] = new_recipe
if self.world.silo[self.player].value == Silo.option_randomize_recipe:
valid_pool = []
for pack in self.world.max_science_pack[self.player].get_allowed_packs():
valid_pool += sorted(science_pack_pools[pack])
new_recipe = self.make_balanced_recipe(recipes["rocket-silo"], valid_pool,
factor = (self.world.max_science_pack[self.player].value+1)/7)
self.additional_advancement_technologies |= {tech.name for tech in
new_recipe.recursive_unlocking_technologies}
self.custom_recipes["rocket-silo"] = new_recipe
# handle marking progressive techs as advancement # handle marking progressive techs as advancement
prog_add = set() prog_add = set()
for tech in self.additional_advancement_technologies: for tech in self.additional_advancement_technologies:
@@ -163,7 +283,9 @@ class Factorio(World):
self.additional_advancement_technologies |= prog_add self.additional_advancement_technologies |= prog_add
def create_item(self, name: str) -> Item: def create_item(self, name: str) -> Item:
assert name in tech_table if name in tech_table:
return FactorioItem(name, name in advancement_technologies or return FactorioItem(name, name in advancement_technologies or
name in self.additional_advancement_technologies, name in self.additional_advancement_technologies,
tech_table[name], self.player) tech_table[name], self.player)
elif name in all_items:
return FactorioItem(name, False, all_items[name], self.player)

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

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