Compare commits

...

1321 Commits
0.1.2 ... 0.2.5

Author SHA1 Message Date
Yussur Mustafa Oraji
1d19868119 v6: Update NPC Trinket Rule 2022-02-18 19:32:36 +01:00
Fabian Dill
840e634161 update docs with NetworkSlot and create_as_hint 2022-02-18 18:54:26 +01:00
Fabian Dill
731eef8c2f bump version 2022-02-18 17:58:45 +01:00
CaitSith2
135ee018a9 update Copyright 2022-02-17 19:03:11 -08:00
Fabian Dill
7633392eea update Copyright 2022-02-17 08:21:26 +01:00
Fabian Dill
daea0f3e5e Core: provide a way to add to CollectionState init and copy
SM: use that way
OoT: use that way
2022-02-17 07:07:34 +01:00
Fabian Dill
c525c80b49 ItemLinks: move item links to events, mess up their logic in doing so and lock them behind plando option "item_links" until they're fixed. 2022-02-17 06:07:20 +01:00
N00byKing
311fb04647 sm64ex: Add option for Bob-omb Buddy Checks 2022-02-16 19:46:28 +01:00
Hussein Farran
219bd9c10e Merge pull request #285 from JarnoWesthof/add_reference_to_cpp_lib
[Docs] Added reference to the cpp lib
2022-02-16 09:01:00 -05:00
Jarno Westhof
6d704eadd7 [Docs] Added reference to the cpp lib 2022-02-16 13:05:47 +01:00
Hussein Farran
32da1993e1 Merge pull request #283 from alwaysintreble/tutorials
Tutorials: Clean up plando guide a bit; explain datapackage page. Add…
2022-02-15 15:58:03 -05:00
alwaysintreble
d4cad980e5 Tutorials: remove /api 2022-02-15 14:17:17 -06:00
Fabian Dill
53340ab22c Core: remove legacy "dynamic_regions", as all regions are now dynamic 2022-02-15 06:29:57 +01:00
alwaysintreble
2d3767a35c Tutorials: Clean up plando guide a bit; explain datapackage page. Add link to the weighted settings page in advanced tutorial. 2022-02-14 17:21:19 -06:00
Fabian Dill
aaa9bc906e WebHost: update dependencies 2022-02-14 21:37:50 +01:00
N00byKing
7503317d49 sm64ex: Add DeathLink Support 2022-02-14 16:37:49 +01:00
Fabian Dill
3fc93a33c8 WebHost: check for duplicate names
Generate: use Counter for duplicate names to make finding the dupes easier
2022-02-14 04:58:21 +01:00
Fabian Dill
d7d1d54a0b Core: generalize pre_fill item pool handling 2022-02-13 23:02:18 +01:00
Fabian Dill
34b9344084 ItemLink; correct validation to allow for None replacement item 2022-02-13 20:19:17 +01:00
espeon65536
779f3a8a61 OoT: regions are not barren if they contain never-exclude items 2022-02-12 17:29:06 +01:00
espeon65536
8c1690ef65 OoT: invert logic of previous commit 2022-02-12 17:29:06 +01:00
espeon65536
85f32d9a97 OoT: make Farore's Wind a never-exclude item if the relevant trick is off 2022-02-12 17:29:06 +01:00
espeon65536
54c7ec5873 OoT: ice traps have the trap attribute 2022-02-12 17:29:06 +01:00
espeon65536
8d260708d3 OoT: ER fixes
Don't allow beatable only to influence priority placements
Shuffle spawns after warp songs to prevent spawn points going to Desert Colossus
Prevent child spawn from priority placing at Colossus if overworld ER is off
2022-02-12 17:29:06 +01:00
espeon65536
f8009e4b84 OoT: certain ER options convert closed forest into closed deku + child start 2022-02-12 17:29:06 +01:00
Alchav
a2260ee6b2 [SM] Fix "No Energy" bugs 2022-02-12 17:28:23 +01:00
Bondo
6193eafb7b Update Text.py (#274)
Changed the Houlihan hint tile to list the winner of the SGLive 2021 tournament in similar style to alttp tournament winners.
2022-02-12 03:01:41 +01:00
black-sliver
a4eea3325f Document id range for items and locations 2022-02-12 03:00:09 +01:00
Jarno Westhof
b93e61b758 [Timnespinner] Implemented get_filler_item_name 2022-02-09 21:08:07 +01:00
Fabian Dill
14448ad97e Multidata: allow SoE/SM/LttP to connect via player name for use in Tracker/Text clients 2022-02-09 21:06:50 +01:00
Yussur Mustafa Oraji
3d17f0d588 sm64ex: Add Course Randomizer and Progressive Keys (#256) 2022-02-09 20:57:38 +01:00
CaitSith2
ee5ea09cbc Add an autofill !hint_location for clicking on a Missing: line, when user uses !missing. 2022-02-08 14:29:24 -08:00
Fabian Dill
aac8ca97ed Core: define unreachables as set 2022-02-07 00:26:44 +01:00
Fabian Dill
e4d6da47a4 LttP: fix rom writing crash because I accidentally defaulted to pep8 naming 2022-02-06 21:44:19 +01:00
Fabian Dill
9f7dbb394e LttP: convert overflow progressive items into highest-allowed-tier of non-progressive item 2022-02-06 20:11:40 +01:00
Fabian Dill
f98063b97a Options: move name verification into class methods, out of Generate.py 2022-02-06 16:37:21 +01:00
black-sliver
ed607bdc37 Fix wrong message when loading apsave
from doubling received_items that happened when moving from world-based to client-based remote_items
2022-02-06 12:28:46 +01:00
N00byKing
a3c3e4cbd4 v6: Add Area Cost Shuffle 2022-02-05 20:24:42 +01:00
ScootyPuffJr1
bffb8a034e [SM]Update Options.py (#268)
* [SM] Update Options.py
2022-02-05 20:23:17 +01:00
Fabian Dill
8242d4fe92 ItemLink: fix wrong variable use 2022-02-05 20:15:56 +01:00
Fabian Dill
279b682ac2 ItemLink: hopefully fix coop functionality 2022-02-05 17:35:12 +01:00
Fabian Dill
43ff476d98 AutoWorld: add "Everything" item_name_group to all worlds 2022-02-05 16:55:11 +01:00
Fabian Dill
28201a6c38 Core: implement first version of ItemLinks 2022-02-05 15:49:19 +01:00
N00byKing
6923800081 v6: Music Randomizer 2022-02-04 23:04:05 +01:00
Jarno Westhof
700b83572e [Timespinner] Added new shop options (#264)
* [Timespinner] Added new shop options
2022-02-04 21:53:47 +01:00
Fabian Dill
6e53cb2deb V6: some cleanup 2022-02-04 21:34:39 +01:00
Yussur Mustafa Oraji
8e04182b3f v6: Add Area Randomizer (#249)
* v6: Add Area Randomizer
2022-02-04 21:22:26 +01:00
Jarno Westhof
9fd6d1b81f [Server] Broadcast hint_cost and location_check_points update changes via RoomUpdate 2022-02-03 13:09:59 +01:00
Fabian Dill
60379d9ae6 LttP: when generating hint tiles, no longer consider Single Arrow as useful, but do consider all varieties of Bow. Additionally, don't create hints for Universal Small Keys 2022-02-03 10:41:31 +01:00
black-sliver
29ba1d4809 Doc: change displayname to display_name in api.md 2022-02-02 23:38:00 +01:00
Fabian Dill
dc4b064c73 Options: change displayname to display_name 2022-02-02 16:29:29 +01:00
Fabian Dill
0f20888563 Options: allow yaml access to Priority Locations 2022-02-01 16:36:14 +01:00
Brad Humphrey
2361f8f9d3 Use logic when placing non-excluded items 2022-02-01 16:35:18 +01:00
Chris Wilson
feba54d5d2 Fix filename for Super Mario 64 info page 2022-01-31 18:39:17 -05:00
Brad Humphrey
3cecab25c7 Add unplaced_items into the fill sweep 2022-01-31 19:17:06 +01:00
Brad Humphrey
814851ba60 Don't require every item to fill 2022-01-31 19:17:06 +01:00
Fabian Dill
6333cc3bea Server: optimize send_multiple 2022-01-31 19:05:00 +01:00
N00byKing
00bf9c569a Add send_multiple command 2022-01-31 18:56:46 +01:00
Jarno
6def1bce25 [Docs] Made LocationInfoPacket more specific 2022-01-31 18:55:20 +01:00
Jarno Westhof
3ab5c90d7c [Docs] updated description on player property of NetworkItem 2022-01-31 18:55:20 +01:00
N00byKing
0507d6923e sm64ex: Add Option to limit stars, replace with junk 2022-01-31 18:54:54 +01:00
N00byKing
e85baa8068 sm64ex: Link to release page 2022-01-31 10:57:57 +01:00
N00byKing
cbed5a0c14 sm64ex,v6: Add Note regarding spaces in arguments 2022-01-31 10:57:43 +01:00
Fabian Dill
e0628ec6c9 WebHost: correct some texts 2022-01-31 10:11:39 +01:00
Chris Wilson
82637ff072 [WebHost] Add version notice to /generate and /uploads 2022-01-30 20:06:03 -05:00
Chris Wilson
a95a18a8b5 [WebHost] weighted-settings: Add cursor hover to user-message 2022-01-30 16:53:53 -05:00
Chris Wilson
d36637ed13 Fix a bug causing the /weighted-settings page to fail to detect a change in the source JSON file 2022-01-30 16:50:04 -05:00
N00byKing
dd5e5dcda7 v6: Add missing info regarding Server Port 2022-01-30 18:49:39 +01:00
Jarno Westhof
0ff7fe8479 [Generation] Fixed creation of new Slot-Info 2022-01-30 17:09:10 +01:00
Fabian Dill
8c638bcfd8 Server: allow LocationScouts to create free hints 2022-01-30 14:14:49 +01:00
Fabian Dill
0bd252e7f5 Server: add slot_info key to Connected 2022-01-30 13:57:12 +01:00
Jarno Westhof
ddd3073132 [Docs] Fixed typo 2022-01-30 13:52:51 +01:00
N00byKing
1788422abc v6: Link to release instead of actions 2022-01-30 10:58:48 +01:00
Fabian Dill
6210630ce2 Core: increment version 2022-01-30 03:45:21 +01:00
Fabian Dill
e5af7d11cc Tests: add ID range checks 2022-01-29 16:10:42 +01:00
Fabian Dill
5777808aa9 git: cleanup gitignore, as a bunch of files/folders no longer exist in AP 2022-01-29 15:39:14 +01:00
Fabian Dill
a97e6833a3 LttP: point guide to snes9x rr which is open source, rather than someone's google drive 2022-01-29 15:38:26 +01:00
Chris Wilson
79408ba0c4 Add *.nes to .gitignore 2022-01-29 00:15:24 -05:00
Fabian Dill
25dd89ed17 MultiServer: delete unused function 2022-01-28 09:29:29 +01:00
Brad Humphrey
dd61d0d395 Don't swap items that reduce access (#247) 2022-01-28 05:40:08 +01:00
Brad Humphrey
65a92746d1 Sort before distribute to preserve seed integrity 2022-01-28 05:39:34 +01:00
N00byKing
695e87689c sm64ex: More name changes 2022-01-28 05:38:41 +01:00
Hussein Farran
8997e786da Merge pull request #242 from N00byKing/patch-2
sm64ex: Clarify Instructions
2022-01-27 13:36:18 -05:00
Fabian Dill
239f1afbbd SM64: increment data version to trigger new names to be downloaded to clients 2022-01-27 15:36:14 +01:00
N00byKing
df09b5baac sm64ex: Rename some Items and Locations according to feedback 2022-01-27 15:35:28 +01:00
Yussur Mustafa Oraji
de4aa78fd6 sm64ex: Clarify Instructions 2022-01-27 14:42:49 +01:00
Zach Parks
8175d4c31f Rogue Legacy: Update world definition for 0.8 changes. (#236)
"
Here's a list of compiled changes for the next major release of the Rogue Legacy Randomizer.

Ability to toggle (or randomize) whether you fight vanilla bosses or challenge bosses.
Ability to change Architect settings (start with unlocked, normal, or disabled)
Ability to adjust Architect's fees.
Ability to combine chests or fairy chests into a single pool, similar to Risk of Rain 2's implementation.
Ability to change blueprints acquirement to being progressive instead of random.
Added additional classes that the player can start with and removed all other classes unlocked except for starting class. You must find them!
Ability to tweak the item pool amounts for stat ups.
Ability to define additional character names.
Ability to get a new diary check every time you enter a newly generated castle.
"
2022-01-26 23:35:56 +01:00
Jarno Westhof
2694bd37ea [Docs] Extended info about bounced packets 2022-01-26 23:29:18 +01:00
N00byKing
954d2e64ef v6: Mitigate Generation Problems 2022-01-26 23:28:03 +01:00
CaitSith2
c2be70b61d Death Link during a crystal/pendant cutscene no longer softlocks while connected. 2022-01-26 07:54:09 -08:00
alwaysintreble
d701a7b04e LTTP: update playerSettings.yaml 2022-01-26 10:02:40 +01:00
Chris Wilson
2925aa6261 Fix incorrect game reference. 2022-01-25 22:03:28 -05:00
Chris Wilson
4ebd43104c Include mention of SNC in Super Metroid setup guide 2022-01-25 21:56:31 -05:00
Hussein Farran
2ebe8d0ed4 Increment RoR2 Data Version 2022-01-25 13:00:11 -05:00
Hussein Farran
b26bce8fde Merge pull request #228 from Mathx2/main
Increase Location count and Item pool count
2022-01-25 10:50:21 -05:00
N00byKing
dc31ee4f7e sm64ex: Note incompatibility with spaces in path 2022-01-25 09:51:25 +01:00
Fabian Dill
1b3b0f199d Generate: improve duplicate key feedback by providing duplicate text, line and column 2022-01-25 04:20:08 +01:00
Fabian Dill
0800cfccb6 CommonClient: fix color related crashes in --nogui mode 2022-01-25 02:25:20 +01:00
Chris Wilson
ea0ff6cbf7 [WebHost] Fix /templates page referencing the wrong directory 2022-01-24 19:11:44 -05:00
Mathx2
341fefda01 Convert revives to a percent of total locations 2022-01-24 14:43:42 -08:00
Mathx2
8550c071a2 Revert Max revives
Set the max revives back to 10 and start to convert it to a percentage of the total locations instead of a static value.
2022-01-24 14:40:51 -08:00
CaitSith2
6b1c555d38 Fix inconsistency in parameter name now that !hint only hints items, not locations. 2022-01-23 22:09:06 -08:00
Brad Humphrey
64ce90d5ca Don't add more locations to the priority fill pool 2022-01-24 06:48:59 +01:00
Fabian Dill
415526d23e Fill: remove warning loggers that confused people 2022-01-24 04:50:49 +01:00
Mathx2
b2ebb65c26 Set Max Weights back to 100
Increasing max weight to 500 for fine tuning was overkill.
Max Locations still set to 500
Max Revives still set to 10% of Max Locations
2022-01-23 16:13:07 -08:00
Fabian Dill
7a7e3544cf Fill: log per-player item and location counts in case of mismatch. 2022-01-24 00:18:00 +01:00
Fabian Dill
9fbc7470c1 Clients: fix incorrect log entry height, by overriding correct height every 30 milliseconds 2022-01-23 23:31:49 +01:00
Yussur Mustafa Oraji
056b38fd2a sm64ex: Remove placeholder text 2022-01-23 21:47:26 +01:00
Yussur Mustafa Oraji
23211dd1ee Add Super Mario 64 (PC Port) to Archipelago (#207)
* Add Super Mario 64
2022-01-23 21:34:30 +01:00
Grrmo
b4ad0ebf52 Created new region for kitty boss (#233)
* Added own region for kitty boss

Kitty boss had the same access restriction as upper lake desolation, which is wrong.
2022-01-23 21:26:32 +01:00
N00byKing
0ee6dd3f77 V6: Raise DoorCost Max to 5 2022-01-23 21:24:46 +01:00
N00byKing
70a422d354 V6: Fix broken Generation for Location "V" 2022-01-23 21:24:46 +01:00
Yussur Mustafa Oraji
9d7975ce33 Update Rules.py 2022-01-23 21:24:46 +01:00
Mathx2
9b5a1bedc0 Increase amount of items allowed in the pool
Multiplied max for all items and revives by 5
2022-01-22 22:52:02 -08:00
Mathx2
1518168843 Increase max number of locations
Updated from 100 to 500
2022-01-22 22:48:20 -08:00
black-sliver
f0cfe30a36 Move remote_items and _start_inventory from world to client (#227) 2022-01-23 06:38:46 +01:00
Alchav
219bcb3521 Item Plando updates (#226)
* Item Plando updates

Add True option for item count to place the number of that item that is in the item pool.
Prioritize plando blocks by location count minus item count, so that the least flexible blocks are handled first to increase likelihood of success.
True and False for Force option are coming in as bools instead of strings, so that had to be accounted for.
Several other bug fixes.
2022-01-22 21:03:13 +01:00
Fabian Dill
c7e87bc16a Setup: add setup specific requirements 2022-01-22 20:35:30 +01:00
Fabian Dill
66c15c8639 fix MultiTracker 2022-01-22 05:19:33 +01:00
Brad Humphrey
00ccecac9c Allow fill_hook to remove things from the pool 2022-01-22 04:40:24 +01:00
black-sliver
102c1fecb6 SoE: allow start_inventory 2022-01-22 04:37:48 +01:00
black-sliver
9d4d92167a SoE: place Wings in Halls NE to avoid softlock 2022-01-22 04:37:48 +01:00
black-sliver
e7fde3bacb SoE: Update to pyevermizer v0.41.0
* invers meaning of two flags
* fixes some softlocks
* see see https://github.com/black-sliver/pyevermizer/releases/tag/v0.41.0
2022-01-22 04:37:48 +01:00
Chris Wilson
c0fe9c179c Add LICENSE files to directories containing assets owned by Archipelago 2022-01-21 22:17:29 -05:00
Jarno Westhof
929c684977 [Bug] fixed collect 2022-01-21 23:13:14 +01:00
Henrique Gemignani Passos Lima
02e776bfe5 Run tests in Python 3.8 and 3.9 2022-01-21 23:12:42 +01:00
black-sliver
0c46cc6843 Add per-client remote_item settings + TextOnly Tag
* Tracker tag will receive all items via server (including local)
* TextOnly tag will receive no items
* TextClient sends TextOnly tag
* precollected items / start_inventory does not get an "Order received" number anymore
* local items do always get an "Order received" number now
* multisave changed, includes version number now, upgrade works for games (not trackers)
2022-01-21 22:42:59 +01:00
Yussur Mustafa Oraji
344f4afdbd Add VVVVVV to Archipelago (#178) 2022-01-21 22:42:11 +01:00
Sunny Bat
4291912577 Add Raft to Archipelago (#174) 2022-01-21 22:41:53 +01:00
Fabian Dill
1e5c4c9b7c Setup: pass used python version into windows installer automatically 2022-01-21 08:39:42 +01:00
Fabian Dill
06ec72a064 Fill: fix for crash when locations are prefilled 2022-01-21 05:04:02 +01:00
Fabian Dill
31a823bc34 Change remaining flags to 0b notation by popularity vote 2022-01-21 00:42:45 +01:00
Alchav
dc6f1c4dd2 Item Plando overhaul (#205) 2022-01-20 19:34:17 +01:00
Jarno Westhof
fc8e3d1787 [Timespinner] Added Talaria Attachment to tracker if QuickSeed is enabled
Added new locations ids to tracker
Added new chest & logic for Ancient pyramid
Made tracker change available locations based on flags
Made tracker only show items that are progression based on selected flags
2022-01-20 04:25:16 +01:00
Jarno Westhof
8a25471fbb [Tracker] Fix bug reported by Grrmo, introduced with my change to multi world location data 2022-01-20 04:24:13 +01:00
Robinde67
ad06d9bb4a Adjuster fixes and added GUI prompt for applying last settings (#173) 2022-01-20 04:19:58 +01:00
Brad Humphrey
ec95ce8329 Allow locations to be prioritized for progress item placement (#189) 2022-01-20 04:19:07 +01:00
Fabian Dill
ab4fb6e69c WebHost: fix /api/room_info 2022-01-19 18:51:26 +01:00
Chris Wilson
238e2d0280 [WebHost] player-settings: Cross-port the name validation from weighted-settings to help ensure people enter a name on their settings file 2022-01-19 00:54:14 -05:00
Chris Wilson
2c8a581923 Merge branch 'main' of https://github.com/ArchipelagoMW/Archipelago into main 2022-01-19 00:38:29 -05:00
Chris Wilson
e878d7d439 [WebHost] player-settings: Default invalid player names to "Player{player}" instead of "noname" 2022-01-19 00:38:17 -05:00
Fabian Dill
4f12660961 Requirements: let's keep whitespace style consistent 2022-01-19 06:15:07 +01:00
Fabian Dill
80a7e4175b MultiServer: update FuzzyWuzzy to TheFuzz 2022-01-19 06:14:13 +01:00
Fabian Dill
b4f17e67d0 Generate: disallow duplicate mapping keys in input files 2022-01-19 04:26:25 +01:00
Jarno Westhof
5df4d2f2fd [Docs] Specified NetworkItem player is about the player slot of the location, not who the item is intended for (#217) 2022-01-18 19:01:51 +01:00
Hussein Farran
ffc7715f1b Merge pull request #204 from Lincoded/patch-1
Increase contrast of SM tracker
2022-01-18 09:32:44 -05:00
Fabian Dill
a6cca3094d WebHost: give proper incompatible version error message.. in the future when this is deployed for next time. 2022-01-18 08:23:38 +01:00
Fabian Dill
b82e0749b7 Network Docs: should put the bits in the right spot 2022-01-18 06:51:16 +01:00
Fabian Dill
5c1d2b3393 Network: unify flags docs and implementation 2022-01-18 06:45:09 +01:00
vgZerst
4841926f83 Note evolution trap scaling in Options docstring 2022-01-18 06:22:00 +01:00
vgZerst
eebf1a5126 Attenuate evolution trap increases
Attenuate evolution trap increases based on game's current evolution_factor to improve difficulty slider scaling. See drive.google.com/file/d/1RBBZV3XRmvgwOTXJhr6aQJIaTatJc2WF
2022-01-18 06:22:00 +01:00
Fabian Dill
028207022a Factorio: support new colors in-game
Various: cleanup and comments
2022-01-18 06:16:16 +01:00
Jarno Westhof
c9fa49d40f [Network_Item] Add item flags to network item so client can distinct some details (#210) 2022-01-18 05:52:29 +01:00
Hussein Farran
5d356d509c Merge pull request #159 from ArchipelagoMW/docs_consolidation
Docs Consolidation
2022-01-17 17:43:30 -05:00
Grrmo
22b361c281 Fixed broken locations in Timespinner (#213)
* Fixed mixed up locations for Aelana's chest and pedestal.
Can provide screenshots for proof.

* Fixed mixed up locations for Upper Lake Desolation double jump cave floor and platform.
Can provide screenshots for proof.

* Fixed up mixed locations for:
Aelana's chest and pedestal
Upper desolation double jump cave platform and floor
upper sealedcave after sirends chest 1 and chest 2

* Updated data version from 6 to 7
2022-01-17 23:15:04 +01:00
Hussein Farran
38b98a97d1 Merge from main and reformat tutorials. 2022-01-17 15:37:34 -05:00
Hussein Farran
9599f54b06 Merge branch 'main' into docs_consolidation
# Conflicts:
#	WebHostLib/static/assets/tutorial/archipelago/advanced_settings_en.md
#	WebHostLib/static/assets/tutorial/archipelago/plando_en.md
#	WebHostLib/static/assets/tutorial/archipelago/triggers_en.md
#	WebHostLib/static/assets/tutorial/timespinner/setup_en.md
2022-01-17 15:37:03 -05:00
Fabian Dill
e74333cbd3 MultiServer: remove location hinting from !hint and /hint; add /hint_location 2022-01-16 02:20:37 +01:00
Alchav
6a7e1d920a User-specified random range (#203)
* Add user-specified random range for yaml options
2022-01-16 01:59:40 +01:00
Fabian Dill
0dc714f947 Options: fix verify_keys breaking options containing lists of dicts 2022-01-15 21:20:34 +01:00
espeon65536
62391d3074 Minecraft tracker: replace bed image url, remove game-complete indicator 2022-01-15 21:16:08 +01:00
Grrmo
c507efd920 Corrected mistake in Regions 2022-01-15 21:15:50 +01:00
espeon65536
6641d428a2 oot: check item name for skip child zelda, not the actual item itself 2022-01-15 21:15:28 +01:00
Fabian Dill
b8afc27e2f Docs: improve "sending_visible" comment 2022-01-14 19:27:54 +01:00
Hussein Farran
d577428ac8 Merge pull request #206 from alwaysintreble/tutorials
add requirements mention to plando tutorial
2022-01-14 11:01:29 -05:00
alwaysintreble
fba8019f98 add requirements mention to plando tutorial 2022-01-13 19:00:29 -06:00
Hussein Farran
6f922ac3ac Merge pull request #202 from alwaysintreble/tutorials
Add a new multi trigger example and explain use of "imaginary" options
2022-01-13 09:16:01 -05:00
Fabian Dill
44cf8efc06 WebHost: count non-owned Rooms of a given Seed 2022-01-13 07:41:31 +01:00
Fabian Dill
1990b893e5 WebHost: fix /api/get_rooms and /api/get_seeds 2022-01-13 07:40:26 +01:00
Chris Wilson
684bb736bc [WebHost] weighted-settings: Include new option types when creating the default settings 2022-01-11 18:06:22 -05:00
Chris Wilson
01d6735803 [WebHost] weighted-settings: Accept new options in switch for option type 2022-01-11 18:00:03 -05:00
Chris Wilson
4e674e0380 [WebHost] weighted-settings: Add items-list, locations-list, and custom-list to JSON config file 2022-01-11 17:36:33 -05:00
Fabian Dill
3acd966241 Options: add "VerifyKeys" Mixin and showcase it for OoT Logic Tricks 2022-01-11 22:01:54 +01:00
Chris Wilson
ee190601ee [WebHost] weighted-settings: Minor style fixes 2022-01-11 04:33:27 -05:00
Chris Wilson
240d1423a3 [WebHost] weighted-settings: Fix start_inventory using the wrong data type 2022-01-11 04:20:33 -05:00
Lincoded
e36f6d25b8 Increase contrast of SM tracker
Improve accessibility by changing text to white and page background to black.

Original contrast ratio was 3.88, and new contrast ratio is 5.4
2022-01-11 00:48:27 -08:00
Chris Wilson
9339019308 [WebHost] weighted-settings: Fix a bug in game choice validation 2022-01-11 02:26:11 -05:00
Chris Wilson
9f5a2d1eb3 [WebHost] weighted-settings: Validate settings before allowing game generation or export 2022-01-11 02:01:31 -05:00
Chris Wilson
a0ade9ea31 [WebHost] weighted-settings: Added basic validation before export 2022-01-11 01:56:14 -05:00
Chris Wilson
71c2db0829 [WebHost] weighted-settings: Improved link to /user-content 2022-01-11 01:36:06 -05:00
Chris Wilson
f33a15dc4e [WebHost] weighted-settings: Added a brief description of what a weighted setting is at the top of the page. 2022-01-11 01:34:48 -05:00
Chris Wilson
c330f4a35e [WebHost] weighted-settings: Implement item and location hints 2022-01-11 01:26:12 -05:00
Chris Wilson
fe25c9c483 Improve styling on weighted-settings 2022-01-10 23:20:15 -05:00
Chris Wilson
f6fcff6a73 Fix a typo on the player-settings page 2022-01-10 22:15:24 -05:00
Chris Wilson
d1146b4fbc Add weighted-settings link to player-settings 2022-01-10 22:08:06 -05:00
Grrmo
9be4a91028 Added German Tutorial for Timespinner 2022-01-10 22:08:25 +01:00
Grrmo
6c3a4b8ffc Added German translation for Timespinner (#200)
* German translation of the setup guide
2022-01-10 22:08:15 +01:00
alwaysintreble
faabcd8cb7 remove a double paste that somehow showed up? 2022-01-09 18:05:57 -06:00
alwaysintreble
fc7319564e properly credit @Black-Sliver for his multi trigger 2022-01-09 16:46:51 -06:00
Fabian Dill
061de66397 MultiServer: don't mark a slot as having Activity if a location check was done through Collect 2022-01-09 23:15:50 +01:00
Hussein Farran
55f21e077a Merge pull request #199 from Grrmo/patch-2
Corrected typos and wrong information
2022-01-09 15:22:28 -05:00
alwaysintreble
821f98eb46 Add a new multi trigger example and explain use of "imaginary" options 2022-01-09 14:13:00 -06:00
Hussein Farran
3ca8164326 WebHost: Address PR feedback and run another reformat. 2022-01-09 15:12:36 -05:00
Hussein Farran
88ce841bf6 WebHost: Make links to game settings less redundant in gameinfo pages.
Reformat all tutorial pages using PyCharm reformat.
2022-01-09 14:57:00 -05:00
Hussein Farran
b94d401d09 Merge branch 'main' into docs_consolidation 2022-01-09 14:50:35 -05:00
Hussein Farran
1c3b25d026 Merge pull request #197 from ThePhar/slay-the-spire-faq
WebHost: Wrote basic setup and info guide for Slay the Spire
2022-01-09 14:48:31 -05:00
Grrmo
84ec3d5353 Corrected typos and wrong information
- Game executable names for Linux and Mac were wrong
- Fixed some typos and changed grammar and semantics in some places
2022-01-09 14:47:28 +01:00
Fabian Dill
bde58fb677 LttP: remove "bonus" small key hyrule castle in case of standard + own_dungeons 2022-01-09 04:48:31 +01:00
Fabian Dill
651e22b14a LttP: keep Small Key Hyrule Castle local even if keyshuffle is wished. 2022-01-09 04:32:25 +01:00
Chris Wilson
111b7e204f [WebHost] weighted-settings: Remove debug output 2022-01-08 20:34:19 -05:00
Chris Wilson
9ff3791d9e [WebHost] weighted-settings: Implement Item Pool settings 2022-01-08 19:59:35 -05:00
Chris Wilson
7380df0256 [WebHost] weighted-settings: Add Item Management section, currently non-functional 2022-01-08 16:59:39 -05:00
Fabian Dill
7e32fa1311 WebHost: fix uploading .archipelago files 2022-01-08 21:21:29 +01:00
Zach Parks
0472147e9a Forgot to update link 2022-01-08 19:57:34 +00:00
Zach Parks
68f282ee83 Slay the Spire: Removed redundant sentence 2022-01-08 19:54:53 +00:00
Zach Parks
4909479c42 Slay the Spire: Wrote a basic set-up guide and info guide for StS 2022-01-08 13:49:58 -06:00
Fabian Dill
82e180cca8 WebHost: mark slot counts as exact, now that an entry for each slot is created in DB 2022-01-08 17:11:39 +01:00
Fabian Dill
aff9114c35 0.2.3 2022-01-08 16:12:56 +01:00
Scipio Wright
f656f08f9b Docs: Cherry pick SM guide update from docs consolidation 2022-01-08 15:40:00 +01:00
Alchav
967e3028fd LTTP - Cap item prices at 4x
I think quadrupled prices will be plenty expensive, and this will stop people who pick "random" from getting 9999 priced items and potentially locking their multiworld behind absurd rupee grinds
2022-01-08 04:59:33 +01:00
Alchav
428af55bd9 LTTP shop price modifier tweak
Ensure shop prices are a multiple of 5 after price modifier
2022-01-07 18:11:31 +01:00
espeon65536
340725d395 OoT: add protection on starting inventory to be only giveable items 2022-01-07 16:01:28 +01:00
espeon65536
f8030393c8 OoT: If skip_child_zelda is on, set rule on Song from Impa to be giveable item 2022-01-07 16:01:28 +01:00
Fabian Dill
f6197d0a8d WebHost: add pretty print version of datapackage for human eyes 2022-01-07 03:32:51 +01:00
black-sliver
969ea5e6ee fix triggers for multiple slots from one yaml 2022-01-07 00:54:31 +01:00
Fabian Dill
d4c6268a46 Generate: allow meta to log-fail as opposed to exception-fail if category is missing in target 2022-01-06 22:01:18 +01:00
Fabian Dill
aeda76c058 WebHost: sort games by alphabet 2022-01-06 19:49:26 +01:00
Fabian Dill
9894d0672f Options: allow Choices to be hashed 2022-01-06 17:03:47 +01:00
Scipio Wright
1964547eb3 Minor fix for OoT game info
Changed Ocarina of Time to Zelda's Letter since that's what other world items look like here.
2022-01-06 06:28:58 +01:00
Fabian Dill
d2e884b1d9 Options: allow Toggles to be hashed 2022-01-06 06:18:54 +01:00
Fabian Dill
80b3a5b1d4 WebHost: fix is_zipfile check for flask FileStorage objects
- and assorted cleanup
2022-01-06 06:09:15 +01:00
lordlou
a6a9989fcf SM small improvements (#190)
* added a fallback default starting location instead of failing generation if an invalid one was chosen

* added Nothing and NoEnergy as hint blacklist

added missing NoEnergy as local items and removed it from progression
2022-01-05 20:15:19 +01:00
Scipio Wright
bce63b0dab Update Super Metroid setup tutorial (#188)
* Update Super Metroid setup tutorial

Setup no longer requires Super Metroid Client, and in fact it gives you an error if you use it. Removed references to it and updated step 5 in the snes9x Multitroid and Bizhawk sections.
2022-01-05 16:58:49 +01:00
Hussein Farran
5ca0b6b18e WebHost: Expand remote commands list. 2022-01-04 18:43:01 -05:00
Hussein Farran
19c0508b83 WebHost: Sned out a typo fix. 2022-01-04 18:36:04 -05:00
Hussein Farran
1891c95ae3 WebHost: Fix up more links and expand commands list. 2022-01-04 18:34:00 -05:00
Hussein Farran
a722ec1c37 Merge branch 'main' into docs_consolidation
# Conflicts:
#	WebHostLib/static/assets/tutorial/timespinner/setup_en.md
2022-01-04 17:20:13 -05:00
Jarno Westhof
0c3b5439e9 [Timespinner] Actually use the correct url in setup doc 2022-01-04 23:02:14 +01:00
Jarno Westhof
963e9d4bb5 [Timespinner] Updated timespinner setup docs (#184)
* [Timespinner] Updated setup docs
2022-01-04 22:56:53 +01:00
Fabian Dill
4dd7c63cab Generate: fix accessibility and progression_balancing override 2022-01-04 20:04:02 +01:00
espeon65536
03a892aded OoT updates (#160)
* OoT: disable mixed entrance pools and decoupled entrances for now

* OoT: fix error message crash in get_hint_area

* Oot Adjuster: kill zootdec if it's not the vanilla rom anymore

* OoT Adjuster: fix dmaTable issue
Adjuster should now work on compiled versions of the software

* OoT: don't skip dungeon items shuffled as any_dungeon for barren hints

* OoT: wrap zootdec remove in try-finally
2022-01-04 17:16:09 +01:00
Zach Parks
b3c1c0bbe8 RogueLegacy: Moved world definition from "legacy" to "rogue-legacy" to avoid confusion with deprecation terms 2022-01-04 04:27:51 +01:00
Chris Wilson
5a064b0979 [WebHost] weighted-settings: Ranges with a total distance <= 10 are always printed in full 2022-01-03 19:56:54 -05:00
Zach Parks
f06e565441 Add Rogue Legacy to Archipelago (#180) 2022-01-03 19:12:32 +01:00
Alchav
41fdafa3fb LTTP Shop updates (#177)
* Shop price modifier and non-lttp item price changes

* Item price modifier setting
2022-01-03 03:07:43 +01:00
Chris Wilson
27c528a6b3 [WebHost] weighted-settings: Add random, random-low, and random-high to range options 2022-01-02 19:57:26 -05:00
Chris Wilson
9623c1fffd [WebHost] weighted-settings: Add collapse/expand buttons to game divs 2022-01-02 18:55:38 -05:00
Chris Wilson
d4e0347d1d [WebHost] weighted-settings: Fix footer style and clean up yaml download 2022-01-02 18:45:45 -05:00
Chris Wilson
74bb057314 Implemented range settings 2022-01-02 18:31:15 -05:00
Jarno Westhof
b2980178d1 [Timespinner] Fixed logic of journal 2022-01-03 00:15:52 +01:00
Chris Wilson
08a0871168 Add game-jumping and hint text css to weighted-settings 2022-01-02 16:31:49 -05:00
Jarno Westhof
51fa00399d [Timespinner] Fixed logic for original wayyy up there location 2022-01-02 17:34:05 +01:00
Ross Bemrose
7622f7f28f Timespinner: Fix missing double-jump checks for LoreChecks locations (#181) 2022-01-02 16:33:29 +01:00
Chris Wilson
d98d693369 Remove debug logging 2022-01-01 17:05:08 -05:00
Chris Wilson
c7e8692964 Fix merge conflict. Very minor difference. 2022-01-01 17:02:51 -05:00
Chris Wilson
0431c3fce0 Much more work on weighted-setting page. Still needs support for range options and item/location settings. 2022-01-01 16:59:58 -05:00
Colin Lenzen
411f0e40b6 Timespinner - Add Lore Checks checks (#171) 2022-01-01 20:44:45 +01:00
Jarno Westhof
a5d2046a87 [Docs] More Links (#179)
* [Docs] More Links

* [Docs] Moved link for data package object
2022-01-01 20:29:38 +01:00
Fabian Dill
f8893a7ed3 WebHost: check uploads against zip magic number instead of .zip 2022-01-01 17:18:48 +01:00
Fabian Dill
93ac018400 SNIClient: make SNI finder a bit smarter 2022-01-01 15:46:08 +01:00
Fabian Dill
6b852d6e1a WebHost Options: hidden games should remain functional, just hidden. 2022-01-01 03:12:32 +01:00
Chris Wilson
06dc76a78b Added locations to generated weighted-settings.json. In-progress /weighted-settings page available on WebHost, currently non-functional as I work on JS backend stuff 2021-12-31 14:42:04 -05:00
Hussein Farran
1ff5908a4c Merge branch 'main' into docs_consolidation
# Conflicts:
#	WebHostLib/static/assets/tutorial/archipelago/plando_en.md
#	WebHostLib/static/assets/tutorial/zelda3/multiworld_en.md
2021-12-31 14:30:59 -05:00
Hussein Farran
e2f61636cc WebHost: Undo all softwrapping changes because people don't like it. Fair enough! 2021-12-31 14:12:22 -05:00
Jarno Westhof
4db4b5305e [Docs] Added links to client implementations (#167) 2021-12-31 20:05:36 +01:00
Chris Wilson
c550fdaee8 WebHost now generates a weighted-settings.json file for use with the upcoming weighted-settings page. 2021-12-31 13:22:23 -05:00
Fabian Dill
d13b7988b7 Core: undo change that made Python 3.9 required 2021-12-31 15:08:30 +01:00
Brad Humphrey
d437f0105a Test remaining locations after swapping 2021-12-30 19:06:03 +01:00
Alchav
b65618030f Remove unnecessary logging.info 2021-12-30 16:55:33 +01:00
Alchav
01a2376b74 Let make_dungeon set up items, then replace 2021-12-30 16:55:33 +01:00
Alchav
d10ddb17b6 Let make_dungeon set up items, then replace 2021-12-30 16:55:33 +01:00
Alchav
c42d489bf7 Pull dungeon item replacements from diff extras 2021-12-30 16:55:33 +01:00
Alchav
8fef6b8d8c Add "Start With" option 2021-12-30 16:55:33 +01:00
Alchav
35b1178c20 Add "Start With" option 2021-12-30 16:55:33 +01:00
Alchav
c0f95755ff Add "Start With" option 2021-12-30 16:55:33 +01:00
Alchav
b7676a3da2 Add "Start With" option for dungeon items 2021-12-30 16:55:33 +01:00
Brad Humphrey
3d65719170 Remove dependency on pytest 2021-12-30 16:55:08 +01:00
Brad Humphrey
18d262c1ae Add test for minimal accessibility 2021-12-30 16:55:08 +01:00
Brad Humphrey
e5fedb90a6 Process swaped items last 2021-12-30 16:55:08 +01:00
Brad Humphrey
dc82b384c5 Add comment about swap count 2021-12-30 16:55:08 +01:00
Brad Humphrey
2f56e40fb7 Include player information in swapped item count 2021-12-30 16:55:08 +01:00
Brad Humphrey
d719eb356f Don't allow items to swap infinitly 2021-12-30 16:55:08 +01:00
Brad Humphrey
6a34fe5184 Add fallback item swap for unreachable items 2021-12-30 16:55:08 +01:00
Brad Humphrey
461961c3be Add test locations to region 2021-12-30 16:55:08 +01:00
Brad Humphrey
39869bcdc5 Add basic fill test cases 2021-12-30 16:55:08 +01:00
Jarno Westhof
a10d7ae5b9 [Timespinner] Fixed some placement logics regarding gyre archives & military fortress
Renamed 'Transition chest #' to 'Gyre chest #'
2021-12-30 16:50:04 +01:00
Fabian Dill
4ed45248eb LttP: Rename "Dark World Shop" overworld door to Village of Outcasts Shop. Note: Now the overworld door, Region, Shop and inside door are named the same. 2021-12-29 11:08:23 +01:00
Fabian Dill
6e4b255be5 Options: make common options overridable in a game section
WebHost: add prog balancing and accessibility to settings page
2021-12-28 18:43:52 +01:00
Hussein Farran
2e56c226db WebHost: Patch downloads now prompt you with a dialog box/file save dialog. 2021-12-28 14:18:49 +01:00
Hussein Farran
844ff402cd WebHost: Improve player enumeration performance in upload.py 2021-12-28 14:18:49 +01:00
Hussein Farran
ec570be178 WebHost: Improve performance in player slot tracking during upload. 2021-12-28 14:18:49 +01:00
Hussein Farran
3508cf21c7 WebHost: Add game listing for all players on room info page. 2021-12-28 14:18:49 +01:00
alwaysintreble
1f4ddc295a tutorials: Point lttp tutorial to SNC instead of Z3. Update some deprecated text. 2021-12-27 22:34:57 +01:00
Hussein Farran
3e16593bb7 WebHost: Wrap SoE guide at 120 chars at request of black-sliver. 2021-12-27 16:08:14 -05:00
Jarno Westhof
4ef0e054d6 [TS] Move 3 transition chest under gyre archives flag + some refactoring 2021-12-27 15:39:42 +01:00
Yussur Mustafa Oraji
61310c50d7 Use absolute path when starting SNI
Causes reliability issues when relative path is used.
2021-12-27 15:39:14 +01:00
wafflesoup
6eab838a70 Update plando_en.md
fixed capitalization in Timespinner example
2021-12-25 22:44:07 +01:00
Fabian Dill
52e01c0925 Factorio: fill in some missing doc strings 2021-12-22 14:00:41 +01:00
Fabian Dill
97d6e80556 Bump 2021-12-21 15:31:04 +01:00
Fabian Dill
d5abadc6d0 Requirements: remove no longer used appdirs and move kivy to core 2021-12-20 23:10:04 +01:00
Jarno
d08d716966 [Timespinner] Added orb damage rando flag 2021-12-20 14:40:01 +00:00
Hussein Farran
a864b893b8 WebHost: Newlines must die. 2021-12-19 23:29:04 -05:00
Hussein Farran
9212505243 WebHost: Remove newline from FAQ. 2021-12-19 23:17:25 -05:00
Hussein Farran
abbcb6dc72 WebHost: Remove links to any MSU pack downloads or pages. 2021-12-19 23:06:40 -05:00
Hussein Farran
3f49c169bb WebHost: Remove newlines and rework hyperlinks in Z5 guide. 2021-12-19 23:01:18 -05:00
Hussein Farran
16c8256f0b WebHost: Undo a negative consequence of merging from Main 2021-12-19 22:54:45 -05:00
Hussein Farran
75d94b04aa Merge branch 'main' into docs_consolidation
# Conflicts:
#	WebHostLib/static/assets/gameInfo/en_Secret of Evermore.md
#	WebHostLib/static/assets/tutorial/archipelago/advanced_settings_en.md
#	WebHostLib/static/assets/tutorial/minecraft/minecraft_en.md
2021-12-19 22:53:06 -05:00
Hussein Farran
b9c2e7636c WebHost: Continue hyperlink fixes and consolidate website usage info to website user guide. 2021-12-19 22:41:05 -05:00
Hussein Farran
df29934968 WebHost: Fix hyperlink accessibility in Factorio guide. 2021-12-19 21:21:16 -05:00
espeon65536
3ee4be2e33 Minecraft client: more general search for mod name 2021-12-19 19:15:09 +00:00
black-sliver
9172cc4925 SoE: Update to pyevermizer v0.40.0
see https://github.com/black-sliver/pyevermizer/releases/tag/v0.40.0
2021-12-19 15:22:19 +00:00
black-sliver
7f03a86dee SoE: Rename 'chaos' to 'full' in options
* was changed upstream
* also update tooltips to be a bit more helpful
2021-12-19 15:22:19 +00:00
black-sliver
1603bab1da SoE: Rename difficulty 'Chaos' to 'Mystery' 2021-12-19 15:22:19 +00:00
black-sliver
70aae514be SoE: fix macos wheel urls 2021-12-19 15:22:19 +00:00
black-sliver
5fa1185d6d SoE: make doc point to upstream guide.md 2021-12-19 15:22:19 +00:00
Fabian Dill
3a2a584ad3 Factorio: fix singles layout not generating correctly. 2021-12-18 13:05:43 +01:00
Fabian Dill
c42f53d64f Factorio: add some more tech tree shapes 2021-12-18 13:01:30 +01:00
Jarno Westhof
450e0eacf4 TS: Relaxed entry logic for lower caves 2021-12-17 19:50:38 +00:00
Fabian Dill
aa40e811f1 LttPAdjuster: ignore alttpr cert 2021-12-17 19:17:41 +01:00
CaitSith2
af96f71190 Fix bug where there is less locations than hint count. 2021-12-16 15:34:18 -08:00
Jarno Westhof
9e4cb6ee33 TS: Fixed review comments 2021-12-14 16:04:50 +00:00
Jarno Westhof
5d0748983b TS: removed todo list :D 2021-12-14 16:04:50 +00:00
Jarno Westhof
c4981e4b91 TS: Fixed unit test 2021-12-14 16:04:50 +00:00
Jarno Westhof
3f36c436ad TS: putting items as non local will correctly be handled by your starting orbs and your first progression item
excluding locations now correctly works for your first progression item in an non inverted seed
Aura blast can now be your starting spell
2021-12-14 16:04:50 +00:00
Jarno Westhof
db456cbcf1 TS: no longer reward a progression item if you already have one in your starting inventory 2021-12-14 16:04:50 +00:00
Jarno Westhof
c0b8384319 TS: putting non consumable items in starting inventory will now remove them from the pool so a duplicate wont drop 2021-12-14 16:04:50 +00:00
Jarno Westhof
13036539b7 TS: Starting with Jewelrybox, Talaria or Meyef in your starting inventory will now set the corresponding flag 2021-12-14 16:04:50 +00:00
Jarno Westhof
5a2e477dba Added sanity check to see if all locations can be assigned to regions 2021-12-14 16:04:50 +00:00
TauAkiou
f003c7130f [WebHost] Add Super Metroid support to Web Tracker (#153)
* [WebHost]: Added Super Metroid tracker, based on TimeSpinner & OOT
2021-12-14 17:04:24 +01:00
CaitSith2
0558351a12 Allow update_sprites to work on strict text only systems 2021-12-13 20:24:54 +01:00
Fabian Dill
3f20bdaaa2 WebHost: split autolaunch and autogen services 2021-12-13 05:48:33 +01:00
Fabian Dill
3bf367d630 WebHost: don't bother queuing empty commands 2021-12-13 01:38:07 +01:00
alwaysintreble
706fc19ab4 tutorials: place a missing / oops 2021-12-11 17:04:07 +00:00
espeon65536
4fe024041d Minecraft client: update Forge to 1.17.1-37.1.1
This fixes the critical security issue recently found in Minecraft.
2021-12-10 19:43:57 +00:00
Fabian Dill
7afbf8b45b OoTAdjuster: check on subprocess compressor 2021-12-10 09:53:50 +01:00
Fabian Dill
e1fc44f4e0 Clients: compatibility change for old Intel graphics. 2021-12-10 09:29:59 +01:00
jtoyoda
21fbb545e8 Adding in missing comas in ff1 game info 2021-12-08 14:23:01 +00:00
jtoyoda
6cd08ea8dc Updating ff1 gameinfo 2021-12-08 14:23:01 +00:00
Fabian Dill
85efee1432 SM: raise Exception instead of sys.exit for custom presets 2021-12-08 09:27:58 +01:00
Hussein Farran
ba9974fe2a Update README.md 2021-12-04 23:07:35 +00:00
CaitSith2
98a038e39e Death link default true/false values for super metroid. 2021-12-04 14:04:28 -08:00
Fabian Dill
33477202b9 WebHost: remove outdated data 2021-12-04 22:12:09 +01:00
CaitSith2
9c74d648f8 Tie the need for satellite recipe to satellite goal, not max science pack. 2021-12-04 06:20:16 -08:00
Fabian Dill
feb2e0be03 Factorio: fix selecting wrong goal requirements due to convoluted if tree. 2021-12-04 10:54:11 +01:00
Fabian Dill
84e76eadd9 SM: rename death_link_survive and update docstring 2021-12-03 22:11:25 +01:00
Fabian Dill
c1a73e7839 WebHost: document how to bring up a slot tracker 2021-12-03 20:54:19 +01:00
espeon65536
75625b143c Core: better pretty-print for OptionList when the list is of non-strings 2021-12-03 18:15:10 +00:00
espeon65536
c10e17d24c Minecraft: remove bad default for StartingItems 2021-12-03 18:15:10 +00:00
Fabian Dill
21d465bcb8 CommonClient: add docstring to /ready 2021-12-03 07:04:17 +01:00
Fabian Dill
47c1300f30 Setup: move templates from /Players into /Players/Templates 2021-12-03 07:01:43 +01:00
Fabian Dill
e7d8149d74 LttP Docs: reword instructions to not accidentally overwrite the SNI Connector with an empty file. 2021-12-03 07:01:21 +01:00
eudaimonistic
a3220ac72d Add known safe MSU-1 list
List assembled for use in competitive Zelda restreams.  Permission sought and granted by author Amarith via DM.
2021-12-03 05:08:34 +00:00
Fabian Dill
994621372c MultiServer: finish removing prompt toolkit 2021-12-03 05:24:43 +01:00
Fabian Dill
9d3cbb19f9 Clients: add docstrings to /items and /locations 2021-12-03 05:14:44 +01:00
Hussein Farran
a8694cfb79 WebHost: Fix hyperlink accessibility in general AP guides. 2021-12-02 21:00:06 -05:00
Hussein Farran
0968730382 WebHost: Continue my hyperlink redemption arc. 2021-12-02 20:42:17 -05:00
Fabian Dill
3110763052 WebHost: allow switching out "/tracker/" for "/generic_tracker/" in a tracker url to get the generic tracker for that slot.
No idea where a good place is to sick a link for it. Maybe on the individual trackers pages?
2021-12-03 02:41:56 +01:00
Hussein Farran
71e5348cbb WebHost: I have not been doing hyperlinks in an accessible fashion. I thought I was. I have failed you.
Follow this advice: https://www.imperial.ac.uk/staff/tools-and-reference/web-guide/training-and-events/materials/accessibility/links/
2021-12-02 20:39:24 -05:00
Hussein Farran
5b399bff89 WebHost: Update English Timespinner documentation. 2021-12-02 20:25:19 -05:00
Hussein Farran
b7ff5d9a57 WebHost: Update English Super Metroid documentation. 2021-12-02 20:22:18 -05:00
Hussein Farran
bfa4d06ecf WebHost: Update English Subnautica documentation. 2021-12-02 20:16:38 -05:00
Hussein Farran
d45bbb89b9 WebHost: Update English SoE documentation. 2021-12-02 20:14:53 -05:00
Hussein Farran
40805ee870 WebHost: Update English Minecraft documentation. 2021-12-02 20:04:44 -05:00
Hussein Farran
385f41d461 WebHost: Draft of commands documentation. 2021-12-02 19:53:58 -05:00
CaitSith2
6f12ed38d9 Add in whitelist for overriding blacklist. 2021-12-02 15:27:48 -08:00
CaitSith2
efb4e5a7b3 Use OptionSet for blacklist 2021-12-02 15:27:00 -08:00
CaitSith2
a15689e380 Allow explicit blacklisting (and whitelisting) of free samples from yaml 2021-12-02 09:26:51 -08:00
CaitSith2
548d893eaa Convenient runtime changing of death link status requires 0.2.1 2021-12-01 23:42:09 -08:00
Fabian Dill
1ec9ab5568 CommonClient: make the Server tooltip no longer fullscreen 2021-12-02 07:47:10 +01:00
Fabian Dill
a767d7723c FF1: update some client texts 2021-12-02 07:14:55 +01:00
Fabian Dill
a60c6176be SM: add client version check for DeathLink 2021-12-02 06:13:44 +01:00
lordlou
83cfd6ec05 SM update (#147)
* fixed generations failing when only bosses are unreachable

* - replaced setting maxDiff to infinity with a bool only affecting boss logics if only bosses are left to finish

* fixed failling generations when using 'fun' settings

Accessibility checks are forced to 'items' if restricted locations are used by VARIA following usage of 'fun' settings

* fixed debug logger

* removed unsupported "suits_restriction" option

* fixed generations failing when only bosses are unreachable (using a less intrusive approach for AP)

* - fixed deathlink emptying reserves

- added death_link_survive option that lets player survive when receiving a deathlink if the have non-empty reserves

* - merged death_link and death_link_survive options
2021-12-02 06:11:42 +01:00
black-sliver
f673dfb7cf SNIClient: add #server= to url for soe/wasm client 2021-12-02 04:44:19 +00:00
Fabian Dill
22d8b0ef30 Clients: add hint_location for autofill 2021-12-02 03:14:26 +01:00
CaitSith2
763edf00f2 Satellite now a possible goal for ALL science pack levels, chosen by option.
Satellite unlocks by respective science pack (or by automation in the case of automation science pack)
2021-11-30 23:18:17 -08:00
Fabian Dill
b7128e6ee2 FF1: add to setup 2021-12-01 02:47:08 +01:00
Fabian Dill
db56f4a6b7 Core: bump version to 0.2.1 2021-12-01 02:39:52 +01:00
espeon65536
3fa253bac5 MC: 1.17 support (#120)
* MC: add death_link option

* Minecraft: 1.17 advancements and logic support

* Update Minecraft tracker to 1.17

* Minecraft: add tests for new advancements

* removed jdk/forge download install out of iss and into MinecraftClient.py using flag --install

* Add required_bosses option
choices are none, ender_dragon, wither, both
postgame advancements are set according to the required boss for completion

* fix docstring for PostgameAdvancements

* Minecraft: add starting_items
List of dicts: item, amount, nbt

* Update descriptions for AdvancementGoal and EggShardsRequired

* Minecraft: fix tests for required_bosses attribute

* Minecraft: updated logic for various dragon-related advancements
Split the logic into can_respawn and can_kill dragon
Free the End, Monsters Hunted, The End Again still require both respawn and kill, since the player needs to kill and be credited with the kill
You Need a Mint and Is It a Plane now require only respawn, since the dragon need only be alive; if killed out of logic, it's ok
The Next Generation only requires kill, since the egg spawns regardless of whether the player was credited with the kill or not

* Minecraft client: ignore prereleases unless --prerelease flag is on

* explicitly state all defaults
change structure shuffle and structure compass defaults to true
update install tutorial to point to player-settings page, as well as removing instructions for manual install

* Minecraft client: add Minecraft version check
Adds a minecraft_version field in the apmc, and downloads only mods which contain that version in the name of the .jar file.
This ensures that the client remains compatible even if new mods are released for later versions, since they won't download a mod for a later version than the apmc says.

Co-authored-by: Kono Tyran <Kono.Tyran@gmail.com>
2021-12-01 02:37:11 +01:00
Hussein Farran
52d8da16f6 WebHost: Alter FF1, Factorio, and Archipelago setup guides to consolidate information and remove unnecessary linebreaks. 2021-11-30 20:00:05 -05:00
Hussein Farran
fc210c2d18 WebHost: Make URLs explicit in FAQ and Archipelago category tutorials.
Remove unnecessary newlines and other tweaks.
2021-11-30 19:34:39 -05:00
Hussein Farran
56ef918a10 WebHost: Remove unnecessary linebreaks and reformat links in game info pages. 2021-11-30 19:09:18 -05:00
Fabian Dill
d7509972e4 SNIClient: fix apsoe handling 2021-12-01 01:01:41 +01:00
Fabian Dill
49a0f473ce Docs: add more explanation to text type of JSONMessagePart 2021-11-30 08:25:22 +01:00
Fabian Dill
520e5feefb Docs: add missed JSONMessagePart types 2021-11-30 06:41:50 +01:00
Fabian Dill
0992087e9a MultiServer: add not found to !hint response and color found text
Clients: text parsing fixes
2021-11-30 06:09:40 +01:00
Fabian Dill
246a5c568b Core: add some more types 2021-11-30 05:33:56 +01:00
Hussein Farran
ac02019930 WebHost: Begin removing unnecessary line breaks. 2021-11-29 22:34:28 -05:00
Hussein Farran
5121b0d09b WebHost: Edit Archipelago category guides and enabled two-spaced nested lists. 2021-11-29 22:26:08 -05:00
black-sliver
c083716627 SoE: update tutorial for 0.2.1 2021-11-29 23:29:50 +00:00
alwaysintreble
31c15c257c Fix Military fortress filling with new location names 2021-11-29 23:29:25 +00:00
Fabian Dill
dcb6da30ef FF1: datapackage is no longer custom 2021-11-29 22:28:51 +01:00
Fabian Dill
c46abd7e65 Client UI: allow auto filling !getitem 2021-11-29 21:35:06 +01:00
black-sliver
f478b65815 SoE: update pyevermizer to 0.39.2
+ printf to debug channel
+ better error handling
+ more error checking
2021-11-29 07:25:58 +00:00
Jarno Westhof
8363d1749b [Timespinner] New seed options and new locations checks (#140) 2021-11-28 22:59:34 +01:00
alwaysintreble
b3ae4b86e4 TS: Rename various locations for clarity (#139)
* Rename various locations for clarity
2021-11-28 22:33:51 +01:00
jtoyoda
6566dde8d0 Initial FF1R implementation (#123)
FF1R
2021-11-28 22:32:08 +01:00
Fabian Dill
7b0b243607 MultiServer: remove promp_toolkit 2021-11-28 04:06:30 +01:00
Fabian Dill
d768379a8a CommonClient: move to explicit thread instead of thread executor to allow proper task cancelling. 2021-11-28 03:27:18 +01:00
Fabian Dill
5e84900ac4 Generate: provide version string under _Generator_Version instead of Archipelago 2021-11-28 02:57:15 +01:00
Fabian Dill
73ae180437 Settings: default collect to goal 2021-11-28 02:10:09 +01:00
Fabian Dill
2097164d32 Clients: handle "Too many close matches" for hint auto fill as well 2021-11-28 01:51:13 +01:00
Fabian Dill
9f0a8e6d48 LttP: add hint options "Vendors" and "Full"
LttP: fix hint grammar if a Location isn't an ALttPLocation
2021-11-27 22:58:12 +01:00
Fabian Dill
5ca737886b SoE: fix gameinfo typo 2021-11-27 22:58:12 +01:00
CaitSith2
11285fb0aa Fixed root cause of science-not-invited 9.223e+18 problem. 2021-11-26 09:16:42 -08:00
Fabian Dill
82de3c95e2 Clients: allow use of console input if stdin is available.
Such as unfrozen + gui
2021-11-26 06:02:03 +01:00
CaitSith2
b0bf66bdcb Factorio: more cleanup of code. Makes it easier to add a max liquids allowed option. 2021-11-25 18:28:07 -08:00
Fabian Dill
8af5855af6 Factorio: cleanup and optimize some requirement graph functions 2021-11-26 02:37:15 +01:00
CaitSith2
383d0f1a66 ensure the tech enabling chemical plant gets marked as advancement if required. 2021-11-25 17:04:22 -08:00
CaitSith2
4dfa1e3227 Merge remote-tracking branch 'origin/main' into main 2021-11-25 16:38:43 -08:00
CaitSith2
1a63ed970a fixed bug with not being able to use fluid barrels as last ingredient in balanced recipes.
fluid barrels don't have a direct recipe name to ingredient name match, but instead recipe name is fill-ingredient.
2021-11-25 16:38:33 -08:00
Fabian Dill
5b5d96971e WebHost: cleanup tracker.py #2 2021-11-25 21:10:28 +01:00
Fabian Dill
71767e8b79 WebHost: cleanup tracker.py 2021-11-25 21:04:02 +01:00
Fabian Dill
bd0b7ea80a WebHost: fix some PEP8 2021-11-25 20:48:58 +01:00
CaitSith2
744b12345a hard-code only steam. Water already appears at logistic-science pack, and crude-oil at chemical. 2021-11-25 10:17:23 -08:00
CaitSith2
2770014988 Merge remote-tracking branch 'origin/main' into main 2021-11-25 09:59:54 -08:00
CaitSith2
31b93dc2f4 Clarify not being able hand craft automation science if it has fluids. 2021-11-25 09:59:07 -08:00
Fabian Dill
81397936ef Merge pull request #141 from espeon65536/oot
Ocarina of Time updates
2021-11-25 17:57:31 +00:00
CaitSith2
722af0a3ca Now possible for randomized science packs/silo/satellite recipe to use fluids. 2021-11-25 09:44:01 -08:00
espeon65536
6641b13511 Merge branch 'main' of https://github.com/ArchipelagoMW/Archipelago into oot 2021-11-24 17:57:06 -06:00
Fabian Dill
5a03c0edd6 WebHost: remove /hosted redirect, all current rooms should be migrated. 2021-11-24 23:49:00 +01:00
CaitSith2
9dbafd3b4b Factorio can now change death link state at runtime. 2021-11-24 01:55:36 -08:00
CaitSith2
1f5d1532e3 Move Death Link change tag to Common Client 2021-11-24 01:38:58 -08:00
Fabian Dill
1f61d8322c LttP: Attribute locations to dark/light world if they are directly present in them, ignoring routing requirements. 2021-11-23 22:47:41 +01:00
Fabian Dill
0c27dbe746 CommonClient: add /items and /locations 2021-11-23 21:47:23 +01:00
Fabian Dill
a3951c2621 Factorio: remove Desync detected message.
To my knowledge it has never warned about an actual desync, and even it did, the code right behind it fixes the desync.
2021-11-23 20:17:42 +01:00
Fabian Dill
c381df6563 MultiServer: filter new locations via sets, instead of if and only echo new checks 2021-11-23 20:16:48 +01:00
Fabian Dill
39ff471772 Factorio: add new Recipe Time randomize options 2021-11-23 19:10:26 +01:00
Chris Wilson
33c8d307ed Update Factorio Setup tutorial 2021-11-23 02:25:34 -05:00
Fabian Dill
26b336d6db MultiServer: fix IncompatibleVersion not triggering 2021-11-22 20:32:59 +01:00
Fabian Dill
fbd5bfd382 WebHost: remove duplicate zfile read 2021-11-22 17:57:23 +01:00
Fabian Dill
e0d6503590 Clients: allow accepting "Did you mean" by clicking on the question. 2021-11-22 17:44:14 +01:00
CaitSith2
b10d9040df Fix "could not randomize recipe" when both silo and satellite are...
...randomized recipes.
2021-11-21 18:25:28 -08:00
CaitSith2
415f045fd8 Fix a range bug on min_energy in make_balanced_recipe 2021-11-21 18:24:25 -08:00
Fabian Dill
f4e34372be Clients: remove color markup in clipboard copy 2021-11-21 23:45:15 +01:00
Fabian Dill
50264993b0 MultiServer: allow null exclusions on GetDataPackage 2021-11-21 18:11:51 +01:00
Fabian Dill
45a6598d18 Generate: return of the meta mystery 2021-11-21 18:09:06 +01:00
Fabian Dill
b205972e44 GitHub Hooks: update python 2021-11-21 17:50:20 +01:00
CaitSith2
3d19c39001 Include number of death_link connected clients in status. 2021-11-21 01:37:23 -08:00
espeon65536
428177bdca patch ROMs correctly with MQ spirit 2021-11-21 00:31:44 -06:00
CaitSith2
c21bd11b66 Merge branch 'satellite_victory' into main 2021-11-20 22:24:34 -08:00
CaitSith2
beb4949044 typo whoops 2021-11-20 21:44:16 -08:00
CaitSith2
1b4659276c Add randomized recipe for Satellite. 2021-11-20 21:44:16 -08:00
CaitSith2
affd707717 Add satellite recipe to needed_recipes if required. 2021-11-20 21:44:16 -08:00
CaitSith2
48ed394d02 Require sending a satellite for victory in space-science-pack seeds. 2021-11-20 21:44:16 -08:00
Fabian Dill
4f00f5509f CommonClient: keep command input focus after enter and allow tabbing between inputs 2021-11-21 05:47:19 +01:00
Fabian Dill
47c5c407ef CommonClient: consolidate Connect packet sending 2021-11-21 02:50:24 +01:00
Fabian Dill
a27d09f81a CommonClient: consolidate shutdown handling 2021-11-21 02:02:40 +01:00
espeon65536
2fb765455c OoT: change internal version number
Allows custom music to work with the ootrandomizer patcher for now
2021-11-20 16:34:50 -06:00
espeon65536
639e6f9a6c OoT: plando entrances 2021-11-20 15:36:57 -06:00
Fabian Dill
3e40de72b2 WebHost: add random choice to options 2021-11-20 17:37:08 +01:00
espeon65536
686812ee9e OoT: Add warp song text replacement 2021-11-20 09:49:33 -06:00
Fabian Dill
80c3b8bbca Factorio: always build dynamic advancement flag 2021-11-20 04:47:19 +01:00
Fabian Dill
824b932961 Clients: copyable log labels 2021-11-19 21:25:01 +01:00
Fabian Dill
7c3ba3bc42 Factorio: fix cumulative advancement flagging 2021-11-19 19:44:34 +01:00
Fabian Dill
c638a2cfb6 LttP: remove SM joke hint to reduce confusion 2021-11-18 18:57:31 +01:00
Fabian Dill
6e29101ecf Generate: remove duplicate .txt 2021-11-18 18:54:17 +01:00
CaitSith2
6b4445e122 move webhost configuration sample yaml to docs 2021-11-17 23:39:21 -08:00
CaitSith2
f7e89695e5 Comment the defaults, with instructions to uncomment and change the values. 2021-11-17 23:38:30 -08:00
Fabian Dill
9cb24280fa Clients: log exception to logfile 2021-11-17 22:46:32 +01:00
espeon65536
cf20c0781f OoT: fixed glitched not rolling
set internal value of shuffle_interior_entrances to False instead of 'off'
2021-11-17 17:05:46 +00:00
Fabian Dill
cd1c38515b WebHost: add remaining and collect to options page 2021-11-17 16:58:43 +01:00
Fabian Dill
a5ca4f1611 Options: document exclude locations and start location hints 2021-11-17 16:45:13 +01:00
alwaysintreble
fc022c98f2 Add example using the various options presented 2021-11-17 15:22:27 +00:00
alwaysintreble
52aebc3094 Add advanced settings guide; add additional info to setup guide 2021-11-17 15:22:27 +00:00
lordlou
2ef60c0cd9 [SM] added support for 65535 different player names in ROM (#133)
* added support for 65535 different player names in ROM
2021-11-17 02:31:46 +01:00
Fabian Dill
10411466d8 WebHost: make meta attribute LongStr instead of str 2021-11-16 23:59:40 +01:00
Fabian Dill
a6cfed0da2 reduce playerSettings.yaml to legacy LttP, remove when LttP transition is complete. 2021-11-16 21:39:08 +01:00
Fabian Dill
5d29184801 WebHost: retrieve PATCH_TARGET from config directly 2021-11-16 21:38:34 +01:00
CaitSith2
f4762cb3f2 Provide a sample webhost configuration yaml.
Not fully documented yet.
2021-11-16 11:01:16 -08:00
CaitSith2
899e9331fa Make /connect archipelago.gg:port reflect PATCH_TARGET. 2021-11-16 11:00:36 -08:00
espeon65536
cc3d5e60a1 OoT: ensure that the last entrance placed in a one-way pool doesn't assume the other targets are reachable 2021-11-16 08:24:30 -06:00
Fabian Dill
97f6003582 MultiServer: fix legacy argument passing in websockets 2021-11-15 20:55:21 +01:00
espeon65536
b217e734cb OoT: fixed Spirit compass chest and Silver Gauntlets chest being moved with wrong condition in CSMC 2021-11-15 10:26:13 -06:00
espeon65536
09fb956ba6 OoT Adjuster: remove -comp from patched output rom name 2021-11-15 08:46:23 -06:00
espeon65536
d8dedbe7fa OoT Adjuster: patch death_link flag 2021-11-15 08:45:30 -06:00
espeon65536
b07345cee7 OoT: actually make misc_hints changeable 2021-11-15 08:40:13 -06:00
espeon65536
4709902819 OoT: add misc_hints option 2021-11-15 08:38:32 -06:00
espeon65536
af9ab30bdf OoT: fix potion shop/cow ER validation being always active 2021-11-15 08:36:00 -06:00
espeon65536
f5e82c0417 Add oot adjuster to setup scripts 2021-11-15 08:32:54 -06:00
espeon65536
a9f6317032 clean up imports and errors 2021-11-15 08:14:55 -06:00
espeon65536
cf09c2aa3d OoT Adjuster: add support for adjusting patch files, outputting ROMs 2021-11-14 22:58:56 -06:00
espeon65536
a53d4219b3 OoT Adjuster source code 2021-11-14 16:50:49 -06:00
Fabian Dill
bd8e1f6531 Setup: prevent clicking next when no rom file is selected. 2021-11-14 23:14:52 +01:00
Fabian Dill
3658c9f8e3 Setup: use GetSNESMD5OfFile more 2021-11-14 22:45:49 +01:00
Fabian Dill
6a912c128d Setup: use GetSNESMD5OfFile (by Black Sliver) 2021-11-14 22:37:27 +01:00
Fabian Dill
71f30b72f4 SNIClient: patch and launch SoE 2021-11-14 21:14:22 +01:00
Fabian Dill
2dc8b77ddc Patch: consolidate some if trees 2021-11-14 21:03:17 +01:00
Fabian Dill
16cd2760a4 Super Metroid: more path fixes 2021-11-14 20:51:17 +01:00
black-sliver
55bfc71269 SoE: produce useful error if ROM does not exist 2021-11-14 15:42:22 +00:00
Fabian Dill
d623cd5ce0 Factorio: fix coop sync printing desync detected 2021-11-14 16:04:44 +01:00
Jarno Westhof
4bbf8858b0 Fixed missing newline 2021-11-14 14:24:55 +00:00
Jarno Westhof
5626ff1582 Fixed some routing logic + make two checks more easily available 2021-11-14 14:24:55 +00:00
Fabian Dill
28f5236719 OoT: fix link in english guide 2021-11-14 15:24:01 +01:00
espeon65536
f9e1db41e9 OoT: implement decoupled entrance pools 2021-11-14 07:30:40 -06:00
espeon65536
61ffdff207 OoT: implement mixed entrance pools 2021-11-14 07:06:09 -06:00
espeon65536
3bcd85aa0a OoT: add options for mixed pools and decoupled entrances 2021-11-14 07:05:58 -06:00
espeon65536
8b60a9e2f0 OoT: Add display names to ER options 2021-11-14 06:55:32 -06:00
Fabian Dill
4cd9711de3 Super Metroid: fix some file paths 2021-11-14 05:27:03 +01:00
Fabian Dill
2ffa0d0e7f Utils: ignore SSL Cert when getting IP 2021-11-13 23:14:26 +01:00
Fabian Dill
586af0de1d SNIClient: remove some debug stuff before release 2021-11-13 23:05:39 +01:00
espeon65536
e90b2c3a5c OoT: kill door of time collision while it's opening 2021-11-13 14:07:17 -06:00
espeon65536
3d6c82861a OoT: give a full Slingshot, Bomb Bag, or Bow for skip_child_zelda 2021-11-13 13:52:50 -06:00
Fabian Dill
fc3b8c40be WebHost: handle SM and SoE 2021-11-13 20:52:30 +01:00
espeon65536
54cd32872e Merge branch 'main' of https://github.com/ArchipelagoMW/Archipelago into main 2021-11-13 13:08:54 -06:00
Fabian Dill
c178006acc Readme: add new games 2021-11-13 16:35:24 +01:00
Fabian Dill
4e43166e1f Setup: consolidate some SNES rom handling 2021-11-13 16:32:19 +01:00
lordlou
452026165f [SM] added support for more than 255 players (will print Archipelago for higher player number) (#130)
* added support for more than 255 players (will print Archipelago for higher player number)
2021-11-13 15:40:20 +01:00
Fabian Dill
82b8b313f0 Setup: add Secret of Evermore 2021-11-13 03:33:25 +01:00
Fabian Dill
b529f95798 Merge pull request #121 from black-sliver/soe
Added Secret of Evermore support
2021-11-12 23:54:39 +00:00
Fabian Dill
2d55cf4bbf Merge branch 'main' into soe 2021-11-12 23:47:34 +00:00
black-sliver
62e0e0bb55 SoE: update pyevermizer to 0.39.1
* Fix softlock when talking to drain guy again
* Disable receiving items while screen is fading (avoids crashes while closing fullscreen windows)
2021-11-13 00:42:40 +01:00
Fabian Dill
83a40d4394 Setup: delete LttPClient 2021-11-12 23:47:52 +01:00
Fabian Dill
4937156021 Setup: revamp for SNIClient and Super Metroid 2021-11-12 23:43:22 +01:00
black-sliver
24596899c9 SoE doc: change apclient link to http:// for now 2021-11-12 21:53:43 +01:00
CaitSith2
cd3f0eabfb Actually require military science pack for rocket silo on military or higher. 2021-11-12 08:31:46 -08:00
espeon65536
34af785e87 OoT: fixed a bug where free_scarecrow and entrance shuffles could not be rolled together 2021-11-12 16:23:37 +00:00
CaitSith2
34cfe7d1df Fix error in SNIClient 2021-11-12 06:48:23 -08:00
espeon65536
2f9e530fd8 OoT: fixed a bug where free_scarecrow and entrance shuffles could not be rolled together 2021-11-12 08:20:40 -06:00
Fabian Dill
ca8f6c2439 Post-Merge Cleanup #2 2021-11-12 14:58:48 +01:00
Fabian Dill
4a8ba0575f Post-Merge Cleanup 2021-11-12 14:36:34 +01:00
lordlou
77ec8d4141 Added Super Metroid support (#46)
Varia Randomizer based implementation
LttPClient -> SNIClient
2021-11-12 14:00:11 +01:00
espeon65536
61ae51b30c OoT ER: Interior and Overworld Entrance Shuffle (#128)
* OoT: add ER retry functionality and custom get_all_state
This all_state does not have events, because they need to be gathered in the world.

* OoT: reenable Interior and Overworld entrance shuffle
2021-11-12 13:58:22 +01:00
CaitSith2
f26d2d5f20 Fix task issues 2021-11-11 12:32:42 -08:00
Fabian Dill
fd07bc3f2c LttP: DeathLink: fix an if tree derp 2021-11-11 21:10:26 +01:00
CaitSith2
8316a1902d Move death link byte to sram 2021-11-11 12:07:17 -08:00
Fabian Dill
650fd5d792 LttP: refine DeathLink handling. 2021-11-11 16:09:08 +01:00
Fabian Dill
82d3e4bc92 Docs: document "Archipelago" special IDs 2021-11-11 11:48:09 +01:00
espeon65536
8eb1f0258c OoT Entrance Randomizer (#125)
Add options:
    "shuffle_grotto_entrances": GrottoEntrances,
    "shuffle_dungeon_entrances": DungeonEntrances,
    "owl_drops": OwlDrops,
    "warp_songs": WarpSongs,
    "spawn_positions": SpawnPositions,
Add Logic Trick:
    "Skip King Zora as Adult with Nothing"
2021-11-11 10:42:08 +01:00
espeon65536
80c86f34a4 Fix get_item command in OOTWorld
Was relying on self.nonadvancement_items, now checks if that attribute is present
2021-11-11 09:28:24 +00:00
black-sliver
3ed7b9f60c SoE: reword webhost doc
Thanks Fainspirit
2021-11-11 09:06:22 +01:00
Fabian Dill
77c18ac819 GenericWorld: implement create_item in case a Spectator ever tries to use !getitem. 2021-11-11 00:23:07 +01:00
black-sliver
0d6c23e4f2 SoE: add documentation to webhost 2021-11-11 00:12:31 +01:00
Fabian Dill
ec9ef21cc0 Tests: add create_item test 2021-11-11 00:06:51 +01:00
Fabian Dill
43323e59ce Logging Revamp 2021-11-10 15:35:43 +01:00
black-sliver
9ada4df151 SoE: include base_checksum in apbp 2021-11-10 09:17:27 +01:00
Fabian Dill
d42d77d3d3 Clients: consolidate argument parsing 2021-11-09 12:53:05 +01:00
Fabian Dill
2007549e01 MultiServer: move PrintJSONMessagePart's found to PrintJSON 2021-11-08 19:13:13 +01:00
Hussein Farran
987bbc761a Add found to PrintJSON packet. 2021-11-08 13:10:17 -05:00
CaitSith2
0b096528d4 implement science-not-invited filtering/scaling if that mod is installed
(Max count of research will be set to 10,000 * player_tech_cost) so as to not have an unreasonable amount.  Also,  other player installed mods, and even the infinite techs will have the max science pack level applied to them.)
2021-11-08 10:04:58 -08:00
Fabian Dill
fa56541b3a CommonClient: explicitly set logging handlers, and explicitly set them to unicode. 2021-11-08 18:57:03 +01:00
Hussein Farran
beb15aa99a Update network protocol.md 2021-11-08 12:48:17 -05:00
Fabian Dill
ca9bf48ffa Network: document ConnectUpdate 2021-11-08 16:58:41 +01:00
Fabian Dill
b9941e40c1 LttP: Allow DeathLink to be adjusted post-gen 2021-11-08 16:34:54 +01:00
Fabian Dill
e8639988ce MultiServer: original_cmd to InvalidPacket 2021-11-08 16:07:37 +01:00
black-sliver
c32f3d6e96 SoE: data_version bump, disable topology, clean up 2021-11-07 23:36:06 +01:00
espeon65536
60697cc8ba OoT: add ROM flag for death_link 2021-11-07 22:07:41 +00:00
espeon65536
c0d3f140f3 OoT: add description for website 2021-11-07 17:30:55 +00:00
espeon65536
d5934a88a7 OoT: ASM modifications to allow for more than 255 players 2021-11-07 17:30:55 +00:00
espeon65536
db2731dfb7 OoT: create OOTWorld.hint_rng earlier in generate_output
Otherwise the generator crashes when trying to make Ganondorf's text with hints off.
2021-11-07 17:30:55 +00:00
espeon65536
97ee73d79f OoT: add DeathLink option 2021-11-07 17:30:55 +00:00
espeon65536
48ce19a923 OoT: add theoretical support for more than 255 players 2021-11-07 17:30:55 +00:00
espeon65536
4f28c3fa46 Add documentation to LogicTricks option 2021-11-07 17:30:55 +00:00
black-sliver
449f4ee92f SoE: apply cut slot name to multidata 2021-11-07 15:56:43 +01:00
black-sliver
79041bdf21 update host.yaml for SoE 2021-11-07 15:43:07 +01:00
black-sliver
655d14ed6e SoE: implement everything else 2021-11-07 15:39:58 +01:00
black-sliver
5d0d9c2890 allow requirements to point to urls 2021-11-07 15:39:58 +01:00
black-sliver
f10163e7d2 SoE: implement logic 2021-11-07 15:39:58 +01:00
Fabian Dill
666e3b5333 MultiServer: add JSONMessagePart["player"] 2021-11-07 14:42:05 +01:00
Fabian Dill
2b124aaff4 MultiServer: add time to RoomInfo 2021-11-07 11:37:58 +01:00
zig-for
00d62fc23f Fix "running from source" link 2021-11-07 08:41:38 +00:00
espeon65536
aa87b78dde Overpowered is no longer hard, instead requires Bastion Remnant + iron pick + basic combat to get gold blocks 2021-11-06 19:59:49 +00:00
espeon65536
6c71bd40fb Minecraft: give client the correct number of required egg shards 2021-11-06 19:59:49 +00:00
CaitSith2
ed40043448 Pick recipe with lowest energy cost for ingredient. 2021-11-06 11:49:03 -07:00
Fabian Dill
5cf7e6e24b DeathLink: add support for the cause field #2 2021-11-06 16:17:10 +01:00
Fabian Dill
720ef936da DeathLink: add support for the cause field 2021-11-06 11:19:59 +01:00
Jarno Westhof
30755b2067 Use base DeathLink option 2021-11-06 10:04:21 +00:00
Jarno Westhof
04f67c114e Routing logic fix for underwater check 2021-11-06 10:04:21 +00:00
Jarno Westhof
ea707a0bc5 [TimeSpinner] Serverside DeathLink + Spoiler log extension 2021-11-06 10:04:21 +00:00
Fabian Dill
f43475f33b MultiServer: declare spectators as default goal-finished 2021-11-06 08:19:10 +01:00
Fabian Dill
739d4d0038 Setup: prepare for Python 3.10 2021-11-04 16:48:02 +01:00
Fabian Dill
e756a77c70 MultiServer: implement Tracker tag
Docs: add InvalidPacket
Docs: add known Tags
Docs: add DeathLink
LttPClient: potentially fix DeathLink chaining
2021-11-04 13:23:13 +01:00
Fabian Dill
bcfa5d0a7e MultiServer: remove accidental loop from !status 2021-11-04 09:01:14 +01:00
Fabian Dill
45f92536a6 MultiServer: add !status command 2021-11-04 08:57:27 +01:00
Fabian Dill
6b0b78d8e0 LttPClient: remove accidental logger remnant #2 2021-11-03 23:27:09 +01:00
Fabian Dill
c336cdc5df LttPClient: remove accidental logger remnant 2021-11-03 23:18:59 +01:00
Fabian Dill
6ea8d07c8f WebHost: /api generate add missing hint_cost and forfeit_mode 2021-11-03 22:38:29 +01:00
Fabian Dill
5c25a08dc1 LttPClient: warn when connection is not made to SNI 2021-11-03 19:58:40 +01:00
Fabian Dill
fe7f109127 WebHost: fix CWE-79/CWE-116 2021-11-03 09:33:47 +01:00
Adam Ziegler
583819c4ae LttP, beemizer: support fine-tuned trap replacements (#113)
* update beemizer logic to separate replacement chance and single vs trap chance

* convert beemizer options to new style
2021-11-03 06:34:11 +01:00
Sandra
cb8da2e757 Marks player names with a pair of asterisks if they have completed their goal. 2021-11-03 04:56:54 +00:00
alwaysintreble
fdc96115e4 Created a general triggers and plando guide for Archipelago. (#101) 2021-11-03 05:55:50 +01:00
Fabian Dill
e019ec5ff7 AutoWorld: add spoiler hooks
Factorio: Move Recipes to new spoiler hooks
2021-11-02 12:29:29 +01:00
Fabian Dill
e4838f6d2b LttPClient: add snes write command 2021-11-02 11:12:13 +01:00
espeon65536
10837e75b2 Minecraft: make A Furious Cocktail hard, Free the End postgame 2021-11-02 05:37:40 +00:00
Fabian Dill
46590c3163 CommonClient.py UI: Server bar: allow connecting via pressing enter 2021-11-01 21:43:17 +01:00
Fabian Dill
e64d5c5f17 Network: implement new packet: ConnectUpdate 2021-11-01 20:00:55 +01:00
Fabian Dill
0e0cc0ad16 LttP: Implement DeathLink 2021-11-01 19:37:47 +01:00
Fabian Dill
8ff01ca979 CommonClient.py UI: log full traceback 2021-11-01 06:40:37 +01:00
CaitSith2
9508a9afc6 Fix leaving the window entirely leaves the server label hover text up. 2021-10-31 08:07:37 -07:00
Fabian Dill
704a0e3078 minor cleanup 2021-10-30 07:52:03 +02:00
Fabian Dill
9bf9f2c611 CommonClient.py: keep track of everyone's games. 2021-10-30 07:33:05 +02:00
Fabian Dill
71c869e65b CommonClient.py UI: add version info to Title 2021-10-29 15:19:10 +02:00
Chris Wilson
2897fa4003 Include references to LttPClient in the LttP tutorial 2021-10-29 04:41:56 -04:00
Fabian Dill
7f020857d1 CommonClient.py UI: Add info on "Server:" label hover
CommonClient.py UI: prevent freeze if UI is closed while waiting on text user input
2021-10-29 10:03:51 +02:00
Jarno Westhof
2217a9304d Fixed v card not getting marked
Changed order of A-D cards
2021-10-29 08:03:49 +00:00
Jarno Westhof
5a389b4855 [Timespinner] made method names lowercase + removed commented out code 2021-10-29 08:03:49 +00:00
Jarno Westhof
bdb9b7803c Added timespinner tracker 2021-10-29 08:03:49 +00:00
Jarno Westhof
4622b3fe36 Fixed bug with items variable 2021-10-29 08:03:49 +00:00
Jarno Westhof
402afd15db Split of trackers into game specific parts 2021-10-29 08:03:49 +00:00
Kyle Franz
82aca3bce4 Fix TR small key getting shuffled away 2021-10-26 16:54:42 +00:00
Chris Wilson
756c6554c9 Update Factorio tutorial 2021-10-25 21:32:58 -04:00
Chris Wilson
3b9753aaf4 Add /info page for Minecraft 2021-10-25 17:48:45 -04:00
Fabian Dill
4472ef20fe Factorio: add DeathLink option 2021-10-25 09:58:08 +02:00
Fabian Dill
c152790011 MultiServer: fix a refactor mistake 2021-10-25 08:24:32 +02:00
Fabian Dill
4e3b8a5178 MultiServer: allow sending another Connect, to update tags, uuid, team etc. 2021-10-25 06:57:06 +02:00
Fabian Dill
375a0ff208 Options: verify starting inventory counts are positive for more than just Factorio 2021-10-25 04:13:25 +02:00
Fabian Dill
57831f0eba FactorioClient: address some common issues 2021-10-24 23:22:06 +02:00
Hussein Farran
c9a3f67121 Update network protocol.md 2021-10-22 19:57:32 -04:00
Fabian Dill
6af1f98c88 CommonClient.py UI: add progressbar representing % of checks done.
CommonClient.py UI: add Commands button that points out /help and !help
CommonClient.py: track permissions
CommonClient.py: track missing locations and checked locations in lib
2021-10-22 05:25:09 +02:00
Fabian Dill
8e35372aad Network: add RoomInfo -> Games
Allows clients to only download relevant parts of the datapackage, or to keep ID lookups per-game, and for Bounce to tell if there will be a receiving end.
2021-10-22 04:46:00 +02:00
Fabian Dill
0f4d285223 TextClient UI: hide panel selection when there's only one panel to select.
CommonClient: remove "/connect " if it was accidentally copy-pasted into server bar.
2021-10-22 00:37:20 +02:00
Fabian Dill
192e592cda Docs: coop 2021-10-21 23:07:39 +02:00
Fabian Dill
1c2c1f286f Some cleanup 2021-10-21 21:06:38 +02:00
Fabian Dill
6e25af9493 LttPClient: fix missed ROM_PLAYER_LIMIT 2021-10-21 20:55:01 +02:00
Fabian Dill
050927008a Tests: add "EmptyStateCanReachSomething" 2021-10-21 20:23:13 +02:00
Fabian Dill
2fe5459c56 Core & LttP: remove 255 player limit 2021-10-21 08:15:47 +02:00
Fabian Dill
8fbbaf7fcb LttPClient: try to find linux SNI executable 2021-10-21 06:43:42 +02:00
Hussein Farran
2f5bdc5cf9 Merge pull request #98 from black-sliver/doc-update
add world api documentation
2021-10-20 19:41:39 -04:00
CaitSith2
17833a0bfc documentation corrections 2021-10-20 12:13:25 -07:00
Fabian Dill
f4e71df946 Requirements: update time! 2021-10-20 20:03:56 +02:00
Fabian Dill
be070b79af MultiServer: add !checked command, as it may be useful for coop. 2021-10-20 19:58:07 +02:00
Hussein Farran
ef8eefd3b4 Create en_Risk of Rain 2.md 2021-10-20 06:34:54 +00:00
Fabian Dill
83f46f6b2b Readme: fix tutorials page link 2021-10-20 08:31:00 +02:00
Fabian Dill
6b4bdf569c MultiServer: coop support
Just connect multiple clients to the same slot
2021-10-20 05:56:28 +02:00
Fabian Dill
7a9f6e2a8e Factorio: Prevent invalid item counts in start items. 2021-10-19 23:23:48 +02:00
Fabian Dill
ce95ff65bd CommonClient: give UI a server connect bar 2021-10-19 05:38:17 +02:00
Fabian Dill
28e724da98 WebHostLib.options: move to makedirs instead of mkdir. 2021-10-19 02:50:18 +02:00
Fabian Dill
a43b027cde Subnautica: add an install guide 2021-10-19 02:41:40 +02:00
Fabian Dill
4b5e36ebf2 FactorioClient: >< 2021-10-19 01:49:51 +02:00
Fabian Dill
89c05cfcae FactorioClient: Fix bridge not sending, and limit bridge to run up to once a second.
Setup: Fix LttP Adjuster needs to be installed with generator/lttp
MultiServer: fix duplicate !forfeits
2021-10-19 01:47:11 +02:00
Fabian Dill
f8569db21b Merge remote-tracking branch 'Archipelago/main' into Archipelago_Main 2021-10-18 22:58:45 +02:00
Fabian Dill
34eba2655e MultiServer: add !collect and collect_mode
CommonClient: make missing and checked location lookups faster
FactorioClient: implement reverse grant technologies for collect/forfeit/coop
2021-10-18 22:58:29 +02:00
Chris Wilson
1625860bd9 Add /info page for Super Metroid 2021-10-18 09:06:24 -04:00
Chris Wilson
f3ddfb96f3 Add Super Metroid setup guide, update LttP setup guide 2021-10-18 09:02:52 -04:00
Fabian Dill
66e198cbb6 Merge branch 'rip_compat' into Archipelago_Main
# Conflicts:
#	MultiServer.py
2021-10-18 08:18:28 +02:00
Vince Lund
33c747a881 Accidently changed variable name 2021-10-18 06:11:25 +00:00
Vince Lund
20d61d14e0 Fixed some spelling 2021-10-18 06:11:25 +00:00
Fabian Dill
833de94ab0 Generate: You can now have triggers in a game section that get run after common triggers and after the game is selected. Their format is the same but they can't overwrite game. 2021-10-17 20:53:06 +02:00
Fabian Dill
c8d6250ada WebHost: set default upload limit to 64 MB, as OoT is chonkers.
WebHost: rename .multidata to .archipelago in a missed flash message
WebHost: correctly parse Factorio slot names with "-"
2021-10-16 20:11:26 +02:00
Fabian Dill
d38e1185bb Setup: auto include auto generated yaml files 2021-10-16 19:40:58 +02:00
Fabian Dill
fdb8ae0cb5 FactorioClient: Warn user about the dangers of AppData
Factorio: improve setup guide somewhat
2021-10-16 19:40:27 +02:00
Fabian Dill
b57306beac MultiServer: Don't send password required indicator if the password is empty string (user intention is likely no password) 2021-10-15 22:08:24 +02:00
Fabian Dill
af6e159644 Docs: retarget :48484 links 2021-10-15 17:33:18 +02:00
Fabian Dill
54e50f69e1 Options: various fixes to get_option_name falsely giving get_current_option_name instead. 2021-10-14 19:42:13 +02:00
Fabian Dill
3f415b8265 WebHost: Re-Remove multidata race difference explanation, as it no longer exists. 2021-10-14 19:41:23 +02:00
Hussein Farran
8ccdb56bf1 Merge pull request #104 from alwaysintreble/ror2
Risk of rain 2: Revert breaking naming change
2021-10-14 13:25:34 -04:00
CaitSith2
17ed957c6b Include military science pack in all techs military or higher.
This does mean you have to get military science online to research your silo.
2021-10-14 10:20:56 -07:00
CaitSith2
e4564abe41 Fix tech-maniac achievement for silo spawn. 2021-10-13 07:03:18 -07:00
alwaysintreble
f16b29b16b Merge branch 'main' into ror2 2021-10-12 09:09:11 -05:00
Chris Wilson
ef8af7d618 Move config files and player-settings js files to /generated/configs and /generated/player-settings and update the pages that use them 2021-10-11 21:37:08 -04:00
Chris Wilson
79e33899a8 Supported game page game links now point to the game info page. Added a link below for the settings pages. 2021-10-11 21:20:31 -04:00
Chris Wilson
11fc220d4d Minor wording change on landing page. 2021-10-11 21:13:40 -04:00
Chris Wilson
a94a30168c Greatly improve the Start Playing page 2021-10-11 21:11:37 -04:00
Chris Wilson
19704920a4 TOuch up host game page 2021-10-11 20:58:05 -04:00
Chris Wilson
e4f4c1f1be Add Start Playing page, clean up /generate page 2021-10-11 20:52:30 -04:00
Jarno Westhof
065931cae7 Greatly reduced number of items marked as never_excluded due to the performance implications it brings 2021-10-11 11:55:46 +00:00
Fabian Dill
78443bffac Core: fix missed precollected change 2021-10-11 01:39:25 +02:00
Fabian Dill
a8b105267c WebHost: add hint cost and forfeit mode to webgen page 2021-10-11 00:46:18 +02:00
Fabian Dill
f7bd637073 Core: fix chain != chain.from_iterable 2021-10-11 00:12:00 +02:00
Fabian Dill
3e6f7f0fad WebHost: add /discord redirect 2021-10-10 21:52:58 +02:00
Jarno Westhof
e301b67e49 Greatly improved performance when no locations are excluded 2021-10-10 18:24:31 +00:00
Jarno Westhof
952d878442 Marked items as never exclude + some more refactorings 2021-10-10 18:24:31 +00:00
Fabian Dill
8f66f94ffa WebHost: Generate: Fix dead link 2021-10-10 20:14:11 +02:00
black-sliver
d79acef59e api.md: update precollected for commit# e66a2a7 2021-10-10 18:39:03 +02:00
Fabian Dill
e66a2a7c30 Core: change precollected_items to dict-style
Core: make sure there are enough threads available during generate_output to prevent deadlocks if event waiting is used
2021-10-10 16:50:08 +02:00
black-sliver
2f04b93fdb api.md: add set Location.event in location skeleton 2021-10-10 14:03:33 +02:00
black-sliver
818e99b39d api.md: add exclusions to create_items, fix bug in generate_output 2021-10-10 13:09:18 +02:00
CaitSith2
96ffe95404 hopefully fix lint error 2021-10-09 21:03:03 -07:00
CaitSith2
438e53d25e hints for visible tech should be free no matter who it is for. 2021-10-09 20:48:13 -07:00
CaitSith2
ca4b0acd92 Add !hint_location command.
As it turns out, because factorio location names are 100% identical to factorio item names,  it is impossible without a command that explicitly hints locations to hint a specific factorio location, or any other game where location names match item names.
2021-10-09 20:47:12 -07:00
CaitSith2
f8deb1bd7f Make visible_sending part of AutoWorld. 2021-10-09 20:38:53 -07:00
alwaysintreble
d8de84e417 Revert Item Pickup to ItemPickup because it broke stuff 2021-10-09 22:11:05 -05:00
espeon65536
eb602aedc3 Fill overworld-shuffle dungeon items with logic
Prevents maps and compasses from failing fast fill
2021-10-09 17:32:10 +00:00
Jarno Westhof
b539892cc0 Fixed Timespinner generation *oops* 2021-10-09 13:58:07 +00:00
Jarno Westhof
ba13d2179d Slightly improved docs about permissions flags 2021-10-09 13:58:07 +00:00
Jarno Westhof
c7a315ac97 Refactorings 2021-10-09 13:58:07 +00:00
alwaysintreble
b1fb793ea4 Ror2: fix generation mistake (#100)
* Risk of Rain 2: logic updates

* Risk of Rain 2: move a variable definition so it can be reused. Reverted a change that broke stuff for some reason.

* Documentation update
2021-10-09 15:57:37 +02:00
Fabian Dill
62db9ad982 MultiServer: send RoomUpdate -> permissions if permissions change 2021-10-09 15:24:08 +02:00
black-sliver
652c9943c2 api.md: add to the list of requirements 2021-10-09 14:35:08 +02:00
black-sliver
9f62575abe api.md: add data_version, clarify ids, add precollected_items 2021-10-09 14:29:52 +02:00
black-sliver
2fd87f703e api.md: fix more stuff based on comments 2021-10-09 13:00:50 +02:00
alwaysintreble
d3780cd9d5 Documentation update 2021-10-09 05:55:50 -05:00
black-sliver
0376705e47 api.md: change 'Your World' based on suggestions 2021-10-09 11:28:15 +02:00
black-sliver
f1fddac655 api.md: add item groups, fix typo, reformat long lines 2021-10-09 11:06:41 +02:00
Fabian Dill
6acd08431e Core: fix set_seed seed passthrough 2021-10-09 02:30:46 +02:00
black-sliver
317f7116c4 api.md: Reword some things based on @Ijwu's suggestions 2021-10-09 02:05:55 +02:00
black-sliver
bf8e99140e api.md: Apply second batch of suggestions from code review
Co-authored-by: Hussein Farran <hmfarran@gmail.com>
2021-10-09 01:15:35 +02:00
black-sliver
6c949c3a52 api.md: Apply first batch of suggestions from code review
Co-authored-by: Hussein Farran <hmfarran@gmail.com>
2021-10-09 00:49:47 +02:00
Hussein Farran
76d591bab5 Update adding games.md 2021-10-08 17:20:05 -04:00
alwaysintreble
d10cab824a Merge branch 'ArchipelagoMW:main' into ror2 2021-10-08 13:29:25 -05:00
alwaysintreble
a93d633d25 Risk of Rain 2: move a variable definition so it can be reused. Reverted a change that broke stuff for some reason. 2021-10-08 13:27:23 -05:00
Fabian Dill
9ebab4a382 Core: fix set_seed argument order 2021-10-08 12:16:15 +02:00
alwaysintreble
cd53dcfe43 Fix typo 2021-10-08 10:10:12 +00:00
black-sliver
87ceef230f api.md: remove useless \s, fix mixin example 2021-10-08 00:39:16 +02:00
black-sliver
a06e81a0ba api.md: add logic and output, fixed some typos, added some typos 2021-10-08 00:25:31 +02:00
black-sliver
59e87e0d27 api.md: fix Item.advancement description 2021-10-07 19:53:19 +02:00
black-sliver
76d1460d0f add api.md work-in-progress v3 2021-10-07 19:41:29 +02:00
Fabian Dill
1985423a97 LttP: fix ER spoiler writing 2021-10-07 04:31:03 +02:00
Fabian Dill
f5afc84cd2 Tests: remove a breakpoint condition that was left ;P 2021-10-06 11:41:57 +02:00
Fabian Dill
1217179f8a Tests: Implement generic default options reachability test
Tests: remove duplicate TestDeathMountain.py
LttP: Move er_seeds out of Main
OriBF: Fix Mapstone typo
2021-10-06 11:32:49 +02:00
Fabian Dill
29a207b73e Docs: update networkgraph 2021-10-06 10:46:42 +02:00
Jarno Westhof
f7ecf02beb Added timespinner to graphml 2021-10-06 08:39:39 +00:00
CaitSith2
c5193ffdd9 GT flashing now disabled by reduce flashing. 2021-10-05 21:12:26 -07:00
Fabian Dill
916ba2ea41 Test: test against item/location ID overlap 2021-10-06 02:12:05 +02:00
espeon65536
3348dce122 Core: try-except-else style 2021-10-05 23:52:22 +00:00
espeon65536
53e6ca6e34 Core: better error message for exclusion failure 2021-10-05 23:52:22 +00:00
espeon65536
0fed7f1295 Core: do not error on location exclusion if the location has an ID value 2021-10-05 23:52:22 +00:00
Fabian Dill
6ade832029 Subnautica: fix Aurora Prawn Suit Bay requires laser cutter
Subnautica: add Dunes North Wreck's PDA to the correct wreck
Subnautica: fix typo in Yellow
Subnautica: fix progression tag for many items
Subnautica: move extra items from valuable item pool to fast-fill

Testclient at https://cdn.discordapp.com/attachments/731214280439103580/895047705552904222/ArchipelagoSubnautica.zip
2021-10-05 23:07:03 +02:00
alwaysintreble
50ba9a56f7 Risk of Rain 2: logic updates 2021-10-05 20:23:27 +00:00
alwaysintreble
990141df47 Risk of Rain 2: logic updates 2021-10-04 22:28:40 -05:00
Hussein Farran
50f7541ef7 Update tutorial listing for z5 2021-10-04 18:30:38 -04:00
Hussein Farran
6a6962b3b9 Merge pull request #94 from alwaysintreble/main
Add a general archipelago setup tutorial
2021-10-04 17:40:28 -04:00
Hussein Farran
3314ad0315 Update setup_en.md 2021-10-04 17:38:29 -04:00
Hussein Farran
9a4a96eedd Merge pull request #95 from Edos512/main
Added OOT tutorials
2021-10-04 17:37:21 -04:00
Edos512
ff4a9d1761 Update WebHostLib/static/assets/tutorial/zeldaOOT/zeldaOOT_en.md
Co-authored-by: Hussein Farran <hmfarran@gmail.com>
2021-10-04 22:45:10 +02:00
Edos512
df2d4a557e Update WebHostLib/static/assets/tutorial/zeldaOOT/zeldaOOT_en.md
Co-authored-by: Hussein Farran <hmfarran@gmail.com>
2021-10-04 22:45:03 +02:00
Edos512
d831923a54 Update WebHostLib/static/assets/tutorial/zeldaOOT/zeldaOOT_en.md
Co-authored-by: Hussein Farran <hmfarran@gmail.com>
2021-10-04 22:44:52 +02:00
Edos512
594183d751 Update WebHostLib/static/assets/tutorial/zeldaOOT/zeldaOOT_en.md
Co-authored-by: Hussein Farran <hmfarran@gmail.com>
2021-10-04 22:44:42 +02:00
Edos512
bddaa954ab Update WebHostLib/static/assets/tutorial/zeldaOOT/zeldaOOT_en.md
Co-authored-by: Hussein Farran <hmfarran@gmail.com>
2021-10-04 22:44:37 +02:00
Edos512
f4a7777018 Update WebHostLib/static/assets/tutorial/zeldaOOT/zeldaOOT_en.md
Co-authored-by: Hussein Farran <hmfarran@gmail.com>
2021-10-04 22:44:15 +02:00
Edos512
8fc9a9c55e Update WebHostLib/static/assets/tutorial/zeldaOOT/zeldaOOT_en.md
Co-authored-by: Hussein Farran <hmfarran@gmail.com>
2021-10-04 22:44:09 +02:00
Edos512
8e457d9b8f Merge branch 'ArchipelagoMW:main' into main 2021-10-04 21:52:14 +02:00
Edos512
aa37c9bf81 Update OOT tutorials
Some typos, added how to solve disconnects.
2021-10-04 21:51:50 +02:00
Edos512
89cbd05600 OOT tutorials added 2021-10-04 21:45:54 +02:00
alwaysintreble
a5e9c4af03 Merge remote-tracking branch 'AP_fork/main' into fork 2021-10-03 18:20:16 -05:00
alwaysintreble
ea753cd8bf Added a general archipelago setup tutorial for installing, generating, and hosting multiworlds. 2021-10-03 17:47:55 -05:00
Fabian Dill
46e9fd7ae3 Rules.py: add typing info 2021-10-03 17:22:47 +02:00
Jarno Westhof
96d7277a22 Fixed Timespinner routing + some typing 2021-10-03 13:44:36 +00:00
Fabian Dill
c937167a11 Options: add option start_location_hints, works identical as start_hints, just for locations 2021-10-03 14:40:25 +02:00
espeon65536
0c59ad7e22 OoT: reenable MQ dungeon support 2021-10-03 08:52:29 +00:00
espeon65536
fa1b93252c OoT: place Deku Shields first in closed forest + shopsanity 2021-10-03 08:52:29 +00:00
espeon65536
0d9e186e18 OoT: place shop progression first rather than only tunics 2021-10-03 08:52:29 +00:00
Fabian Dill
b7aa5a17b7 LttP: Bartering, add price types for replacement items 2021-10-02 10:15:00 +02:00
Fabian Dill
d55a057a4d Merge remote-tracking branch 'Archipelago/main' into Archipelago_Main 2021-10-02 07:01:26 +02:00
Fabian Dill
72976da3a4 readme: adjust Z3randomizer link 2021-10-02 07:01:00 +02:00
Fabian Dill
81afbb55cf Core: increment version 2021-10-02 07:00:16 +02:00
Fabian Dill
d1709764ef Merge branch 'new_shops' into Archipelago_Main 2021-10-02 06:58:43 +02:00
Jarno Westhof
4f7e3d7a45 Fixed routing issue for Inverted seeds 2021-10-01 16:05:26 +00:00
espeon65536
4ca53a6ee0 ALttP: fix dungeon exits in HMG and NL if PoD, Hera or SP are there 2021-10-01 16:04:51 +00:00
espeon65536
efe02e2591 allow swamp BK in first chest in hybrid major glitches 2021-10-01 16:04:51 +00:00
Fabian Dill
391f42b4f2 Timespinner: some game info fixes 2021-09-30 19:51:44 +02:00
Jarno Westhof
cff5db446d Fixed some bugs + added documentation + added a few features (#87)
* Refactorings + minor logic fix

* Fixed unnececerly recalculation of item_name_groups

* Enabled other itemId's so that they can be send to client when desired

* Marked the loss of location 1337158

* Updated network graph

* First draft tinmespinner documentation

* Moved personal items to slot_data rather than location scouts

* Disabled Remote Items

* Updated docs

* Fixed port override
2021-09-30 19:51:07 +02:00
Fabian Dill
858d4c74ce Options: fix start_hints 2021-09-30 19:49:36 +02:00
Fabian Dill
f56bf0db73 MultiServer: remove legacy datapackage keys
MultiServer: remove warning about legacy datapackage use
MultiServer: remove legacy permission flags
Options: add "random" option to all Choices
LttP: remove random special handling from HeartColor
2021-09-30 13:22:25 +02:00
Fabian Dill
4801bb1178 Setup: Move Enemizer from generator to generator/lttp
Setup: Add OoT Rom Size calculation
Setup: Add LttP Rom Size calculation
2021-09-30 09:28:40 +02:00
Fabian Dill
8b2433584d CommonClient: allow running it as text client
CommonClient: move logging init to library
Setup: add TextClient
2021-09-30 09:09:21 +02:00
Fabian Dill
bde02f696b Core: add Item.trap property 2021-09-29 05:21:33 +02:00
Fabian Dill
0afbe7988e Core: fix Item.code type and add Item.name type 2021-09-29 04:44:20 +02:00
Fabian Dill
345d4c58f3 Network: Add docs for new permissions mapping and implement it in CommonClient.py 2021-09-28 17:22:23 +02:00
alwaysintreble
6c44ffaf7a Added a general archipelago setup tutorial for installing, generating, and hosting multiworlds. 2021-09-28 10:17:16 -05:00
alwaysintreble
16454dbc33 Increment data version. 2021-09-28 13:00:02 +00:00
alwaysintreble
89c6fd6ac4 Put links back to being separate but still use them as hyperlinks 2021-09-28 13:00:02 +00:00
alwaysintreble
ea8b6e6438 Adjustment to chaos weights. Add progression logic. 2021-09-28 13:00:02 +00:00
alwaysintreble
c0b25e1f6e Adjustment to chaos weights. Add progression logic. 2021-09-28 13:00:02 +00:00
alwaysintreble
df0335f739 Fix formatting on item weight presets page. 2021-09-28 13:00:02 +00:00
alwaysintreble
1ffe5fc7bb Remove scraps only preset since it doesn't work. Increase item pool to 100. Add direct links in tutorial. 2021-09-28 13:00:02 +00:00
CaitSith2
cf070e6dd9 Fixed non-deterministic rocket silo recipe.
get_allowed_packs() was returning a list of the science packs in a non-deterministic random order, resulting in the recipe being non-deterministic.
2021-09-26 14:02:19 -07:00
Fabian Dill
f9a9189687 LttP: actually fix shop shuffle u with grouped_random progressive 2021-09-26 10:09:40 +02:00
Fabian Dill
9daf1abcd9 LttP: fix shop shuffle u with grouped_random progressive 2021-09-26 09:55:54 +02:00
Fabian Dill
8c525a5e33 Datapackage: log custom mode use 2021-09-26 09:10:27 +02:00
Fabian Dill
952a155003 MultiServer: move permissions to an IntEnum 2021-09-26 09:06:12 +02:00
Fabian Dill
7f35f6f8f4 Factorio/LttP: remove some things that were marked for removal 2021-09-26 08:49:32 +02:00
Fabian Dill
8b9e278593 Guides: Link to new LttP player-settings page 2021-09-26 07:24:47 +02:00
Fabian Dill
655ebcdb07 WebHost: allow .json, .yml on /generate 2021-09-26 06:50:46 +02:00
CaitSith2
ac534a6881 no free rocket silo if its recipe is randomized. 2021-09-24 21:26:11 -07:00
Fabian Dill
59529eba4e Timespinner: some reformatting and type fixes 2021-09-25 02:31:32 +02:00
Fabian Dill
1cef10b309 Timespinner: hide it for now 2021-09-25 01:13:50 +02:00
espeon65536
c3070be14a Update small and boss key counters during the normal update cycle 2021-09-24 23:10:26 +00:00
espeon65536
5570440eb1 Ocarina of Time webtracker 2021-09-24 18:44:25 +00:00
espeon65536
ec0a5df5a1 give Song from Impa and ZL as starting items if skip_child_zelda is on 2021-09-24 18:44:25 +00:00
Edos512
8411b76ee5 Update minecraft_es.md (#80)
* Update minecraft_es.md

Updated spanish minecraft tutorial
2021-09-24 20:42:35 +02:00
Edos512
c0ff90fc86 Update minecraft_es.md
Little warning added
2021-09-24 18:29:43 +02:00
Edos512
f9c8816c43 Update minecraft_es.md
Updated spanish minecraft tutorial
2021-09-24 18:23:22 +02:00
Jarno Westhof
822e8941ed Added Timespinner support (#77)
AP side for 0.1.8 inclusion, Client and Documentation outstanding.
2021-09-24 04:07:32 +02:00
Fabian Dill
7ac9bd8591 tracker.py: run Reformat Code 2021-09-23 13:52:32 +02:00
Fluffyhairedguy
68a5784650 New column for generic tracker (#78)
* Adding order received column to generic tracker. Progressive items will have the most recent number only.
2021-09-23 13:48:25 +02:00
Fabian Dill
67f324b939 Spoiler: remove duplicate start inventory entries 2021-09-23 04:08:36 +02:00
Fabian Dill
8db8c60e75 Core: fix start_inventory ignoring count 2021-09-23 03:53:16 +02:00
Fabian Dill
8e569a1d1f AutoWorld: split remote_start_inventory out from remote_items 2021-09-23 03:48:37 +02:00
Fabian Dill
3caf8bc82b WebHost: Allow plando
Maybe move to a different webpage?
2021-09-23 02:29:24 +02:00
Fabian Dill
3da028415f Factorio: fix random rocket recipe 2021-09-22 08:08:57 +02:00
Fabian Dill
104df1915d UI: no longer close Clients on escape key press 2021-09-22 08:08:38 +02:00
CaitSith2
bfb6d44195 Fix failure to roll seeds with silo: randomize_recipe 2021-09-21 23:05:14 -07:00
Chris Wilson
df0e8bc027 Remove aliased options from player-settings pages 2021-09-22 00:21:57 -04:00
Fabian Dill
442b6ced35 Docs: Update network graph 2021-09-20 12:15:31 +02:00
Fabian Dill
111e11924f LttP: fix multithreading racing condition resulting in Ganon giving the wrong prog bow hint, also have one less world.find_items() which is quite cpu expensive 2021-09-20 01:00:09 +02:00
espeon65536
061cc69a6a Convert color and sfx options into top-level definitions for pickling 2021-09-19 05:23:10 +00:00
espeon65536
f9950e1f01 add comment for suns song 2021-09-19 05:23:10 +00:00
espeon65536
895d259589 correctly write memory address for Song from Composers Grave so it's always recognized by client 2021-09-19 05:23:10 +00:00
Chris Wilson
4ea80f34fa Merge branch 'main' of https://github.com/ArchipelagoMW/Archipelago into main 2021-09-18 16:15:50 -04:00
Chris Wilson
77878bf714 Fill out game info pages for LttP, OoT, Factorio, and Subnautica. Revert MD pages to stop using simple line breaks. 2021-09-18 16:15:40 -04:00
Fabian Dill
f85dde6323 LttP: remove rom handling from Main.py 2021-09-18 22:13:19 +02:00
Fabian Dill
6441f92c9f LttP: remove no longer used argument 2021-09-18 06:56:19 +02:00
Chris Wilson
25b9fc8b6a Better wording for player-settings reset banner 2021-09-17 21:24:52 -04:00
Chris Wilson
090678776e Add version hashing to player-settings pages 2021-09-17 21:23:31 -04:00
Chris Wilson
9be6d443d7 Fix /gameInfo pages not loading markdown correctly 2021-09-17 20:39:53 -04:00
Chris Wilson
678253d037 Fix /games page not working 2021-09-17 20:35:31 -04:00
Fabian Dill
bd561fd191 WebHost: fix Py39(only?) jinja weirdness with undefined attribute checking 2021-09-18 01:32:34 +02:00
Fabian Dill
38b5ee7314 WebHost: working web-gen 2021-09-18 01:02:26 +02:00
Chris Wilson
11245462f0 Added gameInfo page using markdown, removed old game sub-pages and directories 2021-09-17 18:41:26 -04:00
Fabian Dill
351a5b87bf Setup: Make OoT and LttP Rom optional components to the Generator 2021-09-17 10:09:03 +02:00
Fabian Dill
b780257098 MultiServer: fix IgnoreGame missing 'not' 2021-09-17 04:35:38 +02:00
Fabian Dill
4e1f1551ea Subnautica: add 'valuable' item_pool 2021-09-17 04:32:36 +02:00
Fabian Dill
b82e3f2a8a MultiServer: Split InvalidSlot out into InvalidGame and document all error codes. 2021-09-17 04:32:09 +02:00
Fabian Dill
a82bf1bb32 Options: raise Exception if per-game options are in root
Options: implement progression balancing and accessibility on new system
Options: implement the notion of "common" and "per_game_common" options in various systems
Options: centralize item and location name checking
Spoiler: prettier print some lists, sets and dicts
WebHost: add common options into /templates
2021-09-17 00:17:54 +02:00
Chris Wilson
abc0220cfa Include the game name in the generated JSON files used to populate player-settings pages 2021-09-16 17:15:25 -04:00
espeon65536
f17e6f9afd Ensure removed items and events do not appear in the starting inventory multidata and web tracker 2021-09-15 10:40:36 +00:00
espeon65536
16e6b9eed7 Ensure that Sheik in Ice Cavern doesn't get a dungeon item 2021-09-15 10:40:36 +00:00
espeon65536
323415ba9c allow gossip hints for light arrows with either vanilla bridge or nonzero trials required 2021-09-15 10:40:36 +00:00
espeon65536
ae97b5e704 Fix drawing AP items in shops 2021-09-15 10:40:36 +00:00
espeon65536
6b8b30c3c7 fix skull token ranges 2021-09-15 10:40:36 +00:00
espeon65536
0df2b2221d Separate triforce pieces in pool from the item pool setting 2021-09-15 10:40:36 +00:00
espeon65536
e2b36dfa7d remove debug print 2021-09-15 10:40:36 +00:00
espeon65536
4e18f24f3b Add glitchless condition to ganon's castle junk fill 2021-09-15 10:40:36 +00:00
espeon65536
b0d5a51768 Add proportional junk fill to Ganon's Castle 2021-09-15 10:40:36 +00:00
espeon65536
b3d2c22373 accidentally optimized a little too much 2021-09-15 10:40:36 +00:00
espeon65536
cace88e8fa Reenable Chest Size Matches Contents 2021-09-15 10:40:36 +00:00
espeon65536
9c09d84c71 Make AP items into Zelda's Letter, with custom text and proper sfx for advancement 2021-09-15 10:40:36 +00:00
espeon65536
2d27665369 Fix shop items having inconsistent save context information, causing shops to not be sent correctly if fewer than 4 items in any shop 2021-09-15 10:40:36 +00:00
espeon65536
45266caa8d make logic_tricks section in playerSettings clearer 2021-09-15 10:40:36 +00:00
espeon65536
feb1a59902 remove unreachable code in _oot_can_live_dmg 2021-09-15 10:40:36 +00:00
espeon65536
fdec4157da Skip looping over every location in set_rules and set_entrances_based_rules, use filter instead 2021-09-15 10:40:36 +00:00
espeon65536
4e84b20925 optimize set_shop_rules 2021-09-15 10:40:36 +00:00
espeon65536
f952ad5913 turn on guarantee_hint rule 2021-09-15 10:40:36 +00:00
espeon65536
be27586203 make stage_generate_output a class method 2021-09-15 10:40:36 +00:00
espeon65536
9dc3f3f38b Hint generation improvements
Only generate the required hint data for a world based on its hint distribution
Set various major items as nonprogression never_exclude based on settings
2021-09-15 10:40:36 +00:00
espeon65536
f39defbe06 Add "async" hint distribution 2021-09-15 10:40:36 +00:00
espeon65536
890f71a477 fix bug causing songs to never be hinted 2021-09-15 10:40:36 +00:00
espeon65536
bc8e8c5daf add oot ROM selection to inno_setup 2021-09-15 10:40:36 +00:00
espeon65536
37f12809a1 commented out some junk hints unsuitable for AP 2021-09-15 10:40:36 +00:00
espeon65536
f5c0b847a9 make defaults for LacsTokens and BridgeTokens not insane 2021-09-15 10:40:36 +00:00
espeon65536
44d6c3c07e oot updates to playerSettings 2021-09-15 10:40:36 +00:00
espeon65536
da1a2b2957 split shopsanity into two options: "shopsanity" and "shop_slots" 2021-09-15 10:40:36 +00:00
espeon65536
9f6fa2bd05 Rework __init__ to use create_items and pre_fill properly
Puts keys into the itempool along with all other items
Fixes a bug where dungeon smallkeys + nondungeon big keys fails generation
Also includes some minor optimizations mostly relating to iterables
2021-09-15 10:40:36 +00:00
Fabian Dill
5d68dc568f Fill: fix non_local_items breaking in single player 2021-09-15 01:02:06 +02:00
Fabian Dill
ee1ea881e8 LttP: fix Enemizer option handover 2021-09-15 00:24:52 +02:00
Fabian Dill
87add88436 Factorio: add stone as red science option 2021-09-13 23:50:43 +02:00
Fabian Dill
7643609e09 Factorio: add iron ore, copper ore and coal to red science pool 2021-09-13 23:26:45 +02:00
Fabian Dill
73727ab0d1 Merge branch 'Archipelago_Main' into new_shops 2021-09-13 03:38:54 +02:00
Fabian Dill
007a393ab5 Generate: don't count the 0th output file. 2021-09-13 03:38:18 +02:00
Fabian Dill
4ed185a155 Merge branch 'Archipelago_Main' into new_shops 2021-09-13 02:52:03 +02:00
Fabian Dill
fbb220ce85 remove pass 2021-09-13 02:51:59 +02:00
Fabian Dill
0c57d35402 CommonClient: reduce blind sleep time of keep_alive 2021-09-13 02:50:51 +02:00
pepperpow
8cc045f370 Fixes to barter pricing min/max, future key logic, spoiler log 2021-09-13 00:50:38 +00:00
Fabian Dill
80c90c0a00 LttP: why is item pool called difficulty again? 2021-09-13 02:03:59 +02:00
Fabian Dill
c1c92647ca LttP: move some simple Toggle options over to new system part 2 2021-09-13 02:01:15 +02:00
Fabian Dill
033adceb6f LttP: move some simple Toggle options over to new system 2021-09-13 01:32:32 +02:00
Fabian Dill
e57e92bfee CommonClient: reduce blind sleep time of keep_alive 2021-09-12 21:15:37 +02:00
Fabian Dill
4d68000692 Shops: limit "funny_prices" to logic free choices 2021-09-12 20:25:08 +02:00
Fabian Dill
44b5423afc Merge remote-tracking branch 'pepper/bartering-lttp' into new_shops 2021-09-12 19:45:33 +02:00
Fabian Dill
a1a7729c3b Docs: point to existing further documentation. 2021-09-11 22:44:48 +02:00
Fabian Dill
071b0eeb77 MultiServer: add datapackage legacy warning 2021-09-11 22:37:24 +02:00
Fabian Dill
fafc17c7d3 Risk of Rain 2: fix missing ItemPickup location (off by one itempool) 2021-09-11 22:14:39 +02:00
Fabian Dill
7599302920 CommonClient: remove leftover debug print 2021-09-11 22:07:54 +02:00
Hussein Farran
7f8d7231a4 Merge pull request #71 from SolventMercury/main
Add documentation for adding games to Archipelago
2021-09-11 15:55:35 -04:00
Fabian Dill
b1196885d7 CommonClient: implement active keep-alive 2021-09-11 03:59:12 +02:00
Fabian Dill
494cfb3c04 Setup Guides: update LttP en and de guides with SNI 2021-09-10 15:20:45 +02:00
Fabian Dill
6a65981103 Plando: support Item plando on any game (up from only LttP) 2021-09-10 04:28:06 +02:00
Fabian Dill
f508f93d69 Risk of Rain 2: fix lunar item removal affects all following worlds' presets 2021-09-10 04:11:01 +02:00
Fabian Dill
411d4434a3 MultiServer: update to websockets 10 and implement new websockets.broadcast 2021-09-09 18:56:52 +02:00
CaitSith2
d41fce6f91 Check if starting item actually exists before trying to give it to player. 2021-09-09 07:44:45 -07:00
Fabian Dill
282e7b4006 FactorioClient: End the log on "No Archipelago mod was loaded. Aborting." if no bridge mod was found.
CommonClient: give separate error for invalid URI
2021-09-09 16:02:45 +02:00
Hussein Farran
b4c3c5deea Merge pull request #70 from alwaysintreble/main
Risk of Rain 2 dynamic item pool
2021-09-08 15:44:47 -04:00
Hussein Farran
683514d891 Merge branch 'main' into main 2021-09-08 15:03:19 -04:00
alwaysintreble
e9beb21a98 Adjusted chaos preset weights to be a bit more chaotic and optimized item pool generation a bit. 2021-09-08 13:53:06 -05:00
Hussein Farran
bc47f78264 Remove colons in headings. 2021-09-08 13:46:31 -04:00
Hussein Farran
b002f7f862 Update document with relative images and links, as well as updated language and formatting. 2021-09-08 13:43:39 -04:00
alwaysintreble
05dac999a8 Fixed chaos item weight preset so it gets generated per world. 2021-09-08 12:29:29 -05:00
SolventMercury
242595b725 Added guide for adding new games to AP 2021-09-08 07:05:19 -07:00
SolventMercury
48dd1a1aa6 Revert "Add Terraria Support"
This reverts commit 3aacaffe6b.
2021-09-08 07:02:12 -07:00
SolventMercury
3aacaffe6b Add Terraria Support
But it works this time.
Hopefully.
Client still needs to be caught up.
2021-09-08 05:58:34 -07:00
Chris Wilson
61b875256f Add table styling to markdown css 2021-09-07 19:41:04 -04:00
Chris Wilson
14dc450631 Merge branch 'main' of https://github.com/ArchipelagoMW/Archipelago into main 2021-09-07 19:22:18 -04:00
Chris Wilson
6352056528 Enable QOL features in showdown extension 2021-09-07 19:22:04 -04:00
alwaysintreble
bd4f24844b Update documentation for new options. 2021-09-07 17:50:43 -05:00
alwaysintreble
062615b6b1 Revert "Added descriptions to all currently existing options. Please end me"
This reverts commit 29ed40051d.
2021-09-07 17:24:25 -05:00
alwaysintreble
6c9293e4f6 Added a dynamicallly loaded item weight pool with presets. 2021-09-07 17:14:20 -05:00
alwaysintreble
24802d64c7 Reverted some changes 2021-09-07 09:22:12 -05:00
alwaysintreble
5e8a686bb6 Merge remote-tracking branch 'AP_upstream/main' into dev 2021-09-07 08:29:36 -05:00
Jarno Westhof
279ab89a61 Fixed Typo 2021-09-07 08:27:44 +00:00
alwaysintreble
29ed40051d Added descriptions to all currently existing options. Please end me 2021-09-06 23:58:59 -05:00
alwaysintreble
8d05aa6262 Added a custom dynamic item weights pool option. 2021-09-06 23:40:39 -05:00
alwaysintreble
694f942c06 Renamed RiskOfRainItem to RiskOfRain2Item to prevent any potential problems if someone adds Risk of Rain 1 2021-09-06 23:39:27 -05:00
Fabian Dill
105a2d4e13 WebHost: make LttP sprites optional 2021-09-07 00:42:02 +02:00
Hussein Farran
1ee62912fd Merge branch 'main' into main 2021-09-06 13:19:33 -04:00
Hussein Farran
abacca34ee Add startWithDio to slot_data. 2021-09-06 13:15:07 -04:00
Fainspirit
3e4e69735e Fixed awkward phrasing in FAQ 2021-09-05 12:38:07 +00:00
Chris Wilson
4afc351933 Fill out FAQ on the website 2021-09-04 19:23:09 -04:00
Fabian Dill
23b8070b9d Options: allow comparing Choices with other Choices 2021-09-04 17:53:09 +02:00
Fabian Dill
e53b5324f5 Ocarina of Time: remove 32 bit windows executables, as AP never supported it 2021-09-04 14:38:34 +02:00
espeon65536
25bbbdbecd oot hotfix (again) (#66)
* fix hint failure on multigame multiworlds with oot
2021-09-04 14:37:10 +02:00
Fabian Dill
d739d04380 Setup: don't accidentally remove OoT executables. 2021-09-04 03:44:03 +02:00
espeon65536
f7da0265c4 reference __file__ for oot data path 2021-09-04 01:04:15 +00:00
espeon65536
82ae21420d Move hint info gathering to stage_generate_output
only loops over world locations once rather than many times
2021-09-04 01:04:15 +00:00
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
pepperpow
fc62b4e0bd Bartering Update 2021-09-03 13:26:30 -05: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
Hussein Farran
df435eb693 Remove total_items option. 2021-09-01 17:35:16 -04: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
Fabian Dill
1b4762715c print final output name 2021-07-25 16:15:51 +02:00
Fabian Dill
5c21538553 redirect old /hosted to new /room 2021-07-25 16:07:51 +02:00
David St-Louis
85481d7321 Added water filtration and added location positions (#32) 2021-07-25 15:33:47 +02:00
Chris Wilson
153fa16bcf Redirect user to 404 page for non-existing player-settings pages 2021-07-24 23:20:46 -04:00
Chris Wilson
71642f494f Automatically generate and save player settings for every game 2021-07-24 23:09:34 -04:00
Chris Wilson
8ba408385b Update options.py to generate JSON files to be used with player-settings pages 2021-07-24 21:27:56 -04:00
Fabian Dill
d2c420a1fd fix MultiServer file dailog ending targeting 2021-07-25 03:17:22 +02:00
Fabian Dill
855ff480a5 Require Factorio Client with World Gen capability 2021-07-25 03:13:13 +02:00
Fabian Dill
eb586aab55 add empty Subnautica section to playerSettings.yaml 2021-07-24 15:47:52 +02:00
Fabian Dill
b097f30f4d correctly ignore base weights file in generate 2021-07-24 14:42:34 +02:00
Fabian Dill
78f565c706 renamed /hosted/ to /room/
remove no longer used options
allow loading of json data files from webhost when it gets run by gunicorn and similar
2021-07-24 14:08:45 +02:00
Fabian Dill
af30d8b7cd ensure Hyrule Castle Small Key locality in standard + small key shuffle 2021-07-24 01:42:00 +02:00
espeon65536
e79a918c03 Minecraft updates (#29)
* Implement excluded locations

* Update Minecraft to use exclusion_rules for its exclusion pools

* Flag the enchanted books as advancement so they don't go on excluded locations (particularly the Infinity book)

* update playerSettings for exclusion

* new items: 32 Arrows, Saddle, structure compasses for overworld structures

* move structure linking to create_regions instead of generate_basic

* Update Minecraft to use LogicMixin

* add separate can_exclude property, so non-progression items can be marked non-excluded

* separate fill step for nonadvancement nonexcluded items

* made Saddle not a progression item, but also nonexcluded

* fix missing player arg

* remove higher xp amounts from pool, leaving only 50 XP

* fix new Minecraft item IDs

* added shulker box item for starting inventory

* increment client and data version

* change client_version to int instead of tuple

* make saddle a progression item

* added structure compass option and appropriate logic for all compasses

* Update playerSettings.yaml with MC options

* update minecraft tests

* update exclusion procedure for clarity
2021-07-24 01:28:16 +02:00
Daivuk
83dc92c6a5 Added missing PDA location and tweaked item count 2021-07-23 23:25:14 +00:00
espeon65536
64c80c32f0 update exclusion procedure for clarity 2021-07-23 18:18:32 +00:00
espeon65536
12eba33dbf separate fill step for nonadvancement nonexcluded items 2021-07-23 18:18:32 +00:00
espeon65536
0eee1f2d01 add separate can_exclude property, so non-progression items can be marked non-excluded 2021-07-23 18:18:32 +00:00
Fabian Dill
39a5921522 round of post-test fixes 2021-07-23 20:04:51 +02:00
Fabian Dill
c99a689504 Merge remote-tracking branch 'Daivuk/subnautica_clean' into subnautica_test 2021-07-23 15:49:23 +02:00
black-sliver
997a3e18a3 Factorio: add remaining world_gen options, reformat 2021-07-23 10:21:03 +00:00
Fabian Dill
15747f48e9 fix LttP create_regions 2021-07-23 12:03:19 +02:00
Fabian Dill
d62b46f6cd remove outdated comment 2021-07-23 11:31:09 +02:00
Daivuk
d406e4c3d9 Added Subnautica Support 2021-07-22 20:30:33 -04:00
Fabian Dill
fc7d37def4 MultiServer.py: when loading a .zip, create the .archipelago next to it to consistently load the same savegame. 2021-07-23 02:27:45 +02:00
Fabian Dill
f6b3dfe5ba MultiServer: allow loading a .zip containing a .archipelago directly. 2021-07-23 02:19:41 +02:00
Fabian Dill
fe9094dedc re-order Generate.py imports to ensure ModuleUpdate.py works 2021-07-23 01:57:45 +02:00
Fabian Dill
34ff5d9662 create options files on WebHost startup 2021-07-22 18:21:31 +02:00
Fabian Dill
df9bad75ea fix unittests 2021-07-22 15:59:37 +02:00
Fabian Dill
21af3bf563 move LttP create_regions and set_rules to AutoWorld 2021-07-22 15:51:50 +02:00
Fabian Dill
b2f5f095fc turns out windows' built in zip hates LZMA
also fix APMC output path
2021-07-22 01:08:44 +02:00
black-sliver
8a1ac566c8 FactorioClient: wait for instance to stop 2021-07-21 21:33:51 +00:00
Fabian Dill
75bf595f86 add (cached) /api/datapackage_version endpoint 2021-07-21 23:04:22 +02:00
Fabian Dill
312f13e254 add (cached) /api/datapackge endpoint 2021-07-21 22:55:44 +02:00
Fabian Dill
2fc4006dfa RIP: MultiMystery and Mystery, now there's just Generate
Other changes:
host.yaml Multi Mystery options were moved and changed
generate_output now has an output_directory argument
MultiWorld.get_game_players(<game>) now replaces <game>_player_ids
Python venv should now work properly
2021-07-21 18:08:15 +02:00
Fabian Dill
47f7ec16c0 FactorioClient.py: correctly pass keyword arguments to Factorio 2021-07-21 14:42:33 +02:00
Fabian Dill
e105616b96 use dynamic item name groups in State 2021-07-21 09:45:15 +02:00
Fabian Dill
a503134533 remove Gui.py, GuiUtils.py, ER_hint_reference.txt and icon.ico from root directory 2021-07-21 09:18:28 +02:00
Fabian Dill
bceb8540a1 assorted fixes 2021-07-20 21:19:53 +02:00
black-sliver
10c6a70696 Auto-validate Option.schema, Factorio: allow setting pollution values 2021-07-20 18:39:01 +00:00
Hussein Farran
b809d76b79 Move setting of MultiWorld.is_race to MultiWorld.secure. 2021-07-20 18:38:38 +00:00
Hussein Farran
bfad85223b Add race flag to APMC if AP is run with the race arg. 2021-07-20 18:38:38 +00:00
Fabian Dill
b53c5593a8 Automate dumpSprites.py - and then remove it 2021-07-20 18:23:06 +02:00
Fabian Dill
3bfb98a1c6 remove old Factorio tech tree layouts 2021-07-20 13:16:12 +02:00
Fabian Dill
573fde4bbc Merge together FactorioClient.py and FactorioClientGUI.py
Add cmd arguments
Add kivy style file, allowing users to modify it
2021-07-19 21:52:08 +02:00
Fabian Dill
5c8a076790 Add Ori and the Blind Forest
TODO: Mapstone counting, Open, OpenWorld, connection rules, goals
2021-07-16 12:41:37 +02:00
Fabian Dill
20b173453d (for now) only collect ER hint info for LttP
Optimize Entrance
2021-07-16 12:23:05 +02:00
Fabian Dill
3460c9f714 HK: use new logic mixin names 2021-07-15 13:33:24 +02:00
Fabian Dill
69a5bf0159 Add LogicMixin 2021-07-15 13:31:33 +02:00
Fabian Dill
01f0f309d1 add AutoWorld.generate_early, optimize Location 2021-07-15 08:50:08 +02:00
espeon65536
3d67e1dbdb move structure linking to create_regions instead of generate_basic 2021-07-15 06:04:09 +00:00
espeon65536
719e21ac8c update playerSettings for exclusion 2021-07-15 06:04:09 +00:00
espeon65536
14ed3b82a0 Flag the enchanted books as advancement so they don't go on excluded locations (particularly the Infinity book) 2021-07-15 06:04:09 +00:00
espeon65536
9e5e43fcd5 Update Minecraft to use exclusion_rules for its exclusion pools 2021-07-15 06:04:09 +00:00
espeon65536
7493b7f35e Implement excluded locations 2021-07-15 06:04:09 +00:00
Fabian Dill
54b3a57f46 fix GetDataPackage exclusions 2021-07-14 10:35:00 +02:00
Fabian Dill
4f998a6880 Documentation: now in repository. Programming documentation should be in /docs, player/user documentation should be in /WebHostLib/static/assets/tutorial.
Network: implement InvalidPacket, remove InvalidArguments and InvalidCmd
Datapackage: implement per-game versions and per-game package retrieval
2021-07-14 10:02:39 +02:00
Fabian Dill
62a6cdc9f7 allow remote_items to be set via AutoWorld 2021-07-13 19:14:57 +02:00
Fabian Dill
bc83dfa9e2 Factorio: allow artillery-shell stack to 10 2021-07-13 05:13:38 +02:00
Fabian Dill
5adbab1d2b fix FactorioClient not applying world gen preset 2021-07-13 03:44:41 +02:00
Fabian Dill
b0c1a7acce Remove remaining ALTTP import from CommonClient.py and fix /missing in FactorioClient.py 2021-07-12 20:07:02 +02:00
Fabian Dill
14cadbf80d Filter events out of datapackage 2021-07-12 18:47:58 +02:00
Fabian Dill
741ab3e45c cleanup some MC 2021-07-12 18:16:03 +02:00
Fabian Dill
f456dba993 newstyle DataPackage. Both versions in merged format for compatibility for now. 2021-07-12 18:05:46 +02:00
Fabian Dill
50a21fbd74 MultiServer: remove message that could never trigger in current protocol 2021-07-12 15:40:31 +02:00
Fabian Dill
768ae584d3 AutoWorld: add hint_blacklist, automatically generated all_names
MultiServer: revamp hint commands with AutoWorld
2021-07-12 15:33:20 +02:00
Fabian Dill
ae32315bf7 add World.location_names 2021-07-12 15:11:48 +02:00
Fabian Dill
9821e05386 Fix: When sending items via send or getitem, only consider items that belong to that world
Fix: Allow cheat-sending items into unconnected slots
2021-07-12 14:35:44 +02:00
Fabian Dill
38bc3d47ad Fix: No longer allow setting starting items from other games 2021-07-12 14:32:39 +02:00
Fabian Dill
4feb3bf411 Fix: No longer allow setting items from other games to local/non-local 2021-07-12 14:32:22 +02:00
Fabian Dill
b53d6c370b AutoWorld: remove Games Enum (AutoWorldRegister.world_types replaces it) 2021-07-12 14:10:49 +02:00
Fabian Dill
31c550d410 AutoWorld: basic Item handling 2021-07-12 13:54:47 +02:00
Fabian Dill
babd809fa6 Factorio: fix certain recipes (like steel-plate) not getting their crafting time adjusted correctly. 2021-07-10 08:01:39 +02:00
Fabian Dill
54177c7064 bump required LttP Client Version 2021-07-10 07:37:56 +02:00
Fabian Dill
4884184e4a fix autolauncher import 2021-07-09 22:47:35 +02:00
Fabian Dill
4c7ef593be Some optimizations 2021-07-09 17:44:24 +02:00
Fabian Dill
2600e9a805 Factorio: add coal liquefaction and kovarex process to progressive processing 2021-07-09 04:49:19 +02:00
Fabian Dill
6ac74f5686 Mystery: mention failing option name 2021-07-09 03:06:16 +02:00
Fabian Dill
172c1789a8 introduce World.topology_present, to indicate if any meaningful path information is available in the world 2021-07-08 11:07:41 +02:00
Fabian Dill
ffc00b7800 Factorio: fix progressive science pack order 2021-07-08 05:09:34 +02:00
Fabian Dill
f44f015cb9 typo in playerSettings.yaml 2021-07-08 00:02:17 +02:00
Fabian Dill
a4dcda16c1 LttP: update SNI handling to v34 2021-07-07 22:53:01 +02:00
Fabian Dill
9db506ef42 Factorio: recipe randomization (rocket-part and science-packs only for now) 2021-07-07 10:14:58 +02:00
Fabian Dill
007f2caecf LttP: SNI keeps its log outside its folder 2021-07-07 03:49:29 +02:00
Fabian Dill
80a5845695 LttP: Move over to SNI 2021-07-07 03:45:27 +02:00
Fabian Dill
1b5525a8c5 Factorio: fix uranium-ore recipe writing to mod 2021-07-06 23:55:30 +02:00
Fabian Dill
22d45b9571 Factorio: remove loaders from recipe list 2021-07-06 13:15:03 +02:00
Fabian Dill
773602169d Factorio: fix some form mistakes that didn't break anything (yet) 2021-07-06 13:06:45 +02:00
Fabian Dill
b650d3d9e6 Factorio: include recipe amounts in Recipe data 2021-07-06 12:35:27 +02:00
Fabian Dill
9b2171088e Factorio: mark all potential rocket recipe ingredients as advancements 2021-07-06 12:33:33 +02:00
Fabian Dill
e58ae58e24 Factorio: add Progressive Option 2021-07-04 22:21:53 +02:00
Fabian Dill
a11e840d36 Cache some MultiWorld properties 2021-07-04 16:44:27 +02:00
Fabian Dill
7d5b20ccfc Remove temporary solution "OptionSets" in favor of AutoWorld's Options 2021-07-04 16:18:21 +02:00
Fabian Dill
2530d28c9d Move Progressive Items to AutoWorld 2021-07-04 15:47:11 +02:00
Fabian Dill
c669bc3e7f Factorio: correctly display player names with spaces and detect desyncs 2021-07-04 15:25:56 +02:00
espeon65536
5943c8975a fixing the tests for bees again 2021-07-03 01:55:47 +00:00
espeon65536
d9f97f6aad Improve option retrieval to fix test crashing 2021-07-03 01:55:47 +00:00
espeon65536
576521229c Added option for MC bee traps 2021-07-03 01:55:47 +00:00
Fabian Dill
ac919f72a8 Factorio: update setup 2021-07-03 00:30:00 +02:00
Fabian Dill
85ce2aff47 Factorio: RIP Bridge File 2021-07-02 20:52:06 +02:00
Fabian Dill
8030db03ad Merge remote-tracking branch 'Espeon/minecraft' into Archipelago_Main 2021-07-02 20:14:34 +02:00
espeon65536
1e90470862 increment MC client version and network_data_package version 2021-07-02 10:12:06 -05:00
espeon65536
e37ca97bde add bee traps 2021-07-02 10:10:35 -05:00
Fabian Dill
97f45f5d96 FactorioClient:
fix reconnect
add auto-world-gen

todo:
move remaining script output bridge to rcon
2021-07-02 01:58:03 +02:00
Fabian Dill
0a64caf4c5 add Factorio world gen settings 2021-07-02 01:29:49 +02:00
Fabian Dill
eee6fc0f10 increment version 2021-07-01 21:18:08 +02:00
Fabian Dill
60972e026b send packed NetworkItem in PrintJSON 2021-06-30 20:57:00 +02:00
Fabian Dill
fd9123610b mimic ItemSend fields of PrintJSON for hints 2021-06-30 20:45:06 +02:00
alwaysintreble
6458653812 Update Text.py 2021-06-29 22:00:06 +00:00
Fabian Dill
328d448ab2 Auto import worlds to trigger registration 2021-06-29 03:49:29 +02:00
Fabian Dill
10aca70879 update Flask-Compress 2021-06-29 03:27:31 +02:00
Fabian Dill
92edc68890 update prompt_toolkit 2021-06-29 03:26:16 +02:00
Fabian Dill
4d4af9d74e WebHost: Guard each Room via file-lock 2021-06-29 03:11:48 +02:00
espeon65536
92c21de61d Merge branch 'main' of https://github.com/ArchipelagoMW/Archipelago into minecraft 2021-06-28 14:45:20 -05:00
espeon65536
f918d34098 un-disabled villages spawning in nether 2021-06-28 14:41:33 -05:00
Fabian Dill
95e0f551e8 LttP Client: restore auto-snes 2021-06-28 01:27:52 +02:00
espeon65536
43e17f82b0 Updated HK test to use autoworld 2021-06-27 23:26:24 +00:00
espeon65536
c7417623e6 Converted Hollow Knight to AutoWorld system 2021-06-27 23:26:24 +00:00
black-sliver
50ed657b0e Allow running MultiMystery.py from source on linux 2021-06-27 18:00:36 +00:00
Fabian Dill
8b5d7028f7 decrement Factorio Client version
(for now, as nobody has that client yet)
2021-06-27 05:18:44 +02:00
Chris Wilson
aa28b3887f Apply Dewin's suggested filter to the Z3 Player Tracker 2021-06-26 22:32:29 -04:00
Fabian Dill
739b563bc2 Move required Client Version to AutoWorld 2021-06-27 00:23:42 +02:00
Fabian Dill
a3a68de341 Factorio: only create events for required technologies 2021-06-26 06:05:38 +02:00
espeon65536
57c761aa7d Made AdvancementGoal a Range again
also fixed the awful rules formatting
2021-06-25 20:15:07 -05:00
espeon65536
75891b2d38 fix tests again 2021-06-25 19:59:44 -05:00
espeon65536
44943f6bf8 Merge branch 'main' of https://github.com/ArchipelagoMW/Archipelago into minecraft 2021-06-25 19:44:15 -05:00
Fabian Dill
5fdcd2d7c7 Factorio: locale formatting fixes 2021-06-26 00:54:27 +02:00
Fabian Dill
43e3c84635 fix the Hollow Knight Unittest. Yes, the one test. 2021-06-25 23:39:22 +02:00
Fabian Dill
7f8bb10fc5 Move Factorio, Hollow Knight and Minecraft Options into AutoWorld 2021-06-25 23:32:13 +02:00
Chris Wilson
cc85edafc4 Add "Host Game" button back to the website landing page 2021-06-25 16:59:59 -04:00
Fabian Dill
878ab33039 Factorio: fix incomplete crafting category copy 2021-06-25 22:09:09 +02:00
Fabian Dill
4b495557cd Tracker: sort numbers and fractions numerically 2021-06-25 21:15:54 +02:00
Fabian Dill
d1fd1cd788 Tracker: sort Last Activity numerically, instead of text. 2021-06-25 21:05:44 +02:00
Fabian Dill
f870bb3fad MultiServer:
implement a hint recheck that triggers on get_save()
Still torn if I want a single hint list per team and filter on demand, or have filtered lists and re_check on demand.
2021-06-25 21:04:37 +02:00
espeon65536
719f9d7d48 Monsters Hunted made a hard-postgame advancement, so both flags must be set for it to be not junkfilled 2021-06-25 13:57:09 -05:00
espeon65536
fd811bfd1b fix minecraft tests 2021-06-25 13:02:45 -05:00
espeon65536
6837cd2917 Require the ability to respawn the dragon for all dragon-related advancements 2021-06-25 12:43:59 -05:00
espeon65536
f778a263a7 Forbid villages from spawning in the Nether 2021-06-25 12:37:06 -05:00
Fabian Dill
007f66d86e CommonClient.py: fix generic error 2021-06-25 07:25:03 +02:00
Fabian Dill
0e32393acb FactorioClient: only await awaitable tasks 2021-06-25 07:11:06 +02:00
Fabian Dill
20729242f9 allow nested dictionaries in dict_to_lua 2021-06-25 01:55:58 +02:00
Fabian Dill
91655a855d Factorio: exclude science packs and rocket-part from free samples 2021-06-25 01:31:48 +02:00
Fabian Dill
9f2f343f76 Factorio: always display static nodes with full info 2021-06-24 23:51:42 +02:00
Fabian Dill
6c1d164330 LttP: set non-native items to Power Star 2021-06-22 06:25:19 +02:00
Fabian Dill
937fee9019 Factorio: fix locale file formatting 2021-06-22 02:00:35 +02:00
Fabian Dill
023a798ac1 Factorio: refactor visibility option into tech_tree_information
set vanilla technologies to be hidden instead of disabled
          fix advancement icon still showing when no information in tech was supposed to be given
2021-06-21 22:25:49 +02:00
Fabian Dill
07d61f6d47 fix playerSettings.yaml post-merge 2021-06-21 02:51:54 +02:00
Fabian Dill
304f63aedf Merge branch 'espeon' into Archipelago_Main
# Conflicts:
#	playerSettings.yaml
2021-06-21 02:49:06 +02:00
Fabian Dill
30190f373a send /received output to self.output 2021-06-21 02:14:25 +02:00
espeon65536
b51b094cc1 Added HMG to playerSettings 2021-06-18 23:45:03 -05:00
Fabian Dill
f4a2f344a7 format MultiServer.py 2021-06-19 03:03:06 +02:00
Fabian Dill
1e7214a86b fix required plando options triggering on empty string 2021-06-19 01:00:41 +02:00
Fabian Dill
f8fd8b3585 Factorio: add toggle to disable imported blueprints 2021-06-19 01:00:21 +02:00
CaitSith2
644d62c915 Ignore Factorio AP savegame file. 2021-06-18 14:23:55 -07:00
Fabian Dill
741ec36ee1 all requires to be modified by trigggers and linked options 2021-06-18 23:17:12 +02:00
Fabian Dill
a08d7bb1b2 Settings: add requires 2021-06-18 22:15:54 +02:00
espeon65536
16ae77ca1c Plandoing structures causes them to output in the spoiler log 2021-06-16 20:24:36 -05:00
Fabian Dill
a5bf3a8407 Factorio: remove option to turn off random_tech_ingredients 2021-06-16 23:41:43 +02:00
espeon65536
cd0306d513 additional import cleanup 2021-06-16 01:16:19 -05:00
espeon65536
b29d0b8276 Fixed some options in the Minecraft section of playerSettings 2021-06-15 22:27:51 -05:00
Chris Wilson
3ee88fd8fe Merge branch 'main' of https://github.com/ArchipelagoMW/Archipelago into main 2021-06-15 21:18:24 -04:00
Chris Wilson
bc9c93b180 Improvements to the WebHost
- Improved routing structure
- Improved style imports across site
- Added placeholder player-settings pages for Factorio and Minecraft
2021-06-15 21:18:14 -04:00
espeon65536
e49d10ab22 Clean up imports 2021-06-15 18:22:12 -05:00
espeon65536
059946d59e Shifted Minecraft to the new AutoWorld system 2021-06-15 18:15:05 -05:00
espeon65536
6211760922 Merge branch 'main' of https://github.com/ArchipelagoMW/Archipelago into minecraft 2021-06-15 16:58:28 -05:00
Fabian Dill
167958c002 fix legacy weapons trigger 2021-06-15 23:23:39 +02:00
Fabian Dill
8b16ffb629 fix LttP rom options 2021-06-15 21:15:57 +02:00
Fabian Dill
b5193162bf update playerSettings.yaml 2021-06-15 20:26:31 +02:00
Fabian Dill
bc34c237b6 move minecraft plando connections into minecraft 2021-06-15 16:34:36 +02:00
Fabian Dill
d9824d26d2 make Factorio rocket silo a static (and therefore local) node 2021-06-15 15:32:40 +02:00
Fabian Dill
8d08b55e69 move item referencing options into their game's category 2021-06-15 15:10:31 +02:00
Fabian Dill
503c844971 categorize game options 2021-06-15 14:11:46 +02:00
espeon65536
deff356910 Added HMG check to all checks for OWG and NL 2021-06-14 22:10:26 -05:00
Chris Wilson
883ebbf267 Updating WebHost structure 2021-06-14 22:27:43 -04:00
Fabian Dill
cd45116dce dynamify games listing 2021-06-15 02:35:40 +02:00
Chris Wilson
d80362c4b8 Fix 404 pages 2021-06-14 20:20:23 -04:00
Chris Wilson
384e06d6fe Subdirectory pages currently 404. I'll look into this later 2021-06-14 20:18:40 -04:00
Fabian Dill
e6f44a70d0 use flask convention for template fetching 2021-06-15 01:51:40 +02:00
Chris Wilson
0ca90ee7e8 Add subdirectory handling for zelda3, factorio, and minecraft. Add generic 404 page. 2021-06-14 19:35:02 -04:00
Fabian Dill
59a56c803a Log which player's plando has caused a placement failure 2021-06-14 23:42:13 +02:00
Fabian Dill
1e0b44bdc5 set Triforce Piece Defaults 2021-06-14 23:41:47 +02:00
Fabian Dill
2f3296bada remove _ and - from pedestal hint texts 2021-06-14 02:23:41 +02:00
Fabian Dill
434d8e0977 remove _ and - from item hint texts 2021-06-14 02:20:13 +02:00
Fabian Dill
0a89eaaf62 update trigger result key before trigger 2021-06-14 02:14:02 +02:00
Fabian Dill
cea2f81b86 remove IRH special rule now that it's a 1/1 triforce piece hunt 2021-06-13 07:57:34 +02:00
Fabian Dill
86b612f3b5 implement random-middle 2021-06-12 21:05:45 +02:00
espeon65536
d425e5eb6a disable GT junk fill in hybrid 2021-06-12 13:11:14 -05:00
Fabian Dill
183fd33f3f MultiMystery linux compatibility 2021-06-12 16:10:56 +02:00
Chris Wilson
8c82d3e747 Added a page to describe the games currently supported by AP 2021-06-12 02:49:36 -04:00
Chris Wilson
7b495f3d81 Website landing page preliminary update 2021-06-11 20:22:47 -04:00
Fabian Dill
3ea7f1cb03 Factorio Funnels: only sort current funnel, not all funnels 2021-06-11 20:18:28 +02:00
Fabian Dill
2a13fe05c6 fix import error for Hollow Knight 2021-06-11 18:05:49 +02:00
Fabian Dill
2c4c899179 move more Factorio stuff around 2021-06-11 18:02:48 +02:00
Fabian Dill
760fb32016 fix Factorio Recipe Time randomization not being deterministic 2021-06-11 14:47:13 +02:00
Fabian Dill
278f40471b fix open_pyramid default 2021-06-11 14:26:12 +02:00
Fabian Dill
20ca09c730 remove test modules 2021-06-11 14:23:59 +02:00
Fabian Dill
568a71cdbe Start implementing object oriented scaffold for world types
(There's still a lot of work ahead, such as:
registering locations and items to the World, as well as methods to create_item_from_name()
many more method names for various stages
embedding Options into the world type
and many more...)
2021-06-11 14:22:44 +02:00
Fabian Dill
753a5f7cb2 Merge branch 'split' into Archipelago_Main
# Conflicts:
#	Main.py
2021-06-11 13:27:28 +02:00
espeon65536
96e13786cd Fixed broken mirrorless swamp rules 2021-06-10 18:10:25 -05:00
espeon65536
5d6592f296 Merge branch 'main' of https://github.com/espeon65536/Archipelago into main 2021-06-09 11:00:33 -05:00
Fabian Dill
534dd331b9 document item locality options properly 2021-06-09 10:13:18 +02:00
espeon65536
b3b56fcafd removed unnecessary import 2021-06-08 19:32:27 -05:00
espeon65536
671fd50cfb Moved the add_rule for mirrorless swamp to speed it up on invalid entrance shuffle type 2021-06-08 19:19:11 -05:00
espeon65536
eaf19643a9 Cleaned up code for assigning dungeon reentry rules 2021-06-08 19:18:28 -05:00
espeon65536
a582a3781b Moved the addition of HMG-specific connections to fix crossed ER 2021-06-08 18:32:22 -05:00
espeon65536
e0d90e0b21 Properly accounting for agatower not freely opening for dungeon reentry 2021-06-08 18:17:21 -05:00
espeon65536
a73189338c Fixed full ER HMG not ignoring pearl requirements on entrances 2021-06-08 18:15:47 -05:00
Fabian Dill
1e414dd370 fix tests 2021-06-08 22:14:56 +02:00
Fabian Dill
5ea03c71c0 start moving some alttp options over to the new system 2021-06-08 21:58:11 +02:00
espeon65536
d7a46f089e added get_option_name to Range option for spoiler generation 2021-06-08 08:59:06 -05:00
espeon65536
6e33181f05 Changed advancement_goal to a Range option 2021-06-08 08:58:16 -05:00
Fabian Dill
622f8f8158 always check legal range for Range 2021-06-08 15:39:34 +02:00
Fabian Dill
821b0f0f92 document random-high and random-low 2021-06-08 14:56:41 +02:00
Fabian Dill
471b217e99 add random-high and random-low to Range Options 2021-06-08 14:48:00 +02:00
Fabian Dill
adda0eff4a implement Range option type 2021-06-08 14:15:23 +02:00
espeon65536
2001ca6566 Fixed the check on dungeon reentry not working properly 2021-06-08 01:22:16 -05:00
espeon65536
b9a783d7d7 Fixed open connections breaking non-HMG seed generation 2021-06-08 00:50:28 -05:00
espeon65536
eb9ee9f41e Hybrid Major Glitches connections and logic 2021-06-07 20:19:03 -05:00
espeon65536
fae14ad283 Mystery.py correctly recognizes HMG as an option 2021-06-07 19:34:00 -05:00
espeon65536
16c6e17a49 Initial handling of hybrid glitch logic outside of UnderworldGlitchRules 2021-06-07 01:19:27 -05:00
espeon65536
ac31671914 initial hybridmg logic file commit 2021-06-07 00:38:30 -05:00
Fabian Dill
109eb5b9dc start of split 2021-05-13 01:34:59 +02:00
959 changed files with 161436 additions and 17121 deletions

View File

@@ -12,10 +12,10 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
- name: Set up Python 3.9
uses: actions/setup-python@v1
with:
python-version: 3.8
python-version: 3.9
- name: Install dependencies
run: |
python -m pip install --upgrade pip

View File

@@ -9,18 +9,27 @@ jobs:
build:
runs-on: ubuntu-latest
name: Test Python ${{ matrix.python.version }}
strategy:
fail-fast: false
matrix:
python:
- {version: '3.8'}
- {version: '3.9'}
#- {version: '3.10'}
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
- name: Set up Python ${{ matrix.python.version }}
uses: actions/setup-python@v1
with:
python-version: 3.8
python-version: ${{ matrix.python.version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
python ModuleUpdate.py --yes --force
- name: Unittests
run: |
pytest test

25
.gitignore vendored
View File

@@ -4,9 +4,15 @@
*_Spoiler.txt
*.bmbp
*.apbp
*.apm3
*.apmc
*.apz5
*.pyc
*.pyd
*.sfc
*.z64
*.n64
*.nes
*.wixobj
*.lck
*.db3
@@ -16,20 +22,14 @@
*.apsave
build
/build_factorio/
bundle/components.wxs
dist
README.html
.vs/
EnemizerCLI/
RaceRom.py
weights/
/MultiMystery/
/Players/
/QUsb2Snes/
/options.yaml
/config.yaml
/uploads/
/logs/
_persistent_storage.yaml
mystery_result_*.yaml
@@ -38,6 +38,12 @@ success.txt
output/
Output Logs/
/factorio/
/Minecraft Forge Server/
/WebHostLib/static/generated
/freeze_requirements.txt
/Archipelago.zip
/setup.ini
# Byte-compiled / optimized / DLL files
__pycache__/
@@ -144,3 +150,10 @@ dmypy.json
# Cython debug symbols
cython_debug/
#minecraft server stuff
jdk*/
minecraft*/
#pyenv
.python-version

File diff suppressed because it is too large Load Diff

View File

@@ -1,26 +1,29 @@
from __future__ import annotations
import logging
import typing
import asyncio
import urllib.parse
import sys
import typing
import time
import prompt_toolkit
import websockets
from prompt_toolkit.patch_stdout import patch_stdout
import Utils
if __name__ == "__main__":
Utils.init_logging("TextClient", exception_logger="Client")
from MultiServer import CommandProcessor
from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, color, ClientStatus
from Utils import Version
# logging note:
# logging.* gets send to only the text console, logger.* gets send to the WebUI as well, if it's initialized.
from worlds import network_data_package
from worlds.alttp import Items, Regions
from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission
from Utils import Version, stream_input
from worlds import network_data_package, AutoWorldRegister
logger = logging.getLogger("Client")
# without terminal we have to use gui mode
gui_enabled = not sys.stdout or "--nogui" not in sys.argv
class ClientCommandProcessor(CommandProcessor):
def __init__(self, ctx: CommonContext):
self.ctx = ctx
@@ -36,30 +39,30 @@ class ClientCommandProcessor(CommandProcessor):
def _cmd_connect(self, address: str = "") -> bool:
"""Connect to a MultiWorld Server"""
self.ctx.server_address = None
asyncio.create_task(self.ctx.connect(address if address else None))
asyncio.create_task(self.ctx.connect(address if address else None), name="connecting")
return True
def _cmd_disconnect(self) -> bool:
"""Disconnect from a MultiWorld Server"""
self.ctx.server_address = None
asyncio.create_task(self.ctx.disconnect())
asyncio.create_task(self.ctx.disconnect(), name="disconnecting")
return True
def _cmd_received(self) -> bool:
"""List all received items"""
logger.info('Received items:')
logger.info(f'{len(self.ctx.items_received)} received items:')
for index, item in enumerate(self.ctx.items_received, 1):
logging.info('%s from %s (%s) (%d/%d in list)' % (
color(self.ctx.item_name_getter(item.item), 'red', 'bold'),
color(self.ctx.player_names[item.player], 'yellow'),
self.ctx.location_name_getter(item.location), index, len(self.ctx.items_received)))
self.output(f"{self.ctx.item_name_getter(item.item)} from {self.ctx.player_names[item.player]}")
return True
def _cmd_missing(self) -> bool:
"""List all missing location checks, from your local game state"""
if not self.ctx.game:
self.output("No game set, cannot determine missing checks.")
return False
count = 0
checked_count = 0
for location, location_id in Regions.lookup_name_to_id.items():
for location, location_id in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id.items():
if location_id < 0:
continue
if location_id not in self.ctx.locations_checked:
@@ -78,9 +81,20 @@ class ClientCommandProcessor(CommandProcessor):
self.output("No missing location checks found.")
return True
def _cmd_items(self):
"""List all item names for the currently running game."""
self.output(f"Item Names for {self.ctx.game}")
for item_name in AutoWorldRegister.world_types[self.ctx.game].item_name_to_id:
self.output(item_name)
def _cmd_locations(self):
"""List all location names for the currently running game."""
self.output(f"Location Names for {self.ctx.game}")
for location_name in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id:
self.output(location_name)
def _cmd_ready(self):
"""Send ready status to server."""
self.ctx.ready = not self.ctx.ready
if self.ctx.ready:
state = ClientStatus.CLIENT_READY
@@ -88,42 +102,59 @@ class ClientCommandProcessor(CommandProcessor):
else:
state = ClientStatus.CLIENT_CONNECTED
self.output("Unreadied.")
asyncio.create_task(self.ctx.send_msgs([{"cmd": "StatusUpdate", "status": state}]))
asyncio.create_task(self.ctx.send_msgs([{"cmd": "StatusUpdate", "status": state}]), name="send StatusUpdate")
def default(self, raw: str):
asyncio.create_task(self.ctx.send_msgs([{"cmd": "Say", "text": raw}]))
raw = self.ctx.on_user_say(raw)
if raw:
asyncio.create_task(self.ctx.send_msgs([{"cmd": "Say", "text": raw}]), name="send Say")
class CommonContext():
starting_reconnect_delay = 5
current_reconnect_delay = starting_reconnect_delay
command_processor = ClientCommandProcessor
def __init__(self, server_address, password, found_items: bool):
tags: typing.Set[str] = {"AP"}
starting_reconnect_delay: int = 5
current_reconnect_delay: int = starting_reconnect_delay
command_processor: int = ClientCommandProcessor
game = None
ui = None
keep_alive_task = None
items_handling: typing.Optional[int] = None
def __init__(self, server_address, password):
# server state
self.server_address = server_address
self.password = password
self.server_task = None
self.server: typing.Optional[Endpoint] = None
self.server_version = Version(0, 0, 0)
self.hint_cost: typing.Optional[int] = None
self.games: typing.Dict[int, str] = {}
self.permissions = {
"forfeit": "disabled",
"collect": "disabled",
"remaining": "disabled",
}
# own state
self.finished_game = False
self.ready = False
self.found_items = found_items
self.team = None
self.slot = None
self.auth = None
self.seed_name = None
self.locations_checked: typing.Set[int] = set()
self.locations_checked: typing.Set[int] = set() # local state
self.locations_scouted: typing.Set[int] = set()
self.items_received = []
self.missing_locations: typing.List[int] = []
self.checked_locations: typing.List[int] = []
self.missing_locations: typing.Set[int] = set()
self.checked_locations: typing.Set[int] = set() # server state
self.locations_info = {}
self.input_queue = asyncio.Queue()
self.input_requests = 0
self.last_death_link: float = time.time() # last send/received death link on AP layer
# game state
self.player_names: typing.Dict[int: str] = {0: "Archipelago"}
self.exit_event = asyncio.Event()
@@ -133,6 +164,15 @@ class CommonContext():
self.jsontotextparser = JSONtoTextParser(self)
self.set_getters(network_data_package)
# execution
self.keep_alive_task = asyncio.create_task(keep_alive(self), name="Bouncy")
@property
def total_locations(self) -> typing.Optional[int]:
"""Will return None until connected."""
if self.checked_locations or self.missing_locations:
return len(self.checked_locations | self.missing_locations)
async def connection_closed(self):
self.auth = None
self.items_received = []
@@ -143,22 +183,24 @@ class CommonContext():
self.server = None
self.server_task = None
# noinspection PyAttributeOutsideInit
def set_getters(self, data_package: dict, network=False):
if not network: # local data; check if newer data was already downloaded
local_package = Utils.persistent_load().get("datapackage", {}).get("latest", {})
if local_package and local_package["version"] > network_data_package["version"]:
data_package: dict = local_package
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"]:
Utils.persistent_store("datapackage", "latest", network_data_package)
item_lookup: dict = data_package["lookup_any_item_id_to_name"]
locations_lookup: dict = data_package["lookup_any_location_id_to_name"]
item_lookup: dict = {}
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):
return item_lookup.get(code, f'Unknown item (ID:{code})')
@@ -195,27 +237,118 @@ class CommonContext():
def event_invalid_slot(self):
raise Exception('Invalid Slot; please verify that you have connected to the correct world.')
async def server_auth(self, password_requested):
def event_invalid_game(self):
raise Exception('Invalid Game; please verify that you connected with the right game to the correct world.')
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
logger.info('Enter the password required to join this game:')
self.password = await self.console_input()
return self.password
async def send_connect(self, **kwargs):
payload = {
'cmd': 'Connect',
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
'tags': self.tags, 'items_handling': self.items_handling,
'uuid': Utils.get_unique_identifier(), 'game': self.game
}
if kwargs:
payload.update(kwargs)
await self.send_msgs([payload])
async def console_input(self):
self.input_requests += 1
return await self.input_queue.get()
async def connect(self, address= None):
async def connect(self, address=None):
await self.disconnect()
self.server_task = asyncio.create_task(server_loop(self, address))
self.server_task = asyncio.create_task(server_loop(self, address), name="server loop")
def on_print(self, args: dict):
logger.info(args["text"])
def on_print_json(self, args: dict):
if not self.found_items and args.get("type", None) == "ItemSend" and args["receiving"] == args["sending"]:
pass # don't want info on other player's local pickups.
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
def on_user_say(self, text: str) -> typing.Optional[str]:
"""Gets called before sending a Say to the server from the user.
Returned text is sent, or sending is aborted if None is returned."""
return text
def update_permissions(self, permissions: typing.Dict[str, int]):
for permission_name, permission_flag in permissions.items():
try:
flag = Permission(permission_flag)
logger.info(f"{permission_name.capitalize()} permission: {flag.name}")
self.permissions[permission_name] = flag.name
except Exception as e: # safeguard against permissions that may be implemented in the future
logger.exception(e)
async def shutdown(self):
self.server_address = None
if self.server and not self.server.socket.closed:
await self.server.socket.close()
if self.server_task:
await self.server_task
while self.input_requests > 0:
self.input_queue.put_nowait(None)
self.input_requests -= 1
self.keep_alive_task.cancel()
# DeathLink hooks
def on_deathlink(self, data: dict):
"""Gets dispatched when a new DeathLink is triggered by another linked player."""
self.last_death_link = max(data["time"], self.last_death_link)
text = data.get("cause", "")
if text:
logger.info(f"DeathLink: {text}")
else:
logger.info(f"DeathLink: Received from {data['source']}")
async def send_death(self, death_text: str = ""):
logger.info("DeathLink: Sending death to your friends...")
self.last_death_link = time.time()
await self.send_msgs([{
"cmd": "Bounce", "tags": ["DeathLink"],
"data": {
"time": self.last_death_link,
"source": self.player_names[self.slot],
"cause": death_text
}
}])
async def update_death_link(self, death_link):
old_tags = self.tags.copy()
if death_link:
self.tags.add("DeathLink")
else:
self.tags -= {"DeathLink"}
if old_tags != self.tags and self.server and not self.server.socket.closed:
await self.send_msgs([{"cmd": "ConnectUpdate", "tags": self.tags}])
async def keep_alive(ctx: CommonContext, seconds_between_checks=100):
"""some ISPs/network configurations drop TCP connections if no payload is sent (ignore TCP-keep-alive)
so we send a payload to prevent drop and if we were dropped anyway this will cause an auto-reconnect."""
seconds_elapsed = 0
while not ctx.exit_event.is_set():
await asyncio.sleep(1) # short sleep to not block program shutdown
if ctx.server and ctx.slot:
seconds_elapsed += 1
if seconds_elapsed > seconds_between_checks:
await ctx.send_msgs([{"cmd": "Bounce", "slots": [ctx.slot]}])
seconds_elapsed = 0
async def server_loop(ctx: CommonContext, address=None):
@@ -251,25 +384,25 @@ async def server_loop(ctx: CommonContext, address=None):
logger.error('Unable to connect to multiworld server at cached address. '
'Please use the connect button above.')
else:
logger.error('Connection refused by the multiworld server')
logger.exception('Connection refused by the multiworld server')
except websockets.InvalidURI:
logger.exception('Failed to connect to the multiworld server (invalid URI)')
except (OSError, websockets.InvalidURI):
logger.error('Failed to connect to the multiworld server')
logger.exception('Failed to connect to the multiworld server')
except Exception as e:
logger.error('Lost connection to the multiworld server, type /connect to reconnect')
if not isinstance(e, websockets.WebSocketException):
logger.exception(e)
logger.exception('Lost connection to the multiworld server, type /connect to reconnect')
finally:
await ctx.connection_closed()
if ctx.server_address:
logger.info(f"... reconnecting in {ctx.current_reconnect_delay}s")
asyncio.create_task(server_autoreconnect(ctx))
asyncio.create_task(server_autoreconnect(ctx), name="server auto reconnect")
ctx.current_reconnect_delay *= 2
async def server_autoreconnect(ctx: CommonContext):
await asyncio.sleep(ctx.current_reconnect_delay)
if ctx.server_address and ctx.server_task is None:
ctx.server_task = asyncio.create_task(server_loop(ctx))
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
async def process_server_cmd(ctx: CommonContext, args: dict):
@@ -293,14 +426,16 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
logger.info("Server protocol tags: " + ", ".join(args["tags"]))
if args['password']:
logger.info('Password required')
logger.info(f"Forfeit setting: {args['forfeit_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']}"
f" for each location checked. Use !hint for more information.")
ctx.update_permissions(args.get("permissions", {}))
if "games" in args:
ctx.games = {x: game for x, game in enumerate(args["games"], start=1)}
logger.info(
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.check_points = int(args['location_check_points'])
ctx.forfeit_mode = args['forfeit_mode']
ctx.remaining_mode = args['remaining_mode']
if len(args['players']) < 1:
logger.info('No player connected')
else:
@@ -324,19 +459,23 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
errors = args["errors"]
if 'InvalidSlot' in errors:
ctx.event_invalid_slot()
elif 'InvalidGame' in errors:
ctx.event_invalid_game()
elif 'SlotAlreadyTaken' in errors:
raise Exception('Player slot already in use for that team')
elif 'IncompatibleVersion' in errors:
raise Exception('Server reported your client version as incompatible')
elif 'InvalidItemsHandling' in errors:
raise Exception('The item handling flags requested by the client are not supported')
# last to check, recoverable problem
elif 'InvalidPassword' in errors:
logger.error('Invalid password')
ctx.password = None
await ctx.server_auth(True)
else:
elif errors:
raise Exception("Unknown connection errors: " + str(errors))
raise Exception('Connection refused by the multiworld host, no reason provided')
else:
raise Exception('Connection refused by the multiworld host, no reason provided')
elif cmd == 'Connected':
ctx.team = args["team"]
@@ -358,8 +497,8 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
# This list is used to only send to the server what is reported as ACTUALLY Missing.
# This also serves to allow an easy visual of what locations were already checked previously
# when /missing is used for the client side view of what is missing.
ctx.missing_locations = args["missing_locations"]
ctx.checked_locations = args["checked_locations"]
ctx.missing_locations = set(args["missing_locations"])
ctx.checked_locations = set(args["checked_locations"])
elif cmd == 'ReceivedItems':
start_index = args["index"]
@@ -388,6 +527,12 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
ctx.consume_players_package(args["players"])
if "hint_points" in args:
ctx.hint_points = args['hint_points']
if "checked_locations" in args:
checked = set(args["checked_locations"])
ctx.checked_locations |= checked
ctx.missing_locations -= checked
if "permissions" in args:
ctx.update_permissions(args["permissions"])
elif cmd == 'Print':
ctx.on_print(args)
@@ -395,22 +540,30 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
elif cmd == 'PrintJSON':
ctx.on_print_json(args)
elif cmd == 'InvalidArguments':
logger.warning(f"Invalid Arguments: {args['text']}")
elif cmd == 'InvalidPacket':
logger.warning(f"Invalid Packet of {args['type']}: {args['text']}")
elif cmd == "Bounced":
tags = args.get("tags", [])
# we can skip checking "DeathLink" in ctx.tags, as otherwise we wouldn't have been send this
if "DeathLink" in tags and ctx.last_death_link != args["data"]["time"]:
ctx.on_deathlink(args["data"])
else:
logger.debug(f"unknown command {cmd}")
ctx.on_package(cmd, args)
async def console_loop(ctx: CommonContext):
import sys
commandprocessor = ctx.command_processor(ctx)
queue = asyncio.Queue()
stream_input(sys.stdin, queue)
while not ctx.exit_event.is_set():
try:
input_text = await asyncio.get_event_loop().run_in_executor(
None, sys.stdin.readline
)
input_text = input_text.strip()
input_text = await queue.get()
queue.task_done()
if ctx.input_requests > 0:
ctx.input_requests -= 1
@@ -420,4 +573,70 @@ async def console_loop(ctx: CommonContext):
if input_text:
commandprocessor(input_text)
except Exception as e:
logging.exception(e)
logger.exception(e)
def get_base_parser(description=None):
import argparse
parser = argparse.ArgumentParser(description=description)
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
parser.add_argument('--password', default=None, help='Password of the multiworld host.')
if sys.stdout: # If terminal output exists, offer gui-less mode
parser.add_argument('--nogui', default=False, action='store_true', help="Turns off Client GUI.")
return parser
if __name__ == '__main__':
# Text Mode to use !hint and such with games that have no text entry
class TextContext(CommonContext):
tags = {"AP", "IgnoreGame", "TextOnly"}
game = "Archipelago"
items_handling = 0 # don't receive any NetworkItems
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(TextContext, self).server_auth(password_requested)
if not self.auth:
logger.info('Enter slot name:')
self.auth = await self.console_input()
await self.send_connect()
def on_package(self, cmd: str, args: dict):
if cmd == "Connected":
self.game = self.games.get(self.slot, None)
async def main(args):
ctx = TextContext(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
input_task = None
if gui_enabled:
from kvui import TextManager
ctx.ui = TextManager(ctx)
ui_task = asyncio.create_task(ctx.ui.async_run(), name="UI")
else:
ui_task = None
if sys.stdin:
input_task = asyncio.create_task(console_loop(ctx), name="Input")
await ctx.exit_event.wait()
await ctx.shutdown()
if ui_task:
await ui_task
if input_task:
input_task.cancel()
import colorama
parser = get_base_parser(description="Gameless Archipelago Client, for text interfacing.")
args, rest = parser.parse_known_args()
colorama.init()
loop = asyncio.get_event_loop()
loop.run_until_complete(main(args))
loop.close()
colorama.deinit()

View File

@@ -1,359 +0,0 @@
Hint description:
Hints will appear in the following ratios across the 15 telepathic tiles that have hints and the five storyteller locations:
4 hints for inconvenient entrances.
4 hints for random entrances (this can by coincidence pick inconvenient entrances that aren't used for the first set of hints).
3 hints for inconvenient item locations.
5 hints for valuable items.
4 junk hints.
In the vanilla, dungeonssimple, and dungeonsfull shuffles, the following ratios will be used instead:
5 hints for inconvenient item locations.
8 hints for valuable items.
7 junk hints.
In the simple, restricted, and restricted legacy shuffles, these are the ratios:
2 hints for inconvenient entrances.
1 hint for an inconvenient dungeon entrance.
4 hints for random entrances (this can by coincidence pick inconvenient entrances that aren't used for the first set of hints).
3 hints for inconvenient item locations.
5 hints for valuable items.
5 junk hints.
These hints will use the following format:
Entrance hints go "[Entrance on overworld] leads to [interior]".
Inconvenient item locations are a little more custom but amount to "[Location] has [item name]". The item name is literal and will specify which dungeon the dungeon specific items hail from (small key/big key/map/compass).
The valuable items are of the format "[item name] can be found [location]". The item name is again literal, and the location text is taken from Ganon's silver arrow hints. Note that the way it works is that every unique valuable item that exists is considered independently, and you won't get multiple hints for the EXACT same item (so you can only get one hint for Progressive Sword no matter how many swords exist in the seed, but if swords are not progressive, you could get hints for both Master Sword and Tempered Sword). More copies of an item existing does not increase the probability of getting a hint for that particular item (you are equally likely to get a hint for a Progressive Sword as for the Hammer). Unlike the IR, item names are never obfuscated by "something unique", and there is no special bias for hints for GT Big Key or Pegasus Boots.
Hint Locations:
Eastern Palace room before Big Chest
Desert Palace bonk torch room
Tower of Hera entrance room
Tower of Hera Big Chest room
Castle Tower after dark rooms
Palace of Darkness before Bow section
Swamp Palace entryway
Thieves' Town upstairs
Ice Palace entrance
Ice Palace after first drop
Ice Palace tall ice floor room
Misery Mire cutscene room
Turtle Rock entrance
Spectacle Rock cave
Spiky Hint cave
PoD Bdlg NPC
Near PoD Storyteller (bug near bomb wall)
Dark Sanctuary Storyteller (long room with tables)
Near Mire Storyteller (feather duster in winding cave)
SE DW Storyteller (owl in winding cave)
Inconvenient entrance list:
Skull Woods Final
Ice Palace
Misery Mire
Turtle Rock
Ganon's Tower
Mimic Ledge
SW DM Foothills Cave (mirror from upper Bumper ledge)
Hammer Pegs (near purple chest)
Super Bomb cracked wall
Inconvenient location list:
Swamp left (two chests)
Mire left (two chests)
Hera basement
Eastern Palace Big Key chest (protected by anti-fairies)
Thieves' Town Big Chest
Ice Palace Big Chest
Ganon's Tower Big Chest
Purple Chest
Spike Cave
Magic Bat
Sahasrahla (Green Pendant)
In the vanilla, dungeonssimple, and dungeonsfull shuffles, the following two locations are added to the inconvenient locations list:
Graveyard Cave
Mimic Cave
Valuable Items are simply all items that are shown on the pause subscreen (Y, B, or A sections) minus Silver Arrows and plus Triforce Pieces, Magic Upgrades (1/2 or 1/4), and the Single Arrow. If key shuffle is being used, you can additionally get hints for Small Keys or Big Keys but not hints for Maps or Compasses.
While the exact verbage of location names and item names can be found in the source code, here's a copy for reference:
Overworld Entrance naming:
Turtle Rock: Turtle Rock Main
Misery Mire: Misery Mire
Ice Palace: Ice Palace
Skull Woods Final Section: The back of Skull Woods
Death Mountain Return Cave (West): The SW DM Foothills Cave
Mimic Cave: Mimic Ledge
Dark World Hammer Peg Cave: The rows of pegs
Pyramid Fairy: The crack on the pyramid
Eastern Palace: Eastern Palace
Elder House (East): Elder House
Elder House (West): Elder House
Two Brothers House (East): Eastern Quarreling Brothers' house
Old Man Cave (West): The lower DM entrance
Hyrule Castle Entrance (South): The ground level castle door
Thieves Town: Thieves' Town
Bumper Cave (Bottom): The lower Bumper Cave
Swamp Palace: Swamp Palace
Dark Death Mountain Ledge (West): The East dark DM connector ledge
Dark Death Mountain Ledge (East): The East dark DM connector ledge
Superbunny Cave (Top): The summit of dark DM cave
Superbunny Cave (Bottom): The base of east dark DM
Hookshot Cave: The rock on dark DM
Desert Palace Entrance (South): The book sealed passage
Tower of Hera: The Tower of Hera
Two Brothers House (West): The door near the race game
Old Man Cave (East): The SW-most cave on west DM
Old Man House (Bottom): A cave with a door on west DM
Old Man House (Top): The eastmost cave on west DM
Death Mountain Return Cave (East): The westmost cave on west DM
Spectacle Rock Cave Peak: The highest cave on west DM
Spectacle Rock Cave: The right ledge on west DM
Spectacle Rock Cave (Bottom): The left ledge on west DM
Paradox Cave (Bottom): The right paired cave on east DM
Paradox Cave (Middle): The southmost cave on east DM
Paradox Cave (Top): The east DM summit cave
Fairy Ascension Cave (Bottom): The east DM cave behind rocks
Fairy Ascension Cave (Top): The central ledge on east DM
Spiral Cave: The left ledge on east DM
Spiral Cave (Bottom): The SWmost cave on east DM
Palace of Darkness: Palace of Darkness
Hyrule Castle Entrance (West): The left castle door
Hyrule Castle Entrance (East): The right castle door
Agahnims Tower: The sealed castle door
Desert Palace Entrance (West): The westmost building in the desert
Desert Palace Entrance (North): The northmost cave in the desert
Blinds Hideout: Blind's old house
Lake Hylia Fairy: A cave NE of Lake Hylia
Light Hype Fairy: The cave south of your house
Desert Fairy: The cave near the desert
Chicken House: The chicken lady's house
Aginahs Cave: The open desert cave
Sahasrahlas Hut: The house near armos
Cave Shop (Lake Hylia): The cave NW Lake Hylia
Blacksmiths Hut: The old smithery
Sick Kids House: The central house in Kakariko
Lost Woods Gamble: A tree trunk door
Fortune Teller (Light): A building NE of Kakariko
Snitch Lady (East): A house guarded by a snitch
Snitch Lady (West): A house guarded by a snitch
Bush Covered House: A house with an uncut lawn
Tavern (Front): A building with a backdoor
Light World Bomb Hut: A Kakariko building with no door
Kakariko Shop: The old Kakariko shop
Mini Moldorm Cave: The cave south of Lake Hylia
Long Fairy Cave: The eastmost portal cave
Good Bee Cave: The open cave SE Lake Hylia
20 Rupee Cave: The rock SE Lake Hylia
50 Rupee Cave: The rock near the desert
Ice Rod Cave: The sealed cave SE Lake Hylia
Library: The old library
Potion Shop: The witch's building
Dam: The old dam
Lumberjack House: The lumberjack house
Lake Hylia Fortune Teller: The building NW Lake Hylia
Kakariko Gamble Game: The old Kakariko gambling den
Waterfall of Wishing: Going behind the waterfall
Capacity Upgrade: The cave on the island
Bonk Rock Cave: The rock pile near Sanctuary
Graveyard Cave: The graveyard ledge
Checkerboard Cave: The NE desert ledge
Cave 45: The ledge south of haunted grove
Kings Grave: The northeastmost grave
Bonk Fairy (Light): The rock pile near your home
Hookshot Fairy: A cave on east DM
Bonk Fairy (Dark): The rock pile near the old bomb shop
Dark Sanctuary Hint: The dark sanctuary cave
Dark Lake Hylia Fairy: The cave NE dark Lake Hylia
C-Shaped House: The NE house in Village of Outcasts
Big Bomb Shop: The old bomb shop
Dark Death Mountain Fairy: The SW cave on dark DM
Dark Lake Hylia Shop: The building NW dark Lake Hylia
Dark World Shop: The hammer sealed building
Red Shield Shop: The fenced in building
Mire Shed: The western hut in the mire
East Dark World Hint: The dark cave near the eastmost portal
Dark Desert Hint: The cave east of the mire
Spike Cave: The ledge cave on west dark DM
Palace of Darkness Hint: The building south of Kiki
Dark Lake Hylia Ledge Spike Cave: The rock SE dark Lake Hylia
Cave Shop (Dark Death Mountain): The base of east dark DM
Dark World Potion Shop: The building near the catfish
Archery Game: The old archery game
Dark World Lumberjack Shop: The northmost Dark World building
Hype Cave: The cave south of the old bomb shop
Brewery: The Village of Outcasts building with no door
Dark Lake Hylia Ledge Hint: The open cave SE dark Lake Hylia
Chest Game: The westmost building in the Village of Outcasts
Dark Desert Fairy: The eastern hut in the mire
Dark Lake Hylia Ledge Fairy: The sealed cave SE dark Lake Hylia
Fortune Teller (Dark): The building NE the Village of Outcasts
Sanctuary: Sanctuary
Lumberjack Tree Cave: The cave Behind Lumberjacks
Lost Woods Hideout Stump: The stump in Lost Woods
North Fairy Cave: The cave East of Graveyard
Bat Cave Cave: The cave in eastern Kakariko
Kakariko Well Cave: The cave in northern Kakariko
Hyrule Castle Secret Entrance Stairs: The tunnel near the castle
Skull Woods First Section Door: The southeastmost skull
Skull Woods Second Section Door (East): The central open skull
Skull Woods Second Section Door (West): The westmost open skull
Desert Palace Entrance (East): The eastern building in the desert
Turtle Rock Isolated Ledge Entrance: The isolated ledge on east dark DM
Bumper Cave (Top): The upper Bumper Cave
Hookshot Cave Back Entrance: The stairs on the floating island
Destination Entrance Naming:
Hyrule Castle: Hyrule Castle (all three entrances)
Eastern Palace: Eastern Palace
Desert Palace: Desert Palace (all four entrances, including final)
Tower of Hera: Tower of Hera
Palace of Darkness: Palace of Darkness
Swamp Palace: Swamp Palace
Skull Woods: Skull Woods (any entrance including final)
Thieves' Town: Thieves' Town
Ice Palace: Ice Palace
Misery Mire: Misery Mire
Turtle Rock: Turtle Rock (all four entrances)
Ganon's Tower: Ganon's Tower
Castle Tower: Agahnim's Tower
A connector: Paradox Cave, Spectacle Rock Cave, Hookshot Cave, Superbunny Cave, Spiral Cave, Old Man Fetch Cave, Old Man House, Elder House, Quarreling Brothers' House, Bumper Cave, DM Fairy Ascent Cave, DM Exit Cave
A bounty of five items: Mini-moldorm cave, Hype Cave, Blind's Hideout
Sahasrahla: Sahasrahla
A cave with two items: Mire hut, Waterfall Fairy, Pyramid Fairy
A fairy fountain: Any healer fairy cave, either bonk cave with four fairies, the "long fairy" cave
A common shop: Any shop that sells bombs by default
The rare shop: The shop that sells the Red Shield by default
The potion shop: Potion Shop
The bomb shop: Bomb Shop
A fortune teller: Any of the three fortune tellers
A house with a chest: Chicken Lady's house, C-House, Brewery
A cave with an item: Checkerboard cave, Hammer Pegs cave, Cave 45, Graveyard Ledge cave
A cave with a chest: Sanc Bonk Rock Cave, Cape Grave Cave, Ice Rod Cave, Aginah's Cave
The dam: Watergate
The sick kid: Sick Kid
The library: Library
Mimic Cave: Mimic Cave
Spike Cave: Spike Cave
A game of 16 chests: VoO chest game (for the item)
A storyteller: The four DW NPCs who charge 20 rupees for a hint as well as the PoD Bdlg guy who gives a free hint
A cave with some cash: 20 rupee cave, 50 rupee cave (both have thieves and some pots)
A game of chance: Gambling game (just for cash, no items)
A game of skill: Archery minigame
The queen of fairies: Capacity Upgrade Fairy
A drop's exit: Sanctuary, LW Thieves' Hideout, Kakariko Well, Magic Bat, Useless Fairy, Uncle Tunnel, Ganon drop exit
A restock room: The Kakariko bomb/arrow restock room
The tavern: The Kakariko tavern
The grass man: The Kakariko man with many beds
A cold bee: The "wrong side" of Ice Rod cave where you can get a Good Bee
Fairies deep in a cave: Hookshot Fairy
Location naming reference:
Mushroom: in the woods
Master Sword Pedestal: at the pedestal
Bottle Merchant: with a merchant
Stumpy: with tree boy
Flute Spot: underground
Digging Game: underground
Lake Hylia Island: on an island
Floating Island: on an island
Bumper Cave Ledge: on a ledge
Spectacle Rock: atop a rock
Maze Race: at the race
Desert Ledge: in the desert
Pyramid: on the pyramid
Catfish: with a catfish
Ether Tablet: at a monument
Bombos Tablet: at a monument
Hobo: with the hobo
Zora's Ledge: near Zora
King Zora: at a high price
Sunken Treasure: underwater
Floodgate Chest: in the dam
Blacksmith: with the smith
Purple Chest: from a box
Old Man: with the old man
Link's Uncle: with your uncle
Secret Passage: near your uncle
Kakariko Well (5 items): in a well
Lost Woods Hideout: near a thief
Lumberjack Tree: in a hole
Magic Bat: with the bat
Paradox Cave (7 items): in a cave with seven chests
Blind's Hideout (5 items): in a basement
Mini Moldorm Cave (5 items): near Moldorms
Hype Cave (4 back chests): near a bat-like man
Hype Cave - Generous Guy: with a bat-like man
Hookshot Cave (4 items): across pits
Sahasrahla's Hut (chests in back): near the elder
Sahasrahla: with the elder
Waterfall Fairy (2 items): near a fairy
Pyramid Fairy (2 items): near a fairy
Mire Shed (2 items): near sparks
Superbunny Cave (2 items): in a connection
Spiral Cave: in spiral cave
Kakariko Tavern: in the bar
Link's House: in your home
Sick Kid: with the sick
Library: near books
Potion Shop: near potions
Spike Cave: beyond spikes
Mimic Cave: in a cave of mimicry
Chest Game: as a prize
Chicken House: near poultry
Aginah's Cave: with Aginah
Ice Rod Cave: in a frozen cave
Brewery: alone in a home
C-Shaped House: alone in a home
Spectacle Rock Cave: alone in a cave
King's Tomb: alone in a cave
Cave 45: alone in a cave
Graveyard Cave: alone in a cave
Checkerboard Cave: alone in a cave
Bonk Rock Cave: alone in a cave
Peg Cave: alone in a cave
Sanctuary: in Sanctuary
Hyrule Castle - Boomerang Chest: in Hyrule Castle
Hyrule Castle - Map Chest: in Hyrule Castle
Hyrule Castle - Zelda's Chest: in Hyrule Castle
Sewers - Dark Cross: in the sewers
Sewers - Secret Room (3 items): in the sewers
Eastern Palace - Boss: with the Armos
Eastern Palace (otherwise, 5 items): in Eastern Palace
Desert Palace - Boss: with Lanmolas
Desert Palace (otherwise, 5 items): in Desert Palace
Tower of Hera - Boss: with Moldorm
Tower of Hera (otherwise, 5 items): in Tower of Hera
Castle Tower (2 items): in Castle Tower
Palace of Darkness - Boss: with Helmasaur King
Palace of Darkness (otherwise, 13 items): in Palace of Darkness
Swamp Palace - Boss: with Arrghus
Swamp Palace (otherwise, 9 items): in Swamp Palace
Skull Woods - Bridge Room: near Mothula
Skull Woods - Boss: with Mothula
Skull Woods (otherwise, 6 items): in Skull Woods
Thieves' Town - Boss: with Blind
Thieves' Town (otherwise, 7 items): in Thieves' Town
Ice Palace - Boss: with Kholdstare
Ice Palace (otherwise, 7 items): in Ice Palace
Misery Mire - Boss: with Vitreous
Misery Mire (otherwise, 7 items): in Misery Mire
Turtle Rock - Boss: with Trinexx
Turtle Rock (otherwise, 11 items): in Turtle Rock
Ganons Tower (after climb, 4 items): atop Ganon's Tower
Ganon's Tower (otherwise, 23 items): in Ganon's Tower

256
FF1Client.py Normal file
View File

@@ -0,0 +1,256 @@
import asyncio
import json
import time
from asyncio import StreamReader, StreamWriter
from typing import List
import Utils
from CommonClient import CommonContext, server_loop, gui_enabled, console_loop, ClientCommandProcessor, logger, \
get_base_parser
SYSTEM_MESSAGE_ID = 0
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart ff1_connector.lua"
CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure ff1_connector.lua is running"
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart ff1_connector.lua"
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
CONNECTION_CONNECTED_STATUS = "Connected"
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
class FF1CommandProcessor(ClientCommandProcessor):
def __init__(self, ctx: CommonContext):
super().__init__(ctx)
def _cmd_nes(self):
"""Check NES Connection State"""
if isinstance(self.ctx, FF1Context):
logger.info(f"NES Status: {self.ctx.nes_status}")
class FF1Context(CommonContext):
command_processor = FF1CommandProcessor
items_handling = 0b111 # full remote
def __init__(self, server_address, password):
super().__init__(server_address, password)
self.nes_streams: (StreamReader, StreamWriter) = None
self.nes_sync_task = None
self.messages = {}
self.locations_array = None
self.nes_status = CONNECTION_INITIAL_STATUS
self.game = 'Final Fantasy'
self.awaiting_rom = False
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(FF1Context, self).server_auth(password_requested)
if not self.auth:
self.awaiting_rom = True
logger.info('Awaiting connection to NES to get Player information')
return
await self.send_connect()
def _set_message(self, msg: str, msg_id: int):
self.messages[(time.time(), msg_id)] = msg
def on_package(self, cmd: str, args: dict):
if cmd == 'Connected':
self.game = self.games.get(self.slot, None)
asyncio.create_task(parse_locations(self.locations_array, self, True))
elif cmd == 'Print':
msg = args['text']
if ': !' not in msg:
self._set_message(msg, SYSTEM_MESSAGE_ID)
elif cmd == "ReceivedItems":
msg = f"Received {', '.join([self.item_name_getter(item.item) for item in args['items']])}"
self._set_message(msg, SYSTEM_MESSAGE_ID)
elif cmd == 'PrintJSON':
print_type = args['type']
item = args['item']
receiving_player_id = args['receiving']
receiving_player_name = self.player_names[receiving_player_id]
sending_player_id = item.player
sending_player_name = self.player_names[item.player]
if print_type == 'Hint':
msg = f"Hint: Your {self.item_name_getter(item.item)} is at" \
f" {self.player_names[item.player]}'s {self.location_name_getter(item.location)}"
self._set_message(msg, item.item)
elif print_type == 'ItemSend' and receiving_player_id != self.slot:
if sending_player_id == self.slot:
if receiving_player_id == self.slot:
msg = f"You found your own {self.item_name_getter(item.item)}"
else:
msg = f"You sent {self.item_name_getter(item.item)} to {receiving_player_name}"
else:
if receiving_player_id == sending_player_id:
msg = f"{sending_player_name} found their {self.item_name_getter(item.item)}"
else:
msg = f"{sending_player_name} sent {self.item_name_getter(item.item)} to " \
f"{receiving_player_name}"
self._set_message(msg, item.item)
def get_payload(ctx: FF1Context):
current_time = time.time()
return json.dumps(
{
"items": [item.item for item in ctx.items_received],
"messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items()
if key[0] > current_time - 10}
}
)
async def parse_locations(locations_array: List[int], ctx: FF1Context, force: bool):
if locations_array == ctx.locations_array and not force:
return
else:
# print("New values")
ctx.locations_array = locations_array
locations_checked = []
if len(locations_array) > 0xFE and locations_array[0xFE] & 0x02 != 0 and not ctx.finished_game:
await ctx.send_msgs([
{"cmd": "StatusUpdate",
"status": 30}
])
ctx.finished_game = True
for location in ctx.missing_locations:
# index will be - 0x100 or 0x200
index = location
if location < 0x200:
# Location is a chest
index -= 0x100
flag = 0x04
else:
# Location is an NPC
index -= 0x200
flag = 0x02
# print(f"Location: {ctx.location_name_getter(location)}")
# print(f"Index: {str(hex(index))}")
# print(f"value: {locations_array[index] & flag != 0}")
if locations_array[index] & flag != 0:
locations_checked.append(location)
if locations_checked:
# print([ctx.location_name_getter(location) for location in locations_checked])
await ctx.send_msgs([
{"cmd": "LocationChecks",
"locations": locations_checked}
])
async def nes_sync_task(ctx: FF1Context):
logger.info("Starting nes connector. Use /nes for status information")
while not ctx.exit_event.is_set():
error_status = None
if ctx.nes_streams:
(reader, writer) = ctx.nes_streams
msg = get_payload(ctx).encode()
writer.write(msg)
writer.write(b'\n')
try:
await asyncio.wait_for(writer.drain(), timeout=1.5)
try:
# Data will return a dict with up to two fields:
# 1. A keepalive response of the Players Name (always)
# 2. An array representing the memory values of the locations area (if in game)
data = await asyncio.wait_for(reader.readline(), timeout=5)
data_decoded = json.loads(data.decode())
# print(data_decoded)
if ctx.game is not None and 'locations' in data_decoded:
# Not just a keep alive ping, parse
asyncio.create_task(parse_locations(data_decoded['locations'], ctx, False))
if not ctx.auth:
ctx.auth = ''.join([chr(i) for i in data_decoded['playerName'] if i != 0])
if ctx.awaiting_rom:
await ctx.server_auth(False)
except asyncio.TimeoutError:
logger.debug("Read Timed Out, Reconnecting")
error_status = CONNECTION_TIMING_OUT_STATUS
writer.close()
ctx.nes_streams = None
except ConnectionResetError as e:
logger.debug("Read failed due to Connection Lost, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.nes_streams = None
except TimeoutError:
logger.debug("Connection Timed Out, Reconnecting")
error_status = CONNECTION_TIMING_OUT_STATUS
writer.close()
ctx.nes_streams = None
except ConnectionResetError:
logger.debug("Connection Lost, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.nes_streams = None
if ctx.nes_status == CONNECTION_TENTATIVE_STATUS:
if not error_status:
logger.info("Successfully Connected to NES")
ctx.nes_status = CONNECTION_CONNECTED_STATUS
else:
ctx.nes_status = f"Was tentatively connected but error occured: {error_status}"
elif error_status:
ctx.nes_status = error_status
logger.info("Lost connection to nes and attempting to reconnect. Use /nes for status updates")
else:
try:
logger.debug("Attempting to connect to NES")
ctx.nes_streams = await asyncio.wait_for(asyncio.open_connection("localhost", 52980), timeout=10)
ctx.nes_status = CONNECTION_TENTATIVE_STATUS
except TimeoutError:
logger.debug("Connection Timed Out, Trying Again")
ctx.nes_status = CONNECTION_TIMING_OUT_STATUS
continue
except ConnectionRefusedError:
logger.debug("Connection Refused, Trying Again")
ctx.nes_status = CONNECTION_REFUSED_STATUS
continue
if __name__ == '__main__':
# Text Mode to use !hint and such with games that have no text entry
Utils.init_logging("FF1Client")
async def main(args):
ctx = FF1Context(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
if gui_enabled:
input_task = None
from kvui import FF1Manager
ctx.ui = FF1Manager(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
ctx.nes_sync_task = asyncio.create_task(nes_sync_task(ctx), name="NES Sync")
await ctx.exit_event.wait()
ctx.server_address = None
await ctx.shutdown()
if ctx.nes_sync_task:
await ctx.nes_sync_task
if ui_task:
await ui_task
if input_task:
input_task.cancel()
import colorama
parser = get_base_parser()
args, rest = parser.parse_known_args()
colorama.init()
loop = asyncio.get_event_loop()
loop.run_until_complete(main(args))
loop.close()
colorama.deinit()

View File

@@ -4,40 +4,26 @@ import logging
import json
import string
import copy
import subprocess
import sys
from concurrent.futures import ThreadPoolExecutor
import time
import random
import factorio_rcon
import colorama
import asyncio
from queue import Queue
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger
from MultiServer import mark_raw
import Utils
import random
from NetUtils import RawJSONtoTextParser, NetworkItem, ClientStatus, JSONtoTextParser, JSONMessagePart
from worlds.factorio.Technologies import lookup_id_to_name
if __name__ == "__main__":
Utils.init_logging("FactorioClient", exception_logger="Client")
rcon_port = 24242
rcon_password = ''.join(random.choice(string.ascii_letters) for x in range(32))
save_name = "Archipelago"
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger, gui_enabled, \
get_base_parser
from MultiServer import mark_raw
from NetUtils import NetworkItem, ClientStatus, JSONtoTextParser, JSONMessagePart
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO)
options = Utils.get_options()
executable = options["factorio_options"]["executable"]
bin_dir = os.path.dirname(executable)
if not os.path.isdir(bin_dir):
raise FileNotFoundError(bin_dir)
if not os.path.exists(executable):
if os.path.exists(executable + ".exe"):
executable = executable + ".exe"
else:
raise FileNotFoundError(executable)
server_args = (save_name, "--rcon-port", rcon_port, "--rcon-password", rcon_password, *sys.argv[1:])
thread_pool = ThreadPoolExecutor(10)
from worlds.factorio import Factorio
class FactorioCommandProcessor(ClientCommandProcessor):
@@ -47,106 +33,129 @@ class FactorioCommandProcessor(ClientCommandProcessor):
def _cmd_factorio(self, text: str) -> bool:
"""Send the following command to the bound Factorio Server."""
if self.ctx.rcon_client:
# TODO: Print the command non-silently only for race seeds, or otherwise block anything but /factorio /save in race seeds.
self.ctx.print_to_game(f"/factorio {text}")
result = self.ctx.rcon_client.send_command(text)
if result:
self.output(result)
return True
return False
def _cmd_connect(self, address: str = "") -> bool:
"""Connect to a MultiWorld Server"""
if not self.ctx.auth:
self.output("Cannot connect to a server with unknown own identity, bridge to Factorio first.")
return super(FactorioCommandProcessor, self)._cmd_connect(address)
def _cmd_resync(self):
"""Manually trigger a resync."""
self.ctx.awaiting_bridge = True
class FactorioContext(CommonContext):
command_processor = FactorioCommandProcessor
game = "Factorio"
items_handling = 0b111 # full remote
def __init__(self, *args, **kwargs):
super(FactorioContext, self).__init__(*args, **kwargs)
self.send_index = 0
# updated by spinup server
mod_version: Utils.Version = Utils.Version(0, 0, 0)
def __init__(self, server_address, password):
super(FactorioContext, self).__init__(server_address, password)
self.send_index: int = 0
self.rcon_client = None
self.awaiting_bridge = False
self.raw_json_text_parser = RawJSONtoTextParser(self)
self.write_data_path = None
self.death_link_tick: int = 0 # last send death link on Factorio layer
self.factorio_json_text_parser = FactorioJSONtoTextParser(self)
async def server_auth(self, password_requested):
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(FactorioContext, self).server_auth(password_requested)
await self.send_msgs([{"cmd": 'Connect',
'password': self.password, 'name': self.auth, 'version': Utils._version_tuple,
'tags': ['AP'],
'uuid': Utils.get_unique_identifier(), 'game': "Factorio"
}])
if self.rcon_client:
await get_info(self, self.rcon_client) # retrieve current auth code
else:
raise Exception("Cannot connect to a server with unknown own identity, "
"bridge to Factorio first.")
await self.send_connect()
def on_print(self, args: dict):
logger.info(args["text"])
super(FactorioContext, self).on_print(args)
if self.rcon_client:
cleaned_text = args['text'].replace('"', '')
self.rcon_client.send_command(f"/sc game.print(\"[font=default-large-bold]Archipelago:[/font] "
f"{cleaned_text}\")")
self.print_to_game(args['text'])
def on_print_json(self, args: dict):
if not self.found_items and args.get("type", None) == "ItemSend" and args["receiving"] == args["sending"]:
pass # don't want info on other player's local pickups.
text = self.raw_json_text_parser(copy.deepcopy(args["data"]))
logger.info(text)
if self.rcon_client:
text = self.factorio_json_text_parser(args["data"])
cleaned_text = text.replace('"', '')
self.rcon_client.send_command(f"/sc game.print(\"[font=default-large-bold]Archipelago:[/font] "
f"{cleaned_text}\")")
text = self.factorio_json_text_parser(copy.deepcopy(args["data"]))
self.print_to_game(text)
super(FactorioContext, self).on_print_json(args)
@property
def savegame_name(self) -> str:
return f"AP_{self.seed_name}_{self.auth}_Save.zip"
def print_to_game(self, text):
self.rcon_client.send_command(f"/ap-print [font=default-large-bold]Archipelago:[/font] "
f"{text}")
def on_deathlink(self, data: dict):
if self.rcon_client:
self.rcon_client.send_command(f"/ap-deathlink {data['source']}")
super(FactorioContext, self).on_deathlink(data)
def on_package(self, cmd: str, args: dict):
if cmd in {"Connected", "RoomUpdate"}:
# catch up sync anything that is already cleared.
if "checked_locations" in args and args["checked_locations"]:
self.rcon_client.send_commands({item_name: f'/ap-get-technology ap-{item_name}-\t-1' for
item_name in args["checked_locations"]})
async def game_watcher(ctx: FactorioContext, bridge_file: str):
async def game_watcher(ctx: FactorioContext):
bridge_logger = logging.getLogger("FactorioWatcher")
from worlds.factorio.Technologies import lookup_id_to_name
bridge_counter = 0
next_bridge = time.perf_counter() + 1
try:
while not ctx.exit_event.is_set():
if os.path.exists(bridge_file):
bridge_logger.info("Found Factorio Bridge file.")
while not ctx.exit_event.is_set():
if ctx.awaiting_bridge:
ctx.awaiting_bridge = False
with open(bridge_file) as f:
data = json.load(f)
research_data = data["research_done"]
research_data = {int(tech_name.split("-")[1]) for tech_name in research_data}
victory = data["victory"]
ctx.auth = data["slot_name"]
ctx.seed_name = data["seed_name"]
if ctx.awaiting_bridge and ctx.rcon_client and time.perf_counter() > next_bridge:
next_bridge = time.perf_counter() + 1
ctx.awaiting_bridge = False
data = json.loads(ctx.rcon_client.send_command("/ap-sync"))
if data["slot_name"] != ctx.auth:
bridge_logger.warning(f"Connected World is not the expected one {data['slot_name']} != {ctx.auth}")
elif data["seed_name"] != ctx.seed_name:
bridge_logger.warning(
f"Connected Multiworld is not the expected one {data['seed_name']} != {ctx.seed_name}")
else:
data = data["info"]
research_data = data["research_done"]
research_data = {int(tech_name.split("-")[1]) for tech_name in research_data}
victory = data["victory"]
if "death_link" in data: # TODO: Remove this if statement around version 0.2.4 or so
await ctx.update_death_link(data["death_link"])
if not ctx.finished_game and victory:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
if not ctx.finished_game and victory:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
if ctx.locations_checked != research_data:
bridge_logger.debug(
f"New researches done: "
f"{[lookup_id_to_name[rid] for rid in research_data - ctx.locations_checked]}")
ctx.locations_checked = research_data
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}])
death_link_tick = data.get("death_link_tick", 0)
if death_link_tick != ctx.death_link_tick:
ctx.death_link_tick = death_link_tick
if "DeathLink" in ctx.tags:
await ctx.send_death()
await asyncio.sleep(0.1)
if ctx.locations_checked != research_data:
bridge_logger.info(
f"New researches done: "
f"{[lookup_id_to_name[rid] for rid in research_data - ctx.locations_checked]}")
ctx.locations_checked = research_data
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}])
await asyncio.sleep(1)
else:
bridge_counter += 1
if bridge_counter >= 60:
bridge_logger.info(
"Did not find Factorio Bridge file, "
"waiting for mod to run, which requires the server to run, "
"which requires a player to be connected.")
bridge_counter = 0
await asyncio.sleep(1)
except Exception as e:
logging.exception(e)
logging.error("Aborted Factorio Server Bridge")
def stream_factorio_output(pipe, queue):
def stream_factorio_output(pipe, queue, process):
def queuer():
while 1:
while process.poll() is None:
text = pipe.readline().strip()
if text:
queue.put_nowait(text)
@@ -155,114 +164,212 @@ def stream_factorio_output(pipe, queue):
thread = Thread(target=queuer, name="Factorio Output Queue", daemon=True)
thread.start()
return thread
async def factorio_server_watcher(ctx: FactorioContext):
import subprocess
import factorio_rcon
factorio_server_logger = logging.getLogger("FactorioServer")
factorio_process = subprocess.Popen((executable, "--start-server", *(str(elem) for elem in server_args)),
savegame_name = os.path.abspath(ctx.savegame_name)
if not os.path.exists(savegame_name):
logger.info(f"Creating savegame {savegame_name}")
subprocess.run((
executable, "--create", savegame_name, "--preset", "archipelago"
))
factorio_process = subprocess.Popen((executable, "--start-server", ctx.savegame_name,
*(str(elem) for elem in server_args)),
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
stdin=subprocess.DEVNULL,
encoding="utf-8")
factorio_server_logger.info("Started Factorio Server")
factorio_queue = Queue()
stream_factorio_output(factorio_process.stdout, factorio_queue)
stream_factorio_output(factorio_process.stderr, factorio_queue)
script_folder = None
progression_watcher = None
stream_factorio_output(factorio_process.stdout, factorio_queue, factorio_process)
stream_factorio_output(factorio_process.stderr, factorio_queue, factorio_process)
try:
while not ctx.exit_event.is_set():
if factorio_process.poll():
factorio_server_logger.info("Factorio server has exited.")
ctx.exit_event.set()
while not factorio_queue.empty():
msg = factorio_queue.get()
factorio_server_logger.info(msg)
factorio_queue.task_done()
if not ctx.rcon_client and "Starting RCON interface at IP ADDR:" in msg:
ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
# trigger lua interface confirmation
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("/ap-sync")
if not script_folder and "Write data path:" in msg:
script_folder = msg.split("Write data path: ", 1)[1].split("[", 1)[0].strip()
bridge_file = os.path.join(script_folder, "script-output", "ap_bridge.json")
if os.path.exists(bridge_file):
os.remove(bridge_file)
logging.info(f"Bridge File Path: {bridge_file}")
progression_watcher = asyncio.create_task(
game_watcher(ctx, bridge_file), name="FactorioProgressionWatcher")
if not ctx.awaiting_bridge and "Archipelago Bridge File written for game tick " in msg:
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:
ctx.awaiting_bridge = True
factorio_server_logger.debug(msg)
else:
factorio_server_logger.info(msg)
if ctx.rcon_client:
commands = {}
while ctx.send_index < len(ctx.items_received):
transfer_item: NetworkItem = ctx.items_received[ctx.send_index]
item_id = transfer_item.item
player_name = ctx.player_names[transfer_item.player]
if item_id not in lookup_id_to_name:
logging.error(f"Cannot send unknown item ID: {item_id}")
if item_id not in Factorio.item_id_to_name:
factorio_server_logger.error(f"Cannot send unknown item ID: {item_id}")
else:
item_name = 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}.")
ctx.rcon_client.send_command(f'/ap-get-technology {item_name} {player_name}')
commands[ctx.send_index] = f'/ap-get-technology {item_name}\t{ctx.send_index}\t{player_name}'
ctx.send_index += 1
await asyncio.sleep(1)
if commands:
ctx.rcon_client.send_commands(commands)
await asyncio.sleep(0.1)
except Exception as e:
logging.exception(e)
logging.error("Aborted Factorio Server Bridge")
ctx.rcon_client = None
ctx.exit_event.set()
finally:
factorio_process.terminate()
if progression_watcher:
await progression_watcher
factorio_process.wait(5)
async def main():
ctx = FactorioContext(None, None, True)
# testing shortcuts
# ctx.server_address = "localhost"
# ctx.auth = "Nauvis"
if ctx.server_task is None:
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
await asyncio.sleep(3)
input_task = asyncio.create_task(console_loop(ctx), name="Input")
factorio_server_task = asyncio.create_task(factorio_server_watcher(ctx), name="FactorioServer")
await ctx.exit_event.wait()
ctx.server_address = None
ctx.snes_reconnect_address = None
async def get_info(ctx, rcon_client):
info = json.loads(rcon_client.send_command("/ap-rcon-info"))
ctx.auth = info["slot_name"]
ctx.seed_name = info["seed_name"]
# 0.2.0 addition, not present earlier
death_link = bool(info.get("death_link", False))
await ctx.update_death_link(death_link)
await asyncio.gather(input_task, factorio_server_task)
if ctx.server is not None and not ctx.server.socket.closed:
await ctx.server.socket.close()
if ctx.server_task is not None:
await ctx.server_task
await factorio_server_task
async def factorio_spinup_server(ctx: FactorioContext) -> bool:
savegame_name = os.path.abspath("Archipelago.zip")
if not os.path.exists(savegame_name):
logger.info(f"Creating savegame {savegame_name}")
subprocess.run((
executable, "--create", savegame_name
))
factorio_process = subprocess.Popen(
(executable, "--start-server", savegame_name, *(str(elem) for elem in server_args)),
stderr=subprocess.PIPE,
stdout=subprocess.PIPE,
stdin=subprocess.DEVNULL,
encoding="utf-8")
factorio_server_logger.info("Started Information Exchange Factorio Server")
factorio_queue = Queue()
stream_factorio_output(factorio_process.stdout, factorio_queue, factorio_process)
stream_factorio_output(factorio_process.stderr, factorio_queue, factorio_process)
rcon_client = None
try:
while not ctx.auth:
while not factorio_queue.empty():
msg = factorio_queue.get()
factorio_server_logger.info(msg)
if "Loading mod AP-" in msg and msg.endswith("(data.lua)"):
parts = msg.split()
ctx.mod_version = Utils.Version(*(int(number) for number in parts[-2].split(".")))
elif "Write data path: " in msg:
ctx.write_data_path = Utils.get_text_between(msg, "Write data path: ", " [")
if "AppData" in ctx.write_data_path:
logger.warning("It appears your mods are loaded from Appdata, "
"this can lead to problems with multiple Factorio instances. "
"If this is the case, you will get a file locked error running Factorio.")
if not rcon_client and "Starting RCON interface at IP ADDR:" in msg:
rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
if ctx.mod_version == ctx.__class__.mod_version:
raise Exception("No Archipelago mod was loaded. Aborting.")
await get_info(ctx, rcon_client)
await asyncio.sleep(0.01)
while ctx.input_requests > 0:
ctx.input_queue.put_nowait(None)
ctx.input_requests -= 1
except Exception as e:
logger.exception(e)
logger.error("Aborted Factorio Server Bridge")
ctx.exit_event.set()
await input_task
else:
logger.info(
f"Got World Information from AP Mod {tuple(ctx.mod_version)} for seed {ctx.seed_name} in slot {ctx.auth}")
return True
finally:
factorio_process.terminate()
factorio_process.wait(5)
return False
async def main(args):
ctx = FactorioContext(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
input_task = None
if gui_enabled:
from kvui import FactorioManager
ctx.ui = FactorioManager(ctx)
ui_task = asyncio.create_task(ctx.ui.async_run(), name="UI")
else:
ui_task = None
if sys.stdin:
input_task = asyncio.create_task(console_loop(ctx), name="Input")
factorio_server_task = asyncio.create_task(factorio_spinup_server(ctx), name="FactorioSpinupServer")
successful_launch = await factorio_server_task
if successful_launch:
factorio_server_task = asyncio.create_task(factorio_server_watcher(ctx), name="FactorioServer")
progression_watcher = asyncio.create_task(
game_watcher(ctx), name="FactorioProgressionWatcher")
await ctx.exit_event.wait()
ctx.server_address = None
await progression_watcher
await factorio_server_task
await ctx.shutdown()
if ui_task:
await ui_task
if input_task:
input_task.cancel()
class FactorioJSONtoTextParser(JSONtoTextParser):
def _handle_color(self, node: JSONMessagePart):
colors = node["color"].split(";")
for color in colors:
if color in {"red", "green", "blue", "orange", "yellow", "pink", "purple", "white", "black", "gray",
"brown", "cyan", "acid"}:
node["text"] = f"[color={color}]{node['text']}[/color]"
return self._handle_text(node)
elif color == "magenta":
node["text"] = f"[color=pink]{node['text']}[/color]"
if color in self.color_codes:
node["text"] = f"[color=#{self.color_codes[color]}]{node['text']}[/color]"
return self._handle_text(node)
return self._handle_text(node)
if __name__ == '__main__':
parser = get_base_parser(description="Optional arguments to FactorioClient follow. "
"Remaining arguments get passed into bound Factorio instance."
"Refer to Factorio --help for those.")
parser.add_argument('--rcon-port', default='24242', type=int, help='Port to use to communicate with Factorio')
parser.add_argument('--rcon-password', help='Password to authenticate with RCON.')
args, rest = parser.parse_known_args()
colorama.init()
rcon_port = args.rcon_port
rcon_password = args.rcon_password if args.rcon_password else ''.join(
random.choice(string.ascii_letters) for x in range(32))
factorio_server_logger = logging.getLogger("FactorioServer")
options = Utils.get_options()
executable = options["factorio_options"]["executable"]
if not os.path.exists(os.path.dirname(executable)):
raise FileNotFoundError(f"Path {os.path.dirname(executable)} does not exist or could not be accessed.")
if os.path.isdir(executable): # user entered a path to a directory, let's find the executable therein
executable = os.path.join(executable, "factorio")
if not os.path.isfile(executable):
if os.path.isfile(executable + ".exe"):
executable = executable + ".exe"
else:
raise FileNotFoundError(f"Path {executable} is not an executable file.")
server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, *rest)
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.run_until_complete(main(args))
loop.close()
colorama.deinit()

View File

@@ -1,167 +0,0 @@
import os
import logging
import sys
os.makedirs("logs", exist_ok=True)
if getattr(sys, "frozen", False):
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO,
filename=os.path.join("logs", "FactorioClient.txt"), filemode="w")
else:
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO)
logging.getLogger().addHandler(logging.FileHandler(os.path.join("logs", "FactorioClient.txt"), "w"))
os.environ["KIVY_NO_CONSOLELOG"] = "1"
os.environ["KIVY_NO_FILELOG"] = "1"
os.environ["KIVY_NO_ARGS"] = "1"
import asyncio
from CommonClient import server_loop, logger
from FactorioClient import FactorioContext, factorio_server_watcher
async def main():
ctx = FactorioContext(None, None, True)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
factorio_server_task = asyncio.create_task(factorio_server_watcher(ctx), name="FactorioServer")
ui_app = FactorioManager(ctx)
ui_task = asyncio.create_task(ui_app.async_run(), name="UI")
await ctx.exit_event.wait() # wait for signal to exit application
ui_app.stop()
ctx.server_address = None
ctx.snes_reconnect_address = None
# allow tasks to quit
await ui_task
await factorio_server_task
await ctx.server_task
if ctx.server is not None and not ctx.server.socket.closed:
await ctx.server.socket.close()
if ctx.server_task is not None:
await ctx.server_task
while ctx.input_requests > 0: # clear queue for shutdown
ctx.input_queue.put_nowait(None)
ctx.input_requests -= 1
from kivy.app import App
from kivy.uix.label import Label
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 = "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 File 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)
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_string('''
<TabbedPanel>
tab_width: 200
<Row@Label>:
canvas.before:
Color:
rgba: 0.2, 0.2, 0.2, 1
Rectangle:
size: self.size
pos: self.pos
text_size: self.width, None
size_hint_y: None
height: self.texture_size[1]
font_size: dp(20)
<UILog>:
viewclass: 'Row'
scroll_y: 0
effect_cls: "ScrollEffect"
RecycleBoxLayout:
default_size: None, dp(20)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
spacing: dp(3)
''')
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
loop.close()

484
Fill.py
View File

@@ -2,41 +2,50 @@ import logging
import typing
import collections
import itertools
from collections import Counter, deque
from BaseClasses import CollectionState, PlandoItem, Location
from worlds.alttp.Items import ItemFactory
from worlds.alttp.Regions import key_drop_data
from BaseClasses import CollectionState, Location, LocationProgressType, MultiWorld, Item
from worlds.AutoWorld import call_all
class FillError(RuntimeError):
pass
def fill_restrictive(world, base_state: CollectionState, locations, itempool, single_player_placement=False,
lock=False):
def sweep_from_pool():
new_state = base_state.copy()
for item in itempool:
new_state.collect(item, True)
new_state.sweep_for_events()
return new_state
unplaced_items = []
placements = []
reachable_items = {}
def sweep_from_pool(base_state: CollectionState, itempool=[]):
new_state = base_state.copy()
for item in itempool:
reachable_items.setdefault(item.player, []).append(item)
new_state.collect(item, True)
new_state.sweep_for_events()
return new_state
def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations, itempool: typing.List[Item],
single_player_placement=False, lock=False):
unplaced_items = []
placements: typing.List[Location] = []
swapped_items = Counter()
reachable_items: typing.Dict[int, deque] = {}
for item in itempool:
reachable_items.setdefault(item.player, deque()).append(item)
while any(reachable_items.values()) and locations:
items_to_place = [items.pop() for items in reachable_items.values() if items] # grab one item per player
# grab one item per player
items_to_place = [items.pop()
for items in reachable_items.values() if items]
for item in items_to_place:
itempool.remove(item)
maximum_exploration_state = sweep_from_pool()
maximum_exploration_state = sweep_from_pool(
base_state, itempool + unplaced_items)
has_beaten_game = world.has_beaten_game(maximum_exploration_state)
for item_to_place in items_to_place:
if world.accessibility[item_to_place.player] == 'none':
spot_to_fill: typing.Optional[Location] = None
if world.accessibility[item_to_place.player] == 'minimal':
perform_access_check = not world.has_beaten_game(maximum_exploration_state,
item_to_place.player) if single_player_placement else not has_beaten_game
else:
@@ -45,103 +54,146 @@ def fill_restrictive(world, base_state: CollectionState, locations, itempool, si
for i, location in enumerate(locations):
if (not single_player_placement or location.player == item_to_place.player) \
and location.can_fill(maximum_exploration_state, item_to_place, perform_access_check):
spot_to_fill = locations.pop(i) # poping by index is faster than removing by content,
# poping by index is faster than removing by content,
spot_to_fill = locations.pop(i)
# skipping a scan for the element
break
else:
# we filled all reachable spots. Maybe the game can be beaten anyway?
unplaced_items.append(item_to_place)
if world.accessibility[item_to_place.player] != 'none' and world.can_beat_game():
logging.warning(
f'Not all items placed. Game beatable anyway. (Could not place {item_to_place})')
continue
# we filled all reachable spots.
# try swapping this item with previously placed items
for(i, location) in enumerate(placements):
placed_item = location.item
# Unplaceable items can sometimes be swapped infinitely. Limit the
# number of times we will swap an individual item to prevent this
swap_count = swapped_items[placed_item.player,
placed_item.name]
if swap_count > 1:
continue
raise FillError(f'No more spots to place {item_to_place}, locations {locations} are invalid. '
f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}')
location.item = None
placed_item.location = None
swap_state = sweep_from_pool(base_state)
if (not single_player_placement or location.player == item_to_place.player) \
and location.can_fill(swap_state, item_to_place, perform_access_check):
# Verify that placing this item won't reduce available locations
prev_state = swap_state.copy()
prev_state.collect(placed_item)
prev_loc_count = len(
world.get_reachable_locations(prev_state))
swap_state.collect(item_to_place, True)
new_loc_count = len(
world.get_reachable_locations(swap_state))
if new_loc_count >= prev_loc_count:
# Add this item to the existing placement, and
# add the old item to the back of the queue
spot_to_fill = placements.pop(i)
swap_count += 1
swapped_items[placed_item.player,
placed_item.name] = swap_count
reachable_items[placed_item.player].appendleft(
placed_item)
itempool.append(placed_item)
break
# Item can't be placed here, restore original item
location.item = placed_item
placed_item.location = location
if spot_to_fill is None:
# Can't place this item, move on to the next
unplaced_items.append(item_to_place)
continue
world.push_item(spot_to_fill, item_to_place, False)
spot_to_fill.locked = lock
placements.append(spot_to_fill)
spot_to_fill.event = True
spot_to_fill.event = item_to_place.advancement
if len(unplaced_items) > 0 and len(locations) > 0:
# There are leftover unplaceable items and locations that won't accept them
if world.can_beat_game():
logging.warning(
f'Not all items placed. Game beatable anyway. (Could not place {unplaced_items})')
else:
raise FillError(f'No more spots to place {unplaced_items}, locations {locations} are invalid. '
f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}')
itempool.extend(unplaced_items)
def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None):
# If not passed in, then get a shuffled list of locations to fill in
if not fill_locations:
fill_locations = world.get_unfilled_locations()
world.random.shuffle(fill_locations)
def distribute_items_restrictive(world: MultiWorld):
fill_locations = sorted(world.get_unfilled_locations())
world.random.shuffle(fill_locations)
# get items to distribute
world.random.shuffle(world.itempool)
itempool = sorted(world.itempool)
world.random.shuffle(itempool)
progitempool = []
nonexcludeditempool = []
localrestitempool = {player: [] for player in range(1, world.players + 1)}
nonlocalrestitempool = []
restitempool = []
for item in world.itempool:
for item in itempool:
if item.advancement:
progitempool.append(item)
elif item.name in world.local_items[item.player]:
elif item.never_exclude: # this only gets nonprogression items which should not appear in excluded locations
nonexcludeditempool.append(item)
elif item.name in world.local_items[item.player].value:
localrestitempool[item.player].append(item)
elif item.name in world.non_local_items[item.player].value:
nonlocalrestitempool.append(item)
else:
restitempool.append(item)
standard_keyshuffle_players = set()
call_all(world, "fill_hook", progitempool, nonexcludeditempool,
localrestitempool, nonlocalrestitempool, restitempool, fill_locations)
# fill in gtower locations with trash first
for player in world.alttp_player_ids:
if not gftower_trash or not world.ganonstower_vanilla[player] or \
world.logic[player] in {'owglitches', "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)
locations: typing.Dict[LocationProgressType, typing.List[Location]] = {
loc_type: [] for loc_type in LocationProgressType}
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()
for loc in fill_locations:
locations[loc.progress_type].append(loc)
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)
prioritylocations = locations[LocationProgressType.PRIORITY]
defaultlocations = locations[LocationProgressType.DEFAULT]
excludedlocations = locations[LocationProgressType.EXCLUDED]
fill_restrictive(world, world.state, prioritylocations, progitempool)
if prioritylocations:
defaultlocations = prioritylocations + defaultlocations
# 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)
if progitempool:
fill_restrictive(world, world.state, defaultlocations, progitempool)
if progitempool:
raise FillError(
f'Not enough locations for progress items. There are {len(progitempool)} more items than locations')
world.random.shuffle(fill_locations)
fill_restrictive(world, world.state, fill_locations, progitempool)
if nonexcludeditempool:
world.random.shuffle(defaultlocations)
# needs logical fill to not conflict with local items
fill_restrictive(
world, world.state, defaultlocations, nonexcludeditempool)
if nonexcludeditempool:
raise FillError(
f'Not enough locations for non-excluded items. There are {len(nonexcludeditempool)} more items than locations')
defaultlocations = defaultlocations + excludedlocations
world.random.shuffle(defaultlocations)
if any(localrestitempool.values()): # we need to make sure some fills are limited to certain worlds
local_locations = {player: [] for player in world.player_ids}
for location in fill_locations:
for location in defaultlocations:
local_locations[location.player].append(location)
for locations in local_locations.values():
world.random.shuffle(locations)
for player_locations in local_locations.values():
world.random.shuffle(player_locations)
for player, items in localrestitempool.items(): # items already shuffled
player_local_locations = local_locations[player]
@@ -152,29 +204,43 @@ def distribute_items_restrictive(world, gftower_trash=False, fill_locations=None
break
spot_to_fill = player_local_locations.pop()
world.push_item(spot_to_fill, item_to_place, False)
fill_locations.remove(spot_to_fill)
defaultlocations.remove(spot_to_fill)
world.random.shuffle(fill_locations)
for item_to_place in nonlocalrestitempool:
for i, location in enumerate(defaultlocations):
if location.player != item_to_place.player:
world.push_item(defaultlocations.pop(i), item_to_place, False)
break
else:
logging.warning(
f"Could not place non_local_item {item_to_place} among {defaultlocations}, tossing.")
restitempool, fill_locations = fast_fill(world, restitempool, fill_locations)
unplaced = [item for item in progitempool + restitempool]
unfilled = [location.name for location in fill_locations]
world.random.shuffle(defaultlocations)
for location in fill_locations:
world.push_item(location, ItemFactory('Nothing', location.player), False)
restitempool, defaultlocations = fast_fill(
world, restitempool, defaultlocations)
unplaced = progitempool + restitempool
unfilled = [location.name for location in defaultlocations]
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}')
items_counter = Counter([location.item.player for location in world.get_locations()])
locations_counter = Counter([location.player for location in world.get_locations()])
items_counter.update([item.player for item in unplaced])
locations_counter.update([location.player for location in unfilled])
print_data = {"items": items_counter, "locations": locations_counter}
logging.info(f'Per-Player counts: {print_data})')
def fast_fill(world, item_pool: typing.List, fill_locations: typing.List) -> typing.Tuple[typing.List, typing.List]:
def fast_fill(world: MultiWorld, item_pool: typing.List, fill_locations: typing.List) -> typing.Tuple[typing.List, typing.List]:
placing = min(len(item_pool), len(fill_locations))
for item, location in zip(item_pool, fill_locations):
world.push_item(location, item, False)
return item_pool[placing:], fill_locations[placing:]
def flood_items(world):
def flood_items(world: MultiWorld):
# get items to distribute
world.random.shuffle(world.itempool)
itempool = world.itempool
@@ -224,7 +290,7 @@ def flood_items(world):
location_list = world.get_reachable_locations()
world.random.shuffle(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
replace_item = location.item
replace_item.location = None
@@ -234,7 +300,7 @@ def flood_items(world):
break
def balance_multiworld_progression(world):
def balance_multiworld_progression(world: MultiWorld):
balanceable_players = {player for player in range(1, world.players + 1) if world.progression_balancing[player]}
if not balanceable_players:
logging.info('Skipping multiworld progression balancing.')
@@ -244,7 +310,7 @@ def balance_multiworld_progression(world):
checked_locations = set()
unchecked_locations = set(world.get_locations())
reachable_locations_count = {player: 0 for player in world.player_ids}
reachable_locations_count = {player: 0 for player in world.get_all_ids()}
def get_sphere_locations(sphere_state, locations):
sphere_state.sweep_for_events(key_only=True, locations=locations)
@@ -272,7 +338,10 @@ def balance_multiworld_progression(world):
balancing_state.collect(location.item, True, location)
player = location.item.player
# only replace items that end up in another player's world
if not location.locked and player in balancing_players and location.player != player:
if(not location.locked and
player in balancing_players and
location.player != player and
location.progress_type != LocationProgressType.PRIORITY):
candidate_items[player].add(location)
balancing_sphere = get_sphere_locations(balancing_state, balancing_unchecked_locations)
for location in balancing_sphere:
@@ -347,7 +416,8 @@ def balance_multiworld_progression(world):
if world.has_beaten_game(state):
break
elif not sphere_locations:
raise RuntimeError('Not all required items reachable. Something went terribly wrong here.')
logging.warning("Progression Balancing ran out of paths.")
break
def swap_location_item(location_1: Location, location_2: Location, check_locked=True):
@@ -363,73 +433,189 @@ def swap_location_item(location_1: Location, location_2: Location, check_locked=
location_1.event, location_2.event = location_2.event, location_1.event
def distribute_planned(world):
def distribute_planned(world: MultiWorld):
def warn(warning: str, force):
if force in [True, 'fail', 'failure', 'none', False, 'warn', 'warning']:
logging.warning(f'{warning}')
else:
logging.debug(f'{warning}')
def failed(warning: str, force):
if force in [True, 'fail', 'failure']:
raise Exception(warning)
else:
warn(warning, force)
# TODO: remove. Preferably by implementing key drop
from worlds.alttp.Regions import key_drop_data
world_name_lookup = world.world_name_lookup
for player in world.player_ids:
placement: PlandoItem
for placement in world.plando_items[player]:
if placement.location in key_drop_data:
placement.warn(
f"Can't place '{placement.item}' at '{placement.location}', as key drop shuffle locations are not supported yet.")
plando_blocks = []
player_ids = set(world.player_ids)
for player in player_ids:
for block in world.plando_items[player]:
block['player'] = player
if 'force' not in block:
block['force'] = 'silent'
if 'from_pool' not in block:
block['from_pool'] = True
if 'world' not in block:
block['world'] = False
items = []
if "items" in block:
items = block["items"]
if 'count' not in block:
block['count'] = False
elif "item" in block:
items = block["item"]
if 'count' not in block:
block['count'] = 1
else:
failed("You must specify at least one item to place items with plando.", block['force'])
continue
item = ItemFactory(placement.item, player)
target_world: int = placement.world
if target_world is False or world.players == 1:
target_world = player # in own world
elif target_world is True: # in any other world
unfilled = list(location for location in world.get_unfilled_locations_for_players(
placement.location,
set(world.player_ids) - {player}) if location.item_rule(item)
)
if not unfilled:
placement.failed(f"Could not find a world with an unfilled location {placement.location}",
FillError)
continue
if isinstance(items, dict):
item_list = []
for key, value in items.items():
if value is True:
value = world.itempool.count(world.worlds[player].create_item(key))
item_list += [key] * value
items = item_list
if isinstance(items, str):
items = [items]
block['items'] = items
target_world = world.random.choice(unfilled).player
locations = []
if 'location' in block:
locations = block['location'] # just allow 'location' to keep old yamls compatible
elif 'locations' in block:
locations = block['locations']
if isinstance(locations, str):
locations = [locations]
elif target_world is None: # any random world
unfilled = list(location for location in world.get_unfilled_locations_for_players(
placement.location,
set(world.player_ids)) if location.item_rule(item)
)
if not unfilled:
placement.failed(f"Could not find a world with an unfilled location {placement.location}",
FillError)
continue
if isinstance(locations, dict):
location_list = []
for key, value in locations.items():
location_list += [key] * value
locations = location_list
if isinstance(locations, str):
locations = [locations]
block['locations'] = locations
target_world = world.random.choice(unfilled).player
if not block['count']:
block['count'] = (min(len(block['items']), len(block['locations'])) if len(block['locations'])
> 0 else len(block['items']))
if isinstance(block['count'], int):
block['count'] = {'min': block['count'], 'max': block['count']}
if 'min' not in block['count']:
block['count']['min'] = 0
if 'max' not in block['count']:
block['count']['max'] = (min(len(block['items']), len(block['locations'])) if len(block['locations'])
> 0 else len(block['items']))
if block['count']['max'] > len(block['items']):
count = block['count']
failed(f"Plando count {count} greater than items specified", block['force'])
block['count'] = len(block['items'])
if block['count']['max'] > len(block['locations']) > 0:
count = block['count']
failed(f"Plando count {count} greater than locations specified", block['force'])
block['count'] = len(block['locations'])
block['count']['target'] = world.random.randint(block['count']['min'], block['count']['max'])
elif type(target_world) == int: # target world by player id
if block['count']['target'] > 0:
plando_blocks.append(block)
# shuffle, but then sort blocks by number of locations minus number of items,
# so less-flexible blocks get priority
world.random.shuffle(plando_blocks)
plando_blocks.sort(key=lambda block: (len(block['locations']) - block['count']['target']
if len(block['locations']) > 0
else len(world.get_unfilled_locations(player)) - block['count']['target']))
for placement in plando_blocks:
player = placement['player']
try:
target_world = placement['world']
locations = placement['locations']
items = placement['items']
maxcount = placement['count']['target']
from_pool = placement['from_pool']
if target_world is False or world.players == 1: # target own world
worlds = {player}
elif target_world is True: # target any worlds besides own
worlds = set(world.player_ids) - {player}
elif target_world is None: # target all worlds
worlds = set(world.player_ids)
elif type(target_world) == list: # list of target worlds
worlds = []
for listed_world in target_world:
if listed_world not in world_name_lookup:
failed(f"Cannot place item to {target_world}'s world as that world does not exist.",
placement['force'])
continue
worlds.append(world_name_lookup[listed_world])
worlds = set(worlds)
elif type(target_world) == int: # target world by slot number
if target_world not in range(1, world.players + 1):
placement.failed(
failed(
f"Cannot place item in world {target_world} as it is not in range of (1, {world.players})",
ValueError)
placement['force'])
continue
else: # find world by name
worlds = {target_world}
else: # target world by slot name
if target_world not in world_name_lookup:
placement.failed(f"Cannot place item to {target_world}'s world as that world does not exist.",
ValueError)
failed(f"Cannot place item to {target_world}'s world as that world does not exist.",
placement['force'])
continue
target_world = world_name_lookup[target_world]
worlds = {world_name_lookup[target_world]}
location = world.get_location(placement.location, target_world)
if location.item:
placement.failed(f"Cannot place item into already filled location {location}.")
continue
if location.can_fill(world.state, item, False):
candidates = list(location for location in world.get_unfilled_locations_for_players(locations,
worlds))
world.random.shuffle(candidates)
world.random.shuffle(items)
count = 0
err = []
successful_pairs = []
for item_name in items:
item = world.worlds[player].create_item(item_name)
for location in reversed(candidates):
if location in key_drop_data:
warn(
f"Can't place '{item_name}' at '{placement.location}', as key drop shuffle locations are not supported yet.")
continue
if not location.item:
if location.item_rule(item):
if location.can_fill(world.state, item, False):
successful_pairs.append([item, location])
candidates.remove(location)
count = count + 1
break
else:
err.append(f"Can't place item at {location} due to fill condition not met.")
else:
err.append(f"{item_name} not allowed at {location}.")
else:
err.append(f"Cannot place {item_name} into already filled location {location}.")
if count == maxcount:
break
if count < placement['count']['min']:
err = " ".join(err)
m = placement['count']['min']
failed(
f"Plando block failed to place {m - count} of {m} item(s) for {world.player_name[player]}, error(s): {err}",
placement['force'])
for (item, location) in successful_pairs:
world.push_item(location, item, collect=False)
location.event = True # flag location to be checked during fill
location.locked = True
logging.debug(f"Plando placed {item} at {location}")
else:
placement.failed(f"Can't place {item} at {location} due to fill condition not met.")
continue
if from_pool:
try:
world.itempool.remove(item)
except ValueError:
warn(
f"Could not remove {item} from pool for {world.player_name[player]} as it's already missing from it.",
placement['force'])
if placement.from_pool: # Should happen AFTER the item is placed, in case it was allowed to skip failed placement.
try:
world.itempool.remove(item)
except ValueError:
placement.warn(f"Could not remove {item} from pool as it's already missing from it.")
except Exception as e:
raise Exception(
f"Error running plando for player {player} ({world.player_name[player]})") from e

665
Generate.py Normal file
View File

@@ -0,0 +1,665 @@
import argparse
import logging
import random
import urllib.request
import urllib.parse
import typing
import os
from collections import Counter
import string
import ModuleUpdate
ModuleUpdate.update()
import Utils
from worlds.alttp import Options as LttPOptions
from worlds.generic import PlandoConnection
from Utils import parse_yaml, version_tuple, __version__, tuplize_version, get_options
from worlds.alttp.EntranceRandomizer import parse_arguments
from Main import main as ERmain
from BaseClasses import seeddigits, get_seed
import Options
from worlds.alttp import Bosses
from worlds.alttp.Text import TextTable
from worlds.AutoWorld import AutoWorldRegister
import copy
categories = set(AutoWorldRegister.world_types)
def mystery_argparse():
options = get_options()
defaults = options["generator"]
parser = argparse.ArgumentParser(description="CMD Generation Interface, defaults come from host.yaml.")
parser.add_argument('--weights_file_path', default = defaults["weights_file_path"],
help='Path to the weights file to use for rolling game settings, urls are also valid')
parser.add_argument('--samesettings', help='Rolls settings per weights file rather than per player',
action='store_true')
parser.add_argument('--player_files_path', default=defaults["player_files_path"],
help="Input directory for player files.")
parser.add_argument('--seed', help='Define seed number to generate.', type=int)
parser.add_argument('--multi', default=defaults["players"], type=lambda value: max(int(value), 1))
parser.add_argument('--spoiler', type=int, default=defaults["spoiler"])
parser.add_argument('--lttp_rom', default=options["lttp_options"]["rom_file"], help="Path to the 1.0 JP LttP Baserom.")
parser.add_argument('--sm_rom', default=options["sm_options"]["rom_file"], help="Path to the 1.0 JP SM Baserom.")
parser.add_argument('--enemizercli', default=defaults["enemizer_path"])
parser.add_argument('--outputpath', default=options["general_options"]["output_path"])
parser.add_argument('--race', action='store_true', default=defaults["race"])
parser.add_argument('--meta_file_path', default=defaults["meta_file_path"])
parser.add_argument('--log_level', default='info', help='Sets log level')
parser.add_argument('--yaml_output', default=0, type=lambda value: max(int(value), 0),
help='Output rolled mystery results to yaml up to specified number (made for async multiworld)')
parser.add_argument('--plando', default=defaults["plando_options"],
help='List of options that can be set manually. Can be combined, for example "bosses, items"')
args = parser.parse_args()
if not os.path.isabs(args.weights_file_path):
args.weights_file_path = os.path.join(args.player_files_path, args.weights_file_path)
if not os.path.isabs(args.meta_file_path):
args.meta_file_path = os.path.join(args.player_files_path, args.meta_file_path)
args.plando: typing.Set[str] = {arg.strip().lower() for arg in args.plando.split(",")}
return args, options
def get_seed_name(random_source) -> str:
return f"{random_source.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits)
def main(args=None, callback=ERmain):
if not args:
args, options = mystery_argparse()
seed = get_seed(args.seed)
random.seed(seed)
seed_name = get_seed_name(random)
if args.race:
random.seed() # reset to time-based random source
weights_cache = {}
if args.weights_file_path and os.path.exists(args.weights_file_path):
try:
weights_cache[args.weights_file_path] = read_weights_yaml(args.weights_file_path)
except Exception as e:
raise ValueError(f"File {args.weights_file_path} is destroyed. Please fix your yaml.") from e
print(f"Weights: {args.weights_file_path} >> "
f"{get_choice('description', weights_cache[args.weights_file_path], 'No description specified')}")
if args.meta_file_path and os.path.exists(args.meta_file_path):
try:
weights_cache[args.meta_file_path] = read_weights_yaml(args.meta_file_path)
except Exception as e:
raise ValueError(f"File {args.meta_file_path} is destroyed. Please fix your yaml.") from e
meta_weights = weights_cache[args.meta_file_path]
print(f"Meta: {args.meta_file_path} >> {get_choice('meta_description', meta_weights)}")
del(meta_weights["meta_description"])
if args.samesettings:
raise Exception("Cannot mix --samesettings with --meta")
else:
meta_weights = None
player_id = 1
player_files = {}
for file in os.scandir(args.player_files_path):
fname = file.name
if file.is_file() and os.path.join(args.player_files_path, fname) not in {args.meta_file_path, args.weights_file_path}:
path = os.path.join(args.player_files_path, fname)
try:
weights_cache[fname] = read_weights_yaml(path)
except Exception as e:
raise ValueError(f"File {fname} is destroyed. Please fix your yaml.") from e
else:
print(f"P{player_id} Weights: {fname} >> "
f"{get_choice('description', weights_cache[fname], 'No description specified')}")
player_files[player_id] = fname
player_id += 1
args.multi = max(player_id-1, args.multi)
print(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, {seed_name} Seed {seed} with plando: "
f"{', '.join(args.plando)}")
if not weights_cache:
raise Exception(f"No weights found. Provide a general weights file ({args.weights_file_path}) or individual player files. "
f"A mix is also permitted.")
erargs = parse_arguments(['--multi', str(args.multi)])
erargs.seed = seed
erargs.glitch_triforce = options["generator"]["glitch_triforce_room"]
erargs.spoiler = args.spoiler
erargs.race = args.race
erargs.outputname = seed_name
erargs.outputpath = args.outputpath
Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level)
erargs.lttp_rom = args.lttp_rom
erargs.sm_rom = args.sm_rom
erargs.enemizercli = args.enemizercli
settings_cache = {k: (roll_settings(v, args.plando) if args.samesettings else None)
for k, v in weights_cache.items()}
player_path_cache = {}
for player in range(1, args.multi + 1):
player_path_cache[player] = player_files.get(player, args.weights_file_path)
if meta_weights:
for category_name, category_dict in meta_weights.items():
for key in category_dict:
option = get_choice(key, category_dict)
if option is not None:
for player, path in player_path_cache.items():
if category_name is None:
weights_cache[path][key] = option
elif category_name not in weights_cache[path]:
logging.warning(f"Meta: Category {category_name} is not present in {path}.")
else:
weights_cache[path][category_name][key] = option
name_counter = Counter()
erargs.player_settings = {}
for player in range(1, args.multi + 1):
path = player_path_cache[player]
if path:
try:
settings = settings_cache[path] if settings_cache[path] else \
roll_settings(weights_cache[path], args.plando)
for k, v in vars(settings).items():
if v is not None:
try:
getattr(erargs, k)[player] = v
except AttributeError:
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:
raise ValueError(f"File {path} is destroyed. Please fix your yaml.") from e
else:
raise RuntimeError(f'No weights specified for player {player}')
if path == args.weights_file_path: # if name came from the weights file, just use base player name
erargs.name[player] = f"Player{player}"
elif not erargs.name[player]: # if name was not specified, generate it from filename
erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0]
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
if len(set(erargs.name.values())) != len(erargs.name):
raise Exception(f"Names have to be unique. Names: {Counter(erargs.name.values())}")
if args.yaml_output:
import yaml
important = {}
for option, player_settings in vars(erargs).items():
if type(player_settings) == dict:
if all(type(value) != list for value in player_settings.values()):
if len(player_settings.values()) > 1:
important[option] = {player: value for player, value in player_settings.items() if
player <= args.yaml_output}
else:
logging.debug(f"No player settings defined for option '{option}'")
else:
if player_settings != "": # is not empty name
important[option] = player_settings
else:
logging.debug(f"No player settings defined for option '{option}'")
if args.outputpath:
os.makedirs(args.outputpath, exist_ok=True)
with open(os.path.join(args.outputpath if args.outputpath else ".", f"generate_{seed_name}.yaml"), "wt") as f:
yaml.dump(important, f)
callback(erargs, seed)
def read_weights_yaml(path):
try:
if urllib.parse.urlparse(path).scheme:
yaml = str(urllib.request.urlopen(path).read(), "utf-8")
else:
with open(path, 'rb') as f:
yaml = str(f.read(), "utf-8")
except Exception as e:
raise Exception(f"Failed to read weights ({path})") from e
return parse_yaml(yaml)
def interpret_on_off(value) -> bool:
return {"on": True, "off": False}.get(value, value)
def convert_to_on_off(value) -> str:
return {True: "on", False: "off"}.get(value, value)
def get_choice_legacy(option, root, value=None) -> typing.Any:
if option not in root:
return value
if type(root[option]) is list:
return interpret_on_off(random.choices(root[option])[0])
if type(root[option]) is not dict:
return interpret_on_off(root[option])
if not root[option]:
return value
if any(root[option].values()):
return interpret_on_off(
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.")
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):
def __missing__(self, key):
return '{' + key + '}'
def handle_name(name: str, player: int, name_counter: Counter):
name_counter[name] += 1
new_name = "%".join([x.replace("%number%", "{number}").replace("%player%", "{player}") for x in name.split("%%")])
new_name = string.Formatter().vformat(new_name, (), SafeDict(number=name_counter[name],
NUMBER=(name_counter[name] if name_counter[
name] > 1 else ''),
player=player,
PLAYER=(player if player > 1 else '')))
new_name = new_name.strip()[:16]
if new_name == "Archipelago":
raise Exception(f"You cannot name yourself \"{new_name}\"")
return new_name
def prefer_int(input_data: str) -> typing.Union[str, int]:
try:
return int(input_data)
except:
return input_data
available_boss_names: typing.Set[str] = {boss.lower() for boss in Bosses.boss_table if boss not in
{'Agahnim', 'Agahnim2', 'Ganon'}}
available_boss_locations: typing.Set[str] = {f"{loc.lower()}{f' {level}' if level else ''}" for loc, level in
Bosses.boss_location_table}
boss_shuffle_options = {None: 'none',
'none': 'none',
'basic': 'basic',
'full': 'full',
'chaos': 'chaos',
'singularity': 'singularity'
}
goals = {
'ganon': 'ganon',
'crystals': 'crystals',
'bosses': 'bosses',
'pedestal': 'pedestal',
'ganon_pedestal': 'ganonpedestal',
'triforce_hunt': 'triforcehunt',
'local_triforce_hunt': 'localtriforcehunt',
'ganon_triforce_hunt': 'ganontriforcehunt',
'local_ganon_triforce_hunt': 'localganontriforcehunt',
'ice_rod_hunt': 'icerodhunt',
}
def roll_percentage(percentage: typing.Union[int, float]) -> bool:
"""Roll a percentage chance.
percentage is expected to be in range [0, 100]"""
return random.random() < (float(percentage) / 100)
def update_weights(weights: dict, new_weights: dict, type: str, name: str) -> dict:
logging.debug(f'Applying {new_weights}')
new_options = set(new_weights) - set(weights)
weights.update(new_weights)
if new_options:
for new_option in new_options:
logging.warning(f'{type} Suboption "{new_option}" of "{name}" did not '
f'overwrite a root option. '
f'This is probably in error.')
return weights
def roll_linked_options(weights: dict) -> dict:
weights = copy.deepcopy(weights) # make sure we don't write back to other weights sets in same_settings
for option_set in weights["linked_options"]:
if "name" not in option_set:
raise ValueError("One of your linked options does not have a name.")
try:
if roll_percentage(option_set["percentage"]):
logging.debug(f"Linked option {option_set['name']} triggered.")
new_options = option_set["options"]
for category_name, category_options in new_options.items():
currently_targeted_weights = weights
if category_name:
currently_targeted_weights = currently_targeted_weights[category_name]
update_weights(currently_targeted_weights, category_options, "Linked", option_set["name"])
else:
logging.debug(f"linked option {option_set['name']} skipped.")
except Exception as e:
raise ValueError(f"Linked option {option_set['name']} is destroyed. "
f"Please fix your linked option.") from e
return weights
def roll_triggers(weights: dict, triggers: list) -> dict:
weights = copy.deepcopy(weights) # make sure we don't write back to other weights sets in same_settings
weights["_Generator_Version"] = Utils.__version__
for i, option_set in enumerate(triggers):
try:
currently_targeted_weights = weights
category = option_set.get("option_category", None)
if category:
currently_targeted_weights = currently_targeted_weights[category]
key = get_choice("option_name", option_set)
if key not in currently_targeted_weights:
logging.warning(f'Specified option name {option_set["option_name"]} did not '
f'match with a root option. '
f'This is probably in error.')
trigger_result = get_choice("option_result", option_set)
result = get_choice(key, currently_targeted_weights)
currently_targeted_weights[key] = result
if result == trigger_result and roll_percentage(get_choice("percentage", option_set, 100)):
for category_name, category_options in option_set["options"].items():
currently_targeted_weights = weights
if category_name:
currently_targeted_weights = currently_targeted_weights[category_name]
update_weights(currently_targeted_weights, category_options, "Triggered", option_set["option_name"])
except Exception as e:
raise ValueError(f"Your trigger number {i + 1} is destroyed. "
f"Please fix your triggers.") from e
return weights
def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str:
if boss_shuffle in boss_shuffle_options:
return boss_shuffle_options[boss_shuffle]
elif "bosses" in plando_options:
options = boss_shuffle.lower().split(";")
remainder_shuffle = "none" # vanilla
bosses = []
for boss in options:
if boss in boss_shuffle_options:
remainder_shuffle = boss_shuffle_options[boss]
elif "-" in boss:
loc, boss_name = boss.split("-")
if boss_name not in available_boss_names:
raise ValueError(f"Unknown Boss name {boss_name}")
if loc not in available_boss_locations:
raise ValueError(f"Unknown Boss Location {loc}")
level = ''
if loc.split(" ")[-1] in {"top", "middle", "bottom"}:
# split off level
loc = loc.split(" ")
level = f" {loc[-1]}"
loc = " ".join(loc[:-1])
loc = loc.title().replace("Of", "of")
if not Bosses.can_place_boss(boss_name.title(), loc, level):
raise ValueError(f"Cannot place {boss_name} at {loc}{level}")
bosses.append(boss)
elif boss not in available_boss_names:
raise ValueError(f"Unknown Boss name or Boss shuffle option {boss}.")
else:
bosses.append(boss)
return ";".join(bosses + [remainder_shuffle])
else:
raise Exception(f"Boss Shuffle {boss_shuffle} is unknown and boss plando is turned off.")
def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option)):
if option_key in game_weights:
try:
if not option.supports_weighting:
player_option = option.from_any(game_weights[option_key])
else:
player_option = option.from_any(get_choice(option_key, game_weights))
setattr(ret, option_key, player_option)
except Exception as e:
raise Exception(f"Error generating option {option_key} in {ret.game}") from e
else:
if hasattr(player_option, "verify"):
player_option.verify(AutoWorldRegister.world_types[ret.game])
else:
setattr(ret, option_key, option(option.default))
def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("bosses",))):
if "linked_options" in weights:
weights = roll_linked_options(weights)
if "triggers" in weights:
weights = roll_triggers(weights, weights["triggers"])
requirements = weights.get("requires", {})
if requirements:
version = requirements.get("version", __version__)
if tuplize_version(version) > version_tuple:
raise Exception(f"Settings reports required version of generator is at least {version}, "
f"however generator is of version {__version__}")
required_plando_options = requirements.get("plando", "")
if required_plando_options:
required_plando_options = set(option.strip() for option in required_plando_options.split(","))
required_plando_options -= plando_options
if required_plando_options:
if len(required_plando_options) == 1:
raise Exception(f"Settings reports required plando module {', '.join(required_plando_options)}, "
f"which is not enabled.")
else:
raise Exception(f"Settings reports required plando modules {', '.join(required_plando_options)}, "
f"which are not enabled.")
ret = argparse.Namespace()
for option_key in Options.per_game_common_options:
if option_key in weights and option_key not in Options.common_options:
raise Exception(f"Option {option_key} has to be in a game's section, not on its own.")
ret.game = get_choice("game", weights)
if ret.game not in weights:
raise Exception(f"No game options for selected game \"{ret.game}\" found.")
world_type = AutoWorldRegister.world_types[ret.game]
game_weights = weights[ret.game]
if "triggers" in game_weights:
weights = roll_triggers(weights, game_weights["triggers"])
game_weights = weights[ret.game]
ret.name = get_choice('name', weights)
for option_key, option in Options.common_options.items():
setattr(ret, option_key, option.from_any(get_choice(option_key, weights, option.default)))
if ret.game in AutoWorldRegister.world_types:
for option_key, option in world_type.options.items():
handle_option(ret, game_weights, option_key, option)
for option_key, option in Options.per_game_common_options.items():
# skip setting this option if already set from common_options, defaulting to root option
if not (option_key in Options.common_options and option_key not in game_weights):
handle_option(ret, game_weights, option_key, option)
if "items" in plando_options:
ret.plando_items = game_weights.get("plando_items", [])
if ret.game == "Minecraft" or ret.game == "Ocarina of Time":
# bad hardcoded behavior to make this work for now
ret.plando_connections = []
if "connections" in plando_options:
options = game_weights.get("plando_connections", [])
for placement in options:
if roll_percentage(get_choice("percentage", placement, 100)):
ret.plando_connections.append(PlandoConnection(
get_choice("entrance", placement),
get_choice("exit", placement),
get_choice("direction", placement)
))
elif ret.game == "A Link to the Past":
roll_alttp_settings(ret, game_weights, plando_options)
else:
raise Exception(f"Unsupported game {ret.game}")
# not meant to stay here, intended to be removed when itemlinks are stable
if not "item_links" in plando_options:
ret.item_links.value = []
return ret
def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
if "dungeon_items" in weights and get_choice_legacy('dungeon_items', weights, "none") != "none":
raise Exception(f"dungeon_items key in A Link to the Past was removed, but is present in these weights as {get_choice_legacy('dungeon_items', weights, False)}.")
glitches_required = get_choice_legacy('glitches_required', weights)
if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'hybrid_major_glitches', 'minor_glitches']:
logging.warning("Only NMG, OWG, HMG and No Logic supported")
glitches_required = 'none'
ret.logic = {None: 'noglitches', 'none': 'noglitches', 'no_logic': 'nologic', 'overworld_glitches': 'owglitches',
'minor_glitches': 'minorglitches', 'hybrid_major_glitches': 'hybridglitches'}[
glitches_required]
ret.dark_room_logic = get_choice_legacy("dark_room_logic", weights, "lamp")
if not ret.dark_room_logic: # None/False
ret.dark_room_logic = "none"
if ret.dark_room_logic == "sconces":
ret.dark_room_logic = "torches"
if ret.dark_room_logic not in {"lamp", "torches", "none"}:
raise ValueError(f"Unknown Dark Room Logic: \"{ret.dark_room_logic}\"")
entrance_shuffle = get_choice_legacy('entrance_shuffle', weights, 'vanilla')
if entrance_shuffle.startswith('none-'):
ret.shuffle = 'vanilla'
else:
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
goal = get_choice_legacy('goals', weights, 'ganon')
ret.goal = goals[goal]
# TODO consider moving open_pyramid to an automatic variable in the core roller, set to True when
# fast ganon + ganon at hole
ret.open_pyramid = get_choice_legacy('open_pyramid', weights, 'goal')
extra_pieces = get_choice_legacy('triforce_pieces_mode', weights, 'available')
ret.triforce_pieces_required = LttPOptions.TriforcePieces.from_any(get_choice_legacy('triforce_pieces_required', weights, 20))
# sum a percentage to required
if extra_pieces == 'percentage':
percentage = max(100, float(get_choice_legacy('triforce_pieces_percentage', weights, 150))) / 100
ret.triforce_pieces_available = int(round(ret.triforce_pieces_required * percentage, 0))
# vanilla mode (specify how many pieces are)
elif extra_pieces == 'available':
ret.triforce_pieces_available = LttPOptions.TriforcePieces.from_any(
get_choice_legacy('triforce_pieces_available', weights, 30))
# required pieces + fixed extra
elif extra_pieces == 'extra':
extra_pieces = max(0, int(get_choice_legacy('triforce_pieces_extra', weights, 10)))
ret.triforce_pieces_available = ret.triforce_pieces_required + extra_pieces
# change minimum to required pieces to avoid problems
ret.triforce_pieces_available = min(max(ret.triforce_pieces_required, int(ret.triforce_pieces_available)), 90)
ret.shop_shuffle = get_choice_legacy('shop_shuffle', weights, '')
if not ret.shop_shuffle:
ret.shop_shuffle = ''
ret.mode = get_choice_legacy("mode", weights)
ret.difficulty = get_choice_legacy('item_pool', weights)
ret.item_functionality = get_choice_legacy('item_functionality', weights)
boss_shuffle = get_choice_legacy('boss_shuffle', weights)
ret.shufflebosses = get_plando_bosses(boss_shuffle, plando_options)
ret.enemy_damage = {None: 'default',
'default': 'default',
'shuffled': 'shuffled',
'random': 'chaos', # to be removed
'chaos': 'chaos',
}[get_choice_legacy('enemy_damage', weights)]
ret.enemy_health = get_choice_legacy('enemy_health', weights)
ret.timer = {'none': False,
None: False,
False: False,
'timed': 'timed',
'timed_ohko': 'timed-ohko',
'ohko': 'ohko',
'timed_countdown': 'timed-countdown',
'display': 'display'}[get_choice_legacy('timer', weights, False)]
ret.countdown_start_time = int(get_choice_legacy('countdown_start_time', weights, 10))
ret.red_clock_time = int(get_choice_legacy('red_clock_time', weights, -2))
ret.blue_clock_time = int(get_choice_legacy('blue_clock_time', weights, 2))
ret.green_clock_time = int(get_choice_legacy('green_clock_time', weights, 4))
ret.dungeon_counters = get_choice_legacy('dungeon_counters', weights, 'default')
ret.shuffle_prizes = get_choice_legacy('shuffle_prizes', weights, "g")
ret.required_medallions = [get_choice_legacy("misery_mire_medallion", weights, "random"),
get_choice_legacy("turtle_rock_medallion", weights, "random")]
for index, medallion in enumerate(ret.required_medallions):
ret.required_medallions[index] = {"ether": "Ether", "quake": "Quake", "bombos": "Bombos", "random": "random"} \
.get(medallion.lower(), None)
if not ret.required_medallions[index]:
raise Exception(f"unknown Medallion {medallion} for {'misery mire' if index == 0 else 'turtle rock'}")
ret.plando_texts = {}
if "texts" in plando_options:
tt = TextTable()
tt.removeUnwantedText()
options = weights.get("plando_texts", [])
for placement in options:
if roll_percentage(get_choice_legacy("percentage", placement, 100)):
at = str(get_choice_legacy("at", placement))
if at not in tt:
raise Exception(f"No text target \"{at}\" found.")
ret.plando_texts[at] = str(get_choice_legacy("text", placement))
ret.plando_connections = []
if "connections" in plando_options:
options = weights.get("plando_connections", [])
for placement in options:
if roll_percentage(get_choice_legacy("percentage", placement, 100)):
ret.plando_connections.append(PlandoConnection(
get_choice_legacy("entrance", placement),
get_choice_legacy("exit", placement),
get_choice_legacy("direction", placement, "both")
))
ret.sprite_pool = weights.get('sprite_pool', [])
ret.sprite = get_choice_legacy('sprite', weights, "Link")
if 'random_sprite_on_event' in weights:
randomoneventweights = weights['random_sprite_on_event']
if get_choice_legacy('enabled', randomoneventweights, False):
ret.sprite = 'randomon'
ret.sprite += '-hit' if get_choice_legacy('on_hit', randomoneventweights, True) else ''
ret.sprite += '-enter' if get_choice_legacy('on_enter', randomoneventweights, False) else ''
ret.sprite += '-exit' if get_choice_legacy('on_exit', randomoneventweights, False) else ''
ret.sprite += '-slash' if get_choice_legacy('on_slash', randomoneventweights, False) else ''
ret.sprite += '-item' if get_choice_legacy('on_item', randomoneventweights, False) else ''
ret.sprite += '-bonk' if get_choice_legacy('on_bonk', randomoneventweights, False) else ''
ret.sprite = 'randomonall' if get_choice_legacy('on_everything', randomoneventweights, False) else ret.sprite
ret.sprite = 'randomonnone' if ret.sprite == 'randomon' else ret.sprite
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.
for key, value in weights['sprite'].items():
if key.startswith('random'):
ret.sprite_pool += ['random'] * int(value)
else:
ret.sprite_pool += [key] * int(value)
if __name__ == '__main__':
import atexit
confirmation = atexit.register(input, "Press enter to close.")
main()
# in case of error-free exit should not need confirmation
atexit.unregister(confirmation)

1926
Gui.py

File diff suppressed because it is too large Load Diff

View File

@@ -1,192 +0,0 @@
import queue
import threading
import tkinter as tk
from Utils import local_path
def set_icon(window):
logo = tk.PhotoImage(file=local_path('data', 'icon.png'))
window.tk.call('wm', 'iconphoto', window._w, logo)
# Although tkinter is intended to be thread safe, there are many reports of issues
# some which may be platform specific, or depend on if the TCL library was compiled without
# multithreading support. Therefore I will assume it is not thread safe to avoid any possible problems
class BackgroundTask(object):
def __init__(self, window, code_to_run, *args):
self.window = window
self.queue = queue.Queue()
self.running = True
self.process_queue()
self.task = threading.Thread(target=code_to_run, args=(self, *args))
self.task.start()
def stop(self):
self.running = False
# safe to call from worker
def queue_event(self, event):
self.queue.put(event)
def process_queue(self):
try:
while True:
if not self.running:
return
event = self.queue.get_nowait()
event()
if self.running:
#if self is no longer running self.window may no longer be valid
self.window.update_idletasks()
except queue.Empty:
pass
if self.running:
self.window.after(100, self.process_queue)
class BackgroundTaskProgress(BackgroundTask):
def __init__(self, parent, code_to_run, title, *args):
self.parent = parent
self.window = tk.Toplevel(parent)
self.window['padx'] = 5
self.window['pady'] = 5
try:
self.window.attributes("-toolwindow", 1)
except tk.TclError:
pass
self.window.wm_title(title)
self.label_var = tk.StringVar()
self.label_var.set("")
self.label = tk.Label(self.window, textvariable=self.label_var, width=50)
self.label.pack()
self.window.resizable(width=False, height=False)
set_icon(self.window)
self.window.focus()
super().__init__(self.window, code_to_run, *args)
#safe to call from worker thread
def update_status(self, text):
self.queue_event(lambda: self.label_var.set(text))
# only call this in an event callback
def close_window(self):
self.stop()
self.window.destroy()
class ToolTips(object):
# This class derived from wckToolTips which is available under the following license:
# Copyright (c) 1998-2007 by Secret Labs AB
# Copyright (c) 1998-2007 by Fredrik Lundh
#
# By obtaining, using, and/or copying this software and/or its
# associated documentation, you agree that you have read, understood,
# and will comply with the following terms and conditions:
#
# Permission to use, copy, modify, and distribute this software and its
# associated documentation for any purpose and without fee is hereby
# granted, provided that the above copyright notice appears in all
# copies, and that both that copyright notice and this permission notice
# appear in supporting documentation, and that the name of Secret Labs
# AB or the author not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior
# permission.
#
# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO
# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
# FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
label = None
window = None
active = 0
tag = None
after_id = None
@classmethod
def getcontroller(cls, widget):
if cls.tag is None:
cls.tag = "ui_tooltip_%d" % id(cls)
widget.bind_class(cls.tag, "<Enter>", cls.enter)
widget.bind_class(cls.tag, "<Leave>", cls.leave)
widget.bind_class(cls.tag, "<Motion>", cls.motion)
widget.bind_class(cls.tag, "<Destroy>", cls.leave)
# pick suitable colors for tooltips
try:
cls.bg = "systeminfobackground"
cls.fg = "systeminfotext"
widget.winfo_rgb(cls.fg) # make sure system colors exist
widget.winfo_rgb(cls.bg)
except Exception:
cls.bg = "#ffffe0"
cls.fg = "black"
return cls.tag
@classmethod
def register(cls, widget, text):
widget.ui_tooltip_text = text
tags = list(widget.bindtags())
tags.append(cls.getcontroller(widget))
widget.bindtags(tuple(tags))
@classmethod
def unregister(cls, widget):
tags = list(widget.bindtags())
tags.remove(cls.getcontroller(widget))
widget.bindtags(tuple(tags))
# event handlers
@classmethod
def enter(cls, event):
widget = event.widget
if not cls.label:
# create and hide balloon help window
cls.popup = tk.Toplevel(bg=cls.fg, bd=1)
cls.popup.overrideredirect(1)
cls.popup.withdraw()
cls.label = tk.Label(
cls.popup, fg=cls.fg, bg=cls.bg, bd=0, padx=2, justify=tk.LEFT
)
cls.label.pack()
cls.active = 0
cls.xy = event.x_root + 16, event.y_root + 10
cls.event_xy = event.x, event.y
cls.after_id = widget.after(200, cls.display, widget)
@classmethod
def motion(cls, event):
cls.xy = event.x_root + 16, event.y_root + 10
cls.event_xy = event.x, event.y
@classmethod
def display(cls, widget):
if not cls.active:
# display balloon help window
text = widget.ui_tooltip_text
if callable(text):
text = text(widget, cls.event_xy)
cls.label.config(text=text)
cls.popup.deiconify()
cls.popup.lift()
cls.popup.geometry("+%d+%d" % cls.xy)
cls.active = 1
cls.after_id = None
@classmethod
def leave(cls, event):
widget = event.widget
if cls.active:
cls.popup.withdraw()
cls.active = 0
if cls.after_id:
widget.after_cancel(cls.after_id)
cls.after_id = None

View File

@@ -1,8 +1,8 @@
MIT License
Copyright (c) 2017 LLCoolDave
Copyright (c) 2021 Berserker66
Copyright (c) 2021 CaitSith2
Copyright (c) 2022 Berserker66
Copyright (c) 2022 CaitSith2
Copyright (c) 2021 LegendaryLinux
Permission is hereby granted, free of charge, to any person obtaining a copy

File diff suppressed because it is too large Load Diff

824
Main.py
View File

@@ -1,281 +1,210 @@
import copy
from itertools import zip_longest
import collections
from itertools import zip_longest, chain
import logging
import os
import random
import time
import zlib
import concurrent.futures
import pickle
from typing import Dict, Tuple
import tempfile
import zipfile
from typing import Dict, Tuple, Optional, Set
from BaseClasses import MultiWorld, CollectionState, Region, Item
from worlds.alttp.Items import ItemFactory, item_name_groups
from worlds.alttp.Regions import create_regions, mark_light_world_regions, \
lookup_vanilla_location_to_entrance
from worlds.alttp.InvertedRegions import create_inverted_regions, mark_dark_world_regions
from worlds.alttp.EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect
from worlds.alttp.Rom import patch_rom, patch_race_rom, patch_enemizer, apply_rom_settings, LocalRom, get_hash_string
from worlds.alttp.Rules import set_rules
from worlds.alttp.Dungeons import create_dungeons, fill_dungeons, fill_dungeons_restrictive
from BaseClasses import MultiWorld, CollectionState, Region, RegionType, LocationProgressType, Location
from worlds.alttp.Items import item_name_groups
from worlds.alttp.Regions import lookup_vanilla_location_to_entrance
from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned
from worlds.alttp.Shops import create_shops, ShopSlotFill, SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
from worlds.alttp.ItemPool import generate_itempool, difficulties, fill_prizes
from Utils import output_path, parse_player_names, get_options, __version__, _version_tuple
from worlds.hk import gen_hollow
from worlds.hk import create_regions as hk_create_regions
from worlds.factorio import gen_factorio, factorio_create_regions
from worlds.factorio.Mod import generate_mod
from worlds.minecraft import gen_minecraft, fill_minecraft_slot_data, generate_mc_data
from worlds.minecraft.Regions import minecraft_create_regions
from worlds.generic.Rules import locality_rules
from worlds import Games, lookup_any_item_name_to_id
import Patch
from worlds.alttp.Shops import SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
from Utils import output_path, get_options, __version__, version_tuple
from worlds.generic.Rules import locality_rules, exclusion_rules
from worlds import AutoWorld
seeddigits = 20
ordered_areas = (
'Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace',
'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace',
'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total"
)
def get_seed(seed=None):
if seed is None:
random.seed(None)
return random.randint(0, pow(10, seeddigits) - 1)
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, baked_server_options: Optional[Dict[str, object]] = None):
if not baked_server_options:
baked_server_options = get_options()["server_options"]
if args.outputpath:
os.makedirs(args.outputpath, exist_ok=True)
output_path.cached_path = args.outputpath
start = time.perf_counter()
# initialize the world
world = MultiWorld(args.multi)
logger = logging.getLogger('')
world.seed = get_seed(seed)
if args.race:
world.secure()
else:
world.random.seed(world.seed)
world.seed_name = str(args.outputname if args.outputname else world.seed)
logger = logging.getLogger()
world.set_seed(seed, args.race, str(args.outputname if args.outputname else world.seed))
world.shuffle = args.shuffle.copy()
world.logic = args.logic.copy()
world.mode = args.mode.copy()
world.swordless = args.swordless.copy()
world.difficulty = args.difficulty.copy()
world.item_functionality = args.item_functionality.copy()
world.timer = args.timer.copy()
world.progressive = args.progressive.copy()
world.goal = args.goal.copy()
world.local_items = args.local_items.copy()
if hasattr(args, "algorithm"): # current GUI options
world.algorithm = args.algorithm
world.shuffleganon = args.shuffleganon
world.custom = args.custom
world.customitemarray = args.customitemarray
world.accessibility = args.accessibility.copy()
world.retro = args.retro.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.crystals_needed_for_ganon = {
player: world.random.randint(0, 7) if args.crystals_ganon[player] == 'random' else int(
args.crystals_ganon[player]) for player in range(1, world.players + 1)}
world.crystals_needed_for_gt = {
player: world.random.randint(0, 7) if args.crystals_gt[player] == 'random' else int(args.crystals_gt[player])
for player in range(1, world.players + 1)}
world.open_pyramid = args.open_pyramid.copy()
world.boss_shuffle = args.shufflebosses.copy()
world.enemy_shuffle = args.enemy_shuffle.copy()
world.enemy_health = args.enemy_health.copy()
world.enemy_damage = args.enemy_damage.copy()
world.killable_thieves = args.killable_thieves.copy()
world.bush_shuffle = args.bush_shuffle.copy()
world.tile_shuffle = args.tile_shuffle.copy()
world.beemizer = args.beemizer.copy()
world.beemizer_total_chance = args.beemizer_total_chance.copy()
world.beemizer_trap_chance = args.beemizer_trap_chance.copy()
world.timer = args.timer.copy()
world.countdown_start_time = args.countdown_start_time.copy()
world.red_clock_time = args.red_clock_time.copy()
world.blue_clock_time = args.blue_clock_time.copy()
world.green_clock_time = args.green_clock_time.copy()
world.shufflepots = args.shufflepots.copy()
world.progressive = args.progressive.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_required = args.triforce_pieces_required.copy()
world.shop_shuffle = args.shop_shuffle.copy()
world.shop_shuffle_slots = args.shop_shuffle_slots.copy()
world.progression_balancing = args.progression_balancing.copy()
world.shuffle_prizes = args.shuffle_prizes.copy()
world.sprite_pool = args.sprite_pool.copy()
world.dark_room_logic = args.dark_room_logic.copy()
world.plando_items = args.plando_items.copy()
world.plando_texts = args.plando_texts.copy()
world.plando_connections = args.plando_connections.copy()
world.er_seeds = getattr(args, "er_seeds", {})
world.restrict_dungeon_item_on_boss = args.restrict_dungeon_item_on_boss.copy()
world.required_medallions = args.required_medallions.copy()
world.game = args.game.copy()
import Options
for hk_option in Options.hollow_knight_options:
setattr(world, hk_option, getattr(args, hk_option, {}))
for factorio_option in Options.factorio_options:
setattr(world, factorio_option, getattr(args, factorio_option, {}))
for minecraft_option in Options.minecraft_options:
setattr(world, minecraft_option, getattr(args, minecraft_option, {}))
world.player_name = args.name.copy()
world.enemizer = args.enemizercli
world.sprite = args.sprite.copy()
world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
world.rom_seeds = {player: random.Random(world.random.randint(0, 999999999)) for player in range(1, world.players + 1)}
for player in range(1, world.players+1):
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:
# renamed from team to group to not confuse with existing team name use
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"
world.set_options(args)
world.set_item_links()
world.state = CollectionState(world)
logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed)
parsed_names = parse_player_names(args.names, world.players, args.teams)
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("Found 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():
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}}")
AutoWorld.call_all(world, "generate_early")
logger.info('')
for player in world.alttp_player_ids:
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
for player in world.player_ids:
for item_name in args.startinventory[player]:
item = Item(item_name, True, lookup_any_item_name_to_id[item_name], player)
item.game = world.game[player]
world.push_precollected(item)
for item_name, count in world.start_inventory[player].value.items():
for _ in range(count):
world.push_precollected(world.create_item(item_name, player))
for player in world.player_ids:
if player in world.get_game_players("A Link to the Past"):
# enforce pre-defined local items.
if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]:
world.local_items[player].value.add('Triforce Piece')
# enforce pre-defined local items.
if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]:
world.local_items[player].add('Triforce Piece')
# Not possible to place pendants/crystals out side of boss prizes yet.
world.non_local_items[player].value -= item_name_groups['Pendants']
world.non_local_items[player].value -= item_name_groups['Crystals']
# items can't be both local and non-local, prefer local
world.non_local_items[player] -= world.local_items[player]
world.non_local_items[player].value -= world.local_items[player].value
# 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']
logger.info('Creating World.')
AutoWorld.call_all(world, "create_regions")
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']
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.
world.non_local_items[player] -= item_name_groups['Pendants']
world.non_local_items[player] -= item_name_groups['Crystals']
for player in world.hk_player_ids:
hk_create_regions(world, player)
for player in world.factorio_player_ids:
factorio_create_regions(world, player)
for player in world.minecraft_player_ids:
minecraft_create_regions(world, player)
for player in world.alttp_player_ids:
if world.open_pyramid[player] == 'goal':
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'}
elif world.open_pyramid[player] == 'auto':
world.open_pyramid[player] = world.goal[player] in {'crystals', 'ganontriforcehunt', 'localganontriforcehunt', 'ganonpedestal'} and \
(world.shuffle[player] in {'vanilla', 'dungeonssimple', 'dungeonsfull', 'dungeonscrossed'} or not world.shuffle_ganon)
else:
world.open_pyramid[player] = {'on': True, 'off': False, 'yes': True, 'no': False}.get(world.open_pyramid[player], world.open_pyramid[player])
world.triforce_pieces_available[player] = max(world.triforce_pieces_available[player], world.triforce_pieces_required[player])
if world.mode[player] != 'inverted':
create_regions(world, player)
else:
create_inverted_regions(world, player)
create_shops(world, player)
create_dungeons(world, player)
logger.info('Shuffling the World about.')
for player in world.alttp_player_ids:
if world.logic[player] not in ["noglitches", "minorglitches"] and world.shuffle[player] in \
{"vanilla", "dungeonssimple", "dungeonsfull", "simple", "restricted", "full"}:
world.fix_fake_world[player] = False
# seeded entrance shuffle
old_random = world.random
world.random = random.Random(world.er_seeds[player])
if world.mode[player] != 'inverted':
link_entrances(world, player)
mark_light_world_regions(world, player)
else:
link_inverted_entrances(world, player)
mark_dark_world_regions(world, player)
world.random = old_random
plando_connect(world, player)
logger.info('Generating Item Pool.')
for player in world.alttp_player_ids:
generate_itempool(world, player)
logger.info('Creating Items.')
AutoWorld.call_all(world, "create_items")
logger.info('Calculating Access Rules.')
if world.players > 1:
for player in world.player_ids:
locality_rules(world, player)
else:
world.non_local_items[1].value = set()
world.local_items[1].value = set()
for player in world.alttp_player_ids:
set_rules(world, player)
AutoWorld.call_all(world, "set_rules")
for player in world.hk_player_ids:
gen_hollow(world, player)
for player in world.player_ids:
exclusion_rules(world, player, world.exclude_locations[player].value)
world.priority_locations[player].value -= world.exclude_locations[player].value
for location_name in world.priority_locations[player].value:
world.get_location(location_name, player).progress_type = LocationProgressType.PRIORITY
for player in world.factorio_player_ids:
gen_factorio(world, player)
AutoWorld.call_all(world, "generate_basic")
for player in world.minecraft_player_ids:
gen_minecraft(world, player)
# temporary home for item links, should be moved out of Main
for group_id, group in world.groups.items():
# TODO: remove when LttP options are transitioned over
world.difficulty_requirements[group_id] = world.difficulty_requirements[next(iter(group["players"]))]
def find_common_pool(players: Set[int], shared_pool: Set[str]):
advancement = set()
counters = {player: {name: 0 for name in shared_pool} for player in players}
for item in world.itempool:
if item.player in counters and item.name in shared_pool:
counters[item.player][item.name] += 1
if item.advancement:
advancement.add(item.name)
for item in shared_pool:
count = min(counters[player][item] for player in players)
if count:
for player in players:
counters[player][item] = count
else:
for player in players:
del(counters[player][item])
return counters, advancement
common_item_count, common_advancement_items = find_common_pool(group["players"], group["item_pool"])
# TODO: fix logic
if common_advancement_items:
logger.warning(f"Logical requirements for {', '.join(common_advancement_items)} in group {group['name']} "
f"will be incorrect.")
new_itempool = []
for item_name, item_count in next(iter(common_item_count.values())).items():
advancement = item_name in common_advancement_items
for _ in range(item_count):
new_item = group["world"].create_item(item_name)
new_item.advancement = advancement
new_itempool.append(new_item)
region = Region("Menu", RegionType.Generic, "ItemLink", group_id, world)
world.regions.append(region)
locations = region.locations = []
for item in world.itempool:
count = common_item_count.get(item.player, {}).get(item.name, 0)
if count:
loc = Location(group_id, f"Item Link: {item.name} -> {world.player_name[item.player]} {count}",
None, region)
loc.access_rule = lambda state: state.has(item.name, group_id, count)
locations.append(loc)
loc.place_locked_item(item)
common_item_count[item.player][item.name] -= 1
else:
new_itempool.append(item)
itemcount = len(world.itempool)
world.itempool = new_itempool
# can produce more items than were removed
while itemcount > len(world.itempool):
for player in group["players"]:
if group["replacement_items"][player]:
world.itempool.append(AutoWorld.call_single(world, "create_item", player,
group["replacement_items"][player]))
else:
AutoWorld.call_single(world, "create_filler", player)
if any(world.item_links.values()):
world._recache()
world._all_state = None
logger.info("Running Item Plando")
@@ -284,310 +213,209 @@ def main(args, seed=None):
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(f'Filling the world with {len(world.itempool)} items.')
if world.algorithm == 'flood':
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':
distribute_items_restrictive(world, True)
distribute_items_restrictive(world)
logger.info("Filling Shop Slots")
ShopSlotFill(world)
AutoWorld.call_all(world, 'post_fill')
if world.players > 1:
balance_multiworld_progression(world)
logger.info('Generating output files.')
logger.info(f'Beginning output...')
outfilebase = 'AP_' + world.seed_name
rom_names = []
def _gen_rom(team: int, player: int):
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])
output = tempfile.TemporaryDirectory()
with output as temp_dir:
with concurrent.futures.ThreadPoolExecutor(world.players + 2) as pool:
check_accessibility_task = pool.submit(world.fulfills_accessibility)
rom = LocalRom(args.rom)
output_file_futures = [pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir)]
for player in world.player_ids:
# skip starting a thread for methods that say "pass".
if AutoWorld.World.generate_output.__code__ is not world.worlds[player].generate_output.__code__:
output_file_futures.append(
pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
patch_rom(world, rom, player, team, use_enemizer)
def get_entrance_to_region(region: Region):
for entrance in region.entrances:
if entrance.parent_region.type in (RegionType.DarkWorld, RegionType.LightWorld, RegionType.Generic):
return entrance
for entrance in region.entrances: # BFS might be better here, trying DFS for now.
return get_entrance_to_region(entrance.parent_region)
if use_enemizer:
patch_enemizer(world, team, player, rom, args.enemizercli)
# collect ER hint info
er_hint_data = {player: {} for player in world.get_game_players("A Link to the Past") if
world.shuffle[player] != "vanilla" or world.retro[player]}
if args.race:
patch_race_rom(rom, world, player)
for region in world.regions:
if region.player in er_hint_data and region.locations:
main_entrance = get_entrance_to_region(region)
for location in region.locations:
if type(location.address) == int: # skips events and crystals
if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
er_hint_data[region.player][location.address] = main_entrance.name
world.spoiler.hashes[(player, team)] = get_hash_string(rom.hash)
checks_in_area = {player: {area: list() for area in ordered_areas}
for player in range(1, world.players + 1)}
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]
for player in range(1, world.players + 1):
checks_in_area[player]["Total"] = 0
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])
for location in world.get_filled_locations():
if type(location.address) is int:
main_entrance = get_entrance_to_region(location.parent_region)
if location.game != "A Link to the Past":
checks_in_area[location.player]["Light World"].append(location.address)
elif location.parent_region.dungeon:
dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
'Inverted Ganons Tower': 'Ganons Tower'} \
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
checks_in_area[location.player][dungeonname].append(location.address)
elif location.parent_region.type == RegionType.LightWorld:
checks_in_area[location.player]["Light World"].append(location.address)
elif location.parent_region.type == RegionType.DarkWorld:
checks_in_area[location.player]["Dark World"].append(location.address)
elif main_entrance.parent_region.type == RegionType.LightWorld:
checks_in_area[location.player]["Light World"].append(location.address)
elif main_entrance.parent_region.type == RegionType.DarkWorld:
checks_in_area[location.player]["Dark World"].append(location.address)
checks_in_area[location.player]["Total"] += 1
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 '')
oldmancaves = []
takeanyregions = ["Old Man Sword Cave", "Take-Any #1", "Take-Any #2", "Take-Any #3", "Take-Any #4"]
for index, take_any in enumerate(takeanyregions):
for region in [world.get_region(take_any, player) for player in
world.get_game_players("A Link to the Past") 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
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
main_entrance = get_entrance_to_region(region)
if main_entrance.parent_region.type == RegionType.LightWorld:
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
outfilestuffs["difficulty"], # 1
outfilestuffs["item_functionality"], # 2
outfilestuffs["mode"], # 3
outfilestuffs["goal"], # 4
"" if outfilestuffs["timer"] in ['False', 'none', 'display'] else "-" + outfilestuffs["timer"], # 5
er_hint_data[player][location_id] = main_entrance.name
oldmancaves.append(((location_id, player), (item.code, player)))
outfilestuffs["shuffle"], # 6
outfilestuffs["algorithm"], # 7
outfilestuffs["mscb"], # 8
FillDisabledShopSlots(world)
"-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 = output_path(f'{outfilebase}{outfilepname}{outfilesuffix}.sfc')
rom.write_to_file(rompath, hide_enemizer=True)
if args.create_diff:
Patch.create_patch_file(rompath, player=player, player_name = world.player_names[player][team])
return player, team, bytes(rom.name)
def write_multidata():
import NetUtils
slot_data = {}
client_versions = {}
games = {}
minimum_versions = {"server": (0, 2, 4), "clients": client_versions}
slot_info = {}
names = [[name for player, name in sorted(world.player_name.items())]]
for slot in world.player_ids:
client_versions[slot] = world.worlds[slot].get_required_client_version()
games[slot] = world.game[slot]
slot_info[slot] = NetUtils.NetworkSlot(names[0][slot - 1], world.game[slot],
world.player_types[slot])
for slot, group in world.groups.items():
games[slot] = world.game[slot]
slot_info[slot] = NetUtils.NetworkSlot(group["name"], world.game[slot], world.player_types[slot],
group_members=sorted(group["players"]))
precollected_items = {player: [item.code for item in world_precollected]
for player, world_precollected in world.precollected_items.items()}
precollected_hints = {player: set() for player in range(1, world.players + 1 + len(world.groups))}
pool = concurrent.futures.ThreadPoolExecutor()
sending_visible_players = set()
check_accessibility_task = pool.submit(world.fulfills_accessibility)
for slot in world.player_ids:
slot_data[slot] = world.worlds[slot].fill_slot_data()
if world.worlds[slot].sending_visible:
sending_visible_players.add(slot)
rom_futures = []
mod_futures = []
for team in range(world.teams):
for player in world.alttp_player_ids:
rom_futures.append(pool.submit(_gen_rom, team, player))
for player in world.factorio_player_ids:
mod_futures.append(pool.submit(generate_mod, world, player))
def get_entrance_to_region(region: Region):
for entrance in region.entrances:
if entrance.parent_region.type in (RegionType.DarkWorld, RegionType.LightWorld):
return entrance
for entrance in region.entrances: # BFS might be better here, trying DFS for now.
return get_entrance_to_region(entrance.parent_region)
# collect ER hint info
er_hint_data = {player: {} for player in range(1, world.players + 1) if world.shuffle[player] != "vanilla" or world.retro[player]}
from worlds.alttp.Regions import RegionType
for region in world.regions:
if region.player in er_hint_data and region.locations:
main_entrance = get_entrance_to_region(region)
for location in region.locations:
if type(location.address) == int: # skips events and crystals
if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
er_hint_data[region.player][location.address] = main_entrance.name
ordered_areas = ('Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace',
'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace',
'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total")
checks_in_area = {player: {area: list() for area in ordered_areas}
for player in range(1, world.players + 1)}
for player in range(1, world.players + 1):
checks_in_area[player]["Total"] = 0
for location in [loc for loc in world.get_filled_locations() if type(loc.address) is int]:
main_entrance = get_entrance_to_region(location.parent_region)
if location.game != Games.LTTP:
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 = []
takeanyregions = ["Old Man Sword Cave", "Take-Any #1", "Take-Any #2", "Take-Any #3", "Take-Any #4"]
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 = ItemFactory(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)
if main_entrance.parent_region.type == RegionType.LightWorld:
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, mods):
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:
if world.game[slot] == "Factorio":
client_versions[slot] = (0, 1, 2)
else:
client_versions[slot] = (0, 0, 3)
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 visibility
sending_visible_players = set()
for player in world.factorio_player_ids:
if world.visibility[player]:
sending_visible_players.add(player)
for i, team in enumerate(parsed_names):
for player, name in enumerate(team, 1):
if player not in world.alttp_player_ids:
connect_names[name] = (i, player)
for slot in world.hk_player_ids:
slots_data = slot_data[slot] = {}
for option_name in Options.hollow_knight_options:
option = getattr(world, option_name)[slot]
slots_data[option_name] = int(option.value)
for slot in world.minecraft_player_ids:
slot_data[slot] = fill_minecraft_slot_data(world, slot)
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:
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:
def precollect_hint(location):
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, ""))
location.item.code, False, "", location.item.flags)
precollected_hints[location.player].add(hint)
precollected_hints[location.item.player].add(hint)
multidata = zlib.compress(pickle.dumps({
"slot_data" : slot_data,
"games": games,
"names": parsed_names,
"connect_names": connect_names,
"remote_items": {player for player in range(1, world.players + 1) if
world.remote_items[player]},
"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
}), 9)
locations_data: Dict[int, Dict[int, Tuple[int, 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, location.item.flags
if location.player in sending_visible_players:
precollect_hint(location)
elif location.name in world.start_location_hints[location.player]:
precollect_hint(location)
elif location.item.name in world.start_hints[location.item.player]:
precollect_hint(location)
with open(output_path('%s.archipelago' % outfilebase), 'wb') as f:
f.write(bytes([1])) # version of format
f.write(multidata)
for future in mods:
future.result() # collect errors if they occured
multidata = {
"slot_data": slot_data,
"slot_info": slot_info,
"names": names, # TODO: remove around 0.2.5 in favor of slot_info
"games": games, # TODO: remove around 0.2.5 in favor of slot_info
"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},
"remote_start_inventory": {player for player in world.player_ids if
world.worlds[player].remote_start_inventory},
"locations": locations_data,
"checks_in_area": checks_in_area,
"server_options": baked_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_task = pool.submit(write_multidata, rom_futures, mod_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
for player in world.minecraft_player_ids: # Doing this after shutdown prevents the .apmc from being generated if there's an error
generate_mc_data(world, player)
if not args.skip_playthrough:
logger.info('Calculating playthrough.')
create_playthrough(world)
if args.create_spoiler: # needs spoiler.hashes to be filled, that depend on rom_futures being done
world.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase))
multidata = zlib.compress(pickle.dumps(multidata), 9)
with open(os.path.join(temp_dir, f'{outfilebase}.archipelago'), 'wb') as f:
f.write(bytes([3])) # 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 occurred.
multidata_task.result()
for i, future in enumerate(concurrent.futures.as_completed(output_file_futures), start=1):
if i % 10 == 0 or i == len(output_file_futures):
logger.info(f'Generating output files ({i}/{len(output_file_futures)}).')
future.result()
if args.spoiler > 1:
logger.info('Calculating playthrough.')
create_playthrough(world)
if args.spoiler:
world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase))
zipfilename = output_path(f"AP_{world.seed_name}.zip")
logger.info(f'Creating final archive at {zipfilename}.')
with zipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED,
compresslevel=9) as zf:
for file in os.scandir(temp_dir):
zf.write(file.path, arcname=file.name)
logger.info('Done. Enjoy. Total Time: %s', time.perf_counter() - start)
return world
@@ -603,9 +431,9 @@ def create_playthrough(world):
sphere_candidates = set(prog_locations)
logging.debug('Building up collection spheres.')
while sphere_candidates:
state.sweep_for_events(key_only=True)
# build up spheres of collection radius. Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
# build up spheres of collection radius.
# Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
sphere = {location for location in sphere_candidates if state.can_reach(location)}
@@ -622,7 +450,7 @@ def create_playthrough(world):
logging.debug('The following items could not be reached: %s', ['%s (Player %d) at %s (Player %d)' % (
location.item.name, location.item.player, location.name, location.player) for location in
sphere_candidates])
if any([world.accessibility[location.item.player] != 'none' for location in sphere_candidates]):
if any([world.accessibility[location.item.player] != 'minimal' for location in sphere_candidates]):
raise RuntimeError(f'Not all progression items reachable ({sphere_candidates}). '
f'Something went terribly wrong here.')
else:
@@ -636,7 +464,8 @@ def create_playthrough(world):
to_delete = set()
for location in sphere:
# we remove the item at location and check if game is still beatable
logging.debug('Checking if %s (Player %d) is required to beat the game.', location.item.name, location.item.player)
logging.debug('Checking if %s (Player %d) is required to beat the game.', location.item.name,
location.item.player)
old_item = location.item
location.item = None
if world.can_beat_game(state_cache[num]):
@@ -651,9 +480,9 @@ def create_playthrough(world):
# second phase, sphere 0
removed_precollected = []
for item in (i for i in world.precollected_items if i.advancement):
for item in (i for i in chain.from_iterable(world.precollected_items.values()) if i.advancement):
logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
world.precollected_items.remove(item)
world.precollected_items[item.player].remove(item)
world.state.remove(item)
if not world.can_beat_game():
world.push_precollected(item)
@@ -681,7 +510,8 @@ def create_playthrough(world):
collection_spheres.append(sphere)
logging.debug('Calculated final sphere %i, containing %i of %i progress items.', len(collection_spheres), len(sphere), len(required_locations))
logging.debug('Calculated final sphere %i, containing %i of %i progress items.', len(collection_spheres),
len(sphere), len(required_locations))
if not sphere:
raise RuntimeError(f'Not all required items reachable. Unreachable locations: {required_locations}')
@@ -698,19 +528,27 @@ def create_playthrough(world):
pathpairs = zip_longest(pathsiter, pathsiter)
return list(pathpairs)
world.spoiler.paths = dict()
for player in range(1, world.players + 1):
world.spoiler.paths.update({ str(location) : get_path(state, location.parent_region) for sphere in collection_spheres for location in sphere if location.player == player})
if player in world.alttp_player_ids:
for path in dict(world.spoiler.paths).values():
if any(exit == 'Pyramid Fairy' for (_, exit) in path):
if world.mode[player] != 'inverted':
world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = get_path(state, world.get_region('Big Bomb Shop', player))
else:
world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = get_path(state, world.get_region('Inverted Big Bomb Shop', player))
world.spoiler.paths = {}
topology_worlds = (player for player in world.player_ids if world.worlds[player].topology_present)
for player in topology_worlds:
world.spoiler.paths.update(
{str(location): get_path(state, location.parent_region) for sphere in collection_spheres for location in
sphere if location.player == player})
if player in world.get_game_players("A Link to the Past"):
# If Pyramid Fairy Entrance needs to be reached, also path to Big Bomb Shop
# Maybe move the big bomb over to the Event system instead?
if any(exit_path == 'Pyramid Fairy' for path in world.spoiler.paths.values() for (_, exit_path) in path):
if world.mode[player] != 'inverted':
world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = \
get_path(state, world.get_region('Big Bomb Shop', player))
else:
world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = \
get_path(state, world.get_region('Inverted Big Bomb Shop', player))
# 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
chain.from_iterable(world.precollected_items.values())
if item.advancement])}
for i, sphere in enumerate(collection_spheres):
world.spoiler.playthrough[str(i + 1)] = {str(location): str(location.item) for location in sorted(sphere)}

271
MinecraftClient.py Normal file
View File

@@ -0,0 +1,271 @@
import argparse
import os, sys
import re
import atexit
from subprocess import Popen
from shutil import copyfile
from time import strftime
import logging
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]?$")
forge_version = "1.17.1-37.1.1"
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".')
# 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):
for entry in os.scandir(mods_dir):
if entry.name.startswith("aprandomizer") and entry.name.endswith(".jar"):
logging.info(f"Found AP randomizer mod: {entry.name}")
return entry.name
return None
else:
os.mkdir(mods_dir)
logging.info(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)
logging.info(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)
logging.info(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)))
logging.info(f"Copied {os.path.basename(apmc_file)} to {apdata_dir}")
def read_apmc_file(apmc_file):
from base64 import b64decode
import json
with open(apmc_file, 'r') as f:
data = json.loads(b64decode(f.read()))
return data
# Check mod version, download new mod from GitHub releases page if needed.
def update_mod(forge_dir, apmc_file, get_prereleases=False):
ap_randomizer = find_ap_randomizer_jar(forge_dir)
if apmc_file is not None:
data = read_apmc_file(apmc_file)
minecraft_version = data.get('minecraft_version', '')
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
try:
latest_release = next(filter(lambda release: (not release['prerelease'] or get_prereleases) and
(apmc_file is None or minecraft_version in release['assets'][0]['name']),
resp.json()))
if ap_randomizer != latest_release['assets'][0]['name']:
logging.info(f"A new release of the Minecraft AP randomizer mod was found: "
f"{latest_release['assets'][0]['name']}")
if ap_randomizer is not None:
logging.info(f"Your current mod is {ap_randomizer}.")
else:
logging.info(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'])
logging.info("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)
logging.info(f"Wrote new mod file to {new_ap_mod}")
if old_ap_mod is not None:
os.remove(old_ap_mod)
logging.info(f"Removed old mod file from {old_ap_mod}")
else:
logging.error(f"Error retrieving the randomizer mod (status code {apmod_resp.status_code}).")
logging.error(f"Please report this issue on the Archipelago Discord server.")
sys.exit(1)
except StopIteration:
logging.warning(f"No compatible mod version found for {minecraft_version}.")
if not prompt_yes_no("Run server anyway?"):
sys.exit(0)
else:
logging.error(f"Error checking for randomizer mod updates (status code {resp.status_code}).")
logging.error(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
logging.info("You need to agree to the Minecraft EULA in order to run the server.")
logging.info("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()
logging.info(f"Set {eula_path} to true")
else:
sys.exit(0)
# get the current JDK16
def find_jdk_dir() -> str:
for entry in os.listdir():
if os.path.isdir(entry) and entry.startswith("jdk16"):
return os.path.abspath(entry)
# get the java exe location
def find_jdk() -> str:
jdk = find_jdk_dir()
jdk_exe = os.path.join(jdk, "bin", "java.exe")
if os.path.isfile(jdk_exe):
return jdk_exe
# Download Corretto 16 (Amazon JDK)
def download_java():
jdk = find_jdk_dir()
if jdk is not None:
print(f"Removing old JDK...")
from shutil import rmtree
rmtree(jdk)
print(f"Downloading Java...")
jdk_url = "https://corretto.aws/downloads/latest/amazon-corretto-16-x64-windows-jdk.zip"
resp = requests.get(jdk_url)
if resp.status_code == 200: # OK
print(f"Extracting...")
import zipfile
from io import BytesIO
with zipfile.ZipFile(BytesIO(resp.content)) as zf:
zf.extractall()
else:
print(f"Error downloading Java (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)
# download and install forge
def install_forge(directory: str):
jdk = find_jdk()
if jdk is not None:
print(f"Downloading Forge {forge_version}...")
forge_url = f"https://maven.minecraftforge.net/net/minecraftforge/forge/{forge_version}/forge-{forge_version}-installer.jar"
resp = requests.get(forge_url)
if resp.status_code == 200: # OK
forge_install_jar = os.path.join(directory, "forge_install.jar")
if not os.path.exists(directory):
os.mkdir(directory)
with open(forge_install_jar, 'wb') as f:
f.write(resp.content)
print(f"Installing Forge...")
argstring = ' '.join([jdk, "-jar", "\"" + forge_install_jar+ "\"", "--installServer", "\"" + directory + "\""])
install_process = Popen(argstring)
install_process.wait()
os.remove(forge_install_jar)
# Run the Forge server. Return process object
def run_forge_server(forge_dir: str, heap_arg):
java_exe = find_jdk()
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
args_file = os.path.join(forge_dir, "libraries", "net", "minecraftforge", "forge", forge_version, "win_args.txt")
win_args = []
with open(args_file) as argfile:
for line in argfile:
win_args.append(line.strip())
argstring = ' '.join([java_exe, heap_arg] + win_args + ["-nogui"])
logging.info(f"Running Forge server: {argstring}")
os.chdir(forge_dir)
return Popen(argstring)
if __name__ == '__main__':
Utils.init_logging("MinecraftClient")
parser = argparse.ArgumentParser()
parser.add_argument("apmc_file", default=None, nargs='?', help="Path to an Archipelago Minecraft data file (.apmc)")
parser.add_argument('--install', '-i', dest='install', default=False, action='store_true',
help="Download and install Java and the Forge server. Does not launch the client afterwards.")
parser.add_argument('--prerelease', default=False, action='store_true',
help="Auto-update prerelease versions.")
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 args.install:
print("Installing Java and Minecraft Forge")
download_java()
install_forge(forge_dir)
sys.exit(0)
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, apmc_file, args.prerelease)
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,27 +23,44 @@ def update_command():
subprocess.call([sys.executable, '-m', 'pip', 'install', '-r', file, '--upgrade'])
def update():
def update(yes=False, force=False):
global update_ran
if not update_ran:
update_ran = True
if force:
update_command()
return
for req_file in requirements_files:
path = os.path.join(os.path.dirname(sys.argv[0]), req_file)
if not os.path.exists(path):
path = os.path.join(os.path.dirname(__file__), req_file)
with open(path) as requirementsfile:
requirements = pkg_resources.parse_requirements(requirementsfile)
for requirement in requirements:
requirement = str(requirement)
try:
pkg_resources.require(requirement)
except pkg_resources.ResolutionError:
import traceback
traceback.print_exc()
input(f'Requirement {requirement} is not satisfied, press enter to install it')
update_command()
return
for line in requirementsfile:
if line.startswith('https://'):
# extract name and version from url
wheel = line.split('/')[-1]
name, version, _ = wheel.split('-', 2)
line = f'{name}=={version}'
requirements = pkg_resources.parse_requirements(line)
for requirement in requirements:
requirement = str(requirement)
try:
pkg_resources.require(requirement)
except pkg_resources.ResolutionError:
if not yes:
import traceback
traceback.print_exc()
input(f'Requirement {requirement} is not satisfied, press enter to install it')
update_command()
return
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

@@ -1,222 +0,0 @@
import os
import subprocess
import sys
import threading
import concurrent.futures
import argparse
import logging
import random
def feedback(text: str):
logging.info(text)
input("Press Enter to ignore and probably crash.")
if __name__ == "__main__":
logging.basicConfig(format='%(message)s', level=logging.INFO)
try:
import ModuleUpdate
ModuleUpdate.update()
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('--disable_autohost', action='store_true')
args = parser.parse_args()
from Utils import get_public_ipv4, get_options
from Mystery import get_seed_name
options = get_options()
multi_mystery_options = options["multi_mystery_options"]
output_path = options["general_options"]["output_path"]
enemizer_path = multi_mystery_options["enemizer_path"]
player_files_path = multi_mystery_options["player_files_path"]
target_player_count = multi_mystery_options["players"]
glitch_triforce = multi_mystery_options["glitch_triforce_room"]
race = multi_mystery_options["race"]
plando_options = multi_mystery_options["plando_options"]
create_spoiler = multi_mystery_options["create_spoiler"]
zip_roms = multi_mystery_options["zip_roms"]
zip_diffs = multi_mystery_options["zip_diffs"]
zip_apmcs = multi_mystery_options["zip_apmcs"]
zip_spoiler = multi_mystery_options["zip_spoiler"]
zip_multidata = multi_mystery_options["zip_multidata"]
zip_format = multi_mystery_options["zip_format"]
# zip_password = multi_mystery_options["zip_password"] not at this time
meta_file_path = multi_mystery_options["meta_file_path"]
weights_file_path = multi_mystery_options["weights_file_path"]
pre_roll = multi_mystery_options["pre_roll"]
teams = multi_mystery_options["teams"]
rom_file = options["lttp_options"]["rom_file"]
host = options["server_options"]["host"]
port = options["server_options"]["port"]
py_version = f"{sys.version_info.major}.{sys.version_info.minor}"
if not os.path.exists(enemizer_path):
feedback(
f"Enemizer not found at {enemizer_path}, please adjust the path in MultiMystery.py's config or put Enemizer in the default location.")
if not os.path.exists(rom_file):
feedback(f"Base rom is expected as {rom_file} in the Multiworld root folder please place/rename it there.")
player_files = []
os.makedirs(player_files_path, exist_ok=True)
for file in os.listdir(player_files_path):
lfile = file.lower()
if lfile.endswith(".yaml") and lfile != meta_file_path.lower() and lfile != weights_file_path.lower():
player_files.append(file)
logging.info(f"Found player's file {file}.")
player_string = ""
for i, file in enumerate(player_files, 1):
player_string += f"--p{i} \"{os.path.join(player_files_path, file)}\" "
if os.path.exists("ArchipelagoMystery.exe"):
basemysterycommand = "ArchipelagoMystery.exe" # compiled windows
elif os.path.exists("ArchipelagoMystery"):
basemysterycommand = "ArchipelagoMystery" # compiled linux
else:
basemysterycommand = f"py -{py_version} Mystery.py" # source
weights_file_path = os.path.join(player_files_path, weights_file_path)
if os.path.exists(weights_file_path):
target_player_count = max(len(player_files), target_player_count)
else:
target_player_count = len(player_files)
if target_player_count == 0:
feedback(f"No player files found. Please put them in a {player_files_path} folder.")
else:
logging.info(f"{target_player_count} Players found.")
seed_name = get_seed_name(random)
command = f"{basemysterycommand} --multi {target_player_count} {player_string} " \
f"--rom \"{rom_file}\" --enemizercli \"{enemizer_path}\" " \
f"--outputpath \"{output_path}\" --teams {teams} --plando \"{plando_options}\" " \
f"--seed_name {seed_name}"
if create_spoiler:
command += " --create_spoiler"
if create_spoiler == 2:
command += " --skip_playthrough"
if zip_diffs:
command += " --create_diff"
if glitch_triforce:
command += " --glitch_triforce"
if race:
command += " --race"
if os.path.exists(os.path.join(player_files_path, meta_file_path)):
command += f" --meta {os.path.join(player_files_path, meta_file_path)}"
if os.path.exists(weights_file_path):
command += f" --weights {weights_file_path}"
if pre_roll:
command += " --pre_roll"
logging.info(command)
import time
start = time.perf_counter()
text = subprocess.check_output(command, shell=True).decode()
logging.info(f"Took {time.perf_counter() - start:.3f} seconds to generate multiworld.")
multidataname = f"AP_{seed_name}.archipelago"
spoilername = f"AP_{seed_name}_Spoiler.txt"
romfilename = ""
if any((zip_roms, zip_multidata, zip_spoiler, zip_diffs, zip_apmcs)):
import zipfile
compression = {1: zipfile.ZIP_DEFLATED,
2: zipfile.ZIP_LZMA,
3: zipfile.ZIP_BZIP2}[zip_format]
typical_zip_ending = {1: "zip",
2: "7z",
3: "bz2"}[zip_format]
ziplock = threading.Lock()
def pack_file(file: str):
with ziplock:
zf.write(os.path.join(output_path, file), file)
logging.info(f"Packed {file} into zipfile {zipname}")
def remove_zipped_file(file: str):
os.remove(os.path.join(output_path, file))
logging.info(f"Removed {file} which is now present in the zipfile")
zipname = os.path.join(output_path, f"AP_{seed_name}.{typical_zip_ending}")
logging.info(f"Creating zipfile {zipname}")
ipv4 = (host if host else get_public_ipv4()) + ":" + str(port)
def _handle_sfc_file(file: str):
if zip_roms:
pack_file(file)
if zip_roms == 2:
remove_zipped_file(file)
def _handle_diff_file(file: str):
if zip_diffs > 0:
pack_file(file)
if zip_diffs == 2:
remove_zipped_file(file)
def _handle_apmc_file(file: str):
if zip_apmcs:
pack_file(file)
if zip_apmcs == 2:
remove_zipped_file(file)
with concurrent.futures.ThreadPoolExecutor() as pool:
futures = []
files = os.listdir(output_path)
with zipfile.ZipFile(zipname, "w", compression=compression, compresslevel=9) as zf:
for file in files:
if seed_name in file:
if file.endswith(".sfc"):
futures.append(pool.submit(_handle_sfc_file, file))
elif file.endswith(".apbp"):
futures.append(pool.submit(_handle_diff_file, file))
elif file.endswith(".apmc"):
futures.append(pool.submit(_handle_apmc_file, file))
# just handle like a diff file for now
elif file.endswith(".zip"):
futures.append(pool.submit(_handle_diff_file, file))
if zip_multidata and os.path.exists(os.path.join(output_path, multidataname)):
pack_file(multidataname)
if zip_multidata == 2:
remove_zipped_file(multidataname)
if zip_spoiler and create_spoiler:
pack_file(spoilername)
if zip_spoiler == 2:
remove_zipped_file(spoilername)
for future in futures:
future.result() # make sure we close the zip AFTER any packing is done
if not args.disable_autohost:
if os.path.exists(os.path.join(output_path, multidataname)):
if os.path.exists("ArchipelagoServer.exe"):
baseservercommand = "ArchipelagoServer.exe" # compiled windows
elif os.path.exists("ArchipelagoServer"):
baseservercommand = "ArchipelagoServer" # compiled linux
else:
baseservercommand = f"py -{py_version} MultiServer.py" # source
# don't have a mac to test that. If you try to run compiled on mac, good luck.
subprocess.call(f"{baseservercommand} --multidata {os.path.join(output_path, multidataname)}")
except:
import traceback
traceback.print_exc()
input("Press enter to close")

File diff suppressed because it is too large Load Diff

View File

@@ -1,842 +0,0 @@
import argparse
import logging
import random
import urllib.request
import urllib.parse
import typing
import os
from collections import Counter
import string
import ModuleUpdate
from worlds.generic import PlandoItem, PlandoConnection
ModuleUpdate.update()
from Utils import parse_yaml
from worlds.alttp.EntranceRandomizer import parse_arguments
from Main import main as ERmain
from Main import get_seed, seeddigits
import Options
from worlds import lookup_any_item_name_to_id
from worlds.alttp.Items import item_name_groups, item_table
from worlds.alttp import Bosses
from worlds.alttp.Text import TextTable
from worlds.alttp.Regions import location_table, key_drop_data
def mystery_argparse():
parser = argparse.ArgumentParser(add_help=False)
parser.add_argument('--multi', default=1, type=lambda value: min(max(int(value), 1), 255))
multiargs, _ = parser.parse_known_args()
parser = argparse.ArgumentParser()
parser.add_argument('--weights',
help='Path to the weights file to use for rolling game settings, urls are also valid')
parser.add_argument('--samesettings', help='Rolls settings per weights file rather than per player',
action='store_true')
parser.add_argument('--seed', help='Define seed number to generate.', type=int)
parser.add_argument('--multi', default=1, 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('--create_spoiler', action='store_true')
parser.add_argument('--skip_playthrough', action='store_true')
parser.add_argument('--pre_roll', action='store_true')
parser.add_argument('--rom')
parser.add_argument('--enemizercli')
parser.add_argument('--outputpath')
parser.add_argument('--glitch_triforce', action='store_true')
parser.add_argument('--race', action='store_true')
parser.add_argument('--meta', default=None)
parser.add_argument('--log_output_path', help='Path to store output log')
parser.add_argument('--loglevel', default='info', help='Sets log level')
parser.add_argument('--create_diff', action="store_true")
parser.add_argument('--yaml_output', default=0, type=lambda value: min(max(int(value), 0), 255),
help='Output rolled mystery results to yaml up to specified number (made for async multiworld)')
parser.add_argument('--plando', default="bosses",
help='List of options that can be set manually. Can be combined, for example "bosses, items"')
parser.add_argument('--seed_name')
for player in range(1, multiargs.multi + 1):
parser.add_argument(f'--p{player}', help=argparse.SUPPRESS)
args = parser.parse_args()
args.plando: typing.Set[str] = {arg.strip().lower() for arg in args.plando.split(",")}
return args
def get_seed_name(random):
return f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits)
def main(args=None, callback=ERmain):
if not args:
args = mystery_argparse()
seed = get_seed(args.seed)
random.seed(seed)
seed_name = args.seed_name if args.seed_name else get_seed_name(random)
print(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, {seed_name} Seed {seed}")
if args.race:
random.seed() # reset to time-based random source
weights_cache = {}
if args.weights:
try:
weights_cache[args.weights] = get_weights(args.weights)
except Exception as e:
raise ValueError(f"File {args.weights} is destroyed. Please fix your yaml.") from e
print(f"Weights: {args.weights} >> "
f"{get_choice('description', weights_cache[args.weights], 'No description specified')}")
if args.meta:
try:
weights_cache[args.meta] = get_weights(args.meta)
except Exception as e:
raise ValueError(f"File {args.meta} is destroyed. Please fix your yaml.") from e
meta_weights = weights_cache[args.meta]
print(f"Meta: {args.meta} >> {get_choice('meta_description', meta_weights, 'No description specified')}")
if args.samesettings:
raise Exception("Cannot mix --samesettings with --meta")
for player in range(1, args.multi + 1):
path = getattr(args, f'p{player}')
if path:
try:
if path not in weights_cache:
weights_cache[path] = get_weights(path)
print(f"P{player} Weights: {path} >> "
f"{get_choice('description', weights_cache[path], 'No description specified')}")
except Exception as e:
raise ValueError(f"File {path} is destroyed. Please fix your yaml.") from e
erargs = parse_arguments(['--multi', str(args.multi)])
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.create_spoiler
erargs.create_diff = args.create_diff
erargs.glitch_triforce = args.glitch_triforce
erargs.race = args.race
erargs.skip_playthrough = args.skip_playthrough
erargs.outputname = seed_name
erargs.outputpath = args.outputpath
erargs.teams = args.teams
# set up logger
if args.loglevel:
erargs.loglevel = args.loglevel
loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[
erargs.loglevel]
if args.log_output_path:
import sys
class LoggerWriter(object):
def __init__(self, writer):
self._writer = writer
self._msg = ''
def write(self, message):
self._msg = self._msg + message
while '\n' in self._msg:
pos = self._msg.find('\n')
self._writer(self._msg[:pos])
self._msg = self._msg[pos + 1:]
def flush(self):
if self._msg != '':
self._writer(self._msg)
self._msg = ''
log = logging.getLogger("stderr")
log.addHandler(logging.StreamHandler())
sys.stderr = LoggerWriter(log.error)
os.makedirs(args.log_output_path, exist_ok=True)
logging.basicConfig(format='%(message)s', level=loglevel,
filename=os.path.join(args.log_output_path, f"{seed}.log"))
else:
logging.basicConfig(format='%(message)s', level=loglevel)
if args.rom:
erargs.rom = args.rom
if args.enemizercli:
erargs.enemizercli = args.enemizercli
settings_cache = {k: (roll_settings(v, args.plando) if args.samesettings else None)
for k, v in weights_cache.items()}
player_path_cache = {}
for player in range(1, args.multi + 1):
player_path_cache[player] = getattr(args, f'p{player}') if getattr(args, f'p{player}') else args.weights
if args.meta:
for player, path in player_path_cache.items():
weights_cache[path].setdefault("meta_ignore", [])
meta_weights = weights_cache[args.meta]
for key in meta_weights:
option = get_choice(key, meta_weights)
if option is not None:
for player, path in player_path_cache.items():
players_meta = weights_cache[path].get("meta_ignore", [])
if key not in players_meta:
weights_cache[path][key] = option
elif type(players_meta) == dict and players_meta[key] and option not in players_meta[key]:
weights_cache[path][key] = option
name_counter = Counter()
erargs.player_settings = {}
for player in range(1, args.multi + 1):
path = player_path_cache[player]
if path:
try:
settings = settings_cache[path] if settings_cache[path] else \
roll_settings(weights_cache[path], args.plando)
if args.pre_roll:
import yaml
if path == args.weights:
settings.name = f"Player{player}"
elif not settings.name:
settings.name = os.path.splitext(os.path.split(path)[-1])[0]
if "-" not in settings.shuffle and settings.shuffle != "vanilla":
settings.shuffle += f"-{random.randint(0, 2 ** 64)}"
pre_rolled = dict()
pre_rolled["original_seed_number"] = seed
pre_rolled["original_seed_name"] = seed_name
pre_rolled["pre_rolled"] = vars(settings).copy()
if "plando_items" in pre_rolled["pre_rolled"]:
pre_rolled["pre_rolled"]["plando_items"] = [item.to_dict() for item in
pre_rolled["pre_rolled"]["plando_items"]]
if "plando_connections" in pre_rolled["pre_rolled"]:
pre_rolled["pre_rolled"]["plando_connections"] = [connection.to_dict() for connection in
pre_rolled["pre_rolled"][
"plando_connections"]]
with open(os.path.join(args.outputpath if args.outputpath else ".",
f"{os.path.split(path)[-1].split('.')[0]}_pre_rolled.yaml"), "wt") as f:
yaml.dump(pre_rolled, f)
for k, v in vars(settings).items():
if v is not None:
try:
getattr(erargs, k)[player] = v
except AttributeError:
setattr(erargs, k, {player: v})
except Exception as e:
raise ValueError(f"File {path} is destroyed. Please fix your yaml.") from e
else:
raise RuntimeError(f'No weights specified for player {player}')
if path == args.weights: # if name came from the weights file, just use base player name
erargs.name[player] = f"Player{player}"
elif not erargs.name[player]: # if name was not specified, generate it from filename
erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0]
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))
del (erargs.name)
if args.yaml_output:
import yaml
important = {}
for option, player_settings in vars(erargs).items():
if type(player_settings) == dict:
if all(type(value) != list for value in player_settings.values()):
if len(player_settings.values()) > 1:
important[option] = {player: value for player, value in player_settings.items() if
player <= args.yaml_output}
elif len(player_settings.values()) > 0:
important[option] = player_settings[1]
else:
logging.debug(f"No player settings defined for option '{option}'")
else:
if player_settings != "": # is not empty name
important[option] = player_settings
else:
logging.debug(f"No player settings defined for option '{option}'")
if args.outputpath:
os.makedirs(args.outputpath, exist_ok=True)
with open(os.path.join(args.outputpath if args.outputpath else ".", f"mystery_result_{seed}.yaml"), "wt") as f:
yaml.dump(important, f)
callback(erargs, seed)
def get_weights(path):
try:
if urllib.parse.urlparse(path).scheme:
yaml = str(urllib.request.urlopen(path).read(), "utf-8")
else:
with open(path, 'rb') as f:
yaml = str(f.read(), "utf-8")
except Exception as e:
raise Exception(f"Failed to read weights ({path})") from e
return parse_yaml(yaml)
def interpret_on_off(value):
return {"on": True, "off": False}.get(value, value)
def convert_to_on_off(value):
return {True: "on", False: "off"}.get(value, value)
def get_choice(option, root, value=None) -> typing.Any:
if option not in root:
return value
if type(root[option]) is list:
return interpret_on_off(random.choices(root[option])[0])
if type(root[option]) is not dict:
return interpret_on_off(root[option])
if not root[option]:
return value
if any(root[option].values()):
return interpret_on_off(
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):
def __missing__(self, key):
return '{' + key + '}'
def handle_name(name: str, player: int, name_counter: Counter):
name_counter[name] += 1
new_name = "%".join([x.replace("%number%", "{number}").replace("%player%", "{player}") for x in name.split("%%")])
new_name = string.Formatter().vformat(new_name, (), SafeDict(number=name_counter[name],
NUMBER=(name_counter[name] if name_counter[
name] > 1 else ''),
player=player,
PLAYER=(player if player > 1 else '')))
new_name = new_name.strip().replace(' ', '_')[:16]
if new_name == "Archipelago":
raise Exception(f"You cannot name yourself \"{new_name}\"")
return new_name
def prefer_int(input_data: str) -> typing.Union[str, int]:
try:
return int(input_data)
except:
return input_data
available_boss_names: typing.Set[str] = {boss.lower() for boss in Bosses.boss_table if boss not in
{'Agahnim', 'Agahnim2', 'Ganon'}}
available_boss_locations: typing.Set[str] = {f"{loc.lower()}{f' {level}' if level else ''}" for loc, level in
Bosses.boss_location_table}
boss_shuffle_options = {None: 'none',
'none': 'none',
'basic': 'basic',
'full': 'full',
'chaos': 'chaos',
'singularity': 'singularity'
}
goals = {
'ganon': 'ganon',
'crystals': 'crystals',
'bosses': 'bosses',
'pedestal': 'pedestal',
'ganon_pedestal': 'ganonpedestal',
'triforce_hunt': 'triforcehunt',
'local_triforce_hunt': 'localtriforcehunt',
'ganon_triforce_hunt': 'ganontriforcehunt',
'local_ganon_triforce_hunt': 'localganontriforcehunt',
'ice_rod_hunt': 'icerodhunt',
}
# remove sometime before 1.0.0, warn before
legacy_boss_shuffle_options = {
# legacy, will go away:
'simple': 'basic',
'random': 'full',
'normal': 'full'
}
legacy_goals = {
'dungeons': 'bosses',
'fast_ganon': 'crystals',
}
def roll_percentage(percentage: typing.Union[int, float]) -> bool:
"""Roll a percentage chance.
percentage is expected to be in range [0, 100]"""
return random.random() < (float(percentage) / 100)
def update_weights(weights: dict, new_weights: dict, type: str, name: str) -> dict:
logging.debug(f'Applying {new_weights}')
new_options = set(new_weights) - set(weights)
weights.update(new_weights)
if new_options:
for new_option in new_options:
logging.warning(f'{type} Suboption "{new_option}" of "{name}" did not '
f'overwrite a root option. '
f'This is probably in error.')
return weights
def roll_linked_options(weights: dict) -> dict:
weights = weights.copy() # make sure we don't write back to other weights sets in same_settings
for option_set in weights["linked_options"]:
if "name" not in option_set:
raise ValueError("One of your linked options does not have a name.")
try:
if roll_percentage(option_set["percentage"]):
logging.debug(f"Linked option {option_set['name']} triggered.")
if "options" in option_set:
weights = update_weights(weights, option_set["options"], "Linked", option_set["name"])
if "rom_options" in option_set:
rom_weights = weights.get("rom", dict())
rom_weights = update_weights(rom_weights, option_set["rom_options"], "Linked Rom",
option_set["name"])
weights["rom"] = rom_weights
else:
logging.debug(f"linked option {option_set['name']} skipped.")
except Exception as e:
raise ValueError(f"Linked option {option_set['name']} is destroyed. "
f"Please fix your linked option.") from e
return weights
def roll_triggers(weights: dict) -> dict:
weights = weights.copy() # make sure we don't write back to other weights sets in same_settings
weights["_Generator_Version"] = "Archipelago" # Some means for triggers to know if the seed is on main or doors.
for i, option_set in enumerate(weights["triggers"]):
try:
key = get_choice("option_name", option_set)
if key not in weights:
logging.warning(f'Specified option name {option_set["option_name"]} did not '
f'match with a root option. '
f'This is probably in error.')
trigger_result = get_choice("option_result", option_set)
result = get_choice(key, weights)
if result == trigger_result and roll_percentage(get_choice("percentage", option_set, 100)):
if "options" in option_set:
weights = update_weights(weights, option_set["options"], "Triggered", option_set["option_name"])
if "rom_options" in option_set:
rom_weights = weights.get("rom", dict())
rom_weights = update_weights(rom_weights, option_set["rom_options"], "Triggered Rom",
option_set["option_name"])
weights["rom"] = rom_weights
weights[key] = result
except Exception as e:
raise ValueError(f"Your trigger number {i+1} is destroyed. "
f"Please fix your triggers.") from e
return weights
def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str:
if boss_shuffle in legacy_boss_shuffle_options:
new_boss_shuffle = legacy_boss_shuffle_options[boss_shuffle]
logging.warning(f"Boss shuffle {boss_shuffle} is deprecated, "
f"please use {new_boss_shuffle} instead")
return new_boss_shuffle
if boss_shuffle in boss_shuffle_options:
return boss_shuffle_options[boss_shuffle]
elif "bosses" in plando_options:
options = boss_shuffle.lower().split(";")
remainder_shuffle = "none" # vanilla
bosses = []
for boss in options:
if boss in legacy_boss_shuffle_options:
remainder_shuffle = legacy_boss_shuffle_options[boss_shuffle]
logging.warning(f"Boss shuffle {boss} is deprecated, "
f"please use {remainder_shuffle} instead")
if boss in boss_shuffle_options:
remainder_shuffle = boss_shuffle_options[boss]
elif "-" in boss:
loc, boss_name = boss.split("-")
if boss_name not in available_boss_names:
raise ValueError(f"Unknown Boss name {boss_name}")
if loc not in available_boss_locations:
raise ValueError(f"Unknown Boss Location {loc}")
level = ''
if loc.split(" ")[-1] in {"top", "middle", "bottom"}:
# split off level
loc = loc.split(" ")
level = f" {loc[-1]}"
loc = " ".join(loc[:-1])
loc = loc.title().replace("Of", "of")
if not Bosses.can_place_boss(boss_name.title(), loc, level):
raise ValueError(f"Cannot place {boss_name} at {loc}{level}")
bosses.append(boss)
elif boss not in available_boss_names:
raise ValueError(f"Unknown Boss name or Boss shuffle option {boss}.")
else:
bosses.append(boss)
return ";".join(bosses + [remainder_shuffle])
else:
raise Exception(f"Boss Shuffle {boss_shuffle} is unknown and boss plando is turned off.")
def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("bosses",))):
if "pre_rolled" in weights:
pre_rolled = weights["pre_rolled"]
if "plando_items" in pre_rolled:
pre_rolled["plando_items"] = [PlandoItem(item["item"],
item["location"],
item["world"],
item["from_pool"],
item["force"]) for item in pre_rolled["plando_items"]]
if "items" not in plando_options and pre_rolled["plando_items"]:
raise Exception("Item Plando is turned off. Reusing this pre-rolled setting not permitted.")
if "plando_connections" in pre_rolled:
pre_rolled["plando_connections"] = [PlandoConnection(connection["entrance"],
connection["exit"],
connection["direction"]) for connection in
pre_rolled["plando_connections"]]
if "connections" not in plando_options and pre_rolled["plando_connections"]:
raise Exception("Connection Plando is turned off. Reusing this pre-rolled setting not permitted.")
if "bosses" not in plando_options:
try:
pre_rolled["shufflebosses"] = get_plando_bosses(pre_rolled["shufflebosses"], plando_options)
except Exception as ex:
raise Exception("Boss Plando is turned off. Reusing this pre-rolled setting not permitted.") from ex
if pre_rolled.get("plando_texts") and "texts" not in plando_options:
raise Exception("Text Plando is turned off. Reusing this pre-rolled setting not permitted.")
return argparse.Namespace(**pre_rolled)
if "linked_options" in weights:
weights = roll_linked_options(weights)
if "triggers" in weights:
weights = roll_triggers(weights)
ret = argparse.Namespace()
ret.name = get_choice('name', weights)
ret.accessibility = get_choice('accessibility', weights)
ret.progression_balancing = get_choice('progression_balancing', weights, True)
ret.game = get_choice("game", weights, "A Link to the Past")
ret.local_items = set()
for item_name in weights.get('local_items', []):
items = item_name_groups.get(item_name, {item_name})
for item in items:
if item in lookup_any_item_name_to_id:
ret.local_items.add(item)
else:
raise Exception(f"Could not force item {item} to be world-local, as it was not recognized.")
ret.non_local_items = set()
for item_name in weights.get('non_local_items', []):
items = item_name_groups.get(item_name, {item_name})
for item in items:
if item in lookup_any_item_name_to_id:
ret.non_local_items.add(item)
else:
raise Exception(f"Could not force item {item} to be world-non-local, as it was not recognized.")
inventoryweights = weights.get('startinventory', {})
startitems = []
for item in inventoryweights.keys():
itemvalue = get_choice(item, inventoryweights)
if isinstance(itemvalue, int):
for i in range(int(itemvalue)):
startitems.append(item)
elif itemvalue:
startitems.append(item)
ret.startinventory = startitems
ret.start_hints = set(weights.get('start_hints', []))
if ret.game == "A Link to the Past":
roll_alttp_settings(ret, weights, plando_options)
elif ret.game == "Hollow Knight":
for option_name, option in Options.hollow_knight_options.items():
setattr(ret, option_name, option.from_any(get_choice(option_name, weights, True)))
elif ret.game == "Factorio":
for option_name, option in Options.factorio_options.items():
if option_name in weights:
if issubclass(option, Options.OptionDict): # get_choice should probably land in the Option class
setattr(ret, option_name, option.from_any(weights[option_name]))
else:
setattr(ret, option_name, option.from_any(get_choice(option_name, weights)))
else:
setattr(ret, option_name, option(option.default))
elif ret.game == "Minecraft":
for option_name, option in Options.minecraft_options.items():
if option_name in weights:
setattr(ret, option_name, option.from_any(get_choice(option_name, weights)))
else:
setattr(ret, option_name, option(option.default))
# bad hardcoded behavior to make this work for now
ret.plando_connections = []
if "connections" in plando_options:
options = weights.get("plando_connections", [])
for placement in options:
if roll_percentage(get_choice("percentage", placement, 100)):
ret.plando_connections.append(PlandoConnection(
get_choice("entrance", placement),
get_choice("exit", placement),
get_choice("direction", placement, "both")
))
else:
raise Exception(f"Unsupported game {ret.game}")
return ret
def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
glitches_required = get_choice('glitches_required', weights)
if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'minor_glitches']:
logging.warning("Only NMG, OWG and No Logic supported")
glitches_required = 'none'
ret.logic = {None: 'noglitches', 'none': 'noglitches', 'no_logic': 'nologic', 'overworld_glitches': 'owglitches',
'minor_glitches': 'minorglitches'}[
glitches_required]
ret.dark_room_logic = get_choice("dark_room_logic", weights, "lamp")
if not ret.dark_room_logic: # None/False
ret.dark_room_logic = "none"
if ret.dark_room_logic == "sconces":
ret.dark_room_logic = "torches"
if ret.dark_room_logic not in {"lamp", "torches", "none"}:
raise ValueError(f"Unknown Dark Room Logic: \"{ret.dark_room_logic}\"")
ret.restrict_dungeon_item_on_boss = get_choice('restrict_dungeon_item_on_boss', weights, False)
dungeon_items = get_choice('dungeon_items', weights)
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-'):
ret.shuffle = 'vanilla'
else:
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
goal = get_choice('goals', weights, 'ganon')
if goal in legacy_goals:
logging.warning(f"Goal {goal} is depcrecated, please use {legacy_goals[goal]} instead.")
goal = legacy_goals[goal]
ret.goal = goals[goal]
# TODO consider moving open_pyramid to an automatic variable in the core roller, set to True when
# fast ganon + ganon at hole
ret.open_pyramid = get_choice('open_pyramid', weights, 'goal')
ret.crystals_gt = prefer_int(get_choice('tower_open', weights))
ret.crystals_ganon = prefer_int(get_choice('ganon_open', weights))
extra_pieces = get_choice('triforce_pieces_mode', weights, 'available')
ret.triforce_pieces_required = int(get_choice('triforce_pieces_required', weights, 20))
ret.triforce_pieces_required = min(max(1, int(ret.triforce_pieces_required)), 90)
# sum a percentage to required
if extra_pieces == 'percentage':
percentage = max(100, float(get_choice('triforce_pieces_percentage', weights, 150))) / 100
ret.triforce_pieces_available = int(round(ret.triforce_pieces_required * percentage, 0))
# vanilla mode (specify how many pieces are)
elif extra_pieces == 'available':
ret.triforce_pieces_available = int(get_choice('triforce_pieces_available', weights, 30))
# required pieces + fixed extra
elif extra_pieces == 'extra':
extra_pieces = max(0, int(get_choice('triforce_pieces_extra', weights, 10)))
ret.triforce_pieces_available = ret.triforce_pieces_required + extra_pieces
# change minimum to required pieces to avoid problems
ret.triforce_pieces_available = min(max(ret.triforce_pieces_required, int(ret.triforce_pieces_available)), 90)
shuffle_slots = get_choice('shop_shuffle_slots', weights, '0')
if str(shuffle_slots).lower() == "random":
ret.shop_shuffle_slots = random.randint(0, 30)
else:
ret.shop_shuffle_slots = int(shuffle_slots)
ret.shop_shuffle = get_choice('shop_shuffle', weights, '')
if not ret.shop_shuffle:
ret.shop_shuffle = ''
ret.mode = get_choice("mode", weights)
ret.retro = get_choice("retro", weights)
ret.hints = get_choice('hints', weights)
ret.swordless = get_choice('swordless', weights, False)
ret.difficulty = get_choice('item_pool', weights)
ret.item_functionality = get_choice('item_functionality', weights)
boss_shuffle = get_choice('boss_shuffle', weights)
ret.shufflebosses = get_plando_bosses(boss_shuffle, plando_options)
ret.enemy_shuffle = bool(get_choice('enemy_shuffle', weights, False))
ret.killable_thieves = get_choice('killable_thieves', weights, False)
ret.tile_shuffle = get_choice('tile_shuffle', weights, False)
ret.bush_shuffle = get_choice('bush_shuffle', weights, False)
ret.enemy_damage = {None: 'default',
'default': 'default',
'shuffled': 'shuffled',
'random': 'chaos'
}[get_choice('enemy_damage', weights)]
ret.enemy_health = get_choice('enemy_health', weights)
ret.shufflepots = get_choice('pot_shuffle', weights)
ret.beemizer = int(get_choice('beemizer', weights, 0))
ret.timer = {'none': False,
None: False,
False: False,
'timed': 'timed',
'timed_ohko': 'timed-ohko',
'ohko': 'ohko',
'timed_countdown': 'timed-countdown',
'display': 'display'}[get_choice('timer', weights, False)]
ret.countdown_start_time = int(get_choice('countdown_start_time', weights, 10))
ret.red_clock_time = int(get_choice('red_clock_time', weights, -2))
ret.blue_clock_time = int(get_choice('blue_clock_time', weights, 2))
ret.green_clock_time = int(get_choice('green_clock_time', weights, 4))
ret.dungeon_counters = get_choice('dungeon_counters', weights, 'default')
ret.progressive = convert_to_on_off(get_choice('progressive', weights, 'on'))
ret.shuffle_prizes = get_choice('shuffle_prizes', weights, "g")
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):
ret.required_medallions[index] = {"ether": "Ether", "quake": "Quake", "bombos": "Bombos", "random": "random"} \
.get(medallion.lower(), None)
if not ret.required_medallions[index]:
raise Exception(f"unknown Medallion {medallion} for {'misery mire' if index == 0 else 'turtle rock'}")
ret.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 = []
if "items" in plando_options:
def add_plando_item(item: str, location: str):
if item not in item_table:
raise Exception(f"Could not plando item {item} as the item was not recognized")
if location not in location_table and location not in key_drop_data:
raise Exception(
f"Could not plando item {item} at location {location} as the location was not recognized")
ret.plando_items.append(PlandoItem(item, location, location_world, from_pool, force))
options = weights.get("plando_items", [])
for placement in options:
if roll_percentage(get_choice("percentage", placement, 100)):
from_pool = get_choice("from_pool", placement, PlandoItem._field_defaults["from_pool"])
location_world = get_choice("world", placement, PlandoItem._field_defaults["world"])
force = str(get_choice("force", placement, PlandoItem._field_defaults["force"])).lower()
if "items" in placement and "locations" in placement:
items = placement["items"]
locations = placement["locations"]
if isinstance(items, dict):
item_list = []
for key, value in items.items():
item_list += [key] * value
items = item_list
if not items or not locations:
raise Exception("You must specify at least one item and one location to place items.")
random.shuffle(items)
random.shuffle(locations)
for item, location in zip(items, locations):
add_plando_item(item, location)
else:
item = get_choice("item", placement, get_choice("items", placement))
location = get_choice("location", placement)
add_plando_item(item, location)
ret.plando_texts = {}
if "texts" in plando_options:
tt = TextTable()
tt.removeUnwantedText()
options = weights.get("plando_texts", [])
for placement in options:
if roll_percentage(get_choice("percentage", placement, 100)):
at = str(get_choice("at", placement))
if at not in tt:
raise Exception(f"No text target \"{at}\" found.")
ret.plando_texts[at] = str(get_choice("text", placement))
ret.plando_connections = []
if "connections" in plando_options:
options = weights.get("plando_connections", [])
for placement in options:
if roll_percentage(get_choice("percentage", placement, 100)):
ret.plando_connections.append(PlandoConnection(
get_choice("entrance", placement),
get_choice("exit", placement),
get_choice("direction", placement, "both")
))
if 'rom' in weights:
romweights = weights['rom']
ret.sprite_pool = romweights['sprite_pool'] if 'sprite_pool' in romweights else []
ret.sprite = get_choice('sprite', romweights, "Link")
if 'random_sprite_on_event' in romweights:
randomoneventweights = romweights['random_sprite_on_event']
if get_choice('enabled', randomoneventweights, False):
ret.sprite = 'randomon'
ret.sprite += '-hit' if get_choice('on_hit', randomoneventweights, True) else ''
ret.sprite += '-enter' if get_choice('on_enter', randomoneventweights, False) else ''
ret.sprite += '-exit' if get_choice('on_exit', randomoneventweights, False) else ''
ret.sprite += '-slash' if get_choice('on_slash', randomoneventweights, False) else ''
ret.sprite += '-item' if get_choice('on_item', randomoneventweights, False) else ''
ret.sprite += '-bonk' if get_choice('on_bonk', randomoneventweights, False) else ''
ret.sprite = 'randomonall' if get_choice('on_everything', randomoneventweights, False) 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)) \
and 'sprite' in romweights: # Use sprite as a weighted sprite pool, if a sprite pool is not already defined.
for key, value in romweights['sprite'].items():
if key.startswith('random'):
ret.sprite_pool += ['random'] * int(value)
else:
ret.sprite_pool += [key] * int(value)
ret.disablemusic = get_choice('disablemusic', romweights, False)
ret.triforcehud = get_choice('triforcehud', romweights, 'hide_goal')
ret.quickswap = get_choice('quickswap', romweights, True)
ret.fastmenu = get_choice('menuspeed', romweights, "normal")
ret.reduceflashing = get_choice('reduceflashing', romweights, False)
ret.heartcolor = get_choice('heartcolor', romweights, "red")
ret.heartbeep = convert_to_on_off(get_choice('heartbeep', romweights, "normal"))
ret.ow_palettes = get_choice('ow_palettes', romweights, "default")
ret.uw_palettes = get_choice('uw_palettes', romweights, "default")
ret.hud_palettes = get_choice('hud_palettes', romweights, "default")
ret.sword_palettes = get_choice('sword_palettes', romweights, "default")
ret.shield_palettes = get_choice('shield_palettes', romweights, "default")
ret.link_palettes = get_choice('link_palettes', romweights, "default")
else:
ret.quickswap = True
ret.sprite = "Link"
if __name__ == '__main__':
main()

View File

@@ -1,6 +1,5 @@
from __future__ import annotations
import asyncio
import logging
import typing
import enum
from json import JSONEncoder, JSONDecoder
@@ -15,8 +14,10 @@ class JSONMessagePart(typing.TypedDict, total=False):
# optional
type: str
color: str
# mainly for items, optional
found: bool
# owning player for location/item
player: int
# if type == item indicates item flags
flags: int
class ClientStatus(enum.IntEnum):
@@ -27,17 +28,57 @@ class ClientStatus(enum.IntEnum):
CLIENT_GOAL = 30
class SlotType(enum.IntFlag):
spectator = 0b00
player = 0b01
group = 0b10
@property
def always_goal(self) -> bool:
"""Mark this slot has having reached its goal instantly."""
return self.value != 0b01
class Permission(enum.IntFlag):
disabled = 0b000 # 0, completely disables access
enabled = 0b001 # 1, allows manual use
goal = 0b010 # 2, allows manual use after goal completion
auto = 0b110 # 6, forces use after goal completion, only works for forfeit
auto_enabled = 0b111 # 7, forces use after goal completion, allows manual use any time
@staticmethod
def from_text(text: str):
data = 0
if "auto" in text:
data |= 0b110
elif "goal" in text:
data |= 0b010
if "enabled" in text:
data |= 0b001
return Permission(data)
class NetworkPlayer(typing.NamedTuple):
"""Represents a particular player on a particular team."""
team: int
slot: int
alias: str
name: str
class NetworkSlot(typing.NamedTuple):
"""Represents a particular slot across teams."""
name: str
game: str
type: SlotType
group_members: typing.Union[typing.List[int], typing.Tuple] = () # only populated if type == group
class NetworkItem(typing.NamedTuple):
item: int
location: int
player: int
flags: int = 0
def _scan_for_TypedTuples(obj: typing.Any) -> typing.Any:
@@ -45,7 +86,7 @@ def _scan_for_TypedTuples(obj: typing.Any) -> typing.Any:
data = obj._asdict()
data["class"] = obj.__class__.__name__
return data
if isinstance(obj, (tuple, list)):
if isinstance(obj, (tuple, list, set)):
return tuple(_scan_for_TypedTuples(o) for o in obj)
if isinstance(obj, dict):
return {key: _scan_for_TypedTuples(value) for key, value in obj.items()}
@@ -94,60 +135,12 @@ def _object_hook(o: typing.Any) -> typing.Any:
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]):
if not endpoint.socket or not endpoint.socket.open or endpoint.socket.closed:
return
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}")
async def send_encoded_msgs(self, endpoint: Endpoint, msg: str):
if not endpoint.socket or not endpoint.socket.open or endpoint.socket.closed:
return
try:
await endpoint.socket.send(msg)
except websockets.ConnectionClosed:
logging.exception("Exception during send_msgs")
await self.disconnect(endpoint)
else:
if self.log_network:
logging.info(f"Outgoing message: {msg}")
async def disconnect(self, endpoint):
if endpoint in self.endpoints:
self.endpoints.remove(endpoint)
class Endpoint:
socket: websockets.WebSocketServerProtocol
def __init__(self, socket):
self.socket = socket
async def disconnect(self):
raise NotImplementedError
class HandlerMeta(type):
def __new__(mcs, name, bases, attrs):
@@ -166,11 +159,11 @@ class HandlerMeta(type):
break
def __init__(self, *args, **kwargs):
if orig_init:
orig_init(self, *args, **kwargs)
# turn functions into bound methods
self.handlers = {name: method.__get__(self, type(self)) for name, method in
handlers.items()}
if orig_init:
orig_init(self, *args, **kwargs)
attrs['__init__'] = __init__
return super(HandlerMeta, mcs).__new__(mcs, name, bases, attrs)
@@ -189,6 +182,21 @@ class JSONTypes(str, enum.Enum):
class JSONtoTextParser(metaclass=HandlerMeta):
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",
"slateblue": "6D8BE8",
"plum": "AF99EF",
"salmon": "FA8072",
"white": "FFFFFF"
}
def __init__(self, ctx):
self.ctx = ctx
@@ -196,13 +204,13 @@ class JSONtoTextParser(metaclass=HandlerMeta):
return "".join(self.handle_node(section) for section in input_object)
def handle_node(self, node: JSONMessagePart):
type = node.get("type", None)
handler = self.handlers.get(type, self.handlers["text"])
node_type = node.get("type", None)
handler = self.handlers.get(node_type, self.handlers["text"])
return handler(node)
def _handle_color(self, node: JSONMessagePart):
codes = node["color"].split(";")
buffer = "".join(color_code(code) for code in codes)
buffer = "".join(color_code(code) for code in codes if code in color_codes)
return buffer + self._handle_text(node) + color_code("reset")
def _handle_text(self, node: JSONMessagePart):
@@ -220,11 +228,17 @@ class JSONtoTextParser(metaclass=HandlerMeta):
return self._handle_color(node)
def _handle_item_name(self, node: JSONMessagePart):
# todo: use a better info source
from worlds.alttp.Items import progression_items
node["color"] = 'green' if node.get("found", False) else 'cyan'
if node["text"] in progression_items:
node["color"] += ";white_bg"
flags = node.get("flags", 0)
if flags == 0:
node["color"] = 'cyan'
elif flags & 0b001: # advancement
node["color"] = 'plum'
elif flags & 0b010: # never_exclude
node["color"] = 'slateblue'
elif flags & 0b100: # trap
node["color"] = 'salmon'
else:
node["color"] = 'cyan'
return self._handle_color(node)
def _handle_item_id(self, node: JSONMessagePart):
@@ -233,16 +247,16 @@ class JSONtoTextParser(metaclass=HandlerMeta):
return self._handle_item_name(node)
def _handle_location_name(self, node: JSONMessagePart):
node["color"] = 'blue_bg;white'
node["color"] = 'green'
return self._handle_color(node)
def _handle_location_id(self, node: JSONMessagePart):
item_id = int(node["text"])
node["text"] = self.ctx.location_name_getter(item_id)
return self._handle_item_name(node)
return self._handle_location_name(node)
def _handle_entrance_name(self, node: JSONMessagePart):
node["color"] = 'white_bg;black'
node["color"] = 'blue'
return self._handle_color(node)
@@ -268,6 +282,14 @@ def add_json_text(parts: list, text: typing.Any, **kwargs) -> None:
parts.append({"text": str(text), **kwargs})
def add_json_item(parts: list, item_id: int, player: int = 0, item_flags: int = 0, **kwargs) -> None:
parts.append({"text": str(item_id), "player": player, "flags": item_flags, "type": JSONTypes.item_id, **kwargs})
def add_json_location(parts: list, item_id: int, player: int = 0, **kwargs) -> None:
parts.append({"text": str(item_id), "player": player, "type": JSONTypes.location_id, **kwargs})
class Hint(typing.NamedTuple):
receiving_player: int
finding_player: int
@@ -275,13 +297,15 @@ class Hint(typing.NamedTuple):
item: int
found: bool
entrance: str = ""
item_flags: int = 0
def re_check(self, ctx, team) -> Hint:
if self.found:
return self
found = self.location in ctx.location_checks[team, self.finding_player]
if found:
return Hint(self.receiving_player, self.finding_player, self.location, self.item, found, self.entrance)
return Hint(self.receiving_player, self.finding_player, self.location, self.item, found, self.entrance,
self.item_flags)
return self
def __hash__(self):
@@ -292,9 +316,9 @@ class Hint(typing.NamedTuple):
add_json_text(parts, "[Hint]: ")
add_json_text(parts, self.receiving_player, type="player_id")
add_json_text(parts, "'s ")
add_json_text(parts, self.item, type="item_id", found=self.found)
add_json_item(parts, self.item, self.receiving_player, self.item_flags)
add_json_text(parts, " is at ")
add_json_text(parts, self.location, type="location_id")
add_json_location(parts, self.location, self.finding_player)
add_json_text(parts, " in ")
add_json_text(parts, self.finding_player, type="player_id")
if self.entrance:
@@ -302,12 +326,16 @@ class Hint(typing.NamedTuple):
add_json_text(parts, self.entrance, type="entrance_name")
else:
add_json_text(parts, "'s World")
add_json_text(parts, ". ")
if self.found:
add_json_text(parts, ". (found)")
add_json_text(parts, "(found)", type="color", color="green")
else:
add_json_text(parts, ".")
add_json_text(parts, "(not found)", type="color", color="red")
return {"cmd": "PrintJSON", "data": parts, "type": "hint"}
return {"cmd": "PrintJSON", "data": parts, "type": "Hint",
"receiving": self.receiving_player,
"item": NetworkItem(self.item, self.location, self.finding_player, self.item_flags),
"found": self.found}
@property
def local(self):

246
OoTAdjuster.py Normal file
View File

@@ -0,0 +1,246 @@
import tkinter as tk
import argparse
import logging
import random
import os
from itertools import chain
from BaseClasses import MultiWorld
from Options import Choice, Range, Toggle
from worlds.oot import OOTWorld
from worlds.oot.Cosmetics import patch_cosmetics
from worlds.oot.Options import cosmetic_options, sfx_options
from worlds.oot.Rom import Rom, compress_rom_file
from worlds.oot.N64Patch import apply_patch_file
from worlds.oot.Utils import data_path
from Utils import local_path
logger = logging.getLogger('OoTAdjuster')
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--rom', default='',
help='Path to an OoT randomized ROM to adjust.')
parser.add_argument('--vanilla_rom', default='',
help='Path to a vanilla OoT ROM for patching.')
for name, option in chain(cosmetic_options.items(), sfx_options.items()):
parser.add_argument('--'+name, default=None,
help=option.__doc__)
parser.add_argument('--is_glitched', default=False, action='store_true',
help='Setting this to true will enable protection on kokiri tunic colors for weirdshot.')
parser.add_argument('--deathlink',
help='Enable DeathLink system', action='store_true')
args = parser.parse_args()
if not os.path.isfile(args.rom):
adjustGUI()
else:
adjust(args)
def adjustGUI():
from tkinter import Tk, LEFT, BOTTOM, TOP, E, W, \
StringVar, IntVar, Checkbutton, Frame, Label, X, Entry, Button, \
OptionMenu, filedialog, messagebox, ttk
from argparse import Namespace
from Main import __version__ as MWVersion
window = tk.Tk()
window.wm_title(f"Archipelago {MWVersion} OoT Adjuster")
set_icon(window)
opts = Namespace()
# Select ROM
romDialogFrame = Frame(window)
romLabel = Label(romDialogFrame, text='Rom/patch to adjust')
vanillaLabel = Label(romDialogFrame, text='OoT Base Rom')
opts.rom = StringVar()
opts.vanilla_rom = StringVar(value="The Legend of Zelda - Ocarina of Time.z64")
romEntry = Entry(romDialogFrame, textvariable=opts.rom)
vanillaEntry = Entry(romDialogFrame, textvariable=opts.vanilla_rom)
def RomSelect():
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".z64", ".n64", ".apz5")), ("All Files", "*")])
opts.rom.set(rom)
def VanillaSelect():
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".z64", ".n64")), ("All Files", "*")])
opts.vanilla_rom.set(rom)
romSelectButton = Button(romDialogFrame, text='Select Rom', command=RomSelect)
vanillaSelectButton = Button(romDialogFrame, text='Select Rom', command=VanillaSelect)
romDialogFrame.pack(side=TOP, expand=True, fill=X)
romLabel.pack(side=LEFT)
romEntry.pack(side=LEFT, expand=True, fill=X)
romSelectButton.pack(side=LEFT)
vanillaLabel.pack(side=LEFT)
vanillaEntry.pack(side=LEFT, expand=True, fill=X)
vanillaSelectButton.pack(side=LEFT)
# Cosmetic options
romSettingsFrame = Frame(window)
def dropdown_option(type, option_name, row, column):
if type == 'cosmetic':
option = cosmetic_options[option_name]
elif type == 'sfx':
option = sfx_options[option_name]
optionFrame = Frame(romSettingsFrame)
optionFrame.grid(row=row, column=column, sticky=E)
optionLabel = Label(optionFrame, text=option.display_name)
optionLabel.pack(side=LEFT)
setattr(opts, option_name, StringVar())
getattr(opts, option_name).set(option.name_lookup[option.default])
optionMenu = OptionMenu(optionFrame, getattr(opts, option_name), *option.name_lookup.values())
optionMenu.pack(side=LEFT)
dropdown_option('cosmetic', 'default_targeting', 0, 0)
dropdown_option('cosmetic', 'display_dpad', 0, 1)
dropdown_option('cosmetic', 'correct_model_colors', 0, 2)
dropdown_option('cosmetic', 'background_music', 1, 0)
dropdown_option('cosmetic', 'fanfares', 1, 1)
dropdown_option('cosmetic', 'ocarina_fanfares', 1, 2)
dropdown_option('cosmetic', 'kokiri_color', 2, 0)
dropdown_option('cosmetic', 'goron_color', 2, 1)
dropdown_option('cosmetic', 'zora_color', 2, 2)
dropdown_option('cosmetic', 'silver_gauntlets_color', 3, 0)
dropdown_option('cosmetic', 'golden_gauntlets_color', 3, 1)
dropdown_option('cosmetic', 'mirror_shield_frame_color', 3, 2)
dropdown_option('cosmetic', 'navi_color_default_inner', 4, 0)
dropdown_option('cosmetic', 'navi_color_default_outer', 4, 1)
dropdown_option('cosmetic', 'navi_color_enemy_inner', 5, 0)
dropdown_option('cosmetic', 'navi_color_enemy_outer', 5, 1)
dropdown_option('cosmetic', 'navi_color_npc_inner', 6, 0)
dropdown_option('cosmetic', 'navi_color_npc_outer', 6, 1)
dropdown_option('cosmetic', 'navi_color_prop_inner', 7, 0)
dropdown_option('cosmetic', 'navi_color_prop_outer', 7, 1)
# sword_trail_duration, 8, 2
dropdown_option('cosmetic', 'sword_trail_color_inner', 8, 0)
dropdown_option('cosmetic', 'sword_trail_color_outer', 8, 1)
dropdown_option('cosmetic', 'bombchu_trail_color_inner', 9, 0)
dropdown_option('cosmetic', 'bombchu_trail_color_outer', 9, 1)
dropdown_option('cosmetic', 'boomerang_trail_color_inner', 10, 0)
dropdown_option('cosmetic', 'boomerang_trail_color_outer', 10, 1)
dropdown_option('cosmetic', 'heart_color', 11, 0)
dropdown_option('cosmetic', 'magic_color', 12, 0)
dropdown_option('cosmetic', 'a_button_color', 11, 1)
dropdown_option('cosmetic', 'b_button_color', 11, 2)
dropdown_option('cosmetic', 'c_button_color', 12, 1)
dropdown_option('cosmetic', 'start_button_color', 12, 2)
dropdown_option('sfx', 'sfx_navi_overworld', 14, 0)
dropdown_option('sfx', 'sfx_navi_enemy', 14, 1)
dropdown_option('sfx', 'sfx_low_hp', 14, 2)
dropdown_option('sfx', 'sfx_menu_cursor', 15, 0)
dropdown_option('sfx', 'sfx_menu_select', 15, 1)
dropdown_option('sfx', 'sfx_nightfall', 15, 2)
dropdown_option('sfx', 'sfx_horse_neigh', 16, 0)
dropdown_option('sfx', 'sfx_hover_boots', 16, 1)
dropdown_option('sfx', 'sfx_ocarina', 16, 2)
# Special cases
# Sword trail duration is a range
option = cosmetic_options['sword_trail_duration']
optionFrame = Frame(romSettingsFrame)
optionFrame.grid(row=8, column=2, sticky=E)
optionLabel = Label(optionFrame, text=option.display_name)
optionLabel.pack(side=LEFT)
setattr(opts, 'sword_trail_duration', StringVar())
getattr(opts, 'sword_trail_duration').set(option.default)
optionMenu = OptionMenu(optionFrame, getattr(opts, 'sword_trail_duration'), *range(4, 21))
optionMenu.pack(side=LEFT)
# Glitched is a checkbox
opts.is_glitched = IntVar(value=0)
glitched_checkbox = Checkbutton(romSettingsFrame, text="Glitched Logic?", variable=opts.is_glitched)
glitched_checkbox.grid(row=17, column=0, sticky=W)
# Deathlink is a checkbox
opts.deathlink = IntVar(value=0)
deathlink_checkbox = Checkbutton(romSettingsFrame, text="DeathLink (Team Deaths)", variable=opts.deathlink)
deathlink_checkbox.grid(row=17, column=1, sticky=W)
romSettingsFrame.pack(side=TOP)
def adjustRom():
try:
guiargs = Namespace()
options = vars(opts)
for o in options:
result = options[o].get()
if result == 'true':
result = True
if result == 'false':
result = False
setattr(guiargs, o, result)
guiargs.sword_trail_duration = int(guiargs.sword_trail_duration)
path = adjust(guiargs)
except Exception as e:
logging.exception(e)
messagebox.showerror(title="Error while adjusting Rom", message=str(e))
else:
messagebox.showinfo(title="Success", message=f"Rom patched successfully to {path}")
# Adjust button
bottomFrame = Frame(window)
adjustButton = Button(bottomFrame, text='Adjust Rom', command=adjustRom)
adjustButton.pack(side=BOTTOM, padx=(5, 5))
bottomFrame.pack(side=BOTTOM, pady=(5, 5))
window.mainloop()
def set_icon(window):
logo = tk.PhotoImage(file=local_path('data', 'icon.png'))
window.tk.call('wm', 'iconphoto', window._w, logo)
def adjust(args):
# Create a fake world and OOTWorld to use as a base
world = MultiWorld(1)
world.slot_seeds = {1: random}
ootworld = OOTWorld(world, 1)
# Set options in the fake OOTWorld
for name, option in chain(cosmetic_options.items(), sfx_options.items()):
result = getattr(args, name, None)
if result is None:
if issubclass(option, Choice):
result = option.name_lookup[option.default]
elif issubclass(option, Range) or issubclass(option, Toggle):
result = option.default
else:
raise Exception("Unsupported option type")
setattr(ootworld, name, result)
ootworld.logic_rules = 'glitched' if args.is_glitched else 'glitchless'
ootworld.death_link = args.deathlink
delete_zootdec = False
if os.path.splitext(args.rom)[-1] in ['.z64', '.n64']:
# Load up the ROM
rom = Rom(file=args.rom, force_use=True)
delete_zootdec = True
elif os.path.splitext(args.rom)[-1] == '.apz5':
# Load vanilla ROM
rom = Rom(file=args.vanilla_rom, force_use=True)
# Patch file
apply_patch_file(rom, args.rom)
else:
raise Exception("Invalid file extension; requires .n64, .z64, .apz5")
# Call patch_cosmetics
try:
patch_cosmetics(ootworld, rom)
rom.write_byte(rom.sym('DEATH_LINK'), args.deathlink)
# Output new file
path_pieces = os.path.splitext(args.rom)
decomp_path = path_pieces[0] + '-adjusted-decomp.n64'
comp_path = path_pieces[0] + '-adjusted.n64'
rom.write_to_file(decomp_path)
os.chdir(data_path("Compress"))
compress_rom_file(decomp_path, comp_path)
os.remove(decomp_path)
finally:
if delete_zootdec:
os.chdir(os.path.split(__file__)[0])
os.remove("ZOOTDEC.z64")
return comp_path
if __name__ == '__main__':
main()

View File

@@ -1,22 +1,54 @@
from __future__ import annotations
import typing
import random
from schema import Schema, And, Or
class AssembleOptions(type):
def __new__(mcs, name, bases, attrs):
options = attrs["options"] = {}
name_lookup = attrs["name_lookup"] = {}
# merge parent class options
for base in bases:
options.update(base.options)
name_lookup.update(name_lookup)
if getattr(base, "options", None):
options.update(base.options)
name_lookup.update(base.name_lookup)
new_options = {name[7:].lower(): option_id for name, option_id in attrs.items() if
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()})
options.update(new_options)
# apply aliases, without name_lookup
options.update({name[6:].lower(): option_id for name, option_id in attrs.items() if
name.startswith("alias_")})
# auto-validate schema on __init__
if "schema" in attrs.keys():
if "__init__" in attrs:
def validate_decorator(func):
def validate(self, *args, **kwargs):
ret = func(self, *args, **kwargs)
self.value = self.schema.validate(self.value)
return ret
return validate
attrs["__init__"] = validate_decorator(attrs["__init__"])
else:
# construct an __init__ that calls parent __init__
cls = super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs)
def meta__init__(self, *args, **kwargs):
super(cls, self).__init__(*args, **kwargs)
self.value = self.schema.validate(self.value)
cls.__init__ = meta__init__
return cls
return super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs)
@@ -25,19 +57,38 @@ class Option(metaclass=AssembleOptions):
name_lookup: typing.Dict[int, str]
default = 0
def __repr__(self):
return f"{self.__class__.__name__}({self.get_option_name()})"
# convert option_name_long into Name Long as display_name, otherwise name_long is the result.
# Handled in get_option_name()
auto_display_name = False
# can be weighted between selections
supports_weighting = True
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.get_current_option_name()})"
def __hash__(self):
return hash(self.value)
def get_option_name(self):
@property
def current_key(self) -> str:
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.auto_display_name:
return cls.name_lookup[value].replace("_", " ").title()
else:
return cls.name_lookup[value]
def __int__(self) -> int:
return self.value
def __bool__(self):
def __bool__(self) -> bool:
return bool(self.value)
@classmethod
@@ -51,6 +102,7 @@ class Toggle(Option):
default = 0
def __init__(self, value: int):
assert value == 0 or value == 1
self.value = value
@classmethod
@@ -85,18 +137,30 @@ class Toggle(Option):
def __int__(self):
return int(self.value)
def get_option_name(self):
return bool(self.value)
@classmethod
def get_option_name(cls, value):
return ["No", "Yes"][int(value)]
__hash__ = Option.__hash__ # see https://docs.python.org/3/reference/datamodel.html#object.__hash__
class DefaultOnToggle(Toggle):
default = 1
class Choice(Option):
auto_display_name = True
def __init__(self, value: int):
self.value: int = value
@classmethod
def from_text(cls, text: str) -> Choice:
for optionname, value in cls.options.items():
if optionname == text.lower():
text = text.lower()
if text == "random":
return cls(random.choice(list(cls.name_lookup)))
for option_name, value in cls.options.items():
if option_name == text:
return cls(value)
raise KeyError(
f'Could not find option "{text}" for "{cls.__name__}", '
@@ -108,277 +172,340 @@ class Choice(Option):
return cls(data)
return cls.from_text(str(data))
def __eq__(self, other):
if isinstance(other, self.__class__):
return other.value == self.value
elif 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 OptionNameSet(Option):
default = frozenset()
def __ne__(self, other):
if isinstance(other, self.__class__):
return other.value != self.value
elif 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)
elif other is None:
return False
else:
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
def __init__(self, value: typing.Set[str]):
self.value: typing.Set[str] = value
__hash__ = Option.__hash__ # see https://docs.python.org/3/reference/datamodel.html#object.__hash__
class Range(Option, int):
range_start = 0
range_end = 1
def __init__(self, value: int):
if value < self.range_start:
raise Exception(f"{value} is lower than minimum {self.range_start} for option {self.__class__.__name__}")
elif value > self.range_end:
raise Exception(f"{value} is higher than maximum {self.range_end} for option {self.__class__.__name__}")
self.value = value
@classmethod
def from_text(cls, text: str) -> OptionNameSet:
return cls({option.strip() for option in text.split(",")})
def from_text(cls, text: str) -> Range:
text = text.lower()
if text.startswith("random"):
if text == "random-low":
return cls(int(round(random.triangular(cls.range_start, cls.range_end, cls.range_start), 0)))
elif text == "random-high":
return cls(int(round(random.triangular(cls.range_start, cls.range_end, cls.range_end), 0)))
elif text == "random-middle":
return cls(int(round(random.triangular(cls.range_start, cls.range_end), 0)))
elif text.startswith("random-range-"):
textsplit = text.split("-")
try:
random_range = [int(textsplit[len(textsplit) - 2]), int(textsplit[len(textsplit) - 1])]
except ValueError:
raise ValueError(f"Invalid random range {text} for option {cls.__name__}")
random_range.sort()
if random_range[0] < cls.range_start or random_range[1] > cls.range_end:
raise Exception(
f"{random_range[0]}-{random_range[1]} is outside allowed range "
f"{cls.range_start}-{cls.range_end} for option {cls.__name__}")
if text.startswith("random-range-low"):
return cls(int(round(random.triangular(random_range[0], random_range[1], random_range[0]))))
elif text.startswith("random-range-middle"):
return cls(int(round(random.triangular(random_range[0], random_range[1]))))
elif text.startswith("random-range-high"):
return cls(int(round(random.triangular(random_range[0], random_range[1], random_range[1]))))
else:
return cls(int(round(random.randint(random_range[0], random_range[1]))))
else:
return cls(random.randint(cls.range_start, cls.range_end))
return cls(int(text))
@classmethod
def from_any(cls, data: typing.Any) -> OptionNameSet:
if type(data) == set:
def from_any(cls, data: typing.Any) -> Range:
if type(data) == int:
return cls(data)
return cls.from_text(str(data))
def get_option_name(self, value):
return str(value)
class OptionDict(Option):
def __str__(self):
return str(self.value)
class VerifyKeys:
valid_keys = frozenset()
valid_keys_casefold: bool = False
verify_item_name = False
verify_location_name = False
value: typing.Any
@classmethod
def verify_keys(cls, data):
if cls.valid_keys:
data = set(data)
dataset = set(word.casefold() for word in data) if cls.valid_keys_casefold else set(data)
extra = dataset - cls.valid_keys
if extra:
raise Exception(f"Found unexpected key {', '.join(extra)} in {cls}. "
f"Allowed keys: {cls.valid_keys}.")
def verify(self, world):
if self.verify_item_name:
for item_name in self.value:
if item_name not in world.item_names:
raise Exception(f"Item {item_name} from option {self} "
f"is not a valid item name from {world.game}")
elif self.verify_location_name:
for location_name in self.value:
if location_name not in world.world_types[world.game].location_names:
raise Exception(f"Location {location_name} from option {self} "
f"is not a valid location name from {world.game}")
class OptionDict(Option, VerifyKeys):
default = {}
supports_weighting = False
value: typing.Dict[str, typing.Any]
def __init__(self, value: typing.Dict[str, typing.Any]):
self.value: typing.Dict[str, typing.Any] = value
self.value = value
@classmethod
def from_any(cls, data: typing.Dict[str, typing.Any]) -> OptionDict:
if type(data) == dict:
cls.verify_keys(data)
return cls(data)
else:
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
def get_option_name(self):
return str(self.value)
def get_option_name(self, value):
return ", ".join(f"{key}: {v}" for key, v in value.items())
class Logic(Choice):
option_no_glitches = 0
option_minor_glitches = 1
option_overworld_glitches = 2
option_no_logic = 4
alias_owg = 2
def __contains__(self, item):
return item in self.value
class Objective(Choice):
option_crystals = 0
# option_pendants = 1
option_triforce_pieces = 2
option_pedestal = 3
option_bingo = 4
class ItemDict(OptionDict):
# implemented by Generate
verify_item_name = True
def __init__(self, value: typing.Dict[str, int]):
if any(item_count < 1 for item_count in value.values()):
raise Exception("Cannot have non-positive item counts.")
super(ItemDict, self).__init__(value)
class OptionList(Option, VerifyKeys):
default = []
supports_weighting = False
value: list
def __init__(self, value: typing.List[typing.Any]):
self.value = value or []
super(OptionList, self).__init__()
@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:
cls.verify_keys(data)
return cls(data)
return cls.from_text(str(data))
def get_option_name(self, value):
return ", ".join(map(str, value))
def __contains__(self, item):
return item in self.value
class OptionSet(Option, VerifyKeys):
default = frozenset()
supports_weighting = False
value: set
def __init__(self, value: typing.Union[typing.Set[str, typing.Any], typing.List[str, typing.Any]]):
self.value = set(value)
super(OptionSet, self).__init__()
@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:
cls.verify_keys(data)
return cls(data)
elif type(data) == set:
cls.verify_keys(data)
return cls(data)
return cls.from_text(str(data))
def get_option_name(self, value):
return ", ".join(value)
def __contains__(self, item):
return item in self.value
local_objective = Toggle # local triforce pieces, local dungeon prizes etc.
class Goal(Choice):
option_kill_ganon = 0
option_kill_ganon_and_gt_agahnim = 1
option_hand_in = 2
class Accessibility(Choice):
"""Set rules for reachability of your items/locations.
Locations: ensure everything can be reached and acquired.
Items: ensure all logically relevant items can be acquired.
Minimal: ensure what is needed to reach your goal can be acquired."""
display_name = "Accessibility"
option_locations = 0
option_items = 1
option_beatable = 2
class Crystals(Choice):
# can't use IntEnum since there's also random
option_0 = 0
option_1 = 1
option_2 = 2
option_3 = 3
option_4 = 4
option_5 = 5
option_6 = 6
option_7 = 7
option_random = -1
class WorldState(Choice):
option_standard = 1
option_open = 0
option_inverted = 2
class Bosses(Choice):
option_vanilla = 0
option_simple = 1
option_full = 2
option_chaos = 3
option_singularity = 4
class Enemies(Choice):
option_vanilla = 0
option_shuffled = 1
option_chaos = 2
mapshuffle = Toggle
compassshuffle = Toggle
keyshuffle = Toggle
bigkeyshuffle = Toggle
hints = Toggle
RandomizeDreamers = Toggle
RandomizeSkills = Toggle
RandomizeCharms = Toggle
RandomizeKeys = Toggle
RandomizeGeoChests = Toggle
RandomizeMaskShards = Toggle
RandomizeVesselFragments = Toggle
RandomizeCharmNotches = Toggle
RandomizePaleOre = Toggle
RandomizeRancidEggs = Toggle
RandomizeRelics = Toggle
RandomizeMaps = Toggle
RandomizeStags = Toggle
RandomizeGrubs = Toggle
RandomizeWhisperingRoots = Toggle
RandomizeRocks = Toggle
RandomizeSoulTotems = Toggle
RandomizePalaceTotems = Toggle
RandomizeLoreTablets = Toggle
RandomizeLifebloodCocoons = Toggle
RandomizeFlames = Toggle
hollow_knight_randomize_options: typing.Dict[str, Option] = {
"RandomizeDreamers": RandomizeDreamers,
"RandomizeSkills": RandomizeSkills,
"RandomizeCharms": RandomizeCharms,
"RandomizeKeys": RandomizeKeys,
"RandomizeGeoChests": RandomizeGeoChests,
"RandomizeMaskShards": RandomizeMaskShards,
"RandomizeVesselFragments": RandomizeVesselFragments,
"RandomizeCharmNotches": RandomizeCharmNotches,
"RandomizePaleOre": RandomizePaleOre,
"RandomizeRancidEggs": RandomizeRancidEggs,
"RandomizeRelics": RandomizeRelics,
"RandomizeMaps": RandomizeMaps,
"RandomizeStags": RandomizeStags,
"RandomizeGrubs": RandomizeGrubs,
"RandomizeWhisperingRoots": RandomizeWhisperingRoots,
"RandomizeRocks": RandomizeRocks,
"RandomizeSoulTotems": RandomizeSoulTotems,
"RandomizePalaceTotems": RandomizePalaceTotems,
"RandomizeLoreTablets": RandomizeLoreTablets,
"RandomizeLifebloodCocoons": RandomizeLifebloodCocoons,
"RandomizeFlames": RandomizeFlames
}
hollow_knight_skip_options: typing.Dict[str, type(Option)] = {
"MILDSKIPS": Toggle,
"SPICYSKIPS": Toggle,
"FIREBALLSKIPS": Toggle,
"ACIDSKIPS": Toggle,
"SPIKETUNNELS": Toggle,
"DARKROOMS": Toggle,
"CURSED": Toggle,
"SHADESKIPS": Toggle,
}
hollow_knight_options: typing.Dict[str, type(Option)] = {**hollow_knight_randomize_options,
**hollow_knight_skip_options}
class MaxSciencePack(Choice):
option_automation_science_pack = 0
option_logistic_science_pack = 1
option_military_science_pack = 2
option_chemical_science_pack = 3
option_production_science_pack = 4
option_utility_science_pack = 5
option_space_science_pack = 6
default = 6
def get_allowed_packs(self):
return {option.replace("_", "-") for option, value in self.options.items()
if value <= self.value}
class TechCost(Choice):
option_very_easy = 0
option_easy = 1
option_kind = 2
option_normal = 3
option_hard = 4
option_very_hard = 5
option_insane = 6
default = 3
class FreeSamples(Choice):
option_none = 0
option_single_craft = 1
option_half_stack = 2
option_stack = 3
default = 3
class TechTreeLayout(Choice):
option_single = 0
option_small_diamonds = 1
option_medium_diamonds = 2
option_large_diamonds = 3
option_small_pyramids = 4
option_medium_pyramids = 5
option_large_pyramids = 6
option_small_funnels = 7
option_medium_funnels = 8
option_large_funnels = 9
option_funnels = 4
alias_pyramid = 6
alias_funnel = 9
default = 0
class Visibility(Choice):
option_none = 0
option_sending = 1
default = 1
class RecipeTime(Choice):
option_vanilla = 0
option_fast = 1
option_normal = 2
option_slow = 4
option_chaos = 5
class FactorioStartItems(OptionDict):
default = {"burner-mining-drill": 19, "stone-furnace": 19}
factorio_options: typing.Dict[str, type(Option)] = {"max_science_pack": MaxSciencePack,
"tech_tree_layout": TechTreeLayout,
"tech_cost": TechCost,
"free_samples": FreeSamples,
"visibility": Visibility,
"random_tech_ingredients": Toggle,
"starting_items": FactorioStartItems,
"recipe_time": RecipeTime}
class AdvancementGoal(Choice):
option_few = 0
option_normal = 1
option_many = 2
option_minimal = 2
alias_none = 2
default = 1
class CombatDifficulty(Choice):
option_easy = 0
option_normal = 1
option_hard = 2
default = 1
class ProgressionBalancing(DefaultOnToggle):
"""A system that moves progression earlier, to try and prevent the player from getting stuck and bored early."""
display_name = "Progression Balancing"
minecraft_options: typing.Dict[str, type(Option)] = {
"advancement_goal": AdvancementGoal,
"combat_difficulty": CombatDifficulty,
"include_hard_advancements": Toggle,
"include_insane_advancements": Toggle,
"include_postgame_advancements": Toggle,
"shuffle_structures": Toggle
common_options = {
"progression_balancing": ProgressionBalancing,
"accessibility": Accessibility
}
class ItemSet(OptionSet):
# implemented by Generate
verify_item_name = True
class LocalItems(ItemSet):
"""Forces these items to be in their native world."""
display_name = "Local Items"
class NonLocalItems(ItemSet):
"""Forces these items to be outside their native world."""
display_name = "Not Local Items"
class StartInventory(ItemDict):
"""Start with these items."""
verify_item_name = True
display_name = "Start Inventory"
class StartHints(ItemSet):
"""Start with these item's locations prefilled into the !hint command."""
display_name = "Start Hints"
class StartLocationHints(OptionSet):
"""Start with these locations and their item prefilled into the !hint command"""
display_name = "Start Location Hints"
class ExcludeLocations(OptionSet):
"""Prevent these locations from having an important item"""
display_name = "Excluded Locations"
verify_location_name = True
class PriorityLocations(OptionSet):
"""Prevent these locations from having an unimportant item"""
display_name = "Priority Locations"
verify_location_name = True
class DeathLink(Toggle):
"""When you die, everyone dies. Of course the reverse is true too."""
display_name = "Death Link"
class ItemLinks(OptionList):
"""Share part of your item pool with other players."""
default = []
schema = Schema([
{
"name": And(str, len),
"item_pool": [And(str, len)],
"replacement_item": Or(And(str, len), None)
}
])
def verify(self, world):
super(ItemLinks, self).verify(world)
for link in self.value:
for item_name in link["item_pool"]:
if item_name not in world.item_names and item_name not in world.item_name_groups:
raise Exception(f"Item {item_name} from option {self} "
f"is not a valid item name from {world.game}")
if link["replacement_item"] and link["replacement_item"] not in world.item_names:
raise Exception(f"Item {link['replacement_item']} from option {self} "
f"is not a valid item name from {world.game}")
per_game_common_options = {
**common_options, # can be overwritten per-game
"local_items": LocalItems,
"non_local_items": NonLocalItems,
"start_inventory": StartInventory,
"start_hints": StartHints,
"start_location_hints": StartLocationHints,
"exclude_locations": ExcludeLocations,
"priority_locations": PriorityLocations,
"item_links": ItemLinks
}
if __name__ == "__main__":
from worlds.alttp.Options import Logic
import argparse
map_shuffle = Toggle
compass_shuffle = Toggle
key_shuffle = Toggle
big_key_shuffle = Toggle
hints = Toggle
test = argparse.Namespace()
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')
try:
test.logic = Logic.from_text("overworld_glitches_typo")
@@ -388,7 +515,7 @@ if __name__ == "__main__":
test.logic_owg = Logic.from_text("owg")
except KeyError as e:
print(e)
if test.mapshuffle:
print("Mapshuffle is on")
if test.map_shuffle:
print("map_shuffle is on")
print(f"Hints are {bool(test.hints)}")
print(test)

171
Patch.py
View File

@@ -1,8 +1,10 @@
# TODO: convert this into a system like AutoWorld
import shutil
import bsdiff4
import yaml
import os
import lzma
import hashlib
import threading
import concurrent.futures
import zipfile
@@ -10,77 +12,89 @@ import sys
from typing import Tuple, Optional
import Utils
from worlds.alttp.Rom import JAP10HASH
current_patch_version = 2
current_patch_version = 3
GAME_ALTTP = "A Link to the Past"
GAME_SM = "Super Metroid"
GAME_SOE = "Secret of Evermore"
supported_games = {"A Link to the Past", "Super Metroid", "Secret of Evermore"}
preferred_endings = {
GAME_ALTTP: "apbp",
GAME_SM: "apm3",
GAME_SOE: "apsoe"
}
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 generate_yaml(patch: bytes, metadata: Optional[dict] = None, game: str = GAME_ALTTP) -> bytes:
if game == GAME_ALTTP:
from worlds.alttp.Rom import JAP10HASH as HASH
elif game == GAME_SM:
from worlds.sm.Rom import JAP10HASH as HASH
elif game == GAME_SOE:
from worlds.soe.Patch import USHASH as HASH
else:
raise RuntimeError(f"Selected game {game} for base rom not found.")
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:
patch = yaml.dump({"meta": metadata,
"patch": patch,
"game": "A Link to the Past",
"game": game,
# minimum version of patch system expected for patching to be successful
"compatible_version": 1,
"compatible_version": 3,
"version": current_patch_version,
"base_checksum": JAP10HASH})
"base_checksum": HASH})
return patch.encode(encoding="utf-8-sig")
def generate_patch(rom: bytes, metadata: Optional[dict] = None) -> bytes:
def generate_patch(rom: bytes, metadata: Optional[dict] = None, game: str = GAME_ALTTP) -> bytes:
if metadata is None:
metadata = {}
patch = bsdiff4.diff(get_base_rom_bytes(), rom)
return generate_yaml(patch, metadata)
patch = bsdiff4.diff(get_base_rom_data(game), rom)
return generate_yaml(patch, metadata, game)
def create_patch_file(rom_file_to_patch: str, server: str = "", destination: str = None,
player: int = 0, player_name: str = "") -> str:
meta = {"server": server, # allow immediate connection to server in multiworld. Empty string otherwise
player: int = 0, player_name: str = "", game: str = GAME_ALTTP) -> str:
meta = {"server": server, # allow immediate connection to server in multiworld. Empty string otherwise
"player_id": player,
"player_name": player_name}
bytes = generate_patch(load_bytes(rom_file_to_patch),
meta)
target = destination if destination else os.path.splitext(rom_file_to_patch)[0] + ".apbp"
meta,
game)
target = destination if destination else os.path.splitext(rom_file_to_patch)[0] + (
".apbp" if game == GAME_ALTTP else ".apm3")
write_lzma(bytes, target)
return target
def create_rom_bytes(patch_file: str, ignore_version: bool = False) -> Tuple[dict, str, bytearray]:
data = Utils.parse_yaml(lzma.decompress(load_bytes(patch_file)).decode("utf-8-sig"))
game_name = data["game"]
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.")
patched_data = bsdiff4.patch(get_base_rom_bytes(), data["patch"])
patched_data = bsdiff4.patch(get_base_rom_data(game_name), data["patch"])
rom_hash = patched_data[int(0x7FC0):int(0x7FD5)]
data["meta"]["hash"] = "".join(chr(x) for x in rom_hash)
target = os.path.splitext(patch_file)[0] + ".sfc"
return data["meta"], target, patched_data
def get_base_rom_data(game: str):
if game == GAME_ALTTP:
from worlds.alttp.Rom import get_base_rom_bytes
elif game == "alttp": # old version for A Link to the Past
from worlds.alttp.Rom import get_base_rom_bytes
elif game == GAME_SM:
from worlds.sm.Rom import get_base_rom_bytes
elif game == GAME_SOE:
file_name = Utils.get_options()["soe_options"]["rom_file"]
get_base_rom_bytes = lambda: bytes(read_rom(open(file_name, "rb")))
else:
raise RuntimeError("Selected game for base rom not found.")
return get_base_rom_bytes()
def create_rom_file(patch_file: str) -> Tuple[dict, str]:
data, target, patched_data = create_rom_bytes(patch_file)
with open(target, "wb") as f:
@@ -91,7 +105,7 @@ def create_rom_file(patch_file: str) -> Tuple[dict, str]:
def update_patch_data(patch_data: bytes, server: str = "") -> bytes:
data = Utils.parse_yaml(lzma.decompress(patch_data).decode("utf-8-sig"))
data["meta"]["server"] = server
bytes = generate_yaml(data["patch"], data["meta"])
bytes = generate_yaml(data["patch"], data["meta"], data["game"])
return lzma.compress(bytes)
@@ -105,6 +119,14 @@ def write_lzma(data: bytes, path: str):
f.write(data)
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
if __name__ == "__main__":
host = Utils.get_public_ipv4()
options = Utils.get_options()['server_options']
@@ -125,10 +147,63 @@ if __name__ == "__main__":
elif rom.endswith(".apbp"):
print(f"Applying patch {rom}")
data, target = create_rom_file(rom)
romfile, adjusted = Utils.get_adjuster_settings(target)
#romfile, adjusted = Utils.get_adjuster_settings(target)
adjuster_settings = Utils.get_adjuster_settings(GAME_ALTTP)
adjusted = False
if adjuster_settings:
import pprint
from worlds.alttp.Rom import get_base_rom_path
adjuster_settings.rom = target
adjuster_settings.baserom = get_base_rom_path()
adjuster_settings.world = None
whitelist = {"music", "menuspeed", "heartbeep", "heartcolor", "ow_palettes", "quickswap",
"uw_palettes", "sprite", "sword_palettes", "shield_palettes", "hud_palettes",
"reduceflashing", "deathlink"}
printed_options = {name: value for name, value in vars(adjuster_settings).items() if name in whitelist}
if hasattr(adjuster_settings, "sprite_pool"):
sprite_pool = {}
for sprite in getattr(adjuster_settings, "sprite_pool"):
if sprite in sprite_pool:
sprite_pool[sprite] += 1
else:
sprite_pool[sprite] = 1
if sprite_pool:
printed_options["sprite_pool"] = sprite_pool
adjust_wanted = str('no')
if not hasattr(adjuster_settings, 'auto_apply') or 'ask' in adjuster_settings.auto_apply:
adjust_wanted = input(f"Last used adjuster settings were found. Would you like to apply these? \n"
f"{pprint.pformat(printed_options)}\n"
f"Enter yes, no, always or never: ")
if adjuster_settings.auto_apply == 'never': # never adjust, per user request
adjust_wanted = 'no'
elif adjuster_settings.auto_apply == 'always':
adjust_wanted = 'yes'
if adjust_wanted and "never" in adjust_wanted:
adjuster_settings.auto_apply = 'never'
Utils.persistent_store("adjuster", GAME_ALTTP, adjuster_settings)
elif adjust_wanted and "always" in adjust_wanted:
adjuster_settings.auto_apply = 'always'
Utils.persistent_store("adjuster", GAME_ALTTP, adjuster_settings)
if adjust_wanted and adjust_wanted.startswith("y"):
if hasattr(adjuster_settings, "sprite_pool"):
from LttPAdjuster import AdjusterWorld
adjuster_settings.world = AdjusterWorld(getattr(adjuster_settings, "sprite_pool"))
adjusted = True
import LttPAdjuster
_, romfile = LttPAdjuster.adjust(adjuster_settings)
if hasattr(adjuster_settings, "world"):
delattr(adjuster_settings, "world")
else:
adjusted = False
if adjusted:
try:
os.replace(romfile, target)
shutil.move(romfile, target)
romfile = target
except Exception as e:
print(e)
@@ -136,7 +211,13 @@ if __name__ == "__main__":
if 'server' in data:
Utils.persistent_store("servers", data['hash'], data['server'])
print(f"Host is {data['server']}")
elif rom.endswith(".apm3"):
print(f"Applying patch {rom}")
data, target = create_rom_file(rom)
print(f"Created rom {target}.")
if 'server' in data:
Utils.persistent_store("servers", data['hash'], data['server'])
print(f"Host is {data['server']}")
elif rom.endswith(".archipelago"):
import json
import zlib
@@ -162,7 +243,7 @@ if __name__ == "__main__":
def _handle_zip_file_entry(zfinfo: zipfile.ZipInfo, server: str):
data = zfr.read(zfinfo)
if zfinfo.filename.endswith(".apbp"):
if zfinfo.filename.endswith(".apbp") or zfinfo.filename.endswith(".apm3"):
data = update_patch_data(data, server)
with ziplock:
zfw.writestr(zfinfo, data)
@@ -183,4 +264,4 @@ if __name__ == "__main__":
import traceback
traceback.print_exc()
input("Press enter to close.")
input("Press enter to close.")

View File

@@ -6,8 +6,20 @@ Currently, the following games are supported:
* The Legend of Zelda: A Link to the Past
* Factorio
* Minecraft
* Subnautica
* Slay the Spire
* Risk of Rain 2
* The Legend of Zelda: Ocarina of Time
* Timespinner
* Super Metroid
* Secret of Evermore
* Final Fantasy
* Rogue Legacy
* VVVVVV
* Raft
* Super Mario 64
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial).
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
windows binaries.
@@ -29,13 +41,14 @@ Archipelago was directly forked from bonta0's `multiworld_31` branch of ALttPEnt
## Running Archipelago
For most people all you need to do is head over to the [releases](https://github.com/ArchipelagoMW/Archipelago/releases) page then download and run the appropriate installer. The installers function on Windows only.
If you are running Archipelago from a non-Windows system then the likely scenario is that you are comfortable running source code directly. Please see our wiki page on [running Archipelago from source](https://github.com/Berserker66/MultiWorld-Utilities/wiki/Running-from-source).
If you are running Archipelago from a non-Windows system then the likely scenario is that you are comfortable running source code directly. Please see our wiki page on [running Archipelago from source](https://github.com/ArchipelagoMW/Archipelago/wiki/Running-from-source).
## Related Repositories
This project makes use of multiple other projects. We wouldn't be here without these other repositories and the contributions of their developers, past and present.
* [z3randomizer](https://github.com/CaitSith2/z3randomizer)
* [z3randomizer](https://github.com/ArchipelagoMW/z3randomizer)
* [Enemizer](https://github.com/Ijwu/Enemizer)
* [Ocarina of Time Randomizer](https://github.com/TestRunnerSRL/OoT-Randomizer)
## Contributing
Contributions are welcome. We have a few asks of any new contributors.

File diff suppressed because it is too large Load Diff

399
Utils.py
View File

@@ -1,9 +1,20 @@
from __future__ import annotations
import typing
import builtins
import os
import subprocess
import sys
import pickle
import functools
import io
import collections
import importlib
import logging
from tkinter import Tk
def tuplize_version(version: str) -> typing.Tuple[int, ...]:
def tuplize_version(version: str) -> Version:
return Version(*(int(piece, 10) for piece in version.split(".")))
@@ -12,18 +23,11 @@ class Version(typing.NamedTuple):
minor: int
build: int
__version__ = "0.1.2"
_version_tuple = tuplize_version(__version__)
import builtins
import os
import subprocess
import sys
import pickle
import functools
import io
__version__ = "0.2.5"
version_tuple = tuplize_version(__version__)
from yaml import load, dump, safe_load
from yaml import load, dump, SafeLoader
try:
from yaml import CLoader as Loader
@@ -49,26 +53,22 @@ def snes_to_pc(value):
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)):
import collections
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)
def cache_argsless(function):
if function.__code__.co_argcount:
raise Exception("Can only cache 0 argument functions with this cache.")
names = names[players:]
return ret
result = sentinel = object()
def _wrap():
nonlocal result
if result is sentinel:
result = function()
return result
return _wrap
def is_bundled() -> bool:
def is_frozen() -> bool:
return getattr(sys, 'frozen', False)
@@ -76,7 +76,7 @@ def local_path(*path):
if local_path.cached_path:
return os.path.join(local_path.cached_path, *path)
elif is_bundled():
elif is_frozen():
if hasattr(sys, "_MEIPASS"):
# we are running in a PyInstaller bundle
local_path.cached_path = sys._MEIPASS # pylint: disable=protected-access,no-member
@@ -118,135 +118,141 @@ def open_file(filename):
subprocess.call([open_command, filename])
def close_console():
if sys.platform == 'win32':
# windows
import ctypes.wintypes
try:
ctypes.windll.kernel32.FreeConsole()
except Exception:
pass
# from https://gist.github.com/pypt/94d747fe5180851196eb#gistcomment-4015118 with some changes
class UniqueKeyLoader(SafeLoader):
def construct_mapping(self, node, deep=False):
mapping = set()
for key_node, value_node in node.value:
key = self.construct_object(key_node, deep=deep)
if key in mapping:
logging.error(f"YAML duplicates sanity check failed{key_node.start_mark}")
raise KeyError(f"Duplicate key {key} found in YAML. Already found keys: {mapping}.")
mapping.add(key)
return super().construct_mapping(node, deep)
parse_yaml = safe_load
parse_yaml = functools.partial(load, Loader=UniqueKeyLoader)
unsafe_parse_yaml = functools.partial(load, Loader=Loader)
def get_cert_none_ssl_context():
import ssl
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
return ctx
@cache_argsless
def get_public_ipv4() -> str:
import socket
import urllib.request
import logging
ip = socket.gethostbyname(socket.gethostname())
ctx = get_cert_none_ssl_context()
try:
ip = urllib.request.urlopen('https://checkip.amazonaws.com/').read().decode('utf8').strip()
ip = urllib.request.urlopen('https://checkip.amazonaws.com/', context=ctx).read().decode('utf8').strip()
except Exception as e:
try:
ip = urllib.request.urlopen('https://v4.ident.me').read().decode('utf8').strip()
ip = urllib.request.urlopen('https://v4.ident.me', context=ctx).read().decode('utf8').strip()
except:
logging.exception(e)
pass # we could be offline, in a local game, so no point in erroring out
return ip
@cache_argsless
def get_public_ipv6() -> str:
import socket
import urllib.request
import logging
ip = socket.gethostbyname(socket.gethostname())
ctx = get_cert_none_ssl_context()
try:
ip = urllib.request.urlopen('https://v6.ident.me').read().decode('utf8').strip()
ip = urllib.request.urlopen('https://v6.ident.me', context=ctx).read().decode('utf8').strip()
except Exception as e:
logging.exception(e)
pass # we could be offline, in a local game, or ipv6 may not be available
return ip
@cache_argsless
def get_default_options() -> dict:
if not hasattr(get_default_options, "options"):
# Refer to host.yaml for comments as to what all these options mean.
options = {
"general_options": {
"output_path": "output",
},
"factorio_options": {
"executable": "factorio\\bin\\x64\\factorio",
},
"lttp_options": {
"rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc",
"qusb2snes": "QUsb2Snes\\QUsb2Snes.exe",
"rom_start": True,
# Refer to host.yaml for comments as to what all these options mean.
options = {
"general_options": {
"output_path": "output",
},
"factorio_options": {
"executable": os.path.join("factorio", "bin", "x64", "factorio"),
},
"sm_options": {
"rom_file": "Super Metroid (JU).sfc",
"sni": "SNI",
"rom_start": True,
},
"soe_options": {
"rom_file": "Secret of Evermore (USA).sfc",
},
"lttp_options": {
"rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc",
"sni": "SNI",
"rom_start": True,
},
"server_options": {
"host": None,
"port": 38281,
"password": None,
"multidata": None,
"savefile": None,
"disable_save": False,
"loglevel": "info",
"server_password": None,
"disable_item_cheat": False,
"location_check_points": 1,
"hint_cost": 10,
"forfeit_mode": "goal",
"remaining_mode": "goal",
"auto_shutdown": 0,
"compatibility": 2,
"log_network": 0
},
"multi_mystery_options": {
"teams": 1,
"enemizer_path": "EnemizerCLI/EnemizerCLI.Core.exe",
"player_files_path": "Players",
"players": 0,
"weights_file_path": "weights.yaml",
"meta_file_path": "meta.yaml",
"pre_roll": False,
"create_spoiler": 1,
"zip_roms": 0,
"zip_diffs": 2,
"zip_apmcs": 1,
"zip_spoiler": 0,
"zip_multidata": 1,
"zip_format": 1,
"glitch_triforce_room": 1,
"race": 0,
"cpu_threads": 0,
"max_attempts": 0,
"take_first_working": False,
"keep_all_seeds": False,
"log_output_path": "Output Logs",
"log_level": None,
"plando_options": "bosses",
}
},
"server_options": {
"host": None,
"port": 38281,
"password": None,
"multidata": None,
"savefile": None,
"disable_save": False,
"loglevel": "info",
"server_password": None,
"disable_item_cheat": False,
"location_check_points": 1,
"hint_cost": 10,
"forfeit_mode": "goal",
"collect_mode": "disabled",
"remaining_mode": "goal",
"auto_shutdown": 0,
"compatibility": 2,
"log_network": 0
},
"generator": {
"teams": 1,
"enemizer_path": os.path.join("EnemizerCLI", "EnemizerCLI.Core.exe"),
"player_files_path": "Players",
"players": 0,
"weights_file_path": "weights.yaml",
"meta_file_path": "meta.yaml",
"spoiler": 2,
"glitch_triforce_room": 1,
"race": 0,
"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",
}
}
get_default_options.options = options
return get_default_options.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"}
return options
def update_options(src: dict, dest: dict, filename: str, keys: list) -> dict:
import logging
for key, value in src.items():
new_keys = keys.copy()
new_keys.append(key)
option_name = '.'.join(new_keys)
if key not in dest:
dest[key] = value
if filename.endswith("options.yaml") and option_name not in blacklisted_options:
if filename.endswith("options.yaml"):
logging.info(f"Warning: {filename} is missing {option_name}")
elif isinstance(value, dict):
if not isinstance(dest.get(key, None), dict):
if filename.endswith("options.yaml") 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.")
dest[key] = value
else:
@@ -254,6 +260,7 @@ def update_options(src: dict, dest: dict, filename: str, keys: list) -> dict:
return dest
@cache_argsless
def get_options() -> dict:
if not hasattr(get_options, "options"):
locations = ("options.yaml", "host.yaml",
@@ -301,7 +308,6 @@ def persistent_load() -> typing.Dict[dict]:
with open(path, "r") as f:
storage = unsafe_parse_yaml(f.read())
except Exception as e:
import logging
logging.debug(f"Could not read store: {e}")
if storage is None:
storage = {}
@@ -309,65 +315,12 @@ def persistent_load() -> typing.Dict[dict]:
return storage
def get_adjuster_settings(romfile: str) -> typing.Tuple[str, bool]:
if hasattr(get_adjuster_settings, "adjuster_settings"):
adjuster_settings = getattr(get_adjuster_settings, "adjuster_settings")
else:
adjuster_settings = persistent_load().get("adjuster", {}).get("last_settings_3", {})
if adjuster_settings:
import pprint
import Patch
adjuster_settings.rom = romfile
adjuster_settings.baserom = Patch.get_base_rom_path()
adjuster_settings.world = None
whitelist = {"disablemusic", "fastmenu", "heartbeep", "heartcolor", "ow_palettes", "quickswap",
"uw_palettes", "sprite"}
printed_options = {name: value for name, value in vars(adjuster_settings).items() if name in whitelist}
if hasattr(adjuster_settings, "sprite_pool"):
sprite_pool = {}
for sprite in getattr(adjuster_settings, "sprite_pool"):
if sprite in sprite_pool:
sprite_pool[sprite] += 1
else:
sprite_pool[sprite] = 1
if sprite_pool:
printed_options["sprite_pool"] = sprite_pool
if hasattr(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
return romfile, False
else:
adjust_wanted = input(f"Last used adjuster settings were found. Would you like to apply these? \n"
f"{pprint.pformat(printed_options)}\n"
f"Enter yes, no or never: ")
if adjust_wanted and adjust_wanted.startswith("y"):
if hasattr(adjuster_settings, "sprite_pool"):
from LttPAdjuster import AdjusterWorld
adjuster_settings.world = AdjusterWorld(getattr(adjuster_settings, "sprite_pool"))
adjusted = True
import LttPAdjuster
_, romfile = LttPAdjuster.adjust(adjuster_settings)
if hasattr(adjuster_settings, "world"):
delattr(adjuster_settings, "world")
elif adjust_wanted and "never" in adjust_wanted:
persistent_store("adjuster", "never_adjust", True)
return romfile, False
else:
adjusted = False
import logging
if not hasattr(get_adjuster_settings, "adjust_wanted"):
logging.info(f"Skipping post-patch adjustment")
get_adjuster_settings.adjuster_settings = adjuster_settings
get_adjuster_settings.adjust_wanted = adjust_wanted
return romfile, adjusted
return romfile, False
def get_adjuster_settings(gameName: str):
adjuster_settings = persistent_load().get("adjuster", {}).get(gameName, {})
return adjuster_settings
@cache_argsless
def get_unique_identifier():
uuid = persistent_load().get("client", {}).get("uuid", None)
if uuid:
@@ -386,16 +339,28 @@ safe_builtins = {
class RestrictedUnpickler(pickle.Unpickler):
def __init__(self, *args, **kwargs):
super(RestrictedUnpickler, self).__init__(*args, **kwargs)
self.options_module = importlib.import_module("Options")
self.net_utils_module = importlib.import_module("NetUtils")
self.generic_properties_module = importlib.import_module("worlds.generic")
def find_class(self, module, name):
if module == "builtins" and name in safe_builtins:
return getattr(builtins, name)
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint"}:
import NetUtils
return getattr(NetUtils, name)
if module == "Options":
import Options
obj = getattr(Options, name)
if issubclass(obj, Options.Option):
# used by MultiServer -> savegame/multidata
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint", "SlotType", "NetworkSlot"}:
return getattr(self.net_utils_module, name)
# Options and Plando are unpickled by WebHost -> Generate
if module == "worlds.generic" and name in {"PlandoItem", "PlandoConnection"}:
return getattr(self.generic_properties_module, name)
if module.endswith("Options"):
if module == "Options":
mod = self.options_module
else:
mod = importlib.import_module(module)
obj = getattr(mod, name)
if issubclass(obj, self.options_module.Option):
return obj
# Forbid everything else.
raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
@@ -404,4 +369,80 @@ class RestrictedUnpickler(pickle.Unpickler):
def restricted_loads(s):
"""Helper function analogous to pickle.loads()."""
return RestrictedUnpickler(io.BytesIO(s)).load()
return RestrictedUnpickler(io.BytesIO(s)).load()
class KeyedDefaultDict(collections.defaultdict):
def __missing__(self, key):
self[key] = value = self.default_factory(key)
return value
def get_text_between(text: str, start: str, end: str) -> str:
return text[text.index(start) + len(start): text.rindex(end)]
loglevel_mapping = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}
def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, write_mode: str = "w",
log_format: str = "[%(name)s]: %(message)s", exception_logger: str = ""):
loglevel: int = loglevel_mapping.get(loglevel, loglevel)
log_folder = local_path("logs")
os.makedirs(log_folder, exist_ok=True)
root_logger = logging.getLogger()
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
handler.close()
root_logger.setLevel(loglevel)
file_handler = logging.FileHandler(
os.path.join(log_folder, f"{name}.txt"),
write_mode,
encoding="utf-8-sig")
file_handler.setFormatter(logging.Formatter(log_format))
root_logger.addHandler(file_handler)
if sys.stdout:
root_logger.addHandler(
logging.StreamHandler(sys.stdout)
)
# Relay unhandled exceptions to logger.
if not getattr(sys.excepthook, "_wrapped", False): # skip if already modified
orig_hook = sys.excepthook
def handle_exception(exc_type, exc_value, exc_traceback):
if issubclass(exc_type, KeyboardInterrupt):
sys.__excepthook__(exc_type, exc_value, exc_traceback)
return
logging.getLogger(exception_logger).exception("Uncaught exception",
exc_info=(exc_type, exc_value, exc_traceback))
return orig_hook(exc_type, exc_value, exc_traceback)
handle_exception._wrapped = True
sys.excepthook = handle_exception
def stream_input(stream, queue):
def queuer():
while 1:
text = stream.readline().strip()
if text:
queue.put_nowait(text)
from threading import Thread
thread = Thread(target=queuer, name=f"Stream handler for {stream.name}", daemon=True)
thread.start()
return thread
def tkinter_center_window(window: Tk):
window.update()
xPos = int(window.winfo_screenwidth()/2 - window.winfo_reqwidth()/2)
yPos = int(window.winfo_screenheight()/2 - window.winfo_reqheight()/2)
window.geometry("+{}+{}".format(xPos, yPos))
class VersionException(Exception):
pass

View File

@@ -6,11 +6,17 @@ import ModuleUpdate
ModuleUpdate.requirements_files.add(os.path.join("WebHostLib", "requirements.txt"))
ModuleUpdate.update()
# in case app gets imported by something like gunicorn
import Utils
Utils.local_path.cached_path = os.path.dirname(__file__)
from WebHostLib import app as raw_app
from waitress import serve
from WebHostLib.models import db
from WebHostLib.autolauncher import autohost
from WebHostLib.autolauncher import autohost, autogen
from WebHostLib.lttpsprites import update_sprites_lttp
from WebHostLib.options import create as create_options_files
configpath = os.path.abspath("config.yaml")
@@ -30,9 +36,17 @@ if __name__ == "__main__":
multiprocessing.freeze_support()
multiprocessing.set_start_method('spawn')
logging.basicConfig(format='[%(asctime)s] %(message)s', level=logging.INFO)
try:
update_sprites_lttp()
except Exception as e:
logging.exception(e)
logging.warning("Could not update LttP sprites.")
app = get_app()
create_options_files()
if app.config["SELFLAUNCH"]:
autohost(app.config)
if app.config["SELFGEN"]:
autogen(app.config)
if app.config["SELFHOST"]: # using WSGI, you just want to run get_app()
if app.config["DEBUG"]:
autohost(app.config)

View File

@@ -3,10 +3,12 @@ import uuid
import base64
import socket
import jinja2.exceptions
from pony.flask import Pony
from flask import Flask, request, redirect, url_for, render_template, Response, session, abort, send_from_directory
from flask_caching import Cache
from flask_compress import Compress
from worlds.AutoWorld import AutoWorldRegister
from .models import *
@@ -20,13 +22,14 @@ Pony(app)
app.jinja_env.filters['any'] = any
app.jinja_env.filters['all'] = all
app.config["SELFHOST"] = True
app.config["SELFHOST"] = True # application process is in charge of running the websites
app.config["GENERATORS"] = 8 # maximum concurrent world gens
app.config["SELFLAUNCH"] = True
app.config["SELFLAUNCH"] = True # application process is in charge of launching Rooms.
app.config["SELFGEN"] = True # application process is in charge of scheduling Generations.
app.config["DEBUG"] = False
app.config["PORT"] = 80
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 4 * 1024 * 1024 # 4 megabyte limit
app.config['MAX_CONTENT_LENGTH'] = 64 * 1024 * 1024 # 64 megabyte limit
# if you want to deploy, make sure you have a non-guessable secret key
app.config["SECRET_KEY"] = bytes(socket.gethostname(), encoding="utf-8")
# at what amount of worlds should scheduling be used, instead of rolling in the webthread
@@ -74,33 +77,66 @@ def register_session():
session["_id"] = uuid4() # uniquely identify each session without needing a login
@app.errorhandler(404)
@app.errorhandler(jinja2.exceptions.TemplateNotFound)
def page_not_found(err):
return render_template('404.html'), 404
# Start Playing Page
@app.route('/start-playing')
def start_playing():
return render_template(f"startPlaying.html")
@app.route('/weighted-settings')
def weighted_settings():
return render_template(f"weighted-settings.html")
# Player settings pages
@app.route('/games/<string:game>/player-settings')
def player_settings(game):
return render_template(f"player-settings.html", game=game)
# Game Info Pages
@app.route('/games/<string:game>/info/<string:lang>')
def game_info(game, lang):
return render_template('gameInfo.html', game=game, lang=lang)
# List of supported games
@app.route('/games')
def games():
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("supportedGames.html", worlds=worlds)
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
def tutorial(game, file, lang):
return render_template("tutorial.html", game=game, file=file, lang=lang)
@app.route('/tutorial')
@app.route('/tutorial/')
def tutorial_landing():
return render_template("tutorialLanding.html")
@app.route('/player-settings')
def player_settings_simple():
return render_template("playerSettings.html")
@app.route('/weighted-settings')
def player_settings():
return render_template("weightedSettings.html")
@app.route('/faq/<string:lang>/')
def faq(lang):
return render_template("faq.html", lang=lang)
@app.route('/seed/<suuid:seed>')
def viewSeed(seed: UUID):
def view_seed(seed: UUID):
seed = Seed.get(id=seed)
if not seed:
abort(404)
return render_template("viewSeed.html", seed=seed,
rooms=[room for room in seed.rooms if room.owner == session["_id"]])
return render_template("viewSeed.html", seed=seed, slot_count=count(seed.slots))
@app.route('/new_room/<suuid:seed>')
@@ -110,7 +146,7 @@ def new_room(seed: UUID):
abort(404)
room = Room(seed=seed, owner=session["_id"], tracker=uuid4())
commit()
return redirect(url_for("hostRoom", room=room.id))
return redirect(url_for("host_room", room=room.id))
def _read_log(path: str):
@@ -124,20 +160,20 @@ def _read_log(path: str):
@app.route('/log/<suuid:room>')
def display_log(room: UUID):
# noinspection PyTypeChecker
return Response(_read_log(os.path.join("logs", str(room) + ".txt")), mimetype="text/plain;charset=UTF-8")
@app.route('/hosted/<suuid:room>', methods=['GET', 'POST'])
def hostRoom(room: UUID):
@app.route('/room/<suuid:room>', methods=['GET', 'POST'])
def host_room(room: UUID):
room = Room.get(id=room)
if room is None:
return abort(404)
if request.method == "POST":
if room.owner == session["_id"]:
cmd = request.form["cmd"]
Command(room=room, commandtext=cmd)
commit()
if cmd:
Command(room=room, commandtext=cmd)
commit()
with db_session:
room.last_activity = datetime.utcnow() # will trigger a spinup, if it's not already running
@@ -151,6 +187,21 @@ def favicon():
'favicon.ico', mimetype='image/vnd.microsoft.icon')
@app.route('/discord')
def discord():
return redirect("https://discord.gg/archipelago")
@app.route('/datapackage')
@cache.cached()
def get_datapackge():
"""A pretty print version of /api/datapackage"""
from worlds import network_data_package
import json
return Response(json.dumps(network_data_package, indent=4), mimetype="text/plain")
from WebHostLib.customserver import run_server_process
from . import tracker, upload, landing, check, generate, downloads, api # to trigger app routing picking up on it
app.register_blueprint(api.api_endpoints)

View File

@@ -1,24 +1,49 @@
"""API endpoints package."""
from uuid import UUID
from typing import List, Tuple
from flask import Blueprint, abort
from ..models import Room
from ..models import Room, Seed
from .. import cache
api_endpoints = Blueprint('api', __name__, url_prefix="/api")
from . import generate, user # trigger registration
# unsorted/misc endpoints
def get_players(seed: Seed) -> List[Tuple[str, str]]:
return [(slot.player_name, slot.game) for slot in seed.slots]
@api_endpoints.route('/room_status/<suuid:room>')
def room_info(room: UUID):
room = Room.get(id=room)
if room is None:
return abort(404)
return {"tracker": room.tracker,
"players": room.seed.multidata["names"],
"last_port": room.last_port,
"last_activity": room.last_activity,
"timeout": room.timeout}
return {
"tracker": room.tracker,
"players": get_players(room.seed),
"last_port": room.last_port,
"last_activity": room.last_activity,
"timeout": room.timeout
}
@api_endpoints.route('/datapackage')
@cache.cached()
def get_datapackge():
from worlds import network_data_package
return network_data_package
@api_endpoints.route('/datapackage_version')
@cache.cached()
def get_datapackge_versions():
from worlds import network_data_package, AutoWorldRegister
version_package = {game: world.data_version for game, world in AutoWorldRegister.world_types.items()}
version_package["version"] = network_data_package["version"]
return version_package
from . import generate, user # trigger registration

View File

@@ -9,6 +9,7 @@ from pony.orm import commit
from WebHostLib import app, Generation, STATE_QUEUED, Seed, STATE_ERROR
from WebHostLib.check import get_yaml_data, roll_options
from WebHostLib.generate import get_meta
@api_endpoints.route('/generate', methods=['POST'])
@@ -16,7 +17,7 @@ def generate_api():
try:
options = {}
race = False
meta_options_source = {}
if 'file' in request.files:
file = request.files['file']
options = get_yaml_data(file)
@@ -24,14 +25,17 @@ def generate_api():
return {"text": options}, 400
if "race" in request.form:
race = bool(0 if request.form["race"] in {"false"} else int(request.form["race"]))
meta_options_source = request.form
json_data = request.get_json()
if json_data:
meta_options_source = json_data
if 'weights' in json_data:
# example: options = {"player1weights" : {<weightsdata>}}
options = json_data["weights"]
if "race" in json_data:
race = bool(0 if json_data["race"] in {"false"} else int(json_data["race"]))
if not options:
return {"text": "No options found. Expected file attachment or json weights."
}, 400
@@ -39,7 +43,8 @@ def generate_api():
if len(options) > app.config["MAX_ROLL"]:
return {"text": "Max size of multiworld exceeded",
"detail": app.config["MAX_ROLL"]}, 409
meta = get_meta(meta_options_source)
meta["race"] = race
results, gen_options = roll_options(options)
if any(type(result) == str for result in results.values()):
return {"text": str(results),
@@ -48,7 +53,7 @@ def generate_api():
gen = Generation(
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
# convert to json compatible
meta=json.dumps({"race": race}), state=STATE_QUEUED,
meta=json.dumps(meta), state=STATE_QUEUED,
owner=session["_id"])
commit()
return {"text": f"Generation of seed {gen.id} started successfully.",
@@ -60,7 +65,6 @@ def generate_api():
return {"text": "Uncaught Exception:" + str(e)}, 500
@api_endpoints.route('/status/<suuid:seed>')
def wait_seed_api(seed: UUID):
seed_id = seed

View File

@@ -1,7 +1,7 @@
from flask import session, jsonify
from WebHostLib.models import *
from . import api_endpoints
from . import api_endpoints, get_players
@api_endpoints.route('/get_rooms')
@@ -16,7 +16,6 @@ def get_rooms():
"last_port": room.last_port,
"timeout": room.timeout,
"tracker": room.tracker,
"players": room.seed.multidata["names"] if room.seed.multidata else [["Singleplayer"]],
})
return jsonify(response)
@@ -28,6 +27,6 @@ def get_seeds():
response.append({
"seed_id": seed.id,
"creation_time": seed.creation_time,
"players": seed.multidata["names"] if seed.multidata else [["Singleplayer"]],
"players": get_players(seed.slots),
})
return jsonify(response)

View File

@@ -7,6 +7,7 @@ import concurrent.futures
import sys
import typing
import time
import os
from pony.orm import db_session, select, commit
@@ -15,10 +16,13 @@ from Utils import restricted_loads
class CommonLocker():
"""Uses a file lock to signal that something is already running"""
def __init__(self, lockname: str):
lock_folder = "file_locks"
def __init__(self, lockname: str, folder=None):
if folder:
self.lock_folder = folder
os.makedirs(self.lock_folder, exist_ok=True)
self.lockname = lockname
self.lockfile = f"./{self.lockname}.lck"
self.lockfile = os.path.join(self.lock_folder, f"{self.lockname}.lck")
class AlreadyRunningException(Exception):
@@ -26,9 +30,6 @@ class AlreadyRunningException(Exception):
if sys.platform == 'win32':
import os
class Locker(CommonLocker):
def __enter__(self):
try:
@@ -88,14 +89,14 @@ def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
options = restricted_loads(generation.options)
logging.info(f"Generating {generation.id} for {len(options)} players")
pool.apply_async(gen_game, (options,),
{"race": meta["race"],
{"meta": meta,
"sid": generation.id,
"owner": generation.owner},
handle_generation_success, handle_generation_failure)
except:
except Exception as e:
generation.state = STATE_ERROR
commit()
raise
logging.exception(e)
else:
generation.state = STATE_STARTED
@@ -109,6 +110,26 @@ def autohost(config: dict):
def keep_running():
try:
with Locker("autohost"):
while 1:
time.sleep(0.1)
with db_session:
rooms = select(
room for room in Room if
room.last_activity >= datetime.utcnow() - timedelta(days=3))
for room in rooms:
launch_room(room, config)
except AlreadyRunningException:
logging.info("Autohost reports as already running, not starting another.")
import threading
threading.Thread(target=keep_running, name="AP_Autohost").start()
def autogen(config: dict):
def keep_running():
try:
with Locker("autogen"):
with multiprocessing.Pool(config["GENERATORS"], initializer=init_db,
initargs=(config["PONY"],)) as generator_pool:
@@ -128,22 +149,17 @@ def autohost(config: dict):
select(generation for generation in Generation if generation.state == STATE_ERROR).delete()
while 1:
time.sleep(0.50)
time.sleep(0.1)
with db_session:
rooms = select(
room for room in Room if
room.last_activity >= datetime.utcnow() - timedelta(days=3))
for room in rooms:
launch_room(room, config)
to_start = select(
generation for generation in Generation if generation.state == STATE_QUEUED)
for generation in to_start:
launch_generator(generator_pool, generation)
except AlreadyRunningException:
pass
logging.info("Autogen reports as already running, not starting another.")
import threading
threading.Thread(target=keep_running).start()
threading.Thread(target=keep_running, name="AP_Autogen").start()
multiworlds = {}

View File

@@ -12,7 +12,7 @@ def allowed_file(filename):
return filename.endswith(('.txt', ".yaml", ".zip"))
from Mystery import roll_settings
from Generate import roll_settings
from Utils import parse_yaml
@@ -49,9 +49,7 @@ def get_yaml_data(file) -> Union[Dict[str, str], str]:
for file in infolist:
if file.filename.endswith(banned_zip_contents):
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. Your file was deleted."
elif file.filename.endswith(".yaml"):
options[file.filename] = zfile.open(file, "r").read()
elif file.filename.endswith(".txt"):
elif file.filename.endswith((".yaml", ".json", ".yml", ".txt")):
options[file.filename] = zfile.open(file, "r").read()
else:
options = {file.filename: file.read()}
@@ -73,7 +71,8 @@ def roll_options(options: Dict[str, Union[dict, str]]) -> Tuple[Dict[str, Union[
results[filename] = f"Failed to parse YAML data in {filename}: {e}"
else:
try:
rolled_results[filename] = roll_settings(yaml_data, plando_options={"bosses"})
rolled_results[filename] = roll_settings(yaml_data,
plando_options={"bosses", "items", "connections", "texts"})
except Exception as e:
results[filename] = f"Failed to generate mystery in {filename}: {e}"
else:

View File

@@ -2,7 +2,6 @@ from __future__ import annotations
import functools
import logging
import os
import websockets
import asyncio
import socket
@@ -11,7 +10,7 @@ import time
import random
import pickle
import Utils
from .models import *
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor
@@ -20,6 +19,7 @@ from Utils import get_public_ipv4, get_public_ipv6, restricted_loads
class CustomClientMessageProcessor(ClientMessageProcessor):
ctx: WebHostContext
def _cmd_video(self, platform, user):
"""Set a link for your name in the WebHostLib tracker pointing to a video stream"""
if platform.lower().startswith("t"): # twitch
@@ -37,6 +37,7 @@ class CustomClientMessageProcessor(ClientMessageProcessor):
# inject
import MultiServer
MultiServer.client_message_processor = CustomClientMessageProcessor
del (MultiServer)
@@ -48,7 +49,7 @@ class DBCommandProcessor(ServerCommandProcessor):
class WebHostContext(Context):
def __init__(self):
super(WebHostContext, self).__init__("", 0, "", "", 1, 40, True, "enabled", "enabled", 0, 2)
super(WebHostContext, self).__init__("", 0, "", "", 1, 40, True, "enabled", "enabled", "enabled", 0, 2)
self.main_loop = asyncio.get_running_loop()
self.video = {}
self.tags = ["AP", "WebHost"]
@@ -56,7 +57,7 @@ class WebHostContext(Context):
def listen_to_db_commands(self):
cmdprocessor = DBCommandProcessor(self)
while self.running:
while not self.exit_event.is_set():
with db_session:
commands = select(command for command in Command if command.room.id == self.room_id)
if commands:
@@ -75,7 +76,7 @@ class WebHostContext(Context):
else:
self.port = get_random_port()
return self._load(self._decompress(room.seed.multidata), True)
return self._load(self.decompress(room.seed.multidata), True)
@db_session
def init_save(self, enabled: bool = True):
@@ -88,11 +89,11 @@ class WebHostContext(Context):
threading.Thread(target=self.listen_to_db_commands, daemon=True).start()
@db_session
def _save(self, exit_save:bool = False) -> bool:
def _save(self, exit_save: bool = False) -> bool:
room = Room.get(id=self.room_id)
room.multisave = pickle.dumps(self.get_save())
# saving only occurs on activity, so we can "abuse" this information to mark this as last_activity
if not exit_save: # we don't want to count a shutdown as activity, which would restart the server again
if not exit_save: # we don't want to count a shutdown as activity, which would restart the server again
room.last_activity = datetime.utcnow()
return True
@@ -101,20 +102,18 @@ class WebHostContext(Context):
d["video"] = [(tuple(playerslot), videodata) for playerslot, videodata in self.video.items()]
return d
def get_random_port():
return random.randint(49152, 65535)
def run_server_process(room_id, ponyconfig: dict):
# establish DB connection for multidata and multisave
db.bind(**ponyconfig)
db.generate_mapping(check_tables=False)
async def main():
logging.basicConfig(format='[%(asctime)s] %(message)s',
level=logging.INFO,
handlers=[
logging.FileHandler(os.path.join(LOGS_FOLDER, f"{room_id}.txt"), 'a', 'utf-8-sig')])
Utils.init_logging(str(room_id), write_mode="a")
ctx = WebHostContext()
ctx.load(room_id)
ctx.init_save()
@@ -144,7 +143,9 @@ def run_server_process(room_id, ponyconfig: dict):
await ctx.shutdown_task
logging.info("Shutting down")
asyncio.run(main())
from .autolauncher import Locker
with Locker(room_id):
asyncio.run(main())
from WebHostLib import LOGS_FOLDER

View File

@@ -1,10 +1,11 @@
from flask import send_file, Response
from flask import send_file, Response, render_template
from pony.orm import select
from Patch import update_patch_data
from WebHostLib import app, Slot, Room, Seed
from Patch import update_patch_data, preferred_endings
from WebHostLib import app, Slot, Room, Seed, cache
import zipfile
@app.route("/dl_patch/<suuid:room_id>/<int:patch_id>")
def download_patch(room_id, patch_id):
patch = Slot.get(id=patch_id)
@@ -19,7 +20,8 @@ def download_patch(room_id, patch_id):
patch_data = update_patch_data(patch.data, server=f"{app.config['PATCH_TARGET']}:{last_port}")
patch_data = io.BytesIO(patch_data)
fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}.apbp"
fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}." \
f"{preferred_endings[patch.game]}"
return send_file(patch_data, as_attachment=True, attachment_filename=fname)
@@ -28,27 +30,10 @@ def download_spoiler(seed_id):
return Response(Seed.get(id=seed_id).spoiler, mimetype="text/plain")
@app.route("/dl_raw_patch/<suuid:seed_id>/<int:player_id>")
def download_raw_patch(seed_id, player_id: int):
seed = Seed.get(id=seed_id)
patch = select(patch for patch in seed.slots if
patch.player_id == player_id).first()
if not patch:
return "Patch not found"
else:
import io
patch_data = update_patch_data(patch.data, server="")
patch_data = io.BytesIO(patch_data)
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)
@app.route("/slot_file/<suuid:seed_id>/<int:player_id>")
def download_slot_file(seed_id, player_id: int):
seed = Seed.get(id=seed_id)
slot_data: Slot = select(patch for patch in seed.slots if
@app.route("/slot_file/<suuid:room_id>/<int:player_id>")
def download_slot_file(room_id, player_id: int):
room = Room.get(id=room_id)
slot_data: Slot = select(patch for patch in room.seed.slots if
patch.player_id == player_id).first()
if not slot_data:
@@ -57,12 +42,27 @@ def download_slot_file(seed_id, player_id: int):
import io
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":
with zipfile.ZipFile(io.BytesIO(slot_data.data)) as zf:
for name in zf.namelist():
if name.endswith("info.json"):
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:
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

@@ -2,19 +2,33 @@ import os
import tempfile
import random
import json
import zipfile
from collections import Counter
from typing import Dict, Optional as TypeOptional
from Utils import __version__
from flask import request, flash, redirect, url_for, session, render_template
from worlds.alttp.EntranceRandomizer import parse_arguments
from Main import main as ERmain
from Main import get_seed, seeddigits
from Mystery import handle_name
from BaseClasses import seeddigits, get_seed
from Generate import handle_name
import pickle
from .models import *
from WebHostLib import app
from .check import get_yaml_data, roll_options
from .upload import upload_zip_to_db
def get_meta(options_source: dict) -> dict:
meta = {
"hint_cost": int(options_source.get("hint_cost", 10)),
"forfeit_mode": options_source.get("forfeit_mode", "goal"),
"remaining_mode": options_source.get("forfeit_mode", "disabled"),
"collect_mode": options_source.get("collect_mode", "disabled"),
}
return meta
@app.route('/generate', methods=['GET', 'POST'])
@@ -31,6 +45,14 @@ def generate(race=False):
flash(options)
else:
results, gen_options = roll_options(options)
# get form data -> server settings
meta = get_meta(request.form)
meta["race"] = race
if race:
meta["item_cheat"] = False
meta["remaining"] = False
if any(type(result) == str for result in results.values()):
return render_template("checkResult.html", results=results)
elif len(gen_options) > app.config["MAX_ROLL"]:
@@ -40,7 +62,8 @@ def generate(race=False):
gen = Generation(
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
# convert to json compatible
meta=json.dumps({"race": race}), state=STATE_QUEUED,
meta=json.dumps(meta),
state=STATE_QUEUED,
owner=session["_id"])
commit()
@@ -48,18 +71,24 @@ def generate(race=False):
else:
try:
seed_id = gen_game({name: vars(options) for name, options in gen_options.items()},
race=race, owner=session["_id"].int)
meta=meta, owner=session["_id"].int)
except BaseException as e:
from .autolauncher import handle_generation_failure
handle_generation_failure(e)
return render_template("seedError.html", seed_error=(e.__class__.__name__ + ": "+ str(e)))
return render_template("seedError.html", seed_error=(e.__class__.__name__ + ": " + str(e)))
return redirect(url_for("viewSeed", seed=seed_id))
return redirect(url_for("view_seed", seed=seed_id))
return render_template("generate.html", race=race)
return render_template("generate.html", race=race, version=__version__)
def gen_game(gen_options, race=False, owner=None, sid=None):
def gen_game(gen_options, meta: TypeOptional[Dict[str, object]] = None, owner=None, sid=None):
if not meta:
meta: Dict[str, object] = {}
meta.setdefault("hint_cost", 10)
race = meta.get("race", False)
del (meta["race"])
try:
target = tempfile.TemporaryDirectory()
playercount = len(gen_options)
@@ -69,34 +98,34 @@ def gen_game(gen_options, race=False, owner=None, sid=None):
if race:
random.seed() # reset to time-based random source
seedname = "M" + (f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits))
seedname = "W" + (f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits))
erargs = parse_arguments(['--multi', str(playercount)])
erargs.seed = seed
erargs.name = {x: "" for x in range(1, playercount + 1)} # only so it can be overwrittin in mystery
erargs.create_spoiler = not race
erargs.spoiler = 0 if race else 2
erargs.race = race
erargs.skip_playthrough = race
erargs.outputname = seedname
erargs.outputpath = target.name
erargs.teams = 1
erargs.create_diff = True
name_counter = Counter()
for player, (playerfile, settings) in enumerate(gen_options.items(), 1):
for k, v in settings.items():
if v is not None:
getattr(erargs, k)[player] = v
if hasattr(erargs, k):
getattr(erargs, k)[player] = v
else:
setattr(erargs, k, {player: v})
if not erargs.name[player]:
erargs.name[player] = os.path.splitext(os.path.split(playerfile)[-1])[0]
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
if len(set(erargs.name.values())) != len(erargs.name):
raise Exception(f"Names have to be unique. Names: {Counter(erargs.name.values())}")
ERmain(erargs, seed, baked_server_options=meta)
erargs.names = ",".join(erargs.name[i] for i in range(1, playercount + 1))
del (erargs.name)
ERmain(erargs, seed)
return upload_to_db(target.name, owner, sid, race)
return upload_to_db(target.name, sid, owner, race)
except BaseException as e:
if sid:
with db_session:
@@ -104,7 +133,7 @@ def gen_game(gen_options, race=False, owner=None, sid=None):
if gen is not None:
gen.state = STATE_ERROR
meta = json.loads(gen.meta)
meta["error"] = (e.__class__.__name__ + ": "+ str(e))
meta["error"] = (e.__class__.__name__ + ": " + str(e))
gen.meta = json.dumps(meta)
commit()
@@ -116,7 +145,7 @@ def wait_seed(seed: UUID):
seed_id = seed
seed = Seed.get(id=seed_id)
if seed:
return redirect(url_for("viewSeed", seed=seed_id))
return redirect(url_for("view_seed", seed=seed_id))
generation = Generation.get(id=seed_id)
if not generation:
@@ -126,37 +155,19 @@ def wait_seed(seed: UUID):
return render_template("waitSeed.html", seed_id=seed_id)
def upload_to_db(folder, owner, sid, race:bool):
slots = set()
spoiler = ""
multidata = None
def upload_to_db(folder, sid, owner, race):
for file in os.listdir(folder):
file = os.path.join(folder, file)
if file.endswith(".apbp"):
player_text = file.split("_P", 1)[1]
player_name = player_text.split("_", 1)[1].split(".", 1)[0]
player_id = int(player_text.split(".", 1)[0].split("_", 1)[0])
slots.add(Slot(data=open(file, "rb").read(),
player_id=player_id, player_name = player_name, game = "A Link to the Past"))
elif file.endswith(".txt"):
spoiler = open(file, "rt", encoding="utf-8-sig").read()
elif file.endswith(".archipelago"):
multidata = open(file, "rb").read()
if multidata:
with db_session:
if sid:
seed = Seed(multidata=multidata, spoiler=spoiler, slots=slots, owner=owner,
id=sid, meta=json.dumps({"race": race, "tags": ["generated"]}))
else:
seed = Seed(multidata=multidata, spoiler=spoiler, slots=slots, owner=owner,
meta=json.dumps({"race": race, "tags": ["generated"]}))
for patch in slots:
patch.seed = seed
if sid:
gen = Generation.get(id=sid)
if gen is not None:
gen.delete()
return seed.id
else:
raise Exception("Multidata required (.archipelago), but not found.")
if file.endswith(".zip"):
with db_session:
with zipfile.ZipFile(file) as zfile:
res = upload_zip_to_db(zfile, owner, {"race": race}, sid)
if type(res) == "str":
raise Exception(res)
elif res:
seed = res
gen = Generation.get(id=seed.id)
if gen is not None:
gen.delete()
return seed.id
raise Exception("Generation zipfile not found.")

50
WebHostLib/lttpsprites.py Normal file
View File

@@ -0,0 +1,50 @@
import os
import threading
import json
from Utils import local_path
from worlds.alttp.Rom import Sprite
def update_sprites_lttp():
from tkinter import Tk
from LttPAdjuster import get_image_for_sprite
from LttPAdjuster import BackgroundTaskProgress
from LttPAdjuster import BackgroundTaskProgressNullWindow
from LttPAdjuster import update_sprites
# Target directories
input_dir = local_path("data", "sprites", "alttpr")
output_dir = local_path("WebHostLib", "static", "generated")
os.makedirs(os.path.join(output_dir, "sprites"), exist_ok=True)
# update sprites through gui.py's functions
done = threading.Event()
try:
top = Tk()
except:
task = BackgroundTaskProgressNullWindow(update_sprites, lambda successful, resultmessage: done.set())
else:
top.withdraw()
task = BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
while not done.isSet():
task.do_events()
spriteData = []
for file in os.listdir(input_dir):
sprite = Sprite(os.path.join(input_dir, file))
if not sprite.name:
print("Warning:", file, "has no name.")
sprite.name = file.split(".", 1)[0]
if sprite.valid:
with open(os.path.join(output_dir, "sprites", f"{os.path.splitext(file)[0]}.gif"), 'wb') as image:
image.write(get_image_for_sprite(sprite, True))
spriteData.append({"file": file, "author": sprite.author_name, "name": sprite.name})
else:
print(file, "dropped, as it has no valid sprite data.")
spriteData.sort(key=lambda entry: entry["name"])
with open(f'{output_dir}/spriteData.json', 'w') as file:
json.dump({"sprites": spriteData}, file, indent=1)
return spriteData

View File

@@ -40,7 +40,7 @@ class Seed(db.Entity):
creation_time = Required(datetime, default=lambda: datetime.utcnow())
slots = Set(Slot)
spoiler = Optional(LongStr, lazy=True)
meta = Required(str, default=lambda: "{\"race\": false}") # additional meta information/tags
meta = Required(LongStr, default=lambda: "{\"race\": false}") # additional meta information/tags
class Command(db.Entity):
@@ -53,5 +53,5 @@ class Generation(db.Entity):
id = PrimaryKey(UUID, default=uuid4)
owner = Required(UUID)
options = Required(buffer, lazy=True)
meta = Required(str, default=lambda: "{\"race\": false}")
meta = Required(LongStr, default=lambda: "{\"race\": false}")
state = Required(int, default=0, index=True)

143
WebHostLib/options.py Normal file
View File

@@ -0,0 +1,143 @@
import logging
import os
from Utils import __version__
from jinja2 import Template
import yaml
import json
from worlds.AutoWorld import AutoWorldRegister
import Options
target_folder = os.path.join("WebHostLib", "static", "generated")
handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hints", "start_location_hints",
"exclude_locations"}
def create():
os.makedirs(os.path.join(target_folder, 'configs'), exist_ok=True)
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
def default_converter(default_value):
if isinstance(default_value, (set, frozenset)):
return list(default_value)
return default_value
weighted_settings = {
"baseOptions": {
"description": "Generated by https://archipelago.gg/",
"name": "Player",
"game": {},
},
"games": {},
}
for game_name, world in AutoWorldRegister.world_types.items():
all_options = {**world.options, **Options.per_game_common_options}
res = Template(open(os.path.join("WebHostLib", "templates", "options.yaml")).read()).render(
options=all_options,
__version__=__version__, game=game_name, yaml_dump=yaml.dump,
dictify_range=dictify_range, default_converter=default_converter,
)
with open(os.path.join(target_folder, 'configs', game_name + ".yaml"), "w") as f:
f.write(res)
# Generate JSON files for player-settings pages
player_settings = {
"baseOptions": {
"description": "Generated by https://archipelago.gg/",
"game": game_name,
"name": "Player",
},
}
game_options = {}
for option_name, option in all_options.items():
if option_name in handled_in_js:
pass
elif option.options:
game_options[option_name] = this_option = {
"type": "select",
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
"description": option.__doc__ if option.__doc__ else "Please document me!",
"defaultValue": None,
"options": []
}
for sub_option_id, sub_option_name in option.name_lookup.items():
this_option["options"].append({
"name": option.get_option_name(sub_option_id),
"value": sub_option_name,
})
if sub_option_id == option.default:
this_option["defaultValue"] = sub_option_name
this_option["options"].append({
"name": "Random",
"value": "random",
})
elif hasattr(option, "range_start") and hasattr(option, "range_end"):
game_options[option_name] = {
"type": "range",
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
"description": option.__doc__ if option.__doc__ else "Please document me!",
"defaultValue": option.default if hasattr(option, "default") else option.range_start,
"min": option.range_start,
"max": option.range_end,
}
elif getattr(option, "verify_item_name", False):
game_options[option_name] = {
"type": "items-list",
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
"description": option.__doc__ if option.__doc__ else "Please document me!",
}
elif getattr(option, "verify_location_name", False):
game_options[option_name] = {
"type": "locations-list",
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
"description": option.__doc__ if option.__doc__ else "Please document me!",
}
elif hasattr(option, "valid_keys"):
if option.valid_keys:
game_options[option_name] = {
"type": "custom-list",
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
"description": option.__doc__ if option.__doc__ else "Please document me!",
"options": list(option.valid_keys),
}
else:
logging.debug(f"{option} not exported to Web Settings.")
player_settings["gameOptions"] = game_options
os.makedirs(os.path.join(target_folder, 'player-settings'), exist_ok=True)
with open(os.path.join(target_folder, 'player-settings', game_name + ".json"), "w") as f:
json.dump(player_settings, f, indent=2, separators=(',', ': '))
if not world.hidden:
weighted_settings["baseOptions"]["game"][game_name] = 0
weighted_settings["games"][game_name] = {}
weighted_settings["games"][game_name]["gameSettings"] = game_options
weighted_settings["games"][game_name]["gameItems"] = tuple(world.item_names)
weighted_settings["games"][game_name]["gameLocations"] = tuple(world.location_names)
with open(os.path.join(target_folder, 'weighted-settings.json'), "w") as f:
json.dump(weighted_settings, f, indent=2, separators=(',', ': '))

View File

@@ -1,6 +1,6 @@
flask>=2.0.1
pony>=0.7.14
flask>=2.0.3
pony>=0.7.16
waitress>=2.0.0
flask-caching>=1.10.1
Flask-Compress>=1.9.0
Flask-Limiter>=1.4
Flask-Compress>=1.10.1
Flask-Limiter>=2.1.3

View File

@@ -0,0 +1,53 @@
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
showdown.setOption('tables', true);
showdown.setOption('strikethrough', true);
showdown.setOption('literalMidWordUnderscores', true);
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,63 @@
# Frequently Asked Questions
## What is a randomizer?
A randomizer is a modification of a video game which reorganizes the items required to progress through the game. A
normal play-through of a game might require you to use item A to unlock item B, then C, and so forth. In a randomized
game, you might first find item C, then A, then B.
This transforms games from a linear experience into a puzzle, presenting players with a new challenge each time they
play a randomized game. Putting items in non-standard locations can require the player to think about the game world and
the items they encounter in new and interesting ways.
## What happens if an item is placed somewhere it is impossible to get?
The randomizer has many strict sets of rules it must follow when generating a game. One of the functions of these rules
is to ensure items necessary to complete the game will be accessible to the player. Many games also have a subset of
rules allowing certain items to be placed in normally unreachable locations, provided the player has indicated they are
comfortable exploiting certain glitches in the game.
## What is a multi-world?
While a randomizer shuffles a game, a multi-world randomizer shuffles that game for multiple players. For example, in a
two player multi-world, players A and B each get their own randomized version of a game, called seeds. In each player's
game, they may find items which belong to the other player. If player A finds an item which belongs to player B, the
item will be sent to player B's world over the internet.
This creates a cooperative experience during multi-world games, requiring players to rely upon each other to complete
their game.
## What happens if a person has to leave early?
If a player must leave early, they can use Archipelago's forfeit system. When a player forfeits their game, all the
items in that game which belong to other players are sent out automatically, so other players can continue to play.
## What does multi-game mean?
While a multi-world game traditionally requires all players to be playing the same game, a multi-game multi-world allows
players to randomize any of a number of supported games, and send items between them. This allows players of different
games to interact with one another in a single multiplayer environment.
## Can I generate a single-player game with Archipelago?
Yes. All our supported games can be generated as single-player experiences, and so long as you download the software,
the website is not required to generate them.
## How do I get started?
If you are ready to start randomizing games, or want to start playing your favorite randomizer with others, please join
our discord server at the [Archipelago Discord](https://discord.gg/archipelago). There are always people ready to answer
any questions you might have.
## I want to add a game to the Archipelago randomizer. How do I do that?
The best way to get started is to take a look at our code on GitHub
at [Archipelago GitHub Page](https://github.com/ArchipelagoMW/Archipelago).
There you will find examples of games in the worlds folder
at [/worlds Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/worlds).
You may also find developer documentation in the docs folder
at [/docs Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs).
If you have more questions, feel free to ask in the **#archipelago-dev** channel on our Discord.

View File

@@ -0,0 +1,53 @@
window.addEventListener('load', () => {
const gameInfo = document.getElementById('game-info');
new Promise((resolve, reject) => {
const ajax = new XMLHttpRequest();
ajax.onreadystatechange = () => {
if (ajax.readyState !== 4) { return; }
if (ajax.status === 404) {
reject("Sorry, this game's info page is not available in that language yet.");
return;
}
if (ajax.status !== 200) {
reject("Something went wrong while loading the info page.");
return;
}
resolve(ajax.responseText);
};
ajax.open('GET', `${window.location.origin}/static/assets/gameInfo/` +
`${gameInfo.getAttribute('data-lang')}_${gameInfo.getAttribute('data-game')}.md`, true);
ajax.send();
}).then((results) => {
// Populate page with HTML generated from markdown
showdown.setOption('tables', true);
showdown.setOption('strikethrough', true);
showdown.setOption('literalMidWordUnderscores', true);
gameInfo.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);
gameInfo.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,32 @@
# A Link to the Past
## Where is the settings page?
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
Items which the player would normally acquire throughout the game have been moved around. Logic remains, so the game is
always able to be completed, but because of the item shuffle the player may need to access certain areas before they
would in the vanilla game.
## What items and locations get shuffled?
All main inventory items, collectables, and ammunition can be shuffled, and all locations in the game which could
contain any of those items may have their contents changed.
## Which items can be in another player's world?
Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to limit
certain items to your own world.
## What does another world's item look like in LttP?
Items belonging to other worlds are represented by a Power Star from Super Mario World.
## When the player receives an item, what happens?
When the player receives an item, Link will hold the item above his head and display it to the world. It's good for
business!

View File

@@ -0,0 +1,34 @@
# Factorio
## Where is the settings page?
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
In Factorio, the research tree is shuffled, causing certain technologies to be obtained in a non-standard order. Recipe
costs, technology requirements, and science pack requirements may also be shuffled at the player's discretion.
## What Factorio items can appear in other players' worlds?
Factorio's technologies are removed from its tech tree and placed into other players' worlds. When those technologies
are found, they are sent back to Factorio along with, optionally, free samples of those technologies.
## What is a free sample?
A free sample is a single or stack of items in Factorio, granted by a technology received from another world. For
example, receiving the technology `Portable Solar Panel` may also grant the player a stack of portable solar panels, and
place them directly into the player's inventory.
## What does another world's item look like in Factorio?
In Factorio, items which need to be sent to other worlds appear in the tech tree as new research items. They are
represented by the Archipelago icon, and must be researched as if it were a normal technology. Upon successful
completion of research, the item will be sent to its home world.
## When the engineer receives an item, what happens?
When the player receives a technology, it is instantly learned and able to be crafted. A message will appear in the chat
log to notify the player, and if free samples are enabled the player may also receive some items directly to their
inventory.

View File

@@ -0,0 +1,26 @@
# Final Fantasy 1 (NES)
## Where is the settings page?
Unlike most games on Archipelago.gg, Final Fantasy 1's settings are controlled entirely by the original randomzier. You
can find an exhaustive list of documented settings on the FFR
website: [FF1R Website](https://finalfantasyrandomizer.com/)
## What does randomization do to this game?
A better questions is what isn't randomized at this point. Enemies stats and spell, character spells, shop inventory and
boss stats and spells are all commonly randomized. Unlike most other randomizers it is also most standard to shuffle
progression items and non-progression items into separate pools and then redistribute them to their respective
locations. So, for example, Princess Sarah may have the CANOE instead of the LUTE; however, she will never have a Heal
Pot or some armor. There are plenty of other things that can be randomized on the main randomizer
site: [FF1R Website](https://finalfantasyrandomizer.com/)
## What Final Fantasy items can appear in other players' worlds?
All items can appear in other players worlds. This includes consumables, shards, weapons, armor and, of course, key
items.
## What does another world's item look like in Final Fantasy
All local and remote items appear the same. It will say that you received an item and then BOTH the client log and the
emulator will display what was found external to the in-game text box.

View File

@@ -0,0 +1,27 @@
# Minecraft
## Where is the settings page?
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
Recipes are removed from the crafting book and shuffled into the item pool. It can also optionally change which
structures appear in each dimension. Crafting recipes are re-learned when they are received from other players as item
checks, and occasionally when completing your own achievements.
## What is considered a location check in minecraft?
Location checks in are completed when the player completes various Minecraft achievements. Opening the advancements menu
in-game by pressing "L" will display outstanding achievements.
## When the player receives an item, what happens?
When the player receives an item in Minecraft, it either unlocks crafting recipes or puts items into the player's
inventory directly.
## What is the victory condition?
Victory is achieved when the player kills the Ender Dragon, enters the portal in The End, and completes the credits
sequence either by skipping it or watching hit play out.

View File

@@ -0,0 +1,32 @@
# Ocarina of Time
## Where is the settings page?
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
Items which the player would normally acquire throughout the game have been moved around. Logic remains, so the game is
always able to be completed, but because of the item shuffle the player may need to access certain areas before they
would in the vanilla game.
## What items and locations get shuffled?
All main inventory items, collectables, and ammunition can be shuffled, and all locations in the game which could
contain any of those items may have their contents changed. Gold Skultulla locations may also be included as necessary
checks at the user's discretion.
## Which items can be in another player's world?
Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to limit
certain items to your own world.
## What does another world's item look like in OoT?
Items belonging to other worlds are represented by the Zelda's Letter item.
## When the player receives an item, what happens?
When the player receives an item, Link will hold the item above his head and display it to the world. It's good for
business!

View File

@@ -0,0 +1,31 @@
# Raft
## Where is the settings page?
The player settings page for this game is located <a href="../player-settings">here</a>. It contains all the options
you need to configure and export a config file.
## What does randomization do to this game?
All of the items from the Research Table, as well as all the note/blueprint pickups from story islands, are changed to location checks. Blueprint items themselves are never given. The Research Table recipes will *remove* the researched items for that recipe once learned, meaning many more resources must be put into the Research Table to get all the unlocks from it.
## What is the goal of Raft when randomized?
The goal remains the same: To pick up the note that has the frequency for the next unreleased story island from Tangaroa.
## Which items can be in another player's world?
All of the craftable items from the Research Table and Blueprints, as well as frequencies. Since there are more locations in Raft than there are items to receive, Resource Packs with basic earlygame materials and/or duplicate items may be added to the item pool (configurable).
## Which notable unlocks are not randomized?
Most of the story island quests (actions that unlock new areas on the island) remain unchanged. There are three exceptions: The Balboa Island Relay Station quest, the Caravan Island zipline parts quest, and the Caravan Island battery charger quest have all been changed to an Archipelago unlock, as the rewards from these are craftable items or frequencies.
Craftable items like the Machete are mixed into the Archipelago item pool, however quest items like Tape or Berries will function the same.
Decoration Packages are unchanged.
## What does another world's item look like in Raft?
Researches and pickups remain visually unchanged, regardless of what the unlock is.
## When the player receives an item, what happens?
A Raft notification will appear with the item information. The unlock will also appear in the chat. Unlocks that would normally give you the item (eg Machete) will NOT give it to you, but must instead be crafted.
## Are there any limitations compared to vanilla Raft?
- Mods that add new researchable technologies, modify story islands, or give items like blueprints are likely incompatible with Raftipelago.
- Some mods that add items that are always craftable (eg don't add them to the Research Table) may be compatible.
- Mods that do not affect items, notes, blueprints, or story islands have a good chance of being compatible with Raftipelago
- No mods have been comprehensively tested or verified to work with Raftipelago. Use at your own risk.

View File

@@ -0,0 +1,43 @@
# Risk of Rain 2
## Where is the settings page?
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
Risk of Rain is already a random game, by virtue of being a roguelite. The Archipelago mod implements pure multiworld
functionality in which certain chests (made clear via a location check progress bar) will send an item out to the
multiworld. The items that _would have been_ in those chests will be returned to the Risk of Rain player via grants by
other players in other worlds.
## What Risk of Rain items can appear in other players' worlds?
The Risk of Rain items are:
* `Common Item` (White items)
* `Uncommon Item` (Green items)
* `Boss Item` (Yellow items)
* `Legendary Item` (Red items)
* `Lunar Item` (Blue items)
* `Equipment` (Orange items)
* `Dio's Best Friend` (Used if you set the YAML setting `total_revives_available` above `0`)
Each item grants you a random in-game item from the category it belongs to.
When an item is granted by another world to the Risk of Rain player (one of the items listed above) then a random
in-game item of that tier will appear in the Risk of Rain player's inventory. If the item grant is an `Equipment` and
the player already has an equipment item equipped then the _item that was equipped_ will be dropped on the ground and _
the new equipment_ will take it's place. (If you want the old one back, pick it up.)
## What does another world's item look like in Risk of Rain?
When the Risk of Rain player fills up their location check bar then the next spawned item will become an item grant for
another player's world. The item in Risk of Rain will disappear in a poof of smoke and the grant will automatically go
out to the multiworld.
## What is the item pickup step?
The item pickup step is a YAML setting which allows you to set how many items you need to spawn before the _next_ item
that is spawned disappears (in a poof of smoke) and goes out to the multiworld.

View File

@@ -0,0 +1,27 @@
# Rogue Legacy (PC)
## Where is the settings page?
The [player settings page for this game](../player-settings) is located contains all the options you need to configure
and export a config file.
## What does randomization do to this game?
You are not able to buy skill upgrades in the manor upgrade screen, and instead, need to find them in order to level up
your character to make fighting the 5 bosses easier.
## What items and locations get shuffled?
All the skill upgrades, class upgrades, runes packs, and equipment packs are shuffled in the manor upgrade screen, diary
checks, chests and fairy chests, and boss rewards. Skill upgrades are also grouped in packs of 5 to make the finding of
stats less of a chore. Runes and Equipment are also grouped together.
## Which items can be in another player's world?
Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to limit
certain items to your own world.
## When the player receives an item, what happens?
When the player receives an item, your character will hold the item above their head and display it to the world. It's
good for business!

View File

@@ -0,0 +1,35 @@
# Secret of Evermore
## Where is the settings page?
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
Items which would normally be acquired throughout the game have been moved around! Progression logic remains, so the
game is always able to be completed. However, because of the item shuffle, the player may need to access certain areas
before they would in the vanilla game. For example, the Windwalker (flying machine) is accessible as soon as any weapon
is obtained.
Additional help can be found in the [Evermizer guide](https://github.com/black-sliver/evermizer/blob/master/guide.md).
## What items and locations get shuffled?
All gourds/chests/pots, boss drops and alchemists are shuffled. Alchemy ingredients, sniff spot items, call bead spells
and the dog can be randomized using yaml options.
## Which items can be in another player's world?
Any of the items which can be shuffled may also be placed in another player's world. Specific items can be limited to
your own world using plando.
## What does another world's item look like in Secret of Evermore?
Secret of Evermore will display "Sent an Item". Check the client output if you want to know which.
## What happens when the player receives an item?
When the player receives an item, a popup will appear to show which item was received. Items won't be received while a
script is active such as when visiting Nobilia Market or during most Boss Fights. Once all scripts have ended, items
will be received.

View File

@@ -0,0 +1,35 @@
# Slay the Spire (PC)
## Where is the settings page?
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
Every non-boss relic drop, every boss relic and rare card drop, and every other card draw is replaced with an
archipelago item. In heart runs, the blue key is also disconnected from the Archipelago item, so you can gather both.
## What items and locations get shuffled?
15 card packs, 10 relics, and 3 boss relics and rare card drops are shuffled into the item pool and can be found at any
location that would normally give you these items, except for card packs, which are found at every other normal enemy
encounter.
## Which items can be in another player's world?
Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to limit
certain items to your own world.
## When the player receives an item, what happens?
When the player receives an item, you will see the counter in the top right corner with the Archipelago symbol increment
by one. By clicking on this icon, it'll open a menu that lists all the items you received, but have not yet accepted.
You can take any relics and card packs sent to you and add them to your current run. It is advised that you do not open
this menu until you are outside an encounter or event to prevent the game from soft-locking.
## What happens if a player dies in a run?
When a player dies, they will be taken back to the main menu and will need to reconnect to start climbing the spire from
the beginning, but they will have access to all the items ever sent to them in the Archipelago menu in the top right.
Any items found in an earlier run will not be sent again if you encounter them in the same location.

View File

@@ -0,0 +1,34 @@
# Subnautica
## Where is the settings page?
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
The most noticeable change is the complete removal of freestanding technologies. The technology blueprints normally
awarded from scanning those items have been shuffled into location checks throughout the AP item pool.
## What is the goal of Subnautica when randomized?
The goal remains unchanged. Cure the plague, build the Neptune Escape Rocket, and escape into space.
## What items and locations get shuffled?
Most of the technologies the player will need throughout the game will be shuffled. Location checks in Subnautica are
data pads and technology lockers.
## Which items can be in another player's world?
Most technologies may be shuffled into another player's world.
## What does another world's item look like in Subnautica?
Location checks in Subnautica are data pads and technology lockers. Opening one of these will send an item to another
player's world.
## When the player receives a technology, what happens?
When the player receives a technology, the chat log displays a notification the technology has been received.

View File

@@ -0,0 +1,28 @@
# Super Mario 64 EX
## Where is the settings page?
The player settings page for this game contains all the options you need to configure and export a config file. Player
settings page link: [SM64EX Player Settings Page](../player-settings).
## What does randomization do to this game?
All 120 Stars, the 3 Cap Switches, the Basement and Secound Floor Key are now Location Checks and may contain Items for different games as well
as different Items from within SM64.
## What is the goal of SM64EX when randomized?
As in most Mario Games, save the Princess!
## Which items can be in another player's world?
Any of the 120 Stars, and the two Caste Keys. Additionally, Cap Switches are also considered "Items" and the "!"-Boxes will only be active
when someone collects the corresponding Cap Switch Item.
## What does another world's item look like in SM64EX?
The Items are visually unchanged, though after collecting a Message will pop up to inform you what you collected,
and who will receive it.
## When the player receives an item, what happens?
When you receive an Item, a Message will pop up to inform you where you received the Item from,
and which one it is.
NOTE: The Secret Star count in the Menu is broken.

View File

@@ -0,0 +1,31 @@
# Super Metroid
## Where is the settings page?
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
Items which the player would normally acquire throughout the game have been moved around. Logic remains, so the game is
always able to be completed, but because of the item shuffle the player may need to access certain areas before they
would in the vanilla game.
## What items and locations get shuffled?
All power-ups and ammunition can be shuffled, and all locations in the game which could contain any of those items may
have their contents changed.
## Which items can be in another player's world?
Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to limit
certain items to your own world.
## What does another world's item look like in Super Metroid?
A unique item sprite has been added to the game to represent items belonging to another world.
## When the player receives an item, what happens?
When the player receives an item, a text box will appear to show which item was received, and from whom.

View File

@@ -0,0 +1,38 @@
# Timespinner
## Where is the settings page?
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
Items which the player would normally acquire throughout the game have been moved around. Logic remains, so the game is
always able to be completed, but because of the item shuffle the player may need to access certain areas before they
would in the vanilla game. All rings and spells are also randomized into those item locations, therefore you can no
longer craft them at the alchemist
## What is the goal of Timespinner when randomized?
The goal remains unchanged. Kill the Sandman\Nightmare!
## What items and locations get shuffled?
All main inventory items, orbs, collectables, and familiars can be shuffled, and all locations in the game which could
contain any of those items may have their contents changed.
## Which items can be in another player's world?
Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to limit
certain items to your own world.
## What does another world's item look like in Timespinner?
Items belonging to other worlds are represented by the vanilla item Elemental
Beads ([Elemental Beads Wiki Page](https://timespinnerwiki.com/Use_Items)), Elemental Beads have no use in the
randomizer.
## When the player receives an item, what happens?
When the player receives an item, the same items popup will be displayed as when you would normally obtain the item.

View File

@@ -0,0 +1,36 @@
# VVVVVV
## Where is the settings page?
The player settings page for this game contains all the options you need to configure and export a config file. Player
settings page link: [VVVVVV Player Settings Page](../player-settings).
## What does randomization do to this game?
All 20 Trinkets are now Location Checks and may not actually contain Trinkets, but Items for different games.
Optionally, you may enable DoorCost, which will gate away some areas:
- Laboratory
- The Tower
- Space Station 2 and
- Warp Zone
until you've collected some Trinkets.
Examples:
- If you set DoorCost at 2, then to enter Laboratory you will need Trinkets 1-2, for The Tower 3-4, etc.
- If you set DoorCost at 3, then to enter Laboratory you will need Trinkets 1-3, for The Tower 4-6, etc.
## What is the goal of VVVVVV when randomized?
Save all crew members, and finish the story.
## Which items can be in another player's world?
Any of the 20 Trinkets.
## What does another world's item look like in VVVVVV?
The Trinkets are visually unchanged, though after collecting a textbox will pop up to inform you what you collected,
and who will receive it.
## When the player receives an item, what happens?
When you receive a Trinket, the standard Animation will play. Afterwards a textbox will inform you where
you received the Trinket from, and which one it is.
NOTE: You can't check your trinkets in the Spaceship. Instead, you can check them in the pause menu under 'Stats'.
This is especially useful if you have DoorCost enabled.

2
WebHostLib/static/assets/md5.min.js vendored Normal file
View File

@@ -0,0 +1,2 @@
// Copyright © 2011 Sebastian Tschan, https://blueimp.net
!function(n){"use strict";function d(n,t){var r=(65535&n)+(65535&t);return(n>>16)+(t>>16)+(r>>16)<<16|65535&r}function f(n,t,r,e,o,u){return d((c=d(d(t,n),d(e,u)))<<(f=o)|c>>>32-f,r);var c,f}function l(n,t,r,e,o,u,c){return f(t&r|~t&e,n,t,o,u,c)}function v(n,t,r,e,o,u,c){return f(t&e|r&~e,n,t,o,u,c)}function g(n,t,r,e,o,u,c){return f(t^r^e,n,t,o,u,c)}function m(n,t,r,e,o,u,c){return f(r^(t|~e),n,t,o,u,c)}function i(n,t){var r,e,o,u;n[t>>5]|=128<<t%32,n[14+(t+64>>>9<<4)]=t;for(var c=1732584193,f=-271733879,i=-1732584194,a=271733878,h=0;h<n.length;h+=16)c=l(r=c,e=f,o=i,u=a,n[h],7,-680876936),a=l(a,c,f,i,n[h+1],12,-389564586),i=l(i,a,c,f,n[h+2],17,606105819),f=l(f,i,a,c,n[h+3],22,-1044525330),c=l(c,f,i,a,n[h+4],7,-176418897),a=l(a,c,f,i,n[h+5],12,1200080426),i=l(i,a,c,f,n[h+6],17,-1473231341),f=l(f,i,a,c,n[h+7],22,-45705983),c=l(c,f,i,a,n[h+8],7,1770035416),a=l(a,c,f,i,n[h+9],12,-1958414417),i=l(i,a,c,f,n[h+10],17,-42063),f=l(f,i,a,c,n[h+11],22,-1990404162),c=l(c,f,i,a,n[h+12],7,1804603682),a=l(a,c,f,i,n[h+13],12,-40341101),i=l(i,a,c,f,n[h+14],17,-1502002290),c=v(c,f=l(f,i,a,c,n[h+15],22,1236535329),i,a,n[h+1],5,-165796510),a=v(a,c,f,i,n[h+6],9,-1069501632),i=v(i,a,c,f,n[h+11],14,643717713),f=v(f,i,a,c,n[h],20,-373897302),c=v(c,f,i,a,n[h+5],5,-701558691),a=v(a,c,f,i,n[h+10],9,38016083),i=v(i,a,c,f,n[h+15],14,-660478335),f=v(f,i,a,c,n[h+4],20,-405537848),c=v(c,f,i,a,n[h+9],5,568446438),a=v(a,c,f,i,n[h+14],9,-1019803690),i=v(i,a,c,f,n[h+3],14,-187363961),f=v(f,i,a,c,n[h+8],20,1163531501),c=v(c,f,i,a,n[h+13],5,-1444681467),a=v(a,c,f,i,n[h+2],9,-51403784),i=v(i,a,c,f,n[h+7],14,1735328473),c=g(c,f=v(f,i,a,c,n[h+12],20,-1926607734),i,a,n[h+5],4,-378558),a=g(a,c,f,i,n[h+8],11,-2022574463),i=g(i,a,c,f,n[h+11],16,1839030562),f=g(f,i,a,c,n[h+14],23,-35309556),c=g(c,f,i,a,n[h+1],4,-1530992060),a=g(a,c,f,i,n[h+4],11,1272893353),i=g(i,a,c,f,n[h+7],16,-155497632),f=g(f,i,a,c,n[h+10],23,-1094730640),c=g(c,f,i,a,n[h+13],4,681279174),a=g(a,c,f,i,n[h],11,-358537222),i=g(i,a,c,f,n[h+3],16,-722521979),f=g(f,i,a,c,n[h+6],23,76029189),c=g(c,f,i,a,n[h+9],4,-640364487),a=g(a,c,f,i,n[h+12],11,-421815835),i=g(i,a,c,f,n[h+15],16,530742520),c=m(c,f=g(f,i,a,c,n[h+2],23,-995338651),i,a,n[h],6,-198630844),a=m(a,c,f,i,n[h+7],10,1126891415),i=m(i,a,c,f,n[h+14],15,-1416354905),f=m(f,i,a,c,n[h+5],21,-57434055),c=m(c,f,i,a,n[h+12],6,1700485571),a=m(a,c,f,i,n[h+3],10,-1894986606),i=m(i,a,c,f,n[h+10],15,-1051523),f=m(f,i,a,c,n[h+1],21,-2054922799),c=m(c,f,i,a,n[h+8],6,1873313359),a=m(a,c,f,i,n[h+15],10,-30611744),i=m(i,a,c,f,n[h+6],15,-1560198380),f=m(f,i,a,c,n[h+13],21,1309151649),c=m(c,f,i,a,n[h+4],6,-145523070),a=m(a,c,f,i,n[h+11],10,-1120210379),i=m(i,a,c,f,n[h+2],15,718787259),f=m(f,i,a,c,n[h+9],21,-343485551),c=d(c,r),f=d(f,e),i=d(i,o),a=d(a,u);return[c,f,i,a]}function a(n){for(var t="",r=32*n.length,e=0;e<r;e+=8)t+=String.fromCharCode(n[e>>5]>>>e%32&255);return t}function h(n){var t=[];for(t[(n.length>>2)-1]=void 0,e=0;e<t.length;e+=1)t[e]=0;for(var r=8*n.length,e=0;e<r;e+=8)t[e>>5]|=(255&n.charCodeAt(e/8))<<e%32;return t}function e(n){for(var t,r="0123456789abcdef",e="",o=0;o<n.length;o+=1)t=n.charCodeAt(o),e+=r.charAt(t>>>4&15)+r.charAt(15&t);return e}function r(n){return unescape(encodeURIComponent(n))}function o(n){return a(i(h(t=r(n)),8*t.length));var t}function u(n,t){return function(n,t){var r,e,o=h(n),u=[],c=[];for(u[15]=c[15]=void 0,16<o.length&&(o=i(o,8*n.length)),r=0;r<16;r+=1)u[r]=909522486^o[r],c[r]=1549556828^o[r];return e=i(u.concat(h(t)),512+8*t.length),a(i(c.concat(e),640))}(r(n),r(t))}function t(n,t,r){return t?r?u(t,n):e(u(t,n)):r?o(n):e(o(n))}"function"==typeof define&&define.amd?define(function(){return t}):"object"==typeof module&&module.exports?module.exports=t:n.md5=t}(this);

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

@@ -0,0 +1,52 @@
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, small keys, and boss keys in the location-table
const types = ['counter', 'smallkeys', 'bosskeys'];
for (let j = 0; j < types.length; j++) {
let counters = document.getElementsByClassName(types[j]);
const fakeCounters = fakeDOM.getElementsByClassName(types[j]);
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

@@ -0,0 +1,248 @@
let gameName = null;
window.addEventListener('load', () => {
gameName = document.getElementById('player-settings').getAttribute('data-game');
// Update game name on page
document.getElementById('game-name').innerText = gameName;
Promise.all([fetchSettingData()]).then((results) => {
let settingHash = localStorage.getItem(`${gameName}-hash`);
if (!settingHash) {
// If no hash data has been set before, set it now
localStorage.setItem(`${gameName}-hash`, md5(results[0]));
localStorage.removeItem(gameName);
settingHash = md5(results[0]);
}
if (settingHash !== md5(results[0])) {
showUserMessage("Your settings are out of date! Click here to update them! Be aware this will reset " +
"them all to default.");
document.getElementById('user-message').addEventListener('click', resetSettings);
}
// Page setup
createDefaultSettings(results[0]);
buildUI(results[0]);
adjustHeaderWidth();
// Event listeners
document.getElementById('export-settings').addEventListener('click', () => exportSettings());
document.getElementById('generate-race').addEventListener('click', () => generateGame(true));
document.getElementById('generate-game').addEventListener('click', () => generateGame());
// Name input field
const playerSettings = JSON.parse(localStorage.getItem(gameName));
const nameInput = document.getElementById('player-name');
nameInput.addEventListener('keyup', (event) => updateBaseSetting(event));
nameInput.value = playerSettings.name;
}).catch((error) => {
const url = new URL(window.location.href);
window.location.replace(`${url.protocol}//${url.hostname}/page-not-found`);
})
});
const resetSettings = () => {
localStorage.removeItem(gameName);
localStorage.removeItem(`${gameName}-hash`)
window.location.reload();
};
const fetchSettingData = () => new Promise((resolve, reject) => {
const ajax = new XMLHttpRequest();
ajax.onreadystatechange = () => {
if (ajax.readyState !== 4) { return; }
if (ajax.status !== 200) {
reject(ajax.responseText);
return;
}
try{ resolve(JSON.parse(ajax.responseText)); }
catch(error){ reject(error); }
};
ajax.open('GET', `${window.location.origin}/static/generated/player-settings/${gameName}.json`, true);
ajax.send();
});
const createDefaultSettings = (settingData) => {
if (!localStorage.getItem(gameName)) {
const newSettings = {
[gameName]: {},
};
for (let baseOption of Object.keys(settingData.baseOptions)){
newSettings[baseOption] = settingData.baseOptions[baseOption];
}
for (let gameOption of Object.keys(settingData.gameOptions)){
newSettings[gameName][gameOption] = settingData.gameOptions[gameOption].defaultValue;
}
localStorage.setItem(gameName, JSON.stringify(newSettings));
}
};
const buildUI = (settingData) => {
// Game Options
const leftGameOpts = {};
const rightGameOpts = {};
Object.keys(settingData.gameOptions).forEach((key, index) => {
if (index < Object.keys(settingData.gameOptions).length / 2) { leftGameOpts[key] = settingData.gameOptions[key]; }
else { rightGameOpts[key] = settingData.gameOptions[key]; }
});
document.getElementById('game-options-left').appendChild(buildOptionsTable(leftGameOpts));
document.getElementById('game-options-right').appendChild(buildOptionsTable(rightGameOpts));
};
const buildOptionsTable = (settings, romOpts = false) => {
const currentSettings = JSON.parse(localStorage.getItem(gameName));
const table = document.createElement('table');
const tbody = document.createElement('tbody');
Object.keys(settings).forEach((setting) => {
const tr = document.createElement('tr');
// td Left
const tdl = document.createElement('td');
const label = document.createElement('label');
label.setAttribute('for', setting);
label.setAttribute('data-tooltip', settings[setting].description);
label.innerText = `${settings[setting].displayName}:`;
tdl.appendChild(label);
tr.appendChild(tdl);
// td Right
const tdr = document.createElement('td');
let element = null;
switch(settings[setting].type){
case 'select':
element = document.createElement('div');
element.classList.add('select-container');
let select = document.createElement('select');
select.setAttribute('id', setting);
select.setAttribute('data-key', setting);
if (romOpts) { select.setAttribute('data-romOpt', '1'); }
settings[setting].options.forEach((opt) => {
const option = document.createElement('option');
option.setAttribute('value', opt.value);
option.innerText = opt.name;
if ((isNaN(currentSettings[gameName][setting]) &&
(parseInt(opt.value, 10) === parseInt(currentSettings[gameName][setting]))) ||
(opt.value === currentSettings[gameName][setting]))
{
option.selected = true;
}
select.appendChild(option);
});
select.addEventListener('change', (event) => updateGameSetting(event));
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);
tbody.appendChild(tr);
});
table.appendChild(tbody);
return table;
};
const updateBaseSetting = (event) => {
const options = JSON.parse(localStorage.getItem(gameName));
options[event.target.getAttribute('data-key')] = isNaN(event.target.value) ?
event.target.value : parseInt(event.target.value);
localStorage.setItem(gameName, JSON.stringify(options));
};
const updateGameSetting = (event) => {
const options = JSON.parse(localStorage.getItem(gameName));
options[gameName][event.target.getAttribute('data-key')] = isNaN(event.target.value) ?
event.target.value : parseInt(event.target.value, 10);
localStorage.setItem(gameName, JSON.stringify(options));
};
const exportSettings = () => {
const settings = JSON.parse(localStorage.getItem(gameName));
if (!settings.name || settings.name.toLowerCase() === 'player' || settings.name.trim().length === 0) {
return showUserMessage('You must enter a player name!');
}
const yamlText = jsyaml.safeDump(settings, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`);
download(`${document.getElementById('player-name').value}.yaml`, yamlText);
};
/** Create an anchor and trigger a download of a text file. */
const download = (filename, text) => {
const downloadLink = document.createElement('a');
downloadLink.setAttribute('href','data:text/yaml;charset=utf-8,'+ encodeURIComponent(text))
downloadLink.setAttribute('download', filename);
downloadLink.style.display = 'none';
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
};
const generateGame = (raceMode = false) => {
const settings = JSON.parse(localStorage.getItem(gameName));
if (!settings.name || settings.name.toLowerCase() === 'player' || settings.name.trim().length === 0) {
return showUserMessage('You must enter a player name!');
}
axios.post('/api/generate', {
weights: { player: settings },
presetData: { player: settings },
playerCount: 1,
race: raceMode ? '1' : '0',
}).then((response) => {
window.location.href = response.data.url;
}).catch((error) => {
let userMessage = 'Something went wrong and your game could not be generated.';
if (error.response.data.text) {
userMessage += ' ' + error.response.data.text;
}
showUserMessage(userMessage);
console.error(error);
});
};
const showUserMessage = (message) => {
const userMessage = document.getElementById('user-message');
userMessage.innerText = message;
userMessage.classList.add('visible');
window.scrollTo(0, 0);
userMessage.addEventListener('click', () => {
userMessage.classList.remove('visible');
userMessage.addEventListener('click', hideUserMessage);
});
};
const hideUserMessage = () => {
const userMessage = document.getElementById('user-message');
userMessage.classList.remove('visible');
userMessage.removeEventListener('click', hideUserMessage);
};

View File

@@ -1,194 +0,0 @@
window.addEventListener('load', () => {
Promise.all([fetchSettingData(), fetchSpriteData()]).then((results) => {
// Page setup
createDefaultSettings(results[0]);
buildUI(results[0]);
adjustHeaderWidth();
// Event listeners
document.getElementById('export-settings').addEventListener('click', () => exportSettings());
document.getElementById('generate-race').addEventListener('click', () => generateGame(true))
document.getElementById('generate-game').addEventListener('click', () => generateGame());
// Name input field
const playerSettings = JSON.parse(localStorage.getItem('playerSettings'));
const nameInput = document.getElementById('player-name');
nameInput.addEventListener('keyup', (event) => updateSetting(event));
nameInput.value = playerSettings.name;
// Sprite options
const spriteData = JSON.parse(results[1]);
const spriteSelect = document.getElementById('sprite');
spriteData.sprites.forEach((sprite) => {
if (sprite.name.trim().length === 0) { return; }
const option = document.createElement('option');
option.setAttribute('value', sprite.name.trim());
if (playerSettings.rom.sprite === sprite.name.trim()) { option.selected = true; }
option.innerText = sprite.name;
spriteSelect.appendChild(option);
});
}).catch((error) => {
console.error(error);
})
});
const fetchSettingData = () => new Promise((resolve, reject) => {
const ajax = new XMLHttpRequest();
ajax.onreadystatechange = () => {
if (ajax.readyState !== 4) { return; }
if (ajax.status !== 200) {
reject(ajax.responseText);
return;
}
try{ resolve(JSON.parse(ajax.responseText)); }
catch(error){ reject(error); }
};
ajax.open('GET', `${window.location.origin}/static/static/playerSettings.json`, true);
ajax.send();
});
const createDefaultSettings = (settingData) => {
if (!localStorage.getItem('playerSettings')) {
const newSettings = {};
for (let roSetting of Object.keys(settingData.readOnly)){
newSettings[roSetting] = settingData.readOnly[roSetting];
}
for (let generalOption of Object.keys(settingData.generalOptions)){
newSettings[generalOption] = settingData.generalOptions[generalOption];
}
for (let gameOption of Object.keys(settingData.gameOptions)){
newSettings[gameOption] = settingData.gameOptions[gameOption].defaultValue;
}
newSettings.rom = {};
for (let romOption of Object.keys(settingData.romOptions)){
newSettings.rom[romOption] = settingData.romOptions[romOption].defaultValue;
}
localStorage.setItem('playerSettings', JSON.stringify(newSettings));
}
};
const buildUI = (settingData) => {
// Game Options
const leftGameOpts = {};
const rightGameOpts = {};
Object.keys(settingData.gameOptions).forEach((key, index) => {
if (index < Object.keys(settingData.gameOptions).length / 2) { leftGameOpts[key] = settingData.gameOptions[key]; }
else { rightGameOpts[key] = settingData.gameOptions[key]; }
});
document.getElementById('game-options-left').appendChild(buildOptionsTable(leftGameOpts));
document.getElementById('game-options-right').appendChild(buildOptionsTable(rightGameOpts));
// ROM Options
const leftRomOpts = {};
const rightRomOpts = {};
Object.keys(settingData.romOptions).forEach((key, index) => {
if (index < Object.keys(settingData.romOptions).length / 2) { leftRomOpts[key] = settingData.romOptions[key]; }
else { rightRomOpts[key] = settingData.romOptions[key]; }
});
document.getElementById('rom-options-left').appendChild(buildOptionsTable(leftRomOpts, true));
document.getElementById('rom-options-right').appendChild(buildOptionsTable(rightRomOpts, true));
};
const buildOptionsTable = (settings, romOpts = false) => {
const currentSettings = JSON.parse(localStorage.getItem('playerSettings'));
const table = document.createElement('table');
const tbody = document.createElement('tbody');
Object.keys(settings).forEach((setting) => {
const tr = document.createElement('tr');
// td Left
const tdl = document.createElement('td');
const label = document.createElement('label');
label.setAttribute('for', setting);
label.setAttribute('data-tooltip', settings[setting].description);
label.innerText = `${settings[setting].friendlyName}:`;
tdl.appendChild(label);
tr.appendChild(tdl);
// td Right
const tdr = document.createElement('td');
const select = document.createElement('select');
select.setAttribute('id', setting);
select.setAttribute('data-key', setting);
if (romOpts) { select.setAttribute('data-romOpt', '1'); }
settings[setting].options.forEach((opt) => {
const option = document.createElement('option');
option.setAttribute('value', opt.value);
option.innerText = opt.name;
if ((isNaN(currentSettings[setting]) && (parseInt(opt.value, 10) === parseInt(currentSettings[setting]))) ||
(opt.value === currentSettings[setting])) {
option.selected = true;
}
select.appendChild(option);
});
select.addEventListener('change', (event) => updateSetting(event));
tdr.appendChild(select);
tr.appendChild(tdr);
tbody.appendChild(tr);
});
table.appendChild(tbody);
return table;
};
const updateSetting = (event) => {
const options = JSON.parse(localStorage.getItem('playerSettings'));
if (event.target.getAttribute('data-romOpt')) {
options.rom[event.target.getAttribute('data-key')] = isNaN(event.target.value) ?
event.target.value : parseInt(event.target.value, 10);
} else {
options[event.target.getAttribute('data-key')] = isNaN(event.target.value) ?
event.target.value : parseInt(event.target.value, 10);
}
localStorage.setItem('playerSettings', JSON.stringify(options));
};
const exportSettings = () => {
const settings = JSON.parse(localStorage.getItem('playerSettings'));
if (!settings.name || settings.name.trim().length === 0) { settings.name = "noname"; }
const yamlText = jsyaml.safeDump(settings, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`);
download(`${document.getElementById('player-name').value}.yaml`, yamlText);
};
/** Create an anchor and trigger a download of a text file. */
const download = (filename, text) => {
const downloadLink = document.createElement('a');
downloadLink.setAttribute('href','data:text/yaml;charset=utf-8,'+ encodeURIComponent(text))
downloadLink.setAttribute('download', filename);
downloadLink.style.display = 'none';
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
};
const generateGame = (raceMode = false) => {
axios.post('/api/generate', {
weights: { player: localStorage.getItem('playerSettings') },
presetData: { player: localStorage.getItem('playerSettings') },
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.';
userMessage.classList.add('visible');
window.scrollTo(0, 0);
console.error(error);
});
};
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/static/spriteData.json`, true);
ajax.send();
});

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

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

@@ -17,6 +17,47 @@ window.addEventListener('load', () => {
paging: false,
info: false,
dom: "t",
columnDefs: [
{
targets: 'hours',
render: function (data, type, row) {
if (type === "sort" || type === 'type') {
if (data === "None")
return -1;
return parseInt(data);
}
if (data === "None")
return data;
let hours = Math.floor(data / 3600);
let minutes = Math.floor((data - (hours * 3600)) / 60);
if (minutes < 10) {minutes = "0"+minutes;}
return hours+':'+minutes;
}
},
{
targets: 'number',
render: function (data, type, row) {
if (type === "sort" || type === 'type') {
return parseFloat(data);
}
return data;
}
},
{
targets: 'fraction',
render: function (data, type, row) {
let splitted = data.split("/", 1);
let current = splitted[0]
if (type === "sort" || type === 'type') {
return parseInt(current);
}
return data;
}
},
],
// DO NOT use the scrollX or scrollY options. They cause DataTables to split the thead from
// the tbody and render two separate tables.

View File

@@ -20,6 +20,10 @@ window.addEventListener('load', () => {
ajax.send();
}).then((results) => {
// Populate page with HTML generated from markdown
showdown.setOption('tables', true);
showdown.setOption('strikethrough', true);
showdown.setOption('literalMidWordUnderscores', true);
showdown.setOption('disableForced4SpacesIndentedSublists', true);
tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results);
adjustHeaderWidth();

View File

@@ -0,0 +1,37 @@
# Subnautica Randomizer Setup Guide
## Required Software
- Subnautica from: [Subnautica Steam Store Page](https://store.steampowered.com/app/264710/Subnautica/)
- QModManager4 from: [QModManager4 Nexus Mods Page](https://www.nexusmods.com/subnautica/mods/201)
- Archipelago Mod for Subnautica
from: [Subnautica Archipelago Mod Releases Page](https://github.com/Berserker66/ArchipelagoSubnauticaModSrc/releases)
## Installation Procedures
1. Install QModManager4 as per its instructions.
2. The folder you installed QModManager4 into will now have a /QMods directory. It might appear after a start of
Subnautica. You can also create this folder yourself.
3. Unpack the Archipelago Mod into this folder, so that Subnautica/QMods/Archipelago/ is a valid path.
4. Start Subnautica. You should see a Connect Menu in the topleft of your main Menu.
## Troubleshooting
If you don't see the connect window check that you see a qmodmanager_log-Subnautica.txt in Subnautica, if not
QModManager4 is not correctly installed, otherwise open it and look
for `[Info : BepInEx] Loading [Archipelago 1.0.0.0]`, version number doesn't matter. If it doesn't show this, then
QModManager4 didn't find the Archipelago mod, so check your paths.
## Joining a MultiWorld Game
1. In Host, enter the address of the server, such as archipelago.gg:38281, your server host should be able to tell you
this.
2. In Password enter the server password if one exists, otherwise leave blank.
3. In PlayerName enter your "name" field from the yaml, or website config.
4. Hit Connect. If it says successfully authenticated you can now create a new savegame or resume the correct savegame.

View File

@@ -0,0 +1,222 @@
# Advanced YAML Guide
This guide covers more the more advanced options available in YAML files. This guide is intended for the user who is intent on editing their YAML file manually. This guide should take about 10 minutes to read.
If you would like to generate a basic, fully playable, YAML without editing a file then visit the settings page for the game you intend to play. The weighted settings page can also handle most of the advanced settings discussed here.
The settings page can be found on the supported games page, just click the "Settings Page" link under the name of the game you would like.
* Supported games page: [Archipelago Games List](/games)
* Weighted settings page: [Archipelago Weighted Settings](/weighted-settings)
Clicking on the "Export Settings" button at the bottom-left will provide you with a pre-filled YAML with your options.
The player settings page also has an option to download a fully filled out yaml containing every option with every
available setting for the available options.
## YAML Overview
The Archipelago system generates games using player configuration files as input. These are going to be YAML files and
each world will have one of these containing their custom settings for the game that world will play.
## YAML Formatting
YAML files are a format of human-readable config files. The basic syntax of a yaml file will have a `root` node and then
different levels of `nested` nodes that the generator reads in order to determine your settings.
To nest text, the correct syntax is to indent **two spaces over** from its root option. A YAML file can be edited with
whatever text editor you choose to use though I personally recommend that you use Sublime Text. Sublime text
website: [SublimeText Website](https://www.sublimetext.com)
This program out of the box supports the correct formatting for the YAML file, so you will be able to use the tab key
and get proper highlighting for any potential errors made while editing the file. If using any other text editor you
should ensure your indentation is done correctly with two spaces.
A typical YAML file will look like:
```yaml
root_option:
nested_option_one:
option_one_setting_one: 1
option_one_setting_two: 0
nested_option_two:
option_two_setting_one: 14
option_two_setting_two: 43
```
In Archipelago, YAML options are always written out in full lowercase with underscores separating any words. The numbers
following the colons here are weights. The generator will read the weight of every option the roll that option that many
times, the next option as many times as its numbered and so forth.
For the above example `nested_option_one` will have `option_one_setting_one` 1 time and `option_one_setting_two` 0 times
so `option_one_setting_one` is guaranteed to occur.
For `nested_option_two`, `option_two_setting_one` will be rolled 14 times and `option_two_setting_two` will be rolled 43
times against each other. This means `option_two_setting_two` will be more likely to occur, but it isn't guaranteed,
adding more randomness and "mystery" to your settings. Every configurable setting supports weights.
### Root Options
Currently, there are only a few options that are root options. Everything else should be nested within one of these root
options or in some cases nested within other nested options. The only options that should exist in root
are `description`, `name`, `game`, `requires`, `accessibility`, `progression_balancing`, `triggers`, and the name of the
games you want settings for.
* `description` is ignored by the generator and is simply a good way for you to organize if you have multiple files
using this to detail the intention of the file.
* `name` is the player name you would like to use and is used for your slot data to connect with most games. This can
also be filled with multiple names each having a weight to it.
* `game` is where either your chosen game goes or if you would like can be filled with multiple games each with
different weights.
* `requires` details different requirements from the generator for the YAML to work as you expect it to. Generally this
is good for detailing the version of Archipelago this YAML was prepared for as if it is rolled on an older version may
be missing settings and as such will not work as expected. If any plando is used in the file then requiring it here to
ensure it will be used is good practice.
* `accessibility` determines the level of access to the game the generation will expect you to have in order to reach
your completion goal. This supports `items`, `locations`, and `none` and is set to `locations` by default.
* `locations` will guarantee all locations are accessible in your world.
* `items` will guarantee you can acquire all items in your world but may not be able to access all locations. This
mostly comes into play if there is any entrance shuffle in the seed as locations without items in them can be
placed in areas that make them unreachable.
* `none` will only guarantee that the seed is beatable. You will be guaranteed able to finish the seed logically but
may not be able to access all locations or acquire all items. A good example of this is having a big key in the
big chest in a dungeon in ALTTP making it impossible to get and finish the dungeon.
* `progression_balancing` is a system the Archipelago generator uses to try and reduce "BK mode" as much as possible.
This primarily involves moving necessary progression items into earlier logic spheres to make the games more
accessible so that players almost always have something to do. This can be turned `on` or `off` and is `on` by
default.
* `triggers` is one of the more advanced options that allows you to create conditional adjustments. You can read
more triggers in the triggers guide. Triggers
guide: [Archipelago Triggers Guide](/tutorial/archipelago/triggers/en)
### Game Options
One of your root settings will be the name of the game you would like to populate with settings. Since it is possible to
give a weight to any option it is possible to have one file that can generate a seed for you where you don't know which
game you'll play. For these cases you'll want to fill the game options for every game that can be rolled by these
settings. If a game can be rolled it **must** have a settings section even if it is empty.
#### Universal Game Options
Some options in Archipelago can be used by every game but must still be placed within the relevant game's section.
Currently, these options are `start_inventory`, `start_hints`, `local_items`, `non_local_items`, `start_location_hints`
, `exclude_locations`, and various plando options.
See the plando guide for more info on plando options. Plando
guide: [Archipelago Plando Guide](/tutorial/archipelago/plando/en)
* `start_inventory` will give any items defined here to you at the beginning of your game. The format for this must be
the name as it appears in the game files and the amount you would like to start with. For example `Rupees(5): 6` which
will give you 30 rupees.
* `start_hints` gives you free server hints for the defined item/s at the beginning of the game allowing you to hint for
the location without using any hint points.
* `local_items` will force any items you want to be in your world instead of being in another world.
* `non_local_items` is the inverse of `local_items` forcing any items you want to be in another world and won't be
located in your own.
* `start_location_hints` allows you to define a location which you can then hint for to find out what item is located in
it to see how important the location is.
* `exclude_locations` lets you define any locations that you don't want to do and during generation will force a "junk"
item which isn't necessary for progression to go in these locations.
### Random numbers
Options taking a choice of a number can also use a variety of `random` options to choose a number randomly.
* `random` will choose a number allowed for the setting at random
* `random-low` will choose a number allowed for the setting at random, but will be weighted towards lower numbers
* `random-middle` will choose a number allowed for the setting at random, but will be weighted towards the middle of the
range
* `random-high` will choose a number allowed for the setting at random, but will be weighted towards higher numbers
* `random-range-#-#` will choose a number at random from between the specified numbers. For example `random-range-40-60`
will choose a number between 40 and 60
* `random-range-low-#-#`, `random-range-middle-#-#`, and `random-range-high-#-#` will choose a number at random from the
specified numbers, but with the specified weights
### Example
```yaml
description: An example using various advanced options
name: Example Player
game: A Link to the Past
requires:
version: 0.2.0
accessibility: none
progression_balancing: on
A Link to the Past:
smallkey_shuffle:
original_dungeon: 1
any_world: 1
crystals_needed_for_gt:
random-low: 1
crystals_needed_for_ganon:
random-range-high-1-7: 1
start_inventory:
Pegasus Boots: 1
Bombs (3): 2
start_hints:
- Hammer
local_items:
- Bombos
- Ether
- Quake
non_local_items:
- Moon Pearl
start_location_hints:
- Spike Cave
exclude_locations:
- Cave 45
triggers:
- option_category: A Link to the Past
option_name: smallkey_shuffle
option_result: any_world
options:
A Link to the Past:
bigkey_shuffle: any_world
map_shuffle: any_world
compass_shuffle: any_world
```
#### This is a fully functional yaml file that will do all the following things:
* `description` gives us a general overview so if we pull up this file later we can understand the intent.
* `name` is `Example Player` and this will be used in the server console when sending and receiving items.
* `game` is set to `A Link to the Past` meaning that is what game we will play with this file.
* `requires` is set to require release version 0.2.0 or higher.
* `accesibility` is set to `none` which will set this seed to beatable only meaning some locations and items may be
completely inaccessible but the seed will still be completable.
* `progression_balancing` is set on meaning we will likely receive important items earlier increasing the chance of
having things to do.
* `A Link to the Past` defines a location for us to nest all the game options we would like to use for our
game `A Link to the Past`.
* `smallkey_shuffle` is an option for A Link to the Past which determines how dungeon small keys are shuffled. In this
example we have a 1/2 chance for them to either be placed in their original dungeon and a 1/2 chance for them to be
placed anywhere amongst the multiworld.
* `crystals_needed_for_gt` determines the number of crystals required to enter the Ganon's Tower entrance. In this
example a random number will be chosen from the allowed range for this setting (0 through 7) but will be weighted
towards a lower number.
* `crystals_needed_for_ganon` determines the number of crystals required to beat Ganon. In this example a number between
1 and 7 will be chosen at random, weighted towards a high number.
* `start_inventory` defines an area for us to determine what items we would like to start the seed with. For this
example we have:
* `Pegasus Boots: 1` which gives us 1 copy of the Pegasus Boots
* `Bombs (3)` gives us 2 packs of 3 bombs or 6 total bombs
* `start_hints` gives us a starting hint for the hammer available at the beginning of the multiworld which we can use
with no cost.
* `local_items` forces the `Bombos`, `Ether`, and `Quake` medallions to all be placed within our own world, meaning we
have to find it ourselves.
* `non_local_items` forces the `Moon Pearl` to be placed in someone else's world, meaning we won't be able to find it.
* `start_location_hints` gives us a starting hint for the `Spike Cave` location available at the beginning of the
multiworld that can be used for no cost.
* `exclude_locations` forces a not important item to be placed on the `Cave 45` location.
* `triggers` allows us to define a trigger such that if our `smallkey_shuffle` option happens to roll the `any_world`
result it will also ensure that `bigkey_shuffle`, `map_shuffle`, and `compass_shuffle` are also forced to
the `any_world`
result.

View File

@@ -0,0 +1,96 @@
### Helpful Commands
Commands are split into two types: client commands and server commands. Client commands are commands which are executed
by the client and do not affect the Archipelago remote session. Server commands are commands which are executed by the
Archipelago server and affect the Archipelago session or otherwise provide feedback from the server.
In clients which have their own commands the commands are typically prepended by a forward slash:`/`. Remote commands
are always submitted to the server prepended with an exclamation point: `!`.
#### Local Commands
The following list is a list of client commands which may be available to you through your Archipelago client. You
execute these commands in your client window.
The following commands are available in these clients: SNIClient, FactorioClient, FF1Client.
- `/connect <address:port>` Connect to the multiworld server.
- `/disconnect` Disconnects you from your current session.
- `/received` Displays all the items you have found or been sent.
- `/missing` Displays all the locations along with their current status (checked/missing).
- `/items` Lists all the item names for the current game.
- `/locations` Lists all the location names for the current game.
- `/ready` Sends ready status to the server.
- `/help` Returns a list of available commands.
- `/license` Returns the software licensing information.
- Just typing anything will broadcast a message to all players
##### FF1Client Only
The following command is only available when using the FF1Client for the Final Fantasy Randomizer.
- `/nes` Shows the current status of the NES connection.
##### SNIClient Only
The following command is only available when using the SNIClient for SNES based games.
- `/snes` Attempts to connect to your SNES device via SNI.
- `/snes_close` Closes the current SNES connection.
- `/slow_mode` Toggles on or off slow mode, which limits the rate in which you receive items.
##### FactorioClient Only
The following command is only available when using the FactorioClient to play Factorio with Archipelago.
- `/factorio <command text>` Sends the command argument to the Factorio server as a command.
#### Remote Commands
Remote commands may be executed by any client which allows for sending text chat to the Archipelago server. If your
client does not allow for sending chat then you may connect to your game slot with the TextClient which comes with the
Archipelago installation. In order to execute the command you need to merely send a text message with the command,
including the exclamation point.
- `!help` Returns a listing of available remote commands.
- `!license` Returns the software licensing information.
- `!countdown <countdown in seconds>` Starts a countdown using the given seconds value. Useful for synchronizing starts.
Defaults to 10 seconds if no argument is provided.
- `!options` Returns the current server options, including password in plaintext.
- `!admin <command>` Executes a command as if you typed it into the server console. Remote administration must be
enabled.
- `!players` Returns info about the currently connected and non-connected players.
- `!status` Returns information about your team. (Currently all players as teams are unimplemented.)
- `!remaining` Lists the items remaining in your game, but not where they are or who they go to.
- `!missing` Lists the location checks you are missing from the server's perspective.
- `!checked` Lists all the location checks you've done from the server's perspective.
- `!alias <alias>` Sets your alias.
- `!getitem <item>` Cheats an item, if it is enabled in the server.
- `!hint_location <location>` Hints for a location specifically. Useful in games where item names may match location
names such as Factorio.
- `!hint <item name>` Tells you at which location in whose game your Item is. Note you need to have checked some
locations to earn a hint. You can check how many you have by just running `!hint`
- `!forfeit` If you didn't turn on auto-forfeit or if you allowed forfeiting prior to goal completion. Remember that "
forfeiting" actually means sending out your remaining items in your world.
- `!collect` Grants you all the remaining checks in your world. Can only be used after your goal is complete or when you
have forfeited.
#### Host only (on Archipelago.gg or in your server console)
- `/help` Returns a list of commands available in the console.
- `/license` Returns the software licensing information.
- `/countdown <seconds>` Starts a countdown which is sent to all players via text chat. Defaults to 10 seconds if no
argument is provided.
- `/options` Lists the server's current options, including password in plaintext.
- `/save` Saves the state of the current multiworld. Note that the server autosaves on a minute basis.
- `/players` List currently connected players.
- `/exit` Shutdown the server
- `/alias <player name> <alias name>` Assign a player an alias.
- `/collect <player name>` Send out any items remaining in the multiworld belonging to the given player.
- `/forfeit <player name>` Forfeits someone regardless of settings and game completion status
- `/allow_forfeit <player name>` Allows the given player to use the `!forfeit` command.
- `/forbid_forfeit <player name>` Bars the given player from using the `!forfeit` command.
- `/send <player name> <item name>` Grants the given player the specified item.
- `/send_multiple <amount> <player name> <item name>` Grants the given player the stated amount of the specified item.
- `/hint <player name> <item or location name>` Send out a hint for the given item or location for the specified player.
- `/option <option name> <option value>` Set a server option. For a list of options, use the `/options` command.

View File

@@ -0,0 +1,215 @@
# Archipelago Plando Guide
## What is Plando?
The purposes of randomizers is to randomize the items in a game to give a new experience. Plando takes this concept and
changes it up by allowing you to plan out certain aspects of the game by placing certain items in certain locations,
certain bosses in certain rooms, edit text for certain NPCs/signs, or even force certain region connections. Each of
these options are going to be detailed separately as `item plando`, `boss plando`, `text plando`,
and `connection plando`. Every game in archipelago supports item plando but the other plando options are only supported
by certain games. Currently, Minecraft and LTTP both support connection plando, but only LTTP supports text and boss
plando.
### Enabling Plando
On the website plando will already be enabled. If you will be generating the game locally plando features must be
enabled (opt-in).
* To opt-in go to the archipelago installation (default: `C:\ProgramData\Archipelago`), open the host.yaml with a text
editor and find the `plando_options` key. The available plando modules can be enabled by adding them after this such
as
`plando_options: bosses, items, texts, connections`.
* You can add the necessary plando modules for your settings to the `requires` section of your yaml. Doing so will throw an error if the options that you need to generate properly are not enabled to ensure you will get the results you desire. Only enter in the plando modules that you are using here but it should look like:
```yaml
requires:
version: current.version.number
plando: bosses, items, texts, connections
```
## Item Plando
Item plando allows a player to place an item in a specific location or specific locations, place multiple items into a
list of specific locations both in their own game or in another player's game.
* The options for item plando are `from_pool`, `world`, `percentage`, `force`, `count`, and either item and location, or items
and locations.
* `from_pool` determines if the item should be taken *from* the item pool or *added* to it. This can be true or
false and defaults to true if omitted.
* `world` is the target world to place the item in.
* It gets ignored if only one world is generated.
* Can be a number, name, true, false, null, or a list. False is the default.
* If a number is used it targets that slot or player number in the multiworld.
* If a name is used it will target the world with that player name.
* If set to true it will be any player's world besides your own.
* If set to false it will target your own world.
* If set to null it will target a random world in the multiworld.
* If a list of names is used, it will target the games with the player names specified.
* `force` determines whether the generator will fail if the item can't be placed in the location can be true, false,
or silent. Silent is the default.
* If set to true the item must be placed and the generator will throw an error if it is unable to do so.
* If set to false the generator will log a warning if the placement can't be done but will still generate.
* If set to silent and the placement fails it will be ignored entirely.
* `percentage` is the percentage chance for the relevant block to trigger. This can be any value from 0 to 100 and
if omitted will default to 100.
* Single Placement is when you use a plando block to place a single item at a single location.
* `item` is the item you would like to place and `location` is the location to place it.
* Multi Placement uses a plando block to place multiple items in multiple locations until either list is exhausted.
* `items` defines the items to use and a number letting you place multiple of it. You can use true instead of a number to have it use however many of that item are in your item pool.
* `locations` is a list of possible locations those items can be placed in.
* Using the multi placement method, placements are picked randomly.
* Instead of a number, you can use true
* `count` can be used to set the maximum number of items placed from the block. The default is 1 if using `item` and False if using `items`
* If a number is used it will try to place this number of items.
* If set to false it will try to place as many items from the block as it can.
* If `min` and `max` are defined, it will try to place a number of items between these two numbers at random
### Available Items and Locations
A list of all available items and locations can be found in the [website's datapackage](/datapackage). The items and locations will be in the `"item_name_to_id"` and `"location_name_to_id"` sections of the relevant game. You do not need the quotes but the name must be entered in the same as it appears on that page and is caps-sensitive.
### Examples
```yaml
plando_items:
# example block 1 - Timespinner
- item:
Empire Orb: 1
Radiant Orb: 1
location: Starter Chest 1
from_pool: true
world: true
percentage: 50
# example block 2 - Ocarina of Time
- items:
Kokiri Sword: 1
Biggoron Sword: 1
Bow: 1
Magic Meter: 1
Progressive Strength Upgrade: 3
Progressive Hookshot: 2
locations:
- Deku Tree Slingshot Chest
- Dodongos Cavern Bomb Bag Chest
- Jabu Jabus Belly Boomerang Chest
- Bottom of the Well Lens of Truth Chest
- Forest Temple Bow Chest
- Fire Temple Megaton Hammer Chest
- Water Temple Longshot Chest
- Shadow Temple Hover Boots Chest
- Spirit Temple Silver Gauntlets Chest
world: false
# example block 3 - Slay the Spire
- items:
Boss Relic: 3
locations:
- Boss Relic 1
- Boss Relic 2
- Boss Relic 3
# example block 4 - Factorio
- items:
progressive-electric-energy-distribution: 2
electric-energy-accumulators: 1
progressive-turret: 2
locations:
- military
- gun-turret
- logistic-science-pack
- steel-processing
percentage: 80
force: true
# example block 5 - Secret of Evermore
- items:
Levitate: 1
Revealer: 1
Energize: 1
locations:
- Master Sword Pedestal
- Boss Relic 1
world: true
count: 2
# example block 6 - A Link to the Past
- items:
Progressive Sword: 4
world:
- BobsSlaytheSpire
- BobsRogueLegacy
count:
min: 1
max: 4
```
1. This block has a 50% chance to occur, and if it does will place either the Empire Orb or Radiant Orb on another player's
Starter Chest 1 and removes the chosen item from the item pool.
2. This block will always trigger and will place the player's swords, bow, magic meter, strength upgrades, and hookshots
in their own dungeon major item chests.
3. This block will always trigger and will lock boss relics on the bosses.
4. This block has an 80% chance of occurring and when it does will place all but 1 of the items randomly among the four
locations chosen here.
5. This block will always trigger and will attempt to place a random 2 of Levitate, Revealer and Energize into
other players' Master Sword Pedestals or Boss Relic 1 locations.
6. This block will always trigger and will attempt to place a random number, between 1 and 4, of progressive swords
into any locations within the game slots named BobsSlaytheSpire and BobsRogueLegacy
## Boss Plando
As this is currently only supported by A Link to the Past instead of explaining here please refer to the
[relevant guide](/tutorial/zelda3/plando/en)
## Text Plando
As this is currently only supported by A Link to the Past instead of explaining here please refer to the
[relevant guide](/tutorial/zelda3/plando/en)
## Connections Plando
This is currently only supported by Minecraft and A Link to the Past. As the way that these games interact with their
connections is different I will only explain the basics here while more specifics for Link to the Past connection plando
can be found in its plando guide.
* The options for connections are `percentage`, `entrance`, `exit`, and `direction`. Each of these options support
subweights.
* `percentage` is the percentage chance for this connection from 0 to 100 and defaults to 100.
* Every connection has an `entrance` and an `exit`. These can be unlinked like in A Link to the Past insanity entrance
shuffle.
* `direction` can be `both`, `entrance`, or `exit` and determines in which direction this connection will operate.
[Link to the Past connections](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/EntranceShuffle.py#L3852)
[Minecraft connections](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/minecraft/Regions.py#L62)
### Examples
```yaml
plando_connections:
# example block 1 - Link to the Past
- entrance: Cave Shop (Lake Hylia)
exit: Cave 45
direction: entrance
- entrance: Cave 45
exit: Cave Shop (Lake Hylia)
direction: entrance
- entrance: Agahnims Tower
exit: Old Man Cave Exit (West)
direction: exit
# example block 2 - Minecraft
- entrance: Overworld Structure 1
exit: Nether Fortress
direction: both
- entrance: Overworld Structure 2
exit: Village
direction: both
```
1. These connections are decoupled so going into the lake hylia cave shop will take you to the inside of cave 45 and
when you leave the interior you will exit to the cave 45 ledge. Going into the cave 45 entrance will then take you to
the lake hylia cave shop. Walking into the entrance for the old man cave and Agahnim's Tower entrance will both take
you to their locations as normal but leaving old man cave will exit at Agahnim's Tower.
2. This will force a nether fortress and a village to be the overworld structures for your game. Note that for the
Minecraft connection plando to work structure shuffle must be enabled.

View File

@@ -0,0 +1,85 @@
# Archipelago Setup Guide
This guide is intended to provide an overview of how to install, set up, and run the Archipelago multiworld software.
This guide should take about 5 minutes to read.
## Installing the Archipelago software
The most recent public release of Archipelago can be found on the GitHub Releases page. GitHub Releases
page: [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases).
Run the exe file, and after accepting the license agreement you will be prompted on which components you would like to
install.
The generator allows you to generate multiworld games on your computer. The ROM setups are required if anyone in the
game that you generate wants to play any of those games as they are needed to generate the relevant patch files.
The server will allow you to host the multiworld on your machine. Hosting on your machine requires forwarding the port
you are hosting on. The default port for Archipelago is `38281`. If you are unsure how to do this there are plenty of
other guides on the internet that will be more suited to your hardware.
The `Clients` are what are used to connect your game to the multiworld. If the game/games you plan to play are available
here go ahead and install these as well. If the game you choose to play is supported by Archipelago but not listed in
the installation check the setup guide for that game. Installing a client for a ROM based game requires you to have a
legally obtained ROM for that game as well.
## Generating a game
### What is a YAML?
YAML is the file format which Archipelago uses in order to configure a player's world. It allows you to dictate which
game you will be playing as well as the settings you would like for that game.
YAML is a format very similar to JSON however it is made to be more human-readable. If you are ever unsure of the
validity of your YAML file you may check the file by uploading it to the check page on the Archipelago website. Check
page: [YAML Validation Page](/mysterycheck)
### Creating a YAML
YAML files may be generated on the Archipelago website by visiting the games page and clicking the "Settings Page" link
under any game. Clicking "Export Settings" in a game's settings page will download the YAML to your system. Games
page: [Archipelago Games List](/games)
In a multiworld there must be one YAML per world. Any number of players can play on each world using either the game's
native coop system or using Archipelago's coop support. Each world will hold one slot in the multiworld and will have a
slot name and, if the relevant game requires it, files to associate it with that multiworld.
If multiple people plan to play in one world cooperatively then they will only need one YAML for their coop world. If
each player is planning on playing their own game then they will each need a YAML.
### Gather All Player YAMLs
All players that wish to play in the generated multiworld must have a YAML file which contains the settings that they
wish to play with.
Typically, a single participant of the multiworld will gather the YAML files from all other players. After getting the
YAML files of each participant for your multiworld game they can be compressed into a ZIP folder to then be uploaded to
the multiworld generator page. Multiworld generator
page: [Archipelago Seed Generator Page](https://archipelago.gg/generate)
#### Rolling a YAML Locally
It is possible to roll the multiworld locally, using a local Archipelago installation. This is done by entering the
installation directory of the Archipelago installation and placing each YAML file in the `Players` folder. If the folder
does not exist then it can be created manually.
After filling the `Players` folder the `ArchipelagoGenerate.exe` program should be run in order to generate a
multiworld. The output of this process is placed in the `output` folder.
#### Changing local host settings for generation
Sometimes there are various settings that you may want to change before rolling a seed such as enabling race mode,
auto-forfeit, plando support, or setting a password.
All of these settings plus other options are able to be changed by modifying the `host.yaml` file in the Archipelago
installation folder. The settings chosen here are baked into the `.archipelago` file that gets output with the other
files after generation so if rolling locally ensure this file is edited to your liking *before* rolling the seed.
## Hosting an Archipelago Server
The output of rolling a YAML will be a `.archipelago` file which can be subsequently uploaded to the Archipelago host
game page. Archipelago host game page: [Archipelago Seed Upload Page](https://archipelago.gg/uploads)
The `.archipelago` file may be run locally in order to host the multiworld on the local machine. This is done by
running `ArchipelagoServer.exe` and pointing the resulting file selection prompt to the `.archipelago` file that was
generated.

View File

@@ -0,0 +1,129 @@
# Archipelago Triggers Guide
This guide details the use of the Archipelago YAML trigger system. This guide is intended for a more advanced user with
more in-depth knowledge of Archipelago YAML options as well as experience editing YAML files. This guide should take
about 5 minutes to read.
## What are triggers?
Triggers allow you to customize your game settings by allowing you to define one or many options which only occur under
specific conditions. These are essentially "if, then" statements for options in your game. A good example of what you
can do with triggers is the custom mercenary mode YAML that was created using entirely triggers and plando.
Mercenary mode
YAML: [Mercenary Mode YAML on GitHub](https://github.com/alwaysintreble/Archipelago-yaml-dump/blob/main/Snippets/Mercenary%20Mode%20Snippet.yaml)
For more information on plando you can reference the general plando guide or the Link to the Past plando guide.
General plando guide: [Archipelago Plando Guide](/tutorial/archipelago/plando/en)
Link to the Past plando guide: [LttP Plando Guide](/tutorial/zelda3/plando/en)
## Trigger use
Triggers may be defined in either the root or in the relevant game sections. Generally, The best place to do this is the
bottom of the yaml for clear organization.
- Triggers comprise the trigger section and then each trigger must have an `option_category`, `option_name`, and
`option_result` from which it will react to and then an `options` section for the definition of what will happen.
- `option_category` is the defining section from which the option is defined in.
- Example: `A Link to the Past`
- This is the root category the option is located in. If the option you're triggering off of is in root then you
would use `null`, otherwise this is the game for which you want this option trigger to activate.
- `option_name` is the option setting from which the triggered choice is going to react to.
- Example: `shop_item_slots`
- This can be any option from any category defined in the yaml file in either root or a game section.
- `option_result` is the result of this option setting from which you would like to react.
- Example: `15`
- Each trigger must be used for exactly one option result. If you would like the same thing to occur with multiple
results you would need multiple triggers for this.
- `options` is where you define what will happen when this is detected. This can be something as simple as ensuring
another option also gets selected or placing an item in a certain location. It is possible to have multiple things
happen in this section.
- Example:
```yaml
A Link to the Past:
start_inventory:
Rupees (300): 2
```
This format must be:
```yaml
root option:
option to change:
desired result
```
### Examples
The above examples all together will end up looking like this:
```yaml
triggers:
- option_category: A Link to the Past
option_name: shop_item_slots
option_result: 15
options:
A Link to the Past:
start_inventory:
Rupees(300): 2
```
For this example if the generator happens to roll 15 shuffled in shop item slots for your game you'll be granted 600
rupees at the beginning. These can also be used to change other options.
For example:
```yaml
triggers:
- option_category: Timespinner
option_name: SpecificKeycards
option_result: true
options:
Timespinner:
Inverted: true
```
In this example if your world happens to roll SpecificKeycards then your game will also start in inverted.
It is also possible to use imaginary names in options to trigger specific settings. You can use these made up names in
either your main options or to trigger from another trigger. Currently, this is the only way to trigger on "setting 1
AND setting 2".
For example:
```yaml
triggers:
- option_category: Secret of Evermore
option_name: doggomizer
option_result: pupdunk
options:
Secret of Evermore:
difficulty:
normal: 50
pupdunk_hard: 25
pupdunk_mystery: 25
exp_modifier:
150: 50
200: 50
- option_category: Secret of Evermore
option_name: difficulty
option_result: pupdunk_hard
options:
Secret of Evermore:
fix_wings_glitch: false
difficulty: hard
- option_category: Secret of Evermore
option_name: difficulty
option_result: pupdunk_mystery
options:
Secret of Evermore:
fix_wings_glitch: false
difficulty: mystery
```
In this example (thanks to @Black-Sliver) if the `pupdunk` option is rolled then the difficulty values will be rolled
again using the new options `normal`, `pupdunk_hard`, and `pupdunk_mystery`, and the exp modifier will be rerolled using
new weights for 150 and 200. This allows for two more triggers that will only be used for the new `pupdunk_hard`
and `pupdunk_mystery` options so that they will only be triggered on "pupdunk AND hard/mystery".

View File

@@ -0,0 +1,33 @@
# Using the Archipelago Website
This guide encompasses the use cases for rolling and hosting multiworld games on the Archipelago website. This guide
should only take a couple of minutes to read.
## Rolling the Seed On the Website
1. After gathering the YAML files together in one location, select all the files and compress them into a `.ZIP` file.
2. Next go to the "Generate Game" page. Generate game
page: [Archipelago Seed Generation Page](https://archipelago.gg/generate). Here, you can adjust some server settings
such as forfeit rules and the cost for a player to use a hint before generation.
3. After adjusting the host settings to your liking click on the Upload File button and using the explorer window that
opens, navigate to the location where you zipped the player files and upload this zip. The page will generate your
game and refresh multiple times to check on completion status.
4. After the generation completes you will be on a Seed Info page that provides the seed, the date/time of creation, a
link to the spoiler log, if available, and links to any rooms created from this seed.
5. To begin playing, click on "Create New Room", which will take you to the room page. From here you can navigate back
to the Seed Info page or to the Tracker page. Sharing the link to the room page with your friends will provide them
with the necessary info and files for them to connect to the multiworld.
## Hosting a Pre-Generated Multiworld on the Website
The easiest and most recommended method is to generate the game on the website which will allow you to create a private
room with all the necessary files you can share, as well as hosting the game and supporting item trackers for various
games.
If for some reason the seed was rolled on a machine, then either the resulting zip file or the
resulting `AP_XXXXX.archipelago` inside the zip file can be uploaded to the host game page. Host game
page: [Archipelago Seed Upload Page](https://archipelago.gg/uploads)
This will give a page with the seed info and a link to the spoiler log, if it exists. Click on "Create New Room" and
then share the link to the resulting page the other players so that they can download their patches or mods. The room
will also have a link to a Multiworld Tracker and tell you what the players need to connect to from their clients.

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 KiB

View File

@@ -2,50 +2,152 @@
## Required Software
### Server Host
- [Factorio](https://factorio.com)
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
##### Players
### Players
- [Factorio](https://factorio.com)
- Factorio: [Factorio Official Website](https://factorio.com)
- Needed by Players and Hosts
## General Concept
##### Server Hosts
One Server Host exists per Factorio World in an Archipelago Multiworld, any number of modded Factorio players can then connect to that world. From the view of Archipelago, this Factorio host is a client.
## Installation Procedures
- Factorio: [Factorio Official Website](https://factorio.com)
- Needed by Players and Hosts
- Archipelago: [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases)
- Needed by Hosts
## Create a Config (.yaml) File
### What is a config file and why do I need one?
Your config file contains a set of configuration options which provide the generator with information about how it
should generate your game. Each player of a multiworld will provide their own config file. This setup allows each player
to enjoy an experience customized for their taste, and different players in the same multiworld can all have different
options.
### Where do I get a config file?
The Player Settings page on the website allows you to configure your personal settings and export a config file from
them. Factorio player settings page: [Factorio Settings Page](/games/Factorio/player-settings)
### Verifying your config file
If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML
Validator page: [Yaml Validation Page](/mysterycheck)
## Connecting to Someone Else's Factorio Game
Connecting to someone else's game is the simplest way to play Factorio with Archipelago. It allows multiple people to
play in a single world, all contributing to the completion of the seed.
1. Acquire the Archipelago mod for this seed. It should be named `AP_*.zip`, where `*` is the seed number.
2. Copy the mod file into your Factorio `mods` folder, which by default is located at:
`C:\Users\<YourUserName>\AppData\Roaming\Factorio\mods`
3. Get the server address from the person hosting the game you are joining.
4. Launch Factorio
5. Click on "Multiplayer" in the main menu
6. Click on "Connect to address"
7. Enter the address into this box
8. Click "Connect"
## Prepare to Host Your Own Factorio Game
### Defining Some Terms
In Archipelago, multiple Factorio worlds may be played simultaneously. Each of these worlds must be hosted by a Factorio
server, which is connected to the Archipelago Server via middleware.
This guide uses the following terms to refer to the software:
- **Factorio Client** - The Factorio instance which will be used to play the game.
- **Factorio Server** - The Factorio instance which will be used to host the Factorio world. Any number of Factorio
Clients may connect to this server.
- **Archipelago Client** - The middleware software used to connect the Factorio Server to the Archipelago Server.
- **Archipelago Server** - The central Archipelago server, which connects all games to each other.
### What a Playable State Looks Like
- An Archipelago Server
- The generated Factorio Mod, created as a result of running `ArchipelagoGenerate.exe`
- One running instance of `ArchipelagoFactorioClient.exe` (the Archipelago Client) per Factorio world
- A running modded Factorio Server, which should have been started by the Archipelago Client automatically
- A running modded Factorio Client
### 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.
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.
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`:
```ini
config-path=__PATH__executable__/../../config
use-system-read-write-data-directories=false
To play Factorio with Archipelago, a dedicated server setup is required. This dedicated Factorio Server must be
installed separately from your main Factorio Client installation. The recommended way to install two instances of
Factorio on your computer is to download the Factorio installer file directly from
factorio.com: [Factorio Official Website Download Page](https://factorio.com/download).
#### If you purchased Factorio on Steam, GOG, etc.
You can register your copy of Factorio on factorio.com: [Factorio Official Website](https://factorio.com/). You will be
required to create an account, if you have not done so already. As part of that process, you will be able to enter your
Factorio product code. This will allow you to download the game directly from their website.
#### Download the Standalone Version
It is recommended to download the standalone version of Factorio for use as a dedicated server. Doing so prevents any
potential conflicts with your currently-installed version of Factorio. Download the file by clicking on the button
appropriate to your operating system, and extract the folder to a convenient location (we recommend `C:\Factorio` or
similar).
![Factorio Download Options](/static/assets/tutorial/factorio/factorio-download.png)
Next, you should launch your Factorio Server by running `factorio.exe`, which is located at: `bin/x64/factorio.exe`. You
will be asked to log in to your Factorio account using the same credentials you used on Factorio's website. After you
have logged in, you may close the game.
#### Configure your Archipelago Installation
You must modify your `host.yaml` file inside your Archipelago installation directory so that it points to your
standalone Factorio executable. Here is an example of the appropriate setup, note the double `\\` are required:
```yaml
factorio_options:
executable: C:\\factorio\\bin\\x64\\factorio"
```
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.
This allows you to host your own Factorio game.
### Player Setup
- Manually install the AP mod for the correct world you want to join, then use Factorio's built-in multiplayer.
## Hosting Your Own Factorio Game
## Joining a MultiWorld Game
1. Obtain the Factorio mod for this Archipelago seed. It should be named `AP_*.zip`, where `*` is the seed number.
2. Install the mod into your Factorio Server by copying the zip file into the `mods` folder.
3. Install the mod into your Factorio Client by copying the zip file into the `mods` folder, which is likely located
at `C:\Users\YourName\AppData\Roaming\Factorio\mods`.
4. Obtain the Archipelago Server address from the website's host room, or from the server host.
5. Run your Archipelago Client, which is named `ArchilepagoFactorioClient.exe`. This was installed along with
Archipelago if you chose to include it during the installation process.
6. Enter `/connect [server-address]` into the input box at the bottom of the Archipelago Client and press "Enter"
1. Make a fresh world. Using any Factorio, create a savegame with options of your choice and save that world as Archipelago.
![Factorio Client for Archipelago Connection Command](/static/assets/tutorial/factorio/connect-to-ap-server.png)
2. Take that savegame and put it into your Archipelago folder
7. Launch your Factorio Client
8. Click on "Multiplayer" in the main menu
9. Click on "Connect to address"
10. Enter `localhost` into the server address box
11. Click "Connect"
3. Install the generated Factorio AP Mod
For additional client features, issue the `/help` command in the Archipelago Client. Once connected to the AP server,
you can also issue the `!help` command to learn about additional commands like `!hint`.
4. Run FactorioClient, it should launch a Factorio server, which you can control with `/factorio <original factorio commands>`,
* It should say it loaded the Archipelago mod and found a bridge file. If not, the most likely case is that the mod is not correctly installed or activated.
## Allowing Other People to Join Your Game
5. In FactorioClient, do `/connect <Archipelago Server Address>` to join that multiworld. You can find further commands with `/help` as well as `!help` once connected.
1. Ensure your Archipelago Client is running.
2. Ensure port `34197` is forwarded to the computer running the Archipelago Client.
3. Obtain your IP address by visiting whatismyip.com: [WhatIsMyIP Website](https://whatismyip.com/).
4. Provide your IP address to anyone you want to join your game, and have them follow the steps for
"Connecting to Someone Else's Factorio Game" above.
* / commands are run on your local client, ! commands are requests for the AP server
## Troubleshooting
* Players should be able to connect to your Factorio Server and begin playing.
In case any problems should occur, the Archipelago Client will create a file `FactorioClient.txt` in the `/logs`. The
contents of this file may help you troubleshoot an issue on your own and is vital for requesting help from other people
in Archipelago.
## Additional Resources
- Alternate Tutorial by
Umenen: [Factorio (Steam) Archipelago Setup Guide for Windows](https://docs.google.com/document/d/1yZPAaXB-QcetD8FJsmsFrenAHO5V6Y2ctMAyIoT9jS4)
- Factorio Speedrun Guide: [Factorio Speedrun Guide by Nefrums](https://www.youtube.com/watch?v=ExLrmK1c7tA)
- Factorio Wiki: [Factorio Official Wiki](https://wiki.factorio.com/)

View File

@@ -0,0 +1,74 @@
# Final Fantasy 1 (NES) Multiworld Setup Guide
## Required Software
- The FF1Client
- Bundled with Archipelago: [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases)
- The BizHawk emulator. Versions 2.3.1 and higher are supported. Version 2.7 is recommended
- [BizHawk Official Website](http://tasvideos.org/BizHawk.html)
- Your legally obtained Final Fantasy (USA Edition) ROM file, probably named `Final Fantasy (USA).nes`. Neither
Archipelago.gg nor the Final Fantasy Randomizer Community can supply you with this.
## Installation Procedures
1. Download and install the latest version of Archipelago.
1. On Windows, download Setup.Archipelago.<HighestVersion\>.exe and run it
2. Assign Bizhawk version 2.3.1 or higher as your default program for launching `.nes` files.
1. Extract your Bizhawk folder to your Desktop, or somewhere you will remember. Below are optional additional steps
for loading ROMs more conveniently
1. Right-click on a ROM file and select **Open with...**
2. Check the box next to **Always use this app to open .nes files**
3. Scroll to the bottom of the list and click the grey text **Look for another App on this PC**
4. Browse for `EmuHawk.exe` located inside your Bizhawk folder (from step 1) and click **Open**.
## Obtaining your Archipelago yaml file and randomized ROM
Unlike most other Archipelago.gg games Final Fantasy 1 is randomized by the main randomizer at
the [Final Fantasy Randomizer Homepage](https://finalfantasyrandomizer.com/).
Generate a game by going to the site and performing the following steps:
1. Select the randomization options (also known as `Flags` in the community) of your choice. If you do not know what you
prefer, or it is your first time playing select the "Archipelago" preset on the main page.
2. Go to the `Beta` tab and ensure `Archipelago` is enabled. Set your player name to any name that represents you.
3. Upload you `Final Fantasy(USA).nes` (and click `Remember ROM` for the future!)
4. Press the `NEW` button beside `Seed` a few times
5. Click `GENERATE ROM`
It should download two files. One is the `*.nes` file which your emulator will run and the other is the yaml file
required by Archipelago.gg
At this point you are ready to join the multiworld. If you are uncertain on how to generate, host or join a multiworld
please refer to the [game agnostic setup guide](/tutorial/archipelago/setup/en).
## Running the Client Program and Connecting to the Server
Once the Archipelago server has been hosted:
1. Navigate to your Archipelago install folder and run `ArchipelagoFF1Client.exe`
2. Notice the `/connect command` on the server hosting page (It should look like `/connect archipelago.gg:*****`
where ***** are numbers)
3. Type the connect command into the client OR add the port to the pre-populated address on the top bar (it should
already say `archipelago.gg`) and click `connect`
### Running Your Game and Connecting to the Client Program
1. Open Bizhawk 2.3.1 or higher and load your ROM OR click your ROM file if it is already associated with the
extension `*.nes`
2. Click on the Tools menu and click on **Lua Console**
3. Click the folder button to open a new Lua script. (CTL-O or **Script** -> **Open Script**)
4. Navigate to the location you installed Archipelago to. Open data/lua/FF1/ff1_connector.lua
1. If it gives a `NLua.Exceptions.LuaScriptException: .\socket.lua:13: module 'socket.core' not found:` exception
close your emulator entirely, restart it and re-run these steps
2. If it says `Must use a version of bizhawk 2.3.1 or higher`, double-check your Bizhawk version by clicking **
Help** -> **About**
## Play the game
When the client shows both NES and server are connected you are good to go. You can check the connection status of the
NES at any time by running `/nes`
### Other Client Commands
All other commands may be found on the [Archipelago Server and Client Commands Guide](/tutorial/archipelago/commands/en)
.

View File

@@ -2,135 +2,60 @@
## 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)
## 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`
- Minecraft Java Edition from
the [Minecraft Java Edition Store Page](https://www.minecraft.net/en-us/store/minecraft-java-edition) (update 1.17.1)
- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases)
- (select `Minecraft Client` during installation.)
## Configuring your YAML file
### What is a YAML file and why do I need one?
Your YAML file contains a set of configuration options which provide the generator with information about how
it should generate your game. Each player of a multiworld will provide their own YAML file. This setup allows
each player to enjoy an experience customized for their taste, and different players in the same multiworld
can all have different options.
See the guide on setting up a basic YAML at the Archipelago setup
guide: [Basic Multiworld Setup Guide](/tutorial/archipelago/setup/en)
### Where do I get a YAML file?
A basic minecraft yaml will look like this.
```yaml
description: Template Name
# Your name in-game. Spaces will be replaced with underscores and
# there is a 16 character limit
name: YourName
game: Minecraft
# Shared Options supported by all games:
accessibility: locations
progression_balancing: on
# Minecraft Specific Options
# Number of advancements required (out of 92 total) to spawn the
# Ender Dragon and complete the game.
advancement_goal:
few: 0 #30
normal: 1 #50
many: 0 #70
# Modifies the level of items logically required for exploring
# dangerous areas and fighting bosses.
combat_difficulty:
easy: 0
normal: 1
hard: 0
# Junk-fills certain RNG-reliant or tedious advancements with XP rewards.
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;
# this will junk-fill them so you won't have to finish to send some items.
include_postgame_advancements:
on: 0
off: 1
#enables shuffling of villages, outposts, fortresses, bastions, and end cities.
shuffle_structures:
on: 1
off: 0
```
You can customize your settings by visiting the [Minecraft Player Settings Page](/games/Minecraft/player-settings)
## Joining a MultiWorld Game
### Obtain your Minecraft data file
### Obtain Your Minecraft Data File
**Only one yaml file needs to be submitted per minecraft world regardless of how many players play on it.**
When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that
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.
When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that 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.
Put your data file in your forge servers `APData` folder. Make sure to remove any previous data file that was in there
previously.
Double-click on your `.apmc` file to have the Minecraft client auto-launch the installed forge server. Make sure to
leave this window open as this is your server console.
### Connect to the MultiServer
After having placed your data file in the `APData` folder, start the Forge server and make sure you have OP
status by typing `/op YourMinecraftUsername` in the forge server console then connecting in your Minecraft client.
Once in game type `/connect <AP-Address> (Port) (Password)` where `<AP-Address>` is the address of the
Archipelago server. `(Port)` is only required if the Archipelago server is not using the default port of 38281. `(Password)`
is only required if the Archipleago server you are using has a password set.
Using minecraft 1.17.1 connect to the server `localhost`.
Once you are in game type `/connect <AP-Address> (Port) (Password)` where `<AP-Address>` is the address of the
Archipelago server. `(Port)` is only required if the Archipelago server is not using the default port of
38281. `(Password)` is only required if the Archipelago server you are using has a password set.
### Play the game
When the console tells you that you have joined the room, you're ready to begin playing. Congratulations
on successfully joining a multiworld game! At this point any additional minecraft players may connect to your
forge server.
When the console tells you that you have joined the room, you're all set. Congratulations on successfully joining a
multiworld game! At this point any additional minecraft players may connect to your forge server. To start the game once
everyone is ready use the command `/start`.
## Manual Installation
It is highly recommended to ues the Archipelago installer to handle the installation of the forge server for you.
support will not be given for those wishing to manually install forge. For those of you who know how, and wish to do so,
the following links are the versions of the software we use.
### Manual install Software links
- [Minecraft Forge Download Page](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.17.1.html)
- [Minecraft Archipelago Randomizer Mod Releases Page](https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases)
- **DO NOT INSTALL THIS ON YOUR CLIENT**
- [Java 16 Download Page](https://docs.aws.amazon.com/corretto/latest/corretto-16-ug/downloads-list.html)

View File

@@ -1,121 +1,148 @@
# Guia instalación de Minecraft Randomizer
# Instalacion automatica para el huesped de partida
- descarga e instala [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) and activa el
modulo `Minecraft Client`
## Software Requerido
### Servidor
- [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)
### Jugadores
- [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-edition)
## Procedimiento de instalación
### Instalación de servidor dedicado
Solo una persona ha de realizar este proceso y hospedar un servidor dedicado para que los demas jueguen conectandose a él.
1. Descarga el instalador de **Minecraft Forge** 1.16.15 desde el enlace proporcionado, siempre asegurandose de bajar la version mas reciente.
2. Ejecuta el fichero `forge-1.16.5-xx.x.x-installer.jar` y elije **install server**.
- En esta pagina elegiras ademas donde instalar el servidor, importante recordar esta localización en el siguiente paso.
3. Navega al directorio donde hayas instalado el servidor y abre `forge-1.16.5-xx.x.x.jar`
- La primera vez que lances el servidor se cerrara (o no aparecerá nada en absoluto), debería haber un fichero nuevo en el directorio llamado `eula.txt`, el cual que contiene un enlace al EULA de minecraft, cambia la linea a `eula=true` para aceptar el EULA y poder utilizar el software de servidor.
- Esto creara la estructura de directorios apropiada para el siguiente paso
4. Coloca el fichero `aprandomizer-x.x.x.jar` del segundo enlace en el directorio `mods`
- Cuando se ejecute el servidor de nuevo, generara el directorio `APData` que se necesitara para jugar
### Instalación basica para jugadores
- Compra e instala Minecraft a traves del tercer enlace.
**Y listo!**.
Los jugadores solo necesitan una version no modificada de Minecraft para jugar!
### Instalación avanzada para jugadores
***Esto no es requerido para jugar a minecraft randomizado.***
Sin embargo lo recomendamos porque hace la experiencia mas llevadera.
#### Recomended Mods
- [JourneyMap](https://www.curseforge.com/minecraft/mc-mods/journeymap) (Minimap)
1. Instala y ejecuta Minecraft al menos una vez.
2. Ejecuta el fichero `forge-1.16.5-xx.x.x-installer.jar` y elige **install client**.
- Ejecuta Minecraft forge al menos una vez para generar los directorios necesarios para el siguiente paso.
3. Navega a la carpeta de instalación de Minecraft y colocal los mods que quieras en el directorio `mods`
- Los directorios por defecto de instalación son:
- Windows `%APPDATA%\.minecraft\mods`
- macOS `~/Library/Application Support/minecraft/mods`
- Linux `~/.minecraft/mods`
## Configura tu fichero YAML
### Que es un fichero YAML y potque necesito uno?
Tu fichero YAML contiene un numero de opciones que proveen al generador con informacion sobre como debe generar tu juego.
Cada jugador de un multiworld entregara u propio fichero YAML.
Esto permite que cada jugador disfrute de una experiencia personalizada a su gusto y diferentes jugadores dentro del mismo multiworld
pueden tener diferentes opciones
Tu fichero YAML contiene un numero de opciones que proveen al generador con informacion sobre como debe generar tu
juego. Cada jugador de un multiworld entregara u propio fichero YAML. Esto permite que cada jugador disfrute de una
experiencia personalizada a su gusto y diferentes jugadores dentro del mismo multiworld pueden tener diferentes opciones
### Where do I get a YAML file?
Un fichero basico yaml para minecraft tendra este aspecto.
```yaml
# Usado para describir tu yaml. Util si tienes multiples ficheros
description: Template Name
# Tu nombre en el juego. Los espacios son reemplazados por guiones bajos, limitado a 16 caracteres
name: YourName
game: Minecraft
accessibility: locations
# Recomendado no activar esto ya que el pool de objetos de Minecraft es bastante escueto, ademas hay muchas maneras alternativas de obtener los objetivos de Minecraft.
progression_balancing: off
# Cuantos avances se necesitan para hacer aparecer el Ender Dragon y acabar el juego. few = 30, normal = 50 , many = 70
advancement_goal:
few: 0
normal: 1
many: 0
# Modifica el nivel de objetos lógicamente requeridos para explorar areas peligrosas y pelear contra jefes.
combat_difficulty:
easy: 0
normal: 1
hard: 0
# Avances que sean tediosos o basados en suerte tendran simplemente experiencia o cosas no necesarias
include_hard_advancements:
on: 0
off: 1
# Los avances extremadamente difíciles no seran requeridos; esto afecta a How Did We Get Here? y Adventuring Time.
include_insane_advancements:
on: 0
off: 1
# Los avances posteriores a Ender Dragon no tendrán objetos necesarios para que otros jugadores en el caso de un MW acaben su partida.
include_postgame_advancements:
on: 0
off: 1
# Actualmente desactivado; permite la mezcla de pueblos, puestos, fortalezas, bastiones y cuidades.
shuffle_structures:
on: 0
off: 1
```
Un fichero basico yaml para minecraft tendra este aspecto.
```yaml
description: Basic Minecraft Yaml
# Tu nombre en el juego. Espacios seran sustituidos por guinoes bajos y
# hay un limite de 16 caracteres
name: TuNombre
game: Minecraft
# Opciones compartidas por todos los juegos:
accessibility: locations
progression_balancing: on
# Opciones Especficicas para Minecraft
Minecraft:
# Numero de logros requeridos (87 max) para que aparezca el Ender Dragon y completar el juego.
advancement_goal: 50
# Numero de trozos de huevo de dragon a obtener (30 max) antes de que el Ender Dragon aparezca.
egg_shards_required: 10
# Numero de huevos disponibles en la partida (30 max).
egg_shards_available: 15
# Modifica el nivel de objetos logicamente requeridos para
# explorar areas peligrosas y luchar contra jefes.
combat_difficulty:
easy: 0
normal: 1
hard: 0
# Si off, los logros que dependan de suerte o sean tediosos tendran objetos de apoyo, no necesarios para completar el juego.
include_hard_advancements:
on: 0
off: 1
# Si off, los logros muy dificiles tendran objetos de apoyo, no necesarios para completar el juego.
# Solo afecta a How Did We Get Here? and Adventuring Time.
include_insane_advancements:
on: 0
off: 1
# Algunos logros requieren derrotar al Ender Dragon;
# Si esto se queda en off, dichos logros no tendran objetos necesarios.
include_postgame_advancements:
on: 0
off: 1
# Permite el mezclado de villas, puesto, fortalezas, bastiones y ciudades de END.
shuffle_structures:
on: 0
off: 1
# Añade brujulas de estructura al juego,
# apuntaran a la estructura correspondiente mas cercana.
structure_compasses:
on: 0
off: 1
# Reemplaza un porcentaje de objetos innecesarios por trampas abeja
# las cuales crearan multiples abejas agresivas alrededor de los jugadores cuando se reciba.
bee_traps:
0: 1
25: 0
50: 0
75: 0
100: 0
```
## Unirse a un juego MultiWorld
### Obten tu ficheros de datos Minecraft
**Solo un fichero yaml es necesario por mundo minecraft, sin importar el numero de jugadores que jueguen en el.**
Cuando te unes a un juego multiworld, se te pedirá que entregues tu fichero YAML a quien sea que hospede el juego multiworld (no confundir con hospedar el mundo minecraft).
Una vez la generación acabe, el anfitrión te dará un enlace a tu fichero de datos o un zip con los ficheros de todos.
Tu fichero de datos tiene una extensión `.apmc`.
Pon tu fichero de datos en el directorio `APData` de tu forge server. Asegurate de eliminar los que hubiera anteriormente
Cuando te unes a un juego multiworld, se te pedirá que entregues tu fichero YAML a quien sea que hospede el juego
multiworld (no confundir con hospedar el mundo minecraft). Una vez la generación acabe, el anfitrión te dará un enlace a
tu fichero de datos o un zip con los ficheros de todos. Tu fichero de datos tiene una extensión `.apmc`.
Haz doble click en tu fichero `.apmc` para que se arranque el cliente de minecraft y el servidor forge se ejecute.
### Conectar al multiserver
Despues de poner tu fichero en el directorio `APData`, arranca el Forge server y asegurate que tienes el estado OP
tecleando `/op TuUsuarioMinecraft` en la consola del servidor y entonces conectate con tu cliente Minecraft.
Una vez en juego introduce `/connect <AP-Address> (<Password>)` donde `<AP-Address>` es la dirección del servidor
Archipelago. `(<Password>)`
Una vez en juego introduce `/connect <AP-Address> (Port) (<Password>)` donde `<AP-Address>` es la dirección del
servidor. `(Port)` solo es requerido si el servidor Archipelago no esta usando el puerto por defecto 38281.
`(<Password>)`
solo se necesita si el servidor Archipleago tiene un password activo.
### Jugar al juego
Cuando la consola te diga que te has unido a la sala, estas lista/o para empezar a jugar. Felicidades
por unirte exitosamente a un juego multiworld! Llegados a este punto cualquier jugador adicional puede conectarse a tu servidor forge.
Cuando la consola te diga que te has unido a la sala, estas lista/o para empezar a jugar. Felicidades por unirte
exitosamente a un juego multiworld! Llegados a este punto cualquier jugador adicional puede conectarse a tu servidor
forge.
## Procedimiento de instalación manual
Solo es requerido si quieres usar una instalacion de forge por ti mismo, recomendamos usar el instalador de Archipelago
### Software Requerido
- [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)
**NO INSTALES ESTO EN TU CLIENTE MINECRAFT**
### Instalación de servidor dedicado
Solo una persona ha de realizar este proceso y hospedar un servidor dedicado para que los demas jueguen conectandose a
él.
1. Descarga el instalador de **Minecraft Forge** 1.16.5 desde el enlace proporcionado, siempre asegurandose de bajar la
version mas reciente.
2. Ejecuta el fichero `forge-1.16.5-xx.x.x-installer.jar` y elije **install server**.
- En esta pagina elegiras ademas donde instalar el servidor, importante recordar esta localización en el siguiente
paso.
3. Navega al directorio donde hayas instalado el servidor y abre `forge-1.16.5-xx.x.x.jar`
- La primera vez que lances el servidor se cerrara (o no aparecerá nada en absoluto), debería haber un fichero nuevo
en el directorio llamado `eula.txt`, el cual que contiene un enlace al EULA de minecraft, cambia la linea
a `eula=true` para aceptar el EULA y poder utilizar el software de servidor.
- Esto creara la estructura de directorios apropiada para el siguiente paso
4. Coloca el fichero `aprandomizer-x.x.x.jar` del segundo enlace en el directorio `mods`
- Cuando se ejecute el servidor de nuevo, generara el directorio `APData` que se necesitara para jugar

View File

@@ -3,29 +3,39 @@
## Nödvändig Mjukvara
### Server Värd
- [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)
### Spelare
- [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-edition)
## Installationsprocedurer
### Tillägnad
Bara en person behöver göra denna uppsättning och vara värd för en server för alla andra spelare att koppla till.
1. Ladda ner 1.16.5 **Minecraft Forge** installeraren från länken ovanför och se till att ladda ner den senaste rekommenderade versionen.
1. Ladda ner 1.16.5 **Minecraft Forge** installeraren från länken ovanför och se till att ladda ner den senaste
rekommenderade versionen.
2. Kör `forge-1.16.5-xx.x.x-installer.jar` filen och välj **installera server**.
- På denna sida kommer du också välja vart du ska installera servern för att komma ihåg denna katalog. Detta är viktigt för nästa steg.
- På denna sida kommer du också välja vart du ska installera servern för att komma ihåg denna katalog. Detta är
viktigt för nästa steg.
3. Navigera till vart du har installerat servern och öppna `forge-1.16.5-xx.x.x-installer.jar`
- Under första serverstart så kommer den att stängas ner och fråga dig att acceptera Minecrafts EULA. En ny fil kommer skapas vid namn `eula.txt` som har en länk till Minecrafts EULA, och en linje som du behöver byta till `eula=true` för att acceptera Minecrafts EULA.
- Under första serverstart så kommer den att stängas ner och fråga dig att acceptera Minecrafts EULA. En ny fil
kommer skapas vid namn `eula.txt` som har en länk till Minecrafts EULA, och en linje som du behöver byta
till `eula=true` för att acceptera Minecrafts EULA.
- Detta kommer skapa de lämpliga katalogerna för dig att placera filerna i de följande steget.
4. Placera `aprandomizer-x.x.x.jar` länken ovanför i `mods` mappen som ligger ovanför installationen av din forge server.
4. Placera `aprandomizer-x.x.x.jar` länken ovanför i `mods` mappen som ligger ovanför installationen av din forge
server.
- Kör servern igen. Den kommer ladda up och generera den nödvändiga katalogen `APData` för när du är redo att spela!
### Grundläggande Spelaruppsättning
- Köp och installera Minecraft från länken ovanför.
**Du är klar**.
@@ -33,10 +43,12 @@ Bara en person behöver göra denna uppsättning och vara värd för en server f
Andra spelare behöver endast ha en 'Vanilla' omodifierad version av Minecraft för att kunna spela!
### Avancerad Spelaruppsättning
***Detta är inte nödvändigt för att spela ett slumpmässigt Minecraftspel.***
Dock så är det rekommenderat eftersom det hjälper att göra upplevelsen mer trevligt.
#### Rekommenderade Moddar
- [JourneyMap](https://www.curseforge.com/minecraft/mc-mods/journeymap) (Minimap)
@@ -52,17 +64,20 @@ Dock så är det rekommenderat eftersom det hjälper att göra upplevelsen mer t
## Konfigurera Din YAML-fil
### Vad är en YAML-fil och varför behöver jag en?
Din YAML-fil behåller en uppsättning av konfigurationsalternativ som ger generatorn med information om hur
den borde generera ditt spel. Varje spelare i en multivärld kommer behöva ge deras egen YAML-fil. Denna uppsättning tillåter
varje spelare att an njuta av en upplevelse anpassade för deras smaker, och olika spelare i samma multivärld
kan ha helt olika alternativ.
Din YAML-fil behåller en uppsättning av konfigurationsalternativ som ger generatorn med information om hur den borde
generera ditt spel. Varje spelare i en multivärld kommer behöva ge deras egen YAML-fil. Denna uppsättning tillåter varje
spelare att an njuta av en upplevelse anpassade för deras smaker, och olika spelare i samma multivärld kan ha helt olika
alternativ.
### Vart kan jag få tag i en YAML-fil?
En grundläggande Minecraft YAML kommer se ut så här.
```yaml
description: Template Name
# Ditt spelnamn. Mellanslag kommer bli omplacerad med understräck och det är en 16-karaktärsgräns.
name: YourName
name: YourName
game: Minecraft
accessibility: locations
progression_balancing: off
@@ -88,27 +103,32 @@ shuffle_structures:
off: 0
```
För mer detaljer om vad varje inställning gör, kolla standardinställningen `PlayerSettings.yaml` som kommer med Archipelago-installationen.
För mer detaljer om vad varje inställning gör, kolla standardinställningen `PlayerSettings.yaml` som kommer med
Archipelago-installationen.
## Gå med i ett Multivärld-spel
### Skaffa din Minecraft data-fil
**Endast en YAML-fil behöver användats per Minecraft-värld oavsett hur många spelare det är som spelar.**
När du går med it ett Multivärld spel så kommer du bli ombedd att lämna in din YAML-fil till personen som värdar. När detta
är klart så kommer värden att ge dig antingen en länk till att ladda ner din data-fil, eller mer en zip-fil som innehåller allas data-filer.
Din data-fil borde ha en `.apmc` -extension.
När du går med it ett Multivärld spel så kommer du bli ombedd att lämna in din YAML-fil till personen som värdar. När
detta är klart så kommer värden att ge dig antingen en länk till att ladda ner din data-fil, eller mer en zip-fil som
innehåller allas data-filer. Din data-fil borde ha en `.apmc` -extension.
Lägg din data-fil i dina forge-servrar `APData` -mapp. Se till att ta bort alla tidigare data-filer som var i där förut.
### Koppla till Multiservern
Efter du har placerat din data-fil i `APData` -mappen, starta forge-servern och se till att you har OP-status
genom att skriva `/op DittAnvändarnamn` i forger-serverns konsol innan du kopplar dig till din Minecraft klient.
När du är inne i spelet, skriv `/connect <AP-Address> (<Lösenord>)` där `<AP-Address>` är addressen av
Archipelago-servern. `(<Lösenord>)` är endast nödvändigt om Archipelago-servern som du använder har ett tillsatt lösenord.
Efter du har placerat din data-fil i `APData` -mappen, starta forge-servern och se till att you har OP-status genom att
skriva `/op DittAnvändarnamn` i forger-serverns konsol innan du kopplar dig till din Minecraft klient. När du är inne i
spelet, skriv `/connect <AP-Address> (<Lösenord>)` där `<AP-Address>` är addressen av
Archipelago-servern. `(<Lösenord>)` är endast nödvändigt om Archipelago-servern som du använder har ett tillsatt
lösenord.
### Spela spelet
När konsolen har informerat att du har gått med i rummet så är du redo att börja spela. Grattis
att du har lykats med att gått med i ett Multivärld-spel! Vid detta tillfälle, alla ytterligare Minecraft-spelare må koppla
in till din forge-server.
När konsolen har informerat att du har gått med i rummet så är du redo att börja spela. Grattis att du har lykats med
att gått med i ett Multivärld-spel! Vid detta tillfälle, alla ytterligare Minecraft-spelare må koppla in till din
forge-server.

View File

@@ -0,0 +1,89 @@
# Raft Randomizer Setup Guide
## Required Software
- [Raft](https://store.steampowered.com/app/648800/Raft/)
- [Raft Mod Loader](https://www.raftmodding.com/loader) ("*RML*")
- [Raftipelago mod](https://www.raftmodding.com/mods/raftipelago)
## Installation Procedures
1. Install Raft. The currently-supported Raft version is Update 13: The Renovation Update. If you plan on playing Raft mainly with Archipelago, it's recommended to disable Raft auto-updating through Steam, as there is no beta channel to get old builds.
2. Install RML.
3. Install the Raftipelago mod from the Raft Modding website. You should open the auto-installation link on the webpage through RML. Alternatively, you can download the .rmod file and place it in the Mods folder manually.
4. Open RML and click Play. If you've already installed it, the shortcut in the Start Menu is called "RMLLauncher.exe". Raft should start.
5. Open the RML menu. This should open automatically when Raft first loads. If it does not, and you see RML information in the top center of the Raft main menu, press F9 to open it.
6. Navigate to the "Mod manager" tab in the left-hand menu.
7. Click on the plug icon for Raftipelago to load the mod.
## Installation Troubleshooting
You can press F10 to open the console to view any errors when loading the mod.
### DLL/Reflection/Image errors
Restart Raft and try again. These should be ephemeral errors.
### RML says to start Raft through Steam
If this happens, then RML is configured to only inject into an existing instance of Raft, rather than try and start a new one.
You can either:
- Click "Play" after Raft has loaded into the main menu
- Uncheck the box next to the "Disable Automatic Game Start" setting in the Settings menu then click Play.
### RML doesn't do anything when I click Play
If this happens, then RML is configured to only start a new instance of Raft, then inject into that specific instance. This also means that RML has detected an instance of Raft is already running, and will not start a new one.
You can either:
- Close the existing instance of Raft then click Play
- Check the box next to the "Disable Automatic Game Start" setting in the Settings menu then click Play.
## Joining a MultiWorld Game
1. Ensure you're on the Main Menu with Raftipelago loaded.
2. Open the Debug Console by pressing F10.
3. Type */connect {serverAddress} {username} {password}* into the console and hit Enter.
- Example: */connect archipelago.gg:12345 SunnyBat*
- serverAddress must not contain spaces.
- If your username or password contains spaces, surround that value with quotation marks ("). Adding quotation marks even when not necessary (eg "SunnyBat") is fine.
- If your username or password starts with a quotation mark, surround the value with an additional set of quotation marks (eg the value *"myP@s$w0rD* would be entered as *""myP@s$w0rD"*).
4. Start a new game or load an existing one.
- Raftipelago save games are marked as *incompatible* with vanilla Raft. This means when Raftipelago is not loaded, saves made with Raftipelago will show as corrupt/unselectable.
- Avoid using an existing game that was not created with your current run of Raftipelago (either vanilla or a different Raftipelago run). It will work, but if anything is unlocked, it will be automatically registered with Archipelago once the world is loaded. This is irreversible.
5. You can disconnect from an Archipelago server by typing */disconnect confirmDisconnect* into the console and hitting Enter.
## Multiplayer Raft
You're able to have multiple Raft players on a single Raftipelago world. This will work, with a few notes:
- Only the player that creates/loads the world can connect to Archipelago (this is the "host" of the Raft world). Other players do not need to connect; everything will be routed through the the host.
- Resource Packs are only received by the host and any other players connected to the Raft world when the resource pack is received.
- Players other than the host will be labeled as a "Raft Player (Steam name)" when using ingame chat, which will be routed through Archipelago chat.
- Ingame chat will only work when the host is connected to the Archipelago server.
## Game Troubleshooting
### The "Load game" button is disabled for my world / my world is corrupt
Be sure that you click the "Load game" button **after** you load Raftipelago. You can click the Load Game button again to reload all of the saves in your folder (there is no need to restart Raft if the mod loaded successfully).
### I'm certain I'm doing things correctly, but the world is still not loadable
You can bypass Raftipelago world verification checks by loading a backup of the world. If the backup is not loadable, the world is corrupted.
In the future, be sure that when you save the game, the Raftipelago mod is loaded.
### I disconnected from the server! What do I do to reconnect?
Open the console with F10 and type the */connect* command with your server/username/password in again. You do not need to save+quit to the main menu beforehand.

View File

@@ -0,0 +1,52 @@
# Rogue Legacy Randomizer Setup Guide
## Required Software
- Rogue Legacy Randomizer from
the [Rogue Legacy Randomizer Releases Page](https://github.com/ThePhar/RogueLegacyRandomizer/releases)
## Configuring your YAML file
### What is a YAML file and why do I need one?
Your YAML file contains a set of configuration options which provide the generator with information about how it should
generate your game. Each player of a multiworld will provide their own YAML file. This setup allows each player to enjoy
an experience customized for their taste, and different players in the same multiworld can all have different options.
### Where do I get a YAML file?
you can customize your settings by visiting the [Rogue Legacy Settings Page](/games/Rogue%20Legacy/player-settings).
### Connect to the MultiServer
Once in game, press the start button and the AP connection screen should appear. You will fill out the hostname, port,
slot name, and password (if applicable). You should only need to fill out hostname, port, and password if the server
provides an alternative one to the default values.
### Play the game
Once you have entered the required values, you go to Connect and then select Confirm on the "Ready to Start" screen. Now
you're off to start your legacy!
## Manual Installation
In order to run Rogue Legacy Randomizer you will need to have Rogue Legacy installed on your local machine. Extract the
Randomizer release into a desired folder **outside** of your Rogue Legacy install. Copy the following files from your
Rogue Legacy install into the main directory of your Rogue Legacy Randomizer install:
- DS2DEngine.dll
- InputSystem.dll
- Nuclex.Input.dll
- SpriteSystem.dll
- Tweener.dll
And copy the directory from your Rogue Legacy install as well into the main directory of your Rogue Legacy Randomizer
install:
- Content/
Then copy the contents of the CustomContent directory in your Rogue Legacy Randomizer into the newly copied Content
directory and overwrite all files.
**BE SURE YOU ARE REPLACING THE COPIED FILES IN YOUR ROGUE LEGACY RANDOMIZER DIRECTORY AND NOT REPLACING YOUR ROGUE
LEGACY FILES!**

View File

@@ -0,0 +1,107 @@
# 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.
[r2modman Page](https://thunderstore.io/package/ebkr/r2modman/)
### Install Archipelago Mod using r2modman
You can install the Archipelago mod using r2modman in one of two ways.
[Archipelago Mod Download Page](https://thunderstore.io/package/ArchipelagoMW/Archipelago/)
One, you can use the Thunderstore website and click on the "Install with Mod Manager" link.
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
item_weights:
default: 50
new: 0
uncommon: 0
legendary: 0
lunartic: 0
chaos: 0
no_scraps: 0
even: 0
scraps_only: 0
item_pool_presets: true
# custom item weights
green_scrap: 16
red_scrap: 4
yellow_scrap: 1
white_scrap: 32
common_item: 64
uncommon_item: 32
legendary_item: 8
boss_item: 4
lunar_item: 16
equipment: 32
```
| 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 - 100 |
| 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 |
| item_weights | Each option here is a preset item weight that can be used to customize your generate item pool with certain settings. | default, new, uncommon, legendary, lunartic, chaos, no_scraps, even, scraps_only |
| item_pool_presets | A simple toggle to determine whether the item_weight presets are used or the custom item pool as defined below | true/false |
| custom item weights | Each defined item here is a single item in the pool that will have a weight against the other items when the item pool gets generated. These values can be modified to adjust how frequently certain items appear | 0-100|
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)
The player will have the default preset generated item pool with the custom item weights being ignored. (item_weights:
default and item_pool_presets: true)

View File

@@ -0,0 +1,129 @@
# Secret of Evermore Setup Guide
## Required Software
- SNI from: [SNI Releases Page](https://github.com/alttpo/sni/releases)
- v0.0.59 or newer (included in Archipelago 0.2.1 setup)
- Hardware or software capable of loading and playing SNES ROM files
- An emulator capable of connecting to SNI with ROM access. Any one of the following will work:
- snes9x-rr from: [snes9x-rr Releases Page](https://github.com/gocha/snes9x-rr/releases)
- BizHawk from: [BizHawk Website](http://tasvideos.org/BizHawk.html)
- bsnes-plus-nwa from: [bsnes-plus GitHub](https://github.com/black-sliver/bsnes-plus)
- Or SD2SNES, FXPak Pro ([FXPak Pro Store Page](https://krikzz.com/store/home/54-fxpak-pro.html)), or other
compatible hardware.
- Your legally obtained Secret of Evermore US ROM file, probably named `Secret of Evermore (USA).sfc`
## Create a Config (.yaml) File
### What is a config file and why do I need one?
See the guide on setting up a basic YAML at the Archipelago setup
guide: [Basic Multiworld Setup Guide](/tutorial/archipelago/setup/en)
### Where do I get a config file?
The Player Settings page on the website allows you to configure your personal settings and export a config file from
them. Player settings page: [Secret of Evermore Player Settings PAge](/games/Secret%20of%20Evermore/player-settings)
### Verifying your config file
If you would like to validate your config file to make sure it works, you may do so on the YAML Validator
page: [YAML Validation page](/mysterycheck)
## Generating a Single-Player Game
Stand-alone "Evermizer" has a way of balancing single-player games, but may not always be on par feature-wise. Head over
to the [Evermizer Website](https://evermizer.com) if you want to try the official stand-alone, otherwise read below.
1. Navigate to the Player Settings page, configure your options, and click the "Generate Game" button.
- Player Settings page: [Secret of Evermore Player Settings Page](/games/Secret%20of%20Evermore/player-settings)
2. You will be presented with a "Seed Info" page.
3. Click the "Create New Room" link.
4. You will be presented with a server page, from which you can download your patch file.
5. Run your patch file through the apbpatch on evermizer.com and load it in your emulator or console.
* apbpatch page: [Evermizer apbpatch Page](https://evermizer.com/apbpatch)
## Joining a MultiWorld Game
### Obtain your patch file and create your ROM
When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done,
the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch
files. Your patch file should have a `.apsoe` extension.
Put your patch file on your desktop or somewhere convenient, open the apbpatch page on evermizer.com and generate your
ROM from it. Load the ROM file in your emulator or console. apbpatch
page: [Evermizer apbpatch Page](https://evermizer.com/apbpatch)
### Connect to SNI
#### With an emulator
Start SNI either from the Archipelago install folder or the stand-alone version. If this is its first time launching,
you may be prompted to allow it to communicate through the Windows Firewall.
##### snes9x-rr
1. Load your ROM file if it hasn't already been loaded.
2. Click on the File menu and hover on **Lua Scripting**
3. Click on **New Lua Script Window...**
4. In the new window, click **Browse...**
5. Select the `Connector.lua` file from your SNI installation:
* `SNI/lua/x86/Connector.lua` for 32bit snes9x-rr or
* `SNI/lua/x64/Connector.lua` for "x64" snes9x-rr
6. Leave the Lua window open while you are playing.
* If the script window complains about missing `socket.dll` make sure it is in the lua directory.
* If the script window complains about "Bad EXE format", use the other Connector above (x86 <-> x64)
##### BizHawk
1. Ensure you have the BSNES core loaded. You may do this by clicking on the Tools menu in BizHawk and following these
menu options:
`Config --> Cores --> SNES --> BSNES`
Once you have changed the loaded core, you must restart BizHawk.
2. Load your ROM file if it hasn't already been loaded.
3. Click on the Tools menu and click on **Lua Console**
4. Click the button to open a new Lua script.
5. Select any `Connector.lua` file from your SNI installation
##### bsnes-plus-nwa
This should automatically connect to SNI. If this is its first time launching, you may be prompted to allow it to
communicate through the Windows Firewall.
#### With hardware
This guide assumes you have downloaded the correct firmware for your device. If you have not done so already, please do
this now. SD2SNES and FXPak Pro users may download the appropriate firmware on the SD2SNES releases page. SD2SNES
releases page: [SD2SNES Releases Page](https://github.com/RedGuyyyy/sd2snes/releases)
Other hardware may find helpful information on the usb2snes platforms
page: [usb2snes Supported Platforms Page](http://usb2snes.com/#supported-platforms)
1. Copy the ROM file to your SD card.
2. Load the ROM file from the menu.
### Open the client
Open ap-soeclient ([Evermizer Archipelago Client Page](http://evermizer.com/apclient)) in a modern browser. Do not
switch tabs, open it in a new window if you want to use the browser while playing. Do not minimize the window with the
client.
The client should automatically connect to SNI, the "SNES" status should change to green.
### Connect to the Archipelago Server
Enter `/connect server:port` in the client's command prompt and press enter. You'll find `server:port` on the same page
that had the patch file.
### Play the game
When the game is loaded but not yet past the intro cutscene, the "Game" status is yellow. When the client shows "AP" as
green and "Game" as yellow, you're ready to play. The intro can be skipped pressing the START button and "Game" should
change to green. Congratulations on successfully joining a multiworld game!
## Hosting a MultiWorld game
The recommended way to host a game is to use our hosting service on the [seed generation page](/generate). Or check out
the Archipelago website guide for more information: [Archipelago Website Guide](/tutorial/archipelago/using_website/en)

View File

@@ -0,0 +1,36 @@
# Slay the Spire Setup Guide
## Required Software
For steam-based installation, subscribe to the following mods:
- ModTheSpire from the [Slay the Spire Workshop](https://steamcommunity.com/sharedfiles/filedetails/?id=1605060445)
- BaseMod from the [Slay the Spire Workshop](https://steamcommunity.com/workshop/filedetails/?id=1605833019)
- Archipelago Multiworld Randomizer Mod from
the [Slay the Spire Workshop](https://steamcommunity.com/sharedfiles/filedetails/?id=2596397288)
## Configuring your YAML file
### What is a YAML file and why do I need one?
Your YAML file contains a set of configuration options which provide the generator with information about how it should
generate your game. Each player of a multiworld will provide their own YAML file. This setup allows each player to enjoy
an experience customized for their taste, and different players in the same multiworld can all have different options.
### Where do I get a YAML file?
you can customize your settings by visiting
the [Slay the Spire Settings Page](/games/Slay%20the%20Spire/player-settings).
### Connect to the MultiServer
For Steam-based installations, if you are subscribed to ModTheSpire, when you launch the game, you should have the
option to launch the game with mods. On the mod loader screen, ensure you only have the following mods enabled and then
start the game:
- BaseMod
- Archipelago Multiworld Randomizer
Once you are in-game, you will be able to click the **Archipelago** menu option and enter the ip and port (separated by
a colon) in the hostname field and enter your player slot name in the Slot Name field. Then click connect, and now you
are ready to climb the spire!

View File

@@ -0,0 +1,82 @@
# Super Mario 64 EX MultiWorld Setup Guide
## Required Software
- Super Mario 64 US Rom (Japanese may work also. Europe and Shindou not supported)
- Either of [sm64pclauncher](https://github.com/N00byKing/sm64pclauncher/releases) or
- Cloning and building [sm64ex](https://github.com/N00byKing/sm64ex) manually.
NOTE: The above linked sm64pclauncher is a special version designed to work with the Archipelago build of sm64ex.
You can use other sm64-port based builds with it, but you can't use a different launcher with the Archipelago build of sm64ex.
## Installation and Game Start Procedures
# Installation via sm64pclauncher (For Windows)
First, install [MSYS](https://www.msys2.org/) as described on the page. DO NOT INSTALL INTO A FOLDER PATH WITH SPACES.
Do all steps up to including step 6.
Best use default install directory.
Then follow the steps below
1. Go to the page linked for sm64pclauncher, and press on the topmost entry
3. Scroll down, and download the zip file
4. Unpack the zip file in an empty folder
5. Run the Launcher and press build.
6. Set the location where you installed MSYS when prompted. Check the "Install Dependencies" Checkbox
7. Set the Repo link to `https://github.com/N00byKing/sm64ex` and the Branch to `archipelago` (Top two boxes). You can choose the folder (Secound Box) at will, as long as it does not exist yet
8. Point the Launcher to your Super Mario 64 US/JP Rom, and set the Region correspondingly
9. Set Build Options. Recommended: `-jn` where `n` is the Number of CPU Cores, to build faster.
10. SM64EX will now be compiled. The Launcher will appear to have crashed, but this is not likely the case. Best wait a bit, but there may be a problem if it takes longer than 10 Minutes
After it's done, the Build list should have another entry titled with what you named the folder in step 7.
NOTE: For some reason first start of the game always crashes the launcher. Just restart it.
If it still crashes, recheck if you typed the launch options correctly (Described in "Joining a MultiWorld Game")
# Manual Compilation (Linux/Windows)
Dependencies for Linux: `sdl2 glew cmake python make`.
Dependencies for Windows: `mingw-w64-x86_64-gcc mingw-w64-x86_64-glew mingw-w64-x86_64-SDL2 git make python3 cmake`
SM64EX will link `jsoncpp` dynamic if installed. If not, it will compile and link statically.
1. Clone `https://github.com/N00byKing/sm64ex` recursively
2. Enter `sm64ex` and copy your Rom to `baserom.REGION.z64` where `REGION` is either `us` or `jp` respectively.
3. Compile with `make`. For faster compilation set the parameter `-jn` where `n` is the Number of CPU Cores.
The Compiled binary will be in `build/REGION_pc/`.
# Joining a MultiWorld Game
To join, set the following launch options: `--sm64ap_name YourName --sm64ap_ip ServerIP:Port`.
Optionally, add `--sm64ap_passwd "YourPassword"` if the room you are using requires a password. All parameters without quotation marks.
The Name in this case is the one specified in your generated .yaml file.
In case you are using the Archipelago Website, the IP should be `archipelago.gg` and Port `38281`.
If everything worked out, you will see a textbox informing you the connection has been established after the story intro.
## Installation Troubleshooting
Start the game from the command line to view helpful messages regarding SM64EX.
### Game doesn't start after compiling
Most likely you forgot to set the launch options. `--sm64ap_name YourName` and `--sm64ap_ip ServerIP:Port` are required for startup.
If your Name or Password have spaces in them, surround them in quotes.
## Game Troubleshooting
### Known Issues
When using a US Rom, the In-Game messages are missing some letters: `J Q V X Z` and `?`.
The Japanese Version should have no problem displaying these.
### What happens if I lose connection?
SM64EX tries to reconnect a few times, so be patient.
Should the problem still be there after about a minute or two, just save and restart the game.
### How do I update the Game to a new Build?
When manually compiling just pull in changes and run `make` again. Sometimes it helps to run `make clean` before.
When using the Launcher follow the normal build steps, but when choosing a folder name use the same as before. Then continue as normal.

View File

@@ -0,0 +1,150 @@
# Super Metroid Setup Guide
## Required Software
- One of the client programs:
- [SNIClient](https://github.com/ArchipelagoMW/Archipelago/releases), included with the main
Archipelago install. Make sure to check the box for `SNI Client - Super Metroid Patch Setup`
- [SuperNintendoClient](https://github.com/ArchipelagoMW/SuperNintendoClient/releases), an alternate standalone
client for Super Nintendo games
- Hardware or software capable of loading and playing SNES ROM files
- An emulator capable of connecting to SNI such as:
- snes9x Multitroid
from: [snes9x Multitroid Download](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz),
- BizHawk from: [BizHawk Website](http://tasvideos.org/BizHawk.html)
- An SD2SNES, FXPak Pro ([FXPak Pro Store Page](https://krikzz.com/store/home/54-fxpak-pro.html)), or other
compatible hardware
- Your legally obtained Super Metroid ROM file, probably named `Super Metroid (Japan, USA).sfc`
## Installation Procedures
### Windows Setup
1. During the installation of Archipelago, you will have been asked to install the SNI Client. If you did not do this,
or you are on an older version, you may run the installer again to install the SNI Client.
2. During setup, you will be asked to locate your base ROM file. This is your Super Metroid ROM file.
3. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
files.
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
2. Right-click on a ROM file and select **Open with...**
3. Check the box next to **Always use this app to open .sfc files**
4. Scroll to the bottom of the list and click the grey text **Look for another App on this PC**
5. Browse for your emulator's `.exe` file and click **Open**. This file should be located inside the folder you
extracted in step one.
### Macintosh Setup
- We need volunteers to help fill this section! Please contact **Farrak Kilhn** on Discord if you want to help.
## Create a Config (.yaml) File
### What is a config file and why do I need one?
See the guide on setting up a basic YAML at the Archipelago setup
guide: [Basic Multiworld Setup Guide](/tutorial/archipelago/setup/en)
### Where do I get a config file?
The Player Settings page on the website allows you to configure your personal settings and export a config file from
them. Player settings page: [Super Metroid Player Settings Page](/games/Super%20Metroid/player-settings)
### Verifying your config file
If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML
validator page: [YAML Validation page](/mysterycheck)
## Generating a Single-Player Game
1. Navigate to the Player Settings page, configure your options, and click the "Generate Game" button.
- Player Settings page: [Super Metroid Player Settings Page](/games/Super%20Metroid/player-settings)
2. You will be presented with a "Seed Info" page.
3. Click the "Create New Room" link.
4. You will be presented with a server page, from which you can download your patch file.
5. Double-click on your patch file, and the Super Metroid Client will launch automatically, create your ROM from the
patch file, and open your emulator for you.
6. Since this is a single-player game, you will no longer need the client, so feel free to close it.
## Joining a MultiWorld Game
### Obtain your patch file and create your ROM
When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done,
the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch
files. Your patch file should have a `.apm3` extension.
Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically launch the
client, and will also create your ROM in the same place as your patch file.
### Connect to the client
#### With an emulator
When the client launched automatically, SNI should have also automatically launched in the background. If this is its
first time launching, you may be prompted to allow it to communicate through the Windows Firewall.
##### snes9x Multitroid
1. Load your ROM file if it hasn't already been loaded.
2. Click on the File menu and hover on **Lua Scripting**
3. Click on **New Lua Script Window...**
4. In the new window, click **Browse...**
5. Select the connector lua file included with your client
- SuperNintendoClient users should download `sniConnector.lua` from the client download page
- SNIClient users should look in their Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
emulator is 64-bit or 32-bit.
##### BizHawk
1. Ensure you have the BSNES core loaded. You may do this by clicking on the Tools menu in BizHawk and following these
menu options:
`Config --> Cores --> SNES --> BSNES`
Once you have changed the loaded core, you must restart BizHawk.
2. Load your ROM file if it hasn't already been loaded.
3. Click on the Tools menu and click on **Lua Console**
4. Click the button to open a new Lua script.
5. Select the `Connector.lua` file included with your client
- SuperNintendoClient users should download `sniConnector.lua` from the client download page
- SNIClient users should look in their Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
emulator is 64-bit or 32-bit. Please note the most recent versions of BizHawk are 64-bit only.
#### With hardware
This guide assumes you have downloaded the correct firmware for your device. If you have not done so already, please do
this now. SD2SNES and FXPak Pro users may download the appropriate firmware on the SD2SNES releases page. SD2SNES
releases page: [SD2SNES Releases Page](https://github.com/RedGuyyyy/sd2snes/releases)
Other hardware may find helpful information on the usb2snes platforms
page: [usb2snes Supported Platforms Page](http://usb2snes.com/#supported-platforms)
1. Close your emulator, which may have auto-launched.
2. Power on your device and load the ROM.
### Connect to the Archipelago Server
The patch file which launched your client should have automatically connected you to the AP Server. There are a few
reasons this may not happen however, including if the game is hosted on the website but was generated elsewhere. If the
client window shows "Server Status: Not Connected", simply ask the host for the address of the server, and copy/paste it
into the "Server" input field then press enter.
The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected".
### Play the game
When the client shows both SNES Device and Server as connected, you're ready to begin playing. Congratulations on
successfully joining a multiworld game!
## Hosting a MultiWorld game
The recommended way to host a game is to use our hosting service. The process is relatively simple:
1. Collect config files from your players.
2. Create a zip file containing your players' config files.
3. Upload that zip file to the Generate page above.
- Generate page: [WebHost Seed Generation Page](/generate)
4. Wait a moment while the seed is generated.
5. When the seed is generated, you will be redirected to a "Seed Info" page.
6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players, so
they may download their patch files from there.
7. Note that a link to a MultiWorld Tracker is at the top of the room page. The tracker shows the progress of all
players in the game. Any observers may also be given the link to this page.
8. Once all players have joined, you may begin playing.

View File

@@ -0,0 +1,50 @@
# Timespinner Randomizer Installationsanweisungen
## Benötigte Software
- [Timespinner (Steam)](https://store.steampowered.com/app/368620/Timespinner/)
, [Timespinner (Humble)](https://www.humblebundle.com/store/timespinner)
oder [Timespinner (GOG)](https://www.gog.com/game/timespinner) (andere Versionen werden nicht unterstützt)
- [Timespinner Randomizer](https://github.com/JarnoWesthof/TsRandomizer)
## Wie funktioniert's?
Der Timespinner Randomizer lädt die Timespinner.exe im gleichen Verzeichnis und verändert seine Speicherinformationen um
die Randomisierung der Gegenstände zu erlauben
## Installationsanweisungen
1. Die aktuellsten Dateien des Randomizers findest du ganz oben auf dieser
Webseite: [Timespinner Randomizer Releases](https://github.com/JarnoWesthof/TsRandomizer/releases). Lade dir unter '
Assets' die .zip Datei für dein Betriebssystem herunter
2. Entpacke die .zip Datei im Ordner, in dem das Spiel Timespinner installiert ist
## Den Randomizer starten
- auf Windows: Starte die Datei TsRandomizer.exe
- auf Linux: Starte die Datei TsRandomizer.bin.x86_64
- auf Mac: Starte die Datei TsRandomizer.bin.osx
... im Ordner in dem die Inhalte aus der .zip Datei entpackt wurden
Weitere Informationen zum Randomizer findest du hier: [ReadMe](https://github.com/JarnoWesthof/TsRandomizer)
## An einer Multiworld teilnehmen
1. Starte den Randomizer wie unter [Den Randomizer starten](#Den-Randomizer-starten) beschrieben
2. Wähle "New Game"
3. Wechsle "<< Select Seed >>" zu "<< Archiplago >>" indem du "links" auf deinem Controller oder der Tastatur drückst
4. Wähle "<< Archipelago >>" um ein neues Menu zu öffnen, wo du deine Logininformationen für Archipelago eingeben kannst
* ANMERKUNG: Die Eingabefelder unterstützen das Einfügen von Informationen mittels 'Ctrl + V'
5. Wähle "Connect"
6. Wenn alles funktioniert hat, wechselt das Spiel zurück zur Auswahl des Schwierigkeitsgrads und das Spiel startet,
sobald du einen davon ausgewählt hast
## Woher bekomme ich eine Konfigurationsdatei?
Die [Player Settings](https://archipelago.gg/games/Timespinner/player-settings) Seite auf der Website erlaubt dir,
persönliche Einstellungen zu definieren und diese in eine Konfigurationsdatei zu exportieren
* Die Timespinner Randomizer Option "StinkyMaw" ist in Archipelago Seeds aktuell immer an
* Die Timespinner Randomizer Optionen "ProgressiveVerticalMovement" & "ProgressiveKeycards" werden in Archipelago Seeds
aktuell nicht unterstützt

View File

@@ -0,0 +1,41 @@
# Timespinner Randomizer Setup Guide
## Required Software
- [Timespinner (Steam)](https://store.steampowered.com/app/368620/Timespinner/)
, [Timespinner (Humble)](https://www.humblebundle.com/store/timespinner)
or [Timespinner (GOG)](https://www.gog.com/game/timespinner) (other versions are not supported)
- [Timespinner Randomizer](https://github.com/JarnoWesthof/TsRandomizer)
## General Concept
The Timespinner Randomizer loads Timespinner.exe from the same folder, and alters its state in memory to allow for
randomization of the items
## Installation Procedures
Download latest release on [Timespinner Randomizer Releases](https://github.com/JarnoWesthof/TsRandomizer/releases) you
can find the .zip files on the releases page. Download the zip for your current platform. Then extract the zip to the
folder where your Timespinner game is installed. Then just run TsRandomizer.exe (on Windows) or
TsRandomizer.bin.x86_64 (on Linux) or TsRandomizer.bin.osx (on Mac) instead of Timespinner.exe to start the game in
randomized mode. For more info see the [ReadMe](https://github.com/JarnoWesthof/TsRandomizer)
## Joining a MultiWorld Game
1. Run TsRandomizer.exe
2. Select "New Game"
3. Switch "<< Select Seed >>" to "<< Archipelago >>" by pressing left on your controller or keyboard
4. Select "<< Archipelago >>" to open a new menu where you can enter your Archipelago login credentials
* NOTE: the input fields support Ctrl + V pasting of values
5. Select "Connect"
6. If all went well you will be taken back to the difficulty selection menu and the game will start as soon as you
select a difficulty
## Where do I get a config file?
The [Player Settings](https://archipelago.gg/games/Timespinner/player-settings) page on the website allows you to
configure your personal settings and export them into a config file
* The Timespinner Randomizer option "StinkyMaw" is currently always enabled for Archipelago generated seeds
* The Timespinner Randomizer options "ProgressiveVerticalMovement" & "ProgressiveKeycards" are currently not supported
on Archipelago generated seeds

View File

@@ -1,4 +1,96 @@
[
{
"gameTitle": "Archipelago",
"tutorials": [
{
"name": "Multiworld Setup Tutorial",
"description": "A guide to setting up the Archipelago software to generate and host multiworld games on your computer and using the website.",
"files": [
{
"language": "English",
"filename": "archipelago/setup_en.md",
"link": "archipelago/setup/en",
"authors": [
"alwaysintreble"
]
}
]
},
{
"name": "Archipelago Website User Guide",
"description": "A guide to using the Archipelago website to generate multiworlds or host pre-generated multiworlds.",
"files": [
{
"language": "English",
"filename": "archipelago/using_website.md",
"link": "archipelago/using_website/en",
"authors": [
"alwaysintreble"
]
}
]
},
{
"name": "Archipelago Server and Client Commands",
"description": "A guide detailing the commands available to the user when participating in an Archipelago session.",
"files": [
{
"language": "English",
"filename": "archipelago/commands_en.md",
"link": "archipelago/commands/en",
"authors": [
"jat2980",
"Ijwu"
]
}
]
},
{
"name": "Advanced YAML Guide",
"description": "A guide to reading yaml files and editing them to fully customize your game.",
"files": [
{
"language": "English",
"filename": "archipelago/advanced_settings_en.md",
"link": "archipelago/advanced_settings/en",
"authors": [
"alwaysintreble",
"Alchav"
]
}
]
},
{
"name": "Archipelago Triggers Guide",
"description": "A guide to setting up and using triggers in your game settings.",
"files": [
{
"language": "English",
"filename": "archipelago/triggers_en.md",
"link": "archipelago/triggers/en",
"authors": [
"alwaysintreble"
]
}
]
},
{
"name": "Archipelago Plando Guide",
"description": "A guide to understanding and using plando for your game.",
"files": [
{
"language": "English",
"filename": "archipelago/plando_en.md",
"link": "archipelago/plando/en",
"authors": [
"alwaysintreble",
"Alchav"
]
}
]
}
]
},
{
"gameTitle": "The Legend of Zelda: A Link to the Past",
"tutorials": [
@@ -86,6 +178,33 @@
}
]
},
{
"gameTitle": "The Legend of Zelda: Ocarina of Time",
"tutorials": [
{
"name": "Multiworld Setup Tutorial",
"description": "A guide to setting up the Archipelago Ocarina of Time software on your computer.",
"files": [
{
"language": "English",
"filename": "zelda5/setup_en.md",
"link": "zelda5/setup/en",
"authors": [
"Edos"
]
},
{
"language": "Spanish",
"filename": "zelda5/setup_es.md",
"link": "zelda5/setup/es",
"authors": [
"Edos"
]
}
]
}
]
},
{
"gameTitle": "Factorio",
"tutorials": [
@@ -98,7 +217,8 @@
"filename": "factorio/setup_en.md",
"link": "factorio/setup/en",
"authors": [
"Berserker"
"Berserker",
"Farrak Kilhn"
]
}
]
@@ -139,5 +259,225 @@
]
}
]
},
{
"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"
]
}
]
}
]
},
{
"gameTitle": "Raft",
"tutorials": [
{
"name": "Multiworld Setup Guide",
"description": "A guide to setting up Raft integration for Archipelago multiworld games.",
"files": [
{
"language": "English",
"filename": "raft/setup_en.md",
"link": "raft/setup/en",
"authors": [
"SunnyBat",
"Awareqwx"
]
}
]
}
]
},
{
"gameTitle": "Timespinner",
"tutorials": [
{
"name": "Multiworld Setup Guide",
"description": "A guide to setting up the Timespinner randomizer connected to an Archipelago Multiworld",
"files": [
{
"language": "English",
"filename": "timespinner/setup_en.md",
"link": "timespinner/setup/en",
"authors": [
"Jarno"
]
},
{
"language": "German",
"filename": "timespinner/setup_de.md",
"link": "timespinner/setup/de",
"authors": [
"Grrmo",
"Fynxes",
"Blaze0168"
]
}
]
}
]
},
{
"gameTitle": "Subnautica",
"tutorials": [
{
"name": "Multiworld Setup Guide",
"description": "A guide to setting up the Subnautica randomizer connected to an Archipelago Multiworld",
"files": [
{
"language": "English",
"filename": "Subnautica/setup_en.md",
"link": "Subnautica/setup/en",
"authors": [
"Berserker"
]
}
]
}
]
},
{
"gameTitle": "Super Metroid",
"tutorials": [
{
"name": "Multiworld Setup Guide",
"description": "A guide to setting up the Super Metroid Client on your computer. This guide covers single-player, multiworld, and related software.",
"files": [
{
"language": "English",
"filename": "super-metroid/multiworld_en.md",
"link": "super-metroid/multiworld/en",
"authors": [
"Farrak Kilhn"
]
}
]
}
]
},
{
"gameTitle": "Secret of Evermore",
"tutorials": [
{
"name": "Multiworld Setup Guide",
"description": "A guide to playing Secret of Evermore randomizer. This guide covers single-player, multiworld and related software.",
"files": [
{
"language": "English",
"filename": "secret-of-evermore/multiworld_en.md",
"link": "secret-of-evermore/multiworld/en",
"authors": [
"Black Sliver"
]
}
]
}
]
},
{
"gameTitle": "Final Fantasy",
"tutorials": [
{
"name": "Multiworld Setup Guide",
"description": "A guide to playing Final Fantasy multiworld. This guide only covers playing multiworld.",
"files": [
{
"language": "English",
"filename": "ff1/multiworld_en.md",
"link": "ff1/multiworld/en",
"authors": [
"jat2980"
]
}
]
}
]
},
{
"gameTitle": "Rogue Legacy",
"tutorials": [
{
"name": "Multiworld Setup Guide",
"description": "A guide to setting up the Rogue Legacy Randomizer software on your computer. This guide covers single-player, multiworld, and related software.",
"files": [
{
"language": "English",
"filename": "rogue-legacy/rogue-legacy_en.md",
"link": "rogue-legacy/rogue-legacy/en",
"authors": [
"Phar"
]
}
]
}
]
},
{
"gameTitle": "Slay the Spire",
"tutorials": [
{
"name": "Multiworld Setup Guide",
"description": "A guide to setting up Slay the Spire for Archipelago. This guide covers single-player, multiworld, and related software.",
"files": [
{
"language": "English",
"filename": "slay-the-spire/slay-the-spire_en.md",
"link": "slay-the-spire/slay-the-spire/en",
"authors": [
"Phar"
]
}
]
}
]
},
{
"gameTitle": "Super Mario 64 EX",
"tutorials": [
{
"name": "Multiworld Setup Guide",
"description": "A guide to setting up SM64EX for MultiWorld.",
"files": [
{
"language": "English",
"filename": "sm64ex/setup_en.md",
"link": "sm64ex/setup/en",
"authors": [
"N00byKing"
]
}
]
}
]
},
{
"gameTitle": "VVVVVV",
"tutorials": [
{
"name": "Multiworld Setup Guide",
"description": "A guide to setting up VVVVVV for MultiWorld.",
"files": [
{
"language": "English",
"filename": "v6/setup_en.md",
"link": "v6/setup/en",
"authors": [
"N00byKing"
]
}
]
}
]
}
]

View File

@@ -0,0 +1,37 @@
# VVVVVV MultiWorld Setup Guide
## Required Software
- VVVVVV (Bought from the [Steam Store](https://store.steampowered.com/app/70300/VVVVVV/) or [GOG Store](https://www.gog.com/game/vvvvvv) Page, NOT Make and Play Edition!)
- [V6AP](https://github.com/N00byKing/VVVVVV/releases)
## Installation and Game Start Procedures
1. Install VVVVVV through either Steam or GOG
2. Go to the page linked for V6AP, and download the latest release
3. Unpack the zip file where you have VVVVVV installed.
# Joining a MultiWorld Game
To join, set the following launch options: `-v6ap_name YourName -v6ap_ip ServerIP:Port`.
Optionally, add `-v6ap_passwd "YourPassword"` if the room you are using requires a password. All parameters without quotation marks.
The Name in this case is the one specified in your generated .yaml file.
In case you are using the Archipelago Website, the IP should be `archipelago.gg`.
If everything worked out, you will see a textbox informing you the connection has been established after the story intro.
## Installation Troubleshooting
Start the game from the command line to view helpful messages regarding V6AP. These will look something like "V6AP: Message"
### Game no longer starts after copying the .exe
Most likely you forgot to set the launch options. `-v6ap_name YourName` and `-v6ap_ip ServerIP:Port` are required for startup.
If your Name or Password have spaces in them, surround them in quotes.
## Game Troubleshooting
### What happens if I lose connection?
V6AP tries to reconnect a few times, so be patient.
Should the problem still be there after about a minute or two, just save and restart the game.

View File

@@ -1,25 +1,28 @@
# MSU-1 Setup Guide
## What is MSU-1?
MSU-1 allows for the use of custom in-game music. It works on original hardware, the SuperNT, and certain emulators.
This guide will explain how to find custom music packages, often called MSU packs, and how to configure
them for use with original hardware, the SuperNT, and the snes9x emulator.
This guide will explain how to find custom music packages, often called MSU packs, and how to configure them for use
with original hardware, the SuperNT, and the snes9x emulator.
## Where to find MSU Packs
MSU packs are constantly in development. You can find a list of completed packs, as well as in-development packs on
[this Google Spreadsheet](https://docs.google.com/spreadsheets/d/1XRkR4Xy6S24UzYkYBAOv-VYWPKZIoUKgX04RbjF128Q).
MSU packs are constantly in development. We won't link to any packs as most include ripped music from other media.
## What an MSU pack should look like
MSU packs contain many files, most of which are the music files which will be used when playing the game. These files
should be named similarly, with a hyphenated number at the end, and with a `.pcm` extension. It does not matter what
each music file is named, so long as they all follow the same pattern. The most popular filename you will find is
`alttp_msu-X.pcm`, where X is replaced by a number.
each music file is named, so long as they all follow the same pattern. The most popular filename you will find
is `alttp_msu-X.pcm`, where X is replaced by a number.
There is one other type of file you should find inside an MSU pack's folder. This file indicates to the hardware or
to the emulator that MSU should be enabled for this game. This file should be named similarly to the other files in
the folder, but will have a `.msu` extension and be 0 KB in size.
There is one other type of file you should find inside an MSU pack's folder. This file indicates to the hardware or to
the emulator that MSU should be enabled for this game. This file should be named similarly to the other files in the
folder, but will have a `.msu` extension and be 0 KB in size.
A short example of the contents of an MSU pack folder are as follows:
```
List of files inside an MSU pack folder:
alttp_msu.msu
@@ -30,10 +33,12 @@ alttp_msu-34.pcm
```
## How to use an MSU Pack
In all cases, you must rename your ROM file to match the pattern of names inside your MSU pack's folder, then place
your ROM file inside that folder.
In all cases, you must rename your ROM file to match the pattern of names inside your MSU pack's folder, then place your
ROM file inside that folder.
This will cause the folder contents to look like the following:
```
List of files inside an MSU pack folder:
alttp_msu.msu
@@ -45,13 +50,16 @@ alttp_msu-34.pcm
```
### With snes9x
1. Load the ROM file from snes9x.
### With SD2SNES / FXPak on original hardware
1. Load the MSU pack folder onto your SD2SNES / FXPak.
2. Navigate into the MSU pack folder and load your ROM.
### With SD2SNES / FXPak on SuperNT
1. Load the MSU pack folder onto your SD2SNES / FXPak.
2. Power on your SuperNT and navigate to the `Settings` menu.
3. Enter the `Audio` settings.
@@ -63,14 +71,8 @@ alttp_msu-34.pcm
9. Navigate into your MSU pack folder and load your ROM.
## A word of caution to streamers
Many MSU packs use copyrighted music which is not permitted for use on platforms like Twitch and YouTube.
If you choose to stream music from an MSU pack, please ensure you have permission to do so. If you stream
music which has not been licensed to you, or licensed for use in a stream in general, your VOD may be muted.
In the worst case, you may receive a DMCA take-down notice. Please be careful to only stream music for which
you have the rights to do so.
##### Stream-safe MSU packs
Below is a list of MSU packs which, so far as we know, are safe to stream. More will be added to this list as
we learn of them. If you know of any we missed, please let us know!
- Vanilla Game Music
- [Smooth McGroove](https://drive.google.com/open?id=1JDa1jCKg5hG0Km6xNpmIgf4kDMOxVp3n)
Many MSU packs use copyrighted music which is not permitted for use on platforms like Twitch and YouTube. If you choose
to stream music from an MSU pack, please ensure you have permission to do so. If you stream music which has not been
licensed to you, or licensed for use in a stream in general, your VOD may be muted. In the worst case, you may receive a
DMCA take-down notice. Please be careful to only stream music for which you have the rights to do so.

View File

@@ -1,24 +1,31 @@
# MSU-1 Guía de instalación
## Que es MSU-1?
MSU-1 permite el uso de música personalizada durante el juego. Funciona en hardware original, la SuperNT, y algunos emuladores.
Esta guiá explicará como encontrar los packs de música personalizada, comúnmente llamados pack MSU, y como configurarlos
para su uso en hardware original, la SuperNT, and el emulador snes9x.
MSU-1 permite el uso de música personalizada durante el juego. Funciona en hardware original, la SuperNT, y algunos
emuladores. Esta guiá explicará como encontrar los packs de música personalizada, comúnmente llamados pack MSU, y como
configurarlos para su uso en hardware original, la SuperNT, and el emulador snes9x.
## Donde encontrar packs MSU
Los packs MSU están constantemente en desarrollo. Puedes encontrar una lista de pack completos, al igual que packs en desarrollo en
Los packs MSU están constantemente en desarrollo. Puedes encontrar una lista de pack completos, al igual que packs en
desarrollo en
[esta hoja de calculo Google](https://docs.google.com/spreadsheets/d/1XRkR4Xy6S24UzYkYBAOv-VYWPKZIoUKgX04RbjF128Q).
## Que pinta debe tener un pack MSU
Los packs MSU contienen muchos ficheros, la mayoria de los cuales son los archivos de música que se usaran durante el juego. Estos ficheros
deben tener un nombre similar, con un guión seguido por un número al final, y tienen extensión`.pcm`. No importa como se llame
cada archivo de música, siempre y cuando todos sigan el mismo patrón. El nombre más popular es
Los packs MSU contienen muchos ficheros, la mayoria de los cuales son los archivos de música que se usaran durante el
juego. Estos ficheros deben tener un nombre similar, con un guión seguido por un número al final, y tienen
extensión`.pcm`. No importa como se llame cada archivo de música, siempre y cuando todos sigan el mismo patrón. El
nombre más popular es
`alttp_msu-X.pcm`, donde X es un número.
Hay otro tipo de fichero que deberias encontrar en el directorio de un pack MSU. Este archivo indica al hardware o
emulador que MSU debe ser activado para este juego. El fichero tiene un nombre similar al resto, pero tiene como extensión `.msu` y su tamaño es 0 KB.
Hay otro tipo de fichero que deberias encontrar en el directorio de un pack MSU. Este archivo indica al hardware o
emulador que MSU debe ser activado para este juego. El fichero tiene un nombre similar al resto, pero tiene como
extensión `.msu` y su tamaño es 0 KB.
Un pequeño ejemplo de los contenidos de un directorio que contiene un pack MSU:
```
Lista de ficheros dentro de un directorio de pack MSU:
alttp_msu.msu
@@ -29,10 +36,12 @@ alttp_msu-34.pcm
```
## Como usar un pack MSU
En todos los casos, debes renombrar tu fichero de ROM para que coincida con el resto de nombres de fichero del directorio, y copiar/pegar tu fichero rom
dentro de dicho directorio.
En todos los casos, debes renombrar tu fichero de ROM para que coincida con el resto de nombres de fichero del
directorio, y copiar/pegar tu fichero rom dentro de dicho directorio.
Esto hara que los contenidos del directorio sean los siguientes:
```
Lista de ficheros dentro del directorio de pack MSU:
alttp_msu.msu
@@ -44,13 +53,16 @@ alttp_msu-34.pcm
```
### Con snes9x
1. Carga el fichero de rom en snes9x.
### Con SD2SNES / FXPak en hardware original
1. Carga tu directorio de pack MSU en tu SD2SNES / FXPak.
2. Navega hasta el directorio de pack MSU y carga la ROM
### Con SD2SNES / FXPak en SuperNT
1. Carga tu directorio de pack MSU en tu SD2SNES / FXPak.
2. Enciende tu SuperNT y navega al menú `Settings`.
3. Entra en la opcion `Audio`.
@@ -62,13 +74,17 @@ alttp_msu-34.pcm
9. Navega hasta el directorio de pack MSU y carga la ROM
## Aviso a streamers
Muchos packs MSU usan música con derechos de autor la cual no esta permitido su uso en plataformas como Twitch o YouTube.
Si elijes hacer stream de dicha música, tu VOD puede ser silenciado. En el peor caso, puedes recibir una orden de eliminación DMCA.
Por favor, tened cuidado y solo streamear música para la cual tengas los derechos para hacerlo.
Muchos packs MSU usan música con derechos de autor la cual no esta permitido su uso en plataformas como Twitch o
YouTube. Si elijes hacer stream de dicha música, tu VOD puede ser silenciado. En el peor caso, puedes recibir una orden
de eliminación DMCA. Por favor, tened cuidado y solo streamear música para la cual tengas los derechos para hacerlo.
##### Packs MSU seguros para Stream
A continuación enumeramos los packs MSU que, packs which, por lo que sabemos, son seguros para vuestras retransmisiones. Se iran añadiendo mas conforme
vayamos enterandonos. Si sabes alguno que podamos haber olvidado, por favor haznoslo saber!
A continuación enumeramos los packs MSU que, packs which, por lo que sabemos, son seguros para vuestras retransmisiones.
Se iran añadiendo mas conforme vayamos enterandonos. Si sabes alguno que podamos haber olvidado, por favor haznoslo
saber!
- Musica del juego original
- [Smooth McGroove](https://drive.google.com/open?id=1JDa1jCKg5hG0Km6xNpmIgf4kDMOxVp3n)

View File

@@ -1,25 +1,31 @@
# Guide d'installation de MSU-1
## Qu'est-ce que MSU-1 ?
MSU-1 permet l'utilisation de musiques en jeu personnalisées. Cela fonctionne sur une console originale, sur SuperNT, et sur certains émulateurs.
Ce guide explique comment trouver des packs de musiques personnalisées, couremment appelées packs MSU, et comment les configurer
pour les utiliser sur console, sur SuperNT et sur l'émulateur snes9x.
MSU-1 permet l'utilisation de musiques en jeu personnalisées. Cela fonctionne sur une console originale, sur SuperNT, et
sur certains émulateurs. Ce guide explique comment trouver des packs de musiques personnalisées, couremment appelées
packs MSU, et comment les configurer pour les utiliser sur console, sur SuperNT et sur l'émulateur snes9x.
## Où trouver des packs MSU
Les packs MSU sont constamment en développement. Vous pouvez trouver une liste de packs complétés, ainsi que des packs en développement sur
Les packs MSU sont constamment en développement. Vous pouvez trouver une liste de packs complétés, ainsi que des packs
en développement sur
[cette feuille de calcul Google](https://docs.google.com/spreadsheets/d/1XRkR4Xy6S24UzYkYBAOv-VYWPKZIoUKgX04RbjF128Q).
## A quoi ressemble un pack MSU
Les packs MSU contiennent beaucoup de fichiers, la plupart étant des fichiers musicaux qui seront utilisés en cours de jeu. Ces fichiers
doivent être nommés de façon similaire, avec un nombre derrière le tiret, puis l'extension `.pcm`. Le nom de chaque fichier
n'importe pas, du moment qu'ils suivent tous le même motif. Le nom le plus populaire que vous verrez est
Les packs MSU contiennent beaucoup de fichiers, la plupart étant des fichiers musicaux qui seront utilisés en cours de
jeu. Ces fichiers doivent être nommés de façon similaire, avec un nombre derrière le tiret, puis l'extension `.pcm`. Le
nom de chaque fichier n'importe pas, du moment qu'ils suivent tous le même motif. Le nom le plus populaire que vous
verrez est
`alttp_msu-X.pcm`, où X est remplacé par un nombre.
Il existe un autre type de fichier que vous devriez trouver dans le dossier d'un pack MSU. Ce fichier indique au matériel
ou à l'émulateur que MSU doit être activé pour ce jeu. Ce fichier doit être nommé de façon similaires aux autres dans
le dossier, mais il aura une extension `.msu` et pèsera 0 KB.
Il existe un autre type de fichier que vous devriez trouver dans le dossier d'un pack MSU. Ce fichier indique au
matériel ou à l'émulateur que MSU doit être activé pour ce jeu. Ce fichier doit être nommé de façon similaires aux
autres dans le dossier, mais il aura une extension `.msu` et pèsera 0 KB.
Voici un exemple de ce à quoi ressemble le dossier d'un pack MSU :
```
Liste des fichiers dans le dossier d'un pack MSU :
alttp_msu.msu
@@ -30,10 +36,12 @@ alttp_msu-34.pcm
```
## Comment utiliser un pack MSU
Dans tous les cas, vosu devez renommer votre fichier ROM pour qu'il corresponde au même motif que les autres fichiers dans le dossier du pack MSU,
ensuite vous placez votre fichier ROM dans ce dossier.
Dans tous les cas, vosu devez renommer votre fichier ROM pour qu'il corresponde au même motif que les autres fichiers
dans le dossier du pack MSU, ensuite vous placez votre fichier ROM dans ce dossier.
Le contenu du dossier ressemblera alors à ceci :
```
Liste des fichiers dans le dossier d'un pack MSU :
alttp_msu.msu
@@ -45,13 +53,16 @@ alttp_msu-34.pcm
```
### Avec snes9x
1. Chargez le fichier ROM depuis snes9x.
### Avec un SD2SNES / FXPak sur une console originale
1. Mettez le dossier du pack MSU avec la ROM sur votre SD2SNES / FXPak.
2. Naviguez vers ce dossier et chargez votre ROM.
### Avec un SD2SNES / FXPak sur SuperNT
1. Mettez le dossier du pack MSU avec la ROM sur votre SD2SNES / FXPak.
2. Allumez votre SuperNT et naviguez vers le menu `Settings` (paramètres).
3. Entrez dans les paramètres `Audio`.
@@ -63,6 +74,8 @@ alttp_msu-34.pcm
9. Naviguez vers le dossier du pack MSU et chargez votre ROM.
## Avertissement pour les streamers
Beaucoup de packs MSU utilisent des musiques copyrightées ce qui n'est pas permis sur des plateformes comme Twitch et YouTube.
Si vous choisissez de streamer des musiques copyrightées, votre VOD sera peut-être rendue muette. Dans le pire des cas, vous pourriez recevoir
une plainte DMCA pour faire retirer la vidéo. Faites attention à streamer uniquement des musiques pour lesquelles vous avez le droit.
Beaucoup de packs MSU utilisent des musiques copyrightées ce qui n'est pas permis sur des plateformes comme Twitch et
YouTube. Si vous choisissez de streamer des musiques copyrightées, votre VOD sera peut-être rendue muette. Dans le pire
des cas, vous pourriez recevoir une plainte DMCA pour faire retirer la vidéo. Faites attention à streamer uniquement des
musiques pour lesquelles vous avez le droit.

View File

@@ -1,10 +1,11 @@
# A Link to the Past Randomizer Setup Guide
## Benötigte Software
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Included in the above Utilities)
- Hardware oder Software zum Laden und Abspielen von SNES Rom-Dateien
- Ein Emulator, der lua-scripts abspielen kann
- [SNI](https://github.com/alttpo/sni/releases) (Integriert in Archipelago)
- Hardware oder Software zum Laden und Abspielen von SNES Rom-Dateien fähig zu einer Internetverbindung
- Ein Emulator, der mit SNI verbinden kann
([snes9x Multitroid](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz),
[BizHawk](http://tasvideos.org/BizHawk.html))
- Ein SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), oder andere kompatible Hardware
@@ -13,44 +14,49 @@
## Installation Schritt für Schritt
### Windows
1. Lade die Multiworld Utilities herunter und führe die Installation aus. Sei sicher, dass du immer die
aktuellste Version installiert hast.**Die Datei befindet sich im "assets"-Kasten unter der jeweiligen Versionsinfo!**.
Für normale Multiworld-Spiele lädst du die `Setup.Archipelago.exe` herunter.
- Für den Doorrandomizer muss die alternative doors-Variante geladen werden.
- Während der Installation fragt dich das Programm nach der japanischen 1.0 ROM-Datei. Wenn du die Software
bereits installiert hast und einfach nur updaten willst, wirst du nicht nochmal danach gefragt.
- Es kann auch sein,dass der Installer Microsoft Visual C++ installieren möchte.
Wenn du das bereits installiert hast (durch Steam oder andere Programme), wirst du nicht nochmal danach gefragt.
1. Lade die Multiworld Utilities herunter und führe die Installation aus. Sei sicher, dass du immer die aktuellste
Version installiert hast.**Die Datei befindet sich im "assets"-Kasten unter der jeweiligen Versionsinfo!**. Für
normale Multiworld-Spiele lädst du die `Setup.Archipelago.exe` herunter.
- Für den Doorrandomizer muss die alternative doors-Variante geladen werden.
- Während der Installation fragt dich das Programm nach der japanischen 1.0 ROM-Datei. Wenn du die Software bereits
installiert hast und einfach nur updaten willst, wirst du nicht nochmal danach gefragt.
- Es kann auch sein,dass der Installer Microsoft Visual C++ installieren möchte. Wenn du das bereits installiert
hast (durch Steam oder andere Programme), wirst du nicht nochmal danach gefragt.
2. Wenn du einen Emulator benutzt, so ist es sinnvoll, ihn als Standard zum Abspielen für .sfc-dateien einzustellen.
1. Entpacke oder Installiere deinen Emulator(-Ordner) an einen Ort, den du auch wiederfindest
2. Rechtsklicke auf eine .sfc-Datei und wähle **Öffnen mit...**
3. Mache einen Haken in die Box bei **Immer diese App zum Öffnen von .sfc Dateien benutzen **.
4. Scrolle zum Ende und wähle **Weitere Apps** und nochmal am Ende **Andere App auf diesem PC suchen** auswählen.
5. Suche nach der .exe-Datei des Emulators deiner Wahl und wähle **Öffnen**.
Diese Datei befindet sich dort, wo den Emulator in Schritt 1 enpackt/installiert hast.
5. Suche nach der .exe-Datei des Emulators deiner Wahl und wähle **Öffnen**. Diese Datei befindet sich dort, wo den
Emulator in Schritt 1 enpackt/installiert hast.
### Macintosh
### Macintosh
- Es werden freiwillige Helfer gesucht! Meldet euch doch bei **Farrak Kilhn** auf Discord, wenn ihr helfen wollt!
## Erstellen deiner YAML-Datei
### Was ist eine YAML-Datei und wofür brauche ich die?
Deine persönliche YAML-Datei beinhaltet eine Reihe von Einstellungen, die der Zufallsgenerator zum Erstellen
von deinem Spiel benötigt. Jeder Spieler einer Multiworld stellt seine eigene YAML-Datei zur Verfügung. Dadurch kann
jeder Spieler sein Spiel nach seinem eigenen Geschmack gestalten, während andere Spieler unabhängig davon ihre eigenen
Einstellungen wählen können!
Deine persönliche YAML-Datei beinhaltet eine Reihe von Einstellungen, die der Zufallsgenerator zum Erstellen von deinem
Spiel benötigt. Jeder Spieler einer Multiworld stellt seine eigene YAML-Datei zur Verfügung. Dadurch kann jeder Spieler
sein Spiel nach seinem eigenen Geschmack gestalten, während andere Spieler unabhängig davon ihre eigenen Einstellungen
wählen können!
### Wo bekomme ich so eine YAML-Datei her?
Die [Player Settings](/player-settings) Seite auf der Website ermöglicht das einfache Erstellen und Herunterladen
deiner eigenen `yaml` Datei. Drei verschiedene Voreinstellungen können dort gespeichert werden.
Die [Player Settings](/games/A Link to the Past/player-settings) Seite auf der Website ermöglicht das einfache Erstellen
und Herunterladen deiner eigenen `yaml` Datei. Drei verschiedene Voreinstellungen können dort gespeichert werden.
### Deine YAML-Datei ist gewichtet!
Die **Player Settings** Seite hat eine Menge Optionen, die man per Schieber einstellen kann. Das ermöglicht es,
verschiedene Optionen mit unterschiedlichen Wahrscheinlichkeiten in einer Kategorie ausgewürfelt zu werden
Als Beispiel kann man sich die Option "Map Shuffle" als einen Eimer mit Zetteln zur Abstimmung Vorstellen.
So kann man beispielsweise für die Option "On" 20 Zettel mit dieser Option einwerfen und 40 Zettel mit "Off".
Die **Player Settings** Seite hat eine Menge Optionen, die man per Schieber einstellen kann. Das ermöglicht es,
verschiedene Optionen mit unterschiedlichen Wahrscheinlichkeiten in einer Kategorie ausgewürfelt zu werden
Als Beispiel kann man sich die Option "Map Shuffle" als einen Eimer mit Zetteln zur Abstimmung Vorstellen. So kann man
beispielsweise für die Option "On" 20 Zettel mit dieser Option einwerfen und 40 Zettel mit "Off".
Entsprechend in diesem Beispiel liegen dann 60 Zettel im Eimer. 20 für "On" und 40 für "Off". Um die Option
festzulegen, "greift" der Generator in den Eimer und holt sich zufällig einen Zettel heraus. Entsprechend ist die
@@ -60,95 +66,99 @@ Wenn du eine Option nicht gewählt haben möchtest, setze ihren Wert einfach auf
(Es muss aber mindestens eine Option pro Kategorie einen Wert größer Null besitzen, sonst funktioniert die yaml nicht!)
### Überprüfung deiner YAML-Datei
Wenn man sichergehen will, ob die YAML-Datei funktioniert, kann man dies
bei der [YAML Validator](/mysterycheck) Seite tun.
Wenn man sichergehen will, ob die YAML-Datei funktioniert, kann man dies bei der [YAML Validator](/mysterycheck) Seite
tun.
## ein Einzelspielerspiel erstellen
1. Navigiere zur [Generator Seite](/generate) und lade dort deine YAML-Datei hoch.
2. Dir wird eine "Seed Info"-Seite angezeigt, wo du deine Patch-Datei herunterladen kannst.
3. Doppelklicke die Patchdatei und der Emulator sollte nach kurzer Verzögerung mit dem gepatchten Rom starten.
Der Client ist soweit unnötig für Einzelspielerspiele, also kannst diesen und das WebUI einfach schließen.
2. Dir wird eine "Seed Info"-Seite angezeigt, wo du deine Patch-Datei herunterladen kannst.
3. Doppelklicke die Patchdatei und der Emulator sollte nach kurzer Verzögerung mit dem gepatchten Rom starten. Der
Client ist soweit unnötig für Einzelspielerspiele, also kannst diesen und das WebUI einfach schließen.
## Einem MultiWorld-Spiel beitreten
### Erhalte deine Patch-Datei und erstelle dein ROM
Wenn du an einem MultiWorld-Spiel teilnehmen möchtest, wirst du in der Regel vom Host nach deiner YAML-Datei gefragt.
Sobald du diese weitergegeben hast, wird der Host einen Link bereitstellen, wo du deinen Patch oder eine .zip-Datei
mit allen Patches herunterladen kannst. Die Patch-Datei hat immer die Endung `.bmbp`.
Sobald du diese weitergegeben hast, wird der Host einen Link bereitstellen, wo du deinen Patch oder eine .zip-Datei mit
allen Patches herunterladen kannst. Die Patch-Datei hat immer die Endung `.apbp`.
### Mit dem Client verbinden
#### Via Emulator
Wenn der client den Emulator automatisch gestartet hat, wird QUsb2Snes ebenfalls im Hintergrund gestartet.
Wenn dies das erste Mal ist, wird möglicherweise ein Fenster angezeigt, wo man bestätigen muss, dass das Programm
durch die Windows Firewall kommunizieren darf.
Wenn der client den Emulator automatisch gestartet hat, wird SNI ebenfalls im Hintergrund gestartet. Wenn dies das erste
Mal ist, wird möglicherweise ein Fenster angezeigt, wo man bestätigen muss, dass das Programm durch die Windows Firewall
kommunizieren darf.
##### snes9x Multitroid
1. Lade die Entsprechende ROM-Datei, wenn sie nicht schon automatisch geladen wurde.
2. Klicke auf den Reiter "File" oben im Menü und wähle **Lua Scripting**
3. Klicke auf **New Lua Script Window...**
4. Im sich neu öffnenden Fenster, klicke auf **Browse...**
5. Navigiere zum Ort, wo du snes9x Multitroid installiert hast, öffne den `lua`-Ordner und öffne `multibridge.lua`
6. Schaue im Lua-Fenster nach einem Namen, der dir zugeteilt wird und schaue im Client (WebUI im Browser), ob dort
5. Navigiere zum Verzeichnis, wo du Archipelago installiert hast und dort in den Unterordner `SNI`.
6. Wähle dort die `Connector.lua` und klicke auf Öffnen.
7. Schaue im Lua-Fenster nach einem Namen, der dir zugeteilt wird und schaue im Client (WebUI im Browser), ob dort
"Snes Device: Connected" mit demselben Namen dort steht (in der oberen linken Ecke).
##### BizHawk
1. Stelle sicher, dass der BSNES-Core in Bizhawk geladen wird. Dazu musst du auf das Tools-Menü in Bizhawk klicken
und folgende Optionen wählen:
1. Stelle sicher, dass der BSNES-Core in Bizhawk geladen wird. Dazu musst du auf das Tools-Menü in Bizhawk klicken und
folgende Optionen wählen:
`Config --> Cores --> SNES --> BSNES`
2. Lade die entsprechende ROM-Datei, wenn sie nicht schon automatisch geladen wurde.
3. Klicke auf das Tools-Menü und klicke auf **Lua Console**
4. Klicke auf den Button um ein neues Lua-Script zu öffnen.
5. Navigiere zum Verzeichnis, wo du die Multiworld Utilities installiert hast und dort in folgende Ordner:
`QUsb2Snes/Qusb2Snes/LuaBridge`
6. Wähle dort die `luabridge.lua` und klicke auf Öffnen.
5. Navigiere zum Verzeichnis, wo du Archipelago installiert hast und dort in den Unterordner `SNI`.
6. Wähle dort die `Connector.lua` und klicke auf Öffnen.
7. Schaue im Lua-Fenster nach einem Namen, der dir zugeteilt wird und schaue im Client (WebUI im Browser), ob dort
"Snes Device: Connected" mit demselben Namen dort steht (in der oberen linken Ecke)
#### Mit (Original-)Hardware
Dieser Guide setzt voraus, dass du schon die entsprechende Firmware für dein Gerät heruntergeladen hast! Wenn du
das noch nicht getan hast, so tue dies am besten jetzt! SD2SNES und FXPak Pro Nutzer finden die passende Firmware
Dieser Guide setzt voraus, dass du schon die entsprechende Firmware für dein Gerät heruntergeladen hast! Wenn du das
noch nicht getan hast, so tue dies am besten jetzt! SD2SNES und FXPak Pro Nutzer finden die passende Firmware
[hier](https://github.com/RedGuyyyy/sd2snes/releases). Nutzer ähnlicher Hardware finden Hilfestellung
[auf dieser Seite](http://usb2snes.com/#supported-platforms).
**UM MIT HARDWARE ZU VERBINDEN WIRD AKTUELL EINE ALTE VERSION VON QUSB2SNES BENÖTIGT
([v0.7.16](https://github.com/Skarsnik/QUsb2snes/releases/tag/v0.7.16)).**
Neuere Versionen funktionieren möglicherweise nur eingeschränkt, fehlerhaft oder gar nicht!
1. Schließe deinen Emulator, falls er automatisch gestartet haben sollte.
2. Schließe QUsb2Snes, welches automatisch mit dem Client gestartet wurde (in der Taskleiste zu finden).
3. Starte die richtige version von QUsb2Snes (v0.7.16).
4. Starte deine (Original-)Konsole und lade die ROM-Datei.
5. Schaue auf dein Clientfenster, welches nun "Snes Device: Connected" und den namen deiner Konsole
zeigen sollte.
2. Start SNI
3. Starte deine (Original-)Konsole und lade die ROM-Datei.
4. Schaue auf dein Clientfenster, welches nun "Snes Device: Connected" und den namen deiner Konsole zeigen sollte.
### Mit dem MultiServer verbinden
Die Patch-Datei, welche auch den Client gestartet hat, sollte dich automatisch mit dem MultiServer verbunden haben.
Manchmal ist dies nicht der Fall, auch wenn das Spiel auf der Webseite gehostet wird, aber woanders erstellt wurde.
Wenn die WebUI vom Client "Server Status: Not Connected" zeigt, frag deinen Host nach der passenden Adresse
und trage sie einfach in das Textfeld neben "Server" ein und drücke Enter.
Manchmal ist dies nicht der Fall, auch wenn das Spiel auf der Webseite gehostet wird, aber woanders erstellt wurde. Wenn
die WebUI vom Client "Server Status: Not Connected" zeigt, frag deinen Host nach der passenden Adresse und trage sie
einfach in das Textfeld neben "Server" ein und drücke Enter.
Der Client wird versuchen auf die neue Adresse zu verbinden und nach einer Weile "Server Status: Connected" zeigen.
Sollte nach einer Weile der Client sich nicht verbunden haben, lade die Seite neu.
### Spiele das Spiel!
Wenn der Client anzeigt, dass sowohl das SNES-Gerät (oder Emulator) und der Server verbunden sind,
können du und deine Freunde loslegen! Glückwunsch zum erfolgreichen Beitritt zu einem Multiworld-Spiel ;)
Wenn der Client anzeigt, dass sowohl das SNES-Gerät (oder Emulator) und der Server verbunden sind, können du und deine
Freunde loslegen! Glückwunsch zum erfolgreichen Beitritt zu einem Multiworld-Spiel ;)
## Ein Multiworld-Spiel hosten
Die Empfohlene Art, ein Spiel zu hosten, ist, den Service auf
[der website](https://berserkermulti.world/generate) zu nutzen. Das Ganze ist recht einfach:
[der website](/generate) zu nutzen. Das Ganze ist recht einfach:
1. Lasse dir von deinen Mitspielern die YAML-Datei zuschicken.
2. Erstelle einen Zip-komprimierten Ordner´, in den du alle YAML-Dateien deiner Spieler einfügst.
3. Lade diesen Zip-Ordner auf der oben genannten Website hoch.
4. Warte einen Moment, wenn das Spiel erstellt wird.
5. Wenn das Spiel erstellt wurde, wirst du auf eine "Seed Info"-Seite weitergeleitet.
6. Klicke auf "Create New Room". Du wirst auf die Serverseite gebracht. Gib diesen Link deinen Mitspielern,
sodass sie ihre Patch-Dateien von dort herunterladen können.
**Anmerkung:** Die Patch-Dateien von dieser Seite ermöglichen es den Spielern,
automatisch auf den Server zu verbinden. Die Patch-Dateien von der "Seed Info"-Seite tun dies nicht!
7. Oben auf der Serverseite ist ein Link zum MultiWorld-Tracker zum aktuellen Spiel zu finden. Gib diesen Link
ebenfalls deinen Mitspielern, so dass ihr alle den Fortschritt eures Spiels verfolgen könnt! Ihr könnt ihn
auch an Zuschauer weitergeben, so dass sie auf dem Laufenden bleiben.
6. Klicke auf "Create New Room". Du wirst auf die Serverseite gebracht. Gib diesen Link deinen Mitspielern, sodass sie
ihre Patch-Dateien von dort herunterladen können.
**Anmerkung:** Die Patch-Dateien von dieser Seite ermöglichen es den Spielern, automatisch auf den Server zu
verbinden. Die Patch-Dateien von der "Seed Info"-Seite tun dies nicht!
7. Oben auf der Serverseite ist ein Link zum MultiWorld-Tracker zum aktuellen Spiel zu finden. Gib diesen Link ebenfalls
deinen Mitspielern, so dass ihr alle den Fortschritt eures Spiels verfolgen könnt! Ihr könnt ihn auch an Zuschauer
weitergeben, so dass sie auf dem Laufenden bleiben.
8. Wenn alle Spieler verbunden sind, könnt ihr mit dem Spiel loslegen! Viel Spaß!

View File

@@ -1,180 +1,143 @@
# A Link to the Past Randomizer Setup Guide
<div id="tutorial-video-container">
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/mJKEHaiyR_Y" frameborder="0"
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen>
</iframe>
</div>
## Required Software
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Included in the above Utilities)
- One of the client programs:
- [SNIClient](https://github.com/ArchipelagoMW/Archipelago/releases), included with the main
Archipelago install. Make sure to check the box for `SNI Client - A Link to the Past Patch Setup`
- [SuperNintendoClient](https://github.com/ArchipelagoMW/SuperNintendoClient/releases), an alternate standalone
client for Super Nintendo games
- Hardware or software capable of loading and playing SNES ROM files
- An emulator capable of running Lua scripts
([snes9x Multitroid](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz),
- An emulator capable of connecting to SNI
([snes9x rr](https://github.com/gocha/snes9x-rr/releases),
[BizHawk](http://tasvideos.org/BizHawk.html))
- An SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), or other compatible hardware
- Your Japanese v1.0 ROM file, probably named `Zelda no Densetsu - Kamigami no Triforce (Japan).sfc`
## Installation Procedures
### Windows Setup
1. Download and install the MultiWorld Utilities from the link above, making sure to install the most recent version.
**The file is located in the assets section at the bottom of the version information**. If you intend to play normal
multiworld games, you want `Setup.Archipelago.exe`
- If you intend to play the doors variant of multiworld, you will want to download the alternate doors file.
- During the installation process, you will be asked to browse for your Japanese 1.0 ROM file. If you have
installed this software before and are simply upgrading now, you will not be prompted to locate your
ROM file a second time.
- You may also be prompted to install Microsoft Visual C++. If you already have this software on your computer
(possibly because a Steam game installed it already), the installer will not prompt you to install it again.
1. Download and install your preferred client from the link above, making sure to install the most recent version.
**The installer file is located in the assets section at the bottom of the version information**.
- During setup, you will be asked to locate your base ROM file. This is your Japanese Link to the Past ROM file.
2. If you are using an emulator, you should assign your Lua capable emulator as your default program
for launching ROM files.
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
2. Right click on a ROM file and select **Open with...**
2. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
files.
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
2. Right-click on a ROM file and select **Open with...**
3. Check the box next to **Always use this app to open .sfc files**
4. Scroll to the bottom of the list and click the grey text **Look for another App on this PC**
5. Browse for your emulator's `.exe` file and click **Open**. This file should be located inside
the folder you extracted in step one.
5. Browse for your emulator's `.exe` file and click **Open**. This file should be located inside the folder you
extracted in step one.
### Macintosh Setup
- We need volunteers to help fill this section! Please contact **Farrak Kilhn** on Discord if you want to help.
## Create a Config (.yaml) File
## Configuring your YAML file
### What is a config file and why do I need one?
### What is a YAML file and why do I need one?
Your YAML file contains a set of configuration options which provide the generator with information about how
it should generate your game. Each player of a multiworld will provide their own YAML file. This setup allows
each player to enjoy an experience customized for their taste, and different players in the same multiworld
can all have different options.
Your config file contains a set of configuration options which provide the generator with information about how it
should generate your game. Each player of a multiworld will provide their own config file. This setup allows each player
to enjoy an experience customized for their taste, and different players in the same multiworld can all have different
options.
### Where do I get a YAML file?
The [Generate Game](/player-settings) page on the website allows you to configure your personal settings and
export a YAML file from them.
### Where do I get a config file?
### 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.
The [Player Settings](/games/A%20Link%20to%20the%20Past/player-settings) page on the website allows you to configure
your personal settings and export a config file from them.
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.
### Verifying your config file
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
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 config file to make sure it works, you may do so on the
[YAML Validator](/mysterycheck) page.
## Generating a Single-Player Game
1. Navigate to the [Generate Game](/player-settings), configure your options, and click the "Generate Game" button.
2. You will be presented with a "Seed Info" page, where you can download your patch file.
3. Double-click on your patch file, and the emulator should launch with your game automatically. As the
Client is unnecessary for single player games, you may close it and the WebUI.
1. Navigate to the [Player Settings](/games/A%20Link%20to%20the%20Past/player-settings) page, configure your options,
and click the "Generate Game" button.
2. You will be presented with a "Seed Info" page.
3. Click the "Create New Room" link.
4. You will be presented with a server page, from which you can download your patch file.
5. Double-click on your patch file, and the Z3Client will launch automatically, create your ROM from the patch file, and
open your emulator for you.
6. Since this is a single-player game, you will no longer need the client, so feel free to close it.
## Joining a MultiWorld Game
### Obtain your patch file and create your ROM
When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that
is done, the host will provide you with either a link to download your patch file, or with a zip file containing
everyone's patch files. Your patch file should have a `.bmbp` extension.
Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically
launch the client, and will also create your ROM file in the same place as your patch file.
When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done,
the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch
files. Your patch file should have a `.apbp` extension.
Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically launch the
client, and will also create your ROM in the same place as your patch file.
### Connect to the client
#### With an emulator
When the client launched automatically, QUsb2Snes should have also automatically launched in the background.
If this is its first time launching, you may be prompted to allow it to communicate through the Windows
Firewall.
When the client launched automatically, SNI should have also automatically launched in the background. If this is its
first time launching, you may be prompted to allow it to communicate through the Windows Firewall.
##### snes9x Multitroid
1. Load your ROM file if it hasn't already been loaded.
2. Click on the File menu and hover on **Lua Scripting**
3. Click on **New Lua Script Window...**
4. In the new window, click **Browse...**
5. Browse to the location you extracted snes9x Multitroid to, enter the `lua` folder, and choose `multibridge.lua`
6. Observe a name has been assigned to you, and that the client shows "SNES Device: Connected", with that same
name in the upper left corner.
5. Select the connector lua file included with your client
- SuperNintendoClient users should download `sniConnector.lua` from the client download page
- SNIClient users should look in their Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
emulator is 64-bit or 32-bit.
##### BizHawk
1. Ensure you have the BSNES core loaded. You may do this by clicking on the Tools menu in BizHawk and following
these menu options:
1. Ensure you have the BSNES core loaded. You may do this by clicking on the Tools menu in BizHawk and following these
menu options:
`Config --> Cores --> SNES --> BSNES`
Once you have changed the loaded core, you must restart BizHawk.
2. Load your ROM file if it hasn't already been loaded.
3. Click on the Tools menu and click on **Lua Console**
4. Click the button to open a new Lua script.
5. Browse to your MultiWorld Utilities installation directory, and into the following directories:
`QUsb2Snes/Qusb2Snes/LuaBridge`
6. Select `luabridge.lua` and click Open.
7. Observe a name has been assigned to you, and that the client shows "SNES Device: Connected", with that same
name in the upper left corner.
4. Click Script -> Open Script...
5. Select the `Connector.lua` file you downloaded above
- SuperNintendoClient users should download `sniConnector.lua` from the client download page
- SNIClient users should look in their Archipelago folder for `/SNI/lua/x64` or `/SNI/lua/x86` depending on if the
emulator is 64-bit or 32-bit.
#### With hardware
This guide assumes you have downloaded the correct firmware for your device. If you have not
done so already, please do this now. SD2SNES and FXPak Pro users may download the appropriate firmware
This guide assumes you have downloaded the correct firmware for your device. If you have not done so already, please do
this now. SD2SNES and FXPak Pro users may download the appropriate firmware
[here](https://github.com/RedGuyyyy/sd2snes/releases). Other hardware may find helpful information
[on this page](http://usb2snes.com/#supported-platforms).
1. Close your emulator, which may have auto-launched.
2. Power on your device and load the ROM.
3. Observe the client window now shows "SNES Device: Connected", and lists the name of your device.
### Connect to the MultiServer
The patch file which launched your client should have automatically connected you to the MultiServer.
There are a few reasons this may not happen however, including if the game is hosted on the website but
was generated elsewhere. If the client window shows "Server Status: Not Connected", simply ask the host
for the address of the server, and copy/paste it into the "Server" input field then press enter.
### Connect to the Archipelago Server
The client will attempt to reconnect to the new server address, and should momentarily show "Server
Status: Connected". If the client does not connect after a few moments, you may need to refresh the page.
The patch file which launched your client should have automatically connected you to the AP Server. There are a few
reasons this may not happen however, including if the game is hosted on the website but was generated elsewhere. If the
client window shows "Server Status: Not Connected", simply ask the host for the address of the server, and copy/paste it
into the "Server" input field then press enter.
The client will attempt to reconnect to the new server address, and should momentarily show "Server Status: Connected".
### Play the game
When the client shows both SNES Device and Server as connected, you're ready to begin playing. Congratulations
on successfully joining a multiworld game!
When the client shows both SNES Device and Server as connected, you're ready to begin playing. Congratulations on
successfully joining a multiworld game! You can execute various commands in your client. For more information regarding
these commands you can use `/help` for local client commands and `!help` for server commands.
## Hosting a MultiWorld game
The recommended way to host a game is to use the hosting service provided on
[the website](/generate). The process is relatively simple:
1. Collect YAML files from your players.
2. Create a zip file containing your players' YAML files.
The recommended way to host a game is to use our [hosting service](/generate). The process is relatively simple:
1. Collect config files from your players.
2. Create a zip file containing your players' config files.
3. Upload that zip file to the website linked above.
4. Wait a moment while the seed is generated.
5. When the seed is generated, you will be redirected to a "Seed Info" page.
6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players,
so they may download their patch files from there.
**Note:** The patch files provided on this page will allow players to automatically connect to the server,
while the patch files on the "Seed Info" page will not.
7. Note that a link to a MultiWorld Tracker is at the top of the room page. You should also provide this link
to your players, so they can watch the progress of the game. Any observers may also be given the link to
this page.
6. Click "Create New Room". This will take you to the server page. Provide the link to this page to your players, so
they may download their patch files from there.
7. Note that a link to a MultiWorld Tracker is at the top of the room page. The tracker shows the progress of all
players in the game. Any observers may also be given the link to this page.
8. Once all players have joined, you may begin playing.
## Auto-Tracking
If you would like to use auto-tracking for your game, several pieces of software provide this functionality.
The recommended software for auto-tracking is currently
[OpenTracker](https://github.com/trippsc2/OpenTracker/releases).
### Installation
1. Download the appropriate installation file for your computer (Windows users want the `.msi` file).
2. During the installation process, you may be asked to install the Microsoft Visual Studio Build Tools. A link
to this software is provided during the installation procedure, and it must be installed manually.
### Enable auto-tracking
1. With OpenTracker launched, click the Tracking menu at the top of the window, then choose **AutoTracker...**
2. Click the **Get Devices** button
3. Select your SNES device from the drop-down list
4. If you would like to track small keys and dungeon items, check the box labeled **Race Illegal Tracking**
5. Click the **Start Autotracking** button
6. Close the AutoTracker window, as it is no longer necessary

View File

@@ -7,6 +7,7 @@
</div>
## Software requerido
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Incluido en Multiworld Utilities)
- Hardware o software capaz de cargar y ejecutar archivos de ROM de SNES
@@ -19,85 +20,109 @@
## Procedimiento de instalación
### Instalación en Windows
1. Descarga e instala MultiWorld Utilities desde el enlace anterior, asegurando que instalamos la versión más reciente.
**El archivo esta localizado en la sección "assets" en la parte inferior de la información de versión**. Si tu intención es jugar la versión normal de multiworld, necesitarás el archivo `Setup.Archipelago.exe`
- Si estas interesado en jugar la variante que aleatoriza las puertas internas de las mazmorras, necesitaras bajar 'Setup.BerserkerMultiWorld.Doors.exe'
- Durante el proceso de instalación, se te pedirá donde esta situado tu archivo ROM japonés v1.0. Si ya habías instalado este software con anterioridad y simplemente estas actualizando, no se te pedirá la localización del archivo una segunda vez.
- Puede ser que el programa pida la instalación de Microsoft Visual C++. Si ya lo tienes en tu ordenador (posiblemente por que un juego de Steam ya lo haya instalado), el instalador no te pedirá su instalación.
2. Si estas usando un emulador, deberías asignar la versión capaz de ejecutar scripts Lua como programa por defecto para lanzar ficheros de ROM de SNES.
1. Descarga e instala MultiWorld Utilities desde el enlace anterior, asegurando que instalamos la versión más reciente.
**El archivo esta localizado en la sección "assets" en la parte inferior de la información de versión**. Si tu
intención es jugar la versión normal de multiworld, necesitarás el archivo `Setup.Archipelago.exe`
- Si estas interesado en jugar la variante que aleatoriza las puertas internas de las mazmorras, necesitaras bajar '
Setup.BerserkerMultiWorld.Doors.exe'
- Durante el proceso de instalación, se te pedirá donde esta situado tu archivo ROM japonés v1.0. Si ya habías
instalado este software con anterioridad y simplemente estas actualizando, no se te pedirá la localización del
archivo una segunda vez.
- Puede ser que el programa pida la instalación de Microsoft Visual C++. Si ya lo tienes en tu ordenador (
posiblemente por que un juego de Steam ya lo haya instalado), el instalador no te pedirá su instalación.
2. Si estas usando un emulador, deberías asignar la versión capaz de ejecutar scripts Lua como programa por defecto para
lanzar ficheros de ROM de SNES.
1. Extrae tu emulador al escritorio, o cualquier sitio que después recuerdes.
2. Haz click derecho en un fichero de ROM (ha de tener la extensión sfc) y selecciona **Abrir con...**
3. Marca la opción **Usar siempre esta aplicación para abrir los archivos .sfc**
4. Baja hasta el final de la lista y haz click en la opción **Buscar otra aplicación en el equipo** (Si usas Windows 10 es posible que debas hacer click en **Más aplicaciones**)
5. Busca el archivo .exe de tu emulador y haz click en **Abrir**. Este archivo debe estar en el directorio donde extrajiste en el paso 1.
4. Baja hasta el final de la lista y haz click en la opción **Buscar otra aplicación en el equipo** (Si usas Windows
10 es posible que debas hacer click en **Más aplicaciones**)
5. Busca el archivo .exe de tu emulador y haz click en **Abrir**. Este archivo debe estar en el directorio donde
extrajiste en el paso 1.
### Instalación en Macintosh
- ¡Necesitamos voluntarios para rellenar esta seccion! Contactad con **Farrak Kilhn** (en inglés) en Discord si queréis ayudar.
- ¡Necesitamos voluntarios para rellenar esta seccion! Contactad con **Farrak Kilhn** (en inglés) en Discord si queréis
ayudar.
## Configurar tu archivo YAML
### Que es un archivo YAML y por qué necesito uno?
Tu archivo YAML contiene un conjunto de opciones de configuración que proveen al generador con información sobre como debe generar tu juego.
Cada jugador en una partida de multiworld proveerá su propio fichero YAML. Esta configuración permite
que cada jugador disfrute de una experiencia personalizada a su gusto, y cada jugador dentro de la misma partida de multiworld puede tener diferentes opciones.
Tu archivo YAML contiene un conjunto de opciones de configuración que proveen al generador con información sobre como
debe generar tu juego. Cada jugador en una partida de multiworld proveerá su propio fichero YAML. Esta configuración
permite que cada jugador disfrute de una experiencia personalizada a su gusto, y cada jugador dentro de la misma partida
de multiworld puede tener diferentes opciones.
### Donde puedo obtener un fichero YAML?
La página "[Generate Game](/player-settings)" en el sitio web te permite configurar tu configuración personal y
descargar un fichero "YAML".
La página "[Generate Game](/games/A%20Link%20to%20the%20Past/player-settings)" en el sitio web te permite configurar tu
configuración personal y descargar un fichero "YAML".
### Configuración YAML avanzada
Una version mas avanzada del fichero Yaml puede ser creada usando la pagina ["Weighted settings"](/weighted-settings),
la cual te permite tener almacenadas hasta 3 preajustes. La pagina "Weighted Settings" tiene muchas opciones representadas con controles deslizantes. Esto permite
elegir cuan probable los valores de una categoría pueden ser elegidos sobre otros de la misma.
la cual te permite tener almacenadas hasta 3 preajustes. La pagina "Weighted Settings" tiene muchas opciones
representadas con controles deslizantes. Esto permite elegir cuan probable los valores de una categoría pueden ser
elegidos sobre otros de la misma.
Por ejemplo, imagina que el generador crea un cubo llamado "map_shuffle", y pone trozos de papel doblado en él por cada sub-opción.
Ademas imaginemos que tu valor elegido para "on" es 20 y el elegido para "off" es 40.
Por ejemplo, imagina que el generador crea un cubo llamado "map_shuffle", y pone trozos de papel doblado en él por cada
sub-opción. Ademas imaginemos que tu valor elegido para "on" es 20 y el elegido para "off" es 40.
Por tanto, en este ejemplo, habrán 60 trozos de papel. 20 para "on" y 40 para "off".
Cuando el generador esta decidiendo si activar o no "map shuffle" para tu partida,
meterá la mano en el cubo y sacara un trozo de papel al azar. En este ejemplo,
es mucho mas probable (2 de cada 3 veces (40/60)) que "map shuffle" esté desactivado.
Por tanto, en este ejemplo, habrán 60 trozos de papel. 20 para "on" y 40 para "off". Cuando el generador esta decidiendo
si activar o no "map shuffle" para tu partida, meterá la mano en el cubo y sacara un trozo de papel al azar. En este
ejemplo, es mucho mas probable (2 de cada 3 veces (40/60)) que "map shuffle" esté desactivado.
Si quieres que una opción no pueda ser escogida, simplemente asigna el valor 0 a dicha opción. Recuerda que cada opción debe tener
al menos un valor mayor que cero, si no la generación fallará.
Si quieres que una opción no pueda ser escogida, simplemente asigna el valor 0 a dicha opción. Recuerda que cada opción
debe tener al menos un valor mayor que cero, si no la generación fallará.
### Verificando tu archivo YAML
Si quieres validar que tu fichero YAML para asegurarte que funciona correctamente, puedes hacerlo en la pagina
[YAML Validator](/mysterycheck).
## Generar una partida para un jugador
1. Navega a [la pagina Generate game](/player-settings), configura tus opciones, haz click en el boton "Generate game".
1. Navega a [la pagina Generate game](/games/A%20Link%20to%20the%20Past/player-settings), configura tus opciones, haz
click en el boton "Generate game".
2. Se te redigirá a una pagina "Seed Info", donde puedes descargar tu archivo de parche.
3. Haz doble click en tu fichero de parche, y el emulador debería ejecutar tu juego automáticamente. Como el
Cliente no es necesario para partidas de un jugador, puedes cerrarlo junto a la pagina web (que tiene como titulo "Multiworld WebUI") que se ha abierto automáticamente.
3. Haz doble click en tu fichero de parche, y el emulador debería ejecutar tu juego automáticamente. Como el Cliente no
es necesario para partidas de un jugador, puedes cerrarlo junto a la pagina web (que tiene como titulo "Multiworld
WebUI") que se ha abierto automáticamente.
## Unirse a una partida MultiWorld
### Obtener el fichero de parche y crea tu ROM
Cuando te unes a una partida multiworld, debes proveer tu fichero YAML a quien sea el creador de la partida. Una vez
este hecho, el creador te devolverá un enlace para descargar el parche o un fichero zip conteniendo todos los ficheros de parche de la partida
Tu fichero de parche debe tener la extensión `.bmbp`.
Pon tu fichero de parche en el escritorio o en algún sitio conveniente, y haz doble click. Esto debería ejecutar automáticamente
el cliente, y ademas creara la rom en el mismo directorio donde este el fichero de parche.
Cuando te unes a una partida multiworld, debes proveer tu fichero YAML a quien sea el creador de la partida. Una vez
este hecho, el creador te devolverá un enlace para descargar el parche o un fichero zip conteniendo todos los ficheros
de parche de la partida Tu fichero de parche debe tener la extensión `.bmbp`.
Pon tu fichero de parche en el escritorio o en algún sitio conveniente, y haz doble click. Esto debería ejecutar
automáticamente el cliente, y ademas creara la rom en el mismo directorio donde este el fichero de parche.
### Conectar al cliente
#### Con emulador
Cuando el cliente se lance automáticamente, QUsb2Snes debería haberse ejecutado también.
Si es la primera vez que lo ejecutas, puedes ser que el firewall de Windows te pregunte si le permites la comunicación.
Cuando el cliente se lance automáticamente, QUsb2Snes debería haberse ejecutado también. Si es la primera vez que lo
ejecutas, puedes ser que el firewall de Windows te pregunte si le permites la comunicación.
##### snes9x Multitroid
1. Carga tu fichero de ROM, si no lo has hecho ya
2. Abre el menu "File" y situa el raton en **Lua Scripting**
3. Haz click en **New Lua Script Window...**
4. En la nueva ventana, haz click en **Browse...**
5. Navega hacia el directorio donde este situado snes9x Multitroid, entra en el directorio `lua`, y escoge `multibridge.lua`
6. Observa que se ha asignado un nombre al dispositivo, y el cliente muestra "SNES Device: Connected", con el mismo nombre
en la esquina superior izquierda.
5. Navega hacia el directorio donde este situado snes9x Multitroid, entra en el directorio `lua`, y
escoge `multibridge.lua`
6. Observa que se ha asignado un nombre al dispositivo, y el cliente muestra "SNES Device: Connected", con el mismo
nombre en la esquina superior izquierda.
##### BizHawk
1. Asegurate que se ha cargado el nucleo BSNES. Debes hacer esto en el menu Tools y siguiento estas opciones:
`Config --> Cores --> SNES --> BSNES`
Una vez cambiado el nucleo cargado, Bizhawk ha de ser reiniciado.
@@ -107,14 +132,16 @@ Si es la primera vez que lo ejecutas, puedes ser que el firewall de Windows te p
5. Navega al directorio de instalación de MultiWorld Utilities, y en los siguiente directorios:
`QUsb2Snes/Qusb2Snes/LuaBridge`
6. Selecciona `luabridge.lua` y haz click en Abrir.
7. Observa que se ha asignado un nombre al dispositivo, y el cliente muestra "SNES Device: Connected", con el mismo nombre
en la esquina superior izquierda.
7. Observa que se ha asignado un nombre al dispositivo, y el cliente muestra "SNES Device: Connected", con el mismo
nombre en la esquina superior izquierda.
#### Con Hardware
Esta guía asume que ya has descargado el firmware correcto para tu dispositivo. Si no lo has hecho ya, hazlo ahora.
Los usuarios de SD2SNES y FXPak Pro pueden descargar el firmware apropiado
Esta guía asume que ya has descargado el firmware correcto para tu dispositivo. Si no lo has hecho ya, hazlo ahora. Los
usuarios de SD2SNES y FXPak Pro pueden descargar el firmware apropiado
[aqui](https://github.com/RedGuyyyy/sd2snes/releases). Los usuarios de otros dispositivos pueden encontrar información
[en esta página](http://usb2snes.com/#supported-platforms).
1. Cierra tu emulador, el cual debe haberse autoejecutado.
2. Cierra QUsb2Snes, el cual fue ejecutado junto al cliente.
3. Ejecuta la version correcta de QUsb2Snes (v0.7.16).
@@ -122,19 +149,22 @@ Los usuarios de SD2SNES y FXPak Pro pueden descargar el firmware apropiado
5. Observa en el cliente que ahora muestra "SNES Device: Connected", y aparece el nombre del dispositivo.
### Conecta al MultiServer
El fichero de parche que ha lanzado el cliente debe haberte conectado automaticamente al MultiServer.
Hay algunas razonas por las que esto puede que no pase, incluyendo que el juego este hospedado en el sitio web pero
se genero en algún otro sitio. Si el cliente muestra "Server Status: Not Connected", preguntale al creador de la partida
la dirección del servidor, copiala en el campo "Server" y presiona Enter.
El cliente intentara conectarse a esta nueva dirección, y debería mostrar "Server
Status: Connected" en algún momento. Si el cliente no se conecta al cabo de un rato, puede ser que necesites refrescar la pagina web.
El fichero de parche que ha lanzado el cliente debe haberte conectado automaticamente al MultiServer. Hay algunas
razonas por las que esto puede que no pase, incluyendo que el juego este hospedado en el sitio web pero se genero en
algún otro sitio. Si el cliente muestra "Server Status: Not Connected", preguntale al creador de la partida la dirección
del servidor, copiala en el campo "Server" y presiona Enter.
El cliente intentara conectarse a esta nueva dirección, y debería mostrar "Server Status: Connected" en algún momento.
Si el cliente no se conecta al cabo de un rato, puede ser que necesites refrescar la pagina web.
### Jugando
Cuando ambos SNES Device and Server aparezcan como "connected", estas listo para empezar a jugar. Felicidades
por unirte satisfactoriamente a una partida de multiworld!
Cuando ambos SNES Device and Server aparezcan como "connected", estas listo para empezar a jugar. Felicidades por unirte
satisfactoriamente a una partida de multiworld!
## Hospedando una partida de multiworld
La manera recomendad para hospedar una partida es usar el servicio proveído en
[el sitio web](/generate). El proceso es relativamente sencillo:
@@ -143,28 +173,34 @@ La manera recomendad para hospedar una partida es usar el servicio proveído en
3. Carga el fichero zip en el sitio web enlazado anteriormente.
4. Espera a que la seed sea generada.
5. Cuando esto acabe, se te redigirá a una pagina titulada "Seed Info".
6. Haz click en "Create New Room". Esto te llevara a la pagina del servidor. Pasa el enlace a esta pagina a los jugadores
para que puedan descargar los ficheros de parche de ahi.
6. Haz click en "Create New Room". Esto te llevara a la pagina del servidor. Pasa el enlace a esta pagina a los
jugadores para que puedan descargar los ficheros de parche de ahi.
**Nota:** Los ficheros de parche de esta pagina permiten a los jugadores conectarse al servidor automaticamente,
mientras que los de la pagina "Seed info" no.
7. Hay un enlace a un MultiWorld Tracker en la parte superior de la pagina de la sala. Deberías pasar también este enlace
a los jugadores para que puedan ver el progreso de la partida. A los observadores también se les puede pasar este enlace.
7. Hay un enlace a un MultiWorld Tracker en la parte superior de la pagina de la sala. Deberías pasar también este
enlace a los jugadores para que puedan ver el progreso de la partida. A los observadores también se les puede pasar
este enlace.
8. Una vez todos los jugadores se han unido, podeis empezar a jugar.
## Auto-Tracking
Si deseas usar auto-tracking para tu partida, varios programas ofrecen esta funcionalidad.
El programa recomentdado actualmente es:
[OpenTracker](https://github.com/trippsc2/OpenTracker/releases).
### Instalación
1. Descarga el fichero de instalacion apropiado para tu ordenador (Usuarios de windows quieren el fichero ".msi").
2. Durante el proceso de insatalación, puede que se te pida instalar Microsoft Visual Studio Build Tools. Un enlace
este programa se muestra durante la proceso, y debe ser ejecutado manualmente.
2. Durante el proceso de insatalación, puede que se te pida instalar Microsoft Visual Studio Build Tools. Un enlace este
programa se muestra durante la proceso, y debe ser ejecutado manualmente.
### Activar auto-tracking
1. Con OpenTracker ejecutado, haz click en el menu Tracking en la parte superior de la ventana, y elige **AutoTracker...**
1. Con OpenTracker ejecutado, haz click en el menu Tracking en la parte superior de la ventana, y elige **
AutoTracker...**
2. Click the **Get Devices** button
3. Selecciona tu "SNES device" de la lista
4. Si quieres que las llaves y los objetos de mazmorra tambien sean marcados, activa la caja con nombre **Race Illegal Tracking**
4. Si quieres que las llaves y los objetos de mazmorra tambien sean marcados, activa la caja con nombre **Race Illegal
Tracking**
5. Haz click en el boton **Start Autotracking**
6. Cierra la ventana AutoTracker, ya que deja de ser necesaria

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