Compare commits

..

1 Commits

Author SHA1 Message Date
CaitSith2
3738399348 Factorio: Add an Allow Collect Option 2023-11-26 21:25:43 -08:00
97 changed files with 543 additions and 1097 deletions

View File

@@ -115,12 +115,11 @@ class AdventureContext(CommonContext):
msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}"
self._set_message(msg, SYSTEM_MESSAGE_ID)
elif cmd == "Retrieved":
if f"adventure_{self.auth}_freeincarnates_used" in args["keys"]:
self.freeincarnates_used = args["keys"][f"adventure_{self.auth}_freeincarnates_used"]
if self.freeincarnates_used is None:
self.freeincarnates_used = 0
self.freeincarnates_used += self.freeincarnate_pending
self.send_pending_freeincarnates()
self.freeincarnates_used = args["keys"][f"adventure_{self.auth}_freeincarnates_used"]
if self.freeincarnates_used is None:
self.freeincarnates_used = 0
self.freeincarnates_used += self.freeincarnate_pending
self.send_pending_freeincarnates()
elif cmd == "SetReply":
if args["key"] == f"adventure_{self.auth}_freeincarnates_used":
self.freeincarnates_used = args["value"]

View File

@@ -252,20 +252,15 @@ class MultiWorld():
range(1, self.players + 1)}
def set_options(self, args: Namespace) -> None:
# TODO - remove this section once all worlds use options dataclasses
all_keys: Set[str] = {key for player in self.player_ids for key in
AutoWorld.AutoWorldRegister.world_types[self.game[player]].options_dataclass.type_hints}
for option_key in all_keys:
option = Utils.DeprecateDict(f"Getting options from multiworld is now deprecated. "
f"Please use `self.options.{option_key}` instead.")
option.update(getattr(args, option_key, {}))
setattr(self, option_key, option)
for player in self.player_ids:
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
self.worlds[player] = world_type(self, player)
self.worlds[player].random = self.per_slot_randoms[player]
options_dataclass: typing.Type[Options.PerGameCommonOptions] = world_type.options_dataclass
for option_key in world_type.options_dataclass.type_hints:
option_values = getattr(args, option_key, {})
setattr(self, option_key, option_values)
# TODO - remove this loop once all worlds use options dataclasses
options_dataclass: typing.Type[Options.PerGameCommonOptions] = self.worlds[player].options_dataclass
self.worlds[player].options = options_dataclass(**{option_key: getattr(args, option_key)[player]
for option_key in options_dataclass.type_hints})
@@ -496,7 +491,7 @@ class MultiWorld():
else:
return all((self.has_beaten_game(state, p) for p in range(1, self.players + 1)))
def can_beat_game(self, starting_state: Optional[CollectionState] = None) -> bool:
def can_beat_game(self, starting_state: Optional[CollectionState] = None):
if starting_state:
if self.has_beaten_game(starting_state):
return True
@@ -509,7 +504,7 @@ class MultiWorld():
and location.item.advancement and location not in state.locations_checked}
while prog_locations:
sphere: Set[Location] = set()
sphere = set()
# build up spheres of collection radius.
# Everything in each sphere is independent from each other in dependencies and only depends on lower spheres
for location in prog_locations:
@@ -529,19 +524,12 @@ class MultiWorld():
return False
def get_spheres(self) -> Iterator[Set[Location]]:
"""
yields a set of locations for each logical sphere
If there are unreachable locations, the last sphere of reachable
locations is followed by an empty set, and then a set of all of the
unreachable locations.
"""
def get_spheres(self):
state = CollectionState(self)
locations = set(self.get_filled_locations())
while locations:
sphere: Set[Location] = set()
sphere = set()
for location in locations:
if location.can_reach(state):

41
Fill.py
View File

@@ -550,7 +550,7 @@ def flood_items(world: MultiWorld) -> None:
break
def balance_multiworld_progression(multiworld: MultiWorld) -> None:
def balance_multiworld_progression(world: MultiWorld) -> None:
# A system to reduce situations where players have no checks remaining, popularly known as "BK mode."
# Overall progression balancing algorithm:
# Gather up all locations in a sphere.
@@ -558,28 +558,28 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
# If other players are below the threshold value, swap progression in this sphere into earlier spheres,
# which gives more locations available by this sphere.
balanceable_players: typing.Dict[int, float] = {
player: multiworld.worlds[player].options.progression_balancing / 100
for player in multiworld.player_ids
if multiworld.worlds[player].options.progression_balancing > 0
player: world.worlds[player].options.progression_balancing / 100
for player in world.player_ids
if world.worlds[player].options.progression_balancing > 0
}
if not balanceable_players:
logging.info('Skipping multiworld progression balancing.')
else:
logging.info(f'Balancing multiworld progression for {len(balanceable_players)} Players.')
logging.debug(balanceable_players)
state: CollectionState = CollectionState(multiworld)
state: CollectionState = CollectionState(world)
checked_locations: typing.Set[Location] = set()
unchecked_locations: typing.Set[Location] = set(multiworld.get_locations())
unchecked_locations: typing.Set[Location] = set(world.get_locations())
total_locations_count: typing.Counter[int] = Counter(
location.player
for location in multiworld.get_locations()
for location in world.get_locations()
if not location.locked
)
reachable_locations_count: typing.Dict[int, int] = {
player: 0
for player in multiworld.player_ids
if total_locations_count[player] and len(multiworld.get_filled_locations(player)) != 0
for player in world.player_ids
if total_locations_count[player] and len(world.get_filled_locations(player)) != 0
}
balanceable_players = {
player: balanceable_players[player]
@@ -658,7 +658,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
balancing_unchecked_locations.remove(location)
if not location.locked:
balancing_reachables[location.player] += 1
if multiworld.has_beaten_game(balancing_state) or all(
if world.has_beaten_game(balancing_state) or all(
item_percentage(player, reachables) >= threshold_percentages[player]
for player, reachables in balancing_reachables.items()
if player in threshold_percentages):
@@ -675,7 +675,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
locations_to_test = unlocked_locations[player]
items_to_test = list(candidate_items[player])
items_to_test.sort()
multiworld.random.shuffle(items_to_test)
world.random.shuffle(items_to_test)
while items_to_test:
testing = items_to_test.pop()
reducing_state = state.copy()
@@ -687,8 +687,8 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
reducing_state.sweep_for_events(locations=locations_to_test)
if multiworld.has_beaten_game(balancing_state):
if not multiworld.has_beaten_game(reducing_state):
if world.has_beaten_game(balancing_state):
if not world.has_beaten_game(reducing_state):
items_to_replace.append(testing)
else:
reduced_sphere = get_sphere_locations(reducing_state, locations_to_test)
@@ -696,32 +696,33 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
if p < threshold_percentages[player]:
items_to_replace.append(testing)
old_moved_item_count = moved_item_count
replaced_items = False
# sort then shuffle to maintain deterministic behaviour,
# while allowing use of set for better algorithm growth behaviour elsewhere
replacement_locations = sorted(l for l in checked_locations if not l.event and not l.locked)
multiworld.random.shuffle(replacement_locations)
world.random.shuffle(replacement_locations)
items_to_replace.sort()
multiworld.random.shuffle(items_to_replace)
world.random.shuffle(items_to_replace)
# Start swapping items. Since we swap into earlier spheres, no need for accessibility checks.
while replacement_locations and items_to_replace:
old_location = items_to_replace.pop()
for i, new_location in enumerate(replacement_locations):
for new_location in replacement_locations:
if new_location.can_fill(state, old_location.item, False) and \
old_location.can_fill(state, new_location.item, False):
replacement_locations.pop(i)
replacement_locations.remove(new_location)
swap_location_item(old_location, new_location)
logging.debug(f"Progression balancing moved {new_location.item} to {new_location}, "
f"displacing {old_location.item} into {old_location}")
moved_item_count += 1
state.collect(new_location.item, True, new_location)
replaced_items = True
break
else:
logging.warning(f"Could not Progression Balance {old_location.item}")
if old_moved_item_count < moved_item_count:
if replaced_items:
logging.debug(f"Moved {moved_item_count} items so far\n")
unlocked = {fresh for player in balancing_players for fresh in unlocked_locations[player]}
for location in get_sphere_locations(state, unlocked):
@@ -735,7 +736,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
state.collect(location.item, True, location)
checked_locations |= sphere_locations
if multiworld.has_beaten_game(state):
if world.has_beaten_game(state):
break
elif not sphere_locations:
logging.warning("Progression Balancing ran out of paths.")

11
Main.py
View File

@@ -117,17 +117,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
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.
early = world.early_items[player].get(item_name, 0)
if early:
world.early_items[player][item_name] = max(0, early-count)
remaining_count = count-early
if remaining_count > 0:
local_early = world.early_local_items[player].get(item_name, 0)
if local_early:
world.early_items[player][item_name] = max(0, local_early - remaining_count)
del local_early
del early
logger.info('Creating World.')
AutoWorld.call_all(world, "create_regions")

View File

@@ -1033,6 +1033,11 @@ class DeathLink(Toggle):
display_name = "Death Link"
class AllowCollect(DefaultOnToggle):
"""Allows checks in your world to be automatically marked as collected when !collect is run."""
display_name = "Allow Collect"
class ItemLinks(OptionList):
"""Share part of your item pool with other players."""
display_name = "Item Links"

View File

@@ -779,25 +779,6 @@ def deprecate(message: str):
import warnings
warnings.warn(message)
class DeprecateDict(dict):
log_message: str
should_error: bool
def __init__(self, message, error: bool = False) -> None:
self.log_message = message
self.should_error = error
super().__init__()
def __getitem__(self, item: Any) -> Any:
if self.should_error:
deprecate(self.log_message)
elif __debug__:
import warnings
warnings.warn(self.log_message)
return super().__getitem__(item)
def _extend_freeze_support() -> None:
"""Extend multiprocessing.freeze_support() to also work on Non-Windows for spawn."""
# upstream issue: https://github.com/python/cpython/issues/76327

View File

@@ -1,4 +1,3 @@
import os
import zipfile
import base64
from typing import Union, Dict, Set, Tuple
@@ -7,7 +6,13 @@ from flask import request, flash, redirect, url_for, render_template
from markupsafe import Markup
from WebHostLib import app
from WebHostLib.upload import allowed_options, allowed_options_extensions, banned_file
banned_zip_contents = (".sfc",)
def allowed_file(filename):
return filename.endswith(('.txt', ".yaml", ".zip"))
from Generate import roll_settings, PlandoOptions
from Utils import parse_yamls
@@ -46,41 +51,33 @@ def mysterycheck():
def get_yaml_data(files) -> Union[Dict[str, str], str, Markup]:
options = {}
for uploaded_file in files:
if banned_file(uploaded_file.filename):
return ("Uploaded data contained a rom file, which is likely to contain copyrighted material. "
"Your file was deleted.")
# If the user does not select file, the browser will still submit an empty string without a file name.
elif uploaded_file.filename == "":
return "No selected file."
# if user does not select file, browser also
# submit an empty part without filename
if uploaded_file.filename == '':
return 'No selected file'
elif uploaded_file.filename in options:
return f"Conflicting files named {uploaded_file.filename} submitted."
elif uploaded_file and allowed_options(uploaded_file.filename):
return f'Conflicting files named {uploaded_file.filename} submitted'
elif uploaded_file and allowed_file(uploaded_file.filename):
if uploaded_file.filename.endswith(".zip"):
if not zipfile.is_zipfile(uploaded_file):
return f"Uploaded file {uploaded_file.filename} is not a valid .zip file and cannot be opened."
uploaded_file.seek(0) # offset from is_zipfile check
with zipfile.ZipFile(uploaded_file, "r") as zfile:
for file in zfile.infolist():
# Remove folder pathing from str (e.g. "__MACOSX/" folder paths from archives created by macOS).
base_filename = os.path.basename(file.filename)
with zipfile.ZipFile(uploaded_file, 'r') as zfile:
infolist = zfile.infolist()
if base_filename.endswith(".archipelago"):
return Markup("Error: Your .zip file contains an .archipelago file. "
'Did you mean to <a href="/uploads">host a game</a>?')
elif base_filename.endswith(".zip"):
return "Nested .zip files inside a .zip are not supported."
elif banned_file(base_filename):
return ("Uploaded data contained a rom file, which is likely to contain copyrighted "
"material. Your file was deleted.")
# Ignore dot-files.
elif not base_filename.startswith(".") and allowed_options(base_filename):
if any(file.filename.endswith(".archipelago") for file in infolist):
return Markup("Error: Your .zip file contains an .archipelago file. "
'Did you mean to <a href="/uploads">host a game</a>?')
for file in infolist:
if file.filename.endswith(banned_zip_contents):
return ("Uploaded data contained a rom file, "
"which is likely to contain copyrighted material. "
"Your file was deleted.")
elif file.filename.endswith((".yaml", ".json", ".yml", ".txt")):
options[file.filename] = zfile.open(file, "r").read()
else:
options[uploaded_file.filename] = uploaded_file.read()
if not options:
return f"Did not find any valid files to process. Accepted formats: {allowed_options_extensions}"
return "Did not find a .yaml file to process."
return options

View File

@@ -205,12 +205,6 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
ctx.auto_shutdown = Room.get(id=room_id).timeout
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, []))
await ctx.shutdown_task
# ensure auto launch is on the same page in regard to room activity.
with db_session:
room: Room = Room.get(id=ctx.room_id)
room.last_activity = datetime.datetime.utcnow() - datetime.timedelta(seconds=room.timeout + 60)
logging.info("Shutting down")
with Locker(room_id):

View File

@@ -5,5 +5,5 @@ Flask-Caching>=2.1.0
Flask-Compress>=1.14
Flask-Limiter>=3.5.0
bokeh>=3.1.1; python_version <= '3.8'
bokeh>=3.3.2; python_version >= '3.9'
bokeh>=3.2.2; python_version >= '3.9'
markupsafe>=2.1.3

View File

@@ -369,7 +369,7 @@ const setPresets = (optionsData, presetName) => {
break;
}
case 'named_range': {
case 'special_range': {
const selectElement = document.querySelector(`select[data-key='${option}']`);
const rangeElement = document.querySelector(`input[data-key='${option}']`);
const randomElement = document.querySelector(`.randomize-button[data-key='${option}']`);

View File

@@ -576,7 +576,7 @@ class GameSettings {
option = parseInt(option, 10);
let optionAcceptable = false;
if ((option >= setting.min) && (option <= setting.max)) {
if ((option > setting.min) && (option < setting.max)) {
optionAcceptable = true;
}
if (setting.hasOwnProperty('value_names') && Object.values(setting.value_names).includes(option)){

View File

@@ -3,16 +3,6 @@
{% block head %}
<title>Multiworld {{ room.id|suuid }}</title>
{% if should_refresh %}<meta http-equiv="refresh" content="2">{% endif %}
<meta name="og:site_name" content="Archipelago">
<meta property="og:title" content="Multiworld {{ room.id|suuid }}">
<meta property="og:type" content="website" />
{% if room.seed.slots|length < 2 %}
<meta property="og:description" content="{{ room.seed.slots|length }} Player World
{% if room.last_port != -1 %}running on {{ config['HOST_ADDRESS'] }} with port {{ room.last_port }}{% endif %}">
{% else %}
<meta property="og:description" content="{{ room.seed.slots|length }} Players Multiworld
{% if room.last_port != -1 %}running on {{ config['HOST_ADDRESS'] }} with port {{ room.last_port }}{% endif %}">
{% endif %}
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/hostRoom.css") }}"/>
{% endblock %}

View File

@@ -16,7 +16,7 @@
{% with messages = get_flashed_messages() %}
{% if messages %}
<div>
{% for message in messages | unique %}
{% for message in messages %}
<div class="user-message">{{ message }}</div>
{% endfor %}
</div>

View File

@@ -53,7 +53,7 @@
{% endif %}
{% if world.web.options_page is string %}
<span class="link-spacer">|</span>
<a href="{{ world.web.options_page }}">Options Page</a>
<a href="{{ world.web.settings_page }}">Options Page</a>
{% elif world.web.options_page %}
<span class="link-spacer">|</span>
<a href="{{ url_for("player_options", game=game_name) }}">Options Page</a>

View File

@@ -19,22 +19,7 @@ from worlds.Files import AutoPatchRegister
from . import app
from .models import Seed, Room, Slot, GameDataPackage
banned_extensions = (".sfc", ".z64", ".n64", ".nes", ".smc", ".sms", ".gb", ".gbc", ".gba")
allowed_options_extensions = (".yaml", ".json", ".yml", ".txt", ".zip")
allowed_generation_extensions = (".archipelago", ".zip")
def allowed_options(filename: str) -> bool:
return filename.endswith(allowed_options_extensions)
def allowed_generation(filename: str) -> bool:
return filename.endswith(allowed_generation_extensions)
def banned_file(filename: str) -> bool:
return filename.endswith(banned_extensions)
banned_zip_contents = (".sfc", ".z64", ".n64", ".sms", ".gb")
def process_multidata(compressed_multidata, files={}):
decompressed_multidata = MultiServer.Context.decompress(compressed_multidata)
@@ -76,8 +61,8 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
if not owner:
owner = session["_id"]
infolist = zfile.infolist()
if all(allowed_options(file.filename) or file.is_dir() for file in infolist):
flash(Markup("Error: Your .zip file only contains options files. "
if all(file.filename.endswith((".yaml", ".yml")) or file.is_dir() for file in infolist):
flash(Markup("Error: Your .zip file only contains .yaml files. "
'Did you mean to <a href="/generate">generate a game</a>?'))
return
@@ -88,7 +73,7 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
# Load files.
for file in infolist:
handler = AutoPatchRegister.get_handler(file.filename)
if banned_file(file.filename):
if file.filename.endswith(banned_zip_contents):
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. " \
"Your file was deleted."
@@ -151,34 +136,35 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s
flash("No multidata was found in the zip file, which is required.")
@app.route("/uploads", methods=["GET", "POST"])
@app.route('/uploads', methods=['GET', 'POST'])
def uploads():
if request.method == "POST":
# check if the POST request has a file part.
if "file" not in request.files:
flash("No file part in POST request.")
if request.method == 'POST':
# check if the post request has the file part
if 'file' not in request.files:
flash('No file part')
else:
uploaded_file = request.files["file"]
# If the user does not select file, the browser will still submit an empty string without a file name.
if uploaded_file.filename == "":
flash("No selected file.")
elif uploaded_file and allowed_generation(uploaded_file.filename):
if zipfile.is_zipfile(uploaded_file):
with zipfile.ZipFile(uploaded_file, "r") as zfile:
file = request.files['file']
# if user does not select file, browser also
# submit an empty part without filename
if file.filename == '':
flash('No selected file')
elif file and allowed_file(file.filename):
if zipfile.is_zipfile(file):
with zipfile.ZipFile(file, 'r') as zfile:
try:
res = upload_zip_to_db(zfile)
except VersionException:
flash(f"Could not load multidata. Wrong Version detected.")
else:
if res is str:
if type(res) == str:
return res
elif res:
return redirect(url_for("view_seed", seed=res.id))
else:
uploaded_file.seek(0) # offset from is_zipfile check
file.seek(0) # offset from is_zipfile check
# noinspection PyBroadException
try:
multidata = uploaded_file.read()
multidata = file.read()
slots, multidata = process_multidata(multidata)
except Exception as e:
flash(f"Could not load multidata. File may be corrupted or incompatible. ({e})")
@@ -196,3 +182,7 @@ def user_content():
rooms = select(room for room in Room if room.owner == session["_id"])
seeds = select(seed for seed in Seed if seed.owner == session["_id"])
return render_template("userContent.html", rooms=rooms, seeds=seeds)
def allowed_file(filename):
return filename.endswith(('.archipelago', ".zip"))

View File

@@ -13,6 +13,7 @@ from typing import List
import Utils
from Utils import async_start
from worlds import lookup_any_location_id_to_name
from CommonClient import CommonContext, server_loop, gui_enabled, console_loop, ClientCommandProcessor, logger, \
get_base_parser
@@ -152,7 +153,7 @@ def get_payload(ctx: ZeldaContext):
def reconcile_shops(ctx: ZeldaContext):
checked_location_names = [ctx.location_names[location] for location in ctx.checked_locations]
checked_location_names = [lookup_any_location_id_to_name[location] for location in ctx.checked_locations]
shops = [location for location in checked_location_names if "Shop" in location]
left_slots = [shop for shop in shops if "Left" in shop]
middle_slots = [shop for shop in shops if "Middle" in shop]
@@ -190,7 +191,7 @@ async def parse_locations(locations_array, ctx: ZeldaContext, force: bool, zone=
locations_checked = []
location = None
for location in ctx.missing_locations:
location_name = ctx.location_names[location]
location_name = lookup_any_location_id_to_name[location]
if location_name in Locations.overworld_locations and zone == "overworld":
status = locations_array[Locations.major_location_offsets[location_name]]

View File

@@ -1,7 +1,7 @@
import asyncio
import base64
import platform
from typing import Any, ClassVar, Coroutine, Dict, List, Optional, Protocol, Tuple, cast
from typing import Any, ClassVar, Coroutine, Dict, List, Optional, Protocol, Tuple, Type, cast
# CommonClient import first to trigger ModuleUpdater
from CommonClient import CommonContext, server_loop, gui_enabled, \
@@ -10,7 +10,7 @@ from NetUtils import ClientStatus
import Utils
from Utils import async_start
import colorama
import colorama # type: ignore
from zilliandomizer.zri.memory import Memory
from zilliandomizer.zri import events
@@ -45,7 +45,7 @@ class SetRoomCallback(Protocol):
class ZillionContext(CommonContext):
game = "Zillion"
command_processor = ZillionCommandProcessor
command_processor: Type[ClientCommandProcessor] = ZillionCommandProcessor
items_handling = 1 # receive items from other players
known_name: Optional[str]
@@ -278,7 +278,7 @@ class ZillionContext(CommonContext):
logger.warning(f"invalid Retrieved packet to ZillionClient: {args}")
return
keys = cast(Dict[str, Optional[str]], args["keys"])
doors_b64 = keys.get(f"zillion-{self.auth}-doors", None)
doors_b64 = keys[f"zillion-{self.auth}-doors"]
if doors_b64:
logger.info("received door data from server")
doors = base64.b64decode(doors_b64)

View File

@@ -585,7 +585,7 @@ else
-- misaligned, so for GB and GBC we explicitly set the callback on
-- vblank instead.
-- https://github.com/TASEmulators/BizHawk/issues/3711
if emu.getsystemid() == "GB" or emu.getsystemid() == "GBC" or emu.getsystemid() == "SGB" then
if emu.getsystemid() == "GB" or emu.getsystemid() == "GBC" then
event.onmemoryexecute(tick, 0x40, "tick", "System Bus")
else
event.onframeend(tick)

View File

@@ -7,7 +7,7 @@ Contributions are welcome. We have a few requests for new contributors:
* **Ensure that critical changes are covered by tests.**
It is strongly recommended that unit tests are used to avoid regression and to ensure everything is still working.
If you wish to contribute by adding a new game, please take a look at the [logic unit test documentation](/docs/tests.md).
If you wish to contribute by adding a new game, please take a look at the [logic unit test documentation](/docs/world%20api.md#tests).
If you wish to contribute to the website, please take a look at [these tests](/test/webhost).
* **Do not introduce unit test failures/regressions.**

View File

@@ -1,90 +0,0 @@
# Archipelago Unit Testing API
This document covers some of the generic tests available using Archipelago's unit testing system, as well as some basic
steps on how to write your own.
## Generic Tests
Some generic tests are run on every World to ensure basic functionality with default options. These basic tests can be
found in the [general test directory](/test/general).
## Defining World Tests
In order to run tests from your world, you will need to create a `test` package within your world package. This can be
done by creating a `test` directory with a file named `__init__.py` inside it inside your world. By convention, a base
for your world tests can be created in this file that you can then import into other modules.
### WorldTestBase
In order to test basic functionality of varying options, as well as to test specific edge cases or that certain
interactions in the world interact as expected, you will want to use the [WorldTestBase](/test/bases.py). This class
comes with the basics for test setup as well as a few preloaded tests that most worlds might want to check on varying
options combinations.
Example `/worlds/<my_game>/test/__init__.py`:
```python
from test.bases import WorldTestBase
class MyGameTestBase(WorldTestBase):
game = "My Game"
```
The basic tests that WorldTestBase comes with include `test_all_state_can_reach_everything`,
`test_empty_state_can_reach_something`, and `test_fill`. These test that with all collected items everything is
reachable, with no collected items at least something is reachable, and that a valid multiworld can be completed with
all steps being called, respectively.
### Writing Tests
#### Using WorldTestBase
Adding runs for the basic tests for a different option combination is as easy as making a new module in the test
package, creating a class that inherits from your game's TestBase, and defining the options in a dict as a field on the
class. The new module should be named `test_<something>.py` and have at least one class inheriting from the base, or
define its own testing methods. Newly defined test methods should follow standard PEP8 snake_case format and also start
with `test_`.
Example `/worlds/<my_game>/test/test_chest_access.py`:
```python
from . import MyGameTestBase
class TestChestAccess(MyGameTestBase):
options = {
"difficulty": "easy",
"final_boss_hp": 4000,
}
def test_sword_chests(self) -> None:
"""Test locations that require a sword"""
locations = ["Chest1", "Chest2"]
items = [["Sword"]]
# This tests that the provided locations aren't accessible without the provided items, but can be accessed once
# the items are obtained.
# This will also check that any locations not provided don't have the same dependency requirement.
# Optionally, passing only_check_listed=True to the method will only check the locations provided.
self.assertAccessDependency(locations, items)
```
When tests are run, this class will create a multiworld with a single player having the provided options, and run the
generic tests, as well as the new custom test. Each test method definition will create its own separate solo multiworld
that will be cleaned up after. If you don't want to run the generic tests on a base, `run_default_tests` can be
overridden. For more information on what methods are available to your class, check the
[WorldTestBase definition](/test/bases.py#L104).
#### Alternatives to WorldTestBase
Unit tests can also be created using [TestBase](/test/bases.py#L14) or
[unittest.TestCase](https://docs.python.org/3/library/unittest.html#unittest.TestCase) depending on your use case. These
may be useful for generating a multiworld under very specific constraints without using the generic world setup, or for
testing portions of your code that can be tested without relying on a multiworld to be created first.
## Running Tests
In PyCharm, running all tests can be done by right-clicking the root `test` directory and selecting `run Python tests`.
If you do not have pytest installed, you may get import failures. To solve this, edit the run configuration, and set the
working directory of the run to the Archipelago directory. If you only want to run your world's defined tests, repeat
the steps for the test directory within your world.

View File

@@ -870,7 +870,7 @@ TestBase, and can then define options to test in the class body, and run tests i
Example `__init__.py`
```python
from test.bases import WorldTestBase
from test.test_base import WorldTestBase
class MyGameTestBase(WorldTestBase):
@@ -879,7 +879,7 @@ class MyGameTestBase(WorldTestBase):
Next using the rules defined in the above `set_rules` we can test that the chests have the correct access rules.
Example `test_chest_access.py`
Example `testChestAccess.py`
```python
from . import MyGameTestBase
@@ -899,5 +899,3 @@ class TestChestAccess(MyGameTestBase):
# this will test that chests 3-5 can't be accessed without any weapon, but can be with just one of them.
self.assertAccessDependency(locations, items)
```
For more information on tests check the [tests doc](tests.md).

View File

@@ -1,13 +1,13 @@
colorama>=0.4.5
websockets>=12.0
websockets>=11.0.3
PyYAML>=6.0.1
jellyfish>=1.0.3
jinja2>=3.1.2
schema>=0.7.5
kivy>=2.2.1
kivy>=2.2.0
bsdiff4>=1.2.4
platformdirs>=4.0.0
certifi>=2023.11.17
cython>=3.0.6
cython>=3.0.5
cymem>=2.0.8
orjson>=3.9.10

View File

@@ -1,8 +1,5 @@
import unittest
from Fill import distribute_items_restrictive
from worlds.AutoWorld import AutoWorldRegister, call_all
from . import setup_solo_multiworld
from worlds.AutoWorld import AutoWorldRegister
class TestIDs(unittest.TestCase):
@@ -69,34 +66,3 @@ class TestIDs(unittest.TestCase):
for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename):
self.assertEqual(len(world_type.location_id_to_name), len(world_type.location_name_to_id))
def test_postgen_datapackage(self):
"""Generates a solo multiworld and checks that the datapackage is still valid"""
for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename):
multiworld = setup_solo_multiworld(world_type)
distribute_items_restrictive(multiworld)
call_all(multiworld, "post_fill")
datapackage = world_type.get_data_package_data()
for item_group, item_names in datapackage["item_name_groups"].items():
self.assertIsInstance(item_group, str,
f"item_name_group names should be strings: {item_group}")
for item_name in item_names:
self.assertIsInstance(item_name, str,
f"{item_name}, in group {item_group} is not a string")
for loc_group, loc_names in datapackage["location_name_groups"].items():
self.assertIsInstance(loc_group, str,
f"location_name_group names should be strings: {loc_group}")
for loc_name in loc_names:
self.assertIsInstance(loc_name, str,
f"{loc_name}, in group {loc_group} is not a string")
for item_name, item_id in datapackage["item_name_to_id"].items():
self.assertIsInstance(item_name, str,
f"{item_name} is not a valid item name for item_name_to_id")
self.assertIsInstance(item_id, int,
f"{item_id} for {item_name} should be an int")
for loc_name, loc_id in datapackage["location_name_to_id"].items():
self.assertIsInstance(loc_name, str,
f"{loc_name} is not a valid item name for location_name_to_id")
self.assertIsInstance(loc_id, int,
f"{loc_id} for {loc_name} should be an int")

View File

@@ -77,10 +77,6 @@ class AutoWorldRegister(type):
# create missing options_dataclass from legacy option_definitions
# TODO - remove this once all worlds use options dataclasses
if "options_dataclass" not in dct and "option_definitions" in dct:
# TODO - switch to deprecate after a version
if __debug__:
from warnings import warn
warn("Assigning options through option_definitions is now deprecated. Use options_dataclass instead.")
dct["options_dataclass"] = make_dataclass(f"{name}Options", dct["option_definitions"].items(),
bases=(PerGameCommonOptions,))

View File

@@ -97,7 +97,7 @@ async def connect(ctx: BizHawkContext) -> bool:
for port in ports:
try:
ctx.streams = await asyncio.open_connection("127.0.0.1", port)
ctx.streams = await asyncio.open_connection("localhost", port)
ctx.connection_status = ConnectionStatus.TENTATIVE
ctx._port = port
return True

View File

@@ -208,30 +208,19 @@ async def _run_game(rom: str):
if auto_start is True:
emuhawk_path = Utils.get_settings().bizhawkclient_options.emuhawk_path
subprocess.Popen(
[
emuhawk_path,
f"--lua={Utils.local_path('data', 'lua', 'connector_bizhawk_generic.lua')}",
os.path.realpath(rom),
],
cwd=Utils.local_path("."),
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
subprocess.Popen([emuhawk_path, "--lua=data/lua/connector_bizhawk_generic.lua", os.path.realpath(rom)],
cwd=Utils.local_path("."),
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
elif isinstance(auto_start, str):
import shlex
subprocess.Popen(
[
*shlex.split(auto_start),
os.path.realpath(rom)
],
cwd=Utils.local_path("."),
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL
)
subprocess.Popen([*shlex.split(auto_start), os.path.realpath(rom)],
cwd=Utils.local_path("."),
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
async def _patch_and_run_game(patch_file: str):

View File

@@ -264,7 +264,7 @@ def fill_dungeons_restrictive(multiworld: MultiWorld):
if loc in all_state_base.events:
all_state_base.events.remove(loc)
fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True, allow_excluded=True,
fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True,
name="LttP Dungeon Items")

View File

@@ -682,6 +682,8 @@ def get_pool_core(world, player: int):
key_location = world.random.choice(key_locations)
place_item(key_location, "Small Key (Universal)")
pool = pool[:-3]
if world.key_drop_shuffle[player]:
pass # pool.extend([item_to_place] * (len(key_drop_data) - 1))
return (pool, placed_items, precollected_items, clock_mode, treasure_hunt_count, treasure_hunt_icon,
additional_pieces_to_place)

View File

@@ -1,7 +1,8 @@
import typing
from BaseClasses import MultiWorld
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool, PlandoBosses
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, AllowCollect, StartInventoryPool, \
PlandoBosses
class Logic(Choice):
@@ -426,12 +427,6 @@ class BeemizerTrapChance(BeemizerRange):
display_name = "Beemizer Trap Chance"
class AllowCollect(Toggle):
"""Allows for !collect / co-op to auto-open chests containing items for other players.
Off by default, because it currently crashes on real hardware."""
display_name = "Allow Collection of checks for other players"
alttp_options: typing.Dict[str, type(Option)] = {
"crystals_needed_for_gt": CrystalsTower,
"crystals_needed_for_ganon": CrystalsGanon,

View File

@@ -136,8 +136,7 @@ def mirrorless_path_to_castle_courtyard(world, player):
def set_defeat_dungeon_boss_rule(location):
# Lambda required to defer evaluation of dungeon.boss since it will change later if boss shuffle is used
add_rule(location, lambda state: location.parent_region.dungeon.boss.can_defeat(state))
set_rule(location, lambda state: location.parent_region.dungeon.boss.can_defeat(state))
def set_always_allow(spot, rule):
spot.always_allow = rule

View File

@@ -289,17 +289,12 @@ class ALTTPWorld(World):
self.waterfall_fairy_bottle_fill = self.random.choice(bottle_options)
self.pyramid_fairy_bottle_fill = self.random.choice(bottle_options)
if multiworld.mode[player] == 'standard':
if multiworld.smallkey_shuffle[player]:
if (multiworld.smallkey_shuffle[player] not in
(smallkey_shuffle.option_universal, smallkey_shuffle.option_own_dungeons,
smallkey_shuffle.option_start_with)):
self.multiworld.local_early_items[self.player]["Small Key (Hyrule Castle)"] = 1
self.multiworld.local_items[self.player].value.add("Small Key (Hyrule Castle)")
self.multiworld.non_local_items[self.player].value.discard("Small Key (Hyrule Castle)")
if multiworld.bigkey_shuffle[player]:
self.multiworld.local_items[self.player].value.add("Big Key (Hyrule Castle)")
self.multiworld.non_local_items[self.player].value.discard("Big Key (Hyrule Castle)")
if multiworld.mode[player] == 'standard' \
and multiworld.smallkey_shuffle[player] \
and multiworld.smallkey_shuffle[player] != smallkey_shuffle.option_universal \
and multiworld.smallkey_shuffle[player] != smallkey_shuffle.option_own_dungeons \
and multiworld.smallkey_shuffle[player] != smallkey_shuffle.option_start_with:
self.multiworld.local_early_items[self.player]["Small Key (Hyrule Castle)"] = 1
# system for sharing ER layouts
self.er_seed = str(multiworld.random.randint(0, 2 ** 64))

View File

@@ -21,20 +21,7 @@ This client has only been tested with the Official Steam version of the game at
## Downpatching Dark Souls III
To downpatch DS3 for use with Archipelago, use the following instructions from the speedsouls wiki database.
1. Launch Steam (in online mode).
2. Press the Windows Key + R. This will open the Run window.
3. Open the Steam console by typing the following string: steam://open/console , Steam should now open in Console Mode.
4. Insert the string of the depot you wish to download. For the AP supported v1.15, you will want to use: download_depot 374320 374321 4471176929659548333.
5. Steam will now download the depot. Note: There is no progress bar of the download in Steam, but it is still downloading in the background.
6. Turn off auto-updates in Steam by right-clicking Dark Souls III in your library > Properties > Updates > set "Automatic Updates" to "Only update this game when I launch it" (or change the value for AutoUpdateBehavior to 1 in "\Steam\steamapps\appmanifest_374320.acf").
7. Back up your existing game folder in "\Steam\steamapps\common\DARK SOULS III".
8. Return back to Steam console. Once the download is complete, it should say so along with the temporary local directory in which the depot has been stored. This is usually something like "\Steam\steamapps\content\app_XXXXXX\depot_XXXXXX". Back up this game folder as well.
9. Delete your existing game folder in "\Steam\steamapps\common\DARK SOULS III", then replace it with your game folder in "\Steam\steamapps\content\app_XXXXXX\depot_XXXXXX".
10. Back up and delete your save file "DS30000.sl2" in AppData. AppData is hidden by default. To locate it, press Windows Key + R, type %appdata% and hit enter or: open File Explorer > View > Hidden Items and follow "C:\Users\your username\AppData\Roaming\DarkSoulsIII\numbers".
11. If you did all these steps correctly, you should be able to confirm your game version in the upper left corner after launching Dark Souls III.
Follow instructions from the [speedsouls wiki](https://wiki.speedsouls.com/darksouls3:Downpatching) to download version 1.15. Your download command, including the correct depot and manifest ids, will be "download_depot 374320 374321 4471176929659548333"
## Installing the Archipelago mod

View File

@@ -5,7 +5,7 @@ import os
import shutil
import threading
import zipfile
from typing import Optional, TYPE_CHECKING, Any, List, Callable, Tuple, Union
from typing import Optional, TYPE_CHECKING, Any, List, Callable, Tuple
import jinja2
@@ -63,7 +63,7 @@ recipe_time_ranges = {
class FactorioModFile(worlds.Files.APContainer):
game = "Factorio"
compression_method = zipfile.ZIP_DEFLATED # Factorio can't load LZMA archives
writing_tasks: List[Callable[[], Tuple[str, Union[str, bytes]]]]
writing_tasks: List[Callable[[], Tuple[str, str]]]
def __init__(self, *args: Any, **kwargs: Any):
super().__init__(*args, **kwargs)
@@ -164,7 +164,9 @@ def generate_mod(world: "Factorio", output_directory: str):
template_data["free_sample_blacklist"].update({item: 1 for item in multiworld.free_sample_blacklist[player].value})
template_data["free_sample_blacklist"].update({item: 0 for item in multiworld.free_sample_whitelist[player].value})
zf_path = os.path.join(output_directory, versioned_mod_name + ".zip")
mod_dir = os.path.join(output_directory, versioned_mod_name)
zf_path = os.path.join(mod_dir + ".zip")
mod = FactorioModFile(zf_path, player=player, player_name=multiworld.player_name[player])
if world.zip_path:
@@ -175,13 +177,7 @@ def generate_mod(world: "Factorio", output_directory: str):
mod.writing_tasks.append(lambda arcpath=versioned_mod_name+"/"+path_part, content=zf.read(file):
(arcpath, content))
else:
basepath = os.path.join(os.path.dirname(__file__), "data", "mod")
for dirpath, dirnames, filenames in os.walk(basepath):
base_arc_path = versioned_mod_name+"/"+os.path.relpath(dirpath, basepath)
for filename in filenames:
mod.writing_tasks.append(lambda arcpath=base_arc_path+"/"+filename,
file_path=os.path.join(dirpath, filename):
(arcpath, open(file_path, "rb").read()))
shutil.copytree(os.path.join(os.path.dirname(__file__), "data", "mod"), mod_dir, dirs_exist_ok=True)
mod.writing_tasks.append(lambda: (versioned_mod_name + "/data.lua",
data_template.render(**template_data)))
@@ -201,3 +197,5 @@ def generate_mod(world: "Factorio", output_directory: str):
# write the mod file
mod.write()
# clean up
shutil.rmtree(mod_dir)

View File

@@ -2,8 +2,8 @@ from __future__ import annotations
import typing
import datetime
from Options import Choice, OptionDict, OptionSet, Option, DefaultOnToggle, Range, DeathLink, Toggle, \
StartInventoryPool
from Options import Choice, OptionDict, OptionSet, ItemDict, Option, DefaultOnToggle, Range, DeathLink, AllowCollect, \
Toggle, StartInventoryPool
from schema import Schema, Optional, And, Or
# schema helpers
@@ -207,9 +207,10 @@ class RecipeIngredientsOffset(Range):
range_end = 5
class FactorioStartItems(OptionDict):
class FactorioStartItems(ItemDict):
"""Mapping of Factorio internal item-name to amount granted on start."""
display_name = "Starting Items"
verify_item_name = False
default = {"burner-mining-drill": 19, "stone-furnace": 19}
@@ -453,6 +454,7 @@ factorio_options: typing.Dict[str, type(Option)] = {
"evolution_traps": EvolutionTrapCount,
"evolution_trap_increase": EvolutionTrapIncrease,
"death_link": DeathLink,
"allow_collect": AllowCollect,
"energy_link": EnergyLink,
"start_inventory_from_pool": StartInventoryPool,
}

View File

@@ -246,8 +246,7 @@ class Factorio(World):
location.access_rule = lambda state, ingredient=ingredient, custom_recipe=custom_recipe: \
(ingredient not in technology_table or state.has(ingredient, player)) and \
all(state.has(technology.name, player) for sub_ingredient in custom_recipe.ingredients
for technology in required_technologies[sub_ingredient]) and \
all(state.has(technology.name, player) for technology in required_technologies[custom_recipe.crafting_machine])
for technology in required_technologies[sub_ingredient])
else:
location.access_rule = lambda state, ingredient=ingredient: \
all(state.has(technology.name, player) for technology in required_technologies[ingredient])

View File

@@ -11,6 +11,7 @@ TRAP_EVO_FACTOR = {{ evolution_trap_increase }} / 100
MAX_SCIENCE_PACK = {{ max_science_pack }}
GOAL = {{ goal }}
ARCHIPELAGO_DEATH_LINK_SETTING = "archipelago-death-link-{{ slot_player }}-{{ seed_name }}"
ARCHIPELAGO_ALLOW_COLLECT_SETTING = "archipelago-allow-collect-{{ slot_player }}-{{ seed_name }}"
ENERGY_INCREMENT = {{ energy_link * 10000000 }}
ENERGY_LINK_EFFICIENCY = 0.75
@@ -20,6 +21,12 @@ else
DEATH_LINK = 0
end
if settings.global[ARCHIPELAGO_ALLOW_COLLECT_SETTING].value then
ALLOW_COLLECT = 1
else
ALLOW_COLLECT = 0
end
CURRENTLY_DEATH_LOCK = 0
{% if chunk_shuffle %}
@@ -257,6 +264,26 @@ function on_runtime_mod_setting_changed(event)
dumpInfo(force)
end
end
if event.setting == ARCHIPELAGO_ALLOW_COLLECT_SETTING then
local force = game.forces["player"]
if global.received_tech == nil then
global.received_tech = {}
end
if settings.global[ARCHIPELAGO_ALLOW_COLLECT_SETTING].value then
ALLOW_COLLECT = 1
for item_name, _ in pairs(global.received_tech) do
tech = force.technologies[item_name]
if tech ~= nil and tech.researched ~= true then
game.print({"", "Received [technology=" .. tech.name .. "] as it is already checked."})
game.play_sound({path="utility/research_completed"})
tech.researched = true
end
end
global.received_tech = {}
else
ALLOW_COLLECT = 0
end
end
end
script.on_event(defines.events.on_runtime_mod_setting_changed, on_runtime_mod_setting_changed)
@@ -658,18 +685,29 @@ commands.add_command("ap-get-technology", "Grant a technology, used by the Archi
if global.index_sync == nil then
global.index_sync = {}
end
if global.received_tech == nil then
global.received_tech = {}
end
local tech
local force = game.forces["player"]
chunks = split(call.parameter, "\t")
local item_name = chunks[1]
local index = chunks[2]
local index = tonumber(chunks[2]) or chunks[2]
local source = chunks[3] or "Archipelago"
if index == -1 then -- for coop sync and restoring from an older savegame
tech = force.technologies[item_name]
if tech == nil then
game.print("Unknown Item " .. item_name)
return
end
if tech.researched ~= true then
game.print({"", "Received [technology=" .. tech.name .. "] as it is already checked."})
game.play_sound({path="utility/research_completed"})
tech.researched = true
if ALLOW_COLLECT == 1 then
game.print({"", "Received [technology=" .. tech.name .. "] as it is already checked."})
game.play_sound({path="utility/research_completed"})
tech.researched = true
else
global.received_tech[item_name] = 1
end
end
return
elseif progressive_technologies[item_name] ~= nil then

View File

@@ -26,6 +26,8 @@ ap-{{ location.address }}-=Researching this technology sends something to someon
[mod-setting-name]
archipelago-death-link-{{ slot_player }}-{{ seed_name }}=Death Link
archipelago-allow-collect-{{ slot_player }}-{{ seed_name }}=Allow Collect
[mod-setting-description]
archipelago-death-link-{{ slot_player }}-{{ seed_name }}=Kill other players in the same Archipelago Multiworld that also have Death Link turned on, when you die.
archipelago-death-link-{{ slot_player }}-{{ seed_name }}=Kill other players in the same Archipelago Multiworld that also have Death Link turned on, when you die.
archipelago-allow-collect-{{ slot_player }}-{{ seed_name }}=Allows released/collected tech locations to be marked as researched automatically.

View File

@@ -26,5 +26,15 @@ data:extend({
{% else %}
default_value = false
{% endif %}
},
{
type = "bool-setting",
name = "archipelago-allow-collect-{{ slot_player }}-{{ seed_name }}",
setting_type = "runtime-global",
{% if allow_collect %}
default_value = true
{% else %}
default_value = false
{% endif %}
}
})

View File

@@ -187,7 +187,6 @@ item_table = {
"Pazuzu 5F": ItemData(None, ItemClassification.progression),
"Pazuzu 6F": ItemData(None, ItemClassification.progression),
"Dark King": ItemData(None, ItemClassification.progression),
"Tristam Bone Item Given": ItemData(None, ItemClassification.progression),
#"Barred": ItemData(None, ItemClassification.progression),
}

View File

@@ -1,4 +1,4 @@
from Options import Choice, FreeText, Toggle, Range
from Options import Choice, FreeText, Toggle
class Logic(Choice):
@@ -131,21 +131,6 @@ class EnemizerAttacks(Choice):
default = 0
class EnemizerGroups(Choice):
"""Set which enemy groups will be affected by Enemizer."""
display_name = "Enemizer Groups"
option_mobs_only = 0
option_mobs_and_bosses = 1
option_mobs_bosses_and_dark_king = 2
default = 1
class ShuffleResWeakType(Toggle):
"""Resistance and Weakness types are shuffled for all enemies."""
display_name = "Shuffle Resistance/Weakness Types"
default = 0
class ShuffleEnemiesPositions(Toggle):
"""Instead of their original position in a given map, enemies are randomly placed."""
display_name = "Shuffle Enemies' Positions"
@@ -246,81 +231,6 @@ class BattlefieldsBattlesQuantities(Choice):
option_random_one_through_ten = 6
class CompanionLevelingType(Choice):
"""Set how companions gain levels.
Quests: Complete each companion's individual quest for them to promote to their second version.
Quests Extended: Each companion has four exclusive quests, leveling each time a quest is completed.
Save the Crystals (All): Each time a Crystal is saved, all companions gain levels.
Save the Crystals (Individual): Each companion will level to their second version when a specific Crystal is saved.
Benjamin Level: Companions' level tracks Benjamin's."""
option_quests = 0
option_quests_extended = 1
option_save_crystals_individual = 2
option_save_crystals_all = 3
option_benjamin_level = 4
option_benjamin_level_plus_5 = 5
option_benjamin_level_plus_10 = 6
default = 0
display_name = "Companion Leveling Type"
class CompanionSpellbookType(Choice):
"""Update companions' spellbook.
Standard: Original game spellbooks.
Extended: Add some extra spells. Tristam gains Exit and Quake and Reuben gets Blizzard.
Random Balanced: Randomize the spellbooks with an appropriate mix of spells.
Random Chaos: Randomize the spellbooks in total free-for-all."""
option_standard = 0
option_extended = 1
option_random_balanced = 2
option_random_chaos = 3
default = 0
display_name = "Companion Spellbook Type"
class StartingCompanion(Choice):
"""Set a companion to start with.
Random Companion: Randomly select one companion.
Random Plus None: Randomly select a companion, with the possibility of none selected."""
display_name = "Starting Companion"
default = 0
option_none = 0
option_kaeli = 1
option_tristam = 2
option_phoebe = 3
option_reuben = 4
option_random_companion = 5
option_random_plus_none = 6
class AvailableCompanions(Range):
"""Select randomly which companions will join your party. Unavailable companions can still be reached to get their items and complete their quests if needed.
Note: If a Starting Companion is selected, it will always be available, regardless of this setting."""
display_name = "Available Companions"
default = 4
range_start = 0
range_end = 4
class CompanionsLocations(Choice):
"""Set the primary location of companions. Their secondary location is always the same.
Standard: Companions will be at the same locations as in the original game.
Shuffled: Companions' locations are shuffled amongst themselves.
Shuffled Extended: Add all the Temples, as well as Phoebe's House and the Rope Bridge as possible locations."""
display_name = "Companions' Locations"
default = 0
option_standard = 0
option_shuffled = 1
option_shuffled_extended = 2
class KaelisMomFightsMinotaur(Toggle):
"""Transfer Kaeli's requirements (Tree Wither, Elixir) and the two items she's giving to her mom.
Kaeli will be available to join the party right away without the Tree Wither."""
display_name = "Kaeli's Mom Fights Minotaur"
default = 0
option_definitions = {
"logic": Logic,
"brown_boxes": BrownBoxes,
@@ -328,21 +238,12 @@ option_definitions = {
"shattered_sky_coin_quantity": ShatteredSkyCoinQuantity,
"starting_weapon": StartingWeapon,
"progressive_gear": ProgressiveGear,
"leveling_curve": LevelingCurve,
"starting_companion": StartingCompanion,
"available_companions": AvailableCompanions,
"companions_locations": CompanionsLocations,
"kaelis_mom_fight_minotaur": KaelisMomFightsMinotaur,
"companion_leveling_type": CompanionLevelingType,
"companion_spellbook_type": CompanionSpellbookType,
"enemies_density": EnemiesDensity,
"enemies_scaling_lower": EnemiesScalingLower,
"enemies_scaling_upper": EnemiesScalingUpper,
"bosses_scaling_lower": BossesScalingLower,
"bosses_scaling_upper": BossesScalingUpper,
"enemizer_attacks": EnemizerAttacks,
"enemizer_groups": EnemizerGroups,
"shuffle_res_weak_types": ShuffleResWeakType,
"shuffle_enemies_position": ShuffleEnemiesPositions,
"progressive_formations": ProgressiveFormations,
"doom_castle_mode": DoomCastle,
@@ -352,5 +253,6 @@ option_definitions = {
"crest_shuffle": CrestShuffle,
"shuffle_battlefield_rewards": ShuffleBattlefieldRewards,
"map_shuffle_seed": MapShuffleSeed,
"leveling_curve": LevelingCurve,
"battlefields_battles_quantities": BattlefieldsBattlesQuantities,
}

View File

@@ -35,58 +35,46 @@ def generate_output(self, output_directory):
"item_name": location.item.name})
def cc(option):
return option.current_key.title().replace("_", "").replace("OverworldAndDungeons",
"OverworldDungeons").replace("MobsAndBosses", "MobsBosses").replace("MobsBossesAndDarkKing",
"MobsBossesDK").replace("BenjaminLevelPlus", "BenPlus").replace("BenjaminLevel", "BenPlus0").replace(
"RandomCompanion", "Random")
return option.current_key.title().replace("_", "").replace("OverworldAndDungeons", "OverworldDungeons")
def tf(option):
return True if option else False
options = deepcopy(settings_template)
options["name"] = self.multiworld.player_name[self.player]
option_writes = {
"enemies_density": cc(self.multiworld.enemies_density[self.player]),
"chests_shuffle": "Include",
"shuffle_boxes_content": self.multiworld.brown_boxes[self.player] == "shuffle",
"npcs_shuffle": "Include",
"battlefields_shuffle": "Include",
"logic_options": cc(self.multiworld.logic[self.player]),
"shuffle_enemies_position": tf(self.multiworld.shuffle_enemies_position[self.player]),
"enemies_scaling_lower": cc(self.multiworld.enemies_scaling_lower[self.player]),
"enemies_scaling_upper": cc(self.multiworld.enemies_scaling_upper[self.player]),
"bosses_scaling_lower": cc(self.multiworld.bosses_scaling_lower[self.player]),
"bosses_scaling_upper": cc(self.multiworld.bosses_scaling_upper[self.player]),
"enemizer_attacks": cc(self.multiworld.enemizer_attacks[self.player]),
"leveling_curve": cc(self.multiworld.leveling_curve[self.player]),
"battles_quantity": cc(self.multiworld.battlefields_battles_quantities[self.player]) if
self.multiworld.battlefields_battles_quantities[self.player].value < 5 else
"RandomLow" if
self.multiworld.battlefields_battles_quantities[self.player].value == 5 else
"RandomHigh",
"shuffle_battlefield_rewards": tf(self.multiworld.shuffle_battlefield_rewards[self.player]),
"random_starting_weapon": True,
"progressive_gear": tf(self.multiworld.progressive_gear[self.player]),
"tweaked_dungeons": tf(self.multiworld.tweak_frustrating_dungeons[self.player]),
"doom_castle_mode": cc(self.multiworld.doom_castle_mode[self.player]),
"doom_castle_shortcut": tf(self.multiworld.doom_castle_shortcut[self.player]),
"sky_coin_mode": cc(self.multiworld.sky_coin_mode[self.player]),
"sky_coin_fragments_qty": cc(self.multiworld.shattered_sky_coin_quantity[self.player]),
"enable_spoilers": False,
"progressive_formations": cc(self.multiworld.progressive_formations[self.player]),
"map_shuffling": cc(self.multiworld.map_shuffle[self.player]),
"crest_shuffle": tf(self.multiworld.crest_shuffle[self.player]),
"enemizer_groups": cc(self.multiworld.enemizer_groups[self.player]),
"shuffle_res_weak_type": tf(self.multiworld.shuffle_res_weak_types[self.player]),
"companion_leveling_type": cc(self.multiworld.companion_leveling_type[self.player]),
"companion_spellbook_type": cc(self.multiworld.companion_spellbook_type[self.player]),
"starting_companion": cc(self.multiworld.starting_companion[self.player]),
"available_companions": ["Zero", "One", "Two",
"Three", "Four"][self.multiworld.available_companions[self.player].value],
"companions_locations": cc(self.multiworld.companions_locations[self.player]),
"kaelis_mom_fight_minotaur": tf(self.multiworld.kaelis_mom_fight_minotaur[self.player]),
}
option_writes = {
"enemies_density": cc(self.multiworld.enemies_density[self.player]),
"chests_shuffle": "Include",
"shuffle_boxes_content": self.multiworld.brown_boxes[self.player] == "shuffle",
"npcs_shuffle": "Include",
"battlefields_shuffle": "Include",
"logic_options": cc(self.multiworld.logic[self.player]),
"shuffle_enemies_position": tf(self.multiworld.shuffle_enemies_position[self.player]),
"enemies_scaling_lower": cc(self.multiworld.enemies_scaling_lower[self.player]),
"enemies_scaling_upper": cc(self.multiworld.enemies_scaling_upper[self.player]),
"bosses_scaling_lower": cc(self.multiworld.bosses_scaling_lower[self.player]),
"bosses_scaling_upper": cc(self.multiworld.bosses_scaling_upper[self.player]),
"enemizer_attacks": cc(self.multiworld.enemizer_attacks[self.player]),
"leveling_curve": cc(self.multiworld.leveling_curve[self.player]),
"battles_quantity": cc(self.multiworld.battlefields_battles_quantities[self.player]) if
self.multiworld.battlefields_battles_quantities[self.player].value < 5 else
"RandomLow" if
self.multiworld.battlefields_battles_quantities[self.player].value == 5 else
"RandomHigh",
"shuffle_battlefield_rewards": tf(self.multiworld.shuffle_battlefield_rewards[self.player]),
"random_starting_weapon": True,
"progressive_gear": tf(self.multiworld.progressive_gear[self.player]),
"tweaked_dungeons": tf(self.multiworld.tweak_frustrating_dungeons[self.player]),
"doom_castle_mode": cc(self.multiworld.doom_castle_mode[self.player]),
"doom_castle_shortcut": tf(self.multiworld.doom_castle_shortcut[self.player]),
"sky_coin_mode": cc(self.multiworld.sky_coin_mode[self.player]),
"sky_coin_fragments_qty": cc(self.multiworld.shattered_sky_coin_quantity[self.player]),
"enable_spoilers": False,
"progressive_formations": cc(self.multiworld.progressive_formations[self.player]),
"map_shuffling": cc(self.multiworld.map_shuffle[self.player]),
"crest_shuffle": tf(self.multiworld.crest_shuffle[self.player]),
}
for option, data in option_writes.items():
options["Final Fantasy Mystic Quest"][option][data] = 1
@@ -95,7 +83,7 @@ def generate_output(self, output_directory):
'utf8')
self.rom_name_available_event.set()
setup = {"version": "1.5", "name": self.multiworld.player_name[self.player], "romname": rom_name, "seed":
setup = {"version": "1.4", "name": self.multiworld.player_name[self.player], "romname": rom_name, "seed":
hex(self.multiworld.per_slot_randoms[self.player].randint(0, 0xFFFFFFFF)).split("0x")[1].upper()}
starting_items = [output_item_name(item) for item in self.multiworld.precollected_items[self.player]]

View File

@@ -67,10 +67,10 @@ def create_regions(self):
self.multiworld.regions.append(create_region(self.multiworld, self.player, room["name"], room["id"],
[FFMQLocation(self.player, object["name"], location_table[object["name"]] if object["name"] in
location_table else None, object["type"], object["access"],
self.create_item(yaml_item(object["on_trigger"][0])) if object["type"] == "Trigger" else None) for object in
room["game_objects"] if "Hero Chest" not in object["name"] and object["type"] not in ("BattlefieldGp",
"BattlefieldXp") and (object["type"] != "Box" or self.multiworld.brown_boxes[self.player] == "include") and
not (object["name"] == "Kaeli Companion" and not object["on_trigger"])], room["links"]))
self.create_item(yaml_item(object["on_trigger"][0])) if object["type"] == "Trigger" else None) for
object in room["game_objects"] if "Hero Chest" not in object["name"] and object["type"] not in
("BattlefieldGp", "BattlefieldXp") and (object["type"] != "Box" or
self.multiworld.brown_boxes[self.player] == "include")], room["links"]))
dark_king_room = self.multiworld.get_region("Doom Castle Dark King Room", self.player)
dark_king = FFMQLocation(self.player, "Dark King", None, "Trigger", [])

View File

@@ -108,10 +108,8 @@ class FFMQWorld(World):
map_shuffle = multiworld.map_shuffle[world.player].value
crest_shuffle = multiworld.crest_shuffle[world.player].current_key
battlefield_shuffle = multiworld.shuffle_battlefield_rewards[world.player].current_key
companion_shuffle = multiworld.companions_locations[world.player].value
kaeli_mom = multiworld.kaelis_mom_fight_minotaur[world.player].current_key
query = f"s={seed}&m={map_shuffle}&c={crest_shuffle}&b={battlefield_shuffle}&cs={companion_shuffle}&km={kaeli_mom}"
query = f"s={seed}&m={map_shuffle}&c={crest_shuffle}&b={battlefield_shuffle}"
if query in rooms_data:
world.rooms = rooms_data[query]

View File

@@ -827,12 +827,12 @@
id: 164
area: 47
coordinates: [14, 6]
teleporter: [98, 8] # Script for reuben, original value [16, 2]
teleporter: [16, 2]
- name: Fireburg - Hotel
id: 165
area: 47
coordinates: [20, 8]
teleporter: [96, 8] # It's a script now for tristam, original value [17, 2]
teleporter: [17, 2]
- name: Fireburg - GrenadeMan House Script
id: 166
area: 47
@@ -1178,16 +1178,6 @@
area: 60
coordinates: [2, 7]
teleporter: [123, 0]
- name: Lava Dome Pointless Room - Visit Quest Script 1
id: 490
area: 60
coordinates: [4, 4]
teleporter: [99, 8]
- name: Lava Dome Pointless Room - Visit Quest Script 2
id: 491
area: 60
coordinates: [4, 5]
teleporter: [99, 8]
- name: Lava Dome Lower Moon Helm Room - Left Entrance
id: 235
area: 60
@@ -1578,11 +1568,6 @@
area: 79
coordinates: [2, 45]
teleporter: [174, 0]
- name: Mount Gale - Visit Quest
id: 494
area: 79
coordinates: [44, 7]
teleporter: [101, 8]
- name: Windia - Main Entrance 1
id: 312
area: 80
@@ -1628,11 +1613,11 @@
area: 80
coordinates: [21, 39]
teleporter: [30, 5]
- name: Windia - INN's Script # Change to teleporter / Change back to script!
- name: Windia - INN's Script # Change to teleporter
id: 321
area: 80
coordinates: [18, 34]
teleporter: [97, 8] # Original value [79, 8] > [31, 2]
teleporter: [31, 2] # Original value [79, 8]
- name: Windia - Vendor House
id: 322
area: 80
@@ -1712,7 +1697,7 @@
id: 337
area: 82
coordinates: [45, 24]
teleporter: [102, 8] # Changed to script, original value [215, 0]
teleporter: [215, 0]
- name: Windia Inn Lobby - Exit
id: 338
area: 82
@@ -2013,16 +1998,6 @@
area: 95
coordinates: [29, 37]
teleporter: [70, 8]
- name: Light Temple - Visit Quest Script 1
id: 492
area: 95
coordinates: [34, 39]
teleporter: [100, 8]
- name: Light Temple - Visit Quest Script 2
id: 493
area: 95
coordinates: [35, 39]
teleporter: [100, 8]
- name: Ship Dock - Mobius Teleporter Script
id: 397
area: 96

View File

@@ -309,13 +309,13 @@
location: "WindiaBattlefield01"
location_slot: "WindiaBattlefield01"
type: "BattlefieldXp"
access: ["SandCoin", "RiverCoin"]
access: []
- name: "South of Windia Battlefield"
object_id: 0x14
location: "WindiaBattlefield02"
location_slot: "WindiaBattlefield02"
type: "BattlefieldXp"
access: ["SandCoin", "RiverCoin"]
access: []
links:
- target_room: 9 # Focus Tower Windia
location: "FocusTowerWindia"
@@ -739,7 +739,7 @@
object_id: 0x2E
type: "Box"
access: []
- name: "Kaeli Companion"
- name: "Kaeli 1"
object_id: 0
type: "Trigger"
on_trigger: ["Kaeli1"]
@@ -838,7 +838,7 @@
- name: Sand Temple
id: 24
game_objects:
- name: "Tristam Companion"
- name: "Tristam Sand Temple"
object_id: 0
type: "Trigger"
on_trigger: ["Tristam"]
@@ -883,11 +883,6 @@
object_id: 2
type: "NPC"
access: ["Tristam"]
- name: "Tristam Bone Dungeon Item Given"
object_id: 0
type: "Trigger"
on_trigger: ["TristamBoneItemGiven"]
access: ["Tristam"]
links:
- target_room: 25
entrance: 59
@@ -1085,7 +1080,7 @@
object_id: 0x40
type: "Box"
access: []
- name: "Phoebe Companion"
- name: "Phoebe"
object_id: 0
type: "Trigger"
on_trigger: ["Phoebe1"]
@@ -1851,11 +1846,11 @@
access: []
- target_room: 77
entrance: 164
teleporter: [98, 8] # original value [16, 2]
teleporter: [16, 2]
access: []
- target_room: 82
entrance: 165
teleporter: [96, 8] # original value [17, 2]
teleporter: [17, 2]
access: []
- target_room: 208
access: ["Claw"]
@@ -1880,7 +1875,7 @@
object_id: 14
type: "NPC"
access: ["ReubenDadSaved"]
- name: "Reuben Companion"
- name: "Reuben"
object_id: 0
type: "Trigger"
on_trigger: ["Reuben1"]
@@ -1956,7 +1951,12 @@
- name: "Fireburg - Tristam"
object_id: 10
type: "NPC"
access: ["Tristam", "TristamBoneItemGiven"]
access: []
- name: "Tristam Fireburg"
object_id: 0
type: "Trigger"
on_trigger: ["Tristam"]
access: []
links:
- target_room: 76
entrance: 177
@@ -3183,7 +3183,7 @@
access: []
- target_room: 163
entrance: 321
teleporter: [97, 8]
teleporter: [31, 2]
access: []
- target_room: 165
entrance: 322
@@ -3292,7 +3292,7 @@
access: []
- target_room: 164
entrance: 337
teleporter: [102, 8]
teleporter: [215, 0]
access: []
- name: Windia Inn Beds
id: 164

View File

@@ -73,57 +73,14 @@ Final Fantasy Mystic Quest:
Chaos: 0
SelfDestruct: 0
SimpleShuffle: 0
enemizer_groups:
MobsOnly: 0
MobsBosses: 0
MobsBossesDK: 0
shuffle_res_weak_type:
true: 0
false: 0
leveling_curve:
Half: 0
Normal: 0
OneAndHalf: 0
Double: 0
DoubleAndHalf: 0
DoubleHalf: 0
Triple: 0
Quadruple: 0
companion_leveling_type:
Quests: 0
QuestsExtended: 0
SaveCrystalsIndividual: 0
SaveCrystalsAll: 0
BenPlus0: 0
BenPlus5: 0
BenPlus10: 0
companion_spellbook_type:
Standard: 0
Extended: 0
RandomBalanced: 0
RandomChaos: 0
starting_companion:
None: 0
Kaeli: 0
Tristam: 0
Phoebe: 0
Reuben: 0
Random: 0
RandomPlusNone: 0
available_companions:
Zero: 0
One: 0
Two: 0
Three: 0
Four: 0
Random14: 0
Random04: 0
companions_locations:
Standard: 0
Shuffled: 0
ShuffledExtended: 0
kaelis_mom_fight_minotaur:
true: 0
false: 0
battles_quantity:
Ten: 0
Seven: 0

View File

@@ -419,16 +419,17 @@ class HKWorld(World):
def set_rules(self):
world = self.multiworld
player = self.player
goal = world.Goal[player]
if goal == Goal.option_hollowknight:
world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player)
elif goal == Goal.option_siblings:
world.completion_condition[player] = lambda state: state._hk_siblings_ending(player)
elif goal == Goal.option_radiance:
world.completion_condition[player] = lambda state: state._hk_can_beat_radiance(player)
else:
# Any goal
world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) or state._hk_can_beat_radiance(player)
if world.logic[player] != 'nologic':
goal = world.Goal[player]
if goal == Goal.option_hollowknight:
world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player)
elif goal == Goal.option_siblings:
world.completion_condition[player] = lambda state: state._hk_siblings_ending(player)
elif goal == Goal.option_radiance:
world.completion_condition[player] = lambda state: state._hk_can_beat_radiance(player)
else:
# Any goal
world.completion_condition[player] = lambda state: state._hk_can_beat_thk(player) or state._hk_can_beat_radiance(player)
set_rules(self)

View File

@@ -29,7 +29,6 @@ class KH2Context(CommonContext):
self.kh2_local_items = None
self.growthlevel = None
self.kh2connected = False
self.kh2_finished_game = False
self.serverconneced = False
self.item_name_to_data = {name: data for name, data, in item_dictionary_table.items()}
self.location_name_to_data = {name: data for name, data, in all_locations.items()}
@@ -834,9 +833,9 @@ async def kh2_watcher(ctx: KH2Context):
await asyncio.create_task(ctx.verifyItems())
await asyncio.create_task(ctx.verifyLevel())
message = [{"cmd": 'LocationChecks', "locations": ctx.sending}]
if finishedGame(ctx, message) and not ctx.kh2_finished_game:
if finishedGame(ctx, message):
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
ctx.kh2_finished_game = True
ctx.finished_game = True
await ctx.send_msgs(message)
elif not ctx.kh2connected and ctx.serverconneced:
logger.info("Game Connection lost. waiting 15 seconds until trying to reconnect.")

View File

@@ -1020,9 +1020,10 @@ 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
multiworld.worlds[player].item_name_to_id.update({event_name: None for event_name in Events_Table})
for location, item in event_location_to_item.items():
multiworld.get_location(location, player).place_locked_item(
multiworld.worlds[player].create_event_item(item))
multiworld.worlds[player].create_item(item))
def connect_regions(self):

View File

@@ -224,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),
RegionName.Bc: lambda state: self.bc_unlocked(state, 1),
RegionName.Bc2: lambda state: self.bc_unlocked(state, 2),

View File

@@ -119,15 +119,11 @@ class KH2World(World):
item_classification = ItemClassification.useful
else:
item_classification = ItemClassification.filler
created_item = KH2Item(name, item_classification, self.item_name_to_id[name], self.player)
return created_item
def create_event_item(self, name: str) -> Item:
item_classification = ItemClassification.progression
created_item = KH2Item(name, item_classification, None, self.player)
return created_item
def create_items(self) -> None:
"""
Fills ItemPool and manages schmovement, random growth, visit locking and random starting visit locking.
@@ -465,7 +461,7 @@ class KH2World(World):
if location in self.random_super_boss_list:
self.random_super_boss_list.remove(location)
if not self.options.SummonLevelLocationToggle and LocationName.Summonlvl7 in self.random_super_boss_list:
if not self.options.SummonLevelLocationToggle:
self.random_super_boss_list.remove(LocationName.Summonlvl7)
# Testing if the player has the right amount of Bounties for Completion.

View File

@@ -349,19 +349,18 @@ class GfxMod(FreeText, LADXROption):
normal = ''
default = 'Link'
__spriteDir: str = Utils.local_path(os.path.join('data', 'sprites','ladx'))
__spriteFiles: typing.DefaultDict[str, typing.List[str]] = defaultdict(list)
__spriteDir: str = None
extensions = [".bin", ".bdiff", ".png", ".bmp"]
for file in os.listdir(__spriteDir):
name, extension = os.path.splitext(file)
if extension in extensions:
__spriteFiles[name].append(file)
def __init__(self, value: str):
super().__init__(value)
if not GfxMod.__spriteDir:
GfxMod.__spriteDir = Utils.local_path(os.path.join('data', 'sprites','ladx'))
for file in os.listdir(GfxMod.__spriteDir):
name, extension = os.path.splitext(file)
if extension in self.extensions:
GfxMod.__spriteFiles[name].append(file)
def verify(self, world, player_name: str, plando_options) -> None:
if self.value == "Link" or self.value in GfxMod.__spriteFiles:

View File

@@ -1,8 +1,6 @@
"""
Archipelago init file for Lingo
"""
from logging import warning
from BaseClasses import Item, ItemClassification, Tutorial
from worlds.AutoWorld import WebWorld, World
from .items import ALL_ITEM_TABLE, LingoItem
@@ -51,14 +49,6 @@ class LingoWorld(World):
player_logic: LingoPlayerLogic
def generate_early(self):
if not (self.options.shuffle_doors or self.options.shuffle_colors):
if self.multiworld.players == 1:
warning(f"{self.multiworld.get_player_name(self.player)}'s Lingo world doesn't have any progression"
f" items. Please turn on Door Shuffle or Color Shuffle if that doesn't seem right.")
else:
raise Exception(f"{self.multiworld.get_player_name(self.player)}'s Lingo world doesn't have any"
f" progression items. Please turn on Door Shuffle or Color Shuffle.")
self.player_logic = LingoPlayerLogic(self)
def create_regions(self):
@@ -104,11 +94,9 @@ class LingoWorld(World):
classification = item.classification
if hasattr(self, "options") and self.options.shuffle_paintings and len(item.painting_ids) > 0\
and len(item.door_ids) == 0 and all(painting_id not in self.player_logic.painting_mapping
for painting_id in item.painting_ids)\
and "pilgrim_painting2" not in item.painting_ids:
for painting_id in item.painting_ids):
# If this is a "door" that just moves one or more paintings, and painting shuffle is on and those paintings
# go nowhere, then this item should not be progression. The Pilgrim Room painting is special and needs to be
# excluded from this.
# go nowhere, then this item should not be progression.
classification = ItemClassification.filler
return LingoItem(name, classification, item.code, self.player)

View File

@@ -373,7 +373,6 @@
ANOTHER TRY:
id: Entry Room/Panel_advance
tag: topwhite
non_counting: True # This is a counting panel in-game, but it can never count towards the LEVEL 2 panel hunt.
LEVEL 2:
# We will set up special rules for this in code.
id: EndPanel/Panel_level_2
@@ -1034,8 +1033,6 @@
Hallway Room (3): True
Hallway Room (4): True
Hedge Maze: True # through the door to the sectioned-off part of the hedge maze
Cellar:
door: Lookout Entrance
panels:
MASSACRED:
id: Palindrome Room/Panel_massacred_sacred
@@ -1171,21 +1168,11 @@
- KEEP
- BAILEY
- TOWER
Lookout Entrance:
id: Cross Room Doors/Door_missing
location_name: Outside The Agreeable - Lookout Panels
panels:
- NORTH
- WINTER
- DIAMONDS
- FIRE
paintings:
- id: panda_painting
orientation: south
- id: eyes_yellow_painting
orientation: east
- id: pencil_painting7
orientation: north
progression:
Progressive Hallway Room:
- Hallway Door
@@ -2056,7 +2043,7 @@
door: Sixth Floor
Cellar:
room: Room Room
door: Cellar Exit
door: Shortcut to Fifth Floor
Welcome Back Area:
door: Welcome Back
Art Gallery:
@@ -2315,6 +2302,9 @@
id: Master Room/Panel_mastery_mastery3
tag: midwhite
hunt: True
required_door:
room: Orange Tower Seventh Floor
door: Mastery
THE LIBRARY:
id: EndPanel/Panel_library
check: True
@@ -2685,10 +2675,6 @@
Outside The Undeterred: True
Outside The Agreeable: True
Outside The Wanderer: True
The Observant: True
Art Gallery: True
The Scientific: True
Cellar: True
Orange Tower Fifth Floor:
room: Orange Tower Fifth Floor
door: Welcome Back
@@ -3005,7 +2991,8 @@
PATS:
id: Rhyme Room/Panel_wrath_path
colors: purple
tag: forbid
tag: midpurp and rhyme
copy_to_sign: sign15
KNIGHT:
id: Rhyme Room/Panel_knight_write
colors: purple
@@ -3171,8 +3158,6 @@
door: Painting Shortcut
painting: True
Room Room: True # trapdoor
Outside The Agreeable:
painting: True
panels:
UNOPEN:
id: Truncate Room/Panel_unopened_open
@@ -6314,22 +6299,17 @@
SKELETON:
id: Double Room/Panel_bones_syn
tag: syn rhyme
colors: purple
subtag: bot
link: rhyme BONES
REPENTANCE:
id: Double Room/Panel_sentence_rhyme
colors:
- purple
- blue
colors: purple
tag: whole rhyme
subtag: top
link: rhyme SENTENCE
WORD:
id: Double Room/Panel_sentence_whole
colors:
- purple
- blue
colors: blue
tag: whole rhyme
subtag: bot
link: rhyme SENTENCE
@@ -6341,7 +6321,6 @@
link: rhyme DREAM
FANTASY:
id: Double Room/Panel_dream_syn
colors: purple
tag: syn rhyme
subtag: bot
link: rhyme DREAM
@@ -6353,7 +6332,6 @@
link: rhyme MYSTERY
SECRET:
id: Double Room/Panel_mystery_syn
colors: purple
tag: syn rhyme
subtag: bot
link: rhyme MYSTERY
@@ -6408,33 +6386,25 @@
door: Nines
FERN:
id: Double Room/Panel_return_rhyme
colors:
- purple
- black
colors: purple
tag: ant rhyme
subtag: top
link: rhyme RETURN
STAY:
id: Double Room/Panel_return_ant
colors:
- purple
- black
colors: black
tag: ant rhyme
subtag: bot
link: rhyme RETURN
FRIEND:
id: Double Room/Panel_descend_rhyme
colors:
- purple
- black
colors: purple
tag: ant rhyme
subtag: top
link: rhyme DESCEND
RISE:
id: Double Room/Panel_descend_ant
colors:
- purple
- black
colors: black
tag: ant rhyme
subtag: bot
link: rhyme DESCEND
@@ -6446,7 +6416,6 @@
link: rhyme JUMP
BOUNCE:
id: Double Room/Panel_jump_syn
colors: purple
tag: syn rhyme
subtag: bot
link: rhyme JUMP
@@ -6458,7 +6427,6 @@
link: rhyme FALL
PLUNGE:
id: Double Room/Panel_fall_syn
colors: purple
tag: syn rhyme
subtag: bot
link: rhyme FALL
@@ -6488,17 +6456,13 @@
panels:
BIRD:
id: Double Room/Panel_word_rhyme
colors:
- purple
- blue
colors: purple
tag: whole rhyme
subtag: top
link: rhyme WORD
LETTER:
id: Double Room/Panel_word_whole
colors:
- purple
- blue
colors: blue
tag: whole rhyme
subtag: bot
link: rhyme WORD
@@ -6510,7 +6474,6 @@
link: rhyme HIDDEN
CONCEALED:
id: Double Room/Panel_hidden_syn
colors: purple
tag: syn rhyme
subtag: bot
link: rhyme HIDDEN
@@ -6522,7 +6485,6 @@
link: rhyme SILENT
MUTE:
id: Double Room/Panel_silent_syn
colors: purple
tag: syn rhyme
subtag: bot
link: rhyme SILENT
@@ -6569,7 +6531,6 @@
link: rhyme BLOCKED
OBSTRUCTED:
id: Double Room/Panel_blocked_syn
colors: purple
tag: syn rhyme
subtag: bot
link: rhyme BLOCKED
@@ -6581,7 +6542,6 @@
link: rhyme RISE
SWELL:
id: Double Room/Panel_rise_syn
colors: purple
tag: syn rhyme
subtag: bot
link: rhyme RISE
@@ -6593,7 +6553,6 @@
link: rhyme ASCEND
CLIMB:
id: Double Room/Panel_ascend_syn
colors: purple
tag: syn rhyme
subtag: bot
link: rhyme ASCEND
@@ -6605,7 +6564,6 @@
link: rhyme DOUBLE
DUPLICATE:
id: Double Room/Panel_double_syn
colors: purple
tag: syn rhyme
subtag: bot
link: rhyme DOUBLE
@@ -6684,7 +6642,6 @@
link: rhyme CHILD
KID:
id: Double Room/Panel_child_syn
colors: purple
tag: syn rhyme
subtag: bot
link: rhyme CHILD
@@ -6696,7 +6653,6 @@
link: rhyme CRYSTAL
QUARTZ:
id: Double Room/Panel_crystal_syn
colors: purple
tag: syn rhyme
subtag: bot
link: rhyme CRYSTAL
@@ -6708,7 +6664,6 @@
link: rhyme CREATIVE
INNOVATIVE (Bottom):
id: Double Room/Panel_creative_syn
colors: purple
tag: syn rhyme
subtag: bot
link: rhyme CREATIVE
@@ -6927,7 +6882,7 @@
event: True
panels:
- WALL (1)
Cellar Exit:
Shortcut to Fifth Floor:
id:
- Tower Room Area Doors/Door_panel_basement
- Tower Room Area Doors/Door_panel_basement2
@@ -6940,10 +6895,7 @@
door: Excavation
Orange Tower Fifth Floor:
room: Room Room
door: Cellar Exit
Outside The Agreeable:
room: Outside The Agreeable
door: Lookout Entrance
door: Shortcut to Fifth Floor
Outside The Wise:
entrances:
Orange Tower Sixth Floor:
@@ -7367,65 +7319,49 @@
link: change GRAVITY
PART:
id: Chemistry Room/Panel_physics_2
colors:
- blue
- red
colors: blue
tag: blue mid red bot
subtag: mid
link: xur PARTICLE
MATTER:
id: Chemistry Room/Panel_physics_1
colors:
- blue
- red
colors: red
tag: blue mid red bot
subtag: bot
link: xur PARTICLE
ELECTRIC:
id: Chemistry Room/Panel_physics_6
colors:
- purple
- red
colors: purple
tag: purple mid red bot
subtag: mid
link: xpr ELECTRON
ATOM (1):
id: Chemistry Room/Panel_physics_3
colors:
- purple
- red
colors: red
tag: purple mid red bot
subtag: bot
link: xpr ELECTRON
NEUTRAL:
id: Chemistry Room/Panel_physics_7
colors:
- purple
- red
colors: purple
tag: purple mid red bot
subtag: mid
link: xpr NEUTRON
ATOM (2):
id: Chemistry Room/Panel_physics_4
colors:
- purple
- red
colors: red
tag: purple mid red bot
subtag: bot
link: xpr NEUTRON
PROPEL:
id: Chemistry Room/Panel_physics_8
colors:
- purple
- red
colors: purple
tag: purple mid red bot
subtag: mid
link: xpr PROTON
ATOM (3):
id: Chemistry Room/Panel_physics_5
colors:
- purple
- red
colors: red
tag: purple mid red bot
subtag: bot
link: xpr PROTON

View File

@@ -1064,9 +1064,6 @@ doors:
Hallway Door:
item: 444459
location: 445214
Lookout Entrance:
item: 444579
location: 445271
Dread Hallway:
Tenacious Entrance:
item: 444462
@@ -1405,7 +1402,7 @@ doors:
item: 444570
location: 445266
Room Room:
Cellar Exit:
Shortcut to Fifth Floor:
item: 444571
location: 445076
Outside The Wise:

View File

@@ -32,7 +32,7 @@ class LocationChecks(Choice):
option_insanity = 2
class ShuffleColors(DefaultOnToggle):
class ShuffleColors(Toggle):
"""If on, an item is added to the pool for every puzzle color (besides White).
You will need to unlock the requisite colors in order to be able to solve puzzles of that color."""
display_name = "Shuffle Colors"

View File

@@ -190,25 +190,6 @@ class LingoPlayerLogic:
if item.should_include(world):
self.real_items.append(name)
# Calculate the requirements for the fake pilgrimage.
fake_pilgrimage = [
["Second Room", "Exit Door"], ["Crossroads", "Tower Entrance"],
["Orange Tower Fourth Floor", "Hot Crusts Door"], ["Outside The Initiated", "Shortcut to Hub Room"],
["Orange Tower First Floor", "Shortcut to Hub Room"], ["Directional Gallery", "Shortcut to The Undeterred"],
["Orange Tower First Floor", "Salt Pepper Door"], ["Hub Room", "Crossroads Entrance"],
["Champion's Rest", "Shortcut to The Steady"], ["The Bearer", "Shortcut to The Bold"],
["Art Gallery", "Exit"], ["The Tenacious", "Shortcut to Hub Room"],
["Outside The Agreeable", "Tenacious Entrance"]
]
pilgrimage_reqs = AccessRequirements()
for door in fake_pilgrimage:
door_object = DOORS_BY_ROOM[door[0]][door[1]]
if door_object.event or world.options.shuffle_doors == ShuffleDoors.option_none:
pilgrimage_reqs.merge(self.calculate_door_requirements(door[0], door[1], world))
else:
pilgrimage_reqs.doors.add(RoomAndDoor(door[0], door[1]))
self.door_reqs.setdefault("Pilgrim Antechamber", {})["Pilgrimage"] = pilgrimage_reqs
# Create the paintings mapping, if painting shuffle is on.
if painting_shuffle:
# Shuffle paintings until we get something workable.
@@ -388,9 +369,11 @@ class LingoPlayerLogic:
door_object = DOORS_BY_ROOM[room][door]
for req_panel in door_object.panels:
panel_room = room if req_panel.room is None else req_panel.room
access_reqs.rooms.add(panel_room)
sub_access_reqs = self.calculate_panel_requirements(panel_room, req_panel.panel, world)
if req_panel.room is not None and req_panel.room != room:
access_reqs.rooms.add(req_panel.room)
sub_access_reqs = self.calculate_panel_requirements(room if req_panel.room is None else req_panel.room,
req_panel.panel, world)
access_reqs.merge(sub_access_reqs)
self.door_reqs[room][door] = access_reqs
@@ -414,8 +397,8 @@ class LingoPlayerLogic:
unhindered_panels_by_color: dict[Optional[str], int] = {}
for panel_name, panel_data in room_data.items():
# We won't count non-counting panels. THE MASTER has special access rules and is handled separately.
if panel_data.non_counting or panel_name == "THE MASTER":
# We won't count non-counting panels.
if panel_data.non_counting:
continue
# We won't coalesce any panels that have requirements beyond colors. To simplify things for now, we will

View File

@@ -4,7 +4,7 @@ from BaseClasses import Entrance, ItemClassification, Region
from .items import LingoItem
from .locations import LingoLocation
from .player_logic import LingoPlayerLogic
from .rules import lingo_can_use_entrance, make_location_lambda
from .rules import lingo_can_use_entrance, lingo_can_use_pilgrimage, make_location_lambda
from .static_logic import ALL_ROOMS, PAINTINGS, Room, RoomAndDoor
if TYPE_CHECKING:
@@ -25,6 +25,15 @@ def create_region(room: Room, world: "LingoWorld", player_logic: LingoPlayerLogi
return new_region
def handle_pilgrim_room(regions: Dict[str, Region], world: "LingoWorld", player_logic: LingoPlayerLogic) -> None:
target_region = regions["Pilgrim Antechamber"]
source_region = regions["Outside The Agreeable"]
source_region.connect(
target_region,
"Pilgrimage",
lambda state: lingo_can_use_pilgrimage(state, world, player_logic))
def connect_entrance(regions: Dict[str, Region], source_region: Region, target_region: Region, description: str,
door: Optional[RoomAndDoor], world: "LingoWorld", player_logic: LingoPlayerLogic):
connection = Entrance(world.player, description, source_region)
@@ -82,9 +91,7 @@ def create_regions(world: "LingoWorld", player_logic: LingoPlayerLogic) -> None:
connect_entrance(regions, regions[entrance.room], regions[room.name], entrance_name, entrance.door, world,
player_logic)
# Add the fake pilgrimage.
connect_entrance(regions, regions["Outside The Agreeable"], regions["Pilgrim Antechamber"], "Pilgrimage",
RoomAndDoor("Pilgrim Antechamber", "Pilgrimage"), world, player_logic)
handle_pilgrim_room(regions, world, player_logic)
if early_color_hallways:
regions["Starting Room"].connect(regions["Outside The Undeterred"], "Early Color Hallways")

View File

@@ -17,6 +17,23 @@ def lingo_can_use_entrance(state: CollectionState, room: str, door: RoomAndDoor,
return _lingo_can_open_door(state, effective_room, door.door, world, player_logic)
def lingo_can_use_pilgrimage(state: CollectionState, world: "LingoWorld", player_logic: LingoPlayerLogic):
fake_pilgrimage = [
["Second Room", "Exit Door"], ["Crossroads", "Tower Entrance"],
["Orange Tower Fourth Floor", "Hot Crusts Door"], ["Outside The Initiated", "Shortcut to Hub Room"],
["Orange Tower First Floor", "Shortcut to Hub Room"], ["Directional Gallery", "Shortcut to The Undeterred"],
["Orange Tower First Floor", "Salt Pepper Door"], ["Hub Room", "Crossroads Entrance"],
["Champion's Rest", "Shortcut to The Steady"], ["The Bearer", "Shortcut to The Bold"],
["Art Gallery", "Exit"], ["The Tenacious", "Shortcut to Hub Room"],
["Outside The Agreeable", "Tenacious Entrance"]
]
for entrance in fake_pilgrimage:
if not _lingo_can_open_door(state, entrance[0], entrance[1], world, player_logic):
return False
return True
def lingo_can_use_location(state: CollectionState, location: PlayerLocation, world: "LingoWorld",
player_logic: LingoPlayerLogic):
return _lingo_can_satisfy_requirements(state, location.access, world, player_logic)
@@ -39,12 +56,6 @@ def lingo_can_use_level_2_location(state: CollectionState, world: "LingoWorld",
counted_panels += panel_count
if counted_panels >= world.options.level_2_requirement.value - 1:
return True
# THE MASTER has to be handled separately, because it has special access rules.
if state.can_reach("Orange Tower Seventh Floor", "Region", world.player)\
and lingo_can_use_mastery_location(state, world, player_logic):
counted_panels += 1
if counted_panels >= world.options.level_2_requirement.value - 1:
return True
return False

View File

@@ -3,8 +3,7 @@ from . import LingoTestBase
class TestRequiredRoomLogic(LingoTestBase):
options = {
"shuffle_doors": "complex",
"shuffle_colors": "false",
"shuffle_doors": "complex"
}
def test_pilgrim_first(self) -> None:
@@ -50,8 +49,7 @@ class TestRequiredRoomLogic(LingoTestBase):
class TestRequiredDoorLogic(LingoTestBase):
options = {
"shuffle_doors": "complex",
"shuffle_colors": "false",
"shuffle_doors": "complex"
}
def test_through_rhyme(self) -> None:
@@ -78,8 +76,7 @@ class TestRequiredDoorLogic(LingoTestBase):
class TestSimpleDoors(LingoTestBase):
options = {
"shuffle_doors": "simple",
"shuffle_colors": "false",
"shuffle_doors": "simple"
}
def test_requirement(self):

View File

@@ -81,8 +81,7 @@ class TestSimpleHallwayRoom(LingoTestBase):
class TestProgressiveArtGallery(LingoTestBase):
options = {
"shuffle_doors": "complex",
"shuffle_colors": "false",
"shuffle_doors": "complex"
}
def test_item(self):

View File

@@ -40,7 +40,7 @@ mentioned_panels = Set[]
door_groups = {}
directives = Set["entrances", "panels", "doors", "paintings", "progression"]
panel_directives = Set["id", "required_room", "required_door", "required_panel", "colors", "check", "exclude_reduce", "tag", "link", "subtag", "achievement", "copy_to_sign", "non_counting", "hunt"]
panel_directives = Set["id", "required_room", "required_door", "required_panel", "colors", "check", "exclude_reduce", "tag", "link", "subtag", "achievement", "copy_to_sign", "non_counting"]
door_directives = Set["id", "painting_id", "panels", "item_name", "location_name", "skip_location", "skip_item", "group", "include_reduce", "junk_item", "event"]
painting_directives = Set["id", "enter_only", "exit_only", "orientation", "required_door", "required", "required_when_no_doors", "move", "req_blocked", "req_blocked_when_no_doors"]

View File

@@ -170,9 +170,6 @@ pullpc
ScriptTX:
STA $7FD4F1 ; (overwritten instruction)
LDA $05AC ; load map number
CMP.b #$F1 ; check if ancient cave final floor
BNE +
REP #$20
LDA $7FD4EF ; read script item id
CMP.w #$01C2 ; test for ancient key
@@ -264,9 +261,6 @@ SpecialItemGet:
BRA ++
+: CMP.w #$01C2 ; ancient key
BNE +
LDA.w #$0008
ORA $0796
STA $0796 ; set ancient key EV flag ($C3)
LDA.w #$0200
ORA $0797
STA $0797 ; set boss item EV flag ($D1)

View File

@@ -62,7 +62,7 @@ class MessengerWorld(World):
"Money Wrench",
], base_offset)}
required_client_version = (0, 4, 2)
required_client_version = (0, 4, 1)
web = MessengerWeb()

View File

@@ -1,10 +1,12 @@
# The Messenger
## Quick Links
- [Setup](/tutorial/The%20Messenger/setup/en)
- [Options Page](/games/The%20Messenger/player-options)
- [Setup](../../../../tutorial/The%20Messenger/setup/en)
- [Settings Page](../../../../games/The%20Messenger/player-settings)
- [Courier Github](https://github.com/Brokemia/Courier)
- [The Messenger Randomizer Github](https://github.com/minous27/TheMessengerRandomizerMod)
- [The Messenger Randomizer AP Github](https://github.com/alwaysintreble/TheMessengerRandomizerModAP)
- [Jacksonbird8237's Item Tracker](https://github.com/Jacksonbird8237/TheMessengerItemTracker)
- [PopTracker Pack](https://github.com/alwaysintreble/TheMessengerTrackPack)
## What does randomization do in this game?

View File

@@ -1,15 +1,16 @@
# The Messenger Randomizer Setup Guide
## Quick Links
- [Game Info](/games/The%20Messenger/info/en)
- [Options Page](/games/The%20Messenger/player-options)
- [Game Info](../../../../games/The%20Messenger/info/en)
- [Settings Page](../../../../games/The%20Messenger/player-settings)
- [Courier Github](https://github.com/Brokemia/Courier)
- [The Messenger Randomizer AP Github](https://github.com/alwaysintreble/TheMessengerRandomizerModAP)
- [Jacksonbird8237's Item Tracker](https://github.com/Jacksonbird8237/TheMessengerItemTracker)
- [PopTracker Pack](https://github.com/alwaysintreble/TheMessengerTrackPack)
## Installation
1. Read the [Game Info Page](/games/The%20Messenger/info/en) for how the game works, caveats and known issues
1. Read the [Game Info Page](../../../../games/The%20Messenger/info/en) for how the game works, caveats and known issues
2. Download and install Courier Mod Loader using the instructions on the release page
* [Latest release is currently 0.7.1](https://github.com/Brokemia/Courier/releases)
3. Download and install the randomizer mod

View File

@@ -63,10 +63,7 @@ class MessengerRules:
"Searing Crags Seal - Triple Ball Spinner": self.has_vertical,
"Searing Crags - Astral Tea Leaves":
lambda state: state.can_reach("Ninja Village - Astral Seed", "Location", self.player),
"Searing Crags - Key of Strength": lambda state: state.has("Power Thistle", self.player)
and (self.has_dart(state)
or (self.has_wingsuit(state)
and self.can_destroy_projectiles(state))),
"Searing Crags - Key of Strength": lambda state: state.has("Power Thistle", self.player),
# glacial peak
"Glacial Peak Seal - Ice Climbers": self.has_dart,
"Glacial Peak Seal - Projectile Spike Pit": self.can_destroy_projectiles,

View File

@@ -1106,30 +1106,21 @@
"parent_map": "MAP_ROUTE120",
"locations": [
"ITEM_ROUTE_120_NUGGET",
"ITEM_ROUTE_120_FULL_HEAL",
"ITEM_ROUTE_120_REVIVE",
"ITEM_ROUTE_120_HYPER_POTION",
"HIDDEN_ITEM_ROUTE_120_RARE_CANDY_2",
"HIDDEN_ITEM_ROUTE_120_ZINC"
],
"events": [],
"exits": [
"REGION_ROUTE120/NORTH",
"REGION_ROUTE120/SOUTH_PONDS",
"REGION_ROUTE121/WEST"
],
"warps": [
"MAP_ROUTE120:0/MAP_ANCIENT_TOMB:0"
]
},
"REGION_ROUTE120/SOUTH_PONDS": {
"parent_map": "MAP_ROUTE120",
"locations": [
"HIDDEN_ITEM_ROUTE_120_RARE_CANDY_2",
"ITEM_ROUTE_120_FULL_HEAL"
],
"events": [],
"exits": [],
"warps": []
},
"REGION_ROUTE121/WEST": {
"parent_map": "MAP_ROUTE121",
"locations": [

View File

@@ -626,10 +626,6 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
get_entrance("REGION_ROUTE120/NORTH_POND_SHORE -> REGION_ROUTE120/NORTH_POND"),
can_surf
)
set_rule(
get_entrance("REGION_ROUTE120/SOUTH -> REGION_ROUTE120/SOUTH_PONDS"),
can_surf
)
# Route 121
set_rule(

View File

@@ -44,17 +44,13 @@ class TestScorchedSlabPond(PokemonEmeraldTestBase):
class TestSurf(PokemonEmeraldTestBase):
options = {
"npc_gifts": Toggle.option_true,
"hidden_items": Toggle.option_true,
"require_itemfinder": Toggle.option_false
"npc_gifts": Toggle.option_true
}
def test_inaccessible_with_no_surf(self) -> None:
self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_PETALBURG_CITY_ETHER")))
self.assertFalse(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_SOOTHE_BELL")))
self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_LILYCOVE_CITY_MAX_REPEL")))
self.assertFalse(self.can_reach_location(location_name_to_label("HIDDEN_ITEM_ROUTE_120_RARE_CANDY_2")))
self.assertFalse(self.can_reach_location(location_name_to_label("ITEM_ROUTE_120_FULL_HEAL")))
self.assertFalse(self.can_reach_entrance("REGION_ROUTE118/WATER -> REGION_ROUTE118/EAST"))
self.assertFalse(self.can_reach_entrance("REGION_ROUTE119/UPPER -> REGION_FORTREE_CITY/MAIN"))
self.assertFalse(self.can_reach_entrance("MAP_FORTREE_CITY:3/MAP_FORTREE_CITY_MART:0"))
@@ -64,8 +60,6 @@ class TestSurf(PokemonEmeraldTestBase):
self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_PETALBURG_CITY_ETHER")))
self.assertTrue(self.can_reach_location(location_name_to_label("NPC_GIFT_RECEIVED_SOOTHE_BELL")))
self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_LILYCOVE_CITY_MAX_REPEL")))
self.assertTrue(self.can_reach_location(location_name_to_label("HIDDEN_ITEM_ROUTE_120_RARE_CANDY_2")))
self.assertTrue(self.can_reach_location(location_name_to_label("ITEM_ROUTE_120_FULL_HEAL")))
self.assertTrue(self.can_reach_entrance("REGION_ROUTE118/WATER -> REGION_ROUTE118/EAST"))
self.assertTrue(self.can_reach_entrance("REGION_ROUTE119/UPPER -> REGION_FORTREE_CITY/MAIN"))
self.assertTrue(self.can_reach_entrance("MAP_FORTREE_CITY:3/MAP_FORTREE_CITY_MART:0"))

View File

@@ -6,7 +6,7 @@ from NetUtils import ClientStatus
from worlds._bizhawk.client import BizHawkClient
from worlds._bizhawk import read, write, guarded_write
from .locations import location_data
from worlds.pokemon_rb.locations import location_data
logger = logging.getLogger("Client")

View File

@@ -1631,7 +1631,7 @@ def create_regions(self):
connect(multiworld, player, "Cerulean City", "Route 24", one_way=True)
connect(multiworld, player, "Cerulean City", "Cerulean City-T", lambda state: state.has("Help Bill", player))
connect(multiworld, player, "Cerulean City-Outskirts", "Cerulean City", one_way=True)
connect(multiworld, player, "Cerulean City", "Cerulean City-Outskirts", lambda state: logic.can_cut(state, player), one_way=True)
connect(multiworld, player, "Cerulean City-Outskirts", "Cerulean City", lambda state: logic.can_cut(state, player))
connect(multiworld, player, "Cerulean City-Outskirts", "Route 9", lambda state: logic.can_cut(state, player))
connect(multiworld, player, "Cerulean City-Outskirts", "Route 5")
connect(multiworld, player, "Cerulean Cave B1F", "Cerulean Cave B1F-E", lambda state: logic.can_surf(state, player), one_way=True)
@@ -1707,6 +1707,7 @@ def create_regions(self):
connect(multiworld, player, "Route 12-S", "Route 12-Grass", lambda state: logic.can_cut(state, player), one_way=True)
connect(multiworld, player, "Route 12-L", "Lavender Town")
connect(multiworld, player, "Route 10-S", "Lavender Town")
connect(multiworld, player, "Route 8-W", "Saffron City")
connect(multiworld, player, "Route 8", "Lavender Town")
connect(multiworld, player, "Pokemon Tower 6F", "Pokemon Tower 6F-S", lambda state: state.has("Silph Scope", player) or (state.has("Buy Poke Doll", player) and state.multiworld.poke_doll_skip[player]))
connect(multiworld, player, "Route 8", "Route 8-Grass", lambda state: logic.can_cut(state, player), one_way=True)
@@ -1830,8 +1831,7 @@ def create_regions(self):
connect(multiworld, player, "Silph Co 6F", "Silph Co 6F-SW", lambda state: logic.card_key(state, 6, player))
connect(multiworld, player, "Silph Co 7F", "Silph Co 7F-E", lambda state: logic.card_key(state, 7, player))
connect(multiworld, player, "Silph Co 7F-SE", "Silph Co 7F-E", lambda state: logic.card_key(state, 7, player))
connect(multiworld, player, "Silph Co 8F", "Silph Co 8F-W", lambda state: logic.card_key(state, 8, player), one_way=True, name="Silph Co 8F to Silph Co 8F-W (Card Key)")
connect(multiworld, player, "Silph Co 8F-W", "Silph Co 8F", lambda state: logic.card_key(state, 8, player), one_way=True, name="Silph Co 8F-W to Silph Co 8F (Card Key)")
connect(multiworld, player, "Silph Co 8F", "Silph Co 8F-W", lambda state: logic.card_key(state, 8, player))
connect(multiworld, player, "Silph Co 9F", "Silph Co 9F-SW", lambda state: logic.card_key(state, 9, player))
connect(multiworld, player, "Silph Co 9F-NW", "Silph Co 9F-SW", lambda state: logic.card_key(state, 9, player))
connect(multiworld, player, "Silph Co 10F", "Silph Co 10F-SE", lambda state: logic.card_key(state, 10, player))
@@ -1864,23 +1864,22 @@ def create_regions(self):
# access to any part of a city will enable flying to the Pokemon Center
connect(multiworld, player, "Cerulean City-Cave", "Cerulean City", lambda state: logic.can_fly(state, player), one_way=True)
connect(multiworld, player, "Cerulean City-Badge House Backyard", "Cerulean City", lambda state: logic.can_fly(state, player), one_way=True)
connect(multiworld, player, "Cerulean City-T", "Cerulean City", lambda state: logic.can_fly(state, player), one_way=True, name="Cerulean City-T to Cerulean City (Fly)")
connect(multiworld, player, "Fuchsia City-Good Rod House Backyard", "Fuchsia City", lambda state: logic.can_fly(state, player), one_way=True)
connect(multiworld, player, "Saffron City-G", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True, name="Saffron City-G to Saffron City (Fly)")
connect(multiworld, player, "Saffron City-Pidgey", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True, name="Saffron City-Pidgey to Saffron City (Fly)")
connect(multiworld, player, "Saffron City-Silph", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True, name="Saffron City-Silph to Saffron City (Fly)")
connect(multiworld, player, "Saffron City-Copycat", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True, name="Saffron City-Copycat to Saffron City (Fly)")
connect(multiworld, player, "Celadon City-G", "Celadon City", lambda state: logic.can_fly(state, player), one_way=True, name="Celadon City-G to Celadon City (Fly)")
connect(multiworld, player, "Vermilion City-G", "Vermilion City", lambda state: logic.can_fly(state, player), one_way=True, name="Vermilion City-G to Vermilion City (Fly)")
connect(multiworld, player, "Vermilion City-Dock", "Vermilion City", lambda state: logic.can_fly(state, player), one_way=True, name="Vermilion City-Dock to Vermilion City (Fly)")
connect(multiworld, player, "Cinnabar Island-G", "Cinnabar Island", lambda state: logic.can_fly(state, player), one_way=True, name="Cinnabar Island-G to Cinnabar Island (Fly)")
connect(multiworld, player, "Cinnabar Island-M", "Cinnabar Island", lambda state: logic.can_fly(state, player), one_way=True, name="Cinnabar Island-M to Cinnabar Island (Fly)")
connect(multiworld, player, "Saffron City-G", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True)
connect(multiworld, player, "Saffron City-Pidgey", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True)
connect(multiworld, player, "Saffron City-Silph", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True)
connect(multiworld, player, "Saffron City-Copycat", "Saffron City", lambda state: logic.can_fly(state, player), one_way=True)
connect(multiworld, player, "Celadon City-G", "Celadon City", lambda state: logic.can_fly(state, player), one_way=True)
connect(multiworld, player, "Vermilion City-G", "Vermilion City", lambda state: logic.can_fly(state, player), one_way=True)
connect(multiworld, player, "Vermilion City-Dock", "Vermilion City", lambda state: logic.can_fly(state, player), one_way=True)
connect(multiworld, player, "Cinnabar Island-G", "Cinnabar Island", lambda state: logic.can_fly(state, player), one_way=True)
connect(multiworld, player, "Cinnabar Island-M", "Cinnabar Island", lambda state: logic.can_fly(state, player), one_way=True)
# drops
connect(multiworld, player, "Seafoam Islands 1F", "Seafoam Islands B1F", one_way=True, name="Seafoam Islands 1F to Seafoam Islands B1F (Drop)")
connect(multiworld, player, "Seafoam Islands 1F", "Seafoam Islands B1F-NE", one_way=True, name="Seafoam Islands 1F to Seafoam Islands B1F-NE (Drop)")
connect(multiworld, player, "Seafoam Islands B1F", "Seafoam Islands B2F-NW", one_way=True, name="Seafoam Islands 1F to Seafoam Islands B2F-NW (Drop)")
connect(multiworld, player, "Seafoam Islands 1F", "Seafoam Islands B1F", one_way=True)
connect(multiworld, player, "Seafoam Islands 1F", "Seafoam Islands B1F-NE", one_way=True)
connect(multiworld, player, "Seafoam Islands B1F", "Seafoam Islands B2F-NW", one_way=True)
connect(multiworld, player, "Seafoam Islands B1F-NE", "Seafoam Islands B2F-NE", one_way=True)
connect(multiworld, player, "Seafoam Islands B2F-NW", "Seafoam Islands B3F", lambda state: logic.can_strength(state, player) and state.has("Seafoam Exit Boulder", player, 6), one_way=True)
connect(multiworld, player, "Seafoam Islands B2F-NE", "Seafoam Islands B3F", lambda state: logic.can_strength(state, player) and state.has("Seafoam Exit Boulder", player, 6), one_way=True)
@@ -1889,7 +1888,7 @@ def create_regions(self):
# If you haven't dropped the boulders, you'll go straight to B4F
connect(multiworld, player, "Seafoam Islands B2F-NW", "Seafoam Islands B4F-W", one_way=True)
connect(multiworld, player, "Seafoam Islands B2F-NE", "Seafoam Islands B4F-W", one_way=True)
connect(multiworld, player, "Seafoam Islands B3F", "Seafoam Islands B4F", one_way=True, name="Seafoam Islands B1F to Seafoam Islands B4F (Drop)")
connect(multiworld, player, "Seafoam Islands B3F", "Seafoam Islands B4F", one_way=True)
connect(multiworld, player, "Seafoam Islands B3F", "Seafoam Islands B4F-W", lambda state: logic.can_surf(state, player), one_way=True)
connect(multiworld, player, "Pokemon Mansion 3F-SE", "Pokemon Mansion 2F", one_way=True)
connect(multiworld, player, "Pokemon Mansion 3F-SE", "Pokemon Mansion 1F-SE", one_way=True)
@@ -1945,8 +1944,7 @@ def create_regions(self):
connect(multiworld, player, region.name, entrance_data["to"]["map"],
lambda state: logic.rock_tunnel(state, player), one_way=True)
else:
connect(multiworld, player, region.name, entrance_data["to"]["map"], one_way=True,
name=entrance_data["name"] if "name" in entrance_data else None)
connect(multiworld, player, region.name, entrance_data["to"]["map"], one_way=True)
forced_connections = set()

View File

@@ -168,12 +168,12 @@ rom_addresses = {
"Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_0_ITEM": 0x1a61c,
"Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_1_ITEM": 0x1a62a,
"Trainersanity_EVENT_BEAT_SILPH_CO_6F_TRAINER_2_ITEM": 0x1a638,
"Event_SKC6F": 0x1a659,
"Warps_SilphCo6F": 0x1a737,
"Missable_Silph_Co_6F_Item_1": 0x1a787,
"Missable_Silph_Co_6F_Item_2": 0x1a78e,
"Path_Pallet_Oak": 0x1a914,
"Path_Pallet_Player": 0x1a921,
"Event_SKC6F": 0x1a666,
"Warps_SilphCo6F": 0x1a741,
"Missable_Silph_Co_6F_Item_1": 0x1a791,
"Missable_Silph_Co_6F_Item_2": 0x1a798,
"Path_Pallet_Oak": 0x1a91e,
"Path_Pallet_Player": 0x1a92b,
"Warps_CinnabarIsland": 0x1c026,
"Warps_Route1": 0x1c0e9,
"Option_Extra_Key_Items_B": 0x1ca46,

View File

@@ -103,10 +103,12 @@ class RiskOfRainWorld(World):
if self.options.dlc_sotv:
environment_offset_table = shift_by_offset(environment_sotv_table, environment_offset)
environments_pool = {**environments_pool, **environment_offset_table}
# percollect starting environment for stage 1
unlock = self.random.choices(list(environment_available_orderedstages_table[0].keys()), k=1)
self.multiworld.push_precollected(self.create_item(unlock[0]))
environments_pool.pop(unlock[0])
environments_to_precollect = 5 if self.options.begin_with_loop else 1
# percollect environments for each stage (or just stage 1)
for i in range(environments_to_precollect):
unlock = self.random.choices(list(environment_available_orderedstages_table[i].keys()), k=1)
self.multiworld.push_precollected(self.create_item(unlock[0]))
environments_pool.pop(unlock[0])
# Generate item pool
itempool: List[str] = ["Beads of Fealty", "Radar Scanner"]

View File

@@ -142,6 +142,14 @@ class FinalStageDeath(Toggle):
display_name = "Final Stage Death is Win"
class BeginWithLoop(Toggle):
"""
Enable to precollect a full loop of environments.
Only has an effect with Explore Mode.
"""
display_name = "Begin With Loop"
class DLC_SOTV(Toggle):
"""
Enable if you are using SOTV DLC.
@@ -377,6 +385,7 @@ class ROR2Options(PerGameCommonOptions):
total_revivals: TotalRevivals
start_with_revive: StartWithRevive
final_stage_death: FinalStageDeath
begin_with_loop: BeginWithLoop
dlc_sotv: DLC_SOTV
death_link: DeathLink
item_pickup_step: ItemPickupStep

View File

@@ -638,7 +638,7 @@ def set_mission_upgrade_rules_standard(multiworld: MultiWorld, world: World, pla
add_rule(multiworld.get_location(LocationName.radical_highway_omo_2, player),
lambda state: state.has(ItemName.shadow_air_shoes, player))
add_rule(multiworld.get_location(LocationName.weapons_bed_omo_2, player),
lambda state: state.has(ItemName.eggman_large_cannon, player))
lambda state: state.has(ItemName.eggman_jet_engine, player))
add_rule(multiworld.get_location(LocationName.mission_street_omo_3, player),
lambda state: state.has(ItemName.tails_booster, player))

View File

@@ -619,7 +619,7 @@ class SA2BWorld(World):
for name in name_list_base:
for char_idx in range(7):
if char_idx < len(name):
name_list_s.append(chao_name_conversion.get(name[char_idx], 0x5F))
name_list_s.append(chao_name_conversion[name[char_idx]])
else:
name_list_s.append(0x00)

View File

@@ -170,8 +170,7 @@ class GraphBuilder(object):
ap = "Landing Site" # dummy value it'll be overwritten at first collection
while len(itemLocs) > 0 and not (sm.canPassG4() and graph.canAccess(sm, ap, "Landing Site", maxDiff)):
il = itemLocs.pop(0)
# can happen with item links replacement items that its not in the container's itemPool
if il.Location.restricted or il.Item.Type == "ArchipelagoItem" or il.Item not in container.itemPool:
if il.Location.restricted or il.Item.Type == "ArchipelagoItem":
continue
self.log.debug("collecting " + getItemLocStr(il))
container.collect(il)

View File

@@ -1,77 +1,30 @@
import typing
from enum import Enum
from BaseClasses import MultiWorld, Region, Entrance, Location
from .Locations import SM64Location, location_table, locBoB_table, locWhomp_table, locJRB_table, locCCM_table, \
locBBH_table, \
locHMC_table, locLLL_table, locSSL_table, locDDD_table, locSL_table, \
locWDW_table, locTTM_table, locTHI_table, locTTC_table, locRR_table, \
locPSS_table, locSA_table, locBitDW_table, locTotWC_table, locCotMC_table, \
locVCutM_table, locBitFS_table, locWMotR_table, locBitS_table, locSS_table
locVCutM_table, locBitFS_table, locWMotR_table, locBitS_table, locSS_table
class SM64Levels(int, Enum):
BOB_OMB_BATTLEFIELD = 91
WHOMPS_FORTRESS = 241
JOLLY_ROGER_BAY = 121
COOL_COOL_MOUNTAIN = 51
BIG_BOOS_HAUNT = 41
HAZY_MAZE_CAVE = 71
LETHAL_LAVA_LAND = 221
SHIFTING_SAND_LAND = 81
DIRE_DIRE_DOCKS = 231
SNOWMANS_LAND = 101
WET_DRY_WORLD = 111
TALL_TALL_MOUNTAIN = 361
TINY_HUGE_ISLAND_TINY = 132
TINY_HUGE_ISLAND_HUGE = 131
TICK_TOCK_CLOCK = 141
RAINBOW_RIDE = 151
THE_PRINCESS_SECRET_SLIDE = 271
THE_SECRET_AQUARIUM = 201
BOWSER_IN_THE_DARK_WORLD = 171
TOWER_OF_THE_WING_CAP = 291
CAVERN_OF_THE_METAL_CAP = 281
VANISH_CAP_UNDER_THE_MOAT = 181
BOWSER_IN_THE_FIRE_SEA = 191
WING_MARIO_OVER_THE_RAINBOW = 311
# List of all courses, including secrets, without BitS as that one is static
sm64courses = ["Bob-omb Battlefield", "Whomp's Fortress", "Jolly Roger Bay", "Cool, Cool Mountain", "Big Boo's Haunt",
"Hazy Maze Cave", "Lethal Lava Land", "Shifting Sand Land", "Dire, Dire Docks", "Snowman's Land",
"Wet-Dry World", "Tall, Tall Mountain", "Tiny-Huge Island", "Tick Tock Clock", "Rainbow Ride",
"The Princess's Secret Slide", "The Secret Aquarium", "Bowser in the Dark World", "Tower of the Wing Cap",
"Cavern of the Metal Cap", "Vanish Cap under the Moat", "Bowser in the Fire Sea", "Wing Mario over the Rainbow"]
# sm64paintings is a dict of entrances, format LEVEL | AREA
sm64_level_to_paintings: typing.Dict[SM64Levels, str] = {
SM64Levels.BOB_OMB_BATTLEFIELD: "Bob-omb Battlefield",
SM64Levels.WHOMPS_FORTRESS: "Whomp's Fortress",
SM64Levels.JOLLY_ROGER_BAY: "Jolly Roger Bay",
SM64Levels.COOL_COOL_MOUNTAIN: "Cool, Cool Mountain",
SM64Levels.BIG_BOOS_HAUNT: "Big Boo's Haunt",
SM64Levels.HAZY_MAZE_CAVE: "Hazy Maze Cave",
SM64Levels.LETHAL_LAVA_LAND: "Lethal Lava Land",
SM64Levels.SHIFTING_SAND_LAND: "Shifting Sand Land",
SM64Levels.DIRE_DIRE_DOCKS: "Dire, Dire Docks",
SM64Levels.SNOWMANS_LAND: "Snowman's Land",
SM64Levels.WET_DRY_WORLD: "Wet-Dry World",
SM64Levels.TALL_TALL_MOUNTAIN: "Tall, Tall Mountain",
SM64Levels.TINY_HUGE_ISLAND_TINY: "Tiny-Huge Island (Tiny)",
SM64Levels.TINY_HUGE_ISLAND_HUGE: "Tiny-Huge Island (Huge)",
SM64Levels.TICK_TOCK_CLOCK: "Tick Tock Clock",
SM64Levels.RAINBOW_RIDE: "Rainbow Ride"
}
sm64_paintings_to_level = { painting: level for (level,painting) in sm64_level_to_paintings.items() }
# sm64paintings is list of entrances, format LEVEL | AREA. String Reference below
sm64paintings = [91,241,121,51,41,71,221,81,231,101,111,361,132,131,141,151]
sm64paintings_s = ["BOB", "WF", "JRB", "CCM", "BBH", "HMC", "LLL", "SSL", "DDD", "SL", "WDW", "TTM", "THI Tiny", "THI Huge", "TTC", "RR"]
# sm64secrets is list of secret areas
sm64secrets = [271, 201, 171, 291, 281, 181, 191, 311]
sm64secrets_s = ["PSS", "SA", "BitDW", "TOTWC", "COTMC", "VCUTM", "BitFS", "WMOTR"]
# sm64secrets is a dict of secret areas, same format as sm64paintings
sm64_level_to_secrets: typing.Dict[SM64Levels, str] = {
SM64Levels.THE_PRINCESS_SECRET_SLIDE: "The Princess's Secret Slide",
SM64Levels.THE_SECRET_AQUARIUM: "The Secret Aquarium",
SM64Levels.BOWSER_IN_THE_DARK_WORLD: "Bowser in the Dark World",
SM64Levels.TOWER_OF_THE_WING_CAP: "Tower of the Wing Cap",
SM64Levels.CAVERN_OF_THE_METAL_CAP: "Cavern of the Metal Cap",
SM64Levels.VANISH_CAP_UNDER_THE_MOAT: "Vanish Cap under the Moat",
SM64Levels.BOWSER_IN_THE_FIRE_SEA: "Bowser in the Fire Sea",
SM64Levels.WING_MARIO_OVER_THE_RAINBOW: "Wing Mario over the Rainbow"
}
sm64_secrets_to_level = { secret: level for (level,secret) in sm64_level_to_secrets.items() }
sm64_entrances_to_level = { **sm64_paintings_to_level, **sm64_secrets_to_level }
sm64_level_to_entrances = { **sm64_level_to_paintings, **sm64_level_to_secrets }
sm64entrances = sm64paintings + sm64secrets
sm64entrances_s = sm64paintings_s + sm64secrets_s
sm64_internalloc_to_string = dict(zip(sm64paintings+sm64secrets, sm64entrances_s))
sm64_internalloc_to_regionid = dict(zip(sm64paintings+sm64secrets, list(range(13)) + [12,13,14] + list(range(15,15+len(sm64secrets)))))
def create_regions(world: MultiWorld, player: int):
regSS = Region("Menu", player, world, "Castle Area")
@@ -184,13 +137,11 @@ def create_regions(world: MultiWorld, player: int):
regTTM.locations.append(SM64Location(player, "TTM: 100 Coins", location_table["TTM: 100 Coins"], regTTM))
world.regions.append(regTTM)
regTHIT = create_region("Tiny-Huge Island (Tiny)", player, world)
create_default_locs(regTHIT, locTHI_table, player)
regTHI = create_region("Tiny-Huge Island", player, world)
create_default_locs(regTHI, locTHI_table, player)
if (world.EnableCoinStars[player].value):
regTHIT.locations.append(SM64Location(player, "THI: 100 Coins", location_table["THI: 100 Coins"], regTHIT))
world.regions.append(regTHIT)
regTHIH = create_region("Tiny-Huge Island (Huge)", player, world)
world.regions.append(regTHIH)
regTHI.locations.append(SM64Location(player, "THI: 100 Coins", location_table["THI: 100 Coins"], regTHI))
world.regions.append(regTHI)
regFloor3 = create_region("Third Floor", player, world)
world.regions.append(regFloor3)

View File

@@ -1,86 +1,77 @@
from ..generic.Rules import add_rule
from .Regions import connect_regions, SM64Levels, sm64_level_to_paintings, sm64_paintings_to_level, sm64_level_to_secrets, sm64_secrets_to_level, sm64_entrances_to_level, sm64_level_to_entrances
from .Regions import connect_regions, sm64courses, sm64paintings, sm64secrets, sm64entrances
def shuffle_dict_keys(world, obj: dict) -> dict:
keys = list(obj.keys())
values = list(obj.values())
world.random.shuffle(keys)
return dict(zip(keys,values))
def fix_reg(entrance_ids, reg, invalidspot, swaplist, world):
if entrance_ids.index(reg) == invalidspot: # Unlucky :C
swaplist.remove(invalidspot)
rand = world.random.choice(swaplist)
entrance_ids[invalidspot], entrance_ids[rand] = entrance_ids[rand], entrance_ids[invalidspot]
swaplist.append(invalidspot)
swaplist.remove(rand)
def fix_reg(entrance_map: dict, entrance: SM64Levels, invalid_regions: set,
swapdict: dict, world):
if entrance_map[entrance] in invalid_regions: # Unlucky :C
replacement_regions = [(rand_region, rand_entrance) for rand_region, rand_entrance in swapdict.items()
if rand_region not in invalid_regions]
rand_region, rand_entrance = world.random.choice(replacement_regions)
old_dest = entrance_map[entrance]
entrance_map[entrance], entrance_map[rand_entrance] = rand_region, old_dest
swapdict[rand_region] = entrance
swapdict.pop(entrance_map[entrance]) # Entrance now fixed to rand_region
def set_rules(world, player: int, area_connections: dict):
randomized_level_to_paintings = sm64_level_to_paintings.copy()
randomized_level_to_secrets = sm64_level_to_secrets.copy()
if world.AreaRandomizer[player].value == 1: # Some randomization is happening, randomize Courses
randomized_level_to_paintings = shuffle_dict_keys(world,sm64_level_to_paintings)
def set_rules(world, player: int, area_connections):
destination_regions = list(range(13)) + [12,13,14] + list(range(15,15+len(sm64secrets))) # Two instances of Destination Course THI. Past normal course idx are secret regions
secret_entrance_ids = list(range(len(sm64paintings), len(sm64paintings) + len(sm64secrets)))
course_entrance_ids = list(range(len(sm64paintings)))
if world.AreaRandomizer[player].value >= 1: # Some randomization is happening, randomize Courses
world.random.shuffle(course_entrance_ids)
if world.AreaRandomizer[player].value == 2: # Randomize Secrets as well
randomized_level_to_secrets = shuffle_dict_keys(world,sm64_level_to_secrets)
randomized_entrances = { **randomized_level_to_paintings, **randomized_level_to_secrets }
world.random.shuffle(secret_entrance_ids)
entrance_ids = course_entrance_ids + secret_entrance_ids
if world.AreaRandomizer[player].value == 3: # Randomize Courses and Secrets in one pool
randomized_entrances = shuffle_dict_keys(world,randomized_entrances)
swapdict = { entrance: level for (level,entrance) in randomized_entrances.items() }
world.random.shuffle(entrance_ids)
# Guarantee first entrance is a course
fix_reg(randomized_entrances, SM64Levels.BOB_OMB_BATTLEFIELD, sm64_secrets_to_level.keys(), swapdict, world)
swaplist = list(range(len(entrance_ids)))
if entrance_ids.index(0) > 15: # Unlucky :C
rand = world.random.randint(0,15)
entrance_ids[entrance_ids.index(0)], entrance_ids[rand] = entrance_ids[rand], entrance_ids[entrance_ids.index(0)]
swaplist.remove(entrance_ids.index(0))
# Guarantee COTMC is not mapped to HMC, cuz thats impossible
fix_reg(entrance_ids, 20, 5, swaplist, world)
# Guarantee BITFS is not mapped to DDD
fix_reg(randomized_entrances, SM64Levels.BOWSER_IN_THE_FIRE_SEA, {"Dire, Dire Docks"}, swapdict, world)
# Guarantee COTMC is not mapped to HMC, cuz thats impossible. If BitFS -> HMC, also no COTMC -> DDD.
if randomized_entrances[SM64Levels.BOWSER_IN_THE_FIRE_SEA] == "Hazy Maze Cave":
fix_reg(randomized_entrances, SM64Levels.CAVERN_OF_THE_METAL_CAP, {"Hazy Maze Cave", "Dire, Dire Docks"}, swapdict, world)
else:
fix_reg(randomized_entrances, SM64Levels.CAVERN_OF_THE_METAL_CAP, {"Hazy Maze Cave"}, swapdict, world)
fix_reg(entrance_ids, 22, 8, swaplist, world)
if entrance_ids.index(22) == 5: # If BITFS is mapped to HMC...
fix_reg(entrance_ids, 20, 8, swaplist, world) # ... then dont allow COTMC to be mapped to DDD
temp_assign = dict(zip(entrance_ids,destination_regions)) # Used for Rules only
# Destination Format: LVL | AREA with LVL = LEVEL_x, AREA = Area as used in sm64 code
# Cast to int to not rely on availability of SM64Levels enum. Will cause crash in MultiServer otherwise
area_connections.update({int(entrance_lvl): int(sm64_entrances_to_level[destination]) for (entrance_lvl,destination) in randomized_entrances.items()})
randomized_entrances_s = {sm64_level_to_entrances[entrance_lvl]: destination for (entrance_lvl,destination) in randomized_entrances.items()}
area_connections.update({sm64entrances[entrance]: destination for entrance, destination in zip(entrance_ids,sm64entrances)})
connect_regions(world, player, "Menu", randomized_entrances_s["Bob-omb Battlefield"])
connect_regions(world, player, "Menu", randomized_entrances_s["Whomp's Fortress"], lambda state: state.has("Power Star", player, 1))
connect_regions(world, player, "Menu", randomized_entrances_s["Jolly Roger Bay"], lambda state: state.has("Power Star", player, 3))
connect_regions(world, player, "Menu", randomized_entrances_s["Cool, Cool Mountain"], lambda state: state.has("Power Star", player, 3))
connect_regions(world, player, "Menu", randomized_entrances_s["Big Boo's Haunt"], lambda state: state.has("Power Star", player, 12))
connect_regions(world, player, "Menu", randomized_entrances_s["The Princess's Secret Slide"], lambda state: state.has("Power Star", player, 1))
connect_regions(world, player, "Menu", randomized_entrances_s["The Secret Aquarium"], lambda state: state.has("Power Star", player, 3))
connect_regions(world, player, "Menu", randomized_entrances_s["Tower of the Wing Cap"], lambda state: state.has("Power Star", player, 10))
connect_regions(world, player, "Menu", randomized_entrances_s["Bowser in the Dark World"], lambda state: state.has("Power Star", player, world.FirstBowserStarDoorCost[player].value))
connect_regions(world, player, "Menu", sm64courses[temp_assign[0]]) # BOB
connect_regions(world, player, "Menu", sm64courses[temp_assign[1]], lambda state: state.has("Power Star", player, 1)) # WF
connect_regions(world, player, "Menu", sm64courses[temp_assign[2]], lambda state: state.has("Power Star", player, 3)) # JRB
connect_regions(world, player, "Menu", sm64courses[temp_assign[3]], lambda state: state.has("Power Star", player, 3)) # CCM
connect_regions(world, player, "Menu", sm64courses[temp_assign[4]], lambda state: state.has("Power Star", player, 12)) # BBH
connect_regions(world, player, "Menu", sm64courses[temp_assign[16]], lambda state: state.has("Power Star", player, 1)) # PSS
connect_regions(world, player, "Menu", sm64courses[temp_assign[17]], lambda state: state.has("Power Star", player, 3)) # SA
connect_regions(world, player, "Menu", sm64courses[temp_assign[19]], lambda state: state.has("Power Star", player, 10)) # TOTWC
connect_regions(world, player, "Menu", sm64courses[temp_assign[18]], lambda state: state.has("Power Star", player, world.FirstBowserStarDoorCost[player].value)) # BITDW
connect_regions(world, player, "Menu", "Basement", lambda state: state.has("Basement Key", player) or state.has("Progressive Key", player, 1))
connect_regions(world, player, "Basement", randomized_entrances_s["Hazy Maze Cave"])
connect_regions(world, player, "Basement", randomized_entrances_s["Lethal Lava Land"])
connect_regions(world, player, "Basement", randomized_entrances_s["Shifting Sand Land"])
connect_regions(world, player, "Basement", randomized_entrances_s["Dire, Dire Docks"], lambda state: state.has("Power Star", player, world.BasementStarDoorCost[player].value))
connect_regions(world, player, "Hazy Maze Cave", randomized_entrances_s["Cavern of the Metal Cap"])
connect_regions(world, player, "Basement", randomized_entrances_s["Vanish Cap under the Moat"])
connect_regions(world, player, "Basement", randomized_entrances_s["Bowser in the Fire Sea"], lambda state: state.has("Power Star", player, world.BasementStarDoorCost[player].value) and
state.can_reach("DDD: Board Bowser's Sub", 'Location', player))
connect_regions(world, player, "Basement", sm64courses[temp_assign[5]]) # HMC
connect_regions(world, player, "Basement", sm64courses[temp_assign[6]]) # LLL
connect_regions(world, player, "Basement", sm64courses[temp_assign[7]]) # SSL
connect_regions(world, player, "Basement", sm64courses[temp_assign[8]], lambda state: state.has("Power Star", player, world.BasementStarDoorCost[player].value)) # DDD
connect_regions(world, player, "Hazy Maze Cave", sm64courses[temp_assign[20]]) # COTMC
connect_regions(world, player, "Basement", sm64courses[temp_assign[21]]) # VCUTM
connect_regions(world, player, "Basement", sm64courses[temp_assign[22]], lambda state: state.has("Power Star", player, world.BasementStarDoorCost[player].value) and
state.can_reach("DDD: Board Bowser's Sub", 'Location', player)) # BITFS
connect_regions(world, player, "Menu", "Second Floor", lambda state: state.has("Second Floor Key", player) or state.has("Progressive Key", player, 2))
connect_regions(world, player, "Second Floor", randomized_entrances_s["Snowman's Land"])
connect_regions(world, player, "Second Floor", randomized_entrances_s["Wet-Dry World"])
connect_regions(world, player, "Second Floor", randomized_entrances_s["Tall, Tall Mountain"])
connect_regions(world, player, "Second Floor", randomized_entrances_s["Tiny-Huge Island (Tiny)"])
connect_regions(world, player, "Second Floor", randomized_entrances_s["Tiny-Huge Island (Huge)"])
connect_regions(world, player, "Tiny-Huge Island (Tiny)", "Tiny-Huge Island (Huge)")
connect_regions(world, player, "Tiny-Huge Island (Huge)", "Tiny-Huge Island (Tiny)")
connect_regions(world, player, "Second Floor", sm64courses[temp_assign[9]]) # SL
connect_regions(world, player, "Second Floor", sm64courses[temp_assign[10]]) # WDW
connect_regions(world, player, "Second Floor", sm64courses[temp_assign[11]]) # TTM
connect_regions(world, player, "Second Floor", sm64courses[temp_assign[12]]) # THI Tiny
connect_regions(world, player, "Second Floor", sm64courses[temp_assign[13]]) # THI Huge
connect_regions(world, player, "Second Floor", "Third Floor", lambda state: state.has("Power Star", player, world.SecondFloorStarDoorCost[player].value))
connect_regions(world, player, "Third Floor", randomized_entrances_s["Tick Tock Clock"])
connect_regions(world, player, "Third Floor", randomized_entrances_s["Rainbow Ride"])
connect_regions(world, player, "Third Floor", randomized_entrances_s["Wing Mario over the Rainbow"])
connect_regions(world, player, "Third Floor", "Bowser in the Sky", lambda state: state.has("Power Star", player, world.StarsToFinish[player].value))
connect_regions(world, player, "Third Floor", sm64courses[temp_assign[14]]) # TTC
connect_regions(world, player, "Third Floor", sm64courses[temp_assign[15]]) # RR
connect_regions(world, player, "Third Floor", sm64courses[temp_assign[23]]) # WMOTR
connect_regions(world, player, "Third Floor", "Bowser in the Sky", lambda state: state.has("Power Star", player, world.StarsToFinish[player].value)) # BITS
#Special Rules for some Locations
add_rule(world.get_location("BoB: Mario Wings to the Sky", player), lambda state: state.has("Cannon Unlock BoB", player))

View File

@@ -5,7 +5,7 @@ from .Items import item_table, cannon_item_table, SM64Item
from .Locations import location_table, SM64Location
from .Options import sm64_options
from .Rules import set_rules
from .Regions import create_regions, sm64_level_to_entrances
from .Regions import create_regions, sm64courses, sm64entrances_s, sm64_internalloc_to_string, sm64_internalloc_to_regionid
from BaseClasses import Item, Tutorial, ItemClassification
from ..AutoWorld import World, WebWorld
@@ -55,8 +55,8 @@ class SM64World(World):
# Write area_connections to spoiler log
for entrance, destination in self.area_connections.items():
self.multiworld.spoiler.set_entrance(
sm64_level_to_entrances[entrance] + " Entrance",
sm64_level_to_entrances[destination],
sm64_internalloc_to_string[entrance] + " Entrance",
sm64_internalloc_to_string[destination],
'entrance', self.player)
def create_item(self, name: str) -> Item:
@@ -182,7 +182,8 @@ class SM64World(World):
if self.topology_present:
er_hint_data = {}
for entrance, destination in self.area_connections.items():
region = self.multiworld.get_region(sm64_level_to_entrances[destination], self.player)
regionid = sm64_internalloc_to_regionid[destination]
region = self.multiworld.get_region(sm64courses[regionid], self.player)
for location in region.locations:
er_hint_data[location.address] = sm64_level_to_entrances[entrance]
er_hint_data[location.address] = sm64_internalloc_to_string[entrance]
multidata['er_hint_data'][self.player] = er_hint_data

View File

@@ -319,7 +319,7 @@ class Patch:
def WriteZ3Locations(self, locations: List[Location]):
for location in locations:
if (location.Type == LocationType.HeraStandingKey):
self.patches.append((Snes(0x9E3BB), [0xEB]))
self.patches.append((Snes(0x9E3BB), [0xE4] if location.APLocation.item.game == "SMZ3" and location.APLocation.item.item.Type == ItemType.KeyTH else [0xEB]))
elif (location.Type in [LocationType.Pedestal, LocationType.Ether, LocationType.Bombos]):
text = Texts.ItemTextbox(location.APLocation.item.item if location.APLocation.item.game == "SMZ3" else Item(ItemType.Something))
if (location.Type == LocationType.Pedestal):

View File

@@ -1536,7 +1536,6 @@ class StardewLogic:
reach_west = self.can_reach_region(Region.island_west)
reach_hut = self.can_reach_region(Region.leo_hut)
reach_southeast = self.can_reach_region(Region.island_south_east)
reach_field_office = self.can_reach_region(Region.field_office)
reach_pirate_cove = self.can_reach_region(Region.pirate_cove)
reach_outside_areas = And(reach_south, reach_north, reach_west, reach_hut)
reach_volcano_regions = [self.can_reach_region(Region.volcano),
@@ -1545,12 +1544,12 @@ class StardewLogic:
self.can_reach_region(Region.volcano_floor_10)]
reach_volcano = Or(reach_volcano_regions)
reach_all_volcano = And(reach_volcano_regions)
reach_walnut_regions = [reach_south, reach_north, reach_west, reach_volcano, reach_field_office]
reach_walnut_regions = [reach_south, reach_north, reach_west, reach_volcano]
reach_caves = And(self.can_reach_region(Region.qi_walnut_room), self.can_reach_region(Region.dig_site),
self.can_reach_region(Region.gourmand_frog_cave),
self.can_reach_region(Region.colored_crystals_cave),
self.can_reach_region(Region.shipwreck), self.has(Weapon.any_slingshot))
reach_entire_island = And(reach_outside_areas, reach_field_office, reach_all_volcano,
reach_entire_island = And(reach_outside_areas, reach_all_volcano,
reach_caves, reach_southeast, reach_pirate_cove)
if number <= 5:
return Or(reach_south, reach_north, reach_west, reach_volcano)
@@ -1564,8 +1563,7 @@ class StardewLogic:
return reach_entire_island
gems = [Mineral.amethyst, Mineral.aquamarine, Mineral.emerald, Mineral.ruby, Mineral.topaz]
return reach_entire_island & self.has(Fruit.banana) & self.has(gems) & self.can_mine_perfectly() & \
self.can_fish_perfectly() & self.has(Craftable.flute_block) & self.has(Seed.melon) & self.has(Seed.wheat) & self.has(Seed.garlic) & \
self.can_complete_field_office()
self.can_fish_perfectly() & self.has(Craftable.flute_block) & self.has(Seed.melon) & self.has(Seed.wheat) & self.has(Seed.garlic)
def has_everything(self, all_progression_items: Set[str]) -> StardewRule:
all_regions = [region.name for region in vanilla_regions]

View File

@@ -117,9 +117,6 @@ def get_pool_core(world):
else:
possible_level_locations = [location for location in standard_level_locations
if location not in level_locations[8]]
for location in placed_items.keys():
if location in possible_level_locations:
possible_level_locations.remove(location)
for level in range(1, 9):
if world.multiworld.TriforceLocations[world.player] == TriforceLocations.option_vanilla:
placed_items[f"Level {level} Triforce"] = fragment

View File

@@ -257,7 +257,7 @@ Quarry Stoneworks Middle Floor (Quarry Stoneworks) - Quarry Stoneworks Lift - Tr
158125 - 0x00E0C (Lower Row 1) - True - Dots & Eraser
158126 - 0x01489 (Lower Row 2) - 0x00E0C - Dots & Eraser
158127 - 0x0148A (Lower Row 3) - 0x01489 - Dots & Eraser
158128 - 0x014D9 (Lower Row 4) - 0x0148A - Dots & Full Dots & Eraser
158128 - 0x014D9 (Lower Row 4) - 0x0148A - Dots & Eraser
158129 - 0x014E7 (Lower Row 5) - 0x014D9 - Dots
158130 - 0x014E8 (Lower Row 6) - 0x014E7 - Dots & Eraser
@@ -307,9 +307,9 @@ Quarry Boathouse Upper Middle (Quarry Boathouse) - Quarry Boathouse Upper Back -
Quarry Boathouse Upper Back (Quarry Boathouse) - Quarry Boathouse Upper Middle - 0x3865F:
158155 - 0x38663 (Second Barrier Panel) - True - True
Door - 0x3865F (Second Barrier) - 0x38663
158156 - 0x021B5 (Back First Row 1) - True - Stars & Eraser
158156 - 0x021B5 (Back First Row 1) - True - Stars & Stars + Same Colored Symbol & Eraser
158157 - 0x021B6 (Back First Row 2) - 0x021B5 - Stars & Stars + Same Colored Symbol & Eraser
158158 - 0x021B7 (Back First Row 3) - 0x021B6 - Stars & Eraser
158158 - 0x021B7 (Back First Row 3) - 0x021B6 - Stars & Stars + Same Colored Symbol & Eraser
158159 - 0x021BB (Back First Row 4) - 0x021B7 - Stars & Stars + Same Colored Symbol & Eraser
158160 - 0x09DB5 (Back First Row 5) - 0x021BB - Stars & Stars + Same Colored Symbol & Eraser
158161 - 0x09DB1 (Back First Row 6) - 0x09DB5 - Stars & Stars + Same Colored Symbol & Eraser
@@ -427,10 +427,10 @@ Keep Tower (Keep) - Keep - 0x04F8F:
158206 - 0x0361B (Tower Shortcut Panel) - True - True
Door - 0x04F8F (Tower Shortcut) - 0x0361B
158704 - 0x0360E (Laser Panel Hedges) - 0x01A0F & 0x019E7 & 0x019DC & 0x00139 - True
158705 - 0x03317 (Laser Panel Pressure Plates) - 0x033EA & 0x01BE9 & 0x01CD3 & 0x01D3F - Dots & Shapers & Black/White Squares & Rotated Shapers
158705 - 0x03317 (Laser Panel Pressure Plates) - 0x033EA & 0x01BE9 & 0x01CD3 & 0x01D3F - Shapers & Black/White Squares & Rotated Shapers
Laser - 0x014BB (Laser) - 0x0360E | 0x03317
159240 - 0x033BE (Pressure Plates 1 EP) - 0x033EA - True
159241 - 0x033BF (Pressure Plates 2 EP) - 0x01BE9 & 0x01BEA - True
159241 - 0x033BF (Pressure Plates 2 EP) - 0x01BE9 - True
159242 - 0x033DD (Pressure Plates 3 EP) - 0x01CD3 & 0x01CD5 - True
159243 - 0x033E5 (Pressure Plates 4 Left Exit EP) - 0x01D3F - True
159244 - 0x018B6 (Pressure Plates 4 Right Exit EP) - 0x01D3F - True
@@ -516,13 +516,13 @@ Town Red Rooftop (Town):
158607 - 0x17C71 (Rooftop Discard) - True - Triangles
158230 - 0x28AC7 (Red Rooftop 1) - True - Symmetry & Black/White Squares
158231 - 0x28AC8 (Red Rooftop 2) - 0x28AC7 - Symmetry & Black/White Squares
158232 - 0x28ACA (Red Rooftop 3) - 0x28AC8 - Symmetry & Black/White Squares
158232 - 0x28ACA (Red Rooftop 3) - 0x28AC8 - Symmetry & Black/White Squares & Dots
158233 - 0x28ACB (Red Rooftop 4) - 0x28ACA - Symmetry & Black/White Squares & Dots
158234 - 0x28ACC (Red Rooftop 5) - 0x28ACB - Symmetry & Black/White Squares & Dots
158224 - 0x28B39 (Tall Hexagonal) - 0x079DF - True
Town Wooden Rooftop (Town):
158240 - 0x28AD9 (Wooden Rooftop) - 0x28AC1 - Shapers & Dots & Eraser & Full Dots
158240 - 0x28AD9 (Wooden Rooftop) - 0x28AC1 - Rotated Shapers & Dots & Eraser & Full Dots
Town Church (Town):
158227 - 0x28A69 (Church Lattice) - 0x03BB0 - True
@@ -740,7 +740,7 @@ Swamp Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underw
158329 - 0x003B2 (Beyond Rotating Bridge 1) - 0x0000A - Rotated Shapers
158330 - 0x00A1E (Beyond Rotating Bridge 2) - 0x003B2 - Rotated Shapers
158331 - 0x00C2E (Beyond Rotating Bridge 3) - 0x00A1E - Rotated Shapers & Shapers
158332 - 0x00E3A (Beyond Rotating Bridge 4) - 0x00C2E - Rotated Shapers & Shapers
158332 - 0x00E3A (Beyond Rotating Bridge 4) - 0x00C2E - Rotated Shapers
Door - 0x18482 (Blue Water Pump) - 0x00E3A
159332 - 0x3365F (Boat EP) - 0x09DB8 - True
159333 - 0x03731 (Long Bridge Side EP) - 0x17E2B - True
@@ -859,7 +859,7 @@ Treehouse Green Bridge (Treehouse) - Treehouse Green Bridge Front House - 0x17E6
158371 - 0x17E4F (Green Bridge 3) - 0x17E4D - Stars & Shapers & Rotated Shapers
158372 - 0x17E52 (Green Bridge 4 & Directional) - 0x17E4F - Stars & Rotated Shapers
158373 - 0x17E5B (Green Bridge 5) - 0x17E52 - Stars & Shapers & Stars + Same Colored Symbol
158374 - 0x17E5F (Green Bridge 6) - 0x17E5B - Stars & Negative Shapers & Rotated Shapers
158374 - 0x17E5F (Green Bridge 6) - 0x17E5B - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol & Rotated Shapers
158375 - 0x17E61 (Green Bridge 7) - 0x17E5F - Stars & Shapers & Rotated Shapers
Treehouse Green Bridge Front House (Treehouse):
@@ -917,10 +917,10 @@ Mountain Top Layer Bridge (Mountain Floor 1) - Mountain Top Layer At Door - True
158416 - 0x09E78 (Left Row 3) - 0x09E75 - Dots & Shapers
158417 - 0x09E79 (Left Row 4) - 0x09E78 - Shapers & Rotated Shapers
158418 - 0x09E6C (Left Row 5) - 0x09E79 - Stars & Black/White Squares
158419 - 0x09E6F (Left Row 6) - 0x09E6C - Shapers & Dots
158419 - 0x09E6F (Left Row 6) - 0x09E6C - Shapers
158420 - 0x09E6B (Left Row 7) - 0x09E6F - Dots
158421 - 0x33AF5 (Back Row 1) - True - Black/White Squares & Symmetry
158422 - 0x33AF7 (Back Row 2) - 0x33AF5 - Black/White Squares
158422 - 0x33AF7 (Back Row 2) - 0x33AF5 - Black/White Squares & Stars
158423 - 0x09F6E (Back Row 3) - 0x33AF7 - Symmetry & Dots
158424 - 0x09EAD (Trash Pillar 1) - True - Black/White Squares & Shapers
158425 - 0x09EAF (Trash Pillar 2) - 0x09EAD - Black/White Squares & Shapers
@@ -933,7 +933,7 @@ Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near -
158427 - 0x09FD4 (Near Row 2) - 0x09FD3 - Colored Squares & Dots
158428 - 0x09FD6 (Near Row 3) - 0x09FD4 - Stars & Colored Squares & Stars + Same Colored Symbol
158429 - 0x09FD7 (Near Row 4) - 0x09FD6 - Stars & Colored Squares & Stars + Same Colored Symbol & Shapers
158430 - 0x09FD8 (Near Row 5) - 0x09FD7 - Colored Squares
158430 - 0x09FD8 (Near Row 5) - 0x09FD7 - Colored Squares & Symmetry
Door - 0x09FFB (Staircase Near) - 0x09FD8
Mountain Floor 2 Blue Bridge (Mountain Floor 2) - Mountain Floor 2 Beyond Bridge - TrueOneWay - Mountain Floor 2 At Door - 0x09ED8:
@@ -1009,8 +1009,8 @@ Caves (Caves) - Main Island - 0x2D73F | 0x2D859 - Path to Challenge - 0x019A5:
158469 - 0x009A4 (Blue Tunnel Left Third 1) - True - Shapers
158470 - 0x018A0 (Blue Tunnel Right Third 1) - True - Shapers & Symmetry
158471 - 0x00A72 (Blue Tunnel Left Fourth 1) - True - Shapers & Negative Shapers
158472 - 0x32962 (First Floor Left) - True - Rotated Shapers & Shapers
158473 - 0x32966 (First Floor Grounded) - True - Stars & Black/White Squares
158472 - 0x32962 (First Floor Left) - True - Rotated Shapers
158473 - 0x32966 (First Floor Grounded) - True - Stars & Black/White Squares & Stars + Same Colored Symbol
158474 - 0x01A31 (First Floor Middle) - True - Colored Squares
158475 - 0x00B71 (First Floor Right) - True - Colored Squares & Stars & Stars + Same Colored Symbol & Eraser
158478 - 0x288EA (First Wooden Beam) - True - Rotated Shapers

View File

@@ -161,7 +161,7 @@ joke_hints = [
]
def get_always_hint_items(world: "WitnessWorld") -> List[str]:
def get_always_hint_items(world: "WitnessWorld"):
always = [
"Boat",
"Caves Shortcuts",
@@ -187,17 +187,17 @@ def get_always_hint_items(world: "WitnessWorld") -> List[str]:
return always
def get_always_hint_locations(_: "WitnessWorld") -> List[str]:
return [
def get_always_hint_locations(_: "WitnessWorld"):
return {
"Challenge Vault Box",
"Mountain Bottom Floor Discard",
"Theater Eclipse EP",
"Shipwreck Couch EP",
"Mountainside Cloud Cycle EP",
]
}
def get_priority_hint_items(world: "WitnessWorld") -> List[str]:
def get_priority_hint_items(world: "WitnessWorld"):
priority = {
"Caves Mountain Shortcut (Door)",
"Caves Swamp Shortcut (Door)",
@@ -246,11 +246,11 @@ def get_priority_hint_items(world: "WitnessWorld") -> List[str]:
lasers.append("Desert Laser")
priority.update(world.random.sample(lasers, 6))
return sorted(priority)
return priority
def get_priority_hint_locations(_: "WitnessWorld") -> List[str]:
return [
def get_priority_hint_locations(_: "WitnessWorld"):
return {
"Swamp Purple Underwater",
"Shipwreck Vault Box",
"Town RGB Room Left",
@@ -264,7 +264,7 @@ def get_priority_hint_locations(_: "WitnessWorld") -> List[str]:
"Tunnels Theater Flowers EP",
"Boat Shipwreck Green EP",
"Quarry Stoneworks Control Room Left",
]
}
def make_hint_from_item(world: "WitnessWorld", item_name: str, own_itempool: List[Item]):
@@ -365,8 +365,8 @@ def make_hints(world: "WitnessWorld", hint_amount: int, own_itempool: List[Item]
remaining_hints = hint_amount - len(hints)
priority_hint_amount = int(max(0.0, min(len(priority_hint_pairs) / 2, remaining_hints / 2)))
prog_items_in_this_world = sorted(prog_items_in_this_world)
locations_in_this_world = sorted(loc_in_this_world)
prog_items_in_this_world = sorted(list(prog_items_in_this_world))
locations_in_this_world = sorted(list(loc_in_this_world))
world.random.shuffle(prog_items_in_this_world)
world.random.shuffle(locations_in_this_world)

View File

@@ -115,7 +115,6 @@ class WitnessPlayerItems:
# Adjust item classifications based on game settings.
eps_shuffled = self._world.options.shuffle_EPs
come_to_you = self._world.options.elevators_come_to_you
difficulty = self._world.options.puzzle_randomization
for item_name, item_data in self.item_data.items():
if not eps_shuffled and item_name in {"Monastery Garden Entry (Door)",
"Monastery Shortcuts",
@@ -131,12 +130,10 @@ class WitnessPlayerItems:
"Monastery Laser Shortcut (Door)",
"Orchard Second Gate (Door)",
"Jungle Bamboo Laser Shortcut (Door)",
"Keep Pressure Plates 2 Exit (Door)",
"Caves Elevator Controls (Panel)"}:
# Downgrade doors that don't gate progress.
item_data.classification = ItemClassification.useful
elif item_name == "Keep Pressure Plates 2 Exit (Door)" and not (difficulty == "none" and eps_shuffled):
# PP2EP requires the door in vanilla puzzles, otherwise it's unnecessary
item_data.classification = ItemClassification.useful
# Build the mandatory item list.
self._mandatory_items: Dict[str, int] = {}

View File

@@ -70,19 +70,15 @@ class WitnessPlayerLogic:
for items_option in these_items:
all_options.add(items_option.union(dependentItem))
# 0x28A0D depends on another entity for *non-power* reasons -> This dependency needs to be preserved,
# except in Expert, where that dependency doesn't exist, but now there *is* a power dependency.
# In the future, it would be wise to make a distinction between "power dependencies" and other dependencies.
if panel_hex == "0x28A0D" and not any("0x28998" in option for option in these_panels):
these_items = all_options
# Another dependency that is not power-based: The Symmetry Island Upper Panel latches
elif panel_hex == "0x1C349":
these_items = all_options
# For any other door entity, we just return a set with the item that opens it & disregard power dependencies
else:
# 0x28A0D depends on another entity for *non-power* reasons -> This dependency needs to be preserved...
if panel_hex != "0x28A0D":
return frozenset(all_options)
# ...except in Expert, where that dependency doesn't exist, but now there *is* a power dependency.
# In the future, it would be wise to make a distinction between "power dependencies" and other dependencies.
if any("0x28998" in option for option in these_panels):
return frozenset(all_options)
these_items = all_options
disabled_eps = {eHex for eHex in self.COMPLETELY_DISABLED_ENTITIES
if self.REFERENCE_LOGIC.ENTITIES_BY_HEX[eHex]["entityType"] == "EP"}
@@ -375,7 +371,7 @@ class WitnessPlayerLogic:
if lasers:
adjustment_linesets_in_order.append(get_laser_shuffle())
if world.options.shuffle_EPs == "obelisk_sides":
if world.options.shuffle_EPs:
ep_gen = ((ep_hex, ep_obj) for (ep_hex, ep_obj) in self.REFERENCE_LOGIC.ENTITIES_BY_HEX.items()
if ep_obj["entityType"] == "EP")
@@ -489,7 +485,7 @@ class WitnessPlayerLogic:
self.EVENT_NAMES_BY_HEX[self.VICTORY_LOCATION] = "Victory"
for event_hex, event_name in self.EVENT_NAMES_BY_HEX.items():
if event_hex in self.COMPLETELY_DISABLED_ENTITIES or event_hex in self.IRRELEVANT_BUT_NOT_DISABLED_ENTITIES:
if event_hex in self.COMPLETELY_DISABLED_ENTITIES:
continue
self.EVENT_PANELS.add(event_hex)

View File

@@ -71,7 +71,7 @@ class WitnessRegions:
source_region.exits.append(connection)
connection.connect(target_region)
self.created_entrances[source, target].append(connection)
self.created_entrances[(source, target)].append(connection)
# Register any necessary indirect connections
mentioned_regions = {

View File

@@ -66,8 +66,8 @@ def _can_solve_panel(panel: str, world: "WitnessWorld", player: int, player_logi
def _can_move_either_direction(state: CollectionState, source: str, target: str, regio: WitnessRegions) -> bool:
entrance_forward = regio.created_entrances[source, target]
entrance_backward = regio.created_entrances[target, source]
entrance_forward = regio.created_entrances[(source, target)]
entrance_backward = regio.created_entrances[(source, target)]
return (
any(entrance.can_reach(state) for entrance in entrance_forward)

View File

@@ -1,22 +1,3 @@
Disabled Locations:
0x0356B (Challenge Vault Box)
0x04D75 (Vault Door)
0x0A332 (Start Timer)
0x0088E (Small Basic)
0x00BAF (Big Basic)
0x00BF3 (Square)
0x00C09 (Maze Map)
0x00CDB (Stars and Dots)
0x0051F (Symmetry)
0x00524 (Stars and Shapers)
0x00CD4 (Big Basic 2)
0x00CB9 (Choice Squares Right)
0x00CA1 (Choice Squares Middle)
0x00C80 (Choice Squares Left)
0x00C68 (Choice Squares 2 Right)
0x00C59 (Choice Squares 2 Middle)
0x00C22 (Choice Squares 2 Left)
0x034F4 (Maze Hidden 1)
0x034EC (Maze Hidden 2)
0x1C31A (Dots Pillar)
0x1C319 (Squares Pillar)

View File

@@ -33,7 +33,6 @@ class ZillionSettings(settings.Group):
"""File name of the Zillion US rom"""
description = "Zillion US ROM File"
copy_to = "Zillion (UE) [!].sms"
assert ZillionDeltaPatch.hash
md5s = [ZillionDeltaPatch.hash]
class RomStart(str):
@@ -71,11 +70,9 @@ class ZillionWorld(World):
web = ZillionWebWorld()
options_dataclass = ZillionOptions
options: ZillionOptions # type: ignore
settings: typing.ClassVar[ZillionSettings] # type: ignore
# these type: ignore are because of this issue: https://github.com/python/typing/discussions/1486
options: ZillionOptions
settings: typing.ClassVar[ZillionSettings]
topology_present = True # indicate if world type has any meaningful layout/pathing
# map names to their IDs

View File

@@ -41,7 +41,7 @@ def item_counts(cs: CollectionState, p: int) -> Tuple[Tuple[str, int], ...]:
return tuple((item_name, cs.count(item_name, p)) for item_name in item_name_to_id)
LogicCacheType = Dict[int, Tuple[Dict[int, _Counter[str]], FrozenSet[Location]]]
LogicCacheType = Dict[int, Tuple[_Counter[Tuple[str, int]], FrozenSet[Location]]]
""" { hash: (cs.prog_items, accessible_locations) } """

View File

@@ -1,5 +1,5 @@
from typing import cast
from test.bases import WorldTestBase
from test.TestBase import WorldTestBase
from worlds.zillion import ZillionWorld