Compare commits

..

108 Commits

Author SHA1 Message Date
NewSoupVi
1357e32afe Update AutoWorld.py 2024-10-26 05:41:53 +02:00
NewSoupVi
511bb212ed Core: The Item Links fix to end them all
This puts the bandaid that was holding Item Links together for years back on.

It's a bad solution
But it's what we had previously, and the change away from this is what broke them

So in the interest of 0.5.1 releasing this century, maybe we should just go with this.
2024-10-26 05:38:00 +02:00
black-sliver
77ee6d73bc Setup: more typing (#4089) 2024-10-25 08:51:53 +02:00
Scipio Wright
33daebef57 TUNIC: Add prog + useful to some items #4066 2024-10-23 02:30:31 +02:00
Nocallia
05ec14e23c HK: Replace "Hook" in PreciseMovement description to "Claw" (#4078) 2024-10-23 01:26:04 +02:00
Silvris
049a8780b5 Core: fix pickling plando connections (#4054)
* shift plando pickle hack

* Update Utils.py

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-10-22 21:08:25 +02:00
Spineraks
703e3393a6 Yacht Dice: Fix logic (again) so that score doesn't drop when receiving item (#4044)
* Add the yacht dice (from other git) world to the yacht dice fork

* Update .gitignore

* Removed zillion because it doesn't work

* Update .gitignore

* added zillion again...

* Now you can have 0 extra fragments

* Added alt categories, also options

* Added item categories

* Extra categories are now working! 🐶

* changed options and added exceptions

* Testing if I change the generate.py

* Revert "Testing if I change the generate.py"

This reverts commit 7c2b3df617.

* ignore gitignore

* Delete .gitignore

* Update .gitignore

* Update .gitignore

* Update logic, added multiplicative categories

* Changed difficulties

* Update offline mode so that it works again

* Adjusted difficulty

* New version of the apworld, with 1000 as final score, always

Will still need to check difficulty and weights of adding items.
Website is not ready yet, so this version is not usable yet :)

* Changed yaml and small bug fixes

Fix when goal and max are same
Options: changed chance to weight

* no changes, just whitespaces

* changed how logic works

Now you put an array of mults and the cpu gets a couple of tries

* Changed logic, tweaked a bit too

* Preparation for 2.0

* logic tweak

* Logic for alt categories properly now

* Update setup_en.md

* Update en_YachtDice.md

* Improve performance of add_distributions

* Formatting style

* restore gitignore to APMW

* Tweaked generation parameters and methods

* Version 2.0.3

manual input option
max score in logic always 2.0.3
faster gen

* Comments and editing

* Renamed setup guide

* Improved create_items code

* init of locations: remove self.event line

* Moved setting early items to generate_early

* Add my name to CODEOWNERS

* Added Yacht Dice to the readme in list of games

* Improve performance of Yacht Dice

* newline

* Improve typing

* This is actually just slower lol

* Update worlds/yachtdice/Items.py

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Update Options.py

* Styling

* finished text whichstory option

* removed roll and rollfragments; not used

* import; worlds not world :)

* Option groups!

* ruff styling, fix

* ruff format styling!

* styling and capitalization of options

* small comment

* Cleaned up the "state_is_a_list" a little bit

* RUFF 🐶

* Changed filling the itempool for efficiency

Now, we start with 17 extra items in the item pool, it's quite likely you need at least 17 items (~80%?).
And then afterwards, we delete items if we overshoot the target of 1000, and add items if we haven't reached an achievable score of 1000 yet. Also, no need to recompute the entire logic when adding points.

* 🐶

* Removed plando "fix"

* Changed indent of score multiplier

* faster location function

* Comments to docstrings

* fixed making location closest to goal_score be goal_score

* options format

* iterate keys and values of a dict together

* small optimization ListState

* faster collection of categories

* return arguments instead of making a list (will 🐶 later)

* Instead of turning it into a tuple, you can just make a tuple literal

* remove .keys()

* change .random and used enumerate

* some readability improvements

* Remove location "0", we don't use that one

* Remove lookup_id_to_name entirely

I for sure don't use it, and as far as I know it's not one of the mandatory functions for AP, these are item_name_to_id and location_name_to_id.

* .append instead of += for single items, percentile function changed

Also an extra comment for location ids.

* remove ) too many

* Removed sorted from category list

* Hash categories (which makes it slower :( )

Maybe I messed up or misunderstood...
I'll revert this right away since it is 2x slower, probably because of sorted instead of sort?

* Revert "Hash categories (which makes it slower :( )"

This reverts commit 34f2c1aed8.

* temporary push: 40% faster generation test

Small changes in logic make the generation 40% faster.
I'll have to think about how big the changes are. I suspect they are rather limited.
If this is the way to go, I'll remove the temp file and redo the YachtWeights file, I'll remove the functions there and just put the new weights here.

* Add Points item category

* Reverse changes of bad idea :)

* ruff 🐶

* Use numpy and pmf function to speed up gen

Numpy has a built-in way to sum probability mass functions (pmf).
This shaves of 60% of the generation time :D

* Revert "Use numpy and pmf function to speed up gen"

This reverts commit 9290191cb3.

* Step inbetween to change the weights

* Changed the weights to make it faster

135 -> 81 seconds on 100 random yamls

* Adjusted max_dist, split dice_simulation function

* Removed nonlocal and pass arguments instead

* Change "weight-lists" to Dict[str, float]

* Removed the return from ini_locations.

Also added explanations to cat_weights

* Choice options; dont'use .value (will ruff later)

* Only put important options in slotdata

* 🐶

* Add Dict import

* Split the cache per player, limit size to 400.

* 🐶

* added , because of style

* Update apworld version to 2.0.6

2.0.5 is the apworld I released on github to be tested
I never separately released 2.0.4.

* Multiple smaller code improvements

- changed names in YachtWeights so we don't need to translate them in Rules anymore
- we now remember which categories are present in the game, and also put this in slotdata. This we do because only one of two categories is present in a game. If for some reason both are present (plando/getitem/startinventory), we now know which category to ignore
-

* 🐶 ruff

* Mostly minimize_extra_items improvements

- Change logic, generation is now even faster (0.6s per default yaml).
- Made the option 'minimize_extra_items' do a lot more, hopefully this makes the impact of Yacht Dice a little bit less, if you want that. Here's what is also does now:
 - you start with 2 dice and 2 rolls
 - there will be less locations/items at the start of you game

* ruff 🐶

* Removed printing options

* Reworded some option descriptions

* Yacht Dice: setup: change release-link to latest

On the installation page, link to the latest release, instead of the page with all releases

* Several fixes and changes

-change apworld version
-Removed the extra roll (this was not intended)
-change extra_points_added to a mutable list to that it actually does something
-removed variables multipliers_added and items_added
-Rules, don't order by quantity, just by mean_score
-Changed the weights in general to make it faster

* 🐶

* Revert setup to what it was (latest, without S)

* remove temp weights file, shouldn't be here

* Made sure that there is not too many step score multipliers.

Too many step score multipliers lead to gen fails too, probably because you need many categories for them to actually help a lot. So it's hard to use them at the start of the game.

* add filler item name

* Textual fixes and changes

* Remove Victory item and use event instead.

* Revert "Remove Victory item and use event instead."

This reverts commit c2f7d674d3.

* Revert "Textual fixes and changes"

This reverts commit e9432f9245.

* Remove Victory item and make it an event instead

* Yacht Dice logic fix, no decreasing score when obtain item

take 2

* Logic fix: Revert max_tries and mults, change ordering

* Remove spaces :^)

* Updated weights that are stochastically ordered by dice/roll

In the trimming of the weights, sometimes it having 4 rolls would be better than having 5 rolls.
I did a check that this does not happen for any dice increment or roll increment

* Swap for-loops to increase performance

This method is faster if the first for-loop contains fewer items.
Since the function is called with, typically, `dist2` having less items, let's loop over `dist2` first. This makes the entire program 10% faster.

* Remove options with 0 chance from list

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-10-22 21:07:44 +02:00
black-sliver
f709d61d04 WebHost: optimize header-logo.svg (#4073)
* WebHost: reduce precision and optimize header-logo.svg

technically those files will not produce an identical output when rendered
however the difference is virtually impossible to see even when rendered to w=4096

* WebHost: keep original svg
2024-10-20 19:52:04 -04:00
black-sliver
c6d2971d67 WebHost: optimize WebHost theme PNGs (#4071)
using zopfli, saving 30%
2024-10-20 19:51:14 -04:00
Spineraks
af14045c3a Yacht Dice: Proguseful items: Dice and 100 Points #4070 2024-10-19 16:53:02 +02:00
Remy Jette
ede59ef5a1 WebHost: Fix NamedRange option dropdown being blank instead of custom when applying presets (#4063) 2024-10-17 18:40:46 +02:00
Bryce Wilson
63d471514f Pokemon Emerald: Add flag for shoal cave to bounces (#4021)
* Pokemon Emerald: Add shoal cave state to map updates

* Pokemon Emerald: Fix shoal cave flag wrong byte, delay bounce to end of map transition
2024-10-17 03:37:41 +02:00
palex00
ff297f2951 [Aquaria] Adds Poptracker Pack to the Aquaria Setup Guides (#4037)
* Adds Poptracker Pack to the Aquaria Setup Guides

* Updates French Update Guide

* Update worlds/aquaria/docs/setup_fr.md

Co-authored-by: Cipocreep <65617616+Cipocreep@users.noreply.github.com>

* Update worlds/aquaria/docs/setup_fr.md

Co-authored-by: Benny D <78334662+benny-dreamly@users.noreply.github.com>

* Update setup_fr.md

* Update setup_fr.md

---------

Co-authored-by: Cipocreep <65617616+Cipocreep@users.noreply.github.com>
Co-authored-by: Benny D <78334662+benny-dreamly@users.noreply.github.com>
2024-10-17 03:34:10 +02:00
Scipio Wright
a0f49dd7d9 Noita: Add the useful classification to important perks, making them progression + useful #4030 2024-10-17 03:31:53 +02:00
qwint
79cec89e24 Launcher: save default settings before opening file for users (#4042) 2024-10-17 00:27:50 +02:00
Ishigh1
2b0cab82fa CommonClient: Making local datapackage load correctly if it was overriden by a custom one (#3722)
* Added versions and checksums dict

* Added load of local datapackage

* Fixed typo
2024-10-17 00:14:27 +02:00
Fabian Dill
48822227b5 Test: option instances have to be pickleable (#4006) 2024-10-16 23:31:36 +02:00
Fabian Dill
375b5796d9 WebHost: noscript faq and glossary (#4061) 2024-10-16 23:28:42 +02:00
Jarno
c12ed316cf Timespinner: Make hidden options pickleables (#4050)
* Make timespinner hidden options pickleables

* Keep changes minimal

* Change line endings
2024-10-16 23:06:14 +02:00
Bryce Wilson
26577b16dc Pokemon Emerald: Fix opponent blacklist checking wrong option (#4058) 2024-10-15 23:28:36 +02:00
Louis M
af0b5f8cf2 Aquaria Fixing some bugs (#4057)
* Fixing some bugs

* Forgot about this one
2024-10-15 23:22:58 +02:00
Louis M
618564c60a Aquaria: Adding slot data for poptracker (#4056)
* Adds neccessary slot data for Aquaria

* Comma oops

---------

Co-authored-by: palex00 <32203971+palex00@users.noreply.github.com>
2024-10-14 18:53:20 +02:00
Seafo
f2ac937d1e Minecraft: Fix plando connections #4048
Plando connections was broken as a result of https://github.com/ArchipelagoMW/Archipelago/pull/3765
This fixes it.
2024-10-14 00:22:37 +02:00
Scipio Wright
d4d777b101 OoT: Add aliases for Progressive Hookshot (#4052)
* Add aliases for Progressive Hookshot

* Update worlds/oot/__init__.py

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-10-14 00:17:53 +02:00
Fabian Dill
b772d42df5 Core: turn MultiServer item_names and location_names into instance vars (#4053) 2024-10-14 00:15:53 +02:00
Exempt-Medic
e8f3aa96da Timespinner: Two typos #4051 2024-10-13 23:21:36 +02:00
gurglemurgle5
2d0bdebaa9 Docs: Add ConnectUpdate to the list of client packets in the network protocol documentation #4045 2024-10-13 02:01:28 +02:00
Fabian Dill
ef4d1e77e3 Core: make shlex split an attempt with regular split retry (#4046) 2024-10-11 23:24:42 +02:00
Aaron Wagener
f495bf7261 The Messenger: fix missing money wrench rule (#4041)
* The Messenger: fix missing money wrench rule

* add a unit test for money wrench
2024-10-11 03:05:21 +02:00
Exempt-Medic
2751ccdaab DS3: Make your own region cache (#4040)
* Make your own region cache

* Using a string
2024-10-11 03:02:31 +02:00
black-sliver
6287bc27a6 WebHost: Fix too-many-players error not showing (#4033)
* WebHost: fix 'too many players' error not showing

* WebHost, Tests: add basic tests for generate endpoint

* WebHost: hopefully make CodeQL happy with MAX_ROLL redirect
2024-10-05 18:14:22 +02:00
palex00
97f2c25924 [KH2] Adds more options to slot data #4031 2024-10-05 02:13:04 +02:00
Bryce Wilson
e5a0ef799f Pokemon Emerald: Update changelog (#4003) 2024-10-04 21:28:43 +02:00
Silvris
216e0603e1 KDL3: Fix webhost not giving a patch #4023 2024-10-04 21:27:23 +02:00
Fabian Dill
05a67386c6 Core: use shlex splitting instead of whitespace splitting for client and server commands (#4011) 2024-10-02 03:09:43 +02:00
NewSoupVi
0ec9039ca6 The Witness: Small code refactor (cast_not_none) (#3798)
* cast not none

* ruff

* Missed a spot
2024-10-02 00:02:17 +02:00
Aaron Wagener
f06f95d03d Core: move race_mode to read_data instead of stored_data (#4020)
* move race_mode to read_data

* add race_mode to docs
2024-10-01 23:55:34 +02:00
Mysteryem
5a853dfccd Tests: Fix indentation in TestTwoPlayerMulti (#4010)
The "filling multiworld" subtest was at the wrong indentation, so was
only running for the last world_type.

"games" has additionally been added to the subtest to help better
identify failures.

Now that the subtest is actually being run for each world type, this
adds about 20 seconds to the duration of the test on my machine.
2024-10-01 21:30:45 +02:00
Alex Nordstrom
23469fa5c3 LADX: ghost fills ammo to initial max (#4005)
* ghost fills ammo to max

* Revert "ghost fills ammo to max"

This reverts commit 68804fef14.

* fill to first max
2024-10-01 21:09:23 +02:00
Bryce Wilson
dc1da4e88b Pokemon Emerald: Another wonder trade fix (#4014)
* Pokemon Emerald: Another guarded write on wonder trades

* Pokemon Emerald: Reorder sending wonder trade and erasing data

In case the guarded write fails
2024-10-01 21:08:43 +02:00
Aaron Wagener
67f6b458d7 Core: add race mode to multidata and datastore (#4017)
* add race mode to multidata and datastore

* have commonclient check race mode on connect and add it to the tooltip ui
2024-10-01 21:08:13 +02:00
Bryce Wilson
8193fa12b2 BizHawkClient: Fix typing mistake (#3938) 2024-09-28 22:49:11 +02:00
Fabian Dill
de0c498470 Core: update World method comment (#3866) 2024-09-28 22:37:42 +02:00
qwint
7337309426 CommonClient: add more docstrings and comments #3821 2024-09-27 01:34:54 +02:00
Natalie Weizenbaum
3205e9b3a0 DS3: Update setup instructions (#3817)
* DS3: Point the DS3 client link to my GitHub

It's not clear if/when my PR will land for the upstream fork, or if we'll just start using my fork as the primary source of truth. For now, it's the only one with 3.0.0-compatible releases.

* DS3: Document Proton support

* DS3: Document another way to get a YAML template

* DS3: Don't say that the mod will force offline mode

ModEngine2 is *supposed to* do this, but in practice it does not

* Code review

* Update Linux instructions per user experiences
2024-09-27 01:31:50 +02:00
palex00
05439012dc Adjusts Whitespaces in the Plando Doc to be able to be copied directly (#3902)
* Update plando_en.md

* Also adjusts plando_connections indentation

* ughh
2024-09-27 01:30:23 +02:00
soopercool101
177c0fef52 SM64: Remove outdated information on save bugs from setup guide (#3879)
* Remove outdated information from SM64 setup guide

Recent build changes have made it so that old saves no longer remove logical gates or prevent Toads from granting stars, remove info highlighting these issues.

* Better line break location
2024-09-27 01:29:26 +02:00
BadMagic100
5c4e81d046 Hollow Knight: Clean outdated slot data code and comments #3988 2024-09-27 01:27:22 +02:00
agilbert1412
a2d585ba5c Stardew Valley: Add Cinder Shard resource pack (#4001)
* - Add Cinder Shard resource pack

* - Make it ginger island exclusive
2024-09-27 01:26:06 +02:00
Aaron Wagener
5ea55d77b0 The Messenger: add webhost auto connection steps to guide (#3904)
* The Messenger: add webhost auto connection steps to guide and fix doc spacing

* rever comments

* add notes about potential steam popup

* medic's feedback

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-09-27 01:25:41 +02:00
Ziktofel
ab8caea8be SC2: Fix item origins, so including/excluding NCO/BW/EXT items works properly (#3990) 2024-09-27 00:57:21 +02:00
Benny D
a043ed50a6 Timespinner: Fix Typo in Download Location #3997 2024-09-27 00:56:36 +02:00
qwint
e85a835b47 Core: use base collect/remove for item link groups (#3999)
* use base collect/remove for item link groups

* Update BaseClasses.py

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-09-27 00:02:10 +02:00
Felix R
9a9fea0ca2 bumpstik: add hazard bumpers to completion (#3991)
* bumpstik: add hazard bumpers to completion

* bumpstik: update to use has_all_counts for completion
as suggested by ScipioWright
2024-09-26 20:47:03 +02:00
NewSoupVi
e910a37273 Core: Put an assert for parent region in Entrance.can_reach just like the one in Location.can_reach (#3998)
* Core: Move connection.parent_region assert to can_reach

This is how it already works for locations and it feels more correct to me to check in the place where the crash would happen.

Also update location error to be a bit more verbose

* Bring back the other assert

* Update BaseClasses.py
2024-09-25 17:47:38 +02:00
Kory Dondzila
f06d4503d8 Adds link to other players' trackers in player hints. (#3569) 2024-09-23 17:21:03 -04:00
Mrks
8021b457b6 WebHost: Added Games Of A Seed To The User Content Page (#3585)
* Added contained games of a seed to the user content page as tooltip.

* Changed sort handling.

* Limited amount of shown games.

* Added missing dashes.

Co-authored-by: Kory Dondzila <kory.dondzila@atomicobject.com>

* Closing a-tags.

Co-authored-by: Kory Dondzila <kory.dondzila@atomicobject.com>

* Closing a-tags.

Co-authored-by: Kory Dondzila <kory.dondzila@atomicobject.com>

* Moved games list to table cell level.

Co-authored-by: Kory Dondzila <kory.dondzila@atomicobject.com>

* Moved games list to table cell level.

---------

Co-authored-by: Kory Dondzila <kory.dondzila@atomicobject.com>
2024-09-23 17:19:26 -04:00
agilbert1412
d43dc62485 Stardew Valley: Improve Junimo Kart Regions #3984 2024-09-23 00:14:04 +02:00
Silvris
f7ec3d7508 kvui: abstract away client tab additions #3950 2024-09-22 16:24:14 +02:00
CookieCat
99c02a3eb3 AHIT: Fix Death Wish option check typo (#3978)
* duh

* Fuck it

* Major fixes

* a

* b

* Even more fixes

* New option - NoFreeRoamFinale

* a

* Hat Logic Fix

* Just to be safe

* multiworld.random to world.random

* KeyError fix

* Update .gitignore

* Update __init__.py

* Zoinks Scoob

* ffs

* Ruh Roh Raggy, more r-r-r-random bugs!

* 0.9b - cleanup + expanded logic difficulty

* Update Rules.py

* Update Regions.py

* AttributeError fix

* 0.10b - New Options

* 1.0 Preparations

* Docs

* Docs 2

* Fixes

* Update __init__.py

* Fixes

* variable capture my beloathed

* Fixes

* a

* 10 Seconds logic fix

* 1.1

* 1.2

* a

* New client

* More client changes

* 1.3

* Final touch-ups for 1.3

* 1.3.1

* 1.3.3

* Zero Jumps gen error fix

* more fixes

* Formatting improvements

* typo

* Update __init__.py

* Revert "Update __init__.py"

This reverts commit e178a7c0a6.

* init

* Update to new options API

* Missed some

* Snatcher Coins fix

* Missed some more

* some slight touch ups

* rewind

* a

* fix things

* Revert "Merge branch 'main' of https://github.com/CookieCat45/Archipelago-ahit"

This reverts commit a2360fe197, reversing
changes made to b8948bc495.

* Update .gitignore

* 1.3.6

* Final touch-ups

* Fix client and leftover old options api

* Delete setup-ahitclient.py

* Update .gitignore

* old python version fix

* proper warnings for invalid act plandos

* Update worlds/ahit/docs/en_A Hat in Time.md

Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com>

* Update worlds/ahit/docs/setup_en.md

Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com>

* 120 char per line

* "settings" to "options"

* Update DeathWishRules.py

* Update worlds/ahit/docs/en_A Hat in Time.md

Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>

* No more loading the data package

* cleanup + act plando fixes

* almost forgot

* Update Rules.py

* a

* Update worlds/ahit/Options.py

Co-authored-by: Ixrec <ericrhitchcock@gmail.com>

* Options stuff

* oop

* no unnecessary type hints

* warn about depot download length in setup guide

* Update worlds/ahit/Options.py

Co-authored-by: Ixrec <ericrhitchcock@gmail.com>

* typo

Co-authored-by: Ixrec <ericrhitchcock@gmail.com>

* Update worlds/ahit/Rules.py

Co-authored-by: Ixrec <ericrhitchcock@gmail.com>

* review stuff

* More stuff from review

* comment

* 1.5 Update

* link fix?

* link fix 2

* Update setup_en.md

* Update setup_en.md

* Update setup_en.md

* Evil

* Good fucking lord

* Review stuff again + Logic fixes

* More review stuff

* Even more review stuff - we're almost done

* DW review stuff

* Finish up review stuff

* remove leftover stuff

* a

* assert item

* add A Hat in Time to readme/codeowners files

* Fix range options not being corrected properly

* 120 chars per line in docs

* Update worlds/ahit/Regions.py

Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>

* Update worlds/ahit/DeathWishLocations.py

Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>

* Remove some unnecessary option.class.value

* Remove data_version and more option.class.value

* Update worlds/ahit/Items.py

Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>

* Remove the rest of option.class.value

* Update worlds/ahit/DeathWishLocations.py

Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>

* review stuff

* Replace connect_regions with Region.connect

* review stuff

* Remove unnecessary Optional from LocData

* Remove HatType.NONE

* Update worlds/ahit/test/TestActs.py

Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>

* fix so default tests actually don't run

* Improve performance for death wish rules

* rename test file

* change test imports

* 1000 is probably unnecessary

* a

* change state.count to state.has

* stuff

* starting inventory hats fix

* shouldn't have done this lol

* make ship shape task goal equal to number of tasksanity checks if set to 0

* a

* change act shuffle starting acts + logic updates

* dumb

* option groups + lambda capture cringe + typo

* a

* b

* missing option in groups

* c

* Fix Your Contract Has Expired being placed on first level when it shouldn't

* yche fix

* formatting

* major logic bug fix for death wish

* Update Regions.py

* Add missing indirect connections

* Fix generation error from chapter 2 start with act shuffle off

* a

* Revert "a"

This reverts commit df58bbcd99.

* Revert "Fix generation error from chapter 2 start with act shuffle off"

This reverts commit 0f4d441824.

* Fix option typo

* I lied, it's actually two lines

---------

Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com>
Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
Co-authored-by: Ixrec <ericrhitchcock@gmail.com>
Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2024-09-22 16:22:11 +02:00
Scipio Wright
449782a4d8 TUNIC: Add forgotten Laurels rule for Beneath the Vault Boxes #3981 2024-09-22 16:21:10 +02:00
CookieCat
97ca2ad258 AHIT: Fix massive lag spikes in extremely large multiworlds, add extra security to prevent loading the wrong save file for a seed (#3718)
* duh

* Fuck it

* Major fixes

* a

* b

* Even more fixes

* New option - NoFreeRoamFinale

* a

* Hat Logic Fix

* Just to be safe

* multiworld.random to world.random

* KeyError fix

* Update .gitignore

* Update __init__.py

* Zoinks Scoob

* ffs

* Ruh Roh Raggy, more r-r-r-random bugs!

* 0.9b - cleanup + expanded logic difficulty

* Update Rules.py

* Update Regions.py

* AttributeError fix

* 0.10b - New Options

* 1.0 Preparations

* Docs

* Docs 2

* Fixes

* Update __init__.py

* Fixes

* variable capture my beloathed

* Fixes

* a

* 10 Seconds logic fix

* 1.1

* 1.2

* a

* New client

* More client changes

* 1.3

* Final touch-ups for 1.3

* 1.3.1

* 1.3.3

* Zero Jumps gen error fix

* more fixes

* Formatting improvements

* typo

* Update __init__.py

* Revert "Update __init__.py"

This reverts commit e178a7c0a6.

* init

* Update to new options API

* Missed some

* Snatcher Coins fix

* Missed some more

* some slight touch ups

* rewind

* a

* fix things

* Revert "Merge branch 'main' of https://github.com/CookieCat45/Archipelago-ahit"

This reverts commit a2360fe197, reversing
changes made to b8948bc495.

* Update .gitignore

* 1.3.6

* Final touch-ups

* Fix client and leftover old options api

* Delete setup-ahitclient.py

* Update .gitignore

* old python version fix

* proper warnings for invalid act plandos

* Update worlds/ahit/docs/en_A Hat in Time.md

Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com>

* Update worlds/ahit/docs/setup_en.md

Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com>

* 120 char per line

* "settings" to "options"

* Update DeathWishRules.py

* Update worlds/ahit/docs/en_A Hat in Time.md

Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>

* No more loading the data package

* cleanup + act plando fixes

* almost forgot

* Update Rules.py

* a

* Update worlds/ahit/Options.py

Co-authored-by: Ixrec <ericrhitchcock@gmail.com>

* Options stuff

* oop

* no unnecessary type hints

* warn about depot download length in setup guide

* Update worlds/ahit/Options.py

Co-authored-by: Ixrec <ericrhitchcock@gmail.com>

* typo

Co-authored-by: Ixrec <ericrhitchcock@gmail.com>

* Update worlds/ahit/Rules.py

Co-authored-by: Ixrec <ericrhitchcock@gmail.com>

* review stuff

* More stuff from review

* comment

* 1.5 Update

* link fix?

* link fix 2

* Update setup_en.md

* Update setup_en.md

* Update setup_en.md

* Evil

* Good fucking lord

* Review stuff again + Logic fixes

* More review stuff

* Even more review stuff - we're almost done

* DW review stuff

* Finish up review stuff

* remove leftover stuff

* a

* assert item

* add A Hat in Time to readme/codeowners files

* Fix range options not being corrected properly

* 120 chars per line in docs

* Update worlds/ahit/Regions.py

Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>

* Update worlds/ahit/DeathWishLocations.py

Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>

* Remove some unnecessary option.class.value

* Remove data_version and more option.class.value

* Update worlds/ahit/Items.py

Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>

* Remove the rest of option.class.value

* Update worlds/ahit/DeathWishLocations.py

Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>

* review stuff

* Replace connect_regions with Region.connect

* review stuff

* Remove unnecessary Optional from LocData

* Remove HatType.NONE

* Update worlds/ahit/test/TestActs.py

Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>

* fix so default tests actually don't run

* Improve performance for death wish rules

* rename test file

* change test imports

* 1000 is probably unnecessary

* a

* change state.count to state.has

* stuff

* starting inventory hats fix

* shouldn't have done this lol

* make ship shape task goal equal to number of tasksanity checks if set to 0

* a

* change act shuffle starting acts + logic updates

* dumb

* option groups + lambda capture cringe + typo

* a

* b

* missing option in groups

* c

* Fix Your Contract Has Expired being placed on first level when it shouldn't

* yche fix

* formatting

* major logic bug fix for death wish

* Update Regions.py

* Add missing indirect connections

* Fix generation error from chapter 2 start with act shuffle off

* a

* Revert "a"

This reverts commit df58bbcd99.

* Revert "Fix generation error from chapter 2 start with act shuffle off"

This reverts commit 0f4d441824.

* fix async lag

* Update Client.py

* shop item names need this now

* fix indentation

---------

Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com>
Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
Co-authored-by: Ixrec <ericrhitchcock@gmail.com>
Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2024-09-21 23:10:18 +02:00
Kaito Sinclaire
2b88be5791 Doom 1993 (auto-generated files): Update E4 logic (#3957) 2024-09-21 23:06:31 +02:00
agilbert1412
204e940f47 Stardew Valley: Fix Art Of Crabbing Logic and Extract Festival Logic (#3625)
* here you go kaito kid

* here you go kaito kid

* move reward logic in its own method

---------

Co-authored-by: Jouramie <jouramie@hotmail.com>
Co-authored-by: Jérémie Bolduc <16137441+Jouramie@users.noreply.github.com>
2024-09-21 23:05:00 +02:00
Scipio Wright
69d3db21df TUNIC: Deal with the boxes blocking the entrance to Beneath the Vault 2024-09-21 23:02:58 +02:00
qwint
41ddb96b24 HK: add race bool to slot data (#3971) 2024-09-21 16:45:22 +02:00
João Victor
ba8f03516e Docs: added Brazilian Portuguese Translation for Hollow Knight setup guide (#3909)
* add neww pt-br translation

* setup file

* Update setup_pt_br.md

* add ` to paths

* correct grammar

* add space .-.

* add more spaces .-. .-. .-.

* capitalize HK

* Update setup_pt_br.md

* accent not the same as punctuation

* small changes

* Update setup_pt_br.md
2024-09-20 19:19:48 +02:00
Spineraks
0095eecf2b Yacht Dice: Remove Victory item and make it an event instead (#3968)
* Add the yacht dice (from other git) world to the yacht dice fork

* Update .gitignore

* Removed zillion because it doesn't work

* Update .gitignore

* added zillion again...

* Now you can have 0 extra fragments

* Added alt categories, also options

* Added item categories

* Extra categories are now working! 🐶

* changed options and added exceptions

* Testing if I change the generate.py

* Revert "Testing if I change the generate.py"

This reverts commit 7c2b3df617.

* ignore gitignore

* Delete .gitignore

* Update .gitignore

* Update .gitignore

* Update logic, added multiplicative categories

* Changed difficulties

* Update offline mode so that it works again

* Adjusted difficulty

* New version of the apworld, with 1000 as final score, always

Will still need to check difficulty and weights of adding items.
Website is not ready yet, so this version is not usable yet :)

* Changed yaml and small bug fixes

Fix when goal and max are same
Options: changed chance to weight

* no changes, just whitespaces

* changed how logic works

Now you put an array of mults and the cpu gets a couple of tries

* Changed logic, tweaked a bit too

* Preparation for 2.0

* logic tweak

* Logic for alt categories properly now

* Update setup_en.md

* Update en_YachtDice.md

* Improve performance of add_distributions

* Formatting style

* restore gitignore to APMW

* Tweaked generation parameters and methods

* Version 2.0.3

manual input option
max score in logic always 2.0.3
faster gen

* Comments and editing

* Renamed setup guide

* Improved create_items code

* init of locations: remove self.event line

* Moved setting early items to generate_early

* Add my name to CODEOWNERS

* Added Yacht Dice to the readme in list of games

* Improve performance of Yacht Dice

* newline

* Improve typing

* This is actually just slower lol

* Update worlds/yachtdice/Items.py

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Update Options.py

* Styling

* finished text whichstory option

* removed roll and rollfragments; not used

* import; worlds not world :)

* Option groups!

* ruff styling, fix

* ruff format styling!

* styling and capitalization of options

* small comment

* Cleaned up the "state_is_a_list" a little bit

* RUFF 🐶

* Changed filling the itempool for efficiency

Now, we start with 17 extra items in the item pool, it's quite likely you need at least 17 items (~80%?).
And then afterwards, we delete items if we overshoot the target of 1000, and add items if we haven't reached an achievable score of 1000 yet. Also, no need to recompute the entire logic when adding points.

* 🐶

* Removed plando "fix"

* Changed indent of score multiplier

* faster location function

* Comments to docstrings

* fixed making location closest to goal_score be goal_score

* options format

* iterate keys and values of a dict together

* small optimization ListState

* faster collection of categories

* return arguments instead of making a list (will 🐶 later)

* Instead of turning it into a tuple, you can just make a tuple literal

* remove .keys()

* change .random and used enumerate

* some readability improvements

* Remove location "0", we don't use that one

* Remove lookup_id_to_name entirely

I for sure don't use it, and as far as I know it's not one of the mandatory functions for AP, these are item_name_to_id and location_name_to_id.

* .append instead of += for single items, percentile function changed

Also an extra comment for location ids.

* remove ) too many

* Removed sorted from category list

* Hash categories (which makes it slower :( )

Maybe I messed up or misunderstood...
I'll revert this right away since it is 2x slower, probably because of sorted instead of sort?

* Revert "Hash categories (which makes it slower :( )"

This reverts commit 34f2c1aed8.

* temporary push: 40% faster generation test

Small changes in logic make the generation 40% faster.
I'll have to think about how big the changes are. I suspect they are rather limited.
If this is the way to go, I'll remove the temp file and redo the YachtWeights file, I'll remove the functions there and just put the new weights here.

* Add Points item category

* Reverse changes of bad idea :)

* ruff 🐶

* Use numpy and pmf function to speed up gen

Numpy has a built-in way to sum probability mass functions (pmf).
This shaves of 60% of the generation time :D

* Revert "Use numpy and pmf function to speed up gen"

This reverts commit 9290191cb3.

* Step inbetween to change the weights

* Changed the weights to make it faster

135 -> 81 seconds on 100 random yamls

* Adjusted max_dist, split dice_simulation function

* Removed nonlocal and pass arguments instead

* Change "weight-lists" to Dict[str, float]

* Removed the return from ini_locations.

Also added explanations to cat_weights

* Choice options; dont'use .value (will ruff later)

* Only put important options in slotdata

* 🐶

* Add Dict import

* Split the cache per player, limit size to 400.

* 🐶

* added , because of style

* Update apworld version to 2.0.6

2.0.5 is the apworld I released on github to be tested
I never separately released 2.0.4.

* Multiple smaller code improvements

- changed names in YachtWeights so we don't need to translate them in Rules anymore
- we now remember which categories are present in the game, and also put this in slotdata. This we do because only one of two categories is present in a game. If for some reason both are present (plando/getitem/startinventory), we now know which category to ignore
-

* 🐶 ruff

* Mostly minimize_extra_items improvements

- Change logic, generation is now even faster (0.6s per default yaml).
- Made the option 'minimize_extra_items' do a lot more, hopefully this makes the impact of Yacht Dice a little bit less, if you want that. Here's what is also does now:
 - you start with 2 dice and 2 rolls
 - there will be less locations/items at the start of you game

* ruff 🐶

* Removed printing options

* Reworded some option descriptions

* Yacht Dice: setup: change release-link to latest

On the installation page, link to the latest release, instead of the page with all releases

* Several fixes and changes

-change apworld version
-Removed the extra roll (this was not intended)
-change extra_points_added to a mutable list to that it actually does something
-removed variables multipliers_added and items_added
-Rules, don't order by quantity, just by mean_score
-Changed the weights in general to make it faster

* 🐶

* Revert setup to what it was (latest, without S)

* remove temp weights file, shouldn't be here

* Made sure that there is not too many step score multipliers.

Too many step score multipliers lead to gen fails too, probably because you need many categories for them to actually help a lot. So it's hard to use them at the start of the game.

* add filler item name

* Textual fixes and changes

* Remove Victory item and use event instead.

* Revert "Remove Victory item and use event instead."

This reverts commit c2f7d674d3.

* Revert "Textual fixes and changes"

This reverts commit e9432f9245.

* Remove Victory item and make it an event instead

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-09-20 19:07:45 +02:00
Alex Nordstrom
79942c09c2 LADX: define filler item, fix for extra golden leaves (#3918)
* set filler item
also rename "Master Stalfos' Message" to "Nothing" as it shows up in game, and "Gel" to "Zol Attack"

* fix for extra gold leaves

* fix for start_inventory
2024-09-20 16:18:09 +02:00
digiholic
1b15c6920d [OSRS] Adds display names to Options #3954 2024-09-20 16:15:30 +02:00
gaithern
499d79f089 Kingdom Hearts: Fix Hint Spam and Add Setting Queries #3899 2024-09-19 22:32:47 +02:00
NewSoupVi
926e08513c The Witness: Remove some unused code #3852 2024-09-19 01:57:59 +02:00
Silvris
025c550991 Ocarina of Time: options and general cleanup (#3767)
* working?

* missed one

* fix old start inventory usage

* missed global random usage

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-09-18 21:26:59 +02:00
Doug Hoskisson
fced9050a4 Zillion: fix logic cache (#3719) 2024-09-18 21:09:47 +02:00
Faris
2ee8b7535d OSRS: UT integration for OSRS to support chunksanity (#3776) 2024-09-18 20:53:17 +02:00
Remy Jette
0d35cd4679 BizHawkClient: Avoid error launching BizHawkClient via Launcher CLI (#3554)
* Core, BizHawkClient: Support launching BizHawkClient via Launcher command line

* Revert changes to LauncherComponents.py
2024-09-18 20:42:22 +02:00
Alchav
db5d9fbf70 Pokemon R/B: Version 5 Update (#3566)
* Quiz updates

* Enable Partial Trainersanity

* Losable Key Items Still Count

* New options api

* Type Chart Seed

* Continue switching to new options API

* Level Scaling and Quiz fixes

* Level Scaling and Quiz fixes

* Clarify that palettes are only for Super Gameboy

* Type chart seed groups use one random players' options

* remove goal option again

* Text updates

* Trainersanity Trainers ignore Blind Trainers setting

* Re-order simple connecting interiors so that directions are preserved when possible

* Dexsanity exact number

* Year update

* Dexsanity Doc update

* revert accidental file deletion

* Fixes

* Add world parameter to logic calls

* restore correct seeded random object

* missing world.options changes

* Trainersanity table bug fix

* delete entrances as well as exits when restarting door shuffle

* Do not collect route 25 item for level scaling if trainer is trainersanity

* world.options in level_scaling.py

* Update worlds/pokemon_rb/level_scaling.py

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Update worlds/pokemon_rb/encounters.py

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Update worlds/pokemon_rb/encounters.py

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* world -> multiworld

* Fix Cerulean Cave Hidden Item Center Rocks region

* Fix Cerulean Cave Hidden Item Center Rocks region for real

* Remove "self-locking" rules

* Update worlds/pokemon_rb/regions.py

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Fossil events

* Update worlds/pokemon_rb/level_scaling.py

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

---------

Co-authored-by: alchav <alchav@jalchavware.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-09-18 20:37:17 +02:00
jamesbrq
51a6dc150c MLSS: Various bugfixes and QoL updates (#3744)
* Small fixes

* Update Location names + Remove redundant rule

* Fix for str not being returned in get_filler_item_name()

* ASM changes + various name/logic updates

* Remove extra unintended change + Make beanstone/beanlets useful

* Add missing timer logic to client

* Update Rules.py

* Fix bad capitalization

* Small formatting and ASM changes

* Update basepatch.bsdiff

* Update seed verification to be more likely to make a correct comparison

* Add Pipe 10

* Final batch of small fixes

* FINAL CHANGE I SWEAR

* Added victory Item for spoilers

* Update worlds/mlss/Regions.py

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Update worlds/mlss/Items.py

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Fix jokes end logic

* Update worlds/mlss/Regions.py

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Update worlds/mlss/Rules.py

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Update worlds/mlss/Rules.py

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Update worlds/mlss/Rules.py

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Fix jokes end logic

* Item Location mismatch + Check options against rules

* Change List to Set + Check options against rules

* Moved Victory item to event

* Update worlds/mlss/__init__.py

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>

* Update worlds/mlss/__init__.py

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-09-18 19:33:02 +02:00
black-sliver
710609fa60 WebHost: move api/room_status out of __init__.py (#3958)
* WebHost: move room_status out of __init__.py

The old location is unexpected and easy to miss.

* WebHost: fix typing in api/room_status
2024-09-18 10:27:53 +02:00
Fabian Dill
da781bb4ac Core: rename yaml_output to csv_output (#3955) 2024-09-18 04:37:10 +02:00
Aaron Wagener
69487661dd Core: change yaml_output to output a full csv (#3653)
* make yaml_output arg a bool instead of number

* make yaml_output dump all player options as csv

* it sorts by game so swap those columns

* capitalize game and name headers

* use a list and just add an if before adding instead of sorting

* skip options that the world doesn't want displayed

* check if the class is a subclass of Removed specifically instead of the none flag

* don't create empty rows

* add a header for every game option that isn't from the common ones even if they have the same name

* add to webhost gen args so it can still gen
2024-09-18 01:33:03 +02:00
black-sliver
f73c0d9894 WebHost: Better host room v2 (#3948)
* WebHost: add spinner to room command

and show error message if fetch fails due to NetworkError

* WebHost: don't update room log while tab is inactive

* WebHost: don't include log for automated requests

* WebHost: refresh room also for re-spinups

and do that from javascript

* Test, WebHost: send fake user-agent where required

* WebHost: remove wrong comment in host room
2024-09-18 00:47:26 +02:00
Fabian Dill
6fac83b84c Factorio: update API use (#3760)
---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-09-18 00:18:17 +02:00
sgrunt
debb936618 DOOM II: Fix sector 95 assignment in DOOM II MAP17 to correctly flag the BFG9000 location as in the Yellow Key area (#3705)
Co-authored-by: sgrunt <sgrunt1987@gmail.com>
2024-09-18 00:08:18 +02:00
agilbert1412
8c5b65ff26 Stardew Valley: Remove Accessibility and progression balancing from presets #3833 2024-09-18 00:07:40 +02:00
agilbert1412
a7c96436d9 Stardew valley: Add Marlon bedroom entrance rule (#3735)
* - Created a test for the "Mapping Cave Systems" book

* - Added missing rule to marlon's bedroom

* - Can kill any monster, not just green slime

* - Added a compound source structure, but I ended up deciding to not use it here. Still keeping it as it will probably be useful eventually

* - Use the compound source of the monster compoundium (ironic, I know)

* - Add required elevators

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-09-18 00:03:33 +02:00
Aaron Wagener
4e60f3cc54 The Messenger: Fix Portal Plando Issues (#3838)
* add a more clear error message for a missing exit

* remove portal region from the available pool

* ensure plando portals are in the correct spot in the list and it gets cleared correctly
2024-09-18 00:00:26 +02:00
Exempt-Medic
30a0b337a2 DS3: Make Red Eye Orb always require Lift Chamber Key #3857 2024-09-17 23:58:45 +02:00
Scipio Wright
4ea1dddd2f TUNIC: Better logic for Library Lab glass and Fortress leaf piles #3880 2024-09-17 23:57:55 +02:00
Mrks
dc218b7997 LADX: Adding Slot Data For Magpie Tracker (#3582)
* wip: LADX slot_data

* LADX: slot_data

* Sending slot_data to magpie.

* Moved sending slot_data from pushing to pull by Magpie request.

* Adding EoF newline to tracker.py.

* Update Tracker.py

* Update __init__.py

* Update LinksAwakeningClient.py

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2024-09-17 23:56:40 +02:00
Natalie Weizenbaum
78c5489189 DS3: Mark the Archdeacon Set as downstream of Deacons of the Deep (#3883)
This ensures that if Deacons is replaced with Yhorm, the Storm Ruler
won't show up in these locations.
2024-09-17 23:50:02 +02:00
Bryce Wilson
d1a7bc66e6 Pokemon Emerald: Prevent client from spamming goal status update (#3900) 2024-09-17 23:49:36 +02:00
Ziktofel
b982e9ebb4 SC2: Fix /received display bugs (#3949)
* SC2: Fix location display in /received command

* SC2: Backport broken markup fix in /received output from the dev branch

* Cleanup
2024-09-17 23:18:43 +02:00
Bryce Wilson
8f7e0dc441 Core: Improve death link option description (#3951) 2024-09-17 23:17:41 +02:00
Bryce Wilson
5aea8d4ab5 Pokemon Emerald: Update changelog (#3952) 2024-09-17 15:14:05 +02:00
Rensen3
97be5f1dde YGO06: slotdata fix (#3953)
* YGO06: fix slot data for universal tracker

* YGO06: put Extremely Low Deck Bonus after Low Deck Bonus
2024-09-17 15:13:19 +02:00
Mysteryem
dae3fe188d OOT: Fix incorrect region accessibility after update_reachable_regions() (#3712)
`CollectionState.update_reachable_regions()` un-stales the state for all
players, but when checking `OOTRegion.can_reach()`, it would only update
OOT's age region accessibility when the state was stale, so if the state
was always un-staled by `update_reachable_regions()` immediately before
`OOTRegion.can_reach()`, OOT's age region accessibility would never
update.

This patch fixes the issue by replacing use of CollectionState.stale
with a separate stale state dictionary specific to OOT that is only
un-staled by `_oot_update_age_reachable_regions()`.

OOT's collect() and remove() implementations have been updated to stale
the new OOT-specific state.
2024-09-17 15:11:35 +02:00
Exempt-Medic
96542fb2d8 Blasphemous: Move pre_fill to create_items #3901 2024-09-17 15:08:15 +02:00
qwint
ec50b0716a Core: Add color conversions for colorama/terminal output #3940 2024-09-17 14:44:32 +02:00
Bryce Wilson
f8d3c26e3c Pokemon Emerald: Fix unguarded wonder trade write (#3939) 2024-09-17 14:43:22 +02:00
digiholic
1c0cec0de2 [OSRS] Adds Description to OSRS World #3921 2024-09-17 14:42:48 +02:00
Silvris
4692e6f08a MM2: fix Air Shooter minimum damage #3922 2024-09-17 14:42:19 +02:00
Mysteryem
b8d23ec595 MMBN3: Add missing indirect conditions (#3931)
Entrances to SciLab_Cyberworld and Yoka_Cyberworld had logic for being
able to reach SciLab_Overworld, but did not register this indirect
condition.

Entrances to Beach_Cyberworld had logic for being able to reach
Yoka_Overworld, but did not register this indirect condition.

Entrances to Undernet and Secret_Area had logic for having a high enough
explore score, but explore score is calculated based on the
accessibility of a number of regions and no indirect conditions were
being registered for these regions.
2024-09-17 14:41:56 +02:00
Silvris
ce42e42af7 Core: fix single player item links (#3721)
* fix single player item links

* Make a variable and fix weird spacing

* use advancement instead of classification

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-09-17 14:36:05 +02:00
Star Rauchenberger
ee12dda361 Lingo: Added missing connection from The Tenacious -> Hub Room (#3947) 2024-09-16 18:06:20 +02:00
Exempt-Medic
84805a4e54 HK: XBox doesn't exist #3932 2024-09-16 14:30:47 +02:00
Fabian Dill
5530d181da Core: update version number (#3944) 2024-09-16 06:48:13 +02:00
Mysteryem
ed948e3e5b sm64ex: Add missing indirect condition for BitFS randomized entrance (#3926)
The Bowser in the Fire Sea randomized entrance has an access rule that
requires being able to reach "DDD: Board Bowser's Sub", but being able
to reach a location also requires being able to reach the region that
location is in, so an indirect condition is required.
2024-09-13 16:02:13 +02:00
218 changed files with 5166 additions and 4047 deletions

View File

@@ -194,7 +194,9 @@ class MultiWorld():
self.player_types[new_id] = NetUtils.SlotType.group self.player_types[new_id] = NetUtils.SlotType.group
world_type = AutoWorld.AutoWorldRegister.world_types[game] world_type = AutoWorld.AutoWorldRegister.world_types[game]
self.worlds[new_id] = world_type.create_group(self, new_id, players) self.worlds[new_id] = world_type.create_group(self, new_id, players)
self.worlds[new_id].collect_item = classmethod(AutoWorld.World.collect_item).__get__(self.worlds[new_id]) self.worlds[new_id].collect_item = AutoWorld.World.collect_item.__get__(self.worlds[new_id])
self.worlds[new_id].collect = AutoWorld.World.collect.__get__(self.worlds[new_id])
self.worlds[new_id].remove = AutoWorld.World.remove.__get__(self.worlds[new_id])
self.player_name[new_id] = name self.player_name[new_id] = name
new_group = self.groups[new_id] = Group(name=name, game=game, players=players, new_group = self.groups[new_id] = Group(name=name, game=game, players=players,
@@ -342,6 +344,8 @@ class MultiWorld():
region = Region("Menu", group_id, self, "ItemLink") region = Region("Menu", group_id, self, "ItemLink")
self.regions.append(region) self.regions.append(region)
locations = region.locations locations = region.locations
# ensure that progression items are linked first, then non-progression
self.itempool.sort(key=lambda item: item.advancement)
for item in self.itempool: for item in self.itempool:
count = common_item_count.get(item.player, {}).get(item.name, 0) count = common_item_count.get(item.player, {}).get(item.name, 0)
if count: if count:
@@ -718,7 +722,7 @@ class CollectionState():
if new_region in reachable_regions: if new_region in reachable_regions:
blocked_connections.remove(connection) blocked_connections.remove(connection)
elif connection.can_reach(self): elif connection.can_reach(self):
assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region" assert new_region, f"tried to search through an Entrance \"{connection}\" with no connected Region"
reachable_regions.add(new_region) reachable_regions.add(new_region)
blocked_connections.remove(connection) blocked_connections.remove(connection)
blocked_connections.update(new_region.exits) blocked_connections.update(new_region.exits)
@@ -944,6 +948,7 @@ class Entrance:
self.player = player self.player = player
def can_reach(self, state: CollectionState) -> bool: def can_reach(self, state: CollectionState) -> bool:
assert self.parent_region, f"called can_reach on an Entrance \"{self}\" with no parent_region"
if self.parent_region.can_reach(state) and self.access_rule(state): if self.parent_region.can_reach(state) and self.access_rule(state):
if not self.hide_path and not self in state.path: 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))) state.path[self] = (self.name, state.path.get(self.parent_region, (self.parent_region.name, None)))
@@ -1164,7 +1169,7 @@ class Location:
def can_reach(self, state: CollectionState) -> bool: def can_reach(self, state: CollectionState) -> bool:
# Region.can_reach is just a cache lookup, so placing it first for faster abort on average # Region.can_reach is just a cache lookup, so placing it first for faster abort on average
assert self.parent_region, "Can't reach location without region" assert self.parent_region, f"called can_reach on a Location \"{self}\" with no parent_region"
return self.parent_region.can_reach(state) and self.access_rule(state) return self.parent_region.can_reach(state) and self.access_rule(state)
def place_locked_item(self, item: Item): def place_locked_item(self, item: Item):
@@ -1204,26 +1209,13 @@ class Location:
class ItemClassification(IntFlag): class ItemClassification(IntFlag):
filler = 0b0000 filler = 0b0000 # aka trash, as in filler items like ammo, currency etc,
""" aka trash, as in filler items like ammo, currency etc """ progression = 0b0001 # Item that is logically relevant
useful = 0b0010 # Item that is generally quite useful, but not required for anything logical
progression = 0b0001 trap = 0b0100 # detrimental item
""" Item that is logically relevant. skip_balancing = 0b1000 # should technically never occur on its own
Protects this item from being placed on excluded or unreachable locations. """ # Item that is logically relevant, but progression balancing should not touch.
# Typically currency or other counted items.
useful = 0b0010
""" Item that is especially useful.
Protects this item from being placed on excluded or unreachable locations.
When combined with another flag like "progression", it means "an especially useful progression item". """
trap = 0b0100
""" Item that is detrimental in some way. """
skip_balancing = 0b1000
""" should technically never occur on its own
Item that is logically relevant, but progression balancing should not touch.
Typically currency or other counted items. """
progression_skip_balancing = 0b1001 # only progression gets balanced progression_skip_balancing = 0b1001 # only progression gets balanced
def as_flag(self) -> int: def as_flag(self) -> int:

View File

@@ -1,9 +1,10 @@
from __future__ import annotations from __future__ import annotations
import sys
import ModuleUpdate import ModuleUpdate
ModuleUpdate.update() ModuleUpdate.update()
from worlds._bizhawk.context import launch from worlds._bizhawk.context import launch
if __name__ == "__main__": if __name__ == "__main__":
launch() launch(*sys.argv[1:])

View File

@@ -45,10 +45,21 @@ def get_ssl_context():
class ClientCommandProcessor(CommandProcessor): class ClientCommandProcessor(CommandProcessor):
"""
The Command Processor will parse every method of the class that starts with "_cmd_" as a command to be called
when parsing user input, i.e. _cmd_exit will be called when the user sends the command "/exit".
The decorator @mark_raw can be imported from MultiServer and tells the parser to only split on the first
space after the command i.e. "/exit one two three" will be passed in as method("one two three") with mark_raw
and method("one", "two", "three") without.
In addition all docstrings for command methods will be displayed to the user on launch and when using "/help"
"""
def __init__(self, ctx: CommonContext): def __init__(self, ctx: CommonContext):
self.ctx = ctx self.ctx = ctx
def output(self, text: str): def output(self, text: str):
"""Helper function to abstract logging to the CommonClient UI"""
logger.info(text) logger.info(text)
def _cmd_exit(self) -> bool: def _cmd_exit(self) -> bool:
@@ -164,13 +175,14 @@ class ClientCommandProcessor(CommandProcessor):
async_start(self.ctx.send_msgs([{"cmd": "StatusUpdate", "status": state}]), name="send StatusUpdate") async_start(self.ctx.send_msgs([{"cmd": "StatusUpdate", "status": state}]), name="send StatusUpdate")
def default(self, raw: str): def default(self, raw: str):
"""The default message parser to be used when parsing any messages that do not match a command"""
raw = self.ctx.on_user_say(raw) raw = self.ctx.on_user_say(raw)
if raw: if raw:
async_start(self.ctx.send_msgs([{"cmd": "Say", "text": raw}]), name="send Say") async_start(self.ctx.send_msgs([{"cmd": "Say", "text": raw}]), name="send Say")
class CommonContext: class CommonContext:
# Should be adjusted as needed in subclasses # The following attributes are used to Connect and should be adjusted as needed in subclasses
tags: typing.Set[str] = {"AP"} tags: typing.Set[str] = {"AP"}
game: typing.Optional[str] = None game: typing.Optional[str] = None
items_handling: typing.Optional[int] = None items_handling: typing.Optional[int] = None
@@ -343,6 +355,8 @@ class CommonContext:
self.item_names = self.NameLookupDict(self, "item") self.item_names = self.NameLookupDict(self, "item")
self.location_names = self.NameLookupDict(self, "location") self.location_names = self.NameLookupDict(self, "location")
self.versions = {}
self.checksums = {}
self.jsontotextparser = JSONtoTextParser(self) self.jsontotextparser = JSONtoTextParser(self)
self.rawjsontotextparser = RawJSONtoTextParser(self) self.rawjsontotextparser = RawJSONtoTextParser(self)
@@ -429,7 +443,10 @@ class CommonContext:
self.auth = await self.console_input() self.auth = await self.console_input()
async def send_connect(self, **kwargs: typing.Any) -> None: async def send_connect(self, **kwargs: typing.Any) -> None:
""" send `Connect` packet to log in to server """ """
Send a `Connect` packet to log in to the server,
additional keyword args can override any value in the connection packet
"""
payload = { payload = {
'cmd': 'Connect', 'cmd': 'Connect',
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple, 'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
@@ -439,6 +456,7 @@ class CommonContext:
if kwargs: if kwargs:
payload.update(kwargs) payload.update(kwargs)
await self.send_msgs([payload]) await self.send_msgs([payload])
await self.send_msgs([{"cmd": "Get", "keys": ["_read_race_mode"]}])
async def console_input(self) -> str: async def console_input(self) -> str:
if self.ui: if self.ui:
@@ -459,6 +477,7 @@ class CommonContext:
return False return False
def slot_concerns_self(self, slot) -> bool: def slot_concerns_self(self, slot) -> bool:
"""Helper function to abstract player groups, should be used instead of checking slot == self.slot directly."""
if slot == self.slot: if slot == self.slot:
return True return True
if slot in self.slot_info: if slot in self.slot_info:
@@ -466,6 +485,7 @@ class CommonContext:
return False return False
def is_echoed_chat(self, print_json_packet: dict) -> bool: def is_echoed_chat(self, print_json_packet: dict) -> bool:
"""Helper function for filtering out messages sent by self."""
return print_json_packet.get("type", "") == "Chat" \ return print_json_packet.get("type", "") == "Chat" \
and print_json_packet.get("team", None) == self.team \ and print_json_packet.get("team", None) == self.team \
and print_json_packet.get("slot", None) == self.slot and print_json_packet.get("slot", None) == self.slot
@@ -497,13 +517,14 @@ class CommonContext:
"""Gets called before sending a Say to the server from the user. """Gets called before sending a Say to the server from the user.
Returned text is sent, or sending is aborted if None is returned.""" Returned text is sent, or sending is aborted if None is returned."""
return text return text
def on_ui_command(self, text: str) -> None: def on_ui_command(self, text: str) -> None:
"""Gets called by kivy when the user executes a command starting with `/` or `!`. """Gets called by kivy when the user executes a command starting with `/` or `!`.
The command processor is still called; this is just intended for command echoing.""" The command processor is still called; this is just intended for command echoing."""
self.ui.print_json([{"text": text, "type": "color", "color": "orange"}]) self.ui.print_json([{"text": text, "type": "color", "color": "orange"}])
def update_permissions(self, permissions: typing.Dict[str, int]): def update_permissions(self, permissions: typing.Dict[str, int]):
"""Internal method to parse and save server permissions from RoomInfo"""
for permission_name, permission_flag in permissions.items(): for permission_name, permission_flag in permissions.items():
try: try:
flag = Permission(permission_flag) flag = Permission(permission_flag)
@@ -552,26 +573,34 @@ class CommonContext:
needed_updates.add(game) needed_updates.add(game)
continue continue
local_version: int = network_data_package["games"].get(game, {}).get("version", 0) cached_version: int = self.versions.get(game, 0)
local_checksum: typing.Optional[str] = network_data_package["games"].get(game, {}).get("checksum") cached_checksum: typing.Optional[str] = self.checksums.get(game)
# no action required if local version is new enough # no action required if cached version is new enough
if (not remote_checksum and (remote_version > local_version or remote_version == 0)) \ if (not remote_checksum and (remote_version > cached_version or remote_version == 0)) \
or remote_checksum != local_checksum: or remote_checksum != cached_checksum:
cached_game = Utils.load_data_package_for_checksum(game, remote_checksum) local_version: int = network_data_package["games"].get(game, {}).get("version", 0)
cache_version: int = cached_game.get("version", 0) local_checksum: typing.Optional[str] = network_data_package["games"].get(game, {}).get("checksum")
cache_checksum: typing.Optional[str] = cached_game.get("checksum") if ((remote_checksum or remote_version <= local_version and remote_version != 0)
# download remote version if cache is not new enough and remote_checksum == local_checksum):
if (not remote_checksum and (remote_version > cache_version or remote_version == 0)) \ self.update_game(network_data_package["games"][game], game)
or remote_checksum != cache_checksum:
needed_updates.add(game)
else: else:
self.update_game(cached_game, game) cached_game = Utils.load_data_package_for_checksum(game, remote_checksum)
cache_version: int = cached_game.get("version", 0)
cache_checksum: typing.Optional[str] = cached_game.get("checksum")
# download remote version if cache is not new enough
if (not remote_checksum and (remote_version > cache_version or remote_version == 0)) \
or remote_checksum != cache_checksum:
needed_updates.add(game)
else:
self.update_game(cached_game, game)
if needed_updates: if needed_updates:
await self.send_msgs([{"cmd": "GetDataPackage", "games": [game_name]} for game_name in needed_updates]) await self.send_msgs([{"cmd": "GetDataPackage", "games": [game_name]} for game_name in needed_updates])
def update_game(self, game_package: dict, game: str): def update_game(self, game_package: dict, game: str):
self.item_names.update_game(game, game_package["item_name_to_id"]) self.item_names.update_game(game, game_package["item_name_to_id"])
self.location_names.update_game(game, game_package["location_name_to_id"]) self.location_names.update_game(game, game_package["location_name_to_id"])
self.versions[game] = game_package.get("version", 0)
self.checksums[game] = game_package.get("checksum")
def update_data_package(self, data_package: dict): def update_data_package(self, data_package: dict):
for game, game_data in data_package["games"].items(): for game, game_data in data_package["games"].items():
@@ -613,6 +642,7 @@ class CommonContext:
logger.info(f"DeathLink: Received from {data['source']}") logger.info(f"DeathLink: Received from {data['source']}")
async def send_death(self, death_text: str = ""): async def send_death(self, death_text: str = ""):
"""Helper function to send a deathlink using death_text as the unique death cause string."""
if self.server and self.server.socket: if self.server and self.server.socket:
logger.info("DeathLink: Sending death to your friends...") logger.info("DeathLink: Sending death to your friends...")
self.last_death_link = time.time() self.last_death_link = time.time()
@@ -626,6 +656,7 @@ class CommonContext:
}]) }])
async def update_death_link(self, death_link: bool): async def update_death_link(self, death_link: bool):
"""Helper function to set Death Link connection tag on/off and update the connection if already connected."""
old_tags = self.tags.copy() old_tags = self.tags.copy()
if death_link: if death_link:
self.tags.add("DeathLink") self.tags.add("DeathLink")
@@ -635,7 +666,7 @@ class CommonContext:
await self.send_msgs([{"cmd": "ConnectUpdate", "tags": self.tags}]) await self.send_msgs([{"cmd": "ConnectUpdate", "tags": self.tags}])
def gui_error(self, title: str, text: typing.Union[Exception, str]) -> typing.Optional["kvui.MessageBox"]: def gui_error(self, title: str, text: typing.Union[Exception, str]) -> typing.Optional["kvui.MessageBox"]:
"""Displays an error messagebox""" """Displays an error messagebox in the loaded Kivy UI. Override if using a different UI framework"""
if not self.ui: if not self.ui:
return None return None
title = title or "Error" title = title or "Error"
@@ -987,6 +1018,7 @@ async def console_loop(ctx: CommonContext):
def get_base_parser(description: typing.Optional[str] = None): def get_base_parser(description: typing.Optional[str] = None):
"""Base argument parser to be reused for components subclassing off of CommonClient"""
import argparse import argparse
parser = argparse.ArgumentParser(description=description) parser = argparse.ArgumentParser(description=description)
parser.add_argument('--connect', default=None, help='Address of the multiworld host.') parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
@@ -1037,6 +1069,7 @@ def run_as_textclient(*args):
parser.add_argument("url", nargs="?", help="Archipelago connection url") parser.add_argument("url", nargs="?", help="Archipelago connection url")
args = parser.parse_args(args) args = parser.parse_args(args)
# handle if text client is launched using the "archipelago://name:pass@host:port" url from webhost
if args.url: if args.url:
url = urllib.parse.urlparse(args.url) url = urllib.parse.urlparse(args.url)
if url.scheme == "archipelago": if url.scheme == "archipelago":
@@ -1048,6 +1081,7 @@ def run_as_textclient(*args):
else: else:
parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281") parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281")
# use colorama to display colored text highlighting on windows
colorama.init() colorama.init()
asyncio.run(main(args)) asyncio.run(main(args))

22
Fill.py
View File

@@ -475,28 +475,26 @@ def distribute_items_restrictive(multiworld: MultiWorld,
nonlocal lock_later nonlocal lock_later
lock_later.append(location) lock_later.append(location)
single_player = multiworld.players == 1 and not multiworld.groups
if prioritylocations: if prioritylocations:
# "priority fill" # "priority fill"
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool, fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool,
single_player_placement=multiworld.players == 1, swap=False, on_place=mark_for_locking, single_player_placement=single_player, swap=False, on_place=mark_for_locking, name="Priority")
name="Priority")
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool) accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
defaultlocations = prioritylocations + defaultlocations defaultlocations = prioritylocations + defaultlocations
if progitempool: if progitempool:
# "advancement/progression fill" # "advancement/progression fill"
if panic_method == "swap": if panic_method == "swap":
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=True,
swap=True, name="Progression", single_player_placement=single_player)
name="Progression", single_player_placement=multiworld.players == 1)
elif panic_method == "raise": elif panic_method == "raise":
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=False,
swap=False, name="Progression", single_player_placement=single_player)
name="Progression", single_player_placement=multiworld.players == 1)
elif panic_method == "start_inventory": elif panic_method == "start_inventory":
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=False,
swap=False, allow_partial=True, allow_partial=True, name="Progression", single_player_placement=single_player)
name="Progression", single_player_placement=multiworld.players == 1)
if progitempool: if progitempool:
for item in progitempool: for item in progitempool:
logging.debug(f"Moved {item} to start_inventory to prevent fill failure.") logging.debug(f"Moved {item} to start_inventory to prevent fill failure.")
@@ -529,7 +527,7 @@ def distribute_items_restrictive(multiworld: MultiWorld,
if excludedlocations: if excludedlocations:
raise FillError( raise FillError(
f"Not enough filler items for excluded locations. " f"Not enough filler items for excluded locations. "
f"There are {len(excludedlocations)} more excluded locations than excludable items.", f"There are {len(excludedlocations)} more excluded locations than filler or trap items.",
multiworld=multiworld, multiworld=multiworld,
) )

View File

@@ -43,10 +43,10 @@ def mystery_argparse():
parser.add_argument('--race', action='store_true', default=defaults.race) parser.add_argument('--race', action='store_true', default=defaults.race)
parser.add_argument('--meta_file_path', default=defaults.meta_file_path) parser.add_argument('--meta_file_path', default=defaults.meta_file_path)
parser.add_argument('--log_level', default='info', help='Sets log level') parser.add_argument('--log_level', default='info', help='Sets log level')
parser.add_argument('--yaml_output', default=0, type=lambda value: max(int(value), 0), parser.add_argument("--csv_output", action="store_true",
help='Output rolled mystery results to yaml up to specified number (made for async multiworld)') help="Output rolled player options to csv (made for async multiworld).")
parser.add_argument('--plando', default=defaults.plando_options, parser.add_argument("--plando", default=defaults.plando_options,
help='List of options that can be set manually. Can be combined, for example "bosses, items"') help="List of options that can be set manually. Can be combined, for example \"bosses, items\"")
parser.add_argument("--skip_prog_balancing", action="store_true", parser.add_argument("--skip_prog_balancing", action="store_true",
help="Skip progression balancing step during generation.") help="Skip progression balancing step during generation.")
parser.add_argument("--skip_output", action="store_true", parser.add_argument("--skip_output", action="store_true",
@@ -156,6 +156,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
erargs.skip_prog_balancing = args.skip_prog_balancing erargs.skip_prog_balancing = args.skip_prog_balancing
erargs.skip_output = args.skip_output erargs.skip_output = args.skip_output
erargs.name = {} erargs.name = {}
erargs.csv_output = args.csv_output
settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \ settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.sameoptions else None) {fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.sameoptions else None)
@@ -216,28 +217,6 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
if len(set(name.lower() for name in erargs.name.values())) != len(erargs.name): if len(set(name.lower() for name in erargs.name.values())) != len(erargs.name):
raise Exception(f"Names have to be unique. Names: {Counter(name.lower() for name in erargs.name.values())}") raise Exception(f"Names have to be unique. Names: {Counter(name.lower() for name in erargs.name.values())}")
if args.yaml_output:
import yaml
important = {}
for option, player_settings in vars(erargs).items():
if type(player_settings) == dict:
if all(type(value) != list for value in player_settings.values()):
if len(player_settings.values()) > 1:
important[option] = {player: value for player, value in player_settings.items() if
player <= args.yaml_output}
else:
logging.debug(f"No player settings defined for option '{option}'")
else:
if player_settings != "": # is not empty name
important[option] = player_settings
else:
logging.debug(f"No player settings defined for option '{option}'")
if args.outputpath:
os.makedirs(args.outputpath, exist_ok=True)
with open(os.path.join(args.outputpath if args.outputpath else ".", f"generate_{seed_name}.yaml"), "wt") as f:
yaml.dump(important, f)
return erargs, seed return erargs, seed

View File

@@ -35,7 +35,9 @@ from Utils import is_frozen, user_path, local_path, init_logging, open_filename,
def open_host_yaml(): def open_host_yaml():
file = settings.get_settings().filename s = settings.get_settings()
file = s.filename
s.save()
assert file, "host.yaml missing" assert file, "host.yaml missing"
if is_linux: if is_linux:
exe = which('sensible-editor') or which('gedit') or \ exe = which('sensible-editor') or which('gedit') or \

View File

@@ -467,6 +467,8 @@ class LinksAwakeningContext(CommonContext):
def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str], magpie: typing.Optional[bool]) -> None: def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str], magpie: typing.Optional[bool]) -> None:
self.client = LinksAwakeningClient() self.client = LinksAwakeningClient()
self.slot_data = {}
if magpie: if magpie:
self.magpie_enabled = True self.magpie_enabled = True
self.magpie = MagpieBridge() self.magpie = MagpieBridge()
@@ -564,6 +566,8 @@ class LinksAwakeningContext(CommonContext):
def on_package(self, cmd: str, args: dict): def on_package(self, cmd: str, args: dict):
if cmd == "Connected": if cmd == "Connected":
self.game = self.slot_info[self.slot].game self.game = self.slot_info[self.slot].game
self.slot_data = args.get("slot_data", {})
# TODO - use watcher_event # TODO - use watcher_event
if cmd == "ReceivedItems": if cmd == "ReceivedItems":
for index, item in enumerate(args["items"], start=args["index"]): for index, item in enumerate(args["items"], start=args["index"]):
@@ -628,6 +632,7 @@ class LinksAwakeningContext(CommonContext):
self.magpie.set_checks(self.client.tracker.all_checks) self.magpie.set_checks(self.client.tracker.all_checks)
await self.magpie.set_item_tracker(self.client.item_tracker) await self.magpie.set_item_tracker(self.client.item_tracker)
await self.magpie.send_gps(self.client.gps_tracker) await self.magpie.send_gps(self.client.gps_tracker)
self.magpie.slot_data = self.slot_data
except Exception: except Exception:
# Don't let magpie errors take out the client # Don't let magpie errors take out the client
pass pass

View File

@@ -46,6 +46,9 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
multiworld.sprite_pool = args.sprite_pool.copy() multiworld.sprite_pool = args.sprite_pool.copy()
multiworld.set_options(args) multiworld.set_options(args)
if args.csv_output:
from Options import dump_player_options
dump_player_options(multiworld)
multiworld.set_item_links() multiworld.set_item_links()
multiworld.state = CollectionState(multiworld) multiworld.state = CollectionState(multiworld)
logger.info('Archipelago Version %s - Seed: %s\n', __version__, multiworld.seed) logger.info('Archipelago Version %s - Seed: %s\n', __version__, multiworld.seed)
@@ -335,6 +338,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
"seed_name": multiworld.seed_name, "seed_name": multiworld.seed_name,
"spheres": spheres, "spheres": spheres,
"datapackage": data_package, "datapackage": data_package,
"race_mode": int(multiworld.is_race),
} }
AutoWorld.call_all(multiworld, "modify_multidata", multidata) AutoWorld.call_all(multiworld, "modify_multidata", multidata)

View File

@@ -15,6 +15,7 @@ import math
import operator import operator
import pickle import pickle
import random import random
import shlex
import threading import threading
import time import time
import typing import typing
@@ -184,11 +185,9 @@ class Context:
slot_info: typing.Dict[int, NetworkSlot] slot_info: typing.Dict[int, NetworkSlot]
generator_version = Version(0, 0, 0) generator_version = Version(0, 0, 0)
checksums: typing.Dict[str, str] checksums: typing.Dict[str, str]
item_names: typing.Dict[str, typing.Dict[int, str]] = ( item_names: typing.Dict[str, typing.Dict[int, str]]
collections.defaultdict(lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})')))
item_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]] item_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]]
location_names: typing.Dict[str, typing.Dict[int, str]] = ( location_names: typing.Dict[str, typing.Dict[int, str]]
collections.defaultdict(lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})')))
location_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]] location_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]]
all_item_and_group_names: typing.Dict[str, typing.Set[str]] all_item_and_group_names: typing.Dict[str, typing.Set[str]]
all_location_and_group_names: typing.Dict[str, typing.Set[str]] all_location_and_group_names: typing.Dict[str, typing.Set[str]]
@@ -197,7 +196,6 @@ class Context:
""" each sphere is { player: { location_id, ... } } """ """ each sphere is { player: { location_id, ... } } """
logger: logging.Logger logger: logging.Logger
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int, def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
hint_cost: int, item_cheat: bool, release_mode: str = "disabled", collect_mode="disabled", hint_cost: int, item_cheat: bool, release_mode: str = "disabled", collect_mode="disabled",
remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2, remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2,
@@ -268,6 +266,10 @@ class Context:
self.location_name_groups = {} self.location_name_groups = {}
self.all_item_and_group_names = {} self.all_item_and_group_names = {}
self.all_location_and_group_names = {} self.all_location_and_group_names = {}
self.item_names = collections.defaultdict(
lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})'))
self.location_names = collections.defaultdict(
lambda: Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})'))
self.non_hintable_names = collections.defaultdict(frozenset) self.non_hintable_names = collections.defaultdict(frozenset)
self._load_game_data() self._load_game_data()
@@ -427,6 +429,8 @@ class Context:
use_embedded_server_options: bool): use_embedded_server_options: bool):
self.read_data = {} self.read_data = {}
# there might be a better place to put this.
self.read_data["race_mode"] = lambda: decoded_obj.get("race_mode", 0)
mdata_ver = decoded_obj["minimum_versions"]["server"] mdata_ver = decoded_obj["minimum_versions"]["server"]
if mdata_ver > version_tuple: if mdata_ver > version_tuple:
raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver}," raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver},"
@@ -1150,7 +1154,10 @@ class CommandProcessor(metaclass=CommandMeta):
if not raw: if not raw:
return return
try: try:
command = raw.split() try:
command = shlex.split(raw, comments=False)
except ValueError: # most likely: "ValueError: No closing quotation"
command = raw.split()
basecommand = command[0] basecommand = command[0]
if basecommand[0] == self.marker: if basecommand[0] == self.marker:
method = self.commands.get(basecommand[1:].lower(), None) method = self.commands.get(basecommand[1:].lower(), None)

View File

@@ -273,7 +273,8 @@ class RawJSONtoTextParser(JSONtoTextParser):
color_codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34, color_codes = {'reset': 0, 'bold': 1, 'underline': 4, 'black': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34,
'magenta': 35, 'cyan': 36, 'white': 37, 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43, 'magenta': 35, 'cyan': 36, 'white': 37, 'black_bg': 40, 'red_bg': 41, 'green_bg': 42, 'yellow_bg': 43,
'blue_bg': 44, 'magenta_bg': 45, 'cyan_bg': 46, 'white_bg': 47} 'blue_bg': 44, 'magenta_bg': 45, 'cyan_bg': 46, 'white_bg': 47,
'plum': 35, 'slateblue': 34, 'salmon': 31,} # convert ui colors to terminal colors
def color_code(*args): def color_code(*args):

View File

@@ -8,16 +8,17 @@ import numbers
import random import random
import typing import typing
import enum import enum
from collections import defaultdict
from copy import deepcopy from copy import deepcopy
from dataclasses import dataclass from dataclasses import dataclass
from schema import And, Optional, Or, Schema from schema import And, Optional, Or, Schema
from typing_extensions import Self from typing_extensions import Self
from Utils import get_fuzzy_results, is_iterable_except_str from Utils import get_fuzzy_results, is_iterable_except_str, output_path
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
from BaseClasses import PlandoOptions from BaseClasses import MultiWorld, PlandoOptions
from worlds.AutoWorld import World from worlds.AutoWorld import World
import pathlib import pathlib
@@ -1335,7 +1336,7 @@ class PriorityLocations(LocationSet):
class DeathLink(Toggle): class DeathLink(Toggle):
"""When you die, everyone dies. Of course the reverse is true too.""" """When you die, everyone who enabled death link dies. Of course, the reverse is true too."""
display_name = "Death Link" display_name = "Death Link"
rich_text_doc = True rich_text_doc = True
@@ -1532,3 +1533,42 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
with open(os.path.join(target_folder, game_name + ".yaml"), "w", encoding="utf-8-sig") as f: with open(os.path.join(target_folder, game_name + ".yaml"), "w", encoding="utf-8-sig") as f:
f.write(res) f.write(res)
def dump_player_options(multiworld: MultiWorld) -> None:
from csv import DictWriter
game_players = defaultdict(list)
for player, game in multiworld.game.items():
game_players[game].append(player)
game_players = dict(sorted(game_players.items()))
output = []
per_game_option_names = [
getattr(option, "display_name", option_key)
for option_key, option in PerGameCommonOptions.type_hints.items()
]
all_option_names = per_game_option_names.copy()
for game, players in game_players.items():
game_option_names = per_game_option_names.copy()
for player in players:
world = multiworld.worlds[player]
player_output = {
"Game": multiworld.game[player],
"Name": multiworld.get_player_name(player),
}
output.append(player_output)
for option_key, option in world.options_dataclass.type_hints.items():
if issubclass(Removed, option):
continue
display_name = getattr(option, "display_name", option_key)
player_output[display_name] = getattr(world.options, option_key).current_option_name
if display_name not in game_option_names:
all_option_names.append(display_name)
game_option_names.append(display_name)
with open(output_path(f"generate_{multiworld.seed_name}.csv"), mode="w", newline="") as file:
fields = ["Game", "Name", *all_option_names]
writer = DictWriter(file, fields)
writer.writeheader()
writer.writerows(output)

View File

@@ -46,7 +46,7 @@ class Version(typing.NamedTuple):
return ".".join(str(item) for item in self) return ".".join(str(item) for item in self)
__version__ = "0.5.0" __version__ = "0.5.1"
version_tuple = tuplize_version(__version__) version_tuple = tuplize_version(__version__)
is_linux = sys.platform.startswith("linux") is_linux = sys.platform.startswith("linux")
@@ -423,7 +423,7 @@ class RestrictedUnpickler(pickle.Unpickler):
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint", "SlotType", "NetworkSlot"}: if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint", "SlotType", "NetworkSlot"}:
return getattr(self.net_utils_module, name) return getattr(self.net_utils_module, name)
# Options and Plando are unpickled by WebHost -> Generate # Options and Plando are unpickled by WebHost -> Generate
if module == "worlds.generic" and name in {"PlandoItem", "PlandoConnection"}: if module == "worlds.generic" and name == "PlandoItem":
if not self.generic_properties_module: if not self.generic_properties_module:
self.generic_properties_module = importlib.import_module("worlds.generic") self.generic_properties_module = importlib.import_module("worlds.generic")
return getattr(self.generic_properties_module, name) return getattr(self.generic_properties_module, name)
@@ -434,7 +434,7 @@ class RestrictedUnpickler(pickle.Unpickler):
else: else:
mod = importlib.import_module(module) mod = importlib.import_module(module)
obj = getattr(mod, name) obj = getattr(mod, name)
if issubclass(obj, self.options_module.Option): if issubclass(obj, (self.options_module.Option, self.options_module.PlandoConnection)):
return obj return obj
# Forbid everything else. # Forbid everything else.
raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden") raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden")

View File

@@ -267,9 +267,7 @@ class WargrooveContext(CommonContext):
def build(self): def build(self):
container = super().build() container = super().build()
panel = TabbedPanelItem(text="Wargroove") self.add_client_tab("Wargroove", self.build_tracker())
panel.content = self.build_tracker()
self.tabs.add_widget(panel)
return container return container
def build_tracker(self) -> TrackerLayout: def build_tracker(self) -> TrackerLayout:

View File

@@ -1,51 +1,15 @@
"""API endpoints package.""" """API endpoints package."""
from typing import List, Tuple from typing import List, Tuple
from uuid import UUID
from flask import Blueprint, abort, url_for from flask import Blueprint
import worlds.Files from ..models import Seed
from ..models import Room, Seed
api_endpoints = Blueprint('api', __name__, url_prefix="/api") api_endpoints = Blueprint('api', __name__, url_prefix="/api")
# unsorted/misc endpoints
def get_players(seed: Seed) -> List[Tuple[str, str]]: def get_players(seed: Seed) -> List[Tuple[str, str]]:
return [(slot.player_name, slot.game) for slot in seed.slots] return [(slot.player_name, slot.game) for slot in seed.slots]
@api_endpoints.route('/room_status/<suuid:room>') from . import datapackage, generate, room, user # trigger registration
def room_info(room: UUID):
room = Room.get(id=room)
if room is None:
return abort(404)
def supports_apdeltapatch(game: str):
return game in worlds.Files.AutoPatchRegister.patch_types
downloads = []
for slot in sorted(room.seed.slots):
if slot.data and not supports_apdeltapatch(slot.game):
slot_download = {
"slot": slot.player_id,
"download": url_for("download_slot_file", room_id=room.id, player_id=slot.player_id)
}
downloads.append(slot_download)
elif slot.data:
slot_download = {
"slot": slot.player_id,
"download": url_for("download_patch", patch_id=slot.id, room_id=room.id)
}
downloads.append(slot_download)
return {
"tracker": room.tracker,
"players": get_players(room.seed),
"last_port": room.last_port,
"last_activity": room.last_activity,
"timeout": room.timeout,
"downloads": downloads,
}
from . import generate, user, datapackage # trigger registration

42
WebHostLib/api/room.py Normal file
View File

@@ -0,0 +1,42 @@
from typing import Any, Dict
from uuid import UUID
from flask import abort, url_for
import worlds.Files
from . import api_endpoints, get_players
from ..models import Room
@api_endpoints.route('/room_status/<suuid:room_id>')
def room_info(room_id: UUID) -> Dict[str, Any]:
room = Room.get(id=room_id)
if room is None:
return abort(404)
def supports_apdeltapatch(game: str) -> bool:
return game in worlds.Files.AutoPatchRegister.patch_types
downloads = []
for slot in sorted(room.seed.slots):
if slot.data and not supports_apdeltapatch(slot.game):
slot_download = {
"slot": slot.player_id,
"download": url_for("download_slot_file", room_id=room.id, player_id=slot.player_id)
}
downloads.append(slot_download)
elif slot.data:
slot_download = {
"slot": slot.player_id,
"download": url_for("download_patch", patch_id=slot.id, room_id=room.id)
}
downloads.append(slot_download)
return {
"tracker": room.tracker,
"players": get_players(room.seed),
"last_port": room.last_port,
"last_activity": room.last_activity,
"timeout": room.timeout,
"downloads": downloads,
}

View File

@@ -81,6 +81,7 @@ def start_generation(options: Dict[str, Union[dict, str]], meta: Dict[str, Any])
elif len(gen_options) > app.config["MAX_ROLL"]: elif len(gen_options) > app.config["MAX_ROLL"]:
flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players. " flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players. "
f"If you have a larger group, please generate it yourself and upload it.") f"If you have a larger group, please generate it yourself and upload it.")
return redirect(url_for(request.endpoint, **(request.view_args or {})))
elif len(gen_options) >= app.config["JOB_THRESHOLD"]: elif len(gen_options) >= app.config["JOB_THRESHOLD"]:
gen = Generation( gen = Generation(
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}), options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
@@ -134,6 +135,7 @@ def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=Non
{"bosses", "items", "connections", "texts"})) {"bosses", "items", "connections", "texts"}))
erargs.skip_prog_balancing = False erargs.skip_prog_balancing = False
erargs.skip_output = False erargs.skip_output = False
erargs.csv_output = False
name_counter = Counter() name_counter = Counter()
for player, (playerfile, settings) in enumerate(gen_options.items(), 1): for player, (playerfile, settings) in enumerate(gen_options.items(), 1):

View File

@@ -5,6 +5,7 @@ from typing import Any, IO, Dict, Iterator, List, Tuple, Union
import jinja2.exceptions import jinja2.exceptions
from flask import request, redirect, url_for, render_template, Response, session, abort, send_from_directory from flask import request, redirect, url_for, render_template, Response, session, abort, send_from_directory
from pony.orm import count, commit, db_session from pony.orm import count, commit, db_session
from werkzeug.utils import secure_filename
from worlds.AutoWorld import AutoWorldRegister from worlds.AutoWorld import AutoWorldRegister
from . import app, cache from . import app, cache
@@ -69,14 +70,28 @@ def tutorial_landing():
@app.route('/faq/<string:lang>/') @app.route('/faq/<string:lang>/')
@cache.cached() @cache.cached()
def faq(lang): def faq(lang: str):
return render_template("faq.html", lang=lang) import markdown
with open(os.path.join(app.static_folder, "assets", "faq", secure_filename(lang)+".md")) as f:
document = f.read()
return render_template(
"markdown_document.html",
title="Frequently Asked Questions",
html_from_markdown=markdown.markdown(document, extensions=["mdx_breakless_lists"]),
)
@app.route('/glossary/<string:lang>/') @app.route('/glossary/<string:lang>/')
@cache.cached() @cache.cached()
def terms(lang): def glossary(lang: str):
return render_template("glossary.html", lang=lang) import markdown
with open(os.path.join(app.static_folder, "assets", "glossary", secure_filename(lang)+".md")) as f:
document = f.read()
return render_template(
"markdown_document.html",
title="Glossary",
html_from_markdown=markdown.markdown(document, extensions=["mdx_breakless_lists"]),
)
@app.route('/seed/<suuid:seed>') @app.route('/seed/<suuid:seed>')
@@ -132,26 +147,41 @@ def display_log(room: UUID) -> Union[str, Response, Tuple[str, int]]:
return "Access Denied", 403 return "Access Denied", 403
@app.route('/room/<suuid:room>', methods=['GET', 'POST']) @app.post("/room/<suuid:room>")
def host_room_command(room: UUID):
room: Room = Room.get(id=room)
if room is None:
return abort(404)
if room.owner == session["_id"]:
cmd = request.form["cmd"]
if cmd:
Command(room=room, commandtext=cmd)
commit()
return redirect(url_for("host_room", room=room.id))
@app.get("/room/<suuid:room>")
def host_room(room: UUID): def host_room(room: UUID):
room: Room = Room.get(id=room) room: Room = Room.get(id=room)
if room is None: if room is None:
return abort(404) return abort(404)
if request.method == "POST":
if room.owner == session["_id"]:
cmd = request.form["cmd"]
if cmd:
Command(room=room, commandtext=cmd)
commit()
return redirect(url_for("host_room", room=room.id))
now = datetime.datetime.utcnow() now = datetime.datetime.utcnow()
# indicate that the page should reload to get the assigned port # indicate that the page should reload to get the assigned port
should_refresh = not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3) should_refresh = ((not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3))
or room.last_activity < now - datetime.timedelta(seconds=room.timeout))
with db_session: with db_session:
room.last_activity = now # will trigger a spinup, if it's not already running room.last_activity = now # will trigger a spinup, if it's not already running
def get_log(max_size: int = 1024000) -> str: browser_tokens = "Mozilla", "Chrome", "Safari"
automated = ("update" in request.args
or "Discordbot" in request.user_agent.string
or not any(browser_token in request.user_agent.string for browser_token in browser_tokens))
def get_log(max_size: int = 0 if automated else 1024000) -> str:
if max_size == 0:
return ""
try: try:
with open(os.path.join("logs", str(room.id) + ".txt"), "rb") as log: with open(os.path.join("logs", str(room.id) + ".txt"), "rb") as log:
raw_size = 0 raw_size = 0

View File

@@ -9,3 +9,5 @@ bokeh>=3.1.1; python_version <= '3.8'
bokeh>=3.4.3; python_version == '3.9' bokeh>=3.4.3; python_version == '3.9'
bokeh>=3.5.2; python_version >= '3.10' bokeh>=3.5.2; python_version >= '3.10'
markupsafe>=2.1.5 markupsafe>=2.1.5
Markdown>=3.7
mdx-breakless-lists>=1.0.1

View File

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

View File

@@ -1,51 +0,0 @@
window.addEventListener('load', () => {
const tutorialWrapper = document.getElementById('glossary-wrapper');
new Promise((resolve, reject) => {
const ajax = new XMLHttpRequest();
ajax.onreadystatechange = () => {
if (ajax.readyState !== 4) { return; }
if (ajax.status === 404) {
reject("Sorry, the glossary page is not available in that language yet.");
return;
}
if (ajax.status !== 200) {
reject("Something went wrong while loading the glossary.");
return;
}
resolve(ajax.responseText);
};
ajax.open('GET', `${window.location.origin}/static/assets/faq/` +
`glossary_${tutorialWrapper.getAttribute('data-lang')}.md`, true);
ajax.send();
}).then((results) => {
// Populate page with HTML generated from markdown
showdown.setOption('tables', true);
showdown.setOption('strikethrough', true);
showdown.setOption('literalMidWordUnderscores', true);
tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results);
adjustHeaderWidth();
// Reset the id of all header divs to something nicer
for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) {
const headerId = header.innerText.replace(/\s+/g, '-').toLowerCase();
header.setAttribute('id', headerId);
header.addEventListener('click', () => {
window.location.hash = `#${headerId}`;
header.scrollIntoView();
});
}
// Manually scroll the user to the appropriate header if anchor navigation is used
document.fonts.ready.finally(() => {
if (window.location.hash) {
const scrollTarget = document.getElementById(window.location.hash.substring(1));
scrollTarget?.scrollIntoView();
}
});
}).catch((error) => {
console.error(error);
tutorialWrapper.innerHTML =
`<h2>This page is out of logic!</h2>
<h3>Click <a href="${window.location.origin}">here</a> to return to safety.</h3>`;
});
});

View File

@@ -288,6 +288,11 @@ const applyPresets = (presetName) => {
} }
}); });
namedRangeSelect.value = trueValue; namedRangeSelect.value = trueValue;
// It is also possible for a preset to use an unnamed value. If this happens, set the dropdown to "Custom"
if (namedRangeSelect.selectedIndex == -1)
{
namedRangeSelect.value = "custom";
}
} }
// Handle options whose presets are "random" // Handle options whose presets are "random"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 229 KiB

After

Width:  |  Height:  |  Size: 119 KiB

View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 240 38" style="enable-background:new 0 0 240 38;" xml:space="preserve">
<style type="text/css">
.st0{fill:#316B84;}
</style>
<g>
<g>
<path class="st0" d="M59.72,27.96L53.03,4.21L42.25,4.04l1.42,4.37l1.41-0.26l-7.9,24.22h8.44l-0.56-2.27l-0.81-3.27l8.9-5.7
l1.78,11.24h7.97v-4.73L59.72,27.96z M45.62,20.21l3.13-10.84h1.5l2.02,7.44L45.62,20.21z"/>
<path class="st0" d="M78.67,27.96V20.4l-4.11-2.5l3.29-3.78l-0.47-7.46l-2.82-2.45H56.65v5.27l3.81-1.11l2.31,13.36l-2.79,0.73
L61,26.34l5.06-0.52l0.36-6.15l4.32,0.13l3.16,3.62v8.94l12.89,1.49v-5.34L78.67,27.96z M73.27,13.33l-2.18,1.45h-4.64l-0.42-6.57
h5.68l1.55,1.37V13.33z"/>
<polygon class="st0" points="84.65,4.21 93.01,4.21 95.75,6.46 96.26,10.9 92.23,12.43 91.77,9.74 88.97,8.28 85.86,9.82
83.88,15.02 85.51,20.94 88.49,22.38 91.99,20.59 91.99,18.59 96.26,16.96 95.85,22.85 91.81,26.87 84.17,26.55 80.79,23.58
78.87,14.87 80.79,6.94 "/>
<polygon class="st0" points="97.62,4.21 103.33,4.21 102.96,21.08 108.7,20.14 108.34,6.42 113.85,3.28 113.9,19.9 115.75,19.71
115.27,25.86 113.88,25.86 114.27,32.36 108.7,32.36 108.7,26.39 102.96,26.39 102.96,32.36 91.77,33.85 92.2,28.85 97.88,27.96
"/>
<polygon class="st0" points="147.43,28.86 147.43,32.36 162.85,32.36 162.48,25.36 159.5,26 158.89,27.68 154.1,27.24
154.1,21.51 160.81,20.85 160.81,16.48 153.86,16.54 153.86,9.18 158.62,8.43 159.22,9.77 161.85,10.06 162.59,4 147.43,4
147.43,6.54 148.68,7.46 148.68,28.4 "/>
<polygon class="st0" points="163.89,9.24 163.89,4 172.31,4 170.35,26.87 179.55,24.74 179.55,32.36 164.51,32.34 164.65,28.71
165.73,27.84 165.73,9.59 "/>
<path class="st0" d="M193.69,32.36l-0.63-2.51l-2.84-1.89l-4.29-20.14L185.9,4h-11.27l-0.03,3.2l1.87-0.34l-2.79,14.07l-1.37,0.57
v2.85l6.29-1.33l0.4-2.7l4.65-0.89l1.69,12.93H193.69z M179.39,15.11l1.65-6.52l0.89,0.25l0.92,5.45L179.39,15.11z"/>
<polygon class="st0" points="208.47,21.68 210.62,21.12 210.04,18.15 200.51,17.46 198.87,21.13 203.56,21.9 203.32,23.91
200.58,25.19 196.44,23.77 194.48,17.19 196.2,10.02 200.08,8.52 203.31,9.62 202.85,11.75 207.79,13.6 208.83,9.69 204.71,4.21
195.57,4.21 191.24,7.36 189.29,16.87 192.06,27.54 199.03,30.53 203.2,29.3 203.09,32.36 209.01,32.36 209.4,29.95 207.38,28.99
"/>
<path class="st0" d="M230.45,6.26L226.39,4l-8.59-0.01l-4.07,2.86l-2.58,8.9l1.52,11.82l5.61,4.73l7.65,0.01l5.72-4.59l2.47-12.46
L230.45,6.26z M228.23,21.75l-3.95,5.45l-2.16,0.43l-4.6-3.46L216,15.72l2.4-7.02l5.14-0.48l2.97,1.79l1.74,5.83L228.23,21.75z"/>
<path class="st0" d="M116.13,27.48l-0.24,4.88l12.26,0.09l-0.83-5.01l-2.86-0.48l0.14-17.62l2.45-0.42l-0.14-4.85l-10.92,0.36
l0.1,4.6l3.2,0.63l-0.42,17.67L116.13,27.48z"/>
<path class="st0" d="M141.34,4.21l-12.88-0.39v4.26l1.95,0.62v25.15l-1.8,1.41l-0.02,2.63h8.23L136,27.96h6.09l4.57-4.46V7.27
L141.34,4.21z M141.38,20.51l-2.54,1.89l-3.23,0.16L135.4,9.32h3.88l2.1,1.68V20.51z"/>
</g>
<g>
<path class="st0" d="M14.14,11.28c0,0.35-0.02,0.71-0.07,1.05c0.38,0.07,0.76,0.11,1.16,0.11s0.79-0.04,1.16-0.11
c-0.05-0.34-0.07-0.7-0.07-1.05c0-3.23,1.94-6.02,4.72-7.25C20.17,1.68,17.9,0,15.24,0S10.3,1.68,9.42,4.03
C12.2,5.26,14.14,8.04,14.14,11.28z"/>
<path class="st0" d="M18.04,11.28c0,0.16,0.01,0.32,0.02,0.48c0.02,0.3,0.06,0.6,0.13,0.88c0.06,0.28,0.15,0.56,0.25,0.83
c0.11,0.3,0.24,0.58,0.39,0.85c1.42-1.33,3.33-2.15,5.42-2.15s4.01,0.82,5.42,2.15c0.51-0.9,0.79-1.94,0.79-3.04
c0-3.42-2.79-6.22-6.22-6.22c-0.4,0-0.79,0.04-1.16,0.11c-0.28,0.06-0.56,0.13-0.83,0.22c-0.28,0.09-0.56,0.21-0.83,0.35
C19.42,6.77,18.04,8.87,18.04,11.28z"/>
<path class="st0" d="M6.22,12.16c2.1,0,4.01,0.82,5.42,2.15c0.15-0.27,0.28-0.55,0.39-0.85c0.1-0.27,0.19-0.54,0.25-0.83
c0.06-0.28,0.11-0.58,0.13-0.88c0.02-0.15,0.02-0.32,0.02-0.48c0-2.41-1.38-4.51-3.39-5.54C8.77,5.6,8.5,5.49,8.21,5.39
c-0.27-0.1-0.55-0.17-0.83-0.22C7,5.1,6.61,5.06,6.22,5.06C2.79,5.06,0,7.85,0,11.28c0,1.1,0.28,2.14,0.79,3.04
C2.21,12.98,4.12,12.16,6.22,12.16z"/>
<path class="st0" d="M29.21,16.33c-0.18-0.23-0.36-0.44-0.57-0.65c-1.12-1.12-2.67-1.81-4.38-1.81c-1.71,0-3.25,0.69-4.38,1.81
c-0.2,0.2-0.39,0.42-0.56,0.64c-0.18,0.23-0.34,0.47-0.47,0.72c-0.2,0.34-0.36,0.71-0.48,1.09c2.83,1.21,4.81,4.02,4.81,7.28
c0,0.26-0.01,0.52-0.04,0.78c0.37,0.07,0.75,0.1,1.13,0.1c3.43,0,6.22-2.79,6.22-6.22c0-1.11-0.29-2.14-0.8-3.04
C29.54,16.8,29.38,16.56,29.21,16.33z"/>
<path class="st0" d="M12.12,18.14c-0.13-0.38-0.28-0.75-0.48-1.09c-0.14-0.26-0.3-0.5-0.47-0.72c-0.17-0.23-0.36-0.44-0.56-0.64
c-1.12-1.12-2.67-1.81-4.38-1.81s-3.26,0.69-4.38,1.81c-0.21,0.2-0.39,0.42-0.56,0.64c-0.18,0.23-0.34,0.47-0.47,0.72
C0.29,17.94,0,18.98,0,20.08c0,3.43,2.79,6.22,6.22,6.22c0.39,0,0.76-0.03,1.13-0.1c-0.03-0.26-0.04-0.52-0.04-0.78
C7.31,22.15,9.29,19.34,12.12,18.14z"/>
<path class="st0" d="M18.04,19.87c-0.27-0.14-0.55-0.26-0.84-0.35c-0.27-0.09-0.55-0.17-0.84-0.22c-0.37-0.07-0.75-0.1-1.13-0.1
s-0.76,0.03-1.13,0.1c-0.28,0.05-0.57,0.13-0.84,0.22c-0.29,0.1-0.57,0.22-0.84,0.35C10.4,20.9,9.02,23,9.02,25.42
c0,0.07,0,0.14,0.01,0.21c0.01,0.31,0.04,0.61,0.1,0.9c0.05,0.28,0.12,0.57,0.21,0.84c0.82,2.48,3.16,4.27,5.9,4.27
s5.08-1.79,5.9-4.27c0.09-0.27,0.17-0.55,0.21-0.84c0.06-0.3,0.09-0.6,0.1-0.91c0.01-0.07,0.01-0.14,0.01-0.21
C21.45,23,20.07,20.9,18.04,19.87z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -1,66 +1 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?><svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" x="0" y="0" viewBox="0 0 240 38" style="enable-background:new 0 0 240 38" xml:space="preserve"><style>.st0{fill:#316b84}</style><path class="st0" d="M59.72 27.96 53.03 4.21l-10.78-.17 1.42 4.37 1.41-.26-7.9 24.22h8.44l-.56-2.27-.81-3.27 8.9-5.7 1.78 11.24h7.97v-4.73l-3.18.32zm-14.1-7.75 3.13-10.84h1.5l2.02 7.44-6.65 3.4z"/><path class="st0" d="M78.67 27.96V20.4l-4.11-2.5 3.29-3.78-.47-7.46-2.82-2.45H56.65v5.27l3.81-1.11 2.31 13.36-2.79.73L61 26.34l5.06-.52.36-6.15 4.32.13 3.16 3.62v8.94l12.89 1.49v-5.34l-8.12-.55zm-5.4-14.63-2.18 1.45h-4.64l-.42-6.57h5.68l1.55 1.37v3.75z"/><path class="st0" d="M84.65 4.21h8.36l2.74 2.25.51 4.44-4.03 1.53-.46-2.69-2.8-1.46-3.11 1.54-1.98 5.2 1.63 5.92 2.98 1.44 3.5-1.79v-2l4.27-1.63-.41 5.89-4.04 4.02-7.64-.32-3.38-2.97-1.92-8.71 1.92-7.93z"/><path class="st0" d="M97.62 4.21h5.71l-.37 16.87 5.74-.94-.36-13.72 5.51-3.14.05 16.62 1.85-.19-.48 6.15h-1.39l.39 6.5h-5.57v-5.97h-5.74v5.97l-11.19 1.49.43-5 5.68-.89zm49.81 24.65v3.5h15.42l-.37-7-2.98.64-.61 1.68-4.79-.44v-5.73l6.71-.66v-4.37l-6.95.06V9.18l4.76-.75.6 1.34 2.63.29.74-6.06h-15.16v2.54l1.25.92V28.4zm16.46-19.62V4h8.42l-1.96 22.87 9.2-2.13v7.62l-15.04-.02.14-3.63 1.08-.87V9.59z"/><path class="st0" d="m193.69 32.36-.63-2.51-2.84-1.89-4.29-20.14L185.9 4h-11.27l-.03 3.2 1.87-.34-2.79 14.07-1.37.57v2.85l6.29-1.33.4-2.7 4.65-.89 1.69 12.93h8.35zm-14.3-17.25 1.65-6.52.89.25.92 5.45-3.46.82z"/><path class="st0" d="m208.47 21.68 2.15-.56-.58-2.97-9.53-.69-1.64 3.67 4.69.77-.24 2.01-2.74 1.28-4.14-1.42-1.96-6.58 1.72-7.17 3.88-1.5 3.23 1.1-.46 2.13 4.94 1.85 1.04-3.91-4.12-5.48h-9.14l-4.33 3.15-1.95 9.51 2.77 10.67 6.97 2.99 4.17-1.23-.11 3.06h5.92l.39-2.41-2.02-.96zm21.98-15.42L226.39 4l-8.59-.01-4.07 2.86-2.58 8.9 1.52 11.82 5.61 4.73 7.65.01 5.72-4.59 2.47-12.46-3.67-9zm-2.22 15.49-3.95 5.45-2.16.43-4.6-3.46-1.52-8.45 2.4-7.02 5.14-.48 2.97 1.79 1.74 5.83-.02 5.91zm-112.1 5.73-.24 4.88 12.26.09-.83-5.01-2.86-.48.14-17.62 2.45-.42-.14-4.85-10.92.36.1 4.6 3.2.63-.42 17.67-2.74.15zm25.21-23.27-12.88-.39v4.26l1.95.62v25.15l-1.8 1.41-.02 2.63h8.23l-.82-9.93h6.09l4.57-4.46V7.27l-5.32-3.06zm.04 16.3-2.54 1.89-3.23.16-.21-13.24h3.88l2.1 1.68v9.51zM14.14 11.28c0 .35-.02.71-.07 1.05.38.07.76.11 1.16.11s.79-.04 1.16-.11a7.933 7.933 0 0 1 4.65-8.3C20.17 1.68 17.9 0 15.24 0S10.3 1.68 9.42 4.03a7.922 7.922 0 0 1 4.72 7.25z"/><path class="st0" d="M18.04 11.28c0 .16.01.32.02.48.02.3.06.6.13.88.06.28.15.56.25.83.11.3.24.58.39.85 1.42-1.33 3.33-2.15 5.42-2.15s4.01.82 5.42 2.15c.51-.9.79-1.94.79-3.04 0-3.42-2.79-6.22-6.22-6.22-.4 0-.79.04-1.16.11-.28.06-.56.13-.83.22-.28.09-.56.21-.83.35a6.24 6.24 0 0 0-3.38 5.54zm-11.82.88c2.1 0 4.01.82 5.42 2.15.15-.27.28-.55.39-.85.1-.27.19-.54.25-.83.06-.28.11-.58.13-.88.02-.15.02-.32.02-.48a6.23 6.23 0 0 0-3.39-5.54c-.27-.13-.54-.24-.83-.34-.27-.1-.55-.17-.83-.22a6.42 6.42 0 0 0-1.16-.11 6.227 6.227 0 0 0-5.43 9.26 7.885 7.885 0 0 1 5.43-2.16z"/><path class="st0" d="M29.21 16.33c-.18-.23-.36-.44-.57-.65a6.174 6.174 0 0 0-4.38-1.81 6.192 6.192 0 0 0-4.94 2.45c-.18.23-.34.47-.47.72-.2.34-.36.71-.48 1.09a7.923 7.923 0 0 1 4.77 8.06c.37.07.75.1 1.13.1 3.43 0 6.22-2.79 6.22-6.22 0-1.11-.29-2.14-.8-3.04-.15-.23-.31-.47-.48-.7zm-17.09 1.81c-.13-.38-.28-.75-.48-1.09-.14-.26-.3-.5-.47-.72-.17-.23-.36-.44-.56-.64-1.12-1.12-2.67-1.81-4.38-1.81s-3.26.69-4.38 1.81c-.21.2-.39.42-.56.64-.18.23-.34.47-.47.72-.53.89-.82 1.93-.82 3.03 0 3.43 2.79 6.22 6.22 6.22.39 0 .76-.03 1.13-.1a7.902 7.902 0 0 1 4.77-8.06z"/><path class="st0" d="M18.04 19.87c-.27-.14-.55-.26-.84-.35-.27-.09-.55-.17-.84-.22-.37-.07-.75-.1-1.13-.1s-.76.03-1.13.1c-.28.05-.57.13-.84.22-.29.1-.57.22-.84.35a6.225 6.225 0 0 0-3.4 5.55c0 .07 0 .14.01.21.01.31.04.61.1.9.05.28.12.57.21.84.82 2.48 3.16 4.27 5.9 4.27s5.08-1.79 5.9-4.27c.09-.27.17-.55.21-.84.06-.3.09-.6.1-.91.01-.07.01-.14.01-.21a6.24 6.24 0 0 0-3.42-5.54z"/></svg>
<!-- Generator: Adobe Illustrator 26.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 240 38" style="enable-background:new 0 0 240 38;" xml:space="preserve">
<style type="text/css">
.st0{fill:#316B84;}
</style>
<g>
<g>
<path class="st0" d="M59.72,27.96L53.03,4.21L42.25,4.04l1.42,4.37l1.41-0.26l-7.9,24.22h8.44l-0.56-2.27l-0.81-3.27l8.9-5.7
l1.78,11.24h7.97v-4.73L59.72,27.96z M45.62,20.21l3.13-10.84h1.5l2.02,7.44L45.62,20.21z"/>
<path class="st0" d="M78.67,27.96V20.4l-4.11-2.5l3.29-3.78l-0.47-7.46l-2.82-2.45H56.65v5.27l3.81-1.11l2.31,13.36l-2.79,0.73
L61,26.34l5.06-0.52l0.36-6.15l4.32,0.13l3.16,3.62v8.94l12.89,1.49v-5.34L78.67,27.96z M73.27,13.33l-2.18,1.45h-4.64l-0.42-6.57
h5.68l1.55,1.37V13.33z"/>
<polygon class="st0" points="84.65,4.21 93.01,4.21 95.75,6.46 96.26,10.9 92.23,12.43 91.77,9.74 88.97,8.28 85.86,9.82
83.88,15.02 85.51,20.94 88.49,22.38 91.99,20.59 91.99,18.59 96.26,16.96 95.85,22.85 91.81,26.87 84.17,26.55 80.79,23.58
78.87,14.87 80.79,6.94 "/>
<polygon class="st0" points="97.62,4.21 103.33,4.21 102.96,21.08 108.7,20.14 108.34,6.42 113.85,3.28 113.9,19.9 115.75,19.71
115.27,25.86 113.88,25.86 114.27,32.36 108.7,32.36 108.7,26.39 102.96,26.39 102.96,32.36 91.77,33.85 92.2,28.85 97.88,27.96
"/>
<polygon class="st0" points="147.43,28.86 147.43,32.36 162.85,32.36 162.48,25.36 159.5,26 158.89,27.68 154.1,27.24
154.1,21.51 160.81,20.85 160.81,16.48 153.86,16.54 153.86,9.18 158.62,8.43 159.22,9.77 161.85,10.06 162.59,4 147.43,4
147.43,6.54 148.68,7.46 148.68,28.4 "/>
<polygon class="st0" points="163.89,9.24 163.89,4 172.31,4 170.35,26.87 179.55,24.74 179.55,32.36 164.51,32.34 164.65,28.71
165.73,27.84 165.73,9.59 "/>
<path class="st0" d="M193.69,32.36l-0.63-2.51l-2.84-1.89l-4.29-20.14L185.9,4h-11.27l-0.03,3.2l1.87-0.34l-2.79,14.07l-1.37,0.57
v2.85l6.29-1.33l0.4-2.7l4.65-0.89l1.69,12.93H193.69z M179.39,15.11l1.65-6.52l0.89,0.25l0.92,5.45L179.39,15.11z"/>
<polygon class="st0" points="208.47,21.68 210.62,21.12 210.04,18.15 200.51,17.46 198.87,21.13 203.56,21.9 203.32,23.91
200.58,25.19 196.44,23.77 194.48,17.19 196.2,10.02 200.08,8.52 203.31,9.62 202.85,11.75 207.79,13.6 208.83,9.69 204.71,4.21
195.57,4.21 191.24,7.36 189.29,16.87 192.06,27.54 199.03,30.53 203.2,29.3 203.09,32.36 209.01,32.36 209.4,29.95 207.38,28.99
"/>
<path class="st0" d="M230.45,6.26L226.39,4l-8.59-0.01l-4.07,2.86l-2.58,8.9l1.52,11.82l5.61,4.73l7.65,0.01l5.72-4.59l2.47-12.46
L230.45,6.26z M228.23,21.75l-3.95,5.45l-2.16,0.43l-4.6-3.46L216,15.72l2.4-7.02l5.14-0.48l2.97,1.79l1.74,5.83L228.23,21.75z"/>
<path class="st0" d="M116.13,27.48l-0.24,4.88l12.26,0.09l-0.83-5.01l-2.86-0.48l0.14-17.62l2.45-0.42l-0.14-4.85l-10.92,0.36
l0.1,4.6l3.2,0.63l-0.42,17.67L116.13,27.48z"/>
<path class="st0" d="M141.34,4.21l-12.88-0.39v4.26l1.95,0.62v25.15l-1.8,1.41l-0.02,2.63h8.23L136,27.96h6.09l4.57-4.46V7.27
L141.34,4.21z M141.38,20.51l-2.54,1.89l-3.23,0.16L135.4,9.32h3.88l2.1,1.68V20.51z"/>
</g>
<g>
<path class="st0" d="M14.14,11.28c0,0.35-0.02,0.71-0.07,1.05c0.38,0.07,0.76,0.11,1.16,0.11s0.79-0.04,1.16-0.11
c-0.05-0.34-0.07-0.7-0.07-1.05c0-3.23,1.94-6.02,4.72-7.25C20.17,1.68,17.9,0,15.24,0S10.3,1.68,9.42,4.03
C12.2,5.26,14.14,8.04,14.14,11.28z"/>
<path class="st0" d="M18.04,11.28c0,0.16,0.01,0.32,0.02,0.48c0.02,0.3,0.06,0.6,0.13,0.88c0.06,0.28,0.15,0.56,0.25,0.83
c0.11,0.3,0.24,0.58,0.39,0.85c1.42-1.33,3.33-2.15,5.42-2.15s4.01,0.82,5.42,2.15c0.51-0.9,0.79-1.94,0.79-3.04
c0-3.42-2.79-6.22-6.22-6.22c-0.4,0-0.79,0.04-1.16,0.11c-0.28,0.06-0.56,0.13-0.83,0.22c-0.28,0.09-0.56,0.21-0.83,0.35
C19.42,6.77,18.04,8.87,18.04,11.28z"/>
<path class="st0" d="M6.22,12.16c2.1,0,4.01,0.82,5.42,2.15c0.15-0.27,0.28-0.55,0.39-0.85c0.1-0.27,0.19-0.54,0.25-0.83
c0.06-0.28,0.11-0.58,0.13-0.88c0.02-0.15,0.02-0.32,0.02-0.48c0-2.41-1.38-4.51-3.39-5.54C8.77,5.6,8.5,5.49,8.21,5.39
c-0.27-0.1-0.55-0.17-0.83-0.22C7,5.1,6.61,5.06,6.22,5.06C2.79,5.06,0,7.85,0,11.28c0,1.1,0.28,2.14,0.79,3.04
C2.21,12.98,4.12,12.16,6.22,12.16z"/>
<path class="st0" d="M29.21,16.33c-0.18-0.23-0.36-0.44-0.57-0.65c-1.12-1.12-2.67-1.81-4.38-1.81c-1.71,0-3.25,0.69-4.38,1.81
c-0.2,0.2-0.39,0.42-0.56,0.64c-0.18,0.23-0.34,0.47-0.47,0.72c-0.2,0.34-0.36,0.71-0.48,1.09c2.83,1.21,4.81,4.02,4.81,7.28
c0,0.26-0.01,0.52-0.04,0.78c0.37,0.07,0.75,0.1,1.13,0.1c3.43,0,6.22-2.79,6.22-6.22c0-1.11-0.29-2.14-0.8-3.04
C29.54,16.8,29.38,16.56,29.21,16.33z"/>
<path class="st0" d="M12.12,18.14c-0.13-0.38-0.28-0.75-0.48-1.09c-0.14-0.26-0.3-0.5-0.47-0.72c-0.17-0.23-0.36-0.44-0.56-0.64
c-1.12-1.12-2.67-1.81-4.38-1.81s-3.26,0.69-4.38,1.81c-0.21,0.2-0.39,0.42-0.56,0.64c-0.18,0.23-0.34,0.47-0.47,0.72
C0.29,17.94,0,18.98,0,20.08c0,3.43,2.79,6.22,6.22,6.22c0.39,0,0.76-0.03,1.13-0.1c-0.03-0.26-0.04-0.52-0.04-0.78
C7.31,22.15,9.29,19.34,12.12,18.14z"/>
<path class="st0" d="M18.04,19.87c-0.27-0.14-0.55-0.26-0.84-0.35c-0.27-0.09-0.55-0.17-0.84-0.22c-0.37-0.07-0.75-0.1-1.13-0.1
s-0.76,0.03-1.13,0.1c-0.28,0.05-0.57,0.13-0.84,0.22c-0.29,0.1-0.57,0.22-0.84,0.35C10.4,20.9,9.02,23,9.02,25.42
c0,0.07,0,0.14,0.01,0.21c0.01,0.31,0.04,0.61,0.1,0.9c0.05,0.28,0.12,0.57,0.21,0.84c0.82,2.48,3.16,4.27,5.9,4.27
s5.08-1.79,5.9-4.27c0.09-0.27,0.17-0.55,0.21-0.84c0.06-0.3,0.09-0.6,0.1-0.91c0.01-0.07,0.01-0.14,0.01-0.21
C21.45,23,20.07,20.9,18.04,19.87z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 292 KiB

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 161 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 258 B

View File

@@ -58,3 +58,28 @@
overflow-y: auto; overflow-y: auto;
max-height: 400px; max-height: 400px;
} }
.loader{
display: inline-block;
visibility: hidden;
margin-left: 5px;
width: 40px;
aspect-ratio: 4;
--_g: no-repeat radial-gradient(circle closest-side,#fff 90%,#fff0);
background:
var(--_g) 0 50%,
var(--_g) 50% 50%,
var(--_g) 100% 50%;
background-size: calc(100%/3) 100%;
animation: l7 1s infinite linear;
}
.loader.loading{
visibility: visible;
}
@keyframes l7{
33%{background-size:calc(100%/3) 0% ,calc(100%/3) 100%,calc(100%/3) 100%}
50%{background-size:calc(100%/3) 100%,calc(100%/3) 0 ,calc(100%/3) 100%}
66%{background-size:calc(100%/3) 100%,calc(100%/3) 100%,calc(100%/3) 0 }
}

View File

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

View File

@@ -99,14 +99,18 @@
{% if hint.finding_player == player %} {% if hint.finding_player == player %}
<b>{{ player_names_with_alias[(team, hint.finding_player)] }}</b> <b>{{ player_names_with_alias[(team, hint.finding_player)] }}</b>
{% else %} {% else %}
{{ player_names_with_alias[(team, hint.finding_player)] }} <a href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=team, tracked_player=hint.finding_player) }}">
{{ player_names_with_alias[(team, hint.finding_player)] }}
</a>
{% endif %} {% endif %}
</td> </td>
<td> <td>
{% if hint.receiving_player == player %} {% if hint.receiving_player == player %}
<b>{{ player_names_with_alias[(team, hint.receiving_player)] }}</b> <b>{{ player_names_with_alias[(team, hint.receiving_player)] }}</b>
{% else %} {% else %}
{{ player_names_with_alias[(team, hint.receiving_player)] }} <a href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=team, tracked_player=hint.receiving_player) }}">
{{ player_names_with_alias[(team, hint.receiving_player)] }}
</a>
{% endif %} {% endif %}
</td> </td>
<td>{{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }}</td> <td>{{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }}</td>

View File

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

View File

@@ -19,28 +19,30 @@
{% block body %} {% block body %}
{% include 'header/grassHeader.html' %} {% include 'header/grassHeader.html' %}
<div id="host-room"> <div id="host-room">
{% if room.owner == session["_id"] %} <span id="host-room-info">
Room created from <a href="{{ url_for("view_seed", seed=room.seed.id) }}">Seed #{{ room.seed.id|suuid }}</a> {% if room.owner == session["_id"] %}
<br /> Room created from <a href="{{ url_for("view_seed", seed=room.seed.id) }}">Seed #{{ room.seed.id|suuid }}</a>
{% endif %} <br />
{% if room.tracker %} {% endif %}
This room has a <a href="{{ url_for("get_multiworld_tracker", tracker=room.tracker) }}">Multiworld Tracker</a> {% if room.tracker %}
and a <a href="{{ url_for("get_multiworld_sphere_tracker", tracker=room.tracker) }}">Sphere Tracker</a> enabled. This room has a <a href="{{ url_for("get_multiworld_tracker", tracker=room.tracker) }}">Multiworld Tracker</a>
<br /> and a <a href="{{ url_for("get_multiworld_sphere_tracker", tracker=room.tracker) }}">Sphere Tracker</a> enabled.
{% endif %} <br />
The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity. {% endif %}
Should you wish to continue later, The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity.
anyone can simply refresh this page and the server will resume.<br> Should you wish to continue later,
{% if room.last_port == -1 %} anyone can simply refresh this page and the server will resume.<br>
There was an error hosting this Room. Another attempt will be made on refreshing this page. {% if room.last_port == -1 %}
The most likely failure reason is that the multiworld is too old to be loaded now. There was an error hosting this Room. Another attempt will be made on refreshing this page.
{% elif room.last_port %} The most likely failure reason is that the multiworld is too old to be loaded now.
You can connect to this room by using <span class="interactive" {% elif room.last_port %}
data-tooltip="This means address/ip is {{ config['HOST_ADDRESS'] }} and port is {{ room.last_port }}."> You can connect to this room by using <span class="interactive"
'/connect {{ config['HOST_ADDRESS'] }}:{{ room.last_port }}' data-tooltip="This means address/ip is {{ config['HOST_ADDRESS'] }} and port is {{ room.last_port }}.">
</span> '/connect {{ config['HOST_ADDRESS'] }}:{{ room.last_port }}'
in the <a href="{{ url_for("tutorial_landing")}}">client</a>.<br> </span>
{% endif %} in the <a href="{{ url_for("tutorial_landing")}}">client</a>.<br>
{% endif %}
</span>
{{ macros.list_patches_room(room) }} {{ macros.list_patches_room(room) }}
{% if room.owner == session["_id"] %} {% if room.owner == session["_id"] %}
<div style="display: flex; align-items: center;"> <div style="display: flex; align-items: center;">
@@ -49,6 +51,7 @@
<label for="cmd"></label> <label for="cmd"></label>
<input class="form-control" type="text" id="cmd" name="cmd" <input class="form-control" type="text" id="cmd" name="cmd"
placeholder="Server Command. /help to list them, list gets appended to log."> placeholder="Server Command. /help to list them, list gets appended to log.">
<span class="loader"></span>
</div> </div>
</form> </form>
<a href="{{ url_for("display_log", room=room.id) }}"> <a href="{{ url_for("display_log", room=room.id) }}">
@@ -62,6 +65,7 @@
let url = '{{ url_for('display_log', room = room.id) }}'; let url = '{{ url_for('display_log', room = room.id) }}';
let bytesReceived = {{ log_len }}; let bytesReceived = {{ log_len }};
let updateLogTimeout; let updateLogTimeout;
let updateLogImmediately = false;
let awaitingCommandResponse = false; let awaitingCommandResponse = false;
let logger = document.getElementById("logger"); let logger = document.getElementById("logger");
@@ -78,29 +82,36 @@
async function updateLog() { async function updateLog() {
try { try {
let res = await fetch(url, { if (!document.hidden) {
headers: { updateLogImmediately = false;
'Range': `bytes=${bytesReceived}-`, let res = await fetch(url, {
} headers: {
}); 'Range': `bytes=${bytesReceived}-`,
if (res.ok) { }
let text = await res.text(); });
if (text.length > 0) { if (res.ok) {
awaitingCommandResponse = false; let text = await res.text();
if (bytesReceived === 0 || res.status !== 206) { if (text.length > 0) {
logger.innerHTML = ''; awaitingCommandResponse = false;
} if (bytesReceived === 0 || res.status !== 206) {
if (res.status !== 206) { logger.innerHTML = '';
bytesReceived = 0; }
} else { if (res.status !== 206) {
bytesReceived += new Blob([text]).size; bytesReceived = 0;
} } else {
if (logger.innerHTML.endsWith('…')) { bytesReceived += new Blob([text]).size;
logger.innerHTML = logger.innerHTML.substring(0, logger.innerHTML.length - 1); }
} if (logger.innerHTML.endsWith('…')) {
logger.appendChild(document.createTextNode(text)); logger.innerHTML = logger.innerHTML.substring(0, logger.innerHTML.length - 1);
scrollToBottom(logger); }
logger.appendChild(document.createTextNode(text));
scrollToBottom(logger);
let loader = document.getElementById("command-form").getElementsByClassName("loader")[0];
loader.classList.remove("loading");
}
} }
} else {
updateLogImmediately = true;
} }
} }
finally { finally {
@@ -125,20 +136,62 @@
}); });
ev.preventDefault(); // has to happen before first await ev.preventDefault(); // has to happen before first await
form.reset(); form.reset();
let res = await req; let loader = form.getElementsByClassName("loader")[0];
if (res.ok || res.type === 'opaqueredirect') { loader.classList.add("loading");
awaitingCommandResponse = true; try {
window.clearTimeout(updateLogTimeout); let res = await req;
updateLogTimeout = window.setTimeout(updateLog, 100); if (res.ok || res.type === 'opaqueredirect') {
} else { awaitingCommandResponse = true;
window.alert(res.statusText); window.clearTimeout(updateLogTimeout);
updateLogTimeout = window.setTimeout(updateLog, 100);
} else {
loader.classList.remove("loading");
window.alert(res.statusText);
}
} catch (e) {
console.error(e);
loader.classList.remove("loading");
window.alert(e.message);
} }
} }
document.getElementById("command-form").addEventListener("submit", postForm); document.getElementById("command-form").addEventListener("submit", postForm);
updateLogTimeout = window.setTimeout(updateLog, 1000); updateLogTimeout = window.setTimeout(updateLog, 1000);
logger.scrollTop = logger.scrollHeight; logger.scrollTop = logger.scrollHeight;
document.addEventListener("visibilitychange", () => {
if (!document.hidden && updateLogImmediately) {
updateLog();
}
})
</script> </script>
{% endif %} {% endif %}
<script>
function updateInfo() {
let url = new URL(window.location.href);
url.search = "?update";
fetch(url)
.then(res => {
if (!res.ok) {
throw new Error(`HTTP error ${res.status}`);
}
return res.text()
})
.then(text => new DOMParser().parseFromString(text, 'text/html'))
.then(newDocument => {
let el = newDocument.getElementById("host-room-info");
document.getElementById("host-room-info").innerHTML = el.innerHTML;
});
}
if (document.querySelector("meta[http-equiv='refresh']")) {
console.log("Refresh!");
window.addEventListener('load', function () {
for (let i=0; i<3; i++) {
window.setTimeout(updateInfo, Math.pow(2, i) * 2000); // 2, 4, 8s
}
window.stop(); // cancel meta refresh
})
}
</script>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -0,0 +1,13 @@
{% extends 'pageWrapper.html' %}
{% block head %}
{% include 'header/grassHeader.html' %}
<title>{{ title }}</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
{% endblock %}
{% block body %}
<div class="markdown">
{{ html_from_markdown | safe}}
</div>
{% endblock %}

View File

@@ -1,5 +1,21 @@
{% extends 'tablepage.html' %} {% extends 'tablepage.html' %}
{%- macro games(slots) -%}
{%- set gameList = [] -%}
{%- set maxGamesToShow = 10 -%}
{%- for slot in (slots|list|sort(attribute="player_id"))[:maxGamesToShow] -%}
{% set player = "#" + slot["player_id"]|string + " " + slot["player_name"] + " : " + slot["game"] -%}
{% set _ = gameList.append(player) -%}
{%- endfor -%}
{%- if slots|length > maxGamesToShow -%}
{% set _ = gameList.append("... and " + (slots|length - maxGamesToShow)|string + " more") -%}
{%- endif -%}
{{ gameList|join('\n') }}
{%- endmacro -%}
{% block head %} {% block head %}
{{ super() }} {{ super() }}
<title>User Content</title> <title>User Content</title>
@@ -33,10 +49,12 @@
<tr> <tr>
<td><a href="{{ url_for("view_seed", seed=room.seed.id) }}">{{ room.seed.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><a href="{{ url_for("host_room", room=room.id) }}">{{ room.id|suuid }}</a></td>
<td>{{ room.seed.slots|length }}</td> <td title="{{ games(room.seed.slots) }}">
{{ room.seed.slots|length }}
</td>
<td>{{ room.creation_time.strftime("%Y-%m-%d %H:%M") }}</td> <td>{{ room.creation_time.strftime("%Y-%m-%d %H:%M") }}</td>
<td>{{ room.last_activity.strftime("%Y-%m-%d %H:%M") }}</td> <td>{{ room.last_activity.strftime("%Y-%m-%d %H:%M") }}</td>
<td><a href="{{ url_for("disown_room", room=room.id) }}">Delete next maintenance.</td> <td><a href="{{ url_for("disown_room", room=room.id) }}">Delete next maintenance.</a></td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@@ -60,10 +78,15 @@
{% for seed in seeds %} {% for seed in seeds %}
<tr> <tr>
<td><a href="{{ url_for("view_seed", 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 title="{{ games(seed.slots) }}">
{% if seed.multidata %}
{{ seed.slots|length }}
{% else %}
1
{% endif %}
</td> </td>
<td>{{ seed.creation_time.strftime("%Y-%m-%d %H:%M") }}</td> <td>{{ seed.creation_time.strftime("%Y-%m-%d %H:%M") }}</td>
<td><a href="{{ url_for("disown_seed", seed=seed.id) }}">Delete next maintenance.</td> <td><a href="{{ url_for("disown_seed", seed=seed.id) }}">Delete next maintenance.</a></td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>

View File

@@ -268,6 +268,7 @@ Additional arguments added to the [Set](#Set) package that triggered this [SetRe
These packets are sent purely from client to server. They are not accepted by clients. These packets are sent purely from client to server. They are not accepted by clients.
* [Connect](#Connect) * [Connect](#Connect)
* [ConnectUpdate](#ConnectUpdate)
* [Sync](#Sync) * [Sync](#Sync)
* [LocationChecks](#LocationChecks) * [LocationChecks](#LocationChecks)
* [LocationScouts](#LocationScouts) * [LocationScouts](#LocationScouts)
@@ -395,6 +396,7 @@ Some special keys exist with specific return data, all of them have the prefix `
| item_name_groups_{game_name} | dict\[str, list\[str\]\] | item_name_groups belonging to the requested game. | | item_name_groups_{game_name} | dict\[str, list\[str\]\] | item_name_groups belonging to the requested game. |
| location_name_groups_{game_name} | dict\[str, list\[str\]\] | location_name_groups belonging to the requested game. | | location_name_groups_{game_name} | dict\[str, list\[str\]\] | location_name_groups belonging to the requested game. |
| client_status_{team}_{slot} | [ClientStatus](#ClientStatus) | The current game status of the requested player. | | client_status_{team}_{slot} | [ClientStatus](#ClientStatus) | The current game status of the requested player. |
| race_mode | int | 0 if race mode is disabled, and 1 if it's enabled. |
### Set ### Set
Used to write data to the server's data storage, that data can then be shared across worlds or just saved for later. Values for keys in the data storage can be retrieved with a [Get](#Get) package, or monitored with a [SetNotify](#SetNotify) package. Used to write data to the server's data storage, that data can then be shared across worlds or just saved for later. Values for keys in the data storage can be retrieved with a [Get](#Get) package, or monitored with a [SetNotify](#SetNotify) package.
@@ -510,7 +512,7 @@ In JSON this may look like:
| ----- | ----- | | ----- | ----- |
| 0 | Nothing special about this item | | 0 | Nothing special about this item |
| 0b001 | If set, indicates the item can unlock logical advancement | | 0b001 | If set, indicates the item can unlock logical advancement |
| 0b010 | If set, indicates the item is especially useful | | 0b010 | If set, indicates the item is important but not in a way that unlocks advancement |
| 0b100 | If set, indicates the item is a trap | | 0b100 | If set, indicates the item is a trap |
### JSONMessagePart ### JSONMessagePart

View File

@@ -248,8 +248,7 @@ will all have the same ID. Name must not be numeric (must contain at least 1 let
Other classifications include: Other classifications include:
* `filler`: a regular item or trash item * `filler`: a regular item or trash item
* `useful`: item that is especially useful. Cannot be placed on excluded or unreachable locations. When combined with * `useful`: generally quite useful, but not required for anything logical. Cannot be placed on excluded locations
another flag like "progression", it means "an especially useful progression item".
* `trap`: negative impact on the player * `trap`: negative impact on the player
* `skip_balancing`: denotes that an item should not be moved to an earlier sphere for the purpose of balancing (to be * `skip_balancing`: denotes that an item should not be moved to an earlier sphere for the purpose of balancing (to be
combined with `progression`; see below) combined with `progression`; see below)

16
kvui.py
View File

@@ -243,6 +243,9 @@ class ServerLabel(HovererableLabel):
f"\nYou currently have {ctx.hint_points} points." f"\nYou currently have {ctx.hint_points} points."
elif ctx.hint_cost == 0: elif ctx.hint_cost == 0:
text += "\n!hint is free to use." text += "\n!hint is free to use."
if ctx.stored_data and "_read_race_mode" in ctx.stored_data:
text += "\nRace mode is enabled." \
if ctx.stored_data["_read_race_mode"] else "\nRace mode is disabled."
else: else:
text += f"\nYou are not authenticated yet." text += f"\nYou are not authenticated yet."
@@ -536,9 +539,8 @@ class GameManager(App):
# show Archipelago tab if other logging is present # show Archipelago tab if other logging is present
self.tabs.add_widget(panel) self.tabs.add_widget(panel)
hint_panel = TabbedPanelItem(text="Hints") hint_panel = self.add_client_tab("Hints", HintLog(self.json_to_kivy_parser))
self.log_panels["Hints"] = hint_panel.content = HintLog(self.json_to_kivy_parser) self.log_panels["Hints"] = hint_panel.content
self.tabs.add_widget(hint_panel)
if len(self.logging_pairs) == 1: if len(self.logging_pairs) == 1:
self.tabs.default_tab_text = "Archipelago" self.tabs.default_tab_text = "Archipelago"
@@ -572,6 +574,14 @@ class GameManager(App):
return self.container return self.container
def add_client_tab(self, title: str, content: Widget) -> Widget:
"""Adds a new tab to the client window with a given title, and provides a given Widget as its content.
Returns the new tab widget, with the provided content being placed on the tab as content."""
new_tab = TabbedPanelItem(text=title)
new_tab.content = content
self.tabs.add_widget(new_tab)
return new_tab
def update_texts(self, dt): def update_texts(self, dt):
if hasattr(self.tabs.content.children[0], "fix_heights"): if hasattr(self.tabs.content.children[0], "fix_heights"):
self.tabs.content.children[0].fix_heights() # TODO: remove this when Kivy fixes this upstream self.tabs.content.children[0].fix_heights() # TODO: remove this when Kivy fixes this upstream

View File

@@ -5,7 +5,6 @@ import platform
import shutil import shutil
import sys import sys
import sysconfig import sysconfig
import typing
import warnings import warnings
import zipfile import zipfile
import urllib.request import urllib.request
@@ -14,14 +13,14 @@ import json
import threading import threading
import subprocess import subprocess
from collections.abc import Iterable
from hashlib import sha3_512 from hashlib import sha3_512
from pathlib import Path from pathlib import Path
from typing import Dict, Iterable, List, Optional, Sequence, Set, Tuple, Union
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it # This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
requirement = 'cx-Freeze==7.2.0'
try: try:
requirement = 'cx-Freeze==7.2.0'
import pkg_resources import pkg_resources
try: try:
pkg_resources.require(requirement) pkg_resources.require(requirement)
@@ -30,7 +29,7 @@ try:
install_cx_freeze = True install_cx_freeze = True
except ImportError: except ImportError:
install_cx_freeze = True install_cx_freeze = True
pkg_resources = None # type: ignore [assignment] pkg_resources = None # type: ignore[assignment]
if install_cx_freeze: if install_cx_freeze:
# check if pip is available # check if pip is available
@@ -61,7 +60,7 @@ from Cython.Build import cythonize
# On Python < 3.10 LogicMixin is not currently supported. # On Python < 3.10 LogicMixin is not currently supported.
non_apworlds: set = { non_apworlds: Set[str] = {
"A Link to the Past", "A Link to the Past",
"Adventure", "Adventure",
"ArchipIDLE", "ArchipIDLE",
@@ -84,7 +83,7 @@ non_apworlds: set = {
if sys.version_info < (3,10): if sys.version_info < (3,10):
non_apworlds.add("Hollow Knight") non_apworlds.add("Hollow Knight")
def download_SNI(): def download_SNI() -> None:
print("Updating SNI") print("Updating SNI")
machine_to_go = { machine_to_go = {
"x86_64": "amd64", "x86_64": "amd64",
@@ -114,8 +113,8 @@ def download_SNI():
if source_url and source_url.endswith(".zip"): if source_url and source_url.endswith(".zip"):
with urllib.request.urlopen(source_url) as download: with urllib.request.urlopen(source_url) as download:
with zipfile.ZipFile(io.BytesIO(download.read()), "r") as zf: with zipfile.ZipFile(io.BytesIO(download.read()), "r") as zf:
for member in zf.infolist(): for zf_member in zf.infolist():
zf.extract(member, path="SNI") zf.extract(zf_member, path="SNI")
print(f"Downloaded SNI from {source_url}") print(f"Downloaded SNI from {source_url}")
elif source_url and (source_url.endswith(".tar.xz") or source_url.endswith(".tar.gz")): elif source_url and (source_url.endswith(".tar.xz") or source_url.endswith(".tar.gz")):
@@ -129,11 +128,13 @@ def download_SNI():
raise ValueError(f"Unexpected file '{member.name}' in {source_url}") raise ValueError(f"Unexpected file '{member.name}' in {source_url}")
elif member.isdir() and not sni_dir: elif member.isdir() and not sni_dir:
sni_dir = member.name sni_dir = member.name
elif member.isfile() and not sni_dir or not member.name.startswith(sni_dir): elif member.isfile() and not sni_dir or sni_dir and not member.name.startswith(sni_dir):
raise ValueError(f"Expected folder before '{member.name}' in {source_url}") raise ValueError(f"Expected folder before '{member.name}' in {source_url}")
elif member.isfile() and sni_dir: elif member.isfile() and sni_dir:
tf.extract(member) tf.extract(member)
# sadly SNI is in its own folder on non-windows, so we need to rename # sadly SNI is in its own folder on non-windows, so we need to rename
if not sni_dir:
raise ValueError("Did not find SNI in archive")
shutil.rmtree("SNI", True) shutil.rmtree("SNI", True)
os.rename(sni_dir, "SNI") os.rename(sni_dir, "SNI")
print(f"Downloaded SNI from {source_url}") print(f"Downloaded SNI from {source_url}")
@@ -145,7 +146,7 @@ def download_SNI():
print(f"No SNI found for system spec {platform_name} {machine_name}") print(f"No SNI found for system spec {platform_name} {machine_name}")
signtool: typing.Optional[str] signtool: Optional[str]
if os.path.exists("X:/pw.txt"): if os.path.exists("X:/pw.txt"):
print("Using signtool") print("Using signtool")
with open("X:/pw.txt", encoding="utf-8-sig") as f: with open("X:/pw.txt", encoding="utf-8-sig") as f:
@@ -197,13 +198,13 @@ extra_data = ["LICENSE", "data", "EnemizerCLI", "SNI"]
extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else [] extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else []
def remove_sprites_from_folder(folder): def remove_sprites_from_folder(folder: Path) -> None:
for file in os.listdir(folder): for file in os.listdir(folder):
if file != ".gitignore": if file != ".gitignore":
os.remove(folder / file) os.remove(folder / file)
def _threaded_hash(filepath): def _threaded_hash(filepath: Union[str, Path]) -> str:
hasher = sha3_512() hasher = sha3_512()
hasher.update(open(filepath, "rb").read()) hasher.update(open(filepath, "rb").read())
return base64.b85encode(hasher.digest()).decode() return base64.b85encode(hasher.digest()).decode()
@@ -217,11 +218,11 @@ class BuildCommand(setuptools.command.build.build):
yes: bool yes: bool
last_yes: bool = False # used by sub commands of build last_yes: bool = False # used by sub commands of build
def initialize_options(self): def initialize_options(self) -> None:
super().initialize_options() super().initialize_options()
type(self).last_yes = self.yes = False type(self).last_yes = self.yes = False
def finalize_options(self): def finalize_options(self) -> None:
super().finalize_options() super().finalize_options()
type(self).last_yes = self.yes type(self).last_yes = self.yes
@@ -233,27 +234,27 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
('extra-data=', None, 'Additional files to add.'), ('extra-data=', None, 'Additional files to add.'),
] ]
yes: bool yes: bool
extra_data: Iterable # [any] not available in 3.8 extra_data: Iterable[str]
extra_libs: Iterable # work around broken include_files extra_libs: Iterable[str] # work around broken include_files
buildfolder: Path buildfolder: Path
libfolder: Path libfolder: Path
library: Path library: Path
buildtime: datetime.datetime buildtime: datetime.datetime
def initialize_options(self): def initialize_options(self) -> None:
super().initialize_options() super().initialize_options()
self.yes = BuildCommand.last_yes self.yes = BuildCommand.last_yes
self.extra_data = [] self.extra_data = []
self.extra_libs = [] self.extra_libs = []
def finalize_options(self): def finalize_options(self) -> None:
super().finalize_options() super().finalize_options()
self.buildfolder = self.build_exe self.buildfolder = self.build_exe
self.libfolder = Path(self.buildfolder, "lib") self.libfolder = Path(self.buildfolder, "lib")
self.library = Path(self.libfolder, "library.zip") self.library = Path(self.libfolder, "library.zip")
def installfile(self, path, subpath=None, keep_content: bool = False): def installfile(self, path: Path, subpath: Optional[Union[str, Path]] = None, keep_content: bool = False) -> None:
folder = self.buildfolder folder = self.buildfolder
if subpath: if subpath:
folder /= subpath folder /= subpath
@@ -268,7 +269,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
else: else:
print('Warning,', path, 'not found') print('Warning,', path, 'not found')
def create_manifest(self, create_hashes=False): def create_manifest(self, create_hashes: bool = False) -> None:
# Since the setup is now split into components and the manifest is not, # Since the setup is now split into components and the manifest is not,
# it makes most sense to just remove the hashes for now. Not aware of anyone using them. # it makes most sense to just remove the hashes for now. Not aware of anyone using them.
hashes = {} hashes = {}
@@ -290,7 +291,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
json.dump(manifest, open(manifestpath, "wt"), indent=4) json.dump(manifest, open(manifestpath, "wt"), indent=4)
print("Created Manifest") print("Created Manifest")
def run(self): def run(self) -> None:
# start downloading sni asap # start downloading sni asap
sni_thread = threading.Thread(target=download_SNI, name="SNI Downloader") sni_thread = threading.Thread(target=download_SNI, name="SNI Downloader")
sni_thread.start() sni_thread.start()
@@ -341,7 +342,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
# post build steps # post build steps
if is_windows: # kivy_deps is win32 only, linux picks them up automatically if is_windows: # kivy_deps is win32 only, linux picks them up automatically
from kivy_deps import sdl2, glew from kivy_deps import sdl2, glew # type: ignore
for folder in sdl2.dep_bins + glew.dep_bins: for folder in sdl2.dep_bins + glew.dep_bins:
shutil.copytree(folder, self.libfolder, dirs_exist_ok=True) shutil.copytree(folder, self.libfolder, dirs_exist_ok=True)
print(f"copying {folder} -> {self.libfolder}") print(f"copying {folder} -> {self.libfolder}")
@@ -362,7 +363,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
self.installfile(Path(data)) self.installfile(Path(data))
# kivi data files # kivi data files
import kivy import kivy # type: ignore[import-untyped]
shutil.copytree(os.path.join(os.path.dirname(kivy.__file__), "data"), shutil.copytree(os.path.join(os.path.dirname(kivy.__file__), "data"),
self.buildfolder / "data", self.buildfolder / "data",
dirs_exist_ok=True) dirs_exist_ok=True)
@@ -372,7 +373,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
from worlds.AutoWorld import AutoWorldRegister from worlds.AutoWorld import AutoWorldRegister
assert not non_apworlds - set(AutoWorldRegister.world_types), \ assert not non_apworlds - set(AutoWorldRegister.world_types), \
f"Unknown world {non_apworlds - set(AutoWorldRegister.world_types)} designated for .apworld" f"Unknown world {non_apworlds - set(AutoWorldRegister.world_types)} designated for .apworld"
folders_to_remove: typing.List[str] = [] folders_to_remove: List[str] = []
disabled_worlds_folder = "worlds_disabled" disabled_worlds_folder = "worlds_disabled"
for entry in os.listdir(disabled_worlds_folder): for entry in os.listdir(disabled_worlds_folder):
if os.path.isdir(os.path.join(disabled_worlds_folder, entry)): if os.path.isdir(os.path.join(disabled_worlds_folder, entry)):
@@ -393,7 +394,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
shutil.rmtree(world_directory) shutil.rmtree(world_directory)
shutil.copyfile("meta.yaml", self.buildfolder / "Players" / "Templates" / "meta.yaml") shutil.copyfile("meta.yaml", self.buildfolder / "Players" / "Templates" / "meta.yaml")
try: try:
from maseya import z3pr from maseya import z3pr # type: ignore[import-untyped]
except ImportError: except ImportError:
print("Maseya Palette Shuffle not found, skipping data files.") print("Maseya Palette Shuffle not found, skipping data files.")
else: else:
@@ -444,16 +445,16 @@ class AppImageCommand(setuptools.Command):
("app-exec=", None, "The application to run inside the image."), ("app-exec=", None, "The application to run inside the image."),
("yes", "y", 'Answer "yes" to all questions.'), ("yes", "y", 'Answer "yes" to all questions.'),
] ]
build_folder: typing.Optional[Path] build_folder: Optional[Path]
dist_file: typing.Optional[Path] dist_file: Optional[Path]
app_dir: typing.Optional[Path] app_dir: Optional[Path]
app_name: str app_name: str
app_exec: typing.Optional[Path] app_exec: Optional[Path]
app_icon: typing.Optional[Path] # source file app_icon: Optional[Path] # source file
app_id: str # lower case name, used for icon and .desktop app_id: str # lower case name, used for icon and .desktop
yes: bool yes: bool
def write_desktop(self): def write_desktop(self) -> None:
assert self.app_dir, "Invalid app_dir" assert self.app_dir, "Invalid app_dir"
desktop_filename = self.app_dir / f"{self.app_id}.desktop" desktop_filename = self.app_dir / f"{self.app_id}.desktop"
with open(desktop_filename, 'w', encoding="utf-8") as f: with open(desktop_filename, 'w', encoding="utf-8") as f:
@@ -468,7 +469,7 @@ class AppImageCommand(setuptools.Command):
))) )))
desktop_filename.chmod(0o755) desktop_filename.chmod(0o755)
def write_launcher(self, default_exe: Path): def write_launcher(self, default_exe: Path) -> None:
assert self.app_dir, "Invalid app_dir" assert self.app_dir, "Invalid app_dir"
launcher_filename = self.app_dir / "AppRun" launcher_filename = self.app_dir / "AppRun"
with open(launcher_filename, 'w', encoding="utf-8") as f: with open(launcher_filename, 'w', encoding="utf-8") as f:
@@ -491,7 +492,7 @@ $APPDIR/$exe "$@"
""") """)
launcher_filename.chmod(0o755) launcher_filename.chmod(0o755)
def install_icon(self, src: Path, name: typing.Optional[str] = None, symlink: typing.Optional[Path] = None): def install_icon(self, src: Path, name: Optional[str] = None, symlink: Optional[Path] = None) -> None:
assert self.app_dir, "Invalid app_dir" assert self.app_dir, "Invalid app_dir"
try: try:
from PIL import Image from PIL import Image
@@ -513,7 +514,8 @@ $APPDIR/$exe "$@"
if symlink: if symlink:
symlink.symlink_to(dest_file.relative_to(symlink.parent)) symlink.symlink_to(dest_file.relative_to(symlink.parent))
def initialize_options(self): def initialize_options(self) -> None:
assert self.distribution.metadata.name
self.build_folder = None self.build_folder = None
self.app_dir = None self.app_dir = None
self.app_name = self.distribution.metadata.name self.app_name = self.distribution.metadata.name
@@ -527,17 +529,22 @@ $APPDIR/$exe "$@"
)) ))
self.yes = False self.yes = False
def finalize_options(self): def finalize_options(self) -> None:
assert self.build_folder
if not self.app_dir: if not self.app_dir:
self.app_dir = self.build_folder.parent / "AppDir" self.app_dir = self.build_folder.parent / "AppDir"
self.app_id = self.app_name.lower() self.app_id = self.app_name.lower()
def run(self): def run(self) -> None:
assert self.build_folder and self.dist_file, "Command not properly set up"
assert (
self.app_icon and self.app_id and self.app_dir and self.app_exec and self.app_name
), "AppImageCommand not properly set up"
self.dist_file.parent.mkdir(parents=True, exist_ok=True) self.dist_file.parent.mkdir(parents=True, exist_ok=True)
if self.app_dir.is_dir(): if self.app_dir.is_dir():
shutil.rmtree(self.app_dir) shutil.rmtree(self.app_dir)
self.app_dir.mkdir(parents=True) self.app_dir.mkdir(parents=True)
opt_dir = self.app_dir / "opt" / self.distribution.metadata.name opt_dir = self.app_dir / "opt" / self.app_name
shutil.copytree(self.build_folder, opt_dir) shutil.copytree(self.build_folder, opt_dir)
root_icon = self.app_dir / f'{self.app_id}{self.app_icon.suffix}' root_icon = self.app_dir / f'{self.app_id}{self.app_icon.suffix}'
self.install_icon(self.app_icon, self.app_id, symlink=root_icon) self.install_icon(self.app_icon, self.app_id, symlink=root_icon)
@@ -548,7 +555,7 @@ $APPDIR/$exe "$@"
subprocess.call(f'ARCH={build_arch} ./appimagetool -n "{self.app_dir}" "{self.dist_file}"', shell=True) subprocess.call(f'ARCH={build_arch} ./appimagetool -n "{self.app_dir}" "{self.dist_file}"', shell=True)
def find_libs(*args: str) -> typing.Sequence[typing.Tuple[str, str]]: def find_libs(*args: str) -> Sequence[Tuple[str, str]]:
"""Try to find system libraries to be included.""" """Try to find system libraries to be included."""
if not args: if not args:
return [] return []
@@ -556,7 +563,7 @@ def find_libs(*args: str) -> typing.Sequence[typing.Tuple[str, str]]:
arch = build_arch.replace('_', '-') arch = build_arch.replace('_', '-')
libc = 'libc6' # we currently don't support musl libc = 'libc6' # we currently don't support musl
def parse(line): def parse(line: str) -> Tuple[Tuple[str, str, str], str]:
lib, path = line.strip().split(' => ') lib, path = line.strip().split(' => ')
lib, typ = lib.split(' ', 1) lib, typ = lib.split(' ', 1)
for test_arch in ('x86-64', 'i386', 'aarch64'): for test_arch in ('x86-64', 'i386', 'aarch64'):
@@ -577,26 +584,29 @@ def find_libs(*args: str) -> typing.Sequence[typing.Tuple[str, str]]:
ldconfig = shutil.which("ldconfig") ldconfig = shutil.which("ldconfig")
assert ldconfig, "Make sure ldconfig is in PATH" assert ldconfig, "Make sure ldconfig is in PATH"
data = subprocess.run([ldconfig, "-p"], capture_output=True, text=True).stdout.split("\n")[1:] data = subprocess.run([ldconfig, "-p"], capture_output=True, text=True).stdout.split("\n")[1:]
find_libs.cache = { # type: ignore [attr-defined] find_libs.cache = { # type: ignore[attr-defined]
k: v for k, v in (parse(line) for line in data if "=>" in line) k: v for k, v in (parse(line) for line in data if "=>" in line)
} }
def find_lib(lib, arch, libc): def find_lib(lib: str, arch: str, libc: str) -> Optional[str]:
for k, v in find_libs.cache.items(): cache: Dict[Tuple[str, str, str], str] = getattr(find_libs, "cache")
for k, v in cache.items():
if k == (lib, arch, libc): if k == (lib, arch, libc):
return v return v
for k, v, in find_libs.cache.items(): for k, v, in cache.items():
if k[0].startswith(lib) and k[1] == arch and k[2] == libc: if k[0].startswith(lib) and k[1] == arch and k[2] == libc:
return v return v
return None return None
res = [] res: List[Tuple[str, str]] = []
for arg in args: for arg in args:
# try exact match, empty libc, empty arch, empty arch and libc # try exact match, empty libc, empty arch, empty arch and libc
file = find_lib(arg, arch, libc) file = find_lib(arg, arch, libc)
file = file or find_lib(arg, arch, '') file = file or find_lib(arg, arch, '')
file = file or find_lib(arg, '', libc) file = file or find_lib(arg, '', libc)
file = file or find_lib(arg, '', '') file = file or find_lib(arg, '', '')
if not file:
raise ValueError(f"Could not find lib {arg}")
# resolve symlinks # resolve symlinks
for n in range(0, 5): for n in range(0, 5):
res.append((file, os.path.join('lib', os.path.basename(file)))) res.append((file, os.path.join('lib', os.path.basename(file))))

View File

@@ -59,3 +59,12 @@ class TestOptions(unittest.TestCase):
item_links = {1: ItemLinks.from_any(item_link_group), 2: ItemLinks.from_any(item_link_group)} item_links = {1: ItemLinks.from_any(item_link_group), 2: ItemLinks.from_any(item_link_group)}
for link in item_links.values(): for link in item_links.values():
self.assertEqual(link.value[0], item_link_group[0]) self.assertEqual(link.value[0], item_link_group[0])
def test_pickle_dumps(self):
"""Test options can be pickled into database for WebHost generation"""
import pickle
for gamename, world_type in AutoWorldRegister.world_types.items():
if not world_type.hidden:
for option_key, option in world_type.options_dataclass.type_hints.items():
with self.subTest(game=gamename, option=option_key):
pickle.dumps(option(option.default))

View File

@@ -71,7 +71,7 @@ class TestTwoPlayerMulti(MultiworldTestBase):
for world in self.multiworld.worlds.values(): for world in self.multiworld.worlds.values():
world.options.accessibility.value = Accessibility.option_full world.options.accessibility.value = Accessibility.option_full
self.assertSteps(gen_steps) self.assertSteps(gen_steps)
with self.subTest("filling multiworld", seed=self.multiworld.seed): with self.subTest("filling multiworld", games=world_type.game, seed=self.multiworld.seed):
distribute_items_restrictive(self.multiworld) distribute_items_restrictive(self.multiworld)
call_all(self.multiworld, "post_fill") call_all(self.multiworld, "post_fill")
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game") self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")

View File

@@ -0,0 +1,73 @@
import zipfile
from io import BytesIO
from flask import url_for
from . import TestBase
class TestGenerate(TestBase):
def test_valid_yaml(self) -> None:
"""
Verify that posting a valid yaml will start generating a game.
"""
with self.app.app_context(), self.app.test_request_context():
yaml_data = """
name: Player1
game: Archipelago
Archipelago: {}
"""
response = self.client.post(url_for("generate"),
data={"file": (BytesIO(yaml_data.encode("utf-8")), "test.yaml")},
follow_redirects=True)
self.assertEqual(response.status_code, 200)
self.assertTrue("/seed/" in response.request.path or
"/wait/" in response.request.path,
f"Response did not properly redirect ({response.request.path})")
def test_empty_zip(self) -> None:
"""
Verify that posting an empty zip will give an error.
"""
with self.app.app_context(), self.app.test_request_context():
zip_data = BytesIO()
zipfile.ZipFile(zip_data, "w").close()
zip_data.seek(0)
self.assertGreater(len(zip_data.read()), 0)
zip_data.seek(0)
response = self.client.post(url_for("generate"),
data={"file": (zip_data, "test.zip")},
follow_redirects=True)
self.assertIn("user-message", response.text,
"Request did not call flash()")
self.assertIn("not find any valid files", response.text,
"Response shows unexpected error")
self.assertIn("generate-game-form", response.text,
"Response did not get user back to the form")
def test_too_many_players(self) -> None:
"""
Verify that posting too many players will give an error.
"""
max_roll = self.app.config["MAX_ROLL"]
# validate that max roll has a sensible value, otherwise we probably changed how it works
self.assertIsInstance(max_roll, int)
self.assertGreater(max_roll, 1)
self.assertLess(max_roll, 100)
# create a yaml with max_roll+1 players and watch it fail
with self.app.app_context(), self.app.test_request_context():
yaml_data = "---\n".join([
f"name: Player{n}\n"
"game: Archipelago\n"
"Archipelago: {}\n"
for n in range(1, max_roll + 2)
])
response = self.client.post(url_for("generate"),
data={"file": (BytesIO(yaml_data.encode("utf-8")), "test.yaml")},
follow_redirects=True)
self.assertIn("user-message", response.text,
"Request did not call flash()")
self.assertIn("limited to", response.text,
"Response shows unexpected error")
self.assertIn("generate-game-form", response.text,
"Response did not get user back to the form")

View File

@@ -131,7 +131,8 @@ class TestHostFakeRoom(TestBase):
f.write(text) f.write(text)
with self.app.app_context(), self.app.test_request_context(): with self.app.app_context(), self.app.test_request_context():
response = self.client.get(url_for("host_room", room=self.room_id)) response = self.client.get(url_for("host_room", room=self.room_id),
headers={"User-Agent": "Mozilla/5.0"})
response_text = response.get_data(True) response_text = response.get_data(True)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertIn("href=\"/seed/", response_text) self.assertIn("href=\"/seed/", response_text)

View File

@@ -10,7 +10,7 @@ from dataclasses import make_dataclass
from typing import (Any, Callable, ClassVar, Dict, FrozenSet, List, Mapping, Optional, Set, TextIO, Tuple, from typing import (Any, Callable, ClassVar, Dict, FrozenSet, List, Mapping, Optional, Set, TextIO, Tuple,
TYPE_CHECKING, Type, Union) TYPE_CHECKING, Type, Union)
from Options import item_and_loc_options, OptionGroup, PerGameCommonOptions from Options import item_and_loc_options, ItemsAccessibility, OptionGroup, PerGameCommonOptions
from BaseClasses import CollectionState from BaseClasses import CollectionState
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -342,7 +342,7 @@ class World(metaclass=AutoWorldRegister):
# overridable methods that get called by Main.py, sorted by execution order # overridable methods that get called by Main.py, sorted by execution order
# can also be implemented as a classmethod and called "stage_<original_name>", # 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. # in that case the MultiWorld object is passed as the first argument, and it gets called once for the entire multiworld.
# An example of this can be found in alttp as stage_pre_fill # An example of this can be found in alttp as stage_pre_fill
@classmethod @classmethod
@@ -480,6 +480,7 @@ class World(metaclass=AutoWorldRegister):
group = cls(multiworld, new_player_id) group = cls(multiworld, new_player_id)
group.options = cls.options_dataclass(**{option_key: option.from_any(option.default) group.options = cls.options_dataclass(**{option_key: option.from_any(option.default)
for option_key, option in cls.options_dataclass.type_hints.items()}) for option_key, option in cls.options_dataclass.type_hints.items()})
group.options.accessibility = ItemsAccessibility(ItemsAccessibility.option_items)
return group return group

View File

@@ -223,8 +223,8 @@ async def set_message_interval(ctx: BizHawkContext, value: float) -> None:
raise SyncError(f"Expected response of type SET_MESSAGE_INTERVAL_RESPONSE but got {res['type']}") raise SyncError(f"Expected response of type SET_MESSAGE_INTERVAL_RESPONSE but got {res['type']}")
async def guarded_read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[int, int, str]], async def guarded_read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tuple[int, int, str]],
guard_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> typing.Optional[typing.List[bytes]]: guard_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> typing.Optional[typing.List[bytes]]:
"""Reads an array of bytes at 1 or more addresses if and only if every byte in guard_list matches its expected """Reads an array of bytes at 1 or more addresses if and only if every byte in guard_list matches its expected
value. value.
@@ -266,7 +266,7 @@ async def guarded_read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[
return ret return ret
async def read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[int, int, str]]) -> typing.List[bytes]: async def read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tuple[int, int, str]]) -> typing.List[bytes]:
"""Reads data at 1 or more addresses. """Reads data at 1 or more addresses.
Items in `read_list` should be organized `(address, size, domain)` where Items in `read_list` should be organized `(address, size, domain)` where
@@ -278,8 +278,8 @@ async def read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[int, int
return await guarded_read(ctx, read_list, []) return await guarded_read(ctx, read_list, [])
async def guarded_write(ctx: BizHawkContext, write_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]], async def guarded_write(ctx: BizHawkContext, write_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]],
guard_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> bool: guard_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> bool:
"""Writes data to 1 or more addresses if and only if every byte in guard_list matches its expected value. """Writes data to 1 or more addresses if and only if every byte in guard_list matches its expected value.
Items in `write_list` should be organized `(address, value, domain)` where Items in `write_list` should be organized `(address, value, domain)` where
@@ -316,7 +316,7 @@ async def guarded_write(ctx: BizHawkContext, write_list: typing.List[typing.Tupl
return True return True
async def write(ctx: BizHawkContext, write_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> None: async def write(ctx: BizHawkContext, write_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> None:
"""Writes data to 1 or more addresses. """Writes data to 1 or more addresses.
Items in write_list should be organized `(address, value, domain)` where Items in write_list should be organized `(address, value, domain)` where

View File

@@ -15,7 +15,7 @@ if TYPE_CHECKING:
def launch_client(*args) -> None: def launch_client(*args) -> None:
from .context import launch from .context import launch
launch_subprocess(launch, name="BizHawkClient") launch_subprocess(launch, name="BizHawkClient", args=args)
component = Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client, component = Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client,

View File

@@ -239,11 +239,11 @@ async def _patch_and_run_game(patch_file: str):
logger.exception(exc) logger.exception(exc)
def launch() -> None: def launch(*launch_args) -> None:
async def main(): async def main():
parser = get_base_parser() parser = get_base_parser()
parser.add_argument("patch_file", default="", type=str, nargs="?", help="Path to an Archipelago patch file") parser.add_argument("patch_file", default="", type=str, nargs="?", help="Path to an Archipelago patch file")
args = parser.parse_args() args = parser.parse_args(launch_args)
ctx = BizHawkClientContext(args.connect, args.password) ctx = BizHawkClientContext(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="ServerLoop")

View File

@@ -4,7 +4,7 @@ import websockets
import functools import functools
from copy import deepcopy from copy import deepcopy
from typing import List, Any, Iterable from typing import List, Any, Iterable
from NetUtils import decode, encode, JSONtoTextParser, JSONMessagePart, NetworkItem from NetUtils import decode, encode, JSONtoTextParser, JSONMessagePart, NetworkItem, NetworkPlayer
from MultiServer import Endpoint from MultiServer import Endpoint
from CommonClient import CommonContext, gui_enabled, ClientCommandProcessor, logger, get_base_parser from CommonClient import CommonContext, gui_enabled, ClientCommandProcessor, logger, get_base_parser
@@ -101,12 +101,35 @@ class AHITContext(CommonContext):
def on_package(self, cmd: str, args: dict): def on_package(self, cmd: str, args: dict):
if cmd == "Connected": if cmd == "Connected":
self.connected_msg = encode([args]) json = args
# This data is not needed and causes the game to freeze for long periods of time in large asyncs.
if "slot_info" in json.keys():
json["slot_info"] = {}
if "players" in json.keys():
me: NetworkPlayer
for n in json["players"]:
if n.slot == json["slot"] and n.team == json["team"]:
me = n
break
# Only put our player info in there as we actually need it
json["players"] = [me]
if DEBUG:
print(json)
self.connected_msg = encode([json])
if self.awaiting_info: if self.awaiting_info:
self.server_msgs.append(self.room_info) self.server_msgs.append(self.room_info)
self.update_items() self.update_items()
self.awaiting_info = False self.awaiting_info = False
elif cmd == "RoomUpdate":
# Same story as above
json = args
if "players" in json.keys():
json["players"] = []
self.server_msgs.append(encode(json))
elif cmd == "ReceivedItems": elif cmd == "ReceivedItems":
if args["index"] == 0: if args["index"] == 0:
self.full_inventory.clear() self.full_inventory.clear()
@@ -166,6 +189,17 @@ async def proxy(websocket, path: str = "/", ctx: AHITContext = None):
await ctx.disconnect_proxy() await ctx.disconnect_proxy()
break break
if ctx.auth:
name = msg.get("name", "")
if name != "" and name != ctx.auth:
logger.info("Aborting proxy connection: player name mismatch from save file")
logger.info(f"Expected: {ctx.auth}, got: {name}")
text = encode([{"cmd": "PrintJSON",
"data": [{"text": "Connection aborted - player name mismatch"}]}])
await ctx.send_msgs_proxy(text)
await ctx.disconnect_proxy()
break
if ctx.connected_msg and ctx.is_connected(): if ctx.connected_msg and ctx.is_connected():
await ctx.send_msgs_proxy(ctx.connected_msg) await ctx.send_msgs_proxy(ctx.connected_msg)
ctx.update_items() ctx.update_items()

View File

@@ -152,10 +152,10 @@ def create_dw_regions(world: "HatInTimeWorld"):
for name in annoying_dws: for name in annoying_dws:
world.excluded_dws.append(name) world.excluded_dws.append(name)
if not world.options.DWEnableBonus or world.options.DWAutoCompleteBonuses: if not world.options.DWEnableBonus and world.options.DWAutoCompleteBonuses:
for name in death_wishes: for name in death_wishes:
world.excluded_bonuses.append(name) world.excluded_bonuses.append(name)
elif world.options.DWExcludeAnnoyingBonuses: if world.options.DWExcludeAnnoyingBonuses and not world.options.DWAutoCompleteBonuses:
for name in annoying_bonuses: for name in annoying_bonuses:
world.excluded_bonuses.append(name) world.excluded_bonuses.append(name)

View File

@@ -253,7 +253,8 @@ class HatInTimeWorld(World):
else: else:
item_name = loc.item.name item_name = loc.item.name
shop_item_names.setdefault(str(loc.address), item_name) shop_item_names.setdefault(str(loc.address),
f"{item_name} ({self.multiworld.get_player_name(loc.item.player)})")
slot_data["ShopItemNames"] = shop_item_names slot_data["ShopItemNames"] = shop_item_names

View File

@@ -738,9 +738,7 @@ class AquariaRegions:
self.__connect_regions("Sun Temple left area", "Veil left of sun temple", self.__connect_regions("Sun Temple left area", "Veil left of sun temple",
self.sun_temple_l, self.veil_tr_l) self.sun_temple_l, self.veil_tr_l)
self.__connect_regions("Sun Temple left area", "Sun Temple before boss area", self.__connect_regions("Sun Temple left area", "Sun Temple before boss area",
self.sun_temple_l, self.sun_temple_boss_path, self.sun_temple_l, self.sun_temple_boss_path)
lambda state: _has_light(state, self.player) or
_has_sun_crystal(state, self.player))
self.__connect_regions("Sun Temple before boss area", "Sun Temple boss area", self.__connect_regions("Sun Temple before boss area", "Sun Temple boss area",
self.sun_temple_boss_path, self.sun_temple_boss, self.sun_temple_boss_path, self.sun_temple_boss,
lambda state: _has_energy_attack_item(state, self.player)) lambda state: _has_energy_attack_item(state, self.player))
@@ -775,14 +773,11 @@ class AquariaRegions:
self.abyss_l, self.king_jellyfish_cave, self.abyss_l, self.king_jellyfish_cave,
lambda state: (_has_energy_form(state, self.player) and lambda state: (_has_energy_form(state, self.player) and
_has_beast_form(state, self.player)) or _has_beast_form(state, self.player)) or
_has_dual_form(state, self.player)) _has_dual_form(state, self.player))
self.__connect_regions("Abyss left area", "Abyss right area", self.__connect_regions("Abyss left area", "Abyss right area",
self.abyss_l, self.abyss_r) self.abyss_l, self.abyss_r)
self.__connect_one_way_regions("Abyss right area", "Abyss right area, transturtle", self.__connect_regions("Abyss right area", "Abyss right area, transturtle",
self.abyss_r, self.abyss_r_transturtle) self.abyss_r, self.abyss_r_transturtle)
self.__connect_one_way_regions("Abyss right area, transturtle", "Abyss right area",
self.abyss_r_transturtle, self.abyss_r,
lambda state: _has_light(state, self.player))
self.__connect_regions("Abyss right area", "Inside the whale", self.__connect_regions("Abyss right area", "Inside the whale",
self.abyss_r, self.whale, self.abyss_r, self.whale,
lambda state: _has_spirit_form(state, self.player) and lambda state: _has_spirit_form(state, self.player) and
@@ -1092,12 +1087,10 @@ class AquariaRegions:
lambda state: _has_light(state, self.player)) lambda state: _has_light(state, self.player))
add_rule(self.multiworld.get_entrance("Open Water bottom left area to Abyss left area", self.player), add_rule(self.multiworld.get_entrance("Open Water bottom left area to Abyss left area", self.player),
lambda state: _has_light(state, self.player)) lambda state: _has_light(state, self.player))
add_rule(self.multiworld.get_entrance("Sun Temple left area to Sun Temple right area", self.player),
lambda state: _has_light(state, self.player) or _has_sun_crystal(state, self.player))
add_rule(self.multiworld.get_entrance("Sun Temple right area to Sun Temple left area", self.player),
lambda state: _has_light(state, self.player) or _has_sun_crystal(state, self.player))
add_rule(self.multiworld.get_entrance("Veil left of sun temple to Sun Temple left area", self.player), add_rule(self.multiworld.get_entrance("Veil left of sun temple to Sun Temple left area", self.player),
lambda state: _has_light(state, self.player) or _has_sun_crystal(state, self.player)) lambda state: _has_light(state, self.player) or _has_sun_crystal(state, self.player))
add_rule(self.multiworld.get_entrance("Abyss right area, transturtle to Abyss right area", self.player),
lambda state: _has_light(state, self.player))
def __adjusting_manual_rules(self) -> None: def __adjusting_manual_rules(self) -> None:
add_rule(self.multiworld.get_location("Mithalas Cathedral, Mithalan Dress", self.player), add_rule(self.multiworld.get_location("Mithalas Cathedral, Mithalan Dress", self.player),
@@ -1151,6 +1144,10 @@ class AquariaRegions:
lambda state: state.has("Sun God beated", self.player)) lambda state: state.has("Sun God beated", self.player))
add_rule(self.multiworld.get_location("The Body center area, breaking Li's cage", self.player), add_rule(self.multiworld.get_location("The Body center area, breaking Li's cage", self.player),
lambda state: _has_tongue_cleared(state, self.player)) lambda state: _has_tongue_cleared(state, self.player))
add_rule(self.multiworld.get_location(
"Open Water top right area, bulb in the small path before Mithalas",
self.player), lambda state: _has_bind_song(state, self.player)
)
def __no_progression_hard_or_hidden_location(self) -> None: def __no_progression_hard_or_hidden_location(self) -> None:
self.multiworld.get_location("Energy Temple boss area, Fallen God Tooth", self.multiworld.get_location("Energy Temple boss area, Fallen God Tooth",

View File

@@ -130,12 +130,13 @@ class AquariaWorld(World):
return result return result
def __pre_fill_item(self, item_name: str, location_name: str, precollected) -> None: def __pre_fill_item(self, item_name: str, location_name: str, precollected,
itemClassification: ItemClassification = ItemClassification.useful) -> None:
"""Pre-assign an item to a location""" """Pre-assign an item to a location"""
if item_name not in precollected: if item_name not in precollected:
self.exclude.append(item_name) self.exclude.append(item_name)
data = item_table[item_name] data = item_table[item_name]
item = AquariaItem(item_name, ItemClassification.useful, data.id, self.player) item = AquariaItem(item_name, itemClassification, data.id, self.player)
self.multiworld.get_location(location_name, self.player).place_locked_item(item) self.multiworld.get_location(location_name, self.player).place_locked_item(item)
def get_filler_item_name(self): def get_filler_item_name(self):
@@ -164,7 +165,8 @@ class AquariaWorld(World):
self.__pre_fill_item("Transturtle Abyss right", "Abyss right area, Transturtle", precollected) self.__pre_fill_item("Transturtle Abyss right", "Abyss right area, Transturtle", precollected)
self.__pre_fill_item("Transturtle Final Boss", "Final Boss area, Transturtle", precollected) self.__pre_fill_item("Transturtle Final Boss", "Final Boss area, Transturtle", precollected)
# The last two are inverted because in the original game, they are special turtle that communicate directly # The last two are inverted because in the original game, they are special turtle that communicate directly
self.__pre_fill_item("Transturtle Simon Says", "Arnassi Ruins, Transturtle", precollected) self.__pre_fill_item("Transturtle Simon Says", "Arnassi Ruins, Transturtle", precollected,
ItemClassification.progression)
self.__pre_fill_item("Transturtle Arnassi Ruins", "Simon Says area, Transturtle", precollected) self.__pre_fill_item("Transturtle Arnassi Ruins", "Simon Says area, Transturtle", precollected)
for name, data in item_table.items(): for name, data in item_table.items():
if name not in self.exclude: if name not in self.exclude:
@@ -212,4 +214,8 @@ class AquariaWorld(World):
"skip_first_vision": bool(self.options.skip_first_vision.value), "skip_first_vision": bool(self.options.skip_first_vision.value),
"unconfine_home_water_energy_door": self.options.unconfine_home_water.value in [1, 3], "unconfine_home_water_energy_door": self.options.unconfine_home_water.value in [1, 3],
"unconfine_home_water_transturtle": self.options.unconfine_home_water.value in [2, 3], "unconfine_home_water_transturtle": self.options.unconfine_home_water.value in [2, 3],
"bind_song_needed_to_get_under_rock_bulb": bool(self.options.bind_song_needed_to_get_under_rock_bulb),
"no_progression_hard_or_hidden_locations": bool(self.options.no_progression_hard_or_hidden_locations),
"light_needed_to_get_to_dark_places": bool(self.options.light_needed_to_get_to_dark_places),
"turtle_randomizer": self.options.turtle_randomizer.value,
} }

View File

@@ -8,6 +8,8 @@
## Optional Software ## Optional Software
- For sending [commands](/tutorial/Archipelago/commands/en) like `!hint`: the TextClient from [the most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases) - For sending [commands](/tutorial/Archipelago/commands/en) like `!hint`: the TextClient from [the most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases)
- [Aquaria AP Tracker](https://github.com/palex00/aquaria-ap-tracker/releases/latest), for use with
[PopTracker](https://github.com/black-sliver/PopTracker/releases/latest)
## Installation and execution Procedures ## Installation and execution Procedures
@@ -113,3 +115,16 @@ sure that your executable has executable permission:
```bash ```bash
chmod +x aquaria_randomizer chmod +x aquaria_randomizer
``` ```
## Auto-Tracking
Aquaria has a fully functional map tracker that supports auto-tracking.
1. Download [Aquaria AP Tracker](https://github.com/palex00/aquaria-ap-tracker/releases/latest) and
[PopTracker](https://github.com/black-sliver/PopTracker/releases/latest).
2. Put the tracker pack into /packs/ in your PopTracker install.
3. Open PopTracker, and load the Aquaria pack.
4. For autotracking, click on the "AP" symbol at the top.
5. Enter the Archipelago server address (the one you connected your client to), slot name, and password.
This pack will automatically prompt you to update if one is available.

View File

@@ -2,9 +2,14 @@
## Logiciels nécessaires ## Logiciels nécessaires
- Le jeu Aquaria original (trouvable sur la majorité des sites de ventes de jeux vidéo en ligne) - Une copie du jeu Aquaria non-modifiée (disponible sur la majorité des sites de ventes de jeux vidéos en ligne)
- Le client Randomizer d'Aquaria [Aquaria randomizer](https://github.com/tioui/Aquaria_Randomizer/releases) - Le client du Randomizer d'Aquaria [Aquaria randomizer]
(https://github.com/tioui/Aquaria_Randomizer/releases)
## Logiciels optionnels
- De manière optionnel, pour pouvoir envoyer des [commandes](/tutorial/Archipelago/commands/en) comme `!hint`: utilisez le client texte de [la version la plus récente d'Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases) - De manière optionnel, pour pouvoir envoyer des [commandes](/tutorial/Archipelago/commands/en) comme `!hint`: utilisez le client texte de [la version la plus récente d'Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
- [Aquaria AP Tracker](https://github.com/palex00/aquaria-ap-tracker/releases/latest), pour utiliser avec [PopTracker](https://github.com/black-sliver/PopTracker/releases/latest)
## Procédures d'installation et d'exécution ## Procédures d'installation et d'exécution
@@ -116,3 +121,15 @@ pour vous assurer que votre fichier est exécutable:
```bash ```bash
chmod +x aquaria_randomizer chmod +x aquaria_randomizer
``` ```
## Tracking automatique
Aquaria a un tracker complet qui supporte le tracking automatique.
1. Téléchargez [Aquaria AP Tracker](https://github.com/palex00/aquaria-ap-tracker/releases/latest) et [PopTracker](https://github.com/black-sliver/PopTracker/releases/latest).
2. Mettre le fichier compressé du tracker dans le sous-répertoire /packs/ du répertoire d'installation de PopTracker.
3. Lancez PopTracker, et ouvrez le pack d'Aquaria.
4. Pour activer le tracking automatique, cliquez sur le symbole "AP" dans le haut de la fenêtre.
5. Entrez l'adresse du serveur Archipelago (le serveur auquel vous avez connecté le client), le nom de votre slot, et le mot de passe (si un mot de passe est nécessaire).
Le logiciel vous indiquera si une mise à jour du pack est disponible.

View File

@@ -199,8 +199,6 @@ class BlasphemousWorld(World):
self.multiworld.itempool += pool self.multiworld.itempool += pool
def pre_fill(self):
self.place_items_from_dict(unrandomized_dict) self.place_items_from_dict(unrandomized_dict)
if self.options.thorn_shuffle == "vanilla": if self.options.thorn_shuffle == "vanilla":
@@ -335,4 +333,4 @@ class BlasphemousItem(Item):
class BlasphemousLocation(Location): class BlasphemousLocation(Location):
game: str = "Blasphemous" game: str = "Blasphemous"

View File

@@ -125,6 +125,6 @@ class BumpStikWorld(World):
lambda state: state.has("Hazard Bumper", self.player, 25) lambda state: state.has("Hazard Bumper", self.player, 25)
self.multiworld.completion_condition[self.player] = \ self.multiworld.completion_condition[self.player] = \
lambda state: state.has("Booster Bumper", self.player, 5) and \ lambda state: state.has_all_counts({"Booster Bumper": 5, "Treasure Bumper": 32, "Hazard Bumper": 25}, \
state.has("Treasure Bumper", self.player, 32) self.player)

View File

@@ -63,6 +63,9 @@ all_bosses = [
DS3BossInfo("Deacons of the Deep", 3500800, locations = { DS3BossInfo("Deacons of the Deep", 3500800, locations = {
"CD: Soul of the Deacons of the Deep", "CD: Soul of the Deacons of the Deep",
"CD: Small Doll - boss drop", "CD: Small Doll - boss drop",
"CD: Archdeacon White Crown - boss room after killing boss",
"CD: Archdeacon Holy Garb - boss room after killing boss",
"CD: Archdeacon Skirt - boss room after killing boss",
"FS: Hawkwood's Shield - gravestone after Hawkwood leaves", "FS: Hawkwood's Shield - gravestone after Hawkwood leaves",
}), }),
DS3BossInfo("Abyss Watchers", 3300801, before_storm_ruler = True, locations = { DS3BossInfo("Abyss Watchers", 3300801, before_storm_ruler = True, locations = {

View File

@@ -89,6 +89,7 @@ class DarkSouls3World(World):
self.all_excluded_locations = set() self.all_excluded_locations = set()
def generate_early(self) -> None: def generate_early(self) -> None:
self.created_regions = set()
self.all_excluded_locations.update(self.options.exclude_locations.value) self.all_excluded_locations.update(self.options.exclude_locations.value)
# Inform Universal Tracker where Yhorm is being randomized to. # Inform Universal Tracker where Yhorm is being randomized to.
@@ -294,6 +295,7 @@ class DarkSouls3World(World):
new_region.locations.append(new_location) new_region.locations.append(new_location)
self.multiworld.regions.append(new_region) self.multiworld.regions.append(new_region)
self.created_regions.add(region_name)
return new_region return new_region
def create_items(self) -> None: def create_items(self) -> None:
@@ -612,9 +614,7 @@ class DarkSouls3World(World):
self._add_entrance_rule("Painted World of Ariandel (Before Contraption)", "Basin of Vows") self._add_entrance_rule("Painted World of Ariandel (Before Contraption)", "Basin of Vows")
# Define the access rules to some specific locations # Define the access rules to some specific locations
if self._is_location_available("FS: Lift Chamber Key - Leonhard"): self._add_location_rule("HWL: Red Eye Orb - wall tower, miniboss", "Lift Chamber Key")
self._add_location_rule("HWL: Red Eye Orb - wall tower, miniboss",
"Lift Chamber Key")
self._add_location_rule("ID: Bellowing Dragoncrest Ring - drop from B1 towards pit", self._add_location_rule("ID: Bellowing Dragoncrest Ring - drop from B1 towards pit",
"Jailbreaker's Key") "Jailbreaker's Key")
self._add_location_rule("ID: Covetous Gold Serpent Ring - Siegward's cell", "Old Cell Key") self._add_location_rule("ID: Covetous Gold Serpent Ring - Siegward's cell", "Old Cell Key")
@@ -1307,7 +1307,7 @@ class DarkSouls3World(World):
def _add_entrance_rule(self, region: str, rule: Union[CollectionRule, str]) -> None: def _add_entrance_rule(self, region: str, rule: Union[CollectionRule, str]) -> None:
"""Sets a rule for the entrance to the given region.""" """Sets a rule for the entrance to the given region."""
assert region in location_tables assert region in location_tables
if not any(region == reg for reg in self.multiworld.regions.region_cache[self.player]): return if region not in self.created_regions: return
if isinstance(rule, str): if isinstance(rule, str):
if " -> " not in rule: if " -> " not in rule:
assert item_dictionary[rule].classification == ItemClassification.progression assert item_dictionary[rule].classification == ItemClassification.progression

View File

@@ -3,7 +3,7 @@
## Required Software ## Required Software
- [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/) - [Dark Souls III](https://store.steampowered.com/app/374320/DARK_SOULS_III/)
- [Dark Souls III AP Client](https://github.com/Marechal-L/Dark-Souls-III-Archipelago-client/releases) - [Dark Souls III AP Client](https://github.com/nex3/Dark-Souls-III-Archipelago-client/releases/latest)
## Optional Software ## Optional Software
@@ -11,8 +11,9 @@
## Setting Up ## Setting Up
First, download the client from the link above. It doesn't need to go into any particular directory; First, download the client from the link above (`DS3.Archipelago.*.zip`). It doesn't need to go
it'll automatically locate _Dark Souls III_ in your Steam installation folder. into any particular directory; it'll automatically locate _Dark Souls III_ in your Steam
installation folder.
Version 3.0.0 of the randomizer _only_ supports the latest version of _Dark Souls III_, 1.15.2. This Version 3.0.0 of the randomizer _only_ supports the latest version of _Dark Souls III_, 1.15.2. This
is the latest version, so you don't need to do any downpatching! However, if you've already is the latest version, so you don't need to do any downpatching! However, if you've already
@@ -35,8 +36,9 @@ randomized item and (optionally) enemy locations. You only need to do this once
To run _Dark Souls III_ in Archipelago mode: To run _Dark Souls III_ in Archipelago mode:
1. Start Steam. **Do not run in offline mode.** The mod will make sure you don't connect to the 1. Start Steam. **Do not run in offline mode.** Running Steam in offline mode will make certain
DS3 servers, and running Steam in offline mode will make certain scripted invaders fail to spawn. scripted invaders fail to spawn. Instead, change the game itself to offline mode on the menu
screen.
2. Run `launchmod_darksouls3.bat`. This will start _Dark Souls III_ as well as a command prompt that 2. Run `launchmod_darksouls3.bat`. This will start _Dark Souls III_ as well as a command prompt that
you can use to interact with the Archipelago server. you can use to interact with the Archipelago server.
@@ -52,4 +54,21 @@ To run _Dark Souls III_ in Archipelago mode:
### Where do I get a config file? ### Where do I get a config file?
The [Player Options](/games/Dark%20Souls%20III/player-options) page on the website allows you to The [Player Options](/games/Dark%20Souls%20III/player-options) page on the website allows you to
configure your personal options and export them into a config file. configure your personal options and export them into a config file. The [AP client archive] also
includes an options template.
[AP client archive]: https://github.com/nex3/Dark-Souls-III-Archipelago-client/releases/latest
### Does this work with Proton?
The *Dark Souls III* Archipelago randomizer supports running on Linux under Proton. There are a few
things to keep in mind:
* Because `DS3Randomizer.exe` relies on the .NET runtime, you'll need to install
the [.NET Runtime] under **plain [WINE]**, then run `DS3Randomizer.exe` under
plain WINE as well. It won't work as a Proton app!
* To run the game itself, just run `launchmod_darksouls3.bat` under Proton.
[.NET Runtime]: https://dotnet.microsoft.com/en-us/download/dotnet/8.0
[WINE]: https://www.winehq.org/

View File

@@ -2214,13 +2214,13 @@ location_table: Dict[int, LocationDict] = {
'map': 2, 'map': 2,
'index': 217, 'index': 217,
'doom_type': 2006, 'doom_type': 2006,
'region': "Perfect Hatred (E4M2) Blue"}, 'region': "Perfect Hatred (E4M2) Upper"},
351367: {'name': 'Perfect Hatred (E4M2) - Exit', 351367: {'name': 'Perfect Hatred (E4M2) - Exit',
'episode': 4, 'episode': 4,
'map': 2, 'map': 2,
'index': -1, 'index': -1,
'doom_type': -1, 'doom_type': -1,
'region': "Perfect Hatred (E4M2) Blue"}, 'region': "Perfect Hatred (E4M2) Upper"},
351368: {'name': 'Sever the Wicked (E4M3) - Invulnerability', 351368: {'name': 'Sever the Wicked (E4M3) - Invulnerability',
'episode': 4, 'episode': 4,
'map': 3, 'map': 3,

View File

@@ -502,13 +502,12 @@ regions:List[RegionDict] = [
"episode":4, "episode":4,
"connections":[ "connections":[
{"target":"Perfect Hatred (E4M2) Blue","pro":False}, {"target":"Perfect Hatred (E4M2) Blue","pro":False},
{"target":"Perfect Hatred (E4M2) Yellow","pro":False}]}, {"target":"Perfect Hatred (E4M2) Yellow","pro":False},
{"target":"Perfect Hatred (E4M2) Upper","pro":True}]},
{"name":"Perfect Hatred (E4M2) Blue", {"name":"Perfect Hatred (E4M2) Blue",
"connects_to_hub":False, "connects_to_hub":False,
"episode":4, "episode":4,
"connections":[ "connections":[{"target":"Perfect Hatred (E4M2) Upper","pro":False}]},
{"target":"Perfect Hatred (E4M2) Main","pro":False},
{"target":"Perfect Hatred (E4M2) Cave","pro":False}]},
{"name":"Perfect Hatred (E4M2) Yellow", {"name":"Perfect Hatred (E4M2) Yellow",
"connects_to_hub":False, "connects_to_hub":False,
"episode":4, "episode":4,
@@ -518,7 +517,13 @@ regions:List[RegionDict] = [
{"name":"Perfect Hatred (E4M2) Cave", {"name":"Perfect Hatred (E4M2) Cave",
"connects_to_hub":False, "connects_to_hub":False,
"episode":4, "episode":4,
"connections":[]}, "connections":[{"target":"Perfect Hatred (E4M2) Main","pro":False}]},
{"name":"Perfect Hatred (E4M2) Upper",
"connects_to_hub":False,
"episode":4,
"connections":[
{"target":"Perfect Hatred (E4M2) Cave","pro":False},
{"target":"Perfect Hatred (E4M2) Main","pro":False}]},
# Sever the Wicked (E4M3) # Sever the Wicked (E4M3)
{"name":"Sever the Wicked (E4M3) Main", {"name":"Sever the Wicked (E4M3) Main",

View File

@@ -403,9 +403,8 @@ def set_episode4_rules(player, multiworld, pro):
state.has("Chaingun", player, 1)) and (state.has("Plasma gun", player, 1) or state.has("Chaingun", player, 1)) and (state.has("Plasma gun", player, 1) or
state.has("BFG9000", player, 1))) state.has("BFG9000", player, 1)))
set_rule(multiworld.get_entrance("Hell Beneath (E4M1) Main -> Hell Beneath (E4M1) Blue", player), lambda state: set_rule(multiworld.get_entrance("Hell Beneath (E4M1) Main -> Hell Beneath (E4M1) Blue", player), lambda state:
state.has("Shotgun", player, 1) or (state.has("Hell Beneath (E4M1) - Blue skull key", player, 1)) and (state.has("Shotgun", player, 1) or
state.has("Chaingun", player, 1) or state.has("Chaingun", player, 1)))
state.has("Hell Beneath (E4M1) - Blue skull key", player, 1))
# Perfect Hatred (E4M2) # Perfect Hatred (E4M2)
set_rule(multiworld.get_entrance("Hub -> Perfect Hatred (E4M2) Main", player), lambda state: set_rule(multiworld.get_entrance("Hub -> Perfect Hatred (E4M2) Main", player), lambda state:

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