Compare commits

...

241 Commits

Author SHA1 Message Date
PoryGone
a3666f2ae5 SA2B: Fix critical typo #4779 2025-03-30 22:19:24 +02:00
Kaito Sinclaire
c3e000e574 id Tech 1 games: Logic updates (Feb '25) (#4677)
- Across Doom 1993 and Doom 2, any items that are accessible in Ultra-Violence from the start of the level without putting the player in any danger are now considered in logic when that level is first received, without needing any weapons available. This is intended to give generation more possible outs for bad placements.
  - This affects the following maps in Doom 1993:
    - Toxin Refinery (E1M3): 1 location.
    - Command Control (E1M4): 1 location.
    - Computer Station (E1M7): 1 location.
    - Deimos Lab (E2M4): 1 location.
    - Tower of Babel (E2M8): 1 location.
    - Unholy Cathedral (E3M5): 1 location.
  - This affects the following maps in Doom 2:
    - The Waste Tunnels (MAP05): 2 locations.
    - Dead Simple (MAP07): 2 locations.
    - The Pit (MAP09): 1 location.
    - Refueling Base (MAP10): 1 location.
    - Nirvana (MAP21): 1 location, except see below.
    - Icon of Sin (MAP30): 9 locations.
    - Grosse (MAP32): 2 locations.
- Doom 2 has had some more significant logical adjustments made.
  - The following Pro tricks have been added to Pro logic:
    - Circle of Death (MAP11): Lowering the exit wall without the Red key by hitting the switch to do so from the nukage. This makes three items previously locked behind the Red key available early, as well as the exit.
    - Suburbs (MAP16): Reaching the exit without any keys, as the gap between the pillar and the wall is large enough to let you through if you position yourself well. While multiple other squeeze glides exist (for example, you can skip the Yellow key in MAP21 by using one), this one is significantly easier than the rest; it does not require much precision, nor does it require vertical mouse movement.
    - Nirvana (MAP21): Skipping the Blue key, as there is a gigantic gap between the bars that attempt to block you.
    - The Chasm (MAP24): Skipping the Blue key by going extremely far through the nukage and finding one of a couple specific teleporters is now considered a Pro trick, and standard logic now expects the key to be obtained.
  - The following levels have had other logic adjustments:
    - The Waste Tunnels (MAP05): Requirements lowered to Shotgun + Super Shotgun + (Chaingun | Plasma gun).
    - The Crusher (MAP06): Requirements lowered to Shotgun + (Chaingun | Plasma gun) for areas immediately accessible. Going beyond the Blue key door also requires Super Shotgun.
    - The Factory (MAP12): The outdoors area, and the little room to the right of where you start, are accessible in sphere 1. These three items are all easily obtainable with only the pistol. The remaining items that are not in the central area are accessible with (Super Shotgun | Plasma gun), while the items in that area are accessible with Super Shotgun + Chaingun + (Plasma gun | BFG9000). This fixes Episode 2 not having an available sphere 1, and allows solo Episode 2 games.
    - Nirvana (MAP21): As above, the item in the starting room is accessible in sphere 1. Every other item that doesn't require a key is accessible with (Super Shotgun | Plasma gun). The room in which you use the Yellow key is accessible with Super Shotgun + Chaingun + (Plasma gun | BFG9000). This fixes Episode 3 not having an available sphere 1, and allows solo Episode 3 games.
    - The Catacombs (MAP22): The four items in the opening room only require (Shotgun | Super Shotgun | Plasma gun). The rest of the level is as before.
    - Bloodfalls (MAP25): Requirements lowered to Shotgun + Super Shotgun + Chaingun, as this level is unusually easy for its placement in the game. Progressing past the Blue key door additionally requires (Rocket launcher | Plasma gun | BFG9000) solely to deal with the Arch-vile at the end of the level.
    - Wolfenstein (MAP31): Requirements lowered to Chaingun + (Shotgun | Super Shotgun). This is closer to what the game expects from a non-secret hunting player from a pistol start.
- The following logic bugs in Heretic have been fixed:
  - Quay (E5M3): An item in a Blue key locked hallway was previously marked as being in the "Main" region, thus considered to be accessible without that key. It has been moved to the appropriate "Blue" region.
  - Courtyard (E5M4): Logic previously assumed you could reach the Wings of Wrath from the opening room, when that isn't actually possible. Changing this moved some items previously in the "Main" region into a new "Green" region, and items previously in the "Kakis" (Yellow OR Green) are now in a "Yellow" region instead. Fixes #4662.
- For known problematic solo episodes, some additional special cases have been added.
  - Doom 1993, Episode 3: One of either the Shotgun or Chaingun is placed early. Slough of Despair (E3M2) is given as an additional starting level.
  - Doom 2, Episode 3: One of either the Super Shotgun or Plasma gun is placed early.
  - Heretic, Episode 1: The Docks (E1M1) - Yellow key is placed early.
- The following levels (and thus, their items and locations) were renamed, due to typos or other oddities:
  - `Barrels o Fun (MAP23)` -> `Barrels o' Fun (MAP23)`
  - `Wolfenstein2 (MAP31)` -> `Wolfenstein (MAP31)`
  - `Grosse2 (MAP32)` -> `Grosse (MAP32)`
  - `D'Sparil'S Keep (E3M8)` -> `D'Sparil's Keep (E3M8)`
  - `The Aquifier (E3M9)` -> `The Aquifer (E3M9)`
2025-03-29 17:32:33 +01:00
Justus Lind
dd5481930a Muse Dash: Update docs to recommend MelonLoader 0.7.0 rather than 0.6.1 (#4776)
* Tiny version update.

* Update wording because there is no longer a latest button
2025-03-29 01:35:35 +01:00
Scipio Wright
842328c661 TUNIC: Update swamp and atoll fuse logic with weaponry (#4760)
* Update swamp and atoll fuse logic with weaponry

* Add it to the swamp and cath rules too
2025-03-28 21:12:16 +01:00
PoryGone
8f75384e2e SA2B - v2.4 Logic Fixes (#4770)
* Logic tweaks

* Docs updates

* Delete extra file

* One more logic tweak

* Add missing logic change
2025-03-28 21:11:31 +01:00
Fabian Dill
193faa00ce Factorio: fix energylink type back to int (#4768) 2025-03-28 00:28:10 +01:00
Star Rauchenberger
5e5383b399 Lingo: Add painting display names (#4707)
* Lingo: Add painting display names

* Reordered some paintings

* Update generated.dat
2025-03-27 01:32:39 +01:00
threeandthreee
cb6b29dbe3 LADX: fix for unconnected entrances in other worlds #4771 2025-03-25 22:30:25 +01:00
Fabian Dill
82b0819051 Core: ensure requirements files end on newline (#4761) 2025-03-24 22:26:30 +01:00
Jérémie Bolduc
e12ab4afa4 Stardew Valley: Move test option presets to their own file (#4349) 2025-03-24 03:32:34 +01:00
Justus Lind
1416f631cc Core: Add a test that checks all registered patches matches the name of a registered world (#4633)
Co-authored-by: qwint <qwint.42@gmail.com>
2025-03-24 03:30:44 +01:00
Fabian Dill
dbaac47d1e Core: update various requirements (#4731) 2025-03-23 17:24:50 +01:00
Jonathan Tan
cf0ae5e31b The Wind Waker: Implement New Game (#4458)
Adds The Legend of Zelda: The Wind Waker as a supported game in Archipelago. The game uses [LagoLunatic's randomizer](https://github.com/LagoLunatic/wwrando) as its base (regarding logic, options, etc.) and builds from there.
2025-03-23 00:42:17 +01:00
BadMagic100
8891f07362 Core: Allow and require user-provided target name when splitting 1-way entrances for GER (#4746)
* [Core][GER] Allow and require user-provided target name when splitting 1-way entrances

* Move target naming onto a parameter of disconnect_entrance_for_randomization
2025-03-22 20:58:35 +01:00
NewSoupVi
d78974ec59 The Witness: Bump Required Client Version to 0.6.0 (#4763)
The beta client releases already report this.
2025-03-22 20:57:22 +01:00
NewSoupVi
32be26c4d7 The Witness: Make sure the 2025 April Fools feature does not go live with RC3 (#4758) 2025-03-22 20:52:18 +01:00
Jérémie Bolduc
9de49aa419 Stardew Valley: Move all the goal logic into its own file (#4383) 2025-03-22 20:29:16 +01:00
PoryGone
294a67a4b4 SA2B: v2.4 - Minigame Madness (#4663)
Changelog:

Features:
- New Goal
  - Minigame Madness
    - Win a certain number of each type of Minigame Trap, then defeat the Finalhazard to win!
	- How many of each Minigame are required can be set by an Option
	- When the required amount of a Minigame has been received, that Minigame can be replayed in the Chao World Lobby
- New optional Location Checks
  - Bigsanity
    - Go fishing with Big in each stage for a Location Check
  - Itemboxsanity
    - Either Extra Life Boxes or All Item Boxes
- New Items
  - New Traps
    - Literature Trap
	- Controller Drift Trap
	- Poison Trap
	- Bee Trap
  - New Minigame Traps
    - Breakout Trap
	- Fishing Trap
	- Trivia Trap
	- Pokemon Trivia Trap
	- Pokemon Count Trap
	- Number Sequence Trap
	- Light Up Path Trap
	- Pinball Trap
	- Math Quiz Trap
	- Snake Trap
	- Input Sequence Trap
- Trap Link
  - When you receive a trap, you send a copy of it to every other player with Trap Link enabled
- Boss Gate Plando
- Expert Logic Difficulty
	- Use at your own risk. This difficulty requires complete mastery of SA2.
- Missions can now be enabled and disabled per-character, instead of just per-style
- Minigame Difficulty can now be set to "Chaos", which selects a new difficulty randomly per-trap received

Quality of Life:
- Gate Stages and Mission Orders are now displayed in the spoiler log
- Additional play stats are saved and displayed with the randomizer credits
- Stage Locations progress UI now displays in multiple pages when Itemboxsanity is enabled
- Current stage mission order and progress are now shown when paused in-level
- Chaos Emeralds are now shown when paused in-level
- Location Name Groups were created
- Moved SA2B to the new Options system
- Option Presets were created
- Error Messages are more obvious

Bug Fixes:
- Added missing `Dry Lagoon - 12 Animals` location
- Flying Dog boss should no longer crash when you have done at least 3 Intermediate Kart Races
- Invincibility can no longer be received in the King Boom Boo fight, preventing a crash
- Chaos Emeralds should no longer disproportionately end up in Cannon's Core or the final Level Gate
- Going into submenus from the pause menu should no longer reset traps
- `Sonic - Magic Gloves` are now plural
- Junk items will no longer cause a crash when in a falling state
- Chao Garden:
	- Prevent races from occasionally becoming uncompletable when using the "Prize Only" option
	- Properly allow Hero Chao to participate in Dark Races
	- Don't allow the Chao Garden to send locations when connected to an invalid server
	- Prevent the Chao Garden from resetting your life count
	- Fix Chao World Entrance Shuffle causing inaccessible Neutral Garden
	- Fix pressing the 'B' button to take you to the proper location in Chao World Entrance Shuffle
	- Prevent Chao Karate progress icon overflow
	- Prevent changing Chao Timescale while paused or while a Minigame is active
- Logic Fixes:
	- `Mission Street - Chao Key 1` (Hard Logic) now requires no upgrades
	- `Mission Street - Chao Key 2` (Hard Logic) now requires no upgrades
	- `Crazy Gadget - Hidden 1` (Standard Logic) now requires `Sonic - Bounce Bracelet` instead of `Sonic - Light Shoes`
	- `Lost Colony - Hidden 1` (Standard Logic) now requires `Eggman - Jet Engine`
	- `Mad Space - Gold Beetle` (Standard Logic) now only requires `Rouge - Iron Boots`
	- `Cosmic Wall - Gold Beetle` (Standard and Hard Logic) now only requires `Eggman - Jet Engine`
2025-03-22 13:00:07 +01:00
panicbit
0e99888926 LADX: Stop spamming location checks over network (#4757) 2025-03-21 17:10:17 +01:00
qwint
74cbf10930 Civ6: Use AutoPatchRegister to make patch downloadable on webhost #4752 2025-03-20 19:28:16 +01:00
BadMagic100
08d2909b0e Hollow Knight: Include Lumafly links to install mods in docs (#4745) 2025-03-20 11:49:55 -04:00
CaitSith2
0949b11436 ALttP: Don't crash generation if sprite paths don't exist (#4725) 2025-03-20 14:48:30 +01:00
Aaron Wagener
9cdffe7f63 The Messenger: Add display names to the plando options (#4748) 2025-03-19 15:52:14 -04:00
Bryce Wilson
8b2a883669 Pokemon Emerald: Update changelog (#4747) 2025-03-19 02:17:01 +01:00
NewSoupVi
b7fc96100c Revert "Core: update websockets (#4732)" (#4753)
This reverts commit 42eaeb92f0.
2025-03-19 01:39:18 +01:00
Aaron Wagener
63cbc00a40 The Messenger: Fix corrupted future rule (#4749) 2025-03-18 19:01:31 -04:00
CodeGorilla
57b94dba6f Options: Add a column for player ID to --csv_output (#4715) 2025-03-17 21:43:00 +01:00
ironminer888
0dd188e108 LADX: Add more specific "item icon guessing" support for some games (#4706)
* DKC3, PKMN R/B/Em, M&L specific item matches

* MLSS Bean types are now discrete

* Add Doom 1/2 items

* Add Doom 1/2 items, actually

* Add Inscryption items

* Add more SA2B items, Minecraft

* Add VVVVVV

* Add misc items, comma fixes

* Hat in Time items

* Misc changes

* Expand TODO

* Add more OoT items, Pokemon consumables

* KH2

* KH1, adjust KH2 items

* Formatting fixes

* more item changes, fix kh1 name

* Fix KH1 name

* Add Full Heal to MEDICINE graphics

* Final comma fixes before PR

* Add Full Restore as Medicine

* Move some names to generic, drink fixes, double-quotes consistency fix

* moved ROCK SMASH match to PHRASES dict

* Removed some redundant name checks, remove Old Amber check from Emerald

* Added "PASS" generic check as "LETTER" sprite

* Removed TODO

* Corrected KH1 name for real this time

* Icon assignment now uppers freogin item string during comparison

* Doom skull keys are now NIGHTMARE_KEY, added QUILL as generic for FEATHER

* KH2 armor is Blunic, accessories are Ribbons

* KH1 accessories/armor are Blunic

* "ROCK SMASH" is now "BOMB"

* Removed extra space
2025-03-17 11:50:57 -04:00
PoryGone
bf8c840293 Celeste 64: v1.3 Content Update (#4581)
### Features:

- New optional Location Checks
	- Checkpointsanity
- Hair Color
	- Allows for setting of Maddy's hair color in each of No Dash, One Dash, Two Dash, and Feather states
- Other Player Ghosts
	- A game config option allows you to see ghosts of other Celeste 64 players in the multiworld

### Quality of Life:

- Checkpoint Warping
	- Received Checkpoint items allow for warping to their respective checkpoint
		- These items are on their respective checkpoint location if Checkpointsanity is disabled
	- Logic accounts for being able to warp to otherwise inaccessible areas
	- Checkpoints are a possible option for a starting item on Standard Logic + Move Shuffle + Checkpointsanity
- New Options toggle to enable/disable background input

### Bug Fixes:

- Traffic Blocks now correctly appear disabled within Cassettes
2025-03-17 02:46:34 +01:00
black-sliver
c0244f3018 Tests: unroll 2 player gen, add parametrization helper, add docs (#4648)
* Tests: unroll test_multiworlds.TestTwoPlayerMulti

Also adds a helper function that other tests can use to unroll tests.

* Docs: add more details to docs/tests.md

* Explain parametrization, subtests and link to the new helper
* Mention some performance details and work-arounds
* Mention multithreading / pytest-xdist

* Tests: make param.classvar_matrix accept sets

* CI: add test/param.py to type checking

* Tests: add missing typing to test/param.py

* Tests: fix typo in test/param.py doc comment

Co-authored-by: qwint <qwint.42@gmail.com>

* update docs

* Docs: reword note on performance

---------

Co-authored-by: qwint <qwint.42@gmail.com>
2025-03-17 00:16:02 +01:00
black-sliver
8af8502202 CI: pin some actions (#4744) 2025-03-17 00:02:00 +01:00
Fabian Dill
42eaeb92f0 Core: update websockets (#4732) 2025-03-16 22:13:12 +01:00
Alchav
7f35eb8867 Pokémon R/B: Allow generating with all items linked (#4330)
* Pokémon R/B: Allow generating with all items linked

* check priority/excluded locations for pc_item

* Update regions.py

* Un-remove regions.py code
2025-03-16 12:33:24 -04:00
BadMagic100
785569c40c Core: Generic ER fails in stage 1 when the last available target is an indirect conditioned dead end (#4679)
* Add test that stage1 ER will not fail due to speculative sweeping an indirect conditioned dead end

* Skip speculative sweep if it's the last entrance placement

* Better implementation of needs_speculative_sweep

* pep8
2025-03-15 18:56:07 +01:00
Scipio Wright
a9eb70a881 OoT: Remove Outdated Spanish Setup Guide (#4736)
* Remove spanish setup guide from webworld

* Update __init__.py

* Update __init__.py
2025-03-15 07:16:06 -04:00
Scipio Wright
5d3d0c8625 WebHost: Update text for options you can't modify (#4614) 2025-03-15 07:10:07 -04:00
Scipio Wright
7e32feeea3 Webhost: Update random option wording on webhost (#4555)
* Update random option wording on webhost

* Update WebHostLib/templates/playerOptions/macros.html

Co-authored-by: Jouramie <16137441+Jouramie@users.noreply.github.com>
2025-03-15 07:09:04 -04:00
neocerber
0d1935e757 SC2: Add a description of mission order and the impact of collect on a SC2 world (#4398)
* Added mission order to randomized stuff, added a mention to the default option collect on goal, added an issue about mission order progress vs AP collect

* Remove false menion of collect being note modifyable after the mworld was gen

* Simplification of some sentences

* American spelling, header newline, and other

* Revert gray to grey, corrected some colors

* Forgot a gray -> grey

* Replace how the faction color option is described to side-step difference within yaml and client. Both fr/en.
2025-03-14 11:35:58 -04:00
Benny D
9b3ee018e9 Core/Various Worlds: Fix crash/freeze with unicode characters (#4671)
replace colorama.init with just_fix_windows_console
2025-03-14 08:24:37 +01:00
NewSoupVi
1de411ec89 The Witness: Change Regions, Areas and Connections from Dict[str, Any] to dataclasses&NamedTuples (#4415)
* Change Regions, Areas and Connections to dataclasses/NamedTuples

* Move to new file

* we do a little renaming

* Purge the 'lambda' naming in favor of 'rule' or 'WitnessRule'

* missed one

* unnecessary change

* omega oops

* NOOOOOOOO

* Merge error

* mypy thing
2025-03-13 23:59:09 +01:00
LiquidCat64
3192799bbf CVCotM: Clarify the Wii U VC version is unsupported (#4734)
* Comment out VC ROM hash usages and clarify that it's unsupported.

* Update worlds/cvcotm/docs/en_Castlevania - Circle of the Moon.md

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* Update worlds/cvcotm/docs/setup_en.md

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

---------

Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2025-03-13 00:21:09 +01:00
Aaron Wagener
2c8dded52f The Messenger: Fix some transition plando issues (#4720)
* don't allow one-way and two-way entrances to be connected to each other

* add special handling for the tower hq nodes since they share the same parent region
2025-03-10 22:13:49 -04:00
justinspatz
06111ac6cf OOT: Have beehives that only appear as a child not be in logic if only adult can break beehives (#4646)
* Change the logic for the 3 Zora's Domain Beehives to support new rule

Implement new logic changes to these 3 locations

* Update LogicHelpers.json with new rule for beehives that only appear for child link

Added below the "can_break_upper_beehive" a new helper called "can_break_upper_beehive_child" which removes the requirement for hookshot to avoid a logic error in the Zora Domain Beehives where it checks whether child or adult can break beehives, even though these beehives do not appear as an adult.

* Update LogicHelpers.json moving the call for is_child

As is_child is already called for can_use (Boomerang), it's a bit redundant to include the check for using the Boomerang, so it's being moved to be with the Bombchu check to ensure that it's not expected if the Bombchu Logic Rule is turned on that Adult can use bombchus to break the beehives. This effectively does the same thing, but should be better on performance.
2025-03-10 17:39:45 +01:00
agilbert1412
d83294efa7 Stardew valley: Fix Aurora Vineyard Tablet logic (#4512)
* - Add requirement on Aurora Vineyard tablet to start the quest

* - Add rule for using the aurora vineyard staircase

* - Added a test for the tablet

* - Add a few missing items to the test

* - Introduce a new item to split the quest from the door and avoir ER issues

* - Optimize imports

* - Forgot to generate the item

* fix Aurora mess

# Conflicts:
#	worlds/stardew_valley/rules.py
#	worlds/stardew_valley/test/mods/TestMods.py

* fix a couple errors in the cherry picked commit, added a method to improve readability and reduce chance of human error on story quest conditions

* - remove blank line

* - Code review comments

* - fixed weird assert name

* - fixed accidentally surviving line

* - Fixed imports

---------

Co-authored-by: Jouramie <16137441+Jouramie@users.noreply.github.com>
2025-03-10 11:39:35 -04:00
Dinopony
be550ff6fb Landstalker: Several small fixes (#4675)
* Landstalker: Fixed duplicate entrance names when using the "No teleport tree requirements" option

* Landstalker: Fixed more cases of duplicate entrance names when using "Shuffle Trees" with open trees

* Landstalker: Fixed endgame locations being present in "Reach Kazalt" goal

* Landstalker: Fixed Lithograph hint pointing at the wrong player

* Landstalker: Updated docs to remove the link to Steam since game got delisted

* Landstalker: Fixed high value hint_count rarely failing at generation

* Landstalker: Fixed dynamic shop prices being potentially invalid in case of a progression balancing (changes by ExemptMedic)
2025-03-10 11:35:58 -04:00
Patrick Lübcke
dd55409209 Pokémon R/B: Fix Rock Tunnel B1F randomization (#4670)
* Bottom to central path sealed off

* Bottom-to-left-path to right path sealed off

* Central opening (r4444): Left unsealed, paths seperated

* Top right half rocks fixed

* Middle to top opening sealed

* Right hallway seal correctly positioned

* Top right ladder: Fixed overlapping walls
2025-03-10 11:35:40 -04:00
Mysteryem
e267714d44 AHiT: Rework Subcon Forest Boss Arena, Boss Firewall and YCHE logic (#4494)
A new `Subcon Forest - Behind Boss Firewall` region is added for
`Subcon Village - Snatcher Statue Chest`. `Subcon Forest Area` connects
to this new region, requiring either the first
`Progressive Painting Unlock`, or Expert logic +
`NoPaintingSkips: false`.

A new `Subcon Forest Boss Arena` region is added for
`Subcon Forest - Boss Arena Chest` because this is immediately
accessible from YCHE. There are connections to this region from
`Your Contract has Expired` (no requirements) and from
`Subcon Forest - Behind Boss Firewall` (requiring either Hard logic or
`Hookshot Badge` + `TOD Access`).

A reverse connection is also added to Expert logic, for
`Subcon Forest Boss Arena` -> `Subcon Forest - Behind Boss Firewall`.
This could be extended to include Hard logic if there is a reasonable
Cherry Bridge setup.

A reverse connection is also added to Expert logic, for
`Subcon Forest - Behind Boss Firewall` -> `Subcon Forest Area`, so long
as `NoPaintingSkips: false` because it is impossible to burn the
paintings to remove the firewall, from behind the firewall.

A new `Your Contract has Expired - Post Fight` region is added for the
Snatcher post fight cutscene to prevent the Snatcher Hover trick giving
access to YCHE, which would otherwise also give access to the new
`Subcon Forest Boss Arena` Region.

The paintings and boss arena gap logic for `Snatcher Statue Chest` and
`Boss Arena Chest` are now handled using the connections to/from these
new regions rather than being on the locations themselves.

The logic for `Act Completion (Toilet of Doom)` remains unchanged
because it has to be in the `Toilet of Doom` region.

In Expert logic, with `NoPaintingSkips: false`, YCHE is added as a rift
access region to Subcon Forest Time Rift entrances.

The `YCHE Access` event is no longer used and has been removed.

- Fixes painting skips logic for Subcon Village - Snatcher Statue Chest
- Fixes Subcon Forest - Boss Arena Chest being inaccessible from YCHE
- Adds Expert logic to reach `Snatcher Statue Chest` from YCHE
- Adds Expert logic to skip the boss firewall in reverse from YCHE so
long as painting skips are not removed from logic
- Adds Expert logic to access Subcon Forest Time Rift entrances from
YCHE so long as painting skips are not removed from logic
2025-03-10 11:34:10 -04:00
Aaron Wagener
7c30c4a169 The Messenger: Transition Shuffle (#4402)
* The Messenger: transition rando

* remove unused import

* always link both directions for plando when using coupled transitions

* er_type was renamed to randomization_type

* use frozenset for things that shouldn't change

* review suggestions

* do portal and transition shuffle in `connect_entrances`

* remove some unnecessary connections that were causing entrance caching collisions

* add test for strictest possible ER settings

* use unittest.skip on the skipped test, so we don't waste time doing setUp and tearDown

* use the world helpers

* make the plando connection description more verbose

* always add searing crags portal if portal shuffle is disabled

* guarantee an arbitrary number of locations with first connection

* make the constraints more lenient for a bit more variety
2025-03-10 11:16:09 -04:00
Alchav
4882366ffc LTTP: Fix TR Big Key Door Entrance Logic (#4712) 2025-03-10 15:56:05 +01:00
Carter Hesterman
5f73c245fc New Game Implementation: Civilization VI (#3736)
* Init

* remove submodule

* Init

* Update docs

* Fix tests

* Update to use apcivvi

* Update Readme and codeowners

* Minor changes

* Remove .value from options (except starting hint)

* Minor updates

* remove unnecessary property

* Cleanup Rules and Region

* Fix output file generation

* Implement feedback

* Remove 'AP' tag and fix issue with format strings and using same quotes

* Update worlds/civ_6/__init__.py

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* Minor docs changes

* minor updates

* Small rework of create items

* Minor updates

* Remove unused variable

* Move client to Launcher Components with rest of similar clients

* Revert "Move client to Launcher Components with rest of similar clients"

This reverts commit f9fd5df9fd.

* modify component

* Fix generation issues

* Fix tests

* Minor change

* Add improvement and test case

* Minor options changes

* .

* Preliminary Review

* Fix failing test due to slot data serialization

* Format json

* Remove exclude missable boosts

* Update options (update goody hut text, make research multiplier a range)

* Update docs punctuation and slot data init

* Move priority/excluded locations into options

* Implement docs PR feedback

* PR Feedback for options

* PR feedback misc

* Update location classification and fix client type

* Fix typings

* Update research cost multiplier

* Remove unnecessary location priority code

* Remove extrenous use of items()

* WIP PR Feedback

* WIP PR Feedback

* Add victory event

* Add option set for death link effect

* PR improvements

* Update post fill hint to support items with multiple classifications

* remove unnecessary len

* Move location exclusion logic

* Update test to use set instead of accidental dict

* Update docs around progressive eras and boost locations

* Update docs for options to be more readable

* Fix issue with filler items and prehints

* Update filler_data to be static

* Update links in docs

* Minor updates and PR feedback

* Update boosts data

* Update era required items

* Update existing techs

* Update existing techs

* move boost data class

* Update reward data

* Update prereq data

* Update new items and progressive districts

* Remove unused code

* Make filler item name func more efficient

* Update death link text

* Move Civ6 to the end of readme

* Fix bug with hidden locations and location.name

* Partial PR Feedback Implementation

* Format changes

* Minor review feedback

* Modify access rules to use list created in generate_early

* Modify boost rules to precalculate requirements

* Remove option checks from access rules

* Fix issue with pre initialized dicts

* Add inno setup for civ6 client

* Update inno_setup.iss

---------

Co-authored-by: Scipio Wright <scipiowright@gmail.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
Co-authored-by: Exempt-Medic <ExemptMedic@Gmail.com>
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2025-03-10 14:53:26 +01:00
NewSoupVi
21ffc0fc54 Band-aid Linux Build breaking with the release of PyGObject 3.52.1 (#4716)
* Band-aid Linux Build breaking with the release of PyGObject 3.52.1

* Update build.yml

* Release workflow as well
2025-03-10 14:43:52 +01:00
Scipio Wright
e95a41cf93 TUNIC: Add another alias for ladders #4714 2025-03-10 14:24:37 +01:00
Silvris
04771fa4f0 Core: fix pickling plando texts (#4711) 2025-03-09 20:00:00 +01:00
jamesbrq
2639796255 MLSS: Add new goal + Update basepatch to standalone equivalent (#4409)
* Item groups + small changes

* Add alternate goal

* New Locations and Logic Updates + Basepatch

* Update basepatch.bsdiff

* Update Basepatch

* Update basepatch.bsdiff

* Update bowsers castle logic with emblem hunt

* Update Archipelago Unittests.run.xml

* Update Archipelago Unittests.run.xml

* Fix for overlapping ROM addresses

* Update Rom.py

* Update __init__.py

* Update basepatch.bsdiff

* Update Rom.py

* Update client with new helper function

* Update basepatch.bsdiff

* Update worlds/mlss/__init__.py

Co-authored-by: qwint <qwint.42@gmail.com>

* Update worlds/mlss/__init__.py

Co-authored-by: qwint <qwint.42@gmail.com>

* Review Refactor

* Review Refactor

---------

Co-authored-by: qwint <qwint.42@gmail.com>
2025-03-09 11:37:15 -04:00
Jérémie Bolduc
4ebabc1208 Stardew Valley: Move filler pool generation out of the world class (#4372)
* merge group options so specific handling is not needed when generating filler pool

* fix

* remove unneeded imports

* self review

* remove unneeded imports

* looks like typing was missing woopsi
2025-03-08 12:13:33 -05:00
josephwhite
ce34b60712 Super Mario 64: ItemData class and tables (#4321)
* sm64ex: use item data class

* rearrange imports

* Dict to dict

* remove optional typing

* bonus item descriptions since we can also add stuff for webworld easily

* remove item descriptions (rip) and decrease verbosity for classifications

* formatting
2025-03-08 12:07:50 -05:00
Trevor L
54094c6331 Blasphemous: Restrict right half of map start locations to hard difficulty only (#4002)
* Start locations, location name

* Fix tests
2025-03-08 11:59:35 -05:00
Bryce Wilson
3986f6f11a Pokemon Emerald: Randomize rock smash encounters (#3912)
* Pokemon Emerald: WIP add rock smash encounter randomization

* Pokemon Emerald: Refactor encounter data on maps

* Pokemon Emerald: Remove unused import

* Pokemon Emerald: Swap StrEnum for regular Enum and use .value
2025-03-08 11:57:16 -05:00
sgrunt
5662da6f7d Timespinner: Support new flags and settings from the randomizer (#4559)
* Timespinner: Add "no hell spiders" enemy rando option that is present in upstream settings

* Timespinner: Prism Break support tweaks (including tracker support)

* Timespinner: Add support for upstream Lock Key Amadeus flag

* Timespinner: Add support for upstream Risky Warps flag

* Timespinner: Add support for upstream Pyramid Start flag

* Timespinner: fix error in lab connectivity logic

* Timespinner: use has_all to simplify one check

Per PR suggestion.

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* Timespinner: fix apparent logic error inherited from in-rando logic

* Timespinner: adjust "Origins" location logic slightly further to account for a Risky Warps case

* Timespinner: remove the backward compat options for the recent flag additions

* Timespinner: add newly added Gate Keep option from rando

* Timespinner: adjust the laser access colours in the tracker

* Timespinner: fix an item description in the tracker

* Timespinner: based on testing feedback, put Laser Access items in their own category

* Timespinner: add support for new upstream flag Royal Roadblock

* Timespinner: also ensure the new flag gets put in slot data

* Timespinner: fix bug in universal tracker support indicating castle basement is accessible at the lower Rising Tides flooding level

* Timespinner: exclude Talaria Attachment and Timespinner Wheel from pyramid start starter progression items

* Timespinner: fix region logic for the left pyramid warp

* Timespinner: fix main Gyre access logic when Risky Warps warps you behind the lasers

* Timespinner: apply suggested spacing fix

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

---------

Co-authored-by: sgrunt <sgrunt1987@gmail.com>
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2025-03-08 11:54:23 -05:00
Scipio Wright
33a75fb2cb TUNIC: Breakable Shuffle (#4489)
* Starting out

* Rules for breakable regions

* make the rest of it work, it's pr ready, boom

* Make it work in not pot shuffle

* Fix after merge

* Fix item id overlap

* Move breakable, grass, and local fill options in yaml

* Fix groups getting overwritten

* Rename, add new breakables

* Rename more stuff

* Time to rename them again

* Make it actually default for breakable shuffle

* Burn the signs down

* Fix west courtyard pot regions

* Fix fortress courtyard and beneath the fortress loc groups again

* More missing loc group conversions

* Replace instances of world.player with player, same for multiworld

* Update worlds/tunic/__init__.py

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

* Remove unused import
2025-03-08 11:25:47 -05:00
Jérémie Bolduc
ee9bcb84b7 Stardew Valley: Move progressive tool options handling in features (#4374)
* create tool progression feature and unwrap option

* replace option usage with calling feature

* add comment explaining why some logic is a weird place

* replace item creation logic with feature

* self review and add unit tests

* rename test cuz I named them too long

* add a test for the trash can useful stuff cuz I thought there was a bug but turns out it works

* self review again

* remove price_multiplier, turns out it's unused during generation

* damn it 3.11 why are you like this

* use blacksmith region when checking vanilla tools

* fix rule

* move can mine using in tool logic

* remove changes to performance test

* properly set the option I guess

* properly set options 2

* that's what happen when you code too late
2025-03-08 11:19:29 -05:00
Kaito Sinclaire
b5269e9aa4 id Tech Games: Customizable ammo capacity (#3565)
* Doom, Doom 2, Heretic: customizable ammo capacity

* Do not progression balance capacity up items

* Prog fill still doesn't agree, just go with our original idea

* Clean up the new options a bit

- Gave all options a consistent and easily readable naming scheme
  (`max_ammo_<type>` and `added_ammo_<type>`)
- Don't show the new options in the spoiler log,
  as they do not affect logic
- Fix the Doom games' Split Backpack option accidentally referring to
  Heretic's Bag of Holding

The logging change across all three games is incidental, as at some
point I did run into that condition by happenstance and it turns out
that it throws an exception due to bad formatting if it's reached

* Do the visibility change for Heretic as well

* Update required client version

* Remove spoiler log restriction on options

* Remove Visibility import now made redundant
2025-03-08 10:37:54 -05:00
Bryce Wilson
00a6ac3a52 BizHawkClient: Store seed name sent by the server for clients to check (#4702) 2025-03-08 16:14:25 +01:00
Bryce Wilson
ea8a14b003 Pokemon Emerald: Some dexsanity locations contribute evolution items (#3187)
* Pokemon Emerald: Change some dexsanity vanilla items to evo items

If a species evolves via item use (Fire Stone, Metal Coat, etc.), use that as it's vanilla item instead of a ball

* Pokemon Emerald: Remove accidentally added print

* Pokemon Emerald: Update changelog

* Pokemon Emerald: Adjust changelog

* Pokemon Emerald: Remove unnecessary else

* Pokemon Emerald: Fix changelog
2025-03-08 10:13:58 -05:00
CaitSith2
414ab86422 LttP: Fix dungeon counter options. (#4704) 2025-03-08 16:13:32 +01:00
Scipio Wright
d4e2698ae0 TUNIC: Add exception handling to deal with duplicate apworlds (#4634)
* Add exception handling to deal with duplicate apworlds

* Update worlds/tunic/__init__.py
2025-03-08 09:56:29 -05:00
JaredWeakStrike
3f8e3082c0 KH2: Client Optimizations and some QoL (#4547)
* adding qwints suggestions

* add stat increase protection and ingame yml stuff

* idk how I forgot these

* reword things

* Update worlds/kh2/Client.py

Co-authored-by: qwint <qwint.42@gmail.com>

* 3.12 compat

* too long of a line

* why didnt I do this before lol

* reading is hard

* missed one

* forgot the self

* fix crash if you get datapackage that isnt kh2

* update to main?

* update to use 0.10 as base and fix violet's base 0 on hex values

* reverting this because I'm bad at my job

---------

Co-authored-by: qwint <qwint.42@gmail.com>
2025-03-08 08:58:59 -05:00
Justus Lind
0f738935ee Muse Dash: Update song list to Cosmic Radio. (#4554)
* MSR Anthology Vol.2 update

* Missing new line.

* Update to Cosmic Radio 2024
2025-03-08 08:58:26 -05:00
kbranch
9c57976252 LADX: Autotracker improvements (#4445)
* Expand and validate the RAM cache

* Part way through location improvement

* Fixed location tracking

* Preliminary entrance tracking support

* Actually send entrance messages

* Store found entrances on the server

* Bit of cleanup

* Added rupee count, items linked to checks

* Send Magpie a handshAck

* Got my own version wrong

* Remove the Beta name

* Only send slot_data if there's something in it

* Ask the server for entrance updates

* Small fix to stabilize Link's location when changing rooms

* Oops, server storage is shared between worlds

* Deal with null responses from the server

* Added UNUSED_KEY item
2025-03-08 13:32:45 +01:00
NewSoupVi
3e08acf381 The Witness: Move local_items code earlier #4696 2025-03-08 12:26:59 +01:00
Exempt-Medic
113259bc15 Update links (#4690)
* Update links

* Update two more
2025-03-07 20:17:45 -05:00
Natalie Weizenbaum
61afe76eae DS3: Remove the outdated French translation of the setup docs (#4700)
This was causing confusion and Discord support requests because the
instructions there are no longer compatible with the latest version of
Archipelago.

This also lists me as the primary author of the new setup guide.
2025-03-08 01:45:52 +01:00
NewSoupVi
08b3b3ecf5 The Witness: The Secret Feature (#4370)
* Secret Feature

* Fixes

* Fixes and unit tests

* renaming some variables

* Fix the thing

* unit test for elevator egg

* Docstring

* reword

* Fix duplicate locations I think?

* Remove debug thing

* Add the tests back lol

* Make it so that you can exclude an egg to disable it

* Improve hint text for easter eggs

* Update worlds/witness/options.py

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* Update worlds/witness/player_logic.py

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* Update worlds/witness/options.py

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* Update worlds/witness/player_logic.py

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* Update worlds/witness/rules.py

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* Update test_easter_egg_shuffle.py

* This was actually not necessary, since this is the Egg requirements, nothing to do with location names

* Move one of them

* Improve logic

* Lol

* Moar

* Adjust unit tests

* option docstring adjustment

* Recommend door shuffle

* Don't overlap IDs

* Option description idk

* Change the way the difficulties work to reward playing higher modes

* Fix merge

* add some stuff to generate_data_file (this file is not imported during gen, don't review it :D)

* oop

* space

* This can be earlier than I thought, apparently.

* buffer

* Comment

* Make sure the option is VERY visible

* Some mypy stuff

* apparently ruff wants this

* .

* durinig

* Update options.py

* Explain the additional effects of each difficulty

* Fix logic of flood room secret

* Add Southern Peninsula Area

* oop

---------

Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2025-03-08 01:44:06 +01:00
Silent
bc61221ec6 TUNIC: Expanded hexagon quest options (#4076)
* More hex quest updates

- Implement page ability shuffle for hex quest
- Fix keys behind bosses if hex goal is less than 3
- Added check to fix conflicting hex quest options
- Add option to slot data

* Change option comparison

* Change option checking and fix some stuff

- also keep prayer first on low hex counts

* Update option defaulting

* Update option checking

* Fix option assignment again

* Show player name in option warning

* Add new option to universal tracker stuff

* Update __init__.py

* Make helper method for getting total hexagons in itempool

* Update options.py

* Update option value passthrough

* Change ability shuffle to default on

* Check for hexagons option when writing spoiler
2025-03-08 01:43:02 +01:00
threeandthreee
2f0b81e12c LADX: tarins gift improvement (#3970)
* add groups and a preset

* formatting

* pull zig's tarin's gift improvements

* typing

* alias groups for progressive items

* change tarins gift option a bit

* add bush breakers item group

* fix typo

* bush_breaker option, respect non_local_items

* review suggestions

* cleaner
thx exempt

* Update worlds/ladx/__init__.py

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

* fix gen failures for dungeon shuffle

* exclude shovel based on entrance mapping

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-03-08 01:24:58 +01:00
threeandthreee
bb9a6bcd2e LADX: more marin joke text (#3966)
* marin text

* Adds lots of Marin Flavour Text (#32)

* Updates of Splash text 24-09-18

* Re-Adds '

* use pkgutil

* Adds all community suggestions up until 20/09/2024 (#33)

* Adds all community suggestions up until 20/09/2024

* cutting deathlink jokes

---------

Co-authored-by: Alex Nordstrom <a.l.nordstrom@gmail.com>

* drop piracy-adjacent jokes

* marin text was too long

* more submissions

* no longer looking for new maintainer

---------

Co-authored-by: palex00 <32203971+palex00@users.noreply.github.com>
2025-03-08 01:19:51 +01:00
Jérémie Bolduc
c8b7ef1016 Stardew Valley: Fix a logic bug where the Tea Sapling would be considered available without having the recipe (#4703) 2025-03-08 00:14:10 +01:00
Silent
e00467c2a2 TUNIC: Update logic for chest in fortress dark area (#4691)
* Update logic for beneath the vault chest

* use helper method instead

so that it checks the lanternless option
2025-03-06 00:18:27 +01:00
Silent
0eb6150e95 TUNIC: Fix rule for some grass in West Garden (#4682) 2025-03-06 00:17:27 +01:00
Fabian Dill
91d977479d Tests: test that collect and remove have expected behaviour. (#2062)
---------

Co-authored-by: qwint <qwint.42@gmail.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-03-05 23:48:03 +01:00
BadMagic100
cd761db170 Core: Do GER speculative sweep membership checks against a set #4698 2025-02-27 19:21:48 +01:00
Aaron Wagener
026011323e The Messenger: Fix 0 Required Power Seals (#4692) 2025-02-27 11:42:41 -05:00
Silvris
adc5f3a07d MM2: Fix Shuffled Weaknesses Seed Bleed (#4689) 2025-02-27 11:13:37 -05:00
BadMagic100
69940374e1 Core: Only consider requested exits during ER placement and speculative sweep #4684 2025-02-27 17:12:35 +01:00
Scipio Wright
6dc461609b Noita: Fix bug with Traps disabled in 1-player games #4651 2025-02-23 17:27:05 +01:00
threeandthreee
58d460678e LADX: drop rupee farm condition (#4189)
* drop rupee farm condition

* cleanup

* rupee farm backup for all spending checks

* not power bracelet

* oops
2025-02-23 17:11:24 +01:00
Scipio Wright
0f7fd48cdd TUNIC: Add some more rules for Monastery connections (#4564)
* Move a couple locations to monastery

* Connect Quarry Back to Monastery

* Quarry Back -> Monastery with laurels, Monastery -> Monastery Back with wand/sword

* Add Monastery Back region

* Move a couple non-ER locations to monastery back

* Monastery front -> back with sword, wand, or laurels zip

* also laurels zip for non-ER
2025-02-23 17:02:30 +01:00
Natalie Weizenbaum
18de035b4d DS3: Update setup documentation (#4437) 2025-02-22 08:33:58 -05:00
Fabian Dill
11fa43f0a4 Factorio: prevent players from getting stuck from Teleport Traps (#4537) 2025-02-20 00:17:19 +01:00
black-sliver
91a8fc91d6 CI: fix native tests toolchain on windows (#4668)
* CI: ctest: fix trigger on CMakeLists change

* CI: ctest: update cmake version

this removes a warning
and matches gtest

* CI: ctest: remove explicit build mode for MSVC

gtest switched to dynamic libc (/MD), which is default, so this just works now
2025-02-19 13:50:25 +01:00
Fabian Dill
15bde56551 Factorio: prevent invalid starting items count (#4658) 2025-02-17 18:58:38 +01:00
NewSoupVi
d744e086ef MultiServer: Fix hinting an item that someone else already hinted in their slot not resolving correctly (#4655)
* Fix get_hint not checking for finding_player

* Fix using the wrong variable for slot lookup
2025-02-17 15:16:18 +01:00
Scipio Wright
378fa5d5c4 Fix gun missing from combat_items, add new for combat logic cache, very slight refactor of check_combat_reqs to let it do the changeover in a less complicated fashion, fix area being a boss area rather than non-boss area for a check (#4657) 2025-02-17 01:30:40 +01:00
black-sliver
8349774c5c customserver: ignore static datapackage optimization for old games (#4650) 2025-02-16 23:51:36 +01:00
qwint
34795b598a GER: Use Itempool Count for Minimal handling (#4649)
* uses itempool count vs unfilled location count instead of counting prog_items values which could have custom counters

* move unfilled location check to before can_reach

* add tests for successful minimal GER call with extra collect override prog_items in the pool to regression test issue fixed in this PR
2025-02-16 20:21:09 +01:00
JoshuaEagles
efd5004330 Docs: Update SA2B Linux and Steam Deck Setup Guide + Add Celeste 64 Linux Setup Guide (#4593)
* Update Linux and Steam Deck setup guide for sa2b

* Add Linux and Steam Deck setup guide for Celeste 64
2025-02-12 17:47:43 +01:00
Matthew Wells
c799531105 Docs: Add missing plural in faq (#4622) 2025-02-12 17:47:17 +01:00
threeandthreee
5c1ded1fe9 LADX: bomb as logical bush breaker #4636 2025-02-12 17:46:43 +01:00
qwint
b2162bb8e6 Docs: clean up create_item/event example (#4596)
* eyes

* remove line wraps where unnecessary
2025-02-12 17:46:07 +01:00
agilbert1412
f1769a8d00 Stardew Valley: Fixed Powdermelon and option inconsistencies (#4632)
* - Fixed powdermelon season

* - Improve cohesion in presets

* - Update several tooltips to be more consistent and accurate
2025-02-12 17:45:03 +01:00
qwint
f520c1d9f2 Launcher: Allow for --nogui client launches (#4549) 2025-02-10 19:34:27 +01:00
PinkSwitch
910369a7f8 Bizhawk Client: Display Err (#4532)
Co-authored-by: Bryce Wilson
2025-02-10 19:27:10 +01:00
qwint
dbf6b6f935 CC: don't try to reconnect on invalid version (#4606) 2025-02-10 19:23:58 +01:00
qwint
e9c463c897 CC: Force Text Client to always connect with empty game (#4607) 2025-02-10 19:23:09 +01:00
qwint
f4e43ca9e0 LttP: mock world.random in adjuster (#4623) 2025-02-10 19:22:06 +01:00
Fabian Dill
a298be9c41 Core: change HINT_FOUND to 40 and HINT_UNSPECIFIED to 0 (#4620) 2025-02-10 19:19:00 +01:00
Fabian Dill
18bcaa85a2 Test: ensure get_all_state() does not error in between steps (#4612) 2025-02-10 19:18:14 +01:00
Scipio Wright
359f45d50f TUNIC: Combat logic fix (#4589)
* Potential fix for attack issue

* also put the lazy version of the swamp fix in for good measure

* fix extra line

* now it is good

* Add the test, roll the other PR into this one

* Make the test exception more useful

* Remove debug print

* Combat logic fixed?

* Move a few areas to before well instead of east forest

* Put in qwint's suggestions in test

* Implement qwint's suggestions in combat_logic.py

* Implement qwint's suggestions for combat_logic.py

* Fix typo

* Remove experimental from combat logic description

* Remove copy_mixin again

* Add comment about copy_mixin

* Use a more proper random

* Some optimizations from Vi's comments
2025-02-09 19:12:17 +01:00
qwint
f5c574c37a Settings: add format handling to yaml exception marks for readability (#4531) 2025-02-09 12:11:27 +01:00
NewSoupVi
f75a1ae117 KH2: Fix lambda capture issue with weapon slot logic (#4604)
* KH2: Fix lambda capture issue with weapon slot logic

* Update Rules.py

* Improved by JaredWeakStrike (#4605)

* Apparently this wasn't meant to be indented

---------

Co-authored-by: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com>
2025-02-08 00:06:04 +01:00
Kory Dondzila
768ccffe72 Shivers: Update shivers links and guides (#4592) 2025-02-07 21:06:06 +01:00
Martmists
f6668997e6 [AHIT] Fix small options issue (#4615) 2025-02-07 21:02:37 +01:00
shananas
db11c620a7 KH2 Doc Update #4609
Mod Manager Version Number
2025-02-04 17:09:02 +01:00
Jouramie
da48af60dc Stardew Valley: add assert_can_reach_region_* for better tests (#4556)
* add assert_reach_region_*; refactor existing assert_reach_location_* to allow string

* rename asserts
2025-02-04 08:27:23 +01:00
massimilianodelliubaldini
19faaa4104 Core: Fix #4595 by using first type's docstring in a union type (#4600)
* Fix #4595: use first type's docstring in a union type.

* Reuse existing import.
2025-02-04 01:49:07 +01:00
Scipio Wright
628252896e TUNIC: Call Combat Logic experimental (#4594)
* Update options.py

* Update options.py
2025-02-03 15:53:56 +01:00
Mysteryem
f28aff6f9a Core: Replace generator creation/iteration in CollectionState methods (#4587)
* Core: Replace generator creation/iteration in CollectionState methods

Using generators in these functions incurs overhead to create the new
generator instance, call the `any`/`all`/`sum` function and have the
`any`/`all`/`sum` function iterate the generator, which in turn iterates
the iterable.

Replacing the use of generators with for loops is faster.

Getting `self.prog_items[player]` once in advance also improves
performance of iterating longer iterables.

* Add comment on the choice of for loops instead of any()/all()/sum()
2025-02-02 15:25:34 +01:00
Fabian Dill
894732be47 kvui: set home folder to non-default (#4590)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2025-02-02 02:53:16 +01:00
Jouramie
051518e72a Stardew Valley: Fix unresolved reference warning and unused imports (#4360)
* fix unresolved reference warning and unused imports

* revert stuff

* just a commit to rerun the tests cuz messenger fail
2025-02-01 22:07:08 +01:00
Spineraks
b7b78dead3 LADX: Fix generation error on minimal accessibility (#4281)
* [LADX] Fix minimal accessibility

* allow_partial for minimal accessibility

* create the correct partial_all_state

* skip our prefills rather than removing after

* dont rebuild our prefill list

---------

Co-authored-by: threeandthreee <a.l.nordstrom@gmail.com>
2025-02-01 22:03:49 +01:00
Jarno
d1167027f4 Core: Make csv options output ignore hidden options (#4539)
* Core: Make csv options output ignore hidden options

* Update Options.py

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

---------

Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
2025-02-01 02:26:59 +01:00
qwint
445c9b22d6 Settings: Handle empty Groups (#4576)
* export empty groups as an empty dict instead of crashing

* Update settings.py

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

* check instance values from self as well

* Apply suggestions from code review

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

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2025-02-01 02:11:04 +01:00
black-sliver
67e8877143 Docs: fix lower limit of valid IDs in network protocol.md (#4579) 2025-01-31 08:38:17 +01:00
agilbert1412
1fe8024b43 Stardew valley: Add Mod Recipes tests (#4580)
* `- Add Craftsanity Mod tests

* - Add the same test for cooking

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2025-01-30 09:19:06 +01:00
agilbert1412
8e14e463e4 Stardew Valley: Radioactive slot machine should be a ginger island check (#4578) 2025-01-30 09:05:51 +01:00
Jouramie
b8666b2562 Stardew Valley: Remove weird magic trap test? (#4570) 2025-01-29 13:56:50 -05:00
Felix R
57afdfda6f meritous: move completion_condition to set_rules (#4567) 2025-01-29 02:03:37 +01:00
black-sliver
738c21c625 Tests: massively improve the memory leak test performance (#4568)
* Tests: massively improve the memory leak test performance

With the growing number of worlds, GC becomes the bottleneck and slows down the test.

* Tests: fix typing in general/test_memory
2025-01-29 01:52:01 +01:00
black-sliver
41898ed640 MultiServer: implement NoText and deprecate uncompressed Websocket connections (#4540)
* MultiServer: add NoText tag and handling

* MultiServer: deprecate and warn for uncompressed connections

* MultiServer: fix missing space in no compression warning
2025-01-29 01:42:46 +01:00
agilbert1412
1ebc9e2ec0 Stardew Valley: Tests: Restructure the tests that validate Mods + ER together, improved performance (#4557)
* - Unrolled and improved the structure of the test for Mods + ER, to improve total performance and performance on individual tests for threading purposes

* Use | instead of Union[]

Co-authored-by: Jouramie <16137441+Jouramie@users.noreply.github.com>

* - Remove unused using

---------

Co-authored-by: Jouramie <16137441+Jouramie@users.noreply.github.com>
2025-01-28 23:19:20 +01:00
Silvris
9466d5274e MM2: fix plando and weakness special cases (#4561) 2025-01-28 21:45:28 +01:00
NewSoupVi
a53bcb4697 KH2: Use int(..., 0) in Client #4562 2025-01-27 23:13:10 +01:00
Exempt-Medic
8c5592e406 KH2: Fix determinism by using tuples instead of sets (#4548) 2025-01-27 11:06:10 -05:00
Bryce Wilson
41055cd963 Pokemon Emerald: Update changelog (#4551) 2025-01-27 17:01:18 +01:00
Scipio Wright
43874b1d28 Noita: Add clarification to check option descriptions (#4553) 2025-01-27 10:27:43 -05:00
Bryce Wilson
b570aa2ec6 Pokemon Emerald: Clean up free fly blacklist (#4552) 2025-01-27 10:25:31 -05:00
Bryce Wilson
c43233120a Pokemon Emerald: Clarify death link and start inventory descriptions (#4517) 2025-01-27 10:24:26 -05:00
Silvris
57a571cc11 KDL3: Fix world access on non-strict open world (#4543)
* Update rules.py

* lambda capture
2025-01-27 01:52:02 +01:00
Fabian Dill
8622cb6204 Factorio: Inventory Spill Traps (#4457) 2025-01-26 22:14:39 +01:00
qwint
90417e0022 CommonClient: Expand on make_gui docstring (#4449)
* adds docstring to make_gui describing what things you might want to change without dealing with kivy/kvui directly (there are better places to document those)

* Update CommonClient.py

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

* Update CommonClient.py

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

---------

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
2025-01-26 13:06:27 +01:00
josephwhite
96b941ed35 Super Mario 64: Add Star Costs to Spoiler (#4544) 2025-01-25 09:36:23 -05:00
Bryce Wilson
1832bac1a3 BizHawkClient: Update README for get_memory_size (#4511) 2025-01-25 09:35:42 -05:00
qwint
86641223c1 Shivers: Stop using get_all_state cache to fix timing issue #4522
Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2025-01-25 00:35:54 +01:00
black-sliver
cc770418f2 MultiServer: optimize PrintJSON for !release (#4545)
* MultiServer: optimize PrintJSON for !release

* MultiServer: safer comparison

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

---------

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
2025-01-24 23:22:33 +01:00
Scipio Wright
513e361764 TUNIC: Fix UT create_item classification (#4514)
Co-authored-by: Silent <110704408+silent-destroyer@users.noreply.github.com>
2025-01-24 17:10:58 -05:00
Silent
ddf7fdccc7 TUNIC: Add Torch Item (#4538)
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2025-01-24 16:57:23 -05:00
Silent
3df2dbe051 TUNIC: Add ability shuffle information to spoiler log (#4498) 2025-01-24 16:55:49 -05:00
Jasper den Brok
3d1d6908c8 Pokemon Emerald: Add Free Fly Blacklist (#4165)
Co-authored-by: Jasper den Brok <jasper.den.brok@gmail.com>
2025-01-24 16:30:21 -05:00
qwint
7474c27372 Core: Add launch function to call launch_subprocess only if multiprocessing is actually necessary (#4237)
* skips opening a subprocess if kivy (and thus the launcher gui) hasn't been loaded so stdin can function as expected on --nogui and similar

* this exists lol

* keep old function around and use new function for CC component

* fix name=None typing
2025-01-24 19:52:12 +01:00
Scipio Wright
bb0948154d TUNIC: Make the standard entrances get made with tuples instead of sets (#4546)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-01-24 12:42:31 -05:00
CookieCat
fa2816822b AHIT: Fix broken link in setup guide (#4524)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-01-23 16:45:11 -05:00
NewSoupVi
5a42c70675 Core: Fix worlds that rely on other worlds having their Entrances connected before connect_entrances, add unit test (#4530)
* unit test that get all state is called with partial entrances before connect_entrances

* fix the two worlds doing it

* lol

* unused import

* Update test/general/test_entrances.py

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

* Update test_entrances.py

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-01-22 14:00:47 +01:00
JaredWeakStrike
949527f9cb KH2: Bug fixes and game update future proofing (#4075)
Co-authored-by: qwint <qwint.42@gmail.com>
2025-01-21 17:28:33 -05:00
Scipio Wright
1a1b7e9cf4 TUNIC: Reduce range end for local_fill option #4534 2025-01-21 18:39:08 +01:00
Fabian Dill
edacb17171 Factorio: remove debug print (#4533) 2025-01-21 16:12:53 +01:00
qwint
33fd9de281 Core: Add Retry to Priority Fill (#4477)
* adds a retry to priority fill in case the one item per player optimization would cause the priority fill to fail to find valid placements

* Update Fill.py

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

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-01-21 00:56:20 +01:00
qwint
a126dee068 HK: some stuff ruff and pycodestyle complained about (#4523) 2025-01-20 23:42:12 +01:00
qwint
e2b942139a HK: Save GrubHuntGoal by value (#4521) 2025-01-20 19:10:29 +01:00
Scipio Wright
823b17c386 TUNIC: Make grass go in the regular location name group too (#4504)
* Make grass go in the normal loc group too

* Make it not overwrite old groups
2025-01-20 17:44:39 +01:00
Chris J.
05d1b2129a Docs: Update ID Overlapping Docs (#4447) 2025-01-20 11:18:09 -05:00
NewSoupVi
436c0a4104 Core: Add connect_entrances world step/stage (#4420)
* Add connect_entrances

* update ER docs

* fix that test, but also ew

* Add a test that asserts the new finalization

* Rewrite test a bit

* rewrite some more

* blank line

* rewrite rewrite rewrite

* rewrite rewrite rewrite

* RE. WRITE.

* oops

* Bruh

* I guess, while we're at it

* giga oops

* It's been a long day

* Switch KH1 over to this design with permission of GICU

* Revert

* Oops

* Bc I like it

* Update locations.py
2025-01-20 16:07:15 +01:00
Scipio Wright
96f469c737 TUNIC: Fix hero relics not being prog if hex quest is on in combat logic #4509 2025-01-20 16:04:39 +01:00
Scipio Wright
4f77abac4f TUNIC: Fix failure in 1-player grass (#4520)
* Fix failure in 1-player grass

* Update worlds/tunic/__init__.py

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

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-01-20 15:53:30 +01:00
massimilianodelliubaldini
d5cd95c7fb Docs: Clarify usage of slot data for trackers in World API doc (#3986)
* Clarify usage of slot data for trackers in world API.

* Typo.

* Update docs/world api.md

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

* Update docs/world api.md

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

* Update docs/world api.md

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

* Update docs/world api.md

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

* Keep to 120 char lines.

---------

Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-01-20 09:01:45 +01:00
Exempt-Medic
a2fbf856ff SMZ3: Change locality options earlier (#4424) 2025-01-19 23:07:01 -05:00
Exempt-Medic
4fa8c43266 FFMQ: Fix collect_item (#4433)
* Fix FFMQ collect_item
2025-01-19 23:06:09 -05:00
qwint
992841a951 CommonClient: abstract url handling so it's importable (#4068)
Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
Co-authored-by: Jouramie <16137441+Jouramie@users.noreply.github.com>
2025-01-20 02:18:36 +01:00
Exempt-Medic
eb3c3d6bf2 FFMQ: Adds Items Accessibility (#4322) 2025-01-19 20:12:44 -05:00
Fabian Dill
39847c5502 WebHost: sort slots by player_id in api blueprint (#4354) 2025-01-20 02:05:07 +01:00
NewSoupVi
130232b457 Core: Make log time an optional arg & setting for Generate.py as well #4312 2025-01-20 01:56:37 +01:00
Doug Hoskisson
ca8ffe583d Zillion: Priority Dead Ends Feature (#4220) 2025-01-19 18:31:09 -05:00
Doug Hoskisson
563794ab83 Zillion: Use Useful Item Classification (#4179) 2025-01-19 18:29:13 -05:00
Mysteryem
9443861849 Zillion: Finalize item locations in either generate_output or fill_slot_data (#4121)
Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
2025-01-19 18:20:45 -05:00
Nicholas Saylor
cbf4bbbca8 OoT Adjuster: Remove per_slot_randoms (#4264) 2025-01-19 18:17:31 -05:00
Silvris
9e353ebb8e SMZ3: Fix Itemlinks with link_replacement #4099 2025-01-19 07:17:12 -05:00
Bryce Wilson
9183e8f9c9 BizHawkClient: Use built-ins for typing (#4508) 2025-01-19 10:23:06 +01:00
Bryce Wilson
0bb657d2c8 Pokemon Emerald: Use new check_locations helper (#4518) 2025-01-19 10:21:54 +01:00
Jouramie
992f192529 Stardew Valley: Improve generation performance by around 11% by moving calculating from rule evaluation to collect (#4231) 2025-01-18 20:36:01 -05:00
Fabian Dill
1c9409cac9 CommonClient: implement check_locations to send missing locations only (#4484)
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
2025-01-19 00:26:42 +01:00
NewSoupVi
005a143e3e MultiServer: Add slot to SetReply packets (#3747)
* Add slot to datastorage set response

* update docs as well
2025-01-18 19:59:26 +01:00
CarlosBor
8732974857 ALttP: update Spanish Setup Docs (#2670)
Co-authored-by: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com>
2025-01-17 21:38:59 -05:00
black-sliver
1ac8349bd4 CI: update pyright (#4506) 2025-01-17 21:30:18 +01:00
qwint
2b9fa89050 Bizhawk: adds typing to bizhawk component launch (#4505) 2025-01-17 21:22:36 +01:00
Doug Hoskisson
23ea3c0efc Core: some low-hanging fruit on the strict type check (#3416)
* Core: some low-hanging fruit on the strict type check

* bump pyright version

* bump pyright version

* bump pyright and remove file that's no longer easy
2025-01-17 20:14:21 +01:00
Pierre-Alain BESSERO
698d27aada OoT: Allow Crowd Control support for Ocarina of Time (Bizhawk) #4501
Changed the name of the default "receive" function in order to work with Crowd Control
2025-01-17 20:06:20 +01:00
Ishigh1
3a46c9fd3e LADX: Closing the client window closes the window (#4350) 2025-01-17 20:05:02 +01:00
black-sliver
9507300939 SoE: update to v050 (#4497)
* Cuts some cutscenes
* Adds meta data for tracker to detect settings
2025-01-17 18:53:29 +01:00
Scipio Wright
0d6db291de TUNIC: Reorder options (#4491)
* Reorder options

* Also make ability shuffling on by default
2025-01-17 18:30:00 +01:00
digiholic
d218dec826 MMBN3: Logic and Bug Fixes, New Checks (#3646)
* PMDs now check to make sure you have enough unlockers for all of them before any are in logic, to avoid softlocks

* Adds Humor and BlckMnd to the pool and sets logic for Villain and Comedian. Patch not yet updated to remove starting inventory

* Adds Serenade as a check

* Fixes hide and seek completion to use proper Yoka Zoo map. Updates bsdiff patch to 1.2

* Adds option for excluding Secret Area, and item/location groups for further customization

* Update worlds/mmbn3/Locations.py

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

* Update worlds/mmbn3/Regions.py

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

* Update worlds/mmbn3/__init__.py

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

* Update worlds/mmbn3/__init__.py

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

* Update worlds/mmbn3/__init__.py

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

* Replaces can_reach generic with can_reach_region or can_reach_location, where applciable

* Unlocker is now a progression item, Excluded Locations is now a Set

* Missed a merge marker

* Excluded locations is no longer a set since you can't append to a set with +=

* Excluded locations is now a set again since you apparent can append to a set with |=

* Replaces more lists with sets. Fixes wording in option descriptions

* Update worlds/mmbn3/__init__.py

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-01-17 08:41:12 -05:00
Aaron Wagener
3d5c277c31 Core: don't log warnings for plando_items and missing lttp options (#3606)
* Core: don't log a warning for the "options" that are valid in a game section but not on the options system

* don't rebuild a set every loop
2025-01-17 08:39:41 -05:00
Mysteryem
a9435dc6bb KH2: Reduce unnecessary packets sent/requested by the client (#4035) 2025-01-16 22:00:29 -05:00
Mysteryem
8f307c226b Core: Fix the distribution of Options.Range.triangular() (#4283) 2025-01-16 21:59:38 -05:00
threeandthreee
4b8f990960 LADX: Swap out invalid characters in item names (#4495) 2025-01-16 21:59:19 -05:00
threeandthreee
3a5a4b89ee LADX: improved warps across unexplored tiles (#4111) 2025-01-16 21:58:49 -05:00
JaredWeakStrike
1485882642 KH2: Fixes abilities overflowing into items and crashing the game (#4384)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-01-16 21:57:41 -05:00
Scipio Wright
2e4f5a64b3 TUNIC: Make the local_fill option load in a specific number of locations (#4488)
* Make it load in a specific number of locations

* TunicLocation -> Location

* Actually shuffle the list
2025-01-17 03:13:37 +01:00
Mysteryem
90f80ce1c1 AHiT: Various logic fixes (#4492)
* Fix Director boss photo logic

The rules were being added to for the "Director" boss in
`set_enemy_rules()`, which didn't exist because the boss created was
called "Conductor" instead.

The name of the boss has been changed to "Director", to match, because
it is more accurate due to DJ Grooves possibly being the boss instead of
The Conductor.

The missing logic was the `Hookshot Badge` requirement, however, the
boss events are only used as part of the `Camera Tourist - All Clear`
location, which requires every boss event to be reachable, and the
Toxic Flower boss also has a `Hookshot Badge` requirement, so the
missing `Hookshot Badge` for the Director boss had no effect on logic.

The boss event locations are hidden from spoiler output, so to get a
spoiler showing the Director boss event accessed before having
`Hookshot Badge`, spoiler output had to be modified to also show the
hidden locations. Example sphere from playthrough that should not be
possible because it gets the `Hookshot Badge` and the `Conductor` event
(now renamed to `Director`) in the same sphere:

```
5: {
  Act Completion (Time Rift - Dead Bird Studio): Relic (Crayon Box)
  Conductor - Dead Bird Studio Basement: Conductor
  Dead Bird Studio (Rift) - Page: Behind Cardboard Planet: Time Piece
  Dead Bird Studio (Rift) - Page: Near Time Rift Gate: Hookshot Badge
  Picture Perfect - Hats Buy Building: Metro Ticket - Blue
  Snatcher - Your Contract has Expired: Snatcher
}
```

* Add missing Hookshot + Painting logic for Toilet boss picture

Includes the Hard logic of crossing the gap with a cherry bridge instead
of hookshot and the expert logic of being able to skip the boss firewall
with a cherry hover.

* Fix Alpine Skyline - Goat Outpost Horn region

`Alpine Skyline - Goat Outpost Horn` is accessible from The Illness has
Spread, but was being added to the region that is only accessible from
Alpine Free Roam. `Alpine Skyline - Goat Outpost Horn` has been moved to
the region that is accessible from both The Illness has Spread and
Alpine Free Roam.

* Add missing HitType.umbrella logic for Top of HQ Coin in Beat the Heat

Like Heating up Mafia Town, the cannon to the Mafia HQ area only opens
once all the faucets have been turned off by hitting them. This requires
the Umbrella when umbrella logic is enabled, but the Snatcher Coin on
top of Mafia HQ was missing this requirement when accessed from Beat the
Heat.

* Add missing Main Objective requirement for auto-completed Bonus Stamps

When a Main Objective is not excluded, but the bonuses are excluded, the
bonuses auto-complete once the Main Objective is completed. The
requirement to complete the Main Objective was missing, so the logic was
incorrectly awarding bonus stamps as soon as a Contract was unlocked,
even when it was not possible to complete the Main Objective of that
Contract.

* Add missing Hookshot requirement for The Arctic Cruise - Toilet from Bon Voyage!

`The Arctic Cruise - Toilet` is accessed from the `Cruise Ship` region,
but it is only present in the Ship Shape and Bon Voyage! acts.

Ship Shape and Rock the Boat can access `Cruise Ship` without any items,
but Bon Voyage! requires the Hookshot Badge to reach `Cruise Ship`.

With how the logic was set up, it was incorrectly giving access to
`The Arctic Cruise - Toilet` if the player had access to Bon Voyage!
but only had access to `Cruise Ship` through Rock the Boat.

* Fix Expert logic Rush Hour-only ticket skips

The code was checking `if not world.options.NoTicketSkips:`, but that
would only be `True` for `False`. For "rush_hour" (for Rush Hour-only
ticket skips), it would be `False`, causing Rush Hour-only ticket skips
to act as if ticket skips were disabled.

* Remove Mystifying Time Mesa: Zipline gaining Hookshot requirement in moderate logic

Alpine Skyline - Mystifying Time Mesa: Zipline does not normally
require Hookshot Badge because it is an implied requirement due to only
being accessible from Alpine Free Roam which does require Hookshot
Badge. In normal logic difficulty, the location does not have an
explicit Hookshot Badge requirement, but moderate logic was adding a
Hookshot Badge requirement. This extraneous Hookshot Badge requirement
has been removed.

* Fix Act Completion (Queen Vanessa's Manor) not being accessible with Dweller Mask/Brewing Hat

It was logically requiring the Umbrella hit type only, whereas all the
other locations in Queen Vanessa's Manor require the Dweller Bell hit
type which additionally allows Dweller Mask or Brewing Hat.

* Remove Dweller Mask requirement for Subcon Forest - Tall Tree Hookshot Swing

The Dweller Mask is not used in the intended vanilla route to get this
item, so this requirement seems to have been a mistake.

* Remove unused SDJ option for Subcon Forest - Long Tree Climb Chest

Hard logic can already reach this location with nothing (other than
paintings), so the "or" logic of being able to perform an SDJ was
unused.

* Require any non-HUMT Mafia Town act for Hot Air Balloon with nothing

Two buckets/beach balls are required to bucket/ball hover, but there is
only a single beach ball accessible in Heating Up Mafia Town, and
no accessible buckets.

There is an alternative strategy for Top of Lighthouse that only
requires a single beach ball, so that location can still be reached with
nothing from Heating Up Mafia Town.

* Use `get_difficulty()` helper in `set_enemy_rules`

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

---------

Co-authored-by: Exempt-Medic <60412657+exempt-medic@users.noreply.github.com>
2025-01-17 03:10:41 +01:00
black-sliver
78904151b0 Test: fix typo in pytest.ini (#4502)
The typo disabled a bunch of tests :S
2025-01-17 02:10:48 +01:00
black-sliver
9d4bd6eebd pytest: only check tests/ and worlds/ (#4500)
This allows having failing tests in CI in worlds_disabled
and allows moving worlds there to disable tests.
2025-01-17 01:53:50 +01:00
black-sliver
5c56dc0357 SoE: fix logic for drain cave with OoB (#4496)
Also adds py3.13 compat and missing hash for sdist
2025-01-17 01:27:36 +01:00
NewSoupVi
c7810823e8 Core: Fix crash when trying to log an exception (#4313)
* Fix crash when trying to log an exception

In https://github.com/ArchipelagoMW/Archipelago/pull/3028, we added a new logging filter which checked `record.msg`. 

However, you can pass whatever you want into a logging call. In this case, what we missed was ecc3094c70/MultiServer.py (L530C1-L530C37), where we pass an Exception object as the message. This currently causes a crash with the new filter.

The logging module supports this. It has no typing and can handle passing objects as messages just fine.

What you're supposed to use, as far as I understand it, is `record.getMessage()` instead of `record.msg`.

* Update Utils.py

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

---------

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
2025-01-16 18:35:07 +01:00
threeandthreee
902d03d447 LADX: Stabilize Item Pool Option (#3935)
Co-authored-by: Scipio Wright <scipiowright@gmail.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-01-15 21:42:19 -05:00
Scipio Wright
b7621a0923 TLoZ: Fix typo in setup guide (#4486) 2025-01-16 00:52:12 +01:00
Silent
b7baaed391 TUNIC: Grass Randomizer (#3913)
* Fix certain items not being added to slot data

* Change where items get added to slot data

* Add initial grass randomizer stuff

* Fix rules

* Update grass.py

Improve location names

* Remove wand and gun from logic

* Update __init__.py

* Fix logic for two pieces of grass in atoll

* Make early bushes only contain grass

* Backport changes to grass rando (#20)

* Backport changes to grass rando

* add_rule instead of set_rule for the special cases, add special cases for back of swamp laurels area cause I should've made a new region for the swamp upper entrance

* Remove item name group for grass

* Update grass rando option descriptions

- Also ignore grass fill for single player games

* Ignore grass fill option for solo rando

* Update er_rules.py

* Fix pre fill issue

* Remove duplicate option

* Add excluded grass locations back

* Hide grass fill option from simple ui options page

* Check for start with sword before setting grass rules

* Update worlds/tunic/options.py

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* Exclude grass from get_filler_item_name

- non-grass rando games were accidentally seeing grass items get shuffled in as filler, which is funny but probably shouldn't happen

* Update worlds/tunic/__init__.py

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* Apply suggestions from code review

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

* change the rest of grass_fill to local_fill

* Filter out grass from filler_items

* remove -> discard

* Update worlds/tunic/__init__.py

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

* change has_stick to has_melee

* Update grass list with combat logic regions

* More fixes from combat logic merge

* Fix some dumb stuff (#21)

* Reorganize pre fill for grass

* Update option value passthrough

* Update __init__.py

* Fix region name

* Make separate pools for the grass and non-grass fills (#22)

* Make separate pools for the grass and non-grass fills

* Update worlds/tunic/__init__.py

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* Fix those things in the PR (#23)

* Use excludable property

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

---------

Co-authored-by: Scipio Wright <scipiowright@gmail.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-01-16 00:17:07 +01:00
Fabian Dill
9dac7d9cc3 MultiServer: update InvalidPacket text for location scouts (#4485) 2025-01-15 21:50:20 +01:00
Star Rauchenberger
1eefe23f11 Lingo: Add speed boost mode (#3989)
* Add speed boost mode

* Update generated.dat

* Modify the actual trap weights option when speed boost mode is on

* EOF newline

* Update generated.dat
2025-01-15 21:13:29 +01:00
Exempt-Medic
207a76d1b5 OoT: Two Bugfixes (#4389) 2025-01-14 16:39:13 -05:00
Fabian Dill
01df35f215 Factorio: fix Evolution Trap crashing bound server (#4366) 2025-01-14 22:24:46 +01:00
NewSoupVi
bedf746f1d MultiServer: Revert hints being created for already found locations #4367 2025-01-14 21:37:10 +01:00
Exempt-Medic
b91a7ac6fb LADX: Move Locality Changes Earlier (#4478) 2025-01-14 13:52:58 -05:00
agilbert1412
79e6beeec3 Stardew Valley: Update Mod Content (#4416) 2025-01-14 12:47:12 -05:00
Exempt-Medic
dae9d4c575 LTTP: Fix Itemlinks (#4479) 2025-01-14 12:34:40 -05:00
Nicholas Saylor
04928bd83d DKC3: Remove unused variables and imports #4302 2025-01-14 10:49:30 +01:00
Scipio Wright
0f3818e711 Utils: Visualize Regions showing the reachable regions in color (#4436)
* Utils with coloring

* Update example use

* Update Utils.py

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

---------

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
2025-01-14 10:45:59 +01:00
threeandthreee
0f1dc6e19c Codeowners: @threeandthreee as LADX maintainer #4216 2025-01-14 01:35:29 +01:00
Exempt-Medic
ffd0c8b341 Blasphemous: Move Locality Changes Earlier (#4422) 2025-01-13 19:34:56 -05:00
Exempt-Medic
6220963195 Tests: No Creating Items/Locations/Regions in __init__ (#4474) 2025-01-13 18:35:44 -05:00
Exempt-Medic
20119e3162 Faxanadu: Fix generations with itemlinks (#4395) 2025-01-13 18:35:01 -05:00
Louis M
4cb8fa3cdd Aquaria: Fixing itemlink not working (#4473) 2025-01-13 20:09:39 +01:00
qwint
93e8613da7 HK: Abstract and default grub counts (#4336) 2025-01-13 11:08:46 -05:00
Aaron Wagener
f9cc19e150 Fill: Crash if there are remaining unfilled locations (#2830) 2025-01-13 10:52:10 -05:00
Sam Merritt
0f1c119c76 Factorio: improve error message for config validation (#4421) 2025-01-13 09:52:21 +01:00
Alchav
4c734b467f LTTP: Shop and Arrow fixes (#4067)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2025-01-13 08:32:59 +01:00
Silvris
1f966ee705 BizhawkClient: set metadata from patch file (#4346) 2025-01-12 19:01:16 +01:00
Nicholas Saylor
172ad4e57d Adventure: Optimize imports (#4300) 2025-01-12 19:00:20 +01:00
Justus Lind
3f935aac13 Muse Dash: Change Data storage from a .txt file to a .py file and Filter Webhost Song Lists correctly (#4234) 2025-01-12 18:59:16 +01:00
qwint
9928639ce2 Docs: Fix Typo in Rich Text Options Flag Documentation (#4462) 2025-01-12 11:01:42 -05:00
Jouramie
0fc722cb28 Stardew Valley: Remove seasonal farming event, use regions instead (#4379) 2025-01-12 11:01:02 -05:00
Bryce Wilson
4edca0ce54 BizHawkClient: Add command to get size of memory domain (#4439)
* Mega Man 2: Remove mm2 commands from client if rom size too small
2025-01-12 08:03:31 +01:00
Bryce Wilson
70942eda8c BizHawkClient: Fix version warning not falling through to regular execution (#4463) 2025-01-12 07:54:48 +01:00
NewSoupVi
adcb2f59ca MultiServer: Correct tying of Context.groups (#4460) 2025-01-11 22:16:01 +01:00
Alchav
29b34ca9fd Pokémon R/B: Fix Route 11-E to Route-12-W logic (#4435) 2025-01-11 01:31:29 +01:00
Fabian Dill
d97ee5d209 Core: update certifi (#4453) 2025-01-10 23:28:57 +01:00
Fabian Dill
c2bd9df0f7 Subnautica: fix typo and remove no longer used logger (#4456) 2025-01-10 23:28:38 +01:00
Scipio Wright
112bfe0933 TUNIC: Logic for Beneath the Vault Bridge Switch #4432 2025-01-10 22:48:15 +01:00
Alchav
96b500679d LTTP: Add missing GT Pre-Moldorm Bomb Wall Logic (#4440) 2025-01-10 22:40:50 +01:00
Scipio Wright
258ea10c52 TUNIC: Modify UT support to make a better pattern (#3860)
* Modify UT support to make a better pattern

* Handle keyerror for logic_rules option

* Missed self.passthrough value setting

* Less laziness for passthrough

* Remove extra newline

* Fix missing using_ut = True, also remove now unnecessary try except since 0.5.1 is out

* New UT thing, it goes in this PR because it's been open for 5 months for a very very tiny change
2025-01-10 21:49:13 +01:00
lordlou
043ba418ec SM generate without rom (#3460)
* - 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

* SM Varia can now generate without ROM

* removed stage_assert_generate
2025-01-10 21:46:17 +01:00
Fabian Dill
894a8571ee kvui: add autocompleting new hint text input (#3535)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>
2025-01-10 20:21:02 +01:00
ruby0b
874197d940 Linux: move the user home Archipelago dir to $XDG_DATA_HOME (#4347)
This affects builds with non-writable installation directories.
Instead of saving data in ~/Archipelago we now use $XDG_DATA_HOME/Archipelago
(defaulting to ~/.local/share/Archipelago).
If ~/Archipelago still exists we move it to the new location and link ~/Archipelago to it.

Motivation: This follows the [XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/latest/)
to at least some degree and doesn't clutter the user's home directory.
2025-01-10 01:27:49 +01:00
agilbert1412
d3ed40cd4d Stardew Valley: Hide the Mods from the simple options page (#4446) 2025-01-08 08:13:32 +01:00
492 changed files with 41862 additions and 7721 deletions

View File

@@ -1,8 +1,21 @@
{
"include": [
"type_check.py",
"../BizHawkClient.py",
"../Patch.py",
"../test/param.py",
"../test/general/test_groups.py",
"../test/general/test_helpers.py",
"../test/general/test_memory.py",
"../test/general/test_names.py",
"../test/multiworld/__init__.py",
"../test/multiworld/test_multiworlds.py",
"../test/netutils/__init__.py",
"../test/programs/__init__.py",
"../test/programs/test_multi_server.py",
"../test/utils/__init__.py",
"../test/webhost/test_descriptions.py",
"../worlds/AutoSNIClient.py",
"../Patch.py"
"type_check.py"
],
"exclude": [

View File

@@ -132,7 +132,7 @@ jobs:
# 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
"${{ env.PYTHON }}" -m pip install --upgrade pip "PyGObject<3.51.0" 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`"

View File

@@ -11,7 +11,7 @@ on:
- '**.hh?'
- '**.hpp'
- '**.hxx'
- '**.CMakeLists'
- '**/CMakeLists.txt'
- '.github/workflows/ctest.yml'
pull_request:
paths:
@@ -21,7 +21,7 @@ on:
- '**.hh?'
- '**.hpp'
- '**.hxx'
- '**.CMakeLists'
- '**/CMakeLists.txt'
- '.github/workflows/ctest.yml'
jobs:
@@ -36,9 +36,9 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: ilammy/msvc-dev-cmd@v1
- uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756
if: startsWith(matrix.os,'windows')
- uses: Bacondish2023/setup-googletest@v1
- uses: Bacondish2023/setup-googletest@49065d1f7a6d21f6134864dd65980fe5dbe06c73
with:
build-type: 'Release'
- name: Build tests

View File

@@ -64,7 +64,7 @@ jobs:
# 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
"${{ env.PYTHON }}" -m pip install --upgrade pip "PyGObject<3.51.0" 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`"

View File

@@ -26,7 +26,7 @@ jobs:
- name: "Install dependencies"
run: |
python -m pip install --upgrade pip pyright==1.1.358
python -m pip install --upgrade pip pyright==1.1.392.post0
python ModuleUpdate.py --append "WebHostLib/requirements.txt" --force --yes
- name: "pyright: strict check on specific files"

2
.gitignore vendored
View File

@@ -4,11 +4,13 @@
*_Spoiler.txt
*.bmbp
*.apbp
*.apcivvi
*.apl2ac
*.apm3
*.apmc
*.apz5
*.aptloz
*.aptww
*.apemerald
*.pyc
*.pyd

View File

@@ -511,7 +511,7 @@ if __name__ == '__main__':
import colorama
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main())
colorama.deinit()

View File

@@ -869,21 +869,40 @@ class CollectionState():
def has(self, item: str, player: int, count: int = 1) -> bool:
return self.prog_items[player][item] >= count
# for loops are specifically used in all/any/count methods, instead of all()/any()/sum(), to avoid the overhead of
# creating and iterating generator instances. In `return all(player_prog_items[item] for item in items)`, the
# argument to all() would be a new generator instance, for example.
def has_all(self, items: Iterable[str], player: int) -> bool:
"""Returns True if each item name of items is in state at least once."""
return all(self.prog_items[player][item] for item in items)
player_prog_items = self.prog_items[player]
for item in items:
if not player_prog_items[item]:
return False
return True
def has_any(self, items: Iterable[str], player: int) -> bool:
"""Returns True if at least one item name of items is in state at least once."""
return any(self.prog_items[player][item] for item in items)
player_prog_items = self.prog_items[player]
for item in items:
if player_prog_items[item]:
return True
return False
def has_all_counts(self, item_counts: Mapping[str, int], player: int) -> bool:
"""Returns True if each item name is in the state at least as many times as specified."""
return all(self.prog_items[player][item] >= count for item, count in item_counts.items())
player_prog_items = self.prog_items[player]
for item, count in item_counts.items():
if player_prog_items[item] < count:
return False
return True
def has_any_count(self, item_counts: Mapping[str, int], player: int) -> bool:
"""Returns True if at least one item name is in the state at least as many times as specified."""
return any(self.prog_items[player][item] >= count for item, count in item_counts.items())
player_prog_items = self.prog_items[player]
for item, count in item_counts.items():
if player_prog_items[item] >= count:
return True
return False
def count(self, item: str, player: int) -> int:
return self.prog_items[player][item]
@@ -911,11 +930,20 @@ class CollectionState():
def count_from_list(self, items: Iterable[str], player: int) -> int:
"""Returns the cumulative count of items from a list present in state."""
return sum(self.prog_items[player][item_name] for item_name in items)
player_prog_items = self.prog_items[player]
total = 0
for item_name in items:
total += player_prog_items[item_name]
return total
def count_from_list_unique(self, items: Iterable[str], player: int) -> int:
"""Returns the cumulative count of items from a list present in state. Ignores duplicates of the same item."""
return sum(self.prog_items[player][item_name] > 0 for item_name in items)
player_prog_items = self.prog_items[player]
total = 0
for item_name in items:
if player_prog_items[item_name] > 0:
total += 1
return total
# item name group related
def has_group(self, item_name_group: str, player: int, count: int = 1) -> bool:

View File

@@ -31,6 +31,7 @@ import ssl
if typing.TYPE_CHECKING:
import kvui
import argparse
logger = logging.getLogger("Client")
@@ -459,6 +460,13 @@ class CommonContext:
await self.send_msgs([payload])
await self.send_msgs([{"cmd": "Get", "keys": ["_read_race_mode"]}])
async def check_locations(self, locations: typing.Collection[int]) -> set[int]:
"""Send new location checks to the server. Returns the set of actually new locations that were sent."""
locations = set(locations) & self.missing_locations
if locations:
await self.send_msgs([{"cmd": 'LocationChecks', "locations": tuple(locations)}])
return locations
async def console_input(self) -> str:
if self.ui:
self.ui.focus_textinput()
@@ -701,8 +709,16 @@ class CommonContext:
logger.exception(msg, exc_info=exc_info, extra={'compact_gui': True})
self._messagebox_connection_loss = self.gui_error(msg, exc_info[1])
def make_gui(self) -> typing.Type["kvui.GameManager"]:
"""To return the Kivy App class needed for run_gui so it can be overridden before being built"""
def make_gui(self) -> "type[kvui.GameManager]":
"""
To return the Kivy `App` class needed for `run_gui` so it can be overridden before being built
Common changes are changing `base_title` to update the window title of the client and
updating `logging_pairs` to automatically make new tabs that can be filled with their respective logger.
ex. `logging_pairs.append(("Foo", "Bar"))`
will add a "Bar" tab which follows the logger returned from `logging.getLogger("Foo")`
"""
from kvui import GameManager
class TextManager(GameManager):
@@ -891,6 +907,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
ctx.disconnected_intentionally = True
ctx.event_invalid_game()
elif 'IncompatibleVersion' in errors:
ctx.disconnected_intentionally = True
raise Exception('Server reported your client version as incompatible. '
'This probably means you have to update.')
elif 'InvalidItemsHandling' in errors:
@@ -1041,6 +1058,32 @@ def get_base_parser(description: typing.Optional[str] = None):
return parser
def handle_url_arg(args: "argparse.Namespace",
parser: "typing.Optional[argparse.ArgumentParser]" = None) -> "argparse.Namespace":
"""
Parse the url arg "archipelago://name:pass@host:port" from launcher into correct launch args for CommonClient
If alternate data is required the urlparse response is saved back to args.url if valid
"""
if not args.url:
return args
url = urllib.parse.urlparse(args.url)
if url.scheme != "archipelago":
if not parser:
parser = get_base_parser()
parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281")
return args
args.url = 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)
return args
def run_as_textclient(*args):
class TextContext(CommonContext):
# Text Mode to use !hint and such with games that have no text entry
@@ -1053,7 +1096,7 @@ def run_as_textclient(*args):
if password_requested and not self.password:
await super(TextContext, self).server_auth(password_requested)
await self.get_username()
await self.send_connect()
await self.send_connect(game="")
def on_package(self, cmd: str, args: dict):
if cmd == "Connected":
@@ -1082,20 +1125,10 @@ def run_as_textclient(*args):
parser.add_argument("url", nargs="?", help="Archipelago connection url")
args = parser.parse_args(args)
# handle if text client is launched using the "archipelago://name:pass@host:port" url from webhost
if args.url:
url = urllib.parse.urlparse(args.url)
if url.scheme == "archipelago":
args.connect = url.netloc
if url.username:
args.name = urllib.parse.unquote(url.username)
if url.password:
args.password = urllib.parse.unquote(url.password)
else:
parser.error(f"bad url, found {args.url}, expected url in form of archipelago://archipelago.gg:38281")
args = handle_url_arg(args, parser=parser)
# use colorama to display colored text highlighting on windows
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main(args))
colorama.deinit()

View File

@@ -261,7 +261,7 @@ if __name__ == '__main__':
parser = get_base_parser()
args = parser.parse_args()
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main(args))
colorama.deinit()

28
Fill.py
View File

@@ -502,7 +502,13 @@ def distribute_items_restrictive(multiworld: MultiWorld,
# "priority fill"
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool,
single_player_placement=single_player, swap=False, on_place=mark_for_locking,
name="Priority", one_item_per_player=False)
name="Priority", one_item_per_player=True, allow_partial=True)
if prioritylocations:
# retry with one_item_per_player off because some priority fills can fail to fill with that optimization
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool,
single_player_placement=single_player, swap=False, on_place=mark_for_locking,
name="Priority Retry", one_item_per_player=False)
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
defaultlocations = prioritylocations + defaultlocations
@@ -571,6 +577,26 @@ def distribute_items_restrictive(multiworld: MultiWorld,
print_data = {"items": items_counter, "locations": locations_counter}
logging.info(f"Per-Player counts: {print_data})")
more_locations = locations_counter - items_counter
more_items = items_counter - locations_counter
for player in multiworld.player_ids:
if more_locations[player]:
logging.error(
f"Player {multiworld.get_player_name(player)} had {more_locations[player]} more locations than items.")
elif more_items[player]:
logging.warning(
f"Player {multiworld.get_player_name(player)} had {more_items[player]} more items than locations.")
if unfilled:
raise FillError(
f"Unable to fill all locations.\n" +
f"Unfilled locations({len(unfilled)}): {unfilled}"
)
else:
logging.warning(
f"Unable to place all items.\n" +
f"Unplaced items({len(unplaced)}): {unplaced}"
)
def flood_items(multiworld: MultiWorld) -> None:
# get items to distribute

View File

@@ -42,7 +42,9 @@ def mystery_argparse():
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_level', default='info', help='Sets log level')
parser.add_argument('--log_level', default=defaults.loglevel, help='Sets log level')
parser.add_argument('--log_time', help="Add timestamps to STDOUT",
default=defaults.logtime, action='store_true')
parser.add_argument("--csv_output", action="store_true",
help="Output rolled player options to csv (made for async multiworld).")
parser.add_argument("--plando", default=defaults.plando_options,
@@ -75,7 +77,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
seed = get_seed(args.seed)
Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level)
Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level, add_timestamp=args.log_time)
random.seed(seed)
seed_name = get_seed_name(random)
@@ -438,7 +440,7 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
if "linked_options" in weights:
weights = roll_linked_options(weights)
valid_keys = set()
valid_keys = {"triggers"}
if "triggers" in weights:
weights = roll_triggers(weights, weights["triggers"], valid_keys)
@@ -497,16 +499,23 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
for option_key, option in world_type.options_dataclass.type_hints.items():
handle_option(ret, game_weights, option_key, option, plando_options)
valid_keys.add(option_key)
for option_key in game_weights:
if option_key in {"triggers", *valid_keys}:
continue
logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers "
f"for player {ret.name}.")
# TODO remove plando_items after moving it to the options system
valid_keys.add("plando_items")
if PlandoOptions.items in plando_options:
ret.plando_items = copy.deepcopy(game_weights.get("plando_items", []))
if ret.game == "A Link to the Past":
# TODO there are still more LTTP options not on the options system
valid_keys |= {"sprite_pool", "sprite", "random_sprite_on_event"}
roll_alttp_settings(ret, game_weights)
# log a warning for options within a game section that aren't determined as valid
for option_key in game_weights:
if option_key in valid_keys:
continue
logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers "
f"for player {ret.name}.")
return ret

View File

@@ -28,6 +28,7 @@ from CommonClient import (CommonContext, get_base_parser, gui_enabled, logger,
from NetUtils import ClientStatus
from worlds.ladx.Common import BASE_ID as LABaseID
from worlds.ladx.GpsTracker import GpsTracker
from worlds.ladx.TrackerConsts import storage_key
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
@@ -100,19 +101,23 @@ class LAClientConstants:
WRamCheckSize = 0x4
WRamSafetyValue = bytearray([0]*WRamCheckSize)
wRamStart = 0xC000
hRamStart = 0xFF80
hRamSize = 0x80
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.cache_start = LAClientConstants.wRamStart
self.cache_size = LAClientConstants.hRamStart + LAClientConstants.hRamSize - LAClientConstants.wRamStart
self.address = address
self.port = port
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
@@ -131,9 +136,14 @@ class RAGameboy():
async def get_retroarch_status(self):
return await self.send_command("GET_STATUS")
def set_cache_limits(self, cache_start, cache_size):
self.cache_start = cache_start
self.cache_size = cache_size
def set_checks_range(self, checks_start, checks_size):
self.checks_start = checks_start
self.checks_size = checks_size
def set_location_range(self, location_start, location_size, critical_addresses):
self.location_start = location_start
self.location_size = location_size
self.critical_location_addresses = critical_addresses
def send(self, b):
if type(b) is str:
@@ -188,21 +198,57 @@ class RAGameboy():
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
attempts = 0
while True:
# RA doesn't let us do an atomic read of a large enough block of RAM
# Some bytes can't change in between reading location_block and hram_block
location_block = await self.read_memory_block(self.location_start, self.location_size)
hram_block = await self.read_memory_block(LAClientConstants.hRamStart, LAClientConstants.hRamSize)
verification_block = await self.read_memory_block(self.location_start, self.location_size)
valid = True
for address in self.critical_location_addresses:
if location_block[address - self.location_start] != verification_block[address - self.location_start]:
valid = False
if valid:
break
attempts += 1
# Shouldn't really happen, but keep it from choking
if attempts > 5:
return
checks_block = await self.read_memory_block(self.checks_start, self.checks_size)
if not await self.check_safe_gameplay():
return
self.cache = cache
self.cache = bytearray(self.cache_size)
start = self.checks_start - self.cache_start
self.cache[start:start + len(checks_block)] = checks_block
start = self.location_start - self.cache_start
self.cache[start:start + len(location_block)] = location_block
start = LAClientConstants.hRamStart - self.cache_start
self.cache[start:start + len(hram_block)] = hram_block
self.last_cache_read = time.time()
async def read_memory_block(self, address: int, size: int):
block = bytearray()
remaining_size = size
while remaining_size:
chunk = await self.async_read_memory(address + len(block), remaining_size)
remaining_size -= len(chunk)
block += chunk
return block
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:
@@ -359,11 +405,12 @@ class LinksAwakeningClient():
auth = binascii.hexlify(await self.gameboy.async_read_memory(0x0134, 12)).decode()
self.auth = auth
async def wait_and_init_tracker(self):
async def wait_and_init_tracker(self, magpie: MagpieBridge):
await self.wait_for_game_ready()
self.tracker = LocationTracker(self.gameboy)
self.item_tracker = ItemTracker(self.gameboy)
self.gps_tracker = GpsTracker(self.gameboy)
magpie.gps_tracker = self.gps_tracker
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
@@ -405,9 +452,11 @@ class LinksAwakeningClient():
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.gameboy.update_cache()
await self.tracker.readChecks(item_get_cb)
await self.item_tracker.readItems()
await self.gps_tracker.read_location()
await self.gps_tracker.read_entrances()
current_health = (await self.gameboy.read_memory_cache([LAClientConstants.wLinkHealth]))[LAClientConstants.wLinkHealth]
if self.deathlink_debounce and current_health != 0:
@@ -457,7 +506,7 @@ class LinksAwakeningContext(CommonContext):
la_task = None
client = None
# TODO: does this need to re-read on reset?
found_checks = []
found_checks = set()
last_resend = time.time()
magpie_enabled = False
@@ -465,6 +514,10 @@ class LinksAwakeningContext(CommonContext):
magpie_task = None
won = False
@property
def slot_storage_key(self):
return f"{self.slot_info[self.slot].name}_{storage_key}"
def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str], magpie: typing.Optional[bool]) -> None:
self.client = LinksAwakeningClient()
self.slot_data = {}
@@ -505,9 +558,17 @@ class LinksAwakeningContext(CommonContext):
self.ui = LADXManager(self)
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
async def send_new_entrances(self, entrances: typing.Dict[str, str]):
# Store the entrances we find on the server for future sessions
message = [{
"cmd": "Set",
"key": self.slot_storage_key,
"default": {},
"want_reply": False,
"operations": [{"operation": "update", "value": entrances}],
}]
async def send_checks(self):
message = [{"cmd": 'LocationChecks', "locations": self.found_checks}]
await self.send_msgs(message)
had_invalid_slot_data = None
@@ -536,14 +597,20 @@ class LinksAwakeningContext(CommonContext):
logger.info("victory!")
await self.send_msgs(message)
self.won = True
async def request_found_entrances(self):
await self.send_msgs([{"cmd": "Get", "keys": [self.slot_storage_key]}])
# Ask for updates so that players can co-op entrances in a seed
await self.send_msgs([{"cmd": "SetNotify", "keys": [self.slot_storage_key]}])
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())
self.found_checks.update(item_ids)
create_task_log_exception(self.check_locations(self.found_checks))
if self.magpie_enabled:
create_task_log_exception(self.magpie.send_new_checks(ladxr_ids))
@@ -560,6 +627,10 @@ class LinksAwakeningContext(CommonContext):
while self.client.auth == None:
await asyncio.sleep(0.1)
# Just return if we're closing
if self.exit_event.is_set():
return
self.auth = self.client.auth
await self.send_connect()
@@ -572,6 +643,12 @@ class LinksAwakeningContext(CommonContext):
if cmd == "ReceivedItems":
for index, item in enumerate(args["items"], start=args["index"]):
self.client.recvd_checks[index] = item
if cmd == "Retrieved" and self.magpie_enabled and self.slot_storage_key in args["keys"]:
self.client.gps_tracker.receive_found_entrances(args["keys"][self.slot_storage_key])
if cmd == "SetReply" and self.magpie_enabled and args["key"] == self.slot_storage_key:
self.client.gps_tracker.receive_found_entrances(args["value"])
async def sync(self):
sync_msg = [{'cmd': 'Sync'}]
@@ -585,6 +662,12 @@ class LinksAwakeningContext(CommonContext):
checkMetadataTable[check.id])] for check in ladxr_checks]
self.new_checks(checks, [check.id for check in ladxr_checks])
for check in ladxr_checks:
if check.value and check.linkedItem:
linkedItem = check.linkedItem
if 'condition' not in linkedItem or linkedItem['condition'](self.slot_data):
self.client.item_tracker.setExtraItem(check.linkedItem['item'], check.linkedItem['qty'])
async def victory():
await self.send_victory()
@@ -618,21 +701,36 @@ class LinksAwakeningContext(CommonContext):
if not self.client.recvd_checks:
await self.sync()
await self.client.wait_and_init_tracker()
await self.client.wait_and_init_tracker(self.magpie)
min_tick_duration = 0.1
last_tick = time.time()
while True:
await self.client.main_tick(on_item_get, victory, deathlink)
await asyncio.sleep(0.1)
now = time.time()
tick_duration = now - last_tick
sleep_duration = max(min_tick_duration - tick_duration, 0)
await asyncio.sleep(sleep_duration)
last_tick = now
if self.last_resend + 5.0 < now:
self.last_resend = now
await self.send_checks()
await self.check_locations(self.found_checks)
if self.magpie_enabled:
try:
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)
self.magpie.slot_data = self.slot_data
if self.client.gps_tracker.needs_found_entrances:
await self.request_found_entrances()
self.client.gps_tracker.needs_found_entrances = False
new_entrances = await self.magpie.send_gps(self.client.gps_tracker)
if new_entrances:
await self.send_new_entrances(new_entrances)
except Exception:
# Don't let magpie errors take out the client
pass
@@ -701,6 +799,6 @@ async def main():
await ctx.shutdown()
if __name__ == '__main__':
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main())
colorama.deinit()

View File

@@ -33,10 +33,15 @@ WINDOW_MIN_HEIGHT = 525
WINDOW_MIN_WIDTH = 425
class AdjusterWorld(object):
class AdjusterSubWorld(object):
def __init__(self, random):
self.random = random
def __init__(self, sprite_pool):
import random
self.sprite_pool = {1: sprite_pool}
self.per_slot_randoms = {1: random}
self.worlds = {1: self.AdjusterSubWorld(random)}
class ArgumentDefaultsHelpFormatter(argparse.RawTextHelpFormatter):

View File

@@ -370,7 +370,7 @@ if __name__ == "__main__":
import colorama
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main())
colorama.deinit()

View File

@@ -148,7 +148,8 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
else:
multiworld.worlds[1].options.non_local_items.value = set()
multiworld.worlds[1].options.local_items.value = set()
AutoWorld.call_all(multiworld, "connect_entrances")
AutoWorld.call_all(multiworld, "generate_basic")
# remove starting inventory from pool items.

View File

@@ -28,9 +28,11 @@ ModuleUpdate.update()
if typing.TYPE_CHECKING:
import ssl
from NetUtils import ServerConnection
import websockets
import colorama
import websockets
from websockets.extensions.permessage_deflate import PerMessageDeflate
try:
# ponyorm is a requirement for webhost, not default server, so may not be importable
from pony.orm.dbapiprovider import OperationalError
@@ -45,7 +47,7 @@ from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, Networ
from BaseClasses import ItemClassification
min_client_version = Version(0, 1, 6)
colorama.init()
colorama.just_fix_windows_console()
def remove_from_list(container, value):
@@ -119,13 +121,14 @@ def get_saving_second(seed_name: str, interval: int = 60) -> int:
class Client(Endpoint):
version = Version(0, 0, 0)
tags: typing.List[str] = []
tags: typing.List[str]
remote_items: bool
remote_start_inventory: bool
no_items: bool
no_locations: bool
no_text: bool
def __init__(self, socket: websockets.WebSocketServerProtocol, ctx: Context):
def __init__(self, socket: "ServerConnection", ctx: Context) -> None:
super().__init__(socket)
self.auth = False
self.team = None
@@ -175,6 +178,7 @@ class Context:
"compatibility": int}
# team -> slot id -> list of clients authenticated to slot.
clients: typing.Dict[int, typing.Dict[int, typing.List[Client]]]
endpoints: list[Client]
locations: LocationStore # typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int]]]
location_checks: typing.Dict[typing.Tuple[int, int], typing.Set[int]]
hints_used: typing.Dict[typing.Tuple[int, int], int]
@@ -364,18 +368,28 @@ class Context:
return True
def broadcast_all(self, msgs: typing.List[dict]):
msgs = self.dumper(msgs)
endpoints = (endpoint for endpoint in self.endpoints if endpoint.auth)
async_start(self.broadcast_send_encoded_msgs(endpoints, msgs))
msg_is_text = all(msg["cmd"] == "PrintJSON" for msg in msgs)
data = self.dumper(msgs)
endpoints = (
endpoint
for endpoint in self.endpoints
if endpoint.auth and not (msg_is_text and endpoint.no_text)
)
async_start(self.broadcast_send_encoded_msgs(endpoints, data))
def broadcast_text_all(self, text: str, additional_arguments: dict = {}):
self.logger.info("Notice (all): %s" % text)
self.broadcast_all([{**{"cmd": "PrintJSON", "data": [{ "text": text }]}, **additional_arguments}])
def broadcast_team(self, team: int, msgs: typing.List[dict]):
msgs = self.dumper(msgs)
endpoints = (endpoint for endpoint in itertools.chain.from_iterable(self.clients[team].values()))
async_start(self.broadcast_send_encoded_msgs(endpoints, msgs))
msg_is_text = all(msg["cmd"] == "PrintJSON" for msg in msgs)
data = self.dumper(msgs)
endpoints = (
endpoint
for endpoint in itertools.chain.from_iterable(self.clients[team].values())
if not (msg_is_text and endpoint.no_text)
)
async_start(self.broadcast_send_encoded_msgs(endpoints, data))
def broadcast(self, endpoints: typing.Iterable[Client], msgs: typing.List[dict]):
msgs = self.dumper(msgs)
@@ -389,13 +403,13 @@ class Context:
await on_client_disconnected(self, endpoint)
def notify_client(self, client: Client, text: str, additional_arguments: dict = {}):
if not client.auth:
if not client.auth or client.no_text:
return
self.logger.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
async_start(self.send_msgs(client, [{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments}]))
def notify_client_multiple(self, client: Client, texts: typing.List[str], additional_arguments: dict = {}):
if not client.auth:
if not client.auth or client.no_text:
return
async_start(self.send_msgs(client,
[{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments}
@@ -444,7 +458,7 @@ class Context:
self.slot_info = decoded_obj["slot_info"]
self.games = {slot: slot_info.game for slot, slot_info in self.slot_info.items()}
self.groups = {slot: slot_info.group_members for slot, slot_info in self.slot_info.items()
self.groups = {slot: set(slot_info.group_members) for slot, slot_info in self.slot_info.items()
if slot_info.type == SlotType.group}
self.clients = {0: {}}
@@ -743,23 +757,24 @@ class Context:
concerns[player].append(data)
if not hint.local and data not in concerns[hint.finding_player]:
concerns[hint.finding_player].append(data)
# remember hints in all cases
# since hints are bidirectional, finding player and receiving player,
# we can check once if hint already exists
if hint not in self.hints[team, hint.finding_player]:
self.hints[team, hint.finding_player].add(hint)
new_hint_events.add(hint.finding_player)
for player in self.slot_set(hint.receiving_player):
self.hints[team, player].add(hint)
new_hint_events.add(player)
# only remember hints that were not already found at the time of creation
if not hint.found:
# since hints are bidirectional, finding player and receiving player,
# we can check once if hint already exists
if hint not in self.hints[team, hint.finding_player]:
self.hints[team, hint.finding_player].add(hint)
new_hint_events.add(hint.finding_player)
for player in self.slot_set(hint.receiving_player):
self.hints[team, player].add(hint)
new_hint_events.add(player)
self.logger.info("Notice (Team #%d): %s" % (team + 1, format_hint(self, team, hint)))
for slot in new_hint_events:
self.on_new_hint(team, slot)
for slot, hint_data in concerns.items():
if recipients is None or slot in recipients:
clients = self.clients[team].get(slot)
clients = filter(lambda c: not c.no_text, self.clients[team].get(slot, []))
if not clients:
continue
client_hints = [datum[1] for datum in sorted(hint_data, key=lambda x: x[0].finding_player != slot)]
@@ -768,7 +783,7 @@ class Context:
def get_hint(self, team: int, finding_player: int, seeked_location: int) -> typing.Optional[Hint]:
for hint in self.hints[team, finding_player]:
if hint.location == seeked_location:
if hint.location == seeked_location and hint.finding_player == finding_player:
return hint
return None
@@ -818,7 +833,7 @@ def update_aliases(ctx: Context, team: int):
async_start(ctx.send_encoded_msgs(client, cmd))
async def server(websocket, path: str = "/", ctx: Context = None):
async def server(websocket: "ServerConnection", path: str = "/", ctx: Context = None) -> None:
client = Client(websocket, ctx)
ctx.endpoints.append(client)
@@ -909,6 +924,10 @@ async def on_client_joined(ctx: Context, client: Client):
"If your client supports it, "
"you may have additional local commands you can list with /help.",
{"type": "Tutorial"})
if not any(isinstance(extension, PerMessageDeflate) for extension in client.socket.extensions):
ctx.notify_client(client, "Warning: your client does not support compressed websocket connections! "
"It may stop working in the future. If you are a player, please report this to the "
"client's developer.")
ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc)
@@ -1059,21 +1078,37 @@ def send_items_to(ctx: Context, team: int, target_slot: int, *items: NetworkItem
def register_location_checks(ctx: Context, team: int, slot: int, locations: typing.Iterable[int],
count_activity: bool = True):
slot_locations = ctx.locations[slot]
new_locations = set(locations) - ctx.location_checks[team, slot]
new_locations.intersection_update(ctx.locations[slot]) # ignore location IDs unknown to this multidata
new_locations.intersection_update(slot_locations) # ignore location IDs unknown to this multidata
if new_locations:
if count_activity:
ctx.client_activity_timers[team, slot] = datetime.datetime.now(datetime.timezone.utc)
sortable: list[tuple[int, int, int, int]] = []
for location in new_locations:
item_id, target_player, flags = ctx.locations[slot][location]
# extract all fields to avoid runtime overhead in LocationStore
item_id, target_player, flags = slot_locations[location]
# sort/group by receiver and item
sortable.append((target_player, item_id, location, flags))
info_texts: list[dict[str, typing.Any]] = []
for target_player, item_id, location, flags in sorted(sortable):
new_item = NetworkItem(item_id, location, slot, flags)
send_items_to(ctx, team, target_player, new_item)
ctx.logger.info('(Team #%d) %s sent %s to %s (%s)' % (
team + 1, ctx.player_names[(team, slot)], ctx.item_names[ctx.slot_info[target_player].game][item_id],
ctx.player_names[(team, target_player)], ctx.location_names[ctx.slot_info[slot].game][location]))
info_text = json_format_send_event(new_item, target_player)
ctx.broadcast_team(team, [info_text])
if len(info_texts) >= 140:
# split into chunks that are close to compression window of 64K but not too big on the wire
# (roughly 1300-2600 bytes after compression depending on repetitiveness)
ctx.broadcast_team(team, info_texts)
info_texts.clear()
info_texts.append(json_format_send_event(new_item, target_player))
ctx.broadcast_team(team, info_texts)
del info_texts
del sortable
ctx.location_checks[team, slot] |= new_locations
send_new_items(ctx)
@@ -1100,7 +1135,7 @@ def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, st
seeked_item_id = item if isinstance(item, int) else ctx.item_names_for_game(ctx.games[slot])[item]
for finding_player, location_id, item_id, receiving_player, item_flags \
in ctx.locations.find_item(slots, seeked_item_id):
prev_hint = ctx.get_hint(team, slot, location_id)
prev_hint = ctx.get_hint(team, finding_player, location_id)
if prev_hint:
hints.append(prev_hint)
else:
@@ -1786,7 +1821,9 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
ctx.clients[team][slot].append(client)
client.version = args['version']
client.tags = args['tags']
client.no_locations = 'TextOnly' in client.tags or 'Tracker' in client.tags
client.no_locations = "TextOnly" in client.tags or "Tracker" in client.tags
# set NoText for old PopTracker clients that predate the tag to save traffic
client.no_text = "NoText" in client.tags or ("PopTracker" in client.tags and client.version < (0, 5, 1))
connected_packet = {
"cmd": "Connected",
"team": client.team, "slot": client.slot,
@@ -1859,6 +1896,9 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
client.tags = args["tags"]
if set(old_tags) != set(client.tags):
client.no_locations = 'TextOnly' in client.tags or 'Tracker' in client.tags
client.no_text = "NoText" in client.tags or (
"PopTracker" in client.tags and client.version < (0, 5, 1)
)
ctx.broadcast_text_all(
f"{ctx.get_aliased_name(client.team, client.slot)} (Team #{client.team + 1}) has changed tags "
f"from {old_tags} to {client.tags}.",
@@ -1887,7 +1927,8 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
for location in args["locations"]:
if type(location) is not int:
await ctx.send_msgs(client,
[{'cmd': 'InvalidPacket', "type": "arguments", "text": 'LocationScouts',
[{'cmd': 'InvalidPacket', "type": "arguments",
"text": 'Locations has to be a list of integers',
"original_cmd": cmd}])
return
@@ -1990,6 +2031,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
args["cmd"] = "SetReply"
value = ctx.stored_data.get(args["key"], args.get("default", 0))
args["original_value"] = copy.copy(value)
args["slot"] = client.slot
for operation in args["operations"]:
func = modify_functions[operation["operation"]]
value = func(value, operation["value"])

View File

@@ -5,17 +5,18 @@ import enum
import warnings
from json import JSONEncoder, JSONDecoder
import websockets
if typing.TYPE_CHECKING:
from websockets import WebSocketServerProtocol as ServerConnection
from Utils import ByValue, Version
class HintStatus(ByValue, enum.IntEnum):
HINT_FOUND = 0
HINT_UNSPECIFIED = 1
HINT_UNSPECIFIED = 0
HINT_NO_PRIORITY = 10
HINT_AVOID = 20
HINT_PRIORITY = 30
HINT_FOUND = 40
class JSONMessagePart(typing.TypedDict, total=False):
@@ -151,7 +152,7 @@ decode = JSONDecoder(object_hook=_object_hook).decode
class Endpoint:
socket: websockets.WebSocketServerProtocol
socket: "ServerConnection"
def __init__(self, socket):
self.socket = socket

View File

@@ -1,7 +1,6 @@
import tkinter as tk
import argparse
import logging
import random
import os
import zipfile
from itertools import chain
@@ -197,7 +196,6 @@ def set_icon(window):
def adjust(args):
# Create a fake multiworld and OOTWorld to use as a base
multiworld = MultiWorld(1)
multiworld.per_slot_randoms = {1: random}
ootworld = OOTWorld(multiworld, 1)
# Set options in the fake OOTWorld
for name, option in chain(cosmetic_options.items(), sfx_options.items()):

View File

@@ -346,7 +346,7 @@ if __name__ == '__main__':
import colorama
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main())
colorama.deinit()

View File

@@ -137,7 +137,7 @@ class Option(typing.Generic[T], metaclass=AssembleOptions):
If this is False, the docstring is instead interpreted as plain text, and
displayed as-is on the WebHost with whitespace preserved.
If this is None, it inherits the value of `World.rich_text_options_doc`. For
If this is None, it inherits the value of `WebWorld.rich_text_options_doc`. For
backwards compatibility, this defaults to False, but worlds are encouraged to
set it to True and use reStructuredText for their Option documentation.
@@ -689,9 +689,9 @@ class Range(NumericOption):
@classmethod
def weighted_range(cls, text) -> Range:
if text == "random-low":
return cls(cls.triangular(cls.range_start, cls.range_end, cls.range_start))
return cls(cls.triangular(cls.range_start, cls.range_end, 0.0))
elif text == "random-high":
return cls(cls.triangular(cls.range_start, cls.range_end, cls.range_end))
return cls(cls.triangular(cls.range_start, cls.range_end, 1.0))
elif text == "random-middle":
return cls(cls.triangular(cls.range_start, cls.range_end))
elif text.startswith("random-range-"):
@@ -717,11 +717,11 @@ class Range(NumericOption):
f"{random_range[0]}-{random_range[1]} is outside allowed range "
f"{cls.range_start}-{cls.range_end} for option {cls.__name__}")
if text.startswith("random-range-low"):
return cls(cls.triangular(random_range[0], random_range[1], random_range[0]))
return cls(cls.triangular(random_range[0], random_range[1], 0.0))
elif text.startswith("random-range-middle"):
return cls(cls.triangular(random_range[0], random_range[1]))
elif text.startswith("random-range-high"):
return cls(cls.triangular(random_range[0], random_range[1], random_range[1]))
return cls(cls.triangular(random_range[0], random_range[1], 1.0))
else:
return cls(random.randint(random_range[0], random_range[1]))
@@ -739,8 +739,16 @@ class Range(NumericOption):
return str(self.value)
@staticmethod
def triangular(lower: int, end: int, tri: typing.Optional[int] = None) -> int:
return int(round(random.triangular(lower, end, tri), 0))
def triangular(lower: int, end: int, tri: float = 0.5) -> int:
"""
Integer triangular distribution for `lower` inclusive to `end` inclusive.
Expects `lower <= end` and `0.0 <= tri <= 1.0`. The result of other inputs is undefined.
"""
# Use the continuous range [lower, end + 1) to produce an integer result in [lower, end].
# random.triangular is actually [a, b] and not [a, b), so there is a very small chance of getting exactly b even
# when a != b, so ensure the result is never more than `end`.
return min(end, math.floor(random.triangular(0.0, 1.0, tri) * (end - lower + 1) + lower))
class NamedRange(Range):
@@ -1571,10 +1579,11 @@ def dump_player_options(multiworld: MultiWorld) -> None:
player_output = {
"Game": multiworld.game[player],
"Name": multiworld.get_player_name(player),
"ID": player,
}
output.append(player_output)
for option_key, option in world.options_dataclass.type_hints.items():
if issubclass(Removed, option):
if option.visibility == Visibility.none:
continue
display_name = getattr(option, "display_name", option_key)
player_output[display_name] = getattr(world.options, option_key).current_option_name
@@ -1583,7 +1592,7 @@ def dump_player_options(multiworld: MultiWorld) -> None:
game_option_names.append(display_name)
with open(output_path(f"generate_{multiworld.seed_name}.csv"), mode="w", newline="") as file:
fields = ["Game", "Name", *all_option_names]
fields = ["ID", "Game", "Name", *all_option_names]
writer = DictWriter(file, fields)
writer.writeheader()
writer.writerows(output)

View File

@@ -80,6 +80,8 @@ Currently, the following games are supported:
* Saving Princess
* Castlevania: Circle of the Moon
* Inscryption
* Civilization VI
* The Legend of Zelda: The Wind Waker
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled

View File

@@ -735,6 +735,6 @@ async def main() -> None:
if __name__ == '__main__':
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main())
colorama.deinit()

View File

@@ -500,7 +500,7 @@ def main():
import colorama
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(_main())
colorama.deinit()

View File

@@ -152,8 +152,15 @@ def home_path(*path: str) -> str:
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)
xdg_data_home = os.getenv('XDG_DATA_HOME', os.path.expanduser('~/.local/share'))
home_path.cached_path = xdg_data_home + '/Archipelago'
if not os.path.isdir(home_path.cached_path):
legacy_home_path = os.path.expanduser('~/Archipelago')
if os.path.isdir(legacy_home_path):
os.renames(legacy_home_path, home_path.cached_path)
os.symlink(home_path.cached_path, legacy_home_path)
else:
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
@@ -436,7 +443,8 @@ class RestrictedUnpickler(pickle.Unpickler):
else:
mod = importlib.import_module(module)
obj = getattr(mod, name)
if issubclass(obj, (self.options_module.Option, self.options_module.PlandoConnection)):
if issubclass(obj, (self.options_module.Option, self.options_module.PlandoConnection,
self.options_module.PlandoText)):
return obj
# Forbid everything else.
raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden")
@@ -514,8 +522,8 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO,
def filter(self, record: logging.LogRecord) -> bool:
return self.condition(record)
file_handler.addFilter(Filter("NoStream", lambda record: not getattr(record, "NoFile", False)))
file_handler.addFilter(Filter("NoCarriageReturn", lambda record: '\r' not in record.msg))
file_handler.addFilter(Filter("NoStream", lambda record: not getattr(record, "NoFile", False)))
file_handler.addFilter(Filter("NoCarriageReturn", lambda record: '\r' not in record.getMessage()))
root_logger.addHandler(file_handler)
if sys.stdout:
formatter = logging.Formatter(fmt='[%(asctime)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
@@ -933,7 +941,7 @@ def freeze_support() -> None:
def visualize_regions(root_region: Region, file_name: str, *,
show_entrance_names: bool = False, show_locations: bool = True, show_other_regions: bool = True,
linetype_ortho: bool = True) -> None:
linetype_ortho: bool = True, regions_to_highlight: set[Region] | None = None) -> None:
"""Visualize the layout of a world as a PlantUML diagram.
:param root_region: The region from which to start the diagram from. (Usually the "Menu" region of your world.)
@@ -949,16 +957,22 @@ def visualize_regions(root_region: Region, file_name: str, *,
Items without ID will be shown in italics.
:param show_other_regions: (default True) If enabled, regions that can't be reached by traversing exits are shown.
:param linetype_ortho: (default True) If enabled, orthogonal straight line parts will be used; otherwise polylines.
:param regions_to_highlight: Regions that will be highlighted in green if they are reachable.
Example usage in World code:
from Utils import visualize_regions
visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml")
state = self.multiworld.get_all_state(False)
state.update_reachable_regions(self.player)
visualize_regions(self.get_region("Menu"), "my_world.puml", show_entrance_names=True,
regions_to_highlight=state.reachable_regions[self.player])
Example usage in Main code:
from Utils import visualize_regions
for player in multiworld.player_ids:
visualize_regions(multiworld.get_region("Menu", player), f"{multiworld.get_out_file_name_base(player)}.puml")
"""
if regions_to_highlight is None:
regions_to_highlight = set()
assert root_region.multiworld, "The multiworld attribute of root_region has to be filled"
from BaseClasses import Entrance, Item, Location, LocationProgressType, MultiWorld, Region
from collections import deque
@@ -1011,7 +1025,7 @@ def visualize_regions(root_region: Region, file_name: str, *,
uml.append(f"\"{fmt(region)}\" : {{field}} {lock}{fmt(location)}")
def visualize_region(region: Region) -> None:
uml.append(f"class \"{fmt(region)}\"")
uml.append(f"class \"{fmt(region)}\" {'#00FF00' if region in regions_to_highlight else ''}")
if show_locations:
visualize_locations(region)
visualize_exits(region)

View File

@@ -446,6 +446,6 @@ if __name__ == '__main__':
parser = get_base_parser(description="Wargroove Client, for text interfacing.")
args, rest = parser.parse_known_args()
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main(args))
colorama.deinit()

View File

@@ -3,13 +3,13 @@ from typing import List, Tuple
from flask import Blueprint
from ..models import Seed
from ..models import Seed, Slot
api_endpoints = Blueprint('api', __name__, url_prefix="/api")
def get_players(seed: Seed) -> List[Tuple[str, str]]:
return [(slot.player_name, slot.game) for slot in seed.slots]
return [(slot.player_name, slot.game) for slot in seed.slots.order_by(Slot.player_id)]
from . import datapackage, generate, room, user # trigger registration

View File

@@ -30,4 +30,4 @@ def get_seeds():
"creation_time": seed.creation_time,
"players": get_players(seed.slots),
})
return jsonify(response)
return jsonify(response)

View File

@@ -117,6 +117,7 @@ class WebHostContext(Context):
self.gamespackage = {"Archipelago": static_gamespackage.get("Archipelago", {})} # this may be modified by _load
self.item_name_groups = {"Archipelago": static_item_name_groups.get("Archipelago", {})}
self.location_name_groups = {"Archipelago": static_location_name_groups.get("Archipelago", {})}
missing_checksum = False
for game in list(multidata.get("datapackage", {})):
game_data = multidata["datapackage"][game]
@@ -132,11 +133,13 @@ class WebHostContext(Context):
continue
else:
self.logger.warning(f"Did not find game_data_package for {game}: {game_data['checksum']}")
else:
missing_checksum = True # Game rolled on old AP and will load data package from multidata
self.gamespackage[game] = static_gamespackage.get(game, {})
self.item_name_groups[game] = static_item_name_groups.get(game, {})
self.location_name_groups[game] = static_location_name_groups.get(game, {})
if not game_data_packages:
if not game_data_packages and not missing_checksum:
# all static -> use the static dicts directly
self.gamespackage = static_gamespackage
self.item_name_groups = static_item_name_groups

View File

@@ -1,11 +1,11 @@
flask>=3.0.3
werkzeug>=3.0.6
flask>=3.1.0
werkzeug>=3.1.3
pony>=0.7.19
waitress>=3.0.0
waitress>=3.0.2
Flask-Caching>=2.3.0
Flask-Compress>=1.15
Flask-Limiter>=3.8.0
bokeh>=3.5.2
markupsafe>=2.1.5
Flask-Compress>=1.17
Flask-Limiter>=3.12
bokeh>=3.6.3
markupsafe>=3.0.2
Markdown>=3.7
mdx-breakless-lists>=1.0.1

View File

@@ -22,7 +22,7 @@ players to rely upon each other to complete their game.
While a multiworld game traditionally requires all players to be playing the same game, a multi-game multiworld allows
players to randomize any of the supported games, and send items between them. This allows players of different
games to interact with one another in a single multiplayer environment. Archipelago supports multi-game multiworld.
games to interact with one another in a single multiplayer environment. Archipelago supports multi-game multiworlds.
Here is a list of our [Supported Games](https://archipelago.gg/games).
## Can I generate a single-player game with Archipelago?

View File

@@ -75,6 +75,27 @@
#inventory-table img.acquired.green{ /*32CD32*/
filter: hue-rotate(84deg) saturate(10) brightness(0.7);
}
#inventory-table img.acquired.hotpink{ /*FF69B4*/
filter: sepia(100%) hue-rotate(300deg) saturate(10);
}
#inventory-table img.acquired.lightsalmon{ /*FFA07A*/
filter: sepia(100%) hue-rotate(347deg) saturate(10);
}
#inventory-table img.acquired.crimson{ /*DB143B*/
filter: sepia(100%) hue-rotate(318deg) saturate(10) brightness(0.86);
}
#inventory-table span{
color: #B4B4A0;
font-size: 40px;
max-width: 40px;
max-height: 40px;
filter: grayscale(100%) contrast(75%) brightness(30%);
}
#inventory-table span.acquired{
filter: none;
}
#inventory-table div.image-stack{
display: grid;

View File

@@ -213,7 +213,7 @@
{% endmacro %}
{% macro RandomizeButton(option_name, option) %}
<div class="randomize-button" data-tooltip="Toggle randomization for this option!">
<div class="randomize-button" data-tooltip="Pick a random value for this option.">
<label for="random-{{ option_name }}">
<input type="checkbox" id="random-{{ option_name }}" name="random-{{ option_name }}" class="randomize-checkbox" data-option-name="{{ option_name }}" {{ "checked" if option.default == "random" }} />
🎲

View File

@@ -99,6 +99,52 @@
{% endif %}
</div>
</div>
{% if 'PrismBreak' in options or 'LockKeyAmadeus' in options or 'GateKeep' in options %}
<div class="table-row">
{% if 'PrismBreak' in options %}
<div class="C1">
<div class="image-stack">
<div class="stack-front">
<div class="stack-top-left">
<img src="{{ icons['Laser Access'] }}" class="hotpink {{ 'acquired' if 'Laser Access A' in acquired_items }}" title="Laser Access A" />
</div>
<div class="stack-top-right">
<img src="{{ icons['Laser Access'] }}" class="lightsalmon {{ 'acquired' if 'Laser Access I' in acquired_items }}" title="Laser Access I" />
</div>
<div class="stack-bottum-left">
<img src="{{ icons['Laser Access'] }}" class="crimson {{ 'acquired' if 'Laser Access M' in acquired_items }}" title="Laser Access M" />
</div>
</div>
</div>
</div>
{% endif %}
{% if 'LockKeyAmadeus' in options %}
<div class="C2">
<div class="image-stack">
<div class="stack-front">
<div class="stack-top-left">
<img src="{{ icons['Lab Glasses'] }}" class="{{ 'acquired' if 'Lab Access Genza' in acquired_items }}" title="Lab Access Genza" />
</div>
<div class="stack-top-right">
<img src="{{ icons['Eye Orb'] }}" class="{{ 'acquired' if 'Lab Access Dynamo' in acquired_items }}" title="Lab Access Dynamo" />
</div>
<div class="stack-bottum-left">
<img src="{{ icons['Lab Coat'] }}" class="{{ 'acquired' if 'Lab Access Research' in acquired_items }}" title="Lab Access Research" />
</div>
<div class="stack-bottum-right">
<img src="{{ icons['Demon'] }}" class="{{ 'acquired' if 'Lab Access Experiment' in acquired_items }}" title="Lab Access Experiment" />
</div>
</div>
</div>
</div>
{% endif %}
{% if 'GateKeep' in options %}
<div class="C3">
<span class="{{ 'acquired' if 'Drawbridge Key' in acquired_items }}" title="Drawbridge Key">&#10070;</span>
</div>
{% endif %}
</div>
{% endif %}
</div>
<table id="location-table">

View File

@@ -100,7 +100,7 @@
{% else %}
<div class="unsupported-option">
This option is not supported. Please edit your .yaml file manually.
This option cannot be modified here. Please edit your .yaml file manually.
</div>
{% endif %}

View File

@@ -1071,6 +1071,11 @@ if "Timespinner" in network_data_package["games"]:
"Plasma Orb": "https://timespinnerwiki.com/mediawiki/images/4/44/Plasma_Orb.png",
"Kobo": "https://timespinnerwiki.com/mediawiki/images/c/c6/Familiar_Kobo.png",
"Merchant Crow": "https://timespinnerwiki.com/mediawiki/images/4/4e/Familiar_Crow.png",
"Laser Access": "https://timespinnerwiki.com/mediawiki/images/9/99/Historical_Documents.png",
"Lab Glasses": "https://timespinnerwiki.com/mediawiki/images/4/4a/Lab_Glasses.png",
"Eye Orb": "https://timespinnerwiki.com/mediawiki/images/a/a4/Eye_Orb.png",
"Lab Coat": "https://timespinnerwiki.com/mediawiki/images/5/51/Lab_Coat.png",
"Demon": "https://timespinnerwiki.com/mediawiki/images/f/f8/Familiar_Demon.png",
}
timespinner_location_ids = {
@@ -1118,6 +1123,9 @@ if "Timespinner" in network_data_package["games"]:
timespinner_location_ids["Ancient Pyramid"] += [
1337237, 1337238, 1337239,
1337240, 1337241, 1337242, 1337243, 1337244, 1337245]
if (slot_data["PyramidStart"]):
timespinner_location_ids["Ancient Pyramid"] += [
1337233, 1337234, 1337235]
display_data = {}

View File

@@ -386,7 +386,7 @@ if __name__ == '__main__':
parser.add_argument('diff_file', default="", type=str, nargs="?",
help='Path to a Archipelago Binary Patch file')
args = parser.parse_args()
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main(args))
colorama.deinit()

View File

@@ -147,3 +147,8 @@
rectangle: self.x-2, self.y-2, self.width+4, self.height+4
<ServerToolTip>:
pos_hint: {'center_y': 0.5, 'center_x': 0.5}
<AutocompleteHintInput>
size_hint_y: None
height: dp(30)
multiline: False
write_tab: False

View File

@@ -121,6 +121,14 @@ Response:
Expected Response Type: `HASH_RESPONSE`
- `MEMORY_SIZE`
Returns the size in bytes of the specified memory domain.
Expected Response Type: `MEMORY_SIZE_RESPONSE`
Additional Fields:
- `domain` (`string`): The name of the memory domain to check
- `GUARD`
Checks a section of memory against `expected_data`. If the bytes starting
at `address` do not match `expected_data`, the response will have `value`
@@ -216,6 +224,12 @@ Response:
Additional Fields:
- `value` (`string`): The returned hash
- `MEMORY_SIZE_RESPONSE`
Contains the size in bytes of the specified memory domain.
Additional Fields:
- `value` (`number`): The size of the domain in bytes
- `GUARD_RESPONSE`
The result of an attempted `GUARD` request.
@@ -376,6 +390,15 @@ request_handlers = {
return res
end,
["MEMORY_SIZE"] = function (req)
local res = {}
res["type"] = "MEMORY_SIZE_RESPONSE"
res["value"] = memory.getmemorydomainsize(req["domain"])
return res
end,
["GUARD"] = function (req)
local res = {}
local expected_data = base64.decode(req["expected_data"])
@@ -613,9 +636,11 @@ end)
if bizhawk_major < 2 or (bizhawk_major == 2 and bizhawk_minor < 7) then
print("Must use BizHawk 2.7.0 or newer")
elseif bizhawk_major > 2 or (bizhawk_major == 2 and bizhawk_minor > 9) then
print("Warning: This version of BizHawk is newer than this script. If it doesn't work, consider downgrading to 2.9.")
else
if bizhawk_major > 2 or (bizhawk_major == 2 and bizhawk_minor > 10) then
print("Warning: This version of BizHawk is newer than this script. If it doesn't work, consider downgrading to 2.10.")
end
if emu.getsystemid() == "NULL" then
print("No ROM is loaded. Please load a ROM.")
while emu.getsystemid() == "NULL" do

View File

@@ -1816,7 +1816,7 @@ end
-- Main control handling: main loop and socket receive
function receive()
function APreceive()
l, e = ootSocket:receive()
-- Handle incoming message
if e == 'closed' then
@@ -1874,7 +1874,7 @@ function main()
end
if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then
if (frame % 30 == 0) then
receive()
APreceive()
end
elseif (curstate == STATE_UNINITIALIZED) then
if (frame % 60 == 0) then

View File

@@ -45,6 +45,9 @@
# ChecksFinder
/worlds/checksfinder/ @SunCatMC
# Civilization VI
/worlds/civ6/ @hesto2
# Clique
/worlds/clique/ @ThePhar
@@ -99,6 +102,9 @@
# Lingo
/worlds/lingo/ @hatkirby
# Links Awakening DX
/worlds/ladx/ @threeandthreee
# Lufia II Ancient Cave
/worlds/lufia2ac/ @el-u
/worlds/lufia2ac/docs/ @wordfcuk @el-u
@@ -208,6 +214,9 @@
# Wargroove
/worlds/wargroove/ @FlySniper
# The Wind Waker
/worlds/tww/ @tanjo3
# The Witness
/worlds/witness/ @NewSoupVi @blastron
@@ -236,9 +245,6 @@
# Final Fantasy (1)
# /worlds/ff1/
# Links Awakening DX
# /worlds/ladx/
# Ocarina of Time
# /worlds/oot/

View File

@@ -370,19 +370,13 @@ target_group_lookup = bake_target_group_lookup(world, get_target_groups)
#### When to call `randomize_entrances`
The short answer is that you will almost always want to do ER in `pre_fill`. For more information why, continue reading.
The correct step for this is `World.connect_entrances`.
ER begins by collecting the entire item pool and then uses your access rules to try and prevent some kinds of failures.
This means 2 things about when you can call ER:
1. You must supply your item pool before calling ER, or call ER before setting any rules which require items.
2. If you have rules dependent on anything other than items (e.g. `Entrance`s or events), you must set your rules
and create your events before you call ER if you want to guarantee a correct output.
If the conditions above are met, you could theoretically do ER as early as `create_regions`. However, plando is also
a consideration. Since item plando happens between `set_rules` and `pre_fill` and modifies the item pool, doing ER
in `pre_fill` is the only way to account for placements made by item plando, otherwise you risk impossible seeds or
generation failures. Obviously, if your world implements entrance plando, you will likely want to do that before ER as
well.
Currently, you could theoretically do it as early as `World.create_regions` or as late as `pre_fill`.
However, there are upcoming changes to Item Plando and Generic Entrance Randomizer to make the two features work better
together.
These changes necessitate that entrance randomization is done exactly in `World.connect_entrances`.
It is fine for your Entrances to be connected differently or not at all before this step.
#### Informing your client about randomized entrances

View File

@@ -47,6 +47,9 @@ Packets are simple JSON lists in which any number of ordered network commands ca
An object can contain the "class" key, which will tell the content data type, such as "Version" in the following example.
Websocket connections should support per-message compression. Uncompressed connections are deprecated and may stop
working in the future.
Example:
```javascript
[{"cmd": "RoomInfo", "version": {"major": 0, "minor": 1, "build": 3, "class": "Version"}, "tags": ["WebHost"], ... }]
@@ -261,6 +264,7 @@ Sent to clients in response to a [Set](#Set) package if want_reply was set to tr
| key | str | The key that was updated. |
| value | any | The new value for the key. |
| original_value | any | The value the key had before it was updated. Not present on "_read" prefixed special keys. |
| slot | int | The slot that originally sent the Set package causing this change. |
Additional arguments added to the [Set](#Set) package that triggered this [SetReply](#SetReply) will also be passed along.
@@ -359,11 +363,11 @@ An enumeration containing the possible hint states.
```python
import enum
class HintStatus(enum.IntEnum):
HINT_FOUND = 0 # The location has been collected. Status cannot be changed once found.
HINT_UNSPECIFIED = 1 # The receiving player has not specified any status
HINT_UNSPECIFIED = 0 # The receiving player has not specified any status
HINT_NO_PRIORITY = 10 # The receiving player has specified that the item is unneeded
HINT_AVOID = 20 # The receiving player has specified that the item is detrimental
HINT_PRIORITY = 30 # The receiving player has specified that the item is needed
HINT_FOUND = 40 # The location has been collected. Status cannot be changed once found.
```
- Hints for items with `ItemClassification.trap` default to `HINT_AVOID`.
- Hints created with `LocationScouts`, `!hint_location`, or similar (hinting a location) default to `HINT_UNSPECIFIED`.
@@ -529,9 +533,9 @@ In JSON this may look like:
{"item": 3, "location": 3, "player": 3, "flags": 0}
]
```
`item` is the item id of the item. Item ids are only supported in the range of [-2<sup>53</sup>, 2<sup>53</sup> - 1], with anything ≤ 0 reserved for Archipelago use.
`item` is the item id of the item. Item ids are only supported in the range of [-2<sup>53</sup> + 1, 2<sup>53</sup> - 1], with anything ≤ 0 reserved for Archipelago use.
`location` is the location id of the item inside the world. Location ids are only supported in the range of [-2<sup>53</sup>, 2<sup>53</sup> - 1], with anything ≤ 0 reserved for Archipelago use.
`location` is the location id of the item inside the world. Location ids are only supported in the range of [-2<sup>53</sup> + 1, 2<sup>53</sup> - 1], with anything ≤ 0 reserved for Archipelago use.
`player` is the player slot of the world the item is located in, except when inside an [LocationInfo](#LocationInfo) Packet then it will be the slot of the player to receive the item
@@ -744,6 +748,7 @@ Tags are represented as a list of strings, the common client tags follow:
| HintGame | Indicates the client is a hint game, made to send hints instead of locations. Special join/leave message,¹ `game` is optional.² |
| Tracker | Indicates the client is a tracker, made to track instead of sending locations. Special join/leave message,¹ `game` is optional.² |
| TextOnly | Indicates the client is a basic client, made to chat instead of sending locations. Special join/leave message,¹ `game` is optional.² |
| NoText | Indicates the client does not want to receive text messages, improving performance if not needed. |
¹: When connecting or disconnecting, the chat message shows e.g. "tracking".\
²: Allows `game` to be empty or null in [Connect](#connect). Game and version validation will then be skipped.

View File

@@ -95,7 +95,7 @@ user hovers over the yellow "(?)" icon, and included in the YAML templates gener
The WebHost can display Option documentation either as plain text with all whitespace preserved (other than the base
indentation), or as HTML generated from the standard Python [reStructuredText] format. Although plain text is the
default for backwards compatibility, world authors are encouraged to write their Option documentation as
reStructuredText and enable rich text rendering by setting `World.rich_text_options_doc = True`.
reStructuredText and enable rich text rendering by setting `WebWorld.rich_text_options_doc = True`.
[reStructuredText]: https://docutils.sourceforge.io/rst.html

View File

@@ -73,15 +73,47 @@ When tests are run, this class will create a multiworld with a single player hav
generic tests, as well as the new custom test. Each test method definition will create its own separate solo multiworld
that will be cleaned up after. If you don't want to run the generic tests on a base, `run_default_tests` can be
overridden. For more information on what methods are available to your class, check the
[WorldTestBase definition](/test/bases.py#L104).
[WorldTestBase definition](/test/bases.py#L106).
#### Alternatives to WorldTestBase
Unit tests can also be created using [TestBase](/test/bases.py#L14) or
Unit tests can also be created using [TestBase](/test/bases.py#L16) or
[unittest.TestCase](https://docs.python.org/3/library/unittest.html#unittest.TestCase) depending on your use case. These
may be useful for generating a multiworld under very specific constraints without using the generic world setup, or for
testing portions of your code that can be tested without relying on a multiworld to be created first.
#### Parametrization
When defining a test that needs to cover a range of inputs it is useful to parameterize (to run the same test
for multiple inputs) the base test. Some important things to consider when attempting to parametrize your test are:
* [Subtests](https://docs.python.org/3/library/unittest.html#distinguishing-test-iterations-using-subtests)
can be used to have parametrized assertions that show up similar to individual tests but without the overhead
of needing to instantiate multiple tests; however, subtests can not be multithreaded and do not have individual
timing data, so they are not suitable for slow tests.
* Archipelago's tests are test-runner-agnostic. That means tests are not allowed to use e.g. `@pytest.mark.parametrize`.
Instead, we define our own parametrization helpers in [test.param](/test/param.py).
* Classes inheriting from `WorldTestBase`, including those created by the helpers in `test.param`, will run all
base tests by default, make sure the produced tests actually do what you aim for and do not waste a lot of
extra CPU time. Consider using `TestBase` or `unittest.TestCase` directly
or setting `WorldTestBase.run_default_tests` to False.
#### Performance Considerations
Archipelago is big enough that the runtime of unittests can have an impact on productivity.
Individual tests should take less than a second, so they can be properly multithreaded.
Ideally, thorough tests are directed at actual code/functionality. Do not just create and/or fill a ton of individual
Multiworlds that spend most of the test time outside what you actually want to test.
Consider generating/validating "random" games as part of your APWorld release workflow rather than having that be part
of continuous integration, and add minimal reproducers to the "normal" tests for problems that were found.
You can use [@unittest.skipIf](https://docs.python.org/3/library/unittest.html#unittest.skipIf) with an environment
variable to keep all the benefits of the test framework while not running the marked tests by default.
## Running Tests
#### Using Pycharm
@@ -100,3 +132,11 @@ next to the run and debug buttons.
#### Running Tests without Pycharm
Run `pip install pytest pytest-subtests`, then use your IDE to run tests or run `pytest` from the source folder.
#### Running Tests Multithreaded
pytest can run multiple test runners in parallel with the pytest-xdist extension.
Install with `pip install pytest-xdist`.
Run with `pytest -n12` to spawn 12 process that each run 1/12th of the tests.

View File

@@ -222,8 +222,8 @@ could also be progress in a research tree, or even something more abstract like
Each location has a `name` and an `address` (hereafter referred to as an `id`), is placed in a Region, has access rules,
and has a classification. The name needs to be unique within each game and must not be numeric (must contain least 1
letter or symbol). The ID needs to be unique across all games, and is best kept in the same range as the item IDs.
Locations and items can share IDs, so typically a game's locations and items start at the same ID.
letter or symbol). The ID needs to be unique across all locations within the game.
Locations and items can share IDs, and locations can share IDs with other games' locations.
World-specific IDs must be in the range 1 to 2<sup>53</sup>-1; IDs ≤ 0 are global and reserved.
@@ -243,7 +243,9 @@ progression. Progression items will be assigned to locations with higher priorit
and satisfy progression balancing.
The name needs to be unique within each game, meaning if you need to create multiple items with the same name, they
will all have the same ID. Name must not be numeric (must contain at least 1 letter or symbol).
will all have the same ID. Name must not be numeric (must contain at least 1 letter or symbol).
The ID thus also needs to be unique across all items with different names within the game.
Items and locations can share IDs, and items can share IDs with other games' items.
Other classifications include:
@@ -289,7 +291,7 @@ like entrance randomization in logic.
Regions have a list called `exits`, containing `Entrance` objects representing transitions to other regions.
There must be one special region (Called "Menu" by default, but configurable using [origin_region_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L295-L296)),
There must be one special region (Called "Menu" by default, but configurable using [origin_region_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L298-L299)),
from which the logic unfolds. AP assumes that a player will always be able to return to this starting region by resetting the game ("Save and quit").
### Entrances
@@ -329,7 +331,7 @@ Even doing `state.can_reach_location` or `state.can_reach_entrance` is problemat
You can use `multiworld.register_indirect_condition(region, entrance)` to explicitly tell the generator that, when a given region becomes accessible, it is necessary to re-check a specific entrance.
You **must** use `multiworld.register_indirect_condition` if you perform this kind of `can_reach` from an entrance access rule, unless you have a **very** good technical understanding of the relevant code and can reason why it will never lead to problems in your case.
Alternatively, you can set [world.explicit_indirect_conditions = False](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L298-L301),
Alternatively, you can set [world.explicit_indirect_conditions = False](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L301-L304),
avoiding the need for indirect conditions at the expense of performance.
### Item Rules
@@ -490,6 +492,9 @@ In addition, the following methods can be implemented and are called in this ord
after this step. Locations cannot be moved to different regions after this step.
* `set_rules(self)`
called to set access and item rules on locations and entrances.
* `connect_entrances(self)`
by the end of this step, all entrances must exist and be connected to their source and target regions.
Entrance randomization should be done here.
* `generate_basic(self)`
player-specific randomization that does not affect logic can be done here.
* `pre_fill(self)`, `fill_hook(self)` and `post_fill(self)`
@@ -557,17 +562,13 @@ from .items import is_progression # this is just a dummy
def create_item(self, item: str) -> MyGameItem:
# this is called when AP wants to create an item by name (for plando) or when you call it from your own code
classification = ItemClassification.progression if is_progression(item) else
ItemClassification.filler
return MyGameItem(item, classification, self.item_name_to_id[item],
self.player)
classification = ItemClassification.progression if is_progression(item) else ItemClassification.filler
return MyGameItem(item, classification, self.item_name_to_id[item], self.player)
def create_event(self, event: str) -> MyGameItem:
# while we are at it, we can also add a helper to create events
return MyGameItem(event, True, None, self.player)
return MyGameItem(event, ItemClassification.progression, None, self.player)
```
#### create_items
@@ -835,14 +836,16 @@ def generate_output(self, output_directory: str) -> None:
### Slot Data
If the game client needs to know information about the generated seed, a preferred method of transferring the data
is through the slot data. This is filled with the `fill_slot_data` method of your world by returning
a `dict` with `str` keys that can be serialized with json.
But, to not waste resources, it should be limited to data that is absolutely necessary. Slot data is sent to your client
once it has successfully [connected](network%20protocol.md#connected).
If a client or tracker needs to know information about the generated seed, a preferred method of transferring the data
is through the slot data. This is filled with the `fill_slot_data` method of your world by returning a `dict` with
`str` keys that can be serialized with json. However, to not waste resources, it should be limited to data that is
absolutely necessary. Slot data is sent to your client once it has successfully
[connected](network%20protocol.md#connected).
If you need to know information about locations in your world, instead of propagating the slot data, it is preferable
to use [LocationScouts](network%20protocol.md#locationscouts), since that data already exists on the server. The most
common usage of slot data is sending option results that the client needs to be aware of.
to use [LocationScouts](network%20protocol.md#locationscouts), since that data already exists on the server. Adding
item/location pairs is unnecessary since the AP server already retains and freely gives that information to clients
that request it. The most common usage of slot data is sending option results that the client needs to be aware of.
```python
def fill_slot_data(self) -> Dict[str, Any]:

View File

@@ -157,17 +157,16 @@ class ERPlacementState:
def placed_regions(self) -> set[Region]:
return self.collection_state.reachable_regions[self.world.player]
def find_placeable_exits(self, check_validity: bool) -> list[Entrance]:
def find_placeable_exits(self, check_validity: bool, usable_exits: list[Entrance]) -> list[Entrance]:
if check_validity:
blocked_connections = self.collection_state.blocked_connections[self.world.player]
blocked_connections = sorted(blocked_connections, key=lambda x: x.name)
placeable_randomized_exits = [connection for connection in blocked_connections
if not connection.connected_region
and connection.is_valid_source_transition(self)]
placeable_randomized_exits = [ex for ex in usable_exits
if not ex.connected_region
and ex in blocked_connections
and ex.is_valid_source_transition(self)]
else:
# this is on a beaten minimal attempt, so any exit anywhere is fair game
placeable_randomized_exits = [ex for region in self.world.multiworld.get_regions(self.world.player)
for ex in region.exits if not ex.connected_region]
placeable_randomized_exits = [ex for ex in usable_exits if not ex.connected_region]
self.world.random.shuffle(placeable_randomized_exits)
return placeable_randomized_exits
@@ -181,7 +180,8 @@ class ERPlacementState:
self.placements.append(source_exit)
self.pairings.append((source_exit.name, target_entrance.name))
def test_speculative_connection(self, source_exit: Entrance, target_entrance: Entrance) -> bool:
def test_speculative_connection(self, source_exit: Entrance, target_entrance: Entrance,
usable_exits: set[Entrance]) -> bool:
copied_state = self.collection_state.copy()
# simulated connection. A real connection is unsafe because the region graph is shallow-copied and would
# propagate back to the real multiworld.
@@ -198,6 +198,9 @@ class ERPlacementState:
# ignore the source exit, and, if coupled, the reverse exit. They're not actually new
if _exit.name == source_exit.name or (self.coupled and _exit.name == target_entrance.name):
continue
# make sure we are only paying attention to usable exits
if _exit not in usable_exits:
continue
# technically this should be is_valid_source_transition, but that may rely on side effects from
# on_connect, which have not happened here (because we didn't do a real connection, and if we did, we would
# not want them to persist). can_reach is a close enough approximation most of the time.
@@ -262,14 +265,19 @@ def bake_target_group_lookup(world: World, get_target_groups: Callable[[int], li
return { group: get_target_groups(group) for group in unique_groups }
def disconnect_entrance_for_randomization(entrance: Entrance, target_group: int | None = None) -> None:
def disconnect_entrance_for_randomization(entrance: Entrance, target_group: int | None = None,
one_way_target_name: str | None = None) -> None:
"""
Given an entrance in a "vanilla" region graph, splits that entrance to prepare it for randomization
in randomize_entrances. This should be done after setting the type and group of the entrance.
in randomize_entrances. This should be done after setting the type and group of the entrance. Because it attempts
to meet strict entrance naming requirements for coupled mode, this function may produce unintuitive results when
called only on a single entrance; it produces eventually-correct outputs only after calling it on all entrances.
:param entrance: The entrance which will be disconnected in preparation for randomization.
:param target_group: The group to assign to the created ER target. If not specified, the group from
the original entrance will be copied.
:param one_way_target_name: The name of the created ER target if `entrance` is one-way. This argument
is required for one-way entrances and is ignored otherwise.
"""
child_region = entrance.connected_region
parent_region = entrance.parent_region
@@ -284,8 +292,11 @@ def disconnect_entrance_for_randomization(entrance: Entrance, target_group: int
# targets in the child region will be created when the other direction edge is disconnected
target = parent_region.create_er_target(entrance.name)
else:
# for 1-ways, the child region needs a target and coupling/naming is not a concern
target = child_region.create_er_target(child_region.name)
# for 1-ways, the child region needs a target. naming is not a concern for coupling so we
# allow it to be user provided (and require it, to prevent an unhelpful assumed name in pairings)
if not one_way_target_name:
raise ValueError("Cannot disconnect a one-way entrance without a target name specified")
target = child_region.create_er_target(one_way_target_name)
target.randomization_type = entrance.randomization_type
target.randomization_group = target_group or entrance.randomization_group
@@ -326,6 +337,24 @@ def randomize_entrances(
# similar to fill, skip validity checks on entrances if the game is beatable on minimal accessibility
perform_validity_check = True
if not er_targets:
er_targets = sorted([entrance for region in world.multiworld.get_regions(world.player)
for entrance in region.entrances if not entrance.parent_region], key=lambda x: x.name)
if not exits:
exits = sorted([ex for region in world.multiworld.get_regions(world.player)
for ex in region.exits if not ex.connected_region], key=lambda x: x.name)
if len(er_targets) != len(exits):
raise EntranceRandomizationError(f"Unable to randomize entrances due to a mismatched count of "
f"entrances ({len(er_targets)}) and exits ({len(exits)}.")
# used when membership checks are needed on the exit list, e.g. speculative sweep
exits_set = set(exits)
for entrance in er_targets:
entrance_lookup.add(entrance)
# place the menu region and connected start region(s)
er_state.collection_state.update_reachable_regions(world.player)
def do_placement(source_exit: Entrance, target_entrance: Entrance) -> None:
placed_exits, removed_entrances = er_state.connect(source_exit, target_entrance)
# remove the placed targets from consideration
@@ -337,9 +366,37 @@ def randomize_entrances(
if on_connect:
on_connect(er_state, placed_exits)
def needs_speculative_sweep(dead_end: bool, require_new_exits: bool, placeable_exits: list[Entrance]) -> bool:
# speculative sweep is expensive. We currently only do it as a last resort, if we might cap off the graph
# entirely
if len(placeable_exits) > 1:
return False
# in certain stages of randomization we either expect or don't care if the search space shrinks.
# we should never speculative sweep here.
if dead_end or not require_new_exits or not perform_validity_check:
return False
# edge case - if all dead ends have pre-placed progression or indirect connections, they are pulled forward
# into the non dead end stage. In this case, and only this case, it's possible that the last connection may
# actually be placeable in stage 1. We need to skip speculative sweep in this case because we expect the graph
# to get capped off.
# check to see if we are proposing the last placement
if not coupled:
# in uncoupled, this check is easy as there will only be one target.
is_last_placement = len(entrance_lookup) == 1
else:
# a bit harder, there may be 1 or 2 targets depending on if the exit to place is one way or two way.
# if it is two way, we can safely assume that one of the targets is the logical pair of the exit.
desired_target_count = 2 if placeable_exits[0].randomization_type == EntranceType.TWO_WAY else 1
is_last_placement = len(entrance_lookup) == desired_target_count
# if it's not the last placement, we need a sweep
return not is_last_placement
def find_pairing(dead_end: bool, require_new_exits: bool) -> bool:
nonlocal perform_validity_check
placeable_exits = er_state.find_placeable_exits(perform_validity_check)
placeable_exits = er_state.find_placeable_exits(perform_validity_check, exits)
for source_exit in placeable_exits:
target_groups = target_group_lookup[source_exit.randomization_group]
for target_entrance in entrance_lookup.get_targets(target_groups, dead_end, preserve_group_order):
@@ -350,12 +407,10 @@ def randomize_entrances(
# very last exit and check whatever exits we open up are functionally accessible.
# this requirement can be ignored on a beaten minimal, islands are no issue there.
exit_requirement_satisfied = (not perform_validity_check or not require_new_exits
or target_entrance.connected_region not in er_state.placed_regions)
needs_speculative_sweep = (not dead_end and require_new_exits and perform_validity_check
and len(placeable_exits) == 1)
or target_entrance.connected_region not in er_state.placed_regions)
if exit_requirement_satisfied and source_exit.can_connect_to(target_entrance, dead_end, er_state):
if (needs_speculative_sweep
and not er_state.test_speculative_connection(source_exit, target_entrance)):
if (needs_speculative_sweep(dead_end, require_new_exits, placeable_exits)
and not er_state.test_speculative_connection(source_exit, target_entrance, exits_set)):
continue
do_placement(source_exit, target_entrance)
return True
@@ -378,13 +433,14 @@ def randomize_entrances(
and world.multiworld.has_beaten_game(er_state.collection_state, world.player):
# ensure that we have enough locations to place our progression
accessible_location_count = 0
prog_item_count = sum(er_state.collection_state.prog_items[world.player].values())
prog_item_count = len([item for item in world.multiworld.itempool if item.advancement and item.player == world.player])
# short-circuit location checking in this case
if prog_item_count == 0:
return True
for region in er_state.placed_regions:
for loc in region.locations:
if loc.can_reach(er_state.collection_state):
if not loc.item and loc.can_reach(er_state.collection_state):
# don't count locations with preplaced items
accessible_location_count += 1
if accessible_location_count >= prog_item_count:
perform_validity_check = False
@@ -406,21 +462,6 @@ def randomize_entrances(
f"All unplaced entrances: {unplaced_entrances}\n"
f"All unplaced exits: {unplaced_exits}")
if not er_targets:
er_targets = sorted([entrance for region in world.multiworld.get_regions(world.player)
for entrance in region.entrances if not entrance.parent_region], key=lambda x: x.name)
if not exits:
exits = sorted([ex for region in world.multiworld.get_regions(world.player)
for ex in region.exits if not ex.connected_region], key=lambda x: x.name)
if len(er_targets) != len(exits):
raise EntranceRandomizationError(f"Unable to randomize entrances due to a mismatched count of "
f"entrances ({len(er_targets)}) and exits ({len(exits)}.")
for entrance in er_targets:
entrance_lookup.add(entrance)
# place the menu region and connected start region(s)
er_state.collection_state.update_reachable_regions(world.player)
# stage 1 - try to place all the non-dead-end entrances
while entrance_lookup.others:
if not find_pairing(dead_end=False, require_new_exits=True):

View File

@@ -221,6 +221,11 @@ Root: HKCR; Subkey: "{#MyAppName}ygo06patch"; ValueData: "Ar
Root: HKCR; Subkey: "{#MyAppName}ygo06patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}ygo06patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: ".apcivvi"; ValueData: "{#MyAppName}apcivvipatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}apcivvipatch"; ValueData: "Archipelago Civilization 6 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}apcivvipatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLauncher.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}apcivvipatch\shell\open\command"; ValueData: """{app}\ArchipelagoLauncher.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: "";

82
kvui.py
View File

@@ -26,6 +26,10 @@ import Utils
if Utils.is_frozen():
os.environ["KIVY_DATA_DIR"] = Utils.local_path("data")
import platformdirs
os.environ["KIVY_HOME"] = os.path.join(platformdirs.user_config_dir("Archipelago", False), "kivy")
os.makedirs(os.environ["KIVY_HOME"], exist_ok=True)
from kivy.config import Config
Config.set("input", "mouse", "mouse,disable_multitouch")
@@ -40,7 +44,7 @@ from kivy.core.image import ImageLoader, ImageLoaderBase, ImageData
from kivy.base import ExceptionHandler, ExceptionManager
from kivy.clock import Clock
from kivy.factory import Factory
from kivy.properties import BooleanProperty, ObjectProperty
from kivy.properties import BooleanProperty, ObjectProperty, NumericProperty
from kivy.metrics import dp
from kivy.effects.scroll import ScrollEffect
from kivy.uix.widget import Widget
@@ -64,6 +68,7 @@ from kivy.uix.recycleboxlayout import RecycleBoxLayout
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
from kivy.animation import Animation
from kivy.uix.popup import Popup
from kivy.uix.dropdown import DropDown
from kivy.uix.image import AsyncImage
fade_in_animation = Animation(opacity=0, duration=0) + Animation(opacity=1, duration=0.25)
@@ -305,6 +310,50 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
""" Respond to the selection of items in the view. """
self.selected = is_selected
class AutocompleteHintInput(TextInput):
min_chars = NumericProperty(3)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.dropdown = DropDown()
self.dropdown.bind(on_select=lambda instance, x: setattr(self, 'text', x))
self.bind(on_text_validate=self.on_message)
def on_message(self, instance):
App.get_running_app().commandprocessor("!hint "+instance.text)
def on_text(self, instance, value):
if len(value) >= self.min_chars:
self.dropdown.clear_widgets()
ctx: context_type = App.get_running_app().ctx
if not ctx.game:
return
item_names = ctx.item_names._game_store[ctx.game].values()
def on_press(button: Button):
split_text = MarkupLabel(text=button.text).markup
return self.dropdown.select("".join(text_frag for text_frag in split_text
if not text_frag.startswith("[")))
lowered = value.lower()
for item_name in item_names:
try:
index = item_name.lower().index(lowered)
except ValueError:
pass # substring not found
else:
text = escape_markup(item_name)
text = text[:index] + "[b]" + text[index:index+len(value)]+"[/b]"+text[index+len(value):]
btn = Button(text=text, size_hint_y=None, height=dp(30), markup=True)
btn.bind(on_release=on_press)
self.dropdown.add_widget(btn)
if not self.dropdown.attach_to:
self.dropdown.open(self)
else:
self.dropdown.dismiss()
class HintLabel(RecycleDataViewBehavior, BoxLayout):
selected = BooleanProperty(False)
striped = BooleanProperty(False)
@@ -395,8 +444,11 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
if child.collide_point(*touch.pos):
key = child.sort_key
if key == "status":
parent.hint_sorter = lambda element: element["status"]["hint"]["status"]
else: parent.hint_sorter = lambda element: remove_between_brackets.sub("", element[key]["text"]).lower()
parent.hint_sorter = lambda element: status_sort_weights[element["status"]["hint"]["status"]]
else:
parent.hint_sorter = lambda element: (
remove_between_brackets.sub("", element[key]["text"]).lower()
)
if key == parent.sort_key:
# second click reverses order
parent.reversed = not parent.reversed
@@ -570,8 +622,10 @@ class GameManager(App):
# show Archipelago tab if other logging is present
self.tabs.add_widget(panel)
hint_panel = self.add_client_tab("Hints", HintLog(self.json_to_kivy_parser))
hint_panel = self.add_client_tab("Hints", HintLayout())
self.hint_log = HintLog(self.json_to_kivy_parser)
self.log_panels["Hints"] = hint_panel.content
hint_panel.content.add_widget(self.hint_log)
if len(self.logging_pairs) == 1:
self.tabs.default_tab_text = "Archipelago"
@@ -698,7 +752,7 @@ class GameManager(App):
def update_hints(self):
hints = self.ctx.stored_data.get(f"_read_hints_{self.ctx.team}_{self.ctx.slot}", [])
self.log_panels["Hints"].refresh_hints(hints)
self.hint_log.refresh_hints(hints)
# default F1 keybind, opens a settings menu, that seems to break the layout engine once closed
def open_settings(self, *largs):
@@ -753,6 +807,17 @@ class UILog(RecycleView):
element.height = element.texture_size[1]
class HintLayout(BoxLayout):
orientation = "vertical"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
boxlayout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30))
boxlayout.add_widget(Label(text="New Hint:", size_hint_x=None, size_hint_y=None, height=dp(30)))
boxlayout.add_widget(AutocompleteHintInput())
self.add_widget(boxlayout)
status_names: typing.Dict[HintStatus, str] = {
HintStatus.HINT_FOUND: "Found",
HintStatus.HINT_UNSPECIFIED: "Unspecified",
@@ -767,6 +832,13 @@ status_colors: typing.Dict[HintStatus, str] = {
HintStatus.HINT_AVOID: "salmon",
HintStatus.HINT_PRIORITY: "plum",
}
status_sort_weights: dict[HintStatus, int] = {
HintStatus.HINT_FOUND: 0,
HintStatus.HINT_UNSPECIFIED: 1,
HintStatus.HINT_NO_PRIORITY: 2,
HintStatus.HINT_AVOID: 3,
HintStatus.HINT_PRIORITY: 4,
}
class HintLog(RecycleView):

View File

@@ -2,3 +2,6 @@
python_files = test_*.py Test*.py # TODO: remove Test* once all worlds have been ported
python_classes = Test
python_functions = test
testpaths =
test
worlds

View File

@@ -1,14 +1,14 @@
colorama>=0.4.6
websockets>=13.0.1,<14
PyYAML>=6.0.2
jellyfish>=1.1.0
jinja2>=3.1.4
jellyfish>=1.1.3
jinja2>=3.1.6
schema>=0.7.7
kivy>=2.3.0
bsdiff4>=1.2.4
platformdirs>=4.2.2
certifi>=2024.8.30
cython>=3.0.11
cymem>=2.0.8
orjson>=3.10.7
kivy>=2.3.1
bsdiff4>=1.2.6
platformdirs>=4.3.6
certifi>=2025.1.31
cython>=3.0.12
cymem>=2.0.11
orjson>=3.10.15
typing_extensions>=4.12.2

View File

@@ -109,7 +109,7 @@ class Group:
def get_type_hints(cls) -> Dict[str, Any]:
"""Returns resolved type hints for the class"""
if cls._type_cache is None:
if not isinstance(next(iter(cls.__annotations__.values())), str):
if not cls.__annotations__ or not isinstance(next(iter(cls.__annotations__.values())), str):
# non-str: assume already resolved
cls._type_cache = cls.__annotations__
else:
@@ -270,15 +270,20 @@ class Group:
# fetch class to avoid going through getattr
cls = self.__class__
type_hints = cls.get_type_hints()
entries = [e for e in self]
if not entries:
# write empty dict for empty Group with no instance values
cls._dump_value({}, f, indent=" " * level)
# validate group
for name in cls.__annotations__.keys():
assert hasattr(cls, name), f"{cls}.{name} is missing a default value"
# dump ordered members
for name in self:
for name in entries:
attr = cast(object, getattr(self, name))
attr_cls = type_hints[name] if name in type_hints else attr.__class__
attr_cls_origin = typing.get_origin(attr_cls)
while attr_cls_origin is Union: # resolve to first type for doc string
# resolve to first type for doc string
while attr_cls_origin is Union or attr_cls_origin is types.UnionType:
attr_cls = typing.get_args(attr_cls)[0]
attr_cls_origin = typing.get_origin(attr_cls)
if attr_cls.__doc__ and attr_cls.__module__ != "builtins":
@@ -678,6 +683,8 @@ class GeneratorOptions(Group):
race: Race = Race(0)
plando_options: PlandoOptions = PlandoOptions("bosses, connections, texts")
panic_method: PanicMethod = PanicMethod("swap")
loglevel: str = "info"
logtime: bool = False
class SNIOptions(Group):
@@ -785,7 +792,17 @@ class Settings(Group):
if location:
from Utils import parse_yaml
with open(location, encoding="utf-8-sig") as f:
options = parse_yaml(f.read())
from yaml.error import MarkedYAMLError
try:
options = parse_yaml(f.read())
except MarkedYAMLError as ex:
if ex.problem_mark:
f.seek(0)
lines = f.readlines()
problem_line = lines[ex.problem_mark.line]
error_line = " " * ex.problem_mark.column + "^"
raise Exception(f"{ex.context} {ex.problem}\n{problem_line}{error_line}")
raise ex
# TODO: detect if upgrade is required
# TODO: once we have a cache for _world_settings_name_cache, detect if any game section is missing
self.update(options or {})

View File

@@ -18,7 +18,15 @@ def run_locations_benchmark():
class BenchmarkRunner:
gen_steps: typing.Tuple[str, ...] = (
"generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill")
"generate_early",
"create_regions",
"create_items",
"set_rules",
"connect_entrances",
"generate_basic",
"pre_fill",
)
rule_iterations: int = 100_000
if sys.version_info >= (3, 9):

View File

@@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.5)
cmake_minimum_required(VERSION 3.16)
project(ap-cpp-tests)
enable_testing()
@@ -7,8 +7,8 @@ find_package(GTest REQUIRED)
if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
add_definitions("/source-charset:utf-8")
set(CMAKE_CXX_FLAGS_DEBUG "/MTd")
set(CMAKE_CXX_FLAGS_RELEASE "/MT")
# set(CMAKE_CXX_FLAGS_DEBUG "/MDd") # this is the default
# set(CMAKE_CXX_FLAGS_RELEASE "/MD") # this is the default
elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
# enable static analysis for gcc
add_compile_options(-fanalyzer -Werror)

View File

@@ -5,7 +5,15 @@ from BaseClasses import CollectionState, Item, ItemClassification, Location, Mul
from worlds import network_data_package
from worlds.AutoWorld import World, call_all
gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill")
gen_steps = (
"generate_early",
"create_regions",
"create_items",
"set_rules",
"connect_entrances",
"generate_basic",
"pre_fill",
)
def setup_solo_multiworld(

View File

@@ -148,7 +148,7 @@ class TestDisconnectForRandomization(unittest.TestCase):
e.randomization_group = 1
e.connect(r2)
disconnect_entrance_for_randomization(e)
disconnect_entrance_for_randomization(e, one_way_target_name="foo")
self.assertIsNone(e.connected_region)
self.assertEqual([], r1.entrances)
@@ -158,10 +158,22 @@ class TestDisconnectForRandomization(unittest.TestCase):
self.assertEqual(1, len(r2.entrances))
self.assertIsNone(r2.entrances[0].parent_region)
self.assertEqual("r2", r2.entrances[0].name)
self.assertEqual("foo", r2.entrances[0].name)
self.assertEqual(EntranceType.ONE_WAY, r2.entrances[0].randomization_type)
self.assertEqual(1, r2.entrances[0].randomization_group)
def test_disconnect_default_1way_no_vanilla_target_raises(self):
multiworld = generate_test_multiworld()
r1 = Region("r1", 1, multiworld)
r2 = Region("r2", 1, multiworld)
e = r1.create_exit("e")
e.randomization_type = EntranceType.ONE_WAY
e.randomization_group = 1
e.connect(r2)
with self.assertRaises(ValueError):
disconnect_entrance_for_randomization(e)
def test_disconnect_uses_alternate_group(self):
multiworld = generate_test_multiworld()
r1 = Region("r1", 1, multiworld)
@@ -171,7 +183,7 @@ class TestDisconnectForRandomization(unittest.TestCase):
e.randomization_group = 1
e.connect(r2)
disconnect_entrance_for_randomization(e, 2)
disconnect_entrance_for_randomization(e, 2, "foo")
self.assertIsNone(e.connected_region)
self.assertEqual([], r1.entrances)
@@ -181,7 +193,7 @@ class TestDisconnectForRandomization(unittest.TestCase):
self.assertEqual(1, len(r2.entrances))
self.assertIsNone(r2.entrances[0].parent_region)
self.assertEqual("r2", r2.entrances[0].name)
self.assertEqual("foo", r2.entrances[0].name)
self.assertEqual(EntranceType.ONE_WAY, r2.entrances[0].randomization_type)
self.assertEqual(2, r2.entrances[0].randomization_group)
@@ -218,7 +230,7 @@ class TestRandomizeEntrances(unittest.TestCase):
self.assertEqual(80, len(result.pairings))
self.assertEqual(80, len(result.placements))
def test_coupling(self):
def test_coupled(self):
"""tests that in coupled mode, all 2 way transitions have an inverse"""
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 5)
@@ -236,6 +248,36 @@ class TestRandomizeEntrances(unittest.TestCase):
# if we didn't visit every placement the verification on_connect doesn't really mean much
self.assertEqual(len(result.placements), seen_placement_count)
def test_uncoupled_succeeds_stage1_indirect_condition(self):
multiworld = generate_test_multiworld()
menu = multiworld.get_region("Menu", 1)
generate_entrance_pair(menu, "_right", ERTestGroups.RIGHT)
end = Region("End", 1, multiworld)
multiworld.regions.append(end)
generate_entrance_pair(end, "_left", ERTestGroups.LEFT)
multiworld.register_indirect_condition(end, None)
result = randomize_entrances(multiworld.worlds[1], False, directionally_matched_group_lookup)
self.assertSetEqual({
("Menu_right", "End_left"),
("End_left", "Menu_right")
}, set(result.pairings))
def test_coupled_succeeds_stage1_indirect_condition(self):
multiworld = generate_test_multiworld()
menu = multiworld.get_region("Menu", 1)
generate_entrance_pair(menu, "_right", ERTestGroups.RIGHT)
end = Region("End", 1, multiworld)
multiworld.regions.append(end)
generate_entrance_pair(end, "_left", ERTestGroups.LEFT)
multiworld.register_indirect_condition(end, None)
result = randomize_entrances(multiworld.worlds[1], True, directionally_matched_group_lookup)
self.assertSetEqual({
("Menu_right", "End_left"),
("End_left", "Menu_right")
}, set(result.pairings))
def test_uncoupled(self):
"""tests that in uncoupled mode, no transitions have an (intentional) inverse"""
multiworld = generate_test_multiworld()
@@ -311,6 +353,37 @@ class TestRandomizeEntrances(unittest.TestCase):
self.assertEqual([], [exit_ for region in multiworld.get_regions()
for exit_ in region.exits if not exit_.connected_region])
def test_minimal_entrance_rando_with_collect_override(self):
"""
tests that entrance randomization can complete with minimal accessibility and unreachable exits
when the world defines a collect override that add extra values to prog_items
"""
multiworld = generate_test_multiworld()
multiworld.worlds[1].options.accessibility = Accessibility.from_any(Accessibility.option_minimal)
multiworld.completion_condition[1] = lambda state: state.can_reach("region24", player=1)
generate_disconnected_region_grid(multiworld, 5, 1)
prog_items = generate_items(10, 1, True)
multiworld.itempool += prog_items
filler_items = generate_items(15, 1, False)
multiworld.itempool += filler_items
e = multiworld.get_entrance("region1_right", 1)
set_rule(e, lambda state: False)
old_collect = multiworld.worlds[1].collect
def new_collect(state, item):
old_collect(state, item)
state.prog_items[item.player]["counter"] += 300
multiworld.worlds[1].collect = new_collect
randomize_entrances(multiworld.worlds[1], False, directionally_matched_group_lookup)
self.assertEqual([], [entrance for region in multiworld.get_regions()
for entrance in region.entrances if not entrance.parent_region])
self.assertEqual([], [exit_ for region in multiworld.get_regions()
for exit_ in region.exits if not exit_.connected_region])
def test_restrictive_region_requirement_does_not_fail(self):
multiworld = generate_test_multiworld()
generate_disconnected_region_grid(multiworld, 2, 1)

View File

@@ -0,0 +1,63 @@
import unittest
from worlds.AutoWorld import AutoWorldRegister, call_all, World
from . import setup_solo_multiworld
class TestBase(unittest.TestCase):
def test_entrance_connection_steps(self):
"""Tests that Entrances are connected and not changed after connect_entrances."""
def get_entrance_name_to_source_and_target_dict(world: World):
return [
(entrance.name, entrance.parent_region, entrance.connected_region)
for entrance in world.get_entrances()
]
gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "connect_entrances")
additional_steps = ("generate_basic", "pre_fill")
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest("Game", game_name=game_name):
multiworld = setup_solo_multiworld(world_type, gen_steps)
original_entrances = get_entrance_name_to_source_and_target_dict(multiworld.worlds[1])
self.assertTrue(
all(entrance[1] is not None and entrance[2] is not None for entrance in original_entrances),
f"{game_name} had unconnected entrances after connect_entrances"
)
for step in additional_steps:
with self.subTest("Step", step=step):
call_all(multiworld, step)
step_entrances = get_entrance_name_to_source_and_target_dict(multiworld.worlds[1])
self.assertEqual(
original_entrances, step_entrances, f"{game_name} modified entrances during {step}"
)
def test_all_state_before_connect_entrances(self):
"""Before connect_entrances, Entrance objects may be unconnected.
Thus, we test that get_all_state is performed with allow_partial_entrances if used before or during
connect_entrances."""
gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "connect_entrances")
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest("Game", game_name=game_name):
multiworld = setup_solo_multiworld(world_type, ())
original_get_all_state = multiworld.get_all_state
def patched_get_all_state(use_cache: bool, allow_partial_entrances: bool = False):
self.assertTrue(allow_partial_entrances, (
"Before the connect_entrances step finishes, other worlds might still have partial entrances. "
"As such, any call to get_all_state must use allow_partial_entrances = True."
))
return original_get_all_state(use_cache, allow_partial_entrances)
multiworld.get_all_state = patched_get_all_state
for step in gen_steps:
with self.subTest("Step", step=step):
call_all(multiworld, step)

View File

@@ -1,6 +1,8 @@
import unittest
from typing import Callable, Dict, Optional
from typing_extensions import override
from BaseClasses import CollectionState, MultiWorld, Region
@@ -8,6 +10,7 @@ class TestHelpers(unittest.TestCase):
multiworld: MultiWorld
player: int = 1
@override
def setUp(self) -> None:
self.multiworld = MultiWorld(self.player)
self.multiworld.game[self.player] = "helper_test_game"
@@ -38,15 +41,15 @@ class TestHelpers(unittest.TestCase):
"TestRegion1": {"TestRegion2": "connection"},
"TestRegion2": {"TestRegion1": None},
}
reg_exit_set: Dict[str, set[str]] = {
"TestRegion1": {"TestRegion3"}
}
exit_rules: Dict[str, Callable[[CollectionState], bool]] = {
"TestRegion1": lambda state: state.has("test_item", self.player)
}
self.multiworld.regions += [Region(region, self.player, self.multiworld, regions[region]) for region in regions]
with self.subTest("Test Location Creation Helper"):
@@ -73,7 +76,7 @@ class TestHelpers(unittest.TestCase):
entrance_name = exit_name if exit_name else f"{parent} -> {exit_reg}"
self.assertEqual(exit_rules[exit_reg],
self.multiworld.get_entrance(entrance_name, self.player).access_rule)
for region in reg_exit_set:
current_region = self.multiworld.get_region(region, self.player)
current_region.add_exits(reg_exit_set[region])

View File

@@ -39,7 +39,7 @@ class TestImplemented(unittest.TestCase):
"""Tests that if a world creates slot data, it's json serializable."""
for game_name, world_type in AutoWorldRegister.world_types.items():
# has an await for generate_output which isn't being called
if game_name in {"Ocarina of Time", "Zillion"}:
if game_name in {"Ocarina of Time"}:
continue
multiworld = setup_solo_multiworld(world_type)
with self.subTest(game=game_name, seed=multiworld.seed):
@@ -117,3 +117,12 @@ class TestImplemented(unittest.TestCase):
f"\nUnexpectedly reachable locations in sphere {sphere_num}:"
f"\n{reachable_only_with_explicit}")
self.fail("Unreachable")
def test_no_items_or_locations_or_regions_submitted_in_init(self):
"""Test that worlds don't submit items/locations/regions to the multiworld in __init__"""
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest("Game", game=game_name):
multiworld = setup_solo_multiworld(world_type, ())
self.assertEqual(len(multiworld.itempool), 0)
self.assertEqual(len(multiworld.get_locations()), 0)
self.assertEqual(len(multiworld.get_regions()), 0)

View File

@@ -1,5 +1,6 @@
import unittest
from BaseClasses import CollectionState
from worlds.AutoWorld import AutoWorldRegister, call_all
from . import setup_solo_multiworld
@@ -8,12 +9,31 @@ class TestBase(unittest.TestCase):
def test_create_item(self):
"""Test that a world can successfully create all items in its datapackage"""
for game_name, world_type in AutoWorldRegister.world_types.items():
proxy_world = setup_solo_multiworld(world_type, ()).worlds[1]
multiworld = setup_solo_multiworld(world_type, steps=("generate_early", "create_regions", "create_items"))
proxy_world = multiworld.worlds[1]
for item_name in world_type.item_name_to_id:
test_state = CollectionState(multiworld)
with self.subTest("Create Item", item_name=item_name, game_name=game_name):
item = proxy_world.create_item(item_name)
with self.subTest("Item Name", item_name=item_name, game_name=game_name):
self.assertEqual(item.name, item_name)
if item.advancement:
with self.subTest("Item State Collect", item_name=item_name, game_name=game_name):
test_state.collect(item, True)
with self.subTest("Item State Remove", item_name=item_name, game_name=game_name):
test_state.remove(item)
self.assertEqual(test_state.prog_items, multiworld.state.prog_items,
"Item Collect -> Remove should restore empty state.")
else:
with self.subTest("Item State Collect No Change", item_name=item_name, game_name=game_name):
# Non-Advancement should not modify state.
test_state.collect(item)
self.assertEqual(test_state.prog_items, multiworld.state.prog_items)
def test_item_name_group_has_valid_item(self):
"""Test that all item name groups contain valid items. """
# This cannot test for Event names that you may have declared for logic, only sendable Items.
@@ -67,7 +87,7 @@ class TestBase(unittest.TestCase):
def test_itempool_not_modified(self):
"""Test that worlds don't modify the itempool after `create_items`"""
gen_steps = ("generate_early", "create_regions", "create_items")
additional_steps = ("set_rules", "generate_basic", "pre_fill")
additional_steps = ("set_rules", "connect_entrances", "generate_basic", "pre_fill")
excluded_games = ("Links Awakening DX", "Ocarina of Time", "SMZ3")
worlds_to_test = {game: world
for game, world in AutoWorldRegister.world_types.items() if game not in excluded_games}
@@ -84,7 +104,7 @@ class TestBase(unittest.TestCase):
def test_locality_not_modified(self):
"""Test that worlds don't modify the locality of items after duplicates are resolved"""
gen_steps = ("generate_early", "create_regions", "create_items")
additional_steps = ("set_rules", "generate_basic", "pre_fill")
additional_steps = ("set_rules", "connect_entrances", "generate_basic", "pre_fill")
worlds_to_test = {game: world for game, world in AutoWorldRegister.world_types.items()}
for game_name, world_type in worlds_to_test.items():
with self.subTest("Game", game=game_name):

View File

@@ -45,6 +45,12 @@ class TestBase(unittest.TestCase):
self.assertEqual(location_count, len(multiworld.get_locations()),
f"{game_name} modified locations count during rule creation")
call_all(multiworld, "connect_entrances")
self.assertEqual(region_count, len(multiworld.get_regions()),
f"{game_name} modified region count during rule creation")
self.assertEqual(location_count, len(multiworld.get_locations()),
f"{game_name} modified locations count during rule creation")
call_all(multiworld, "generate_basic")
self.assertEqual(region_count, len(multiworld.get_regions()),
f"{game_name} modified region count during generate_basic")

View File

@@ -1,16 +1,21 @@
import unittest
from BaseClasses import MultiWorld
from worlds.AutoWorld import AutoWorldRegister
from . import setup_solo_multiworld
class TestWorldMemory(unittest.TestCase):
def test_leak(self):
def test_leak(self) -> None:
"""Tests that worlds don't leak references to MultiWorld or themselves with default options."""
import gc
import weakref
refs: dict[str, weakref.ReferenceType[MultiWorld]] = {}
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest("Game", game_name=game_name):
with self.subTest("Game creation", game_name=game_name):
weak = weakref.ref(setup_solo_multiworld(world_type))
gc.collect()
refs[game_name] = weak
gc.collect()
for game_name, weak in refs.items():
with self.subTest("Game cleanup", game_name=game_name):
self.assertFalse(weak(), "World leaked a reference")

View File

@@ -3,7 +3,7 @@ from worlds.AutoWorld import AutoWorldRegister
class TestNames(unittest.TestCase):
def test_item_names_format(self):
def test_item_names_format(self) -> None:
"""Item names must not be all numeric in order to differentiate between ID and name in !hint"""
for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename):
@@ -11,7 +11,7 @@ class TestNames(unittest.TestCase):
self.assertFalse(item_name.isnumeric(),
f"Item name \"{item_name}\" is invalid. It must not be numeric.")
def test_location_name_format(self):
def test_location_name_format(self) -> None:
"""Location names must not be all numeric in order to differentiate between ID and name in !hint_location"""
for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename):

View File

@@ -0,0 +1,11 @@
import unittest
from worlds.AutoWorld import AutoWorldRegister
from worlds.Files import AutoPatchRegister
class TestPatches(unittest.TestCase):
def test_patch_name_matches_game(self) -> None:
for game_name in AutoPatchRegister.patch_types:
with self.subTest(game=game_name):
self.assertIn(game_name, AutoWorldRegister.world_types.keys(),
f"Patch '{game_name}' does not match the name of any world.")

View File

@@ -2,11 +2,11 @@ import unittest
from BaseClasses import CollectionState
from worlds.AutoWorld import AutoWorldRegister
from . import setup_solo_multiworld
from . import setup_solo_multiworld, gen_steps
class TestBase(unittest.TestCase):
gen_steps = ["generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill"]
gen_steps = gen_steps
default_settings_unreachable_regions = {
"A Link to the Past": {

View File

@@ -0,0 +1,19 @@
import unittest
import os
class TestBase(unittest.TestCase):
def test_requirements_file_ends_on_newline(self):
"""Test that all requirements files end on a newline"""
import Utils
requirements_files = [Utils.local_path("requirements.txt"),
Utils.local_path("WebHostLib", "requirements.txt")]
worlds_path = Utils.local_path("worlds")
for entry in os.listdir(worlds_path):
requirements_path = os.path.join(worlds_path, entry, "requirements.txt")
if os.path.isfile(requirements_path):
requirements_files.append(requirements_path)
for requirements_file in requirements_files:
with self.subTest(path=requirements_file):
with open(requirements_file) as f:
self.assertEqual(f.read()[-1], "\n")

View File

@@ -0,0 +1,29 @@
import unittest
from worlds.AutoWorld import AutoWorldRegister, call_all
from . import setup_solo_multiworld
class TestBase(unittest.TestCase):
gen_steps = (
"generate_early",
"create_regions",
)
test_steps = (
"create_items",
"set_rules",
"connect_entrances",
"generate_basic",
"pre_fill",
)
def test_all_state_is_available(self):
"""Ensure all_state can be created at certain steps."""
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest("Game", game=game_name):
multiworld = setup_solo_multiworld(world_type, self.gen_steps)
for step in self.test_steps:
with self.subTest("Step", step=step):
call_all(multiworld, step)
self.assertTrue(multiworld.get_all_state(False, True))

View File

@@ -1,5 +1,5 @@
import unittest
from typing import List, Tuple
from typing import ClassVar, List, Tuple
from unittest import TestCase
from BaseClasses import CollectionState, Location, MultiWorld
@@ -7,6 +7,7 @@ from Fill import distribute_items_restrictive
from Options import Accessibility
from worlds.AutoWorld import AutoWorldRegister, call_all, call_single
from ..general import gen_steps, setup_multiworld
from ..param import classvar_matrix
class MultiworldTestBase(TestCase):
@@ -63,15 +64,18 @@ class TestAllGamesMultiworld(MultiworldTestBase):
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
@classvar_matrix(game=AutoWorldRegister.world_types.keys())
class TestTwoPlayerMulti(MultiworldTestBase):
game: ClassVar[str]
def test_two_player_single_game_fills(self) -> None:
"""Tests that a multiworld of two players for each registered game world can generate."""
for world_type in AutoWorldRegister.world_types.values():
self.multiworld = setup_multiworld([world_type, world_type], ())
for world in self.multiworld.worlds.values():
world.options.accessibility.value = Accessibility.option_full
self.assertSteps(gen_steps)
with self.subTest("filling multiworld", games=world_type.game, seed=self.multiworld.seed):
distribute_items_restrictive(self.multiworld)
call_all(self.multiworld, "post_fill")
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
world_type = AutoWorldRegister.world_types[self.game]
self.multiworld = setup_multiworld([world_type, world_type], ())
for world in self.multiworld.worlds.values():
world.options.accessibility.value = Accessibility.option_full
self.assertSteps(gen_steps)
with self.subTest("filling multiworld", games=world_type.game, seed=self.multiworld.seed):
distribute_items_restrictive(self.multiworld)
call_all(self.multiworld, "post_fill")
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")

46
test/param.py Normal file
View File

@@ -0,0 +1,46 @@
import itertools
import sys
from typing import Any, Callable, Iterable
def classvar_matrix(**kwargs: Iterable[Any]) -> Callable[[type], None]:
"""
Create a new class for each variation of input, allowing to generate a TestCase matrix / parametrization that
supports multi-threading and has better reporting for ``unittest --durations=...`` and ``pytest --durations=...``
than subtests.
The kwargs will be set as ClassVars in the newly created classes. Use as ::
@classvar_matrix(var_name=[value1, value2])
class MyTestCase(unittest.TestCase):
var_name: typing.ClassVar[...]
:param kwargs: A dict of ClassVars to set, where key is the variable name and value is a list of all values.
:return: A decorator to be applied to a class.
"""
keys: tuple[str]
values: Iterable[Iterable[Any]]
keys, values = zip(*kwargs.items())
values = map(lambda v: sorted(v) if isinstance(v, (set, frozenset)) else v, values)
permutations_dicts = [dict(zip(keys, v)) for v in itertools.product(*values)]
def decorator(cls: type) -> None:
mod = sys.modules[cls.__module__]
for permutation in permutations_dicts:
class Unrolled(cls): # type: ignore
pass
for k, v in permutation.items():
setattr(Unrolled, k, v)
params = ", ".join([f"{k}={repr(v)}" for k, v in permutation.items()])
params = f"{{{params}}}"
Unrolled.__module__ = cls.__module__
Unrolled.__qualname__ = f"{cls.__qualname__}{params}"
setattr(mod, f"{cls.__name__}{params}", Unrolled)
return None
return decorator

View File

@@ -378,6 +378,10 @@ class World(metaclass=AutoWorldRegister):
"""Method for setting the rules on the World's regions and locations."""
pass
def connect_entrances(self) -> None:
"""Method to finalize the source and target regions of the World's entrances"""
pass
def generate_basic(self) -> None:
"""
Useful for randomizing things that don't affect logic but are better to be determined before the output stage.

View File

@@ -87,7 +87,7 @@ class Component:
processes = weakref.WeakSet()
def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] = ()) -> None:
def launch_subprocess(func: Callable, name: str | None = None, args: Tuple[str, ...] = ()) -> None:
global processes
import multiprocessing
process = multiprocessing.Process(target=func, name=name, args=args)
@@ -95,6 +95,14 @@ def launch_subprocess(func: Callable, name: str = None, args: Tuple[str, ...] =
processes.add(process)
def launch(func: Callable, name: str | None = None, args: Tuple[str, ...] = ()) -> None:
from Utils import is_kivy_running
if is_kivy_running():
launch_subprocess(func, name, args)
else:
func(*args)
class SuffixIdentifier:
suffixes: Iterable[str]
@@ -111,7 +119,7 @@ class SuffixIdentifier:
def launch_textclient(*args):
import CommonClient
launch_subprocess(CommonClient.run_as_textclient, name="TextClient", args=args)
launch(CommonClient.run_as_textclient, name="TextClient", args=args)
def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, pathlib.Path]]:

View File

@@ -55,6 +55,7 @@ async def lock(ctx) -> None
async def unlock(ctx) -> None
async def get_hash(ctx) -> str
async def get_memory_size(ctx, domain: str) -> int
async def get_system(ctx) -> str
async def get_cores(ctx) -> dict[str, str]
async def ping(ctx) -> None
@@ -168,9 +169,10 @@ select dialog and they will be associated with BizHawkClient. This does not affe
associate the file extension with Archipelago.
`validate_rom` is called to figure out whether a given ROM belongs to your client. It will only be called when a ROM is
running on a system you specified in your `system` class variable. In most cases, that will be a single system and you
can be sure that you're not about to try to read from nonexistent domains or out of bounds. If you decide to claim this
ROM as yours, this is where you should do setup for things like `items_handling`.
running on a system you specified in your `system` class variable. Take extra care here, because your code will run
against ROMs that you have no control over. If you're reading an address deep in ROM, you might want to check the size
of ROM before you attempt to read it using `get_memory_size`. If you decide to claim this ROM as yours, this is where
you should do setup for things like `items_handling`.
`game_watcher` is the "main loop" of your client where you should be checking memory and sending new items to the ROM.
`BizHawkClient` will make sure that your `game_watcher` only runs when your client has validated the ROM, and will do
@@ -268,6 +270,8 @@ server connection before trying to interact with it.
- By default, the player will be asked to provide their slot name after connecting to the server and validating, and
that input will be used to authenticate with the `Connect` command. You can override `set_auth` in your own client to
set it automatically based on data in the ROM or on your client instance.
- Use `get_memory_size` inside `validate_rom` if you need to read at large addresses, in case some other game has a
smaller ROM size.
- You can override `on_package` in your client to watch raw packages, but don't forget you also have access to a
subclass of `CommonContext` and its API.
- You can import `BizHawkClientContext` for type hints using `typing.TYPE_CHECKING`. Importing it without conditions at

View File

@@ -10,7 +10,7 @@ import base64
import enum
import json
import sys
import typing
from typing import Any, Sequence
BIZHAWK_SOCKET_PORT_RANGE_START = 43055
@@ -44,10 +44,10 @@ class SyncError(Exception):
class BizHawkContext:
streams: typing.Optional[typing.Tuple[asyncio.StreamReader, asyncio.StreamWriter]]
streams: tuple[asyncio.StreamReader, asyncio.StreamWriter] | None
connection_status: ConnectionStatus
_lock: asyncio.Lock
_port: typing.Optional[int]
_port: int | None
def __init__(self) -> None:
self.streams = None
@@ -122,12 +122,12 @@ async def get_script_version(ctx: BizHawkContext) -> int:
return int(await ctx._send_message("VERSION"))
async def send_requests(ctx: BizHawkContext, req_list: typing.List[typing.Dict[str, typing.Any]]) -> typing.List[typing.Dict[str, typing.Any]]:
async def send_requests(ctx: BizHawkContext, req_list: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Sends a list of requests to the BizHawk connector and returns their responses.
It's likely you want to use the wrapper functions instead of this."""
responses = json.loads(await ctx._send_message(json.dumps(req_list)))
errors: typing.List[ConnectorError] = []
errors: list[ConnectorError] = []
for response in responses:
if response["type"] == "ERROR":
@@ -151,7 +151,7 @@ async def ping(ctx: BizHawkContext) -> None:
async def get_hash(ctx: BizHawkContext) -> str:
"""Gets the system name for the currently loaded ROM"""
"""Gets the hash value of the currently loaded ROM"""
res = (await send_requests(ctx, [{"type": "HASH"}]))[0]
if res["type"] != "HASH_RESPONSE":
@@ -160,6 +160,16 @@ async def get_hash(ctx: BizHawkContext) -> str:
return res["value"]
async def get_memory_size(ctx: BizHawkContext, domain: str) -> int:
"""Gets the size in bytes of the specified memory domain"""
res = (await send_requests(ctx, [{"type": "MEMORY_SIZE", "domain": domain}]))[0]
if res["type"] != "MEMORY_SIZE_RESPONSE":
raise SyncError(f"Expected response of type MEMORY_SIZE_RESPONSE but got {res['type']}")
return res["value"]
async def get_system(ctx: BizHawkContext) -> str:
"""Gets the system name for the currently loaded ROM"""
res = (await send_requests(ctx, [{"type": "SYSTEM"}]))[0]
@@ -170,7 +180,7 @@ async def get_system(ctx: BizHawkContext) -> str:
return res["value"]
async def get_cores(ctx: BizHawkContext) -> typing.Dict[str, str]:
async def get_cores(ctx: BizHawkContext) -> dict[str, str]:
"""Gets the preferred cores for systems with multiple cores. Only systems with multiple available cores have
entries."""
res = (await send_requests(ctx, [{"type": "PREFERRED_CORES"}]))[0]
@@ -223,8 +233,8 @@ async def set_message_interval(ctx: BizHawkContext, value: float) -> None:
raise SyncError(f"Expected response of type SET_MESSAGE_INTERVAL_RESPONSE but got {res['type']}")
async def guarded_read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tuple[int, int, str]],
guard_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> typing.Optional[typing.List[bytes]]:
async def guarded_read(ctx: BizHawkContext, read_list: Sequence[tuple[int, int, str]],
guard_list: Sequence[tuple[int, Sequence[int], str]]) -> list[bytes] | None:
"""Reads an array of bytes at 1 or more addresses if and only if every byte in guard_list matches its expected
value.
@@ -252,7 +262,7 @@ async def guarded_read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tu
"domain": domain
} for address, size, domain in read_list])
ret: typing.List[bytes] = []
ret: list[bytes] = []
for item in res:
if item["type"] == "GUARD_RESPONSE":
if not item["value"]:
@@ -266,7 +276,7 @@ async def guarded_read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tu
return ret
async def read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tuple[int, int, str]]) -> typing.List[bytes]:
async def read(ctx: BizHawkContext, read_list: Sequence[tuple[int, int, str]]) -> list[bytes]:
"""Reads data at 1 or more addresses.
Items in `read_list` should be organized `(address, size, domain)` where
@@ -278,8 +288,8 @@ async def read(ctx: BizHawkContext, read_list: typing.Sequence[typing.Tuple[int,
return await guarded_read(ctx, read_list, [])
async def guarded_write(ctx: BizHawkContext, write_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]],
guard_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> bool:
async def guarded_write(ctx: BizHawkContext, write_list: Sequence[tuple[int, Sequence[int], str]],
guard_list: Sequence[tuple[int, Sequence[int], str]]) -> bool:
"""Writes data to 1 or more addresses if and only if every byte in guard_list matches its expected value.
Items in `write_list` should be organized `(address, value, domain)` where
@@ -316,7 +326,7 @@ async def guarded_write(ctx: BizHawkContext, write_list: typing.Sequence[typing.
return True
async def write(ctx: BizHawkContext, write_list: typing.Sequence[typing.Tuple[int, typing.Sequence[int], str]]) -> None:
async def write(ctx: BizHawkContext, write_list: Sequence[tuple[int, Sequence[int], str]]) -> None:
"""Writes data to 1 or more addresses.
Items in write_list should be organized `(address, value, domain)` where

View File

@@ -5,9 +5,9 @@ A module containing the BizHawkClient base class and metaclass
from __future__ import annotations
import abc
from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional, Tuple, Union
from typing import TYPE_CHECKING, Any, ClassVar
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components, launch_subprocess
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components, launch as launch_component
if TYPE_CHECKING:
from .context import BizHawkClientContext
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
def launch_client(*args) -> None:
from .context import launch
launch_subprocess(launch, name="BizHawkClient", args=args)
launch_component(launch, name="BizHawkClient", args=args)
component = Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client,
@@ -24,9 +24,9 @@ components.append(component)
class AutoBizHawkClientRegister(abc.ABCMeta):
game_handlers: ClassVar[Dict[Tuple[str, ...], Dict[str, BizHawkClient]]] = {}
game_handlers: ClassVar[dict[tuple[str, ...], dict[str, BizHawkClient]]] = {}
def __new__(cls, name: str, bases: Tuple[type, ...], namespace: Dict[str, Any]) -> AutoBizHawkClientRegister:
def __new__(cls, name: str, bases: tuple[type, ...], namespace: dict[str, Any]) -> AutoBizHawkClientRegister:
new_class = super().__new__(cls, name, bases, namespace)
# Register handler
@@ -54,7 +54,7 @@ class AutoBizHawkClientRegister(abc.ABCMeta):
return new_class
@staticmethod
async def get_handler(ctx: "BizHawkClientContext", system: str) -> Optional[BizHawkClient]:
async def get_handler(ctx: "BizHawkClientContext", system: str) -> BizHawkClient | None:
for systems, handlers in AutoBizHawkClientRegister.game_handlers.items():
if system in systems:
for handler in handlers.values():
@@ -65,13 +65,13 @@ class AutoBizHawkClientRegister(abc.ABCMeta):
class BizHawkClient(abc.ABC, metaclass=AutoBizHawkClientRegister):
system: ClassVar[Union[str, Tuple[str, ...]]]
system: ClassVar[str | tuple[str, ...]]
"""The system(s) that the game this client is for runs on"""
game: ClassVar[str]
"""The game this client is for"""
patch_suffix: ClassVar[Optional[Union[str, Tuple[str, ...]]]]
patch_suffix: ClassVar[str | tuple[str, ...] | None]
"""The file extension(s) this client is meant to open and patch (e.g. ".apz3")"""
@abc.abstractmethod

View File

@@ -6,7 +6,7 @@ checking or launching the client, otherwise it will probably cause circular impo
import asyncio
import enum
import subprocess
from typing import Any, Dict, Optional
from typing import Any
from CommonClient import CommonContext, ClientCommandProcessor, get_base_parser, server_loop, logger, gui_enabled
import Patch
@@ -41,17 +41,18 @@ class BizHawkClientCommandProcessor(ClientCommandProcessor):
class BizHawkClientContext(CommonContext):
command_processor = BizHawkClientCommandProcessor
server_seed_name: str | None = None
auth_status: AuthStatus
password_requested: bool
client_handler: Optional[BizHawkClient]
slot_data: Optional[Dict[str, Any]] = None
rom_hash: Optional[str] = None
client_handler: BizHawkClient | None
slot_data: dict[str, Any] | None = None
rom_hash: str | None = None
bizhawk_ctx: BizHawkContext
watcher_timeout: float
"""The maximum amount of time the game watcher loop will wait for an update from the server before executing"""
def __init__(self, server_address: Optional[str], password: Optional[str]):
def __init__(self, server_address: str | None, password: str | None):
super().__init__(server_address, password)
self.auth_status = AuthStatus.NOT_AUTHENTICATED
self.password_requested = False
@@ -68,6 +69,8 @@ class BizHawkClientContext(CommonContext):
if cmd == "Connected":
self.slot_data = args.get("slot_data", None)
self.auth_status = AuthStatus.AUTHENTICATED
elif cmd == "RoomInfo":
self.server_seed_name = args.get("seed_name", None)
if self.client_handler is not None:
self.client_handler.on_package(self, cmd, args)
@@ -100,6 +103,7 @@ class BizHawkClientContext(CommonContext):
async def disconnect(self, allow_autoreconnect: bool=False):
self.auth_status = AuthStatus.NOT_AUTHENTICATED
self.server_seed_name = None
await super().disconnect(allow_autoreconnect)
@@ -231,20 +235,28 @@ async def _run_game(rom: str):
)
async def _patch_and_run_game(patch_file: str):
def _patch_and_run_game(patch_file: str):
try:
metadata, output_file = Patch.create_rom_file(patch_file)
Utils.async_start(_run_game(output_file))
return metadata
except Exception as exc:
logger.exception(exc)
Utils.messagebox("Error Patching Game", str(exc), True)
return {}
def launch(*launch_args) -> None:
def launch(*launch_args: str) -> None:
async def main():
parser = get_base_parser()
parser.add_argument("patch_file", default="", type=str, nargs="?", help="Path to an Archipelago patch file")
args = parser.parse_args(launch_args)
if args.patch_file != "":
metadata = _patch_and_run_game(args.patch_file)
if "server" in metadata:
args.connect = metadata["server"]
ctx = BizHawkClientContext(args.connect, args.password)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
@@ -252,9 +264,6 @@ def launch(*launch_args) -> None:
ctx.run_gui()
ctx.run_cli()
if args.patch_file != "":
Utils.async_start(_patch_and_run_game(args.patch_file))
watcher_task = asyncio.create_task(_game_watcher(ctx), name="GameWatcher")
try:
@@ -267,6 +276,6 @@ def launch(*launch_args) -> None:
Utils.init_logging("BizHawkClient", exception_logger="Client")
import colorama
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main())
colorama.deinit()

View File

@@ -3,4 +3,4 @@ mpyq>=0.2.5
portpicker>=1.5.2
aiohttp>=3.8.4
loguru>=0.7.0
protobuf==3.20.3
protobuf==3.20.3

View File

@@ -1,9 +1,8 @@
from __future__ import annotations
from typing import Dict
from dataclasses import dataclass
from Options import Choice, Option, DefaultOnToggle, DeathLink, Range, Toggle, PerGameCommonOptions
from Options import Choice, DefaultOnToggle, DeathLink, Range, Toggle, PerGameCommonOptions
class FreeincarnateMax(Range):

View File

@@ -1,6 +1,6 @@
from BaseClasses import MultiWorld, Region, Entrance, LocationProgressType
from Options import PerGameCommonOptions
from .Locations import location_table, LocationData, AdventureLocation, dragon_room_to_region
from .Locations import location_table, AdventureLocation, dragon_room_to_region
def connect(world: MultiWorld, player: int, source: str, target: str, rule: callable = lambda state: True,

View File

@@ -2,15 +2,15 @@ import hashlib
import json
import os
import zipfile
from typing import Optional, Any
import Utils
from .Locations import AdventureLocation, LocationData
from settings import get_settings
from worlds.Files import APPatch, AutoPatchRegister
from typing import Any
import bsdiff4
import Utils
from settings import get_settings
from worlds.Files import APPatch, AutoPatchRegister
from .Locations import LocationData
ADVENTUREHASH: str = "157bddb7192754a45372be196797f284"

View File

@@ -1,35 +1,24 @@
import base64
import copy
import itertools
import math
import os
import settings
import typing
from enum import IntFlag
from typing import Any, ClassVar, Dict, List, Optional, Set, Tuple
from typing import ClassVar, Dict, Optional, Tuple
from BaseClasses import Entrance, Item, ItemClassification, MultiWorld, Region, Tutorial, \
LocationProgressType
import settings
from BaseClasses import Item, ItemClassification, MultiWorld, Tutorial, LocationProgressType
from Utils import __version__
from Options import AssembleOptions
from worlds.AutoWorld import WebWorld, World
from Fill import fill_restrictive
from worlds.generic.Rules import add_rule, set_rule
from .Options import DragonRandoType, DifficultySwitchA, DifficultySwitchB, \
AdventureOptions
from .Rom import get_base_rom_bytes, get_base_rom_path, AdventureDeltaPatch, apply_basepatch, \
AdventureAutoCollectLocation
from worlds.LauncherComponents import Component, components, SuffixIdentifier
from .Items import item_table, ItemData, nothing_item_id, event_table, AdventureItem, standard_item_max
from .Locations import location_table, base_location_id, LocationData, get_random_room_in_regions
from .Offsets import static_item_data_location, items_ram_start, static_item_element_size, item_position_table, \
static_first_dragon_index, connector_port_offset, yorgle_speed_data_location, grundle_speed_data_location, \
rhindle_speed_data_location, item_ram_addresses, start_castle_values, start_castle_offset
from .Options import DragonRandoType, DifficultySwitchA, DifficultySwitchB, AdventureOptions
from .Regions import create_regions
from .Rom import get_base_rom_bytes, get_base_rom_path, AdventureDeltaPatch, apply_basepatch, AdventureAutoCollectLocation
from .Rules import set_rules
from worlds.LauncherComponents import Component, components, SuffixIdentifier
# Adventure
components.append(Component('Adventure Client', 'AdventureClient', file_identifier=SuffixIdentifier('.apadvn')))

View File

@@ -261,6 +261,6 @@ def launch():
# options = Utils.get_options()
import colorama
colorama.init()
colorama.just_fix_windows_console()
asyncio.run(main())
colorama.deinit()

View File

@@ -141,9 +141,12 @@ def set_dw_rules(world: "HatInTimeWorld"):
add_dw_rules(world, all_clear)
add_rule(main_stamp, main_objective.access_rule)
add_rule(all_clear, main_objective.access_rule)
# Only set bonus stamp rules if we don't auto complete bonuses
# Only set bonus stamp rules to require All Clear if we don't auto complete bonuses
if not world.options.DWAutoCompleteBonuses and not world.is_bonus_excluded(all_clear.name):
add_rule(bonus_stamps, all_clear.access_rule)
else:
# As soon as the Main Objective is completed, the bonuses auto-complete.
add_rule(bonus_stamps, main_objective.access_rule)
if world.options.DWShuffle:
for i in range(len(world.dw_shuffle)-1):
@@ -343,6 +346,7 @@ def create_enemy_events(world: "HatInTimeWorld"):
def set_enemy_rules(world: "HatInTimeWorld"):
no_tourist = "Camera Tourist" in world.excluded_dws or "Camera Tourist" in world.excluded_bonuses
difficulty = get_difficulty(world)
for enemy, regions in hit_list.items():
if no_tourist and enemy in bosses:
@@ -372,6 +376,14 @@ def set_enemy_rules(world: "HatInTimeWorld"):
or state.has("Zipline Unlock - The Lava Cake Path", world.player)
or state.has("Zipline Unlock - The Windmill Path", world.player))
elif enemy == "Toilet":
if area == "Toilet of Doom":
# The boss firewall is in the way and can only be skipped on Expert logic using a cherry hover.
add_rule(event, lambda state: has_paintings(state, world, 1, allow_skip=difficulty == Difficulty.EXPERT))
if difficulty < Difficulty.HARD:
# Hard logic and above can cross the boss arena gap with a cherry bridge.
add_rule(event, lambda state: can_use_hookshot(state, world))
elif enemy == "Director":
if area == "Dead Bird Studio Basement":
add_rule(event, lambda state: can_use_hookshot(state, world))
@@ -430,7 +442,7 @@ hit_list = {
# Bosses
"Mafia Boss": ["Down with the Mafia!", "Encore! Encore!", "Boss Rush"],
"Conductor": ["Dead Bird Studio Basement", "Killing Two Birds", "Boss Rush"],
"Director": ["Dead Bird Studio Basement", "Killing Two Birds", "Boss Rush"],
"Toilet": ["Toilet of Doom", "Boss Rush"],
"Snatcher": ["Your Contract has Expired", "Breaching the Contract", "Boss Rush",
@@ -454,7 +466,7 @@ triple_enemy_locations = [
bosses = [
"Mafia Boss",
"Conductor",
"Director",
"Toilet",
"Snatcher",
"Toxic Flower",

View File

@@ -206,7 +206,7 @@ ahit_locations = {
"Subcon Village - Graveyard Ice Cube": LocData(2000325077, "Subcon Forest Area"),
"Subcon Village - House Top": LocData(2000325471, "Subcon Forest Area"),
"Subcon Village - Ice Cube House": LocData(2000325469, "Subcon Forest Area"),
"Subcon Village - Snatcher Statue Chest": LocData(2000323730, "Subcon Forest Area", paintings=1),
"Subcon Village - Snatcher Statue Chest": LocData(2000323730, "Subcon Forest Behind Boss Firewall"),
"Subcon Village - Stump Platform Chest": LocData(2000323729, "Subcon Forest Area"),
"Subcon Forest - Giant Tree Climb": LocData(2000325470, "Subcon Forest Area"),
@@ -233,7 +233,7 @@ ahit_locations = {
"Subcon Forest - Long Tree Climb Chest": LocData(2000323734, "Subcon Forest Area",
required_hats=[HatType.DWELLER], paintings=2),
"Subcon Forest - Boss Arena Chest": LocData(2000323735, "Subcon Forest Area"),
"Subcon Forest - Boss Arena Chest": LocData(2000323735, "Subcon Forest Boss Arena"),
"Subcon Forest - Manor Rooftop": LocData(2000325466, "Subcon Forest Area",
hit_type=HitType.dweller_bell, paintings=1),
@@ -264,7 +264,6 @@ ahit_locations = {
required_hats=[HatType.DWELLER], paintings=3),
"Subcon Forest - Tall Tree Hookshot Swing": LocData(2000324766, "Subcon Forest Area",
required_hats=[HatType.DWELLER],
hookshot=True,
paintings=3),
@@ -323,7 +322,7 @@ ahit_locations = {
"Alpine Skyline - The Twilight Path": LocData(2000334434, "Alpine Skyline Area", required_hats=[HatType.DWELLER]),
"Alpine Skyline - The Twilight Bell: Wide Purple Platform": LocData(2000336478, "The Twilight Bell"),
"Alpine Skyline - The Twilight Bell: Ice Platform": LocData(2000335826, "The Twilight Bell"),
"Alpine Skyline - Goat Outpost Horn": LocData(2000334760, "Alpine Skyline Area"),
"Alpine Skyline - Goat Outpost Horn": LocData(2000334760, "Alpine Skyline Area (TIHS)", hookshot=True),
"Alpine Skyline - Windy Passage": LocData(2000334776, "Alpine Skyline Area (TIHS)", hookshot=True),
"Alpine Skyline - The Windmill: Inside Pon Cluster": LocData(2000336395, "The Windmill"),
"Alpine Skyline - The Windmill: Entrance": LocData(2000335783, "The Windmill"),
@@ -407,12 +406,12 @@ act_completions = {
hit_type=HitType.umbrella_or_brewing, hookshot=True, paintings=1),
"Act Completion (Queen Vanessa's Manor)": LocData(2000312017, "Queen Vanessa's Manor",
hit_type=HitType.umbrella, paintings=1),
hit_type=HitType.dweller_bell, paintings=1),
"Act Completion (Mail Delivery Service)": LocData(2000312032, "Mail Delivery Service",
required_hats=[HatType.SPRINT]),
"Act Completion (Your Contract has Expired)": LocData(2000311390, "Your Contract has Expired",
"Act Completion (Your Contract has Expired)": LocData(2000311390, "Your Contract has Expired - Post Fight",
hit_type=HitType.umbrella),
"Act Completion (Time Rift - Pipe)": LocData(2000313069, "Time Rift - Pipe", hookshot=True),
@@ -878,7 +877,7 @@ snatcher_coins = {
dlc_flags=HatDLC.death_wish),
"Snatcher Coin - Top of HQ (DW: BTH)": LocData(0, "Beat the Heat", snatcher_coin="Snatcher Coin - Top of HQ",
dlc_flags=HatDLC.death_wish),
hit_type=HitType.umbrella, dlc_flags=HatDLC.death_wish),
"Snatcher Coin - Top of Tower": LocData(0, "Mafia Town Area (HUMT)", snatcher_coin="Snatcher Coin - Top of Tower",
dlc_flags=HatDLC.death_wish),
@@ -977,7 +976,6 @@ event_locs = {
**snatcher_coins,
"HUMT Access": LocData(0, "Heating Up Mafia Town"),
"TOD Access": LocData(0, "Toilet of Doom"),
"YCHE Access": LocData(0, "Your Contract has Expired"),
"AFR Access": LocData(0, "Alpine Free Roam"),
"TIHS Access": LocData(0, "The Illness has Spread"),

View File

@@ -338,7 +338,7 @@ class MinExtraYarn(Range):
There must be at least this much more yarn over the total number of yarn needed to craft all hats.
For example, if this option's value is 10, and the total yarn needed to craft all hats is 40,
there must be at least 50 yarn in the pool."""
display_name = "Max Extra Yarn"
display_name = "Min Extra Yarn"
range_start = 5
range_end = 15
default = 10

View File

@@ -347,7 +347,7 @@ def create_regions(world: "HatInTimeWorld"):
sf_act3 = create_region_and_connect(world, "Toilet of Doom", "Subcon Forest - Act 3", subcon_forest)
sf_act4 = create_region_and_connect(world, "Queen Vanessa's Manor", "Subcon Forest - Act 4", subcon_forest)
sf_act5 = create_region_and_connect(world, "Mail Delivery Service", "Subcon Forest - Act 5", subcon_forest)
create_region_and_connect(world, "Your Contract has Expired", "Subcon Forest - Finale", subcon_forest)
sf_finale = create_region_and_connect(world, "Your Contract has Expired", "Subcon Forest - Finale", subcon_forest)
# ------------------------------------------- ALPINE SKYLINE ------------------------------------------ #
alpine_skyline = create_region_and_connect(world, "Alpine Skyline", "Telescope -> Alpine Skyline", spaceship)
@@ -386,11 +386,24 @@ def create_regions(world: "HatInTimeWorld"):
create_rift_connections(world, create_region(world, "Time Rift - Bazaar"))
sf_area: Region = create_region(world, "Subcon Forest Area")
sf_behind_boss_firewall: Region = create_region(world, "Subcon Forest Behind Boss Firewall")
sf_boss_arena: Region = create_region(world, "Subcon Forest Boss Arena")
sf_area.connect(sf_behind_boss_firewall, "SF Area -> SF Behind Boss Firewall")
sf_behind_boss_firewall.connect(sf_boss_arena, "SF Behind Boss Firewall -> SF Boss Arena")
sf_act1.connect(sf_area, "Subcon Forest Entrance CO")
sf_act2.connect(sf_area, "Subcon Forest Entrance SW")
sf_act3.connect(sf_area, "Subcon Forest Entrance TOD")
sf_act4.connect(sf_area, "Subcon Forest Entrance QVM")
sf_act5.connect(sf_area, "Subcon Forest Entrance MDS")
# YCHE puts the player directly in the boss arena, with no access to the rest of Subcon Forest by default.
sf_finale.connect(sf_boss_arena, "Subcon Forest Entrance YCHE")
# To support the Snatcher Hover expert logic for Act Completion (Your Contract has Expired), the act completion has
# to go in a separate region because the Snatcher Hover gives direct access to the Act Completion, but does not
# give access to the act itself.
sf_finale_post_fight: Region = create_region(world, "Your Contract has Expired - Post Fight")
# This connection must never have any rules placed on it because they will not be inherited when setting up act
# connections, only the rules for the entrances to the act and the rules for the Act Completion are inherited.
sf_finale.connect(sf_finale_post_fight, "YCHE -> YCHE - Post Fight")
create_rift_connections(world, create_region(world, "Time Rift - Sleepy Subcon"))
create_rift_connections(world, create_region(world, "Time Rift - Pipe"))
@@ -947,6 +960,16 @@ def get_shuffled_region(world: "HatInTimeWorld", region: str) -> str:
return name
def get_region_shuffled_to(world: "HatInTimeWorld", region: str) -> str:
if world.options.ActRandomizer:
original_ci: str = chapter_act_info[region]
shuffled_ci = world.act_connections[original_ci]
return next(act_name for act_name, ci in chapter_act_info.items()
if ci == shuffled_ci)
else:
return region
def get_region_location_count(world: "HatInTimeWorld", region_name: str, included_only: bool = True) -> int:
count = 0
region = world.multiworld.get_region(region_name, world.player)

View File

@@ -414,7 +414,7 @@ def set_moderate_rules(world: "HatInTimeWorld"):
# Moderate: Mystifying Time Mesa time trial without hats
set_rule(world.multiworld.get_location("Alpine Skyline - Mystifying Time Mesa: Zipline", world.player),
lambda state: can_use_hookshot(state, world))
lambda state: True)
# Moderate: Goat Refinery from TIHS with Sprint only
add_rule(world.multiworld.get_location("Alpine Skyline - Goat Refinery", world.player),
@@ -481,9 +481,8 @@ def set_hard_rules(world: "HatInTimeWorld"):
set_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree B", world.player),
lambda state: has_paintings(state, world, 3))
# Cherry bridge over boss arena gap (painting still expected)
set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player),
lambda state: has_paintings(state, world, 1, False) or state.has("YCHE Access", world.player))
# Cherry bridge over boss arena gap
set_rule(world.get_entrance("SF Behind Boss Firewall -> SF Boss Arena"), lambda state: True)
set_rule(world.multiworld.get_location("Subcon Forest - Noose Treehouse", world.player),
lambda state: has_paintings(state, world, 2, True))
@@ -493,9 +492,6 @@ def set_hard_rules(world: "HatInTimeWorld"):
lambda state: has_paintings(state, world, 3, True))
# SDJ
add_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player),
lambda state: can_use_hat(state, world, HatType.SPRINT) and has_paintings(state, world, 2), "or")
add_rule(world.multiworld.get_location("Act Completion (Time Rift - Curly Tail Trail)", world.player),
lambda state: can_use_hat(state, world, HatType.SPRINT), "or")
@@ -533,7 +529,10 @@ def set_expert_rules(world: "HatInTimeWorld"):
# Expert: Mafia Town - Above Boats, Top of Lighthouse, and Hot Air Balloon with nothing
set_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player), lambda state: True)
set_rule(world.multiworld.get_location("Mafia Town - Top of Lighthouse", world.player), lambda state: True)
set_rule(world.multiworld.get_location("Mafia Town - Hot Air Balloon", world.player), lambda state: True)
# There are not enough buckets/beach balls to bucket/ball hover in Heating Up Mafia Town, so any other Mafia Town
# act is required.
add_rule(world.multiworld.get_location("Mafia Town - Hot Air Balloon", world.player),
lambda state: state.can_reach_region("Mafia Town Area", world.player), "or")
# Expert: Clear Dead Bird Studio with nothing
for loc in world.multiworld.get_region("Dead Bird Studio - Post Elevator Area", world.player).locations:
@@ -566,31 +565,65 @@ def set_expert_rules(world: "HatInTimeWorld"):
lambda state: True)
# Expert: Cherry Hovering
subcon_area = world.multiworld.get_region("Subcon Forest Area", world.player)
yche = world.multiworld.get_region("Your Contract has Expired", world.player)
entrance = yche.connect(subcon_area, "Subcon Forest Entrance YCHE")
# Skipping the boss firewall is possible with a Cherry Hover.
set_rule(world.get_entrance("SF Area -> SF Behind Boss Firewall"),
lambda state: has_paintings(state, world, 1, True))
# The boss arena gap can be crossed in reverse with a Cherry Hover.
subcon_boss_arena = world.get_region("Subcon Forest Boss Arena")
subcon_behind_boss_firewall = world.get_region("Subcon Forest Behind Boss Firewall")
subcon_boss_arena.connect(subcon_behind_boss_firewall, "SF Boss Arena -> SF Behind Boss Firewall")
if world.options.NoPaintingSkips:
add_rule(entrance, lambda state: has_paintings(state, world, 1))
subcon_area = world.get_region("Subcon Forest Area")
# The boss firewall can be skipped in reverse with a Cherry Hover, but it is not possible to remove the boss
# firewall from reverse because the paintings to burn to remove the firewall are on the other side of the firewall.
# Therefore, a painting skip is required. The paintings could be burned by already having access to
# "Subcon Forest Area" through another entrance, but making a new connection to "Subcon Forest Area" in that case
# would be pointless.
if not world.options.NoPaintingSkips:
# The import cannot be done at the module-level because it would cause a circular import.
from .Regions import get_region_shuffled_to
subcon_behind_boss_firewall.connect(subcon_area, "SF Behind Boss Firewall -> SF Area")
# Because the Your Contract has Expired entrance can now reach "Subcon Forest Area", it needs to be connected to
# each of the Subcon Forest Time Rift entrances, like the other Subcon Forest Acts.
yche = world.get_region("Your Contract has Expired")
def connect_to_shuffled_act_at(original_act_name):
region_name = get_region_shuffled_to(world, original_act_name)
return yche.connect(world.get_region(region_name), f"{original_act_name} Portal - Entrance YCHE")
# Rules copied from `Rules.set_rift_rules()` with painting logic removed because painting skips must be
# available.
entrance = connect_to_shuffled_act_at("Time Rift - Pipe")
add_rule(entrance, lambda state: can_clear_required_act(state, world, "Subcon Forest - Act 2"))
reg_act_connection(world, world.get_entrance("Subcon Forest - Act 2").connected_region, entrance)
entrance = connect_to_shuffled_act_at("Time Rift - Village")
add_rule(entrance, lambda state: can_clear_required_act(state, world, "Subcon Forest - Act 4"))
reg_act_connection(world, world.get_entrance("Subcon Forest - Act 4").connected_region, entrance)
entrance = connect_to_shuffled_act_at("Time Rift - Sleepy Subcon")
add_rule(entrance, lambda state: has_relic_combo(state, world, "UFO"))
set_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player),
lambda state: can_use_hookshot(state, world) and can_hit(state, world)
and has_paintings(state, world, 1, True))
# Set painting rules only. Skipping paintings is determined in has_paintings
set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player),
lambda state: has_paintings(state, world, 1, True))
set_rule(world.multiworld.get_location("Subcon Forest - Magnet Badge Bush", world.player),
lambda state: has_paintings(state, world, 3, True))
# You can cherry hover to Snatcher's post-fight cutscene, which completes the level without having to fight him
subcon_area.connect(yche, "Snatcher Hover")
set_rule(world.multiworld.get_location("Act Completion (Your Contract has Expired)", world.player),
lambda state: True)
yche_post_fight = world.get_region("Your Contract has Expired - Post Fight")
subcon_area.connect(yche_post_fight, "Snatcher Hover")
# Cherry Hover from YCHE also works, so there are no requirements for the Act Completion.
set_rule(world.get_location("Act Completion (Your Contract has Expired)"), lambda state: True)
if world.is_dlc2():
# Expert: clear Rush Hour with nothing
if not world.options.NoTicketSkips:
if world.options.NoTicketSkips != NoTicketSkips.option_true:
set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), lambda state: True)
else:
set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player),
@@ -681,12 +714,18 @@ def set_subcon_rules(world: "HatInTimeWorld"):
lambda state: can_use_hat(state, world, HatType.BREWING) or state.has("Umbrella", world.player)
or can_use_hat(state, world, HatType.DWELLER))
# You can't skip over the boss arena wall without cherry hover, so these two need to be set this way
set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player),
lambda state: state.has("TOD Access", world.player) and can_use_hookshot(state, world)
and has_paintings(state, world, 1, False) or state.has("YCHE Access", world.player))
# You can't skip over the boss arena wall without cherry hover.
set_rule(world.get_entrance("SF Area -> SF Behind Boss Firewall"),
lambda state: has_paintings(state, world, 1, False))
# The painting wall can't be skipped without cherry hover, which is Expert
# The hookpoints to cross the boss arena gap are only present in Toilet of Doom.
set_rule(world.get_entrance("SF Behind Boss Firewall -> SF Boss Arena"),
lambda state: state.has("TOD Access", world.player)
and can_use_hookshot(state, world))
# The Act Completion is in the Toilet of Doom region, so the same rules as passing the boss firewall and crossing
# the boss arena gap are required. "TOD Access" is implied from the region so does not need to be included in the
# rule.
set_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player),
lambda state: can_use_hookshot(state, world) and can_hit(state, world)
and has_paintings(state, world, 1, False))
@@ -739,7 +778,7 @@ def set_dlc1_rules(world: "HatInTimeWorld"):
# This particular item isn't present in Act 3 for some reason, yes in vanilla too
add_rule(world.multiworld.get_location("The Arctic Cruise - Toilet", world.player),
lambda state: state.can_reach("Bon Voyage!", "Region", world.player)
lambda state: (state.can_reach("Bon Voyage!", "Region", world.player) and can_use_hookshot(state, world))
or state.can_reach("Ship Shape", "Region", world.player))

View File

@@ -12,13 +12,13 @@ from .DeathWishRules import set_dw_rules, create_enemy_events, hit_list, bosses
from worlds.AutoWorld import World, WebWorld, CollectionState
from worlds.generic.Rules import add_rule
from typing import List, Dict, TextIO
from worlds.LauncherComponents import Component, components, icon_paths, launch_subprocess, Type
from worlds.LauncherComponents import Component, components, icon_paths, launch as launch_component, Type
from Utils import local_path
def launch_client():
from .Client import launch
launch_subprocess(launch, name="AHITClient")
launch_component(launch, name="AHITClient")
components.append(Component("A Hat in Time Client", "AHITClient", func=launch_client,

View File

@@ -21,7 +21,7 @@
3. Click the **Betas** tab. In the **Beta Participation** dropdown, select `tcplink`.
While it downloads, you can subscribe to the [Archipelago workshop mod.]((https://steamcommunity.com/sharedfiles/filedetails/?id=3026842601))
While it downloads, you can subscribe to the [Archipelago workshop mod](https://steamcommunity.com/sharedfiles/filedetails/?id=3026842601).
4. Once the game finishes downloading, start it up.
@@ -62,4 +62,4 @@ The level that the relic set unlocked will stay unlocked.
### When I start a new save file, the intro cinematic doesn't get skipped, Hat Kid's body is missing and the mod doesn't work!
There is a bug on older versions of A Hat in Time that causes save file creation to fail to work properly
if you have too many save files. Delete them and it should fix the problem.
if you have too many save files. Delete them and it should fix the problem.

View File

@@ -119,7 +119,9 @@ def KholdstareDefeatRule(state, player: int) -> bool:
def VitreousDefeatRule(state, player: int) -> bool:
return can_shoot_arrows(state, player) or has_melee_weapon(state, player)
return ((can_shoot_arrows(state, player) and can_use_bombs(state, player, 10))
or can_shoot_arrows(state, player, 35) or state.has("Silver Bow", player)
or has_melee_weapon(state, player))
def TrinexxDefeatRule(state, player: int) -> bool:

View File

@@ -464,7 +464,7 @@ async def track_locations(ctx, roomid, roomdata) -> bool:
snes_logger.info(f"Discarding recent {len(new_locations)} checks as ROM Status has changed.")
return False
else:
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": new_locations}])
await ctx.check_locations(new_locations)
await snes_flush_writes(ctx)
return True

View File

@@ -484,8 +484,7 @@ def generate_itempool(world):
if multiworld.randomize_cost_types[player]:
# Heart and Arrow costs require all Heart Container/Pieces and Arrow Upgrades to be advancement items for logic
for item in items:
if (item.name in ("Boss Heart Container", "Sanctuary Heart Container", "Piece of Heart")
or "Arrow Upgrade" in item.name):
if item.name in ("Boss Heart Container", "Sanctuary Heart Container", "Piece of Heart"):
item.classification = ItemClassification.progression
else:
# Otherwise, logic has some branches where having 4 hearts is one possible requirement (of several alternatives)
@@ -713,7 +712,7 @@ def get_pool_core(world, player: int):
pool.remove("Rupees (20)")
if retro_bow:
replace = {'Single Arrow', 'Arrows (10)', 'Arrow Upgrade (+5)', 'Arrow Upgrade (+10)', 'Arrow Upgrade (50)'}
replace = {'Single Arrow', 'Arrows (10)', 'Arrow Upgrade (+5)', 'Arrow Upgrade (+10)', 'Arrow Upgrade (70)'}
pool = ['Rupees (5)' if item in replace else item for item in pool]
if world.small_key_shuffle[player] == small_key_shuffle.option_universal:
pool.extend(diff.universal_keys)

View File

@@ -7,7 +7,7 @@ from worlds.AutoWorld import World
def GetBeemizerItem(world, player: int, item):
item_name = item if isinstance(item, str) else item.name
if item_name not in trap_replaceable:
if item_name not in trap_replaceable or player in world.groups:
return item
# first roll - replaceable item should be replaced, within beemizer_total_chance
@@ -110,9 +110,9 @@ item_table = {'Bow': ItemData(IC.progression, None, 0x0B, 'You have\nchosen the\
'Crystal 7': ItemData(IC.progression, 'Crystal', (0x08, 0x34, 0x64, 0x40, 0x7C, 0x06), None, None, None, None, None, None, "a blue crystal"),
'Single Arrow': ItemData(IC.filler, None, 0x43, 'a lonely arrow\nsits here.', 'and the arrow', 'stick-collecting kid', 'sewing needle for sale', 'fungus for arrow', 'archer boy sews again', 'an arrow'),
'Arrows (10)': ItemData(IC.filler, None, 0x44, 'This will give\nyou ten shots\nwith your bow!', 'and the arrow pack','stick-collecting kid', 'sewing kit for sale', 'fungus for arrows', 'archer boy sews again','ten arrows'),
'Arrow Upgrade (+10)': ItemData(IC.useful, None, 0x54, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'),
'Arrow Upgrade (+5)': ItemData(IC.useful, None, 0x53, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'),
'Arrow Upgrade (70)': ItemData(IC.useful, None, 0x4D, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'),
'Arrow Upgrade (+10)': ItemData(IC.progression_skip_balancing, None, 0x54, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'),
'Arrow Upgrade (+5)': ItemData(IC.progression_skip_balancing, None, 0x53, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'),
'Arrow Upgrade (70)': ItemData(IC.progression_skip_balancing, None, 0x4D, 'increase arrow\nstorage, low\nlow price', 'and the quiver', 'quiver-enlarging kid', 'arrow boost for sale', 'witch and more skewers', 'upgrade boy sews more again', 'arrow capacity'),
'Single Bomb': ItemData(IC.filler, None, 0x27, 'I make things\ngo BOOM! But\njust once.', 'and the explosion', 'the bomb-holding kid', 'firecracker for sale', 'blend fungus into bomb', '\'splosion boy explodes again', 'a bomb'),
'Bombs (3)': ItemData(IC.filler, None, 0x28, 'I make things\ngo triple\nBOOM!!!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'three bombs'),
'Bombs (10)': ItemData(IC.filler, None, 0x31, 'I make things\ngo BOOM! Ten\ntimes!', 'and the explosions', 'the bomb-holding kid', 'firecrackers for sale', 'blend fungus into bombs', '\'splosion boy explodes again', 'ten bombs'),

View File

@@ -515,10 +515,15 @@ def _populate_sprite_table():
logging.debug(f"Spritefile {file} could not be loaded as a valid sprite.")
with concurrent.futures.ThreadPoolExecutor() as pool:
for dir in [user_path('data', 'sprites', 'alttpr'), user_path('data', 'sprites', 'custom')]:
sprite_paths = [user_path('data', 'sprites', 'alttpr'), user_path('data', 'sprites', 'custom')]
for dir in [dir for dir in sprite_paths if os.path.isdir(dir)]:
for file in os.listdir(dir):
pool.submit(load_sprite_from_file, os.path.join(dir, file))
if "link" not in _sprite_table:
logging.info("Link sprite was not loaded. Loading link from base rom")
load_sprite_from_file(get_base_rom_path())
class Sprite():
sprite_size = 28672
@@ -554,6 +559,11 @@ class Sprite():
self.sprite = filedata[0x80000:0x87000]
self.palette = filedata[0xDD308:0xDD380]
self.glove_palette = filedata[0xDEDF5:0xDEDF9]
h = hashlib.md5()
h.update(filedata)
if h.hexdigest() == LTTPJPN10HASH:
self.name = "Link"
self.author_name = "Nintendo"
elif filedata.startswith(b'ZSPR'):
self.from_zspr(filedata, filename)
else:
@@ -1547,9 +1557,9 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
rom.write_byte(0x18003B, 0x01 if world.map_shuffle[player] else 0x00) # maps showing crystals on overworld
# compasses showing dungeon count
if local_world.clock_mode or not world.dungeon_counters[player]:
if local_world.clock_mode or world.dungeon_counters[player] == 'off':
rom.write_byte(0x18003C, 0x00) # Currently must be off if timer is on, because they use same HUD location
elif world.dungeon_counters[player] is True:
elif world.dungeon_counters[player] == 'on':
rom.write_byte(0x18003C, 0x02) # always on
elif world.compass_shuffle[player] or world.dungeon_counters[player] == 'pickup':
rom.write_byte(0x18003C, 0x01) # show on pickup

View File

@@ -592,9 +592,9 @@ def global_rules(multiworld: MultiWorld, player: int):
lambda state: can_kill_most_things(state, player, 8) and has_fire_source(state, player) and state.multiworld.get_entrance('Ganons Tower Torch Rooms', player).parent_region.dungeon.bosses['middle'].can_defeat(state))
set_rule(multiworld.get_location('Ganons Tower - Mini Helmasaur Key Drop', player), lambda state: can_kill_most_things(state, player, 1))
set_rule(multiworld.get_location('Ganons Tower - Pre-Moldorm Chest', player),
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7))
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 7) and can_use_bombs(state, player))
set_rule(multiworld.get_entrance('Ganons Tower Moldorm Door', player),
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8))
lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8) and can_use_bombs(state, player))
set_rule(multiworld.get_entrance('Ganons Tower Moldorm Gap', player),
lambda state: state.has('Hookshot', player) and state.multiworld.get_entrance('Ganons Tower Moldorm Gap', player).parent_region.dungeon.bosses['top'].can_defeat(state))
set_defeat_dungeon_boss_rule(multiworld.get_location('Agahnim 2', player))
@@ -1120,28 +1120,28 @@ def toss_junk_item(world, player):
raise Exception("Unable to find a junk item to toss to make room for a TR small key")
def set_trock_key_rules(world, player):
def set_trock_key_rules(multiworld, player):
# First set all relevant locked doors to impassible.
for entrance in ['Turtle Rock Dark Room Staircase', 'Turtle Rock (Chain Chomp Room) (North)', 'Turtle Rock (Chain Chomp Room) (South)', 'Turtle Rock Entrance to Pokey Room', 'Turtle Rock (Pokey Room) (South)', 'Turtle Rock (Pokey Room) (North)', 'Turtle Rock Big Key Door']:
set_rule(world.get_entrance(entrance, player), lambda state: False)
set_rule(multiworld.get_entrance(entrance, player), lambda state: False)
all_state = world.get_all_state(use_cache=False)
all_state = multiworld.get_all_state(use_cache=False, allow_partial_entrances=True)
all_state.reachable_regions[player] = set() # wipe reachable regions so that the locked doors actually work
all_state.stale[player] = True
# Check if each of the four main regions of the dungoen can be reached. The previous code section prevents key-costing moves within the dungeon.
can_reach_back = all_state.can_reach(world.get_region('Turtle Rock (Eye Bridge)', player))
can_reach_front = all_state.can_reach(world.get_region('Turtle Rock (Entrance)', player))
can_reach_big_chest = all_state.can_reach(world.get_region('Turtle Rock (Big Chest)', player))
can_reach_middle = all_state.can_reach(world.get_region('Turtle Rock (Second Section)', player))
can_reach_back = all_state.can_reach(multiworld.get_region('Turtle Rock (Eye Bridge)', player))
can_reach_front = all_state.can_reach(multiworld.get_region('Turtle Rock (Entrance)', player))
can_reach_big_chest = all_state.can_reach(multiworld.get_region('Turtle Rock (Big Chest)', player))
can_reach_middle = all_state.can_reach(multiworld.get_region('Turtle Rock (Second Section)', player))
# If you can't enter from the back, the door to the front of TR requires only 2 small keys if the big key is in one of these chests since 2 key doors are locked behind the big key door.
# If you can only enter from the middle, this includes all locations that can only be reached by exiting the front. This can include Laser Bridge and Crystaroller if the front and back connect via Dark DM Ledge!
front_locked_locations = {('Turtle Rock - Compass Chest', player), ('Turtle Rock - Roller Room - Left', player), ('Turtle Rock - Roller Room - Right', player)}
if can_reach_middle and not can_reach_back and not can_reach_front:
normal_regions = all_state.reachable_regions[player].copy()
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: True)
set_rule(world.get_entrance('Turtle Rock (Pokey Room) (South)', player), lambda state: True)
set_rule(multiworld.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: True)
set_rule(multiworld.get_entrance('Turtle Rock (Pokey Room) (South)', player), lambda state: True)
all_state.update_reachable_regions(player)
front_locked_regions = all_state.reachable_regions[player].difference(normal_regions)
front_locked_locations = set((location.name, player) for region in front_locked_regions for location in region.locations)
@@ -1151,37 +1151,38 @@ def set_trock_key_rules(world, player):
# Big key door requires the big key, obviously. We removed this rule in the previous section to flag front_locked_locations correctly,
# otherwise crystaroller room might not be properly marked as reachable through the back.
set_rule(world.get_entrance('Turtle Rock Big Key Door', player), lambda state: state.has('Big Key (Turtle Rock)', player))
set_rule(multiworld.get_entrance('Turtle Rock Big Key Door', player), lambda state: state.has('Big Key (Turtle Rock)', player) and can_kill_most_things(state, player, 10) and can_bomb_or_bonk(state, player))
# No matter what, the key requirement for going from the middle to the bottom should be five keys.
set_rule(world.get_entrance('Turtle Rock Dark Room Staircase', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 5))
set_rule(multiworld.get_entrance('Turtle Rock Dark Room Staircase', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 5))
# Now we need to set rules based on which entrances we have access to. The most important point is whether we have back access. If we have back access, we
# might open all the locked doors in any order, so we need maximally restrictive rules.
if can_reach_back:
set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: (state._lttp_has_key('Small Key (Turtle Rock)', player, 6) or location_item_name(state, 'Turtle Rock - Big Key Chest', player) == ('Small Key (Turtle Rock)', player)))
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 5))
set_rule(world.get_entrance('Turtle Rock (Pokey Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6))
set_rule(multiworld.get_location('Turtle Rock - Big Key Chest', player), lambda state: (state._lttp_has_key('Small Key (Turtle Rock)', player, 6) or location_item_name(state, 'Turtle Rock - Big Key Chest', player) == ('Small Key (Turtle Rock)', player)))
set_rule(multiworld.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 5))
set_rule(multiworld.get_entrance('Turtle Rock (Pokey Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6))
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6))
set_rule(world.get_entrance('Turtle Rock (Pokey Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6))
set_rule(world.get_entrance('Turtle Rock Entrance to Pokey Room', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 5))
set_rule(multiworld.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6))
set_rule(multiworld.get_entrance('Turtle Rock (Pokey Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6))
set_rule(multiworld.get_entrance('Turtle Rock Entrance to Pokey Room', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 5))
else:
# Middle to front requires 3 keys if the back is locked by this door, otherwise 5
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3)
set_rule(multiworld.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3)
if item_name_in_location_names(state, 'Big Key (Turtle Rock)', player, front_locked_locations.union({('Turtle Rock - Pokey 1 Key Drop', player)}))
else state._lttp_has_key('Small Key (Turtle Rock)', player, 5))
# Middle to front requires 4 keys if the back is locked by this door, otherwise 6
set_rule(world.get_entrance('Turtle Rock (Pokey Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 4)
set_rule(multiworld.get_entrance('Turtle Rock (Pokey Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 4)
if item_name_in_location_names(state, 'Big Key (Turtle Rock)', player, front_locked_locations)
else state._lttp_has_key('Small Key (Turtle Rock)', player, 6))
# Front to middle requires 3 keys (if the middle is accessible then these doors can be avoided, otherwise no keys can be wasted)
set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3))
set_rule(world.get_entrance('Turtle Rock (Pokey Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2))
set_rule(world.get_entrance('Turtle Rock Entrance to Pokey Room', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 1))
set_rule(multiworld.get_entrance('Turtle Rock (Chain Chomp Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 3))
set_rule(multiworld.get_entrance('Turtle Rock (Pokey Room) (North)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2))
set_rule(multiworld.get_entrance('Turtle Rock Entrance to Pokey Room', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 1))
set_rule(world.get_location('Turtle Rock - Big Key Chest', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, tr_big_key_chest_keys_needed(state)))
set_rule(multiworld.get_location('Turtle Rock - Big Key Chest', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, tr_big_key_chest_keys_needed(state)))
def tr_big_key_chest_keys_needed(state):
# This function handles the key requirements for the TR Big Chest in the situations it having the Big Key should logically require 2 keys, small key
@@ -1194,30 +1195,30 @@ def set_trock_key_rules(world, player):
return 6
# If TR is only accessible from the middle, the big key must be further restricted to prevent softlock potential
if not can_reach_front and not world.small_key_shuffle[player]:
if not can_reach_front and not multiworld.small_key_shuffle[player]:
# Must not go in the Big Key Chest - only 1 other chest available and 2+ keys required for all other chests
forbid_item(world.get_location('Turtle Rock - Big Key Chest', player), 'Big Key (Turtle Rock)', player)
forbid_item(multiworld.get_location('Turtle Rock - Big Key Chest', player), 'Big Key (Turtle Rock)', player)
if not can_reach_big_chest:
# Must not go in the Chain Chomps chest - only 2 other chests available and 3+ keys required for all other chests
forbid_item(world.get_location('Turtle Rock - Chain Chomps', player), 'Big Key (Turtle Rock)', player)
forbid_item(world.get_location('Turtle Rock - Pokey 2 Key Drop', player), 'Big Key (Turtle Rock)', player)
if world.accessibility[player] == 'full':
if world.big_key_shuffle[player] and can_reach_big_chest:
forbid_item(multiworld.get_location('Turtle Rock - Chain Chomps', player), 'Big Key (Turtle Rock)', player)
forbid_item(multiworld.get_location('Turtle Rock - Pokey 2 Key Drop', player), 'Big Key (Turtle Rock)', player)
if multiworld.accessibility[player] == 'full':
if multiworld.big_key_shuffle[player] and can_reach_big_chest:
# Must not go in the dungeon - all 3 available chests (Chomps, Big Chest, Crystaroller) must be keys to access laser bridge, and the big key is required first
for location in ['Turtle Rock - Chain Chomps', 'Turtle Rock - Compass Chest',
'Turtle Rock - Pokey 1 Key Drop', 'Turtle Rock - Pokey 2 Key Drop',
'Turtle Rock - Roller Room - Left', 'Turtle Rock - Roller Room - Right']:
forbid_item(world.get_location(location, player), 'Big Key (Turtle Rock)', player)
forbid_item(multiworld.get_location(location, player), 'Big Key (Turtle Rock)', player)
else:
# A key is required in the Big Key Chest to prevent a possible softlock. Place an extra key to ensure 100% locations still works
item = item_factory('Small Key (Turtle Rock)', world.worlds[player])
location = world.get_location('Turtle Rock - Big Key Chest', player)
item = item_factory('Small Key (Turtle Rock)', multiworld.worlds[player])
location = multiworld.get_location('Turtle Rock - Big Key Chest', player)
location.place_locked_item(item)
toss_junk_item(world, player)
toss_junk_item(multiworld, player)
if world.accessibility[player] != 'full':
set_always_allow(world.get_location('Turtle Rock - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Turtle Rock)' and item.player == player
and state.can_reach(state.multiworld.get_region('Turtle Rock (Second Section)', player)))
if multiworld.accessibility[player] != 'full':
set_always_allow(multiworld.get_location('Turtle Rock - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Turtle Rock)' and item.player == player
and state.can_reach(state.multiworld.get_region('Turtle Rock (Second Section)', player)))
def set_big_bomb_rules(world, player):

View File

@@ -170,7 +170,8 @@ def push_shop_inventories(multiworld):
# Retro Bow arrows will already have been pushed
if (not multiworld.retro_bow[location.player]) or ((item_name, location.item.player)
!= ("Single Arrow", location.player)):
location.shop.push_inventory(location.shop_slot, item_name, location.shop_price,
location.shop.push_inventory(location.shop_slot, item_name,
round(location.shop_price * get_price_modifier(location.item)),
1, location.item.player if location.item.player != location.player else 0,
location.shop_price_type)
location.shop_price = location.shop.inventory[location.shop_slot]["price"] = min(location.shop_price,

View File

@@ -15,18 +15,18 @@ def can_bomb_clip(state: CollectionState, region: LTTPRegion, player: int) -> bo
def can_buy_unlimited(state: CollectionState, item: str, player: int) -> bool:
return any(shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(state) for
shop in state.multiworld.shops)
shop in state.multiworld.shops)
def can_buy(state: CollectionState, item: str, player: int) -> bool:
return any(shop.region.player == player and shop.has(item) and shop.region.can_reach(state) for
shop in state.multiworld.shops)
shop in state.multiworld.shops)
def can_shoot_arrows(state: CollectionState, player: int) -> bool:
def can_shoot_arrows(state: CollectionState, player: int, count: int = 0) -> bool:
if state.multiworld.retro_bow[player]:
return (state.has('Bow', player) or state.has('Silver Bow', player)) and can_buy(state, 'Single Arrow', player)
return state.has('Bow', player) or state.has('Silver Bow', player)
return (state.has('Bow', player) or state.has('Silver Bow', player)) and can_hold_arrows(state, player, count)
def has_triforce_pieces(state: CollectionState, player: int) -> bool:
@@ -61,13 +61,13 @@ def heart_count(state: CollectionState, player: int) -> int:
# Warning: This only considers items that are marked as advancement items
diff = state.multiworld.worlds[player].difficulty_requirements
return min(state.count('Boss Heart Container', player), diff.boss_heart_container_limit) \
+ state.count('Sanctuary Heart Container', player) \
+ state.count('Sanctuary Heart Container', player) \
+ min(state.count('Piece of Heart', player), diff.heart_piece_limit) // 4 \
+ 3 # starting hearts
+ 3 # starting hearts
def can_extend_magic(state: CollectionState, player: int, smallmagic: int = 16,
fullrefill: bool = False): # This reflects the total magic Link has, not the total extra he has.
fullrefill: bool = False): # This reflects the total magic Link has, not the total extra he has.
basemagic = 8
if state.has('Magic Upgrade (1/4)', player):
basemagic = 32
@@ -84,11 +84,18 @@ def can_extend_magic(state: CollectionState, player: int, smallmagic: int = 16,
def can_hold_arrows(state: CollectionState, player: int, quantity: int):
arrows = 30 + ((state.count("Arrow Upgrade (+5)", player) * 5) + (state.count("Arrow Upgrade (+10)", player) * 10)
+ (state.count("Bomb Upgrade (50)", player) * 50))
# Arrow Upgrade (+5) beyond the 6th gives +10
arrows += max(0, ((state.count("Arrow Upgrade (+5)", player) - 6) * 10))
return min(70, arrows) >= quantity
if state.multiworld.worlds[player].options.shuffle_capacity_upgrades:
if quantity == 0:
return True
if state.has("Arrow Upgrade (70)", player):
arrows = 70
else:
arrows = (30 + (state.count("Arrow Upgrade (+5)", player) * 5)
+ (state.count("Arrow Upgrade (+10)", player) * 10))
# Arrow Upgrade (+5) beyond the 6th gives +10
arrows += max(0, ((state.count("Arrow Upgrade (+5)", player) - 6) * 10))
return min(70, arrows) >= quantity
return quantity <= 30 or state.has("Capacity Upgrade Shop", player)
def can_use_bombs(state: CollectionState, player: int, quantity: int = 1) -> bool:
@@ -146,19 +153,19 @@ def can_get_good_bee(state: CollectionState, player: int) -> bool:
def can_retrieve_tablet(state: CollectionState, player: int) -> bool:
return state.has('Book of Mudora', player) and (has_beam_sword(state, player) or
(state.multiworld.swordless[player] and
state.has("Hammer", player)))
state.has("Hammer", player)))
def has_sword(state: CollectionState, player: int) -> bool:
return state.has('Fighter Sword', player) \
or state.has('Master Sword', player) \
or state.has('Tempered Sword', player) \
or state.has('Golden Sword', player)
or state.has('Master Sword', player) \
or state.has('Tempered Sword', player) \
or state.has('Golden Sword', player)
def has_beam_sword(state: CollectionState, player: int) -> bool:
return state.has('Master Sword', player) or state.has('Tempered Sword', player) or state.has('Golden Sword',
player)
player)
def has_melee_weapon(state: CollectionState, player: int) -> bool:
@@ -171,9 +178,9 @@ def has_fire_source(state: CollectionState, player: int) -> bool:
def can_melt_things(state: CollectionState, player: int) -> bool:
return state.has('Fire Rod', player) or \
(state.has('Bombos', player) and
(state.multiworld.swordless[player] or
has_sword(state, player)))
(state.has('Bombos', player) and
(state.multiworld.swordless[player] or
has_sword(state, player)))
def has_misery_mire_medallion(state: CollectionState, player: int) -> bool:

View File

@@ -1,224 +1,123 @@
# Guía de instalación para A Link to the Past Randomizer Multiworld
<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>
## Software requerido
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Incluido en Multiworld Utilities)
- Hardware o software capaz de cargar y ejecutar archivos de ROM de SNES
- Un emulador capaz de ejecutar scripts Lua
([snes9x rr](https://github.com/gocha/snes9x-rr/releases),
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
- [SNI](https://github.com/alttpo/sni/releases). Esto está incluido automáticamente en la instalación de Archipelago.
- SNI no es compatible con (Q)Usb2Snes.
- Hardware o software capaz de cargar y ejecutar archivos de ROM de SNES, por ejemplo:
- Un emulador capaz de conectarse a SNI
([snes9x-nwa](https://github.com/Skarsnik/snes9x-emunwa/releases), [snes9x-rr](https://github.com/gocha/snes9x-rr/releases),
[BSNES-plus](https://github.com/black-sliver/bsnes-plus),
[BizHawk](https://tasvideos.org/BizHawk), o
[RetroArch](https://retroarch.com?page=platforms) 1.10.1 o más nuevo). O,
- Un flashcart SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), o otro hardware compatible
[RetroArch](https://retroarch.com?page=platforms) 1.10.1 o más nuevo).
- Un SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), u otro hardware compatible. **nota:
Las SNES minis modificadas no tienen soporte de SNI. Algunos usuarios dicen haber tenido éxito con Qusb2Snes para esta consola,
pero no tiene soporte.**
- Tu archivo ROM japones v1.0, probablemente se llame `Zelda no Densetsu - Kamigami no Triforce (Japan).sfc`
## Procedimiento de instalación
### Instalación en Windows
1. Descarga e instala MultiWorld Utilities desde el enlace anterior, asegurando que instalamos la versión más reciente.
**El archivo esta localizado en la sección "assets" en la parte inferior de la información de versión**. Si tu
intención es jugar la versión normal de multiworld, necesitarás el archivo `Setup.Archipelago.exe`
- Si estas interesado en jugar la variante que aleatoriza las puertas internas de las mazmorras, necesitaras bajar '
Setup.BerserkerMultiWorld.Doors.exe'
- Durante el proceso de instalación, se te pedirá donde esta situado tu archivo ROM japonés v1.0. Si ya habías
instalado este software con anterioridad y simplemente estas actualizando, no se te pedirá la localización del
archivo una segunda vez.
- Puede ser que el programa pida la instalación de Microsoft Visual C++. Si ya lo tienes en tu ordenador (
posiblemente por que un juego de Steam ya lo haya instalado), el instalador no te pedirá su instalación.
2. Si estas usando un emulador, deberías asignar la versión capaz de ejecutar scripts Lua como programa por defecto para
lanzar ficheros de ROM de SNES.
1. Extrae tu emulador al escritorio, o cualquier sitio que después recuerdes.
2. Haz click derecho en un fichero de ROM (ha de tener la extensión sfc) y selecciona **Abrir con...**
3. Marca la opción **Usar siempre esta aplicación para abrir los archivos .sfc**
4. Baja hasta el final de la lista y haz click en la opción **Buscar otra aplicación en el equipo** (Si usas Windows
10 es posible que debas hacer click en **Más aplicaciones**)
5. Busca el archivo .exe de tu emulador y haz click en **Abrir**. Este archivo debe estar en el directorio donde
extrajiste en el paso 1.
### Instalación en Macintosh
- ¡Necesitamos voluntarios para rellenar esta seccion! Contactad con **Farrak Kilhn** (en inglés) en Discord si queréis
ayudar.
## Configurar tu archivo YAML
### Que es un archivo YAML y por qué necesito uno?
Tu archivo YAML contiene un conjunto de opciones de configuración que proveen al generador con información sobre como
debe generar tu juego. Cada jugador en una partida de multiworld proveerá su propio fichero YAML. Esta configuración
permite que cada jugador disfrute de una experiencia personalizada a su gusto, y cada jugador dentro de la misma partida
de multiworld puede tener diferentes opciones.
### Donde puedo obtener un fichero YAML?
La página "[Generate Game](/games/A%20Link%20to%20the%20Past/player-options)" en el sitio web te permite configurar tu
configuración personal y descargar un fichero "YAML".
### Configuración YAML avanzada
Una version mas avanzada del fichero Yaml puede ser creada usando la pagina
["Weighted settings"](/games/A Link to the Past/weighted-options),
la cual te permite tener almacenadas hasta 3 preajustes. La pagina "Weighted Settings" tiene muchas opciones
representadas con controles deslizantes. Esto permite elegir cuan probable los valores de una categoría pueden ser
elegidos sobre otros de la misma.
Por ejemplo, imagina que el generador crea un cubo llamado "map_shuffle", y pone trozos de papel doblado en él por cada
sub-opción. Ademas imaginemos que tu valor elegido para "on" es 20 y el elegido para "off" es 40.
Por tanto, en este ejemplo, habrán 60 trozos de papel. 20 para "on" y 40 para "off". Cuando el generador esta decidiendo
si activar o no "map shuffle" para tu partida, meterá la mano en el cubo y sacara un trozo de papel al azar. En este
ejemplo, es mucho mas probable (2 de cada 3 veces (40/60)) que "map shuffle" esté desactivado.
Si quieres que una opción no pueda ser escogida, simplemente asigna el valor 0 a dicha opción. Recuerda que cada opción
debe tener al menos un valor mayor que cero, si no la generación fallará.
### Verificando tu archivo YAML
Si quieres validar que tu fichero YAML para asegurarte que funciona correctamente, puedes hacerlo en la pagina
[YAML Validator](/check).
## Generar una partida para un jugador
1. Navega a [la pagina Generate game](/games/A%20Link%20to%20the%20Past/player-options), configura tus opciones, haz
click en el boton "Generate game".
2. Se te redigirá a una pagina "Seed Info", donde puedes descargar tu archivo de parche.
3. Haz doble click en tu fichero de parche, y el emulador debería ejecutar tu juego automáticamente. Como el Cliente no
es necesario para partidas de un jugador, puedes cerrarlo junto a la pagina web (que tiene como titulo "Multiworld
WebUI") que se ha abierto automáticamente.
## Unirse a una partida MultiWorld
1. Descarga e instala [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest).
**El archivo del instalador se encuentra en la sección de assets al final de la información de version**.
2. La primera vez que realices una generación local o parchees tu juego, se te pedirá que ubiques tu archivo ROM base.
Este es tu archivo ROM de Link to the Past japonés. Esto sólo debe hacerse una vez.
4. Si estás usando un emulador, deberías de asignar tu emulador con compatibilidad con Lua como el programa por defecto para abrir archivos
ROM.
1. Extrae la carpeta de tu emulador al Escritorio, o algún otro sitio que vayas a recordar.
2. Haz click derecho en un archivo ROM y selecciona **Abrir con...**
3. Marca la casilla junto a **Usar siempre este programa para abrir archivos .sfc**
4. Baja al final de la lista y haz click en el texto gris **Buscar otro programa en este PC**
5. Busca el archivo `.exe` de tu emulador y haz click en **Abrir**. Este archivo debería de encontrarse dentro de la carpeta que
extrajiste en el paso uno.
### Obtener el fichero de parche y crea tu ROM
Cuando te unes a una partida multiworld, debes proveer tu fichero YAML a quien sea el creador de la partida. Una vez
Cuando te unas a una partida multiworld, se te pedirá enviarle tu archivo de configuración a quien quiera que esté creando. Una vez eso
este hecho, el creador te devolverá un enlace para descargar el parche o un fichero zip conteniendo todos los ficheros
de parche de la partida Tu fichero de parche debe tener la extensión `.aplttp`.
de parche de la partida. Tu fichero de parche debe de tener la extensión `.aplttp`.
Pon tu fichero de parche en el escritorio o en algún sitio conveniente, y haz doble click. Esto debería ejecutar
automáticamente el cliente, y ademas creara la rom en el mismo directorio donde este el fichero de parche.
Pon tu fichero de parche en el escritorio o en algún sitio conveniente, y hazle doble click. Esto debería ejecutar
automáticamente el cliente, y además creará la rom en el mismo directorio donde este el fichero de parche.
### Conectar al cliente
#### Con emulador
Cuando el cliente se lance automáticamente, QUsb2Snes debería haberse ejecutado también. Si es la primera vez que lo
ejecutas, puedes ser que el firewall de Windows te pregunte si le permites la comunicación.
Cuando el cliente se lance automáticamente, SNI debería de ejecutarse en segundo plano. Si es la
primera vez que se ejecuta, tal vez se te pida permitir que se comunique a través del firewall de Windows
#### snes9x-nwa
1. Haz click en el menu Network y marca 'Enable Emu Network Control
2. Carga tu archivo ROM si no lo habías hecho antes
##### snes9x-rr
1. Carga tu fichero de ROM, si no lo has hecho ya
1. Carga tu fichero ROM, si no lo has hecho ya
2. Abre el menu "File" y situa el raton en **Lua Scripting**
3. Haz click en **New Lua Script Window...**
4. En la nueva ventana, haz click en **Browse...**
5. Navega hacia el directorio donde este situado snes9x-rr, entra en el directorio `lua`, y
escoge `multibridge.lua`
6. Observa que se ha asignado un nombre al dispositivo, y el cliente muestra "SNES Device: Connected", con el mismo
nombre en la esquina superior izquierda.
5. Selecciona el archivo lua conector incluido con tu cliente
- Busca en la carpeta de Archipelago `/SNI/lua/`.
6. Si ves un error mientras carga el script que dice `socket.dll missing` o algo similar, ve a la carpeta de
el lua que estas usando en tu gestor de archivos y copia el `socket.dll` a la raíz de tu instalación de snes9x.
##### BNES-Plus
1. Cargue su archivo ROM si aún no se ha cargado.
2. El emulador debería conectarse automáticamente mientras SNI se está ejecutando.
##### BizHawk
1. Asegurate que se ha cargado el nucleo BSNES. Debes hacer esto en el menu Tools y siguiento estas opciones:
`Config --> Cores --> SNES --> BSNES`
Una vez cambiado el nucleo cargado, BizHawk ha de ser reiniciado.
1. Asegurate que se ha cargado el núcleo BSNES. Se hace en la barra de menú principal, bajo:
- (≤ 2.8) `Config``Cores``SNES``BSNES`
- (≥ 2.9) `Config``Preferred Cores``SNES``BSNESv115+`
2. Carga tu fichero de ROM, si no lo has hecho ya.
3. Haz click en el menu Tools y en la opción **Lua Console**
4. Haz click en el botón para abrir un nuevo script Lua.
5. Navega al directorio de instalación de MultiWorld Utilities, y en los siguiente directorios:
`QUsb2Snes/Qusb2Snes/LuaBridge`
6. Selecciona `luabridge.lua` y haz click en Abrir.
7. Observa que se ha asignado un nombre al dispositivo, y el cliente muestra "SNES Device: Connected", con el mismo
nombre en la esquina superior izquierda.
Si has cambiado tu preferencia de núcleo tras haber cargado la ROM, no te olvides de volverlo a cargar (atajo por defecto: Ctrl+R).
3. Arrastra el archivo `Connector.lua` que has descargado a la ventana principal de EmuHawk.
- Busca en la carpeta de Archipelago `/SNI/lua/`.
- También podrías abrir la consola de Lua manualmente, hacer click en `Script``Open Script`, e ir a `Connector.lua`
con el selector de archivos.
##### RetroArch 1.10.1 o más nuevo
Sólo hay que segiur estos pasos una vez.
Sólo hay que seguir estos pasos una vez.
1. Comienza en la pantalla del menú principal de RetroArch.
2. Ve a Ajustes --> Interfaz de usario. Configura "Mostrar ajustes avanzados" en ON.
3. Ve a Ajustes --> Red. Configura "Comandos de red" en ON. (Se encuentra bajo Request Device 16.) Deja en 55355 (el
default) el Puerto de comandos de red.
3. Ve a Ajustes --> Red. Pon "Comandos de red" en ON. (Se encuentra bajo Request Device 16.) Deja en 55355 el valor por defecto,
el Puerto de comandos de red.
![Captura de pantalla del ajuste Comandos de red](/static/generated/docs/A%20Link%20to%20the%20Past/retroarch-network-commands-en.png)
4. Ve a Menú principal --> Actualizador en línea --> Descargador de núcleos. Desplázate y selecciona "Nintendo - SNES /
SFC (bsnes-mercury Performance)".
Cuando cargas un ROM, asegúrate de seleccionar un núcleo **bsnes-mercury**. Estos son los sólos núcleos que permiten
Cuando cargas un ROM, asegúrate de seleccionar un núcleo **bsnes-mercury**. Estos son los únicos núcleos que permiten
que herramientas externas lean datos del ROM.
#### Con Hardware
Esta guía asume que ya has descargado el firmware correcto para tu dispositivo. Si no lo has hecho ya, hazlo ahora. Los
Esta guía asume que ya has descargado el firmware correcto para tu dispositivo. Si no lo has hecho ya, por favor hazlo ahora. Los
usuarios de SD2SNES y FXPak Pro pueden descargar el firmware apropiado
[aqui](https://github.com/RedGuyyyy/sd2snes/releases). Los usuarios de otros dispositivos pueden encontrar información
[aqui](https://github.com/RedGuyyyy/sd2snes/releases). Puede que los usuarios de otros dispositivos encuentren informacion útil
[en esta página](http://usb2snes.com/#supported-platforms).
1. Cierra tu emulador, el cual debe haberse autoejecutado.
2. Cierra QUsb2Snes, el cual fue ejecutado junto al cliente.
3. Ejecuta la version correcta de QUsb2Snes (v0.7.16).
4. Enciende tu dispositivo y carga la ROM.
5. Observa en el cliente que ahora muestra "SNES Device: Connected", y aparece el nombre del dispositivo.
2. Enciende tu dispositivo y carga la ROM.
### Conecta al MultiServer
### Conecta al Servidor Archipelago
El fichero de parche que ha lanzado el cliente debe haberte conectado automaticamente al MultiServer. Hay algunas
razonas por las que esto puede que no pase, incluyendo que el juego este hospedado en el sitio web pero se genero en
algún otro sitio. Si el cliente muestra "Server Status: Not Connected", preguntale al creador de la partida la dirección
del servidor, copiala en el campo "Server" y presiona Enter.
El fichero de parche que ha lanzado el cliente debería de haberte conectado automaticamente al MultiServer. Sin embargo hay algunas
razones por las que puede que esto no suceda, como que la partida este hospedada en la página web pero generada en otra parte. Si la
ventana del cliente muestra "Server Status: Not Connected", simplemente preguntale al creador de la partida la dirección
del servidor, cópiala en el campo "Server" y presiona Enter.
El cliente intentara conectarse a esta nueva dirección, y debería mostrar "Server Status: Connected" en algún momento.
Si el cliente no se conecta al cabo de un rato, puede ser que necesites refrescar la pagina web.
El cliente intentará conectarse a esta nueva dirección, y debería mostrar "Server Status: Connected" momentáneamente.
### Jugando
### Jugar al juego
Cuando ambos SNES Device and Server aparezcan como "connected", estas listo para empezar a jugar. Felicidades por unirte
satisfactoriamente a una partida de multiworld!
## Hospedando una partida de multiworld
La manera recomendad para hospedar una partida es usar el servicio proveído en
[el sitio web](/generate). El proceso es relativamente sencillo:
1. Recolecta los ficheros YAML de todos los jugadores que participen.
2. Crea un fichero ZIP conteniendo esos ficheros.
3. Carga el fichero zip en el sitio web enlazado anteriormente.
4. Espera a que la seed sea generada.
5. Cuando esto acabe, se te redigirá a una pagina titulada "Seed Info".
6. Haz click en "Create New Room". Esto te llevara a la pagina del servidor. Pasa el enlace a esta pagina a los
jugadores para que puedan descargar los ficheros de parche de ahi.
**Nota:** Los ficheros de parche de esta pagina permiten a los jugadores conectarse al servidor automaticamente,
mientras que los de la pagina "Seed info" no.
7. Hay un enlace a un MultiWorld Tracker en la parte superior de la pagina de la sala. Deberías pasar también este
enlace a los jugadores para que puedan ver el progreso de la partida. A los observadores también se les puede pasar
este enlace.
8. Una vez todos los jugadores se han unido, podeis empezar a jugar.
## Auto-Tracking
Si deseas usar auto-tracking para tu partida, varios programas ofrecen esta funcionalidad.
El programa recomentdado actualmente es:
[OpenTracker](https://github.com/trippsc2/OpenTracker/releases).
### Instalación
1. Descarga el fichero de instalacion apropiado para tu ordenador (Usuarios de windows quieren el fichero ".msi").
2. Durante el proceso de insatalación, puede que se te pida instalar Microsoft Visual Studio Build Tools. Un enlace este
programa se muestra durante la proceso, y debe ser ejecutado manualmente.
### Activar auto-tracking
1. Con OpenTracker ejecutado, haz click en el menu Tracking en la parte superior de la ventana, y elige **
AutoTracker...**
2. Click the **Get Devices** button
3. Selecciona tu "SNES device" de la lista
4. Si quieres que las llaves y los objetos de mazmorra tambien sean marcados, activa la caja con nombre **Race Illegal
Tracking**
5. Haz click en el boton **Start Autotracking**
6. Cierra la ventana AutoTracker, ya que deja de ser necesaria
Cuando el cliente muestre tanto el dispositivo SNES como el servidor como conectados, estas listo para empezar a jugar. Felicidades por
haberte unido a una partida multiworld con exito! Puedes ejecutar varios comandos en tu cliente. Para mas informacion
acerca de estos comando puedes usar `/help` para comandos locales del cliente y `!help` para comandos de servidor.

View File

@@ -1,2 +1,2 @@
maseya-z3pr>=1.0.0rc1
xxtea>=3.0.0
xxtea>=3.0.0

View File

@@ -130,19 +130,21 @@ class TestGanonsTower(TestDungeon):
["Ganons Tower - Pre-Moldorm Chest", False, []],
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Progressive Bow']],
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Bomb Upgrade (50)']],
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Big Key (Ganons Tower)']],
["Ganons Tower - Pre-Moldorm Chest", False, [], ['Lamp', 'Fire Rod']],
["Ganons Tower - Pre-Moldorm Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp']],
["Ganons Tower - Pre-Moldorm Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod']],
["Ganons Tower - Pre-Moldorm Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp']],
["Ganons Tower - Pre-Moldorm Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod']],
["Ganons Tower - Validation Chest", False, []],
["Ganons Tower - Validation Chest", False, [], ['Hookshot']],
["Ganons Tower - Validation Chest", False, [], ['Progressive Bow']],
["Ganons Tower - Validation Chest", False, [], ['Bomb Upgrade (50)']],
["Ganons Tower - Validation Chest", False, [], ['Big Key (Ganons Tower)']],
["Ganons Tower - Validation Chest", False, [], ['Lamp', 'Fire Rod']],
["Ganons Tower - Validation Chest", False, [], ['Progressive Sword', 'Hammer']],
["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Progressive Sword']],
["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Progressive Sword']],
["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Hammer']],
["Ganons Tower - Validation Chest", True, ['Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Hammer']],
["Ganons Tower - Validation Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Progressive Sword']],
["Ganons Tower - Validation Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Progressive Sword']],
["Ganons Tower - Validation Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Lamp', 'Hookshot', 'Hammer']],
["Ganons Tower - Validation Chest", True, ['Bomb Upgrade (50)', 'Progressive Bow', 'Big Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Small Key (Ganons Tower)', 'Fire Rod', 'Hookshot', 'Hammer']],
])

View File

@@ -77,5 +77,5 @@ class TestMiseryMire(TestDungeon):
["Misery Mire - Boss", False, [], ['Bomb Upgrade (+5)', 'Bomb Upgrade (+10)', 'Bomb Upgrade (50)']],
["Misery Mire - Boss", True, ['Bomb Upgrade (+5)', 'Big Key (Misery Mire)', 'Lamp', 'Cane of Somaria', 'Progressive Sword', 'Pegasus Boots']],
["Misery Mire - Boss", True, ['Bomb Upgrade (+5)', 'Big Key (Misery Mire)', 'Lamp', 'Cane of Somaria', 'Hammer', 'Pegasus Boots']],
["Misery Mire - Boss", True, ['Bomb Upgrade (+5)', 'Big Key (Misery Mire)', 'Lamp', 'Cane of Somaria', 'Progressive Bow', 'Pegasus Boots']],
["Misery Mire - Boss", True, ['Bomb Upgrade (+5)', 'Big Key (Misery Mire)', 'Lamp', 'Cane of Somaria', 'Progressive Bow', 'Arrow Upgrade (+5)', 'Pegasus Boots']],
])

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