Compare commits

..

5 Commits

Author SHA1 Message Date
Fabian Dill
3469c3277d Merge branch 'main' into core_optimize_deprecatedict 2024-01-05 17:14:58 +01:00
Fabian Dill
c087f881fd Merge branch 'main' into core_optimize_deprecatedict 2024-01-04 15:58:49 +01:00
Fabian Dill
f754cf9cb3 Merge branch 'main' into core_optimize_deprecatedict 2024-01-04 15:58:29 +01:00
Fabian Dill
16487a877c Merge branch 'main' into core_optimize_deprecatedict 2024-01-03 14:07:26 +01:00
Fabian Dill
8996786331 Core: optimize DeprecateDict 2024-01-01 17:43:12 +01:00
138 changed files with 972 additions and 6077 deletions

View File

@@ -1,5 +0,0 @@
[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=14 --max-doc-length=120 --max-line-length=120 --statistics ${{ env.diff }}
flake8 --count --max-complexity=10 --max-doc-length=120 --max-line-length=120 --statistics ${{ env.diff }}
- name: "mypy: Type check modified files"
continue-on-error: true

View File

@@ -1,18 +0,0 @@
<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,6 +1056,9 @@ 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": [game_name]} for game_name in needed_updates])
await self.send_msgs([{"cmd": "GetDataPackage", "games": list(needed_updates)}])
def update_game(self, game_package: dict):
for item_name, item_id in game_package["item_name_to_id"].items():
@@ -477,7 +477,6 @@ 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)
@@ -612,10 +611,6 @@ async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None)
address = f"ws://{address}" if "://" not in address \
else address.replace("archipelago://", "ws://")
uri = urllib.parse.urlparse(address)
if uri.username and uri.password is None:
# Fix for Firefox stripping empty password https://bugzilla.mozilla.org/show_bug.cgi?id=1876952
address = address.replace("@", ":@")
server_url = urllib.parse.urlparse(address)
if server_url.username:
@@ -732,6 +727,7 @@ 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,9 +114,7 @@ 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 getattr(world.worlds[player].options,
"start_inventory_from_pool",
StartInventoryPool({})).value.items():
for item_name, count in world.start_inventory_from_pool.setdefault(player, 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.
@@ -169,14 +167,10 @@ 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(getattr(world.worlds[player].options, "start_inventory_from_pool", None) for player in world.player_ids):
if any(world.start_inventory_from_pool[player].value for player in world.player_ids):
new_items: List[Item] = []
depletion_pool: Dict[int, Dict[str, int]] = {
player: getattr(world.worlds[player].options,
"start_inventory_from_pool",
StartInventoryPool({})).value.copy()
for player in world.player_ids
}
player: world.start_inventory_from_pool[player].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,29 +4,14 @@ 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)
_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'),))
update_ran = getattr(sys, "frozen", False) or multiprocessing.parent_process()
if not update_ran:
for entry in os.scandir(os.path.join(local_dir, "worlds")):

View File

@@ -2210,24 +2210,25 @@ 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():
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.")
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:
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.")
else:
await asyncio.sleep(seconds)

View File

@@ -58,7 +58,6 @@ 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__()
def __getitem__(self, item: Any) -> Any:
if self.should_error:
deprecate(self.log_message)
elif __debug__:
if __debug__:
def __getitem__(self, item: Any) -> Any:
if self.should_error:
deprecate(self.log_message)
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:
files = request.files.getlist('file')
options = get_yaml_data(files)
file = request.files['file']
options = get_yaml_data(file)
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="auto">Automatic on goal completion</option>
<option value="goal">Allow !collect after goal completion</option>
<option value="auto">Automatic on goal completion</option>
<option value="auto-enabled">
Automatic on goal completion and manual !collect
</option>
@@ -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,9 +164,6 @@
# 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. 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. |
| Name | Type | Notes |
| ---- | ---- | ---- |
| time | float | Unix Time Stamp of time of death. |
| cause | str | Optional. Text to explain the cause of death, ex. "Berserker was run over by a train." |
| source | str | Name of the player who first died. Can be a slot name, but can also be a name from within a multiplayer game. |

View File

@@ -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.38.33130') < 0);
Result := (CompareStr(strVersion, 'v14.32.31332') < 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.3.0
kivy>=2.2.1
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("auto")
collect_mode: CollectMode = CollectMode("auto")
release_mode: ReleaseMode = ReleaseMode("goal")
collect_mode: CollectMode = CollectMode("goal")
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, connections, texts")
plando_options: PlandoOptions = PlandoOptions("bosses")
class SNIOptions(Group):

View File

@@ -54,6 +54,7 @@ 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
@@ -75,6 +76,7 @@ non_apworlds: set = {
"Ocarina of Time",
"Overcooked! 2",
"Raft",
"Secret of Evermore",
"Slay the Spire",
"Sudoku",
"Super Mario 64",
@@ -303,6 +305,7 @@ 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
@@ -349,18 +352,6 @@ 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.worlds[1].options.exclude_locations.value
excluded = self.multiworld.exclude_locations[1].value
state = self.multiworld.get_all_state(False)
for location in self.multiworld.get_locations():
if location.name not in excluded:

View File

@@ -1,127 +0,0 @@
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,6 +1,5 @@
import unittest
from worlds.AutoWorld import AutoWorldRegister, call_all
from worlds.AutoWorld import AutoWorldRegister
from . import setup_solo_multiworld
@@ -54,7 +53,7 @@ class TestBase(unittest.TestCase):
f"{game_name} Item count MUST meet or exceed the number of locations",
)
def test_items_in_datapackage(self):
def testItemsInDatapackage(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):
@@ -70,20 +69,3 @@ 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,10 +10,3 @@ 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.worlds[1].options.exclude_locations.value
excluded = world.exclude_locations[1].value
state = world.get_all_state(False)
for location in world.get_locations():
if location.name not in excluded:

View File

@@ -1,7 +1,5 @@
import io
import unittest
import json
import yaml
class TestDocs(unittest.TestCase):
@@ -25,7 +23,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_weights(self):
def test_generation_queued(self):
options = {
"Tester1":
{
@@ -42,19 +40,3 @@ 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,12 +1,24 @@
from .texture import FillType_Drawable, FillType_Vec, Texture
""" 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
class FillType_Shape(FillType_Drawable):
texture: Texture
texture: FillType_Texture
def __init__(self,
*,
texture: Texture = ...,
texture: FillType_Texture = ...,
pos: FillType_Vec = ...,
size: FillType_Vec = ...) -> None: ...
@@ -23,6 +35,6 @@ class Rectangle(FillType_Shape):
def __init__(self,
*,
source: str = ...,
texture: Texture = ...,
texture: FillType_Texture = ...,
pos: FillType_Vec = ...,
size: FillType_Vec = ...) -> None: ...

View File

@@ -1,13 +0,0 @@
""" 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

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

View File

@@ -79,8 +79,8 @@ class AutoWorldRegister(type):
if "options_dataclass" not in dct and "option_definitions" in dct:
# TODO - switch to deprecate after a version
if __debug__:
logging.warning(f"{name} Assigned options through option_definitions which is now deprecated. "
"Please use options_dataclass instead.")
from warnings import warn
warn("Assigning options through option_definitions is now deprecated. Use options_dataclass instead.")
dct["options_dataclass"] = make_dataclass(f"{name}Options", dct["option_definitions"].items(),
bases=(PerGameCommonOptions,))
@@ -328,7 +328,7 @@ class World(metaclass=AutoWorldRegister):
def create_items(self) -> None:
"""
Method for creating and submitting items to the itempool. Items and Regions must *not* be created and submitted
Method for creating and submitting items to the itempool. Items and Regions should *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

View File

@@ -26,13 +26,6 @@ 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,25 +7,16 @@ from ..AutoWorld import WebWorld, World
class Bk_SudokuWebWorld(WebWorld):
options_page = "games/Sudoku/info/en"
theme = 'partyTime'
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]
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']
)
]
class Bk_SudokuWorld(World):

View File

@@ -1,21 +0,0 @@
# 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

@@ -1,27 +0,0 @@
# 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,26 +11,16 @@ from .Rules import get_button_rule
class CliqueWebWorld(WebWorld):
theme = "partyTime"
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]
tutorials = [
Tutorial(
tutorial_name="Start Guide",
description="A guide to playing Clique.",
language="English",
file_name="guide_en.md",
link="guide/en",
authors=["Phar"]
)
]
class CliqueWorld(World):

View File

@@ -1,18 +0,0 @@
# 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

@@ -1,25 +0,0 @@
# 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,23 +13,14 @@ client_version = 0
class DLCqwebworld(WebWorld):
setup_en = Tutorial(
tutorials = [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

@@ -1,49 +0,0 @@
# 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

@@ -1,55 +0,0 @@
# # 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 in (b'\x00', b'\x55') or check_2 in (b'\x00', b'\x55'):
if check_1 == b'\x00' or check_2 == b'\x00':
return
def get_range(data_range):

View File

@@ -223,6 +223,11 @@ 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,8 +821,7 @@ class KH2Context(CommonContext):
def finishedGame(ctx: KH2Context, message):
if ctx.kh2slotdata['FinalXemnas'] == 1:
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:
if not ctx.final_xemnas and ctx.kh2_loc_name_to_id[LocationName.FinalXemnas] in ctx.locations_checked:
ctx.final_xemnas = True
# three proofs
if ctx.kh2slotdata['Goal'] == 0:

View File

@@ -2,7 +2,22 @@ import typing
from BaseClasses import Item
from .Names import ItemName
from .Subclasses import ItemData
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
# 0x130000
Reports_Table = {
@@ -194,7 +209,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),
@@ -334,7 +349,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,9 +1,19 @@
import typing
from BaseClasses import Location
from .Names import LocationName, ItemName, RegionName
from .Subclasses import LocationData
from .Regions import KH2REGIONS
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
# data's addrcheck sys3 addr obtained roomid bit index is eventid
LoD_Checks = {
@@ -531,7 +541,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"),
}
@@ -796,75 +806,74 @@ 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.FinalXemnasEventLocation: ItemName.Victory,
LocationName.DataMarluxiaEventLocation: ItemName.DataMarluxiaEvent,
LocationName.TerraEventLocation: ItemName.TerraEvent,
LocationName.TwilightThornEventLocation: ItemName.TwilightThornEvent,
LocationName.Axel1EventLocation: ItemName.Axel1Event,
LocationName.Axel2EventLocation: ItemName.Axel2Event,
LocationName.DataRoxasEventLocation: ItemName.DataRoxasEvent,
}
all_weapon_slot = {
LocationName.FAKESlot,
@@ -1352,9 +1361,3 @@ 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 . import default_itempool_option
from worlds.kh2 import default_itempool_option
class SoraEXP(Range):

View File

@@ -1,11 +1,9 @@
import typing
from BaseClasses import MultiWorld, Region
from . import Locations
from .Subclasses import KH2Location
from .Names import LocationName, RegionName
from .Items import Events_Table
from .Locations import KH2Location, event_location_to_item
from . import LocationName, RegionName, Events_Table
KH2REGIONS: typing.Dict[str, typing.List[str]] = {
"Menu": [],
@@ -790,7 +788,7 @@ KH2REGIONS: typing.Dict[str, typing.List[str]] = {
LocationName.ArmoredXemnas2EventLocation
],
RegionName.FinalXemnas: [
LocationName.FinalXemnasEventLocation
LocationName.FinalXemnas
],
RegionName.DataXemnas: [
LocationName.XemnasDataPowerBoost,
@@ -1022,8 +1020,7 @@ 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 Locations.event_location_to_item.items():
for location, item in 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, SupportAbility_Table
from .Items import exclusion_item_table, visit_locking_dict, DonaldAbility_Table, GoofyAbility_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,8 +83,6 @@ 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:
@@ -226,7 +224,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),
@@ -268,11 +266,9 @@ 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.FinalXemnasEventLocation, self.player)
final_xemnas_location = self.multiworld.get_location(LocationName.FinalXemnas, 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]:
@@ -421,7 +417,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) and state.has(ItemName.ProofofConnection, self.player),
RegionName.Terra: lambda state: self.get_terra_rules(state),
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(),

View File

@@ -1,29 +0,0 @@
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,7 +12,6 @@ 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():
@@ -50,9 +49,7 @@ 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]
@@ -256,8 +253,11 @@ class KH2World(World):
self.goofy_gen_early()
self.keyblade_gen_early()
# final xemnas isn't a location anymore
# self.total_locations -= 1
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
if self.options.WeaponSlotStartHint:
for location in all_weapon_slot:

View File

@@ -399,26 +399,6 @@ 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.
@@ -464,7 +444,6 @@ 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](/games/Landstalker%20-%20The%20Treasures%20of%20King%20Nole/player-settings) on the website allows
you to easily configure your personal settings
The [Player Settings Page](../player-settings) on the website allows you to easily configure your personal settings
and export a config file from them.
## How-to-play

View File

@@ -1118,13 +1118,7 @@
id: Cross Room/Panel_north_missing
colors: green
tag: forbid
required_panel:
- room: Outside The Bold
panel: MOUTH
- room: Outside The Bold
panel: YEAST
- room: Outside The Bold
panel: WET
required_room: Outside The Bold
DIAMONDS:
id: Cross Room/Panel_diamonds_missing
colors: green
@@ -2641,6 +2635,12 @@
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,53 +2651,31 @@
id: Countdown Panels/Panel_colorful_colorful
check: True
tag: forbid
required_panel:
required_door:
- room: The Colorful (White)
panel: BEGIN
door: Progress Door
- room: The Colorful (Black)
panel: FOUND
door: Progress Door
- room: The Colorful (Red)
panel: LOAF
door: Progress Door
- room: The Colorful (Yellow)
panel: CREAM
door: Progress Door
- room: The Colorful (Blue)
panel: SUN
door: Progress Door
- room: The Colorful (Purple)
panel: SPOON
door: Progress Door
- room: The Colorful (Orange)
panel: LETTERS
door: Progress Door
- room: The Colorful (Green)
panel: WALLS
door: Progress Door
- room: The Colorful (Brown)
panel: IRON
door: Progress Door
- room: The Colorful (Gray)
panel: OBSTACLE
door: Progress Door
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:
@@ -4224,6 +4202,9 @@
SIX:
id: Backside Room/Panel_six_six_5
tag: midwhite
colors:
- red
- yellow
hunt: True
required_door:
room: Number Hunt
@@ -4299,6 +4280,9 @@
SIX:
id: Backside Room/Panel_six_six_6
tag: midwhite
colors:
- red
- yellow
hunt: True
required_door:
room: Number Hunt
@@ -4420,14 +4404,9 @@
colors: blue
tag: forbid
required_panel:
- room: The Bearer (West)
panel: SMILE
- room: Outside The Bold
panel: MOUTH
- room: Outside The Bold
panel: YEAST
- room: Outside The Bold
panel: WET
room: The Bearer (West)
panel: SMILE
required_room: Outside The Bold
Cross Tower (South):
entrances: # No roof access
The Bearer (North):

View File

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

View File

@@ -28,10 +28,6 @@ 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":
@@ -74,8 +70,6 @@ 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,13 +21,6 @@ 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.
@@ -124,7 +117,6 @@ 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,8 +83,7 @@ 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)\
or (room_name == "The Colorful" and not world.options.progressive_colorful):
if room_name == "Orange Tower" and not world.options.progressive_orange_tower:
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
@@ -224,7 +223,7 @@ class LingoPlayerLogic:
"kind of logic error.")
if door_shuffle != ShuffleDoors.option_none and location_classification != LocationClassification.insanity \
and not early_color_hallways:
and not early_color_hallways is False:
# 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,7 +34,6 @@ class MuseDashCollections:
"Rush-Hour",
"Find this Month's Featured Playlist",
"PeroPero in the Universe",
"umpopoff"
]
album_items: Dict[str, AlbumData] = {}
@@ -82,22 +81,11 @@ class MuseDashCollections:
steamer_mode = sections[3] == "True"
if song_name in self.DIFF_OVERRIDES:
# 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
# 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
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|11
Monosugoi Space Shuttle de Koishi ga Monosugoi uta|55-2|Touhou Mugakudan -2-|False|3|5|7|
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,12 +501,4 @@ 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|
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|
Swing Sweet Twee Dance|68-5|Gambler's Tricks|False|4|7|10|

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 = 508 # Note will probably not reach this high if any other settings are done.
range_end = 500 # Note will probably not reach this high if any other settings are done.
default = 40
display_name = "Additional Song Count"

View File

@@ -328,6 +328,5 @@ 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,
"hasFiller": True,
"gradeNeeded": self.options.grade_needed.value
}

View File

@@ -66,11 +66,5 @@ class DifficultyRanges(MuseDashTestBase):
for song_name in muse_dash_world.md_collection.DIFF_OVERRIDES:
song = muse_dash_world.md_collection.song_items[song_name]
# 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,
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.")

42
worlds/noita/Events.py Normal file
View File

@@ -0,0 +1,42 @@
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,14 +1,9 @@
import itertools
from collections import Counter
from typing import Dict, List, NamedTuple, Set, TYPE_CHECKING
from typing import Dict, List, NamedTuple, Set
from BaseClasses import Item, ItemClassification
from .options import BossesAsChecks, VictoryCondition, ExtraOrbs
if TYPE_CHECKING:
from . import NoitaWorld
else:
NoitaWorld = object
from BaseClasses import Item, ItemClassification, MultiWorld
from .Options import BossesAsChecks, VictoryCondition, ExtraOrbs
class ItemData(NamedTuple):
@@ -49,40 +44,39 @@ def create_kantele(victory_condition: VictoryCondition) -> List[str]:
return ["Kantele"] if victory_condition.value >= VictoryCondition.option_pure_ending else []
def create_random_items(world: NoitaWorld, weights: Dict[str, int], count: int) -> List[str]:
def create_random_items(multiworld: MultiWorld, player: int, weights: Dict[str, int], count: int) -> List[str]:
filler_pool = weights.copy()
if not world.options.bad_effects:
if multiworld.bad_effects[player].value == 0:
del filler_pool["Trap"]
return world.random.choices(population=list(filler_pool.keys()),
weights=list(filler_pool.values()),
k=count)
return multiworld.random.choices(population=list(filler_pool.keys()),
weights=list(filler_pool.values()),
k=count)
def create_all_items(world: NoitaWorld) -> None:
player = world.player
locations_to_fill = len(world.multiworld.get_unfilled_locations(player))
def create_all_items(multiworld: MultiWorld, player: int) -> None:
locations_to_fill = len(multiworld.get_unfilled_locations(player))
itempool = (
create_fixed_item_pool()
+ 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)
+ 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])
)
# 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(world, shop_only_filler_weights, 39 - len(itempool))
itempool += create_random_items(multiworld, player, 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 world.multiworld.players == 1:
for location in world.multiworld.get_unfilled_locations(player):
if multiworld.players == 1:
for location in multiworld.get_unfilled_locations(player):
if "Shop Item" in location.name:
location.item = create_item(player, itempool.pop())
locations_to_fill = len(world.multiworld.get_unfilled_locations(player))
locations_to_fill = len(multiworld.get_unfilled_locations(player))
itempool += create_random_items(world, filler_weights, locations_to_fill - len(itempool))
world.multiworld.itempool += [create_item(player, name) for name in itempool]
itempool += create_random_items(multiworld, player, filler_weights, locations_to_fill - len(itempool))
multiworld.itempool += [create_item(player, name) for name in itempool]
# 110000 - 110032

View File

@@ -201,10 +201,11 @@ location_region_mapping: Dict[str, Dict[str, LocationData]] = {
}
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)}
# 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}
location_name_groups: Dict[str, Set[str]] = {"shop": set(), "orb": set(), "boss": set(), "chest": set(),
@@ -214,11 +215,9 @@ location_name_to_id: Dict[str, int] = {}
for location_group in location_region_mapping.values():
for locname, locinfo in location_group.items():
# 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}
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)

View File

@@ -1,5 +1,5 @@
from Options import Choice, DeathLink, DefaultOnToggle, Range, StartInventoryPool, PerGameCommonOptions
from dataclasses import dataclass
from typing import Dict
from Options import AssembleOptions, Choice, DeathLink, DefaultOnToggle, Range, StartInventoryPool
class PathOption(Choice):
@@ -99,16 +99,16 @@ class ShopPrice(Choice):
default = 100
@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
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,
}

View File

@@ -1,43 +1,48 @@
# Regions are areas in your game that you travel to.
from typing import Dict, List, TYPE_CHECKING
from typing import Dict, Set, List
from BaseClasses import Entrance, Region
from . import locations
from .events import create_all_events
if TYPE_CHECKING:
from . import NoitaWorld
from BaseClasses import Entrance, MultiWorld, Region
from . import Locations
def create_locations(world: "NoitaWorld", region: Region) -> None:
locs = locations.location_region_mapping.get(region.name, {})
for location_name, location_data in locs.items():
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():
location_type = location_data.ltype
flag = location_data.flag
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
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
region.add_locations(locations.make_location_range(location_name, location_data.id, amount),
locations.NoitaLocation)
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)
# Creates a new Region with the locations found in `location_region_mapping` and adds them to the world.
def create_region(world: "NoitaWorld", region_name: str) -> Region:
new_region = Region(region_name, world.player, world.multiworld)
create_locations(world, new_region)
def create_region(multiworld: MultiWorld, player: int, region_name: str) -> Region:
new_region = Region(region_name, player, multiworld)
add_locations(multiworld, player, new_region)
return new_region
def create_regions(world: "NoitaWorld") -> Dict[str, Region]:
return {name: create_region(world, name) for name in noita_regions}
def create_regions(multiworld: MultiWorld, player: int) -> Dict[str, Region]:
return {name: create_region(multiworld, player, name) for name in noita_regions}
# An "Entrance" is really just a connection between two regions
@@ -55,12 +60,11 @@ def create_connections(player: int, regions: Dict[str, Region]) -> None:
# Creates all regions and connections. Called from NoitaWorld.
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)
def create_all_regions_and_connections(multiworld: MultiWorld, player: int) -> None:
created_regions = create_regions(multiworld, player)
create_connections(player, created_regions)
world.multiworld.regions += created_regions.values()
multiworld.regions += created_regions.values()
# Oh, what a tangled web we weave

166
worlds/noita/Rules.py Normal file
View File

@@ -0,0 +1,166 @@
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,8 +1,6 @@
from BaseClasses import Item, Tutorial
from worlds.AutoWorld import WebWorld, World
from typing import Dict, Any
from . import events, items, locations, regions, rules
from .options import NoitaOptions
from . import Events, Items, Locations, Options, Regions, Rules
class NoitaWeb(WebWorld):
@@ -26,14 +24,13 @@ class NoitaWorld(World):
"""
game = "Noita"
options: NoitaOptions
options_dataclass = NoitaOptions
option_definitions = Options.noita_options
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()
@@ -43,21 +40,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) -> 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 fill_slot_data(self):
return {name: getattr(self.multiworld, name)[self.player].value for name in self.option_definitions}
def create_regions(self) -> None:
regions.create_all_regions_and_connections(self)
Regions.create_all_regions_and_connections(self.multiworld, self.player)
Events.create_all_events(self.multiworld, self.player)
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)
Items.create_all_items(self.multiworld, self.player)
def set_rules(self) -> None:
rules.create_all_rules(self)
Rules.create_all_rules(self.multiworld, self.player)
def get_filler_item_name(self) -> str:
return self.random.choice(items.filler_items)
return self.multiworld.random.choice(Items.filler_items)

View File

@@ -1,43 +0,0 @@
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,172 +0,0 @@
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 = tuple(normalized_name_tricks.keys())
valid_keys = frozenset(normalized_name_tricks)
valid_keys_casefold = True

View File

@@ -118,16 +118,7 @@ class OOTWeb(WebWorld):
["TheLynk"]
)
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]
tutorials = [setup, setup_es, setup_fr]
class OOTWorld(World):

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

View File

@@ -1,41 +0,0 @@
# 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.

View File

@@ -1,108 +0,0 @@
# 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, LocationProgressType
from BaseClasses import ItemClassification, MultiWorld, Tutorial
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 (Goal, ItemPoolType, RandomizeWildPokemon, RandomizeBadges, RandomizeTrainerParties, RandomizeHms,
from .options import (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,60 +146,6 @@ 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,9 +353,7 @@ class PokemonRedBlueWorld(World):
location.show_in_spoiler = False
def intervene(move, test_state):
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:
if self.multiworld.randomize_wild_pokemon[self.player]:
accessible_slots = [loc for loc in self.multiworld.get_reachable_locations(test_state, self.player) if
loc.type == "Wild Encounter"]
@@ -365,6 +363,8 @@ 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,6 +3,7 @@ from __future__ import annotations
import asyncio
import copy
import ctypes
import json
import logging
import multiprocessing
import os.path
@@ -14,7 +15,6 @@ import queue
import zipfile
import io
import random
import concurrent.futures
from pathlib import Path
# CommonClient import first to trigger ModuleUpdater
@@ -42,7 +42,6 @@ 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
@@ -211,11 +210,6 @@ 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()
@@ -226,7 +220,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: (beths_body_available(state, player) and state.can_reach("Underground Lake", "Region", player))
"Final Riddle: Guillotine Dropped": lambda state: state.can_reach("Underground Lake", "Region", player)
},
"elevators": {
"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 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 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 Office Elevator",
"Puzzle Solved Underground 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 Office Elevator",
"Puzzle Solved Underground 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 Office Elevator",
"Puzzle Solved Underground Elevator",
"Puzzle Solved Three Floor Elevator",
"Puzzle Hint Found: Combo Lock in Mailbox",
"Puzzle Hint Found: Orange Symbol",

View File

@@ -40,7 +40,6 @@ 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
@@ -54,22 +53,6 @@ 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):
@@ -86,7 +69,7 @@ class SMZ3SNIClient(SNIClient):
ctx.finished_game = True
return
data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + send_progress_addr_ptr_offset, 4)
data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD3C, 4)
if data is None:
return
@@ -94,14 +77,14 @@ class SMZ3SNIClient(SNIClient):
recv_item = data[2] | (data[3] << 8)
while (recv_index < recv_item):
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)
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)
recv_index += 1
snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + send_progress_addr_ptr_offset, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD3C, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
from .TotalSMZ3.Location import locations_start_id
from . import convertLocSMZ3IDToAPID
@@ -112,7 +95,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 + recv_progress_addr_ptr_offset, 4)
data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD36, 4)
if data is None:
return
@@ -124,12 +107,9 @@ 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 * 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]))
snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + item_out_ptr * 2, bytes([player_id, item_id]))
item_out_ptr += 1
snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + recv_progress_addr_table_offset, bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF]))
snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD38, 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,8 +616,7 @@ class Patch:
"H" if self.myWorld.Config.SMLogic == Config.SMLogic.Hard else \
"X"
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.title = f"ZSM{Patch.Major}{Patch.Minor}{Patch.Patch}{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')))

70
worlds/soe/Logic.py Normal file
View File

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

@@ -1,18 +1,16 @@
from dataclasses import dataclass, fields
from typing import Any, cast, Dict, Iterator, List, Tuple, Protocol
import typing
from Options import AssembleOptions, Choice, DeathLink, DefaultOnToggle, Option, PerGameCommonOptions, \
ProgressionBalancing, Range, Toggle
from Options import Range, Choice, Toggle, DefaultOnToggle, AssembleOptions, DeathLink, ProgressionBalancing
# typing boilerplate
class FlagsProtocol(Protocol):
class FlagsProtocol(typing.Protocol):
value: int
default: int
flags: List[str]
flags: typing.List[str]
class FlagProtocol(Protocol):
class FlagProtocol(typing.Protocol):
value: int
default: int
flag: str
@@ -20,7 +18,7 @@ class FlagProtocol(Protocol):
# meta options
class EvermizerFlags:
flags: List[str]
flags: typing.List[str]
def to_flag(self: FlagsProtocol) -> str:
return self.flags[self.value]
@@ -202,13 +200,13 @@ class TrapCount(Range):
# more meta options
class ItemChanceMeta(AssembleOptions):
def __new__(mcs, name: str, bases: Tuple[type], attrs: Dict[Any, Any]) -> "ItemChanceMeta":
def __new__(mcs, name, bases, attrs):
if 'item_name' in attrs:
attrs["display_name"] = f"{attrs['item_name']} Chance"
attrs["range_start"] = 0
attrs["range_end"] = 100
cls = super(ItemChanceMeta, mcs).__new__(mcs, name, bases, attrs)
return cast(ItemChanceMeta, cls)
return super(ItemChanceMeta, mcs).__new__(mcs, name, bases, attrs)
class TrapChance(Range, metaclass=ItemChanceMeta):
@@ -249,52 +247,33 @@ class SoEProgressionBalancing(ProgressionBalancing):
special_range_names = {**ProgressionBalancing.special_range_names, "normal": default}
# 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
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,
}

View File

@@ -1,5 +1,5 @@
import os
from typing import BinaryIO, Optional
from typing import 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: BinaryIO, strip_header: bool = True) -> bytes:
def read_rom(stream, strip_header=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: BinaryIO, strip_header: bool = 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

@@ -4,23 +4,18 @@ 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 .logic import SoEPlayerLogic
from .options import Difficulty, EnergyCore, SoEOptions
from .patch import SoEDeltaPatch, get_base_rom_path
from BaseClasses import Entrance, Item, ItemClassification, Location, LocationProgressType, Region, Tutorial
from Utils import output_path
if typing.TYPE_CHECKING:
from BaseClasses import MultiWorld, CollectionState
__all__ = ["pyevermizer", "SoEWorld"]
import pyevermizer # from package
# from . import pyevermizer # as part of the source tree
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:
@@ -29,17 +24,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()
@@ -81,7 +76,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 Medallion',
'Gunpowder', 'Grease', 'Feather', 'Ethanol', 'Dry Ice', 'Crystal', 'Clay', 'Brimstone', 'Bone', 'Atlas Amulet',
'Ash', 'Acorn'
)
_other_items = (
@@ -89,8 +84,8 @@ _other_items = (
)
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
def _match_item_name(item, substr: str) -> bool:
sub = item.name.split(' ', 1)[1] if item.name[0].isdigit() else item.name
return sub == substr or sub == substr+'s'
@@ -161,11 +156,10 @@ 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: typing.ClassVar[str] = "Secret of Evermore"
options_dataclass = SoEOptions
options: SoEOptions
game: str = "Secret of Evermore"
option_definitions = soe_options
settings: typing.ClassVar[SoESettings]
topology_present = False
data_version = 4
@@ -176,21 +170,31 @@ class SoEWorld(World):
location_name_to_id, location_id_to_raw = _get_location_mapping()
item_name_groups = _get_item_grouping()
logic: SoEPlayerLogic
trap_types = [name[12:] for name in option_definitions if name.startswith('trap_chance_')]
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, multiworld: "MultiWorld", player: int):
def __init__(self, *args, **kwargs):
self.connect_name_available_event = threading.Event()
super(SoEWorld, self).__init__(multiworld, player)
super(SoEWorld, self).__init__(*args, **kwargs)
def generate_early(self) -> None:
# 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)
# 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
def create_event(self, event: str) -> Item:
return SoEItem(event, ItemClassification.progression, None, self.player)
@@ -210,20 +214,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") -> None:
def stage_assert_generate(cls, multiworld):
rom_file = get_base_rom_path()
if not os.path.exists(rom_file):
raise FileNotFoundError(rom_file)
def create_regions(self) -> None:
def create_regions(self):
# exclude 'hidden' on easy
max_difficulty = 1 if self.options.difficulty == Difficulty.option_easy else 256
max_difficulty = 1 if self.multiworld.difficulty[self.player] == 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: pyevermizer.Location) -> int:
def get_sphere_index(evermizer_loc):
"""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
@@ -248,18 +252,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.options.difficulty.value]
for location in self.random.sample(spheres[trash_sphere][typ], count):
count = counts[self.multiworld.difficulty[self.player].value]
for location in self.multiworld.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: pyevermizer.Item) -> bool:
def sphere1_blocked_items_rule(item):
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.options.difficulty not in (Difficulty.option_easy, Difficulty.option_mystery):
if self.multiworld.difficulty[item.player] not in (Difficulty.option_easy, Difficulty.option_mystery):
if item.name in {"Laser Lance", "Atom Smasher", "Diamond Eye"}:
return False
return True
@@ -269,13 +273,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.options.difficulty == Difficulty.option_mystery:
late_count = self.random.randint(0, 2)
if self.multiworld.difficulty[self.player] == Difficulty.option_mystery:
late_count = self.multiworld.random.randint(0, 2)
else:
late_count = self.options.difficulty.value
late_count = self.multiworld.difficulty[self.player].value
late_bosses = ("Tiny", "Aquagoth", "Megataur", "Rimsala",
"Mungola", "Lightning Storm", "Magmar", "Volcano Viper")
late_locations = self.random.sample(late_bosses, late_count)
late_locations = self.multiworld.random.sample(late_bosses, late_count)
# add locations to the world
for sphere in spheres.values():
@@ -289,17 +293,17 @@ class SoEWorld(World):
menu.connect(ingame, "New Game")
self.multiworld.regions += [ingame]
def create_items(self) -> None:
def create_items(self):
# add regular items to the pool
exclusions: typing.List[str] = []
if self.options.energy_core != EnergyCore.option_shuffle:
if self.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: pyevermizer.Item) -> bool:
def is_ingredient(item):
for ingredient in _ingredients:
if _match_item_name(item, ingredient):
return True
@@ -307,72 +311,84 @@ class SoEWorld(World):
# add energy core fragments to the pool
ingredients = [n for n, item in enumerate(items) if is_ingredient(item)]
if self.options.energy_core == EnergyCore.option_fragments:
if self.energy_core == EnergyCore.option_fragments:
items.append(self.create_item("Energy Core Fragment")) # replaces the vanilla energy core
for _ in range(self.options.available_fragments - 1):
for _ in range(self.available_fragments - 1):
if len(ingredients) < 1:
break # out of ingredients to replace
r = self.random.choice(ingredients)
r = self.multiworld.random.choice(ingredients)
ingredients.remove(r)
items[r] = self.create_item("Energy Core Fragment")
# add traps to the pool
trap_count = self.options.trap_count.value
trap_names: typing.List[str] = []
trap_weights: typing.List[int] = []
trap_count = self.multiworld.trap_count[self.player].value
trap_chances = {}
trap_names = {}
if trap_count > 0:
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]
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)
def create_trap() -> Item:
return self.create_item(self.random.choices(trap_names, trap_weights)[0])
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"
for _ in range(trap_count):
if len(ingredients) < 1:
break # out of ingredients to replace
r = self.random.choice(ingredients)
r = self.multiworld.random.choice(ingredients)
ingredients.remove(r)
items[r] = create_trap()
self.multiworld.itempool += items
def set_rules(self) -> None:
def set_rules(self):
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: self.logic.has(state, pyevermizer.P_FINAL_BOSS))
lambda state: state.soe_has(pyevermizer.P_FINAL_BOSS, self.multiworld, self.player))
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: "CollectionState") -> bool:
def rule(state) -> bool:
for count, progress in requires:
if not self.logic.has(state, progress, count):
if not state.soe_has(progress, self.multiworld, self.player, count):
return False
return True
return rule
def generate_basic(self) -> None:
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):
# 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.random.choice(self._halls_ne_chest_names)
wings_location = self.multiworld.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.options.energy_core == EnergyCore.option_vanilla:
if self.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.random.randint(0, 2 ** 16 - 1) # TODO: make this an option for "full" plando?
self.evermizer_seed = self.multiworld.random.randint(0, 2 ** 16 - 1) # TODO: make this an option for "full" plando?
def generate_output(self, output_directory: str) -> None:
def generate_output(self, output_directory: str):
player_name = self.multiworld.get_player_name(self.player)
self.connect_name = player_name[:32]
while len(self.connect_name.encode('utf-8')) > 32:
@@ -381,21 +397,24 @@ class SoEWorld(World):
placement_file = ""
out_file = ""
try:
money = self.options.money_modifier.value
exp = self.options.exp_modifier.value
money = self.multiworld.money_modifier[self.player].value
exp = self.multiworld.exp_modifier[self.player].value
switches: typing.List[str] = []
if self.options.death_link.value:
if self.multiworld.death_link[self.player].value:
switches.append("--death-link")
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)))
if self.energy_core == EnergyCore.option_fragments:
switches.extend(('--available-fragments', str(self.available_fragments),
'--required-fragments', str(self.required_fragments)))
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
flags += self.options.flags
for option_name in self.option_definitions:
option = getattr(self.multiworld, option_name)[self.player]
if hasattr(option, 'to_flag'):
flags += option.to_flag()
with open(placement_file, "wb") as f: # generate placement file
for location in self.multiworld.get_locations(self.player):
@@ -429,7 +448,7 @@ class SoEWorld(World):
except FileNotFoundError:
pass
def modify_multidata(self, multidata: typing.Dict[str, typing.Any]) -> None:
def modify_multidata(self, multidata: dict):
# 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
@@ -438,7 +457,7 @@ class SoEWorld(World):
multidata["connect_names"][self.connect_name] = payload
def get_filler_item_name(self) -> str:
return self.random.choice(list(self.item_name_groups["Ingredients"]))
return self.multiworld.random.choice(list(self.item_name_groups["Ingredients"]))
class SoEItem(Item):

View File

@@ -1,85 +0,0 @@
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,4 +1,4 @@
from test.bases import WorldTestBase
from test.TestBase 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: bool = True) -> None:
satisfied=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,14 +18,3 @@ 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.Mapping[str, typing.Iterable[int]]) -> typing.List[str]:
def _resolveGourds(gourds: typing.Dict[str, typing.Iterable[int]]):
return [f"{name} #{number}" for name, numbers in gourds.items() for number in numbers]
def test_bronze_axe(self) -> None:
def testBronzeAxe(self):
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 test_bronze_spear_plus(self) -> None:
def testBronzeSpearPlus(self):
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 test_fragments(self) -> None:
def testFragments(self):
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 test_no_weapon(self) -> None:
def testNoWeapon(self):
self.collect_by_name(["Diamond Eye", "Wheel", "Gauge", "Energy Core Fragment"])
self.assertBeatable(False)
def test_no_rocket(self) -> None:
def testNoRocket(self):
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 test_core(self) -> None:
def testCore(self):
self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"])
self.assertBeatable(False)
self.collect_by_name(["Energy Core"])
self.assertBeatable(True)
def test_no_weapon(self) -> None:
def testNoWeapon(self):
self.collect_by_name(["Diamond Eye", "Wheel", "Gauge", "Energy Core"])
self.assertBeatable(False)
def test_no_rocket(self) -> None:
def testNoRocket(self):
self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Energy Core"])
self.assertBeatable(False)

View File

@@ -1,21 +0,0 @@
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 test_oob_access(self) -> None:
def testOoBAccess(self):
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 test_oob_goal(self) -> None:
def testOoBGoal(self):
# 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 test_sequence_breaks_access(self) -> None:
def testSequenceBreaksAccess(self):
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 test_sequence_breaks_goal(self) -> None:
def testSequenceBreaksGoal(self):
in_logic = self.options["sequence_breaks"] == "logic"
# don't need Energy Core with sequence breaks in logic

View File

@@ -1,56 +0,0 @@
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,8 +170,6 @@ 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

@@ -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.options.non_local_items]
if weapon not in world.multiworld.non_local_items[world.player]]
if not final_starting_weapons:
final_starting_weapons = starting_weapons
starting_weapon = random.choice(final_starting_weapons)
if world.options.StartingPosition == StartingPosition.option_safe:
if world.multiworld.StartingPosition[world.player] == StartingPosition.option_safe:
placed_items[start_weapon_locations[0]] = starting_weapon
elif world.options.StartingPosition in \
elif world.multiworld.StartingPosition[world.player] in \
[StartingPosition.option_unsafe, StartingPosition.option_dangerous]:
if world.options.StartingPosition == StartingPosition.option_dangerous:
if world.multiworld.StartingPosition[world.player] == StartingPosition.option_dangerous:
for location in dangerous_weapon_locations:
if world.options.ExpandedPool or "Drop" not in location:
if world.multiworld.ExpandedPool[world.player] 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.options.ExpandedPool:
if world.multiworld.ExpandedPool[world.player]:
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.options.TriforceLocations == TriforceLocations.option_vanilla:
if world.multiworld.TriforceLocations[world.player] == TriforceLocations.option_vanilla:
placed_items[f"Level {level} Triforce"] = fragment
elif world.options.TriforceLocations == TriforceLocations.option_dungeons:
elif world.multiworld.TriforceLocations[world.player] == 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.options.ExpandedPool > 0:
if world.multiworld.ExpandedPool[world.player] > 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.options.ExpandedPool:
if world.multiworld.ExpandedPool[world.player]:
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,6 +1,5 @@
import typing
from dataclasses import dataclass
from Options import Option, DefaultOnToggle, Choice, PerGameCommonOptions
from Options import Option, DefaultOnToggle, Choice
class ExpandedPool(DefaultOnToggle):
@@ -33,8 +32,9 @@ class StartingPosition(Choice):
option_dangerous = 2
option_very_dangerous = 3
@dataclass
class TlozOptions(PerGameCommonOptions):
ExpandedPool: ExpandedPool
TriforceLocations: TriforceLocations
StartingPosition: StartingPosition
tloz_options: typing.Dict[str, type(Option)] = {
"ExpandedPool": ExpandedPool,
"TriforceLocations": TriforceLocations,
"StartingPosition": StartingPosition
}

View File

@@ -11,7 +11,6 @@ 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):
@@ -24,7 +23,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 options.StartingPosition < StartingPosition.option_dangerous \
if world.StartingPosition[player] < 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))
@@ -67,7 +66,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 options.ExpandedPool:
if world.ExpandedPool[player]:
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),
@@ -76,13 +75,13 @@ def set_rules(tloz_world: "TLoZWorld"):
lambda state: state.has("Recorder", player))
for location in food_locations:
if options.ExpandedPool or "Drop" not in location:
if world.ExpandedPool[player] 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 options.ExpandedPool:
if world.ExpandedPool[player]:
add_rule(world.get_location("Level 8 Bomb Drop (Darknuts North)", player),
lambda state: state.has("Bow", player) and state.has_group("arrows", player))
@@ -107,13 +106,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 options.ExpandedPool:
if world.ExpandedPool[player]:
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 options.ExpandedPool:
if world.ExpandedPool[player]:
add_rule(world.get_location("Take Any Item Left", player),
lambda state: state.has_group("candles", player) or
state.has("Raft", player))

View File

@@ -13,7 +13,7 @@ from .ItemPool import generate_itempool, starting_weapons, dangerous_weapon_loca
from .Items import item_table, item_prices, item_game_ids
from .Locations import location_table, level_locations, major_locations, shop_locations, all_level_locations, \
standard_level_locations, shop_price_location_ids, secret_money_ids, location_ids, food_locations
from .Options import TlozOptions
from .Options import tloz_options
from .Rom import TLoZDeltaPatch, get_base_rom_path, first_quest_dungeon_items_early, first_quest_dungeon_items_late
from .Rules import set_rules
from worlds.AutoWorld import World, WebWorld
@@ -63,8 +63,7 @@ class TLoZWorld(World):
This randomizer shuffles all the items in the game around, leading to a new adventure
every time.
"""
options_dataclass = TlozOptions
options: TlozOptions
option_definitions = tloz_options
settings: typing.ClassVar[TLoZSettings]
game = "The Legend of Zelda"
topology_present = False
@@ -133,7 +132,7 @@ class TLoZWorld(World):
for i, level in enumerate(level_locations):
for location in level:
if self.options.ExpandedPool or "Drop" not in location:
if self.multiworld.ExpandedPool[self.player] or "Drop" not in location:
self.levels[i + 1].locations.append(
self.create_location(location, self.location_name_to_id[location], self.levels[i + 1]))
@@ -145,7 +144,7 @@ class TLoZWorld(World):
self.levels[level].locations.append(boss_event)
for location in major_locations:
if self.options.ExpandedPool or "Take Any" not in location:
if self.multiworld.ExpandedPool[self.player] or "Take Any" not in location:
overworld.locations.append(
self.create_location(location, self.location_name_to_id[location], overworld))
@@ -312,7 +311,7 @@ class TLoZWorld(World):
return self.multiworld.random.choice(self.filler_items)
def fill_slot_data(self) -> Dict[str, Any]:
if self.options.ExpandedPool:
if self.multiworld.ExpandedPool[self.player]:
take_any_left = self.multiworld.get_location("Take Any Item Left", self.player).item
take_any_middle = self.multiworld.get_location("Take Any Item Middle", self.player).item
take_any_right = self.multiworld.get_location("Take Any Item Right", self.player).item

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