Compare commits

...

2244 Commits
0.1.6 ... 0.4.0

Author SHA1 Message Date
Fabian Dill
8971340a66 Core: update version to 0.4.0 2023-03-31 14:43:05 +02:00
zig-for
30cfd3186c LADX: Fix local paths (#1634) 2023-03-31 14:05:51 +02:00
Chris Wilson
1dc4e2b44b Restore "random" option to weighted-settings (#1635)
* Restore "random" option to weighted-settings, adjust capitalization of hardcoded settings

* Set default value as "random" for Choice, TextChoice, and Toggle options with no default value
2023-03-30 16:01:31 -07:00
alwaysintreble
d5b4a91a13 The Messenger: Have users explicitly remove old version (#1632) 2023-03-30 23:18:56 +02:00
alwaysintreble
bf5282dfa8 add Toggle options back to player settings and remove unnecessary check (#1633) 2023-03-30 16:56:26 -04:00
agilbert1412
4eea91daab Stardew Valley: Improve Documentation (#1631) 2023-03-30 16:25:25 +02:00
Wilhelm Schürmann
20e80d06cf LADX: Add Lua connector for BizHawk (#1579)
This is a Lua script for BizHawk that implements the relevant parts of
the RetroArch networking API used by the Archipelago LADX Client.

socket.lua and core.dll are exact copies of the same files in
data/lua/OOT and various other folders. There is a PR consolidating
these into the base folder, which this commit is anticipating.

LADX "just works"(tm) when this is loaded in Bizhawk.
2023-03-30 15:55:38 +02:00
Rosalie-A
59b78528a9 TLoZ: Fix description and off-by-one error (#1625) 2023-03-30 15:31:16 +02:00
Fabian Dill
cd4fd18706 Core: update modules (#1612) 2023-03-30 15:30:43 +02:00
KernRat
af44c1ba3d Factorio: Fixed outdated energy link troughput in docs (#1626) 2023-03-30 15:30:25 +02:00
Alchav
3ef0a56ec2 Pokémon R/B: Another quiz fix 2023-03-30 15:29:55 +02:00
t3hf1gm3nt
4ff282a384 [TLOZ] Pols Voice Logic Fix (#1630)
* TLOZ: Pols Voice Logic Fix

Was informed that Pols Voice require certain weapons to kill that might not be guaranteed by the starting weapon. This is the only regular enemy in the game that cannot be killed by either any of the starting weapons or bombs which can be bought, so adding this rule should prevent any issues.
2023-03-30 15:29:06 +02:00
Chris Wilson
f3dad894ec Update ArchipIDLE item count to 200, and add a few more items (#1627) 2023-03-29 18:27:35 -07:00
Chris Wilson
a5373e3672 [WebHost] Add support for items-list, locations-list, and custom-list option types to weighted-settings (#1614)
* Add support for items-list, locations-list, and custom-list option types to weighted-settings

* Fix subclass detection for `items-list`, `locations-list`, and `custom-list` in weighted-settings

* Move specially handled options alongside each other in the UI, and split them into logical groupings

* Fix header text and location for Priority an Exclusion locations

* Add universally supported "random" option to `Choice` and `TextChoice` options in weighted-settings

* Update description text for exclusion items to clarify they also prevent helpful items.

* Be technically correct and call them `useful` items.
2023-03-29 14:37:39 -07:00
black-sliver
639606e0be worlds,clients,kvui: use user_path (#1622) 2023-03-29 20:14:45 +02:00
zig-for
bb79073ce7 LADX: Fix autotracking for shop items (#1623) 2023-03-29 14:56:10 +02:00
zig-for
53b3cd029e LADX: Add options for sword music and nag messages (#1621) 2023-03-29 14:48:10 +02:00
zig-for
99bd525c8e LADX: use verify() to verify sprites (#1620) 2023-03-29 14:42:08 +02:00
zig-for
d14ab97849 LADX: Fix gen failures with non-ascii player names, fix missing custom (#1619) 2023-03-29 14:41:11 +02:00
zig-for
f50e85b401 LADX: Fix repeated rupee adds overwriting instead of adding (#1618) 2023-03-29 14:40:19 +02:00
Fabian Dill
b64565594a kvui: block settings menu 2023-03-29 14:39:06 +02:00
JaredWeakStrike
ae7dad8bf9 Fixed Blacklist and python 3.8 support (#1616) 2023-03-28 18:02:06 +02:00
alwaysintreble
b7c74919b7 Spoiler: add player name to sphere 0 items in playthrough (#1607) 2023-03-27 19:42:15 +02:00
Jarno
a7f7f91aaf Timespinner: LOGIC FIX, RC BUG (#1610) 2023-03-27 19:17:50 +02:00
JaredWeakStrike
e62f989ce8 Kh2 rc2 fixes (#1608) 2023-03-27 19:17:06 +02:00
Chris Wilson
21c6c28755 [WebHost] Fix weighted-settings UI incorrectly populating range values (#1602)
* Fix a bug causing weighted-settings UI to incorrectly display range values with no default as having a value of 25.

* Do not set min and max values of range options to zero by default. This causes clutter in saved `.yaml` files.
2023-03-27 08:55:13 -07:00
Alchav
f0403b9c9d Pokémon R/B: Sort fix 2023-03-27 15:28:17 +02:00
Chris Wilson
f09f3663d6 [WebHost] Unify style and behavior of popover and mobile menus (#1596)
* [WebHost] Unify styles for popover and mobile menus. Adjust popover menu to function the same as the mobile menu, and toggle via JS.

* Remove class `first-link` in favor of CSS `:first-child`.

* Adjust mobile menu link font size and padding. Change wording of popover trigger text. Add border-right to popover menu. Change "Upload Host File" to "Host Game"

* Change mobile menu text to "Host Game"
2023-03-27 00:12:10 -04:00
Brooty Johnson
b5bd93c420 fixed duplicate location (#1604)
removed duplicate location for "PW: Vilhelm's Leggings"
2023-03-27 03:04:04 +02:00
Fabian Dill
90813c0f4b Test: explicitly make sure there is no double use of location Name or ID (#1605) 2023-03-26 16:04:13 -07:00
JusticePS
e2c4293a6d Adventure: Fix basepatch access from other working directories (#1600) 2023-03-26 19:34:18 +02:00
Payden K. Pringle
963c33c02a Docs: Update BizHawk Open Script wording to be clearer. (#1599) 2023-03-26 19:34:00 +02:00
black-sliver
7d603e7d8d CI: run build_exe twice (#1598)
* CI: run build_exe twice ...

... to find broken conditional imports

* CI: build again in venv
2023-03-26 00:54:56 +01:00
black-sliver
f2e1495d39 Setup: fix broken cx_freeze import 2023-03-26 00:20:14 +01:00
Tarokarr
7927b2ee25 LADX: Added: Credits for sprite sheets (#1594) 2023-03-25 22:29:59 +01:00
alwaysintreble
4f2b13a674 The Messenger, Docs: Change links for the release (#1595)
* change links to point to my fork

* Add documentation about reconnecting because that's necessary apparently

* change the bug report link

* be non ambiguous

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-03-25 22:11:51 +01:00
black-sliver
ffd7d5da74 ModuleUpdate/Setup: install pkg_resources, check pip, typing and cleanup (#1593)
* ModuleUpdater/setup: install pkg_resources and check for pip

plus minor cleanup in the github actions

* ModuleUpdate/setup: make flake8 happy

* ModuleUpdate/setup: make mypy happier
2023-03-25 19:54:42 +01:00
Trevor L
67eb370200 Blasphemous: Change ending option (#1592) 2023-03-25 19:37:25 +01:00
JaredWeakStrike
4456e36fbb KH2: fixes medal being shadow archive memory address (#1589) 2023-03-25 19:36:48 +01:00
ScootyPuffJr1
7fd9e71b3c TLOZ: Fix Broken Links to Player Settings Page 2023-03-25 19:36:13 +01:00
alwaysintreble
f4a68f1c3d Core: add an everywhere location group (#1582) 2023-03-25 19:35:19 +01:00
Fabian Dill
754a57cf69 WebHost: give active rooms a chance to reclaim their port 2023-03-25 19:34:09 +01:00
lordlou
384577e421 SM: cx_freeze fix (#1584) 2023-03-25 19:30:38 +01:00
alwaysintreble
0ed3865c30 The Messenger: Fix a missing location rule and missing known issue (#1586)
* fix missing rule

* document a missing known issue

* fix a break when shuffle seals is off

* test the thing i just fixed

* invert the if so it's a bit faster
2023-03-25 00:55:24 +01:00
zig-for
77b2ed54a6 LADX: Fix D6 keylogic (#1585)
* fix keylogic for d6

* markup required keys for keylogic

* add test

* Update __init__.py
2023-03-25 00:23:42 +01:00
PoryGone
0386d9f6d2 Fix SA2B Option Display Name (#1591) 2023-03-25 00:22:47 +01:00
Fabian Dill
7e52b6d8bb MultiServer: if there is a hint cost, don't make it 0 (#1581) 2023-03-24 23:14:34 +01:00
kindasneaki
03cf525b2c Webhost: Add dropdown menus (#1553)
* Change archipelago mod download page

* Docs: change connecting to archipelago in RoR2 setup guide

* dropdowns for links

* change some relative sizing

* change links and reorder links

* dropdowns for links

* change some relative sizing

* change links and reorder links

* mobile view was showing on desktop early

* add in missing relative font sizes

* clean up and add a temp downdown img

* move links around

* added cloud border

* move arrow to the left side
2023-03-23 22:11:39 -07:00
zig-for
e1f46d623c LADX: Pass in seed_name and auth separately (#1575) 2023-03-23 21:23:58 +01:00
zig-for
5bb6ff0ce0 LADX: Fixup missing descriptions (#1576) 2023-03-23 21:22:42 +01:00
zig-for
256f493ada LADX: fix web gen (#1574) 2023-03-23 14:53:48 +01:00
Chris Wilson
3ec2d45f4f [WebHost] Improve mobile styles for WebHost (#1571)
* Improve mobile styles for WebHost

- Mobile view works properly on mobile Chrome and Firefox
- Added thin-window view for desktop browsers that have been reduced in size
- Tested in Chrome and Firefox on mobile and desktop

* Improve style for small-width desktop popover
2023-03-23 00:20:34 -07:00
black-sliver
b3895750ab Docs: from source on macOS: update supported python versions 2023-03-22 19:50:30 +01:00
alwaysintreble
7591404151 OOT: set default for adult trade start if the set is empty 2023-03-22 18:39:32 +01:00
JusticePS
d48e1e447f Adventure: implement new game (#1531)
Adds Adventure for the Atari 2600, NTSC version. New randomizer, not based on prior works. Somewhat atypical of current AP rom patch games; The generator does not require the adventure rom, but writes some data to an .apadvn APContainer file that the client uses along with a base bsdiff patch to generate a final rom file.
2023-03-22 15:25:55 +01:00
JaredWeakStrike
206f8cf5ed KH2: fixed bugs of rc1 (#1565)
KH2Client:
- Now checks if the world id is in the list of checks. This fixed sending out stuff on the movie
- Cleaned up unused inports
- Not getting starting invo if the game is not open when you connect to the server

__init__:
-Cleaned up print statements
- Fixed the spoiler log not outputting the right amount of mcguffins after fixing them in the case of the player messing up their settings

Openkh:
-Fixed putting the correct dummy item on levels
2023-03-22 15:21:41 +01:00
Alchav
0c6b1827fe Pokemon R/B: Pokemon Tower Wild Pokemon logic 2023-03-22 15:20:23 +01:00
PoryGone
017f91c1b5 LADX: Remove duplicate Link's Awakening setup (#1573) 2023-03-22 15:19:29 +01:00
Jarno
95b01def6b Timespinner: RC bug, lake serene is dry, when Rising Tides is off (#1570)
* Tinmespinner: RC bug, lake serene is dry, when Rising Tides is off

* yes
2023-03-22 03:32:37 -07:00
black-sliver
5977e401d5 MultiServer: include 'Archipelago' in games, versions and checksums 2023-03-21 23:36:53 +01:00
PoryGone
21a3c74783 SA2B: v2.1 Content Update (#1563)
Changelog:

Features:
- New goal
  - Grand Prix
    - Complete all of the Kart Races to win!
- New optional Location Checks
  - Omosanity (Activating Omochao)
  - Kart Race Mode
- Ring Loss option
  - `Classic` - lose all rings on hit
  - `Modern` - lose 20 rings on hit
  - `OHKO` - instantly die on hit, regardless of ring count (shields still protect you)
- New Trap
  - Pong Trap

Quality of Life:
- SA2B is now distributed as an `.apworld`
- Maximum possible number of Emblems in item pool is increased from 180 to 250
- An indicator now shows on the Stage Select screen when `Cannon's Core` is available
- Certain traps (`Exposition` and `Pong`) are now possible to receive on `Route 101` and `Route 280`
- Certain traps (`Confusion`, `Chaos Control`, `Exposition` and `Pong`) are now possible to receive on `FinalHazard`

Bug Fixes:
- Actually swap Intermediate and Expert Chao Races correctly
- Don't always grant double score for killing Gold Beetles anymore
- Ensure upgrades are applied properly, even when received while dying
- Fix the Message Queue getting disordered when receiving many messages in quick succession
- Fix Logic errors
  - `City Escape - 3` (Hard Logic) now requires no upgrades
  - `Mission Street - Pipe 2` (Hard Logic) now requires no upgrades
  - `Crazy Gadget - Pipe 3` (Hard Logic) now requires no upgrades
  - `Egg Quarters - 3` (Hard Logic) now requires only `Rouge - Mystic Melody`
  - `Mad Space - 5` (Hard Logic) now requires no upgrades

Co-authored-by: RaspberrySpaceJam <tyler.summers@gmail.com>
2023-03-21 21:26:13 +01:00
Zach Parks
2fb9176511 Clique: The greatest game of all time. (#1566) 2023-03-21 21:23:45 +01:00
alwaysintreble
1c69fb3c3c The Messenger: Add more difficult logic options (#1550) 2023-03-21 21:21:27 +01:00
Fabian Dill
91502505a1 WebHost: fix type in states template 2023-03-21 20:05:37 +01:00
black-sliver
01c13ca243 Docs: some clarification in running from source 2023-03-21 18:28:45 +01:00
Fabian Dill
c2a8b842de Core: typo 2023-03-21 15:53:10 +01:00
alwaysintreble
856efebc39 Multiserver: Only update client status for a slot when the first enters and the last leaves (#1358) 2023-03-21 15:50:50 +01:00
Alchav
5a4203649d Pokémon R/B: Quiz fix 2023-03-21 15:46:11 +01:00
Alchav
ddb764a9b6 Pokémon R/B: Skip unnecessary sweeps for events 2023-03-21 15:46:11 +01:00
Rosalie-A
9f65f22fac TLoZ: Installer Info (#1554) 2023-03-21 15:45:31 +01:00
Magnemania
b7ff9b69ba WG: Added Client to Setup (#1556) 2023-03-21 14:18:59 +01:00
Fabian Dill
012e6ba24c WebHost: add Status to MultiTracker 2023-03-21 14:06:38 +01:00
The T
cd9d0bebc8 Add LADX to Readme.md (#1559)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-03-20 23:55:53 +01:00
Brooty Johnson
3fa6588637 TLoZ: Update instructions (#1558)
Added 'Optional Software' to instructions for setting up Z1
2023-03-20 23:45:36 +01:00
JaredWeakStrike
e6d16c905c KH2: Fixed inno_setup and fixed readme.md (#1557) 2023-03-20 23:43:29 +01:00
Fabian Dill
958829d491 Launcher: dynamic Launcher 2023-03-20 23:40:34 +01:00
KonoTyran
9ee37b0ec5 [Minecraft] Update MinecraftClient.py (#1524)
* add support for direct url for mod to download.
make a nice error message if the chosen release channel is empty.

* Rip out call to github api as it is no longer used.

* rework error message to be more descriptive, and provide basic troubleshooting.

fix command line data version not correctly overriding apmc data version.
2023-03-20 11:44:48 -07:00
zig-for
81a239325d Links Awakening: Implement New Game (#1334)
Adds Link's Awakening: DX. Fully imports and forks LADXR, with permission - https://github.com/daid/LADXR
2023-03-20 17:26:03 +01:00
JaredWeakStrike
67bf12369a KH2 game implementation (#1438) 2023-03-20 17:19:55 +01:00
toasterparty
d4b793902f [OC2] Overworld Logic (#1530) 2023-03-20 17:16:19 +01:00
Fabian Dill
6671b21a86 Core: Generic excluded fill (#1511) 2023-03-20 17:10:12 +01:00
el-u
6d13dc4944 lufia2ac: new features, bug fixes, and more (#1549)
### New features

- ***Architect mode***
  Usually the cave is randomized by the game, meaning that each attempt will produce a different dungeon. However, with this new feature the player can, between runs, opt into keeping the same cave. If activated, they will then encounter the same floor layouts, same enemy spawns, and same red chest contents as on their previous attempt.   

- ***Custom item pool***
  Previously, the multiworld item pool consisted entirely of random blue chest items because, well, the permanent checks are blue chests and that's what one would normally get from these. While blue chest items often greatly increase your odds against regular enemies, being able to defeat the Master can be contingent on having an appropriate equipment setup of red chest items (such as Dekar blade) or even enemy drops (such as Hidora rock), most of which cannot normally be obtained from blue chests.
  With the custom item pool option, players now have the freedom to place any cave item into the multiworld itempool for their world.

- ***Enemy floor number, enemy sprite, and enemy movement pattern randomization***
  Experienced players can deduce a lot of information about the opposition they will be facing, for example: Given the current floor number, one can know in advance which of the enemy types will have a chance to spawn on that floor. And when seeing a particular enemy sprite, one can already know which enemy types one might have to face in battle if one were to come in contact with it, and also how that enemy group will move through the dungeon.
  Three new randomization options are added for players who want to spice up their game: one can shuffle which enemy types appear on which floor, one can shuffle which sprite is used by which enemy type, and one can shuffle which movement pattern is used by which sprite.

- ***EXP modifier***
  Just a simple multiplier option to allow people to level up faster. (For technical reasons, the maximum amount of EXP that can be awarded for a single enemy is limited to 65535, but even with the maximum allowed modifier of 500% there are only 6 enemy types in the cave that can reach this cap.)


### Balance change

- ***proportionally adjust chest type distribution to accommodate increased blue chest chance***
  One of the main problems that became apparent in the current version has to do with the distribution of chest contents. The game considers 6 categories, namely: consumable (mostly non-restorative), consumable (restorative), blue chest item, spell, gear, and weapon. Since only blue chests count as multiworld locations, we want to have a mechanism to customize the blue chest chance.
  Given how the chest types are detetermined in game, a naive implementation of an increased blue chest chance causes only the consumable chance to be decreased in return. In practice, this has resulted in some players of worlds with a high blue chest chance struggling (more than usual) to keep their party alive because they were always low on comsumables that restore HP and MP.
  The new algorithm tries to avoid this one-sided effect by having an increase in blue chest chance resulting in a decrease of all other types, calculated in such a way that the relative distribution of the other 5 categories stays (approximately) the same.


### Bug fixes

- ***prevent using party member items if character is already in party***
  This should have been changed at the same time that 6eb00621e3 was made, but oh well... 

- ***fix glitched sprite when opening a chest immediately after receiving an item***
  When opening a chest right after receiving a multiworld item (such that there were two item get animations in the exact same iteration of the game main loop), the item from the chest would display an incorrect sprite in the wrong place. Fixed by cleaning up some relevant memory addresses after getting the multiworld item.

- ***fix death link***
  There was a condition in `deathlink_kill_player` that looked kinda smart (it checked the time against `last_death_link`), but actually wasn't smart at all because `deathlink_kill_player` is executed as an async task and the main thread will update `last_death_link` after creating the task, meaning that whether or not the incoming death link would actually be passed to the game seems to have been up to a race condition. Fixed by simply removing that check.


### Other

- ***add Lufia II Ancient Cave (and SMW) to the network diagram***
  These two games were missing from the SNES sector.

- ***implement get_filler_item_name***
  Place a restorative consumable instead of a completely random item. (Now the only known problem with item links in lufia2ac is... that noone has ever tested item links. But this should be an improvement at least. Anyway, now #1172 can come ;)
  And btw., if you think that the implementation of random selection in this method looks weird, that's because it is indeed weird. (It tries to recreate the algorithm that the game itself uses when it generates a replacement item for a chest that would contain a spell that the party already knows.)

- ***store all options in a dataclass***
  This is basically like using #993 (but without actual support from core). It makes the lufia2ac world code much nicer to maintain because one doesn't have to change 5 different places anymore when adding or renaming an option.

- ***remove master_hp.scale***
  I have to admit: `scale` was a mistake. Never have I seen a single option value cause so many user misconceptions. Some people assume it affects enemies other than the Master; some people assume it affects stats other than HP; and many people will just assume it is a magic option that will somehow counterbalance whatever settings combination they are currently trying to shoot themselves in the foot with.
  On top of that, the `scale` mechanism probably doesn't provide a good user experience even when used for its intended purpose (since having reached floor XY in general doesn't mean you will have the power to deplete XY% of the Masters usual HP; especially given that, due to the randomness of loot, you are never guaranteed to be able to defeat the vanilla Master even when you have cleared 100% of the floors).
  The intended target audience of the `master_hp` option are people who want to fight the Master (and know how to fight it), but also want to lessen (to a degree of their choosing) the harsh dependence on the specific equipment setups that are usually required to win this fight even when having done all 99 floors. They can achieve this by setting the `master_hp` option to a numeric value appropriate for the level of challenge they are seeking. Therefore, nothing of value should be lost by removing the special `scale` value from the `master_hp` option, while at the same time a major source of user confusion will be eliminated.

- ***typing***
  This (combined with the switch to the option dataclass) greatly reduces the typing problems in the lufia2ac world. The remaining typing errors mostly fall into 4 categories:
  1. Lambdas with defaults (which seem to be incorrectly reported as an error due to a mypy bug)
  1. Classmethods that return instances (which could probably be improved using PEP 673 "Self" types, but that would require Python 3.11 as the minimum supported version)
  1. Everything that inherits from TextChoice (which is a typing mess in core)
  1. Everything related to asar.py (which does not have proper typing and lies outside of this project)

## How was this tested?

https://discord.com/channels/731205301247803413/1080852357442707476 and others
2023-03-20 17:04:57 +01:00
Zach Parks
ff9f563d4a Deprecate data_version and introduce checksum for DataPackages. (#684)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-03-20 17:01:08 +01:00
Fabian Dill
d825576f12 Factorio: content update
Energy Link:
  * Transfer and Storage increased by 10X
  * Cost of building increased by roughly 10X
  * This is a way to address their effect on performance, as users now need a tenth of them to get the same throughput, it also differentiates them from Accumulators
5 new Traps:
  * Teleport Trap
  * Grenade Trap
  * Cluster Grenade Trap
  * Artillery Trap
  * Atomic Rocket Trap
When max science is lower than min science, the two are now swapped.
Max Evolution Trap count was changed from 25 -> 10.
New option: Ingredients Offset
  * When creating random recipes, use this many more or less ingredients in the new recipe.
2023-03-15 21:38:32 +01:00
Alchav
5d6184f1fd STS: update slot_seeds to per_slot_randoms 2023-03-15 09:10:35 +01:00
alwaysintreble
e433246f0c The Messenger: docs improvement (#1545)
* The Messenger: docs improvement

* more wordy mod link

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* indent

* revert accidental indent

oop

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-03-14 21:18:55 +01:00
black-sliver
3a190a8fb2 CI: more filters, update CodeQL (#1540)
* CI: fix and more greedy filtering

* CI: only run lint if *.py changed

* CI: only run CodeQL if supported file changed

* CI: fix unittests still triggering for build.yml

* CI: update CodeQL action

* CI: trigger codeql when changing the workflow
2023-03-14 19:29:20 +01:00
Alchav
4b7033fce7 Pokemon R/B: Version 3 final touches (#1542)
* Pokémon R/B: Dexsanity balls

* Pokémon R/B: Early Parcel improvement

* Pokémon R/B: Early Parcel dexsanity stuff only when dexsanity
2023-03-14 18:36:17 +01:00
lordlou
37499b40a1 SMZ3: shop check fix 2 (#1538) 2023-03-14 18:31:51 +01:00
black-sliver
ca2c0e6ce2 CI: update stuff (#1534)
* CI: skip SNI, skip unittests if not needed, run build for setup.py

* CI: update actions

* CI: update upload-artifact

Fixes more warnings
2023-03-14 01:32:00 +01:00
black-sliver
2a28a6de28 Pokemon: apply rename of location_item_name 2023-03-14 01:30:58 +01:00
alwaysintreble
573a1a8402 Core: Add a function to allow worlds to easily allow self-locking items (#1383)
* implement function to allow self locking items for items accessibility

* swap some lttp locations to use new functionality

* lambda capture `item_name` and `location`

* don't lambda capture location

* Revert weird visual indent

* make location.always_allow additive

* fix always_allow rule for multiple items

* don't need to lambda capture item_names

* oop

* move player assignment to the beginning

* always_allow should only be for that player so prevent non_local_items

* messenger got merged so have it use this

* Core: fix doc string indentation for allow_self_locking_items

* Core: fix doc string indentation for allow_self_locking_items, number two

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-03-14 00:55:34 +01:00
Jarno
060ee926e7 Core: type specified missing per_game_common_options (#1509) 2023-03-13 23:45:56 +01:00
Alchav
df55455fc0 Pokémon R/B: Version 3 (#1520)
* Coin items received or found in the Game Corner are now shuffled, locations require Coin Case
* Prizesanity option (shuffle Game Corner Prizes)
* DexSanity option: location checks for marking Pokémon as caught in your Pokédex. Also an option to set all Pokémon in your Pokédex as seen from the start, to aid in locating them.
* Option to randomize the layout of the Rock Tunnel.
* Area 1-to-1 mapping: When one instance of a Wild Pokémon in a given area is randomized, all instances of that Pokémon will be the same. So that if a route had 3 different Pokémon before, it will have 3 after randomization.
* Option to randomize the moves taught by TMs.
* Exact controls for TM/HM compatibility chances.
* Option to randomize Pokémon's pallets or set them based on primary type.
* Added Cinnabar Gym trainers to Trainersanity and randomized the quiz questions and answers. Getting a correct answer will flag the trainer as defeated so that you can obtain the Trainersanity check without defeating the trainer if you answer correctly.
2023-03-13 23:40:55 +01:00
Fabian Dill
4d7bd929bc WebHost: update modules 2023-03-13 21:34:24 +01:00
Fabian Dill
030e41363a Setup: update cx-Freeze 2023-03-13 21:34:07 +01:00
Qwazzy
4bc0e84a7f Docs: SM64 Guide update to explain how to launch the game with batch files (#768)
* Update setup_en.md

Added several sections in regards to opening the completed SM64 build with batch files instead of SM64PCLauncher.

* Update setup_en.md

* Update setup_en.md

* Update setup_en.md

* Update setup_en.md

* Apply suggestions from code review

Co-authored-by: Yussur Mustafa Oraji <N00byKing@hotmail.de>
Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>

* Apply more suggestions from code review

matches the original suggestion from SoldierofOrder

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: Yussur Mustafa Oraji <N00byKing@hotmail.de>
Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>
2023-03-13 00:58:17 +01:00
alwaysintreble
070a92e76c The Messenger: implement new game (#1494)
* initial commit of messenger integration

* setup no_logic and needed slot_data

* fix some typos and determinism

* make all of it deterministic

* add documentation

* swapped to non local items so change the fed data

* ~~deathlink~~

* satisfy the docs test

* update doc test to show expected name

* split custom classes into a separate file and fix an errant rule

* make access dependency test give more useful errors

* implement tests

* remove some unneccessary back entrances and make names clearer

* fix some big dumbs

* successful unit tests are good also some slight reorganizing

* add astral tea quest line, and potentially power seals as items

* if TYPE_CHECKING... aahhhhhh

* oop forgot to remove legacy code

* having the seed and leaves as actual items doesn't seem to do anything so remove them. locations still work though

* update setup guide with some changes

* Tower HQ was creating duplicate locations

* allow self locking items

* cleanup

* move self_locking_items function to core

* docstring

* implement choice of notes needed for music box

* test the default value

* don't create any starting inventory items

* make item creation faster

* change default accessibility and power seals options

* improve documentation

* precollected_items is a dict of Items...

* implement shop chest goal

* tests

* always assign total and required seals

* add new goals and set music box as requiring shop chest on shop chest goals instead of just setting it as the completion

* fix dumb test quirk

* implement music box skip as an option

* world rewrite/cleanup

* default to apworld and add game to readme

* revert bleeding commits from other PRs

* more bleeds

* fix some errors in options docstrings

* ???

* make my set rules method not have an awful name

* test cleanup

* add a test for item accessibility

* fix issues with tests

* make the self locking item behavior work correctly

* misc cleanup

* more general cleanup to be a good example

* quick rules rewrite

* more general cleanup and typing

* more speed, more clean

* bump data version

* make sure the locked item belongs to current player

* fix bad name and indent. call MessengerItem directly for events

* add poptracker pack to docs

* doc cleanup and "known issues" section that I probably won't be able to fix any time soon.

* missed some spots

* add another bug i forgot about

* be consistently wrong
2023-03-12 15:05:50 +01:00
Fabian Dill
39563cc347 WebHost: add game and Factorio to multitracker (#1526)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-03-12 12:38:13 +01:00
alwaysintreble
54cce4c392 LTTP: fix ice rod hunt boss shuffle (#1529) 2023-03-12 11:03:48 +01:00
el-u
426a81a065 oot/alttp: fix bugs found through MMBN3 testing (#1527) 2023-03-11 20:15:30 +01:00
alwaysintreble
04e6a8eae8 FFR: add option __doc__s 2023-03-11 13:38:27 +01:00
NewSoupVi
0cfdc973f6 The Witness - Expert logic bug (could lead to broken seeds) (#1525) 2023-03-11 10:09:09 +01:00
alwaysintreble
f3ca0a21c9 Docs: Add an option api doc (#1181)
* write up an option api doc

* address reviews

* some clarification

* add note about using schema

* Add ItemSet and formatting

* bulletpoint option defining

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>

* split random description to new sentence

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>

* use inclusive and parallel language for example

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>

* changes from review

* commas

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>

* capitalize Toggle

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>

* the sliver conventions

---------

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>
2023-03-11 01:14:44 +01:00
Chris Wilson
5fef41eb97 [WebHost] Make site header mobile-friendly (#1523) 2023-03-10 17:21:17 -05:00
alwaysintreble
4068ba2f15 LTTP: add option __doc__s (#1521)
* LTTP: add option `__doc__`s

* review comments
2023-03-10 07:59:47 +01:00
NewSoupVi
b1599c557f The Witness: Death Link + Small bug fixes (#1515)
* Fully functional DeathLink implementation. But it's always on right now :D

* Death Link options. Last commit: All entity names being sent through slot_data

* Tutorial Gate Close logic fix

* Improved option tooltip wording

* Fixed shuffle_postgame: false not excluding some locations

* Link to latest stable client rather than full releases page
2023-03-10 07:58:00 +01:00
Fabian Dill
7fdf38b2ad WebHost: automatically fill PATCH_TARGET -> HOST_ADDRESS and re-use it for rooms (#1518) 2023-03-09 21:31:00 +01:00
Freya Arbjerg
2e76085cf1 WebHost: Fix generic tracker Datatables (#1519) 2023-03-09 19:24:38 +01:00
Fabian Dill
c61f467218 WebHost: fix location_name_group related spinup crash 2023-03-09 12:31:35 +01:00
KonoTyran
942d689093 [Slay the Spire] Enable support for modded characters, and add downfall support (#1368)
* add ability to choose custom characters in STS

* bump required protocol (client?) version.

* fix slot data fill.

* add downfall mode, as well as characters.

* small change in documentation for character choice as it now uses internal ID's instead of visible titles... because other languages are a thing.
2023-03-08 20:14:54 -08:00
espeon65536
5e1aa52373 Minecraft rewrite (#1493)
* Minecraft: rewrite to modern AP standards

* Fix gitignore to not try to ignore the entire minecraft world

* minecraft: clean up MC-specific tests

* minecraft: use pkgutil instead of open

* minecraft: ship as apworld

* mc: update region to new api

* Increase upper limit on advancement and egg shard goals

* mc: reduce egg shard count by 5 for structure compasses

* Minecraft: add more tests
Ensures data loading works; tests beatability with various options at their max setting; new tests for 1.19 advancements

* test improvements

* mc: typing and imports cleanup

* parens

* mc: condense filler item code and override get_filler_item_name
2023-03-08 20:13:52 -08:00
Freya Arbjerg
a95e51deda Add generic multiworld tracker, move lttp multiworld tracker (#1478)
Co-authored-by: Berserker
2023-03-08 22:39:15 +01:00
alwaysintreble
738319462d Spoiler: Don't double print if world overrides common options (#1505) 2023-03-08 22:19:38 +01:00
alwaysintreble
e3deb822ad Core: implement location_name_groups (#1502) 2023-03-08 22:15:28 +01:00
Jarno
d57314a407 Timespinner: Bring back starter progression item (#1508) 2023-03-08 20:05:30 +01:00
t3hf1gm3nt
5a8e6e61f5 TLOZ: Code Cleanup (#1514)
- consolidated declaration and population of level location lists
- moved floor_location_game_ids_late declaration for consistency
- moved generate_itempool to create_items, where it belongs
- mention that expanded pool includes take any caves in the option description again
- removed unnecessary StartingPosition check regarding Take Any Caves (leftover from older StartingPosition behavior I believe)
- use proper comparisons to option keys instead of hardcoded ints
2023-03-08 11:22:14 +01:00
Magnemania
17e90ce12c SC2: Greater variety on short generations (#1367)
Originally, short generations used an artificial cull to create balanced mission distributions. This resulted in campaigns that were somewhat too consistent, and on some standard settings combinations, this resulted in campaigns having The Outlaws as the second mission 100% of the time. It also caused generation to fail a bit too easily if the player excluded too many missions.

This removes the cull and adds an additional early Easy mission slot to all of the reduced sized campaigns.

When playing on No Build settings, this also pushes many of the missions down a difficulty level to ensure greater variety, and pushes additional missions down on Advanced Tactics.

Additional small fixes:

The in-world Excluded Missions validation check is replaced by the core OptionSet check.
Fixed issue with Existing Items not getting their upgrades locked with Units Always Have Upgrades on.
2023-03-07 14:14:49 +01:00
NewSoupVi
016157a0eb Witness: Fixed settings combination not rolling (see description)
Settings combination:
- EP Shuffle
- disable_non_randomized
- doors: panels or doors: none

An Event Item was being created that is inaccessible. This is fixed now.
(The fix makes sure that player_logic is not trying to create events for the sake of EPs that are disabled)

Note: These two sets should probably be merged anyway, they used to behave differenty but no longer really do. But that will require some extra care on the client side as well.
2023-03-07 09:13:54 +01:00
Fabian Dill
5b64c5f934 Subnautica: fix exported radiation logic (#1507) 2023-03-07 09:09:24 +01:00
alwaysintreble
414166f6a2 Core: Minor Options cleanup (#1182)
* Options.py cleanup

* TextChoice cleanup

* make Option.current_option_name a property

* title the TextChoce option names

* satisfy the linter

* a little more options cleanup

* move the typing import

* typing should be PlandoSettings

* fix incorrect conflict merging

* make imports local

* the tests seem to want me to import these twice though i hate it.

* changes from review. Make the various Location verifying Options `LocationSet`

* remove unnecessary fluff

* begrudgingly support get_current_option_name. Leave a comment that worlds shouldn't be touching this

* log a deprecation warning and return the property for `get_current_option_name()`

---------

Co-authored-by: beauxq <beauxq@yahoo.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-03-07 08:44:20 +01:00
beauxq
e6109394ad Zillion: use Option.current_key
and other minor fixes
2023-03-07 08:33:33 +01:00
Fabian Dill
8ca25fed63 Setup: clean up imports 2023-03-06 12:54:32 +01:00
0rganics
227d59ecfb WebHost: Add a ChecksFinder tracker (#1333)
Co-authored-by: Chris Wilson <chris@legendserver.info>
2023-03-05 14:17:04 +01:00
Fabian Dill
08c17c83d4 Setup: auto download SNI (#1312)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-03-05 14:10:05 +01:00
Rosalie-A
efb2ab4505 TLoZ: Implementing The Legend of Zelda (#1354)
Co-authored-by: t3hf1gm3nt <59876300+t3hf1gm3nt@users.noreply.github.com>
Co-authored-by: Alchav <59858495+Alchav@users.noreply.github.com>
2023-03-05 13:31:31 +01:00
Joethepic
3a68ce3faa PKMN: Make Exp All early (#1422) 2023-03-05 10:08:32 +01:00
black-sliver
e78800d1bc Item Plando: make world selection deterministic 2023-03-05 02:16:55 +01:00
Jérémie Bolduc
96d7a3a64c Stardew Valley: Fix generation issue with Master Angler goal and vanilla tools (#1498)
* - Can Catch every fish doesn't need fishing rods if they are not shuffled

* add has_max_fishing_rod

* add test for master angler + vanilla tools

---------

Co-authored-by: Alex Gilbert <alexgilbert@yahoo.com>
2023-03-04 18:34:51 +01:00
recklesscoder
30b70b2055 Misc collected fixes (#1497) 2023-03-04 16:34:10 +01:00
Jarno
cd234fc04a Timespinner: Fixed Dry lake serene oddity (#1501) 2023-03-04 16:31:44 +01:00
zig-for
d74c4c4c94 Core: Remove ALTTP cruft from BaseClasses (#1451) 2023-03-04 08:23:52 +01:00
Jarno
a4b61118cf Timespinner: Refactorings + fix for #1460 (#1484) 2023-03-04 08:16:05 +01:00
FlySniper
9fa1f4e85f Wargroove: Fixed Wargroove Client not removing communication files (#1492) 2023-03-03 18:24:09 +01:00
black-sliver
3a926849a0 CI: run unittests on macos
this is to ensure dependencies can be installed and loaded on macos (on AMD64)
2023-03-03 18:22:31 +01:00
Alchav
798d823397 Core: Check for show_in_spoiler (#1500) 2023-03-03 17:22:46 +01:00
Fabian Dill
4ea582f14e Windows: allow arm64 setup (#1496) 2023-03-03 09:51:36 +01:00
alwaysintreble
21fb16291d Tests: test that the game is beatable for WorldTestBases (#1495)
* Tests: test that the game is beatable for WorldTestBases

* update docstring

* don't test the bases with default options for real this time

* invert the property so worlds can use it easier

* setup check should be or

* test class needs to always be constructed

* skip default tests before multiworld setup

* check if the calling method is in the base's __dict__

* shorter property and functional setup skipping

* shorter property and functional setup skipping
2023-03-03 00:30:40 +01:00
NewSoupVi
805f33c39e Witness: Bugfixes in response to beta tests (#1473)
* Make all Keep Pressure Plates logically required for the Laser Panel

* Added more Tutorial checks

* Added the remaining two Shipwreck Boat EPs to the exclude list for normal

* Improved itempool filling system, added warning if usefuls had to be eaten

* Moved creation of said warning string to utils

* Fixed logic bug causing broken seeds on Mountain Floor 2

* Hints system change

* Expert Logic Fix

* Fixed typo

* Better wording

* Added missing games to junk hints

* Made sure Entrance names are unique

* Fixed missing Obelisk Side

* Disable Non Randomized + EP Shuffle fix

* Fixed disable_non_randomized precompleted EPs being 'disabled' instead of 'precompleted'

* Fixed if/elif error

* Tutorial Gate Open local symbol item becomes local_early_item in expert instead

* Bump required client version. There is a beta client that sends 0.3.9.

* Removed print statement, oops

* Fixed itempool manipulation in pre_fill

* Replaced string concats with fstrings

* Improved make_warning_string function signature

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Improved performance on removing multiple items from multiworld itempool

* Comment

* Fixed errors with the code

* Made removal from itempool not fail unit test for multiple references

* Moved all item creation to create_items, got rid of itempool modifying system

* Colored Squares is no longer a good item, that's outdated

* Removed double if

* React to from_pool: false by removing a junk item

* Fixed warning if only Fnc Brain was removed

* Make use of string truthiness instead

* Made reading of plandoed items safer

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-03-03 00:08:24 +01:00
el-u
0cf8206660 launcher: add .apl2ac support 2023-03-01 22:56:14 +01:00
Fabian Dill
2c20b56478 Core: count the world types 2023-03-01 05:47:46 +01:00
FlySniper
1d2f7d8669 Wargroove: Fixed the find all dogs check activating prematurely (#1486) 2023-02-28 16:26:48 +01:00
Fabian Dill
0733775f2c Subnautica: Allow either utility room for progression 2023-02-28 11:22:47 +01:00
black-sliver
d6f3b27695 DKC3, SMW: use user_path for file
Same as for other games, this will resolve to ~/Archipelago on Linux, if the install folder is read-only
2023-02-28 09:51:32 +01:00
black-sliver
ce7e6bcf33 Readme: fix order 2023-02-28 09:15:23 +01:00
Jarno
2c4658a7e0 Docs: More games more fun 2023-02-27 23:19:33 +01:00
TheLynk
79b8733b13 OoT, MC: add new translation setup in french (#1410)
* add new translation

* Add translation for OOT Setup in french

* Update setup_fr.md

* Update worlds/oot/docs/setup_fr.md

Co-authored-by: Ludovic Marechal <marechal-l@gmx.com>

* Update setup_fr.md

Fix treu to true

* Update worlds/oot/docs/setup_fr.md

Co-authored-by: Marech <marechal-l@gmx.com>

* Update OOT Init and Update Minecraft Init

* Fix formatting errors

---------

Co-authored-by: Ludovic Marechal <marechal-l@gmx.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-02-27 23:17:54 +01:00
alwaysintreble
9cb9cbe47d Tests: test that worlds don't create regions or locations after create_items (#1465)
* Tests: test that worlds don't create regions or locations after `create_items`

* recache during the location counts just to be extra safe

* adjust typing and use a Tuple instead of a list

* remove unused import
2023-02-27 02:13:24 +01:00
alwaysintreble
7cad53c31a Docs: add docstrings to the World class 2023-02-27 01:39:30 +01:00
alwaysintreble
f3bdf0c5ed Tests: test all state and empty state on world test bases (#1476)
* Tests: test all state and empty state on world test bases

* actually add the test methods to the dict

* only test if the world test base has non default options

* remove temp logging

* ditch the meta class and document methods

* Tests: WorldTestBase comment and docstring cleanup

* skip default tests if setUp or world_setup are modified and use a property

* negation hurts my head

* docstring

* use a better name for the property

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-02-27 01:24:54 +01:00
Jérémie Bolduc
af7d0dbf37 Stardew Valley: implement new game (#1455)
* Stardew Valley Archipelago implementation

* fix breaking changes

* - Added and Updated Documentation for the game

* Removed fun

* Remove entire idea of step, due to possible inconsistency with the main AP core

* Commented out the desired steps, fix renaming after rebase

* Fixed wording

* tests now passes on 3.8

* run flake8

* remove dependency so apworld work again

* remove dependency for real

* - Fix Formatting in the Game Page
- Removed disabled Option Descriptions for Entrance Randomizer
- Improved Game Page's description of the Arcade Machine buffs
- Trimmed down the text on the Options page for Arcade Machines, so that it is smaller

* - Removed blankspace

* remove player field

* remove None check in options

* document the scripts

* fix pytest warning

* use importlib.resources.files

* fix

* add version requirement to importlib_resources

* remove __init__.py from data folder

* increment data version

* let the __init__.py for 3.9

* use sorted() instead of list()

* replace frozenset from fish_data with tuples

* remove dependency on pytest

* - Add a bit of text to the guide to tell them about how to redeem some received items

* - Added a comment about which mod version to use

* change single quotes for double quotes

* Minimum client version both ways

* Changed version number to be more specific. The mod will handle deciding

---------

Co-authored-by: Alex Gilbert <alexgilbert@yahoo.com>
2023-02-27 01:19:15 +01:00
Fabian Dill
0286edf20c Subnautica: fix the test that I didn't mean to push yet 2023-02-26 09:51:02 +01:00
Fabian Dill
05e36cab1c Subnautica: increment version 2023-02-26 09:48:51 +01:00
Klenoa
50425985c4 Subnautica: Rename location check (id 33029) (#1120)
Update southwest grassy plateaus wreck to 'Grassy Plateaus Southwest Wreck - Databox' instead of 'Grassy Plateaus West Wreck - Databox'
2023-02-26 09:48:08 +01:00
Marech
062d6eeace DS3: Added DLC Items/Locations + corresponding option and added an option to enable materials/consumables/estus randomization (#1301)
- Added more progressive locations and associated items.
- Added an option to enable materials/consumables/estus randomization, some players complain about the number of locations and the randomness of those items.
- Added an option to add DLC Items and Locations to the pool, the player must own both the ASHES OF ARIANDEL and the RINGED CITY DLC.

Co-authored-by: Br00ty <83629348+Br00ty@users.noreply.github.com>
Co-authored-by: Friðberg Reynir Traustason <fridberg.traustason@gmail.com>
2023-02-26 06:35:03 +01:00
alwaysintreble
6c460bcbf7 LTTP: Move LTTP spoiler writing out of core (#1467) 2023-02-25 04:02:51 +01:00
toasterparty
b8659d28cc [OC2] DeathLink (#1470) 2023-02-24 08:32:15 +01:00
alwaysintreble
0b12d80008 Tracker: get game names from slot_info instead of multidata["games"] and render custom game names on generic tracker (#1453) 2023-02-24 08:30:11 +01:00
FlySniper
5966aa5327 Wargroove: Implement New Game (#1401)
This adds Wargroove to the list of supported games. Wargroove uses a custom non-linear campaign over the vanilla and double trouble campaigns. A Wargroove client has been added which does a lot of heavy lifting for the Wargroove implementation and must be always on during gameplay. The mod source files can be found here: https://github.com/FlySniper/WargrooveArchipelagoMod
2023-02-24 07:35:09 +01:00
Trevor L
7c68e91d4a Blasphemous: Implement new game (#1446)
Adds @BrandenEK's Blasphemous Randomizer as a new Archipelago game.
2023-02-24 07:33:09 +01:00
alwaysintreble
1d6ab13015 ArchipIDLE: add a completion condition instead of hard coding tests around a game (#1444) 2023-02-23 21:16:10 -05:00
CaitSith2
cb3d40624c Timespinner: Make RisingTidesOverrides consistent with normal yaml behaviour. (#1474)
* Make RisingTidesOverrides consistent with normal yaml behaviour.

* Each of the options can be either string directly specifying the option, or dictionary.
* If dictionary, ensure that at least one of the options is greater than zero.

* Made keys optional

* A lot less copy/pasta.

---------

Co-authored-by: Jarno Westhof <jarnowesthof@gmail.com>
2023-02-22 17:11:27 -08:00
alwaysintreble
0eb66957b1 SMW: change random in generate_output to use slot random 2023-02-20 18:43:23 +01:00
alwaysintreble
53e2232f29 Docs: document world docs and tests (#1463)
* Docs: document world docs and tests

* regions and items shouldn't be created after `create_items`

* Changes from review

* Restructure game info section

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* w

* urls can have extension probably

* reorder the methods by call order

* fix grammar mistake in ordered method list

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-02-19 23:16:56 +01:00
Fabian Dill
ecd2675ea8 Tests: check that Regions are reachable (#1034)
* Tests: check that Regions are reachable
try to prevent errors from unconnected/never reachable Regions

* Test region access (#1039)

* Tests: note oot's default unreachable regions

* [SM] Fixed failing testAllStateCanReachEverything (#1087)

* [SM] Fixed failing testAllStateCanReachEverything

- by adding exclusion for Regions used only when corresponding Starting Location is used
- by removing unnecessary VARIA Regions used only for EscapeRando (not supported in AP anyway)

* Update worlds/sm/Regions.py

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Update worlds/sm/Rules.py

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Update worlds/sm/Regions.py

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Update test/general/TestReachability.py

---------

Co-authored-by: espeon65536 <81029175+espeon65536@users.noreply.github.com>
Co-authored-by: lordlou <87331798+lordlou@users.noreply.github.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-02-19 23:09:54 +01:00
Jarno
fc2e555b4a Timespinner: many new stuffs (#1433)
* Timespinner: added RisingTides and DadPercent flags

* Implemented logic for DadPercent and RisingTides

* Fixed TODO's

* Logic fixes

* Fixed + removed LogicMixins

* Fixes

* More Fixes

* Added UnchainedKeys flag

* Fixed available items in pool with UnchainedKeys

* Fixed typing callable

* Fixed generation failures

* More refactorings

* Implemented traps

* Fixed more typo

* Fixed copy paste bug

* Fixed teleporter logic

* Fixed traps from pool

* Fixed pyramid gates bug that causes a crash on connecting

* Fixed seed reproduceability

* Fixed logic eye for eye spy
Now consider warp beacons as starter progression items

* Attempt to add tracker icons using table

* Replaced table layout with css grid

* Fixed tracker + added Timespinner was apworld capatible

* Updated archipelago items description

* updated URL

* Cleared up text

* Fixed based on self review of PR

* Fixed unit tests

* Fixed seed reproduceability when the traps yaml option is not provided

* Fixed logic for flooded basement

* Implemented Beserkers review result

I am not sure why, i guess this is just to make adding future games less conflicting?

Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>

* Added two new options (thanks to WeffJebster)

* Apply suggestions from code review

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Addition review results

---------

Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-02-19 21:22:30 +01:00
black-sliver
df020bb389 Style Guide: add world consistency 2023-02-19 19:34:45 +01:00
PoryGone
7760034ff7 DKC3 and SMW: Remove relative imports (#1472) 2023-02-19 09:10:32 +01:00
black-sliver
3e7794d5dc SoE: update evermizer to 044
* Fix energy core despawning when looting failed
* Fix fish guy dialog when cure was already obtained
see https://github.com/black-sliver/pyevermizer/releases/tag/v0.44.0 for more details
2023-02-18 15:18:51 +01:00
black-sliver
a3e8bb474a ModuleUpdater: allow new syntax, nicer output 2023-02-18 15:18:51 +01:00
kindasneaki
e4c95c940a RoR2: regions unreachable fix (#1459) 2023-02-17 22:08:18 +01:00
recklesscoder
daa1809a0f WebHost: Tweaks to search on tracker pages (#1307)
* WebHost: Tweaks to search on tracker pages
- Pressing `Ctrl+F` or `/` now focuses the search box.
- Typing now automatically focuses the search box.
- Pressing `Escape` now clears the search and scrolls to the top.

* WebHost/Trackers: Focus search box on load

* WebHost/Trackers: Remove overriding of Ctrl+F and /
2023-02-17 13:24:21 -05:00
Jarno
0a1261eb84 WebHost: Add tutorials to sitemap and hide settings link for games without settings (#1452)
* WebHost: Add tutorials to sitemap and hide settings link for games without settings

* Fixed some typing imports
2023-02-17 13:16:37 -05:00
toasterparty
b62be6f7f4 [OC2] Colored Ramp Button Items (#1466)
Before: 1 item activates all 4
After: 7 items activate 7 buttons, creating more divergent routes

Also, I consolidated the 6 filler emotes into a single "Emote Wheel" item to make space in the item pool.

I bumped my data version and min AP version to indicate this change.

The corresponding oc2-modding update is **v1.6.0**
2023-02-17 09:25:56 +01:00
toasterparty
ce2553a2b3 [OC2] Location Balancing (#1458) 2023-02-17 09:21:56 +01:00
Fabian Dill
18c4b4b1fe Subnautica: move code to be a better example 2023-02-17 08:50:22 +01:00
Fabian Dill
a85ca9cc87 Subnautica: add logic dumper for mod (#1386)
* Subnautica: add logic dumper for mod

* Subnautica: export more data

* Subnautica: fix some Cyclops logic
2023-02-16 00:40:19 +01:00
el-u
ad4846cedd core: clarify usage of classmethods in World class (#1449) 2023-02-16 00:28:02 +01:00
recklesscoder
b20be3ccec Docs/Factorio: Document EnergyLink (#1456)
* Docs/Factorio: Document EnergyLink

* Docs/Factorio: EnergyLink clarification
2023-02-16 00:25:46 +01:00
alwaysintreble
8af7908cd0 Tests: datapackage and more multiworld renaming (#1454)
* Tests: add a test that created items and locations exist in the datapackage

* move FF validation to `assert_generate` and remove test exclusion

* test created location addresses are correct

* make the assertion proper and more verbose

* make item count test ~~a bit faster~~ a lot nicer

* 120 blaze it

* name test multiworld setup better and fix another over 120 line in FFR
2023-02-15 22:46:10 +01:00
alwaysintreble
f078750b72 LTTP: make the enemizer check a property and only check for it once instead of per world (#1448)
* LTTP: do the enemizer check in `stage_assert_generate` and break after checking any world for enemizer succeeds

* use multiworld

* catch a missed `used_enemizer` check and add typing

* more typing
2023-02-14 22:22:39 +01:00
alwaysintreble
7cbeb8438b core: rip out RegionType and rework Region class (#814) 2023-02-14 01:06:43 +01:00
Fabian Dill
f7a0542898 kvui: limit UI side logs to by default 1000 messages 2023-02-13 09:02:19 +01:00
recklesscoder
cc61f16e57 Protocol: Improve machine-readability of prints (#1388)
* Protocol: Improve machine-readability of prints

* Factorio: Make use of new PrintJSON fields for echo detection.

* Protocol: Add message field to chat prints.
2023-02-13 03:17:25 +01:00
alwaysintreble
9e3c2e2464 Tests: test that exits to Regions are the parents of the Entrance (#1442) 2023-02-13 02:05:52 +01:00
Fabian Dill
f528175d8a Core: prepare server for removal of names in multidata (#1430) 2023-02-13 01:56:20 +01:00
Fabian Dill
803d7105a1 kivy: allow user-defined text colors in data/client.kv (#1429) 2023-02-13 01:55:43 +01:00
el-u
a40f6058b5 lufia2ac: fix mismatched exits/parent region 2023-02-13 00:46:46 +01:00
toasterparty
0ff3c693d5 [OC2] Relax Horde Logic for Horde H-8 and Winter H-4 (#1439) 2023-02-10 21:42:28 +01:00
Fabian Dill
873a374a69 SNIclient: connect fixes (#1436) 2023-02-07 10:16:39 +01:00
black-sliver
60584b7617 CI: more pip to fix the build 2023-02-07 10:10:27 +01:00
black-sliver
e24a85ca5c CI: update SNI to v0.0.88 2023-02-07 10:10:27 +01:00
lordlou
cc0540d3fb SMZ3: keysanity accessibility fix (#1428) 2023-02-07 03:14:03 +01:00
NewSoupVi
c360b9266c Witness: Renaming: Mill -> Stoneworks, correcting order for 'First, Second, ...', removed all instances of 'Door (Door)' (#1435) 2023-02-07 03:12:47 +01:00
beauxq
6148213e43 Zillion: fix name data overflow 2023-02-07 03:11:48 +01:00
Jarno
ff175008a1 Core: Phase out Print packets (#1364) 2023-02-05 22:06:38 +01:00
kindasneaki
cae1e683e2 RoR2: 1.20 content update (#1396)
## Adding in Explore Mode:

Features include:
* Added in `environments` to be items.
* `Location checks` are now `environment based` instead of being able to get them from anywhere.
* Added in support for the `DLC Survivors of the void` which include `Void Items` and `3 new maps` that come with it. (option added to use DLC)

---------

Co-authored-by: Dogpetkid <dogpetkid@gmail.com>
2023-02-05 21:51:03 +01:00
vgZerst
fb1a9e9c5a WebHost: add checks percent done column to tracker (#1376)
* WebHost: add checks percent done column to tracker

* WebHost: add checks percent done column to tracker
2023-02-04 00:04:00 -05:00
CaitSith2
555a0da46d Core: Rename the missed slot_seeds. (#1432)
* Rename the missed slot_seeds.

* Fixed a threaded context random error.
2023-02-03 19:39:18 +01:00
NewSoupVi
0817305d5b Witness: Added an option tooltip for "Environmental Puzzles Difficulty" option (+ another bugfix) (#1431)
* Added an option tooltip

* Fixed eclipse being on in EP difficulty normal
2023-02-03 19:38:54 +01:00
Fabian Dill
995c978628 Core: replace global random state with descriptive error (#1424)
* Core: replace global random state with descriptive error

* Core: make random a proxy object and rename slot_seeds
2023-02-02 01:14:23 +01:00
NewSoupVi
4de7ebd8b0 The Witness: v4 Content Update (#1338)
## New Features:

- EP Shuffle (Individual or Obelisk Sides, with varying difficulty levels)
- Ability to play without Puzzle Randomization (I.e. vanilla + AP layer)
- Pet the Dog to get a Puzzle Skip :) (No, really.)

## Changes:

- Starting inventory behavior improved (Consider starting items like doors and lasers logically even if they aren't part of the mode)
- Audio Log hint system improved (On low hint counts, you will no longer get the same locations hinted every time, i.e. always hints are shuffled)

## Fixes:

- Many fixes to symbol requirements
- Fixes to "shuffle_postgame" (What checks are evaluated as "postgame" in specific modes)
- Logically irrelevant doors are now "useful" instead of "progression"
2023-02-01 21:18:07 +01:00
PoryGone
3cef39a387 SMW & DKC3: Ship as .apworld (#1426) 2023-02-01 21:15:01 +01:00
el-u
ffff9ece55 core: properly declare from_any as an abstract classmethod 2023-02-01 21:14:11 +01:00
PoryGone
dc2aa5f41e SMW: v1.1 Content Update (#1344)
* Make Bowser unkillable on Egg Hunt

* Increment Data Package version

Changed a location name.

* Baseline for Bowser Rooms shuffling

* Add boss shuffle

* Remove extra space

* Overworld Palette Shuffle

* Fix Literature Trap typo

* Handle Queuing traps and new Timer Trap

* Fix trap name and actually create them

* Early Climb and Overworld Speed

* Add correct tooltip for Early Climb

* Tooltip text edit

* Address unconnected regions

* Add option to fully exclude Special Zone levels from the seed

* Fix Chocolate Island 4 Dragon Coins logic

* Update worlds/smw/Client.py to use `getattr`
2023-01-30 05:53:56 +01:00
black-sliver
428344b6bc setup: honor build automation ...
... and reorder imports to PEP it up
2023-01-30 02:33:41 +01:00
Fabian Dill
ea2175cb8a MultiServer: load old forfeit_mode if release_mode not present 2023-01-30 00:54:57 +01:00
Fabian Dill
11873e059a Setup: don't use dependency before it's installed 2023-01-29 22:36:04 +01:00
Fabian Dill
6c1023a88c Subnautica: fix swim_rule considers items property use (#1419) 2023-01-29 22:12:39 +01:00
The T
0be0732a2b WebHost: FAQ: change "seeds" to "a world" where world is the right term.
Berserker has frequently corrected, that each player's game is a single world, inside a larger seed; not that each player's game is a seed.
2023-01-29 22:11:53 +01:00
lordlou
c9aa283711 SMZ3: chest game fix (#1417)
Fixed DW Chest Game always sending checks for the 2 chests. The checks sent were the proper "Chest Game" location for the first time the player would open the second chest but all other times, it would send either the last check that was done or default to sending location 0x00 which is SM "Power Bomb (Crateria surface)".
2023-01-28 01:51:19 +01:00
Fabian Dill
cf2204a861 Factorio: add option "Tech Cost Distribution" (#1404)
* Factorio: add option "Tech Cost Distribution"

* TextClient: None out game on disconnect

* TextClient: disconnect is async
2023-01-27 15:38:12 -08:00
Fabian Dill
dfdcad28e5 TextcVient: game reset (#1416)
* TextClient: None out game on disconnect
2023-01-28 00:32:48 +01:00
Fabian Dill
ab4324c901 Factorio: add option "ramping tech cost" (#1403)
* Factorio: add option "ramping tech cost"

* Factorio: fix missing s

* Factorio: add display_name to ranmping tech costs
2023-01-27 15:30:05 -08:00
Fabian Dill
1e251dcdc0 Setup: new cx-Freeze just dropped 2023-01-27 20:06:56 +01:00
espeon65536
9c1f7bfea9 oot: remove special NL exceptions in entrance randomization
turns out they were causing lots of issues
2023-01-26 21:24:27 +01:00
KonoTyran
5393563700 MultiServer: Data Storage Additions #1411
adds 3 new operations to datastorage that allows adding and removing of elements from list and dicts.
2023-01-25 06:14:46 +01:00
toasterparty
28576f2b0d OC2: decrease default difficulty (#1413) 2023-01-25 01:04:13 +01:00
Fabian Dill
ba519fecd0 Setup: update some stuff to 6.14.0 cx-Freeze (#1412)
* Setup: update some stuff to 6.14.0 cx-Freeze

* Fix BuildCommand and replace include_files by cutom step

* setup.py: bit more cleanup for extra_libs

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2023-01-25 00:20:26 +01:00
espeon65536
86fb450ecc Core: recache all locations before locality rules
Some worlds would not trigger a recache, causing locations to be missed when setting locality rules.
2023-01-24 06:14:31 +01:00
SonicRPika
920240cb6f Pokemon Red and Blue: Fix to having all traps disabled (#1408) 2023-01-24 04:34:45 +01:00
Fabian Dill
53dd0d5a7d SC2: verify downloaded data is a zipfile 2023-01-24 03:54:23 +01:00
Fabian Dill
807f544b26 SC2: use warning log level for potentially broken map files 2023-01-24 03:45:43 +01:00
SoldierofOrder
1d1693df62 SC2Client: Changes to /download_data and feedback (#1347)
* SC2Client: Added feedback for users who have map files, but no version #.

* SC2Client: Fixed a missing space.

* SC2Client: /download_data now always forces a download.
2023-01-24 03:44:12 +01:00
Fabian Dill
51574959ec Setup: add moduleupdater prompt to setup.py 2023-01-24 03:42:55 +01:00
Fabian Dill
04f726aef2 kvui: always display all tab headers (#1399) 2023-01-24 03:42:34 +01:00
CaitSith2
8a4298e504 ALttP: Fix hint tile hints being potentially useless with item links. (#1400)
* ALttP: Fix hint tile hints being potentially useless with item links.

* use the set returned from world.get_player_groups(player)

* Move the group resolving to BaseClasses. Fix silver arrow hints as well.
2023-01-24 03:42:13 +01:00
Fabian Dill
e7f8f40464 Factorio: fix automation-level tech costs before automation (#1402)
* Factorio: fix automation-level tech costs before automation

* Factorio: remove double-rolling of science cost
2023-01-24 03:36:50 +01:00
Fabian Dill
847582ff5f Server: fix release_mode (#1407)
* Server: fix release_mode

* Core: actually rename forfeit to release across the program
2023-01-24 03:36:27 +01:00
recklesscoder
1a44f5cf1c CommonClient: Fix address pre-selection (#1406) 2023-01-23 04:59:51 +01:00
Fabian Dill
032bc75070 Core: demote logfile deletion loglevel to debug 2023-01-23 02:23:16 +01:00
Alchav
fb47483212 Pokémon R/B: Fix trainersanity location name 2023-01-23 00:38:38 +01:00
Alchav
d185df3972 Pokémon R/B: Use local random object when randomizing trainer parties in generate_output 2023-01-23 00:38:38 +01:00
lordlou
941dcb60e5 SM: fixed flawed and limited comeback check (#1398)
The issue at hand is fixing impossible seeds generated by a lack of properly checking if the player can come back to its previous region after reaching for a new location, as reported here: https://discord.com/channels/731205301247803413/1050529825212874763/1050529825212874763

The previous attempt at checking for comeback was only done against "Landing Site" and the custom start region which is a partial solution at best. For exemple, generating a single player plando seed with a custom starting location at "red_brinstar_elevator" with a forced red door at "RedTowerElevatorBottomLeft" and 2 Missiles set at "Morphing Ball" and "Energy Tank, Brinstar Ceiling" would generate an impossible seed where the player is expected to go through the green door "LandingSiteRight" with no Supers to go to the only possible next location "Power Bomb (red Brinstar spike room)". This is because the comeback check would pass because it would consider coming back to "Landing Site" enough.

The proposed solution is keeping a record of the last accessed region when collecting items. It would then be used as the source of the comeback check with the destination being the new location. This check had to be moved from can_fill() to can_reach() because the maximum_exploration_state of the AP filler only use can_reach().

Its still not perfect because collect() can be called in batch for many items at a time so the last accessed region will be set as the last collected item and will be used for the next comeback checks.

This was tested a bit with the given exemple above (its now failing generation) and by generating some 8 SM players seed with many door color rando, area rando and boss rando enabled.
2023-01-23 00:36:18 +01:00
Fabian Dill
25756831b7 Core: mark version as 0.3.8 2023-01-21 17:30:30 +01:00
Fabian Dill
9add1495d5 SSL support (#1340) 2023-01-21 17:29:27 +01:00
SonicRPika
34dba007dc Pokemon Red and Blue: Updates to trap weights and tracker support (#1395)
* Added cerulean_cave_condition to fill_slot_data

Added `cerulean_cave_condition` to the `fill_slot_data` function, for a poptracker feature being worked on as it was missing

* Added the potential for any traps to be disabled

Adding the ability to disable any kind of trap, for example if you want any status trap except being Poisoned. Will add a contingency to not try and roll a trap if they are all set to disabled.

* Added contingency to if all traps are disabled

Added a contingency to creating items such that it doesn't try to create a trap if all the traps are disabled

* Updated variable name

Edited name of variable to follow PEP 8 variable naming conventions
2023-01-20 18:49:12 +01:00
Fabian Dill
02d3eef565 Core: convert mixture of Plando Options and Settings into just Options 2023-01-19 17:20:23 +01:00
Fabian Dill
c839a76fe7 LttP: allow hinting and tracking "Take Any" type shops (#1392)
* LttP: allow hinting and tracking "Take Any" type shops
fix broken behaviour since bow/cave split


Co-authored-by: CaitSith2 <d_good@caitsith2.com>
2023-01-19 16:17:43 +01:00
alwaysintreble
29e1c3dcf4 LTTP: fix open pyramid for real this time (#1393) 2023-01-19 16:17:16 +01:00
recklesscoder
f6616da5a9 Docs/Subnautica: Updated console instructions, misc clarifications (#1394) 2023-01-19 16:09:08 +01:00
Fabian Dill
8678e02d54 Subnautica: correct doc string placement for early seaglide 2023-01-19 00:03:26 +01:00
Fabian Dill
2f37bedc92 Tests: ensure item name groups do not collide with item names (#1074) 2023-01-18 15:45:48 +01:00
Alchav
91fdfe3e17 Pokémon R/B: Add inheritance to "Completely Random" option as well 2023-01-18 04:26:40 +01:00
Alchav
a41b0051a6 Pokémon R/B: Fix TM/HM compatibility bug 2023-01-18 04:26:40 +01:00
alwaysintreble
b8abe9f980 Tests: add a test to check for dupe locations (#1378) 2023-01-15 20:18:32 +01:00
alwaysintreble
dd3ae5ecbd core: write the plando settings to the spoiler log (#1248)
Co-authored-by: Zach Parks <zach@alliware.com>
2023-01-15 18:10:26 +01:00
PoryGone
e96602d31b SA2B: Fix Gate region connections (#1384) 2023-01-15 17:55:36 +01:00
el-u
81d953daa3 alttp: add item rules for prize locations (#1380) 2023-01-14 14:29:54 +01:00
Alchav
bd774a454e Pokémon R/B: Fix Safari Zone Gate bug (#1381) 2023-01-14 04:59:09 +01:00
espeon65536
ca724c92ad oot: force itempool to higher settings if required by heart logic 2023-01-13 23:53:13 +01:00
espeon65536
11eebbbd32 Ocarina of Time: 0.3.7 hotfixes round 2 (#1351)
* oot: repair closed forest + dungeon ER

* oot: finally skip triforce pieces in balancing

* oot: fix mq_dungeons_mode set to mq or count

* oot: force 0.3.7 client
hopefully this makes people update

* oot: temp fix for skip-child-zelda crash
eventually I want to decide on a better fix for this though

* oot: remove skip-child-zelda item inside if tree

* oot: fix classification of some thieves hideout locations in tracker

* oot: fix regional shuffle for hideout keys and ganon boss key

* oot: properly attach hints to dungeon locations

* Fix entrance shuffle flag not being set correctly due to new dungeon shuffle option format
2023-01-12 20:20:49 +01:00
Doug Hoskisson
608794cded ZillionClient: fix manual disconnect (#1266) 2023-01-07 10:27:43 +01:00
eudaimonistic
816de5ff02 Docs: code_of_conduct.md (#1350)
Update to point of contact.
2023-01-07 10:24:41 +01:00
Fabian Dill
0b941e2268 LttP: attempt at preventing ghost location checks (#1355) 2023-01-07 10:22:15 +01:00
beauxq
57713cda50 Zillion: minor terrain logic update
standing on a moving walkway requires 2 columns of standing space
2023-01-07 10:12:45 +01:00
Jarno
f56cdd6ec3 Sudoku: Hints will no-longer duplicate (#1371) 2023-01-07 10:10:20 +01:00
t3hf1gm3nt
773c517757 update LTTP player template to add all universal AP options (#1372) 2023-01-07 10:09:33 +01:00
JoshuaEagles
2509b7fa3f SA2B: Add Linux section to setup guide (#1374) 2023-01-07 10:00:19 +01:00
toasterparty
10652d23e0 [OC2] Logic: fixes fails when horde levels/items are excluded from location pool (#1369) 2023-01-05 15:49:50 +01:00
JoshuaEagles
f0bc3d33ac Subnautica: add Linux note to setup guide (#1365) 2023-01-04 15:26:32 +01:00
toasterparty
92d1ed60c6 [OC2] Fix "Moon 1-5" never appearing in level pool (#1366) 2023-01-04 15:21:52 +01:00
Zach Parks
fe2b431821 MultiServer: Remove forced_auto_forfeit (#1363) 2023-01-02 19:26:34 -06:00
Zach Parks
0cc83698f9 Docs: Add special name keywords to docs. (#1353) 2023-01-02 14:42:47 -06:00
Alchav
428f643b07 Pokémon R/B: Fix Pokémon Tower 7F crash (#1362) 2023-01-02 13:29:44 -06:00
Fabian Dill
d4e2b75520 Clients: retry connection with ssl (#1341) 2023-01-02 20:24:54 +01:00
Fabian Dill
96cc7f79dc Subnautica: fix early seaglide 2023-01-02 20:24:14 +01:00
Fabian Dill
bdfbc7e14a Network: allow sending frozenset 2023-01-02 20:23:31 +01:00
Fabian Dill
94c6562f82 Tests: make sure DB overwrite actually takes 2023-01-02 20:23:00 +01:00
alwaysintreble
22fe31a141 Generate: fix default utils options (#1361) 2023-01-02 12:48:31 -06:00
alwaysintreble
72fa19ee1f MultiServer/WebHost: rename all references to forfeit and deprecate it (#1243)
* Webhost: rename all references to forfeit and deprecate it

* needed some renames in multiserver for all the commands to function

* remove forfeit commands

* support forfeit_mode for clients

* rename `forfeit_player` to `release_player`
2023-01-02 12:29:21 -06:00
Zach Parks
d899e918b4 Rogue Legacy: Fix early vendors and architect... again. (#1359) 2023-01-02 12:25:47 -06:00
Zach Parks
33d31c4f0f WebHost: Capitalize Special Range choices to keep consistency. (#1360) 2023-01-02 12:25:33 -06:00
Jarno
9c3c69702a WebHost: Fixed game order by title in Site Map (#1349) 2023-01-02 12:24:08 -06:00
Alchav
dae1a3e0f9 Pokemon R/B: Add Revive to better_shops (#1352) 2023-01-02 12:21:08 -06:00
Alchav
1f1ef10cfe [Pokémon R/B] Fix DeathLink softlock and increment data version (#1348) 2022-12-24 08:25:34 +01:00
Alchav
760af59308 [Pokemon R/B] Fix missing lift key logic 2022-12-23 09:39:21 +01:00
Alchav
3dd7e3e706 [Pokemon R/B] actually implement lose_money_on_blackout 2022-12-23 09:39:21 +01:00
espeon65536
189b129dca oot: repair closed forest + dungeon ER 2022-12-22 06:40:51 +01:00
Fabian Dill
092e8d14ad Core: Make apworlds function mostly before Python 3.10 2022-12-20 17:24:04 +01:00
el-u
4cfc73b582 lufia2ac: rename starting_capsule/starting_party options to default_capsule/default_party 2022-12-19 12:45:49 +01:00
el-u
ff9c11d772 lufia2ac: prevent using party member items if party is full 2022-12-19 12:45:49 +01:00
Alchav
b83aec5c12 [Pokemon R/B] add logic to Fighting Dojo and check for non-vanilla old_man setting for cinnabar gym 2022-12-19 07:04:26 +01:00
Alchav
caf63dd737 [Pokemon R/B] Allow 0 exp setting and add logic rule to Cinnabar Gym to ensure higher level Pokémon are catchable 2022-12-19 07:04:26 +01:00
espeon65536
395d35571c Ocarina of Time 0.3.7 hotfixes (#1336)
- Since multiprocessing and exceptions apparently don't interact well, any exceptions in `generate_output` are handled more gracefully now. The event is always set as well, so the process will no longer hang on an exception there.
- The triforce object file was renamed in 7.0. I didn't change the filename in the code, so it would crash on non-Windows systems.
- Some hint distributions just crash, so I'm temporarily removing them. It will take a while to port the relevant functionality and I'd rather not hold up the next version release.
2022-12-19 07:03:38 +01:00
Fabian Dill
e0be79639c Subnautica: 2.0 compatibility (#1329) 2022-12-17 17:42:02 +01:00
Fabian Dill
37b7f0d32d WebHost: fix LttP tracker crash 2022-12-17 15:22:29 +01:00
el-u
50677ee6a2 lufia2ac: document changes from the vanilla game 2022-12-17 09:26:26 +01:00
PoryGone
f8bc3359c7 SMW: Adjust Butter Bridge 2 - Dragon Coins logic (#1332) 2022-12-17 09:18:23 +01:00
toasterparty
6e537e17e6 [OC2] New Option - Shorter Horde Levels (#1328) 2022-12-16 16:52:15 +01:00
toasterparty
e853fc208b [OC2] Remove DLC items from item pool if vanilla level order (#1323) 2022-12-16 16:48:57 +01:00
toasterparty
1a36da33b4 [OC2] Horde Levels Logically Give 1-Star (#1322) 2022-12-16 16:48:12 +01:00
0rganics
56fc614588 SC2: Bugfix for web tracker (#1324) 2022-12-13 07:35:12 +01:00
espeon65536
47f1fcf382 OoT: Fix region_has_shortcuts crash when boss shuffle is on (#1325) 2022-12-13 07:34:17 +01:00
el-u
51c6be047f Lufia II Ancient Cave: implement new game (#1218)
Co-authored-by: wordfcuk <greili1985@gmail.com>
2022-12-12 02:36:18 +01:00
Fabian Dill
2c46c48ba9 WebHost: reduce tracker refresh delay (#1290) 2022-12-12 00:30:43 +01:00
Alchav
32820ba653 Pokemon R/B: Bug fixes and add trap weights (#1319)
* [Pokemon R/B] Move type chart rando to generate_early and add trap weights

* [Pokemon R/B] Update patching process on client to verify hash
2022-12-11 13:51:28 -06:00
Fabian Dill
6173bc6e03 Core: move create_playthrough under Spoiler as method (#1310)
Core: split create_playthrough, allowing skipping of paths
2022-12-11 13:48:26 -06:00
Magnemania
e71ea94fe5 SC2: Various bugfixes (#1267)
* SC2: Fixed nondeterminism resulting from early unit placement

* SC2: Renamed "world" arguments to "multiworld"

* SC2: Fixed All-In Ground including anti-air in defense score, fixed error in beats_protoss_deathball

* SC2: Fixed No Logic using logic on Beat events

* SC2: Fixed /unfinished command failing when All-In available
2022-12-11 13:46:24 -06:00
0rganics
e3f169b4c3 WebHost: Add SC2WoL game specific tracker (#1270) 2022-12-11 13:43:31 -06:00
kindasneaki
e4e74074f0 Docs: change connecting to archipelago in RoR2 setup guide (#1320)
* Change archipelago mod download page

* Docs: change connecting to archipelago in RoR2 setup guide
2022-12-11 13:38:11 -06:00
Fabian Dill
149630d532 Docs: add remote_start_inventory info in generate_output (#1316) 2022-12-11 14:14:27 +01:00
Fabian Dill
2dcfbff751 Tests: now autoload tests from /worlds/*/test (#1318)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-12-11 13:15:23 +01:00
PoryGone
ec45479c52 SMW: Fix some stages walking Mario onto invalid tile on recomplete (#1317)
If you beat a stage that was already beaten, Mario would sometimes walk onto a tile that he shouldn't.
2022-12-11 04:14:04 +01:00
espeon65536
aee0df5359 Ocarina of Time 7.0 (#1277)
## What is this fixing or adding?
- Adds the majority of OoTR 7.0 features:
  - Pot shuffle, Freestanding item shuffle, Crate shuffle, Beehive shuffle
  - Key rings mode
  - Dungeon shortcuts to speed up dungeons
  - "Regional" shuffle for dungeon items
  - New options for shop pricing in shopsanity
  - Expanded Ganon's Boss Key shuffle options
  - Pre-planted beans
  - Improved Chest Appearance Matches Contents mode
  - Blue Fire Arrows
  - Bonk self-damage
  - Finer control over MQ dungeons and spawn position randomization
- Several bugfixes as a result of the update:
  - Items recognized by the server and valid starting items are now in a 1-to-1 correspondence. In particular, starting with keys is now supported.
  - Entrance randomization success rate improved. Hopefully it is now at 100%. 

Co-authored-by: Zach Parks <zach@alliware.com>
2022-12-11 04:11:40 +01:00
Fabian Dill
2cdd03f786 Network: implement 0.4 marked compatibility removals (#757)
* world remote items handling
* players list when connecting
2022-12-11 02:59:17 +01:00
Zach Parks
ce42fda85f Rogue Legacy: Fix early vendors and architect not being added to pool. (#1314) 2022-12-10 17:24:05 -06:00
Fabian Dill
78a18dee4e Subnautica: give Early Seaglide a display name (#1313) 2022-12-10 11:00:18 -06:00
beauxq
b7d46004e2 Zillion: fix invalid slot data from race condition 2022-12-10 11:15:19 +01:00
Jarno
c3fe341736 Docs: slot_data typing (#1300)
* Docs: slot_data typing

* Properly escaped brackets [ ]
2022-12-09 10:24:08 +01:00
Fabian Dill
79bb43b77c Core: embed custom datapackage into .archipelago (#1288)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-12-08 21:23:31 +01:00
Zach Parks
bedc78d335 Rogue Legacy: Remove relative imports and move to .apworld. (#1304)
* Remove relative import and remove `set_rule` usage.

* Set Rogue Legacy to be .apworld.
2022-12-08 08:54:49 -06:00
t3hf1gm3nt
1b582e5b09 docs: update pokemon and oot setup guides about changing lua options (#1303)
- Pokemon guide was missing a point about changing the Lua option in Bizhawk.
- The same point in the OoT guide was missing a step about restarting Bizhawk after changing the Lua option, so updated that as well.
2022-12-08 10:52:52 +01:00
Alchav
f278dd95c5 [Pokémon R/B] Major bug fix + dialogue change (#1305)
* [Pokemon R/B] Major bug fixes

* [Pokemon R/B] Dialogue updates
2022-12-08 10:51:01 +01:00
PoryGone
92f75f3e03 SA2B: Add missing Whistle location (#1306) 2022-12-08 10:47:39 +01:00
Jarno
f5adc7bdc5 docs: world api fixed link (#1299) 2022-12-08 02:57:49 +01:00
Fabian Dill
78d4da53a7 Tests: verify and fix host.yaml/Utils.py match (#1302) 2022-12-08 02:06:34 +01:00
Alchav
e206c065bf Pokémon Red and Blue: Version 2 (#1282)
Adds Trainersanity option (Each non-scripted trainer has a location check, adding 317 locations)
Adds Randomize Pokedex option. It is required to obtain items from Oak's Aides.
Adds option to add all normal shop items to all normal shops.
Adds DeathLink option.
Adds traps.
Improves Type Chart randomization.
Items can be received during battle.
Stores start inventory in ROM. Only requests remote start inventory if patch is from v1.
Fixes logic bugs.
Various other improvements.
2022-12-08 00:38:34 +01:00
Fabian Dill
5273812039 Core: default distribute Factorio and Subnautica as .apworld (#1260) 2022-12-06 23:40:30 -06:00
Fabian Dill
7c3af68e59 ItemLinks: allow linking replacement items as well (#1274) 2022-12-06 23:37:47 -06:00
PoryGone
449973687b SA2B: v2.0 Content Update (#1294)
Changelog:

Features:
- Completely reworked mission progression system
  - Control of which mission types can be active per-gameplay-style
  - Control of how many missions are active per-gameplay-style
  - Mission order shuffle
- Two new Chaos Emerald Hunt goals
  - `Chaos Emerald Hunt` involves finding the seven Chaos Emeralds and beating Green Hill
  - `FinalHazard Chaos Emerald Hunt` is the same, but with the FinalHazard fight at the end of Green Hill
- New optional Location Checks
  - Keysanity (Chao Containers)
  - Whistlesanity (Animal Pipes and hidden whistle spots)
  - Beetlesanity (Destroying Gold Beetles)
- Option to require clearing all active Cannon's Core Missions for access to the Biolizard fight in `Biolizard` goal
- Hard Logic option
- More Music Options
  - Option to use SADX music
  - New `Singularity` music shuffle option
- Option to choose the Narrator theme 
- New Traps
  - Tiny Trap is now permanent within a level
  - Gravity Trap
  - Exposition Trap
  
Quality of Life:
- Significant revamp to Stage Select screen information conveyance
  - Icons are displayed for:
    - Relevant character's upgrades
    - Which location checks are active/checked
    - Chaos Emeralds found (if relevant)
    - Gate and Cannon's Core emblem costs
  - The above stage-specific info can also be viewed when paused in-level
    - The current mission is also displayed when paused
- Emblem Symbol on Mission Select subscreen now only displays if a high enough rank has been gotten on that mission to send the location check
- Hints including SA2B locations will now specify which Gate that level is located in
- Save file now stores slot name to help prevent false location checks in the case of one player having multiple SA2B slots in the same seed
- Chao Intermediate and Expert race sets are now swapped, per player feedback
  - Intermediate now includes Beginner + Challenge + Hero + Dark
  - Expert now includes Beginner + Challenge + Hero + Dark + Jewel
- New mod config option for the color of the Message Queue text

Bug Fixes:
- Fixed bug where game stops properly tracking items after 127 have been received.
- Several logic fixes
- Game now refers to `Knuckles - Shovel Claws` correctly
- Minor AP World code cleanup
2022-12-07 06:20:02 +01:00
Fabian Dill
f5638552cc Docs: add ClassVar marker to World class (#1224) 2022-12-06 23:12:56 -06:00
Fabian Dill
78ee19de51 Pokemon R/B: always bind file handling to client (#1261) 2022-12-06 23:02:54 -06:00
Zach Parks
82444229be Rogue Legacy: More refactoring and clean up. (#1297)
* Rogue Legacy: More refactoring and clean up.

* Also marked Blacksmith as Progression as it's used in a rule.

* Remove extra newline.

* Prevent divide by zero type error.

* Scratch last commit, got the math mixed in my head.

* Clarified name of rule regarding percentage of stat upgrades.

* Move early vendors/architect creation into `create_items` logic.

* Rename parameter in `create_region`.

* Rename local var in `create_region`.

* Removed accidental links in Markdown docs.

* Refactor `create_region` signature and caller.

* Remove redundant if-else.

* Revert change to if-else, and moved item_pool to function instead of obj var.

* Rename LegacyLogic to RLLogic.

* Remove LogicMixin for rules.
2022-12-06 20:49:55 -06:00
alwaysintreble
2cc03d003a Core: fix bug that caused world option overrides to fail (#1293)
* core: fix bug that caused world option overrides to fail

* copy paste sliver's better code that works as intended

* Fix whitespace

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-12-06 00:50:11 +01:00
recklesscoder
0e4fa378dd WebHost: Detect confusion of settings zip and seed zip (#1227) 2022-12-06 00:40:51 +01:00
Fabian Dill
ffc000ec91 Network: remove deprecated IgnoreGame tag 2022-12-05 23:20:19 +01:00
Fabian Dill
32b8f9f9f3 WebHost: restore old silent ignore of mimetypes in json getting (#1292)
* WebHost: restore old silent ignore of mimetypes in json getting of /api/generate

* Tests: add tests for /api/generate
2022-12-05 22:27:15 +01:00
Yoshi348
4412434976 meta.yaml: update progression balancing (#1283)
* Update meta.yaml to numeric progression balancing

Instead of the old on/off system

* normal/disabled
2022-12-05 22:26:44 +01:00
espeon65536
9bdbced51f Hylics 2: create victory location earlier to ensure it is cached correctly (#1291)
Fixes generation issues where the victory location could not be found from MultiWorld.get_locations
2022-12-04 21:04:01 -06:00
Fabian Dill
bd574ef261 WebHost: save datatables state (#1145)
* WebHost: save datatables state

* WebHost: Fix DataTables local storage keys.

Co-authored-by: recklesscoder <57289227+recklesscoder@users.noreply.github.com>
2022-12-04 20:39:07 -06:00
BordynConfused
45719eb7e0 Hylics2:Logic Fixes (#1281)
Added Juice Ranch and Worm Pod to logic

Extra parenthesis removed

* Delete log.txt
Not used

Transitioned all whitespace characters to Linux /n from Windows /r/n

Updated rules for Rescue Blerol 1 and 2 to avoid impossible seed generation.  Fixed member requirements for Vault Bomb. Added Juice Ranch: Fridge for consistency with other checks behind combat. Added party_shuffle rules to vault medallions, removed jail key requirement (not needed)
2022-12-05 02:25:16 +01:00
kindasneaki
d81fd280fa RoR2: Change risk of rain mod download page link (#1269) 2022-12-04 19:16:03 -06:00
Fabian Dill
6b57275859 Server: attempt to make nothing found message clearer (#1289) 2022-12-04 19:06:13 -06:00
Jarno
63f012cce7 Timespinner: Enter Sandman flag (#1263) 2022-12-04 22:02:46 +01:00
Fabian Dill
679cb3e197 Factorio: fix revealed tech tree hinting old location names 2022-12-04 21:29:35 +01:00
Fabian Dill
38b5a90c07 WebHost: update modules 2022-12-04 21:29:02 +01:00
Fabian Dill
203f17f0f6 Subnautica: prompt users to update 2022-12-04 21:28:44 +01:00
Fabian Dill
65995cd586 Network: implement read_only datastore keys: hints and slot_data (#1286)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-12-03 23:29:33 +01:00
PoryGone
64e2d55e92 SMW: Bug+logic fixes (#1279)
* Allow levels to scroll vertically without climb or run

* Account for needing Yoshi for VB2 Dragon Coins

* Don't allow messages on intro level
2022-12-02 06:25:02 +01:00
lordlou
ef66f64030 SMZ3: shop check fix (#1257) 2022-12-01 03:51:29 +01:00
NewSoupVi
e641c3ca1b The Witness: Fix Expert PP2 Access Logic 2022-12-01 03:46:11 +01:00
alwaysintreble
111c3186bd Core: change signatures in autoworld from world to multiworld (#1273) 2022-12-01 03:45:22 +01:00
toasterparty
f0e9080108 [OC2] Difficulty Adjustment/Documentation (#1278)
* Slightly relax difficulty of final level

* Table to help translate player skill to yaml setting
2022-12-01 03:44:08 +01:00
Jarno Westhof
fd8867c782 [Witness, Docs] Updated link to repository owner, as repository was transferred 2022-12-01 03:42:49 +01:00
Fabian Dill
f81d2653e0 Subnautica: implement swim_rule option and skip goal prog balancing (#1258) 2022-11-28 07:43:04 +01:00
Fabian Dill
1288f15e45 Core: Fill fix local logic conflict (#1271) 2022-11-28 07:03:09 +01:00
Fabian Dill
cde2a6e754 Core: log additional system info that may be relevant to errors (#1255) 2022-11-27 19:52:36 -06:00
Jarno
81dd1e359b Sudoku: Updated text about duplicated hints now the client has been improved (#1264) 2022-11-27 19:30:35 -06:00
alwaysintreble
8dffd87bee LttP: fix open pyramid settings parsing (#1253)
* lttp: fix open pyramid settings prasing

* accidentally left default changed when committing
2022-11-27 19:29:18 -06:00
Fabian Dill
67be80e59d Docs: datapackage typing (#1229)
* Docs: add ClassVar marker to World class

* Docs: add typing to network_data_package
2022-11-27 19:25:53 -06:00
recklesscoder
ff1f5569e7 Factorio: Option descriptions, peaceful mode setup, docs (#1233)
* Docs/Factorio: Flesh out lacking option descriptions.

* Docs/Factorio: Added instructions on peaceful mode.

* Docs/Factorio: Use subheadings for each "other setting".

* Docs/Factorio: Mention that `tech_tree_information: full` grants hints.

* Docs/Factorio: Instructions to use .yaml checker after editing your .yaml manually.
2022-11-27 04:01:10 +01:00
NewSoupVi
8b9b482972 The Witness: Logic Fix (Broken Seed reported)
Jungle Discard requires Arrows, not Triangles
2022-11-27 02:08:56 +01:00
tctompk
d0ce44cd38 Pokemon RB: Fix broken link in Pokemon setup instructions (#1254)
The setup_en.md file had an un-escaped URL that was breaking a link; this fixes the link by escaping the spaces in the URL.
2022-11-24 21:11:46 +01:00
PoryGone
aae78a8a12 Core: Add MultiServer command to check a specific location (#1242) 2022-11-20 22:38:34 +01:00
Fabian Dill
7a5e11e8d4 Core: streamline Multiworld.get_*_locations calls (#1234) 2022-11-20 20:50:32 +01:00
Zach Parks
a9ab53cb8b WebHost/Core: Defer creating slots that have patch files until after multidata is loaded and remove redundant code. (#1250)
* WebHost: On uploads, infer player name if missing in file name.

* Remove conditional logic for not including player name in file name.

* quick readability tweak to "fix"

* Refactored `upload_zip_to_db` to clean up redundancies and fix issues

* Rename `patches` to `files`

* fix comment
2022-11-20 13:39:52 -06:00
Fabian Dill
5ed8c2e1c0 Core: datetime-tag log files and delete old ones 2022-11-20 18:43:44 +01:00
Fabian Dill
67128ece38 LttP: make xxtea only required for race generation 2022-11-20 18:43:26 +01:00
Rome Reginelli
8aed24151f SNIClient: fix SNI not launching with old host.yaml (#1249)
This happens when switching from Appimage 0.3.5 to Appimage 0.3.6 without deleting the user's host.yaml
2022-11-20 11:14:13 +01:00
black-sliver
3e6c097348 SoE: update source wheel for py3.11 on windows 2022-11-18 01:43:53 +01:00
Fabian Dill
8ce3fd5518 Core: update cx-Freeze 2022-11-17 23:53:50 +01:00
Alchav
93a354cd81 [Core] Item plando early locations (and non-early locations) (#1228) 2022-11-17 17:40:44 +01:00
espeon65536
774581b7ba HK: fix crash if shop locations are at max and extra shop slots is nonzero 2022-11-17 17:36:42 +01:00
NewSoupVi
95f90851ac The Witness: Update docs, credits, junk hints (#1240)
* Updated Credits & AP-game hints

* Updated docs
2022-11-17 17:35:59 +01:00
PoryGone
1cd1bfea4d SMW: Prevent Killing Bowser on Yoshi Egg Hunt (#1241)
* Make Bowser unkillable on Egg Hunt

Changed a location name.
2022-11-16 20:20:55 +01:00
espeon65536
edd1fff4b7 Core: make early_items internal only (#1177)
Co-authored-by: beauxq <beauxq@yahoo.com>
2022-11-16 17:32:33 +01:00
Zach Parks
4d79920fa6 Merge pull request #944
* WebHost: Remove "Random" as an option and move to separate button in …

* Merge branch 'main' into randomize-button

* Tweaked color and changed text of tooltip.

* Merge branch 'main' into randomize-button
2022-11-12 22:03:44 -05:00
recklesscoder
7665935227 Merge pull request #1231
* Web/Style: Fix code block padding.
2022-11-12 21:58:11 -05:00
Zach Parks
5139475068 Slay the Spire: Correct tool tip for heart_run to match actual behaviour (#1236) 2022-11-12 18:52:36 -08:00
PoryGone
adcee639a2 SMW: Adjust Early Double Levels (#1238) 2022-11-13 01:33:22 +01:00
Magnemania
fde97fca5b SC2: Increment required client version (#1237) 2022-11-13 01:32:33 +01:00
recklesscoder
e108b67ca5 Docs/OoT: Add savestate fix and links to Bizhawk and Logic wiki pages. 2022-11-12 02:19:26 +01:00
NewSoupVi
17da06f763 The Witness: Fix "doors: panels" items referring to the wrong panels 2022-11-12 02:18:51 +01:00
PoryGone
2ff737175f SMW: Fix Baby Yoshi and Rope Jump Exploits (#1226)
* Fix Baby Yoshi and Rope Jump Exploits

* Fix logic error of Forest of Illusion 3 Secret Exit
2022-11-11 11:50:29 +01:00
NewSoupVi
b0b8268249 Witness: Expert logic broken seed fixes (by IHNN) (#1223)
Co-authored-by: metzner <unconfigured@null.spigotmc.org>
2022-11-10 04:05:00 +01:00
espeon65536
4e5c10ad66 OoT: make Bottles and Adult Trade Item hintable groups (#1222)
* OoT: make Bottles and Adult Trade Item hintable groups
2022-11-09 22:07:14 +01:00
Alchav
350e1e6287 Pokemon Red and Blue: lua updates (#1216) 2022-11-09 15:15:16 +01:00
Magnemania
63c0d027e7 SC2: Bugfixes for YAML Option Interactions (#1215)
Early Unit: Now respects Excluded Items when selecting a random unit.

Units Always Have Upgrades: Changed removal cascade behavior; now additionally checks to see if an associated item is already locked when attempting to remove, and locks associated items if so. Occasionally caused issues with starter items in the past, frequently caused issues with yaml-defined Locked Items.
2022-11-09 13:53:55 +01:00
jtoyoda
a014bb4ab7 FF1: Updating FFR Docs for new Archipelago location (#1221) 2022-11-08 18:58:29 -06:00
black-sliver
0d10fec395 DS3: update data_version (#1220) 2022-11-09 01:53:08 +01:00
Ludovic Marechal
0cbee4ac3e DS3: Add progressive locations, fix the randomize_weapons_level option and add some options ( Deathlink ) (#1206)
* Update items_data.py

added `Red and White Round Shield`, `Crystal Scroll`, `Magic Stoneplate Ring`, and `Outrider Knight` gear.

* Update locations_data.py

Added `US: Red and White Round Shield`, `CKG: Magic Stoneplate Ring`, `GA: Outrider Knight` set, and `GA: Crystal Scroll`

* Update __init__.py

Add `Karla's Ashes` requirements

* Update items_data.py

Add `Irithyll Rapier, Hollow's Ashes, Irina's Ashes, Karla's Ashes, Cornyx's Ashes, and Orbeck's Ashes`

* Update locations_data.py

Add `Irithyll Rapier, Hollow's Ashes, Irina's Ashes, Karla's Ashes, Orbeck's Ashes, and Cornyx's Ashes`

* Update items_data.py

removed "hollows ashes"

* Update locations_data.py

remove "hollows ashes"

* Revert "WebHost: Add the DarkSouls3 entry to upload and download the client file"

This reverts commit 5e7c2d4cee.

* ds3: setup progressive locations

* ds3: Use fill_slot_data instead of generate_output

* ds3: Add no_spell_requirements, no_equip_load and death_link option

* ds3: Add some progressive locations

* DS3: Increment data_version

* DS3: Fix item name in rule

* DS3: Set required client version to 0.3.6 and added offsets between items and location tables for backward compatibility

* DS3: Resolve Python 3.8 compatibility

* DS3: Removed useless region for locations IDs consistency

* DS3: Changed i in loop

* DS3: Remove AP.json from the documentation

* DS3: Put back json upload and download

* DS3: Avoid empty downloads

* DS3: Fix randomize_weapons_level option

* DS3: Remove options duplicate entries

* DS3: Change location rule according to review

Co-authored-by: Br00ty <83629348+Br00ty@users.noreply.github.com>
2022-11-09 01:17:43 +01:00
toasterparty
70cab99caf OC2: Fix typo in description (#1214) 2022-11-07 19:50:30 +01:00
Fabian Dill
c1e97bcbff WebHost: allow setting a generation time limit (#1209)
* WebHost: allow setting a generation time limit.
2022-11-06 21:37:11 +01:00
shadow42085
e2eaafbf70 WebHost: Update Super Metroid Tracker Links (#1211)
varia randomizer site updated
2022-11-06 14:31:02 -06:00
NewSoupVi
66d594e95b The Witness: Add required client version (#1212)
Co-authored-by: metzner <unconfigured@null.spigotmc.org>
2022-11-06 14:26:56 -06:00
Fabian Dill
a9bf0008ba Factorio: add rocket-silo as required technology (#1207) 2022-11-06 14:24:03 -06:00
NewSoupVi
f2426ae603 The Witness: Logic Fix (#1210)
Co-authored-by: metzner <unconfigured@null.spigotmc.org>
2022-11-06 17:31:53 +01:00
Zach Parks
462ddce72c Templates: Remove auto wordwrap, fix manual indentation, and reformat docstrings for world options in main. (#1201)
* Fix wrapping too early if docstring is within 120 characters and re-indent other lines.

* Remove auto-word wrapping and tweaked all worlds' option docstrings formatting.

Options should wrap around the 120 character mark to prevent template files from being too long horizontally. This also allows manual-indentation to work again.

* Fix missing '#' on empty lines in output from docstring.
2022-11-06 08:28:16 -06:00
Alchav
d10bb3c6c1 [Pokémon Red and Blue] more improvements (#1208)
* Generated patch includes base patch

* location ID range start match item ID start

* remove unused import

* Change Oak's Aides defaults to be more sync-friendly
2022-11-06 09:07:41 +01:00
Fabian Dill
61232ca756 Factorio: set useless technologies to be researched from the start (#1205) 2022-11-05 14:01:02 -05:00
AkumaGath17
8f325a4f2b [Minecraft] AP Tracker (Items & Advancements) Update to 1.19 (#1168)
* Tracker hud test

* Added Relevant Items Icon

* Minecraft Tracker Update Test

* Minecraft Tracker Update

* Minecraft Tracker Missing Advancements

* Removed Enchanted Books

* Revert fix

* Added Relevant Books

* Tracker Update

* Minecraft Tracker Update (Saddle 3D Model to Icon)

Co-authored-by: AkumaDark17 <akumaspartanmc@hotmail.com>
2022-11-05 06:41:03 -07:00
NewSoupVi
d28738a918 The Witness: Logic Fix & Generation Fix (#1204)
* Logic fix

* static object modification instead of copy error fixed

Co-authored-by: metzner <unconfigured@null.spigotmc.org>
2022-11-04 23:10:29 -05:00
PoryGone
1f3d048462 DKC3: Fix Generation Hang from missed self.world reference (#1203)
* Baseline patching and logic for DKC3

* Client can send, but not yet receive

* Alpha Test Baseline

* Bug Fixes and Starting Lives Option

* Finish BBH, add world hints

* Add music shuffle

* Boomer Costs Text

* Stubbed in Collect behaviour

* Adjust Gyrocopter option

* Add Bonus Coin junk replacement and tracker support

* Delete bad logs

* Undo host.yaml change

* Refactored SNIClient

* Make Swanky Free

* Fix Typo

* Undo SNIClient run_game hack

* Fix Typo

* Remove Bosses from Level Shuffle

* Remove duplicate kivy Data

* Add DKC3 Docs and increment Data version

* Remove dead code

* Fix mislabeled region

* Add Dark Souls 3 to README

* Always force Cog on Rocket Rush Flag

* Fix Single Ski lock and too many DK Coins

* Update Retroarch version number

* Don't send DKC3 through LttP Adjuster

* Comment Location ROM Table

* Change ROM Hash prefix to D3

* Remove redundant constructor

* Add ROM Change Safeguards

* Properly mark WRAM accesses

* Remove outdated region connect

* Fix syntax error

* Fix Game description

* Fix SNES Bank Access

* Add isso_setup for DKC3

* Double Quote strings

* Escape single quotes I guess

* Add two locations to Trade Sequence List

* Remove trace sequence locations from ROM data dict

* Prevent Krematoa Crash, add crash robustness

* Remove print statements

* Don't remove ctx.rom if save file dies

* Consolidate logic for readability

* Add link to tracker on DKC3 Setup doc

* Remove false information from setup guide

* Fix file extension in setup doc

* Bug Fixes

* Add KONGsanity and cheat options

* First Pass Collect behavior

* Fix level unlock data

* Make ! only indicate KONG letters on KONGsanity

* Fix Level Name in locations

* Adjust junk pool logic

* Fix Knautilus Connections

* Fix Wrinkly Softlock

* Fix missed world reference
2022-11-04 21:40:31 -05:00
PoryGone
b161a5241f SMW: Add link to Tracker in setup guide (#1202) 2022-11-04 21:36:58 -05:00
Fabian Dill
208a0c6b08 WebHost: prevent infinite spinup loop of Rooms 2022-11-04 20:07:28 +01:00
Jarno
c3c1ce5827 Sudoku: Fixed setup link (#1200) 2022-11-04 20:03:39 +01:00
recklesscoder
889bc9d1b4 FactorioClient: Warn about Windows console input. 2022-11-04 18:00:22 +01:00
recklesscoder
165a38dd58 Factorio: Added commands for checking energy link from CLI and in game. 2022-11-04 17:59:05 +01:00
recklesscoder
88088dd054 CommonClient & SNIClient: Fixes for reconnecting (#1193)
* CommonClient & SNIClient: Fixes for reconnecting.
- CommonClient: Allow manual reconnect by typing /connect.
- CommonClient: Don't prompt to reconnect if there is nothing to reconnect to.
- CommonClient: Hide the connection loss modal popup when attempting to connect again.
- CommonClient & SNIClient: Cancel auto-reconnect tasks when the user intervenes.

* (Fix imports for linting.)
2022-11-04 17:57:58 +01:00
Doug Hoskisson
c933fa7e34 Core: optimize early items and add unit test (#1197)
* optimize early items and add unit test

* move sorting list init closer to sorting
2022-11-04 17:56:47 +01:00
alwaysintreble
f1123f2662 Core: more useful error for entrance without region (#1186) 2022-11-03 15:17:34 +01:00
Fabian Dill
0f034ddcf7 Factorio: make client use CommonClient location lookup for technologies 2022-11-03 14:19:47 +01:00
recklesscoder
7f3eda4623 Subnautica: Fix creature_scan_logic: removed allowing Cuddlefish. 2022-11-03 10:48:50 +01:00
alwaysintreble
2b0e7f05da Docs: fix broken link in contributing.md (#1185) 2022-11-02 22:02:06 -05:00
recklesscoder
e204deab02 Factorio: Fix saving on exit on Windows.
Fix detecting of server shutdown.
2022-11-03 00:17:26 +01:00
Magnemania
56afd62175 SC2: Bugfix, incorrectly adding to an array rather than appending (#1183) 2022-11-02 17:01:51 -05:00
Alchav
44204ac9be Pokemon Red/Blue: Fix location name in Pokémon Red and Blue rules (#1184) 2022-11-02 17:01:32 -05:00
espeon65536
6c3852a2a9 OoT: lock fast-filled shop locations (#1188)
ensures that progression balancing doesn't move them later
2022-11-02 17:01:03 -05:00
Jarno
124ae198e4 Sudoku: Updated Sudoku docs now we use standalone deployment (#1180) 2022-11-02 20:21:34 +01:00
alwaysintreble
030b767751 LttP: fix triforce piece same reference (#1179) 2022-11-02 20:08:38 +01:00
NewSoupVi
5ca724a454 The Witness: Logic fix
A broken seed was reported where Colored Squares was on a panel locked by Colored Squares. This PR fixes this problem.
2022-11-02 20:07:43 +01:00
Jarno
af3b752093 WebHost: Fixed warning ".gitignore dropped, as it has no valid sprite data." (#1174)
* Fixed warning ".gitignore dropped, as it has no valid sprite data." on webhost

* Changed to exclude files starting with .
2022-11-02 19:06:00 +01:00
alwaysintreble
c378933274 Overcooked 2: Revert OC2Level world references to be named correctly, optimize location scouting. (#1175)
* fix oc2 level world references

* optimize the local items discovery a bit
2022-11-02 16:11:46 +01:00
Doug Hoskisson
da392239a0 MultiServer and clients: async coroutine starter in Utils.py (#1143)
* async coroutine starter in Utils.py

* refactor from static class to function

* async_start docstring
2022-11-02 15:51:35 +01:00
espeon65536
a6e1e14fee Ocarina of Time: Itemlinks and bugfixes (#1157)
* OoT: ER improvements
Include dungeon rewards in itempool to allow for ER improvement
Better validate_world function by checking for multi-entrance incompatibility more efficiently
Fix some generation failures by ensuring all entrances placed with logic
Introduce bias to some interior entrance placement to improve generation rate

* OoT: fix overworld ER spoiler information

* OoT: rewrite dungeon item placement algorithm
in particular, no longer assumes that exactly the number of vanilla keys is present, which lets it place more or fewer items.

* OoT: auto-send more locations
Now should autosend cows, DMT/DMC great fairies, medigoron, and bombchu salesman
This should be every check autosending. these ones are super weird for some reason and didn't get fixed with the others

* OoT: add items forced local by settings to AP's local_items

* OoT: fast-fill shop junk items

* OoT: ensure that Kokiri Shop is always reachable immediately in closed forest
hence Deku Shield can be bought to leave the forest

* OoT: randomize internal connect name
Connect name is now a random 16-character string.
This should prevent any issues with connecting to a room with the wrong ROM with probability almost 1.

* OoT: introduce TrackRandomRange for trials hint and mq dungeon maps

* OoT: enable proper itemlinking of songs and dungeon items, with restricted placements according to player settings

* OoT: barren hint oversight fix

* OoT: allow NL + ER to roll properly

* OoT: 3.8 compatibility
set and list builtins don't have proper typing support until 3.9, apparently

* OoT: remove Gerudo Membership Card location from the pool if fortress open and card not randomized
another long-standing bug squished

* OoT: exclude locations in the itemlink song fill if they aren't also priority

* OoT: prevent data bleed when client isn't closed between different game connections
I don't understand why people keep doing this

* OoT: linter appeasement
it was a real error though

* fixing merge conflicts is hard

* oot merge update #2

* OoT: removed accidentally duplicated code
2022-11-02 09:32:08 +01:00
alwaysintreble
95378233fc Templates: Update template output and add min and max comments for named special_range options. (#1164)
* add min and max comments for named special_range options

* comment all special range options and dictate the min and max in comment block

* make it cleaner

* make it cleanerer

* make it cleanererer

* Reformat template for more consistent comments.

* Fixed missing note on some special settings.

* Small tweak to template.

* Update playerSettings.yaml to match auto-generated template with all ALTTP options.

* Fix edge case with `special_range_cutoff` and revert playerSettings.yaml.

Co-authored-by: Zach Parks <zach@alliware.com>
2022-11-01 18:07:56 -05:00
Fabian Dill
85130f2bbd WebHost: update modules (#1176)
Fix bokeh 3.0.0 attribute removal and touch up stats.py while at it
2022-11-01 17:01:53 -05:00
Ludovic Marechal
ab9f3767e2 DS3: Use slot_data instead of the external Json file (#1155)
* Update items_data.py

added `Red and White Round Shield`, `Crystal Scroll`, `Magic Stoneplate Ring`, and `Outrider Knight` gear.

* Update locations_data.py

Added `US: Red and White Round Shield`, `CKG: Magic Stoneplate Ring`, `GA: Outrider Knight` set, and `GA: Crystal Scroll`

* Update __init__.py

Add `Karla's Ashes` requirements

* Update items_data.py

Add `Irithyll Rapier, Hollow's Ashes, Irina's Ashes, Karla's Ashes, Cornyx's Ashes, and Orbeck's Ashes`

* Update locations_data.py

Add `Irithyll Rapier, Hollow's Ashes, Irina's Ashes, Karla's Ashes, Orbeck's Ashes, and Cornyx's Ashes`

* Update items_data.py

removed "hollows ashes"

* Update locations_data.py

remove "hollows ashes"

* Revert "WebHost: Add the DarkSouls3 entry to upload and download the client file"

This reverts commit 5e7c2d4cee.

* ds3: Use fill_slot_data instead of generate_output

* DS3: Increment data_version

* DS3: Fix item name in rule

* DS3: Set required client version to 0.3.6 and added offsets between items and location tables for backward compatibility

* DS3: Resolve Python 3.8 compatibility

* DS3: Removed useless region for locations IDs consistency

* DS3: Changed i in loop

* DS3: Remove AP.json from the documentation

* DS3: Put back json upload and download

* DS3: Avoid empty downloads

(cherry picked from commit c4c485140d)

Co-authored-by: Br00ty <83629348+Br00ty@users.noreply.github.com>
2022-11-01 22:58:08 +01:00
Zach Parks
bf142b32c9 Rogue Legacy: Rename world to multiworld in local variables and function signatures. (#1169) 2022-11-01 16:14:09 -05:00
lordlou
05c06a57af SMZ3: completion condition fix and more info in spoiler (#1133)
* first working (most of the time) progression generation for SM using VariaRandomizer's rules, items, locations and accessPoint (as regions)

* first working single-world randomized SM rom patches

* - SM now displays message when getting an item outside for someone else (fills ROM item table)

This is dependant on modifications done to sm_randomizer_rom project

* First working MultiWorld SM

* some missing things:

- player name inject in ROM and get in client
- end game get from ROM in client
- send self item to server
- add player names table in ROM

* replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better)

* - reenabled balancing

* post rebase fixes

* updated SmClient.py

* + added VariaRandomizer LICENSE

* + added sm_randomizer_rom project (which builds sm.ips)

* Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning

* properly revert change made to CollectionState and more cleaning

* Fixed multiworld support patch not working with VariaRandomizer's

* missing file commit

* Fixed syntax error in unused code to satisfy Linter

* Revert "Fixed multiworld support patch not working with VariaRandomizer's"

This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b.

* many fixes and improovement

- fixed seeded generation
- fixed broken logic when more than one SM world
- added missing rules for inter-area transitions
- added basic patch presence for logic
- added DoorManager init call to reflect present patches for logic
- moved CollectionState addition out of BaseClasses into SM world
- added condition to apply progitempool presorting only if SM world is present
- set Bosses item id to None to prevent them going into multidata
- now use get_game_players

* first working (most of the time) progression generation for SM using VariaRandomizer's rules, items, locations and accessPoint (as regions)

* first working single-world randomized SM rom patches

* - SM now displays message when getting an item outside for someone else (fills ROM item table)

This is dependant on modifications done to sm_randomizer_rom project

* First working MultiWorld SM

* some missing things:

- player name inject in ROM and get in client
- end game get from ROM in client
- send self item to server
- add player names table in ROM

* replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better)

* - reenabled balancing

* post rebase fixes

* updated SmClient.py

* + added VariaRandomizer LICENSE

* + added sm_randomizer_rom project (which builds sm.ips)

* Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning

* properly revert change made to CollectionState and more cleaning

* Fixed multiworld support patch not working with VariaRandomizer's

* missing file commit

* Fixed syntax error in unused code to satisfy Linter

* Revert "Fixed multiworld support patch not working with VariaRandomizer's"

This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b.

* many fixes and improovement

- fixed seeded generation
- fixed broken logic when more than one SM world
- added missing rules for inter-area transitions
- added basic patch presence for logic
- added DoorManager init call to reflect present patches for logic
- moved CollectionState addition out of BaseClasses into SM world
- added condition to apply progitempool presorting only if SM world is present
- set Bosses item id to None to prevent them going into multidata
- now use get_game_players

* Fixed multiworld support patch not working with VariaRandomizer's

Added stage_fill_hook to set morph first in progitempool
Added back VariaRandomizer's standard patches

* + added missing files from variaRandomizer project

* + added missing variaRandomizer files (custom sprites)

+ started integrating VariaRandomizer options (WIP)

* Some fixes for player and server name display

- fixed player name of 16 characters reading too far in SM client
- fixed 12 bytes SM player name limit (now 16)
- fixed server name not being displayed in SM when using server cheat ( now displays RECEIVED FROM ARCHIPELAGO)
- request: temporarly changed default seed names displayed in SM main menu to OWTCH

* Fixed Goal completion not triggering in smClient

* integrated VariaRandomizer's options into AP (WIP)

- startAP is working
- door rando is working
- skillset is working

* - fixed itemsounds.ips crash by always including nofanfare.ips into multiworld.ips (itemsounds is now always applied and "itemsounds" preset must always be "off")

* skillset are now instanced per player instead of being a singleton class

* RomPatches are now instanced per player instead of being a singleton class

* DoorManager is now instanced per player instead of being a singleton class

* - fixed the last bugs that prevented generation of >1 SM world

* fixed crash when no skillset preset is specified in randoPreset (default to "casual")

* maxDifficulty support and itemsounds removal

- added support for maxDifficulty
- removed itemsounds patch as its always applied from multiworld patch for now

* Fixed bad merge

* Post merge adaptation

* fixed player name length fix that got lost with the merge

* fixed generation with other game type than SM

* added default randoPreset json for SM in playerSettings.yaml

* fixed broken SM client following merge

* beautified json skillset presets

* Fixed ArchipelagoSmClient not building

* Fixed conflict between mutliworld patch and beam_doors_plms patch

- doorsColorsRando now working

* SM generation now outputs APBP

- Fixed paths for patches and presets when frozen

* added missing file and fixed multithreading issue

* temporarily set data_version = 0

* more work

- added support for AP starting items
- fixed client crash with gamemode being None
- patch.py "compatible_version" is now 3

* commited missing asm files

fixed start item reserve breaking game (was using bad write offset when patching)

* Nothing item are now handled game-side. the game will now skip displaying a message box for received Nothing item (but the client will still receive it).

fixed crash in SMClient when loosing connection to SNI

* fixed No Energy Item missing its ID

fixed Plando

* merge post fixes

* fixed start item Grapple, XRay and Reserve HUD, as well as graphic beams (except ice palette color)

* fixed freeze in blue brinstar caused by Varia's custom PLM not being filled with proper Multiworld PLM address (altLocsAddresses)

* fixed start item x-ray HUD display

* Fixed start items being sent by the server (is all handled in ROM)

Start items are now not removed from itempool anymore
Nothing Item is now local_items so no player will ever pickup Nothing. Doing so reduces contribution of this world to the Multiworld the more Nothing there is though.
Fixed crash (and possibly passing but broken) at generation where the static list of IPSPatches used by all SM worlds was being modified

* fixed settings that could be applied to any SM players

* fixed auth to server only using player name (now does as ALTTP to authenticate)

* - fixed End Credits broken text

* added non SM item name display

* added all supported SM options in playerSettings.yaml

* fixed locations needing a list of parent regions (now generate a region for each location with one-way exits to each (previously) parent region

did some cleaning (mainly reverts on unnecessary core classes

* minor setting fixes and tweaks

- merged Area and lightArea settings
- made missileQty, superQty and powerBombQty use value from 10 to 90 and divide value by float(10) when generating
- fixed inverted layoutPatch setting

* added option start_inventory_removes_from_pool

fixed option names formatting
fixed lint errors
small code and repo cleanup

* Hopefully fixed ROR2 that could not send any items

* - fixed missing required change to ROR2

* fixed 0 hp when respawning without having ever saved (start items were not updating the save checksum)

* fixed typo with doors_colors_rando

* fixed checksum

* added custom sprites for off-world items (progression or not)

the original AP sprite was made with PierRoulette's SM Item Sprite Utility by ijwu

* - added missing change following upstream merge

- changed patch filename extension from apbp to apm3 so patch can be used with the new client

* added morph placement options: early means local and sphere 1

* fixed failing unit tests

* - fixed broken custom_preset options

* - big cleanup to remove unnecessary or unsupported features

* - more cleanup

* - moved sm_randomizer_rom and all always applied patches into an external project that outputs basepatch.ips

- small cleanup

* - added comment to refer to project for generating basepatch.ips (https://github.com/lordlou/SMBasepatch)

* fixed g4_skip patch that can be not applied if hud is enabled

* - fixed off world sprite that can have broken graphics (restricted to use only first 2 palette)

* - updated basepatch to reflect g4_skip removal

- moved more asm files to SMBasepatch project

* - tourian grey doors at baby metroid are now always flashing (allowing to go back if needed)

* fixed wrong path if using built as exe

* - cleaned exposed maxDifficulty options

- removed always enabled Knows

* Merged LttPClient and SMClient into SNIClient

* added varia_custom Preset Option that fetch a preset (read from a new varia_custom_preset Option) from varia's web service

* small doc precision

* - added death_link support

- fixed broken Goal Completion
- post merge fix

* - removed now useless presets

* - fixed bad internal mapping with maxDiff

- increases maxDiff if only Bosses is preventing beating the game

* - added support for lowercase custom preset sections (knows, settings and controller)

- fixed controller settings not applying to ROM

* - fixed death loop when dying with Door rando, bomb or speed booster as starting items

- varia's backup save should now be usable (automatically enabled when doing door rando)

* -added docstring for generated yaml

* fixed bad merge

* fixed broken infinity max difficulty

* commented debug prints

* adjusted credits to mark progression speed and difficulty as Non Available

* added support for more than 255 players (will print Archipelago for higher player number)

* fixed missing cleanup

* added support for 65535 different player names in ROM

* fixed generations failing when only bosses are unreachable

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

* fixed failling generations when using 'fun' settings

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

* fixed debug logger

* removed unsupported "suits_restriction" option

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

* - fixed deathlink emptying reserves

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

* - merged death_link and death_link_survive options

* fixed death_link

* added a fallback default starting location instead of failing generation if an invalid one was chosen

* added Nothing and NoEnergy as hint blacklist

added missing NoEnergy as local items and removed it from progression

* added missing completion_condition when TowerCrystals is lower than GanonCrystals

added Rewards and Medallions infos to spoiler

* Update worlds/smz3/__init__.py

Yes, indeed. Thank you!

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
Co-authored-by: Zach Parks <zach@alliware.com>
2022-11-01 14:52:04 -05:00
beauxq
0f7adaaf7b Zillion: LAGPL license 2022-11-01 20:40:57 +01:00
Doug Hoskisson
962e48c078 Zillion: fix unreproducible seeds (#1166)
* fix zillion unreproducible seeds

* world to multiworld merge
2022-11-01 14:45:17 +01:00
black-sliver
95ea0541e6 Core: improve fulfills_accessibility performance 2022-11-01 14:08:24 +01:00
black-sliver
0ed3baabd4 Core: add generic handling of excluded locations
Currently there can be locations that are marked as excluded,
but don't have rules to enforce it, while fill has special handling
for excluded locations already.

This change removes special rules, and adds a generic rule instead.
2022-11-01 14:08:24 +01:00
black-sliver
2db55ac50b Core: make fulfills_accessibility deterministic and fix some typing 2022-11-01 14:08:24 +01:00
black-sliver
bea8d37a3c SoE: fix false positives in early sphere collection (#1165) 2022-11-01 13:14:38 +01:00
Alchav
813015e007 [Pokemon] Fixes and updates (#1108)
* [Pokemon] Logic fixes

* [Pokemon] Fix seed name length

* [Pokemon] Location name changes

* [Pokemon] Hidden Item Nurse Bed logic fix

* Badges Needed description update

* Ensure player name does not exceed 16 bytes

* Player name check fix

* Remove unique items in start_inventory from item pool

* Vending Machine Drinks will not be created as filler

* Skip trainer text

* Badges needed for viridian gym text

* Add slot data for trackers

* free fly map in slot data and old_man = vanilla allows more free fly maps

* Re-add mistakenly removed slot data item

* Add tracker link to setup doc

* Doc fix

* Fix base patch

* Change pre_fill to generate_basic so items are pre-filled before item links

* Rename some hidden locations

* missing file from commit and revert one errant location name change
2022-11-01 07:02:15 +01:00
recklesscoder
c1d7abd06e CommonClient: Some GUI polish (#1139)
* CommonClient: Focus text field when requesting input.

* CommonClient: Store and prefill last server address.

* CommonClient: Focus and select portion of server address upon start.

* CommonClient: Don't allow editing of address while connected.

* CommonClient: Don't make pressing Enter in the address bar disconnect you.

* CommonClient: Use TextInput.text_validate_unfocus over jank workaround.

* CommonClient: Fixed hang when closing after failed handshake.

* CommonClient: Made scrollbar wider and interactable.
2022-11-01 06:54:40 +01:00
strotlog
655f287d42 SM: Fix unobtainable items in remote items+item links combo (#1151)
* SM: fix using item links together with remote items

* SM: write 0 index for excess player ids

* some style and minor fixes (strotlog/Archipelago#1)

* more typing in SM patching

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
2022-11-01 06:42:11 +01:00
Alchav
802119502d Core: Fix early items bug with priority locations (#1167) 2022-10-31 22:26:55 -05:00
alwaysintreble
2af510328e Core: rename world to multiworld (#931)
* rename references to `Multiworld` in core to `multiworld` instead of `world`

* fix smz3

* fix oot

* fix low hanging fruit

* revert mysteriously broken spacing in world api.md

* fix more randomly broken spacing

* hate

* that better be all of it

* begrudgingly move over smw

* ._.

* missed some worlds

* this is getting tedious now

* Missed some self.world definitions

Co-authored-by: espeon65536 <espeon65536@gmail.com>
Co-authored-by: Zach Parks <zach@alliware.com>
2022-10-31 21:41:21 -05:00
black-sliver
87f4a97f1e Core: make player name case-insensitive 2022-10-30 17:12:00 +01:00
black-sliver
1bb99d391d Factorio: deterministic locations 2022-10-30 17:11:34 +01:00
Zach Parks
1cad51b1af Rogue Legacy: World folder clean up and generation improvements. (#1148)
* Minor cleanup and renaming of some files/functions.

* Rename `LegacyWorld` and `LegacyWeb` to RLWorld and RLWeb.

* Undo accidental change to comment.

* Undo accidental change to comment.

* Restructure Items.py format and combine all tables into one.

* Restructure Locations.py format and combine all tables into one.

* Split boss event items into separate boss entries.

* Remove definitions folder.

* Reformatted __init__.py for Rogue Legacy.

* Allow fairy chests to be disabled.

* Add working prefill logic for early vendors.

* Re-introduce Early Architect setting.

* Revamped rules and regions and can now generate games.

* Fix normal vendors breaking everything.

* Fix early vendor logic and add fairy chest logic to require Dragons or Enchantress + runes.

* Fix issue with duplicate items being created.

* Move event placement into __init__.py and fix duplicate Vendors.

* Tweak weights and spacing.

* Update documentation and include bug report link.

* Fix relative link for template file.

* Increase amount of chest locations in `location_table`.

* Correct a refactor rename gone wrong.

* Remove unused reference in imports.

* Tweak mistake in boss name in place_events.

* English is hard.

* Tweak some lines in __init__.py to use `.settings()` method.

* Add unique id tests for Rogue Legacy.

IDs are mixed around, so let's try to avoid accidentally using the same identifier twice.

* Fix typo in doc.

* Simplify `fill_slot_data`.

* Change prefix on `_place_events` to maintain convention.

* Remove items that are **not** progression from rules.
2022-10-29 22:15:06 -05:00
Doug Hoskisson
09d8c4b912 MultiServer: fix case sensitivity in server commands (#1156)
* fix case sensitivity in server commands

* improve ambiguous match breakout

* worried about accidentally swapping team and slot

* Remove now unused import
2022-10-30 00:41:07 +02:00
Br00ty
ed23a426ec DS3: fix shield item/location (#1158)
* fixed item id

fixed item id for "Blessed Red and White Shield"

* fixed location id

fixed location id for "US: Blessed Red and White Shield"
2022-10-29 19:06:56 +02:00
Ludovic Marechal
c711264d1a DS3: Added a few new items and locations (#1059)
* Update items_data.py

added `Red and White Round Shield`, `Crystal Scroll`, `Magic Stoneplate Ring`, and `Outrider Knight` gear.

* Update locations_data.py

Added `US: Red and White Round Shield`, `CKG: Magic Stoneplate Ring`, `GA: Outrider Knight` set, and `GA: Crystal Scroll`

* Update __init__.py

Add `Karla's Ashes` requirements

* Update items_data.py

Add `Irithyll Rapier, Hollow's Ashes, Irina's Ashes, Karla's Ashes, Cornyx's Ashes, and Orbeck's Ashes`

* Update locations_data.py

Add `Irithyll Rapier, Hollow's Ashes, Irina's Ashes, Karla's Ashes, Orbeck's Ashes, and Cornyx's Ashes`

* Update items_data.py

removed "hollows ashes"

* Update locations_data.py

remove "hollows ashes"

* DS3: Increment data_version

* DS3: Fix item name in rule

* DS3: Set required client version to 0.3.6 and added offsets between items and location tables for backward compatibility

* DS3: Resolve Python 3.8 compatibility

* DS3: Removed useless region for locations IDs consistency

* DS3: Changed i in loop

Co-authored-by: Br00ty <83629348+Br00ty@users.noreply.github.com>
2022-10-29 13:35:33 +02:00
black-sliver
3dfbbc5057 Doc: Clarify annotations in style guide (#1149)
* Doc: Clarify annotations in style guide

* Fix typo

* Update docs/style.md

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
2022-10-28 23:02:23 +02:00
Doug Hoskisson
f298b8d6e7 Zillion: validate rescue item links (#1140) 2022-10-28 21:56:50 +02:00
Fabian Dill
53974d568b Factorio: revamped location system (#1147) 2022-10-28 21:00:06 +02:00
black-sliver
ec0389eefb CI: use build_exe in automated build 2022-10-28 20:59:06 +02:00
Ryan
0c54c47023 Docs: Add ArchipelagoRS to the Network Protocol docs (#1153) 2022-10-28 19:24:08 +02:00
CaitSith2
80db8a33af Don't leak info about what exists or not if player can't afford the hint (#1146) 2022-10-28 02:45:18 -07:00
AkumaGath17
e6c6b00109 Minecraft: Two by two logical requirement fix (#1152)
* [Minecraft] Two by two logical requirement fix

* Two by two update

* Two by Two logical fix [Description in order]

* Two by Two fix [Bucket only= False]

Along with the others isolated items checks
2022-10-28 10:34:46 +02:00
Alchav
813ea6ef8b [SM64] Separate Entrance Shuffle pools option and MIPS cost option improvement (#1137)
* Add separate pool option for entrance shuffle and swap MIPS costs if MIPS1Cost is greater

* Changes based on N00by's suggestions

* split into secret_entrance_ids and course_entrance_ids
2022-10-28 01:43:02 +02:00
Doug Hoskisson
c09e089f9d Docs: Zillion: RetroArch core and early_items recommendation. (#1150)
* add Genesis Plus GX to Zillion docs

* zillion early items recommendation
2022-10-28 01:35:18 +02:00
recklesscoder
cfff12d8d7 Factorio: Added ability to chat from within the game. (#1068)
* Factorio: Added ability to chat from within the game.
This also allows using commands such as !hint from within the game.

* Factorio: Only prepend player names to chat in multiplayer.

* Factorio: Mirror chat sent from the FactorioClient UI to the Factorio server.

* Factorio: Remove local coordinates from outgoing chat.

* Factorio: Added setting to disable bridging chat out.
Added client command to toggle this setting at run-time.

* Factorio: Added in-game command to toggle chat bridging setting at run-time.

* .

* Factorio: Document toggle for chat bridging feature.

* (Removed superfluous type annotations.)

* (Removed hard to read regex.)

* Docs/Factorio: Fix display of multiline code snippets.
2022-10-28 00:45:26 +02:00
recklesscoder
924f484be0 Factorio: Add optional filtering for item sends displayed in-game (#1142)
* Factorio: Added feature to filter item sends displayed in-game.

* Factorio: Document item send filter feature.

* Factorio: Fix item send filter for item links.

* (Removed superfluous type annotations.)

* CommonClient: Added is_uninteresting_item_send helper.
2022-10-28 00:07:57 +02:00
Doug Hoskisson
aeb78eaa10 Zillion: map tracker in client (#1136)
* Option RangeWithSpecialMax

* amendment to typing in web options

* compare string with number

* lots of work on zillion

* fix zillion fill logic

* fix a few more issues in zillion fill logic

* can make zillion patch and use it

* put multi items in zillion rom

* work on ZillionClient

* logging and auth in client

* work on sending and receiving items

* implement item_handling flag

* fix locations ids to NuktiServer package

* use rewrite of zri

* cache logic rule data for performance

* use new id maps

* fix some problems with the big recent merge

* ZillionClient: use new context manager for Memory class

* fix ItemClassification for Zillion items
and some debug statements for asserts,
documentation on running scripts for manual testing
type correction in CommonContext

* fix some issues in client, start on docs, put rescue and item ram addresses in slot data

* use new location name system
fix item locations getting out of sync in progression balancing

* zillion client can read slot name from game

* zillion: new item names

* remove extra unneeded import

* newer options (room gen and starting cards)

* update comment in zillion patch

* zillion non static regions

* change some logging, update some comments

* allow ZillionClient to exit in certain situations

* todo note to fix options doc strings

* don't force auto forfeit

* rework validation of floppy requirement and item counts
and fix race condition in generate_output

* reorganize Zillion component structure
with System class

* documentation updates for Zillion

* attempt inno_setup.iss

* remove todo comment for something done

* update comment

* rework item count zillion options
and some small cleanups

* fix location check count

* data package version 1

* Zillion can pass unit tests without rom

* fix freeze if closing ZillionClient while it's waiting for server login

* specify commit hash for zilliandomizer package

* some changes to options validation

* Zillion doors saved on multiworld server

* add missing function in inno_setup
and name of vanilla continues in options

* rework zillion sync task and context

* Apply documentation suggestions from SoldierofOrder

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>

* update zillion package

* workaround for asyncio udp bug

There is a bug in Python in Windows
https://github.com/python/cpython/issues/91227
that makes it so if I look for RetroArch before it's ready, it breaks the asyncio udp transport system.

As a workaround, we don't look for RetroArch until the user asks for it with /sms

* a few of the smaller suggestions from review

* logic only looks at my locations
instead of all the multiworld locations

* some adjustments from pull request discussion
and some unit tests

* patch webhost changes from pull request discussion

* zillion logic tests

* better vblr test

* test interaction of character rescue items with logic

* move unit tests to new worlds folder

* comment improvements

* fix minor logic issue
and add memory read timeout

* capitalization in option display names
Opa-Opa is a proper noun

* client toggle side panel with /map

* displays map

* fix map transparency

* fix broken launcher

* better way to specify grid container

* start kivy typing

* have a map that updates with item checks

but it breaks other parts of the UI

* fix layout bug

* aspect ratio of image
and some type checking details

* Fix loading of map for compiled builds

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>
Co-authored-by: Doug Hoskisson <doughoskisson@novuslabs.com>
Co-authored-by: CaitSith2 <d_good@caitsith2.com>
2022-10-27 02:30:22 -07:00
toasterparty
6134578c60 Overcooked! 2: slightly relax 3-star logic (#1144) 2022-10-27 11:19:48 +02:00
Fabian Dill
b57ca33c31 Logging: more digits for IDs and counts (#1141)
* Logging: we now need 9 digits for IDs

* Logging: we now need {dynamic} digits for IDs

* Logging: we now need {dynamic} digits for counts
2022-10-27 09:18:25 +02:00
Alchav
4b18920819 Early Items option (#1086)
* Early Items option

* Early Items description update

* Change Early Items to dict

* Rewrite early items as extra fill steps

* Move if statement up

* Document early_items

* Move early_items handling before fill_hook

* Apply suggestions from code review

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>

* Subnautica pre-fill moved to early_items

* Subnautica early items fix

* Remove unit test bug workaround

* beauxq's pr

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
2022-10-27 09:00:24 +02:00
Magnemania
700fe8b75e SC2: New Settings, Logic improvements (#1110)
* Switched mission item group to a list comprehension to fix missile shuffle errors

* Logic for reducing mission and item counts

* SC2: Piercing the Shroud/Maw of the Void requirements now DRY

* SC2: Logic for All-In, may need further refinement

* SC2: Additional mission orders and starting locations

* SC2: New Mission Order options for shorter campaigns and smaller item pools

* Using location table for hardcoded starter unit

* SC2: Options to curate random item pool and control early unit placement

* SC2: Proper All-In logic

* SC2: Grid, Mini Grid and Blitz mission orders

* SC2: Required Tactics and Unit Upgrade options, better connected item handling

* SC2: Client compatibility with Grid settings

* SC2: Mission rando now uses world random

* SC2: Alternate final missions, new logic, fixes

* SC2: Handling alternate final missions, identifying final mission on client

* SC2: Minor changes to handle edge-case generation failures

* SC2: Removed invalid type hints for Python 3.8

* Revert "SC2: Removed invalid type hints for Python 3.8"

This reverts commit 7851b9f7a3.

* SC2: Removed invalid type hints for Python 3.8

* SC2: Removed invalid type hints for Python 3.8

* SC2: Removed invalid type hints for Python 3.8

* SC2: Removed invalid type hints for Python 3.8

* SC2: Changed location loop to enumerate

* SC2: Passing category names through slot data

* SC2: Cleaned up unnecessary _create_items method

* SC2: Removed vestigial extra_locations field from MissionInfo

* SC2: Client backwards compatibility

* SC2: Fixed item generation issue where item is present in both locked and unlocked inventories

* SC2: Removed Missile Turret from defense rating on maps without air

* SC2: No logic locations point to same access rule

Co-authored-by: michaelasantiago <michael.alec.santiago@gmail.com>
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2022-10-26 12:24:54 +02:00
PoryGone
d5efc71344 Core: SNI Client Refactor (#1083)
* First Pass removal of game-specific code

* SMW, DKC3, and SM hooked into AutoClient

* All SNES autoclients functional

* Fix ALttP Deathlink

* Don't default to being ALttP, and properly error check ctx.game

* Adjust variable naming

* In response to:
> we should probably document usage somewhere. I'm open to suggestions of where this should be documented.

I think the most valuable documentation for APIs is docstrings and full typing.

about websockets change in imports - from websockets documentation:
> For convenience, many public APIs can be imported from the websockets package. However, this feature is incompatible with static code analysis. It breaks autocompletion in an IDE or type checking with mypy. If you’re using such tools, use the real import paths.

* todo note for python 3.11
typing.NotRequired

* missed staging in previous commit

* added missing death Game States for DeathLink

Co-authored-by: beauxq <beauxq@users.noreply.github.com>
Co-authored-by: lordlou <87331798+lordlou@users.noreply.github.com>
2022-10-25 19:54:43 +02:00
Fabian Dill
6535836e5c Subnautica: don't override plando during pre_fill 2022-10-25 01:35:01 +02:00
lordlou
89d1a80e01 SM: morph first in pool remove (#1134)
* first working (most of the time) progression generation for SM using VariaRandomizer's rules, items, locations and accessPoint (as regions)

* first working single-world randomized SM rom patches

* - SM now displays message when getting an item outside for someone else (fills ROM item table)

This is dependant on modifications done to sm_randomizer_rom project

* First working MultiWorld SM

* some missing things:

- player name inject in ROM and get in client
- end game get from ROM in client
- send self item to server
- add player names table in ROM

* replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better)

* - reenabled balancing

* post rebase fixes

* updated SmClient.py

* + added VariaRandomizer LICENSE

* + added sm_randomizer_rom project (which builds sm.ips)

* Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning

* properly revert change made to CollectionState and more cleaning

* Fixed multiworld support patch not working with VariaRandomizer's

* missing file commit

* Fixed syntax error in unused code to satisfy Linter

* Revert "Fixed multiworld support patch not working with VariaRandomizer's"

This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b.

* many fixes and improovement

- fixed seeded generation
- fixed broken logic when more than one SM world
- added missing rules for inter-area transitions
- added basic patch presence for logic
- added DoorManager init call to reflect present patches for logic
- moved CollectionState addition out of BaseClasses into SM world
- added condition to apply progitempool presorting only if SM world is present
- set Bosses item id to None to prevent them going into multidata
- now use get_game_players

* first working (most of the time) progression generation for SM using VariaRandomizer's rules, items, locations and accessPoint (as regions)

* first working single-world randomized SM rom patches

* - SM now displays message when getting an item outside for someone else (fills ROM item table)

This is dependant on modifications done to sm_randomizer_rom project

* First working MultiWorld SM

* some missing things:

- player name inject in ROM and get in client
- end game get from ROM in client
- send self item to server
- add player names table in ROM

* replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better)

* - reenabled balancing

* post rebase fixes

* updated SmClient.py

* + added VariaRandomizer LICENSE

* + added sm_randomizer_rom project (which builds sm.ips)

* Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning

* properly revert change made to CollectionState and more cleaning

* Fixed multiworld support patch not working with VariaRandomizer's

* missing file commit

* Fixed syntax error in unused code to satisfy Linter

* Revert "Fixed multiworld support patch not working with VariaRandomizer's"

This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b.

* many fixes and improovement

- fixed seeded generation
- fixed broken logic when more than one SM world
- added missing rules for inter-area transitions
- added basic patch presence for logic
- added DoorManager init call to reflect present patches for logic
- moved CollectionState addition out of BaseClasses into SM world
- added condition to apply progitempool presorting only if SM world is present
- set Bosses item id to None to prevent them going into multidata
- now use get_game_players

* Fixed multiworld support patch not working with VariaRandomizer's

Added stage_fill_hook to set morph first in progitempool
Added back VariaRandomizer's standard patches

* + added missing files from variaRandomizer project

* + added missing variaRandomizer files (custom sprites)

+ started integrating VariaRandomizer options (WIP)

* Some fixes for player and server name display

- fixed player name of 16 characters reading too far in SM client
- fixed 12 bytes SM player name limit (now 16)
- fixed server name not being displayed in SM when using server cheat ( now displays RECEIVED FROM ARCHIPELAGO)
- request: temporarly changed default seed names displayed in SM main menu to OWTCH

* Fixed Goal completion not triggering in smClient

* integrated VariaRandomizer's options into AP (WIP)

- startAP is working
- door rando is working
- skillset is working

* - fixed itemsounds.ips crash by always including nofanfare.ips into multiworld.ips (itemsounds is now always applied and "itemsounds" preset must always be "off")

* skillset are now instanced per player instead of being a singleton class

* RomPatches are now instanced per player instead of being a singleton class

* DoorManager is now instanced per player instead of being a singleton class

* - fixed the last bugs that prevented generation of >1 SM world

* fixed crash when no skillset preset is specified in randoPreset (default to "casual")

* maxDifficulty support and itemsounds removal

- added support for maxDifficulty
- removed itemsounds patch as its always applied from multiworld patch for now

* Fixed bad merge

* Post merge adaptation

* fixed player name length fix that got lost with the merge

* fixed generation with other game type than SM

* added default randoPreset json for SM in playerSettings.yaml

* fixed broken SM client following merge

* beautified json skillset presets

* Fixed ArchipelagoSmClient not building

* Fixed conflict between mutliworld patch and beam_doors_plms patch

- doorsColorsRando now working

* SM generation now outputs APBP

- Fixed paths for patches and presets when frozen

* added missing file and fixed multithreading issue

* temporarily set data_version = 0

* more work

- added support for AP starting items
- fixed client crash with gamemode being None
- patch.py "compatible_version" is now 3

* commited missing asm files

fixed start item reserve breaking game (was using bad write offset when patching)

* Nothing item are now handled game-side. the game will now skip displaying a message box for received Nothing item (but the client will still receive it).

fixed crash in SMClient when loosing connection to SNI

* fixed No Energy Item missing its ID

fixed Plando

* merge post fixes

* fixed start item Grapple, XRay and Reserve HUD, as well as graphic beams (except ice palette color)

* fixed freeze in blue brinstar caused by Varia's custom PLM not being filled with proper Multiworld PLM address (altLocsAddresses)

* fixed start item x-ray HUD display

* Fixed start items being sent by the server (is all handled in ROM)

Start items are now not removed from itempool anymore
Nothing Item is now local_items so no player will ever pickup Nothing. Doing so reduces contribution of this world to the Multiworld the more Nothing there is though.
Fixed crash (and possibly passing but broken) at generation where the static list of IPSPatches used by all SM worlds was being modified

* fixed settings that could be applied to any SM players

* fixed auth to server only using player name (now does as ALTTP to authenticate)

* - fixed End Credits broken text

* added non SM item name display

* added all supported SM options in playerSettings.yaml

* fixed locations needing a list of parent regions (now generate a region for each location with one-way exits to each (previously) parent region

did some cleaning (mainly reverts on unnecessary core classes

* minor setting fixes and tweaks

- merged Area and lightArea settings
- made missileQty, superQty and powerBombQty use value from 10 to 90 and divide value by float(10) when generating
- fixed inverted layoutPatch setting

* added option start_inventory_removes_from_pool

fixed option names formatting
fixed lint errors
small code and repo cleanup

* Hopefully fixed ROR2 that could not send any items

* - fixed missing required change to ROR2

* fixed 0 hp when respawning without having ever saved (start items were not updating the save checksum)

* fixed typo with doors_colors_rando

* fixed checksum

* added custom sprites for off-world items (progression or not)

the original AP sprite was made with PierRoulette's SM Item Sprite Utility by ijwu

* - added missing change following upstream merge

- changed patch filename extension from apbp to apm3 so patch can be used with the new client

* added morph placement options: early means local and sphere 1

* fixed failing unit tests

* - fixed broken custom_preset options

* - big cleanup to remove unnecessary or unsupported features

* - more cleanup

* - moved sm_randomizer_rom and all always applied patches into an external project that outputs basepatch.ips

- small cleanup

* - added comment to refer to project for generating basepatch.ips (https://github.com/lordlou/SMBasepatch)

* fixed g4_skip patch that can be not applied if hud is enabled

* - fixed off world sprite that can have broken graphics (restricted to use only first 2 palette)

* - updated basepatch to reflect g4_skip removal

- moved more asm files to SMBasepatch project

* - tourian grey doors at baby metroid are now always flashing (allowing to go back if needed)

* fixed wrong path if using built as exe

* - cleaned exposed maxDifficulty options

- removed always enabled Knows

* Merged LttPClient and SMClient into SNIClient

* added varia_custom Preset Option that fetch a preset (read from a new varia_custom_preset Option) from varia's web service

* small doc precision

* - added death_link support

- fixed broken Goal Completion
- post merge fix

* - removed now useless presets

* - fixed bad internal mapping with maxDiff

- increases maxDiff if only Bosses is preventing beating the game

* - added support for lowercase custom preset sections (knows, settings and controller)

- fixed controller settings not applying to ROM

* - fixed death loop when dying with Door rando, bomb or speed booster as starting items

- varia's backup save should now be usable (automatically enabled when doing door rando)

* -added docstring for generated yaml

* fixed bad merge

* fixed broken infinity max difficulty

* commented debug prints

* adjusted credits to mark progression speed and difficulty as Non Available

* added support for more than 255 players (will print Archipelago for higher player number)

* fixed missing cleanup

* added support for 65535 different player names in ROM

* fixed generations failing when only bosses are unreachable

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

* fixed failling generations when using 'fun' settings

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

* fixed debug logger

* removed unsupported "suits_restriction" option

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

* - fixed deathlink emptying reserves

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

* - merged death_link and death_link_survive options

* fixed death_link

* added a fallback default starting location instead of failing generation if an invalid one was chosen

* added Nothing and NoEnergy as hint blacklist

added missing NoEnergy as local items and removed it from progression

* removed now unecessary sorting of Morph balls at end of item pool

Its messing with priority locations feature.
2022-10-24 10:28:08 +02:00
beauxq
ad445629bd Zillion: fix unit tests
previous fix was incorrect
2022-10-24 01:28:06 +02:00
Doug Hoskisson
37c5865c0e Core: Options: fix shared default instances (#1130) 2022-10-23 18:28:09 +02:00
Doug Hoskisson
52726139b4 Zillion: support unicode player names (#1131)
* work on unicode and seed verification

* update zilliandomizer

* fix log message
2022-10-23 18:18:05 +02:00
Doug Hoskisson
24105ac249 Tests: fix random failures on Zillion tests (#1128)
* tests: fix random failures on Zillion tests

Normally there's a low probably that the game doesn't require a power-up that it usually requires.
This makes sure it always has that requirement for tests.

* better type narrowing
2022-10-22 03:42:16 +02:00
Alchav
f18df4c1df [Core] Fix priority location handling in accessibility corrections (#1121)
* Fix priority location handling in accessibility corrections

* Don't lock empty locations

* black sliver's suggested change

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-10-22 03:29:20 +02:00
black-sliver
04b6c31076 SoE: update to v042 and balancing changes (#1125)
* SoE: rebalancing and cleanup

* ModuleUpdate: make url detection more generic

* SoE: change item rules to depend on target player difficulty

* SoE: Update to pyevermizer 0.41.0

* adds footknight
* adds location difficulty

* SoE: minor optimization in item rule

if .. in is faster with sets

* SoE: drop support of patch format v3

* SoE: fix some typing and warnings

* SoE: cleanup imports
2022-10-21 23:26:40 +02:00
Fabian Dill
40c3ef35c7 LttP: fix Inverted Big Bomb Shop indirect connection rule 2022-10-21 23:25:52 +02:00
Fabian Dill
28483a6c14 Generate: don't try to include meta or filler weights file as player 2022-10-21 23:25:26 +02:00
ScootyPuffJr1
fa077defe0 [Factorio] Minor fix for typo on setup doc 2022-10-21 08:58:12 +02:00
Chris Wilson
47b4e2782b WebHost: Fix weighted-settings to not save full set of range options to localStorage (#1100) 2022-10-20 20:10:38 -05:00
Doug Hoskisson
265ee7098a New Game: Zillion (#1081)
* Option RangeWithSpecialMax

* amendment to typing in web options

* compare string with number

* lots of work on zillion

* fix zillion fill logic

* fix a few more issues in zillion fill logic

* can make zillion patch and use it

* put multi items in zillion rom

* work on ZillionClient

* logging and auth in client

* work on sending and receiving items

* implement item_handling flag

* fix locations ids to NuktiServer package

* use rewrite of zri

* cache logic rule data for performance

* use new id maps

* fix some problems with the big recent merge

* ZillionClient: use new context manager for Memory class

* fix ItemClassification for Zillion items
and some debug statements for asserts,
documentation on running scripts for manual testing
type correction in CommonContext

* fix some issues in client, start on docs, put rescue and item ram addresses in slot data

* use new location name system
fix item locations getting out of sync in progression balancing

* zillion client can read slot name from game

* zillion: new item names

* remove extra unneeded import

* newer options (room gen and starting cards)

* update comment in zillion patch

* zillion non static regions

* change some logging, update some comments

* allow ZillionClient to exit in certain situations

* todo note to fix options doc strings

* don't force auto forfeit

* rework validation of floppy requirement and item counts
and fix race condition in generate_output

* reorganize Zillion component structure
with System class

* documentation updates for Zillion

* attempt inno_setup.iss

* remove todo comment for something done

* update comment

* rework item count zillion options
and some small cleanups

* fix location check count

* data package version 1

* Zillion can pass unit tests without rom

* fix freeze if closing ZillionClient while it's waiting for server login

* specify commit hash for zilliandomizer package

* some changes to options validation

* Zillion doors saved on multiworld server

* add missing function in inno_setup
and name of vanilla continues in options

* rework zillion sync task and context

* Apply documentation suggestions from SoldierofOrder

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>

* update zillion package

* workaround for asyncio udp bug

There is a bug in Python in Windows
https://github.com/python/cpython/issues/91227
that makes it so if I look for RetroArch before it's ready, it breaks the asyncio udp transport system.

As a workaround, we don't look for RetroArch until the user asks for it with /sms

* a few of the smaller suggestions from review

* logic only looks at my locations
instead of all the multiworld locations

* some adjustments from pull request discussion
and some unit tests

* patch webhost changes from pull request discussion

* zillion logic tests

* better vblr test

* test interaction of character rescue items with logic

* move unit tests to new worlds folder

* comment improvements

* fix minor logic issue
and add memory read timeout

* capitalization in option display names
Opa-Opa is a proper noun

* redirect zz stdout to debug

* fix option validation bug making unbeatable seeds

* remove line that does nothing

* attach logic cache to world

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>
Co-authored-by: Doug Hoskisson <doughoskisson@novuslabs.com>
2022-10-20 19:41:11 +02:00
black-sliver
ed76c13961 Core: assert that items have a single reference (#1075)
* Core: assert that items have a single reference

* Fix duplicate item reference in The Witness

* Ori: fix duplicate item references

* DKC3: fix duplicate item references

* RL: fix duplicate item references

* SA2B: fix duplicate item references

* SMW: fix duplicate item references

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2022-10-20 10:42:33 +02:00
NewSoupVi
40b7e78178 The Witness: More Puzzle Skips, Softlock Fix (#1117)
* Softlock fix: 'Swamp Maze Controls' item also locks maze control other side now

* Increase default & max amount of Puzzle Skips (in response to client side changes to how they work)

* Logically require shortbox lasers to start the elevator
2022-10-20 05:05:36 +02:00
Sunny Bat
1900d9382a Raft: Update rules to account for navigation (#1118) 2022-10-19 08:47:33 +02:00
Doug Hoskisson
f12b73f487 Tests: world test base class (#1116)
* world test base class

* game not instance property

* I would have guessed that this only collected 1.

* game property

* move SoE tests into worlds

* don't force auto world setup
2022-10-18 18:54:41 +02:00
black-sliver
49ae79e5ce Tests: add fill tests for #1109 and #1114 (#1115)
* Test: for fill issues fixed in PR #1109

* Test: for double sweep collect fixed in PR #1114
2022-10-18 10:20:57 +02:00
Fabian Dill
4da6a0bb98 Fill: fix duplicate event pickups 2022-10-18 08:59:51 +02:00
Fabian Dill
af0cfc5a38 Fill: Priority locks when placing and does not swap. (#1099)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-10-18 01:07:06 +02:00
Fabian Dill
1aa3e431c8 LttP: fix ganons tower trash fill deleting items that did not fit (#1113) 2022-10-17 09:52:34 +02:00
Fabian Dill
b533ffb9e8 Locality: rewrite for linear memory consumption, from quadratic (#1091) 2022-10-17 03:22:02 +02:00
Fabian Dill
bb46ee7fc1 WebHost: optimize imports 2022-10-17 01:24:02 +02:00
black-sliver
acf7fda26a Main: Fill: more opportunities for swapping 2022-10-17 00:45:51 +02:00
Alchav
f7fc6fa7aa Core: Fix forbid_important_item_rule with parenthesis (#1107) 2022-10-16 07:32:17 +02:00
Alchav
5e97463bdc [Pokemon R/B] Fix inno_setup mistake (#1105) 2022-10-16 02:53:07 +02:00
Doug Hoskisson
ca9c3d05d6 Docs: information on Retrieved packet (#1101) 2022-10-15 13:44:39 +02:00
espeon65536
51f65f4b9e OoT: ER algorithm improvements (#1103)
* OoT: ER improvements
Include dungeon rewards in itempool to allow for ER improvement
Better validate_world function by checking for multi-entrance incompatibility more efficiently
Fix some generation failures by ensuring all entrances placed with logic
Introduce bias to some interior entrance placement to improve generation rate

* OoT: fix overworld ER spoiler information

* OoT: rewrite dungeon item placement algorithm
in particular, no longer assumes that exactly the number of vanilla keys is present, which lets it place more or fewer items.
2022-10-15 12:39:04 +02:00
recklesscoder
1f01404ca4 WebHost: Fixed scrolling to anchors (#1085)
* WebHost: Modernized anchor code

* WebHost: Fixed scrolling to anchors

* WebHost: Fixed scrolling to anchors when fonts are being loaded

* WebHost: Anchor PR changes requested by LegendaryLinux
2022-10-14 17:09:17 -04:00
Fabian Dill
bbb6ee89cf Hylics 2: add to readme (#1094) 2022-10-14 22:53:49 +02:00
Fabian Dill
0aea1e780f Fill: create minimal excluded location rule only once 2022-10-14 22:53:16 +02:00
Fabian Dill
722b3c5369 Core: make add_rule set if it finds an empty rule (#1093)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-10-14 22:52:45 +02:00
black-sliver
097ac189e4 SoE: add tests ... (#1097)
* SoE: add tests ...

... for goals, bronze axe and bronze spear+

* SoE: fix tests
2022-10-14 19:35:53 +02:00
toasterparty
7f3f886e41 Overcooked! 2: Implementation (#1046)
Overcooked! 2 is a couch co-op arcade game with a very high skill ceiling. It has a small but occult following, and the community craves a reason to keep coming back besides just grinding high scores. as such, this PR represents 3 major milestones in one:

 * The launch of OC2 Modding, a modding framework which is the first public mod for the game beyond simple RAM trainers
 * The launch of OC2 Randomizer
 * The integration of OC2 Randomizer in Archipelago
2022-10-13 19:57:50 +02:00
Alchav
3bd4ef3f3d [Pokémon R/B] Fixes (#1096)
* Prevent legendaries from being shuffled into restless soul encounter

* Prevent Poke Tower 6F wild mons from being same as restless soul

* fix non-deterministic generation
2022-10-13 19:55:21 +02:00
Yussur Mustafa Oraji
6b9073acd7 sm64ex: Update min client version 2022-10-13 11:36:04 +02:00
Jarno
e708bea819 [Sudoku] Added new BK mode game (#910)
Co-authored-by: Hussein Farran <hmfarran@gmail.com>
Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>
2022-10-13 07:55:00 +02:00
Trevor L
b014ce082b Hylics 2: Implement new game (#1058) 2022-10-13 07:51:25 +02:00
Alchav
30a4bcbbbe [Pokemon Red and Blue] Initial implementation (#1016) 2022-10-13 07:45:52 +02:00
Alchav
0afb7096de Core: improve sweep_for_events efficiency (#1092) 2022-10-13 05:46:07 +02:00
alwaysintreble
f909576813 core: Generic boss plando handler (#1044)
* fix some blunders i made when implementing this

* move generic functions to core class

* move lttp specific stuff out and split up from_text a bit for more modularity

* slightly optimize from_text call order

* don't make changes on github apparently. reading hard

* Metaclass Magic

* do a check against the base class

* copy paste strikes again

* use option default instead of hardcoded "none". check locations and bosses aren't reused.

* throw dupe location error for lttp

* generic singularity support with a bool

* forgot to enable it for lttp

* better error handling

* PlandoBosses: fix inheritance of singularity

* Tests: PlandoBosses

* fix case insensitive tests

* Tests: cleanup PlandoBosses tests

* f in the chat

* oop

* split location into a different variable

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* pass the list of options as `option_list`

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-10-12 20:28:32 +02:00
lordlou
9f684b3dc0 SMZ3: Shield Upgrade typo fix (#1088)
fixed "Shield Uprade" typo

See lordlou/alttp_sm_combo_randomizer_rom@3da5313
2022-10-11 08:38:00 +02:00
recklesscoder
37a40499fa Docs/sm64ex: address common questions
- Added bolded note that one must use a new file with each new seed
- Added troubleshooting section for when one didn't use a new file
- Worded compilation step 8 more explicitly
- Added link to compilation options list
- Added link to TextClient for using commands
2022-10-10 09:14:13 +02:00
recklesscoder
099c4fca3c Docs: Polished Trigger and Plando guides (#1080)
* Docs: Polished Trigger and Plando guides

* Docs: Trigger/Plando guide polish PR suggestions by SoldierofOrder

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>

* Docs: More Trigger/Plando guide polish

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>
2022-10-10 09:10:01 +02:00
Joethepic
106d630ad7 HK: changes dreamers to skip prog balance (#1077) 2022-10-10 02:26:33 +02:00
alwaysintreble
4c0c93b083 core: allow string defaults in yaml templates (#1051)
* allow string defaults in yaml templates

* have default_converter handle strings

* handle all default values in the yaml

* allow for random range options

* yaml dump dicts

* strip the whities

* rip out the converter

* accidentally stripped the dicts

* goodbye readability
2022-10-10 00:16:59 +02:00
Doug Hoskisson
3cbbf905d1 Docs: how to run web host and generate template yamls (#1071) 2022-10-09 04:20:01 +02:00
SoldierofOrder
414ebf2640 SC2: Add an automated installation process for the maps and mod within SC2Client. (#928) 2022-10-09 04:19:17 +02:00
NewSoupVi
3297be7902 The Witness: Expert & Hints (#1072) 2022-10-09 04:13:52 +02:00
recklesscoder
7b3ef012b9 Factorio: Prevent pipes from breaking on invalid UTF-8 in client (#1078) 2022-10-09 04:10:22 +02:00
black-sliver
af6a72c3c3 AppImage: provide LD_LIBRARY_PATH
this fixes libssl1.1 not being found
2022-10-07 22:24:14 +02:00
recklesscoder
38b7bdfe60 WebHost: Fixed some document titles (#1063) 2022-10-06 18:01:07 -04:00
Gertimoshka
4c266e6eff hostRoom.css Changes (#957)
* hostRoom.css Changes

Makes the console be a scrollable object, for easier use with commands

* Update hostRoom.css

* Requested Change

Requested Change
2022-10-06 17:53:20 -04:00
Fabian Dill
8a6c9ff4b8 WebHost: clear yaml template folder before populating it 2022-10-03 08:48:53 +02:00
Doug Hoskisson
fdd7ffb089 Core: move output file name logic into core (#1066)
* move output file name logic into core

I see the same logic with small variations in each different world implementation.
It seems to me, it would be better in the core to keep it consistent.

* missed a few

* remove review comment

* + smw

* double quote strings

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* revert change to DS3 output file name

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-10-02 16:53:18 +02:00
black-sliver
b8e467fbb8 ModuleUpdate: skip disabled/hidden folders (#1070)
* ModuleUpdate: skip non-worlds

* ModuleUpdate: don't skip _* folders

- _* folders may be used for libraries
- this means to properly disable a world, it has to be renamed with a preceding `.`
2022-10-01 17:38:39 +02:00
Fabian Dill
411cd51a92 SC2: dynamically create Beat <mission_name> Events, preventing copy-paste errors (#1023) 2022-10-01 16:22:24 +02:00
Fabian Dill
e9e15e854d SC2: make apworld compatible (#1024)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-10-01 15:24:05 +02:00
black-sliver
4943d26160 APWorld: make it behave more like a regular world
- set sys.modules so it can be imported with worlds.*
- overwrite __package__ so it can reference ..generic
- fix deprecation warning
2022-10-01 11:51:42 +02:00
Fabian Dill
060a04700d Core: allow generic access to indirect_connections (#1056) 2022-09-30 04:58:19 +02:00
Fabian Dill
61e39f355d Core remove legacy patch (#1047)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-09-30 00:36:30 +02:00
Yussur Mustafa Oraji
8ab0b410c3 sm64ex: Document new connection status notifications 2022-09-29 23:56:46 +02:00
Fabian Dill
d897aaade2 Docs: Ensure Discord links are permanent. (#1064) 2022-09-29 23:15:12 +02:00
black-sliver
0191df88d7 Doc: network protocol: clarify want_reply 2022-09-29 21:15:34 +02:00
recklesscoder
bee1fd9b5a Subnautica: Updated Setup Guide (#1062)
- Added sections for console commands and known issues.
- Updated "Resuming" section to reflect current functionality.
- Removed implication that one might have to create the QMods folder. (If it's missing, then you've already messed up step 1.)
- Renamed "Connect Menu" to "connect form" to be less confusing. Generally fixed word capitalization to conform to standard English. Minor wording changes.
2022-09-29 21:04:04 +02:00
Jarno
dd7d3a02a4 WebHost: Fixed Oculus Ring from showing up on tracker (#1065) 2022-09-29 20:18:21 +02:00
PoryGone
13edfa60be Super Mario World: Implement New Game (#1045) 2022-09-29 20:16:59 +02:00
Alchav
885c8d3fcc Fix minimal accessibility failures (#726) 2022-09-29 19:59:57 +02:00
black-sliver
e6a4925f0c Doc: update apclientpp to header-only (#1054) 2022-09-29 00:09:04 +02:00
Doug Hoskisson
c96b6d7b95 Core: some typing and docs in various parts of the interface (#1060)
* some typing and docs in various parts of the interface

* fix whitespace in docstring

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* suggested changes from discussion

* remove redundant import

* adjust type for json messages

* for options module detection:
 module.lower().endswith("options")

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-09-28 23:54:10 +02:00
alwaysintreble
8bc8b412a3 Core: fix unweighted options for meta files (#1053) 2022-09-28 23:02:42 +02:00
PoryGone
b4b9ff5d82 Docs: Update snes9x Links (#1048) 2022-09-27 13:26:33 +02:00
black-sliver
b21b5cceb8 Doc, SoE: Logic mixin: no underscore for public members (#1049)
* Doc: logic mixin, drop underscore, clarify

conventionally, we added a leading underscore to logic mixins' function
names. This is noisy in the warning section of IDEs. Leading underscores
should only be used for private/protected functions.

In addition, the use of self.world and/or requirement to (no) pass in stuff
was not made clear earlier.

* SoE: fix _ warnings for logic mixin
2022-09-25 18:00:22 +02:00
CaitSith2
813ee5ee3b Factorio: Add explicit support for factory-levels mod. (#1050)
* Factorio: Add explicit support for factory-levels mod.

* Fix inconsistent space/tabs
2022-09-24 02:43:00 -07:00
Fabian Dill
be1158ad78 Windows: update VC Redistributable to 14.32.31332 from 14.29.30037 2022-09-22 08:46:48 +02:00
black-sliver
6d5ddf3cad MultiServer: allow using IDs for hints 2022-09-20 18:38:31 +02:00
black-sliver
809bda02d1 Test: item/location name must not be numeric 2022-09-20 18:38:16 +02:00
black-sliver
2d5ec6ce22 Doc: item/location name must not be numeric 2022-09-20 18:38:16 +02:00
black-sliver
a95d0ce9ef Doc: clarify requirements.txt in world api.md 2022-09-20 09:48:30 +02:00
alwaysintreble
267d9234e5 core: fix options with "random" as default value not generating (#1033)
* core: fix options with "random" as default value not generating

when option is missing from the player yaml,

Using this in #893 and tested there.

* remove if

* OptionSets default to frozenset so handle that

* range had some specific instances of assuming default as a valid value so change this here to call the from_any

* isinstance instead of type

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-09-19 22:40:15 +02:00
black-sliver
4686881566 WebHost: CustomServer: use defaultdicts
also change non_hintable to defaultdict in MultiServer and add some typing
2022-09-19 01:20:36 +02:00
SoldierofOrder
101dab0ea4 SC2: Add helpful feedback when failing to locate SC2 (#1032)
* SC2: The client now throws a descriptive error when ExecuteInfo.txt exists but is empty, and offers more helpful suggestions when the file doesn't exist.

* SC2: Replaced the new RuntimeError with a warning in the logger to keep things consistent.

* Removed communism

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-09-18 16:48:36 +02:00
Fabian Dill
c2d69cb05e Core: add generic interface to add ER data to hints (#1014) 2022-09-18 14:30:43 +02:00
black-sliver
58f66e0f42 autoworld: don't load files/folders starting with '.' (#1030)
* autoworld: don't load files/folders starting with '.'

The imports fail if the folder has a '.' in the name, with a somewhat obscure error, and adding a '.' in front of it is what a linux user might expect to use when disabling a world temporarily.

* autoworld: use tuple to filter .* and _*
2022-09-18 13:02:05 +02:00
Fabian Dill
0215e1fa28 SC2: always show uncollected locations (#1007) 2022-09-18 12:40:35 +02:00
black-sliver
1c0a93acad doc: update use of relative/absolute imports
it matters for apworlds to function
2022-09-18 10:22:17 +02:00
NewSoupVi
4fcde135e5 The Witness: Renaming, Options, Logic Fixes (#1000)
Fixes to postgame detection for "shuffle_postgame"
Renamed many locations to be symbol-independent ("Outside Tutorial Dots Introduction" becomes "Outside Tutorial Shed Row"). This is to set up future alternate modes, like Sigma Expert, which use completely different symbols.
Renamed most door items to be shorter, more consistent, and less... stupid. ("Bunker Bunker Entry Door" -> "Bunker Entry")
Removed "shuffle_uncommon"
Many logic fixes
2022-09-18 04:20:59 +02:00
alwaysintreble
332dde154f core: new freetext and textchoice options (#728)
* add freetext and freetextchoice options

* fix textchoice. create plando_bosses bool so worlds can check if boss plando is enabled

* remove strange unneccessary \ escapes

* lttp: rip boss plando out of core

* fix broken text methods so they read the data correctly

* revert `None` key in boss_shuffle_options. fix failing tests

* lttp: rewrite boss plando

* lttp: rewrite boss shuffle

* add generic verification step and allow options to set a plando module

* add default typing to plando_options set

* use PlandoSettings intflag for lttp boss plando

* fix plandosettings boss flag check

* minor lttp init cleanup

* make suggested changes. account for "random" existing within plando boss options

* override eq operator

* Please document me!

* Forgot to mention it supports plando

* remove auto_display_name

* Throw warning alerting user to which shuffle is being used if plando is off. Set the remaining boss shuffle in init and boss placement cleanup

* move the convoluted string matching to `from_text`

* remove unneccessary text lowering and actually turn off plando option when it's disabled

* typing

* strong typing for verify method and reorder

* typing is your friend

* log warning correctly

* 3.8 support :(

* also list apparently

* rip out old boss shuffle spoiler code

* verification step for plando bosses and locations

* update plando guide to reference new supported behavior

* empty string is not `None`. remove unneccessary error throw

* Fix bad ordering

* validate boss_shuffle only contains a normal boss option at the end

* get random choice from a list dummy

* >:(

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* minor textchoice cleanup

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-09-17 02:55:33 +02:00
black-sliver
8d51205e8f Alttp: only check item.type for own items with retro_cave 2022-09-17 02:25:09 +02:00
black-sliver
ff05e9d7d5 MultiServer: produce nicer output ...
... for headless and when cancelling the file open dialog
2022-09-17 02:24:51 +02:00
N00byKing
516a52c041 sm64ex: Fix WDW 1Up Block Logic 2022-09-17 02:13:32 +02:00
Alchav
9daa64741b New, smarter fast_fill function (#646)
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2022-09-17 02:06:25 +02:00
Fabian Dill
af11fa5150 Core: auto alias (#1022)
* Test: check that default templates can be parsed into Option objects
2022-09-16 00:32:30 +02:00
TheBigSalarius
156e9e0e43 FF1: Throw exception for Noverworld 2022-09-12 03:48:07 +02:00
Fabian Dill
ef46979bd8 SC2: fix client freezing on exit while SC2 is running (#1011) 2022-09-12 02:08:33 +02:00
Fabian Dill
b2aa251c47 SC2: fix bitflag overflow when multiple instances of an Item are acquired (#1008) 2022-09-12 01:51:25 +02:00
Fabian Dill
e204a0fce6 Subnautica: fix missed item and correct other item pool counts to fit it 2022-09-12 01:44:10 +02:00
TheBigSalarius
bb386d3bd7 FF1: fix FF1Client messaging and scoped lua messaging with printjson
Corrects the issue causing the client and lua messaging not displaying properly after the printjson changes
2022-09-12 01:19:51 +02:00
Fabian Dill
88a225764a FF1: fix printjson 2022-09-12 01:19:51 +02:00
espeon65536
99d2caa57d ALttP: remove link_palettes option (#1004)
* ALttP: remove link_palettes option
It doesn't work anyway so better to have it not visible.
2022-09-07 20:16:32 +02:00
lordlou
ade82e3d60 SM: varia tracker fix (#1006) 2022-09-06 19:56:23 +02:00
Fabian Dill
7c04e7e06f MultiServer: save goal completion flag 2022-09-05 22:11:26 +02:00
Fabian Dill
baf51e5959 SC2: fix Launching Mission: text pulling the unshuffled ID. (#1001)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-09-05 21:09:03 +02:00
toasterparty
8aad75ed23 Tests: Check for Holes in the Item Pool (#992)
* test for holes in the item pool

* Update test/general/TestItems.py

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

* Update test/general/TestItems.py

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

* Update test/general/TestItems.py

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Update test/general/TestItems.py

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

Co-authored-by: alwaysintreble <mmmcheese158@gmail.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-09-05 10:02:40 +02:00
black-sliver
1792b66b3a CI: fix automated builds, update SNI and Enemizer
* Launcher.py always running ModuleUpdate breaks setup.py build --yes
* Use env variables in github workflows
* Update SNI and Enemizer versions in github workflows
* Minor cleanup in workflows
* Silence pycharm warning in Launcher.py
2022-09-05 09:23:08 +02:00
wildham0
5e8ac74b2a FFR: fix NoOverworld mode (#999)
* Add Sigil/Mark to item list
2022-09-05 09:21:00 +02:00
PoryGone
2acc129381 SA2B: Fix typo in doc string (#997) 2022-09-04 14:45:45 +02:00
lordlou
0cbb3c2839 SMZ3: data package fix (#996) 2022-09-03 23:52:09 +02:00
espeon65536
539d2e80f1 OoT: prevent glitched + mq dungeons
this combo is not allowed on main ootr, so we won't have it here either
2022-09-03 21:26:31 +02:00
lordlou
f9e28004a0 SMZ3: item link gt fill fix (#995) 2022-09-03 21:25:55 +02:00
Sunny Bat
b7cfcc9272 New features and fixes for Raft (#984)
* Add DeathLink, small logic changes

* Fix generation, rules, use bool for slotData

* Add more island options

* Update Shovel-related logic

* Update docs
2022-09-03 21:25:04 +02:00
Fabian Dill
4b6d46fd74 Core: update modules 2022-09-03 09:55:47 +02:00
Alchav
b45d8bf221 Patch: Save patch file extension in archipelago.json (#968) 2022-09-02 23:37:37 +02:00
Fabian Dill
f7d107fc0c Subnautica: add some more missed aggressive creatures 2022-09-02 09:06:33 +02:00
alwaysintreble
b14d694e1e templates: fix bug report label 2022-09-01 22:33:22 +02:00
skrawpie
8d2333006a Minecraft: Added shuffled recipe list to en_Minecraft.md (#980)
Co-authored-by: KonoTyran <Kono.Tyran@gmail.com>
2022-09-01 21:26:04 +02:00
Fabian Dill
e413619c26 Tests: verify that a world doesn't use the same ID multiple times (#985) 2022-09-01 21:25:06 +02:00
Yussur Mustafa Oraji
03f66a922d sm64ex: Fix a Location (#979) 2022-09-01 21:21:53 +02:00
black-sliver
b115bdafe7 CI/Doc: Use pytest subtests (#986)
* CI/Doc: use pytest-subtests

* CI: clean up pip installs a bit

* make lint and unittests install the same stuff
* make sure to install wheel, which is a recommended (not required) dependency for everything pip
2022-09-01 09:30:28 +02:00
lordlou
0444fdc379 SM: wasteland ap (#983) 2022-09-01 02:20:30 +02:00
Fabian Dill
c617bba959 SC2: client revamp (#967)
SC2 client now relies almost entirely on the map file and server for the locations and just facilitates them, should make it significantly more resilient to objectives being added or removed


* SC2: fix client crash on printjson messages with more [ than ]

* SC2: move text to queue, that actually clears memory

* SC2: Announce which mission is being loaded


Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-08-31 20:55:15 +02:00
lordlou
8da1cfeeb7 SM: remove events from data package (#973) 2022-08-31 06:14:17 +02:00
black-sliver
fcfc2c2e10 WebHost: fix local_path on python 3.8 (#981)
* WebHost: fix local_path on python 3.8

`__file__` is relative in 3.8, so `os.path.dirname(__file__)` ends up being an empty string breaking calls to `local_path()` (without arguments)

* WebHost: add comment to local_path override
2022-08-31 00:10:18 +02:00
espeon65536
a753905ee4 OoT bug fixes (#955)
* OoT: fix shop patching crash due to Item changes

* OoT: more informative failure in triforce piece replacement

* OoT: in triforce hunt, remove ganon BK from pool and lock the door

* OoT: no longer store trap information on the item
2022-08-30 20:54:40 +02:00
strotlog
2a7babce68 SM+SMZ3: don't abandon checks that happen while disconnected from AP (#946) 2022-08-30 17:16:21 +02:00
Fabian Dill
60d1a27079 Subnautica: revamp aggressive creature scans (#966)
* add forgotten aggressive creatures
* fix logic requirements
* added option to opt out of aggressive creature scans
2022-08-30 17:14:34 +02:00
Fabian Dill
4a2a184db1 Core: remove game-specific arguments from Generate (#971)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-08-30 17:12:33 +02:00
alwaysintreble
45fb735320 Clients: allow games without datapackage (#978) 2022-08-30 00:16:13 +02:00
PoryGone
3eb9e7050f DKC3: Fix Wrinkly Softlock (#963) 2022-08-29 20:04:02 +02:00
CaitSith2
26aed9351e Factorio: Fix a bug with single craft free samples. (#974) 2022-08-29 05:58:26 +02:00
Fabian Dill
b1ffbc49c9 LttPAdjuster: fix GUI for invalid sprite files (#885)
* LttPAdjuster: ignore invalid sprite files

* LttPAdjuster: ignore .gitignore in sprites

* LttPAdjuster: log and show message for invalid sprites

* Alttp: set sprite.valid to False for bad zspr and apsprite ...

... when throwing exceptions

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-08-28 18:30:19 +02:00
Fabian Dill
6d6111de2a Launcher: add ModuleUpdate 2022-08-27 11:13:33 +02:00
Fabian Dill
cc8ce32c61 Options: fix corner case where Toggle.value and Toggle.__int__ would be bool
Which lead to a connect failure in Raft
2022-08-27 11:12:28 +02:00
strotlog
4c94bb0ad5 WebHost: sort game list case-insensitively again 2022-08-26 18:20:37 +02:00
strotlog
af19180ff0 SM: Fix rolling saves, add SRAM features
- fix receiving items in an old save (issue #855) by moving receive queue's read pointer to a per-saveslot value
- clear SRAM over $70:2000, and invalidate save data, when booting a new seed number for the first time
- copy important ROM data to SRAM so future clients don't have to read ROM
2022-08-26 10:32:22 +02:00
CaitSith2
a175aa93e7 Factorio: Detect if more than one AP factorio mod is loaded. (#964) 2022-08-26 10:31:30 +02:00
Zach Parks
a78863fde1 Docs: Update community supported libraries in api doc (#788)
* Docs: Update client supported libraries in api doc

* left align table column

* Update table of languages to include Haxe lib and remarks

* Reformat table

* Changed verbiage on SNI remark
2022-08-26 02:12:37 -05:00
Fabian Dill
0d6cbd9093 Core: convert item name groups to frozenset
Some worlds define them in lists, this speeds up lookup via state.has_group() or similar
2022-08-24 00:19:27 +02:00
Magnemania
1aaf89ff2c SC2: Switched mission item group to a list comprehension to fix missile shuffle errors (#959) 2022-08-23 23:20:39 +02:00
Fabian Dill
295ea97544 Subnautica: increment client version 2022-08-23 23:19:46 +02:00
Fabian Dill
33103b209d WebHost: fix error on save 2022-08-23 23:19:19 +02:00
Fabian Dill
fab12dca0b SC2: add anti air to Devil's Playground Victory
People seem to be on the mission long enough to get attacked by Mutalisks, so Victory should require anti air.
Optional Objectives are doable quite comfortably before Mutalisks show up, allowing the anti-air to be on them for later in the mission.
2022-08-23 23:06:58 +02:00
Fabian Dill
c390801c4c Test: verify file webhost file creations work to some degree (#953)
WebHost: fix some file creation paths
2022-08-23 01:07:17 +02:00
Fabian Dill
e548abd332 Subnautica: use correct option parent class (#954)
* Subnautica: use correct option parent class

* Update Options.py
2022-08-22 19:02:29 -04:00
Jarno
0a5b24be2b [Core] Phase out Print packets and added Countdown type to print json (#812)
* [Core] Added Countdown type to print json to distinct the count down message from other types

* Added backward compatibility check

* Fixed review comments

* Updated header category

* Apply suggestions from code review

Co-authored-by: Hussein Farran <hmfarran@gmail.com>

* Completely phased out Print in favor of PrintJson

* Updated docs to warn about phasing out of Print

* Removed faulty import

Co-authored-by: Hussein Farran <hmfarran@gmail.com>
2022-08-23 01:02:10 +02:00
Chris Wilson
7f41cafffc Explaining the "Style Lockdown" (#940)
* First pass at a contribution guide for the website. Suggestions are welcome.

* Attempt to make the WebHost change guide describe the intent of the style restrictions more accurately.

* Try to improve the explanation of the intention behind the style restrictions.
2022-08-22 19:01:21 -04:00
alwaysintreble
d66f981be6 Github: templates and new user interface (#870)
* move some docs out of readme and link with the headers

* PR template

* bug report template

* task and feature request templates

* md cleanup

* forgot the template

* make expected results separate section

* move pr template to .github. remove assignment field on tasks

* add headers to pr template

* Requested changes

* suggested changes from @black-sliver and @SoldierofOrder

* Update docs/code_of_conduct.md

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>

* Update docs/contributing.md

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>

* Update docs/contributing.md

Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>

Co-authored-by: Hussein Farran <hmfarran@gmail.com>
Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>
2022-08-23 00:39:55 +02:00
alwaysintreble
b66a265726 Docs: Make webworld attribute descriptions docstrings instead of comments for nice IDE things (#929) 2022-08-22 23:50:16 +02:00
Fabian Dill
c695f91198 Subnautica: add Options to Creature Scans (#950) 2022-08-22 23:35:41 +02:00
CaitSith2
11cbc0b40b Factorio: Make the energy bridge a different color. (#952) 2022-08-22 23:30:42 +02:00
N00byKing
87d91aeef3 sm64ex: Option for 1Up Block Rando 2022-08-22 17:52:56 +02:00
Fabian Dill
6a6dfcbaff Core: add some types to generic.Rules 2022-08-22 17:51:06 +02:00
NewSoupVi
9553627136 Witness: More bug fixes (#937)
* Fixed disable_non_randomized and other bugs

* Slight performance & code sensibility increase

* Added River Shortcut to Garden as a disabled check in disable_non_randomized

* Changed no progression items exception to a warning

* Added a list of disabled panels to slot_data for disable_non_randomized, so the client can automatically disable the right panels in the future

* Made no progression exception conditional on playercount
2022-08-22 05:50:01 +02:00
PoryGone
a4a8894d22 Add /SNI to .gitignore (#949) 2022-08-22 01:20:35 +02:00
wordfcuk
bf217dcf85 RoR2: Fixed the link to the game settings page (#945) 2022-08-21 17:30:30 +02:00
CaitSith2
484ee9f065 OoT: More item.type bugs. (#930) 2022-08-21 01:55:41 +02:00
Zach Parks
bba82ccd6c WebHost: Remove "Wiki" link from footer (#943) 2022-08-20 19:17:23 -04:00
alwaysintreble
fb122df5f5 RoR2: code cleanup and styling consistency (#833)
* build locations dict dynamically from the TotalLocations option. Minor styling cleanup

* Minor items styling cleanup. remove unused event items

* minor options cleanup. clarify preset toggle slightly better

* make items.py more readable. add chaos weights dict to use as reference point for generation

* small rules styling and consistency cleanup

* create less regions and other init cleanup

* move region creation to less function calls and move revivals calculation

* typing

* use enum instead of hardcoded ints. fix bug i introduced

* better typing
2022-08-20 19:09:35 -04:00
KonoTyran
be8c3131d8 fix allay advancements requiring note block on the wrong one. (#896) 2022-08-20 19:02:50 -04:00
Fabian Dill
9341332379 WebHost: allow newlines in data-tooltip (#921)
* WebHost: allow newlines in data-tooltip

* WebHost: Tooltips: strip surrounding whitespace

* WebHost: unify tooltips behaviour

* WebHost: unify labels around tooltips

* WebHost: changing tooltips width to max-width to allow small tooltips to not have empty space.

* Minor modifications to tooltips

- Reduce tooltip target to (?) spans
- Set fixed width of 260px on tooltips
- Add space between : and (?) on player-settings
- Removed cursor:pointer on tooltips
- Fix labels for checkboxes on generate.html

Co-authored-by: Chris Wilson <chris@legendserver.info>
2022-08-20 18:58:46 -04:00
Fabian Dill
83bcb441bf Factorio: typo 2022-08-21 00:34:36 +02:00
PoryGone
a074d16297 DKC3 v1.1.0 (#938)
Features:

* KONGsanity option (Collect all KONG letters in each level for a check)
* Autosave option
* Difficulty option
* MERRY option
* Handle collected/co-op locations


Bugfixes:

 * Fixed Mekanos softlock
 * Prevent Brothers Bear giving extra Banana Birds
 * Fixed Banana Bird Mother check sending prematurely
 * Fix Logic bug with Krematoa level costs
2022-08-20 16:46:44 +02:00
TheCondor07
89ab4aff9c SC2: Logic changes and fixes, 6 new locations, 2 removed locations (#933) 2022-08-19 22:50:44 +02:00
lordlou
0ac67bfe76 Smz3 early sword fix (#939) 2022-08-19 15:02:39 +02:00
Fabian Dill
0d61192c67 Factorio: make apworld compatible(#935) 2022-08-18 01:33:40 +02:00
Fabian Dill
a1aa9c17ff Core: convert is_zip to zip_path 2022-08-18 01:20:30 +02:00
Henrique Gemignani Passos Lima
d0faa36eef Fix CommonClient.server_loop with nogui
When running client without a gui, ctx.ui is None
2022-08-18 01:18:01 +02:00
Fabian Dill
22c8153ba8 WebHost: fix indentation in tracker.py 2022-08-17 22:15:56 +02:00
CaitSith2
6602c580f4 Fix another item.type crash bug. (#927)
* Fix another item.type crash bug.

* Another location that can crash, in the instance of plando fixed.
2022-08-17 07:16:14 -07:00
Joethepic
431a9b7023 Docs: Mc: fix version in setup guide (#873)
Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com>
2022-08-17 09:59:22 +02:00
Fabian Dill
d426226bce LttP: run optimize imports on __init__ 2022-08-16 23:57:59 +02:00
Fabian Dill
09afdc2553 Webhost: prevent tracker crashes with LttP key itemlinks (#922) 2022-08-16 23:57:26 +02:00
Fabian Dill
ca83905d9f Core: allow loading worlds from zip modules (#747)
* Core: allow loading worlds from zip modules
RoR2: make it zipimport compatible (remove relative imports beyond local top-level)

* WebHost: add support for .apworld
2022-08-15 23:52:03 +02:00
black-sliver
086295adbb AutoWorld: add preliminary .apworld specification (#903)
* AutoWorld: add preliminary .apworld specification

* Doc: apworld specification: fix typo
2022-08-15 23:47:32 +02:00
alwaysintreble
81cf1508e0 Core: Refactor Autoworld.options to Autoworld.option_definitions (#906)
* refactor `world.options` -> `world.option_definitions`

* rename world api reference

* missed some self.options
2022-08-15 23:46:59 +02:00
Fabian Dill
8484193151 Core: crash if non_local pool is too big 2022-08-15 23:36:07 +02:00
Fabian Dill
d10fbf8263 Minecraft: update requests 2022-08-15 23:35:51 +02:00
Fabian Dill
f73b3d71bf Factorio: fix typo 2022-08-15 23:03:03 +02:00
Fabian Dill
d48d775a59 Subnautica: fix 2 logic/locations bugs and add a bit of docs (#917) 2022-08-15 22:53:59 +02:00
Yussur Mustafa Oraji
f716bfc58f sm64ex: Fix Second Floor Door Cost (#909) 2022-08-15 17:29:35 +02:00
Jarno Westhof
97b388747a Docs: Added DS3 & DK3 to network graph 2022-08-15 16:56:55 +02:00
lordlou
898fa203ad Smz3 updated to version 11.3 (#886) 2022-08-15 16:48:13 +02:00
CaitSith2
c02c6ee58c Fix generation failure for Final Fantasy 1 and Dark Souls 3. (#907)
* Fix generation failure for Final Fantasy 1.

* Fix spoiler log giving "Location (Player x): Item (Player y)" for FF1.

* Dark Soul 3 Items/Locations now get player names in spoiler log.
2022-08-14 12:34:46 -07:00
black-sliver
23b04b5069 SM: correctly check if items are SM items 2022-08-14 13:38:52 +02:00
Ludovic Marechal
0ed0d17f38 DS3: Update the setup guide (#878)
* Merge pull request #1 from eudaimonistic/patch-2

Update setup_en.md

(cherry picked from commit 41567697fb89e74301afe651fbde0bafca5946e0)

* DS3: Update english documentation

* DS3: Add French setup guide

* DS3: Fix space formatting in doc

* DS3: Resolve comment
2022-08-14 00:07:36 +02:00
espeon65536
645ede869f OoT: Fix blind item.type reference (#905)
* oot: remove blind reference to item.type

* oot: logical reasoning is hard

* oot: fix blind item.type reference
2022-08-13 04:36:06 +02:00
black-sliver
f5e48c850d Utils: lazy decimal import
decimal is kinda big, there is no noticable difference in performance and the import is unused by webhost's customserver
2022-08-13 00:20:08 +02:00
Fabian Dill
9bd035a19d WebHost: make a fresh Room reload page once if port is not assigned yet 2022-08-12 16:01:02 +02:00
Fabian Dill
2e428f906c Core: document KeyedDefaultDict 2022-08-12 08:34:33 +02:00
black-sliver
b702ae482b Core: clean up Utils.py
* fix import order
* lazy import shutil
* lazy import jellyfish (also speed-up by 0.8%, probably because of inlining)
* yaml:
  * explicitely call Loader UnsafeLoader
  * use CDumper, twice as fast
  * stop leaking leak imported names load and load_all
* open_file: use absolute path
* replace quotes in touched code
* add some typing in touched code
* stringify type hinting for non-imports
* %s/.format -> f
* freeze safe_builtins
* remove double-caching in get_options()
* get rid of some warnings
2022-08-12 08:07:45 +02:00
black-sliver
b8ca41b45f Utils: SI: fix rounding problems (#895)
* Utils: SI: fix rounding problems

999.999 would give 1000.00 instead of 1.00k

* Tests: add Utils: SI tests
2022-08-12 00:46:11 +02:00
CaitSith2
adc16fdd3d Factorio: Don't send researches completed by editor extensions testing forces. (#894) 2022-08-11 18:11:34 +02:00
NewSoupVi
b32d0efe6d Witness: Logic fix for Treehouse in Doors (#892) 2022-08-11 15:57:33 +02:00
black-sliver
c96acbfa23 TextClient: receive all items
By popular demand, this makes /received work again.

Closes #887
2022-08-11 01:06:58 +02:00
black-sliver
ffe528467e Generate: remove period for easy copy&paste
Double-clicking in terminal may select the period, resulting in a bad filename in clipboard.
Also fixing quotes.
2022-08-11 01:06:43 +02:00
Fabian Dill
b989698740 WebHost: fix datapackage typo 2022-08-11 01:04:53 +02:00
Fabian Dill
29e0975832 Clients: prepare for removal of players key in RoomInfo 2022-08-11 00:48:38 +02:00
CaitSith2
e1e2526322 LttP: Do a check for enemizer much earlier in generation. (#875) 2022-08-10 22:21:52 +02:00
Fabian Dill
f2e83c37e9 WebHost: use title-typical sorting for game titles (#883) 2022-08-09 22:21:45 +02:00
Fabian Dill
debda5d111 MultiServer: swap auto-forfeit with auto-collect order
That way the forfeit for items for players that are still playing appear last in the log, which is the visible text in at least the py clients
2022-08-09 16:58:02 +02:00
alwaysintreble
2c4e819010 docs: plando update (#861)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-08-09 10:47:01 +02:00
alwaysintreble
b3700dabf2 Core: Fix meta.yaml and allow the None game category for common options (#845) 2022-08-09 02:29:00 +02:00
TheCondor07
fb2979d9ef SC2: Added Difficulty Override to Client (#863) 2022-08-09 00:20:51 +02:00
Fabian Dill
a378d62dfd SC2: fix Moebius Factor rescue condition (#882) 2022-08-08 23:20:18 +02:00
lordlou
eb5ba72cfc Smz3 min accessibility fix (#880) 2022-08-08 22:23:22 +02:00
Fabian Dill
c1e9d0ab4f WebHost: allow customserver to skip importing worlds subsystem for hosting a Room (#877)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-08-07 18:28:50 +02:00
black-sliver
181cc47079 Core: cleanup BaseClasses.Location
This is just cleanup and has virtually no performance impact.
2022-08-07 13:11:12 +02:00
Zach Parks
04eef669f9 StS: Add a description for the game. (#876) 2022-08-06 21:36:32 -05:00
PoryGone
9167e5363d DKC3: Correct File Extension in Setup Guide (#872) 2022-08-06 13:26:02 +02:00
Zach Parks
f1c5c9a148 WebHost: Fix OptionDicts that define valid_keys from outputting as [] on Weighted Settings export. (#874)
* WebHost: Fix OptionDicts that define valid_keys from outputting as [] on Weighted Settings export
2022-08-06 13:25:37 +02:00
Joethepic
69e5627cd7 HK: fix indentation on mimic grubs (#868) 2022-08-06 02:11:10 +02:00
PoryGone
ae3e6c29e3 DKC3: Add Link to Tracker from Setup Guide (#871) 2022-08-06 00:53:48 +02:00
black-sliver
f6da81ac70 Core: cleanup Item classes (#849) 2022-08-06 00:49:54 +02:00
Jarno Westhof
dd6e212519 [Core] Colorama fix 2022-08-05 17:17:40 +02:00
Fabian Dill
95bba50223 WebHost: fix filename rename in flask update 2022-08-05 17:16:26 +02:00
Fabian Dill
21f7c6c0ad Core: optimize away Item.world (#840)
* Core: optimize away Item.world

* Update test/general/TestFill.py

* Test: undo unnecessary changes

* lttp: remove two more Item.world writes

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-08-05 17:09:21 +02:00
Fabian Dill
d15c30f63b Stats: limit to recognized games 2022-08-05 17:01:02 +02:00
Fabian Dill
db5b7e5db9 Core: update version 2022-08-05 14:32:09 +02:00
black-sliver
7c808bb03b SMZ3: Fix Swamp Palace Entrace for minimal accessibility 2022-08-05 14:29:36 +02:00
black-sliver
530b6cc360 SMZ3: FixJunkFillGT making invalid placements 2022-08-05 14:29:22 +02:00
Fabian Dill
95012c004f Subnautica: update docs with resume info (#853)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: strotlog <49286967+strotlog@users.noreply.github.com>
2022-08-05 14:23:21 +02:00
Fabian Dill
59918b9dbc Core: patch stream_input to ignore non-parsable input (such as EOF encoded as 0xff) (#854) 2022-08-03 14:53:14 +02:00
alwaysintreble
b47cca4515 HK: Add bug report link (#824)
Co-authored-by: Hussein Farran <hmfarran@gmail.com>
2022-08-03 14:41:27 +02:00
CaitSith2
5f27019855 Add an optional path to factorio server-settings.json (#851)
* Add an optional path to factorio server-settings.json

* factorio: changes

* use forward slashs in host.yaml going forward.  (works on all OSes.)
* comment out the host.yaml server_settings option.
* assume that server_settings is NOT provided and explicitly check for its existence in factorio_client.
2022-08-01 14:57:30 -07:00
NewSoupVi
0b228834c2 The Witness: Logic fix (unbeatable seed) (#850) 2022-08-01 20:09:34 +02:00
Fabian Dill
57979b9287 WebHost: update flask (#804) 2022-08-01 12:41:15 +02:00
CaitSith2
4b85000960 Fixed a crafting category bug related to fluids. (#848) 2022-07-31 14:01:39 -07:00
Zach Parks
d1f34d088b WebHost: Add links to "Setup Guides" in Supported Games page (#847)
* WebHost: Add links to "Setup Guides" in Supported Games page

* Remove a hanging console.log() I left in
2022-07-31 11:17:26 -04:00
alwaysintreble
3bc9392e5b Core: have generation print plando settings as string instead of numbers (#843)
* have generation print plando settings as string instead of numbers

* Change to __str__

* Make to_string not a class method

* Suggested fix

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Fix the fix

* Better quotes

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-07-31 12:02:36 +02:00
lordlou
75165803a0 Sm smz3 create item fix (#844) 2022-07-31 11:08:41 +02:00
lordlou
afc9c772be Sm broken start location fix (#841)
* - fixed basepatches application order breaking (at least) starting location
2022-07-30 18:42:02 +02:00
PoryGone
07450bb83d Migrate DKC3 to APDeltaPatch (#838)
* Add DKC3 to APDeltaPatch

* Undo unintended commit

* More undoing

* Remove else clause
2022-07-29 01:51:22 +02:00
Fabian Dill
2ff7e83ad9 WebHost: make a deeply buried if tree for games a bit more automatic 2022-07-29 01:47:19 +02:00
black-sliver
d817fdcfdb Doc: move Running from source from wiki to docs (#797)
* Doc: move "Running from source" from wiki to docs/

* Doc: update links and reformat running from source

* Doc: implement suggestions in "Running from source"

thanks @alwaysintreble

* Doc: update link to "Running from source"

also link docs/ folder

* Doc: Running from source: Apply suggestions from code review

Co-authored-by: KonoTyran <Kono.Tyran@gmail.com>

Co-authored-by: KonoTyran <Kono.Tyran@gmail.com>
2022-07-29 01:18:59 +02:00
PoryGone
f3d966897f Prevent Krematoa Crash (#832)
* Prevent Krematoa Crash, add crash robustness

* Remove print statements

* Don't remove ctx.rom if save file dies

* Consolidate logic for readability
2022-07-29 01:13:00 +02:00
Jarno
9acaf1c279 [Docs] Further explained the mythical InvalidPacket (#828)
* [Docs] Further explained the mythical `InvalidPacket`

* Fixed header category

* Update docs/network protocol.md

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Update docs/network protocol.md

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Hussein Farran <hmfarran@gmail.com>

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: Hussein Farran <hmfarran@gmail.com>
2022-07-29 01:11:52 +02:00
NewSoupVi
fd6a0b547f Witness: Fatal logic bug fix (#837)
* Renamed some event items

* Fatal logic bug: Door panels did not check their symbol items
2022-07-28 23:43:35 +02:00
lordlou
c02f355479 Smz3 no progression gt fix (#818) 2022-07-28 12:04:48 +02:00
black-sliver
7d9203ef84 CI: update SNI to 0.0.82 2022-07-28 07:55:53 +02:00
Fabian Dill
e849e4792d WebHost: games played per day plot per game on stats page (#827)
* WebHost: generate stats page palette for maximum hue difference between neighbours.

* WebHost: add per game played stats
2022-07-27 23:36:20 +02:00
Fabian Dill
4565b3af8d DKC3: fix missing default options in Utils.py 2022-07-27 23:34:14 +02:00
Fabian Dill
e5b868e0e9 WebHost: fix 30 days cutoff for stats (#826) 2022-07-27 23:09:40 +02:00
Fabian Dill
489450d3fa SNIClient: fix program not exiting if SNI does not exist nor is running 2022-07-27 22:45:53 +02:00
Fabian Dill
73afab67c8 LttP: fix deprecated use of isSet() (#831) 2022-07-27 22:21:06 +02:00
SoldierofOrder
c61f77029b SC2 docs: Extensive reworks and rewordings. (#809) 2022-07-26 16:53:30 +02:00
Fabian Dill
79702aba65 WebHost: flask caching did a rename 2022-07-26 09:53:18 +02:00
strotlog
1e366ff66f SM: smoother co-op, basepatch internal improvements (#793)
* SM: remote touch instantly, pull ips refactor and symbols

* SM: remove hard-coded ROM address writes

* SM: Full length player table, incl. receive-only player ids

+ apply PR feedback (correct graphic offset, readable data file paths)
2022-07-26 09:43:39 +02:00
Fabian Dill
a0482cf27e Archipidle: Fix forgotten version increment when a new item was added 2022-07-26 09:32:21 +02:00
Ludovic Marechal
288a623ab6 Update ds3 locations and items (#819)
* DS3: Add more rules to avoid softlocks, remove Path of the Dragon gesture location/item and remove useless comments

* DS3: Add more Hostile NPCs locations/items

* DS3: Add missing key items to the key items list
2022-07-26 09:31:16 +02:00
Alchav
3b2037a2d4 HK - focus location (#778) 2022-07-25 22:19:07 +02:00
Fabian Dill
ce536fa3ac Subnautica: fix Multipurpose Room not acquirable in valuable item pool
BaseRoomFragment doesn't exist in vanilla, so when valuable item pool marked it as scannable in vanilla location it did not work, as it's technically BaseRoom
BaseRoom is also required to install other modules into, modules that are already marked as useful, so logically if it's required for other useful stuff it should also be marked as useful
By switching from Fragment to non-fragment one now needs 1 out of 2 instead of 2 out of 2 items, which I consider a plus as well.
2022-07-25 22:17:42 +02:00
PoryGone
41883e44e7 DKC3 - Logic Softlock Fix (#817)
* Add two locations to Trade Sequence List

* Remove trace sequence locations from ROM data dict
2022-07-25 21:34:31 +02:00
Yussur Mustafa Oraji
c3ff201b90 sm64ex: Various Features (#790)
* sm64ex: Course and Secret Randomizer

* sm64ex: Allow higher star door costs, raise minimum amount of stars, deprecate ExtraStars

* sm64ex: Support setting MIPS costs

* sm64ex: Safeguard MIPS Costs
2022-07-25 18:39:31 +02:00
espeon65536
e6635cdd77 OOT updates (#821)
* oot: remove all escape characters in LogicTricks.py

* only attempt to connect to client once

* oot: don't kill player outside ToT or in market entrance
fixed camera makes the game crash outside ToT. added market entrance to be safe, it doesn't matter if you don't die there
2022-07-25 02:07:22 +02:00
NewSoupVi
cfc9d79c79 The Witness: Small changes in response to beta tests (#801)
* Option order and better tooltip

* Logic fix: Hedge Laser requires access to all Hedges

* Add item groups: Lasers, Symbols, Doors

* Update worlds/witness/items.py

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Comment for clarity

* Logic fix

* Another logic fix

Co-authored-by: metzner <unconfigured@null.spigotmc.org>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-07-23 12:42:14 +02:00
lordlou
fe2c355739 Sm beam door speedkeep fun accessibility (#785)
added speedkeep option
now forces accessibility to "minimal" instead of (to be deprecated) "item" when "fun" settings is used
2022-07-22 09:44:58 +02:00
alwaysintreble
04c3429839 LttP: Fix scam options (#806) 2022-07-22 00:04:41 -05:00
PoryGone
cabbe0aaf6 Donkey Kong Country 3 Implementation (#798)
* Baseline patching and logic for DKC3

* Client can send, but not yet receive

* Alpha Test Baseline

* Bug Fixes and Starting Lives Option

* Finish BBH, add world hints

* Add music shuffle

* Boomer Costs Text

* Stubbed in Collect behaviour

* Adjust Gyrocopter option

* Add Bonus Coin junk replacement and tracker support

* Delete bad logs

* Undo host.yaml change

* Refactored SNIClient

* Make Swanky Free

* Fix Typo

* Undo SNIClient run_game hack

* Fix Typo

* Remove Bosses from Level Shuffle

* Remove duplicate kivy Data

* Add DKC3 Docs and increment Data version

* Remove dead code

* Fix mislabeled region

* Add Dark Souls 3 to README

* Always force Cog on Rocket Rush Flag

* Fix Single Ski lock and too many DK Coins

* Update Retroarch version number

* Don't send DKC3 through LttP Adjuster

* Comment Location ROM Table

* Change ROM Hash prefix to D3

* Remove redundant constructor

* Add ROM Change Safeguards

* Properly mark WRAM accesses

* Remove outdated region connect

* Fix syntax error

* Fix Game description

* Fix SNES Bank Access

* Add isso_setup for DKC3

* Double Quote strings

* Escape single quotes I guess
2022-07-22 00:02:25 -05:00
Jolteon0163
a7787d87f9 Add to the ArchipIDLE items list (#807)
* Add to the ArchipIDLE items list

* Update Items.py

* Update Items.py
2022-07-21 18:08:07 -04:00
KonoTyran
79b851189f HK - Fix typos in option names
Fixed max charm and max geo cost display names.
2022-07-21 09:57:10 -07:00
Fabian Dill
9e972eafb2 Subnautica: Add DeathLink (#803) 2022-07-21 08:39:34 -05:00
Fabian Dill
53a995372f Subnautica: add missed PDA 2022-07-21 10:08:19 +02:00
Fabian Dill
17351021b3 Factorio: update rcon lib 2022-07-20 22:29:51 +02:00
Ludovic Marechal
8ff2c1b6f3 DS3: Add the Dark Souls 3 World into Archipelago (#769) 2022-07-20 12:48:14 +02:00
strotlog
45aea2c8ff ChecksFinder: Linux support via wine (#795)
* ChecksFinder: Linux support via wine

* ChecksFinder: account for custom $WINEPREFIX

* ChecksFinder: wine detection
2022-07-19 07:44:04 +02:00
Fabian Dill
9f5e40283a WebHost: reduce server uptime (#794)
* WebHost: attempt to improve wording of server resume
* WebHost: reduce default room timeout to 2 hours


Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-07-18 21:10:29 +02:00
lordlou
025309ec64 SMZ3: Pedestal hint (#792)
* - fixed missing pedestal and tablets hint text for foreign items (was "Don't waste yout time!", is now "A small victory!")

- small precision to SMZ3 and SM docs about "What does another world's item look like in Super Metroid"
2022-07-17 19:40:23 -05:00
NewSoupVi
bd4850b2b5 The Witness 0.3.4 features (#780)
New options:

Shuffle Doors: Many doors in the game will open on their own upon receiving an item ("key").
Variant - Shuffle Door/Control Panels: Many panels in the game that open doors or control devices in the world will be off until receiving their respective item ("key").
Shuffle Lasers: Lasers no longer activate by solving the laser panel, instead you will get an item that activates the laser.
Shuffle Symbols: Now that there is something else to shuffle (doors / door panels), you can turn off Symbol Rando.
Shuffle Postgame (replaces "Shuffle Hard"): The randomizer will now determine by your settings which panels are in the "postgame" - Meaning they can only be accessed after you can complete your win condition anyway.
2022-07-17 12:56:22 +02:00
Fabian Dill
472e114fb9 Final Fantasy: fix outdated advancement flag 2022-07-17 11:19:00 +02:00
espeon65536
828bcb1266 OoT: Fix gerudo_fortress on normal (#784) 2022-07-16 13:00:00 -05:00
t3hf1gm3nt
9897f4eb4b LTTP: Yaml Update (#765)
removes vendor option from hints, adds scam setting, and adds P option to shop shuffle.
2022-07-16 12:56:23 -05:00
Fabian Dill
e1ef820184 Subnautica: add creature scans 2022-07-16 19:54:55 +02:00
lordlou
b3ad766680 SMZ3: Item link support (#756)
* first working (most of the time) progression generation for SM using VariaRandomizer's rules, items, locations and accessPoint (as regions)

* first working single-world randomized SM rom patches

* - SM now displays message when getting an item outside for someone else (fills ROM item table)

This is dependant on modifications done to sm_randomizer_rom project

* First working MultiWorld SM

* some missing things:

- player name inject in ROM and get in client
- end game get from ROM in client
- send self item to server
- add player names table in ROM

* replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better)

* - reenabled balancing

* post rebase fixes

* updated SmClient.py

* + added VariaRandomizer LICENSE

* + added sm_randomizer_rom project (which builds sm.ips)

* Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning

* properly revert change made to CollectionState and more cleaning

* Fixed multiworld support patch not working with VariaRandomizer's

* missing file commit

* Fixed syntax error in unused code to satisfy Linter

* Revert "Fixed multiworld support patch not working with VariaRandomizer's"

This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b.

* many fixes and improovement

- fixed seeded generation
- fixed broken logic when more than one SM world
- added missing rules for inter-area transitions
- added basic patch presence for logic
- added DoorManager init call to reflect present patches for logic
- moved CollectionState addition out of BaseClasses into SM world
- added condition to apply progitempool presorting only if SM world is present
- set Bosses item id to None to prevent them going into multidata
- now use get_game_players

* first working (most of the time) progression generation for SM using VariaRandomizer's rules, items, locations and accessPoint (as regions)

* first working single-world randomized SM rom patches

* - SM now displays message when getting an item outside for someone else (fills ROM item table)

This is dependant on modifications done to sm_randomizer_rom project

* First working MultiWorld SM

* some missing things:

- player name inject in ROM and get in client
- end game get from ROM in client
- send self item to server
- add player names table in ROM

* replaced CollectionState inheritance from SMBoolManager with a composition of an array of it (required to generation more than one SM world, which is still fails but is better)

* - reenabled balancing

* post rebase fixes

* updated SmClient.py

* + added VariaRandomizer LICENSE

* + added sm_randomizer_rom project (which builds sm.ips)

* Moved VariaRandomizer and sm_randomizer_rom projects inside worlds/sm and done some cleaning

* properly revert change made to CollectionState and more cleaning

* Fixed multiworld support patch not working with VariaRandomizer's

* missing file commit

* Fixed syntax error in unused code to satisfy Linter

* Revert "Fixed multiworld support patch not working with VariaRandomizer's"

This reverts commit fb3ca18528bb331995e3d3051648c8f84d04c08b.

* many fixes and improovement

- fixed seeded generation
- fixed broken logic when more than one SM world
- added missing rules for inter-area transitions
- added basic patch presence for logic
- added DoorManager init call to reflect present patches for logic
- moved CollectionState addition out of BaseClasses into SM world
- added condition to apply progitempool presorting only if SM world is present
- set Bosses item id to None to prevent them going into multidata
- now use get_game_players

* Fixed multiworld support patch not working with VariaRandomizer's

Added stage_fill_hook to set morph first in progitempool
Added back VariaRandomizer's standard patches

* + added missing files from variaRandomizer project

* + added missing variaRandomizer files (custom sprites)

+ started integrating VariaRandomizer options (WIP)

* Some fixes for player and server name display

- fixed player name of 16 characters reading too far in SM client
- fixed 12 bytes SM player name limit (now 16)
- fixed server name not being displayed in SM when using server cheat ( now displays RECEIVED FROM ARCHIPELAGO)
- request: temporarly changed default seed names displayed in SM main menu to OWTCH

* Fixed Goal completion not triggering in smClient

* integrated VariaRandomizer's options into AP (WIP)

- startAP is working
- door rando is working
- skillset is working

* - fixed itemsounds.ips crash by always including nofanfare.ips into multiworld.ips (itemsounds is now always applied and "itemsounds" preset must always be "off")

* skillset are now instanced per player instead of being a singleton class

* RomPatches are now instanced per player instead of being a singleton class

* DoorManager is now instanced per player instead of being a singleton class

* - fixed the last bugs that prevented generation of >1 SM world

* fixed crash when no skillset preset is specified in randoPreset (default to "casual")

* maxDifficulty support and itemsounds removal

- added support for maxDifficulty
- removed itemsounds patch as its always applied from multiworld patch for now

* Fixed bad merge

* Post merge adaptation

* fixed player name length fix that got lost with the merge

* fixed generation with other game type than SM

* added default randoPreset json for SM in playerSettings.yaml

* fixed broken SM client following merge

* beautified json skillset presets

* Fixed ArchipelagoSmClient not building

* Fixed conflict between mutliworld patch and beam_doors_plms patch

- doorsColorsRando now working

* SM generation now outputs APBP

- Fixed paths for patches and presets when frozen

* added missing file and fixed multithreading issue

* temporarily set data_version = 0

* more work

- added support for AP starting items
- fixed client crash with gamemode being None
- patch.py "compatible_version" is now 3

* commited missing asm files

fixed start item reserve breaking game (was using bad write offset when patching)

* Nothing item are now handled game-side. the game will now skip displaying a message box for received Nothing item (but the client will still receive it).

fixed crash in SMClient when loosing connection to SNI

* fixed No Energy Item missing its ID

fixed Plando

* merge post fixes

* fixed start item Grapple, XRay and Reserve HUD, as well as graphic beams (except ice palette color)

* fixed freeze in blue brinstar caused by Varia's custom PLM not being filled with proper Multiworld PLM address (altLocsAddresses)

* fixed start item x-ray HUD display

* Fixed start items being sent by the server (is all handled in ROM)

Start items are now not removed from itempool anymore
Nothing Item is now local_items so no player will ever pickup Nothing. Doing so reduces contribution of this world to the Multiworld the more Nothing there is though.
Fixed crash (and possibly passing but broken) at generation where the static list of IPSPatches used by all SM worlds was being modified

* fixed settings that could be applied to any SM players

* fixed auth to server only using player name (now does as ALTTP to authenticate)

* - fixed End Credits broken text

* added non SM item name display

* added all supported SM options in playerSettings.yaml

* fixed locations needing a list of parent regions (now generate a region for each location with one-way exits to each (previously) parent region

did some cleaning (mainly reverts on unnecessary core classes

* minor setting fixes and tweaks

- merged Area and lightArea settings
- made missileQty, superQty and powerBombQty use value from 10 to 90 and divide value by float(10) when generating
- fixed inverted layoutPatch setting

* added option start_inventory_removes_from_pool

fixed option names formatting
fixed lint errors
small code and repo cleanup

* Hopefully fixed ROR2 that could not send any items

* - fixed missing required change to ROR2

* fixed 0 hp when respawning without having ever saved (start items were not updating the save checksum)

* fixed typo with doors_colors_rando

* fixed checksum

* added custom sprites for off-world items (progression or not)

the original AP sprite was made with PierRoulette's SM Item Sprite Utility by ijwu

* - added missing change following upstream merge

- changed patch filename extension from apbp to apm3 so patch can be used with the new client

* added morph placement options: early means local and sphere 1

* fixed failing unit tests

* - fixed broken custom_preset options

* - big cleanup to remove unnecessary or unsupported features

* - more cleanup

* - moved sm_randomizer_rom and all always applied patches into an external project that outputs basepatch.ips

- small cleanup

* - added comment to refer to project for generating basepatch.ips (https://github.com/lordlou/SMBasepatch)

* fixed g4_skip patch that can be not applied if hud is enabled

* - fixed off world sprite that can have broken graphics (restricted to use only first 2 palette)

* - updated basepatch to reflect g4_skip removal

- moved more asm files to SMBasepatch project

* - tourian grey doors at baby metroid are now always flashing (allowing to go back if needed)

* fixed wrong path if using built as exe

* - cleaned exposed maxDifficulty options

- removed always enabled Knows

* Merged LttPClient and SMClient into SNIClient

* added varia_custom Preset Option that fetch a preset (read from a new varia_custom_preset Option) from varia's web service

* small doc precision

* - added death_link support

- fixed broken Goal Completion
- post merge fix

* - removed now useless presets

* - fixed bad internal mapping with maxDiff

- increases maxDiff if only Bosses is preventing beating the game

* - added support for lowercase custom preset sections (knows, settings and controller)

- fixed controller settings not applying to ROM

* - fixed death loop when dying with Door rando, bomb or speed booster as starting items

- varia's backup save should now be usable (automatically enabled when doing door rando)

* -added docstring for generated yaml

* fixed bad merge

* fixed broken infinity max difficulty

* commented debug prints

* adjusted credits to mark progression speed and difficulty as Non Available

* added support for more than 255 players (will print Archipelago for higher player number)

* fixed missing cleanup

* added support for 65535 different player names in ROM

* fixed generations failing when only bosses are unreachable

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

* fixed failling generations when using 'fun' settings

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

* fixed debug logger

* removed unsupported "suits_restriction" option

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

* - fixed deathlink emptying reserves

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

* - merged death_link and death_link_survive options

* fixed death_link

* added a fallback default starting location instead of failing generation if an invalid one was chosen

* added Nothing and NoEnergy as hint blacklist

added missing NoEnergy as local items and removed it from progression

* - enabled local item dialog boxes for dungeon and keycard items when keysanity is used

* - fixed ItemLink support

* fixed shops sending checks

* Added get_filler_item_name() returning a random junk item

Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2022-07-16 12:47:26 -05:00
Fabian Dill
74b19dc1f5 WebHost: cleanup generate and hopefully fix SQL concurrency problems 2022-07-16 19:44:29 +02:00
Fabian Dill
449bc93307 Rogue Legacy: obliterate any outdated remnants before installer adds new files 2022-07-16 19:40:59 +02:00
Fabian Dill
622af17705 MultiServer: make !hint prefer non-local 2022-07-16 12:19:24 +02:00
Fabian Dill
a42f7f99fe Factorio: specify rcon version 2022-07-16 01:31:20 +02:00
black-sliver
3c6bd555b4 doc: add style guide (#746)
* doc: add style guide

* doc: style guide for python and markdown

* doc: consistent use of periods and explicit double quotes in style guide

Co-authored-by: Hussein Farran <hmfarran@gmail.com>

* doc: better define string style in style guide

* doc: add format string literals to style guide

* doc: add HTML, CSS and JS to style guide

Co-authored-by: Hussein Farran <hmfarran@gmail.com>
2022-07-15 23:52:35 +02:00
Rome Reginelli
a4211d5f11 Improve Risk of Rain 2 docs (#770)
* Improve Risk of Rain 2 docs

* RoR2: clarify custom item weight settings

* Update worlds/ror2/docs/en_Risk of Rain 2.md

Co-authored-by: Hussein Farran <hmfarran@gmail.com>
2022-07-15 17:24:40 -04:00
Vale
090c5bcf00 RoR2: FinalStageDeath (#766)
Added a YAML option for 'FinalStageDeath', a toggle for 'death on the final boss stage counts as a win'. Defaults to on.

Co-authored-by: Vale <58179315+DelosIX@users.noreply.github.com>
2022-07-15 17:21:36 -04:00
alwaysintreble
82850d7f66 Ror2: reduce locations to 250 and mark legendary items as useful (#776)
* reduce total locations to 250

* minor styling cleanup. mark legendary items as useful

* 😡

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-07-15 17:19:36 -04:00
Yussur Mustafa Oraji
86112351a6 sm64ex: Adapt area_connections slotdata Format (#767) 2022-07-15 20:04:26 +02:00
black-sliver
ce789d1e3e SoE: texts, energy core, fragments, useful (#777)
* fix missing fields in custom prog balancing option
* fix typos and pep8
* update and implement pyevermizer 0.41.3
  * allow randomizing energy core
  * add energy core fragments (turn in at Prof. Ruffleberg)
  * rename some items to avoid confusion
  * differentiate between progression and useful
* remove obsolete 'Bazooka' group
* don't add items to the pool that get removed
2022-07-15 18:01:07 +02:00
Fabian Dill
73fb1b8074 Subnautica: updates (#759)
* Subnautica: add more goals

* Subnautica: fix wrongly positioned Databox

* Subnautica: allow techs to remain vanilla

* Subnautica: make zipimport compatible

* Subnautica: force two Seaglide fragments into local sphere 1
2022-07-15 17:41:53 +02:00
black-sliver
8e15fe51b6 Put common options first (#774)
* this applies to yaml and webhost
* this allows overwriting common options from the world
2022-07-15 06:54:29 +02:00
Fabian Dill
aa954b776d MultiServer: add /status and allow status command to dynamically filter for Tags 2022-07-15 02:52:26 +02:00
jsd1982
76f6eb1434 SNIClient: update default SNI port from 8080 to 23074 2022-07-15 02:51:36 +02:00
Yussur Mustafa Oraji
e38308bac3 sm64ex: Allow setting Big Star Door requirements (#773)
* sm64ex: Allow setting Big Star Door requirements

* sm64ex: Lower requirements for StarsToFinish
2022-07-14 18:37:14 +02:00
SoldierofOrder
e804f592de SC2: Windows ".dll missing" fix and fix for finding SC2 install automatically (#721) 2022-07-14 09:51:00 +02:00
Fabian Dill
6e0a0c5c4a Core: skip second sanity check when pushing an item into a location (-O) (#745) 2022-07-14 09:46:03 +02:00
alwaysintreble
122590fc68 lttp: move open pyramid to new options system (#762) 2022-07-14 09:39:53 +02:00
lordlou
c806366469 Sm comeback too strict (#755) 2022-07-14 09:37:45 +02:00
Zach Parks
0d3bd6e2e8 gitignore general Windows/macOS files (#763) 2022-07-10 19:24:07 +00:00
Fabian Dill
beac0b1acd Requirements: update some modules 2022-07-10 19:37:50 +02:00
Bicoloursnake
1cc9c7a469 Doc: Add english mac guide (running from source) (#744)
* Create RunFromSourceGuideForMac.md

* Update and rename RunFromSourceGuideForMac.md to docs/RunFromSourceGuideForMac.md

* Clarified the source code download.

* Rename docs/RunFromSourceGuideForMac.md to worlds/generic/docs/RunFromSourceGuideForMac.md

* Update __init__.py

* Noted the case where a user might want EnemizerCLI

* Updated document to reflect requested changes

Updated to reflect the requested changes as well as including some information on virtual environments.

* Added Capital Letters to SNIClient.py

* Reworked Document Structure

Numeric order of lists now makes sense and changed the virtual environment section to match Archipelago tradition.

* Update __init__.py

* Minor Changes for clarity's sake

* Renamed file to make webhost happy

* Changed mac guide filename
2022-07-10 03:16:41 +02:00
CaitSith2
17db0805a7 Allow potentially all rocket-part ingredients to be fluids. (#753) 2022-07-09 12:35:38 +02:00
CaitSith2
2f53972c85 Factorio: fix accidental removal of fluids from make_balanced_recipe (#754) 2022-07-08 15:35:33 +02:00
black-sliver
9ac780102e Subnautica: display item_pool as Item Pool on the settings page 2022-07-07 21:22:24 +02:00
Fabian Dill
60b80083e0 LttP: fix shop inventory corruption in upgrade fairy 2022-07-07 13:25:17 +02:00
alwaysintreble
8597b04c41 WebHost: Advanced guide cleanup (#725)
* advanced yaml cleanup

* Update advanced_settings_en.md

* i hate this game now

* formatting reverting
2022-07-06 16:06:32 -05:00
Fabian Dill
6a60c46a99 Subnautica: fix generation crash on valuable item pool (#739) 2022-07-06 17:04:22 -04:00
Fabian Dill
5c2163a1a7 WebHost: fix comment typo 2022-07-06 22:32:33 +02:00
Hussein Farran
a49bcd618d Dev Docs: Add SA2B and SC2 to network diagram (#719)
* Add SA2B and SC2 to network diagram

* Remove jpg version of image.

* Fix png of image... Github web editor borked it

* Update network diagram.svg

* We're back to light mode, friends.

Use SVG and JPG that are valid and let you zoom in properly.
2022-07-06 16:12:53 -04:00
Doug Hoskisson
d76b41afe7 RL: Rename Rogue Legacy Folder (#452)
* rename rogue legacy
"`rogue-legacy` is not a valid python module name"

* revert rename of the documentation file
2022-07-06 14:18:28 -05:00
Sunny Bat
ab2b635a77 Update Raft for Final Chapter (#724) 2022-07-06 04:37:08 +02:00
strotlog
7072c7bd45 docs: fix 2 URLs (#738)
* URL of image in Alttp ES tutorial
* Link to RA download in SMZ3 EN tutorial
2022-07-04 10:49:25 +02:00
Alchav
530c5500c3 Break out of fill loop if locations is empty (#690) 2022-07-03 17:11:11 +02:00
Daniel Grace
8870b577d0 Hollow Knight June 2022 Updates (#720)
This is a combined PR for assorted Hollow Knight updates for June 2022 that have cleared testing. It supersedes any HK-exclusive PRs open by myself or @Alchav unless stated otherwise.

Summary of changes below:

 * Implement Split Claw, Split Cloak, Split Superdash, Randomize Nail, Randomize Focus, Randomize Swim and Elevator 
 * Pass options (@Alchav)
 * Add support for Deathlink with three different modes (@dewiniaid)
 * Add customizable additional shop slots per-shop (@Alchav) and overall (@dewiniaid)
 * Overhaul shop cost output to be more generic and account for all locations with standard costs (such as Stag Stations, Cornifer, and Divine) (@dewiniaid)
 * Add "CostSanity", allowing random prices using any cost type to be chosen for any location with a cost. (e.g. a Stag station requiring 15 grubs to obtain an item)
 * Item classification fixes (Map and Journal items are fillter, Mask Shards/Pale Ore/Vessel Fragments are useful) (@Alchav)
 * Fix Ijii -> Jiji (@Alchav )
 * General code quality updates

The above changes are only for the HK world.
2022-07-03 17:10:10 +02:00
Colin Lenzen
7d85ab471a [Timespinner] Rename flag and add tiered loot settings (#699) 2022-07-03 17:05:44 +02:00
Fabian Dill
3205cbf932 Generate: convert plando settings to an IntFlag with error reporting for unknown plando names (#735) 2022-07-03 14:11:52 +02:00
Fabian Dill
b9fb4de878 BaseClasses: make ItemClassification properties faster 2022-07-02 13:56:35 +02:00
Jarno Westhof
bcd7096e1d [The Witness] Update data_version as it was forgotten for 0.3.3
# Conflicts:
#	worlds/witness/docs/setup_en.md
2022-07-02 12:19:08 +02:00
strotlog
b206f2846a SNES games: use JPN as abbreviation for Japan/Japanese 2022-07-02 12:16:15 +02:00
CaitSith2
8a8bc6aa34 Factorio: Fix impossible seeds for rocket-part recipes as well. (#733) 2022-07-01 00:40:31 +02:00
black-sliver
bce7c258c3 CI: update Enemizer to 7.0.1 2022-06-30 22:55:05 +02:00
Fabian Dill
cea7278faf LttP: now that Enemizer allows for AP rom name, rename it. (#730)
* LttP: now that Enemizer allows for AP rom name, rename it.

* LttP: fix missing Enemizer message parenthesis
2022-06-30 10:00:37 -07:00
alwaysintreble
d7a9b98ce8 fix glossary link on sitemap 2022-06-29 22:08:38 +02:00
Alchav
7dcde12e2e Revert SC2 item classifications 2022-06-29 12:15:19 +02:00
black-sliver
ba2a5c4744 MC: add non-windows install to docs (#713)
* MC: add non-windows install to docs

* MC: better link naming for non-windows doc

Co-authored-by: Hussein Farran <hmfarran@gmail.com>

* MC: doc change manual forge link to index

By removing the direct link to the version we avoid having to update it all the time and users will have to check the other version numbers for manual installation anyway.

Co-authored-by: Hussein Farran <hmfarran@gmail.com>
2022-06-28 19:23:18 +02:00
espeon65536
39ac3c38bf sm64: only apply DDD 100 coin star rule if the location exists (#716) 2022-06-27 23:03:34 -07:00
alwaysintreble
61f751a1db docs: add common terms documentation to website (#680)
* docs: add common terms documentation to website

* minor cleanup

* some rewording and reformatting.

* tighten up world definition clarity

Co-authored-by: Rome Reginelli <mduo13@gmail.com>

* Clarify seed definition a bit better

Co-authored-by: Rome Reginelli <mduo13@gmail.com>

* add text for "out of logic" and that slot names must be unique

* rename common terms to glossary

Co-authored-by: Rome Reginelli <mduo13@gmail.com>
2022-06-27 23:34:47 -04:00
alwaysintreble
5f2193f2e4 ror2: update setup guide (#671)
* ror2: remove yaml template from guide and link to player settings page. Add documentation on chat client

* ror2: copy paste the good config description like everyone else.
2022-06-27 21:05:55 -04:00
Daniel Grace
98b714f84a HK: Add options for Deathlink. (#672) 2022-06-27 21:05:29 -04:00
alwaysintreble
2a0198b618 multiserver: allow !release as an alias for !forfeit (#693)
* multiserver: allow `!release` as an alias for `!forfeit`

* create `/release` command. Add some periods to messages that print in console and point users to release

* Add a missing space on line 1135

Co-authored-by: Chris Wilson <chris@legendserver.info>
2022-06-27 20:59:42 -04:00
The T
cd9f8f3119 SM64: DDD 100 Coins in Entrance Rando should expect sub removal (#711)
I brought this up in #super-mario-64, and the minor consensus is that 100 Coins is "possible", the same way Red Coins is possible.

According to a FAQ online, DDD has 106 coins. That means you are still required to get at least 5 of the red coins in order to get the 100 coin star. If we already have a rule stating the Red Coins require the sub to be removed (by reaching Bowser in the Fire Sea), it should apply to the 100 coins as well.

The consensus on it being "possible" was that it requires a very specific triple jump. There is no "Strict" category for this since it isn't caps/cannons-based, but it is extremely unreasonable to casual play. If you want to sequence break it, go for it, but I don't think it should be expected.
2022-06-27 07:43:48 -05:00
CaitSith2
37b569eca6 Changes: (#639)
* Changes:

* When client loses connection to the server through no fault of your own, it no longer forgets your username.
* It is now possible to do /connect archipelago://username:password@server:port or to paste archipelago://username:password@server:port into the connect bar and hit connect, and have both the username/password filled in that way.

* Switch checksfinder client to getting username from url if suppplied by url.

* Correct the print statement
2022-06-27 03:10:41 -07:00
Kippi00
d317111d20 Updates to ALTTP, SM, and SMZ3 guides (#703) 2022-06-27 09:40:01 +02:00
alwaysintreble
3f1d216d28 docs: add reference to text client and commands to a few setup guides (#694) 2022-06-26 21:52:24 -04:00
Joethepic
0ca3d73ae9 makes easier to find where to put the launch options for steam version v6 (#712)
* Update setup_en.md

* Update setup_en.md

* Update setup_en.md

* Update setup_en.md

* typo fix spaces clarification

Co-authored-by: Zach Parks <zach@alliware.com>

* Grammar corrections, clarifications, removed redundant explanations

* Markdown syntax fix

Co-authored-by: Zach Parks <zach@alliware.com>
Co-authored-by: Chris Wilson <chris@legendserver.info>
2022-06-26 19:08:16 -04:00
alwaysintreble
1972d531b9 MC: fix broken brewing image on minecraft tracker (#707) 2022-06-25 14:11:20 -05:00
alwaysintreble
5006c79a00 SM64: Add common mistake and troubleshooting to setup guide (#708) 2022-06-25 14:07:03 -05:00
Daniel Grace
8788ee1aa7 [HK] Further updates for White Palace logic, (#662) 2022-06-25 20:15:03 +02:00
Chris Wilson
17ba73b0b8 Rename author to authors for consistency 2022-06-25 19:10:20 +02:00
rsyh93
0407df83b7 SC2: add Linux setup to tutorial (#679)
also fixes some formatting
2022-06-25 14:12:30 +02:00
alwaysintreble
f140aadafe Alttp: fix broken msu es link (#702) 2022-06-25 13:15:57 +02:00
Grrmo
b41c6185e4 TS: Fix broken link to german setup guide (#700)
The German tutorial link pointed to the English version
2022-06-25 00:29:25 +02:00
NewSoupVi
aa3d7f5e21 Small Witness fixes (#698) 2022-06-24 19:25:23 +02:00
black-sliver
efadf6fdf4 UX: More errors (#697)
* SNIClient: adjuster, ignore missing Tk

* UI: add support for gtk/kde messagebox

* SNIClient: show error when patching fails
2022-06-23 19:26:30 +02:00
black-sliver
12863e9b04 CI: update enemizer and sni (#696) 2022-06-23 19:25:55 +02:00
Chris Wilson
1843618c99 Add stone theme to WebHost (#645)
* Add stone theme

* Fix h2 color, change rogue-legacy to stone theme (approved by Phar)

* Add stone theme preview to world api.md

* Different stone theme preview to match other images
2022-06-22 20:31:40 -04:00
alwaysintreble
4e5071fd68 core: add a link to FAQ to the repo readme 2022-06-22 16:30:43 +02:00
TheCondor07
6e918edce1 SC2: Updated apsc2 version required (#691) 2022-06-22 11:49:00 +02:00
Fabian Dill
80ff5a18b1 remove limit of 1000 Yotta-Joule in EnergyLink (#689) 2022-06-21 20:50:40 +02:00
Fabian Dill
d112cc585f Clients: fix /received calling a dict instead of indexing (#688) 2022-06-21 15:46:35 +02:00
Fabian Dill
3fec33f56c Clients: fix clients not requesting Archipelago DataPackage updates unless spectator is present. 2022-06-21 09:02:11 +02:00
Alchav
68674deb00 FF1 - classify some items as useful (#669) 2022-06-20 21:17:57 +02:00
PoryGone
a9e530721d SA2B v1.1.0 (#673)
Co-authored-by: RaspberrySpaceJam <tyler.summers@gmail.com>
2022-06-20 21:12:13 +02:00
black-sliver
03e9034a98 Server: minify cmd json
This saves between 7 and 15% where compression is unavailable.
2022-06-20 07:52:21 +02:00
Daniel Grace
6970c5ce97 HK: Bugfix shop requirements to be >= rather than >.
This was causing off-by-one errors, which were problematic if e.g. a Grubfather slot wanted all 46 grubs.
2022-06-20 07:46:25 +02:00
alwaysintreble
10b3803a7f ror2: correctly mark Dio's as progression and mark equipment as useful 2022-06-19 22:26:48 +02:00
Fabian Dill
a7e8c82633 Factorio: more condensed raw_recipes creation
(by black-sliver)
2022-06-19 21:55:03 +02:00
Fabian Dill
6d4c4295b3 Factorio: use resources data 2022-06-19 21:48:30 +02:00
black-sliver
47edc356ad api.md update and rename (#676)
* api.md: update for ItemClassification

* world api.md: rename from api.md
2022-06-19 15:19:46 +02:00
black-sliver
b551e3a2ad SoE: change default prog balancing to 30 2022-06-19 14:17:42 +02:00
black-sliver
a9c32bc2e2 MinecraftClient: Linux fixes (#668)
* MC: open file selector if client is run without apmc

* MC: linux fixes

* we don't use shell anymore
* use user_path for forge_dir. Unless read-only, this is the same as what cwd is set to.
2022-06-19 04:54:10 -07:00
alwaysintreble
60c7be87f8 lttp: update requirement version for lttp template yaml 2022-06-19 01:59:50 +02:00
Fabian Dill
2bac78b4a4 Factorio: manual crude-oil recipe seems no longer needed and actually messed with costs 2022-06-18 13:57:28 +02:00
Fabian Dill
c4769eeebb Factorio: load fluids from exported data 2022-06-18 13:40:10 +02:00
espeon65536
51341f6255 MC client: use user_path to fix appimage permissions 2022-06-18 13:21:54 +02:00
Daniel Grace
c7a32dc91b Sort hints by found/not found and then other world/own world. (#642)
This updates notify_hints() as follows:

  - Sort hints by their 'found' attribute in reverse during the first
    iteration, so items not found will show at the bottom.
  - Store a tuple of (hint, hint.as_network_message()) in concerns rather
    than just the hint so the raw hint data remains available for later
    sorting.
  - Do the logging.info call as part of this iteration instead of doing
    a second iteration pass that does nothing but logging.
  - Iterate over concerns (and look up connected clients) rather than
    iterating over all clients (and checking for concerns)
2022-06-18 09:19:08 +02:00
black-sliver
3623678c93 Launcher: always use kvui 2022-06-18 09:17:10 +02:00
Fabian Dill
a5d516e179 Factorio: fix impossible recipes requiring stacking non-stacking items
Factorio: speedup load time
2022-06-18 09:15:14 +02:00
black-sliver
2045905c9b setup.py: fix setuptools>=61 compatibility
Closes ArchipelagoMW/Archipelago#391
2022-06-17 15:09:58 +02:00
Fabian Dill
26c027a075 Core: downgrade item classification to int before writing to file 2022-06-17 06:10:30 +02:00
Fabian Dill
b86ee20f3f Core: fix ItemLinks setting advancement flag 2022-06-17 05:26:11 +02:00
Fabian Dill
50c75e9684 Core: increment version 2022-06-17 03:57:02 +02:00
Fabian Dill
d87c3d5323 LttP: update manual yaml 2022-06-17 03:48:54 +02:00
Fabian Dill
247f674749 Network remove roominfo players (#661) 2022-06-17 03:34:50 +02:00
Fabian Dill
74fe03414c HK: extractor now needs to check for BOM 2022-06-17 03:25:08 +02:00
Fabian Dill
65d213c494 kivy: include in frozen library zip 2022-06-17 03:24:38 +02:00
Fabian Dill
05a51346f9 LttP: fix Ganon's Tower trash prefill ignoring item_rules (#648) 2022-06-17 03:24:15 +02:00
Fabian Dill
6c525e1fe6 Core: move multiple Item properties into a single Flag (#638) 2022-06-17 03:23:27 +02:00
Fabian Dill
5be00e28dd Tests: always display all warnings
WebHost: fix a warning about new cache names
2022-06-17 03:22:43 +02:00
Fabian Dill
d81dbbd951 CommonClient: revamp DataPackage handling 2022-06-17 03:22:20 +02:00
Fabian Dill
83dee9d667 MultiServer: introduce LocationScouts create_as_hint -> only_new 2022-06-17 03:21:33 +02:00
NewSoupVi
7d79cff66f The Witness - 0.3.3 features and fixes (#617)
New option: "Early Secret Area" (Opens a door to the Challenge Area from the start of the game)
New option: Victory Conditions "Mountaintop Box Short" and "Mountaintop Box Long"
New options: Number of Lasers of Mountain, Number of Lasers for Challenge
New option & item: Add some number of "Puzzle Skips", which let you skip one puzzle in the game

Many logic fixes
2022-06-16 03:04:45 +02:00
Alchav
0a63bd0fc6 Meritous get_filler_item_name 2022-06-15 19:05:48 +02:00
Fabian Dill
55d8c8c928 Generate: ignore files starting with ., something about Macs having a .DS_STORE or something. (#656)
* Generate: ignore files starting with ., something about Macs having a .DS_STORE or something.

* Generate: .name is important
2022-06-14 18:10:41 -07:00
Fabian Dill
681f7041dc Tracker: fix order received column being empty 2022-06-14 08:13:02 -07:00
Kono Tyran
d5f15e6408 fix spaces in folder names failing to launch forge. 2022-06-14 06:56:47 -07:00
Fabian Dill
70d510dff8 Options: fix all games templates breaking due to invalid progression balancing 2022-06-14 03:56:02 +02:00
CaitSith2
2a5c128267 ChecksFinder Client refactored to import CommonClient components. 2022-06-14 01:38:10 +02:00
Daniel Grace
e5a1052089 Hollow Knight updates (goals, WP/POP, etc.) (#438)
* Hollow Knight updates:

- Add configurable goals (Any, THK, Siblings, Radiance)
  - Change base logic to require Opened_Black_Egg_Temple instead of
    requiring 3 dreamers.  This is future-proof for transition rando,
    where Black Egg might not have been located yet.
  - Add combat logic for THK and Radiance on par with Rando4's boss logic,
    so itemless HK shouldn't be required.
- Existing completion logic now uses Black_Egg_te

- Add White Palace options
  (Exclude, King Fragment Only, No Path of Pain, Include)
  - Excluded WP may still be required for King Fragment if Charms are
    not randomized
  - Simply don't place WP locations that are excluded
  - Distinguish between POP locations (required for POP), WP checks (
    actual item locations), WP transitions (relevant for future transition
    rando), and WP events (logically required to reach King Fragment)
  - Many transitions were listed twice.  Remove duplicates.
  - Sort transitions by scene

- For randomizable locations that have no logical significance when not
    randomized, simply skip adding them to the pool entirely for
    theoretically faster generation.

* Hollow Knight updates

  - Support random starting geo up to 1000 geo.
  - Always include locations rather than dropping unrandomized "logicless"
    ones, as it is required to best support same-slot coop.
2022-06-13 08:23:03 +02:00
Fabian Dill
8c64f6221e WebHost: update Flask-Limiter 2022-06-13 08:20:17 +02:00
Fabian Dill
0869a2acc3 SNIClient: prevent hang on exit if waiting on devices from SNI 2022-06-13 08:18:52 +02:00
Fabian Dill
e7ea827f02 Options: introduce SpecialRange (#630)
* Options: introduce SpecialRange

* Include SpecialRange data in player-settings and weighted-settings JSON files

* Add support for SpecialRange to player-settings pages

* Add support for SpecialRange options to weighted-settings. Also fixed a bug which would cause the page to crash if an unknown setting was detected.

Co-authored-by: Chris Wilson <chris@legendserver.info>
2022-06-12 17:33:14 -04:00
Joethepic
84b6ece31d Itemlink tutorial improvement (#611)
* Update Items.py

* Update advanced_settings_en.md

* Update Items.py

* Update advanced_settings_en.md

* Update advanced_settings_en.md

* improve consistency

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

* fix formating on game setting in example

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

* change version

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

* Update advanced_settings_en.md

* Update advanced_settings_en.md

* Update advanced_settings_en.md

* tutorials: add description for game weight and properly document item links

* tutorials: add description for null replacement

* Update worlds/generic/docs/advanced_settings_en.md

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

* Update advanced_settings_en.md

* Update advanced_settings_en.md

* Update worlds/generic/docs/advanced_settings_en.md

Co-authored-by: Hussein Farran <hmfarran@gmail.com>

* Update worlds/generic/docs/advanced_settings_en.md

Co-authored-by: Hussein Farran <hmfarran@gmail.com>

* Update worlds/generic/docs/advanced_settings_en.md

Co-authored-by: Hussein Farran <hmfarran@gmail.com>

Co-authored-by: alwaysintreble <mmmcheese158@gmail.com>
Co-authored-by: Hussein Farran <hmfarran@gmail.com>
2022-06-12 17:24:19 -04:00
Zach Parks
1bcc5b6582 WebHost: Allow "random" to be default option for toggles and choices. (#640) 2022-06-12 07:48:52 +02:00
KonoTyran
c8c025ac34 Minecraft 1.19 (#623) 2022-06-11 23:22:16 +02:00
CaitSith2
d82d70ac97 Fix the possibility of manually assigning 'random' via alias_random 2022-06-11 23:20:56 +02:00
alwaysintreble
3e86fd4e57 Tutorials: hide ArchipIDLE (#622)
* Don't copy files of hidden worlds

* tutorials: hardcode not generating ArchipIDLE tutorial files outside april

* tutorials: ignore hidden worlds unless it's 'Archipelago'

* add parenthesis to prevent ambiguity
2022-06-10 19:49:12 -04:00
Alchav
964eda13cc Fix LTTP filler items (#621) 2022-06-10 13:23:03 +02:00
CaitSith2
c16815b16d Fix Room log 2022-06-10 13:20:35 +02:00
Colin Lenzen
74ee8ec459 [Timespinner] Add Boss Randomization Settings (#598)
* [Timespinner] Add Boss Randomization Settings
2022-06-10 01:07:47 +02:00
t3hf1gm3nt
22ea72c1b2 OOT: Add note about common issue with lua option in the configuration step (#629)
* OOT: Add note about common issue with lua option in the configuration step

More and more people have issues with connecting with OoT because fresh installs of newer versions of Bizhawk show having "Lua+LuaInterface" selected when it actually loads "Nlua+KopiLua" instead until you toggle between the two options. Hopefully adding this bolded note will help new users avoid this problem in the future.
2022-06-10 00:48:05 +02:00
Zach Parks
613dc4184a ALTTP: Updates to setup documents (#628)
Co-authored-by: alwaysintreble <mmmcheese158@gmail.com>
2022-06-10 00:47:01 +02:00
Fabian Dill
9a471aff1b WebHost: request maximum amount of file handles from the system for autolauncher. (#625)
* WebHost: request maximum amount of file handles from the system for autolauncher.

* WebHostLib: wrap resource import into try to restore windows compatibility
2022-06-09 13:14:12 -07:00
Fabian Dill
e69e42cabc SNIClient: sort devices for consistent key
SNIClient: get rid of * import
2022-06-09 13:05:30 -07:00
Fabian Dill
1281426075 HK: allow shuffling charm costs, instead of randomizing. (#441) 2022-06-09 00:27:43 +02:00
Fabian Dill
8b1baafddf SC2: send ItemLink messages to ingame as well 2022-06-09 00:20:36 +02:00
Kippi00
ee65d7e5fa Document multi-game YAMLs (#619) 2022-06-08 18:15:47 -04:00
Chris Wilson
df0ae205cd Update LICENSE files for WebHost assets (#616) 2022-06-08 17:17:50 -04:00
Fabian Dill
1cbd384569 Generate: sort input files, preventing arbitrary order from OS layer. 2022-06-08 00:36:13 +02:00
Fabian Dill
e47527087e WebHost: some updates (#603)
* WebHost: Make custom server prefer ipv4 for display

* WebHost: Make server retry saving in case of connection issues

* WebHost: fix autolaunch guardians getting stuck waiting for the oldest two rooms.
Probably not related to the issues of the system itself getting stuck, but should be fixed anyway.

* WebHost: logfile is meant to be guarded by access cookie

* WebHost: set patch target to null if port is not valid, disabling auto-connect
2022-06-08 00:35:35 +02:00
Fabian Dill
517a2db9d8 Clients: some improvements (#602)
* Clients: some improvements
SNIClient is the only client that uses slow_mode, so its definition should be moved there.
type info for CommandProcessor was int for some reason.
Moved a lot of type info from init to class body, making it easier for type checkers to find.
getLogger("") and getLogger(None) is technically different, just happens that our root logger is "", fixed it in case of future confusion though.

* Logging: log that init_logging was run and what the current AP version is.
2022-06-08 00:34:45 +02:00
black-sliver
fbf993566d Clients: UX improvements (#615) 2022-06-07 00:15:08 +02:00
black-sliver
25bea47872 Appimage: include libssl (#613) 2022-06-05 22:52:16 +02:00
black-sliver
78f22e895e requirements: update cx-Freeze, fix compatibility
this conflicts with and replaces commit #f9b12b51080c7bbbf3d52c79453ac6c8222a03c5
2022-06-04 21:12:45 +02:00
black-sliver
fa3925cd74 Ui: add open_filename helper
* native look & feel on Linux (Gnome and KDE)
* falls back to tkinter
2022-06-04 21:12:45 +02:00
black-sliver
d9418d5ce1 Core: move is_linux, _macos, _windows to Utils.py 2022-06-04 21:12:45 +02:00
black-sliver
103f9e0b85 UI: add Utils.messagebox
automatically uses either new kvui.MessageBox or tkinter.messagebox
2022-06-04 21:12:45 +02:00
black-sliver
a2fc3d5b71 AppImage: better compatibility
* old startup script did not work with dash
* add missing libcrypt in cx_freeze
2022-06-04 21:12:45 +02:00
Kono Tyran
c66d64b9d8 update minecraft_en.md wording slightly and minecraft version 2022-06-04 11:32:51 -07:00
TheCondor07
0dd67f40ba SC2: UI update, Relegate No Build Option, and Filler Item Update (#606) 2022-06-03 20:18:36 +02:00
Fabian Dill
f5dc39ddf0 kvui: fix warning about "X missing in __all__" when importing from kivy.base instead of correct module 2022-06-03 07:57:57 -07:00
t3hf1gm3nt
6b47776b11 TS: Add region names to location names, and other location name clarifications (#570)
* Add region names to location names, and other location name clarification changes
2022-06-03 12:27:02 +02:00
strotlog
2b73c7f9e4 config: Use valid default enemizer_path on Linux (and Windows) 2022-06-02 02:15:05 +02:00
Fabian Dill
4558ac66fa SNIClient: run adjuster for new aplttp file type 2022-06-01 08:30:28 -07:00
Fabian Dill
d0a98949f5 LttP: split Retro into Retro Bows and Retro Caves (#588) 2022-06-01 08:29:21 -07:00
Fabian Dill
e13e7f286c Tracker: fix ItemLinks items not being attributed to inventory 2022-06-01 08:28:16 -07:00
Fabian Dill
0045e3f9f7 WebHost: update flask-caching 2022-06-01 08:26:30 -07:00
Fabian Dill
ff608b72a2 Tests: add test to check for typo'd item name group definitions (#594)
* Tests: add test to check for typo'd item name group definitions
Factorio: item *name* group was pointing to IDs instead.
Server: prevent crash when using Event-filled item name group

* Server: prevent crash when /hint'ing for an item name group with events
2022-06-01 08:25:40 -07:00
Fabian Dill
19c3c8056b Server: remove compat to ~0.2 unversioned save data
If the savegame was loaded in the last few months, it will have already been upgraded.
2022-06-01 08:21:54 -07:00
black-sliver
d31c24bbf7 Doc: deprecate datapackage_version 2022-05-30 09:52:12 +02:00
lordlou
768f9497fd Sm remote item fix (#592) 2022-05-30 07:12:01 +02:00
TheCondor07
20be691f36 SC2: GUI Mission Launcher (#586)
* SC2: Functioning Starcraft 2 Mission Launcher UI

* AutoWorld: add .__file__ attribute to AutoWorlds
This tries to help with a recurring easy to make mistake, where ./worlds/myworld does not exist in frozen form and is instead ./lib/worlds/myworld

* SC2: get .kv file path correctly when frozen too

Co-authored-by: TheCondor07 <TheCondorian07@gmail.com>
Co-authored-by: Fabian Dill <fabian.dill@web.de>
2022-05-30 07:11:01 +02:00
Berserker66
3dd3f045e6 WebHost: use non-blocking file lock on unix, just like windows 2022-05-29 08:00:28 -07:00
black-sliver
6d3538a35b AppImage: fix build (#589)
* CI: build: use ARCH= for AppImage

* WebHost: pin flask-caching

until https://github.com/pallets-eco/flask-caching/pull/352 is merged or fixed otherwise
2022-05-28 23:20:46 +02:00
Fabian Dill
1a0bfecb5f LttP: convert vendors hint into separate scams option 2022-05-28 20:08:06 +02:00
Felix R
5d3b4c8efd Meritous: Minor logic change (#584) 2022-05-28 00:52:14 +02:00
TheCondor07
8adc0dd7eb SC2: Fixed issue in random mission order with some missions being available too early 2022-05-27 20:53:06 +02:00
Jarno Westhof
2cb71c5352 [Timespinner] Removed backwarp from refugee camp to library from logic 2022-05-27 20:51:29 +02:00
TheCondor07
b6068f4519 SC2: Updated webhost details page 2022-05-27 18:32:33 +02:00
Fabian Dill
21a6b0143d MC: fix Bee Trap name 2022-05-26 20:49:24 -07:00
Fabian Dill
28949853f7 Setup: "ParseVersion" gives Deprecated Warning, fixing the warning. 2022-05-26 20:17:44 -07:00
Fabian Dill
65c83393bb SC2: fix copy pasta in client 2022-05-26 20:11:46 -07:00
Fabian Dill
960988ddcd WebHost: undo autoconnect link as not all browsers behave like Vivaldi. (#577)
* WebHost: undo autoconnect link as not all browsers behave like Vivaldi.

* Increase tooltip z-index

Co-authored-by: Chris Wilson <chris@legendserver.info>
2022-05-26 21:13:49 -04:00
Fabian Dill
fb99dca83e WebHost: update waitress and bokeh (#575) 2022-05-26 20:58:48 -04:00
TheCondor07
e786243738 SC2: Option for random mission order (#569) 2022-05-26 19:28:10 +02:00
espeon65536
cec0e2cbfb OoT Client: deathlink toggle 2022-05-26 19:26:07 +02:00
espeon65536
dadd7d4693 OoT: big poe count option returns 2022-05-26 19:26:07 +02:00
espeon65536
dc558f906c OoT: lua script reads MQ dungeon address dynamically from autotracker context
finally I can stop updating this every version
2022-05-26 19:26:07 +02:00
espeon65536
8184e99409 OoT: add version check to lua script + client 2022-05-26 19:26:07 +02:00
espeon65536
ac87629550 OoT: write data into autotracking context
useful for the client and autotrackers to gather data easily
2022-05-26 19:26:07 +02:00
espeon65536
1c231b703a OoT: trap display rework
Traps from all games now disguise themselves as OoT items
Traps all display "[Player] is a FOOL!" when picked up
2022-05-26 19:26:07 +02:00
espeon65536
a66b11e6ec OoT: remove warning message during multidata manipulation 2022-05-26 19:26:07 +02:00
espeon65536
4f24c4ea78 OoT: write double-ended shuffled entrances to spoiler log more clearly 2022-05-26 19:26:07 +02:00
Fabian Dill
a800b148a2 Clients: allow "&[]" in tooltips, as kivy-escaped characters and fix similar translate issues in copy-paste clipboard 2022-05-26 07:46:23 -07:00
espeon65536
1710e15e49 MC: Bee Trap is renamed and trap 2022-05-26 07:45:14 -07:00
N00byKing
a332d4935d v6,sm64ex: Use standard Death Link option name 2022-05-26 07:05:19 +02:00
lordlou
9b855c7de0 Sm various fixes (#518) 2022-05-25 08:50:32 +02:00
Fabian Dill
e8be80ccd7 Network: remove "SlotAlreadyTaken" from docs and clients, as it was removed from the server in 0.2 2022-05-24 19:16:53 -07:00
Zach Parks
c661da57d8 add tooltip for Plando Options on Generate page (#563) 2022-05-23 19:17:41 -04:00
Fabian Dill
4165f58414 Clients: now featuring tooltips and some general cleanup (#564)
* Clients: now featuring tooltips and some general cleanup

* Clients: fade in tooltip over 0.25 seconds

* Clients: reset slot and team when disconnecting

* Clients: allow joining multiworld via link (TextClient only for now)
2022-05-23 15:20:02 -07:00
TheCondor07
7126b7bca0 SC2: Launch game in fullscreen mode. 2022-05-23 17:05:55 +02:00
CaitSith2
a7f647e3ca Block collection of Sahasrahlah. (#562) 2022-05-22 18:02:33 -07:00
Fabian Dill
e901a87afd LttP: fix adjuster partial settings store crash 2022-05-22 15:07:12 -07:00
Fabian Dill
9eb237b3af Clients: some cleanup 2022-05-22 04:49:55 -07:00
Fabian Dill
909ea9dc99 WebHost: fix plando options type error 2022-05-22 04:44:26 -07:00
Fabian Dill
86013328d6 Factorio: fix crude-oil related crashes (#552) 2022-05-21 20:57:26 +02:00
jtoyoda
0c80cd017f Adding in error message for FF1 if player name is empty in the ROM 2022-05-21 20:52:58 +02:00
TheCondor07
2b8a0f8cd8 SC2: Better set-up instructs and a section for those having issues 2022-05-21 20:52:37 +02:00
Alchav
e1926c973e [SC2] Item name groups and item game name fix (#555) 2022-05-21 20:52:00 +02:00
Chris Wilson
f515f680a4 ArchipIDLE is only visible during April 2022-05-21 20:51:24 +02:00
Fabian Dill
effba9fdec Factorio: fix crude-oil having no requirements at all 2022-05-21 02:49:20 +02:00
Fabian Dill
388f064307 SC2: fix typo in AllInMap Choice 2022-05-20 17:49:05 -07:00
TheCondor07
bb15485965 SC2: Quality of Life Changes/Fixes to Prepare For Future Feature (#550) 2022-05-21 02:47:16 +02:00
CaitSith2
cb9db5dff1 Verify start location hint 2022-05-20 18:42:36 +02:00
TheCondor07
3b644a0af1 SC2: Changed All In to require either previous mission instead of both 2022-05-20 17:06:12 +02:00
Fabian Dill
8ce2ecfaac SC2: more cleanup and fix setup compile 2022-05-19 19:18:12 -07:00
Fabian Dill
bdd9ca76ee WebHost: fix title (#544)
This is pretty simple. Approved.
2022-05-19 21:26:23 -04:00
Fabian Dill
44ae50083d SC2: setup fix link 2022-05-20 01:45:00 +02:00
Fabian Dill
e5d999c755 SC2: prevent freeze when X-ing out the window 2022-05-19 09:19:42 -07:00
espeon65536
4e90ebc7d9 MC: add 1.18.2 advancements (#537)
* MC: add 1.18.2 advancements and update options to match

* client version 8

* MC: multiworkd -> multiworld

* MC: account for overworld villager in Star Trader logic
Also standardized Surge Protector and VVFrightening logic

* MC: fix _mc_overworld_villager
some day I won't second-guess myself when writing logic
2022-05-19 09:15:23 -07:00
Alchav
dbf0458575 Implement get_filler_item_name for various games (#451) 2022-05-19 15:37:26 +02:00
TheCondor07
e6e44b8747 SC2: Updated /available and /unfinished to better handle collects 2022-05-19 05:34:46 +02:00
weffjebster
2b702528fd [Timespinner]HP cap setting (#536) 2022-05-19 05:25:08 +02:00
Colin Lenzen
23144ff204 [Timespinner] Add Show Item Drops in Bestiary 2022-05-19 05:24:31 +02:00
TheCondor07
764b6c78c5 SC2: Turned weaker upgrades into trash items 2022-05-19 05:23:57 +02:00
Fabian Dill
051e19e9c1 Core: tkinter import may only be needed for type-info and can be skipped in certain cases for speed of startup 2022-05-19 05:23:02 +02:00
Fabian Dill
ad99850192 SC2: some cleanup (#532)
* SC2: some cleanup

* SC2: some cleanup in client
2022-05-18 18:03:33 -07:00
alwaysintreble
c93eeb3607 tests: implement test to check for game_info file (#531) 2022-05-19 00:08:29 +02:00
TheCondor07
551cf8442f Starcraft 2 Wings of Liberty AP Implementation (#528) 2022-05-18 23:27:38 +02:00
Fabian Dill
90d506ee7c Fill: fix type-crash on unfilled having either str or Location
Fill: speed up Counter creation by skipping intermediary list creation
2022-05-18 22:40:40 +02:00
alwaysintreble
45bca78e75 docs: add tutorials to api documentation 2022-05-18 21:29:59 +02:00
alwaysintreble
11faca1940 docs: update various broken links/images and fix a few small typos. point some links to current webhost server rather than hardcoding archipelago.gg 2022-05-18 21:29:59 +02:00
Fabian Dill
47b179dec4 setup: utf-8-sig signing 2022-05-18 11:57:10 -07:00
PoryGone
05efbe0af8 SA2B Style Improvements (#525) 2022-05-18 14:56:43 +02:00
alwaysintreble
48a7587c5a Fix broken plando guide links 2022-05-18 14:55:53 +02:00
metzner
ff82145633 The Witness: Updated Setup Guide, now referencing the PopTracker map- & auto-tracking package! 2022-05-17 16:09:41 +02:00
wafflesoup
dcc703f454 Webworld docs: Removed extra space 2022-05-17 16:09:16 +02:00
Jarno Westhof
07f66fb15a [Timespinner] Make DamageRandoOverrides a bit easier to work with and compatible with older yamls (#517) 2022-05-15 14:39:38 -07:00
CaitSith2
c0fb7d9f9a Add local and non_local items to item_links (#506)
* Add local and non_local items to item_links

* Whoops, don't pass list of list to verify_items.

* Give a did you mean result in the exception.
2022-05-15 07:41:11 -07:00
beauxq
2b6fc6dd3a only accept true and false for a range if they make sense 2022-05-15 16:31:26 +02:00
lordlou
e147495fb9 Sm unbeatable seed fix (#514) 2022-05-15 16:29:56 +02:00
Fabian Dill
b2e65a19a2 Webhost serialize fixes (#512)
* Main: compress world type output log

* WebHost: ensure plando_options is serializable to json
2022-05-14 14:05:21 -07:00
Fabian Dill
44638ccc1a Fill: fix priority_locations being undone by prog_balancing shop shuffle and other late-fills (#513) 2022-05-14 14:04:16 -07:00
Fabian Dill
5f4b2cfa52 Main: compress world type output log (#509) 2022-05-14 11:52:57 -07:00
jtoyoda
0bc2301530 Updating docs to remove reference to the AP preset 2022-05-14 19:50:25 +02:00
Fabian Dill
d1eda38745 Clients: centralize UI and input behaviour 2022-05-14 12:01:11 +02:00
PoryGone
dc10421531 Sonic Adventure 2: Battle Implementation (#501) 2022-05-14 12:00:49 +02:00
black-sliver
00f5975a3c CI: build release AppImage on ubuntu-18.04
Change to oldest available container to maximize compatibility
2022-05-14 11:56:13 +02:00
metzner
b41f444013 Logic Fix (Potentially gamebreaking) 2022-05-14 11:55:48 +02:00
CaitSith2
89b4060a06 Fix Plando options pickling error 2022-05-14 11:53:37 +02:00
CaitSith2
98ca001da6 Fix for variable progression balancing when yaml has progression on/off. 2022-05-14 11:52:57 +02:00
weffjebster
b0b41711d4 Adding damage rando v2 options to timespinner rando (#503) 2022-05-14 11:52:35 +02:00
CaitSith2
3f691d6977 Add "exclude" to item links (#497) 2022-05-11 16:37:18 -07:00
alwaysintreble
977159e572 Webworld docs: move gameinfo documentation to their world folders and copy them for webhost use. (#455) 2022-05-11 20:05:53 +02:00
black-sliver
9e15e754c2 Doc: use RegionType.Generic in api.md 2022-05-11 11:53:57 +02:00
Doug Hoskisson
c085ee47ed variable-progression-balancing (#356) 2022-05-11 09:13:21 +02:00
Fabian Dill
a5ca118bbf Test: rename (#499) 2022-05-10 23:51:18 -07:00
KonoTyran
521122fd4f Minecraft Version support (#458)
* add support for other java/forge versions

* fix fetching correct mod for specified version.

* add support for other java/forge versions

* fix fetching correct mod for specified version.

* convert MinecraftClient.py to read forge versions from Randomizer Mod Repo.

* add minecraft_versions.json to gitignore.

* remove redundant json import

* update host to release.
add forge checking,
fixed duplicated code due to merge.

* clerify that beta channel will most likely make games no longer playable on release channel

* convert commetns to docstrings.
2022-05-10 21:00:53 -07:00
Fabian Dill
86933d8150 LttP: ensure non-native items are rendered as star in Shops (#486)
* LttP: ensure non-native items are rendered as star in Shops

* LttP: ensure non-native items are rendered as star in Shops - fix missing player number lookup
2022-05-10 20:41:44 -07:00
Alchav
976f34c19f Fix Harmless Hellway logic
Original logic from SMZ3 is:

items.KeyPD >= (GetLocation("Palace of Darkness - Harmless Hellway").ItemIs(KeyPD, World) ?
                        (items.Hammer && items.Bow && items.Lamp) || config.Keysanity ? 4 : 3 :
                        (items.Hammer && items.Bow && items.Lamp) || config.Keysanity ? 6 : 5))

I believe these parentheses are needed to correctly replicate this logic
2022-05-11 04:20:30 +02:00
Fabian Dill
a56340663c Test: check that all_state can complete game 2022-05-10 19:20:15 -07:00
Fabian Dill
e3900e9f99 Test: fix wrong name 2022-05-10 19:20:15 -07:00
Fabian Dill
e8b1362172 Test: check for working completion condition 2022-05-10 19:20:15 -07:00
espeon65536
f6d857b5b5 Core: make progression balancing deterministic (#295) 2022-05-11 04:12:26 +02:00
Fabian Dill
aa9f43dea1 Fuzzy: switch to damerau_levenshtein_distance with ignored case 2022-05-10 19:09:07 -07:00
Fabian Dill
513ab62ce7 Fuzzy: replace thefuzz with jellyfish
GPL -> BSD2Clause and should be faster though I haven't tested it myself and just trusted people on the internet.
Jellyfish also allows us access to many more algorithms should they be any better. Trying out Jaro distance now instead of Levenshtein.
2022-05-10 19:09:07 -07:00
black-sliver
a020dea277 Doc: fix wrong naming in api.md example code 2022-05-10 16:52:41 +02:00
espeon65536
19dd447dcb OoT: update connector.lua to use new names 2022-05-10 05:50:39 -07:00
Kono Tyran
eb1abd9222 Fix broken image link in Factorio setup_en.md 2022-05-09 11:12:05 -07:00
NewSoupVi
9ab7c8d9e5 Witness: Changes in response to Beta run 1 (#494)
Co-authored-by: metzner <unconfigured@null.spigotmc.org>
2022-05-09 07:20:28 +02:00
Hussein Farran
1e592b4681 Update network protocol doc to extend intra-doc linking (#489) 2022-05-06 10:01:43 -04:00
espeon65536
40a08d0d84 SM64 logic fixes and ER handling (#488)
* SM64: add painting name to location hints if area randomizer

* SM64: fix BitFS access logic
Using can_reach regions in an entrance's logic is unsafe because reachable_regions won't be updated if no progression locations are reached. can_reach location is safe.

* SM64: rework logic for correctness and consistency
- BoB Mario Wings to the Sky is extremely difficult with cap and no cannon, will never be required
- DDD Collect the Caps no longer requires metal cap except on strict cap
- Cavern of the Metal Cap red coins no longer requires metal cap except on strict cap
- CCM, TTM, WDW cannons added on strict cannons for their expected stars
- BoB 100 coins requires cap or cannon if both are strict, since only 99 coins are available otherwise

* SM64: write entrances to spoiler log

* SM64: tweak format of WDW cannon rules
2022-05-06 13:33:39 +02:00
CaitSith2
517e72f442 Add options to generate page (#450)
* Add Item cheat permission to generate page.

* Indicate that both remaining_mode and item cheat are disabled in race mode.

* Add server_password

* refine tooltips and help for server_password and !admin command.

* Add Plando options to generation page.

* Remove debugging code

* Style adjustments and HTML formatting and tag fixes with the goal of making the page nicer looking and not as vertical.

Co-authored-by: Chris Wilson <chris@legendserver.info>
2022-05-04 20:03:19 -07:00
black-sliver
ea51df432d Factorio: map gen: allow arbitrary property expressions
Can be used to override tile generation; we don't want to define all of them
2022-05-05 02:34:11 +02:00
black-sliver
c27bfc515e Factorio: map gen: allow width and height
Don't accept arbitrary keys to catch typos.
This should be all 'basic' map gen settings now.
2022-05-05 02:34:11 +02:00
Fabian Dill
7fad0b0f51 Test: introduce test for every game has a tutorial (#478) 2022-05-03 22:14:03 +02:00
Fabian Dill
76663f819b Merge pull request #483 from espeon65536/oot
Ocarina of Time: V6.2 updates
2022-05-02 11:54:42 +02:00
Fabian Dill
666760f0cf Merge branch 'main' into oot 2022-05-02 11:54:00 +02:00
espeon65536
2d73f2f46e OoT: update mq dungeon table address in lua script
Plan is to automate this next version, so the lua doesn't need updating every time I mess with the ROM
2022-05-01 14:48:33 -05:00
espeon65536
c8e54bbcd0 OoT: add Thieves' Hideout keys/locations to tracker 2022-05-01 14:44:49 -05:00
espeon65536
76a4dce66a OoT: move Thieves' Hideout location IDs to match with old ID 2022-05-01 14:44:26 -05:00
espeon65536
c102d602b3 OoT: update ASM to version 6.2 2022-05-01 13:27:53 -05:00
espeon65536
e711490f6c OoT: bump data version 2022-05-01 13:07:15 -05:00
espeon65536
c801cdbb3b OoT: update logic files, naming, and logic tricks to version 6.2
Gerudo Training Grounds -> Ground
Composers Grave -> Royal Familys Tomb
Gerudo Fortress -> Thieves Hideout for the indoor sections
2022-05-01 13:05:52 -05:00
metzner
9d638671bb Removed Tutorial Gate Close as a location for compatibility with current randomizer version 2022-04-30 15:10:50 -07:00
metzner
4a703481ba Removed Mountain Trap Door Triple Exit location. 2022-04-30 15:10:50 -07:00
metzner
897cbb9826 Moved Quarry Big Panel to uncommon 2022-04-30 15:10:50 -07:00
metzner
bb710cc360 Fix: Traps weren't showing up 2022-04-30 15:10:50 -07:00
Fabian Dill
5eab07d8d6 Network: add games argument to GetDataPackage (#473) 2022-04-30 04:39:08 +02:00
espeon65536
894a30b9bd Check for ROMs at beginning of generation (#475) 2022-04-30 03:37:28 +02:00
Fabian Dill
e8579771a5 Requirements: update websockets 2022-04-29 17:52:41 -07:00
Fabian Dill
09670a4475 Factorio: demote EnergyLink text to debug logging level. 2022-04-29 16:56:54 -07:00
Fabian Dill
ff783cf9a5 WebHost: update flask 2022-04-29 16:54:42 -07:00
beauxq
46d31c3ee3 typing, mostly in AutoWorld.py
includes a bugfix (that was found by static type checking)
in `get_filler_item_name`
2022-04-29 03:00:39 +02:00
NewSoupVi
3e8c821c02 Add The Witness (#467)
* Added The Witness


Co-authored-by: metzner <unconfigured@null.spigotmc.org>
Co-authored-by: Jarno Westhof <jarnowesthof@gmail.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2022-04-29 00:42:11 +02:00
Alchav
50eaf712a9 Remove outdated disclaimer 2022-04-29 00:29:21 +02:00
espeon65536
f476747ade OoT: remove early ROM check
Will be replaced with an Autoworld class method, can_generate
2022-04-28 09:44:53 -05:00
espeon65536
d8d881085f OoT: permit dungeon_items: overworld to fill into shops 2022-04-27 21:45:31 -05:00
espeon65536
fd6e1b3046 OoT: fix bad interaction between dungeon_items: overworld and songs: dungeon 2022-04-27 21:43:16 -05:00
espeon65536
d6697924cb OoT: item links don't crash
still point to not-helpful locations though
2022-04-27 21:11:04 -05:00
espeon65536
3001926ae4 OoT: fix locations pointing to wrong entrance in server hints 2022-04-27 20:12:32 -05:00
Doug Hoskisson
578451fcfa add some typing info to CollectionState (#468) 2022-04-27 21:19:53 +02:00
espeon65536
d57bdf6dc3 OoT: No Logic modifications
NL now uses the glitchless world graph, which enables entrance randomizer
NL forces all logic tricks on, progression balancing off, minimal accessibility
2022-04-26 15:16:02 -05:00
espeon65536
0309fac592 OoT: check for existence of ROM at start of generation 2022-04-26 13:43:02 -05:00
Fabian Dill
9ecd320c8c OoT: prevent connection from outdated clients 2022-04-26 07:40:01 -07:00
CaitSith2
c326566bd2 Show "did you mean 'item/location_name'" in invalid item/location error. (#469) 2022-04-26 02:28:43 -07:00
black-sliver
4f10dbb896 Test: add missing cleanup in TestGenerate
fixes a warning on some systems
2022-04-24 19:32:08 +02:00
N00byKing
cb6d377796 sm64ex: Rule updates 2022-04-24 08:29:26 -07:00
Jarno Westhof
b5f58b0a03 Fixed copy paste issue 2022-04-24 08:28:14 -07:00
N00byKing
9ee5fae476 sm64ex: Update dependency in documentation 2022-04-24 08:27:31 -07:00
Hussein Farran
81feb2fd5e [Docs] Update network diagram into mermaid diagram syntax. (#446) 2022-04-24 11:20:14 -04:00
Colin Lenzen
75a76fb184 Include options in options dict 2022-04-24 05:01:53 +02:00
Colin Lenzen
21f1ccbfb4 Timespinner: Options to Support Loot Randomization 2022-04-24 05:01:53 +02:00
Fabian Dill
0f5a7cda6c LttP: fix retro allowing arrows in "P" price shuffle in shops (#448) 2022-04-22 09:12:51 +02:00
Fabian Dill
acd7bce903 Logging: change text loggers to log current time 2022-04-22 09:11:50 +02:00
Chris Wilson
1afacd28a1 Fix chart indent 2022-04-20 14:30:36 -07:00
Chris Wilson
6e171d19f0 Remove no longer needed control data 2022-04-20 14:30:36 -07:00
Chris Wilson
66921499ad Display multiple charts per row, reduce overall chart size 2022-04-20 14:30:36 -07:00
Fabian Dill
249972c7fd webhost: stats improvements 2022-04-20 14:30:36 -07:00
Fabian Dill
dae0e233b8 WebHost: add a /stats page 2022-04-20 14:30:36 -07:00
CaitSith2
8bb566a250 Fix remaining_mode on webhost. (#449)
* Fix remaining_mode on webhost.

* Actually use the correct parameter for remaining_mode.
2022-04-20 10:46:05 -04:00
Rob McAuley
6a25bbeef0 Fix other instances of /tutorial/archipelago 2022-04-17 15:54:52 +02:00
Rob McAuley
6286ac4a3b Fix lowercase letter in link leading to 404 2022-04-17 15:54:52 +02:00
Vince Lund
447f99ea15 Documentation: Added example of item_links 2022-04-17 15:53:57 +02:00
N00byKing
587d4dc8b6 v6,sm64ex: Allow location exclusions 2022-04-15 02:02:39 +02:00
black-sliver
b5613ffcf5 OoT: mark Compress/Decompress as executables 2022-04-13 23:34:44 +02:00
KonoTyran
1fe82b1312 Add bug report link to WebWorld (#440)
* Add bug report link to WebWorld

* change bug_report_page to an optional
reword bug report link text.

* update Minecraft bug report page to a template.

* change wording of link.

* add `bug_report_page` documentation to api.md
2022-04-12 17:37:05 -04:00
Fabian Dill
a4daa78c0b HK: plando charm cost (#431)
* HK: Charm costs in spoiler log now with charm name.

* HK: Allow Plando Charm costs

* HK: skip unnecessary checks
https://github.com/ArchipelagoMW/Archipelago/pull/431#discussion_r847804916
2022-04-12 11:13:52 -04:00
Jarno Westhof
618bdfc917 [Core] Allow multiple worlds in one yaml (#428) 2022-04-12 10:57:29 +02:00
CaitSith2
8e68aa0ccd Add group collect (#424)
* Add group collect

* code cleanup
2022-04-10 14:08:54 -07:00
Fabian Dill
df3757657e Setup: fix SMZ3 and SoE file bindings 2022-04-10 10:03:24 -07:00
Vince Lund
0eea1a1d89 Timespinner: Added Lore names to Downloads 2022-04-10 09:28:33 +02:00
Fabian Dill
15dcdca6fc HK: slight optimization
items are marked as advancement if they have an additional effect, so instead of a lookup we can just refer to a bool that's already local as a quick pre-check
2022-04-08 21:53:30 +02:00
Fabian Dill
7a6aef03e7 HK: Charm costs in spoiler log now with charm name. 2022-04-08 21:53:17 +02:00
Fabian Dill
c61f3b9110 MC: make slot data json compatible
(Changing base type of Options in recent PR broke this)
2022-04-08 21:37:08 +02:00
black-sliver
42fecc7491 Core: change how required versions work, deprecate IgnoreGame (#426)
`AutoWorld.World`s can set required_server_version and required_client_version properties. Drop `get_required_client_version()`.
`MultiServer` will set an absolute minimum client version based on its capability (protocol level).
`IgnoreVersion` tag is replaced by using `Tracker` or `TextOnly` with empty or null `game`.
Ignoring game will also ignore game's required_client_version (and fall back to server capability).
2022-04-08 11:16:36 +02:00
Doug Hoskisson
0acca6dd64 Options.py typing (#412)
* Options.py typing
use NumericOption class inheriting from numbers.Integral instead of int
also can sometimes take text like:
"high": high end of range
"low": low end of range
"true", "on": default if it exists, otherwise high end of range
"false", "off": zero if zero is the low end

* just low, high, and default for range text

Co-authored-by: Doug Hoskisson <doughoskisson@novuslabs.com>
2022-04-07 13:42:30 -04:00
Fabian Dill
ec00d1b710 SMZ3: allow TextClient to connect by name (#423) 2022-04-07 10:50:55 -04:00
Fabian Dill
f093e90c23 ModuleUpdate: add it to a few more common entry points
MinecraftClient: add requests import to requirements.txt
2022-04-07 15:21:47 +02:00
black-sliver
3d1f6d9b82 Clients: don't use stdin when loading steam overlay 2022-04-07 12:28:00 +02:00
CaitSith2
9bdcbb9008 Fix item links. 2022-04-07 10:22:17 +02:00
Fabian Dill
491e6c8730 HK: don't progression balance "Currency"-like progression items (#419)
* HK: don't progression balance "Currency"-like progression items

* only skip prog balancing on charms that don't unlock checks by themselves

Co-authored-by: Kono Tyran <HAklowner@gmail.com>
2022-04-05 18:41:15 -04:00
Fabian Dill
d32d268d97 WebHost: add yaml checker to sitemap and drop "mystery", as we've been doing in various places (#421) 2022-04-05 15:17:47 +02:00
Fabian Dill
30c447b9f3 Lttp adjuster (#417)
* LttP: Allow running Adjuster with positional arg rom (windows -> open with)

* LttP: use "proper" logging in adjuster and load baserom from local directory if not found.
2022-04-05 09:16:06 -04:00
Fabian Dill
2def8f35ad KH: what? yeah, it's HK (#420)
* KH: what? yeah, it's HK
someone this hadn't been spotted yet.

* KH: also fix the start AST Node, just in case we add those in at some point (currently they resolve to True/False anyway)
2022-04-05 09:01:33 -04:00
Chris Wilson
f2055daf1a Add a /sitemap to the WebHost (#418) 2022-04-05 07:14:30 +02:00
CaitSith2
944571ea89 LttP: Add Allow collect option, default Off. (#414)
* LttP: Add Allow collect option, default Off.

* Add allow_collect to the sample yaml.
2022-04-05 03:54:49 +02:00
Fabian Dill
f7c601b863 HK: Fix web gen
By allowing pickle to find the options
2022-04-05 02:35:55 +02:00
Fabian Dill
7315da2ccb AutoWorld: don't import __pycache__ 2022-04-05 02:26:58 +02:00
Fabian Dill
2f7f6a0b58 Setup: copy LttP yaml to build automatically 2022-04-05 02:26:41 +02:00
Chris Wilson
3f43051c35 [WebHost] Do not calculate settingHash multiple times in weighted-settings 2022-04-04 16:48:59 -07:00
Chris Wilson
535c35310d [WebHost] Fix a bug causing player-settings to fail to update the hash on JSON updates 2022-04-04 16:48:59 -07:00
Chris Wilson
8fbe6a4511 [WebHost] Only calculate settingHash once in player-settings 2022-04-04 16:48:59 -07:00
Chris Wilson
07ff0f1026 [WebHost] Fix /user-content styles (#408) 2022-04-03 20:16:15 -04:00
Fabian Dill
a080288e3e Core: update version (#407) 2022-04-03 19:39:01 -04:00
Fabian Dill
71bd87f293 HK: don't flag maps as progression 2022-04-03 19:38:39 -04:00
Fabian Dill
574e2abba8 HK: write shop prices to spoiler log 2022-04-03 19:38:39 -04:00
Hussein Farran
cffa772801 Fix unit test and generation failures. Whoops. 2022-04-03 19:38:39 -04:00
Fabian Dill
66bd793306 HK: add item name groups 2022-04-03 19:38:39 -04:00
Hussein Farran
0eb37883ca Add docstrings to hollow knight YAML options. 2022-04-03 19:38:39 -04:00
Hussein Farran
356384ab05 Add Hollow Knight setup guide, game info, and to README 2022-04-03 19:38:39 -04:00
Fabian Dill
8c2c6877b6 HK: sort shop contents by cost 2022-04-03 19:38:39 -04:00
Fabian Dill
d1d40d8a60 HK: ignore relics logic
HK: write sets ordered, to reduce history changes
2022-04-03 19:38:39 -04:00
Fabian Dill
b026a0a372 HK: write charm costs to spoiler 2022-04-03 19:38:39 -04:00
Fabian Dill
73bcd0058a HK: force disabled options to actually be disabled 2022-04-03 19:38:39 -04:00
Fabian Dill
0cf396e5d6 HK: account for "Start" location in another place 2022-04-03 19:38:39 -04:00
Fabian Dill
1bc09d4292 make black sliver happy 2022-04-03 19:38:39 -04:00
Fabian Dill
97d0c51db1 HK: allow webgen 2022-04-03 19:38:39 -04:00
Fabian Dill
ed1c11267c Options: loudly crash if random text is not recognized, instead of de… (#401)
* Options: loudly crash if random text is not recognized, instead of defaulting to full "random"

* Update Options.py

Co-authored-by: Hussein Farran <hmfarran@gmail.com>

Co-authored-by: Hussein Farran <hmfarran@gmail.com>
2022-04-03 19:37:57 -04:00
Fabian Dill
a3e1ac896f Generate: don't fail on marked utf-8 files (#399)
utf-8-sig will fallback to non-sig automatically
2022-04-03 15:55:46 -04:00
Zach Parks
37d9eb2752 Added filesafe player name function and updated generator functions in all worlds to use filesafe player name during output
Thanks Windows for your bad filesystem.
2022-04-03 20:45:44 +02:00
CaitSith2
05e267a0bd Prevent use of old collection clients without boss collection blocklist. (#406) 2022-04-03 14:45:06 -04:00
Fabian Dill
d1f0a29a02 OoT: fix patching deltas when run from another folder 2022-04-03 20:44:27 +02:00
Fabian Dill
fb2e780c56 LttP/SMZ3: some more file ending fixes (#393) 2022-04-03 13:42:18 -04:00
Fabian Dill
ba3257f850 ItemLinks: prevent attempts at cross-game (#402) 2022-04-03 13:09:05 -04:00
Fabian Dill
215d5e9adf AutoWorld: ensure WebWorld is instantiated, preventing an easy mistake. (#404) 2022-04-03 13:08:50 -04:00
black-sliver
5392b32d5c SoE: WebWorld theme and fix long standing bug (#397) 2022-04-03 04:48:43 +02:00
alwaysintreble
4dd0a75914 multiworld tracker: properly fix item link breaking tracker 2022-04-03 02:03:48 +02:00
CaitSith2
a2212002ae Link to the Past Block collection of bosses. (#395) 2022-04-03 01:39:28 +02:00
lordlou
91ccee3513 [SM] remote item back compat fix (#400) 2022-04-03 01:36:31 +02:00
black-sliver
2a593d5d0a CI: add windows build action
set setuptools to 60.x until the issue is resolved
change retention to 7 days
2022-04-02 04:49:42 +02:00
black-sliver
a93b3d79aa Minecraft fixes (#388) 2022-04-02 04:49:27 +02:00
black-sliver
938ab32cda CI: bigger unittest matrix 2022-04-02 00:17:53 +02:00
Jarno Westhof
6f5ab05345 [Docs] Added WebWorld Theme (#387) 2022-04-01 22:39:39 +02:00
Chris Wilson
95f8647f09 Added 50 items to ArchipIDLE (#385) 2022-04-01 10:04:42 -04:00
jonloveslegos
06c8caa3cc Fixed checksfinder client failing when getting an item before sending one, and fixed checksfinder client not appearing in the installer (#383) 2022-04-01 07:55:06 +02:00
Fabian Dill
d206a562df rename ChecksFinder folder (#380) 2022-04-01 01:17:46 -04:00
Fabian Dill
a0a290e481 Setup: fix OoT conditions for file page (#381) 2022-04-01 01:17:26 -04:00
Fabian Dill
266ff0c520 Setup: fix apparently forgotten file endings (#382)
I feel like I did this... mh.
2022-04-01 01:16:54 -04:00
Fabian Dill
931bf7da16 SMZ3: fix loading TextScript on systems that don't default to something utf-8 compatible (#384) 2022-04-01 01:14:20 -04:00
black-sliver
fe4a26d034 CI: add Generate.py tests
* allows ModuleUpdate to be run outside of local_dir
* adds windows-latest to the unittest matrix
2022-04-01 06:16:13 +02:00
Zach Parks
dca70a99ad Webhost - Update copyright year 2022-04-01 04:46:02 +02:00
Fabian Dill
1a24a73ccd add forgotten file (#378) 2022-03-31 22:14:51 -04:00
Fabian Dill
ae163319e0 More bug fixes 2022-04-01 03:54:30 +02:00
Fabian Dill
65864e273b Fix bugs 2022-04-01 03:54:30 +02:00
Fabian Dill
199b778d2b Bug Squashing 2022-04-01 03:54:30 +02:00
Fabian Dill
70e3c47120 Core: update version 2022-04-01 03:54:30 +02:00
Fabian Dill
eddc5d6524 Options: some more typing 2022-04-01 03:21:31 +02:00
strotlog
fae3068c25 Documentation: Add RetroArch emu to SNES games (#365)
* Documentation: Add RetroArch emu to SNES games

* Documentation: fix screenshot alt text
2022-03-31 18:09:13 -04:00
Fabian Dill
b9014b2a60 Setup: update cx-Freeze (#370) 2022-03-31 18:08:48 -04:00
Chris Wilson
6b07b6407c Add ArchipIDLE setup guide (#375) 2022-03-31 18:08:13 -04:00
black-sliver
a10b987f1c CI: add release action 2022-03-31 23:29:56 +02:00
Fabian Dill
1f16310797 Options: fix "toggle: random" always being True 2022-03-31 22:57:19 +02:00
Jarno Westhof
0fd59063d9 [Timespinner] Fixed typo 2022-03-31 22:16:23 +02:00
Jarno Westhof
aab477b874 Value is not actually a member of a Set package 2022-03-31 20:16:04 +02:00
black-sliver
098d939653 Generate: fix windows support (#368) 2022-03-31 08:22:01 +02:00
black-sliver
7d830362a7 Setup, Launcher, Linux Support (#359) 2022-03-31 05:08:15 +02:00
Fabian Dill
0db1660369 MultiServer: fix crash when hint_location hits a location that can exist, but did not exist in this multiworld. 2022-03-30 19:59:34 -07:00
strotlog
c471a70b35 Documentation: Fix order, title, and link to SMZ3 2022-03-31 04:57:52 +02:00
alwaysintreble
6aef6f2c11 Revert "increment data version since progression of items changed"
This reverts commit 2243686847.
2022-03-31 04:57:01 +02:00
alwaysintreble
000f0bf2f1 clean, concise method for flagging dio's as progression
Co-authored-by: Hussein Farran <hmfarran@gmail.com>
2022-03-31 04:57:01 +02:00
alwaysintreble
0f1c08b43a increment data version since progression of items changed 2022-03-31 04:57:01 +02:00
alwaysintreble
76ffb5cd53 fix event location and victory rules. 2022-03-31 04:57:01 +02:00
alwaysintreble
23d245d43c Update RoR2 logic to use event locations 2022-03-31 04:57:01 +02:00
Chris Wilson
aabc86fc01 ArchipIDLE "Improvements" (#366)
* Increase ArchipIDLE location count to 150

* Reduce location count per seed to 100. Game will now complete between 50 and 100 minutes.

* Add 50 more items to ArchipIDLE

* Update data_version to 2
2022-03-30 21:34:39 -04:00
Fabian Dill
cebd7fb545 Core: check for key-only once in sweep (#361) 2022-03-30 21:30:06 -04:00
Fabian Dill
8337689640 Options: more feedback on bad compares (#362) 2022-03-30 21:29:45 -04:00
Fabian Dill
0263130126 SM: fix Nothing type crash (#363) 2022-03-30 21:29:08 -04:00
jonloveslegos
c472d740ec fixed the checksfinder game info saying the wrong number 2022-03-29 09:19:13 +02:00
jonloveslegos
0fd244eee0 Added to the ChecksFinder docs 2022-03-29 09:19:13 +02:00
Chris Wilson
7dcb6f66da Website Style Upgrade (#353)
* [WebHost] Update WebHost to include modular themes system, remove unused and outdated assets

* Landing Page Updates

* Markdown updates, colors coming later

* Remove testing theme from FF1

* Color updates for markdown styles

* Updates to generated pages, so many updates

* [WebHost] Update WebHost to include modular themes system, remove unused and outdated assets

* Landing Page Updates

* Markdown updates, colors coming later

* Remove testing theme from FF1

* Color updates for markdown styles

* Updates to generated pages, so many updates

* Seed download page improvements

* Add styles to weighted-settings page

* Minor adjustments to styles

* Revert base theme to grass

* Add more items to ArchipIDLE

* [WebHost] Update WebHost to include modular themes system, remove unused and outdated assets

* Landing Page Updates

* Markdown updates, colors coming later

* Remove testing theme from FF1

* Color updates for markdown styles

* Updates to generated pages, so many updates

* Seed download page improvements

* [WebHost] Update WebHost to include modular themes system, remove unused and outdated assets

* Landing Page Updates

* Markdown updates, colors coming later

* Remove testing theme from FF1

* Color updates for markdown styles

* Updates to generated pages, so many updates

* Add styles to weighted-settings page

* Minor adjustments to styles

* Revert base theme to grass

* Add more items to ArchipIDLE

* Improve Archipidle item name

* [WebHost] Update background images, waiting on jungle.png, added partyTime theme

* [WebHost] Fix tab ordering on landing page, remove islands on screen scale, fix tutorial page width scaling

* [WebHost] Final touches to WebHost

* Improve get_world_theme function, add partyTime theme to ArchipIDLE WebWorld

* Remove sending_visible from AutoWorld

* AP Ocarina of Time Client (#352)

* Core: update jinja (#351)

* some typing and cleaning, mostly in Fill.py (#349)

* some typing and cleaning, mostly in Fill.py

* address missing Option types

* resolve a few TODOs discussed in pull request

* SM: Optimize a bit (#350)

* SM: Optimize a bit

* SM: init bosses only once

* New World Order (#355)

* Core: update jinja

* SM: Optimize a bit

* AutoWorld: import worlds in alphabetical order, to be predictable rather than arbitrary

Co-authored-by: Hussein Farran <hmfarran@gmail.com>

* Remove references to Z5Client in English OoT setup guide

* Prevent markdown code blocks from overflowing their container

Co-authored-by: espeon65536 <81029175+espeon65536@users.noreply.github.com>
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
Co-authored-by: Hussein Farran <hmfarran@gmail.com>
2022-03-28 20:12:17 -04:00
Fabian Dill
14956d27bd Core: don't sweep excluded locations for accessibility check, as they are forbidden from having progression anyway. (#357) 2022-03-28 20:03:57 -04:00
Fabian Dill
420be2c44f New World Order (#355)
* Core: update jinja

* SM: Optimize a bit

* AutoWorld: import worlds in alphabetical order, to be predictable rather than arbitrary

Co-authored-by: Hussein Farran <hmfarran@gmail.com>
2022-03-27 21:45:14 -04:00
Fabian Dill
3bb3a902b3 SM: Optimize a bit (#350)
* SM: Optimize a bit

* SM: init bosses only once
2022-03-27 19:50:58 -04:00
Doug Hoskisson
2b138ac940 some typing and cleaning, mostly in Fill.py (#349)
* some typing and cleaning, mostly in Fill.py

* address missing Option types

* resolve a few TODOs discussed in pull request
2022-03-27 19:47:47 -04:00
Fabian Dill
b6eeef1db6 Core: update jinja (#351) 2022-03-27 19:44:25 -04:00
espeon65536
469dda7d85 AP Ocarina of Time Client (#352) 2022-03-27 21:44:22 +02:00
Fabian Dill
3c2933d587 V6: fix area cost always referencing last area cost, instead of current index (#348)
* V6: fix area cost always referencing last area cost, instead of current index
* V6: autoformat Rules.py
* V6: correct a location name for rule application
2022-03-26 10:16:28 +01:00
Alchav
3b128c8512 SM - Option to remove empty locations (#323) 2022-03-26 07:26:55 +01:00
lordlou
fb1be7b003 [SM] min client version change (#347) 2022-03-26 02:36:13 +01:00
lordlou
e0aa52ed27 [SMZ3] player count fix (#346) 2022-03-26 02:35:55 +01:00
Fabian Dill
64ac619b46 Core: use assert correctly (#345)
Core: add some more types to State and add count() method
2022-03-25 20:12:54 -04:00
Fabian Dill
902472be32 Core: fix place_locked_item not setting location back-reference (#344) 2022-03-25 17:57:00 -04:00
Fabian Dill
cb024b00d9 Fill: don't crash before debug output in case of unfilled locations (#342) 2022-03-24 12:47:20 -04:00
Fabian Dill
75de616465 Core: remove sending_visible (#339)
* Core: remove sending_visible
Only used by Factorio and that use predates start_location_hints, which works perfectly fine for this purpose.

* Factorio: minor cleanup
2022-03-24 12:15:52 -04:00
Fabian Dill
c12d8e2f46 WebHost: remove duplicate file ending dot (#343) 2022-03-24 12:03:05 -04:00
strotlog
d8087660e6 SM: remove SNIClient read of duplicative ROM name (#340) 2022-03-24 11:40:02 -04:00
alwaysintreble
87a8e6e20c Documentation: minor updates (#320)
* documentation: add links to other guides in adding games.md

* documentation: add webworld to api.md

* documentation: point people to docs folder and discord for help with adding games

* tutorial: go a bit more in depth on downloading a template yaml

* Make Ijwu happy

* point to baseclasses.py in api.md and reformat links a bit
2022-03-24 09:21:08 -04:00
black-sliver
b599a7607d SoE: mark traps as being traps 2022-03-24 01:49:45 +01:00
black-sliver
a6b22d1f41 Doc: rewrite patch section (#336)
this gets rid of a lot of information that is not required
and somewhat adds best practice to it
2022-03-23 19:47:27 -04:00
Fabian Dill
8e59761b03 BaseClasses: more type annotations (#337) 2022-03-23 19:46:26 -04:00
Jarno Westhof
8599506497 [Docs] Datastorage (#333) 2022-03-23 22:20:55 +01:00
Fabian Dill
e4ab10fe92 MultiServer: try to import tkinter, then provide some feedback (#329)
* MultiServer: try to import tkinter, then provide some feedback

TK may not be installed alongside python on some systems, like minimal linux installations.

* specify tkinter package

Co-authored-by: Hussein Farran <hmfarran@gmail.com>

Co-authored-by: Hussein Farran <hmfarran@gmail.com>
2022-03-23 08:53:35 -04:00
Fabian Dill
171c297d1b Options: implement additional assert checking for duplicate option ID (#332)
Options: change "random" prevention to assert, so it doesn't get checked in compiled version, as it's a source-code-time issue.
2022-03-22 21:28:15 -04:00
black-sliver
5eccb0ed49 api.md: clarify get_required_client_version (#334) 2022-03-22 21:22:58 -04:00
black-sliver
f326de2686 SoE: require client 0.2.6
Require latest https://github.com/black-sliver/ap-soeclient/
currently hosted on evermizer.com/apclient.beta
2022-03-23 02:21:47 +01:00
black-sliver
2ca6b7f929 SoE: add traps and death link 2022-03-23 02:21:47 +01:00
black-sliver
79afae17e7 SoE: add item groups 2022-03-23 02:21:47 +01:00
black-sliver
cb4d9dc365 SoE: some cleanup 2022-03-23 02:21:47 +01:00
jonloveslegos
4bf8b98681 Added my game made specifically for AP, ChecksFinder (Minesweeper) (#302) 2022-03-22 23:30:10 +01:00
Fabian Dill
7f1371ec00 SNIClient: provide example full connect command when required and some pep8 (#330) 2022-03-22 14:13:04 -04:00
espeon65536
cb3db8ae16 ALttP: fix ROM crash when loading mail/shield overflow sprite in hard/expert 2022-03-22 18:59:47 +01:00
Fabian Dill
cf2e37f92d Options: sort values when displaying OptionSet (#326) 2022-03-22 10:25:34 -04:00
Fabian Dill
92319b0e31 Options: implement item name groups for item sets options (#325)
* Options: implement item name groups for item sets options

* Options: update outdated comments; verify is done by the verify mixin parent class nowadays
2022-03-21 15:49:54 -04:00
Fabian Dill
d4ff653937 Clients: change scouted locations_info to full NetworkItem (#324) 2022-03-21 10:26:38 -04:00
lordlou
7df12930ef [SM] Add support for Remote Items (#317) 2022-03-21 05:34:47 +01:00
lordlou
9ba70951d5 [SMZ3] tutorial (#322) 2022-03-20 16:12:53 +01:00
espeon65536
2d25369d06 Core: fix division by zero in case of spectator slot 2022-03-20 16:08:22 +01:00
Alchav
affcaf1c02 ItemLink - ensure no extra fillers are created (#316) 2022-03-20 16:07:51 +01:00
Fabian Dill
7e314c0d7a Multidata: don't include start inventory events in sendable items (#319) 2022-03-18 13:19:21 -04:00
Fabian Dill
1266ca314c Options: some display name renames that were missed (#318) 2022-03-18 13:17:19 -04:00
Fabian Dill
7394598aff Patch: update to version 4 (#312) 2022-03-18 04:53:09 +01:00
Felix R
b02a710bc5 Add Meritous (#278) 2022-03-18 04:30:47 +01:00
Fabian Dill
ce6966a823 WebHost: update modules (#314) 2022-03-16 08:53:11 -04:00
Alchav
689183edc0 [RL] Specify list of available classes (#262) 2022-03-16 02:31:14 +01:00
Hussein Farran
43113c7844 Merge pull request #308 from ArchipelagoMW/quick-recipe
Quick recipe
2022-03-15 13:19:34 -04:00
Hussein Farran
fb8879a919 Merge pull request #307 from ArchipelagoMW/energy-bridge
Factorio: increase cost of Energy Bridge
2022-03-15 13:18:52 -04:00
Hussein Farran
136b9f9138 Merge pull request #309 from ArchipelagoMW/update-requirements
Requirements: update modules and move bsdiff4 to be a common module
2022-03-15 13:17:17 -04:00
Hussein Farran
eea326561e Merge pull request #310 from ArchipelagoMW/lttp-client-version
LttP: update required client version as behaviour changes were introd…
2022-03-15 13:16:30 -04:00
Fabian Dill
e3781c68be Requirements: update modules and move bsdiff4 to be a common module 2022-03-15 14:17:03 +01:00
Fabian Dill
d2927dc68f LttP: update required client version as behaviour changes were introduced with location check writes to savegame 2022-03-15 14:07:32 +01:00
Fabian Dill
ca95d47127 Factorio: improve generation speed of make_quick_recipe slightly 2022-03-15 14:02:05 +01:00
Fabian Dill
a5a0c94a2c Factorio: increase cost of Energy Bridge 2022-03-15 14:01:15 +01:00
lordlou
cfa49ee757 Add SMZ3 support (#270) 2022-03-15 13:55:57 +01:00
jtoyoda
8921baecd0 Adding in support for bizhawk 2.8 2022-03-14 23:29:02 +01:00
Fabian Dill
8b78477c69 WebHost: order guides by alphabet 2022-03-14 21:30:18 +01:00
Fabian Dill
14633724f2 MultiServer: don't count groups as players in status message 2022-03-14 20:31:57 +01:00
Fabian Dill
8d3ea9c50f Factorio: write Group names to mod 2022-03-14 20:26:16 +01:00
Fabian Dill
32a58b1adb Progression Balancing: fix ItemLinks and Spectator interactions 2022-03-14 20:10:49 +01:00
Fabian Dill
f01a31ce56 Factorio: add recipe for energy bridge 2022-03-14 19:40:35 +01:00
Chris Wilson
3f69c3a2ab Merge pull request #304 from LegendaryLinux/webhost-archipidle
[WebHost] Add docblock and FAQ pages for ArchipIDLE
2022-03-13 23:52:32 -04:00
Chris Wilson
e0f3d6d0d7 [WebHost] Add docblock and FAQ pages for ArchipIDLE 2022-03-13 23:44:30 -04:00
Chris Wilson
a8f148acac Merge pull request #303 from LegendaryLinux/archipidle
Fix generation issues with ArchipIDLE
2022-03-13 23:17:48 -04:00
Chris Wilson
0c57af40dc [ArchipIDLE] Rename locations to indicate the time required to wait 2022-03-13 22:56:46 -04:00
Chris Wilson
0714be6b73 [ArchipIDLE] Prevent overwriting global item pool 2022-03-13 20:44:08 -04:00
Chris Wilson
b5ce6f0bb0 [ArchipIDLE] Fix inefficiency caused by indentation error 2022-03-13 20:42:20 -04:00
Chris Wilson
67d59067eb [ArchipIDLE] Use shuffled item_table during generation 2022-03-13 20:39:13 -04:00
Chris Wilson
f1984a103d [ArchipIDLE] Set only 20 items as progressive 2022-03-13 15:31:27 -04:00
Chris Wilson
41fd7a8a56 Fixed failing tests 2022-03-13 14:37:56 -04:00
Chris Wilson
14ac139d03 Added world for ArchipIDLE 2022-03-13 04:04:12 -04:00
Yussur Mustafa Oraji
97b1ae5ee9 v6,sm64ex: Add support for offline singeplayer seeds (#301) 2022-03-12 22:05:54 +01:00
espeon65536
15e0763ed5 Update progression balancing algorithm (#300)
* New progression balancing algo: computes based on percentage of locations available rather than absolute number of locations
2022-03-12 22:05:03 +01:00
CaitSith2
3ce5d14210 changes
* Fix bug in overworld collected item checks.
* Don't mark checks as checked on the same cycle that its written just in case write fails for some reason. It will be later confirmed by a successful read of the newly written value on a future cycle.
2022-03-07 17:32:28 -08:00
CaitSith2
2c884e2ca5 Mark LttP items as collected in game if item is not owned by player. 2022-03-07 14:10:07 -08:00
CaitSith2
c204fb9b14 Fix LocationInfo packet handling. 2022-03-07 11:21:29 -08:00
Fabian Dill
69721d2d04 MultiServer: remove no longer needed value check from Set packet 2022-03-04 22:48:27 +01:00
Fabian Dill
73b14d3826 Factorio: rename "data" to "keys" to make EnergyLink work 2022-03-04 21:41:07 +01:00
Fabian Dill
7ca6f24e6c MultiServer: allow multiple, ordered operations
MultiServer: rename "data" on Get, Retrieved and SetNotify to "keys"
MultiServer: add some more operators
SniClient: some pep8 cleanup
2022-03-04 21:36:18 +01:00
lordlou
2c3e3f0d43 Sm/slot data (#299) 2022-03-02 19:41:03 +01:00
Alchav
3b68c6902c Save game options with server save data (#294) 2022-03-02 00:39:58 +01:00
espeon65536
c5926fcf2b OoT: rename all option displayname to display_name 2022-03-02 00:38:24 +01:00
lordlou
e6546eea85 Sm/slot data (#298)
for trackers
2022-03-02 00:37:52 +01:00
lordlou
892357cc2c Sm/item link support (#297) 2022-03-02 00:37:11 +01:00
CaitSith2
7c6fb26eb7 Filter new line characters from connect bar text input. 2022-02-28 18:25:07 -08:00
Fabian Dill
491530ad60 LttP: fix reveal bytes for Mysery Mire Prize 2022-02-24 23:43:33 +01:00
Fabian Dill
6667c1f03d Factorio: set parenthesis correctly 2022-02-24 22:50:51 +01:00
Fabian Dill
e985fc41ce Factorio: make EnergyLink an option 2022-02-24 22:40:16 +01:00
CaitSith2
508eb04e94 Tweak energy bridge values
ENERGY_INCREMENT now set dynamically by whatever the ap-energy-bridge buffer capacity ends up being.
2022-02-24 13:16:18 -08:00
Fabian Dill
68e9368bb3 EnergyLink: cleanup the second 2022-02-24 06:17:51 +01:00
CaitSith2
db152e6790 Fix deathlink killing the game watcher on startup. 2022-02-23 21:13:17 -08:00
Fabian Dill
6bf2f5611a EnergyLink: lots of cleanup 2022-02-24 04:47:01 +01:00
CaitSith2
11a13967d5 Report precisely what item link is invalid instead of ALL of them. 2022-02-23 16:21:53 -08:00
Fabian Dill
05fe423ef1 Factorio: implement EnergyLink 2022-02-24 00:51:31 +01:00
CaitSith2
6e0165986f Move duplicate name item link check to verify. 2022-02-23 15:17:24 -08:00
t3hf1gm3nt
f167e11905 Update ALttP in-game hints (#289) 2022-02-23 19:29:37 +01:00
Jarno Westhof
727cae902a [Subnautica] I guess someone had todo it 2022-02-23 19:26:17 +01:00
Fabian Dill
f38f9a47da Webhost: support groups without loading multidata on every /room request 2022-02-23 19:16:45 +01:00
CaitSith2
7708d3d157 Don't list item_link on neither trackers nor main patch download page. 2022-02-23 01:51:49 -08:00
Fabian Dill
4c64c5ad05 Spectator: fix data type 2022-02-23 04:02:11 +01:00
Fabian Dill
534ce179ec MultiServer: fix sending items_handling warning 2022-02-23 03:35:24 +01:00
espeon65536
1b73bacde1 Minecraft: add death_link attr to test world 2022-02-23 02:44:47 +01:00
espeon65536
a13ad32ec5 Minecraft: save some memory with static rules on Locations 2022-02-23 02:44:47 +01:00
espeon65536
13a6c86077 Minecraft: require bed for can_adventure if death link is on by default 2022-02-23 02:44:47 +01:00
espeon65536
5fc1b760f4 Minecraft: only add egg shards to the pool if at least 1 is required 2022-02-23 02:44:47 +01:00
jtoyoda
a6d78d9af7 Adding in the ability to disable messages in the client 2022-02-23 02:44:27 +01:00
CaitSith2
48669e96d1 Remove players from item_link pool if they don't contribute any items to the pool. 2022-02-22 16:35:41 -08:00
CaitSith2
071161176e Deny same item_link name from same player. Also report which player caused the item_link errors. 2022-02-22 16:32:37 -08:00
CaitSith2
f046d76c59 make sure starting location hints also apply to all applicable item_link players. 2022-02-22 12:49:43 -08:00
Fabian Dill
53ab224fba MultiServer: rip Store, Modify -> Set, Retrieve -> Get, Modified -> SetReply, ModifyNotify -> SetNotify 2022-02-22 12:17:21 +01:00
Fabian Dill
5faf1f27de MultiServer: add network commands Store, Retrieve, Modify and ModifyNotify 2022-02-22 11:48:08 +01:00
Fabian Dill
f38b970ea2 ItemLinks: hopefully fix remaining generation issues 2022-02-22 10:14:26 +01:00
Fabian Dill
5dbccfcbbd ItemLinks: fix all_state not collecting event locations 2022-02-22 09:49:01 +01:00
CaitSith2
de5249f99e start_hints now work for items in item_link pools. 2022-02-21 15:33:39 -08:00
CaitSith2
420320f896 Fix item_links not even rolling 2022-02-21 14:59:01 -08:00
Hussein Farran
06ac2d1805 Merge pull request #290 from N00byKing/patch-1
sm64ex: Documentation Updates
2022-02-21 11:35:14 -05:00
jtoyoda
cdc0b7a649 Fixing unit tests for FFR by excluding tests that use Default settings as FFR logic is controlled by the original randomizer 2022-02-21 00:01:27 +01:00
jtoyoda
6c7be51221 Adding in check to ensure there is at least one item in the FFR item pool 2022-02-21 00:01:27 +01:00
Fabian Dill
1159137c0d FF1: set up special settings page (remote website) 2022-02-20 21:54:00 +01:00
Fabian Dill
a98cb040b7 Core: Region type hints and some init optimization 2022-02-20 19:19:56 +01:00
Fabian Dill
170213e6d4 Core: reduce memory use of "Entrance" class
SM64: reduce count of lambda creations (memory/cpu speedup)
2022-02-20 19:10:08 +01:00
Yussur Mustafa Oraji
129c6d2d1e sm64ex: Documentation Updates 2022-02-20 12:41:16 +01:00
Fabian Dill
ad75ee8c50 Multiserver: warn about missing items_handling 2022-02-20 04:17:27 +01:00
Fabian Dill
e94b99da65 SNIClient: make address optional for multi-snes 2022-02-20 04:17:27 +01:00
CaitSith2
4f47709d32 Add entrance info to start hints. 2022-02-19 10:52:05 -08:00
Fabian Dill
71ea8d7148 Multiserver: provide compat for 0.2.3 and somewhat older multidata 2022-02-19 17:50:56 +01:00
Fabian Dill
919223cd2f Super Metroid: fix start_inventory 2022-02-19 17:43:16 +01:00
CaitSith2
fd8cace362 Reworked hints for item_link 2022-02-18 13:03:55 -08:00
Fabian Dill
18d937d83e Core: shuffle around AutoWorld imports 2022-02-18 20:29:44 +01:00
Yussur Mustafa Oraji
1d19868119 v6: Update NPC Trinket Rule 2022-02-18 19:32:36 +01:00
Fabian Dill
840e634161 update docs with NetworkSlot and create_as_hint 2022-02-18 18:54:26 +01:00
Fabian Dill
731eef8c2f bump version 2022-02-18 17:58:45 +01:00
CaitSith2
135ee018a9 update Copyright 2022-02-17 19:03:11 -08:00
Fabian Dill
7633392eea update Copyright 2022-02-17 08:21:26 +01:00
Fabian Dill
daea0f3e5e Core: provide a way to add to CollectionState init and copy
SM: use that way
OoT: use that way
2022-02-17 07:07:34 +01:00
Fabian Dill
c525c80b49 ItemLinks: move item links to events, mess up their logic in doing so and lock them behind plando option "item_links" until they're fixed. 2022-02-17 06:07:20 +01:00
N00byKing
311fb04647 sm64ex: Add option for Bob-omb Buddy Checks 2022-02-16 19:46:28 +01:00
Hussein Farran
219bd9c10e Merge pull request #285 from JarnoWesthof/add_reference_to_cpp_lib
[Docs] Added reference to the cpp lib
2022-02-16 09:01:00 -05:00
Jarno Westhof
6d704eadd7 [Docs] Added reference to the cpp lib 2022-02-16 13:05:47 +01:00
Hussein Farran
32da1993e1 Merge pull request #283 from alwaysintreble/tutorials
Tutorials: Clean up plando guide a bit; explain datapackage page. Add…
2022-02-15 15:58:03 -05:00
alwaysintreble
d4cad980e5 Tutorials: remove /api 2022-02-15 14:17:17 -06:00
Fabian Dill
53340ab22c Core: remove legacy "dynamic_regions", as all regions are now dynamic 2022-02-15 06:29:57 +01:00
alwaysintreble
2d3767a35c Tutorials: Clean up plando guide a bit; explain datapackage page. Add link to the weighted settings page in advanced tutorial. 2022-02-14 17:21:19 -06:00
Fabian Dill
aaa9bc906e WebHost: update dependencies 2022-02-14 21:37:50 +01:00
N00byKing
7503317d49 sm64ex: Add DeathLink Support 2022-02-14 16:37:49 +01:00
Fabian Dill
3fc93a33c8 WebHost: check for duplicate names
Generate: use Counter for duplicate names to make finding the dupes easier
2022-02-14 04:58:21 +01:00
Fabian Dill
d7d1d54a0b Core: generalize pre_fill item pool handling 2022-02-13 23:02:18 +01:00
Fabian Dill
34b9344084 ItemLink; correct validation to allow for None replacement item 2022-02-13 20:19:17 +01:00
espeon65536
779f3a8a61 OoT: regions are not barren if they contain never-exclude items 2022-02-12 17:29:06 +01:00
espeon65536
8c1690ef65 OoT: invert logic of previous commit 2022-02-12 17:29:06 +01:00
espeon65536
85f32d9a97 OoT: make Farore's Wind a never-exclude item if the relevant trick is off 2022-02-12 17:29:06 +01:00
espeon65536
54c7ec5873 OoT: ice traps have the trap attribute 2022-02-12 17:29:06 +01:00
espeon65536
8d260708d3 OoT: ER fixes
Don't allow beatable only to influence priority placements
Shuffle spawns after warp songs to prevent spawn points going to Desert Colossus
Prevent child spawn from priority placing at Colossus if overworld ER is off
2022-02-12 17:29:06 +01:00
espeon65536
f8009e4b84 OoT: certain ER options convert closed forest into closed deku + child start 2022-02-12 17:29:06 +01:00
Alchav
a2260ee6b2 [SM] Fix "No Energy" bugs 2022-02-12 17:28:23 +01:00
Bondo
6193eafb7b Update Text.py (#274)
Changed the Houlihan hint tile to list the winner of the SGLive 2021 tournament in similar style to alttp tournament winners.
2022-02-12 03:01:41 +01:00
black-sliver
a4eea3325f Document id range for items and locations 2022-02-12 03:00:09 +01:00
Jarno Westhof
b93e61b758 [Timnespinner] Implemented get_filler_item_name 2022-02-09 21:08:07 +01:00
Fabian Dill
14448ad97e Multidata: allow SoE/SM/LttP to connect via player name for use in Tracker/Text clients 2022-02-09 21:06:50 +01:00
Yussur Mustafa Oraji
3d17f0d588 sm64ex: Add Course Randomizer and Progressive Keys (#256) 2022-02-09 20:57:38 +01:00
CaitSith2
ee5ea09cbc Add an autofill !hint_location for clicking on a Missing: line, when user uses !missing. 2022-02-08 14:29:24 -08:00
Fabian Dill
aac8ca97ed Core: define unreachables as set 2022-02-07 00:26:44 +01:00
Fabian Dill
e4d6da47a4 LttP: fix rom writing crash because I accidentally defaulted to pep8 naming 2022-02-06 21:44:19 +01:00
Fabian Dill
9f7dbb394e LttP: convert overflow progressive items into highest-allowed-tier of non-progressive item 2022-02-06 20:11:40 +01:00
Fabian Dill
f98063b97a Options: move name verification into class methods, out of Generate.py 2022-02-06 16:37:21 +01:00
black-sliver
ed607bdc37 Fix wrong message when loading apsave
from doubling received_items that happened when moving from world-based to client-based remote_items
2022-02-06 12:28:46 +01:00
N00byKing
a3c3e4cbd4 v6: Add Area Cost Shuffle 2022-02-05 20:24:42 +01:00
ScootyPuffJr1
bffb8a034e [SM]Update Options.py (#268)
* [SM] Update Options.py
2022-02-05 20:23:17 +01:00
Fabian Dill
8242d4fe92 ItemLink: fix wrong variable use 2022-02-05 20:15:56 +01:00
Fabian Dill
279b682ac2 ItemLink: hopefully fix coop functionality 2022-02-05 17:35:12 +01:00
Fabian Dill
43ff476d98 AutoWorld: add "Everything" item_name_group to all worlds 2022-02-05 16:55:11 +01:00
Fabian Dill
28201a6c38 Core: implement first version of ItemLinks 2022-02-05 15:49:19 +01:00
N00byKing
6923800081 v6: Music Randomizer 2022-02-04 23:04:05 +01:00
Jarno Westhof
700b83572e [Timespinner] Added new shop options (#264)
* [Timespinner] Added new shop options
2022-02-04 21:53:47 +01:00
Fabian Dill
6e53cb2deb V6: some cleanup 2022-02-04 21:34:39 +01:00
Yussur Mustafa Oraji
8e04182b3f v6: Add Area Randomizer (#249)
* v6: Add Area Randomizer
2022-02-04 21:22:26 +01:00
Jarno Westhof
9fd6d1b81f [Server] Broadcast hint_cost and location_check_points update changes via RoomUpdate 2022-02-03 13:09:59 +01:00
Fabian Dill
60379d9ae6 LttP: when generating hint tiles, no longer consider Single Arrow as useful, but do consider all varieties of Bow. Additionally, don't create hints for Universal Small Keys 2022-02-03 10:41:31 +01:00
black-sliver
29ba1d4809 Doc: change displayname to display_name in api.md 2022-02-02 23:38:00 +01:00
Fabian Dill
dc4b064c73 Options: change displayname to display_name 2022-02-02 16:29:29 +01:00
Fabian Dill
0f20888563 Options: allow yaml access to Priority Locations 2022-02-01 16:36:14 +01:00
Brad Humphrey
2361f8f9d3 Use logic when placing non-excluded items 2022-02-01 16:35:18 +01:00
Chris Wilson
feba54d5d2 Fix filename for Super Mario 64 info page 2022-01-31 18:39:17 -05:00
Brad Humphrey
3cecab25c7 Add unplaced_items into the fill sweep 2022-01-31 19:17:06 +01:00
Brad Humphrey
814851ba60 Don't require every item to fill 2022-01-31 19:17:06 +01:00
Fabian Dill
6333cc3bea Server: optimize send_multiple 2022-01-31 19:05:00 +01:00
N00byKing
00bf9c569a Add send_multiple command 2022-01-31 18:56:46 +01:00
Jarno
6def1bce25 [Docs] Made LocationInfoPacket more specific 2022-01-31 18:55:20 +01:00
Jarno Westhof
3ab5c90d7c [Docs] updated description on player property of NetworkItem 2022-01-31 18:55:20 +01:00
N00byKing
0507d6923e sm64ex: Add Option to limit stars, replace with junk 2022-01-31 18:54:54 +01:00
N00byKing
e85baa8068 sm64ex: Link to release page 2022-01-31 10:57:57 +01:00
N00byKing
cbed5a0c14 sm64ex,v6: Add Note regarding spaces in arguments 2022-01-31 10:57:43 +01:00
Fabian Dill
e0628ec6c9 WebHost: correct some texts 2022-01-31 10:11:39 +01:00
Chris Wilson
82637ff072 [WebHost] Add version notice to /generate and /uploads 2022-01-30 20:06:03 -05:00
Chris Wilson
a95a18a8b5 [WebHost] weighted-settings: Add cursor hover to user-message 2022-01-30 16:53:53 -05:00
Chris Wilson
d36637ed13 Fix a bug causing the /weighted-settings page to fail to detect a change in the source JSON file 2022-01-30 16:50:04 -05:00
N00byKing
dd5e5dcda7 v6: Add missing info regarding Server Port 2022-01-30 18:49:39 +01:00
Jarno Westhof
0ff7fe8479 [Generation] Fixed creation of new Slot-Info 2022-01-30 17:09:10 +01:00
Fabian Dill
8c638bcfd8 Server: allow LocationScouts to create free hints 2022-01-30 14:14:49 +01:00
Fabian Dill
0bd252e7f5 Server: add slot_info key to Connected 2022-01-30 13:57:12 +01:00
Jarno Westhof
ddd3073132 [Docs] Fixed typo 2022-01-30 13:52:51 +01:00
N00byKing
1788422abc v6: Link to release instead of actions 2022-01-30 10:58:48 +01:00
Fabian Dill
6210630ce2 Core: increment version 2022-01-30 03:45:21 +01:00
Fabian Dill
e5af7d11cc Tests: add ID range checks 2022-01-29 16:10:42 +01:00
Fabian Dill
5777808aa9 git: cleanup gitignore, as a bunch of files/folders no longer exist in AP 2022-01-29 15:39:14 +01:00
Fabian Dill
a97e6833a3 LttP: point guide to snes9x rr which is open source, rather than someone's google drive 2022-01-29 15:38:26 +01:00
Chris Wilson
79408ba0c4 Add *.nes to .gitignore 2022-01-29 00:15:24 -05:00
Fabian Dill
25dd89ed17 MultiServer: delete unused function 2022-01-28 09:29:29 +01:00
Brad Humphrey
dd61d0d395 Don't swap items that reduce access (#247) 2022-01-28 05:40:08 +01:00
Brad Humphrey
65a92746d1 Sort before distribute to preserve seed integrity 2022-01-28 05:39:34 +01:00
N00byKing
695e87689c sm64ex: More name changes 2022-01-28 05:38:41 +01:00
Hussein Farran
8997e786da Merge pull request #242 from N00byKing/patch-2
sm64ex: Clarify Instructions
2022-01-27 13:36:18 -05:00
Fabian Dill
239f1afbbd SM64: increment data version to trigger new names to be downloaded to clients 2022-01-27 15:36:14 +01:00
N00byKing
df09b5baac sm64ex: Rename some Items and Locations according to feedback 2022-01-27 15:35:28 +01:00
Yussur Mustafa Oraji
de4aa78fd6 sm64ex: Clarify Instructions 2022-01-27 14:42:49 +01:00
Zach Parks
8175d4c31f Rogue Legacy: Update world definition for 0.8 changes. (#236)
"
Here's a list of compiled changes for the next major release of the Rogue Legacy Randomizer.

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

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

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

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

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

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

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

* added Nothing and NoEnergy as hint blacklist

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

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

* OoT: fix error message crash in get_hint_area

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

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

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

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

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

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

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

* fixed failling generations when using 'fun' settings

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

* fixed debug logger

* removed unsupported "suits_restriction" option

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

* - fixed deathlink emptying reserves

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

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

* Minecraft: 1.17 advancements and logic support

* Update Minecraft tracker to 1.17

* Minecraft: add tests for new advancements

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

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

* fix docstring for PostgameAdvancements

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

* Update descriptions for AdvancementGoal and EggShardsRequired

* Minecraft: fix tests for required_bosses attribute

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

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

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

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

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

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

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

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

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

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

* Fixed unnececerly recalculation of item_name_groups

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

* Marked the loss of location 1337158

* Updated network graph

* First draft tinmespinner documentation

* Moved personal items to slot_data rather than location scouts

* Disabled Remote Items

* Updated docs

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

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

* added some basic options

* rule parser works now at least

* make sure to commit everything this time

* temporary change to BaseClasses for oot

* overworld location graph builds mostly correctly

* adding oot data files

* commenting out world options until later since they only existed to make the RuleParser work

* conversion functions between AP ids and OOT ids

* world graph outputs

* set scrub prices

* itempool generates, entrances connected, way too many options added

* fixed set_rules and set_shop_rules

* temp baseclasses changes

* Reaches the fill step now, old event-based system retained in case the new way breaks

* Song placements and misc fixes everywhere

* temporary changes to make oot work

* changed root exits for AP fill framework

* prevent infinite recursion due to OoT sharing usage of the address field

* age reachability works hopefully, songs are broken again

* working spoiler log generation on beatable-only

* Logic tricks implemented

* need this for logic tricks

* fixed map/compass being placed on Serenade location

* kill unreachable events before filling the world

* add a bunch of utility functions to prepare for rom patching

* move OptionList into generic options

* fixed some silly bugs with OptionList

* properly seed all random behavior (so far)

* ROM generation working

* fix hints trying to get alttp dungeon hint texts

* continue fixing hints

* add oot to network data package

* change item and location IDs to 66000 and 67000 range respectively

* push removed items to precollected items

* fixed various issues with cross-contamination with multiple world generation

* reenable glitched logic (hopefully)

* glitched world files age-check fix

* cleaned up some get_locations calls

* added token shuffle and scrub shuffle, modified some options slightly to make the parsing work

* reenable MQ dungeons

* fix forest mq exception

* made targeting style an option for now, will be cosmetic later

* reminder to move targeting to cosmetics

* some oot option maintenance

* enabled starting time of day

* fixed issue breaking shop slots in multiworld generation

* added "off" option for text shuffle and hints

* shopsanity functionality restored

* change patch file extension

* remove unnecessary utility functions + imports

* update MIT license

* change option to "patch_uncompressed_rom" instead of "compress_rom"

* compliance with new AutoWorld systems

* Kill only internal events, remove non-internal big poe event in code

* re-add the big poe event and handle it correctly

* remove extra method in Range option

* fix typo

* Starting items, starting with consumables option

* do not remove nonexistent item

* move set_shop_rules to after shop items are placed

* some cleanup

* add retries for song placement

* flagged Skull Mask and Mask of Truth as advancement items

* update OoT to use LogicMixin

* Fixed trying to assign starting items from the wrong players

* fixed song retry step

* improved option handling, comments, and starting item replacements

* DefaultOnToggle writes Yes or No to spoiler

* enable compression of output if Compress executable is present

* clean up compression

* check whether (de)compressor exists before running the process

* allow specification of rom path in host.yaml

* check if decompressed file already exists before decompressing again

* fix triforce hunt generation

* rename all the oot state functions with prefix

* OoT: mark triforce pieces as completion goal for triforce hunt

* added overworld and any-dungeon shuffle for dungeon items

* Hide most unshuffled locations and events from the list of locations in spoiler

* build oot option ranges with a generic function instead of defining each separately

* move oot output-type control to host.yaml instead of individual yamls

* implement dungeon song shuffle

* minor improvements to overworld dungeon item shuffle

* remove random ice trap names in shops, mostly to avoid maintaining a massive censor list

* always output patch file to folder, remove option to generate ROM in preparation for removal

* re-add the fix for infinite recursion due to not being light or dark world

* change AP-sendable to Ocarina of Time model, since the triforce piece has some extra code apparently

* oot: remove item_names and location_names

* oot: minor fixes

* oot: comment out ROM patching

* oot: only add CollectionState objects on creation if actually needed

* main entrance shuffle method and entrances-based rules

* fix entrances based rules

* disable master quest and big poe count options for client compatibility

* use get_player_name instead of get_player_names

* fix OptionList

* fix oot options for new option system

* new coop section in oot rom: expand player names to 16 bytes, write AP_PLAYER_NAME at end of PLAYER_NAMES

* fill AP player name in oot rom with 0 instead of 0xDF

* encode player name with ASCII for fixed-width

* revert oot player name array to 8 bytes per name

* remove Pierre location if fast scarecrow is on

* check player name length

* "free_scarecrow" not "fast_scarecrow"

* OoT locations now properly store the AP ID instead of the oot internal ID

* oot __version__ updates in lockstep with AP version

* pull in unmodified oot cosmetic files

* also grab JSONDump since it's needed apparently

* gather extra needed methods, modify imports

* delete cosmetics log, replace all instances of SettingsList with OOTWorld

* cosmetic options working, except for sound effects (due to ear-safe issues)

* SFX, Music, and Fanfare randomization reenabled

* move OoT data files into the worlds folder

* move Compress and Decompress into oot data folder

* Replace get_all_state with custom method to avoid the cache

* OoT ROM: increment item counter before setting incoming item/player values to 0, preventing desync issues

* set data_version to 0

* make Kokiri Sword shuffle off by default

* reenable "Random Choice" for various cosmetic options

* kill Ruto's Letter turnin if open fountain
also fix for shopsanity

* place Buy Goron/Zora Tunic first in shop shuffle

* make ice traps appear as other items instead of breaking generation

* managed to break ice traps on non-major-only

* only handle ice traps if they are on

* fix shopsanity for non-oot games, and write player name instead of player number

* light arrows hint uses player name instead of player number

* Reenable "skip child zelda" option

* fix entrances_based_rules

* fix ganondorf hint if starting with light arrows

* fix dungeonitem shuffle and shopsanity interaction

* remove has_all_of, has_any_of, count_of in BaseClasses, replace usage with has_all, has_any, has_group

* force local giveable item on ZL if skip_child_zelda and shuffle_song_items is any

* keep bosses and bombchu bowling chus out of data package

* revert workaround for infinite recursion and fix it properly

* fix shared shop id caches during patching process

* fix shop text box overflows, as much as possible

* add default oot host.yaml option

* add .apz5, .n64, .z64 to gitignore

* Properly document and name all (functioning) OOT options

* clean up some imports

* remove unnecessary files from oot's data

* fix typo in gitignore

* readd the Compress and Decompress utilities, since they are needed for generation

* cleanup of imports and some minor optimizations

* increase shop offset for item IDs to 0xCB

* remove shop item AP ids entirely

* prevent triforce pieces for other players from being received by yourself

* add "excluded" property to Location

* Hint system adapted and reenabled; hints still unseeded

* make hints deterministic with lists instead of sets

* do not allow hints to point to Light Arrows on non-vanilla bridge

* foreign locations hint as their full name in OoT rather than their region

* checkedLocations now stores hint names by player ID, so that the same location in different worlds can have hints associated

* consolidate versioning in Utils

* ice traps appear as major items rather than any progression item

* set prescription and claim check as defaults for adult trade item settings

* add oot options to playerSettings

* allow case-insensitive logic tricks in yaml

* fix oot shopsanity option formatting

* Write OoT override info even if local item, enabling local checks to show up immediately in the client

* implement CollectionState.can_live_dmg for oot glitched logic

* filter item names for invalid characters when patching shops

* make ice traps appear according to the settings of the world they are shuffled into, rather than the original world

* set hidden-spoiler items and locations with Shop items to events

* make GF carpenters, Gerudo Card, Malon, ZL, and Impa events if the relevant settings are enabled, preventing them from appearing in the client on game start

* Fix oot Glitched and No Logic generation

* fix indenting

* Greatly reduce displayed cosmetic options

* Change oot data version to 1

* add apz5 distribution to webhost

* print player name if an ALttP dungeon contains a good item for OoT world

* delete unneeded commented code

* remove OcarinaSongs import to satisfy lint
2021-09-02 14:35:05 +02:00
Fabian Dill
74c30ce09a Fill: remove/delay some LttP imports 2021-09-02 03:45:37 +02:00
Chris Wilson
859316353e Link /games to player-settings pages, add link to template file to player-settings, add markdown style formatting to /templates 2021-09-01 20:47:36 -04:00
Hussein Farran
63c9bea724 Remove total_items option. 2021-09-01 21:47:29 +00:00
Hussein Farran
df435eb693 Remove total_items option. 2021-09-01 17:35:16 -04:00
espeon65536
c73b994305 use_cache argument to get_all_state 2021-09-01 19:21:03 +00:00
espeon65536
88451d4239 Skip caching get_all_state while setting rules
Since rules have not been set for later worlds, the cache believes the completion condition is freely available if it had been placed previously, which breaks beatable-only key placement.
2021-09-01 19:21:03 +00:00
CaitSith2
f74db254f6 fix typo in default value. 2021-09-01 09:18:43 -07:00
Fabian Dill
3cb0a22e17 LttP: crash on outdated dungeon_items use 2021-09-01 17:56:35 +02:00
Fabian Dill
ca3e01b15e LttPClient: prevent crash when trying to access sys.stdin 2021-09-01 17:56:19 +02:00
espeon65536
e9d1dcc46c set get_all_cache properly 2021-09-01 14:49:29 +00:00
Fabian Dill
7fd0f1a5bf Subnautica: implement create_item and therefore start_inventory 2021-09-01 16:46:44 +02:00
Fabian Dill
2d65fbf798 Merge pull request #58 from Ijwu/main
Risk of Rain 2 support
2021-09-01 11:30:41 +00:00
Fabian Dill
ac915d00fc Merge branch 'main' into main 2021-09-01 11:23:30 +00:00
espeon65536
fbb8d6b132 invalidate state cache so that reachable_regions are recalculated during TR key logic 2021-09-01 11:22:30 +00:00
espeon65536
fb0f70b3e3 make owg entrances in inverted 2021-09-01 11:22:30 +00:00
espeon65536
17929415ee actually set owg rules 2021-09-01 11:22:30 +00:00
espeon65536
631b6788c6 remove keys option for get_all_state, collect dungeon-local keys, and fix all uses of the state 2021-09-01 11:22:30 +00:00
espeon65536
7972aa6320 split building owg connections and setting the rules for those connections 2021-09-01 11:22:30 +00:00
espeon65536
138c884684 wipe reachable regions during TR key logic checks to ensure properly finding logic regions 2021-09-01 11:22:30 +00:00
Hussein Farran
f5ef98287a Add docstring to RiskOfRainWorld 2021-08-31 20:45:09 -04:00
Hussein Farran
5188b41ab0 Update RoR2 guide. 2021-08-31 20:42:16 -04:00
Hussein Farran
f83ba6e615 Add YAML options and update slot data.
Add TotalItems YAML option.
Add AllowLunarItems YAML option.
Send along TotalRevivals number with slot data.
2021-08-31 20:38:44 -04:00
Hussein Farran
cc2a72eb82 Locations/Events now None id 2021-08-31 20:21:52 -04:00
Chris Wilson
4fcce66505 Move game names and descriptions into AutoWorld, fix option value names on player-settings pages 2021-08-31 17:28:46 -04:00
Fabian Dill
66627d8a66 Options: match Toggle's get_option_name signature to Choice's 2021-08-31 22:52:14 +02:00
Fabian Dill
adfd68f83c Options: fix get_option_name 2021-08-31 22:14:18 +02:00
Fabian Dill
ddc619f2e7 WebHost: sample yamls: some formatting issues 2021-08-31 19:56:45 +02:00
Fabian Dill
ff2e57705e WebHost: sample yamls now render Range defaults correctly 2021-08-31 19:54:55 +02:00
Fabian Dill
a6a859b272 WebHost: fix sample yamls that have no options.
WebHost: hide hidden games from templates listing
2021-08-31 19:06:24 +02:00
Fabian Dill
88c5ebdd2f WebHost: add per-game yaml file downloads 2021-08-31 18:58:54 +02:00
Hussein Farran
3d578bcc98 Set force_auto_forfeit for RoR2 2021-08-31 10:08:19 -04:00
Hussein Farran
c3290af2bd Merge branch 'ArchipelagoMW:main' into main 2021-08-31 10:07:40 -04:00
Fabian Dill
01f1545b3e AutoWorld: add forced_auto_forfeit and set it for StS 2021-08-31 16:04:54 +02:00
Hussein Farran
fc8e849db5 Remove location id from Victory location. 2021-08-31 10:01:09 -04:00
Hussein Farran
9115e59f15 Add RoR2 to README 2021-08-31 08:37:01 -04:00
Hussein Farran
2f4b248a45 Add more information to the RoR2 docs. 2021-08-31 00:25:48 -04:00
Hussein Farran
2f28afb46e Add RoR2 Docs 2021-08-31 00:17:08 -04:00
Hussein Farran
e960d7b58c Merge branch 'main' of https://github.com/Ijwu/Archipelago into main 2021-08-30 21:43:18 -04:00
Fabian Dill
321569c542 Factorio: Fix random rocket-silo recipe unable to pick ingredients where recipe name != product name 2021-08-31 01:47:00 +02:00
Fabian Dill
df037c54ff LttP: fix dungeon original item rule calling
Found by Espeon
2021-08-30 23:52:40 +02:00
Fabian Dill
d859cecffb Options: use isinstance instead of type for Choice comparison 2021-08-30 23:07:19 +02:00
Fabian Dill
fd6e009c4b Fill: fix placing non_local + non advancement items 2021-08-30 22:20:44 +02:00
Fabian Dill
4520051ec9 Slay the Spire: add to playerSettings.yaml 2021-08-30 22:19:48 +02:00
Fabian Dill
b90b73859a Slay the Spire: add to playerSettings.yaml 2021-08-30 20:07:25 +02:00
Fabian Dill
6c357b61cc LttP: re-remove LttP import in BaseClasses 2021-08-30 19:11:12 +02:00
Fabian Dill
12957db90f Options: implement __eq__ assert for possible checks 2021-08-30 19:08:10 +02:00
CaitSith2
3c74f561d5 LttP: Fix smallkey_shuffle in menu display
use smallkey_shuffle.option_universal from worlds.alttp.Options rather than "universal" for compare operations on universal checking.
2021-08-30 09:59:20 -07:00
Fabian Dill
cc70a6fa26 LttP: make shuffle names consistent 2021-08-30 18:00:39 +02:00
Fabian Dill
1c42564d90 LttP: remove leftover location binding 2021-08-30 16:47:34 +02:00
Fabian Dill
e76c870c09 Unittest: fix TestInvertedBombRules 2021-08-30 16:38:21 +02:00
Fabian Dill
5daadcb2d5 LttP: implement new dungeon_items handling
LttP: move glitch_boots to new options system
WebHost: options.yaml no longer lists aliases
General: remove region.can_fill, it was only used as a hack to make dungeon-specific items to work
2021-08-30 16:31:56 +02:00
espeon65536
a124a7a82a Create event Blaze Spawner containing Blaze Rods, preventing scenarios where the only progression in a sphere is to gain access to a fortress, which crashes playthrough generation 2021-08-30 08:15:21 +00:00
espeon65536
a65bf60cea add structure compasses to itempool in a fixed order 2021-08-30 08:15:21 +00:00
Fabian Dill
3fa28a3fdb LttP: fix import mistake 2021-08-30 01:18:30 +02:00
Fabian Dill
baa7992a7a AutoWorld: add post_fill
LttP: Move ShopSlotFill to post_fill
2021-08-30 01:16:04 +02:00
Fabian Dill
7ba4bfc0d5 Generate: make sure no None items make it into multidata. 2021-08-30 00:52:57 +02:00
Fabian Dill
11fedef2f5 Generate: turn off interpret_on_off for newstyle options 2021-08-29 20:21:49 +02:00
Hussein Farran
944347a2b3 Risk of Rain 2 implementation 2021-08-29 14:02:02 -04:00
Fabian Dill
8c72b0a6c4 AutoYAML: proper multi-line comments 2021-08-29 18:13:38 +02:00
Fabian Dill
5d62d4e063 Clients: logging fixes 2021-08-29 17:38:35 +02:00
Adam Ziegler
9b05537a0e fix argument, logger name 2021-08-29 15:31:02 +00:00
Adam Ziegler
fd0a87626e list connected SNESes if more than one; allow connecting to specific one 2021-08-29 15:31:02 +00:00
KonoTyran
9402d82405 Slay the Spire (#54)
Add Slay the Spire
2021-08-29 17:30:44 +02:00
Fabian Dill
da6674760c LttP: convert MultiWorld.dungeons to dict for faster lookup 2021-08-29 16:02:28 +02:00
Fabian Dill
ee03371dd0 LttP: make heartbeep off functional again 2021-08-29 15:43:16 +02:00
Fabian Dill
a975c8fd00 LttP: Format non-native Location hints better 2021-08-28 23:18:45 +02:00
Fabian Dill
60840da740 LttP: fix dungeon local items to be local to their own dungeon 2021-08-28 22:58:23 +02:00
Fabian Dill
de567cc701 LttP: Move more functionality into ALttPItem from Item
LttP: More efficiently build !hint entrance info
LttP: More efficiently check for and build Big Bomb Shop playthrough path
2021-08-28 12:56:52 +02:00
Fabian Dill
de4775b0c8 LttP: Move difficulties and er seed sharing to generate_early 2021-08-28 00:26:02 +02:00
Fabian Dill
104cc0ea83 document World.hidden 2021-08-27 20:46:33 +02:00
CaitSith2
5bb8de500a Fix issue with syncing tech tree post-forfeit. 2021-08-27 10:41:29 -07:00
Fabian Dill
21255b3b46 LttP: Rename Shop Slot 1, 2, 3 to Shop Slot Left, Center, Right
General: Move generic IDs from LttP to new Generic World
Generate: ensure thread errors are collected before data from their completion may be referenced in playthrough/spoiler
2021-08-27 14:52:33 +02:00
espeon65536
e8da9924c6 allow collecting silver bow if noglitches or swordless, even if the limit is under 2 2021-08-27 07:44:05 +00:00
espeon65536
96b38aba04 mark TRBK as impassable during initial pass for TR key logic, so that crystaroller can be marked as front-locked 2021-08-27 07:44:05 +00:00
espeon65536
b8b51965d2 skip first sweep_for_events in playthrough computation, so keys are no longer treated as special 2021-08-27 07:44:05 +00:00
espeon65536
be46d128bc do not double-collect keys during playthrough computation, since they are progression items now 2021-08-27 07:44:05 +00:00
Fabian Dill
c05f1ed24f to be or not to be 2021-08-26 18:25:15 +02:00
Fabian Dill
99775ec1bd Generate: require that player names be unique again 2021-08-26 17:22:55 +02:00
Fabian Dill
f4f043ac87 MultiServer: categorize methods 2021-08-26 16:19:37 +02:00
Fabian Dill
acbca78e2d update Prompt Toolkit 2021-08-24 09:52:45 +02:00
Fabian Dill
30ac7baa2c FactorioClient: Batch-Send RCON commands when receiving catch-up locations and multiple items. 2021-08-24 09:52:12 +02:00
espeon65536
21a5170337 remove double negative in apmc file check 2021-08-24 04:02:28 +00:00
espeon65536
3a5a6a096b add .apmc and Forge server to gitignore 2021-08-24 04:02:28 +00:00
espeon65536
578ae70150 update playerSettings.yaml 2021-08-24 04:02:28 +00:00
espeon65536
57282e76a4 add send_defeated_mobs as option 2021-08-24 04:02:28 +00:00
espeon65536
7aaa652ef5 Give docstrings and display names to Minecraft options 2021-08-24 04:02:28 +00:00
espeon65536
81da0d2ba4 Minecraft client: skip deleting and recopying an apmc file that is already in APData 2021-08-24 04:02:28 +00:00
espeon65536
ce6cdcaf92 Minecraft client: prevent options.yaml/host.yaml contamination from non-install directories 2021-08-24 04:02:28 +00:00
espeon65536
4730a928b5 Minecraft client: fix NoneType-related error if run without apmc file 2021-08-24 04:02:28 +00:00
Chris Wilson
4c0f0a16c9 Updates to WebHost
- Support displayname option for Options module
- Improvements to landing page
- Added multi-language capable FAQ page
- Removed weighted-settings page
- Removed references to weighted-settings page
2021-08-22 20:01:58 -04:00
Fabian Dill
b07fc80f3f AutoWorld: if any world data_version is set to 0, set it for the main datapackage 2021-08-22 04:22:34 +02:00
Fabian Dill
6a3d1fcaf4 LttP & Factorio: fix item state removal for progressive items. 2021-08-21 06:55:08 +02:00
Fabian Dill
4aeb3cd3dc WebHost: allow /tutorial and /tutorial/ 2021-08-20 22:41:23 +02:00
Fabian Dill
6dc2000638 CommonClient.py: move in gui_enabled 2021-08-20 22:31:17 +02:00
Fabian Dill
72610d8c2f Core: log world ID ranges 2021-08-16 18:40:26 +02:00
Fabian Dill
0f55fa4f45 FactorioClient: allow setting a folder and find the executable in it, instead of trying to run a folder. 2021-08-15 13:46:58 +02:00
Fabian Dill
aec39c919c Minecraft: add missing minecraft defaults 2021-08-15 02:32:36 +02:00
Kono Tyran
a0849f9416 fixed error if destination folder did not exist already. 2021-08-15 00:05:54 +00:00
1466 changed files with 312918 additions and 17372 deletions

35
.github/ISSUE_TEMPLATE/bug_report.yaml vendored Normal file
View File

@@ -0,0 +1,35 @@
name: Bug Report
description: File a bug report.
title: "Bug: "
labels:
- bug / fix
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report! If this bug occurred during local generation check your
Archipelago install for a log (probably `C:\ProgramData\Archipelago\logs`)
and upload it with this report, as well as all yaml files used.
- type: textarea
id: what-happened
attributes:
label: What happened?
validations:
required: true
- type: textarea
id: expected-results
attributes:
label: What were the expected results?
validations:
required: true
- type: dropdown
id: version
attributes:
label: Software
description: Where did this bug occur?
options:
- Website
- Local generation
- While playing
validations:
required: true

View File

@@ -0,0 +1,17 @@
name: Feature Request
description: Request a feature!
title: "Category: "
labels:
- enhancement
body:
- type: markdown
attributes:
value: |
Please replace `Category` in the title with what this feature will be targeting, such as Core generation,
website, documentation, or a game.
Note: this is not for requesting new games to be added. If you would like to request a game, the best place to
ask is about it is in the [discord](https://archipelago.gg/discord).
- type: textarea
id: feature
attributes:
label: What feature would you like to see?

10
.github/ISSUE_TEMPLATE/task.yaml vendored Normal file
View File

@@ -0,0 +1,10 @@
name: Task
description: Submit a task to be done. If this is not targeting core, it should likely be elsewhere.
title: "Core: "
labels:
- core
- enhancement
body:
- type: textarea
attributes:
label: What task needs to be completed?

12
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,12 @@
Please format your title with what portion of the project this pull request is
targeting and what it's changing.
ex. "MyGame4: implement new game" or "Docs: add new guide for customizing MyGame3"
## What is this fixing or adding?
## How was this tested?
## If this makes graphical changes, please attach screenshots.

112
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,112 @@
# This workflow will build a release-like distribution when manually dispatched
name: Build
on:
push:
paths:
- '.github/workflows/build.yml'
- 'setup.py'
- 'requirements.txt'
pull_request:
paths:
- '.github/workflows/build.yml'
- 'setup.py'
- 'requirements.txt'
workflow_dispatch:
env:
ENEMIZER_VERSION: 7.1
APPIMAGETOOL_VERSION: 13
jobs:
# build-release-macos: # LF volunteer
build-win-py38: # RCs will still be built and signed by hand
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- name: Install python
uses: actions/setup-python@v4
with:
python-version: '3.8'
- name: Download run-time dependencies
run: |
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip
Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force
- name: Build
run: |
python -m pip install --upgrade pip
python setup.py build_exe --yes
$NAME="$(ls build)".Split('.',2)[1]
$ZIP_NAME="Archipelago_$NAME.7z"
echo "ZIP_NAME=$ZIP_NAME" >> $Env:GITHUB_ENV
New-Item -Path dist -ItemType Directory -Force
cd build
Rename-Item exe.$NAME Archipelago
7z a -mx=9 -mhe=on -ms "../dist/$ZIP_NAME" Archipelago
- name: Store 7z
uses: actions/upload-artifact@v3
with:
name: ${{ env.ZIP_NAME }}
path: dist/${{ env.ZIP_NAME }}
retention-days: 7 # keep for 7 days, should be enough
build-ubuntu1804:
runs-on: ubuntu-18.04
steps:
# - copy code below to release.yml -
- uses: actions/checkout@v3
- name: Install base dependencies
run: |
sudo apt update
sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0
sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below
- name: Get a recent python
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install build-time dependencies
run: |
echo "PYTHON=python3.9" >> $GITHUB_ENV
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
chmod a+rx appimagetool-x86_64.AppImage
./appimagetool-x86_64.AppImage --appimage-extract
echo -e '#/bin/sh\n./squashfs-root/AppRun "$@"' > appimagetool
chmod a+rx appimagetool
- name: Download run-time dependencies
run: |
wget -nv https://github.com/Ijwu/Enemizer/releases/download/$ENEMIZER_VERSION/ubuntu.16.04-x64.7z
7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z
- name: Build
run: |
# pygobject is an optional dependency for kivy that's not in requirements
# charset-normalizer was somehow incomplete in the github runner
"${{ env.PYTHON }}" -m venv venv
source venv/bin/activate
"${{ env.PYTHON }}" -m pip install --upgrade pip PyGObject charset-normalizer
python setup.py build_exe --yes bdist_appimage --yes
echo -e "setup.py build output:\n `ls build`"
echo -e "setup.py dist output:\n `ls dist`"
cd dist && export APPIMAGE_NAME="`ls *.AppImage`" && cd ..
export TAR_NAME="${APPIMAGE_NAME%.AppImage}.tar.gz"
(cd build && DIR_NAME="`ls | grep exe`" && mv "$DIR_NAME" Archipelago && tar -czvf ../dist/$TAR_NAME Archipelago && mv Archipelago "$DIR_NAME")
echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV
echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV
# - copy code above to release.yml -
- name: Build Again
run: |
source venv/bin/activate
python setup.py build_exe --yes
- name: Store AppImage
uses: actions/upload-artifact@v3
with:
name: ${{ env.APPIMAGE_NAME }}
path: dist/${{ env.APPIMAGE_NAME }}
retention-days: 7
- name: Store .tar.gz
uses: actions/upload-artifact@v3
with:
name: ${{ env.TAR_NAME }}
path: dist/${{ env.TAR_NAME }}
retention-days: 7

View File

@@ -14,9 +14,17 @@ name: "CodeQL"
on:
push:
branches: [ main ]
paths:
- '**.py'
- '**.js'
- '.github/workflows/codeql-analysis.yml'
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
paths:
- '**.py'
- '**.js'
- '.github/workflows/codeql-analysis.yml'
schedule:
- cron: '44 8 * * 1'
@@ -35,11 +43,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -50,7 +58,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -64,4 +72,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@v2

View File

@@ -3,23 +3,29 @@
name: lint
on: [push, pull_request]
on:
push:
paths:
- '**.py'
pull_request:
paths:
- '**.py'
jobs:
build:
flake8:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
uses: actions/setup-python@v1
- uses: actions/checkout@v3
- name: Set up Python 3.9
uses: actions/setup-python@v4
with:
python-version: 3.8
python-version: 3.9
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
python -m pip install --upgrade pip wheel
pip install flake8
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: |

85
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,85 @@
# This workflow will create a release and store builds to it when an x.y.z tag is pushed
name: Release
on:
push:
tags:
- '*.*.*'
env:
ENEMIZER_VERSION: 7.1
APPIMAGETOOL_VERSION: 13
jobs:
create-release:
runs-on: ubuntu-latest
steps:
- name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV # tag x.y.z will become "Archipelago x.y.z"
- name: Create Release
uses: softprops/action-gh-release@b7e450da2a4b4cb4bfbae528f788167786cfcedf
with:
draft: true # don't publish right away, especially since windows build is added by hand
prerelease: false
name: Archipelago ${{ env.RELEASE_VERSION }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# build-release-windows: # this is done by hand because of signing
# build-release-macos: # LF volunteer
build-release-ubuntu1804:
runs-on: ubuntu-18.04
steps:
- name: Set env
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
# - code below copied from build.yml -
- uses: actions/checkout@v3
- name: Install base dependencies
run: |
sudo apt update
sudo apt -y install build-essential p7zip xz-utils wget libglib2.0-0
sudo apt -y install python3-gi libgirepository1.0-dev # should pull dependencies for gi installation below
- name: Get a recent python
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install build-time dependencies
run: |
echo "PYTHON=python3.9" >> $GITHUB_ENV
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
chmod a+rx appimagetool-x86_64.AppImage
./appimagetool-x86_64.AppImage --appimage-extract
echo -e '#/bin/sh\n./squashfs-root/AppRun "$@"' > appimagetool
chmod a+rx appimagetool
- name: Download run-time dependencies
run: |
wget -nv https://github.com/Ijwu/Enemizer/releases/download/$ENEMIZER_VERSION/ubuntu.16.04-x64.7z
7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z
- name: Build
run: |
# pygobject is an optional dependency for kivy that's not in requirements
# charset-normalizer was somehow incomplete in the github runner
"${{ env.PYTHON }}" -m venv venv
source venv/bin/activate
"${{ env.PYTHON }}" -m pip install --upgrade pip PyGObject charset-normalizer
python setup.py build_exe --yes bdist_appimage --yes
echo -e "setup.py build output:\n `ls build`"
echo -e "setup.py dist output:\n `ls dist`"
cd dist && export APPIMAGE_NAME="`ls *.AppImage`" && cd ..
export TAR_NAME="${APPIMAGE_NAME%.AppImage}.tar.gz"
(cd build && DIR_NAME="`ls | grep exe`" && mv "$DIR_NAME" Archipelago && tar -czvf ../dist/$TAR_NAME Archipelago && mv Archipelago "$DIR_NAME")
echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV
echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV
# - code above copied from build.yml -
- name: Add to Release
uses: softprops/action-gh-release@b7e450da2a4b4cb4bfbae528f788167786cfcedf
with:
draft: true # see above
prerelease: false
name: Archipelago ${{ env.RELEASE_VERSION }}
files: |
dist/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -3,24 +3,58 @@
name: unittests
on: [push, pull_request]
on:
push:
paths:
- '**'
- '!docs/**'
- '!setup.py'
- '!*.iss'
- '!.gitignore'
- '!.github/workflows/**'
- '.github/workflows/unittests.yml'
pull_request:
paths:
- '**'
- '!docs/**'
- '!setup.py'
- '!*.iss'
- '!.gitignore'
- '!.github/workflows/**'
- '.github/workflows/unittests.yml'
jobs:
build:
runs-on: ${{ matrix.os }}
name: Test Python ${{ matrix.python.version }} ${{ matrix.os }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
python:
- {version: '3.8'}
- {version: '3.9'}
- {version: '3.10'}
include:
- python: {version: '3.8'} # win7 compat
os: windows-latest
- python: {version: '3.10'} # current
os: windows-latest
- python: {version: '3.10'} # current
os: macos-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
uses: actions/setup-python@v1
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python.version }}
uses: actions/setup-python@v4
with:
python-version: 3.8
python-version: ${{ matrix.python.version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
python ModuleUpdate.py --yes --force
pip install pytest pytest-subtests
python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt"
- name: Unittests
run: |
pytest test
pytest

50
.gitignore vendored
View File

@@ -4,9 +4,21 @@
*_Spoiler.txt
*.bmbp
*.apbp
*.apl2ac
*.apm3
*.apmc
*.apz5
*.aptloz
*.pyc
*.pyd
*.sfc
*.z64
*.n64
*.nes
*.sms
*.gb
*.gbc
*.gba
*.wixobj
*.lck
*.db3
@@ -14,6 +26,7 @@
*multisave
*.archipelago
*.apsave
*.BIN
build
bundle/components.wxs
@@ -21,14 +34,10 @@ dist
README.html
.vs/
EnemizerCLI/
RaceRom.py
weights/
/MultiMystery/
/Players/
/QUsb2Snes/
/SNI/
/options.yaml
/config.yaml
/uploads/
/logs/
_persistent_storage.yaml
mystery_result_*.yaml
@@ -37,7 +46,14 @@ success.txt
output/
Output Logs/
/factorio/
/Minecraft Forge Server/
/WebHostLib/static/generated
/freeze_requirements.txt
/Archipelago.zip
/setup.ini
/installdelete.iss
/data/user.kv
/datapackage
# Byte-compiled / optimized / DLL files
__pycache__/
@@ -71,6 +87,7 @@ MANIFEST
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
installer.log
# Unit test / coverage reports
htmlcov/
@@ -109,17 +126,22 @@ target/
profile_default/
ipython_config.py
# vim editor
*.swp
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
.venv*
env/
venv/
ENV/
env.bak/
venv.bak/
.code-workspace
shell.nix
# Spyder project settings
.spyderproject
@@ -145,4 +167,18 @@ dmypy.json
# Cython debug symbols
cython_debug/
Archipelago.zip
# minecraft server stuff
jdk*/
minecraft*/
minecraft_versions.json
!worlds/minecraft/
# pyenv
.python-version
# OS General Files
.DS_Store
.AppleDouble
.LSOverride
Thumbs.db
[Dd]esktop.ini

516
AdventureClient.py Normal file
View File

@@ -0,0 +1,516 @@
import asyncio
import hashlib
import json
import time
import os
import bsdiff4
import subprocess
import zipfile
from asyncio import StreamReader, StreamWriter, CancelledError
from typing import List
import Utils
from NetUtils import ClientStatus
from Utils import async_start
from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \
get_base_parser
from worlds.adventure import AdventureDeltaPatch
from worlds.adventure.Locations import base_location_id
from worlds.adventure.Rom import AdventureForeignItemInfo, AdventureAutoCollectLocation, BatNoTouchLocation
from worlds.adventure.Items import base_adventure_item_id, standard_item_max, item_table
from worlds.adventure.Offsets import static_item_element_size, connector_port_offset
SYSTEM_MESSAGE_ID = 0
CONNECTION_TIMING_OUT_STATUS = \
"Connection timing out. Please restart your emulator, then restart adventure_connector.lua"
CONNECTION_REFUSED_STATUS = \
"Connection Refused. Please start your emulator and make sure adventure_connector.lua is running"
CONNECTION_RESET_STATUS = \
"Connection was reset. Please restart your emulator, then restart adventure_connector.lua"
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
CONNECTION_CONNECTED_STATUS = "Connected"
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
SCRIPT_VERSION = 1
class AdventureCommandProcessor(ClientCommandProcessor):
def __init__(self, ctx: CommonContext):
super().__init__(ctx)
def _cmd_2600(self):
"""Check 2600 Connection State"""
if isinstance(self.ctx, AdventureContext):
logger.info(f"2600 Status: {self.ctx.atari_status}")
def _cmd_aconnect(self):
"""Discard current atari 2600 connection state"""
if isinstance(self.ctx, AdventureContext):
self.ctx.atari_sync_task.cancel()
class AdventureContext(CommonContext):
command_processor = AdventureCommandProcessor
game = 'Adventure'
lua_connector_port: int = 17242
def __init__(self, server_address, password):
super().__init__(server_address, password)
self.freeincarnates_used: int = -1
self.freeincarnate_pending: int = 0
self.foreign_items: [AdventureForeignItemInfo] = []
self.autocollect_items: [AdventureAutoCollectLocation] = []
self.atari_streams: (StreamReader, StreamWriter) = None
self.atari_sync_task = None
self.messages = {}
self.locations_array = None
self.atari_status = CONNECTION_INITIAL_STATUS
self.awaiting_rom = False
self.display_msgs = True
self.deathlink_pending = False
self.set_deathlink = False
self.client_compatibility_mode = 0
self.items_handling = 0b111
self.checked_locations_sent: bool = False
self.port_offset = 0
self.bat_no_touch_locations: [BatNoTouchLocation] = []
self.local_item_locations = {}
self.dragon_speed_info = {}
options = Utils.get_options()
self.display_msgs = options["adventure_options"]["display_msgs"]
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(AdventureContext, self).server_auth(password_requested)
if not self.auth:
self.auth = self.player_name
if not self.auth:
self.awaiting_rom = True
logger.info('Awaiting connection to adventure_connector to get Player information')
return
await self.send_connect()
def _set_message(self, msg: str, msg_id: int):
if self.display_msgs:
self.messages[(time.time(), msg_id)] = msg
def on_package(self, cmd: str, args: dict):
if cmd == 'Connected':
self.locations_array = None
if Utils.get_options()["adventure_options"].get("death_link", False):
self.set_deathlink = True
async_start(self.get_freeincarnates_used())
elif cmd == "RoomInfo":
self.seed_name = args['seed_name']
elif cmd == 'Print':
msg = args['text']
if ': !' not in msg:
self._set_message(msg, SYSTEM_MESSAGE_ID)
elif cmd == "ReceivedItems":
msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}"
self._set_message(msg, SYSTEM_MESSAGE_ID)
elif cmd == "Retrieved":
self.freeincarnates_used = args["keys"][f"adventure_{self.auth}_freeincarnates_used"]
if self.freeincarnates_used is None:
self.freeincarnates_used = 0
self.freeincarnates_used += self.freeincarnate_pending
self.send_pending_freeincarnates()
elif cmd == "SetReply":
if args["key"] == f"adventure_{self.auth}_freeincarnates_used":
self.freeincarnates_used = args["value"]
if self.freeincarnates_used is None:
self.freeincarnates_used = 0
self.freeincarnates_used += self.freeincarnate_pending
self.send_pending_freeincarnates()
def on_deathlink(self, data: dict):
self.deathlink_pending = True
super().on_deathlink(data)
def run_gui(self):
from kvui import GameManager
class AdventureManager(GameManager):
logging_pairs = [
("Client", "Archipelago")
]
base_title = "Archipelago Adventure Client"
self.ui = AdventureManager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
async def get_freeincarnates_used(self):
if self.server and not self.server.socket.closed:
await self.send_msgs([{"cmd": "SetNotify", "keys": [f"adventure_{self.auth}_freeincarnates_used"]}])
await self.send_msgs([{"cmd": "Get", "keys": [f"adventure_{self.auth}_freeincarnates_used"]}])
def send_pending_freeincarnates(self):
if self.freeincarnate_pending > 0:
async_start(self.send_pending_freeincarnates_impl(self.freeincarnate_pending))
self.freeincarnate_pending = 0
async def send_pending_freeincarnates_impl(self, send_val: int) -> None:
await self.send_msgs([{"cmd": "Set", "key": f"adventure_{self.auth}_freeincarnates_used",
"default": 0, "want_reply": False,
"operations": [{"operation": "add", "value": send_val}]}])
async def used_freeincarnate(self) -> None:
if self.server and not self.server.socket.closed:
await self.send_msgs([{"cmd": "Set", "key": f"adventure_{self.auth}_freeincarnates_used",
"default": 0, "want_reply": True,
"operations": [{"operation": "add", "value": 1}]}])
else:
self.freeincarnate_pending = self.freeincarnate_pending + 1
def convert_item_id(ap_item_id: int):
static_item_index = ap_item_id - base_adventure_item_id
return static_item_index * static_item_element_size
def get_payload(ctx: AdventureContext):
current_time = time.time()
items = []
dragon_speed_update = {}
diff_a_locked = ctx.diff_a_mode > 0
diff_b_locked = ctx.diff_b_mode > 0
freeincarnate_count = 0
for item in ctx.items_received:
item_id_str = str(item.item)
if base_adventure_item_id < item.item <= standard_item_max:
items.append(convert_item_id(item.item))
elif item_id_str in ctx.dragon_speed_info:
if item.item in dragon_speed_update:
last_index = len(ctx.dragon_speed_info[item_id_str]) - 1
dragon_speed_update[item.item] = ctx.dragon_speed_info[item_id_str][last_index]
else:
dragon_speed_update[item.item] = ctx.dragon_speed_info[item_id_str][0]
elif item.item == item_table["Left Difficulty Switch"].id:
diff_a_locked = False
elif item.item == item_table["Right Difficulty Switch"].id:
diff_b_locked = False
elif item.item == item_table["Freeincarnate"].id:
freeincarnate_count = freeincarnate_count + 1
freeincarnates_available = 0
if ctx.freeincarnates_used >= 0:
freeincarnates_available = freeincarnate_count - (ctx.freeincarnates_used + ctx.freeincarnate_pending)
ret = json.dumps(
{
"items": items,
"messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items()
if key[0] > current_time - 10},
"deathlink": ctx.deathlink_pending,
"dragon_speeds": dragon_speed_update,
"difficulty_a_locked": diff_a_locked,
"difficulty_b_locked": diff_b_locked,
"freeincarnates_available": freeincarnates_available,
"bat_logic": ctx.bat_logic
}
)
ctx.deathlink_pending = False
return ret
async def parse_locations(data: List, ctx: AdventureContext):
locations = data
# for loc_name, loc_data in location_table.items():
# if flags["EventFlag"][280] & 1 and not ctx.finished_game:
# await ctx.send_msgs([
# {"cmd": "StatusUpdate",
# "status": 30}
# ])
# ctx.finished_game = True
if locations == ctx.locations_array:
return
ctx.locations_array = locations
if locations is not None:
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": locations}])
def send_ap_foreign_items(adventure_context):
foreign_item_json_list = []
autocollect_item_json_list = []
bat_no_touch_locations_json_list = []
for fi in adventure_context.foreign_items:
foreign_item_json_list.append(fi.get_dict())
for fi in adventure_context.autocollect_items:
autocollect_item_json_list.append(fi.get_dict())
for ntl in adventure_context.bat_no_touch_locations:
bat_no_touch_locations_json_list.append(ntl.get_dict())
payload = json.dumps(
{
"foreign_items": foreign_item_json_list,
"autocollect_items": autocollect_item_json_list,
"local_item_locations": adventure_context.local_item_locations,
"bat_no_touch_locations": bat_no_touch_locations_json_list
}
)
print("sending foreign items")
msg = payload.encode()
(reader, writer) = adventure_context.atari_streams
writer.write(msg)
writer.write(b'\n')
def send_checked_locations_if_needed(adventure_context):
if not adventure_context.checked_locations_sent and adventure_context.checked_locations is not None:
if len(adventure_context.checked_locations) == 0:
return
checked_short_ids = []
for location in adventure_context.checked_locations:
checked_short_ids.append(location - base_location_id)
print("Sending checked locations")
payload = json.dumps(
{
"checked_locations": checked_short_ids,
}
)
msg = payload.encode()
(reader, writer) = adventure_context.atari_streams
writer.write(msg)
writer.write(b'\n')
adventure_context.checked_locations_sent = True
async def atari_sync_task(ctx: AdventureContext):
logger.info("Starting Atari 2600 connector. Use /2600 for status information")
while not ctx.exit_event.is_set():
try:
error_status = None
if ctx.atari_streams:
(reader, writer) = ctx.atari_streams
msg = get_payload(ctx).encode()
writer.write(msg)
writer.write(b'\n')
try:
await asyncio.wait_for(writer.drain(), timeout=1.5)
try:
# Data will return a dict with 1+ fields
# 1. A keepalive response of the Players Name (always)
# 2. romhash field with sha256 hash of the ROM memory region
# 3. locations, messages, and deathLink
# 4. freeincarnate, to indicate a freeincarnate was used
data = await asyncio.wait_for(reader.readline(), timeout=5)
data_decoded = json.loads(data.decode())
if 'scriptVersion' not in data_decoded or data_decoded['scriptVersion'] != SCRIPT_VERSION:
msg = "You are connecting with an incompatible Lua script version. Ensure your connector " \
"Lua and AdventureClient are from the same Archipelago installation."
logger.info(msg, extra={'compact_gui': True})
ctx.gui_error('Error', msg)
error_status = CONNECTION_RESET_STATUS
if ctx.seed_name and bytes(ctx.seed_name, encoding='ASCII') != ctx.seed_name_from_data:
msg = "The server is running a different multiworld than your client is. " \
"(invalid seed_name)"
logger.info(msg, extra={'compact_gui': True})
ctx.gui_error('Error', msg)
error_status = CONNECTION_RESET_STATUS
if 'romhash' in data_decoded:
if ctx.rom_hash.upper() != data_decoded['romhash'].upper():
msg = "The rom hash does not match the client rom hash data"
print("got " + data_decoded['romhash'])
print("expected " + str(ctx.rom_hash))
logger.info(msg, extra={'compact_gui': True})
ctx.gui_error('Error', msg)
error_status = CONNECTION_RESET_STATUS
if ctx.auth is None:
ctx.auth = ctx.player_name
if ctx.awaiting_rom:
await ctx.server_auth(False)
if 'locations' in data_decoded and ctx.game and ctx.atari_status == CONNECTION_CONNECTED_STATUS \
and not error_status and ctx.auth:
# Not just a keep alive ping, parse
async_start(parse_locations(data_decoded['locations'], ctx))
if 'deathLink' in data_decoded and data_decoded['deathLink'] > 0 and 'DeathLink' in ctx.tags:
dragon_name = "a dragon"
if data_decoded['deathLink'] == 1:
dragon_name = "Rhindle"
elif data_decoded['deathLink'] == 2:
dragon_name = "Yorgle"
elif data_decoded['deathLink'] == 3:
dragon_name = "Grundle"
print (ctx.auth + " has been eaten by " + dragon_name )
await ctx.send_death(ctx.auth + " has been eaten by " + dragon_name)
# TODO - also if player reincarnates with a dragon onscreen ' dies to avoid being eaten by '
if 'victory' in data_decoded and not ctx.finished_game:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
if 'freeincarnate' in data_decoded:
await ctx.used_freeincarnate()
if ctx.set_deathlink:
await ctx.update_death_link(True)
send_checked_locations_if_needed(ctx)
except asyncio.TimeoutError:
logger.debug("Read Timed Out, Reconnecting")
error_status = CONNECTION_TIMING_OUT_STATUS
writer.close()
ctx.atari_streams = None
except ConnectionResetError as e:
logger.debug("Read failed due to Connection Lost, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.atari_streams = None
except TimeoutError:
logger.debug("Connection Timed Out, Reconnecting")
error_status = CONNECTION_TIMING_OUT_STATUS
writer.close()
ctx.atari_streams = None
except ConnectionResetError:
logger.debug("Connection Lost, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.atari_streams = None
except CancelledError:
logger.debug("Connection Cancelled, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.atari_streams = None
pass
except Exception as e:
print("unknown exception " + e)
raise
if ctx.atari_status == CONNECTION_TENTATIVE_STATUS:
if not error_status:
logger.info("Successfully Connected to 2600")
ctx.atari_status = CONNECTION_CONNECTED_STATUS
ctx.checked_locations_sent = False
send_ap_foreign_items(ctx)
send_checked_locations_if_needed(ctx)
else:
ctx.atari_status = f"Was tentatively connected but error occurred: {error_status}"
elif error_status:
ctx.atari_status = error_status
logger.info("Lost connection to 2600 and attempting to reconnect. Use /2600 for status updates")
else:
try:
port = ctx.lua_connector_port + ctx.port_offset
logger.debug(f"Attempting to connect to 2600 on port {port}")
print(f"Attempting to connect to 2600 on port {port}")
ctx.atari_streams = await asyncio.wait_for(
asyncio.open_connection("localhost",
port),
timeout=10)
ctx.atari_status = CONNECTION_TENTATIVE_STATUS
except TimeoutError:
logger.debug("Connection Timed Out, Trying Again")
ctx.atari_status = CONNECTION_TIMING_OUT_STATUS
continue
except ConnectionRefusedError:
logger.debug("Connection Refused, Trying Again")
ctx.atari_status = CONNECTION_REFUSED_STATUS
continue
except CancelledError:
pass
except CancelledError:
pass
print("exiting atari sync task")
async def run_game(romfile):
auto_start = Utils.get_options()["adventure_options"].get("rom_start", True)
rom_args = Utils.get_options()["adventure_options"].get("rom_args")
if auto_start is True:
import webbrowser
webbrowser.open(romfile)
elif os.path.isfile(auto_start):
open_args = [auto_start, romfile]
if rom_args is not None:
open_args.insert(1, rom_args)
subprocess.Popen(open_args,
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
async def patch_and_run_game(patch_file, ctx):
base_name = os.path.splitext(patch_file)[0]
comp_path = base_name + '.a26'
try:
base_rom = AdventureDeltaPatch.get_source_data()
except Exception as msg:
logger.info(msg, extra={'compact_gui': True})
ctx.gui_error('Error', msg)
with open(Utils.user_path("data", "adventure_basepatch.bsdiff4"), "rb") as file:
basepatch = bytes(file.read())
base_patched_rom_data = bsdiff4.patch(base_rom, basepatch)
with zipfile.ZipFile(patch_file, 'r') as patch_archive:
if not AdventureDeltaPatch.check_version(patch_archive):
logger.error("apadvn version doesn't match this client. Make sure your generator and client are the same")
raise Exception("apadvn version doesn't match this client.")
ctx.foreign_items = AdventureDeltaPatch.read_foreign_items(patch_archive)
ctx.autocollect_items = AdventureDeltaPatch.read_autocollect_items(patch_archive)
ctx.local_item_locations = AdventureDeltaPatch.read_local_item_locations(patch_archive)
ctx.dragon_speed_info = AdventureDeltaPatch.read_dragon_speed_info(patch_archive)
ctx.seed_name_from_data, ctx.player_name = AdventureDeltaPatch.read_rom_info(patch_archive)
ctx.diff_a_mode, ctx.diff_b_mode = AdventureDeltaPatch.read_difficulty_switch_info(patch_archive)
ctx.bat_logic = AdventureDeltaPatch.read_bat_logic(patch_archive)
ctx.bat_no_touch_locations = AdventureDeltaPatch.read_bat_no_touch(patch_archive)
ctx.rom_deltas = AdventureDeltaPatch.read_rom_deltas(patch_archive)
ctx.auth = ctx.player_name
patched_rom_data = AdventureDeltaPatch.apply_rom_deltas(base_patched_rom_data, ctx.rom_deltas)
rom_hash = hashlib.sha256()
rom_hash.update(patched_rom_data)
ctx.rom_hash = rom_hash.hexdigest()
ctx.port_offset = patched_rom_data[connector_port_offset]
with open(comp_path, "wb") as patched_rom_file:
patched_rom_file.write(patched_rom_data)
async_start(run_game(comp_path))
if __name__ == '__main__':
Utils.init_logging("AdventureClient")
async def main():
parser = get_base_parser()
parser.add_argument('patch_file', default="", type=str, nargs="?",
help='Path to an ADVNTURE.BIN rom file')
parser.add_argument('port', default=17242, type=int, nargs="?",
help='port for adventure_connector connection')
args = parser.parse_args()
ctx = AdventureContext(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
ctx.atari_sync_task = asyncio.create_task(atari_sync_task(ctx), name="Adventure Sync")
if args.patch_file:
ext = args.patch_file.split(".")[len(args.patch_file.split(".")) - 1].lower()
if ext == "apadvn":
logger.info("apadvn file supplied, beginning patching process...")
async_start(patch_and_run_game(args.patch_file, ctx))
else:
logger.warning(f"Unknown patch file extension {ext}")
if args.port is int:
ctx.lua_connector_port = args.port
await ctx.exit_event.wait()
ctx.server_address = None
await ctx.shutdown()
if ctx.atari_sync_task:
await ctx.atari_sync_task
print("finished atari_sync_task (main)")
import colorama
colorama.init()
asyncio.run(main())
colorama.deinit()

File diff suppressed because it is too large Load Diff

172
ChecksFinderClient.py Normal file
View File

@@ -0,0 +1,172 @@
from __future__ import annotations
import os
import sys
import asyncio
import shutil
import ModuleUpdate
ModuleUpdate.update()
import Utils
if __name__ == "__main__":
Utils.init_logging("ChecksFinderClient", exception_logger="Client")
from NetUtils import NetworkItem, ClientStatus
from CommonClient import gui_enabled, logger, get_base_parser, ClientCommandProcessor, \
CommonContext, server_loop
class ChecksFinderClientCommandProcessor(ClientCommandProcessor):
def _cmd_resync(self):
"""Manually trigger a resync."""
self.output(f"Syncing items.")
self.ctx.syncing = True
class ChecksFinderContext(CommonContext):
command_processor: int = ChecksFinderClientCommandProcessor
game = "ChecksFinder"
items_handling = 0b111 # full remote
def __init__(self, server_address, password):
super(ChecksFinderContext, self).__init__(server_address, password)
self.send_index: int = 0
self.syncing = False
self.awaiting_bridge = False
# self.game_communication_path: files go in this path to pass data between us and the actual game
if "localappdata" in os.environ:
self.game_communication_path = os.path.expandvars(r"%localappdata%/ChecksFinder")
else:
# not windows. game is an exe so let's see if wine might be around to run it
if "WINEPREFIX" in os.environ:
wineprefix = os.environ["WINEPREFIX"]
elif shutil.which("wine") or shutil.which("wine-stable"):
wineprefix = os.path.expanduser("~/.wine") # default root of wine system data, deep in which is app data
else:
msg = "ChecksFinderClient couldn't detect system type. Unable to infer required game_communication_path"
logger.error("Error: " + msg)
Utils.messagebox("Error", msg, error=True)
sys.exit(1)
self.game_communication_path = os.path.join(
wineprefix,
"drive_c",
os.path.expandvars("users/$USER/Local Settings/Application Data/ChecksFinder"))
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(ChecksFinderContext, self).server_auth(password_requested)
await self.get_username()
await self.send_connect()
async def connection_closed(self):
await super(ChecksFinderContext, self).connection_closed()
for root, dirs, files in os.walk(self.game_communication_path):
for file in files:
if file.find("obtain") <= -1:
os.remove(root + "/" + file)
@property
def endpoints(self):
if self.server:
return [self.server]
else:
return []
async def shutdown(self):
await super(ChecksFinderContext, self).shutdown()
for root, dirs, files in os.walk(self.game_communication_path):
for file in files:
if file.find("obtain") <= -1:
os.remove(root+"/"+file)
def on_package(self, cmd: str, args: dict):
if cmd in {"Connected"}:
if not os.path.exists(self.game_communication_path):
os.makedirs(self.game_communication_path)
for ss in self.checked_locations:
filename = f"send{ss}"
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
f.close()
if cmd in {"ReceivedItems"}:
start_index = args["index"]
if start_index != len(self.items_received):
for item in args['items']:
filename = f"AP_{str(NetworkItem(*item).location)}PLR{str(NetworkItem(*item).player)}.item"
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
f.write(str(NetworkItem(*item).item))
f.close()
if cmd in {"RoomUpdate"}:
if "checked_locations" in args:
for ss in self.checked_locations:
filename = f"send{ss}"
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
f.close()
def run_gui(self):
"""Import kivy UI system and start running it as self.ui_task."""
from kvui import GameManager
class ChecksFinderManager(GameManager):
logging_pairs = [
("Client", "Archipelago")
]
base_title = "Archipelago ChecksFinder Client"
self.ui = ChecksFinderManager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
async def game_watcher(ctx: ChecksFinderContext):
from worlds.checksfinder.Locations import lookup_id_to_name
while not ctx.exit_event.is_set():
if ctx.syncing == True:
sync_msg = [{'cmd': 'Sync'}]
if ctx.locations_checked:
sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)})
await ctx.send_msgs(sync_msg)
ctx.syncing = False
sending = []
victory = False
for root, dirs, files in os.walk(ctx.game_communication_path):
for file in files:
if file.find("send") > -1:
st = file.split("send", -1)[1]
sending = sending+[(int(st))]
if file.find("victory") > -1:
victory = True
ctx.locations_checked = sending
message = [{"cmd": 'LocationChecks', "locations": sending}]
await ctx.send_msgs(message)
if not ctx.finished_game and victory:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
await asyncio.sleep(0.1)
if __name__ == '__main__':
async def main(args):
ctx = ChecksFinderContext(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
progression_watcher = asyncio.create_task(
game_watcher(ctx), name="ChecksFinderProgressionWatcher")
await ctx.exit_event.wait()
ctx.server_address = None
await progression_watcher
await ctx.shutdown()
import colorama
parser = get_base_parser(description="ChecksFinder Client, for text interfacing.")
args, rest = parser.parse_known_args()
colorama.init()
asyncio.run(main(args))
colorama.deinit()

View File

@@ -1,19 +1,37 @@
from __future__ import annotations
import logging
import typing
import asyncio
import urllib.parse
import sys
import typing
import time
import functools
import ModuleUpdate
ModuleUpdate.update()
import websockets
import Utils
if __name__ == "__main__":
Utils.init_logging("TextClient", exception_logger="Client")
from MultiServer import CommandProcessor
from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, color, ClientStatus
from Utils import Version
from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, \
ClientStatus, Permission, NetworkSlot, RawJSONtoTextParser
from Utils import Version, stream_input, async_start
from worlds import network_data_package, AutoWorldRegister
import os
if typing.TYPE_CHECKING:
import kvui
logger = logging.getLogger("Client")
# without terminal, we have to use gui mode
gui_enabled = not sys.stdout or "--nogui" not in sys.argv
class ClientCommandProcessor(CommandProcessor):
def __init__(self, ctx: CommonContext):
@@ -29,25 +47,32 @@ class ClientCommandProcessor(CommandProcessor):
def _cmd_connect(self, address: str = "") -> bool:
"""Connect to a MultiWorld Server"""
self.ctx.server_address = None
asyncio.create_task(self.ctx.connect(address if address else None))
if address:
self.ctx.server_address = None
self.ctx.username = None
elif not self.ctx.server_address:
self.output("Please specify an address.")
return False
async_start(self.ctx.connect(address if address else None), name="connecting")
return True
def _cmd_disconnect(self) -> bool:
"""Disconnect from a MultiWorld Server"""
self.ctx.server_address = None
asyncio.create_task(self.ctx.disconnect())
async_start(self.ctx.disconnect(), name="disconnecting")
return True
def _cmd_received(self) -> bool:
"""List all received items"""
logger.info(f'{len(self.ctx.items_received)} received items:')
self.output(f'{len(self.ctx.items_received)} received items:')
for index, item in enumerate(self.ctx.items_received, 1):
self.output(f"{self.ctx.item_name_getter(item.item)} from {self.ctx.player_names[item.player]}")
self.output(f"{self.ctx.item_names[item.item]} from {self.ctx.player_names[item.player]}")
return True
def _cmd_missing(self) -> bool:
"""List all missing location checks, from your local game state"""
if not self.ctx.game:
self.output("No game set, cannot determine missing checks.")
return False
count = 0
checked_count = 0
for location, location_id in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id.items():
@@ -69,7 +94,26 @@ class ClientCommandProcessor(CommandProcessor):
self.output("No missing location checks found.")
return True
def _cmd_items(self):
"""List all item names for the currently running game."""
if not self.ctx.game:
self.output("No game set, cannot determine existing items.")
return False
self.output(f"Item Names for {self.ctx.game}")
for item_name in AutoWorldRegister.world_types[self.ctx.game].item_name_to_id:
self.output(item_name)
def _cmd_locations(self):
"""List all location names for the currently running game."""
if not self.ctx.game:
self.output("No game set, cannot determine existing locations.")
return False
self.output(f"Location Names for {self.ctx.game}")
for location_name in AutoWorldRegister.world_types[self.ctx.game].location_name_to_id:
self.output(location_name)
def _cmd_ready(self):
"""Send ready status to server."""
self.ctx.ready = not self.ctx.ready
if self.ctx.ready:
state = ClientStatus.CLIENT_READY
@@ -77,26 +121,82 @@ class ClientCommandProcessor(CommandProcessor):
else:
state = ClientStatus.CLIENT_CONNECTED
self.output("Unreadied.")
asyncio.create_task(self.ctx.send_msgs([{"cmd": "StatusUpdate", "status": state}]))
async_start(self.ctx.send_msgs([{"cmd": "StatusUpdate", "status": state}]), name="send StatusUpdate")
def default(self, raw: str):
asyncio.create_task(self.ctx.send_msgs([{"cmd": "Say", "text": raw}]))
raw = self.ctx.on_user_say(raw)
if raw:
async_start(self.ctx.send_msgs([{"cmd": "Say", "text": raw}]), name="send Say")
class CommonContext():
starting_reconnect_delay = 5
current_reconnect_delay = starting_reconnect_delay
command_processor = ClientCommandProcessor
game: None
ui: None
class CommonContext:
# Should be adjusted as needed in subclasses
tags: typing.Set[str] = {"AP"}
game: typing.Optional[str] = None
items_handling: typing.Optional[int] = None
want_slot_data: bool = True # should slot_data be retrieved via Connect
def __init__(self, server_address, password):
# data package
# Contents in flux until connection to server is made, to download correct data for this multiworld.
item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})')
location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})')
# defaults
starting_reconnect_delay: int = 5
current_reconnect_delay: int = starting_reconnect_delay
command_processor: typing.Type[CommandProcessor] = ClientCommandProcessor
ui = None
ui_task: typing.Optional["asyncio.Task[None]"] = None
input_task: typing.Optional["asyncio.Task[None]"] = None
keep_alive_task: typing.Optional["asyncio.Task[None]"] = None
server_task: typing.Optional["asyncio.Task[None]"] = None
autoreconnect_task: typing.Optional["asyncio.Task[None]"] = None
disconnected_intentionally: bool = False
server: typing.Optional[Endpoint] = None
server_version: Version = Version(0, 0, 0)
current_energy_link_value: typing.Optional[int] = None # to display in UI, gets set by server
last_death_link: float = time.time() # last send/received death link on AP layer
# remaining type info
slot_info: typing.Dict[int, NetworkSlot]
server_address: typing.Optional[str]
password: typing.Optional[str]
hint_cost: typing.Optional[int]
player_names: typing.Dict[int, str]
finished_game: bool
ready: bool
auth: typing.Optional[str]
seed_name: typing.Optional[str]
# locations
locations_checked: typing.Set[int] # local state
locations_scouted: typing.Set[int]
items_received: typing.List[NetworkItem]
missing_locations: typing.Set[int] # server state
checked_locations: typing.Set[int] # server state
server_locations: typing.Set[int] # all locations the server knows of, missing_location | checked_locations
locations_info: typing.Dict[int, NetworkItem]
# internals
# current message box through kvui
_messagebox: typing.Optional["kvui.MessageBox"] = None
# message box reporting a loss of connection
_messagebox_connection_loss: typing.Optional["kvui.MessageBox"] = None
def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str]) -> None:
# server state
self.server_address = server_address
self.username = None
self.password = password
self.server_task = None
self.server: typing.Optional[Endpoint] = None
self.server_version = Version(0, 0, 0)
self.hint_cost = None
self.slot_info = {}
self.permissions = {
"release": "disabled",
"collect": "disabled",
"remaining": "disabled",
}
# own state
self.finished_game = False
@@ -106,77 +206,77 @@ class CommonContext():
self.auth = None
self.seed_name = None
self.locations_checked: typing.Set[int] = set()
self.locations_scouted: typing.Set[int] = set()
self.locations_checked = set() # local state
self.locations_scouted = set()
self.items_received = []
self.missing_locations: typing.List[int] = []
self.checked_locations: typing.List[int] = []
self.missing_locations = set() # server state
self.checked_locations = set() # server state
self.server_locations = set() # all locations the server knows of, missing_location | checked_locations
self.locations_info = {}
self.input_queue = asyncio.Queue()
self.input_requests = 0
# game state
self.player_names: typing.Dict[int: str] = {0: "Archipelago"}
self.player_names = {0: "Archipelago"}
self.exit_event = asyncio.Event()
self.watcher_event = asyncio.Event()
self.slow_mode = False
self.jsontotextparser = JSONtoTextParser(self)
self.set_getters(network_data_package)
self.update_data_package(network_data_package)
# execution
self.keep_alive_task = asyncio.create_task(keep_alive(self), name="Bouncy")
@property
def suggested_address(self) -> str:
if self.server_address:
return self.server_address
return Utils.persistent_load().get("client", {}).get("last_server_address", "")
@functools.cached_property
def raw_text_parser(self) -> RawJSONtoTextParser:
return RawJSONtoTextParser(self)
@property
def total_locations(self) -> typing.Optional[int]:
"""Will return None until connected."""
if self.checked_locations or self.missing_locations:
return len(self.checked_locations | self.missing_locations)
async def connection_closed(self):
if self.server and self.server.socket is not None:
await self.server.socket.close()
self.reset_server_state()
def reset_server_state(self):
self.auth = None
self.slot = None
self.team = None
self.items_received = []
self.locations_info = {}
self.server_version = Version(0, 0, 0)
if self.server and self.server.socket is not None:
await self.server.socket.close()
self.server = None
self.server_task = None
self.hint_cost = None
self.permissions = {
"release": "disabled",
"collect": "disabled",
"remaining": "disabled",
}
def set_getters(self, data_package: dict, network=False):
if not network: # local data; check if newer data was already downloaded
local_package = Utils.persistent_load().get("datapackage", {}).get("latest", {})
if local_package and local_package["version"] > network_data_package["version"]:
data_package: dict = local_package
elif network: # check if data from server is newer
if data_package["version"] > network_data_package["version"]:
Utils.persistent_store("datapackage", "latest", network_data_package)
item_lookup: dict = {}
locations_lookup: dict = {}
for game, gamedata in data_package["games"].items():
for item_name, item_id in gamedata["item_name_to_id"].items():
item_lookup[item_id] = item_name
for location_name, location_id in gamedata["location_name_to_id"].items():
locations_lookup[location_id] = location_name
def get_item_name_from_id(code: int):
return item_lookup.get(code, f'Unknown item (ID:{code})')
self.item_name_getter = get_item_name_from_id
def get_location_name_from_address(address: int):
return locations_lookup.get(address, f'Unknown location (ID:{address})')
self.location_name_getter = get_location_name_from_address
@property
def endpoints(self):
if self.server:
return [self.server]
else:
return []
async def disconnect(self):
async def disconnect(self, allow_autoreconnect: bool = False):
if not allow_autoreconnect:
self.disconnected_intentionally = True
if self.cancel_autoreconnect():
logger.info("Cancelled auto-reconnect.")
if self.server and not self.server.socket.closed:
await self.server.socket.close()
if self.server_task is not None:
await self.server_task
async def send_msgs(self, msgs):
async def send_msgs(self, msgs: typing.List[typing.Any]) -> None:
""" `msgs` JSON serializable """
if not self.server or not self.server.socket.open or self.server.socket.closed:
return
await self.server.socket.send(encode(msgs))
@@ -188,19 +288,69 @@ class CommonContext():
def event_invalid_slot(self):
raise Exception('Invalid Slot; please verify that you have connected to the correct world.')
async def server_auth(self, password_requested):
def event_invalid_game(self):
raise Exception('Invalid Game; please verify that you connected with the right game to the correct world.')
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
logger.info('Enter the password required to join this game:')
self.password = await self.console_input()
return self.password
async def console_input(self):
async def get_username(self):
if not self.auth:
self.auth = self.username
if not self.auth:
logger.info('Enter slot name:')
self.auth = await self.console_input()
async def send_connect(self, **kwargs: typing.Any) -> None:
""" send `Connect` packet to log in to server """
payload = {
'cmd': 'Connect',
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
'tags': self.tags, 'items_handling': self.items_handling,
'uuid': Utils.get_unique_identifier(), 'game': self.game, "slot_data": self.want_slot_data,
}
if kwargs:
payload.update(kwargs)
await self.send_msgs([payload])
async def console_input(self) -> str:
if self.ui:
self.ui.focus_textinput()
self.input_requests += 1
return await self.input_queue.get()
async def connect(self, address=None):
async def connect(self, address: typing.Optional[str] = None) -> None:
""" disconnect any previous connection, and open new connection to the server """
await self.disconnect()
self.server_task = asyncio.create_task(server_loop(self, address))
self.server_task = asyncio.create_task(server_loop(self, address), name="server loop")
def cancel_autoreconnect(self) -> bool:
if self.autoreconnect_task:
self.autoreconnect_task.cancel()
self.autoreconnect_task = None
return True
return False
def slot_concerns_self(self, slot) -> bool:
if slot == self.slot:
return True
if slot in self.slot_info:
return self.slot in self.slot_info[slot].group_members
return False
def is_echoed_chat(self, print_json_packet: dict) -> bool:
return print_json_packet.get("type", "") == "Chat" \
and print_json_packet.get("team", None) == self.team \
and print_json_packet.get("slot", None) == self.slot
def is_uninteresting_item_send(self, print_json_packet: dict) -> bool:
"""Helper function for filtering out ItemSend prints that do not concern the local player."""
return print_json_packet.get("type", "") == "ItemSend" \
and not self.slot_concerns_self(print_json_packet["receiving"]) \
and not self.slot_concerns_self(print_json_packet["item"].player)
def on_print(self, args: dict):
logger.info(args["text"])
@@ -216,9 +366,191 @@ class CommonContext():
"""For custom package handling in subclasses."""
pass
def on_user_say(self, text: str) -> typing.Optional[str]:
"""Gets called before sending a Say to the server from the user.
Returned text is sent, or sending is aborted if None is returned."""
return text
async def server_loop(ctx: CommonContext, address=None):
cached_address = None
def update_permissions(self, permissions: typing.Dict[str, int]):
for permission_name, permission_flag in permissions.items():
try:
flag = Permission(permission_flag)
logger.info(f"{permission_name.capitalize()} permission: {flag.name}")
self.permissions[permission_name] = flag.name
except Exception as e: # safeguard against permissions that may be implemented in the future
logger.exception(e)
async def shutdown(self):
self.server_address = ""
self.username = None
self.cancel_autoreconnect()
if self.server and not self.server.socket.closed:
await self.server.socket.close()
if self.server_task:
await self.server_task
while self.input_requests > 0:
self.input_queue.put_nowait(None)
self.input_requests -= 1
self.keep_alive_task.cancel()
if self.ui_task:
await self.ui_task
if self.input_task:
self.input_task.cancel()
# DataPackage
async def prepare_data_package(self, relevant_games: typing.Set[str],
remote_date_package_versions: typing.Dict[str, int],
remote_data_package_checksums: typing.Dict[str, str]):
"""Validate that all data is present for the current multiworld.
Download, assimilate and cache missing data from the server."""
# by documentation any game can use Archipelago locations/items -> always relevant
relevant_games.add("Archipelago")
needed_updates: typing.Set[str] = set()
for game in relevant_games:
if game not in remote_date_package_versions and game not in remote_data_package_checksums:
continue
remote_version: int = remote_date_package_versions.get(game, 0)
remote_checksum: typing.Optional[str] = remote_data_package_checksums.get(game)
if remote_version == 0 and not remote_checksum: # custom data package and no checksum for this game
needed_updates.add(game)
continue
local_version: int = network_data_package["games"].get(game, {}).get("version", 0)
local_checksum: typing.Optional[str] = network_data_package["games"].get(game, {}).get("checksum")
# no action required if local version is new enough
if (not remote_checksum and (remote_version > local_version or remote_version == 0)) \
or remote_checksum != local_checksum:
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)
if needed_updates:
await self.send_msgs([{"cmd": "GetDataPackage", "games": list(needed_updates)}])
def update_game(self, game_package: dict):
for item_name, item_id in game_package["item_name_to_id"].items():
self.item_names[item_id] = item_name
for location_name, location_id in game_package["location_name_to_id"].items():
self.location_names[location_id] = location_name
def update_data_package(self, data_package: dict):
for game, game_data in data_package["games"].items():
self.update_game(game_data)
def consume_network_data_package(self, data_package: dict):
self.update_data_package(data_package)
current_cache = Utils.persistent_load().get("datapackage", {}).get("games", {})
current_cache.update(data_package["games"])
Utils.persistent_store("datapackage", "games", current_cache)
for game, game_data in data_package["games"].items():
Utils.store_data_package_for_checksum(game, game_data)
# DeathLink hooks
def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None:
"""Gets dispatched when a new DeathLink is triggered by another linked player."""
self.last_death_link = max(data["time"], self.last_death_link)
text = data.get("cause", "")
if text:
logger.info(f"DeathLink: {text}")
else:
logger.info(f"DeathLink: Received from {data['source']}")
async def send_death(self, death_text: str = ""):
if self.server and self.server.socket:
logger.info("DeathLink: Sending death to your friends...")
self.last_death_link = time.time()
await self.send_msgs([{
"cmd": "Bounce", "tags": ["DeathLink"],
"data": {
"time": self.last_death_link,
"source": self.player_names[self.slot],
"cause": death_text
}
}])
async def update_death_link(self, death_link: bool):
old_tags = self.tags.copy()
if death_link:
self.tags.add("DeathLink")
else:
self.tags -= {"DeathLink"}
if old_tags != self.tags and self.server and not self.server.socket.closed:
await self.send_msgs([{"cmd": "ConnectUpdate", "tags": self.tags}])
def gui_error(self, title: str, text: typing.Union[Exception, str]) -> typing.Optional["kvui.MessageBox"]:
"""Displays an error messagebox"""
if not self.ui:
return None
title = title or "Error"
from kvui import MessageBox
if self._messagebox:
self._messagebox.dismiss()
# make "Multiple exceptions" look nice
text = str(text).replace('[Errno', '\n[Errno').strip()
# split long messages into title and text
parts = title.split('. ', 1)
if len(parts) == 1:
parts = title.split(', ', 1)
if len(parts) > 1:
text = parts[1] + '\n\n' + text
title = parts[0]
# display error
self._messagebox = MessageBox(title, text, error=True)
self._messagebox.open()
return self._messagebox
def handle_connection_loss(self, msg: str) -> None:
"""Helper for logging and displaying a loss of connection. Must be called from an except block."""
exc_info = sys.exc_info()
logger.exception(msg, exc_info=exc_info, extra={'compact_gui': True})
self._messagebox_connection_loss = self.gui_error(msg, exc_info[1])
def run_gui(self):
"""Import kivy UI system and start running it as self.ui_task."""
from kvui import GameManager
class TextManager(GameManager):
logging_pairs = [
("Client", "Archipelago")
]
base_title = "Archipelago Text Client"
self.ui = TextManager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
def run_cli(self):
if sys.stdin:
# steam overlay breaks when starting console_loop
if 'gameoverlayrenderer' in os.environ.get('LD_PRELOAD', ''):
logger.info("Skipping terminal input, due to conflicting Steam Overlay detected. Please use GUI only.")
else:
self.input_task = asyncio.create_task(console_loop(self), name="Input")
async def keep_alive(ctx: CommonContext, seconds_between_checks=100):
"""some ISPs/network configurations drop TCP connections if no payload is sent (ignore TCP-keep-alive)
so we send a payload to prevent drop and if we were dropped anyway this will cause an auto-reconnect."""
seconds_elapsed = 0
while not ctx.exit_event.is_set():
await asyncio.sleep(1) # short sleep to not block program shutdown
if ctx.server and ctx.slot:
seconds_elapsed += 1
if seconds_elapsed > seconds_between_checks:
await ctx.send_msgs([{"cmd": "Bounce", "slots": [ctx.slot]}])
seconds_elapsed = 0
async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None) -> None:
if ctx.server and ctx.server.socket:
logger.error('Already connected')
return
@@ -231,44 +563,67 @@ async def server_loop(ctx: CommonContext, address=None):
logger.info('Please connect to an Archipelago server.')
return
address = f"ws://{address}" if "://" not in address else address
port = urllib.parse.urlparse(address).port or 38281
ctx.cancel_autoreconnect()
if ctx._messagebox_connection_loss:
ctx._messagebox_connection_loss.dismiss()
ctx._messagebox_connection_loss = None
address = f"ws://{address}" if "://" not in address \
else address.replace("archipelago://", "ws://")
server_url = urllib.parse.urlparse(address)
if server_url.username:
ctx.username = server_url.username
if server_url.password:
ctx.password = server_url.password
port = server_url.port or 38281
def reconnect_hint() -> str:
return ", type /connect to reconnect" if ctx.server_address else ""
logger.info(f'Connecting to Archipelago server at {address}')
try:
socket = await websockets.connect(address, port=port, ping_timeout=None, ping_interval=None)
if ctx.ui is not None:
ctx.ui.update_address_bar(server_url.netloc)
ctx.server = Endpoint(socket)
logger.info('Connected')
ctx.server_address = address
ctx.current_reconnect_delay = ctx.starting_reconnect_delay
ctx.disconnected_intentionally = False
async for data in ctx.server.socket:
for msg in decode(data):
await process_server_cmd(ctx, msg)
logger.warning('Disconnected from multiworld server, type /connect to reconnect')
except ConnectionRefusedError:
if cached_address:
logger.error('Unable to connect to multiworld server at cached address. '
'Please use the connect button above.')
logger.warning(f"Disconnected from multiworld server{reconnect_hint()}")
except websockets.InvalidMessage:
# probably encrypted
if address.startswith("ws://"):
await server_loop(ctx, "ws" + address[1:])
else:
logger.error('Connection refused by the multiworld server')
except (OSError, websockets.InvalidURI):
logger.error('Failed to connect to the multiworld server')
except Exception as e:
logger.error('Lost connection to the multiworld server, type /connect to reconnect')
if not isinstance(e, websockets.WebSocketException):
logger.exception(e)
ctx.handle_connection_loss(f"Lost connection to the multiworld server due to InvalidMessage"
f"{reconnect_hint()}")
except ConnectionRefusedError:
ctx.handle_connection_loss("Connection refused by the server. "
"May not be running Archipelago on that address or port.")
except websockets.InvalidURI:
ctx.handle_connection_loss("Failed to connect to the multiworld server (invalid URI)")
except OSError:
ctx.handle_connection_loss("Failed to connect to the multiworld server")
except Exception:
ctx.handle_connection_loss(f"Lost connection to the multiworld server{reconnect_hint()}")
finally:
await ctx.connection_closed()
if ctx.server_address:
logger.info(f"... reconnecting in {ctx.current_reconnect_delay}s")
asyncio.create_task(server_autoreconnect(ctx))
if ctx.server_address and ctx.username and not ctx.disconnected_intentionally:
logger.info(f"... automatically reconnecting in {ctx.current_reconnect_delay} seconds")
assert ctx.autoreconnect_task is None
ctx.autoreconnect_task = asyncio.create_task(server_autoreconnect(ctx), name="server auto reconnect")
ctx.current_reconnect_delay *= 2
async def server_autoreconnect(ctx: CommonContext):
await asyncio.sleep(ctx.current_reconnect_delay)
if ctx.server_address and ctx.server_task is None:
ctx.server_task = asyncio.create_task(server_loop(ctx))
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
async def process_server_cmd(ctx: CommonContext, args: dict):
@@ -279,7 +634,9 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
raise
if cmd == 'RoomInfo':
if ctx.seed_name and ctx.seed_name != args["seed_name"]:
logger.info("The server is running a different multiworld than your client is. (invalid seed_name)")
msg = "The server is running a different multiworld than your client is. (invalid seed_name)"
logger.info(msg, extra={'compact_gui': True})
ctx.gui_error('Error', msg)
else:
logger.info('--------------------------------')
logger.info('Room Information:')
@@ -292,44 +649,49 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
logger.info("Server protocol tags: " + ", ".join(args["tags"]))
if args['password']:
logger.info('Password required')
logger.info(f"Forfeit setting: {args['forfeit_mode']}")
logger.info(f"Remaining setting: {args['remaining_mode']}")
ctx.update_permissions(args.get("permissions", {}))
logger.info(
f"A !hint costs {args['hint_cost']}% of your total location count as points"
f" and you get {args['location_check_points']}"
f" for each location checked. Use !hint for more information.")
ctx.hint_cost = int(args['hint_cost'])
ctx.check_points = int(args['location_check_points'])
ctx.forfeit_mode = args['forfeit_mode']
ctx.remaining_mode = args['remaining_mode']
if len(args['players']) < 1:
logger.info('No player connected')
else:
args['players'].sort()
current_team = -1
logger.info('Players:')
for network_player in args['players']:
if network_player.team != current_team:
logger.info(f' Team #{network_player.team + 1}')
current_team = network_player.team
logger.info(' %s (Player %d)' % (network_player.alias, network_player.slot))
if args["datapackage_version"] > network_data_package["version"] or args["datapackage_version"] == 0:
await ctx.send_msgs([{"cmd": "GetDataPackage"}])
if "players" in args: # TODO remove when servers sending this are outdated
players = args.get("players", [])
if len(players) < 1:
logger.info('No player connected')
else:
players.sort()
current_team = -1
logger.info('Connected Players:')
for network_player in players:
if network_player.team != current_team:
logger.info(f' Team #{network_player.team + 1}')
current_team = network_player.team
logger.info(' %s (Player %d)' % (network_player.alias, network_player.slot))
# update data package
data_package_versions = args.get("datapackage_versions", {})
data_package_checksums = args.get("datapackage_checksums", {})
await ctx.prepare_data_package(set(args["games"]), data_package_versions, data_package_checksums)
await ctx.server_auth(args['password'])
elif cmd == 'DataPackage':
logger.info("Got new ID/Name Datapackage")
ctx.set_getters(args['data'], network=True)
logger.info("Got new ID/Name DataPackage")
ctx.consume_network_data_package(args['data'])
elif cmd == 'ConnectionRefused':
errors = args["errors"]
if 'InvalidSlot' in errors:
ctx.event_invalid_slot()
elif 'SlotAlreadyTaken' in errors:
raise Exception('Player slot already in use for that team')
elif 'InvalidGame' in errors:
ctx.event_invalid_game()
elif 'IncompatibleVersion' in errors:
raise Exception('Server reported your client version as incompatible')
elif 'InvalidItemsHandling' in errors:
raise Exception('The item handling flags requested by the client are not supported')
# last to check, recoverable problem
elif 'InvalidPassword' in errors:
logger.error('Invalid password')
@@ -341,8 +703,11 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
raise Exception('Connection refused by the multiworld host, no reason provided')
elif cmd == 'Connected':
ctx.username = ctx.auth
ctx.team = args["team"]
ctx.slot = args["slot"]
# int keys get lost in JSON transfer
ctx.slot_info = {int(pid): data for pid, data in args["slot_info"].items()}
ctx.consume_players_package(args["players"])
msgs = []
if ctx.locations_checked:
@@ -360,8 +725,12 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
# This list is used to only send to the server what is reported as ACTUALLY Missing.
# This also serves to allow an easy visual of what locations were already checked previously
# when /missing is used for the client side view of what is missing.
ctx.missing_locations = args["missing_locations"]
ctx.checked_locations = args["checked_locations"]
ctx.missing_locations = set(args["missing_locations"])
ctx.checked_locations = set(args["checked_locations"])
ctx.server_locations = ctx.missing_locations | ctx. checked_locations
server_url = urllib.parse.urlparse(ctx.server_address)
Utils.persistent_store("client", "last_server_address", server_url.netloc)
elif cmd == 'ReceivedItems':
start_index = args["index"]
@@ -380,9 +749,8 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
ctx.watcher_event.set()
elif cmd == 'LocationInfo':
for item, location, player in args['locations']:
if location not in ctx.locations_info:
ctx.locations_info[location] = (item, player)
for item in [NetworkItem(*item) for item in args['locations']]:
ctx.locations_info[item.location] = item
ctx.watcher_event.set()
elif cmd == "RoomUpdate":
@@ -390,6 +758,12 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
ctx.consume_players_package(args["players"])
if "hint_points" in args:
ctx.hint_points = args['hint_points']
if "checked_locations" in args:
checked = set(args["checked_locations"])
ctx.checked_locations |= checked
ctx.missing_locations -= checked
if "permissions" in args:
ctx.update_permissions(args["permissions"])
elif cmd == 'Print':
ctx.on_print(args)
@@ -401,8 +775,15 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
logger.warning(f"Invalid Packet of {args['type']}: {args['text']}")
elif cmd == "Bounced":
pass
tags = args.get("tags", [])
# we can skip checking "DeathLink" in ctx.tags, as otherwise we wouldn't have been send this
if "DeathLink" in tags and ctx.last_death_link != args["data"]["time"]:
ctx.on_deathlink(args["data"])
elif cmd == "SetReply":
if args["key"] == "EnergyLink":
ctx.current_energy_link_value = args["value"]
if ctx.ui:
ctx.ui.set_new_energy_link_value()
else:
logger.debug(f"unknown command {cmd}")
@@ -410,14 +791,13 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
async def console_loop(ctx: CommonContext):
import sys
commandprocessor = ctx.command_processor(ctx)
queue = asyncio.Queue()
stream_input(sys.stdin, queue)
while not ctx.exit_event.is_set():
try:
input_text = await asyncio.get_event_loop().run_in_executor(
None, sys.stdin.readline
)
input_text = input_text.strip()
input_text = await queue.get()
queue.task_done()
if ctx.input_requests > 0:
ctx.input_requests -= 1
@@ -428,3 +808,71 @@ async def console_loop(ctx: CommonContext):
commandprocessor(input_text)
except Exception as e:
logger.exception(e)
def get_base_parser(description: typing.Optional[str] = None):
import argparse
parser = argparse.ArgumentParser(description=description)
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
parser.add_argument('--password', default=None, help='Password of the multiworld host.')
if sys.stdout: # If terminal output exists, offer gui-less mode
parser.add_argument('--nogui', default=False, action='store_true', help="Turns off Client GUI.")
return parser
if __name__ == '__main__':
# Text Mode to use !hint and such with games that have no text entry
class TextContext(CommonContext):
tags = {"AP", "TextOnly"}
game = "" # empty matches any game since 0.3.2
items_handling = 0b111 # receive all items for /received
want_slot_data = False # Can't use game specific slot_data
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(TextContext, self).server_auth(password_requested)
await self.get_username()
await self.send_connect()
def on_package(self, cmd: str, args: dict):
if cmd == "Connected":
self.game = self.slot_info[self.slot].game
async def disconnect(self, allow_autoreconnect: bool = False):
self.game = ""
await super().disconnect(allow_autoreconnect)
async def main(args):
ctx = TextContext(args.connect, args.password)
ctx.auth = args.name
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
await ctx.exit_event.wait()
await ctx.shutdown()
import colorama
parser = get_base_parser(description="Gameless Archipelago Client, for text interfacing.")
parser.add_argument('--name', default=None, help="Slot Name to connect as.")
parser.add_argument("url", nargs="?", help="Archipelago connection url")
args = parser.parse_args()
if args.url:
url = urllib.parse.urlparse(args.url)
args.connect = url.netloc
if url.username:
args.name = urllib.parse.unquote(url.username)
if url.password:
args.password = urllib.parse.unquote(url.password)
colorama.init()
asyncio.run(main(args))
colorama.deinit()

267
FF1Client.py Normal file
View File

@@ -0,0 +1,267 @@
import asyncio
import copy
import json
import time
from asyncio import StreamReader, StreamWriter
from typing import List
import Utils
from Utils import async_start
from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \
get_base_parser
SYSTEM_MESSAGE_ID = 0
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart ff1_connector.lua"
CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure ff1_connector.lua is running"
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart ff1_connector.lua"
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
CONNECTION_CONNECTED_STATUS = "Connected"
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
DISPLAY_MSGS = True
class FF1CommandProcessor(ClientCommandProcessor):
def __init__(self, ctx: CommonContext):
super().__init__(ctx)
def _cmd_nes(self):
"""Check NES Connection State"""
if isinstance(self.ctx, FF1Context):
logger.info(f"NES Status: {self.ctx.nes_status}")
def _cmd_toggle_msgs(self):
"""Toggle displaying messages in bizhawk"""
global DISPLAY_MSGS
DISPLAY_MSGS = not DISPLAY_MSGS
logger.info(f"Messages are now {'enabled' if DISPLAY_MSGS else 'disabled'}")
class FF1Context(CommonContext):
command_processor = FF1CommandProcessor
game = 'Final Fantasy'
items_handling = 0b111 # full remote
def __init__(self, server_address, password):
super().__init__(server_address, password)
self.nes_streams: (StreamReader, StreamWriter) = None
self.nes_sync_task = None
self.messages = {}
self.locations_array = None
self.nes_status = CONNECTION_INITIAL_STATUS
self.awaiting_rom = False
self.display_msgs = True
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(FF1Context, self).server_auth(password_requested)
if not self.auth:
self.awaiting_rom = True
logger.info('Awaiting connection to NES to get Player information')
return
await self.send_connect()
def _set_message(self, msg: str, msg_id: int):
if DISPLAY_MSGS:
self.messages[time.time(), msg_id] = msg
def on_package(self, cmd: str, args: dict):
if cmd == 'Connected':
async_start(parse_locations(self.locations_array, self, True))
elif cmd == 'Print':
msg = args['text']
if ': !' not in msg:
self._set_message(msg, SYSTEM_MESSAGE_ID)
def on_print_json(self, args: dict):
if self.ui:
self.ui.print_json(copy.deepcopy(args["data"]))
else:
text = self.jsontotextparser(copy.deepcopy(args["data"]))
logger.info(text)
relevant = args.get("type", None) in {"Hint", "ItemSend"}
if relevant:
item = args["item"]
# goes to this world
if self.slot_concerns_self(args["receiving"]):
relevant = True
# found in this world
elif self.slot_concerns_self(item.player):
relevant = True
# not related
else:
relevant = False
if relevant:
item = args["item"]
msg = self.raw_text_parser(copy.deepcopy(args["data"]))
self._set_message(msg, item.item)
def run_gui(self):
from kvui import GameManager
class FF1Manager(GameManager):
logging_pairs = [
("Client", "Archipelago")
]
base_title = "Archipelago Final Fantasy 1 Client"
self.ui = FF1Manager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
def get_payload(ctx: FF1Context):
current_time = time.time()
return json.dumps(
{
"items": [item.item for item in ctx.items_received],
"messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items()
if key[0] > current_time - 10}
}
)
async def parse_locations(locations_array: List[int], ctx: FF1Context, force: bool):
if locations_array == ctx.locations_array and not force:
return
else:
# print("New values")
ctx.locations_array = locations_array
locations_checked = []
if len(locations_array) > 0xFE and locations_array[0xFE] & 0x02 != 0 and not ctx.finished_game:
await ctx.send_msgs([
{"cmd": "StatusUpdate",
"status": 30}
])
ctx.finished_game = True
for location in ctx.missing_locations:
# index will be - 0x100 or 0x200
index = location
if location < 0x200:
# Location is a chest
index -= 0x100
flag = 0x04
else:
# Location is an NPC
index -= 0x200
flag = 0x02
# print(f"Location: {ctx.location_names[location]}")
# print(f"Index: {str(hex(index))}")
# print(f"value: {locations_array[index] & flag != 0}")
if locations_array[index] & flag != 0:
locations_checked.append(location)
if locations_checked:
# print([ctx.location_names[location] for location in locations_checked])
await ctx.send_msgs([
{"cmd": "LocationChecks",
"locations": locations_checked}
])
async def nes_sync_task(ctx: FF1Context):
logger.info("Starting nes connector. Use /nes for status information")
while not ctx.exit_event.is_set():
error_status = None
if ctx.nes_streams:
(reader, writer) = ctx.nes_streams
msg = get_payload(ctx).encode()
writer.write(msg)
writer.write(b'\n')
try:
await asyncio.wait_for(writer.drain(), timeout=1.5)
try:
# Data will return a dict with up to two fields:
# 1. A keepalive response of the Players Name (always)
# 2. An array representing the memory values of the locations area (if in game)
data = await asyncio.wait_for(reader.readline(), timeout=5)
data_decoded = json.loads(data.decode())
# print(data_decoded)
if ctx.game is not None and 'locations' in data_decoded:
# Not just a keep alive ping, parse
async_start(parse_locations(data_decoded['locations'], ctx, False))
if not ctx.auth:
ctx.auth = ''.join([chr(i) for i in data_decoded['playerName'] if i != 0])
if ctx.auth == '':
logger.info("Invalid ROM detected. No player name built into the ROM. Please regenerate"
"the ROM using the same link but adding your slot name")
if ctx.awaiting_rom:
await ctx.server_auth(False)
except asyncio.TimeoutError:
logger.debug("Read Timed Out, Reconnecting")
error_status = CONNECTION_TIMING_OUT_STATUS
writer.close()
ctx.nes_streams = None
except ConnectionResetError as e:
logger.debug("Read failed due to Connection Lost, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.nes_streams = None
except TimeoutError:
logger.debug("Connection Timed Out, Reconnecting")
error_status = CONNECTION_TIMING_OUT_STATUS
writer.close()
ctx.nes_streams = None
except ConnectionResetError:
logger.debug("Connection Lost, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.nes_streams = None
if ctx.nes_status == CONNECTION_TENTATIVE_STATUS:
if not error_status:
logger.info("Successfully Connected to NES")
ctx.nes_status = CONNECTION_CONNECTED_STATUS
else:
ctx.nes_status = f"Was tentatively connected but error occured: {error_status}"
elif error_status:
ctx.nes_status = error_status
logger.info("Lost connection to nes and attempting to reconnect. Use /nes for status updates")
else:
try:
logger.debug("Attempting to connect to NES")
ctx.nes_streams = await asyncio.wait_for(asyncio.open_connection("localhost", 52980), timeout=10)
ctx.nes_status = CONNECTION_TENTATIVE_STATUS
except TimeoutError:
logger.debug("Connection Timed Out, Trying Again")
ctx.nes_status = CONNECTION_TIMING_OUT_STATUS
continue
except ConnectionRefusedError:
logger.debug("Connection Refused, Trying Again")
ctx.nes_status = CONNECTION_REFUSED_STATUS
continue
if __name__ == '__main__':
# Text Mode to use !hint and such with games that have no text entry
Utils.init_logging("FF1Client")
options = Utils.get_options()
DISPLAY_MSGS = options["ffr_options"]["display_msgs"]
async def main(args):
ctx = FF1Context(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
ctx.nes_sync_task = asyncio.create_task(nes_sync_task(ctx), name="NES Sync")
await ctx.exit_event.wait()
ctx.server_address = None
await ctx.shutdown()
if ctx.nes_sync_task:
await ctx.nes_sync_task
import colorama
parser = get_base_parser()
args = parser.parse_args()
colorama.init()
asyncio.run(main(args))
colorama.deinit()

View File

@@ -4,38 +4,45 @@ import logging
import json
import string
import copy
import sys
import re
import subprocess
import factorio_rcon
import sys
import time
import random
import typing
import ModuleUpdate
ModuleUpdate.update()
import factorio_rcon
import colorama
import asyncio
from queue import Queue
from CommonClient import CommonContext, server_loop, console_loop, ClientCommandProcessor, logger
from MultiServer import mark_raw
import Utils
import random
def check_stdin() -> None:
if Utils.is_windows and sys.stdin:
print("WARNING: Console input is not routed reliably on Windows, use the GUI instead.")
if __name__ == "__main__":
Utils.init_logging("FactorioClient", exception_logger="Client")
check_stdin()
from CommonClient import CommonContext, server_loop, ClientCommandProcessor, logger, gui_enabled, get_base_parser
from MultiServer import mark_raw
from NetUtils import NetworkItem, ClientStatus, JSONtoTextParser, JSONMessagePart
from Utils import async_start
from worlds.factorio import Factorio
os.makedirs("logs", exist_ok=True)
# Log to file in gui case
if getattr(sys, "frozen", False) and not "--nogui" in sys.argv:
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO,
filename=os.path.join("logs", "FactorioClient.txt"), filemode="w", force=True)
else:
logging.basicConfig(format='[%(name)s]: %(message)s', level=logging.INFO, force=True)
logging.getLogger().addHandler(logging.FileHandler(os.path.join("logs", "FactorioClient.txt"), "w"))
gui_enabled = Utils.is_frozen() or "--nogui" not in sys.argv
class FactorioCommandProcessor(ClientCommandProcessor):
ctx: FactorioContext
def _cmd_energy_link(self):
"""Print the status of the energy link."""
self.output(f"Energy Link: {self.ctx.energy_link_status}")
@mark_raw
def _cmd_factorio(self, text: str) -> bool:
"""Send the following command to the bound Factorio Server."""
@@ -52,79 +59,173 @@ class FactorioCommandProcessor(ClientCommandProcessor):
"""Manually trigger a resync."""
self.ctx.awaiting_bridge = True
def _cmd_toggle_send_filter(self):
"""Toggle filtering of item sends that get displayed in-game to only those that involve you."""
self.ctx.toggle_filter_item_sends()
def _cmd_toggle_chat(self):
"""Toggle sending of chat messages from players on the Factorio server to Archipelago."""
self.ctx.toggle_bridge_chat_out()
class FactorioContext(CommonContext):
command_processor = FactorioCommandProcessor
game = "Factorio"
items_handling = 0b111 # full remote
# updated by spinup server
mod_version: Utils.Version = Utils.Version(0, 0, 0)
def __init__(self, server_address, password):
super(FactorioContext, self).__init__(server_address, password)
self.send_index = 0
self.send_index: int = 0
self.rcon_client = None
self.awaiting_bridge = False
self.write_data_path = None
self.death_link_tick: int = 0 # last send death link on Factorio layer
self.factorio_json_text_parser = FactorioJSONtoTextParser(self)
self.energy_link_increment = 0
self.last_deplete = 0
self.filter_item_sends: bool = False
self.multiplayer: bool = False # whether multiple different players have connected
self.bridge_chat_out: bool = True
async def server_auth(self, password_requested):
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(FactorioContext, self).server_auth(password_requested)
if not self.auth:
if self.rcon_client:
get_info(self, self.rcon_client) # retrieve current auth code
else:
raise Exception("Cannot connect to a server with unknown own identity, "
"bridge to Factorio first.")
if self.rcon_client:
await get_info(self, self.rcon_client) # retrieve current auth code
else:
raise Exception("Cannot connect to a server with unknown own identity, "
"bridge to Factorio first.")
await self.send_msgs([{"cmd": 'Connect',
'password': self.password, 'name': self.auth, 'version': Utils.version_tuple,
'tags': ['AP'],
'uuid': Utils.get_unique_identifier(), 'game': "Factorio"
}])
await self.send_connect()
def on_print(self, args: dict):
logger.info(args["text"])
super(FactorioContext, self).on_print(args)
if self.rcon_client:
self.print_to_game(args['text'])
if not args['text'].startswith(self.player_names[self.slot] + ":"):
self.print_to_game(args['text'])
def on_print_json(self, args: dict):
if self.rcon_client:
text = self.factorio_json_text_parser(copy.deepcopy(args["data"]))
self.print_to_game(text)
if (not self.filter_item_sends or not self.is_uninteresting_item_send(args)) \
and not self.is_echoed_chat(args):
text = self.factorio_json_text_parser(copy.deepcopy(args["data"]))
if not text.startswith(self.player_names[self.slot] + ":"): # TODO: Remove string heuristic in the future.
self.print_to_game(text)
super(FactorioContext, self).on_print_json(args)
@property
def savegame_name(self) -> str:
return f"AP_{self.seed_name}_{self.auth}.zip"
return f"AP_{self.seed_name}_{self.auth}_Save.zip"
def print_to_game(self, text):
# TODO: remove around version 0.2
if self.mod_version < Utils.Version(0, 1, 6):
text = text.replace('"', '')
self.rcon_client.send_command(f"/sc game.print(\"[font=default-large-bold]Archipelago:[/font] "
f"{text}\")")
self.rcon_client.send_command(f"/ap-print [font=default-large-bold]Archipelago:[/font] "
f"{text}")
@property
def energy_link_status(self) -> str:
if not self.energy_link_increment:
return "Disabled"
elif self.current_energy_link_value is None:
return "Standby"
else:
self.rcon_client.send_command(f"/ap-print [font=default-large-bold]Archipelago:[/font] "
f"{text}")
return f"{Utils.format_SI_prefix(self.current_energy_link_value)}J"
def on_deathlink(self, data: dict):
if self.rcon_client:
self.rcon_client.send_command(f"/ap-deathlink {data['source']}")
super(FactorioContext, self).on_deathlink(data)
def on_package(self, cmd: str, args: dict):
if cmd == "Connected":
if cmd in {"Connected", "RoomUpdate"}:
# catch up sync anything that is already cleared.
for tech in args["checked_locations"]:
item_name = f"ap-{tech}-"
self.rcon_client.send_command(f'/ap-get-technology {item_name}\t-1')
if "checked_locations" in args and args["checked_locations"]:
self.rcon_client.send_commands({item_name: f'/ap-get-technology ap-{item_name}-\t-1' for
item_name in args["checked_locations"]})
if cmd == "Connected" and self.energy_link_increment:
async_start(self.send_msgs([{
"cmd": "SetNotify", "keys": ["EnergyLink"]
}]))
elif cmd == "SetReply":
if args["key"] == "EnergyLink":
if self.energy_link_increment and args.get("last_deplete", -1) == self.last_deplete:
# it's our deplete request
gained = int(args["original_value"] - args["value"])
gained_text = Utils.format_SI_prefix(gained) + "J"
if gained:
logger.debug(f"EnergyLink: Received {gained_text}. "
f"{Utils.format_SI_prefix(args['value'])}J remaining.")
self.rcon_client.send_command(f"/ap-energylink {gained}")
def on_user_say(self, text: str) -> typing.Optional[str]:
# Mirror chat sent from the UI to the Factorio server.
self.print_to_game(f"{self.player_names[self.slot]}: {text}")
return text
async def chat_from_factorio(self, user: str, message: str) -> None:
if not self.bridge_chat_out:
return
# Pass through commands
if message.startswith("!"):
await self.send_msgs([{"cmd": "Say", "text": message}])
return
# Omit messages that contain local coordinates
if "[gps=" in message:
return
prefix = f"({user}) " if self.multiplayer else ""
await self.send_msgs([{"cmd": "Say", "text": f"{prefix}{message}"}])
def toggle_filter_item_sends(self) -> None:
self.filter_item_sends = not self.filter_item_sends
if self.filter_item_sends:
announcement = "Item sends are now filtered."
else:
announcement = "Item sends are no longer filtered."
logger.info(announcement)
self.print_to_game(announcement)
def toggle_bridge_chat_out(self) -> None:
self.bridge_chat_out = not self.bridge_chat_out
if self.bridge_chat_out:
announcement = "Chat is now bridged to Archipelago."
else:
announcement = "Chat is no longer bridged to Archipelago."
logger.info(announcement)
self.print_to_game(announcement)
def run_gui(self):
from kvui import GameManager
class FactorioManager(GameManager):
logging_pairs = [
("Client", "Archipelago"),
("FactorioServer", "Factorio Server Log"),
("FactorioWatcher", "Bridge Data Log"),
]
base_title = "Archipelago Factorio Client"
self.ui = FactorioManager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
async def game_watcher(ctx: FactorioContext):
bridge_logger = logging.getLogger("FactorioWatcher")
from worlds.factorio.Technologies import lookup_id_to_name
next_bridge = time.perf_counter() + 1
try:
while not ctx.exit_event.is_set():
if ctx.awaiting_bridge and ctx.rcon_client:
# TODO: restore on-demand refresh
if ctx.rcon_client and time.perf_counter() > next_bridge:
next_bridge = time.perf_counter() + 1
ctx.awaiting_bridge = False
data = json.loads(ctx.rcon_client.send_command("/ap-sync"))
if data["slot_name"] != ctx.auth:
if not ctx.auth:
pass # auth failed, wait for new attempt
elif data["slot_name"] != ctx.auth:
bridge_logger.warning(f"Connected World is not the expected one {data['slot_name']} != {ctx.auth}")
elif data["seed_name"] != ctx.seed_name:
bridge_logger.warning(
@@ -134,18 +235,50 @@ async def game_watcher(ctx: FactorioContext):
research_data = data["research_done"]
research_data = {int(tech_name.split("-")[1]) for tech_name in research_data}
victory = data["victory"]
await ctx.update_death_link(data["death_link"])
ctx.multiplayer = data.get("multiplayer", False)
if not ctx.finished_game and victory:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
if ctx.locations_checked != research_data:
bridge_logger.info(
bridge_logger.debug(
f"New researches done: "
f"{[lookup_id_to_name[rid] for rid in research_data - ctx.locations_checked]}")
f"{[ctx.location_names[rid] for rid in research_data - ctx.locations_checked]}")
ctx.locations_checked = research_data
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(research_data)}])
await asyncio.sleep(1)
death_link_tick = data.get("death_link_tick", 0)
if death_link_tick != ctx.death_link_tick:
ctx.death_link_tick = death_link_tick
if "DeathLink" in ctx.tags:
async_start(ctx.send_death())
if ctx.energy_link_increment:
in_world_bridges = data["energy_bridges"]
if in_world_bridges:
in_world_energy = data["energy"]
if in_world_energy < (ctx.energy_link_increment * in_world_bridges):
# attempt to refill
ctx.last_deplete = time.time()
async_start(ctx.send_msgs([{
"cmd": "Set", "key": "EnergyLink", "operations":
[{"operation": "add", "value": -ctx.energy_link_increment * in_world_bridges},
{"operation": "max", "value": 0}],
"last_deplete": ctx.last_deplete
}]))
# Above Capacity - (len(Bridges) * ENERGY_INCREMENT)
elif in_world_energy > (in_world_bridges * ctx.energy_link_increment * 5) - \
ctx.energy_link_increment*in_world_bridges:
value = ctx.energy_link_increment * in_world_bridges
async_start(ctx.send_msgs([{
"cmd": "Set", "key": "EnergyLink", "operations":
[{"operation": "add", "value": value}]
}]))
ctx.rcon_client.send_command(
f"/ap-energylink -{value}")
logger.debug(f"EnergyLink: Sent {Utils.format_SI_prefix(value)}J")
await asyncio.sleep(0.1)
except Exception as e:
logging.exception(e)
@@ -153,6 +286,8 @@ async def game_watcher(ctx: FactorioContext):
def stream_factorio_output(pipe, queue, process):
pipe.reconfigure(errors="replace")
def queuer():
while process.poll() is None:
text = pipe.readline().strip()
@@ -185,24 +320,40 @@ async def factorio_server_watcher(ctx: FactorioContext):
stream_factorio_output(factorio_process.stderr, factorio_queue, factorio_process)
try:
while not ctx.exit_event.is_set():
if factorio_process.poll():
if factorio_process.poll() is not None:
factorio_server_logger.info("Factorio server has exited.")
ctx.exit_event.set()
while not factorio_queue.empty():
msg = factorio_queue.get()
factorio_server_logger.info(msg)
factorio_queue.task_done()
if not ctx.rcon_client and "Starting RCON interface at IP ADDR:" in msg:
ctx.rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
# TODO: remove around version 0.2
if ctx.mod_version < Utils.Version(0, 1, 6):
ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')")
ctx.rcon_client.send_command("/sc game.print('Starting Archipelago Bridge')")
if not ctx.server:
logger.info("Established bridge to Factorio Server. "
"Ready to connect to Archipelago via /connect")
check_stdin()
if not ctx.awaiting_bridge and "Archipelago Bridge Data available for game tick " in msg:
ctx.awaiting_bridge = True
factorio_server_logger.debug(msg)
elif re.match(r"^[0-9.]+ Script @[^ ]+\.lua:\d+: Player command energy-link$", msg):
factorio_server_logger.debug(msg)
ctx.print_to_game(f"Energy Link: {ctx.energy_link_status}")
elif re.match(r"^[0-9.]+ Script @[^ ]+\.lua:\d+: Player command toggle-ap-send-filter$", msg):
factorio_server_logger.debug(msg)
ctx.toggle_filter_item_sends()
elif re.match(r"^[0-9.]+ Script @[^ ]+\.lua:\d+: Player command toggle-ap-chat$", msg):
factorio_server_logger.debug(msg)
ctx.toggle_bridge_chat_out()
else:
factorio_server_logger.info(msg)
match = re.match(r"^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d \[CHAT\] ([^:]+): (.*)$", msg)
if match:
await ctx.chat_from_factorio(match.group(1), match.group(2))
if ctx.rcon_client:
commands = {}
while ctx.send_index < len(ctx.items_received):
transfer_item: NetworkItem = ctx.items_received[ctx.send_index]
item_id = transfer_item.item
@@ -212,28 +363,59 @@ async def factorio_server_watcher(ctx: FactorioContext):
else:
item_name = Factorio.item_id_to_name[item_id]
factorio_server_logger.info(f"Sending {item_name} to Nauvis from {player_name}.")
ctx.rcon_client.send_command(f'/ap-get-technology {item_name}\t{ctx.send_index}\t{player_name}')
commands[ctx.send_index] = f'/ap-get-technology {item_name}\t{ctx.send_index}\t{player_name}'
ctx.send_index += 1
if commands:
ctx.rcon_client.send_commands(commands)
await asyncio.sleep(0.1)
except Exception as e:
logging.exception(e)
logging.error("Aborted Factorio Server Bridge")
ctx.rcon_client = None
ctx.exit_event.set()
finally:
factorio_process.terminate()
factorio_process.wait(5)
if factorio_process.poll() is not None:
if ctx.rcon_client:
ctx.rcon_client.close()
ctx.rcon_client = None
return
sent_quit = False
if ctx.rcon_client:
# Attempt clean quit through RCON.
try:
ctx.rcon_client.send_command("/quit")
except factorio_rcon.RCONNetworkError:
pass
else:
sent_quit = True
ctx.rcon_client.close()
ctx.rcon_client = None
if not sent_quit:
# Attempt clean quit using SIGTERM. (Note that on Windows this kills the process instead.)
factorio_process.terminate()
try:
factorio_process.wait(10)
except subprocess.TimeoutExpired:
factorio_process.kill()
def get_info(ctx, rcon_client):
async def get_info(ctx: FactorioContext, rcon_client: factorio_rcon.RCONClient):
info = json.loads(rcon_client.send_command("/ap-rcon-info"))
ctx.auth = info["slot_name"]
ctx.seed_name = info["seed_name"]
# 0.2.0 addition, not present earlier
death_link = bool(info.get("death_link", False))
ctx.energy_link_increment = info.get("energy_link", 0)
logger.debug(f"Energy Link Increment: {ctx.energy_link_increment}")
if ctx.energy_link_increment and ctx.ui:
ctx.ui.enable_energy_link()
await ctx.update_death_link(death_link)
async def factorio_spinup_server(ctx: FactorioContext):
async def factorio_spinup_server(ctx: FactorioContext) -> bool:
savegame_name = os.path.abspath("Archipelago.zip")
if not os.path.exists(savegame_name):
logger.info(f"Creating savegame {savegame_name}")
@@ -259,115 +441,113 @@ async def factorio_spinup_server(ctx: FactorioContext):
if "Loading mod AP-" in msg and msg.endswith("(data.lua)"):
parts = msg.split()
ctx.mod_version = Utils.Version(*(int(number) for number in parts[-2].split(".")))
elif "Write data path: " in msg:
ctx.write_data_path = Utils.get_text_between(msg, "Write data path: ", " [")
if "AppData" in ctx.write_data_path:
logger.warning("It appears your mods are loaded from Appdata, "
"this can lead to problems with multiple Factorio instances. "
"If this is the case, you will get a file locked error running Factorio.")
if not rcon_client and "Starting RCON interface at IP ADDR:" in msg:
rcon_client = factorio_rcon.RCONClient("localhost", rcon_port, rcon_password)
get_info(ctx, rcon_client)
if ctx.mod_version == ctx.__class__.mod_version:
raise Exception("No Archipelago mod was loaded. Aborting.")
await get_info(ctx, rcon_client)
await asyncio.sleep(0.01)
if ctx.mod_version == ctx.__class__.mod_version:
raise Exception("No Archipelago mod was loaded. Aborting.")
except Exception as e:
logging.exception(e)
logging.error("Aborted Factorio Server Bridge")
logger.exception(e, extra={"compact_gui": True})
msg = "Aborted Factorio Server Bridge"
logger.error(msg)
ctx.gui_error(msg, e)
ctx.exit_event.set()
else:
logger.info(f"Got World Information from AP Mod {tuple(ctx.mod_version)} for seed {ctx.seed_name} in slot {ctx.auth}")
logger.info(
f"Got World Information from AP Mod {tuple(ctx.mod_version)} for seed {ctx.seed_name} in slot {ctx.auth}")
return True
finally:
factorio_process.terminate()
factorio_process.wait(5)
return False
async def main(args):
ctx = FactorioContext(args.connect, args.password)
ctx.filter_item_sends = initial_filter_item_sends
ctx.bridge_chat_out = initial_bridge_chat_out
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
if gui_enabled:
input_task = None
from kvui import FactorioManager
ctx.ui = FactorioManager(ctx)
ui_task = asyncio.create_task(ctx.ui.async_run(), name="UI")
else:
input_task = asyncio.create_task(console_loop(ctx), name="Input")
ui_task = None
ctx.run_gui()
ctx.run_cli()
factorio_server_task = asyncio.create_task(factorio_spinup_server(ctx), name="FactorioSpinupServer")
await factorio_server_task
factorio_server_task = asyncio.create_task(factorio_server_watcher(ctx), name="FactorioServer")
progression_watcher = asyncio.create_task(
game_watcher(ctx), name="FactorioProgressionWatcher")
successful_launch = await factorio_server_task
if successful_launch:
factorio_server_task = asyncio.create_task(factorio_server_watcher(ctx), name="FactorioServer")
progression_watcher = asyncio.create_task(
game_watcher(ctx), name="FactorioProgressionWatcher")
await ctx.exit_event.wait()
ctx.server_address = None
await ctx.exit_event.wait()
ctx.server_address = None
await progression_watcher
await factorio_server_task
await progression_watcher
await factorio_server_task
if ctx.server and not ctx.server.socket.closed:
await ctx.server.socket.close()
if ctx.server_task:
await ctx.server_task
while ctx.input_requests > 0:
ctx.input_queue.put_nowait(None)
ctx.input_requests -= 1
if ui_task:
await ui_task
if input_task:
input_task.cancel()
await ctx.shutdown()
class FactorioJSONtoTextParser(JSONtoTextParser):
def _handle_color(self, node: JSONMessagePart):
colors = node["color"].split(";")
for color in colors:
if color in {"red", "green", "blue", "orange", "yellow", "pink", "purple", "white", "black", "gray",
"brown", "cyan", "acid"}:
node["text"] = f"[color={color}]{node['text']}[/color]"
return self._handle_text(node)
elif color == "magenta":
node["text"] = f"[color=pink]{node['text']}[/color]"
if color in self.color_codes:
node["text"] = f"[color=#{self.color_codes[color]}]{node['text']}[/color]"
return self._handle_text(node)
return self._handle_text(node)
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(description="Optional arguments to FactorioClient follow. "
"Remaining arguments get passed into bound Factorio instance."
"Refer to Factorio --help for those.")
parser = get_base_parser(description="Optional arguments to FactorioClient follow. "
"Remaining arguments get passed into bound Factorio instance."
"Refer to Factorio --help for those.")
parser.add_argument('--rcon-port', default='24242', type=int, help='Port to use to communicate with Factorio')
parser.add_argument('--rcon-password', help='Password to authenticate with RCON.')
parser.add_argument('--connect', default=None, help='Address of the multiworld host.')
parser.add_argument('--password', default=None, help='Password of the multiworld host.')
if not Utils.is_frozen(): # Frozen state has no cmd window in the first place
parser.add_argument('--nogui', default=False, action='store_true', help="Turns off Client GUI.")
parser.add_argument('--server-settings', help='Factorio server settings configuration file.')
args, rest = parser.parse_known_args()
colorama.init()
rcon_port = args.rcon_port
rcon_password = args.rcon_password if args.rcon_password else ''.join(random.choice(string.ascii_letters) for x in range(32))
rcon_password = args.rcon_password if args.rcon_password else ''.join(
random.choice(string.ascii_letters) for x in range(32))
factorio_server_logger = logging.getLogger("FactorioServer")
options = Utils.get_options()
executable = options["factorio_options"]["executable"]
bin_dir = os.path.dirname(executable)
if not os.path.exists(bin_dir):
raise FileNotFoundError(f"Path {bin_dir} does not exist or could not be accessed.")
if not os.path.isdir(bin_dir):
raise NotADirectoryError(f"Path {bin_dir} is not a directory.")
if not os.path.exists(executable):
if os.path.exists(executable + ".exe"):
server_settings = args.server_settings if args.server_settings else options["factorio_options"].get("server_settings", None)
if server_settings:
server_settings = os.path.abspath(server_settings)
if not isinstance(options["factorio_options"]["filter_item_sends"], bool):
logging.warning(f"Warning: Option filter_item_sends should be a bool.")
initial_filter_item_sends = bool(options["factorio_options"]["filter_item_sends"])
if not isinstance(options["factorio_options"]["bridge_chat_out"], bool):
logging.warning(f"Warning: Option bridge_chat_out should be a bool.")
initial_bridge_chat_out = bool(options["factorio_options"]["bridge_chat_out"])
if not os.path.exists(os.path.dirname(executable)):
raise FileNotFoundError(f"Path {os.path.dirname(executable)} does not exist or could not be accessed.")
if os.path.isdir(executable): # user entered a path to a directory, let's find the executable therein
executable = os.path.join(executable, "factorio")
if not os.path.isfile(executable):
if os.path.isfile(executable + ".exe"):
executable = executable + ".exe"
else:
raise FileNotFoundError(f"Path {executable} is not an executable file.")
server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, *rest)
if server_settings and os.path.isfile(server_settings):
server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, "--server-settings", server_settings, *rest)
else:
server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password, *rest)
loop = asyncio.get_event_loop()
loop.run_until_complete(main(args))
loop.close()
asyncio.run(main(args))
colorama.deinit()

888
Fill.py

File diff suppressed because it is too large Load Diff

View File

@@ -1,54 +1,58 @@
from __future__ import annotations
import argparse
import logging
import random
import urllib.request
import urllib.parse
import typing
import os
from collections import Counter
import random
import string
import urllib.parse
import urllib.request
from collections import Counter, ChainMap
from typing import Dict, Tuple, Callable, Any, Union
import ModuleUpdate
ModuleUpdate.update()
import Utils
from worlds.alttp import Options as LttPOptions
from worlds.generic import PlandoItem, PlandoConnection
from Utils import parse_yaml, version_tuple, __version__, tuplize_version, get_options
from worlds.generic import PlandoConnection
from Utils import parse_yamls, version_tuple, __version__, tuplize_version, get_options, user_path
from worlds.alttp.EntranceRandomizer import parse_arguments
from Main import main as ERmain
from Main import get_seed, seeddigits
from BaseClasses import seeddigits, get_seed, PlandoOptions
import Options
from worlds.alttp.Items import item_name_groups, item_table
from worlds.alttp import Bosses
from worlds.alttp.Text import TextTable
from worlds.alttp.Regions import location_table, key_drop_data
from worlds.AutoWorld import AutoWorldRegister
import copy
categories = set(AutoWorldRegister.world_types)
def mystery_argparse():
options = get_options()
defaults = options["generator"]
def resolve_path(path: str, resolver: Callable[[str], str]) -> str:
return path if os.path.isabs(path) else resolver(path)
parser = argparse.ArgumentParser(description="CMD Generation Interface, defaults come from host.yaml.")
parser.add_argument('--weights_file_path', default = defaults["weights_file_path"],
parser.add_argument('--weights_file_path', default=defaults["weights_file_path"],
help='Path to the weights file to use for rolling game settings, urls are also valid')
parser.add_argument('--samesettings', help='Rolls settings per weights file rather than per player',
action='store_true')
parser.add_argument('--player_files_path', default=defaults["player_files_path"],
parser.add_argument('--player_files_path', default=resolve_path(defaults["player_files_path"], user_path),
help="Input directory for player files.")
parser.add_argument('--seed', help='Define seed number to generate.', type=int)
parser.add_argument('--multi', default=defaults["players"], type=lambda value: min(max(int(value), 1), 255))
parser.add_argument('--multi', default=defaults["players"], type=lambda value: max(int(value), 1))
parser.add_argument('--spoiler', type=int, default=defaults["spoiler"])
parser.add_argument('--rom', default=options["lttp_options"]["rom_file"], help="Path to the 1.0 JP LttP Baserom.")
parser.add_argument('--enemizercli', default=defaults["enemizer_path"])
parser.add_argument('--outputpath', default=options["general_options"]["output_path"])
parser.add_argument('--outputpath', default=resolve_path(options["general_options"]["output_path"], user_path),
help="Path to output folder. Absolute or relative to cwd.") # absolute or relative to cwd
parser.add_argument('--race', action='store_true', default=defaults["race"])
parser.add_argument('--meta_file_path', default=defaults["meta_file_path"])
parser.add_argument('--log_output_path', help='Path to store output log')
parser.add_argument('--log_level', default='info', help='Sets log level')
parser.add_argument('--yaml_output', default=0, type=lambda value: min(max(int(value), 0), 255),
parser.add_argument('--yaml_output', default=0, type=lambda value: max(int(value), 0),
help='Output rolled mystery results to yaml up to specified number (made for async multiworld)')
parser.add_argument('--plando', default=defaults["plando_options"],
help='List of options that can be set manually. Can be combined, for example "bosses, items"')
@@ -57,12 +61,12 @@ def mystery_argparse():
args.weights_file_path = os.path.join(args.player_files_path, args.weights_file_path)
if not os.path.isabs(args.meta_file_path):
args.meta_file_path = os.path.join(args.player_files_path, args.meta_file_path)
args.plando: typing.Set[str] = {arg.strip().lower() for arg in args.plando.split(",")}
args.plando: PlandoOptions = PlandoOptions.from_option_string(args.plando)
return args, options
def get_seed_name(random):
return f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits)
def get_seed_name(random_source) -> str:
return f"{random_source.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits)
def main(args=None, callback=ERmain):
@@ -76,22 +80,25 @@ def main(args=None, callback=ERmain):
if args.race:
random.seed() # reset to time-based random source
weights_cache = {}
weights_cache: Dict[str, Tuple[Any, ...]] = {}
if args.weights_file_path and os.path.exists(args.weights_file_path):
try:
weights_cache[args.weights_file_path] = read_weights_yaml(args.weights_file_path)
weights_cache[args.weights_file_path] = read_weights_yamls(args.weights_file_path)
except Exception as e:
raise ValueError(f"File {args.weights_file_path} is destroyed. Please fix your yaml.") from e
print(f"Weights: {args.weights_file_path} >> "
f"{get_choice('description', weights_cache[args.weights_file_path], 'No description specified')}")
f"{get_choice('description', weights_cache[args.weights_file_path][-1], 'No description specified')}")
if args.meta_file_path and os.path.exists(args.meta_file_path):
try:
weights_cache[args.meta_file_path] = read_weights_yaml(args.meta_file_path)
meta_weights = read_weights_yamls(args.meta_file_path)[-1]
except Exception as e:
raise ValueError(f"File {args.meta_file_path} is destroyed. Please fix your yaml.") from e
meta_weights = weights_cache[args.meta_file_path]
print(f"Meta: {args.meta_file_path} >> {get_choice('meta_description', meta_weights, 'No description specified')}")
print(f"Meta: {args.meta_file_path} >> {get_choice('meta_description', meta_weights)}")
try: # meta description allows us to verify that the file named meta.yaml is intentionally a meta file
del(meta_weights["meta_description"])
except Exception as e:
raise ValueError("No meta description found for meta.yaml. Unable to verify.") from e
if args.samesettings:
raise Exception("Cannot mix --samesettings with --meta")
else:
@@ -100,94 +107,99 @@ def main(args=None, callback=ERmain):
player_files = {}
for file in os.scandir(args.player_files_path):
fname = file.name
if file.is_file() and os.path.join(args.player_files_path, fname) not in {args.meta_file_path, args.weights_file_path}:
if file.is_file() and not fname.startswith(".") and \
os.path.join(args.player_files_path, fname) not in {args.meta_file_path, args.weights_file_path}:
path = os.path.join(args.player_files_path, fname)
try:
weights_cache[fname] = read_weights_yaml(path)
weights_cache[fname] = read_weights_yamls(path)
except Exception as e:
raise ValueError(f"File {fname} is destroyed. Please fix your yaml.") from e
else:
print(f"P{player_id} Weights: {fname} >> "
f"{get_choice('description', weights_cache[fname], 'No description specified')}")
player_files[player_id] = fname
# sort dict for consistent results across platforms:
weights_cache = {key: value for key, value in sorted(weights_cache.items())}
for filename, yaml_data in weights_cache.items():
if filename not in {args.meta_file_path, args.weights_file_path}:
for yaml in yaml_data:
print(f"P{player_id} Weights: {filename} >> "
f"{get_choice('description', yaml, 'No description specified')}")
player_files[player_id] = filename
player_id += 1
args.multi = max(player_id-1, args.multi)
print(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, {seed_name} Seed {seed}")
args.multi = max(player_id - 1, args.multi)
print(f"Generating for {args.multi} player{'s' if args.multi > 1 else ''}, {seed_name} Seed {seed} with plando: "
f"{args.plando}")
if not weights_cache:
raise Exception(f"No weights found. Provide a general weights file ({args.weights_file_path}) or individual player files. "
f"A mix is also permitted.")
erargs = parse_arguments(['--multi', str(args.multi)])
erargs.seed = seed
erargs.name = {x: "" for x in range(1, args.multi + 1)} # only so it can be overwrittin in mystery
erargs.create_spoiler = args.spoiler > 0
erargs.plando_options = args.plando
erargs.glitch_triforce = options["generator"]["glitch_triforce_room"]
erargs.spoiler = args.spoiler
erargs.race = args.race
erargs.skip_playthrough = args.spoiler < 2
erargs.outputname = seed_name
erargs.outputpath = args.outputpath
# set up logger
if args.log_level:
erargs.loglevel = args.log_level
loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[
erargs.loglevel]
Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level)
if args.log_output_path:
os.makedirs(args.log_output_path, exist_ok=True)
logging.basicConfig(format='%(message)s', level=loglevel, force=True,
filename=os.path.join(args.log_output_path, f"{seed}.log"))
else:
logging.basicConfig(format='%(message)s', level=loglevel, force=True)
settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None)
for fname, yamls in weights_cache.items()}
erargs.rom = args.rom
erargs.enemizercli = args.enemizercli
if meta_weights:
for category_name, category_dict in meta_weights.items():
for key in category_dict:
option = roll_meta_option(key, category_name, category_dict)
if option is not None:
for path in weights_cache:
for yaml in weights_cache[path]:
if category_name is None:
for category in yaml:
if category in AutoWorldRegister.world_types and key in Options.common_options:
yaml[category][key] = option
elif category_name not in yaml:
logging.warning(f"Meta: Category {category_name} is not present in {path}.")
else:
yaml[category_name][key] = option
settings_cache = {k: (roll_settings(v, args.plando) if args.samesettings else None)
for k, v in weights_cache.items()}
player_path_cache = {}
for player in range(1, args.multi + 1):
player_path_cache[player] = player_files.get(player, args.weights_file_path)
if meta_weights:
for player, path in player_path_cache.items():
weights_cache[path].setdefault("meta_ignore", [])
for key in meta_weights:
option = get_choice(key, meta_weights)
if option is not None:
for player, path in player_path_cache.items():
players_meta = weights_cache[path].get("meta_ignore", [])
if key not in players_meta:
weights_cache[path][key] = option
elif type(players_meta) == dict and players_meta[key] and option not in players_meta[key]:
weights_cache[path][key] = option
name_counter = Counter()
erargs.player_settings = {}
for player in range(1, args.multi + 1):
player = 1
while player <= args.multi:
path = player_path_cache[player]
if path:
try:
settings = settings_cache[path] if settings_cache[path] else \
roll_settings(weights_cache[path], args.plando)
for k, v in vars(settings).items():
if v is not None:
try:
getattr(erargs, k)[player] = v
except AttributeError:
setattr(erargs, k, {player: v})
except Exception as e:
raise Exception(f"Error setting {k} to {v} for player {player}") from e
settings: Tuple[argparse.Namespace, ...] = settings_cache[path] if settings_cache[path] else \
tuple(roll_settings(yaml, args.plando) for yaml in weights_cache[path])
for settingsObject in settings:
for k, v in vars(settingsObject).items():
if v is not None:
try:
getattr(erargs, k)[player] = v
except AttributeError:
setattr(erargs, k, {player: v})
except Exception as e:
raise Exception(f"Error setting {k} to {v} for player {player}") from e
if path == args.weights_file_path: # if name came from the weights file, just use base player name
erargs.name[player] = f"Player{player}"
elif not erargs.name[player]: # if name was not specified, generate it from filename
erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0]
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
player += 1
except Exception as e:
raise ValueError(f"File {path} is destroyed. Please fix your yaml.") from e
else:
raise RuntimeError(f'No weights specified for player {player}')
if path == args.weights_file_path: # if name came from the weights file, just use base player name
erargs.name[player] = f"Player{player}"
elif not erargs.name[player]: # if name was not specified, generate it from filename
erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0]
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
if len(set(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())}")
if args.yaml_output:
import yaml
@@ -198,8 +210,6 @@ def main(args=None, callback=ERmain):
if len(player_settings.values()) > 1:
important[option] = {player: value for player, value in player_settings.items() if
player <= args.yaml_output}
elif len(player_settings.values()) > 0:
important[option] = player_settings[1]
else:
logging.debug(f"No player settings defined for option '{option}'")
@@ -216,28 +226,28 @@ def main(args=None, callback=ERmain):
callback(erargs, seed)
def read_weights_yaml(path):
def read_weights_yamls(path) -> Tuple[Any, ...]:
try:
if urllib.parse.urlparse(path).scheme:
yaml = str(urllib.request.urlopen(path).read(), "utf-8")
if urllib.parse.urlparse(path).scheme in ('https', 'file'):
yaml = str(urllib.request.urlopen(path).read(), "utf-8-sig")
else:
with open(path, 'rb') as f:
yaml = str(f.read(), "utf-8")
yaml = str(f.read(), "utf-8-sig")
except Exception as e:
raise Exception(f"Failed to read weights ({path})") from e
return parse_yaml(yaml)
return tuple(parse_yamls(yaml))
def interpret_on_off(value):
def interpret_on_off(value) -> bool:
return {"on": True, "off": False}.get(value, value)
def convert_to_on_off(value):
def convert_to_on_off(value) -> str:
return {True: "on", False: "off"}.get(value, value)
def get_choice(option, root, value=None) -> typing.Any:
def get_choice_legacy(option, root, value=None) -> Any:
if option not in root:
return value
if type(root[option]) is list:
@@ -252,17 +262,31 @@ def get_choice(option, root, value=None) -> typing.Any:
raise RuntimeError(f"All options specified in \"{option}\" are weighted as zero.")
def get_choice(option, root, value=None) -> Any:
if option not in root:
return value
if type(root[option]) is list:
return random.choices(root[option])[0]
if type(root[option]) is not dict:
return root[option]
if not root[option]:
return value
if any(root[option].values()):
return random.choices(list(root[option].keys()), weights=list(map(int, root[option].values())))[0]
raise RuntimeError(f"All options specified in \"{option}\" are weighted as zero.")
class SafeDict(dict):
def __missing__(self, key):
return '{' + key + '}'
def handle_name(name: str, player: int, name_counter: Counter):
name_counter[name] += 1
name_counter[name.lower()] += 1
number = name_counter[name.lower()]
new_name = "%".join([x.replace("%number%", "{number}").replace("%player%", "{player}") for x in name.split("%%")])
new_name = string.Formatter().vformat(new_name, (), SafeDict(number=name_counter[name],
NUMBER=(name_counter[name] if name_counter[
name] > 1 else ''),
new_name = string.Formatter().vformat(new_name, (), SafeDict(number=number,
NUMBER=(number if number > 1 else ''),
player=player,
PLAYER=(player if player > 1 else '')))
new_name = new_name.strip()[:16]
@@ -271,26 +295,13 @@ def handle_name(name: str, player: int, name_counter: Counter):
return new_name
def prefer_int(input_data: str) -> typing.Union[str, int]:
def prefer_int(input_data: str) -> Union[str, int]:
try:
return int(input_data)
except:
return input_data
available_boss_names: typing.Set[str] = {boss.lower() for boss in Bosses.boss_table if boss not in
{'Agahnim', 'Agahnim2', 'Ganon'}}
available_boss_locations: typing.Set[str] = {f"{loc.lower()}{f' {level}' if level else ''}" for loc, level in
Bosses.boss_location_table}
boss_shuffle_options = {None: 'none',
'none': 'none',
'basic': 'basic',
'full': 'full',
'chaos': 'chaos',
'singularity': 'singularity'
}
goals = {
'ganon': 'ganon',
'crystals': 'crystals',
@@ -305,7 +316,7 @@ goals = {
}
def roll_percentage(percentage: typing.Union[int, float]) -> bool:
def roll_percentage(percentage: Union[int, float]) -> bool:
"""Roll a percentage chance.
percentage is expected to be in range [0, 100]"""
return random.random() < (float(percentage) / 100)
@@ -323,8 +334,30 @@ def update_weights(weights: dict, new_weights: dict, type: str, name: str) -> di
return weights
def roll_meta_option(option_key, game: str, category_dict: Dict) -> Any:
if not game:
return get_choice(option_key, category_dict)
if game in AutoWorldRegister.world_types:
game_world = AutoWorldRegister.world_types[game]
options = ChainMap(game_world.option_definitions, Options.per_game_common_options)
if option_key in options:
if options[option_key].supports_weighting:
return get_choice(option_key, category_dict)
return category_dict[option_key]
if game == "A Link to the Past": # TODO wow i hate this
if option_key in {"glitches_required", "dark_room_logic", "entrance_shuffle", "goals", "triforce_pieces_mode",
"triforce_pieces_percentage", "triforce_pieces_available", "triforce_pieces_extra",
"triforce_pieces_required", "shop_shuffle", "mode", "item_pool", "item_functionality",
"boss_shuffle", "enemy_damage", "enemy_health", "timer", "countdown_start_time",
"red_clock_time", "blue_clock_time", "green_clock_time", "dungeon_counters", "shuffle_prizes",
"misery_mire_medallion", "turtle_rock_medallion", "sprite_pool", "sprite",
"random_sprite_on_event"}:
return get_choice(option_key, category_dict)
raise Exception(f"Error generating meta option {option_key} for {game}.")
def roll_linked_options(weights: dict) -> dict:
weights = weights.copy() # make sure we don't write back to other weights sets in same_settings
weights = copy.deepcopy(weights) # make sure we don't write back to other weights sets in same_settings
for option_set in weights["linked_options"]:
if "name" not in option_set:
raise ValueError("One of your linked options does not have a name.")
@@ -345,10 +378,10 @@ def roll_linked_options(weights: dict) -> dict:
return weights
def roll_triggers(weights: dict) -> dict:
weights = weights.copy() # make sure we don't write back to other weights sets in same_settings
weights["_Generator_Version"] = "Archipelago" # Some means for triggers to know if the seed is on main or doors.
for i, option_set in enumerate(weights["triggers"]):
def roll_triggers(weights: dict, triggers: list) -> dict:
weights = copy.deepcopy(weights) # make sure we don't write back to other weights sets in same_settings
weights["_Generator_Version"] = Utils.__version__
for i, option_set in enumerate(triggers):
try:
currently_targeted_weights = weights
category = option_set.get("option_category", None)
@@ -375,47 +408,28 @@ def roll_triggers(weights: dict) -> dict:
return weights
def get_plando_bosses(boss_shuffle: str, plando_options: typing.Set[str]) -> str:
if boss_shuffle in boss_shuffle_options:
return boss_shuffle_options[boss_shuffle]
elif "bosses" in plando_options:
options = boss_shuffle.lower().split(";")
remainder_shuffle = "none" # vanilla
bosses = []
for boss in options:
if boss in boss_shuffle_options:
remainder_shuffle = boss_shuffle_options[boss]
elif "-" in boss:
loc, boss_name = boss.split("-")
if boss_name not in available_boss_names:
raise ValueError(f"Unknown Boss name {boss_name}")
if loc not in available_boss_locations:
raise ValueError(f"Unknown Boss Location {loc}")
level = ''
if loc.split(" ")[-1] in {"top", "middle", "bottom"}:
# split off level
loc = loc.split(" ")
level = f" {loc[-1]}"
loc = " ".join(loc[:-1])
loc = loc.title().replace("Of", "of")
if not Bosses.can_place_boss(boss_name.title(), loc, level):
raise ValueError(f"Cannot place {boss_name} at {loc}{level}")
bosses.append(boss)
elif boss not in available_boss_names:
raise ValueError(f"Unknown Boss name or Boss shuffle option {boss}.")
def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option), plando_options: PlandoOptions):
if option_key in game_weights:
try:
if not option.supports_weighting:
player_option = option.from_any(game_weights[option_key])
else:
bosses.append(boss)
return ";".join(bosses + [remainder_shuffle])
player_option = option.from_any(get_choice(option_key, game_weights))
setattr(ret, option_key, player_option)
except Exception as e:
raise Exception(f"Error generating option {option_key} in {ret.game}") from e
else:
player_option.verify(AutoWorldRegister.world_types[ret.game], ret.name, plando_options)
else:
raise Exception(f"Boss Shuffle {boss_shuffle} is unknown and boss plando is turned off.")
setattr(ret, option_key, option.from_any(option.default)) # call the from_any here to support default "random"
def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("bosses",))):
def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.bosses):
if "linked_options" in weights:
weights = roll_linked_options(weights)
if "triggers" in weights:
weights = roll_triggers(weights)
weights = roll_triggers(weights, weights["triggers"])
requirements = weights.get("requires", {})
if requirements:
@@ -423,97 +437,66 @@ def roll_settings(weights: dict, plando_options: typing.Set[str] = frozenset(("b
if tuplize_version(version) > version_tuple:
raise Exception(f"Settings reports required version of generator is at least {version}, "
f"however generator is of version {__version__}")
required_plando_options = requirements.get("plando", "")
if required_plando_options:
required_plando_options = set(option.strip() for option in required_plando_options.split(","))
required_plando_options -= plando_options
required_plando_options = PlandoOptions.from_option_string(requirements.get("plando", ""))
if required_plando_options not in plando_options:
if required_plando_options:
if len(required_plando_options) == 1:
raise Exception(f"Settings reports required plando module {', '.join(required_plando_options)}, "
f"which is not enabled.")
else:
raise Exception(f"Settings reports required plando modules {', '.join(required_plando_options)}, "
f"which are not enabled.")
raise Exception(f"Settings reports required plando module {str(required_plando_options)}, "
f"which is not enabled.")
ret = argparse.Namespace()
ret.name = get_choice('name', weights)
ret.accessibility = get_choice('accessibility', weights)
ret.progression_balancing = get_choice('progression_balancing', weights, True)
for option_key in Options.per_game_common_options:
if option_key in weights and option_key not in Options.common_options:
raise Exception(f"Option {option_key} has to be in a game's section, not on its own.")
ret.game = get_choice("game", weights)
if ret.game not in weights:
raise Exception(f"No game options for selected game \"{ret.game}\" found.")
world_type = AutoWorldRegister.world_types[ret.game]
game_weights = weights[ret.game]
ret.local_items = set()
for item_name in game_weights.get('local_items', []):
items = world_type.item_name_groups.get(item_name, {item_name})
for item in items:
if item in world_type.item_names:
ret.local_items.add(item)
else:
raise Exception(f"Could not force item {item} to be world-local, as it was not recognized.")
ret.non_local_items = set()
for item_name in game_weights.get('non_local_items', []):
items = world_type.item_name_groups.get(item_name, {item_name})
for item in items:
if item in world_type.item_names:
ret.non_local_items.add(item)
else:
raise Exception(f"Could not force item {item} to be world-non-local, as it was not recognized.")
if "triggers" in game_weights:
weights = roll_triggers(weights, game_weights["triggers"])
game_weights = weights[ret.game]
inventoryweights = game_weights.get('start_inventory', {})
startitems = []
for item in inventoryweights.keys():
itemvalue = get_choice(item, inventoryweights)
if isinstance(itemvalue, int):
for i in range(int(itemvalue)):
startitems.append(item)
elif itemvalue:
startitems.append(item)
ret.startinventory = startitems
ret.start_hints = set(game_weights.get('start_hints', []))
ret.excluded_locations = set()
for location in game_weights.get('exclude_locations', []):
if location in world_type.location_names:
ret.excluded_locations.add(location)
else:
raise Exception(f"Could not exclude location {location}, as it was not recognized.")
ret.name = get_choice('name', weights)
for option_key, option in Options.common_options.items():
setattr(ret, option_key, option.from_any(get_choice(option_key, weights, option.default)))
if ret.game in AutoWorldRegister.world_types:
for option_name, option in AutoWorldRegister.world_types[ret.game].options.items():
if option_name in game_weights:
try:
if issubclass(option, Options.OptionDict):
setattr(ret, option_name, option.from_any(game_weights[option_name]))
else:
setattr(ret, option_name, option.from_any(get_choice(option_name, game_weights)))
except Exception as e:
raise Exception(f"Error generating option {option_name} in {ret.game}") from e
else:
setattr(ret, option_name, option(option.default))
if ret.game == "Minecraft":
for option_key, option in world_type.option_definitions.items():
handle_option(ret, game_weights, option_key, option, plando_options)
for option_key, option in Options.per_game_common_options.items():
# skip setting this option if already set from common_options, defaulting to root option
if option_key not in world_type.option_definitions and \
(option_key not in Options.common_options or option_key in game_weights):
handle_option(ret, game_weights, option_key, option, plando_options)
if PlandoOptions.items in plando_options:
ret.plando_items = game_weights.get("plando_items", [])
if ret.game == "Minecraft" or ret.game == "Ocarina of Time":
# bad hardcoded behavior to make this work for now
ret.plando_connections = []
if "connections" in plando_options:
if PlandoOptions.connections in plando_options:
options = game_weights.get("plando_connections", [])
for placement in options:
if roll_percentage(get_choice("percentage", placement, 100)):
ret.plando_connections.append(PlandoConnection(
get_choice("entrance", placement),
get_choice("exit", placement),
get_choice("direction", placement, "both")
get_choice("direction", placement)
))
elif ret.game == "A Link to the Past":
roll_alttp_settings(ret, game_weights, plando_options)
else:
raise Exception(f"Unsupported game {ret.game}")
return ret
def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
glitches_required = get_choice('glitches_required', weights)
if "dungeon_items" in weights and get_choice_legacy('dungeon_items', weights, "none") != "none":
raise Exception(f"dungeon_items key in A Link to the Past was removed, but is present in these weights as {get_choice_legacy('dungeon_items', weights, False)}.")
glitches_required = get_choice_legacy('glitches_required', weights)
if glitches_required not in [None, 'none', 'no_logic', 'overworld_glitches', 'hybrid_major_glitches', 'minor_glitches']:
logging.warning("Only NMG, OWG, HMG and No Logic supported")
glitches_required = 'none'
@@ -521,7 +504,7 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
'minor_glitches': 'minorglitches', 'hybrid_major_glitches': 'hybridglitches'}[
glitches_required]
ret.dark_room_logic = get_choice("dark_room_logic", weights, "lamp")
ret.dark_room_logic = get_choice_legacy("dark_room_logic", weights, "lamp")
if not ret.dark_room_logic: # None/False
ret.dark_room_logic = "none"
if ret.dark_room_logic == "sconces":
@@ -529,94 +512,56 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
if ret.dark_room_logic not in {"lamp", "torches", "none"}:
raise ValueError(f"Unknown Dark Room Logic: \"{ret.dark_room_logic}\"")
ret.restrict_dungeon_item_on_boss = get_choice('restrict_dungeon_item_on_boss', weights, False)
dungeon_items = get_choice('dungeon_items', weights)
if dungeon_items == 'full' or dungeon_items == True:
dungeon_items = 'mcsb'
elif dungeon_items == 'standard':
dungeon_items = ""
elif not dungeon_items:
dungeon_items = ""
if "u" in dungeon_items:
dungeon_items.replace("s", "")
ret.mapshuffle = get_choice('map_shuffle', weights, 'm' in dungeon_items)
ret.compassshuffle = get_choice('compass_shuffle', weights, 'c' in dungeon_items)
ret.keyshuffle = get_choice('smallkey_shuffle', weights,
'universal' if 'u' in dungeon_items else 's' in dungeon_items)
ret.bigkeyshuffle = get_choice('bigkey_shuffle', weights, 'b' in dungeon_items)
entrance_shuffle = get_choice('entrance_shuffle', weights, 'vanilla')
entrance_shuffle = get_choice_legacy('entrance_shuffle', weights, 'vanilla')
if entrance_shuffle.startswith('none-'):
ret.shuffle = 'vanilla'
else:
ret.shuffle = entrance_shuffle if entrance_shuffle != 'none' else 'vanilla'
goal = get_choice('goals', weights, 'ganon')
goal = get_choice_legacy('goals', weights, 'ganon')
ret.goal = goals[goal]
# TODO consider moving open_pyramid to an automatic variable in the core roller, set to True when
# fast ganon + ganon at hole
ret.open_pyramid = get_choice('open_pyramid', weights, 'goal')
extra_pieces = get_choice('triforce_pieces_mode', weights, 'available')
extra_pieces = get_choice_legacy('triforce_pieces_mode', weights, 'available')
ret.triforce_pieces_required = LttPOptions.TriforcePieces.from_any(get_choice('triforce_pieces_required', weights, 20))
ret.triforce_pieces_required = LttPOptions.TriforcePieces.from_any(get_choice_legacy('triforce_pieces_required', weights, 20))
# sum a percentage to required
if extra_pieces == 'percentage':
percentage = max(100, float(get_choice('triforce_pieces_percentage', weights, 150))) / 100
percentage = max(100, float(get_choice_legacy('triforce_pieces_percentage', weights, 150))) / 100
ret.triforce_pieces_available = int(round(ret.triforce_pieces_required * percentage, 0))
# vanilla mode (specify how many pieces are)
elif extra_pieces == 'available':
ret.triforce_pieces_available = LttPOptions.TriforcePieces.from_any(
get_choice('triforce_pieces_available', weights, 30))
get_choice_legacy('triforce_pieces_available', weights, 30))
# required pieces + fixed extra
elif extra_pieces == 'extra':
extra_pieces = max(0, int(get_choice('triforce_pieces_extra', weights, 10)))
extra_pieces = max(0, int(get_choice_legacy('triforce_pieces_extra', weights, 10)))
ret.triforce_pieces_available = ret.triforce_pieces_required + extra_pieces
# change minimum to required pieces to avoid problems
ret.triforce_pieces_available = min(max(ret.triforce_pieces_required, int(ret.triforce_pieces_available)), 90)
ret.shop_shuffle = get_choice('shop_shuffle', weights, '')
ret.shop_shuffle = get_choice_legacy('shop_shuffle', weights, '')
if not ret.shop_shuffle:
ret.shop_shuffle = ''
ret.mode = get_choice("mode", weights)
ret.retro = get_choice("retro", weights)
ret.mode = get_choice_legacy("mode", weights)
ret.hints = get_choice('hints', weights)
ret.difficulty = get_choice_legacy('item_pool', weights)
ret.swordless = get_choice('swordless', weights, False)
ret.item_functionality = get_choice_legacy('item_functionality', weights)
ret.difficulty = get_choice('item_pool', weights)
ret.item_functionality = get_choice('item_functionality', weights)
boss_shuffle = get_choice('boss_shuffle', weights)
ret.shufflebosses = get_plando_bosses(boss_shuffle, plando_options)
ret.enemy_shuffle = bool(get_choice('enemy_shuffle', weights, False))
ret.killable_thieves = get_choice('killable_thieves', weights, False)
ret.tile_shuffle = get_choice('tile_shuffle', weights, False)
ret.bush_shuffle = get_choice('bush_shuffle', weights, False)
ret.enemy_damage = {None: 'default',
'default': 'default',
'shuffled': 'shuffled',
'random': 'chaos', # to be removed
'chaos': 'chaos',
}[get_choice('enemy_damage', weights)]
}[get_choice_legacy('enemy_damage', weights)]
ret.enemy_health = get_choice('enemy_health', weights)
ret.shufflepots = get_choice('pot_shuffle', weights)
ret.beemizer = int(get_choice('beemizer', weights, 0))
ret.enemy_health = get_choice_legacy('enemy_health', weights)
ret.timer = {'none': False,
None: False,
@@ -625,19 +570,19 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
'timed_ohko': 'timed-ohko',
'ohko': 'ohko',
'timed_countdown': 'timed-countdown',
'display': 'display'}[get_choice('timer', weights, False)]
'display': 'display'}[get_choice_legacy('timer', weights, False)]
ret.countdown_start_time = int(get_choice('countdown_start_time', weights, 10))
ret.red_clock_time = int(get_choice('red_clock_time', weights, -2))
ret.blue_clock_time = int(get_choice('blue_clock_time', weights, 2))
ret.green_clock_time = int(get_choice('green_clock_time', weights, 4))
ret.countdown_start_time = int(get_choice_legacy('countdown_start_time', weights, 10))
ret.red_clock_time = int(get_choice_legacy('red_clock_time', weights, -2))
ret.blue_clock_time = int(get_choice_legacy('blue_clock_time', weights, 2))
ret.green_clock_time = int(get_choice_legacy('green_clock_time', weights, 4))
ret.dungeon_counters = get_choice('dungeon_counters', weights, 'default')
ret.dungeon_counters = get_choice_legacy('dungeon_counters', weights, 'default')
ret.shuffle_prizes = get_choice('shuffle_prizes', weights, "g")
ret.shuffle_prizes = get_choice_legacy('shuffle_prizes', weights, "g")
ret.required_medallions = [get_choice("misery_mire_medallion", weights, "random"),
get_choice("turtle_rock_medallion", weights, "random")]
ret.required_medallions = [get_choice_legacy("misery_mire_medallion", weights, "random"),
get_choice_legacy("turtle_rock_medallion", weights, "random")]
for index, medallion in enumerate(ret.required_medallions):
ret.required_medallions[index] = {"ether": "Ether", "quake": "Quake", "bombos": "Bombos", "random": "random"} \
@@ -645,88 +590,45 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options):
if not ret.required_medallions[index]:
raise Exception(f"unknown Medallion {medallion} for {'misery mire' if index == 0 else 'turtle rock'}")
ret.glitch_boots = get_choice('glitch_boots', weights, True)
if get_choice("local_keys", weights, "l" in dungeon_items):
# () important for ordering of commands, without them the Big Keys section is part of the Small Key else
ret.local_items |= item_name_groups["Small Keys"] if ret.keyshuffle else set()
ret.local_items |= item_name_groups["Big Keys"] if ret.bigkeyshuffle else set()
ret.plando_items = []
if "items" in plando_options:
def add_plando_item(item: str, location: str):
if item not in item_table:
raise Exception(f"Could not plando item {item} as the item was not recognized")
if location not in location_table and location not in key_drop_data:
raise Exception(
f"Could not plando item {item} at location {location} as the location was not recognized")
ret.plando_items.append(PlandoItem(item, location, location_world, from_pool, force))
options = weights.get("plando_items", [])
for placement in options:
if roll_percentage(get_choice("percentage", placement, 100)):
from_pool = get_choice("from_pool", placement, PlandoItem._field_defaults["from_pool"])
location_world = get_choice("world", placement, PlandoItem._field_defaults["world"])
force = str(get_choice("force", placement, PlandoItem._field_defaults["force"])).lower()
if "items" in placement and "locations" in placement:
items = placement["items"]
locations = placement["locations"]
if isinstance(items, dict):
item_list = []
for key, value in items.items():
item_list += [key] * value
items = item_list
if not items or not locations:
raise Exception("You must specify at least one item and one location to place items.")
random.shuffle(items)
random.shuffle(locations)
for item, location in zip(items, locations):
add_plando_item(item, location)
else:
item = get_choice("item", placement, get_choice("items", placement))
location = get_choice("location", placement)
add_plando_item(item, location)
ret.plando_texts = {}
if "texts" in plando_options:
if PlandoOptions.texts in plando_options:
tt = TextTable()
tt.removeUnwantedText()
options = weights.get("plando_texts", [])
for placement in options:
if roll_percentage(get_choice("percentage", placement, 100)):
at = str(get_choice("at", placement))
if roll_percentage(get_choice_legacy("percentage", placement, 100)):
at = str(get_choice_legacy("at", placement))
if at not in tt:
raise Exception(f"No text target \"{at}\" found.")
ret.plando_texts[at] = str(get_choice("text", placement))
ret.plando_texts[at] = str(get_choice_legacy("text", placement))
ret.plando_connections = []
if "connections" in plando_options:
if PlandoOptions.connections in plando_options:
options = weights.get("plando_connections", [])
for placement in options:
if roll_percentage(get_choice("percentage", placement, 100)):
if roll_percentage(get_choice_legacy("percentage", placement, 100)):
ret.plando_connections.append(PlandoConnection(
get_choice("entrance", placement),
get_choice("exit", placement),
get_choice("direction", placement, "both")
get_choice_legacy("entrance", placement),
get_choice_legacy("exit", placement),
get_choice_legacy("direction", placement, "both")
))
ret.sprite_pool = weights.get('sprite_pool', [])
ret.sprite = get_choice('sprite', weights, "Link")
ret.sprite = get_choice_legacy('sprite', weights, "Link")
if 'random_sprite_on_event' in weights:
randomoneventweights = weights['random_sprite_on_event']
if get_choice('enabled', randomoneventweights, False):
if get_choice_legacy('enabled', randomoneventweights, False):
ret.sprite = 'randomon'
ret.sprite += '-hit' if get_choice('on_hit', randomoneventweights, True) else ''
ret.sprite += '-enter' if get_choice('on_enter', randomoneventweights, False) else ''
ret.sprite += '-exit' if get_choice('on_exit', randomoneventweights, False) else ''
ret.sprite += '-slash' if get_choice('on_slash', randomoneventweights, False) else ''
ret.sprite += '-item' if get_choice('on_item', randomoneventweights, False) else ''
ret.sprite += '-bonk' if get_choice('on_bonk', randomoneventweights, False) else ''
ret.sprite = 'randomonall' if get_choice('on_everything', randomoneventweights, False) else ret.sprite
ret.sprite += '-hit' if get_choice_legacy('on_hit', randomoneventweights, True) else ''
ret.sprite += '-enter' if get_choice_legacy('on_enter', randomoneventweights, False) else ''
ret.sprite += '-exit' if get_choice_legacy('on_exit', randomoneventweights, False) else ''
ret.sprite += '-slash' if get_choice_legacy('on_slash', randomoneventweights, False) else ''
ret.sprite += '-item' if get_choice_legacy('on_item', randomoneventweights, False) else ''
ret.sprite += '-bonk' if get_choice_legacy('on_bonk', randomoneventweights, False) else ''
ret.sprite = 'randomonall' if get_choice_legacy('on_everything', randomoneventweights, False) else ret.sprite
ret.sprite = 'randomonnone' if ret.sprite == 'randomon' else ret.sprite
if (not ret.sprite_pool or get_choice('use_weighted_sprite_pool', randomoneventweights, False)) \
if (not ret.sprite_pool or get_choice_legacy('use_weighted_sprite_pool', randomoneventweights, False)) \
and 'sprite' in weights: # Use sprite as a weighted sprite pool, if a sprite pool is not already defined.
for key, value in weights['sprite'].items():
if key.startswith('random'):
@@ -740,4 +642,4 @@ if __name__ == '__main__':
confirmation = atexit.register(input, "Press enter to close.")
main()
# in case of error-free exit should not need confirmation
atexit.unregister(confirmation)
atexit.unregister(confirmation)

906
KH2Client.py Normal file
View File

@@ -0,0 +1,906 @@
import os
import asyncio
import ModuleUpdate
import json
import Utils
from pymem import pymem
from worlds.kh2.Items import exclusionItem_table, CheckDupingItems
from worlds.kh2 import all_locations, item_dictionary_table, exclusion_table
from worlds.kh2.WorldLocations import *
from worlds import network_data_package
if __name__ == "__main__":
Utils.init_logging("KH2Client", exception_logger="Client")
from NetUtils import ClientStatus
from CommonClient import gui_enabled, logger, get_base_parser, ClientCommandProcessor, \
CommonContext, server_loop
ModuleUpdate.update()
kh2_loc_name_to_id = network_data_package["games"]["Kingdom Hearts 2"]["location_name_to_id"]
# class KH2CommandProcessor(ClientCommandProcessor):
class KH2Context(CommonContext):
# command_processor: int = KH2CommandProcessor
game = "Kingdom Hearts 2"
items_handling = 0b101 # Indicates you get items sent from other worlds.
def __init__(self, server_address, password):
super(KH2Context, self).__init__(server_address, password)
self.kh2LocalItems = None
self.ability = None
self.growthlevel = None
self.KH2_sync_task = None
self.syncing = False
self.kh2connected = False
self.serverconneced = False
self.item_name_to_data = {name: data for name, data, in item_dictionary_table.items()}
self.location_name_to_data = {name: data for name, data, in all_locations.items()}
self.lookup_id_to_item: typing.Dict[int, str] = {data.code: item_name for item_name, data in
item_dictionary_table.items() if data.code}
self.lookup_id_to_Location: typing.Dict[int, str] = {data.code: item_name for item_name, data in
all_locations.items() if data.code}
self.location_name_to_worlddata = {name: data for name, data, in all_world_locations.items()}
self.location_table = {}
self.collectible_table = {}
self.collectible_override_flags_address = 0
self.collectible_offsets = {}
self.sending = []
# flag for if the player has gotten their starting inventory from the server
self.hasStartingInvo = False
# list used to keep track of locations+items player has. Used for disoneccting
self.kh2seedsave = {"checked_locations": {"0": []},
"starting_inventory": self.hasStartingInvo,
# Character: [back of invo, front of invo]
"SoraInvo": [0x25CC, 0x2546],
"DonaldInvo": [0x2678, 0x2658],
"GoofyInvo": [0x278E, 0x276C],
"AmountInvo": {
"ServerItems": {
"Ability": {},
"Amount": {},
"Growth": {"High Jump": 0, "Quick Run": 0, "Dodge Roll": 0, "Aerial Dodge": 0,
"Glide": 0},
"Bitmask": [],
"Weapon": {"Sora": [], "Donald": [], "Goofy": []},
"Equipment": [],
"Magic": {},
"StatIncrease": {},
"Boost": {},
},
"LocalItems": {
"Ability": {},
"Amount": {},
"Growth": {"High Jump": 0, "Quick Run": 0, "Dodge Roll": 0,
"Aerial Dodge": 0, "Glide": 0},
"Bitmask": [],
"Weapon": {"Sora": [], "Donald": [], "Goofy": []},
"Equipment": [],
"Magic": {},
"StatIncrease": {},
"Boost": {},
}},
# 1,3,255 are in this list in case the player gets locations in those "worlds" and I need to still have them checked
"worldIdChecks": {
"1": [], # world of darkness (story cutscenes)
"2": [],
"3": [], # destiny island doesn't have checks to ima put tt checks here
"4": [],
"5": [],
"6": [],
"7": [],
"8": [],
"9": [],
"10": [],
"11": [],
# atlantica isn't a supported world. if you go in atlantica it will check dc
"12": [],
"13": [],
"14": [],
"15": [],
# world map, but you only go to the world map while on the way to goa so checking hb
"16": [],
"17": [],
"18": [],
"255": [], # starting screen
},
"Levels": {
"SoraLevel": 0,
"ValorLevel": 0,
"WisdomLevel": 0,
"LimitLevel": 0,
"MasterLevel": 0,
"FinalLevel": 0,
},
"SoldEquipment": [],
"SoldBoosts": {"Power Boost": 0,
"Magic Boost": 0,
"Defense Boost": 0,
"AP Boost": 0}
}
self.slotDataProgressionNames = {}
self.kh2seedname = None
self.kh2slotdata = None
self.itemamount = {}
# sora equipped, valor equipped, master equipped, final equipped
self.keybladeAnchorList = (0x24F0, 0x32F4, 0x339C, 0x33D4)
if "localappdata" in os.environ:
self.game_communication_path = os.path.expandvars(r"%localappdata%\KH2AP")
self.amountOfPieces = 0
# hooked object
self.kh2 = None
self.ItemIsSafe = False
self.game_connected = False
self.finalxemnas = False
self.worldid = {
# 1: {}, # world of darkness (story cutscenes)
2: TT_Checks,
# 3: {}, # destiny island doesn't have checks to ima put tt checks here
4: HB_Checks,
5: BC_Checks,
6: Oc_Checks,
7: AG_Checks,
8: LoD_Checks,
9: HundredAcreChecks,
10: PL_Checks,
11: DC_Checks, # atlantica isn't a supported world. if you go in atlantica it will check dc
12: DC_Checks,
13: TR_Checks,
14: HT_Checks,
15: HB_Checks, # world map, but you only go to the world map while on the way to goa so checking hb
16: PR_Checks,
17: SP_Checks,
18: TWTNW_Checks,
# 255: {}, # starting screen
}
# 0x2A09C00+0x40 is the sve anchor. +1 is the last saved room
self.sveroom = 0x2A09C00 + 0x41
# 0 not in battle 1 in yellow battle 2 red battle #short
self.inBattle = 0x2A0EAC4 + 0x40
self.onDeath = 0xAB9078
# PC Address anchors
self.Now = 0x0714DB8
self.Save = 0x09A70B0
self.Sys3 = 0x2A59DF0
self.Bt10 = 0x2A74880
self.BtlEnd = 0x2A0D3E0
self.Slot1 = 0x2A20C98
self.chest_set = set(exclusion_table["Chests"])
self.keyblade_set = set(CheckDupingItems["Weapons"]["Keyblades"])
self.staff_set = set(CheckDupingItems["Weapons"]["Staffs"])
self.shield_set = set(CheckDupingItems["Weapons"]["Shields"])
self.all_weapons = self.keyblade_set.union(self.staff_set).union(self.shield_set)
self.equipment_categories = CheckDupingItems["Equipment"]
self.armor_set = set(self.equipment_categories["Armor"])
self.accessories_set = set(self.equipment_categories["Accessories"])
self.all_equipment = self.armor_set.union(self.accessories_set)
self.Equipment_Anchor_Dict = {
"Armor": [0x2504, 0x2506, 0x2508, 0x250A],
"Accessories": [0x2514, 0x2516, 0x2518, 0x251A]}
self.AbilityQuantityDict = {}
self.ability_categories = CheckDupingItems["Abilities"]
self.sora_ability_set = set(self.ability_categories["Sora"])
self.donald_ability_set = set(self.ability_categories["Donald"])
self.goofy_ability_set = set(self.ability_categories["Goofy"])
self.all_abilities = self.sora_ability_set.union(self.donald_ability_set).union(self.goofy_ability_set)
self.boost_set = set(CheckDupingItems["Boosts"])
self.stat_increase_set = set(CheckDupingItems["Stat Increases"])
self.AbilityQuantityDict = {item: self.item_name_to_data[item].quantity for item in self.all_abilities}
# Growth:[level 1,level 4,slot]
self.growth_values_dict = {"High Jump": [0x05E, 0x061, 0x25CE],
"Quick Run": [0x62, 0x65, 0x25D0],
"Dodge Roll": [0x234, 0x237, 0x25D2],
"Aerial Dodge": [0x066, 0x069, 0x25D4],
"Glide": [0x6A, 0x6D, 0x25D6]}
self.boost_to_anchor_dict = {
"Power Boost": 0x24F9,
"Magic Boost": 0x24FA,
"Defense Boost": 0x24FB,
"AP Boost": 0x24F8}
self.AbilityCodeList = [self.item_name_to_data[item].code for item in exclusionItem_table["Ability"]]
self.master_growth = {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"}
self.bitmask_item_code = [
0x130000, 0x130001, 0x130002, 0x130003, 0x130004, 0x130005, 0x130006, 0x130007
, 0x130008, 0x130009, 0x13000A, 0x13000B, 0x13000C
, 0x13001F, 0x130020, 0x130021, 0x130022, 0x130023
, 0x13002A, 0x13002B, 0x13002C, 0x13002D]
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(KH2Context, self).server_auth(password_requested)
await self.get_username()
await self.send_connect()
async def connection_closed(self):
self.kh2connected = False
self.serverconneced = False
if self.kh2seedname is not None and self.auth is not None:
with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"),
'w') as f:
f.write(json.dumps(self.kh2seedsave, indent=4))
await super(KH2Context, self).connection_closed()
async def disconnect(self, allow_autoreconnect: bool = False):
self.kh2connected = False
self.serverconneced = False
if self.kh2seedname not in {None} and self.auth not in {None}:
with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"),
'w') as f:
f.write(json.dumps(self.kh2seedsave, indent=4))
await super(KH2Context, self).disconnect()
@property
def endpoints(self):
if self.server:
return [self.server]
else:
return []
async def shutdown(self):
if self.kh2seedname not in {None} and self.auth not in {None}:
with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"),
'w') as f:
f.write(json.dumps(self.kh2seedsave, indent=4))
await super(KH2Context, self).shutdown()
def on_package(self, cmd: str, args: dict):
if cmd in {"RoomInfo"}:
self.kh2seedname = args['seed_name']
if not os.path.exists(self.game_communication_path):
os.makedirs(self.game_communication_path)
if not os.path.exists(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json"):
with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"),
'wt') as f:
pass
elif os.path.exists(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json"):
with open(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json", 'r') as f:
self.kh2seedsave = json.load(f)
if cmd in {"Connected"}:
for player in args['players']:
if str(player.slot) not in self.kh2seedsave["checked_locations"]:
self.kh2seedsave["checked_locations"].update({str(player.slot): []})
self.kh2slotdata = args['slot_data']
self.serverconneced = True
self.kh2LocalItems = {int(location): item for location, item in self.kh2slotdata["LocalItems"].items()}
try:
self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX")
logger.info("You are now auto-tracking")
self.kh2connected = True
except Exception as e:
logger.info("Line 247")
if self.kh2connected:
logger.info("Connection Lost")
self.kh2connected = False
logger.info(e)
if cmd in {"ReceivedItems"}:
start_index = args["index"]
if start_index != len(self.items_received):
for item in args['items']:
# starting invo from server
if item.location in {-2}:
if not self.kh2seedsave["starting_inventory"]:
asyncio.create_task(self.give_item(item.item))
# if location is not already given or is !getitem
elif item.location not in self.kh2seedsave["checked_locations"][str(item.player)] \
or item.location in {-1}:
asyncio.create_task(self.give_item(item.item))
if item.location not in self.kh2seedsave["checked_locations"][str(item.player)] \
and item.location not in {-1, -2}:
self.kh2seedsave["checked_locations"][str(item.player)].append(item.location)
if not self.kh2seedsave["starting_inventory"]:
self.kh2seedsave["starting_inventory"] = True
if cmd in {"RoomUpdate"}:
if "checked_locations" in args:
new_locations = set(args["checked_locations"])
# TODO: make this take locations from other players on the same slot so proper coop happens
# items_to_give = [self.kh2slotdata["LocalItems"][str(location_id)] for location_id in new_locations if
# location_id in self.kh2LocalItems.keys()]
self.checked_locations |= new_locations
async def checkWorldLocations(self):
try:
currentworldint = int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x0714DB8, 1), "big")
if currentworldint in self.worldid:
curworldid = self.worldid[currentworldint]
for location, data in curworldid.items():
if location not in self.locations_checked \
and (int.from_bytes(
self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1),
"big") & 0x1 << data.bitIndex) > 0:
self.locations_checked.add(location)
self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))]
except Exception as e:
logger.info("Line 285")
if self.kh2connected:
logger.info("Connection Lost.")
self.kh2connected = False
logger.info(e)
async def checkLevels(self):
try:
for location, data in SoraLevels.items():
currentLevel = int.from_bytes(
self.kh2.read_bytes(self.kh2.base_address + self.Save + 0x24FF, 1), "big")
if location not in self.locations_checked \
and currentLevel >= data.bitIndex:
if self.kh2seedsave["Levels"]["SoraLevel"] < currentLevel:
self.kh2seedsave["Levels"]["SoraLevel"] = currentLevel
self.locations_checked.add(location)
self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))]
formDict = {
0: ["ValorLevel", ValorLevels], 1: ["WisdomLevel", WisdomLevels], 2: ["LimitLevel", LimitLevels],
3: ["MasterLevel", MasterLevels], 4: ["FinalLevel", FinalLevels]}
for i in range(5):
for location, data in formDict[i][1].items():
formlevel = int.from_bytes(
self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1), "big")
if location not in self.locations_checked \
and formlevel >= data.bitIndex:
if formlevel > self.kh2seedsave["Levels"][formDict[i][0]]:
self.kh2seedsave["Levels"][formDict[i][0]] = formlevel
self.locations_checked.add(location)
self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))]
except Exception as e:
logger.info("Line 312")
if self.kh2connected:
logger.info("Connection Lost.")
self.kh2connected = False
logger.info(e)
async def checkSlots(self):
try:
for location, data in weaponSlots.items():
if location not in self.locations_checked:
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1),
"big") > 0:
self.locations_checked.add(location)
self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))]
for location, data in formSlots.items():
if location not in self.locations_checked:
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1),
"big") & 0x1 << data.bitIndex > 0:
self.locations_checked.add(location)
self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))]
except Exception as e:
if self.kh2connected:
logger.info("Line 333")
logger.info("Connection Lost.")
self.kh2connected = False
logger.info(e)
async def verifyChests(self):
try:
currentworld = str(int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x0714DB8, 1), "big"))
for location in self.kh2seedsave["worldIdChecks"][currentworld]:
locationName = self.lookup_id_to_Location[location]
if locationName in self.chest_set:
if locationName in self.location_name_to_worlddata.keys():
locationData = self.location_name_to_worlddata[locationName]
if int.from_bytes(
self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained, 1),
"big") & 0x1 << locationData.bitIndex == 0:
roomData = int.from_bytes(
self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained,
1), "big")
self.kh2.write_bytes(self.kh2.base_address + self.Save + locationData.addrObtained,
(roomData | 0x01 << locationData.bitIndex).to_bytes(1, 'big'), 1)
except Exception as e:
if self.kh2connected:
logger.info("Line 350")
logger.info("Connection Lost.")
self.kh2connected = False
logger.info(e)
async def verifyLevel(self):
for leveltype, anchor in {"SoraLevel": 0x24FF,
"ValorLevel": 0x32F6,
"WisdomLevel": 0x332E,
"LimitLevel": 0x3366,
"MasterLevel": 0x339E,
"FinalLevel": 0x33D6}.items():
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + anchor, 1), "big") < \
self.kh2seedsave["Levels"][leveltype]:
self.kh2.write_bytes(self.kh2.base_address + self.Save + anchor,
(self.kh2seedsave["Levels"][leveltype]).to_bytes(1, 'big'), 1)
def verifyLocation(self, location):
locationData = self.location_name_to_worlddata[location]
locationName = self.lookup_id_to_Location[location]
isChecked = True
if locationName not in levels_locations:
if (int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained, 1),
"big") & 0x1 << locationData.bitIndex) == 0:
isChecked = False
elif locationName in SoraLevels:
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + 0x24FF, 1),
"big") < locationData.bitIndex:
isChecked = False
elif int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained, 1),
"big") < locationData.bitIndex:
isChecked = False
return isChecked
async def give_item(self, item, ItemType="ServerItems"):
try:
itemname = self.lookup_id_to_item[item]
itemcode = self.item_name_to_data[itemname]
if itemcode.ability:
abilityInvoType = 0
TwilightZone = 2
if ItemType == "LocalItems":
abilityInvoType = 1
TwilightZone = -2
if itemname in {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"}:
self.kh2seedsave["AmountInvo"][ItemType]["Growth"][itemname] += 1
return
if itemname not in self.kh2seedsave["AmountInvo"][ItemType]["Ability"]:
self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname] = []
# appending the slot that the ability should be in
if len(self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname]) < \
self.AbilityQuantityDict[itemname]:
if itemname in self.sora_ability_set:
self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname].append(
self.kh2seedsave["SoraInvo"][abilityInvoType])
self.kh2seedsave["SoraInvo"][abilityInvoType] -= TwilightZone
elif itemname in self.donald_ability_set:
self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname].append(
self.kh2seedsave["DonaldInvo"][abilityInvoType])
self.kh2seedsave["DonaldInvo"][abilityInvoType] -= TwilightZone
else:
self.kh2seedsave["AmountInvo"][ItemType]["Ability"][itemname].append(
self.kh2seedsave["GoofyInvo"][abilityInvoType])
self.kh2seedsave["GoofyInvo"][abilityInvoType] -= TwilightZone
elif itemcode.code in self.bitmask_item_code:
if itemname not in self.kh2seedsave["AmountInvo"][ItemType]["Bitmask"]:
self.kh2seedsave["AmountInvo"][ItemType]["Bitmask"].append(itemname)
elif itemcode.memaddr in {0x3594, 0x3595, 0x3596, 0x3597, 0x35CF, 0x35D0}:
if itemname in self.kh2seedsave["AmountInvo"][ItemType]["Magic"]:
self.kh2seedsave["AmountInvo"][ItemType]["Magic"][itemname] += 1
else:
self.kh2seedsave["AmountInvo"][ItemType]["Magic"][itemname] = 1
elif itemname in self.all_equipment:
self.kh2seedsave["AmountInvo"][ItemType]["Equipment"].append(itemname)
elif itemname in self.all_weapons:
if itemname in self.keyblade_set:
self.kh2seedsave["AmountInvo"][ItemType]["Weapon"]["Sora"].append(itemname)
elif itemname in self.staff_set:
self.kh2seedsave["AmountInvo"][ItemType]["Weapon"]["Donald"].append(itemname)
else:
self.kh2seedsave["AmountInvo"][ItemType]["Weapon"]["Goofy"].append(itemname)
elif itemname in self.boost_set:
if itemname in self.kh2seedsave["AmountInvo"][ItemType]["Boost"]:
self.kh2seedsave["AmountInvo"][ItemType]["Boost"][itemname] += 1
else:
self.kh2seedsave["AmountInvo"][ItemType]["Boost"][itemname] = 1
elif itemname in self.stat_increase_set:
if itemname in self.kh2seedsave["AmountInvo"][ItemType]["StatIncrease"]:
self.kh2seedsave["AmountInvo"][ItemType]["StatIncrease"][itemname] += 1
else:
self.kh2seedsave["AmountInvo"][ItemType]["StatIncrease"][itemname] = 1
else:
if itemname in self.kh2seedsave["AmountInvo"][ItemType]["Amount"]:
self.kh2seedsave["AmountInvo"][ItemType]["Amount"][itemname] += 1
else:
self.kh2seedsave["AmountInvo"][ItemType]["Amount"][itemname] = 1
except Exception as e:
if self.kh2connected:
logger.info("Line 398")
logger.info("Connection Lost.")
self.kh2connected = False
logger.info(e)
def run_gui(self):
"""Import kivy UI system and start running it as self.ui_task."""
from kvui import GameManager
class KH2Manager(GameManager):
logging_pairs = [
("Client", "Archipelago")
]
base_title = "Archipelago KH2 Client"
self.ui = KH2Manager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
async def IsInShop(self, sellable, master_boost):
# journal = 0x741230 shop = 0x741320
# if journal=-1 and shop = 5 then in shop
# if journam !=-1 and shop = 10 then journal
journal = self.kh2.read_short(self.kh2.base_address + 0x741230)
shop = self.kh2.read_short(self.kh2.base_address + 0x741320)
if (journal == -1 and shop == 5) or (journal != -1 and shop == 10):
# print("your in the shop")
sellable_dict = {}
for itemName in sellable:
itemdata = self.item_name_to_data[itemName]
amount = int.from_bytes(
self.kh2.read_bytes(self.kh2.base_address + self.Save + itemdata.memaddr, 1), "big")
sellable_dict[itemName] = amount
while (journal == -1 and shop == 5) or (journal != -1 and shop == 10):
journal = self.kh2.read_short(self.kh2.base_address + 0x741230)
shop = self.kh2.read_short(self.kh2.base_address + 0x741320)
await asyncio.sleep(0.5)
for item, amount in sellable_dict.items():
itemdata = self.item_name_to_data[item]
afterShop = int.from_bytes(
self.kh2.read_bytes(self.kh2.base_address + self.Save + itemdata.memaddr, 1), "big")
if afterShop < amount:
if item in master_boost:
self.kh2seedsave["SoldBoosts"][item] += (amount - afterShop)
else:
self.kh2seedsave["SoldEquipment"].append(item)
async def verifyItems(self):
try:
local_amount = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Amount"].keys())
server_amount = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Amount"].keys())
master_amount = local_amount | server_amount
local_ability = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Ability"].keys())
server_ability = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Ability"].keys())
master_ability = local_ability | server_ability
local_bitmask = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Bitmask"])
server_bitmask = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Bitmask"])
master_bitmask = local_bitmask | server_bitmask
local_keyblade = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Weapon"]["Sora"])
local_staff = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Weapon"]["Donald"])
local_shield = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Weapon"]["Goofy"])
server_keyblade = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Weapon"]["Sora"])
server_staff = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Weapon"]["Donald"])
server_shield = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Weapon"]["Goofy"])
master_keyblade = local_keyblade | server_keyblade
master_staff = local_staff | server_staff
master_shield = local_shield | server_shield
local_equipment = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Equipment"])
server_equipment = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Equipment"])
master_equipment = local_equipment | server_equipment
local_magic = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Magic"].keys())
server_magic = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Magic"].keys())
master_magic = local_magic | server_magic
local_stat = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["StatIncrease"].keys())
server_stat = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["StatIncrease"].keys())
master_stat = local_stat | server_stat
local_boost = set(self.kh2seedsave["AmountInvo"]["LocalItems"]["Boost"].keys())
server_boost = set(self.kh2seedsave["AmountInvo"]["ServerItems"]["Boost"].keys())
master_boost = local_boost | server_boost
master_sell = master_equipment | master_staff | master_shield | master_boost
await asyncio.create_task(self.IsInShop(master_sell, master_boost))
for itemName in master_amount:
itemData = self.item_name_to_data[itemName]
amountOfItems = 0
if itemName in local_amount:
amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["Amount"][itemName]
if itemName in server_amount:
amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["Amount"][itemName]
if itemName == "Torn Page":
# Torn Pages are handled differently because they can be consumed.
# Will check the progression in 100 acre and - the amount of visits
# amountofitems-amount of visits done
for location, data in tornPageLocks.items():
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1),
"big") & 0x1 << data.bitIndex > 0:
amountOfItems -= 1
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
"big") != amountOfItems and amountOfItems >= 0:
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
amountOfItems.to_bytes(1, 'big'), 1)
for itemName in master_keyblade:
itemData = self.item_name_to_data[itemName]
# if the inventory slot for that keyblade is less than the amount they should have
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
"big") != 1 and int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x1CFF, 1),
"big") != 13:
# Checking form anchors for the keyblade
if self.kh2.read_short(self.kh2.base_address + self.Save + 0x24F0) == itemData.kh2id \
or self.kh2.read_short(self.kh2.base_address + self.Save + 0x32F4) == itemData.kh2id \
or self.kh2.read_short(self.kh2.base_address + self.Save + 0x339C) == itemData.kh2id \
or self.kh2.read_short(self.kh2.base_address + self.Save + 0x33D4) == itemData.kh2id:
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
(0).to_bytes(1, 'big'), 1)
else:
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
(1).to_bytes(1, 'big'), 1)
for itemName in master_staff:
itemData = self.item_name_to_data[itemName]
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
"big") != 1 \
and self.kh2.read_short(self.kh2.base_address + self.Save + 0x2604) != itemData.kh2id \
and itemName not in self.kh2seedsave["SoldEquipment"]:
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
(1).to_bytes(1, 'big'), 1)
for itemName in master_shield:
itemData = self.item_name_to_data[itemName]
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
"big") != 1 \
and self.kh2.read_short(self.kh2.base_address + self.Save + 0x2718) != itemData.kh2id \
and itemName not in self.kh2seedsave["SoldEquipment"]:
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
(1).to_bytes(1, 'big'), 1)
for itemName in master_ability:
itemData = self.item_name_to_data[itemName]
ability_slot = []
if itemName in local_ability:
ability_slot += self.kh2seedsave["AmountInvo"]["LocalItems"]["Ability"][itemName]
if itemName in server_ability:
ability_slot += self.kh2seedsave["AmountInvo"]["ServerItems"]["Ability"][itemName]
for slot in ability_slot:
current = self.kh2.read_short(self.kh2.base_address + self.Save + slot)
ability = current & 0x0FFF
if ability | 0x8000 != (0x8000 + itemData.memaddr):
self.kh2.write_short(self.kh2.base_address + self.Save + slot, itemData.memaddr)
for itemName in self.master_growth:
growthLevel = self.kh2seedsave["AmountInvo"]["ServerItems"]["Growth"][itemName] \
+ self.kh2seedsave["AmountInvo"]["LocalItems"]["Growth"][itemName]
if growthLevel > 0:
slot = self.growth_values_dict[itemName][2]
min_growth = self.growth_values_dict[itemName][0]
max_growth = self.growth_values_dict[itemName][1]
if growthLevel > 4:
growthLevel = 4
current_growth_level = self.kh2.read_short(self.kh2.base_address + self.Save + slot)
ability = current_growth_level & 0x0FFF
# if the player should be getting a growth ability
if ability | 0x8000 != 0x8000 + min_growth - 1 + growthLevel:
# if it should be level one of that growth
if 0x8000 + min_growth - 1 + growthLevel <= 0x8000 + min_growth or ability < min_growth:
self.kh2.write_short(self.kh2.base_address + self.Save + slot, min_growth)
# if it is already in the inventory
elif ability | 0x8000 < (0x8000 + max_growth):
self.kh2.write_short(self.kh2.base_address + self.Save + slot, current_growth_level + 1)
for itemName in master_bitmask:
itemData = self.item_name_to_data[itemName]
itemMemory = int.from_bytes(
self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), "big")
if (int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
"big") & 0x1 << itemData.bitmask) == 0:
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
(itemMemory | 0x01 << itemData.bitmask).to_bytes(1, 'big'), 1)
for itemName in master_equipment:
itemData = self.item_name_to_data[itemName]
isThere = False
if itemName in self.accessories_set:
Equipment_Anchor_List = self.Equipment_Anchor_Dict["Accessories"]
else:
Equipment_Anchor_List = self.Equipment_Anchor_Dict["Armor"]
# Checking form anchors for the equipment
for slot in Equipment_Anchor_List:
if self.kh2.read_short(self.kh2.base_address + self.Save + slot) == itemData.kh2id:
isThere = True
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
"big") != 0:
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
(0).to_bytes(1, 'big'), 1)
break
if not isThere and itemName not in self.kh2seedsave["SoldEquipment"]:
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
"big") != 1:
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
(1).to_bytes(1, 'big'), 1)
for itemName in master_magic:
itemData = self.item_name_to_data[itemName]
amountOfItems = 0
if itemName in local_magic:
amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["Magic"][itemName]
if itemName in server_magic:
amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["Magic"][itemName]
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
"big") != amountOfItems \
and int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x741320, 1), "big") in {10, 8}:
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
amountOfItems.to_bytes(1, 'big'), 1)
for itemName in master_stat:
itemData = self.item_name_to_data[itemName]
amountOfItems = 0
if itemName in local_stat:
amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["StatIncrease"][itemName]
if itemName in server_stat:
amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["StatIncrease"][itemName]
if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
"big") != amountOfItems \
and int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Slot1 + 0x1B2, 1),
"big") >= 5:
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
amountOfItems.to_bytes(1, 'big'), 1)
for itemName in master_boost:
itemData = self.item_name_to_data[itemName]
amountOfItems = 0
if itemName in local_boost:
amountOfItems += self.kh2seedsave["AmountInvo"]["LocalItems"]["Boost"][itemName]
if itemName in server_boost:
amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["Boost"][itemName]
amountOfBoostsInInvo = int.from_bytes(
self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1),
"big")
amountOfUsedBoosts = int.from_bytes(
self.kh2.read_bytes(self.kh2.base_address + self.Save + self.boost_to_anchor_dict[itemName], 1),
"big")
# Ap Boots start at +50 for some reason
if itemName == "AP Boost":
amountOfUsedBoosts -= 50
totalBoosts = (amountOfBoostsInInvo + amountOfUsedBoosts)
if totalBoosts <= amountOfItems - self.kh2seedsave["SoldBoosts"][itemName] and amountOfBoostsInInvo < 255:
self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr,
(amountOfBoostsInInvo + 1).to_bytes(1, 'big'), 1)
except Exception as e:
logger.info("Line 573")
if self.kh2connected:
logger.info("Connection Lost.")
self.kh2connected = False
logger.info(e)
def finishedGame(ctx: KH2Context, message):
if ctx.kh2slotdata['FinalXemnas'] == 1:
if 0x1301ED in message[0]["locations"]:
ctx.finalxemnas = True
# three proofs
if ctx.kh2slotdata['Goal'] == 0:
if int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x36B2, 1), "big") > 0 \
and int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x36B3, 1), "big") > 0 \
and int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x36B4, 1), "big") > 0:
if ctx.kh2slotdata['FinalXemnas'] == 1:
if ctx.finalxemnas:
return True
else:
return False
else:
return True
else:
return False
elif ctx.kh2slotdata['Goal'] == 1:
if int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + ctx.Save + 0x3641, 1), "big") >= \
ctx.kh2slotdata['LuckyEmblemsRequired']:
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B2, (1).to_bytes(1, 'big'), 1)
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B3, (1).to_bytes(1, 'big'), 1)
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B4, (1).to_bytes(1, 'big'), 1)
if ctx.kh2slotdata['FinalXemnas'] == 1:
if ctx.finalxemnas:
return True
else:
return False
else:
return True
else:
return False
elif ctx.kh2slotdata['Goal'] == 2:
for boss in ctx.kh2slotdata["hitlist"]:
if boss in message[0]["locations"]:
ctx.amountOfPieces += 1
if ctx.amountOfPieces >= ctx.kh2slotdata["BountyRequired"]:
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B2, (1).to_bytes(1, 'big'), 1)
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B3, (1).to_bytes(1, 'big'), 1)
ctx.kh2.write_bytes(ctx.kh2.base_address + ctx.Save + 0x36B4, (1).to_bytes(1, 'big'), 1)
if ctx.kh2slotdata['FinalXemnas'] == 1:
if ctx.finalxemnas:
return True
else:
return False
else:
return True
else:
return False
async def kh2_watcher(ctx: KH2Context):
while not ctx.exit_event.is_set():
try:
if ctx.kh2connected and ctx.serverconneced:
ctx.sending = []
await asyncio.create_task(ctx.checkWorldLocations())
await asyncio.create_task(ctx.checkLevels())
await asyncio.create_task(ctx.checkSlots())
await asyncio.create_task(ctx.verifyChests())
await asyncio.create_task(ctx.verifyItems())
await asyncio.create_task(ctx.verifyLevel())
message = [{"cmd": 'LocationChecks', "locations": ctx.sending}]
if finishedGame(ctx, message):
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
location_ids = []
location_ids = [location for location in message[0]["locations"] if location not in location_ids]
for location in location_ids:
currentWorld = int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + 0x0714DB8, 1), "big")
if location not in ctx.kh2seedsave["worldIdChecks"][str(currentWorld)]:
ctx.kh2seedsave["worldIdChecks"][str(currentWorld)].append(location)
if location in ctx.kh2LocalItems:
item = ctx.kh2slotdata["LocalItems"][str(location)]
await asyncio.create_task(ctx.give_item(item, "LocalItems"))
await ctx.send_msgs(message)
elif not ctx.kh2connected and ctx.serverconneced:
logger.info("Game is not open. Disconnecting from Server.")
await ctx.disconnect()
except Exception as e:
logger.info("Line 661")
if ctx.kh2connected:
logger.info("Connection Lost.")
ctx.kh2connected = False
logger.info(e)
await asyncio.sleep(0.5)
if __name__ == '__main__':
async def main(args):
ctx = KH2Context(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
progression_watcher = asyncio.create_task(
kh2_watcher(ctx), name="KH2ProgressionWatcher")
await ctx.exit_event.wait()
ctx.server_address = None
await progression_watcher
await ctx.shutdown()
import colorama
parser = get_base_parser(description="KH2 Client, for text interfacing.")
args, rest = parser.parse_known_args()
colorama.init()
asyncio.run(main(args))
colorama.deinit()

View File

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

216
Launcher.py Normal file
View File

@@ -0,0 +1,216 @@
"""
Archipelago launcher for bundled app.
* if run with APBP as argument, launch corresponding client.
* if run with executable as argument, run it passing argv[2:] as arguments
* if run without arguments, open launcher GUI
Scroll down to components= to add components to the launcher as well as setup.py
"""
import argparse
import itertools
import shlex
import subprocess
import sys
from os.path import isfile
from shutil import which
from typing import Sequence, Union, Optional
from worlds.LauncherComponents import Component, components, Type, SuffixIdentifier
if __name__ == "__main__":
import ModuleUpdate
ModuleUpdate.update()
from Utils import is_frozen, user_path, local_path, init_logging, open_filename, messagebox, \
is_windows, is_macos, is_linux
def open_host_yaml():
file = user_path('host.yaml')
if is_linux:
exe = which('sensible-editor') or which('gedit') or \
which('xdg-open') or which('gnome-open') or which('kde-open')
subprocess.Popen([exe, file])
elif is_macos:
exe = which("open")
subprocess.Popen([exe, file])
else:
import webbrowser
webbrowser.open(file)
def open_patch():
suffixes = []
for c in components:
if isfile(get_exe(c)[-1]):
suffixes += c.file_identifier.suffixes if c.type == Type.CLIENT and \
isinstance(c.file_identifier, SuffixIdentifier) else []
try:
filename = open_filename('Select patch', (('Patches', suffixes),))
except Exception as e:
messagebox('Error', str(e), error=True)
else:
file, _, component = identify(filename)
if file and component:
launch([*get_exe(component), file], component.cli)
def browse_files():
file = user_path()
if is_linux:
exe = which('xdg-open') or which('gnome-open') or which('kde-open')
subprocess.Popen([exe, file])
elif is_macos:
exe = which("open")
subprocess.Popen([exe, file])
else:
import webbrowser
webbrowser.open(file)
components.extend([
# Functions
Component('Open host.yaml', func=open_host_yaml),
Component('Open Patch', func=open_patch),
Component('Browse Files', func=browse_files),
])
def identify(path: Union[None, str]):
if path is None:
return None, None, None
for component in components:
if component.handles_file(path):
return path, component.script_name, component
return (None, None, None) if '/' in path or '\\' in path else (None, path, None)
def get_exe(component: Union[str, Component]) -> Optional[Sequence[str]]:
if isinstance(component, str):
name = component
component = None
if name.startswith('Archipelago'):
name = name[11:]
if name.endswith('.exe'):
name = name[:-4]
if name.endswith('.py'):
name = name[:-3]
if not name:
return None
for c in components:
if c.script_name == name or c.frozen_name == f'Archipelago{name}':
component = c
break
if not component:
return None
if is_frozen():
suffix = '.exe' if is_windows else ''
return [local_path(f'{component.frozen_name}{suffix}')]
else:
return [sys.executable, local_path(f'{component.script_name}.py')]
def launch(exe, in_terminal=False):
if in_terminal:
if is_windows:
subprocess.Popen(['start', *exe], shell=True)
return
elif is_linux:
terminal = which('x-terminal-emulator') or which('gnome-terminal') or which('xterm')
if terminal:
subprocess.Popen([terminal, '-e', shlex.join(exe)])
return
elif is_macos:
terminal = [which('open'), '-W', '-a', 'Terminal.app']
subprocess.Popen([*terminal, *exe])
return
subprocess.Popen(exe)
def run_gui():
from kvui import App, ContainerLayout, GridLayout, Button, Label
class Launcher(App):
base_title: str = "Archipelago Launcher"
container: ContainerLayout
grid: GridLayout
_tools = {c.display_name: c for c in components if c.type == Type.TOOL and isfile(get_exe(c)[-1])}
_clients = {c.display_name: c for c in components if c.type == Type.CLIENT and isfile(get_exe(c)[-1])}
_adjusters = {c.display_name: c for c in components if c.type == Type.ADJUSTER and isfile(get_exe(c)[-1])}
_funcs = {c.display_name: c for c in components if c.type == Type.FUNC}
def __init__(self, ctx=None):
self.title = self.base_title
self.ctx = ctx
self.icon = r"data/icon.png"
super().__init__()
def build(self):
self.container = ContainerLayout()
self.grid = GridLayout(cols=2)
self.container.add_widget(self.grid)
button_layout = self.grid # make buttons fill the window
for (tool, client) in itertools.zip_longest(itertools.chain(
self._tools.items(), self._funcs.items(), self._adjusters.items()), self._clients.items()):
# column 1
if tool:
button = Button(text=tool[0])
button.component = tool[1]
button.bind(on_release=self.component_action)
button_layout.add_widget(button)
else:
button_layout.add_widget(Label())
# column 2
if client:
button = Button(text=client[0])
button.component = client[1]
button.bind(on_press=self.component_action)
button_layout.add_widget(button)
else:
button_layout.add_widget(Label())
return self.container
@staticmethod
def component_action(button):
if button.component.type == Type.FUNC:
button.component.func()
else:
launch(get_exe(button.component), button.component.cli)
Launcher().run()
def main(args: Optional[Union[argparse.Namespace, dict]] = None):
if isinstance(args, argparse.Namespace):
args = {k: v for k, v in args._get_kwargs()}
elif not args:
args = {}
if "Patch|Game|Component" in args:
file, component, _ = identify(args["Patch|Game|Component"])
if file:
args['file'] = file
if component:
args['component'] = component
if 'file' in args:
subprocess.run([*get_exe(args['component']), args['file'], *args['args']])
elif 'component' in args:
subprocess.run([*get_exe(args['component']), *args['args']])
else:
run_gui()
if __name__ == '__main__':
init_logging('Launcher')
parser = argparse.ArgumentParser(description='Archipelago Launcher')
parser.add_argument('Patch|Game|Component', type=str, nargs='?',
help="Pass either a patch file, a generated game or the name of a component to run.")
parser.add_argument('args', nargs="*", help="Arguments to pass to component.")
main(parser.parse_args())

609
LinksAwakeningClient.py Normal file
View File

@@ -0,0 +1,609 @@
import ModuleUpdate
ModuleUpdate.update()
import Utils
if __name__ == "__main__":
Utils.init_logging("LinksAwakeningContext", exception_logger="Client")
import asyncio
import base64
import binascii
import io
import logging
import select
import socket
import time
import typing
import urllib
import colorama
from CommonClient import (CommonContext, get_base_parser, gui_enabled, logger,
server_loop)
from NetUtils import ClientStatus
from worlds.ladx.Common import BASE_ID as LABaseID
from worlds.ladx.GpsTracker import GpsTracker
from worlds.ladx.ItemTracker import ItemTracker
from worlds.ladx.LADXR.checkMetadata import checkMetadataTable
from worlds.ladx.Locations import get_locations_to_id, meta_to_name
from worlds.ladx.Tracker import LocationTracker, MagpieBridge
class GameboyException(Exception):
pass
class RetroArchDisconnectError(GameboyException):
pass
class InvalidEmulatorStateError(GameboyException):
pass
class BadRetroArchResponse(GameboyException):
pass
def magpie_logo():
from kivy.uix.image import CoreImage
binary_data = """
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXN
SR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA
7DAcdvqGQAAADGSURBVDhPhVLBEcIwDHOYhjHCBuXHj2OTbAL8+
MEGZIxOQ1CinOOk0Op0bmo7tlXXeR9FJMYDLOD9mwcLjQK7+hSZ
wgcWMZJOAGeGKtChNHFL0j+FZD3jSCuo0w7l03wDrWdg00C4/aW
eDEYNenuzPOfPspBnxf0kssE80vN0L8361j10P03DK4x6FHabuV
ear8fHme+b17rwSjbAXeUMLb+EVTV2QHm46MWQanmnydA98KsVS
XkV+qFpGQXrLhT/fqraQeQLuplpNH5g+WkAAAAASUVORK5CYII="""
binary_data = base64.b64decode(binary_data)
data = io.BytesIO(binary_data)
return CoreImage(data, ext="png").texture
class LAClientConstants:
# Connector version
VERSION = 0x01
#
# Memory locations of LADXR
ROMGameID = 0x0051 # 4 bytes
SlotName = 0x0134
# Unused
# ROMWorldID = 0x0055
# ROMConnectorVersion = 0x0056
# RO: We should only act if this is higher then 6, as it indicates that the game is running normally
wGameplayType = 0xDB95
# RO: Starts at 0, increases every time an item is received from the server and processed
wLinkSyncSequenceNumber = 0xDDF6
wLinkStatusBits = 0xDDF7 # RW:
# Bit0: wLinkGive* contains valid data, set from script cleared from ROM.
wLinkHealth = 0xDB5A
wLinkGiveItem = 0xDDF8 # RW
wLinkGiveItemFrom = 0xDDF9 # RW
# All of these six bytes are unused, we can repurpose
# wLinkSendItemRoomHigh = 0xDDFA # RO
# wLinkSendItemRoomLow = 0xDDFB # RO
# wLinkSendItemTarget = 0xDDFC # RO
# wLinkSendItemItem = 0xDDFD # RO
# wLinkSendShopItem = 0xDDFE # RO, which item to send (1 based, order of the shop items)
# RO, which player to send to, but it's just the X position of the NPC used, so 0x18 is player 0
# wLinkSendShopTarget = 0xDDFF
wRecvIndex = 0xDDFE # 0xDB58
wCheckAddress = 0xC0FF - 0x4
WRamCheckSize = 0x4
WRamSafetyValue = bytearray([0]*WRamCheckSize)
MinGameplayValue = 0x06
MaxGameplayValue = 0x1A
VictoryGameplayAndSub = 0x0102
class RAGameboy():
cache = []
cache_start = 0
cache_size = 0
last_cache_read = None
socket = None
def __init__(self, address, port) -> None:
self.address = address
self.port = port
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
assert (self.socket)
self.socket.setblocking(False)
def get_retroarch_version(self):
self.send(b'VERSION\n')
select.select([self.socket], [], [])
response_str, addr = self.socket.recvfrom(16)
return response_str.rstrip()
def get_retroarch_status(self, timeout):
self.send(b'GET_STATUS\n')
select.select([self.socket], [], [], timeout)
response_str, addr = self.socket.recvfrom(1000, )
return response_str.rstrip()
def set_cache_limits(self, cache_start, cache_size):
self.cache_start = cache_start
self.cache_size = cache_size
def send(self, b):
if type(b) is str:
b = b.encode('ascii')
self.socket.sendto(b, (self.address, self.port))
def recv(self):
select.select([self.socket], [], [])
response, _ = self.socket.recvfrom(4096)
return response
async def async_recv(self):
response = await asyncio.get_event_loop().sock_recv(self.socket, 4096)
return response
async def check_safe_gameplay(self, throw=True):
async def check_wram():
check_values = await self.async_read_memory(LAClientConstants.wCheckAddress, LAClientConstants.WRamCheckSize)
if check_values != LAClientConstants.WRamSafetyValue:
if throw:
raise InvalidEmulatorStateError()
return False
return True
if not await check_wram():
if throw:
raise InvalidEmulatorStateError()
return False
gameplay_value = await self.async_read_memory(LAClientConstants.wGameplayType)
gameplay_value = gameplay_value[0]
# In gameplay or credits
if not (LAClientConstants.MinGameplayValue <= gameplay_value <= LAClientConstants.MaxGameplayValue) and gameplay_value != 0x1:
if throw:
logger.info("invalid emu state")
raise InvalidEmulatorStateError()
return False
if not await check_wram():
return False
return True
# We're sadly unable to update the whole cache at once
# as RetroArch only gives back some number of bytes at a time
# So instead read as big as chunks at a time as we can manage
async def update_cache(self):
# First read the safety address - if it's invalid, bail
self.cache = []
if not await self.check_safe_gameplay():
return
cache = []
remaining_size = self.cache_size
while remaining_size:
block = await self.async_read_memory(self.cache_start + len(cache), remaining_size)
remaining_size -= len(block)
cache += block
if not await self.check_safe_gameplay():
return
self.cache = cache
self.last_cache_read = time.time()
async def read_memory_cache(self, addresses):
# TODO: can we just update once per frame?
if not self.last_cache_read or self.last_cache_read + 0.1 < time.time():
await self.update_cache()
if not self.cache:
return None
assert (len(self.cache) == self.cache_size)
for address in addresses:
assert self.cache_start <= address <= self.cache_start + self.cache_size
r = {address: self.cache[address - self.cache_start]
for address in addresses}
return r
async def async_read_memory_safe(self, address, size=1):
# whenever we do a read for a check, we need to make sure that we aren't reading
# garbage memory values - we also need to protect against reading a value, then the emulator resetting
#
# ...actually, we probably _only_ need the post check
# Check before read
if not await self.check_safe_gameplay():
return None
# Do read
r = await self.async_read_memory(address, size)
# Check after read
if not await self.check_safe_gameplay():
return None
return r
def read_memory(self, address, size=1):
command = "READ_CORE_MEMORY"
self.send(f'{command} {hex(address)} {size}\n')
response = self.recv()
splits = response.decode().split(" ", 2)
assert (splits[0] == command)
# Ignore the address for now
# TODO: transform to bytes
if splits[2][:2] == "-1" or splits[0] != "READ_CORE_MEMORY":
raise BadRetroArchResponse()
return bytearray.fromhex(splits[2])
async def async_read_memory(self, address, size=1):
command = "READ_CORE_MEMORY"
self.send(f'{command} {hex(address)} {size}\n')
response = await self.async_recv()
response = response[:-1]
splits = response.decode().split(" ", 2)
assert (splits[0] == command)
# Ignore the address for now
# TODO: transform to bytes
return bytearray.fromhex(splits[2])
def write_memory(self, address, bytes):
command = "WRITE_CORE_MEMORY"
self.send(f'{command} {hex(address)} {" ".join(hex(b) for b in bytes)}')
select.select([self.socket], [], [])
response, _ = self.socket.recvfrom(4096)
splits = response.decode().split(" ", 3)
assert (splits[0] == command)
if splits[2] == "-1":
logger.info(splits[3])
class LinksAwakeningClient():
socket = None
gameboy = None
tracker = None
auth = None
game_crc = None
pending_deathlink = False
deathlink_debounce = True
recvd_checks = {}
def msg(self, m):
logger.info(m)
s = f"SHOW_MSG {m}\n"
self.gameboy.send(s)
def __init__(self, retroarch_address="127.0.0.1", retroarch_port=55355):
self.gameboy = RAGameboy(retroarch_address, retroarch_port)
async def wait_for_retroarch_connection(self):
logger.info("Waiting on connection to Retroarch...")
while True:
try:
version = self.gameboy.get_retroarch_version()
NO_CONTENT = b"GET_STATUS CONTENTLESS"
status = NO_CONTENT
core_type = None
GAME_BOY = b"game_boy"
while status == NO_CONTENT or core_type != GAME_BOY:
try:
status = self.gameboy.get_retroarch_status(0.1)
if status.count(b" ") < 2:
await asyncio.sleep(1.0)
continue
GET_STATUS, PLAYING, info = status.split(b" ", 2)
if status.count(b",") < 2:
await asyncio.sleep(1.0)
continue
core_type, rom_name, self.game_crc = info.split(b",", 2)
if core_type != GAME_BOY:
logger.info(
f"Core type should be '{GAME_BOY}', found {core_type} instead - wrong type of ROM?")
await asyncio.sleep(1.0)
continue
except (BlockingIOError, TimeoutError) as e:
await asyncio.sleep(0.1)
pass
logger.info(f"Connected to Retroarch {version} {info}")
self.gameboy.read_memory(0x1000)
return
except ConnectionResetError:
await asyncio.sleep(1.0)
pass
def reset_auth(self):
auth = binascii.hexlify(self.gameboy.read_memory(0x0134, 12)).decode()
if self.auth:
assert (auth == self.auth)
self.auth = auth
async def wait_and_init_tracker(self):
await self.wait_for_game_ready()
self.tracker = LocationTracker(self.gameboy)
self.item_tracker = ItemTracker(self.gameboy)
self.gps_tracker = GpsTracker(self.gameboy)
async def recved_item_from_ap(self, item_id, from_player, next_index):
# Don't allow getting an item until you've got your first check
if not self.tracker.has_start_item():
return
# Spin until we either:
# get an exception from a bad read (emu shut down or reset)
# beat the game
# the client handles the last pending item
status = (await self.gameboy.async_read_memory_safe(LAClientConstants.wLinkStatusBits))[0]
while not (await self.is_victory()) and status & 1 == 1:
time.sleep(0.1)
status = (await self.gameboy.async_read_memory_safe(LAClientConstants.wLinkStatusBits))[0]
item_id -= LABaseID
# The player name table only goes up to 100, so don't go past that
# Even if it didn't, the remote player _index_ byte is just a byte, so 255 max
if from_player > 100:
from_player = 100
next_index += 1
self.gameboy.write_memory(LAClientConstants.wLinkGiveItem, [
item_id, from_player])
status |= 1
status = self.gameboy.write_memory(LAClientConstants.wLinkStatusBits, [status])
self.gameboy.write_memory(LAClientConstants.wRecvIndex, [next_index])
async def wait_for_game_ready(self):
logger.info("Waiting on game to be in valid state...")
while not await self.gameboy.check_safe_gameplay(throw=False):
pass
logger.info("Ready!")
last_index = 0
async def is_victory(self):
return (await self.gameboy.read_memory_cache([LAClientConstants.wGameplayType]))[LAClientConstants.wGameplayType] == 1
async def main_tick(self, item_get_cb, win_cb, deathlink_cb):
await self.tracker.readChecks(item_get_cb)
await self.item_tracker.readItems()
await self.gps_tracker.read_location()
next_index = self.gameboy.read_memory(LAClientConstants.wRecvIndex)[0]
if next_index != self.last_index:
self.last_index = next_index
# logger.info(f"Got new index {next_index}")
current_health = (await self.gameboy.read_memory_cache([LAClientConstants.wLinkHealth]))[LAClientConstants.wLinkHealth]
if self.deathlink_debounce and current_health != 0:
self.deathlink_debounce = False
elif not self.deathlink_debounce and current_health == 0:
# logger.info("YOU DIED.")
await deathlink_cb()
self.deathlink_debounce = True
if self.pending_deathlink:
logger.info("Got a deathlink")
self.gameboy.write_memory(LAClientConstants.wLinkHealth, [0])
self.pending_deathlink = False
self.deathlink_debounce = True
if await self.is_victory():
await win_cb()
recv_index = (await self.gameboy.async_read_memory_safe(LAClientConstants.wRecvIndex))[0]
# Play back one at a time
if recv_index in self.recvd_checks:
item = self.recvd_checks[recv_index]
await self.recved_item_from_ap(item.item, item.player, recv_index)
all_tasks = set()
def create_task_log_exception(awaitable) -> asyncio.Task:
async def _log_exception(awaitable):
try:
return await awaitable
except Exception as e:
logger.exception(e)
pass
finally:
all_tasks.remove(task)
task = asyncio.create_task(_log_exception(awaitable))
all_tasks.add(task)
class LinksAwakeningContext(CommonContext):
tags = {"AP"}
game = "Links Awakening DX"
items_handling = 0b101
want_slot_data = True
la_task = None
client = None
# TODO: does this need to re-read on reset?
found_checks = []
last_resend = time.time()
magpie = MagpieBridge()
magpie_task = None
won = False
def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str]) -> None:
self.client = LinksAwakeningClient()
super().__init__(server_address, password)
def run_gui(self) -> None:
import webbrowser
import kvui
from kvui import Button, GameManager
from kivy.uix.image import Image
class LADXManager(GameManager):
logging_pairs = [
("Client", "Archipelago"),
("Tracker", "Tracker"),
]
base_title = "Archipelago Links Awakening DX Client"
def build(self):
b = super().build()
button = Button(text="", size=(30, 30), size_hint_x=None,
on_press=lambda _: webbrowser.open('https://magpietracker.us/?enable_autotracker=1'))
image = Image(size=(16, 16), texture=magpie_logo())
button.add_widget(image)
def set_center(_, center):
image.center = center
button.bind(center=set_center)
self.connect_layout.add_widget(button)
return b
self.ui = LADXManager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
async def send_checks(self):
message = [{"cmd": 'LocationChecks', "locations": self.found_checks}]
await self.send_msgs(message)
ENABLE_DEATHLINK = False
async def send_deathlink(self):
if self.ENABLE_DEATHLINK:
message = [{"cmd": 'Deathlink',
'time': time.time(),
'cause': 'Had a nightmare',
# 'source': self.slot_info[self.slot].name,
}]
await self.send_msgs(message)
async def send_victory(self):
if not self.won:
message = [{"cmd": "StatusUpdate",
"status": ClientStatus.CLIENT_GOAL}]
logger.info("victory!")
await self.send_msgs(message)
self.won = True
async def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None:
if self.ENABLE_DEATHLINK:
self.client.pending_deathlink = True
def new_checks(self, item_ids, ladxr_ids):
self.found_checks += item_ids
create_task_log_exception(self.send_checks())
create_task_log_exception(self.magpie.send_new_checks(ladxr_ids))
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(LinksAwakeningContext, self).server_auth(password_requested)
self.auth = self.client.auth
await self.get_username()
await self.send_connect()
def on_package(self, cmd: str, args: dict):
if cmd == "Connected":
self.game = self.slot_info[self.slot].game
# TODO - use watcher_event
if cmd == "ReceivedItems":
for index, item in enumerate(args["items"], args["index"]):
self.client.recvd_checks[index] = item
item_id_lookup = get_locations_to_id()
async def run_game_loop(self):
def on_item_get(ladxr_checks):
checks = [self.item_id_lookup[meta_to_name(
checkMetadataTable[check.id])] for check in ladxr_checks]
self.new_checks(checks, [check.id for check in ladxr_checks])
async def victory():
await self.send_victory()
async def deathlink():
await self.send_deathlink()
self.magpie_task = asyncio.create_task(self.magpie.serve())
# yield to allow UI to start
await asyncio.sleep(0)
while True:
try:
# TODO: cancel all client tasks
logger.info("(Re)Starting game loop")
self.found_checks.clear()
await self.client.wait_for_retroarch_connection()
self.client.reset_auth()
await self.client.wait_and_init_tracker()
while True:
await self.client.main_tick(on_item_get, victory, deathlink)
await asyncio.sleep(0.1)
now = time.time()
if self.last_resend + 5.0 < now:
self.last_resend = now
await self.send_checks()
self.magpie.set_checks(self.client.tracker.all_checks)
await self.magpie.set_item_tracker(self.client.item_tracker)
await self.magpie.send_gps(self.client.gps_tracker)
except GameboyException:
time.sleep(1.0)
pass
async def main():
parser = get_base_parser(description="Link's Awakening Client.")
parser.add_argument("--url", help="Archipelago connection url")
parser.add_argument('diff_file', default="", type=str, nargs="?",
help='Path to a .apladx Archipelago Binary Patch file')
args = parser.parse_args()
logger.info(args)
if args.diff_file:
import Patch
logger.info("patch file was supplied - creating rom...")
meta, rom_file = Patch.create_rom_file(args.diff_file)
if "server" in meta:
args.url = meta["server"]
logger.info(f"wrote rom file to {rom_file}")
if args.url:
url = urllib.parse.urlparse(args.url)
args.connect = url.netloc
if url.password:
args.password = urllib.parse.unquote(url.password)
ctx = LinksAwakeningContext(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
# TODO: nothing about the lambda about has to be in a lambda
ctx.la_task = create_task_log_exception(ctx.run_game_loop())
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
await ctx.exit_event.wait()
await ctx.shutdown()
if __name__ == '__main__':
colorama.init()
asyncio.run(main())
colorama.deinit()

View File

@@ -14,20 +14,28 @@ import tkinter as tk
from argparse import Namespace
from concurrent.futures import as_completed, ThreadPoolExecutor
from glob import glob
from tkinter import Tk, Frame, Label, StringVar, Entry, filedialog, messagebox, Button, LEFT, X, TOP, LabelFrame, \
IntVar, Checkbutton, E, OptionMenu, Toplevel, BOTTOM, RIGHT, font as font, PhotoImage
from tkinter import Tk, Frame, Label, StringVar, Entry, filedialog, messagebox, Button, Radiobutton, LEFT, X, TOP, LabelFrame, \
IntVar, Checkbutton, E, W, OptionMenu, Toplevel, BOTTOM, RIGHT, font as font, PhotoImage
from tkinter.constants import DISABLED, NORMAL
from urllib.parse import urlparse
from urllib.request import urlopen
import ModuleUpdate
ModuleUpdate.update()
from worlds.alttp.Rom import Sprite, LocalRom, apply_rom_settings, get_base_rom_bytes
from Utils import output_path, local_path, open_file
from Utils import output_path, local_path, user_path, open_file, get_cert_none_ssl_context, persistent_store, \
get_adjuster_settings, tkinter_center_window, init_logging
GAME_ALTTP = "A Link to the Past"
class AdjusterWorld(object):
def __init__(self, sprite_pool):
import random
self.sprite_pool = {1: sprite_pool}
self.slot_seeds = {1: random}
self.per_slot_randoms = {1: random}
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
@@ -39,9 +47,9 @@ class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):
def main():
parser = argparse.ArgumentParser(formatter_class=ArgumentDefaultsHelpFormatter)
parser.add_argument('--rom', default='ER_base.sfc', help='Path to an ALttP rom to adjust.')
parser.add_argument('rom', nargs="?", default='AP_LttP.sfc', help='Path to an ALttP rom to adjust.')
parser.add_argument('--baserom', default='Zelda no Densetsu - Kamigami no Triforce (Japan).sfc',
help='Path to an ALttP JAP(1.0) rom to use as a base.')
help='Path to an ALttP Japan(1.0) rom to use as a base.')
parser.add_argument('--loglevel', default='info', const='info', nargs='?',
choices=['error', 'info', 'warning', 'debug'], help='Select level of logging for output.')
parser.add_argument('--menuspeed', default='normal', const='normal', nargs='?',
@@ -51,6 +59,8 @@ def main():
(default: %(default)s)
''')
parser.add_argument('--quickswap', help='Enable quick item swapping with L and R.', action='store_true')
parser.add_argument('--deathlink', help='Enable DeathLink system.', action='store_true')
parser.add_argument('--allowcollect', help='Allow collection of other player items', action='store_true')
parser.add_argument('--disablemusic', help='Disables game music.', action='store_true')
parser.add_argument('--triforcehud', default='hide_goal', const='hide_goal', nargs='?',
choices=['normal', 'hide_goal', 'hide_required', 'hide_both'],
@@ -75,9 +85,9 @@ def main():
parser.add_argument('--ow_palettes', default='default',
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
'sick'])
parser.add_argument('--link_palettes', default='default',
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
'sick'])
# parser.add_argument('--link_palettes', default='default',
# choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
# 'sick'])
parser.add_argument('--shield_palettes', default='default',
choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy',
'sick'])
@@ -101,14 +111,15 @@ def main():
parser.add_argument('--update_sprites', action='store_true', help='Update Sprite Database, then exit.')
args = parser.parse_args()
args.music = not args.disablemusic
if args.update_sprites:
run_sprite_update()
sys.exit()
# set up logger
loglevel = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}[
args.loglevel]
logging.basicConfig(format='%(message)s', level=loglevel)
if args.update_sprites:
run_sprite_update()
sys.exit()
if not os.path.isfile(args.rom):
adjustGUI()
else:
@@ -117,18 +128,20 @@ def main():
sys.exit(1)
args, path = adjust(args=args)
from Utils import persistent_store
if isinstance(args.sprite, Sprite):
args.sprite = args.sprite.name
persistent_store("adjuster", "last_settings_3", args)
persistent_store("adjuster", GAME_ALTTP, args)
def adjust(args):
start = time.perf_counter()
init_logging("LttP Adjuster")
logger = logging.getLogger('Adjuster')
logger.info('Patching ROM.')
vanillaRom = args.baserom
if os.path.splitext(args.rom)[-1].lower() == '.apbp':
if not os.path.exists(vanillaRom) and not os.path.isabs(vanillaRom):
vanillaRom = local_path(vanillaRom)
if os.path.splitext(args.rom)[-1].lower() == '.aplttp':
import Patch
meta, args.rom = Patch.create_rom_file(args.rom)
@@ -152,7 +165,8 @@ def adjust(args):
world = getattr(args, "world")
apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.menuspeed, args.music,
args.sprite, palettes_options, reduceflashing=args.reduceflashing or racerom, world=world)
args.sprite, palettes_options, reduceflashing=args.reduceflashing or racerom, world=world,
deathlink=args.deathlink, allowcollect=args.allowcollect)
path = output_path(f'{os.path.basename(args.rom)[:-4]}_adjusted.sfc')
rom.write_to_file(path)
@@ -183,7 +197,7 @@ def adjustGUI():
romEntry2 = Entry(romDialogFrame, textvariable=romVar2)
def RomSelect2():
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc", ".apbp")), ("All Files", "*")])
rom = filedialog.askopenfilename(filetypes=[("Rom Files", (".sfc", ".smc", ".aplttp")), ("All Files", "*")])
romVar2.set(rom)
romSelectButton2 = Button(romDialogFrame, text='Select Rom', command=RomSelect2)
@@ -194,6 +208,7 @@ def adjustGUI():
def adjustRom():
guiargs = Namespace()
guiargs.auto_apply = rom_vars.auto_apply.get()
guiargs.heartbeep = rom_vars.heartbeepVar.get()
guiargs.heartcolor = rom_vars.heartcolorVar.get()
guiargs.menuspeed = rom_vars.menuspeedVar.get()
@@ -205,6 +220,8 @@ def adjustGUI():
guiargs.quickswap = bool(rom_vars.quickSwapVar.get())
guiargs.music = bool(rom_vars.MusicVar.get())
guiargs.reduceflashing = bool(rom_vars.disableFlashingVar.get())
guiargs.deathlink = bool(rom_vars.DeathLinkVar.get())
guiargs.allowcollect = bool(rom_vars.AllowCollectVar.get())
guiargs.rom = romVar2.get()
guiargs.baserom = romVar.get()
guiargs.sprite = rom_vars.sprite
@@ -221,36 +238,70 @@ def adjustGUI():
messagebox.showerror(title="Error while adjusting Rom", message=str(e))
else:
messagebox.showinfo(title="Success", message=f"Rom patched successfully to {path}")
from Utils import persistent_store
if isinstance(guiargs.sprite, Sprite):
guiargs.sprite = guiargs.sprite.name
persistent_store("adjuster", "last_settings_3", guiargs)
delattr(guiargs, "rom")
persistent_store("adjuster", GAME_ALTTP, guiargs)
def saveGUISettings():
guiargs = Namespace()
guiargs.auto_apply = rom_vars.auto_apply.get()
guiargs.heartbeep = rom_vars.heartbeepVar.get()
guiargs.heartcolor = rom_vars.heartcolorVar.get()
guiargs.menuspeed = rom_vars.menuspeedVar.get()
guiargs.ow_palettes = rom_vars.owPalettesVar.get()
guiargs.uw_palettes = rom_vars.uwPalettesVar.get()
guiargs.hud_palettes = rom_vars.hudPalettesVar.get()
guiargs.sword_palettes = rom_vars.swordPalettesVar.get()
guiargs.shield_palettes = rom_vars.shieldPalettesVar.get()
guiargs.quickswap = bool(rom_vars.quickSwapVar.get())
guiargs.music = bool(rom_vars.MusicVar.get())
guiargs.reduceflashing = bool(rom_vars.disableFlashingVar.get())
guiargs.deathlink = bool(rom_vars.DeathLinkVar.get())
guiargs.allowcollect = bool(rom_vars.AllowCollectVar.get())
guiargs.baserom = romVar.get()
if isinstance(rom_vars.sprite, Sprite):
guiargs.sprite = rom_vars.sprite.name
else:
guiargs.sprite = rom_vars.sprite
guiargs.sprite_pool = rom_vars.sprite_pool
persistent_store("adjuster", GAME_ALTTP, guiargs)
messagebox.showinfo(title="Success", message="Settings saved to persistent storage")
adjustButton = Button(bottomFrame2, text='Adjust Rom', command=adjustRom)
rom_options_frame.pack(side=TOP)
adjustButton.pack(side=BOTTOM, padx=(5, 5))
adjustButton.pack(side=LEFT, padx=(5,5))
bottomFrame2.pack(side=BOTTOM, pady=(5, 5))
saveButton = Button(bottomFrame2, text='Save Settings', command=saveGUISettings)
saveButton.pack(side=LEFT, padx=(5,5))
bottomFrame2.pack(side=TOP, pady=(5,5))
tkinter_center_window(adjustWindow)
adjustWindow.mainloop()
def run_sprite_update():
import threading
done = threading.Event()
top = Tk()
top.withdraw()
BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
while not done.isSet():
top.update()
print("Done updating sprites")
try:
top = Tk()
except:
task = BackgroundTaskProgressNullWindow(update_sprites, lambda successful, resultmessage: done.set())
else:
top.withdraw()
task = BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
while not done.is_set():
task.do_events()
logging.info("Done updating sprites")
def update_sprites(task, on_finish=None):
resultmessage = ""
successful = True
sprite_dir = local_path("data", "sprites", "alttpr")
sprite_dir = user_path("data", "sprites", "alttpr")
os.makedirs(sprite_dir, exist_ok=True)
ctx = get_cert_none_ssl_context()
def finished():
task.close_window()
@@ -259,7 +310,7 @@ def update_sprites(task, on_finish=None):
try:
task.update_status("Downloading alttpr sprites list")
with urlopen('https://alttpr.com/sprites') as response:
with urlopen('https://alttpr.com/sprites', context=ctx) as response:
sprites_arr = json.loads(response.read().decode("utf-8"))
except Exception as e:
resultmessage = "Error getting list of alttpr sprites. Sprites not updated.\n\n%s: %s" % (type(e).__name__, e)
@@ -272,26 +323,26 @@ def update_sprites(task, on_finish=None):
current_sprites = [os.path.basename(file) for file in glob(sprite_dir + '/*')]
alttpr_sprites = [(sprite['file'], os.path.basename(urlparse(sprite['file']).path))
for sprite in sprites_arr if sprite["author"] != "Nintendo"]
needed_sprites = [(sprite_url, filename) for (sprite_url, filename) in alttpr_sprites if filename not in current_sprites]
needed_sprites = [(sprite_url, filename) for (sprite_url, filename) in alttpr_sprites if
filename not in current_sprites]
alttpr_filenames = [filename for (_, filename) in alttpr_sprites]
obsolete_sprites = [sprite for sprite in current_sprites if sprite not in alttpr_filenames]
except Exception as e:
resultmessage = "Error Determining which sprites to update. Sprites not updated.\n\n%s: %s" % (type(e).__name__, e)
resultmessage = "Error Determining which sprites to update. Sprites not updated.\n\n%s: %s" % (
type(e).__name__, e)
successful = False
task.queue_event(finished)
return
def dl(sprite_url, filename):
target = os.path.join(sprite_dir, filename)
with urlopen(sprite_url) as response, open(target, 'wb') as out:
with urlopen(sprite_url, context=ctx) as response, open(target, 'wb') as out:
shutil.copyfileobj(response, out)
def rem(sprite):
os.remove(os.path.join(sprite_dir, sprite))
with ThreadPoolExecutor() as pool:
dl_tasks = []
rem_tasks = []
@@ -313,7 +364,7 @@ def update_sprites(task, on_finish=None):
except Exception as e:
logging.exception(e)
resultmessage = "Error downloading sprite. Not all sprites updated.\n\n%s: %s" % (
type(e).__name__, e)
type(e).__name__, e)
successful = False
for rem_task in as_completed(rem_tasks):
@@ -324,7 +375,7 @@ def update_sprites(task, on_finish=None):
except Exception as e:
logging.exception(e)
resultmessage = "Error removing obsolete sprite. Not all sprites updated.\n\n%s: %s" % (
type(e).__name__, e)
type(e).__name__, e)
successful = False
if successful:
@@ -362,7 +413,7 @@ class BackgroundTask(object):
event = self.queue.get_nowait()
event()
if self.running:
#if self is no longer running self.window may no longer be valid
# if self is no longer running self.window may no longer be valid
self.window.update_idletasks()
except queue.Empty:
pass
@@ -397,16 +448,48 @@ class BackgroundTaskProgress(BackgroundTask):
def update_status(self, text):
self.queue_event(lambda: self.label_var.set(text))
def do_events(self):
self.parent.update()
# only call this in an event callback
def close_window(self):
self.stop()
self.window.destroy()
class BackgroundTaskProgressNullWindow(BackgroundTask):
def __init__(self, code_to_run, *args):
super().__init__(None, code_to_run, *args)
def process_queue(self):
try:
while True:
if not self.running:
return
event = self.queue.get_nowait()
event()
except queue.Empty:
pass
def do_events(self):
self.process_queue()
def update_status(self, text):
self.queue_event(lambda: logging.info(text))
def close_window(self):
self.stop()
def get_rom_frame(parent=None):
adjuster_settings = get_adjuster_settings(GAME_ALTTP)
if not adjuster_settings:
adjuster_settings = Namespace()
adjuster_settings.baserom = "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"
romFrame = Frame(parent)
baseRomLabel = Label(romFrame, text='LttP Base Rom: ')
romVar = StringVar(value="Zelda no Densetsu - Kamigami no Triforce (Japan).sfc")
romVar = StringVar(value=adjuster_settings.baserom)
romEntry = Entry(romFrame, textvariable=romVar)
def RomSelect():
@@ -420,6 +503,7 @@ def get_rom_frame(parent=None):
romVar.set(rom)
romSelectButton['state'] = "disabled"
romSelectButton["text"] = "ROM verified"
romSelectButton = Button(romFrame, text='Select Rom', command=RomSelect)
baseRomLabel.pack(side=LEFT)
@@ -431,6 +515,31 @@ def get_rom_frame(parent=None):
def get_rom_options_frame(parent=None):
adjuster_settings = get_adjuster_settings(GAME_ALTTP)
defaults = {
"auto_apply": 'ask',
"music": True,
"reduceflashing": True,
"deathlink": False,
"sprite": None,
"quickswap": True,
"menuspeed": 'normal',
"heartcolor": 'red',
"heartbeep": 'normal',
"ow_palettes": 'default',
"uw_palettes": 'default',
"hud_palettes": 'default',
"sword_palettes": 'default',
"shield_palettes": 'default',
"sprite_pool": [],
"allowcollect": False,
}
if not adjuster_settings:
adjuster_settings = Namespace()
for key, defaultvalue in defaults.items():
if not hasattr(adjuster_settings, key):
setattr(adjuster_settings, key, defaultvalue)
romOptionsFrame = LabelFrame(parent, text="Rom options")
romOptionsFrame.columnconfigure(0, weight=1)
romOptionsFrame.columnconfigure(1, weight=1)
@@ -439,22 +548,30 @@ def get_rom_options_frame(parent=None):
vars = Namespace()
vars.MusicVar = IntVar()
vars.MusicVar.set(1)
vars.MusicVar.set(adjuster_settings.music)
MusicCheckbutton = Checkbutton(romOptionsFrame, text="Music", variable=vars.MusicVar)
MusicCheckbutton.grid(row=0, column=0, sticky=E)
vars.disableFlashingVar = IntVar(value=1)
disableFlashingCheckbutton = Checkbutton(romOptionsFrame, text="Disable flashing (anti-epilepsy)", variable=vars.disableFlashingVar)
disableFlashingCheckbutton.grid(row=6, column=0, sticky=E)
vars.disableFlashingVar = IntVar(value=adjuster_settings.reduceflashing)
disableFlashingCheckbutton = Checkbutton(romOptionsFrame, text="Disable flashing (anti-epilepsy)",
variable=vars.disableFlashingVar)
disableFlashingCheckbutton.grid(row=6, column=0, sticky=W)
vars.DeathLinkVar = IntVar(value=adjuster_settings.deathlink)
DeathLinkCheckbutton = Checkbutton(romOptionsFrame, text="DeathLink (Team Deaths)", variable=vars.DeathLinkVar)
DeathLinkCheckbutton.grid(row=7, column=0, sticky=W)
vars.AllowCollectVar = IntVar(value=adjuster_settings.allowcollect)
AllowCollectCheckbutton = Checkbutton(romOptionsFrame, text="Allow Collect", variable=vars.AllowCollectVar)
AllowCollectCheckbutton.grid(row=8, column=0, sticky=W)
spriteDialogFrame = Frame(romOptionsFrame)
spriteDialogFrame.grid(row=0, column=1)
baseSpriteLabel = Label(spriteDialogFrame, text='Sprite:')
vars.spriteNameVar = StringVar()
vars.sprite = None
vars.sprite = adjuster_settings.sprite
def set_sprite(sprite_param):
nonlocal vars
if isinstance(sprite_param, str):
@@ -467,8 +584,8 @@ def get_rom_options_frame(parent=None):
vars.sprite = sprite_param
vars.spriteNameVar.set(vars.sprite.name)
set_sprite(None)
vars.spriteNameVar.set('(unchanged)')
set_sprite(adjuster_settings.sprite)
#vars.spriteNameVar.set(adjuster_settings.sprite)
spriteEntry = Label(spriteDialogFrame, textvariable=vars.spriteNameVar)
def SpriteSelect():
@@ -481,7 +598,7 @@ def get_rom_options_frame(parent=None):
spriteEntry.pack(side=LEFT)
spriteSelectButton.pack(side=LEFT)
vars.quickSwapVar = IntVar(value=1)
vars.quickSwapVar = IntVar(value=adjuster_settings.quickswap)
quickSwapCheckbutton = Checkbutton(romOptionsFrame, text="L/R Quickswapping", variable=vars.quickSwapVar)
quickSwapCheckbutton.grid(row=1, column=0, sticky=E)
@@ -490,8 +607,9 @@ def get_rom_options_frame(parent=None):
menuspeedLabel = Label(menuspeedFrame, text='Menu speed')
menuspeedLabel.pack(side=LEFT)
vars.menuspeedVar = StringVar()
vars.menuspeedVar.set('normal')
menuspeedOptionMenu = OptionMenu(menuspeedFrame, vars.menuspeedVar, 'normal', 'instant', 'double', 'triple', 'quadruple', 'half')
vars.menuspeedVar.set(adjuster_settings.menuspeed)
menuspeedOptionMenu = OptionMenu(menuspeedFrame, vars.menuspeedVar, 'normal', 'instant', 'double', 'triple',
'quadruple', 'half')
menuspeedOptionMenu.pack(side=LEFT)
heartcolorFrame = Frame(romOptionsFrame)
@@ -499,7 +617,7 @@ def get_rom_options_frame(parent=None):
heartcolorLabel = Label(heartcolorFrame, text='Heart color')
heartcolorLabel.pack(side=LEFT)
vars.heartcolorVar = StringVar()
vars.heartcolorVar.set('red')
vars.heartcolorVar.set(adjuster_settings.heartcolor)
heartcolorOptionMenu = OptionMenu(heartcolorFrame, vars.heartcolorVar, 'red', 'blue', 'green', 'yellow', 'random')
heartcolorOptionMenu.pack(side=LEFT)
@@ -508,7 +626,7 @@ def get_rom_options_frame(parent=None):
heartbeepLabel = Label(heartbeepFrame, text='Heartbeep')
heartbeepLabel.pack(side=LEFT)
vars.heartbeepVar = StringVar()
vars.heartbeepVar.set('normal')
vars.heartbeepVar.set(adjuster_settings.heartbeep)
heartbeepOptionMenu = OptionMenu(heartbeepFrame, vars.heartbeepVar, 'double', 'normal', 'half', 'quarter', 'off')
heartbeepOptionMenu.pack(side=LEFT)
@@ -517,8 +635,9 @@ def get_rom_options_frame(parent=None):
owPalettesLabel = Label(owPalettesFrame, text='Overworld palettes')
owPalettesLabel.pack(side=LEFT)
vars.owPalettesVar = StringVar()
vars.owPalettesVar.set('default')
owPalettesOptionMenu = OptionMenu(owPalettesFrame, vars.owPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
vars.owPalettesVar.set(adjuster_settings.ow_palettes)
owPalettesOptionMenu = OptionMenu(owPalettesFrame, vars.owPalettesVar, 'default', 'good', 'blackout', 'grayscale',
'negative', 'classic', 'dizzy', 'sick', 'puke')
owPalettesOptionMenu.pack(side=LEFT)
uwPalettesFrame = Frame(romOptionsFrame)
@@ -526,8 +645,9 @@ def get_rom_options_frame(parent=None):
uwPalettesLabel = Label(uwPalettesFrame, text='Dungeon palettes')
uwPalettesLabel.pack(side=LEFT)
vars.uwPalettesVar = StringVar()
vars.uwPalettesVar.set('default')
uwPalettesOptionMenu = OptionMenu(uwPalettesFrame, vars.uwPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
vars.uwPalettesVar.set(adjuster_settings.uw_palettes)
uwPalettesOptionMenu = OptionMenu(uwPalettesFrame, vars.uwPalettesVar, 'default', 'good', 'blackout', 'grayscale',
'negative', 'classic', 'dizzy', 'sick', 'puke')
uwPalettesOptionMenu.pack(side=LEFT)
hudPalettesFrame = Frame(romOptionsFrame)
@@ -535,8 +655,9 @@ def get_rom_options_frame(parent=None):
hudPalettesLabel = Label(hudPalettesFrame, text='HUD palettes')
hudPalettesLabel.pack(side=LEFT)
vars.hudPalettesVar = StringVar()
vars.hudPalettesVar.set('default')
hudPalettesOptionMenu = OptionMenu(hudPalettesFrame, vars.hudPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
vars.hudPalettesVar.set(adjuster_settings.hud_palettes)
hudPalettesOptionMenu = OptionMenu(hudPalettesFrame, vars.hudPalettesVar, 'default', 'good', 'blackout',
'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
hudPalettesOptionMenu.pack(side=LEFT)
swordPalettesFrame = Frame(romOptionsFrame)
@@ -544,8 +665,9 @@ def get_rom_options_frame(parent=None):
swordPalettesLabel = Label(swordPalettesFrame, text='Sword palettes')
swordPalettesLabel.pack(side=LEFT)
vars.swordPalettesVar = StringVar()
vars.swordPalettesVar.set('default')
swordPalettesOptionMenu = OptionMenu(swordPalettesFrame, vars.swordPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
vars.swordPalettesVar.set(adjuster_settings.sword_palettes)
swordPalettesOptionMenu = OptionMenu(swordPalettesFrame, vars.swordPalettesVar, 'default', 'good', 'blackout',
'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
swordPalettesOptionMenu.pack(side=LEFT)
shieldPalettesFrame = Frame(romOptionsFrame)
@@ -553,8 +675,9 @@ def get_rom_options_frame(parent=None):
shieldPalettesLabel = Label(shieldPalettesFrame, text='Shield palettes')
shieldPalettesLabel.pack(side=LEFT)
vars.shieldPalettesVar = StringVar()
vars.shieldPalettesVar.set('default')
shieldPalettesOptionMenu = OptionMenu(shieldPalettesFrame, vars.shieldPalettesVar, 'default', 'good', 'blackout', 'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
vars.shieldPalettesVar.set(adjuster_settings.shield_palettes)
shieldPalettesOptionMenu = OptionMenu(shieldPalettesFrame, vars.shieldPalettesVar, 'default', 'good', 'blackout',
'grayscale', 'negative', 'classic', 'dizzy', 'sick', 'puke')
shieldPalettesOptionMenu.pack(side=LEFT)
spritePoolFrame = Frame(romOptionsFrame)
@@ -562,7 +685,8 @@ def get_rom_options_frame(parent=None):
baseSpritePoolLabel = Label(spritePoolFrame, text='Sprite Pool:')
vars.spritePoolCountVar = StringVar()
vars.sprite_pool = []
vars.sprite_pool = adjuster_settings.sprite_pool
def set_sprite_pool(sprite_param):
nonlocal vars
operation = "add"
@@ -580,7 +704,7 @@ def get_rom_options_frame(parent=None):
vars.spritePoolCountVar.set(str(len(vars.sprite_pool)))
set_sprite_pool(None)
vars.spritePoolCountVar.set('0')
vars.spritePoolCountVar.set(len(adjuster_settings.sprite_pool))
spritePoolEntry = Label(spritePoolFrame, textvariable=vars.spritePoolCountVar)
def SpritePoolSelect():
@@ -600,6 +724,18 @@ def get_rom_options_frame(parent=None):
spritePoolSelectButton.pack(side=LEFT)
spritePoolClearButton.pack(side=LEFT)
vars.auto_apply = StringVar(value=adjuster_settings.auto_apply)
autoApplyFrame = Frame(romOptionsFrame)
autoApplyFrame.grid(row=9, column=0, columnspan=2, sticky=W)
filler = Label(autoApplyFrame, text="Automatically apply last used settings on opening .aplttp files")
filler.pack(side=TOP, expand=True, fill=X)
askRadio = Radiobutton(autoApplyFrame, text='Ask', variable=vars.auto_apply, value='ask')
askRadio.pack(side=LEFT, padx=5, pady=5)
alwaysRadio = Radiobutton(autoApplyFrame, text='Always', variable=vars.auto_apply, value='always')
alwaysRadio.pack(side=LEFT, padx=5, pady=5)
neverRadio = Radiobutton(autoApplyFrame, text='Never', variable=vars.auto_apply, value='never')
neverRadio.pack(side=LEFT, padx=5, pady=5)
return romOptionsFrame, vars, set_sprite
@@ -618,6 +754,7 @@ class SpriteSelector():
self.window['pady'] = 5
self.spritesPerRow = 32
self.all_sprites = []
self.invalid_sprites = []
self.sprite_pool = spritePool
def open_custom_sprite_dir(_evt):
@@ -632,8 +769,10 @@ class SpriteSelector():
title_link.pack(side=LEFT)
title_link.bind("<Button-1>", open_custom_sprite_dir)
self.icon_section(alttpr_frametitle, self.alttpr_sprite_dir, 'ALTTPR sprites not found. Click "Update alttpr sprites" to download them.')
self.icon_section(custom_frametitle, self.custom_sprite_dir, 'Put sprites in the custom sprites folder (see open link above) to have them appear here.')
self.icon_section(alttpr_frametitle, self.alttpr_sprite_dir,
'ALTTPR sprites not found. Click "Update alttpr sprites" to download them.')
self.icon_section(custom_frametitle, self.custom_sprite_dir,
'Put sprites in the custom sprites folder (see open link above) to have them appear here.')
if not randomOnEvent:
self.sprite_pool_section(spritePool)
@@ -646,6 +785,9 @@ class SpriteSelector():
button = Button(frame, text="Update alttpr sprites", command=self.update_alttpr_sprites)
button.pack(side=RIGHT, padx=(5, 0))
button = Button(frame, text="Do not adjust sprite",command=self.use_default_sprite)
button.pack(side=LEFT,padx=(0,5))
button = Button(frame, text="Default Link sprite", command=self.use_default_link_sprite)
button.pack(side=LEFT, padx=(0, 5))
@@ -663,35 +805,43 @@ class SpriteSelector():
self.randomOnItemVar = IntVar()
self.randomOnBonkVar = IntVar()
self.randomOnRandomVar = IntVar()
self.randomOnAllVar = IntVar()
if self.randomOnEvent:
button = Checkbutton(frame, text="Hit", command=self.update_random_button, variable=self.randomOnHitVar)
button.pack(side=LEFT, padx=(0, 5))
self.buttonHit = Checkbutton(frame, text="Hit", command=self.update_random_button, variable=self.randomOnHitVar)
self.buttonHit.pack(side=LEFT, padx=(0, 5))
button = Checkbutton(frame, text="Enter", command=self.update_random_button, variable=self.randomOnEnterVar)
button.pack(side=LEFT, padx=(0, 5))
self.buttonEnter = Checkbutton(frame, text="Enter", command=self.update_random_button, variable=self.randomOnEnterVar)
self.buttonEnter.pack(side=LEFT, padx=(0, 5))
button = Checkbutton(frame, text="Exit", command=self.update_random_button, variable=self.randomOnExitVar)
button.pack(side=LEFT, padx=(0, 5))
self.buttonExit = Checkbutton(frame, text="Exit", command=self.update_random_button, variable=self.randomOnExitVar)
self.buttonExit.pack(side=LEFT, padx=(0, 5))
button = Checkbutton(frame, text="Slash", command=self.update_random_button, variable=self.randomOnSlashVar)
button.pack(side=LEFT, padx=(0, 5))
self.buttonSlash = Checkbutton(frame, text="Slash", command=self.update_random_button, variable=self.randomOnSlashVar)
self.buttonSlash.pack(side=LEFT, padx=(0, 5))
button = Checkbutton(frame, text="Item", command=self.update_random_button, variable=self.randomOnItemVar)
button.pack(side=LEFT, padx=(0, 5))
self.buttonItem = Checkbutton(frame, text="Item", command=self.update_random_button, variable=self.randomOnItemVar)
self.buttonItem.pack(side=LEFT, padx=(0, 5))
button = Checkbutton(frame, text="Bonk", command=self.update_random_button, variable=self.randomOnBonkVar)
button.pack(side=LEFT, padx=(0, 5))
self.buttonBonk = Checkbutton(frame, text="Bonk", command=self.update_random_button, variable=self.randomOnBonkVar)
self.buttonBonk.pack(side=LEFT, padx=(0, 5))
button = Checkbutton(frame, text="Random", command=self.update_random_button, variable=self.randomOnRandomVar)
button.pack(side=LEFT, padx=(0, 5))
self.buttonRandom = Checkbutton(frame, text="Random", command=self.update_random_button, variable=self.randomOnRandomVar)
self.buttonRandom.pack(side=LEFT, padx=(0, 5))
if adjuster:
button = Button(frame, text="Current sprite from rom", command=self.use_default_sprite)
button.pack(side=LEFT, padx=(0, 5))
self.buttonAll = Checkbutton(frame, text="All", command=self.update_random_button, variable=self.randomOnAllVar)
self.buttonAll.pack(side=LEFT, padx=(0, 5))
set_icon(self.window)
self.window.focus()
tkinter_center_window(self.window)
if self.invalid_sprites:
invalid = sorted(self.invalid_sprites)
logging.warning(f"The following sprites are invalid: {', '.join(invalid)}")
msg = f"{invalid[0]} "
msg += f"and {len(invalid)-1} more are invalid" if len(invalid) > 1 else "is invalid"
messagebox.showerror("Invalid sprites detected", msg, parent=self.window)
def remove_from_sprite_pool(self, button, spritename):
self.callback(("remove", spritename))
@@ -757,7 +907,13 @@ class SpriteSelector():
sprites = []
for file in os.listdir(path):
sprites.append((file, Sprite(os.path.join(path, file))))
if file == '.gitignore':
continue
sprite = Sprite(os.path.join(path, file))
if sprite.valid:
sprites.append((file, sprite))
else:
self.invalid_sprites.append(file)
sprites.sort(key=lambda s: str.lower(s[1].name or "").strip())
@@ -805,7 +961,6 @@ class SpriteSelector():
BackgroundTaskProgress(self.parent, update_sprites, "Updating Sprites", on_finish)
def browse_for_sprite(self):
sprite = filedialog.askopenfilename(
filetypes=[("All Sprite Sources", (".zspr", ".spr", ".sfc", ".smc")),
@@ -819,7 +974,6 @@ class SpriteSelector():
self.callback(None)
self.window.destroy()
def use_default_sprite(self):
self.callback(None)
self.window.destroy()
@@ -833,9 +987,31 @@ class SpriteSelector():
self.add_to_sprite_pool("link")
def update_random_button(self):
if self.randomOnRandomVar.get():
if self.randomOnAllVar.get():
randomon = "all"
self.buttonHit.config(state=DISABLED)
self.buttonEnter.config(state=DISABLED)
self.buttonExit.config(state=DISABLED)
self.buttonSlash.config(state=DISABLED)
self.buttonItem.config(state=DISABLED)
self.buttonBonk.config(state=DISABLED)
self.buttonRandom.config(state=DISABLED)
elif self.randomOnRandomVar.get():
randomon = "random"
self.buttonHit.config(state=DISABLED)
self.buttonEnter.config(state=DISABLED)
self.buttonExit.config(state=DISABLED)
self.buttonSlash.config(state=DISABLED)
self.buttonItem.config(state=DISABLED)
self.buttonBonk.config(state=DISABLED)
else:
self.buttonHit.config(state=NORMAL)
self.buttonEnter.config(state=NORMAL)
self.buttonExit.config(state=NORMAL)
self.buttonSlash.config(state=NORMAL)
self.buttonItem.config(state=NORMAL)
self.buttonBonk.config(state=NORMAL)
self.buttonRandom.config(state=NORMAL)
randomon = "-hit" if self.randomOnHitVar.get() else ""
randomon += "-enter" if self.randomOnEnterVar.get() else ""
randomon += "-exit" if self.randomOnExitVar.get() else ""
@@ -874,11 +1050,11 @@ class SpriteSelector():
@property
def alttpr_sprite_dir(self):
return local_path("data", "sprites", "alttpr")
return user_path("data", "sprites", "alttpr")
@property
def custom_sprite_dir(self):
return local_path("data", "sprites", "custom")
return user_path("data", "sprites", "custom")
def get_image_for_sprite(sprite, gif_only: bool = False):
@@ -923,7 +1099,8 @@ def get_image_for_sprite(sprite, gif_only: bool = False):
gif_lsd = bytearray(7)
gif_lsd[0] = width
gif_lsd[2] = height
gif_lsd[4] = 0xF4 # 32 color palette follows. transparant + 15 for sprite + 1 for shadow=17 which rounds up to 32 as nearest power of 2
gif_lsd[
4] = 0xF4 # 32 color palette follows. transparant + 15 for sprite + 1 for shadow=17 which rounds up to 32 as nearest power of 2
gif_lsd[5] = 0 # background color is zero
gif_lsd[6] = 0 # aspect raio not specified
gif_gct = bytearray(3 * 32)
@@ -943,7 +1120,8 @@ def get_image_for_sprite(sprite, gif_only: bool = False):
gif_id[7] = height
gif_id[9] = 0 # no local color table
gif_img_minimum_code_size = bytes([7]) # we choose 7 bits, so that each pixel is represented by a byte, for conviennce.
gif_img_minimum_code_size = bytes(
[7]) # we choose 7 bits, so that each pixel is represented by a byte, for conviennce.
clear = 0x80
stop = 0x81
@@ -1100,5 +1278,6 @@ class ToolTips(object):
widget.after_cancel(cls.after_id)
cls.after_id = None
if __name__ == '__main__':
main()
main()

717
Main.py
View File

@@ -1,189 +1,120 @@
from itertools import zip_longest
import collections
import logging
import os
import random
import time
import zlib
import concurrent.futures
import pickle
import tempfile
import zipfile
from typing import Dict, Tuple
from typing import Dict, List, Tuple, Optional, Set
from BaseClasses import MultiWorld, CollectionState, Region, RegionType
from worlds.alttp.Items import item_name_groups
from worlds.alttp.Regions import lookup_vanilla_location_to_entrance
from BaseClasses import Item, MultiWorld, CollectionState, Region, LocationProgressType, Location
import worlds
from worlds.alttp.SubClasses import LTTPRegionType
from worlds.alttp.Regions import is_main_entrance
from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned
from worlds.alttp.Shops import ShopSlotFill, SHOP_ID_START, total_shop_slots, FillDisabledShopSlots
from worlds.alttp.ItemPool import difficulties
from worlds.alttp.Shops import FillDisabledShopSlots
from Utils import output_path, get_options, __version__, version_tuple
from worlds.generic.Rules import locality_rules, exclusion_rules
from worlds import AutoWorld
seeddigits = 20
ordered_areas = (
'Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace',
'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace',
'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total"
)
def get_seed(seed=None):
if seed is None:
random.seed(None)
return random.randint(0, pow(10, seeddigits) - 1)
return seed
def get_same_seed(world: MultiWorld, seed_def: tuple) -> str:
seeds: Dict[tuple, str] = getattr(world, "__named_seeds", {})
if seed_def in seeds:
return seeds[seed_def]
seeds[seed_def] = str(world.random.randint(0, 2 ** 64))
world.__named_seeds = seeds
return seeds[seed_def]
def main(args, seed=None):
def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = None):
if not baked_server_options:
baked_server_options = get_options()["server_options"]
if args.outputpath:
os.makedirs(args.outputpath, exist_ok=True)
output_path.cached_path = args.outputpath
start = time.perf_counter()
# initialize the world
world = MultiWorld(args.multi)
logger = logging.getLogger('')
world.seed = get_seed(seed)
if args.race:
world.secure()
else:
world.random.seed(world.seed)
world.seed_name = str(args.outputname if args.outputname else world.seed)
logger = logging.getLogger()
world.set_seed(seed, args.race, str(args.outputname) if args.outputname else None)
world.plando_options = args.plando_options
world.shuffle = args.shuffle.copy()
world.logic = args.logic.copy()
world.mode = args.mode.copy()
world.swordless = args.swordless.copy()
world.difficulty = args.difficulty.copy()
world.item_functionality = args.item_functionality.copy()
world.timer = args.timer.copy()
world.goal = args.goal.copy()
world.local_items = args.local_items.copy()
if hasattr(args, "algorithm"): # current GUI options
world.algorithm = args.algorithm
world.shuffleganon = args.shuffleganon
world.custom = args.custom
world.customitemarray = args.customitemarray
world.accessibility = args.accessibility.copy()
world.retro = args.retro.copy()
world.hints = args.hints.copy()
world.mapshuffle = args.mapshuffle.copy()
world.compassshuffle = args.compassshuffle.copy()
world.keyshuffle = args.keyshuffle.copy()
world.bigkeyshuffle = args.bigkeyshuffle.copy()
world.open_pyramid = args.open_pyramid.copy()
world.boss_shuffle = args.shufflebosses.copy()
world.enemy_shuffle = args.enemy_shuffle.copy()
world.enemy_health = args.enemy_health.copy()
world.enemy_damage = args.enemy_damage.copy()
world.killable_thieves = args.killable_thieves.copy()
world.bush_shuffle = args.bush_shuffle.copy()
world.tile_shuffle = args.tile_shuffle.copy()
world.beemizer = args.beemizer.copy()
world.timer = args.timer.copy()
world.beemizer_total_chance = args.beemizer_total_chance.copy()
world.beemizer_trap_chance = args.beemizer_trap_chance.copy()
world.countdown_start_time = args.countdown_start_time.copy()
world.red_clock_time = args.red_clock_time.copy()
world.blue_clock_time = args.blue_clock_time.copy()
world.green_clock_time = args.green_clock_time.copy()
world.shufflepots = args.shufflepots.copy()
world.dungeon_counters = args.dungeon_counters.copy()
world.glitch_boots = args.glitch_boots.copy()
world.triforce_pieces_available = args.triforce_pieces_available.copy()
world.triforce_pieces_required = args.triforce_pieces_required.copy()
world.shop_shuffle = args.shop_shuffle.copy()
world.progression_balancing = args.progression_balancing.copy()
world.shuffle_prizes = args.shuffle_prizes.copy()
world.sprite_pool = args.sprite_pool.copy()
world.dark_room_logic = args.dark_room_logic.copy()
world.plando_items = args.plando_items.copy()
world.plando_texts = args.plando_texts.copy()
world.plando_connections = args.plando_connections.copy()
world.er_seeds = getattr(args, "er_seeds", {})
world.restrict_dungeon_item_on_boss = args.restrict_dungeon_item_on_boss.copy()
world.required_medallions = args.required_medallions.copy()
world.game = args.game.copy()
world.set_options(args)
world.player_name = args.name.copy()
world.alttp_rom = args.rom
world.enemizer = args.enemizercli
world.sprite = args.sprite.copy()
world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option.
world.slot_seeds = {player: random.Random(world.random.getrandbits(64)) for player in
range(1, world.players + 1)}
world.set_options(args)
world.set_item_links()
world.state = CollectionState(world)
logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed)
logger.info(f"Found {len(AutoWorld.AutoWorldRegister.world_types)} World Types:")
longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types)
max_item = 0
max_location = 0
for cls in AutoWorld.AutoWorldRegister.world_types.values():
if cls.item_id_to_name:
max_item = max(max_item, max(cls.item_id_to_name))
max_location = max(max_location, max(cls.location_id_to_name))
item_digits = len(str(max_item))
location_digits = len(str(max_location))
item_count = len(str(max(len(cls.item_names) for cls in AutoWorld.AutoWorldRegister.world_types.values())))
location_count = len(str(max(len(cls.location_names) for cls in AutoWorld.AutoWorldRegister.world_types.values())))
del max_item, max_location
for name, cls in AutoWorld.AutoWorldRegister.world_types.items():
if not cls.hidden and len(cls.item_names) > 0:
logger.info(f" {name:{longest_name}}: {len(cls.item_names):{item_count}} "
f"Items (IDs: {min(cls.item_id_to_name):{item_digits}} - "
f"{max(cls.item_id_to_name):{item_digits}}) | "
f"{len(cls.location_names):{location_count}} "
f"Locations (IDs: {min(cls.location_id_to_name):{location_digits}} - "
f"{max(cls.location_id_to_name):{location_digits}})")
del item_digits, location_digits, item_count, location_count
AutoWorld.call_stage(world, "assert_generate")
AutoWorld.call_all(world, "generate_early")
# system for sharing ER layouts
for player in world.get_game_players("A Link to the Past"):
world.er_seeds[player] = str(world.random.randint(0, 2 ** 64))
if "-" in world.shuffle[player]:
shuffle, seed = world.shuffle[player].split("-", 1)
world.shuffle[player] = shuffle
if shuffle == "vanilla":
world.er_seeds[player] = "vanilla"
elif seed.startswith("group-") or args.race:
world.er_seeds[player] = get_same_seed(world, (
shuffle, seed, world.retro[player], world.mode[player], world.logic[player]))
else: # not a race or group seed, use set seed as is.
world.er_seeds[player] = seed
elif world.shuffle[player] == "vanilla":
world.er_seeds[player] = "vanilla"
logger.info('Archipelago Version %s - Seed: %s\n', __version__, world.seed)
logger.info("Found World Types:")
longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types)
for name, cls in AutoWorld.AutoWorldRegister.world_types.items():
logger.info(f" {name:{longest_name}}: {len(cls.item_names):3} Items | {len(cls.location_names):3} Locations")
logger.info('')
for player in world.get_game_players("A Link to the Past"):
world.difficulty_requirements[player] = difficulties[world.difficulty[player]]
for player in world.player_ids:
for item_name in args.startinventory[player]:
world.push_precollected(world.create_item(item_name, player))
for player in world.player_ids:
if player in world.get_game_players("A Link to the Past"):
# enforce pre-defined local items.
if world.goal[player] in ["localtriforcehunt", "localganontriforcehunt"]:
world.local_items[player].add('Triforce Piece')
# dungeon items can't be in non-local if the appropriate dungeon item shuffle setting is not set.
if not world.mapshuffle[player]:
world.non_local_items[player] -= item_name_groups['Maps']
if not world.compassshuffle[player]:
world.non_local_items[player] -= item_name_groups['Compasses']
if not world.keyshuffle[player]:
world.non_local_items[player] -= item_name_groups['Small Keys']
# This could probably use a more elegant solution.
elif world.keyshuffle[player] == True and world.mode[player] == "Standard":
world.local_items[player].add("Small Key (Hyrule Castle)")
if not world.bigkeyshuffle[player]:
world.non_local_items[player] -= item_name_groups['Big Keys']
# Not possible to place pendants/crystals out side of boss prizes yet.
world.non_local_items[player] -= item_name_groups['Pendants']
world.non_local_items[player] -= item_name_groups['Crystals']
# items can't be both local and non-local, prefer local
world.non_local_items[player] -= world.local_items[player]
for item_name, count in world.start_inventory[player].value.items():
for _ in range(count):
world.push_precollected(world.create_item(item_name, player))
logger.info('Creating World.')
AutoWorld.call_all(world, "create_regions")
@@ -191,22 +122,115 @@ def main(args, seed=None):
logger.info('Creating Items.')
AutoWorld.call_all(world, "create_items")
# All worlds should have finished creating all regions, locations, and entrances.
# Recache to ensure that they are all visible for locality rules.
world._recache()
logger.info('Calculating Access Rules.')
for player in world.player_ids:
# items can't be both local and non-local, prefer local
world.non_local_items[player].value -= world.local_items[player].value
world.non_local_items[player].value -= set(world.local_early_items[player])
if world.players > 1:
for player in world.player_ids:
locality_rules(world, player)
locality_rules(world)
else:
world.non_local_items[1].value = set()
world.local_items[1].value = set()
AutoWorld.call_all(world, "set_rules")
for player in world.player_ids:
exclusion_rules(world, player, args.excluded_locations[player])
exclusion_rules(world, player, world.exclude_locations[player].value)
world.priority_locations[player].value -= world.exclude_locations[player].value
for location_name in world.priority_locations[player].value:
world.get_location(location_name, player).progress_type = LocationProgressType.PRIORITY
AutoWorld.call_all(world, "generate_basic")
logger.info("Running Item Plando")
# temporary home for item links, should be moved out of Main
for group_id, group in world.groups.items():
def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[
Optional[Dict[int, Dict[str, int]]], Optional[Dict[str, int]]
]:
classifications: Dict[str, int] = collections.defaultdict(int)
counters = {player: {name: 0 for name in shared_pool} for player in players}
for item in world.itempool:
if item.player in counters and item.name in shared_pool:
counters[item.player][item.name] += 1
classifications[item.name] |= item.classification
for item in world.itempool:
item.world = world
for player in players.copy():
if all([counters[player][item] == 0 for item in shared_pool]):
players.remove(player)
del (counters[player])
if not players:
return None, None
for item in shared_pool:
count = min(counters[player][item] for player in players)
if count:
for player in players:
counters[player][item] = count
else:
for player in players:
del (counters[player][item])
return counters, classifications
common_item_count, classifications = find_common_pool(group["players"], group["item_pool"])
if not common_item_count:
continue
new_itempool: List[Item] = []
for item_name, item_count in next(iter(common_item_count.values())).items():
for _ in range(item_count):
new_item = group["world"].create_item(item_name)
# mangle together all original classification bits
new_item.classification |= classifications[item_name]
new_itempool.append(new_item)
region = Region("Menu", group_id, world, "ItemLink")
world.regions.append(region)
locations = region.locations = []
for item in world.itempool:
count = common_item_count.get(item.player, {}).get(item.name, 0)
if count:
loc = Location(group_id, f"Item Link: {item.name} -> {world.player_name[item.player]} {count}",
None, region)
loc.access_rule = lambda state, item_name = item.name, group_id_ = group_id, count_ = count: \
state.has(item_name, group_id_, count_)
locations.append(loc)
loc.place_locked_item(item)
common_item_count[item.player][item.name] -= 1
else:
new_itempool.append(item)
itemcount = len(world.itempool)
world.itempool = new_itempool
while itemcount > len(world.itempool):
items_to_add = []
for player in group["players"]:
if group["link_replacement"]:
item_player = group_id
else:
item_player = player
if group["replacement_items"][player]:
items_to_add.append(AutoWorld.call_single(world, "create_item", item_player,
group["replacement_items"][player]))
else:
items_to_add.append(AutoWorld.call_single(world, "create_filler", item_player))
world.random.shuffle(items_to_add)
world.itempool.extend(items_to_add[:itemcount - len(world.itempool)])
if any(world.item_links.values()):
world._recache()
world._all_state = None
logger.info("Running Item Plando")
distribute_planned(world)
@@ -214,326 +238,181 @@ def main(args, seed=None):
AutoWorld.call_all(world, "pre_fill")
logger.info('Fill the world.')
logger.info(f'Filling the world with {len(world.itempool)} items.')
if world.algorithm == 'flood':
flood_items(world) # different algo, biased towards early game progress items
elif world.algorithm == 'balanced':
distribute_items_restrictive(world)
logger.info("Filling Shop Slots")
ShopSlotFill(world)
AutoWorld.call_all(world, 'post_fill')
if world.players > 1:
balance_multiworld_progression(world)
logger.info('Generating output files.')
outfilebase = 'AP_' + world.seed_name
logger.info(f'Beginning output...')
pool = concurrent.futures.ThreadPoolExecutor()
# we're about to output using multithreading, so we're removing the global random state to prevent accidental use
world.random.passthrough = False
outfilebase = 'AP_' + world.seed_name
output = tempfile.TemporaryDirectory()
with output as temp_dir:
check_accessibility_task = pool.submit(world.fulfills_accessibility)
with concurrent.futures.ThreadPoolExecutor(world.players + 2) as pool:
check_accessibility_task = pool.submit(world.fulfills_accessibility)
output_file_futures = []
output_file_futures = [pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir)]
for player in world.player_ids:
# skip starting a thread for methods that say "pass".
if AutoWorld.World.generate_output.__code__ is not world.worlds[player].generate_output.__code__:
output_file_futures.append(
pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
for player in world.player_ids:
output_file_futures.append(pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir))
output_file_futures.append(pool.submit(AutoWorld.call_stage, world, "generate_output", temp_dir))
# collect ER hint info
er_hint_data: Dict[int, Dict[int, str]] = {}
AutoWorld.call_all(world, 'extend_hint_information', er_hint_data)
def get_entrance_to_region(region: Region):
for entrance in region.entrances:
if entrance.parent_region.type in (RegionType.DarkWorld, RegionType.LightWorld, RegionType.Generic):
return entrance
for entrance in region.entrances: # BFS might be better here, trying DFS for now.
return get_entrance_to_region(entrance.parent_region)
checks_in_area = {player: {area: list() for area in ordered_areas}
for player in range(1, world.players + 1)}
# collect ER hint info
er_hint_data = {player: {} for player in world.get_game_players("A Link to the Past") if
world.shuffle[player] != "vanilla" or world.retro[player]}
for player in range(1, world.players + 1):
checks_in_area[player]["Total"] = 0
for region in world.regions:
if region.player in er_hint_data and region.locations:
main_entrance = get_entrance_to_region(region)
for location in region.locations:
if type(location.address) == int: # skips events and crystals
if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
er_hint_data[region.player][location.address] = main_entrance.name
ordered_areas = ('Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace',
'Tower of Hera', 'Palace of Darkness', 'Swamp Palace', 'Skull Woods', 'Thieves Town', 'Ice Palace',
'Misery Mire', 'Turtle Rock', 'Ganons Tower', "Total")
checks_in_area = {player: {area: list() for area in ordered_areas}
for player in range(1, world.players + 1)}
for player in range(1, world.players + 1):
checks_in_area[player]["Total"] = 0
for location in [loc for loc in world.get_filled_locations() if type(loc.address) is int]:
main_entrance = get_entrance_to_region(location.parent_region)
if location.game != "A Link to the Past":
checks_in_area[location.player]["Light World"].append(location.address)
elif location.parent_region.dungeon:
dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
'Inverted Ganons Tower': 'Ganons Tower'} \
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
checks_in_area[location.player][dungeonname].append(location.address)
elif main_entrance.parent_region.type == RegionType.LightWorld:
checks_in_area[location.player]["Light World"].append(location.address)
elif main_entrance.parent_region.type == RegionType.DarkWorld:
checks_in_area[location.player]["Dark World"].append(location.address)
checks_in_area[location.player]["Total"] += 1
oldmancaves = []
takeanyregions = ["Old Man Sword Cave", "Take-Any #1", "Take-Any #2", "Take-Any #3", "Take-Any #4"]
for index, take_any in enumerate(takeanyregions):
for region in [world.get_region(take_any, player) for player in range(1, world.players + 1) if
world.retro[player]]:
item = world.create_item(region.shop.inventory[(0 if take_any == "Old Man Sword Cave" else 1)]['item'],
region.player)
player = region.player
location_id = SHOP_ID_START + total_shop_slots + index
main_entrance = get_entrance_to_region(region)
if main_entrance.parent_region.type == RegionType.LightWorld:
checks_in_area[player]["Light World"].append(location_id)
else:
checks_in_area[player]["Dark World"].append(location_id)
checks_in_area[player]["Total"] += 1
er_hint_data[player][location_id] = main_entrance.name
oldmancaves.append(((location_id, player), (item.code, player)))
FillDisabledShopSlots(world)
def write_multidata():
import NetUtils
slot_data = {}
client_versions = {}
minimum_versions = {"server": (0, 1, 1), "clients": client_versions}
games = {}
for slot in world.player_ids:
client_versions[slot] = world.worlds[slot].get_required_client_version()
games[slot] = world.game[slot]
precollected_items = {player: [] for player in range(1, world.players + 1)}
for item in world.precollected_items:
precollected_items[item.player].append(item.code)
precollected_hints = {player: set() for player in range(1, world.players + 1)}
# for now special case Factorio tech_tree_information
sending_visible_players = set()
for player in world.get_game_players("Factorio"):
if world.tech_tree_information[player].value == 2:
sending_visible_players.add(player)
for slot in world.player_ids:
slot_data[slot] = world.worlds[slot].fill_slot_data()
locations_data: Dict[int, Dict[int, Tuple[int, int]]] = {player: {} for player in world.player_ids}
for location in world.get_filled_locations():
if type(location.address) == int:
locations_data[location.player][location.address] = location.item.code, location.item.player
if location.player in sending_visible_players and location.item.player != location.player:
hint = NetUtils.Hint(location.item.player, location.player, location.address,
location.item.code, False)
precollected_hints[location.player].add(hint)
precollected_hints[location.item.player].add(hint)
elif location.item.name in args.start_hints[location.item.player]:
hint = NetUtils.Hint(location.item.player, location.player, location.address,
location.item.code, False,
er_hint_data.get(location.player, {}).get(location.address, ""))
precollected_hints[location.player].add(hint)
if type(location.address) is int:
if location.game != "A Link to the Past":
checks_in_area[location.player]["Light World"].append(location.address)
else:
main_entrance = location.parent_region.get_connecting_entrance(is_main_entrance)
if location.parent_region.dungeon:
dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower',
'Inverted Ganons Tower': 'Ganons Tower'} \
.get(location.parent_region.dungeon.name, location.parent_region.dungeon.name)
checks_in_area[location.player][dungeonname].append(location.address)
elif location.parent_region.type == LTTPRegionType.LightWorld:
checks_in_area[location.player]["Light World"].append(location.address)
elif location.parent_region.type == LTTPRegionType.DarkWorld:
checks_in_area[location.player]["Dark World"].append(location.address)
elif main_entrance.parent_region.type == LTTPRegionType.LightWorld:
checks_in_area[location.player]["Light World"].append(location.address)
elif main_entrance.parent_region.type == LTTPRegionType.DarkWorld:
checks_in_area[location.player]["Dark World"].append(location.address)
checks_in_area[location.player]["Total"] += 1
FillDisabledShopSlots(world)
def write_multidata():
import NetUtils
slot_data = {}
client_versions = {}
games = {}
minimum_versions = {"server": AutoWorld.World.required_server_version, "clients": client_versions}
slot_info = {}
names = [[name for player, name in sorted(world.player_name.items())]]
for slot in world.player_ids:
player_world: AutoWorld.World = world.worlds[slot]
minimum_versions["server"] = max(minimum_versions["server"], player_world.required_server_version)
client_versions[slot] = player_world.required_client_version
games[slot] = world.game[slot]
slot_info[slot] = NetUtils.NetworkSlot(names[0][slot - 1], world.game[slot],
world.player_types[slot])
for slot, group in world.groups.items():
games[slot] = world.game[slot]
slot_info[slot] = NetUtils.NetworkSlot(group["name"], world.game[slot], world.player_types[slot],
group_members=sorted(group["players"]))
precollected_items = {player: [item.code for item in world_precollected if type(item.code) == int]
for player, world_precollected in world.precollected_items.items()}
precollected_hints = {player: set() for player in range(1, world.players + 1 + len(world.groups))}
for slot in world.player_ids:
slot_data[slot] = world.worlds[slot].fill_slot_data()
def precollect_hint(location):
entrance = er_hint_data.get(location.player, {}).get(location.address, "")
hint = NetUtils.Hint(location.item.player, location.player, location.address,
location.item.code, False, entrance, location.item.flags)
precollected_hints[location.player].add(hint)
if location.item.player not in world.groups:
precollected_hints[location.item.player].add(hint)
else:
for player in world.groups[location.item.player]["players"]:
precollected_hints[player].add(hint)
multidata = {
"slot_data": slot_data,
"games": games,
"names": [[name for player, name in sorted(world.player_name.items())]],
"connect_names": {name: (0, player) for player, name in world.player_name.items()},
"remote_items": {player for player in world.player_ids if
world.worlds[player].remote_items},
"locations": locations_data,
"checks_in_area": checks_in_area,
"server_options": get_options()["server_options"],
"er_hint_data": er_hint_data,
"precollected_items": precollected_items,
"precollected_hints": precollected_hints,
"version": tuple(version_tuple),
"tags": ["AP"],
"minimum_versions": minimum_versions,
"seed_name": world.seed_name
}
AutoWorld.call_all(world, "modify_multidata", multidata)
locations_data: Dict[int, Dict[int, Tuple[int, int, int]]] = {player: {} for player in world.player_ids}
for location in world.get_filled_locations():
if type(location.address) == int:
assert location.item.code is not None, "item code None should be event, " \
"location.address should then also be None. Location: " \
f" {location}"
locations_data[location.player][location.address] = \
location.item.code, location.item.player, location.item.flags
if location.name in world.start_location_hints[location.player]:
precollect_hint(location)
elif location.item.name in world.start_hints[location.item.player]:
precollect_hint(location)
elif any([location.item.name in world.start_hints[player]
for player in world.groups.get(location.item.player, {}).get("players", [])]):
precollect_hint(location)
multidata = zlib.compress(pickle.dumps(multidata), 9)
# embedded data package
data_package = {
game_world.game: worlds.network_data_package["games"][game_world.game]
for game_world in world.worlds.values()
}
with open(os.path.join(temp_dir, f'{outfilebase}.archipelago'), 'wb') as f:
f.write(bytes([1])) # version of format
f.write(multidata)
multidata = {
"slot_data": slot_data,
"slot_info": slot_info,
"names": names, # TODO: remove after 0.3.9
"connect_names": {name: (0, player) for player, name in world.player_name.items()},
"locations": locations_data,
"checks_in_area": checks_in_area,
"server_options": baked_server_options,
"er_hint_data": er_hint_data,
"precollected_items": precollected_items,
"precollected_hints": precollected_hints,
"version": tuple(version_tuple),
"tags": ["AP"],
"minimum_versions": minimum_versions,
"seed_name": world.seed_name,
"datapackage": data_package,
}
AutoWorld.call_all(world, "modify_multidata", multidata)
multidata = zlib.compress(pickle.dumps(multidata), 9)
multidata_task = pool.submit(write_multidata)
if not check_accessibility_task.result():
if not world.can_beat_game():
raise Exception("Game appears as unbeatable. Aborting.")
else:
logger.warning("Location Accessibility requirements not fulfilled.")
if multidata_task:
multidata_task.result() # retrieve exception if one exists
pool.shutdown() # wait for all queued tasks to complete
if not args.skip_playthrough:
with open(os.path.join(temp_dir, f'{outfilebase}.archipelago'), 'wb') as f:
f.write(bytes([3])) # version of format
f.write(multidata)
multidata_task = pool.submit(write_multidata)
if not check_accessibility_task.result():
if not world.can_beat_game():
raise Exception("Game appears as unbeatable. Aborting.")
else:
logger.warning("Location Accessibility requirements not fulfilled.")
# retrieve exceptions via .result() if they occurred.
multidata_task.result()
for i, future in enumerate(concurrent.futures.as_completed(output_file_futures), start=1):
if i % 10 == 0 or i == len(output_file_futures):
logger.info(f'Generating output files ({i}/{len(output_file_futures)}).')
future.result()
if args.spoiler > 1:
logger.info('Calculating playthrough.')
create_playthrough(world)
if args.create_spoiler:
world.spoiler.create_playthrough(create_paths=args.spoiler > 2)
if args.spoiler:
world.spoiler.to_file(os.path.join(temp_dir, '%s_Spoiler.txt' % outfilebase))
for future in output_file_futures:
future.result()
zipfilename = output_path(f"AP_{world.seed_name}.zip")
logger.info(f'Creating final archive at {zipfilename}.')
logger.info(f"Creating final archive at {zipfilename}")
with zipfile.ZipFile(zipfilename, mode="w", compression=zipfile.ZIP_DEFLATED,
compresslevel=9) as zf:
for file in os.scandir(temp_dir):
zf.write(os.path.join(temp_dir, file), arcname=file.name)
zf.write(file.path, arcname=file.name)
logger.info('Done. Enjoy. Total Time: %s', time.perf_counter() - start)
return world
def create_playthrough(world):
"""Destructive to the world while it is run, damage gets repaired afterwards."""
# get locations containing progress items
prog_locations = {location for location in world.get_filled_locations() if location.item.advancement}
state_cache = [None]
collection_spheres = []
state = CollectionState(world)
sphere_candidates = set(prog_locations)
logging.debug('Building up collection spheres.')
while sphere_candidates:
state.sweep_for_events(key_only=True)
# build up spheres of collection radius.
# Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
sphere = {location for location in sphere_candidates if state.can_reach(location)}
for location in sphere:
state.collect(location.item, True, location)
sphere_candidates -= sphere
collection_spheres.append(sphere)
state_cache.append(state.copy())
logging.debug('Calculated sphere %i, containing %i of %i progress items.', len(collection_spheres), len(sphere),
len(prog_locations))
if not sphere:
logging.debug('The following items could not be reached: %s', ['%s (Player %d) at %s (Player %d)' % (
location.item.name, location.item.player, location.name, location.player) for location in
sphere_candidates])
if any([world.accessibility[location.item.player] != 'none' for location in sphere_candidates]):
raise RuntimeError(f'Not all progression items reachable ({sphere_candidates}). '
f'Something went terribly wrong here.')
else:
world.spoiler.unreachables = sphere_candidates
break
# in the second phase, we cull each sphere such that the game is still beatable,
# reducing each range of influence to the bare minimum required inside it
restore_later = {}
for num, sphere in reversed(tuple(enumerate(collection_spheres))):
to_delete = set()
for location in sphere:
# we remove the item at location and check if game is still beatable
logging.debug('Checking if %s (Player %d) is required to beat the game.', location.item.name,
location.item.player)
old_item = location.item
location.item = None
if world.can_beat_game(state_cache[num]):
to_delete.add(location)
restore_later[location] = old_item
else:
# still required, got to keep it around
location.item = old_item
# cull entries in spheres for spoiler walkthrough at end
sphere -= to_delete
# second phase, sphere 0
removed_precollected = []
for item in (i for i in world.precollected_items if i.advancement):
logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
world.precollected_items.remove(item)
world.state.remove(item)
if not world.can_beat_game():
world.push_precollected(item)
else:
removed_precollected.append(item)
# we are now down to just the required progress items in collection_spheres. Unfortunately
# the previous pruning stage could potentially have made certain items dependant on others
# in the same or later sphere (because the location had 2 ways to access but the item originally
# used to access it was deemed not required.) So we need to do one final sphere collection pass
# to build up the correct spheres
required_locations = {item for sphere in collection_spheres for item in sphere}
state = CollectionState(world)
collection_spheres = []
while required_locations:
state.sweep_for_events(key_only=True)
sphere = set(filter(state.can_reach, required_locations))
for location in sphere:
state.collect(location.item, True, location)
required_locations -= sphere
collection_spheres.append(sphere)
logging.debug('Calculated final sphere %i, containing %i of %i progress items.', len(collection_spheres),
len(sphere), len(required_locations))
if not sphere:
raise RuntimeError(f'Not all required items reachable. Unreachable locations: {required_locations}')
def flist_to_iter(node):
while node:
value, node = node
yield value
def get_path(state, region):
reversed_path_as_flist = state.path.get(region, (region, None))
string_path_flat = reversed(list(map(str, flist_to_iter(reversed_path_as_flist))))
# Now we combine the flat string list into (region, exit) pairs
pathsiter = iter(string_path_flat)
pathpairs = zip_longest(pathsiter, pathsiter)
return list(pathpairs)
world.spoiler.paths = {}
topology_worlds = (player for player in world.player_ids if world.worlds[player].topology_present)
for player in topology_worlds:
world.spoiler.paths.update(
{str(location): get_path(state, location.parent_region) for sphere in collection_spheres for location in
sphere if location.player == player})
if player in world.get_game_players("A Link to the Past"):
for path in dict(world.spoiler.paths).values():
if any(exit_path == 'Pyramid Fairy' for (_, exit_path) in path):
if world.mode[player] != 'inverted':
world.spoiler.paths[str(world.get_region('Big Bomb Shop', player))] = \
get_path(state,world.get_region('Big Bomb Shop', player))
else:
world.spoiler.paths[str(world.get_region('Inverted Big Bomb Shop', player))] = \
get_path(state,world.get_region('Inverted Big Bomb Shop', player))
# we can finally output our playthrough
world.spoiler.playthrough = {"0": sorted([str(item) for item in world.precollected_items if item.advancement])}
for i, sphere in enumerate(collection_spheres):
world.spoiler.playthrough[str(i + 1)] = {str(location): str(location.item) for location in sorted(sphere)}
# repair the world again
for location, item in restore_later.items():
location.item = item
for item in removed_precollected:
world.push_precollected(item)

View File

@@ -1,15 +1,19 @@
import argparse
import os, sys
import json
import os
import sys
import re
import atexit
import shutil
from subprocess import Popen
from shutil import copyfile
from base64 import b64decode
from time import strftime
import logging
import requests
import Utils
from Utils import is_windows
atexit.register(input, "Press enter to exit.")
@@ -30,87 +34,81 @@ def prompt_yes_no(prompt):
print('Please respond with "y" or "n".')
# Find Forge jar file; raise error if not found
def find_forge_jar(forge_dir):
for entry in os.scandir(forge_dir):
if ".jar" in entry.name and "forge" in entry.name:
print(f"Found forge .jar: {entry.name}")
return entry.name
raise FileNotFoundError(f"Could not find forge .jar in {forge_dir}.")
# Create mods folder if needed; find AP randomizer jar; return None if not found.
def find_ap_randomizer_jar(forge_dir):
"""Create mods folder if needed; find AP randomizer jar; return None if not found."""
mods_dir = os.path.join(forge_dir, 'mods')
if os.path.isdir(mods_dir):
ap_mod_re = re.compile(r"^aprandomizer-[\d\.]+\.jar$")
for entry in os.scandir(mods_dir):
match = ap_mod_re.match(entry.name)
if match:
print(f"Found AP randomizer mod: {match.group()}")
return match.group()
if entry.name.startswith("aprandomizer") and entry.name.endswith(".jar"):
logging.info(f"Found AP randomizer mod: {entry.name}")
return entry.name
return None
else:
os.mkdir(mods_dir)
print(f"Created mods folder in {forge_dir}")
logging.info(f"Created mods folder in {forge_dir}")
return None
# Create APData folder if needed; clean .apmc files from APData; copy given .apmc into directory.
def replace_apmc_files(forge_dir, apmc_file):
"""Create APData folder if needed; clean .apmc files from APData; copy given .apmc into directory."""
if apmc_file is None:
return
apdata_dir = os.path.join(forge_dir, 'APData')
copy_apmc = True
if not os.path.isdir(apdata_dir):
os.mkdir(apdata_dir)
print(f"Created APData folder in {forge_dir}")
logging.info(f"Created APData folder in {forge_dir}")
for entry in os.scandir(apdata_dir):
if ".apmc" in entry.name and entry.is_file():
os.remove(entry.path)
print(f"Removed {entry.name} in {apdata_dir}")
copyfile(apmc_file, os.path.join(apdata_dir, os.path.basename(apmc_file)))
print(f"Copied {os.path.basename(apmc_file)} to {apdata_dir}")
if entry.name.endswith(".apmc") and entry.is_file():
if not os.path.samefile(apmc_file, entry.path):
os.remove(entry.path)
logging.info(f"Removed {entry.name} in {apdata_dir}")
else: # apmc already in apdata
copy_apmc = False
if copy_apmc:
copyfile(apmc_file, os.path.join(apdata_dir, os.path.basename(apmc_file)))
logging.info(f"Copied {os.path.basename(apmc_file)} to {apdata_dir}")
# Check mod version, download new mod from GitHub releases page if needed.
def update_mod(forge_dir):
def read_apmc_file(apmc_file):
from base64 import b64decode
with open(apmc_file, 'r') as f:
return json.loads(b64decode(f.read()))
def update_mod(forge_dir, url: str):
"""Check mod version, download new mod from GitHub releases page if needed. """
ap_randomizer = find_ap_randomizer_jar(forge_dir)
client_releases_endpoint = "https://api.github.com/repos/KonoTyran/Minecraft_AP_Randomizer/releases"
resp = requests.get(client_releases_endpoint)
if resp.status_code == 200: # OK
latest_release = resp.json()[0]
if ap_randomizer != latest_release['assets'][0]['name']:
print(f"A new release of the Minecraft AP randomizer mod was found: {latest_release['assets'][0]['name']}")
if ap_randomizer is not None:
print(f"Your current mod is {ap_randomizer}.")
else:
print(f"You do not have the AP randomizer mod installed.")
if prompt_yes_no("Would you like to update?"):
old_ap_mod = os.path.join(forge_dir, 'mods', ap_randomizer) if ap_randomizer is not None else None
new_ap_mod = os.path.join(forge_dir, 'mods', latest_release['assets'][0]['name'])
print("Downloading AP randomizer mod. This may take a moment...")
apmod_resp = requests.get(latest_release['assets'][0]['browser_download_url'])
if apmod_resp.status_code == 200:
with open(new_ap_mod, 'wb') as f:
f.write(apmod_resp.content)
print(f"Wrote new mod file to {new_ap_mod}")
if old_ap_mod is not None:
os.remove(old_ap_mod)
print(f"Removed old mod file from {old_ap_mod}")
else:
print(f"Error retrieving the randomizer mod (status code {apmod_resp.status_code}).")
print(f"Please report this issue on the Archipelago Discord server.")
sys.exit(1)
os.path.basename(url)
if ap_randomizer is not None:
logging.info(f"Your current mod is {ap_randomizer}.")
else:
print(f"Error checking for randomizer mod updates (status code {resp.status_code}).")
print(f"If this was not expected, please report this issue on the Archipelago Discord server.")
if not prompt_yes_no("Continue anyways?"):
sys.exit(0)
logging.info(f"You do not have the AP randomizer mod installed.")
if ap_randomizer != os.path.basename(url):
logging.info(f"A new release of the Minecraft AP randomizer mod was found: "
f"{os.path.basename(url)}")
if prompt_yes_no("Would you like to update?"):
old_ap_mod = os.path.join(forge_dir, 'mods', ap_randomizer) if ap_randomizer is not None else None
new_ap_mod = os.path.join(forge_dir, 'mods', os.path.basename(url))
logging.info("Downloading AP randomizer mod. This may take a moment...")
apmod_resp = requests.get(url)
if apmod_resp.status_code == 200:
with open(new_ap_mod, 'wb') as f:
f.write(apmod_resp.content)
logging.info(f"Wrote new mod file to {new_ap_mod}")
if old_ap_mod is not None:
os.remove(old_ap_mod)
logging.info(f"Removed old mod file from {old_ap_mod}")
else:
logging.error(f"Error retrieving the randomizer mod (status code {apmod_resp.status_code}).")
logging.error(f"Please report this issue on the Archipelago Discord server.")
sys.exit(1)
# Check if the EULA is agreed to, and prompt the user to read and agree if necessary.
def check_eula(forge_dir):
"""Check if the EULA is agreed to, and prompt the user to read and agree if necessary."""
eula_path = os.path.join(forge_dir, "eula.txt")
if not os.path.isfile(eula_path):
# Create eula.txt
@@ -122,59 +120,225 @@ def check_eula(forge_dir):
text = f.read()
if 'false' in text:
# Prompt user to agree to the EULA
print("You need to agree to the Minecraft EULA in order to run the server.")
print("The EULA can be found at https://account.mojang.com/documents/minecraft_eula")
logging.info("You need to agree to the Minecraft EULA in order to run the server.")
logging.info("The EULA can be found at https://account.mojang.com/documents/minecraft_eula")
if prompt_yes_no("Do you agree to the EULA?"):
f.seek(0)
f.write(text.replace('false', 'true'))
f.truncate()
print(f"Set {eula_path} to true")
logging.info(f"Set {eula_path} to true")
else:
sys.exit(0)
# Run the Forge server. Return process object
def run_forge_server(forge_dir, heap_arg):
forge_server = find_forge_jar(forge_dir)
def find_jdk_dir(version: str) -> str:
"""get the specified versions jdk directory"""
for entry in os.listdir():
if os.path.isdir(entry) and entry.startswith(f"jdk{version}"):
return os.path.abspath(entry)
java_exe = os.path.abspath(os.path.join('jre8', 'bin', 'java.exe'))
def find_jdk(version: str) -> str:
"""get the java exe location"""
if is_windows:
jdk = find_jdk_dir(version)
jdk_exe = os.path.join(jdk, "bin", "java.exe")
if os.path.isfile(jdk_exe):
return jdk_exe
else:
jdk_exe = shutil.which(options["minecraft_options"].get("java", "java"))
if not jdk_exe:
raise Exception("Could not find Java. Is Java installed on the system?")
return jdk_exe
def download_java(java: str):
"""Download Corretto (Amazon JDK)"""
jdk = find_jdk_dir(java)
if jdk is not None:
print(f"Removing old JDK...")
from shutil import rmtree
rmtree(jdk)
print(f"Downloading Java...")
jdk_url = f"https://corretto.aws/downloads/latest/amazon-corretto-{java}-x64-windows-jdk.zip"
resp = requests.get(jdk_url)
if resp.status_code == 200: # OK
print(f"Extracting...")
import zipfile
from io import BytesIO
with zipfile.ZipFile(BytesIO(resp.content)) as zf:
zf.extractall()
else:
print(f"Error downloading Java (status code {resp.status_code}).")
print(f"If this was not expected, please report this issue on the Archipelago Discord server.")
if not prompt_yes_no("Continue anyways?"):
sys.exit(0)
def install_forge(directory: str, forge_version: str, java_version: str):
"""download and install forge"""
java_exe = find_jdk(java_version)
if java_exe is not None:
print(f"Downloading Forge {forge_version}...")
forge_url = f"https://maven.minecraftforge.net/net/minecraftforge/forge/{forge_version}/forge-{forge_version}-installer.jar"
resp = requests.get(forge_url)
if resp.status_code == 200: # OK
forge_install_jar = os.path.join(directory, "forge_install.jar")
if not os.path.exists(directory):
os.mkdir(directory)
with open(forge_install_jar, 'wb') as f:
f.write(resp.content)
print(f"Installing Forge...")
install_process = Popen([java_exe, "-jar", forge_install_jar, "--installServer", directory])
install_process.wait()
os.remove(forge_install_jar)
def run_forge_server(forge_dir: str, java_version: str, heap_arg: str) -> Popen:
"""Run the Forge server."""
java_exe = find_jdk(java_version)
if not os.path.isfile(java_exe):
java_exe = "java" # try to fall back on java in the PATH
heap_arg = max_heap_re.match(max_heap).group()
heap_arg = max_heap_re.match(heap_arg).group()
if heap_arg[-1] in ['b', 'B']:
heap_arg = heap_arg[:-1]
heap_arg = "-Xmx" + heap_arg
argstring = ' '.join([java_exe, heap_arg, "-jar", forge_server, "-nogui"])
print(f"Running Forge server: {argstring}")
os_args = "win_args.txt" if is_windows else "unix_args.txt"
args_file = os.path.join(forge_dir, "libraries", "net", "minecraftforge", "forge", forge_version, os_args)
forge_args = []
with open(args_file) as argfile:
for line in argfile:
forge_args.extend(line.strip().split(" "))
args = [java_exe, heap_arg, *forge_args, "-nogui"]
logging.info(f"Running Forge server: {args}")
os.chdir(forge_dir)
return Popen(argstring)
return Popen(args)
def get_minecraft_versions(version, release_channel="release"):
version_file_endpoint = "https://raw.githubusercontent.com/KonoTyran/Minecraft_AP_Randomizer/master/versions/minecraft_versions.json"
resp = requests.get(version_file_endpoint)
local = False
if resp.status_code == 200: # OK
try:
data = resp.json()
except requests.exceptions.JSONDecodeError:
logging.warning(f"Unable to fetch version update file, using local version. (status code {resp.status_code}).")
local = True
else:
logging.warning(f"Unable to fetch version update file, using local version. (status code {resp.status_code}).")
local = True
if local:
with open(Utils.user_path("minecraft_versions.json"), 'r') as f:
data = json.load(f)
else:
with open(Utils.user_path("minecraft_versions.json"), 'w') as f:
json.dump(data, f)
try:
if version:
return next(filter(lambda entry: entry["version"] == version, data[release_channel]))
else:
return resp.json()[release_channel][0]
except (StopIteration, KeyError):
logging.error(f"No compatible mod version found for client version {version} on \"{release_channel}\" channel.")
if release_channel != "release":
logging.error("Consider switching \"release_channel\" to \"release\" in your Host.yaml file")
else:
logging.error("No suitable mod found on the \"release\" channel. Please Contact us on discord to report this error.")
sys.exit(0)
def is_correct_forge(forge_dir) -> bool:
if os.path.isdir(os.path.join(forge_dir, "libraries", "net", "minecraftforge", "forge", forge_version)):
return True
return False
if __name__ == '__main__':
Utils.init_logging("MinecraftClient")
parser = argparse.ArgumentParser()
parser.add_argument("apmc_file", default=None, nargs='?', help="Path to an Archipelago Minecraft data file (.apmc)")
parser.add_argument('--install', '-i', dest='install', default=False, action='store_true',
help="Download and install Java and the Forge server. Does not launch the client afterwards.")
parser.add_argument('--release_channel', '-r', dest="channel", type=str, action='store',
help="Specify release channel to use.")
parser.add_argument('--java', '-j', metavar='17', dest='java', type=str, default=False, action='store',
help="specify java version.")
parser.add_argument('--forge', '-f', metavar='1.18.2-40.1.0', dest='forge', type=str, default=False, action='store',
help="specify forge version. (Minecraft Version-Forge Version)")
parser.add_argument('--version', '-v', metavar='9', dest='data_version', type=int, action='store',
help="specify Mod data version to download.")
args = parser.parse_args()
options = Utils.get_options()
apmc_file = os.path.abspath(args.apmc_file)
forge_dir = options["minecraft_options"]["forge_directory"]
max_heap = options["minecraft_options"]["max_heap_size"]
apmc_file = os.path.abspath(args.apmc_file) if args.apmc_file else None
# Change to executable's working directory
os.chdir(os.path.abspath(os.path.dirname(sys.argv[0])))
if apmc_file is not None and not os.path.isfile(apmc_file):
raise FileNotFoundError(f"Path {apmc_file} does not exist or could not be accessed.")
if not os.path.isdir(forge_dir):
raise NotADirectoryError(f"Path {forge_dir} does not exist or could not be accessed.")
options = Utils.get_options()
channel = args.channel or options["minecraft_options"]["release_channel"]
apmc_data = None
data_version = args.data_version or None
if apmc_file is None and not args.install:
apmc_file = Utils.open_filename('Select APMC file', (('APMC File', ('.apmc',)),))
if apmc_file is not None and data_version is None:
apmc_data = read_apmc_file(apmc_file)
data_version = apmc_data.get('client_version', '')
versions = get_minecraft_versions(data_version, channel)
forge_dir = Utils.user_path(options["minecraft_options"]["forge_directory"])
max_heap = options["minecraft_options"]["max_heap_size"]
forge_version = args.forge or versions["forge"]
java_version = args.java or versions["java"]
mod_url = versions["url"]
java_dir = find_jdk_dir(java_version)
if args.install:
if is_windows:
print("Installing Java")
download_java(java_version)
if not is_correct_forge(forge_dir):
print("Installing Minecraft Forge")
install_forge(forge_dir, forge_version, java_version)
else:
print("Correct Forge version already found, skipping install.")
sys.exit(0)
if apmc_data is None:
raise FileNotFoundError(f"APMC file does not exist or is inaccessible at the given location ({apmc_file})")
if is_windows:
if java_dir is None or not os.path.isdir(java_dir):
if prompt_yes_no("Did not find java directory. Download and install java now?"):
download_java(java_version)
java_dir = find_jdk_dir(java_version)
if java_dir is None or not os.path.isdir(java_dir):
raise NotADirectoryError(f"Path {java_dir} does not exist or could not be accessed.")
if not is_correct_forge(forge_dir):
if prompt_yes_no(f"Did not find forge version {forge_version} download and install it now?"):
install_forge(forge_dir, forge_version, java_version)
if not os.path.isdir(forge_dir):
raise NotADirectoryError(f"Path {forge_dir} does not exist or could not be accessed.")
if not max_heap_re.match(max_heap):
raise Exception(f"Max heap size {max_heap} in incorrect format. Use a number followed by M or G, e.g. 512M or 2G.")
update_mod(forge_dir)
update_mod(forge_dir, mod_url)
replace_apmc_files(forge_dir, apmc_file)
check_eula(forge_dir)
server_process = run_forge_server(forge_dir, max_heap)
server_process = run_forge_server(forge_dir, java_version, max_heap)
server_process.wait()

View File

@@ -1,9 +1,10 @@
import os
import sys
import subprocess
import pkg_resources
import warnings
requirements_files = {'requirements.txt'}
local_dir = os.path.dirname(__file__)
requirements_files = {os.path.join(local_dir, 'requirements.txt')}
if sys.version_info < (3, 8, 6):
raise RuntimeError("Incompatible Python Version. 3.8.7+ is supported.")
@@ -11,49 +12,118 @@ if sys.version_info < (3, 8, 6):
update_ran = getattr(sys, "frozen", False) # don't run update if environment is frozen/compiled
if not update_ran:
for entry in os.scandir("worlds"):
if entry.is_dir():
req_file = os.path.join(entry.path, "requirements.txt")
if os.path.exists(req_file):
requirements_files.add(req_file)
for entry in os.scandir(os.path.join(local_dir, "worlds")):
# skip .* (hidden / disabled) folders
if not entry.name.startswith("."):
if entry.is_dir():
req_file = os.path.join(entry.path, "requirements.txt")
if os.path.exists(req_file):
requirements_files.add(req_file)
def check_pip():
# detect if pip is available
try:
import pip # noqa: F401
except ImportError:
raise RuntimeError("pip not available. Please install pip.")
def confirm(msg: str):
try:
input(f"\n{msg}")
except KeyboardInterrupt:
print("\nAborting")
sys.exit(1)
def update_command():
check_pip()
for file in requirements_files:
subprocess.call([sys.executable, '-m', 'pip', 'install', '-r', file, '--upgrade'])
subprocess.call([sys.executable, "-m", "pip", "install", "-r", file, "--upgrade"])
def update(yes = False, force = False):
def install_pkg_resources(yes=False):
try:
import pkg_resources # noqa: F401
except ImportError:
check_pip()
if not yes:
confirm("pkg_resources not found, press enter to install it")
subprocess.call([sys.executable, "-m", "pip", "install", "--upgrade", "setuptools"])
def update(yes=False, force=False):
global update_ran
if not update_ran:
update_ran = True
if force:
update_command()
return
install_pkg_resources(yes=yes)
import pkg_resources
for req_file in requirements_files:
path = os.path.join(os.path.dirname(sys.argv[0]), req_file)
if not os.path.exists(path):
path = os.path.join(os.path.dirname(__file__), req_file)
with open(path) as requirementsfile:
requirements = pkg_resources.parse_requirements(requirementsfile)
for requirement in requirements:
requirement = str(requirement)
try:
pkg_resources.require(requirement)
except pkg_resources.ResolutionError:
if not yes:
import traceback
traceback.print_exc()
input(f'Requirement {requirement} is not satisfied, press enter to install it')
update_command()
return
for line in requirementsfile:
if not line or line[0] == "#":
continue # ignore comments
if line.startswith(("https://", "git+https://")):
# extract name and version for url
rest = line.split('/')[-1]
line = ""
if "#egg=" in rest:
# from egg info
rest, egg = rest.split("#egg=", 1)
egg = egg.split(";", 1)[0].rstrip()
if any(compare in egg for compare in ("==", ">=", ">", "<", "<=", "!=")):
warnings.warn(f"Specifying version as #egg={egg} will become unavailable in pip 25.0. "
"Use name @ url#version instead.", DeprecationWarning)
line = egg
else:
egg = ""
if "@" in rest and not line:
raise ValueError("Can't deduce version from requirement")
elif not line:
# from filename
rest = rest.replace(".zip", "-").replace(".tar.gz", "-")
name, version, _ = rest.split("-", 2)
line = f'{egg or name}=={version}'
elif "@" in line and "#" in line:
# PEP 508 does not allow us to specify a version, so we use custom syntax
# name @ url#version ; marker
name, rest = line.split("@", 1)
version = rest.split("#", 1)[1].split(";", 1)[0].rstrip()
line = f"{name.rstrip()}=={version}"
if ";" in rest: # keep marker
line += rest[rest.find(";"):]
requirements = pkg_resources.parse_requirements(line)
for requirement in map(str, requirements):
try:
pkg_resources.require(requirement)
except pkg_resources.ResolutionError:
if not yes:
import traceback
traceback.print_exc()
confirm(f"Requirement {requirement} is not satisfied, press enter to install it")
update_command()
return
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description='Install archipelago requirements')
parser.add_argument('-y', '--yes', dest='yes', action='store_true', help='answer "yes" to all questions')
parser.add_argument('-f', '--force', dest='force', action='store_true', help='force update')
parser.add_argument('-a', '--append', nargs="*", dest='additional_requirements',
help='List paths to additional requirement files.')
args = parser.parse_args()
if args.additional_requirements:
requirements_files.update(args.additional_requirements)
update(args.yes, args.force)

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,5 @@
from __future__ import annotations
import asyncio
import logging
import typing
import enum
from json import JSONEncoder, JSONDecoder
@@ -15,8 +14,10 @@ class JSONMessagePart(typing.TypedDict, total=False):
# optional
type: str
color: str
# mainly for items, optional
found: bool
# owning player for location/item
player: int
# if type == item indicates item flags
flags: int
class ClientStatus(enum.IntEnum):
@@ -27,17 +28,57 @@ class ClientStatus(enum.IntEnum):
CLIENT_GOAL = 30
class SlotType(enum.IntFlag):
spectator = 0b00
player = 0b01
group = 0b10
@property
def always_goal(self) -> bool:
"""Mark this slot as having reached its goal instantly."""
return self.value != 0b01
class Permission(enum.IntFlag):
disabled = 0b000 # 0, completely disables access
enabled = 0b001 # 1, allows manual use
goal = 0b010 # 2, allows manual use after goal completion
auto = 0b110 # 6, forces use after goal completion, only works for release
auto_enabled = 0b111 # 7, forces use after goal completion, allows manual use any time
@staticmethod
def from_text(text: str):
data = 0
if "auto" in text:
data |= 0b110
elif "goal" in text:
data |= 0b010
if "enabled" in text:
data |= 0b001
return Permission(data)
class NetworkPlayer(typing.NamedTuple):
"""Represents a particular player on a particular team."""
team: int
slot: int
alias: str
name: str
class NetworkSlot(typing.NamedTuple):
"""Represents a particular slot across teams."""
name: str
game: str
type: SlotType
group_members: typing.Union[typing.List[int], typing.Tuple] = () # only populated if type == group
class NetworkItem(typing.NamedTuple):
item: int
location: int
player: int
flags: int = 0
def _scan_for_TypedTuples(obj: typing.Any) -> typing.Any:
@@ -45,7 +86,7 @@ def _scan_for_TypedTuples(obj: typing.Any) -> typing.Any:
data = obj._asdict()
data["class"] = obj.__class__.__name__
return data
if isinstance(obj, (tuple, list)):
if isinstance(obj, (tuple, list, set, frozenset)):
return tuple(_scan_for_TypedTuples(o) for o in obj)
if isinstance(obj, dict):
return {key: _scan_for_TypedTuples(value) for key, value in obj.items()}
@@ -55,10 +96,11 @@ def _scan_for_TypedTuples(obj: typing.Any) -> typing.Any:
_encode = JSONEncoder(
ensure_ascii=False,
check_circular=False,
separators=(',', ':'),
).encode
def encode(obj):
def encode(obj: typing.Any) -> str:
return _encode(_scan_for_TypedTuples(obj))
@@ -67,9 +109,11 @@ def get_any_version(data: dict) -> Version:
return Version(int(data["major"]), int(data["minor"]), int(data["build"]))
whitelist = {"NetworkPlayer": NetworkPlayer,
"NetworkItem": NetworkItem,
}
allowlist = {
"NetworkPlayer": NetworkPlayer,
"NetworkItem": NetworkItem,
"NetworkSlot": NetworkSlot
}
custom_hooks = {
"Version": get_any_version
@@ -81,7 +125,7 @@ def _object_hook(o: typing.Any) -> typing.Any:
hook = custom_hooks.get(o.get("class", None), None)
if hook:
return hook(o)
cls = whitelist.get(o.get("class", None), None)
cls = allowlist.get(o.get("class", None), None)
if cls:
for key in tuple(o):
if key not in cls._fields:
@@ -94,62 +138,12 @@ def _object_hook(o: typing.Any) -> typing.Any:
decode = JSONDecoder(object_hook=_object_hook).decode
class Node:
endpoints: typing.List
dumper = staticmethod(encode)
loader = staticmethod(decode)
def __init__(self):
self.endpoints = []
super(Node, self).__init__()
self.log_network = 0
def broadcast_all(self, msgs):
msgs = self.dumper(msgs)
for endpoint in self.endpoints:
asyncio.create_task(self.send_encoded_msgs(endpoint, msgs))
async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool:
if not endpoint.socket or not endpoint.socket.open:
return False
msg = self.dumper(msgs)
try:
await endpoint.socket.send(msg)
except websockets.ConnectionClosed:
logging.exception(f"Exception during send_msgs, could not send {msg}")
await self.disconnect(endpoint)
else:
if self.log_network:
logging.info(f"Outgoing message: {msg}")
return True
async def send_encoded_msgs(self, endpoint: Endpoint, msg: str) -> bool:
if not endpoint.socket or not endpoint.socket.open:
return False
try:
await endpoint.socket.send(msg)
except websockets.ConnectionClosed:
logging.exception("Exception during send_encoded_msgs")
await self.disconnect(endpoint)
else:
if self.log_network:
logging.info(f"Outgoing message: {msg}")
return True
async def disconnect(self, endpoint):
if endpoint in self.endpoints:
self.endpoints.remove(endpoint)
class Endpoint:
socket: websockets.WebSocketServerProtocol
def __init__(self, socket):
self.socket = socket
async def disconnect(self):
raise NotImplementedError
class HandlerMeta(type):
def __new__(mcs, name, bases, attrs):
@@ -168,11 +162,11 @@ class HandlerMeta(type):
break
def __init__(self, *args, **kwargs):
if orig_init:
orig_init(self, *args, **kwargs)
# turn functions into bound methods
self.handlers = {name: method.__get__(self, type(self)) for name, method in
handlers.items()}
if orig_init:
orig_init(self, *args, **kwargs)
attrs['__init__'] = __init__
return super(HandlerMeta, mcs).__new__(mcs, name, bases, attrs)
@@ -191,6 +185,21 @@ class JSONTypes(str, enum.Enum):
class JSONtoTextParser(metaclass=HandlerMeta):
color_codes = {
# not exact color names, close enough but decent looking
"black": "000000",
"red": "EE0000",
"green": "00FF7F",
"yellow": "FAFAD2",
"blue": "6495ED",
"magenta": "EE00EE",
"cyan": "00EEEE",
"slateblue": "6D8BE8",
"plum": "AF99EF",
"salmon": "FA8072",
"white": "FFFFFF"
}
def __init__(self, ctx):
self.ctx = ctx
@@ -198,13 +207,13 @@ class JSONtoTextParser(metaclass=HandlerMeta):
return "".join(self.handle_node(section) for section in input_object)
def handle_node(self, node: JSONMessagePart):
type = node.get("type", None)
handler = self.handlers.get(type, self.handlers["text"])
node_type = node.get("type", None)
handler = self.handlers.get(node_type, self.handlers["text"])
return handler(node)
def _handle_color(self, node: JSONMessagePart):
codes = node["color"].split(";")
buffer = "".join(color_code(code) for code in codes)
buffer = "".join(color_code(code) for code in codes if code in color_codes)
return buffer + self._handle_text(node) + color_code("reset")
def _handle_text(self, node: JSONMessagePart):
@@ -222,26 +231,32 @@ class JSONtoTextParser(metaclass=HandlerMeta):
return self._handle_color(node)
def _handle_item_name(self, node: JSONMessagePart):
# todo: use a better info source
from worlds.alttp.Items import progression_items
node["color"] = 'green' if node.get("found", False) else 'cyan'
if node["text"] in progression_items:
node["color"] += ";white_bg"
flags = node.get("flags", 0)
if flags == 0:
node["color"] = 'cyan'
elif flags & 0b001: # advancement
node["color"] = 'plum'
elif flags & 0b010: # useful
node["color"] = 'slateblue'
elif flags & 0b100: # trap
node["color"] = 'salmon'
else:
node["color"] = 'cyan'
return self._handle_color(node)
def _handle_item_id(self, node: JSONMessagePart):
item_id = int(node["text"])
node["text"] = self.ctx.item_name_getter(item_id)
node["text"] = self.ctx.item_names[item_id]
return self._handle_item_name(node)
def _handle_location_name(self, node: JSONMessagePart):
node["color"] = 'blue_bg;white'
node["color"] = 'green'
return self._handle_color(node)
def _handle_location_id(self, node: JSONMessagePart):
item_id = int(node["text"])
node["text"] = self.ctx.location_name_getter(item_id)
return self._handle_item_name(node)
node["text"] = self.ctx.location_names[item_id]
return self._handle_location_name(node)
def _handle_entrance_name(self, node: JSONMessagePart):
node["color"] = 'blue'
@@ -255,7 +270,7 @@ class RawJSONtoTextParser(JSONtoTextParser):
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,
'blue_bg': 44, 'purple_bg': 45, 'cyan_bg': 46, 'white_bg': 47}
'blue_bg': 44, 'magenta_bg': 45, 'cyan_bg': 46, 'white_bg': 47}
def color_code(*args):
@@ -270,6 +285,14 @@ def add_json_text(parts: list, text: typing.Any, **kwargs) -> None:
parts.append({"text": str(text), **kwargs})
def add_json_item(parts: list, item_id: int, player: int = 0, item_flags: int = 0, **kwargs) -> None:
parts.append({"text": str(item_id), "player": player, "flags": item_flags, "type": JSONTypes.item_id, **kwargs})
def add_json_location(parts: list, item_id: int, player: int = 0, **kwargs) -> None:
parts.append({"text": str(item_id), "player": player, "type": JSONTypes.location_id, **kwargs})
class Hint(typing.NamedTuple):
receiving_player: int
finding_player: int
@@ -277,13 +300,15 @@ class Hint(typing.NamedTuple):
item: int
found: bool
entrance: str = ""
item_flags: int = 0
def re_check(self, ctx, team) -> Hint:
if self.found:
return self
found = self.location in ctx.location_checks[team, self.finding_player]
if found:
return Hint(self.receiving_player, self.finding_player, self.location, self.item, found, self.entrance)
return Hint(self.receiving_player, self.finding_player, self.location, self.item, found, self.entrance,
self.item_flags)
return self
def __hash__(self):
@@ -294,9 +319,9 @@ class Hint(typing.NamedTuple):
add_json_text(parts, "[Hint]: ")
add_json_text(parts, self.receiving_player, type="player_id")
add_json_text(parts, "'s ")
add_json_text(parts, self.item, type="item_id", found=self.found)
add_json_item(parts, self.item, self.receiving_player, self.item_flags)
add_json_text(parts, " is at ")
add_json_text(parts, self.location, type="location_id")
add_json_location(parts, self.location, self.finding_player)
add_json_text(parts, " in ")
add_json_text(parts, self.finding_player, type="player_id")
if self.entrance:
@@ -304,14 +329,16 @@ class Hint(typing.NamedTuple):
add_json_text(parts, self.entrance, type="entrance_name")
else:
add_json_text(parts, "'s World")
add_json_text(parts, ". ")
if self.found:
add_json_text(parts, ". (found)")
add_json_text(parts, "(found)", type="color", color="green")
else:
add_json_text(parts, ".")
add_json_text(parts, "(not found)", type="color", color="red")
return {"cmd": "PrintJSON", "data": parts, "type": "Hint",
"receiving": self.receiving_player,
"item": NetworkItem(self.item, self.location, self.finding_player)}
"item": NetworkItem(self.item, self.location, self.finding_player, self.item_flags),
"found": self.found}
@property
def local(self):

252
OoTAdjuster.py Normal file
View File

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

343
OoTClient.py Normal file
View File

@@ -0,0 +1,343 @@
import asyncio
import json
import os
import multiprocessing
import subprocess
import zipfile
from asyncio import StreamReader, StreamWriter
# CommonClient import first to trigger ModuleUpdater
from CommonClient import CommonContext, server_loop, gui_enabled, \
ClientCommandProcessor, logger, get_base_parser
import Utils
from Utils import async_start
from worlds import network_data_package
from worlds.oot.Rom import Rom, compress_rom_file
from worlds.oot.N64Patch import apply_patch_file
from worlds.oot.Utils import data_path
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart oot_connector.lua"
CONNECTION_REFUSED_STATUS = "Connection refused. Please start your emulator and make sure oot_connector.lua is running"
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart oot_connector.lua"
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
CONNECTION_CONNECTED_STATUS = "Connected"
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
"""
Payload: lua -> client
{
playerName: string,
locations: dict,
deathlinkActive: bool,
isDead: bool,
gameComplete: bool
}
Payload: client -> lua
{
items: list,
playerNames: list,
triggerDeath: bool
}
Deathlink logic:
"Dead" is true <-> Link is at 0 hp.
deathlink_pending: we need to kill the player
deathlink_sent_this_death: we interacted with the multiworld on this death, waiting to reset with living link
"""
oot_loc_name_to_id = network_data_package["games"]["Ocarina of Time"]["location_name_to_id"]
script_version: int = 3
def get_item_value(ap_id):
return ap_id - 66000
class OoTCommandProcessor(ClientCommandProcessor):
def __init__(self, ctx):
super().__init__(ctx)
def _cmd_n64(self):
"""Check N64 Connection State"""
if isinstance(self.ctx, OoTContext):
logger.info(f"N64 Status: {self.ctx.n64_status}")
def _cmd_deathlink(self):
"""Toggle deathlink from client. Overrides default setting."""
if isinstance(self.ctx, OoTContext):
self.ctx.deathlink_client_override = True
self.ctx.deathlink_enabled = not self.ctx.deathlink_enabled
async_start(self.ctx.update_death_link(self.ctx.deathlink_enabled), name="Update Deathlink")
class OoTContext(CommonContext):
command_processor = OoTCommandProcessor
items_handling = 0b001 # full local
def __init__(self, server_address, password):
super().__init__(server_address, password)
self.game = 'Ocarina of Time'
self.n64_streams: (StreamReader, StreamWriter) = None
self.n64_sync_task = None
self.n64_status = CONNECTION_INITIAL_STATUS
self.awaiting_rom = False
self.location_table = {}
self.collectible_table = {}
self.collectible_override_flags_address = 0
self.collectible_offsets = {}
self.deathlink_enabled = False
self.deathlink_pending = False
self.deathlink_sent_this_death = False
self.deathlink_client_override = False
self.version_warning = False
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(OoTContext, self).server_auth(password_requested)
if not self.auth:
self.awaiting_rom = True
logger.info('Awaiting connection to Bizhawk to get player information')
return
await self.send_connect()
def on_deathlink(self, data: dict):
self.deathlink_pending = True
super().on_deathlink(data)
def run_gui(self):
from kvui import GameManager
class OoTManager(GameManager):
logging_pairs = [
("Client", "Archipelago")
]
base_title = "Archipelago Ocarina of Time Client"
self.ui = OoTManager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
def on_package(self, cmd, args):
if cmd == 'Connected':
slot_data = args.get('slot_data', None)
if slot_data:
self.collectible_override_flags_address = slot_data.get('collectible_override_flags', 0)
self.collectible_offsets = slot_data.get('collectible_flag_offsets', {})
def get_payload(ctx: OoTContext):
if ctx.deathlink_enabled and ctx.deathlink_pending:
trigger_death = True
ctx.deathlink_sent_this_death = True
else:
trigger_death = False
payload = json.dumps({
"items": [get_item_value(item.item) for item in ctx.items_received],
"playerNames": [name for (i, name) in ctx.player_names.items() if i != 0],
"triggerDeath": trigger_death,
"collectibleOverrides": ctx.collectible_override_flags_address,
"collectibleOffsets": ctx.collectible_offsets
})
return payload
async def parse_payload(payload: dict, ctx: OoTContext, force: bool):
# Refuse to do anything if ROM is detected as changed
if ctx.auth and payload['playerName'] != ctx.auth:
logger.warning("ROM change detected. Disconnecting and reconnecting...")
ctx.deathlink_enabled = False
ctx.deathlink_client_override = False
ctx.finished_game = False
ctx.location_table = {}
ctx.collectible_table = {}
ctx.deathlink_pending = False
ctx.deathlink_sent_this_death = False
ctx.auth = payload['playerName']
await ctx.send_connect()
return
# Turn on deathlink if it is on, and if the client hasn't overriden it
if payload['deathlinkActive'] and not ctx.deathlink_enabled and not ctx.deathlink_client_override:
await ctx.update_death_link(True)
ctx.deathlink_enabled = True
# Game completion handling
if payload['gameComplete'] and not ctx.finished_game:
await ctx.send_msgs([{
"cmd": "StatusUpdate",
"status": 30
}])
ctx.finished_game = True
# Locations handling
locations = payload['locations']
collectibles = payload['collectibles']
if ctx.location_table != locations or ctx.collectible_table != collectibles:
ctx.location_table = locations
ctx.collectible_table = collectibles
locs1 = [oot_loc_name_to_id[loc] for loc, b in ctx.location_table.items() if b]
locs2 = [int(loc) for loc, b in ctx.collectible_table.items() if b]
await ctx.send_msgs([{
"cmd": "LocationChecks",
"locations": locs1 + locs2
}])
# Deathlink handling
if ctx.deathlink_enabled:
if payload['isDead']: # link is dead
ctx.deathlink_pending = False
if not ctx.deathlink_sent_this_death:
ctx.deathlink_sent_this_death = True
await ctx.send_death()
else: # link is alive
ctx.deathlink_sent_this_death = False
async def n64_sync_task(ctx: OoTContext):
logger.info("Starting n64 connector. Use /n64 for status information.")
while not ctx.exit_event.is_set():
error_status = None
if ctx.n64_streams:
(reader, writer) = ctx.n64_streams
msg = get_payload(ctx).encode()
writer.write(msg)
writer.write(b'\n')
try:
await asyncio.wait_for(writer.drain(), timeout=1.5)
try:
data = await asyncio.wait_for(reader.readline(), timeout=10)
data_decoded = json.loads(data.decode())
reported_version = data_decoded.get('scriptVersion', 0)
if reported_version >= script_version:
if ctx.game is not None and 'locations' in data_decoded:
# Not just a keep alive ping, parse
async_start(parse_payload(data_decoded, ctx, False))
if not ctx.auth:
ctx.auth = data_decoded['playerName']
if ctx.awaiting_rom:
await ctx.server_auth(False)
else:
if not ctx.version_warning:
logger.warning(f"Your Lua script is version {reported_version}, expected {script_version}. "
"Please update to the latest version. "
"Your connection to the Archipelago server will not be accepted.")
ctx.version_warning = True
except asyncio.TimeoutError:
logger.debug("Read Timed Out, Reconnecting")
error_status = CONNECTION_TIMING_OUT_STATUS
writer.close()
ctx.n64_streams = None
except ConnectionResetError as e:
logger.debug("Read failed due to Connection Lost, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.n64_streams = None
except TimeoutError:
logger.debug("Connection Timed Out, Reconnecting")
error_status = CONNECTION_TIMING_OUT_STATUS
writer.close()
ctx.n64_streams = None
except ConnectionResetError:
logger.debug("Connection Lost, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.n64_streams = None
if ctx.n64_status == CONNECTION_TENTATIVE_STATUS:
if not error_status:
logger.info("Successfully Connected to N64")
ctx.n64_status = CONNECTION_CONNECTED_STATUS
else:
ctx.n64_status = f"Was tentatively connected but error occured: {error_status}"
elif error_status:
ctx.n64_status = error_status
logger.info("Lost connection to N64 and attempting to reconnect. Use /n64 for status updates")
else:
try:
logger.debug("Attempting to connect to N64")
ctx.n64_streams = await asyncio.wait_for(asyncio.open_connection("localhost", 28921), timeout=10)
ctx.n64_status = CONNECTION_TENTATIVE_STATUS
except TimeoutError:
logger.debug("Connection Timed Out, Trying Again")
ctx.n64_status = CONNECTION_TIMING_OUT_STATUS
continue
except ConnectionRefusedError:
logger.debug("Connection Refused, Trying Again")
ctx.n64_status = CONNECTION_REFUSED_STATUS
continue
async def run_game(romfile):
auto_start = Utils.get_options()["oot_options"].get("rom_start", True)
if auto_start is True:
import webbrowser
webbrowser.open(romfile)
elif os.path.isfile(auto_start):
subprocess.Popen([auto_start, romfile],
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
async def patch_and_run_game(apz5_file):
apz5_file = os.path.abspath(apz5_file)
base_name = os.path.splitext(apz5_file)[0]
decomp_path = base_name + '-decomp.z64'
comp_path = base_name + '.z64'
# Load vanilla ROM, patch file, compress ROM
rom_file_name = Utils.get_options()["oot_options"]["rom_file"]
if not os.path.exists(rom_file_name):
rom_file_name = Utils.user_path(rom_file_name)
rom = Rom(rom_file_name)
apply_patch_file(rom, apz5_file,
sub_file=(os.path.basename(base_name) + '.zpf'
if zipfile.is_zipfile(apz5_file)
else None))
rom.write_to_file(decomp_path)
os.chdir(data_path("Compress"))
compress_rom_file(decomp_path, comp_path)
os.remove(decomp_path)
async_start(run_game(comp_path))
if __name__ == '__main__':
Utils.init_logging("OoTClient")
async def main():
multiprocessing.freeze_support()
parser = get_base_parser()
parser.add_argument('apz5_file', default="", type=str, nargs="?",
help='Path to an APZ5 file')
args = parser.parse_args()
if args.apz5_file:
logger.info("APZ5 file supplied, beginning patching process...")
async_start(patch_and_run_game(args.apz5_file))
ctx = OoTContext(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="Server Loop")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
ctx.n64_sync_task = asyncio.create_task(n64_sync_task(ctx), name="N64 Sync")
await ctx.exit_event.wait()
ctx.server_address = None
await ctx.shutdown()
if ctx.n64_sync_task:
await ctx.n64_sync_task
import colorama
colorama.init()
asyncio.run(main())
colorama.deinit()

File diff suppressed because it is too large Load Diff

186
Patch.py
View File

@@ -1,171 +1,35 @@
import bsdiff4
import yaml
from __future__ import annotations
import os
import lzma
import threading
import concurrent.futures
import zipfile
import sys
from typing import Tuple, Optional
from typing import Tuple, Optional, TypedDict
import Utils
if __name__ == "__main__":
import ModuleUpdate
ModuleUpdate.update()
from worlds.Files import AutoPatchRegister, APDeltaPatch
current_patch_version = 2
class RomMeta(TypedDict):
server: str
player: Optional[int]
player_name: str
def generate_yaml(patch: bytes, metadata: Optional[dict] = None) -> bytes:
from worlds.alttp.Rom import JAP10HASH
patch = yaml.dump({"meta": metadata,
"patch": patch,
"game": "A Link to the Past",
# minimum version of patch system expected for patching to be successful
"compatible_version": 1,
"version": current_patch_version,
"base_checksum": JAP10HASH})
return patch.encode(encoding="utf-8-sig")
def generate_patch(rom: bytes, metadata: Optional[dict] = None) -> bytes:
from worlds.alttp.Rom import get_base_rom_bytes
if metadata is None:
metadata = {}
patch = bsdiff4.diff(get_base_rom_bytes(), rom)
return generate_yaml(patch, metadata)
def create_patch_file(rom_file_to_patch: str, server: str = "", destination: str = None,
player: int = 0, player_name: str = "") -> str:
meta = {"server": server, # allow immediate connection to server in multiworld. Empty string otherwise
"player_id": player,
"player_name": player_name}
bytes = generate_patch(load_bytes(rom_file_to_patch),
meta)
target = destination if destination else os.path.splitext(rom_file_to_patch)[0] + ".apbp"
write_lzma(bytes, target)
return target
def create_rom_bytes(patch_file: str, ignore_version: bool = False) -> Tuple[dict, str, bytearray]:
from worlds.alttp.Rom import get_base_rom_bytes
data = Utils.parse_yaml(lzma.decompress(load_bytes(patch_file)).decode("utf-8-sig"))
if not ignore_version and data["compatible_version"] > current_patch_version:
raise RuntimeError("Patch file is incompatible with this patcher, likely an update is required.")
patched_data = bsdiff4.patch(get_base_rom_bytes(), data["patch"])
rom_hash = patched_data[int(0x7FC0):int(0x7FD5)]
data["meta"]["hash"] = "".join(chr(x) for x in rom_hash)
target = os.path.splitext(patch_file)[0] + ".sfc"
return data["meta"], target, patched_data
def create_rom_file(patch_file: str) -> Tuple[dict, str]:
data, target, patched_data = create_rom_bytes(patch_file)
with open(target, "wb") as f:
f.write(patched_data)
return data, target
def update_patch_data(patch_data: bytes, server: str = "") -> bytes:
data = Utils.parse_yaml(lzma.decompress(patch_data).decode("utf-8-sig"))
data["meta"]["server"] = server
bytes = generate_yaml(data["patch"], data["meta"])
return lzma.compress(bytes)
def load_bytes(path: str) -> bytes:
with open(path, "rb") as f:
return f.read()
def write_lzma(data: bytes, path: str):
with lzma.LZMAFile(path, 'wb') as f:
f.write(data)
def create_rom_file(patch_file: str) -> Tuple[RomMeta, str]:
auto_handler = AutoPatchRegister.get_handler(patch_file)
if auto_handler:
handler: APDeltaPatch = auto_handler(patch_file)
target = os.path.splitext(patch_file)[0]+handler.result_file_ending
handler.patch(target)
return {"server": handler.server,
"player": handler.player,
"player_name": handler.player_name}, target
raise NotImplementedError(f"No Handler for {patch_file} found.")
if __name__ == "__main__":
host = Utils.get_public_ipv4()
options = Utils.get_options()['server_options']
if options['host']:
host = options['host']
address = f"{host}:{options['port']}"
ziplock = threading.Lock()
print(f"Host for patches to be created is {address}")
with concurrent.futures.ThreadPoolExecutor() as pool:
for rom in sys.argv:
try:
if rom.endswith(".sfc"):
print(f"Creating patch for {rom}")
result = pool.submit(create_patch_file, rom, address)
result.add_done_callback(lambda task: print(f"Created patch {task.result()}"))
elif rom.endswith(".apbp"):
print(f"Applying patch {rom}")
data, target = create_rom_file(rom)
romfile, adjusted = Utils.get_adjuster_settings(target)
if adjusted:
try:
os.replace(romfile, target)
romfile = target
except Exception as e:
print(e)
print(f"Created rom {romfile if adjusted else target}.")
if 'server' in data:
Utils.persistent_store("servers", data['hash'], data['server'])
print(f"Host is {data['server']}")
elif rom.endswith(".archipelago"):
import json
import zlib
with open(rom, 'rb') as fr:
multidata = zlib.decompress(fr.read()).decode("utf-8")
with open(rom + '.txt', 'w') as fw:
fw.write(multidata)
multidata = json.loads(multidata)
for romname in multidata['roms']:
Utils.persistent_store("servers", "".join(chr(byte) for byte in romname[2]), address)
from Utils import get_options
multidata["server_options"] = get_options()["server_options"]
multidata = zlib.compress(json.dumps(multidata).encode("utf-8"), 9)
with open(rom + "_updated.archipelago", 'wb') as f:
f.write(multidata)
elif rom.endswith(".zip"):
print(f"Updating host in patch files contained in {rom}")
def _handle_zip_file_entry(zfinfo: zipfile.ZipInfo, server: str):
data = zfr.read(zfinfo)
if zfinfo.filename.endswith(".apbp"):
data = update_patch_data(data, server)
with ziplock:
zfw.writestr(zfinfo, data)
return zfinfo.filename
futures = []
with zipfile.ZipFile(rom, "r") as zfr:
updated_zip = os.path.splitext(rom)[0] + "_updated.zip"
with zipfile.ZipFile(updated_zip, "w", compression=zipfile.ZIP_DEFLATED,
compresslevel=9) as zfw:
for zfname in zfr.namelist():
futures.append(pool.submit(_handle_zip_file_entry, zfr.getinfo(zfname), address))
for future in futures:
print(f"File {future.result()} added to {os.path.split(updated_zip)[1]}")
except:
import traceback
traceback.print_exc()
input("Press enter to close.")
def read_rom(stream, strip_header=True) -> bytearray:
"""Reads rom into bytearray and optionally strips off any smc header"""
buffer = bytearray(stream.read())
if strip_header and len(buffer) % 0x400 == 0x200:
return buffer[0x200:]
return buffer
for file in sys.argv[1:]:
meta_data, result_file = create_rom_file(file)
print(f"Patch with meta-data {meta_data} was written to {result_file}")

351
PokemonClient.py Normal file
View File

@@ -0,0 +1,351 @@
import asyncio
import json
import time
import os
import bsdiff4
import subprocess
import zipfile
from asyncio import StreamReader, StreamWriter
from typing import List
import Utils
from Utils import async_start
from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \
get_base_parser
from worlds.pokemon_rb.locations import location_data
from worlds.pokemon_rb.rom import RedDeltaPatch, BlueDeltaPatch
location_map = {"Rod": {}, "EventFlag": {}, "Missable": {}, "Hidden": {}, "list": {}, "DexSanityFlag": {}}
location_bytes_bits = {}
for location in location_data:
if location.ram_address is not None:
if type(location.ram_address) == list:
location_map[type(location.ram_address).__name__][(location.ram_address[0].flag, location.ram_address[1].flag)] = location.address
location_bytes_bits[location.address] = [{'byte': location.ram_address[0].byte, 'bit': location.ram_address[0].bit},
{'byte': location.ram_address[1].byte, 'bit': location.ram_address[1].bit}]
else:
location_map[type(location.ram_address).__name__][location.ram_address.flag] = location.address
location_bytes_bits[location.address] = {'byte': location.ram_address.byte, 'bit': location.ram_address.bit}
SYSTEM_MESSAGE_ID = 0
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart pkmn_rb.lua"
CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure pkmn_rb.lua is running"
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart pkmn_rb.lua"
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
CONNECTION_CONNECTED_STATUS = "Connected"
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
DISPLAY_MSGS = True
SCRIPT_VERSION = 3
class GBCommandProcessor(ClientCommandProcessor):
def __init__(self, ctx: CommonContext):
super().__init__(ctx)
def _cmd_gb(self):
"""Check Gameboy Connection State"""
if isinstance(self.ctx, GBContext):
logger.info(f"Gameboy Status: {self.ctx.gb_status}")
class GBContext(CommonContext):
command_processor = GBCommandProcessor
game = 'Pokemon Red and Blue'
def __init__(self, server_address, password):
super().__init__(server_address, password)
self.gb_streams: (StreamReader, StreamWriter) = None
self.gb_sync_task = None
self.messages = {}
self.locations_array = None
self.gb_status = CONNECTION_INITIAL_STATUS
self.awaiting_rom = False
self.display_msgs = True
self.deathlink_pending = False
self.set_deathlink = False
self.client_compatibility_mode = 0
self.items_handling = 0b001
self.sent_release = False
self.sent_collect = False
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(GBContext, self).server_auth(password_requested)
if not self.auth:
self.awaiting_rom = True
logger.info('Awaiting connection to Bizhawk to get Player information')
return
await self.send_connect()
def _set_message(self, msg: str, msg_id: int):
if DISPLAY_MSGS:
self.messages[(time.time(), msg_id)] = msg
def on_package(self, cmd: str, args: dict):
if cmd == 'Connected':
self.locations_array = None
if 'death_link' in args['slot_data'] and args['slot_data']['death_link']:
self.set_deathlink = True
elif cmd == "RoomInfo":
self.seed_name = args['seed_name']
elif cmd == 'Print':
msg = args['text']
if ': !' not in msg:
self._set_message(msg, SYSTEM_MESSAGE_ID)
elif cmd == "ReceivedItems":
msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}"
self._set_message(msg, SYSTEM_MESSAGE_ID)
def on_deathlink(self, data: dict):
self.deathlink_pending = True
super().on_deathlink(data)
def run_gui(self):
from kvui import GameManager
class GBManager(GameManager):
logging_pairs = [
("Client", "Archipelago")
]
base_title = "Archipelago Pokémon Client"
self.ui = GBManager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
def get_payload(ctx: GBContext):
current_time = time.time()
ret = json.dumps(
{
"items": [item.item for item in ctx.items_received],
"messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items()
if key[0] > current_time - 10},
"deathlink": ctx.deathlink_pending,
"options": ((ctx.permissions['release'] in ('goal', 'enabled')) * 2) + (ctx.permissions['collect'] in ('goal', 'enabled'))
}
)
ctx.deathlink_pending = False
return ret
async def parse_locations(data: List, ctx: GBContext):
locations = []
flags = {"EventFlag": data[:0x140], "Missable": data[0x140:0x140 + 0x20],
"Hidden": data[0x140 + 0x20: 0x140 + 0x20 + 0x0E],
"Rod": data[0x140 + 0x20 + 0x0E:0x140 + 0x20 + 0x0E + 0x01]}
if len(data) > 0x140 + 0x20 + 0x0E + 0x01:
flags["DexSanityFlag"] = data[0x140 + 0x20 + 0x0E + 0x01:]
else:
flags["DexSanityFlag"] = [0] * 19
for flag_type, loc_map in location_map.items():
for flag, loc_id in loc_map.items():
if flag_type == "list":
if (flags["EventFlag"][location_bytes_bits[loc_id][0]['byte']] & 1 << location_bytes_bits[loc_id][0]['bit']
and flags["Missable"][location_bytes_bits[loc_id][1]['byte']] & 1 << location_bytes_bits[loc_id][1]['bit']):
locations.append(loc_id)
elif flags[flag_type][location_bytes_bits[loc_id]['byte']] & 1 << location_bytes_bits[loc_id]['bit']:
locations.append(loc_id)
if flags["EventFlag"][280] & 1 and not ctx.finished_game:
await ctx.send_msgs([
{"cmd": "StatusUpdate",
"status": 30}
])
ctx.finished_game = True
if locations == ctx.locations_array:
return
ctx.locations_array = locations
if locations is not None:
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": locations}])
async def gb_sync_task(ctx: GBContext):
logger.info("Starting GB connector. Use /gb for status information")
while not ctx.exit_event.is_set():
error_status = None
if ctx.gb_streams:
(reader, writer) = ctx.gb_streams
msg = get_payload(ctx).encode()
writer.write(msg)
writer.write(b'\n')
try:
await asyncio.wait_for(writer.drain(), timeout=1.5)
try:
# Data will return a dict with up to two fields:
# 1. A keepalive response of the Players Name (always)
# 2. An array representing the memory values of the locations area (if in game)
data = await asyncio.wait_for(reader.readline(), timeout=5)
data_decoded = json.loads(data.decode())
if 'scriptVersion' not in data_decoded or data_decoded['scriptVersion'] != SCRIPT_VERSION:
msg = "You are connecting with an incompatible Lua script version. Ensure your connector Lua " \
"and PokemonClient are from the same Archipelago installation."
logger.info(msg, extra={'compact_gui': True})
ctx.gui_error('Error', msg)
error_status = CONNECTION_RESET_STATUS
ctx.client_compatibility_mode = data_decoded['clientCompatibilityVersion']
if ctx.client_compatibility_mode == 0:
ctx.items_handling = 0b101 # old patches will not have local start inventory, must be requested
if ctx.seed_name and ctx.seed_name != ''.join([chr(i) for i in data_decoded['seedName'] if i != 0]):
msg = "The server is running a different multiworld than your client is. (invalid seed_name)"
logger.info(msg, extra={'compact_gui': True})
ctx.gui_error('Error', msg)
error_status = CONNECTION_RESET_STATUS
ctx.seed_name = ''.join([chr(i) for i in data_decoded['seedName'] if i != 0])
if not ctx.auth:
ctx.auth = ''.join([chr(i) for i in data_decoded['playerName'] if i != 0])
if ctx.auth == '':
msg = "Invalid ROM detected. No player name built into the ROM."
logger.info(msg, extra={'compact_gui': True})
ctx.gui_error('Error', msg)
error_status = CONNECTION_RESET_STATUS
if ctx.awaiting_rom:
await ctx.server_auth(False)
if 'locations' in data_decoded and ctx.game and ctx.gb_status == CONNECTION_CONNECTED_STATUS \
and not error_status and ctx.auth:
# Not just a keep alive ping, parse
async_start(parse_locations(data_decoded['locations'], ctx))
if 'deathLink' in data_decoded and data_decoded['deathLink'] and 'DeathLink' in ctx.tags:
await ctx.send_death(ctx.auth + " is out of usable Pokémon! " + ctx.auth + " blacked out!")
if 'options' in data_decoded:
msgs = []
if data_decoded['options'] & 4 and not ctx.sent_release:
ctx.sent_release = True
msgs.append({"cmd": "Say", "text": "!release"})
if data_decoded['options'] & 8 and not ctx.sent_collect:
ctx.sent_collect = True
msgs.append({"cmd": "Say", "text": "!collect"})
if msgs:
await ctx.send_msgs(msgs)
if ctx.set_deathlink:
await ctx.update_death_link(True)
except asyncio.TimeoutError:
logger.debug("Read Timed Out, Reconnecting")
error_status = CONNECTION_TIMING_OUT_STATUS
writer.close()
ctx.gb_streams = None
except ConnectionResetError as e:
logger.debug("Read failed due to Connection Lost, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.gb_streams = None
except TimeoutError:
logger.debug("Connection Timed Out, Reconnecting")
error_status = CONNECTION_TIMING_OUT_STATUS
writer.close()
ctx.gb_streams = None
except ConnectionResetError:
logger.debug("Connection Lost, Reconnecting")
error_status = CONNECTION_RESET_STATUS
writer.close()
ctx.gb_streams = None
if ctx.gb_status == CONNECTION_TENTATIVE_STATUS:
if not error_status:
logger.info("Successfully Connected to Gameboy")
ctx.gb_status = CONNECTION_CONNECTED_STATUS
else:
ctx.gb_status = f"Was tentatively connected but error occured: {error_status}"
elif error_status:
ctx.gb_status = error_status
logger.info("Lost connection to Gameboy and attempting to reconnect. Use /gb for status updates")
else:
try:
logger.debug("Attempting to connect to Gameboy")
ctx.gb_streams = await asyncio.wait_for(asyncio.open_connection("localhost", 17242), timeout=10)
ctx.gb_status = CONNECTION_TENTATIVE_STATUS
except TimeoutError:
logger.debug("Connection Timed Out, Trying Again")
ctx.gb_status = CONNECTION_TIMING_OUT_STATUS
continue
except ConnectionRefusedError:
logger.debug("Connection Refused, Trying Again")
ctx.gb_status = CONNECTION_REFUSED_STATUS
continue
async def run_game(romfile):
auto_start = Utils.get_options()["pokemon_rb_options"].get("rom_start", True)
if auto_start is True:
import webbrowser
webbrowser.open(romfile)
elif os.path.isfile(auto_start):
subprocess.Popen([auto_start, romfile],
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
async def patch_and_run_game(game_version, patch_file, ctx):
base_name = os.path.splitext(patch_file)[0]
comp_path = base_name + '.gb'
if game_version == "blue":
delta_patch = BlueDeltaPatch
else:
delta_patch = RedDeltaPatch
try:
base_rom = delta_patch.get_source_data()
except Exception as msg:
logger.info(msg, extra={'compact_gui': True})
ctx.gui_error('Error', msg)
with zipfile.ZipFile(patch_file, 'r') as patch_archive:
with patch_archive.open('delta.bsdiff4', 'r') as stream:
patch = stream.read()
patched_rom_data = bsdiff4.patch(base_rom, patch)
with open(comp_path, "wb") as patched_rom_file:
patched_rom_file.write(patched_rom_data)
async_start(run_game(comp_path))
if __name__ == '__main__':
Utils.init_logging("PokemonClient")
options = Utils.get_options()
async def main():
parser = get_base_parser()
parser.add_argument('patch_file', default="", type=str, nargs="?",
help='Path to an APRED or APBLUE patch file')
args = parser.parse_args()
ctx = GBContext(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
ctx.gb_sync_task = asyncio.create_task(gb_sync_task(ctx), name="GB Sync")
if args.patch_file:
ext = args.patch_file.split(".")[len(args.patch_file.split(".")) - 1].lower()
if ext == "apred":
logger.info("APRED file supplied, beginning patching process...")
async_start(patch_and_run_game("red", args.patch_file, ctx))
elif ext == "apblue":
logger.info("APBLUE file supplied, beginning patching process...")
async_start(patch_and_run_game("blue", args.patch_file, ctx))
else:
logger.warning(f"Unknown patch file extension {ext}")
await ctx.exit_event.wait()
ctx.server_address = None
await ctx.shutdown()
if ctx.gb_sync_task:
await ctx.gb_sync_task
import colorama
colorama.init()
asyncio.run(main())
colorama.deinit()

View File

@@ -7,8 +7,44 @@ Currently, the following games are supported:
* Factorio
* Minecraft
* Subnautica
* Slay the Spire
* Risk of Rain 2
* The Legend of Zelda: Ocarina of Time
* Timespinner
* Super Metroid
* Secret of Evermore
* Final Fantasy
* Rogue Legacy
* VVVVVV
* Raft
* Super Mario 64
* Meritous
* Super Metroid/Link to the Past combo randomizer (SMZ3)
* ChecksFinder
* ArchipIDLE
* Hollow Knight
* The Witness
* Sonic Adventure 2: Battle
* Starcraft 2: Wings of Liberty
* Donkey Kong Country 3
* Dark Souls 3
* Super Mario World
* Pokémon Red and Blue
* Hylics 2
* Overcooked! 2
* Zillion
* Lufia II Ancient Cave
* Blasphemous
* Wargroove
* Stardew Valley
* The Legend of Zelda
* The Messenger
* Kingdom Hearts 2
* The Legend of Zelda: Link's Awakening DX
* Clique
* Adventure
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial).
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
windows binaries.
@@ -30,30 +66,20 @@ Archipelago was directly forked from bonta0's `multiworld_31` branch of ALttPEnt
## Running Archipelago
For most people all you need to do is head over to the [releases](https://github.com/ArchipelagoMW/Archipelago/releases) page then download and run the appropriate installer. The installers function on Windows only.
If you are running Archipelago from a non-Windows system then the likely scenario is that you are comfortable running source code directly. Please see our wiki page on [running Archipelago from source](https://github.com/Berserker66/MultiWorld-Utilities/wiki/Running-from-source).
If you are running Archipelago from a non-Windows system then the likely scenario is that you are comfortable running source code directly. Please see our doc on [running Archipelago from source](docs/running%20from%20source.md).
## Related Repositories
This project makes use of multiple other projects. We wouldn't be here without these other repositories and the contributions of their developers, past and present.
* [z3randomizer](https://github.com/CaitSith2/z3randomizer)
* [z3randomizer](https://github.com/ArchipelagoMW/z3randomizer)
* [Enemizer](https://github.com/Ijwu/Enemizer)
* [Ocarina of Time Randomizer](https://github.com/TestRunnerSRL/OoT-Randomizer)
## Contributing
Contributions are welcome. We have a few asks of any new contributors.
For contribution guidelines, please see our [Contributing doc.](/docs/contributing.md)
* Ensure that all changes which affect logic are covered by unit tests.
* Do not introduce any unit test failures/regressions.
Otherwise, we tend to judge code on a case to case basis. It is a generally good idea to stick to PEP-8 guidelines to ensure consistency with existing code. (And to make the linter happy.)
## FAQ
For Frequently asked questions, please see the website's [FAQ Page.](https://archipelago.gg/faq/en/)
## Code of Conduct
We conduct ourselves openly and inclusively here. Please do not contribute to an environment which makes other people uncomfortable. This means that we expect all contributors or participants here to:
* Be welcoming and inclusive in tone and language.
* Be respectful of others and their abilities.
* Show empathy when speaking with others.
* Be gracious and accept feedback and constructive criticism.
These guidelines apply to all channels of communication within this GitHub repository. Please be respectful in both public channels, such as issues, and private, such as private messaging or emails.
Any incidents of abuse may be reported directly to Ijwu at hmfarran@gmail.com.
Please refer to our [code of conduct.](/docs/code_of_conduct.md)

723
SNIClient.py Normal file
View File

@@ -0,0 +1,723 @@
from __future__ import annotations
import sys
import threading
import time
import multiprocessing
import os
import subprocess
import base64
import logging
import asyncio
import enum
import typing
from json import loads, dumps
# CommonClient import first to trigger ModuleUpdater
from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser
import Utils
from Utils import async_start
from MultiServer import mark_raw
if typing.TYPE_CHECKING:
from worlds.AutoSNIClient import SNIClient
if __name__ == "__main__":
Utils.init_logging("SNIClient", exception_logger="Client")
import colorama
from websockets.client import connect as websockets_connect, WebSocketClientProtocol
from websockets.exceptions import WebSocketException, ConnectionClosed
snes_logger = logging.getLogger("SNES")
class DeathState(enum.IntEnum):
killing_player = 1
alive = 2
dead = 3
class SNIClientCommandProcessor(ClientCommandProcessor):
ctx: SNIContext
def _cmd_slow_mode(self, toggle: str = "") -> None:
"""Toggle slow mode, which limits how fast you send / receive items."""
if toggle:
self.ctx.slow_mode = toggle.lower() in {"1", "true", "on"}
else:
self.ctx.slow_mode = not self.ctx.slow_mode
self.output(f"Setting slow mode to {self.ctx.slow_mode}")
@mark_raw
def _cmd_snes(self, snes_options: str = "") -> bool:
"""Connect to a snes. Optionally include network address of a snes to connect to,
otherwise show available devices; and a SNES device number if more than one SNES is detected.
Examples: "/snes", "/snes 1", "/snes localhost:23074 1" """
if self.ctx.snes_state in {SNESState.SNES_ATTACHED, SNESState.SNES_CONNECTED, SNESState.SNES_CONNECTING}:
self.output("Already connected to SNES. Disconnecting first.")
self._cmd_snes_close()
return self.connect_to_snes(snes_options)
def connect_to_snes(self, snes_options: str = "") -> bool:
snes_address = self.ctx.snes_address
snes_device_number = -1
options = snes_options.split()
num_options = len(options)
if num_options > 0:
snes_device_number = int(options[0])
if num_options > 1:
snes_address = options[0]
snes_device_number = int(options[1])
self.ctx.snes_reconnect_address = None
if self.ctx.snes_connect_task:
self.ctx.snes_connect_task.cancel()
self.ctx.snes_connect_task = asyncio.create_task(snes_connect(self.ctx, snes_address, snes_device_number),
name="SNES Connect")
return True
def _cmd_snes_close(self) -> bool:
"""Close connection to a currently connected snes"""
self.ctx.snes_reconnect_address = None
self.ctx.cancel_snes_autoreconnect()
if self.ctx.snes_socket and not self.ctx.snes_socket.closed:
async_start(self.ctx.snes_socket.close())
return True
else:
return False
# Left here for quick re-addition for debugging.
# def _cmd_snes_write(self, address, data):
# """Write the specified byte (base10) to the SNES' memory address (base16)."""
# if self.ctx.snes_state != SNESState.SNES_ATTACHED:
# self.output("No attached SNES Device.")
# return False
# snes_buffered_write(self.ctx, int(address, 16), bytes([int(data)]))
# async_start(snes_flush_writes(self.ctx))
# self.output("Data Sent")
# return True
# def _cmd_snes_read(self, address, size=1):
# """Read the SNES' memory address (base16)."""
# if self.ctx.snes_state != SNESState.SNES_ATTACHED:
# self.output("No attached SNES Device.")
# return False
# data = await snes_read(self.ctx, int(address, 16), size)
# self.output(f"Data Read: {data}")
# return True
class SNIContext(CommonContext):
command_processor: typing.Type[SNIClientCommandProcessor] = SNIClientCommandProcessor
game: typing.Optional[str] = None # set in validate_rom
items_handling: typing.Optional[int] = None # set in game_watcher
snes_connect_task: "typing.Optional[asyncio.Task[None]]" = None
snes_autoreconnect_task: typing.Optional["asyncio.Task[None]"] = None
snes_address: str
snes_socket: typing.Optional[WebSocketClientProtocol]
snes_state: SNESState
snes_attached_device: typing.Optional[typing.Tuple[int, str]]
snes_reconnect_address: typing.Optional[str]
snes_recv_queue: "asyncio.Queue[bytes]"
snes_request_lock: asyncio.Lock
snes_write_buffer: typing.List[typing.Tuple[int, bytes]]
snes_connector_lock: threading.Lock
death_state: DeathState
killing_player_task: "typing.Optional[asyncio.Task[None]]"
allow_collect: bool
slow_mode: bool
client_handler: typing.Optional[SNIClient]
awaiting_rom: bool
rom: typing.Optional[bytes]
prev_rom: typing.Optional[bytes]
hud_message_queue: typing.List[str] # TODO: str is a guess, is this right?
death_link_allow_survive: bool
def __init__(self, snes_address: str, server_address: str, password: str) -> None:
super(SNIContext, self).__init__(server_address, password)
# snes stuff
self.snes_address = snes_address
self.snes_socket = None
self.snes_state = SNESState.SNES_DISCONNECTED
self.snes_attached_device = None
self.snes_reconnect_address = None
self.snes_recv_queue = asyncio.Queue()
self.snes_request_lock = asyncio.Lock()
self.snes_write_buffer = []
self.snes_connector_lock = threading.Lock()
self.death_state = DeathState.alive # for death link flop behaviour
self.killing_player_task = None
self.allow_collect = False
self.slow_mode = False
self.client_handler = None
self.awaiting_rom = False
self.rom = None
self.prev_rom = None
async def connection_closed(self) -> None:
await super(SNIContext, self).connection_closed()
self.awaiting_rom = False
def event_invalid_slot(self) -> typing.NoReturn:
if self.snes_socket is not None and not self.snes_socket.closed:
async_start(self.snes_socket.close())
raise Exception("Invalid ROM detected, "
"please verify that you have loaded the correct rom and reconnect your snes (/snes)")
async def server_auth(self, password_requested: bool = False) -> None:
if password_requested and not self.password:
await super(SNIContext, self).server_auth(password_requested)
if self.rom is None:
self.awaiting_rom = True
snes_logger.info(
"No ROM detected, awaiting snes connection to authenticate to the multiworld server (/snes)")
return
self.awaiting_rom = False
# TODO: This looks kind of hacky...
# Context.auth is meant to be the "name" parameter in send_connect,
# which has to be a str (bytes is not json serializable).
# But here, Context.auth is being used for something else
# (where it has to be bytes because it is compared with rom elsewhere).
# If we need to save something to compare with rom elsewhere,
# it should probably be in a different variable,
# and let auth be used for what it's meant for.
self.auth = self.rom
auth = base64.b64encode(self.rom).decode()
await self.send_connect(name=auth)
def cancel_snes_autoreconnect(self) -> bool:
if self.snes_autoreconnect_task:
self.snes_autoreconnect_task.cancel()
self.snes_autoreconnect_task = None
return True
return False
def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None:
if not self.killing_player_task or self.killing_player_task.done():
self.killing_player_task = asyncio.create_task(deathlink_kill_player(self))
super(SNIContext, self).on_deathlink(data)
async def handle_deathlink_state(self, currently_dead: bool) -> None:
# in this state we only care about triggering a death send
if self.death_state == DeathState.alive:
if currently_dead:
self.death_state = DeathState.dead
await self.send_death()
# in this state we care about confirming a kill, to move state to dead
elif self.death_state == DeathState.killing_player:
# this is being handled in deathlink_kill_player(ctx) already
pass
# in this state we wait until the player is alive again
elif self.death_state == DeathState.dead:
if not currently_dead:
self.death_state = DeathState.alive
async def shutdown(self) -> None:
await super(SNIContext, self).shutdown()
self.cancel_snes_autoreconnect()
if self.snes_connect_task:
try:
await asyncio.wait_for(self.snes_connect_task, 1)
except asyncio.TimeoutError:
self.snes_connect_task.cancel()
def on_package(self, cmd: str, args: typing.Dict[str, typing.Any]) -> None:
if cmd in {"Connected", "RoomUpdate"}:
if "checked_locations" in args and args["checked_locations"]:
new_locations = set(args["checked_locations"])
self.checked_locations |= new_locations
self.locations_scouted |= new_locations
# Items belonging to the player should not be marked as checked in game,
# since the player will likely need that item.
# Once the games handled by SNIClient gets made to be remote items,
# this will no longer be needed.
async_start(self.send_msgs([{"cmd": "LocationScouts", "locations": list(new_locations)}]))
def run_gui(self) -> None:
from kvui import GameManager
class SNIManager(GameManager):
logging_pairs = [
("Client", "Archipelago"),
("SNES", "SNES"),
]
base_title = "Archipelago SNI Client"
self.ui = SNIManager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") # type: ignore
async def deathlink_kill_player(ctx: SNIContext) -> None:
ctx.death_state = DeathState.killing_player
while ctx.death_state == DeathState.killing_player and \
ctx.snes_state == SNESState.SNES_ATTACHED:
if ctx.client_handler is None:
continue
await ctx.client_handler.deathlink_kill_player(ctx)
ctx.last_death_link = time.time()
_global_snes_reconnect_delay = 5
class SNESState(enum.IntEnum):
SNES_DISCONNECTED = 0
SNES_CONNECTING = 1
SNES_CONNECTED = 2
SNES_ATTACHED = 3
def launch_sni() -> None:
sni_path = Utils.get_options()["sni_options"]["sni_path"]
if not os.path.isdir(sni_path):
sni_path = Utils.local_path(sni_path)
if os.path.isdir(sni_path):
dir_entry: "os.DirEntry[str]"
for dir_entry in os.scandir(sni_path):
if dir_entry.is_file():
lower_file = dir_entry.name.lower()
if (lower_file.startswith("sni.") and not lower_file.endswith(".proto")) or (lower_file == "sni"):
sni_path = dir_entry.path
break
if os.path.isfile(sni_path):
snes_logger.info(f"Attempting to start {sni_path}")
import sys
if not sys.stdout: # if it spawns a visible console, may as well populate it
subprocess.Popen(os.path.abspath(sni_path), cwd=os.path.dirname(sni_path))
else:
proc = subprocess.Popen(os.path.abspath(sni_path), cwd=os.path.dirname(sni_path),
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
try:
proc.wait(.1) # wait a bit to see if startup fails (missing dependencies)
snes_logger.info('Failed to start SNI. Try running it externally for error output.')
except subprocess.TimeoutExpired:
pass # seems to be running
else:
snes_logger.info(
f"Attempt to start SNI was aborted as path {sni_path} was not found, "
f"please start it yourself if it is not running")
async def _snes_connect(ctx: SNIContext, address: str) -> WebSocketClientProtocol:
address = f"ws://{address}" if "://" not in address else address
snes_logger.info("Connecting to SNI at %s ..." % address)
seen_problems: typing.Set[str] = set()
while True:
try:
snes_socket = await websockets_connect(address, ping_timeout=None, ping_interval=None)
except Exception as e:
problem = "%s" % e
# only tell the user about new problems, otherwise silently lay in wait for a working connection
if problem not in seen_problems:
seen_problems.add(problem)
snes_logger.error(f"Error connecting to SNI ({problem})")
if len(seen_problems) == 1:
# this is the first problem. Let's try launching SNI if it isn't already running
launch_sni()
await asyncio.sleep(1)
else:
return snes_socket
class SNESRequest(typing.TypedDict):
Opcode: str
Space: str
Operands: typing.List[str]
# TODO: When Python 3.11 is the lowest version supported, `Operands` can use `typing.NotRequired` (pep-0655)
# Then the `Operands` key doesn't need to be given for opcodes that don't use it.
async def get_snes_devices(ctx: SNIContext) -> typing.List[str]:
socket = await _snes_connect(ctx, ctx.snes_address) # establish new connection to poll
DeviceList_Request: SNESRequest = {
"Opcode": "DeviceList",
"Space": "SNES",
"Operands": []
}
await socket.send(dumps(DeviceList_Request))
reply: typing.Dict[str, typing.Any] = loads(await socket.recv())
devices: typing.List[str] = reply['Results'] if 'Results' in reply and len(reply['Results']) > 0 else []
if not devices:
snes_logger.info('No SNES device found. Please connect a SNES device to SNI.')
while not devices and not ctx.exit_event.is_set():
await asyncio.sleep(0.1)
await socket.send(dumps(DeviceList_Request))
reply = loads(await socket.recv())
devices = reply['Results'] if 'Results' in reply and len(reply['Results']) > 0 else []
if devices:
await verify_snes_app(socket)
await socket.close()
return sorted(devices)
async def verify_snes_app(socket: WebSocketClientProtocol) -> None:
AppVersion_Request = {
"Opcode": "AppVersion",
}
await socket.send(dumps(AppVersion_Request))
app: str = loads(await socket.recv())["Results"][0]
if "SNI" not in app:
snes_logger.warning(f"Warning: Did not find SNI as the endpoint, instead {app} was found.")
async def snes_connect(ctx: SNIContext, address: str, deviceIndex: int = -1) -> None:
global _global_snes_reconnect_delay
if ctx.snes_socket is not None and ctx.snes_state == SNESState.SNES_CONNECTED:
if ctx.rom:
snes_logger.error('Already connected to SNES, with rom loaded.')
else:
snes_logger.error('Already connected to SNI, likely awaiting a device.')
return
ctx.cancel_snes_autoreconnect()
device = None
recv_task = None
ctx.snes_state = SNESState.SNES_CONNECTING
socket = await _snes_connect(ctx, address)
ctx.snes_socket = socket
ctx.snes_state = SNESState.SNES_CONNECTED
try:
devices = await get_snes_devices(ctx)
device_count = len(devices)
if device_count == 1:
device = devices[0]
elif ctx.snes_reconnect_address:
assert ctx.snes_attached_device
if ctx.snes_attached_device[1] in devices:
device = ctx.snes_attached_device[1]
else:
device = devices[ctx.snes_attached_device[0]]
elif device_count > 1:
if deviceIndex == -1:
snes_logger.info(f"Found {device_count} SNES devices. "
f"Connect to one with /snes <address> <device number>. For example /snes {address} 1")
for idx, availableDevice in enumerate(devices):
snes_logger.info(str(idx + 1) + ": " + availableDevice)
elif (deviceIndex < 0) or (deviceIndex - 1) > device_count:
snes_logger.warning("SNES device number out of range")
else:
device = devices[deviceIndex - 1]
if device is None:
await snes_disconnect(ctx)
return
snes_logger.info("Attaching to " + device)
Attach_Request: SNESRequest = {
"Opcode": "Attach",
"Space": "SNES",
"Operands": [device]
}
await ctx.snes_socket.send(dumps(Attach_Request))
ctx.snes_state = SNESState.SNES_ATTACHED
ctx.snes_attached_device = (devices.index(device), device)
ctx.snes_reconnect_address = address
recv_task = asyncio.create_task(snes_recv_loop(ctx))
except Exception as e:
ctx.snes_state = SNESState.SNES_DISCONNECTED
if task_alive(recv_task):
if not ctx.snes_socket.closed:
await ctx.snes_socket.close()
else:
if ctx.snes_socket is not None:
if not ctx.snes_socket.closed:
await ctx.snes_socket.close()
ctx.snes_socket = None
snes_logger.error(f"Error connecting to snes ({e}), retrying in {_global_snes_reconnect_delay} seconds")
ctx.snes_autoreconnect_task = asyncio.create_task(snes_autoreconnect(ctx), name="snes auto-reconnect")
_global_snes_reconnect_delay *= 2
else:
_global_snes_reconnect_delay = ctx.starting_reconnect_delay
snes_logger.info(f"Attached to {device}")
async def snes_disconnect(ctx: SNIContext) -> None:
if ctx.snes_socket:
if not ctx.snes_socket.closed:
await ctx.snes_socket.close()
ctx.snes_socket = None
def task_alive(task: typing.Optional[asyncio.Task]) -> bool:
if task:
return not task.done()
return False
async def snes_autoreconnect(ctx: SNIContext) -> None:
await asyncio.sleep(_global_snes_reconnect_delay)
if not ctx.snes_socket and not task_alive(ctx.snes_connect_task):
address = ctx.snes_reconnect_address if ctx.snes_reconnect_address else ctx.snes_address
ctx.snes_connect_task = asyncio.create_task(snes_connect(ctx, address), name="SNES Connect")
async def snes_recv_loop(ctx: SNIContext) -> None:
try:
if ctx.snes_socket is None:
raise Exception("invalid context state - snes_socket not connected")
async for msg in ctx.snes_socket:
ctx.snes_recv_queue.put_nowait(typing.cast(bytes, msg))
snes_logger.warning("Snes disconnected")
except Exception as e:
if not isinstance(e, WebSocketException):
snes_logger.exception(e)
snes_logger.error("Lost connection to the snes, type /snes to reconnect")
finally:
socket, ctx.snes_socket = ctx.snes_socket, None
if socket is not None and not socket.closed:
await socket.close()
ctx.snes_state = SNESState.SNES_DISCONNECTED
ctx.snes_recv_queue = asyncio.Queue()
ctx.hud_message_queue = []
ctx.rom = None
if ctx.snes_reconnect_address:
snes_logger.info(f"... automatically reconnecting to snes in {_global_snes_reconnect_delay} seconds")
assert ctx.snes_autoreconnect_task is None
ctx.snes_autoreconnect_task = asyncio.create_task(snes_autoreconnect(ctx), name="snes auto-reconnect")
async def snes_read(ctx: SNIContext, address: int, size: int) -> typing.Optional[bytes]:
try:
await ctx.snes_request_lock.acquire()
if (
ctx.snes_state != SNESState.SNES_ATTACHED or
ctx.snes_socket is None or
not ctx.snes_socket.open or
ctx.snes_socket.closed
):
return None
GetAddress_Request: SNESRequest = {
"Opcode": "GetAddress",
"Space": "SNES",
"Operands": [hex(address)[2:], hex(size)[2:]]
}
try:
await ctx.snes_socket.send(dumps(GetAddress_Request))
except ConnectionClosed:
return None
data: bytes = bytes()
while len(data) < size:
try:
data += await asyncio.wait_for(ctx.snes_recv_queue.get(), 5)
except asyncio.TimeoutError:
break
if len(data) != size:
snes_logger.error('Error reading %s, requested %d bytes, received %d' % (hex(address), size, len(data)))
if len(data):
snes_logger.error(str(data))
snes_logger.warning('Communication Failure with SNI')
if ctx.snes_socket is not None and not ctx.snes_socket.closed:
await ctx.snes_socket.close()
return None
return data
finally:
ctx.snes_request_lock.release()
async def snes_write(ctx: SNIContext, write_list: typing.List[typing.Tuple[int, bytes]]) -> bool:
try:
await ctx.snes_request_lock.acquire()
if ctx.snes_state != SNESState.SNES_ATTACHED or ctx.snes_socket is None or \
not ctx.snes_socket.open or ctx.snes_socket.closed:
return False
PutAddress_Request: SNESRequest = {"Opcode": "PutAddress", "Operands": [], 'Space': 'SNES'}
try:
for address, data in write_list:
PutAddress_Request['Operands'] = [hex(address)[2:], hex(len(data))[2:]]
# REVIEW: above: `if snes_socket is None: return False`
# Does it need to be checked again?
if ctx.snes_socket is not None:
await ctx.snes_socket.send(dumps(PutAddress_Request))
await ctx.snes_socket.send(data)
else:
snes_logger.warning(f"Could not send data to SNES: {data}")
except ConnectionClosed:
return False
return True
finally:
ctx.snes_request_lock.release()
def snes_buffered_write(ctx: SNIContext, address: int, data: bytes) -> None:
if ctx.snes_write_buffer and (ctx.snes_write_buffer[-1][0] + len(ctx.snes_write_buffer[-1][1])) == address:
# append to existing write command, bundling them
ctx.snes_write_buffer[-1] = (ctx.snes_write_buffer[-1][0], ctx.snes_write_buffer[-1][1] + data)
else:
ctx.snes_write_buffer.append((address, data))
async def snes_flush_writes(ctx: SNIContext) -> None:
if not ctx.snes_write_buffer:
return
# swap buffers
ctx.snes_write_buffer, writes = [], ctx.snes_write_buffer
await snes_write(ctx, writes)
async def game_watcher(ctx: SNIContext) -> None:
perf_counter = time.perf_counter()
while not ctx.exit_event.is_set():
try:
await asyncio.wait_for(ctx.watcher_event.wait(), 0.125)
except asyncio.TimeoutError:
pass
ctx.watcher_event.clear()
if not ctx.rom or not ctx.client_handler:
ctx.finished_game = False
ctx.death_link_allow_survive = False
from worlds.AutoSNIClient import AutoSNIClientRegister
ctx.client_handler = await AutoSNIClientRegister.get_handler(ctx)
if not ctx.client_handler:
continue
if not ctx.rom:
continue
if not ctx.prev_rom or ctx.prev_rom != ctx.rom:
ctx.locations_checked = set()
ctx.locations_scouted = set()
ctx.locations_info = {}
ctx.prev_rom = ctx.rom
if ctx.awaiting_rom:
await ctx.server_auth(False)
elif ctx.server is None:
snes_logger.warning("ROM detected but no active multiworld server connection. " +
"Connect using command: /connect server:port")
if not ctx.client_handler:
continue
rom_validated = await ctx.client_handler.validate_rom(ctx)
if not rom_validated or (ctx.auth and ctx.auth != ctx.rom):
snes_logger.warning("ROM change detected, please reconnect to the multiworld server")
await ctx.disconnect(allow_autoreconnect=True)
ctx.client_handler = None
ctx.rom = None
ctx.command_processor(ctx).connect_to_snes()
continue
delay = 7 if ctx.slow_mode else 0
if time.perf_counter() - perf_counter < delay:
continue
perf_counter = time.perf_counter()
await ctx.client_handler.game_watcher(ctx)
async def run_game(romfile: str) -> None:
auto_start = typing.cast(typing.Union[bool, str],
Utils.get_options()["sni_options"].get("snes_rom_start", True))
if auto_start is True:
import webbrowser
webbrowser.open(romfile)
elif isinstance(auto_start, str) and os.path.isfile(auto_start):
subprocess.Popen([auto_start, romfile],
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
async def main() -> None:
multiprocessing.freeze_support()
parser = get_base_parser()
parser.add_argument('diff_file', default="", type=str, nargs="?",
help='Path to a Archipelago Binary Patch file')
parser.add_argument('--snes', default='localhost:23074', help='Address of the SNI server.')
parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
args = parser.parse_args()
if args.diff_file:
import Patch
logging.info("Patch file was supplied. Creating sfc rom..")
try:
meta, romfile = Patch.create_rom_file(args.diff_file)
except Exception as e:
Utils.messagebox('Error', str(e), True)
raise
args.connect = meta["server"]
logging.info(f"Wrote rom file to {romfile}")
if args.diff_file.endswith(".apsoe"):
import webbrowser
webbrowser.open(f"http://www.evermizer.com/apclient/#server={meta['server']}")
logging.info("Starting Evermizer Client in your Browser...")
import time
time.sleep(3)
sys.exit()
elif args.diff_file.endswith(".aplttp"):
from worlds.alttp.Client import get_alttp_settings
adjustedromfile, adjusted = get_alttp_settings(romfile)
async_start(run_game(adjustedromfile if adjusted else romfile))
else:
async_start(run_game(romfile))
ctx = SNIContext(args.snes, args.connect, args.password)
if ctx.server_task is None:
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
ctx.snes_connect_task = asyncio.create_task(snes_connect(ctx, ctx.snes_address), name="SNES Connect")
watcher_task = asyncio.create_task(game_watcher(ctx), name="GameWatcher")
await ctx.exit_event.wait()
ctx.server_address = None
ctx.snes_reconnect_address = None
if ctx.snes_socket is not None and not ctx.snes_socket.closed:
await ctx.snes_socket.close()
await watcher_task
await ctx.shutdown()
if __name__ == '__main__':
colorama.init()
asyncio.run(main())
colorama.deinit()

1052
Starcraft2Client.py Normal file

File diff suppressed because it is too large Load Diff

703
Utils.py
View File

@@ -1,6 +1,32 @@
from __future__ import annotations
import asyncio
import json
import typing
import builtins
import os
import subprocess
import sys
import pickle
import functools
import io
import collections
import importlib
import logging
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union
from yaml import load, load_all, dump, SafeLoader
try:
from yaml import CLoader as UnsafeLoader
from yaml import CDumper as Dumper
except ImportError:
from yaml import Loader as UnsafeLoader
from yaml import Dumper
if typing.TYPE_CHECKING:
import tkinter
import pathlib
def tuplize_version(version: str) -> Version:
@@ -13,67 +39,61 @@ class Version(typing.NamedTuple):
build: int
__version__ = "0.1.6"
__version__ = "0.4.0"
version_tuple = tuplize_version(__version__)
import builtins
import os
import subprocess
import sys
import pickle
import functools
import io
import collections
from yaml import load, dump, safe_load
try:
from yaml import CLoader as Loader
except ImportError:
from yaml import Loader
is_linux = sys.platform.startswith("linux")
is_macos = sys.platform == "darwin"
is_windows = sys.platform in ("win32", "cygwin", "msys")
def int16_as_bytes(value):
def int16_as_bytes(value: int) -> typing.List[int]:
value = value & 0xFFFF
return [value & 0xFF, (value >> 8) & 0xFF]
def int32_as_bytes(value):
def int32_as_bytes(value: int) -> typing.List[int]:
value = value & 0xFFFFFFFF
return [value & 0xFF, (value >> 8) & 0xFF, (value >> 16) & 0xFF, (value >> 24) & 0xFF]
def pc_to_snes(value):
def pc_to_snes(value: int) -> int:
return ((value << 1) & 0x7F0000) | (value & 0x7FFF) | 0x8000
def snes_to_pc(value):
def snes_to_pc(value: int) -> int:
return ((value & 0x7F0000) >> 1) | (value & 0x7FFF)
def cache_argsless(function):
if function.__code__.co_argcount:
raise Exception("Can only cache 0 argument functions with this cache.")
RetType = typing.TypeVar("RetType")
result = sentinel = object()
def _wrap():
def cache_argsless(function: typing.Callable[[], RetType]) -> typing.Callable[[], RetType]:
assert not function.__code__.co_argcount, "Can only cache 0 argument functions with this cache."
sentinel = object()
result: typing.Union[object, RetType] = sentinel
def _wrap() -> RetType:
nonlocal result
if result is sentinel:
result = function()
return result
return typing.cast(RetType, result)
return _wrap
def is_frozen() -> bool:
return getattr(sys, 'frozen', False)
return typing.cast(bool, getattr(sys, 'frozen', False))
def local_path(*path):
if local_path.cached_path:
return os.path.join(local_path.cached_path, *path)
def local_path(*path: str) -> str:
"""
Returns path to a file in the local Archipelago installation or source.
This might be read-only and user_path should be used instead for ROMs, configuration, etc.
"""
if hasattr(local_path, 'cached_path'):
pass
elif is_frozen():
if hasattr(sys, "_MEIPASS"):
# we are running in a PyInstaller bundle
@@ -83,7 +103,7 @@ def local_path(*path):
local_path.cached_path = os.path.dirname(os.path.abspath(sys.argv[0]))
else:
import __main__
if hasattr(__main__, "__file__"):
if hasattr(__main__, "__file__") and os.path.isfile(__main__.__file__):
# we are running in a normal Python environment
local_path.cached_path = os.path.dirname(os.path.abspath(__main__.__file__))
else:
@@ -93,45 +113,109 @@ def local_path(*path):
return os.path.join(local_path.cached_path, *path)
local_path.cached_path = None
def home_path(*path: str) -> str:
"""Returns path to a file in the user home's Archipelago directory."""
if hasattr(home_path, 'cached_path'):
pass
elif sys.platform.startswith('linux'):
home_path.cached_path = os.path.expanduser('~/Archipelago')
os.makedirs(home_path.cached_path, 0o700, exist_ok=True)
else:
# not implemented
home_path.cached_path = local_path() # this will generate the same exceptions we got previously
return os.path.join(home_path.cached_path, *path)
def output_path(*path):
if output_path.cached_path:
def user_path(*path: str) -> str:
"""Returns either local_path or home_path based on write permissions."""
if hasattr(user_path, "cached_path"):
pass
elif os.access(local_path(), os.W_OK):
user_path.cached_path = local_path()
else:
user_path.cached_path = home_path()
# populate home from local - TODO: upgrade feature
if user_path.cached_path != local_path() and not os.path.exists(user_path("host.yaml")):
import shutil
for dn in ("Players", "data/sprites"):
shutil.copytree(local_path(dn), user_path(dn), dirs_exist_ok=True)
for fn in ("manifest.json", "host.yaml"):
shutil.copy2(local_path(fn), user_path(fn))
return os.path.join(user_path.cached_path, *path)
def cache_path(*path: str) -> str:
"""Returns path to a file in the user's Archipelago cache directory."""
if hasattr(cache_path, "cached_path"):
pass
else:
import platformdirs
cache_path.cached_path = platformdirs.user_cache_dir("Archipelago", False)
return os.path.join(cache_path.cached_path, *path)
def output_path(*path: str) -> str:
if hasattr(output_path, 'cached_path'):
return os.path.join(output_path.cached_path, *path)
output_path.cached_path = local_path(get_options()["general_options"]["output_path"])
output_path.cached_path = user_path(get_options()["general_options"]["output_path"])
path = os.path.join(output_path.cached_path, *path)
os.makedirs(os.path.dirname(path), exist_ok=True)
return path
output_path.cached_path = None
def open_file(filename):
if sys.platform == 'win32':
def open_file(filename: typing.Union[str, "pathlib.Path"]) -> None:
if is_windows:
os.startfile(filename)
else:
open_command = 'open' if sys.platform == 'darwin' else 'xdg-open'
from shutil import which
open_command = which("open") if is_macos else (which("xdg-open") or which("gnome-open") or which("kde-open"))
subprocess.call([open_command, filename])
parse_yaml = safe_load
unsafe_parse_yaml = functools.partial(load, Loader=Loader)
# from https://gist.github.com/pypt/94d747fe5180851196eb#gistcomment-4015118 with some changes
class UniqueKeyLoader(SafeLoader):
def construct_mapping(self, node, deep=False):
mapping = set()
for key_node, value_node in node.value:
key = self.construct_object(key_node, deep=deep)
if key in mapping:
logging.error(f"YAML duplicates sanity check failed{key_node.start_mark}")
raise KeyError(f"Duplicate key {key} found in YAML. Already found keys: {mapping}.")
mapping.add(key)
return super().construct_mapping(node, deep)
parse_yaml = functools.partial(load, Loader=UniqueKeyLoader)
parse_yamls = functools.partial(load_all, Loader=UniqueKeyLoader)
unsafe_parse_yaml = functools.partial(load, Loader=UnsafeLoader)
del load, load_all # should not be used. don't leak their names
def get_cert_none_ssl_context():
import ssl
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
return ctx
@cache_argsless
def get_public_ipv4() -> str:
import socket
import urllib.request
import logging
ip = socket.gethostbyname(socket.gethostname())
ctx = get_cert_none_ssl_context()
try:
ip = urllib.request.urlopen('https://checkip.amazonaws.com/').read().decode('utf8').strip()
ip = urllib.request.urlopen("https://checkip.amazonaws.com/", context=ctx, timeout=10).read().decode("utf8").strip()
except Exception as e:
# noinspection PyBroadException
try:
ip = urllib.request.urlopen('https://v4.ident.me').read().decode('utf8').strip()
except:
ip = urllib.request.urlopen("https://v4.ident.me", context=ctx, timeout=10).read().decode("utf8").strip()
except Exception:
logging.exception(e)
pass # we could be offline, in a local game, so no point in erroring out
return ip
@@ -141,31 +225,46 @@ def get_public_ipv4() -> str:
def get_public_ipv6() -> str:
import socket
import urllib.request
import logging
ip = socket.gethostbyname(socket.gethostname())
ctx = get_cert_none_ssl_context()
try:
ip = urllib.request.urlopen('https://v6.ident.me').read().decode('utf8').strip()
ip = urllib.request.urlopen("https://v6.ident.me", context=ctx, timeout=10).read().decode("utf8").strip()
except Exception as e:
logging.exception(e)
pass # we could be offline, in a local game, or ipv6 may not be available
return ip
OptionsType = typing.Dict[str, typing.Dict[str, typing.Any]]
@cache_argsless
def get_default_options() -> dict:
def get_default_options() -> OptionsType:
# Refer to host.yaml for comments as to what all these options mean.
options = {
"general_options": {
"output_path": "output",
},
"factorio_options": {
"executable": "factorio\\bin\\x64\\factorio",
"executable": os.path.join("factorio", "bin", "x64", "factorio"),
"filter_item_sends": False,
"bridge_chat_out": True,
},
"sni_options": {
"sni_path": "SNI",
"snes_rom_start": True,
},
"sm_options": {
"rom_file": "Super Metroid (JU).sfc",
},
"soe_options": {
"rom_file": "Secret of Evermore (USA).sfc",
},
"lttp_options": {
"rom_file": "Zelda no Densetsu - Kamigami no Triforce (Japan).sfc",
"sni": "SNI",
"rom_start": True,
},
"ladx_options": {
"rom_file": "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc",
},
"server_options": {
"host": None,
@@ -179,31 +278,75 @@ def get_default_options() -> dict:
"disable_item_cheat": False,
"location_check_points": 1,
"hint_cost": 10,
"forfeit_mode": "goal",
"release_mode": "goal",
"collect_mode": "disabled",
"remaining_mode": "goal",
"auto_shutdown": 0,
"compatibility": 2,
"log_network": 0
},
"generator": {
"teams": 1,
"enemizer_path": "EnemizerCLI/EnemizerCLI.Core.exe",
"enemizer_path": os.path.join("EnemizerCLI", "EnemizerCLI.Core"),
"player_files_path": "Players",
"players": 0,
"weights_file_path": "weights.yaml",
"meta_file_path": "meta.yaml",
"spoiler": 2,
"spoiler": 3,
"glitch_triforce_room": 1,
"race": 0,
"plando_options": "bosses",
}
},
"minecraft_options": {
"forge_directory": "Minecraft Forge server",
"max_heap_size": "2G",
"release_channel": "release"
},
"oot_options": {
"rom_file": "The Legend of Zelda - Ocarina of Time.z64",
"rom_start": True
},
"dkc3_options": {
"rom_file": "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc",
},
"smw_options": {
"rom_file": "Super Mario World (USA).sfc",
},
"zillion_options": {
"rom_file": "Zillion (UE) [!].sms",
# RetroArch doesn't make it easy to launch a game from the command line.
# You have to know the path to the emulator core library on the user's computer.
"rom_start": "retroarch",
},
"pokemon_rb_options": {
"red_rom_file": "Pokemon Red (UE) [S][!].gb",
"blue_rom_file": "Pokemon Blue (UE) [S][!].gb",
"rom_start": True
},
"ffr_options": {
"display_msgs": True,
},
"lufia2ac_options": {
"rom_file": "Lufia II - Rise of the Sinistrals (USA).sfc",
},
"tloz_options": {
"rom_file": "Legend of Zelda, The (U) (PRG0) [!].nes",
"rom_start": True,
"display_msgs": True,
},
"wargroove_options": {
"root_directory": "C:/Program Files (x86)/Steam/steamapps/common/Wargroove"
},
"adventure_options": {
"rom_file": "ADVNTURE.BIN",
"display_msgs": True,
"rom_start": True,
"rom_args": ""
},
}
return options
def update_options(src: dict, dest: dict, filename: str, keys: list) -> dict:
import logging
def update_options(src: dict, dest: dict, filename: str, keys: list) -> OptionsType:
for key, value in src.items():
new_keys = keys.copy()
new_keys.append(key)
@@ -223,54 +366,42 @@ def update_options(src: dict, dest: dict, filename: str, keys: list) -> dict:
@cache_argsless
def get_options() -> dict:
if not hasattr(get_options, "options"):
locations = ("options.yaml", "host.yaml",
local_path("options.yaml"), local_path("host.yaml"))
def get_options() -> OptionsType:
filenames = ("options.yaml", "host.yaml")
locations: typing.List[str] = []
if os.path.join(os.getcwd()) != local_path():
locations += filenames # use files from cwd only if it's not the local_path
locations += [user_path(filename) for filename in filenames]
for location in locations:
if os.path.exists(location):
with open(location) as f:
options = parse_yaml(f.read())
for location in locations:
if os.path.exists(location):
with open(location) as f:
options = parse_yaml(f.read())
return update_options(get_default_options(), options, location, list())
get_options.options = update_options(get_default_options(), options, location, list())
break
else:
raise FileNotFoundError(f"Could not find {locations[1]} to load options.")
return get_options.options
def get_item_name_from_id(code: int) -> str:
from worlds import lookup_any_item_id_to_name
return lookup_any_item_id_to_name.get(code, f'Unknown item (ID:{code})')
def get_location_name_from_id(code: int) -> str:
from worlds import lookup_any_location_id_to_name
return lookup_any_location_id_to_name.get(code, f'Unknown location (ID:{code})')
raise FileNotFoundError(f"Could not find {filenames[1]} to load options.")
def persistent_store(category: str, key: typing.Any, value: typing.Any):
path = local_path("_persistent_storage.yaml")
path = user_path("_persistent_storage.yaml")
storage: dict = persistent_load()
category = storage.setdefault(category, {})
category[key] = value
with open(path, "wt") as f:
f.write(dump(storage))
f.write(dump(storage, Dumper=Dumper))
def persistent_load() -> typing.Dict[dict]:
def persistent_load() -> typing.Dict[str, dict]:
storage = getattr(persistent_load, "storage", None)
if storage:
return storage
path = local_path("_persistent_storage.yaml")
path = user_path("_persistent_storage.yaml")
storage: dict = {}
if os.path.exists(path):
try:
with open(path, "r") as f:
storage = unsafe_parse_yaml(f.read())
except Exception as e:
import logging
logging.debug(f"Could not read store: {e}")
if storage is None:
storage = {}
@@ -278,63 +409,48 @@ def persistent_load() -> typing.Dict[dict]:
return storage
def get_adjuster_settings(romfile: str) -> typing.Tuple[str, bool]:
if hasattr(get_adjuster_settings, "adjuster_settings"):
adjuster_settings = getattr(get_adjuster_settings, "adjuster_settings")
else:
adjuster_settings = persistent_load().get("adjuster", {}).get("last_settings_3", {})
if adjuster_settings:
import pprint
from worlds.alttp.Rom import get_base_rom_path
adjuster_settings.rom = romfile
adjuster_settings.baserom = get_base_rom_path()
adjuster_settings.world = None
whitelist = {"music", "menuspeed", "heartbeep", "heartcolor", "ow_palettes", "quickswap",
"uw_palettes", "sprite"}
printed_options = {name: value for name, value in vars(adjuster_settings).items() if name in whitelist}
if hasattr(adjuster_settings, "sprite_pool"):
sprite_pool = {}
for sprite in getattr(adjuster_settings, "sprite_pool"):
if sprite in sprite_pool:
sprite_pool[sprite] += 1
else:
sprite_pool[sprite] = 1
if sprite_pool:
printed_options["sprite_pool"] = sprite_pool
def get_file_safe_name(name: str) -> str:
return "".join(c for c in name if c not in '<>:"/\\|?*')
if hasattr(get_adjuster_settings, "adjust_wanted"):
adjust_wanted = getattr(get_adjuster_settings, "adjust_wanted")
elif persistent_load().get("adjuster", {}).get("never_adjust", False): # never adjust, per user request
return romfile, False
else:
adjust_wanted = input(f"Last used adjuster settings were found. Would you like to apply these? \n"
f"{pprint.pformat(printed_options)}\n"
f"Enter yes, no or never: ")
if adjust_wanted and adjust_wanted.startswith("y"):
if hasattr(adjuster_settings, "sprite_pool"):
from LttPAdjuster import AdjusterWorld
adjuster_settings.world = AdjusterWorld(getattr(adjuster_settings, "sprite_pool"))
def load_data_package_for_checksum(game: str, checksum: typing.Optional[str]) -> Dict[str, Any]:
if checksum and game:
if checksum != get_file_safe_name(checksum):
raise ValueError(f"Bad symbols in checksum: {checksum}")
path = cache_path("datapackage", get_file_safe_name(game), f"{checksum}.json")
if os.path.exists(path):
try:
with open(path, "r", encoding="utf-8-sig") as f:
return json.load(f)
except Exception as e:
logging.debug(f"Could not load data package: {e}")
adjusted = True
import LttPAdjuster
_, romfile = LttPAdjuster.adjust(adjuster_settings)
# fall back to old cache
cache = persistent_load().get("datapackage", {}).get("games", {}).get(game, {})
if cache.get("checksum") == checksum:
return cache
if hasattr(adjuster_settings, "world"):
delattr(adjuster_settings, "world")
elif adjust_wanted and "never" in adjust_wanted:
persistent_store("adjuster", "never_adjust", True)
return romfile, False
else:
adjusted = False
import logging
if not hasattr(get_adjuster_settings, "adjust_wanted"):
logging.info(f"Skipping post-patch adjustment")
get_adjuster_settings.adjuster_settings = adjuster_settings
get_adjuster_settings.adjust_wanted = adjust_wanted
return romfile, adjusted
return romfile, False
# cache does not match
return {}
def store_data_package_for_checksum(game: str, data: typing.Dict[str, Any]) -> None:
checksum = data.get("checksum")
if checksum and game:
if checksum != get_file_safe_name(checksum):
raise ValueError(f"Bad symbols in checksum: {checksum}")
game_folder = cache_path("datapackage", get_file_safe_name(game))
os.makedirs(game_folder, exist_ok=True)
try:
with open(os.path.join(game_folder, f"{checksum}.json"), "w", encoding="utf-8-sig") as f:
json.dump(data, f, ensure_ascii=False, separators=(",", ":"))
except Exception as e:
logging.debug(f"Could not store data package: {e}")
def get_adjuster_settings(game_name: str) -> typing.Dict[str, typing.Any]:
adjuster_settings = persistent_load().get("adjuster", {}).get(game_name, {})
return adjuster_settings
@cache_argsless
@@ -349,27 +465,39 @@ def get_unique_identifier():
return uuid
safe_builtins = {
safe_builtins = frozenset((
'set',
'frozenset',
}
))
class RestrictedUnpickler(pickle.Unpickler):
def __init__(self, *args, **kwargs):
super(RestrictedUnpickler, self).__init__(*args, **kwargs)
self.options_module = importlib.import_module("Options")
self.net_utils_module = importlib.import_module("NetUtils")
self.generic_properties_module = importlib.import_module("worlds.generic")
def find_class(self, module, name):
if module == "builtins" and name in safe_builtins:
return getattr(builtins, name)
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint"}:
import NetUtils
return getattr(NetUtils, name)
if module == "Options":
import Options
obj = getattr(Options, name)
if issubclass(obj, Options.Option):
# used by MultiServer -> savegame/multidata
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint", "SlotType", "NetworkSlot"}:
return getattr(self.net_utils_module, name)
# Options and Plando are unpickled by WebHost -> Generate
if module == "worlds.generic" and name in {"PlandoItem", "PlandoConnection"}:
return getattr(self.generic_properties_module, name)
# pep 8 specifies that modules should have "all-lowercase names" (options, not Options)
if module.lower().endswith("options"):
if module == "Options":
mod = self.options_module
else:
mod = importlib.import_module(module)
obj = getattr(mod, name)
if issubclass(obj, self.options_module.Option):
return obj
# Forbid everything else.
raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
(module, name))
raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden")
def restricted_loads(s):
@@ -378,6 +506,269 @@ def restricted_loads(s):
class KeyedDefaultDict(collections.defaultdict):
"""defaultdict variant that uses the missing key as argument to default_factory"""
default_factory: typing.Callable[[typing.Any], typing.Any]
def __missing__(self, key):
self[key] = value = self.default_factory(key)
return value
return value
def get_text_between(text: str, start: str, end: str) -> str:
return text[text.index(start) + len(start): text.rindex(end)]
def get_text_after(text: str, start: str) -> str:
return text[text.index(start) + len(start):]
loglevel_mapping = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}
def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, write_mode: str = "w",
log_format: str = "[%(name)s at %(asctime)s]: %(message)s",
exception_logger: typing.Optional[str] = None):
import datetime
loglevel: int = loglevel_mapping.get(loglevel, loglevel)
log_folder = user_path("logs")
os.makedirs(log_folder, exist_ok=True)
root_logger = logging.getLogger()
for handler in root_logger.handlers[:]:
root_logger.removeHandler(handler)
handler.close()
root_logger.setLevel(loglevel)
if "a" not in write_mode:
name += f"_{datetime.datetime.now().strftime('%Y_%m_%d_%H_%M_%S')}"
file_handler = logging.FileHandler(
os.path.join(log_folder, f"{name}.txt"),
write_mode,
encoding="utf-8-sig")
file_handler.setFormatter(logging.Formatter(log_format))
root_logger.addHandler(file_handler)
if sys.stdout:
root_logger.addHandler(
logging.StreamHandler(sys.stdout)
)
# Relay unhandled exceptions to logger.
if not getattr(sys.excepthook, "_wrapped", False): # skip if already modified
orig_hook = sys.excepthook
def handle_exception(exc_type, exc_value, exc_traceback):
if issubclass(exc_type, KeyboardInterrupt):
sys.__excepthook__(exc_type, exc_value, exc_traceback)
return
logging.getLogger(exception_logger).exception("Uncaught exception",
exc_info=(exc_type, exc_value, exc_traceback))
return orig_hook(exc_type, exc_value, exc_traceback)
handle_exception._wrapped = True
sys.excepthook = handle_exception
def _cleanup():
for file in os.scandir(log_folder):
if file.name.endswith(".txt"):
last_change = datetime.datetime.fromtimestamp(file.stat().st_mtime)
if datetime.datetime.now() - last_change > datetime.timedelta(days=7):
try:
os.unlink(file.path)
except Exception as e:
logging.exception(e)
else:
logging.debug(f"Deleted old logfile {file.path}")
import threading
threading.Thread(target=_cleanup, name="LogCleaner").start()
import platform
logging.info(
f"Archipelago ({__version__}) logging initialized"
f" on {platform.platform()}"
f" running Python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
)
def stream_input(stream, queue):
def queuer():
while 1:
try:
text = stream.readline().strip()
except UnicodeDecodeError as e:
logging.exception(e)
else:
if text:
queue.put_nowait(text)
from threading import Thread
thread = Thread(target=queuer, name=f"Stream handler for {stream.name}", daemon=True)
thread.start()
return thread
def tkinter_center_window(window: "tkinter.Tk") -> None:
window.update()
x = int(window.winfo_screenwidth() / 2 - window.winfo_reqwidth() / 2)
y = int(window.winfo_screenheight() / 2 - window.winfo_reqheight() / 2)
window.geometry(f"+{x}+{y}")
class VersionException(Exception):
pass
def chaining_prefix(index: int, labels: typing.Tuple[str]) -> str:
text = ""
max_label = len(labels) - 1
while index > max_label:
text += labels[-1]
index -= max_label
return labels[index] + text
# noinspection PyPep8Naming
def format_SI_prefix(value, power=1000, power_labels=("", "k", "M", "G", "T", "P", "E", "Z", "Y")) -> str:
"""Formats a value into a value + metric/si prefix. More info at https://en.wikipedia.org/wiki/Metric_prefix"""
import decimal
n = 0
value = decimal.Decimal(value)
limit = power - decimal.Decimal("0.005")
while value >= limit:
value /= power
n += 1
return f"{value.quantize(decimal.Decimal('1.00'))} {chaining_prefix(n, power_labels)}"
def get_fuzzy_results(input_word: str, wordlist: typing.Sequence[str], limit: typing.Optional[int] = None) \
-> typing.List[typing.Tuple[str, int]]:
import jellyfish
def get_fuzzy_ratio(word1: str, word2: str) -> float:
return (1 - jellyfish.damerau_levenshtein_distance(word1.lower(), word2.lower())
/ max(len(word1), len(word2)))
limit: int = limit if limit else len(wordlist)
return list(
map(
lambda container: (container[0], int(container[1]*100)), # convert up to limit to int %
sorted(
map(lambda candidate:
(candidate, get_fuzzy_ratio(input_word, candidate)),
wordlist),
key=lambda element: element[1],
reverse=True)[0:limit]
)
)
def open_filename(title: str, filetypes: typing.Sequence[typing.Tuple[str, typing.Sequence[str]]]) \
-> typing.Optional[str]:
def run(*args: str):
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
if is_linux:
# prefer native dialog
from shutil import which
kdialog = which("kdialog")
if kdialog:
k_filters = '|'.join((f'{text} (*{" *".join(ext)})' for (text, ext) in filetypes))
return run(kdialog, f"--title={title}", "--getopenfilename", ".", k_filters)
zenity = which("zenity")
if zenity:
z_filters = (f'--file-filter={text} ({", ".join(ext)}) | *{" *".join(ext)}' for (text, ext) in filetypes)
return run(zenity, f"--title={title}", "--file-selection", *z_filters)
# fall back to tk
try:
import tkinter
import tkinter.filedialog
except Exception as e:
logging.error('Could not load tkinter, which is likely not installed. '
f'This attempt was made because open_filename was used for "{title}".')
raise e
else:
root = tkinter.Tk()
root.withdraw()
return tkinter.filedialog.askopenfilename(title=title, filetypes=((t[0], ' '.join(t[1])) for t in filetypes))
def messagebox(title: str, text: str, error: bool = False) -> None:
def run(*args: str):
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
def is_kivy_running():
if "kivy" in sys.modules:
from kivy.app import App
return App.get_running_app() is not None
return False
if is_kivy_running():
from kvui import MessageBox
MessageBox(title, text, error).open()
return
if is_linux and "tkinter" not in sys.modules:
# prefer native dialog
from shutil import which
kdialog = which("kdialog")
if kdialog:
return run(kdialog, f"--title={title}", "--error" if error else "--msgbox", text)
zenity = which("zenity")
if zenity:
return run(zenity, f"--title={title}", f"--text={text}", "--error" if error else "--info")
# fall back to tk
try:
import tkinter
from tkinter.messagebox import showerror, showinfo
except Exception as e:
logging.error('Could not load tkinter, which is likely not installed. '
f'This attempt was made because messagebox was used for "{title}".')
raise e
else:
root = tkinter.Tk()
root.withdraw()
showerror(title, text) if error else showinfo(title, text)
root.update()
def title_sorted(data: typing.Sequence, key=None, ignore: typing.Set = frozenset(("a", "the"))):
"""Sorts a sequence of text ignoring typical articles like "a" or "the" in the beginning."""
def sorter(element: Union[str, Dict[str, Any]]) -> str:
if (not isinstance(element, str)):
element = element["title"]
parts = element.split(maxsplit=1)
if parts[0].lower() in ignore:
return parts[1].lower()
else:
return element.lower()
return sorted(data, key=lambda i: sorter(key(i)) if key else sorter(i))
def read_snes_rom(stream: BinaryIO, strip_header: bool = True) -> bytearray:
"""Reads rom into bytearray and optionally strips off any smc header"""
buffer = bytearray(stream.read())
if strip_header and len(buffer) % 0x400 == 0x200:
return buffer[0x200:]
return buffer
_faf_tasks: "Set[asyncio.Task[None]]" = set()
def async_start(co: Coroutine[typing.Any, typing.Any, bool], name: Optional[str] = None) -> None:
"""
Use this to start a task when you don't keep a reference to it or immediately await it,
to prevent early garbage collection. "fire-and-forget"
"""
# https://docs.python.org/3.10/library/asyncio-task.html#asyncio.create_task
# Python docs:
# ```
# Important: Save a reference to the result of [asyncio.create_task],
# to avoid a task disappearing mid-execution.
# ```
# This implementation follows the pattern given in that documentation.
task = asyncio.create_task(co, name=name)
_faf_tasks.add(task)
task.add_done_callback(_faf_tasks.discard)

445
WargrooveClient.py Normal file
View File

@@ -0,0 +1,445 @@
from __future__ import annotations
import atexit
import os
import sys
import asyncio
import random
import shutil
from typing import Tuple, List, Iterable, Dict
from worlds.wargroove import WargrooveWorld
from worlds.wargroove.Items import item_table, faction_table, CommanderData, ItemData
import ModuleUpdate
ModuleUpdate.update()
import Utils
import json
import logging
if __name__ == "__main__":
Utils.init_logging("WargrooveClient", exception_logger="Client")
from NetUtils import NetworkItem, ClientStatus
from CommonClient import gui_enabled, logger, get_base_parser, ClientCommandProcessor, \
CommonContext, server_loop
wg_logger = logging.getLogger("WG")
class WargrooveClientCommandProcessor(ClientCommandProcessor):
def _cmd_resync(self):
"""Manually trigger a resync."""
self.output(f"Syncing items.")
self.ctx.syncing = True
def _cmd_commander(self, *commander_name: Iterable[str]):
"""Set the current commander to the given commander."""
if commander_name:
self.ctx.set_commander(' '.join(commander_name))
else:
if self.ctx.can_choose_commander:
commanders = self.ctx.get_commanders()
wg_logger.info('Unlocked commanders: ' +
', '.join((commander.name for commander, unlocked in commanders if unlocked)))
wg_logger.info('Locked commanders: ' +
', '.join((commander.name for commander, unlocked in commanders if not unlocked)))
else:
wg_logger.error('Cannot set commanders in this game mode.')
class WargrooveContext(CommonContext):
command_processor: int = WargrooveClientCommandProcessor
game = "Wargroove"
items_handling = 0b111 # full remote
current_commander: CommanderData = faction_table["Starter"][0]
can_choose_commander: bool = False
commander_defense_boost_multiplier: int = 0
income_boost_multiplier: int = 0
starting_groove_multiplier: float
faction_item_ids = {
'Starter': 0,
'Cherrystone': 52025,
'Felheim': 52026,
'Floran': 52027,
'Heavensong': 52028,
'Requiem': 52029,
'Outlaw': 52030
}
buff_item_ids = {
'Income Boost': 52023,
'Commander Defense Boost': 52024,
}
def __init__(self, server_address, password):
super(WargrooveContext, self).__init__(server_address, password)
self.send_index: int = 0
self.syncing = False
self.awaiting_bridge = False
# self.game_communication_path: files go in this path to pass data between us and the actual game
if "appdata" in os.environ:
options = Utils.get_options()
root_directory = os.path.join(options["wargroove_options"]["root_directory"])
data_directory = os.path.join("lib", "worlds", "wargroove", "data")
dev_data_directory = os.path.join("worlds", "wargroove", "data")
appdata_wargroove = os.path.expandvars(os.path.join("%APPDATA%", "Chucklefish", "Wargroove"))
if not os.path.isfile(os.path.join(root_directory, "win64_bin", "wargroove64.exe")):
print_error_and_close("WargrooveClient couldn't find wargroove64.exe. "
"Unable to infer required game_communication_path")
self.game_communication_path = os.path.join(root_directory, "AP")
if not os.path.exists(self.game_communication_path):
os.makedirs(self.game_communication_path)
self.remove_communication_files()
atexit.register(self.remove_communication_files)
if not os.path.isdir(appdata_wargroove):
print_error_and_close("WargrooveClient couldn't find Wargoove in appdata!"
"Boot Wargroove and then close it to attempt to fix this error")
if not os.path.isdir(data_directory):
data_directory = dev_data_directory
if not os.path.isdir(data_directory):
print_error_and_close("WargrooveClient couldn't find Wargoove mod and save files in install!")
shutil.copytree(data_directory, appdata_wargroove, dirs_exist_ok=True)
else:
print_error_and_close("WargrooveClient couldn't detect system type. "
"Unable to infer required game_communication_path")
async def server_auth(self, password_requested: bool = False):
if password_requested and not self.password:
await super(WargrooveContext, self).server_auth(password_requested)
await self.get_username()
await self.send_connect()
async def connection_closed(self):
await super(WargrooveContext, self).connection_closed()
self.remove_communication_files()
@property
def endpoints(self):
if self.server:
return [self.server]
else:
return []
async def shutdown(self):
await super(WargrooveContext, self).shutdown()
self.remove_communication_files()
def remove_communication_files(self):
for root, dirs, files in os.walk(self.game_communication_path):
for file in files:
os.remove(root + "/" + file)
def on_package(self, cmd: str, args: dict):
if cmd in {"Connected"}:
filename = f"AP_settings.json"
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
slot_data = args["slot_data"]
json.dump(args["slot_data"], f)
self.can_choose_commander = slot_data["can_choose_commander"]
print('can choose commander:', self.can_choose_commander)
self.starting_groove_multiplier = slot_data["starting_groove_multiplier"]
self.income_boost_multiplier = slot_data["income_boost"]
self.commander_defense_boost_multiplier = slot_data["commander_defense_boost"]
f.close()
for ss in self.checked_locations:
filename = f"send{ss}"
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
f.close()
self.update_commander_data()
self.ui.update_tracker()
random.seed(self.seed_name + str(self.slot))
# Our indexes start at 1 and we have 24 levels
for i in range(1, 25):
filename = f"seed{i}"
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
f.write(str(random.randint(0, 4294967295)))
f.close()
if cmd in {"RoomInfo"}:
self.seed_name = args["seed_name"]
if cmd in {"ReceivedItems"}:
received_ids = [item.item for item in self.items_received]
for network_item in self.items_received:
filename = f"AP_{str(network_item.item)}.item"
path = os.path.join(self.game_communication_path, filename)
# Newly-obtained items
if not os.path.isfile(path):
open(path, 'w').close()
# Announcing commander unlocks
item_name = self.item_names[network_item.item]
if item_name in faction_table.keys():
for commander in faction_table[item_name]:
logger.info(f"{commander.name} has been unlocked!")
with open(path, 'w') as f:
item_count = received_ids.count(network_item.item)
if self.buff_item_ids["Income Boost"] == network_item.item:
f.write(f"{item_count * self.income_boost_multiplier}")
elif self.buff_item_ids["Commander Defense Boost"] == network_item.item:
f.write(f"{item_count * self.commander_defense_boost_multiplier}")
else:
f.write(f"{item_count}")
f.close()
print_filename = f"AP_{str(network_item.item)}.item.print"
print_path = os.path.join(self.game_communication_path, print_filename)
if not os.path.isfile(print_path):
open(print_path, 'w').close()
with open(print_path, 'w') as f:
f.write("Received " +
self.item_names[network_item.item] +
" from " +
self.player_names[network_item.player])
f.close()
self.update_commander_data()
self.ui.update_tracker()
if cmd in {"RoomUpdate"}:
if "checked_locations" in args:
for ss in self.checked_locations:
filename = f"send{ss}"
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
f.close()
def run_gui(self):
"""Import kivy UI system and start running it as self.ui_task."""
from kvui import GameManager, HoverBehavior, ServerToolTip
from kivy.uix.tabbedpanel import TabbedPanelItem
from kivy.lang import Builder
from kivy.uix.button import Button
from kivy.uix.togglebutton import ToggleButton
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.gridlayout import GridLayout
from kivy.uix.image import AsyncImage, Image
from kivy.uix.stacklayout import StackLayout
from kivy.uix.label import Label
from kivy.properties import ColorProperty
from kivy.uix.image import Image
import pkgutil
class TrackerLayout(BoxLayout):
pass
class CommanderSelect(BoxLayout):
pass
class CommanderButton(ToggleButton):
pass
class FactionBox(BoxLayout):
pass
class CommanderGroup(BoxLayout):
pass
class ItemTracker(BoxLayout):
pass
class ItemLabel(Label):
pass
class WargrooveManager(GameManager):
logging_pairs = [
("Client", "Archipelago"),
("WG", "WG Console"),
]
base_title = "Archipelago Wargroove Client"
ctx: WargrooveContext
unit_tracker: ItemTracker
trigger_tracker: BoxLayout
boost_tracker: BoxLayout
commander_buttons: Dict[int, List[CommanderButton]]
tracker_items = {
"Swordsman": ItemData(None, "Unit", False),
"Dog": ItemData(None, "Unit", False),
**item_table
}
def build(self):
container = super().build()
panel = TabbedPanelItem(text="Wargroove")
panel.content = self.build_tracker()
self.tabs.add_widget(panel)
return container
def build_tracker(self) -> TrackerLayout:
try:
tracker = TrackerLayout(orientation="horizontal")
commander_select = CommanderSelect(orientation="vertical")
self.commander_buttons = {}
for faction, commanders in faction_table.items():
faction_box = FactionBox(size_hint=(None, None), width=100 * len(commanders), height=70)
commander_group = CommanderGroup()
commander_buttons = []
for commander in commanders:
commander_button = CommanderButton(text=commander.name, group="commanders")
if faction == "Starter":
commander_button.disabled = False
commander_button.bind(on_press=lambda instance: self.ctx.set_commander(instance.text))
commander_buttons.append(commander_button)
commander_group.add_widget(commander_button)
self.commander_buttons[faction] = commander_buttons
faction_box.add_widget(Label(text=faction, size_hint_x=None, pos_hint={'left': 1}, size_hint_y=None, height=10))
faction_box.add_widget(commander_group)
commander_select.add_widget(faction_box)
item_tracker = ItemTracker(padding=[0,20])
self.unit_tracker = BoxLayout(orientation="vertical")
other_tracker = BoxLayout(orientation="vertical")
self.trigger_tracker = BoxLayout(orientation="vertical")
self.boost_tracker = BoxLayout(orientation="vertical")
other_tracker.add_widget(self.trigger_tracker)
other_tracker.add_widget(self.boost_tracker)
item_tracker.add_widget(self.unit_tracker)
item_tracker.add_widget(other_tracker)
tracker.add_widget(commander_select)
tracker.add_widget(item_tracker)
self.update_tracker()
return tracker
except Exception as e:
print(e)
def update_tracker(self):
received_ids = [item.item for item in self.ctx.items_received]
for faction, item_id in self.ctx.faction_item_ids.items():
for commander_button in self.commander_buttons[faction]:
commander_button.disabled = not (faction == "Starter" or item_id in received_ids)
self.unit_tracker.clear_widgets()
self.trigger_tracker.clear_widgets()
for name, item in self.tracker_items.items():
if item.type in ("Unit", "Trigger"):
status_color = (1, 1, 1, 1) if item.code is None or item.code in received_ids else (0.6, 0.2, 0.2, 1)
label = ItemLabel(text=name, color=status_color)
if item.type == "Unit":
self.unit_tracker.add_widget(label)
else:
self.trigger_tracker.add_widget(label)
self.boost_tracker.clear_widgets()
extra_income = received_ids.count(52023) * self.ctx.income_boost_multiplier
extra_defense = received_ids.count(52024) * self.ctx.commander_defense_boost_multiplier
income_boost = ItemLabel(text="Extra Income: " + str(extra_income))
defense_boost = ItemLabel(text="Comm Defense: " + str(100 + extra_defense))
self.boost_tracker.add_widget(income_boost)
self.boost_tracker.add_widget(defense_boost)
self.ui = WargrooveManager(self)
data = pkgutil.get_data(WargrooveWorld.__module__, "Wargroove.kv").decode()
Builder.load_string(data)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
def update_commander_data(self):
if self.can_choose_commander:
faction_items = 0
faction_item_names = [faction + ' Commanders' for faction in faction_table.keys()]
for network_item in self.items_received:
if self.item_names[network_item.item] in faction_item_names:
faction_items += 1
starting_groove = (faction_items - 1) * self.starting_groove_multiplier
# Must be an integer larger than 0
starting_groove = int(max(starting_groove, 0))
data = {
"commander": self.current_commander.internal_name,
"starting_groove": starting_groove
}
else:
data = {
"commander": "seed",
"starting_groove": 0
}
filename = 'commander.json'
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
json.dump(data, f)
if self.ui:
self.ui.update_tracker()
def set_commander(self, commander_name: str) -> bool:
"""Sets the current commander to the given one, if possible"""
if not self.can_choose_commander:
wg_logger.error("Cannot set commanders in this game mode.")
return
match_name = commander_name.lower()
for commander, unlocked in self.get_commanders():
if commander.name.lower() == match_name or commander.alt_name and commander.alt_name.lower() == match_name:
if unlocked:
self.current_commander = commander
self.syncing = True
wg_logger.info(f"Commander set to {commander.name}.")
self.update_commander_data()
return True
else:
wg_logger.error(f"Commander {commander.name} has not been unlocked.")
return False
else:
wg_logger.error(f"{commander_name} is not a recognized Wargroove commander.")
def get_commanders(self) -> List[Tuple[CommanderData, bool]]:
"""Gets a list of commanders with their unlocked status"""
commanders = []
received_ids = [item.item for item in self.items_received]
for faction in faction_table.keys():
unlocked = faction == 'Starter' or self.faction_item_ids[faction] in received_ids
commanders += [(commander, unlocked) for commander in faction_table[faction]]
return commanders
async def game_watcher(ctx: WargrooveContext):
from worlds.wargroove.Locations import location_table
while not ctx.exit_event.is_set():
if ctx.syncing == True:
sync_msg = [{'cmd': 'Sync'}]
if ctx.locations_checked:
sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)})
await ctx.send_msgs(sync_msg)
ctx.syncing = False
sending = []
victory = False
for root, dirs, files in os.walk(ctx.game_communication_path):
for file in files:
if file.find("send") > -1:
st = file.split("send", -1)[1]
sending = sending+[(int(st))]
if file.find("victory") > -1:
victory = True
ctx.locations_checked = sending
message = [{"cmd": 'LocationChecks', "locations": sending}]
await ctx.send_msgs(message)
if not ctx.finished_game and victory:
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.finished_game = True
await asyncio.sleep(0.1)
def print_error_and_close(msg):
logger.error("Error: " + msg)
Utils.messagebox("Error", msg, error=True)
sys.exit(1)
if __name__ == '__main__':
async def main(args):
ctx = WargrooveContext(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
progression_watcher = asyncio.create_task(
game_watcher(ctx), name="WargrooveProgressionWatcher")
await ctx.exit_event.wait()
ctx.server_address = None
await progression_watcher
await ctx.shutdown()
import colorama
parser = get_base_parser(description="Wargroove Client, for text interfacing.")
args, rest = parser.parse_known_args()
colorama.init()
asyncio.run(main(args))
colorama.deinit()

View File

@@ -1,49 +1,136 @@
import os
import multiprocessing
import logging
import typing
import ModuleUpdate
ModuleUpdate.requirements_files.add(os.path.join("WebHostLib", "requirements.txt"))
ModuleUpdate.update()
# in case app gets imported by something like gunicorn
import Utils
Utils.local_path.cached_path = os.path.dirname(__file__)
from WebHostLib import app as raw_app
Utils.local_path.cached_path = os.path.dirname(__file__) or "." # py3.8 is not abs. remove "." when dropping 3.8
from WebHostLib import register, app as raw_app
from waitress import serve
from WebHostLib.models import db
from WebHostLib.autolauncher import autohost
from WebHostLib.autolauncher import autohost, autogen
from WebHostLib.lttpsprites import update_sprites_lttp
from WebHostLib.options import create as create_options_files
configpath = os.path.abspath("config.yaml")
if not os.path.exists(configpath): # fall back to config.yaml in home
configpath = os.path.abspath(Utils.user_path('config.yaml'))
def get_app():
register()
app = raw_app
if os.path.exists(configpath):
if os.path.exists(configpath) and not app.config["TESTING"]:
import yaml
app.config.from_file(configpath, yaml.safe_load)
logging.info(f"Updated config from {configpath}")
if not app.config["HOST_ADDRESS"]:
logging.info("Getting public IP, as HOST_ADDRESS is empty.")
app.config["HOST_ADDRESS"] = Utils.get_public_ipv4()
logging.info(f"HOST_ADDRESS was set to {app.config['HOST_ADDRESS']}")
db.bind(**app.config["PONY"])
db.generate_mapping(create_tables=True)
return app
def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]]:
import json
import shutil
import zipfile
zfile: zipfile.ZipInfo
from worlds.AutoWorld import AutoWorldRegister
worlds = {}
data = []
for game, world in AutoWorldRegister.world_types.items():
if hasattr(world.web, 'tutorials') and (not world.hidden or game == 'Archipelago'):
worlds[game] = world
base_target_path = Utils.local_path("WebHostLib", "static", "generated", "docs")
for game, world in worlds.items():
# copy files from world's docs folder to the generated folder
target_path = os.path.join(base_target_path, game)
os.makedirs(target_path, exist_ok=True)
if world.zip_path:
zipfile_path = world.zip_path
assert os.path.isfile(zipfile_path), f"{zipfile_path} is not a valid file(path)."
assert zipfile.is_zipfile(zipfile_path), f"{zipfile_path} is not a valid zipfile."
with zipfile.ZipFile(zipfile_path) as zf:
for zfile in zf.infolist():
if not zfile.is_dir() and "/docs/" in zfile.filename:
zf.extract(zfile, target_path)
else:
source_path = Utils.local_path(os.path.dirname(world.__file__), "docs")
files = os.listdir(source_path)
for file in files:
shutil.copyfile(Utils.local_path(source_path, file), Utils.local_path(target_path, file))
# build a json tutorial dict per game
game_data = {'gameTitle': game, 'tutorials': []}
for tutorial in world.web.tutorials:
# build dict for the json file
current_tutorial = {
'name': tutorial.tutorial_name,
'description': tutorial.description,
'files': [{
'language': tutorial.language,
'filename': game + '/' + tutorial.file_name,
'link': f'{game}/{tutorial.link}',
'authors': tutorial.authors
}]
}
# check if the name of the current guide exists already
for guide in game_data['tutorials']:
if guide and tutorial.tutorial_name == guide['name']:
guide['files'].append(current_tutorial['files'][0])
break
else:
game_data['tutorials'].append(current_tutorial)
data.append(game_data)
with open(Utils.local_path("WebHostLib", "static", "generated", "tutorials.json"), 'w', encoding='utf-8-sig') as json_target:
generic_data = {}
for games in data:
if 'Archipelago' in games['gameTitle']:
generic_data = data.pop(data.index(games))
sorted_data = [generic_data] + Utils.title_sorted(data, key=lambda entry: entry["gameTitle"])
json.dump(sorted_data, json_target, indent=2, ensure_ascii=False)
return sorted_data
if __name__ == "__main__":
multiprocessing.freeze_support()
multiprocessing.set_start_method('spawn')
logging.basicConfig(format='[%(asctime)s] %(message)s', level=logging.INFO)
update_sprites_lttp()
try:
update_sprites_lttp()
except Exception as e:
logging.exception(e)
logging.warning("Could not update LttP sprites.")
app = get_app()
create_options_files()
create_ordered_tutorials_file()
if app.config["SELFLAUNCH"]:
autohost(app.config)
if app.config["SELFGEN"]:
autogen(app.config)
if app.config["SELFHOST"]: # using WSGI, you just want to run get_app()
if app.config["DEBUG"]:
autohost(app.config)
app.run(debug=True, port=app.config["PORT"])
else:
serve(app, port=app.config["PORT"], threads=app.config["WAITRESS_THREADS"])

46
WebHostLib/README.md Normal file
View File

@@ -0,0 +1,46 @@
# WebHost
## Contribution Guidelines
**Thank you for your interest in contributing to the Archipelago website!**
Much of the content on the website is generated automatically, but there are some things
that need a personal touch. For those things, we rely on contributions from both the core
team and the community. The current primary maintainer of the website is Farrak Kilhn.
He may be found on Discord as `Farrak Kilhn#0418`, or on GitHub as `LegendaryLinux`.
### Small Changes
Little changes like adding a button or a couple new select elements are perfectly fine.
Tweaks to style specific to a PR's content are also probably not a problem. For example, if
you build a new page which needs two side by side tables, and you need to write a CSS file
specific to your page, that is perfectly reasonable.
### Content Additions
Once you develop a new feature or add new content the website, make a pull request. It will
be reviewed by the community and there will probably be some discussion around it. Depending
on the size of the feature, and if new styles are required, there may be an additional step
before the PR is accepted wherein Farrak works with the designer to implement styles.
### Restrictions on Style Changes
A professional designer is paid to develop the styles and assets for the Archipelago website.
In an effort to maintain a consistent look and feel, pull requests which *exclusively*
change site styles are rejected. Please note this applies to code which changes the overall
look and feel of the site, not to small tweaks to CSS for your custom page. The intention
behind these restrictions is to maintain a curated feel for the design of the site. If
any PR affects the overall feel of the site but includes additive changes, there will
likely be a conversation about how to implement those changes without compromising the
curated site style. It is therefore worth noting there are a couple files which, if
changed in your pull request, will cause it to draw additional scrutiny.
These closely guarded files are:
- `globalStyles.css`
- `islandFooter.css`
- `landing.css`
- `markdown.css`
- `tooltip.css`
### Site Themes
There are several themes available for game pages. It is possible to request a new theme in
the `#art-and-design` channel on Discord. Because themes are created by the designer, they
are not free, and take some time to create. Farrak works closely with the designer to implement
these themes, and pays for the assets out of pocket. Therefore, only a couple themes per year
are added. If a proposed theme seems like a cool idea and the community likes it, there is a
good chance it will become a reality.

View File

@@ -1,15 +1,15 @@
import os
import uuid
import base64
import os
import socket
import uuid
import jinja2.exceptions
from pony.flask import Pony
from flask import Flask, request, redirect, url_for, render_template, Response, session, abort, send_from_directory
from flask import Flask
from flask_caching import Cache
from flask_compress import Compress
from pony.flask import Pony
from werkzeug.routing import BaseConverter
from .models import *
from Utils import title_sorted
UPLOAD_FOLDER = os.path.relpath('uploads')
LOGS_FOLDER = os.path.relpath('logs')
@@ -21,17 +21,22 @@ Pony(app)
app.jinja_env.filters['any'] = any
app.jinja_env.filters['all'] = all
app.config["SELFHOST"] = True
app.config["SELFHOST"] = True # application process is in charge of running the websites
app.config["GENERATORS"] = 8 # maximum concurrent world gens
app.config["SELFLAUNCH"] = True
app.config["SELFLAUNCH"] = True # application process is in charge of launching Rooms.
app.config["SELFLAUNCHCERT"] = None # can point to a SSL Certificate to encrypt Room websocket connections
app.config["SELFLAUNCHKEY"] = None # can point to a SSL Certificate Key to encrypt Room websocket connections
app.config["SELFGEN"] = True # application process is in charge of scheduling Generations.
app.config["DEBUG"] = False
app.config["PORT"] = 80
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 4 * 1024 * 1024 # 4 megabyte limit
app.config['MAX_CONTENT_LENGTH'] = 64 * 1024 * 1024 # 64 megabyte limit
# if you want to deploy, make sure you have a non-guessable secret key
app.config["SECRET_KEY"] = bytes(socket.gethostname(), encoding="utf-8")
# at what amount of worlds should scheduling be used, instead of rolling in the webthread
# at what amount of worlds should scheduling be used, instead of rolling in the web-thread
app.config["JOB_THRESHOLD"] = 2
# after what time in seconds should generation be aborted, freeing the queue slot. Can be set to None to disable.
app.config["JOB_TIME"] = 600
app.config['SESSION_PERMANENT'] = True
# waitress uses one thread for I/O, these are for processing of views that then get sent
@@ -44,15 +49,13 @@ app.config["PONY"] = {
'create_db': True
}
app.config["MAX_ROLL"] = 20
app.config["CACHE_TYPE"] = "simple"
app.config["CACHE_TYPE"] = "flask_caching.backends.SimpleCache"
app.config["JSON_AS_ASCII"] = False
app.config["PATCH_TARGET"] = "archipelago.gg"
app.config["HOST_ADDRESS"] = ""
cache = Cache(app)
Compress(app)
from werkzeug.routing import BaseConverter
class B64UUIDConverter(BaseConverter):
@@ -66,149 +69,20 @@ class B64UUIDConverter(BaseConverter):
# short UUID
app.url_map.converters["suuid"] = B64UUIDConverter
app.jinja_env.filters['suuid'] = lambda value: base64.urlsafe_b64encode(value.bytes).rstrip(b'=').decode('ascii')
app.jinja_env.filters["title_sorted"] = title_sorted
@app.before_request
def register_session():
session.permanent = True # technically 31 days after the last visit
if not session.get("_id", None):
session["_id"] = uuid4() # uniquely identify each session without needing a login
def register():
"""Import submodules, triggering their registering on flask routing.
Note: initializes worlds subsystem."""
# has automatic patch integration
import worlds.AutoWorld
import worlds.Files
app.jinja_env.filters['supports_apdeltapatch'] = lambda game_name: \
game_name in worlds.Files.AutoPatchRegister.patch_types
from WebHostLib.customserver import run_server_process
# to trigger app routing picking up on it
from . import tracker, upload, landing, check, generate, downloads, api, stats, misc
@app.errorhandler(404)
@app.errorhandler(jinja2.exceptions.TemplateNotFound)
def page_not_found(err):
return render_template('404.html'), 404
games_list = {
"A Link to the Past": ("The Legend of Zelda: A Link to the Past",
"""
The Legend of Zelda: A Link to the Past is an action/adventure game. Take on the role of
Link, a boy who is destined to save the land of Hyrule. Delve through three palaces and nine
dungeons on your quest to rescue the descendents of the seven wise men and defeat the evil
Ganon!"""),
"Factorio": ("Factorio",
"""
Factorio is a game about automation. You play as an engineer who has crash landed on the planet
Nauvis, an inhospitable world filled with dangerous creatures called biters. Build a factory,
research new technologies, and become more efficient in your quest to build a rocket and return home.
"""),
"Minecraft": ("Minecraft",
"""
Minecraft is a game about creativity. In a world made entirely of cubes, you explore, discover, mine,
craft, and try not to explode. Delve deep into the earth and discover abandoned mines, ancient
structures, and materials to create a portal to another world. Defeat the Ender Dragon, and claim
victory!"""),
"Subnautica": ("Subnautica",
"""
Subnautica is an undersea exploration game. Stranded on an alien world, you become infected by
an unknown bacteria. The planet's automatic quarantine will shoot you down if you try to leave.
You must find a cure for yourself, build an escape rocket, and leave the planet.
"""),
}
# Player settings pages
@app.route('/games/<string:game>/player-settings')
def player_settings(game):
return render_template(f"player-settings.html", game=game)
# Game sub-pages
@app.route('/games/<string:game>/<string:page>')
def game_pages(game, page):
return render_template(f"/games/{game}/{page}.html")
# Game landing pages
@app.route('/games/<game>')
def game_page(game):
return render_template(f"/games/{game}/{game}.html")
# List of supported games
@app.route('/games')
def games():
return render_template("games/games.html", games_list=games_list)
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
def tutorial(game, file, lang):
return render_template("tutorial.html", game=game, file=file, lang=lang)
@app.route('/tutorial')
def tutorial_landing():
return render_template("tutorialLanding.html")
@app.route('/weighted-settings')
def weighted_settings():
return render_template("weightedSettings.html")
@app.route('/seed/<suuid:seed>')
def viewSeed(seed: UUID):
seed = Seed.get(id=seed)
if not seed:
abort(404)
return render_template("viewSeed.html", seed=seed,
rooms=[room for room in seed.rooms if room.owner == session["_id"]])
@app.route('/new_room/<suuid:seed>')
def new_room(seed: UUID):
seed = Seed.get(id=seed)
if not seed:
abort(404)
room = Room(seed=seed, owner=session["_id"], tracker=uuid4())
commit()
return redirect(url_for("hostRoom", room=room.id))
def _read_log(path: str):
if os.path.exists(path):
with open(path, encoding="utf-8-sig") as log:
yield from log
else:
yield f"Logfile {path} does not exist. " \
f"Likely a crash during spinup of multiworld instance or it is still spinning up."
@app.route('/log/<suuid:room>')
def display_log(room: UUID):
# noinspection PyTypeChecker
return Response(_read_log(os.path.join("logs", str(room) + ".txt")), mimetype="text/plain;charset=UTF-8")
@app.route('/room/<suuid:room>', methods=['GET', 'POST'])
def hostRoom(room: UUID):
room = Room.get(id=room)
if room is None:
return abort(404)
if request.method == "POST":
if room.owner == session["_id"]:
cmd = request.form["cmd"]
Command(room=room, commandtext=cmd)
commit()
with db_session:
room.last_activity = datetime.utcnow() # will trigger a spinup, if it's not already running
return render_template("hostRoom.html", room=room)
@app.route('/hosted/<suuid:room>', methods=['GET', 'POST'])
def hostRoomRedirect(room: UUID):
return redirect(url_for("hostRoom", room=room))
@app.route('/favicon.ico')
def favicon():
return send_from_directory(os.path.join(app.root_path, 'static/static'),
'favicon.ico', mimetype='image/vnd.microsoft.icon')
from WebHostLib.customserver import run_server_process
from . import tracker, upload, landing, check, generate, downloads, api # to trigger app routing picking up on it
app.register_blueprint(api.api_endpoints)
app.register_blueprint(api.api_endpoints)

View File

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

View File

@@ -1,14 +1,15 @@
import json
import pickle
from uuid import UUID
from . import api_endpoints
from flask import request, session, url_for
from flask import request, session, url_for, Markup
from pony.orm import commit
from WebHostLib import app, Generation, STATE_QUEUED, Seed, STATE_ERROR
from WebHostLib import app
from WebHostLib.check import get_yaml_data, roll_options
from WebHostLib.generate import get_meta
from WebHostLib.models import Generation, STATE_QUEUED, Seed, STATE_ERROR
from . import api_endpoints
@api_endpoints.route('/generate', methods=['POST'])
@@ -16,22 +17,30 @@ def generate_api():
try:
options = {}
race = False
meta_options_source = {}
if 'file' in request.files:
file = request.files['file']
options = get_yaml_data(file)
if type(options) == str:
if isinstance(options, Markup):
return {"text": options.striptags()}, 400
if isinstance(options, str):
return {"text": options}, 400
if "race" in request.form:
race = bool(0 if request.form["race"] in {"false"} else int(request.form["race"]))
meta_options_source = request.form
# json_data is optional, we can have it silently fall to None as it used to do.
# See https://flask.palletsprojects.com/en/2.2.x/api/#flask.Request.get_json -> Changelog -> 2.1
json_data = request.get_json(silent=True)
json_data = request.get_json()
if json_data:
meta_options_source = json_data
if 'weights' in json_data:
# example: options = {"player1weights" : {<weightsdata>}}
options = json_data["weights"]
if "race" in json_data:
race = bool(0 if json_data["race"] in {"false"} else int(json_data["race"]))
if not options:
return {"text": "No options found. Expected file attachment or json weights."
}, 400
@@ -39,8 +48,9 @@ def generate_api():
if len(options) > app.config["MAX_ROLL"]:
return {"text": "Max size of multiworld exceeded",
"detail": app.config["MAX_ROLL"]}, 409
results, gen_options = roll_options(options)
meta = get_meta(meta_options_source)
meta["race"] = race
results, gen_options = roll_options(options, meta["plando_options"])
if any(type(result) == str for result in results.values()):
return {"text": str(results),
"detail": results}, 400
@@ -48,7 +58,7 @@ def generate_api():
gen = Generation(
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
# convert to json compatible
meta=json.dumps({"race": race}), state=STATE_QUEUED,
meta=json.dumps(meta), state=STATE_QUEUED,
owner=session["_id"])
commit()
return {"text": f"Generation of seed {gen.id} started successfully.",
@@ -60,7 +70,6 @@ def generate_api():
return {"text": "Uncaught Exception:" + str(e)}, 500
@api_endpoints.route('/status/<suuid:seed>')
def wait_seed_api(seed: UUID):
seed_id = seed

View File

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

View File

@@ -1,13 +1,14 @@
from __future__ import annotations
import logging
import json
import logging
import multiprocessing
from datetime import timedelta, datetime
import concurrent.futures
import sys
import typing
import time
import os
import sys
import threading
import time
import typing
from datetime import timedelta, datetime
from pony.orm import db_session, select, commit
@@ -17,6 +18,7 @@ from Utils import restricted_loads
class CommonLocker():
"""Uses a file lock to signal that something is already running"""
lock_folder = "file_locks"
def __init__(self, lockname: str, folder=None):
if folder:
self.lock_folder = folder
@@ -53,7 +55,7 @@ else: # unix
def __enter__(self):
try:
self.fp = open(self.lockfile, "wb")
fcntl.flock(self.fp.fileno(), fcntl.LOCK_EX)
fcntl.flock(self.fp.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
except OSError as e:
raise AlreadyRunningException() from e
@@ -89,14 +91,14 @@ def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
options = restricted_loads(generation.options)
logging.info(f"Generating {generation.id} for {len(options)} players")
pool.apply_async(gen_game, (options,),
{"race": meta["race"],
{"meta": meta,
"sid": generation.id,
"owner": generation.owner},
handle_generation_success, handle_generation_failure)
except:
except Exception as e:
generation.state = STATE_ERROR
commit()
raise
logging.exception(e)
else:
generation.state = STATE_STARTED
@@ -110,6 +112,27 @@ def autohost(config: dict):
def keep_running():
try:
with Locker("autohost"):
run_guardian()
while 1:
time.sleep(0.1)
with db_session:
rooms = select(
room for room in Room if
room.last_activity >= datetime.utcnow() - timedelta(days=3))
for room in rooms:
launch_room(room, config)
except AlreadyRunningException:
logging.info("Autohost reports as already running, not starting another.")
import threading
threading.Thread(target=keep_running, name="AP_Autohost").start()
def autogen(config: dict):
def keep_running():
try:
with Locker("autogen"):
with multiprocessing.Pool(config["GENERATORS"], initializer=init_db,
initargs=(config["PONY"],)) as generator_pool:
@@ -129,58 +152,95 @@ def autohost(config: dict):
select(generation for generation in Generation if generation.state == STATE_ERROR).delete()
while 1:
time.sleep(0.50)
time.sleep(0.1)
with db_session:
rooms = select(
room for room in Room if
room.last_activity >= datetime.utcnow() - timedelta(days=3))
for room in rooms:
launch_room(room, config)
# for update locks the database row(s) during transaction, preventing writes from elsewhere
to_start = select(
generation for generation in Generation if generation.state == STATE_QUEUED)
generation for generation in Generation
if generation.state == STATE_QUEUED).for_update()
for generation in to_start:
launch_generator(generator_pool, generation)
except AlreadyRunningException:
pass
logging.info("Autogen reports as already running, not starting another.")
import threading
threading.Thread(target=keep_running).start()
threading.Thread(target=keep_running, name="AP_Autogen").start()
multiworlds = {}
guardians = concurrent.futures.ThreadPoolExecutor(2, thread_name_prefix="Guardian")
multiworlds: typing.Dict[type(Room.id), MultiworldInstance] = {}
class MultiworldInstance():
def __init__(self, room: Room, config: dict):
self.room_id = room.id
self.process: typing.Optional[multiprocessing.Process] = None
multiworlds[self.room_id] = self
with guardian_lock:
multiworlds[self.room_id] = self
self.ponyconfig = config["PONY"]
self.cert = config["SELFLAUNCHCERT"]
self.key = config["SELFLAUNCHKEY"]
self.host = config["HOST_ADDRESS"]
def start(self):
if self.process and self.process.is_alive():
return False
logging.info(f"Spinning up {self.room_id}")
self.process = multiprocessing.Process(group=None, target=run_server_process,
args=(self.room_id, self.ponyconfig),
name="MultiHost")
self.process.start()
self.guardian = guardians.submit(self._collect)
process = multiprocessing.Process(group=None, target=run_server_process,
args=(self.room_id, self.ponyconfig, get_static_server_data(),
self.cert, self.key, self.host),
name="MultiHost")
process.start()
# bind after start to prevent thread sync issues with guardian.
self.process = process
def stop(self):
if self.process:
self.process.terminate()
self.process = None
def _collect(self):
def done(self):
return self.process and not self.process.is_alive()
def collect(self):
self.process.join() # wait for process to finish
self.process = None
self.guardian = None
guardian = None
guardian_lock = threading.Lock()
def run_guardian():
global guardian
global multiworlds
with guardian_lock:
if not guardian:
try:
import resource
except ModuleNotFoundError:
pass # unix only module
else:
# Each Server is another file handle, so request as many as we can from the system
file_limit = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
# set soft limit to hard limit
resource.setrlimit(resource.RLIMIT_NOFILE, (file_limit, file_limit))
def guard():
while 1:
time.sleep(1)
done = []
with guardian_lock:
for key, instance in multiworlds.items():
if instance.done():
instance.collect()
done.append(key)
for key in done:
del (multiworlds[key])
guardian = threading.Thread(name="Guardian", target=guard)
from .models import Room, Generation, STATE_QUEUED, STATE_STARTED, STATE_ERROR, db, Seed
from .customserver import run_server_process
from .customserver import run_server_process, get_static_server_data
from .generate import gen_game

View File

@@ -1,7 +1,7 @@
import zipfile
from typing import *
from flask import request, flash, redirect, url_for, session, render_template
from flask import request, flash, redirect, url_for, render_template, Markup
from WebHostLib import app
@@ -12,12 +12,12 @@ def allowed_file(filename):
return filename.endswith(('.txt', ".yaml", ".zip"))
from Generate import roll_settings
from Utils import parse_yaml
from Generate import roll_settings, PlandoOptions
from Utils import parse_yamls
@app.route('/mysterycheck', methods=['GET', 'POST'])
def mysterycheck():
@app.route('/check', methods=['GET', 'POST'])
def check():
if request.method == 'POST':
# check if the post request has the file part
if 'file' not in request.files:
@@ -25,16 +25,20 @@ def mysterycheck():
else:
file = request.files['file']
options = get_yaml_data(file)
if type(options) == str:
if isinstance(options, str):
flash(options)
else:
results, _ = roll_options(options)
return render_template("checkResult.html", results=results)
return render_template("check.html")
def get_yaml_data(file) -> Union[Dict[str, str], str]:
@app.route('/mysterycheck')
def mysterycheck():
return redirect(url_for("check"), 301)
def get_yaml_data(file) -> Union[Dict[str, str], str, Markup]:
options = {}
# if user does not select file, browser also
# submit an empty part without filename
@@ -46,12 +50,14 @@ def get_yaml_data(file) -> Union[Dict[str, str], str]:
with zipfile.ZipFile(file, 'r') as zfile:
infolist = zfile.infolist()
if any(file.filename.endswith(".archipelago") for file in infolist):
return Markup("Error: Your .zip file contains an .archipelago file. "
'Did you mean to <a href="/uploads">host a game</a>?')
for file in infolist:
if file.filename.endswith(banned_zip_contents):
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. Your file was deleted."
elif file.filename.endswith(".yaml"):
options[file.filename] = zfile.open(file, "r").read()
elif file.filename.endswith(".txt"):
elif file.filename.endswith((".yaml", ".json", ".yml", ".txt")):
options[file.filename] = zfile.open(file, "r").read()
else:
options = {file.filename: file.read()}
@@ -60,20 +66,29 @@ def get_yaml_data(file) -> Union[Dict[str, str], str]:
return options
def roll_options(options: Dict[str, Union[dict, str]]) -> Tuple[Dict[str, Union[str, bool]], Dict[str, dict]]:
def roll_options(options: Dict[str, Union[dict, str]],
plando_options: Set[str] = frozenset({"bosses", "items", "connections", "texts"})) -> \
Tuple[Dict[str, Union[str, bool]], Dict[str, dict]]:
plando_options = PlandoOptions.from_set(set(plando_options))
results = {}
rolled_results = {}
for filename, text in options.items():
try:
if type(text) is dict:
yaml_data = text
yaml_datas = (text, )
else:
yaml_data = parse_yaml(text)
yaml_datas = tuple(parse_yamls(text))
except Exception as e:
results[filename] = f"Failed to parse YAML data in {filename}: {e}"
else:
try:
rolled_results[filename] = roll_settings(yaml_data, plando_options={"bosses"})
if len(yaml_datas) == 1:
rolled_results[filename] = roll_settings(yaml_datas[0],
plando_options=plando_options)
else:
for i, yaml_data in enumerate(yaml_datas):
rolled_results[f"{filename}/{i + 1}"] = roll_settings(yaml_data,
plando_options=plando_options)
except Exception as e:
results[filename] = f"Failed to generate mystery in {filename}: {e}"
else:

View File

@@ -1,25 +1,30 @@
from __future__ import annotations
import asyncio
import collections
import datetime
import functools
import logging
import os
import websockets
import asyncio
import pickle
import random
import socket
import threading
import time
import random
import pickle
import typing
import websockets
from pony.orm import commit, db_session, select
from .models import *
import Utils
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor
from Utils import get_public_ipv4, get_public_ipv6, restricted_loads
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor, load_server_cert
from Utils import get_public_ipv4, get_public_ipv6, restricted_loads, cache_argsless
from .models import Command, GameDataPackage, Room, db
class CustomClientMessageProcessor(ClientMessageProcessor):
ctx: WebHostContext
def _cmd_video(self, platform, user):
"""Set a link for your name in the WebHostLib tracker pointing to a video stream"""
if platform.lower().startswith("t"): # twitch
@@ -37,8 +42,9 @@ class CustomClientMessageProcessor(ClientMessageProcessor):
# inject
import MultiServer
MultiServer.client_message_processor = CustomClientMessageProcessor
del (MultiServer)
del MultiServer
class DBCommandProcessor(ServerCommandProcessor):
@@ -47,16 +53,27 @@ class DBCommandProcessor(ServerCommandProcessor):
class WebHostContext(Context):
def __init__(self):
super(WebHostContext, self).__init__("", 0, "", "", 1, 40, True, "enabled", "enabled", 0, 2)
room_id: int
def __init__(self, static_server_data: dict):
# static server data is used during _load_game_data to load required data,
# without needing to import worlds system, which takes quite a bit of memory
self.static_server_data = static_server_data
super(WebHostContext, self).__init__("", 0, "", "", 1, 40, True, "enabled", "enabled", "enabled", 0, 2)
del self.static_server_data
self.main_loop = asyncio.get_running_loop()
self.video = {}
self.tags = ["AP", "WebHost"]
def _load_game_data(self):
for key, value in self.static_server_data.items():
setattr(self, key, value)
self.non_hintable_names = collections.defaultdict(frozenset, self.non_hintable_names)
def listen_to_db_commands(self):
cmdprocessor = DBCommandProcessor(self)
while self.running:
while not self.exit_event.is_set():
with db_session:
commands = select(command for command in Command if command.room.id == self.room_id)
if commands:
@@ -75,7 +92,20 @@ class WebHostContext(Context):
else:
self.port = get_random_port()
return self._load(self._decompress(room.seed.multidata), True)
multidata = self.decompress(room.seed.multidata)
game_data_packages = {}
for game in list(multidata["datapackage"]):
game_data = multidata["datapackage"][game]
if "checksum" in game_data:
if self.gamespackage.get(game, {}).get("checksum") == game_data["checksum"]:
# non-custom. remove from multidata
# games package could be dropped from static data once all rooms embed data package
del multidata["datapackage"][game]
else:
data = Utils.restricted_loads(GameDataPackage.get(checksum=game_data["checksum"]).data)
game_data_packages[game] = data
return self._load(multidata, game_data_packages, True)
@db_session
def init_save(self, enabled: bool = True):
@@ -88,12 +118,12 @@ class WebHostContext(Context):
threading.Thread(target=self.listen_to_db_commands, daemon=True).start()
@db_session
def _save(self, exit_save:bool = False) -> bool:
def _save(self, exit_save: bool = False) -> bool:
room = Room.get(id=self.room_id)
room.multisave = pickle.dumps(self.get_save())
# saving only occurs on activity, so we can "abuse" this information to mark this as last_activity
if not exit_save: # we don't want to count a shutdown as activity, which would restart the server again
room.last_activity = datetime.utcnow()
if not exit_save: # we don't want to count a shutdown as activity, which would restart the server again
room.last_activity = datetime.datetime.utcnow()
return True
def get_save(self) -> dict:
@@ -101,44 +131,68 @@ class WebHostContext(Context):
d["video"] = [(tuple(playerslot), videodata) for playerslot, videodata in self.video.items()]
return d
def get_random_port():
return random.randint(49152, 65535)
def run_server_process(room_id, ponyconfig: dict):
@cache_argsless
def get_static_server_data() -> dict:
import worlds
data = {
"non_hintable_names": {},
"gamespackage": worlds.network_data_package["games"],
"item_name_groups": {world_name: world.item_name_groups for world_name, world in
worlds.AutoWorldRegister.world_types.items()},
"location_name_groups": {world_name: world.location_name_groups for world_name, world in
worlds.AutoWorldRegister.world_types.items()},
}
for world_name, world in worlds.AutoWorldRegister.world_types.items():
data["non_hintable_names"][world_name] = world.hint_blacklist
return data
def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str],
host: str):
# establish DB connection for multidata and multisave
db.bind(**ponyconfig)
db.generate_mapping(check_tables=False)
async def main():
logging.basicConfig(format='[%(asctime)s] %(message)s',
level=logging.INFO,
handlers=[
logging.FileHandler(os.path.join(LOGS_FOLDER, f"{room_id}.txt"), 'a', 'utf-8-sig')])
ctx = WebHostContext()
Utils.init_logging(str(room_id), write_mode="a")
ctx = WebHostContext(static_server_data)
ctx.load(room_id)
ctx.init_save()
ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None
try:
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, ctx.port, ping_timeout=None,
ping_interval=None)
ping_interval=None, ssl=ssl_context)
await ctx.server
except Exception: # likely port in use - in windows this is OSError, but I didn't check the others
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), ctx.host, 0, ping_timeout=None,
ping_interval=None)
ping_interval=None, ssl=ssl_context)
await ctx.server
port = 0
for wssocket in ctx.server.ws_server.sockets:
socketname = wssocket.getsockname()
if wssocket.family == socket.AF_INET6:
logging.info(f'Hosting game at [{get_public_ipv6()}]:{socketname[1]}')
with db_session:
room = Room.get(id=ctx.room_id)
room.last_port = socketname[1]
# Prefer IPv4, as most users seem to not have working ipv6 support
if not port:
port = socketname[1]
elif wssocket.family == socket.AF_INET:
logging.info(f'Hosting game at {get_public_ipv4()}:{socketname[1]}')
port = socketname[1]
if port:
logging.info(f'Hosting game at {host}:{port}')
with db_session:
room = Room.get(id=ctx.room_id)
room.last_port = port
else:
logging.exception("Could not determine port. Likely hosting failure.")
with db_session:
ctx.auto_shutdown = Room.get(id=room_id).timeout
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, []))
@@ -147,7 +201,17 @@ def run_server_process(room_id, ponyconfig: dict):
from .autolauncher import Locker
with Locker(room_id):
asyncio.run(main())
from WebHostLib import LOGS_FOLDER
try:
asyncio.run(main())
except KeyboardInterrupt:
with db_session:
room = Room.get(id=room_id)
# ensure the Room does not spin up again on its own, minute of safety buffer
room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(minutes=1, seconds=room.timeout)
except:
with db_session:
room = Room.get(id=room_id)
room.last_port = -1
# ensure the Room does not spin up again on its own, minute of safety buffer
room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(minutes=1, seconds=room.timeout)
raise

View File

@@ -1,9 +1,14 @@
from flask import send_file, Response
import json
import zipfile
from io import BytesIO
from flask import send_file, Response, render_template
from pony.orm import select
from Patch import update_patch_data
from WebHostLib import app, Slot, Room, Seed
import zipfile
from worlds.Files import AutoPatchRegister
from . import app, cache
from .models import Slot, Room, Seed
@app.route("/dl_patch/<suuid:room_id>/<int:patch_id>")
def download_patch(room_id, patch_id):
@@ -11,16 +16,33 @@ def download_patch(room_id, patch_id):
if not patch:
return "Patch not found"
else:
import io
room = Room.get(id=room_id)
last_port = room.last_port
patch_data = update_patch_data(patch.data, server=f"{app.config['PATCH_TARGET']}:{last_port}")
patch_data = io.BytesIO(patch_data)
fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}.apbp"
return send_file(patch_data, as_attachment=True, attachment_filename=fname)
filelike = BytesIO(patch.data)
greater_than_version_3 = zipfile.is_zipfile(filelike)
if greater_than_version_3:
# Python's zipfile module cannot overwrite/delete files in a zip, so we recreate the whole thing in ram
new_file = BytesIO()
with zipfile.ZipFile(filelike, "a") as zf:
with zf.open("archipelago.json", "r") as f:
manifest = json.load(f)
manifest["server"] = f"{app.config['HOST_ADDRESS']}:{last_port}" if last_port else None
with zipfile.ZipFile(new_file, "w") as new_zip:
for file in zf.infolist():
if file.filename == "archipelago.json":
new_zip.writestr("archipelago.json", json.dumps(manifest))
else:
new_zip.writestr(file.filename, zf.read(file), file.compress_type, 9)
if "patch_file_ending" in manifest:
patch_file_ending = manifest["patch_file_ending"]
else:
patch_file_ending = AutoPatchRegister.patch_types[patch.game].patch_file_ending
fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}" \
f"{patch_file_ending}"
new_file.seek(0)
return send_file(new_file, as_attachment=True, download_name=fname)
else:
return "Old Patch file, no longer compatible."
@app.route("/dl_spoiler/<suuid:seed_id>")
@@ -28,28 +50,11 @@ def download_spoiler(seed_id):
return Response(Seed.get(id=seed_id).spoiler, mimetype="text/plain")
@app.route("/dl_raw_patch/<suuid:seed_id>/<int:player_id>")
def download_raw_patch(seed_id, player_id: int):
seed = Seed.get(id=seed_id)
patch = select(patch for patch in seed.slots if
patch.player_id == player_id).first()
if not patch:
return "Patch not found"
else:
import io
patch_data = update_patch_data(patch.data, server="")
patch_data = io.BytesIO(patch_data)
fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](seed_id)}.apbp"
return send_file(patch_data, as_attachment=True, attachment_filename=fname)
@app.route("/slot_file/<suuid:room_id>/<int:player_id>")
def download_slot_file(room_id, player_id: int):
room = Room.get(id=room_id)
slot_data: Slot = select(patch for patch in room.seed.slots if
patch.player_id == player_id).first()
patch.player_id == player_id).first()
if not slot_data:
return "Slot Data not found"
@@ -59,13 +64,43 @@ def download_slot_file(room_id, player_id: int):
if slot_data.game == "Minecraft":
from worlds.minecraft import mc_update_output
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apmc"
data = mc_update_output(slot_data.data, server=app.config['PATCH_TARGET'], port=room.last_port)
return send_file(io.BytesIO(data), as_attachment=True, attachment_filename=fname)
data = mc_update_output(slot_data.data, server=app.config['HOST_ADDRESS'], port=room.last_port)
return send_file(io.BytesIO(data), as_attachment=True, download_name=fname)
elif slot_data.game == "Factorio":
with zipfile.ZipFile(io.BytesIO(slot_data.data)) as zf:
for name in zf.namelist():
if name.endswith("info.json"):
fname = name.rsplit("/", 1)[0]+".zip"
fname = name.rsplit("/", 1)[0] + ".zip"
elif slot_data.game == "Ocarina of Time":
stream = io.BytesIO(slot_data.data)
if zipfile.is_zipfile(stream):
with zipfile.ZipFile(stream) as zf:
for name in zf.namelist():
if name.endswith(".zpf"):
fname = name.rsplit(".", 1)[0] + ".apz5"
else: # pre-ootr-7.0 support
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apz5"
elif slot_data.game == "VVVVVV":
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_SP.apv6"
elif slot_data.game == "Zillion":
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_SP.apzl"
elif slot_data.game == "Super Mario 64":
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_SP.apsm64ex"
elif slot_data.game == "Dark Souls III":
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}.json"
elif slot_data.game == "Kingdom Hearts 2":
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.zip"
else:
return "Game download not supported."
return send_file(io.BytesIO(slot_data.data), as_attachment=True, attachment_filename=fname)
return send_file(io.BytesIO(slot_data.data), as_attachment=True, download_name=fname)
@app.route("/templates")
@cache.cached()
def list_yaml_templates():
files = []
from worlds.AutoWorld import AutoWorldRegister
for world_name, world in AutoWorldRegister.world_types.items():
if not world.hidden:
files.append(world_name)
return render_template("templates.html", files=files)

View File

@@ -1,20 +1,45 @@
import os
import tempfile
import random
import json
import os
import pickle
import random
import tempfile
import zipfile
import concurrent.futures
from collections import Counter
from typing import Dict, Optional, Any
from flask import request, flash, redirect, url_for, session, render_template
from pony.orm import commit, db_session
from worlds.alttp.EntranceRandomizer import parse_arguments
from BaseClasses import seeddigits, get_seed
from Generate import handle_name, PlandoOptions
from Main import main as ERmain
from Main import get_seed, seeddigits
from Generate import handle_name
import pickle
from .models import *
from Utils import __version__
from WebHostLib import app
from worlds.alttp.EntranceRandomizer import parse_arguments
from .check import get_yaml_data, roll_options
from .models import Generation, STATE_ERROR, STATE_QUEUED, Seed, UUID
from .upload import upload_zip_to_db
def get_meta(options_source: dict) -> dict:
plando_options = {
options_source.get("plando_bosses", ""),
options_source.get("plando_items", ""),
options_source.get("plando_connections", ""),
options_source.get("plando_texts", "")
}
plando_options -= {""}
server_options = {
"hint_cost": int(options_source.get("hint_cost", 10)),
"release_mode": options_source.get("release_mode", "goal"),
"remaining_mode": options_source.get("remaining_mode", "disabled"),
"collect_mode": options_source.get("collect_mode", "disabled"),
"item_cheat": bool(int(options_source.get("item_cheat", 1))),
"server_password": options_source.get("server_password", None),
}
return {"server_options": server_options, "plando_options": list(plando_options)}
@app.route('/generate', methods=['GET', 'POST'])
@@ -27,20 +52,28 @@ def generate(race=False):
else:
file = request.files['file']
options = get_yaml_data(file)
if type(options) == str:
if isinstance(options, str):
flash(options)
else:
results, gen_options = roll_options(options)
meta = get_meta(request.form)
meta["race"] = race
results, gen_options = roll_options(options, meta["plando_options"])
if race:
meta["server_options"]["item_cheat"] = False
meta["server_options"]["remaining_mode"] = "disabled"
if any(type(result) == str for result in results.values()):
return render_template("checkResult.html", results=results)
elif len(gen_options) > app.config["MAX_ROLL"]:
flash(f"Sorry, generating of multiworlds is limited to {app.config['MAX_ROLL']} players for now. "
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.")
elif len(gen_options) >= app.config["JOB_THRESHOLD"]:
gen = Generation(
options=pickle.dumps({name: vars(options) for name, options in gen_options.items()}),
# convert to json compatible
meta=json.dumps({"race": race}), state=STATE_QUEUED,
meta=json.dumps(meta),
state=STATE_QUEUED,
owner=session["_id"])
commit()
@@ -48,52 +81,81 @@ def generate(race=False):
else:
try:
seed_id = gen_game({name: vars(options) for name, options in gen_options.items()},
race=race, owner=session["_id"].int)
meta=meta, owner=session["_id"].int)
except BaseException as e:
from .autolauncher import handle_generation_failure
handle_generation_failure(e)
return render_template("seedError.html", seed_error=(e.__class__.__name__ + ": "+ str(e)))
return render_template("seedError.html", seed_error=(e.__class__.__name__ + ": " + str(e)))
return redirect(url_for("viewSeed", seed=seed_id))
return redirect(url_for("view_seed", seed=seed_id))
return render_template("generate.html", race=race)
return render_template("generate.html", race=race, version=__version__)
def gen_game(gen_options, race=False, owner=None, sid=None):
try:
def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=None, sid=None):
if not meta:
meta: Dict[str, Any] = {}
meta.setdefault("server_options", {}).setdefault("hint_cost", 10)
race = meta.setdefault("race", False)
def task():
target = tempfile.TemporaryDirectory()
playercount = len(gen_options)
seed = get_seed()
random.seed(seed)
if race:
random.seed() # reset to time-based random source
random.seed() # use time-based random source
else:
random.seed(seed)
seedname = "M" + (f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits))
seedname = "W" + (f"{random.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits))
erargs = parse_arguments(['--multi', str(playercount)])
erargs.seed = seed
erargs.name = {x: "" for x in range(1, playercount + 1)} # only so it can be overwrittin in mystery
erargs.create_spoiler = not race
erargs.name = {x: "" for x in range(1, playercount + 1)} # only so it can be overwritten in mystery
erargs.spoiler = 0 if race else 3
erargs.race = race
erargs.skip_playthrough = race
erargs.outputname = seedname
erargs.outputpath = target.name
erargs.teams = 1
erargs.plando_options = PlandoOptions.from_set(meta.setdefault("plando_options",
{"bosses", "items", "connections", "texts"}))
name_counter = Counter()
for player, (playerfile, settings) in enumerate(gen_options.items(), 1):
for k, v in settings.items():
if v is not None:
getattr(erargs, k)[player] = v
if hasattr(erargs, k):
getattr(erargs, k)[player] = v
else:
setattr(erargs, k, {player: v})
if not erargs.name[player]:
erargs.name[player] = os.path.splitext(os.path.split(playerfile)[-1])[0]
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
if len(set(erargs.name.values())) != len(erargs.name):
raise Exception(f"Names have to be unique. Names: {Counter(erargs.name.values())}")
ERmain(erargs, seed, baked_server_options=meta["server_options"])
ERmain(erargs, seed)
return upload_to_db(target.name, sid, owner, race)
thread_pool = concurrent.futures.ThreadPoolExecutor(max_workers=1)
thread = thread_pool.submit(task)
return upload_to_db(target.name, owner, sid, race)
try:
return thread.result(app.config["JOB_TIME"])
except concurrent.futures.TimeoutError as e:
if sid:
with db_session:
gen = Generation.get(id=sid)
if gen is not None:
gen.state = STATE_ERROR
meta = json.loads(gen.meta)
meta["error"] = (
"Allowed time for Generation exceeded, please consider generating locally instead. " +
e.__class__.__name__ + ": " + str(e))
gen.meta = json.dumps(meta)
commit()
except BaseException as e:
if sid:
with db_session:
@@ -101,9 +163,8 @@ def gen_game(gen_options, race=False, owner=None, sid=None):
if gen is not None:
gen.state = STATE_ERROR
meta = json.loads(gen.meta)
meta["error"] = (e.__class__.__name__ + ": "+ str(e))
meta["error"] = (e.__class__.__name__ + ": " + str(e))
gen.meta = json.dumps(meta)
commit()
raise
@@ -113,7 +174,7 @@ def wait_seed(seed: UUID):
seed_id = seed
seed = Seed.get(id=seed_id)
if seed:
return redirect(url_for("viewSeed", seed=seed_id))
return redirect(url_for("view_seed", seed=seed_id))
generation = Generation.get(id=seed_id)
if not generation:
@@ -123,37 +184,19 @@ def wait_seed(seed: UUID):
return render_template("waitSeed.html", seed_id=seed_id)
def upload_to_db(folder, owner, sid, race:bool):
slots = set()
spoiler = ""
multidata = None
def upload_to_db(folder, sid, owner, race):
for file in os.listdir(folder):
file = os.path.join(folder, file)
if file.endswith(".apbp"):
player_text = file.split("_P", 1)[1]
player_name = player_text.split("_", 1)[1].split(".", 1)[0]
player_id = int(player_text.split(".", 1)[0].split("_", 1)[0])
slots.add(Slot(data=open(file, "rb").read(),
player_id=player_id, player_name = player_name, game = "A Link to the Past"))
elif file.endswith(".txt"):
spoiler = open(file, "rt", encoding="utf-8-sig").read()
elif file.endswith(".archipelago"):
multidata = open(file, "rb").read()
if multidata:
with db_session:
if sid:
seed = Seed(multidata=multidata, spoiler=spoiler, slots=slots, owner=owner,
id=sid, meta=json.dumps({"race": race, "tags": ["generated"]}))
else:
seed = Seed(multidata=multidata, spoiler=spoiler, slots=slots, owner=owner,
meta=json.dumps({"race": race, "tags": ["generated"]}))
for patch in slots:
patch.seed = seed
if sid:
gen = Generation.get(id=sid)
if gen is not None:
gen.delete()
return seed.id
else:
raise Exception("Multidata required (.archipelago), but not found.")
if file.endswith(".zip"):
with db_session:
with zipfile.ZipFile(file) as zfile:
res = upload_zip_to_db(zfile, owner, {"race": race}, sid)
if type(res) == "str":
raise Exception(res)
elif res:
seed = res
gen = Generation.get(id=seed.id)
if gen is not None:
gen.delete()
return seed.id
raise Exception("Generation zipfile not found.")

View File

@@ -1,7 +1,11 @@
from datetime import timedelta, datetime
from flask import render_template
from pony.orm import count
from WebHostLib import app, cache
from .models import *
from datetime import timedelta
from .models import Room, Seed
@app.route('/', methods=['GET', 'POST'])
@cache.cached(timeout=300) # cache has to appear under app route for caching to work

View File

@@ -2,7 +2,7 @@ import os
import threading
import json
from Utils import local_path
from Utils import local_path, user_path
from worlds.alttp.Rom import Sprite
@@ -10,24 +10,29 @@ def update_sprites_lttp():
from tkinter import Tk
from LttPAdjuster import get_image_for_sprite
from LttPAdjuster import BackgroundTaskProgress
from LttPAdjuster import BackgroundTaskProgressNullWindow
from LttPAdjuster import update_sprites
# Target directories
input_dir = local_path("data", "sprites", "alttpr")
output_dir = local_path("WebHostLib", "static", "generated")
input_dir = user_path("data", "sprites", "alttpr")
output_dir = local_path("WebHostLib", "static", "generated") # TODO: move to user_path
os.makedirs(os.path.join(output_dir, "sprites"), exist_ok=True)
# update sprites through gui.py's functions
done = threading.Event()
top = Tk()
top.withdraw()
BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
try:
top = Tk()
except:
task = BackgroundTaskProgressNullWindow(update_sprites, lambda successful, resultmessage: done.set())
else:
top.withdraw()
task = BackgroundTaskProgress(top, update_sprites, "Updating Sprites", lambda succesful, resultmessage: done.set())
while not done.isSet():
top.update()
task.do_events()
spriteData = []
for file in os.listdir(input_dir):
for file in (file for file in os.listdir(input_dir) if not file.startswith(".")):
sprite = Sprite(os.path.join(input_dir, file))
if not sprite.name:

172
WebHostLib/misc.py Normal file
View File

@@ -0,0 +1,172 @@
import datetime
import os
from typing import List, Dict, Union
import jinja2.exceptions
from flask import request, redirect, url_for, render_template, Response, session, abort, send_from_directory
from pony.orm import count, commit, db_session
from worlds.AutoWorld import AutoWorldRegister
from . import app, cache
from .models import Seed, Room, Command, UUID, uuid4
def get_world_theme(game_name: str):
if game_name in AutoWorldRegister.world_types:
return AutoWorldRegister.world_types[game_name].web.theme
return 'grass'
@app.before_request
def register_session():
session.permanent = True # technically 31 days after the last visit
if not session.get("_id", None):
session["_id"] = uuid4() # uniquely identify each session without needing a login
@app.errorhandler(404)
@app.errorhandler(jinja2.exceptions.TemplateNotFound)
def page_not_found(err):
return render_template('404.html'), 404
# Start Playing Page
@app.route('/start-playing')
def start_playing():
return render_template(f"startPlaying.html")
@app.route('/weighted-settings')
def weighted_settings():
return render_template(f"weighted-settings.html")
# Player settings pages
@app.route('/games/<string:game>/player-settings')
def player_settings(game):
return render_template(f"player-settings.html", game=game, theme=get_world_theme(game))
# Game Info Pages
@app.route('/games/<string:game>/info/<string:lang>')
def game_info(game, lang):
return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game))
# List of supported games
@app.route('/games')
def games():
worlds = {}
for game, world in AutoWorldRegister.world_types.items():
if not world.hidden:
worlds[game] = world
return render_template("supportedGames.html", worlds=worlds)
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
def tutorial(game, file, lang):
return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game))
@app.route('/tutorial/')
def tutorial_landing():
return render_template("tutorialLanding.html")
@app.route('/faq/<string:lang>/')
def faq(lang):
return render_template("faq.html", lang=lang)
@app.route('/glossary/<string:lang>/')
def terms(lang):
return render_template("glossary.html", lang=lang)
@app.route('/seed/<suuid:seed>')
def view_seed(seed: UUID):
seed = Seed.get(id=seed)
if not seed:
abort(404)
return render_template("viewSeed.html", seed=seed, slot_count=count(seed.slots))
@app.route('/new_room/<suuid:seed>')
def new_room(seed: UUID):
seed = Seed.get(id=seed)
if not seed:
abort(404)
room = Room(seed=seed, owner=session["_id"], tracker=uuid4())
commit()
return redirect(url_for("host_room", room=room.id))
def _read_log(path: str):
if os.path.exists(path):
with open(path, encoding="utf-8-sig") as log:
yield from log
else:
yield f"Logfile {path} does not exist. " \
f"Likely a crash during spinup of multiworld instance or it is still spinning up."
@app.route('/log/<suuid:room>')
def display_log(room: UUID):
room = Room.get(id=room)
if room is None:
return abort(404)
if room.owner == session["_id"]:
return Response(_read_log(os.path.join("logs", str(room.id) + ".txt")), mimetype="text/plain;charset=UTF-8")
return "Access Denied", 403
@app.route('/room/<suuid:room>', methods=['GET', 'POST'])
def host_room(room: UUID):
room: Room = Room.get(id=room)
if room is None:
return abort(404)
if request.method == "POST":
if room.owner == session["_id"]:
cmd = request.form["cmd"]
if cmd:
Command(room=room, commandtext=cmd)
commit()
now = datetime.datetime.utcnow()
# 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)
with db_session:
room.last_activity = now # will trigger a spinup, if it's not already running
return render_template("hostRoom.html", room=room, should_refresh=should_refresh)
@app.route('/favicon.ico')
def favicon():
return send_from_directory(os.path.join(app.root_path, 'static/static'),
'favicon.ico', mimetype='image/vnd.microsoft.icon')
@app.route('/discord')
def discord():
return redirect("https://discord.gg/8Z65BR2")
@app.route('/datapackage')
@cache.cached()
def get_datapackage():
"""A pretty print version of /api/datapackage"""
from worlds import network_data_package
import json
return Response(json.dumps(network_data_package, indent=4), mimetype="text/plain")
@app.route('/index')
@app.route('/sitemap')
def get_sitemap():
available_games: List[Dict[str, Union[str, bool]]] = []
for game, world in AutoWorldRegister.world_types.items():
if not world.hidden:
has_settings: bool = isinstance(world.web.settings_page, bool) and world.web.settings_page
available_games.append({ 'title': game, 'has_settings': has_settings })
return render_template("siteMap.html", games=available_games)

View File

@@ -1,6 +1,6 @@
from datetime import datetime
from uuid import UUID, uuid4
from pony.orm import *
from pony.orm import Database, PrimaryKey, Required, Set, Optional, buffer, LongStr
db = Database()
@@ -12,7 +12,7 @@ STATE_ERROR = -1
class Slot(db.Entity):
id = PrimaryKey(int, auto=True)
player_id = Required(int)
player_name = Required(str, 16)
player_name = Required(str)
data = Optional(bytes, lazy=True)
seed = Optional('Seed')
game = Required(str)
@@ -27,8 +27,9 @@ class Room(db.Entity):
seed = Required('Seed', index=True)
multisave = Optional(buffer, lazy=True)
show_spoiler = Required(int, default=0) # 0 -> never, 1 -> after completion, -> 2 always
timeout = Required(int, default=lambda: 6 * 60 * 60) # seconds since last activity to shutdown
timeout = Required(int, default=lambda: 2 * 60 * 60) # seconds since last activity to shutdown
tracker = Optional(UUID, index=True)
# Port special value -1 means the server errored out. Another attempt can be made with a page refresh
last_port = Optional(int, default=lambda: 0)
@@ -40,7 +41,7 @@ class Seed(db.Entity):
creation_time = Required(datetime, default=lambda: datetime.utcnow())
slots = Set(Slot)
spoiler = Optional(LongStr, lazy=True)
meta = Required(str, default=lambda: "{\"race\": false}") # additional meta information/tags
meta = Required(LongStr, default=lambda: "{\"race\": false}") # additional meta information/tags
class Command(db.Entity):
@@ -53,5 +54,10 @@ class Generation(db.Entity):
id = PrimaryKey(UUID, default=uuid4)
owner = Required(UUID)
options = Required(buffer, lazy=True)
meta = Required(str, default=lambda: "{\"race\": false}")
meta = Required(LongStr, default=lambda: "{\"race\": false}")
state = Required(int, default=0, index=True)
class GameDataPackage(db.Entity):
checksum = PrimaryKey(str)
data = Required(bytes)

View File

@@ -1,64 +1,178 @@
import os
from Utils import __version__
from jinja2 import Template
import yaml
import json
import logging
import os
import typing
import yaml
from jinja2 import Template
import Options
from Utils import __version__, local_path
from worlds.AutoWorld import AutoWorldRegister
target_folder = os.path.join("WebHostLib", "static", "generated")
handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hints", "start_location_hints",
"exclude_locations", "priority_locations"}
def create():
target_folder = local_path("WebHostLib", "static", "generated")
yaml_folder = os.path.join(target_folder, "configs")
os.makedirs(yaml_folder, exist_ok=True)
for file in os.listdir(yaml_folder):
full_path: str = os.path.join(yaml_folder, file)
if os.path.isfile(full_path):
os.unlink(full_path)
def dictify_range(option: typing.Union[Options.Range, Options.SpecialRange]):
data = {option.default: 50}
for sub_option in ["random", "random-low", "random-high"]:
if sub_option != option.default:
data[sub_option] = 0
notes = {}
for name, number in getattr(option, "special_range_names", {}).items():
notes[name] = f"equivalent to {number}"
if number in data:
data[name] = data[number]
del data[number]
else:
data[name] = 0
return data, notes
def get_html_doc(option_type: type(Options.Option)) -> str:
if not option_type.__doc__:
return "Please document me!"
return "\n".join(line.strip() for line in option_type.__doc__.split("\n")).strip()
weighted_settings = {
"baseOptions": {
"description": "Generated by https://archipelago.gg/",
"name": "Player",
"game": {},
},
"games": {},
}
for game_name, world in AutoWorldRegister.world_types.items():
res = Template(open(os.path.join("WebHostLib", "templates", "options.yaml")).read()).render(
options=world.options, __version__=__version__, game=game_name, yaml_dump=yaml.dump
all_options: typing.Dict[str, Options.AssembleOptions] = {
**Options.per_game_common_options,
**world.option_definitions
}
with open(local_path("WebHostLib", "templates", "options.yaml")) as f:
file_data = f.read()
res = Template(file_data).render(
options=all_options,
__version__=__version__, game=game_name, yaml_dump=yaml.dump,
dictify_range=dictify_range,
)
with open(os.path.join(target_folder, game_name + ".yaml"), "w") as f:
del file_data
with open(os.path.join(target_folder, "configs", game_name + ".yaml"), "w", encoding="utf-8") as f:
f.write(res)
# Generate JSON files for player-settings pages
player_settings = {
"baseOptions": {
"description": "Generated by https://archipelago.gg/",
"game": game_name,
"name": "Player",
},
}
game_options = {}
for option_name, option in world.options.items():
if option.options:
this_option = {
for option_name, option in all_options.items():
if option_name in handled_in_js:
pass
elif issubclass(option, Options.Choice) or issubclass(option, Options.Toggle):
game_options[option_name] = this_option = {
"type": "select",
"friendlyName": option.friendly_name if hasattr(option, "friendly_name") else option_name,
"description": option.__doc__ if option.__doc__ else "Please document me!",
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
"description": get_html_doc(option),
"defaultValue": None,
"options": []
}
for sub_option_name, sub_option_id in option.options.items():
this_option["options"].append({
"name": sub_option_name,
"value": sub_option_name,
})
for sub_option_id, sub_option_name in option.name_lookup.items():
if sub_option_name != "random":
this_option["options"].append({
"name": option.get_option_name(sub_option_id),
"value": sub_option_name,
})
if sub_option_id == option.default:
this_option["defaultValue"] = sub_option_name
game_options[option_name] = this_option
if not this_option["defaultValue"]:
this_option["defaultValue"] = "random"
elif hasattr(option, "range_start") and hasattr(option, "range_end"):
elif issubclass(option, Options.Range):
game_options[option_name] = {
"type": "range",
"friendlyName": option.friendly_name if hasattr(option, "friendly_name") else option_name,
"description": option.__doc__ if option.__doc__ else "Please document me!",
"defaultValue": option.default if hasattr(option, "default") else option.range_start,
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
"description": get_html_doc(option),
"defaultValue": option.default if hasattr(
option, "default") and option.default != "random" else option.range_start,
"min": option.range_start,
"max": option.range_end,
}
if issubclass(option, Options.SpecialRange):
game_options[option_name]["type"] = 'special_range'
game_options[option_name]["value_names"] = {}
for key, val in option.special_range_names.items():
game_options[option_name]["value_names"][key] = val
elif issubclass(option, Options.ItemSet):
game_options[option_name] = {
"type": "items-list",
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
"description": get_html_doc(option),
}
elif issubclass(option, Options.LocationSet):
game_options[option_name] = {
"type": "locations-list",
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
"description": get_html_doc(option),
}
elif issubclass(option, Options.VerifyKeys):
if option.valid_keys:
game_options[option_name] = {
"type": "custom-list",
"displayName": option.display_name if hasattr(option, "display_name") else option_name,
"description": get_html_doc(option),
"options": list(option.valid_keys),
}
else:
logging.debug(f"{option} not exported to Web Settings.")
player_settings["gameOptions"] = game_options
with open(os.path.join(target_folder, game_name + ".json"), "w") as f:
f.write(json.dumps(player_settings, indent=2, separators=(',', ': ')))
os.makedirs(os.path.join(target_folder, 'player-settings'), exist_ok=True)
with open(os.path.join(target_folder, 'player-settings', game_name + ".json"), "w") as f:
json.dump(player_settings, f, indent=2, separators=(',', ': '))
if not world.hidden and world.web.settings_page is True:
# Add the random option to Choice, TextChoice, and Toggle settings
for option in game_options.values():
if option["type"] == "select":
option["options"].append({"name": "Random", "value": "random"})
if not option["defaultValue"]:
option["defaultValue"] = "random"
weighted_settings["baseOptions"]["game"][game_name] = 0
weighted_settings["games"][game_name] = {}
weighted_settings["games"][game_name]["gameSettings"] = game_options
weighted_settings["games"][game_name]["gameItems"] = tuple(world.item_names)
weighted_settings["games"][game_name]["gameLocations"] = tuple(world.location_names)
with open(os.path.join(target_folder, 'weighted-settings.json'), "w") as f:
json.dump(weighted_settings, f, indent=2, separators=(',', ': '))

View File

@@ -1,6 +1,7 @@
flask>=2.0.1
pony>=0.7.14
waitress>=2.0.0
flask-caching>=1.10.1
Flask-Compress>=1.10.1
Flask-Limiter>=1.4
flask>=2.2.3
pony>=0.7.16
waitress>=2.1.2
Flask-Caching>=2.0.2
Flask-Compress>=1.13
Flask-Limiter>=3.3.0
bokeh>=3.1.0

View File

@@ -4,6 +4,7 @@ window.addEventListener('load', () => {
"ordering": true,
"info": false,
"dom": "t",
"stateSave": true,
});
console.log(tables);
});

View File

@@ -0,0 +1,40 @@
window.addEventListener('load', () => {
// Mobile menu handling
const menuButton = document.getElementById('base-header-mobile-menu-button');
const mobileMenu = document.getElementById('base-header-mobile-menu');
menuButton.addEventListener('click', (evt) => {
evt.preventDefault();
evt.stopPropagation();
if (!mobileMenu.style.display || mobileMenu.style.display === 'none') {
return mobileMenu.style.display = 'flex';
}
mobileMenu.style.display = 'none';
});
window.addEventListener('resize', () => {
mobileMenu.style.display = 'none';
});
// Popover handling
const popoverText = document.getElementById('base-header-popover-text');
const popoverMenu = document.getElementById('base-header-popover-menu');
popoverText.addEventListener('click', (evt) => {
evt.preventDefault();
evt.stopPropagation();
if (!popoverMenu.style.display || popoverMenu.style.display === 'none') {
return popoverMenu.style.display = 'flex';
}
popoverMenu.style.display = 'none';
});
document.body.addEventListener('click', () => {
mobileMenu.style.display = 'none';
popoverMenu.style.display = 'none';
});
});

View File

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

View File

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

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

View File

@@ -0,0 +1,94 @@
# Multiworld Glossary
There are a lot of common terms used when playing in different game randomizer communities and in multiworld as well.
This document serves as a lookup for common terms that may be used by users in the community or in various other
documentation.
## Item
Items are what get shuffled around in your world or other worlds that you then receive. This could be a sword, a stat
upgrade, a spell, or any other potential receivable for your game.
## Location
Locations are where items are placed in your game. Whenever you interact with a location, you or another player will
then receive an item. A location could be a chest, an enemy drop, a shop purchase, or any other interactable that can
contain items in your game.
## Check
A check is a common term for when you "check", or pick up, a location. In terms of Archipelago this is usually used for
when a player goes to a location and sends its item, or "checks" the location. Players will often reference their now
randomized locations as checks.
## Slot
A slot is the player name and number assigned during generation. The number of slots is equal to the number of players,
or "worlds", created. Each name must be unique as these are used to identify the slot user.
## World
World in terms of Archipelago can mean multiple things and is used interchangeably in many situations.
* During gameplay, a world is a single instance of a game, occupying one player "slot". However,
Archipelago allows multiple players to connect to the same slot; then those players can share a world
and complete it cooperatively. For games with native cooperative play, you can also play together and
share a world that way, usually with only one player connected to the multiworld.
* On the programming side, a world typically represents the package that integrates Archipelago with a
particular game. For example this could be the entire `worlds/factorio` directory.
## RNG
Acronym for "Random Number Generator." Archipelago uses its own custom Random object with a unique seed per generation,
or, if running from source, a seed can be supplied and this seed will control all randomization during generation as all
game worlds will have access to it.
## Seed
A "seed" is a number used to initialize a pseudorandom number generator. Whenever you generate a new game on Archipelago
this is a new "seed" as it has unique item placement, and you can create multiple "rooms" on the Archipelago site from a
single seed. Using the same seed results in the random placement being the same.
## Room
Whenever you generate a seed on the Archipelago website you will be put on a seed page that contains all the seed info
with a link to the spoiler if one exists and will show how many unique rooms exist per seed. Each room has its own
unique identifier that is separate from the seed. The room page is where you can find information to connect to the
multiworld and download any patches if necessary. If you have a particularly fun or interesting seed, and you want to
share it with somebody you can link them to this seed page, where they can generate a new room to play it! For seeds
generated with race mode enabled, the seed page will only show rooms created by the unique user so the seed page is
perfectly safe to share for racing purposes.
## Logic
Base behavior of all seeds generated by Archipelago is they are expected to be completable based on the requirements of
the settings. This is done by using "logic" in order to determine valid locations to place items while still being able
to reach said location without this item. For the purposes of the randomizer a location is considered "in logic" if you
can reach it with your current toolset of items or skills based on settings. Some players are able to obtain locations
"out of logic" by performing various glitches or tricks that the settings may not account for and tend to mention this
when sending out an item they obtained this way.
## Progression
Certain items will allow access to more locations and are considered progression items as they "progress" the seed.
## Trash
A term used for "filler" items that have no bearing on the generation and are either marginally useful for the player
or useless. These items can be very useful depending on the player but are never very important and as such are usually
termed trash.
## Burger King / BK Mode
A term used in multiworlds when a player is unable to continue to progress and is awaiting an item. The term came to be
after a player, allegedly, was unable to progress during a multiworld and went to Burger King while waiting to receive
items from other players.
* "Logical BK" is when the player is unable to progress according to the settings of their game but may still be able to do
things that would be "out of logic" by the generation.
* "Hard / full BK" is when the player is completely unable to progress even with tricks they may know and are unable to
continue to play, aside from doing something like killing enemies for experience or money.
## Sphere
Archipelago calculates the game playthrough by using a "sphere" system where it has a state for each player and checks
to see what the players are able to reach with their current items. Any location that is reachable with the current
state of items is a "sphere." For the purposes of Archipelago it starts playthrough calculation by distributing sphere 0
items which are items that are either forced in the player's inventory by the game or placed in the `start_inventory` in
their settings. Sphere 1 is then all accessible locations the players can reach with all the items they received from
sphere 0, or their starting inventory. The playthrough continues in this fashion calculating a number of spheres until
all players have completed their goal.
## Scouts / Scouting
In some games there are locations that have visible items even if the item itself is unobtainable at the current time.
Some games utilize a scouting feature where when the player "sees" the item it will give a free hint for the item in the
client letting the players know what the exact item is, since if the item was for that game it would know but the item
being foreign is a lot harder to represent visually.

View File

@@ -0,0 +1,51 @@
window.addEventListener('load', () => {
const gameInfo = document.getElementById('game-info');
new Promise((resolve, reject) => {
const ajax = new XMLHttpRequest();
ajax.onreadystatechange = () => {
if (ajax.readyState !== 4) { return; }
if (ajax.status === 404) {
reject("Sorry, this game's info page is not available in that language yet.");
return;
}
if (ajax.status !== 200) {
reject("Something went wrong while loading the info page.");
return;
}
resolve(ajax.responseText);
};
ajax.open('GET', `${window.location.origin}/static/generated/docs/${gameInfo.getAttribute('data-game')}/` +
`${gameInfo.getAttribute('data-lang')}_${gameInfo.getAttribute('data-game')}.md`, true);
ajax.send();
}).then((results) => {
// Populate page with HTML generated from markdown
showdown.setOption('tables', true);
showdown.setOption('strikethrough', true);
showdown.setOption('literalMidWordUnderscores', true);
gameInfo.innerHTML += (new showdown.Converter()).makeHtml(results);
adjustHeaderWidth();
// Reset the id of all header divs to something nicer
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);
gameInfo.innerHTML =
`<h2>This page is out of logic!</h2>
<h3>Click <a href="${window.location.origin}">here</a> to return to safety.</h3>`;
});
});

View File

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

@@ -0,0 +1,6 @@
window.addEventListener('load', () => {
$(".table-wrapper").scrollsync({
y_sync: true,
x_sync: true
});
});

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

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

View File

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

View File

@@ -4,17 +4,31 @@ window.addEventListener('load', () => {
gameName = document.getElementById('player-settings').getAttribute('data-game');
// Update game name on page
document.getElementById('game-name').innerHTML = gameName;
document.getElementById('game-name').innerText = gameName;
fetchSettingData().then((results) => {
let settingHash = localStorage.getItem(`${gameName}-hash`);
if (!settingHash) {
// If no hash data has been set before, set it now
settingHash = md5(JSON.stringify(results));
localStorage.setItem(`${gameName}-hash`, settingHash);
localStorage.removeItem(gameName);
}
if (settingHash !== md5(JSON.stringify(results))) {
showUserMessage("Your settings are out of date! Click here to update them! Be aware this will reset " +
"them all to default.");
document.getElementById('user-message').addEventListener('click', resetSettings);
}
Promise.all([fetchSettingData()]).then((results) => {
// Page setup
createDefaultSettings(results[0]);
buildUI(results[0]);
createDefaultSettings(results);
buildUI(results);
adjustHeaderWidth();
// Event listeners
document.getElementById('export-settings').addEventListener('click', () => exportSettings());
document.getElementById('generate-race').addEventListener('click', () => generateGame(true))
document.getElementById('generate-race').addEventListener('click', () => generateGame(true));
document.getElementById('generate-game').addEventListener('click', () => generateGame());
// Name input field
@@ -22,12 +36,19 @@ window.addEventListener('load', () => {
const nameInput = document.getElementById('player-name');
nameInput.addEventListener('keyup', (event) => updateBaseSetting(event));
nameInput.value = playerSettings.name;
}).catch((error) => {
}).catch((e) => {
console.error(e);
const url = new URL(window.location.href);
window.location.replace(`${url.protocol}//${url.hostname}/page-not-found`);
})
});
const resetSettings = () => {
localStorage.removeItem(gameName);
localStorage.removeItem(`${gameName}-hash`)
window.location.reload();
};
const fetchSettingData = () => new Promise((resolve, reject) => {
const ajax = new XMLHttpRequest();
ajax.onreadystatechange = () => {
@@ -39,7 +60,7 @@ const fetchSettingData = () => new Promise((resolve, reject) => {
try{ resolve(JSON.parse(ajax.responseText)); }
catch(error){ reject(error); }
};
ajax.open('GET', `${window.location.origin}/static/generated/${gameName}.json`, true);
ajax.open('GET', `${window.location.origin}/static/generated/player-settings/${gameName}.json`, true);
ajax.send();
});
@@ -81,9 +102,15 @@ const buildOptionsTable = (settings, romOpts = false) => {
// td Left
const tdl = document.createElement('td');
const label = document.createElement('label');
label.textContent = `${settings[setting].displayName}: `;
label.setAttribute('for', setting);
label.setAttribute('data-tooltip', settings[setting].description);
label.innerText = `${settings[setting].friendlyName}:`;
const questionSpan = document.createElement('span');
questionSpan.classList.add('interactive');
questionSpan.setAttribute('data-tooltip', settings[setting].description);
questionSpan.innerText = '(?)';
label.appendChild(questionSpan);
tdl.appendChild(label);
tr.appendChild(tdl);
@@ -91,6 +118,8 @@ const buildOptionsTable = (settings, romOpts = false) => {
const tdr = document.createElement('td');
let element = null;
const randomButton = document.createElement('button');
switch(settings[setting].type){
case 'select':
element = document.createElement('div');
@@ -111,8 +140,21 @@ const buildOptionsTable = (settings, romOpts = false) => {
}
select.appendChild(option);
});
select.addEventListener('change', (event) => updateGameSetting(event));
select.addEventListener('change', (event) => updateGameSetting(event.target));
element.appendChild(select);
// Randomize button
randomButton.innerText = '🎲';
randomButton.classList.add('randomize-button');
randomButton.setAttribute('data-key', setting);
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
randomButton.addEventListener('click', (event) => toggleRandomize(event, [select]));
if (currentSettings[gameName][setting] === 'random') {
randomButton.classList.add('active');
select.disabled = true;
}
element.appendChild(randomButton);
break;
case 'range':
@@ -127,20 +169,119 @@ const buildOptionsTable = (settings, romOpts = false) => {
range.value = currentSettings[gameName][setting];
range.addEventListener('change', (event) => {
document.getElementById(`${setting}-value`).innerText = event.target.value;
updateGameSetting(event);
updateGameSetting(event.target);
});
element.appendChild(range);
let rangeVal = document.createElement('span');
rangeVal.classList.add('range-value');
rangeVal.setAttribute('id', `${setting}-value`);
rangeVal.innerText = currentSettings[gameName][setting] ?? settings[setting].defaultValue;
rangeVal.innerText = currentSettings[gameName][setting] !== 'random' ?
currentSettings[gameName][setting] : settings[setting].defaultValue;
element.appendChild(rangeVal);
// Randomize button
randomButton.innerText = '🎲';
randomButton.classList.add('randomize-button');
randomButton.setAttribute('data-key', setting);
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
randomButton.addEventListener('click', (event) => toggleRandomize(event, [range]));
if (currentSettings[gameName][setting] === 'random') {
randomButton.classList.add('active');
range.disabled = true;
}
element.appendChild(randomButton);
break;
case 'special_range':
element = document.createElement('div');
element.classList.add('special-range-container');
// Build the select element
let specialRangeSelect = document.createElement('select');
specialRangeSelect.setAttribute('data-key', setting);
Object.keys(settings[setting].value_names).forEach((presetName) => {
let presetOption = document.createElement('option');
presetOption.innerText = presetName;
presetOption.value = settings[setting].value_names[presetName];
const words = presetOption.innerText.split("_");
for (let i = 0; i < words.length; i++) {
words[i] = words[i][0].toUpperCase() + words[i].substring(1);
}
presetOption.innerText = words.join(" ");
specialRangeSelect.appendChild(presetOption);
});
let customOption = document.createElement('option');
customOption.innerText = 'Custom';
customOption.value = 'custom';
customOption.selected = true;
specialRangeSelect.appendChild(customOption);
if (Object.values(settings[setting].value_names).includes(Number(currentSettings[gameName][setting]))) {
specialRangeSelect.value = Number(currentSettings[gameName][setting]);
}
// Build range element
let specialRangeWrapper = document.createElement('div');
specialRangeWrapper.classList.add('special-range-wrapper');
let specialRange = document.createElement('input');
specialRange.setAttribute('type', 'range');
specialRange.setAttribute('data-key', setting);
specialRange.setAttribute('min', settings[setting].min);
specialRange.setAttribute('max', settings[setting].max);
specialRange.value = currentSettings[gameName][setting];
// Build rage value element
let specialRangeVal = document.createElement('span');
specialRangeVal.classList.add('range-value');
specialRangeVal.setAttribute('id', `${setting}-value`);
specialRangeVal.innerText = currentSettings[gameName][setting] !== 'random' ?
currentSettings[gameName][setting] : settings[setting].defaultValue;
// Configure select event listener
specialRangeSelect.addEventListener('change', (event) => {
if (event.target.value === 'custom') { return; }
// Update range slider
specialRange.value = event.target.value;
document.getElementById(`${setting}-value`).innerText = event.target.value;
updateGameSetting(event.target);
});
// Configure range event handler
specialRange.addEventListener('change', (event) => {
// Update select element
specialRangeSelect.value =
(Object.values(settings[setting].value_names).includes(parseInt(event.target.value))) ?
parseInt(event.target.value) : 'custom';
document.getElementById(`${setting}-value`).innerText = event.target.value;
updateGameSetting(event.target);
});
element.appendChild(specialRangeSelect);
specialRangeWrapper.appendChild(specialRange);
specialRangeWrapper.appendChild(specialRangeVal);
element.appendChild(specialRangeWrapper);
// Randomize button
randomButton.innerText = '🎲';
randomButton.classList.add('randomize-button');
randomButton.setAttribute('data-key', setting);
randomButton.setAttribute('data-tooltip', 'Toggle randomization for this option!');
randomButton.addEventListener('click', (event) => toggleRandomize(
event, [specialRange, specialRangeSelect])
);
if (currentSettings[gameName][setting] === 'random') {
randomButton.classList.add('active');
specialRange.disabled = true;
specialRangeSelect.disabled = true;
}
specialRangeWrapper.appendChild(randomButton);
break;
default:
console.error(`Unknown setting type: ${settings[setting].type}`);
console.error(setting);
console.error(`Ignoring unknown setting type: ${settings[setting].type} with name ${setting}`);
return;
}
@@ -153,6 +294,25 @@ const buildOptionsTable = (settings, romOpts = false) => {
return table;
};
const toggleRandomize = (event, inputElements) => {
const active = event.target.classList.contains('active');
const randomButton = event.target;
if (active) {
randomButton.classList.remove('active');
for (const element of inputElements) {
element.disabled = undefined;
updateGameSetting(element);
}
} else {
randomButton.classList.add('active');
for (const element of inputElements) {
element.disabled = true;
updateGameSetting(randomButton);
}
}
};
const updateBaseSetting = (event) => {
const options = JSON.parse(localStorage.getItem(gameName));
options[event.target.getAttribute('data-key')] = isNaN(event.target.value) ?
@@ -160,16 +320,25 @@ const updateBaseSetting = (event) => {
localStorage.setItem(gameName, JSON.stringify(options));
};
const updateGameSetting = (event) => {
const updateGameSetting = (settingElement) => {
const options = JSON.parse(localStorage.getItem(gameName));
options[gameName][event.target.getAttribute('data-key')] = isNaN(event.target.value) ?
event.target.value : parseInt(event.target.value, 10);
if (settingElement.classList.contains('randomize-button')) {
// If the event passed in is the randomize button, then we know what we must do.
options[gameName][settingElement.getAttribute('data-key')] = 'random';
} else {
options[gameName][settingElement.getAttribute('data-key')] = isNaN(settingElement.value) ?
settingElement.value : parseInt(settingElement.value, 10);
}
localStorage.setItem(gameName, JSON.stringify(options));
};
const exportSettings = () => {
const settings = JSON.parse(localStorage.getItem(gameName));
if (!settings.name || settings.name.trim().length === 0) { settings.name = "noname"; }
if (!settings.name || settings.name.toLowerCase() === 'player' || settings.name.trim().length === 0) {
return showUserMessage('You must enter a player name!');
}
const yamlText = jsyaml.safeDump(settings, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`);
download(`${document.getElementById('player-name').value}.yaml`, yamlText);
};
@@ -186,21 +355,41 @@ const download = (filename, text) => {
};
const generateGame = (raceMode = false) => {
const settings = JSON.parse(localStorage.getItem(gameName));
if (!settings.name || settings.name.toLowerCase() === 'player' || settings.name.trim().length === 0) {
return showUserMessage('You must enter a player name!');
}
axios.post('/api/generate', {
weights: { player: localStorage.getItem(gameName) },
presetData: { player: localStorage.getItem(gameName) },
weights: { player: settings },
presetData: { player: settings },
playerCount: 1,
race: raceMode ? '1' : '0',
}).then((response) => {
window.location.href = response.data.url;
}).catch((error) => {
const userMessage = document.getElementById('user-message');
userMessage.innerText = 'Something went wrong and your game could not be generated.';
let userMessage = 'Something went wrong and your game could not be generated.';
if (error.response.data.text) {
userMessage.innerText += ' ' + error.response.data.text;
userMessage += ' ' + error.response.data.text;
}
userMessage.classList.add('visible');
window.scrollTo(0, 0);
showUserMessage(userMessage);
console.error(error);
});
};
const showUserMessage = (message) => {
const userMessage = document.getElementById('user-message');
userMessage.innerText = message;
userMessage.classList.add('visible');
window.scrollTo(0, 0);
userMessage.addEventListener('click', () => {
userMessage.classList.remove('visible');
userMessage.addEventListener('click', hideUserMessage);
});
};
const hideUserMessage = () => {
const userMessage = document.getElementById('user-message');
userMessage.classList.remove('visible');
userMessage.removeEventListener('click', hideUserMessage);
};

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,7 @@
const adjustTableHeight = () => {
const tablesContainer = document.getElementById('tables-container');
if (!tablesContainer)
return;
const upperDistance = tablesContainer.getBoundingClientRect().top;
const containerHeight = window.innerHeight - upperDistance;
@@ -17,6 +19,14 @@ window.addEventListener('load', () => {
paging: false,
info: false,
dom: "t",
stateSave: true,
stateSaveCallback: function(settings, data) {
delete data.search;
localStorage.setItem(`DataTables_${settings.sInstance}_/tracker`, JSON.stringify(data));
},
stateLoadCallback: function(settings) {
return JSON.parse(localStorage.getItem(`DataTables_${settings.sInstance}_/tracker`));
},
columnDefs: [
{
targets: 'hours',
@@ -63,16 +73,44 @@ window.addEventListener('load', () => {
// the tbody and render two separate tables.
});
document.getElementById('search').addEventListener('keyup', (event) => {
tables.search(event.target.value);
console.info(tables.search());
const searchBox = document.getElementById("search");
searchBox.value = tables.search();
searchBox.focus();
searchBox.select();
const doSearch = () => {
tables.search(searchBox.value);
tables.draw();
};
searchBox.addEventListener("keyup", doSearch);
window.addEventListener("keydown", (event) => {
if (!event.ctrlKey && !event.altKey && event.key.length === 1 && document.activeElement !== searchBox) {
searchBox.focus();
searchBox.select();
}
if (!event.ctrlKey && !event.altKey && !event.shiftKey && event.key === "Escape") {
if (searchBox.value !== "") {
searchBox.value = "";
doSearch();
}
searchBox.blur();
if (!document.getElementById("tables-container"))
window.scroll(0, 0);
event.preventDefault();
}
});
const tracker = document.getElementById('tracker-wrapper').getAttribute('data-tracker');
const target_second = document.getElementById('tracker-wrapper').getAttribute('data-second') + 3;
function getSleepTimeSeconds(){
// -40 % 60 is -40, which is absolutely wrong and should burn
var sleepSeconds = (((target_second - new Date().getSeconds()) % 60) + 60) % 60;
return sleepSeconds || 60;
}
const update = () => {
const target = $("<div></div>");
const tracker = document.getElementById('tracker-wrapper').getAttribute('data-tracker');
target.load("/tracker/" + tracker, function (response, status) {
console.log("Updating Tracker...");
target.load(location.href, function (response, status) {
if (status === "success") {
target.find(".table").each(function (i, new_table) {
const new_trs = $(new_table).find("tbody>tr");
@@ -90,19 +128,14 @@ window.addEventListener('load', () => {
console.log(response);
}
})
setTimeout(update, getSleepTimeSeconds()*1000);
}
setInterval(update, 30000);
setTimeout(update, getSleepTimeSeconds()*1000);
window.addEventListener('resize', () => {
adjustTableHeight();
tables.draw();
});
$(".table-wrapper").scrollsync({
y_sync: true,
x_sync: true
});
adjustTableHeight();
});

View File

@@ -14,34 +14,41 @@ window.addEventListener('load', () => {
}
resolve(ajax.responseText);
};
ajax.open('GET', `${window.location.origin}/static/assets/tutorial/` +
ajax.open('GET', `${window.location.origin}/static/generated/docs/` +
`${tutorialWrapper.getAttribute('data-game')}/${tutorialWrapper.getAttribute('data-file')}_` +
`${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);
showdown.setOption('disableForced4SpacesIndentedSublists', true);
tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results);
adjustHeaderWidth();
const title = document.querySelector('h1')
if (title) {
document.title = title.textContent;
}
// Reset the id of all header divs to something nicer
const headers = Array.from(document.querySelectorAll('h1, h2, h3, h4, h5, h6'));
const scrollTargetIndex = window.location.href.search(/#[A-z0-9-_]*$/);
for (let i=0; i < headers.length; i++){
const headerId = headers[i].innerText.replace(/[ ]/g,'-').toLowerCase()
headers[i].setAttribute('id', headerId);
headers[i].addEventListener('click', () =>
window.location.href = window.location.href.substring(0, scrollTargetIndex) + `#${headerId}`);
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
if (scrollTargetIndex > -1) {
try{
const scrollTarget = window.location.href.substring(scrollTargetIndex + 1);
document.getElementById(scrollTarget).scrollIntoView({ behavior: "smooth" });
} catch(error) {
console.error(error);
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 =

View File

@@ -1,52 +0,0 @@
# Factorio Randomizer Setup Guide
## Required Software
### Server Host
- [Factorio](https://factorio.com)
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
### Players
- [Factorio](https://factorio.com)
## General Concept
One Server Host exists per Factorio World in an Archipelago Multiworld, any number of modded Factorio players can then connect to that world. From the view of Archipelago, this Factorio host is a client.
## Installation Procedures
### Dedicated Server Setup
You need a dedicated isolated Factorio installation that the FactorioClient can take control over. If you intend to both host a world and play on the same device, you will need two separate Factorio installations; one for the FactorioClient to hook into and control, and one for you to play on.
The easiest and cheapest way to do so is to either buy or register a Factorio key on factorio.com, which allows you to download as many Factorio games as you want. If you own a steam copy already you can link your account on the website.
1. Download the latest Factorio from https://factorio.com/download for your system, for Windows the recommendation is "win64-manual".
2. Make sure the Factorio you play and the Factorio you use for hosting do not share paths. If you downloaded the "manual" version, this is already the case, otherwise, go into the hosting Factorio's folder and put the following text into its `config-path.cfg`:
```ini
config-path=__PATH__executable__/../../config
use-system-read-write-data-directories=false
```
3. In this same folder if there are shortcuts named "mods" and "saves" delete these and replace with folders with the same names.
4. Navigate to where you installed ArchipelagoFactorioClient and open the host.yaml file as text. Find the entry `executable` under `factorio_options` and set it to point to your hosting Factorio.exe. If you put Factorio into your Archipelago folder, this would already match.<br>
ex.
```yaml
factorio_options:
executable: C:\\Program Files\\factorio\\bin\\x64\\factorio"
```
### Player Setup
- Manually install the AP mod for the correct world you want to join, then use Factorio's built-in multiplayer. If you're connecting to a FactorioClient on the same system you will connect to localhost
## Joining a MultiWorld Game
1. Install the generated Factorio AP Mod (would be in /Mods after step 2 of Setup)
2. Run FactorioClient, it should launch a Factorio server, which you can control with /factorio <original factorio commands>,
* It should start up, create a world and become ready for Factorio connections.
3. In FactorioClient, do /connect <Archipelago Server Address> to join that multiworld. You can find further commands with /help as well as !help once connected.
* / commands are run on your local client, ! commands are requests for the AP server
* Players should be able to connect to your Factorio Server and begin playing.
4. You can join yourself by connecting to address localhost, other people will need to connect to your IP and you may need to port forward for the Factorio Server for those connections.

View File

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

View File

@@ -1,121 +0,0 @@
# Guia instalación de Minecraft Randomizer
## Software Requerido
### Servidor
- [Minecraft Forge](https://files.minecraftforge.net/net/minecraftforge/forge/index_1.16.5.html)
- [Minecraft Archipelago Randomizer Mod](https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases)
### Jugadores
- [Minecraft Java Edition](https://www.minecraft.net/en-us/store/minecraft-java-edition)
## Procedimiento de instalación
### Instalación de servidor dedicado
Solo una persona ha de realizar este proceso y hospedar un servidor dedicado para que los demas jueguen conectandose a él.
1. Descarga el instalador de **Minecraft Forge** 1.16.15 desde el enlace proporcionado, siempre asegurandose de bajar la version mas reciente.
2. Ejecuta el fichero `forge-1.16.5-xx.x.x-installer.jar` y elije **install server**.
- En esta pagina elegiras ademas donde instalar el servidor, importante recordar esta localización en el siguiente paso.
3. Navega al directorio donde hayas instalado el servidor y abre `forge-1.16.5-xx.x.x.jar`
- La primera vez que lances el servidor se cerrara (o no aparecerá nada en absoluto), debería haber un fichero nuevo en el directorio llamado `eula.txt`, el cual que contiene un enlace al EULA de minecraft, cambia la linea a `eula=true` para aceptar el EULA y poder utilizar el software de servidor.
- Esto creara la estructura de directorios apropiada para el siguiente paso
4. Coloca el fichero `aprandomizer-x.x.x.jar` del segundo enlace en el directorio `mods`
- Cuando se ejecute el servidor de nuevo, generara el directorio `APData` que se necesitara para jugar
### Instalación basica para jugadores
- Compra e instala Minecraft a traves del tercer enlace.
**Y listo!**.
Los jugadores solo necesitan una version no modificada de Minecraft para jugar!
### Instalación avanzada para jugadores
***Esto no es requerido para jugar a minecraft randomizado.***
Sin embargo lo recomendamos porque hace la experiencia mas llevadera.
#### Recomended Mods
- [JourneyMap](https://www.curseforge.com/minecraft/mc-mods/journeymap) (Minimap)
1. Instala y ejecuta Minecraft al menos una vez.
2. Ejecuta el fichero `forge-1.16.5-xx.x.x-installer.jar` y elige **install client**.
- Ejecuta Minecraft forge al menos una vez para generar los directorios necesarios para el siguiente paso.
3. Navega a la carpeta de instalación de Minecraft y colocal los mods que quieras en el directorio `mods`
- Los directorios por defecto de instalación son:
- Windows `%APPDATA%\.minecraft\mods`
- macOS `~/Library/Application Support/minecraft/mods`
- Linux `~/.minecraft/mods`
## Configura tu fichero YAML
### Que es un fichero YAML y potque necesito uno?
Tu fichero YAML contiene un numero de opciones que proveen al generador con informacion sobre como debe generar tu juego.
Cada jugador de un multiworld entregara u propio fichero YAML.
Esto permite que cada jugador disfrute de una experiencia personalizada a su gusto y diferentes jugadores dentro del mismo multiworld
pueden tener diferentes opciones
### Where do I get a YAML file?
Un fichero basico yaml para minecraft tendra este aspecto.
```yaml
# Usado para describir tu yaml. Util si tienes multiples ficheros
description: Template Name
# Tu nombre en el juego. Los espacios son reemplazados por guiones bajos, limitado a 16 caracteres
name: YourName
game: Minecraft
accessibility: locations
# Recomendado no activar esto ya que el pool de objetos de Minecraft es bastante escueto, ademas hay muchas maneras alternativas de obtener los objetivos de Minecraft.
progression_balancing: off
# Cuantos avances se necesitan para hacer aparecer el Ender Dragon y acabar el juego. few = 30, normal = 50 , many = 70
advancement_goal:
few: 0
normal: 1
many: 0
# Modifica el nivel de objetos lógicamente requeridos para explorar areas peligrosas y pelear contra jefes.
combat_difficulty:
easy: 0
normal: 1
hard: 0
# Avances que sean tediosos o basados en suerte tendran simplemente experiencia o cosas no necesarias
include_hard_advancements:
on: 0
off: 1
# Los avances extremadamente difíciles no seran requeridos; esto afecta a How Did We Get Here? y Adventuring Time.
include_insane_advancements:
on: 0
off: 1
# Los avances posteriores a Ender Dragon no tendrán objetos necesarios para que otros jugadores en el caso de un MW acaben su partida.
include_postgame_advancements:
on: 0
off: 1
# Actualmente desactivado; permite la mezcla de pueblos, puestos, fortalezas, bastiones y cuidades.
shuffle_structures:
on: 0
off: 1
```
## Unirse a un juego MultiWorld
### Obten tu ficheros de datos Minecraft
**Solo un fichero yaml es necesario por mundo minecraft, sin importar el numero de jugadores que jueguen en el.**
Cuando te unes a un juego multiworld, se te pedirá que entregues tu fichero YAML a quien sea que hospede el juego multiworld (no confundir con hospedar el mundo minecraft).
Una vez la generación acabe, el anfitrión te dará un enlace a tu fichero de datos o un zip con los ficheros de todos.
Tu fichero de datos tiene una extensión `.apmc`.
Pon tu fichero de datos en el directorio `APData` de tu forge server. Asegurate de eliminar los que hubiera anteriormente
### Conectar al multiserver
Despues de poner tu fichero en el directorio `APData`, arranca el Forge server y asegurate que tienes el estado OP
tecleando `/op TuUsuarioMinecraft` en la consola del servidor y entonces conectate con tu cliente Minecraft.
Una vez en juego introduce `/connect <AP-Address> (<Password>)` donde `<AP-Address>` es la dirección del servidor
Archipelago. `(<Password>)`
solo se necesita si el servidor Archipleago tiene un password activo.
### Jugar al juego
Cuando la consola te diga que te has unido a la sala, estas lista/o para empezar a jugar. Felicidades
por unirte exitosamente a un juego multiworld! Llegados a este punto cualquier jugador adicional puede conectarse a tu servidor forge.

View File

@@ -1,143 +0,0 @@
[
{
"gameTitle": "The Legend of Zelda: A Link to the Past",
"tutorials": [
{
"name": "Multiworld Setup Tutorial",
"description": "A guide to setting up the Archipelago ALttP software on your computer. This guide covers single-player, multiworld, and related software.",
"files": [
{
"language": "English",
"filename": "zelda3/multiworld_en.md",
"link": "zelda3/multiworld/en",
"authors": [
"Farrak Kilhn"
]
},
{
"language": "Deutsch",
"filename": "zelda3/multiworld_de.md",
"link": "zelda3/multiworld/de",
"authors": [
"Fischfilet"
]
},
{
"language": "Español",
"filename": "zelda3/multiworld_es.md",
"link": "zelda3/multiworld/es",
"authors": [
"Edos"
]
},
{
"language": "Français",
"filename": "zelda3/multiworld_fr.md",
"link": "zelda3/multiworld/fr",
"authors": [
"Coxla"
]
}
]
},
{
"name": "MSU-1 Setup Tutorial",
"description": "A guide to setting up MSU-1, which allows for custom in-game music.",
"files": [
{
"language": "English",
"filename": "zelda3/msu1_en.md",
"link": "zelda3/msu1/en",
"authors": [
"Farrak Kilhn"
]
},
{
"language": "Español",
"filename": "zelda3/msu1_es.md",
"link": "zelda3/msu1/es",
"authors": [
"Edos"
]
},
{
"language": "Français",
"filename": "msu1_fr.md",
"link": "zelda3/msu1/fr",
"authors": [
"Coxla"
]
}
]
},
{
"name": "Plando Tutorial",
"description": "A guide to creating Multiworld Plandos",
"files": [
{
"language": "English",
"filename": "zelda3/plando_en.md",
"link": "zelda3/plando/en",
"authors": [
"Berserker"
]
}
]
}
]
},
{
"gameTitle": "Factorio",
"tutorials": [
{
"name": "Multiworld Setup Tutorial",
"description": "A guide to setting up the Archipelago Factorio software on your computer.",
"files": [
{
"language": "English",
"filename": "factorio/setup_en.md",
"link": "factorio/setup/en",
"authors": [
"Berserker"
]
}
]
}
]
},
{
"gameTitle": "Minecraft",
"tutorials": [
{
"name": "Multiworld Setup Tutorial",
"description": "A guide to setting up the Archipelago Minecraft software on your computer. This guide covers single-player, multiworld, and related software.",
"files": [
{
"language": "English",
"filename": "minecraft/minecraft_en.md",
"link": "minecraft/minecraft/en",
"authors": [
"Kono Tyran"
]
},
{
"language": "Spanish",
"filename": "minecraft/minecraft_es.md",
"link": "minecraft/minecraft/es",
"authors": [
"Edos"
]
},
{
"language": "Swedish",
"filename": "minecraft/minecraft_sv.md",
"link": "minecraft/minecraft/sv",
"authors": [
"Albinum"
]
}
]
}
]
}
]

View File

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

View File

@@ -23,6 +23,7 @@ window.addEventListener('load', () => {
games.forEach((game) => {
const gameTitle = document.createElement('h2');
gameTitle.innerText = game.gameTitle;
gameTitle.id = `${encodeURIComponent(game.gameTitle)}`;
tutorialDiv.appendChild(gameTitle);
game.tutorials.forEach((tutorial) => {
@@ -65,7 +66,16 @@ window.addEventListener('load', () => {
showError();
console.error(error);
}
// Check if we are on an anchor when coming in, and scroll to it.
const hash = window.location.hash;
if (hash) {
const offset = 128; // To account for navbar banner at top of page.
window.scrollTo(0, 0);
const rect = document.getElementById(hash.slice(1)).getBoundingClientRect();
window.scrollTo(rect.left, rect.top - offset);
}
};
ajax.open('GET', `${window.location.origin}/static/assets/tutorial/tutorials.json`, true);
ajax.open('GET', `${window.location.origin}/static/generated/tutorials.json`, true);
ajax.send();
});

View File

@@ -6,6 +6,7 @@ window.addEventListener('load', () => {
"order": [[ 3, "desc" ]],
"info": false,
"dom": "t",
"stateSave": true,
});
$("#seeds-table").DataTable({
"paging": false,
@@ -13,5 +14,6 @@ window.addEventListener('load', () => {
"order": [[ 2, "desc" ]],
"info": false,
"dom": "t",
"stateSave": true,
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -1,486 +0,0 @@
let spriteData = null;
window.addEventListener('load', () => {
const gameSettings = document.getElementById('weighted-settings');
Promise.all([fetchWeightedSettingsYaml(), fetchWeightedSettingsJson(), fetchSpriteData()]).then((results) => {
// Load YAML into object
const sourceData = jsyaml.safeLoad(results[0], { json: true });
const wsVersion = sourceData.ws_version;
delete sourceData.ws_version; // Do not include the settings version number in the export
// Check if settings exist in localStorage. If no settings are present, this is a first load (or reset to default)
// and the version number should be silently updated
if (!localStorage.getItem('weightedSettings1')) {
localStorage.setItem('wsVersion', wsVersion);
}
// Update localStorage with three settings objects. Preserve original objects if present.
for (let i=1; i<=3; i++) {
const localSettings = JSON.parse(localStorage.getItem(`weightedSettings${i}`));
const updatedObj = localSettings ? Object.assign(sourceData, localSettings) : sourceData;
localStorage.setItem(`weightedSettings${i}`, JSON.stringify(updatedObj));
}
// Build the entire UI
buildUI(JSON.parse(results[1]), JSON.parse(results[2]));
// Populate the UI and add event listeners
populateSettings();
document.getElementById('preset-number').addEventListener('change', populateSettings);
gameSettings.addEventListener('change', handleOptionChange);
gameSettings.addEventListener('keyup', handleOptionChange);
document.getElementById('export-button').addEventListener('click', exportSettings);
document.getElementById('reset-to-default').addEventListener('click', resetToDefaults);
adjustHeaderWidth();
if (localStorage.getItem('wsVersion') !== wsVersion) {
const userWarning = document.getElementById('user-warning');
const messageSpan = document.createElement('span');
messageSpan.innerHTML = "A new version of the weighted settings file is available. Click here to update!" +
"<br />Be aware this will also reset your presets, so you should export them now if you want to save them.";
userWarning.appendChild(messageSpan);
userWarning.style.display = 'block';
userWarning.addEventListener('click', resetToDefaults);
}
}).catch((error) => {
console.error(error);
gameSettings.innerHTML = `
<h2>Something went wrong while loading your game settings page.</h2>
<h2>${error}</h2>
<h2><a href="${window.location.origin}">Click here to return to safety!</a></h2>
`
});
document.getElementById('generate-game').addEventListener('click', () => generateGame());
document.getElementById('generate-race').addEventListener('click', () => generateGame(true));
});
const fetchWeightedSettingsYaml = () => new Promise((resolve, reject) => {
const ajax = new XMLHttpRequest();
ajax.onreadystatechange = () => {
if (ajax.readyState !== 4) { return; }
if (ajax.status !== 200) {
reject("Unable to fetch source yaml file.");
return;
}
resolve(ajax.responseText);
};
ajax.open('GET', `${window.location.origin}/static/static/weightedSettings.yaml` ,true);
ajax.send();
});
const fetchWeightedSettingsJson = () => new Promise((resolve, reject) => {
const ajax = new XMLHttpRequest();
ajax.onreadystatechange = () => {
if (ajax.readyState !== 4) { return; }
if (ajax.status !== 200) {
reject('Unable to fetch JSON schema file');
return;
}
resolve(ajax.responseText);
};
ajax.open('GET', `${window.location.origin}/static/static/weightedSettings.json`, true);
ajax.send();
});
const fetchSpriteData = () => new Promise((resolve, reject) => {
const ajax = new XMLHttpRequest();
ajax.onreadystatechange = () => {
if (ajax.readyState !== 4) { return; }
if (ajax.status !== 200) {
reject('Unable to fetch sprite data.');
return;
}
resolve(ajax.responseText);
};
ajax.open('GET', `${window.location.origin}/static/generated/spriteData.json`, true);
ajax.send();
});
const handleOptionChange = (event) => {
if(!event.target.matches('.setting')) { return; }
const presetNumber = document.getElementById('preset-number').value;
const settings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`))
const settingString = event.target.getAttribute('data-setting');
document.getElementById(settingString).innerText = event.target.value;
if(getSettingValue(settings, settingString) !== false){
const keys = settingString.split('.');
switch (keys.length) {
case 1:
settings[keys[0]] = isNaN(event.target.value) ?
event.target.value : parseInt(event.target.value, 10);
break;
case 2:
settings[keys[0]][keys[1]] = isNaN(event.target.value) ?
event.target.value : parseInt(event.target.value, 10);
break;
case 3:
settings[keys[0]][keys[1]][keys[2]] = isNaN(event.target.value) ?
event.target.value : parseInt(event.target.value, 10);
break;
default:
console.warn(`Unknown setting string received: ${settingString}`)
return;
}
// Save the updated settings object bask to localStorage
localStorage.setItem(`weightedSettings${presetNumber}`, JSON.stringify(settings));
}else{
console.warn(`Unknown setting string received: ${settingString}`)
}
};
const populateSettings = () => {
buildSpriteOptions();
const presetNumber = document.getElementById('preset-number').value;
const settings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`))
const settingsInputs = Array.from(document.querySelectorAll('.setting'));
settingsInputs.forEach((input) => {
const settingString = input.getAttribute('data-setting');
const settingValue = getSettingValue(settings, settingString);
if(settingValue !== false){
input.value = settingValue;
document.getElementById(settingString).innerText = settingValue;
}
});
};
/**
* Returns the value of the settings object, or false if the settings object does not exist
* @param settings
* @param keyString
* @returns {string} | bool
*/
const getSettingValue = (settings, keyString) => {
const keys = keyString.split('.');
let currentVal = settings;
keys.forEach((key) => {
if(typeof(key) === 'string' && currentVal.hasOwnProperty(key)){
currentVal = currentVal[key];
}else{
currentVal = false;
}
});
return currentVal;
};
const exportSettings = () => {
const presetNumber = document.getElementById('preset-number').value;
const settings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`));
const yamlText = jsyaml.safeDump(settings, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`);
download(`${settings.description}.yaml`, yamlText);
};
const resetToDefaults = () => {
[1, 2, 3].forEach((presetNumber) => localStorage.removeItem(`weightedSettings${presetNumber}`));
location.reload();
};
/** Create an anchor and trigger a download of a text file. */
const download = (filename, text) => {
const downloadLink = document.createElement('a');
downloadLink.setAttribute('href','data:text/yaml;charset=utf-8,'+ encodeURIComponent(text))
downloadLink.setAttribute('download', filename);
downloadLink.style.display = 'none';
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
};
const buildUI = (settings, spriteData) => {
const settingsWrapper = document.getElementById('settings-wrapper');
const settingTypes = {
gameOptions: 'Game Options',
romOptions: 'ROM Options',
}
Object.keys(settingTypes).forEach((settingTypeKey) => {
const sectionHeader = document.createElement('h2');
sectionHeader.innerText = settingTypes[settingTypeKey];
settingsWrapper.appendChild(sectionHeader);
Object.values(settings[settingTypeKey]).forEach((setting) => {
if (typeof(setting.inputType) === 'undefined' || !setting.inputType){
console.error(setting);
throw new Error('Setting with no inputType specified.');
}
switch(setting.inputType){
case 'text':
// Currently, all text input is handled manually because there is very little of it
return;
case 'range':
buildRangeSettings(settingsWrapper, setting);
return;
default:
console.error(setting);
throw new Error('Unhandled inputType specified.');
}
});
});
// Build sprite options
const spriteOptionsHeader = document.createElement('h2');
spriteOptionsHeader.innerText = 'Sprite Options';
settingsWrapper.appendChild(spriteOptionsHeader);
const spriteOptionsWrapper = document.createElement('div');
spriteOptionsWrapper.setAttribute('id', 'sprite-options-wrapper');
spriteOptionsWrapper.className = 'setting-wrapper';
settingsWrapper.appendChild(spriteOptionsWrapper);
// Append sprite picker
settingsWrapper.appendChild(buildSpritePicker(spriteData));
};
const buildSpriteOptions = () => {
const spriteOptionsWrapper = document.getElementById('sprite-options-wrapper');
// Clear the contents of the wrapper div
while(spriteOptionsWrapper.firstChild){
spriteOptionsWrapper.removeChild(spriteOptionsWrapper.lastChild);
}
const spriteOptionsTitle = document.createElement('span');
spriteOptionsTitle.className = 'title-span';
spriteOptionsTitle.innerText = 'Alternate Sprites';
spriteOptionsWrapper.appendChild(spriteOptionsTitle);
const spriteOptionsDescription = document.createElement('span');
spriteOptionsDescription.className = 'description-span';
spriteOptionsDescription.innerHTML = 'Choose an alternate sprite to play the game with. Additional randomization ' +
'options are documented in the ' +
'<a href="https://github.com/Berserker66/MultiWorld-Utilities/blob/main/playerSettings.yaml#L374">settings file</a>.';
spriteOptionsWrapper.appendChild(spriteOptionsDescription);
const spriteOptionsTable = document.createElement('table');
spriteOptionsTable.setAttribute('id', 'sprite-options-table');
spriteOptionsTable.className = 'option-set';
const tbody = document.createElement('tbody');
tbody.setAttribute('id', 'sprites-tbody');
const currentPreset = document.getElementById('preset-number').value;
const playerSettings = JSON.parse(localStorage.getItem(`weightedSettings${currentPreset}`));
// Manually add a row for random sprites
addSpriteRow(tbody, playerSettings, 'random');
// Add a row for each sprite currently present in the player's settings
Object.keys(playerSettings.rom.sprite).forEach((spriteName) => {
if(['random'].indexOf(spriteName) > -1) return;
addSpriteRow(tbody, playerSettings, spriteName)
});
spriteOptionsTable.appendChild(tbody);
spriteOptionsWrapper.appendChild(spriteOptionsTable);
};
const buildRangeSettings = (parentElement, settings) => {
// Ensure we are operating on a range-specific setting
if(typeof(settings.inputType) === 'undefined' || settings.inputType !== 'range'){
throw new Error('Invalid input type provided to buildRangeSettings func.');
}
const settingWrapper = document.createElement('div');
settingWrapper.className = 'setting-wrapper';
if(typeof(settings.friendlyName) !== 'undefined' && settings.friendlyName){
const sectionTitle = document.createElement('span');
sectionTitle.className = 'title-span';
sectionTitle.innerText = settings.friendlyName;
settingWrapper.appendChild(sectionTitle);
}
if(settings.description){
const description = document.createElement('span');
description.className = 'description-span';
description.innerText = settings.description;
settingWrapper.appendChild(description);
}
// Create table
const optionSetTable = document.createElement('table');
optionSetTable.className = 'option-set';
// Create table body
const tbody = document.createElement('tbody');
Object.keys(settings.subOptions).forEach((setting) => {
// Overwrite setting key name with real object
setting = settings.subOptions[setting];
const settingId = (Math.random() * 1000000).toString();
// Create rows for each option
const optionRow = document.createElement('tr');
// Option name td
const optionName = document.createElement('td');
optionName.className = 'option-name';
const optionLabel = document.createElement('label');
optionLabel.setAttribute('for', settingId);
optionLabel.setAttribute('data-tooltip', setting.description);
optionLabel.innerText = setting.friendlyName;
optionName.appendChild(optionLabel);
optionRow.appendChild(optionName);
// Option value td
const optionValue = document.createElement('td');
optionValue.className = 'option-value';
const input = document.createElement('input');
input.className = 'setting';
input.setAttribute('id', settingId);
input.setAttribute('type', 'range');
input.setAttribute('min', '0');
input.setAttribute('max', '100');
input.setAttribute('data-setting', setting.keyString);
input.value = setting.defaultValue;
optionValue.appendChild(input);
const valueDisplay = document.createElement('span');
valueDisplay.setAttribute('id', setting.keyString);
valueDisplay.innerText = setting.defaultValue;
optionValue.appendChild(valueDisplay);
optionRow.appendChild(optionValue);
tbody.appendChild(optionRow);
});
optionSetTable.appendChild(tbody);
settingWrapper.appendChild(optionSetTable);
parentElement.appendChild(settingWrapper);
};
const addSpriteRow = (tbody, playerSettings, spriteName) => {
const rowId = (Math.random() * 1000000).toString();
const optionId = (Math.random() * 1000000).toString();
const tr = document.createElement('tr');
tr.setAttribute('id', rowId);
// Option Name
const optionName = document.createElement('td');
optionName.className = 'option-name';
const label = document.createElement('label');
label.htmlFor = optionId;
label.innerText = spriteName;
optionName.appendChild(label);
if(['random', 'random_sprite_on_event'].indexOf(spriteName) === -1) {
const deleteButton = document.createElement('span');
deleteButton.setAttribute('data-sprite', spriteName);
deleteButton.setAttribute('data-row-id', rowId);
deleteButton.innerText = ' (❌)';
deleteButton.className = 'delete-button';
optionName.appendChild(deleteButton);
deleteButton.addEventListener('click', removeSpriteOption);
}
tr.appendChild(optionName);
// Option Value
const optionValue = document.createElement('td');
optionValue.className = 'option-value';
const input = document.createElement('input');
input.className = 'setting';
input.setAttribute('id', optionId);
input.setAttribute('type', 'range');
input.setAttribute('min', '0');
input.setAttribute('max', '100');
input.setAttribute('data-setting', `rom.sprite.${spriteName}`);
input.value = "50";
optionValue.appendChild(input);
// Value display
const valueDisplay = document.createElement('span');
valueDisplay.setAttribute('id', `rom.sprite.${spriteName}`);
valueDisplay.innerText = playerSettings.rom.sprite.hasOwnProperty(spriteName) ?
playerSettings.rom.sprite[spriteName] : '0';
optionValue.appendChild(valueDisplay);
tr.appendChild(optionValue);
tbody.appendChild(tr);
};
const addSpriteOption = (event) => {
const presetNumber = document.getElementById('preset-number').value;
const playerSettings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`));
const spriteName = event.target.getAttribute('data-sprite');
if (Object.keys(playerSettings.rom.sprite).indexOf(spriteName) !== -1) {
// Do not add the same sprite twice
return;
}
// Add option to playerSettings object
playerSettings.rom.sprite[event.target.getAttribute('data-sprite')] = 50;
localStorage.setItem(`weightedSettings${presetNumber}`, JSON.stringify(playerSettings));
// Add <tr> to #sprite-options-table
const tbody = document.getElementById('sprites-tbody');
addSpriteRow(tbody, playerSettings, spriteName);
};
const removeSpriteOption = (event) => {
const presetNumber = document.getElementById('preset-number').value;
const playerSettings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`));
const spriteName = event.target.getAttribute('data-sprite');
// Remove option from playerSettings object
delete playerSettings.rom.sprite[spriteName];
localStorage.setItem(`weightedSettings${presetNumber}`, JSON.stringify(playerSettings));
// Remove <tr> from #sprite-options-table
const tr = document.getElementById(event.target.getAttribute('data-row-id'));
tr.parentNode.removeChild(tr);
};
const buildSpritePicker = (spriteData) => {
const spritePicker = document.createElement('div');
spritePicker.setAttribute('id', 'sprite-picker');
// Build description
const description = document.createElement('span');
description.innerText = 'To add a sprite to your playable list, click the one you want below.';
spritePicker.appendChild(description);
const sprites = document.createElement('div');
sprites.setAttribute('id', 'sprite-picker-sprites');
spriteData.sprites.forEach((sprite) => {
const spriteImg = document.createElement('img');
let spriteGifFile = sprite.file.split('.');
spriteGifFile.pop();
spriteGifFile = spriteGifFile.join('.') + '.gif';
spriteImg.setAttribute('src', `static/generated/sprites/${spriteGifFile}`);
spriteImg.setAttribute('data-sprite', sprite.file.split('.')[0]);
spriteImg.setAttribute('alt', sprite.name);
// Wrap the image in a span to allow for tooltip presence
const imgWrapper = document.createElement('span');
imgWrapper.className = 'sprite-img-wrapper';
imgWrapper.setAttribute('data-tooltip', `${sprite.name}${sprite.author ? `, by ${sprite.author}` : ''}`);
imgWrapper.appendChild(spriteImg);
imgWrapper.setAttribute('data-sprite', sprite.name);
sprites.appendChild(imgWrapper);
imgWrapper.addEventListener('click', addSpriteOption);
});
spritePicker.appendChild(sprites);
return spritePicker;
};
const generateGame = (raceMode = false) => {
const presetNumber = document.getElementById('preset-number').value;
axios.post('/api/generate', {
weights: { player: localStorage.getItem(`weightedSettings${presetNumber}`) },
presetData: { player: localStorage.getItem(`weightedSettings${presetNumber}`) },
playerCount: 1,
race: raceMode ? '1' : '0',
}).then((response) => {
window.location.href = response.data.url;
}).catch((error) => {
const userMessage = document.getElementById('user-message');
userMessage.innerText = 'Something went wrong and your game could not be generated.';
if (error.response.data.text) {
userMessage.innerText += ' ' + error.response.data.text;
}
userMessage.classList.add('visible');
window.scrollTo(0, 0);
console.error(error);
});
};

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

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