Compare commits

...

337 Commits
0.1.9 ... 0.2.2

Author SHA1 Message Date
Fabian Dill
97d6e80556 Bump 2021-12-21 15:31:04 +01:00
Fabian Dill
d5abadc6d0 Requirements: remove no longer used appdirs and move kivy to core 2021-12-20 23:10:04 +01:00
Jarno
d08d716966 [Timespinner] Added orb damage rando flag 2021-12-20 14:40:01 +00:00
espeon65536
3ee4be2e33 Minecraft client: more general search for mod name 2021-12-19 19:15:09 +00:00
black-sliver
9172cc4925 SoE: Update to pyevermizer v0.40.0
see https://github.com/black-sliver/pyevermizer/releases/tag/v0.40.0
2021-12-19 15:22:19 +00:00
black-sliver
7f03a86dee SoE: Rename 'chaos' to 'full' in options
* was changed upstream
* also update tooltips to be a bit more helpful
2021-12-19 15:22:19 +00:00
black-sliver
1603bab1da SoE: Rename difficulty 'Chaos' to 'Mystery' 2021-12-19 15:22:19 +00:00
black-sliver
70aae514be SoE: fix macos wheel urls 2021-12-19 15:22:19 +00:00
black-sliver
5fa1185d6d SoE: make doc point to upstream guide.md 2021-12-19 15:22:19 +00:00
Fabian Dill
3a2a584ad3 Factorio: fix singles layout not generating correctly. 2021-12-18 13:05:43 +01:00
Fabian Dill
c42f53d64f Factorio: add some more tech tree shapes 2021-12-18 13:01:30 +01:00
Jarno Westhof
450e0eacf4 TS: Relaxed entry logic for lower caves 2021-12-17 19:50:38 +00:00
Fabian Dill
aa40e811f1 LttPAdjuster: ignore alttpr cert 2021-12-17 19:17:41 +01:00
CaitSith2
af96f71190 Fix bug where there is less locations than hint count. 2021-12-16 15:34:18 -08:00
Jarno Westhof
9e4cb6ee33 TS: Fixed review comments 2021-12-14 16:04:50 +00:00
Jarno Westhof
5d0748983b TS: removed todo list :D 2021-12-14 16:04:50 +00:00
Jarno Westhof
c4981e4b91 TS: Fixed unit test 2021-12-14 16:04:50 +00:00
Jarno Westhof
3f36c436ad TS: putting items as non local will correctly be handled by your starting orbs and your first progression item
excluding locations now correctly works for your first progression item in an non inverted seed
Aura blast can now be your starting spell
2021-12-14 16:04:50 +00:00
Jarno Westhof
db456cbcf1 TS: no longer reward a progression item if you already have one in your starting inventory 2021-12-14 16:04:50 +00:00
Jarno Westhof
c0b8384319 TS: putting non consumable items in starting inventory will now remove them from the pool so a duplicate wont drop 2021-12-14 16:04:50 +00:00
Jarno Westhof
13036539b7 TS: Starting with Jewelrybox, Talaria or Meyef in your starting inventory will now set the corresponding flag 2021-12-14 16:04:50 +00:00
Jarno Westhof
5a2e477dba Added sanity check to see if all locations can be assigned to regions 2021-12-14 16:04:50 +00:00
TauAkiou
f003c7130f [WebHost] Add Super Metroid support to Web Tracker (#153)
* [WebHost]: Added Super Metroid tracker, based on TimeSpinner & OOT
2021-12-14 17:04:24 +01:00
CaitSith2
0558351a12 Allow update_sprites to work on strict text only systems 2021-12-13 20:24:54 +01:00
Fabian Dill
3f20bdaaa2 WebHost: split autolaunch and autogen services 2021-12-13 05:48:33 +01:00
Fabian Dill
3bf367d630 WebHost: don't bother queuing empty commands 2021-12-13 01:38:07 +01:00
alwaysintreble
706fc19ab4 tutorials: place a missing / oops 2021-12-11 17:04:07 +00:00
espeon65536
4fe024041d Minecraft client: update Forge to 1.17.1-37.1.1
This fixes the critical security issue recently found in Minecraft.
2021-12-10 19:43:57 +00:00
Fabian Dill
7afbf8b45b OoTAdjuster: check on subprocess compressor 2021-12-10 09:53:50 +01:00
Fabian Dill
e1fc44f4e0 Clients: compatibility change for old Intel graphics. 2021-12-10 09:29:59 +01:00
jtoyoda
21fbb545e8 Adding in missing comas in ff1 game info 2021-12-08 14:23:01 +00:00
jtoyoda
6cd08ea8dc Updating ff1 gameinfo 2021-12-08 14:23:01 +00:00
Fabian Dill
85efee1432 SM: raise Exception instead of sys.exit for custom presets 2021-12-08 09:27:58 +01:00
Hussein Farran
ba9974fe2a Update README.md 2021-12-04 23:07:35 +00:00
CaitSith2
98a038e39e Death link default true/false values for super metroid. 2021-12-04 14:04:28 -08:00
Fabian Dill
33477202b9 WebHost: remove outdated data 2021-12-04 22:12:09 +01:00
CaitSith2
9c74d648f8 Tie the need for satellite recipe to satellite goal, not max science pack. 2021-12-04 06:20:16 -08:00
Fabian Dill
feb2e0be03 Factorio: fix selecting wrong goal requirements due to convoluted if tree. 2021-12-04 10:54:11 +01:00
Fabian Dill
84e76eadd9 SM: rename death_link_survive and update docstring 2021-12-03 22:11:25 +01:00
Fabian Dill
c1a73e7839 WebHost: document how to bring up a slot tracker 2021-12-03 20:54:19 +01:00
espeon65536
75625b143c Core: better pretty-print for OptionList when the list is of non-strings 2021-12-03 18:15:10 +00:00
espeon65536
c10e17d24c Minecraft: remove bad default for StartingItems 2021-12-03 18:15:10 +00:00
Fabian Dill
21d465bcb8 CommonClient: add docstring to /ready 2021-12-03 07:04:17 +01:00
Fabian Dill
47c1300f30 Setup: move templates from /Players into /Players/Templates 2021-12-03 07:01:43 +01:00
Fabian Dill
e7d8149d74 LttP Docs: reword instructions to not accidentally overwrite the SNI Connector with an empty file. 2021-12-03 07:01:21 +01:00
eudaimonistic
a3220ac72d Add known safe MSU-1 list
List assembled for use in competitive Zelda restreams.  Permission sought and granted by author Amarith via DM.
2021-12-03 05:08:34 +00:00
Fabian Dill
994621372c MultiServer: finish removing prompt toolkit 2021-12-03 05:24:43 +01:00
Fabian Dill
9d3cbb19f9 Clients: add docstrings to /items and /locations 2021-12-03 05:14:44 +01:00
Fabian Dill
3110763052 WebHost: allow switching out "/tracker/" for "/generic_tracker/" in a tracker url to get the generic tracker for that slot.
No idea where a good place is to sick a link for it. Maybe on the individual trackers pages?
2021-12-03 02:41:56 +01:00
CaitSith2
6f12ed38d9 Add in whitelist for overriding blacklist. 2021-12-02 15:27:48 -08:00
CaitSith2
efb4e5a7b3 Use OptionSet for blacklist 2021-12-02 15:27:00 -08:00
CaitSith2
a15689e380 Allow explicit blacklisting (and whitelisting) of free samples from yaml 2021-12-02 09:26:51 -08:00
CaitSith2
548d893eaa Convenient runtime changing of death link status requires 0.2.1 2021-12-01 23:42:09 -08:00
Fabian Dill
1ec9ab5568 CommonClient: make the Server tooltip no longer fullscreen 2021-12-02 07:47:10 +01:00
Fabian Dill
a767d7723c FF1: update some client texts 2021-12-02 07:14:55 +01:00
Fabian Dill
a60c6176be SM: add client version check for DeathLink 2021-12-02 06:13:44 +01:00
lordlou
83cfd6ec05 SM update (#147)
* fixed generations failing when only bosses are unreachable

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

* fixed failling generations when using 'fun' settings

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

* fixed debug logger

* removed unsupported "suits_restriction" option

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

* - fixed deathlink emptying reserves

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

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

* Minecraft: 1.17 advancements and logic support

* Update Minecraft tracker to 1.17

* Minecraft: add tests for new advancements

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

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

* fix docstring for PostgameAdvancements

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

* Update descriptions for AdvancementGoal and EggShardsRequired

* Minecraft: fix tests for required_bosses attribute

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

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

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

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

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

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

* convert beemizer options to new style
2021-11-03 06:34:11 +01:00
Sandra
cb8da2e757 Marks player names with a pair of asterisks if they have completed their goal. 2021-11-03 04:56:54 +00:00
alwaysintreble
fdc96115e4 Created a general triggers and plando guide for Archipelago. (#101) 2021-11-03 05:55:50 +01:00
Fabian Dill
e019ec5ff7 AutoWorld: add spoiler hooks
Factorio: Move Recipes to new spoiler hooks
2021-11-02 12:29:29 +01:00
Fabian Dill
e4838f6d2b LttPClient: add snes write command 2021-11-02 11:12:13 +01:00
espeon65536
10837e75b2 Minecraft: make A Furious Cocktail hard, Free the End postgame 2021-11-02 05:37:40 +00:00
Fabian Dill
46590c3163 CommonClient.py UI: Server bar: allow connecting via pressing enter 2021-11-01 21:43:17 +01:00
Fabian Dill
e64d5c5f17 Network: implement new packet: ConnectUpdate 2021-11-01 20:00:55 +01:00
Fabian Dill
0e0cc0ad16 LttP: Implement DeathLink 2021-11-01 19:37:47 +01:00
Fabian Dill
8ff01ca979 CommonClient.py UI: log full traceback 2021-11-01 06:40:37 +01:00
CaitSith2
9508a9afc6 Fix leaving the window entirely leaves the server label hover text up. 2021-10-31 08:07:37 -07:00
Fabian Dill
704a0e3078 minor cleanup 2021-10-30 07:52:03 +02:00
Fabian Dill
9bf9f2c611 CommonClient.py: keep track of everyone's games. 2021-10-30 07:33:05 +02:00
Fabian Dill
71c869e65b CommonClient.py UI: add version info to Title 2021-10-29 15:19:10 +02:00
Chris Wilson
2897fa4003 Include references to LttPClient in the LttP tutorial 2021-10-29 04:41:56 -04:00
Fabian Dill
7f020857d1 CommonClient.py UI: Add info on "Server:" label hover
CommonClient.py UI: prevent freeze if UI is closed while waiting on text user input
2021-10-29 10:03:51 +02:00
Jarno Westhof
2217a9304d Fixed v card not getting marked
Changed order of A-D cards
2021-10-29 08:03:49 +00:00
Jarno Westhof
5a389b4855 [Timespinner] made method names lowercase + removed commented out code 2021-10-29 08:03:49 +00:00
Jarno Westhof
bdb9b7803c Added timespinner tracker 2021-10-29 08:03:49 +00:00
Jarno Westhof
4622b3fe36 Fixed bug with items variable 2021-10-29 08:03:49 +00:00
Jarno Westhof
402afd15db Split of trackers into game specific parts 2021-10-29 08:03:49 +00:00
Kyle Franz
82aca3bce4 Fix TR small key getting shuffled away 2021-10-26 16:54:42 +00:00
Chris Wilson
756c6554c9 Update Factorio tutorial 2021-10-25 21:32:58 -04:00
Chris Wilson
3b9753aaf4 Add /info page for Minecraft 2021-10-25 17:48:45 -04:00
Fabian Dill
4472ef20fe Factorio: add DeathLink option 2021-10-25 09:58:08 +02:00
Fabian Dill
c152790011 MultiServer: fix a refactor mistake 2021-10-25 08:24:32 +02:00
Fabian Dill
4e3b8a5178 MultiServer: allow sending another Connect, to update tags, uuid, team etc. 2021-10-25 06:57:06 +02:00
Fabian Dill
375a0ff208 Options: verify starting inventory counts are positive for more than just Factorio 2021-10-25 04:13:25 +02:00
Fabian Dill
57831f0eba FactorioClient: address some common issues 2021-10-24 23:22:06 +02:00
Hussein Farran
c9a3f67121 Update network protocol.md 2021-10-22 19:57:32 -04:00
Fabian Dill
6af1f98c88 CommonClient.py UI: add progressbar representing % of checks done.
CommonClient.py UI: add Commands button that points out /help and !help
CommonClient.py: track permissions
CommonClient.py: track missing locations and checked locations in lib
2021-10-22 05:25:09 +02:00
Fabian Dill
8e35372aad Network: add RoomInfo -> Games
Allows clients to only download relevant parts of the datapackage, or to keep ID lookups per-game, and for Bounce to tell if there will be a receiving end.
2021-10-22 04:46:00 +02:00
Fabian Dill
0f4d285223 TextClient UI: hide panel selection when there's only one panel to select.
CommonClient: remove "/connect " if it was accidentally copy-pasted into server bar.
2021-10-22 00:37:20 +02:00
Fabian Dill
192e592cda Docs: coop 2021-10-21 23:07:39 +02:00
Fabian Dill
1c2c1f286f Some cleanup 2021-10-21 21:06:38 +02:00
Fabian Dill
6e25af9493 LttPClient: fix missed ROM_PLAYER_LIMIT 2021-10-21 20:55:01 +02:00
Fabian Dill
050927008a Tests: add "EmptyStateCanReachSomething" 2021-10-21 20:23:13 +02:00
Fabian Dill
2fe5459c56 Core & LttP: remove 255 player limit 2021-10-21 08:15:47 +02:00
Fabian Dill
8fbbaf7fcb LttPClient: try to find linux SNI executable 2021-10-21 06:43:42 +02:00
Hussein Farran
2f5bdc5cf9 Merge pull request #98 from black-sliver/doc-update
add world api documentation
2021-10-20 19:41:39 -04:00
CaitSith2
17833a0bfc documentation corrections 2021-10-20 12:13:25 -07:00
Fabian Dill
f4e71df946 Requirements: update time! 2021-10-20 20:03:56 +02:00
Fabian Dill
be070b79af MultiServer: add !checked command, as it may be useful for coop. 2021-10-20 19:58:07 +02:00
Hussein Farran
ef8eefd3b4 Create en_Risk of Rain 2.md 2021-10-20 06:34:54 +00:00
Fabian Dill
83f46f6b2b Readme: fix tutorials page link 2021-10-20 08:31:00 +02:00
Fabian Dill
6b4bdf569c MultiServer: coop support
Just connect multiple clients to the same slot
2021-10-20 05:56:28 +02:00
Fabian Dill
7a9f6e2a8e Factorio: Prevent invalid item counts in start items. 2021-10-19 23:23:48 +02:00
Fabian Dill
ce95ff65bd CommonClient: give UI a server connect bar 2021-10-19 05:38:17 +02:00
Fabian Dill
28e724da98 WebHostLib.options: move to makedirs instead of mkdir. 2021-10-19 02:50:18 +02:00
Fabian Dill
a43b027cde Subnautica: add an install guide 2021-10-19 02:41:40 +02:00
Fabian Dill
4b5e36ebf2 FactorioClient: >< 2021-10-19 01:49:51 +02:00
Fabian Dill
89c05cfcae FactorioClient: Fix bridge not sending, and limit bridge to run up to once a second.
Setup: Fix LttP Adjuster needs to be installed with generator/lttp
MultiServer: fix duplicate !forfeits
2021-10-19 01:47:11 +02:00
Fabian Dill
f8569db21b Merge remote-tracking branch 'Archipelago/main' into Archipelago_Main 2021-10-18 22:58:45 +02:00
Fabian Dill
34eba2655e MultiServer: add !collect and collect_mode
CommonClient: make missing and checked location lookups faster
FactorioClient: implement reverse grant technologies for collect/forfeit/coop
2021-10-18 22:58:29 +02:00
Chris Wilson
1625860bd9 Add /info page for Super Metroid 2021-10-18 09:06:24 -04:00
Chris Wilson
f3ddfb96f3 Add Super Metroid setup guide, update LttP setup guide 2021-10-18 09:02:52 -04:00
Fabian Dill
66e198cbb6 Merge branch 'rip_compat' into Archipelago_Main
# Conflicts:
#	MultiServer.py
2021-10-18 08:18:28 +02:00
Vince Lund
33c747a881 Accidently changed variable name 2021-10-18 06:11:25 +00:00
Vince Lund
20d61d14e0 Fixed some spelling 2021-10-18 06:11:25 +00:00
Fabian Dill
833de94ab0 Generate: You can now have triggers in a game section that get run after common triggers and after the game is selected. Their format is the same but they can't overwrite game. 2021-10-17 20:53:06 +02:00
Fabian Dill
c8d6250ada WebHost: set default upload limit to 64 MB, as OoT is chonkers.
WebHost: rename .multidata to .archipelago in a missed flash message
WebHost: correctly parse Factorio slot names with "-"
2021-10-16 20:11:26 +02:00
Fabian Dill
d38e1185bb Setup: auto include auto generated yaml files 2021-10-16 19:40:58 +02:00
Fabian Dill
fdb8ae0cb5 FactorioClient: Warn user about the dangers of AppData
Factorio: improve setup guide somewhat
2021-10-16 19:40:27 +02:00
black-sliver
d79acef59e api.md: update precollected for commit# e66a2a7 2021-10-10 18:39:03 +02:00
black-sliver
2f04b93fdb api.md: add set Location.event in location skeleton 2021-10-10 14:03:33 +02:00
black-sliver
818e99b39d api.md: add exclusions to create_items, fix bug in generate_output 2021-10-10 13:09:18 +02:00
black-sliver
652c9943c2 api.md: add to the list of requirements 2021-10-09 14:35:08 +02:00
black-sliver
9f62575abe api.md: add data_version, clarify ids, add precollected_items 2021-10-09 14:29:52 +02:00
black-sliver
2fd87f703e api.md: fix more stuff based on comments 2021-10-09 13:00:50 +02:00
black-sliver
0376705e47 api.md: change 'Your World' based on suggestions 2021-10-09 11:28:15 +02:00
black-sliver
f1fddac655 api.md: add item groups, fix typo, reformat long lines 2021-10-09 11:06:41 +02:00
black-sliver
317f7116c4 api.md: Reword some things based on @Ijwu's suggestions 2021-10-09 02:05:55 +02:00
black-sliver
bf8e99140e api.md: Apply second batch of suggestions from code review
Co-authored-by: Hussein Farran <hmfarran@gmail.com>
2021-10-09 01:15:35 +02:00
black-sliver
6c949c3a52 api.md: Apply first batch of suggestions from code review
Co-authored-by: Hussein Farran <hmfarran@gmail.com>
2021-10-09 00:49:47 +02:00
black-sliver
87ceef230f api.md: remove useless \s, fix mixin example 2021-10-08 00:39:16 +02:00
black-sliver
a06e81a0ba api.md: add logic and output, fixed some typos, added some typos 2021-10-08 00:25:31 +02:00
black-sliver
59e87e0d27 api.md: fix Item.advancement description 2021-10-07 19:53:19 +02:00
black-sliver
76d1460d0f add api.md work-in-progress v3 2021-10-07 19:41:29 +02:00
Fabian Dill
f56bf0db73 MultiServer: remove legacy datapackage keys
MultiServer: remove warning about legacy datapackage use
MultiServer: remove legacy permission flags
Options: add "random" option to all Choices
LttP: remove random special handling from HeartColor
2021-09-30 13:22:25 +02:00
296 changed files with 64076 additions and 15195 deletions

View File

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

View File

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

5
.gitignore vendored
View File

@@ -4,6 +4,7 @@
*_Spoiler.txt
*.bmbp
*.apbp
*.apm3
*.apmc
*.apz5
*.pyc
@@ -151,3 +152,7 @@ dmypy.json
cython_debug/
Archipelago.zip
#minecraft server stuff
jdk*/
minecraft*/

View File

@@ -103,7 +103,8 @@ class MultiWorld():
set_player_attr('boss_shuffle', 'none')
set_player_attr('enemy_health', 'default')
set_player_attr('enemy_damage', 'default')
set_player_attr('beemizer', 0)
set_player_attr('beemizer_total_chance', 0)
set_player_attr('beemizer_trap_chance', 0)
set_player_attr('escape_assist', [])
set_player_attr('open_pyramid', False)
set_player_attr('treasure_hunt_icon', 'Triforce Piece')
@@ -254,14 +255,18 @@ class MultiWorld():
def get_items(self) -> list:
return [loc.item for loc in self.get_filled_locations()] + self.itempool
def find_items(self, item, player: int) -> List[Location]:
def find_item_locations(self, item, player: int) -> List[Location]:
return [location for location in self.get_locations() if
location.item is not None and location.item.name == item and location.item.player == player]
location.item and location.item.name == item and location.item.player == player]
def find_item(self, item, player: int) -> Location:
return next(location for location in self.get_locations() if
location.item and location.item.name == item and location.item.player == player)
def find_items_in_locations(self, items: Set[str], player: int) -> List[Location]:
return [location for location in self.get_locations() if
location.item and location.item.name in items and location.item.player == player]
def create_item(self, item_name: str, player: int) -> Item:
return self.worlds[player].create_item(item_name)
@@ -581,7 +586,7 @@ class CollectionState(object):
return any(shop.region.player == player and shop.has(item) and shop.region.can_reach(self) for
shop in self.world.shops)
def item_count(self, item, player: int) -> int:
def item_count(self, item: str, player: int) -> int:
return self.prog_items[item, player]
def has_triforce_pieces(self, count: int, player: int) -> bool:
@@ -708,23 +713,23 @@ class CollectionState(object):
def has_turtle_rock_medallion(self, player: int) -> bool:
return self.has(self.world.required_medallions[player][1], player)
def can_boots_clip_lw(self, player):
def can_boots_clip_lw(self, player: int):
if self.world.mode[player] == 'inverted':
return self.has('Pegasus Boots', player) and self.has('Moon Pearl', player)
return self.has('Pegasus Boots', player)
def can_boots_clip_dw(self, player):
def can_boots_clip_dw(self, player: int):
if self.world.mode[player] != 'inverted':
return self.has('Pegasus Boots', player) and self.has('Moon Pearl', player)
return self.has('Pegasus Boots', player)
def can_get_glitched_speed_lw(self, player):
def can_get_glitched_speed_lw(self, player: int):
rules = [self.has('Pegasus Boots', player), any([self.has('Hookshot', player), self.has_sword(player)])]
if self.world.mode[player] == 'inverted':
rules.append(self.has('Moon Pearl', player))
return all(rules)
def can_superbunny_mirror_with_sword(self, player):
def can_superbunny_mirror_with_sword(self, player: int):
return self.has('Magic Mirror', player) and self.has_sword(player)
def can_get_glitched_speed_dw(self, player: int):
@@ -753,7 +758,7 @@ class CollectionState(object):
return changed
def remove(self, item):
def remove(self, item: Item):
changed = self.world.worlds[item.player].remove(self, item)
if changed:
# invalidate caches, nothing can be trusted anymore now
@@ -771,14 +776,14 @@ class RegionType(int, Enum):
Dungeon = 4
@property
def is_indoors(self):
def is_indoors(self) -> bool:
"""Shorthand for checking if Cave or Dungeon"""
return self in (RegionType.Cave, RegionType.Dungeon)
class Region(object):
def __init__(self, name: str, type, hint, player: int, world: Optional[MultiWorld] = None):
def __init__(self, name: str, type: str, hint, player: int, world: Optional[MultiWorld] = None):
self.name = name
self.type = type
self.entrances = []
@@ -793,12 +798,12 @@ class Region(object):
self.hint_text = hint
self.player = player
def can_reach(self, state: CollectionState):
def can_reach(self, state: CollectionState) -> bool:
if state.stale[self.player]:
state.update_reachable_regions(self.player)
return self in state.reachable_regions[self.player]
def can_reach_private(self, state: CollectionState):
def can_reach_private(self, state: CollectionState) -> bool:
for entrance in self.entrances:
if entrance.can_reach(state):
if not self in state.path:
@@ -826,7 +831,7 @@ class Entrance(object):
self.player = player
self.hide_path = False
def can_reach(self, state):
def can_reach(self, state: CollectionState) -> bool:
if self.parent_region.can_reach(state) and self.access_rule(state):
if not self.hide_path and not self in state.path:
state.path[self] = (self.name, state.path.get(self.parent_region, (self.parent_region.name, None)))
@@ -834,7 +839,7 @@ class Entrance(object):
return False
def connect(self, region, addresses=None, target=None):
def connect(self, region: Region, addresses=None, target = None):
self.connected_region = region
self.target = target
self.addresses = addresses
@@ -860,11 +865,11 @@ class Dungeon(object):
self.world = None
@property
def boss(self):
def boss(self) -> Optional[Boss]:
return self.bosses.get(None, None)
@boss.setter
def boss(self, value):
def boss(self, value: Optional[Boss]):
self.bosses[None] = value
@property
@@ -891,7 +896,7 @@ class Dungeon(object):
class Boss():
def __init__(self, name, enemizer_name, defeat_rule, player: int):
def __init__(self, name: str, enemizer_name: str, defeat_rule, player: int):
self.name = name
self.enemizer_name = enemizer_name
self.defeat_rule = defeat_rule
@@ -1177,6 +1182,7 @@ class Spoiler():
return json.dumps(out)
def to_file(self, filename):
from worlds.AutoWorld import call_all, call_single, call_stage
self.parse_data()
def bool_to_text(variable: Union[bool, str]) -> str:
@@ -1198,6 +1204,7 @@ class Spoiler():
Utils.__version__, self.world.seed))
outfile.write('Filling Algorithm: %s\n' % self.world.algorithm)
outfile.write('Players: %d\n' % self.world.players)
call_stage(self.world, "write_spoiler_header", outfile)
for player in range(1, self.world.players + 1):
if self.world.players > 1:
@@ -1211,6 +1218,7 @@ class Spoiler():
if options:
for f_option, option in options.items():
write_option(f_option, option)
call_single(self.world, "write_spoiler_header", player, outfile)
if player in self.world.get_game_players("A Link to the Past"):
outfile.write('%s%s\n' % ('Hash: ', self.hashes[player]))
@@ -1245,7 +1253,6 @@ class Spoiler():
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.world.shuffle_prizes[player])
if self.entrances:
@@ -1260,13 +1267,8 @@ class Spoiler():
outfile.write('\n\nMedallions:\n')
for dungeon, medallion in self.medallions.items():
outfile.write(f'\n{dungeon}: {medallion}')
factorio_players = self.world.get_game_players("Factorio")
if factorio_players:
outfile.write('\n\nRecipes:\n')
for player in factorio_players:
name = self.world.get_player_name(player)
for recipe in self.world.worlds[player].custom_recipes.values():
outfile.write(f"\n{recipe.name} ({name}): {recipe.ingredients} -> {recipe.products}")
call_all(self.world, "write_spoiler", outfile)
outfile.write('\n\nLocations:\n\n')
outfile.write('\n'.join(
@@ -1307,6 +1309,7 @@ class Spoiler():
path_listings.append("{}\n {}".format(location, "\n => ".join(path_lines)))
outfile.write('\n'.join(path_listings))
call_all(self.world, "write_spoiler_end", outfile)
seeddigits = 20

View File

@@ -1,25 +1,27 @@
from __future__ import annotations
import logging
import typing
import asyncio
import urllib.parse
import sys
import os
import typing
import time
import websockets
import Utils
if __name__ == "__main__":
Utils.init_logging("TextClient", exception_logger="Client")
from MultiServer import CommandProcessor
from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission
from Utils import Version
from Utils import Version, stream_input
from worlds import network_data_package, AutoWorldRegister
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)
# without terminal we have to use gui mode
gui_enabled = not sys.stdout or "--nogui" not in sys.argv
class ClientCommandProcessor(CommandProcessor):
@@ -37,13 +39,13 @@ class ClientCommandProcessor(CommandProcessor):
def _cmd_connect(self, address: str = "") -> bool:
"""Connect to a MultiWorld Server"""
self.ctx.server_address = None
asyncio.create_task(self.ctx.connect(address if address else None))
asyncio.create_task(self.ctx.connect(address if address else None), name="connecting")
return True
def _cmd_disconnect(self) -> bool:
"""Disconnect from a MultiWorld Server"""
self.ctx.server_address = None
asyncio.create_task(self.ctx.disconnect())
asyncio.create_task(self.ctx.disconnect(), name="disconnecting")
return True
def _cmd_received(self) -> bool:
@@ -55,6 +57,9 @@ class ClientCommandProcessor(CommandProcessor):
def _cmd_missing(self) -> bool:
"""List all missing location checks, from your local game state"""
if not self.ctx.game:
self.output("No game set, cannot determine missing checks.")
return False
count = 0
checked_count = 0
for location, location_id in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id.items():
@@ -76,7 +81,20 @@ class ClientCommandProcessor(CommandProcessor):
self.output("No missing location checks found.")
return True
def _cmd_items(self):
"""List all item names for the currently running game."""
self.output(f"Item Names for {self.ctx.game}")
for item_name in AutoWorldRegister.world_types[self.ctx.game].item_name_to_id:
self.output(item_name)
def _cmd_locations(self):
"""List all location names for the currently running game."""
self.output(f"Location Names for {self.ctx.game}")
for location_name in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id:
self.output(location_name)
def _cmd_ready(self):
"""Send ready status to server."""
self.ctx.ready = not self.ctx.ready
if self.ctx.ready:
state = ClientStatus.CLIENT_READY
@@ -84,13 +102,16 @@ class ClientCommandProcessor(CommandProcessor):
else:
state = ClientStatus.CLIENT_CONNECTED
self.output("Unreadied.")
asyncio.create_task(self.ctx.send_msgs([{"cmd": "StatusUpdate", "status": state}]))
asyncio.create_task(self.ctx.send_msgs([{"cmd": "StatusUpdate", "status": state}]), name="send StatusUpdate")
def default(self, raw: str):
asyncio.create_task(self.ctx.send_msgs([{"cmd": "Say", "text": raw}]))
raw = self.ctx.on_user_say(raw)
if raw:
asyncio.create_task(self.ctx.send_msgs([{"cmd": "Say", "text": raw}]), name="send Say")
class CommonContext():
tags: typing.Set[str] = {"AP"}
starting_reconnect_delay: int = 5
current_reconnect_delay: int = starting_reconnect_delay
command_processor: int = ClientCommandProcessor
@@ -105,6 +126,13 @@ class CommonContext():
self.server_task = None
self.server: typing.Optional[Endpoint] = None
self.server_version = Version(0, 0, 0)
self.hint_cost: typing.Optional[int] = None
self.games: typing.Dict[int, str] = {}
self.permissions = {
"forfeit": "disabled",
"collect": "disabled",
"remaining": "disabled",
}
# own state
self.finished_game = False
@@ -114,16 +142,18 @@ class CommonContext():
self.auth = None
self.seed_name = None
self.locations_checked: typing.Set[int] = set()
self.locations_checked: typing.Set[int] = set() # local state
self.locations_scouted: typing.Set[int] = set()
self.items_received = []
self.missing_locations: typing.List[int] = []
self.checked_locations: typing.List[int] = []
self.missing_locations: typing.Set[int] = set()
self.checked_locations: typing.Set[int] = set() # server state
self.locations_info = {}
self.input_queue = asyncio.Queue()
self.input_requests = 0
self.last_death_link: float = time.time() # last send/received death link on AP layer
# game state
self.player_names: typing.Dict[int: str] = {0: "Archipelago"}
self.exit_event = asyncio.Event()
@@ -134,7 +164,13 @@ class CommonContext():
self.set_getters(network_data_package)
# execution
self.keep_alive_task = asyncio.create_task(keep_alive(self))
self.keep_alive_task = asyncio.create_task(keep_alive(self), name="Bouncy")
@property
def total_locations(self) -> typing.Optional[int]:
"""Will return None until connected."""
if self.checked_locations or self.missing_locations:
return len(self.checked_locations | self.missing_locations)
async def connection_closed(self):
self.auth = None
@@ -146,6 +182,7 @@ class CommonContext():
self.server = None
self.server_task = None
# noinspection PyAttributeOutsideInit
def set_getters(self, data_package: dict, network=False):
if not network: # local data; check if newer data was already downloaded
local_package = Utils.persistent_load().get("datapackage", {}).get("latest", {})
@@ -208,13 +245,24 @@ class CommonContext():
self.password = await self.console_input()
return self.password
async def send_connect(self, **kwargs):
payload = {
"cmd": 'Connect',
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
'tags': self.tags,
'uuid': Utils.get_unique_identifier(), 'game': self.game
}
if kwargs:
payload.update(kwargs)
await self.send_msgs([payload])
async def console_input(self):
self.input_requests += 1
return await self.input_queue.get()
async def connect(self, address=None):
await self.disconnect()
self.server_task = asyncio.create_task(server_loop(self, address))
self.server_task = asyncio.create_task(server_loop(self, address), name="server loop")
def on_print(self, args: dict):
logger.info(args["text"])
@@ -230,6 +278,64 @@ class CommonContext():
"""For custom package handling in subclasses."""
pass
def on_user_say(self, text: str) -> typing.Optional[str]:
"""Gets called before sending a Say to the server from the user.
Returned text is sent, or sending is aborted if None is returned."""
return text
def update_permissions(self, permissions: typing.Dict[str, int]):
for permission_name, permission_flag in permissions.items():
try:
flag = Permission(permission_flag)
logger.info(f"{permission_name.capitalize()} permission: {flag.name}")
self.permissions[permission_name] = flag.name
except Exception as e: # safeguard against permissions that may be implemented in the future
logger.exception(e)
async def shutdown(self):
self.server_address = None
if self.server and not self.server.socket.closed:
await self.server.socket.close()
if self.server_task:
await self.server_task
while self.input_requests > 0:
self.input_queue.put_nowait(None)
self.input_requests -= 1
self.keep_alive_task.cancel()
# DeathLink hooks
def on_deathlink(self, data: dict):
"""Gets dispatched when a new DeathLink is triggered by another linked player."""
self.last_death_link = max(data["time"], self.last_death_link)
text = data.get("cause", "")
if text:
logger.info(f"DeathLink: {text}")
else:
logger.info(f"DeathLink: Received from {data['source']}")
async def send_death(self, death_text: str = ""):
logger.info("DeathLink: Sending death to your friends...")
self.last_death_link = time.time()
await self.send_msgs([{
"cmd": "Bounce", "tags": ["DeathLink"],
"data": {
"time": self.last_death_link,
"source": self.player_names[self.slot],
"cause": death_text
}
}])
async def update_death_link(self, death_link):
old_tags = self.tags.copy()
if death_link:
self.tags.add("DeathLink")
else:
self.tags -= {"DeathLink"}
if old_tags != self.tags and self.server and not self.server.socket.closed:
await self.send_msgs([{"cmd": "ConnectUpdate", "tags": self.tags}])
async def keep_alive(ctx: CommonContext, seconds_between_checks=100):
"""some ISPs/network configurations drop TCP connections if no payload is sent (ignore TCP-keep-alive)
@@ -288,14 +394,14 @@ async def server_loop(ctx: CommonContext, address=None):
await ctx.connection_closed()
if ctx.server_address:
logger.info(f"... reconnecting in {ctx.current_reconnect_delay}s")
asyncio.create_task(server_autoreconnect(ctx))
asyncio.create_task(server_autoreconnect(ctx), name="server auto reconnect")
ctx.current_reconnect_delay *= 2
async def server_autoreconnect(ctx: CommonContext):
await asyncio.sleep(ctx.current_reconnect_delay)
if ctx.server_address and ctx.server_task is None:
ctx.server_task = asyncio.create_task(server_loop(ctx))
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
async def process_server_cmd(ctx: CommonContext, args: dict):
@@ -319,10 +425,9 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
logger.info("Server protocol tags: " + ", ".join(args["tags"]))
if args['password']:
logger.info('Password required')
for permission_name, permission_flag in args.get("permissions", {}).items():
flag = Permission(permission_flag)
logger.info(f"{permission_name.capitalize()} permission: {flag.name}")
ctx.update_permissions(args.get("permissions", {}))
if "games" in args:
ctx.games = {x: game for x, game in enumerate(args["games"], start=1)}
logger.info(
f"A !hint costs {args['hint_cost']}% of your total location count as points"
f" and you get {args['location_check_points']}"
@@ -389,8 +494,8 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
# This list is used to only send to the server what is reported as ACTUALLY Missing.
# This also serves to allow an easy visual of what locations were already checked previously
# when /missing is used for the client side view of what is missing.
ctx.missing_locations = args["missing_locations"]
ctx.checked_locations = args["checked_locations"]
ctx.missing_locations = set(args["missing_locations"])
ctx.checked_locations = set(args["checked_locations"])
elif cmd == 'ReceivedItems':
start_index = args["index"]
@@ -419,6 +524,12 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
ctx.consume_players_package(args["players"])
if "hint_points" in args:
ctx.hint_points = args['hint_points']
if "checked_locations" in args:
checked = set(args["checked_locations"])
ctx.checked_locations |= checked
ctx.missing_locations -= checked
if "permissions" in args:
ctx.update_permissions(args["permissions"])
elif cmd == 'Print':
ctx.on_print(args)
@@ -430,7 +541,10 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
logger.warning(f"Invalid Packet of {args['type']}: {args['text']}")
elif cmd == "Bounced":
pass
tags = args.get("tags", [])
# we can skip checking "DeathLink" in ctx.tags, as otherwise we wouldn't have been send this
if "DeathLink" in tags and ctx.last_death_link != args["data"]["time"]:
ctx.on_deathlink(args["data"])
else:
logger.debug(f"unknown command {cmd}")
@@ -441,12 +555,12 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
async def console_loop(ctx: CommonContext):
import sys
commandprocessor = ctx.command_processor(ctx)
queue = asyncio.Queue()
stream_input(sys.stdin, queue)
while not ctx.exit_event.is_set():
try:
input_text = await asyncio.get_event_loop().run_in_executor(
None, sys.stdin.readline
)
input_text = input_text.strip()
input_text = await queue.get()
queue.task_done()
if ctx.input_requests > 0:
ctx.input_requests -= 1
@@ -459,20 +573,23 @@ async def console_loop(ctx: CommonContext):
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"))
def get_base_parser(description=None):
import argparse
parser = argparse.ArgumentParser(description=description)
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
parser.add_argument('--password', default=None, help='Password of the multiworld host.')
if sys.stdout: # If terminal output exists, offer gui-less mode
parser.add_argument('--nogui', default=False, action='store_true', help="Turns off Client GUI.")
return parser
if __name__ == '__main__':
# Text Mode to use !hint and such with games that have no text entry
init_logging("TextClient")
class TextContext(CommonContext):
tags = {"AP", "IgnoreGame"}
game = "Archipelago"
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(TextContext, self).server_auth(password_requested)
@@ -480,50 +597,37 @@ if __name__ == '__main__':
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
}])
await self.send_connect()
def on_package(self, cmd: str, args: dict):
if cmd == "Connected":
self.game = self.games.get(self.slot, None)
async def main(args):
ctx = TextContext(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
input_task = None
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
if sys.stdin:
input_task = asyncio.create_task(console_loop(ctx), name="Input")
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
await ctx.shutdown()
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.")
parser = get_base_parser(description="Gameless Archipelago Client, for text interfacing.")
args, rest = parser.parse_known_args()
colorama.init()

255
FF1Client.py Normal file
View File

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

View File

@@ -5,23 +5,26 @@ import json
import string
import copy
import subprocess
import factorio_rcon
import sys
import time
import random
import factorio_rcon
import colorama
import asyncio
from queue import Queue
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger, gui_enabled, \
init_logging
from MultiServer import mark_raw
import Utils
import random
if __name__ == "__main__":
Utils.init_logging("FactorioClient", exception_logger="Client")
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger, gui_enabled, \
get_base_parser
from MultiServer import mark_raw
from NetUtils import NetworkItem, ClientStatus, JSONtoTextParser, JSONMessagePart
from worlds.factorio import Factorio
init_logging("FactorioClient")
class FactorioCommandProcessor(ClientCommandProcessor):
ctx: FactorioContext
@@ -52,34 +55,27 @@ class FactorioContext(CommonContext):
def __init__(self, server_address, password):
super(FactorioContext, self).__init__(server_address, password)
self.send_index = 0
self.send_index: int = 0
self.rcon_client = None
self.awaiting_bridge = False
self.write_data_path = None
self.death_link_tick: int = 0 # last send death link on Factorio layer
self.factorio_json_text_parser = FactorioJSONtoTextParser(self)
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(FactorioContext, self).server_auth(password_requested)
if not self.auth:
if self.rcon_client:
get_info(self, self.rcon_client) # retrieve current auth code
else:
raise Exception("Cannot connect to a server with unknown own identity, "
"bridge to Factorio first.")
if self.rcon_client:
await get_info(self, self.rcon_client) # retrieve current auth code
else:
raise Exception("Cannot connect to a server with unknown own identity, "
"bridge to Factorio first.")
await self.send_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_connect()
def on_print(self, args: dict):
logger.info(args["text"])
super(FactorioContext, self).on_print(args)
if self.rcon_client:
self.print_to_game(args['text'])
@@ -91,16 +87,21 @@ class FactorioContext(CommonContext):
@property
def savegame_name(self) -> str:
return f"AP_{self.seed_name}_{self.auth}.zip"
return f"AP_{self.seed_name}_{self.auth}_Save.zip"
def print_to_game(self, text):
self.rcon_client.send_command(f"/ap-print [font=default-large-bold]Archipelago:[/font] "
f"{text}")
def on_deathlink(self, data: dict):
if self.rcon_client:
self.rcon_client.send_command(f"/ap-deathlink {data['source']}")
super(FactorioContext, self).on_deathlink(data)
def on_package(self, cmd: str, args: dict):
if cmd == "Connected":
if cmd in {"Connected", "RoomUpdate"}:
# catch up sync anything that is already cleared.
if args["checked_locations"]:
if "checked_locations" in args and args["checked_locations"]:
self.rcon_client.send_commands({item_name: f'/ap-get-technology ap-{item_name}-\t-1' for
item_name in args["checked_locations"]})
@@ -108,9 +109,11 @@ class FactorioContext(CommonContext):
async def game_watcher(ctx: FactorioContext):
bridge_logger = logging.getLogger("FactorioWatcher")
from worlds.factorio.Technologies import lookup_id_to_name
next_bridge = time.perf_counter() + 1
try:
while not ctx.exit_event.is_set():
if ctx.awaiting_bridge and ctx.rcon_client:
if ctx.awaiting_bridge and ctx.rcon_client and time.perf_counter() > next_bridge:
next_bridge = time.perf_counter() + 1
ctx.awaiting_bridge = False
data = json.loads(ctx.rcon_client.send_command("/ap-sync"))
if data["slot_name"] != ctx.auth:
@@ -123,18 +126,26 @@ async def game_watcher(ctx: FactorioContext):
research_data = data["research_done"]
research_data = {int(tech_name.split("-")[1]) for tech_name in research_data}
victory = data["victory"]
if "death_link" in data: # TODO: Remove this if statement around version 0.2.4 or so
await ctx.update_death_link(data["death_link"])
if not ctx.finished_game and victory:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
if ctx.locations_checked != research_data:
bridge_logger.info(
bridge_logger.debug(
f"New researches done: "
f"{[lookup_id_to_name[rid] for rid in research_data - ctx.locations_checked]}")
ctx.locations_checked = research_data
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}])
await asyncio.sleep(1)
death_link_tick = data.get("death_link_tick", 0)
if death_link_tick != ctx.death_link_tick:
ctx.death_link_tick = death_link_tick
if "DeathLink" in ctx.tags:
await ctx.send_death()
await asyncio.sleep(0.1)
except Exception as e:
logging.exception(e)
@@ -180,7 +191,8 @@ async def factorio_server_watcher(ctx: FactorioContext):
while not factorio_queue.empty():
msg = factorio_queue.get()
factorio_server_logger.info(msg)
factorio_queue.task_done()
if not ctx.rcon_client and "Starting RCON interface at IP ADDR:" in msg:
ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
if not ctx.server:
@@ -189,7 +201,9 @@ async def factorio_server_watcher(ctx: FactorioContext):
if not ctx.awaiting_bridge and "Archipelago Bridge Data available for game tick " in msg:
ctx.awaiting_bridge = True
factorio_server_logger.debug(msg)
else:
factorio_server_logger.info(msg)
if ctx.rcon_client:
commands = {}
while ctx.send_index < len(ctx.items_received):
@@ -218,10 +232,13 @@ async def factorio_server_watcher(ctx: FactorioContext):
factorio_process.wait(5)
def get_info(ctx, rcon_client):
async def get_info(ctx, rcon_client):
info = json.loads(rcon_client.send_command("/ap-rcon-info"))
ctx.auth = info["slot_name"]
ctx.seed_name = info["seed_name"]
# 0.2.0 addition, not present earlier
death_link = bool(info.get("death_link", False))
await ctx.update_death_link(death_link)
async def factorio_spinup_server(ctx: FactorioContext) -> bool:
@@ -250,11 +267,17 @@ async def factorio_spinup_server(ctx: FactorioContext) -> bool:
if "Loading mod AP-" in msg and msg.endswith("(data.lua)"):
parts = msg.split()
ctx.mod_version = Utils.Version(*(int(number) for number in parts[-2].split(".")))
elif "Write data path: " in msg:
ctx.write_data_path = Utils.get_text_between(msg, "Write data path: ", " [")
if "AppData" in ctx.write_data_path:
logger.warning("It appears your mods are loaded from Appdata, "
"this can lead to problems with multiple Factorio instances. "
"If this is the case, you will get a file locked error running Factorio.")
if not rcon_client and "Starting RCON interface at IP ADDR:" in msg:
rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
if ctx.mod_version == ctx.__class__.mod_version:
raise Exception("No Archipelago mod was loaded. Aborting.")
get_info(ctx, rcon_client)
await get_info(ctx, rcon_client)
await asyncio.sleep(0.01)
except Exception as e:
@@ -275,14 +298,15 @@ async def factorio_spinup_server(ctx: FactorioContext) -> bool:
async def main(args):
ctx = FactorioContext(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
input_task = None
if gui_enabled:
input_task = None
from kvui import FactorioManager
ctx.ui = FactorioManager(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
if sys.stdin:
input_task = asyncio.create_task(console_loop(ctx), name="Input")
factorio_server_task = asyncio.create_task(factorio_spinup_server(ctx), name="FactorioSpinupServer")
succesful_launch = await factorio_server_task
if succesful_launch:
@@ -296,14 +320,7 @@ async def main(args):
await progression_watcher
await factorio_server_task
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
await ctx.shutdown()
if ui_task:
await ui_task
@@ -327,17 +344,11 @@ class FactorioJSONtoTextParser(JSONtoTextParser):
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(description="Optional arguments to FactorioClient follow. "
"Remaining arguments get passed into bound Factorio instance."
"Refer to Factorio --help for those.")
parser = get_base_parser(description="Optional arguments to FactorioClient follow. "
"Remaining arguments get passed into bound Factorio instance."
"Refer to Factorio --help for those.")
parser.add_argument('--rcon-port', default='24242', type=int, help='Port to use to communicate with Factorio')
parser.add_argument('--rcon-password', help='Password to authenticate with RCON.')
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()

View File

@@ -56,7 +56,6 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations,
logging.warning(
f'Not all items placed. Game beatable anyway. (Could not place {item_to_place})')
continue
raise FillError(f'No more spots to place {item_to_place}, locations {locations} are invalid. '
f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}')

View File

@@ -12,6 +12,7 @@ import ModuleUpdate
ModuleUpdate.update()
import Utils
from worlds.alttp import Options as LttPOptions
from worlds.generic import PlandoItem, PlandoConnection
from Utils import parse_yaml, version_tuple, __version__, tuplize_version, get_options
@@ -37,16 +38,16 @@ def mystery_argparse():
parser.add_argument('--player_files_path', default=defaults["player_files_path"],
help="Input directory for player files.")
parser.add_argument('--seed', help='Define seed number to generate.', type=int)
parser.add_argument('--multi', default=defaults["players"], type=lambda value: min(max(int(value), 1), 255))
parser.add_argument('--multi', default=defaults["players"], type=lambda value: max(int(value), 1))
parser.add_argument('--spoiler', type=int, default=defaults["spoiler"])
parser.add_argument('--rom', default=options["lttp_options"]["rom_file"], help="Path to the 1.0 JP LttP Baserom.")
parser.add_argument('--lttp_rom', default=options["lttp_options"]["rom_file"], help="Path to the 1.0 JP LttP Baserom.")
parser.add_argument('--sm_rom', default=options["sm_options"]["rom_file"], help="Path to the 1.0 JP SM Baserom.")
parser.add_argument('--enemizercli', default=defaults["enemizer_path"])
parser.add_argument('--outputpath', default=options["general_options"]["output_path"])
parser.add_argument('--race', action='store_true', default=defaults["race"])
parser.add_argument('--meta_file_path', default=defaults["meta_file_path"])
parser.add_argument('--log_output_path', help='Path to store output log')
parser.add_argument('--log_level', default='info', help='Sets log level')
parser.add_argument('--yaml_output', default=0, type=lambda value: min(max(int(value), 0), 255),
parser.add_argument('--yaml_output', default=0, type=lambda value: max(int(value), 0),
help='Output rolled mystery results to yaml up to specified number (made for async multiworld)')
parser.add_argument('--plando', default=defaults["plando_options"],
help='List of options that can be set manually. Can be combined, for example "bosses, items"')
@@ -89,7 +90,8 @@ def main(args=None, callback=ERmain):
except Exception as e:
raise ValueError(f"File {args.meta_file_path} is destroyed. Please fix your yaml.") from e
meta_weights = weights_cache[args.meta_file_path]
print(f"Meta: {args.meta_file_path} >> {get_choice('meta_description', meta_weights, 'No description specified')}")
print(f"Meta: {args.meta_file_path} >> {get_choice('meta_description', meta_weights)}")
del(meta_weights["meta_description"])
if args.samesettings:
raise Exception("Cannot mix --samesettings with --meta")
else:
@@ -125,20 +127,10 @@ def main(args=None, callback=ERmain):
erargs.outputname = seed_name
erargs.outputpath = args.outputpath
# set up logger
if args.log_level:
erargs.loglevel = args.log_level
loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[
erargs.loglevel]
Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level)
if args.log_output_path:
os.makedirs(args.log_output_path, exist_ok=True)
logging.basicConfig(format='%(message)s', level=loglevel, force=True,
filename=os.path.join(args.log_output_path, f"{seed}.log"))
else:
logging.basicConfig(format='%(message)s', level=loglevel, force=True)
erargs.rom = args.rom
erargs.lttp_rom = args.lttp_rom
erargs.sm_rom = args.sm_rom
erargs.enemizercli = args.enemizercli
settings_cache = {k: (roll_settings(v, args.plando) if args.samesettings else None)
@@ -148,17 +140,17 @@ def main(args=None, callback=ERmain):
player_path_cache[player] = player_files.get(player, args.weights_file_path)
if meta_weights:
for player, path in player_path_cache.items():
weights_cache[path].setdefault("meta_ignore", [])
for key in meta_weights:
option = get_choice(key, meta_weights)
if option is not None:
for player, path in player_path_cache.items():
players_meta = weights_cache[path].get("meta_ignore", [])
if key not in players_meta:
weights_cache[path][key] = option
elif type(players_meta) == dict and players_meta[key] and option not in players_meta[key]:
weights_cache[path][key] = option
for category_name, category_dict in meta_weights.items():
for key in category_dict:
option = get_choice(key, category_dict)
if option is not None:
for player, path in player_path_cache.items():
if category_name is None:
weights_cache[path][key] = option
elif category_name not in weights_cache[path]:
raise Exception(f"Meta: Category {category_name} is not present in {path}.")
else:
weights_cache[path][category_name][key] = option
name_counter = Counter()
erargs.player_settings = {}
@@ -359,10 +351,10 @@ def roll_linked_options(weights: dict) -> dict:
return weights
def roll_triggers(weights: dict) -> dict:
def roll_triggers(weights: dict, triggers: list) -> dict:
weights = weights.copy() # make sure we don't write back to other weights sets in same_settings
weights["_Generator_Version"] = "Archipelago" # Some means for triggers to know if the seed is on main or doors.
for i, option_set in enumerate(weights["triggers"]):
weights["_Generator_Version"] = Utils.__version__
for i, option_set in enumerate(triggers):
try:
currently_targeted_weights = weights
category = option_set.get("option_category", None)
@@ -455,7 +447,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
weights = roll_linked_options(weights)
if "triggers" in weights:
weights = roll_triggers(weights)
weights = roll_triggers(weights, weights["triggers"])
requirements = weights.get("requires", {})
if requirements:
@@ -480,15 +472,21 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
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)
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]
if "triggers" in game_weights:
weights = roll_triggers(weights, game_weights["triggers"])
game_weights = weights[ret.game]
ret.name = get_choice('name', weights)
for option_key, option in Options.common_options.items():
setattr(ret, option_key, option.from_any(get_choice(option_key, weights, option.default)))
if ret.game in AutoWorldRegister.world_types:
for option_key, option in world_type.options.items():
handle_option(ret, game_weights, option_key, option)
@@ -496,7 +494,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
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":
if ret.game == "Minecraft" or ret.game == "Ocarina of Time":
# bad hardcoded behavior to make this work for now
ret.plando_connections = []
if "connections" in plando_options:
@@ -506,7 +504,7 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
ret.plando_connections.append(PlandoConnection(
get_choice("entrance", placement),
get_choice("exit", placement),
get_choice("direction", placement, "both")
get_choice("direction", placement)
))
elif ret.game == "A Link to the Past":
roll_alttp_settings(ret, game_weights, plando_options)
@@ -628,8 +626,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
ret.enemy_health = get_choice_legacy('enemy_health', weights)
ret.beemizer = int(get_choice_legacy('beemizer', weights, 0))
ret.timer = {'none': False,
None: False,
False: False,

View File

@@ -15,12 +15,12 @@ from argparse import Namespace
from concurrent.futures import as_completed, ThreadPoolExecutor
from glob import glob
from tkinter import Tk, Frame, Label, StringVar, Entry, filedialog, messagebox, Button, LEFT, X, TOP, LabelFrame, \
IntVar, Checkbutton, E, OptionMenu, Toplevel, BOTTOM, RIGHT, font as font, PhotoImage
IntVar, Checkbutton, E, W, OptionMenu, Toplevel, BOTTOM, RIGHT, font as font, PhotoImage
from urllib.parse import urlparse
from urllib.request import urlopen
from worlds.alttp.Rom import Sprite, LocalRom, apply_rom_settings, get_base_rom_bytes
from Utils import output_path, local_path, open_file
from Utils import output_path, local_path, open_file, get_cert_none_ssl_context, persistent_store
class AdjusterWorld(object):
@@ -51,6 +51,7 @@ def main():
(default: %(default)s)
''')
parser.add_argument('--quickswap', help='Enable quick item swapping with L and R.', action='store_true')
parser.add_argument('--deathlink', help='Enable DeathLink system.', action='store_true')
parser.add_argument('--disablemusic', help='Disables game music.', action='store_true')
parser.add_argument('--triforcehud', default='hide_goal', const='hide_goal', nargs='?',
choices=['normal', 'hide_goal', 'hide_required', 'hide_both'],
@@ -101,14 +102,15 @@ def main():
parser.add_argument('--update_sprites', action='store_true', help='Update Sprite Database, then exit.')
args = parser.parse_args()
args.music = not args.disablemusic
if args.update_sprites:
run_sprite_update()
sys.exit()
# set up logger
loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[
args.loglevel]
logging.basicConfig(format='%(message)s', level=loglevel)
if args.update_sprites:
run_sprite_update()
sys.exit()
if not os.path.isfile(args.rom):
adjustGUI()
else:
@@ -117,7 +119,6 @@ def main():
sys.exit(1)
args, path = adjust(args=args)
from Utils import persistent_store
if isinstance(args.sprite, Sprite):
args.sprite = args.sprite.name
persistent_store("adjuster", "last_settings_3", args)
@@ -152,7 +153,8 @@ def adjust(args):
world = getattr(args, "world")
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.menuspeed, args.music,
args.sprite, palettes_options, reduceflashing=args.reduceflashing or racerom, world=world)
args.sprite, palettes_options, reduceflashing=args.reduceflashing or racerom, world=world,
deathlink=args.deathlink)
path = output_path(f'{os.path.basename(args.rom)[:-4]}_adjusted.sfc')
rom.write_to_file(path)
@@ -205,6 +207,7 @@ def adjustGUI():
guiargs.quickswap = bool(rom_vars.quickSwapVar.get())
guiargs.music = bool(rom_vars.MusicVar.get())
guiargs.reduceflashing = bool(rom_vars.disableFlashingVar.get())
guiargs.deathlink = bool(rom_vars.DeathLinkVar.get())
guiargs.rom = romVar2.get()
guiargs.baserom = romVar.get()
guiargs.sprite = rom_vars.sprite
@@ -221,7 +224,6 @@ def adjustGUI():
messagebox.showerror(title="Error while adjusting Rom", message=str(e))
else:
messagebox.showinfo(title="Success", message=f"Rom patched successfully to {path}")
from Utils import persistent_store
if isinstance(guiargs.sprite, Sprite):
guiargs.sprite = guiargs.sprite.name
persistent_store("adjuster", "last_settings_3", guiargs)
@@ -238,12 +240,16 @@ def adjustGUI():
def run_sprite_update():
import threading
done = threading.Event()
top = Tk()
top.withdraw()
BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
try:
top = Tk()
except:
task = BackgroundTaskProgressNullWindow(update_sprites, lambda successful, resultmessage: done.set())
else:
top.withdraw()
task = BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
while not done.isSet():
top.update()
print("Done updating sprites")
task.do_events()
logging.info("Done updating sprites")
def update_sprites(task, on_finish=None):
@@ -251,7 +257,7 @@ def update_sprites(task, on_finish=None):
successful = True
sprite_dir = local_path("data", "sprites", "alttpr")
os.makedirs(sprite_dir, exist_ok=True)
ctx = get_cert_none_ssl_context()
def finished():
task.close_window()
if on_finish:
@@ -259,7 +265,7 @@ def update_sprites(task, on_finish=None):
try:
task.update_status("Downloading alttpr sprites list")
with urlopen('https://alttpr.com/sprites') as response:
with urlopen('https://alttpr.com/sprites', context=ctx) as response:
sprites_arr = json.loads(response.read().decode("utf-8"))
except Exception as e:
resultmessage = "Error getting list of alttpr sprites. Sprites not updated.\n\n%s: %s" % (type(e).__name__, e)
@@ -272,26 +278,26 @@ def update_sprites(task, on_finish=None):
current_sprites = [os.path.basename(file) for file in glob(sprite_dir + '/*')]
alttpr_sprites = [(sprite['file'], os.path.basename(urlparse(sprite['file']).path))
for sprite in sprites_arr if sprite["author"] != "Nintendo"]
needed_sprites = [(sprite_url, filename) for (sprite_url, filename) in alttpr_sprites if filename not in current_sprites]
needed_sprites = [(sprite_url, filename) for (sprite_url, filename) in alttpr_sprites if
filename not in current_sprites]
alttpr_filenames = [filename for (_, filename) in alttpr_sprites]
obsolete_sprites = [sprite for sprite in current_sprites if sprite not in alttpr_filenames]
except Exception as e:
resultmessage = "Error Determining which sprites to update. Sprites not updated.\n\n%s: %s" % (type(e).__name__, e)
resultmessage = "Error Determining which sprites to update. Sprites not updated.\n\n%s: %s" % (
type(e).__name__, e)
successful = False
task.queue_event(finished)
return
def dl(sprite_url, filename):
target = os.path.join(sprite_dir, filename)
with urlopen(sprite_url) as response, open(target, 'wb') as out:
with urlopen(sprite_url, context=ctx) as response, open(target, 'wb') as out:
shutil.copyfileobj(response, out)
def rem(sprite):
os.remove(os.path.join(sprite_dir, sprite))
with ThreadPoolExecutor() as pool:
dl_tasks = []
rem_tasks = []
@@ -313,7 +319,7 @@ def update_sprites(task, on_finish=None):
except Exception as e:
logging.exception(e)
resultmessage = "Error downloading sprite. Not all sprites updated.\n\n%s: %s" % (
type(e).__name__, e)
type(e).__name__, e)
successful = False
for rem_task in as_completed(rem_tasks):
@@ -324,7 +330,7 @@ def update_sprites(task, on_finish=None):
except Exception as e:
logging.exception(e)
resultmessage = "Error removing obsolete sprite. Not all sprites updated.\n\n%s: %s" % (
type(e).__name__, e)
type(e).__name__, e)
successful = False
if successful:
@@ -362,7 +368,7 @@ class BackgroundTask(object):
event = self.queue.get_nowait()
event()
if self.running:
#if self is no longer running self.window may no longer be valid
# if self is no longer running self.window may no longer be valid
self.window.update_idletasks()
except queue.Empty:
pass
@@ -397,12 +403,39 @@ class BackgroundTaskProgress(BackgroundTask):
def update_status(self, text):
self.queue_event(lambda: self.label_var.set(text))
def do_events(self):
self.parent.update()
# only call this in an event callback
def close_window(self):
self.stop()
self.window.destroy()
class BackgroundTaskProgressNullWindow(BackgroundTask):
def __init__(self, code_to_run, *args):
super().__init__(None, code_to_run, *args)
def process_queue(self):
try:
while True:
if not self.running:
return
event = self.queue.get_nowait()
event()
except queue.Empty:
pass
def do_events(self):
self.process_queue()
def update_status(self, text):
self.queue_event(lambda: logging.info(text))
def close_window(self):
self.stop()
def get_rom_frame(parent=None):
romFrame = Frame(parent)
baseRomLabel = Label(romFrame, text='LttP Base Rom: ')
@@ -420,6 +453,7 @@ def get_rom_frame(parent=None):
romVar.set(rom)
romSelectButton['state'] = "disabled"
romSelectButton["text"] = "ROM verified"
romSelectButton = Button(romFrame, text='Select Rom', command=RomSelect)
baseRomLabel.pack(side=LEFT)
@@ -444,17 +478,21 @@ def get_rom_options_frame(parent=None):
MusicCheckbutton.grid(row=0, column=0, sticky=E)
vars.disableFlashingVar = IntVar(value=1)
disableFlashingCheckbutton = Checkbutton(romOptionsFrame, text="Disable flashing (anti-epilepsy)", variable=vars.disableFlashingVar)
disableFlashingCheckbutton.grid(row=6, column=0, sticky=E)
disableFlashingCheckbutton = Checkbutton(romOptionsFrame, text="Disable flashing (anti-epilepsy)",
variable=vars.disableFlashingVar)
disableFlashingCheckbutton.grid(row=6, column=0, sticky=W)
vars.DeathLinkVar = IntVar(value=0)
DeathLinkCheckbutton = Checkbutton(romOptionsFrame, text="DeathLink (Team Deaths)", variable=vars.DeathLinkVar)
DeathLinkCheckbutton.grid(row=7, column=0, sticky=W)
spriteDialogFrame = Frame(romOptionsFrame)
spriteDialogFrame.grid(row=0, column=1)
baseSpriteLabel = Label(spriteDialogFrame, text='Sprite:')
vars.spriteNameVar = StringVar()
vars.sprite = None
def set_sprite(sprite_param):
nonlocal vars
if isinstance(sprite_param, str):
@@ -491,7 +529,8 @@ def get_rom_options_frame(parent=None):
menuspeedLabel.pack(side=LEFT)
vars.menuspeedVar = StringVar()
vars.menuspeedVar.set('normal')
menuspeedOptionMenu = OptionMenu(menuspeedFrame, vars.menuspeedVar, 'normal', 'instant', 'double', 'triple', 'quadruple', 'half')
menuspeedOptionMenu = OptionMenu(menuspeedFrame, vars.menuspeedVar, 'normal', 'instant', 'double', 'triple',
'quadruple', 'half')
menuspeedOptionMenu.pack(side=LEFT)
heartcolorFrame = Frame(romOptionsFrame)
@@ -518,7 +557,8 @@ def get_rom_options_frame(parent=None):
owPalettesLabel.pack(side=LEFT)
vars.owPalettesVar = StringVar()
vars.owPalettesVar.set('default')
owPalettesOptionMenu = OptionMenu(owPalettesFrame, vars.owPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
owPalettesOptionMenu = OptionMenu(owPalettesFrame, vars.owPalettesVar, 'default', 'good', 'blackout', 'grayscale',
'negative', 'classic', 'dizzy', 'sick', 'puke')
owPalettesOptionMenu.pack(side=LEFT)
uwPalettesFrame = Frame(romOptionsFrame)
@@ -527,7 +567,8 @@ def get_rom_options_frame(parent=None):
uwPalettesLabel.pack(side=LEFT)
vars.uwPalettesVar = StringVar()
vars.uwPalettesVar.set('default')
uwPalettesOptionMenu = OptionMenu(uwPalettesFrame, vars.uwPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
uwPalettesOptionMenu = OptionMenu(uwPalettesFrame, vars.uwPalettesVar, 'default', 'good', 'blackout', 'grayscale',
'negative', 'classic', 'dizzy', 'sick', 'puke')
uwPalettesOptionMenu.pack(side=LEFT)
hudPalettesFrame = Frame(romOptionsFrame)
@@ -536,7 +577,8 @@ def get_rom_options_frame(parent=None):
hudPalettesLabel.pack(side=LEFT)
vars.hudPalettesVar = StringVar()
vars.hudPalettesVar.set('default')
hudPalettesOptionMenu = OptionMenu(hudPalettesFrame, vars.hudPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
hudPalettesOptionMenu = OptionMenu(hudPalettesFrame, vars.hudPalettesVar, 'default', 'good', 'blackout',
'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
hudPalettesOptionMenu.pack(side=LEFT)
swordPalettesFrame = Frame(romOptionsFrame)
@@ -545,7 +587,8 @@ def get_rom_options_frame(parent=None):
swordPalettesLabel.pack(side=LEFT)
vars.swordPalettesVar = StringVar()
vars.swordPalettesVar.set('default')
swordPalettesOptionMenu = OptionMenu(swordPalettesFrame, vars.swordPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
swordPalettesOptionMenu = OptionMenu(swordPalettesFrame, vars.swordPalettesVar, 'default', 'good', 'blackout',
'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
swordPalettesOptionMenu.pack(side=LEFT)
shieldPalettesFrame = Frame(romOptionsFrame)
@@ -554,7 +597,8 @@ def get_rom_options_frame(parent=None):
shieldPalettesLabel.pack(side=LEFT)
vars.shieldPalettesVar = StringVar()
vars.shieldPalettesVar.set('default')
shieldPalettesOptionMenu = OptionMenu(shieldPalettesFrame, vars.shieldPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
shieldPalettesOptionMenu = OptionMenu(shieldPalettesFrame, vars.shieldPalettesVar, 'default', 'good', 'blackout',
'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
shieldPalettesOptionMenu.pack(side=LEFT)
spritePoolFrame = Frame(romOptionsFrame)
@@ -563,6 +607,7 @@ def get_rom_options_frame(parent=None):
vars.spritePoolCountVar = StringVar()
vars.sprite_pool = []
def set_sprite_pool(sprite_param):
nonlocal vars
operation = "add"
@@ -632,8 +677,10 @@ class SpriteSelector():
title_link.pack(side=LEFT)
title_link.bind("<Button-1>", open_custom_sprite_dir)
self.icon_section(alttpr_frametitle, self.alttpr_sprite_dir, 'ALTTPR sprites not found. Click "Update alttpr sprites" to download them.')
self.icon_section(custom_frametitle, self.custom_sprite_dir, 'Put sprites in the custom sprites folder (see open link above) to have them appear here.')
self.icon_section(alttpr_frametitle, self.alttpr_sprite_dir,
'ALTTPR sprites not found. Click "Update alttpr sprites" to download them.')
self.icon_section(custom_frametitle, self.custom_sprite_dir,
'Put sprites in the custom sprites folder (see open link above) to have them appear here.')
if not randomOnEvent:
self.sprite_pool_section(spritePool)
@@ -683,7 +730,8 @@ class SpriteSelector():
button = Checkbutton(frame, text="Bonk", command=self.update_random_button, variable=self.randomOnBonkVar)
button.pack(side=LEFT, padx=(0, 5))
button = Checkbutton(frame, text="Random", command=self.update_random_button, variable=self.randomOnRandomVar)
button = Checkbutton(frame, text="Random", command=self.update_random_button,
variable=self.randomOnRandomVar)
button.pack(side=LEFT, padx=(0, 5))
if adjuster:
@@ -805,7 +853,6 @@ class SpriteSelector():
BackgroundTaskProgress(self.parent, update_sprites, "Updating Sprites", on_finish)
def browse_for_sprite(self):
sprite = filedialog.askopenfilename(
filetypes=[("All Sprite Sources", (".zspr", ".spr", ".sfc", ".smc")),
@@ -819,7 +866,6 @@ class SpriteSelector():
self.callback(None)
self.window.destroy()
def use_default_sprite(self):
self.callback(None)
self.window.destroy()
@@ -923,7 +969,8 @@ def get_image_for_sprite(sprite, gif_only: bool = False):
gif_lsd = bytearray(7)
gif_lsd[0] = width
gif_lsd[2] = height
gif_lsd[4] = 0xF4 # 32 color palette follows. transparant + 15 for sprite + 1 for shadow=17 which rounds up to 32 as nearest power of 2
gif_lsd[
4] = 0xF4 # 32 color palette follows. transparant + 15 for sprite + 1 for shadow=17 which rounds up to 32 as nearest power of 2
gif_lsd[5] = 0 # background color is zero
gif_lsd[6] = 0 # aspect raio not specified
gif_gct = bytearray(3 * 32)
@@ -943,7 +990,8 @@ def get_image_for_sprite(sprite, gif_only: bool = False):
gif_id[7] = height
gif_id[9] = 0 # no local color table
gif_img_minimum_code_size = bytes([7]) # we choose 7 bits, so that each pixel is represented by a byte, for conviennce.
gif_img_minimum_code_size = bytes(
[7]) # we choose 7 bits, so that each pixel is represented by a byte, for conviennce.
clear = 0x80
stop = 0x81
@@ -1100,5 +1148,6 @@ class ToolTips(object):
widget.after_cancel(cls.after_id)
cls.after_id = None
if __name__ == '__main__':
main()
main()

19
Main.py
View File

@@ -34,7 +34,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
output_path.cached_path = args.outputpath
start = time.perf_counter()
# initialize the world
world = MultiWorld(args.multi)
@@ -59,7 +58,8 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
world.boss_shuffle = args.shufflebosses.copy()
world.enemy_health = args.enemy_health.copy()
world.enemy_damage = args.enemy_damage.copy()
world.beemizer = args.beemizer.copy()
world.beemizer_total_chance = args.beemizer_total_chance.copy()
world.beemizer_trap_chance = args.beemizer_trap_chance.copy()
world.timer = args.timer.copy()
world.countdown_start_time = args.countdown_start_time.copy()
world.red_clock_time = args.red_clock_time.copy()
@@ -92,7 +92,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
if not cls.hidden:
logger.info(f" {name:{longest_name}}: {len(cls.item_names):3} Items | "
f"{len(cls.location_names):3} Locations")
logger.info(f" Item IDs: {min(cls.item_id_to_name):{numlength}} - "
logger.info(f" Item IDs: {min(cls.item_id_to_name):{numlength}} - "
f"{max(cls.item_id_to_name):{numlength}} | "
f"Location IDs: {min(cls.location_id_to_name):{numlength}} - "
f"{max(cls.location_id_to_name):{numlength}}")
@@ -151,7 +151,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
AutoWorld.call_all(world, "pre_fill")
logger.info('Fill the world.')
logger.info(f'Filling the world with {len(world.itempool)} items.')
if world.algorithm == 'flood':
flood_items(world) # different algo, biased towards early game progress items
@@ -197,8 +197,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
er_hint_data[region.player][location.address] = main_entrance.name
checks_in_area = {player: {area: list() for area in ordered_areas}
for player in range(1, world.players + 1)}
@@ -215,6 +213,10 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
'Inverted Ganons Tower': 'Ganons Tower'} \
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
checks_in_area[location.player][dungeonname].append(location.address)
elif location.parent_region.type == RegionType.LightWorld:
checks_in_area[location.player]["Light World"].append(location.address)
elif location.parent_region.type == RegionType.DarkWorld:
checks_in_area[location.player]["Dark World"].append(location.address)
elif main_entrance.parent_region.type == RegionType.LightWorld:
checks_in_area[location.player]["Light World"].append(location.address)
elif main_entrance.parent_region.type == RegionType.DarkWorld:
@@ -248,7 +250,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
import NetUtils
slot_data = {}
client_versions = {}
minimum_versions = {"server": (0, 1, 1), "clients": client_versions}
minimum_versions = {"server": (0, 1, 8), "clients": client_versions}
games = {}
for slot in world.player_ids:
client_versions[slot] = world.worlds[slot].get_required_client_version()
@@ -319,8 +321,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
logger.warning("Location Accessibility requirements not fulfilled.")
# retrieve exceptions via .result() if they occured.
if multidata_task:
multidata_task.result()
multidata_task.result()
for i, future in enumerate(concurrent.futures.as_completed(output_file_futures), start=1):
if i % 10 == 0 or i == len(output_file_futures):
logger.info(f'Generating output files ({i}/{len(output_file_futures)}).')

View File

@@ -4,8 +4,8 @@ import re
import atexit
from subprocess import Popen
from shutil import copyfile
from base64 import b64decode
from time import strftime
import logging
import requests
@@ -15,6 +15,7 @@ atexit.register(input, "Press enter to exit.")
# 1 or more digits followed by m or g, then optional b
max_heap_re = re.compile(r"^\d+[mMgG][bB]?$")
forge_version = "1.17.1-37.1.1"
def prompt_yes_no(prompt):
@@ -30,29 +31,18 @@ def prompt_yes_no(prompt):
print('Please respond with "y" or "n".')
# Find Forge jar file; raise error if not found
def find_forge_jar(forge_dir):
for entry in os.scandir(forge_dir):
if ".jar" in entry.name and "forge" in entry.name:
print(f"Found forge .jar: {entry.name}")
return entry.name
raise FileNotFoundError(f"Could not find forge .jar in {forge_dir}.")
# Create mods folder if needed; find AP randomizer jar; return None if not found.
def find_ap_randomizer_jar(forge_dir):
mods_dir = os.path.join(forge_dir, 'mods')
if os.path.isdir(mods_dir):
ap_mod_re = re.compile(r"^aprandomizer-[\d\.]+\.jar$")
for entry in os.scandir(mods_dir):
match = ap_mod_re.match(entry.name)
if match:
print(f"Found AP randomizer mod: {match.group()}")
return match.group()
if entry.name.startswith("aprandomizer") and entry.name.endswith(".jar"):
logging.info(f"Found AP randomizer mod: {entry.name}")
return entry.name
return None
else:
os.mkdir(mods_dir)
print(f"Created mods folder in {forge_dir}")
logging.info(f"Created mods folder in {forge_dir}")
return None
@@ -64,52 +54,73 @@ def replace_apmc_files(forge_dir, apmc_file):
copy_apmc = True
if not os.path.isdir(apdata_dir):
os.mkdir(apdata_dir)
print(f"Created APData folder in {forge_dir}")
logging.info(f"Created APData folder in {forge_dir}")
for entry in os.scandir(apdata_dir):
if entry.name.endswith(".apmc") and entry.is_file():
if not os.path.samefile(apmc_file, entry.path):
os.remove(entry.path)
print(f"Removed {entry.name} in {apdata_dir}")
logging.info(f"Removed {entry.name} in {apdata_dir}")
else: # apmc already in apdata
copy_apmc = False
if copy_apmc:
copyfile(apmc_file, os.path.join(apdata_dir, os.path.basename(apmc_file)))
print(f"Copied {os.path.basename(apmc_file)} to {apdata_dir}")
logging.info(f"Copied {os.path.basename(apmc_file)} to {apdata_dir}")
def read_apmc_file(apmc_file):
from base64 import b64decode
import json
with open(apmc_file, 'r') as f:
data = json.loads(b64decode(f.read()))
return data
# Check mod version, download new mod from GitHub releases page if needed.
def update_mod(forge_dir):
def update_mod(forge_dir, apmc_file, get_prereleases=False):
ap_randomizer = find_ap_randomizer_jar(forge_dir)
if apmc_file is not None:
data = read_apmc_file(apmc_file)
minecraft_version = data.get('minecraft_version', '')
client_releases_endpoint = "https://api.github.com/repos/KonoTyran/Minecraft_AP_Randomizer/releases"
resp = requests.get(client_releases_endpoint)
if resp.status_code == 200: # OK
latest_release = resp.json()[0]
if ap_randomizer != latest_release['assets'][0]['name']:
print(f"A new release of the Minecraft AP randomizer mod was found: {latest_release['assets'][0]['name']}")
if ap_randomizer is not None:
print(f"Your current mod is {ap_randomizer}.")
else:
print(f"You do not have the AP randomizer mod installed.")
if prompt_yes_no("Would you like to update?"):
old_ap_mod = os.path.join(forge_dir, 'mods', ap_randomizer) if ap_randomizer is not None else None
new_ap_mod = os.path.join(forge_dir, 'mods', latest_release['assets'][0]['name'])
print("Downloading AP randomizer mod. This may take a moment...")
apmod_resp = requests.get(latest_release['assets'][0]['browser_download_url'])
if apmod_resp.status_code == 200:
with open(new_ap_mod, 'wb') as f:
f.write(apmod_resp.content)
print(f"Wrote new mod file to {new_ap_mod}")
if old_ap_mod is not None:
os.remove(old_ap_mod)
print(f"Removed old mod file from {old_ap_mod}")
try:
latest_release = next(filter(lambda release: (not release['prerelease'] or get_prereleases) and
(apmc_file is None or minecraft_version in release['assets'][0]['name']),
resp.json()))
if ap_randomizer != latest_release['assets'][0]['name']:
logging.info(f"A new release of the Minecraft AP randomizer mod was found: "
f"{latest_release['assets'][0]['name']}")
if ap_randomizer is not None:
logging.info(f"Your current mod is {ap_randomizer}.")
else:
print(f"Error retrieving the randomizer mod (status code {apmod_resp.status_code}).")
print(f"Please report this issue on the Archipelago Discord server.")
sys.exit(1)
logging.info(f"You do not have the AP randomizer mod installed.")
if prompt_yes_no("Would you like to update?"):
old_ap_mod = os.path.join(forge_dir, 'mods', ap_randomizer) if ap_randomizer is not None else None
new_ap_mod = os.path.join(forge_dir, 'mods', latest_release['assets'][0]['name'])
logging.info("Downloading AP randomizer mod. This may take a moment...")
apmod_resp = requests.get(latest_release['assets'][0]['browser_download_url'])
if apmod_resp.status_code == 200:
with open(new_ap_mod, 'wb') as f:
f.write(apmod_resp.content)
logging.info(f"Wrote new mod file to {new_ap_mod}")
if old_ap_mod is not None:
os.remove(old_ap_mod)
logging.info(f"Removed old mod file from {old_ap_mod}")
else:
logging.error(f"Error retrieving the randomizer mod (status code {apmod_resp.status_code}).")
logging.error(f"Please report this issue on the Archipelago Discord server.")
sys.exit(1)
except StopIteration:
logging.warning(f"No compatible mod version found for {minecraft_version}.")
if not prompt_yes_no("Run server anyway?"):
sys.exit(0)
else:
print(f"Error checking for randomizer mod updates (status code {resp.status_code}).")
print(f"If this was not expected, please report this issue on the Archipelago Discord server.")
logging.error(f"Error checking for randomizer mod updates (status code {resp.status_code}).")
logging.error(f"If this was not expected, please report this issue on the Archipelago Discord server.")
if not prompt_yes_no("Continue anyways?"):
sys.exit(0)
@@ -127,22 +138,80 @@ def check_eula(forge_dir):
text = f.read()
if 'false' in text:
# Prompt user to agree to the EULA
print("You need to agree to the Minecraft EULA in order to run the server.")
print("The EULA can be found at https://account.mojang.com/documents/minecraft_eula")
logging.info("You need to agree to the Minecraft EULA in order to run the server.")
logging.info("The EULA can be found at https://account.mojang.com/documents/minecraft_eula")
if prompt_yes_no("Do you agree to the EULA?"):
f.seek(0)
f.write(text.replace('false', 'true'))
f.truncate()
print(f"Set {eula_path} to true")
logging.info(f"Set {eula_path} to true")
else:
sys.exit(0)
# Run the Forge server. Return process object
def run_forge_server(forge_dir, heap_arg):
forge_server = find_forge_jar(forge_dir)
# get the current JDK16
def find_jdk_dir() -> str:
for entry in os.listdir():
if os.path.isdir(entry) and entry.startswith("jdk16"):
return os.path.abspath(entry)
java_exe = os.path.abspath(os.path.join('jre8', 'bin', 'java.exe'))
# get the java exe location
def find_jdk() -> str:
jdk = find_jdk_dir()
jdk_exe = os.path.join(jdk, "bin", "java.exe")
if os.path.isfile(jdk_exe):
return jdk_exe
# Download Corretto 16 (Amazon JDK)
def download_java():
jdk = find_jdk_dir()
if jdk is not None:
print(f"Removing old JDK...")
from shutil import rmtree
rmtree(jdk)
print(f"Downloading Java...")
jdk_url = "https://corretto.aws/downloads/latest/amazon-corretto-16-x64-windows-jdk.zip"
resp = requests.get(jdk_url)
if resp.status_code == 200: # OK
print(f"Extracting...")
import zipfile
from io import BytesIO
with zipfile.ZipFile(BytesIO(resp.content)) as zf:
zf.extractall()
else:
print(f"Error downloading Java (status code {resp.status_code}).")
print(f"If this was not expected, please report this issue on the Archipelago Discord server.")
if not prompt_yes_no("Continue anyways?"):
sys.exit(0)
# download and install forge
def install_forge(directory: str):
jdk = find_jdk()
if jdk is not None:
print(f"Downloading Forge {forge_version}...")
forge_url = f"https://maven.minecraftforge.net/net/minecraftforge/forge/{forge_version}/forge-{forge_version}-installer.jar"
resp = requests.get(forge_url)
if resp.status_code == 200: # OK
forge_install_jar = os.path.join(directory, "forge_install.jar")
if not os.path.exists(directory):
os.mkdir(directory)
with open(forge_install_jar, 'wb') as f:
f.write(resp.content)
print(f"Installing Forge...")
argstring = ' '.join([jdk, "-jar", "\"" + forge_install_jar+ "\"", "--installServer", "\"" + directory + "\""])
install_process = Popen(argstring)
install_process.wait()
os.remove(forge_install_jar)
# Run the Forge server. Return process object
def run_forge_server(forge_dir: str, heap_arg):
java_exe = find_jdk()
if not os.path.isfile(java_exe):
java_exe = "java" # try to fall back on java in the PATH
@@ -151,15 +220,26 @@ def run_forge_server(forge_dir, heap_arg):
heap_arg = heap_arg[:-1]
heap_arg = "-Xmx" + heap_arg
argstring = ' '.join([java_exe, heap_arg, "-jar", forge_server, "-nogui"])
print(f"Running Forge server: {argstring}")
args_file = os.path.join(forge_dir, "libraries", "net", "minecraftforge", "forge", forge_version, "win_args.txt")
win_args = []
with open(args_file) as argfile:
for line in argfile:
win_args.append(line.strip())
argstring = ' '.join([java_exe, heap_arg] + win_args + ["-nogui"])
logging.info(f"Running Forge server: {argstring}")
os.chdir(forge_dir)
return Popen(argstring)
if __name__ == '__main__':
Utils.init_logging("MinecraftClient")
parser = argparse.ArgumentParser()
parser.add_argument("apmc_file", default=None, nargs='?', help="Path to an Archipelago Minecraft data file (.apmc)")
parser.add_argument('--install', '-i', dest='install', default=False, action='store_true',
help="Download and install Java and the Forge server. Does not launch the client afterwards.")
parser.add_argument('--prerelease', default=False, action='store_true',
help="Auto-update prerelease versions.")
args = parser.parse_args()
apmc_file = os.path.abspath(args.apmc_file) if args.apmc_file else None
@@ -171,6 +251,12 @@ if __name__ == '__main__':
forge_dir = options["minecraft_options"]["forge_directory"]
max_heap = options["minecraft_options"]["max_heap_size"]
if args.install:
print("Installing Java and Minecraft Forge")
download_java()
install_forge(forge_dir)
sys.exit(0)
if apmc_file is not None and not os.path.isfile(apmc_file):
raise FileNotFoundError(f"Path {apmc_file} does not exist or could not be accessed.")
if not os.path.isdir(forge_dir):
@@ -178,7 +264,7 @@ if __name__ == '__main__':
if not max_heap_re.match(max_heap):
raise Exception(f"Max heap size {max_heap} in incorrect format. Use a number followed by M or G, e.g. 512M or 2G.")
update_mod(forge_dir)
update_mod(forge_dir, apmc_file, args.prerelease)
replace_apmc_files(forge_dir, apmc_file)
check_eula(forge_dir)
server_process = run_forge_server(forge_dir, max_heap)

View File

@@ -35,18 +35,25 @@ def update(yes = False, force = False):
if not os.path.exists(path):
path = os.path.join(os.path.dirname(__file__), req_file)
with open(path) as requirementsfile:
requirements = pkg_resources.parse_requirements(requirementsfile)
for requirement in requirements:
requirement = str(requirement)
try:
pkg_resources.require(requirement)
except pkg_resources.ResolutionError:
if not yes:
import traceback
traceback.print_exc()
input(f'Requirement {requirement} is not satisfied, press enter to install it')
update_command()
return
for line in requirementsfile:
if line.startswith('https://'):
# extract name and version from url
url = line.split(';')[0]
wheel = line.split('/')[-1]
name, version, _ = wheel.split('-',2)
line = f'{name}=={version}'
requirements = pkg_resources.parse_requirements(line)
for requirement in requirements:
requirement = str(requirement)
try:
pkg_resources.require(requirement)
except pkg_resources.ResolutionError:
if not yes:
import traceback
traceback.print_exc()
input(f'Requirement {requirement} is not satisfied, press enter to install it')
update_command()
return
if __name__ == "__main__":

View File

@@ -13,18 +13,19 @@ import datetime
import threading
import random
import pickle
import itertools
import time
import ModuleUpdate
import NetUtils
ModuleUpdate.update()
import websockets
import colorama
import prompt_toolkit
from prompt_toolkit.patch_stdout import patch_stdout
from fuzzywuzzy import process as fuzzy_process
import NetUtils
from worlds.AutoWorld import AutoWorldRegister
proxy_worlds = {name: world(None, 0) for name, world in AutoWorldRegister.world_types.items()}
@@ -44,7 +45,6 @@ class Client(Endpoint):
def __init__(self, socket: websockets.WebSocketServerProtocol, ctx: Context):
super().__init__(socket)
self.auth = False
self.name = None
self.team = None
self.slot = None
self.send_index = 0
@@ -52,6 +52,13 @@ class Client(Endpoint):
self.messageprocessor = client_message_processor(ctx, self)
self.ctx = weakref.ref(ctx)
@property
def name(self) -> str:
ctx = self.ctx()
if ctx:
return ctx.player_names[self.team, self.slot]
return "Deallocated"
team_slot = typing.Tuple[int, int]
@@ -66,15 +73,21 @@ class Context:
"password": str,
"forfeit_mode": str,
"remaining_mode": str,
"collect_mode": str,
"item_cheat": bool,
"compatibility": int}
# team -> slot id -> list of clients authenticated to slot.
clients: typing.Dict[int, typing.Dict[int, typing.List[Client]]]
locations: typing.Dict[int, typing.Dict[int, typing.Tuple[int, int]]]
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
hint_cost: int, item_cheat: bool, forfeit_mode: str = "disabled", remaining_mode: str = "disabled",
auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2, log_network: bool = False):
hint_cost: int, item_cheat: bool, forfeit_mode: str = "disabled", collect_mode="disabled",
remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2,
log_network: bool = False):
super(Context, self).__init__()
self.log_network = log_network
self.endpoints = []
self.clients = {}
self.compatibility: int = compatibility
self.shutdown_task = None
self.data_filename = None
@@ -86,7 +99,8 @@ class Context:
self.allow_forfeits = {}
self.remote_items = set()
self.remote_start_inventory = set()
self.locations: typing.Dict[int, typing.Dict[int, typing.Tuple[int, int]]] = {}
# player location_id item_id target_player_id
self.locations = {}
self.host = host
self.port = port
self.server_password = server_password
@@ -102,8 +116,9 @@ class Context:
self.hints: typing.Dict[team_slot, typing.Set[NetUtils.Hint]] = collections.defaultdict(set)
self.forfeit_mode: str = forfeit_mode
self.remaining_mode: str = remaining_mode
self.collect_mode: str = collect_mode
self.item_cheat = item_cheat
self.running = True
self.exit_event = asyncio.Event()
self.client_activity_timers: typing.Dict[
team_slot, datetime.datetime] = {} # datetime of last new item check
self.client_connection_timers: typing.Dict[
@@ -166,23 +181,25 @@ class Context:
logging.info(f"Outgoing broadcast: {msg}")
return True
def broadcast_all(self, msgs):
def broadcast_all(self, msgs: typing.List[dict]):
msgs = self.dumper(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: int, msgs):
def broadcast_team(self, team: int, msgs: typing.List[dict]):
msgs = self.dumper(msgs)
endpoints = (endpoint for endpoint in self.endpoints if endpoint.auth and endpoint.team == team)
endpoints = (endpoint for endpoint in itertools.chain.from_iterable(self.clients[team].values()))
asyncio.create_task(self.broadcast_send_encoded_msgs(endpoints, msgs))
def broadcast(self, endpoints: typing.Iterable[Endpoint], msgs):
def broadcast(self, endpoints: typing.Iterable[Client], msgs: typing.List[dict]):
msgs = self.dumper(msgs)
asyncio.create_task(self.broadcast_send_encoded_msgs(endpoints, msgs))
async def disconnect(self, endpoint):
async def disconnect(self, endpoint: Client):
if endpoint in self.endpoints:
self.endpoints.remove(endpoint)
if endpoint.slot and endpoint in self.clients[endpoint.team][endpoint.slot]:
self.clients[endpoint.team][endpoint.slot].remove(endpoint)
await on_client_disconnected(self, endpoint)
# text
@@ -239,8 +256,11 @@ class Context:
for player, version in clients_ver.items():
self.minimum_client_versions[player] = Utils.Version(*version)
self.clients = {}
for team, names in enumerate(decoded_obj['names']):
self.clients[team] = {}
for player, name in enumerate(names, 1):
self.clients[team][player] = []
self.player_names[team, player] = name
self.player_name_lookup[name] = team, player
self.seed_name = decoded_obj["seed_name"]
@@ -260,6 +280,11 @@ class Context:
self.received_items[team, slot] = [NetworkItem(item_code, -2, 0) for item_code in item_codes]
for slot, hints in decoded_obj["precollected_hints"].items():
self.hints[team, slot].update(hints)
# declare slots without checks as done, as they're assumed to be spectators
for slot, locations in self.locations.items():
if not locations:
for team in self.clients:
self.client_game_state[team, slot] = ClientStatus.CLIENT_GOAL
if use_embedded_server_options:
server_options = decoded_obj.get("server_options", {})
self._set_options(server_options)
@@ -310,7 +335,7 @@ class Context:
if not self.auto_saver_thread:
def save_regularly():
import time
while self.running:
while not self.exit_event.is_set():
time.sleep(self.auto_save_interval)
if self.save_dirty:
logging.debug("Saving via thread.")
@@ -405,6 +430,17 @@ class Context:
else:
return self.player_names[team, slot]
def on_goal_achieved(self, client: Client):
finished_msg = f'{self.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1})' \
f' has completed their goal.'
self.notify_all(finished_msg)
if "auto" in self.forfeit_mode:
forfeit_player(self, client.team, client.slot)
elif proxy_worlds[self.games[client.slot]].forced_auto_forfeit:
forfeit_player(self, client.team, client.slot)
if "auto" in self.collect_mode:
collect_player(self, client.team, client.slot)
def notify_hints(ctx: Context, team: int, hints: typing.List[NetUtils.Hint]):
concerns = collections.defaultdict(list)
@@ -416,25 +452,23 @@ def notify_hints(ctx: Context, team: int, hints: typing.List[NetUtils.Hint]):
for text in (format_hint(ctx, team, hint) for hint in hints):
logging.info("Notice (Team #%d): %s" % (team + 1, text))
for client in ctx.endpoints:
if client.auth and client.team == team:
client_hints = concerns[client.slot]
if client_hints:
for slot, clients in ctx.clients[team].items():
client_hints = concerns[slot]
if client_hints:
for client in clients:
asyncio.create_task(ctx.send_msgs(client, client_hints))
def update_aliases(ctx: Context, team: int, client: typing.Optional[Client] = None):
def update_aliases(ctx: Context, team: int):
cmd = ctx.dumper([{"cmd": "RoomUpdate",
"players": ctx.get_players_package()}])
if client is None:
for client in ctx.endpoints:
if client.team == team and client.auth:
asyncio.create_task(ctx.send_encoded_msgs(client, cmd))
else:
asyncio.create_task(ctx.send_encoded_msgs(client, cmd))
for clients in ctx.clients[team].values():
for client in clients:
asyncio.create_task(ctx.send_encoded_msgs(client, cmd))
async def server(websocket, path, ctx: Context):
async def server(websocket, path: str = "/", ctx: Context = None):
client = Client(websocket, ctx)
ctx.endpoints.append(client)
@@ -459,27 +493,32 @@ async def server(websocket, path, ctx: Context):
async def on_client_connected(ctx: Context, client: Client):
players = []
for team, clients in ctx.clients.items():
for slot, connected_clients in clients.items():
if connected_clients:
name = ctx.player_names[team, slot]
players.append(
NetworkPlayer(team, slot,
ctx.name_aliases.get((team, slot), name), name)
)
await ctx.send_msgs(client, [{
'cmd': 'RoomInfo',
'password': bool(ctx.password),
'players': [
NetworkPlayer(client.team, client.slot, ctx.name_aliases.get((client.team, client.slot), client.name),
client.name) for client
in ctx.endpoints if client.auth],
'players': players,
'games': [ctx.games[x] for x in range(1, len(ctx.games) + 1)],
# tags are for additional features in the communication.
# 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"],
'datapackage_versions': {game: game_data["version"] for game, game_data
in network_data_package["games"].items()},
'seed_name': ctx.seed_name
'seed_name': ctx.seed_name,
'time': time.time(),
}])
@@ -487,6 +526,7 @@ def get_permissions(ctx) -> typing.Dict[str, Permission]:
return {
"forfeit": Permission.from_text(ctx.forfeit_mode),
"remaining": Permission.from_text(ctx.remaining_mode),
"collect": Permission.from_text(ctx.collect_mode)
}
@@ -498,14 +538,11 @@ async def on_client_disconnected(ctx: Context, client: Client):
async def on_client_joined(ctx: Context, client: Client):
update_client_status(ctx, client, ClientStatus.CLIENT_CONNECTED)
version_str = '.'.join(str(x) for x in client.version)
verb = "tracking" if "Tracker" in client.tags else "playing"
ctx.notify_all(
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) "
f"playing {ctx.games[client.slot]} has joined. "
f"{verb} {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)
@@ -549,27 +586,61 @@ def get_players_string(ctx: Context):
return f'{len(auth_clients)} players of {len(ctx.player_names)} connected ' + text[:-1]
def get_status_string(ctx: Context, team: int):
text = "Player Status on your team:"
for slot in ctx.locations:
connected = len(ctx.clients[team][slot])
death_link = len([client for client in ctx.clients[team][slot] if "DeathLink" in client.tags])
completion_text = f"({len(ctx.location_checks[team, slot])}/{len(ctx.locations[slot])})"
death_text = f" {death_link} of which are death link" if connected else ""
goal_text = " and has finished." if ctx.client_game_state[team, slot] == ClientStatus.CLIENT_GOAL else "."
text += f"\n{ctx.get_aliased_name(team, slot)} has {connected} connection{'' if connected == 1 else 's'}" \
f"{death_text}{goal_text} {completion_text}"
return text
def get_received_items(ctx: Context, team: int, player: int) -> typing.List[NetworkItem]:
return ctx.received_items.setdefault((team, player), [])
def send_new_items(ctx: Context):
for client in ctx.endpoints:
if client.auth: # can't send to disconnected client
items = get_received_items(ctx, client.team, client.slot)
if len(items) > client.send_index:
asyncio.create_task(ctx.send_msgs(client, [{
"cmd": "ReceivedItems",
"index": client.send_index,
"items": items[client.send_index:]}]))
client.send_index = len(items)
for team, clients in ctx.clients.items():
for slot, clients in clients.items():
items = get_received_items(ctx, team, slot)
for client in clients:
if len(items) > client.send_index:
asyncio.create_task(ctx.send_msgs(client, [{
"cmd": "ReceivedItems",
"index": client.send_index,
"items": items[client.send_index:]}]))
client.send_index = len(items)
def update_checked_locations(ctx: Context, team: int, slot: int):
ctx.broadcast(ctx.clients[team][slot],
[{"cmd": "RoomUpdate", "checked_locations": get_checked_checks(ctx, team, slot)}])
def forfeit_player(ctx: Context, team: int, slot: int):
# register any locations that are in the multidata
"""register any locations that are in the multidata"""
all_locations = set(ctx.locations[slot])
ctx.notify_all("%s (Team #%d) has forfeited" % (ctx.player_names[(team, slot)], team + 1))
register_location_checks(ctx, team, slot, all_locations)
update_checked_locations(ctx, team, slot)
def collect_player(ctx: Context, team: int, slot: int):
"""register any locations that are in the multidata, pointing towards this player"""
all_locations = collections.defaultdict(set)
for source_slot, location_data in ctx.locations.items():
for location_id, (item_id, target_player_id) in location_data.items():
if target_player_id == slot:
all_locations[source_slot].add(location_id)
ctx.notify_all("%s (Team #%d) has collected" % (ctx.player_names[(team, slot)], team + 1))
for source_player, location_ids in all_locations.items():
register_location_checks(ctx, team, source_player, location_ids)
update_checked_locations(ctx, team, source_player)
def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[int]:
@@ -582,27 +653,28 @@ def get_remaining(ctx: Context, team: int, slot: int) -> typing.List[int]:
def register_location_checks(ctx: Context, team: int, slot: int, locations: typing.Iterable[int]):
new_locations = set(locations) - ctx.location_checks[team, slot]
new_locations.intersection_update(ctx.locations[slot]) # ignore location IDs unknown to this multidata
if new_locations:
ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc)
for location in new_locations:
if location in ctx.locations[slot]:
item_id, target_player = ctx.locations[slot][location]
new_item = NetworkItem(item_id, location, slot)
if target_player != slot or slot in ctx.remote_items:
get_received_items(ctx, team, target_player).append(new_item)
item_id, target_player = ctx.locations[slot][location]
new_item = NetworkItem(item_id, location, slot)
if target_player != slot or slot in ctx.remote_items:
get_received_items(ctx, team, target_player).append(new_item)
logging.info('(Team #%d) %s sent %s to %s (%s)' % (
team + 1, ctx.player_names[(team, slot)], get_item_name_from_id(item_id),
ctx.player_names[(team, target_player)], get_location_name_from_id(location)))
info_text = json_format_send_event(new_item, target_player)
ctx.broadcast_team(team, [info_text])
logging.info('(Team #%d) %s sent %s to %s (%s)' % (
team + 1, ctx.player_names[(team, slot)], get_item_name_from_id(item_id),
ctx.player_names[(team, target_player)], get_location_name_from_id(location)))
info_text = json_format_send_event(new_item, target_player)
ctx.broadcast_team(team, [info_text])
ctx.location_checks[team, slot] |= new_locations
send_new_items(ctx)
for client in ctx.endpoints:
if client.team == team and client.slot == slot:
asyncio.create_task(ctx.send_msgs(client, [{"cmd": "RoomUpdate",
"hint_points": get_client_points(ctx, client)}]))
ctx.broadcast(ctx.clients[team][slot], [{
"cmd": "RoomUpdate",
"hint_points": get_slot_points(ctx, team, slot),
"checked_locations": new_locations, # send back new checks only
}])
ctx.save()
@@ -652,15 +724,15 @@ def json_format_send_event(net_item: NetworkItem, receiving_player: int):
NetUtils.add_json_text(parts, net_item.player, type=NetUtils.JSONTypes.player_id)
if net_item.player == receiving_player:
NetUtils.add_json_text(parts, " found their ")
NetUtils.add_json_text(parts, net_item.item, type=NetUtils.JSONTypes.item_id)
NetUtils.add_json_item(parts, net_item.item, net_item.player)
else:
NetUtils.add_json_text(parts, " sent ")
NetUtils.add_json_text(parts, net_item.item, type=NetUtils.JSONTypes.item_id)
NetUtils.add_json_item(parts, net_item.item, receiving_player)
NetUtils.add_json_text(parts, " to ")
NetUtils.add_json_text(parts, receiving_player, type=NetUtils.JSONTypes.player_id)
NetUtils.add_json_text(parts, " (")
NetUtils.add_json_text(parts, net_item.location, type=NetUtils.JSONTypes.location_id)
NetUtils.add_json_location(parts, net_item.location, net_item.player)
NetUtils.add_json_text(parts, ")")
return {"cmd": "PrintJSON", "data": parts, "type": "ItemSend",
@@ -874,6 +946,11 @@ class ClientMessageProcessor(CommonCommandProcessor):
self.output(get_players_string(self.ctx))
return True
def _cmd_status(self) -> bool:
"""Get status information about your team."""
self.output(get_status_string(self.ctx, self.client.team))
return True
def _cmd_forfeit(self) -> bool:
"""Surrender and send your remaining items out to their recipients"""
if self.ctx.allow_forfeits.get((self.client.team, self.client.slot), False):
@@ -896,6 +973,25 @@ class ClientMessageProcessor(CommonCommandProcessor):
" You can ask the server admin for a /forfeit")
return False
def _cmd_collect(self) -> bool:
"""Send your remaining items to yourself"""
if "enabled" in self.ctx.collect_mode:
collect_player(self.ctx, self.client.team, self.client.slot)
return True
elif "disabled" in self.ctx.collect_mode:
self.output(
"Sorry, client collecting has been disabled on this server. You can ask the server admin for a /collect")
return False
else: # is auto or goal
if self.ctx.client_game_state[self.client.team, self.client.slot] == ClientStatus.CLIENT_GOAL:
collect_player(self.ctx, self.client.team, self.client.slot)
return True
else:
self.output(
"Sorry, client collecting requires you to have beaten the game on this server."
" You can ask the server admin for a /collect")
return False
def _cmd_remaining(self) -> bool:
"""List remaining items in your game, but not their location or recipient"""
if self.ctx.remaining_mode == "enabled":
@@ -927,7 +1023,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
def _cmd_missing(self) -> bool:
"""List all missing location checks from the server's perspective"""
locations = get_missing_checks(self.ctx, self.client)
locations = get_missing_checks(self.ctx, self.client.team, self.client.slot)
if locations:
texts = [f'Missing: {get_location_name_from_id(location)}' for location in locations]
@@ -937,6 +1033,19 @@ class ClientMessageProcessor(CommonCommandProcessor):
self.output("No missing location checks found.")
return True
def _cmd_checked(self) -> bool:
"""List all done location checks from the server's perspective"""
locations = get_checked_checks(self.ctx, self.client.team, self.client.slot)
if locations:
texts = [f'Checked: {get_location_name_from_id(location)}' for location in locations]
texts.append(f"Found {len(locations)} done location checks")
self.ctx.notify_client_multiple(self.client, texts)
else:
self.output("No done location checks found.")
return True
@mark_raw
def _cmd_alias(self, alias_name: str = ""):
"""Set your alias to the passed name."""
@@ -989,7 +1098,8 @@ class ClientMessageProcessor(CommonCommandProcessor):
return True
else:
world = proxy_worlds[self.ctx.games[self.client.slot]]
item_name, usable, response = get_intended_text(input_text, world.all_names if not explicit_location else world.location_names)
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.")
@@ -1072,22 +1182,22 @@ class ClientMessageProcessor(CommonCommandProcessor):
@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.
for example !hint_location atomic-bomb to get a spoiler peek for that location.
(In the case of factorio, or any other game where item names and location names are identical,
this command must be used explicitly.)"""
return self.get_hints(location, True)
def get_checked_checks(ctx: Context, client: Client) -> typing.List[int]:
def get_checked_checks(ctx: Context, team: int, slot: int) -> typing.List[int]:
return [location_id for
location_id in ctx.locations[client.slot] if
location_id in ctx.location_checks[client.team, client.slot]]
location_id in ctx.locations[slot] if
location_id in ctx.location_checks[team, slot]]
def get_missing_checks(ctx: Context, client: Client) -> typing.List[int]:
def get_missing_checks(ctx: Context, team: int, slot: int) -> typing.List[int]:
return [location_id for
location_id in ctx.locations[client.slot] if
location_id not in ctx.location_checks[client.team, client.slot]]
location_id in ctx.locations[slot] if
location_id not in ctx.location_checks[team, slot]]
def get_client_points(ctx: Context, client: Client) -> int:
@@ -1095,24 +1205,30 @@ def get_client_points(ctx: Context, client: Client) -> int:
ctx.get_hint_cost(client.slot) * ctx.hints_used[client.team, client.slot])
def get_slot_points(ctx: Context, team: int, slot: int) -> int:
return (ctx.location_check_points * len(ctx.location_checks[team, slot]) -
ctx.get_hint_cost(slot) * ctx.hints_used[team, slot])
async def process_client_cmd(ctx: Context, client: Client, args: dict):
try:
cmd: str = args["cmd"]
except:
logging.exception(f"Could not get command from {args}")
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "cmd",
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "cmd", "original_cmd": None,
"text": f"Could not get command from {args} at `cmd`"}])
raise
if type(cmd) is not str:
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "cmd",
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "cmd", "original_cmd": None,
"text": f"Command should be str, got {type(cmd)}"}])
return
if cmd == 'Connect':
if not args or 'password' not in args or type(args['password']) not in [str, type(None)] or \
'game' not in args:
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", 'text': 'Connect'}])
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", 'text': 'Connect',
"original_cmd": cmd}])
return
errors = set()
@@ -1127,26 +1243,9 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
game = ctx.games[slot]
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:
# likely same player with a "ghosted" slot. We bust the ghost.
if "uuid" in args and ctx.client_ids[team, slot] == args["uuid"]:
await ctx.send_msgs(clients[0], [{"cmd": "Print", "text": "You are getting kicked "
"by yourself reconnecting."}])
await clients[0].socket.close() # we have to await the DC of the ghost, so not to create data pasta
client.name = ctx.player_names[(team, slot)]
client.team = team
client.slot = slot
else:
errors.add('SlotAlreadyTaken')
else:
client.name = ctx.player_names[(team, slot)]
client.team = team
client.slot = slot
minver = ctx.minimum_client_versions[slot]
if minver > args['version']:
errors.add('IncompatibleVersion')
minver = ctx.minimum_client_versions[slot]
if minver > args['version']:
errors.add('IncompatibleVersion')
# only exact version match allowed
if ctx.compatibility == 0 and args['version'] != version_tuple:
@@ -1155,30 +1254,40 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
logging.info(f"A client connection was refused due to: {errors}, the sent connect information was {args}.")
await ctx.send_msgs(client, [{"cmd": "ConnectionRefused", "errors": list(errors)}])
else:
team, slot = ctx.connect_names[args['name']]
if client.auth and client.team is not None and client.slot in ctx.clients[client.team]:
ctx.clients[team][slot].remove(client) # re-auth, remove old entry
if client.team != team or client.slot != slot:
client.auth = False # swapping Team/Slot
client.team = team
client.slot = slot
ctx.client_ids[client.team, client.slot] = args["uuid"]
client.auth = True
ctx.clients[team][slot].append(client)
client.version = args['version']
client.tags = args['tags']
reply = [{
"cmd": "Connected",
"team": client.team, "slot": client.slot,
"players": ctx.get_players_package(),
"missing_locations": get_missing_checks(ctx, client),
"checked_locations": get_checked_checks(ctx, client),
# get is needed for old multidata that was sparsely populated
"slot_data": ctx.slot_data.get(client.slot, {})
"missing_locations": get_missing_checks(ctx, team, slot),
"checked_locations": get_checked_checks(ctx, team, slot),
"slot_data": ctx.slot_data[client.slot]
}]
items = get_received_items(ctx, client.team, client.slot)
if items:
reply.append({"cmd": 'ReceivedItems', "index": 0, "items": items})
client.send_index = len(items)
if not client.auth: # if this was a Re-Connect, don't print to console
client.auth = True
await on_client_joined(ctx, client)
await ctx.send_msgs(client, reply)
await on_client_joined(ctx, client)
elif cmd == "GetDataPackage":
exclusions = set(args.get("exclusions", []))
exclusions = args.get("exclusions", [])
if exclusions:
exclusions = set(exclusions)
games = {name: game_data for name, game_data in network_data_package["games"].items()
if name not in exclusions}
package = network_data_package.copy()
@@ -1188,8 +1297,23 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
else:
await ctx.send_msgs(client, [{"cmd": "DataPackage",
"data": network_data_package}])
elif client.auth:
if cmd == 'Sync':
if cmd == "ConnectUpdate":
if not args:
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", 'text': cmd,
"original_cmd": cmd}])
return
if "tags" in args:
old_tags = client.tags
client.tags = args["tags"]
if set(old_tags) != set(client.tags):
ctx.notify_all(
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has changed tags "
f"from {old_tags} to {client.tags}.")
elif cmd == 'Sync':
items = get_received_items(ctx, client.team, client.slot)
if items:
client.send_index = len(items)
@@ -1197,14 +1321,20 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
"items": items}])
elif cmd == 'LocationChecks':
register_location_checks(ctx, client.team, client.slot, args["locations"])
if "Tracker" in client.tags:
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "cmd",
"text": "Trackers can't register new Location Checks",
"original_cmd": cmd}])
else:
register_location_checks(ctx, client.team, client.slot, args["locations"])
elif cmd == 'LocationScouts':
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'}])
[{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts',
"original_cmd": cmd}])
return
target_item, target_player = ctx.locations[client.slot][location]
locs.append(NetworkItem(target_item, location, target_player))
@@ -1216,7 +1346,8 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
elif cmd == 'Say':
if "text" not in args or type(args["text"]) is not str or not args["text"].isprintable():
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'Say'}])
await ctx.send_msgs(client, [{'cmd': 'InvalidPacket', "type": "arguments", "text": 'Say',
"original_cmd": cmd}])
return
client.messageprocessor(args["text"])
@@ -1239,12 +1370,7 @@ def update_client_status(ctx: Context, client: Client, new_status: ClientStatus)
current = ctx.client_game_state[client.team, client.slot]
if current != ClientStatus.CLIENT_GOAL: # can't undo goal completion
if new_status == ClientStatus.CLIENT_GOAL:
finished_msg = f'{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has completed their goal.'
ctx.notify_all(finished_msg)
if "auto" in ctx.forfeit_mode:
forfeit_player(ctx, client.team, client.slot)
elif proxy_worlds[ctx.games[client.slot]].forced_auto_forfeit:
forfeit_player(ctx, client.team, client.slot)
ctx.on_goal_achieved(client)
ctx.client_game_state[client.team, client.slot] = new_status
@@ -1262,20 +1388,6 @@ class ServerCommandProcessor(CommonCommandProcessor):
def default(self, raw: str):
self.ctx.notify_all('[Server]: ' + raw)
@mark_raw
def _cmd_kick(self, player_name: str) -> bool:
"""Kick specified player from the server"""
for client in self.ctx.endpoints:
if client.auth and client.name.lower() == player_name.lower() and client.socket and not client.socket.closed:
asyncio.create_task(client.socket.close())
self.output(f"Kicked {self.ctx.get_aliased_name(client.team, client.slot)}")
if self.ctx.commandprocessor.client == client:
self.ctx.commandprocessor.client = None
return True
self.output(f"Could not find player {player_name} to kick")
return False
def _cmd_save(self) -> bool:
"""Save current state to multidata"""
if self.ctx.saving:
@@ -1296,7 +1408,7 @@ class ServerCommandProcessor(CommonCommandProcessor):
asyncio.create_task(self.ctx.server.ws_server._close())
if self.ctx.shutdown_task:
self.ctx.shutdown_task.cancel()
self.ctx.running = False
self.ctx.exit_event.set()
return True
@mark_raw
@@ -1324,9 +1436,21 @@ class ServerCommandProcessor(CommonCommandProcessor):
self.output(response)
return False
@mark_raw
def _cmd_collect(self, player_name: str) -> bool:
"""Send out the remaining items to player."""
seeked_player = player_name.lower()
for (team, slot), name in self.ctx.player_names.items():
if name.lower() == seeked_player:
collect_player(self.ctx, team, slot)
return True
self.output(f"Could not find player {player_name} to collect")
return False
@mark_raw
def _cmd_forfeit(self, player_name: str) -> bool:
"""Send out the remaining items from a player's game to their intended recipients"""
"""Send out the remaining items from a player to their intended recipients"""
seeked_player = player_name.lower()
for (team, slot), name in self.ctx.player_names.items():
if name.lower() == seeked_player:
@@ -1430,7 +1554,7 @@ 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"}:
if option_name in {"forfeit_mode", "remaining_mode", "collect_mode"}:
self.ctx.broadcast_all([{"cmd": "RoomUpdate", 'permissions': get_permissions(self.ctx)}])
return True
else:
@@ -1441,11 +1565,17 @@ class ServerCommandProcessor(CommonCommandProcessor):
async def console(ctx: Context):
session = prompt_toolkit.PromptSession()
while ctx.running:
with patch_stdout():
input_text = await session.prompt_async()
import sys
queue = asyncio.Queue()
Utils.stream_input(sys.stdin, queue)
while not ctx.exit_event.is_set():
try:
# I don't get why this while loop is needed. Works fine without it on clients,
# but the queue.get() for server never fulfills if the queue is empty when entering the await.
while queue.qsize() == 0:
await asyncio.sleep(0.05)
input_text = await queue.get()
queue.task_done()
ctx.commandprocessor(input_text)
except:
import traceback
@@ -1476,6 +1606,15 @@ def parse_args() -> argparse.Namespace:
goal: !forfeit can be used after goal completion
auto-enabled: !forfeit is available and automatically triggered on goal completion
''')
parser.add_argument('--collect_mode', default=defaults["collect_mode"], nargs='?',
choices=['auto', 'enabled', 'disabled', "goal", "auto-enabled"], help='''\
Select !collect Accessibility. (default: %(default)s)
auto: Automatic "collect" on goal completion
enabled: !collect is always available
disabled: !collect is never available
goal: !collect can be used after goal completion
auto-enabled: !collect is available and automatically triggered on goal completion
''')
parser.add_argument('--remaining_mode', default=defaults["remaining_mode"], nargs='?',
choices=['enabled', 'disabled', "goal"], help='''\
Select !remaining Accessibility. (default: %(default)s)
@@ -1502,10 +1641,10 @@ def parse_args() -> argparse.Namespace:
async def auto_shutdown(ctx, to_cancel=None):
await asyncio.sleep(ctx.auto_shutdown)
while ctx.running:
while not ctx.exit_event.is_set():
if not ctx.client_activity_timers.values():
asyncio.create_task(ctx.server.ws_server._close())
ctx.running = False
ctx.exit_event.set()
if to_cancel:
for task in to_cancel:
task.cancel()
@@ -1516,7 +1655,7 @@ async def auto_shutdown(ctx, to_cancel=None):
seconds = ctx.auto_shutdown - delta.total_seconds()
if seconds < 0:
asyncio.create_task(ctx.server.ws_server._close())
ctx.running = False
ctx.exit_event.set()
if to_cancel:
for task in to_cancel:
task.cancel()
@@ -1526,11 +1665,11 @@ async def auto_shutdown(ctx, to_cancel=None):
async def main(args: argparse.Namespace):
logging.basicConfig(force=True,
format='[%(asctime)s] %(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
Utils.init_logging("Server", loglevel=args.loglevel.lower())
ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points,
args.hint_cost, not args.disable_item_cheat, args.forfeit_mode, args.remaining_mode,
args.hint_cost, not args.disable_item_cheat, args.forfeit_mode, args.collect_mode,
args.remaining_mode,
args.auto_shutdown, args.compatibility, args.log_network)
data_filename = args.multidata
@@ -1550,7 +1689,7 @@ async def main(args: argparse.Namespace):
ctx.init_save(not args.disable_save)
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ping_timeout=None,
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), host=ctx.host, port=ctx.port, ping_timeout=None,
ping_interval=None)
ip = args.host if args.host else Utils.get_public_ipv4()
logging.info('Hosting game at %s:%d (%s)' % (ip, ctx.port,
@@ -1560,7 +1699,8 @@ async def main(args: argparse.Namespace):
console_task = asyncio.create_task(console(ctx))
if ctx.auto_shutdown:
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, [console_task]))
await console_task
await ctx.exit_event.wait()
console_task.cancel()
if ctx.shutdown_task:
await ctx.shutdown_task

View File

@@ -1,4 +1,6 @@
from __future__ import annotations
import logging
import typing
import enum
from json import JSONEncoder, JSONDecoder
@@ -13,8 +15,8 @@ class JSONMessagePart(typing.TypedDict, total=False):
# optional
type: str
color: str
# mainly for items, optional
found: bool
# owning player for location/item
player: int
class ClientStatus(enum.IntEnum):
@@ -62,7 +64,7 @@ def _scan_for_TypedTuples(obj: typing.Any) -> typing.Any:
data = obj._asdict()
data["class"] = obj.__class__.__name__
return data
if isinstance(obj, (tuple, list)):
if isinstance(obj, (tuple, list, set)):
return tuple(_scan_for_TypedTuples(o) for o in obj)
if isinstance(obj, dict):
return {key: _scan_for_TypedTuples(value) for key, value in obj.items()}
@@ -111,7 +113,6 @@ def _object_hook(o: typing.Any) -> typing.Any:
decode = JSONDecoder(object_hook=_object_hook).decode
class Endpoint:
socket: websockets.WebSocketServerProtocol
@@ -139,11 +140,11 @@ class HandlerMeta(type):
break
def __init__(self, *args, **kwargs):
if orig_init:
orig_init(self, *args, **kwargs)
# turn functions into bound methods
self.handlers = {name: method.__get__(self, type(self)) for name, method in
handlers.items()}
if orig_init:
orig_init(self, *args, **kwargs)
attrs['__init__'] = __init__
return super(HandlerMeta, mcs).__new__(mcs, name, bases, attrs)
@@ -193,11 +194,7 @@ class JSONtoTextParser(metaclass=HandlerMeta):
return self._handle_color(node)
def _handle_item_name(self, node: JSONMessagePart):
# todo: use a better info source
from worlds.alttp.Items import progression_items
node["color"] = 'green' if node.get("found", False) else 'cyan'
if node["text"] in progression_items:
node["color"] += ";white_bg"
node["color"] = 'cyan'
return self._handle_color(node)
def _handle_item_id(self, node: JSONMessagePart):
@@ -206,13 +203,13 @@ class JSONtoTextParser(metaclass=HandlerMeta):
return self._handle_item_name(node)
def _handle_location_name(self, node: JSONMessagePart):
node["color"] = 'blue_bg;white'
node["color"] = 'green'
return self._handle_color(node)
def _handle_location_id(self, node: JSONMessagePart):
item_id = int(node["text"])
node["text"] = self.ctx.location_name_getter(item_id)
return self._handle_item_name(node)
return self._handle_location_name(node)
def _handle_entrance_name(self, node: JSONMessagePart):
node["color"] = 'blue'
@@ -241,6 +238,14 @@ def add_json_text(parts: list, text: typing.Any, **kwargs) -> None:
parts.append({"text": str(text), **kwargs})
def add_json_item(parts: list, item_id: int, player: int = 0, **kwargs) -> None:
parts.append({"text": str(item_id), "player": player, "type": JSONTypes.item_id, **kwargs})
def add_json_location(parts: list, item_id: int, player: int = 0, **kwargs) -> None:
parts.append({"text": str(item_id), "player": player, "type": JSONTypes.location_id, **kwargs})
class Hint(typing.NamedTuple):
receiving_player: int
finding_player: int
@@ -265,9 +270,9 @@ class Hint(typing.NamedTuple):
add_json_text(parts, "[Hint]: ")
add_json_text(parts, self.receiving_player, type="player_id")
add_json_text(parts, "'s ")
add_json_text(parts, self.item, type="item_id", found=self.found)
add_json_item(parts, self.item, self.receiving_player)
add_json_text(parts, " is at ")
add_json_text(parts, self.location, type="location_id")
add_json_location(parts, self.location, self.finding_player)
add_json_text(parts, " in ")
add_json_text(parts, self.finding_player, type="player_id")
if self.entrance:
@@ -275,14 +280,16 @@ class Hint(typing.NamedTuple):
add_json_text(parts, self.entrance, type="entrance_name")
else:
add_json_text(parts, "'s World")
add_json_text(parts, ". ")
if self.found:
add_json_text(parts, ". (found)")
add_json_text(parts, "(found)", type="color", color="green")
else:
add_json_text(parts, ".")
add_json_text(parts, "(not found)", type="color", color="red")
return {"cmd": "PrintJSON", "data": parts, "type": "Hint",
"receiving": self.receiving_player,
"item": NetworkItem(self.item, self.location, self.finding_player)}
"item": NetworkItem(self.item, self.location, self.finding_player),
"found": self.found}
@property
def local(self):

237
OoTAdjuster.py Normal file
View File

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

View File

@@ -139,10 +139,8 @@ class Choice(Option):
@classmethod
def from_text(cls, text: str) -> Choice:
text = text.lower()
# TODO: turn on after most people have adjusted their yamls to no longer have suboptions with "random" in them
# maybe in 0.2?
# if text == "random":
# return cls(random.choice(list(cls.options.values())))
if text == "random":
return cls(random.choice(list(cls.name_lookup)))
for optionname, value in cls.options.items():
if optionname == text:
return cls(value)
@@ -264,13 +262,23 @@ class OptionDict(Option):
return item in self.value
class ItemDict(OptionDict):
# implemented by Generate
verify_item_name = True
def __init__(self, value: typing.Dict[str, int]):
if any(item_count < 1 for item_count in value.values()):
raise Exception("Cannot have non-positive item counts.")
super(ItemDict, self).__init__(value)
class OptionList(Option):
default = []
supports_weighting = False
value: list
def __init__(self, value: typing.List[str, typing.Any]):
self.value = value
def __init__(self, value: typing.List[typing.Any]):
self.value = value or []
super(OptionList, self).__init__()
@classmethod
@@ -284,7 +292,7 @@ class OptionList(Option):
return cls.from_text(str(data))
def get_option_name(self, value):
return ", ".join(value)
return ", ".join(map(str, value))
def __contains__(self, item):
return item in self.value
@@ -359,7 +367,7 @@ class NonLocalItems(ItemSet):
displayname = "Not Local Items"
class StartInventory(OptionDict):
class StartInventory(ItemDict):
"""Start with these items."""
verify_item_name = True
displayname = "Start Inventory"
@@ -371,6 +379,7 @@ class StartHints(ItemSet):
class StartLocationHints(OptionSet):
"""Start with these locations and their item prefilled into the !hint command"""
displayname = "Start Location Hints"
@@ -380,13 +389,18 @@ class ExcludeLocations(OptionSet):
verify_location_name = True
class DeathLink(Toggle):
"""When you die, everyone dies. Of course the reverse is true too."""
displayname = "Death Link"
per_game_common_options = {
"local_items": LocalItems,
"non_local_items": NonLocalItems,
"start_inventory": StartInventory,
"start_hints": StartHints,
"start_location_hints": StartLocationHints,
"exclude_locations": OptionSet
"exclude_locations": ExcludeLocations
}
if __name__ == "__main__":

View File

@@ -1,3 +1,5 @@
# TODO: convert this into a system like AutoWorld
import bsdiff4
import yaml
import os
@@ -10,54 +12,88 @@ from typing import Tuple, Optional
import Utils
current_patch_version = 3
current_patch_version = 2
GAME_ALTTP = "A Link to the Past"
GAME_SM = "Super Metroid"
GAME_SOE = "Secret of Evermore"
supported_games = {"A Link to the Past", "Super Metroid", "Secret of Evermore"}
preferred_endings = {
GAME_ALTTP: "apbp",
GAME_SM: "apm3",
GAME_SOE: "apsoe"
}
def generate_yaml(patch: bytes, metadata: Optional[dict] = None) -> bytes:
from worlds.alttp.Rom import JAP10HASH
def generate_yaml(patch: bytes, metadata: Optional[dict] = None, game: str = GAME_ALTTP) -> bytes:
if game == GAME_ALTTP:
from worlds.alttp.Rom import JAP10HASH as HASH
elif game == GAME_SM:
from worlds.sm.Rom import JAP10HASH as HASH
elif game == GAME_SOE:
from worlds.soe.Patch import USHASH as HASH
else:
raise RuntimeError(f"Selected game {game} for base rom not found.")
patch = yaml.dump({"meta": metadata,
"patch": patch,
"game": "A Link to the Past",
"game": game,
# minimum version of patch system expected for patching to be successful
"compatible_version": 1,
"compatible_version": 3,
"version": current_patch_version,
"base_checksum": JAP10HASH})
"base_checksum": HASH})
return patch.encode(encoding="utf-8-sig")
def generate_patch(rom: bytes, metadata: Optional[dict] = None) -> bytes:
from worlds.alttp.Rom import get_base_rom_bytes
def generate_patch(rom: bytes, metadata: Optional[dict] = None, game: str = GAME_ALTTP) -> bytes:
if metadata is None:
metadata = {}
patch = bsdiff4.diff(get_base_rom_bytes(), rom)
return generate_yaml(patch, metadata)
patch = bsdiff4.diff(get_base_rom_data(game), rom)
return generate_yaml(patch, metadata, game)
def create_patch_file(rom_file_to_patch: str, server: str = "", destination: str = None,
player: int = 0, player_name: str = "") -> str:
meta = {"server": server, # allow immediate connection to server in multiworld. Empty string otherwise
player: int = 0, player_name: str = "", game: str = GAME_ALTTP) -> str:
meta = {"server": server, # allow immediate connection to server in multiworld. Empty string otherwise
"player_id": player,
"player_name": player_name}
bytes = generate_patch(load_bytes(rom_file_to_patch),
meta)
target = destination if destination else os.path.splitext(rom_file_to_patch)[0] + ".apbp"
meta,
game)
target = destination if destination else os.path.splitext(rom_file_to_patch)[0] + (
".apbp" if game == GAME_ALTTP else ".apm3")
write_lzma(bytes, target)
return target
def create_rom_bytes(patch_file: str, ignore_version: bool = False) -> Tuple[dict, str, bytearray]:
from worlds.alttp.Rom import get_base_rom_bytes
data = Utils.parse_yaml(lzma.decompress(load_bytes(patch_file)).decode("utf-8-sig"))
game_name = data["game"]
if not ignore_version and data["compatible_version"] > current_patch_version:
raise RuntimeError("Patch file is incompatible with this patcher, likely an update is required.")
patched_data = bsdiff4.patch(get_base_rom_bytes(), data["patch"])
patched_data = bsdiff4.patch(get_base_rom_data(game_name), data["patch"])
rom_hash = patched_data[int(0x7FC0):int(0x7FD5)]
data["meta"]["hash"] = "".join(chr(x) for x in rom_hash)
target = os.path.splitext(patch_file)[0] + ".sfc"
return data["meta"], target, patched_data
def get_base_rom_data(game: str):
if game == GAME_ALTTP:
from worlds.alttp.Rom import get_base_rom_bytes
elif game == "alttp": # old version for A Link to the Past
from worlds.alttp.Rom import get_base_rom_bytes
elif game == GAME_SM:
from worlds.sm.Rom import get_base_rom_bytes
elif game == GAME_SOE:
file_name = Utils.get_options()["soe_options"]["rom_file"]
get_base_rom_bytes = lambda: bytes(read_rom(open(file_name, "rb")))
else:
raise RuntimeError("Selected game for base rom not found.")
return get_base_rom_bytes()
def create_rom_file(patch_file: str) -> Tuple[dict, str]:
data, target, patched_data = create_rom_bytes(patch_file)
with open(target, "wb") as f:
@@ -68,7 +104,7 @@ def create_rom_file(patch_file: str) -> Tuple[dict, str]:
def update_patch_data(patch_data: bytes, server: str = "") -> bytes:
data = Utils.parse_yaml(lzma.decompress(patch_data).decode("utf-8-sig"))
data["meta"]["server"] = server
bytes = generate_yaml(data["patch"], data["meta"])
bytes = generate_yaml(data["patch"], data["meta"], data["game"])
return lzma.compress(bytes)
@@ -82,6 +118,14 @@ def write_lzma(data: bytes, path: str):
f.write(data)
def read_rom(stream, strip_header=True) -> bytearray:
"""Reads rom into bytearray and optionally strips off any smc header"""
buffer = bytearray(stream.read())
if strip_header and len(buffer) % 0x400 == 0x200:
return buffer[0x200:]
return buffer
if __name__ == "__main__":
host = Utils.get_public_ipv4()
options = Utils.get_options()['server_options']
@@ -113,7 +157,13 @@ if __name__ == "__main__":
if 'server' in data:
Utils.persistent_store("servers", data['hash'], data['server'])
print(f"Host is {data['server']}")
elif rom.endswith(".apm3"):
print(f"Applying patch {rom}")
data, target = create_rom_file(rom)
print(f"Created rom {target}.")
if 'server' in data:
Utils.persistent_store("servers", data['hash'], data['server'])
print(f"Host is {data['server']}")
elif rom.endswith(".archipelago"):
import json
import zlib
@@ -139,7 +189,7 @@ if __name__ == "__main__":
def _handle_zip_file_entry(zfinfo: zipfile.ZipInfo, server: str):
data = zfr.read(zfinfo)
if zfinfo.filename.endswith(".apbp"):
if zfinfo.filename.endswith(".apbp") or zfinfo.filename.endswith(".apm3"):
data = update_patch_data(data, server)
with ziplock:
zfw.writestr(zfinfo, data)
@@ -160,12 +210,4 @@ if __name__ == "__main__":
import traceback
traceback.print_exc()
input("Press enter to close.")
def read_rom(stream, strip_header=True) -> bytearray:
"""Reads rom into bytearray and optionally strips off any smc header"""
buffer = bytearray(stream.read())
if strip_header and len(buffer) % 0x400 == 0x200:
return buffer[0x200:]
return buffer
input("Press enter to close.")

View File

@@ -11,8 +11,11 @@ Currently, the following games are supported:
* Risk of Rain 2
* The Legend of Zelda: Ocarina of Time
* Timespinner
* Super Metroid
* Secret of Evermore
* Final Fantasy
For setup and instructions check out our [tutorials page](/tutorial).
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
windows binaries.
@@ -34,7 +37,7 @@ Archipelago was directly forked from bonta0's `multiworld_31` branch of ALttPEnt
## Running Archipelago
For most people all you need to do is head over to the [releases](https://github.com/ArchipelagoMW/Archipelago/releases) page then download and run the appropriate installer. The installers function on Windows only.
If you are running Archipelago from a non-Windows system then the likely scenario is that you are comfortable running source code directly. Please see our wiki page on [running Archipelago from source](https://github.com/Berserker66/MultiWorld-Utilities/wiki/Running-from-source).
If you are running Archipelago from a non-Windows system then the likely scenario is that you are comfortable running source code directly. Please see our wiki page on [running Archipelago from source](https://github.com/ArchipelagoMW/Archipelago/wiki/Running-from-source).
## Related Repositories
This project makes use of multiple other projects. We wouldn't be here without these other repositories and the contributions of their developers, past and present.

View File

@@ -1,6 +1,6 @@
import argparse
import atexit
exit_func = atexit.register(input, "Press enter to close.")
from __future__ import annotations
import sys
import threading
import time
import multiprocessing
@@ -12,29 +12,37 @@ import logging
import asyncio
from json import loads, dumps
from Utils import get_item_name_from_id
from Utils import get_item_name_from_id, init_logging
import ModuleUpdate
ModuleUpdate.update()
if __name__ == "__main__":
init_logging("SNIClient", exception_logger="Client")
import colorama
from NetUtils import *
from worlds.alttp import Regions, Shops
from worlds.alttp import Items
from worlds.alttp.Rom import ROM_PLAYER_LIMIT
from worlds.sm.Rom import ROM_PLAYER_LIMIT as SM_ROM_PLAYER_LIMIT
import Utils
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, gui_enabled, init_logging
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, gui_enabled, get_base_parser
from Patch import GAME_ALTTP, GAME_SM
init_logging("LttPClient")
snes_logger = logging.getLogger("SNES")
from MultiServer import mark_raw
class DeathState(enum.IntEnum):
killing_player = 1
alive = 2
dead = 3
class LttPCommandProcessor(ClientCommandProcessor):
ctx: Context
def _cmd_slow_mode(self, toggle: str = ""):
"""Toggle slow mode, which limits how fast you send / receive items."""
if toggle:
@@ -46,17 +54,18 @@ class LttPCommandProcessor(ClientCommandProcessor):
@mark_raw
def _cmd_snes(self, snes_options: str = "") -> bool:
"""Connect to a snes. Optionally include network address of a snes to connect to, otherwise show available devices; and a SNES device number if more than one SNES is detected"""
"""Connect to a snes. Optionally include network address of a snes to connect to,
otherwise show available devices; and a SNES device number if more than one SNES is detected"""
snes_address = self.ctx.snes_address
snes_device_number = -1
options = snes_options.split()
num_options = len(options)
if num_options > 0:
snes_address = options[0]
if num_options > 1:
try:
snes_device_number = int(options[1])
@@ -64,7 +73,7 @@ class LttPCommandProcessor(ClientCommandProcessor):
pass
self.ctx.snes_reconnect_address = None
asyncio.create_task(snes_connect(self.ctx, snes_address, snes_device_number))
asyncio.create_task(snes_connect(self.ctx, snes_address, snes_device_number), name="SNES Connect")
return True
def _cmd_snes_close(self) -> bool:
@@ -76,6 +85,19 @@ class LttPCommandProcessor(ClientCommandProcessor):
else:
return False
# Left here for quick re-addition for debugging.
# def _cmd_snes_write(self, address, data):
# """Write the specified byte (base10) to the SNES' memory address (base16)."""
# if self.ctx.snes_state != SNESState.SNES_ATTACHED:
# self.output("No attached SNES Device.")
# return False
#
# snes_buffered_write(self.ctx, int(address, 16), bytes([int(data)]))
# asyncio.create_task(snes_flush_writes(self.ctx))
# self.output("Data Sent")
# return True
class Context(CommonContext):
command_processor = LttPCommandProcessor
game = "A Link to the Past"
@@ -93,6 +115,8 @@ class Context(CommonContext):
self.snes_request_lock = asyncio.Lock()
self.snes_write_buffer = []
self.snes_connector_lock = threading.Lock()
self.death_state = DeathState.alive # for death link flop behaviour
self.killing_player_task = None
self.awaiting_rom = False
self.rom = None
@@ -108,7 +132,7 @@ class Context(CommonContext):
raise Exception('Invalid ROM detected, '
'please verify that you have loaded the correct rom and reconnect your snes (/snes)')
async def server_auth(self, password_requested):
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(Context, self).server_auth(password_requested)
if self.rom is None:
@@ -119,11 +143,55 @@ class Context(CommonContext):
self.awaiting_rom = False
self.auth = self.rom
auth = base64.b64encode(self.rom).decode()
await self.send_msgs([{"cmd": 'Connect',
'password': self.password, 'name': auth, 'version': Utils.version_tuple,
'tags': get_tags(self),
'uuid': Utils.get_unique_identifier(), 'game': "A Link to the Past"
}])
await self.send_connect(name=auth)
def on_deathlink(self, data: dict):
if not self.killing_player_task or self.killing_player_task.done():
self.killing_player_task = asyncio.create_task(deathlink_kill_player(self))
super(Context, self).on_deathlink(data)
async def handle_deathlink_state(self, currently_dead: bool):
# in this state we only care about triggering a death send
if self.death_state == DeathState.alive:
if currently_dead:
self.death_state = DeathState.dead
await self.send_death()
# in this state we care about confirming a kill, to move state to dead
elif self.death_state == DeathState.killing_player:
# this is being handled in deathlink_kill_player(ctx) already
pass
# in this state we wait until the player is alive again
elif self.death_state == DeathState.dead:
if not currently_dead:
self.death_state = DeathState.alive
async def deathlink_kill_player(ctx: Context):
ctx.death_state = DeathState.killing_player
while ctx.death_state == DeathState.killing_player and \
ctx.snes_state == SNESState.SNES_ATTACHED:
if ctx.game == GAME_ALTTP:
snes_buffered_write(ctx, WRAM_START + 0xF36D, bytes([0])) # set current health to 0
snes_buffered_write(ctx, WRAM_START + 0x0373, bytes([8])) # deal 1 full heart of damage at next opportunity
elif ctx.game == GAME_SM:
snes_buffered_write(ctx, WRAM_START + 0x09C2, bytes([0, 0])) # set current health to 0
if not ctx.death_link_allow_survive:
snes_buffered_write(ctx, WRAM_START + 0x09D6, bytes([0, 0])) # set current reserve to 0
await snes_flush_writes(ctx)
await asyncio.sleep(1)
gamemode = None
if ctx.game == GAME_ALTTP:
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
if not gamemode or gamemode[0] in DEATH_MODES:
ctx.death_state = DeathState.dead
elif ctx.game == GAME_SM:
gamemode = await snes_read(ctx, WRAM_START + 0x0998, 1)
health = await snes_read(ctx, WRAM_START + 0x09C2, 2)
if health is not None:
health = health[0] | (health[1] << 8)
if not gamemode or gamemode[0] in SM_DEATH_MODES or (ctx.death_link_allow_survive and health is not None and health > 0):
ctx.death_state = DeathState.dead
ctx.last_death_link = time.time()
def color_item(item_id: int, green: bool = False) -> str:
@@ -136,6 +204,7 @@ def color_item(item_id: int, green: bool = False) -> str:
SNES_RECONNECT_DELAY = 5
# LttP
ROM_START = 0x000000
WRAM_START = 0xF50000
WRAM_SIZE = 0x20000
@@ -146,6 +215,7 @@ ROMNAME_SIZE = 0x15
INGAME_MODES = {0x07, 0x09, 0x0b}
ENDGAME_MODES = {0x19, 0x1a}
DEATH_MODES = {0x12}
SAVEDATA_START = WRAM_START + 0xF000
SAVEDATA_SIZE = 0x500
@@ -161,6 +231,21 @@ SCOUTREPLY_ITEM_ADDR = SAVEDATA_START + 0x4D9 # 1 byte
SCOUTREPLY_PLAYER_ADDR = SAVEDATA_START + 0x4DA # 1 byte
SHOP_ADDR = SAVEDATA_START + 0x302 # 2 bytes
DEATH_LINK_ACTIVE_ADDR = ROMNAME_START + 0x15 # 1 byte
# SM
SM_ROMNAME_START = 0x1C4F00
SM_INGAME_MODES = {0x07, 0x09, 0x0b}
SM_ENDGAME_MODES = {0x26, 0x27}
SM_DEATH_MODES = {0x15, 0x17, 0x18, 0x19, 0x1A}
SM_RECV_PROGRESS_ADDR = SRAM_START + 0x2000 # 2 bytes
SM_RECV_ITEM_ADDR = SAVEDATA_START + 0x4D2 # 1 byte
SM_RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3 # 1 byte
SM_DEATH_LINK_ACTIVE_ADDR = ROM_START + 0x277f04 # 1 byte
location_shop_ids = set([info[0] for name, info in Shops.shop_table.items()])
location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
@@ -384,7 +469,7 @@ location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
'Ganons Tower - Pre-Moldorm Chest': (0x3d, 0x40),
'Ganons Tower - Validation Chest': (0x4d, 0x10)}
location_table_uw_id = {Regions.lookup_name_to_id[name] : data for name, data in location_table_uw.items()}
location_table_uw_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_uw.items()}
location_table_npc = {'Mushroom': 0x1000,
'King Zora': 0x2,
@@ -400,7 +485,7 @@ location_table_npc = {'Mushroom': 0x1000,
'Stumpy': 0x8,
'Bombos Tablet': 0x200}
location_table_npc_id = {Regions.lookup_name_to_id[name] : data for name, data in location_table_npc.items()}
location_table_npc_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_npc.items()}
location_table_ow = {'Flute Spot': 0x2a,
'Sunken Treasure': 0x3b,
@@ -415,14 +500,15 @@ location_table_ow = {'Flute Spot': 0x2a,
'Bumper Cave Ledge': 0x4a,
'Floating Island': 0x5}
location_table_ow_id = {Regions.lookup_name_to_id[name] : data for name, data in location_table_ow.items()}
location_table_ow_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_ow.items()}
location_table_misc = {'Bottle Merchant': (0x3c9, 0x2),
'Purple Chest': (0x3c9, 0x10),
"Link's Uncle": (0x3c6, 0x1),
'Hobo': (0x3c9, 0x1)}
location_table_misc_id = {Regions.lookup_name_to_id[name] : data for name, data in location_table_misc.items()}
location_table_misc_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_misc.items()}
class SNESState(enum.IntEnum):
SNES_DISCONNECTED = 0
@@ -438,16 +524,18 @@ def launch_sni(ctx: Context):
sni_path = Utils.local_path(sni_path)
if os.path.isdir(sni_path):
for file in os.listdir(sni_path):
if file.startswith("sni.") and not file.endswith(".proto"):
lower_file = file.lower()
if (lower_file.startswith("sni.") and not lower_file.endswith(".proto")) or lower_file == "sni":
sni_path = os.path.join(sni_path, file)
if os.path.isfile(sni_path):
snes_logger.info(f"Attempting to start {sni_path}")
import subprocess
if Utils.is_frozen(): # if it spawns a visible console, may as well populate it
import sys
if not sys.stdout: # if it spawns a visible console, may as well populate it
subprocess.Popen(sni_path, cwd=os.path.dirname(sni_path))
else:
subprocess.Popen(sni_path, cwd=os.path.dirname(sni_path), stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
subprocess.Popen(sni_path, cwd=os.path.dirname(sni_path), stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
else:
snes_logger.info(
f"Attempt to start SNI was aborted as path {sni_path} was not found, "
@@ -497,13 +585,23 @@ async def get_snes_devices(ctx: Context):
await socket.send(dumps(DeviceList_Request))
reply = loads(await socket.recv())
devices = reply['Results'] if 'Results' in reply and len(reply['Results']) > 0 else None
await verify_snes_app(socket)
await socket.close()
return devices
async def snes_connect(ctx: Context, address, deviceIndex = -1):
async def verify_snes_app(socket):
AppVersion_Request = {
"Opcode": "AppVersion",
}
await socket.send(dumps(AppVersion_Request))
app: str = loads(await socket.recv())["Results"][0]
if "SNI" not in app:
snes_logger.warning(f"Warning: Did not find SNI as the endpoint, instead {app} was found.")
async def snes_connect(ctx: Context, address, deviceIndex=-1):
global SNES_RECONNECT_DELAY
if ctx.snes_socket is not None and ctx.snes_state == SNESState.SNES_CONNECTED:
if ctx.rom:
@@ -532,7 +630,8 @@ async def snes_connect(ctx: Context, address, deviceIndex = -1):
device = devices[ctx.snes_attached_device[0]]
elif numDevices > 1:
if deviceIndex == -1:
snes_logger.info("Found " + str(numDevices) + " SNES devices; connect to one with /snes <address> <device number>:")
snes_logger.info(
"Found " + str(numDevices) + " SNES devices; connect to one with /snes <address> <device number>:")
for idx, availableDevice in enumerate(devices):
snes_logger.info(str(idx + 1) + ": " + availableDevice)
@@ -542,7 +641,7 @@ async def snes_connect(ctx: Context, address, deviceIndex = -1):
else:
device = devices[deviceIndex - 1]
if device is None:
await snes_disconnect(ctx)
return
@@ -588,11 +687,6 @@ async def snes_disconnect(ctx: Context):
async def snes_autoreconnect(ctx: Context):
# unfortunately currently broken. See: https://github.com/prompt-toolkit/python-prompt-toolkit/issues/1033
# with prompt_toolkit.shortcuts.ProgressBar() as pb:
# for _ in pb(range(100)):
# await asyncio.sleep(RECONNECT_DELAY/100)
await asyncio.sleep(SNES_RECONNECT_DELAY)
if ctx.snes_reconnect_address and ctx.snes_socket is None:
await snes_connect(ctx, ctx.snes_reconnect_address)
@@ -703,12 +797,6 @@ async def snes_flush_writes(ctx: Context):
await snes_write(ctx, writes)
# kept as function for easier wrapping by plugins
def get_tags(ctx: Context):
tags = ['AP']
return tags
async def track_locations(ctx: Context, roomid, roomdata):
new_locations = []
@@ -716,7 +804,8 @@ async def track_locations(ctx: Context, roomid, roomdata):
new_locations.append(location_id)
ctx.locations_checked.add(location_id)
location = ctx.location_name_getter(location_id)
snes_logger.info(f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
snes_logger.info(
f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
try:
if roomid in location_shop_ids:
@@ -786,7 +875,6 @@ async def track_locations(ctx: Context, roomid, roomdata):
if misc_data[offset - 0x3c6] & mask != 0 and location_id not in ctx.locations_checked:
new_check(location_id)
if new_locations:
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": new_locations}])
@@ -803,11 +891,25 @@ async def game_watcher(ctx: Context):
if not ctx.rom:
ctx.finished_game = False
rom = await snes_read(ctx, ROMNAME_START, ROMNAME_SIZE)
ctx.death_link_allow_survive = False
game_name = await snes_read(ctx, SM_ROMNAME_START, 2)
if game_name is None:
continue
elif game_name == b"SM":
ctx.game = GAME_SM
else:
ctx.game = GAME_ALTTP
rom = await snes_read(ctx, SM_ROMNAME_START if ctx.game == GAME_SM else ROMNAME_START, ROMNAME_SIZE)
if rom is None or rom == bytes([0] * ROMNAME_SIZE):
continue
ctx.rom = rom
death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR if ctx.game == GAME_ALTTP else
SM_DEATH_LINK_ACTIVE_ADDR, 1)
if death_link:
ctx.death_link_allow_survive = bool(death_link[0] & 0b10)
await ctx.update_death_link(bool(death_link[0] & 0b1))
if not ctx.prev_rom or ctx.prev_rom != ctx.rom:
ctx.locations_checked = set()
ctx.locations_scouted = set()
@@ -820,64 +922,131 @@ async def game_watcher(ctx: Context):
snes_logger.warning("ROM change detected, please reconnect to the multiworld server")
await ctx.disconnect()
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
gameend = await snes_read(ctx, SAVEDATA_START + 0x443, 1)
game_timer = await snes_read(ctx, SAVEDATA_START + 0x42E, 4)
if gamemode is None or gameend is None or game_timer is None or \
(gamemode[0] not in INGAME_MODES and gamemode[0] not in ENDGAME_MODES):
continue
if ctx.game == GAME_ALTTP:
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time():
currently_dead = gamemode[0] in DEATH_MODES
await ctx.handle_deathlink_state(currently_dead)
delay = 7 if ctx.slow_mode else 2
if gameend[0]:
if not ctx.finished_game:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
if time.perf_counter() - perf_counter < delay:
gameend = await snes_read(ctx, SAVEDATA_START + 0x443, 1)
game_timer = await snes_read(ctx, SAVEDATA_START + 0x42E, 4)
if gamemode is None or gameend is None or game_timer is None or \
(gamemode[0] not in INGAME_MODES and gamemode[0] not in ENDGAME_MODES):
continue
delay = 7 if ctx.slow_mode else 2
if gameend[0]:
if not ctx.finished_game:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
if time.perf_counter() - perf_counter < delay:
continue
else:
perf_counter = time.perf_counter()
else:
perf_counter = time.perf_counter()
else:
game_timer = game_timer[0] | (game_timer[1] << 8) | (game_timer[2] << 16) | (game_timer[3] << 24)
if abs(game_timer - prev_game_timer) < (delay * 60):
game_timer = game_timer[0] | (game_timer[1] << 8) | (game_timer[2] << 16) | (game_timer[3] << 24)
if abs(game_timer - prev_game_timer) < (delay * 60):
continue
else:
prev_game_timer = game_timer
if gamemode in ENDGAME_MODES: # triforce room and credits
continue
else:
prev_game_timer = game_timer
if gamemode in ENDGAME_MODES: # triforce room and credits
continue
data = await snes_read(ctx, RECV_PROGRESS_ADDR, 8)
if data is None:
continue
data = await snes_read(ctx, RECV_PROGRESS_ADDR, 8)
if data is None:
continue
recv_index = data[0] | (data[1] << 8)
recv_item = data[2]
roomid = data[4] | (data[5] << 8)
roomdata = data[6]
scout_location = data[7]
recv_index = data[0] | (data[1] << 8)
recv_item = data[2]
roomid = data[4] | (data[5] << 8)
roomdata = data[6]
scout_location = data[7]
if recv_index < len(ctx.items_received) and recv_item == 0:
item = ctx.items_received[recv_index]
recv_index += 1
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
color(ctx.item_name_getter(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
ctx.location_name_getter(item.location), recv_index, len(ctx.items_received)))
if recv_index < len(ctx.items_received) and recv_item == 0:
item = ctx.items_received[recv_index]
recv_index += 1
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
color(ctx.item_name_getter(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
ctx.location_name_getter(item.location), recv_index, len(ctx.items_received)))
snes_buffered_write(ctx, RECV_PROGRESS_ADDR,
bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
snes_buffered_write(ctx, RECV_ITEM_ADDR,
bytes([item.item]))
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR,
bytes([min(ROM_PLAYER_LIMIT, item.player) if item.player != ctx.slot else 0]))
if scout_location > 0 and scout_location in ctx.locations_info:
snes_buffered_write(ctx, SCOUTREPLY_LOCATION_ADDR,
bytes([scout_location]))
snes_buffered_write(ctx, SCOUTREPLY_ITEM_ADDR,
bytes([ctx.locations_info[scout_location][0]]))
snes_buffered_write(ctx, SCOUTREPLY_PLAYER_ADDR,
bytes([min(ROM_PLAYER_LIMIT, ctx.locations_info[scout_location][1])]))
snes_buffered_write(ctx, RECV_PROGRESS_ADDR, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
snes_buffered_write(ctx, RECV_ITEM_ADDR, bytes([item.item]))
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR, bytes([item.player if item.player != ctx.slot else 0]))
if scout_location > 0 and scout_location in ctx.locations_info:
snes_buffered_write(ctx, SCOUTREPLY_LOCATION_ADDR, bytes([scout_location]))
snes_buffered_write(ctx, SCOUTREPLY_ITEM_ADDR, bytes([ctx.locations_info[scout_location][0]]))
snes_buffered_write(ctx, SCOUTREPLY_PLAYER_ADDR, bytes([ctx.locations_info[scout_location][1]]))
await snes_flush_writes(ctx)
await snes_flush_writes(ctx)
if scout_location > 0 and scout_location not in ctx.locations_scouted:
ctx.locations_scouted.add(scout_location)
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": [scout_location]}])
await track_locations(ctx, roomid, roomdata)
elif ctx.game == GAME_SM:
gamemode = await snes_read(ctx, WRAM_START + 0x0998, 1)
if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time():
currently_dead = gamemode[0] in SM_DEATH_MODES
await ctx.handle_deathlink_state(currently_dead)
if gamemode is not None and gamemode[0] in SM_ENDGAME_MODES:
if not ctx.finished_game:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
continue
if scout_location > 0 and scout_location not in ctx.locations_scouted:
ctx.locations_scouted.add(scout_location)
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": [scout_location]}])
await track_locations(ctx, roomid, roomdata)
data = await snes_read(ctx, SM_RECV_PROGRESS_ADDR + 0x680, 4)
if data is None:
continue
recv_index = data[0] | (data[1] << 8)
recv_item = data[2] | (data[3] << 8)
while (recv_index < recv_item):
itemAdress = recv_index * 8
message = await snes_read(ctx, SM_RECV_PROGRESS_ADDR + 0x700 + itemAdress, 8)
# worldId = message[0] | (message[1] << 8) # unused
# itemId = message[2] | (message[3] << 8) # unused
itemIndex = (message[4] | (message[5] << 8)) >> 3
recv_index += 1
snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + 0x680, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
from worlds.sm.Locations import locations_start_id
location_id = locations_start_id + itemIndex
ctx.locations_checked.add(location_id)
location = ctx.location_name_getter(location_id)
snes_logger.info(f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}])
data = await snes_read(ctx, SM_RECV_PROGRESS_ADDR + 0x600, 4)
if data is None:
continue
# recv_itemOutPtr = data[0] | (data[1] << 8) # unused
itemOutPtr = data[2] | (data[3] << 8)
from worlds.sm.Items import items_start_id
if itemOutPtr < len(ctx.items_received):
item = ctx.items_received[itemOutPtr]
itemId = item.item - items_start_id
playerID = item.player if item.player <= SM_ROM_PLAYER_LIMIT else 0
snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + itemOutPtr * 4, bytes([playerID & 0xFF, (playerID >> 8) & 0xFF, itemId & 0xFF, (itemId >> 8) & 0xFF]))
itemOutPtr += 1
snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + 0x602, bytes([itemOutPtr & 0xFF, (itemOutPtr >> 8) & 0xFF]))
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
color(ctx.item_name_getter(item.item), 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
ctx.location_name_getter(item.location), itemOutPtr, len(ctx.items_received)))
await snes_flush_writes(ctx)
async def run_game(romfile):
@@ -889,71 +1058,69 @@ async def run_game(romfile):
subprocess.Popen([auto_start, romfile],
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
async def main():
multiprocessing.freeze_support()
parser = argparse.ArgumentParser()
parser = get_base_parser()
parser.add_argument('diff_file', default="", type=str, nargs="?",
help='Path to a Archipelago Binary Patch file')
parser.add_argument('--snes', default='localhost:8080', help='Address of the SNI server.')
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
parser.add_argument('--password', default=None, help='Password of the multiworld host.')
parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
parser.add_argument('--founditems', default=False, action='store_true',
help='Show items found by other players for themselves.')
if not Utils.is_frozen(): # Frozen state has no cmd window in the first place
parser.add_argument('--nogui', default=False, action='store_true', help="Turns off Client GUI.")
args = parser.parse_args()
logging.basicConfig(format='%(message)s', level=getattr(logging, args.loglevel.upper(), logging.INFO))
if args.diff_file:
import Patch
logging.info("Patch file was supplied. Creating sfc rom..")
meta, romfile = Patch.create_rom_file(args.diff_file)
args.connect = meta["server"]
if "server" in meta:
args.connect = meta["server"]
logging.info(f"Wrote rom file to {romfile}")
adjustedromfile, adjusted = Utils.get_adjuster_settings(romfile, gui_enabled)
if adjusted:
try:
shutil.move(adjustedromfile, romfile)
adjustedromfile = romfile
except Exception as e:
logging.exception(e)
asyncio.create_task(run_game(adjustedromfile if adjusted else romfile))
if args.diff_file.endswith(".apsoe"):
import webbrowser
webbrowser.open("http://www.evermizer.com/apclient/" +
(f"#server={meta['server']}" if "server" in meta else ""))
logging.info("Starting Evermizer Client in your Browser...")
import time
time.sleep(3)
sys.exit()
elif args.diff_file.endswith((".apbp", "apz3")):
adjustedromfile, adjusted = Utils.get_adjuster_settings(romfile, gui_enabled)
if adjusted:
try:
shutil.move(adjustedromfile, romfile)
adjustedromfile = romfile
except Exception as e:
logging.exception(e)
asyncio.create_task(run_game(adjustedromfile if adjusted else romfile))
else:
asyncio.create_task(run_game(romfile))
ctx = Context(args.snes, args.connect, args.password)
if ctx.server_task is None:
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
input_task = None
if gui_enabled:
input_task = None
from kvui import LttPManager
ctx.ui = LttPManager(ctx)
from kvui import SNIManager
ctx.ui = SNIManager(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
if sys.stdin:
input_task = asyncio.create_task(console_loop(ctx), name="Input")
snes_connect_task = asyncio.create_task(snes_connect(ctx, ctx.snes_address))
snes_connect_task = asyncio.create_task(snes_connect(ctx, ctx.snes_address), name="SNES Connect")
watcher_task = asyncio.create_task(game_watcher(ctx), name="GameWatcher")
await ctx.exit_event.wait()
if snes_connect_task:
snes_connect_task.cancel()
ctx.server_address = None
ctx.snes_reconnect_address = None
await watcher_task
if ctx.server and not ctx.server.socket.closed:
await ctx.server.socket.close()
if ctx.server_task:
await ctx.server_task
if ctx.snes_socket is not None and not ctx.snes_socket.closed:
await ctx.snes_socket.close()
while ctx.input_requests > 0:
ctx.input_queue.put_nowait(None)
ctx.input_requests -= 1
if snes_connect_task:
snes_connect_task.cancel()
await watcher_task
await ctx.shutdown()
if ui_task:
await ui_task
@@ -968,4 +1135,3 @@ if __name__ == '__main__':
loop.run_until_complete(main())
loop.close()
colorama.deinit()
atexit.unregister(exit_func)

112
Utils.py
View File

@@ -1,6 +1,16 @@
from __future__ import annotations
import typing
import builtins
import os
import subprocess
import sys
import pickle
import functools
import io
import collections
import importlib
import logging
def tuplize_version(version: str) -> Version:
@@ -13,18 +23,9 @@ class Version(typing.NamedTuple):
build: int
__version__ = "0.1.9"
__version__ = "0.2.2"
version_tuple = tuplize_version(__version__)
import builtins
import os
import subprocess
import sys
import pickle
import functools
import io
import collections
import importlib
from yaml import load, dump, safe_load
try:
@@ -120,17 +121,25 @@ parse_yaml = safe_load
unsafe_parse_yaml = functools.partial(load, Loader=Loader)
def get_cert_none_ssl_context():
import ssl
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
return ctx
@cache_argsless
def get_public_ipv4() -> str:
import socket
import urllib.request
import logging
ip = socket.gethostbyname(socket.gethostname())
ctx = get_cert_none_ssl_context()
try:
ip = urllib.request.urlopen('https://checkip.amazonaws.com/').read().decode('utf8').strip()
ip = urllib.request.urlopen('https://checkip.amazonaws.com/', context=ctx).read().decode('utf8').strip()
except Exception as e:
try:
ip = urllib.request.urlopen('https://v4.ident.me').read().decode('utf8').strip()
ip = urllib.request.urlopen('https://v4.ident.me', context=ctx).read().decode('utf8').strip()
except:
logging.exception(e)
pass # we could be offline, in a local game, so no point in erroring out
@@ -141,10 +150,10 @@ def get_public_ipv4() -> str:
def get_public_ipv6() -> str:
import socket
import urllib.request
import logging
ip = socket.gethostbyname(socket.gethostname())
ctx = get_cert_none_ssl_context()
try:
ip = urllib.request.urlopen('https://v6.ident.me').read().decode('utf8').strip()
ip = urllib.request.urlopen('https://v6.ident.me', context=ctx).read().decode('utf8').strip()
except Exception as e:
logging.exception(e)
pass # we could be offline, in a local game, or ipv6 may not be available
@@ -161,6 +170,14 @@ def get_default_options() -> dict:
"factorio_options": {
"executable": "factorio\\bin\\x64\\factorio",
},
"sm_options": {
"rom_file": "Super Metroid (JU).sfc",
"sni": "SNI",
"rom_start": True,
},
"soe_options": {
"rom_file": "Secret of Evermore (USA).sfc",
},
"lttp_options": {
"rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc",
"sni": "SNI",
@@ -180,6 +197,7 @@ def get_default_options() -> dict:
"location_check_points": 1,
"hint_cost": 10,
"forfeit_mode": "goal",
"collect_mode": "disabled",
"remaining_mode": "goal",
"auto_shutdown": 0,
"compatibility": 2,
@@ -210,7 +228,6 @@ def get_default_options() -> dict:
def update_options(src: dict, dest: dict, filename: str, keys: list) -> dict:
import logging
for key, value in src.items():
new_keys = keys.copy()
new_keys.append(key)
@@ -277,7 +294,6 @@ def persistent_load() -> typing.Dict[dict]:
with open(path, "r") as f:
storage = unsafe_parse_yaml(f.read())
except Exception as e:
import logging
logging.debug(f"Could not read store: {e}")
if storage is None:
storage = {}
@@ -310,7 +326,6 @@ def get_adjuster_settings(romfile: str, skip_questions: bool = False) -> typing.
if sprite_pool:
printed_options["sprite_pool"] = sprite_pool
if hasattr(get_adjuster_settings, "adjust_wanted"):
adjust_wanted = getattr(get_adjuster_settings, "adjust_wanted")
elif persistent_load().get("adjuster", {}).get("never_adjust", False): # never adjust, per user request
@@ -337,7 +352,6 @@ def get_adjuster_settings(romfile: str, skip_questions: bool = False) -> typing.
return romfile, False
else:
adjusted = False
import logging
if not hasattr(get_adjuster_settings, "adjust_wanted"):
logging.info(f"Skipping post-patch adjustment")
get_adjuster_settings.adjuster_settings = adjuster_settings
@@ -401,4 +415,62 @@ def restricted_loads(s):
class KeyedDefaultDict(collections.defaultdict):
def __missing__(self, key):
self[key] = value = self.default_factory(key)
return value
return value
def get_text_between(text: str, start: str, end: str) -> str:
return text[text.index(start) + len(start): text.rindex(end)]
loglevel_mapping = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}
def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, write_mode: str = "w",
log_format: str = "[%(name)s]: %(message)s", exception_logger: str = ""):
loglevel: int = loglevel_mapping.get(loglevel, loglevel)
log_folder = local_path("logs")
os.makedirs(log_folder, exist_ok=True)
root_logger = logging.getLogger()
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
handler.close()
root_logger.setLevel(loglevel)
file_handler = logging.FileHandler(
os.path.join(log_folder, f"{name}.txt"),
write_mode,
encoding="utf-8-sig")
file_handler.setFormatter(logging.Formatter(log_format))
root_logger.addHandler(file_handler)
if sys.stdout:
root_logger.addHandler(
logging.StreamHandler(sys.stdout)
)
# Relay unhandled exceptions to logger.
if not getattr(sys.excepthook, "_wrapped", False): # skip if already modified
orig_hook = sys.excepthook
def handle_exception(exc_type, exc_value, exc_traceback):
if issubclass(exc_type, KeyboardInterrupt):
sys.__excepthook__(exc_type, exc_value, exc_traceback)
return
logging.getLogger(exception_logger).exception("Uncaught exception",
exc_info=(exc_type, exc_value, exc_traceback))
return orig_hook(exc_type, exc_value, exc_traceback)
handle_exception._wrapped = True
sys.excepthook = handle_exception
def stream_input(stream, queue):
def queuer():
while 1:
text = stream.readline().strip()
if text:
queue.put_nowait(text)
from threading import Thread
thread = Thread(target=queuer, name=f"Stream handler for {stream.name}", daemon=True)
thread.start()
return thread

View File

@@ -14,7 +14,7 @@ from WebHostLib import app as raw_app
from waitress import serve
from WebHostLib.models import db
from WebHostLib.autolauncher import autohost
from WebHostLib.autolauncher import autohost, autogen
from WebHostLib.lttpsprites import update_sprites_lttp
from WebHostLib.options import create as create_options_files
@@ -45,6 +45,8 @@ if __name__ == "__main__":
create_options_files()
if app.config["SELFLAUNCH"]:
autohost(app.config)
if app.config["SELFGEN"]:
autogen(app.config)
if app.config["SELFHOST"]: # using WSGI, you just want to run get_app()
if app.config["DEBUG"]:
autohost(app.config)

View File

@@ -22,13 +22,14 @@ Pony(app)
app.jinja_env.filters['any'] = any
app.jinja_env.filters['all'] = all
app.config["SELFHOST"] = True
app.config["SELFHOST"] = True # application process is in charge of running the websites
app.config["GENERATORS"] = 8 # maximum concurrent world gens
app.config["SELFLAUNCH"] = True
app.config["SELFLAUNCH"] = True # application process is in charge of launching Rooms.
app.config["SELFGEN"] = True # application process is in charge of scheduling Generations.
app.config["DEBUG"] = False
app.config["PORT"] = 80
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 4 * 1024 * 1024 # 4 megabyte limit
app.config['MAX_CONTENT_LENGTH'] = 64 * 1024 * 1024 # 64 megabyte limit
# if you want to deploy, make sure you have a non-guessable secret key
app.config["SECRET_KEY"] = bytes(socket.gethostname(), encoding="utf-8")
# at what amount of worlds should scheduling be used, instead of rolling in the webthread
@@ -126,7 +127,7 @@ def faq(lang):
@app.route('/seed/<suuid:seed>')
def viewSeed(seed: UUID):
def view_seed(seed: UUID):
seed = Seed.get(id=seed)
if not seed:
abort(404)
@@ -141,7 +142,7 @@ def new_room(seed: UUID):
abort(404)
room = Room(seed=seed, owner=session["_id"], tracker=uuid4())
commit()
return redirect(url_for("hostRoom", room=room.id))
return redirect(url_for("host_room", room=room.id))
def _read_log(path: str):
@@ -155,20 +156,20 @@ def _read_log(path: str):
@app.route('/log/<suuid:room>')
def display_log(room: UUID):
# noinspection PyTypeChecker
return Response(_read_log(os.path.join("logs", str(room) + ".txt")), mimetype="text/plain;charset=UTF-8")
@app.route('/room/<suuid:room>', methods=['GET', 'POST'])
def hostRoom(room: UUID):
def host_room(room: UUID):
room = Room.get(id=room)
if room is None:
return abort(404)
if request.method == "POST":
if room.owner == session["_id"]:
cmd = request.form["cmd"]
Command(room=room, commandtext=cmd)
commit()
if cmd:
Command(room=room, commandtext=cmd)
commit()
with db_session:
room.last_activity = datetime.utcnow() # will trigger a spinup, if it's not already running
@@ -176,20 +177,17 @@ def hostRoom(room: UUID):
return render_template("hostRoom.html", room=room)
@app.route('/hosted/<suuid:room>', methods=['GET', 'POST'])
def hostRoomRedirect(room: UUID):
return redirect(url_for("hostRoom", room=room))
@app.route('/favicon.ico')
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

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

View File

@@ -110,6 +110,26 @@ def autohost(config: dict):
def keep_running():
try:
with Locker("autohost"):
while 1:
time.sleep(0.1)
with db_session:
rooms = select(
room for room in Room if
room.last_activity >= datetime.utcnow() - timedelta(days=3))
for room in rooms:
launch_room(room, config)
except AlreadyRunningException:
logging.info("Autohost reports as already running, not starting another.")
import threading
threading.Thread(target=keep_running, name="AP_Autohost").start()
def autogen(config: dict):
def keep_running():
try:
with Locker("autogen"):
with multiprocessing.Pool(config["GENERATORS"], initializer=init_db,
initargs=(config["PONY"],)) as generator_pool:
@@ -129,22 +149,17 @@ def autohost(config: dict):
select(generation for generation in Generation if generation.state == STATE_ERROR).delete()
while 1:
time.sleep(0.50)
time.sleep(0.1)
with db_session:
rooms = select(
room for room in Room if
room.last_activity >= datetime.utcnow() - timedelta(days=3))
for room in rooms:
launch_room(room, config)
to_start = select(
generation for generation in Generation if generation.state == STATE_QUEUED)
for generation in to_start:
launch_generator(generator_pool, generation)
except AlreadyRunningException:
pass
logging.info("Autogen reports as already running, not starting another.")
import threading
threading.Thread(target=keep_running).start()
threading.Thread(target=keep_running, name="AP_Autogen").start()
multiworlds = {}

View File

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

View File

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

View File

@@ -20,6 +20,16 @@ from .check import get_yaml_data, roll_options
from .upload import upload_zip_to_db
def get_meta(options_source: dict) -> dict:
meta = {
"hint_cost": int(options_source.get("hint_cost", 10)),
"forfeit_mode": options_source.get("forfeit_mode", "goal"),
"remaining_mode": options_source.get("forfeit_mode", "disabled"),
"collect_mode": options_source.get("collect_mode", "disabled"),
}
return meta
@app.route('/generate', methods=['GET', 'POST'])
@app.route('/generate/<race>', methods=['GET', 'POST'])
def generate(race=False):
@@ -35,9 +45,9 @@ def generate(race=False):
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}
meta = get_meta(request.form)
meta["race"] = race
if race:
meta["item_cheat"] = False
meta["remaining"] = False
@@ -66,7 +76,7 @@ def generate(race=False):
handle_generation_failure(e)
return render_template("seedError.html", seed_error=(e.__class__.__name__ + ": " + str(e)))
return redirect(url_for("viewSeed", seed=seed_id))
return redirect(url_for("view_seed", seed=seed_id))
return render_template("generate.html", race=race)
@@ -133,7 +143,7 @@ def wait_seed(seed: UUID):
seed_id = seed
seed = Seed.get(id=seed_id)
if seed:
return redirect(url_for("viewSeed", seed=seed_id))
return redirect(url_for("view_seed", seed=seed_id))
generation = Generation.get(id=seed_id)
if not generation:

View File

@@ -10,6 +10,7 @@ def update_sprites_lttp():
from tkinter import Tk
from LttPAdjuster import get_image_for_sprite
from LttPAdjuster import BackgroundTaskProgress
from LttPAdjuster import BackgroundTaskProgressNullWindow
from LttPAdjuster import update_sprites
# Target directories
@@ -19,11 +20,15 @@ def update_sprites_lttp():
os.makedirs(os.path.join(output_dir, "sprites"), exist_ok=True)
# update sprites through gui.py's functions
done = threading.Event()
top = Tk()
top.withdraw()
BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
try:
top = Tk()
except:
task = BackgroundTaskProgressNullWindow(update_sprites, lambda successful, resultmessage: done.set())
else:
top.withdraw()
task = BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
while not done.isSet():
top.update()
task.do_events()
spriteData = []

View File

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

View File

@@ -32,8 +32,7 @@ def create():
dictify_range=dictify_range, default_converter=default_converter,
)
if not os.path.isdir(os.path.join(target_folder, 'configs')):
os.mkdir(os.path.join(target_folder, 'configs'))
os.makedirs(os.path.join(target_folder, 'configs'), exist_ok=True)
with open(os.path.join(target_folder, 'configs', game_name + ".yaml"), "w") as f:
f.write(res)
@@ -50,7 +49,7 @@ def create():
game_options = {}
for option_name, option in world.options.items():
if option.options:
this_option = {
game_options[option_name] = this_option = {
"type": "select",
"displayName": option.displayname if hasattr(option, "displayname") else option_name,
"description": option.__doc__ if option.__doc__ else "Please document me!",
@@ -67,7 +66,10 @@ def create():
if sub_option_id == option.default:
this_option["defaultValue"] = sub_option_name
game_options[option_name] = this_option
this_option["options"].append({
"name": "Random",
"value": "random",
})
elif hasattr(option, "range_start") and hasattr(option, "range_end"):
game_options[option_name] = {
@@ -81,8 +83,7 @@ def create():
player_settings["gameOptions"] = game_options
if not os.path.isdir(os.path.join(target_folder, 'player-settings')):
os.mkdir(os.path.join(target_folder, 'player-settings'))
os.makedirs(os.path.join(target_folder, 'player-settings'), exist_ok=True)
with open(os.path.join(target_folder, 'player-settings', game_name + ".json"), "w") as f:
f.write(json.dumps(player_settings, indent=2, separators=(',', ': ')))

View File

@@ -1,4 +1,4 @@
flask>=2.0.1
flask>=2.0.2
pony>=0.7.14
waitress>=2.0.0
flask-caching>=1.10.1

View File

@@ -22,7 +22,7 @@ player's game, they may find items which belong to the other player. If player A
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.
their game.
## What happens if a person has to leave early?
If a player must leave early, they can use Archipelago's forfeit system. When a player forfeits their game, all

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ window.addEventListener('load', () => {
gameName = document.getElementById('player-settings').getAttribute('data-game');
// Update game name on page
document.getElementById('game-name').innerHTML = gameName;
document.getElementById('game-name').innerText = gameName;
Promise.all([fetchSettingData()]).then((results) => {
let settingHash = localStorage.getItem(`${gameName}-hash`);

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,180 @@
# Archipelago Plando Guide
## What is Plando?
The purposes of randomizers is to randomize the items in a game to give a new experience.
Plando takes this concept and changes it up by allowing you to plan out certain aspects of the game by placing certain
items in certain locations, certain bosses in certain rooms, edit text for certain NPCs/signs, or even force certain region
connections. Each of these options are going to be detailed separately as `item plando`, `boss plando`, `text plando`,
and `connection plando`. Every game in archipelago supports item plando but the other plando options are only supported
by certain games. Currently, Minecraft and LTTP both support connection plando, but only LTTP supports text and boss plando.
### Enabling Plando
On the website plando will already be enabled. If you will be generating the game locally plando features must be enabled (opt-in).
* To opt-in go to the archipelago installation (default: `C:\ProgramData\Archipelago`), open the host.yaml with a text
editor and find the `plando_options` key. The available plando modules can be enabled by adding them after this such as
`plando_options: bosses, items, texts, connections`.
## Item Plando
Item plando allows a player to place an item in a specific location or specific locations, place multiple items into
a list of specific locations both in their own game or in another player's game. **Note that there's a very good chance that
cross-game plando could very well be broken i.e. placing on of your items in someone else's world playing a different game.**
* The options for item plando are `from_pool`, `world`, `percentage`, `force`, and either item and location, or items and locations.
* `from_pool` determines if the item should be taken *from* the item pool or *added* to it. This can be true or false
and defaults to true if omitted.
* `world` is the target world to place the item in.
* It gets ignored if only one world is generated.
* Can be a number, name, true, false, or null. False is the default.
* If a number is used it targets that slot or player number in the multiworld.
* If a name is used it will target the world with that player name.
* If set to true it will be any player's world besides your own.
* If set to false it will target your own world.
* If set to null it will target a random world in the multiworld.
* `force` determines whether the generator will fail if the item can't be placed in the location can be true, false,
or silent. Silent is the default.
* If set to true the item must be placed and the generator will throw an error if it is unable to do so.
* If set to false the generator will log a warning if the placement can't be done but will still generate.
* If set to silent and the placement fails it will be ignored entirely.
* `percentage` is the percentage chance for the relevant block to trigger. This can be any value from 0 to 100 and if
omitted will default to 100.
* Single Placement is when you use a plando block to place a single item at a single location.
* `item` is the item you would like to place and `location` is the location to place it.
* Multi Placement uses a plando block to place multiple items in multiple locations until either list is exhausted.
* `items` defines the items to use and a number letting you place multiple of it.
* `locations` is a list of possible locations those items can be placed in.
* Using the multi placement method, placements are picked randomly.
### Available Items
* [A Link to the Past](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Items.py#L52)
* [Factorio Non-Progressive](https://wiki.factorio.com/Technologies) Note that these use the *internal names*. For example, `advanced-electronics`
* [Factorio Progressive](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/factorio/Technologies.py#L374)
* [Minecraft](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/minecraft/Items.py#L14)
* [Ocarina of Time](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/oot/Items.py#L61)
* [Risk of Rain 2](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/ror2/Items.py#L8)
* [Slay the Spire](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/spire/Items.py#L13)
* [Subnautica](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/subnautica/items.json)
* [Timespinner](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/timespinner/Items.py#L11)
### Available Locations
* [A Link to the Past](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Regions.py#L429)
* [Factorio](https://wiki.factorio.com/Technologies) Same as items
* [Minecraft](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/minecraft/Locations.py#L18)
* [Ocarina of Time](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/oot/LocationList.py#L38)
* [Risk of Rain 2](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/ror2/Locations.py#L17) This is a special
case. The locations are "ItemPickup[number]" up to the maximum set in the yaml.
* [Slay the Spire](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/spire/Locations.py)
* [Subnautica](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/subnautica/locations.json)
* [Timespinner](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/timespinner/Locations.py#L13)
A list of all available items and locations can also be found in the [server's datapackage](/api/datapackage).
### Examples
```yaml
plando_items:
# example block 1 - Timespinner
- item:
Empire Orb: 1
Radiant Orb: 1
location: Starter Chest 1
from_pool: true
world: true
percentage: 50
# example block 2 - Ocarina of Time
- items:
Kokiri Sword: 1
Biggoron Sword: 1
Bow: 1
Magic Meter: 1
Progressive Strength Upgrade: 3
Progressive Hookshot: 2
locations:
- Deku Tree Slingshot Chest
- Dodongos Cavern Bomb Bag Chest
- Jabu Jabus Belly Boomerang Chest
- Bottom of the Well Lens of Truth Chest
- Forest Temple Bow Chest
- Fire Temple Megaton Hammer Chest
- Water Temple Longshot Chest
- Shadow Temple Hover Boots Chest
- Spirit Temple Silver Gauntlets Chest
world: false
# example block 3 - Slay the Spire
- items:
Boss Relic: 3
locations:
Boss Relic 1
Boss Relic 2
Boss Relic 3
# example block 4 - Factorio
- items:
progressive-electric-energy-distribution: 2
electric-energy-accumulators: 1
progressive-turret: 2
locations:
military
gun-turret
logistic-science-pack
steel-processing
percentage: 80
force: true
```
1. This block has a 50% chance to occur, and if it does will place either the Empire Orb or Radiant Orb on another player's
Starter Chest 1 and removes the chosen item from the item pool.
2. This block will always trigger and will place the player's swords, bow, magic meter, strength upgrades, and hookshots
in their own dungeon major item chests.
3. This block will always trigger and will lock boss relics on the bosses.
4. This block has an 80% chance of occuring and when it does will place all but 1 of the items randomly among the four
locations chosen here.
## Boss Plando
As this is currently only supported by A Link to the Past instead of explaining here please refer to the
[relevant guide](/tutorial/zelda3/plando/en)
## Text Plando
As this is currently only supported by A Link to the Past instead of explaining here please refer to the
[relevant guide](/tutorial/zelda3/plando/en)
## Connections Plando
This is currently only supported by Minecraft and A Link to the Past. As the way that these games interact with
their connections is different I will only explain the basics here while more specifics for Link to the Past connection
plando can be found in its plando guide.
* The options for connections are `percentage`, `entrance`, `exit`, and `direction`. Each of these options support subweights.
* `percentage` is the percentage chance for this connection from 0 to 100 and defaults to 100.
* Every connection has an `entrance` and an `exit`. These can be unlinked like in A Link to the Past insanity entrance shuffle.
* `direction` can be `both`, `entrance`, or `exit` and determines in which direction this connection will operate.
[Link to the Past connections](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/EntranceShuffle.py#L3852)
[Minecraft connections](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/minecraft/Regions.py#L62)
### Examples
```yaml
plando_connections:
# example block 1 - Link to the Past
- entrance: Cave Shop (Lake Hylia)
exit: Cave 45
direction: entrance
- entrance: Cave 45
exit: Cave Shop (Lake Hylia)
direction: entrance
- entrance: Agahnims Tower
exit: Old Man Cave Exit (West)
direction: exit
# example block 2 - Minecraft
- entrance: Overworld Structure 1
exit: Nether Fortress
direction: both
- entrance: Overworld Structure 2
exit: Village
direction: both
```
1. These connections are decoupled so going into the lake hylia cave shop will take you to the inside of cave 45 and
when you leave the interior you will exit to the cave 45 ledge. Going into the cave 45 entrance will then take you to the
lake hylia cave shop. Walking into the entrance for the old man cave and Agahnim's Tower entrance will both take you to
their locations as normal but leaving old man cave will exit at Agahnim's Tower.
2. This will force a nether fortress and a village to be the overworld structures for your game. Note that for the Minecraft
connection plando to work structure shuffle must be enabled.

View File

@@ -2,26 +2,78 @@
## 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.
Run the exe file, and after accepting the license agreement you will be prompted on which components you would like to install.
The generator allows you to generate multiworld games on your computer. The ROM setups are optional but are required if
anyone in the game that you generate wants to play any of those games as they are needed to generate the relevant patch
files. The server will allow you to host the multiworld on your machine but this also requires you to port forward. The
default port for Archipelago is `38281`. If you are unsure how to do this there are plenty of other guides on the internet
that will be more suited to your hardware. The `Clients` are what you use to connect your game to the multiworld. If the
game/games you plan to play are available here go ahead and install these as well. If the game you choose to play is
supported by Archipelago but not listed in the installation check the relevant tutorial.
## Generating a game
### Creating a YAML
In a multiworld there must be one YAML per world. Any number of players can play on each world using either the game's
native coop system or using archipelago's coop support. Each world will hold one slot in the multiworld and will have a
slot name and, if the relevant game requires it, files to associate it with that multiworld. If multiple people plan to
play in one world cooperatively then they will only need one YAML for their coop world, but if each player is planning on
playing their own game then they will each need a YAML. These YAML files can be generated by going to the relevant game's
player settings page, entering the name they want to use for the game, setting the options to what they would like to
play with and then clicking on the export settings button. This will then download a YAML file that will contain all of
these options and this can then be given to whoever is going to generate the game.
### 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.
All players that wish to play in the generated multiworld must have a YAML file which contains the settings that they wish to play with.
A YAML is a file which contains human readable markup. In other words, this is a settings file kind of like an INI file or a TOML file.
Each player can go to the game's player settings page in order to determine the settings how they want them and then download a YAML file containing these settings.
After getting 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.
After getting the YAML files of each participant for your multiworld game, these can all either be placed together in the
`Archipelago\Players` folder or compressed into a ZIP folder to then be uploaded to the [website generator](/generate).
If rolling locally ensure that the folder is clear of any files you do not wish to include in the game such as the
included default player settings files.
#### Changing local host settings for generation
Sometimes there are various settings that you may want to change before rolling a seed such as enabling race mode,
auto-forfeit, plando support, or setting a password. All of these settings plus other options are able to be changed by
modifying the `host.yaml` file in the base `Archipelago` folder. The settings chosen here are baked into
the serverdata file that gets output with the other files after generation so if rolling locally ensure this file is edited
to your liking *before* rolling the seed.
### Rolling the seed
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.
#### On the Website
After gathering the YAML files together in one location, select all of the files and compress them into a .zip folder.
Next go to the [Start Playing](/start-playing) page and click on `generate a randomized game` to reach the website generator.
Here, you can adjust some server settings such as forfeit rules and the cost for a player to use a hint before generation.
After adjusting the host settings to your liking click on the Upload File button and using the explorer window that opens,
navigate to the location where you zipped the player files and upload this zip. The page will generate your game and refresh
multiple times to check on completion status. After the generation completes you will be on a Seed Info page that provides
the seed, the date/time of creation, a link to the spoiler log, if available, and links to any rooms created from this seed.
To begin playing, click on `Create New Room`, which will take you to the room page. From here you can navigate back to thse
Seed Info page or to the Tracker page. Sharing the link to this page with your friends will provide them with the
necessary info and files for them to connect to the multiworld.
#### Rolling using the generation program
After gathering the YAML files together in the `Archipelago\Players` folder, run the program `ArchipelagoGenerate.exe`
in the base `Archipelago` folder. This will then open a console window and either silently close itself or spit out an
error. If you receive an error, it is likely due to an error in the YAML file. If the error is unhelpful in figuring
out the issue asking in the ***#tech-support*** channel of our Discord for help with finding it is highly recommended.
The generator will put a zip folder into your `Archipelago\output` folder with the format `AP_XXXXXXXXX`.zip.
This contains the patch files and relevant mods for the players as well as the serverdata for the host.
## Hosting 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.
The easiest and most recommended method is to generate the game on the website which will allow you to create a private
room with all the necessary files you can share, as well as hosting the game and supporting item trackers for various games.
If for some reason the seed was rolled on a machine, then either the resulting zip file or the resulting `AP_XXXXX.archipelago`
inside the zip file can be uploaded to the [upload page](/uploads). This will give a page with the seed info and have a
link to the spoiler if it exists. Click on Create New room and then share the link for the room with the other players
so that they can download their patches or mods. The room will also have a link to a Multiworld Tracker and tell you
what the players need to connect to from their clients.
### Hosting a seed locally
For this we'll assume you have already port forwarding `38281` and have generated a seed that is still in the `outputs` folder. Next, you'll want to run `ArchipelagoServer.exe`. A window will open in order to open the multiworld data for the game. You can either use the generated zip folder or extract the .archipelago file and use it. If everything worked correctly the console window should tell you it's now hosting a game with the IP, port, and password that clients will need in order to connect.
Extract the patch and mod files then send those to your friends and you're done!
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

@@ -0,0 +1,69 @@
# Archipelago Triggers Guide
## What are triggers?
Triggers allow you to customize your game settings by allowing you to define certain options or even a variety of
settings to occur or "trigger" under certain conditions. These are essentially "if, then statements" for options in your game.
A good example of what you can do with triggers is the custom
[mercenary mode](https://github.com/alwaysintreble/Archipelago-yaml-dump/blob/main/Snippets/Mercenary%20Mode%20Snippet.yaml)
that was created using entirely triggers and plando. For more information on plando you can reference
[this guide](/tutorial/archipelago/plando/en) or [this guide](/tutorial/zelda3/plando/en).
## Trigger use
Triggers have to be defined in the root of the yaml file meaning it must be outside of a game section.
The best place to do this is the bottom of the yaml.
- Triggers comprise of the trigger section and then each trigger must have an `option_category`, `option_name`, and
`option_result` from which it will react to and then an `options` section where the definition of what will happen.
- `option_category` is the defining section from which the option is defined in.
- Example: `A Link to the Past`
- This is the root category the option is located in. If the option you're triggering off of is in root then you
would use `null`, otherwise this is the game for which you want this option trigger to activate.
- `option_name` is the option setting from which the triggered choice is going to react to.
- Example: `shop_item_slots`
- This can be any option from any category defined in the yaml file in either root or a game section except for `game`.
- `option_result` is the result of this option setting from which you would like to react.
- Example: `15`
- Each trigger must be used for exactly one option result. If you would like the same thing to occur with multiple
results you would need multiple triggers for this.
- `options` is where you define what will happen when this is detected. This can be something as simple as ensuring
another option also gets selected or placing an item in a certain location.
- Example:
```yaml
A Link to the Past:
start_inventory:
Rupees (300): 2
```
This format must be:
```yaml
root option:
option to change:
desired result
```
### Examples
The above examples all together will end up looking like this:
```yaml
triggers:
- option_category: A Link to the Past
option_name: shop_item_slots
option_result: 15
options:
A Link to the Past:
start_inventory:
Rupees(300): 2
```
For this example if the generator happens to roll 15 shuffled in shop item slots for your game you'll be granted 600 rupees at the beginning.
These can also be used to change other options.
For example:
```yaml
triggers:
- option_category: Timespinner
option_name: SpecificKeycards
option_result: true
options:
Timespinner:
Inverted: true
```
In this example if your world happens to roll SpecificKeycards then your game will also start in inverted.

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 KiB

View File

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

View File

@@ -0,0 +1,114 @@
# Final Fantasy 1 (NES) Multiworld Setup Guide
## Required Software
- The FF1Client which is bundled with [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
- The [BizHawk](http://tasvideos.org/BizHawk.html) emulator. Versions 2.3.1 and higher are supported.
Version 2.7 is recommended
- Your Final Fantasy (USA Edition) ROM file, probably named `Final Fantasy (USA).nes`. Neither Archipelago.gg nor the
Final Fantasy Randomizer Community can supply you with this.
## Installation Procedures
1. Download and install the latest version of [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
1. On Windows, download Setup.Archipelago.<HighestVersion>.exe and run it
2. Assign Bizhawk version 2.3.1 or higher as your default program for launching `.nes` files.
1. Extract your Bizhawk folder to your Desktop, or somewhere you will remember. Below are optional additional
steps for loading ROMs more conveniently
1. Right-click on a ROM file and select **Open with...**
2. Check the box next to **Always use this app to open .nes files**
3. Scroll to the bottom of the list and click the grey text **Look for another App on this PC**
4. Browse for `EmuHawk.exe` located inside your Bizhawk folder (from step 1) and click **Open**.
## Playing a Multiworld
Playing a multiworld on Archipelago.gg has 3 key components:
1. The Server which is hosting a game for all players.
2. The Client Program. For Final Fantasy 1, it is a standalone program but other randomizers may build it in.
3. The Game itself, in this case running on Bizhawk, which then connects to the Client running on your computer.
To set this up the following steps are required:
1. (Each Player) Generate your own yaml file and randomized ROM
2. (Host Only) Generate a randomized game with you and 0 or more players using Archipelago
3. (Host Only) Run the Archipelago Server
4. (Each Player) Run your client program and connect it to the Server
5. (Each Player) Run your game and connect it to your client program
6. (Each Player) Play the game and have fun!
### Obtaining your Archipelago yaml file and randomized ROM
Unlike most other Archipelago.gg games Final Fantasy 1 is randomized by the
[main randomizer](https://finalfantasyrandomizer.com/). Generate a game by going to the site and performing the
following steps:
1. Select the randomization options (also known as `Flags` in the community) of your choice. If you do not know what
you prefer, or it is your first time playing select the "Archipelago" preset on the main page.
2. Go to the `Beta` tab and ensure `Archipelago` is enabled. Set your player name to any name that represents you.
3. Upload you `Final Fantasy(USA).nes` (and click `Remember ROM` for the future!)
4. Press the `NEW` button beside `Seed` a few times
5. Click `GENERATE ROM`
It should download two files. One is the `*.nes` file which your emulator will run and the other is the yaml file
required by Archipelago.gg
### Generating the Multiworld and Starting the Server
The game can be generated locally or by Archipelago.gg.
#### Generating on Archipelago.gg (Recommended)
1. Gather all yaml files
2. Create a zip file containing all of the yaml files. Make sure it is a `*.zip` not a `*.7z` or a `*.rar`
3. Navigate to the [Generate Page](https://archipelago.gg/generate) and click `Upload File`
1. For your first game keep `Forfeit Permission` as `Automatic on goal completion`. Forfeiting actually means
giving out all of the items remaining in your game in this case so you do not block anyone else.
2. For your first game keep `Hint Cost` at 10%
4. Select your zip file
#### Generating Locally
1. Navigate to your Archipelago install directory
2. Empty the `Players` directory then fill it with one yaml per player including your own which you got from the
finalfantasyrandomizer website above
3. Run `ArchipelagoGenerate.exe` (double-click it in File Explorer)
4. You will find your generated game in the `output` directory
#### Starting the server
If you generated on Archipelago.gg click `Create New Room` on the results page to start your server
If you generated locally simply navigate to the [Host Game Page](https://archipelago.gg/uploads) and upload the file
in the `output` directory
### Running the Client Program and Connecting to the Server
1. Navigate to your Archipelago install folder and run `ArchipelagoFF1Client.exe`
2. Notice the `/connect command` on the server hosting page (It should look like `/connect archipelago.gg:*****` where
***** are numbers)
3. Type the connect command into the client OR add the port to the pre-populated address on the top bar (it should
already say `archipelago.gg`) and click `connect`
#### Running Your Game and Connecting to the Client Program
1. Open Bizhawk 2.3.1 or higher and load your ROM OR
click your ROM file if it is already associated with the extension `*.nes`
2. Click on the Tools menu and click on **Lua Console**
3. Click the folder button to open a new Lua script. (CTL-O or **Script** -> **Open Script**)
4. Navigate to the location you installed Archipelago to. Open data/lua/FF1/ff1_connector.lua
1. If it gives a `NLua.Exceptions.LuaScriptException: .\socket.lua:13: module 'socket.core' not found:` exception
close your emulator entirely, restart it and re-run these steps
2. If it says `Must use a version of bizhawk 2.3.1 or higher`, double-check your Bizhawk version by clicking
**Help** -> **About**
### Play the game
When the client shows both NES and server are connected you are good to go. You can check the connection status of the
NES at any time by running `/nes`
### Helpful Commands
Commands are broken into two types: `/` and `!` commands. The difference is that `/commands` are local to your machine
and game whereas `!` commands ask the server. Most of the time you can use local commands.
#### Local Commands
- `/connect <address with port number>` connect to the multiworld server
- `/disconnect` if you accidentally connected to the wrong port run this to disconnect and then reconnect using
- `/nes` Shows the current status of the NES connection
- `/received` Displays all the items you have found or been sent
- `/missing` Displays all the locations along with their current status (checked/missing)
- Just typing anything will broadcast a message to all players
#### Remote Commands
- `!hint <item name>` Tells you at which location in whose game your Item is. Note you need to have checked some locations
to earn a hint. You can check how many you have by just running `!hint`
- `!forfeit` If you didn't turn on auto-forfeit or you allowed forfeiting prior to goal completion. Remember that
"forfeiting" actually means sending out your remaining items in your world
#### Host only (on Archipelago.gg)
`/forfeit <Player Name>` Forfeits someone regardless of settings and game completion status

View File

@@ -1,11 +1,9 @@
# Minecraft Randomizer Setup Guide
#Automatic Hosting Install
- download and install [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) and choose the `Minecraft Client` module
## Required Software
- [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-edition)
- [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-edition) (update 1.17.1)
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) (select `Minecraft Client` during installation.)
## Configuring your YAML file
@@ -16,73 +14,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?
A basic minecraft yaml will look like this.
```yaml
description: Basic Minecraft Yaml
# Your name in-game. Spaces will be replaced with underscores and
# there is a 16 character limit
name: YourName
game: Minecraft
# Shared Options supported by all games:
accessibility: locations
progression_balancing: on
# Minecraft Specific Options
Minecraft:
# Number of advancements required (87 max) to spawn the Ender Dragon and complete the game.
advancement_goal: 50
# Number of dragon egg shards to collect (30 max) before the Ender Dragon will spawn.
egg_shards_required: 10
# Number of egg shards available in the pool (30 max).
egg_shards_available: 15
# Modifies the level of items logically required for
# exploring dangerous areas and fighting bosses.
combat_difficulty:
easy: 0
normal: 1
hard: 0
# Junk-fills certain RNG-reliant or tedious advancements.
include_hard_advancements:
on: 0
off: 1
# Junk-fills extremely difficult advancements;
# this is only How Did We Get Here? and Adventuring Time.
include_insane_advancements:
on: 0
off: 1
# Some advancements require defeating the Ender Dragon;
# this will junk-fill them, so you won't have to finish them to send some items.
include_postgame_advancements:
on: 0
off: 1
# Enables shuffling of villages, outposts, fortresses, bastions, and end cities.
shuffle_structures:
on: 0
off: 1
# Adds structure compasses to the item pool,
# which point to the nearest indicated structure.
structure_compasses:
on: 0
off: 1
# Replaces a percentage of junk items with bee traps
# which spawn multiple angered bees around every player when received.
bee_traps:
0: 1
25: 0
50: 0
75: 0
100: 0
```
you can customize your settings by visiting the [minecraft player settings](/games/Minecraft/player-settings)
## Joining a MultiWorld Game
@@ -93,38 +25,34 @@ When you join a multiworld game, you will be asked to provide your YAML file to
is done, the host will provide you with either a link to download your data file, or with a zip file containing
everyone's data files. Your data file should have a `.apmc` extension.
double click on your `.apmc` file to have the minecraft client auto-launch the installed forge server.
double-click on your `.apmc` file to have the minecraft client auto-launch the installed forge server.
make sure to leave this window open as this is your server console.
### Connect to the MultiServer
After having placed your data file in the `APData` folder, start the Forge server and make sure you have OP
status by typing `/op YourMinecraftUsername` in the forge server console then connecting in your Minecraft client.
Using minecraft 1.17.1 connect to the server `localhost`.
Once in game type `/connect <AP-Address> (Port) (Password)` where `<AP-Address>` is the address of the
Archipelago server. `(Port)` is only required if the Archipelago server is not using the default port of 38281. `(Password)`
is only required if the Archipleago server you are using has a password set.
### Play the game
When the console tells you that you have joined the room, you're ready to begin playing. Congratulations
When the console tells you that you have joined the room, you're all set. Congratulations
on successfully joining a multiworld game! At this point any additional minecraft players may connect to your
forge server.
forge server. to star the game once everyone is ready type `/start`.
### Useful commands
- `!help` displays a list all server commands
- `!hint` will display how many hint points you have, along with any hints that have been given that are related to your game.
- `!hint (item)` will ask the server to tell you where (item) is
- `!hint_location (location)` will ask the server to tell you what item is on (location)
## Manual Installation Procedures
this is only required if you wish to set up a forge install yourself, its recommended to just use the Archipelago Installer.
###Required Software
- [Minecraft Forge](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.16.5.html)
## Manual Installation
it is highly recommended to ues the Archipelago installer to handle the installation of the forge server for you.
support will not be given for those wishing to manually install forge. but for those of you who know how, and wish to
do so the following links are the versions of the software we use.
### Manual install Software links
- [Minecraft Forge](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.17.1.html)
- [Minecraft Archipelago Randomizer Mod](https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases)
**DO NOT INSTALL THIS ON YOUR CLIENT**
### Dedicated Server Setup
Only one person has to do this setup and host a dedicated server for everyone else playing to connect to.
1. Download the 1.16.5 **Minecraft Forge** installer from the link above, making sure to download the most recent recommended version.
- [Java 16](https://docs.aws.amazon.com/corretto/latest/corretto-16-ug/downloads-list.html)
2. Run the `forge-1.16.5-xx.x.x-installer.jar` file and choose **install server**.
- On this page you will also choose where to install the server to remember this directory it's important in the next step.
3. Navigate to where you installed the server and open `forge-1.16.5-xx.x.x.jar`
- Upon first launch of the server it will close and ask you to accept Minecraft's EULA. There will be a new file called `eula.txt` that contains a link to Minecraft's EULA, and a line that you need to change to `eula=true` to accept Minecraft's EULA.
- This will create the appropriate directories for you to place the files in the following step.
4. Place the `aprandomizer-x.x.x.jar` from the link above file into the `mods` folder of the above installation of your forge server.
- Once again run the server, it will load up and generate the required directory `APData` for when you are ready to play a game!

View File

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

View File

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

View File

@@ -4,7 +4,7 @@
"tutorials": [
{
"name": "Multiworld Setup Tutorial",
"description": "A Guide to setting up the Archipelago software to generate multiworld games on your computer.",
"description": "A guide to setting up the Archipelago software to generate and host multiworld games on your computer and using the website.",
"files": [
{
"language": "English",
@@ -15,6 +15,48 @@
]
}
]
},
{
"name": "Using Advanced Settings",
"description": "A guide to reading yaml files and editing them to fully customize your game.",
"files": [
{
"language": "English",
"filename": "archipelago/advanced_settings_en.md",
"link": "archipelago/advanced_settings/en",
"authors": [
"alwaysintreble"
]
}
]
},
{
"name": "Archipelago Triggers Guide",
"description": "A guide to setting up and using triggers in your game settings.",
"files": [
{
"language": "English",
"filename": "archipelago/triggers_en.md",
"link": "archipelago/triggers/en",
"authors": [
"alwaysintreble"
]
}
]
},
{
"name": "Archipelago Plando Guide",
"description": "A guide to understanding and using plando for your game.",
"files": [
{
"language": "English",
"filename": "archipelago/plando_en.md",
"link": "archipelago/plando/en",
"authors": [
"alwaysintreble"
]
}
]
}
]
},
@@ -144,7 +186,8 @@
"filename": "factorio/setup_en.md",
"link": "factorio/setup/en",
"authors": [
"Berserker"
"Berserker",
"Farrak Kilhn"
]
}
]
@@ -223,5 +266,81 @@
]
}
]
},
{
"gameTitle": "Subnautica",
"tutorials": [
{
"name": "Multiworld Setup Guide",
"description": "A guide to setting up the Subnautica randomizer connected to an Archipelago Multiworld",
"files": [
{
"language": "English",
"filename": "Subnautica/setup_en.md",
"link": "Subnautica/setup/en",
"authors": [
"Berserker"
]
}
]
}
]
},
{
"gameTitle": "Super Metroid",
"tutorials": [
{
"name": "Multiworld Setup Guide",
"description": "A guide to setting up the Super Metroid Client on your computer. This guide covers single-player, multiworld, and related software.",
"files": [
{
"language": "English",
"filename": "super-metroid/multiworld_en.md",
"link": "super-metroid/multiworld/en",
"authors": [
"Farrak Kilhn"
]
}
]
}
]
},
{
"gameTitle": "Secret of Evermore",
"tutorials": [
{
"name": "Multiworld Setup Guide",
"description": "A guide to playing Secret of Evermore randomizer. This guide covers single-player, multiworld and related software.",
"files": [
{
"language": "English",
"filename": "secret-of-evermore/multiworld_en.md",
"link": "secret-of-evermore/multiworld/en",
"authors": [
"Black Sliver"
]
}
]
}
]
},
{
"gameTitle": "Final Fantasy",
"tutorials": [
{
"name": "Multiworld Setup Guide",
"description": "A guide to playing Final Fantasy multiworld. This guide only covers playing multiworld.",
"files": [
{
"language": "English",
"filename": "ff1/multiworld_en.md",
"link": "ff1/multiworld/en",
"authors": [
"jat2980"
]
}
]
}
]
}
]

View File

@@ -74,3 +74,4 @@ Below is a list of MSU packs which, so far as we know, are safe to stream. More
we learn of them. If you know of any we missed, please let us know!
- Vanilla Game Music
- [Smooth McGroove](https://drive.google.com/open?id=1JDa1jCKg5hG0Km6xNpmIgf4kDMOxVp3n)
- Zelda community member Amarith assembled the following list for the purpose of competitive restreams. While we have not ourselves verified this list, all submissions required VoD proof they were not muted. Generally speaking, MSU-1 packs are less safe if they contain lyrics at any point. This list was only tested on Twitch and results for other platforms may vary. [Restream-Safe List](https://tinyurl.com/MSUsApprovedForLeagueChannels)

View File

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

View File

@@ -31,8 +31,8 @@
- Example: `simple`
- Runs a particular boss shuffle mode to finish construction instead of vanilla placement, typically used as
a last instruction.
- [Available Bosses](https://github.com/Berserker66/MultiWorld-Utilities/blob/65fa39df95c90c9b66141aee8b16b7e560d00819/Bosses.py#L135)
- [Available Arenas](https://github.com/Berserker66/MultiWorld-Utilities/blob/65fa39df95c90c9b66141aee8b16b7e560d00819/Bosses.py#L186)
- [Available Bosses](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Bosses.py#L135)
- [Available Arenas](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Bosses.py#L150)
#### Examples
```yaml
@@ -79,8 +79,8 @@ boss_shuffle:
- placements are picked randomly, not sorted in any way
- Warning: Placing non-Dungeon Prizes on Prize locations and
Prizes on non-Prize locations will break the game in various ways.
- [Available Items](https://github.com/Berserker66/MultiWorld-Utilities/blob/3b5ba161dea223b96e9b1fc890e03469d9c6eb59/Items.py#L26)
- [Available Locations](https://github.com/Berserker66/MultiWorld-Utilities/blob/3b5ba161dea223b96e9b1fc890e03469d9c6eb59/Regions.py#L418)
- [Available Items](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Items.py#L52)
- [Available Locations](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Regions.py#L434)
#### Examples
```yaml
@@ -133,10 +133,10 @@ Link's House and removes the picked item from the item pool.
- `\n` is a newline.
- `@` is the entered player's name.
- Warning: Text Mapper does not support full unicode.
- [Alphabet](https://github.com/Berserker66/MultiWorld-Utilities/blob/65fa39df95c90c9b66141aee8b16b7e560d00819/Text.py#L756)
- [Alphabet](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Text.py#L758)
- at is the location within the game to attach the text to.
- can be weighted.
- [List of targets](https://github.com/Berserker66/MultiWorld-Utilities/blob/65fa39df95c90c9b66141aee8b16b7e560d00819/Text.py#L1498)
- [List of targets](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Text.py#L1499)
#### Example
```yaml
@@ -160,7 +160,7 @@ This has a 50% chance to trigger at all. If it does, it throws a coin between `u
- entrance is the overworld door
- exit is the underworld exit
- direction can be `both`, `entrance` or `exit`
- doors can be found in [this file](https://github.com/Berserker66/MultiWorld-Utilities/blob/main/EntranceShuffle.py)
- doors can be found in [this file](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/EntranceShuffle.py#L3852)
#### Example

View File

@@ -28,7 +28,7 @@ can all have different options.
### Where do I get a YAML file?
A basic OOT yaml will look like this. (There are lots of cosmetic options that have been removed for the sake of this tutorial, if you want to see a complete list, download (Archipelago)[https://github.com/ArchipelagoMW/Archipelago/releases] and look for the sample file in the "Players" folder))
A basic OOT yaml will look like this. There are lots of cosmetic options that have been removed for the sake of this tutorial, if you want to see a complete list, download [Archipelago](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

File diff suppressed because it is too large Load Diff

View File

@@ -1,442 +0,0 @@
# What is this file?
# This file contains options which allow you to configure your multiworld experience while allowing others
# to play how they want as well.
# How do I use it?
# The options in this file are weighted. This means the higher number you assign to a value, the more
# chances you have for that option to be chosen. For example, an option like this:
#
# map_shuffle:
# on: 5
# off: 15
#
# Means you have 5 chances for map shuffle to occur, and 15 chances for map shuffle to be turned off
# I've never seen a file like this before. What characters am I allowed to use?
# This is a .yaml file. You are allowed to use most characters.
# To test if your yaml is valid or not, you can use this website:
# http://www.yamllint.com/
# For use with the weighted-settings page on the website. Changing this value will cause all users to be prompted
# to update their settings. The version number should match the current released version number, and the revision
# should be updated manually by whoever edits this file.
ws_version: 4.1.1 rev0
description: Template Name # Used to describe your yaml. Useful if you have multiple files
name: YourName # Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit
### Logic Section ###
glitches_required: # Determine the logic required to complete the seed
none: 50 # No glitches required
minor_glitches: 0 # Puts fake flipper, waterwalk, super bunny shenanigans, and etc into logic
overworld_glitches: 0 # Assumes the player has knowledge of both overworld major glitches (boots clips, mirror clips) and minor glitches (fake flipper, super bunny shenanigans, water walk and etc.)
no_logic: 0 # Your own items are placed with no regard to any logic; such as your Fire Rod can be on your Trinexx.
# Other players items are placed into your world under OWG logic
dark_room_logic: # Logic for unlit dark rooms
lamp: 50 # require the Lamp for these rooms to be considered accessible.
torches: 0 # in addition to lamp, allow the fire rod and presence of easily accessible torches for access
none: 0 # all dark rooms are always considered doable, meaning this may force completion of rooms in complete darkness
restrict_dungeon_item_on_boss: # aka ambrosia boss items
on: 0 # prevents unshuffled compasses, maps and keys to be boss drops, they can still drop keysanity and other players' items
off: 50
### End of Logic Section ###
meta_ignore: # Nullify options specified in the meta.yaml file. Adding an option here guarantees it will not occur in your seed, even if the .yaml file specifies it
mode:
- inverted # Never play inverted seeds
retro:
- on # Never play retro seeds
weapons:
- swordless # Never play a swordless seed
map_shuffle: # Shuffle dungeon maps into the world and other dungeons, including other players' worlds
on: 0
off: 50
compass_shuffle: # Shuffle compasses into the world and other dungeons, including other players' worlds
on: 0
off: 50
smallkey_shuffle: # Shuffle small keys into the world and other dungeons, including other players' worlds
on: 0
universal: 0 # allows small keys to be used in any dungeon and adds shops to buy more
off: 50
bigkey_shuffle: # Shuffle big keys into the world and other dungeons, including other players' worlds
on: 0
off: 50
local_keys: # Keep small keys and big keys local to your world
on: 0
off: 50
dungeon_items: # Alternative to the 4 shuffles and local_keys above this, does nothing until the respective 4 shuffles and local_keys above are deleted
mc: 0 # Shuffle maps and compasses
none: 50 # Shuffle none of the 4
mcsb: 0 # Shuffle all of the 4, any combination of m, c, s and b will shuffle the respective item, or not if it's missing, so you can add more options here
lmcsb: 0 # Like mcsb above, but with keys kept local to your world. l is what makes your keys local, or not if it's missing
ub: 0 # universal small keys and shuffled big keys
# you can add more combos of these letters here
dungeon_counters:
on: 0 # Always display amount of items checked in a dungeon
pickup: 50 # Show when compass is picked up
default: 0 # Show when compass is picked up if the compass itself is shuffled
off: 0 # Never show item count in dungeons
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
progressive: # Enable or disable progressive items (swords, shields, bow)
on: 50 # All items are progressive
off: 0 # No items are progressive
random: 0 # Randomly decides for all items. Swords could be progressive, shields might not be
entrance_shuffle: # Documentation: https://alttpr.com/en/options#entrance_shuffle
none: 50 # Vanilla game map. All entrances and exits lead to their original locations. You probably want this option
dungeonssimple: 0 # Shuffle just dungeons amongst each other, swapping dungeons entirely, so Hyrule Castle is always 1 dungeon
dungeonsfull: 0 # Shuffle any dungeon entrance with any dungeon interior, so Hyrule Castle can be 4 different dungeons
simple: 0 # Entrances are grouped together before being randomized. Simple uses the most strict grouping rules
restricted: 0 # Less strict than simple
full: 0 # Less strict than restricted
crossed: 0 # Less strict than full
insanity: 0 # Very few grouping rules. Good luck
goals:
ganon: 50 # Climb GT, defeat Agahnim 2, and then kill Ganon
fast_ganon: 0 # Only killing Ganon is required. The hole is always open. However, items may still be placed in GT
dungeons: 0 # Defeat the boss of all dungeons, including Agahnim's tower and GT (Aga 2)
pedestal: 0 # Pull the Triforce from the Master Sword pedestal
ganon_pedestal: 0 # Pull the Master Sword pedestal, then kill Ganon
triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then turn them in to Murahadala in front of Hyrule Castle
local_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout your world, then turn them in to Murahadala in front of Hyrule Castle
ganon_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then kill Ganon
local_ganon_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout your world, then kill Ganon
ice_rod_hunt: 0 # You start with everything needed to 216 the seed. Find the Ice rod, then kill Trinexx at Turtle rock.
pyramid_open:
goal: 50 # Opens the pyramid if the goal requires you to kill Ganon, unless the goal is Slow Ganon or All Dungeons
auto: 0 # Same as Goal, but also opens when any non-dungeon entrance shuffle is used
yes: 0 # Pyramid hole is always open. Ganon's vulnerable condition is still required before he can he hurt
no: 0 # Pyramid hole is always closed until you defeat Agahnim atop Ganon's Tower
triforce_pieces_mode: #Determine how to calculate the extra available triforce pieces.
extra: 0 # available = triforce_pieces_extra + triforce_pieces_required
percentage: 0 # available = (triforce_pieces_percentage /100) * triforce_pieces_required
available: 50 # available = triforce_pieces_available
triforce_pieces_extra: # Set to how many extra triforces pieces are available to collect in the world.
# Format "pieces: chance"
0: 0
5: 50
10: 50
15: 0
20: 0
triforce_pieces_percentage: # Set to how many triforce pieces according to a percentage of the required ones, are available to collect in the world.
# Format "pieces: chance"
100: 0 #No extra
150: 50 #Half the required will be added as extra
200: 0 #There are the double of the required ones available.
triforce_pieces_available: # Set to how many triforces pieces are available to collect in the world. Default is 30. Max is 90, Min is 1
# Format "pieces: chance"
25: 0
30: 50
40: 0
50: 0
triforce_pieces_required: # Set to how many out of X triforce pieces you need to win the game in a triforce hunt. Default is 20. Max is 90, Min is 1
# Format "pieces: chance"
15: 0
20: 50
30: 0
40: 0
50: 0
tower_open: # Crystals required to open GT
'0': 80
'1': 70
'2': 60
'3': 50
'4': 40
'5': 30
'6': 20
'7': 10
random: 0
ganon_open: # Crystals required to hurt Ganon
'0': 80
'1': 70
'2': 60
'3': 50
'4': 40
'5': 30
'6': 20
'7': 10
random: 0
mode:
standard: 50 # Begin the game by rescuing Zelda from her cell and escorting her to the Sanctuary
open: 50 # Begin the game from your choice of Link's House or the Sanctuary
inverted: 0 # Begin in the Dark World. The Moon Pearl is required to avoid bunny-state in Light World, and the Light World game map is altered
retro:
on: 0 # you must buy a quiver to use the bow, take-any caves and an old-man cave are added to the world. You may need to find your sword from the old man's cave
off: 50
hints:
'on': 50 # Hint tiles sometimes give item location hints
'off': 0 # Hint tiles provide gameplay tips
weapons: # Specifically, swords
randomized: 0 # Swords are placed randomly throughout the world
assured: 50 # Begin with a sword, the rest are placed randomly throughout the world
vanilla: 0 # Swords are placed in vanilla locations in your own game (Uncle, Pyramid Fairy, Smiths, Pedestal)
swordless: 0 # Your swords are replaced by rupees. Gameplay changes have been made to accommodate this change
item_pool:
easy: 0 # Doubled upgrades, progressives, and etc
normal: 50 # Item availability remains unchanged from vanilla game
hard: 0 # Reduced upgrade availability (max: 14 hearts, blue mail, tempered sword, fire shield, no silvers unless swordless)
expert: 0 # Minimum upgrade availability (max: 8 hearts, green mail, master sword, fighter shield, no silvers unless swordless)
item_functionality:
easy: 0 # Allow Hammer to damage ganon, Allow Hammer tablet collection, Allow swordless medallion use everywhere.
normal: 50 # Vanilla item functionality
hard: 0 # Reduced helpfulness of items (potions less effective, can't catch faeries, cape uses double magic, byrna does not grant invulnerability, boomerangs do not stun, silvers disabled outside ganon)
expert: 0 # Vastly reduces the helpfulness of items (potions barely effective, can't catch faeries, cape uses double magic, byrna does not grant invulnerability, boomerangs and hookshot do not stun, silvers disabled outside ganon)
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 around missing items.
tile_shuffle: # Randomize the tile layouts in flying tile rooms
on: 0
off: 50
misery_mire_medallion: # required medallion to open Misery Mire front entrance
random: 50
ether: 0
bombos: 0
quake: 0
turtle_rock_medallion: # required medallion to open Turtle Rock front entrance
random: 50
ether: 0
bombos: 0
quake: 0
### Enemizer Section ###
boss_shuffle:
none: 50 # Vanilla bosses
simple: 0 # Existing bosses except Ganon and Agahnim are shuffled throughout dungeons
full: 0 # 3 bosses can occur twice
random: 0 # Any boss can appear any amount of times
singularity: 0 # Picks a boss, tries to put it everywhere that works, if there's spaces remaining it picks a boss to fill those
enemy_shuffle: # Randomize enemy placement
on: 0
off: 50
killable_thieves: # Make thieves killable
on: 0 # Usually turned on together with enemy_shuffle to make annoying thief placement more manageable
off: 50
bush_shuffle: # Randomize the chance that bushes have enemies and the enemies under said bush
on: 0
off: 50
enemy_damage:
default: 50 # Vanilla enemy damage
shuffled: 0 # Enemies deal 0 to 4 hearts and armor helps
random: 0 # Enemies deal 0 to 8 hearts and armor just reshuffles the damage
enemy_health:
default: 50 # Vanilla enemy HP
easy: 0 # Enemies have reduced health
hard: 0 # Enemies have increased health
expert: 0 # Enemies have greatly increased health
pot_shuffle:
'on': 0 # Keys, items, and buttons hidden under pots in dungeons are shuffled with other pots in their supertile
'off': 50 # Default pot item locations
### End of Enemizer Section ###
beemizer: # Remove items from the global item pool and replace them with single bees and bee traps
0: 50 # No bee traps are placed
1: 0 # 25% of rupees, bombs and arrows are replaced with bees, of which 60% are traps and 40% single bees
2: 0 # 50% of rupees, bombs and arrows are replaced with bees, of which 70% are traps and 30% single bees
3: 0 # 75% of rupees, bombs and arrows are replaced with bees, of which 80% are traps and 20% single bees
4: 0 # 100% of rupees, bombs and arrows are replaced with bees, of which 90% are traps and 10% single bees
### Shop Settings ###
shop_shuffle_slots: # Maximum amount of shop slots to be filled with regular item pool items (such as Moon Pearl)
0: 50
10: 0
20: 0
30: 0
shop_shuffle:
none: 50
g: 0 # Generate new default inventories for overworld/underworld shops, and unique shops
f: 0 # Generate new default inventories for every shop independently
p: 0 # Randomize the prices of the items in shop inventories
u: 0 # Shuffle capacity upgrades into the item pool (and allow them to traverse the multiworld)
w: 0 # Consider witch's hut like any other shop and shuffle/randomize it too
gp: 0 # Shuffle inventories and randomize prices
fp: 0 # Randomize items in every shop and their prices
ufp: 0 # Randomize items and prices in every shop, and include capacity upgrades in item pool
wfp: 0 # Randomize items and prices in every shop, and include potion shop inventory in shuffle
ufpw: 0 # Randomize items and prices in every shop, shuffle potion shop inventory, and include capacity upgrades
# You can add more combos
### End of Shop Section ###
shuffle_prizes: # aka drops
none: 0 # do not shuffle prize packs
g: 50 # shuffle "general" price packs, as in enemy, tree pull, dig etc.
b: 0 # shuffle "bonk" price packs
bg: 0 # shuffle both
timer:
none: 50 # No timer will be displayed.
timed: 0 # Starts with clock at zero. Green clocks subtract 4 minutes (total 20). Blue clocks subtract 2 minutes (total 10). Red clocks add two minutes (total 10). Winner is the player with the lowest time at the end.
timed_ohko: 0 # Starts the clock at ten minutes. Green clocks add five minutes (total 25). As long as the clock as at zero, Link will die in one hit.
ohko: 0 # Timer always at zero. Permanent OHKO.
timed_countdown: 0 # Starts the clock with forty minutes. Same clocks as timed mode, but if the clock hits zero you lose. You can still keep playing, though.
display: 0 # Displays a timer, but otherwise does not affect gameplay or the item pool.
countdown_start_time: # For timed_ohko and timed_countdown timer modes, the amount of time in minutes to start with
0: 0 # For timed_ohko, starts in OHKO mode when starting the game
10: 50
20: 0
30: 0
60: 0
red_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a red clock
-2: 50
1: 0
blue_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a blue clock
1: 0
2: 50
green_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a green clock
4: 50
10: 0
15: 0
# Can be uncommented to use it
# 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"
# Can be uncommented to use it
# startinventory: # Begin the file with the listed items/upgrades
# Pegasus Boots: on
# Bomb Upgrade (+10): 4
# Arrow Upgrade (+10): 4
glitch_boots:
on: 50 # Start with Pegasus Boots in any glitched logic mode that makes use of them
off: 0
linked_options:
- name: crosskeys
options: # These overwrite earlier options if the percentage chance triggers
entrance_shuffle: crossed
bigkey_shuffle: true
compass_shuffle: true
map_shuffle: true
smallkey_shuffle: true
percentage: 0 # Set this to the percentage chance you want crosskeys
- name: localcrosskeys
options: # These overwrite earlier options if the percentage chance triggers
entrance_shuffle: crossed
bigkey_shuffle: true
compass_shuffle: true
map_shuffle: true
smallkey_shuffle: true
local_items: # Forces keys to be local to your own world
- "Small Keys"
- "Big Keys"
percentage: 0 # Set this to the percentage chance you want local crosskeys
- name: enemizer
options:
boss_shuffle: # Subchances can be injected too, which then get rolled
simple: 1
full: 1
random: 1
singularity: 1
enemy_damage:
shuffled: 1
random: 1
enemy_health:
easy: 1
hard: 1
expert: 1
percentage: 0 # Set this to the percentage chance you want enemizer
### door rando only options ###
door_shuffle: # Only available if the host uses the doors branch, it is ignored otherwise
vanilla: 50 # Everything should be like in vanilla
basic: 0 # Dungeons are shuffled within themselves
crossed: 0 # Dungeons are shuffled across each other
intensity: # Only available if the host uses the doors branch, it is ignored otherwise
1: 50 # Shuffles normal doors and spiral staircases
2: 0 # And shuffles open edges and straight staircases
3: 0 # And shuffles dungeon lobbies
random: 0 # Picks one of those at random
key_drop_shuffle: # Only available if the host uses the doors branch, it is ignored otherwise
on: 0 # Enables the small keys dropped by enemies or under pots, and the big key dropped by the Ball & Chain guard to be shuffled into the pool. This extends the number of checks to 249.
off: 50
experimental: # Only available if the host uses the doors branch, it is ignored otherwise
on: 0 # Enables experimental features.
off: 50
debug: # Only available if the host uses the doors branch, it is ignored otherwise
on: 0 # Enables debugging features. Currently, these are the Item collection counter. (overwrites total triforce pieces) and Castle Gate closed indicator.
off: 50
### end of door rando only options ###
rom:
#sprite_pool: # When specified, limits the pool of sprites used for randomon-event to the specified pool. Uncomment to use this.
# - link
# - pride link
# - penguin link
# - random # You can specify random multiple times for however many potentially unique random sprites you want in your pool.
sprite: # Enter the name of your preferred sprite and weight it appropriately
random: 0
link: 50 # To add other sprites: open the gui/Creator, go to adjust, select a sprite and write down the name the gui calls it
disablemusic: # If "on", all in-game music will be disabled
on: 0
off: 50
quickswap: # Enable switching items by pressing the L+R shoulder buttons
on: 50
off: 0
triforcehud: # Disable visibility of the triforce hud unless collecting a piece or speaking to Murahadala
normal: 50 # original behavior (always visible)
hide_goal: 0 # hide counter until a piece is collected or speaking to Murahadala
hide_required: 0 # Always visible, but required amount is invisible until determined by Murahadala
hide_both: 0 # Hide both under above circumstances
reduceflashing: # Reduces instances of flashing such as lightning attacks, weather, ether and more.
on: 50
off: 0
menuspeed: # Controls how fast the item menu opens and closes
normal: 50
instant: 0
double: 0
triple: 0
quadruple: 0
half: 0
heartcolor: # Controls the color of your health hearts
red: 50
blue: 0
green: 0
yellow: 0
random: 0
heartbeep: # Controls the frequency of the low-health beeping
double: 0
normal: 50
half: 0
quarter: 0
off: 0
ow_palettes: # Change the colors of the overworld
default: 50 # No changes
random: 0 # Shuffle the colors, with harmony in mind
blackout: 0 # everything black / blind mode
grayscale: 0
negative: 0
classic: 0
dizzy: 0
sick: 0
puke: 0
uw_palettes: # Change the colors of caves and dungeons
default: 50 # No changes
random: 0 # Shuffle the colors, with harmony in mind
blackout: 0 # everything black / blind mode
grayscale: 0
negative: 0
classic: 0
dizzy: 0
sick: 0
puke: 0
hud_palettes: # Change the colors of the hud
default: 50 # No changes
random: 0 # Shuffle the colors, with harmony in mind
blackout: 0 # everything black / blind mode
grayscale: 0
negative: 0
classic: 0
dizzy: 0
sick: 0
puke: 0
sword_palettes: # Change the colors of swords
default: 50 # No changes
random: 0 # Shuffle the colors, with harmony in mind
blackout: 0 # everything black / blind mode
grayscale: 0
negative: 0
classic: 0
dizzy: 0
sick: 0
puke: 0
shield_palettes: # Change the colors of shields
default: 50 # No changes
random: 0 # Shuffle the colors, with harmony in mind
blackout: 0 # everything black / blind mode
grayscale: 0
negative: 0
classic: 0
dizzy: 0
sick: 0
puke: 0

View File

@@ -80,12 +80,18 @@
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5);
}
.markdown ul{
.markdown h4, .markdown h5,.markdown h6{
margin-bottom: 0.5rem;
}
.markdown ul{
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.markdown ol{
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.markdown li{

View File

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

View File

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

View File

@@ -34,7 +34,14 @@
<table>
<tbody>
<tr>
<td><label for="forfeit_mode">Forfeit Permission:</label></td>
<td>
<label for="forfeit_mode">Forfeit Permission:</label>
<span
class="interactive"
data-tooltip="A forfeit releases all remaining items from the locations
in your world.">(?)
</span>
</td>
<td>
<select name="forfeit_mode" id="forfeit_mode">
<option value="auto">Automatic on goal completion</option>
@@ -46,12 +53,49 @@
</td>
</tr>
<tr>
<td>
<label for="collect_mode">Collect Permission:</label>
<span
class="interactive"
data-tooltip="A collect releases all of your remaining items to you
from across the multiworld.">(?)
</span>
</td>
<td>
<select name="collect_mode" id="collect_mode">
<option value="goal">Allow !collect after goal completion</option>
<option value="auto">Automatic on goal completion</option>
<option value="auto-enabled">Automatic on goal completion and manual !collect</option>
<option value="enabled">Manual !collect</option>
<option value="disabled">Disabled</option>
</select>
</td>
</tr>
<tr>
<td>
<label for="remaining_mode">Remaining Permission:</label>
<span
class="interactive"
data-tooltip="Remaining lists all items still in your world by name only.">(?)
</span>
</td>
<td>
<select name="remaining_mode" id="remaining_mode">
<option value="disabled">Disabled</option>
<option value="goal">Allow !remaining after goal completion</option>
<option value="enabled">Manual !remaining</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>
class="interactive"
data-tooltip="After gathering this many checks, players can !hint <itemname>
to get the location of that hint item.">(?)
</span>
</td>

View File

@@ -9,7 +9,7 @@
{% include 'header/grassHeader.html' %}
<div id="host-room">
{% if room.owner == session["_id"] %}
Room created from <a href="{{ url_for("viewSeed", seed=room.seed.id) }}">Seed #{{ room.seed.id|suuid }}</a>
Room created from <a href="{{ url_for("view_seed", seed=room.seed.id) }}">Seed #{{ room.seed.id|suuid }}</a>
<br>
{% endif %}
{% if room.tracker %}
@@ -20,7 +20,7 @@
later,
you can simply refresh this page and the server will be started again.<br>
{% if room.last_port %}
You can connect to this room by using '/connect archipelago.gg:{{ room.last_port }}'
You can connect to this room by using '/connect {{ config['PATCH_TARGET'] }}:{{ room.last_port }}'
in the <a href="{{ url_for("tutorial_landing")}}">client</a>.<br>{% endif %}
{{ macros.list_patches_room(room) }}
{% if room.owner == session["_id"] %}

View File

@@ -1,7 +1,7 @@
{% macro list_rooms(rooms) -%}
<ul>
{% for room in rooms %}
<li><a href="{{ url_for("hostRoom", room=room.id) }}">Room #{{ room.id|suuid }}</a></li>
<li><a href="{{ url_for("host_room", room=room.id) }}">Room #{{ room.id|suuid }}</a></li>
{% endfor %}
{{ caller() }}
</ul>

View File

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

View File

@@ -0,0 +1,85 @@
<!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/supermetroidTracker.css') }}"/>
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/supermetroidTracker.js') }}"></script>
</head>
<body>
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
<table id="inventory-table">
<tr>
<td><img src="{{ icons['Charge Beam'] }}" class="{{ 'acquired' if 'Charge Beam' in acquired_items }}" title="Charge Beam" /></td>
<td><img src="{{ icons['Ice Beam'] }}" class="{{ 'acquired' if 'Ice Beam' in acquired_items }}" title="Ice Beam" /></td>
<td><img src="{{ icons['Wave Beam'] }}" class="{{ 'acquired' if 'Wave Beam' in acquired_items }}" title="Wave Beam" /></td>
<td><img src="{{ icons['Spazer'] }}" class="{{ 'acquired' if 'Spazer' in acquired_items }}" title="S p a z e r" /></td>
<td><img src="{{ icons['Plasma Beam'] }}" class="{{ 'acquired' if 'Plasma Beam' in acquired_items }}" title="Plasma Beam" /></td>
<td><img src="{{ icons['Varia Suit'] }}" class="{{ 'acquired' if 'Varia Suit' in acquired_items }}" title="Varia Suit" /></td>
<td><img src="{{ icons['Gravity Suit'] }}" class="{{ 'acquired' if 'Gravity Suit' in acquired_items }}" title="Gravity Suit" /></td>
</tr>
<tr>
<td><img src="{{ icons['Morph Ball'] }}" class="{{ 'acquired' if 'Morph Ball' in acquired_items }}" title="Morph Ball" /></td>
<td><img src="{{ icons['Bomb'] }}" class="{{ 'acquired' if 'Bomb' in acquired_items }}" title="Bomb" /></td>
<td><img src="{{ icons['Spring Ball'] }}" class="{{ 'acquired' if 'Spring Ball' in acquired_items }}" title="Spring Ball" /></td>
<td><img src="{{ icons['Screw Attack'] }}" class="{{ 'acquired' if 'Screw Attack' in acquired_items }}" title="Screw Attack" /></td>
<td><img src="{{ icons['Hi-Jump Boots'] }}" class="{{ 'acquired' if 'Hi-Jump Boots' in acquired_items }}" title="Hi-Jump Boots" /></td>
<td><img src="{{ icons['Space Jump'] }}" class="{{ 'acquired' if 'Space Jump' in acquired_items }}" title="Space Jump" /></td>
<td><img src="{{ icons['Speed Booster'] }}" class="{{ 'acquired' if 'Speed Booster' in acquired_items }}" title="Speed Booster" /></td>
</tr>
<tr>
<td>
<div class="counted-item">
<img src="{{ icons['Energy Tank'] }}" class="{{ 'acquired' if energy_count > 0 }}" title="Energy Tank" />
<div class="item-count">{{ energy_count }}</div>
</div>
</td>
<td>
<div class="counted-item">
<img src="{{ icons['Reserve Tank'] }}" class="{{ 'acquired' if reserve_count > 0 }}" title="Reserve Tank" />
<div class="item-count">{{ reserve_count }}</div>
</div>
</td>
<td>
<div class="counted-item">
<img src="{{ icons['Missile'] }}" class="{{ 'acquired' if missile_count > 0 }}" title="Missile ({{missile_count * 5}})" />
<div class="item-count">{{ missile_count }}</div>
</div>
</td>
<td>
<div class="counted-item">
<img src="{{ icons['Super Missile'] }}" class="{{ 'acquired' if super_count > 0 }}" title="Super Missile ({{super_count * 5}})" />
<div class="item-count">{{ super_count }}</div>
</div>
</td>
<td>
<div class="counted-item">
<img src="{{ icons['Power Bomb'] }}" class="{{ 'acquired' if power_count > 0 }}" title="Power Bomb ({{power_count * 5}})" />
<div class="item-count">{{ power_count }}</div>
</div>
</td>
<td><img src="{{ icons['Grappling Beam'] }}" class="{{ 'acquired' if 'Grappling Beam' in acquired_items }}" title="Grappling Beam" /></td>
<td><img src="{{ icons['X-Ray Scope'] }}" class="{{ 'acquired' if 'X-Ray Scope' in acquired_items }}" title="X-Ray Scope" /></td>
</tr>
</table>
<table id="location-table">
{% for area in checks_done %}
<tr class="location-category" id="{{area}}-header">
<td>{{ area }} {{'▼' if area != 'Total'}}</td>
<td class="counter">{{ checks_done[area] }} / {{ checks_in_area[area] }}</td>
</tr>
<tbody class="locations hide" id="{{area}}">
{% for location in location_info[area] %}
<tr>
<td class="location-name">{{ location }}</td>
<td class="counter">{{ '✔' if location_info[area][location] else '' }}</td>
</tr>
{% endfor %}
</tbody>
{% endfor %}
</table>
</div>
</body>
</html>

View File

@@ -0,0 +1,85 @@
<!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/timespinnerTracker.css') }}"/>
<script type="application/ecmascript" src="{{ url_for('static', filename='assets/timespinnerTracker.js') }}"></script>
</head>
<body>
<div id="player-tracker-wrapper" data-tracker="{{ room.tracker|suuid }}">
<table id="inventory-table">
<tr>
<td><img src="{{ icons['Timespinner Wheel'] }}" class="{{ 'acquired' if 'Timespinner Wheel' in acquired_items }}" title="Timespinner Wheel" /></td>
<td><img src="{{ icons['Timespinner Spindle'] }}" class="{{ 'acquired' if 'Timespinner Spindle' in acquired_items }}" title="Timespinner Spindle" /></td>
<td><img src="{{ icons['Timespinner Gear 1'] }}" class="{{ 'acquired' if 'Timespinner Gear 1' in acquired_items }}" title="Timespinner Gear 1" /></td>
<td><img src="{{ icons['Timespinner Gear 2'] }}" class="{{ 'acquired' if 'Timespinner Gear 2' in acquired_items }}" title="Timespinner Gear 2" /></td>
<td><img src="{{ icons['Timespinner Gear 3'] }}" class="{{ 'acquired' if 'Timespinner Gear 3' in acquired_items }}" title="Timespinner Gear 3" /></td>
</tr>
<tr>
<td><img src="{{ icons['Talaria Attachment'] }}" class="{{ 'acquired' if 'Talaria Attachment' in acquired_items }}" title="Talaria Attachment" /></td>
<td><img src="{{ icons['Succubus Hairpin'] }}" class="{{ 'acquired' if 'Succubus Hairpin' in acquired_items }}" title="Succubus Hairpin" /></td>
<td><img src="{{ icons['Lightwall'] }}" class="{{ 'acquired' if 'Lightwall' in acquired_items }}" title="Lightwall" /></td>
<td><img src="{{ icons['Celestial Sash'] }}" class="{{ 'acquired' if 'Celestial Sash' in acquired_items }}" title="Celestial Sash" /></td>
<td><img src="{{ icons['Twin Pyramid Key'] }}" class="{{ 'acquired' if 'Twin Pyramid Key' in acquired_items }}" title="Twin Pyramid Key" /></td>
</tr>
<tr>
<td><img src="{{ icons['Security Keycard A'] }}" class="{{ 'acquired' if 'Security Keycard A' in acquired_items }}" title="Security Keycard A" /></td>
<td><img src="{{ icons['Security Keycard B'] }}" class="{{ 'acquired' if 'Security Keycard B' in acquired_items }}" title="Security Keycard B" /></td>
<td><img src="{{ icons['Security Keycard C'] }}" class="{{ 'acquired' if 'Security Keycard C' in acquired_items }}" title="Security Keycard C" /></td>
<td><img src="{{ icons['Security Keycard D'] }}" class="{{ 'acquired' if 'Security Keycard D' in acquired_items }}" title="Security Keycard D" /></td>
<td><img src="{{ icons['Library Keycard V'] }}" class="{{ 'acquired' if 'Library Keycard V' in acquired_items }}" title="Library Keycard V" /></td>
</tr>
<tr>
<td><img src="{{ icons['Tablet'] }}" class="{{ 'acquired' if 'Tablet' in acquired_items }}" title="Tablet" /></td>
<td><img src="{{ icons['Elevator Keycard'] }}" class="{{ 'acquired' if 'Elevator Keycard' in acquired_items }}" title="Elevator Keycard" /></td>
<td><img src="{{ icons['Oculus Ring'] }}" class="{{ 'acquired' if 'Oculus Ring' in acquired_items }}" title="Oculus Ring" /></td>
<td><img src="{{ icons['Water Mask'] }}" class="{{ 'acquired' if 'Water Mask' in acquired_items }}" title="Water Mask" /></td>
<td><img src="{{ icons['Gas Mask'] }}" class="{{ 'acquired' if 'Gas Mask' in acquired_items }}" title="Gas Mask" /></td>
</tr>
<tr>
<td><img src="{{ icons['Kobo'] }}" class="{{ 'acquired' if 'Kobo' in acquired_items }}" title="Kobo" /></td>
<td><img src="{{ icons['Merchant Crow'] }}" class="{{ 'acquired' if 'Merchant Crow' in acquired_items }}" title="Merchant Crow" /></td>
{% if 'Djinn Inferno' in acquired_items %}
<td><img src="{{ icons['Djinn Inferno'] }}" class="acquired" title="Djinn Inferno" /></td>
{% elif 'Pyro Ring' in acquired_items %}
<td><img src="{{ icons['Pyro Ring'] }}" class="acquired" title="Pyro Ring" /></td>
{% elif 'Fire Orb' in acquired_items %}
<td><img src="{{ icons['Fire Orb'] }}" class="acquired" title="Fire Orb" /></td>
{% elif 'Infernal Flames' in acquired_items %}
<td><img src="{{ icons['Infernal Flames'] }}" class="acquired" title="Infernal Flames" /></td>
{% else %}
<td><img src="{{ icons['Djinn Inferno'] }}" title="Djinn Inferno" /></td>
{% endif %}
{% if 'Royal Ring' in acquired_items %}
<td><img src="{{ icons['Royal Ring'] }}" class="acquired" title="Royal Ring" /></td>
{% elif 'Plasma Geyser' in acquired_items %}
<td><img src="{{ icons['Plasma Geyser'] }}" class="acquired" title="Plasma Geyser" /></td>
{% elif 'Plasma Orb' in acquired_items %}
<td><img src="{{ icons['Plasma Orb'] }}" class="acquired" title="Plasma Orb" /></td>
{% else %}
<td><img src="{{ icons['Royal Ring'] }}" title="Royal Ring" /></td>
{% endif %}
</tr>
</table>
<table id="location-table">
{% for area in checks_done %}
<tr class="location-category" id="{{area}}-header">
<td>{{ area }} {{'▼' if area != 'Total'}}</td>
<td class="counter">{{ checks_done[area] }} / {{ checks_in_area[area] }}</td>
</tr>
<tbody class="locations hide" id="{{area}}">
{% for location in location_info[area] %}
<tr>
<td class="location-name">{{ location }}</td>
<td class="counter">{{ '✔' if location_info[area][location] else '' }}</td>
</tr>
{% endfor %}
</tbody>
{% endfor %}
</table>
</div>
</body>
</html>

View File

@@ -20,7 +20,7 @@
Multistream
</a>
</span>
<span class="info">This tracker will automatically update itself periodically.</span>
<span class="info">Clicking on a slot's number will bring up a slot-specific auto-tracker. This tracker will automatically update itself periodically.</span>
</div>
<div id="tables-container">
{% for team, players in inventory.items() %}

View File

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

View File

@@ -28,34 +28,16 @@
<td><a href="{{ url_for("download_spoiler", seed_id=seed.id) }}">Download</a></td>
</tr>
{% endif %}
{% if seed.multidata %}
<tr>
<td>Rooms:&nbsp;</td>
<td>
{% call macros.list_rooms(rooms) %}
<li>
<a href="{{ url_for("new_room", seed=seed.id) }}">Create New Room</a>
</li>
{% endcall %}
</td>
</tr>
{% else %}
<tr>
<td>Files:&nbsp;</td>
<td>
<ul>
{% for slot in seed.slots %}
<td>Rooms:&nbsp;</td>
<td>
{% call macros.list_rooms(rooms) %}
<li>
<a href="{{ url_for("download_raw_patch", seed_id=seed.id, player_id=slot.player_id) }}">Player {{ slot.player_name }}</a>
<a href="{{ url_for("new_room", seed=seed.id) }}">Create New Room</a>
</li>
{% endfor %}
</ul>
{% endcall %}
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -10,10 +10,7 @@ from pony.orm import flush, select
from WebHostLib import app, Seed, Room, Slot
from Utils import parse_yaml
accepted_zip_contents = {"patches": ".apbp",
"spoiler": ".txt",
"multidata": ".archipelago"}
from Patch import preferred_endings
banned_zip_contents = (".sfc",)
@@ -29,15 +26,17 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
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"):
elif file.filename.endswith(tuple(preferred_endings.values())):
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
return "Old format cannot be uploaded (outdated .apbp)"
metadata = yaml_data["meta"]
slots.add(Slot(data=data, player_name=metadata["player_name"],
slots.add(Slot(data=data,
player_name=metadata["player_name"],
player_id=metadata["player_id"],
game="A Link to the Past"))
game=yaml_data["game"]))
elif file.filename.endswith(".apmc"):
data = zfile.open(file, "r").read()
@@ -48,7 +47,7 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
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("-")
_, seed_name, slot_id, slot_name = file.filename.rsplit("_", 1)[0].split("-", 3)
slots.add(Slot(data=zfile.open(file, "r").read(), player_name=slot_name,
player_id=int(slot_id[1:]), game="Factorio"))
@@ -66,8 +65,8 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
MultiServer.Context._decompress(multidata)
except:
flash("Could not load multidata. File may be corrupted or incompatible.")
else:
multidata = zfile.open(file).read()
multidata = None
if multidata:
flush() # commit slots
seed = Seed(multidata=multidata, spoiler=spoiler, slots=slots, owner=owner, meta=json.dumps(meta),
@@ -99,7 +98,7 @@ def uploads():
if type(res) == str:
return res
elif res:
return redirect(url_for("viewSeed", seed=res.id))
return redirect(url_for("view_seed", seed=res.id))
else:
try:
multidata = file.read()
@@ -110,9 +109,9 @@ def uploads():
else:
seed = Seed(multidata=multidata, owner=session["_id"])
flush() # place into DB and generate ids
return redirect(url_for("viewSeed", seed=seed.id))
return redirect(url_for("view_seed", seed=seed.id))
else:
flash("Not recognized file format. Awaiting a .multidata file.")
flash("Not recognized file format. Awaiting a .archipelago file or .zip containing one.")
return render_template("hostGame.html")

Binary file not shown.

View File

@@ -1,9 +1,9 @@
<TabbedPanel>
tab_width: 200
<Row@Label>:
<SelectableLabel>:
canvas.before:
Color:
rgba: 0.2, 0.2, 0.2, 1
rgba: (.0, 0.9, .1, .3) if self.selected else (0.2, 0.2, 0.2, 1)
Rectangle:
size: self.size
pos: self.pos
@@ -13,13 +13,42 @@
font_size: dp(20)
markup: True
<UILog>:
viewclass: 'Row'
viewclass: 'SelectableLabel'
scroll_y: 0
effect_cls: "ScrollEffect"
RecycleBoxLayout:
SelectableRecycleBoxLayout:
default_size: None, dp(20)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
spacing: dp(3)
spacing: dp(3)
<ServerLabel>:
text: "Server:"
size_hint_x: None
<ContainerLayout>:
size_hint_x: 1
size_hint_y: 1
pos: (0, 0)
<ServerToolTip>:
size: self.texture_size
size_hint: None, None
font_size: dp(18)
pos_hint: {'center_y': 0.5, 'center_x': 0.5}
halign: "left"
canvas.before:
Color:
rgba: 0.2, 0.2, 0.2, 1
Rectangle:
size: self.size
pos: self.pos
Color:
rgba: 0.098, 0.337, 0.431, 1
Line:
width: 3
rectangle: self.x-2, self.y-2, self.width+4, self.height+4
Color:
rgba: 0.235, 0.678, 0.843, 1
Line:
width: 1
rectangle: self.x-2, self.y-2, self.width+4, self.height+4

BIN
data/lua/FF1/core.dll Normal file

Binary file not shown.

View File

@@ -0,0 +1,542 @@
local socket = require("socket")
local json = require('json')
local math = require('math')
local STATE_OK = "Ok"
local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected"
local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made"
local STATE_UNINITIALIZED = "Uninitialized"
local ITEM_INDEX = 0x03
local WEAPON_INDEX = 0x07
local ARMOR_INDEX = 0x0B
local goldLookup = {
[0x16C] = 10,
[0x16D] = 20,
[0x16E] = 25,
[0x16F] = 30,
[0x170] = 55,
[0x171] = 70,
[0x172] = 85,
[0x173] = 110,
[0x174] = 135,
[0x175] = 155,
[0x176] = 160,
[0x177] = 180,
[0x178] = 240,
[0x179] = 255,
[0x17A] = 260,
[0x17B] = 295,
[0x17C] = 300,
[0x17D] = 315,
[0x17E] = 330,
[0x17F] = 350,
[0x180] = 385,
[0x181] = 400,
[0x182] = 450,
[0x183] = 500,
[0x184] = 530,
[0x185] = 575,
[0x186] = 620,
[0x187] = 680,
[0x188] = 750,
[0x189] = 795,
[0x18A] = 880,
[0x18B] = 1020,
[0x18C] = 1250,
[0x18D] = 1455,
[0x18E] = 1520,
[0x18F] = 1760,
[0x190] = 1975,
[0x191] = 2000,
[0x192] = 2750,
[0x193] = 3400,
[0x194] = 4150,
[0x195] = 5000,
[0x196] = 5450,
[0x197] = 6400,
[0x198] = 6720,
[0x199] = 7340,
[0x19A] = 7690,
[0x19B] = 7900,
[0x19C] = 8135,
[0x19D] = 9000,
[0x19E] = 9300,
[0x19F] = 9500,
[0x1A0] = 9900,
[0x1A1] = 10000,
[0x1A2] = 12350,
[0x1A3] = 13000,
[0x1A4] = 13450,
[0x1A5] = 14050,
[0x1A6] = 14720,
[0x1A7] = 15000,
[0x1A8] = 17490,
[0x1A9] = 18010,
[0x1AA] = 19990,
[0x1AB] = 20000,
[0x1AC] = 20010,
[0x1AD] = 26000,
[0x1AE] = 45000,
[0x1AF] = 65000
}
local extensionConsumableLookup = {
[432] = 0x3C,
[436] = 0x3C,
[440] = 0x3C,
[433] = 0x3D,
[437] = 0x3D,
[441] = 0x3D,
[434] = 0x3E,
[438] = 0x3E,
[442] = 0x3E,
[435] = 0x3F,
[439] = 0x3F,
[443] = 0x3F
}
local itemMessages = {}
local consumableStacks = nil
local prevstate = ""
local curstate = STATE_UNINITIALIZED
local ff1Socket = nil
local frame = 0
local u8 = nil
local wU8 = nil
local isNesHawk = false
--Sets correct memory access functions based on whether NesHawk or QuickNES is loaded
local function defineMemoryFunctions()
local memDomain = {}
local domains = memory.getmemorydomainlist()
if domains[1] == "System Bus" then
--NesHawk
isNesHawk = true
memDomain["systembus"] = function() memory.usememorydomain("System Bus") end
memDomain["saveram"] = function() memory.usememorydomain("Battery RAM") end
memDomain["rom"] = function() memory.usememorydomain("PRG ROM") end
elseif domains[1] == "WRAM" then
--QuickNES
memDomain["systembus"] = function() memory.usememorydomain("System Bus") end
memDomain["saveram"] = function() memory.usememorydomain("WRAM") end
memDomain["rom"] = function() memory.usememorydomain("PRG ROM") end
end
return memDomain
end
local memDomain = defineMemoryFunctions()
u8 = memory.read_u8
wU8 = memory.write_u8
uRange = memory.readbyterange
local function StateOKForMainLoop()
memDomain.saveram()
local A = u8(0x102) -- Party Made
local B = u8(0x0FC)
local C = u8(0x0A3)
return A ~= 0x00 and not (A== 0xF2 and B == 0xF2 and C == 0xF2)
end
function table.empty (self)
for _, _ in pairs(self) do
return false
end
return true
end
function slice (tbl, s, e)
local pos, new = 1, {}
for i = s + 1, e do
new[pos] = tbl[i]
pos = pos + 1
end
return new
end
local bizhawk_version = client.getversion()
local is23Or24Or25 = (bizhawk_version=="2.3.1") or (bizhawk_version:sub(1,3)=="2.4") or (bizhawk_version:sub(1,3)=="2.5")
local is26To27 = (bizhawk_version:sub(1,3)=="2.6") or (bizhawk_version:sub(1,3)=="2.7")
local function getMaxMessageLength()
if is23Or24Or25 then
return client.screenwidth()/11
elseif is26To27 then
return client.screenwidth()/12
end
end
local function drawText(x, y, message, color)
if is23Or24Or25 then
gui.addmessage(message)
elseif is26To27 then
gui.drawText(x, y, message, color, 0xB0000000, 18, "Courier New", nil, nil, nil, "client")
end
end
local function clearScreen()
if is23Or24Or25 then
return
elseif is26To27 then
drawText(0, 0, "", "black")
end
end
local function drawMessages()
if table.empty(itemMessages) then
clearScreen()
return
end
local y = 10
found = false
maxMessageLength = getMaxMessageLength()
for k, v in pairs(itemMessages) do
if v["TTL"] > 0 then
message = v["message"]
while true do
drawText(5, y, message:sub(1, maxMessageLength), v["color"])
y = y + 16
message = message:sub(maxMessageLength + 1, message:len())
if message:len() == 0 then
break
end
end
newTTL = 0
if is26To27 then
newTTL = itemMessages[k]["TTL"] - 1
end
itemMessages[k]["TTL"] = newTTL
found = true
end
end
if found == false then
clearScreen()
end
end
function generateLocationChecked()
memDomain.saveram()
data = uRange(0x01FF, 0x101)
data[0] = nil
return data
end
function setConsumableStacks()
memDomain.rom()
consumableStacks = {}
-- In order shards, tent, cabin, house, heal, pure, soft, ext1, ext2, ext3, ex4
consumableStacks[0x35] = 1
consumableStacks[0x36] = u8(0x47400) + 1
consumableStacks[0x37] = u8(0x47401) + 1
consumableStacks[0x38] = u8(0x47402) + 1
consumableStacks[0x39] = u8(0x47403) + 1
consumableStacks[0x3A] = u8(0x47404) + 1
consumableStacks[0x3B] = u8(0x47405) + 1
consumableStacks[0x3C] = u8(0x47406) + 1
consumableStacks[0x3D] = u8(0x47407) + 1
consumableStacks[0x3E] = u8(0x47408) + 1
consumableStacks[0x3F] = u8(0x47409) + 1
end
function getEmptyWeaponSlots()
memDomain.saveram()
ret = {}
count = 1
slot1 = uRange(0x118, 0x4)
slot2 = uRange(0x158, 0x4)
slot3 = uRange(0x198, 0x4)
slot4 = uRange(0x1D8, 0x4)
for i,v in pairs(slot1) do
if v == 0 then
ret[count] = 0x118 + i
count = count + 1
end
end
for i,v in pairs(slot2) do
if v == 0 then
ret[count] = 0x158 + i
count = count + 1
end
end
for i,v in pairs(slot3) do
if v == 0 then
ret[count] = 0x198 + i
count = count + 1
end
end
for i,v in pairs(slot4) do
if v == 0 then
ret[count] = 0x1D8 + i
count = count + 1
end
end
return ret
end
function getEmptyArmorSlots()
memDomain.saveram()
ret = {}
count = 1
slot1 = uRange(0x11C, 0x4)
slot2 = uRange(0x15C, 0x4)
slot3 = uRange(0x19C, 0x4)
slot4 = uRange(0x1DC, 0x4)
for i,v in pairs(slot1) do
if v == 0 then
ret[count] = 0x11C + i
count = count + 1
end
end
for i,v in pairs(slot2) do
if v == 0 then
ret[count] = 0x15C + i
count = count + 1
end
end
for i,v in pairs(slot3) do
if v == 0 then
ret[count] = 0x19C + i
count = count + 1
end
end
for i,v in pairs(slot4) do
if v == 0 then
ret[count] = 0x1DC + i
count = count + 1
end
end
return ret
end
function processBlock(block)
local msgBlock = block['messages']
if msgBlock ~= nil then
for i, v in pairs(msgBlock) do
if itemMessages[i] == nil then
local msg = {TTL=450, message=v, color=0xFFFF0000}
itemMessages[i] = msg
end
end
end
local itemsBlock = block["items"]
memDomain.saveram()
isInGame = u8(0x102)
if itemsBlock ~= nil and isInGame ~= 0x00 then
if consumableStacks == nil then
setConsumableStacks()
end
memDomain.saveram()
-- print('ITEMBLOCK: ')
-- print(itemsBlock)
itemIndex = u8(ITEM_INDEX)
-- print('ITEMINDEX: '..itemIndex)
for i, v in pairs(slice(itemsBlock, itemIndex, #itemsBlock)) do
-- Minus the offset and add to the correct domain
local memoryLocation = v
if v >= 0x100 and v <= 0x114 then
-- This is a key item
memoryLocation = memoryLocation - 0x0E0
wU8(memoryLocation, 0x01)
elseif v >= 0x1E0 then
-- This is a movement item
-- Minus Offset (0x100) - movement offset (0xE0)
memoryLocation = memoryLocation - 0x1E0
-- Canal is a flipped bit
if memoryLocation == 0x0C then
wU8(memoryLocation, 0x00)
else
wU8(memoryLocation, 0x01)
end
elseif v >= 0x16C and v <= 0x1AF then
-- This is a gold item
amountToAdd = goldLookup[v]
biggest = u8(0x01E)
medium = u8(0x01D)
smallest = u8(0x01C)
currentValue = 0x10000 * biggest + 0x100 * medium + smallest
newValue = currentValue + amountToAdd
newBiggest = math.floor(newValue / 0x10000)
newMedium = math.floor(math.fmod(newValue, 0x10000) / 0x100)
newSmallest = math.floor(math.fmod(newValue, 0x100))
wU8(0x01E, newBiggest)
wU8(0x01D, newMedium)
wU8(0x01C, newSmallest)
elseif v >= 0x115 and v <= 0x11B then
-- This is a regular consumable OR a shard
-- Minus Offset (0x100) + item offset (0x20)
memoryLocation = memoryLocation - 0x0E0
currentValue = u8(memoryLocation)
amountToAdd = consumableStacks[memoryLocation]
if currentValue < 99 then
wU8(memoryLocation, currentValue + amountToAdd)
end
elseif v >= 0x1B0 and v <= 0x1BB then
-- This is an extension consumable
memoryLocation = extensionConsumableLookup[v]
currentValue = u8(memoryLocation)
amountToAdd = consumableStacks[memoryLocation]
if currentValue < 99 then
value = currentValue + amountToAdd
if value > 99 then
value = 99
end
wU8(memoryLocation, value)
end
end
end
if #itemsBlock ~= itemIndex then
wU8(ITEM_INDEX, #itemsBlock)
end
memDomain.saveram()
weaponIndex = u8(WEAPON_INDEX)
emptyWeaponSlots = getEmptyWeaponSlots()
lastUsedWeaponIndex = weaponIndex
-- print('WEAPON_INDEX: '.. weaponIndex)
memDomain.saveram()
for i, v in pairs(slice(itemsBlock, weaponIndex, #itemsBlock)) do
if v >= 0x11C and v <= 0x143 then
-- Minus the offset and add to the correct domain
local itemValue = v - 0x11B
if #emptyWeaponSlots > 0 then
slot = table.remove(emptyWeaponSlots, 1)
wU8(slot, itemValue)
lastUsedWeaponIndex = weaponIndex + i
else
break
end
end
end
if lastUsedWeaponIndex ~= weaponIndex then
wU8(WEAPON_INDEX, lastUsedWeaponIndex)
end
memDomain.saveram()
armorIndex = u8(ARMOR_INDEX)
emptyArmorSlots = getEmptyArmorSlots()
lastUsedArmorIndex = armorIndex
-- print('ARMOR_INDEX: '.. armorIndex)
memDomain.saveram()
for i, v in pairs(slice(itemsBlock, armorIndex, #itemsBlock)) do
if v >= 0x144 and v <= 0x16B then
-- Minus the offset and add to the correct domain
local itemValue = v - 0x143
if #emptyArmorSlots > 0 then
slot = table.remove(emptyArmorSlots, 1)
wU8(slot, itemValue)
lastUsedArmorIndex = armorIndex + i
else
break
end
end
end
if lastUsedArmorIndex ~= armorIndex then
wU8(ARMOR_INDEX, lastUsedArmorIndex)
end
end
end
function difference(a, b)
local aa = {}
for k,v in pairs(a) do aa[v]=true end
for k,v in pairs(b) do aa[v]=nil end
local ret = {}
local n = 0
for k,v in pairs(a) do
if aa[v] then n=n+1 ret[n]=v end
end
return ret
end
function receive()
l, e = ff1Socket:receive()
if e == 'closed' then
if curstate == STATE_OK then
print("Connection closed")
end
curstate = STATE_UNINITIALIZED
return
elseif e == 'timeout' then
print("timeout")
return
elseif e ~= nil then
print(e)
curstate = STATE_UNINITIALIZED
return
end
processBlock(json.decode(l))
-- Determine Message to send back
memDomain.rom()
local playerName = uRange(0x7BCBF, 0x41)
playerName[0] = nil
local retTable = {}
retTable["playerName"] = playerName
if StateOKForMainLoop() then
retTable["locations"] = generateLocationChecked()
end
msg = json.encode(retTable).."\n"
local ret, error = ff1Socket:send(msg)
if ret == nil then
print(error)
elseif curstate == STATE_INITIAL_CONNECTION_MADE then
curstate = STATE_TENTATIVELY_CONNECTED
elseif curstate == STATE_TENTATIVELY_CONNECTED then
print("Connected!")
itemMessages["(0,0)"] = {TTL=240, message="Connected", color="green"}
curstate = STATE_OK
end
end
function main()
if (is23Or24Or25 or is26To27) == false then
print("Must use a version of bizhawk 2.3.1 or higher")
return
end
server, error = socket.bind('localhost', 52980)
while true do
gui.drawEllipse(248, 9, 6, 6, "Black", "Yellow")
frame = frame + 1
drawMessages()
if not (curstate == prevstate) then
-- console.log("Current state: "..curstate)
prevstate = curstate
end
if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then
if (frame % 60 == 0) then
gui.drawEllipse(248, 9, 6, 6, "Black", "Blue")
receive()
else
gui.drawEllipse(248, 9, 6, 6, "Black", "Green")
end
elseif (curstate == STATE_UNINITIALIZED) then
gui.drawEllipse(248, 9, 6, 6, "Black", "White")
if (frame % 60 == 0) then
gui.drawEllipse(248, 9, 6, 6, "Black", "Yellow")
drawText(5, 8, "Waiting for client", 0xFFFF0000)
drawText(5, 32, "Please start FF1Client.exe", 0xFFFF0000)
-- Advance so the messages are drawn
emu.frameadvance()
server:settimeout(2)
print("Attempting to connect")
local client, timeout = server:accept()
if timeout == nil then
-- print('Initial Connection Made')
curstate = STATE_INITIAL_CONNECTION_MADE
ff1Socket = client
ff1Socket:settimeout(0)
end
end
end
emu.frameadvance()
end
end
main()

380
data/lua/FF1/json.lua Normal file
View File

@@ -0,0 +1,380 @@
--
-- json.lua
--
-- Copyright (c) 2015 rxi
--
-- This library is free software; you can redistribute it and/or modify it
-- under the terms of the MIT license. See LICENSE for details.
--
local json = { _version = "0.1.0" }
-------------------------------------------------------------------------------
-- Encode
-------------------------------------------------------------------------------
local encode
local escape_char_map = {
[ "\\" ] = "\\\\",
[ "\"" ] = "\\\"",
[ "\b" ] = "\\b",
[ "\f" ] = "\\f",
[ "\n" ] = "\\n",
[ "\r" ] = "\\r",
[ "\t" ] = "\\t",
}
local escape_char_map_inv = { [ "\\/" ] = "/" }
for k, v in pairs(escape_char_map) do
escape_char_map_inv[v] = k
end
local function escape_char(c)
return escape_char_map[c] or string.format("\\u%04x", c:byte())
end
local function encode_nil(val)
return "null"
end
local function encode_table(val, stack)
local res = {}
stack = stack or {}
-- Circular reference?
if stack[val] then error("circular reference") end
stack[val] = true
if val[1] ~= nil or next(val) == nil then
-- Treat as array -- check keys are valid and it is not sparse
local n = 0
for k in pairs(val) do
if type(k) ~= "number" then
error("invalid table: mixed or invalid key types")
end
n = n + 1
end
if n ~= #val then
error("invalid table: sparse array")
end
-- Encode
for i, v in ipairs(val) do
table.insert(res, encode(v, stack))
end
stack[val] = nil
return "[" .. table.concat(res, ",") .. "]"
else
-- Treat as an object
for k, v in pairs(val) do
if type(k) ~= "string" then
error("invalid table: mixed or invalid key types")
end
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
end
stack[val] = nil
return "{" .. table.concat(res, ",") .. "}"
end
end
local function encode_string(val)
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
end
local function encode_number(val)
-- Check for NaN, -inf and inf
if val ~= val or val <= -math.huge or val >= math.huge then
error("unexpected number value '" .. tostring(val) .. "'")
end
return string.format("%.14g", val)
end
local type_func_map = {
[ "nil" ] = encode_nil,
[ "table" ] = encode_table,
[ "string" ] = encode_string,
[ "number" ] = encode_number,
[ "boolean" ] = tostring,
}
encode = function(val, stack)
local t = type(val)
local f = type_func_map[t]
if f then
return f(val, stack)
end
error("unexpected type '" .. t .. "'")
end
function json.encode(val)
return ( encode(val) )
end
-------------------------------------------------------------------------------
-- Decode
-------------------------------------------------------------------------------
local parse
local function create_set(...)
local res = {}
for i = 1, select("#", ...) do
res[ select(i, ...) ] = true
end
return res
end
local space_chars = create_set(" ", "\t", "\r", "\n")
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
local literals = create_set("true", "false", "null")
local literal_map = {
[ "true" ] = true,
[ "false" ] = false,
[ "null" ] = nil,
}
local function next_char(str, idx, set, negate)
for i = idx, #str do
if set[str:sub(i, i)] ~= negate then
return i
end
end
return #str + 1
end
local function decode_error(str, idx, msg)
--local line_count = 1
--local col_count = 1
--for i = 1, idx - 1 do
-- col_count = col_count + 1
-- if str:sub(i, i) == "\n" then
-- line_count = line_count + 1
-- col_count = 1
-- end
-- end
-- emu.message( string.format("%s at line %d col %d", msg, line_count, col_count) )
end
local function codepoint_to_utf8(n)
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
local f = math.floor
if n <= 0x7f then
return string.char(n)
elseif n <= 0x7ff then
return string.char(f(n / 64) + 192, n % 64 + 128)
elseif n <= 0xffff then
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
elseif n <= 0x10ffff then
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
f(n % 4096 / 64) + 128, n % 64 + 128)
end
error( string.format("invalid unicode codepoint '%x'", n) )
end
local function parse_unicode_escape(s)
local n1 = tonumber( s:sub(3, 6), 16 )
local n2 = tonumber( s:sub(9, 12), 16 )
-- Surrogate pair?
if n2 then
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
else
return codepoint_to_utf8(n1)
end
end
local function parse_string(str, i)
local has_unicode_escape = false
local has_surrogate_escape = false
local has_escape = false
local last
for j = i + 1, #str do
local x = str:byte(j)
if x < 32 then
decode_error(str, j, "control character in string")
end
if last == 92 then -- "\\" (escape char)
if x == 117 then -- "u" (unicode escape sequence)
local hex = str:sub(j + 1, j + 5)
if not hex:find("%x%x%x%x") then
decode_error(str, j, "invalid unicode escape in string")
end
if hex:find("^[dD][89aAbB]") then
has_surrogate_escape = true
else
has_unicode_escape = true
end
else
local c = string.char(x)
if not escape_chars[c] then
decode_error(str, j, "invalid escape char '" .. c .. "' in string")
end
has_escape = true
end
last = nil
elseif x == 34 then -- '"' (end of string)
local s = str:sub(i + 1, j - 1)
if has_surrogate_escape then
s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape)
end
if has_unicode_escape then
s = s:gsub("\\u....", parse_unicode_escape)
end
if has_escape then
s = s:gsub("\\.", escape_char_map_inv)
end
return s, j + 1
else
last = x
end
end
decode_error(str, i, "expected closing quote for string")
end
local function parse_number(str, i)
local x = next_char(str, i, delim_chars)
local s = str:sub(i, x - 1)
local n = tonumber(s)
if not n then
decode_error(str, i, "invalid number '" .. s .. "'")
end
return n, x
end
local function parse_literal(str, i)
local x = next_char(str, i, delim_chars)
local word = str:sub(i, x - 1)
if not literals[word] then
decode_error(str, i, "invalid literal '" .. word .. "'")
end
return literal_map[word], x
end
local function parse_array(str, i)
local res = {}
local n = 1
i = i + 1
while 1 do
local x
i = next_char(str, i, space_chars, true)
-- Empty / end of array?
if str:sub(i, i) == "]" then
i = i + 1
break
end
-- Read token
x, i = parse(str, i)
res[n] = x
n = n + 1
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "]" then break end
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
end
return res, i
end
local function parse_object(str, i)
local res = {}
i = i + 1
while 1 do
local key, val
i = next_char(str, i, space_chars, true)
-- Empty / end of object?
if str:sub(i, i) == "}" then
i = i + 1
break
end
-- Read key
if str:sub(i, i) ~= '"' then
decode_error(str, i, "expected string for key")
end
key, i = parse(str, i)
-- Read ':' delimiter
i = next_char(str, i, space_chars, true)
if str:sub(i, i) ~= ":" then
decode_error(str, i, "expected ':' after key")
end
i = next_char(str, i + 1, space_chars, true)
-- Read value
val, i = parse(str, i)
-- Set
res[key] = val
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "}" then break end
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
end
return res, i
end
local char_func_map = {
[ '"' ] = parse_string,
[ "0" ] = parse_number,
[ "1" ] = parse_number,
[ "2" ] = parse_number,
[ "3" ] = parse_number,
[ "4" ] = parse_number,
[ "5" ] = parse_number,
[ "6" ] = parse_number,
[ "7" ] = parse_number,
[ "8" ] = parse_number,
[ "9" ] = parse_number,
[ "-" ] = parse_number,
[ "t" ] = parse_literal,
[ "f" ] = parse_literal,
[ "n" ] = parse_literal,
[ "[" ] = parse_array,
[ "{" ] = parse_object,
}
parse = function(str, idx)
local chr = str:sub(idx, idx)
local f = char_func_map[chr]
if f then
return f(str, idx)
end
decode_error(str, idx, "unexpected character '" .. chr .. "'")
end
function json.decode(str)
if type(str) ~= "string" then
error("expected argument of type string, got " .. type(str))
end
return ( parse(str, next_char(str, 1, space_chars, true)) )
end
return json

132
data/lua/FF1/socket.lua Normal file
View File

@@ -0,0 +1,132 @@
-----------------------------------------------------------------------------
-- LuaSocket helper module
-- Author: Diego Nehab
-- RCS ID: $Id: socket.lua,v 1.22 2005/11/22 08:33:29 diego Exp $
-----------------------------------------------------------------------------
-----------------------------------------------------------------------------
-- Declare module and import dependencies
-----------------------------------------------------------------------------
local base = _G
local string = require("string")
local math = require("math")
local socket = require("socket.core")
module("socket")
-----------------------------------------------------------------------------
-- Exported auxiliar functions
-----------------------------------------------------------------------------
function connect(address, port, laddress, lport)
local sock, err = socket.tcp()
if not sock then return nil, err end
if laddress then
local res, err = sock:bind(laddress, lport, -1)
if not res then return nil, err end
end
local res, err = sock:connect(address, port)
if not res then return nil, err end
return sock
end
function bind(host, port, backlog)
local sock, err = socket.tcp()
if not sock then return nil, err end
sock:setoption("reuseaddr", true)
local res, err = sock:bind(host, port)
if not res then return nil, err end
res, err = sock:listen(backlog)
if not res then return nil, err end
return sock
end
try = newtry()
function choose(table)
return function(name, opt1, opt2)
if base.type(name) ~= "string" then
name, opt1, opt2 = "default", name, opt1
end
local f = table[name or "nil"]
if not f then base.error("unknown key (".. base.tostring(name) ..")", 3)
else return f(opt1, opt2) end
end
end
-----------------------------------------------------------------------------
-- Socket sources and sinks, conforming to LTN12
-----------------------------------------------------------------------------
-- create namespaces inside LuaSocket namespace
sourcet = {}
sinkt = {}
BLOCKSIZE = 2048
sinkt["close-when-done"] = function(sock)
return base.setmetatable({
getfd = function() return sock:getfd() end,
dirty = function() return sock:dirty() end
}, {
__call = function(self, chunk, err)
if not chunk then
sock:close()
return 1
else return sock:send(chunk) end
end
})
end
sinkt["keep-open"] = function(sock)
return base.setmetatable({
getfd = function() return sock:getfd() end,
dirty = function() return sock:dirty() end
}, {
__call = function(self, chunk, err)
if chunk then return sock:send(chunk)
else return 1 end
end
})
end
sinkt["default"] = sinkt["keep-open"]
sink = choose(sinkt)
sourcet["by-length"] = function(sock, length)
return base.setmetatable({
getfd = function() return sock:getfd() end,
dirty = function() return sock:dirty() end
}, {
__call = function()
if length <= 0 then return nil end
local size = math.min(socket.BLOCKSIZE, length)
local chunk, err = sock:receive(size)
if err then return nil, err end
length = length - string.len(chunk)
return chunk
end
})
end
sourcet["until-closed"] = function(sock)
local done
return base.setmetatable({
getfd = function() return sock:getfd() end,
dirty = function() return sock:dirty() end
}, {
__call = function()
if done then return nil end
local chunk, err, partial = sock:receive(socket.BLOCKSIZE)
if not err then return chunk
elseif err == "closed" then
sock:close()
done = 1
return partial
else return nil, err end
end
})
end
sourcet["default"] = sourcet["until-closed"]
source = choose(sourcet)

645
docs/api.md Normal file
View File

@@ -0,0 +1,645 @@
# Archipelago API
This document tries to explain some internals required to implement a game for
Archipelago's generation and server. Once a seed is generated, a client or mod is
required to send and receive items between the game and server.
Client implementation is out of scope of this document. Please refer to an
existing game that provides a similar API to yours.
Refer to the following documents as well:
* [network protocol.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/network%20protocol.md)
* [adding games.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/adding%20games.md)
Archipelago will be abbreviated as "AP" from now on.
## Language
AP worlds are written in python3.
Clients that connect to the server to sync items can be in any language that
allows using WebSockets.
## Coding style
AP follows all the PEPs. When in doubt use an IDE with coding style
linter, for example PyCharm Community Edition.
## Docstrings
Docstrings are strings attached to an object in Python that describe what the
object is supposed to be. Certain docstrings will be picked up and used by AP.
They are assigned by writing a string without any assignment right below a
definition. The string must be a triple-quoted string.
Example:
```python
from worlds.AutoWorld import World
class MyGameWorld(World):
"""This is the description of My Game that will be displayed on the AP
website."""
```
## Definitions
### World Class
A `World` class is the class with all the specifics of a certain game to be
included. It will be instantiated for each player that rolls a seed for that
game.
### MultiWorld Object
The `MultiWorld` object references the whole multiworld (all items and locations
for all players) and is accessible through `self.world` inside a `World` object.
### Player
The player is just an integer in AP and is accessible through `self.player`
inside a World object.
### Player Options
Players provide customized settings for their World in the form of yamls.
Those are accessible through `self.world.<option_name>[self.player]`. A dict
of valid options has to be provided in `self.options`. Options are automatically
added to the `World` object for easy access.
### World Options
Any AP installation can provide settings for a world, for example a ROM file,
accessible through `Utils.get_options()['<world>_options']['<option>']`.
Users can set those in their `host.yaml` file.
### Locations
Locations are places where items can be located in your game. This may be chests
or boss drops for RPG-like games but could also be progress in a research tree.
Each location has a `name` and an `id` (a.k.a. "code" or "address"), is placed
in a Region and has access rules.
The name needs to be unique in each game, the ID needs to be unique across all
games and is best in the same range as the item IDs.
Special locations with ID `None` can hold events.
### Items
Items are all things that can "drop" for your game. This may be RPG items like
weapons, could as well be technologies you normally research in a research tree.
Each item has a `name`, an `id` (can be known as "code"), and an `advancement`
flag. An advancement item is an item which a player may require to advance in
their world. Advancement items will be assigned to locations with higher
priority and moved around to meet defined rules and accomplish progression
balancing.
Special items with ID `None` can mark events (read below).
### Events
Events will mark some progress. You define an event location, an
event item, strap some rules to the location (i.e. hold certain
items) and manually place the event item at the event location.
Events can be used to either simplify the logic or to get better spoiler logs.
Events will show up in the spoiler playthrough but they do not represent actual
items or locations within the game.
There is one special case for events: Victory. To get the win condition to show
up in the spoiler log, you create an event item and place it at an event
location with the `access_rules` for game completion. Once that's done, the
world's win condition can be as simple as checking for that item.
By convention the victory event is called `"Victory"`. It can be placed at one
or more event locations based on player options.
### Regions
Regions are logical groups of locations that share some common access rules. If
location logic is written from scratch, using regions greatly simplifies the
definition and allow to somewhat easily implement things like entrance
randomizer in logic.
Regions have a list called `exits` which are `Entrance` objects representing
transitions to other regions.
There has to be one special region "Menu" from which the logic unfolds. AP
assumes that a player will always be able to return to the "Menu" region by
resetting the game ("Save and quit").
### Entrances
An `Entrance` connects to a region, is assigned to region's exits and has rules
to define if it and thus the connected region is accessible.
They can be static (regular logic) or be defined/connected during generation
(entrance randomizer).
### Access Rules
An access rule is a function that returns `True` or `False` for a `Location` or
`Entrance` based on the the current `state` (items that can be collected).
### Item Rules
An item rule is a function that returns `True` or `False` for a `Location` based
on a single item. It can be used to reject placement of an item there.
## Implementation
### Your World
All code for your world implementation should be placed in a python package in
the `/worlds` directory. The starting point for the package is `__init.py__`.
Conventionally, your world class is placed in that file.
World classes must inherit from the `World` class in `/worlds/AutoWorld.py`,
which can be imported as `..AutoWorld.World` from your package.
AP will pick up your world automatically due to the `AutoWorld` implementation.
### Requirements
If your world needs specific python packages, they can be listed in
`world/[world_name]/requirements.txt`.
See [pip documentation](https://pip.pypa.io/en/stable/cli/pip_install/#requirements-file-format)
### Relative Imports
AP will only import the `__init__.py`. Depending on code size it makes sense to
use multiple files and use relative imports to access them.
e.g. `from .Options import mygame_options` from your `__init__.py` will load
`world/[world_name]/Options.py` and make its `mygame_options` accesible.
When imported names pile up it may be easier to use `from . import Options`
and access the variable as `Options.mygame_options`.
### Your Item Type
Each world uses its own subclass of `BaseClasses.Item`. The constuctor can be
overridden to attach additional data to it, e.g. "price in shop".
Since the constructor is only ever called from your code, you can add whatever
arguments you like to the constructor.
In its simplest form we only set the game name and use the default constuctor
```python
from BaseClasses import Item
class MyGameItem(Item):
game: str = "My Game"
```
By convention this class definition will either be placed in your `__init__.py`
or your `Items.py`. For a more elaborate example see `worlds/oot/Items.py`.
### Your location type
The same we have done for items above, we will do for locations
```python
from BaseClasses import Location
class MyGameLocation(Location):
game: str = "My Game"
# override constructor to automatically mark event locations as such
def __init__(self, player: int, name = '', code = None, parent = None):
super(MyGameLocation, self).__init__(player, name, code, parent)
self.event = code is None
```
in your `__init__.py` or your `Locations.py`.
### Options
By convention options are defined in `Options.py` and will be used when parsing
the players' yaml files.
Each option has its own class, inherits from a base option type, has a docstring
to describe it and a `displayname` property for display on the website and in
spoiler logs.
The actual name as used in the yaml is defined in a `dict[str, Option]`, that is
assigned to the world under `self.options`.
Common option types are `Toggle`, `DefaultOnToggle`, `Choice`, `Range`.
For more see `Options.py` in AP's base directory.
#### Toggle, DefaultOnToggle
Those don't need any additional properties defined. After parsing the option,
its `value` will either be True or False.
#### Range
Define properties `range_start`, `range_end` and `default`. Ranges will be
displayed as sliders on the website and can be set to random in the yaml.
#### Choice
Choices are like toggles, but have more options than just True and False.
Define a property `option_<name> = <number>` per selectable value and
`default = <number>` to set the default selection. Aliases can be set by
defining a property `alias_<name> = <same number>`.
One special case where aliases are required is when option name is `yes`, `no`,
`on` or `off` because they parse to `True` or `False`:
```python
option_off = 0
option_on = 1
option_some = 2
alias_false = 0
alias_true = 1
default = 0
```
#### Sample
```python
# Options.py
from Options import Toggle, Range, Choice, Option
import typing
class Difficulty(Choice):
"""Sets overall game difficulty."""
displayname = "Difficulty"
option_easy = 0
option_normal = 1
option_hard = 2
alias_beginner = 0 # same as easy
alias_expert = 2 # same as hard
default = 1 # default to normal
class FinalBossHP(Range):
"""Sets the HP of the final boss"""
displayname = "Final Boss HP"
range_start = 100
range_end = 10000
default = 2000
class FixXYZGlitch(Toggle):
"""Fixes ABC when you do XYZ"""
displayname = "Fix XYZ Glitch"
# By convention we call the options dict variable `<world>_options`.
mygame_options: typing.Dict[str, type(Option)] = {
"difficulty": Difficulty,
"final_boss_hp": FinalBossHP,
"fix_xyz_glitch": FixXYZGlitch
}
```
```python
# __init__.py
from ..AutoWorld import World
from .Options import mygame_options # import the options dict
class MyGameWorld(World):
#...
options = mygame_options # assign the options dict to the world
#...
```
### Local or Remote
A world with `remote_items` set to `True` gets all items items from the server
and no item from the local game. So for an RPG opening a chest would not add
any item to your inventory, instead the server will send you what was in that
chest. The advantage is that a generic mod can be used that does not need to
know anything about the seed.
A world with `remote_items` set to `False` will locally reward its local items.
For console games this can remove delay and make script/animation/dialog flow
more natural. These games typically have been edited to 'bake in' the items.
### A World Class Skeleton
```python
# world/mygame/__init__.py
from .Options import mygame_options # the options we defined earlier
from .Items import mygame_items # data used below to add items to the World
from .Locations import mygame_locations # same as above
from ..AutoWorld import World
from BaseClasses import Region, Location, Entrance, Item
from Utils import get_options, output_path
class MyGameItem(Item): # or from Items import MyGameItem
game = "My Game" # name of the game/world this item is from
class MyGameLocation(Location): # or from Locations import MyGameLocation
game = "My Game" # name of the game/world this location is in
class MyGameWorld(World):
"""Insert description of the world/game here."""
game: str = "My Game" # name of the game/world
options = mygame_options # options the player can set
topology_present: bool = True # show path to required location checks in spoiler
remote_items: bool = False # True if all items come from the server
remote_start_inventory: bool = False # True if start inventory comes from the server
# data_version is used to signal that items, locations or their names
# changed. Set this to 0 during development so other games' clients do not
# cache any texts, then increase by 1 for each release that makes changes.
data_version = 0
# ID of first item and location, could be hard-coded but code may be easier
# to read with this as a propery.
base_id = 1234
# Instead of dynamic numbering, IDs could be part of data.
# The following two dicts are required for the generation to know which
# items exist. They could be generated from json or something else. They can
# include events, but don't have to since events will be placed manually.
item_name_to_id = {name: id for
id, name in enumerate(mygame_items, base_id)}
location_name_to_id = {name: id for
id, name in enumerate(mygame_locations, base_id)}
# Items can be grouped using their names to allow easy checking if any item
# from that group has been collected. Group names can also be used for !hint
item_name_groups = {
"weapons": {"sword", "lance"}
}
```
### Generation
The world has to provide the following things for generation
* the properties mentioned above
* additions to the item pool
* additions to the regions list: at least one called "Menu"
* locations placed inside those regions
* a `def create_item(self, item: str) -> MyGameItem` for plando/manual placing
* applying `self.world.precollected_items` for plando/start inventory
if not using a `remote_start_inventory`
* a `def generate_output(self, output_directory: str)` that creates the output
if there is output to be generated. If only items are randomized and
`remote_items = True` it is possible to have a generic mod and output
generation can be skipped. In all other cases this is required. When this is
called, `self.world.get_locations()` has all locations for all players, with
properties `item` pointing to the item and `player` identifying the player.
`self.world.get_filled_locations(self.player)` will filter for this world.
`item.player` can be used to see if it's a local item.
In addition the following methods can be implemented
* `def generate_early(self)`
called per player before any items or locations are created. You can set
properties on your world here. Already has access to player options and RNG.
* `def create_regions(self)`
called to place player's regions into the MultiWorld's regions list. If it's
hard to separate, this can be done during `generate_early` or `basic` as well.
* `def create_items(self)`
called to place player's items into the MultiWorld's itempool.
* `def set_rules(self)`
called to set access and item rules on locations and entrances.
* `def generate_basic(self)`
called after the previous steps. Some placement and player specific
randomizations can be done here. After this step all regions and items have
to be in the MultiWorld's regions and itempool.
* `pre_fill`, `fill_hook` and `post_fill` are called to modify item placement
before, during and after the regular fill process, before `generate_output`.
* `fill_slot_data` and `modify_multidata` can be used to modify the data that
will be used by the server to host the MultiWorld.
* `def get_required_client_version(self)`
can return a tuple of 3 ints to make sure the client is compatible to this
world (e.g. item IDs) when connecting.
#### generate_early
```python
def generate_early(self):
# read player settings to world instance
self.final_boss_hp = self.world.final_boss_hp[self.player].value
```
#### create_item
```python
# we need a way to know if an item provides progress in the game ("key item")
# this can be part of the items definition, or depend on recipe randomization
from .Items import is_progression # this is just a dummy
def create_item(self, item: str):
# This is called when AP wants to create an item by name (for plando) or
# when you call it from your own code.
return MyGameItem(item, is_progression(item), self.item_name_to_id[item],
self.player)
def create_event(self, event: str):
# while we are at it, we can also add a helper to create events
return MyGameItem(event, True, None, self.player)
```
#### create_items
```python
def create_items(self):
# Add items to the Multiworld.
# If there are two of the same item, the item has to be twice in the pool.
# Which items are added to the pool may depend on player settings,
# e.g. custom win condition like triforce hunt.
# Having an item in the start inventory won't remove it from the pool.
# If an item can't have duplicates it has to be excluded manually.
# List of items to exclude, as a copy since it will be destroyed below
exclude = [item for item in self.world.precollected_items[self.player]]
for item in map(self.create_item, mygame_items):
if item in exclude:
exclude.remove(item) # this is destructive. create unique list above
self.world.itempool.append(self.create_item('nothing'))
else:
self.world.itempool.append(item)
# itempool and number of locations should match up.
# If this is not the case we want to fill the itempool with junk.
junk = 0 # calculate this based on player settings
self.world.itempool += [self.create_item('nothing') for _ in range(junk)]
```
#### create_regions
```python
def create_regions(self):
# Add regions to the multiworld. "Menu" is the required starting point.
# Arguments to Region() are name, type, human_readable_name, player, world
r = Region("Menu", None, "Menu", self.player, self.world)
# Set Region.exits to a list of entrances that are reachable from region
r.exits = [Entrance(self.player, "New game", r)] # or use r.exits.append
# Append region to MultiWorld's regions
self.world.regions.append(r) # or use += [r...]
r = Region("Main Area", None, "Main Area", self.player, self.world)
# Add main area's locations to main area (all but final boss)
r.locations = [MyGameLocation(self.player, location.name,
self.location_name_to_id[location.name], r)]
r.exits = [Entrance(self.player, "Boss Door", r)]
self.world.regions.append(r)
r = Region("Boss Room", None, "Boss Room", self.player, self.world)
# add event to Boss Room
r.locations = [MyGameLocation(self.player, "Final Boss", None, r)]
self.world.regions.append(r)
# If entrances are not randomized, they should be connected here, otherwise
# they can also be connected at a later stage.
self.world.get_entrance("New Game", self.player)\
.connect(self.world.get_region("Main Area", self.player))
self.world.get_entrance("Boss Door", self.player)\
.connect(self.world.get_region("Boss Room", self.player))
# If setting location access rules from data is easier here, set_rules can
# possibly omitted.
```
#### generate_basic
```python
def generate_basic(self):
# place "Victory" at "Final Boss" and set collection as win condition
self.world.get_location("Final Boss", self.player)\
.place_locked_item(self.create_event("Victory"))
self.world.completion_condition[self.player] = \
lambda state: state.has("Victory", self.player)
# place item Herb into location Chest1 for some reason
item = self.create_item("Herb")
self.world.get_location("Chest1", self.player).place_locked_item(item)
# in most cases it's better to do this at the same time the itempool is
# filled to avoid accidental duplicates:
# manually placed and still in the itempool
```
### Setting Rules
```python
from ..generic.Rules import add_rule, set_rule, forbid_item
from Items import get_item_type
def set_rules(self):
# For some worlds this step can be omitted if either a Logic mixin
# (see below) is used, it's easier to apply the rules from data during
# location generation or everything is in generate_basic
# set a simple rule for an region
set_rule(self.world.get_entrance("Boss Door", self.player),
lambda state: state.has("Boss Key", self.player))
# combine rules to require two items
add_rule(self.world.get_location("Chest2", self.player),
lambda state: state.has("Sword", self.player))
add_rule(self.world.get_location("Chest2", self.player),
lambda state: state.has("Shield", self.player))
# or simply combine yourself
set_rule(self.world.get_location("Chest2", self.player),
lambda state: state.has("Sword", self.player) and
state.has("Shield", self.player))
# require two of an item
set_rule(self.world.get_location("Chest3", self.player),
lambda state: state.has("Key", self.player, 2))
# require one item from an item group
add_rule(self.world.get_location("Chest3", self.player),
lambda state: state.has_group("weapons", self.player))
# state also has .item_count() for items, .has_any() and.has_all() for sets
# and .count_group() for groups
# set_rule is likely to be a bit faster than add_rule
# disallow placing a specific local item at a specific location
forbid_item(self.world.get_location("Chest4", self.player), "Sword")
# disallow placing items with a specific property
add_item_rule(self.world.get_location("Chest5", self.player),
lambda item: get_item_type(item) == "weapon")
# get_item_type needs to take player/world into account
# if MyGameItem has a type property, a more direct implementation would be
add_item_rule(self.world.get_location("Chest5", self.player),
lambda item: item.player != self.player or\
item.my_type == "weapon")
# location.item_rule = ... is likely to be a bit faster
```
### Logic Mixin
While lambdas and events could do pretty much anything, by convention we
implement more complex logic in logic mixins, even if there is no need to add
properties to the `BaseClasses.CollectionState` state object.
When importing a file that defines a class that inherits from
`..AutoWorld.LogicMixin` the state object's class is automatically extended by
the mixin's members. These members should be prefixed with underscore following
the name of the implementing world. This is due to sharing a namespace with all
other logic mixins.
Typical uses are defining methods that are used instead of `state.has`
in lambdas, e.g.`state._mygame_has(custom, world, player)` or recurring checks
like `state._mygame_can_do_something(world, player)` to simplify lambdas.
More advanced uses could be to add additional variables to the state object,
override `World.collect(self, state, item)` and `remove(self, state, item)`
to update the state object, and check those added variables in added methods.
Please do this with caution and only when neccessary.
#### Sample
```python
# Logic.py
from ..AutoWorld import LogicMixin
class MyGameLogic(LogicMixin):
def _mygame_has_key(self, world: MultiWorld, player: int):
# Arguments above are free to choose
# it may make sense to use World as argument instead of MultiWorld
return self.has('key', player) # or whatever
```
```python
# __init__.py
from ..generic.Rules import set_rule
import .Logic # apply the mixin by importing its file
class MyGameWorld(World):
# ...
def set_rules(self):
set_rule(self.world.get_location("A Door", self.player),
lamda state: state._myworld_has_key(self.world, self.player))
```
### Generate Output
```python
from .Mod import generate_mod
def generate_output(self, output_directory: str):
# How to generate the mod or ROM highly depends on the game
# if the mod is written in Lua, Jinja can be used to fill a template
# if the mod reads a json file, `json.dump()` can be used to generate that
# code below is a dummy
data = {
"seed": self.world.seed_name, # to verify the server's multiworld
"slot": self.world.player_name[self.player], # to connect to server
"items": {location.name: location.item.name
if location.item.player == self.player else "Remote"
for location in self.world.get_filled_locations(self.player)},
# store start_inventory from player's .yaml
"starter_items": [item.name for item
in self.world.precollected_items[self.player]],
"final_boss_hp": self.final_boss_hp,
# store option name "easy", "normal" or "hard" for difficuly
"difficulty": self.world.difficulty[self.player].current_key,
# store option value True or False for fixing a glitch
"fix_xyz_glitch": self.world.fix_xyz_glitch[self.player].value
}
# point to a ROM specified by the installation
src = Utils.get_options()["mygame_options"]["rom_file"]
# or point to worlds/mygame/data/mod_template
src = os.path.join(os.path.dirname(__file__), "data", "mod_template")
# generate output path
mod_name = f"AP-{self.world.seed_name}-P{self.player}-{self.world.player_name[self.player]}"
out_file = os.path.join(output_directory, mod_name + ".zip")
# generate the file
generate_mod(src, out_file, data)
```

View File

@@ -44,6 +44,7 @@ These packets are are sent from the multiworld server to the client. They are no
* [PrintJSON](#PrintJSON)
* [DataPackage](#DataPackage)
* [Bounced](#Bounced)
* [InvalidPacket](#InvalidPacket)
### RoomInfo
Sent to clients when they connect to an Archipelago server.
@@ -53,15 +54,17 @@ 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.|
| permissions | dict\[str, Permission\[int\]\] | Mapping of permission name to [Permission](#Permission), keys are: "forfeit" and "remaining". |
| permissions | dict\[str, Permission\[int\]\] | Mapping of permission name to [Permission](#Permission), keys are: "forfeit", "collect" 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. |
| datapackage_version | int | Data version of the [data package](#Data Package Contents) the server will send. Used to update the client's (optional) local cache. |
| datapackage_versions | dict[str, int] | Data versions of the individual games' data packages the server will send. |
| games | list\[str\] | sorted list of game names for the players, so first player's game will be games\[0\]. Matches game names in datapackage. |
| datapackage_version | int | Data version of the [data package](#Data-Package-Contents) the server will send. Used to update the client's (optional) local cache. |
| datapackage_versions | dict\[str, int\] | Data versions of the individual games' data packages the server will send. |
| seed_name | str | uniquely identifying name of this generation |
| time | float | Unix time stamp of "now". Send for time synchronization if wanted for things like the DeathLink Bounce. |
#### forfeit_mode
#### forfeit
Dictates what is allowed when it comes to a player forfeiting their run. A forfeit is an action which distributes the rest of the items in a player's run to those other players awaiting them.
* `auto`: Distributes a player's items to other players when they complete their goal.
@@ -70,7 +73,17 @@ Dictates what is allowed when it comes to a player forfeiting their run. A forfe
* `disabled`: All forfeit modes disabled.
* `goal`: Allows for manual use of forfeit command once a player completes their goal. (Disabled until goal completion)
#### remaining_mode
#### collect
Dictates what is allowed when it comes to a player collecting their run. A collect is an action which sends the rest of the items in a player's run.
* `auto`: Automatically when they complete their goal.
* `enabled`: Denotes that players may !collect at any time in the game.
* `auto-enabled`: Both of the above options together.
* `disabled`: All collect modes disabled.
* `goal`: Allows for manual use of collect command once a player completes their goal. (Disabled until goal completion)
#### remaining
Dictates what is allowed when it comes to a player querying the items remaining in their run.
* `goal`: Allows a player to query for items remaining in their run but only after they completed their own goal.
@@ -119,15 +132,18 @@ Sent to clients to acknowledge a received [LocationScouts](#LocationScouts) pack
### RoomUpdate
Sent when there is a need to update information about the present game session. Generally useful for async games.
Once authenticated (received Connected), this may also contain data from Connected.
#### Arguments
The arguments for RoomUpdate are identical to [RoomInfo](#RoomInfo) barring two:
The arguments for RoomUpdate are identical to [RoomInfo](#RoomInfo) barring:
| Name | Type | Notes |
| ---- | ---- | ----- |
| hint_points | int | New argument. The client's current hint points. |
| players | list\[NetworkPlayer\] | Changed argument. Always sends all players, whether connected or not. |
| checked_locations | May be a partial update, containing new locations that were checked, especially from a coop partner in the same slot. |
| missing_locations | Should never be sent as an update, if needed is the inverse of checked_locations. |
All arguments for this packet are optional.
All arguments for this packet are optional, only changes are sent.
### Print
Sent to clients purely to display a message to the player.
@@ -142,9 +158,10 @@ Sent to clients purely to display a message to the player. This packet differs f
| Name | Type | Notes |
| ---- | ---- | ----- |
| data | list\[JSONMessagePart\] | See [JSONMessagePart](#JSONMessagePart) for more details on this type. |
| type | str | May be present to indicate the nature of this message. Known types are Hint and ItemSend.
| receiving | int | Is present if type is Hint or ItemSend and marks the destination player's ID.
| item | NetworkItem | Is present if type is Hint or ItemSend and marks the source player id, location id and item id.
| type | str | May be present to indicate the nature of this message. Known types are Hint and ItemSend. |
| receiving | int | Is present if type is Hint or ItemSend and marks the destination player's ID. |
| item | NetworkItem | Is present if type is Hint or ItemSend and marks the source player id, location id and item id. |
| found | bool | Is present if type is Hint, denotes whether the location hinted for was checked. |
### DataPackage
Sent to clients to provide what is known as a 'data package' which contains information to enable a client to most easily communicate with the Archipelago server. Contents include things like location id to name mappings, among others; see [Data Package Contents](#Data-Package-Contents) for more info.
@@ -162,7 +179,14 @@ Sent to clients after a client requested this message be sent to them, more info
| ---- | ---- | ----- |
| data | dict | The data in the Bounce package copied |
### InvalidPacket
Sent to clients if the server caught a problem with a packet. This only occurs for errors that are explicitly checked for.
| Name | Type | Notes |
| ---- | ---- | ----- |
| type | string | "cmd" if the Packet isn't available/allowed, "arguments" if the problem is with the package data. |
| text | string | Error text explaining the caught error. |
| original_cmd | string | Echoes the cmd it failed on. May be null if the cmd was not found.
## (Client -> Server)
These packets are sent purely from client to server. They are not accepted by clients.
@@ -186,11 +210,19 @@ Sent by the client to initiate a connection to an Archipelago game session.
| name | str | The player name for this client. |
| uuid | str | Unique identifier for player client. |
| version | NetworkVersion | An object representing the Archipelago version this client supports. |
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. |
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. [Tags](#Tags) |
#### Authentication
Many, if not all, other packets require a successfully authenticated client. This is described in more detail in [Archipelago Connection Handshake](#Archipelago-Connection-Handshake).
### ConnectUpdate
Update arguments from the Connect package, currently only updating tags is supported.
#### Arguments
| Name | Type | Notes |
| ---- | ---- | ----- |
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. [Tags](#Tags) |
### Sync
Sent to server to request a [ReceivedItems](#ReceivedItems) packet to synchronize items.
#### Arguments
@@ -234,7 +266,7 @@ Requests the data package from the server. Does not require client authenticatio
#### Arguments
| Name | Type | Notes |
| ------ | ----- | ------ |
| exclusions | 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
@@ -243,13 +275,22 @@ the server will forward the message to all those targets to which any one requir
#### Arguments
| Name | Type | Notes |
| ------ | ----- | ------ |
| games | list[str] | Optional. Game names that should receive this message |
| slots | list[int] | Optional. Player IDs that should receive this message |
| tags | list[str] | Optional. Client tags that should receive this message |
| games | list\[str\] | Optional. Game names that should receive this message |
| slots | list\[int\] | Optional. Player IDs that should receive this message |
| tags | list\[str\] | Optional. Client tags that should receive this message |
| data | dict | Any data you want to send |
## Appendix
### Coop
Coop in Archipelago is automatically facilitated by the server, however some of the default behaviour may not be what you desire.
If the game in question is a remote-items game (attribute on AutoWorld), then all items will always be sent and received.
If the game in question is not a remote-items game, then any items that are placed within the same world will not be send by the server.
To manually react to others in the same player slot doing checks, listen to [RoomUpdate](#RoomUpdate) -> checked_locations.
### NetworkPlayer
A list of objects. Each object denotes one player. Each object has four fields about the player, in this order: `team`, `slot`, `alias`, and `name`. `team` and `slot` are ints, `alias` and `name` are strs.
@@ -303,15 +344,26 @@ class JSONMessagePart(TypedDict):
type: Optional[str]
color: Optional[str]
text: Optional[str]
player: Optional[int] # marks owning player id for location/item
```
`type` is used to denote the intent of the message part. This can be used to indicate special information which may be rendered differently depending on client. How these types are displayed in Archipelago's ALttP client is not the end-all be-all. Other clients may choose to interpret and display these messages differently.
Possible values for `type` include:
* player_id
* item_id
* location_id
`color` is used to denote a console color to display the message part with. This is limited to console colors due to backwards compatibility needs with games such as ALttP. Although background colors as well as foreground colors are listed, only one may be applied to a [JSONMessagePart](#JSONMessagePart) at a time.
| Name | Notes |
| ---- | ----- |
| text | Regular text content. Is the default type and as such may be omitted. |
| player_id | player ID of someone on your team, should be resolved to Player Name |
| player_name | Player Name, could be a player within a multiplayer game or from another team, not ID resolvable |
| item_id | Item ID, should be resolved to Item Name |
| item_name | Item Name, not currently used over network, but supported by reference Clients. |
| location_id | Location ID, should be resolved to Location Name |
| location_name |Location Name, not currently used over network, but supported by reference Clients. |
| entrance_name | Entrance Name. No ID mapping exists. |
| color | Regular text that should be colored. Only `type` that will contain `color` data. |
`color` is used to denote a console color to display the message part with and is only send if the `type` is `color`. This is limited to console colors due to backwards compatibility needs with games such as ALttP. Although background colors as well as foreground colors are listed, only one may be applied to a [JSONMessagePart](#JSONMessagePart) at a time.
Color options:
* bold
@@ -365,7 +417,7 @@ 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 = 0b110 # 6, forces use after goal completion, only works for forfeit and collect
auto_enabled = 0b111 # 7, forces use after goal completion, allows manual use any time
```
@@ -377,6 +429,8 @@ We encourage clients to cache the data package they receive on disk, or otherwis
Note:
* Any ID is unique to its type across AP: Item 56 only exists once and Location 56 only exists once.
* Any Name is unique to its type across its own Game only: Single Arrow can exist in two games.
* The IDs from the game "Archipelago" may be used in any other game.
Especially Location ID -1: Cheat Console and -2: Server (typically Remote Start Inventory)
#### Contents
| Name | Type | Notes |
@@ -386,8 +440,28 @@ Note:
#### GameData
GameData is a **dict** but contains these keys and values. It's broken out into another "type" for ease of documentation.
| Name | Type | Notes |
| ---- | ---- | ----- |
| item_name_to_id | dict[str, int] | Mapping of all item names to their respective ID. |
| location_name_to_id | dict[str, int] | Mapping of all location names to their respective ID. |
| version | int | Version number of this game's data |
### Tags
Tags are represented as a list of strings, the common Client tags follow:
| Name | Notes |
| ----- | ---- |
| AP | Signifies that this client is a reference client, its usefulness is mostly in debugging to compare client behaviours more easily. |
| IgnoreGame | Tells the server to ignore the "game" attribute in the [Connect](#Connect) packet. |
| DeathLink | Client participates in the DeathLink mechanic, therefore will send and receive DeathLink bounce packets |
| Tracker | Tells the server that this client is actually a Tracker and will refuse new locations from this client. |
### DeathLink
A special kind of Bounce packet that can be supported by any AP game. It targets the tag "DeathLink" and carries the following data:
| Name | Type | Notes |
| ---- | ---- | ---- |
| time | float | Unix Time Stamp of time of death. |
| cause | str | Optional. Text to explain the cause of death, ex. "Berserker was run over by a train." |
| source | str | Name of the player who first died. Can be a slot name, but can also be a name from within a multiplayer game. |

View File

@@ -0,0 +1,52 @@
# This is a sample configuration for the Web host.
# If you wish to change any of these, rename this file to config.yaml
# Default values are shown here. Uncomment and change the values as desired.
# TODO
#SELFHOST: true
# Maximum concurrent world gens
#GENERATORS: 8
# TODO
#SELFLAUNCH: true
# TODO
#DEBUG: false
# Web hosting port
#PORT: 80
# Place where uploads go.
#UPLOAD_FOLDER: uploads
# Maximum upload size. Default is 64 megabyte (64 * 1024 * 1024)
#MAX_CONTENT_LENGTH: 67108864
# Secret key used to determine important things like cookie authentication of room/seed page ownership.
# If you wish to deploy, uncomment the following line and set it to something not easily guessable.
# SECRET_KEY: "Your secret key here"
# TODO
#JOB_THRESHOLD: 2
# waitress uses one thread for I/O, these are for processing of view that get sent
#WAITRESS_THREADS: 10
# Database provider details:
#PONY:
# provider: "sqlite"
# filename: "ap.db3" # This MUST be the ABSOLUTE PATH to the file.
# create_db: true
# Maximum number of players that are allowed to be rolled on the server. After this limit, one should roll locally and upload the results.
#MAX_ROLL: 20
# TODO
#CACHE_TYPE: "simple"
# TODO
#JSON_AS_ASCII: false
# Patch target. This is the address encoded into the patch that will be used for client auto-connect.
#PATCH_TARGET: archipelago.gg

View File

@@ -23,12 +23,21 @@ server_options:
# so for example hint_cost: 20 would mean that for every 20% of available checks, you get the ability to hint, for a total of 5
hint_cost: 10 # Set to 0 if you want free hints
# Forfeit modes
# A Forfeit sends out the remaining items *from* a world that forfeits
# "disabled" -> clients can't forfeit,
# "enabled" -> clients can always forfeit
# "auto" -> automatic forfeit on goal completion, "goal" -> clients can forfeit after achieving their goal
# "auto" -> automatic forfeit on goal completion
# "auto-enabled" -> automatic forfeit on goal completion and manual forfeit is also enabled
# "goal" -> forfeit is allowed after goal completion
forfeit_mode: "goal"
# Collect modes
# A Collect sends the remaining items *to* a world that collects
# "disabled" -> clients can't collect,
# "enabled" -> clients can always collect
# "auto" -> automatic collect on goal completion
# "auto-enabled" -> automatic collect on goal completion and manual collect is also enabled
# "goal" -> collect is allowed after goal completion
collect_mode: "goal"
# Remaining modes
# !remaining handling, that tells a client which items remain in their pool
# "enabled" -> Client can always ask for remaining items
@@ -82,6 +91,15 @@ lttp_options:
# True for operating system default program
# Alternatively, a path to a program to open the .sfc file with
rom_start: true
sm_options:
# File name of the v1.0 J rom
rom_file: "Super Metroid (JU).sfc"
# Set this to your SNI folder location if you want the MultiClient to attempt an auto start, does nothing if not found
sni: "SNI"
# Set this to false to never autostart a rom (such as after patching)
# True for operating system default program
# Alternatively, a path to a program to open the .sfc file with
rom_start: true
factorio_options:
executable: "factorio\\bin\\x64\\factorio"
minecraft_options:
@@ -89,4 +107,7 @@ minecraft_options:
max_heap_size: "2G"
oot_options:
# File name of the OoT v1.0 ROM
rom_file: "The Legend of Zelda - Ocarina of Time.z64"
rom_file: "The Legend of Zelda - Ocarina of Time.z64"
soe_options:
# File name of the SoE US ROM
rom_file: "Secret of Evermore (USA).sfc"

369
inno_setup_310.iss Normal file
View File

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

View File

@@ -29,7 +29,7 @@ ArchitecturesAllowed=x64
AllowNoIcons=yes
SetupIconFile={#MyAppIcon}
UninstallDisplayIcon={app}\{#MyAppExeName}
; you will likely have to remove the following signtool line when testing/debugging localy. Don't include that change in PRs.
; you will likely have to remove the following signtool line when testing/debugging locally. Don't include that change in PRs.
SignTool= signtool
LicenseFile= LICENSE
WizardStyle= modern
@@ -48,66 +48,89 @@ Name: "playing"; Description: "Installation for playing purposes"
Name: "custom"; Description: "Custom installation"; Flags: iscustom
[Components]
Name: "core"; Description: "Core Files"; Types: full hosting playing custom; Flags: fixed
Name: "generator"; Description: "Generator"; Types: full hosting
Name: "generator/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
Name: "client/factorio"; Description: "Factorio"; Types: full playing
Name: "core"; Description: "Core Files"; Types: full hosting playing custom; Flags: fixed
Name: "generator"; Description: "Generator"; Types: full hosting
Name: "generator/sm"; Description: "Super Metroid ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
Name: "generator/soe"; Description: "Secret of Evermore ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 3145728; Flags: disablenouninstallwarning
Name: "generator/lttp"; Description: "A Link to the Past ROM Setup and Enemizer"; Types: full hosting; ExtraDiskSpaceRequired: 5191680
Name: "generator/oot"; Description: "Ocarina of Time ROM Setup"; Types: full hosting; ExtraDiskSpaceRequired: 100663296; Flags: disablenouninstallwarning
Name: "server"; Description: "Server"; Types: full hosting
Name: "client"; Description: "Clients"; Types: full playing
Name: "client/sni"; Description: "SNI Client"; Types: full playing
Name: "client/sni/lttp"; Description: "SNI Client - A Link to the Past Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Types: full playing; Flags: disablenouninstallwarning
Name: "client/factorio"; Description: "Factorio"; Types: full playing
Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278
Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing
Name: "client/oot"; Description: "Ocarina of Time Adjuster"; Types: full playing
Name: "client/ff1"; Description: "Final Fantasy 1"; Types: full playing
Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing
[Dirs]
NAME: "{app}"; Flags: setntfscompression; Permissions: everyone-modify users-modify authusers-modify;
[Files]
Source: "{code:GetROMPath}"; DestDir: "{app}"; DestName: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"; Flags: external; Components: client/lttp or generator/lttp
Source: "{code:GetROMPath}"; DestDir: "{app}"; DestName: "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"; Flags: external; Components: client/sni/lttp or generator/lttp
Source: "{code:GetSMROMPath}"; DestDir: "{app}"; DestName: "Super Metroid (JU).sfc"; Flags: external; Components: client/sni/sm or generator/sm
Source: "{code:GetSoEROMPath}"; DestDir: "{app}"; DestName: "Secret of Evermore (USA).sfc"; Flags: external; Components: generator/soe
Source: "{code:GetOoTROMPath}"; DestDir: "{app}"; DestName: "The Legend of Zelda - Ocarina of Time.z64"; Flags: external; Components: generator/oot
Source: "{#sourcepath}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "{#sourcepath}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/lttp
Source: "{#sourcepath}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/sni
Source: "{#sourcepath}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp
Source: "{#sourcepath}\ArchipelagoGenerate.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: generator
Source: "{#sourcepath}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: server
Source: "{#sourcepath}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio
Source: "{#sourcepath}\ArchipelagoTextClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/text
Source: "{#sourcepath}\ArchipelagoLttPClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/lttp
Source: "{#sourcepath}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/lttp
Source: "{#sourcepath}\ArchipelagoSNIClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni
Source: "{#sourcepath}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni/lttp or generator/lttp
Source: "{#sourcepath}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft
Source: "{#sourcepath}\ArchipelagoOoTAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/oot
Source: "{#sourcepath}\ArchipelagoFF1Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ff1
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
;minecraft temp files
Source: "{tmp}\forge-installer.jar"; DestDir: "{app}"; Flags: skipifsourcedoesntexist external deleteafterinstall; Components: client/minecraft
[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} Text Client"; Filename: "{app}\ArchipelagoTextClient.exe"; Components: client/text
Name: "{group}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Components: client/sni
Name: "{group}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Components: client/factorio
Name: "{group}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Components: client/minecraft
Name: "{group}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Components: client/ff1
Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon
Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Components: server
Name: "{commondesktop}\{#MyAppName} LttP Client"; Filename: "{app}\ArchipelagoLttPClient.exe"; Tasks: desktopicon; Components: client/lttp
Name: "{commondesktop}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Tasks: desktopicon; Components: client/sni
Name: "{commondesktop}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Tasks: desktopicon; Components: client/factorio
Name: "{commondesktop}\{#MyAppName} Final Fantasy 1 Client"; Filename: "{app}\ArchipelagoFF1Client.exe"; Tasks: desktopicon; Components: client/factorio
[Run]
Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/passive /norestart"; Check: IsVCRedist64BitNeeded; StatusMsg: "Installing VC++ redistributable..."
Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."; Components: client/lttp or generator
Filename: "{app}\jre8\bin\java.exe"; Parameters: "-jar ""{app}\forge-installer.jar"" --installServer ""{app}\Minecraft Forge server"""; Flags: runhidden; Check: IsForgeNeeded(); StatusMsg: "Installing Forge Server..."; Components: client/minecraft
Filename: "{app}\ArchipelagoLttPAdjuster"; Parameters: "--update_sprites"; StatusMsg: "Updating Sprite Library..."; Components: client/sni/lttp or generator/lttp
Filename: "{app}\ArchipelagoMinecraftClient.exe"; Parameters: "--install"; StatusMsg: "Installing Forge Server..."; Components: client/minecraft
[UninstallDelete]
Type: dirifempty; Name: "{app}"
[InstallDelete]
Type: files; Name: "{app}\ArchipelagoLttPClient.exe"
[Registry]
Root: HKCR; Subkey: ".apbp"; ValueData: "{#MyAppName}patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/lttp
Root: HKCR; Subkey: "{#MyAppName}patch"; ValueData: "Archipelago Binary Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/lttp
Root: HKCR; Subkey: "{#MyAppName}patch\DefaultIcon"; ValueData: "{app}\ArchipelagoLttPClient.exe,0"; ValueType: string; ValueName: ""; Components: client/lttp
Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\ArchipelagoLttPClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/lttp
Root: HKCR; Subkey: ".apbp"; ValueData: "{#MyAppName}patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}patch"; ValueData: "Archipelago Binary Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: ".apm3"; ValueData: "{#MyAppName}smpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}smpatch"; ValueData: "Archipelago Super Metroid Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}smpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}smpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: ".apsoe"; ValueData: "{#MyAppName}soepatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}soepatch"; ValueData: "Archipelago Secret of Evermore Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}soepatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
Root: HKCR; Subkey: "{#MyAppName}soepatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1""";
Root: HKCR; Subkey: ".apmc"; ValueData: "{#MyAppName}mcdata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/minecraft
Root: HKCR; Subkey: "{#MyAppName}mcdata"; ValueData: "Archipelago Minecraft Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/minecraft
@@ -120,12 +143,10 @@ Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{ap
Root: HKCR; Subkey: "{#MyAppName}multidata\shell\open\command"; ValueData: """{app}\ArchipelagoServer.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: server
[Code]
const
SHCONTCH_NOPROGRESSBOX = 4;
SHCONTCH_RESPONDYESTOALL = 16;
FORGE_VERSION = '1.16.5-36.2.0';
// See: https://stackoverflow.com/a/51614652/2287576
function IsVCRedist64BitNeeded(): boolean;
@@ -147,88 +168,67 @@ begin
end;
end;
function IsForgeNeeded(): boolean;
begin
Result := True;
if (FileExists(ExpandConstant('{app}')+'\Minecraft Forge Server\forge-'+FORGE_VERSION+'.jar')) then
Result := False;
end;
function IsJavaNeeded(): boolean;
begin
Result := True;
if (FileExists(ExpandConstant('{app}')+'\jre8\bin\java.exe')) then
Result := False;
end;
function OnDownloadMinecraftProgress(const Url, FileName: String; const Progress, ProgressMax: Int64): Boolean;
begin
if Progress = ProgressMax then
Log(Format('Successfully downloaded Minecraft additional files to {tmp}: %s', [FileName]));
Result := True;
end;
procedure UnZip(ZipPath, TargetPath: string);
var
Shell: Variant;
ZipFile: Variant;
TargetFolder: Variant;
begin
Shell := CreateOleObject('Shell.Application');
ZipFile := Shell.NameSpace(ZipPath);
if VarIsClear(ZipFile) then
RaiseException(
Format('ZIP file "%s" does not exist or cannot be opened', [ZipPath]));
TargetFolder := Shell.NameSpace(TargetPath);
if VarIsClear(TargetFolder) then
RaiseException(Format('Target path "%s" does not exist', [TargetPath]));
TargetFolder.CopyHere(
ZipFile.Items, SHCONTCH_NOPROGRESSBOX or SHCONTCH_RESPONDYESTOALL);
end;
var ROMFilePage: TInputFileWizardPage;
var R : longint;
var rom: string;
var lttprom: string;
var LttPROMFilePage: TInputFileWizardPage;
var smrom: string;
var SMRomFilePage: TInputFileWizardPage;
var soerom: string;
var SoERomFilePage: TInputFileWizardPage;
var ootrom: string;
var OoTROMFilePage: TInputFileWizardPage;
var MinecraftDownloadPage: TDownloadWizardPage;
procedure AddRomPage();
function GetSNESMD5OfFile(const rom: string): string;
var data: AnsiString;
begin
rom := FileSearch('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', WizardDirValue());
if LoadStringFromFile(rom, data) then
begin
if Length(data) mod 1024 = 512 then
begin
data := copy(data, 513, Length(data)-512);
end;
Result := GetMD5OfString(data);
end;
end;
function CheckRom(name: string; hash: string): string;
var rom: string;
begin
log('Handling ' + name)
rom := FileSearch(name, WizardDirValue());
if Length(rom) > 0 then
begin
log('existing ROM found');
log(IntToStr(CompareStr(GetMD5OfFile(rom), '03a63945398191337e896e5771f77173')));
if CompareStr(GetMD5OfFile(rom), '03a63945398191337e896e5771f77173') = 0 then
log(IntToStr(CompareStr(GetSNESMD5OfFile(rom), hash)));
if CompareStr(GetSNESMD5OfFile(rom), hash) = 0 then
begin
log('existing ROM verified');
Result := rom;
exit;
end;
log('existing ROM failed verification');
end;
rom := ''
ROMFilePage :=
end;
function AddRomPage(name: string): TInputFileWizardPage;
begin
Result :=
CreateInputFilePage(
wpSelectComponents,
'Select ROM File',
'Where is your Zelda no Densetsu - Kamigami no Triforce (Japan).sfc located?',
'Where is your ' + name + ' located?',
'Select the file, then click Next.');
ROMFilePage.Add(
Result.Add(
'Location of ROM file:',
'SNES ROM files|*.sfc|All files|*.*',
'SNES ROM files|*.sfc;*.smc|All files|*.*',
'.sfc');
end;
procedure AddMinecraftDownloads();
begin
MinecraftDownloadPage := CreateDownloadPage(SetupMessage(msgWizardPreparing), SetupMessage(msgPreparingDesc), @OnDownloadMinecraftProgress);
end;
procedure AddOoTRomPage();
begin
ootrom := FileSearch('The Legend of Zelda - Ocarina of Time.z64', WizardDirValue());
@@ -261,63 +261,61 @@ end;
function NextButtonClick(CurPageID: Integer): Boolean;
begin
if (CurPageID = wpReady) and (WizardIsComponentSelected('client/minecraft')) then begin
MinecraftDownloadPage.Clear;
if(IsForgeNeeded()) then
MinecraftDownloadPage.Add('https://maven.minecraftforge.net/net/minecraftforge/forge/'+FORGE_VERSION+'/forge-'+FORGE_VERSION+'-installer.jar','forge-installer.jar','');
if(IsJavaNeedeD()) then
MinecraftDownloadPage.Add('https://corretto.aws/downloads/latest/amazon-corretto-8-x64-windows-jre.zip','java.zip','');
MinecraftDownloadPage.Show;
try
try
MinecraftDownloadPage.Download;
Result := True;
except
if MinecraftDownloadPage.AbortedByUser then
Log('Aborted by user.')
else
SuppressibleMsgBox(AddPeriod(GetExceptionMessage), mbCriticalError, MB_OK, IDOK);
Result := False;
end;
finally
if( isJavaNeeded() ) then
if(ForceDirectories(ExpandConstant('{app}'))) then
UnZip(ExpandConstant('{tmp}')+'\java.zip',ExpandConstant('{app}'));
MinecraftDownloadPage.Hide;
end;
if (assigned(LttPROMFilePage)) and (CurPageID = LttPROMFilePage.ID) then
Result := not (LttPROMFilePage.Values[0] = '')
else if (assigned(SMROMFilePage)) and (CurPageID = SMROMFilePage.ID) then
Result := not (SMROMFilePage.Values[0] = '')
else if (assigned(SoEROMFilePage)) and (CurPageID = SoEROMFilePage.ID) then
Result := not (SoEROMFilePage.Values[0] = '')
else if (assigned(OoTROMFilePage)) and (CurPageID = OoTROMFilePage.ID) then
Result := not (OoTROMFilePage.Values[0] = '')
else
Result := True;
end else
Result := True;
end;
procedure InitializeWizard();
begin
AddOoTRomPage();
AddRomPage();
AddMinecraftDownloads();
end;
function ShouldSkipPage(PageID: Integer): Boolean;
begin
Result := False;
if (assigned(ROMFilePage)) and (PageID = ROMFilePage.ID) then
Result := not (WizardIsComponentSelected('client/lttp') or WizardIsComponentSelected('generator/lttp'));
if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then
Result := not (WizardIsComponentSelected('generator/oot'));
end;
function GetROMPath(Param: string): string;
begin
if Length(rom) > 0 then
Result := rom
else if Assigned(RomFilePage) then
if Length(lttprom) > 0 then
Result := lttprom
else if Assigned(LttPRomFilePage) then
begin
R := CompareStr(GetMD5OfFile(ROMFilePage.Values[0]), '03a63945398191337e896e5771f77173')
R := CompareStr(GetSNESMD5OfFile(LttPROMFilePage.Values[0]), '03a63945398191337e896e5771f77173')
if R <> 0 then
MsgBox('ALttP ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
Result := ROMFilePage.Values[0]
Result := LttPROMFilePage.Values[0]
end
else
Result := '';
end;
function GetSMROMPath(Param: string): string;
begin
if Length(smrom) > 0 then
Result := smrom
else if Assigned(SMRomFilePage) then
begin
R := CompareStr(GetSNESMD5OfFile(SMROMFilePage.Values[0]), '21f3e98df4780ee1c667b84e57d88675')
if R <> 0 then
MsgBox('Super Metroid ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
Result := SMROMFilePage.Values[0]
end
else
Result := '';
end;
function GetSoEROMPath(Param: string): string;
begin
if Length(soerom) > 0 then
Result := soerom
else if Assigned(SoERomFilePage) then
begin
R := CompareStr(GetSNESMD5OfFile(SoEROMFilePage.Values[0]), '6e9c94511d04fac6e0a1e582c170be3a')
if R <> 0 then
MsgBox('Secret of Evermore ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
Result := SoEROMFilePage.Values[0]
end
else
Result := '';
@@ -332,9 +330,40 @@ begin
R := CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '5bd1fe107bf8106b2ab6650abecd54d6') * CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '6697768a7a7df2dd27a692a2638ea90b') * CompareStr(GetMD5OfFile(OoTROMFilePage.Values[0]), '05f0f3ebacbc8df9243b6148ffe4792f');
if R <> 0 then
MsgBox('OoT ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
Result := OoTROMFilePage.Values[0]
end
else
Result := '';
end;
procedure InitializeWizard();
begin
AddOoTRomPage();
lttprom := CheckRom('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc', '03a63945398191337e896e5771f77173');
if Length(lttprom) = 0 then
LttPROMFilePage:= AddRomPage('Zelda no Densetsu - Kamigami no Triforce (Japan).sfc');
smrom := CheckRom('Super Metroid (JU).sfc', '21f3e98df4780ee1c667b84e57d88675');
if Length(smrom) = 0 then
SMRomFilePage:= AddRomPage('Super Metroid (JU).sfc');
soerom := CheckRom('Secret of Evermore (USA).sfc', '6e9c94511d04fac6e0a1e582c170be3a');
if Length(soerom) = 0 then
SoEROMFilePage:= AddRomPage('Secret of Evermore (USA).sfc');
end;
function ShouldSkipPage(PageID: Integer): Boolean;
begin
Result := False;
if (assigned(LttPROMFilePage)) and (PageID = LttPROMFilePage.ID) then
Result := not (WizardIsComponentSelected('client/sni/lttp') or WizardIsComponentSelected('generator/lttp'));
if (assigned(SMROMFilePage)) and (PageID = SMROMFilePage.ID) then
Result := not (WizardIsComponentSelected('client/sni/sm') or WizardIsComponentSelected('generator/sm'));
if (assigned(SoEROMFilePage)) and (PageID = SoEROMFilePage.ID) then
Result := not (WizardIsComponentSelected('generator/soe'));
if (assigned(OoTROMFilePage)) and (PageID = OoTROMFilePage.ID) then
Result := not (WizardIsComponentSelected('generator/oot'));
end;

317
kvui.py
View File

@@ -1,42 +1,255 @@
import os
import logging
import typing
import asyncio
os.environ["KIVY_NO_CONSOLELOG"] = "1"
os.environ["KIVY_NO_FILELOG"] = "1"
os.environ["KIVY_NO_ARGS"] = "1"
os.environ["KIVY_LOG_ENABLE"] = "0"
from kivy.base import Config
Config.set("input", "mouse", "mouse,disable_multitouch")
Config.set('kivy', 'exit_on_escape', '0')
Config.set('graphics', 'multisamples', '0') # multisamples crash old intel drivers
from kivy.app import App
from kivy.base import ExceptionHandler, ExceptionManager, Config
from kivy.core.window import Window
from kivy.core.clipboard import Clipboard
from kivy.core.text.markup import MarkupLabel
from kivy.base import ExceptionHandler, ExceptionManager, Clock
from kivy.factory import Factory
from kivy.properties import BooleanProperty, ObjectProperty
from kivy.uix.button import Button
from kivy.uix.gridlayout import GridLayout
from kivy.uix.textinput import TextInput
from kivy.uix.recycleview import RecycleView
from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.label import Label
from kivy.uix.progressbar import ProgressBar
from kivy.utils import escape_markup
from kivy.lang import Builder
from kivy.uix.recycleview.views import RecycleDataViewBehavior
from kivy.uix.behaviors import FocusBehavior
from kivy.uix.recycleboxlayout import RecycleBoxLayout
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
import Utils
from NetUtils import JSONtoTextParser, JSONMessagePart
if typing.TYPE_CHECKING:
import CommonClient
context_type = CommonClient.CommonContext
else:
context_type = object
# I was surprised to find this didn't already exist in kivy :(
class HoverBehavior(object):
"""from https://stackoverflow.com/a/605348110"""
hovered = BooleanProperty(False)
border_point = ObjectProperty(None)
def __init__(self, **kwargs):
self.register_event_type('on_enter')
self.register_event_type('on_leave')
Window.bind(mouse_pos=self.on_mouse_pos)
Window.bind(on_cursor_leave=self.on_cursor_leave)
super(HoverBehavior, self).__init__(**kwargs)
def on_mouse_pos(self, *args):
if not self.get_root_window():
return # do proceed if I'm not displayed <=> If have no parent
pos = args[1]
# Next line to_widget allow to compensate for relative layout
inside = self.collide_point(*self.to_widget(*pos))
if self.hovered == inside:
return # We have already done what was needed
self.border_point = pos
self.hovered = inside
if inside:
self.dispatch("on_enter")
else:
self.dispatch("on_leave")
def on_cursor_leave(self, *args):
# if the mouse left the window, it is obviously no longer inside the hover label.
self.hovered = BooleanProperty(False)
self.border_point = ObjectProperty(None)
self.dispatch("on_leave")
Factory.register('HoverBehavior', HoverBehavior)
class ServerToolTip(Label):
pass
class ServerLabel(HoverBehavior, Label):
def __init__(self, *args, **kwargs):
super(ServerLabel, self).__init__(*args, **kwargs)
self.layout = FloatLayout()
self.popuplabel = ServerToolTip(text="Test")
self.layout.add_widget(self.popuplabel)
def on_enter(self):
self.popuplabel.text = self.get_text()
App.get_running_app().root.add_widget(self.layout)
def on_leave(self):
App.get_running_app().root.remove_widget(self.layout)
def get_text(self):
if self.ctx.server:
ctx = self.ctx
text = f"Connected to: {ctx.server_address}."
if ctx.slot is not None:
text += f"\nYou are Slot Number {ctx.slot} in Team Number {ctx.team}, " \
f"named {ctx.player_names[ctx.slot]}."
if ctx.items_received:
text += f"\nYou have received {len(ctx.items_received)} items. " \
f"You can list them in order with /received."
if ctx.total_locations:
text += f"\nYou have checked {len(ctx.checked_locations)} " \
f"out of {ctx.total_locations} locations. " \
f"You can get more info on missing checks with /missing."
if ctx.permissions:
text += "\nPermissions:"
for permission_name, permission_data in ctx.permissions.items():
text += f"\n {permission_name}: {permission_data}"
if ctx.hint_cost is not None and ctx.total_locations:
text += f"\nA new !hint <itemname> costs {ctx.hint_cost}% of checks made. " \
f"For you this means every {max(0, int(ctx.hint_cost * 0.01 * ctx.total_locations))} " \
"location checks."
elif ctx.hint_cost == 0:
text += "\n!hint is free to use."
else:
text += f"\nYou are not authenticated yet."
return text
else:
return "No current server connection. \nPlease connect to an Archipelago server."
@property
def ctx(self) -> context_type:
return App.get_running_app().ctx
class MainLayout(GridLayout):
pass
class ContainerLayout(FloatLayout):
pass
class SelectableRecycleBoxLayout(FocusBehavior, LayoutSelectionBehavior,
RecycleBoxLayout):
""" Adds selection and focus behaviour to the view. """
class SelectableLabel(RecycleDataViewBehavior, Label):
""" Add selection support to the Label """
index = None
selected = BooleanProperty(False)
def refresh_view_attrs(self, rv, index, data):
""" Catch and handle the view changes """
self.index = index
return super(SelectableLabel, self).refresh_view_attrs(
rv, index, data)
def on_touch_down(self, touch):
""" Add selection on touch down """
if super(SelectableLabel, self).on_touch_down(touch):
return True
if self.collide_point(*touch.pos):
if self.selected:
self.parent.clear_selection()
else:
# Not a fan of the following few lines, but they work.
temp = MarkupLabel(text=self.text).markup
text = "".join(part for part in temp if not part.startswith(("[color", "[/color]")))
cmdinput = App.get_running_app().textinput
if not cmdinput.text and " did you mean " in text:
for question in ("Didn't find something that closely matches, did you mean ",
"Too many close matches, did you mean "):
if text.startswith(question):
name = Utils.get_text_between(text, question,
"? (")
cmdinput.text = f"!{App.get_running_app().last_autofillable_command} {name}"
break
Clipboard.copy(text)
return self.parent.select_with_touch(self.index, touch)
def apply_selection(self, rv, index, is_selected):
""" Respond to the selection of items in the view. """
self.selected = is_selected
class GameManager(App):
logging_pairs = [
("Client", "Archipelago"),
]
base_title: str = "Archipelago Client"
last_autofillable_command: str
def __init__(self, ctx):
def __init__(self, ctx: context_type):
self.title = self.base_title
self.ctx = ctx
self.commandprocessor = ctx.command_processor(ctx)
self.icon = r"data/icon.png"
self.json_to_kivy_parser = KivyJSONtoTextParser(ctx)
self.log_panels = {}
# keep track of last used command to autofill on click
self.last_autofillable_command = "hint"
autofillable_commands = ("hint_location", "hint", "getitem")
original_say = ctx.on_user_say
def intercept_say(text):
text = original_say(text)
if text:
for command in autofillable_commands:
if text.startswith("!"+command):
self.last_autofillable_command = command
break
return text
ctx.on_user_say = intercept_say
super(GameManager, self).__init__()
def build(self):
self.grid = GridLayout()
self.container = ContainerLayout()
self.grid = MainLayout()
self.grid.cols = 1
connect_layout = BoxLayout(orientation="horizontal", size_hint_y=None, height=30)
# top part
server_label = ServerLabel()
connect_layout.add_widget(server_label)
self.server_connect_bar = TextInput(text="archipelago.gg", size_hint_y=None, height=30, multiline=False,
write_tab=False)
self.server_connect_bar.bind(on_text_validate=self.connect_button_action)
connect_layout.add_widget(self.server_connect_bar)
self.server_connect_button = Button(text="Connect", size=(100, 30), size_hint_y=None, size_hint_x=None)
self.server_connect_button.bind(on_press=self.connect_button_action)
connect_layout.add_widget(self.server_connect_button)
self.grid.add_widget(connect_layout)
self.progressbar = ProgressBar(size_hint_y=None, height=3)
self.grid.add_widget(self.progressbar)
self.tabs = TabbedPanel()
# middle part
self.tabs = TabbedPanel(size_hint_y=1)
self.tabs.default_tab_text = "All"
self.log_panels["All"] = self.tabs.default_tab_content = UILog(*(logging.getLogger(logger_name)
for logger_name, name in
self.logging_pairs))
@@ -48,13 +261,67 @@ class GameManager(App):
self.tabs.add_widget(panel)
self.grid.add_widget(self.tabs)
textinput = TextInput(size_hint_y=None, height=30, multiline=False)
textinput.bind(on_text_validate=self.on_message)
self.grid.add_widget(textinput)
if len(self.logging_pairs) == 1:
# Hide Tab selection if only one tab
self.tabs.clear_tabs()
self.tabs.do_default_tab = False
self.tabs.current_tab.height = 0
self.tabs.tab_height = 0
# bottom part
bottom_layout = BoxLayout(orientation="horizontal", size_hint_y=None, height=30)
info_button = Button(height=30, text="Command:", size_hint_x=None)
info_button.bind(on_release=self.command_button_action)
bottom_layout.add_widget(info_button)
self.textinput = TextInput(size_hint_y=None, height=30, multiline=False, write_tab=False)
self.textinput.bind(on_text_validate=self.on_message)
def text_focus(event):
"""Needs to be set via delay, as unfocusing happens after on_message"""
self.textinput.focus = True
self.textinput.text_focus = text_focus
bottom_layout.add_widget(self.textinput)
self.grid.add_widget(bottom_layout)
self.commandprocessor("/help")
return self.grid
Clock.schedule_interval(self.update_texts, 1 / 30)
self.container.add_widget(self.grid)
return self.container
def update_texts(self, dt):
if self.ctx.server:
self.title = self.base_title + " " + Utils.__version__ + \
f" | Connected to: {self.ctx.server_address} " \
f"{'.'.join(str(e) for e in self.ctx.server_version)}"
self.server_connect_button.text = "Disconnect"
self.progressbar.max = len(self.ctx.checked_locations) + len(self.ctx.missing_locations)
self.progressbar.value = len(self.ctx.checked_locations)
else:
self.server_connect_button.text = "Connect"
self.title = self.base_title + " " + Utils.__version__
self.progressbar.value = 0
def command_button_action(self, button):
if self.ctx.server:
logging.getLogger("Client").info("/help for client commands and !help for server commands.")
else:
logging.getLogger("Client").info("/help for client commands and once you are connected, "
"!help for server commands.")
def connect_button_action(self, button):
if self.ctx.server:
self.ctx.server_address = None
asyncio.create_task(self.ctx.disconnect())
else:
asyncio.create_task(self.ctx.connect(self.server_connect_bar.text.replace("/connect ", "")))
def on_stop(self):
# "kill" input tasks
for x in range(self.ctx.input_requests):
self.ctx.input_queue.put_nowait("")
self.ctx.input_requests = 0
self.ctx.exit_event.set()
def on_message(self, textinput: TextInput):
@@ -67,6 +334,9 @@ class GameManager(App):
self.ctx.input_queue.put_nowait(input_text)
elif input_text:
self.commandprocessor(input_text)
Clock.schedule_once(textinput.text_focus)
except Exception as e:
logging.getLogger("Client").exception(e)
@@ -82,31 +352,38 @@ class FactorioManager(GameManager):
("FactorioServer", "Factorio Server Log"),
("FactorioWatcher", "Bridge Data Log"),
]
title = "Archipelago Factorio Client"
base_title = "Archipelago Factorio Client"
class LttPManager(GameManager):
class SNIManager(GameManager):
logging_pairs = [
("Client", "Archipelago"),
("SNES", "SNES"),
]
title = "Archipelago LttP Client"
base_title = "Archipelago SNI Client"
class TextManager(GameManager):
logging_pairs = [
("Client", "Archipelago")
]
title = "Archipelago Text Client"
base_title = "Archipelago Text Client"
class FF1Manager(GameManager):
logging_pairs = [
("Client", "Archipelago")
]
base_title = "Archipelago Final Fantasy 1 Client"
class LogtoUI(logging.Handler):
def __init__(self, on_log):
super(LogtoUI, self).__init__(logging.DEBUG)
super(LogtoUI, self).__init__(logging.INFO)
self.on_log = on_log
def handle(self, record: logging.LogRecord) -> None:
self.on_log(record)
self.on_log(self.format(record))
class UILog(RecycleView):
@@ -118,8 +395,8 @@ class UILog(RecycleView):
for logger in loggers_to_handle:
logger.addHandler(LogtoUI(self.on_log))
def on_log(self, record: logging.LogRecord) -> None:
self.data.append({"text": escape_markup(record.getMessage())})
def on_log(self, record: str) -> None:
self.data.append({"text": escape_markup(record)})
def on_message_markup(self, text):
self.data.append({"text": text})
@@ -129,8 +406,8 @@ class E(ExceptionHandler):
logger = logging.getLogger("Client")
def handle_exception(self, inst):
self.logger.exception(inst)
return ExceptionManager.RAISE
self.logger.exception("Uncaught Exception:", exc_info=inst)
return ExceptionManager.PASS
class KivyJSONtoTextParser(JSONtoTextParser):
@@ -159,6 +436,4 @@ 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

@@ -4,12 +4,6 @@
# For example, if a meta.yaml fast_ganon result is rolled, every player will have that fast_ganon goal
# There is the special case of null, which ignores that part of the meta.yaml,
# allowing for a chance for that meta to not take effect
# Players can also have a meta_ignore option to ignore specific options
# Example of ignore that would be in a player's file:
# meta_ignore:
# mode:
# inverted
# This means, if mode is meta-rolled and the result happens to be inverted, then defer to the player's yaml instead.
meta_description: Meta-Mystery file with the intention of having similar-length completion times for a hopefully better experience
null:
progression_balancing: # Progression balancing tries to make sure that the player has *something* towards any players goal in each "sphere"
@@ -33,26 +27,6 @@ A Link to the Past:
open: 60
inverted: 10
null: 10 # Maintain individual world states
tower_open:
'0': 8
'1': 7
'2': 6
'3': 5
'4': 4
'5': 3
'6': 2
'7': 1
random: 10 # A different GT open time should not usually result in a vastly different completion time, unless ganon goal and tower_open > ganon_open
ganon_open:
'0': 3
'1': 4
'2': 5
'3': 6
'4': 7
'5': 8
'6': 9
'7': 10
random: 5 # This will mean differing completion times. But leaving it for that surprise effect
triforce_pieces_mode: #Determine how to calculate the extra available triforce pieces.
extra: 0 # available = triforce_pieces_extra + triforce_pieces_required
percentage: 0 # available = (triforce_pieces_percentage /100) * triforce_pieces_required

View File

@@ -24,12 +24,8 @@ name: YourName{number} # Your name in-game. Spaces will be replaced with undersc
#{number} will be replaced with the counter value of the name.
#{NUMBER} will be replaced with the counter value of the name if the counter value is greater than 1.
game: # Pick a game to play
A Link to the Past: 0
Factorio: 0
Minecraft: 0
Subnautica: 0
Slay the Spire: 0
Ocarina of Time: 0
A Link to the Past: 1
requires:
version: 0.1.7 # Version of Archipelago required for this yaml to work as expected.
# Shared Options supported by all games:
@@ -54,232 +50,9 @@ progression_balancing:
# - "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.
# exclude_locations: # Force certain locations to never contain progression items, and always be filled with junk.
# - "Master Sword Pedestal"
Subnautica: {}
Slay the Spire:
character: # Pick What Character you wish to play with.
ironclad: 50
silent: 50
defect: 50
watcher: 50
ascension: # What Ascension do you wish to play with.
# you can add additional values between minimum and maximum
0: 50 # minimum value
20: 0 # maximum value
random: 0
random-low: 0
random-high: 0
heart_run: # Whether or not you will need to collect they 3 keys to unlock the final act
# and beat the heart to finish the game.
false: 50
true: 0
Factorio:
tech_tree_layout:
single: 1
small_diamonds: 1
medium_diamonds: 1
large_diamonds: 1
small_pyramids: 1
medium_pyramids: 1
large_pyramids: 1
small_funnels: 1
medium_funnels: 1
large_funnels: 1
recipe_time: # randomize the time it takes for any recipe to craft, this includes smelting, chemical lab, hand crafting etc.
vanilla: 1
fast: 0 # 25% to 100% of original time
normal: 0 # 50 % to 200% of original time
slow: 0 # 100% to 400% of original time
chaos: 0 # 25% to 400% of original time
recipe_ingredients:
rocket: 1 # only randomize rocket part recipe
science_pack: 1 # also randomize science pack ingredients
max_science_pack:
automation_science_pack: 0
logistic_science_pack: 0
military_science_pack: 0
chemical_science_pack: 0
production_science_pack: 0
utility_science_pack: 0
space_science_pack: 1
tech_cost:
very_easy : 0
easy : 0
kind : 0
normal : 1
hard : 0
very_hard : 0
insane : 0
silo:
vanilla: 1
randomize_recipe: 0
spawn: 0 # spawn silo near player spawn point
free_samples:
none: 1
single_craft: 0
half_stack: 0
stack: 0
progressive:
on: 1
off: 0
grouped_random: 0
tech_tree_information:
none: 0
advancement: 0 # show which items are a logical advancement
full: 1 # show full info on each tech node
imported_blueprints: # can be turned off to prevent access to blueprints created outside the current world
on: 1
off: 0
starting_items:
burner-mining-drill: 19
stone-furnace: 19
# Note: Total amount of traps cannot exceed 4, if the sum of them is higher it will get automatically capped.
evolution_traps:
# Trap items that when received increase the enemy evolution.
0: 1
1: 0
2: 0
3: 0
4: 0
random: 0
random-low: 0
random-middle: 0
random-high: 0
evolution_trap_increase:
# If present, % increase of Evolution with each trap received.
5: 0
10: 1
15: 0
20: 0
100: 0
random: 0
random-low: 0
random-middle: 0
random-high: 0
attack_traps:
# Trap items that when received trigger an attack on your base.
0: 1
1: 0
2: 0
3: 0
4: 0
random: 0
random-low: 0
random-middle: 0
random-high: 0
world_gen:
# frequency, size, richness, terrain segmentation, starting area and water are all of https://wiki.factorio.com/Types/MapGenSize
# inverse of water scale
terrain_segmentation: 0.5
water: 1.5
autoplace_controls:
# set size to 0 to disable
coal:
frequency: 1
size: 3
richness: 6
copper-ore:
frequency: 1
size: 3
richness: 6
crude-oil:
frequency: 1
size: 3
richness: 6
enemy-base:
frequency: 1
size: 1
richness: 1
iron-ore:
frequency: 1
size: 3
richness: 6
stone:
frequency: 1
size: 3
richness: 6
trees:
frequency: 1
size: 1
richness: 1
uranium-ore:
frequency: 1
size: 3
richness: 6
seed: null # turn into positive number to create specific seed
starting_area: 1
peaceful_mode: 0
cliff_settings:
name: cliff
cliff_elevation_0: 10 # base elevation, can't be changed in GUI
cliff_elevation_interval: 40 # 40/frequency
richness: 1 # 0: off, >0: continuity
property_expression_names:
"control-setting:moisture:bias": 0 # grass bias -0.5 to +0.5
"control-setting:moisture:frequency:multiplier": 1 # 1/scale in GUI
"control-setting:aux:bias": 0 # -sand/+red desert bias -0.5 to +0.5
"control-setting:aux:frequency:multiplier": 1 # 1/scale in GUI
pollution:
enabled: true
diffusion_ratio: 0.02
ageing: 1 # GUI dissipation factor
enemy_attack_pollution_consumption_modifier: 1
min_pollution_to_damage_trees: 60
pollution_restored_per_tree_damage: 10
enemy_evolution:
enabled: true
time_factor: 40.0e-7 # GUI value * 0.0000001
destroy_factor: 200.0e-5 # GUI value * 0.00001
pollution_factor: 9.0e-7 # GUI value * 0.0000001
enemy_expansion:
enabled: true
max_expansion_distance: 7
settler_group_min_size: 5
settler_group_max_size: 20
min_expansion_cooldown: 14400 # 1 to 60 min in ticks
max_expansion_cooldown: 216000 # 5 to 180 min in ticks
Minecraft:
advancement_goal: 50 # Number of advancements required (87 max) to spawn the Ender Dragon and complete the game.
egg_shards_required: # Number of dragon egg shards to collect (30 max) before the Ender Dragon will spawn.
0: 1
5: 0
10: 0
20: 0
egg_shards_available: # Number of egg shards available in the pool (30 max).
0: 1
5: 0
10: 0
20: 0
combat_difficulty: # Modifies the level of items logically required for exploring dangerous areas and fighting bosses.
easy: 0
normal: 1
hard: 0
include_hard_advancements: # Junk-fills certain RNG-reliant or tedious advancements.
on: 0
off: 1
include_insane_advancements: # Junk-fills extremely difficult advancements; this is only How Did We Get Here? and Adventuring Time.
on: 0
off: 1
include_postgame_advancements: # Some advancements require defeating the Ender Dragon; this will junk-fill them so you won't have to finish to send some items.
on: 0
off: 1
shuffle_structures: # Enables shuffling of villages, outposts, fortresses, bastions, and end cities.
on: 0
off: 1
structure_compasses: # Adds structure compasses to the item pool, which point to the nearest indicated structure.
on: 0
off: 1
bee_traps: # Replaces a percentage of junk items with bee traps, which spawn multiple angered bees around every player when received.
0: 1
25: 0
50: 0
75: 0
100: 0
send_defeated_mobs: # Send killed mobs to other Minecraft worlds which have this option enabled.
on: 0
off: 1
A Link to the Past:
### Logic Section ###
glitches_required: # Determine the logic required to complete the seed
@@ -481,13 +254,20 @@ A Link to the Past:
'on': 0 # Keys, items, and buttons hidden under pots in dungeons are shuffled with other pots in their supertile
'off': 50 # Default pot item locations
### End of Enemizer Section ###
beemizer: # Remove items from the global item pool and replace them with single bees (fill bottles) and bee traps
0: 50 # No bee traps are placed
1: 0 # 25% of rupees, bombs and arrows are replaced with bees, of which 60% are traps and 40% single bees
2: 0 # 50% of rupees, bombs and arrows are replaced with bees, of which 70% are traps and 30% single bees
3: 0 # 75% of rupees, bombs and arrows are replaced with bees, of which 80% are traps and 20% single bees
4: 0 # 100% of rupees, bombs and arrows are replaced with bees, of which 90% are traps and 10% single bees
5: 0 # 100% of rupees, bombs and arrows are replaced with bees, of which 100% are traps and 0% single bees
### Beemizer ###
# can add weights for any whole number between 0 and 100
beemizer_total_chance: # Remove items from the global item pool and replace them with single bees (fill bottles) and bee traps
0: 50 # No junk fill items are replaced (Beemizer is off)
25: 0 # 25% chance for each junk fill item (rupees, bombs and arrows) to be replaced with bees
50: 0 # 50% chance for each junk fill item (rupees, bombs and arrows) to be replaced with bees
75: 0 # 75% chance for each junk fill item (rupees, bombs and arrows) to be replaced with bees
100: 0 # All junk fill items (rupees, bombs and arrows) are replaced with bees
beemizer_trap_chance:
60: 50 # 60% chance for each beemizer replacement to be a trap, 40% chance to be a single bee
70: 0 # 70% chance for each beemizer replacement to be a trap, 30% chance to be a single bee
80: 0 # 80% chance for each beemizer replacement to be a trap, 20% chance to be a single bee
90: 0 # 90% chance for each beemizer replacement to be a trap, 10% chance to be a single bee
100: 0 # All beemizer replacements are traps
### Shop Settings ###
shop_item_slots: # Maximum amount of shop slots to be filled with regular item pool items (such as Moon Pearl)
0: 50
@@ -676,657 +456,6 @@ A Link to the Past:
vanilla: 0 # Swords are placed in vanilla locations in your own game (Uncle, Pyramid Fairy, Smiths, Pedestal)
swordless: 0 # swordless mode
Ocarina of Time:
logic_rules: # Set the logic used for the generator.
glitchless: 50
glitched: 0
no_logic: 0
logic_no_night_tokens_without_suns_song: # Nighttime skulltulas will logically require Sun's Song.
false: 50
true: 0
open_forest: # Set the state of Kokiri Forest and the path to Deku Tree.
open: 50
closed_deku: 0
closed: 0
open_kakariko: # Set the state of the Kakariko Village gate.
open: 50
zelda: 0
closed: 0
open_door_of_time: # Open the Door of Time by default, without the Song of Time.
false: 0
true: 50
zora_fountain: # Set the state of King Zora, blocking the way to Zora's Fountain.
open: 0
adult: 0
closed: 50
gerudo_fortress: # Set the requirements for access to Gerudo Fortress.
normal: 0
fast: 50
open: 0
bridge: # Set the requirements for the Rainbow Bridge.
open: 0
vanilla: 0
stones: 0
medallions: 50
dungeons: 0
tokens: 0
trials: # Set the number of required trials in Ganon's Castle.
# you can add additional values between minimum and maximum
0: 0 # minimum value
6: 0 # maximum value
random: 50
random-low: 0
random-high: 0
starting_age: # Choose which age Link will start as.
child: 50
adult: 0
triforce_hunt: # Gather pieces of the Triforce scattered around the world to complete the game.
false: 50
true: 0
triforce_goal: # Number of Triforce pieces required to complete the game.
# 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
bridge_stones: # Set the number of Spiritual Stones required for the rainbow bridge.
# you can add additional values between minimum and maximum
0: 0 # minimum value
3: 0 # maximum value
random: 50
random-low: 0
random-high: 0
bridge_medallions: # Set the number of medallions required for the rainbow bridge.
# you can add additional values between minimum and maximum
0: 0 # minimum value
6: 0 # maximum value
random: 50
random-low: 0
random-high: 0
bridge_rewards: # Set the number of dungeon rewards required for the rainbow bridge.
# you can add additional values between minimum and maximum
0: 0 # minimum value
9: 0 # maximum value
random: 50
random-low: 0
random-high: 0
bridge_tokens: # Set the number of Gold Skulltula Tokens required for the rainbow bridge.
# you can add additional values between minimum and maximum
0: 0 # minimum value
100: 0 # maximum value
random: 50
random-low: 0
random-high: 0
shuffle_mapcompass: # Control where to shuffle dungeon maps and compasses.
remove: 0
startwith: 50
vanilla: 0
dungeon: 0
overworld: 0
any_dungeon: 0
keysanity: 0
shuffle_smallkeys: # Control where to shuffle dungeon small keys.
remove: 0
vanilla: 0
dungeon: 50
overworld: 0
any_dungeon: 0
keysanity: 0
shuffle_fortresskeys: # Control where to shuffle the Gerudo Fortress small keys.
vanilla: 50
overworld: 0
any_dungeon: 0
keysanity: 0
shuffle_bosskeys: # Control where to shuffle boss keys, except the Ganon's Castle Boss Key.
remove: 0
vanilla: 0
dungeon: 50
overworld: 0
any_dungeon: 0
keysanity: 0
shuffle_ganon_bosskey: # Control where to shuffle the Ganon's Castle Boss Key.
remove: 50
vanilla: 0
dungeon: 0
overworld: 0
any_dungeon: 0
keysanity: 0
on_lacs: 0
enhance_map_compass: # Map tells if a dungeon is vanilla or MQ. Compass tells what the dungeon reward is.
false: 50
true: 0
lacs_condition: # Set the requirements for the Light Arrow Cutscene in the Temple of Time.
vanilla: 50
stones: 0
medallions: 0
dungeons: 0
tokens: 0
lacs_stones: # Set the number of Spiritual Stones required for LACS.
# you can add additional values between minimum and maximum
0: 0 # minimum value
3: 0 # maximum value
random: 50
random-low: 0
random-high: 0
lacs_medallions: # Set the number of medallions required for LACS.
# you can add additional values between minimum and maximum
0: 0 # minimum value
6: 0 # maximum value
random: 50
random-low: 0
random-high: 0
lacs_rewards: # Set the number of dungeon rewards required for LACS.
# you can add additional values between minimum and maximum
0: 0 # minimum value
9: 0 # maximum value
random: 50
random-low: 0
random-high: 0
lacs_tokens: # Set the number of Gold Skulltula Tokens required for LACS.
# you can add additional values between minimum and maximum
0: 0 # minimum value
100: 0 # maximum value
random: 50
random-low: 0
random-high: 0
shuffle_song_items: # Set where songs can appear.
song: 50
dungeon: 0
any: 0
shopsanity: # Randomizes shop contents. "fixed_number" randomizes a specific number of items per shop; "random_number" randomizes the value for each shop.
off: 50
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
overworld: 0
all: 0
shuffle_scrubs: # Shuffle the items sold by Business Scrubs, and set the prices.
off: 50
low: 0
affordable: 0
expensive: 0
shuffle_cows: # Cows give items when Epona's Song is played.
false: 50
true: 0
shuffle_kokiri_sword: # Shuffle Kokiri Sword into the item pool.
false: 50
true: 0
shuffle_ocarinas: # Shuffle the Fairy Ocarina and Ocarina of Time into the item pool.
false: 50
true: 0
shuffle_weird_egg: # Shuffle the Weird Egg from Malon at Hyrule Castle.
false: 50
true: 0
shuffle_gerudo_card: # Shuffle the Gerudo Membership Card into the item pool.
false: 50
true: 0
shuffle_beans: # Adds a pack of 10 beans to the item pool and changes the bean salesman to sell one item for 60 rupees.
false: 50
true: 0
shuffle_medigoron_carpet_salesman: # Shuffle the items sold by Medigoron and the Haunted Wasteland Carpet Salesman.
false: 50
true: 0
skip_child_zelda: # Game starts with Zelda's Letter, the item at Zelda's Lullaby, and the relevant events already completed.
false: 50
true: 0
no_escape_sequence: # Skips the tower collapse sequence between the Ganondorf and Ganon fights.
false: 0
true: 50
no_guard_stealth: # The crawlspace into Hyrule Castle skips straight to Zelda.
false: 0
true: 50
no_epona_race: # Epona can always be summoned with Epona's Song.
false: 0
true: 50
skip_some_minigame_phases: # Dampe Race and Horseback Archery give both rewards if the second condition is met on the first attempt.
false: 0
true: 50
complete_mask_quest: # All masks are immediately available to borrow from the Happy Mask Shop.
false: 50
true: 0
useful_cutscenes: # Reenables the Poe cutscene in Forest Temple, Darunia in Fire Temple, and Twinrova introduction. Mostly useful for glitched.
false: 50
true: 0
fast_chests: # All chest animations are fast. If disabled, major items have a slow animation.
false: 0
true: 50
free_scarecrow: # Pulling out the ocarina near a scarecrow spot spawns Pierre without needing the song.
false: 50
true: 0
fast_bunny_hood: # Bunny Hood lets you move 1.5x faster like in Majora's Mask.
false: 50
true: 0
chicken_count: # Controls the number of Cuccos for Anju to give an item as child.
# you can add additional values between minimum and maximum
0: 0 # minimum value
7: 0 # maximum value
random: 50
random-low: 0
random-high: 0
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
agony: 0
always: 50
false: 0
hint_dist: # Choose the hint distribution to use. Affects the frequency of strong hints, which items are always hinted, etc.
balanced: 50
ddr: 0
league: 0
mw2: 0
scrubs: 0
strong: 0
tournament: 0
useless: 0
very_strong: 0
text_shuffle: # Randomizes text in the game for comedic effect.
none: 50
except_hints: 0
complete: 0
damage_multiplier: # Controls the amount of damage Link takes.
half: 0
normal: 50
double: 0
quadruple: 0
ohko: 0
no_collectible_hearts: # Hearts will not drop from enemies or objects.
false: 50
true: 0
starting_tod: # Change the starting time of day.
default: 50
sunrise: 0
morning: 0
noon: 0
afternoon: 0
sunset: 0
evening: 0
midnight: 0
witching_hour: 0
start_with_consumables: # Start the game with full Deku Sticks and Deku Nuts.
false: 50
true: 0
start_with_rupees: # Start with a full wallet. Wallet upgrades will also fill your wallet.
false: 50
true: 0
item_pool_value: # Changes the number of items available in the game.
plentiful: 0
balanced: 50
scarce: 0
minimal: 0
junk_ice_traps: # Adds ice traps to the item pool.
off: 0
normal: 50
extra: 0
mayhem: 0
onslaught: 0
ice_trap_appearance: # Changes the appearance of ice traps as freestanding items.
major_only: 50
junk_only: 0
anything: 0
logic_earliest_adult_trade: # Earliest item that can appear in the adult trade sequence.
pocket_egg: 0
pocket_cucco: 0
cojiro: 0
odd_mushroom: 0
poachers_saw: 0
broken_sword: 0
prescription: 50
eyeball_frog: 0
eyedrops: 0
claim_check: 0
logic_latest_adult_trade: # Latest item that can appear in the adult trade sequence.
pocket_egg: 0
pocket_cucco: 0
cojiro: 0
odd_mushroom: 0
poachers_saw: 0
broken_sword: 0
prescription: 0
eyeball_frog: 0
eyedrops: 0
claim_check: 50
default_targeting: # Default targeting option.
hold: 50
switch: 0
display_dpad: # Show dpad icon on HUD for quick actions (ocarina, hover boots, iron boots).
false: 0
true: 50
correct_model_colors: # Makes in-game models match their HUD element colors.
false: 0
true: 50
background_music: # Randomize or disable background music.
normal: 50
off: 0
randomized: 0
fanfares: # Randomize or disable item fanfares.
normal: 50
off: 0
randomized: 0
ocarina_fanfares: # Enable ocarina songs as fanfares. These are longer than usual fanfares. Does nothing without fanfares randomized.
false: 50
true: 0
kokiri_color: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code.
random_choice: 0
completely_random: 0
kokiri_green: 50
goron_red: 0
zora_blue: 0
black: 0
white: 0
azure_blue: 0
vivid_cyan: 0
light_red: 0
fuchsia: 0
purple: 0
majora_purple: 0
twitch_purple: 0
purple_heart: 0
persian_rose: 0
dirty_yellow: 0
blush_pink: 0
hot_pink: 0
rose_pink: 0
orange: 0
gray: 0
gold: 0
silver: 0
beige: 0
teal: 0
blood_red: 0
blood_orange: 0
royal_blue: 0
sonic_blue: 0
nes_green: 0
dark_green: 0
lumen: 0
goron_color: # Choose a color. Uses the same options as "kokiri_color".
random_choice: 0
completely_random: 0
goron_red: 50
zora_color: # Choose a color. Uses the same options as "kokiri_color".
random_choice: 0
completely_random: 0
zora_blue: 50
silver_gauntlets_color: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code.
random_choice: 0
completely_random: 0
silver: 50
gold: 0
black: 0
green: 0
blue: 0
bronze: 0
red: 0
sky_blue: 0
pink: 0
magenta: 0
orange: 0
lime: 0
purple: 0
golden_gauntlets_color: # Choose a color. Uses the same options as "silver_gauntlets_color".
random_choice: 0
completely_random: 0
gold: 50
mirror_shield_frame_color: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code.
random_choice: 0
completely_random: 0
red: 50
green: 0
blue: 0
yellow: 0
cyan: 0
magenta: 0
orange: 0
gold: 0
purple: 0
pink: 0
navi_color_default_inner: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code.
random_choice: 0
completely_random: 0
rainbow: 0
gold: 0
white: 50
green: 0
light_blue: 0
yellow: 0
red: 0
magenta: 0
black: 0
tatl: 0
tael: 0
fi: 0
ciela: 0
epona: 0
ezlo: 0
king_of_red_lions: 0
linebeck: 0
loftwing: 0
midna: 0
phantom_zelda: 0
navi_color_default_outer: # Choose a color. Uses the same options as "navi_color_default_inner" plus "match_inner".
random_choice: 0
completely_random: 0
match_inner: 50
navi_color_enemy_inner: # Choose a color. Uses the same options as "navi_color_default_inner".
random_choice: 0
completely_random: 0
yellow: 50
navi_color_enemy_outer: # Choose a color. Uses the same options as "navi_color_default_inner" plus "match_inner".
random_choice: 0
completely_random: 0
match_inner: 50
navi_color_npc_inner: # Choose a color. Uses the same options as "navi_color_default_inner".
random_choice: 0
completely_random: 0
light_blue: 50
navi_color_npc_outer: # Choose a color. Uses the same options as "navi_color_default_inner" plus "match_inner".
random_choice: 0
completely_random: 0
match_inner: 50
navi_color_prop_inner: # Choose a color. Uses the same options as "navi_color_default_inner".
random_choice: 0
completely_random: 0
green: 50
navi_color_prop_outer: # Choose a color. Uses the same options as "navi_color_default_inner" plus "match_inner".
random_choice: 0
completely_random: 0
match_inner: 50
sword_trail_duration: # Set the duration for sword trails.
# you can add additional values between minimum and maximum
4: 50 # minimum value
20: 0 # maximum value
random: 0
random-low: 0
random-high: 0
sword_trail_color_inner: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code.
random_choice: 0
completely_random: 0
rainbow: 0
white: 50
red: 0
green: 0
blue: 0
cyan: 0
magenta: 0
orange: 0
gold: 0
purple: 0
pink: 0
sword_trail_color_outer: # Uses the same options as "sword_trail_color_inner" plus "match_inner".
random_choice: 0
completely_random: 0
match_inner: 50
bombchu_trail_color_inner: # Uses the same options as "sword_trail_color_inner".
random_choice: 0
completely_random: 0
red: 50
bombchu_trail_color_outer: # Uses the same options as "sword_trail_color_inner" plus "match_inner".
random_choice: 0
completely_random: 0
match_inner: 50
boomerang_trail_color_inner: # Uses the same options as "sword_trail_color_inner".
random_choice: 0
completely_random: 0
yellow: 50
boomerang_trail_color_outer: # Uses the same options as "sword_trail_color_inner" plus "match_inner".
random_choice: 0
completely_random: 0
match_inner: 50
heart_color: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code.
random_choice: 0
completely_random: 0
red: 50
green: 0
blue: 0
yellow: 0
magic_color: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code.
random_choice: 0
completely_random: 0
green: 50
red: 0
blue: 0
purple: 0
pink: 0
yellow: 0
white: 0
a_button_color: # Choose a color. "random_choice" selects a random option. "completely_random" generates a random hex code.
random_choice: 0
completely_random: 0
n64_blue: 50
n64_green: 0
n64_red: 0
gamecube_green: 0
gamecube_red: 0
gamecube_grey: 0
yellow: 0
black: 0
white: 0
magenta: 0
ruby: 0
sapphire: 0
lime: 0
cyan: 0
purple: 0
orange: 0
b_button_color: # Choose a color. Uses the same options as "a_button_color".
random_choice: 0
completely_random: 0
n64_green: 50
c_button_color: # Choose a color. Uses the same options as "a_button_color".
random_choice: 0
completely_random: 0
yellow: 50
start_button_color: # Choose a color. Uses the same options as "a_button_color".
random_choice: 0
completely_random: 0
n64_red: 50
sfx_navi_overworld: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound.
default: 50
completely_random: 0
random_ear_safe: 0
random_choice: 0
none: 0
sfx_navi_enemy: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound.
default: 50
completely_random: 0
random_ear_safe: 0
random_choice: 0
none: 0
sfx_low_hp: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound.
default: 50
completely_random: 0
random_ear_safe: 0
random_choice: 0
none: 0
sfx_menu_cursor: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound.
default: 50
completely_random: 0
random_ear_safe: 0
random_choice: 0
none: 0
sfx_menu_select: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound.
default: 50
completely_random: 0
random_ear_safe: 0
random_choice: 0
none: 0
sfx_nightfall: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound.
default: 50
completely_random: 0
random_ear_safe: 0
random_choice: 0
none: 0
sfx_horse_neigh: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound.
default: 50
completely_random: 0
random_ear_safe: 0
random_choice: 0
none: 0
sfx_hover_boots: # Choose a sound effect. "random_choice" selects a random option. "random_ear_safe" selects a random safe option. "completely_random" selects any random sound.
default: 50
completely_random: 0
random_ear_safe: 0
random_choice: 0
sfx_ocarina: # Change the sound of the ocarina.
ocarina: 50
malon: 0
whistle: 0
harp: 0
grind_organ: 0
flute: 0
# 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
mode:
- inverted # Never play inverted seeds
retro:
- on # Never play retro seeds
swordless:
- on # Never play a swordless seed
linked_options:
- name: crosskeys
options: # These overwrite earlier options if the percentage chance triggers
@@ -1408,4 +537,4 @@ triggers:
percentage: 0 # AND has a 0 percent chance (meaning this is default disabled, just to show how it works)
options: # then inserts these options
A Link to the Past:
swordless: off
swordless: off

View File

@@ -1,8 +1,7 @@
colorama>=0.4.4
websockets>=10.0
PyYAML>=5.4.1
websockets>=10.1
PyYAML>=6.0
fuzzywuzzy>=0.18.0
prompt_toolkit>=3.0.20
appdirs>=1.4.4
jinja2>=3.0.1
jinja2>=3.0.3
schema>=0.7.4
kivy>=2.0.0

View File

@@ -7,8 +7,6 @@ import cx_Freeze
from kivy_deps import sdl2, glew
from Utils import version_tuple
is_64bits = sys.maxsize > 2 ** 32
arch_folder = "exe.{platform}-{version}".format(platform=sysconfig.get_platform(),
version=sysconfig.get_python_version())
buildfolder = Path("build", arch_folder)
@@ -75,13 +73,17 @@ scripts = {
"MultiServer.py": ("ArchipelagoServer", False, icon),
"Generate.py": ("ArchipelagoGenerate", False, icon),
"CommonClient.py": ("ArchipelagoTextClient", True, icon),
# LttP
"LttPClient.py": ("ArchipelagoLttPClient", True, icon),
# SNI
"SNIClient.py": ("ArchipelagoSNIClient", True, icon),
"LttPAdjuster.py": ("ArchipelagoLttPAdjuster", True, icon),
# Factorio
"FactorioClient.py": ("ArchipelagoFactorioClient", True, icon),
# Minecraft
"MinecraftClient.py": ("ArchipelagoMinecraftClient", False, mcicon),
# Ocarina of Time
"OoTAdjuster.py": ("ArchipelagoOoTAdjuster", True, icon),
# FF1
"FF1Client.py": ("ArchipelagoFF1Client", True, icon),
}
exes = []
@@ -139,13 +141,21 @@ for folder in sdl2.dep_bins + glew.dep_bins:
shutil.copytree(folder, libfolder, dirs_exist_ok=True)
print('copying', folder, '->', libfolder)
extra_data = ["LICENSE", "data", "EnemizerCLI", "host.yaml", "SNI", "meta.yaml"]
extra_data = ["LICENSE", "data", "EnemizerCLI", "host.yaml", "SNI"]
for data in extra_data:
installfile(Path(data))
os.makedirs(buildfolder / "Players", exist_ok=True)
shutil.copyfile("playerSettings.yaml", buildfolder / "Players" / "weightedSettings.yaml")
os.makedirs(buildfolder / "Players" / "Templates", exist_ok=True)
from WebHostLib.options import create
create()
from worlds.AutoWorld import AutoWorldRegister
for worldname, worldtype in AutoWorldRegister.world_types.items():
if not worldtype.hidden:
file_name = worldname+".yaml"
shutil.copyfile(os.path.join("WebHostLib", "static", "generated", "configs", file_name),
buildfolder / "Players" / "Templates" / file_name)
shutil.copyfile("meta.yaml", buildfolder / "Players" / "Templates" / "meta.yaml")
try:
from maseya import z3pr

View File

@@ -1,30 +0,0 @@
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))

12
test/general/TestItems.py Normal file
View File

@@ -0,0 +1,12 @@
import unittest
from worlds.AutoWorld import AutoWorldRegister
class TestBase(unittest.TestCase):
def testCreateItem(self):
for game_name, world_type in AutoWorldRegister.world_types.items():
proxy_world = world_type(None, 0) # this is identical to MultiServer.py creating worlds
for item_name in world_type.item_name_to_id:
with self.subTest("Create Item", item_name=item_name, game_name=game_name):
item = proxy_world.create_item(item_name)
self.assertEqual(item.name, item_name)

View File

@@ -0,0 +1,32 @@
import unittest
from BaseClasses import CollectionState
from worlds.AutoWorld import AutoWorldRegister
from . import setup_default_world
class TestBase(unittest.TestCase):
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 = setup_default_world(world_type)
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))
def testEmptyStateCanReachSomething(self):
for game_name, world_type in AutoWorldRegister.world_types.items():
if game_name != "Archipelago":
with self.subTest("Game", game=game_name):
world = setup_default_world(world_type)
state = CollectionState(world)
locations = set()
for location in world.get_locations():
if location.can_reach(state):
locations.add(location)
self.assertGreater(len(locations), 0,
msg="Need to be able to reach at least one location to get started.")

View File

@@ -1,12 +1,8 @@
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():

21
test/general/__init__.py Normal file
View File

@@ -0,0 +1,21 @@
from argparse import Namespace
from BaseClasses import MultiWorld
from worlds.AutoWorld import call_all
gen_steps = ["generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill"]
def setup_default_world(world_type):
world = MultiWorld(1)
world.game[1] = world_type.game
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 gen_steps:
call_all(world, step)
return world

View File

@@ -613,19 +613,24 @@ class TestAdvancements(TestMinecraft):
["You Need a Mint", False, [], ['Progressive Resource Crafting']],
["You Need a Mint", False, [], ['Flint and Steel']],
["You Need a Mint", False, [], ['Progressive Tools']],
["You Need a Mint", False, ['Progressive Weapons'], ['Progressive Weapons', 'Progressive Weapons']],
["You Need a Mint", False, [], ['Progressive Armor']],
["You Need a Mint", False, [], ['Progressive Weapons']],
["You Need a Mint", False, [], ['Progressive Armor', 'Shield']],
["You Need a Mint", False, [], ['Brewing']],
["You Need a Mint", False, [], ['Bottles']],
["You Need a Mint", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
["You Need a Mint", False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']],
["You Need a Mint", False, [], ['Archery']],
["You Need a Mint", False, [], ['Bottles']],
["You Need a Mint", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket',
'Progressive Weapons', 'Progressive Weapons', 'Archery', 'Progressive Armor',
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Bottles']],
["You Need a Mint", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket',
'Progressive Weapons', 'Progressive Armor', 'Brewing',
'3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Bottles']],
["You Need a Mint", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools',
'Progressive Weapons', 'Progressive Weapons', 'Archery', 'Progressive Armor',
'Brewing', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Bottles']],
'Progressive Weapons', 'Progressive Armor', 'Brewing',
'3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Bottles']],
["You Need a Mint", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket',
'Progressive Weapons', 'Shield', 'Brewing',
'3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Bottles']],
["You Need a Mint", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools',
'Progressive Weapons', 'Shield', 'Brewing',
'3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Bottles']],
])
def test_42047(self):
@@ -954,7 +959,11 @@ class TestAdvancements(TestMinecraft):
def test_42072(self):
self.run_location_tests([
["A Throwaway Joke", True, []],
["A Throwaway Joke", False, []],
["A Throwaway Joke", False, [], ['Progressive Weapons']],
["A Throwaway Joke", False, [], ['Campfire', 'Progressive Resource Crafting']],
["A Throwaway Joke", True, ['Progressive Weapons', 'Campfire']],
["A Throwaway Joke", True, ['Progressive Weapons', 'Progressive Resource Crafting']],
])
def test_42073(self):
@@ -1132,9 +1141,138 @@ class TestAdvancements(TestMinecraft):
def test_42091(self):
self.run_location_tests([
["Overpowered", False, []],
["Overpowered", False, ['Progressive Resource Crafting'], ['Progressive Resource Crafting']],
["Overpowered", False, ['Progressive Tools'], ['Flint and Steel', 'Progressive Tools', 'Progressive Tools']],
["Overpowered", True, ['Progressive Resource Crafting', 'Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools']],
["Overpowered", True, ['Progressive Resource Crafting', 'Progressive Resource Crafting', 'Progressive Tools', 'Bucket', 'Flint and Steel']],
["Overpowered", False, [], ['Progressive Resource Crafting']],
["Overpowered", False, [], ['Flint and Steel']],
["Overpowered", False, ['Progressive Tools', 'Progressive Tools', 'Bucket', 'Flint and Steel']],
["Overpowered", False, [], ['Progressive Weapons']],
["Overpowered", False, [], ['Progressive Armor', 'Shield']],
["Overpowered", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Weapons', 'Progressive Armor']],
["Overpowered", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons', 'Progressive Armor']],
["Overpowered", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Weapons', 'Shield']],
["Overpowered", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons', 'Shield']],
])
def test_42092(self):
self.run_location_tests([
["Wax On", False, []],
["Wax On", False, [], ["Progressive Tools"]],
["Wax On", False, [], ["Campfire"]],
["Wax On", False, ["Progressive Resource Crafting"], ["Progressive Resource Crafting"]],
["Wax On", True, ["Progressive Tools", "Progressive Resource Crafting", "Progressive Resource Crafting", "Campfire"]],
])
def test_42093(self):
self.run_location_tests([
["Wax Off", False, []],
["Wax Off", False, [], ["Progressive Tools"]],
["Wax Off", False, [], ["Campfire"]],
["Wax Off", False, ["Progressive Resource Crafting"], ["Progressive Resource Crafting"]],
["Wax Off", True, ["Progressive Tools", "Progressive Resource Crafting", "Progressive Resource Crafting", "Campfire"]],
])
def test_42094(self):
self.run_location_tests([
["The Cutest Predator", False, []],
["The Cutest Predator", False, [], ["Progressive Tools"]],
["The Cutest Predator", False, [], ["Progressive Resource Crafting"]],
["The Cutest Predator", False, [], ["Bucket"]],
["The Cutest Predator", True, ["Progressive Tools", "Progressive Resource Crafting", "Bucket"]],
])
def test_42095(self):
self.run_location_tests([
["The Healing Power of Friendship", False, []],
["The Healing Power of Friendship", False, [], ["Progressive Tools"]],
["The Healing Power of Friendship", False, [], ["Progressive Resource Crafting"]],
["The Healing Power of Friendship", False, [], ["Bucket"]],
["The Healing Power of Friendship", True, ["Progressive Tools", "Progressive Resource Crafting", "Bucket"]],
])
def test_42096(self):
self.run_location_tests([
["Is It a Bird?", False, []],
["Is It a Bird?", False, [], ["Progressive Weapons"]],
["Is It a Bird?", False, [], ["Progressive Tools"]],
["Is It a Bird?", False, [], ["Progressive Resource Crafting"]],
["Is It a Bird?", False, [], ["Spyglass"]],
["Is It a Bird?", True, ["Progressive Weapons", "Progressive Tools", "Progressive Resource Crafting", "Spyglass"]],
])
def test_42097(self):
self.run_location_tests([
["Is It a Balloon?", False, []],
["Is It a Balloon?", False, [], ['Progressive Resource Crafting']],
["Is It a Balloon?", False, [], ['Flint and Steel']],
["Is It a Balloon?", False, [], ['Progressive Tools']],
["Is It a Balloon?", False, [], ['Progressive Weapons']],
["Is It a Balloon?", False, [], ['Spyglass']],
["Is It a Balloon?", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
["Is It a Balloon?", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket', 'Progressive Weapons', 'Spyglass']],
["Is It a Balloon?", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools', 'Progressive Weapons', 'Spyglass']],
])
def test_42098(self):
self.run_location_tests([
["Is It a Plane?", False, []],
["Is It a Plane?", False, [], ['Progressive Resource Crafting']],
["Is It a Plane?", False, [], ['Flint and Steel']],
["Is It a Plane?", False, [], ['Progressive Tools']],
["Is It a Plane?", False, [], ['Progressive Weapons']],
["Is It a Plane?", False, [], ['Progressive Armor', 'Shield']],
["Is It a Plane?", False, [], ['Brewing']],
["Is It a Plane?", False, ['Progressive Tools', 'Progressive Tools'], ['Bucket', 'Progressive Tools']],
["Is It a Plane?", False, ['3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls'], ['3 Ender Pearls']],
["Is It a Plane?", False, [], ['Spyglass']],
["Is It a Plane?", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket',
'Progressive Weapons', 'Progressive Armor', 'Brewing',
'3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Spyglass']],
["Is It a Plane?", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools',
'Progressive Weapons', 'Progressive Armor', 'Brewing',
'3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Spyglass']],
["Is It a Plane?", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Bucket',
'Progressive Weapons', 'Shield', 'Brewing',
'3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Spyglass']],
["Is It a Plane?", True, ['Progressive Resource Crafting', 'Progressive Tools', 'Flint and Steel', 'Progressive Tools', 'Progressive Tools',
'Progressive Weapons', 'Shield', 'Brewing',
'3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', '3 Ender Pearls', 'Spyglass']],
])
def test_42099(self):
self.run_location_tests([
["Surge Protector", False, []],
["Surge Protector", False, [], ['Channeling Book']],
["Surge Protector", False, ['Progressive Resource Crafting'], ['Progressive Resource Crafting']],
["Surge Protector", False, [], ['Enchanting']],
["Surge Protector", False, [], ['Progressive Tools']],
["Surge Protector", False, [], ['Progressive Weapons']],
["Surge Protector", True, ['Progressive Weapons', 'Progressive Tools', 'Progressive Tools', 'Progressive Tools',
'Enchanting', 'Progressive Resource Crafting', 'Progressive Resource Crafting', 'Channeling Book']],
])
def test_42100(self):
self.run_location_tests([
["Light as a Rabbit", False, []],
["Light as a Rabbit", False, [], ["Progressive Weapons"]],
["Light as a Rabbit", False, [], ["Progressive Tools"]],
["Light as a Rabbit", False, [], ["Progressive Resource Crafting"]],
["Light as a Rabbit", False, [], ["Bucket"]],
["Light as a Rabbit", True, ["Progressive Weapons", "Progressive Tools", "Progressive Resource Crafting", "Bucket"]],
])
def test_42101(self):
self.run_location_tests([
["Glow and Behold!", False, []],
["Glow and Behold!", False, [], ["Progressive Weapons"]],
["Glow and Behold!", False, [], ["Progressive Resource Crafting", "Campfire"]],
["Glow and Behold!", True, ["Progressive Weapons", "Progressive Resource Crafting"]],
["Glow and Behold!", True, ["Progressive Weapons", "Campfire"]],
])
def test_42102(self):
self.run_location_tests([
["Whatever Floats Your Goat!", False, []],
["Whatever Floats Your Goat!", False, [], ["Progressive Weapons"]],
["Whatever Floats Your Goat!", False, [], ["Progressive Resource Crafting", "Campfire"]],
["Whatever Floats Your Goat!", True, ["Progressive Weapons", "Progressive Resource Crafting"]],
["Whatever Floats Your Goat!", True, ["Progressive Weapons", "Campfire"]],
])

View File

@@ -4,7 +4,7 @@ from BaseClasses import MultiWorld
from worlds import AutoWorld
from worlds.minecraft import MinecraftWorld
from worlds.minecraft.Items import MinecraftItem, item_table
from worlds.minecraft.Options import AdvancementGoal, CombatDifficulty, BeeTraps
from worlds.minecraft.Options import *
from Options import Toggle, Range
# Converts the name of an item into an item object
@@ -30,16 +30,17 @@ class TestMinecraft(TestBase):
self.world = MultiWorld(1)
self.world.game[1] = "Minecraft"
self.world.worlds[1] = MinecraftWorld(self.world, 1)
exclusion_pools = ['hard', 'insane', 'postgame']
exclusion_pools = ['hard', 'unreasonable', 'postgame']
for pool in exclusion_pools:
setattr(self.world, f"include_{pool}_advancements", [False, False])
setattr(self.world, f"include_{pool}_advancements", {1: False})
setattr(self.world, "advancement_goal", {1: AdvancementGoal(30)})
setattr(self.world, "shuffle_structures", {1: Toggle(False)})
setattr(self.world, "combat_difficulty", {1: CombatDifficulty(1)}) # normal
setattr(self.world, "egg_shards_required", {1: EggShardsRequired(0)})
setattr(self.world, "egg_shards_available", {1: EggShardsAvailable(0)})
setattr(self.world, "required_bosses", {1: BossGoal(1)}) # ender dragon
setattr(self.world, "shuffle_structures", {1: ShuffleStructures(False)})
setattr(self.world, "bee_traps", {1: BeeTraps(0)})
setattr(self.world, "combat_difficulty", {1: CombatDifficulty(1)}) # normal
setattr(self.world, "structure_compasses", {1: Toggle(False)})
setattr(self.world, "egg_shards_required", {1: Range(0)})
setattr(self.world, "egg_shards_available", {1: Range(0)})
AutoWorld.call_single(self.world, "create_regions", 1)
AutoWorld.call_single(self.world, "generate_basic", 1)
AutoWorld.call_single(self.world, "set_rules", 1)

View File

@@ -1,7 +1,8 @@
from __future__ import annotations
from typing import Dict, Set, Tuple, List, Optional
from typing import Dict, Set, Tuple, List, Optional, TextIO
from BaseClasses import MultiWorld, Item, CollectionState, Location
from Options import Option
class AutoWorldRegister(type):
@@ -67,7 +68,7 @@ class World(metaclass=AutoWorldRegister):
"""A World object encompasses a game's Items, Locations, Rules and additional data or functionality required.
A Game should have its own subclass of World in which it defines the required data structures."""
options: dict = {} # link your Options mapping
options: Dict[str, type(Option)] = {} # link your Options mapping
game: str # name the game
topology_present: bool = False # indicate if world type has any meaningful layout/pathing
all_names: Set[str] = frozenset() # gets automatically populated with all item, item group and location names
@@ -122,7 +123,7 @@ class World(metaclass=AutoWorldRegister):
self.player = player
# overridable methods that get called by Main.py, sorted by execution order
# can also be implemented as a classmethod and called "stage_<original_name",
# can also be implemented as a classmethod and called "stage_<original_name>",
# in that case the MultiWorld object is passed as an argument and it gets called once for the entire multiworld.
# An example of this can be found in alttp as stage_pre_fill
def generate_early(self):
@@ -144,6 +145,7 @@ class World(metaclass=AutoWorldRegister):
"""Optional method that is supposed to be used for special fill stages. This is run *after* plando."""
pass
@classmethod
def fill_hook(cls, progitempool: List[Item], nonexcludeditempool: List[Item],
localrestitempool: Dict[int, List[Item]], nonlocalrestitempool: Dict[int, List[Item]],
restitempool: List[Item], fill_locations: List[Location]):
@@ -168,11 +170,25 @@ class World(metaclass=AutoWorldRegister):
pass
def get_required_client_version(self) -> Tuple[int, int, int]:
return 0, 0, 3
return 0, 1, 6
# end of Main.py calls
# Spoiler writing is optional, these may not get called.
def write_spoiler_header(self, spoiler_handle: TextIO):
"""Write to the spoiler header. If individual it's right at the end of that player's options,
if as stage it's right under the common header before per-player options."""
pass
def collect_item(self, state: CollectionState, item: Item, remove=False) -> Optional[str]:
def write_spoiler(self, spoiler_handle: TextIO):
"""Write to the spoiler "middle", this is after the per-player options and before locations,
meant for useful or interesting info."""
pass
def write_spoiler_end(self, spoiler_handle: TextIO):
"""Write to the end of the spoiler"""
pass
# end of ordered Main.py calls
def collect_item(self, state: CollectionState, item: Item, remove: bool = False) -> Optional[str]:
"""Collect an item name into state. For speed reasons items that aren't logically useful get skipped.
Collect None to skip item.
:param remove: indicate if this is meant to remove from state instead of adding."""

View File

@@ -3,7 +3,8 @@ import os
__all__ = {"lookup_any_item_id_to_name",
"lookup_any_location_id_to_name",
"network_data_package"}
"network_data_package",
"AutoWorldRegister"}
# import all submodules to trigger AutoWorldRegister
for file in os.scandir(os.path.dirname(__file__)):
@@ -27,10 +28,6 @@ for world_name, world in AutoWorldRegister.world_types.items():
lookup_any_location_id_to_name.update(world.location_id_to_name)
network_data_package = {
# Remove with 0.2.0
"lookup_any_location_id_to_name": lookup_any_location_id_to_name, # legacy, to be removed
"lookup_any_item_id_to_name": lookup_any_item_id_to_name, # legacy, to be removed
"version": sum(world.data_version for world in AutoWorldRegister.world_types.values()),
"games": games,
}

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