Compare commits

...

165 Commits
0.2.3 ... 0.2.4

Author SHA1 Message Date
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
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
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
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
Hussein Farran
3e16593bb7 WebHost: Wrap SoE guide at 120 chars at request of black-sliver. 2021-12-27 16:08:14 -05: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
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
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
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
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
150 changed files with 7727 additions and 2331 deletions

View File

@@ -9,13 +9,22 @@ 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.9
- name: Set up Python ${{ matrix.python.version }}
uses: actions/setup-python@v1
with:
python-version: 3.9
python-version: ${{ matrix.python.version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip

12
.gitignore vendored
View File

@@ -12,6 +12,7 @@
*.sfc
*.z64
*.n64
*.nes
*.wixobj
*.lck
*.db3
@@ -26,14 +27,9 @@ dist
README.html
.vs/
EnemizerCLI/
RaceRom.py
weights/
/MultiMystery/
/Players/
/QUsb2Snes/
/options.yaml
/config.yaml
/uploads/
/logs/
_persistent_storage.yaml
mystery_result_*.yaml
@@ -44,6 +40,10 @@ Output Logs/
/factorio/
/Minecraft Forge Server/
/WebHostLib/static/generated
/freeze_requirements.txt
/Archipelago.zip
/setup.ini
# Byte-compiled / optimized / DLL files
__pycache__/
@@ -151,8 +151,6 @@ dmypy.json
# Cython debug symbols
cython_debug/
Archipelago.zip
#minecraft server stuff
jdk*/
minecraft*/

View File

@@ -334,11 +334,14 @@ class MultiWorld():
return [location for location in self.get_locations() if
(player is None or location.player == player) and location.item is None and location.can_reach(state)]
def get_unfilled_locations_for_players(self, location_name: str, players: Iterable[int]):
def get_unfilled_locations_for_players(self, locations, players: Iterable[int]):
for player in players:
location = self.get_location(location_name, player)
if location.item is None:
yield location
if len(locations) == 0:
locations = [location.name for location in self.get_unfilled_locations(player)]
for location_name in locations:
location = self._location_cache.get((location_name, player), None)
if location is not None and location.item is None:
yield location
def unlocks_new_location(self, item) -> bool:
temp_state = self.state.copy()
@@ -787,7 +790,7 @@ class Region(object):
self.type = type_
self.entrances = []
self.exits = []
self.locations = []
self.locations: List[Location] = []
self.dungeon = None
self.shop = None
self.world = world
@@ -908,6 +911,11 @@ class Boss():
return f"Boss({self.name})"
class LocationProgressType(Enum):
DEFAULT = 1
PRIORITY = 2
EXCLUDED = 3
class Location():
# If given as integer, then this is the shop's inventory index
shop_slot: Optional[int] = None
@@ -919,6 +927,7 @@ class Location():
show_in_spoiler: bool = True
excluded: bool = False
crystal: bool = False
progress_type: LocationProgressType = LocationProgressType.DEFAULT
always_allow = staticmethod(lambda item, state: False)
access_rule = staticmethod(lambda state: True)
item_rule = staticmethod(lambda item: True)
@@ -1012,6 +1021,10 @@ class Item():
def pedestal_hint_text(self):
return getattr(self, "_pedestal_hint_text", self.name.replace("_", " ").replace("-", " "))
@property
def flags(self) -> int:
return self.advancement + (self.never_exclude << 1) + (self.trap << 2)
def __eq__(self, other):
return self.name == other.name and self.player == other.player

View File

@@ -118,6 +118,7 @@ class CommonContext():
game = None
ui = None
keep_alive_task = None
items_handling: typing.Optional[int] = None
def __init__(self, server_address, password):
# server state
@@ -247,9 +248,9 @@ class CommonContext():
async def send_connect(self, **kwargs):
payload = {
"cmd": 'Connect',
'cmd': 'Connect',
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
'tags': self.tags,
'tags': self.tags, 'items_handling': self.items_handling,
'uuid': Utils.get_unique_identifier(), 'game': self.game
}
if kwargs:
@@ -464,6 +465,8 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
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')
@@ -587,8 +590,9 @@ if __name__ == '__main__':
# Text Mode to use !hint and such with games that have no text entry
class TextContext(CommonContext):
tags = {"AP", "IgnoreGame"}
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:

View File

@@ -30,6 +30,9 @@ class FF1CommandProcessor(ClientCommandProcessor):
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
@@ -40,8 +43,6 @@ class FF1Context(CommonContext):
self.game = 'Final Fantasy'
self.awaiting_rom = False
command_processor = FF1CommandProcessor
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(FF1Context, self).server_auth(password_requested)

View File

@@ -49,6 +49,7 @@ class FactorioCommandProcessor(ClientCommandProcessor):
class FactorioContext(CommonContext):
command_processor = FactorioCommandProcessor
game = "Factorio"
items_handling = 0b111 # full remote
# updated by spinup server
mod_version: Utils.Version = Utils.Version(0, 0, 0)
@@ -308,8 +309,8 @@ async def main(args):
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")
succesful_launch = await factorio_server_task
if succesful_launch:
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")
@@ -333,12 +334,8 @@ 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)

389
Fill.py
View File

@@ -5,8 +5,8 @@ import itertools
from collections import Counter, deque
from BaseClasses import CollectionState, Location, MultiWorld, Item
from worlds.generic import PlandoItem
from BaseClasses import CollectionState, Location, LocationProgressType, MultiWorld, Item
from worlds.AutoWorld import call_all
@@ -14,7 +14,7 @@ class FillError(RuntimeError):
pass
def sweep_from_pool(base_state: CollectionState, itempool):
def sweep_from_pool(base_state: CollectionState, itempool=[]):
new_state = base_state.copy()
for item in itempool:
new_state.collect(item, True)
@@ -25,10 +25,10 @@ def sweep_from_pool(base_state: CollectionState, itempool):
def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations, itempool: typing.List[Item],
single_player_placement=False, lock=False):
unplaced_items = []
placements = []
placements: typing.List[Location] = []
swapped_items = Counter()
reachable_items: dict[str, deque] = {}
reachable_items: typing.Dict[int, deque] = {}
for item in itempool:
reachable_items.setdefault(item.player, deque()).append(item)
@@ -39,10 +39,11 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations,
for item in items_to_place:
itempool.remove(item)
maximum_exploration_state = sweep_from_pool(base_state, itempool)
has_beaten_game = world.has_beaten_game(maximum_exploration_state)
for item_to_place in items_to_place:
spot_to_fill: Location = 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
@@ -59,33 +60,52 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations,
else:
# we filled all reachable spots.
# try swaping this item with previously placed items
# try swapping this item with previously placed items
for(i, location) in enumerate(placements):
placed_item = location.item
# Unplaceable items can sometimes be swapped infinitely. Limit the
# number of times we will swap an individual item to prevent this
if swapped_items[placed_item.player, placed_item.name] > 0:
swap_count = swapped_items[placed_item.player,
placed_item.name]
if swap_count > 1:
continue
location.item = None
placed_item.location = None
swap_state = sweep_from_pool(base_state, itempool)
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):
# Add this item to the exisiting placement, and
# add the old item to the back of the queue
spot_to_fill = placements.pop(i)
swapped_items[placed_item.player,
placed_item.name] += 1
reachable_items[placed_item.player].appendleft(
placed_item)
itempool.append(placed_item)
break
else:
# Item can't be placed here, restore original item
location.item = placed_item
placed_item.location = location
if spot_to_fill == None:
# 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:
# Maybe the game can be beaten anyway?
unplaced_items.append(item_to_place)
if world.accessibility[item_to_place.player] != 'minimal' and world.can_beat_game():
@@ -103,21 +123,20 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations,
itempool.extend(unplaced_items)
def distribute_items_restrictive(world: MultiWorld, fill_locations=None):
# If not passed in, then get a shuffled list of locations to fill in
if not 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.never_exclude: # this only gets nonprogression items which should not appear in excluded locations
@@ -129,21 +148,47 @@ def distribute_items_restrictive(world: MultiWorld, fill_locations=None):
else:
restitempool.append(item)
world.random.shuffle(fill_locations)
call_all(world, "fill_hook", progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool, restitempool, fill_locations)
call_all(world, "fill_hook", progitempool, nonexcludeditempool,
localrestitempool, nonlocalrestitempool, restitempool, fill_locations)
fill_restrictive(world, world.state, fill_locations, progitempool)
locations: typing.Dict[LocationProgressType, typing.List[Location]] = {
type: [] for type in LocationProgressType}
for loc in fill_locations:
locations[loc.progress_type].append(loc)
prioritylocations = locations[LocationProgressType.PRIORITY]
defaultlocations = locations[LocationProgressType.DEFAULT]
excludedlocations = locations[LocationProgressType.EXCLUDED]
fill_restrictive(world, world.state, prioritylocations, progitempool)
if prioritylocations:
defaultlocations = prioritylocations + defaultlocations
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')
if nonexcludeditempool:
world.random.shuffle(fill_locations)
fill_restrictive(world, world.state, fill_locations, nonexcludeditempool) # needs logical fill to not conflict with local items
world.random.shuffle(defaultlocations)
# needs logical fill to not conflict with local items
nonexcludeditempool, defaultlocations = fast_fill(
world, nonexcludeditempool, defaultlocations)
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]
@@ -154,24 +199,33 @@ def distribute_items_restrictive(world: MultiWorld, 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)
for item_to_place in nonlocalrestitempool:
for i, location in enumerate(fill_locations):
for i, location in enumerate(defaultlocations):
if location.player != item_to_place.player:
world.push_item(fill_locations.pop(i), item_to_place, False)
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 {fill_locations}, tossing.")
logging.warning(
f"Could not place non_local_item {item_to_place} among {defaultlocations}, tossing.")
world.random.shuffle(fill_locations)
world.random.shuffle(defaultlocations)
restitempool, fill_locations = fast_fill(world, restitempool, fill_locations)
restitempool, defaultlocations = fast_fill(
world, restitempool, defaultlocations)
unplaced = progitempool + restitempool
unfilled = [location.name for location in fill_locations]
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: MultiWorld, item_pool: typing.List, fill_locations: typing.List) -> typing.Tuple[typing.List, typing.List]:
@@ -279,7 +333,10 @@ def balance_multiworld_progression(world: MultiWorld):
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:
@@ -369,80 +426,190 @@ def swap_location_item(location_1: Location, location_2: Location, check_locked=
location_1.item.location = location_1
location_2.item.location = location_2
location_1.event, location_2.event = location_2.event, location_1.event
def distribute_planned(world: MultiWorld):
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:
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
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
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]
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
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'])
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:
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.")
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):
failed(
f"Cannot place item in world {target_world} as it is not in range of (1, {world.players})",
placement['force'])
continue
item = world.worlds[player].create_item(placement.item)
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
target_world = world.random.choice(unfilled).player
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
target_world = world.random.choice(unfilled).player
elif type(target_world) == int: # target world by player id
if target_world not in range(1, world.players + 1):
placement.failed(
f"Cannot place item in world {target_world} as it is not in range of (1, {world.players})",
ValueError)
continue
else: # find world by 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)
continue
target_world = 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}.")
worlds = {target_world}
else: # target world by slot name
if target_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 = {world_name_lookup[target_world]}
if location.can_fill(world.state, item, False):
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 placement.from_pool: # Should happen AFTER the item is placed, in case it was allowed to skip failed placement.
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}")
if from_pool:
try:
world.itempool.remove(item)
except ValueError:
placement.warn(f"Could not remove {item} from pool as it's already missing from it.")
warn(
f"Could not remove {item} from pool for {world.player_name[player]} as it's already missing from it.",
placement['force'])
except Exception as e:
raise Exception(f"Error running plando for player {player} ({world.player_name[player]})") from e
raise Exception(
f"Error running plando for player {player} ({world.player_name[player]})") from e

View File

@@ -61,8 +61,8 @@ def mystery_argparse():
return args, options
def get_seed_name(random):
return f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits)
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):
@@ -191,8 +191,6 @@ def main(args=None, callback=ERmain):
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}'")
@@ -222,11 +220,11 @@ def read_weights_yaml(path):
return parse_yaml(yaml)
def interpret_on_off(value):
def interpret_on_off(value) -> bool:
return {"on": True, "off": False}.get(value, value)
def convert_to_on_off(value):
def convert_to_on_off(value) -> str:
return {True: "on", False: "off"}.get(value, value)
@@ -496,7 +494,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
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 = roll_item_plando(world_type, game_weights)
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 = []
@@ -515,45 +513,6 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
raise Exception(f"Unsupported game {ret.game}")
return ret
def roll_item_plando(world_type, weights):
plando_items = []
def add_plando_item(item: str, location: str):
if item not in world_type.item_name_to_id:
raise Exception(f"Could not plando item {item} as the item was not recognized")
if location not in world_type.location_name_to_id:
raise Exception(
f"Could not plando item {item} at location {location} as the location was not recognized")
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_legacy("percentage", placement, 100)):
from_pool = get_choice_legacy("from_pool", placement, PlandoItem._field_defaults["from_pool"])
location_world = get_choice_legacy("world", placement, PlandoItem._field_defaults["world"])
force = str(get_choice_legacy("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_legacy("item", placement, get_choice_legacy("items", placement))
location = get_choice_legacy("location", placement)
add_plando_item(item, location)
return plando_items
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)}.")

View File

@@ -14,14 +14,15 @@ import tkinter as tk
from argparse import Namespace
from concurrent.futures import as_completed, ThreadPoolExecutor
from glob import glob
from tkinter import Tk, Frame, Label, StringVar, Entry, filedialog, messagebox, Button, LEFT, X, TOP, LabelFrame, \
from tkinter import Tk, Frame, Label, StringVar, Entry, filedialog, messagebox, Button, Radiobutton, LEFT, X, TOP, LabelFrame, \
IntVar, Checkbutton, E, W, OptionMenu, Toplevel, BOTTOM, RIGHT, font as font, PhotoImage
from tkinter.constants import DISABLED, NORMAL
from urllib.parse import urlparse
from urllib.request import urlopen
from worlds.alttp.Rom import Sprite, LocalRom, apply_rom_settings, get_base_rom_bytes
from Utils import output_path, local_path, open_file, get_cert_none_ssl_context, persistent_store
from Utils import output_path, local_path, open_file, get_cert_none_ssl_context, persistent_store, get_adjuster_settings, tkinter_center_window
from Patch import GAME_ALTTP
class AdjusterWorld(object):
def __init__(self, sprite_pool):
@@ -121,7 +122,7 @@ def main():
args, path = adjust(args=args)
if isinstance(args.sprite, Sprite):
args.sprite = args.sprite.name
persistent_store("adjuster", "last_settings_3", args)
persistent_store("adjuster", GAME_ALTTP, args)
def adjust(args):
@@ -196,6 +197,7 @@ def adjustGUI():
def adjustRom():
guiargs = Namespace()
guiargs.auto_apply = rom_vars.auto_apply.get()
guiargs.heartbeep = rom_vars.heartbeepVar.get()
guiargs.heartcolor = rom_vars.heartcolorVar.get()
guiargs.menuspeed = rom_vars.menuspeedVar.get()
@@ -226,14 +228,43 @@ def adjustGUI():
messagebox.showinfo(title="Success", message=f"Rom patched successfully to {path}")
if isinstance(guiargs.sprite, Sprite):
guiargs.sprite = guiargs.sprite.name
persistent_store("adjuster", "last_settings_3", guiargs)
delattr(guiargs, "rom")
persistent_store("adjuster", GAME_ALTTP, guiargs)
def saveGUISettings():
guiargs = Namespace()
guiargs.auto_apply = rom_vars.auto_apply.get()
guiargs.heartbeep = rom_vars.heartbeepVar.get()
guiargs.heartcolor = rom_vars.heartcolorVar.get()
guiargs.menuspeed = rom_vars.menuspeedVar.get()
guiargs.ow_palettes = rom_vars.owPalettesVar.get()
guiargs.uw_palettes = rom_vars.uwPalettesVar.get()
guiargs.hud_palettes = rom_vars.hudPalettesVar.get()
guiargs.sword_palettes = rom_vars.swordPalettesVar.get()
guiargs.shield_palettes = rom_vars.shieldPalettesVar.get()
guiargs.quickswap = bool(rom_vars.quickSwapVar.get())
guiargs.music = bool(rom_vars.MusicVar.get())
guiargs.reduceflashing = bool(rom_vars.disableFlashingVar.get())
guiargs.deathlink = bool(rom_vars.DeathLinkVar.get())
guiargs.baserom = romVar.get()
if isinstance(rom_vars.sprite, Sprite):
guiargs.sprite = rom_vars.sprite.name
else:
guiargs.sprite = rom_vars.sprite
guiargs.sprite_pool = rom_vars.sprite_pool
persistent_store("adjuster", GAME_ALTTP, guiargs)
messagebox.showinfo(title="Success", message="Settings saved to persistent storage")
adjustButton = Button(bottomFrame2, text='Adjust Rom', command=adjustRom)
rom_options_frame.pack(side=TOP)
adjustButton.pack(side=BOTTOM, padx=(5, 5))
adjustButton.pack(side=LEFT, padx=(5,5))
bottomFrame2.pack(side=BOTTOM, pady=(5, 5))
saveButton = Button(bottomFrame2, text='Save Settings', command=saveGUISettings)
saveButton.pack(side=LEFT, padx=(5,5))
bottomFrame2.pack(side=TOP, pady=(5,5))
tkinter_center_window(adjustWindow)
adjustWindow.mainloop()
@@ -437,9 +468,14 @@ class BackgroundTaskProgressNullWindow(BackgroundTask):
def get_rom_frame(parent=None):
adjuster_settings = get_adjuster_settings(GAME_ALTTP)
if not adjuster_settings:
adjuster_settings = Namespace()
adjuster_settings.baserom = "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"
romFrame = Frame(parent)
baseRomLabel = Label(romFrame, text='LttP Base Rom: ')
romVar = StringVar(value="Zelda no Densetsu - Kamigami no Triforce (Japan).sfc")
romVar = StringVar(value=adjuster_settings.baserom)
romEntry = Entry(romFrame, textvariable=romVar)
def RomSelect():
@@ -465,6 +501,26 @@ def get_rom_frame(parent=None):
def get_rom_options_frame(parent=None):
adjuster_settings = get_adjuster_settings(GAME_ALTTP)
if not adjuster_settings:
adjuster_settings = Namespace()
adjuster_settings.auto_apply = 'ask'
adjuster_settings.music = True
adjuster_settings.reduceflashing = True
adjuster_settings.deathlink = False
adjuster_settings.sprite = None
adjuster_settings.quickswap = True
adjuster_settings.menuspeed = 'normal'
adjuster_settings.heartcolor = 'red'
adjuster_settings.heartbeep = 'normal'
adjuster_settings.ow_palettes = 'default'
adjuster_settings.uw_palettes = 'default'
adjuster_settings.hud_palettes = 'default'
adjuster_settings.sword_palettes = 'default'
adjuster_settings.shield_palettes = 'default'
if not hasattr(adjuster_settings, 'sprite_pool'):
adjuster_settings.sprite_pool = []
romOptionsFrame = LabelFrame(parent, text="Rom options")
romOptionsFrame.columnconfigure(0, weight=1)
romOptionsFrame.columnconfigure(1, weight=1)
@@ -473,16 +529,16 @@ def get_rom_options_frame(parent=None):
vars = Namespace()
vars.MusicVar = IntVar()
vars.MusicVar.set(1)
vars.MusicVar.set(adjuster_settings.music)
MusicCheckbutton = Checkbutton(romOptionsFrame, text="Music", variable=vars.MusicVar)
MusicCheckbutton.grid(row=0, column=0, sticky=E)
vars.disableFlashingVar = IntVar(value=1)
vars.disableFlashingVar = IntVar(value=adjuster_settings.reduceflashing)
disableFlashingCheckbutton = Checkbutton(romOptionsFrame, text="Disable flashing (anti-epilepsy)",
variable=vars.disableFlashingVar)
disableFlashingCheckbutton.grid(row=6, column=0, sticky=W)
vars.DeathLinkVar = IntVar(value=0)
vars.DeathLinkVar = IntVar(value=adjuster_settings.deathlink)
DeathLinkCheckbutton = Checkbutton(romOptionsFrame, text="DeathLink (Team Deaths)", variable=vars.DeathLinkVar)
DeathLinkCheckbutton.grid(row=7, column=0, sticky=W)
@@ -491,7 +547,7 @@ def get_rom_options_frame(parent=None):
baseSpriteLabel = Label(spriteDialogFrame, text='Sprite:')
vars.spriteNameVar = StringVar()
vars.sprite = None
vars.sprite = adjuster_settings.sprite
def set_sprite(sprite_param):
nonlocal vars
@@ -505,8 +561,8 @@ def get_rom_options_frame(parent=None):
vars.sprite = sprite_param
vars.spriteNameVar.set(vars.sprite.name)
set_sprite(None)
vars.spriteNameVar.set('(unchanged)')
set_sprite(adjuster_settings.sprite)
#vars.spriteNameVar.set(adjuster_settings.sprite)
spriteEntry = Label(spriteDialogFrame, textvariable=vars.spriteNameVar)
def SpriteSelect():
@@ -519,7 +575,7 @@ def get_rom_options_frame(parent=None):
spriteEntry.pack(side=LEFT)
spriteSelectButton.pack(side=LEFT)
vars.quickSwapVar = IntVar(value=1)
vars.quickSwapVar = IntVar(value=adjuster_settings.quickswap)
quickSwapCheckbutton = Checkbutton(romOptionsFrame, text="L/R Quickswapping", variable=vars.quickSwapVar)
quickSwapCheckbutton.grid(row=1, column=0, sticky=E)
@@ -528,7 +584,7 @@ def get_rom_options_frame(parent=None):
menuspeedLabel = Label(menuspeedFrame, text='Menu speed')
menuspeedLabel.pack(side=LEFT)
vars.menuspeedVar = StringVar()
vars.menuspeedVar.set('normal')
vars.menuspeedVar.set(adjuster_settings.menuspeed)
menuspeedOptionMenu = OptionMenu(menuspeedFrame, vars.menuspeedVar, 'normal', 'instant', 'double', 'triple',
'quadruple', 'half')
menuspeedOptionMenu.pack(side=LEFT)
@@ -538,7 +594,7 @@ def get_rom_options_frame(parent=None):
heartcolorLabel = Label(heartcolorFrame, text='Heart color')
heartcolorLabel.pack(side=LEFT)
vars.heartcolorVar = StringVar()
vars.heartcolorVar.set('red')
vars.heartcolorVar.set(adjuster_settings.heartcolor)
heartcolorOptionMenu = OptionMenu(heartcolorFrame, vars.heartcolorVar, 'red', 'blue', 'green', 'yellow', 'random')
heartcolorOptionMenu.pack(side=LEFT)
@@ -547,7 +603,7 @@ def get_rom_options_frame(parent=None):
heartbeepLabel = Label(heartbeepFrame, text='Heartbeep')
heartbeepLabel.pack(side=LEFT)
vars.heartbeepVar = StringVar()
vars.heartbeepVar.set('normal')
vars.heartbeepVar.set(adjuster_settings.heartbeep)
heartbeepOptionMenu = OptionMenu(heartbeepFrame, vars.heartbeepVar, 'double', 'normal', 'half', 'quarter', 'off')
heartbeepOptionMenu.pack(side=LEFT)
@@ -556,7 +612,7 @@ def get_rom_options_frame(parent=None):
owPalettesLabel = Label(owPalettesFrame, text='Overworld palettes')
owPalettesLabel.pack(side=LEFT)
vars.owPalettesVar = StringVar()
vars.owPalettesVar.set('default')
vars.owPalettesVar.set(adjuster_settings.ow_palettes)
owPalettesOptionMenu = OptionMenu(owPalettesFrame, vars.owPalettesVar, 'default', 'good', 'blackout', 'grayscale',
'negative', 'classic', 'dizzy', 'sick', 'puke')
owPalettesOptionMenu.pack(side=LEFT)
@@ -566,7 +622,7 @@ def get_rom_options_frame(parent=None):
uwPalettesLabel = Label(uwPalettesFrame, text='Dungeon palettes')
uwPalettesLabel.pack(side=LEFT)
vars.uwPalettesVar = StringVar()
vars.uwPalettesVar.set('default')
vars.uwPalettesVar.set(adjuster_settings.uw_palettes)
uwPalettesOptionMenu = OptionMenu(uwPalettesFrame, vars.uwPalettesVar, 'default', 'good', 'blackout', 'grayscale',
'negative', 'classic', 'dizzy', 'sick', 'puke')
uwPalettesOptionMenu.pack(side=LEFT)
@@ -576,7 +632,7 @@ def get_rom_options_frame(parent=None):
hudPalettesLabel = Label(hudPalettesFrame, text='HUD palettes')
hudPalettesLabel.pack(side=LEFT)
vars.hudPalettesVar = StringVar()
vars.hudPalettesVar.set('default')
vars.hudPalettesVar.set(adjuster_settings.hud_palettes)
hudPalettesOptionMenu = OptionMenu(hudPalettesFrame, vars.hudPalettesVar, 'default', 'good', 'blackout',
'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
hudPalettesOptionMenu.pack(side=LEFT)
@@ -586,7 +642,7 @@ def get_rom_options_frame(parent=None):
swordPalettesLabel = Label(swordPalettesFrame, text='Sword palettes')
swordPalettesLabel.pack(side=LEFT)
vars.swordPalettesVar = StringVar()
vars.swordPalettesVar.set('default')
vars.swordPalettesVar.set(adjuster_settings.sword_palettes)
swordPalettesOptionMenu = OptionMenu(swordPalettesFrame, vars.swordPalettesVar, 'default', 'good', 'blackout',
'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
swordPalettesOptionMenu.pack(side=LEFT)
@@ -596,7 +652,7 @@ def get_rom_options_frame(parent=None):
shieldPalettesLabel = Label(shieldPalettesFrame, text='Shield palettes')
shieldPalettesLabel.pack(side=LEFT)
vars.shieldPalettesVar = StringVar()
vars.shieldPalettesVar.set('default')
vars.shieldPalettesVar.set(adjuster_settings.shield_palettes)
shieldPalettesOptionMenu = OptionMenu(shieldPalettesFrame, vars.shieldPalettesVar, 'default', 'good', 'blackout',
'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
shieldPalettesOptionMenu.pack(side=LEFT)
@@ -606,7 +662,7 @@ def get_rom_options_frame(parent=None):
baseSpritePoolLabel = Label(spritePoolFrame, text='Sprite Pool:')
vars.spritePoolCountVar = StringVar()
vars.sprite_pool = []
vars.sprite_pool = adjuster_settings.sprite_pool
def set_sprite_pool(sprite_param):
nonlocal vars
@@ -625,7 +681,7 @@ def get_rom_options_frame(parent=None):
vars.spritePoolCountVar.set(str(len(vars.sprite_pool)))
set_sprite_pool(None)
vars.spritePoolCountVar.set('0')
vars.spritePoolCountVar.set(len(adjuster_settings.sprite_pool))
spritePoolEntry = Label(spritePoolFrame, textvariable=vars.spritePoolCountVar)
def SpritePoolSelect():
@@ -645,6 +701,18 @@ def get_rom_options_frame(parent=None):
spritePoolSelectButton.pack(side=LEFT)
spritePoolClearButton.pack(side=LEFT)
vars.auto_apply = StringVar(value=adjuster_settings.auto_apply)
autoApplyFrame = Frame(romOptionsFrame)
autoApplyFrame.grid(row=8, column=0, columnspan=2, sticky=W)
filler = Label(autoApplyFrame, text="Automatically apply last used settings on opening .apbp files")
filler.pack(side=TOP, expand=True, fill=X)
askRadio = Radiobutton(autoApplyFrame, text='Ask', variable=vars.auto_apply, value='ask')
askRadio.pack(side=LEFT, padx=5, pady=5)
alwaysRadio = Radiobutton(autoApplyFrame, text='Always', variable=vars.auto_apply, value='always')
alwaysRadio.pack(side=LEFT, padx=5, pady=5)
neverRadio = Radiobutton(autoApplyFrame, text='Never', variable=vars.auto_apply, value='never')
neverRadio.pack(side=LEFT, padx=5, pady=5)
return romOptionsFrame, vars, set_sprite
@@ -693,6 +761,9 @@ class SpriteSelector():
button = Button(frame, text="Update alttpr sprites", command=self.update_alttpr_sprites)
button.pack(side=RIGHT, padx=(5, 0))
button = Button(frame, text="Do not adjust sprite",command=self.use_default_sprite)
button.pack(side=LEFT,padx=(0,5))
button = Button(frame, text="Default Link sprite", command=self.use_default_link_sprite)
button.pack(side=LEFT, padx=(0, 5))
@@ -710,36 +781,36 @@ class SpriteSelector():
self.randomOnItemVar = IntVar()
self.randomOnBonkVar = IntVar()
self.randomOnRandomVar = IntVar()
self.randomOnAllVar = IntVar()
if self.randomOnEvent:
button = Checkbutton(frame, text="Hit", command=self.update_random_button, variable=self.randomOnHitVar)
button.pack(side=LEFT, padx=(0, 5))
self.buttonHit = Checkbutton(frame, text="Hit", command=self.update_random_button, variable=self.randomOnHitVar)
self.buttonHit.pack(side=LEFT, padx=(0, 5))
button = Checkbutton(frame, text="Enter", command=self.update_random_button, variable=self.randomOnEnterVar)
button.pack(side=LEFT, padx=(0, 5))
self.buttonEnter = Checkbutton(frame, text="Enter", command=self.update_random_button, variable=self.randomOnEnterVar)
self.buttonEnter.pack(side=LEFT, padx=(0, 5))
button = Checkbutton(frame, text="Exit", command=self.update_random_button, variable=self.randomOnExitVar)
button.pack(side=LEFT, padx=(0, 5))
self.buttonExit = Checkbutton(frame, text="Exit", command=self.update_random_button, variable=self.randomOnExitVar)
self.buttonExit.pack(side=LEFT, padx=(0, 5))
button = Checkbutton(frame, text="Slash", command=self.update_random_button, variable=self.randomOnSlashVar)
button.pack(side=LEFT, padx=(0, 5))
self.buttonSlash = Checkbutton(frame, text="Slash", command=self.update_random_button, variable=self.randomOnSlashVar)
self.buttonSlash.pack(side=LEFT, padx=(0, 5))
button = Checkbutton(frame, text="Item", command=self.update_random_button, variable=self.randomOnItemVar)
button.pack(side=LEFT, padx=(0, 5))
self.buttonItem = Checkbutton(frame, text="Item", command=self.update_random_button, variable=self.randomOnItemVar)
self.buttonItem.pack(side=LEFT, padx=(0, 5))
button = Checkbutton(frame, text="Bonk", command=self.update_random_button, variable=self.randomOnBonkVar)
button.pack(side=LEFT, padx=(0, 5))
self.buttonBonk = Checkbutton(frame, text="Bonk", command=self.update_random_button, variable=self.randomOnBonkVar)
self.buttonBonk.pack(side=LEFT, padx=(0, 5))
button = Checkbutton(frame, text="Random", command=self.update_random_button,
variable=self.randomOnRandomVar)
button.pack(side=LEFT, padx=(0, 5))
self.buttonRandom = Checkbutton(frame, text="Random", command=self.update_random_button, variable=self.randomOnRandomVar)
self.buttonRandom.pack(side=LEFT, padx=(0, 5))
if adjuster:
button = Button(frame, text="Current sprite from rom", command=self.use_default_sprite)
button.pack(side=LEFT, padx=(0, 5))
self.buttonAll = Checkbutton(frame, text="All", command=self.update_random_button, variable=self.randomOnAllVar)
self.buttonAll.pack(side=LEFT, padx=(0, 5))
set_icon(self.window)
self.window.focus()
tkinter_center_window(self.window)
def remove_from_sprite_pool(self, button, spritename):
self.callback(("remove", spritename))
@@ -879,9 +950,31 @@ class SpriteSelector():
self.add_to_sprite_pool("link")
def update_random_button(self):
if self.randomOnRandomVar.get():
if self.randomOnAllVar.get():
randomon = "all"
self.buttonHit.config(state=DISABLED)
self.buttonEnter.config(state=DISABLED)
self.buttonExit.config(state=DISABLED)
self.buttonSlash.config(state=DISABLED)
self.buttonItem.config(state=DISABLED)
self.buttonBonk.config(state=DISABLED)
self.buttonRandom.config(state=DISABLED)
elif self.randomOnRandomVar.get():
randomon = "random"
self.buttonHit.config(state=DISABLED)
self.buttonEnter.config(state=DISABLED)
self.buttonExit.config(state=DISABLED)
self.buttonSlash.config(state=DISABLED)
self.buttonItem.config(state=DISABLED)
self.buttonBonk.config(state=DISABLED)
else:
self.buttonHit.config(state=NORMAL)
self.buttonEnter.config(state=NORMAL)
self.buttonExit.config(state=NORMAL)
self.buttonSlash.config(state=NORMAL)
self.buttonItem.config(state=NORMAL)
self.buttonBonk.config(state=NORMAL)
self.buttonRandom.config(state=NORMAL)
randomon = "-hit" if self.randomOnHitVar.get() else ""
randomon += "-enter" if self.randomOnEnterVar.get() else ""
randomon += "-exit" if self.randomOnExitVar.get() else ""
@@ -1150,4 +1243,4 @@ class ToolTips(object):
if __name__ == '__main__':
main()
main()

13
Main.py
View File

@@ -9,7 +9,7 @@ import tempfile
import zipfile
from typing import Dict, Tuple, Optional
from BaseClasses import MultiWorld, CollectionState, Region, RegionType
from BaseClasses import Item, MultiWorld, CollectionState, Region, RegionType
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
@@ -258,7 +258,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
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)}
# for now special case Factorio tech_tree_information
sending_visible_players = set()
for slot in world.player_ids:
@@ -268,16 +268,17 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
def precollect_hint(location):
hint = NetUtils.Hint(location.item.player, location.player, location.address,
location.item.code, False)
location.item.code, False, "", location.item.flags)
precollected_hints[location.player].add(hint)
precollected_hints[location.item.player].add(hint)
locations_data: Dict[int, Dict[int, Tuple[int, int]]] = {player: {} for player in world.player_ids}
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
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]:
@@ -310,7 +311,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
multidata = zlib.compress(pickle.dumps(multidata), 9)
with open(os.path.join(temp_dir, f'{outfilebase}.archipelago'), 'wb') as f:
f.write(bytes([1])) # version of format
f.write(bytes([2])) # version of format
f.write(multidata)
multidata_task = pool.submit(write_multidata)

View File

@@ -23,7 +23,7 @@ def update_command():
subprocess.call([sys.executable, '-m', 'pip', 'install', '-r', file, '--upgrade'])
def update(yes = False, force = False):
def update(yes=False, force=False):
global update_ran
if not update_ran:
update_ran = True
@@ -38,9 +38,8 @@ def update(yes = False, force = False):
for line in requirementsfile:
if line.startswith('https://'):
# extract name and version from url
url = line.split(';')[0]
wheel = line.split('/')[-1]
name, version, _ = wheel.split('-',2)
name, version, _ = wheel.split('-', 2)
line = f'{name}=={version}'
requirements = pkg_resources.parse_requirements(line)
for requirement in requirements:
@@ -58,6 +57,7 @@ def update(yes = False, force = False):
if __name__ == "__main__":
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')

View File

@@ -23,7 +23,7 @@ ModuleUpdate.update()
import websockets
import colorama
from fuzzywuzzy import process as fuzzy_process
from thefuzz import process as fuzzy_process
import NetUtils
from worlds.AutoWorld import AutoWorldRegister
@@ -41,6 +41,10 @@ colorama.init()
class Client(Endpoint):
version = Version(0, 0, 0)
tags: typing.List[str] = []
remote_items: bool
remote_start_inventory: bool
no_items: bool
no_locations: bool
def __init__(self, socket: websockets.WebSocketServerProtocol, ctx: Context):
super().__init__(socket)
@@ -52,6 +56,20 @@ class Client(Endpoint):
self.messageprocessor = client_message_processor(ctx, self)
self.ctx = weakref.ref(ctx)
@property
def items_handling(self):
if self.no_items:
return 0
return 1 + (self.remote_items << 1) + (self.remote_start_inventory << 2)
@items_handling.setter
def items_handling(self, value: int):
if not (value & 0b001) and (value & 0b110):
raise ValueError("Invalid flag combination")
self.no_items = not (value & 0b001)
self.remote_items = bool(value & 0b010)
self.remote_start_inventory = bool(value & 0b100)
@property
def name(self) -> str:
ctx = self.ctx()
@@ -78,7 +96,8 @@ class Context:
"compatibility": int}
# team -> slot id -> list of clients authenticated to slot.
clients: typing.Dict[int, typing.Dict[int, typing.List[Client]]]
locations: typing.Dict[int, typing.Dict[int, typing.Tuple[int, int]]]
locations: typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int]]]
save_version = 2
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
hint_cost: int, item_cheat: bool, forfeit_mode: str = "disabled", collect_mode="disabled",
@@ -108,6 +127,7 @@ class Context:
self.server = None
self.countdown_timer = 0
self.received_items = {}
self.start_inventory = {}
self.name_aliases: typing.Dict[team_slot, str] = {}
self.location_checks = collections.defaultdict(set)
self.hint_cost = hint_cost
@@ -241,8 +261,8 @@ class Context:
@staticmethod
def decompress(data: bytes) -> dict:
format_version = data[0]
if format_version != 1:
raise Exception("Incompatible multidata.")
if format_version > 2:
raise Utils.VersionException("Incompatible multidata.")
return restricted_loads(zlib.decompress(data[1:]))
def _load(self, decoded_obj: dict, use_embedded_server_options: bool):
@@ -273,11 +293,10 @@ class Context:
self.er_hint_data = {int(player): {int(address): name for address, name in loc_data.items()}
for player, loc_data in decoded_obj["er_hint_data"].items()}
self.games = decoded_obj["games"]
# award remote-items start inventory:
# load start inventory:
for slot, item_codes in decoded_obj["precollected_items"].items():
self.start_inventory[slot] = [NetworkItem(item_code, -2, 0) for item_code in item_codes]
for team in range(len(decoded_obj['names'])):
for slot, item_codes in decoded_obj["precollected_items"].items():
if slot in self.remote_start_inventory:
self.received_items[team, slot] = [NetworkItem(item_code, -2, 0) for item_code in item_codes]
for slot, hints in decoded_obj["precollected_hints"].items():
self.hints[team, slot].update(hints)
# declare slots without checks as done, as they're assumed to be spectators
@@ -351,6 +370,7 @@ class Context:
def get_save(self) -> dict:
self.recheck_hints()
d = {
"version": self.save_version,
"connect_names": self.connect_names,
"received_items": self.received_items,
"hints_used": dict(self.hints_used),
@@ -370,7 +390,22 @@ class Context:
def set_save(self, savedata: dict):
if self.connect_names != savedata["connect_names"]:
raise Exception("This savegame does not appear to match the loaded multiworld.")
self.received_items = savedata["received_items"]
if "version" not in savedata:
# upgrade from version 1
# this is not perfect but good enough for old games to continue
for old, items in savedata["received_items"].items():
self.received_items[(*old, True)] = items
self.received_items[(*old, False)] = items.copy()
for (team, slot, remote) in self.received_items:
# remove start inventory from items, since this is separate now
start_inventory = get_start_inventory(self, team, slot, slot in self.remote_start_inventory)
if start_inventory:
del self.received_items[team, slot, remote][:len(start_inventory)]
logging.info("Upgraded save data")
elif savedata["version"] > self.save_version:
raise Exception("This savegame is newer than the server.")
else:
self.received_items = savedata["received_items"]
self.hints_used.update(savedata["hints_used"])
self.hints.update(savedata["hints"])
@@ -543,7 +578,10 @@ async def on_client_joined(ctx: Context, client: Client):
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) "
f"{verb} {ctx.games[client.slot]} has joined. "
f"Client({version_str}), {client.tags}).")
ctx.notify_client(client, "Now that you are connected, "
"you can use !help to list commands to run via the server."
"If your client supports it, "
"you may have additional local commands you can list with /help.")
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
@@ -599,21 +637,29 @@ def get_status_string(ctx: Context, team: int):
return text
def get_received_items(ctx: Context, team: int, player: int) -> typing.List[NetworkItem]:
return ctx.received_items.setdefault((team, player), [])
def get_received_items(ctx: Context, team: int, player: int, remote_items: bool) -> typing.List[NetworkItem]:
return ctx.received_items.setdefault((team, player, remote_items), [])
def get_start_inventory(ctx: Context, team: int, player: int, remote_start_inventory: bool) -> typing.List[NetworkItem]:
return ctx.start_inventory.setdefault(player, []) if remote_start_inventory else []
def send_new_items(ctx: Context):
for team, clients in ctx.clients.items():
for slot, clients in clients.items():
items = get_received_items(ctx, team, slot)
for client in clients:
if len(items) > client.send_index:
if client.no_items:
continue
start_inventory = get_start_inventory(ctx, team, slot, client.remote_start_inventory)
items = get_received_items(ctx, team, slot, client.remote_items)
if len(start_inventory) + len(items) > client.send_index:
first_new_item = max(0, client.send_index - len(start_inventory))
asyncio.create_task(ctx.send_msgs(client, [{
"cmd": "ReceivedItems",
"index": client.send_index,
"items": items[client.send_index:]}]))
client.send_index = len(items)
"items": start_inventory[client.send_index:] + items[first_new_item:]}]))
client.send_index = len(start_inventory) + len(items)
def update_checked_locations(ctx: Context, team: int, slot: int):
@@ -633,13 +679,13 @@ def collect_player(ctx: Context, team: int, slot: int):
"""register any locations that are in the multidata, pointing towards this player"""
all_locations = collections.defaultdict(set)
for source_slot, location_data in ctx.locations.items():
for location_id, (item_id, target_player_id) in location_data.items():
if target_player_id == slot:
for location_id, values in location_data.items():
if values[1] == slot:
all_locations[source_slot].add(location_id)
ctx.notify_all("%s (Team #%d) has collected" % (ctx.player_names[(team, slot)], team + 1))
for source_player, location_ids in all_locations.items():
register_location_checks(ctx, team, source_player, location_ids)
register_location_checks(ctx, team, source_player, location_ids, count_activity=False)
update_checked_locations(ctx, team, source_player)
@@ -651,16 +697,25 @@ def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[int]:
return sorted(items)
def register_location_checks(ctx: Context, team: int, slot: int, locations: typing.Iterable[int]):
def register_location_checks(ctx: Context, team: int, slot: int, locations: typing.Iterable[int],
count_activity: bool = True):
new_locations = set(locations) - ctx.location_checks[team, slot]
new_locations.intersection_update(ctx.locations[slot]) # ignore location IDs unknown to this multidata
if new_locations:
ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc)
if count_activity:
ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc)
for location in new_locations:
item_id, target_player = ctx.locations[slot][location]
new_item = NetworkItem(item_id, location, slot)
if target_player != slot or slot in ctx.remote_items:
get_received_items(ctx, team, target_player).append(new_item)
if len(ctx.locations[slot][location]) == 3:
item_id, target_player, flags = ctx.locations[slot][location]
else:
# TODO: remove around version 0.2.5
item_id, target_player = ctx.locations[slot][location]
flags = 0
new_item = NetworkItem(item_id, location, slot, flags)
if target_player != slot:
get_received_items(ctx, team, target_player, False).append(new_item)
get_received_items(ctx, team, target_player, True).append(new_item)
logging.info('(Team #%d) %s sent %s to %s (%s)' % (
team + 1, ctx.player_names[(team, slot)], get_item_name_from_id(item_id),
@@ -679,32 +734,41 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
ctx.save()
def notify_team(ctx: Context, team: int, text: str):
logging.info("Notice (Team #%d): %s" % (team + 1, text))
ctx.broadcast_team(team, [['Print', {"text": text}]])
def collect_hints(ctx: Context, team: int, slot: int, item: str) -> typing.List[NetUtils.Hint]:
hints = []
seeked_item_id = proxy_worlds[ctx.games[slot]].item_name_to_id[item]
for finding_player, check_data in ctx.locations.items():
for location_id, result in check_data.items():
item_id, receiving_player = result
if len(result) == 3:
item_id, receiving_player, item_flags = result
else:
# TODO: remove around version 0.2.5
item_id, receiving_player = result
item_flags = 0
if receiving_player == slot and item_id == seeked_item_id:
found = location_id in ctx.location_checks[team, finding_player]
entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "")
hints.append(NetUtils.Hint(receiving_player, finding_player, location_id, item_id, found, entrance))
hints.append(NetUtils.Hint(receiving_player, finding_player, location_id, item_id, found, entrance,
item_flags))
return hints
def collect_hints_location(ctx: Context, team: int, slot: int, location: str) -> typing.List[NetUtils.Hint]:
seeked_location: int = proxy_worlds[ctx.games[slot]].location_name_to_id[location]
item_id, receiving_player = ctx.locations[slot].get(seeked_location, (None, None))
if item_id:
result = ctx.locations[slot].get(seeked_location, (None, None, None))
if result:
if len(result) == 3:
item_id, receiving_player, item_flags = result
else:
# TODO: remove around version 0.2.5
item_id, receiving_player = result
item_flags = 0
found = seeked_location in ctx.location_checks[team, slot]
entrance = ctx.er_hint_data.get(slot, {}).get(seeked_location, "")
return [NetUtils.Hint(receiving_player, slot, seeked_location, item_id, found, entrance)]
return [NetUtils.Hint(receiving_player, slot, seeked_location, item_id, found, entrance, item_flags)]
return []
@@ -724,10 +788,10 @@ def json_format_send_event(net_item: NetworkItem, receiving_player: int):
NetUtils.add_json_text(parts, net_item.player, type=NetUtils.JSONTypes.player_id)
if net_item.player == receiving_player:
NetUtils.add_json_text(parts, " found their ")
NetUtils.add_json_item(parts, net_item.item, net_item.player)
NetUtils.add_json_item(parts, net_item.item, net_item.player, net_item.flags)
else:
NetUtils.add_json_text(parts, " sent ")
NetUtils.add_json_item(parts, net_item.item, receiving_player)
NetUtils.add_json_item(parts, net_item.item, receiving_player, net_item.flags)
NetUtils.add_json_text(parts, " to ")
NetUtils.add_json_text(parts, receiving_player, type=NetUtils.JSONTypes.player_id)
@@ -1073,7 +1137,8 @@ class ClientMessageProcessor(CommonCommandProcessor):
world.item_names)
if usable:
new_item = NetworkItem(world.create_item(item_name).code, -1, self.client.slot)
get_received_items(self.ctx, self.client.team, self.client.slot).append(new_item)
get_received_items(self.ctx, self.client.team, self.client.slot, False).append(new_item)
get_received_items(self.ctx, self.client.team, self.client.slot, True).append(new_item)
self.ctx.notify_all(
'Cheat console: sending "' + item_name + '" to ' + self.ctx.get_aliased_name(self.client.team,
self.client.slot))
@@ -1086,7 +1151,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
self.output("Cheating is disabled.")
return False
def get_hints(self, input_text: str, explicit_location: bool = False) -> bool:
def get_hints(self, input_text: str, for_location: bool = False) -> bool:
points_available = get_client_points(self.ctx, self.client)
if not input_text:
hints = {hint.re_check(self.ctx, self.client.team) for hint in
@@ -1098,20 +1163,21 @@ class ClientMessageProcessor(CommonCommandProcessor):
return True
else:
world = proxy_worlds[self.ctx.games[self.client.slot]]
item_name, usable, response = get_intended_text(input_text,
world.all_names if not explicit_location else world.location_names)
names = world.location_names if for_location else world.all_item_and_group_names
hint_name, usable, response = get_intended_text(input_text,
names)
if usable:
if item_name in world.hint_blacklist:
self.output(f"Sorry, \"{item_name}\" is marked as non-hintable.")
if hint_name in world.hint_blacklist:
self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.")
hints = []
elif item_name in world.item_name_groups and not explicit_location:
elif not for_location and hint_name in world.item_name_groups: # item group name
hints = []
for item in world.item_name_groups[item_name]:
for item in world.item_name_groups[hint_name]:
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item))
elif item_name in world.item_names and not explicit_location: # item name
hints = collect_hints(self.ctx, self.client.team, self.client.slot, item_name)
elif not for_location and hint_name in world.item_names: # item name
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name)
else: # location name
hints = collect_hints_location(self.ctx, self.client.team, self.client.slot, item_name)
hints = collect_hints_location(self.ctx, self.client.team, self.client.slot, hint_name)
cost = self.ctx.get_hint_cost(self.client.slot)
if hints:
new_hints = set(hints) - self.ctx.hints[self.client.team, self.client.slot]
@@ -1172,19 +1238,17 @@ class ClientMessageProcessor(CommonCommandProcessor):
return False
@mark_raw
def _cmd_hint(self, item_or_location: str = "") -> bool:
"""Use !hint {item_name/location_name},
for example !hint Lamp or !hint Link's House to get a spoiler peek for that location or item.
def _cmd_hint(self, item: str = "") -> bool:
"""Use !hint {item_name},
for example !hint Lamp to get a spoiler peek for that item.
If hint costs are on, this will only give you one new result,
you can rerun the command to get more in that case."""
return self.get_hints(item_or_location)
return self.get_hints(item)
@mark_raw
def _cmd_hint_location(self, location: str = "") -> bool:
"""Use !hint_location {location_name},
for example !hint_location atomic-bomb to get a spoiler peek for that location.
(In the case of factorio, or any other game where item names and location names are identical,
this command must be used explicitly.)"""
for example !hint_location atomic-bomb to get a spoiler peek for that location."""
return self.get_hints(location, True)
@@ -1246,6 +1310,16 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
minver = ctx.minimum_client_versions[slot]
if minver > args['version']:
errors.add('IncompatibleVersion')
if args.get('items_handling', None) is None:
# fall back to load from multidata
client.no_items = False
client.remote_items = slot in ctx.remote_items
client.remote_start_inventory = slot in ctx.remote_start_inventory
else:
try:
client.items_handling = args['items_handling']
except (ValueError, TypeError):
errors.add('InvalidItemsHandling')
# only exact version match allowed
if ctx.compatibility == 0 and args['version'] != version_tuple:
@@ -1266,6 +1340,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
ctx.clients[team][slot].append(client)
client.version = args['version']
client.tags = args['tags']
client.no_locations = 'TextOnly' in client.tags or 'Tracker' in client.tags
reply = [{
"cmd": "Connected",
"team": client.team, "slot": client.slot,
@@ -1274,10 +1349,11 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
"checked_locations": get_checked_checks(ctx, team, slot),
"slot_data": ctx.slot_data[client.slot]
}]
items = get_received_items(ctx, client.team, client.slot)
if items:
reply.append({"cmd": 'ReceivedItems', "index": 0, "items": items})
client.send_index = len(items)
start_inventory = get_start_inventory(ctx, team, slot, client.remote_start_inventory)
items = get_received_items(ctx, client.team, client.slot, client.remote_items)
if (start_inventory or items) and not client.no_items:
reply.append({"cmd": 'ReceivedItems', "index": 0, "items": start_inventory + items})
client.send_index = len(start_inventory) + len(items)
if not client.auth: # if this was a Re-Connect, don't print to console
client.auth = True
await on_client_joined(ctx, client)
@@ -1305,23 +1381,42 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
"original_cmd": cmd}])
return
if args.get('items_handling', None) is not None and client.items_handling != args['items_handling']:
try:
client.items_handling = args['items_handling']
start_inventory = get_start_inventory(ctx, client.team, client.slot, client.remote_start_inventory)
items = get_received_items(ctx, client.team, client.slot, client.remote_items)
if (items or start_inventory) and not client.no_items:
client.send_index = len(start_inventory) + len(items)
await ctx.send_msgs(client, [{"cmd": "ReceivedItems", "index": 0,
"items": start_inventory + items}])
else:
client.send_index = 0
except (ValueError, TypeError) as err:
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', 'type': 'arguments',
'text': f'Invalid items_handling: {err}',
'original_cmd': cmd}])
return
if "tags" in args:
old_tags = client.tags
client.tags = args["tags"]
if set(old_tags) != set(client.tags):
client.no_locations = 'TextOnly' in client.tags or 'Tracker' in client.tags
ctx.notify_all(
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has changed tags "
f"from {old_tags} to {client.tags}.")
elif cmd == 'Sync':
items = get_received_items(ctx, client.team, client.slot)
if items:
client.send_index = len(items)
start_inventory = get_start_inventory(ctx, client.team, client.slot, client.remote_start_inventory)
items = get_received_items(ctx, client.team, client.slot, client.remote_items)
if (start_inventory or items) and not client.no_items:
client.send_index = len(start_inventory) + len(items)
await ctx.send_msgs(client, [{"cmd": "ReceivedItems", "index": 0,
"items": items}])
"items": start_inventory + items}])
elif cmd == 'LocationChecks':
if "Tracker" in client.tags:
if client.no_locations:
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "cmd",
"text": "Trackers can't register new Location Checks",
"original_cmd": cmd}])
@@ -1336,8 +1431,14 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
[{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts',
"original_cmd": cmd}])
return
target_item, target_player = ctx.locations[client.slot][location]
locs.append(NetworkItem(target_item, location, target_player))
if len(ctx.locations[client.slot][location]) == 3:
target_item, target_player, flags = ctx.locations[client.slot][location]
else:
# TODO: remove around version 0.2.5
target_item, target_player = ctx.locations[client.slot][location]
flags = 0
locs.append(NetworkItem(target_item, location, target_player, flags))
await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}])
@@ -1497,7 +1598,8 @@ class ServerCommandProcessor(CommonCommandProcessor):
item, usable, response = get_intended_text(item, world.item_names)
if usable:
new_item = NetworkItem(world.item_name_to_id[item], -1, 0)
get_received_items(self.ctx, team, slot).append(new_item)
get_received_items(self.ctx, team, slot, True).append(new_item)
get_received_items(self.ctx, team, slot, False).append(new_item)
self.ctx.notify_all('Cheat console: sending "' + item + '" to ' +
self.ctx.get_aliased_name(team, slot))
send_new_items(self.ctx)
@@ -1509,23 +1611,44 @@ class ServerCommandProcessor(CommonCommandProcessor):
self.output(response)
return False
def _cmd_hint(self, player_name: str, *item_or_location: str) -> bool:
"""Send out a hint for a player's item or location to their team"""
def _cmd_hint(self, player_name: str, *item: str) -> bool:
"""Send out a hint for a player's item to their team"""
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
if usable:
team, slot = self.ctx.player_name_lookup[seeked_player]
item = " ".join(item_or_location)
item = " ".join(item)
world = proxy_worlds[self.ctx.games[slot]]
item, usable, response = get_intended_text(item, world.all_names)
item, usable, response = get_intended_text(item, world.all_item_and_group_names)
if usable:
if item in world.item_name_groups:
hints = []
for item in world.item_name_groups[item]:
hints.extend(collect_hints(self.ctx, team, slot, item))
elif item in world.item_names: # item name
else: # item name
hints = collect_hints(self.ctx, team, slot, item)
else: # location name
hints = collect_hints_location(self.ctx, team, slot, item)
if hints:
notify_hints(self.ctx, team, hints)
else:
self.output("No hints found.")
return True
else:
self.output(response)
return False
else:
self.output(response)
return False
def _cmd_hint_location(self, player_name: str, *location: str) -> bool:
"""Send out a hint for a player's location to their team"""
seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values())
if usable:
team, slot = self.ctx.player_name_lookup[seeked_player]
item = " ".join(location)
world = proxy_worlds[self.ctx.games[slot]]
item, usable, response = get_intended_text(item, world.location_names)
if usable:
hints = collect_hints_location(self.ctx, team, slot, item)
if hints:
notify_hints(self.ctx, team, hints)
else:

View File

@@ -17,6 +17,8 @@ class JSONMessagePart(typing.TypedDict, total=False):
color: str
# owning player for location/item
player: int
# if type == item indicates item flags
flags: int
class ClientStatus(enum.IntEnum):
@@ -57,6 +59,7 @@ class NetworkItem(typing.NamedTuple):
item: int
location: int
player: int
flags: int = 0
def _scan_for_TypedTuples(obj: typing.Any) -> typing.Any:
@@ -163,6 +166,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
@@ -176,7 +194,7 @@ class JSONtoTextParser(metaclass=HandlerMeta):
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):
@@ -194,7 +212,17 @@ class JSONtoTextParser(metaclass=HandlerMeta):
return self._handle_color(node)
def _handle_item_name(self, node: JSONMessagePart):
node["color"] = 'cyan'
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):
@@ -238,8 +266,8 @@ 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, **kwargs) -> None:
parts.append({"text": str(item_id), "player": player, "type": JSONTypes.item_id, **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:
@@ -253,13 +281,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):
@@ -270,7 +300,7 @@ 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_item(parts, self.item, self.receiving_player)
add_json_item(parts, self.item, self.receiving_player, self.item_flags)
add_json_text(parts, " is at ")
add_json_location(parts, self.location, self.finding_player)
add_json_text(parts, " in ")
@@ -288,7 +318,7 @@ class Hint(typing.NamedTuple):
return {"cmd": "PrintJSON", "data": parts, "type": "Hint",
"receiving": self.receiving_player,
"item": NetworkItem(self.item, self.location, self.finding_player),
"item": NetworkItem(self.item, self.location, self.finding_player, self.item_flags),
"found": self.found}
@property

View File

@@ -210,6 +210,23 @@ class Range(Option, int):
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:
randomrange = [int(textsplit[len(textsplit)-2]), int(textsplit[len(textsplit)-1])]
except ValueError:
raise ValueError(f"Invalid random range {text} for option {cls.__name__}")
randomrange.sort()
if randomrange[0] < cls.range_start or randomrange[1] > cls.range_end:
raise Exception(f"{randomrange[0]}-{randomrange[1]} is outside allowed range {cls.range_start}-{cls.range_end} for option {cls.__name__}")
if text.startswith("random-range-low"):
return cls(int(round(random.triangular(randomrange[0], randomrange[1], randomrange[0]))))
elif text.startswith("random-range-middle"):
return cls(int(round(random.triangular(randomrange[0], randomrange[1]))))
elif text.startswith("random-range-high"):
return cls(int(round(random.triangular(randomrange[0], randomrange[1], randomrange[1]))))
else:
return cls(int(round(random.randint(randomrange[0], randomrange[1]))))
else:
return cls(random.randint(cls.range_start, cls.range_end))
return cls(int(text))
@@ -244,7 +261,22 @@ class OptionNameSet(Option):
return cls.from_text(str(data))
class OptionDict(Option):
class VerifyKeys:
valid_keys = frozenset()
valid_keys_casefold: bool = False
@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}.")
class OptionDict(Option, VerifyKeys):
default = {}
supports_weighting = False
value: typing.Dict[str, typing.Any]
@@ -255,6 +287,7 @@ class OptionDict(Option):
@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)}")
@@ -276,7 +309,7 @@ class ItemDict(OptionDict):
super(ItemDict, self).__init__(value)
class OptionList(Option):
class OptionList(Option, VerifyKeys):
default = []
supports_weighting = False
value: list
@@ -292,6 +325,7 @@ class OptionList(Option):
@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))

View File

@@ -1,5 +1,6 @@
# TODO: convert this into a system like AutoWorld
import shutil
import bsdiff4
import yaml
import os
@@ -146,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)

View File

@@ -15,6 +15,9 @@ Currently, the following games are supported:
* 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/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled

View File

@@ -11,6 +11,7 @@ import shutil
import logging
import asyncio
from json import loads, dumps
from tkinter import font
from Utils import get_item_name_from_id, init_logging
@@ -101,6 +102,7 @@ class LttPCommandProcessor(ClientCommandProcessor):
class Context(CommonContext):
command_processor = LttPCommandProcessor
game = "A Link to the Past"
items_handling = None # set in game_watcher
def __init__(self, snes_address, server_address, password):
super(Context, self).__init__(server_address, password)
@@ -171,15 +173,24 @@ async def deathlink_kill_player(ctx: Context):
while ctx.death_state == DeathState.killing_player and \
ctx.snes_state == SNESState.SNES_ATTACHED:
if ctx.game == GAME_ALTTP:
snes_buffered_write(ctx, WRAM_START + 0xF36D, bytes([0])) # set current health to 0
snes_buffered_write(ctx, WRAM_START + 0x0373, bytes([8])) # deal 1 full heart of damage at next opportunity
invincible = await snes_read(ctx, WRAM_START + 0x037B, 1)
last_health = await snes_read(ctx, WRAM_START + 0xF36D, 1)
await asyncio.sleep(0.25)
health = await snes_read(ctx, WRAM_START + 0xF36D, 1)
if not invincible or not last_health or not health:
ctx.death_state = DeathState.dead
ctx.last_death_link = time.time()
continue
if not invincible[0] and last_health[0] == health[0]:
snes_buffered_write(ctx, WRAM_START + 0xF36D, bytes([0])) # set current health to 0
snes_buffered_write(ctx, WRAM_START + 0x0373, bytes([8])) # deal 1 full heart of damage at next opportunity
elif ctx.game == GAME_SM:
snes_buffered_write(ctx, WRAM_START + 0x09C2, bytes([0, 0])) # set current health to 0
if not ctx.death_link_allow_survive:
snes_buffered_write(ctx, WRAM_START + 0x09D6, bytes([0, 0])) # set current reserve to 0
await snes_flush_writes(ctx)
await asyncio.sleep(1)
gamemode = None
if ctx.game == GAME_ALTTP:
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
if not gamemode or gamemode[0] in DEATH_MODES:
@@ -194,14 +205,6 @@ async def deathlink_kill_player(ctx: Context):
ctx.last_death_link = time.time()
def color_item(item_id: int, green: bool = False) -> str:
item_name = get_item_name_from_id(item_id)
item_colors = ['green' if green else 'cyan']
if item_name in Items.progression_items:
item_colors.append("white_bg")
return color(item_name, *item_colors)
SNES_RECONNECT_DELAY = 5
# LttP
@@ -900,8 +903,10 @@ async def game_watcher(ctx: Context):
continue
elif game_name == b"SM":
ctx.game = GAME_SM
ctx.items_handling = 0b001 # full local
else:
ctx.game = GAME_ALTTP
ctx.items_handling = 0b001 # full local
rom = await snes_read(ctx, SM_ROMNAME_START if ctx.game == GAME_SM else ROMNAME_START, ROMNAME_SIZE)
if rom is None or rom == bytes([0] * ROMNAME_SIZE):
@@ -1087,13 +1092,7 @@ async def main():
time.sleep(3)
sys.exit()
elif args.diff_file.endswith((".apbp", "apz3")):
adjustedromfile, adjusted = Utils.get_adjuster_settings(romfile, gui_enabled)
if adjusted:
try:
shutil.move(adjustedromfile, romfile)
adjustedromfile = romfile
except Exception as e:
logging.exception(e)
adjustedromfile, adjusted = get_alttp_settings(romfile)
asyncio.create_task(run_game(adjustedromfile if adjusted else romfile))
else:
asyncio.create_task(run_game(romfile))
@@ -1131,6 +1130,126 @@ async def main():
if input_task:
input_task.cancel()
def get_alttp_settings(romfile: str):
lastSettings = Utils.get_adjuster_settings(GAME_ALTTP)
adjusted = False
adjustedromfile = ''
if lastSettings:
choice = 'no'
if not hasattr(lastSettings, 'auto_apply') or 'ask' in lastSettings.auto_apply:
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(lastSettings).items() if name in whitelist}
if hasattr(lastSettings, "sprite_pool"):
sprite_pool = {}
for sprite in lastSettings.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
import pprint
if gui_enabled:
from tkinter import Tk, PhotoImage, Label, LabelFrame, Frame, Button
applyPromptWindow = Tk()
applyPromptWindow.resizable(False, False)
applyPromptWindow.protocol('WM_DELETE_WINDOW',lambda: onButtonClick())
logo = PhotoImage(file=Utils.local_path('data', 'icon.png'))
applyPromptWindow.tk.call('wm', 'iconphoto', applyPromptWindow._w, logo)
applyPromptWindow.wm_title("Last adjuster settings LttP")
label = LabelFrame(applyPromptWindow,
text='Last used adjuster settings were found. Would you like to apply these?')
label.grid(column=0,row=0, padx=5, pady=5, ipadx=5, ipady=5)
label.grid_columnconfigure (0, weight=1)
label.grid_columnconfigure (1, weight=1)
label.grid_columnconfigure (2, weight=1)
label.grid_columnconfigure (3, weight=1)
def onButtonClick(answer: str='no'):
setattr(onButtonClick, 'choice', answer)
applyPromptWindow.destroy()
framedOptions = Frame(label)
framedOptions.grid(column=0, columnspan=4,row=0)
framedOptions.grid_columnconfigure(0, weight=1)
framedOptions.grid_columnconfigure(1, weight=1)
framedOptions.grid_columnconfigure(2, weight=1)
curRow = 0
curCol = 0
for name, value in printed_options.items():
Label(framedOptions, text=name+": "+str(value)).grid(column=curCol, row=curRow, padx=5)
if(curCol==2):
curRow+=1
curCol=0
else:
curCol+=1
yesButton = Button(label, text='Yes', command=lambda: onButtonClick('yes'), width=10)
yesButton.grid(column=0, row=1)
noButton = Button(label, text='No', command=lambda: onButtonClick('no'), width=10)
noButton.grid(column=1, row=1)
alwaysButton = Button(label, text='Always', command=lambda: onButtonClick('always'), width=10)
alwaysButton.grid(column=2, row=1)
neverButton = Button(label, text='Never', command=lambda: onButtonClick('never'), width=10)
neverButton.grid(column=3, row=1)
Utils.tkinter_center_window(applyPromptWindow)
applyPromptWindow.mainloop()
choice = getattr(onButtonClick, 'choice')
else:
choice = 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 choice and choice.startswith("y"):
choice = 'yes'
elif choice and "never" in choice:
choice = 'no'
lastSettings.auto_apply = 'never'
Utils.persistent_store("adjuster", GAME_ALTTP, lastSettings)
elif choice and "always" in choice:
choice = 'yes'
lastSettings.auto_apply = 'always'
Utils.persistent_store("adjuster", GAME_ALTTP, lastSettings)
else:
choice = 'no'
elif 'never' in lastSettings.auto_apply:
choice = 'no'
elif 'always' in lastSettings.auto_apply:
choice = 'yes'
if 'yes' in choice:
from worlds.alttp.Rom import get_base_rom_path
lastSettings.rom = romfile
lastSettings.baserom = get_base_rom_path()
lastSettings.world = None
if hasattr(lastSettings, "sprite_pool"):
from LttPAdjuster import AdjusterWorld
lastSettings.world = AdjusterWorld(getattr(lastSettings, "sprite_pool"))
adjusted = True
import LttPAdjuster
_, adjustedromfile = LttPAdjuster.adjust(lastSettings)
if hasattr(lastSettings, "world"):
delattr(lastSettings, "world")
else:
adjusted = False;
if adjusted:
try:
shutil.move(adjustedromfile, romfile)
adjustedromfile = romfile
except Exception as e:
logging.exception(e)
else:
adjusted = False
return adjustedromfile, adjusted
if __name__ == '__main__':
colorama.init()

View File

@@ -11,6 +11,7 @@ import io
import collections
import importlib
import logging
from tkinter import Tk
def tuplize_version(version: str) -> Version:
@@ -23,10 +24,10 @@ class Version(typing.NamedTuple):
build: int
__version__ = "0.2.3"
__version__ = "0.2.4"
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
@@ -117,7 +118,20 @@ def open_file(filename):
subprocess.call([open_command, filename])
parse_yaml = safe_load
# 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 = functools.partial(load, Loader=UniqueKeyLoader)
unsafe_parse_yaml = functools.partial(load, Loader=Loader)
@@ -301,63 +315,9 @@ def persistent_load() -> typing.Dict[dict]:
return storage
def get_adjuster_settings(romfile: str, skip_questions: bool = False) -> 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
from worlds.alttp.Rom import get_base_rom_path
adjuster_settings.rom = romfile
adjuster_settings.baserom = get_base_rom_path()
adjuster_settings.world = None
whitelist = {"music", "menuspeed", "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
elif skip_questions:
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
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
@@ -474,3 +434,15 @@ def stream_input(stream, queue):
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

@@ -136,8 +136,7 @@ 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>')

View File

@@ -1,28 +1,33 @@
"""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')
@@ -39,3 +44,6 @@ def get_datapackge_versions():
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

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

@@ -1,3 +1,4 @@
import logging
import os
from Utils import __version__
from jinja2 import Template
@@ -9,6 +10,9 @@ 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)
@@ -59,7 +63,10 @@ def create():
game_options = {}
for option_name, option in all_options.items():
if option.options:
if option_name in handled_in_js:
pass
elif option.options:
game_options[option_name] = this_option = {
"type": "select",
"displayName": option.displayname if hasattr(option, "displayname") else option_name,
@@ -92,6 +99,32 @@ def create():
"max": option.range_end,
}
elif getattr(option, "verify_item_name", False):
game_options[option_name] = {
"type": "items-list",
"displayName": option.displayname if hasattr(option, "displayname") 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.displayname if hasattr(option, "displayname") 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.displayname if hasattr(option, "displayname") 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)

View File

@@ -3,4 +3,4 @@ pony>=0.7.14
waitress>=2.0.0
flask-caching>=1.10.1
Flask-Compress>=1.10.1
Flask-Limiter>=1.4
Flask-Limiter>=2.1

View File

@@ -1,52 +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.
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.
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.
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.
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.
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.
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](https://discord.gg/8Z65BR2). There are always people ready to answer any questions
you might have.
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](https://github.com/ArchipelagoMW/Archipelago).
There, you will find examples of games in the [worlds](https://github.com/ArchipelagoMW/Archipelago/tree/main/worlds)
folder, as well as some [documentation](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs) on our
network interfaces.
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,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

@@ -1,26 +1,32 @@
# A Link to the Past
## 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.
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.
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.
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

@@ -1,29 +1,34 @@
# Factorio
## 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.
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.
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.
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.
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

@@ -1,21 +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 [here](https://finalfantasyrandomizer.com/)
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 our
[main randomizer site](https://finalfantasyrandomizer.com/)
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.
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.
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

@@ -1,22 +1,27 @@
# Minecraft
## 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.
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.
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.
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

@@ -1,26 +1,32 @@
# Ocarina of Time
## 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.
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.
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.
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 an Ocarina of Time.
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

@@ -1,17 +1,21 @@
# Risk of Rain 2
## 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.
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.
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)
@@ -23,14 +27,17 @@ The Risk of Rain items are:
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.)
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.
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.
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

@@ -1,22 +1,27 @@
# Rogue Legacy (PC)
## 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.
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.
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.
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.
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
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

@@ -1,29 +1,35 @@
# Secret of Evermore
## Where is the settings page?
The player settings page for this game is located <a href="../player-settings">here</a>. It contains all options
necessary to configure and export a config file.
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 [guide](https://github.com/black-sliver/evermizer/blob/master/guide.md).
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.
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

@@ -1,27 +1,34 @@
# Subnautica
## 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.
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.
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

@@ -1,25 +1,31 @@
# Super Metroid
## 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.
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.
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.
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.
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

@@ -1,28 +1,38 @@
# Timespinner
## 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.
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, therefor you can no longer craft them at the alchemist
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 familiers can be shuffled, and all locations in the game which could
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.
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](https://timespinnerwiki.com/Use_Items), Elemental Beads have no use in the randomizer
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
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.

View File

@@ -16,11 +16,9 @@ window.addEventListener('load', () => {
}
if (settingHash !== md5(results[0])) {
const userMessage = document.getElementById('user-message');
userMessage.innerText = "Your settings are out of date! Click here to update them! Be aware this will reset " +
"them all to default.";
userMessage.style.display = "block";
userMessage.addEventListener('click', resetSettings);
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
@@ -191,7 +189,9 @@ const updateGameSetting = (event) => {
const exportSettings = () => {
const settings = JSON.parse(localStorage.getItem(gameName));
if (!settings.name || settings.name.trim().length === 0) { settings.name = "noname"; }
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);
};
@@ -208,21 +208,41 @@ const download = (filename, text) => {
};
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: localStorage.getItem(gameName) },
presetData: { player: localStorage.getItem(gameName) },
weights: { player: settings },
presetData: { player: settings },
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.';
let userMessage = 'Something went wrong and your game could not be generated.';
if (error.response.data.text) {
userMessage.innerText += ' ' + error.response.data.text;
userMessage += ' ' + error.response.data.text;
}
userMessage.classList.add('visible');
window.scrollTo(0, 0);
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

@@ -23,6 +23,7 @@ window.addEventListener('load', () => {
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

@@ -2,31 +2,36 @@
## Required Software
- [Subnautica](https://store.steampowered.com/app/264710/Subnautica/)
- [QModManager4](https://www.nexusmods.com/subnautica/mods/201)
- [Archipelago Mod for Subnautica](https://github.com/Berserker66/ArchipelagoSubnauticaModSrc/releases)
- 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.
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.
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.
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.
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 succesfully authenticated you can now create a new savegame or resume the correct savegame.
4. Hit Connect. If it says successfully authenticated you can now create a new savegame or resume the correct savegame.

View File

@@ -1,23 +1,38 @@
# Advanced Game Options Guide
# 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.
The Archipelago system generates games using player configuration files as input. Generally these are going to be
YAML files and each player will have one of these containing their custom settings for the randomized game they want to play.
On the website when you customize your settings from one of the game player settings pages which you can reach from the
[supported games page](/games). Clicking on the export settings button at the bottom will provide you with a pre-filled out
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.
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 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](https://archipelago.gg/games)
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 <span data-tooltip="Allegedly.">human-readable</span> markup config files. The basic syntax
of a yaml file will have `root` and then different levels of `nested` text that the generator "reads" in order to determine
your settings. To nest text, the correct syntax is **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](https://www.sublimetext.com/).
This program out of the box supports the correct formatting for the YAML file, so you will be able to tab and get proper
highlighting for any potential errors made while editing the file. If using any other text editor such as Notepad or
Notepad++ whenever you move to nest an option that it is done with two spaces and not tabs.
Typical YAML format will look as follows:
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:
@@ -28,67 +43,101 @@ root_option:
option_two_setting_two: 43
```
In Archipelago YAML options are always written out in full lowercase with underscores separating any words. The numbers
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.
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.
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.
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.
* `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
about this [here](/tutorial/archipelago/triggers/en).
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 in the format
`GameName`. 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.
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](/tutorial/archipelago/plando/en).
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.
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.
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.
* `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.
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.
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
@@ -105,6 +154,10 @@ 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
@@ -132,29 +185,39 @@ triggers:
```
#### 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.
* `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.
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.
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.
* `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.
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,95 @@
### 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.
- `/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

@@ -1,85 +1,118 @@
# 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.
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).
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`.
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`.
* If you are not the one doing the generation or even if you are you can add to the `requires` section of your yaml so
that it 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. **Note that there's a very good chance that
cross-game plando could very well be broken i.e. placing on of your items in someone else's world playing a different game.**
* The options for item plando are `from_pool`, `world`, `percentage`, `force`, 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, or null. 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.
* `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.
* `locations` is a list of possible locations those items can be placed in.
* Using the multi placement method, placements are picked randomly.
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
* [A Link to the Past](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Items.py#L52)
* [Factorio Non-Progressive](https://wiki.factorio.com/Technologies) Note that these use the *internal names*. For example, `advanced-electronics`
* [Factorio Non-Progressive](https://wiki.factorio.com/Technologies) Note that these use the *internal names*. For
example, `advanced-electronics`
* [Factorio Progressive](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/factorio/Technologies.py#L374)
* [Final Fantasy 1](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/ff1/data/items.json)
* [Minecraft](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/minecraft/Items.py#L14)
* [Ocarina of Time](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/oot/Items.py#L61)
* [Risk of Rain 2](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/ror2/Items.py#L8)
* [Rogue Legacy](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/rogue-legacy/Names/ItemName.py)
* [Slay the Spire](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/spire/Items.py#L13)
* [Subnautica](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/subnautica/items.json)
* [Super Metroid](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/sm/variaRandomizer/rando/Items.py#L37) Look for "Name="
* [Timespinner](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/timespinner/Items.py#L11)
### Available Locations
* [A Link to the Past](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Regions.py#L429)
* [Factorio](https://wiki.factorio.com/Technologies) Same as items
* [Final Fantasy 1](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/ff1/data/locations.json)
* [Minecraft](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/minecraft/Locations.py#L18)
* [Ocarina of Time](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/oot/LocationList.py#L38)
* [Risk of Rain 2](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/ror2/Locations.py#L17) This is a special
case. The locations are "ItemPickup[number]" up to the maximum set in the yaml.
* [Risk of Rain 2](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/ror2/Locations.py#L17) This is a
special case. The locations are "ItemPickup[number]" up to the maximum set in the yaml.
* [Rogue Legacy](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/rogue-legacy/Names/LocationName.py)
* [Slay the Spire](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/spire/Locations.py)
* [Subnautica](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/subnautica/locations.json)
* [Super Metroid](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/sm/variaRandomizer/graph/location.py#L132)
* [Timespinner](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/timespinner/Locations.py#L13)
A list of all available items and locations can also be found in the [server's datapackage](/api/datapackage).
### Examples
```yaml
plando_items:
# example block 1 - Timespinner
# example block 1 - Timespinner
- item:
Empire Orb: 1
Radiant Orb: 1
location: Starter chest 1
location: Starter Chest 1
from_pool: true
world: true
percentage: 50
# example block 2 - Ocarina of Time
# example block 2 - Ocarina of Time
- items:
Kokiri Sword: 1
Biggoron Sword: 1
@@ -98,51 +131,83 @@ plando_items:
- Shadow Temple Hover Boots Chest
- Spirit Temple Silver Gauntlets Chest
world: false
# example block 3 - Slay the Spire
# example block 3 - Slay the Spire
- items:
Boss Relic: 3
locations:
Boss Relic 1
Boss Relic 2
Boss Relic 3
- Boss Relic 1
- Boss Relic 2
- Boss Relic 3
# example block 4 - Factorio
# 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
- 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 occuring and when it does will place all but 1 of the items randomly among the four
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
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.
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.
* 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)
@@ -150,9 +215,10 @@ plando can be found in its plando guide.
[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
# example block 1 - Link to the Past
- entrance: Cave Shop (Lake Hylia)
exit: Cave 45
direction: entrance
@@ -162,8 +228,8 @@ plando_connections:
- entrance: Agahnims Tower
exit: Old Man Cave Exit (West)
direction: exit
# example block 2 - Minecraft
# example block 2 - Minecraft
- entrance: Overworld Structure 1
exit: Nether Fortress
direction: both
@@ -173,8 +239,8 @@ plando_connections:
```
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.
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

@@ -1,79 +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 [here](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 optional but 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 but this also requires you to port forward. 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 you use 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 relevant tutorial.
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
### Creating a YAML
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, but if each player is planning on
playing their own game then they will each need a YAML. These YAML files can be generated by going to the relevant game's
player settings page, entering the name they want to use for the game, setting the options to what they would like to
play with and then clicking on the export settings button. This will then download a YAML file that will contain all of
these options and this can then be given to whoever is going to generate the game.
### What is 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.
A YAML is a file which contains human readable markup. In other words, this is a settings file kind of like an INI file or a TOML file.
Each player can go to the game's player settings page in order to determine the settings how they want them and then download a YAML file containing these settings.
After getting the YAML files of each participant for your multiworld game, these can all either be placed together in the
`Archipelago\Players` folder or compressed into a ZIP folder to then be uploaded to the [website generator](/generate).
If rolling locally ensure that the folder is clear of any files you do not wish to include in the game such as the
included default player settings files.
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 base `Archipelago` folder. The settings chosen here are baked into
the serverdata 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.
### Rolling the seed
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.
#### On the Website
After gathering the YAML files together in one location, select all of the files and compress them into a .zip folder.
Next go to the [Start Playing](/start-playing) page and click on `generate a randomized game` to reach the website generator.
Here, you can adjust some server settings such as forfeit rules and the cost for a player to use a hint before generation.
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. 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.
To begin playing, click on `Create New Room`, which will take you to the room page. From here you can navigate back to thse
Seed Info page or to the Tracker page. Sharing the link to this page with your friends will provide them with the
necessary info and files for them to connect to the multiworld.
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.
#### Rolling using the generation program
After gathering the YAML files together in the `Archipelago\Players` folder, run the program `ArchipelagoGenerate.exe`
in the base `Archipelago` folder. This will then open a console window and either silently close itself or spit out an
error. If you receive an error, it is likely due to an error in the YAML file. If the error is unhelpful in figuring
out the issue asking in the ***#tech-support*** channel of our Discord for help with finding it is highly recommended.
The generator will put a zip folder into your `Archipelago\output` folder with the format `AP_XXXXXXXXX`.zip.
This contains the patch files and relevant mods for the players as well as the serverdata for the host.
## Hosting an Archipelago Server
## Hosting a multiworld
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)
### Uploading the seed to 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 [upload page](/uploads). This will give a page with the seed info and have a
link to the spoiler if it exists. Click on Create New room and then share the link for the room with 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.
### Hosting a seed locally
For this we'll assume you have already port forwarding `38281` and have generated a seed that is still in the `outputs`
folder. Next, you'll want to run `ArchipelagoServer.exe`. A window will open in order to open the multiworld data for the
game. You can either use the generated zip folder or extract the .archipelago file and use it. If everything worked correctly the console window should tell you it's now hosting a game with the IP, port, and password that clients will need in order to connect.
Extract the patch and mod files then send those to your friends, and you're done!
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

@@ -1,37 +1,52 @@
# 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 certain options or even a variety of
settings to occur or "trigger" under certain 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](https://github.com/alwaysintreble/Archipelago-yaml-dump/blob/main/Snippets/Mercenary%20Mode%20Snippet.yaml)
that was created using entirely triggers and plando. For more information on plando you can reference
[this guide](/tutorial/archipelago/plando/en) or [this guide](/tutorial/zelda3/plando/en).
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 have to be defined in the root of the yaml file meaning it must be outside of a game section.
The best place to do this is the bottom of the yaml.
- Triggers comprise of 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 where the definition of what will happen.
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.
- 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 except for `game`.
- 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.
- Example:
- 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
@@ -41,7 +56,9 @@ This format must be:
```
### Examples
The above examples all together will end up looking like this:
```yaml
triggers:
- option_category: A Link to the Past
@@ -53,10 +70,11 @@ The above examples all together will end up looking like this:
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 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
@@ -66,4 +84,46 @@ For example:
Timespinner:
Inverted: true
```
In this example if your world happens to roll SpecificKeycards then your game will also start in inverted.
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.

View File

@@ -1,37 +1,47 @@
# Factorio Randomizer Setup Guide
## Required Software
##### Players
- [Factorio](https://factorio.com) - Needed by Players and Hosts
- Factorio: [Factorio Official Website](https://factorio.com)
- Needed by Players and Hosts
##### Server Hosts
- [Factorio](https://factorio.com) - Needed by Players and Hosts
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) - Needed by Hosts
- 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.
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](/games/Factorio/player-settings) page on the website allows you to configure
your personal settings and export a config file from them.
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](/mysterycheck) page.
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.
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\YourName\AppData\Roaming\Factorio\mods`
3. Get the server address from the person hosting the game you are joining
`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"
@@ -41,17 +51,20 @@ people to play in a single world, all contributing to the completion of the seed
## 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.
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.
- **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
@@ -59,70 +72,82 @@ Factorio Clients may connect to this server.
- A running modded Factorio Client
### Dedicated Server Setup
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](https://factorio.com/download).
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](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
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).<br />
<img src="/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.
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"
executable: C:\\factorio\\bin\\x64\\factorio"
```
With all that complete, you are now able to...
This allows you to host your own Factorio game.
## Host Your Own Factorio Game
## Hosting Your Own Factorio 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`.
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.
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"
<br /><img src="/static/assets/tutorial/factorio/connect-to-ap-server.png" />
![Factorio Client for Archipelago Connection Command](/static/assets/tutorial/factorio/connect-to-ap-server.png)
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"
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`.
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`.
## Allowing Other People to Join Your Game
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 [this website](https://whatismyip.com/).
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.
"Connecting to Someone Else's Factorio Game" above.
## Troubleshooting
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.
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](https://docs.google.com/document/d/1yZPAaXB-QcetD8FJsmsFrenAHO5V6Y2ctMAyIoT9jS4)
- [Factorio Speedrun Guide](https://www.youtube.com/watch?v=ExLrmK1c7tA)
- [Factorio Wiki](https://wiki.factorio.com/)
- 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

@@ -1,43 +1,35 @@
# Final Fantasy 1 (NES) Multiworld Setup Guide
## Required Software
- The FF1Client which is bundled with [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
- The [BizHawk](http://tasvideos.org/BizHawk.html) emulator. Versions 2.3.1 and higher are supported.
Version 2.7 is recommended
- Your 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.
- 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](https://github.com/ArchipelagoMW/Archipelago/releases)
1. On Windows, download Setup.Archipelago.<HighestVersion>.exe and run it
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**.
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**.
## Playing a Multiworld
Playing a multiworld on Archipelago.gg has 3 key components:
1. The Server which is hosting a game for all players.
2. The Client Program. For Final Fantasy 1, it is a standalone program but other randomizers may build it in.
3. The Game itself, in this case running on Bizhawk, which then connects to the Client running on your computer.
## Obtaining your Archipelago yaml file and randomized ROM
To set this up the following steps are required:
1. (Each Player) Generate your own yaml file and randomized ROM
2. (Host Only) Generate a randomized game with you and 0 or more players using Archipelago
3. (Host Only) Run the Archipelago Server
4. (Each Player) Run your client program and connect it to the Server
5. (Each Player) Run your game and connect it to your client program
6. (Each Player) Play the game and have fun!
Unlike most other Archipelago.gg games Final Fantasy 1 is randomized by the main randomizer at
the [Final Fantasy Randomizer Homepage](https://finalfantasyrandomizer.com/).
### Obtaining your Archipelago yaml file and randomized ROM
Unlike most other Archipelago.gg games Final Fantasy 1 is randomized by the
[main randomizer](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.
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
@@ -46,69 +38,37 @@ you prefer, or it is your first time playing select the "Archipelago" preset on
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
### Generating the Multiworld and Starting the Server
The game can be generated locally or 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).
#### Generating on Archipelago.gg (Recommended)
1. Gather all yaml files
2. Create a zip file containing all of the yaml files. Make sure it is a `*.zip` not a `*.7z` or a `*.rar`
3. Navigate to the [Generate Page](https://archipelago.gg/generate) and click `Upload File`
1. For your first game keep `Forfeit Permission` as `Automatic on goal completion`. Forfeiting actually means
giving out all of the items remaining in your game in this case so you do not block anyone else.
2. For your first game keep `Hint Cost` at 10%
4. Select your zip file
## Running the Client Program and Connecting to the Server
#### Generating Locally
1. Navigate to your Archipelago install directory
2. Empty the `Players` directory then fill it with one yaml per player including your own which you got from the
finalfantasyrandomizer website above
3. Run `ArchipelagoGenerate.exe` (double-click it in File Explorer)
4. You will find your generated game in the `output` directory
Once the Archipelago server has been hosted:
#### Starting the server
If you generated on Archipelago.gg click `Create New Room` on the results page to start your server
If you generated locally simply navigate to the [Host Game Page](https://archipelago.gg/uploads) and upload the file
in the `output` directory
### Running the Client Program and Connecting to the Server
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)
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`
### 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**
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
### 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`
### Helpful Commands
Commands are broken into two types: `/` and `!` commands. The difference is that `/commands` are local to your machine
and game whereas `!` commands ask the server. Most of the time you can use local commands.
### Other Client Commands
#### Local Commands
- `/connect <address with port number>` connect to the multiworld server
- `/disconnect` if you accidentally connected to the wrong port run this to disconnect and then reconnect using
- `/nes` Shows the current status of the NES connection
- `/received` Displays all the items you have found or been sent
- `/missing` Displays all the locations along with their current status (checked/missing)
- Just typing anything will broadcast a message to all players
#### Remote Commands
- `!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 you allowed forfeiting prior to goal completion. Remember that
"forfeiting" actually means sending out your remaining items in your world
#### Host only (on Archipelago.gg)
`/forfeit <Player Name>` Forfeits someone regardless of settings and game completion status
All other commands may be found on the [Archipelago Server and Client Commands Guide](/tutorial/archipelago/commands/en)
.

View File

@@ -2,57 +2,60 @@
## Required Software
- [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-edition) (update 1.17.1)
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) (select `Minecraft Client` during installation.)
- 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?
you can customize your settings by visiting the [minecraft player settings](/games/Minecraft/player-settings)
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.
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.
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
Using minecraft 1.17.1 connect to the server `localhost`.
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.
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 all set. Congratulations
on successfully joining a multiworld game! At this point any additional minecraft players may connect to your
forge server. to star the game once everyone is ready type `/start`.
### Useful commands
- `!help` displays a list all server commands
- `!hint` will display how many hint points you have, along with any hints that have been given that are related to your game.
- `!hint (item)` will ask the server to tell you where (item) is
- `!hint_location (location)` will ask the server to tell you what item is on (location)
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. but 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](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.17.1.html)
- [Minecraft Archipelago Randomizer Mod](https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases)
**DO NOT INSTALL THIS ON YOUR CLIENT**
- [Java 16](https://docs.aws.amazon.com/corretto/latest/corretto-16-ug/downloads-list.html)
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,7 +1,9 @@
# 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`
# 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
@@ -10,13 +12,15 @@
## 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
description: Basic Minecraft Yaml
# Tu nombre en el juego. Espacios seran sustituidos por guinoes bajos y
@@ -31,52 +35,52 @@ progression_balancing: on
Minecraft:
# Numero de logros requeridos (87 max) para que aparezca el Ender Dragon y completar el juego.
advancement_goal: 50
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
egg_shards_required: 10
# Numero de huevos disponibles en la partida (30 max).
egg_shards_available: 15
egg_shards_available: 15
# Modifica el nivel de objetos logicamente requeridos para
# explorar areas peligrosas y luchar contra jefes.
combat_difficulty:
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:
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:
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:
include_postgame_advancements:
on: 0
off: 1
# Permite el mezclado de villas, puesto, fortalezas, bastiones y ciudades de END.
shuffle_structures:
shuffle_structures:
on: 0
off: 1
# Añade brujulas de estructura al juego,
# apuntaran a la estructura correspondiente mas cercana.
structure_compasses:
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:
bee_traps:
0: 1
25: 0
50: 0
@@ -87,44 +91,57 @@ Minecraft:
## 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`.
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> (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.
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
### 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**
**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.
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.
- 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.
- 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`

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

@@ -2,29 +2,34 @@
## Required Software
- [Rogue Legacy Randomizer](https://github.com/ThePhar/RogueLegacyRandomizer/releases)
- 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.
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 <a href="/games/Rogue Legacy/player-settings">rogue legacy settings page here</a>.
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!
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:
@@ -41,7 +46,7 @@ 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.
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

@@ -1,39 +1,46 @@
# Risk of Rain 2 Setup Guide
## Install using r2modman
### Install r2modman
Head on over to the r2modman page on Thunderstore and follow the installation instructions.
[https://thunderstore.io/package/ebkr/r2modman/](https://thunderstore.io/package/ebkr/r2modman/)
[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.
[https://thunderstore.io/package/ArchipelagoMW/Archipelago/](https://thunderstore.io/package/ArchipelagoMW/Archipelago/)
[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.
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.
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.
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.
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
@@ -82,10 +89,11 @@ Risk of Rain 2:
| 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)
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 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)
@@ -95,4 +103,5 @@ 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)
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

@@ -1,59 +1,69 @@
# Secret of Evermore Setup Guide
## Required Software
- [SNI](https://github.com/alttpo/sni/releases) v0.0.59 or newer (included in Archipelago 0.2.1 setup)
- 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
- [snes9x-rr](https://github.com/gocha/snes9x-rr/releases) or
- [BizHawk](http://tasvideos.org/BizHawk.html) or
- [bsnes-plus-nwa](https://github.com/black-sliver/bsnes-plus)
- Or SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), or other compatible hardware
- Your Secret of Evermore US ROM file, probably named `Secret of Evermore (USA).sfc`
- 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?
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.
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](/games/Secret%20of%20Evermore/player-settings) page on the website allows you to configure your
personal settings and export a config file from them.
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](/mysterycheck) page.
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 [evermizer.com](https://evermizer.com) if you want to try the official stand-alone, otherwise read below.
1. Navigate to the [Player Settings](/games/Secret%20of%20Evermore/player-settings) page, configure your options, and
click the "Generate Game" button.
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 [apbpatch](https://evermizer.com/apbpatch) and load it in your emulator or console.
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 [apbpatch](https://evermizer.com/apbpatch) and
generate your ROM from it. Load the ROM file in your emulator or console.
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.
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...**
@@ -67,8 +77,9 @@ If this is its first time launching, you may be prompted to allow it to communic
* 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:
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.
@@ -77,43 +88,42 @@ If this is its first time launching, you may be prompted to allow it to communic
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.
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
[here](https://github.com/RedGuyyyy/sd2snes/releases). Other hardware may find helpful information
[on this page](http://usb2snes.com/#supported-platforms).
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](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.
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](/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.
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.
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,81 @@
# 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/actions/workflows/ci.yml?query=branch%3Aarchipelago+event%3Apush) 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.
## 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

@@ -2,8 +2,11 @@
## Required Software
- SNI Client
- Included in Archipelago download
- 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
@@ -17,8 +20,8 @@
### 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.
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.
@@ -85,8 +88,10 @@ first time launching, you may be prompted to allow it to communicate through the
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 in the `Archipelago\SNI\lua` folder.
- Use x86 for 32-bit or x64 for 64-bit.
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
@@ -97,8 +102,10 @@ first time launching, you may be prompted to allow it to communicate through the
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 in `Archipelago\SNI\lua` folder.
- Use x86 for 32-bit or x64 for 64-bit. Please note the most recent versions of BizHawk are 64-bit only.
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

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

@@ -2,29 +2,40 @@
## 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 (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
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 TsRandomizerItemTracker.bin.x86_64 (on linux) or TsRandomizerItemTracker.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)
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 "<< Archiplago >>" by pressing left on the controller or keyboard
4. Select "<< Archiplago >>" to open a new menu where you can enter your Archipelago login credentails
* NOTE: the input fields support Ctrl + V pasting of values
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 the difficulty selection menu and the game will start as soon as you select a difficulty
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 a config file from them.
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
* The Timespinner Randomizer options "ProgressiveVerticalMovement" & "ProgressiveKeycards" are currently not supported
on Archipelago generated seeds

View File

@@ -17,7 +17,36 @@
]
},
{
"name": "Using Advanced Settings",
"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": [
{
@@ -25,7 +54,8 @@
"filename": "archipelago/advanced_settings_en.md",
"link": "archipelago/advanced_settings/en",
"authors": [
"alwaysintreble"
"alwaysintreble",
"Alchav"
]
}
]
@@ -53,7 +83,8 @@
"filename": "archipelago/plando_en.md",
"link": "archipelago/plando/en",
"authors": [
"alwaysintreble"
"alwaysintreble",
"Alchav"
]
}
]
@@ -248,6 +279,26 @@
}
]
},
{
"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": [
@@ -262,6 +313,16 @@
"authors": [
"Jarno"
]
},
{
"language": "German",
"filename": "timespinner/setup_de.md",
"link": "timespinner/setup/de",
"authors": [
"Grrmo",
"Fynxes",
"Blaze0168"
]
}
]
}
@@ -361,5 +422,62 @@
]
}
]
},
{
"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/actions/workflows/ci.yml?query=branch%3Aarchipelago)
## Installation and Game Start Procedures
1. Install VVVVVV through either Steam or GOG
2. Go to the page linked for V6AP, and press on the topmost entry
3. Scroll down, and download the zip file corresponding to your platform (NOTE: Linux currently does not build automatically. Linux users will have to compile manually for now. Mac is unsupported, but may work if [APCpp](https://github.com/N00byKing/APCpp) is compiled and supplied)
4. 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"`.
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"` are required for startup.
## 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,15 +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)
- Zelda community member Amarith assembled the following list for the purpose of competitive restreams. While we have not ourselves verified this list, all submissions required VoD proof they were not muted. Generally speaking, MSU-1 packs are less safe if they contain lyrics at any point. This list was only tested on Twitch and results for other platforms may vary. [Restream-Safe List](https://tinyurl.com/MSUsApprovedForLeagueChannels)
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,6 +1,7 @@
# A Link to the Past Randomizer Setup Guide
## Benötigte Software
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
- [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
@@ -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](/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.
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,30 +66,35 @@ 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 `.apbp`.
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 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.
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...**
@@ -94,8 +105,9 @@ durch die Windows Firewall kommunizieren darf.
"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**
@@ -106,32 +118,34 @@ durch die Windows Firewall kommunizieren darf.
"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).
1. Schließe deinen Emulator, falls er automatisch gestartet haben 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.
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](/generate) zu nutzen. Das Ganze ist recht einfach:
@@ -140,11 +154,11 @@ Die Empfohlene Art, ein Spiel zu hosten, ist, den Service auf
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,100 +1,111 @@
# A Link to the Past Randomizer Setup Guide
## Required Software
- [SNIClient](https://github.com/ArchipelagoMW/Archipelago/releases) included with the main Archipelago install
or [SuperNintendoClient](https://github.com/ArchipelagoMW/SuperNintendoClient/releases)
- If installing Archipelago, make sure to check the box for `SNI Client - A Link to the Past Patch Setup`
- [SNI](https://github.com/alttpo/sni/releases) (Included in both clients from the first step)
- 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 connecting to SNI
([snes9x Multitroid](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz),
([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
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**.
**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. 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.
## 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.
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](/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.
### 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](/mysterycheck) page.
## Generating a Single-Player Game
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.
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 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.
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, 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.
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.
- 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 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.
- 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).
@@ -102,20 +113,22 @@ done so already, please do this now. SD2SNES and FXPak Pro users may download th
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".
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! 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.
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 our [hosting service](/generate). The process is relatively simple:
1. Collect config files from your players.
@@ -123,8 +136,8 @@ The recommended way to host a game is to use our [hosting service](/generate). T
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.
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

@@ -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](/games/A%20Link%20to%20the%20Past/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](/games/A%20Link%20to%20the%20Past/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

View File

@@ -7,102 +7,126 @@
</div>
## Logiciels requis
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Inclus dans les utilitaires précédents)
- Une solution logicielle ou matérielle capable de charger et de lancer des fichiers ROM de SNES
- Un émulateur capable d'éxécuter des scripts Lua
([snes9x Multitroid](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz),
[BizHawk](http://tasvideos.org/BizHawk.html))
- Un SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), ou une autre solution matérielle compatible
- Un SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), ou une autre solution matérielle
compatible
- Le fichier ROM de la v1.0 japonaise, sûrement nommé `Zelda no Densetsu - Kamigami no Triforce (Japan).sfc`
## Procédure d'installation
### Installation sur Windows
1. Téléchargez et installez les utilitaires du MultiWorld à l'aide du lien au-dessus, faites attention à bien installer la version la plus récente.
**Le fichier se situe dans la section "assets" en bas des informations de version**. Si vous voulez jouer des parties classiques de multiworld,
téléchargez `Setup.BerserkerMultiWorld.exe`
- Si vous voulez jouer à la version alternative avec le mélangeur de portes dans les donjons, vous téléchargez le fichier
`Setup.BerserkerMultiWorld.Doors.exe`.
- Durant le processus d'installation, il vous sera demandé de localiser votre ROM v1.0 japonaise. Si vous avez déjà installé le logiciel
auparavant et qu'il s'agit simplement d'une mise à jour, la localisation de la ROM originale ne sera pas requise.
- Il vous sera peut-être également demandé d'installer Microsoft Visual C++. Si vous le possédez déjà (possiblement parce qu'un
jeu Steam l'a déjà installé), l'installateur ne reproposera pas de l'installer.
2. Si vous utilisez un émulateur, il est recommandé d'assigner votre émulateur capable d'éxécuter des scripts Lua comme programme
par défaut pour ouvrir vos ROMs.
1. Extrayez votre dossier d'émulateur sur votre Bureau, ou à un endroit dont vous vous souviendrez.
1. Téléchargez et installez les utilitaires du MultiWorld à l'aide du lien au-dessus, faites attention à bien installer
la version la plus récente.
**Le fichier se situe dans la section "assets" en bas des informations de version**. Si vous voulez jouer des parties
classiques de multiworld, téléchargez `Setup.BerserkerMultiWorld.exe`
- Si vous voulez jouer à la version alternative avec le mélangeur de portes dans les donjons, vous téléchargez le
fichier
`Setup.BerserkerMultiWorld.Doors.exe`.
- Durant le processus d'installation, il vous sera demandé de localiser votre ROM v1.0 japonaise. Si vous avez déjà
installé le logiciel auparavant et qu'il s'agit simplement d'une mise à jour, la localisation de la ROM originale
ne sera pas requise.
- Il vous sera peut-être également demandé d'installer Microsoft Visual C++. Si vous le possédez déjà (possiblement
parce qu'un jeu Steam l'a déjà installé), l'installateur ne reproposera pas de l'installer.
2. Si vous utilisez un émulateur, il est recommandé d'assigner votre émulateur capable d'éxécuter des scripts Lua comme
programme par défaut pour ouvrir vos ROMs.
1. Extrayez votre dossier d'émulateur sur votre Bureau, ou à un endroit dont vous vous souviendrez.
2. Faites un clic droit sur un fichier ROM et sélectionnez **Ouvrir avec...**
3. Cochez la case à côté de **Toujours utiliser cette application pour ouvrir les fichiers .sfc**
4. Descendez jusqu'en bas de la liste et sélectionnez **Rechercher une autre application sur ce PC**
5. Naviguez dans les dossiers jusqu'au fichier `.exe` de votre émulateur et choisissez **Ouvrir**. Ce fichier devrait
se trouver dans le dossier que vous avez extrait à la première étape.
5. Naviguez dans les dossiers jusqu'au fichier `.exe` de votre émulateur et choisissez **Ouvrir**. Ce fichier
devrait se trouver dans le dossier que vous avez extrait à la première étape.
### Installation sur Mac
- Des volontaires sont recherchés pour remplir cette section ! Contactez **Farrak Kilhn** sur Discord si vous voulez aider.
- Des volontaires sont recherchés pour remplir cette section ! Contactez **Farrak Kilhn** sur Discord si vous voulez
aider.
## Configurer son fichier YAML
### Qu'est-ce qu'un fichier YAML et pourquoi en ai-je besoin ?
Votre fichier YAML contient un ensemble d'options de configuration qui fournissent au générateur des informations
sur comment il devrait générer votre seed. Chaque joueur d'un multiwolrd devra fournir son propre fichier YAML. Cela permet à chaque
joueur d'apprécier une expérience customisée selon ses goûts, et les différents joueurs d'un même multiworld peuvent avoir différentes options.
Votre fichier YAML contient un ensemble d'options de configuration qui fournissent au générateur des informations sur
comment il devrait générer votre seed. Chaque joueur d'un multiwolrd devra fournir son propre fichier YAML. Cela permet
à chaque joueur d'apprécier une expérience customisée selon ses goûts, et les différents joueurs d'un même multiworld
peuvent avoir différentes options.
### Où est-ce que j'obtiens un fichier YAML ?
La page [Génération de partie](/games/A%20Link%20to%20the%20Past/player-settings) vous permet de configurer vos paramètres personnels et de les exporter vers un fichier YAML.
La page [Génération de partie](/games/A%20Link%20to%20the%20Past/player-settings) vous permet de configurer vos
paramètres personnels et de les exporter vers un fichier YAML.
### Configuration avancée du fichier YAML
Une version plus avancée du fichier YAML peut être créée en utilisant la page des [paramètres de pondération](/weighted-settings), qui vous permet
de configurer jusqu'à trois préréglages. Cette page a de nombreuses options qui sont essentiellement représentées avec des curseurs glissants.
Cela vous permet de choisir quelles sont les chances qu'une certaine option apparaisse par rapport aux autres disponibles dans une même catégorie.
Une version plus avancée du fichier YAML peut être créée en utilisant la page
des [paramètres de pondération](/weighted-settings), qui vous permet de configurer jusqu'à trois préréglages. Cette page
a de nombreuses options qui sont essentiellement représentées avec des curseurs glissants. Cela vous permet de choisir
quelles sont les chances qu'une certaine option apparaisse par rapport aux autres disponibles dans une même catégorie.
Par exemple, imaginez que le générateur crée un seau étiqueté "Mélange des cartes", et qu'il place un morceau de papier
pour chaque sous-option. Imaginez également que la valeur pour "On" est 20 et la valeur pour "Off" est 40.
Dans cet exemple, il y a soixante morceaux de papier dans le seau : vingt pour "On" et quarante pour "Off". Quand le générateur
décide s'il doit oui ou non activer le mélange des cartes pour votre partie, , il tire aléatoirement un papier dans le seau.
Dans cet exemple, il y a de plus grandes chances d'avoir le mélange de cartes désactivé.
Dans cet exemple, il y a soixante morceaux de papier dans le seau : vingt pour "On" et quarante pour "Off". Quand le
générateur décide s'il doit oui ou non activer le mélange des cartes pour votre partie, , il tire aléatoirement un
papier dans le seau. Dans cet exemple, il y a de plus grandes chances d'avoir le mélange de cartes désactivé.
S'il y a une option dont vous ne voulez jamais, mettez simplement sa valeur à zéro. N'oubliez pas qu'il faut que pour chaque paramètre il faut
au moins une option qui soit paramétrée sur un nombre strictement positif.
S'il y a une option dont vous ne voulez jamais, mettez simplement sa valeur à zéro. N'oubliez pas qu'il faut que pour
chaque paramètre il faut au moins une option qui soit paramétrée sur un nombre strictement positif.
### Vérifier son fichier YAML
Si vous voulez valider votre fichier YAML pour être sûr qu'il fonctionne, vous pouvez le vérifier sur la page du
[Validateur de YAML](/mysterycheck).
## Générer une partie pour un joueur
1. Aller sur la page [Génération de partie](/games/A%20Link%20to%20the%20Past/player-settings), configurez vos options, et cliquez sur le bouton "Generate Game".
1. Aller sur la page [Génération de partie](/games/A%20Link%20to%20the%20Past/player-settings), configurez vos options,
et cliquez sur le bouton "Generate Game".
2. Il vous sera alors présenté une page d'informations sur la seed, où vous pourrez télécharger votre patch.
3. Double-cliquez sur le patch et l'émulateur devrait se lancer automatiquement avec la seed. Etant donné que le client
n'est pas requis pour les parties à un joueur, vous pouvez le fermer ainsi que l'interface Web (WebUI).
n'est pas requis pour les parties à un joueur, vous pouvez le fermer ainsi que l'interface Web (WebUI).
## Rejoindre un MultiWorld
### Obtenir son patch et créer sa ROM
Quand vous rejoignez un multiworld, il vous sera demandé de fournir votre fichier YAML à celui qui héberge la partie
ou s'occupe de la génération. Une fois cela fait, l'hôte vous fournira soit un lien pour télécharger votre patch, soit un fichier `.zip` contenant
les patchs de tous les joueurs. Votre patch devrait avoir l'extension `.bmbp`.
Placez votre patch sur votre bureau ou dans un dossier simple d'accès, et double-cliquez dessus. Cela devrait lancer automatiquement
le client, et devrait créer la ROM dans le même dossier que votre patch.
Quand vous rejoignez un multiworld, il vous sera demandé de fournir votre fichier YAML à celui qui héberge la partie ou
s'occupe de la génération. Une fois cela fait, l'hôte vous fournira soit un lien pour télécharger votre patch, soit un
fichier `.zip` contenant les patchs de tous les joueurs. Votre patch devrait avoir l'extension `.bmbp`.
Placez votre patch sur votre bureau ou dans un dossier simple d'accès, et double-cliquez dessus. Cela devrait lancer
automatiquement le client, et devrait créer la ROM dans le même dossier que votre patch.
### Se connecter au client
#### Avec un émulateur
Quand le client se lance automatiquement, QUsb2Snes devrait se lancer automatiquement également en arrière-plan.
Si c'est la première fois qu'il démarre, il vous sera peut-être demandé de l'autoriser à communiquer
à travers le pare-feu Windows.
Quand le client se lance automatiquement, QUsb2Snes devrait se lancer automatiquement également en arrière-plan. Si
c'est la première fois qu'il démarre, il vous sera peut-être demandé de l'autoriser à communiquer à travers le pare-feu
Windows.
##### snes9x Multitroid
1. Chargez votre ROM si ce n'est pas déjà fait.
2. Cliquez sur le menu "File" et survolez l'option **Lua Scripting**
3. Cliquez alors sur **New Lua Script Window...**
4. Dans la nouvelle fenêtre, sélectionnez **Browse...**
5. Dirigez vous vers le dossier où vous avez extrait snes9x Multitroid, allez dans le dossier `lua`, puis choisissez `multibridge.lua`
6. Remarquez qu'un nom vous a été assigné, et que l'interface Web affiche "SNES Device: Connected", avec ce même nom dans le coin en haut à gauche.
5. Dirigez vous vers le dossier où vous avez extrait snes9x Multitroid, allez dans le dossier `lua`, puis
choisissez `multibridge.lua`
6. Remarquez qu'un nom vous a été assigné, et que l'interface Web affiche "SNES Device: Connected", avec ce même nom
dans le coin en haut à gauche.
##### BizHawk
1. Assurez vous d'avoir le coeur BSNES chargé. Cela est possible en cliquant sur le menu "Tools" de BizHawk et suivant ces options de menu :
1. Assurez vous d'avoir le coeur BSNES chargé. Cela est possible en cliquant sur le menu "Tools" de BizHawk et suivant
ces options de menu :
`Config --> Cores --> SNES --> BSNES`
Une fois le coeur changé, vous devez redémarrer BizHawk.
2. Chargez votre ROM si ce n'est pas déjà fait.
@@ -111,11 +135,13 @@ Si c'est la première fois qu'il démarre, il vous sera peut-être demandé de l
5. Dirigez vous vers le dossier d'installation des utilitaires du MultiWorld, puis dans les dossiers suivants :
`QUsb2Snes/Qusb2Snes/LuaBridge`
6. Sélectionnez `luabridge.lua` et cliquez sur "Open".
7. Remarquez qu'un nom vous a été assigné, et que l'interface Web affiche "SNES Device: Connected", avec ce même nom dans le coin en haut à gauche.
7. Remarquez qu'un nom vous a été assigné, et que l'interface Web affiche "SNES Device: Connected", avec ce même nom
dans le coin en haut à gauche.
#### Avec une solution matérielle
Ce guide suppose que vous avez télchargé le bon micro-logiciel pour votre appareil. Si ce n'est pas déjà le cas, faites le maintenant.
Les utilisateurs de SD2SNES et de FXPak Pro peuvent télécharger le micro-logiciel approprié
Ce guide suppose que vous avez télchargé le bon micro-logiciel pour votre appareil. Si ce n'est pas déjà le cas, faites
le maintenant. Les utilisateurs de SD2SNES et de FXPak Pro peuvent télécharger le micro-logiciel approprié
[ici](https://github.com/RedGuyyyy/sd2snes/releases). Pour les autres solutions, de l'aide peut être trouvée
[sur cette page](http://usb2snes.com/#supported-platforms).
@@ -126,20 +152,24 @@ Les utilisateurs de SD2SNES et de FXPak Pro peuvent télécharger le micro-logic
5. Remarquez que l'interface Web affiche "SNES Device: Connected", avec le nom de votre appareil.
### Se connecter au MultiServer
Le patch qui a lancé le client devrait vous avoir connecté automatiquement au MultiServer.
Il y a cependant quelques cas où cela peut ne pas se produire, notamment si le multiworld est hébergé sur ce site, mais a été généré ailleurs.
Si l'interface Web affiche "Server Status: Not Connected", demandez simplement à l'hôte l'adresse du serveur,
et copiez/collez la dans le champ "Server" puis appuyez sur Entrée.
Le client essaiera de vous reconnecter à la nouvelle adresse du serveur, et devrait mentionner "Server
Status: Connected". Si le client ne se connecte pas après quelques instants, il faudra peut-être rafraîchir la page de l'interface Web.
Le patch qui a lancé le client devrait vous avoir connecté automatiquement au MultiServer. Il y a cependant quelques cas
où cela peut ne pas se produire, notamment si le multiworld est hébergé sur ce site, mais a été généré ailleurs. Si
l'interface Web affiche "Server Status: Not Connected", demandez simplement à l'hôte l'adresse du serveur, et
copiez/collez la dans le champ "Server" puis appuyez sur Entrée.
Le client essaiera de vous reconnecter à la nouvelle adresse du serveur, et devrait mentionner "Server Status:
Connected". Si le client ne se connecte pas après quelques instants, il faudra peut-être rafraîchir la page de
l'interface Web.
### Jouer au jeu
Une fois que l'interface Web affiche que la SNES et le serveur sont connectés, vous êtes prêt à jouer. Félicitations
pour avoir rejoint un multiworld !
## Héberger un MultiWorld
La méthode recommandée pour héberger une partie est d'utiliser le service d'hébergement fourni par
La méthode recommandée pour héberger une partie est d'utiliser le service d'hébergement fourni par
[le site](https://berserkermulti.world/generate). Le processus est relativement simple :
1. Récupérez les fichiers YAML des joueurs.
@@ -147,26 +177,32 @@ La méthode recommandée pour héberger une partie est d'utiliser le service d'h
3. Téléversez l'archive zip sur le lien ci-dessus.
4. Attendez un moment que les seed soient générées.
5. Lorsque les seeds sont générées, vous serez redirigé vers une page d'informations "Seed Info".
6. Cliquez sur "Create New Room". Cela vous amènera à la page du serveur. Fournissez le lien de cette page aux autres joueurs
afin qu'ils puissent récupérer leurs patchs.
6. Cliquez sur "Create New Room". Cela vous amènera à la page du serveur. Fournissez le lien de cette page aux autres
joueurs afin qu'ils puissent récupérer leurs patchs.
**Note:** Les patchs fournis sur cette page permettront aux joueurs de se connecteur automatiquement au serveur,
tandis que ceux de la page "Seed Info" non.
7. Remarquez qu'un lien vers le traqueur du MultiWorld est en haut de la page de la salle. Vous devriez également fournir ce lien aux joueurs
pour qu'ils puissent suivre la progression de la partie. N'importe quel personne voulant observer devrait avoir accès à ce lien.
7. Remarquez qu'un lien vers le traqueur du MultiWorld est en haut de la page de la salle. Vous devriez également
fournir ce lien aux joueurs pour qu'ils puissent suivre la progression de la partie. N'importe quel personne voulant
observer devrait avoir accès à ce lien.
8. Une fois que tous les joueurs ont rejoint, vous pouvez commencer à jouer.
## Auto-tracking
Si vous voulez utiliser l'auto-tracking, plusieurs logiciels offrent cette possibilité.
Le logiciel recommandé pour l'auto-tracking actuellement est
[OpenTracker](https://github.com/trippsc2/OpenTracker/releases).
### Installation
1. Téléchargez le fichier d'installation approprié pour votre ordinateur (Les utilisateurs Windows voudront le fichier `.msi`).
2. Durant le processus d'installation, il vous sera peut-être demandé d'installer les outils "Microsoft Visual Studio Build Tools". Un
lien est fourni durant l'installation d'OpenTracker, et celle des outils doit se faire manuellement.
1. Téléchargez le fichier d'installation approprié pour votre ordinateur (Les utilisateurs Windows voudront le
fichier `.msi`).
2. Durant le processus d'installation, il vous sera peut-être demandé d'installer les outils "Microsoft Visual Studio
Build Tools". Un lien est fourni durant l'installation d'OpenTracker, et celle des outils doit se faire manuellement.
### Activer l'auto-tracking
1. Une fois OpenTracker démarré, cliquez sur le menu "Tracking" en haut de la fenêtre, puis choisissez **AutoTracker...**
1. Une fois OpenTracker démarré, cliquez sur le menu "Tracking" en haut de la fenêtre, puis choisissez **
AutoTracker...**
2. Appuyez sur le bouton **Get Devices**
3. Sélectionnez votre appareil SNES dans la liste déroulante.
4. Si vous voulez tracquer les petites clés ainsi que les objets des donjons, cochez la case **Race Illegal Tracking**

View File

@@ -1,25 +1,24 @@
# A Link to the Past Randomizer Plando Guide
## Configuration
1. Plando features have to be enabled first, before they can be used (opt-in).
2. To do so, go to your installation directory (Windows default: `C:\ProgramData\Archipelago`),
then open the host.yaml file with a text editor.
3. In it, you're looking for the option key `plando_options`. To enable all plando modules you can set the
value to
`bosses, items, texts, connections`
2. To do so, go to your installation directory (Windows default: `C:\ProgramData\Archipelago`), then open the host.yaml
file with a text editor.
3. In it, you're looking for the option key `plando_options`. To enable all plando modules you can set the value
to `bosses, items, texts, connections`
## Modules
### Bosses
- This module is enabled by default and available to be used on
[https://archipelago.gg/generate](/generate)
- This module is enabled by default and available to be used on [https://archipelago.gg/generate](/generate)
- Plando versions of boss shuffles can be added like any other boss shuffle option in a yaml and weighted.
- Boss Plando works as a list of instructions from left to right, if any arenas are empty at the end,
it defaults to vanilla
- Instructions are separated by a semicolon
- Boss Plando works as a list of instructions from left to right, if any arenas are empty at the end, it defaults to
vanilla.
- Instructions are separated by a semicolon.
- Available Instructions:
- Direct Placement:
- Direct Placement:
- Example: `Eastern Palace-Trinexx`
- Takes a particular Arena and particular boss, then places that boss into that arena
- Ganons Tower has 3 placements, `Ganons Tower Top`, `Ganons Tower Middle` and `Ganons Tower Bottom`
@@ -29,12 +28,13 @@
- In this example, it would fill Desert Palace, but not Tower of Hera.
- Boss Shuffle:
- Example: `simple`
- Runs a particular boss shuffle mode to finish construction instead of vanilla placement, typically used as
a last instruction.
- Runs a particular boss shuffle mode to finish construction instead of vanilla placement, typically used as
a last instruction.
- [Available Bosses](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Bosses.py#L135)
- [Available Arenas](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Bosses.py#L150)
#### Examples
```yaml
boss_shuffle:
Turtle Rock-Trinexx;basic: 1
@@ -42,14 +42,15 @@ boss_shuffle:
Mothula: 3
Ganons Tower Bottom-Kholdstare;Trinexx;Kholdstare: 4
```
1. Would be basic boss shuffle but prevent Trinexx from appearing outside of Turtle Rock,
as there's only one Trinexx in the pool
1. Would be basic boss shuffle but prevent Trinexx from appearing outside of Turtle Rock, as there's only one Trinexx in
the pool
2. Regular full boss shuffle. With a 2 in 10 chance to occur.
3. A Mothula Singularity, as Mothula works in any arena.
4. A Trinexx -> Kholdstare Singularity that prevents ice Trinexx in GT
### Items
- This module is disabled by default.
- Has the options from_pool, world, percentage, force and either item and location or items and locations
- All of these options support subweights
@@ -77,12 +78,13 @@ boss_shuffle:
- items denotes the items to use, can be given a number to have multiple of that item
- locations lists the possible locations those items can be placed in
- placements are picked randomly, not sorted in any way
- Warning: Placing non-Dungeon Prizes on Prize locations and
Prizes on non-Prize locations will break the game in various ways.
- Warning: Placing non-Dungeon Prizes on Prize locations and Prizes on non-Prize locations will break the game in
various ways.
- [Available Items](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Items.py#L52)
- [Available Locations](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Regions.py#L434)
#### Examples
```yaml
plando_items:
- item: # 1
@@ -119,26 +121,28 @@ plando_items:
from_pool: true
```
1. has a 50% chance to occur, which if it does places either the Lamp or Fire Rod in one's own
Link's House and removes the picked item from the item pool.
1. has a 50% chance to occur, which if it does places either the Lamp or Fire Rod in one's own Link's House and removes
the picked item from the item pool.
2. Always triggers and places the Swords and Bows into one's own Big Chests
3. Locks Pendants to The Light World and therefore Crystals to dark world
### Texts
- This module is disabled by default.
- Has the options `text`, `at`, and `percentage`
- percentage is the percentage chance for this text to be placed, can be omitted entirely for 100%
- text is the text to be placed.
- can be weighted.
- `\n` is a newline.
- `\n` is a newline.
- `@` is the entered player's name.
- Warning: Text Mapper does not support full unicode.
- [Alphabet](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Text.py#L758)
- at is the location within the game to attach the text to.
- can be weighted.
- [List of targets](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Text.py#L1499)
#### Example
```yaml
plando_texts:
- text: "This is a plando.\nYou've been warned."
@@ -147,11 +151,13 @@ plando_texts:
uncle_dying_sewer: 1
percentage: 50
```
![Uncle Example](https://cdn.discordapp.com/attachments/731214280439103580/794953870903083058/unknown.png)
This has a 50% chance to trigger at all. If it does, it throws a coin between `uncle_leaving_text` and
`uncle_dying_sewer`, then places the text "This is a plando. You've been warned." at that location.
![Example plando text at Uncle](https://cdn.discordapp.com/attachments/731214280439103580/794953870903083058/unknown.png)
This has a 50% chance to trigger at all. If it does, it throws a coin between `uncle_leaving_text`
and `uncle_dying_sewer`, then places the text "This is a plando. You've been warned." at that location.
### Connections
- This module is disabled by default.
- Has the options `percentage`, `entrance`, `exit` and `direction`.
- All options support subweights
@@ -160,10 +166,11 @@ This has a 50% chance to trigger at all. If it does, it throws a coin between `u
- entrance is the overworld door
- exit is the underworld exit
- direction can be `both`, `entrance` or `exit`
- doors can be found in [this file](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/EntranceShuffle.py#L3852)
- doors can be found
in [this file](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/EntranceShuffle.py#L3852)
#### Example
```yaml
plando_connections:
- entrance: Links House
@@ -174,8 +181,8 @@ plando_connections:
direction: both
```
The first block connects the overworld entrance that normally leads to Link's House
to put you into the HC West Wing instead, exiting from within there will put you at the Overworld exiting Link's House.
The first block connects the overworld entrance that normally leads to Link's House to put you into the HC West Wing
instead, exiting from within there will put you at the Overworld exiting Link's House.
Without the second block, you'd still exit from within Link's House to outside Link's House and the left side
Balcony Entrance would still lead into HC West Wing
Without the second block, you'd still exit from within Link's House to outside Link's House and the left side Balcony
Entrance would still lead into HC West Wing

View File

@@ -2,17 +2,22 @@
## Important
As we are using Z5Client and Bizhawk, this guide is only applicable to Windows.
As we are using Z5Client and BizHawk, this guide is only applicable to Windows.
## Required Software
- [bizhawk+script+Z5Client](https://github.com/ArchipelagoMW/Z5Client/releases) We recommend download Z5Client-setup as it makes some steps automatic.
- BizHawk and Z5Client from: [Z5Client Releases Page](https://github.com/ArchipelagoMW/Z5Client/releases)
- We recommend download Z5Client-setup as it makes some steps automatic.
## Install Emulator and client
Download getBizhawk.ps1 from previous link. Place it on the folder where you want your emulator to be installed, right click on it and select "Run with PowerShell". This will download all the needed dependencies used by the emulator. This can take a while.
Download getBizhawk.ps1 from previous link. Place it on the folder where you want your emulator to be installed, right
click on it and select "Run with PowerShell". This will download all the needed dependencies used by the emulator. This
can take a while.
It is strongly recommended to associate N64 rom extension (\*.n64) to the Bizhawk we've just installed. To do so, we simple have to search any N64 rom we happened to own, right click and select "Open with...", we unfold the list that appears and select the bottom option "Look for another application", we browse to Bizhawk folder and select EmuHawk.exe
It is strongly recommended to associate N64 rom extension (\*.n64) to the BizHawk we've just installed. To do so, we
simply have to search any N64 rom we happened to own, right click and select "Open with...", we unfold the list that
appears and select the bottom option "Look for another application", we browse to BizHawk folder and select EmuHawk.exe
Place the ootMulti.lua file from the previous link inside the "lua" folder from the just installed emulator.
@@ -21,14 +26,18 @@ Install the Z5Client using its setup.
## 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.
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?
A basic OOT yaml will look like this. There are lots of cosmetic options that have been removed for the sake of this tutorial, if you want to see a complete list, download [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) and look for the sample file in the "Players" folder.
A basic OOT yaml will look like this. There are lots of cosmetic options that have been removed for the sake of this
tutorial, if you want to see a complete list, download Archipelago from
the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) and look for the sample file in
the "Players" folder.
```yaml
description: Default Ocarina of Time Template # Used to describe your yaml. Useful if you have multiple files
# Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit
@@ -371,17 +380,24 @@ Ocarina of Time:
### Obtain your OOT patch file
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 `.z5ap` 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 `.z5ap` extension.
Double click on your `.z5ap` file to start Z5Client and start the ROM patch process. Once the process is finished (this can take a while), the emulator will be started automatically (If we associated the extension to the emulator as recommended)
Double-click on your `.z5ap` file to start Z5Client and start the ROM patch process. Once the process is finished (this
can take a while), the emulator will be started automatically (If we associated the extension to the emulator as
recommended)
### Connect to multiserver
Once both the Z5Client and the emulator are started we must connect them. Within the emulator we click on the "Tools" menu and select "Lua console". In the new window click on the folder icon and look for the ootMulti.lua file. Once the file is loaded it will connect automatically to Z5Client.
### Connect to the Multiserver
Note: We strongly advise you don't open any emulator menu while it and Z5client are connected, as the script will halt and disconnects can happen. If you get disconnected just double click on the script again.
Once both the Z5Client and the emulator are started we must connect them. Within the emulator we click on the "Tools"
menu and select "Lua console". In the new window click on the folder icon and look for the ootMulti.lua file. Once the
file is loaded it will connect automatically to Z5Client.
To connect the client to the multiserver simply put address:port on the textfield on top and press enter (if the server uses password, type on the bottom textfield /connect <address>:<port> [password], to connect)
Note: We strongly advise you don't open any emulator menu while it and Z5client are connected, as the script will halt
and disconnects can happen. If you get disconnected just double-click on the script again.
To connect the client to the multiserver simply put `<address>:<port>` on the textfield on top and press enter (if the
server uses password, type in the bottom textfield `/connect <address>:<port> [password]`)
Now you are ready to start your adventure in Hyrule.

View File

@@ -6,13 +6,19 @@ Al usar el cliente y bizhawk, esta guia solo es aplicable en Windows.
## Software Requerido
- [bizhawk+script+Z5Client](https://github.com/ArchipelagoMW/Z5Client/releases) Recomendamos bajar el setup de Z5client ya que automatizara varios pasos mas adelante
- [bizhawk+script+Z5Client](https://github.com/ArchipelagoMW/Z5Client/releases) Recomendamos bajar el setup de Z5client
ya que automatizara varios pasos mas adelante
## Instala emulador y cliente
Descarga el fichero getBizhawk.ps1 del enlace anterior. Colocalo en la carpeta donde desees instalar el emulador, haz click derecho en él y selecciona "Ejecutar con PowerShell". Esto descargará todas las dependencias necesarias para el emulador. Puede tardar un rato.
Descarga el fichero getBizhawk.ps1 del enlace anterior. Colocalo en la carpeta donde desees instalar el emulador, haz
click derecho en él y selecciona "Ejecutar con PowerShell". Esto descargará todas las dependencias necesarias para el
emulador. Puede tardar un rato.
Es recomendable asociar la extensión de las roms de N64 (\*.n64) al bizhawk que hemos instalado anteriormente. Para hacerlo simplemente debemos buscar alguna rom de n64 que tengamos, hacer click derecho, seleccionar "Abrir con...", desplegar la lista y buscar la opción "Buscar otra aplicación", navegar hasta el directorio de bizhawk y seleccionar EmuHawk.exe
Es recomendable asociar la extensión de las roms de N64 (\*.n64) al bizhawk que hemos instalado anteriormente. Para
hacerlo simplemente debemos buscar alguna rom de n64 que tengamos, hacer click derecho, seleccionar "Abrir con...",
desplegar la lista y buscar la opción "Buscar otra aplicación", navegar hasta el directorio de bizhawk y seleccionar
EmuHawk.exe
Situa el fichero ootMulti.lua del enlace anterior en la carpeta "lua" del emulador recien instalado.
@@ -21,16 +27,21 @@ Instala el cliente Z5Client.
## Configura tu fichero YAML
### Que es un fichero YAML y por qué necesito uno?
Tu fichero YAML contiene un numero de opciones que proveen al generador con información 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 información 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 OOT tendra este aspecto. (Hay muchas opciones cosméticas que se han ignorado para este tutorial, si quieres ver una lista completa, descarga (Archipelago)[https://github.com/ArchipelagoMW/Archipelago/releases] y buscar el fichero de ejemplo en el directorio "Players"))
Un fichero basico yaml para OOT tendra este aspecto. (Hay muchas opciones cosméticas que se han ignorado para este
tutorial, si quieres ver una lista completa, descarga (
Archipelago)[https://github.com/ArchipelagoMW/Archipelago/releases] y buscar el fichero de ejemplo en el directorio "
Players"))
```yaml
description: Default Ocarina of Time Template # Describe tu fichero yalm
\# Tu nombre en el juego. Los espacio seran reemplazados por _ y hay un limite de 16 caracteres
\# Tu nombre en el juego. Los espacio seran reemplazados por _ y hay un limite de 16 caracteres
name: YourName{number}
game:
Ocarina of Time: 1
@@ -355,18 +366,26 @@ Ocarina of Time:
### Obten tu parche
Cuando te unes a un juego multiworld, se te pedirá que entregues tu fichero YAML a quien sea que hospede el juego
multiworld. 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 `.z5ap`.
Cuando te unes a un juego multiworld, se te pedirá que entregues tu fichero YAML a quien sea que hospede el juego multiworld.
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 `.z5ap`.
Haz doble click en tu fichero `.z5ap` para que se arranque el Z5Client y realize el parcheado de la ROM. Una vez acabe el parcheado de la rom (esto puede llevar un tiempo) se abrira automaticamente el emulador (Si se ha asociado la extensión al emulador tal como hemos recomendado)
Haz doble click en tu fichero `.z5ap` para que se arranque el Z5Client y realize el parcheado de la ROM. Una vez acabe
el parcheado de la rom (esto puede llevar un tiempo) se abrira automaticamente el emulador (Si se ha asociado la
extensión al emulador tal como hemos recomendado)
### Conectar al multiserver
Una vez arrancado tanto el Z5Client como el emulador hay que conectarlo entre ellos, para ello simplemente accede al menú "Tools" y selecciona "Lua console". En la nueva ventana, dale al icono de la carpeta y busca el fichero ootMulti.lua. Al cargar dicho fichero se conectara automaticamente con el cliente.
Nota: Es muy recomendable que no se abra ningún menú del emulador mientras esten emulador y Z5Client conectados, ya que el script de conexión se para en ese caso y pueden provocar desconexiones. Si se pierde la conexion, simplemente haz doble click en el script de nuevo.
Una vez arrancado tanto el Z5Client como el emulador hay que conectarlo entre ellos, para ello simplemente accede al
menú "Tools" y selecciona "Lua console". En la nueva ventana, dale al icono de la carpeta y busca el fichero
ootMulti.lua. Al cargar dicho fichero se conectara automaticamente con el cliente.
Para conectar el cliente con el servidor simplemente pon la direccion_IP:puerto en la caja de texto de arriba y presiona enter (si el servidor tiene contraseña, en la caja de texto de abajo escribir /connect direccion:puerto contraseña, para conectar)
Nota: Es muy recomendable que no se abra ningún menú del emulador mientras esten emulador y Z5Client conectados, ya que
el script de conexión se para en ese caso y pueden provocar desconexiones. Si se pierde la conexion, simplemente haz
doble click en el script de nuevo.
Para conectar el cliente con el servidor simplemente pon la direccion_IP:puerto en la caja de texto de arriba y presiona
enter (si el servidor tiene contraseña, en la caja de texto de abajo escribir /connect direccion:puerto contraseña, para
conectar)
Y ya estas listo, para emprender tu aventura por Hyrule.

View File

@@ -85,24 +85,30 @@ const createDefaultSettings = (settingData) => {
newSettings[game][gameSetting]['random-low'] = 0;
newSettings[game][gameSetting]['random-high'] = 0;
break;
case 'items-list':
case 'locations-list':
case 'custom-list':
newSettings[game][gameSetting] = [];
break;
default:
console.error(`Unknown setting type for ${game} setting ${gameSetting}: ${setting.type}`);
}
}
newSettings[game].start_inventory = [];
newSettings[game].start_inventory = {};
newSettings[game].exclude_locations = [];
newSettings[game].local_items = [];
newSettings[game].non_local_items = [];
newSettings[game].start_hints = [];
newSettings[game].start_location_hints = [];
}
localStorage.setItem('weighted-settings', JSON.stringify(newSettings));
}
};
// TODO: Include item configs: start_inventory, local_items, non_local_items, start_hints
// TODO: Include location configs: exclude_locations
const buildUI = (settingData) => {
// Build the game-choice div
buildGameChoice(settingData.games);
@@ -128,19 +134,30 @@ const buildUI = (settingData) => {
expandButton.classList.add('invisible');
gameDiv.appendChild(expandButton);
const optionsDiv = buildOptionsDiv(game, settingData.games[game].gameSettings);
gameDiv.appendChild(optionsDiv);
const weightedSettingsDiv = buildWeightedSettingsDiv(game, settingData.games[game].gameSettings);
gameDiv.appendChild(weightedSettingsDiv);
const itemsDiv = buildItemsDiv(game, settingData.games[game].gameItems);
gameDiv.appendChild(itemsDiv);
const hintsDiv = buildHintsDiv(game, settingData.games[game].gameItems, settingData.games[game].gameLocations);
gameDiv.appendChild(hintsDiv);
gamesWrapper.appendChild(gameDiv);
collapseButton.addEventListener('click', () => {
collapseButton.classList.add('invisible');
optionsDiv.classList.add('invisible');
weightedSettingsDiv.classList.add('invisible');
itemsDiv.classList.add('invisible');
hintsDiv.classList.add('invisible');
expandButton.classList.remove('invisible');
});
expandButton.addEventListener('click', () => {
collapseButton.classList.remove('invisible');
optionsDiv.classList.remove('invisible');
weightedSettingsDiv.classList.remove('invisible');
itemsDiv.classList.remove('invisible');
hintsDiv.classList.remove('invisible');
expandButton.classList.add('invisible');
});
});
@@ -207,10 +224,10 @@ const buildGameChoice = (games) => {
gameChoiceDiv.appendChild(table);
};
const buildOptionsDiv = (game, settings) => {
const buildWeightedSettingsDiv = (game, settings) => {
const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
const optionsWrapper = document.createElement('div');
optionsWrapper.classList.add('settings-wrapper');
const settingsWrapper = document.createElement('div');
settingsWrapper.classList.add('settings-wrapper');
Object.keys(settings).forEach((settingName) => {
const setting = settings[settingName];
@@ -268,27 +285,6 @@ const buildOptionsDiv = (game, settings) => {
break;
case 'range':
const hintText = document.createElement('p');
hintText.classList.add('hint-text');
hintText.innerHTML = 'This is a range option. You may enter valid numerical values in the text box below, ' +
`then press the "Add" button to add a weight for it.<br />Minimum value: ${setting.min}<br />` +
`Maximum value: ${setting.max}`;
settingWrapper.appendChild(hintText);
const addOptionDiv = document.createElement('div');
addOptionDiv.classList.add('add-option-div');
const optionInput = document.createElement('input');
optionInput.setAttribute('id', `${game}-${settingName}-option`);
optionInput.setAttribute('placeholder', `${setting.min} - ${setting.max}`);
addOptionDiv.appendChild(optionInput);
const addOptionButton = document.createElement('button');
addOptionButton.innerText = 'Add';
addOptionDiv.appendChild(addOptionButton);
settingWrapper.appendChild(addOptionDiv);
optionInput.addEventListener('keydown', (evt) => {
if (evt.key === 'Enter') { addOptionButton.dispatchEvent(new Event('click')); }
});
const rangeTable = document.createElement('table');
const rangeTbody = document.createElement('tbody');
@@ -324,6 +320,79 @@ const buildOptionsDiv = (game, settings) => {
rangeTbody.appendChild(tr);
}
} else {
const hintText = document.createElement('p');
hintText.classList.add('hint-text');
hintText.innerHTML = 'This is a range option. You may enter a valid numerical value in the text box ' +
`below, then press the "Add" button to add a weight for it.<br />Minimum value: ${setting.min}<br />` +
`Maximum value: ${setting.max}`;
settingWrapper.appendChild(hintText);
const addOptionDiv = document.createElement('div');
addOptionDiv.classList.add('add-option-div');
const optionInput = document.createElement('input');
optionInput.setAttribute('id', `${game}-${settingName}-option`);
optionInput.setAttribute('placeholder', `${setting.min} - ${setting.max}`);
addOptionDiv.appendChild(optionInput);
const addOptionButton = document.createElement('button');
addOptionButton.innerText = 'Add';
addOptionDiv.appendChild(addOptionButton);
settingWrapper.appendChild(addOptionDiv);
optionInput.addEventListener('keydown', (evt) => {
if (evt.key === 'Enter') { addOptionButton.dispatchEvent(new Event('click')); }
});
addOptionButton.addEventListener('click', () => {
const optionInput = document.getElementById(`${game}-${settingName}-option`);
let option = optionInput.value;
if (!option || !option.trim()) { return; }
option = parseInt(option, 10);
if ((option < setting.min) || (option > setting.max)) { return; }
optionInput.value = '';
if (document.getElementById(`${game}-${settingName}-${option}-range`)) { return; }
const tr = document.createElement('tr');
const tdLeft = document.createElement('td');
tdLeft.classList.add('td-left');
tdLeft.innerText = option;
tr.appendChild(tdLeft);
const tdMiddle = document.createElement('td');
tdMiddle.classList.add('td-middle');
const range = document.createElement('input');
range.setAttribute('type', 'range');
range.setAttribute('id', `${game}-${settingName}-${option}-range`);
range.setAttribute('data-game', game);
range.setAttribute('data-setting', settingName);
range.setAttribute('data-option', option);
range.setAttribute('min', 0);
range.setAttribute('max', 50);
range.addEventListener('change', updateGameSetting);
range.value = currentSettings[game][settingName][parseInt(option, 10)];
tdMiddle.appendChild(range);
tr.appendChild(tdMiddle);
const tdRight = document.createElement('td');
tdRight.setAttribute('id', `${game}-${settingName}-${option}`)
tdRight.classList.add('td-right');
tdRight.innerText = range.value;
tr.appendChild(tdRight);
const tdDelete = document.createElement('td');
tdDelete.classList.add('td-delete');
const deleteButton = document.createElement('span');
deleteButton.classList.add('range-option-delete');
deleteButton.innerText = '❌';
deleteButton.addEventListener('click', () => {
range.value = 0;
range.dispatchEvent(new Event('change'));
rangeTbody.removeChild(tr);
});
tdDelete.appendChild(deleteButton);
tr.appendChild(tdDelete);
rangeTbody.appendChild(tr);
});
Object.keys(currentSettings[game][settingName]).forEach((option) => {
if (currentSettings[game][settingName][option] > 0) {
const tr = document.createElement('tr');
@@ -403,58 +472,18 @@ const buildOptionsDiv = (game, settings) => {
rangeTable.appendChild(rangeTbody);
settingWrapper.appendChild(rangeTable);
break;
addOptionButton.addEventListener('click', () => {
const optionInput = document.getElementById(`${game}-${settingName}-option`);
let option = optionInput.value;
if (!option || !option.trim()) { return; }
option = parseInt(option, 10);
if ((option < setting.min) || (option > setting.max)) { return; }
optionInput.value = '';
if (document.getElementById(`${game}-${settingName}-${option}-range`)) { return; }
case 'items-list':
// TODO
break;
const tr = document.createElement('tr');
const tdLeft = document.createElement('td');
tdLeft.classList.add('td-left');
tdLeft.innerText = option;
tr.appendChild(tdLeft);
case 'locations-list':
// TODO
break;
const tdMiddle = document.createElement('td');
tdMiddle.classList.add('td-middle');
const range = document.createElement('input');
range.setAttribute('type', 'range');
range.setAttribute('id', `${game}-${settingName}-${option}-range`);
range.setAttribute('data-game', game);
range.setAttribute('data-setting', settingName);
range.setAttribute('data-option', option);
range.setAttribute('min', 0);
range.setAttribute('max', 50);
range.addEventListener('change', updateGameSetting);
range.value = currentSettings[game][settingName][parseInt(option, 10)];
tdMiddle.appendChild(range);
tr.appendChild(tdMiddle);
const tdRight = document.createElement('td');
tdRight.setAttribute('id', `${game}-${settingName}-${option}`)
tdRight.classList.add('td-right');
tdRight.innerText = range.value;
tr.appendChild(tdRight);
const tdDelete = document.createElement('td');
tdDelete.classList.add('td-delete');
const deleteButton = document.createElement('span');
deleteButton.classList.add('range-option-delete');
deleteButton.innerText = '❌';
deleteButton.addEventListener('click', () => {
range.value = 0;
range.dispatchEvent(new Event('change'));
rangeTbody.removeChild(tr);
});
tdDelete.appendChild(deleteButton);
tr.appendChild(tdDelete);
rangeTbody.appendChild(tr);
});
case 'custom-list':
// TODO
break;
default:
@@ -462,10 +491,358 @@ const buildOptionsDiv = (game, settings) => {
return;
}
optionsWrapper.appendChild(settingWrapper);
settingsWrapper.appendChild(settingWrapper);
});
return optionsWrapper;
return settingsWrapper;
};
const buildItemsDiv = (game, items) => {
// Sort alphabetical, in pace
items.sort();
const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
const itemsDiv = document.createElement('div');
itemsDiv.classList.add('items-div');
const itemsDivHeader = document.createElement('h3');
itemsDivHeader.innerText = 'Item Pool';
itemsDiv.appendChild(itemsDivHeader);
const itemsDescription = document.createElement('p');
itemsDescription.classList.add('setting-description');
itemsDescription.innerText = 'Choose if you would like to start with items, or control if they are placed in ' +
'your seed or someone else\'s.';
itemsDiv.appendChild(itemsDescription);
const itemsHint = document.createElement('p');
itemsHint.classList.add('hint-text');
itemsHint.innerText = 'Drag and drop items from one box to another.';
itemsDiv.appendChild(itemsHint);
const itemsWrapper = document.createElement('div');
itemsWrapper.classList.add('items-wrapper');
// Create container divs for each category
const availableItemsWrapper = document.createElement('div');
availableItemsWrapper.classList.add('item-set-wrapper');
availableItemsWrapper.innerText = 'Available Items';
const availableItems = document.createElement('div');
availableItems.classList.add('item-container');
availableItems.setAttribute('id', `${game}-available_items`);
availableItems.addEventListener('dragover', itemDragoverHandler);
availableItems.addEventListener('drop', itemDropHandler);
const startInventoryWrapper = document.createElement('div');
startInventoryWrapper.classList.add('item-set-wrapper');
startInventoryWrapper.innerText = 'Start Inventory';
const startInventory = document.createElement('div');
startInventory.classList.add('item-container');
startInventory.setAttribute('id', `${game}-start_inventory`);
startInventory.setAttribute('data-setting', 'start_inventory');
startInventory.addEventListener('dragover', itemDragoverHandler);
startInventory.addEventListener('drop', itemDropHandler);
const localItemsWrapper = document.createElement('div');
localItemsWrapper.classList.add('item-set-wrapper');
localItemsWrapper.innerText = 'Local Items';
const localItems = document.createElement('div');
localItems.classList.add('item-container');
localItems.setAttribute('id', `${game}-local_items`);
localItems.setAttribute('data-setting', 'local_items')
localItems.addEventListener('dragover', itemDragoverHandler);
localItems.addEventListener('drop', itemDropHandler);
const nonLocalItemsWrapper = document.createElement('div');
nonLocalItemsWrapper.classList.add('item-set-wrapper');
nonLocalItemsWrapper.innerText = 'Non-Local Items';
const nonLocalItems = document.createElement('div');
nonLocalItems.classList.add('item-container');
nonLocalItems.setAttribute('id', `${game}-non_local_items`);
nonLocalItems.setAttribute('data-setting', 'non_local_items');
nonLocalItems.addEventListener('dragover', itemDragoverHandler);
nonLocalItems.addEventListener('drop', itemDropHandler);
// Populate the divs
items.forEach((item) => {
if (Object.keys(currentSettings[game].start_inventory).includes(item)){
const itemDiv = buildItemQtyDiv(game, item);
itemDiv.setAttribute('data-setting', 'start_inventory');
startInventory.appendChild(itemDiv);
} else if (currentSettings[game].local_items.includes(item)) {
const itemDiv = buildItemDiv(game, item);
itemDiv.setAttribute('data-setting', 'local_items');
localItems.appendChild(itemDiv);
} else if (currentSettings[game].non_local_items.includes(item)) {
const itemDiv = buildItemDiv(game, item);
itemDiv.setAttribute('data-setting', 'non_local_items');
nonLocalItems.appendChild(itemDiv);
} else {
const itemDiv = buildItemDiv(game, item);
availableItems.appendChild(itemDiv);
}
});
availableItemsWrapper.appendChild(availableItems);
startInventoryWrapper.appendChild(startInventory);
localItemsWrapper.appendChild(localItems);
nonLocalItemsWrapper.appendChild(nonLocalItems);
itemsWrapper.appendChild(availableItemsWrapper);
itemsWrapper.appendChild(startInventoryWrapper);
itemsWrapper.appendChild(localItemsWrapper);
itemsWrapper.appendChild(nonLocalItemsWrapper);
itemsDiv.appendChild(itemsWrapper);
return itemsDiv;
};
const buildItemDiv = (game, item) => {
const itemDiv = document.createElement('div');
itemDiv.classList.add('item-div');
itemDiv.setAttribute('id', `${game}-${item}`);
itemDiv.setAttribute('data-game', game);
itemDiv.setAttribute('data-item', item);
itemDiv.setAttribute('draggable', 'true');
itemDiv.innerText = item;
itemDiv.addEventListener('dragstart', (evt) => {
evt.dataTransfer.setData('text/plain', itemDiv.getAttribute('id'));
});
return itemDiv;
};
const buildItemQtyDiv = (game, item) => {
const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
const itemQtyDiv = document.createElement('div');
itemQtyDiv.classList.add('item-qty-div');
itemQtyDiv.setAttribute('id', `${game}-${item}`);
itemQtyDiv.setAttribute('data-game', game);
itemQtyDiv.setAttribute('data-item', item);
itemQtyDiv.setAttribute('draggable', 'true');
itemQtyDiv.innerText = item;
const inputWrapper = document.createElement('div');
inputWrapper.classList.add('item-qty-input-wrapper')
const itemQty = document.createElement('input');
itemQty.setAttribute('value', currentSettings[game].start_inventory.hasOwnProperty(item) ?
currentSettings[game].start_inventory[item] : '1');
itemQty.setAttribute('data-game', game);
itemQty.setAttribute('data-setting', 'start_inventory');
itemQty.setAttribute('data-option', item);
itemQty.setAttribute('maxlength', '3');
itemQty.addEventListener('keyup', (evt) => {
evt.target.value = isNaN(parseInt(evt.target.value)) ? 0 : parseInt(evt.target.value);
updateItemSetting(evt);
});
inputWrapper.appendChild(itemQty);
itemQtyDiv.appendChild(inputWrapper);
itemQtyDiv.addEventListener('dragstart', (evt) => {
evt.dataTransfer.setData('text/plain', itemQtyDiv.getAttribute('id'));
});
return itemQtyDiv;
};
const itemDragoverHandler = (evt) => {
evt.preventDefault();
};
const itemDropHandler = (evt) => {
evt.preventDefault();
const sourceId = evt.dataTransfer.getData('text/plain');
const sourceDiv = document.getElementById(sourceId);
const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
const game = sourceDiv.getAttribute('data-game');
const item = sourceDiv.getAttribute('data-item');
const oldSetting = sourceDiv.hasAttribute('data-setting') ? sourceDiv.getAttribute('data-setting') : null;
const newSetting = evt.target.hasAttribute('data-setting') ? evt.target.getAttribute('data-setting') : null;
const itemDiv = newSetting === 'start_inventory' ? buildItemQtyDiv(game, item) : buildItemDiv(game, item);
if (oldSetting) {
if (oldSetting === 'start_inventory') {
if (currentSettings[game][oldSetting].hasOwnProperty(item)) {
delete currentSettings[game][oldSetting][item];
}
} else {
if (currentSettings[game][oldSetting].includes(item)) {
currentSettings[game][oldSetting].splice(currentSettings[game][oldSetting].indexOf(item), 1);
}
}
}
if (newSetting) {
itemDiv.setAttribute('data-setting', newSetting);
document.getElementById(`${game}-${newSetting}`).appendChild(itemDiv);
if (newSetting === 'start_inventory') {
currentSettings[game][newSetting][item] = 1;
} else {
if (!currentSettings[game][newSetting].includes(item)){
currentSettings[game][newSetting].push(item);
}
}
} else {
// No setting was assigned, this item has been removed from the settings
document.getElementById(`${game}-available_items`).appendChild(itemDiv);
}
// Remove the source drag object
sourceDiv.parentElement.removeChild(sourceDiv);
// Save the updated settings
localStorage.setItem('weighted-settings', JSON.stringify(currentSettings));
};
const buildHintsDiv = (game, items, locations) => {
const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
// Sort alphabetical, in place
items.sort();
locations.sort();
const hintsDiv = document.createElement('div');
hintsDiv.classList.add('hints-div');
const hintsHeader = document.createElement('h3');
hintsHeader.innerText = 'Item & Location Hints';
hintsDiv.appendChild(hintsHeader);
const hintsDescription = document.createElement('p');
hintsDescription.classList.add('setting-description');
hintsDescription.innerText = 'Choose any items or locations to begin the game with the knowledge of where those ' +
' items are, or what those locations contain. Excluded locations will not contain progression items.';
hintsDiv.appendChild(hintsDescription);
const itemHintsContainer = document.createElement('div');
itemHintsContainer.classList.add('hints-container');
const itemHintsWrapper = document.createElement('div');
itemHintsWrapper.classList.add('hints-wrapper');
itemHintsWrapper.innerText = 'Starting Item Hints';
const itemHintsDiv = document.createElement('div');
itemHintsDiv.classList.add('item-container');
items.forEach((item) => {
const itemDiv = document.createElement('div');
itemDiv.classList.add('hint-div');
const itemLabel = document.createElement('label');
itemLabel.setAttribute('for', `${game}-start_hints-${item}`);
const itemCheckbox = document.createElement('input');
itemCheckbox.setAttribute('type', 'checkbox');
itemCheckbox.setAttribute('id', `${game}-start_hints-${item}`);
itemCheckbox.setAttribute('data-game', game);
itemCheckbox.setAttribute('data-setting', 'start_hints');
itemCheckbox.setAttribute('data-option', item);
if (currentSettings[game].start_hints.includes(item)) {
itemCheckbox.setAttribute('checked', 'true');
}
itemCheckbox.addEventListener('change', hintChangeHandler);
itemLabel.appendChild(itemCheckbox);
const itemName = document.createElement('span');
itemName.innerText = item;
itemLabel.appendChild(itemName);
itemDiv.appendChild(itemLabel);
itemHintsDiv.appendChild(itemDiv);
});
itemHintsWrapper.appendChild(itemHintsDiv);
itemHintsContainer.appendChild(itemHintsWrapper);
const locationHintsWrapper = document.createElement('div');
locationHintsWrapper.classList.add('hints-wrapper');
locationHintsWrapper.innerText = 'Starting Location Hints';
const locationHintsDiv = document.createElement('div');
locationHintsDiv.classList.add('item-container');
locations.forEach((location) => {
const locationDiv = document.createElement('div');
locationDiv.classList.add('hint-div');
const locationLabel = document.createElement('label');
locationLabel.setAttribute('for', `${game}-start_location_hints-${location}`);
const locationCheckbox = document.createElement('input');
locationCheckbox.setAttribute('type', 'checkbox');
locationCheckbox.setAttribute('id', `${game}-start_location_hints-${location}`);
locationCheckbox.setAttribute('data-game', game);
locationCheckbox.setAttribute('data-setting', 'start_location_hints');
locationCheckbox.setAttribute('data-option', location);
if (currentSettings[game].start_location_hints.includes(location)) {
locationCheckbox.setAttribute('checked', '1');
}
locationCheckbox.addEventListener('change', hintChangeHandler);
locationLabel.appendChild(locationCheckbox);
const locationName = document.createElement('span');
locationName.innerText = location;
locationLabel.appendChild(locationName);
locationDiv.appendChild(locationLabel);
locationHintsDiv.appendChild(locationDiv);
});
locationHintsWrapper.appendChild(locationHintsDiv);
itemHintsContainer.appendChild(locationHintsWrapper);
const excludeLocationsWrapper = document.createElement('div');
excludeLocationsWrapper.classList.add('hints-wrapper');
excludeLocationsWrapper.innerText = 'Exclude Locations';
const excludeLocationsDiv = document.createElement('div');
excludeLocationsDiv.classList.add('item-container');
locations.forEach((location) => {
const locationDiv = document.createElement('div');
locationDiv.classList.add('hint-div');
const locationLabel = document.createElement('label');
locationLabel.setAttribute('for', `${game}-exclude_locations-${location}`);
const locationCheckbox = document.createElement('input');
locationCheckbox.setAttribute('type', 'checkbox');
locationCheckbox.setAttribute('id', `${game}-exclude_locations-${location}`);
locationCheckbox.setAttribute('data-game', game);
locationCheckbox.setAttribute('data-setting', 'exclude_locations');
locationCheckbox.setAttribute('data-option', location);
if (currentSettings[game].exclude_locations.includes(location)) {
locationCheckbox.setAttribute('checked', '1');
}
locationCheckbox.addEventListener('change', hintChangeHandler);
locationLabel.appendChild(locationCheckbox);
const locationName = document.createElement('span');
locationName.innerText = location;
locationLabel.appendChild(locationName);
locationDiv.appendChild(locationLabel);
excludeLocationsDiv.appendChild(locationDiv);
});
excludeLocationsWrapper.appendChild(excludeLocationsDiv);
itemHintsContainer.appendChild(excludeLocationsWrapper);
hintsDiv.appendChild(itemHintsContainer);
return hintsDiv;
};
const hintChangeHandler = (evt) => {
const currentSettings = JSON.parse(localStorage.getItem('weighted-settings'));
const game = evt.target.getAttribute('data-game');
const setting = evt.target.getAttribute('data-setting');
const option = evt.target.getAttribute('data-option');
if (evt.target.checked) {
if (!currentSettings[game][setting].includes(option)) {
currentSettings[game][setting].push(option);
}
} else {
if (currentSettings[game][setting].includes(option)) {
currentSettings[game][setting].splice(currentSettings[game][setting].indexOf(option), 1);
}
}
localStorage.setItem('weighted-settings', JSON.stringify(currentSettings));
};
const updateVisibleGames = () => {
@@ -511,25 +888,44 @@ const updateBaseSetting = (event) => {
localStorage.setItem('weighted-settings', JSON.stringify(settings));
};
const updateGameSetting = (event) => {
const updateGameSetting = (evt) => {
const options = JSON.parse(localStorage.getItem('weighted-settings'));
const game = event.target.getAttribute('data-game');
const setting = event.target.getAttribute('data-setting');
const option = event.target.getAttribute('data-option');
const type = event.target.getAttribute('data-type');
document.getElementById(`${game}-${setting}-${option}`).innerText = event.target.value;
options[game][setting][option] = isNaN(event.target.value) ?
event.target.value : parseInt(event.target.value, 10);
const game = evt.target.getAttribute('data-game');
const setting = evt.target.getAttribute('data-setting');
const option = evt.target.getAttribute('data-option');
document.getElementById(`${game}-${setting}-${option}`).innerText = evt.target.value;
options[game][setting][option] = isNaN(evt.target.value) ?
evt.target.value : parseInt(evt.target.value, 10);
localStorage.setItem('weighted-settings', JSON.stringify(options));
};
const exportSettings = () => {
const updateItemSetting = (evt) => {
const options = JSON.parse(localStorage.getItem('weighted-settings'));
const game = evt.target.getAttribute('data-game');
const setting = evt.target.getAttribute('data-setting');
const option = evt.target.getAttribute('data-option');
if (setting === 'start_inventory') {
options[game][setting][option] = evt.target.value.trim() ? parseInt(evt.target.value) : 0;
} else {
options[game][setting][option] = isNaN(evt.target.value) ?
evt.target.value : parseInt(evt.target.value, 10);
}
localStorage.setItem('weighted-settings', JSON.stringify(options));
};
const validateSettings = () => {
const settings = JSON.parse(localStorage.getItem('weighted-settings'));
const userMessage = document.getElementById('user-message');
let errorMessage = null;
// User must choose a name for their file
if (!settings.name || settings.name.trim().length === 0 || settings.name.toLowerCase().trim() === 'player') {
const userMessage = document.getElementById('user-message');
userMessage.innerText = 'You forgot to set your player name at the top of the page!';
userMessage.classList.add('visible');
window.scrollTo(0, 0);
userMessage.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
return;
}
@@ -549,9 +945,41 @@ const exportSettings = () => {
delete settings[game][setting][option];
}
});
if (
Object.keys(settings[game][setting]).length === 0 &&
!Array.isArray(settings[game][setting]) &&
setting !== 'start_inventory'
) {
errorMessage = `${game} // ${setting} has no values above zero!`;
}
});
});
if (Object.keys(settings.game).length === 0) {
errorMessage = 'You have not chosen a game to play!';
}
// If an error occurred, alert the user and do not export the file
if (errorMessage) {
userMessage.innerText = errorMessage;
userMessage.classList.add('visible');
userMessage.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
return;
}
// If no error occurred, hide the user message if it is visible
userMessage.classList.remove('visible');
return settings;
};
const exportSettings = () => {
const settings = validateSettings();
if (!settings) { return; }
const yamlText = jsyaml.safeDump(settings, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`);
download(`${document.getElementById('player-name').value}.yaml`, yamlText);
};
@@ -568,9 +996,12 @@ const download = (filename, text) => {
};
const generateGame = (raceMode = false) => {
const settings = validateSettings();
if (!settings) { return; }
axios.post('/api/generate', {
weights: { player: localStorage.getItem('weighted-settings') },
presetData: { player: localStorage.getItem('weighted-settings') },
weights: { player: JSON.stringify(settings) },
presetData: { player: JSON.stringify(settings) },
playerCount: 1,
race: raceMode ? '1' : '0',
}).then((response) => {
@@ -582,7 +1013,10 @@ const generateGame = (raceMode = false) => {
userMessage.innerText += ' ' + error.response.data.text;
}
userMessage.classList.add('visible');
window.scrollTo(0, 0);
userMessage.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
console.error(error);
});
};

View File

@@ -1,4 +1,4 @@
Copyright 2020 Berserker66 (Fabian Dill)
Copyright 2020 LegendaryLinux (Chris Wilson)
Copyright 2022 Berserker66 (Fabian Dill)
Copyright 2022 LegendaryLinux (Chris Wilson)
All rights reserved.

View File

@@ -0,0 +1,4 @@
Copyright 2022 Berserker66 (Fabian Dill)
Copyright 2022 LegendaryLinux (Chris Wilson)
All rights reserved.

View File

@@ -0,0 +1,4 @@
Copyright 2022 Berserker66 (Fabian Dill)
Copyright 2022 LegendaryLinux (Chris Wilson)
All rights reserved.

View File

@@ -41,6 +41,7 @@ html{
#player-settings #user-message.visible{
display: block;
cursor: pointer;
}
#player-settings h1{

View File

@@ -54,7 +54,7 @@
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
background-color: #546E7A;
color: #000000;
color: #ffffff;
padding: 0 3px 3px;
font-size: 14px;
cursor: default;
@@ -102,3 +102,7 @@
.hide {
display: none;
}
body {
background-color: #000000;
}

View File

@@ -90,6 +90,102 @@ html{
cursor: pointer;
}
#weighted-settings .items-wrapper{
display: flex;
flex-direction: row;
justify-content: space-between;
}
#weighted-settings .items-div h3{
margin-bottom: 0.5rem;
}
#weighted-settings .items-wrapper .item-set-wrapper{
width: 24%;
font-weight: bold;
}
#weighted-settings .item-container{
border: 1px solid #ffffff;
border-radius: 2px;
width: 100%;
height: 300px;
overflow-y: auto;
overflow-x: hidden;
margin-top: 0.125rem;
font-weight: normal;
}
#weighted-settings .item-container .item-div{
padding: 0.125rem 0.5rem;
cursor: pointer;
}
#weighted-settings .item-container .item-div:hover{
background-color: rgba(0, 0, 0, 0.1);
}
#weighted-settings .item-container .item-qty-div{
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 0.125rem 0.5rem;
cursor: pointer;
}
#weighted-settings .item-container .item-qty-div .item-qty-input-wrapper{
display: flex;
flex-direction: column;
justify-content: space-around;
}
#weighted-settings .item-container .item-qty-div input{
min-width: unset;
width: 1.5rem;
text-align: center;
}
#weighted-settings .item-container .item-qty-div:hover{
background-color: rgba(0, 0, 0, 0.1);
}
#weighted-settings .hints-div{
margin-top: 2rem;
}
#weighted-settings .hints-div h3{
margin-bottom: 0.5rem;
}
#weighted-settings .hints-div .hints-container{
display: flex;
flex-direction: row;
justify-content: space-between;
font-weight: bold;
}
#weighted-settings .hints-div .hints-wrapper{
width: 32.5%;
}
#weighted-settings .hints-div .hints-wrapper .hint-div{
display: flex;
flex-direction: row;
cursor: pointer;
user-select: none;
-moz-user-select: none;
}
#weighted-settings .hints-div .hints-wrapper .hint-div:hover{
background-color: rgba(0, 0, 0, 0.1);
}
#weighted-settings .hints-div .hints-wrapper .hint-div label{
flex-grow: 1;
padding: 0.125rem 0.5rem;
cursor: pointer;
}
#weighted-settings #weighted-settings-button-row{
display: flex;
flex-direction: row;

View File

@@ -43,7 +43,6 @@
<td><img src="{{ icons['Fishing Rod'] }}" class="{{ 'acquired' if 'Fishing Rod' in acquired_items }}" title="Fishing Rod" /></td>
<td><img src="{{ icons['Campfire'] }}" class="{{ 'acquired' if 'Campfire' in acquired_items }}" title="Campfire" /></td>
<td><img src="{{ icons['Spyglass'] }}" class="{{ 'acquired' if 'Spyglass' in acquired_items }}" title="Spyglass" /></td>
<td><img src="{{ icons['Dragon Head'] }}" class="{{ 'acquired' if game_finished }}" title="Ender Dragon" /></td>
</tr>
</table>
<table id="location-table">

View File

@@ -18,10 +18,13 @@
or download a settings file you can use to participate in a MultiWorld.</p>
<p>
A list of all games you have generated can be found <a href="/user-content">here</a>.
A more advanced settings configuration for all games can be found on the
<a href="/weighted-settings">Weighted Settings</a> page.
<br />
Advanced users can download a template file for this game
<a href="/static/generated/configs/{{ game }}.yaml">here</a>.
A list of all games you have generated can be found on the <a href="/user-content">User Content Page</a>.
<br />
You may also download the
<a href="/static/generated/configs/{{ game }}.yaml">template file for this game</a>.
</p>
<p><label for="player-name">Please enter your player name. This will appear in-game as you send and receive

View File

@@ -14,7 +14,7 @@
<h1>Option Templates (YAML)</h1>
<ul>
{% for file in files %}
<li><a href="{{ url_for('static', filename="generated/"+file+".yaml") }}">{{ file }}</a></li>
<li><a href="{{ url_for('static', filename="generated/configs/"+file+".yaml") }}">{{ file }}</a></li>
{% endfor %}
</ul>
</div>

View File

@@ -17,7 +17,7 @@
<td><img src="{{ icons['Timespinner Gear 3'] }}" class="{{ 'acquired' if 'Timespinner Gear 3' in acquired_items }}" title="Timespinner Gear 3" /></td>
</tr>
<tr>
<td><img src="{{ icons['Talaria Attachment'] }}" class="{{ 'acquired' if 'Talaria Attachment' in acquired_items }}" title="Talaria Attachment" /></td>
<td><img src="{{ icons['Talaria Attachment'] }}" class="{{ 'acquired' if 'Talaria Attachment' in acquired_items or 'QuickSeed' in options }}" title="Talaria Attachment" /></td>
<td><img src="{{ icons['Succubus Hairpin'] }}" class="{{ 'acquired' if 'Succubus Hairpin' in acquired_items }}" title="Succubus Hairpin" /></td>
<td><img src="{{ icons['Lightwall'] }}" class="{{ 'acquired' if 'Lightwall' in acquired_items }}" title="Lightwall" /></td>
<td><img src="{{ icons['Celestial Sash'] }}" class="{{ 'acquired' if 'Celestial Sash' in acquired_items }}" title="Celestial Sash" /></td>
@@ -28,18 +28,35 @@
<td><img src="{{ icons['Security Keycard B'] }}" class="{{ 'acquired' if 'Security Keycard B' in acquired_items }}" title="Security Keycard B" /></td>
<td><img src="{{ icons['Security Keycard C'] }}" class="{{ 'acquired' if 'Security Keycard C' in acquired_items }}" title="Security Keycard C" /></td>
<td><img src="{{ icons['Security Keycard D'] }}" class="{{ 'acquired' if 'Security Keycard D' in acquired_items }}" title="Security Keycard D" /></td>
<td><img src="{{ icons['Library Keycard V'] }}" class="{{ 'acquired' if 'Library Keycard V' in acquired_items }}" title="Library Keycard V" /></td>
{% if 'DownloadableItems' in options %}
<td><img src="{{ icons['Library Keycard V'] }}" class="{{ 'acquired' if 'Library Keycard V' in acquired_items }}" title="Library Keycard V" /></td>
{% else %}
<td></td>
{% endif %}
</tr>
<tr>
<td><img src="{{ icons['Tablet'] }}" class="{{ 'acquired' if 'Tablet' in acquired_items }}" title="Tablet" /></td>
{% if 'DownloadableItems' in options %}
<td><img src="{{ icons['Tablet'] }}" class="{{ 'acquired' if 'Tablet' in acquired_items }}" title="Tablet" /></td>
{% else %}
<td></td>
{% endif %}
<td><img src="{{ icons['Elevator Keycard'] }}" class="{{ 'acquired' if 'Elevator Keycard' in acquired_items }}" title="Elevator Keycard" /></td>
<td><img src="{{ icons['Oculus Ring'] }}" class="{{ 'acquired' if 'Oculus Ring' in acquired_items }}" title="Oculus Ring" /></td>
{% if 'FacebookMode' in options %}
<td><img src="{{ icons['Oculus Ring'] }}" class="{{ 'acquired' if 'Oculus Ring' in acquired_items }}" title="Oculus Ring" /></td>
{% else %}
<td></td>
{% endif %}
<td><img src="{{ icons['Water Mask'] }}" class="{{ 'acquired' if 'Water Mask' in acquired_items }}" title="Water Mask" /></td>
<td><img src="{{ icons['Gas Mask'] }}" class="{{ 'acquired' if 'Gas Mask' in acquired_items }}" title="Gas Mask" /></td>
</tr>
<tr>
<td><img src="{{ icons['Kobo'] }}" class="{{ 'acquired' if 'Kobo' in acquired_items }}" title="Kobo" /></td>
<td><img src="{{ icons['Merchant Crow'] }}" class="{{ 'acquired' if 'Merchant Crow' in acquired_items }}" title="Merchant Crow" /></td>
{% if 'GyreArchives' in options %}
<td><img src="{{ icons['Kobo'] }}" class="{{ 'acquired' if 'Kobo' in acquired_items }}" title="Kobo" /></td>
<td><img src="{{ icons['Merchant Crow'] }}" class="{{ 'acquired' if 'Merchant Crow' in acquired_items }}" title="Merchant Crow" /></td>
{% else %}
<td></td>
<td></td>
{% endif %}
{% if 'Djinn Inferno' in acquired_items %}
<td><img src="{{ icons['Djinn Inferno'] }}" class="acquired" title="Djinn Inferno" /></td>

View File

@@ -31,7 +31,7 @@
<tr>
<td><a href="{{ url_for("view_seed", seed=room.seed.id) }}">{{ room.seed.id|suuid }}</a></td>
<td><a href="{{ url_for("host_room", room=room.id) }}">{{ room.id|suuid }}</a></td>
<td>>={{ room.seed.slots|length }}</td>
<td>{{ room.seed.slots|length }}</td>
<td>{{ room.creation_time.strftime("%Y-%m-%d %H:%M") }}</td>
<td>{{ room.last_activity.strftime("%Y-%m-%d %H:%M") }}</td>
</tr>
@@ -56,7 +56,7 @@
{% for seed in seeds %}
<tr>
<td><a href="{{ url_for("view_seed", seed=seed.id) }}">{{ seed.id|suuid }}</a></td>
<td>{% if seed.multidata %}>={{ seed.slots|length }}{% else %}1{% endif %}
<td>{% if seed.multidata %}{{ seed.slots|length }}{% else %}1{% endif %}
</td>
<td>{{ seed.creation_time.strftime("%Y-%m-%d %H:%M") }}</td>
</tr>

View File

@@ -22,6 +22,10 @@
<td>Created:&nbsp;</td>
<td id="creation-time" data-creation-time="{{ seed.creation_time }}"></td>
</tr>
<tr>
<td>Players:&nbsp;</td>
<td>{{ slot_count }}</td>
</tr>
{% if seed.spoiler %}
<tr>
<td>Spoiler:&nbsp;</td>
@@ -31,12 +35,16 @@
<tr>
<td>Rooms:&nbsp;</td>
<td>
{% call macros.list_rooms(rooms) %}
{% call macros.list_rooms(seed.rooms | selectattr("owner", "eq", session["_id"])) %}
<li>
<a href="{{ url_for("new_room", seed=seed.id) }}">Create New Room</a>
</li>
{% endcall %}
{% if seed.rooms %}<span>
There are a total of {{ seed.rooms | length }} Rooms, only those created by you are linked above.
</span>{% endif %}
</td>
</tr>
</tbody>
</table>

View File

@@ -14,10 +14,15 @@
<div id="weighted-settings" data-game="{{ game }}">
<div id="user-message"></div>
<h1>Weighted Settings</h1>
<p>Weighted Settings allows you to choose how likely a particular option is to be used in game generation.
The higher an option is weighted, the more likely the option will be chosen. Think of them like
entries in a raffle.</p>
<p>Choose the games and options you would like to play with! You may generate a single-player game from
this page, or download a settings file you can use to participate in a MultiWorld.</p>
<p>A list of all games you have generated can be found <a href="/user-content">here</a>.</p>
<p>A list of all games you have generated can be found on the <a href="/user-content">User Content</a>
page.</p>
<p><label for="player-name">Please enter your player name. This will appear in-game as you send and receive
items if you are playing in a MultiWorld.</label><br />

View File

@@ -254,7 +254,7 @@ def get_static_room_data(room: Room):
return result
multidata = Context.decompress(room.seed.multidata)
# in > 100 players this can take a bit of time and is the main reason for the cache
locations: Dict[int, Dict[int, Tuple[int, int]]] = multidata['locations']
locations: Dict[int, Dict[int, Tuple[int, int, int]]] = multidata['locations']
names: Dict[int, Dict[int, str]] = multidata["names"]
seed_checks_in_area = checks_in_area.copy()
@@ -274,7 +274,7 @@ def get_static_room_data(room: Room):
for playernumber in range(1, len(names[0]) + 1)}
result = locations, names, use_door_tracker, player_checks_in_area, player_location_to_area, \
multidata["precollected_items"], multidata["games"]
multidata["precollected_items"], multidata["games"], multidata["slot_data"]
_multidata_cache[room.seed.id] = result
return result
@@ -292,7 +292,7 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int, want
# Collect seed information and pare it down to a single player
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \
precollected_items, games = get_static_room_data(room)
precollected_items, games, slot_data = get_static_room_data(room)
player_name = names[tracked_team][tracked_player - 1]
location_to_area = player_location_to_area[tracked_player]
inventory = collections.Counter()
@@ -317,7 +317,10 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int, want
# If the player does not have the item, do nothing
for location in locations_checked:
if location in player_locations:
item, recipient = player_locations[location]
if len(player_locations[location]) == 3:
item, recipient, flags = player_locations[location]
else: # TODO: remove around version 0.2.5
item, recipient = player_locations[location]
if recipient == tracked_player: # a check done for the tracked player
attribute_item_solo(inventory, item)
if ms_player == tracked_player: # a check done by the tracked player
@@ -326,7 +329,7 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int, want
specific_tracker = game_specific_trackers.get(games[tracked_player], None)
if specific_tracker and not want_generic:
return specific_tracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name,
seed_checks_in_area, checks_done)
seed_checks_in_area, checks_done, slot_data[tracked_player])
else:
return __renderGenericTracker(multisave, room, locations, inventory, tracked_team, tracked_player, player_name,
seed_checks_in_area, checks_done)
@@ -337,9 +340,9 @@ def get_generic_tracker(tracker: UUID, tracked_team: int, tracked_player: int):
return getPlayerTracker(tracker, tracked_team, tracked_player, True)
def __renderAlttpTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int]]],
def __renderAlttpTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
inventory: Counter, team: int, player: int, player_name: str,
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int]) -> str:
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict) -> str:
# Note the presence of the triforce item
game_state = multisave.get("client_game_state", {}).get((team, player), 0)
@@ -381,7 +384,11 @@ def __renderAlttpTracker(multisave: Dict[str, Any], room: Room, locations: Dict[
player_big_key_locations = set()
player_small_key_locations = set()
for loc_data in locations.values():
for item_id, item_player in loc_data.values():
for values in loc_data.values():
if len(values) == 3:
item_id, item_player, flags = values
else: # TODO: remove around version 0.2.5
item_id, item_player = values
if item_player == player:
if item_id in ids_big_key:
player_big_key_locations.add(ids_big_key[item_id])
@@ -398,9 +405,9 @@ def __renderAlttpTracker(multisave: Dict[str, Any], room: Room, locations: Dict[
**display_data)
def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int]]],
def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
inventory: Counter, team: int, player: int, playerName: str,
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int]) -> str:
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict) -> str:
icons = {
"Wooden Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d2/Wooden_Pickaxe_JE3_BE3.png",
@@ -421,7 +428,7 @@ def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: D
"Bucket": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/f/fc/Bucket_JE2_BE2.png",
"Bow": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/a/ab/Bow_%28Pull_2%29_JE1_BE1.png",
"Shield": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c6/Shield_JE2_BE1.png",
"Red Bed": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/dc/Red_Bed_JE4_BE3.png",
"Red Bed": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/6/6a/Red_Bed_%28N%29.png",
"Netherite Scrap": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/33/Netherite_Scrap_JE2_BE1.png",
"Flint and Steel": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/94/Flint_and_Steel_JE4_BE2.png",
"Enchanting Table": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/3/31/Enchanting_Table.gif",
@@ -429,7 +436,6 @@ def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: D
"Campfire": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/9/91/Campfire_JE2_BE2.gif",
"Water Bottle": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/7/75/Water_Bottle_JE2_BE2.png",
"Spyglass": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c1/Spyglass_JE2_BE1.png",
"Dragon Head": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/b/b6/Dragon_Head.png",
}
minecraft_location_ids = {
@@ -501,9 +507,9 @@ def __renderMinecraftTracker(multisave: Dict[str, Any], room: Room, locations: D
**display_data)
def __renderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int]]],
def __renderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
inventory: Counter, team: int, player: int, playerName: str,
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int]) -> str:
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict) -> str:
icons = {
"Fairy Ocarina": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/OoT_Fairy_Ocarina_Icon.png",
@@ -687,9 +693,9 @@ def __renderOoTTracker(multisave: Dict[str, Any], room: Room, locations: Dict[in
**display_data)
def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int]]],
def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
inventory: Counter, team: int, player: int, playerName: str,
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int]) -> str:
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict[str, Any]) -> str:
icons = {
"Timespinner Wheel": "https://timespinnerwiki.com/mediawiki/images/7/76/Timespinner_Wheel.png",
@@ -733,11 +739,9 @@ def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations:
1337050, 1337051, 1337052, 1337053, 1337054, 1337055, 1337056, 1337057, 1337058, 1337059,
1337060, 1337061, 1337062, 1337063, 1337064, 1337065, 1337066, 1337067, 1337068, 1337069,
1337070, 1337071, 1337072, 1337073, 1337074, 1337075, 1337076, 1337077, 1337078, 1337079,
1337080, 1337081, 1337082, 1337083, 1337084, 1337085, 1337156, 1337157, 1337159,
1337160, 1337161, 1337162, 1337163, 1337164, 1337165, 1337166, 1337167, 1337168, 1337169,
1337170, 1337237, 1337238],
1337080, 1337081, 1337082, 1337083, 1337084, 1337085],
"Past": [
1337086, 1337087, 1337088, 1337089,
1337086, 1337087, 1337088, 1337089,
1337090, 1337091, 1337092, 1337093, 1337094, 1337095, 1337096, 1337097, 1337098, 1337099,
1337100, 1337101, 1337102, 1337103, 1337104, 1337105, 1337106, 1337107, 1337108, 1337109,
1337110, 1337111, 1337112, 1337113, 1337114, 1337115, 1337116, 1337117, 1337118, 1337119,
@@ -745,10 +749,31 @@ def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations:
1337130, 1337131, 1337132, 1337133, 1337134, 1337135, 1337136, 1337137, 1337138, 1337139,
1337140, 1337141, 1337142, 1337143, 1337144, 1337145, 1337146, 1337147, 1337148, 1337149,
1337150, 1337151, 1337152, 1337153, 1337154, 1337155,
1337171, 1337172, 1337173, 1337174, 1337175, 1337176],
"Ancient Pyramid": [1337246, 1337247, 1337248, 1337249]
1337171, 1337172, 1337173, 1337174, 1337175],
"Ancient Pyramid": [
1337236,
1337246, 1337247, 1337248, 1337249]
}
if(slot_data["DownloadableItems"]):
timespinner_location_ids["Present"] += [
1337156, 1337157, 1337159,
1337160, 1337161, 1337162, 1337163, 1337164, 1337165, 1337166, 1337167, 1337168, 1337169,
1337170]
if(slot_data["Cantoran"]):
timespinner_location_ids["Past"].append(1337176)
if(slot_data["LoreChecks"]):
timespinner_location_ids["Present"] += [
1337177, 1337178, 1337179,
1337180, 1337181, 1337182, 1337183, 1337184, 1337185, 1337186, 1337187]
timespinner_location_ids["Past"] += [
1337188, 1337189,
1337190, 1337191, 1337192, 1337193, 1337194, 1337195, 1337196, 1337197, 1337198]
if(slot_data["GyreArchives"]):
timespinner_location_ids["Ancient Pyramid"] += [
1337237, 1337238, 1337239,
1337240, 1337241, 1337242, 1337243, 1337244, 1337245]
display_data = {}
# Victory condition
@@ -765,18 +790,18 @@ def __renderTimespinnerTracker(multisave: Dict[str, Any], room: Room, locations:
checks_done['Total'] = len(checked_locations)
checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in timespinner_location_ids.items()}
checks_in_area['Total'] = sum(checks_in_area.values())
acquired_items = {lookup_any_item_id_to_name[id] for id in inventory if id in lookup_any_item_id_to_name}
options = {k for k, v in slot_data.items() if v}
return render_template("timespinnerTracker.html",
inventory=inventory, icons=icons,
acquired_items={lookup_any_item_id_to_name[id] for id in inventory if
id in lookup_any_item_id_to_name},
inventory=inventory, icons=icons, acquired_items=acquired_items,
player=player, team=team, room=room, player_name=playerName,
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
**display_data)
options=options, **display_data)
def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int]]],
def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
inventory: Counter, team: int, player: int, playerName: str,
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int]) -> str:
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int], slot_data: Dict) -> str:
icons = {
"Energy Tank": "https://randommetroidsolver.pythonanywhere.com/solver/static/images/ETank.png",
@@ -875,17 +900,19 @@ def __renderSuperMetroidTracker(multisave: Dict[str, Any], room: Room, locations
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
**display_data)
def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int]]],
def __renderGenericTracker(multisave: Dict[str, Any], room: Room, locations: Dict[int, Dict[int, Tuple[int, int, int]]],
inventory: Counter, team: int, player: int, playerName: str,
seed_checks_in_area: Dict[int, Dict[str, int]], checks_done: Dict[str, int]) -> str:
checked_locations = multisave.get("location_checks", {}).get((team, player), set())
player_received_items = {}
if multisave.get('version', 0) > 0:
# add numbering to all items but starter_inventory
ordered_items = multisave.get('received_items', {}).get((team, player, True), [])
else:
ordered_items = multisave.get('received_items', {}).get((team, player), [])
for order_index, networkItem in enumerate(
multisave.get('received_items', {}).get((team, player), []),
start=1
):
for order_index, networkItem in enumerate(ordered_items, start=1):
player_received_items[networkItem.item] = order_index
return render_template("genericTracker.html",
@@ -903,7 +930,7 @@ def getTracker(tracker: UUID):
if not room:
abort(404)
locations, names, use_door_tracker, seed_checks_in_area, player_location_to_area, \
precollected_items, games = get_static_room_data(room)
precollected_items, games, slot_data = get_static_room_data(room)
inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1)}
for teamnumber, team in enumerate(names)}
@@ -931,7 +958,11 @@ def getTracker(tracker: UUID):
if location not in player_locations or location not in player_location_to_area[player]:
continue
item, recipient = player_locations[location]
if len(player_locations[location]) == 3:
item, recipient, flags = player_locations[location]
else: # TODO: remove around version 0.2.5
item, recipient = player_locations[location]
attribute_item(inventory, team, recipient, item)
checks_done[team][player][player_location_to_area[player][location]] += 1
checks_done[team][player]["Total"] += 1
@@ -943,12 +974,16 @@ def getTracker(tracker: UUID):
player_big_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)}
player_small_key_locations = {playernumber: set() for playernumber in range(1, len(names[0]) + 1)}
for loc_data in locations.values():
for item_id, item_player in loc_data.values():
for values in loc_data.values():
if len(values) == 3:
item_id, item_player, flags = values
else: # TODO: remove around version 0.2.5
item_id, item_player = values
if item_id in ids_big_key:
player_big_key_locations[item_player].add(ids_big_key[item_id])
elif item_id in ids_small_key:
player_small_key_locations[item_player].add(ids_small_key[item_id])
group_big_key_locations = set()
group_key_locations = set()
for player in range(1, len(names[0]) + 1):

View File

@@ -9,7 +9,7 @@ from flask import request, flash, redirect, url_for, session, render_template
from pony.orm import flush, select
from WebHostLib import app, Seed, Room, Slot
from Utils import parse_yaml
from Utils import parse_yaml, VersionException
from Patch import preferred_endings
banned_zip_contents = (".sfc",)
@@ -102,18 +102,23 @@ def uploads():
elif file and allowed_file(file.filename):
if zipfile.is_zipfile(file):
with zipfile.ZipFile(file, 'r') as zfile:
res = upload_zip_to_db(zfile)
if type(res) == str:
return res
elif res:
return redirect(url_for("view_seed", seed=res.id))
try:
res = upload_zip_to_db(zfile)
except VersionException:
flash(f"Could not load multidata. Wrong Version detected.")
else:
if type(res) == str:
return res
elif res:
return redirect(url_for("view_seed", seed=res.id))
else:
file.seek(0) # offset from is_zipfile check
# noinspection PyBroadException
try:
multidata = file.read()
MultiServer.Context.decompress(multidata)
except:
flash("Could not load multidata. File may be corrupted or incompatible.")
except Exception as e:
flash(f"Could not load multidata. File may be corrupted or incompatible. ({e})")
else:
seed = Seed(multidata=multidata, owner=session["_id"])
flush() # place into DB and generate ids

View File

@@ -99,13 +99,14 @@ Sent to clients when the server refuses connection. This is sent during the init
#### Arguments
| Name | Type | Notes |
| ---- | ---- | ----- |
| errors | list\[str\] | Optional. When provided, should contain any one of: `InvalidSlot`, `InvalidGame`, `SlotAlreadyTaken`, `IncompatibleVersion`, or `InvalidPassword`. |
| errors | list\[str\] | Optional. When provided, should contain any one of: `InvalidSlot`, `InvalidGame`, `SlotAlreadyTaken`, `IncompatibleVersion`, `InvalidPassword`, or `InvalidItemsHandling`. |
InvalidSlot indicates that the sent 'name' field did not match any auth entry on the server.
InvalidGame indicates that a correctly named slot was found, but the game for it mismatched.
SlotAlreadyTaken indicates a connection with a different uuid is already established.
IncompatibleVersion indicates a version mismatch.
InvalidPassword indicates the wrong, or no password when it was required, was sent.
InvalidItemsHandling indicates a wrong value type or flag combination was sent.
### Connected
Sent to clients when the connection handshake is successfully completed.
@@ -164,7 +165,7 @@ Sent to clients purely to display a message to the player. This packet differs f
| data | list\[[JSONMessagePart](#JSONMessagePart)\] | Type of this part of the message. |
| type | str | May be present to indicate the nature of this message. Known types are Hint and ItemSend. |
| receiving | int | Is present if type is Hint or ItemSend and marks the destination player's ID. |
| item | [NetworkItem](#NetworkItem) | Is present if type is Hint or ItemSend and marks the source player id, location id and item id. |
| item | [NetworkItem](#NetworkItem) | Is present if type is Hint or ItemSend and marks the source player id, location id, item id and item flags. |
| found | bool | Is present if type is Hint, denotes whether the location hinted for was checked. |
### DataPackage
@@ -176,12 +177,15 @@ Sent to clients to provide what is known as a 'data package' which contains info
| data | [DataPackageObject](#Data-Package-Contents) | The data package as a JSON object. |
### Bounced
Sent to clients after a client requested this message be sent to them, more info in the Bounce package.
Sent to clients after a client requested this message be sent to them, more info in the [Bounce](#Bounce) package.
#### Arguments
| Name | Type | Notes |
| ---- | ---- | ----- |
| data | dict | The data in the Bounce package copied |
| games | list\[str\] | Optional. Game names this message is targeting |
| slots | list\[int\] | Optional. Player slot IDs that this message is targeting |
| tags | list\[str\] | Optional. Client [Tags](#Tags) this message is targeting |
| data | dict | The data in the [Bounce](#Bounce) package copied |
### InvalidPacket
Sent to clients if the server caught a problem with a packet. This only occurs for errors that are explicitly checked for.
@@ -214,17 +218,28 @@ Sent by the client to initiate a connection to an Archipelago game session.
| name | str | The player name for this client. |
| uuid | str | Unique identifier for player client. |
| version | [NetworkVersion](#NetworkVersion) | An object representing the Archipelago version this client supports. |
| items_handling | int | Flags configuring which items should be sent by the server. Read below for individual flags.
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. [Tags](#Tags) |
#### items_handling flags
| Value | Meaning |
| ----- | ------- |
| 0b000 | No ReceivedItems is sent to you, ever. |
| 0b001 | Indicates you get items sent from other worlds. |
| 0b010 | Indicates you get items sent from your own world. Requires 0b001 to be set. |
| 0b100 | Indicates you get your starting inventory sent. Requires 0b001 to be set. |
| null | Null or undefined loads settings from world definition for backwards compatibility. This is deprecated. |
#### Authentication
Many, if not all, other packets require a successfully authenticated client. This is described in more detail in [Archipelago Connection Handshake](#Archipelago-Connection-Handshake).
### ConnectUpdate
Update arguments from the Connect package, currently only updating tags is supported.
Update arguments from the Connect package, currently only updating tags and items_handling is supported.
#### Arguments
| Name | Type | Notes |
| ---- | ---- | ----- |
| items_handling | int | Flags configuring which items should be sent by the server.
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. [Tags](#Tags) |
### Sync
@@ -329,15 +344,29 @@ class NetworkItem(NamedTuple):
item: int
location: int
player: int
flags: int
```
In JSON this may look like:
```js
[
{"item": 1, "location": 1, "player": 0},
{"item": 2, "location": 2, "player": 0},
{"item": 3, "location": 3, "player": 0}
{"item": 1, "location": 1, "player": 0, "flags": 1},
{"item": 2, "location": 2, "player": 0, "flags": 2},
{"item": 3, "location": 3, "player": 0, "flags": 0}
]
```
`item` is the item id of the item
`location` is the location id of the item inside the world
`player` is the player slot of the world the item is located in
`flags` are bit flags:
| Flag | Meaning |
| ----- | ----- |
| 0 | Nothing special about this item |
| 0b001 | If set, indicates the item can unlock logical advancement |
| 0b010 | If set, indicates the item is important but not in a way that unlocks advancement |
| 0b100 | If set, indicates the item is a trap |
### JSONMessagePart
Message nodes sent along with [PrintJSON](#PrintJSON) packet to be reconstructed into a legible message. The nodes are intended to be read in the order they are listed in the packet.
@@ -346,9 +375,10 @@ Message nodes sent along with [PrintJSON](#PrintJSON) packet to be reconstructed
from typing import TypedDict, Optional
class JSONMessagePart(TypedDict):
type: Optional[str]
color: Optional[str]
text: Optional[str]
player: Optional[int] # marks owning player id for location/item
color: Optional[str] # only available if type is a color
flags: Optional[int] # only available if type is an item_id or item_name
player: Optional[int] # only available if type is either item or location
```
`type` is used to denote the intent of the message part. This can be used to indicate special information which may be rendered differently depending on client. How these types are displayed in Archipelago's ALttP client is not the end-all be-all. Other clients may choose to interpret and display these messages differently.
@@ -362,7 +392,7 @@ Possible values for `type` include:
| item_id | Item ID, should be resolved to Item Name |
| item_name | Item Name, not currently used over network, but supported by reference Clients. |
| location_id | Location ID, should be resolved to Location Name |
| location_name |Location Name, not currently used over network, but supported by reference Clients. |
| location_name | Location Name, not currently used over network, but supported by reference Clients. |
| entrance_name | Entrance Name. No ID mapping exists. |
| color | Regular text that should be colored. Only `type` that will contain `color` data. |
@@ -390,6 +420,8 @@ Color options:
* white_bg
`text` is the content of the message part to be displayed.
`player` marks owning player id for location/item,
`flags` contains the [NetworkItem](#NetworkItem) flags that belong to the item
### Client States
An enumeration containing the possible client states that may be used to inform the server in [StatusUpdate](#StatusUpdate).
@@ -459,7 +491,8 @@ Tags are represented as a list of strings, the common Client tags follow:
| AP | Signifies that this client is a reference client, its usefulness is mostly in debugging to compare client behaviours more easily. |
| IgnoreGame | Tells the server to ignore the "game" attribute in the [Connect](#Connect) packet. |
| DeathLink | Client participates in the DeathLink mechanic, therefore will send and receive DeathLink bounce packets |
| Tracker | Tells the server that this client is actually a Tracker and will refuse new locations from this client. |
| Tracker | Tells the server that this client is actually a Tracker, will refuse new locations from this client and send all items as if they were remote items. |
| TextOnly | Tells the server that this client will not send locations and does not want to receive items. |
### DeathLink
A special kind of Bounce packet that can be supported by any AP game. It targets the tag "DeathLink" and carries the following data:

View File

@@ -1,9 +1,11 @@
#define sourcepath "build\exe.win-amd64-3.10"
#define source_path ReadIni(SourcePath + "\setup.ini", "Data", "source_path")
#define min_windows ReadIni(SourcePath + "\setup.ini", "Data", "min_windows")
#define MyAppName "Archipelago"
#define MyAppExeName "ArchipelagoServer.exe"
#define MyAppIcon "data/icon.ico"
#dim VersionTuple[4]
#define MyAppVersion ParseVersion('build\exe.win-amd64-3.10\ArchipelagoServer.exe', VersionTuple[0], VersionTuple[1], VersionTuple[2], VersionTuple[3])
#define MyAppVersion ParseVersion(source_path + '\ArchipelagoServer.exe', VersionTuple[0], VersionTuple[1], VersionTuple[2], VersionTuple[3])
#define MyAppVersionText Str(VersionTuple[0])+"."+Str(VersionTuple[1])+"."+Str(VersionTuple[2])
@@ -34,6 +36,7 @@ SignTool= signtool
LicenseFile= LICENSE
WizardStyle= modern
SetupLogging=yes
MinVersion={#min_windows}
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
@@ -73,19 +76,19 @@ Source: "{code:GetROMPath}"; DestDir: "{app}"; DestName: "Zelda no Densetsu - Ka
Source: "{code:GetSMROMPath}"; DestDir: "{app}"; DestName: "Super Metroid (JU).sfc"; Flags: external; Components: client/sni/sm or generator/sm
Source: "{code:GetSoEROMPath}"; DestDir: "{app}"; DestName: "Secret of Evermore (USA).sfc"; Flags: external; Components: generator/soe
Source: "{code:GetOoTROMPath}"; DestDir: "{app}"; DestName: "The Legend of Zelda - Ocarina of Time.z64"; Flags: external; Components: generator/oot
Source: "{#sourcepath}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "{#sourcepath}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/sni
Source: "{#sourcepath}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp
Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/sni
Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp
Source: "{#sourcepath}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator
Source: "{#sourcepath}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server
Source: "{#sourcepath}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio
Source: "{#sourcepath}\ArchipelagoTextClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/text
Source: "{#sourcepath}\ArchipelagoSNIClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni
Source: "{#sourcepath}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni/lttp or generator/lttp
Source: "{#sourcepath}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft
Source: "{#sourcepath}\ArchipelagoOoTAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot
Source: "{#sourcepath}\ArchipelagoFF1Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ff1
Source: "{#source_path}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator
Source: "{#source_path}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server
Source: "{#source_path}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio
Source: "{#source_path}\ArchipelagoTextClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/text
Source: "{#source_path}\ArchipelagoSNIClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni
Source: "{#source_path}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni/lttp or generator/lttp
Source: "{#source_path}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft
Source: "{#source_path}\ArchipelagoOoTAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot
Source: "{#source_path}\ArchipelagoFF1Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ff1
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
[Icons]

View File

@@ -1,369 +0,0 @@
#define sourcepath "build\exe.win-amd64-3.8"
#define MyAppName "Archipelago"
#define MyAppExeName "ArchipelagoServer.exe"
#define MyAppIcon "data/icon.ico"
#dim VersionTuple[4]
#define MyAppVersion ParseVersion('build\exe.win-amd64-3.8\ArchipelagoServer.exe', VersionTuple[0], VersionTuple[1], VersionTuple[2], VersionTuple[3])
#define MyAppVersionText Str(VersionTuple[0])+"."+Str(VersionTuple[1])+"."+Str(VersionTuple[2])
[Setup]
; NOTE: The value of AppId uniquely identifies this application.
; Do not use the same AppId value in installers for other applications.
AppId={{918BA46A-FAB8-460C-9DFF-AE691E1C865B}}
AppName={#MyAppName}
AppCopyright=Distributed under MIT License
AppVerName={#MyAppName} {#MyAppVersionText}
VersionInfoVersion={#MyAppVersion}
DefaultDirName={commonappdata}\{#MyAppName}
DisableProgramGroupPage=yes
DefaultGroupName=Archipelago
OutputDir=setups
OutputBaseFilename=Setup {#MyAppName} {#MyAppVersionText}
Compression=lzma2
SolidCompression=yes
LZMANumBlockThreads=8
ArchitecturesInstallIn64BitMode=x64
ChangesAssociations=yes
ArchitecturesAllowed=x64
AllowNoIcons=yes
SetupIconFile={#MyAppIcon}
UninstallDisplayIcon={app}\{#MyAppExeName}
; you will likely have to remove the following signtool line when testing/debugging locally. Don't include that change in PRs.
SignTool= signtool
LicenseFile= LICENSE
WizardStyle= modern
SetupLogging=yes
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}";
[Types]
Name: "full"; Description: "Full installation"
Name: "hosting"; Description: "Installation for hosting purposes"
Name: "playing"; Description: "Installation for playing purposes"
Name: "custom"; Description: "Custom installation"; Flags: iscustom
[Components]
Name: "core"; Description: "Core Files"; Types: full hosting playing custom; Flags: fixed
Name: "generator"; Description: "Generator"; Types: full hosting
Name: "generator/sm"; Description: "Super Metroid ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
Name: "generator/soe"; Description: "Secret of Evermore ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
Name: "generator/lttp"; Description: "A Link to the Past ROM Setup and Enemizer"; Types: full hosting; ExtraDiskSpaceRequired: 5191680
Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 100663296; Flags: disablenouninstallwarning
Name: "server"; Description: "Server"; Types: full hosting
Name: "client"; Description: "Clients"; Types: full playing
Name: "client/sni"; Description: "SNI Client"; Types: full playing
Name: "client/sni/lttp"; Description: "SNI Client - A Link to the Past Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
Name: "client/factorio"; Description: "Factorio"; Types: full playing
Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
Name: "client/oot"; Description: "Ocarina of Time Adjuster"; Types: full playing
Name: "client/ff1"; Description: "Final Fantasy 1"; Types: full playing
Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing
[Dirs]
NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify;
[Files]
Source: "{code:GetROMPath}"; DestDir: "{app}"; DestName: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"; Flags: external; Components: client/sni/lttp or generator/lttp
Source: "{code:GetSMROMPath}"; DestDir: "{app}"; DestName: "Super Metroid (JU).sfc"; Flags: external; Components: client/sni/sm or generator/sm
Source: "{code:GetSoEROMPath}"; DestDir: "{app}"; DestName: "Secret of Evermore (USA).sfc"; Flags: external; Components: generator/soe
Source: "{code:GetOoTROMPath}"; DestDir: "{app}"; DestName: "The Legend of Zelda - Ocarina of Time.z64"; Flags: external; Components: generator/oot
Source: "{#sourcepath}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "{#sourcepath}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/sni
Source: "{#sourcepath}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp
Source: "{#sourcepath}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator
Source: "{#sourcepath}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server
Source: "{#sourcepath}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio
Source: "{#sourcepath}\ArchipelagoTextClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/text
Source: "{#sourcepath}\ArchipelagoSNIClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni
Source: "{#sourcepath}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni/lttp or generator/lttp
Source: "{#sourcepath}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft
Source: "{#sourcepath}\ArchipelagoOoTAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot
Source: "{#sourcepath}\ArchipelagoFF1Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ff1
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
[Icons]
Name: "{group}\{#MyAppName} Folder"; Filename: "{app}";
Name: "{group}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Components: server
Name: "{group}\{#MyAppName} Text Client"; Filename: "{app}\ArchipelagoTextClient.exe"; Components: client/text
Name: "{group}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Components: client/sni
Name: "{group}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Components: client/factorio
Name: "{group}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Components: client/minecraft
Name: "{group}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Components: client/ff1
Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon
Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Components: server
Name: "{commondesktop}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Tasks: desktopicon; Components: client/sni
Name: "{commondesktop}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Tasks: desktopicon; Components: client/factorio
Name: "{commondesktop}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Tasks: desktopicon; Components: client/factorio
[Run]
Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..."
Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."; Components: client/sni/lttp or generator/lttp
Filename: "{app}\ArchipelagoMinecraftClient.exe"; Parameters: "--install"; StatusMsg: "Installing Forge Server..."; Components: client/minecraft
[UninstallDelete]
Type: dirifempty; Name: "{app}"
[InstallDelete]
Type: files; Name: "{app}\ArchipelagoLttPClient.exe"
[Registry]
Root: HKCR; Subkey: ".apbp"; ValueData: "{#MyAppName}patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}patch"; ValueData: "Archipelago Binary Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: ".apm3"; ValueData: "{#MyAppName}smpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}smpatch"; ValueData: "Archipelago Super Metroid Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}smpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}smpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: ".apsoe"; ValueData: "{#MyAppName}soepatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}soepatch"; ValueData: "Archipelago Secret of Evermore Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}soepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}soepatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1""";
Root: HKCR; Subkey: ".apmc"; ValueData: "{#MyAppName}mcdata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/minecraft
Root: HKCR; Subkey: "{#MyAppName}mcdata"; ValueData: "Archipelago Minecraft Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/minecraft
Root: HKCR; Subkey: "{#MyAppName}mcdata\DefaultIcon"; ValueData: "{app}\ArchipelagoMinecraftClient.exe,0"; ValueType: string; ValueName: ""; Components: client/minecraft
Root: HKCR; Subkey: "{#MyAppName}mcdata\shell\open\command"; ValueData: """{app}\ArchipelagoMinecraftClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/minecraft
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: server
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: server
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: ""; Components: server
Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{app}\ArchipelagoServer.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: server
[Code]
const
SHCONTCH_NOPROGRESSBOX = 4;
SHCONTCH_RESPONDYESTOALL = 16;
// See: https://stackoverflow.com/a/51614652/2287576
function IsVCRedist64BitNeeded(): boolean;
var
strVersion: string;
begin
if (RegQueryStringValue(HKEY_LOCAL_MACHINE,
'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64', 'Version', strVersion)) then
begin
// Is the installed version at least the packaged one ?
Log('VC Redist x64 Version : found ' + strVersion);
Result := (CompareStr(strVersion, 'v14.29.30037') < 0);
end
else
begin
// Not even an old version installed
Log('VC Redist x64 is not already installed');
Result := True;
end;
end;
var R : longint;
var lttprom: string;
var LttPROMFilePage: TInputFileWizardPage;
var smrom: string;
var SMRomFilePage: TInputFileWizardPage;
var soerom: string;
var SoERomFilePage: TInputFileWizardPage;
var ootrom: string;
var OoTROMFilePage: TInputFileWizardPage;
function GetSNESMD5OfFile(const rom: string): string;
var data: AnsiString;
begin
if LoadStringFromFile(rom, data) then
begin
if Length(data) mod 1024 = 512 then
begin
data := copy(data, 513, Length(data)-512);
end;
Result := GetMD5OfString(data);
end;
end;
function CheckRom(name: string; hash: string): string;
var rom: string;
begin
log('Handling ' + name)
rom := FileSearch(name, WizardDirValue());
if Length(rom) > 0 then
begin
log('existing ROM found');
log(IntToStr(CompareStr(GetSNESMD5OfFile(rom), hash)));
if CompareStr(GetSNESMD5OfFile(rom), hash) = 0 then
begin
log('existing ROM verified');
Result := rom;
exit;
end;
log('existing ROM failed verification');
end;
end;
function AddRomPage(name: string): TInputFileWizardPage;
begin
Result :=
CreateInputFilePage(
wpSelectComponents,
'Select ROM File',
'Where is your ' + name + ' located?',
'Select the file, then click Next.');
Result.Add(
'Location of ROM file:',
'SNES ROM files|*.sfc;*.smc|All files|*.*',
'.sfc');
end;
procedure AddOoTRomPage();
begin
ootrom := FileSearch('The Legend of Zelda - Ocarina of Time.z64', WizardDirValue());
if Length(ootrom) > 0 then
begin
log('existing ROM found');
log(IntToStr(CompareStr(GetMD5OfFile(ootrom), '5bd1fe107bf8106b2ab6650abecd54d6'))); // normal
log(IntToStr(CompareStr(GetMD5OfFile(ootrom), '6697768a7a7df2dd27a692a2638ea90b'))); // byteswapped
log(IntToStr(CompareStr(GetMD5OfFile(ootrom), '05f0f3ebacbc8df9243b6148ffe4792f'))); // decompressed
if (CompareStr(GetMD5OfFile(ootrom), '5bd1fe107bf8106b2ab6650abecd54d6') = 0) or (CompareStr(GetMD5OfFile(ootrom), '6697768a7a7df2dd27a692a2638ea90b') = 0) or (CompareStr(GetMD5OfFile(ootrom), '05f0f3ebacbc8df9243b6148ffe4792f') = 0) then
begin
log('existing ROM verified');
exit;
end;
log('existing ROM failed verification');
end;
ootrom := ''
OoTROMFilePage :=
CreateInputFilePage(
wpSelectComponents,
'Select ROM File',
'Where is your OoT 1.0 ROM located?',
'Select the file, then click Next.');
OoTROMFilePage.Add(
'Location of ROM file:',
'N64 ROM files (*.z64, *.n64)|*.z64;*.n64|All files|*.*',
'.z64');
end;
function NextButtonClick(CurPageID: Integer): Boolean;
begin
if (assigned(LttPROMFilePage)) and (CurPageID = LttPROMFilePage.ID) then
Result := not (LttPROMFilePage.Values[0] = '')
else if (assigned(SMROMFilePage)) and (CurPageID = SMROMFilePage.ID) then
Result := not (SMROMFilePage.Values[0] = '')
else if (assigned(SoEROMFilePage)) and (CurPageID = SoEROMFilePage.ID) then
Result := not (SoEROMFilePage.Values[0] = '')
else if (assigned(OoTROMFilePage)) and (CurPageID = OoTROMFilePage.ID) then
Result := not (OoTROMFilePage.Values[0] = '')
else
Result := True;
end;
function GetROMPath(Param: string): string;
begin
if Length(lttprom) > 0 then
Result := lttprom
else if Assigned(LttPRomFilePage) then
begin
R := CompareStr(GetSNESMD5OfFile(LttPROMFilePage.Values[0]), '03a63945398191337e896e5771f77173')
if R <> 0 then
MsgBox('ALttP ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
Result := LttPROMFilePage.Values[0]
end
else
Result := '';
end;
function GetSMROMPath(Param: string): string;
begin
if Length(smrom) > 0 then
Result := smrom
else if Assigned(SMRomFilePage) then
begin
R := CompareStr(GetSNESMD5OfFile(SMROMFilePage.Values[0]), '21f3e98df4780ee1c667b84e57d88675')
if R <> 0 then
MsgBox('Super Metroid ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
Result := SMROMFilePage.Values[0]
end
else
Result := '';
end;
function GetSoEROMPath(Param: string): string;
begin
if Length(soerom) > 0 then
Result := soerom
else if Assigned(SoERomFilePage) then
begin
R := CompareStr(GetSNESMD5OfFile(SoEROMFilePage.Values[0]), '6e9c94511d04fac6e0a1e582c170be3a')
if R <> 0 then
MsgBox('Secret of Evermore ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
Result := SoEROMFilePage.Values[0]
end
else
Result := '';
end;
function GetOoTROMPath(Param: string): string;
begin
if Length(ootrom) > 0 then
Result := ootrom
else if Assigned(OoTROMFilePage) then
begin
R := CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '5bd1fe107bf8106b2ab6650abecd54d6') * CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '6697768a7a7df2dd27a692a2638ea90b') * CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '05f0f3ebacbc8df9243b6148ffe4792f');
if R <> 0 then
MsgBox('OoT ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
Result := OoTROMFilePage.Values[0]
end
else
Result := '';
end;
procedure InitializeWizard();
begin
AddOoTRomPage();
lttprom := CheckRom('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', '03a63945398191337e896e5771f77173');
if Length(lttprom) = 0 then
LttPROMFilePage:= AddRomPage('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc');
smrom := CheckRom('Super Metroid (JU).sfc', '21f3e98df4780ee1c667b84e57d88675');
if Length(smrom) = 0 then
SMRomFilePage:= AddRomPage('Super Metroid (JU).sfc');
soerom := CheckRom('Secret of Evermore (USA).sfc', '6e9c94511d04fac6e0a1e582c170be3a');
if Length(soerom) = 0 then
SoEROMFilePage:= AddRomPage('Secret of Evermore (USA).sfc');
end;
function ShouldSkipPage(PageID: Integer): Boolean;
begin
Result := False;
if (assigned(LttPROMFilePage)) and (PageID = LttPROMFilePage.ID) then
Result := not (WizardIsComponentSelected('client/sni/lttp') or WizardIsComponentSelected('generator/lttp'));
if (assigned(SMROMFilePage)) and (PageID = SMROMFilePage.ID) then
Result := not (WizardIsComponentSelected('client/sni/sm') or WizardIsComponentSelected('generator/sm'));
if (assigned(SoEROMFilePage)) and (PageID = SoEROMFilePage.ID) then
Result := not (WizardIsComponentSelected('generator/soe'));
if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then
Result := not (WizardIsComponentSelected('generator/oot'));
end;

20
kvui.py
View File

@@ -290,6 +290,7 @@ class GameManager(App):
return self.container
def update_texts(self, dt):
self.tabs.content.children[0].fix_heights() # TODO: remove this when Kivy fixes this upstream
if self.ctx.server:
self.title = self.base_title + " " + Utils.__version__ + \
f" | Connected to: {self.ctx.server_address} " \
@@ -340,7 +341,7 @@ class GameManager(App):
except Exception as e:
logging.getLogger("Client").exception(e)
def print_json(self, data):
def print_json(self, data: typing.List[JSONMessagePart]):
text = self.json_to_kivy_parser(data)
self.log_panels["Archipelago"].on_message_markup(text)
self.log_panels["All"].on_message_markup(text)
@@ -401,6 +402,12 @@ class UILog(RecycleView):
def on_message_markup(self, text):
self.data.append({"text": text})
def fix_heights(self):
"""Workaround fix for divergent texture and layout heights"""
for element in self.children[0].children:
if element.height != element.texture_size[1]:
element.height = element.texture_size[1]
class E(ExceptionHandler):
logger = logging.getLogger("Client")
@@ -411,17 +418,6 @@ class E(ExceptionHandler):
class KivyJSONtoTextParser(JSONtoTextParser):
color_codes = {
# not exact color names, close enough but decent looking
"black": "000000",
"red": "EE0000",
"green": "00FF7F",
"yellow": "FAFAD2",
"blue": "6495ED",
"magenta": "EE00EE",
"cyan": "00EEEE",
"white": "FFFFFF"
}
def _handle_color(self, node: JSONMessagePart):
colors = node["color"].split(";")

View File

@@ -25,9 +25,8 @@ name: YourName{number} # Your name in-game. Spaces will be replaced with undersc
#{NUMBER} will be replaced with the counter value of the name if the counter value is greater than 1.
game: # Pick a game to play
A Link to the Past: 1
requires:
version: 0.1.7 # Version of Archipelago required for this yaml to work as expected.
version: 0.2.3 # Version of Archipelago required for this yaml to work as expected.
# Shared Options supported by all games:
accessibility:
items: 0 # Guarantees you will be able to acquire all items, but you may not be able to access all locations
@@ -36,23 +35,6 @@ accessibility:
progression_balancing:
on: 50 # A system to reduce BK, as in times during which you can't do anything by moving your items into an earlier access sphere to make it likely you have stuff to do
off: 0 # Turn this off if you don't mind a longer multiworld, or can glitch/sequence break around missing items.
# The following 4 options can be uncommented and moved into a game's section they should affect
# start_inventory: # Begin the file with the listed items/upgrades
# Please only use items for the correct game, use triggers if need to be have seperated lists.
# Pegasus Boots: on
# Bomb Upgrade (+10): 4
# Arrow Upgrade (+10): 4
# start_hints: # Begin the game with these items' locations revealed to you at the start of the game. Get the info via !hint in your client.
# - Moon Pearl
# local_items: # Force certain items to appear in your world only, not across the multiworld. Recognizes some group names, like "Swords"
# - "Moon Pearl"
# - "Small Keys"
# - "Big Keys"
# non_local_items: # Force certain items to appear outside your world only, unless in single-player. Recognizes some group names, like "Swords"
# - "Progressive Weapons"
# exclude_locations: # Force certain locations to never contain progression items, and always be filled with junk.
# - "Master Sword Pedestal"
A Link to the Past:
### Logic Section ###
glitches_required: # Determine the logic required to complete the seed
@@ -76,6 +58,7 @@ A Link to the Past:
own_world: 0
any_world: 0
different_world: 0
start_with: 0
smallkey_shuffle: # Small Key Placement
original_dungeon: 50
own_dungeons: 0
@@ -83,18 +66,21 @@ A Link to the Past:
any_world: 0
different_world: 0
universal: 0
start_with: 0
compass_shuffle: # Compass Placement
original_dungeon: 50
own_dungeons: 0
own_world: 0
any_world: 0
different_world: 0
start_with: 0
map_shuffle: # Map Placement
original_dungeon: 50
own_dungeons: 0
own_world: 0
any_world: 0
different_world: 0
start_with: 0
dungeon_counters:
on: 0 # Always display amount of items checked in a dungeon
pickup: 50 # Show when compass is picked up
@@ -165,40 +151,31 @@ A Link to the Past:
50: 0
crystals_needed_for_gt: # Crystals required to open GT
0: 0
1: 0
2: 0
3: 0
4: 0
5: 0
6: 0
7: 0
7: 50
random: 0
random-low: 50 # any valid number, weighted towards the lower end
random-low: 0 # any valid number, weighted towards the lower end
random-middle: 0 # any valid number, weighted towards the central range
random-high: 0 # any valid number, weighted towards the higher end
crystals_needed_for_ganon: # Crystals required to hurt Ganon
0: 0
1: 0
2: 0
3: 0
4: 0
5: 0
6: 0
7: 0
7: 50
random: 0
random-low: 0
random-middle: 0
random-high: 50
random-high: 0
mode:
standard: 50 # Begin the game by rescuing Zelda from her cell and escorting her to the Sanctuary
standard: 0 # Begin the game by rescuing Zelda from her cell and escorting her to the Sanctuary
open: 50 # Begin the game from your choice of Link's House or the Sanctuary
inverted: 0 # Begin in the Dark World. The Moon Pearl is required to avoid bunny-state in Light World, and the Light World game map is altered
retro:
on: 0 # you must buy a quiver to use the bow, take-any caves and an old-man cave are added to the world. You may need to find your sword from the old man's cave
off: 50
hints:
'on': 50 # Hint tiles sometimes give item location hints
'off': 0 # Hint tiles provide gameplay tips
hints: # Vendors: King Zora and Bottle Merchant say what they're selling.
# On/Full: Put item and entrance placement hints on telepathic tiles and some NPCs, Full removes joke hints.
'on': 50
vendors: 0
'off': 0
full: 0
swordless:
on: 0 # Your swords are replaced by rupees. Gameplay changes have been made to accommodate this change
off: 1
@@ -275,6 +252,14 @@ A Link to the Past:
15: 0
30: 0
random: 0 # 0 to 30 evenly distributed
shop_price_modifier: # Percentage modifier for shuffled item prices in shops
# you can add additional values between minimum and maximum
0: 0 # minimum value
400: 0 # maximum value
random: 0
random-low: 0
random-high: 0
100: 50
shop_shuffle:
none: 50
g: 0 # Generate new default inventories for overworld/underworld shops, and unique shops
@@ -456,6 +441,10 @@ A Link to the Past:
vanilla: 0 # Swords are placed in vanilla locations in your own game (Uncle, Pyramid Fairy, Smiths, Pedestal)
swordless: 0 # swordless mode
death_link:
false: 50
true: 0
linked_options:
- name: crosskeys
options: # These overwrite earlier options if the percentage chance triggers
@@ -537,4 +526,4 @@ triggers:
percentage: 0 # AND has a 0 percent chance (meaning this is default disabled, just to show how it works)
options: # then inserts these options
A Link to the Past:
swordless: off
swordless: off

View File

@@ -1,7 +1,7 @@
colorama>=0.4.4
websockets>=10.1
PyYAML>=6.0
fuzzywuzzy>=0.18.0
thefuzz[speedup]>=0.19.0
jinja2>=3.0.3
schema>=0.7.4
kivy>=2.0.0

View File

@@ -3,6 +3,16 @@ import shutil
import sys
import sysconfig
from pathlib import Path
import ModuleUpdate
# I don't really want to have another root directory file for a single requirement, but this special case is also jank.
# Might move this into a cleaner solution when I think of one.
with open("freeze_requirements.txt", "w") as f:
f.write("cx-Freeze>=6.9\n")
ModuleUpdate.requirements_files.add("freeze_requirements.txt")
ModuleUpdate.requirements_files.add(os.path.join("WebHostLib", "requirements.txt"))
ModuleUpdate.update()
import cx_Freeze
from kivy_deps import sdl2, glew
from Utils import version_tuple
@@ -180,3 +190,8 @@ if signtool:
remove_sprites_from_folder(buildfolder / "data" / "sprites" / "alttpr")
manifest_creation(buildfolder)
if sys.platform == "win32":
with open("setup.ini", "w") as f:
min_supported_windows = "6.2.9200" if sys.version_info > (3, 9) else "6.0.6000"
f.write(f"[Data]\nsource_path={buildfolder}\nmin_windows={min_supported_windows}\n")

View File

@@ -1,9 +1,9 @@
from typing import NamedTuple, List
from typing import List
import unittest
from worlds.AutoWorld import World
from Fill import FillError, fill_restrictive
from BaseClasses import MultiWorld, Region, RegionType, Item, Location
from worlds.generic.Rules import set_rule
from Fill import FillError, balance_multiworld_progression, fill_restrictive, distribute_items_restrictive
from BaseClasses import Entrance, LocationProgressType, MultiWorld, Region, RegionType, Item, Location
from worlds.generic.Rules import CollectionRule, set_rule
def generate_multi_world(players: int = 1) -> MultiWorld:
@@ -19,31 +19,87 @@ def generate_multi_world(players: int = 1) -> MultiWorld:
"Menu Region Hint", player_id, multi_world)
multi_world.regions.append(region)
multi_world.set_seed()
multi_world.set_seed(0)
multi_world.set_default_common_options()
return multi_world
class PlayerDefinition(NamedTuple):
class PlayerDefinition(object):
world: MultiWorld
id: int
menu: Region
locations: List[Location]
prog_items: List[Item]
basic_items: List[Item]
regions: List[Region]
def __init__(self, world: MultiWorld, id: int, menu: Region, locations: List[Location] = [], prog_items: List[Item] = [], basic_items: List[Item] = []):
self.world = world
self.id = id
self.menu = menu
self.locations = locations
self.prog_items = prog_items
self.basic_items = basic_items
self.regions = [menu]
def generate_region(self, parent: Region, size: int, access_rule: CollectionRule = lambda state: True) -> Region:
region_tag = "_region" + str(len(self.regions))
region_name = "player" + str(self.id) + region_tag
region = Region("player" + str(self.id) + region_tag, RegionType.Generic,
"Region Hint", self.id, self.world)
self.locations += generate_locations(size,
self.id, None, region, region_tag)
entrance = Entrance(self.id, region_name + "_entrance", parent)
parent.exits.append(entrance)
entrance.connect(region)
entrance.access_rule = access_rule
self.regions.append(region)
self.world.regions.append(region)
return region
def generate_player_data(multi_world: MultiWorld, player_id: int, location_count: int, prog_item_count: int) -> PlayerDefinition:
def fillRegion(world: MultiWorld, region: Region, items: List[Item]) -> List[Item]:
items = items.copy()
while len(items) > 0:
location = region.locations.pop(0)
region.locations.append(location)
if location.item:
return items
item = items.pop(0)
world.push_item(location, item, False)
location.event = item.advancement
return items
def regionContains(region: Region, item: Item) -> bool:
for location in region.locations:
if location.item == item:
return True
return False
def generate_player_data(multi_world: MultiWorld, player_id: int, location_count: int = 0, prog_item_count: int = 0, basic_item_count: int = 0) -> PlayerDefinition:
menu = multi_world.get_region("Menu", player_id)
locations = generate_locations(location_count, player_id, None, menu)
prog_items = generate_items(prog_item_count, player_id, True)
multi_world.itempool += prog_items
basic_items = generate_items(basic_item_count, player_id, False)
multi_world.itempool += basic_items
return PlayerDefinition(player_id, menu, locations, prog_items)
return PlayerDefinition(multi_world, player_id, menu, locations, prog_items, basic_items)
def generate_locations(count: int, player_id: int, address: int = None, region: Region = None) -> List[Location]:
def generate_locations(count: int, player_id: int, address: int = None, region: Region = None, tag: str = "") -> List[Location]:
locations = []
prefix = "player" + str(player_id) + tag + "_location"
for i in range(count):
name = "player" + str(player_id) + "_location" + str(i)
name = prefix + str(i)
location = Location(player_id, name, address, region)
locations.append(location)
region.locations.append(location)
@@ -52,14 +108,19 @@ def generate_locations(count: int, player_id: int, address: int = None, region:
def generate_items(count: int, player_id: int, advancement: bool = False, code: int = None) -> List[Item]:
items = []
type = "prog" if advancement else ""
for i in range(count):
name = "player" + str(player_id) + "_item" + str(i)
name = "player" + str(player_id) + "_" + type + "item" + str(i)
items.append(Item(name, advancement, code, player_id))
return items
class TestBase(unittest.TestCase):
def test_basic_fill_restrictive(self):
def names(objs: list) -> List[str]:
return map(lambda o: o.name, objs)
class TestFillRestrictive(unittest.TestCase):
def test_basic_fill(self):
multi_world = generate_multi_world()
player1 = generate_player_data(multi_world, 1, 2, 2)
@@ -76,7 +137,7 @@ class TestBase(unittest.TestCase):
self.assertEqual([], player1.locations)
self.assertEqual([], player1.prog_items)
def test_ordered_fill_restrictive(self):
def test_ordered_fill(self):
multi_world = generate_multi_world()
player1 = generate_player_data(multi_world, 1, 2, 2)
items = player1.prog_items
@@ -92,7 +153,7 @@ class TestBase(unittest.TestCase):
self.assertEqual(locations[0].item, items[0])
self.assertEqual(locations[1].item, items[1])
def test_fill_restrictive_remaining_locations(self):
def test_partial_fill(self):
multi_world = generate_multi_world()
player1 = generate_player_data(multi_world, 1, 3, 2)
@@ -106,7 +167,7 @@ class TestBase(unittest.TestCase):
item0.name, player1.id) and state.has(item1.name, player1.id)
set_rule(loc1, lambda state: state.has(
item0.name, player1.id))
#forces a swap
# forces a swap
set_rule(loc2, lambda state: state.has(
item0.name, player1.id))
fill_restrictive(multi_world, multi_world.state,
@@ -117,7 +178,7 @@ class TestBase(unittest.TestCase):
self.assertEqual(1, len(player1.locations))
self.assertEqual(player1.locations[0], loc2)
def test_minimal_fill_restrictive(self):
def test_minimal_fill(self):
multi_world = generate_multi_world()
player1 = generate_player_data(multi_world, 1, 2, 2)
@@ -137,7 +198,7 @@ class TestBase(unittest.TestCase):
# Unnecessary unreachable Item
self.assertEqual(locations[1].item, items[0])
def test_reversed_fill_restrictive(self):
def test_reversed_fill(self):
multi_world = generate_multi_world()
player1 = generate_player_data(multi_world, 1, 2, 2)
@@ -155,7 +216,7 @@ class TestBase(unittest.TestCase):
self.assertEqual(loc0.item, item1)
self.assertEqual(loc1.item, item0)
def test_multi_step_fill_restrictive(self):
def test_multi_step_fill(self):
multi_world = generate_multi_world()
player1 = generate_player_data(multi_world, 1, 4, 4)
@@ -179,7 +240,7 @@ class TestBase(unittest.TestCase):
self.assertEqual(locations[2].item, items[0])
self.assertEqual(locations[3].item, items[3])
def test_impossible_fill_restrictive(self):
def test_impossible_fill(self):
multi_world = generate_multi_world()
player1 = generate_player_data(multi_world, 1, 2, 2)
items = player1.prog_items
@@ -195,7 +256,7 @@ class TestBase(unittest.TestCase):
self.assertRaises(FillError, fill_restrictive, multi_world, multi_world.state,
player1.locations.copy(), player1.prog_items.copy())
def test_circular_fill_restrictive(self):
def test_circular_fill(self):
multi_world = generate_multi_world()
player1 = generate_player_data(multi_world, 1, 3, 3)
@@ -215,7 +276,7 @@ class TestBase(unittest.TestCase):
self.assertRaises(FillError, fill_restrictive, multi_world, multi_world.state,
player1.locations.copy(), player1.prog_items.copy())
def test_competing_fill_restrictive(self):
def test_competing_fill(self):
multi_world = generate_multi_world()
player1 = generate_player_data(multi_world, 1, 2, 2)
@@ -231,7 +292,7 @@ class TestBase(unittest.TestCase):
self.assertRaises(FillError, fill_restrictive, multi_world, multi_world.state,
player1.locations.copy(), player1.prog_items.copy())
def test_multiplayer_fill_restrictive(self):
def test_multiplayer_fill(self):
multi_world = generate_multi_world(2)
player1 = generate_player_data(multi_world, 1, 2, 2)
player2 = generate_player_data(multi_world, 2, 2, 2)
@@ -251,7 +312,7 @@ class TestBase(unittest.TestCase):
self.assertEqual(player2.locations[0].item, player1.prog_items[0])
self.assertEqual(player2.locations[1].item, player2.prog_items[0])
def test_multiplayer_rules_fill_restrictive(self):
def test_multiplayer_rules_fill(self):
multi_world = generate_multi_world(2)
player1 = generate_player_data(multi_world, 1, 2, 2)
player2 = generate_player_data(multi_world, 2, 2, 2)
@@ -273,3 +334,274 @@ class TestBase(unittest.TestCase):
self.assertEqual(player1.locations[1].item, player2.prog_items[1])
self.assertEqual(player2.locations[0].item, player1.prog_items[0])
self.assertEqual(player2.locations[1].item, player1.prog_items[1])
def test_restrictive_progress(self):
multi_world = generate_multi_world()
player1 = generate_player_data(multi_world, 1, prog_item_count=25)
items = player1.prog_items.copy()
multi_world.completion_condition[player1.id] = lambda state: state.has_all(
names(player1.prog_items), player1.id)
region1 = player1.generate_region(player1.menu, 5)
region2 = player1.generate_region(player1.menu, 5, lambda state: state.has_all(
names(items[2:7]), player1.id))
region3 = player1.generate_region(player1.menu, 5, lambda state: state.has_all(
names(items[7:12]), player1.id))
region4 = player1.generate_region(player1.menu, 5, lambda state: state.has_all(
names(items[12:17]), player1.id))
region5 = player1.generate_region(player1.menu, 5, lambda state: state.has_all(
names(items[17:22]), player1.id))
locations = multi_world.get_unfilled_locations()
fill_restrictive(multi_world, multi_world.state,
locations, player1.prog_items)
class TestDistributeItemsRestrictive(unittest.TestCase):
def test_basic_distribute(self):
multi_world = generate_multi_world()
player1 = generate_player_data(
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
locations = player1.locations
prog_items = player1.prog_items
basic_items = player1.basic_items
distribute_items_restrictive(multi_world)
self.assertEqual(locations[0].item, basic_items[0])
self.assertEqual(locations[1].item, prog_items[0])
self.assertEqual(locations[2].item, prog_items[1])
self.assertEqual(locations[3].item, basic_items[1])
def test_excluded_distribute(self):
multi_world = generate_multi_world()
player1 = generate_player_data(
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
locations = player1.locations
locations[1].progress_type = LocationProgressType.EXCLUDED
locations[2].progress_type = LocationProgressType.EXCLUDED
distribute_items_restrictive(multi_world)
self.assertFalse(locations[1].item.advancement)
self.assertFalse(locations[2].item.advancement)
def test_non_excluded_item_distribute(self):
multi_world = generate_multi_world()
player1 = generate_player_data(
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
locations = player1.locations
basic_items = player1.basic_items
locations[1].progress_type = LocationProgressType.EXCLUDED
basic_items[1].never_exclude = True
distribute_items_restrictive(multi_world)
self.assertEqual(locations[1].item, basic_items[0])
def test_too_many_excluded_distribute(self):
multi_world = generate_multi_world()
player1 = generate_player_data(
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
locations = player1.locations
locations[0].progress_type = LocationProgressType.EXCLUDED
locations[1].progress_type = LocationProgressType.EXCLUDED
locations[2].progress_type = LocationProgressType.EXCLUDED
self.assertRaises(FillError, distribute_items_restrictive, multi_world)
def test_non_excluded_item_must_distribute(self):
multi_world = generate_multi_world()
player1 = generate_player_data(
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
locations = player1.locations
basic_items = player1.basic_items
locations[1].progress_type = LocationProgressType.EXCLUDED
locations[2].progress_type = LocationProgressType.EXCLUDED
basic_items[0].never_exclude = True
basic_items[1].never_exclude = True
self.assertRaises(FillError, distribute_items_restrictive, multi_world)
def test_priority_distribute(self):
multi_world = generate_multi_world()
player1 = generate_player_data(
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
locations = player1.locations
locations[0].progress_type = LocationProgressType.PRIORITY
locations[3].progress_type = LocationProgressType.PRIORITY
distribute_items_restrictive(multi_world)
self.assertTrue(locations[0].item.advancement)
self.assertTrue(locations[3].item.advancement)
def test_excess_priority_distribute(self):
multi_world = generate_multi_world()
player1 = generate_player_data(
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
locations = player1.locations
locations[0].progress_type = LocationProgressType.PRIORITY
locations[1].progress_type = LocationProgressType.PRIORITY
locations[2].progress_type = LocationProgressType.PRIORITY
distribute_items_restrictive(multi_world)
self.assertFalse(locations[3].item.advancement)
def test_multiple_world_priority_distribute(self):
multi_world = generate_multi_world(3)
player1 = generate_player_data(
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
player2 = generate_player_data(
multi_world, 2, 4, prog_item_count=1, basic_item_count=3)
player3 = generate_player_data(
multi_world, 3, 6, prog_item_count=4, basic_item_count=2)
player1.locations[2].progress_type = LocationProgressType.PRIORITY
player1.locations[3].progress_type = LocationProgressType.PRIORITY
player2.locations[1].progress_type = LocationProgressType.PRIORITY
player3.locations[0].progress_type = LocationProgressType.PRIORITY
player3.locations[1].progress_type = LocationProgressType.PRIORITY
player3.locations[2].progress_type = LocationProgressType.PRIORITY
player3.locations[3].progress_type = LocationProgressType.PRIORITY
distribute_items_restrictive(multi_world)
self.assertTrue(player1.locations[2].item.advancement)
self.assertTrue(player1.locations[3].item.advancement)
self.assertTrue(player2.locations[1].item.advancement)
self.assertTrue(player3.locations[0].item.advancement)
self.assertTrue(player3.locations[1].item.advancement)
self.assertTrue(player3.locations[2].item.advancement)
self.assertTrue(player3.locations[3].item.advancement)
def test_can_remove_locations_in_fill_hook(self):
multi_world = generate_multi_world()
player1 = generate_player_data(
multi_world, 1, 4, prog_item_count=2, basic_item_count=2)
removed_item: list[Item] = []
removed_location: list[Location] = []
def fill_hook(progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool, restitempool, fill_locations):
removed_item.append(restitempool.pop(0))
removed_location.append(fill_locations.pop(0))
multi_world.worlds[player1.id].fill_hook = fill_hook
distribute_items_restrictive(multi_world)
self.assertIsNone(removed_item[0].location)
self.assertIsNone(removed_location[0].item)
def test_seed_robust_to_item_order(self):
mw1 = generate_multi_world()
gen1 = generate_player_data(
mw1, 1, 4, prog_item_count=2, basic_item_count=2)
distribute_items_restrictive(mw1)
mw2 = generate_multi_world()
gen2 = generate_player_data(
mw2, 1, 4, prog_item_count=2, basic_item_count=2)
mw2.itempool.append(mw2.itempool.pop(0))
distribute_items_restrictive(mw2)
self.assertEqual(gen1.locations[0].item, gen2.locations[0].item)
self.assertEqual(gen1.locations[1].item, gen2.locations[1].item)
self.assertEqual(gen1.locations[2].item, gen2.locations[2].item)
self.assertEqual(gen1.locations[3].item, gen2.locations[3].item)
def test_seed_robust_to_location_order(self):
mw1 = generate_multi_world()
gen1 = generate_player_data(
mw1, 1, 4, prog_item_count=2, basic_item_count=2)
distribute_items_restrictive(mw1)
mw2 = generate_multi_world()
gen2 = generate_player_data(
mw2, 1, 4, prog_item_count=2, basic_item_count=2)
reg = mw2.get_region("Menu", gen2.id)
reg.locations.append(reg.locations.pop(0))
distribute_items_restrictive(mw2)
self.assertEqual(gen1.locations[0].item, gen2.locations[0].item)
self.assertEqual(gen1.locations[1].item, gen2.locations[1].item)
self.assertEqual(gen1.locations[2].item, gen2.locations[2].item)
self.assertEqual(gen1.locations[3].item, gen2.locations[3].item)
class TestBalanceMultiworldProgression(unittest.TestCase):
def assertRegionContains(self, region: Region, item: Item):
for location in region.locations:
if location.item and location.item == item:
return True
self.fail("Expected " + region.name + " to contain " + item.name +
"\n Contains" + str(list(map(lambda location: location.item, region.locations))))
def setUp(self):
multi_world = generate_multi_world(2)
self.multi_world = multi_world
player1 = generate_player_data(
multi_world, 1, prog_item_count=2, basic_item_count=40)
self.player1 = player1
player2 = generate_player_data(
multi_world, 2, prog_item_count=2, basic_item_count=40)
self.player2 = player2
multi_world.completion_condition[player1.id] = lambda state: state.has(
player1.prog_items[0].name, player1.id) and state.has(
player1.prog_items[1].name, player1.id)
multi_world.completion_condition[player2.id] = lambda state: state.has(
player2.prog_items[0].name, player2.id) and state.has(
player2.prog_items[1].name, player2.id)
items = player1.basic_items + player2.basic_items
# Sphere 1
region = player1.generate_region(player1.menu, 20)
items = fillRegion(multi_world, region, [
player1.prog_items[0]] + items)
# Sphere 2
region = player1.generate_region(
player1.regions[1], 20, lambda state: state.has(player1.prog_items[0].name, player1.id))
items = fillRegion(
multi_world, region, [player1.prog_items[1], player2.prog_items[0]] + items)
# Sphere 3
region = player2.generate_region(
player2.menu, 20, lambda state: state.has(player2.prog_items[0].name, player2.id))
items = fillRegion(multi_world, region, [
player2.prog_items[1]] + items)
multi_world.progression_balancing[player1.id] = True
multi_world.progression_balancing[player2.id] = True
def test_balances_progression(self):
self.assertRegionContains(
self.player1.regions[2], self.player2.prog_items[0])
balance_multiworld_progression(self.multi_world)
self.assertRegionContains(
self.player1.regions[1], self.player2.prog_items[0])
def test_ignores_priority_locations(self):
self.player2.prog_items[0].location.progress_type = LocationProgressType.PRIORITY
balance_multiworld_progression(self.multi_world)
self.assertRegionContains(
self.player1.regions[2], self.player2.prog_items[0])

54
test/general/TestIDs.py Normal file
View File

@@ -0,0 +1,54 @@
import unittest
from worlds.AutoWorld import AutoWorldRegister
class TestIDs(unittest.TestCase):
def testUniqueItems(self):
known_item_ids = set()
for gamename, world_type in AutoWorldRegister.world_types.items():
current = len(known_item_ids)
known_item_ids |= set(world_type.item_id_to_name)
self.assertEqual(len(known_item_ids) - len(world_type.item_id_to_name), current)
def testUniqueLocations(self):
known_location_ids = set()
for gamename, world_type in AutoWorldRegister.world_types.items():
current = len(known_location_ids)
known_location_ids |= set(world_type.location_id_to_name)
self.assertEqual(len(known_location_ids) - len(world_type.location_id_to_name), current)
def testRangeItems(self):
"""There are Javascript clients, which are limited to 2**53 integer size."""
for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename):
for item_id in world_type.item_id_to_name:
self.assertLess(item_id, 2**53)
def testRangeLocations(self):
"""There are Javascript clients, which are limited to 2**53 integer size."""
for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename):
for location_id in world_type.location_id_to_name:
self.assertLess(location_id, 2**53)
def testReservedItems(self):
"""negative item IDs are reserved to the special "Archipelago" world."""
for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename):
if gamename == "Archipelago":
for item_id in world_type.item_id_to_name:
self.assertLess(item_id, 0)
else:
for item_id in world_type.item_id_to_name:
self.assertGreater(item_id, 0)
def testReservedLocations(self):
"""negative location IDs are reserved to the special "Archipelago" world."""
for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename):
if gamename == "Archipelago":
for location_id in world_type.location_id_to_name:
self.assertLess(location_id, 0)
else:
for location_id in world_type.location_id_to_name:
self.assertGreater(location_id, 0)

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