Compare commits

...

239 Commits
0.1.7 ... 0.1.9

Author SHA1 Message Date
Fabian Dill
b57306beac MultiServer: Don't send password required indicator if the password is empty string (user intention is likely no password) 2021-10-15 22:08:24 +02:00
Fabian Dill
af6e159644 Docs: retarget :48484 links 2021-10-15 17:33:18 +02:00
Fabian Dill
54e50f69e1 Options: various fixes to get_option_name falsely giving get_current_option_name instead. 2021-10-14 19:42:13 +02:00
Fabian Dill
3f415b8265 WebHost: Re-Remove multidata race difference explanation, as it no longer exists. 2021-10-14 19:41:23 +02:00
Hussein Farran
8ccdb56bf1 Merge pull request #104 from alwaysintreble/ror2
Risk of rain 2: Revert breaking naming change
2021-10-14 13:25:34 -04:00
CaitSith2
17ed957c6b Include military science pack in all techs military or higher.
This does mean you have to get military science online to research your silo.
2021-10-14 10:20:56 -07:00
CaitSith2
e4564abe41 Fix tech-maniac achievement for silo spawn. 2021-10-13 07:03:18 -07:00
alwaysintreble
f16b29b16b Merge branch 'main' into ror2 2021-10-12 09:09:11 -05:00
Chris Wilson
ef8af7d618 Move config files and player-settings js files to /generated/configs and /generated/player-settings and update the pages that use them 2021-10-11 21:37:08 -04:00
Chris Wilson
79e33899a8 Supported game page game links now point to the game info page. Added a link below for the settings pages. 2021-10-11 21:20:31 -04:00
Chris Wilson
11fc220d4d Minor wording change on landing page. 2021-10-11 21:13:40 -04:00
Chris Wilson
a94a30168c Greatly improve the Start Playing page 2021-10-11 21:11:37 -04:00
Chris Wilson
19704920a4 TOuch up host game page 2021-10-11 20:58:05 -04:00
Chris Wilson
e4f4c1f1be Add Start Playing page, clean up /generate page 2021-10-11 20:52:30 -04:00
Jarno Westhof
065931cae7 Greatly reduced number of items marked as never_excluded due to the performance implications it brings 2021-10-11 11:55:46 +00:00
Fabian Dill
78443bffac Core: fix missed precollected change 2021-10-11 01:39:25 +02:00
Fabian Dill
a8b105267c WebHost: add hint cost and forfeit mode to webgen page 2021-10-11 00:46:18 +02:00
Fabian Dill
f7bd637073 Core: fix chain != chain.from_iterable 2021-10-11 00:12:00 +02:00
Fabian Dill
3e6f7f0fad WebHost: add /discord redirect 2021-10-10 21:52:58 +02:00
Jarno Westhof
e301b67e49 Greatly improved performance when no locations are excluded 2021-10-10 18:24:31 +00:00
Jarno Westhof
952d878442 Marked items as never exclude + some more refactorings 2021-10-10 18:24:31 +00:00
Fabian Dill
8f66f94ffa WebHost: Generate: Fix dead link 2021-10-10 20:14:11 +02:00
Fabian Dill
e66a2a7c30 Core: change precollected_items to dict-style
Core: make sure there are enough threads available during generate_output to prevent deadlocks if event waiting is used
2021-10-10 16:50:08 +02:00
CaitSith2
96ffe95404 hopefully fix lint error 2021-10-09 21:03:03 -07:00
CaitSith2
438e53d25e hints for visible tech should be free no matter who it is for. 2021-10-09 20:48:13 -07:00
CaitSith2
ca4b0acd92 Add !hint_location command.
As it turns out, because factorio location names are 100% identical to factorio item names,  it is impossible without a command that explicitly hints locations to hint a specific factorio location, or any other game where location names match item names.
2021-10-09 20:47:12 -07:00
CaitSith2
f8deb1bd7f Make visible_sending part of AutoWorld. 2021-10-09 20:38:53 -07:00
alwaysintreble
d8de84e417 Revert Item Pickup to ItemPickup because it broke stuff 2021-10-09 22:11:05 -05:00
espeon65536
eb602aedc3 Fill overworld-shuffle dungeon items with logic
Prevents maps and compasses from failing fast fill
2021-10-09 17:32:10 +00:00
Jarno Westhof
b539892cc0 Fixed Timespinner generation *oops* 2021-10-09 13:58:07 +00:00
Jarno Westhof
ba13d2179d Slightly improved docs about permissions flags 2021-10-09 13:58:07 +00:00
Jarno Westhof
c7a315ac97 Refactorings 2021-10-09 13:58:07 +00:00
alwaysintreble
b1fb793ea4 Ror2: fix generation mistake (#100)
* Risk of Rain 2: logic updates

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

* Documentation update
2021-10-09 15:57:37 +02:00
Fabian Dill
62db9ad982 MultiServer: send RoomUpdate -> permissions if permissions change 2021-10-09 15:24:08 +02:00
alwaysintreble
d3780cd9d5 Documentation update 2021-10-09 05:55:50 -05:00
Fabian Dill
6acd08431e Core: fix set_seed seed passthrough 2021-10-09 02:30:46 +02:00
Hussein Farran
76d591bab5 Update adding games.md 2021-10-08 17:20:05 -04:00
alwaysintreble
d10cab824a Merge branch 'ArchipelagoMW:main' into ror2 2021-10-08 13:29:25 -05:00
alwaysintreble
a93d633d25 Risk of Rain 2: move a variable definition so it can be reused. Reverted a change that broke stuff for some reason. 2021-10-08 13:27:23 -05:00
Fabian Dill
9ebab4a382 Core: fix set_seed argument order 2021-10-08 12:16:15 +02:00
alwaysintreble
cd53dcfe43 Fix typo 2021-10-08 10:10:12 +00:00
Fabian Dill
1985423a97 LttP: fix ER spoiler writing 2021-10-07 04:31:03 +02:00
Fabian Dill
f5afc84cd2 Tests: remove a breakpoint condition that was left ;P 2021-10-06 11:41:57 +02:00
Fabian Dill
1217179f8a Tests: Implement generic default options reachability test
Tests: remove duplicate TestDeathMountain.py
LttP: Move er_seeds out of Main
OriBF: Fix Mapstone typo
2021-10-06 11:32:49 +02:00
Fabian Dill
29a207b73e Docs: update networkgraph 2021-10-06 10:46:42 +02:00
Jarno Westhof
f7ecf02beb Added timespinner to graphml 2021-10-06 08:39:39 +00:00
CaitSith2
c5193ffdd9 GT flashing now disabled by reduce flashing. 2021-10-05 21:12:26 -07:00
Fabian Dill
916ba2ea41 Test: test against item/location ID overlap 2021-10-06 02:12:05 +02:00
espeon65536
3348dce122 Core: try-except-else style 2021-10-05 23:52:22 +00:00
espeon65536
53e6ca6e34 Core: better error message for exclusion failure 2021-10-05 23:52:22 +00:00
espeon65536
0fed7f1295 Core: do not error on location exclusion if the location has an ID value 2021-10-05 23:52:22 +00:00
Fabian Dill
6ade832029 Subnautica: fix Aurora Prawn Suit Bay requires laser cutter
Subnautica: add Dunes North Wreck's PDA to the correct wreck
Subnautica: fix typo in Yellow
Subnautica: fix progression tag for many items
Subnautica: move extra items from valuable item pool to fast-fill

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

* Fixed unnececerly recalculation of item_name_groups

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

* Marked the loss of location 1337158

* Updated network graph

* First draft tinmespinner documentation

* Moved personal items to slot_data rather than location scouts

* Disabled Remote Items

* Updated docs

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

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

View File

@@ -10,6 +10,9 @@ from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple
import secrets
import random
import Options
import Utils
class MultiWorld():
debug_types = False
@@ -22,9 +25,9 @@ class MultiWorld():
plando_texts: List[Dict[str, str]]
plando_items: List
plando_connections: List
er_seeds: Dict[int, str]
worlds: Dict[int, Any]
is_race: bool = False
precollected_items: Dict[int, List[Item]]
class AttributeProxy():
def __init__(self, rule):
@@ -44,7 +47,7 @@ class MultiWorld():
self.itempool = []
self.seed = None
self.seed_name: str = "Unavailable"
self.precollected_items = []
self.precollected_items = {player: [] for player in self.player_ids}
self.state = CollectionState(self)
self._cached_entrances = None
self._cached_locations = None
@@ -63,28 +66,29 @@ class MultiWorld():
self.dynamic_regions = []
self.dynamic_locations = []
self.spoiler = Spoiler(self)
self.fix_trock_doors = self.AttributeProxy(lambda player: self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted')
self.fix_skullwoods_exit = self.AttributeProxy(lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
self.fix_palaceofdarkness_exit = self.AttributeProxy(lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
self.fix_trock_exit = self.AttributeProxy(lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
self.fix_trock_doors = self.AttributeProxy(
lambda player: self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted')
self.fix_skullwoods_exit = self.AttributeProxy(
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
self.fix_palaceofdarkness_exit = self.AttributeProxy(
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
self.fix_trock_exit = self.AttributeProxy(
lambda player: self.shuffle[player] not in ['vanilla', 'simple', 'restricted', 'dungeonssimple'])
self.NOTCURSED = self.AttributeProxy(lambda player: not self.CURSED[player])
for player in range(1, players + 1):
def set_player_attr(attr, val):
self.__dict__.setdefault(attr, {})[player] = val
set_player_attr('tech_tree_layout_prerequisites', {})
set_player_attr('_region_cache', {})
set_player_attr('shuffle', "vanilla")
set_player_attr('logic', "noglitches")
set_player_attr('mode', 'open')
set_player_attr('swordless', False)
set_player_attr('difficulty', 'normal')
set_player_attr('item_functionality', 'normal')
set_player_attr('timer', False)
set_player_attr('goal', 'ganon')
set_player_attr('accessibility', 'items')
set_player_attr('retro', False)
set_player_attr('hints', True)
set_player_attr('required_medallions', ['Ether', 'Quake'])
set_player_attr('swamp_patch_required', False)
set_player_attr('powder_patch_required', False)
@@ -97,12 +101,8 @@ class MultiWorld():
set_player_attr('fix_fake_world', True)
set_player_attr('difficulty_requirements', None)
set_player_attr('boss_shuffle', 'none')
set_player_attr('enemy_shuffle', False)
set_player_attr('enemy_health', 'default')
set_player_attr('enemy_damage', 'default')
set_player_attr('killable_thieves', False)
set_player_attr('tile_shuffle', False)
set_player_attr('bush_shuffle', False)
set_player_attr('beemizer', 0)
set_player_attr('escape_assist', [])
set_player_attr('open_pyramid', False)
@@ -114,16 +114,12 @@ class MultiWorld():
set_player_attr('blue_clock_time', 2)
set_player_attr('green_clock_time', 4)
set_player_attr('can_take_damage', True)
set_player_attr('progression_balancing', True)
set_player_attr('local_items', set())
set_player_attr('non_local_items', set())
set_player_attr('triforce_pieces_available', 30)
set_player_attr('triforce_pieces_required', 20)
set_player_attr('shop_shuffle', 'off')
set_player_attr('shuffle_prizes', "g")
set_player_attr('sprite_pool', [])
set_player_attr('dark_room_logic', "lamp")
set_player_attr('restrict_dungeon_item_on_boss', False)
set_player_attr('plando_items', [])
set_player_attr('plando_texts', {})
set_player_attr('plando_connections', [])
@@ -131,16 +127,38 @@ class MultiWorld():
set_player_attr('completion_condition', lambda state: True)
self.custom_data = {}
self.worlds = {}
self.slot_seeds = {}
def set_seed(self, seed: Optional[int] = None, secure: bool = False, name: Optional[str] = None):
self.seed = get_seed(seed)
if secure:
self.secure()
else:
self.random.seed(self.seed)
self.seed_name = name if name else str(self.seed)
self.slot_seeds = {player: random.Random(self.random.getrandbits(64)) for player in
range(1, self.players + 1)}
def set_options(self, args):
from worlds import AutoWorld
for player in self.player_ids:
self.custom_data[player] = {}
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
for option in world_type.options:
setattr(self, option, getattr(args, option, {}))
for option_key in world_type.options:
setattr(self, option_key, getattr(args, option_key, {}))
for option_key in Options.common_options:
setattr(self, option_key, getattr(args, option_key, {}))
for option_key in Options.per_game_common_options:
setattr(self, option_key, getattr(args, option_key, {}))
self.worlds[player] = world_type(self, player)
# intended for unittests
def set_default_common_options(self):
for option_key, option in Options.common_options.items():
setattr(self, option_key, {player_id: option(option.default) for player_id in self.player_ids})
for option_key, option in Options.per_game_common_options.items():
setattr(self, option_key, {player_id: option(option.default) for player_id in self.player_ids})
def secure(self):
self.random = secrets.SystemRandom()
self.is_race = True
@@ -228,7 +246,7 @@ class MultiWorld():
if item.name in subworld.dungeon_local_item_names:
subworld.collect(ret, item)
ret.sweep_for_events()
if use_cache:
self._all_state = ret
return ret
@@ -249,7 +267,7 @@ class MultiWorld():
def push_precollected(self, item: Item):
item.world = self
self.precollected_items.append(item)
self.precollected_items[item.player].append(item)
self.state.collect(item, True)
def push_item(self, location: Location, item: Item, collect: bool = True):
@@ -268,7 +286,7 @@ class MultiWorld():
else:
raise RuntimeError('Cannot assign item %s to location %s.' % (item, location))
def get_entrances(self) -> list:
def get_entrances(self) -> List[Entrance]:
if self._cached_entrances is None:
self._cached_entrances = [entrance for region in self.regions for entrance in region.entrances]
return self._cached_entrances
@@ -276,7 +294,7 @@ class MultiWorld():
def clear_entrance_cache(self):
self._cached_entrances = None
def get_locations(self) -> list:
def get_locations(self) -> List[Location]:
if self._cached_locations is None:
self._cached_locations = [location for region in self.regions for location in region.locations]
return self._cached_locations
@@ -284,7 +302,7 @@ class MultiWorld():
def clear_location_cache(self):
self._cached_locations = None
def get_unfilled_locations(self, player=None) -> list:
def get_unfilled_locations(self, player=None) -> List[Location]:
if player is not None:
return [location for location in self.get_locations() if
location.player == player and not location.item]
@@ -293,19 +311,19 @@ class MultiWorld():
def get_unfilled_dungeon_locations(self):
return [location for location in self.get_locations() if not location.item and location.parent_region.dungeon]
def get_filled_locations(self, player=None) -> list:
def get_filled_locations(self, player=None) -> List[Location]:
if player is not None:
return [location for location in self.get_locations() if
location.player == player and location.item is not None]
return [location for location in self.get_locations() if location.item is not None]
def get_reachable_locations(self, state=None, player=None) -> list:
def get_reachable_locations(self, state=None, player=None) -> List[Location]:
if state is None:
state = self.state
return [location for location in self.get_locations() if
(player is None or location.player == player) and location.can_reach(state)]
def get_placeable_locations(self, state=None, player=None) -> list:
def get_placeable_locations(self, state=None, player=None) -> List[Location]:
if state is None:
state = self.state
return [location for location in self.get_locations() if
@@ -333,7 +351,7 @@ class MultiWorld():
else:
return all((self.has_beaten_game(state, p) for p in range(1, self.players + 1)))
def can_beat_game(self, starting_state : Optional[CollectionState]=None):
def can_beat_game(self, starting_state: Optional[CollectionState] = None):
if starting_state:
if self.has_beaten_game(starting_state):
return True
@@ -390,21 +408,21 @@ class MultiWorld():
"""Check if accessibility rules are fulfilled with current or supplied state."""
if not state:
state = CollectionState(self)
players = {"none" : set(),
players = {"minimal": set(),
"items": set(),
"locations": set()}
for player, access in self.accessibility.items():
players[access].add(player)
players[access.current_key].add(player)
beatable_fulfilled = False
def location_conditition(location : Location):
def location_conditition(location: Location):
"""Determine if this location has to be accessible, location is already filtered by location_relevant"""
if location.player in players["none"]:
if location.player in players["minimal"]:
return False
return True
def location_relevant(location : Location):
def location_relevant(location: Location):
"""Determine if this location is relevant to sweep."""
if location.player in players["locations"] or location.event or \
(location.item and location.item.advancement):
@@ -456,8 +474,9 @@ class CollectionState(object):
self.path = {}
self.locations_checked = set()
self.stale = {player: True for player in range(1, parent.players + 1)}
for item in parent.precollected_items:
self.collect(item, True)
for items in parent.precollected_items.values():
for item in items:
self.collect(item, True)
def update_reachable_regions(self, player: int):
from worlds.alttp.EntranceShuffle import indirect_connections
@@ -497,7 +516,8 @@ class CollectionState(object):
ret.prog_items = self.prog_items.copy()
ret.reachable_regions = {player: copy.copy(self.reachable_regions[player]) for player in
range(1, self.world.players + 1)}
ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in range(1, self.world.players + 1)}
ret.blocked_connections = {player: copy.copy(self.blocked_connections[player]) for player in
range(1, self.world.players + 1)}
ret.events = copy.copy(self.events)
ret.path = copy.copy(self.path)
ret.locations_checked = copy.copy(self.locations_checked)
@@ -533,10 +553,10 @@ class CollectionState(object):
def has(self, item, player: int, count: int = 1):
return self.prog_items[item, player] >= count
def has_all(self, items: Set[str], player:int):
def has_all(self, items: Set[str], player: int):
return all(self.prog_items[item, player] for item in items)
def has_any(self, items: Set[str], player:int):
def has_any(self, items: Set[str], player: int):
return any(self.prog_items[item, player] for item in items)
def has_group(self, item_name_group: str, player: int, count: int = 1):
@@ -636,10 +656,10 @@ class CollectionState(object):
self.is_not_bunny(cave, player)
)
def can_retrieve_tablet(self, player:int) -> bool:
def can_retrieve_tablet(self, player: int) -> bool:
return self.has('Book of Mudora', player) and (self.has_beam_sword(player) or
(self.world.swordless[player] and
self.has("Hammer", player)))
(self.world.swordless[player] and
self.has("Hammer", player)))
def has_sword(self, player: int) -> bool:
return self.has('Fighter Sword', player) \
@@ -648,7 +668,8 @@ class CollectionState(object):
or self.has('Golden Sword', player)
def has_beam_sword(self, player: int) -> bool:
return self.has('Master Sword', player) or self.has('Tempered Sword', player) or self.has('Golden Sword', player)
return self.has('Master Sword', player) or self.has('Tempered Sword', player) or self.has('Golden Sword',
player)
def has_melee_weapon(self, player: int) -> bool:
return self.has_sword(player) or self.has('Hammer', player)
@@ -712,7 +733,7 @@ class CollectionState(object):
rules.append(self.has('Moon Pearl', player))
return all(rules)
def can_bomb_clip(self, region: Region, player: int) -> bool:
def can_bomb_clip(self, region: Region, player: int) -> bool:
return self.is_not_bunny(region, player) and self.has('Pegasus Boots', player)
def collect(self, item: Item, event: bool = False, location: Location = None) -> bool:
@@ -740,12 +761,13 @@ class CollectionState(object):
self.blocked_connections[item.player] = set()
self.stale[item.player] = True
@unique
class RegionType(int, Enum):
Generic = 0
LightWorld = 1
DarkWorld = 2
Cave = 3 # Also includes Houses
Cave = 3 # Also includes Houses
Dungeon = 4
@property
@@ -867,6 +889,7 @@ class Dungeon(object):
def __str__(self):
return self.world.get_name_string_for_object(self) if self.world else f'{self.name} (Player {self.player})'
class Boss():
def __init__(self, name, enemizer_name, defeat_rule, player: int):
self.name = name
@@ -880,6 +903,7 @@ class Boss():
def __repr__(self):
return f"Boss({self.name})"
class Location():
# If given as integer, then this is the shop's inventory index
shop_slot: Optional[int] = None
@@ -895,7 +919,7 @@ class Location():
access_rule = staticmethod(lambda state: True)
item_rule = staticmethod(lambda item: True)
def __init__(self, player: int, name: str = '', address:int = None, parent=None):
def __init__(self, player: int, name: str = '', address: int = None, parent=None):
self.name: str = name
self.address: Optional[int] = address
self.parent_region: Region = parent
@@ -948,9 +972,12 @@ class Location():
class Item():
location: Optional[Location] = None
world: Optional[MultiWorld] = None
code: Optional[str] = None # an item with ID None is called an Event, and does not get written to multidata
code: Optional[int] = None # an item with ID None is called an Event, and does not get written to multidata
name: str
game: str = "Generic"
type: str = None
# indicates if this is a negative impact item. Causes these to be handled differently by various games.
trap: bool = False
# change manually to ensure that a specific non-progression item never goes on an excluded location
never_exclude = False
@@ -1009,67 +1036,87 @@ class Spoiler():
self.medallions = {}
self.playthrough = {}
self.unreachables = []
self.startinventory = []
self.locations = {}
self.paths = {}
self.metadata = {}
self.shops = []
self.bosses = OrderedDict()
def set_entrance(self, entrance, exit, direction, player):
if self.world.players == 1:
self.entrances[(entrance, direction, player)] = OrderedDict([('entrance', entrance), ('exit', exit), ('direction', direction)])
self.entrances[(entrance, direction, player)] = OrderedDict(
[('entrance', entrance), ('exit', exit), ('direction', direction)])
else:
self.entrances[(entrance, direction, player)] = OrderedDict([('player', player), ('entrance', entrance), ('exit', exit), ('direction', direction)])
self.entrances[(entrance, direction, player)] = OrderedDict(
[('player', player), ('entrance', entrance), ('exit', exit), ('direction', direction)])
def parse_data(self):
self.medallions = OrderedDict()
for player in self.world.get_game_players("A Link to the Past"):
self.medallions[f'Misery Mire ({self.world.get_player_name(player)})'] = self.world.required_medallions[player][0]
self.medallions[f'Turtle Rock ({self.world.get_player_name(player)})'] = self.world.required_medallions[player][1]
self.startinventory = list(map(str, self.world.precollected_items))
self.medallions[f'Misery Mire ({self.world.get_player_name(player)})'] = \
self.world.required_medallions[player][0]
self.medallions[f'Turtle Rock ({self.world.get_player_name(player)})'] = \
self.world.required_medallions[player][1]
self.locations = OrderedDict()
listed_locations = set()
lw_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.LightWorld and loc.show_in_spoiler]
self.locations['Light World'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in lw_locations])
lw_locations = [loc for loc in self.world.get_locations() if
loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.LightWorld and loc.show_in_spoiler]
self.locations['Light World'] = OrderedDict(
[(str(location), str(location.item) if location.item is not None else 'Nothing') for location in
lw_locations])
listed_locations.update(lw_locations)
dw_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.DarkWorld and loc.show_in_spoiler]
self.locations['Dark World'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in dw_locations])
dw_locations = [loc for loc in self.world.get_locations() if
loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.DarkWorld and loc.show_in_spoiler]
self.locations['Dark World'] = OrderedDict(
[(str(location), str(location.item) if location.item is not None else 'Nothing') for location in
dw_locations])
listed_locations.update(dw_locations)
cave_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.Cave and loc.show_in_spoiler]
self.locations['Caves'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in cave_locations])
cave_locations = [loc for loc in self.world.get_locations() if
loc not in listed_locations and loc.parent_region and loc.parent_region.type == RegionType.Cave and loc.show_in_spoiler]
self.locations['Caves'] = OrderedDict(
[(str(location), str(location.item) if location.item is not None else 'Nothing') for location in
cave_locations])
listed_locations.update(cave_locations)
for dungeon in self.world.dungeons.values():
dungeon_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.parent_region and loc.parent_region.dungeon == dungeon and loc.show_in_spoiler]
self.locations[str(dungeon)] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in dungeon_locations])
dungeon_locations = [loc for loc in self.world.get_locations() if
loc not in listed_locations and loc.parent_region and loc.parent_region.dungeon == dungeon and loc.show_in_spoiler]
self.locations[str(dungeon)] = OrderedDict(
[(str(location), str(location.item) if location.item is not None else 'Nothing') for location in
dungeon_locations])
listed_locations.update(dungeon_locations)
other_locations = [loc for loc in self.world.get_locations() if loc not in listed_locations and loc.show_in_spoiler]
other_locations = [loc for loc in self.world.get_locations() if
loc not in listed_locations and loc.show_in_spoiler]
if other_locations:
self.locations['Other Locations'] = OrderedDict([(str(location), str(location.item) if location.item is not None else 'Nothing') for location in other_locations])
self.locations['Other Locations'] = OrderedDict(
[(str(location), str(location.item) if location.item is not None else 'Nothing') for location in
other_locations])
listed_locations.update(other_locations)
self.shops = []
from worlds.alttp.Shops import ShopType
from worlds.alttp.Shops import ShopType, price_type_display_name, price_rate_display
for shop in self.world.shops:
if not shop.custom:
continue
shopdata = {'location': str(shop.region),
'type': 'Take Any' if shop.type == ShopType.TakeAny else 'Shop'
}
shopdata = {
'location': str(shop.region),
'type': 'Take Any' if shop.type == ShopType.TakeAny else 'Shop'
}
for index, item in enumerate(shop.inventory):
if item is None:
continue
shopdata['item_{}'.format(index)] = "{}{}".format(item['item'], item['price']) if item['price'] else item['item']
my_price = item['price'] // price_rate_display.get(item['price_type'], 1)
shopdata['item_{}'.format(
index)] = f"{item['item']}{my_price} {price_type_display_name[item['price_type']]}"
if item['player'] > 0:
shopdata['item_{}'.format(index)] = shopdata['item_{}'.format(index)].replace('', '(Player {}) — '.format(item['player']))
shopdata['item_{}'.format(index)] = shopdata['item_{}'.format(index)].replace('',
'(Player {}) — '.format(
item['player']))
if item['max'] == 0:
continue
@@ -1077,7 +1124,8 @@ class Spoiler():
if item['replacement'] is None:
continue
shopdata['item_{}'.format(index)] += ", {} - {}".format(item['replacement'], item['replacement_price']) if item['replacement_price'] else item['replacement']
shopdata['item_{}'.format(
index)] += f", {item['replacement']} - {item['replacement_price']} {price_type_display_name[item['replacement_price_type']]}"
self.shops.append(shopdata)
for player in self.world.get_game_players("A Link to the Past"):
@@ -1086,7 +1134,8 @@ class Spoiler():
self.bosses[str(player)]["Desert Palace"] = self.world.get_dungeon("Desert Palace", player).boss.name
self.bosses[str(player)]["Tower Of Hera"] = self.world.get_dungeon("Tower of Hera", player).boss.name
self.bosses[str(player)]["Hyrule Castle"] = "Agahnim"
self.bosses[str(player)]["Palace Of Darkness"] = self.world.get_dungeon("Palace of Darkness", player).boss.name
self.bosses[str(player)]["Palace Of Darkness"] = self.world.get_dungeon("Palace of Darkness",
player).boss.name
self.bosses[str(player)]["Swamp Palace"] = self.world.get_dungeon("Swamp Palace", player).boss.name
self.bosses[str(player)]["Skull Woods"] = self.world.get_dungeon("Skull Woods", player).boss.name
self.bosses[str(player)]["Thieves Town"] = self.world.get_dungeon("Thieves Town", player).boss.name
@@ -1094,58 +1143,28 @@ class Spoiler():
self.bosses[str(player)]["Misery Mire"] = self.world.get_dungeon("Misery Mire", player).boss.name
self.bosses[str(player)]["Turtle Rock"] = self.world.get_dungeon("Turtle Rock", player).boss.name
if self.world.mode[player] != 'inverted':
self.bosses[str(player)]["Ganons Tower Basement"] = self.world.get_dungeon('Ganons Tower', player).bosses['bottom'].name
self.bosses[str(player)]["Ganons Tower Middle"] = self.world.get_dungeon('Ganons Tower', player).bosses['middle'].name
self.bosses[str(player)]["Ganons Tower Top"] = self.world.get_dungeon('Ganons Tower', player).bosses['top'].name
self.bosses[str(player)]["Ganons Tower Basement"] = \
self.world.get_dungeon('Ganons Tower', player).bosses['bottom'].name
self.bosses[str(player)]["Ganons Tower Middle"] = self.world.get_dungeon('Ganons Tower', player).bosses[
'middle'].name
self.bosses[str(player)]["Ganons Tower Top"] = self.world.get_dungeon('Ganons Tower', player).bosses[
'top'].name
else:
self.bosses[str(player)]["Ganons Tower Basement"] = self.world.get_dungeon('Inverted Ganons Tower', player).bosses['bottom'].name
self.bosses[str(player)]["Ganons Tower Middle"] = self.world.get_dungeon('Inverted Ganons Tower', player).bosses['middle'].name
self.bosses[str(player)]["Ganons Tower Top"] = self.world.get_dungeon('Inverted Ganons Tower', player).bosses['top'].name
self.bosses[str(player)]["Ganons Tower Basement"] = \
self.world.get_dungeon('Inverted Ganons Tower', player).bosses['bottom'].name
self.bosses[str(player)]["Ganons Tower Middle"] = \
self.world.get_dungeon('Inverted Ganons Tower', player).bosses['middle'].name
self.bosses[str(player)]["Ganons Tower Top"] = \
self.world.get_dungeon('Inverted Ganons Tower', player).bosses['top'].name
self.bosses[str(player)]["Ganons Tower"] = "Agahnim 2"
self.bosses[str(player)]["Ganon"] = "Ganon"
from Utils import __version__ as APVersion
self.metadata = {'version': APVersion,
'logic': self.world.logic,
'dark_room_logic': self.world.dark_room_logic,
'mode': self.world.mode,
'retro': self.world.retro,
'swordless': self.world.swordless,
'goal': self.world.goal,
'shuffle': self.world.shuffle,
'item_pool': self.world.difficulty,
'item_functionality': self.world.item_functionality,
'open_pyramid': self.world.open_pyramid,
'accessibility': self.world.accessibility,
'hints': self.world.hints,
'boss_shuffle': self.world.boss_shuffle,
'enemy_shuffle': self.world.enemy_shuffle,
'enemy_health': self.world.enemy_health,
'enemy_damage': self.world.enemy_damage,
'killable_thieves': self.world.killable_thieves,
'tile_shuffle': self.world.tile_shuffle,
'bush_shuffle': self.world.bush_shuffle,
'beemizer': self.world.beemizer,
'shufflepots': self.world.shufflepots,
'players': self.world.players,
'progression_balancing': self.world.progression_balancing,
'triforce_pieces_available': self.world.triforce_pieces_available,
'triforce_pieces_required': self.world.triforce_pieces_required,
'shop_shuffle': self.world.shop_shuffle,
'shuffle_prizes': self.world.shuffle_prizes,
'sprite_pool': self.world.sprite_pool,
'restrict_dungeon_item_on_boss': self.world.restrict_dungeon_item_on_boss,
'game': self.world.game,
'er_seeds': self.world.er_seeds
}
def to_json(self):
self.parse_data()
out = OrderedDict()
out['Entrances'] = list(self.entrances.values())
out.update(self.locations)
out['Starting Inventory'] = self.startinventory
out['Special'] = self.medallions
if self.hashes:
out['Hashes'] = self.hashes
@@ -1154,7 +1173,6 @@ class Spoiler():
out['playthrough'] = self.playthrough
out['paths'] = self.paths
out['Bosses'] = self.bosses
out['meta'] = self.metadata
return json.dumps(out)
@@ -1166,10 +1184,18 @@ class Spoiler():
return variable
return 'Yes' if variable else 'No'
def write_option(option_key: str, option_obj: type(Options.Option)):
res = getattr(self.world, option_key)[player]
displayname = getattr(option_obj, "displayname", option_key)
try:
outfile.write(f'{displayname + ":":33}{res.get_current_option_name()}\n')
except:
raise Exception
with open(filename, 'w', encoding="utf-8-sig") as outfile:
outfile.write(
'Archipelago Version %s - Seed: %s\n\n' % (
self.metadata['version'], self.world.seed))
Utils.__version__, self.world.seed))
outfile.write('Filling Algorithm: %s\n' % self.world.algorithm)
outfile.write('Players: %d\n' % self.world.players)
@@ -1177,68 +1203,51 @@ class Spoiler():
if self.world.players > 1:
outfile.write('\nPlayer %d: %s\n' % (player, self.world.get_player_name(player)))
outfile.write('Game: %s\n' % self.world.game[player])
if self.world.players > 1:
outfile.write('Progression Balanced: %s\n' % (
'Yes' if self.metadata['progression_balancing'][player] else 'No'))
outfile.write('Accessibility: %s\n' % self.metadata['accessibility'][player])
for f_option, option in Options.common_options.items():
write_option(f_option, option)
for f_option, option in Options.per_game_common_options.items():
write_option(f_option, option)
options = self.world.worlds[player].options
if options:
for f_option, option in options.items():
res = getattr(self.world, f_option)[player]
displayname = getattr(option, "displayname", f_option)
outfile.write(f'{displayname + ":":33}{res.get_current_option_name()}\n')
write_option(f_option, option)
if player in self.world.get_game_players("A Link to the Past"):
outfile.write('%s%s\n' % ('Hash: ', self.hashes[player]))
outfile.write('Logic: %s\n' % self.metadata['logic'][player])
outfile.write('Dark Room Logic: %s\n' % self.metadata['dark_room_logic'][player])
outfile.write('Restricted Boss Drops: %s\n' %
bool_to_text(self.metadata['restrict_dungeon_item_on_boss'][player]))
outfile.write('Mode: %s\n' % self.metadata['mode'][player])
outfile.write('Retro: %s\n' %
('Yes' if self.metadata['retro'][player] else 'No'))
outfile.write('Swordless: %s\n' % ('Yes' if self.metadata['swordless'][player] else 'No'))
outfile.write('Goal: %s\n' % self.metadata['goal'][player])
if "triforce" in self.metadata["goal"][player]: # triforce hunt
outfile.write('Logic: %s\n' % self.world.logic[player])
outfile.write('Dark Room Logic: %s\n' % self.world.dark_room_logic[player])
outfile.write('Mode: %s\n' % self.world.mode[player])
outfile.write('Goal: %s\n' % self.world.goal[player])
if "triforce" in self.world.goal[player]: # triforce hunt
outfile.write("Pieces available for Triforce: %s\n" %
self.metadata['triforce_pieces_available'][player])
self.world.triforce_pieces_available[player])
outfile.write("Pieces required for Triforce: %s\n" %
self.metadata["triforce_pieces_required"][player])
outfile.write('Difficulty: %s\n' % self.metadata['item_pool'][player])
outfile.write('Item Functionality: %s\n' % self.metadata['item_functionality'][player])
outfile.write('Entrance Shuffle: %s\n' % self.metadata['shuffle'][player])
if self.metadata['shuffle'][player] != "vanilla":
outfile.write('Entrance Shuffle Seed %s\n' % self.metadata['er_seeds'][player])
self.world.triforce_pieces_required[player])
outfile.write('Difficulty: %s\n' % self.world.difficulty[player])
outfile.write('Item Functionality: %s\n' % self.world.item_functionality[player])
outfile.write('Entrance Shuffle: %s\n' % self.world.shuffle[player])
if self.world.shuffle[player] != "vanilla":
outfile.write('Entrance Shuffle Seed %s\n' % self.world.worlds[player].er_seed)
outfile.write('Pyramid hole pre-opened: %s\n' % (
'Yes' if self.metadata['open_pyramid'][player] else 'No'))
'Yes' if self.world.open_pyramid[player] else 'No'))
outfile.write('Shop inventory shuffle: %s\n' %
bool_to_text("i" in self.metadata["shop_shuffle"][player]))
bool_to_text("i" in self.world.shop_shuffle[player]))
outfile.write('Shop price shuffle: %s\n' %
bool_to_text("p" in self.metadata["shop_shuffle"][player]))
bool_to_text("p" in self.world.shop_shuffle[player]))
outfile.write('Shop upgrade shuffle: %s\n' %
bool_to_text("u" in self.metadata["shop_shuffle"][player]))
bool_to_text("u" in self.world.shop_shuffle[player]))
outfile.write('New Shop inventory: %s\n' %
bool_to_text("g" in self.metadata["shop_shuffle"][player] or
"f" in self.metadata["shop_shuffle"][player]))
bool_to_text("g" in self.world.shop_shuffle[player] or
"f" in self.world.shop_shuffle[player]))
outfile.write('Custom Potion Shop: %s\n' %
bool_to_text("w" in self.metadata["shop_shuffle"][player]))
outfile.write('Boss shuffle: %s\n' % self.metadata['boss_shuffle'][player])
outfile.write(
'Enemy shuffle: %s\n' % bool_to_text(self.metadata['enemy_shuffle'][player]))
outfile.write('Enemy health: %s\n' % self.metadata['enemy_health'][player])
outfile.write('Enemy damage: %s\n' % self.metadata['enemy_damage'][player])
outfile.write(f'Killable thieves: {bool_to_text(self.metadata["killable_thieves"][player])}\n')
outfile.write(f'Shuffled tiles: {bool_to_text(self.metadata["tile_shuffle"][player])}\n')
outfile.write(f'Shuffled bushes: {bool_to_text(self.metadata["bush_shuffle"][player])}\n')
outfile.write(
'Hints: %s\n' % ('Yes' if self.metadata['hints'][player] else 'No'))
outfile.write('Beemizer: %s\n' % self.metadata['beemizer'][player])
outfile.write('Pot shuffle %s\n'
% ('Yes' if self.metadata['shufflepots'][player] else 'No'))
bool_to_text("w" in self.world.shop_shuffle[player]))
outfile.write('Boss shuffle: %s\n' % self.world.boss_shuffle[player])
outfile.write('Enemy health: %s\n' % self.world.enemy_health[player])
outfile.write('Enemy damage: %s\n' % self.world.enemy_damage[player])
outfile.write('Beemizer: %s\n' % self.world.beemizer[player])
outfile.write('Prize shuffle %s\n' %
self.metadata['shuffle_prizes'][player])
self.world.shuffle_prizes[player])
if self.entrances:
outfile.write('\n\nEntrances:\n\n')
outfile.write('\n'.join(['%s%s %s %s' % (f'{self.world.get_player_name(entry["player"])}: '
@@ -1259,27 +1268,31 @@ class Spoiler():
for recipe in self.world.worlds[player].custom_recipes.values():
outfile.write(f"\n{recipe.name} ({name}): {recipe.ingredients} -> {recipe.products}")
if self.startinventory:
outfile.write('\n\nStarting Inventory:\n\n')
outfile.write('\n'.join(self.startinventory))
outfile.write('\n\nLocations:\n\n')
outfile.write('\n'.join(['%s: %s' % (location, item) for grouping in self.locations.values() for (location, item) in grouping.items()]))
outfile.write('\n'.join(
['%s: %s' % (location, item) for grouping in self.locations.values() for (location, item) in
grouping.items()]))
if self.shops:
outfile.write('\n\nShops:\n\n')
outfile.write('\n'.join("{} [{}]\n {}".format(shop['location'], shop['type'], "\n ".join(item for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if item)) for shop in self.shops))
outfile.write('\n'.join("{} [{}]\n {}".format(shop['location'], shop['type'], "\n ".join(
item for item in [shop.get('item_0', None), shop.get('item_1', None), shop.get('item_2', None)] if
item)) for shop in self.shops))
for player in self.world.get_game_players("A Link to the Past"):
if self.world.boss_shuffle[player] != 'none':
bossmap = self.bosses[str(player)] if self.world.players > 1 else self.bosses
outfile.write(f'\n\nBosses{(f" ({self.world.get_player_name(player)})" if self.world.players > 1 else "")}:\n')
outfile.write(' '+'\n '.join([f'{x}: {y}' for x, y in bossmap.items()]))
outfile.write(
f'\n\nBosses{(f" ({self.world.get_player_name(player)})" if self.world.players > 1 else "")}:\n')
outfile.write(' ' + '\n '.join([f'{x}: {y}' for x, y in bossmap.items()]))
outfile.write('\n\nPlaythrough:\n\n')
outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join([' %s: %s' % (location, item) for (location, item) in sphere.items()] if sphere_nr != '0' else [f' {item}' for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()]))
outfile.write('\n'.join(['%s: {\n%s\n}' % (sphere_nr, '\n'.join(
[' %s: %s' % (location, item) for (location, item) in sphere.items()] if sphere_nr != '0' else [
f' {item}' for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()]))
if self.unreachables:
outfile.write('\n\nUnreachable Items:\n\n')
outfile.write('\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables]))
outfile.write(
'\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables]))
if self.paths:
outfile.write('\n\nPaths:\n\n')
@@ -1294,3 +1307,13 @@ class Spoiler():
path_listings.append("{}\n {}".format(location, "\n => ".join(path_lines)))
outfile.write('\n'.join(path_listings))
seeddigits = 20
def get_seed(seed=None):
if seed is None:
random.seed(None)
return random.randint(0, pow(10, seeddigits) - 1)
return seed

View File

@@ -4,12 +4,13 @@ import typing
import asyncio
import urllib.parse
import sys
import os
import websockets
import Utils
from MultiServer import CommandProcessor
from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, color, ClientStatus
from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission
from Utils import Version
from worlds import network_data_package, AutoWorldRegister
@@ -17,6 +18,10 @@ logger = logging.getLogger("Client")
gui_enabled = Utils.is_frozen() or "--nogui" not in sys.argv
log_folder = Utils.local_path("logs")
os.makedirs(log_folder, exist_ok=True)
class ClientCommandProcessor(CommandProcessor):
def __init__(self, ctx: CommonContext):
self.ctx = ctx
@@ -86,11 +91,12 @@ class ClientCommandProcessor(CommandProcessor):
class CommonContext():
starting_reconnect_delay = 5
current_reconnect_delay = starting_reconnect_delay
command_processor = ClientCommandProcessor
game: None
ui: None
starting_reconnect_delay: int = 5
current_reconnect_delay: int = starting_reconnect_delay
command_processor: int = ClientCommandProcessor
game = None
ui = None
keep_alive_task = None
def __init__(self, server_address, password):
# server state
@@ -127,6 +133,9 @@ class CommonContext():
self.jsontotextparser = JSONtoTextParser(self)
self.set_getters(network_data_package)
# execution
self.keep_alive_task = asyncio.create_task(keep_alive(self))
async def connection_closed(self):
self.auth = None
self.items_received = []
@@ -190,7 +199,10 @@ class CommonContext():
def event_invalid_slot(self):
raise Exception('Invalid Slot; please verify that you have connected to the correct world.')
async def server_auth(self, password_requested):
def event_invalid_game(self):
raise Exception('Invalid Game; please verify that you connected with the right game to the correct world.')
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
logger.info('Enter the password required to join this game:')
self.password = await self.console_input()
@@ -219,6 +231,19 @@ class CommonContext():
pass
async def keep_alive(ctx: CommonContext, seconds_between_checks=100):
"""some ISPs/network configurations drop TCP connections if no payload is sent (ignore TCP-keep-alive)
so we send a payload to prevent drop and if we were dropped anyway this will cause an auto-reconnect."""
seconds_elapsed = 0
while not ctx.exit_event.is_set():
await asyncio.sleep(1) # short sleep to not block program shutdown
if ctx.server and ctx.slot:
seconds_elapsed += 1
if seconds_elapsed > seconds_between_checks:
await ctx.send_msgs([{"cmd": "Bounce", "slots": [ctx.slot]}])
seconds_elapsed = 0
async def server_loop(ctx: CommonContext, address=None):
cached_address = None
if ctx.server and ctx.server.socket:
@@ -252,13 +277,13 @@ async def server_loop(ctx: CommonContext, address=None):
logger.error('Unable to connect to multiworld server at cached address. '
'Please use the connect button above.')
else:
logger.error('Connection refused by the multiworld server')
logger.exception('Connection refused by the multiworld server')
except websockets.InvalidURI:
logger.exception('Failed to connect to the multiworld server (invalid URI)')
except (OSError, websockets.InvalidURI):
logger.error('Failed to connect to the multiworld server')
logger.exception('Failed to connect to the multiworld server')
except Exception as e:
logger.error('Lost connection to the multiworld server, type /connect to reconnect')
if not isinstance(e, websockets.WebSocketException):
logger.exception(e)
logger.exception('Lost connection to the multiworld server, type /connect to reconnect')
finally:
await ctx.connection_closed()
if ctx.server_address:
@@ -294,16 +319,17 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
logger.info("Server protocol tags: " + ", ".join(args["tags"]))
if args['password']:
logger.info('Password required')
logger.info(f"Forfeit setting: {args['forfeit_mode']}")
logger.info(f"Remaining setting: {args['remaining_mode']}")
for permission_name, permission_flag in args.get("permissions", {}).items():
flag = Permission(permission_flag)
logger.info(f"{permission_name.capitalize()} permission: {flag.name}")
logger.info(
f"A !hint costs {args['hint_cost']}% of your total location count as points"
f" and you get {args['location_check_points']}"
f" for each location checked. Use !hint for more information.")
ctx.hint_cost = int(args['hint_cost'])
ctx.check_points = int(args['location_check_points'])
ctx.forfeit_mode = args['forfeit_mode']
ctx.remaining_mode = args['remaining_mode']
if len(args['players']) < 1:
logger.info('No player connected')
else:
@@ -327,7 +353,8 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
errors = args["errors"]
if 'InvalidSlot' in errors:
ctx.event_invalid_slot()
elif 'InvalidGame' in errors:
ctx.event_invalid_game()
elif 'SlotAlreadyTaken' in errors:
raise Exception('Player slot already in use for that team')
elif 'IncompatibleVersion' in errors:
@@ -430,3 +457,78 @@ async def console_loop(ctx: CommonContext):
commandprocessor(input_text)
except Exception as e:
logger.exception(e)
def init_logging(name: str):
if gui_enabled:
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO,
filename=os.path.join(log_folder, f"{name}.txt"), filemode="w", force=True)
else:
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO, force=True)
logging.getLogger().addHandler(logging.FileHandler(os.path.join(log_folder, f"{name}.txt"), "w"))
if __name__ == '__main__':
# Text Mode to use !hint and such with games that have no text entry
init_logging("TextClient")
class TextContext(CommonContext):
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(TextContext, self).server_auth(password_requested)
if not self.auth:
logger.info('Enter slot name:')
self.auth = await self.console_input()
await self.send_msgs([{"cmd": 'Connect',
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
'tags': ['AP', 'IgnoreGame'],
'uuid': Utils.get_unique_identifier(), 'game': self.game
}])
async def main(args):
ctx = TextContext(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
if gui_enabled:
input_task = None
from kvui import TextManager
ctx.ui = TextManager(ctx)
ui_task = asyncio.create_task(ctx.ui.async_run(), name="UI")
else:
input_task = asyncio.create_task(console_loop(ctx), name="Input")
ui_task = None
await ctx.exit_event.wait()
ctx.server_address = None
if ctx.server and not ctx.server.socket.closed:
await ctx.server.socket.close()
if ctx.server_task:
await ctx.server_task
while ctx.input_requests > 0:
ctx.input_queue.put_nowait(None)
ctx.input_requests -= 1
if ui_task:
await ui_task
if input_task:
input_task.cancel()
import argparse
import colorama
parser = argparse.ArgumentParser(description="Gameless Archipelago Client, for text interfaction.")
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
parser.add_argument('--password', default=None, help='Password of the multiworld host.')
if not Utils.is_frozen(): # Frozen state has no cmd window in the first place
parser.add_argument('--nogui', default=False, action='store_true', help="Turns off Client GUI.")
args, rest = parser.parse_known_args()
colorama.init()
loop = asyncio.get_event_loop()
loop.run_until_complete(main(args))
loop.close()
colorama.deinit()

View File

@@ -10,7 +10,8 @@ import factorio_rcon
import colorama
import asyncio
from queue import Queue
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger, gui_enabled
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger, gui_enabled, \
init_logging
from MultiServer import mark_raw
import Utils
@@ -19,17 +20,7 @@ from NetUtils import NetworkItem, ClientStatus, JSONtoTextParser, JSONMessagePar
from worlds.factorio import Factorio
log_folder = Utils.local_path("logs")
os.makedirs(log_folder, exist_ok=True)
if gui_enabled:
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO,
filename=os.path.join(log_folder, "FactorioClient.txt"), filemode="w", force=True)
else:
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO, force=True)
logging.getLogger().addHandler(logging.FileHandler(os.path.join(log_folder, "FactorioClient.txt"), "w"))
init_logging("FactorioClient")
class FactorioCommandProcessor(ClientCommandProcessor):
@@ -66,7 +57,7 @@ class FactorioContext(CommonContext):
self.awaiting_bridge = False
self.factorio_json_text_parser = FactorioJSONtoTextParser(self)
async def server_auth(self, password_requested):
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(FactorioContext, self).server_auth(password_requested)
@@ -77,11 +68,15 @@ class FactorioContext(CommonContext):
raise Exception("Cannot connect to a server with unknown own identity, "
"bridge to Factorio first.")
await self.send_msgs([{"cmd": 'Connect',
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
'tags': ['AP'],
'uuid': Utils.get_unique_identifier(), 'game': "Factorio"
}])
await self.send_msgs([{
"cmd": 'Connect',
'password': self.password,
'name': self.auth,
'version': Utils.version_tuple,
'tags': ['AP'],
'uuid': Utils.get_unique_identifier(),
'game': "Factorio"
}])
def on_print(self, args: dict):
logger.info(args["text"])
@@ -99,14 +94,8 @@ class FactorioContext(CommonContext):
return f"AP_{self.seed_name}_{self.auth}.zip"
def print_to_game(self, text):
# TODO: remove around version 0.2
if self.mod_version < Utils.Version(0, 1, 6):
text = text.replace('"', '')
self.rcon_client.send_command(f"/sc game.print(\"[font=default-large-bold]Archipelago:[/font] "
f"{text}\")")
else:
self.rcon_client.send_command(f"/ap-print [font=default-large-bold]Archipelago:[/font] "
f"{text}")
self.rcon_client.send_command(f"/ap-print [font=default-large-bold]Archipelago:[/font] "
f"{text}")
def on_package(self, cmd: str, args: dict):
if cmd == "Connected":
@@ -194,10 +183,6 @@ async def factorio_server_watcher(ctx: FactorioContext):
factorio_server_logger.info(msg)
if not ctx.rcon_client and "Starting RCON interface at IP ADDR:" in msg:
ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
# TODO: remove around version 0.2
if ctx.mod_version < Utils.Version(0, 1, 6):
ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')")
ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')")
if not ctx.server:
logger.info("Established bridge to Factorio Server. "
"Ready to connect to Archipelago via /connect")
@@ -239,7 +224,7 @@ def get_info(ctx, rcon_client):
ctx.seed_name = info["seed_name"]
async def factorio_spinup_server(ctx: FactorioContext):
async def factorio_spinup_server(ctx: FactorioContext) -> bool:
savegame_name = os.path.abspath("Archipelago.zip")
if not os.path.exists(savegame_name):
logger.info(f"Creating savegame {savegame_name}")
@@ -267,23 +252,24 @@ async def factorio_spinup_server(ctx: FactorioContext):
ctx.mod_version = Utils.Version(*(int(number) for number in parts[-2].split(".")))
if not rcon_client and "Starting RCON interface at IP ADDR:" in msg:
rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
if ctx.mod_version == ctx.__class__.mod_version:
raise Exception("No Archipelago mod was loaded. Aborting.")
get_info(ctx, rcon_client)
await asyncio.sleep(0.01)
if ctx.mod_version == ctx.__class__.mod_version:
raise Exception("No Archipelago mod was loaded. Aborting.")
except Exception as e:
logging.exception(e)
logging.error("Aborted Factorio Server Bridge")
logger.exception(e)
logger.error("Aborted Factorio Server Bridge")
ctx.exit_event.set()
else:
logger.info(f"Got World Information from AP Mod {tuple(ctx.mod_version)} for seed {ctx.seed_name} in slot {ctx.auth}")
logger.info(
f"Got World Information from AP Mod {tuple(ctx.mod_version)} for seed {ctx.seed_name} in slot {ctx.auth}")
return True
finally:
factorio_process.terminate()
factorio_process.wait(5)
return False
async def main(args):
@@ -298,16 +284,17 @@ async def main(args):
input_task = asyncio.create_task(console_loop(ctx), name="Input")
ui_task = None
factorio_server_task = asyncio.create_task(factorio_spinup_server(ctx), name="FactorioSpinupServer")
await factorio_server_task
factorio_server_task = asyncio.create_task(factorio_server_watcher(ctx), name="FactorioServer")
progression_watcher = asyncio.create_task(
game_watcher(ctx), name="FactorioProgressionWatcher")
succesful_launch = await factorio_server_task
if succesful_launch:
factorio_server_task = asyncio.create_task(factorio_server_watcher(ctx), name="FactorioServer")
progression_watcher = asyncio.create_task(
game_watcher(ctx), name="FactorioProgressionWatcher")
await ctx.exit_event.wait()
ctx.server_address = None
await ctx.exit_event.wait()
ctx.server_address = None
await progression_watcher
await factorio_server_task
await progression_watcher
await factorio_server_task
if ctx.server and not ctx.server.socket.closed:
await ctx.server.socket.close()
@@ -355,7 +342,8 @@ if __name__ == '__main__':
args, rest = parser.parse_known_args()
colorama.init()
rcon_port = args.rcon_port
rcon_password = args.rcon_password if args.rcon_password else ''.join(random.choice(string.ascii_letters) for x in range(32))
rcon_password = args.rcon_password if args.rcon_password else ''.join(
random.choice(string.ascii_letters) for x in range(32))
factorio_server_logger = logging.getLogger("FactorioServer")
options = Utils.get_options()

View File

@@ -36,7 +36,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations,
has_beaten_game = world.has_beaten_game(maximum_exploration_state)
for item_to_place in items_to_place:
if world.accessibility[item_to_place.player] == 'none':
if world.accessibility[item_to_place.player] == 'minimal':
perform_access_check = not world.has_beaten_game(maximum_exploration_state,
item_to_place.player) if single_player_placement else not has_beaten_game
else:
@@ -52,7 +52,7 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations,
else:
# we filled all reachable spots. Maybe the game can be beaten anyway?
unplaced_items.append(item_to_place)
if world.accessibility[item_to_place.player] != 'none' and world.can_beat_game():
if world.accessibility[item_to_place.player] != 'minimal' and world.can_beat_game():
logging.warning(
f'Not all items placed. Game beatable anyway. (Could not place {item_to_place})')
continue
@@ -87,9 +87,9 @@ def distribute_items_restrictive(world: MultiWorld, fill_locations=None):
progitempool.append(item)
elif item.never_exclude: # this only gets nonprogression items which should not appear in excluded locations
nonexcludeditempool.append(item)
elif item.name in world.local_items[item.player]:
elif item.name in world.local_items[item.player].value:
localrestitempool[item.player].append(item)
elif item.name in world.non_local_items[item.player]:
elif item.name in world.non_local_items[item.player].value:
nonlocalrestitempool.append(item)
else:
restitempool.append(item)

View File

@@ -17,12 +17,10 @@ from worlds.generic import PlandoItem, PlandoConnection
from Utils import parse_yaml, version_tuple, __version__, tuplize_version, get_options
from worlds.alttp.EntranceRandomizer import parse_arguments
from Main import main as ERmain
from Main import get_seed, seeddigits
from BaseClasses import seeddigits, get_seed
import Options
from worlds.alttp.Items import item_table
from worlds.alttp import Bosses
from worlds.alttp.Text import TextTable
from worlds.alttp.Regions import location_table, key_drop_data
from worlds.AutoWorld import AutoWorldRegister
categories = set(AutoWorldRegister.world_types)
@@ -113,17 +111,17 @@ def main(args=None, callback=ERmain):
player_id += 1
args.multi = max(player_id-1, args.multi)
print(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, {seed_name} Seed {seed}")
print(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, {seed_name} Seed {seed} with plando: "
f"{', '.join(args.plando)}")
if not weights_cache:
raise Exception(f"No weights found. Provide a general weights file ({args.weights_file_path}) or individual player files. "
f"A mix is also permitted.")
erargs = parse_arguments(['--multi', str(args.multi)])
erargs.seed = seed
erargs.create_spoiler = args.spoiler > 0
erargs.glitch_triforce = options["generator"]["glitch_triforce_room"]
erargs.spoiler = args.spoiler
erargs.race = args.race
erargs.skip_playthrough = args.spoiler < 2
erargs.outputname = seed_name
erargs.outputpath = args.outputpath
@@ -426,6 +424,32 @@ def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str
raise Exception(f"Boss Shuffle {boss_shuffle} is unknown and boss plando is turned off.")
def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option)):
if option_key in game_weights:
try:
if not option.supports_weighting:
player_option = option.from_any(game_weights[option_key])
else:
player_option = option.from_any(get_choice(option_key, game_weights))
setattr(ret, option_key, player_option)
except Exception as e:
raise Exception(f"Error generating option {option_key} in {ret.game}") from e
else:
# verify item names existing
if getattr(player_option, "verify_item_name", False):
for item_name in player_option.value:
if item_name not in AutoWorldRegister.world_types[ret.game].item_names:
raise Exception(f"Item {item_name} from option {player_option} "
f"is not a valid item name from {ret.game}")
elif getattr(player_option, "verify_location_name", False):
for location_name in player_option.value:
if location_name not in AutoWorldRegister.world_types[ret.game].location_names:
raise Exception(f"Location {location_name} from option {player_option} "
f"is not a valid location name from {ret.game}")
else:
setattr(ret, option_key, option(option.default))
def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("bosses",))):
if "linked_options" in weights:
weights = roll_linked_options(weights)
@@ -452,63 +476,26 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
f"which are not enabled.")
ret = argparse.Namespace()
for option_key in Options.per_game_common_options:
if option_key in weights:
raise Exception(f"Option {option_key} has to be in a game's section, not on its own.")
ret.name = get_choice('name', weights)
ret.accessibility = get_choice('accessibility', weights)
ret.progression_balancing = get_choice('progression_balancing', weights, True)
for option_key, option in Options.common_options.items():
setattr(ret, option_key, option.from_any(get_choice(option_key, weights, option.default)))
ret.game = get_choice("game", weights)
if ret.game not in weights:
raise Exception(f"No game options for selected game \"{ret.game}\" found.")
world_type = AutoWorldRegister.world_types[ret.game]
game_weights = weights[ret.game]
ret.local_items = set()
for item_name in game_weights.get('local_items', []):
items = world_type.item_name_groups.get(item_name, {item_name})
for item in items:
if item in world_type.item_names:
ret.local_items.add(item)
else:
raise Exception(f"Could not force item {item} to be world-local, as it was not recognized.")
ret.non_local_items = set()
for item_name in game_weights.get('non_local_items', []):
items = world_type.item_name_groups.get(item_name, {item_name})
for item in items:
if item in world_type.item_names:
ret.non_local_items.add(item)
else:
raise Exception(f"Could not force item {item} to be world-non-local, as it was not recognized.")
inventoryweights = game_weights.get('start_inventory', {})
startitems = []
for item in inventoryweights.keys():
itemvalue = get_choice_legacy(item, inventoryweights)
if isinstance(itemvalue, int):
for i in range(int(itemvalue)):
startitems.append(item)
elif itemvalue:
startitems.append(item)
ret.startinventory = startitems
ret.start_hints = set(game_weights.get('start_hints', []))
ret.excluded_locations = set()
for location in game_weights.get('exclude_locations', []):
if location in world_type.location_names:
ret.excluded_locations.add(location)
else:
raise Exception(f"Could not exclude location {location}, as it was not recognized.")
if ret.game in AutoWorldRegister.world_types:
for option_name, option in AutoWorldRegister.world_types[ret.game].options.items():
if option_name in game_weights:
try:
if issubclass(option, Options.OptionDict) or issubclass(option, Options.OptionList):
setattr(ret, option_name, option.from_any(game_weights[option_name]))
else:
setattr(ret, option_name, option.from_any(get_choice(option_name, game_weights)))
except Exception as e:
raise Exception(f"Error generating option {option_name} in {ret.game}") from e
else:
setattr(ret, option_name, option(option.default))
for option_key, option in world_type.options.items():
handle_option(ret, game_weights, option_key, option)
for option_key, option in Options.per_game_common_options.items():
handle_option(ret, game_weights, option_key, option)
if "items" in plando_options:
ret.plando_items = roll_item_plando(world_type, game_weights)
if ret.game == "Minecraft":
# bad hardcoded behavior to make this work for now
ret.plando_connections = []
@@ -528,6 +515,44 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
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)}.")
@@ -547,8 +572,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
if ret.dark_room_logic not in {"lamp", "torches", "none"}:
raise ValueError(f"Unknown Dark Room Logic: \"{ret.dark_room_logic}\"")
ret.restrict_dungeon_item_on_boss = get_choice_legacy('restrict_dungeon_item_on_boss', weights, False)
entrance_shuffle = get_choice_legacy('entrance_shuffle', weights, 'vanilla')
if entrance_shuffle.startswith('none-'):
ret.shuffle = 'vanilla'
@@ -588,11 +611,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
ret.shop_shuffle = ''
ret.mode = get_choice_legacy("mode", weights)
ret.retro = get_choice_legacy("retro", weights)
ret.hints = get_choice_legacy('hints', weights)
ret.swordless = get_choice_legacy('swordless', weights, False)
ret.difficulty = get_choice_legacy('item_pool', weights)
@@ -601,12 +619,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
boss_shuffle = get_choice_legacy('boss_shuffle', weights)
ret.shufflebosses = get_plando_bosses(boss_shuffle, plando_options)
ret.enemy_shuffle = bool(get_choice_legacy('enemy_shuffle', weights, False))
ret.killable_thieves = get_choice_legacy('killable_thieves', weights, False)
ret.tile_shuffle = get_choice_legacy('tile_shuffle', weights, False)
ret.bush_shuffle = get_choice_legacy('bush_shuffle', weights, False)
ret.enemy_damage = {None: 'default',
'default': 'default',
'shuffled': 'shuffled',
@@ -616,8 +628,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
ret.enemy_health = get_choice_legacy('enemy_health', weights)
ret.shufflepots = get_choice_legacy('pot_shuffle', weights)
ret.beemizer = int(get_choice_legacy('beemizer', weights, 0))
ret.timer = {'none': False,
@@ -647,42 +657,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
if not ret.required_medallions[index]:
raise Exception(f"unknown Medallion {medallion} for {'misery mire' if index == 0 else 'turtle rock'}")
ret.plando_items = []
if "items" in plando_options:
def add_plando_item(item: str, location: str):
if item not in item_table:
raise Exception(f"Could not plando item {item} as the item was not recognized")
if location not in location_table and location not in key_drop_data:
raise Exception(
f"Could not plando item {item} at location {location} as the location was not recognized")
ret.plando_items.append(PlandoItem(item, location, location_world, from_pool, force))
options = weights.get("plando_items", [])
for placement in options:
if roll_percentage(get_choice_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)
ret.plando_texts = {}
if "texts" in plando_options:
tt = TextTable()

View File

@@ -26,24 +26,14 @@ from NetUtils import *
from worlds.alttp import Regions, Shops
from worlds.alttp import Items
import Utils
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, gui_enabled
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, gui_enabled, init_logging
init_logging("LttPClient")
snes_logger = logging.getLogger("SNES")
from MultiServer import mark_raw
log_folder = Utils.local_path("logs")
os.makedirs(log_folder, exist_ok=True)
# Log to file in gui case
if gui_enabled:
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO,
filename=os.path.join(log_folder, "LttPClient.txt"), filemode="w", force=True)
else:
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO, force=True)
logging.getLogger().addHandler(logging.FileHandler(os.path.join(log_folder, "LttPClient.txt"), "w"))
class LttPCommandProcessor(ClientCommandProcessor):
def _cmd_slow_mode(self, toggle: str = ""):
"""Toggle slow mode, which limits how fast you send / receive items."""

146
Main.py
View File

@@ -1,14 +1,13 @@
from itertools import zip_longest
from itertools import zip_longest, chain
import logging
import os
import random
import time
import zlib
import concurrent.futures
import pickle
import tempfile
import zipfile
from typing import Dict, Tuple
from typing import Dict, Tuple, Optional
from BaseClasses import MultiWorld, CollectionState, Region, RegionType
from worlds.alttp.Items import item_name_groups
@@ -19,17 +18,17 @@ from Utils import output_path, get_options, __version__, version_tuple
from worlds.generic.Rules import locality_rules, exclusion_rules
from worlds import AutoWorld
seeddigits = 20
ordered_areas = (
'Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace',
'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace',
'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total"
)
def get_seed(seed=None):
if seed is None:
random.seed(None)
return random.randint(0, pow(10, seeddigits) - 1)
return seed
def main(args, seed=None):
def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = None):
if not baked_server_options:
baked_server_options = get_options()["server_options"]
if args.outputpath:
os.makedirs(args.outputpath, exist_ok=True)
output_path.cached_path = args.outputpath
@@ -39,73 +38,51 @@ def main(args, seed=None):
# initialize the world
world = MultiWorld(args.multi)
logger = logging.getLogger('')
world.seed = get_seed(seed)
if args.race:
world.secure()
else:
world.random.seed(world.seed)
world.seed_name = str(args.outputname if args.outputname else world.seed)
logger = logging.getLogger()
world.set_seed(seed, args.race, str(args.outputname if args.outputname else world.seed))
world.shuffle = args.shuffle.copy()
world.logic = args.logic.copy()
world.mode = args.mode.copy()
world.swordless = args.swordless.copy()
world.difficulty = args.difficulty.copy()
world.item_functionality = args.item_functionality.copy()
world.timer = args.timer.copy()
world.goal = args.goal.copy()
world.local_items = args.local_items.copy()
if hasattr(args, "algorithm"): # current GUI options
world.algorithm = args.algorithm
world.shuffleganon = args.shuffleganon
world.custom = args.custom
world.customitemarray = args.customitemarray
world.accessibility = args.accessibility.copy()
world.retro = args.retro.copy()
world.hints = args.hints.copy()
world.open_pyramid = args.open_pyramid.copy()
world.boss_shuffle = args.shufflebosses.copy()
world.enemy_shuffle = args.enemy_shuffle.copy()
world.enemy_health = args.enemy_health.copy()
world.enemy_damage = args.enemy_damage.copy()
world.killable_thieves = args.killable_thieves.copy()
world.bush_shuffle = args.bush_shuffle.copy()
world.tile_shuffle = args.tile_shuffle.copy()
world.beemizer = args.beemizer.copy()
world.timer = args.timer.copy()
world.countdown_start_time = args.countdown_start_time.copy()
world.red_clock_time = args.red_clock_time.copy()
world.blue_clock_time = args.blue_clock_time.copy()
world.green_clock_time = args.green_clock_time.copy()
world.shufflepots = args.shufflepots.copy()
world.dungeon_counters = args.dungeon_counters.copy()
world.triforce_pieces_available = args.triforce_pieces_available.copy()
world.triforce_pieces_required = args.triforce_pieces_required.copy()
world.shop_shuffle = args.shop_shuffle.copy()
world.progression_balancing = args.progression_balancing.copy()
world.shuffle_prizes = args.shuffle_prizes.copy()
world.sprite_pool = args.sprite_pool.copy()
world.dark_room_logic = args.dark_room_logic.copy()
world.plando_items = args.plando_items.copy()
world.plando_texts = args.plando_texts.copy()
world.plando_connections = args.plando_connections.copy()
world.er_seeds = getattr(args, "er_seeds", {})
world.restrict_dungeon_item_on_boss = args.restrict_dungeon_item_on_boss.copy()
world.required_medallions = args.required_medallions.copy()
world.game = args.game.copy()
world.set_options(args)
world.player_name = args.name.copy()
world.alttp_rom = args.rom
world.enemizer = args.enemizercli
world.sprite = args.sprite.copy()
world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
world.slot_seeds = {player: random.Random(world.random.getrandbits(64)) for player in
range(1, world.players + 1)}
logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed)
logger.info("Found World Types:")
@@ -125,21 +102,22 @@ def main(args, seed=None):
logger.info('')
for player in world.player_ids:
for item_name in args.startinventory[player]:
world.push_precollected(world.create_item(item_name, player))
for item_name, count in world.start_inventory[player].value.items():
for _ in range(count):
world.push_precollected(world.create_item(item_name, player))
for player in world.player_ids:
if player in world.get_game_players("A Link to the Past"):
# enforce pre-defined local items.
if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]:
world.local_items[player].add('Triforce Piece')
world.local_items[player].value.add('Triforce Piece')
# Not possible to place pendants/crystals out side of boss prizes yet.
world.non_local_items[player] -= item_name_groups['Pendants']
world.non_local_items[player] -= item_name_groups['Crystals']
world.non_local_items[player].value -= item_name_groups['Pendants']
world.non_local_items[player].value -= item_name_groups['Crystals']
# items can't be both local and non-local, prefer local
world.non_local_items[player] -= world.local_items[player]
world.non_local_items[player].value -= world.local_items[player].value
logger.info('Creating World.')
AutoWorld.call_all(world, "create_regions")
@@ -151,11 +129,14 @@ def main(args, seed=None):
if world.players > 1:
for player in world.player_ids:
locality_rules(world, player)
else:
world.non_local_items[1].value = set()
world.local_items[1].value = set()
AutoWorld.call_all(world, "set_rules")
for player in world.player_ids:
exclusion_rules(world, player, args.excluded_locations[player])
exclusion_rules(world, player, world.exclude_locations[player].value)
AutoWorld.call_all(world, "generate_basic")
@@ -187,16 +168,15 @@ def main(args, seed=None):
output = tempfile.TemporaryDirectory()
with output as temp_dir:
with concurrent.futures.ThreadPoolExecutor() as pool:
with concurrent.futures.ThreadPoolExecutor(world.players + 2) as pool:
check_accessibility_task = pool.submit(world.fulfills_accessibility)
output_file_futures = []
output_file_futures = [pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir)]
for player in world.player_ids:
# skip starting a thread for methods that say "pass".
if AutoWorld.World.generate_output.__code__ is not world.worlds[player].generate_output.__code__:
output_file_futures.append(pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
output_file_futures.append(pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir))
output_file_futures.append(
pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
def get_entrance_to_region(region: Region):
for entrance in region.entrances:
@@ -217,9 +197,7 @@ def main(args, seed=None):
if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
er_hint_data[region.player][location.address] = main_entrance.name
ordered_areas = ('Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace',
'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace',
'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total")
checks_in_area = {player: {area: list() for area in ordered_areas}
for player in range(1, world.players + 1)}
@@ -246,10 +224,11 @@ def main(args, seed=None):
oldmancaves = []
takeanyregions = ["Old Man Sword Cave", "Take-Any #1", "Take-Any #2", "Take-Any #3", "Take-Any #4"]
for index, take_any in enumerate(takeanyregions):
for region in [world.get_region(take_any, player) for player in range(1, world.players + 1) if
world.retro[player]]:
item = world.create_item(region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'],
region.player)
for region in [world.get_region(take_any, player) for player in
world.get_game_players("A Link to the Past") if world.retro[player]]:
item = world.create_item(
region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'],
region.player)
player = region.player
location_id = SHOP_ID_START + total_shop_slots + index
@@ -274,18 +253,22 @@ def main(args, seed=None):
for slot in world.player_ids:
client_versions[slot] = world.worlds[slot].get_required_client_version()
games[slot] = world.game[slot]
precollected_items = {player: [] for player in range(1, world.players + 1)}
for item in world.precollected_items:
precollected_items[item.player].append(item.code)
precollected_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 player in world.get_game_players("Factorio"):
if world.tech_tree_information[player].value == 2:
sending_visible_players.add(player)
for slot in world.player_ids:
slot_data[slot] = world.worlds[slot].fill_slot_data()
if world.worlds[slot].sending_visible:
sending_visible_players.add(slot)
def precollect_hint(location):
hint = NetUtils.Hint(location.item.player, location.player, location.address,
location.item.code, False)
precollected_hints[location.player].add(hint)
precollected_hints[location.item.player].add(hint)
locations_data: Dict[int, Dict[int, Tuple[int, int]]] = {player: {} for player in world.player_ids}
for location in world.get_filled_locations():
@@ -293,17 +276,12 @@ def main(args, seed=None):
# item code None should be event, location.address should then also be None
assert location.item.code is not None
locations_data[location.player][location.address] = location.item.code, location.item.player
if location.player in sending_visible_players and location.item.player != location.player:
hint = NetUtils.Hint(location.item.player, location.player, location.address,
location.item.code, False)
precollected_hints[location.player].add(hint)
precollected_hints[location.item.player].add(hint)
elif location.item.name in args.start_hints[location.item.player]:
hint = NetUtils.Hint(location.item.player, location.player, location.address,
location.item.code, False,
er_hint_data.get(location.player, {}).get(location.address, ""))
precollected_hints[location.player].add(hint)
precollected_hints[location.item.player].add(hint)
if location.player in sending_visible_players:
precollect_hint(location)
elif location.name in world.start_location_hints[location.player]:
precollect_hint(location)
elif location.item.name in world.start_hints[location.item.player]:
precollect_hint(location)
multidata = {
"slot_data": slot_data,
@@ -312,9 +290,11 @@ def main(args, seed=None):
"connect_names": {name: (0, player) for player, name in world.player_name.items()},
"remote_items": {player for player in world.player_ids if
world.worlds[player].remote_items},
"remote_start_inventory": {player for player in world.player_ids if
world.worlds[player].remote_start_inventory},
"locations": locations_data,
"checks_in_area": checks_in_area,
"server_options": get_options()["server_options"],
"server_options": baked_server_options,
"er_hint_data": er_hint_data,
"precollected_items": precollected_items,
"precollected_hints": precollected_hints,
@@ -341,16 +321,16 @@ def main(args, seed=None):
# retrieve exceptions via .result() if they occured.
if multidata_task:
multidata_task.result()
for i, future in enumerate(concurrent.futures.as_completed(output_file_futures)):
if i % 10 == 0:
for i, future in enumerate(concurrent.futures.as_completed(output_file_futures), start=1):
if i % 10 == 0 or i == len(output_file_futures):
logger.info(f'Generating output files ({i}/{len(output_file_futures)}).')
future.result()
if not args.skip_playthrough:
if args.spoiler > 1:
logger.info('Calculating playthrough.')
create_playthrough(world)
if args.create_spoiler:
if args.spoiler:
world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase))
zipfilename = output_path(f"AP_{world.seed_name}.zip")
@@ -393,7 +373,7 @@ def create_playthrough(world):
logging.debug('The following items could not be reached: %s', ['%s (Player %d) at %s (Player %d)' % (
location.item.name, location.item.player, location.name, location.player) for location in
sphere_candidates])
if any([world.accessibility[location.item.player] != 'none' for location in sphere_candidates]):
if any([world.accessibility[location.item.player] != 'minimal' for location in sphere_candidates]):
raise RuntimeError(f'Not all progression items reachable ({sphere_candidates}). '
f'Something went terribly wrong here.')
else:
@@ -423,9 +403,9 @@ def create_playthrough(world):
# second phase, sphere 0
removed_precollected = []
for item in (i for i in world.precollected_items if i.advancement):
for item in (i for i in chain.from_iterable(world.precollected_items.values()) if i.advancement):
logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
world.precollected_items.remove(item)
world.precollected_items[item.player].remove(item)
world.state.remove(item)
if not world.can_beat_game():
world.push_precollected(item)
@@ -489,7 +469,9 @@ def create_playthrough(world):
get_path(state, world.get_region('Inverted Big Bomb Shop', player))
# we can finally output our playthrough
world.spoiler.playthrough = {"0": sorted([str(item) for item in world.precollected_items if item.advancement])}
world.spoiler.playthrough = {"0": sorted([str(item) for item in
chain.from_iterable(world.precollected_items.values())
if item.advancement])}
for i, sphere in enumerate(collection_spheres):
world.spoiler.playthrough[str(i + 1)] = {str(location): str(location.item) for location in sorted(sphere)}

View File

@@ -26,12 +26,13 @@ from prompt_toolkit.patch_stdout import patch_stdout
from fuzzywuzzy import process as fuzzy_process
from worlds.AutoWorld import AutoWorldRegister
proxy_worlds = {name: world(None, 0) for name, world in AutoWorldRegister.world_types.items()}
from worlds import network_data_package, lookup_any_item_id_to_name, lookup_any_location_id_to_name
import Utils
from Utils import get_item_name_from_id, get_location_name_from_id, \
version_tuple, restricted_loads, Version
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission
colorama.init()
@@ -84,6 +85,7 @@ class Context:
self.connect_names = {} # names of slots clients can connect to
self.allow_forfeits = {}
self.remote_items = set()
self.remote_start_inventory = set()
self.locations: typing.Dict[int, typing.Dict[int, typing.Tuple[int, int]]] = {}
self.host = host
self.port = port
@@ -150,17 +152,33 @@ class Context:
logging.info(f"Outgoing message: {msg}")
return True
async def broadcast_send_encoded_msgs(self, endpoints: typing.Iterable[Endpoint], msg: str) -> bool:
sockets = []
for endpoint in endpoints:
if endpoint.socket and endpoint.socket.open:
sockets.append(endpoint.socket)
try:
websockets.broadcast(sockets, msg)
except RuntimeError:
logging.exception("Exception during broadcast_send_encoded_msgs")
else:
if self.log_network:
logging.info(f"Outgoing broadcast: {msg}")
return True
def broadcast_all(self, msgs):
msgs = self.dumper(msgs)
for endpoint in self.endpoints:
if endpoint.auth:
asyncio.create_task(self.send_encoded_msgs(endpoint, msgs))
endpoints = (endpoint for endpoint in self.endpoints if endpoint.auth)
asyncio.create_task(self.broadcast_send_encoded_msgs(endpoints, msgs))
def broadcast_team(self, team, msgs):
def broadcast_team(self, team: int, msgs):
msgs = self.dumper(msgs)
for client in self.endpoints:
if client.auth and client.team == team:
asyncio.create_task(self.send_encoded_msgs(client, msgs))
endpoints = (endpoint for endpoint in self.endpoints if endpoint.auth and endpoint.team == team)
asyncio.create_task(self.broadcast_send_encoded_msgs(endpoints, msgs))
def broadcast(self, endpoints: typing.Iterable[Endpoint], msgs):
msgs = self.dumper(msgs)
asyncio.create_task(self.broadcast_send_encoded_msgs(endpoints, msgs))
async def disconnect(self, endpoint):
if endpoint in self.endpoints:
@@ -229,6 +247,7 @@ class Context:
self.random.seed(self.seed_name)
self.connect_names = decoded_obj['connect_names']
self.remote_items = decoded_obj['remote_items']
self.remote_start_inventory = decoded_obj.get('remote_start_inventory', decoded_obj['remote_items'])
self.locations = decoded_obj['locations']
self.slot_data = decoded_obj['slot_data']
self.er_hint_data = {int(player): {int(address): name for address, name in loc_data.items()}
@@ -237,7 +256,7 @@ class Context:
# award remote-items start inventory:
for team in range(len(decoded_obj['names'])):
for slot, item_codes in decoded_obj["precollected_items"].items():
if slot in self.remote_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)
@@ -275,7 +294,7 @@ class Context:
if not self.save_filename:
import os
name, ext = os.path.splitext(self.data_filename)
self.save_filename = name + '.apsave' if ext.lower() in ('.archipelago','.zip') \
self.save_filename = name + '.apsave' if ext.lower() in ('.archipelago', '.zip') \
else self.data_filename + '_' + 'apsave'
try:
with open(self.save_filename, 'rb') as f:
@@ -442,7 +461,7 @@ async def server(websocket, path, ctx: Context):
async def on_client_connected(ctx: Context, client: Client):
await ctx.send_msgs(client, [{
'cmd': 'RoomInfo',
'password': ctx.password is not None,
'password': bool(ctx.password),
'players': [
NetworkPlayer(client.team, client.slot, ctx.name_aliases.get((client.team, client.slot), client.name),
client.name) for client
@@ -451,8 +470,10 @@ async def on_client_connected(ctx: Context, client: Client):
# Name them by feature or fork, as you feel is appropriate.
'tags': ctx.tags,
'version': Utils.version_tuple,
# TODO ~0.2.0 remove forfeit_mode and remaining_mode in favor of permissions
'forfeit_mode': ctx.forfeit_mode,
'remaining_mode': ctx.remaining_mode,
'permissions': get_permissions(ctx),
'hint_cost': ctx.hint_cost,
'location_check_points': ctx.location_check_points,
'datapackage_version': network_data_package["version"],
@@ -462,6 +483,13 @@ async def on_client_connected(ctx: Context, client: Client):
}])
def get_permissions(ctx) -> typing.Dict[str, Permission]:
return {
"forfeit": Permission.from_text(ctx.forfeit_mode),
"remaining": Permission.from_text(ctx.remaining_mode),
}
async def on_client_disconnected(ctx: Context, client: Client):
if client.auth:
await on_client_left(ctx, client)
@@ -474,6 +502,10 @@ async def on_client_joined(ctx: Context, client: Client):
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) "
f"playing {ctx.games[client.slot]} has joined. "
f"Client({version_str}), {client.tags}).")
# TODO: remove with 0.2
if client.version < Version(0, 1, 7):
ctx.notify_client(client,
"Warning: Your client's datapackage handling may be unsupported soon. (Version < 0.1.7)")
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
@@ -945,14 +977,9 @@ class ClientMessageProcessor(CommonCommandProcessor):
self.output("Cheating is disabled.")
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.
If hint costs are on, this will only give you one new result,
you can rerun the command to get more in that case."""
def get_hints(self, input_text: str, explicit_location: bool = False) -> bool:
points_available = get_client_points(self.ctx, self.client)
if not item_or_location:
if not input_text:
hints = {hint.re_check(self.ctx, self.client.team) for hint in
self.ctx.hints[self.client.team, self.client.slot]}
self.ctx.hints[self.client.team, self.client.slot] = hints
@@ -962,16 +989,16 @@ class ClientMessageProcessor(CommonCommandProcessor):
return True
else:
world = proxy_worlds[self.ctx.games[self.client.slot]]
item_name, usable, response = get_intended_text(item_or_location, world.all_names)
item_name, usable, response = get_intended_text(input_text, world.all_names if not explicit_location else world.location_names)
if usable:
if item_name in world.hint_blacklist:
self.output(f"Sorry, \"{item_name}\" is marked as non-hintable.")
hints = []
elif item_name in world.item_name_groups:
elif item_name in world.item_name_groups and not explicit_location:
hints = []
for item in world.item_name_groups[item_name]:
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item))
elif item_name in world.item_names: # item name
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)
else: # location name
hints = collect_hints_location(self.ctx, self.client.team, self.client.slot, item_name)
@@ -1004,19 +1031,25 @@ class ClientMessageProcessor(CommonCommandProcessor):
hints.append(hint)
can_pay -= 1
self.ctx.hints_used[self.client.team, self.client.slot] += 1
points_available = get_client_points(self.ctx, self.client)
if not hint.found:
self.ctx.hints[self.client.team, hint.finding_player].add(hint)
self.ctx.hints[self.client.team, hint.receiving_player].add(hint)
if not_found_hints:
if hints:
if hints and cost and int((points_available // cost) == 0):
self.output(
f"There may be more hintables, however, you cannot afford to pay for any more. "
f" You have {points_available} and need at least "
f"{self.ctx.get_hint_cost(self.client.slot)}.")
elif hints:
self.output(
"There may be more hintables, you can rerun the command to find more.")
else:
self.output(f"You can't afford the hint. "
f"You have {points_available} points and need at least "
f"{self.ctx.get_hint_cost(self.client.slot)}")
f"{self.ctx.get_hint_cost(self.client.slot)}.")
notify_hints(self.ctx, self.client.team, hints)
self.ctx.save()
return True
@@ -1028,6 +1061,22 @@ class ClientMessageProcessor(CommonCommandProcessor):
self.output(response)
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.
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)
@mark_raw
def _cmd_hint_location(self, location: str = "") -> bool:
"""Use !hint_location {location_name},
for example !hint 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.)"""
return self.get_hints(location, True)
def get_checked_checks(ctx: Context, client: Client) -> typing.List[int]:
return [location_id for
@@ -1076,8 +1125,8 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
else:
team, slot = ctx.connect_names[args['name']]
game = ctx.games[slot]
if args['game'] != game:
errors.add('InvalidSlot')
if "IgnoreGame" not in args["tags"] and args['game'] != game:
errors.add('InvalidGame')
# this can only ever be 0 or 1 elements
clients = [c for c in ctx.endpoints if c.auth and c.slot == slot and c.team == team]
if clients:
@@ -1154,7 +1203,8 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
locs = []
for location in args["locations"]:
if type(location) is not int or location not in lookup_any_location_id_to_name:
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts'}])
await ctx.send_msgs(client,
[{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts'}])
return
target_item, target_player = ctx.locations[client.slot][location]
locs.append(NetworkItem(target_item, location, target_player))
@@ -1380,6 +1430,8 @@ class ServerCommandProcessor(CommonCommandProcessor):
return input_text
setattr(self.ctx, option_name, attrtype(option))
self.output(f"Set option {option_name} to {getattr(self.ctx, option_name)}")
if option_name in {"forfeit_mode", "remaining_mode"}:
self.ctx.broadcast_all([{"cmd": "RoomUpdate", 'permissions': get_permissions(self.ctx)}])
return True
else:
known = (f"{option}:{otype}" for option, otype in self.ctx.simple_options.items())

View File

@@ -25,6 +25,25 @@ class ClientStatus(enum.IntEnum):
CLIENT_GOAL = 30
class Permission(enum.IntEnum):
disabled = 0b000 # 0, completely disables access
enabled = 0b001 # 1, allows manual use
goal = 0b010 # 2, allows manual use after goal completion
auto = 0b110 # 6, forces use after goal completion, only works for forfeit
auto_enabled = 0b111 # 7, forces use after goal completion, allows manual use any time
@staticmethod
def from_text(text: str):
data = 0
if "auto" in text:
data |= 0b110
elif "goal" in text:
data |= 0b010
if "enabled" in text:
data |= 0b001
return Permission(data)
class NetworkPlayer(typing.NamedTuple):
team: int
slot: int
@@ -150,8 +169,8 @@ class JSONtoTextParser(metaclass=HandlerMeta):
return "".join(self.handle_node(section) for section in input_object)
def handle_node(self, node: JSONMessagePart):
type = node.get("type", None)
handler = self.handlers.get(type, self.handlers["text"])
node_type = node.get("type", None)
handler = self.handlers.get(node_type, self.handlers["text"])
return handler(node)
def _handle_color(self, node: JSONMessagePart):

View File

@@ -29,7 +29,9 @@ class AssembleOptions(type):
def validate(self, *args, **kwargs):
func(self, *args, **kwargs)
self.value = self.schema.validate(self.value)
return validate
attrs["__init__"] = validate_decorator(attrs["__init__"])
return super(AssembleOptions, mcs).__new__(mcs, name, bases, attrs)
@@ -43,6 +45,9 @@ class Option(metaclass=AssembleOptions):
# Handled in get_option_name()
autodisplayname = False
# can be weighted between selections
supports_weighting = True
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.get_current_option_name()})"
@@ -81,6 +86,7 @@ class Toggle(Option):
default = 0
def __init__(self, value: int):
assert value == 0 or value == 1
self.value = value
@classmethod
@@ -119,6 +125,7 @@ class Toggle(Option):
def get_option_name(cls, value):
return ["No", "Yes"][int(value)]
class DefaultOnToggle(Toggle):
default = 1
@@ -150,19 +157,23 @@ class Choice(Option):
return cls.from_text(str(data))
def __eq__(self, other):
if isinstance(other, str):
if isinstance(other, self.__class__):
return other.value == self.value
elif isinstance(other, str):
assert other in self.options
return other == self.current_key
elif isinstance(other, int):
assert other in self.name_lookup
return other == self.value
elif isinstance(other, bool):
elif isinstance(other, bool):
return other == bool(self.value)
else:
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
def __ne__(self, other):
if isinstance(other, str):
if isinstance(other, self.__class__):
return other.value != self.value
elif isinstance(other, str):
assert other in self.options
return other != self.current_key
elif isinstance(other, int):
@@ -170,9 +181,12 @@ class Choice(Option):
return other != self.value
elif isinstance(other, bool):
return other != bool(self.value)
elif other is None:
return False
else:
raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}")
class Range(Option, int):
range_start = 0
range_end = 1
@@ -205,7 +219,7 @@ class Range(Option, int):
return cls.from_text(str(data))
def get_option_name(self, value):
return str(self.value)
return str(value)
def __str__(self):
return str(self.value)
@@ -230,9 +244,11 @@ class OptionNameSet(Option):
class OptionDict(Option):
default = {}
supports_weighting = False
value: typing.Dict[str, typing.Any]
def __init__(self, value: typing.Dict[str, typing.Any]):
self.value: typing.Dict[str, typing.Any] = value
self.value = value
@classmethod
def from_any(cls, data: typing.Dict[str, typing.Any]) -> OptionDict:
@@ -242,14 +258,20 @@ class OptionDict(Option):
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
def get_option_name(self, value):
return str(value)
return ", ".join(f"{key}: {v}" for key, v in value.items())
def __contains__(self, item):
return item in self.value
class OptionList(Option):
class OptionList(Option):
default = []
supports_weighting = False
value: list
def __init__(self, value: typing.List[str, typing.Any]):
self.value = value
super(OptionList, self).__init__()
@classmethod
def from_text(cls, text: str):
@@ -262,23 +284,116 @@ class OptionList(Option):
return cls.from_text(str(data))
def get_option_name(self, value):
return str(value)
return ", ".join(value)
def __contains__(self, item):
return item in self.value
class OptionSet(Option):
default = frozenset()
supports_weighting = False
value: set
def __init__(self, value: typing.Union[typing.Set[str, typing.Any], typing.List[str, typing.Any]]):
self.value = set(value)
super(OptionSet, self).__init__()
@classmethod
def from_text(cls, text: str):
return cls([option.strip() for option in text.split(",")])
@classmethod
def from_any(cls, data: typing.Any):
if type(data) == list:
return cls(data)
elif type(data) == set:
return cls(data)
return cls.from_text(str(data))
def get_option_name(self, value):
return ", ".join(value)
def __contains__(self, item):
return item in self.value
local_objective = Toggle # local triforce pieces, local dungeon prizes etc.
class Accessibility(Choice):
"""Set rules for reachability of your items/locations.
Locations: ensure everything can be reached and acquired.
Items: ensure all logically relevant items can be acquired.
Minimal: ensure what is needed to reach your goal can be acquired."""
option_locations = 0
option_items = 1
option_beatable = 2
option_minimal = 2
alias_none = 2
default = 1
class ProgressionBalancing(DefaultOnToggle):
"""A system that moves progression earlier, to try and prevent the player from getting stuck and bored early."""
common_options = {
"progression_balancing": ProgressionBalancing,
"accessibility": Accessibility
}
class ItemSet(OptionSet):
# implemented by Generate
verify_item_name = True
class LocalItems(ItemSet):
"""Forces these items to be in their native world."""
displayname = "Local Items"
class NonLocalItems(ItemSet):
"""Forces these items to be outside their native world."""
displayname = "Not Local Items"
class StartInventory(OptionDict):
"""Start with these items."""
verify_item_name = True
displayname = "Start Inventory"
class StartHints(ItemSet):
"""Start with these item's locations prefilled into the !hint command."""
displayname = "Start Hints"
class StartLocationHints(OptionSet):
displayname = "Start Location Hints"
class ExcludeLocations(OptionSet):
"""Prevent these locations from having an important item"""
displayname = "Excluded Locations"
verify_location_name = True
per_game_common_options = {
"local_items": LocalItems,
"non_local_items": NonLocalItems,
"start_inventory": StartInventory,
"start_hints": StartHints,
"start_location_hints": StartLocationHints,
"exclude_locations": OptionSet
}
if __name__ == "__main__":
from worlds.alttp.Options import Logic
import argparse
map_shuffle = Toggle
compass_shuffle = Toggle
keyshuffle = Toggle

View File

@@ -10,8 +10,9 @@ Currently, the following games are supported:
* Slay the Spire
* Risk of Rain 2
* The Legend of Zelda: Ocarina of Time
* Timespinner
For setup and instructions check out our [tutorials page](http://archipelago.gg:48484/tutorial).
For setup and instructions check out our [tutorials page](/tutorial).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
windows binaries.
@@ -38,7 +39,7 @@ If you are running Archipelago from a non-Windows system then the likely scenari
## Related Repositories
This project makes use of multiple other projects. We wouldn't be here without these other repositories and the contributions of their developers, past and present.
* [z3randomizer](https://github.com/CaitSith2/z3randomizer)
* [z3randomizer](https://github.com/ArchipelagoMW/z3randomizer)
* [Enemizer](https://github.com/Ijwu/Enemizer)
* [Ocarina of Time Randomizer](https://github.com/TestRunnerSRL/OoT-Randomizer)

View File

@@ -13,7 +13,7 @@ class Version(typing.NamedTuple):
build: int
__version__ = "0.1.7"
__version__ = "0.1.9"
version_tuple = tuplize_version(__version__)
import builtins
@@ -24,7 +24,7 @@ import pickle
import functools
import io
import collections
import importlib
from yaml import load, dump, safe_load
try:
@@ -365,16 +365,28 @@ safe_builtins = {
class RestrictedUnpickler(pickle.Unpickler):
def __init__(self, *args, **kwargs):
super(RestrictedUnpickler, self).__init__(*args, **kwargs)
self.options_module = importlib.import_module("Options")
self.net_utils_module = importlib.import_module("NetUtils")
self.generic_properties_module = importlib.import_module("worlds.generic")
def find_class(self, module, name):
if module == "builtins" and name in safe_builtins:
return getattr(builtins, name)
# used by MultiServer -> savegame/multidata
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint"}:
import NetUtils
return getattr(NetUtils, name)
if module == "Options":
import Options
obj = getattr(Options, name)
if issubclass(obj, Options.Option):
return getattr(self.net_utils_module, name)
# Options and Plando are unpickled by WebHost -> Generate
if module == "worlds.generic" and name in {"PlandoItem", "PlandoConnection"}:
return getattr(self.generic_properties_module, name)
if module.endswith("Options"):
if module == "Options":
mod = self.options_module
else:
mod = importlib.import_module(module)
obj = getattr(mod, name)
if issubclass(obj, self.options_module.Option):
return obj
# Forbid everything else.
raise pickle.UnpicklingError("global '%s.%s' is forbidden" %

View File

@@ -36,7 +36,11 @@ if __name__ == "__main__":
multiprocessing.freeze_support()
multiprocessing.set_start_method('spawn')
logging.basicConfig(format='[%(asctime)s] %(message)s', level=logging.INFO)
update_sprites_lttp()
try:
update_sprites_lttp()
except Exception as e:
logging.exception(e)
logging.warning("Could not update LttP sprites.")
app = get_app()
create_options_files()
if app.config["SELFLAUNCH"]:

View File

@@ -82,22 +82,22 @@ def page_not_found(err):
return render_template('404.html'), 404
# Start Playing Page
@app.route('/start-playing')
def start_playing():
return render_template(f"startPlaying.html")
# Player settings pages
@app.route('/games/<string:game>/player-settings')
def player_settings(game):
return render_template(f"player-settings.html", game=game)
# Game sub-pages
@app.route('/games/<string:game>/<string:page>')
def game_pages(game, page):
return render_template(f"/games/{game}/{page}.html")
# Game landing pages
@app.route('/games/<game>')
def game_page(game):
return render_template(f"/games/{game}/{game}.html")
# Game Info Pages
@app.route('/games/<string:game>/info/<string:lang>')
def game_info(game, lang):
return render_template('gameInfo.html', game=game, lang=lang)
# List of supported games
@@ -107,7 +107,7 @@ def games():
for game, world in AutoWorldRegister.world_types.items():
if not world.hidden:
worlds[game] = world.__doc__ if world.__doc__ else "No description provided."
return render_template("games/games.html", worlds=worlds)
return render_template("supportedGames.html", worlds=worlds)
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
@@ -186,6 +186,9 @@ def favicon():
return send_from_directory(os.path.join(app.root_path, 'static/static'),
'favicon.ico', mimetype='image/vnd.microsoft.icon')
@app.route('/discord')
def discord():
return redirect("https://discord.gg/archipelago")
from WebHostLib.customserver import run_server_process
from . import tracker, upload, landing, check, generate, downloads, api # to trigger app routing picking up on it

View File

@@ -89,7 +89,7 @@ def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
options = restricted_loads(generation.options)
logging.info(f"Generating {generation.id} for {len(options)} players")
pool.apply_async(gen_game, (options,),
{"race": meta["race"],
{"meta": meta,
"sid": generation.id,
"owner": generation.owner},
handle_generation_success, handle_generation_failure)

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ import yaml
import json
from worlds.AutoWorld import AutoWorldRegister
import Options
target_folder = os.path.join("WebHostLib", "static", "generated")
@@ -18,19 +19,30 @@ def create():
option.range_end: "maximum value"
}
return data, notes
def default_converter(default_value):
if isinstance(default_value, (set, frozenset)):
return list(default_value)
return default_value
for game_name, world in AutoWorldRegister.world_types.items():
res = Template(open(os.path.join("WebHostLib", "templates", "options.yaml")).read()).render(
options=world.options, __version__=__version__, game=game_name, yaml_dump=yaml.dump,
dictify_range=dictify_range
options={**world.options, **Options.per_game_common_options},
__version__=__version__, game=game_name, yaml_dump=yaml.dump,
dictify_range=dictify_range, default_converter=default_converter,
)
with open(os.path.join(target_folder, game_name + ".yaml"), "w") as f:
if not os.path.isdir(os.path.join(target_folder, 'configs')):
os.mkdir(os.path.join(target_folder, 'configs'))
with open(os.path.join(target_folder, 'configs', game_name + ".yaml"), "w") as f:
f.write(res)
# Generate JSON files for player-settings pages
player_settings = {
"baseOptions": {
"description": "Generated by https://archipelago.gg/",
"game": game_name,
"name": "Player",
},
}
@@ -46,7 +58,7 @@ def create():
"options": []
}
for sub_option_name, sub_option_id in option.options.items():
for sub_option_id, sub_option_name in option.name_lookup.items():
this_option["options"].append({
"name": option.get_option_name(sub_option_id),
"value": sub_option_name,
@@ -69,5 +81,8 @@ def create():
player_settings["gameOptions"] = game_options
with open(os.path.join(target_folder, game_name + ".json"), "w") as f:
if not os.path.isdir(os.path.join(target_folder, 'player-settings')):
os.mkdir(os.path.join(target_folder, 'player-settings'))
with open(os.path.join(target_folder, 'player-settings', game_name + ".json"), "w") as f:
f.write(json.dumps(player_settings, indent=2, separators=(',', ': ')))

View File

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

View File

@@ -1,10 +1,52 @@
# Frequently Asked Questions
## What is a randomizer?
Who's on first.
A randomizer is a modification of a video game which reorganizes the items required to progress through the game.
A normal play-through of a game might require you to use item A to unlock item B, then C, and so forth. In a
randomized game, you might first find item C, then A, then B.
This transforms games from a linear experience into a puzzle, presenting players with a new challenge each time they
play a randomized game. Putting items in non-standard locations can require the player to think about the game world
and the items they encounter in new and interesting ways.
## What happens if an item is placed somewhere it is impossible to get?
The randomizer has many strict sets of rules it must follow when generating a game. One of the functions of these
rules is to ensure items necessary to complete the game will be accessible to the player. Many games also have a
subset of rules allowing certain items to be placed in normally unreachable locations, provided the player has
indicated they are comfortable exploiting certain glitches in the game.
## What is a multi-world?
What's on second.
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. Currently, a maximum of 255 players can participate in a single multi-world.
## What happens if a person has to leave early?
If a player must leave early, they can use Archipelago's forfeit system. When a player forfeits their game, all
the items in that game which belong to other players are sent out automatically, so other players can continue to
play.
## What does multi-game mean?
I don't know's on third.
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.
## 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.
If you have more questions, feel free to ask in the **#archipelago-dev** channel on our Discord.

View File

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

View File

@@ -0,0 +1,26 @@
# 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.
## What does randomization do to this game?
Items which the player would normally acquire throughout the game have been moved around. Logic remains, so the game
is always able to be completed, but because of the item shuffle the player may need to access certain areas before
they would in the vanilla game.
## What items and locations get shuffled?
All main inventory items, collectables, and ammunition can be shuffled, and all locations in the game which could
contain any of those items may have their contents changed.
## Which items can be in another player's world?
Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to
limit certain items to your own world.
## What does another world's item look like in LttP?
Items belonging to other worlds are represented by a Power Star from Super Mario World.
## When the player receives an item, what happens?
When the player receives an item, Link will hold the item above his head and display it to the world. It's good for
business!

View File

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

View File

@@ -0,0 +1,26 @@
# 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.
## What does randomization do to this game?
Items which the player would normally acquire throughout the game have been moved around. Logic remains, so the game
is always able to be completed, but because of the item shuffle the player may need to access certain areas before
they would in the vanilla game.
## What items and locations get shuffled?
All main inventory items, collectables, and ammunition can be shuffled, and all locations in the game which could
contain any of those items may have their contents changed. Gold Skultulla locations may also be included as necessary
checks at the user's discretion.
## Which items can be in another player's world?
Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to
limit certain items to your own world.
## What does another world's item look like in OoT?
Items belonging to other worlds are represented by an Ocarina of Time.
## 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,27 @@
# 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.
## What does randomization do to this game?
The most noticeable change is the complete removal of freestanding technologies. The technology blueprints normally
awarded from scanning those items have been shuffled into location checks throughout the AP item pool.
## What is the goal of Subnautica when randomized?
The goal remains unchanged. Cure the plague, build the Neptune Escape Rocket, and escape into space.
## What items and locations get shuffled?
Most of the technologies the player will need throughout the game will be shuffled. Location checks in Subnautica are
data pads and technology lockers.
## Which items can be in another player's world?
Most technologies may be shuffled into another player's world.
## What does another world's item look like in Subnautica?
Location checks in Subnautica are data pads and technology lockers. Opening one of these will send an item to
another player's world.
## When the player receives a technology, what happens?
When the player receives a technology, the chat log displays a notification the technology has been received.

View File

@@ -0,0 +1,28 @@
# 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.
## 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
## 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
contain any of those items may have their contents changed.
## Which items can be in another player's world?
Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to
limit certain items to your own world.
## What does another world's item look like in Timespinner?
Items belonging to other worlds are represented by the vanilla item [Elemental Beads](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

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

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

View File

@@ -0,0 +1,52 @@
window.addEventListener('load', () => {
// Reload tracker every 15 seconds
const url = window.location;
setInterval(() => {
const ajax = new XMLHttpRequest();
ajax.onreadystatechange = () => {
if (ajax.readyState !== 4) { return; }
// Create a fake DOM using the returned HTML
const domParser = new DOMParser();
const fakeDOM = domParser.parseFromString(ajax.responseText, 'text/html');
// Update item tracker
document.getElementById('inventory-table').innerHTML = fakeDOM.getElementById('inventory-table').innerHTML;
// Update only counters, small keys, and boss keys in the location-table
const types = ['counter', 'smallkeys', 'bosskeys'];
for (let j = 0; j < types.length; j++) {
let counters = document.getElementsByClassName(types[j]);
const fakeCounters = fakeDOM.getElementsByClassName(types[j]);
for (let i = 0; i < counters.length; i++) {
counters[i].innerHTML = fakeCounters[i].innerHTML;
}
}
};
ajax.open('GET', url);
ajax.send();
}, 15000)
// Collapsible advancement sections
const categories = document.getElementsByClassName("location-category");
for (let i = 0; i < categories.length; i++) {
let hide_id = categories[i].id.split('-')[0];
if (hide_id == 'Total') {
continue;
}
categories[i].addEventListener('click', function() {
// Toggle the advancement list
document.getElementById(hide_id).classList.toggle("hide");
// Change text of the header
const tab_header = document.getElementById(hide_id+'-header').children[0];
const orig_text = tab_header.innerHTML;
let new_text;
if (orig_text.includes("▼")) {
new_text = orig_text.replace("▼", "▲");
}
else {
new_text = orig_text.replace("▲", "▼");
}
tab_header.innerHTML = new_text;
});
}
});

View File

@@ -7,6 +7,22 @@ window.addEventListener('load', () => {
document.getElementById('game-name').innerHTML = gameName;
Promise.all([fetchSettingData()]).then((results) => {
let settingHash = localStorage.getItem(`${gameName}-hash`);
if (!settingHash) {
// If no hash data has been set before, set it now
localStorage.setItem(`${gameName}-hash`, md5(results[0]));
localStorage.removeItem(gameName);
settingHash = md5(results[0]);
}
if (settingHash !== md5(results[0])) {
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);
}
// Page setup
createDefaultSettings(results[0]);
buildUI(results[0]);
@@ -14,7 +30,7 @@ window.addEventListener('load', () => {
// Event listeners
document.getElementById('export-settings').addEventListener('click', () => exportSettings());
document.getElementById('generate-race').addEventListener('click', () => generateGame(true))
document.getElementById('generate-race').addEventListener('click', () => generateGame(true));
document.getElementById('generate-game').addEventListener('click', () => generateGame());
// Name input field
@@ -28,6 +44,12 @@ window.addEventListener('load', () => {
})
});
const resetSettings = () => {
localStorage.removeItem(gameName);
localStorage.removeItem(`${gameName}-hash`)
window.location.reload();
};
const fetchSettingData = () => new Promise((resolve, reject) => {
const ajax = new XMLHttpRequest();
ajax.onreadystatechange = () => {
@@ -39,7 +61,7 @@ const fetchSettingData = () => new Promise((resolve, reject) => {
try{ resolve(JSON.parse(ajax.responseText)); }
catch(error){ reject(error); }
};
ajax.open('GET', `${window.location.origin}/static/generated/${gameName}.json`, true);
ajax.open('GET', `${window.location.origin}/static/generated/player-settings/${gameName}.json`, true);
ajax.send();
});

View File

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

View File

@@ -0,0 +1,27 @@
# Archipelago Setup Guide
## 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 check the relevant tutorial.
## Generating a game
### Gather all player YAMLS
All players that wish to play in the generated multiworld must have a YAML file which contains all of 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 all of the YAML files 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.
### Rolling the seed
After gathering all of 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 you figuring out the issue asking in the ***#tech-support*** channel of our Discord. The generator will put a zip folder into your `Archipelago\output` folder with the format `AP_XXXXXXXXX`.zip. This contains all of the patch files and relevant mods for the players as well as the serverdata for the host.
### Changing host settings
Sometimes there are various settings that you may want to change before rolling a seed such as enabling race mode, auto-forfeit, or set 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.
## Hosting a multiworld
### Uploading the seed to the website
The easiest and most recommended method is to upload the zip file that you generated to the website [here](/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 fo rhe 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!

View File

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

View File

@@ -4,13 +4,14 @@
### 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/](https://thunderstore.io/package/ebkr/r2modman/)
### Install Archipelago Mod using r2modman
You can install the Archipelago mod using r2modman in one of two ways.
One, you can use the Thunderstore website and click on the "Install with Mod Manager" link.
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/](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.
@@ -46,15 +47,41 @@ Risk of Rain 2:
start_with_revive: true
item_pickup_step: 1
enable_lunar: true
item_weights:
default: 50
new: 0
uncommon: 0
legendary: 0
lunartic: 0
chaos: 0
no_scraps: 0
even: 0
scraps_only: 0
item_pool_presets: true
# custom item weights
green_scrap: 16
red_scrap: 4
yellow_scrap: 1
white_scrap: 32
common_item: 64
uncommon_item: 32
legendary_item: 8
boss_item: 4
lunar_item: 16
equipment: 32
```
| Name | Description | Allowed values |
| ---- | ----------- | -------------- |
| total_locations | The total number of location checks that will be attributed to the Risk of Rain player. This option is ALSO the total number of items in the item pool for the Risk of Rain player. | 10 - 50 |
| total_locations | The total number of location checks that will be attributed to the Risk of Rain player. This option is ALSO the total number of items in the item pool for the Risk of Rain player. | 10 - 100 |
| total_revivals | The total number of items in the Risk of Rain player's item pool (items other players pick up for them) replaced with `Dio's Best Friend`. | 0 - 5 |
| start_with_revive | Starts the player off with a `Dio's Best Friend`. Functionally equivalent to putting a `Dio's Best Friend` in your `starting_inventory`. | true/false |
| item_pickup_step | The number of item pickups which you are allowed to claim before they become an Archipelago location check. | 0 - 5 |
| enable_lunar | Allows for lunar items to be shuffled into the item pool on behalf of the Risk of Rain player. | true/false |
| item_weights | Each option here is a preset item weight that can be used to customize your generate item pool with certain settings. | default, new, uncommon, legendary, lunartic, chaos, no_scraps, even, scraps_only |
| item_pool_presets | A simple toggle to determine whether the item_weight presets are used or the custom item pool as defined below | true/false |
| custom item weights | Each defined item here is a single item in the pool that will have a weight against the other items when the item pool gets generated. These values can be modified to adjust how frequently certain items appear | 0-100|
Using the example YAML above: the Risk of Rain 2 player will have 15 total items which they can pick up for other players. (total_locations = 15)
@@ -66,4 +93,6 @@ They will have 4 of the items which other players can grant them replaced with `
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 lunar items shuffled into the item pool on their behalf. (enable_lunar = true)
The player will have the default preset generated item pool with the custom item weights being ignored. (item_weights: default and item_pool_presets: true)

View File

@@ -0,0 +1,60 @@
# Timespinner Randomizer Setup Guide
## Required Software
- [Timespinner (steam)](https://store.steampowered.com/app/368620/Timespinner/) or [Timespinner (drm free)](https://www.humblebundle.com/store/timespinner)
- [Timespinner Randomizer](https://github.com/JarnoWesthof/TsRandomizer)
## General Concept
The timespinner Randomizer loads Timespinner.exe from the same folder, and alters its state in memory to allow for randomization of the items
## Installation Procedures
Download latest version of [Timespinner Randomizer](https://github.com/JarnoWesthof/TsRandomizer) 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 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
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
## YAML Settings
An example YAML would look like this:
```yaml
description: Default Timespinner Template
name: Lunais{number} # Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit
game:
Timespinner: 1
requires:
version: 0.1.8
Timespinner:
StartWithJewelryBox: # Start with Jewelry Box unlocked
false: 50
true: 0
DownloadableItems: # With the tablet you will be able to download items at terminals
false: 50
true: 50
FacebookMode: # Requires Oculus Rift(ng) to spot the weakspots in walls and floors
false: 50
true: 0
StartWithMeyef: # Start with Meyef, ideal for when you want to play multiplayer
false: 50
true: 50
QuickSeed: # Start with Talaria Attachment, Nyoom!
false: 50
true: 0
SpecificKeycards: # Keycards can only open corresponding doors
false: 0
true: 50
Inverted: # Start in the past
false: 50
true: 50
```
* All Options are either enabled or not, if values are specified for both true & false the generator will select one based on weight
* The Timespinner Randomizer option "StinkyMaw" is currently always enabled for Archipelago generated seeds
* The Timespinner Randomizer options "ProgressiveVerticalMovement" & "ProgressiveKeycards" are currently not supported on Archipelago generated seeds

View File

@@ -1,4 +1,23 @@
[
{
"gameTitle": "Archipelago",
"tutorials": [
{
"name": "Multiworld Setup Tutorial",
"description": "A Guide to setting up the Archipelago software to generate multiworld games on your computer.",
"files": [
{
"language": "English",
"filename": "archipelago/setup_en.md",
"link": "archipelago/setup/en",
"authors": [
"alwaysintreble"
]
}
]
}
]
},
{
"gameTitle": "The Legend of Zelda: A Link to the Past",
"tutorials": [
@@ -86,6 +105,33 @@
}
]
},
{
"gameTitle": "The Legend of Zelda: Ocarina of Time",
"tutorials": [
{
"name": "Multiworld Setup Tutorial",
"description": "A guide to setting up the Archipelago Ocarina of Time software on your computer.",
"files": [
{
"language": "English",
"filename": "zelda5/setup_en.md",
"link": "zelda5/setup/en",
"authors": [
"Edos"
]
},
{
"language": "Spanish",
"filename": "zelda5/setup_es.md",
"link": "zelda5/setup/es",
"authors": [
"Edos"
]
}
]
}
]
},
{
"gameTitle": "Factorio",
"tutorials": [
@@ -158,5 +204,24 @@
]
}
]
},
{
"gameTitle": "Timespinner",
"tutorials": [
{
"name": "Multiworld Setup Guide",
"description": "A guide to setting up the Timespinner randomizer connected to an Archipelago Multiworld",
"files": [
{
"language": "English",
"filename": "timespinner/setup_en.md",
"link": "timespinner/setup/en",
"authors": [
"Jarno"
]
}
]
}
]
}
]

View File

@@ -2,9 +2,9 @@
## Benötigte Software
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Included in the above Utilities)
- Hardware oder Software zum Laden und Abspielen von SNES Rom-Dateien
- Ein Emulator, der lua-scripts abspielen kann
- [SNI](https://github.com/alttpo/sni/releases) (Integriert in Archipelago)
- Hardware oder Software zum Laden und Abspielen von SNES Rom-Dateien fähig zu einer Internetverbindung
- Ein Emulator, der mit SNI verbinden kann
([snes9x Multitroid](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz),
[BizHawk](http://tasvideos.org/BizHawk.html))
- Ein SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), oder andere kompatible Hardware
@@ -42,7 +42,7 @@ jeder Spieler sein Spiel nach seinem eigenen Geschmack gestalten, während ander
Einstellungen wählen können!
### Wo bekomme ich so eine YAML-Datei her?
Die [Player Settings](/player-settings) Seite auf der Website ermöglicht das einfache Erstellen und Herunterladen
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!
@@ -74,12 +74,12 @@ bei der [YAML Validator](/mysterycheck) Seite tun.
### Erhalte deine Patch-Datei und erstelle dein ROM
Wenn du an einem MultiWorld-Spiel teilnehmen möchtest, wirst du in der Regel vom Host nach deiner YAML-Datei gefragt.
Sobald du diese weitergegeben hast, wird der Host einen Link bereitstellen, wo du deinen Patch oder eine .zip-Datei
mit allen Patches herunterladen kannst. Die Patch-Datei hat immer die Endung `.bmbp`.
mit allen Patches herunterladen kannst. Die Patch-Datei hat immer die Endung `.apbp`.
### Mit dem Client verbinden
#### Via Emulator
Wenn der client den Emulator automatisch gestartet hat, wird QUsb2Snes ebenfalls im Hintergrund gestartet.
Wenn 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.
@@ -88,8 +88,9 @@ durch die Windows Firewall kommunizieren darf.
2. Klicke auf den Reiter "File" oben im Menü und wähle **Lua Scripting**
3. Klicke auf **New Lua Script Window...**
4. Im sich neu öffnenden Fenster, klicke auf **Browse...**
5. Navigiere zum Ort, wo du snes9x Multitroid installiert hast, öffne den `lua`-Ordner und öffne `multibridge.lua`
6. Schaue im Lua-Fenster nach einem Namen, der dir zugeteilt wird und schaue im Client (WebUI im Browser), ob dort
5. Navigiere zum Verzeichnis, wo du Archipelago installiert hast und dort in den Unterordner `SNI`.
6. Wähle dort die `Connector.lua` und klicke auf Öffnen.
7. Schaue im Lua-Fenster nach einem Namen, der dir zugeteilt wird und schaue im Client (WebUI im Browser), ob dort
"Snes Device: Connected" mit demselben Namen dort steht (in der oberen linken Ecke).
##### BizHawk
@@ -99,9 +100,8 @@ durch die Windows Firewall kommunizieren darf.
2. Lade die entsprechende ROM-Datei, wenn sie nicht schon automatisch geladen wurde.
3. Klicke auf das Tools-Menü und klicke auf **Lua Console**
4. Klicke auf den Button um ein neues Lua-Script zu öffnen.
5. Navigiere zum Verzeichnis, wo du die Multiworld Utilities installiert hast und dort in folgende Ordner:
`QUsb2Snes/Qusb2Snes/LuaBridge`
6. Wähle dort die `luabridge.lua` und klicke auf Öffnen.
5. Navigiere zum Verzeichnis, wo du Archipelago installiert hast und dort in den Unterordner `SNI`.
6. Wähle dort die `Connector.lua` und klicke auf Öffnen.
7. Schaue im Lua-Fenster nach einem Namen, der dir zugeteilt wird und schaue im Client (WebUI im Browser), ob dort
"Snes Device: Connected" mit demselben Namen dort steht (in der oberen linken Ecke)
@@ -111,15 +111,11 @@ das noch nicht getan hast, so tue dies am besten jetzt! SD2SNES und FXPak Pro Nu
[hier](https://github.com/RedGuyyyy/sd2snes/releases). Nutzer ähnlicher Hardware finden Hilfestellung
[auf dieser Seite](http://usb2snes.com/#supported-platforms).
**UM MIT HARDWARE ZU VERBINDEN WIRD AKTUELL EINE ALTE VERSION VON QUSB2SNES BENÖTIGT
([v0.7.16](https://github.com/Skarsnik/QUsb2snes/releases/tag/v0.7.16)).**
Neuere Versionen funktionieren möglicherweise nur eingeschränkt, fehlerhaft oder gar nicht!
1. Schließe deinen Emulator, falls er automatisch gestartet haben sollte.
2. Schließe QUsb2Snes, welches automatisch mit dem Client gestartet wurde (in der Taskleiste zu finden).
3. Starte die richtige version von QUsb2Snes (v0.7.16).
4. Starte deine (Original-)Konsole und lade die ROM-Datei.
5. Schaue auf dein Clientfenster, welches nun "Snes Device: Connected" und den namen deiner Konsole
2. Start SNI
3. Starte deine (Original-)Konsole und lade die ROM-Datei.
4. Schaue auf dein Clientfenster, welches nun "Snes Device: Connected" und den namen deiner Konsole
zeigen sollte.
### Mit dem MultiServer verbinden
@@ -137,7 +133,7 @@ können du und deine Freunde loslegen! Glückwunsch zum erfolgreichen Beitritt z
## Ein Multiworld-Spiel hosten
Die Empfohlene Art, ein Spiel zu hosten, ist, den Service auf
[der website](https://berserkermulti.world/generate) zu nutzen. Das Ganze ist recht einfach:
[der website](/generate) zu nutzen. Das Ganze ist recht einfach:
1. Lasse dir von deinen Mitspielern die YAML-Datei zuschicken.
2. Erstelle einen Zip-komprimierten Ordner´, in den du alle YAML-Dateien deiner Spieler einfügst.

View File

@@ -8,9 +8,9 @@
## Required Software
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Included in the above Utilities)
- [SNI](https://github.com/alttpo/sni/releases) (Included in Archipelago)
- Hardware or software capable of loading and playing SNES ROM files
- An emulator capable of running Lua scripts
- An emulator capable of connecting to SNI
([snes9x Multitroid](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz),
[BizHawk](http://tasvideos.org/BizHawk.html))
- An SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), or other compatible hardware
@@ -19,10 +19,9 @@
## Installation Procedures
### Windows Setup
1. Download and install the MultiWorld Utilities from the link above, making sure to install the most recent version.
1. Download and install Archipelago from the link above, making sure to install the most recent version.
**The file is located in the assets section at the bottom of the version information**. If you intend to play normal
multiworld games, you want `Setup.Archipelago.exe`
- If you intend to play the doors variant of multiworld, you will want to download the alternate doors file.
- During the installation process, you will be asked to browse for your Japanese 1.0 ROM file. If you have
installed this software before and are simply upgrading now, you will not be prompted to locate your
ROM file a second time.
@@ -50,7 +49,7 @@ each player to enjoy an experience customized for their taste, and different pla
can all have different options.
### Where do I get a YAML file?
The [Generate Game](/player-settings) page on the website allows you to configure your personal settings and
The [Generate Game](/games/A Link to the Past/player-settings) page on the website allows you to configure your personal settings and
export a YAML file from them.
### Verifying your YAML file
@@ -58,7 +57,7 @@ If you would like to validate your YAML file to make sure it works, you may do s
[YAML Validator](/mysterycheck) page.
## Generating a Single-Player Game
1. Navigate to the [Generate Game](/player-settings), configure your options, and click the "Generate Game" button.
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, where you can download your patch file.
3. Double-click on your patch file, and the emulator should launch with your game automatically. As the
Client is unnecessary for single player games, you may close it and the WebUI.
@@ -68,7 +67,7 @@ If you would like to validate your YAML file to make sure it works, you may do s
### Obtain your patch file and create your ROM
When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that
is done, the host will provide you with either a link to download your patch file, or with a zip file containing
everyone's patch files. Your patch file should have a `.bmbp` extension.
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 file in the same place as your patch file.
@@ -76,7 +75,7 @@ launch the client, and will also create your ROM file in the same place as your
### Connect to the client
#### With an emulator
When the client launched automatically, QUsb2Snes should have also automatically launched in the background.
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.
@@ -98,8 +97,8 @@ Firewall.
3. Click on the Tools menu and click on **Lua Console**
4. Click the button to open a new Lua script.
5. Browse to your MultiWorld Utilities installation directory, and into the following directories:
`QUsb2Snes/Qusb2Snes/LuaBridge`
6. Select `luabridge.lua` and click Open.
`SNI`
6. Select `Connector.lua` and click Open.
7. Observe a name has been assigned to you, and that the client shows "SNES Device: Connected", with that same
name in the upper left corner.

View File

@@ -43,7 +43,7 @@ Cada jugador en una partida de multiworld proveerá su propio fichero YAML. Esta
que cada jugador disfrute de una experiencia personalizada a su gusto, y cada jugador dentro de la misma partida de multiworld puede tener diferentes opciones.
### Donde puedo obtener un fichero YAML?
La página "[Generate Game](/player-settings)" en el sitio web te permite configurar tu configuración personal y
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
@@ -67,7 +67,7 @@ Si quieres validar que tu fichero YAML para asegurarte que funciona correctament
[YAML Validator](/mysterycheck).
## Generar una partida para un jugador
1. Navega a [la pagina Generate game](/player-settings), configura tus opciones, haz click en el boton "Generate game".
1. Navega a [la pagina Generate game](/games/A%20Link%20to%20the%20Past/player-settings), configura tus opciones, haz click en el boton "Generate game".
2. Se te redigirá a una pagina "Seed Info", donde puedes descargar tu archivo de parche.
3. Haz doble click en tu fichero de parche, y el emulador debería ejecutar tu juego automáticamente. Como el
Cliente no es necesario para partidas de un jugador, puedes cerrarlo junto a la pagina web (que tiene como titulo "Multiworld WebUI") que se ha abierto automáticamente.

View File

@@ -49,7 +49,7 @@ sur comment il devrait générer votre seed. Chaque joueur d'un multiwolrd devra
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](/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
@@ -71,7 +71,7 @@ Si vous voulez valider votre fichier YAML pour être sûr qu'il fonctionne, vous
[Validateur de YAML](/mysterycheck).
## Générer une partie pour un joueur
1. Aller sur la page [Génération de partie](/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).

View File

@@ -0,0 +1,387 @@
# Setup Guide for Ocarina of time Archipelago
## Important
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.
## 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.
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
Place the ootMulti.lua file from the previous link inside the "lua" folder from the just installed emulator.
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.
### 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))
```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
name: YourName
game:
Ocarina of Time: 1
requires:
version: 0.1.7 # 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
locations: 50 # Guarantees you will be able to access all locations, and therefore all items
none: 0 # Guarantees only that the game is beatable. You may not be able to access all locations or acquire all items
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.
Ocarina of Time:
logic_rules: # Set the logic used for the generator.
glitchless: 50
glitched: 0
no_logic: 0
logic_no_night_tokens_without_suns_song: # Nighttime skulltulas will logically require Sun's Song.
false: 50
true: 0
open_forest: # Set the state of Kokiri Forest and the path to Deku Tree.
open: 50
closed_deku: 0
closed: 0
open_kakariko: # Set the state of the Kakariko Village gate.
open: 50
zelda: 0
closed: 0
open_door_of_time: # Open the Door of Time by default, without the Song of Time.
false: 0
true: 50
zora_fountain: # Set the state of King Zora, blocking the way to Zora's Fountain.
open: 0
adult: 0
closed: 50
gerudo_fortress: # Set the requirements for access to Gerudo Fortress.
normal: 0
fast: 50
open: 0
bridge: # Set the requirements for the Rainbow Bridge.
open: 0
vanilla: 0
stones: 0
medallions: 50
dungeons: 0
tokens: 0
trials: # Set the number of required trials in Ganon's Castle.
# you can add additional values between minimum and maximum
0: 50 # minimum value
6: 0 # maximum value
random: 0
random-low: 0
random-high: 0
starting_age: # Choose which age Link will start as.
child: 50
adult: 0
triforce_hunt: # Gather pieces of the Triforce scattered around the world to complete the game.
false: 50
true: 0
triforce_goal: # Number of Triforce pieces required to complete the game. Total number placed determined by the Item Pool setting.
# you can add additional values between minimum and maximum
1: 0 # minimum value
50: 0 # maximum value
random: 0
random-low: 0
random-high: 0
20: 50
bombchus_in_logic: # Bombchus are properly considered in logic. The first found pack will have 20 chus; Kokiri Shop and Bazaar sell refills; bombchus open Bombchu Bowling.
false: 50
true: 0
bridge_stones: # Set the number of Spiritual Stones required for the rainbow bridge.
# you can add additional values between minimum and maximum
0: 0 # minimum value
3: 50 # maximum value
random: 0
random-low: 0
random-high: 0
bridge_medallions: # Set the number of medallions required for the rainbow bridge.
# you can add additional values between minimum and maximum
0: 0 # minimum value
6: 50 # maximum value
random: 0
random-low: 0
random-high: 0
bridge_rewards: # Set the number of dungeon rewards required for the rainbow bridge.
# you can add additional values between minimum and maximum
0: 0 # minimum value
9: 50 # maximum value
random: 0
random-low: 0
random-high: 0
bridge_tokens: # Set the number of Gold Skulltula Tokens required for the rainbow bridge.
# you can add additional values between minimum and maximum
0: 0 # minimum value
100: 50 # maximum value
random: 0
random-low: 0
random-high: 0
shuffle_mapcompass: # Control where to shuffle dungeon maps and compasses.
remove: 0
startwith: 50
vanilla: 0
dungeon: 0
overworld: 0
any_dungeon: 0
keysanity: 0
shuffle_smallkeys: # Control where to shuffle dungeon small keys.
remove: 0
vanilla: 0
dungeon: 50
overworld: 0
any_dungeon: 0
keysanity: 0
shuffle_fortresskeys: # Control where to shuffle the Gerudo Fortress small keys.
vanilla: 50
overworld: 0
any_dungeon: 0
keysanity: 0
shuffle_bosskeys: # Control where to shuffle boss keys, except the Ganon's Castle Boss Key.
remove: 0
vanilla: 0
dungeon: 50
overworld: 0
any_dungeon: 0
keysanity: 0
shuffle_ganon_bosskey: # Control where to shuffle the Ganon's Castle Boss Key.
remove: 50
vanilla: 0
dungeon: 0
overworld: 0
any_dungeon: 0
keysanity: 0
on_lacs: 0
enhance_map_compass: # Map tells if a dungeon is vanilla or MQ. Compass tells what the dungeon reward is.
false: 50
true: 0
lacs_condition: # Set the requirements for the Light Arrow Cutscene in the Temple of Time.
vanilla: 50
stones: 0
medallions: 0
dungeons: 0
tokens: 0
lacs_stones: # Set the number of Spiritual Stones required for LACS.
# you can add additional values between minimum and maximum
0: 0 # minimum value
3: 50 # maximum value
random: 0
random-low: 0
random-high: 0
lacs_medallions: # Set the number of medallions required for LACS.
# you can add additional values between minimum and maximum
0: 0 # minimum value
6: 50 # maximum value
random: 0
random-low: 0
random-high: 0
lacs_rewards: # Set the number of dungeon rewards required for LACS.
# you can add additional values between minimum and maximum
0: 0 # minimum value
9: 50 # maximum value
random: 0
random-low: 0
random-high: 0
lacs_tokens: # Set the number of Gold Skulltula Tokens required for LACS.
# you can add additional values between minimum and maximum
0: 0 # minimum value
100: 50 # maximum value
random: 0
random-low: 0
random-high: 0
shuffle_song_items: # Set where songs can appear.
song: 50
dungeon: 0
any: 0
shopsanity: # Randomizes shop contents. Set to "off" to not shuffle shops; "0" shuffles shops but does not allow multiworld items in shops.
0: 0
1: 0
2: 0
3: 0
4: 0
random_value: 0
off: 50
tokensanity: # Token rewards from Gold Skulltulas are shuffled into the pool.
off: 50
dungeons: 0
overworld: 0
all: 0
shuffle_scrubs: # Shuffle the items sold by Business Scrubs, and set the prices.
off: 50
low: 0
regular: 0
random_prices: 0
shuffle_cows: # Cows give items when Epona's Song is played.
false: 50
true: 0
shuffle_kokiri_sword: # Shuffle Kokiri Sword into the item pool.
false: 50
true: 0
shuffle_ocarinas: # Shuffle the Fairy Ocarina and Ocarina of Time into the item pool.
false: 50
true: 0
shuffle_weird_egg: # Shuffle the Weird Egg from Malon at Hyrule Castle.
false: 50
true: 0
shuffle_gerudo_card: # Shuffle the Gerudo Membership Card into the item pool.
false: 50
true: 0
shuffle_beans: # Adds a pack of 10 beans to the item pool and changes the bean salesman to sell one item for 60 rupees.
false: 50
true: 0
shuffle_medigoron_carpet_salesman: # Shuffle the items sold by Medigoron and the Haunted Wasteland Carpet Salesman.
false: 50
true: 0
skip_child_zelda: # Game starts with Zelda's Letter, the item at Zelda's Lullaby, and the relevant events already completed.
false: 50
true: 0
no_escape_sequence: # Skips the tower collapse sequence between the Ganondorf and Ganon fights.
false: 0
true: 50
no_guard_stealth: # The crawlspace into Hyrule Castle skips straight to Zelda.
false: 0
true: 50
no_epona_race: # Epona can always be summoned with Epona's Song.
false: 0
true: 50
skip_some_minigame_phases: # Dampe Race and Horseback Archery give both rewards if the second condition is met on the first attempt.
false: 0
true: 50
complete_mask_quest: # All masks are immediately available to borrow from the Happy Mask Shop.
false: 50
true: 0
useful_cutscenes: # Reenables the Poe cutscene in Forest Temple, Darunia in Fire Temple, and Twinrova introduction. Mostly useful for glitched.
false: 50
true: 0
fast_chests: # All chest animations are fast. If disabled, major items have a slow animation.
false: 0
true: 50
free_scarecrow: # Pulling out the ocarina near a scarecrow spot spawns Pierre without needing the song.
false: 50
true: 0
fast_bunny_hood: # Bunny Hood lets you move 1.5x faster like in Majora's Mask.
false: 50
true: 0
chicken_count: # Controls the number of Cuccos for Anju to give an item as child.
\# you can add additional values between minimum and maximum
0: 0 # minimum value
7: 50 # maximum value
random: 0
random-low: 0
random-high: 0
hints: # Gossip Stones can give hints about item locations.
none: 0
mask: 0
agony: 0
always: 50
hint_dist: # Choose the hint distribution to use. Affects the frequency of strong hints, which items are always hinted, etc.
balanced: 50
ddr: 0
league: 0
mw2: 0
scrubs: 0
strong: 0
tournament: 0
useless: 0
very_strong: 0
text_shuffle: # Randomizes text in the game for comedic effect.
none: 50
except_hints: 0
complete: 0
damage_multiplier: # Controls the amount of damage Link takes.
half: 0
normal: 50
double: 0
quadruple: 0
ohko: 0
no_collectible_hearts: # Hearts will not drop from enemies or objects.
false: 50
true: 0
starting_tod: # Change the starting time of day.
default: 50
sunrise: 0
morning: 0
noon: 0
afternoon: 0
sunset: 0
evening: 0
midnight: 0
witching_hour: 0
start_with_consumables: # Start the game with full Deku Sticks and Deku Nuts.
false: 50
true: 0
start_with_rupees: # Start with a full wallet. Wallet upgrades will also fill your wallet.
false: 50
true: 0
item_pool_value: # Changes the number of items available in the game.
plentiful: 0
balanced: 50
scarce: 0
minimal: 0
junk_ice_traps: # Adds ice traps to the item pool.
off: 0
normal: 50
on: 0
mayhem: 0
onslaught: 0
ice_trap_appearance: # Changes the appearance of ice traps as freestanding items.
major_only: 50
junk_only: 0
anything: 0
logic_earliest_adult_trade: # Earliest item that can appear in the adult trade sequence.
pocket_egg: 0
pocket_cucco: 0
cojiro: 0
odd_mushroom: 0
poachers_saw: 0
broken_sword: 0
prescription: 50
eyeball_frog: 0
eyedrops: 0
claim_check: 0
logic_latest_adult_trade: # Latest item that can appear in the adult trade sequence.
pocket_egg: 0
pocket_cucco: 0
cojiro: 0
odd_mushroom: 0
poachers_saw: 0
broken_sword: 0
prescription: 0
eyeball_frog: 0
eyedrops: 0
claim_check: 50
```
## Joining a MultiWorld Game
### 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.
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.
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 on the bottom textfield /connect <address>:<port> [password], to connect)
Now you are ready to start your adventure in Hyrule.

View File

@@ -0,0 +1,372 @@
# Guia instalación de Ocarina of time Archipelago
## Nota importante
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
## 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.
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.
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
### 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"))
```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
name: YourName{number}
game:
Ocarina of Time: 1
requires:
version: 0.1.7 # Version de archipelago minima.
\# Opciones compartidas por todos los juegos:
accessibility:
items: 0 # Garantiza que puedes obtener todos los objetos pero no todas las localizaciones
locations: 50 # Garantiza que puedes obtener todas las localizaciones
none: 0 # Solo garantiza que el juego pueda completarse.
progression_balancing:
on: 50 # Un sistema para reducir tiempos de espera en una partida multiworld
off: 0
Ocarina of Time:
logic_rules: # Logica usada por el randomizer.
glitchless: 50
glitched: 0
no_logic: 0
logic_no_night_tokens_without_suns_song: # Las skulltulas nocturnas requeriran la cancion del sol por logica
false: 50
true: 0
open_forest: # Indica el estado del bosque Kokiri y el camino al Arbol Deku.
open: 50
closed_deku: 0
closed: 0
open_kakariko: # Indica el estado de la puerta de Kakariko hacia la montaña de la muerte.
open: 50
zelda: 0
closed: 0
open_door_of_time: # Abre la puerta del tiempo sin la cancion del tiempo.
false: 0
true: 50
zora_fountain: # Indica el estado del rey zora bloqueando el camino a la fuente Zora.
open: 0
adult: 0
closed: 50
gerudo_fortress: # Indica los requerimientos para acceder a la fortaleza Gerudo.
normal: 0
fast: 50
open: 0
bridge: # Indica los requerimientos para el puente arco iris.
open: 0
vanilla: 0
stones: 0
medallions: 50
dungeons: 0
tokens: 0
trials: # Numero de pruebas dentro del castillo de Ganon.
0: 50 # minimum value
6: 0 # maximum value
random: 0
random-low: 0
random-high: 0
starting_age: # Indica la edad con la que empieza link.
child: 50
adult: 0
triforce_hunt: # Reune piezas de trifuerza para completar el juego.
false: 50
true: 0
triforce_goal: # Numero de piezas de trifuerza requeridas. El numero de piezas disponibles es determinado por la opcion "Item pool".
1: 0 # minimum value
50: 0 # maximum value
random: 0
random-low: 0
random-high: 0
20: 50
bombchus_in_logic: # Los bombchus son considerados para la logica. El primer pack encontrado da 20 chus y las tiendas kokiri y el bazaar los venden. Bombchus abren la bolera.
false: 50
true: 0
bridge_stones: # Numero de piedras para abrir el puente arco iris.
0: 0 # minimum value
3: 50 # maximum value
random: 0
random-low: 0
random-high: 0
bridge_medallions: # Numero de medallones para abrir el puente arco iris.
0: 0 # minimum value
6: 50 # maximum value
random: 0
random-low: 0
random-high: 0
bridge_rewards: # Numero de mazmorras (cualquier combinacion de medallones y piedras) para abrir el puente arco iris.
0: 0 # minimum value
9: 50 # maximum value
random: 0
random-low: 0
random-high: 0
bridge_tokens: # Numero de skultullas de oro requeridas para el puente arco iris.
0: 0 # minimum value
100: 50 # maximum value
random: 0
random-low: 0
random-high: 0
shuffle_mapcompass: # Controla donde pueden aparecer los mapas y las brujulas.
remove: 0
startwith: 50
vanilla: 0
dungeon: 0
overworld: 0
any_dungeon: 0
keysanity: 0
shuffle_smallkeys: # Controla donde pueden aparecer las llaves pequeñas.
remove: 0
vanilla: 0
dungeon: 50
overworld: 0
any_dungeon: 0
keysanity: 0
shuffle_fortresskeys: # Controla donde pueden aparecer las llaves de la fortaleza Gerudo.
vanilla: 50
overworld: 0
any_dungeon: 0
keysanity: 0
shuffle_bosskeys: # Controla donde pueden aparecer las llaves de jefe (excepto la llave del castillo de ganon).
remove: 0
vanilla: 0
dungeon: 50
overworld: 0
any_dungeon: 0
keysanity: 0
shuffle_ganon_bosskey: # Controla donde puede aparecer la llave del jefe del castillo de Ganon.
remove: 50
vanilla: 0
dungeon: 0
overworld: 0
any_dungeon: 0
keysanity: 0
on_lacs: 0
enhance_map_compass: # El mapa indica si una dungeon es clasica o Master Quest. Las brujulas indican la recompensa de mazmorra.
false: 50
true: 0
lacs_condition: # Marca el requerimiento para la escena de las flechas de luz (LACS) en el templo del tiempo.
vanilla: 50
stones: 0
medallions: 0
dungeons: 0
tokens: 0
lacs_stones: # Marca el numero de piedras espirituales requeridas para LACS
0: 0 # minimum value
3: 50 # maximum value
random: 0
random-low: 0
random-high: 0
lacs_medallions: # Marca el numero de medallones requeridas para LACS.
0: 0 # minimum value
6: 50 # maximum value
random: 0
random-low: 0
random-high: 0
lacs_rewards: # Marca el numero de recompensas de mazmorra requeridas para LACS.
0: 0 # minimum value
9: 50 # maximum value
random: 0
random-low: 0
random-high: 0
lacs_tokens: # Marca el numero de Skulltulas de oro requeridas para LACS.
0: 0 # minimum value
100: 50 # maximum value
random: 0
random-low: 0
random-high: 0
shuffle_song_items: # Marca donde pueden aparecer las canciones.
song: 50
dungeon: 0
any: 0
shopsanity: # Aleatoriza el contenido de las tiendas. "off" para no mezclar las tiendas; "0" mezcla las tiendas pero no permite objetos unicos en ellas.
0: 0
1: 0
2: 0
3: 0
4: 0
random_value: 0
off: 50
tokensanity: # Indica si las Skulltulas de oro pueden tener objetos que no sean su ficha.
off: 50
dungeons: 0
overworld: 0
all: 0
shuffle_scrubs: # Aleatoriza los objetos de los Scrubs vendedores y marca su precio.
off: 50
low: 0
regular: 0
random_prices: 0
shuffle_cows: # Las vacas dan objetos cuando les tocas las cancion de Epona.
false: 50
true: 0
shuffle_kokiri_sword: # Aleatoriza la posicion de la espada Kokiri.
false: 50
true: 0
shuffle_ocarinas: # Aleatoriza la posicion de las ocarinas.
false: 50
true: 0
shuffle_weird_egg: # Aleatoriza la posicion del huevo extraño.
false: 50
true: 0
shuffle_gerudo_card: # Aleatoriza la posicion de la tarjeta de membresia Gerudo.
false: 50
true: 0
shuffle_beans: # Añade un pack de 10 judias magicas al juego y el vendedor vende un solo objeto por 60 rupias.
false: 50
true: 0
shuffle_medigoron_carpet_salesman: # Aleatoriza el objeto que vende Medigoron y el vendedor de la alfombra voladora del paramo maldito.
false: 50
true: 0
skip_child_zelda: # Empieza el juego con la carta de zelda, el objeto que daria impa al enseñar la nana de zelda. Y zelda se considera ya visitada (puedes ir directamente a ver a Saria al bosque y a Malon al rancho)
false: 50
true: 0
no_escape_sequence: # Elimina la huida de link y zelda despues de ganar a Ganondorf.
false: 0
true: 50
no_guard_stealth: # Elimina la escena de sigilo antes de ver a Zelda.
false: 0
true: 50
no_epona_race: # No necesitas hacer la carrera para invocar a Epona.
false: 0
true: 50
skip_some_minigame_phases: # La carrera de Dampe y el minijuego de arco a caballo dan ambras recompensas a la vez si se cumplen las condiciones.
false: 0
true: 50
complete_mask_quest: # Todas las mascaras estan disponibles.
false: 50
true: 0
useful_cutscenes: # Ciertas escenas se mantienen (como los Poes del templo del bosque, Darunia o Twinrova. Principalmente util para modos con Glitches.
false: 50
true: 0
fast_chests: # Los cofres siempre se cogen rapido. Si se desactiva, los objetos importantes tienen animacion lenta. (IMPORTANTE: TODOS LOS OBJETOS QUE VAYAN A OTROS MUNDOS SE CONSIDERAN IMPORTANTES)
false: 0
true: 50
free_scarecrow: # Sacara la ocraina cerca de un punto con espantapajaros invoca a Pierre sin necesidad de la cancion.
false: 50
true: 0
fast_bunny_hood: # La capucha conejo mejora tu velocidad como en Majora's Mask.
false: 50
true: 0
chicken_count: # Numero de Cuccos que Anju necesita en el corral para que te de el objeto.
0: 0 # minimum value
7: 50 # maximum value
random: 0
random-low: 0
random-high: 0
hints: # Marca el requerimiento para que las piedras chivatas den pistas.
none: 0
mask: 0
agony: 0
always: 50
hint_dist: # Elije la distribucion de pistas
balanced: 50
ddr: 0
league: 0
mw2: 0
scrubs: 0
strong: 0
tournament: 0
useless: 0
very_strong: 0
damage_multiplier: # Controla el daño que recibe Link.
half: 0
normal: 50
double: 0
quadruple: 0
ohko: 0
no_collectible_hearts: # No caen corazones de enemigos u objetos.
false: 50
true: 0
starting_tod: # Cambia el momento del dia al empezar el juego.
default: 50
sunrise: 0
morning: 0
noon: 0
afternoon: 0
sunset: 0
evening: 0
midnight: 0
witching_hour: 0
start_with_consumables: # Empieza el juego con el maximo de palos y nueves Deku que pueda llevar Link.
false: 50
true: 0
start_with_rupees: # Empieza el juego con la cartera llena. Las mejoras de cartera vienen llenas.
false: 50
true: 0
item_pool_value: # Cambia el numero de objetos disponibles en el juego.
plentiful: 0
balanced: 50
scarce: 0
minimal: 0
junk_ice_traps: # Añade trampas de hielo.
off: 0
normal: 50
on: 0
mayhem: 0
onslaught: 0
ice_trap_appearance: # Cambia la apariencia de las trampas de hielo cuando aparecen como objetos fuera de cofres.
major_only: 50
junk_only: 0
anything: 0
logic_earliest_adult_trade: # Objeto mas bajo que puede aparecer en la secuencia de cambios de Link Adulto.
pocket_egg: 0
pocket_cucco: 0
cojiro: 0
odd_mushroom: 0
poachers_saw: 0
broken_sword: 0
prescription: 50
eyeball_frog: 0
eyedrops: 0
claim_check: 0
logic_latest_adult_trade: # Objeto mas tardio que puede aparecer en la secuencia de cambios de Link Adulto.
pocket_egg: 0
pocket_cucco: 0
cojiro: 0
odd_mushroom: 0
poachers_saw: 0
broken_sword: 0
prescription: 0
eyeball_frog: 0
eyedrops: 0
claim_check: 50
```
## Unirse a un juego MultiWorld
### 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`.
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.
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

@@ -25,6 +25,21 @@
margin-bottom: 1rem;
}
#generate-game-form{
#generate-game-form-wrapper table td{
text-align: left;
padding-right: 0.5rem;
}
#generate-form-button-row{
display: flex;
flex-direction: row;
justify-content: center;
}
#file-input{
display: none;
}
.interactive{
color: #ffef00;
}

View File

@@ -118,3 +118,21 @@
width: 100%;
text-align: right;
}
.markdown table{
border-collapse: collapse;
margin-bottom: 0.5rem;
}
.markdown table th{
text-align: left;
font-weight: bold;
border: 1px solid #eeffeb;
padding: 0.25rem;
}
.markdown table td{
text-align: left;
border: 1px solid #eeffeb;
padding: 0.25rem;
}

View File

@@ -0,0 +1,136 @@
#player-tracker-wrapper{
margin: 0;
}
#inventory-table{
border-top: 2px solid #000000;
border-left: 2px solid #000000;
border-right: 2px solid #000000;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
padding: 3px 3px 10px;
width: 448px;
background-color: rgb(60, 114, 157);
}
#inventory-table td{
width: 40px;
height: 40px;
text-align: center;
vertical-align: middle;
}
#inventory-table img{
height: 100%;
max-width: 40px;
max-height: 40px;
filter: grayscale(100%) contrast(75%) brightness(30%);
}
#inventory-table img.acquired{
filter: none;
}
#inventory-table div.counted-item {
position: relative;
}
#inventory-table div.item-count {
position: absolute;
color: white;
font-family: monospace;
font-weight: bold;
font-size: 1.1em;
bottom: 0px;
right: 8px;
}
#location-table{
width: 448px;
border-left: 2px solid #000000;
border-right: 2px solid #000000;
border-bottom: 2px solid #000000;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
background-color: rgb(60, 114, 157);
padding: 0 3px 3px;
font-family: monospace;
font-size: 15px;
cursor: default;
}
#location-table th{
vertical-align: middle;
text-align: left;
padding-right: 10px;
}
#location-table td{
padding-top: 2px;
padding-bottom: 2px;
line-height: 20px;
}
#location-table td.counter {
text-align: right;
font-size: 15px;
}
#location-table td.toggle-arrow {
text-align: right;
}
#location-table tr#Total-header {
font-weight: bold;
}
#location-table img{
height: 100%;
max-width: 30px;
max-height: 30px;
}
#location-table tbody.locations {
font-size: 13px;
}
#location-table td.location-name {
padding-left: 16px;
}
.hide {
display: none;
}
.right-align {
text-align: right;
font-weight: bold;
}
#location-table td:first-child {
width: 272px;
}
.location-category td:first-child {
padding-right: 16px;
}
#inventory-table img.acquired#lullaby{
filter: sepia(100%) hue-rotate(-60deg); /* css trick to hue-shift a static image */
}
#inventory-table img.acquired#epona{
filter: sepia(100%) hue-rotate(-20deg) saturate(250%);
}
#inventory-table img.acquired#saria{
filter: sepia(100%) hue-rotate(60deg) saturate(150%);
}
#inventory-table img.acquired#sun{
filter: sepia(100%) hue-rotate(15deg) saturate(200%) brightness(120%);
}
#inventory-table img.acquired#time{
filter: sepia(100%) hue-rotate(160deg) saturate(150%);
}

View File

@@ -0,0 +1,12 @@
#start-playing-wrapper{
display: flex;
flex-direction: row;
justify-content: center;
flex-wrap: wrap;
}
#start-playing{
width: 700px;
min-height: 240px;
text-align: center;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,33 +11,67 @@
{% include 'header/oceanHeader.html' %}
<div id="generate-game-wrapper">
<div id="generate-game" class="grass-island">
<h1>Upload Config{% if race %} (Race Mode){% endif %}</h1>
<h1>Generate Game{% if race %} (Race Mode){% endif %}</h1>
<p>
This page allows you to generate a game by uploading a yaml file or a zip file containing yaml files.
If you do not have a config (yaml) file yet, you may create one on the
<a href="/player-settings">Player Settings</a> page.
This page allows you to generate a game by uploading a config file or a zip file containing config
files. If you do not have a config (.yaml) file yet, you may create one on the game's settings page,
which you can find via the <a href="{{ url_for("games") }}">supported games list</a>.
</p>
<p>
{% if race -%}
This game will be generated in race mode, meaning the spoiler log will be unavailable,
roms will be encrypted, and single-player games will have no multidata files.
This game will be generated in race mode,
meaning the spoiler log will be unavailable and game specific protections will be in place,
like ROM encryption or cheat mode removal.
{%- else -%}
If you would like to generate a race game,
<a href="{{ url_for("generate", race=True) }}">click here.</a> Race games are generated without
a spoiler log, the ROMs are encrypted, and single-player games will not include a multidata file.
<a href="{{ url_for("generate", race=True) }}">click here.</a><br />
Race games are generated without a spoiler log and game specific protections will be in place,
like ROM encryption or cheat mode removal.
{%- endif -%}
</p>
<p>
After generation is complete, you will have the option to download a patch file.
This patch file can be opened with the
<a href="https://github.com/ArchipelagoMW/Archipelago/releases">client</a>, which can be
used to to create a rom file. In-browser patching is planned for the future.
</p>
<div id="generate-game-form-wrapper">
<form id="generate-game-form" method="post" enctype="multipart/form-data">
<input id="file-input" type="file" name="file">
<table>
<tbody>
<tr>
<td><label for="forfeit_mode">Forfeit Permission:</label></td>
<td>
<select name="forfeit_mode" id="forfeit_mode">
<option value="auto">Automatic on goal completion</option>
<option value="goal">Allow !forfeit after goal completion</option>
<option value="auto-enabled">Automatic on goal completion and manual !forfeit</option>
<option value="enabled">Manual !forfeit</option>
<option value="disabled">Disabled</option>
</select>
</td>
</tr>
<tr>
<td>
<label for="hint_cost"> Hint Cost:</label>
<span
class="interactive"
data-tooltip="After gathering this many checks, players can !hint <itemname>
to get the location of that hint item.">(?)
</span>
</td>
<td>
<select name="hint_cost" id="hint_cost">
{% for n in range(0, 110, 5) %}
<option {% if n == 10 %}selected="selected" {% endif %} value="{{ n }}">
{% if n > 100 %}Off{% else %}{{ n }}%{% endif %}
</option>
{% endfor %}
</select>
</td>
</tr>
</tbody>
</table>
<div id="generate-form-button-row">
<input id="file-input" type="file" name="file">
</div>
</form>
<button id="generate-game-button">Upload</button>
<button id="generate-game-button">Upload File</button>
</div>
</div>
</div>

View File

@@ -20,6 +20,7 @@
<tr>
<th>Item</th>
<th>Amount</th>
<th>Order Received</th>
</tr>
</thead>
<tbody>
@@ -28,6 +29,7 @@
<tr>
<td>{{ name | item_name }}</td>
<td>{{ count }}</td>
<td>{{received_items[name]}}</td>
</tr>
{%- endfor -%}

View File

@@ -13,7 +13,7 @@
<div id="base-header-right">
<a href="/games">supported games</a>
<a href="/tutorial">setup guides</a>
<a href="/uploads">start game</a>
<a href="/start-playing">start playing</a>
<a href="/faq/en">f.a.q.</a>
<a href="https://discord.gg/8Z65BR2" target="_blank">discord</a>
</div>

View File

@@ -15,17 +15,17 @@
<h1>Host Game</h1>
<p>
This page allows you to host a game which was not generated by the website. For example, if you have
generated a doors game on your own computer, you may upload the zip file created by the generator to
host the game here. This will also provide the tracker, and the ability for your players to download
generated a game on your own computer, you may upload the zip file created by the generator to
host the game here. This will also provide a tracker, and the ability for your players to download
their patch files.
<br /><br />
In addition to a zip file created by the generator, you may upload a multidata file here as well.
In addition to the zip file created by the generator, you may upload a multidata file here as well.
</p>
<div id="host-game-form-wrapper">
<form id="host-game-form" method="post" enctype="multipart/form-data">
<input id="file-input" type="file" name="file">
</form>
<button id="host-game-button">Upload</button>
<button id="host-game-button">Upload File</button>
</div>
</div>
</div>

View File

@@ -13,7 +13,7 @@
<h4>multiworld multi-game randomizer</h4>
</div>
<div id="landing-links">
<a href="/uploads" id="mid-button">start<br />game</a>
<a href="/start-playing" id="mid-button">start<br />playing</a>
<a href="/games" id="far-left-button">supported<br />games</a>
<a href="/tutorial" id="mid-left-button">setup guides</a>
<a href="https://discord.gg/8Z65BR2" id="far-right-button" target="_blank">discord</a>
@@ -50,7 +50,7 @@
</p>
<p>
<span class="variable">{{ seeds }}</span>
games were created and
games were generated and
<span class="variable">{{ rooms }}</span>
were hosted in the last 7 days.
</p>

View File

@@ -0,0 +1,180 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>{{ player_name }}&apos;s Tracker</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/ootTracker.css') }}"/>
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/ootTracker.js') }}"></script>
</head>
<body>
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
<table id="inventory-table">
<tr>
<td><img src="{{ ocarina_url }}" class="{{ 'acquired' if 'Ocarina' in acquired_items }}" title="Ocarina" /></td>
<td><img src="{{ icons['Bombs'] }}" class="{{ 'acquired' if 'Bomb Bag' in acquired_items }}" title="Bombs" /></td>
<td><img src="{{ icons['Bow'] }}" class="{{ 'acquired' if 'Bow' in acquired_items }}" title="Fairy Bow" /></td>
<td><img src="{{ icons['Fire Arrows'] }}" class="{{ 'acquired' if 'Fire Arrows' in acquired_items }}" title="Fire Arrows" /></td>
<td><img src="{{ icons['Kokiri Sword'] }}" class="{{ 'acquired' if 'Kokiri Sword' in acquired_items }}" title="Kokiri Sword" /></td>
<td><img src="{{ icons['Biggoron Sword'] }}" class="{{ 'acquired' if 'Biggoron Sword' in acquired_items }}" title="Biggoron's Sword" /></td>
<td><img src="{{ icons['Mirror Shield'] }}" class="{{ 'acquired' if 'Mirror Shield' in acquired_items }}" title="Mirror Shield" /></td>
</tr>
<tr>
<td><img src="{{ icons['Slingshot'] }}" class="{{ 'acquired' if 'Slingshot' in acquired_items }}" title="Slingshot" /></td>
<td><img src="{{ icons['Bombchus'] }}" class="{{ 'acquired' if has_bombchus }}" title="Bombchus" /></td>
<td>
<div class="counted-item">
<img src="{{ hookshot_url }}" class="{{ 'acquired' if 'Progressive Hookshot' in acquired_items }}" title="Progressive Hookshot" />
<div class="item-count">{{ hookshot_length }}</div>
</div>
</td>
<td><img src="{{ icons['Ice Arrows'] }}" class="{{ 'acquired' if 'Ice Arrows' in acquired_items }}" title="Ice Arrows" /></td>
<td><img src="{{ strength_upgrade_url }}" class="{{ 'acquired' if 'Progressive Strength Upgrade' in acquired_items }}" title="Progressive Strength Upgrade" /></td>
<td><img src="{{ icons['Goron Tunic'] }}" class="{{ 'acquired' if 'Goron Tunic' in acquired_items }}" title="Goron Tunic" /></td>
<td><img src="{{ icons['Zora Tunic'] }}" class="{{ 'acquired' if 'Zora Tunic' in acquired_items }}" title="Zora Tunic" /></td>
</tr>
<tr>
<td><img src="{{ icons['Boomerang'] }}" class="{{ 'acquired' if 'Boomerang' in acquired_items }}" title="Boomerang" /></td>
<td><img src="{{ icons['Lens of Truth'] }}" class="{{ 'acquired' if 'Lens of Truth' in acquired_items }}" title="Lens of Truth" /></td>
<td><img src="{{ icons['Megaton Hammer'] }}" class="{{ 'acquired' if 'Megaton Hammer' in acquired_items }}" title="Megaton Hammer" /></td>
<td><img src="{{ icons['Light Arrows'] }}" class="{{ 'acquired' if 'Light Arrows' in acquired_items }}" title="Light Arrows" /></td>
<td><img src="{{ scale_url }}" class="{{ 'acquired' if 'Progressive Scale' in acquired_items }}" title="Progressive Scale" /></td>
<td><img src="{{ icons['Iron Boots'] }}" class="{{ 'acquired' if 'Iron Boots' in acquired_items }}" title="Iron Boots" /></td>
<td><img src="{{ icons['Hover Boots'] }}" class="{{ 'acquired' if 'Hover Boots' in acquired_items }}" title="Hover Boots" /></td>
</tr>
<tr>
<td>
<div class="counted-item">
<img src="{{ bottle_url }}" class="{{ 'acquired' if bottle_count > 0 }}" title="Bottles" />
<div class="item-count">{{ bottle_count if bottle_count > 0 else '' }}</div>
</div>
</td>
<td><img src="{{ icons['Dins Fire'] }}" class="{{ 'acquired' if 'Dins Fire' in acquired_items }}" title="Din's Fire" /></td>
<td><img src="{{ icons['Farores Wind'] }}" class="{{ 'acquired' if 'Farores Wind' in acquired_items }}" title="Farore's Wind" /></td>
<td><img src="{{ icons['Nayrus Love'] }}" class="{{ 'acquired' if 'Nayrus Love' in acquired_items }}" title="Nayru's Love" /></td>
<td>
<div class="counted-item">
<img src="{{ wallet_url }}" class="{{ 'acquired' if 'Progressive Wallet' in acquired_items }}" title="Progressive Wallet" />
<div class="item-count">{{ wallet_size }}</div>
</div>
</td>
<td><img src="{{ magic_meter_url }}" class="{{ 'acquired' if 'Magic Meter' in acquired_items }}" title="Magic Meter" /></td>
<td><img src="{{ icons['Gerudo Membership Card'] }}" class="{{ 'acquired' if 'Gerudo Membership Card' in acquired_items }}" title="Gerudo Membership Card" /></td>
</tr>
<tr>
<td>
<div class="counted-item">
<img src="{{ icons['Zeldas Lullaby'] }}" class="{{ 'acquired' if 'Zeldas Lullaby' in acquired_items }}" title="Zelda's Lullaby" id="lullaby"/>
<div class="item-count">Zelda</div>
</div>
</td>
<td>
<div class="counted-item">
<img src="{{ icons['Eponas Song'] }}" class="{{ 'acquired' if 'Eponas Song' in acquired_items }}" title="Epona's Song" id="epona" />
<div class="item-count">Epona</div>
</div>
</td>
<td>
<div class="counted-item">
<img src="{{ icons['Sarias Song'] }}" class="{{ 'acquired' if 'Sarias Song' in acquired_items }}" title="Saria's Song" id="saria"/>
<div class="item-count">Saria</div>
</div>
</td>
<td>
<div class="counted-item">
<img src="{{ icons['Suns Song'] }}" class="{{ 'acquired' if 'Suns Song' in acquired_items }}" title="Sun's Song" id="sun"/>
<div class="item-count">Sun</div>
</div>
</td>
<td>
<div class="counted-item">
<img src="{{ icons['Song of Time'] }}" class="{{ 'acquired' if 'Song of Time' in acquired_items }}" title="Song of Time" id="time"/>
<div class="item-count">Time</div>
</div>
</td>
<td>
<div class="counted-item">
<img src="{{ icons['Song of Storms'] }}" class="{{ 'acquired' if 'Song of Storms' in acquired_items }}" title="Song of Storms" />
<div class="item-count">Storms</div>
</div>
</td>
<td>
<div class="counted-item">
<img src="{{ icons['Gold Skulltula Token'] }}" class="{{ 'acquired' if token_count > 0 }}" title="Gold Skulltula Tokens" />
<div class="item-count">{{ token_count }}</div>
</div>
</td>
</tr>
<tr>
<td>
<div class="counted-item">
<img src="{{ icons['Minuet of Forest'] }}" class="{{ 'acquired' if 'Minuet of Forest' in acquired_items }}" title="Minuet of Forest" />
<div class="item-count">Min</div>
</div>
</td>
<td>
<div class="counted-item">
<img src="{{ icons['Bolero of Fire'] }}" class="{{ 'acquired' if 'Bolero of Fire' in acquired_items }}" title="Bolero of Fire" />
<div class="item-count">Bol</div>
</div>
</td>
<td>
<div class="counted-item">
<img src="{{ icons['Serenade of Water'] }}" class="{{ 'acquired' if 'Serenade of Water' in acquired_items }}" title="Serenade of Water" />
<div class="item-count">Ser</div>
</div>
</td>
<td>
<div class="counted-item">
<img src="{{ icons['Requiem of Spirit'] }}" class="{{ 'acquired' if 'Requiem of Spirit' in acquired_items }}" title="Requiem of Spirit" />
<div class="item-count">Req</div>
</div>
</td>
<td>
<div class="counted-item">
<img src="{{ icons['Nocturne of Shadow'] }}" class="{{ 'acquired' if 'Nocturne of Shadow' in acquired_items }}" title="Nocturne of Shadow" />
<div class="item-count">Noc</div>
</div>
</td>
<td>
<div class="counted-item">
<img src="{{ icons['Prelude of Light'] }}" class="{{ 'acquired' if 'Prelude of Light' in acquired_items }}" title="Prelude of Light" />
<div class="item-count">Pre</div>
</div>
</td>
<td>
<div class="counted-item">
<img src="{{ icons['Triforce'] if game_finished else icons['Triforce Piece'] }}" class="{{ 'acquired' if game_finished or piece_count > 0 }}" title="{{ 'Triforce' if game_finished else 'Triforce Pieces' }}" id=triforce />
<div class="item-count">{{ piece_count if piece_count > 0 else '' }}</div>
</div>
</td>
</tr>
</table>
<table id="location-table">
<tr>
<td></td>
<td><img src="{{ icons['Small Key'] }}" title="Small Keys" /></td>
<td><img src="{{ icons['Boss Key'] }}" title="Boss Key" /></td>
<td class="right-align">Items</td>
</tr>
{% for area in checks_done %}
<tr class="location-category" id="{{area}}-header">
<td>{{ area }} {{'▼' if area != 'Total'}}</td>
<td class="smallkeys">{{ small_key_counts.get(area, '-') }}</td>
<td class="bosskeys">{{ boss_key_counts.get(area, '-') }}</td>
<td class="counter">{{ checks_done[area] }} / {{ checks_in_area[area] }}</td>
</tr>
<tbody class="locations hide" id="{{area}}">
{% for location in location_info[area] %}
<tr>
<td class="location-name">{{ location }}</td>
<td></td>
<td></td>
<td class="counter">{{ '✔' if location_info[area][location] else '' }}</td>
</tr>
{% endfor %}
</tbody>
{% endfor %}
</table>
</div>
</body>
</html>

View File

@@ -36,22 +36,7 @@ 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"
{%- macro range_option(option) %}
# you can add additional values between minimum and maximum
{%- set data, notes = dictify_range(option) %}
@@ -62,14 +47,14 @@ progression_balancing:
{{ game }}:
{%- for option_key, option in options.items() %}
{{ option_key }}:{% if option.__doc__ %} # {{ option.__doc__ | replace('\n', '\n#') | indent(4, first=False) }}{% endif %}
{%- if option.range_start is defined %}
{%- if option.range_start is defined and option.range_start is number %}
{{- range_option(option) -}}
{%- elif option.options -%}
{%- for suboption_option_id, sub_option_name in option.name_lookup.items() %}
{{ sub_option_name }}: {% if suboption_option_id == option.default %}50{% else %}0{% endif %}
{%- endfor -%}
{%- else %}
{{ yaml_dump(option.default) | indent(4, first=False) }}
{{ yaml_dump(default_converter(option.default)) | indent(4, first=False) }}
{%- endif -%}
{%- endfor %}
{% if not options %}{}{% endif %}

View File

@@ -4,6 +4,7 @@
<title>{{ game }} Settings</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/player-settings.css") }}" />
<script type="application/ecmascript" src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/md5.min.js") }}"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/js-yaml.min.js") }}"></script>
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/player-settings.js") }}"></script>
{% endblock %}
@@ -20,7 +21,7 @@
A list of all games you have generated can be found <a href="/user-content">here</a>.
<br />
Advanced users can download a template file for this game
<a href="/static/generated/{{ game }}.yaml">here</a>.
<a href="/static/generated/configs/{{ game }}.yaml">here</a>.
</p>
<p><label for="player-name">Please enter your player name. This will appear in-game as you send and receive

View File

@@ -0,0 +1,32 @@
{% extends 'pageWrapper.html' %}
{% block head %}
{{ super() }}
<title>Start Playing</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/startPlaying.css") }}" />
{% endblock %}
{% block body %}
{% include 'header/oceanHeader.html' %}
<div id="start-playing-wrapper">
<div id="start-playing" class="grass-island {% if rooms %}wider{% endif %}">
<h1>Start Playing</h1>
<p>
If you're ready to start playing but don't know where to begin, check out the
<a href="/tutorial">tutorials</a> page. It has all the resources you need to create a config file
and get started. If you already have a config file, or a zip file containing them, read on.
<br /><br />
To start playing a game, you'll first need to <a href="/generate">generate a randomized game</a>.
You'll need to upload either a config file or a zip file containing one more more config files.
<br /><br />
If you have already generated a game and just need to host it, this site can<br />
<a href="uploads">host a pre-generated game</a> for you.
</p>
</div>
</div>
{% include 'islandFooter.html' %}
{% endblock %}

View File

@@ -10,8 +10,12 @@
<div id="games">
<h1>Currently Supported Games</h1>
{% for game, description in worlds.items() %}
<h3><a href="{{ url_for("game_page", game=game) }}/player-settings">{{ game }}</a></h3>
<p>{{ description }}</p>
<h3><a href="{{ url_for("game_info", game=game, lang="en") }}">{{ game }}</a></h3>
<p>
<a href="{{ url_for("player_settings", game=game) }}">Settings Page</a>
<br />
{{ description }}
</p>
{% endfor %}
</div>
{% endblock %}

View File

@@ -3,7 +3,7 @@
{% block head %}
{% include 'header/grassHeader.html' %}
<title>Archipelago</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tutorial.css") }}" />
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.1/showdown.min.js"
integrity="sha512-L03kznCrNOfVxOUovR6ESfCz9Gfny7gihUX/huVbQB9zjODtYpxaVtIaAkpetoiyV2eqWbvxMH9fiSv5enX7bw=="
crossorigin="anonymous"></script>
@@ -11,7 +11,7 @@
{% endblock %}
{% block body %}
<div id="tutorial-wrapper" data-game="{{ game }}" data-file="{{ file }}" data-lang="{{ lang }}">
<div id="tutorial-wrapper" class="markdown" data-game="{{ game }}" data-file="{{ file }}" data-lang="{{ lang }}">
<!-- Content generated by JavaScript -->
</div>
{% endblock %}

View File

@@ -12,10 +12,6 @@
<div id="view-seed-wrapper">
<div id="view-seed" class="grass-island">
<h1>Seed Info</h1>
{% if not seed.multidata and not seed.spoiler %}
<p>Single Player Race Rom: No spoiler or multidata exists, parts of the rom are encrypted and rooms
cannot be created.</p>
{% endif %}
<table>
<tbody>
<tr>
@@ -33,18 +29,6 @@
</tr>
{% endif %}
{% if seed.multidata %}
<tr>
<td>Players:&nbsp;</td>
<td>
<ul>
{% for patch in seed.slots|sort(attribute='player_id') %}
<li>
<a href="{{ url_for("download_raw_patch", seed_id=seed.id, player_id=patch.player_id) }}">{{ patch.player_name }}</a>
</li>
{% endfor %}
</ul>
</td>
</tr>
<tr>
<td>Rooms:&nbsp;</td>
<td>

View File

@@ -10,6 +10,7 @@ from WebHostLib import app, cache, Room
from Utils import restricted_loads
from worlds import lookup_any_item_id_to_name, lookup_any_location_id_to_name
def get_alttp_id(item_name):
return Items.item_table[item_name][2]
@@ -283,6 +284,7 @@ def render_timedelta(delta: datetime.timedelta):
_multidata_cache = {}
def get_location_table(checks_table: dict) -> dict:
loc_to_area = {}
for area, locations in checks_table.items():
@@ -292,6 +294,7 @@ def get_location_table(checks_table: dict) -> dict:
loc_to_area[location] = area
return loc_to_area
def get_static_room_data(room: Room):
result = _multidata_cache.get(room.seed.id, None)
if result:
@@ -311,7 +314,7 @@ def get_static_room_data(room: Room):
seed_checks_in_area["Total"] = 249
player_checks_in_area = {playernumber: {areaname: len(multidata["checks_in_area"][playernumber][areaname])
if areaname != "Total" else multidata["checks_in_area"][playernumber]["Total"]
if areaname != "Total" else multidata["checks_in_area"][playernumber]["Total"]
for areaname in ordered_areas}
for playernumber in range(1, len(names[0]) + 1)}
player_location_to_area = {playernumber: get_location_table(multidata["checks_in_area"][playernumber])
@@ -373,9 +376,9 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
for location in locations_checked:
if location in player_locations:
item, recipient = player_locations[location]
if recipient == tracked_player: # a check done for the tracked player
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
if ms_player == tracked_player: # a check done by the tracked player
checks_done[location_to_area[location]] += 1
checks_done["Total"] += 1
if games[tracked_player] == "A Link to the Past":
@@ -403,28 +406,28 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
# Determine which icon to use
display_data = {}
for item_name, item_id in progressive_items.items():
level = min(inventory[item_id], len(progressive_names[item_name])-1)
level = min(inventory[item_id], len(progressive_names[item_name]) - 1)
display_name = progressive_names[item_name][level]
acquired = True
if not display_name:
acquired = False
display_name = progressive_names[item_name][level+1]
display_name = progressive_names[item_name][level + 1]
base_name = item_name.split(maxsplit=1)[1].lower()
display_data[base_name+"_acquired"] = acquired
display_data[base_name+"_url"] = icons[display_name]
display_data[base_name + "_acquired"] = acquired
display_data[base_name + "_url"] = icons[display_name]
# The single player tracker doesn't care about overworld, underworld, and total checks. Maybe it should?
sp_areas = ordered_areas[2:15]
return render_template("lttpTracker.html", inventory=inventory,
player_name=player_name, room=room, icons=icons, checks_done=checks_done,
checks_in_area=seed_checks_in_area[tracked_player], acquired_items={lookup_any_item_id_to_name[id] for id in inventory},
checks_in_area=seed_checks_in_area[tracked_player],
acquired_items={lookup_any_item_id_to_name[id] for id in inventory},
small_key_ids=small_key_ids, big_key_ids=big_key_ids, sp_areas=sp_areas,
key_locations=player_small_key_locations[tracked_player],
big_key_locations=player_big_key_locations[tracked_player],
**display_data)
elif games[tracked_player] == "Minecraft":
elif games[tracked_player] == "Minecraft":
minecraft_icons = {
"Wooden Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/d/d2/Wooden_Pickaxe_JE3_BE3.png",
"Stone Pickaxe": "https://static.wikia.nocookie.net/minecraft_gamepedia/images/c/c4/Stone_Pickaxe_JE2_BE2.png",
@@ -455,14 +458,14 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
}
minecraft_location_ids = {
"Story": [42073, 42080, 42081, 42023, 42082, 42027, 42039, 42085, 42002, 42009, 42010,
"Story": [42073, 42080, 42081, 42023, 42082, 42027, 42039, 42085, 42002, 42009, 42010,
42070, 42041, 42049, 42090, 42004, 42031, 42025, 42029, 42051, 42077, 42089],
"Nether": [42017, 42044, 42069, 42058, 42034, 42060, 42066, 42076, 42064, 42071, 42021,
"Nether": [42017, 42044, 42069, 42058, 42034, 42060, 42066, 42076, 42064, 42071, 42021,
42062, 42008, 42061, 42033, 42011, 42006, 42019, 42000, 42040, 42001, 42015, 42014],
"The End": [42052, 42005, 42012, 42032, 42030, 42042, 42018, 42038, 42046],
"Adventure": [42047, 42086, 42087, 42050, 42059, 42055, 42072, 42003, 42035, 42016, 42020,
"Adventure": [42047, 42086, 42087, 42050, 42059, 42055, 42072, 42003, 42035, 42016, 42020,
42048, 42054, 42068, 42043, 42074, 42075, 42024, 42026, 42037, 42045, 42056, 42088],
"Husbandry": [42065, 42067, 42078, 42022, 42007, 42079, 42013, 42028,
"Husbandry": [42065, 42067, 42078, 42022, 42007, 42079, 42013, 42028,
42036, 42057, 42063, 42053, 42083, 42084, 42091]
}
@@ -482,10 +485,10 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
"Progressive Resource Crafting": ["Iron Ingot", "Iron Ingot", "Block of Iron"]
}
for item_name, item_id in progressive_items.items():
level = min(inventory[item_id], len(progressive_names[item_name])-1)
level = min(inventory[item_id], len(progressive_names[item_name]) - 1)
display_name = progressive_names[item_name][level]
base_name = item_name.split(maxsplit=1)[1].lower().replace(' ', '_')
display_data[base_name+"_url"] = minecraft_icons[display_name]
display_data[base_name + "_url"] = minecraft_icons[display_name]
# Multi-items
multi_items = {
@@ -496,7 +499,7 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
base_name = item_name.split()[-1].lower()
count = inventory[item_id]
if count >= 0:
display_data[base_name+"_count"] = count
display_data[base_name + "_count"] = count
# Victory condition
game_state = multisave.get("client_game_state", {}).get((tracked_team, tracked_player), 0)
@@ -505,26 +508,214 @@ def getPlayerTracker(tracker: UUID, tracked_team: int, tracked_player: int):
# Turn location IDs into advancement tab counts
checked_locations = multisave.get("location_checks", {}).get((tracked_team, tracked_player), set())
lookup_name = lambda id: lookup_any_location_id_to_name[id]
location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations}
for tab_name, tab_locations in minecraft_location_ids.items()}
checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations])
for tab_name, tab_locations in minecraft_location_ids.items()}
location_info = {tab_name: {lookup_name(id): (id in checked_locations) for id in tab_locations}
for tab_name, tab_locations in minecraft_location_ids.items()}
checks_done = {tab_name: len([id for id in tab_locations if id in checked_locations])
for tab_name, tab_locations in minecraft_location_ids.items()}
checks_done['Total'] = len(checked_locations)
checks_in_area = {tab_name: len(tab_locations) for tab_name, tab_locations in minecraft_location_ids.items()}
checks_in_area['Total'] = sum(checks_in_area.values())
return render_template("minecraftTracker.html",
inventory=inventory, icons=minecraft_icons, acquired_items={lookup_any_item_id_to_name[id] for id in inventory if id in lookup_any_item_id_to_name},
return render_template("minecraftTracker.html",
inventory=inventory, icons=minecraft_icons,
acquired_items={lookup_any_item_id_to_name[id] for id in inventory if
id in lookup_any_item_id_to_name},
player=tracked_player, team=tracked_team, room=room, player_name=player_name,
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
**display_data)
elif games[tracked_player] == "Ocarina of Time":
oot_icons = {
"Fairy Ocarina": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/OoT_Fairy_Ocarina_Icon.png",
"Ocarina of Time": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Ocarina_of_Time_Icon.png",
"Slingshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/32/OoT_Fairy_Slingshot_Icon.png",
"Boomerang": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/d/d5/OoT_Boomerang_Icon.png",
"Bottle": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/fc/OoT_Bottle_Icon.png",
"Rutos Letter": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/OoT_Letter_Icon.png",
"Bombs": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/11/OoT_Bomb_Icon.png",
"Bombchus": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/36/OoT_Bombchu_Icon.png",
"Lens of Truth": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/05/OoT_Lens_of_Truth_Icon.png",
"Bow": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/9a/OoT_Fairy_Bow_Icon.png",
"Hookshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/77/OoT_Hookshot_Icon.png",
"Longshot": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/a/a4/OoT_Longshot_Icon.png",
"Megaton Hammer": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/93/OoT_Megaton_Hammer_Icon.png",
"Fire Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/1e/OoT_Fire_Arrow_Icon.png",
"Ice Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/3c/OoT_Ice_Arrow_Icon.png",
"Light Arrows": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/76/OoT_Light_Arrow_Icon.png",
"Dins Fire": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/d/da/OoT_Din%27s_Fire_Icon.png",
"Farores Wind": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/7/7a/OoT_Farore%27s_Wind_Icon.png",
"Nayrus Love": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/be/OoT_Nayru%27s_Love_Icon.png",
"Kokiri Sword": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/5/53/OoT_Kokiri_Sword_Icon.png",
"Biggoron Sword": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/2e/OoT_Giant%27s_Knife_Icon.png",
"Mirror Shield": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b0/OoT_Mirror_Shield_Icon_2.png",
"Goron Bracelet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b7/OoT_Goron%27s_Bracelet_Icon.png",
"Silver Gauntlets": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/b/b9/OoT_Silver_Gauntlets_Icon.png",
"Golden Gauntlets": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/6/6a/OoT_Golden_Gauntlets_Icon.png",
"Goron Tunic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/1/1c/OoT_Goron_Tunic_Icon.png",
"Zora Tunic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/2c/OoT_Zora_Tunic_Icon.png",
"Silver Scale": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Silver_Scale_Icon.png",
"Gold Scale": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/95/OoT_Golden_Scale_Icon.png",
"Iron Boots": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/34/OoT_Iron_Boots_Icon.png",
"Hover Boots": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/22/OoT_Hover_Boots_Icon.png",
"Adults Wallet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/f9/OoT_Adult%27s_Wallet_Icon.png",
"Giants Wallet": r"https://static.wikia.nocookie.net/zelda_gamepedia_en/images/8/87/OoT_Giant%27s_Wallet_Icon.png",
"Small Magic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/9f/OoT3D_Magic_Jar_Icon.png",
"Large Magic": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/3/3e/OoT3D_Large_Magic_Jar_Icon.png",
"Gerudo Membership Card": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/4e/OoT_Gerudo_Token_Icon.png",
"Gold Skulltula Token": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/47/OoT_Token_Icon.png",
"Triforce Piece": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/0b/SS_Triforce_Piece_Icon.png",
"Triforce": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/6/68/ALttP_Triforce_Title_Sprite.png",
"Zeldas Lullaby": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
"Eponas Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
"Sarias Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
"Suns Song": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
"Song of Time": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
"Song of Storms": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/2/21/Grey_Note.png",
"Minuet of Forest": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/e/e4/Green_Note.png",
"Bolero of Fire": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/f/f0/Red_Note.png",
"Serenade of Water": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/0/0f/Blue_Note.png",
"Requiem of Spirit": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/a/a4/Orange_Note.png",
"Nocturne of Shadow": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/97/Purple_Note.png",
"Prelude of Light": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/9/90/Yellow_Note.png",
"Small Key": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/e/e5/OoT_Small_Key_Icon.png",
"Boss Key": "https://static.wikia.nocookie.net/zelda_gamepedia_en/images/4/40/OoT_Boss_Key_Icon.png",
}
display_data = {}
# Determine display for progressive items
progressive_items = {
"Progressive Hookshot": 66128,
"Progressive Strength Upgrade": 66129,
"Progressive Wallet": 66133,
"Progressive Scale": 66134,
"Magic Meter": 66138,
"Ocarina": 66139,
}
progressive_names = {
"Progressive Hookshot": ["Hookshot", "Hookshot", "Longshot"],
"Progressive Strength Upgrade": ["Goron Bracelet", "Goron Bracelet", "Silver Gauntlets", "Golden Gauntlets"],
"Progressive Wallet": ["Adults Wallet", "Adults Wallet", "Giants Wallet", "Giants Wallet"],
"Progressive Scale": ["Silver Scale", "Silver Scale", "Gold Scale"],
"Magic Meter": ["Small Magic", "Small Magic", "Large Magic"],
"Ocarina": ["Fairy Ocarina", "Fairy Ocarina", "Ocarina of Time"]
}
for item_name, item_id in progressive_items.items():
level = min(inventory[item_id], len(progressive_names[item_name])-1)
display_name = progressive_names[item_name][level]
if item_name.startswith("Progressive"):
base_name = item_name.split(maxsplit=1)[1].lower().replace(' ', '_')
else:
base_name = item_name.lower().replace(' ', '_')
display_data[base_name+"_url"] = oot_icons[display_name]
if base_name == "hookshot":
display_data['hookshot_length'] = {0: '', 1: 'H', 2: 'L'}.get(level)
if base_name == "wallet":
display_data['wallet_size'] = {0: '99', 1: '200', 2: '500', 3: '999'}.get(level)
# Determine display for bottles. Show letter if it's obtained, determine bottle count
bottle_ids = [66015, 66020, 66021, 66140, 66141, 66142, 66143, 66144, 66145, 66146, 66147, 66148]
display_data['bottle_count'] = min(sum(map(lambda item_id: inventory[item_id], bottle_ids)), 4)
display_data['bottle_url'] = oot_icons['Rutos Letter'] if inventory[66021] > 0 else oot_icons['Bottle']
# Determine bombchu display
display_data['has_bombchus'] = any(map(lambda item_id: inventory[item_id] > 0, [66003, 66106, 66107, 66137]))
# Multi-items
multi_items = {
"Gold Skulltula Token": 66091,
"Triforce Piece": 66202,
}
for item_name, item_id in multi_items.items():
base_name = item_name.split()[-1].lower()
count = inventory[item_id]
display_data[base_name+"_count"] = inventory[item_id]
# Gather dungeon locations
area_id_ranges = {
"Overworld": (67000, 67280),
"Deku Tree": (67281, 67303),
"Dodongo's Cavern": (67304, 67334),
"Jabu Jabu's Belly": (67335, 67359),
"Bottom of the Well": (67360, 67384),
"Forest Temple": (67385, 67420),
"Fire Temple": (67421, 67457),
"Water Temple": (67458, 67484),
"Shadow Temple": (67485, 67532),
"Spirit Temple": (67533, 67582),
"Ice Cavern": (67583, 67596),
"Gerudo Training Grounds": (67597, 67635),
"Ganon's Castle": (67636, 67673),
}
def lookup_and_trim(id, area):
full_name = lookup_any_location_id_to_name[id]
if id == 67673:
return full_name[13:] # Ganons Tower Boss Key Chest
if area != 'Overworld':
return full_name[len(area):] # trim dungeon name. leaves an extra space that doesn't display, or trims fully for DC/Jabu/GC
return full_name
checked_locations = multisave.get("location_checks", {}).get((tracked_team, tracked_player), set()).intersection(set(locations[tracked_player]))
location_info = {area: {lookup_and_trim(id, area): id in checked_locations for id in range(min_id, max_id+1) if id in locations[tracked_player]}
for area, (min_id, max_id) in area_id_ranges.items()}
checks_done = {area: len(list(filter(lambda x: x, location_info[area].values()))) for area in area_id_ranges}
checks_in_area = {area: len([id for id in range(min_id, max_id+1) if id in locations[tracked_player]])
for area, (min_id, max_id) in area_id_ranges.items()}
checks_done['Total'] = sum(checks_done.values())
checks_in_area['Total'] = sum(checks_in_area.values())
# Give skulltulas on non-tracked locations
non_tracked_locations = multisave.get("location_checks", {}).get((tracked_team, tracked_player), set()).difference(set(locations[tracked_player]))
for id in non_tracked_locations:
if "GS" in lookup_and_trim(id, ''):
display_data["token_count"] += 1
# Gather small and boss key info
small_key_counts = {
"Forest Temple": inventory[66175],
"Fire Temple": inventory[66176],
"Water Temple": inventory[66177],
"Spirit Temple": inventory[66178],
"Shadow Temple": inventory[66179],
"Bottom of the Well": inventory[66180],
"Gerudo Training Grounds": inventory[66181],
"Ganon's Castle": inventory[66183],
}
boss_key_counts = {
"Forest Temple": '' if inventory[66149] else '',
"Fire Temple": '' if inventory[66150] else '',
"Water Temple": '' if inventory[66151] else '',
"Spirit Temple": '' if inventory[66152] else '',
"Shadow Temple": '' if inventory[66153] else '',
"Ganon's Castle": '' if inventory[66154] else '',
}
# Victory condition
game_state = multisave.get("client_game_state", {}).get((tracked_team, tracked_player), 0)
display_data['game_finished'] = game_state == 30
return render_template("ootTracker.html",
inventory=inventory, player=tracked_player, team=tracked_team, room=room, player_name=player_name,
icons=oot_icons, acquired_items={lookup_any_item_id_to_name[id] for id in inventory},
checks_done=checks_done, checks_in_area=checks_in_area, location_info=location_info,
small_key_counts=small_key_counts, boss_key_counts=boss_key_counts,
**display_data)
else:
checked_locations = multisave.get("location_checks", {}).get((tracked_team, tracked_player), set())
player_received_items = {}
for order_index, networkItem in enumerate(
multisave.get('received_items', {}).get((tracked_team, tracked_player), []),
start=1
):
player_received_items[networkItem.item] = order_index
return render_template("genericTracker.html",
inventory=inventory,
player=tracked_player, team=tracked_team, room=room, player_name=player_name,
checked_locations= checked_locations, not_checked_locations = set(locations[tracked_player])-checked_locations)
checked_locations=checked_locations,
not_checked_locations=set(locations[tracked_player]) - checked_locations,
received_items=player_received_items)
@app.route('/tracker/<suuid:tracker>')
@@ -602,4 +793,4 @@ def getTracker(tracker: UUID):
checks_in_area=seed_checks_in_area, activity_timers=activity_timers,
key_locations=group_key_locations, small_key_ids=small_key_ids, big_key_ids=big_key_ids,
video=video, big_key_locations=group_big_key_locations,
hints=hints, long_player_names = long_player_names)
hints=hints, long_player_names=long_player_names)

View File

@@ -3,6 +3,7 @@ import lzma
import json
import base64
import MultiServer
import uuid
from flask import request, flash, redirect, url_for, session, render_template
from pony.orm import flush, select
@@ -17,6 +18,68 @@ accepted_zip_contents = {"patches": ".apbp",
banned_zip_contents = (".sfc",)
def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, sid=None):
if not owner:
owner = session["_id"]
infolist = zfile.infolist()
slots = set()
spoiler = ""
multidata = None
for file in infolist:
if file.filename.endswith(banned_zip_contents):
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. " \
"Your file was deleted."
elif file.filename.endswith(".apbp"):
data = zfile.open(file, "r").read()
yaml_data = parse_yaml(lzma.decompress(data).decode("utf-8-sig"))
if yaml_data["version"] < 2:
return "Old format cannot be uploaded (outdated .apbp)", 500
metadata = yaml_data["meta"]
slots.add(Slot(data=data, player_name=metadata["player_name"],
player_id=metadata["player_id"],
game="A Link to the Past"))
elif file.filename.endswith(".apmc"):
data = zfile.open(file, "r").read()
metadata = json.loads(base64.b64decode(data).decode("utf-8"))
slots.add(Slot(data=data, player_name=metadata["player_name"],
player_id=metadata["player_id"],
game="Minecraft"))
elif file.filename.endswith(".zip"):
# Factorio mods need a specific name or they do not function
_, seed_name, slot_id, slot_name = file.filename.rsplit("_", 1)[0].split("-")
slots.add(Slot(data=zfile.open(file, "r").read(), player_name=slot_name,
player_id=int(slot_id[1:]), game="Factorio"))
elif file.filename.endswith(".apz5"):
# .apz5 must be named specifically since they don't contain any metadata
_, seed_name, slot_id, slot_name = file.filename.split('.')[0].split('_', 3)
slots.add(Slot(data=zfile.open(file, "r").read(), player_name=slot_name,
player_id=int(slot_id[1:]), game="Ocarina of Time"))
elif file.filename.endswith(".txt"):
spoiler = zfile.open(file, "r").read().decode("utf-8-sig")
elif file.filename.endswith(".archipelago"):
try:
multidata = zfile.open(file).read()
MultiServer.Context._decompress(multidata)
except:
flash("Could not load multidata. File may be corrupted or incompatible.")
else:
multidata = zfile.open(file).read()
if multidata:
flush() # commit slots
seed = Seed(multidata=multidata, spoiler=spoiler, slots=slots, owner=owner, meta=json.dumps(meta),
id=sid if sid else uuid.uuid4())
flush() # create seed
for slot in slots:
slot.seed = seed
return seed
else:
flash("No multidata was found in the zip file, which is required.")
@app.route('/uploads', methods=['GET', 'POST'])
def uploads():
if request.method == 'POST':
@@ -31,64 +94,12 @@ def uploads():
flash('No selected file')
elif file and allowed_file(file.filename):
if file.filename.endswith(".zip"):
slots = set()
spoiler = ""
multidata = None
with zipfile.ZipFile(file, 'r') as zfile:
infolist = zfile.infolist()
for file in infolist:
if file.filename.endswith(banned_zip_contents):
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. Your file was deleted."
elif file.filename.endswith(".apbp"):
data = zfile.open(file, "r").read()
yaml_data = parse_yaml(lzma.decompress(data).decode("utf-8-sig"))
if yaml_data["version"] < 2:
return "Old format cannot be uploaded (outdated .apbp)", 500
metadata = yaml_data["meta"]
slots.add(Slot(data=data, player_name=metadata["player_name"],
player_id=metadata["player_id"],
game="A Link to the Past"))
elif file.filename.endswith(".apmc"):
data = zfile.open(file, "r").read()
metadata = json.loads(base64.b64decode(data).decode("utf-8"))
slots.add(Slot(data=data, player_name=metadata["player_name"],
player_id=metadata["player_id"],
game="Minecraft"))
elif file.filename.endswith(".zip"):
# Factorio mods needs a specific name or they do not function
_, seed_name, slot_id, slot_name = file.filename.rsplit("_", 1)[0].split("-")
slots.add(Slot(data=zfile.open(file, "r").read(), player_name=slot_name,
player_id=int(slot_id[1:]), game="Factorio"))
elif file.filename.endswith(".apz5"):
# .apz5 must be named specifically since they don't contain any metadata
_, seed_name, slot_id, slot_name = file.filename.split('.')[0].split('_', 3)
slots.add(Slot(data=zfile.open(file, "r").read(), player_name=slot_name,
player_id=int(slot_id[1:]), game="Ocarina of Time"))
elif file.filename.endswith(".txt"):
spoiler = zfile.open(file, "r").read().decode("utf-8-sig")
elif file.filename.endswith(".archipelago"):
try:
multidata = zfile.open(file).read()
MultiServer.Context._decompress(multidata)
except:
flash("Could not load multidata. File may be corrupted or incompatible.")
else:
multidata = zfile.open(file).read()
if multidata:
flush() # commit slots
seed = Seed(multidata=multidata, spoiler=spoiler, slots=slots, owner=session["_id"])
flush() # create seed
for slot in slots:
slot.seed = seed
return redirect(url_for("viewSeed", seed=seed.id))
else:
flash("No multidata was found in the zip file, which is required.")
res = upload_zip_to_db(zfile)
if type(res) == str:
return res
elif res:
return redirect(url_for("viewSeed", seed=res.id))
else:
try:
multidata = file.read()

Binary file not shown.

342
docs/adding games.md Normal file
View File

@@ -0,0 +1,342 @@
# How do I add a game to Archipelago?
This guide is going to try and be a broad summary of how you can do just that.
There are two key steps to incorporating a game into Archipelago:
- Game Modification
- Archipelago Server Integration
# Game Modification
One half of the work required to integrate a game into Archipelago is the development of the game client. This is
typically done through a modding API or other modification process, described further down.
As an example, modifications to a game typically include (more on this later):
- Hooking into when a 'location check' is completed.
- Networking with the Archipelago server.
- Optionally, UI or HUD updates to show status of the multiworld session or Archipelago server connection.
In order to determine how to modify a game, refer to the following sections.
## Engine Identification
This is a good way to make the modding process much easier. Being able to identify what engine a game was made in is critical. The first step is to look at a game's files. Let's go over what some game files might look like. Its important that you be able to see file extensions, so be sure to enable that feature in your file viewer of choice.
Examples are provided below.
### Creepy Castle
![Creepy Castle Root Directory in Window's Explorer](./img/creepy-castle-directory.png)
This is the delightful title Creepy Castle, which is a fantastic game that I highly recommend. Its also your worst-case
scenario as a modder. All thats present here is an executable file and some meta-information that Steam uses. You have
basically nothing here to work with. If you want to change this game, the only option you have is to do some pretty nasty
disassembly and reverse engineering work, which is outside the scope of this tutorial. Lets look at some other examples
of game releases.
### Heavy Bullets
![Heavy Bullets Root Directory in Window's Explorer](./img/heavy-bullets-directory.png)
Heres the release files for another game, Heavy Bullets. We see a .exe file, like expected, and a few more files.
“hello.txt” is a text file, which we can quickly skim in any text editor. Many games have them in some form, usually
with a name like README.txt, and they may contain information about a game, such as a EULA, terms of service, licensing
information, credits, and general info about the game. You usually wont find anything too helpful here, but it never
hurts to check. In this case, it contains some credits and a changelog for the game, so nothing too important.
“steam_api.dll” is a file you can safely ignore, its just some code used to interface with Steam.
The directory “HEAVY_BULLETS_Data”, however, has some good news.
![Heavy Bullets Data Directory in Window's Explorer](./img/heavy-bullets-data-directory.png)
Jackpot! It might not be obvious what youre looking at here, but I can instantly tell from this folders contents that
what we have is a game made in the Unity Engine. If you look in the sub-folders, youll seem some .dll files which affirm
our suspicions. Telltale signs for this are directories titled “Managed” and “Mono”, as well as the numbered, extension-less
level files and the sharedassets files. Well tell you a bit about why seeing a Unity game is such good news later,
but for now, this is what one looks like. Also keep your eyes out for an executable with a name like UnityCrashHandler,
thats another dead giveaway.
### Stardew Valley
![Stardew Valley Root Directory in Window's Explorer](./img/stardew-valley-directory.png)
This is the game contents of Stardew Valley. A lot more to look at here, but some key takeaways.
Notice the .dll files which include “CSharp” in their name. This tells us that the game was made in C#, which is good news.
More on that later.
### Gato Roboto
![Gato Roboto Root Directory in Window's Explorer](./img/gato-roboto-directory.png)
Our last example is the game Gato Roboto. This game is made in GameMaker, which is another green flag to look out for.
The giveaway is the file titled "data.win". This immediately tips us off that this game was made in GameMaker.
This isn't all you'll ever see looking at game files, but it's a good place to start.
As a general rule, the more files a game has out in plain sight, the more you'll be able to change.
This especially applies in the case of code or script files - always keep a lookout for anything you can use to your
advantage!
## Open or Leaked Source Games
As a side note, many games have either been made open source, or have had source files leaked at some point.
This can be a boon to any would-be modder, for obvious reasons.
Always be sure to check - a quick internet search for "(Game) Source Code" might not give results often, but when it
does you're going to have a much better time.
Be sure never to distribute source code for games that you decompile or find if you do not have express permission to do
so, or to redistribute any materials obtained through similar methods, as this is illegal and unethical.
## Modifying Release Versions of Games
However, for now we'll assume you haven't been so lucky, and have to work with only whats sitting in your install directory.
Some developers are kind enough to deliberately leave you ways to alter their games, like modding tools,
but these are often not geared to the kind of work you'll be doing and may not help much.
As a general rule, any modding tool that lets you write actual code is something worth using.
### Research
The first step is to research your game. Even if you've been dealt the worst hand in terms of engine modification,
it's possible other motivated parties have concocted useful tools for your game already.
Always be sure to search the Internet for the efforts of other modders.
### Analysis Tools
Depending on the games underlying engine, there may be some tools you can use either in lieu of or in addition to existing game tools.
#### [dnSpy](https://github.com/dnSpy/dnSpy/releases)
The first tool in your toolbox is dnSpy.
dnSpy is useful for opening and modifying code files, like .exe and .dll files, that were made in C#.
This won't work for executable files made by other means, and obfuscated code (code which was deliberately made
difficult to reverse engineer) will thwart it, but 9 times out of 10 this is exactly what you need.
You'll want to avoid opening common library files in dnSpy, as these are unlikely to contain the data you're looking to
modify.
For Unity games, the file youll want to open will be the file (Data Folder)/Managed/Assembly-CSharp.dll, as pictured below:
![Heavy Bullets Managed Directory in Window's Explorer](./img/heavy-bullets-managed-directory.png)
This file will contain the data of the actual game.
For other C# games, the file you want is usually just the executable itself.
With dnSpy, you can view the games C# code, but the tool isnt perfect.
Although the names of classes, methods, variables, and more will be preserved, code structures may not remain entirely intact. This is because compilers will often subtly rewrite code to be more optimal, so that it works the same as the original code but uses fewer resources. Compiled C# files also lose comments and other documentation.
#### [UndertaleModTool](https://github.com/krzys-h/UndertaleModTool/releases)
This is currently the best tool for modifying games made in GameMaker, and supports games made in both GMS 1 and 2.
It allows you to modify code in GML, if the game wasn't made with the wrong compiler (usually something you don't have
to worry about).
You'll want to open the data.win file, as this is where all the goods are kept.
Like dnSpy, you wont be able to see comments.
In addition, you will be able to see and modify many hidden fields on items that GameMaker itself will often hide from
creators.
Fonts in particular are notoriously complex, and to add new sprites you may need to modify existing sprite sheets.
#### [CheatEngine](https://cheatengine.org/)
CheatEngine is a tool with a very long and storied history.
Be warned that because it performs live modifications to the memory of other processes, it will likely be flagged as
malware (because this behavior is most commonly found in malware and rarely used by other programs).
If you use CheatEngine, you need to have a deep understanding of how computers work at the nuts and bolts level,
including binary data formats, addressing, and assembly language programming.
The tool itself is highly complex and even I have not yet charted its expanses.
However, it can also be a very powerful tool in the right hands, allowing you to query and modify gamestate without ever
modifying the actual game itself.
In theory it is compatible with any piece of software you can run on your computer, but there is no "easy way" to do
anything with it.
### What Modifications You Should Make to the Game
We talked about this briefly in [Game Modification](#game-modification) section.
The next step is to know what you need to make the game do now that you can modify it. Here are your key goals:
- Modify the game so that checks are shuffled
- Know when the player has completed a check, and react accordingly
- Listen for messages from the Archipelago server
- Modify the game to display messages from the Archipelago server
- Add interface for connecting to the Archipelago server with passwords and sessions
- Add commands for manually rewarding, re-syncing, forfeiting, and other actions
To elaborate, you need to be able to inform the server whenever you check locations, print out messages that you receive
from the server in-game so players can read them, award items when the server tells you to, sync and re-sync when necessary,
avoid double-awarding items while still maintaining game file integrity, and allow players to manually enter commands in
case the client or server make mistakes.
Refer to the [Network Protocol documentation](./network%20protocol.md) for how to communicate with Archipelago's servers.
## But my Game is a console game. Can I still add it?
That depends what console?
### My Game is a recent game for the PS4/Xbox-One/Nintendo Switch/etc
Most games for recent generations of console platforms are inaccessible to the typical modder. It is generally advised
that you do not attempt to work with these games as they are difficult to modify and are protected by their copyright
holders. Most modern AAA game studios will provide a modding interface or otherwise deny modifications for their console games.
### My Game isnt that old, its for the Wii/PS2/360/etc
This is very complex, but doable.
If you don't have good knowledge of stuff like Assembly programming, this is not where you want to learn it.
There exist many disassembly and debugging tools, but more recent content may have lackluster support.
### My Game is a classic for the SNES/Sega Genesis/etc
Thats a lot more feasible.
There are many good tools available for understanding and modifying games on these older consoles, and the emulation
community will have figured out the bulk of the consoles secrets.
Look for debugging tools, but be ready to learn assembly.
Old consoles usually have their own unique dialects of ASM youll need to get used to.
Also make sure theres a good way to interface with a running emulator, since thats the only way you can connect these
older consoles to the Internet.
There are also hardware mods and flash carts, which can do the same things an emulator would when connected to a computer,
but these will require the same sort of interface software to be written in order to work properly - from your perspective
the two won't really look any different.
### My Game is an exclusive for the Super Baby Magic Dream Boy. Its this console from the Soviet Union that-
Unless you have a circuit schematic for the Super Baby Magic Dream Boy sitting on your desk, no.
Obscurity is your enemy there will likely be little to no emulator or modding information, and youd essentially be
working from scratch.
## How to Distribute Game Modifications
**NEVER EVER distribute anyone else's copyrighted work UNLESS THEY EXPLICITLY GIVE YOU PERMISSION TO DO SO!!!**
This is a good way to get any project you're working on sued out from under you.
The right way to distribute modified versions of a game's binaries, assuming that the licensing terms do not allow you
to copy them wholesale, is as patches.
There are many patch formats, which I'll cover in brief. The common theme is that you cant distribute anything that wasn't
made by you. Patches are files that describe how your modified file differs from the original one, thus avoiding the
issue of distributing someone elses original work.
Users who have a copy of the game just need to apply the patch, and those who dont are unable to play.
### IPS Patches
This is an extremely simple, early patch format, but is limited to games of about 16 Megabytes in size or less.
You will often find IPS patches being used to distribute mods for old video game ROMs.
IPS patches are a delta patch format, which means they act only as a simple list of alterations that need to be made to
an original file in order to produce a new one.
Archipelago may use pre-made IPS patches to apply specific changes to a game, but will not create IPS patches as a means
of distributing game modifications. Although IPS patches can be applied quickly, creating them is quite slow, so using
them for distributing randomized games is not current practice.
However, due to the format's simplicity, even patch files of this type can unintentionally include copyrighted data.
This is because IPS patches don't have a good way to shift existing data in a file, and thus if data has to be moved
forward x number of bytes, which might be necessary for data insertion, the patch will simply include a copy of the
shifted bytes after the inserted ones.
Increasing and decreasing file size is also not a universally supported operation, due to the patch format's age.
### BPS Patches
BPS is the younger cousin of the IPS patch.
More flexible and theoretically future-proofed for any file size, BPS patches are based on the idea of linear patching.
Unlike IPS patches, which use a system called delta patching, linear patches act as a series of steps for creating a
modified file from scratch through a combination of original data and patch data, which is appended onto the end of the
modified game file as the patch progresses.
This means that some operations, like inserting data into the middle of a file instead of simply overwriting it,
are much easier to do.
However, like IPS, it isn't a format well suited to randomizers, due to the asymmetric costs of creating and applying
BPS patches.
### Xdelta Patches
Xdelta is the true successor to IPS, featuring better optimization and verification, and manages to transcend many of
the limitations of IPS. However, Xdelta patches are particularly expensive to create.
### bsdiff
bsdiff is the current format adopted by Archipelago for creating and distributing patches.
It is much faster to create patches of this variety, which is why it sees use in this application.
### Mod files
Games which support modding will usually just let you drag and drop the mods files into a folder somewhere.
Mod files come in many forms, but the rules about not distributing other people's content remain the same.
## Archipelago Integration
Integrating a randomizer into Archipelago involves a few steps.
There are several things that may need to be done, but the most important is to create an implementation of the
`World` class specific to your game. This implementation should exist as a Python module within the `worlds` folder
in the Archipelago file structure.
This encompasses most of the data for your game the items available, what checks you have, the logic for reaching those
checks, what options to offer for the players yaml file, and the code to initialize all this data.
Heres an example of what your world module can look like:
![Example world module directory open in Window's Explorer](./img/archipelago-world-directory-example.png)
Let's give a quick breakdown of what the contents for these files look like.
This is just one example of an Archipelago world - the way things are done below is not an immutable property of Archipelago.
### Items.py
This file is used to define the items which exist in a given game.
![Example Items.py file open in Notepad++](./img/example-items-py-file.png)
Some important things to note here. The center of our Items.py file is the item_table, which individually lists every
item in the game and associates them with an ItemData.
This file is rather skeletal - most of the actual data has been stripped out for simplicity.
Each ItemData gives a numeric ID to associate with the item and a boolean telling us whether the item might allow the
player to do more than they would have been able to before.
Next there's the item_frequencies. This simply tells Archipelago how many times each item appears in the pool.
Items that appear exactly once need not be listed - Archipelago will interpret absence from this dictionary as meaning
that the item appears once.
Lastly, note the `lookup_id_to_name` dictionary, which is typically imported and used in your Archipelago `World`
implementation. This is how Archipelago is told about the items in your world.
### Locations.py
This file lists all locations in the game.
![Example Locations.py file open in Notepad++](./img/example-locations-py-file.png)
First is the achievement_table. It lists each location, the region that it can be found in (more on regions later),
and a numeric ID to associate with each location.
The exclusion table is a series of dictionaries which are used to exclude certain checks from the pool of progression
locations based on user settings, and the events table associates certain specific checks with specific items.
`lookup_id_to_name` is also present for locations, though this is a separate dictionary, to be clear.
### Options.py
This file details options to be searched for in a player's YAML settings file.
![Example Options.py file open in Notepad++](./img/example-options-py-file.png)
There are several types of option Archipelago has support for.
In our case, we have three separate choices a player can toggle, either On or Off.
You can also have players choose between a number of predefined values, or have them provide a numeric value within a
specified range.
### Regions.py
This file contains data which defines the world's topology.
In other words, it details how different regions of the game connect to each other.
![Example Regions.py file open in Notepad++](./img/example-regions-py-file.png)
`terraria_regions` contains a list of tuples.
The first element of the tuple is the name of the region, and the second is a list of connections that lead out of the region.
`mandatory_connections` describe where the connection leads.
Above this data is a function called `link_terraria_structures` which uses our defined regions and connections to create
something more usable for Archipelago, but this has been left out for clarity.
### Rules.py
This is the file that details rules for what players can and cannot logically be required to do, based on items and settings.
![Example Rules.py file open in Notepad++](./img/example-rules-py-file.png)
This is the most complicated part of the job, and is one part of Archipelago that is likely to see some changes in the future.
The first class, called `TerrariaLogic`, is an extension of the `LogicMixin` class.
This is where you would want to define methods for evaluating certain conditions, which would then return a boolean to
indicate whether conditions have been met. Your rule definitions should start with some sort of identifier to delineate it
from other games, as all rules are mixed together due to `LogicMixin`. In our case, `_terraria_rule` would be a better name.
The method below, `set_rules()`, is where you would assign these functions as "rules", using lambdas to associate these
functions or combinations of them (or any other code that evaluates to a boolean, in my case just the placeholder `True`)
to certain tasks, like checking locations or using entrances.
### \_\_init\_\_.py
This is the file that actually extends the `World` class, and is where you expose functionality and data to Archipelago.
![Example \_\_init\_\_.py file open in Notepad++](./img/example-init-py-file.png)
This is the most important file for the implementation, and technically the only one you need, but it's best to keep this
file as short as possible and use other script files to do most of the heavy lifting.
If you've done things well, this will just be where you assign everything you set up in the other files to their associated
fields in the class being extended.
This is also a good place to put game-specific quirky behavior that needs to be managed, as it tends to make things a bit
cluttered if you put these things elsewhere.
The various methods and attributes are documented in `/worlds/AutoWorld.py[World]`,
though it is also recommended to look at existing implementations to see how all this works first-hand.
Once you get all that, all that remains to do is test the game and publish your work.

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

View File

@@ -53,8 +53,7 @@ Sent to clients when they connect to an Archipelago server.
| version | NetworkVersion | Object denoting the version of Archipelago which the server is running. See [NetworkVersion](#NetworkVersion) for more details. |
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. Example: `WebHost` |
| password | bool | Denoted whether a password is required to join this room.|
| forfeit_mode | str | `auto`, `enabled`, `disabled`, `auto-enabled` or `goal`. |
| remaining_mode | str | `enabled`, `disabled`, `goal` |
| permissions | dict\[str, Permission\[int\]\] | Mapping of permission name to [Permission](#Permission), keys are: "forfeit" and "remaining". |
| hint_cost | int | The amount of points it costs to receive a hint from the server. |
| location_check_points | int | The amount of hint points you receive per item/location check completed. ||
| players | list\[NetworkPlayer\] | Sent only if the client is properly authenticated (see [Archipelago Connection Handshake](#Archipelago-Connection-Handshake)). Information on the players currently connected to the server. See [NetworkPlayer](#NetworkPlayer) for more details. |
@@ -83,7 +82,13 @@ 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`, `SlotAlreadyTaken`, `IncompatibleVersion`, or `InvalidPassword`. |
| errors | list\[str\] | Optional. When provided, should contain any one of: `InvalidSlot`, `InvalidGame`, `SlotAlreadyTaken`, `IncompatibleVersion`, or `InvalidPassword`. |
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.
### Connected
Sent to clients when the connection handshake is successfully completed.
@@ -213,7 +218,7 @@ Sent to the server to update on the sender's status. Examples include readiness
#### Arguments
| Name | Type | Notes |
| ---- | ---- | ----- |
| status | int | One of [Client States](#Client-States). Follow the link for more information. |
| status | ClientStatus\[int\] | One of [Client States](#Client-States). Send as int. Follow the link for more information. |
### Say
Basic chat command which sends text to the server to be distributed to other clients.
@@ -229,7 +234,7 @@ Requests the data package from the server. Does not require client authenticatio
#### Arguments
| Name | Type | Notes |
| ------ | ----- | ------ |
| exlusions | list[str] | Optional. If specified, will not send back the specified data. Such as, ["Factorio"] -> Datapackage without Factorio data.|
| exclusions | list[str] | Optional. If specified, will not send back the specified data. Such as, ["Factorio"] -> Datapackage without Factorio data.|
### Bounce
Send this message to the server, tell it which clients should receive the message and
@@ -335,7 +340,7 @@ An enumeration containing the possible client states that may be used to inform
```python
import enum
class CLientStatus(enum.IntEnum):
class ClientStatus(enum.IntEnum):
CLIENT_UNKNOWN = 0
CLIENT_READY = 10
CLIENT_PLAYING = 20
@@ -352,6 +357,18 @@ class Version(NamedTuple):
build: int
```
### Permission
An enumeration containing the possible command permission, for commands that may be restricted.
```python
import enum
class Permission(enum.IntEnum):
disabled = 0b000 # 0, completely disables access
enabled = 0b001 # 1, allows manual use
goal = 0b010 # 2, allows manual use after goal completion
auto = 0b110 # 6, forces use after goal completion, only works for forfeit
auto_enabled = 0b111 # 7, forces use after goal completion, allows manual use any time
```
### Data Package Contents
A data package is a JSON object which may contain arbitrary metadata to enable a client to interact with the Archipelago server most easily. Currently, this package is used to send ID to name mappings so that clients need not maintain their own mappings.

File diff suppressed because it is too large Load Diff

BIN
docs/network.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

View File

@@ -27,6 +27,7 @@ server_options:
# "enabled" -> clients can always forfeit
# "auto" -> automatic forfeit on goal completion, "goal" -> clients can forfeit after achieving their goal
# "auto-enabled" -> automatic forfeit on goal completion and manual forfeit is also enabled
# "goal" -> forfeit is allowed after goal completion
forfeit_mode: "goal"
# Remaining modes
# !remaining handling, that tells a client which items remain in their pool

View File

@@ -50,24 +50,29 @@ 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/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
Name: "server"; Description: "Server"; Types: full hosting
Name: "client"; Description: "Clients"; Types: full playing
Name: "client/lttp"; Description: "A Link to the Past"; Types: full playing hosting
Name: "client/lttp"; Description: "A Link to the Past"; Types: full playing
Name: "client/factorio"; Description: "Factorio"; Types: full playing
Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
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/lttp or generator
Source: "{#sourcepath}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, *exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "{code:GetROMPath}"; DestDir: "{app}"; DestName: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"; Flags: external; Components: client/lttp or generator/lttp
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/lttp
Source: "{#sourcepath}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator
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}\ArchipelagoLttPClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/lttp
Source: "{#sourcepath}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/lttp
Source: "{#sourcepath}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft
@@ -79,6 +84,7 @@ Source: "{tmp}\forge-installer.jar"; DestDir: "{app}"; Flags: skipifsourcedoesnt
[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/lttp
Name: "{group}\{#MyAppName} LttP Client"; Filename: "{app}\ArchipelagoLttPClient.exe"; Components: client/lttp
Name: "{group}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Components: client/factorio
Name: "{group}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Components: client/minecraft
@@ -186,6 +192,8 @@ end;
var ROMFilePage: TInputFileWizardPage;
var R : longint;
var rom: string;
var ootrom: string;
var OoTROMFilePage: TInputFileWizardPage;
var MinecraftDownloadPage: TDownloadWizardPage;
procedure AddRomPage();
@@ -221,6 +229,36 @@ begin
MinecraftDownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), @OnDownloadMinecraftProgress);
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 (CurPageID = wpReady) and (WizardIsComponentSelected('client/minecraft')) then begin
@@ -253,7 +291,8 @@ begin
end;
procedure InitializeWizard();
begin
begin
AddOoTRomPage();
AddRomPage();
AddMinecraftDownloads();
end;
@@ -263,7 +302,9 @@ function ShouldSkipPage(PageID: Integer): Boolean;
begin
Result := False;
if (assigned(ROMFilePage)) and (PageID = ROMFilePage.ID) then
Result := not (WizardIsComponentSelected('client/lttp') or WizardIsComponentSelected('generator'));
Result := not (WizardIsComponentSelected('client/lttp') or WizardIsComponentSelected('generator/lttp'));
if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then
Result := not (WizardIsComponentSelected('generator/oot'));
end;
function GetROMPath(Param: string): string;
@@ -274,10 +315,26 @@ begin
begin
R := CompareStr(GetMD5OfFile(ROMFilePage.Values[0]), '03a63945398191337e896e5771f77173')
if R <> 0 then
MsgBox('ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
MsgBox('ALttP ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
Result := ROMFilePage.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;

11
kvui.py
View File

@@ -16,6 +16,7 @@ from kivy.lang import Builder
import Utils
from NetUtils import JSONtoTextParser, JSONMessagePart
class GameManager(App):
logging_pairs = [
("Client", "Archipelago"),
@@ -83,6 +84,7 @@ class FactorioManager(GameManager):
]
title = "Archipelago Factorio Client"
class LttPManager(GameManager):
logging_pairs = [
("Client", "Archipelago"),
@@ -90,6 +92,14 @@ class LttPManager(GameManager):
]
title = "Archipelago LttP Client"
class TextManager(GameManager):
logging_pairs = [
("Client", "Archipelago")
]
title = "Archipelago Text Client"
class LogtoUI(logging.Handler):
def __init__(self, on_log):
super(LogtoUI, self).__init__(logging.DEBUG)
@@ -150,4 +160,5 @@ class KivyJSONtoTextParser(JSONtoTextParser):
ExceptionManager.add_handler(E())
Config.set("input", "mouse", "mouse,disable_multitouch")
Config.set('kivy', 'exit_on_escape', '0')
Builder.load_file(Utils.local_path("data", "client.kv"))

View File

@@ -29,8 +29,9 @@ game: # Pick a game to play
Minecraft: 0
Subnautica: 0
Slay the Spire: 0
Ocarina of Time: 0
requires:
version: 0.1.6 # Version of Archipelago required for this yaml to work as expected.
version: 0.1.7 # 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
@@ -722,13 +723,20 @@ Ocarina of Time:
triforce_hunt: # Gather pieces of the Triforce scattered around the world to complete the game.
false: 50
true: 0
triforce_goal: # Number of Triforce pieces required to complete the game. Total number placed determined by the Item Pool setting.
triforce_goal: # Number of Triforce pieces required to complete the game.
# you can add additional values between minimum and maximum
1: 0 # minimum value
50: 0 # maximum value
random: 50
random-low: 0
random-high: 0
extra_triforce_percentage: # Percentage of additional Triforce pieces in the pool, separate from the item pool setting.
# you can add additional values between minimum and maximum
0: 0 # minimum value
100: 0 # maximum value
random: 50
random-low: 0
random-high: 0
bombchus_in_logic: # Bombchus are properly considered in logic. The first found pack will have 20 chus; Kokiri Shop and Bazaar sell refills; bombchus open Bombchu Bowling.
false: 50
true: 0
@@ -836,14 +844,17 @@ Ocarina of Time:
song: 50
dungeon: 0
any: 0
shopsanity: # Randomizes shop contents. Set to "off" to not shuffle shops; "0" shuffles shops but does not allow multiworld items in shops.
shopsanity: # Randomizes shop contents. "fixed_number" randomizes a specific number of items per shop; "random_number" randomizes the value for each shop.
off: 50
"0": 0
"1": 0
"2": 0
"3": 0
"4": 0
random_value: 0
fixed_number: 0
random_number: 0
shop_slots: # Number of items per shop to be randomized into the main itempool. Only active if Shopsanity is set to "fixed_number."
# you can add additional values between minimum and maximum
0: 0 # minimum value
4: 0 # maximum value
random: 50
random-low: 0
random-high: 0
tokensanity: # Token rewards from Gold Skulltulas are shuffled into the pool.
off: 50
dungeons: 0
@@ -912,6 +923,9 @@ Ocarina of Time:
random: 50
random-low: 0
random-high: 0
correct_chest_sizes: # Changes chests containing progression into large chests, and nonprogression into small chests.
false: 50
true: 0
hints: # Gossip Stones can give hints about item locations.
none: 0
mask: 0
@@ -1283,8 +1297,26 @@ Ocarina of Time:
harp: 0
grind_organ: 0
flute: 0
logic_tricks:
[]
# Uncomment this section to enable logical tricks for Ocarina of Time.
# Add logic tricks keyed by "nice" name rather than internal name: "Hidden Grottos without Stone of Agony", not "logic_grottos_without_agony"
# The following is the typical set of racing tricks, though you can add or remove them as desired.
# logic_tricks:
# - Fewer Tunic Requirements
# - Hidden Grottos without Stone of Agony
# - Child Deadhand without Kokiri Sword
# - Man on Roof without Hookshot
# - Dodongo's Cavern Spike Trap Room Jump without Hover Boots
# - Hammer Rusted Switches Through Walls
# - Windmill PoH as Adult with Nothing
# - Crater's Bean PoH with Hover Boots
# - Forest Temple East Courtyard Vines with Hookshot
# - Bottom of the Well without Lens of Truth
# - Ganon's Castle without Lens of Truth
# - Gerudo Training Grounds without Lens of Truth
# - Shadow Temple before Invisible Moving Platform without Lens of Truth
# - Shadow Temple beyond Invisible Moving Platform without Lens of Truth
# - Spirit Temple without Lens of Truth
# meta_ignore, linked_options and triggers work for any game
meta_ignore: # Nullify options specified in the meta.yaml file. Adding an option here guarantees it will not occur in your seed, even if the .yaml file specifies it

View File

@@ -1,5 +1,5 @@
colorama>=0.4.4
websockets>=9.1
websockets>=10.0
PyYAML>=5.4.1
fuzzywuzzy>=0.18.0
prompt_toolkit>=3.0.20

View File

@@ -74,6 +74,7 @@ scripts = {
# Core
"MultiServer.py": ("ArchipelagoServer", False, icon),
"Generate.py": ("ArchipelagoGenerate", False, icon),
"CommonClient.py": ("ArchipelagoTextClient", True, icon),
# LttP
"LttPClient.py": ("ArchipelagoLttPClient", True, icon),
"LttPAdjuster.py": ("ArchipelagoLttPAdjuster", True, icon),
@@ -162,6 +163,9 @@ if signtool:
os.system(signtool + os.path.join(buildfolder, exe.target_name))
print(f"Signing SNI")
os.system(signtool + os.path.join(buildfolder, "SNI", "SNI.exe"))
print(f"Signing OoT Utils")
for exe_path in (("Compress", "Compress.exe"), ("Decompress", "Decompress.exe")):
os.system(signtool + os.path.join(buildfolder, "lib", "worlds", "oot", "data", *exe_path))
remove_sprites_from_folder(buildfolder / "data" / "sprites" / "alttpr")

30
test/Reachability.py Normal file
View File

@@ -0,0 +1,30 @@
import unittest
from argparse import Namespace
from BaseClasses import MultiWorld
from worlds.AutoWorld import AutoWorldRegister, call_all
class TestBase(unittest.TestCase):
_state_cache = {}
gen_steps = ["generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill"]
def testAllStateCanReachEverything(self):
for game_name, world_type in AutoWorldRegister.world_types.items():
if game_name != "Ori and the Blind Forest": # TODO: fix Ori Logic
with self.subTest("Game", game=game_name):
world = MultiWorld(1)
world.game[1] = game_name
world.player_name = {1: "Tester"}
world.set_seed()
args = Namespace()
for name, option in world_type.options.items():
setattr(args, name, {1: option.from_any(option.default)})
world.set_options(args)
world.set_default_common_options()
for step in self.gen_steps:
call_all(world, step)
state = world.get_all_state(False)
for location in world.get_locations():
with self.subTest("Location should be reached", location=location):
self.assertTrue(location.can_reach(state))

View File

@@ -1,38 +0,0 @@
from test.vanilla.TestVanilla import TestVanilla
class TestDeathMountain(TestVanilla):
def testWestDeathMountain(self):
self.run_location_tests([
["Ether Tablet", False, []],
["Ether Tablet", False, [], ['Progressive Glove', 'Flute']],
["Ether Tablet", False, [], ['Lamp', 'Flute']],
["Ether Tablet", False, [], ['Magic Mirror', 'Hookshot']],
["Ether Tablet", False, [], ['Magic Mirror', 'Hammer']],
["Ether Tablet", False, ['Progressive Sword'], ['Progressive Sword']],
["Ether Tablet", False, [], ['Book of Mudora']],
["Ether Tablet", True, ['Flute', 'Magic Mirror', 'Book of Mudora', 'Progressive Sword', 'Progressive Sword']],
["Ether Tablet", True, ['Progressive Glove', 'Lamp', 'Magic Mirror', 'Book of Mudora', 'Progressive Sword', 'Progressive Sword']],
["Ether Tablet", True, ['Flute', 'Hammer', 'Hookshot', 'Book of Mudora', 'Progressive Sword', 'Progressive Sword']],
["Ether Tablet", True, ['Progressive Glove', 'Lamp', 'Hammer', 'Hookshot', 'Book of Mudora', 'Progressive Sword', 'Progressive Sword']],
["Old Man", False, []],
["Old Man", False, [], ['Progressive Glove', 'Flute']],
["Old Man", False, [], ['Lamp']],
["Old Man", True, ['Flute', 'Lamp']],
["Old Man", True, ['Progressive Glove', 'Lamp']],
["Spectacle Rock Cave", False, []],
["Spectacle Rock Cave", False, [], ['Progressive Glove', 'Flute']],
["Spectacle Rock Cave", False, [], ['Lamp', 'Flute']],
["Spectacle Rock Cave", True, ['Flute']],
["Spectacle Rock Cave", True, ['Progressive Glove', 'Lamp']],
["Spectacle Rock", False, []],
["Spectacle Rock", False, [], ['Progressive Glove', 'Flute']],
["Spectacle Rock", False, [], ['Lamp', 'Flute']],
["Spectacle Rock", False, [], ['Magic Mirror']],
["Spectacle Rock", True, ['Flute', 'Magic Mirror']],
["Spectacle Rock", True, ['Progressive Glove', 'Lamp', 'Magic Mirror']],
])

23
test/TestUniqueness.py Normal file
View File

@@ -0,0 +1,23 @@
import unittest
from BaseClasses import MultiWorld
from worlds.AutoWorld import AutoWorldRegister
class TestBase(unittest.TestCase):
world: MultiWorld
_state_cache = {}
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)

View File

@@ -19,6 +19,7 @@ class TestDungeon(unittest.TestCase):
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args)
self.world.set_default_common_options()
self.starting_regions = [] # Where to start exploring
self.remove_exits = [] # Block dungeon exits
self.world.difficulty_requirements[1] = difficulties['normal']

View File

@@ -19,6 +19,7 @@ class TestInverted(TestBase):
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args)
self.world.set_default_common_options()
self.world.difficulty_requirements[1] = difficulties['normal']
self.world.mode[1] = "inverted"
create_inverted_regions(self.world, 1)

View File

@@ -20,6 +20,7 @@ class TestInvertedBombRules(unittest.TestCase):
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args)
self.world.set_default_common_options()
self.world.difficulty_requirements[1] = difficulties['normal']
create_inverted_regions(self.world, 1)
create_dungeons(self.world, 1)

View File

@@ -20,6 +20,7 @@ class TestInvertedMinor(TestBase):
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args)
self.world.set_default_common_options()
self.world.mode[1] = "inverted"
self.world.logic[1] = "minorglitches"
self.world.difficulty_requirements[1] = difficulties['normal']

View File

@@ -21,6 +21,7 @@ class TestInvertedOWG(TestBase):
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args)
self.world.set_default_common_options()
self.world.logic[1] = "owglitches"
self.world.mode[1] = "inverted"
self.world.difficulty_requirements[1] = difficulties['normal']
@@ -34,7 +35,7 @@ class TestInvertedOWG(TestBase):
self.world.itempool.extend(ItemFactory(['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], 1))
self.world.get_location('Agahnim 1', 1).item = None
self.world.get_location('Agahnim 2', 1).item = None
self.world.precollected_items.clear()
self.world.precollected_items[1].clear()
self.world.itempool.append(ItemFactory('Pegasus Boots', 1))
mark_light_world_regions(self.world, 1)
self.world.worlds[1].set_rules()

View File

@@ -20,6 +20,7 @@ class TestMinor(TestBase):
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args)
self.world.set_default_common_options()
self.world.logic[1] = "minorglitches"
self.world.difficulty_requirements[1] = difficulties['normal']
create_regions(self.world, 1)

View File

@@ -21,6 +21,7 @@ class TestVanillaOWG(TestBase):
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args)
self.world.set_default_common_options()
self.world.difficulty_requirements[1] = difficulties['normal']
self.world.logic[1] = "owglitches"
create_regions(self.world, 1)
@@ -33,7 +34,7 @@ class TestVanillaOWG(TestBase):
self.world.itempool.extend(ItemFactory(['Green Pendant', 'Red Pendant', 'Blue Pendant', 'Beat Agahnim 1', 'Beat Agahnim 2', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 5', 'Crystal 6', 'Crystal 7'], 1))
self.world.get_location('Agahnim 1', 1).item = None
self.world.get_location('Agahnim 2', 1).item = None
self.world.precollected_items.clear()
self.world.precollected_items[1].clear()
self.world.itempool.append(ItemFactory('Pegasus Boots', 1))
mark_dark_world_regions(self.world, 1)
self.world.worlds[1].set_rules()

View File

@@ -19,6 +19,7 @@ class TestVanilla(TestBase):
for name, option in AutoWorld.AutoWorldRegister.world_types["A Link to the Past"].options.items():
setattr(args, name, {1: option.from_any(option.default)})
self.world.set_options(args)
self.world.set_default_common_options()
self.world.logic[1] = "noglitches"
self.world.difficulty_requirements[1] = difficulties['normal']
create_regions(self.world, 1)

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