Compare commits

..

68 Commits

Author SHA1 Message Date
Fabian Dill
6d4ce04067 Core/Subnautica: demonstrate a way to on-demand specify WebWorld 2024-01-26 23:51:11 +01:00
black-sliver
aa72f671bc SoE: fix naming of atlas medallion (#2747)
In pyevermizer, it's called Atlas Medallion, not Amulet, leading to an
empty group and to code not considering them as an alchemy ingredient
when swapping out for a trap or an energy core fragment.

Also adds a test.
2024-01-21 19:34:24 +01:00
Scipio Wright
5f9ce2b7b6 Noita: Update to use new Options API (#2370)
Reworking the options to make it work with the new options API.
Also reworked stuff in several spots to use world: NoitaWorld instead of multiworld: MultiWorld
2024-01-19 21:31:45 +01:00
zig-for
1307754f02 LADX: music shuffle (#2101) 2024-01-19 21:14:26 +01:00
Bicoloursnake
ac7b707e3e OOT: Adjust the Logic Trick Keys to be an ordered object (#2736) 2024-01-18 02:18:03 +01:00
Star Rauchenberger
ec440b7785 Lingo: NORTH requires hint panels (#2732) 2024-01-18 01:58:48 +01:00
Danaël V
4c901dcfc0 TUNIC: Change Tunic to TUNIC (#2720) 2024-01-18 01:56:34 +01:00
Fabian Dill
834b6e35b4 Setup: auto update vc redist (#2502) 2024-01-18 01:52:33 +01:00
Fabian Dill
602c2966fc LttP: move _hint_text to SubClasses (#2532) 2024-01-16 17:23:18 +01:00
black-sliver
49ecd4b9c1 CI: flake8: max-complexity=14 (#2731)
The value of 10 does not really fit some of our world patterns and values
up to 15 may be acceptable. Looking at some worlds, 14 seems to be
achievable without too much work and reduces the noise in test output,
making it more usable.
2024-01-16 17:10:58 +01:00
black-sliver
de8fe21d4a Tests: create sane cov defaults (#2728) 2024-01-16 17:10:19 +01:00
NewSoupVi
4fdeec4f70 The Witness: Cleanup - Options Access, data version, snake_case for file name (#2631) 2024-01-16 15:33:34 +01:00
NewSoupVi
71a3e2230d The Witness: Allow Mountain Lasers to go up to 11 instead of 7. (#2618) 2024-01-16 15:27:09 +01:00
JaredWeakStrike
325a510ba7 KH2: Promise charm logic (#2635) 2024-01-16 15:26:18 +01:00
NewSoupVi
5dcaa6ca20 The Witness: Death Link Amnesty (#2646) 2024-01-16 15:24:10 +01:00
NewSoupVi
e15873e861 The Witness: Bonk trap support (#2645) 2024-01-16 15:23:30 +01:00
NewSoupVi
5c7bae7940 The Witness: Local Laser Shuffle + Option Presets (#2590)
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
2024-01-16 15:14:06 +01:00
NewSoupVi
e6f7ed5060 The Witness: Progressive Symmetry (#2644) 2024-01-16 15:13:04 +01:00
NewSoupVi
1c2dcb7b01 The Witness: Add Desert Control Panels (#2643) 2024-01-16 15:11:52 +01:00
Star Rauchenberger
5df7a8f686 Lingo: Disable forced good item when early color hallways is on (#2729) 2024-01-16 15:10:59 +01:00
Bryce Wilson
3a588099bd Pokemon Emerald: Automatically exclude locations based on goal (#2655) 2024-01-16 15:09:47 +01:00
Yussur Mustafa Oraji
d000b52ae0 V6: Use new options api (#2668)
* v6: Use new options API

* v6: Add display names for some options
2024-01-16 13:38:19 +01:00
NewSoupVi
fe3bc8d6be The Witness: Add Obelisk Side locations to always and priority hints (#2665) 2024-01-16 13:14:38 +01:00
NewSoupVi
7affb885ba The Witness: Add "Town Desert Laser Redirect Control (Panel)" as an item (#2669) 2024-01-16 13:13:44 +01:00
Star Rauchenberger
d390d2eff8 Lingo: Remove colors from Bearer SIXes (#2677) 2024-01-16 13:13:02 +01:00
JaredWeakStrike
0efc13fc8a KH2: Location Groups and Subclasses (#2700) 2024-01-16 13:12:33 +01:00
Star Rauchenberger
c6896c6af9 Lingo: Make The Colorful optionally progressive (#2711) 2024-01-16 13:11:20 +01:00
Star Rauchenberger
adad7b532d Lingo: Turn The Colorful into a countdown achievement (#2710)
The Colorful currently, in logic, does not expect you to solve the achievement panel until all of the doors are opened. This is not enforced by the client in complex door shuffle. It is also not typical of how achievements in Lingo usually work, and it ended up this way because of the fact that The Colorful is, uniquely, not a countdown panel. This change modifies logic so that solving each panel within The Colorful is required in order to access the achievement, rather than opening all of the doors. This will be accompanied by a change to the client that will turn the achievement panel into a countdown.
2024-01-16 13:09:54 +01:00
Held_der_Zeit
d756960a0b Worlds Docs: Translations German (Clique, BK Sudoku, OoT) (#2581)
* Sudoku German

* German OOT (+ Room Image)

* German Clique

* german translation

* translation flexibility - ff1

* german setup - oot

* Transaltion Flexibilty - SM64

* translation flexibilty - factorio

* translation flexibilty - kh2

* translation flexibility - Super Metroid

* translation flexibility - Stardew Valley

* german translation added - clique

* translation flexibility - terraria

* translation flexibilty - checksfinder

* Sudoku Setup - Grammar Fix

* Sudoku Main - Fix Grammar

* Revert "translation flexibility - ff1"

This reverts commit 6df434c682.

* Revert "Transaltion Flexibilty - SM64"

This reverts commit 754bf95d2f.

* Revert "translation flexibilty - factorio"

This reverts commit db1226a9de.

* Revert "translation flexibility - Super Metroid"

This reverts commit ca5bd9a64a.

* Revert "translation flexibilty - kh2"

This reverts commit 076534ee32.

* Revert "translation flexibility - Stardew Valley"

This reverts commit 4b13701394.

* Revert "translation flexibility - terraria"

This reverts commit a0abfc8a03.

* Revert "translation flexibilty - checksfinder"

This reverts commit a4de49961d.

* Sugesstion - Fixes in Grammar (and Typos)

One or two suggesstions need to be changed a bit further (such as an incomplete sentence)

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

* Apply suggestions from code review

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

* Apply suggestions from code review

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

* Update guide_de.md

* Update setup_de.md

* Update de_Sudoku.md

* Update __init__.py

* Update worlds/oot/docs/setup_de.md

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

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2024-01-16 06:54:48 +01:00
Alchav
30ec080449 FFMQ: Reset protection (#2727)
Bizhawk's "hard reset" option fills RAM with 0x55s. This causes game completion to be erroneously flagged, and likely many erroneous location checks with it. This fix checks for 0x55 and will not proceed to process anything if present.
2024-01-16 01:21:02 +01:00
Fabian Dill
79e2f7e357 Tests: test that World.options is not set on the class (#2725)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2024-01-15 20:50:16 +01:00
t3hf1gm3nt
b4077a0717 TLOZ: properly assign options (#2726)
whoops used a = instead of a :
mad that im doing a literal one character change PR :/
2024-01-15 20:19:18 +01:00
black-sliver
518b04c08e SoE: minor typing and style fixes (#2724)
* SoE: fix typing for tests

* SoE: explicitly export pyevermizer

To support loading the module from source (rather than module) we import
pyevermizer from `__init__.py` in other files. This has been an implicit export
and `mypy --strict` disables implicit exports, so we export it explicitly now.

* SoE: fix style in patch.py

* SoE: remove unused imports

* SoE: fix format mistakes

* SoE: cleaner typing in SoEOptions.flags

as suggested by beauxq
2024-01-15 09:17:46 +01:00
GodlFire
d10f8f66c7 Shivers: Fix rule logic for location 'Final Riddle: Guillotine Dropped' (#2706) 2024-01-15 04:48:44 +01:00
t3hf1gm3nt
6d393fe42b TLOZ: update to new options API (#2714) 2024-01-15 04:47:32 +01:00
agilbert1412
5b93db121f Stardew Valley: Added missing rule on the club card (#2722) 2024-01-15 04:29:30 +01:00
Fabian Dill
ad074490bc Test: add location access rule benchmark (#2433) 2024-01-14 21:30:00 +01:00
Fabian Dill
6ac3d5c651 Core: set consistent server defaults (#2566) 2024-01-14 21:24:34 +01:00
Doug Hoskisson
ed6b7b2670 Zillion: remove old option access from item link validation (#2673)
* Zillion: remove old option access from item link validation
and a little bit a cleaning in other stuff nearby

* one option access missed
2024-01-14 15:48:30 +01:00
Doug Hoskisson
6904bd5885 Typing: improve kivy type stubs (#2681) 2024-01-14 15:31:13 +01:00
black-sliver
962b9b28f0 Setup: don't install webhost dependencies (#2717)
also makes ModuleUpdate detect changed requirements for update()
2024-01-14 03:09:03 +01:00
Aaron Wagener
37b03807fd Core: Log the worlds still using the old options API (#2707) 2024-01-14 03:04:12 +01:00
Aaron Wagener
73e41cb701 Core: migrate start_inventory_from_pool to new options API (#2666)
* Core: migrate start_inventory_from_pool to new options API

* get the other spot too

* skip {}

* oops

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

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2024-01-14 02:57:53 +01:00
Aaron Wagener
cfd758168c Tests: add a test for worlds to not modify the itempool after create_items (#1460)
* Tests: add a test for worlds to only modify the itempool in `create_items`

* extend test multiworld setup instead of a new function

* cleanup the test a bit

* put more strict wording in `create_items` docstring

* list of shame

* Don't call `set_rules` before testing

* remove ChecksFinder from the list of shame

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
2024-01-14 02:15:35 +01:00
Nicholas Saylor
01fb44c186 Docs: Added Disabled World information to README.md (#2705)
* Add rationale for OriBF being disabled

* Removed periods

* Added warning to README.md

* Apply suggestions from code review

Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>

* Added disable date

Meant to provide context for any updates the world may need (For example, this world would need to change to the new options sstem in 0.4.4)

* Moved rationale to local README
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Apply suggestions from code review

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

---------

Co-authored-by: Silvris <58583688+Silvris@users.noreply.github.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2024-01-13 21:10:16 +01:00
Aaron Wagener
2725c0258f Docs: specify that deathlink cause should contain the player name (#2557)
* Docs: specify that the cause should contain the player name

* accidental whitespace moment

* fix table formatting
2024-01-13 19:23:14 +01:00
black-sliver
0c0adb0745 Core: update kivy (#2718) 2024-01-13 18:01:36 +01:00
Scipio Wright
4a85f21c25 TUNIC: Update game page for blurb about playing vanilla first (#2712)
* Update en_Tunic.md

* Change emphasis a bit

* Move the "haven't played before" section up

* settings -> options

* Update worlds/tunic/docs/en_Tunic.md

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

* Update setup as well with settings -> options and some recent changes to the in-game settings

---------

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2024-01-13 15:12:43 +01:00
Silent
3933fd3929 TUNIC: Implement New Game (#2172) 2024-01-12 20:32:15 +01:00
Ame
b241644e54 Docs: add FR guide for DLCQuest (#2699)
* Docs: add Translate FR guide for DLCQuest

* Add Translate

* fix

* Update worlds/dlcquest/docs/fr_DLCQuest.md

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

* Fix Translate

* Fix translate

* Update __init__.py

* Update worlds/dlcquest/__init__.py

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

* Update worlds/dlcquest/__init__.py

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

---------

Co-authored-by: Danaël V. <104455676+ReverM@users.noreply.github.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2024-01-12 20:26:50 +01:00
black-sliver
e00b5a7d17 SoE: use new AP API and naming and make APworld (#2701)
* SoE: new file naming

also fixes test base deprecation

* SoE: use options_dataclass

* SoE: moar typing

* SoE: no more multiworld.random

* SoE: replace LogicMixin by SoEPlayerLogic object

* SoE: add test that rocket parts always exist

* SoE: Even moar typing

* SoE: can haz apworld now

* SoE: pep up test naming

* SoE: use self.options for trap chances

* SoE: remove unused import with outdated comment

* SoE: move flag and trap extraction to dataclass

as suggested by beauxq

* SoE: test trap option parsing and item generation
2024-01-12 01:07:40 +01:00
Alchav
47dd36456e Pokémon R/B: Fix move intervention (#2687) 2024-01-12 00:49:54 +01:00
Fabian Dill
4ce8a7ec4d PyCharm: ship a working unittest run config (#2694) 2024-01-12 00:49:14 +01:00
Kory Dondzila
a99c1e15ad Shivers: Fixes issue with office elevator rule logic. (#2690)
Office elevator logic was written as
can reach Underground Tunnels OR can reach Office AND have Key for Office Elevator

Meaning that key for office elevator was not required if Underground Tunnels could be reached when it should be.

Changed to
(can reach Underground Tunnels OR can reach Office) AND have Key for Office Elevator
2024-01-12 00:48:22 +01:00
Fabian Dill
44de140add SC2: run download_data via concurrent.futures (#2704) 2024-01-12 00:40:33 +01:00
Doug Hoskisson
ac2387e17c Tests: remove deprecated option access from WorldTestBase (#2671)
* remove deprecated option access from `WorldTestBase`

* one in test_reachability
2024-01-12 00:22:04 +01:00
Danaël V
2760deb5b6 Docs: Fix broken link in Landstalker setup Guide (#2680)
* Cleaning up (#4)

Cleanup

* Update landstalker_setup_en.md

Fixed Redirect
2024-01-12 00:18:11 +01:00
Remy Jette
f530895c33 WebHost: Fix /api/generate (#2693) 2024-01-11 17:44:12 -05:00
Justus Lind
b6f3ccb8c5 Touhou Mugakudan 3 song update. (#2659)
- Adds all the songs from the Touhou Mugakudan -Ⅲ- update. 
- Increases the upper limit of additional songs to 508 due to there being 512 songs now.
- Finally fixes umpopoff. As it was the only song that had less than 3 difficulties but also didn't have proper difficulty values
2024-01-11 23:13:39 +01:00
Flori
388413fcdd Hollow Knight: Fix fragile/unbreakable charm variants counting as 2 distinct charms in logic (#2683)
Deletes CHARM of the 3 unbreakable charms, adds 0.5 CHARM to Queen_fragment, King_Fragment and Void_heart
2024-01-11 23:10:25 +01:00
JaredWeakStrike
4045c6a9cf KH2: Fix relative import (#2702) 2024-01-11 00:56:43 +01:00
JaredWeakStrike
e082c83dc7 KH2: Fix plando breaking because of keyblades (#2678) 2024-01-10 18:22:54 +01:00
Doug Hoskisson
82410fd554 Zillion: client win location check (#2682) 2024-01-10 17:52:43 +01:00
JaredWeakStrike
570ba28bee KH2: Fix Terra logic (#2676) 2024-01-10 06:22:04 +01:00
Alchav
b0638b993d FFMQ: Fix starting progressive gear (#2685) 2024-01-09 03:57:38 +01:00
lordlou
89f211f31e SMZ3: 0.4.4 backward compat client fix (#2667)
fixed broken client compatibility with any seed generated before 0.4.4 introduced with the recent change to the message queue.
2024-01-07 13:13:52 +01:00
Fabian Dill
70fdd6b90d Core: clean up MultiServer.py/auto_shutdown (#2552) 2024-01-07 01:42:57 +01:00
Fabian Dill
f22daca74e CommonClient: request datapackage per-game (#2563) 2024-01-07 01:42:16 +01:00
139 changed files with 6099 additions and 986 deletions

5
.coveragerc Normal file
View File

@@ -0,0 +1,5 @@
[report]
exclude_lines =
pragma: no cover
if TYPE_CHECKING:
if typing.TYPE_CHECKING:

View File

@@ -71,7 +71,7 @@ jobs:
continue-on-error: true
if: env.diff != '' && matrix.task == 'flake8'
run: |
flake8 --count --max-complexity=10 --max-doc-length=120 --max-line-length=120 --statistics ${{ env.diff }}
flake8 --count --max-complexity=14 --max-doc-length=120 --max-line-length=120 --statistics ${{ env.diff }}
- name: "mypy: Type check modified files"
continue-on-error: true

View File

@@ -0,0 +1,18 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Archipelago Unittests" type="tests" factoryName="Unittests">
<module name="Archipelago" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="_new_pattern" value="&quot;&quot;" />
<option name="_new_additionalArguments" value="&quot;&quot;" />
<option name="_new_target" value="&quot;$PROJECT_DIR$/test&quot;" />
<option name="_new_targetType" value="&quot;PATH&quot;" />
<method v="2" />
</configuration>
</component>

View File

@@ -1056,9 +1056,6 @@ class Location:
@property
def hint_text(self) -> str:
hint_text = getattr(self, "_hint_text", None)
if hint_text:
return hint_text
return "at " + self.name.replace("_", " ").replace("-", " ")

View File

@@ -460,7 +460,7 @@ class CommonContext:
else:
self.update_game(cached_game)
if needed_updates:
await self.send_msgs([{"cmd": "GetDataPackage", "games": list(needed_updates)}])
await self.send_msgs([{"cmd": "GetDataPackage", "games": [game_name]} for game_name in needed_updates])
def update_game(self, game_package: dict):
for item_name, item_id in game_package["item_name_to_id"].items():
@@ -477,6 +477,7 @@ class CommonContext:
current_cache = Utils.persistent_load().get("datapackage", {}).get("games", {})
current_cache.update(data_package["games"])
Utils.persistent_store("datapackage", "games", current_cache)
logger.info(f"Got new ID/Name DataPackage for {', '.join(data_package['games'])}")
for game, game_data in data_package["games"].items():
Utils.store_data_package_for_checksum(game, game_data)
@@ -727,7 +728,6 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
await ctx.server_auth(args['password'])
elif cmd == 'DataPackage':
logger.info("Got new ID/Name DataPackage")
ctx.consume_network_data_package(args['data'])
elif cmd == 'ConnectionRefused':

12
Main.py
View File

@@ -114,7 +114,9 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
for _ in range(count):
world.push_precollected(world.create_item(item_name, player))
for item_name, count in world.start_inventory_from_pool.setdefault(player, StartInventoryPool({})).value.items():
for item_name, count in getattr(world.worlds[player].options,
"start_inventory_from_pool",
StartInventoryPool({})).value.items():
for _ in range(count):
world.push_precollected(world.create_item(item_name, player))
# remove from_pool items also from early items handling, as starting is plenty early.
@@ -167,10 +169,14 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
# remove starting inventory from pool items.
# Because some worlds don't actually create items during create_items this has to be as late as possible.
if any(world.start_inventory_from_pool[player].value for player in world.player_ids):
if any(getattr(world.worlds[player].options, "start_inventory_from_pool", None) for player in world.player_ids):
new_items: List[Item] = []
depletion_pool: Dict[int, Dict[str, int]] = {
player: world.start_inventory_from_pool[player].value.copy() for player in world.player_ids}
player: getattr(world.worlds[player].options,
"start_inventory_from_pool",
StartInventoryPool({})).value.copy()
for player in world.player_ids
}
for player, items in depletion_pool.items():
player_world: AutoWorld.World = world.worlds[player]
for count in items.values():

View File

@@ -4,14 +4,29 @@ import subprocess
import multiprocessing
import warnings
local_dir = os.path.dirname(__file__)
requirements_files = {os.path.join(local_dir, 'requirements.txt')}
if sys.version_info < (3, 8, 6):
raise RuntimeError("Incompatible Python Version. 3.8.7+ is supported.")
# don't run update if environment is frozen/compiled or if not the parent process (skip in subprocess)
update_ran = getattr(sys, "frozen", False) or multiprocessing.parent_process()
_skip_update = bool(getattr(sys, "frozen", False) or multiprocessing.parent_process())
update_ran = _skip_update
class RequirementsSet(set):
def add(self, e):
global update_ran
update_ran &= _skip_update
super().add(e)
def update(self, *s):
global update_ran
update_ran &= _skip_update
super().update(*s)
local_dir = os.path.dirname(__file__)
requirements_files = RequirementsSet((os.path.join(local_dir, 'requirements.txt'),))
if not update_ran:
for entry in os.scandir(os.path.join(local_dir, "worlds")):

View File

@@ -2210,25 +2210,24 @@ def parse_args() -> argparse.Namespace:
async def auto_shutdown(ctx, to_cancel=None):
await asyncio.sleep(ctx.auto_shutdown)
def inactivity_shutdown():
ctx.server.ws_server.close()
ctx.exit_event.set()
if to_cancel:
for task in to_cancel:
task.cancel()
logging.info("Shutting down due to inactivity.")
while not ctx.exit_event.is_set():
if not ctx.client_activity_timers.values():
ctx.server.ws_server.close()
ctx.exit_event.set()
if to_cancel:
for task in to_cancel:
task.cancel()
logging.info("Shutting down due to inactivity.")
inactivity_shutdown()
else:
newest_activity = max(ctx.client_activity_timers.values())
delta = datetime.datetime.now(datetime.timezone.utc) - newest_activity
seconds = ctx.auto_shutdown - delta.total_seconds()
if seconds < 0:
ctx.server.ws_server.close()
ctx.exit_event.set()
if to_cancel:
for task in to_cancel:
task.cancel()
logging.info("Shutting down due to inactivity.")
inactivity_shutdown()
else:
await asyncio.sleep(seconds)

View File

@@ -58,6 +58,7 @@ Currently, the following games are supported:
* Heretic
* Landstalker: The Treasures of King Nole
* Final Fantasy Mystic Quest
* TUNIC
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

@@ -789,13 +789,13 @@ class DeprecateDict(dict):
self.should_error = error
super().__init__()
if __debug__:
def __getitem__(self, item: Any) -> Any:
if self.should_error:
deprecate(self.log_message)
def __getitem__(self, item: Any) -> Any:
if self.should_error:
deprecate(self.log_message)
elif __debug__:
import warnings
warnings.warn(self.log_message)
return super().__getitem__(item)
return super().__getitem__(item)
def _extend_freeze_support() -> None:

View File

@@ -20,8 +20,8 @@ def generate_api():
race = False
meta_options_source = {}
if 'file' in request.files:
file = request.files['file']
options = get_yaml_data(file)
files = request.files.getlist('file')
options = get_yaml_data(files)
if isinstance(options, Markup):
return {"text": options.striptags()}, 400
if isinstance(options, str):

View File

@@ -69,8 +69,8 @@
</td>
<td>
<select name="collect_mode" id="collect_mode">
<option value="goal">Allow !collect after goal completion</option>
<option value="auto">Automatic on goal completion</option>
<option value="goal">Allow !collect after goal completion</option>
<option value="auto-enabled">
Automatic on goal completion and manual !collect
</option>
@@ -93,9 +93,9 @@
{% if race -%}
<option value="disabled">Disabled in Race mode</option>
{%- else -%}
<option value="disabled">Disabled</option>
<option value="goal">Allow !remaining after goal completion</option>
<option value="enabled">Manual !remaining</option>
<option value="disabled">Disabled</option>
{%- endif -%}
</select>
</td>
@@ -185,12 +185,12 @@ Warning: playthrough can take a significant amount of time for larger multiworld
</span>
</td>
<td>
<input type="checkbox" id="plando_items" name="plando_items" value="items">
<label for="plando_items">Items</label><br>
<input type="checkbox" id="plando_bosses" name="plando_bosses" value="bosses" checked>
<label for="plando_bosses">Bosses</label><br>
<input type="checkbox" id="plando_items" name="plando_items" value="items" checked>
<label for="plando_items">Items</label><br>
<input type="checkbox" id="plando_connections" name="plando_connections" value="connections" checked>
<label for="plando_connections">Connections</label><br>

View File

@@ -164,6 +164,9 @@
# The Legend of Zelda (1)
/worlds/tloz/ @Rosalie-A @t3hf1gm3nt
# TUNIC
/worlds/tunic/ @silent-destroyer
# Undertale
/worlds/undertale/ @jonloveslegos

View File

@@ -675,8 +675,8 @@ Tags are represented as a list of strings, the common Client tags follow:
### DeathLink
A special kind of Bounce packet that can be supported by any AP game. It targets the tag "DeathLink" and carries the following data:
| Name | Type | Notes |
| ---- | ---- | ---- |
| time | float | Unix Time Stamp of time of death. |
| cause | str | Optional. Text to explain the cause of death, ex. "Berserker was run over by a train." |
| source | str | Name of the player who first died. Can be a slot name, but can also be a name from within a multiplayer game. |
| Name | Type | Notes |
|--------|-------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
| time | float | Unix Time Stamp of time of death. |
| cause | str | Optional. Text to explain the cause of death. When provided, or checked, this should contain the player name, ex. "Berserker was run over by a train." |
| source | str | Name of the player who first died. Can be a slot name, but can also be a name from within a multiplayer game. |

View File

@@ -197,7 +197,7 @@ begin
begin
// Is the installed version at least the packaged one ?
Log('VC Redist x64 Version : found ' + strVersion);
Result := (CompareStr(strVersion, 'v14.32.31332') < 0);
Result := (CompareStr(strVersion, 'v14.38.33130') < 0);
end
else
begin

View File

@@ -4,7 +4,7 @@ PyYAML>=6.0.1
jellyfish>=1.0.3
jinja2>=3.1.2
schema>=0.7.5
kivy>=2.2.1
kivy>=2.3.0
bsdiff4>=1.2.4
platformdirs>=4.0.0
certifi>=2023.11.17

View File

@@ -597,8 +597,8 @@ class ServerOptions(Group):
disable_item_cheat: Union[DisableItemCheat, bool] = False
location_check_points: LocationCheckPoints = LocationCheckPoints(1)
hint_cost: HintCost = HintCost(10)
release_mode: ReleaseMode = ReleaseMode("goal")
collect_mode: CollectMode = CollectMode("goal")
release_mode: ReleaseMode = ReleaseMode("auto")
collect_mode: CollectMode = CollectMode("auto")
remaining_mode: RemainingMode = RemainingMode("goal")
auto_shutdown: AutoShutdown = AutoShutdown(0)
compatibility: Compatibility = Compatibility(2)
@@ -673,7 +673,7 @@ class GeneratorOptions(Group):
spoiler: Spoiler = Spoiler(3)
glitch_triforce_room: GlitchTriforceRoom = GlitchTriforceRoom(1) # why is this here?
race: Race = Race(0)
plando_options: PlandoOptions = PlandoOptions("bosses")
plando_options: PlandoOptions = PlandoOptions("bosses, connections, texts")
class SNIOptions(Group):

View File

@@ -54,7 +54,6 @@ if __name__ == "__main__":
# TODO: move stuff to not require this
import ModuleUpdate
ModuleUpdate.update(yes="--yes" in sys.argv or "-y" in sys.argv)
ModuleUpdate.update_ran = False # restore for later
from worlds.LauncherComponents import components, icon_paths
from Utils import version_tuple, is_windows, is_linux
@@ -76,7 +75,6 @@ non_apworlds: set = {
"Ocarina of Time",
"Overcooked! 2",
"Raft",
"Secret of Evermore",
"Slay the Spire",
"Sudoku",
"Super Mario 64",
@@ -305,7 +303,6 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
print(f"Outputting to: {self.buildfolder}")
os.makedirs(self.buildfolder, exist_ok=True)
import ModuleUpdate
ModuleUpdate.requirements_files.add(os.path.join("WebHostLib", "requirements.txt"))
ModuleUpdate.update(yes=self.yes)
# auto-build cython modules
@@ -352,6 +349,18 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
for folder in sdl2.dep_bins + glew.dep_bins:
shutil.copytree(folder, self.libfolder, dirs_exist_ok=True)
print(f"copying {folder} -> {self.libfolder}")
# windows needs Visual Studio C++ Redistributable
# Installer works for x64 and arm64
print("Downloading VC Redist")
import certifi
import ssl
context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=certifi.where())
with urllib.request.urlopen(r"https://aka.ms/vs/17/release/vc_redist.x64.exe",
context=context) as download:
vc_redist = download.read()
print(f"Download complete, {len(vc_redist) / 1024 / 1024:.2f} MBytes downloaded.", )
with open("VC_redist.x64.exe", "wb") as vc_file:
vc_file.write(vc_redist)
for data in self.extra_data:
self.installfile(Path(data))

View File

@@ -285,7 +285,7 @@ class WorldTestBase(unittest.TestCase):
if not (self.run_default_tests and self.constructed):
return
with self.subTest("Game", game=self.game):
excluded = self.multiworld.exclude_locations[1].value
excluded = self.multiworld.worlds[1].options.exclude_locations.value
state = self.multiworld.get_all_state(False)
for location in self.multiworld.get_locations():
if location.name not in excluded:

127
test/benchmark/__init__.py Normal file
View File

@@ -0,0 +1,127 @@
import time
class TimeIt:
def __init__(self, name: str, time_logger=None):
self.name = name
self.logger = time_logger
self.timer = None
self.end_timer = None
def __enter__(self):
self.timer = time.perf_counter()
return self
@property
def dif(self):
return self.end_timer - self.timer
def __exit__(self, exc_type, exc_val, exc_tb):
if not self.end_timer:
self.end_timer = time.perf_counter()
if self.logger:
self.logger.info(f"{self.dif:.4f} seconds in {self.name}.")
if __name__ == "__main__":
import argparse
import logging
import gc
import collections
import typing
# makes this module runnable from its folder.
import sys
import os
sys.path.remove(os.path.dirname(__file__))
new_home = os.path.normpath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
os.chdir(new_home)
sys.path.append(new_home)
from Utils import init_logging, local_path
local_path.cached_path = new_home
from BaseClasses import MultiWorld, CollectionState, Location
from worlds import AutoWorld
from worlds.AutoWorld import call_all
init_logging("Benchmark Runner")
logger = logging.getLogger("Benchmark")
class BenchmarkRunner:
gen_steps: typing.Tuple[str, ...] = (
"generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill")
rule_iterations: int = 100_000
if sys.version_info >= (3, 9):
@staticmethod
def format_times_from_counter(counter: collections.Counter[str], top: int = 5) -> str:
return "\n".join(f" {time:.4f} in {name}" for name, time in counter.most_common(top))
else:
@staticmethod
def format_times_from_counter(counter: collections.Counter, top: int = 5) -> str:
return "\n".join(f" {time:.4f} in {name}" for name, time in counter.most_common(top))
def location_test(self, test_location: Location, state: CollectionState, state_name: str) -> float:
with TimeIt(f"{test_location.game} {self.rule_iterations} "
f"runs of {test_location}.access_rule({state_name})", logger) as t:
for _ in range(self.rule_iterations):
test_location.access_rule(state)
# if time is taken to disentangle complex ref chains,
# this time should be attributed to the rule.
gc.collect()
return t.dif
def main(self):
for game in sorted(AutoWorld.AutoWorldRegister.world_types):
summary_data: typing.Dict[str, collections.Counter[str]] = {
"empty_state": collections.Counter(),
"all_state": collections.Counter(),
}
try:
multiworld = MultiWorld(1)
multiworld.game[1] = game
multiworld.player_name = {1: "Tester"}
multiworld.set_seed(0)
multiworld.state = CollectionState(multiworld)
args = argparse.Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types[game].options_dataclass.type_hints.items():
setattr(args, name, {
1: option.from_any(getattr(option, "default"))
})
multiworld.set_options(args)
gc.collect()
for step in self.gen_steps:
with TimeIt(f"{game} step {step}", logger):
call_all(multiworld, step)
gc.collect()
locations = sorted(multiworld.get_unfilled_locations())
if not locations:
continue
all_state = multiworld.get_all_state(False)
for location in locations:
time_taken = self.location_test(location, multiworld.state, "empty_state")
summary_data["empty_state"][location.name] = time_taken
time_taken = self.location_test(location, all_state, "all_state")
summary_data["all_state"][location.name] = time_taken
total_empty_state = sum(summary_data["empty_state"].values())
total_all_state = sum(summary_data["all_state"].values())
logger.info(f"{game} took {total_empty_state/len(locations):.4f} "
f"seconds per location in empty_state and {total_all_state/len(locations):.4f} "
f"in all_state. (all times summed for {self.rule_iterations} runs.)")
logger.info(f"Top times in empty_state:\n"
f"{self.format_times_from_counter(summary_data['empty_state'])}")
logger.info(f"Top times in all_state:\n"
f"{self.format_times_from_counter(summary_data['all_state'])}")
except Exception as e:
logger.exception(e)
runner = BenchmarkRunner()
runner.main()

View File

@@ -1,5 +1,6 @@
import unittest
from worlds.AutoWorld import AutoWorldRegister
from worlds.AutoWorld import AutoWorldRegister, call_all
from . import setup_solo_multiworld
@@ -53,7 +54,7 @@ class TestBase(unittest.TestCase):
f"{game_name} Item count MUST meet or exceed the number of locations",
)
def testItemsInDatapackage(self):
def test_items_in_datapackage(self):
"""Test that any created items in the itempool are in the datapackage"""
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest("Game", game=game_name):
@@ -69,3 +70,20 @@ class TestBase(unittest.TestCase):
with self.subTest("Name should be valid", game=game_name, item=name):
self.assertIn(name, valid_names,
"All item descriptions must match defined item names")
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")
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}
for game_name, world_type in worlds_to_test.items():
with self.subTest("Game", game=game_name):
multiworld = setup_solo_multiworld(world_type, gen_steps)
created_items = multiworld.itempool.copy()
for step in additional_steps:
with self.subTest("step", step=step):
call_all(multiworld, step)
self.assertEqual(created_items, multiworld.itempool,
f"{game_name} modified the itempool during {step}")

View File

@@ -10,3 +10,10 @@ class TestOptions(unittest.TestCase):
for option_key, option in world_type.options_dataclass.type_hints.items():
with self.subTest(game=gamename, option=option_key):
self.assertTrue(option.__doc__)
def test_options_are_not_set_by_world(self):
"""Test that options attribute is not already set"""
for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename):
self.assertFalse(hasattr(world_type, "options"),
f"Unexpected assignment to {world_type.__name__}.options!")

View File

@@ -37,7 +37,7 @@ class TestBase(unittest.TestCase):
unreachable_regions = self.default_settings_unreachable_regions.get(game_name, set())
with self.subTest("Game", game=game_name):
world = setup_solo_multiworld(world_type)
excluded = world.exclude_locations[1].value
excluded = world.worlds[1].options.exclude_locations.value
state = world.get_all_state(False)
for location in world.get_locations():
if location.name not in excluded:

View File

@@ -1,5 +1,7 @@
import io
import unittest
import json
import yaml
class TestDocs(unittest.TestCase):
@@ -23,7 +25,7 @@ class TestDocs(unittest.TestCase):
response = self.client.post("/api/generate")
self.assertIn("No options found. Expected file attachment or json weights.", response.text)
def test_generation_queued(self):
def test_generation_queued_weights(self):
options = {
"Tester1":
{
@@ -40,3 +42,19 @@ class TestDocs(unittest.TestCase):
json_data = response.get_json()
self.assertTrue(json_data["text"].startswith("Generation of seed "))
self.assertTrue(json_data["text"].endswith(" started successfully."))
def test_generation_queued_file(self):
options = {
"game": "Archipelago",
"name": "Tester",
"Archipelago": {}
}
response = self.client.post(
"/api/generate",
data={
'file': (io.BytesIO(yaml.dump(options, encoding="utf-8")), "test.yaml")
},
)
json_data = response.get_json()
self.assertTrue(json_data["text"].startswith("Generation of seed "))
self.assertTrue(json_data["text"].endswith(" started successfully."))

View File

@@ -1,24 +1,12 @@
""" FillType_* is not a real kivy type - just something to fill unknown typing. """
from typing import Sequence
FillType_Vec = Sequence[int]
class FillType_Drawable:
def __init__(self, *, pos: FillType_Vec = ..., size: FillType_Vec = ...) -> None: ...
class FillType_Texture(FillType_Drawable):
pass
from .texture import FillType_Drawable, FillType_Vec, Texture
class FillType_Shape(FillType_Drawable):
texture: FillType_Texture
texture: Texture
def __init__(self,
*,
texture: FillType_Texture = ...,
texture: Texture = ...,
pos: FillType_Vec = ...,
size: FillType_Vec = ...) -> None: ...
@@ -35,6 +23,6 @@ class Rectangle(FillType_Shape):
def __init__(self,
*,
source: str = ...,
texture: FillType_Texture = ...,
texture: Texture = ...,
pos: FillType_Vec = ...,
size: FillType_Vec = ...) -> None: ...

View File

@@ -0,0 +1,13 @@
""" FillType_* is not a real kivy type - just something to fill unknown typing. """
from typing import Sequence
FillType_Vec = Sequence[int]
class FillType_Drawable:
def __init__(self, *, pos: FillType_Vec = ..., size: FillType_Vec = ...) -> None: ...
class Texture:
pass

View File

@@ -0,0 +1,9 @@
import io
from kivy.graphics.texture import Texture
class CoreImage:
texture: Texture
def __init__(self, data: io.BytesIO, ext: str) -> None: ...

View File

@@ -39,7 +39,8 @@ class AutoWorldRegister(type):
def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoWorldRegister:
if "web" in dct:
assert isinstance(dct["web"], WebWorld), "WebWorld has to be instantiated."
assert isinstance(dct["web"], WebWorld) or isinstance(dct["web"], staticproperty), \
"WebWorld has to be instantiated."
# filter out any events
dct["item_name_to_id"] = {name: id for name, id in dct["item_name_to_id"].items() if id}
dct["location_name_to_id"] = {name: id for name, id in dct["location_name_to_id"].items() if id}
@@ -79,8 +80,8 @@ class AutoWorldRegister(type):
if "options_dataclass" not in dct and "option_definitions" in dct:
# TODO - switch to deprecate after a version
if __debug__:
from warnings import warn
warn("Assigning options through option_definitions is now deprecated. Use options_dataclass instead.")
logging.warning(f"{name} Assigned options through option_definitions which is now deprecated. "
"Please use options_dataclass instead.")
dct["options_dataclass"] = make_dataclass(f"{name}Options", dct["option_definitions"].items(),
bases=(PerGameCommonOptions,))
@@ -328,7 +329,7 @@ class World(metaclass=AutoWorldRegister):
def create_items(self) -> None:
"""
Method for creating and submitting items to the itempool. Items and Regions should *not* be created and submitted
Method for creating and submitting items to the itempool. Items and Regions must *not* be created and submitted
to the MultiWorld after this step. If items need to be placed during pre_fill use `get_prefill_items`.
"""
pass
@@ -484,6 +485,11 @@ class LogicMixin(metaclass=AutoLogicRegister):
pass
class staticproperty(staticmethod):
def __get__(self, *args):
return self.__func__()
def data_package_checksum(data: "GamesPackage") -> str:
"""Calculates the data package checksum for a game from a dict"""
assert "checksum" not in data, "Checksum already in data"

View File

@@ -26,6 +26,13 @@ class ALttPLocation(Location):
self.player_address = player_address
self._hint_text = hint_text
@property
def hint_text(self) -> str:
hint_text = getattr(self, "_hint_text", None)
if hint_text:
return hint_text
return "at " + self.name.replace("_", " ").replace("-", " ")
class ALttPItem(Item):
game: str = "A Link to the Past"

View File

@@ -7,16 +7,25 @@ from ..AutoWorld import WebWorld, World
class Bk_SudokuWebWorld(WebWorld):
options_page = "games/Sudoku/info/en"
theme = 'partyTime'
tutorials = [
Tutorial(
tutorial_name='Setup Guide',
description='A guide to playing BK Sudoku',
language='English',
file_name='setup_en.md',
link='setup/en',
authors=['Jarno']
)
]
setup_en = Tutorial(
tutorial_name='Setup Guide',
description='A guide to playing BK Sudoku',
language='English',
file_name='setup_en.md',
link='setup/en',
authors=['Jarno']
)
setup_de = Tutorial(
tutorial_name='Setup Anleitung',
description='Eine Anleitung um BK-Sudoku zu spielen',
language='Deutsch',
file_name='setup_de.md',
link='setup/de',
authors=['Held_der_Zeit']
)
tutorials = [setup_en, setup_de]
class Bk_SudokuWorld(World):

View File

@@ -0,0 +1,21 @@
# BK-Sudoku
## Was ist das für ein Spiel?
BK-Sudoku ist kein typisches Archipelago-Spiel; stattdessen ist es ein gewöhnlicher Sudoku-Client der sich zu jeder
beliebigen Multiworld verbinden kann. Einmal verbunden kannst du ein 9x9 Sudoku spielen um einen zufälligen Hinweis
für dein Spiel zu erhalten. Es ist zwar langsam, aber es gibt dir etwas zu tun, solltest du mal nicht in der Lage sein
weitere „Checks” zu erreichen.
(Wer mag kann auch einfach so Sudoku spielen. Man muss nicht mit einer Multiworld verbunden sein, um ein Sudoku zu
spielen/generieren.)
## Wie werden Hinweise freigeschalten?
Nach dem Lösen eines Sudokus wird für den verbundenen Slot ein zufällig ausgewählter Hinweis freigegeben, für einen
Gegenstand der noch nicht gefunden wurde.
## Wo ist die Seite für die Einstellungen?
Es gibt keine Seite für die Einstellungen. Dieses Spiel kann nicht in deinen YAML-Dateien benutzt werden. Stattdessen
kann sich der Client mit einem beliebigen Slot einer Multiworld verbinden. In dem Client selbst kann aber der
Schwierigkeitsgrad des Sudoku ausgewählt werden.

View File

@@ -0,0 +1,27 @@
# BK-Sudoku Setup Anleitung
## Benötigte Software
- [Bk-Sudoku](https://github.com/Jarno458/sudoku)
- Windows 8 oder höher
## Generelles Konzept
Dies ist ein Client, der sich mit jedem beliebigen Slot einer Multiworld verbinden kann. Er lässt dich ein (9x9) Sudoku
spielen, um zufällige Hinweise für den verbundenen Slot freizuschalten.
Aufgrund des Fakts, dass der Sudoku-Client sich zu jedem beliebigen Slot verbinden kann, ist es daher nicht notwendig
eine YAML für dieses Spiel zu generieren, da es keinen neuen Slot zur Multiworld-Session hinzufügt.
## Installationsprozess
Gehe zu der aktuellsten (latest) Veröffentlichung der [BK-Sudoku Releases](https://github.com/Jarno458/sudoku/releases).
Downloade und extrahiere/entpacke die `Bk_Sudoku.zip`-Datei.
## Verbinden mit einer Multiworld
1. Starte `Bk_Sudoku.exe`
2. Trage den Namen des Slots ein, mit dem du dich verbinden möchtest
3. Trage die Server-URL und den Port ein
4. Drücke auf Verbinden (connect)
5. Wähle deinen Schwierigkeitsgrad
6. Versuche das Sudoku zu Lösen

View File

@@ -11,16 +11,26 @@ from .Rules import get_button_rule
class CliqueWebWorld(WebWorld):
theme = "partyTime"
tutorials = [
Tutorial(
tutorial_name="Start Guide",
description="A guide to playing Clique.",
language="English",
file_name="guide_en.md",
link="guide/en",
authors=["Phar"]
)
]
setup_en = Tutorial(
tutorial_name="Start Guide",
description="A guide to playing Clique.",
language="English",
file_name="guide_en.md",
link="guide/en",
authors=["Phar"]
)
setup_de = Tutorial(
tutorial_name="Anleitung zum Anfangen",
description="Eine Anleitung um Clique zu spielen.",
language="Deutsch",
file_name="guide_de.md",
link="guide/de",
authors=["Held_der_Zeit"]
)
tutorials = [setup_en, setup_de]
class CliqueWorld(World):

View File

@@ -0,0 +1,18 @@
# Clique
## Was ist das für ein Spiel?
~~Clique ist ein psychologisches Überlebens-Horror Spiel, in dem der Spieler der Versuchung wiederstehen muss große~~
~~(rote) Knöpfe zu drücken.~~
Clique ist ein scherzhaftes Spiel, welches für Archipelago im März 2023 entwickelt wurde, um zu zeigen, wie einfach
es sein kann eine Welt für Archipelago zu entwicklen. Das Ziel des Spiels ist es den großen (standardmäßig) roten
Knopf zu drücken. Wenn ein Spieler auf dem `hard_mode` (schwieriger Modus) spielt, muss dieser warten bis jemand
anderes in der Multiworld den Knopf aktiviert, damit er gedrückt werden kann.
Clique kann auf den meisten modernen, HTML5-fähigen Browsern gespielt werden.
## Wo ist die Seite für die Einstellungen?
Die [Seite für die Spielereinstellungen dieses Spiels](../player-options) enthält alle Optionen die man benötigt um
eine YAML-Datei zu konfigurieren und zu exportieren.

View File

@@ -0,0 +1,25 @@
# Clique Anleitung
Nachdem dein Seed generiert wurde, gehe auf die Website von [Clique dem Spiel](http://clique.pharware.com/) und gib
Server-Daten, deinen Slot-Namen und ein Passwort (falls vorhanden) ein. Klicke dann auf "Connect" (Verbinden).
Wenn du auf "Einfach" spielst, kannst du unbedenklich den Knopf drücken und deine "Befriedigung" erhalten.
Wenn du auf "Schwer" spielst, ist es sehr wahrscheinlich, dass du warten musst bevor du dein Ziel erreichen kannst.
Glücklicherweise läuft Click auf den meißten großen Browsern, die HTML5 unterstützen. Das heißt du kannst Clique auf
deinem Handy starten und produktiv sein während du wartest!
Falls du einige Ideen brauchst was du tun kannst, während du wartest bis der Knopf aktiviert wurde, versuche
(mindestens) eins der Folgenden:
- Dein Zimmer aufräumen.
- Die Wäsche machen.
- Etwas Essen von einem X-Belieben Fast Food Restaruant holen.
- Das tägliche Wordle machen.
- ~~Deine Seele an **Phar** verkaufen.~~
- Deine Hausaufgaben erledigen.
- Deine Post abholen.
~~Solltest du auf irgendwelche Probleme in diesem Spiel stoßen, solltest du keinesfalls nicht **thephar** auf~~
~~Discord kontaktieren. *zwinker* *zwinker*~~

View File

@@ -13,14 +13,23 @@ client_version = 0
class DLCqwebworld(WebWorld):
tutorials = [Tutorial(
setup_en = Tutorial(
"Multiworld Setup Tutorial",
"A guide to setting up the Archipelago DLCQuest game on your computer.",
"English",
"setup_en.md",
"setup/en",
["axe_y"]
)]
)
setup_fr = Tutorial(
"Guide de configuration MultiWorld",
"Un guide pour configurer DLCQuest sur votre PC.",
"Français",
"setup_fr.md",
"setup/fr",
["Deoxis"]
)
tutorials = [setup_en, setup_fr]
class DLCqworld(World):

View File

@@ -0,0 +1,49 @@
# DLC Quest
## Où se trouve la page des paramètres ?
La [page des paramètres du joueur pour ce jeu](../player-settings) contient tous les paramètres dont vous avez besoin pour configurer et exporter le fichier.
## Quel est l'effet de la randomisation sur ce jeu ?
Les DLC seront obtenus en tant que check pour le multiworld. Il existe également d'autres checks optionnels dans DLC Quest.
## Quel est le but de DLC Quest ?
DLC Quest a deux campagnes, et le joueur peut choisir celle qu'il veut jouer pour sa partie.
Il peut également choisir de faire les deux campagnes.
## Quels sont les emplacements dans DLC quest ?
Les emplacements dans DLC Quest comprennent toujours
- les achats de DLC auprès du commerçant
- Les objectifs liés aux récompenses
- Tuer des moutons dans DLC Quest
- Objectifs spécifiques de l'attribution dans Live Freemium or Die
Il existe également un certain nombres de critères de localisation qui sont optionnels et que les joueurs peuvent choisir d'inclure ou non dans leur sélection :
- Objets que votre personnage peut obtenir de différentes manières
- Swords
- Gun
- Box of Various Supplies
- Humble Indie Bindle
- Pickaxe
- Coinsanity : Pièces de monnaie, soit individuellement, soit sous forme de lots personnalisés
## Quels objets peuvent se trouver dans le monde d'un autre joueur ?
Tous les DLC du jeu sont mélangés dans le stock d'objets. Les objets liés aux contrôles optionnels décrits ci-dessus sont également dans le stock
Il y a aussi de nouveaux objets pièges, utilisés comme substituts, basés sur les désagréments du jeu vanille.
- Zombie Sheep
- Loading Screens
- Temporary Spikes
## Que se passe-t-il lorsque le joueur reçoit un objet ?
Chaque fois qu'un objet est reçu en ligne, une notification apparaît à l'écran pour en informer le joueur.
Certains objets sont accompagnés d'une animation ou d'une scène qui se déroule immédiatement après leur réception.
Les objets reçus hors ligne ne sont pas accompagnés d'une animation ou d'une scène, et sont simplement activés lors de la connexion.

View File

@@ -0,0 +1,55 @@
# # Guide de configuration MultiWorld de DLCQuest
## Logiciels requis
- DLC Quest sur PC (Recommandé: [Version Steam](https://store.steampowered.com/app/230050/DLC_Quest/))
- [DLCQuestipelago](https://github.com/agilbert1412/DLCQuestipelago/releases)
- BepinEx (utilisé comme un modloader pour DLCQuest. La version du mod ci-dessus inclut BepInEx si vous choisissez la version d'installation complète)
## Logiciels optionnels
- [Archipelago] (https://github.com/ArchipelagoMW/Archipelago/releases)
- (Uniquement pour le TextClient)
## Créer un fichier de configuration (.yaml)
### Qu'est-ce qu'un fichier YAML et pourquoi en ai-je besoin ?
Voir le guide d'Archipelago sur la mise en place d'un YAML de base : [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
### Où puis-je obtenir un fichier YAML ?
Vous pouvez personnaliser vos paramètres en visitant la [page des paramètres du joueur DLC Quest] (/games/DLCQuest/player-settings).
## Rejoindre une partie multi-monde
### Installer le mod
- Télécharger le [DLCQuestipelago mod release](https://github.com/agilbert1412/DLCQuestipelago/releases). Si c'est la première fois que vous installez le mod, ou si vous n'êtes pas à l'aise avec l'édition manuelle de fichiers, vous devriez choisir l'Installateur. Il se chargera de la plus grande partie du travail pour vous
- Extraire l'archive .zip à l'emplacement de votre choix
- Exécutez "DLCQuestipelagoInstaller.exe".
![image](https://i.imgur.com/2sPhMgs.png)
- Le programme d'installation devrait décrire ce qu'il fait à chaque étape, et vous demandera votre avis si nécessaire.
- Il vous permettra de choisir l'emplacement d'installation de votre jeu moddé et vous proposera un emplacement par défaut
- Il **essayera** de trouver votre jeu DLCQuest sur votre ordinateur et, en cas d'échec, vous demandera d'indiquer le chemin d'accès.
- Il vous offrira la possibilité de créer un raccourci sur le bureau pour le lanceur moddé.
### Se connecter au MultiServer
- Localisez le fichier "ArchipelagoConnectionInfo.json", qui se situe dans le même emplacement que votre installation moddée. Vous pouvez éditer ce fichier avec n'importe quel éditeur de texte, et vous devez entrer l'adresse IP du serveur, le port et votre nom de joueur dans les champs appropriés.
- Exécutez BepInEx.NET.Framework.Launcher.exe. Si vous avez opté pour un raccourci sur le bureau, vous le trouverez avec une icône et un nom plus reconnaissable.
![image](https://i.imgur.com/ZUiFrhf.png)
- Votre jeu devrait se lancer en même temps qu'une console de modloader, qui contiendra des informations de débogage importantes si vous rencontrez des problèmes.
- Le jeu devrait se connecter automatiquement, et tenter de se reconnecter si votre internet ou le serveur se déconnecte, pendant que vous jouez.
### Interagir avec le MultiWorld depuis le jeu
Vous ne pouvez pas envoyer de commandes au serveur ou discuter avec les autres joueurs depuis DLC Quest, car le jeu ne dispose pas d'un moyen approprié pour saisir du texte.
Vous pouvez suivre l'activité du serveur dans votre console BepInEx, car les messages de chat d'Archipelago y seront affichés.
Vous devrez utiliser [Archipelago Text Client] (https://github.com/ArchipelagoMW/Archipelago/releases) si vous voulez envoyer des commandes.

View File

@@ -71,7 +71,7 @@ class FFMQClient(SNIClient):
received = await snes_read(ctx, RECEIVED_DATA[0], RECEIVED_DATA[1])
data = await snes_read(ctx, READ_DATA_START, READ_DATA_END - READ_DATA_START)
check_2 = await snes_read(ctx, 0xF53749, 1)
if check_1 == b'\x00' or check_2 == b'\x00':
if check_1 in (b'\x00', b'\x55') or check_2 in (b'\x00', b'\x55'):
return
def get_range(data_range):

View File

@@ -223,11 +223,6 @@ for item, data in item_table.items():
def create_items(self) -> None:
items = []
starting_weapon = self.multiworld.starting_weapon[self.player].current_key.title().replace("_", " ")
if self.multiworld.progressive_gear[self.player]:
for item_group in prog_map:
if starting_weapon in self.item_name_groups[item_group]:
starting_weapon = prog_map[item_group]
break
self.multiworld.push_precollected(self.create_item(starting_weapon))
self.multiworld.push_precollected(self.create_item("Steel Armor"))
if self.multiworld.sky_coin_mode[self.player] == "start_with":

File diff suppressed because one or more lines are too long

View File

@@ -821,7 +821,8 @@ class KH2Context(CommonContext):
def finishedGame(ctx: KH2Context, message):
if ctx.kh2slotdata['FinalXemnas'] == 1:
if not ctx.final_xemnas and ctx.kh2_loc_name_to_id[LocationName.FinalXemnas] in ctx.locations_checked:
if not ctx.final_xemnas and ctx.kh2_read_byte(ctx.Save + all_world_locations[LocationName.FinalXemnas].addrObtained) \
& 0x1 << all_world_locations[LocationName.FinalXemnas].bitIndex > 0:
ctx.final_xemnas = True
# three proofs
if ctx.kh2slotdata['Goal'] == 0:

View File

@@ -2,22 +2,7 @@ import typing
from BaseClasses import Item
from .Names import ItemName
class KH2Item(Item):
game: str = "Kingdom Hearts 2"
class ItemData(typing.NamedTuple):
quantity: int = 0
kh2id: int = 0
# Save+ mem addr
memaddr: int = 0
# some items have bitmasks. if bitmask>0 bitor to give item else
bitmask: int = 0
# if ability then
ability: bool = False
from .Subclasses import ItemData
# 0x130000
Reports_Table = {
@@ -209,7 +194,7 @@ Armor_Table = {
ItemName.GrandRibbon: ItemData(1, 157, 0x35D4),
}
Usefull_Table = {
ItemName.MickeyMunnyPouch: ItemData(1, 535, 0x3695), # 5000 munny per
ItemName.MickeyMunnyPouch: ItemData(1, 535, 0x3695), # 5000 munny per
ItemName.OletteMunnyPouch: ItemData(2, 362, 0x363C), # 2500 munny per
ItemName.HadesCupTrophy: ItemData(1, 537, 0x3696),
ItemName.UnknownDisk: ItemData(1, 462, 0x365F),
@@ -349,7 +334,7 @@ GoofyAbility_Table = {
Wincon_Table = {
ItemName.LuckyEmblem: ItemData(kh2id=367, memaddr=0x3641), # letter item
ItemName.Victory: ItemData(kh2id=263, memaddr=0x111),
# ItemName.Victory: ItemData(kh2id=263, memaddr=0x111),
ItemName.Bounty: ItemData(kh2id=461, memaddr=0x365E), # Dummy 14
# ItemName.UniversalKey:ItemData(,365,0x363F,0)#Tournament Poster
}

View File

@@ -1,19 +1,9 @@
import typing
from BaseClasses import Location
from .Names import LocationName, ItemName
class KH2Location(Location):
game: str = "Kingdom Hearts 2"
class LocationData(typing.NamedTuple):
locid: int
yml: str
charName: str = "Sora"
charNumber: int = 1
from .Names import LocationName, ItemName, RegionName
from .Subclasses import LocationData
from .Regions import KH2REGIONS
# data's addrcheck sys3 addr obtained roomid bit index is eventid
LoD_Checks = {
@@ -541,7 +531,7 @@ TWTNW_Checks = {
LocationName.Xemnas1: LocationData(26, "Double Get Bonus"),
LocationName.Xemnas1GetBonus: LocationData(26, "Second Get Bonus"),
LocationName.Xemnas1SecretAnsemReport13: LocationData(537, "Chest"),
LocationName.FinalXemnas: LocationData(71, "Get Bonus"),
# LocationName.FinalXemnas: LocationData(71, "Get Bonus"),
LocationName.XemnasDataPowerBoost: LocationData(554, "Chest"),
}
@@ -806,74 +796,75 @@ Atlantica_Checks = {
}
event_location_to_item = {
LocationName.HostileProgramEventLocation: ItemName.HostileProgramEvent,
LocationName.McpEventLocation: ItemName.McpEvent,
LocationName.HostileProgramEventLocation: ItemName.HostileProgramEvent,
LocationName.McpEventLocation: ItemName.McpEvent,
# LocationName.ASLarxeneEventLocation: ItemName.ASLarxeneEvent,
LocationName.DataLarxeneEventLocation: ItemName.DataLarxeneEvent,
LocationName.BarbosaEventLocation: ItemName.BarbosaEvent,
LocationName.GrimReaper1EventLocation: ItemName.GrimReaper1Event,
LocationName.GrimReaper2EventLocation: ItemName.GrimReaper2Event,
LocationName.DataLuxordEventLocation: ItemName.DataLuxordEvent,
LocationName.DataAxelEventLocation: ItemName.DataAxelEvent,
LocationName.CerberusEventLocation: ItemName.CerberusEvent,
LocationName.OlympusPeteEventLocation: ItemName.OlympusPeteEvent,
LocationName.HydraEventLocation: ItemName.HydraEvent,
LocationName.DataLarxeneEventLocation: ItemName.DataLarxeneEvent,
LocationName.BarbosaEventLocation: ItemName.BarbosaEvent,
LocationName.GrimReaper1EventLocation: ItemName.GrimReaper1Event,
LocationName.GrimReaper2EventLocation: ItemName.GrimReaper2Event,
LocationName.DataLuxordEventLocation: ItemName.DataLuxordEvent,
LocationName.DataAxelEventLocation: ItemName.DataAxelEvent,
LocationName.CerberusEventLocation: ItemName.CerberusEvent,
LocationName.OlympusPeteEventLocation: ItemName.OlympusPeteEvent,
LocationName.HydraEventLocation: ItemName.HydraEvent,
LocationName.OcPainAndPanicCupEventLocation: ItemName.OcPainAndPanicCupEvent,
LocationName.OcCerberusCupEventLocation: ItemName.OcCerberusCupEvent,
LocationName.HadesEventLocation: ItemName.HadesEvent,
LocationName.OcCerberusCupEventLocation: ItemName.OcCerberusCupEvent,
LocationName.HadesEventLocation: ItemName.HadesEvent,
# LocationName.ASZexionEventLocation: ItemName.ASZexionEvent,
LocationName.DataZexionEventLocation: ItemName.DataZexionEvent,
LocationName.Oc2TitanCupEventLocation: ItemName.Oc2TitanCupEvent,
LocationName.Oc2GofCupEventLocation: ItemName.Oc2GofCupEvent,
LocationName.DataZexionEventLocation: ItemName.DataZexionEvent,
LocationName.Oc2TitanCupEventLocation: ItemName.Oc2TitanCupEvent,
LocationName.Oc2GofCupEventLocation: ItemName.Oc2GofCupEvent,
# LocationName.Oc2CupsEventLocation: ItemName.Oc2CupsEventLocation,
LocationName.HadesCupEventLocations: ItemName.HadesCupEvents,
LocationName.PrisonKeeperEventLocation: ItemName.PrisonKeeperEvent,
LocationName.OogieBoogieEventLocation: ItemName.OogieBoogieEvent,
LocationName.ExperimentEventLocation: ItemName.ExperimentEvent,
LocationName.HadesCupEventLocations: ItemName.HadesCupEvents,
LocationName.PrisonKeeperEventLocation: ItemName.PrisonKeeperEvent,
LocationName.OogieBoogieEventLocation: ItemName.OogieBoogieEvent,
LocationName.ExperimentEventLocation: ItemName.ExperimentEvent,
# LocationName.ASVexenEventLocation: ItemName.ASVexenEvent,
LocationName.DataVexenEventLocation: ItemName.DataVexenEvent,
LocationName.ShanYuEventLocation: ItemName.ShanYuEvent,
LocationName.AnsemRikuEventLocation: ItemName.AnsemRikuEvent,
LocationName.StormRiderEventLocation: ItemName.StormRiderEvent,
LocationName.DataXigbarEventLocation: ItemName.DataXigbarEvent,
LocationName.RoxasEventLocation: ItemName.RoxasEvent,
LocationName.XigbarEventLocation: ItemName.XigbarEvent,
LocationName.LuxordEventLocation: ItemName.LuxordEvent,
LocationName.SaixEventLocation: ItemName.SaixEvent,
LocationName.XemnasEventLocation: ItemName.XemnasEvent,
LocationName.ArmoredXemnasEventLocation: ItemName.ArmoredXemnasEvent,
LocationName.ArmoredXemnas2EventLocation: ItemName.ArmoredXemnas2Event,
LocationName.DataVexenEventLocation: ItemName.DataVexenEvent,
LocationName.ShanYuEventLocation: ItemName.ShanYuEvent,
LocationName.AnsemRikuEventLocation: ItemName.AnsemRikuEvent,
LocationName.StormRiderEventLocation: ItemName.StormRiderEvent,
LocationName.DataXigbarEventLocation: ItemName.DataXigbarEvent,
LocationName.RoxasEventLocation: ItemName.RoxasEvent,
LocationName.XigbarEventLocation: ItemName.XigbarEvent,
LocationName.LuxordEventLocation: ItemName.LuxordEvent,
LocationName.SaixEventLocation: ItemName.SaixEvent,
LocationName.XemnasEventLocation: ItemName.XemnasEvent,
LocationName.ArmoredXemnasEventLocation: ItemName.ArmoredXemnasEvent,
LocationName.ArmoredXemnas2EventLocation: ItemName.ArmoredXemnas2Event,
# LocationName.FinalXemnasEventLocation: ItemName.FinalXemnasEvent,
LocationName.DataXemnasEventLocation: ItemName.DataXemnasEvent,
LocationName.ThresholderEventLocation: ItemName.ThresholderEvent,
LocationName.BeastEventLocation: ItemName.BeastEvent,
LocationName.DarkThornEventLocation: ItemName.DarkThornEvent,
LocationName.XaldinEventLocation: ItemName.XaldinEvent,
LocationName.DataXaldinEventLocation: ItemName.DataXaldinEvent,
LocationName.TwinLordsEventLocation: ItemName.TwinLordsEvent,
LocationName.GenieJafarEventLocation: ItemName.GenieJafarEvent,
LocationName.DataXemnasEventLocation: ItemName.DataXemnasEvent,
LocationName.ThresholderEventLocation: ItemName.ThresholderEvent,
LocationName.BeastEventLocation: ItemName.BeastEvent,
LocationName.DarkThornEventLocation: ItemName.DarkThornEvent,
LocationName.XaldinEventLocation: ItemName.XaldinEvent,
LocationName.DataXaldinEventLocation: ItemName.DataXaldinEvent,
LocationName.TwinLordsEventLocation: ItemName.TwinLordsEvent,
LocationName.GenieJafarEventLocation: ItemName.GenieJafarEvent,
# LocationName.ASLexaeusEventLocation: ItemName.ASLexaeusEvent,
LocationName.DataLexaeusEventLocation: ItemName.DataLexaeusEvent,
LocationName.ScarEventLocation: ItemName.ScarEvent,
LocationName.GroundShakerEventLocation: ItemName.GroundShakerEvent,
LocationName.DataSaixEventLocation: ItemName.DataSaixEvent,
LocationName.HBDemyxEventLocation: ItemName.HBDemyxEvent,
LocationName.ThousandHeartlessEventLocation: ItemName.ThousandHeartlessEvent,
LocationName.Mushroom13EventLocation: ItemName.Mushroom13Event,
LocationName.SephiEventLocation: ItemName.SephiEvent,
LocationName.DataDemyxEventLocation: ItemName.DataDemyxEvent,
LocationName.CorFirstFightEventLocation: ItemName.CorFirstFightEvent,
LocationName.CorSecondFightEventLocation: ItemName.CorSecondFightEvent,
LocationName.TransportEventLocation: ItemName.TransportEvent,
LocationName.OldPeteEventLocation: ItemName.OldPeteEvent,
LocationName.FuturePeteEventLocation: ItemName.FuturePeteEvent,
LocationName.DataLexaeusEventLocation: ItemName.DataLexaeusEvent,
LocationName.ScarEventLocation: ItemName.ScarEvent,
LocationName.GroundShakerEventLocation: ItemName.GroundShakerEvent,
LocationName.DataSaixEventLocation: ItemName.DataSaixEvent,
LocationName.HBDemyxEventLocation: ItemName.HBDemyxEvent,
LocationName.ThousandHeartlessEventLocation: ItemName.ThousandHeartlessEvent,
LocationName.Mushroom13EventLocation: ItemName.Mushroom13Event,
LocationName.SephiEventLocation: ItemName.SephiEvent,
LocationName.DataDemyxEventLocation: ItemName.DataDemyxEvent,
LocationName.CorFirstFightEventLocation: ItemName.CorFirstFightEvent,
LocationName.CorSecondFightEventLocation: ItemName.CorSecondFightEvent,
LocationName.TransportEventLocation: ItemName.TransportEvent,
LocationName.OldPeteEventLocation: ItemName.OldPeteEvent,
LocationName.FuturePeteEventLocation: ItemName.FuturePeteEvent,
# LocationName.ASMarluxiaEventLocation: ItemName.ASMarluxiaEvent,
LocationName.DataMarluxiaEventLocation: ItemName.DataMarluxiaEvent,
LocationName.TerraEventLocation: ItemName.TerraEvent,
LocationName.TwilightThornEventLocation: ItemName.TwilightThornEvent,
LocationName.Axel1EventLocation: ItemName.Axel1Event,
LocationName.Axel2EventLocation: ItemName.Axel2Event,
LocationName.DataRoxasEventLocation: ItemName.DataRoxasEvent,
LocationName.DataMarluxiaEventLocation: ItemName.DataMarluxiaEvent,
LocationName.TerraEventLocation: ItemName.TerraEvent,
LocationName.TwilightThornEventLocation: ItemName.TwilightThornEvent,
LocationName.Axel1EventLocation: ItemName.Axel1Event,
LocationName.Axel2EventLocation: ItemName.Axel2Event,
LocationName.DataRoxasEventLocation: ItemName.DataRoxasEvent,
LocationName.FinalXemnasEventLocation: ItemName.Victory,
}
all_weapon_slot = {
LocationName.FAKESlot,
@@ -1361,3 +1352,9 @@ exclusion_table = {
location for location, data in all_locations.items() if location not in event_location_to_item.keys() and location not in popups_set and location != LocationName.StationofSerenityPotion and data.yml == "Chest"
}
}
location_groups: typing.Dict[str, list]
location_groups = {
Region_Name: [loc for loc in Region_Locs if "Event" not in loc]
for Region_Name, Region_Locs in KH2REGIONS.items() if Region_Locs
}

View File

@@ -2,7 +2,7 @@ from dataclasses import dataclass
from Options import Choice, Range, Toggle, ItemDict, PerGameCommonOptions, StartInventoryPool
from worlds.kh2 import default_itempool_option
from . import default_itempool_option
class SoraEXP(Range):

View File

@@ -1,9 +1,11 @@
import typing
from BaseClasses import MultiWorld, Region
from . import Locations
from .Locations import KH2Location, event_location_to_item
from . import LocationName, RegionName, Events_Table
from .Subclasses import KH2Location
from .Names import LocationName, RegionName
from .Items import Events_Table
KH2REGIONS: typing.Dict[str, typing.List[str]] = {
"Menu": [],
@@ -788,7 +790,7 @@ KH2REGIONS: typing.Dict[str, typing.List[str]] = {
LocationName.ArmoredXemnas2EventLocation
],
RegionName.FinalXemnas: [
LocationName.FinalXemnas
LocationName.FinalXemnasEventLocation
],
RegionName.DataXemnas: [
LocationName.XemnasDataPowerBoost,
@@ -1020,7 +1022,8 @@ def create_regions(self):
multiworld.regions += [create_region(multiworld, player, active_locations, region, locations) for region, locations in
KH2REGIONS.items()]
# fill the event locations with events
for location, item in event_location_to_item.items():
for location, item in Locations.event_location_to_item.items():
multiworld.get_location(location, player).place_locked_item(
multiworld.worlds[player].create_event_item(item))

View File

@@ -1,7 +1,7 @@
from typing import Dict, Callable, TYPE_CHECKING
from BaseClasses import CollectionState
from .Items import exclusion_item_table, visit_locking_dict, DonaldAbility_Table, GoofyAbility_Table
from .Items import exclusion_item_table, visit_locking_dict, DonaldAbility_Table, GoofyAbility_Table, SupportAbility_Table
from .Locations import exclusion_table, popups_set, Goofy_Checks, Donald_Checks
from .Names import LocationName, ItemName, RegionName
from worlds.generic.Rules import add_rule, forbid_items, add_item_rule
@@ -83,6 +83,8 @@ class KH2Rules:
return state.has(ItemName.TornPages, self.player, amount)
def level_locking_unlock(self, state: CollectionState, amount):
if self.world.options.Promise_Charm and state.has(ItemName.PromiseCharm, self.player):
return True
return amount <= sum([state.count(item_name, self.player) for item_name in visit_locking_dict["2VisitLocking"]])
def summon_levels_unlocked(self, state: CollectionState, amount) -> bool:
@@ -224,7 +226,7 @@ class KH2WorldRules(KH2Rules):
RegionName.Pl2: lambda state: self.pl_unlocked(state, 2),
RegionName.Ag: lambda state: self.ag_unlocked(state, 1),
RegionName.Ag2: lambda state: self.ag_unlocked(state, 2) and self.kh2_has_all([ItemName.FireElement,ItemName.BlizzardElement,ItemName.ThunderElement],state),
RegionName.Ag2: lambda state: self.ag_unlocked(state, 2) and self.kh2_has_all([ItemName.FireElement, ItemName.BlizzardElement, ItemName.ThunderElement], state),
RegionName.Bc: lambda state: self.bc_unlocked(state, 1),
RegionName.Bc2: lambda state: self.bc_unlocked(state, 2),
@@ -266,9 +268,11 @@ class KH2WorldRules(KH2Rules):
add_item_rule(location, lambda item: item.player == self.player and item.name in GoofyAbility_Table.keys())
elif location.name in Donald_Checks:
add_item_rule(location, lambda item: item.player == self.player and item.name in DonaldAbility_Table.keys())
else:
add_item_rule(location, lambda item: item.player == self.player and item.name in SupportAbility_Table.keys())
def set_kh2_goal(self):
final_xemnas_location = self.multiworld.get_location(LocationName.FinalXemnas, self.player)
final_xemnas_location = self.multiworld.get_location(LocationName.FinalXemnasEventLocation, self.player)
if self.multiworld.Goal[self.player] == "three_proofs":
final_xemnas_location.access_rule = lambda state: self.kh2_has_all(three_proofs, state)
if self.multiworld.FinalXemnas[self.player]:
@@ -417,7 +421,7 @@ class KH2FightRules(KH2Rules):
RegionName.DataLexaeus: lambda state: self.get_data_lexaeus_rules(state),
RegionName.OldPete: lambda state: self.get_old_pete_rules(),
RegionName.FuturePete: lambda state: self.get_future_pete_rules(state),
RegionName.Terra: lambda state: self.get_terra_rules(state),
RegionName.Terra: lambda state: self.get_terra_rules(state) and state.has(ItemName.ProofofConnection, self.player),
RegionName.DataMarluxia: lambda state: self.get_data_marluxia_rules(state),
RegionName.Barbosa: lambda state: self.get_barbosa_rules(state),
RegionName.GrimReaper1: lambda state: self.get_grim_reaper1_rules(),

29
worlds/kh2/Subclasses.py Normal file
View File

@@ -0,0 +1,29 @@
import typing
from BaseClasses import Location, Item
class KH2Location(Location):
game: str = "Kingdom Hearts 2"
class LocationData(typing.NamedTuple):
locid: int
yml: str
charName: str = "Sora"
charNumber: int = 1
class KH2Item(Item):
game: str = "Kingdom Hearts 2"
class ItemData(typing.NamedTuple):
quantity: int = 0
kh2id: int = 0
# Save+ mem addr
memaddr: int = 0
# some items have bitmasks. if bitmask>0 bitor to give item else
bitmask: int = 0
# if ability then
ability: bool = False

View File

@@ -12,6 +12,7 @@ from .OpenKH import patch_kh2
from .Options import KingdomHearts2Options
from .Regions import create_regions, connect_regions
from .Rules import *
from .Subclasses import KH2Item
def launch_client():
@@ -49,7 +50,9 @@ class KH2World(World):
for item_id, item in enumerate(item_dictionary_table.keys(), 0x130000)}
location_name_to_id = {item: location
for location, item in enumerate(all_locations.keys(), 0x130000)}
item_name_groups = item_groups
location_name_groups = location_groups
visitlocking_dict: Dict[str, int]
plando_locations: Dict[str, str]
@@ -253,11 +256,8 @@ class KH2World(World):
self.goofy_gen_early()
self.keyblade_gen_early()
if self.multiworld.FinalXemnas[self.player]:
self.plando_locations[LocationName.FinalXemnas] = ItemName.Victory
else:
self.plando_locations[LocationName.FinalXemnas] = self.create_filler().name
self.total_locations -= 1
# final xemnas isn't a location anymore
# self.total_locations -= 1
if self.options.WeaponSlotStartHint:
for location in all_weapon_slot:

View File

@@ -399,6 +399,26 @@ class Palette(Choice):
option_pink = 4
option_inverted = 5
class Music(Choice, LADXROption):
"""
[Vanilla] Regular Music
[Shuffled] Shuffled Music
[Off] No music
"""
ladxr_name = "music"
option_vanilla = 0
option_shuffled = 1
option_off = 2
def to_ladxr_option(self, all_options):
s = ""
if self.value == self.option_shuffled:
s = "random"
elif self.value == self.option_off:
s = "off"
return self.ladxr_name, s
class WarpImprovements(DefaultOffToggle):
"""
[On] Adds remake style warp screen to the game. Choose your warp destination on the map after jumping in a portal and press B to select.
@@ -444,6 +464,7 @@ links_awakening_options: typing.Dict[str, typing.Type[Option]] = {
'shuffle_maps': ShuffleMaps,
'shuffle_compasses': ShuffleCompasses,
'shuffle_stone_beaks': ShuffleStoneBeaks,
'music': Music,
'music_change_condition': MusicChangeCondition,
'nag_messages': NagMessages,
'ap_title_screen': APTitleScreen,

View File

@@ -30,8 +30,8 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
### Where do I get a config file?
The [Player Settings Page](../player-settings) on the website allows you to easily configure your personal settings
and export a config file from them.
The [Player Settings Page](/games/Landstalker%20-%20The%20Treasures%20of%20King%20Nole/player-settings) on the website allows
you to easily configure your personal settings
## How-to-play

View File

@@ -1118,7 +1118,13 @@
id: Cross Room/Panel_north_missing
colors: green
tag: forbid
required_room: Outside The Bold
required_panel:
- room: Outside The Bold
panel: MOUTH
- room: Outside The Bold
panel: YEAST
- room: Outside The Bold
panel: WET
DIAMONDS:
id: Cross Room/Panel_diamonds_missing
colors: green
@@ -2635,12 +2641,6 @@
panels:
- OBSTACLE
The Colorful:
# The set of required_doors in the achievement panel should prevent
# generation from asking you to solve The Colorful before opening all of the
# doors. Access from the roof is included so that the painting here could be
# an entrance. The client will have to be hardcoded to not open the door to
# the achievement until all of the doors are open, whether by solving the
# panels or through receiving items.
entrances:
The Colorful (Gray):
room: The Colorful (Gray)
@@ -2651,31 +2651,53 @@
id: Countdown Panels/Panel_colorful_colorful
check: True
tag: forbid
required_door:
required_panel:
- room: The Colorful (White)
door: Progress Door
panel: BEGIN
- room: The Colorful (Black)
door: Progress Door
panel: FOUND
- room: The Colorful (Red)
door: Progress Door
panel: LOAF
- room: The Colorful (Yellow)
door: Progress Door
panel: CREAM
- room: The Colorful (Blue)
door: Progress Door
panel: SUN
- room: The Colorful (Purple)
door: Progress Door
panel: SPOON
- room: The Colorful (Orange)
door: Progress Door
panel: LETTERS
- room: The Colorful (Green)
door: Progress Door
panel: WALLS
- room: The Colorful (Brown)
door: Progress Door
panel: IRON
- room: The Colorful (Gray)
door: Progress Door
panel: OBSTACLE
achievement: The Colorful
paintings:
- id: arrows_painting_12
orientation: north
progression:
Progressive Colorful:
- room: The Colorful (White)
door: Progress Door
- room: The Colorful (Black)
door: Progress Door
- room: The Colorful (Red)
door: Progress Door
- room: The Colorful (Yellow)
door: Progress Door
- room: The Colorful (Blue)
door: Progress Door
- room: The Colorful (Purple)
door: Progress Door
- room: The Colorful (Orange)
door: Progress Door
- room: The Colorful (Green)
door: Progress Door
- room: The Colorful (Brown)
door: Progress Door
- room: The Colorful (Gray)
door: Progress Door
Welcome Back Area:
entrances:
Starting Room:
@@ -4202,9 +4224,6 @@
SIX:
id: Backside Room/Panel_six_six_5
tag: midwhite
colors:
- red
- yellow
hunt: True
required_door:
room: Number Hunt
@@ -4280,9 +4299,6 @@
SIX:
id: Backside Room/Panel_six_six_6
tag: midwhite
colors:
- red
- yellow
hunt: True
required_door:
room: Number Hunt
@@ -4404,9 +4420,14 @@
colors: blue
tag: forbid
required_panel:
room: The Bearer (West)
panel: SMILE
required_room: Outside The Bold
- room: The Bearer (West)
panel: SMILE
- room: Outside The Bold
panel: MOUTH
- room: Outside The Bold
panel: YEAST
- room: Outside The Bold
panel: WET
Cross Tower (South):
entrances: # No roof access
The Bearer (North):

View File

@@ -1452,3 +1452,4 @@ progression:
Progressive Fearless: 444470
Progressive Orange Tower: 444482
Progressive Art Gallery: 444563
Progressive Colorful: 444580

View File

@@ -28,6 +28,10 @@ class ItemData(NamedTuple):
# door shuffle is on and tower isn't progressive
return world.options.shuffle_doors != ShuffleDoors.option_none \
and not world.options.progressive_orange_tower
elif self.mode == "the colorful":
# complex door shuffle is on and colorful isn't progressive
return world.options.shuffle_doors == ShuffleDoors.option_complex \
and not world.options.progressive_colorful
elif self.mode == "complex door":
return world.options.shuffle_doors == ShuffleDoors.option_complex
elif self.mode == "door group":
@@ -70,6 +74,8 @@ def load_item_data():
if room_name in PROGRESSION_BY_ROOM and door_name in PROGRESSION_BY_ROOM[room_name]:
if room_name == "Orange Tower":
door_mode = "orange tower"
elif room_name == "The Colorful":
door_mode = "the colorful"
else:
door_mode = "special"

View File

@@ -21,6 +21,13 @@ class ProgressiveOrangeTower(DefaultOnToggle):
display_name = "Progressive Orange Tower"
class ProgressiveColorful(DefaultOnToggle):
"""When "Shuffle Doors" is on "complex", this setting governs the manner in which The Colorful opens up.
If off, there is an item for each room of The Colorful, meaning that random rooms in the middle of the sequence can open up without giving you access to them.
If on, there are ten progressive items, which open up the sequence from White forward."""
display_name = "Progressive Colorful"
class LocationChecks(Choice):
"""On "normal", there will be a location check for each panel set that would ordinarily open a door, as well as for
achievement panels and a small handful of other panels.
@@ -117,6 +124,7 @@ class DeathLink(Toggle):
class LingoOptions(PerGameCommonOptions):
shuffle_doors: ShuffleDoors
progressive_orange_tower: ProgressiveOrangeTower
progressive_colorful: ProgressiveColorful
location_checks: LocationChecks
shuffle_colors: ShuffleColors
shuffle_panels: ShufflePanels

View File

@@ -83,7 +83,8 @@ class LingoPlayerLogic:
def handle_non_grouped_door(self, room_name: str, door_data: Door, world: "LingoWorld"):
if room_name in PROGRESSION_BY_ROOM and door_data.name in PROGRESSION_BY_ROOM[room_name]:
if room_name == "Orange Tower" and not world.options.progressive_orange_tower:
if (room_name == "Orange Tower" and not world.options.progressive_orange_tower)\
or (room_name == "The Colorful" and not world.options.progressive_colorful):
self.set_door_item(room_name, door_data.name, door_data.item_name)
else:
progressive_item_name = PROGRESSION_BY_ROOM[room_name][door_data.name].item_name
@@ -223,7 +224,7 @@ class LingoPlayerLogic:
"kind of logic error.")
if door_shuffle != ShuffleDoors.option_none and location_classification != LocationClassification.insanity \
and not early_color_hallways is False:
and not early_color_hallways:
# If shuffle doors is on, force a useful item onto the HI panel. This may not necessarily get you out of BK,
# but the goal is to allow you to reach at least one more check. The non-painting ones are hardcoded right
# now. We only allow the entrance to the Pilgrim Room if color shuffle is off, because otherwise there are

View File

@@ -34,6 +34,7 @@ class MuseDashCollections:
"Rush-Hour",
"Find this Month's Featured Playlist",
"PeroPero in the Universe",
"umpopoff"
]
album_items: Dict[str, AlbumData] = {}
@@ -81,11 +82,22 @@ class MuseDashCollections:
steamer_mode = sections[3] == "True"
if song_name in self.DIFF_OVERRIDES:
# Note: These difficulties may not actually be representative of these songs.
# The game does not provide these difficulties so they have to be filled in.
diff_of_easy = 4
diff_of_hard = 7
diff_of_master = 10
# These songs use non-standard difficulty values. Which are being overriden with standard values.
# But also avoid filling any missing difficulties (i.e. 0s) with a difficulty value.
if sections[4] != '0':
diff_of_easy = 4
else:
diff_of_easy = None
if sections[5] != '0':
diff_of_hard = 7
else:
diff_of_hard = None
if sections[6] != '0':
diff_of_master = 10
else:
diff_of_master = None
else:
diff_of_easy = self.parse_song_difficulty(sections[4])
diff_of_hard = self.parse_song_difficulty(sections[5])

View File

@@ -119,7 +119,7 @@ Prestige and Vestige|56-4|Give Up TREATMENT Vol.11|True|6|8|11|
Tiny Fate|56-5|Give Up TREATMENT Vol.11|False|7|9|11|
Tsuki ni Murakumo Hana ni Kaze|55-0|Touhou Mugakudan -2-|False|3|5|7|
Patchouli's - Best Hit GSK|55-1|Touhou Mugakudan -2-|False|3|5|8|
Monosugoi Space Shuttle de Koishi ga Monosugoi uta|55-2|Touhou Mugakudan -2-|False|3|5|7|
Monosugoi Space Shuttle de Koishi ga Monosugoi uta|55-2|Touhou Mugakudan -2-|False|3|5|7|11
Kakoinaki Yo wa Ichigo no Tsukikage|55-3|Touhou Mugakudan -2-|False|3|6|8|
Psychedelic Kizakura Doumei|55-4|Touhou Mugakudan -2-|False|4|7|10|
Mischievous Sensation|55-5|Touhou Mugakudan -2-|False|5|7|9|
@@ -501,4 +501,12 @@ slic.hertz|68-1|Gambler's Tricks|True|5|7|9|
Fuzzy-Navel|68-2|Gambler's Tricks|True|6|8|10|11
Swing Edge|68-3|Gambler's Tricks|True|4|8|10|
Twisted Escape|68-4|Gambler's Tricks|True|5|8|10|11
Swing Sweet Twee Dance|68-5|Gambler's Tricks|False|4|7|10|
Swing Sweet Twee Dance|68-5|Gambler's Tricks|False|4|7|10|
Sanyousei SAY YA!!!|43-42|MD Plus Project|False|4|6|8|
YUKEMURI TAMAONSEN II|43-43|MD Plus Project|False|3|6|9|
Samayoi no mei Amatsu|69-0|Touhou Mugakudan -3-|False|4|6|9|
INTERNET SURVIVOR|69-1|Touhou Mugakudan -3-|False|5|8|10|
Shuki*RaiRai|69-2|Touhou Mugakudan -3-|False|5|7|9|
HELLOHELL|69-3|Touhou Mugakudan -3-|False|4|7|10|
Calamity Fortune|69-4|Touhou Mugakudan -3-|True|6|8|10|11
Tsurupettan|69-5|Touhou Mugakudan -3-|True|2|5|8|

View File

@@ -36,7 +36,7 @@ class AdditionalSongs(Range):
- The final song count may be lower due to other settings.
"""
range_start = 15
range_end = 500 # Note will probably not reach this high if any other settings are done.
range_end = 508 # Note will probably not reach this high if any other settings are done.
default = 40
display_name = "Additional Song Count"

View File

@@ -328,5 +328,6 @@ class MuseDashWorld(World):
"victoryLocation": self.victory_song_name,
"deathLink": self.options.death_link.value,
"musicSheetWinCount": self.get_music_sheet_win_count(),
"gradeNeeded": self.options.grade_needed.value
"gradeNeeded": self.options.grade_needed.value,
"hasFiller": True,
}

View File

@@ -66,5 +66,11 @@ class DifficultyRanges(MuseDashTestBase):
for song_name in muse_dash_world.md_collection.DIFF_OVERRIDES:
song = muse_dash_world.md_collection.song_items[song_name]
self.assertTrue(song.easy is not None and song.hard is not None and song.master is not None,
# umpopoff is a one time weird song. Its currently the only song in the game
# with non-standard difficulties and also doesn't have 3 or more difficulties.
if song_name == 'umpopoff':
self.assertTrue(song.easy is None and song.hard is not None and song.master is None,
f"Song '{song_name}' difficulty not set when it should be.")
else:
self.assertTrue(song.easy is not None and song.hard is not None and song.master is not None,
f"Song '{song_name}' difficulty not set when it should be.")

View File

@@ -1,42 +0,0 @@
from typing import Dict
from BaseClasses import Item, ItemClassification, Location, MultiWorld, Region
from . import Items, Locations
def create_event(player: int, name: str) -> Item:
return Items.NoitaItem(name, ItemClassification.progression, None, player)
def create_location(player: int, name: str, region: Region) -> Location:
return Locations.NoitaLocation(player, name, None, region)
def create_locked_location_event(multiworld: MultiWorld, player: int, region_name: str, item: str) -> Location:
region = multiworld.get_region(region_name, player)
new_location = create_location(player, item, region)
new_location.place_locked_item(create_event(player, item))
region.locations.append(new_location)
return new_location
def create_all_events(multiworld: MultiWorld, player: int) -> None:
for region, event in event_locks.items():
create_locked_location_event(multiworld, player, region, event)
multiworld.completion_condition[player] = lambda state: state.has("Victory", player)
# Maps region names to event names
event_locks: Dict[str, str] = {
"The Work": "Victory",
"Mines": "Portal to Holy Mountain 1",
"Coal Pits": "Portal to Holy Mountain 2",
"Snowy Depths": "Portal to Holy Mountain 3",
"Hiisi Base": "Portal to Holy Mountain 4",
"Underground Jungle": "Portal to Holy Mountain 5",
"The Vault": "Portal to Holy Mountain 6",
"Temple of the Art": "Portal to Holy Mountain 7",
}

View File

@@ -1,166 +0,0 @@
from typing import List, NamedTuple, Set
from BaseClasses import CollectionState, MultiWorld
from . import Items, Locations
from .Options import BossesAsChecks, VictoryCondition
from worlds.generic import Rules as GenericRules
class EntranceLock(NamedTuple):
source: str
destination: str
event: str
items_needed: int
entrance_locks: List[EntranceLock] = [
EntranceLock("Mines", "Coal Pits Holy Mountain", "Portal to Holy Mountain 1", 1),
EntranceLock("Coal Pits", "Snowy Depths Holy Mountain", "Portal to Holy Mountain 2", 2),
EntranceLock("Snowy Depths", "Hiisi Base Holy Mountain", "Portal to Holy Mountain 3", 3),
EntranceLock("Hiisi Base", "Underground Jungle Holy Mountain", "Portal to Holy Mountain 4", 4),
EntranceLock("Underground Jungle", "Vault Holy Mountain", "Portal to Holy Mountain 5", 5),
EntranceLock("The Vault", "Temple of the Art Holy Mountain", "Portal to Holy Mountain 6", 6),
EntranceLock("Temple of the Art", "Laboratory Holy Mountain", "Portal to Holy Mountain 7", 7),
]
holy_mountain_regions: List[str] = [
"Coal Pits Holy Mountain",
"Snowy Depths Holy Mountain",
"Hiisi Base Holy Mountain",
"Underground Jungle Holy Mountain",
"Vault Holy Mountain",
"Temple of the Art Holy Mountain",
"Laboratory Holy Mountain",
]
wand_tiers: List[str] = [
"Wand (Tier 1)", # Coal Pits
"Wand (Tier 2)", # Snowy Depths
"Wand (Tier 3)", # Hiisi Base
"Wand (Tier 4)", # Underground Jungle
"Wand (Tier 5)", # The Vault
"Wand (Tier 6)", # Temple of the Art
]
items_hidden_from_shops: List[str] = ["Gold (200)", "Gold (1000)", "Potion", "Random Potion", "Secret Potion",
"Chaos Die", "Greed Die", "Kammi", "Refreshing Gourd", "Sädekivi", "Broken Wand",
"Powder Pouch"]
perk_list: List[str] = list(filter(Items.item_is_perk, Items.item_table.keys()))
# ----------------
# Helper Functions
# ----------------
def has_perk_count(state: CollectionState, player: int, amount: int) -> bool:
return sum(state.count(perk, player) for perk in perk_list) >= amount
def has_orb_count(state: CollectionState, player: int, amount: int) -> bool:
return state.count("Orb", player) >= amount
def forbid_items_at_location(multiworld: MultiWorld, location_name: str, items: Set[str], player: int):
location = multiworld.get_location(location_name, player)
GenericRules.forbid_items_for_player(location, items, player)
# ----------------
# Rule Functions
# ----------------
# Prevent gold and potions from appearing as purchasable items in shops (because physics will destroy them)
def ban_items_from_shops(multiworld: MultiWorld, player: int) -> None:
for location_name in Locations.location_name_to_id.keys():
if "Shop Item" in location_name:
forbid_items_at_location(multiworld, location_name, items_hidden_from_shops, player)
# Prevent high tier wands from appearing in early Holy Mountain shops
def ban_early_high_tier_wands(multiworld: MultiWorld, player: int) -> None:
for i, region_name in enumerate(holy_mountain_regions):
wands_to_forbid = wand_tiers[i+1:]
locations_in_region = Locations.location_region_mapping[region_name].keys()
for location_name in locations_in_region:
forbid_items_at_location(multiworld, location_name, wands_to_forbid, player)
# Prevent high tier wands from appearing in the Secret shop
wands_to_forbid = wand_tiers[3:]
locations_in_region = Locations.location_region_mapping["Secret Shop"].keys()
for location_name in locations_in_region:
forbid_items_at_location(multiworld, location_name, wands_to_forbid, player)
def lock_holy_mountains_into_spheres(multiworld: MultiWorld, player: int) -> None:
for lock in entrance_locks:
location = multiworld.get_entrance(f"From {lock.source} To {lock.destination}", player)
GenericRules.set_rule(location, lambda state, evt=lock.event: state.has(evt, player))
def holy_mountain_unlock_conditions(multiworld: MultiWorld, player: int) -> None:
victory_condition = multiworld.victory_condition[player].value
for lock in entrance_locks:
location = multiworld.get_location(lock.event, player)
if victory_condition == VictoryCondition.option_greed_ending:
location.access_rule = lambda state, items_needed=lock.items_needed: (
has_perk_count(state, player, items_needed//2)
)
elif victory_condition == VictoryCondition.option_pure_ending:
location.access_rule = lambda state, items_needed=lock.items_needed: (
has_perk_count(state, player, items_needed//2) and
has_orb_count(state, player, items_needed)
)
elif victory_condition == VictoryCondition.option_peaceful_ending:
location.access_rule = lambda state, items_needed=lock.items_needed: (
has_perk_count(state, player, items_needed//2) and
has_orb_count(state, player, items_needed * 3)
)
def biome_unlock_conditions(multiworld: MultiWorld, player: int):
lukki_entrances = multiworld.get_region("Lukki Lair", player).entrances
magical_entrances = multiworld.get_region("Magical Temple", player).entrances
wizard_entrances = multiworld.get_region("Wizards' Den", player).entrances
for entrance in lukki_entrances:
entrance.access_rule = lambda state: state.has("Melee Immunity Perk", player) and\
state.has("All-Seeing Eye Perk", player)
for entrance in magical_entrances:
entrance.access_rule = lambda state: state.has("All-Seeing Eye Perk", player)
for entrance in wizard_entrances:
entrance.access_rule = lambda state: state.has("All-Seeing Eye Perk", player)
def victory_unlock_conditions(multiworld: MultiWorld, player: int) -> None:
victory_condition = multiworld.victory_condition[player].value
victory_location = multiworld.get_location("Victory", player)
if victory_condition == VictoryCondition.option_pure_ending:
victory_location.access_rule = lambda state: has_orb_count(state, player, 11)
elif victory_condition == VictoryCondition.option_peaceful_ending:
victory_location.access_rule = lambda state: has_orb_count(state, player, 33)
# ----------------
# Main Function
# ----------------
def create_all_rules(multiworld: MultiWorld, player: int) -> None:
if multiworld.players > 1:
ban_items_from_shops(multiworld, player)
ban_early_high_tier_wands(multiworld, player)
lock_holy_mountains_into_spheres(multiworld, player)
holy_mountain_unlock_conditions(multiworld, player)
biome_unlock_conditions(multiworld, player)
victory_unlock_conditions(multiworld, player)
# Prevent the Map perk (used to find Toveri) from being on Toveri (boss)
if multiworld.bosses_as_checks[player].value >= BossesAsChecks.option_all_bosses:
forbid_items_at_location(multiworld, "Toveri", {"Spatial Awareness Perk"}, player)

View File

@@ -1,6 +1,8 @@
from BaseClasses import Item, Tutorial
from worlds.AutoWorld import WebWorld, World
from . import Events, Items, Locations, Options, Regions, Rules
from typing import Dict, Any
from . import events, items, locations, regions, rules
from .options import NoitaOptions
class NoitaWeb(WebWorld):
@@ -24,13 +26,14 @@ class NoitaWorld(World):
"""
game = "Noita"
option_definitions = Options.noita_options
options: NoitaOptions
options_dataclass = NoitaOptions
item_name_to_id = Items.item_name_to_id
location_name_to_id = Locations.location_name_to_id
item_name_to_id = items.item_name_to_id
location_name_to_id = locations.location_name_to_id
item_name_groups = Items.item_name_groups
location_name_groups = Locations.location_name_groups
item_name_groups = items.item_name_groups
location_name_groups = locations.location_name_groups
data_version = 2
web = NoitaWeb()
@@ -40,21 +43,21 @@ class NoitaWorld(World):
raise Exception("Noita yaml's slot name has invalid character(s).")
# Returned items will be sent over to the client
def fill_slot_data(self):
return {name: getattr(self.multiworld, name)[self.player].value for name in self.option_definitions}
def fill_slot_data(self) -> Dict[str, Any]:
return self.options.as_dict("death_link", "victory_condition", "path_option", "hidden_chests",
"pedestal_checks", "orbs_as_checks", "bosses_as_checks", "extra_orbs", "shop_price")
def create_regions(self) -> None:
Regions.create_all_regions_and_connections(self.multiworld, self.player)
Events.create_all_events(self.multiworld, self.player)
regions.create_all_regions_and_connections(self)
def create_item(self, name: str) -> Item:
return Items.create_item(self.player, name)
return items.create_item(self.player, name)
def create_items(self) -> None:
Items.create_all_items(self.multiworld, self.player)
items.create_all_items(self)
def set_rules(self) -> None:
Rules.create_all_rules(self.multiworld, self.player)
rules.create_all_rules(self)
def get_filler_item_name(self) -> str:
return self.multiworld.random.choice(Items.filler_items)
return self.random.choice(items.filler_items)

43
worlds/noita/events.py Normal file
View File

@@ -0,0 +1,43 @@
from typing import Dict, TYPE_CHECKING
from BaseClasses import Item, ItemClassification, Location, Region
from . import items, locations
if TYPE_CHECKING:
from . import NoitaWorld
def create_event(player: int, name: str) -> Item:
return items.NoitaItem(name, ItemClassification.progression, None, player)
def create_location(player: int, name: str, region: Region) -> Location:
return locations.NoitaLocation(player, name, None, region)
def create_locked_location_event(player: int, region: Region, item: str) -> Location:
new_location = create_location(player, item, region)
new_location.place_locked_item(create_event(player, item))
region.locations.append(new_location)
return new_location
def create_all_events(world: "NoitaWorld", created_regions: Dict[str, Region]) -> None:
for region_name, event in event_locks.items():
region = created_regions[region_name]
create_locked_location_event(world.player, region, event)
world.multiworld.completion_condition[world.player] = lambda state: state.has("Victory", world.player)
# Maps region names to event names
event_locks: Dict[str, str] = {
"The Work": "Victory",
"Mines": "Portal to Holy Mountain 1",
"Coal Pits": "Portal to Holy Mountain 2",
"Snowy Depths": "Portal to Holy Mountain 3",
"Hiisi Base": "Portal to Holy Mountain 4",
"Underground Jungle": "Portal to Holy Mountain 5",
"The Vault": "Portal to Holy Mountain 6",
"Temple of the Art": "Portal to Holy Mountain 7",
}

View File

@@ -1,9 +1,14 @@
import itertools
from collections import Counter
from typing import Dict, List, NamedTuple, Set
from typing import Dict, List, NamedTuple, Set, TYPE_CHECKING
from BaseClasses import Item, ItemClassification, MultiWorld
from .Options import BossesAsChecks, VictoryCondition, ExtraOrbs
from BaseClasses import Item, ItemClassification
from .options import BossesAsChecks, VictoryCondition, ExtraOrbs
if TYPE_CHECKING:
from . import NoitaWorld
else:
NoitaWorld = object
class ItemData(NamedTuple):
@@ -44,39 +49,40 @@ def create_kantele(victory_condition: VictoryCondition) -> List[str]:
return ["Kantele"] if victory_condition.value >= VictoryCondition.option_pure_ending else []
def create_random_items(multiworld: MultiWorld, player: int, weights: Dict[str, int], count: int) -> List[str]:
def create_random_items(world: NoitaWorld, weights: Dict[str, int], count: int) -> List[str]:
filler_pool = weights.copy()
if multiworld.bad_effects[player].value == 0:
if not world.options.bad_effects:
del filler_pool["Trap"]
return multiworld.random.choices(population=list(filler_pool.keys()),
weights=list(filler_pool.values()),
k=count)
return world.random.choices(population=list(filler_pool.keys()),
weights=list(filler_pool.values()),
k=count)
def create_all_items(multiworld: MultiWorld, player: int) -> None:
locations_to_fill = len(multiworld.get_unfilled_locations(player))
def create_all_items(world: NoitaWorld) -> None:
player = world.player
locations_to_fill = len(world.multiworld.get_unfilled_locations(player))
itempool = (
create_fixed_item_pool()
+ create_orb_items(multiworld.victory_condition[player], multiworld.extra_orbs[player])
+ create_spatial_awareness_item(multiworld.bosses_as_checks[player])
+ create_kantele(multiworld.victory_condition[player])
+ create_orb_items(world.options.victory_condition, world.options.extra_orbs)
+ create_spatial_awareness_item(world.options.bosses_as_checks)
+ create_kantele(world.options.victory_condition)
)
# if there's not enough shop-allowed items in the pool, we can encounter gen issues
# 39 is the number of shop-valid items we need to guarantee
if len(itempool) < 39:
itempool += create_random_items(multiworld, player, shop_only_filler_weights, 39 - len(itempool))
itempool += create_random_items(world, shop_only_filler_weights, 39 - len(itempool))
# this is so that it passes tests and gens if you have minimal locations and only one player
if multiworld.players == 1:
for location in multiworld.get_unfilled_locations(player):
if world.multiworld.players == 1:
for location in world.multiworld.get_unfilled_locations(player):
if "Shop Item" in location.name:
location.item = create_item(player, itempool.pop())
locations_to_fill = len(multiworld.get_unfilled_locations(player))
locations_to_fill = len(world.multiworld.get_unfilled_locations(player))
itempool += create_random_items(multiworld, player, filler_weights, locations_to_fill - len(itempool))
multiworld.itempool += [create_item(player, name) for name in itempool]
itempool += create_random_items(world, filler_weights, locations_to_fill - len(itempool))
world.multiworld.itempool += [create_item(player, name) for name in itempool]
# 110000 - 110032

View File

@@ -201,11 +201,10 @@ location_region_mapping: Dict[str, Dict[str, LocationData]] = {
}
# Iterating the hidden chest and pedestal locations here to avoid clutter above
def generate_location_entries(locname: str, locinfo: LocationData) -> Dict[str, int]:
if locinfo.ltype in ["chest", "pedestal"]:
return {f"{locname} {i + 1}": locinfo.id + i for i in range(20)}
return {locname: locinfo.id}
def make_location_range(location_name: str, base_id: int, amt: int) -> Dict[str, int]:
if amt == 1:
return {location_name: base_id}
return {f"{location_name} {i+1}": base_id + i for i in range(amt)}
location_name_groups: Dict[str, Set[str]] = {"shop": set(), "orb": set(), "boss": set(), "chest": set(),
@@ -215,9 +214,11 @@ location_name_to_id: Dict[str, int] = {}
for location_group in location_region_mapping.values():
for locname, locinfo in location_group.items():
location_name_to_id.update(generate_location_entries(locname, locinfo))
if locinfo.ltype in ["chest", "pedestal"]:
for i in range(20):
location_name_groups[locinfo.ltype].add(f"{locname} {i + 1}")
else:
location_name_groups[locinfo.ltype].add(locname)
# Iterating the hidden chest and pedestal locations here to avoid clutter above
amount = 20 if locinfo.ltype in ["chest", "pedestal"] else 1
entries = make_location_range(locname, locinfo.id, amount)
location_name_to_id.update(entries)
location_name_groups[locinfo.ltype].update(entries.keys())
shop_locations = {name for name in location_name_to_id.keys() if "Shop Item" in name}

View File

@@ -1,5 +1,5 @@
from typing import Dict
from Options import AssembleOptions, Choice, DeathLink, DefaultOnToggle, Range, StartInventoryPool
from Options import Choice, DeathLink, DefaultOnToggle, Range, StartInventoryPool, PerGameCommonOptions
from dataclasses import dataclass
class PathOption(Choice):
@@ -99,16 +99,16 @@ class ShopPrice(Choice):
default = 100
noita_options: Dict[str, AssembleOptions] = {
"start_inventory_from_pool": StartInventoryPool,
"death_link": DeathLink,
"bad_effects": Traps,
"victory_condition": VictoryCondition,
"path_option": PathOption,
"hidden_chests": HiddenChests,
"pedestal_checks": PedestalChecks,
"orbs_as_checks": OrbsAsChecks,
"bosses_as_checks": BossesAsChecks,
"extra_orbs": ExtraOrbs,
"shop_price": ShopPrice,
}
@dataclass
class NoitaOptions(PerGameCommonOptions):
start_inventory_from_pool: StartInventoryPool
death_link: DeathLink
bad_effects: Traps
victory_condition: VictoryCondition
path_option: PathOption
hidden_chests: HiddenChests
pedestal_checks: PedestalChecks
orbs_as_checks: OrbsAsChecks
bosses_as_checks: BossesAsChecks
extra_orbs: ExtraOrbs
shop_price: ShopPrice

View File

@@ -1,48 +1,43 @@
# Regions are areas in your game that you travel to.
from typing import Dict, Set, List
from typing import Dict, List, TYPE_CHECKING
from BaseClasses import Entrance, MultiWorld, Region
from . import Locations
from BaseClasses import Entrance, Region
from . import locations
from .events import create_all_events
if TYPE_CHECKING:
from . import NoitaWorld
def add_location(player: int, loc_name: str, id: int, region: Region) -> None:
location = Locations.NoitaLocation(player, loc_name, id, region)
region.locations.append(location)
def add_locations(multiworld: MultiWorld, player: int, region: Region) -> None:
locations = Locations.location_region_mapping.get(region.name, {})
for location_name, location_data in locations.items():
def create_locations(world: "NoitaWorld", region: Region) -> None:
locs = locations.location_region_mapping.get(region.name, {})
for location_name, location_data in locs.items():
location_type = location_data.ltype
flag = location_data.flag
opt_orbs = multiworld.orbs_as_checks[player].value
opt_bosses = multiworld.bosses_as_checks[player].value
opt_paths = multiworld.path_option[player].value
opt_num_chests = multiworld.hidden_chests[player].value
opt_num_pedestals = multiworld.pedestal_checks[player].value
is_orb_allowed = location_type == "orb" and flag <= world.options.orbs_as_checks
is_boss_allowed = location_type == "boss" and flag <= world.options.bosses_as_checks
amount = 0
if flag == locations.LocationFlag.none or is_orb_allowed or is_boss_allowed:
amount = 1
elif location_type == "chest" and flag <= world.options.path_option:
amount = world.options.hidden_chests.value
elif location_type == "pedestal" and flag <= world.options.path_option:
amount = world.options.pedestal_checks.value
is_orb_allowed = location_type == "orb" and flag <= opt_orbs
is_boss_allowed = location_type == "boss" and flag <= opt_bosses
if flag == Locations.LocationFlag.none or is_orb_allowed or is_boss_allowed:
add_location(player, location_name, location_data.id, region)
elif location_type == "chest" and flag <= opt_paths:
for i in range(opt_num_chests):
add_location(player, f"{location_name} {i+1}", location_data.id + i, region)
elif location_type == "pedestal" and flag <= opt_paths:
for i in range(opt_num_pedestals):
add_location(player, f"{location_name} {i+1}", location_data.id + i, region)
region.add_locations(locations.make_location_range(location_name, location_data.id, amount),
locations.NoitaLocation)
# Creates a new Region with the locations found in `location_region_mapping` and adds them to the world.
def create_region(multiworld: MultiWorld, player: int, region_name: str) -> Region:
new_region = Region(region_name, player, multiworld)
add_locations(multiworld, player, new_region)
def create_region(world: "NoitaWorld", region_name: str) -> Region:
new_region = Region(region_name, world.player, world.multiworld)
create_locations(world, new_region)
return new_region
def create_regions(multiworld: MultiWorld, player: int) -> Dict[str, Region]:
return {name: create_region(multiworld, player, name) for name in noita_regions}
def create_regions(world: "NoitaWorld") -> Dict[str, Region]:
return {name: create_region(world, name) for name in noita_regions}
# An "Entrance" is really just a connection between two regions
@@ -60,11 +55,12 @@ def create_connections(player: int, regions: Dict[str, Region]) -> None:
# Creates all regions and connections. Called from NoitaWorld.
def create_all_regions_and_connections(multiworld: MultiWorld, player: int) -> None:
created_regions = create_regions(multiworld, player)
create_connections(player, created_regions)
def create_all_regions_and_connections(world: "NoitaWorld") -> None:
created_regions = create_regions(world)
create_connections(world.player, created_regions)
create_all_events(world, created_regions)
multiworld.regions += created_regions.values()
world.multiworld.regions += created_regions.values()
# Oh, what a tangled web we weave

172
worlds/noita/rules.py Normal file
View File

@@ -0,0 +1,172 @@
from typing import List, NamedTuple, Set, TYPE_CHECKING
from BaseClasses import CollectionState
from . import items, locations
from .options import BossesAsChecks, VictoryCondition
from worlds.generic import Rules as GenericRules
if TYPE_CHECKING:
from . import NoitaWorld
class EntranceLock(NamedTuple):
source: str
destination: str
event: str
items_needed: int
entrance_locks: List[EntranceLock] = [
EntranceLock("Mines", "Coal Pits Holy Mountain", "Portal to Holy Mountain 1", 1),
EntranceLock("Coal Pits", "Snowy Depths Holy Mountain", "Portal to Holy Mountain 2", 2),
EntranceLock("Snowy Depths", "Hiisi Base Holy Mountain", "Portal to Holy Mountain 3", 3),
EntranceLock("Hiisi Base", "Underground Jungle Holy Mountain", "Portal to Holy Mountain 4", 4),
EntranceLock("Underground Jungle", "Vault Holy Mountain", "Portal to Holy Mountain 5", 5),
EntranceLock("The Vault", "Temple of the Art Holy Mountain", "Portal to Holy Mountain 6", 6),
EntranceLock("Temple of the Art", "Laboratory Holy Mountain", "Portal to Holy Mountain 7", 7),
]
holy_mountain_regions: List[str] = [
"Coal Pits Holy Mountain",
"Snowy Depths Holy Mountain",
"Hiisi Base Holy Mountain",
"Underground Jungle Holy Mountain",
"Vault Holy Mountain",
"Temple of the Art Holy Mountain",
"Laboratory Holy Mountain",
]
wand_tiers: List[str] = [
"Wand (Tier 1)", # Coal Pits
"Wand (Tier 2)", # Snowy Depths
"Wand (Tier 3)", # Hiisi Base
"Wand (Tier 4)", # Underground Jungle
"Wand (Tier 5)", # The Vault
"Wand (Tier 6)", # Temple of the Art
]
items_hidden_from_shops: Set[str] = {"Gold (200)", "Gold (1000)", "Potion", "Random Potion", "Secret Potion",
"Chaos Die", "Greed Die", "Kammi", "Refreshing Gourd", "Sädekivi", "Broken Wand",
"Powder Pouch"}
perk_list: List[str] = list(filter(items.item_is_perk, items.item_table.keys()))
# ----------------
# Helper Functions
# ----------------
def has_perk_count(state: CollectionState, player: int, amount: int) -> bool:
return sum(state.count(perk, player) for perk in perk_list) >= amount
def has_orb_count(state: CollectionState, player: int, amount: int) -> bool:
return state.count("Orb", player) >= amount
def forbid_items_at_locations(world: "NoitaWorld", shop_locations: Set[str], forbidden_items: Set[str]):
for shop_location in shop_locations:
location = world.multiworld.get_location(shop_location, world.player)
GenericRules.forbid_items_for_player(location, forbidden_items, world.player)
# ----------------
# Rule Functions
# ----------------
# Prevent gold and potions from appearing as purchasable items in shops (because physics will destroy them)
# def ban_items_from_shops(world: "NoitaWorld") -> None:
# for location_name in Locations.location_name_to_id.keys():
# if "Shop Item" in location_name:
# forbid_items_at_location(world, location_name, items_hidden_from_shops)
def ban_items_from_shops(world: "NoitaWorld") -> None:
forbid_items_at_locations(world, locations.shop_locations, items_hidden_from_shops)
# Prevent high tier wands from appearing in early Holy Mountain shops
def ban_early_high_tier_wands(world: "NoitaWorld") -> None:
for i, region_name in enumerate(holy_mountain_regions):
wands_to_forbid = set(wand_tiers[i+1:])
locations_in_region = set(locations.location_region_mapping[region_name].keys())
forbid_items_at_locations(world, locations_in_region, wands_to_forbid)
# Prevent high tier wands from appearing in the Secret shop
wands_to_forbid = set(wand_tiers[3:])
locations_in_region = set(locations.location_region_mapping["Secret Shop"].keys())
forbid_items_at_locations(world, locations_in_region, wands_to_forbid)
def lock_holy_mountains_into_spheres(world: "NoitaWorld") -> None:
for lock in entrance_locks:
location = world.multiworld.get_entrance(f"From {lock.source} To {lock.destination}", world.player)
GenericRules.set_rule(location, lambda state, evt=lock.event: state.has(evt, world.player))
def holy_mountain_unlock_conditions(world: "NoitaWorld") -> None:
victory_condition = world.options.victory_condition.value
for lock in entrance_locks:
location = world.multiworld.get_location(lock.event, world.player)
if victory_condition == VictoryCondition.option_greed_ending:
location.access_rule = lambda state, items_needed=lock.items_needed: (
has_perk_count(state, world.player, items_needed//2)
)
elif victory_condition == VictoryCondition.option_pure_ending:
location.access_rule = lambda state, items_needed=lock.items_needed: (
has_perk_count(state, world.player, items_needed//2) and
has_orb_count(state, world.player, items_needed)
)
elif victory_condition == VictoryCondition.option_peaceful_ending:
location.access_rule = lambda state, items_needed=lock.items_needed: (
has_perk_count(state, world.player, items_needed//2) and
has_orb_count(state, world.player, items_needed * 3)
)
def biome_unlock_conditions(world: "NoitaWorld"):
lukki_entrances = world.multiworld.get_region("Lukki Lair", world.player).entrances
magical_entrances = world.multiworld.get_region("Magical Temple", world.player).entrances
wizard_entrances = world.multiworld.get_region("Wizards' Den", world.player).entrances
for entrance in lukki_entrances:
entrance.access_rule = lambda state: state.has("Melee Immunity Perk", world.player) and\
state.has("All-Seeing Eye Perk", world.player)
for entrance in magical_entrances:
entrance.access_rule = lambda state: state.has("All-Seeing Eye Perk", world.player)
for entrance in wizard_entrances:
entrance.access_rule = lambda state: state.has("All-Seeing Eye Perk", world.player)
def victory_unlock_conditions(world: "NoitaWorld") -> None:
victory_condition = world.options.victory_condition.value
victory_location = world.multiworld.get_location("Victory", world.player)
if victory_condition == VictoryCondition.option_pure_ending:
victory_location.access_rule = lambda state: has_orb_count(state, world.player, 11)
elif victory_condition == VictoryCondition.option_peaceful_ending:
victory_location.access_rule = lambda state: has_orb_count(state, world.player, 33)
# ----------------
# Main Function
# ----------------
def create_all_rules(world: "NoitaWorld") -> None:
if world.multiworld.players > 1:
ban_items_from_shops(world)
ban_early_high_tier_wands(world)
lock_holy_mountains_into_spheres(world)
holy_mountain_unlock_conditions(world)
biome_unlock_conditions(world)
victory_unlock_conditions(world)
# Prevent the Map perk (used to find Toveri) from being on Toveri (boss)
if world.options.bosses_as_checks.value >= BossesAsChecks.option_all_bosses:
toveri = world.multiworld.get_location("Toveri", world.player)
GenericRules.forbid_items_for_player(toveri, {"Spatial Awareness Perk"}, world.player)

View File

@@ -1271,7 +1271,7 @@ class LogicTricks(OptionList):
https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/oot/LogicTricks.py
"""
display_name = "Logic Tricks"
valid_keys = frozenset(normalized_name_tricks)
valid_keys = tuple(normalized_name_tricks.keys())
valid_keys_casefold = True

View File

@@ -118,7 +118,16 @@ class OOTWeb(WebWorld):
["TheLynk"]
)
tutorials = [setup, setup_es, setup_fr]
setup_de = Tutorial(
setup.tutorial_name,
setup.description,
"Deutsch",
"setup_de.md",
"setup/de",
["Held_der_Zeit"]
)
tutorials = [setup, setup_es, setup_fr, setup_de]
class OOTWorld(World):

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@@ -0,0 +1,41 @@
# The Legend of Zelda: Ocarina of Time
## Wo ist die Seite für die Einstellungen?
Die [Seite für die Spielereinstellungen dieses Spiels](../player-options) enthält alle Optionen die man benötigt um
eine YAML-Datei zu konfigurieren und zu exportieren.
## Was macht der Randomizer in diesem Spiel?
Items, welche der Spieler für gewöhnlich im Verlauf des Spiels erhalten würde, wurden umhergemischt. Die Logik bleit
bestehen, damit ist das Spiel immer durchspielbar. Doch weil die Items durch das ganze Spiel gemischt wurden, müssen
manche Bereiche früher bescuht werden, als man es in Vanilla tun würde.
Eine Liste von implementierter Logik, die unoffensichtlich erscheinen kann, kann
[hier (Englisch)](https://wiki.ootrandomizer.com/index.php?title=Logic) gefunden werden.
## Welche Items und Bereiche werden gemischt?
Alle ausrüstbare und sammelbare Gegenstände, sowie Munition können gemischt werden. Und alle Bereiche, die einen
dieser Items enthalten könnten, haben (sehr wahrscheinlich) ihren Inhalt verändert. Goldene Skulltulas können ebenfalls
dazugezählt werden, je nach Wunsch des Spielers.
## Welche Items können in sich in der Welt eines anderen Spielers befinden?
Jedes dieser Items, die gemicht werden können, können in einer Multiworld auch in der Welt eines anderen Spielers
fallen. Es ist jedoch möglich ausgewählte Items auf deine eigene Welt zu beschränken.
## Wie sieht ein Item einer anderen Welt in OoT aus?
Items, die zu einer anderen Welt gehören, werden repräsentiert durch Zelda's Brief.
## Was passiert, wenn der Spieler ein Item erhält?
Sobald der Spieler ein Item erhält, wird Link das Item über seinen Kopf halten und der ganzen Welt präsentieren.
Gut für's Geschäft!
## Einzigartige Lokale Befehle
Die folgenden Befehle stehen nur im OoTClient, um mit Archipelago zu spielen, zur Verfügung:
- `/n64` Überprüffe den Verbindungsstatus deiner N64
- `/deathlink` Schalte den "Deathlink" des Clients um. Überschreibt die zuvor konfigurierten Einstellungen.

108
worlds/oot/docs/setup_de.md Normal file
View File

@@ -0,0 +1,108 @@
# Setup Anleitung für Ocarina of Time: Archipelago Edition
## WICHTIG
Da wir BizHawk benutzen, gilt diese Anleitung nur für Windows und Linux.
## Benötigte Software
- BizHawk: [BizHawk Veröffentlichungen von TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
- Version 2.3.1 und später werden unterstützt. Version 2.9 ist empfohlen.
- Detailierte Installtionsanweisungen für BizHawk können über den obrigen Link gefunden werden.
- Windows-Benutzer müssen die Prerequisiten installiert haben. Diese können ebenfalls über
den obrigen Link gefunden werden.
- Der integrierte Archipelago-Client, welcher [hier](https://github.com/ArchipelagoMW/Archipelago/releases) installiert
werden kann.
- Eine `Ocarina of Time v1.0 US(?) ROM`. (Nicht aus Europa und keine Master-Quest oder Debug-Rom!)
## Konfigurieren von BizHawk
Sobald Bizhawk einmal installiert wurde, öffne **EmuHawk** und ändere die folgenen Einsteluungen:
- (≤ 2.8) Gehe zu `Config > Customize`. Wechlse zu dem `Advanced`-Reiter, wechsle dann den `Lua Core` von "NLua+KopiLua" zu
`"Lua+LuaInterface"`. Starte danach EmuHawk neu. Dies ist zwingend notwendig, damit die Lua-Scripts, mit denen man sich mit dem Client verbindet, ordnungsgemäß funktionieren.
**ANMERKUNG: Selbst wenn "Lua+LuaInterface" bereits ausgewählt ist, wechsle zwischen den beiden Optionen umher und**
**wähle es erneut aus. Neue Installationen oder Versionen von EmuHawk neigen dazu "Lua+LuaInterface" als die**
**Standard-Option anzuzeigen, aber laden dennoch "NLua+KopiLua", bis dieser Schritt getan ist.**
- Unter `Config > Customize > Advanced`, gehe sicher dass der Haken bei `AutoSaveRAM` ausgeählt ist, und klicke dann
den 5s-Knopf. Dies verringert die Wahrscheinlichkeit den Speicherfrotschritt zu verlieren, sollte der Emulator mal
abstürzen.
- **(Optional)** Unter `Config > Customize` kannst du die Haken in den "Run in background"
(Laufe weiter im Hintergrund) und "Accept background input" (akzeptiere Tastendruck im Hintergrund) Kästchen setzen.
Dies erlaubt dir das Spiel im Hintergrund weiter zu spielen, selbst wenn ein anderes Fenster aktiv ist. (Nützlich bei
mehreren oder eher großen Bildschrimen/Monitoren.)
- Unter `Config > Hotkeys` sind viele Hotkeys, die mit oft genuten Tasten belegt worden sind. Es wird empfohlen die
meisten (oder alle) Hotkeys zu deaktivieren. Dies kann schnell mit `Esc` erledigt werden.
- Wird mit einem Kontroller gespielt, bei der Tastenbelegung (bei einem Laufendem Spiel, unter
`Config > Controllers...`), deaktiviere "P1 A Up", "P1 A Down", "P1 A Left", and "P1 A Right" und gehe stattdessen in
den Reiter `Analog Controls` um den Stick zu belegen, da sonst Probleme beim Zielen auftreten (mit dem Bogen oder
ähnliches). Y-Axis ist für Oben und Unten, und die X-Axis ist für Links und Rechts.
- Unter `N64` setze einen Haken bei "Use Expansion Slot" (Benutze Erweiterungs-Slot). Dies wird benötigt damit
savestates/schnellspeichern funktioniert. (Das N64-Menü taucht nur **nach** dem laden einer N64-ROM auf.)
Es wird sehr empfohlen N64 Rom-Erweiterungen (\*.n64, \*.z64) mit dem Emuhawk - welcher zuvor installiert wurde - zu
verknüpfen.
Um dies zu tun, muss eine beliebige N64 Rom aufgefunden werden, welche in deinem Besitz ist, diese Rechtsklicken und
dann auf "Öffnen mit..." gehen. Gehe dann auf "Andere App auswählen" und suche nach deinen BizHawk-Ordner, in der
sich der Emulator befindet, und wähle dann `EmuHawk.exe` **(NICHT "DiscoHawk.exe"!)** aus.
Eine Alternative BizHawk Setup Anleitung (auf Englisch), sowie weitere Hilfe bei Problemen kann
[hier](https://wiki.ootrandomizer.com/index.php?title=Bizhawk) gefunden werden.
## Erstelle eine YAML-Datei
### Was ist eine YAML-Datei und Warum brauch ich eine?
Eine YAML-Datie enthält einen Satz an einstellbaren Optionen, die dem Generator mitteilen, wie
dein Spiel generiert werden soll. In einer Multiworld stellt jeder Spieler eine eigene YAML-Datei zur Verfügung. Dies
erlaubt jeden Spieler eine personalisierte Erfahrung nach derem Geschmack. Damit kann auch jeder Spieler in einer
Multiworld (des gleichen Spiels) völlig unterschiedliche Einstellungen haben.
Für weitere Informationen, besuche die allgemeine Anleitung zum Erstellen einer
YAML-Datei: [Archipelago Setup Anleitung](/tutorial/Archipelago/setup/en)
### Woher bekomme ich eine YAML-Datei?
Die Seite für die Spielereinstellungen auf dieser Website erlaubt es dir deine persönlichen Einstellungen nach
vorlieben zu konfigurieren und eine YAML-Datei zu exportieren.
Seite für die Spielereinstellungen:
[Seite für die Spielereinstellungen von Ocarina of Time](/games/Ocarina%20of%20Time/player-options)
### Überprüfen deiner YAML-Datei
Wenn du deine YAML-Datei überprüfen möchtest, um sicher zu gehen, dass sie funktioniert, kannst du dies auf der
YAML-Überprüfungsseite tun.
YAML-Überprüfungsseite: [YAML-Überprüfungsseite](/check)
## Beitreten einer Multiworld
### Erhalte deinen OoT-Patch
(Der folgende Prozess ist bei den meisten ROM-basierenden Spielen sehr ähnlich.)
Wenn du einer Multiworld beitrittst, wirst du gefordert eine YAML-Datei bei dem Host abzugeben. Ist dies getan,
erhälst du (in der Regel) einen Link vom Host der Multiworld. Dieser führt dich zu einem Raum, in dem alle
teilnehmenden Spieler (bzw. Welten) aufgelistet sind. Du solltest dich dann auf **deine** Welt konzentrieren
und klicke dann auf `Download APZ5 File...`.
![Screenshot of a Multiworld Room with an Ocarina of Time Player](/static/generated/docs/Ocarina%20of%20Time/MultiWorld-room_oot.png)
Führe die `.apz5`-Datei mit einem Doppelklick aus, um deinen Ocarina Of Time-Client zu starten, sowie das patchen
deiner ROM. Ist dieser Prozess fertig (kann etwas dauern), startet sich der Client und der Emulator automatisch
(sofern das "Öffnen mit..." ausgewählt wurde).
### Verbinde zum Multiserver
Sind einmal der Client und der Emulator gestartet, müssen sie nur noch miteinander verbunden werden. Gehe dazu in
deinen Archipelago-Ordner, dann zu `data/lua`, und füge das `connector_oot.lua` Script per Drag&Drop (ziehen und
fallen lassen) auf das EmuHawk-Fenster. (Alternativ kannst du die Lua-Konsole manuell öffnen, gehe dazu auf
`Script > Open Script` und durchsuche die Ordner nach `data/lua/connector_oot.lua`.)
Um den Client mit dem Multiserver zu verbinden, füge einfach `<Adresse>:<Port>` in das Textfeld ganz oben im
Client ein und drücke Enter oder "Connect" (verbinden). Wird ein Passwort benötigt, musst du es danach unten in das
Textfeld (für den Chat und Befehle) eingeben.
Alternativ kannst du auch in dem unterem Textfeld den folgenden Befehl schreiben:
`/connect <Adresse>:<Port> [Passwort]` (wie die Adresse und der Port lautet steht in dem Raum, oder wird von deinem
Host an dich weitergegeben.)
Beispiel: `/connect archipelago.gg:12345 Passw123`
Du bist nun bereit für dein Zeitreise-Abenteuer in Hyrule.

View File

@@ -7,7 +7,7 @@ import logging
import os
from typing import Any, Set, List, Dict, Optional, Tuple, ClassVar
from BaseClasses import ItemClassification, MultiWorld, Tutorial
from BaseClasses import ItemClassification, MultiWorld, Tutorial, LocationProgressType
from Fill import FillError, fill_restrictive
from Options import Toggle
import settings
@@ -20,7 +20,7 @@ from .items import (ITEM_GROUPS, PokemonEmeraldItem, create_item_label_to_code_m
offset_item_value)
from .locations import (LOCATION_GROUPS, PokemonEmeraldLocation, create_location_label_to_id_map,
create_locations_with_tags)
from .options import (ItemPoolType, RandomizeWildPokemon, RandomizeBadges, RandomizeTrainerParties, RandomizeHms,
from .options import (Goal, ItemPoolType, RandomizeWildPokemon, RandomizeBadges, RandomizeTrainerParties, RandomizeHms,
RandomizeStarters, LevelUpMoves, RandomizeAbilities, RandomizeTypes, TmCompatibility,
HmCompatibility, RandomizeStaticEncounters, NormanRequirement, PokemonEmeraldOptions)
from .pokemon import get_random_species, get_random_move, get_random_damaging_move, get_random_type
@@ -146,6 +146,60 @@ class PokemonEmeraldWorld(World):
self.multiworld.regions.extend(regions.values())
# Exclude locations which are always locked behind the player's goal
def exclude_locations(location_names: List[str]):
for location_name in location_names:
try:
self.multiworld.get_location(location_name,
self.player).progress_type = LocationProgressType.EXCLUDED
except KeyError:
continue # Location not in multiworld
if self.options.goal == Goal.option_champion:
# Always required to beat champion before receiving this
exclude_locations([
"Littleroot Town - S.S. Ticket from Norman"
])
# S.S. Ticket requires beating champion, so ferry is not accessible until after goal
if not self.options.enable_ferry:
exclude_locations([
"SS Tidal - Hidden Item in Lower Deck Trash Can",
"SS Tidal - TM49 from Thief"
])
# Construction workers don't move until champion is defeated
if "Safari Zone Construction Workers" not in self.options.remove_roadblocks.value:
exclude_locations([
"Safari Zone NE - Hidden Item North",
"Safari Zone NE - Hidden Item East",
"Safari Zone NE - Item on Ledge",
"Safari Zone SE - Hidden Item in South Grass 1",
"Safari Zone SE - Hidden Item in South Grass 2",
"Safari Zone SE - Item in Grass"
])
elif self.options.goal == Goal.option_norman:
# If the player sets their options such that Surf or the Balance
# Badge is vanilla, a very large number of locations become
# "post-Norman". Similarly, access to the E4 may require you to
# defeat Norman as an event or to get his badge, making postgame
# locations inaccessible. Detecting these situations isn't trivial
# and excluding all locations requiring Surf would be a bad idea.
# So for now we just won't touch it and blame the user for
# constructing their options in this way. Players usually expect
# to only partially complete their world when playing this goal
# anyway.
# Locations which are directly unlocked by defeating Norman.
exclude_locations([
"Petalburg Gym - Balance Badge",
"Petalburg Gym - TM42 from Norman",
"Petalburg City - HM03 from Wally's Uncle",
"Dewford Town - TM36 from Sludge Bomb Man",
"Mauville City - Basement Key from Wattson",
"Mauville City - TM24 from Wattson"
])
def create_items(self) -> None:
item_locations: List[PokemonEmeraldLocation] = [
location

View File

@@ -353,7 +353,9 @@ class PokemonRedBlueWorld(World):
location.show_in_spoiler = False
def intervene(move, test_state):
if self.multiworld.randomize_wild_pokemon[self.player]:
move_bit = pow(2, poke_data.hm_moves.index(move) + 2)
viable_mons = [mon for mon in self.local_poke_data if self.local_poke_data[mon]["tms"][6] & move_bit]
if self.multiworld.randomize_wild_pokemon[self.player] and viable_mons:
accessible_slots = [loc for loc in self.multiworld.get_reachable_locations(test_state, self.player) if
loc.type == "Wild Encounter"]
@@ -363,8 +365,6 @@ class PokemonRedBlueWorld(World):
zones.add(loc.name.split(" - ")[0])
return len(zones)
move_bit = pow(2, poke_data.hm_moves.index(move) + 2)
viable_mons = [mon for mon in self.local_poke_data if self.local_poke_data[mon]["tms"][6] & move_bit]
placed_mons = [slot.item.name for slot in accessible_slots]
if self.multiworld.area_1_to_1_mapping[self.player]:

View File

@@ -3,7 +3,6 @@ from __future__ import annotations
import asyncio
import copy
import ctypes
import json
import logging
import multiprocessing
import os.path
@@ -15,6 +14,7 @@ import queue
import zipfile
import io
import random
import concurrent.futures
from pathlib import Path
# CommonClient import first to trigger ModuleUpdater
@@ -42,6 +42,7 @@ import colorama
from NetUtils import ClientStatus, NetworkItem, RawJSONtoTextParser, JSONtoTextParser, JSONMessagePart
from MultiServer import mark_raw
pool = concurrent.futures.ThreadPoolExecutor(1)
loop = asyncio.get_event_loop_policy().new_event_loop()
nest_asyncio.apply(loop)
max_bonus: int = 13
@@ -210,6 +211,11 @@ class StarcraftClientProcessor(ClientCommandProcessor):
def _cmd_download_data(self) -> bool:
"""Download the most recent release of the necessary files for playing SC2 with
Archipelago. Will overwrite existing files."""
pool.submit(self._download_data)
return True
@staticmethod
def _download_data() -> bool:
if "SC2PATH" not in os.environ:
check_game_install_path()
@@ -220,7 +226,7 @@ class StarcraftClientProcessor(ClientCommandProcessor):
metadata = None
tempzip, metadata = download_latest_release_zip(DATA_REPO_OWNER, DATA_REPO_NAME, DATA_API_VERSION,
metadata=metadata, force_download=True)
metadata=metadata, force_download=True)
if tempzip != '':
try:

View File

@@ -151,14 +151,14 @@ def get_rules_lookup(player: int):
"Puzzle Solved Maze Door": lambda state: state.can_reach("Projector Room", "Region", player),
"Puzzle Solved Theater Door": lambda state: state.can_reach("Underground Lake", "Region", player),
"Puzzle Solved Columns of RA": lambda state: state.can_reach("Underground Lake", "Region", player),
"Final Riddle: Guillotine Dropped": lambda state: state.can_reach("Underground Lake", "Region", player)
"Final Riddle: Guillotine Dropped": lambda state: (beths_body_available(state, player) and state.can_reach("Underground Lake", "Region", player))
},
"elevators": {
"Puzzle Solved Underground Elevator": lambda state: ((state.can_reach("Underground Lake", "Region", player) or state.can_reach("Office", "Region", player)
and state.has("Key for Office Elevator", player))),
"Puzzle Solved Office Elevator": lambda state: ((state.can_reach("Underground Lake", "Region", player) or state.can_reach("Office", "Region", player))
and state.has("Key for Office Elevator", player)),
"Puzzle Solved Bedroom Elevator": lambda state: (state.can_reach("Office", "Region", player) and state.has_all({"Key for Bedroom Elevator","Crawling"}, player)),
"Puzzle Solved Three Floor Elevator": lambda state: (((state.can_reach("Maintenance Tunnels", "Region", player) or state.can_reach("Blue Maze", "Region", player))
and state.has("Key for Three Floor Elevator", player)))
"Puzzle Solved Three Floor Elevator": lambda state: ((state.can_reach("Maintenance Tunnels", "Region", player) or state.can_reach("Blue Maze", "Region", player))
and state.has("Key for Three Floor Elevator", player))
},
"lightning": {
"Ixupi Captured Lightning": lambda state: lightning_capturable(state, player)

View File

@@ -42,7 +42,7 @@
"Information Plaque: Aliens (UFO)"
],
"elevators": [
"Puzzle Solved Underground Elevator",
"Puzzle Solved Office Elevator",
"Puzzle Solved Bedroom Elevator",
"Puzzle Solved Three Floor Elevator"
],

View File

@@ -110,7 +110,7 @@
"Information Plaque: Astronomical Construction (UFO)",
"Information Plaque: Guillotine (Torture)",
"Information Plaque: Aliens (UFO)",
"Puzzle Solved Underground Elevator",
"Puzzle Solved Office Elevator",
"Puzzle Solved Bedroom Elevator",
"Puzzle Solved Three Floor Elevator",
"Ixupi Captured Lightning"
@@ -129,7 +129,7 @@
"Ixupi Captured Sand",
"Ixupi Captured Metal",
"Ixupi Captured Lightning",
"Puzzle Solved Underground Elevator",
"Puzzle Solved Office Elevator",
"Puzzle Solved Three Floor Elevator",
"Puzzle Hint Found: Combo Lock in Mailbox",
"Puzzle Hint Found: Orange Symbol",

View File

@@ -40,6 +40,7 @@ class SMZ3SNIClient(SNIClient):
if rom_name is None or rom_name == bytes([0] * ROMNAME_SIZE) or rom_name[:3] != b"ZSM":
return False
ctx.smz3_new_message_queue = rom_name[7] in b"1234567890"
ctx.game = self.game
ctx.items_handling = 0b101 # local items and remote start inventory
@@ -53,6 +54,22 @@ class SMZ3SNIClient(SNIClient):
if ctx.server is None or ctx.slot is None:
# not successfully connected to a multiworld server, cannot process the game sending items
return
send_progress_addr_ptr_offset = 0x680
send_progress_size = 8
send_progress_message_byte_offset = 4
send_progress_addr_table_offset = 0x700
recv_progress_addr_ptr_offset = 0x600
recv_progress_size = 4
recv_progress_addr_table_offset = 0x602
if ctx.smz3_new_message_queue:
send_progress_addr_ptr_offset = 0xD3C
send_progress_size = 2
send_progress_message_byte_offset = 0
send_progress_addr_table_offset = 0xDA0
recv_progress_addr_ptr_offset = 0xD36
recv_progress_size = 2
recv_progress_addr_table_offset = 0xD38
currentGame = await snes_read(ctx, SRAM_START + 0x33FE, 2)
if (currentGame is not None):
@@ -69,7 +86,7 @@ class SMZ3SNIClient(SNIClient):
ctx.finished_game = True
return
data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD3C, 4)
data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + send_progress_addr_ptr_offset, 4)
if data is None:
return
@@ -77,14 +94,14 @@ class SMZ3SNIClient(SNIClient):
recv_item = data[2] | (data[3] << 8)
while (recv_index < recv_item):
item_address = recv_index * 2
message = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xDA0 + item_address, 2)
is_z3_item = ((message[1] & 0x80) != 0)
masked_part = (message[1] & 0x7F) if is_z3_item else message[1]
item_index = ((message[0] | (masked_part << 8)) >> 3) + (256 if is_z3_item else 0)
item_address = recv_index * send_progress_size
message = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + send_progress_addr_table_offset + item_address, send_progress_size)
is_z3_item = ((message[send_progress_message_byte_offset+1] & 0x80) != 0)
masked_part = (message[send_progress_message_byte_offset+1] & 0x7F) if is_z3_item else message[send_progress_message_byte_offset+1]
item_index = ((message[send_progress_message_byte_offset] | (masked_part << 8)) >> 3) + (256 if is_z3_item else 0)
recv_index += 1
snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD3C, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + send_progress_addr_ptr_offset, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
from .TotalSMZ3.Location import locations_start_id
from . import convertLocSMZ3IDToAPID
@@ -95,7 +112,7 @@ class SMZ3SNIClient(SNIClient):
snes_logger.info(f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}])
data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD36, 4)
data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + recv_progress_addr_ptr_offset, 4)
if data is None:
return
@@ -107,9 +124,12 @@ class SMZ3SNIClient(SNIClient):
item_id = item.item - items_start_id
player_id = item.player if item.player < SMZ3_ROM_PLAYER_LIMIT else 0
snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + item_out_ptr * 2, bytes([player_id, item_id]))
snes_buffered_write(ctx,
SMZ3_RECV_PROGRESS_ADDR + item_out_ptr * recv_progress_size,
bytes([player_id, item_id]) if ctx.smz3_new_message_queue else
bytes([player_id & 0xFF, (player_id >> 8) & 0xFF, item_id & 0xFF, (item_id >> 8) & 0xFF]))
item_out_ptr += 1
snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD38, bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF]))
snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + recv_progress_addr_table_offset, bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF]))
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
color(ctx.item_names[item.item], 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
ctx.location_names[item.location], item_out_ptr, len(ctx.items_received)))

View File

@@ -616,7 +616,8 @@ class Patch:
"H" if self.myWorld.Config.SMLogic == Config.SMLogic.Hard else \
"X"
self.title = f"ZSM{Patch.Major}{Patch.Minor}{Patch.Patch}{z3Glitch}{smGlitch}{self.myWorld.Id}{self.seed:08x}".ljust(21)[:21]
from Utils import __version__
self.title = f"ZSM{Patch.Major}{Patch.Minor}{Patch.Patch}{__version__.replace('.', '')[0:3]}{z3Glitch}{smGlitch}{self.myWorld.Id}{self.seed:08x}".ljust(21)[:21]
self.patches.append((Snes(0x00FFC0), bytearray(self.title, 'utf8')))
self.patches.append((Snes(0x80FFC0), bytearray(self.title, 'utf8')))

View File

@@ -1,70 +0,0 @@
from typing import Protocol, Set
from BaseClasses import MultiWorld
from worlds.AutoWorld import LogicMixin
from . import pyevermizer
from .Options import EnergyCore, OutOfBounds, SequenceBreaks
# TODO: Options may preset certain progress steps (i.e. P_ROCK_SKIP), set in generate_early?
# TODO: resolve/flatten/expand rules to get rid of recursion below where possible
# Logic.rules are all rules including locations, excluding those with no progress (i.e. locations that only drop items)
rules = [rule for rule in pyevermizer.get_logic() if len(rule.provides) > 0]
# Logic.items are all items and extra items excluding non-progression items and duplicates
item_names: Set[str] = set()
items = [item for item in filter(lambda item: item.progression, pyevermizer.get_items() + pyevermizer.get_extra_items())
if item.name not in item_names and not item_names.add(item.name)]
class LogicProtocol(Protocol):
def has(self, name: str, player: int) -> bool: ...
def count(self, name: str, player: int) -> int: ...
def soe_has(self, progress: int, world: MultiWorld, player: int, count: int) -> bool: ...
def _soe_count(self, progress: int, world: MultiWorld, player: int, max_count: int) -> int: ...
# when this module is loaded, this mixin will extend BaseClasses.CollectionState
class SecretOfEvermoreLogic(LogicMixin):
def _soe_count(self: LogicProtocol, progress: int, world: MultiWorld, player: int, max_count: int = 0) -> int:
"""
Returns reached count of one of evermizer's progress steps based on collected items.
i.e. returns 0-3 for P_DE based on items providing CHECK_BOSS,DIAMOND_EYE_DROP
"""
n = 0
for item in items:
for pvd in item.provides:
if pvd[1] == progress:
if self.has(item.name, player):
n += self.count(item.name, player) * pvd[0]
if n >= max_count > 0:
return n
for rule in rules:
for pvd in rule.provides:
if pvd[1] == progress and pvd[0] > 0:
has = True
for req in rule.requires:
if not self.soe_has(req[1], world, player, req[0]):
has = False
break
if has:
n += pvd[0]
if n >= max_count > 0:
return n
return n
def soe_has(self: LogicProtocol, progress: int, world: MultiWorld, player: int, count: int = 1) -> bool:
"""
Returns True if count of one of evermizer's progress steps is reached based on collected items. i.e. 2 * P_DE
"""
if progress == pyevermizer.P_ENERGY_CORE: # logic is shared between worlds, so we override in the call
w = world.worlds[player]
if w.energy_core == EnergyCore.option_fragments:
progress = pyevermizer.P_CORE_FRAGMENT
count = w.required_fragments
elif progress == pyevermizer.P_ALLOW_OOB:
if world.worlds[player].out_of_bounds == OutOfBounds.option_logic:
return True
elif progress == pyevermizer.P_ALLOW_SEQUENCE_BREAKS:
if world.worlds[player].sequence_breaks == SequenceBreaks.option_logic:
return True
return self._soe_count(progress, world, player, count) >= count

View File

@@ -4,18 +4,23 @@ import os.path
import threading
import typing
# from . import pyevermizer # as part of the source tree
import pyevermizer # from package
import settings
from BaseClasses import Item, ItemClassification, Location, LocationProgressType, Region, Tutorial
from Utils import output_path
from worlds.AutoWorld import WebWorld, World
from worlds.generic.Rules import add_item_rule, set_rule
from BaseClasses import Entrance, Item, ItemClassification, Location, LocationProgressType, Region, Tutorial
from Utils import output_path
from .logic import SoEPlayerLogic
from .options import Difficulty, EnergyCore, SoEOptions
from .patch import SoEDeltaPatch, get_base_rom_path
import pyevermizer # from package
# from . import pyevermizer # as part of the source tree
if typing.TYPE_CHECKING:
from BaseClasses import MultiWorld, CollectionState
__all__ = ["pyevermizer", "SoEWorld"]
from . import Logic # load logic mixin
from .Options import soe_options, Difficulty, EnergyCore, RequiredFragments, AvailableFragments
from .Patch import SoEDeltaPatch, get_base_rom_path
"""
In evermizer:
@@ -24,17 +29,17 @@ Items are uniquely defined by a pair of (type, id).
For most items this is their vanilla location (i.e. CHECK_GOURD, number).
Items have `provides`, which give the actual progression
instead of providing multiple events per item, we iterate through them in Logic.py
instead of providing multiple events per item, we iterate through them in logic.py
e.g. Found any weapon
Locations have `requires` and `provides`.
Requirements have to be converted to (access) rules for AP
e.g. Chest locked behind having a weapon
Provides could be events, but instead we iterate through the entire logic in Logic.py
Provides could be events, but instead we iterate through the entire logic in logic.py
e.g. NPC available after fighting a Boss
Rules are special locations that don't have a physical location
instead of implementing virtual locations and virtual items, we simply use them in Logic.py
instead of implementing virtual locations and virtual items, we simply use them in logic.py
e.g. 2DEs+Wheel+Gauge = Rocket
Rules and Locations live on the same logic tree returned by pyevermizer.get_logic()
@@ -76,7 +81,7 @@ for _loc in _locations:
# item helpers
_ingredients = (
'Wax', 'Water', 'Vinegar', 'Root', 'Oil', 'Mushroom', 'Mud Pepper', 'Meteorite', 'Limestone', 'Iron',
'Gunpowder', 'Grease', 'Feather', 'Ethanol', 'Dry Ice', 'Crystal', 'Clay', 'Brimstone', 'Bone', 'Atlas Amulet',
'Gunpowder', 'Grease', 'Feather', 'Ethanol', 'Dry Ice', 'Crystal', 'Clay', 'Brimstone', 'Bone', 'Atlas Medallion',
'Ash', 'Acorn'
)
_other_items = (
@@ -84,8 +89,8 @@ _other_items = (
)
def _match_item_name(item, substr: str) -> bool:
sub = item.name.split(' ', 1)[1] if item.name[0].isdigit() else item.name
def _match_item_name(item: pyevermizer.Item, substr: str) -> bool:
sub: str = item.name.split(' ', 1)[1] if item.name[0].isdigit() else item.name
return sub == substr or sub == substr+'s'
@@ -156,10 +161,11 @@ class SoESettings(settings.Group):
class SoEWorld(World):
"""
Secret of Evermore is a SNES action RPG. You learn alchemy spells, fight bosses and gather rocket parts to visit a
space station where the final boss must be defeated.
space station where the final boss must be defeated.
"""
game: str = "Secret of Evermore"
option_definitions = soe_options
game: typing.ClassVar[str] = "Secret of Evermore"
options_dataclass = SoEOptions
options: SoEOptions
settings: typing.ClassVar[SoESettings]
topology_present = False
data_version = 4
@@ -170,31 +176,21 @@ class SoEWorld(World):
location_name_to_id, location_id_to_raw = _get_location_mapping()
item_name_groups = _get_item_grouping()
trap_types = [name[12:] for name in option_definitions if name.startswith('trap_chance_')]
logic: SoEPlayerLogic
evermizer_seed: int
connect_name: str
energy_core: int
sequence_breaks: int
out_of_bounds: int
available_fragments: int
required_fragments: int
_halls_ne_chest_names: typing.List[str] = [loc.name for loc in _locations if 'Halls NE' in loc.name]
def __init__(self, *args, **kwargs):
def __init__(self, multiworld: "MultiWorld", player: int):
self.connect_name_available_event = threading.Event()
super(SoEWorld, self).__init__(*args, **kwargs)
super(SoEWorld, self).__init__(multiworld, player)
def generate_early(self) -> None:
# store option values that change logic
self.energy_core = self.multiworld.energy_core[self.player].value
self.sequence_breaks = self.multiworld.sequence_breaks[self.player].value
self.out_of_bounds = self.multiworld.out_of_bounds[self.player].value
self.required_fragments = self.multiworld.required_fragments[self.player].value
if self.required_fragments > self.multiworld.available_fragments[self.player].value:
self.multiworld.available_fragments[self.player].value = self.required_fragments
self.available_fragments = self.multiworld.available_fragments[self.player].value
# create logic from options
if self.options.required_fragments.value > self.options.available_fragments.value:
self.options.available_fragments.value = self.options.required_fragments.value
self.logic = SoEPlayerLogic(self.player, self.options)
def create_event(self, event: str) -> Item:
return SoEItem(event, ItemClassification.progression, None, self.player)
@@ -214,20 +210,20 @@ class SoEWorld(World):
return SoEItem(item.name, classification, self.item_name_to_id[item.name], self.player)
@classmethod
def stage_assert_generate(cls, multiworld):
def stage_assert_generate(cls, _: "MultiWorld") -> None:
rom_file = get_base_rom_path()
if not os.path.exists(rom_file):
raise FileNotFoundError(rom_file)
def create_regions(self):
def create_regions(self) -> None:
# exclude 'hidden' on easy
max_difficulty = 1 if self.multiworld.difficulty[self.player] == Difficulty.option_easy else 256
max_difficulty = 1 if self.options.difficulty == Difficulty.option_easy else 256
# TODO: generate *some* regions from locations' requirements?
menu = Region('Menu', self.player, self.multiworld)
self.multiworld.regions += [menu]
def get_sphere_index(evermizer_loc):
def get_sphere_index(evermizer_loc: pyevermizer.Location) -> int:
"""Returns 0, 1 or 2 for locations in spheres 1, 2, 3+"""
if len(evermizer_loc.requires) == 1 and evermizer_loc.requires[0][1] != pyevermizer.P_WEAPON:
return 2
@@ -252,18 +248,18 @@ class SoEWorld(World):
# mark some as excluded based on numbers above
for trash_sphere, fills in trash_fills.items():
for typ, counts in fills.items():
count = counts[self.multiworld.difficulty[self.player].value]
for location in self.multiworld.random.sample(spheres[trash_sphere][typ], count):
count = counts[self.options.difficulty.value]
for location in self.random.sample(spheres[trash_sphere][typ], count):
assert location.name != "Energy Core #285", "Error in sphere generation"
location.progress_type = LocationProgressType.EXCLUDED
def sphere1_blocked_items_rule(item):
def sphere1_blocked_items_rule(item: pyevermizer.Item) -> bool:
if isinstance(item, SoEItem):
# disable certain items in sphere 1
if item.name in {"Gauge", "Wheel"}:
return False
# and some more for non-easy, non-mystery
if self.multiworld.difficulty[item.player] not in (Difficulty.option_easy, Difficulty.option_mystery):
if self.options.difficulty not in (Difficulty.option_easy, Difficulty.option_mystery):
if item.name in {"Laser Lance", "Atom Smasher", "Diamond Eye"}:
return False
return True
@@ -273,13 +269,13 @@ class SoEWorld(World):
add_item_rule(location, sphere1_blocked_items_rule)
# make some logically late(r) bosses priority locations to increase complexity
if self.multiworld.difficulty[self.player] == Difficulty.option_mystery:
late_count = self.multiworld.random.randint(0, 2)
if self.options.difficulty == Difficulty.option_mystery:
late_count = self.random.randint(0, 2)
else:
late_count = self.multiworld.difficulty[self.player].value
late_count = self.options.difficulty.value
late_bosses = ("Tiny", "Aquagoth", "Megataur", "Rimsala",
"Mungola", "Lightning Storm", "Magmar", "Volcano Viper")
late_locations = self.multiworld.random.sample(late_bosses, late_count)
late_locations = self.random.sample(late_bosses, late_count)
# add locations to the world
for sphere in spheres.values():
@@ -293,17 +289,17 @@ class SoEWorld(World):
menu.connect(ingame, "New Game")
self.multiworld.regions += [ingame]
def create_items(self):
def create_items(self) -> None:
# add regular items to the pool
exclusions: typing.List[str] = []
if self.energy_core != EnergyCore.option_shuffle:
if self.options.energy_core != EnergyCore.option_shuffle:
exclusions.append("Energy Core") # will be placed in generate_basic or replaced by a fragment below
items = list(map(lambda item: self.create_item(item), (item for item in _items if item.name not in exclusions)))
# remove one pair of wings that will be placed in generate_basic
items.remove(self.create_item("Wings"))
def is_ingredient(item):
def is_ingredient(item: pyevermizer.Item) -> bool:
for ingredient in _ingredients:
if _match_item_name(item, ingredient):
return True
@@ -311,84 +307,72 @@ class SoEWorld(World):
# add energy core fragments to the pool
ingredients = [n for n, item in enumerate(items) if is_ingredient(item)]
if self.energy_core == EnergyCore.option_fragments:
if self.options.energy_core == EnergyCore.option_fragments:
items.append(self.create_item("Energy Core Fragment")) # replaces the vanilla energy core
for _ in range(self.available_fragments - 1):
for _ in range(self.options.available_fragments - 1):
if len(ingredients) < 1:
break # out of ingredients to replace
r = self.multiworld.random.choice(ingredients)
r = self.random.choice(ingredients)
ingredients.remove(r)
items[r] = self.create_item("Energy Core Fragment")
# add traps to the pool
trap_count = self.multiworld.trap_count[self.player].value
trap_chances = {}
trap_names = {}
trap_count = self.options.trap_count.value
trap_names: typing.List[str] = []
trap_weights: typing.List[int] = []
if trap_count > 0:
for trap_type in self.trap_types:
trap_option = getattr(self.multiworld, f'trap_chance_{trap_type}')[self.player]
trap_chances[trap_type] = trap_option.value
trap_names[trap_type] = trap_option.item_name
trap_chances_total = sum(trap_chances.values())
if trap_chances_total == 0:
for trap_type in trap_chances:
trap_chances[trap_type] = 1
trap_chances_total = len(trap_chances)
for trap_option in self.options.trap_chances:
trap_names.append(trap_option.item_name)
trap_weights.append(trap_option.value)
if sum(trap_weights) == 0:
trap_weights = [1 for _ in trap_weights]
def create_trap() -> Item:
v = self.multiworld.random.randrange(trap_chances_total)
for t, c in trap_chances.items():
if v < c:
return self.create_item(trap_names[t])
v -= c
assert False, "Bug in create_trap"
return self.create_item(self.random.choices(trap_names, trap_weights)[0])
for _ in range(trap_count):
if len(ingredients) < 1:
break # out of ingredients to replace
r = self.multiworld.random.choice(ingredients)
r = self.random.choice(ingredients)
ingredients.remove(r)
items[r] = create_trap()
self.multiworld.itempool += items
def set_rules(self):
def set_rules(self) -> None:
self.multiworld.completion_condition[self.player] = lambda state: state.has('Victory', self.player)
# set Done from goal option once we have multiple goals
set_rule(self.multiworld.get_location('Done', self.player),
lambda state: state.soe_has(pyevermizer.P_FINAL_BOSS, self.multiworld, self.player))
lambda state: self.logic.has(state, pyevermizer.P_FINAL_BOSS))
set_rule(self.multiworld.get_entrance('New Game', self.player), lambda state: True)
for loc in _locations:
location = self.multiworld.get_location(loc.name, self.player)
set_rule(location, self.make_rule(loc.requires))
def make_rule(self, requires: typing.List[typing.Tuple[int, int]]) -> typing.Callable[[typing.Any], bool]:
def rule(state) -> bool:
def rule(state: "CollectionState") -> bool:
for count, progress in requires:
if not state.soe_has(progress, self.multiworld, self.player, count):
if not self.logic.has(state, progress, count):
return False
return True
return rule
def make_item_type_limit_rule(self, item_type: int):
return lambda item: item.player != self.player or self.item_id_to_raw[item.code].type == item_type
def generate_basic(self):
def generate_basic(self) -> None:
# place Victory event
self.multiworld.get_location('Done', self.player).place_locked_item(self.create_event('Victory'))
# place wings in halls NE to avoid softlock
wings_location = self.multiworld.random.choice(self._halls_ne_chest_names)
wings_location = self.random.choice(self._halls_ne_chest_names)
wings_item = self.create_item('Wings')
self.multiworld.get_location(wings_location, self.player).place_locked_item(wings_item)
# place energy core at vanilla location for vanilla mode
if self.energy_core == EnergyCore.option_vanilla:
if self.options.energy_core == EnergyCore.option_vanilla:
energy_core = self.create_item('Energy Core')
self.multiworld.get_location('Energy Core #285', self.player).place_locked_item(energy_core)
# generate stuff for later
self.evermizer_seed = self.multiworld.random.randint(0, 2 ** 16 - 1) # TODO: make this an option for "full" plando?
self.evermizer_seed = self.random.randint(0, 2 ** 16 - 1) # TODO: make this an option for "full" plando?
def generate_output(self, output_directory: str):
def generate_output(self, output_directory: str) -> None:
player_name = self.multiworld.get_player_name(self.player)
self.connect_name = player_name[:32]
while len(self.connect_name.encode('utf-8')) > 32:
@@ -397,24 +381,21 @@ class SoEWorld(World):
placement_file = ""
out_file = ""
try:
money = self.multiworld.money_modifier[self.player].value
exp = self.multiworld.exp_modifier[self.player].value
money = self.options.money_modifier.value
exp = self.options.exp_modifier.value
switches: typing.List[str] = []
if self.multiworld.death_link[self.player].value:
if self.options.death_link.value:
switches.append("--death-link")
if self.energy_core == EnergyCore.option_fragments:
switches.extend(('--available-fragments', str(self.available_fragments),
'--required-fragments', str(self.required_fragments)))
if self.options.energy_core == EnergyCore.option_fragments:
switches.extend(('--available-fragments', str(self.options.available_fragments.value),
'--required-fragments', str(self.options.required_fragments.value)))
rom_file = get_base_rom_path()
out_base = output_path(output_directory, self.multiworld.get_out_file_name_base(self.player))
out_file = out_base + '.sfc'
placement_file = out_base + '.txt'
patch_file = out_base + '.apsoe'
flags = 'l' # spoiler log
for option_name in self.option_definitions:
option = getattr(self.multiworld, option_name)[self.player]
if hasattr(option, 'to_flag'):
flags += option.to_flag()
flags += self.options.flags
with open(placement_file, "wb") as f: # generate placement file
for location in self.multiworld.get_locations(self.player):
@@ -448,7 +429,7 @@ class SoEWorld(World):
except FileNotFoundError:
pass
def modify_multidata(self, multidata: dict):
def modify_multidata(self, multidata: typing.Dict[str, typing.Any]) -> None:
# wait for self.connect_name to be available.
self.connect_name_available_event.wait()
# we skip in case of error, so that the original error in the output thread is the one that gets raised
@@ -457,7 +438,7 @@ class SoEWorld(World):
multidata["connect_names"][self.connect_name] = payload
def get_filler_item_name(self) -> str:
return self.multiworld.random.choice(list(self.item_name_groups["Ingredients"]))
return self.random.choice(list(self.item_name_groups["Ingredients"]))
class SoEItem(Item):

85
worlds/soe/logic.py Normal file
View File

@@ -0,0 +1,85 @@
import typing
from typing import Callable, Set
from . import pyevermizer
from .options import EnergyCore, OutOfBounds, SequenceBreaks, SoEOptions
if typing.TYPE_CHECKING:
from BaseClasses import CollectionState
# TODO: Options may preset certain progress steps (i.e. P_ROCK_SKIP), set in generate_early?
# TODO: resolve/flatten/expand rules to get rid of recursion below where possible
# Logic.rules are all rules including locations, excluding those with no progress (i.e. locations that only drop items)
rules = [rule for rule in pyevermizer.get_logic() if len(rule.provides) > 0]
# Logic.items are all items and extra items excluding non-progression items and duplicates
item_names: Set[str] = set()
items = [item for item in filter(lambda item: item.progression, pyevermizer.get_items() + pyevermizer.get_extra_items())
if item.name not in item_names and not item_names.add(item.name)] # type: ignore[func-returns-value]
class SoEPlayerLogic:
__slots__ = "player", "out_of_bounds", "sequence_breaks", "has"
player: int
out_of_bounds: bool
sequence_breaks: bool
has: Callable[..., bool]
"""
Returns True if count of one of evermizer's progress steps is reached based on collected items. i.e. 2 * P_DE
"""
def __init__(self, player: int, options: "SoEOptions"):
self.player = player
self.out_of_bounds = options.out_of_bounds == OutOfBounds.option_logic
self.sequence_breaks = options.sequence_breaks == SequenceBreaks.option_logic
if options.energy_core == EnergyCore.option_fragments:
# override logic for energy core fragments
required_fragments = options.required_fragments.value
def fragmented_has(state: "CollectionState", progress: int, count: int = 1) -> bool:
if progress == pyevermizer.P_ENERGY_CORE:
progress = pyevermizer.P_CORE_FRAGMENT
count = required_fragments
return self._has(state, progress, count)
self.has = fragmented_has
else:
# default (energy core) logic
self.has = self._has
def _count(self, state: "CollectionState", progress: int, max_count: int = 0) -> int:
"""
Returns reached count of one of evermizer's progress steps based on collected items.
i.e. returns 0-3 for P_DE based on items providing CHECK_BOSS,DIAMOND_EYE_DROP
"""
n = 0
for item in items:
for pvd in item.provides:
if pvd[1] == progress:
if state.has(item.name, self.player):
n += state.count(item.name, self.player) * pvd[0]
if n >= max_count > 0:
return n
for rule in rules:
for pvd in rule.provides:
if pvd[1] == progress and pvd[0] > 0:
has = True
for req in rule.requires:
if not self.has(state, req[1], req[0]):
has = False
break
if has:
n += pvd[0]
if n >= max_count > 0:
return n
return n
def _has(self, state: "CollectionState", progress: int, count: int = 1) -> bool:
"""Default implementation of has"""
if self.out_of_bounds is True and progress == pyevermizer.P_ALLOW_OOB:
return True
if self.sequence_breaks is True and progress == pyevermizer.P_ALLOW_SEQUENCE_BREAKS:
return True
return self._count(state, progress, count) >= count

View File

@@ -1,16 +1,18 @@
import typing
from dataclasses import dataclass, fields
from typing import Any, cast, Dict, Iterator, List, Tuple, Protocol
from Options import Range, Choice, Toggle, DefaultOnToggle, AssembleOptions, DeathLink, ProgressionBalancing
from Options import AssembleOptions, Choice, DeathLink, DefaultOnToggle, Option, PerGameCommonOptions, \
ProgressionBalancing, Range, Toggle
# typing boilerplate
class FlagsProtocol(typing.Protocol):
class FlagsProtocol(Protocol):
value: int
default: int
flags: typing.List[str]
flags: List[str]
class FlagProtocol(typing.Protocol):
class FlagProtocol(Protocol):
value: int
default: int
flag: str
@@ -18,7 +20,7 @@ class FlagProtocol(typing.Protocol):
# meta options
class EvermizerFlags:
flags: typing.List[str]
flags: List[str]
def to_flag(self: FlagsProtocol) -> str:
return self.flags[self.value]
@@ -200,13 +202,13 @@ class TrapCount(Range):
# more meta options
class ItemChanceMeta(AssembleOptions):
def __new__(mcs, name, bases, attrs):
def __new__(mcs, name: str, bases: Tuple[type], attrs: Dict[Any, Any]) -> "ItemChanceMeta":
if 'item_name' in attrs:
attrs["display_name"] = f"{attrs['item_name']} Chance"
attrs["range_start"] = 0
attrs["range_end"] = 100
return super(ItemChanceMeta, mcs).__new__(mcs, name, bases, attrs)
cls = super(ItemChanceMeta, mcs).__new__(mcs, name, bases, attrs)
return cast(ItemChanceMeta, cls)
class TrapChance(Range, metaclass=ItemChanceMeta):
@@ -247,33 +249,52 @@ class SoEProgressionBalancing(ProgressionBalancing):
special_range_names = {**ProgressionBalancing.special_range_names, "normal": default}
soe_options: typing.Dict[str, AssembleOptions] = {
"difficulty": Difficulty,
"energy_core": EnergyCore,
"required_fragments": RequiredFragments,
"available_fragments": AvailableFragments,
"money_modifier": MoneyModifier,
"exp_modifier": ExpModifier,
"sequence_breaks": SequenceBreaks,
"out_of_bounds": OutOfBounds,
"fix_cheats": FixCheats,
"fix_infinite_ammo": FixInfiniteAmmo,
"fix_atlas_glitch": FixAtlasGlitch,
"fix_wings_glitch": FixWingsGlitch,
"shorter_dialogs": ShorterDialogs,
"short_boss_rush": ShortBossRush,
"ingredienizer": Ingredienizer,
"sniffamizer": Sniffamizer,
"callbeadamizer": Callbeadamizer,
"musicmizer": Musicmizer,
"doggomizer": Doggomizer,
"turdo_mode": TurdoMode,
"death_link": DeathLink,
"trap_count": TrapCount,
"trap_chance_quake": TrapChanceQuake,
"trap_chance_poison": TrapChancePoison,
"trap_chance_confound": TrapChanceConfound,
"trap_chance_hud": TrapChanceHUD,
"trap_chance_ohko": TrapChanceOHKO,
"progression_balancing": SoEProgressionBalancing,
}
# noinspection SpellCheckingInspection
@dataclass
class SoEOptions(PerGameCommonOptions):
difficulty: Difficulty
energy_core: EnergyCore
required_fragments: RequiredFragments
available_fragments: AvailableFragments
money_modifier: MoneyModifier
exp_modifier: ExpModifier
sequence_breaks: SequenceBreaks
out_of_bounds: OutOfBounds
fix_cheats: FixCheats
fix_infinite_ammo: FixInfiniteAmmo
fix_atlas_glitch: FixAtlasGlitch
fix_wings_glitch: FixWingsGlitch
shorter_dialogs: ShorterDialogs
short_boss_rush: ShortBossRush
ingredienizer: Ingredienizer
sniffamizer: Sniffamizer
callbeadamizer: Callbeadamizer
musicmizer: Musicmizer
doggomizer: Doggomizer
turdo_mode: TurdoMode
death_link: DeathLink
trap_count: TrapCount
trap_chance_quake: TrapChanceQuake
trap_chance_poison: TrapChancePoison
trap_chance_confound: TrapChanceConfound
trap_chance_hud: TrapChanceHUD
trap_chance_ohko: TrapChanceOHKO
progression_balancing: SoEProgressionBalancing
@property
def trap_chances(self) -> Iterator[TrapChance]:
for field in fields(self):
option = getattr(self, field.name)
if isinstance(option, TrapChance):
yield option
@property
def flags(self) -> str:
flags = ''
for field in fields(self):
option = getattr(self, field.name)
if isinstance(option, (EvermizerFlag, EvermizerFlags)):
assert isinstance(option, Option)
# noinspection PyUnresolvedReferences
flags += option.to_flag()
return flags

View File

@@ -1,5 +1,5 @@
import os
from typing import Optional
from typing import BinaryIO, Optional
import Utils
from worlds.Files import APDeltaPatch
@@ -30,7 +30,7 @@ def get_base_rom_path(file_name: Optional[str] = None) -> str:
return file_name
def read_rom(stream, strip_header=True) -> bytes:
def read_rom(stream: BinaryIO, strip_header: bool = True) -> bytes:
"""Reads rom into bytearray and optionally strips off any smc header"""
data = stream.read()
if strip_header and len(data) % 0x400 == 0x200:
@@ -40,5 +40,5 @@ def read_rom(stream, strip_header=True) -> bytes:
if __name__ == '__main__':
import sys
print('Please use ../../Patch.py', file=sys.stderr)
print('Please use ../../patch.py', file=sys.stderr)
sys.exit(1)

View File

@@ -1,4 +1,4 @@
from test.TestBase import WorldTestBase
from test.bases import WorldTestBase
from typing import Iterable
@@ -6,7 +6,7 @@ class SoETestBase(WorldTestBase):
game = "Secret of Evermore"
def assertLocationReachability(self, reachable: Iterable[str] = (), unreachable: Iterable[str] = (),
satisfied=True) -> None:
satisfied: bool = True) -> None:
"""
Tests that unreachable can't be reached. Tests that reachable can be reached if satisfied=True.
Usage: test with satisfied=False, collect requirements into state, test again with satisfied=True
@@ -18,3 +18,14 @@ class SoETestBase(WorldTestBase):
for location in unreachable:
self.assertFalse(self.can_reach_location(location),
f"{location} is reachable but shouldn't be")
def testRocketPartsExist(self) -> None:
"""Tests that rocket parts exist and are unique"""
self.assertEqual(len(self.get_items_by_name("Gauge")), 1)
self.assertEqual(len(self.get_items_by_name("Wheel")), 1)
diamond_eyes = self.get_items_by_name("Diamond Eye")
self.assertEqual(len(diamond_eyes), 3)
# verify diamond eyes are individual items
self.assertFalse(diamond_eyes[0] is diamond_eyes[1])
self.assertFalse(diamond_eyes[0] is diamond_eyes[2])
self.assertFalse(diamond_eyes[1] is diamond_eyes[2])

View File

@@ -4,10 +4,10 @@ from . import SoETestBase
class AccessTest(SoETestBase):
@staticmethod
def _resolveGourds(gourds: typing.Dict[str, typing.Iterable[int]]):
def _resolveGourds(gourds: typing.Mapping[str, typing.Iterable[int]]) -> typing.List[str]:
return [f"{name} #{number}" for name, numbers in gourds.items() for number in numbers]
def testBronzeAxe(self):
def test_bronze_axe(self) -> None:
gourds = {
"Pyramid bottom": (118, 121, 122, 123, 124, 125),
"Pyramid top": (140,)
@@ -16,7 +16,7 @@ class AccessTest(SoETestBase):
items = [["Bronze Axe"]]
self.assertAccessDependency(locations, items)
def testBronzeSpearPlus(self):
def test_bronze_spear_plus(self) -> None:
locations = ["Megataur"]
items = [["Bronze Spear"], ["Lance (Weapon)"], ["Laser Lance"]]
self.assertAccessDependency(locations, items)

View File

@@ -8,7 +8,7 @@ class TestFragmentGoal(SoETestBase):
"required_fragments": 20,
}
def testFragments(self):
def test_fragments(self) -> None:
self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"])
self.assertBeatable(False) # 0 fragments
fragments = self.get_items_by_name("Energy Core Fragment")
@@ -24,11 +24,11 @@ class TestFragmentGoal(SoETestBase):
self.assertEqual(self.count("Energy Core Fragment"), 21)
self.assertBeatable(True)
def testNoWeapon(self):
def test_no_weapon(self) -> None:
self.collect_by_name(["Diamond Eye", "Wheel", "Gauge", "Energy Core Fragment"])
self.assertBeatable(False)
def testNoRocket(self):
def test_no_rocket(self) -> None:
self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Energy Core Fragment"])
self.assertBeatable(False)
@@ -38,16 +38,16 @@ class TestShuffleGoal(SoETestBase):
"energy_core": "shuffle",
}
def testCore(self):
def test_core(self) -> None:
self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"])
self.assertBeatable(False)
self.collect_by_name(["Energy Core"])
self.assertBeatable(True)
def testNoWeapon(self):
def test_no_weapon(self) -> None:
self.collect_by_name(["Diamond Eye", "Wheel", "Gauge", "Energy Core"])
self.assertBeatable(False)
def testNoRocket(self):
def test_no_rocket(self) -> None:
self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Energy Core"])
self.assertBeatable(False)

View File

@@ -0,0 +1,21 @@
from unittest import TestCase
from .. import SoEWorld
class TestMapping(TestCase):
def test_atlas_medallion_name_group(self) -> None:
"""
Test that we used the pyevermizer name for Atlas Medallion (not Amulet) in item groups.
"""
self.assertIn("Any Atlas Medallion", SoEWorld.item_name_groups)
def test_atlas_medallion_name_items(self) -> None:
"""
Test that we used the pyevermizer name for Atlas Medallion (not Amulet) in items.
"""
found_medallion = False
for name in SoEWorld.item_name_to_id:
self.assertNotIn("Atlas Amulet", name, "Expected Atlas Medallion, not Amulet")
if "Atlas Medallion" in name:
found_medallion = True
self.assertTrue(found_medallion, "Did not find Atlas Medallion in items")

View File

@@ -6,7 +6,7 @@ class OoBTest(SoETestBase):
"""Tests that 'on' doesn't put out-of-bounds in logic. This is also the test base for OoB in logic."""
options: typing.Dict[str, typing.Any] = {"out_of_bounds": "on"}
def testOoBAccess(self):
def test_oob_access(self) -> None:
in_logic = self.options["out_of_bounds"] == "logic"
# some locations that just need a weapon + OoB
@@ -37,7 +37,7 @@ class OoBTest(SoETestBase):
self.collect_by_name("Diamond Eye")
self.assertLocationReachability(reachable=de_reachable, unreachable=de_unreachable, satisfied=in_logic)
def testOoBGoal(self):
def test_oob_goal(self) -> None:
# still need Energy Core with OoB if sequence breaks are not in logic
for item in ["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"]:
self.collect_by_name(item)

View File

@@ -6,7 +6,7 @@ class SequenceBreaksTest(SoETestBase):
"""Tests that 'on' doesn't put sequence breaks in logic. This is also the test base for in-logic."""
options: typing.Dict[str, typing.Any] = {"sequence_breaks": "on"}
def testSequenceBreaksAccess(self):
def test_sequence_breaks_access(self) -> None:
in_logic = self.options["sequence_breaks"] == "logic"
# some locations that just need any weapon + sequence break
@@ -30,7 +30,7 @@ class SequenceBreaksTest(SoETestBase):
self.collect_by_name("Bronze Spear") # Escape now just needs either Megataur or Rimsala dead
self.assertEqual(self.can_reach_location("Escape"), in_logic)
def testSequenceBreaksGoal(self):
def test_sequence_breaks_goal(self) -> None:
in_logic = self.options["sequence_breaks"] == "logic"
# don't need Energy Core with sequence breaks in logic

View File

@@ -0,0 +1,56 @@
import typing
from dataclasses import fields
from . import SoETestBase
from ..options import SoEOptions
if typing.TYPE_CHECKING:
from .. import SoEWorld
class Bases:
# class in class to avoid running tests for TrapTest class
class TrapTestBase(SoETestBase):
"""Test base for trap tests"""
option_name_to_item_name = {
# filtering by name here validates that there is no confusion between name and type
field.name: field.type.item_name for field in fields(SoEOptions) if field.name.startswith("trap_chance_")
}
def test_dataclass(self) -> None:
"""Test that the dataclass helper property returns the expected sequence"""
self.assertGreater(len(self.option_name_to_item_name), 0, "Expected more than 0 trap types")
world: "SoEWorld" = typing.cast("SoEWorld", self.multiworld.worlds[1])
item_name_to_rolled_option = {option.item_name: option for option in world.options.trap_chances}
# compare that all fields are present - that is property in dataclass and selector code in test line up
self.assertEqual(sorted(self.option_name_to_item_name.values()), sorted(item_name_to_rolled_option),
"field names probably do not match field types")
# sanity check that chances are correctly set and returned by property
for option_name, item_name in self.option_name_to_item_name.items():
self.assertEqual(item_name_to_rolled_option[item_name].value,
self.options.get(option_name, item_name_to_rolled_option[item_name].default))
def test_trap_count(self) -> None:
"""Test that total trap count is correct"""
self.assertEqual(self.options["trap_count"],
len(self.get_items_by_name(self.option_name_to_item_name.values())))
class TestTrapAllZeroChance(Bases.TrapTestBase):
"""Tests all zero chances still gives traps if trap_count is set."""
options: typing.Dict[str, typing.Any] = {
"trap_count": 1,
**{name: 0 for name in Bases.TrapTestBase.option_name_to_item_name}
}
class TestTrapNoConfound(Bases.TrapTestBase):
"""Tests that one zero chance does not give that trap."""
options: typing.Dict[str, typing.Any] = {
"trap_count": 99,
"trap_chance_confound": 0,
}
def test_no_confound_trap(self) -> None:
self.assertEqual(self.option_name_to_item_name["trap_chance_confound"], "Confound Trap")
self.assertEqual(len(self.get_items_by_name("Confound Trap")), 0)

View File

@@ -170,6 +170,8 @@ def set_entrance_rules(logic, multiworld, player, world_options: StardewValleyOp
logic.received("Bus Repair").simplify())
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_skull_cavern, player),
logic.received(Wallet.skull_key).simplify())
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_casino, player),
logic.received("Club Card").simplify())
for floor in range(25, 200 + 25, 25):
MultiWorldRules.set_rule(multiworld.get_entrance(dig_to_skull_floor(floor), player),
logic.can_mine_to_skull_cavern_floor(floor).simplify())

View File

@@ -5,7 +5,8 @@ import itertools
from typing import List, Dict, Any, cast
from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification
from worlds.AutoWorld import World, WebWorld
from worlds.AutoWorld import World, WebWorld, staticproperty
from Utils import cache_argsless
from . import items
from . import locations
from . import creatures
@@ -16,21 +17,26 @@ from .rules import set_rules
logger = logging.getLogger("Subnautica")
class SubnaticaWeb(WebWorld):
tutorials = [Tutorial(
"Multiworld Setup Guide",
"A guide to setting up the Subnautica randomizer connected to an Archipelago Multiworld",
"English",
"setup_en.md",
"setup/en",
["Berserker"]
)]
all_locations = {data["name"]: loc_id for loc_id, data in locations.location_table.items()}
all_locations.update(creatures.creature_locations)
@staticproperty
@cache_argsless
def web():
class SubnaticaWeb(WebWorld):
tutorials = [Tutorial(
"Multiworld Setup Guide",
"A guide to setting up the Subnautica randomizer connected to an Archipelago Multiworld",
"English",
"setup_en.md",
"setup/en",
["Berserker"]
)]
return SubnaticaWeb()
class SubnauticaWorld(World):
"""
Subnautica is an undersea exploration game. Stranded on an alien world, you become infected by
@@ -38,7 +44,7 @@ class SubnauticaWorld(World):
You must find a cure for yourself, build an escape rocket, and leave the planet.
"""
game = "Subnautica"
web = SubnaticaWeb()
web = web
item_name_to_id = {data.name: item_id for item_id, data in items.item_table.items()}
location_name_to_id = all_locations

View File

@@ -94,17 +94,17 @@ def get_pool_core(world):
# Starting Weapon
start_weapon_locations = starting_weapon_locations.copy()
final_starting_weapons = [weapon for weapon in starting_weapons
if weapon not in world.multiworld.non_local_items[world.player]]
if weapon not in world.options.non_local_items]
if not final_starting_weapons:
final_starting_weapons = starting_weapons
starting_weapon = random.choice(final_starting_weapons)
if world.multiworld.StartingPosition[world.player] == StartingPosition.option_safe:
if world.options.StartingPosition == StartingPosition.option_safe:
placed_items[start_weapon_locations[0]] = starting_weapon
elif world.multiworld.StartingPosition[world.player] in \
elif world.options.StartingPosition in \
[StartingPosition.option_unsafe, StartingPosition.option_dangerous]:
if world.multiworld.StartingPosition[world.player] == StartingPosition.option_dangerous:
if world.options.StartingPosition == StartingPosition.option_dangerous:
for location in dangerous_weapon_locations:
if world.multiworld.ExpandedPool[world.player] or "Drop" not in location:
if world.options.ExpandedPool or "Drop" not in location:
start_weapon_locations.append(location)
placed_items[random.choice(start_weapon_locations)] = starting_weapon
else:
@@ -115,7 +115,7 @@ def get_pool_core(world):
# Triforce Fragments
fragment = "Triforce Fragment"
if world.multiworld.ExpandedPool[world.player]:
if world.options.ExpandedPool:
possible_level_locations = [location for location in all_level_locations
if location not in level_locations[8]]
else:
@@ -125,15 +125,15 @@ def get_pool_core(world):
if location in possible_level_locations:
possible_level_locations.remove(location)
for level in range(1, 9):
if world.multiworld.TriforceLocations[world.player] == TriforceLocations.option_vanilla:
if world.options.TriforceLocations == TriforceLocations.option_vanilla:
placed_items[f"Level {level} Triforce"] = fragment
elif world.multiworld.TriforceLocations[world.player] == TriforceLocations.option_dungeons:
elif world.options.TriforceLocations == TriforceLocations.option_dungeons:
placed_items[possible_level_locations.pop(random.randint(0, len(possible_level_locations) - 1))] = fragment
else:
pool.append(fragment)
# Level 9 junk fill
if world.multiworld.ExpandedPool[world.player] > 0:
if world.options.ExpandedPool > 0:
spots = random.sample(level_locations[8], len(level_locations[8]) // 2)
for spot in spots:
junk = random.choice(list(minor_items.keys()))
@@ -142,7 +142,7 @@ def get_pool_core(world):
# Finish Pool
final_pool = basic_pool
if world.multiworld.ExpandedPool[world.player]:
if world.options.ExpandedPool:
final_pool = {
item: basic_pool.get(item, 0) + minor_items.get(item, 0) + take_any_items.get(item, 0)
for item in set(basic_pool) | set(minor_items) | set(take_any_items)

View File

@@ -1,5 +1,6 @@
import typing
from Options import Option, DefaultOnToggle, Choice
from dataclasses import dataclass
from Options import Option, DefaultOnToggle, Choice, PerGameCommonOptions
class ExpandedPool(DefaultOnToggle):
@@ -32,9 +33,8 @@ class StartingPosition(Choice):
option_dangerous = 2
option_very_dangerous = 3
tloz_options: typing.Dict[str, type(Option)] = {
"ExpandedPool": ExpandedPool,
"TriforceLocations": TriforceLocations,
"StartingPosition": StartingPosition
}
@dataclass
class TlozOptions(PerGameCommonOptions):
ExpandedPool: ExpandedPool
TriforceLocations: TriforceLocations
StartingPosition: StartingPosition

View File

@@ -11,6 +11,7 @@ if TYPE_CHECKING:
def set_rules(tloz_world: "TLoZWorld"):
player = tloz_world.player
world = tloz_world.multiworld
options = tloz_world.options
# Boss events for a nicer spoiler log play through
for level in range(1, 9):
@@ -23,7 +24,7 @@ def set_rules(tloz_world: "TLoZWorld"):
# No dungeons without weapons except for the dangerous weapon locations if we're dangerous, no unsafe dungeons
for i, level in enumerate(tloz_world.levels[1:10]):
for location in level.locations:
if world.StartingPosition[player] < StartingPosition.option_dangerous \
if options.StartingPosition < StartingPosition.option_dangerous \
or location.name not in dangerous_weapon_locations:
add_rule(world.get_location(location.name, player),
lambda state: state.has_group("weapons", player))
@@ -66,7 +67,7 @@ def set_rules(tloz_world: "TLoZWorld"):
lambda state: state.has("Recorder", player))
add_rule(world.get_location("Level 7 Boss", player),
lambda state: state.has("Recorder", player))
if world.ExpandedPool[player]:
if options.ExpandedPool:
add_rule(world.get_location("Level 7 Key Drop (Stalfos)", player),
lambda state: state.has("Recorder", player))
add_rule(world.get_location("Level 7 Bomb Drop (Digdogger)", player),
@@ -75,13 +76,13 @@ def set_rules(tloz_world: "TLoZWorld"):
lambda state: state.has("Recorder", player))
for location in food_locations:
if world.ExpandedPool[player] or "Drop" not in location:
if options.ExpandedPool or "Drop" not in location:
add_rule(world.get_location(location, player),
lambda state: state.has("Food", player))
add_rule(world.get_location("Level 8 Item (Magical Key)", player),
lambda state: state.has("Bow", player) and state.has_group("arrows", player))
if world.ExpandedPool[player]:
if options.ExpandedPool:
add_rule(world.get_location("Level 8 Bomb Drop (Darknuts North)", player),
lambda state: state.has("Bow", player) and state.has_group("arrows", player))
@@ -106,13 +107,13 @@ def set_rules(tloz_world: "TLoZWorld"):
for location in stepladder_locations:
add_rule(world.get_location(location, player),
lambda state: state.has("Stepladder", player))
if world.ExpandedPool[player]:
if options.ExpandedPool:
for location in stepladder_locations_expanded:
add_rule(world.get_location(location, player),
lambda state: state.has("Stepladder", player))
# Don't allow Take Any Items until we can actually get in one
if world.ExpandedPool[player]:
if options.ExpandedPool:
add_rule(world.get_location("Take Any Item Left", player),
lambda state: state.has_group("candles", player) or
state.has("Raft", player))

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