Merge branch 'main' into stardew-french

This commit is contained in:
Chris Wilson
2024-04-06 19:28:26 -04:00
110 changed files with 1055 additions and 1432 deletions

33
Fill.py
View File

@@ -198,10 +198,16 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
# There are leftover unplaceable items and locations that won't accept them
if multiworld.can_beat_game():
logging.warning(
f'Not all items placed. Game beatable anyway. (Could not place {unplaced_items})')
f"Not all items placed. Game beatable anyway.\nCould not place:\n"
f"{', '.join(str(item) for item in unplaced_items)}")
else:
raise FillError(f'No more spots to place {unplaced_items}, locations {locations} are invalid. '
f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}')
raise FillError(f"No more spots to place {len(unplaced_items)} items. Remaining locations are invalid.\n"
f"Unplaced items:\n"
f"{', '.join(str(item) for item in unplaced_items)}\n"
f"Unfilled locations:\n"
f"{', '.join(str(location) for location in locations)}\n"
f"Already placed {len(placements)}:\n"
f"{', '.join(str(place) for place in placements)}")
item_pool.extend(unplaced_items)
@@ -273,8 +279,13 @@ def remaining_fill(multiworld: MultiWorld,
if unplaced_items and locations:
# There are leftover unplaceable items and locations that won't accept them
raise FillError(f'No more spots to place {unplaced_items}, locations {locations} are invalid. '
f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}')
raise FillError(f"No more spots to place {len(unplaced_items)} items. Remaining locations are invalid.\n"
f"Unplaced items:\n"
f"{', '.join(str(item) for item in unplaced_items)}\n"
f"Unfilled locations:\n"
f"{', '.join(str(location) for location in locations)}\n"
f"Already placed {len(placements)}:\n"
f"{', '.join(str(place) for place in placements)}")
itempool.extend(unplaced_items)
@@ -457,7 +468,9 @@ def distribute_items_restrictive(multiworld: MultiWorld) -> None:
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, name="Progression")
if progitempool:
raise FillError(
f'Not enough locations for progress items. There are {len(progitempool)} more items than locations')
f"Not enough locations for progression items. "
f"There are {len(progitempool)} more progression items than there are available locations."
)
accessibility_corrections(multiworld, multiworld.state, defaultlocations)
for location in lock_later:
@@ -470,7 +483,9 @@ def distribute_items_restrictive(multiworld: MultiWorld) -> None:
remaining_fill(multiworld, excludedlocations, filleritempool, "Remaining Excluded")
if excludedlocations:
raise FillError(
f"Not enough filler items for excluded locations. There are {len(excludedlocations)} more locations than items")
f"Not enough filler items for excluded locations. "
f"There are {len(excludedlocations)} more excluded locations than filler or trap items."
)
restitempool = filleritempool + usefulitempool
@@ -481,13 +496,13 @@ def distribute_items_restrictive(multiworld: MultiWorld) -> None:
if unplaced or unfilled:
logging.warning(
f'Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}')
f"Unplaced items({len(unplaced)}): {unplaced} - Unfilled Locations({len(unfilled)}): {unfilled}")
items_counter = Counter(location.item.player for location in multiworld.get_locations() if location.item)
locations_counter = Counter(location.player for location in multiworld.get_locations())
items_counter.update(item.player for item in unplaced)
locations_counter.update(location.player for location in unfilled)
print_data = {"items": items_counter, "locations": locations_counter}
logging.info(f'Per-Player counts: {print_data})')
logging.info(f"Per-Player counts: {print_data})")
def flood_items(multiworld: MultiWorld) -> None:

View File

@@ -26,6 +26,7 @@ from worlds.alttp.EntranceRandomizer import parse_arguments
from worlds.alttp.Text import TextTable
from worlds.AutoWorld import AutoWorldRegister
from worlds.generic import PlandoConnection
from worlds import failed_world_loads
def mystery_argparse():
@@ -34,8 +35,8 @@ def mystery_argparse():
parser = argparse.ArgumentParser(description="CMD Generation Interface, defaults come from host.yaml.")
parser.add_argument('--weights_file_path', default=defaults.weights_file_path,
help='Path to the weights file to use for rolling game settings, urls are also valid')
parser.add_argument('--samesettings', help='Rolls settings per weights file rather than per player',
help='Path to the weights file to use for rolling game options, urls are also valid')
parser.add_argument('--sameoptions', help='Rolls options per weights file rather than per player',
action='store_true')
parser.add_argument('--player_files_path', default=defaults.player_files_path,
help="Input directory for player files.")
@@ -103,8 +104,8 @@ def main(args=None, callback=ERmain):
del(meta_weights["meta_description"])
except Exception as e:
raise ValueError("No meta description found for meta.yaml. Unable to verify.") from e
if args.samesettings:
raise Exception("Cannot mix --samesettings with --meta")
if args.sameoptions:
raise Exception("Cannot mix --sameoptions with --meta")
else:
meta_weights = None
player_id = 1
@@ -156,7 +157,7 @@ def main(args=None, callback=ERmain):
erargs.skip_output = args.skip_output
settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None)
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.sameoptions else None)
for fname, yamls in weights_cache.items()}
if meta_weights:
@@ -458,7 +459,11 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
ret.game = get_choice("game", weights)
if ret.game not in AutoWorldRegister.world_types:
picks = Utils.get_fuzzy_results(ret.game, AutoWorldRegister.world_types, limit=1)[0]
picks = Utils.get_fuzzy_results(ret.game, list(AutoWorldRegister.world_types) + failed_world_loads, limit=1)[0]
if picks[0] in failed_world_loads:
raise Exception(f"No functional world found to handle game {ret.game}. "
f"Did you mean '{picks[0]}' ({picks[1]}% sure)? "
f"If so, it appears the world failed to initialize correctly.")
raise Exception(f"No world found to handle game {ret.game}. Did you mean '{picks[0]}' ({picks[1]}% sure)? "
f"Check your spelling or installation of that world.")

View File

@@ -100,7 +100,7 @@ components.extend([
# Functions
Component("Open host.yaml", func=open_host_yaml),
Component("Open Patch", func=open_patch),
Component("Generate Template Settings", func=generate_yamls),
Component("Generate Template Options", func=generate_yamls),
Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")),
Component("18+ Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")),
Component("Browse Files", func=browse_files),

View File

@@ -86,9 +86,9 @@ We recognize that there is a strong community of incredibly smart people that ha
Archipelago was directly forked from bonta0's `multiworld_31` branch of ALttPEntranceRandomizer (this project has a long legacy of its own, please check it out linked above) on January 12, 2020. The repository was then named to _MultiWorld-Utilities_ to better encompass its intended function. As Archipelago matured, then known as "Berserker's MultiWorld" by some, we found it necessary to transform our repository into a root level repository (as opposed to a 'forked repo') and change the name (which came later) to better reflect our project.
## Running Archipelago
For most people all you need to do is head over to the [releases](https://github.com/ArchipelagoMW/Archipelago/releases) page then download and run the appropriate installer. The installers function on Windows only.
For most people, all you need to do is head over to the [releases](https://github.com/ArchipelagoMW/Archipelago/releases) page then download and run the appropriate installer, or AppImage for Linux-based systems.
If you are running Archipelago from a non-Windows system then the likely scenario is that you are comfortable running source code directly. Please see our doc on [running Archipelago from source](docs/running%20from%20source.md).
If you are a developer or are running on a platform with no compiled releases available, please see our doc on [running Archipelago from source](docs/running%20from%20source.md).
## Related Repositories
This project makes use of multiple other projects. We wouldn't be here without these other repositories and the contributions of their developers, past and present.

View File

@@ -6,6 +6,7 @@ import multiprocessing
import threading
import time
import typing
from uuid import UUID
from datetime import timedelta, datetime
from pony.orm import db_session, select, commit
@@ -62,6 +63,16 @@ def autohost(config: dict):
def keep_running():
try:
with Locker("autohost"):
# delete unowned user-content
with db_session:
# >>> bool(uuid.UUID(int=0))
# True
rooms = Room.select(lambda room: room.owner == UUID(int=0)).delete(bulk=True)
seeds = Seed.select(lambda seed: seed.owner == UUID(int=0) and not seed.rooms).delete(bulk=True)
slots = Slot.select(lambda slot: not slot.seed).delete(bulk=True)
# Command gets deleted by ponyorm Cascade Delete, as Room is Required
if rooms or seeds or slots:
logging.info(f"{rooms} Rooms, {seeds} Seeds and {slots} Slots have been deleted.")
run_guardian()
while 1:
time.sleep(0.1)
@@ -191,6 +202,6 @@ def run_guardian():
guardian = threading.Thread(name="Guardian", target=guard)
from .models import Room, Generation, STATE_QUEUED, STATE_STARTED, STATE_ERROR, db, Seed
from .models import Room, Generation, STATE_QUEUED, STATE_STARTED, STATE_ERROR, db, Seed, Slot
from .customserver import run_server_process, get_static_server_data
from .generate import gen_game

View File

@@ -28,7 +28,7 @@ def check():
results, _ = roll_options(options)
if len(options) > 1:
# offer combined file back
combined_yaml = "---\n".join(f"# original filename: {file_name}\n{file_content.decode('utf-8-sig')}"
combined_yaml = "\n---\n".join(f"# original filename: {file_name}\n{file_content.decode('utf-8-sig')}"
for file_name, file_content in options.items())
combined_yaml = base64.b64encode(combined_yaml.encode("utf-8-sig")).decode()
else:

View File

@@ -49,12 +49,6 @@ def weighted_options():
return render_template("weighted-options.html")
# TODO for back compat. remove around 0.4.5
@app.route("/games/<string:game>/player-settings")
def player_settings(game: str):
return redirect(url_for("player_options", game=game), 301)
# Player options pages
@app.route("/games/<string:game>/player-options")
@cache.cached()

View File

@@ -294,7 +294,8 @@
<td>{{ sc2_icon('HERC') }}</td>
<td>{{ sc2_icon('Juggernaut Plating (HERC)') }}</td>
<td>{{ sc2_icon('Kinetic Foam (HERC)') }}</td>
<td colspan="5"></td>
<td>{{ sc2_icon('Resource Efficiency (HERC)') }}</td>
<td colspan="4"></td>
<td>{{ sc2_icon('Widow Mine') }}</td>
<td>{{ sc2_icon('Drilling Claws (Widow Mine)') }}</td>
<td>{{ sc2_icon('Concealment (Widow Mine)') }}</td>

View File

@@ -25,6 +25,7 @@
<th class="center">Players</th>
<th>Created (UTC)</th>
<th>Last Activity (UTC)</th>
<th>Mark for deletion</th>
</tr>
</thead>
<tbody>
@@ -35,6 +36,7 @@
<td>{{ room.seed.slots|length }}</td>
<td>{{ room.creation_time.strftime("%Y-%m-%d %H:%M") }}</td>
<td>{{ room.last_activity.strftime("%Y-%m-%d %H:%M") }}</td>
<td><a href="{{ url_for("disown_room", room=room.id) }}">Delete next maintenance.</td>
</tr>
{% endfor %}
</tbody>
@@ -51,6 +53,7 @@
<th>Seed</th>
<th class="center">Players</th>
<th>Created (UTC)</th>
<th>Mark for deletion</th>
</tr>
</thead>
<tbody>
@@ -60,6 +63,7 @@
<td>{% if seed.multidata %}{{ seed.slots|length }}{% else %}1{% endif %}
</td>
<td>{{ seed.creation_time.strftime("%Y-%m-%d %H:%M") }}</td>
<td><a href="{{ url_for("disown_seed", seed=seed.id) }}">Delete next maintenance.</td>
</tr>
{% endfor %}
</tbody>

View File

@@ -124,10 +124,13 @@ class TrackerData:
@_cache_results
def get_player_inventory_counts(self, team: int, player: int) -> collections.Counter:
"""Retrieves a dictionary of all items received by their id and their received count."""
items = self.get_player_received_items(team, player)
received_items = self.get_player_received_items(team, player)
starting_items = self.get_player_starting_inventory(team, player)
inventory = collections.Counter()
for item in items:
for item in received_items:
inventory[item.item] += 1
for item in starting_items:
inventory[item] += 1
return inventory
@@ -358,10 +361,13 @@ def get_enabled_multiworld_trackers(room: Room) -> Dict[str, Callable]:
def render_generic_tracker(tracker_data: TrackerData, team: int, player: int) -> str:
game = tracker_data.get_player_game(team, player)
# Add received index to all received items, excluding starting inventory.
received_items_in_order = {}
for received_index, network_item in enumerate(tracker_data.get_player_received_items(team, player), start=1):
received_items_in_order[network_item.item] = received_index
starting_inventory = tracker_data.get_player_starting_inventory(team, player)
for index, item in enumerate(starting_inventory):
received_items_in_order[item] = index
for index, network_item in enumerate(tracker_data.get_player_received_items(team, player),
start=len(starting_inventory)):
received_items_in_order[network_item.item] = index
return render_template(
template_name_or_list="genericTracker.html",
@@ -1674,6 +1680,7 @@ if "Starcraft 2" in network_data_package["games"]:
"Resource Efficiency (Spectre)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png",
"Juggernaut Plating (HERC)": organics_icon_base_url + "JuggernautPlating.png",
"Kinetic Foam (HERC)": organics_icon_base_url + "KineticFoam.png",
"Resource Efficiency (HERC)": github_icon_base_url + "blizzard/btn-ability-hornerhan-salvagebonus.png",
"Hellion": "https://static.wikia.nocookie.net/starcraft/images/5/56/Hellion_SC2_Icon1.jpg",
"Vulture": github_icon_base_url + "blizzard/btn-unit-terran-vulture.png",

View File

@@ -7,7 +7,7 @@ import zipfile
import zlib
from io import BytesIO
from flask import request, flash, redirect, url_for, session, render_template
from flask import request, flash, redirect, url_for, session, render_template, abort
from markupsafe import Markup
from pony.orm import commit, flush, select, rollback
from pony.orm.core import TransactionIntegrityError
@@ -219,3 +219,29 @@ 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)
@app.route("/disown_seed/<suuid:seed>", methods=["GET"])
def disown_seed(seed):
seed = Seed.get(id=seed)
if not seed:
return abort(404)
if seed.owner != session["_id"]:
return abort(403)
seed.owner = 0
return redirect(url_for("user_content"))
@app.route("/disown_room/<suuid:room>", methods=["GET"])
def disown_room(room):
room = Room.get(id=room)
if not room:
return abort(404)
if room.owner != session["_id"]:
return abort(403)
room.owner = 0
return redirect(url_for("user_content"))

Binary file not shown.

View File

@@ -1,269 +1,78 @@
# How do I add a game to Archipelago?
This guide is going to try and be a broad summary of how you can do just that.
There are two key steps to incorporating a game into Archipelago:
- Game Modification
- Archipelago Server Integration
Refer to the following documents as well:
- [network protocol.md](/docs/network%20protocol.md) for network communication between client and server.
- [world api.md](/docs/world%20api.md) for documentation on server side code and creating a world package.
# Game Modification
One half of the work required to integrate a game into Archipelago is the development of the game client. This is
typically done through a modding API or other modification process, described further down.
As an example, modifications to a game typically include (more on this later):
- Hooking into when a 'location check' is completed.
- Networking with the Archipelago server.
- Optionally, UI or HUD updates to show status of the multiworld session or Archipelago server connection.
In order to determine how to modify a game, refer to the following sections.
## Engine Identification
This is a good way to make the modding process much easier. Being able to identify what engine a game was made in is
critical. The first step is to look at a game's files. Let's go over what some game files might look like. Its
important that you be able to see file extensions, so be sure to enable that feature in your file viewer of choice.
Examples are provided below.
### Creepy Castle
![Creepy Castle Root Directory in Windows Explorer](/docs/img/creepy-castle-directory.png)
This is the delightful title Creepy Castle, which is a fantastic game that I highly recommend. Its also your worst-case
scenario as a modder. All thats present here is an executable file and some meta-information that Steam uses. You have
basically nothing here to work with. If you want to change this game, the only option you have is to do some pretty
nasty disassembly and reverse engineering work, which is outside the scope of this tutorial. Lets look at some other
examples of game releases.
### Heavy Bullets
![Heavy Bullets Root Directory in Window's Explorer](/docs/img/heavy-bullets-directory.png)
Heres the release files for another game, Heavy Bullets. We see a .exe file, like expected, and a few more files.
“hello.txt” is a text file, which we can quickly skim in any text editor. Many games have them in some form, usually
with a name like README.txt, and they may contain information about a game, such as a EULA, terms of service, licensing
information, credits, and general info about the game. You usually wont find anything too helpful here, but it never
hurts to check. In this case, it contains some credits and a changelog for the game, so nothing too important.
“steam_api.dll” is a file you can safely ignore, its just some code used to interface with Steam.
The directory “HEAVY_BULLETS_Data”, however, has some good news.
![Heavy Bullets Data Directory in Window's Explorer](/docs/img/heavy-bullets-data-directory.png)
Jackpot! It might not be obvious what youre looking at here, but I can instantly tell from this folders contents that
what we have is a game made in the Unity Engine. If you look in the sub-folders, youll seem some .dll files which
affirm our suspicions. Telltale signs for this are directories titled “Managed” and “Mono”, as well as the numbered,
extension-less level files and the sharedassets files. If you've identified the game as a Unity game, some useful tools
and information to help you on your journey can be found at this
[Unity Game Hacking guide.](https://github.com/imadr/Unity-game-hacking)
### Stardew Valley
![Stardew Valley Root Directory in Window's Explorer](/docs/img/stardew-valley-directory.png)
This is the game contents of Stardew Valley. A lot more to look at here, but some key takeaways.
Notice the .dll files which include “CSharp” in their name. This tells us that the game was made in C#, which is good
news. Many games made in C# can be modified using the same tools found in our Unity game hacking toolset; namely BepInEx
and MonoMod.
### Gato Roboto
![Gato Roboto Root Directory in Window's Explorer](/docs/img/gato-roboto-directory.png)
Our last example is the game Gato Roboto. This game is made in GameMaker, which is another green flag to look out for.
The giveaway is the file titled "data.win". This immediately tips us off that this game was made in GameMaker. For
modifying GameMaker games the [Undertale Mod Tool](https://github.com/krzys-h/UndertaleModTool) is incredibly helpful.
This isn't all you'll ever see looking at game files, but it's a good place to start.
As a general rule, the more files a game has out in plain sight, the more you'll be able to change.
This especially applies in the case of code or script files - always keep a lookout for anything you can use to your
advantage!
## Open or Leaked Source Games
As a side note, many games have either been made open source, or have had source files leaked at some point.
This can be a boon to any would-be modder, for obvious reasons. Always be sure to check - a quick internet search for
"(Game) Source Code" might not give results often, but when it does, you're going to have a much better time.
Be sure never to distribute source code for games that you decompile or find if you do not have express permission to do
so, or to redistribute any materials obtained through similar methods, as this is illegal and unethical.
## Modifying Release Versions of Games
However, for now we'll assume you haven't been so lucky, and have to work with only whats sitting in your install
directory. Some developers are kind enough to deliberately leave you ways to alter their games, like modding tools,
but these are often not geared to the kind of work you'll be doing and may not help much.
As a general rule, any modding tool that lets you write actual code is something worth using.
### Research
The first step is to research your game. Even if you've been dealt the worst hand in terms of engine modification,
it's possible other motivated parties have concocted useful tools for your game already.
Always be sure to search the Internet for the efforts of other modders.
### Other helpful tools
Depending on the games underlying engine, there may be some tools you can use either in lieu of or in addition to
existing game tools.
#### [CheatEngine](https://cheatengine.org/)
CheatEngine is a tool with a very long and storied history.
Be warned that because it performs live modifications to the memory of other processes, it will likely be flagged as
malware (because this behavior is most commonly found in malware and rarely used by other programs).
If you use CheatEngine, you need to have a deep understanding of how computers work at the nuts and bolts level,
including binary data formats, addressing, and assembly language programming.
The tool itself is highly complex and even I have not yet charted its expanses.
However, it can also be a very powerful tool in the right hands, allowing you to query and modify gamestate without ever
modifying the actual game itself.
In theory it is compatible with any piece of software you can run on your computer, but there is no "easy way" to do
anything with it.
### What Modifications You Should Make to the Game
We talked about this briefly in [Game Modification](#game-modification) section.
The next step is to know what you need to make the game do now that you can modify it. Here are your key goals:
- Know when the player has checked a location, and react accordingly
- Be able to receive items from the server on the fly
- Keep an index for items received in order to resync from disconnections
- Add interface for connecting to the Archipelago server with passwords and sessions
- Add commands for manually rewarding, re-syncing, releasing, and other actions
Refer to the [Network Protocol documentation](/docs/network%20protocol.md) for how to communicate with Archipelago's
servers.
## But my Game is a console game. Can I still add it?
That depends what console?
### My Game is a recent game for the PS4/Xbox-One/Nintendo Switch/etc
Most games for recent generations of console platforms are inaccessible to the typical modder. It is generally advised
that you do not attempt to work with these games as they are difficult to modify and are protected by their copyright
holders. Most modern AAA game studios will provide a modding interface or otherwise deny modifications for their console
games.
### My Game isnt that old, its for the Wii/PS2/360/etc
This is very complex, but doable.
If you don't have good knowledge of stuff like Assembly programming, this is not where you want to learn it.
There exist many disassembly and debugging tools, but more recent content may have lackluster support.
### My Game is a classic for the SNES/Sega Genesis/etc
Thats a lot more feasible.
There are many good tools available for understanding and modifying games on these older consoles, and the emulation
community will have figured out the bulk of the consoles secrets.
Look for debugging tools, but be ready to learn assembly.
Old consoles usually have their own unique dialects of ASM youll need to get used to.
Also make sure theres a good way to interface with a running emulator, since thats the only way you can connect these
older consoles to the Internet.
There are also hardware mods and flash carts, which can do the same things an emulator would when connected to a
computer, but these will require the same sort of interface software to be written in order to work properly; from your
perspective the two won't really look any different.
### My Game is an exclusive for the Super Baby Magic Dream Boy. Its this console from the Soviet Union that-
Unless you have a circuit schematic for the Super Baby Magic Dream Boy sitting on your desk, no.
Obscurity is your enemy there will likely be little to no emulator or modding information, and youd essentially be
working from scratch.
## How to Distribute Game Modifications
**NEVER EVER distribute anyone else's copyrighted work UNLESS THEY EXPLICITLY GIVE YOU PERMISSION TO DO SO!!!**
This is a good way to get any project you're working on sued out from under you.
The right way to distribute modified versions of a game's binaries, assuming that the licensing terms do not allow you
to copy them wholesale, is as patches.
There are many patch formats, which I'll cover in brief. The common theme is that you cant distribute anything that
wasn't made by you. Patches are files that describe how your modified file differs from the original one, thus avoiding
the issue of distributing someone elses original work.
Users who have a copy of the game just need to apply the patch, and those who dont are unable to play.
### Patches
#### IPS
IPS patches are a simple list of chunks to replace in the original to generate the output. It is not possible to encode
moving of a chunk, so they may inadvertently contain copyrighted material and should be avoided unless you know it's
fine.
#### UPS, BPS, VCDIFF (xdelta), bsdiff
Other patch formats generate the difference between two streams (delta patches) with varying complexity. This way it is
possible to insert bytes or move chunks without including any original data. Bsdiff is highly optimized and includes
compression, so this format is used by APBP.
Only a bsdiff module is integrated into AP. If the final patch requires or is based on any other patch, convert them to
bsdiff or APBP before adding it to the AP source code as "basepatch.bsdiff4" or "basepatch.apbp".
#### APBP Archipelago Binary Patch
Starting with version 4 of the APBP format, this is a ZIP file containing metadata in `archipelago.json` and additional
files required by the game / patching process. For ROM-based games the ZIP will include a `delta.bsdiff4` which is the
bsdiff between the original and the randomized ROM.
To make using APBP easy, they can be generated by inheriting from `worlds.Files.APDeltaPatch`.
### Mod files
Games which support modding will usually just let you drag and drop the mods files into a folder somewhere.
Mod files come in many forms, but the rules about not distributing other people's content remain the same.
They can either be generic and modify the game using a seed or `slot_data` from the AP websocket, or they can be
generated per seed. If at all possible, it's generally best practice to collect your world information from `slot_data`
so that the users don't have to move files around in order to play.
If the mod is generated by AP and is installed from a ZIP file, it may be possible to include APBP metadata for easy
integration into the Webhost by inheriting from `worlds.Files.APContainer`.
## Archipelago Integration
In order for your game to communicate with the Archipelago server and generate the necessary randomized information,
you must create a world package in the main Archipelago repo. This section will cover the requisites and expectations
and show the basics of a world. More in depth documentation on the available API can be read in
the [world api doc.](/docs/world%20api.md)
For setting up your working environment with Archipelago refer
to [running from source](/docs/running%20from%20source.md) and the [style guide](/docs/style.md).
### Requirements
A world implementation requires a few key things from its implementation
- A folder within `worlds` that contains an `__init__.py`
- This is what defines it as a Python package and how it's able to be imported
into Archipelago's generation system. During generation time only code that is
defined within this file will be run. It's suggested to split up your information
into more files to improve readability, but all of that information can be
imported at its base level within your world.
- A `World` subclass where you create your world and define all of its rules
and the following requirements:
- Your items and locations need a `item_name_to_id` and `location_name_to_id`,
respectively, mapping.
- An `option_definitions` mapping of your game options with the format
`{name: Class}`, where `name` uses Python snake_case.
- You must define your world's `create_item` method, because this may be called
by the generator in certain circumstances
- When creating your world you submit items and regions to the Multiworld.
- These are lists of said objects which you can access at
`self.multiworld.itempool` and `self.multiworld.regions`. Best practice for
adding to these lists is with either `append` or `extend`, where `append` is a
single object and `extend` is a list.
- Do not use `=` as this will delete other worlds' items and regions.
- Regions are containers for holding your world's Locations.
- Locations are where players will "check" for items and must exist within
a region. It's also important for your world's submitted items to be the same as
its submitted locations count.
- You must always have a "Menu" Region from which the generation algorithm
uses to enter the game and access locations.
- Make sure to check out [world maintainer.md](/docs/world%20maintainer.md) before publishing.
# Adding Games
Adding a new game to Archipelago has two major parts:
* Game Modification to communicate with Archipelago server (hereafter referred to as "client")
* Archipelago Generation and Server integration plugin (hereafter referred to as "world")
This document will attempt to illustrate the bare minimum requirements and expectations of both parts of a new world
integration. As game modification wildly varies by system and engine, and has no bearing on the Archipelago protocol,
it will not be detailed here.
## Client
The client is an intermediary program between the game and the Archipelago server. This can either be a direct
modification to the game, an external program, or both. This can be implemented in nearly any modern language, but it
must fulfill a few requirements in order to function as expected. The specific requirements the game client must follow
to behave as expected are:
* Handle both secure and unsecure websocket connections
* Detect and react when a location has been "checked" by the player by sending a network packet to the server
* Receive and parse network packets when the player receives an item from the server, and reward it to the player on
demand
* **Any** of your items can be received any number of times, up to and far surpassing those that the game might
normally expect from features such as starting inventory, item link replacement, or item cheating
* Players and the admin can cheat items to the player at any time with a server command, and these items may not have
a player or location attributed to them
* Be able to change the port for saved connection info
* Rooms hosted on the website attempt to reserve their port, but since there are a limited number of ports, this
privilege can be lost, requiring the room to be moved to a new port
* Reconnect if the connection is unstable and lost while playing
* Keep an index for items received in order to resync. The ItemsReceived Packets are a single list with guaranteed
order.
* Receive items that were sent to the player while they were not connected to the server
* The player being able to complete checks while offline and sending them when reconnecting is a good bonus, but not
strictly required
* Send a status update packet alerting the server that the player has completed their goal
Libraries for most modern languages and the spec for various packets can be found in the
[network protocol](/docs/network%20protocol.md) API reference document.
## World
The world is your game integration for the Archipelago generator, webhost, and multiworld server. It contains all the
information necessary for creating the items and locations to be randomized, the logic for item placement, the
datapackage information so other game clients can recognize your game data, and documentation. Your world must be
written as a Python package to be loaded by Archipelago. This is currently done by creating a fork of the Archipelago
repository and creating a new world package in `/worlds/`. A bare minimum world implementation must satisfy the
following requirements:
* A folder within `/worlds/` that contains an `__init__.py`
* A `World` subclass where you create your world and define all of its rules
* A unique game name
* For webhost documentation and behaviors, a `WebWorld` subclass that must be instantiated in the `World` class
definition
* The game_info doc must follow the format `{language_code}_{game_name}.md`
* A mapping for items and locations defining their names and ids for clients to be able to identify them. These are
`item_name_to_id` and `location_name_to_id`, respectively.
* Create an item when `create_item` is called both by your code and externally
* An `options_dataclass` defining the options players have available to them
* A `Region` for your player with the name "Menu" to start from
* Create a non-zero number of locations and add them to your regions
* Create a non-zero number of items **equal** to the number of locations and add them to the multiworld itempool
* All items submitted to the multiworld itempool must not be manually placed by the World. If you need to place specific
items, there are multiple ways to do so, but they should not be added to the multiworld itempool.
Notable caveats:
* The "Menu" region will always be considered the "start" for the player
* The "Menu" region is *always* considered accessible; i.e. the player is expected to always be able to return to the
start of the game from anywhere
* When submitting regions or items to the multiworld (multiworld.regions and multiworld.itempool respectively), use
`append`, `extend`, or `+=`. **Do not use `=`**
* Regions are simply containers for locations that share similar access rules. They do not have to map to
concrete, physical areas within your game and can be more abstract like tech trees or a questline.
The base World class can be found in [AutoWorld](/worlds/AutoWorld.py). Methods available for your world to call during
generation can be found in [BaseClasses](/BaseClasses.py) and [Fill](/Fill.py). Some examples and documentation
regarding the API can be found in the [world api doc](/docs/world%20api.md).
Before publishing, make sure to also check out [world maintainer.md](/docs/world%20maintainer.md).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

View File

@@ -31,6 +31,9 @@ There are also a number of community-supported libraries available that implemen
| GameMaker: Studio 2.x+ | [see Discord](https://discord.com/channels/731205301247803413/1166418532519653396) | |
## Synchronizing Items
After a client connects, it will receive all previously collected items for its associated slot in a [ReceivedItems](#ReceivedItems) packet. This will include items the client may have already processed in a previous play session.
To ensure the client is able to reject those items if it needs to, each item in the packet has an associated `index` argument. You will need to find a way to save the "last processed item index" to the player's local savegame, a local file, or something to that effect. Before connecting, you should load that "last processed item index" value and compare against it in your received items handling.
When the client receives a [ReceivedItems](#ReceivedItems) packet, if the `index` argument does not match the next index that the client expects then it is expected that the client will re-sync items with the server. This can be accomplished by sending the server a [Sync](#Sync) packet and then a [LocationChecks](#LocationChecks) packet.
Even if the client detects a desync, it can still accept the items provided in this packet to prevent gameplay interruption.

View File

@@ -10,10 +10,9 @@ Archipelago will be abbreviated as "AP" from now on.
## Option Definitions
Option parsing in AP is done using different Option classes. For each option you would like to have in your game, you
need to create:
- A new option class with a docstring detailing what the option will do to your user.
- A `display_name` to be displayed on the webhost.
- A new entry in the `option_definitions` dict for your World.
By style and convention, the internal names should be snake_case.
- A new option class, with a docstring detailing what the option does, to be exposed to the user.
- A new entry in the `options_dataclass` definition for your World.
By style and convention, the dataclass attributes should be `snake_case`.
### Option Creation
- If the option supports having multiple sub_options, such as Choice options, these can be defined with
@@ -43,7 +42,7 @@ from Options import Toggle, Range, Choice, PerGameCommonOptions
class StartingSword(Toggle):
"""Adds a sword to your starting inventory."""
display_name = "Start With Sword"
display_name = "Start With Sword" # this is the option name as it's displayed to the user on the webhost and in the spoiler log
class Difficulty(Choice):

View File

@@ -1,7 +1,7 @@
# Archipelago Settings API
The settings API describes how to use installation-wide config and let the user configure them, like paths, etc. using
host.yaml. For the player settings / player yamls see [options api.md](options api.md).
host.yaml. For the player options / player yamls see [options api.md](options api.md).
The settings API replaces `Utils.get_options()` and `Utils.get_default_options()`
as well as the predefined `host.yaml` in the repository.

View File

@@ -1,591 +0,0 @@
# What is this file?
# This file contains options which allow you to configure your multiworld experience while allowing others
# to play how they want as well.
# How do I use it?
# The options in this file are weighted. This means the higher number you assign to a value, the more
# chances you have for that option to be chosen. For example, an option like this:
#
# map_shuffle:
# on: 5
# off: 15
#
# Means you have 5 chances for map shuffle to occur, and 15 chances for map shuffle to be turned off
# I've never seen a file like this before. What characters am I allowed to use?
# This is a .yaml file. You are allowed to use most characters.
# To test if your yaml is valid or not, you can use this website:
# http://www.yamllint.com/
description: Template Name # Used to describe your yaml. Useful if you have multiple files
name: YourName{number} # Your name in-game. Spaces will be replaced with underscores and there is a 16 character limit
#{player} will be replaced with the player's slot number.
#{PLAYER} will be replaced with the player's slot number if that slot number is greater than 1.
#{number} will be replaced with the counter value of the name.
#{NUMBER} will be replaced with the counter value of the name if the counter value is greater than 1.
game: # Pick a game to play
A Link to the Past: 1
requires:
version: 0.4.4 # Version of Archipelago required for this yaml to work as expected.
A Link to the Past:
progression_balancing:
# A system that can move progression earlier, to try and prevent the player from getting stuck and bored early.
# A lower setting means more getting stuck. A higher setting means less getting stuck.
#
# You can define additional values between the minimum and maximum values.
# Minimum value is 0
# Maximum value is 99
random: 0
random-low: 0
random-high: 0
disabled: 0 # equivalent to 0
normal: 50 # equivalent to 50
extreme: 0 # equivalent to 99
accessibility:
# Set rules for reachability of your items/locations.
# Locations: ensure everything can be reached and acquired.
# Items: ensure all logically relevant items can be acquired.
# Minimal: ensure what is needed to reach your goal can be acquired.
locations: 0
items: 50
minimal: 0
local_items:
# Forces these items to be in their native world.
[ ]
non_local_items:
# Forces these items to be outside their native world.
[ ]
start_inventory:
# Start with these items.
{ }
start_hints:
# Start with these item's locations prefilled into the !hint command.
[ ]
start_location_hints:
# Start with these locations and their item prefilled into the !hint command
[ ]
exclude_locations:
# Prevent these locations from having an important item
[ ]
priority_locations:
# Prevent these locations from having an unimportant item
[ ]
item_links:
# Share part of your item pool with other players.
[ ]
### Logic Section ###
glitches_required: # Determine the logic required to complete the seed
none: 50 # No glitches required
minor_glitches: 0 # Puts fake flipper, waterwalk, super bunny shenanigans, and etc into logic
overworld_glitches: 0 # Assumes the player has knowledge of both overworld major glitches (boots clips, mirror clips) and minor glitches
hybrid_major_glitches: 0 # In addition to overworld glitches, also requires underworld clips between dungeons.
no_logic: 0 # Your own items are placed with no regard to any logic; such as your Fire Rod can be on your Trinexx.
# Other players items are placed into your world under HMG logic
dark_room_logic: # Logic for unlit dark rooms
lamp: 50 # require the Lamp for these rooms to be considered accessible.
torches: 0 # in addition to lamp, allow the fire rod and presence of easily accessible torches for access
none: 0 # all dark rooms are always considered doable, meaning this may force completion of rooms in complete darkness
restrict_dungeon_item_on_boss: # aka ambrosia boss items
on: 0 # prevents unshuffled compasses, maps and keys to be boss drops, they can still drop keysanity and other players' items
off: 50
### End of Logic Section ###
bigkey_shuffle: # Big Key Placement
original_dungeon: 50
own_dungeons: 0
own_world: 0
any_world: 0
different_world: 0
start_with: 0
smallkey_shuffle: # Small Key Placement
original_dungeon: 50
own_dungeons: 0
own_world: 0
any_world: 0
different_world: 0
universal: 0
start_with: 0
key_drop_shuffle: # Shuffle keys found in pots or dropped from killed enemies
off: 50
on: 0
compass_shuffle: # Compass Placement
original_dungeon: 50
own_dungeons: 0
own_world: 0
any_world: 0
different_world: 0
start_with: 0
map_shuffle: # Map Placement
original_dungeon: 50
own_dungeons: 0
own_world: 0
any_world: 0
different_world: 0
start_with: 0
dungeon_counters:
on: 0 # Always display amount of items checked in a dungeon
pickup: 50 # Show when compass is picked up
default: 0 # Show when compass is picked up if the compass itself is shuffled
off: 0 # Never show item count in dungeons
progressive: # Enable or disable progressive items (swords, shields, bow)
on: 50 # All items are progressive
off: 0 # No items are progressive
grouped_random: 0 # Randomly decides for all items. Swords could be progressive, shields might not be
entrance_shuffle:
none: 50 # Vanilla game map. All entrances and exits lead to their original locations. You probably want this option
dungeonssimple: 0 # Shuffle just dungeons amongst each other, swapping dungeons entirely, so Hyrule Castle is always 1 dungeon
dungeonsfull: 0 # Shuffle any dungeon entrance with any dungeon interior, so Hyrule Castle can be 4 different dungeons, but keep dungeons to a specific world
dungeonscrossed: 0 # like dungeonsfull, but allow cross-world traversal through a dungeon. Warning: May force repeated dungeon traversal
simple: 0 # Entrances are grouped together before being randomized. Simple uses the most strict grouping rules
restricted: 0 # Less strict than simple
full: 0 # Less strict than restricted
crossed: 0 # Less strict than full
insanity: 0 # Very few grouping rules. Good luck
# you can also define entrance shuffle seed, like so:
crossed-1000: 0 # using this method, you can have the same layout as another player and share entrance information
# however, many other settings like logic, world state, retro etc. may affect the shuffle result as well.
crossed-group-myfriends: 0 # using this method, everyone with "group-myfriends" will share the same seed
goals:
ganon: 50 # Climb GT, defeat Agahnim 2, and then kill Ganon
crystals: 0 # Only killing Ganon is required. However, items may still be placed in GT
bosses: 0 # Defeat the boss of all dungeons, including Agahnim's tower and GT (Aga 2)
pedestal: 0 # Pull the Triforce from the Master Sword pedestal
ganon_pedestal: 0 # Pull the Master Sword pedestal, then kill Ganon
triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then turn them in to Murahadala in front of Hyrule Castle
local_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout your world, then turn them in to Murahadala in front of Hyrule Castle
ganon_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout the worlds, then kill Ganon
local_ganon_triforce_hunt: 0 # Collect 20 of 30 Triforce pieces spread throughout your world, then kill Ganon
ice_rod_hunt: 0 # You start with everything needed to 216 the seed. Find the Ice rod, then kill Trinexx at Turtle rock.
open_pyramid:
goal: 50 # Opens the pyramid if the goal requires you to kill Ganon, unless the goal is Slow Ganon or All Dungeons
auto: 0 # Same as Goal, but also is closed if holes are shuffled and ganon is part of the shuffle pool
open: 0 # Pyramid hole is always open. Ganon's vulnerable condition is still required before he can he hurt
closed: 0 # Pyramid hole is always closed until you defeat Agahnim atop Ganon's Tower
triforce_pieces_mode: #Determine how to calculate the extra available triforce pieces.
extra: 0 # available = triforce_pieces_extra + triforce_pieces_required
percentage: 0 # available = (triforce_pieces_percentage /100) * triforce_pieces_required
available: 50 # available = triforce_pieces_available
triforce_pieces_extra: # Set to how many extra triforces pieces are available to collect in the world.
# Format "pieces: chance"
0: 0
5: 50
10: 50
15: 0
20: 0
triforce_pieces_percentage: # Set to how many triforce pieces according to a percentage of the required ones, are available to collect in the world.
# Format "pieces: chance"
100: 0 #No extra
150: 50 #Half the required will be added as extra
200: 0 #There are the double of the required ones available.
triforce_pieces_available: # Set to how many triforces pieces are available to collect in the world. Default is 30. Max is 90, Min is 1
# Format "pieces: chance"
25: 0
30: 50
40: 0
50: 0
triforce_pieces_required: # Set to how many out of X triforce pieces you need to win the game in a triforce hunt. Default is 20. Max is 90, Min is 1
# Format "pieces: chance"
15: 0
20: 50
30: 0
40: 0
50: 0
crystals_needed_for_gt: # Crystals required to open GT
0: 0
7: 50
random: 0
random-low: 0 # any valid number, weighted towards the lower end
random-middle: 0 # any valid number, weighted towards the central range
random-high: 0 # any valid number, weighted towards the higher end
crystals_needed_for_ganon: # Crystals required to hurt Ganon
0: 0
7: 50
random: 0
random-low: 0
random-middle: 0
random-high: 0
mode:
standard: 0 # Begin the game by rescuing Zelda from her cell and escorting her to the Sanctuary
open: 50 # Begin the game from your choice of Link's House or the Sanctuary
inverted: 0 # Begin in the Dark World. The Moon Pearl is required to avoid bunny-state in Light World, and the Light World game map is altered
retro_bow:
on: 0 # Zelda-1 like mode. You have to purchase a quiver to shoot arrows using rupees.
off: 50
retro_caves:
on: 0 # Zelda-1 like mode. There are randomly placed take-any caves that contain one Sword and choices of Heart Container/Blue Potion.
off: 50
hints: # On/Full: Put item and entrance placement hints on telepathic tiles and some NPCs, Full removes joke hints.
'on': 50
'off': 0
full: 0
scams: # If on, these Merchants will no longer tell you what they're selling.
'off': 50
'king_zora': 0
'bottle_merchant': 0
'all': 0
swordless:
on: 0 # Your swords are replaced by rupees. Gameplay changes have been made to accommodate this change
off: 1
item_pool:
easy: 0 # Doubled upgrades, progressives, and etc
normal: 50 # Item availability remains unchanged from vanilla game
hard: 0 # Reduced upgrade availability (max: 14 hearts, blue mail, tempered sword, fire shield, no silvers unless swordless)
expert: 0 # Minimum upgrade availability (max: 8 hearts, green mail, master sword, fighter shield, no silvers unless swordless)
item_functionality:
easy: 0 # Allow Hammer to damage ganon, Allow Hammer tablet collection, Allow swordless medallion use everywhere.
normal: 50 # Vanilla item functionality
hard: 0 # Reduced helpfulness of items (potions less effective, can't catch faeries, cape uses double magic, byrna does not grant invulnerability, boomerangs do not stun, silvers disabled outside ganon)
expert: 0 # Vastly reduces the helpfulness of items (potions barely effective, can't catch faeries, cape uses double magic, byrna does not grant invulnerability, boomerangs and hookshot do not stun, silvers disabled outside ganon)
tile_shuffle: # Randomize the tile layouts in flying tile rooms
on: 0
off: 50
misery_mire_medallion: # required medallion to open Misery Mire front entrance
random: 50
Ether: 0
Bombos: 0
Quake: 0
turtle_rock_medallion: # required medallion to open Turtle Rock front entrance
random: 50
Ether: 0
Bombos: 0
Quake: 0
### Enemizer Section ###
boss_shuffle:
none: 50 # Vanilla bosses
basic: 0 # Existing bosses except Ganon and Agahnim are shuffled throughout dungeons
full: 0 # 3 bosses can occur twice
chaos: 0 # Any boss can appear any amount of times
singularity: 0 # Picks a boss, tries to put it everywhere that works, if there's spaces remaining it picks a boss to fill those
enemy_shuffle: # Randomize enemy placement
on: 0
off: 50
killable_thieves: # Make thieves killable
on: 0 # Usually turned on together with enemy_shuffle to make annoying thief placement more manageable
off: 50
bush_shuffle: # Randomize the chance that bushes have enemies and the enemies under said bush
on: 0
off: 50
enemy_damage:
default: 50 # Vanilla enemy damage
shuffled: 0 # Enemies deal 0 to 4 hearts and armor helps
chaos: 0 # Enemies deal 0 to 8 hearts and armor just reshuffles the damage
enemy_health:
default: 50 # Vanilla enemy HP
easy: 0 # Enemies have reduced health
hard: 0 # Enemies have increased health
expert: 0 # Enemies have greatly increased health
pot_shuffle:
'on': 0 # Keys, items, and buttons hidden under pots in dungeons are shuffled with other pots in their supertile
'off': 50 # Default pot item locations
### End of Enemizer Section ###
### Beemizer ###
# can add weights for any whole number between 0 and 100
beemizer_total_chance: # Remove items from the global item pool and replace them with single bees (fill bottles) and bee traps
0: 50 # No junk fill items are replaced (Beemizer is off)
25: 0 # 25% chance for each junk fill item (rupees, bombs and arrows) to be replaced with bees
50: 0 # 50% chance for each junk fill item (rupees, bombs and arrows) to be replaced with bees
75: 0 # 75% chance for each junk fill item (rupees, bombs and arrows) to be replaced with bees
100: 0 # All junk fill items (rupees, bombs and arrows) are replaced with bees
beemizer_trap_chance:
60: 50 # 60% chance for each beemizer replacement to be a trap, 40% chance to be a single bee
70: 0 # 70% chance for each beemizer replacement to be a trap, 30% chance to be a single bee
80: 0 # 80% chance for each beemizer replacement to be a trap, 20% chance to be a single bee
90: 0 # 90% chance for each beemizer replacement to be a trap, 10% chance to be a single bee
100: 0 # All beemizer replacements are traps
### Shop Settings ###
shop_item_slots: # Maximum amount of shop slots to be filled with regular item pool items (such as Moon Pearl)
0: 50
5: 0
15: 0
30: 0
random: 0 # 0 to 30 evenly distributed
shop_price_modifier: # Percentage modifier for shuffled item prices in shops
# you can add additional values between minimum and maximum
0: 0 # minimum value
400: 0 # maximum value
random: 0
random-low: 0
random-high: 0
100: 50
shop_shuffle:
none: 50
g: 0 # Generate new default inventories for overworld/underworld shops, and unique shops
f: 0 # Generate new default inventories for every shop independently
i: 0 # Shuffle default inventories of the shops around
p: 0 # Randomize the prices of the items in shop inventories
u: 0 # Shuffle capacity upgrades into the item pool (and allow them to traverse the multiworld)
w: 0 # Consider witch's hut like any other shop and shuffle/randomize it too
P: 0 # Prices of the items in shop inventories cost hearts, arrow, or bombs instead of rupees
ip: 0 # Shuffle inventories and randomize prices
fpu: 0 # Generate new inventories, randomize prices and shuffle capacity upgrades into item pool
uip: 0 # Shuffle inventories, randomize prices and shuffle capacity upgrades into the item pool
# You can add more combos
### End of Shop Section ###
shuffle_prizes: # aka drops
none: 0 # do not shuffle prize packs
g: 50 # shuffle "general" prize packs, as in enemy, tree pull, dig etc.
b: 0 # shuffle "bonk" prize packs
bg: 0 # shuffle both
timer:
none: 50 # No timer will be displayed.
timed: 0 # Starts with clock at zero. Green clocks subtract 4 minutes (total 20). Blue clocks subtract 2 minutes (total 10). Red clocks add two minutes (total 10). Winner is the player with the lowest time at the end.
timed_ohko: 0 # Starts the clock at ten minutes. Green clocks add five minutes (total 25). As long as the clock as at zero, Link will die in one hit.
ohko: 0 # Timer always at zero. Permanent OHKO.
timed_countdown: 0 # Starts the clock with forty minutes. Same clocks as timed mode, but if the clock hits zero you lose. You can still keep playing, though.
display: 0 # Displays a timer, but otherwise does not affect gameplay or the item pool.
countdown_start_time: # For timed_ohko and timed_countdown timer modes, the amount of time in minutes to start with
0: 0 # For timed_ohko, starts in OHKO mode when starting the game
10: 50
20: 0
30: 0
60: 0
red_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a red clock
-2: 50
1: 0
blue_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a blue clock
1: 0
2: 50
green_clock_time: # For all timer modes, the amount of time in minutes to gain or lose when picking up a green clock
4: 50
10: 0
15: 0
glitch_boots:
on: 50 # Start with Pegasus Boots in any glitched logic mode that makes use of them
off: 0
# rom options section
random_sprite_on_event: # An alternative to specifying randomonhit / randomonexit / etc... in sprite down below.
enabled: # If enabled, sprite down below is ignored completely, (although it may become the sprite pool)
on: 0
off: 1
on_hit: # Random sprite on hit. Being hit by things that cause 0 damage still counts.
on: 1
off: 0
on_enter: # Random sprite on underworld entry. Note that entering hobo counts.
on: 0
off: 1
on_exit: # Random sprite on underworld exit. Exiting hobo does not count.
on: 0
off: 1
on_slash: # Random sprite on sword slash. Note, it still counts if you attempt to slash while swordless.
on: 0
off: 1
on_item: # Random sprite on getting an item. Anything that causes you to hold an item above your head counts.
on: 0
off: 1
on_bonk: # Random sprite on bonk.
on: 0
off: 1
on_everything: # Random sprite on ALL currently implemented events, even if not documented at present time.
on: 0
off: 1
use_weighted_sprite_pool: # Always on if no sprite_pool exists, otherwise it controls whether to use sprite as a weighted sprite pool
on: 0
off: 1
#sprite_pool: # When specified, limits the pool of sprites used for randomon-event to the specified pool. Uncomment to use this.
# - link
# - pride link
# - penguin link
# - random # You can specify random multiple times for however many potentially unique random sprites you want in your pool.
sprite: # Enter the name of your preferred sprite and weight it appropriately
random: 0
randomonhit: 0 # Random sprite on hit
randomonenter: 0 # Random sprite on entering the underworld.
randomonexit: 0 # Random sprite on exiting the underworld.
randomonslash: 0 # Random sprite on sword slashes
randomonitem: 0 # Random sprite on getting items.
randomonbonk: 0 # Random sprite on bonk.
# You can combine these events like this. randomonhit-enter-exit if you want it on hit, enter, exit.
randomonall: 0 # Random sprite on any and all currently supported events. Refer to above for the supported events.
Link: 50 # To add other sprites: open the gui/Creator, go to adjust, select a sprite and write down the name the gui calls it
music: # If "off", all in-game music will be disabled
on: 50
off: 0
quickswap: # Enable switching items by pressing the L+R shoulder buttons
on: 50
off: 0
triforcehud: # Disable visibility of the triforce hud unless collecting a piece or speaking to Murahadala
normal: 0 # original behavior (always visible)
hide_goal: 50 # hide counter until a piece is collected or speaking to Murahadala
hide_required: 0 # Always visible, but required amount is invisible until determined by Murahadala
hide_both: 0 # Hide both under above circumstances
reduceflashing: # Reduces instances of flashing such as lightning attacks, weather, ether and more.
on: 50
off: 0
menuspeed: # Controls how fast the item menu opens and closes
normal: 50
instant: 0
double: 0
triple: 0
quadruple: 0
half: 0
heartcolor: # Controls the color of your health hearts
red: 50
blue: 0
green: 0
yellow: 0
random: 0
heartbeep: # Controls the frequency of the low-health beeping
double: 0
normal: 50
half: 0
quarter: 0
off: 0
ow_palettes: # Change the colors of the overworld
default: 50 # No changes
good: 0 # Shuffle the colors, with harmony in mind
blackout: 0 # everything black / blind mode
grayscale: 0
negative: 0
classic: 0
dizzy: 0
sick: 0
puke: 0
uw_palettes: # Change the colors of caves and dungeons
default: 50 # No changes
good: 0 # Shuffle the colors, with harmony in mind
blackout: 0 # everything black / blind mode
grayscale: 0
negative: 0
classic: 0
dizzy: 0
sick: 0
puke: 0
hud_palettes: # Change the colors of the hud
default: 50 # No changes
good: 0 # Shuffle the colors, with harmony in mind
blackout: 0 # everything black / blind mode
grayscale: 0
negative: 0
classic: 0
dizzy: 0
sick: 0
puke: 0
sword_palettes: # Change the colors of swords
default: 50 # No changes
good: 0 # Shuffle the colors, with harmony in mind
blackout: 0 # everything black / blind mode
grayscale: 0
negative: 0
classic: 0
dizzy: 0
sick: 0
puke: 0
shield_palettes: # Change the colors of shields
default: 50 # No changes
good: 0 # Shuffle the colors, with harmony in mind
blackout: 0 # everything black / blind mode
grayscale: 0
negative: 0
classic: 0
dizzy: 0
sick: 0
puke: 0
# triggers that replace options upon rolling certain options
legacy_weapons: # this is not an actual option, just a set of weights to trigger from
trigger_disabled: 50
randomized: 0 # Swords are placed randomly throughout the world
assured: 0 # Begin with a sword, the rest are placed randomly throughout the world
vanilla: 0 # Swords are placed in vanilla locations in your own game (Uncle, Pyramid Fairy, Smiths, Pedestal)
swordless: 0 # swordless mode
death_link:
false: 50
true: 0
allow_collect: # Allows for !collect / co-op to auto-open chests containing items for other players.
# Off by default, because it currently crashes on real hardware.
false: 50
true: 0
linked_options:
- name: crosskeys
options: # These overwrite earlier options if the percentage chance triggers
A Link to the Past:
entrance_shuffle: crossed
bigkey_shuffle: true
compass_shuffle: true
map_shuffle: true
smallkey_shuffle: true
percentage: 0 # Set this to the percentage chance you want crosskeys
- name: localcrosskeys
options: # These overwrite earlier options if the percentage chance triggers
A Link to the Past:
entrance_shuffle: crossed
bigkey_shuffle: true
compass_shuffle: true
map_shuffle: true
smallkey_shuffle: true
local_items: # Forces keys to be local to your own world
- "Small Keys"
- "Big Keys"
percentage: 0 # Set this to the percentage chance you want local crosskeys
- name: enemizer
options:
A Link to the Past:
boss_shuffle: # Subchances can be injected too, which then get rolled
basic: 1
full: 1
chaos: 1
singularity: 1
enemy_damage:
shuffled: 1
chaos: 1
enemy_health:
easy: 1
hard: 1
expert: 1
percentage: 0 # Set this to the percentage chance you want enemizer
triggers:
# trigger block for legacy weapons mode, to enable these add weights to legacy_weapons
- option_name: legacy_weapons
option_result: randomized
option_category: A Link to the Past
options:
A Link to the Past:
swordless: off
- option_name: legacy_weapons
option_result: assured
option_category: A Link to the Past
options:
A Link to the Past:
swordless: off
start_inventory:
Progressive Sword: 1
- option_name: legacy_weapons
option_result: vanilla
option_category: A Link to the Past
options:
A Link to the Past:
swordless: off
plando_items:
- items:
Progressive Sword: 4
locations:
- Master Sword Pedestal
- Pyramid Fairy - Left
- Blacksmith
- Link's Uncle
- option_name: legacy_weapons
option_result: swordless
option_category: A Link to the Past
options:
A Link to the Past:
swordless: on
# end of legacy weapons block
- option_name: enemy_damage # targets enemy_damage
option_category: A Link to the Past
option_result: shuffled # if it rolls shuffled
percentage: 0 # AND has a 0 percent chance (meaning this is default disabled, just to show how it works)
options: # then inserts these options
A Link to the Past:
swordless: off

View File

@@ -11,4 +11,4 @@ certifi>=2023.11.17
cython>=3.0.8
cymem>=2.0.8
orjson>=3.9.10
typing-extensions>=4.7.0
typing_extensions>=4.7.0

View File

@@ -1,6 +1,6 @@
"""
Application settings / host.yaml interface using type hints.
This is different from player settings.
This is different from player options.
"""
import os.path

View File

@@ -20,9 +20,13 @@ __all__ = {
"user_folder",
"GamesPackage",
"DataPackage",
"failed_world_loads",
}
failed_world_loads: List[str] = []
class GamesPackage(TypedDict, total=False):
item_name_groups: Dict[str, List[str]]
item_name_to_id: Dict[str, int]
@@ -87,6 +91,7 @@ class WorldSource:
file_like.seek(0)
import logging
logging.exception(file_like.read())
failed_world_loads.append(os.path.basename(self.path).rsplit(".", 1)[0])
return False

View File

@@ -43,7 +43,7 @@ an experience customized for their taste, and different players in the same mult
You can generate a yaml or download a template by visiting the [Adventure Options Page](/games/Adventure/player-options)
### What are recommended settings to tweak for beginners to the rando?
### What are recommended options to tweak for beginners to the rando?
Setting difficulty_switch_a and lowering the dragons' speeds makes the dragons easier to avoid. Adding Chalice to
local_items guarantees you'll visit at least one of the interesting castles, as it can only be placed in a castle or
the credits room.

View File

@@ -42,7 +42,7 @@ une expérience personnalisée à leur goût, et différents joueurs dans le mê
### Où puis-je obtenir un fichier YAML ?
Vous pouvez générer un yaml ou télécharger un modèle en visitant la [page des paramètres d'aventure](/games/Adventure/player-settings)
Vous pouvez générer un yaml ou télécharger un modèle en visitant la [page des paramètres d'aventure](/games/Adventure/player-options)
### Quels sont les paramètres recommandés pour s'initier à la rando ?
Régler la difficulty_switch_a et réduire la vitesse des dragons rend les dragons plus faciles à éviter. Ajouter Calice à
@@ -72,4 +72,4 @@ configuré pour le faire automatiquement.
Pour connecter le client au multiserveur, mettez simplement `<adresse>:<port>` dans le champ de texte en haut et appuyez sur Entrée (si le
le serveur utilise un mot de passe, saisissez dans le champ de texte inférieur `/connect <adresse> :<port> [mot de passe]`)
Appuyez sur Réinitialiser et commencez à jouer
Appuyez sur Réinitialiser et commencez à jouer

View File

@@ -3,6 +3,8 @@ from collections import defaultdict
from .OverworldGlitchRules import overworld_glitch_connections
from .UnderworldGlitchRules import underworld_glitch_connections
from .Regions import mark_light_world_regions
from .InvertedRegions import mark_dark_world_regions
def link_entrances(world, player):
@@ -1827,6 +1829,10 @@ def plando_connect(world, player: int):
func(world, connection.entrance, connection.exit, player)
except Exception as e:
raise Exception(f"Could not connect using {connection}") from e
if world.mode[player] != 'inverted':
mark_light_world_regions(world, player)
else:
mark_dark_world_regions(world, player)
LW_Dungeon_Entrances = ['Desert Palace Entrance (South)',

View File

@@ -381,8 +381,8 @@ def create_inverted_regions(world, player):
create_dungeon_region(world, player, 'Skull Woods First Section (Top)', 'Skull Woods', ['Skull Woods - Big Chest'], ['Skull Woods First Section (Top) One-Way Path']),
create_dungeon_region(world, player, 'Skull Woods Second Section (Drop)', 'Skull Woods', None, ['Skull Woods Second Section (Drop)']),
create_dungeon_region(world, player, 'Skull Woods Second Section', 'Skull Woods', ['Skull Woods - Big Key Chest', 'Skull Woods - West Lobby Pot Key'], ['Skull Woods Second Section Exit (East)', 'Skull Woods Second Section Exit (West)']),
create_dungeon_region(world, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room', 'Skull Woods - Spike Corner Key Drop'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']),
create_dungeon_region(world, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Boss', 'Skull Woods - Prize']),
create_dungeon_region(world, player, 'Skull Woods Final Section (Entrance)', 'Skull Woods', ['Skull Woods - Bridge Room'], ['Skull Woods Torch Room', 'Skull Woods Final Section Exit']),
create_dungeon_region(world, player, 'Skull Woods Final Section (Mothula)', 'Skull Woods', ['Skull Woods - Spike Corner Key Drop', 'Skull Woods - Boss', 'Skull Woods - Prize']),
create_dungeon_region(world, player, 'Ice Palace (Entrance)', 'Ice Palace', ['Ice Palace - Jelly Key Drop', 'Ice Palace - Compass Chest'], ['Ice Palace (Second Section)', 'Ice Palace Exit']),
create_dungeon_region(world, player, 'Ice Palace (Second Section)', 'Ice Palace', ['Ice Palace - Conveyor Key Drop'], ['Ice Palace (Main)']),
create_dungeon_region(world, player, 'Ice Palace (Main)', 'Ice Palace', ['Ice Palace - Freezor Chest',

View File

@@ -4,7 +4,7 @@ import Utils
import worlds.Files
LTTPJPN10HASH: str = "03a63945398191337e896e5771f77173"
RANDOMIZERBASEHASH: str = "35d010bc148e0ea0ee68e81e330223f1"
RANDOMIZERBASEHASH: str = "8704fb9b9fa4fad52d4d2f9a95fb5360"
ROM_PLAYER_LIMIT: int = 255
import io

View File

@@ -345,42 +345,43 @@ class ALTTPWorld(World):
def create_regions(self):
player = self.player
world = self.multiworld
multiworld = self.multiworld
if world.mode[player] != 'inverted':
create_regions(world, player)
if multiworld.mode[player] != 'inverted':
create_regions(multiworld, player)
else:
create_inverted_regions(world, player)
create_shops(world, player)
create_inverted_regions(multiworld, player)
create_shops(multiworld, player)
self.create_dungeons()
if world.glitches_required[player] not in ["no_glitches", "minor_glitches"] and world.entrance_shuffle[player] in \
{"vanilla", "dungeons_simple", "dungeons_full", "simple", "restricted", "full"}:
world.fix_fake_world[player] = False
if (multiworld.glitches_required[player] not in ["no_glitches", "minor_glitches"] and
multiworld.entrance_shuffle[player] in [
"vanilla", "dungeons_simple", "dungeons_full", "simple", "restricted", "full"]):
multiworld.fix_fake_world[player] = False
# seeded entrance shuffle
old_random = world.random
world.random = random.Random(self.er_seed)
old_random = multiworld.random
multiworld.random = random.Random(self.er_seed)
if world.mode[player] != 'inverted':
link_entrances(world, player)
mark_light_world_regions(world, player)
if multiworld.mode[player] != 'inverted':
link_entrances(multiworld, player)
mark_light_world_regions(multiworld, player)
for region_name, entrance_name in indirect_connections_not_inverted.items():
world.register_indirect_condition(world.get_region(region_name, player),
world.get_entrance(entrance_name, player))
multiworld.register_indirect_condition(multiworld.get_region(region_name, player),
multiworld.get_entrance(entrance_name, player))
else:
link_inverted_entrances(world, player)
mark_dark_world_regions(world, player)
link_inverted_entrances(multiworld, player)
mark_dark_world_regions(multiworld, player)
for region_name, entrance_name in indirect_connections_inverted.items():
world.register_indirect_condition(world.get_region(region_name, player),
world.get_entrance(entrance_name, player))
multiworld.register_indirect_condition(multiworld.get_region(region_name, player),
multiworld.get_entrance(entrance_name, player))
world.random = old_random
plando_connect(world, player)
multiworld.random = old_random
plando_connect(multiworld, player)
for region_name, entrance_name in indirect_connections.items():
world.register_indirect_condition(world.get_region(region_name, player),
world.get_entrance(entrance_name, player))
multiworld.register_indirect_condition(multiworld.get_region(region_name, player),
multiworld.get_entrance(entrance_name, player))
def collect_item(self, state: CollectionState, item: Item, remove=False):
item_name = item.name

View File

@@ -47,12 +47,12 @@ wählen können!
### Wo bekomme ich so eine YAML-Datei her?
Die [Player Settings](/games/A Link to the Past/player-settings) Seite auf der Website ermöglicht das einfache Erstellen
Die [Player Options](/games/A Link to the Past/player-options) Seite auf der Website ermöglicht das einfache Erstellen
und Herunterladen deiner eigenen `yaml` Datei. Drei verschiedene Voreinstellungen können dort gespeichert werden.
### Deine YAML-Datei ist gewichtet!
Die **Player Settings** Seite hat eine Menge Optionen, die man per Schieber einstellen kann. Das ermöglicht es,
Die **Player Options** Seite hat eine Menge Optionen, die man per Schieber einstellen kann. Das ermöglicht es,
verschiedene Optionen mit unterschiedlichen Wahrscheinlichkeiten in einer Kategorie ausgewürfelt zu werden
Als Beispiel kann man sich die Option "Map Shuffle" als einen Eimer mit Zetteln zur Abstimmung Vorstellen. So kann man

View File

@@ -59,7 +59,7 @@ de multiworld puede tener diferentes opciones.
### Donde puedo obtener un fichero YAML?
La página "[Generate Game](/games/A%20Link%20to%20the%20Past/player-settings)" en el sitio web te permite configurar tu
La página "[Generate Game](/games/A%20Link%20to%20the%20Past/player-options)" en el sitio web te permite configurar tu
configuración personal y descargar un fichero "YAML".
### Configuración YAML avanzada
@@ -86,7 +86,7 @@ Si quieres validar que tu fichero YAML para asegurarte que funciona correctament
## Generar una partida para un jugador
1. Navega a [la pagina Generate game](/games/A%20Link%20to%20the%20Past/player-settings), configura tus opciones, haz
1. Navega a [la pagina Generate game](/games/A%20Link%20to%20the%20Past/player-options), configura tus opciones, haz
click en el boton "Generate game".
2. Se te redigirá a una pagina "Seed Info", donde puedes descargar tu archivo de parche.
3. Haz doble click en tu fichero de parche, y el emulador debería ejecutar tu juego automáticamente. Como el Cliente no

View File

@@ -60,7 +60,7 @@ peuvent avoir différentes options.
### Où est-ce que j'obtiens un fichier YAML ?
La page [Génération de partie](/games/A%20Link%20to%20the%20Past/player-settings) vous permet de configurer vos
La page [Génération de partie](/games/A%20Link%20to%20the%20Past/player-options) vous permet de configurer vos
paramètres personnels et de les exporter vers un fichier YAML.
### Configuration avancée du fichier YAML
@@ -87,7 +87,7 @@ Si vous voulez valider votre fichier YAML pour être sûr qu'il fonctionne, vous
## Générer une partie pour un joueur
1. Aller sur la page [Génération de partie](/games/A%20Link%20to%20the%20Past/player-settings), configurez vos options,
1. Aller sur la page [Génération de partie](/games/A%20Link%20to%20the%20Past/player-options), configurez vos options,
et cliquez sur le bouton "Generate Game".
2. Il vous sera alors présenté une page d'informations sur la seed, où vous pourrez télécharger votre patch.
3. Double-cliquez sur le patch et l'émulateur devrait se lancer automatiquement avec la seed. Etant donné que le client
@@ -207,4 +207,4 @@ Le logiciel recommandé pour l'auto-tracking actuellement est
3. Sélectionnez votre appareil SNES dans la liste déroulante.
4. Si vous voulez tracquer les petites clés ainsi que les objets des donjons, cochez la case **Race Illegal Tracking**
5. Cliquez sur le bouton **Start Autotracking**
6. Fermez la fenêtre "AutoTracker" maintenant, elle n'est plus nécessaire
6. Fermez la fenêtre "AutoTracker" maintenant, elle n'est plus nécessaire

View File

@@ -8,5 +8,5 @@
[ArchipIDLE GitHub Releases Page](https://github.com/ArchipelagoMW/archipidle/releases)
3. Enter the server address in the `Server Address` field and press enter
4. Enter your slot name when prompted. This should be the same as the `name` you entered on the
setting page above, or the `name` field in your yaml file.
options page above, or the `name` field in your yaml file.
5. Click the "Begin!" button.

View File

@@ -1,11 +1,10 @@
# Guide de configuration d'ArchipIdle
## Rejoindre une partie MultiWorld
1. Générez un fichier `.yaml` à partir de la [page des paramètres du lecteur ArchipIDLE](/games/ArchipIDLE/player-settings)
1. Générez un fichier `.yaml` à partir de la [page des paramètres du lecteur ArchipIDLE](/games/ArchipIDLE/player-options)
2. Ouvrez le client ArchipIDLE dans votre navigateur Web en :
- Accédez au [Client ArchipIDLE](http://idle.multiworld.link)
- Téléchargez le client et exécutez-le localement à partir du
[Page des versions d'ArchipIDLE GitHub](https://github.com/ArchipelagoMW/archipidle/releases)
- Accédez au [Client ArchipIDLE](http://idle.multiworld.link)
- Téléchargez le client et exécutez-le localement à partir du [Page des versions d'ArchipIDLE GitHub](https://github.com/ArchipelagoMW/archipidle/releases)
3. Entrez l'adresse du serveur dans le champ `Server Address` et appuyez sur Entrée
4. Entrez votre nom d'emplacement lorsque vous y êtes invité. Il doit être le même que le `name` que vous avez saisi sur le
page de configuration ci-dessus, ou le champ `name` dans votre fichier yaml.

View File

@@ -48,9 +48,9 @@ class CV64Web(WebWorld):
class CV64World(World):
"""
Castlevania for the Nintendo 64 is the first 3D game in the franchise. As either whip-wielding Belmont descendant
Reinhardt Schneider or powerful sorceress Carrie Fernandez, brave many terrifying traps and foes as you make your
way to Dracula's chamber and stop his rule of terror!
Castlevania for the Nintendo 64 is the first 3D game in the Castlevania franchise. As either whip-wielding Belmont
descendant Reinhardt Schneider or powerful sorceress Carrie Fernandez, brave many terrifying traps and foes as you
make your way to Dracula's chamber and stop his rule of terror!
"""
game = "Castlevania 64"
item_name_groups = {

View File

@@ -1,8 +1,8 @@
# Castlevania 64
## Where is the settings page?
## Where is the options page?
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
The [player options page for this game](../player-options) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
@@ -91,7 +91,7 @@ either filler, useful, or a trap.
When you pick up someone else's item, you will not receive anything and the item textbox will show up to announce what you
found and who it was for. The color of the text will tell you its classification:
- <font color="moccasin">Light brown-ish</font>: Common
- <font color="moccasin">Light brown-ish</font>: Filler
- <font color="white">White</font>/<font color="yellow">Yellow</font>: Useful
- <font color="yellow">Yellow</font>/<font color="lime">Green</font>: Progression
- <font color="yellow">Yellow</font>/<font color="red">Red</font>: Trap
@@ -116,7 +116,7 @@ Enabling Carrie Logic will also expect the following:
- Orb-sniping dogs through the front gates in Villa
Library Skip is **NOT** logically expected on any setting. The basement hallway crack will always logically expect two Nitros
Library Skip is **NOT** logically expected by any options. The basement arena crack will always logically expect two Nitros
and two Mandragoras even with Hard Logic on due to the possibility of wasting a pair on the upper wall, after managing
to skip past it. And plus, the RNG manip may not even be possible after picking up all the items in the Nitro room.

View File

@@ -28,8 +28,8 @@ the White Jewels.
## Generating and Patching a Game
1. Create your settings file (YAML). You can make one on the
[Castlevania 64 settings page](../../../games/Castlevania 64/player-settings).
1. Create your options file (YAML). You can make one on the
[Castlevania 64 options page](../../../games/Castlevania%2064/player-options).
2. Follow the general Archipelago instructions for [generating a game](../../Archipelago/setup/en#generating-a-game).
This will generate an output file for you. Your patch file will have the `.apcv64` file extension.
3. Open `ArchipelagoLauncher.exe`

View File

@@ -144,8 +144,9 @@ class HardLogic(Toggle):
class MultiHitBreakables(Toggle):
"""Adds the items that drop from the objects that break in three hits to the pool. There are 17 of these throughout
the game, adding up to 74 checks in total with all stages.
"""Adds the items that drop from the objects that break in three hits to the pool. There are 18 of these throughout
the game, adding up to 79 or 80 checks (depending on sub-weapons
being shuffled anywhere or not) in total with all stages.
The game will be modified to
remember exactly which of their items you've picked up instead of simply whether they were broken or not."""
display_name = "Multi-hit Breakables"
@@ -337,13 +338,13 @@ class BigToss(Toggle):
"""Makes every non-immobilizing damage source launch you as if you got hit by Behemoth's charge.
Press A while tossed to cancel the launch momentum and avoid being thrown off ledges.
Hold Z to have all incoming damage be treated as it normally would.
Any tricks that might be possible with it are NOT considered in logic on any setting."""
Any tricks that might be possible with it are NOT considered in logic by any options."""
display_name = "Big Toss"
class PantherDash(Choice):
"""Hold C-right at any time to sprint way faster. Any tricks that might be
possible with it are NOT considered in logic on any setting and any boss
possible with it are NOT considered in logic by any options and any boss
fights with boss health meters, if started, are expected to be finished
before leaving their arenas if Dracula's Condition is bosses. Jumpless will
prevent jumping while moving at the increased speed to ensure logic cannot be broken with it."""

View File

@@ -29,5 +29,5 @@ placez-le à la racine du jeu (ex: "SteamLibrary\steamapps\common\DARK SOULS III
## Où trouver le fichier de configuration ?
La [Page de configuration](/games/Dark%20Souls%20III/player-settings) sur le site vous permez de configurer vos
La [Page de configuration](/games/Dark%20Souls%20III/player-options) sur le site vous permez de configurer vos
paramètres et de les exporter sous la forme d'un fichier.

View File

@@ -2,7 +2,7 @@
## Où se trouve la page des paramètres ?
La [page des paramètres du joueur pour ce jeu](../player-settings) contient tous les paramètres dont vous avez besoin pour configurer et exporter le fichier.
La [page des paramètres du joueur pour ce jeu](../player-options) contient tous les paramètres dont vous avez besoin pour configurer et exporter le fichier.
## Quel est l'effet de la randomisation sur ce jeu ?
@@ -46,4 +46,4 @@ Il y a aussi de nouveaux objets pièges, utilisés comme substituts, basés sur
Chaque fois qu'un objet est reçu en ligne, une notification apparaît à l'écran pour en informer le joueur.
Certains objets sont accompagnés d'une animation ou d'une scène qui se déroule immédiatement après leur réception.
Les objets reçus hors ligne ne sont pas accompagnés d'une animation ou d'une scène, et sont simplement activés lors de la connexion.
Les objets reçus hors ligne ne sont pas accompagnés d'une animation ou d'une scène, et sont simplement activés lors de la connexion.

View File

@@ -18,7 +18,7 @@ Voir le guide d'Archipelago sur la mise en place d'un YAML de base : [Basic Mult
### Où puis-je obtenir un fichier YAML ?
Vous pouvez personnaliser vos paramètres en visitant la [page des paramètres du joueur DLC Quest] (/games/DLCQuest/player-settings).
Vous pouvez personnaliser vos paramètres en visitant la [page des paramètres du joueur DLC Quest](/games/DLCQuest/player-options).
## Rejoindre une partie multi-monde
@@ -52,4 +52,4 @@ Vous pouvez personnaliser vos paramètres en visitant la [page des paramètres d
Vous ne pouvez pas envoyer de commandes au serveur ou discuter avec les autres joueurs depuis DLC Quest, car le jeu ne dispose pas d'un moyen approprié pour saisir du texte.
Vous pouvez suivre l'activité du serveur dans votre console BepInEx, car les messages de chat d'Archipelago y seront affichés.
Vous devrez utiliser [Archipelago Text Client] (https://github.com/ArchipelagoMW/Archipelago/releases) si vous voulez envoyer des commandes.
Vous devrez utiliser [Archipelago Text Client] (https://github.com/ArchipelagoMW/Archipelago/releases) si vous voulez envoyer des commandes.

View File

@@ -21,7 +21,7 @@ import Utils
from CommonClient import ClientCommandProcessor, CommonContext, logger, server_loop, gui_enabled, get_base_parser
from MultiServer import mark_raw
from NetUtils import ClientStatus, NetworkItem, JSONtoTextParser, JSONMessagePart
from Utils import async_start
from Utils import async_start, get_file_safe_name
def check_stdin() -> None:
@@ -120,7 +120,7 @@ class FactorioContext(CommonContext):
@property
def savegame_name(self) -> str:
return f"AP_{self.seed_name}_{self.auth}_Save.zip"
return get_file_safe_name(f"AP_{self.seed_name}_{self.auth}")+"_Save.zip"
def print_to_game(self, text):
self.rcon_client.send_command(f"/ap-print [font=default-large-bold]Archipelago:[/font] "

View File

@@ -2,27 +2,28 @@
This guide covers more the more advanced options available in YAML files. This guide is intended for the user who plans
to edit their YAML file manually. This guide should take about 10 minutes to read.
If you would like to generate a basic, fully playable YAML without editing a file, then visit the settings page for the
If you would like to generate a basic, fully playable YAML without editing a file, then visit the options page for the
game you intend to play. The weighted settings page can also handle most of the advanced settings discussed here.
The settings page can be found on the supported games page, just click the "Settings Page" link under the name of the
game you would like.
The options page can be found on the supported games page, just click the "Options Page" link under the name of the
game you would like.
* Supported games page: [Archipelago Games List](/games)
* Weighted settings page: [Archipelago Weighted Settings](/weighted-settings)
Clicking on the "Export Settings" button at the bottom-left will provide you with a pre-filled YAML with your options.
The player settings page also has a link to download a full template file for that game which will have every option
Clicking on the "Export Options" button at the bottom-left will provide you with a pre-filled YAML with your options.
The player options page also has a link to download a full template file for that game which will have every option
possible for the game including some that don't display correctly on the site.
## YAML Overview
The Archipelago system generates games using player configuration files as input. These are going to be YAML files and
each world will have one of these containing their custom settings for the game that world will play.
each world will have one of these containing their custom options for the game that world will play.
## YAML Formatting
YAML files are a format of human-readable config files. The basic syntax of a yaml file will have a `root` node and then
different levels of `nested` nodes that the generator reads in order to determine your settings.
different levels of `nested` nodes that the generator reads in order to determine your options.
To nest text, the correct syntax is to indent **two spaces over** from its root option. A YAML file can be edited with
whatever text editor you choose to use though I personally recommend that you use Sublime Text. Sublime text
@@ -53,13 +54,13 @@ so `option_one_setting_one` is guaranteed to occur.
For `nested_option_two`, `option_two_setting_one` will be rolled 14 times and `option_two_setting_two` will be rolled 43
times against each other. This means `option_two_setting_two` will be more likely to occur, but it isn't guaranteed,
adding more randomness and "mystery" to your settings. Every configurable setting supports weights.
adding more randomness and "mystery" to your options. Every configurable setting supports weights.
## Root Options
Currently, there are only a few options that are root options. Everything else should be nested within one of these root
options or in some cases nested within other nested options. The only options that should exist in root
are `description`, `name`, `game`, `requires`, and the name of the games you want settings for.
are `description`, `name`, `game`, `requires`, and the name of the games you want options for.
* `description` is ignored by the generator and is simply a good way for you to organize if you have multiple files
using this to detail the intention of the file.
@@ -79,15 +80,15 @@ are `description`, `name`, `game`, `requires`, and the name of the games you wan
* `requires` details different requirements from the generator for the YAML to work as you expect it to. Generally this
is good for detailing the version of Archipelago this YAML was prepared for as, if it is rolled on an older version,
settings may be missing and as such it will not work as expected. If any plando is used in the file then requiring it
options may be missing and as such it will not work as expected. If any plando is used in the file then requiring it
here to ensure it will be used is good practice.
## Game Options
One of your root settings will be the name of the game you would like to populate with settings. Since it is possible to
One of your root options will be the name of the game you would like to populate with options. Since it is possible to
give a weight to any option, it is possible to have one file that can generate a seed for you where you don't know which
game you'll play. For these cases you'll want to fill the game options for every game that can be rolled by these
settings. If a game can be rolled it **must** have a settings section even if it is empty.
settings. If a game can be rolled it **must** have an options section even if it is empty.
### Universal Game Options

View File

@@ -201,7 +201,7 @@ Kirby's Dream Land 3:
As this is currently only supported by A Link to the Past, instead of finding an explanation here, please refer to the
relevant guide: [A Link to the Past Plando Guide](/tutorial/A%20Link%20to%20the%20Past/plando/en)
## Connections Plando
## Connection Plando
This is currently only supported by a few games, including A Link to the Past, Minecraft, and Ocarina of Time. As the way that these games interact with their
connections is different, only the basics are explained here. More specific information for connection plando in A Link to the Past can be found in

View File

@@ -39,7 +39,7 @@ to your Archipelago installation.
### What is a YAML?
YAML is the file format which Archipelago uses in order to configure a player's world. It allows you to dictate which
game you will be playing as well as the settings you would like for that game.
game you will be playing as well as the options you would like for that game.
YAML is a format very similar to JSON however it is made to be more human-readable. If you are ever unsure of the
validity of your YAML file you may check the file by uploading it to the check page on the Archipelago website:
@@ -48,10 +48,10 @@ validity of your YAML file you may check the file by uploading it to the check p
### Creating a YAML
YAML files may be generated on the Archipelago website by visiting the [games page](/games) and clicking the
"Settings Page" link under the relevant game. Clicking "Export Settings" in a game's settings page will download the
"Options Page" link under the relevant game. Clicking "Export Options" in a game's options page will download the
YAML to your system.
Alternatively, you can run `ArchipelagoLauncher.exe` and click on `Generate Template Settings` to create a set of template
Alternatively, you can run `ArchipelagoLauncher.exe` and click on `Generate Template Options` to create a set of template
YAMLs for each game in your Archipelago install (including for APWorlds). These will be placed in your `Players/Templates` folder.
In a multiworld there must be one YAML per world. Any number of players can play on each world using either the game's
@@ -66,17 +66,17 @@ each player is planning on playing their own game then they will each need a YAM
#### On the website
The easiest way to get started playing an Archipelago generated game, after following the base setup from the game's
setup guide, is to find the game on the [Archipelago Games List](/games), click on `Settings Page`, set the settings for
setup guide, is to find the game on the [Archipelago Games List](/games), click on `Options Page`, set the options for
how you want to play, and click `Generate Game` at the bottom of the page. This will create a page for the seed, from
which you can create a room, and then [connect](#connecting-to-an-archipelago-server).
If you have downloaded the settings, or have created a settings file manually, this file can be uploaded on the
If you have downloaded the options, or have created an options file manually, this file can be uploaded on the
[Generation Page](/generate) where you can also set any specific hosting settings.
#### On your local installation
To generate a game on your local machine, make sure to install the Archipelago software. Navigate to your Archipelago
installation (usually C:\ProgramData\Archipelago), and place the settings file you have either created or downloaded
installation (usually C:\ProgramData\Archipelago), and place the options file you have either created or downloaded
from the website in the `Players` folder.
Run `ArchipelagoGenerate.exe`, or click on `Generate` in the launcher, and it will inform you whether the generation
@@ -97,7 +97,7 @@ resources, and host the resulting multiworld on the website.
#### Gather All Player YAMLs
All players that wish to play in the generated multiworld must have a YAML file which contains the settings that they
All players that wish to play in the generated multiworld must have a YAML file which contains the options that they
wish to play with. One person should gather all files from all participants in the generated multiworld. It is possible
for a single player to have multiple games, or even multiple slots of a single game, but each YAML must have a unique
player name.
@@ -129,7 +129,7 @@ need the corresponding ROM files.
Sometimes there are various settings that you may want to change before rolling a seed such as enabling race mode,
auto-release, plando support, or setting a password.
All of these settings, plus other options, may be changed by modifying the `host.yaml` file in the Archipelago
All of these settings, plus more, can be changed by modifying the `host.yaml` file in the Archipelago
installation folder. You can quickly access this file by clicking on `Open host.yaml` in the launcher. The settings
chosen here are baked into the `.archipelago` file that gets output with the other files after generation, so if you
are rolling locally, ensure this file is edited to your liking **before** rolling the seed. This file is overwritten
@@ -207,4 +207,4 @@ when creating your [YAML file](#creating-a-yaml). If the game is hosted on the w
room page. The name is case-sensitive.
* `Password` is the password set by the host in order to join the multiworld. By default, this will be empty and is almost
never required, but one can be set when generating the game. Generally, leave this field blank when it exists,
unless you know that a password was set, and what that password is.
unless you know that a password was set, and what that password is.

View File

@@ -6,7 +6,7 @@ about 5 minutes to read.
## What are triggers?
Triggers allow you to customize your game settings by allowing you to define one or many options which only occur under
Triggers allow you to customize your game options by allowing you to define one or many options which only occur under
specific conditions. These are essentially "if, then" statements for options in your game. A good example of what you
can do with triggers is the [custom mercenary mode YAML
](https://github.com/alwaysintreble/Archipelago-yaml-dump/blob/main/Snippets/Mercenary%20Mode%20Snippet.yaml) that was
@@ -148,4 +148,4 @@ In this example, if the `start_location` option rolls `landing_site`, only a sta
If `aqueduct` is rolled, a starting hint for Gravity Suit will also be created alongside the hint for Morph Ball.
Note that for lists, items can only be added, not removed or replaced. For dicts, defining a value for a present key will
replace that value within the dict.
replace that value within the dict.

View File

@@ -1,4 +1,5 @@
import typing
import re
from .ExtractedData import logic_options, starts, pool_options
from .Rules import cost_terms
@@ -11,12 +12,16 @@ if typing.TYPE_CHECKING:
else:
Random = typing.Any
locations = {"option_" + start: i for i, start in enumerate(starts)}
# This way the dynamic start names are picked up by the MetaClass Choice belongs to
StartLocation = type("StartLocation", (Choice,), {"__module__": __name__, "auto_display_name": False, **locations,
"__doc__": "Choose your start location. "
"This is currently only locked to King's Pass."})
StartLocation = type("StartLocation", (Choice,), {
"__module__": __name__,
"auto_display_name": False,
"display_name": "Start Location",
"__doc__": "Choose your start location. "
"This is currently only locked to King's Pass.",
**locations,
})
del (locations)
option_docstrings = {
@@ -49,8 +54,7 @@ option_docstrings = {
"RandomizeBossEssence": "Randomize boss essence drops, such as those for defeating Warrior Dreams, into the item "
"pool and open their locations\n for randomization.",
"RandomizeGrubs": "Randomize Grubs into the item pool and open their locations for randomization.",
"RandomizeMimics": "Randomize Mimic Grubs into the item pool and open their locations for randomization."
"Mimic Grubs are always placed\n in your own game.",
"RandomizeMimics": "Randomize Mimic Grubs into the item pool and open their locations for randomization.",
"RandomizeMaps": "Randomize Maps into the item pool. This causes Cornifer to give you a message allowing you to see"
" and buy an item\n that is randomized into that location as well.",
"RandomizeStags": "Randomize Stag Stations unlocks into the item pool as well as placing randomized items "
@@ -99,8 +103,12 @@ default_on = {
"RandomizeKeys",
"RandomizeMaskShards",
"RandomizeVesselFragments",
"RandomizeCharmNotches",
"RandomizePaleOre",
"RandomizeRelics"
"RandomizeRancidEggs"
"RandomizeRelics",
"RandomizeStags",
"RandomizeLifebloodCocoons"
}
shop_to_option = {
@@ -117,6 +125,7 @@ shop_to_option = {
hollow_knight_randomize_options: typing.Dict[str, type(Option)] = {}
splitter_pattern = re.compile(r'(?<!^)(?=[A-Z])')
for option_name, option_data in pool_options.items():
extra_data = {"__module__": __name__, "items": option_data[0], "locations": option_data[1]}
if option_name in option_docstrings:
@@ -125,6 +134,7 @@ for option_name, option_data in pool_options.items():
option = type(option_name, (DefaultOnToggle,), extra_data)
else:
option = type(option_name, (Toggle,), extra_data)
option.display_name = splitter_pattern.sub(" ", option_name)
globals()[option.__name__] = option
hollow_knight_randomize_options[option.__name__] = option
@@ -133,11 +143,14 @@ for option_name in logic_options.values():
if option_name in hollow_knight_randomize_options:
continue
extra_data = {"__module__": __name__}
# some options, such as elevator pass, appear in logic_options despite explicitly being
# handled below as classes.
if option_name in option_docstrings:
extra_data["__doc__"] = option_docstrings[option_name]
option = type(option_name, (Toggle,), extra_data)
globals()[option.__name__] = option
hollow_knight_logic_options[option.__name__] = option
option.display_name = splitter_pattern.sub(" ", option_name)
globals()[option.__name__] = option
hollow_knight_logic_options[option.__name__] = option
class RandomizeElevatorPass(Toggle):
@@ -269,11 +282,11 @@ class RandomCharmCosts(NamedRange):
random_source.shuffle(charms)
return charms
else:
charms = [0]*self.charm_count
charms = [0] * self.charm_count
for x in range(self.value):
index = random_source.randint(0, self.charm_count-1)
index = random_source.randint(0, self.charm_count - 1)
while charms[index] > 5:
index = random_source.randint(0, self.charm_count-1)
index = random_source.randint(0, self.charm_count - 1)
charms[index] += 1
return charms
@@ -404,6 +417,7 @@ class WhitePalace(Choice):
class ExtraPlatforms(DefaultOnToggle):
"""Places additional platforms to make traveling throughout Hallownest more convenient."""
display_name = "Extra Platforms"
class AddUnshuffledLocations(Toggle):
@@ -413,6 +427,7 @@ class AddUnshuffledLocations(Toggle):
Note: This will increase the number of location checks required to purchase
hints to the total maximum.
"""
display_name = "Add Unshuffled Locations"
class DeathLinkShade(Choice):
@@ -430,6 +445,7 @@ class DeathLinkShade(Choice):
option_shadeless = 1
option_shade = 2
default = 2
display_name = "Deathlink Shade Handling"
class DeathLinkBreaksFragileCharms(Toggle):
@@ -439,6 +455,7 @@ class DeathLinkBreaksFragileCharms(Toggle):
** Self-death fragile charm behavior is not changed; if a self-death normally breaks fragile charms in vanilla, it
will continue to do so.
"""
display_name = "Deathlink Breaks Fragile Charms"
class StartingGeo(Range):
@@ -462,18 +479,20 @@ class CostSanity(Choice):
alias_yes = 1
option_shopsonly = 2
option_notshops = 3
display_name = "Cost Sanity"
display_name = "Costsanity"
class CostSanityHybridChance(Range):
"""The chance that a CostSanity cost will include two components instead of one, e.g. Grubs + Essence"""
range_end = 100
default = 10
display_name = "Costsanity Hybrid Chance"
cost_sanity_weights: typing.Dict[str, type(Option)] = {}
for term, cost in cost_terms.items():
option_name = f"CostSanity{cost.option}Weight"
display_name = f"Costsanity {cost.option} Weight"
extra_data = {
"__module__": __name__, "range_end": 1000,
"__doc__": (
@@ -486,10 +505,10 @@ for term, cost in cost_terms.items():
extra_data["__doc__"] += " Geo costs will never be chosen for Grubfather, Seer, or Egg Shop."
option = type(option_name, (Range,), extra_data)
option.display_name = display_name
globals()[option.__name__] = option
cost_sanity_weights[option.__name__] = option
hollow_knight_options: typing.Dict[str, type(Option)] = {
**hollow_knight_randomize_options,
RandomizeElevatorPass.__name__: RandomizeElevatorPass,

View File

@@ -8,7 +8,9 @@ config file.
## What does randomization do to this game?
Randomization swaps around the locations of items. The items being swapped around are chosen within your YAML.
Shop costs are presently always randomized.
Shop costs are presently always randomized. Items which could be randomized, but are not, will remain unmodified in
their usual locations. In particular, when the items at Grubfather and Seer are partially randomized, randomized items
will be obtained from a chest in the room, while unrandomized items will be given by the NPC as normal.
## What Hollow Knight items can appear in other players' worlds?

View File

@@ -3,6 +3,8 @@
## Required Software
* Download and unzip the Lumafly Mod Manager from the [Lumafly website](https://themulhima.github.io/Lumafly/).
* A legal copy of Hollow Knight.
* Steam, Gog, and Xbox Game Pass versions of the game are supported.
* Windows, Mac, and Linux (including Steam Deck) are supported.
## Installing the Archipelago Mod using Lumafly
1. Launch Lumafly and ensure it locates your Hollow Knight installation directory.
@@ -10,25 +12,25 @@
* If desired, also install "Archipelago Map Mod" to use as an in-game tracker.
3. Launch the game, you're all set!
### What to do if Lumafly fails to find your XBox Game Pass installation directory
1. Enter the XBox app and move your mouse over "Hollow Knight" on the left sidebar.
2. Click the three points then click "Manage".
3. Go to the "Files" tab and select "Browse...".
4. Click "Hollow Knight", then "Content", then click the path bar and copy it.
5. Run Lumafly as an administrator and, when it asks you for the path, paste what you copied in step 4.
#### Alternative Method:
1. Click on your profile then "Settings".
2. Go to the "General" tab and select "CHANGE FOLDER".
3. Look for a folder where you want to install the game (preferably inside a folder on your desktop) and copy the path.
4. Run Lumafly as an administrator and, when it asks you for the path, paste what you copied in step 3.
Note: The path folder needs to have the "Hollow Knight_Data" folder inside.
### What to do if Lumafly fails to find your installation directory
1. Find the directory manually.
* Xbox Game Pass:
1. Enter the XBox app and move your mouse over "Hollow Knight" on the left sidebar.
2. Click the three points then click "Manage".
3. Go to the "Files" tab and select "Browse...".
4. Click "Hollow Knight", then "Content", then click the path bar and copy it.
* Steam:
1. You likely put your Steam library in a non-standard place. If this is the case, you probably know where
it is. Find your steam library and then find the Hollow Knight folder and copy the path.
* Windows - `C:\Program Files (x86)\Steam\steamapps\common\Hollow Knight`
* Linux/Steam Deck - ~/.local/share/Steam/steamapps/common/Hollow Knight
* Mac - ~/Library/Application Support/Steam/steamapps/common/Hollow Knight/hollow_knight.app
2. Run Lumafly as an administrator and, when it asks you for the path, paste the path you copied.
## Configuring your YAML File
### What is a YAML and why do I need one?
You can see the [basic multiworld setup guide](/tutorial/Archipelago/setup/en) here on the Archipelago website to learn
about why Archipelago uses YAML files and what they're for.
An YAML file is the way that you provide your player options to Archipelago.
See the [basic multiworld setup guide](/tutorial/Archipelago/setup/en) here on the Archipelago website to learn more.
### Where do I get a YAML?
You can use the [game options page for Hollow Knight](/games/Hollow%20Knight/player-options) here on the Archipelago
@@ -44,9 +46,7 @@ website to generate a YAML using a graphical interface.
* If you are waiting for a countdown then wait for it to lapse before hitting Start.
* Or hit Start then pause the game once you're in it.
## Commands
While playing the multiworld you can interact with the server using various commands listed in the
[commands guide](/tutorial/Archipelago/commands/en). As this game does not have an in-game text client at the moment,
You can optionally connect to the multiworld using the text client, which can be found in the
[main Archipelago installation](https://github.com/ArchipelagoMW/Archipelago/releases) as Archipelago Text Client to
enter these commands.
## Hints and other commands
While playing in a multiworld, you can interact with the server using various commands listed in the
[commands guide](/tutorial/Archipelago/commands/en). You can use the Archipelago Text Client to do this,
which is included in the latest release of the [Archipelago software](https://github.com/ArchipelagoMW/Archipelago/releases/latest).

View File

@@ -34,8 +34,6 @@ class Hylics2World(World):
location_name_to_id = {data["name"]: loc_id for loc_id, data in all_locations.items()}
option_definitions = Options.hylics2_options
topology_present: bool = True
data_version = 3
start_location = "Waynehouse"
@@ -51,10 +49,6 @@ class Hylics2World(World):
return Hylics2Item(name, self.all_items[item_id]["classification"], item_id, player=self.player)
def add_item(self, name: str, classification: ItemClassification, code: int) -> "Item":
return Hylics2Item(name, classification, code, self.player)
def create_event(self, event: str):
return Hylics2Item(event, ItemClassification.progression_skip_balancing, None, self.player)
@@ -62,7 +56,7 @@ class Hylics2World(World):
# set random starting location if option is enabled
def generate_early(self):
if self.multiworld.random_start[self.player]:
i = self.multiworld.random.randint(0, 3)
i = self.random.randint(0, 3)
if i == 0:
self.start_location = "Waynehouse"
elif i == 1:
@@ -77,26 +71,26 @@ class Hylics2World(World):
pool = []
# add regular items
for i, data in Items.item_table.items():
if data["count"] > 0:
for j in range(data["count"]):
pool.append(self.add_item(data["name"], data["classification"], i))
for item in Items.item_table.values():
if item["count"] > 0:
for _ in range(item["count"]):
pool.append(self.create_item(item["name"]))
# add party members if option is enabled
if self.multiworld.party_shuffle[self.player]:
for i, data in Items.party_item_table.items():
pool.append(self.add_item(data["name"], data["classification"], i))
for item in Items.party_item_table.values():
pool.append(self.create_item(item["name"]))
# handle gesture shuffle
if not self.multiworld.gesture_shuffle[self.player]: # add gestures to pool like normal
for i, data in Items.gesture_item_table.items():
pool.append(self.add_item(data["name"], data["classification"], i))
for item in Items.gesture_item_table.values():
pool.append(self.create_item(item["name"]))
# add '10 Bones' items if medallion shuffle is enabled
if self.multiworld.medallion_shuffle[self.player]:
for i, data in Items.medallion_item_table.items():
for j in range(data["count"]):
pool.append(self.add_item(data["name"], data["classification"], i))
for item in Items.medallion_item_table.values():
for _ in range(item["count"]):
pool.append(self.create_item(item["name"]))
# add to world's pool
self.multiworld.itempool += pool
@@ -107,48 +101,45 @@ class Hylics2World(World):
if self.multiworld.gesture_shuffle[self.player] == 2: # vanilla locations
gestures = Items.gesture_item_table
self.multiworld.get_location("Waynehouse: TV", self.player)\
.place_locked_item(self.add_item(gestures[200678]["name"], gestures[200678]["classification"], 200678))
.place_locked_item(self.create_item("POROMER BLEB"))
self.multiworld.get_location("Afterlife: TV", self.player)\
.place_locked_item(self.add_item(gestures[200683]["name"], gestures[200683]["classification"], 200683))
.place_locked_item(self.create_item("TELEDENUDATE"))
self.multiworld.get_location("New Muldul: TV", self.player)\
.place_locked_item(self.add_item(gestures[200679]["name"], gestures[200679]["classification"], 200679))
.place_locked_item(self.create_item("SOUL CRISPER"))
self.multiworld.get_location("Viewax's Edifice: TV", self.player)\
.place_locked_item(self.add_item(gestures[200680]["name"], gestures[200680]["classification"], 200680))
.place_locked_item(self.create_item("TIME SIGIL"))
self.multiworld.get_location("TV Island: TV", self.player)\
.place_locked_item(self.add_item(gestures[200681]["name"], gestures[200681]["classification"], 200681))
.place_locked_item(self.create_item("CHARGE UP"))
self.multiworld.get_location("Juice Ranch: TV", self.player)\
.place_locked_item(self.add_item(gestures[200682]["name"], gestures[200682]["classification"], 200682))
.place_locked_item(self.create_item("FATE SANDBOX"))
self.multiworld.get_location("Foglast: TV", self.player)\
.place_locked_item(self.add_item(gestures[200684]["name"], gestures[200684]["classification"], 200684))
.place_locked_item(self.create_item("LINK MOLLUSC"))
self.multiworld.get_location("Drill Castle: TV", self.player)\
.place_locked_item(self.add_item(gestures[200688]["name"], gestures[200688]["classification"], 200688))
.place_locked_item(self.create_item("NEMATODE INTERFACE"))
self.multiworld.get_location("Sage Airship: TV", self.player)\
.place_locked_item(self.add_item(gestures[200685]["name"], gestures[200685]["classification"], 200685))
.place_locked_item(self.create_item("BOMBO - GENESIS"))
elif self.multiworld.gesture_shuffle[self.player] == 1: # TVs only
gestures = list(Items.gesture_item_table.items())
tvs = list(Locations.tv_location_table.items())
gestures = [gesture["name"] for gesture in Items.gesture_item_table.values()]
tvs = [tv["name"] for tv in Locations.tv_location_table.values()]
# if Extra Items in Logic is enabled place CHARGE UP first and make sure it doesn't get
# placed at Sage Airship: TV or Foglast: TV
if self.multiworld.extra_items_in_logic[self.player]:
tv = self.multiworld.random.choice(tvs)
gest = gestures.index((200681, Items.gesture_item_table[200681]))
while tv[1]["name"] == "Sage Airship: TV" or tv[1]["name"] == "Foglast: TV":
tv = self.multiworld.random.choice(tvs)
self.multiworld.get_location(tv[1]["name"], self.player)\
.place_locked_item(self.add_item(gestures[gest][1]["name"], gestures[gest][1]["classification"],
gestures[gest]))
gestures.remove(gestures[gest])
tv = self.random.choice(tvs)
while tv == "Sage Airship: TV" or tv == "Foglast: TV":
tv = self.random.choice(tvs)
self.multiworld.get_location(tv, self.player)\
.place_locked_item(self.create_item("CHARGE UP"))
gestures.remove("CHARGE UP")
tvs.remove(tv)
for i in range(len(gestures)):
gest = self.multiworld.random.choice(gestures)
tv = self.multiworld.random.choice(tvs)
self.multiworld.get_location(tv[1]["name"], self.player)\
.place_locked_item(self.add_item(gest[1]["name"], gest[1]["classification"], gest[0]))
gestures.remove(gest)
tvs.remove(tv)
self.random.shuffle(gestures)
self.random.shuffle(tvs)
while gestures:
gesture = gestures.pop()
tv = tvs.pop()
self.get_location(tv).place_locked_item(self.create_item(gesture))
def fill_slot_data(self) -> Dict[str, Any]:

View File

@@ -36,8 +36,10 @@ KDL3_STARS_FLAG = SRAM_1_START + 0x901A
KDL3_GIFTING_FLAG = SRAM_1_START + 0x901C
KDL3_LEVEL_ADDR = SRAM_1_START + 0x9020
KDL3_IS_DEMO = SRAM_1_START + 0x5AD5
KDL3_GAME_STATE = SRAM_1_START + 0x36D0
KDL3_GAME_SAVE = SRAM_1_START + 0x3617
KDL3_CURRENT_WORLD = SRAM_1_START + 0x363F
KDL3_CURRENT_LEVEL = SRAM_1_START + 0x3641
KDL3_GAME_STATE = SRAM_1_START + 0x36D0
KDL3_LIFE_COUNT = SRAM_1_START + 0x39CF
KDL3_KIRBY_HP = SRAM_1_START + 0x39D1
KDL3_BOSS_HP = SRAM_1_START + 0x39D5
@@ -46,8 +48,6 @@ KDL3_LIFE_VISUAL = SRAM_1_START + 0x39E3
KDL3_HEART_STARS = SRAM_1_START + 0x53A7
KDL3_WORLD_UNLOCK = SRAM_1_START + 0x53CB
KDL3_LEVEL_UNLOCK = SRAM_1_START + 0x53CD
KDL3_CURRENT_WORLD = SRAM_1_START + 0x53CF
KDL3_CURRENT_LEVEL = SRAM_1_START + 0x53D3
KDL3_BOSS_STATUS = SRAM_1_START + 0x53D5
KDL3_INVINCIBILITY_TIMER = SRAM_1_START + 0x54B1
KDL3_MG5_STATUS = SRAM_1_START + 0x5EE4
@@ -74,7 +74,9 @@ deathlink_messages = defaultdict(lambda: " was defeated.", {
0x0202: " was out-numbered by Pon & Con.",
0x0203: " was defeated by Ado's powerful paintings.",
0x0204: " was clobbered by King Dedede.",
0x0205: " lost their battle against Dark Matter."
0x0205: " lost their battle against Dark Matter.",
0x0300: " couldn't overcome the Boss Butch.",
0x0400: " is bad at jumping.",
})
@@ -281,6 +283,11 @@ class KDL3SNIClient(SNIClient):
for i in range(5):
level_data = await snes_read(ctx, KDL3_LEVEL_ADDR + (14 * i), 14)
self.levels[i] = unpack("HHHHHHH", level_data)
self.levels[5] = [0x0205, # Hyper Zone
0, # MG-5, can't send from here
0x0300, # Boss Butch
0x0400, # Jumping
0, 0, 0]
if self.consumables is None:
consumables = await snes_read(ctx, KDL3_CONSUMABLE_FLAG, 1)
@@ -314,7 +321,7 @@ class KDL3SNIClient(SNIClient):
current_world = struct.unpack("H", await snes_read(ctx, KDL3_CURRENT_WORLD, 2))[0]
current_level = struct.unpack("H", await snes_read(ctx, KDL3_CURRENT_LEVEL, 2))[0]
currently_dead = current_hp[0] == 0x00
message = deathlink_messages[self.levels[current_world][current_level - 1]]
message = deathlink_messages[self.levels[current_world][current_level]]
await ctx.handle_deathlink_state(currently_dead, f"{ctx.player_names[ctx.slot]}{message}")
recv_count = await snes_read(ctx, KDL3_RECV_COUNT, 2)

View File

@@ -28,16 +28,30 @@ first_stage_blacklist = {
0x77001C, # 5-4 needs Burning
}
first_world_limit = {
# We need to limit the number of very restrictive stages in level 1 on solo gens
*first_stage_blacklist, # all three of the blacklist stages need 2+ items for both checks
0x770007,
0x770008,
0x770013,
0x77001E,
def generate_valid_level(level, stage, possible_stages, slot_random):
new_stage = slot_random.choice(possible_stages)
if level == 1 and stage == 0 and new_stage in first_stage_blacklist:
return generate_valid_level(level, stage, possible_stages, slot_random)
else:
return new_stage
}
def generate_rooms(world: "KDL3World", door_shuffle: bool, level_regions: typing.Dict[int, Region]):
def generate_valid_level(world: "KDL3World", level, stage, possible_stages, placed_stages):
new_stage = world.random.choice(possible_stages)
if level == 1:
if stage == 0 and new_stage in first_stage_blacklist:
return generate_valid_level(world, level, stage, possible_stages, placed_stages)
elif not (world.multiworld.players > 1 or world.options.consumables or world.options.starsanity) and \
new_stage in first_world_limit and \
sum(p_stage in first_world_limit for p_stage in placed_stages) >= 2:
return generate_valid_level(world, level, stage, possible_stages, placed_stages)
return new_stage
def generate_rooms(world: "KDL3World", level_regions: typing.Dict[int, Region]):
level_names = {LocationName.level_names[level]: level for level in LocationName.level_names}
room_data = orjson.loads(get_data(__name__, os.path.join("data", "Rooms.json")))
rooms: typing.Dict[str, KDL3Room] = dict()
@@ -49,8 +63,8 @@ def generate_rooms(world: "KDL3World", door_shuffle: bool, level_regions: typing
room.add_locations({location: world.location_name_to_id[location] if location in world.location_name_to_id else
None for location in room_entry["locations"]
if (not any(x in location for x in ["1-Up", "Maxim"]) or
world.options.consumables.value) and ("Star" not in location
or world.options.starsanity.value)},
world.options.consumables.value) and ("Star" not in location
or world.options.starsanity.value)},
KDL3Location)
rooms[room.name] = room
for location in room.locations:
@@ -62,33 +76,25 @@ def generate_rooms(world: "KDL3World", door_shuffle: bool, level_regions: typing
world.multiworld.regions.extend(world.rooms)
first_rooms: typing.Dict[int, KDL3Room] = dict()
if door_shuffle:
# first, we need to generate the notable edge cases
# 5-6 is the first, being the most restrictive
# half of its rooms are required to be vanilla, but can be in different orders
# the room before it *must* contain the copy ability required to unlock the room's goal
raise NotImplementedError()
else:
for name, room in rooms.items():
if room.room == 0:
if room.stage == 7:
first_rooms[0x770200 + room.level - 1] = room
else:
first_rooms[0x770000 + ((room.level - 1) * 6) + room.stage] = room
exits = dict()
for def_exit in room.default_exits:
target = f"{level_names[room.level]} {room.stage} - {def_exit['room']}"
access_rule = tuple(def_exit["access_rule"])
exits[target] = lambda state, rule=access_rule: state.has_all(rule, world.player)
room.add_exits(
exits.keys(),
exits
)
if world.options.open_world:
if any("Complete" in location.name for location in room.locations):
room.add_locations({f"{level_names[room.level]} {room.stage} - Stage Completion": None},
KDL3Location)
for name, room in rooms.items():
if room.room == 0:
if room.stage == 7:
first_rooms[0x770200 + room.level - 1] = room
else:
first_rooms[0x770000 + ((room.level - 1) * 6) + room.stage] = room
exits = dict()
for def_exit in room.default_exits:
target = f"{level_names[room.level]} {room.stage} - {def_exit['room']}"
access_rule = tuple(def_exit["access_rule"])
exits[target] = lambda state, rule=access_rule: state.has_all(rule, world.player)
room.add_exits(
exits.keys(),
exits
)
if world.options.open_world:
if any("Complete" in location.name for location in room.locations):
room.add_locations({f"{level_names[room.level]} {room.stage} - Stage Completion": None},
KDL3Location)
for level in world.player_levels:
for stage in range(6):
@@ -102,7 +108,7 @@ def generate_rooms(world: "KDL3World", door_shuffle: bool, level_regions: typing
if world.options.open_world or stage == 0:
level_regions[level].add_exits([first_rooms[proper_stage].name])
else:
world.multiworld.get_location(world.location_id_to_name[world.player_levels[level][stage-1]],
world.multiworld.get_location(world.location_id_to_name[world.player_levels[level][stage - 1]],
world.player).parent_region.add_exits([first_rooms[proper_stage].name])
level_regions[level].add_exits([first_rooms[0x770200 + level - 1].name])
@@ -141,8 +147,7 @@ def generate_valid_levels(world: "KDL3World", enforce_world: bool, enforce_patte
or (enforce_pattern and ((candidate - 1) & 0x00FFFF) % 6 == stage)
or (enforce_pattern == enforce_world)
]
new_stage = generate_valid_level(level, stage, stage_candidates,
world.random)
new_stage = generate_valid_level(world, level, stage, stage_candidates, levels[level])
possible_stages.remove(new_stage)
levels[level][stage] = new_stage
except Exception:
@@ -218,7 +223,7 @@ def create_levels(world: "KDL3World") -> None:
level_shuffle == 1,
level_shuffle == 2)
generate_rooms(world, False, levels)
generate_rooms(world, levels)
level6.add_locations({LocationName.goals[world.options.goal]: None}, KDL3Location)

View File

@@ -264,7 +264,7 @@ def set_rules(world: "KDL3World") -> None:
for r in [range(1, 31), range(44, 51)]:
for i in r:
set_rule(world.multiworld.get_location(f"Cloudy Park 4 - Star {i}", world.player),
lambda state: can_reach_clean(state, world.player))
lambda state: can_reach_coo(state, world.player))
for i in [18, *list(range(20, 25))]:
set_rule(world.multiworld.get_location(f"Cloudy Park 6 - Star {i}", world.player),
lambda state: can_reach_ice(state, world.player))

View File

@@ -206,6 +206,8 @@ class KDL3World(World):
locations = [self.multiworld.get_location(spawn, self.player) for spawn in spawns]
items = [self.create_item(animal) for animal in animal_pool]
allstate = self.multiworld.get_all_state(False)
self.random.shuffle(locations)
self.random.shuffle(items)
fill_restrictive(self.multiworld, allstate, locations, items, True, True)
else:
animal_friends = animal_friend_spawns.copy()

View File

@@ -1,8 +1,8 @@
# Kirby's Dream Land 3
## Where is the settings page?
## Where is the options page?
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
The [player options page for this game](../player-options) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
@@ -15,6 +15,7 @@ as Heart Stars, 1-Ups, and Invincibility Candy will be shuffled into the pool fo
- Purifying a boss after acquiring a certain number of Heart Stars
(indicated by their portrait flashing in the level select)
- If enabled, 1-Ups and Maxim Tomatoes
- If enabled, every single Star Piece within a stage
## When the player receives an item, what happens?
A sound effect will play, and Kirby will immediately receive the effects of that item, such as being able to receive Copy Abilities from enemies that

View File

@@ -43,8 +43,8 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
### Where do I get a config file?
The [Player Settings](/games/Kirby's%20Dream%20Land%203/player-settings) page on the website allows you to configure
your personal settings and export a config file from them.
The [Player Options](/games/Kirby's%20Dream%20Land%203/player-options) page on the website allows you to configure
your personal options and export a config file from them.
### Verifying your config file
@@ -53,7 +53,7 @@ If you would like to validate your config file to make sure it works, you may do
## Generating a Single-Player Game
1. Navigate to the [Player Settings](/games/Kirby's%20Dream%20Land%203/player-settings) page, configure your options,
1. Navigate to the [Player Options](/games/Kirby's%20Dream%20Land%203/player-options) page, configure your options,
and click the "Generate Game" button.
2. You will be presented with a "Seed Info" page.
3. Click the "Create New Room" link.

View File

@@ -33,7 +33,8 @@ class TestLocations(KDL3TestBase):
self.run_location_test(LocationName.iceberg_kogoesou, ["Burning"])
self.run_location_test(LocationName.iceberg_samus, ["Ice"])
self.run_location_test(LocationName.iceberg_name, ["Burning", "Coo", "ChuChu"])
self.run_location_test(LocationName.iceberg_angel, ["Cutter", "Burning", "Spark", "Parasol", "Needle", "Clean", "Stone", "Ice"])
self.run_location_test(LocationName.iceberg_angel, ["Cutter", "Burning", "Spark", "Parasol", "Needle", "Clean",
"Stone", "Ice"])
def run_location_test(self, location: str, itempool: typing.List[str]):
items = itempool.copy()

View File

@@ -184,19 +184,22 @@ class LinksAwakeningWorld(World):
self.pre_fill_items = []
# For any and different world, set item rule instead
for option in ["maps", "compasses", "small_keys", "nightmare_keys", "stone_beaks", "instruments"]:
option = "shuffle_" + option
for dungeon_item_type in ["maps", "compasses", "small_keys", "nightmare_keys", "stone_beaks", "instruments"]:
option = "shuffle_" + dungeon_item_type
option = self.player_options[option]
dungeon_item_types[option.ladxr_item] = option.value
# The color dungeon does not contain an instrument
num_items = 8 if dungeon_item_type == "instruments" else 9
if option.value == DungeonItemShuffle.option_own_world:
self.multiworld.local_items[self.player].value |= {
ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, 10)
ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, num_items + 1)
}
elif option.value == DungeonItemShuffle.option_different_world:
self.multiworld.non_local_items[self.player].value |= {
ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, 10)
ladxr_item_to_la_item_name[f"{option.ladxr_item}{i}"] for i in range(1, num_items + 1)
}
# option_original_dungeon = 0
# option_own_dungeons = 1

View File

@@ -1,4 +1,5 @@
import logging
from datetime import date
from typing import Any, ClassVar, Dict, List, Optional, TextIO
from BaseClasses import CollectionState, Entrance, Item, ItemClassification, MultiWorld, Tutorial
@@ -9,7 +10,8 @@ from worlds.AutoWorld import WebWorld, World
from worlds.LauncherComponents import Component, Type, components
from .client_setup import launch_game
from .connections import CONNECTIONS, RANDOMIZED_CONNECTIONS, TRANSITIONS
from .constants import ALL_ITEMS, ALWAYS_LOCATIONS, BOSS_LOCATIONS, FILLER, NOTES, PHOBEKINS, PROG_ITEMS, USEFUL_ITEMS
from .constants import ALL_ITEMS, ALWAYS_LOCATIONS, BOSS_LOCATIONS, FILLER, NOTES, PHOBEKINS, PROG_ITEMS, TRAPS, \
USEFUL_ITEMS
from .options import AvailablePortals, Goal, Logic, MessengerOptions, NotesNeeded, ShuffleTransitions
from .portals import PORTALS, add_closed_portal_reqs, disconnect_portals, shuffle_portals, validate_portals
from .regions import LEVELS, MEGA_SHARDS, LOCATIONS, REGION_CONNECTIONS
@@ -110,7 +112,7 @@ class MessengerWorld(World):
},
}
required_client_version = (0, 4, 3)
required_client_version = (0, 4, 4)
web = MessengerWeb()
@@ -127,6 +129,7 @@ class MessengerWorld(World):
portal_mapping: List[int]
transitions: List[Entrance]
reachable_locs: int = 0
filler: Dict[str, int]
def generate_early(self) -> None:
if self.options.goal == Goal.option_power_seal_hunt:
@@ -146,8 +149,9 @@ class MessengerWorld(World):
self.starting_portals = [f"{portal} Portal"
for portal in starting_portals[:3] +
self.random.sample(starting_portals[3:], k=self.options.available_portals - 3)]
# super complicated method for adding searing crags to starting portals if it wasn't chosen
# need to add a check for transition shuffle when that gets added back in
# TODO add a check for transition shuffle when that gets added back in
if not self.options.shuffle_portals and "Searing Crags Portal" not in self.starting_portals:
self.starting_portals.append("Searing Crags Portal")
if len(self.starting_portals) > 4:
@@ -155,6 +159,10 @@ class MessengerWorld(World):
if portal in self.starting_portals]
self.starting_portals.remove(self.random.choice(portals_to_strip))
self.filler = FILLER.copy()
if (not hasattr(self.options, "traps") and date.today() < date(2024, 4, 2)) or self.options.traps:
self.filler.update(TRAPS)
self.plando_portals = []
self.portal_mapping = []
self.spoiler_portal_mapping = {}
@@ -182,12 +190,13 @@ class MessengerWorld(World):
def create_items(self) -> None:
# create items that are always in the item pool
main_movement_items = ["Rope Dart", "Wingsuit"]
precollected_names = [item.name for item in self.multiworld.precollected_items[self.player]]
itempool: List[MessengerItem] = [
self.create_item(item)
for item in self.item_name_to_id
if "Time Shard" not in item and item not in {
if item not in {
"Power Seal", *NOTES, *FIGURINES, *main_movement_items,
*{collected_item.name for collected_item in self.multiworld.precollected_items[self.player]},
*precollected_names, *FILLER, *TRAPS,
}
]
@@ -199,7 +208,7 @@ class MessengerWorld(World):
if self.options.goal == Goal.option_open_music_box:
# make a list of all notes except those in the player's defined starting inventory, and adjust the
# amount we need to put in the itempool and precollect based on that
notes = [note for note in NOTES if note not in self.multiworld.precollected_items[self.player]]
notes = [note for note in NOTES if note not in precollected_names]
self.random.shuffle(notes)
precollected_notes_amount = NotesNeeded.range_end - \
self.options.notes_needed - \
@@ -228,8 +237,8 @@ class MessengerWorld(World):
remaining_fill = len(self.multiworld.get_unfilled_locations(self.player)) - len(itempool)
if remaining_fill < 10:
self._filler_items = self.random.choices(
list(FILLER)[2:],
weights=list(FILLER.values())[2:],
list(self.filler)[2:],
weights=list(self.filler.values())[2:],
k=remaining_fill
)
filler = [self.create_filler() for _ in range(remaining_fill)]
@@ -300,8 +309,8 @@ class MessengerWorld(World):
def get_filler_item_name(self) -> str:
if not getattr(self, "_filler_items", None):
self._filler_items = [name for name in self.random.choices(
list(FILLER),
weights=list(FILLER.values()),
list(self.filler),
weights=list(self.filler.values()),
k=20
)]
return self._filler_items.pop(0)
@@ -335,6 +344,9 @@ class MessengerWorld(World):
if name in {*USEFUL_ITEMS, *USEFUL_SHOP_ITEMS}:
return ItemClassification.useful
if name in TRAPS:
return ItemClassification.trap
return ItemClassification.filler

View File

@@ -48,6 +48,11 @@ FILLER = {
"Time Shard (500)": 5,
}
TRAPS = {
"Teleport Trap": 5,
"Prophecy Trap": 10,
}
# item_name_to_id needs to be deterministic and match upstream
ALL_ITEMS = [
*NOTES,
@@ -71,6 +76,8 @@ ALL_ITEMS = [
*SHOP_ITEMS,
*FIGURINES,
"Money Wrench",
"Teleport Trap",
"Prophecy Trap",
]
# locations

View File

@@ -49,30 +49,29 @@ for it. The groups you can use for The Messenger are:
## Other changes
* The player can return to the Tower of Time HQ at any point by selecting the button from the options menu
* This can cause issues if used at specific times. Current known:
* During Boss fights
* After Courage Note collection (Corrupted Future chase)
* This is currently an expected action in logic. If you do need to teleport during this chase sequence, it
is recommended to quit to title and reload the save
* This can cause issues if used at specific times. If used in any of these known problematic areas, immediately
quit to title and reload the save. The currently known areas include:
* During Boss fights
* After Courage Note collection (Corrupted Future chase)
* After reaching ninja village a teleport option is added to the menu to reach it quickly
* Toggle Windmill Shuriken button is added to option menu once the item is received
* The mod option menu will also have a hint item button, as well as a release and collect button that are all placed when
the player fulfills the necessary conditions.
* The mod option menu will also have a hint item button, as well as a release and collect button that are all placed
when the player fulfills the necessary conditions.
* After running the game with the mod, a config file (APConfig.toml) will be generated in your game folder that can be
used to modify certain settings such as text size and color. This can also be used to specify a player name that can't
be entered in game.
used to modify certain settings such as text size and color. This can also be used to specify a player name that can't
be entered in game.
## Known issues
* Ruxxtin Coffin cutscene will sometimes not play correctly, but will still reward the item
* If you receive the Magic Firefly while in Quillshroom Marsh, The De-curse Queen cutscene will not play. You can exit
to Searing Crags and re-enter to get it to play correctly.
* Sometimes upon teleporting back to HQ, Ninja will run left and enter a different portal than the one entered by the
player. This may also cause a softlock.
to Searing Crags and re-enter to get it to play correctly.
* Teleporting back to HQ, then returning to the same level you just left through a Portal can cause Ninja to run left
and enter a different portal than the one entered by the player or lead to other incorrect inputs, causing a soft lock
* Text entry menus don't accept controller input
* In power seal hunt mode, the chest must be opened by entering the shop from a level. Teleporting to HQ and opening the
chest will not work.
chest will not work.
## What do I do if I have a problem?
If you believe something happened that isn't intended, please get the `log.txt` from the folder of your game installation
and send a bug report either on GitHub or the [Archipelago Discord Server](http://archipelago.gg/discord)
If you believe something happened that isn't intended, please get the `log.txt` from the folder of your game
installation and send a bug report either on GitHub or the [Archipelago Discord Server](http://archipelago.gg/discord)

View File

@@ -18,17 +18,17 @@ Read changes to the base game on the [Game Info Page](/games/The%20Messenger/inf
3. Click on "The Messenger"
4. Follow the prompts
These steps can also be followed to launch the game and check for mod updates after the initial setup.
### Manual Installation
1. 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)
2. Download and install the randomizer mod
1. Download the latest TheMessengerRandomizerAP.zip from
[The Messenger Randomizer Mod AP releases page](https://github.com/alwaysintreble/TheMessengerRandomizerModAP/releases)
[The Messenger Randomizer Mod AP releases page](https://github.com/alwaysintreble/TheMessengerRandomizerModAP/releases)
2. Extract the zip file to `TheMessenger/Mods/` of your game's install location
* You cannot have both the non-AP randomizer and the AP randomizer installed at the same time. The AP randomizer
is backwards compatible, so the non-AP mod can be safely removed, and you can still play seeds generated from the
non-AP randomizer.
* You cannot have both the non-AP randomizer and the AP randomizer installed at the same time
3. Optionally, Backup your save game
* On Windows
1. Press `Windows Key + R` to open run
@@ -46,13 +46,13 @@ Read changes to the base game on the [Game Info Page](/games/The%20Messenger/inf
3. Enter connection info using the relevant option buttons
* **The game is limited to alphanumerical characters, `.`, and `-`.**
* This defaults to `archipelago.gg` and does not need to be manually changed if connecting to a game hosted on the
website.
website.
* If using a name that cannot be entered in the in game menus, there is a config file (APConfig.toml) in the game
directory. When using this, all connection information must be entered in the file.
directory. When using this, all connection information must be entered in the file.
4. Select the `Connect to Archipelago` button
5. Navigate to save file selection
6. Start a new game
* If you're already connected, deleting a save will not disconnect you and is completely safe.
* If you're already connected, deleting an existing save will not disconnect you and is completely safe.
## Continuing a MultiWorld Game

View File

@@ -1,4 +1,5 @@
from dataclasses import dataclass
from datetime import date
from typing import Dict
from schema import And, Optional, Or, Schema
@@ -88,7 +89,7 @@ class ShuffleTransitions(Choice):
class Goal(Choice):
"""Requirement to finish the game."""
"""Requirement to finish the game. To win with the power seal hunt goal, you must enter the Music Box through the shop chest."""
display_name = "Goal"
option_open_music_box = 0
option_power_seal_hunt = 1
@@ -123,6 +124,11 @@ class RequiredSeals(Range):
default = range_end
class Traps(Toggle):
"""Whether traps should be included in the itempool."""
display_name = "Include Traps"
class ShopPrices(Range):
"""Percentage modifier for shuffled item prices in shops"""
display_name = "Shop Prices Modifier"
@@ -199,3 +205,6 @@ class MessengerOptions(DeathLinkMixin, PerGameCommonOptions):
percent_seals_required: RequiredSeals
shop_price: ShopPrices
shop_price_plan: PlannedShopPrices
if date.today() > date(2024, 4, 1):
traps: Traps

View File

@@ -1,3 +1,4 @@
from copy import deepcopy
from typing import List, TYPE_CHECKING
from BaseClasses import CollectionState, PlandoOptions
@@ -18,24 +19,6 @@ PORTALS = [
]
REGION_ORDER = [
"Autumn Hills",
"Forlorn Temple",
"Catacombs",
"Bamboo Creek",
"Howling Grotto",
"Quillshroom Marsh",
"Searing Crags",
"Glacial Peak",
"Tower of Time",
"Cloud Ruins",
"Underworld",
"Riviere Turquoise",
"Elemental Skylands",
"Sunken Shrine",
]
SHOP_POINTS = {
"Autumn Hills": [
"Climbing Claws",
@@ -204,30 +187,48 @@ CHECKPOINTS = {
}
REGION_ORDER = [
"Autumn Hills",
"Forlorn Temple",
"Catacombs",
"Bamboo Creek",
"Howling Grotto",
"Quillshroom Marsh",
"Searing Crags",
"Glacial Peak",
"Tower of Time",
"Cloud Ruins",
"Underworld",
"Riviere Turquoise",
"Elemental Skylands",
"Sunken Shrine",
]
def shuffle_portals(world: "MessengerWorld") -> None:
def create_mapping(in_portal: str, warp: str) -> None:
nonlocal available_portals
"""shuffles the output of the portals from the main hub"""
def create_mapping(in_portal: str, warp: str) -> str:
"""assigns the chosen output to the input"""
parent = out_to_parent[warp]
exit_string = f"{parent.strip(' ')} - "
if "Portal" in warp:
exit_string += "Portal"
world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}00"))
elif warp_point in SHOP_POINTS[parent]:
exit_string += f"{warp_point} Shop"
world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}1{SHOP_POINTS[parent].index(warp_point)}"))
elif warp in SHOP_POINTS[parent]:
exit_string += f"{warp} Shop"
world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}1{SHOP_POINTS[parent].index(warp)}"))
else:
exit_string += f"{warp_point} Checkpoint"
world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}2{CHECKPOINTS[parent].index(warp_point)}"))
exit_string += f"{warp} Checkpoint"
world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}2{CHECKPOINTS[parent].index(warp)}"))
world.spoiler_portal_mapping[in_portal] = exit_string
connect_portal(world, in_portal, exit_string)
available_portals.remove(warp)
if shuffle_type < ShufflePortals.option_anywhere:
available_portals = [port for port in available_portals if port not in shop_points[parent]]
return parent
def handle_planned_portals(plando_connections: List[PlandoConnection]) -> None:
"""checks the provided plando connections for portals and connects them"""
for connection in plando_connections:
if connection.entrance not in PORTALS:
continue
@@ -236,22 +237,28 @@ def shuffle_portals(world: "MessengerWorld") -> None:
world.plando_portals.append(connection.entrance)
shuffle_type = world.options.shuffle_portals
shop_points = SHOP_POINTS.copy()
shop_points = deepcopy(SHOP_POINTS)
for portal in PORTALS:
shop_points[portal].append(f"{portal} Portal")
if shuffle_type > ShufflePortals.option_shops:
shop_points.update(CHECKPOINTS)
for area, points in CHECKPOINTS.items():
shop_points[area] += points
out_to_parent = {checkpoint: parent for parent, checkpoints in shop_points.items() for checkpoint in checkpoints}
available_portals = [val for zone in shop_points.values() for val in zone]
world.random.shuffle(available_portals)
plando = world.multiworld.plando_connections[world.player]
if plando and world.multiworld.plando_options & PlandoOptions.connections:
handle_planned_portals(plando)
world.multiworld.plando_connections[world.player] = [connection for connection in plando
if connection.entrance not in PORTALS]
for portal in PORTALS:
warp_point = world.random.choice(available_portals)
create_mapping(portal, warp_point)
if portal in world.plando_portals:
continue
warp_point = available_portals.pop()
parent = create_mapping(portal, warp_point)
if shuffle_type < ShufflePortals.option_anywhere:
available_portals = [port for port in available_portals if port not in shop_points[parent]]
world.random.shuffle(available_portals)
def connect_portal(world: "MessengerWorld", portal: str, out_region: str) -> None:

View File

@@ -16,7 +16,7 @@ guide : [Guide de configuration de base de Multiworld](/tutorial/Archipelago/se
### Où puis-je obtenir un fichier YAML ?
Vous pouvez personnaliser vos paramètres Minecraft en allant sur la [page des paramètres de joueur](/games/Minecraft/player-settings)
Vous pouvez personnaliser vos paramètres Minecraft en allant sur la [page des paramètres de joueur](/games/Minecraft/player-options)
## Rejoindre une partie MultiWorld
@@ -71,4 +71,4 @@ les liens suivants sont les versions des logiciels que nous utilisons.
- [Page des versions du mod Minecraft Archipelago Randomizer] (https://github.com/KonoTyran/Minecraft_AP_Randomizer/releases)
- **NE PAS INSTALLER CECI SUR VOTRE CLIENT**
- [Amazon Corretto](https://docs.aws.amazon.com/corretto/)
- choisissez la version correspondante et sélectionnez "Téléchargements" sur la gauche
- choisissez la version correspondante et sélectionnez "Téléchargements" sur la gauche

View File

@@ -103,8 +103,6 @@ shuffle_structures:
off: 0
```
För mer detaljer om vad varje inställning gör, kolla standardinställningen `PlayerSettings.yaml` som kommer med
Archipelago-installationen.
## Gå med i ett Multivärld-spel

View File

@@ -2,7 +2,7 @@
## Enlaces rápidos
- [Página Principal](../../../../games/Muse%20Dash/info/en)
- [Página de Configuraciones](../../../../games/Muse%20Dash/player-settings)
- [Página de Configuraciones](../../../../games/Muse%20Dash/player-options)
## Software Requerido
@@ -27,7 +27,7 @@
Si todo fue instalado correctamente, un botón aparecerá en la parte inferior derecha del juego una vez abierto, que te permitirá conectarte al servidor de Archipelago.
## Generar un juego MultiWorld
1. Entra a la página de [configuraciones de jugador](/games/Muse%20Dash/player-settings) y configura las opciones del juego a tu gusto.
1. Entra a la página de [configuraciones de jugador](/games/Muse%20Dash/player-options) y configura las opciones del juego a tu gusto.
2. Genera tu archivo YAML y úsalo para generar un juego nuevo en el radomizer
- (Instrucciones sobre como generar un juego en Archipelago disponibles en la [guía web de Archipelago en Inglés](/tutorial/Archipelago/setup/en))

View File

@@ -46,7 +46,7 @@ guide : [Guide de configuration de base de Multiworld](/tutorial/Archipelago/set
### Où puis-je obtenir un fichier de configuration (.yaml) ?
La page Paramètres du lecteur sur le site Web vous permet de configurer vos paramètres personnels et d'exporter un fichier de configuration depuis eux. Page des paramètres du joueur : [Page des paramètres du joueur d'Ocarina of Time](/games/Ocarina%20of%20Time/player-settings)
La page Paramètres du lecteur sur le site Web vous permet de configurer vos paramètres personnels et d'exporter un fichier de configuration depuis eux. Page des paramètres du joueur : [Page des paramètres du joueur d'Ocarina of Time](/games/Ocarina%20of%20Time/player-options)
### Vérification de votre fichier de configuration
@@ -67,4 +67,4 @@ Une fois le client et l'émulateur démarrés, vous devez les connecter. Accéde
Pour connecter le client au multiserveur, mettez simplement `<adresse>:<port>` dans le champ de texte en haut et appuyez sur Entrée (si le serveur utilise un mot de passe, tapez dans le champ de texte inférieur `/connect <adresse>:<port> [mot de passe]`)
Vous êtes maintenant prêt à commencer votre aventure dans Hyrule.
Vous êtes maintenant prêt à commencer votre aventure dans Hyrule.

View File

@@ -300,6 +300,7 @@ class PokemonEmeraldWorld(World):
# Locations which are directly unlocked by defeating Norman.
exclude_locations([
"Petalburg Gym - Leader Norman",
"Petalburg Gym - Balance Badge",
"Petalburg Gym - TM42 from Norman",
"Petalburg City - HM03 from Wally's Uncle",
@@ -568,14 +569,6 @@ class PokemonEmeraldWorld(World):
self.modified_misc_pokemon = copy.deepcopy(emerald_data.misc_pokemon)
self.modified_starters = copy.deepcopy(emerald_data.starters)
randomize_abilities(self)
randomize_learnsets(self)
randomize_tm_hm_compatibility(self)
randomize_legendary_encounters(self)
randomize_misc_pokemon(self)
randomize_opponent_parties(self)
randomize_starters(self)
# Modify catch rate
min_catch_rate = min(self.options.min_catch_rate.value, 255)
for species in self.modified_species.values():
@@ -590,6 +583,14 @@ class PokemonEmeraldWorld(World):
new_moves.add(new_move)
self.modified_tmhm_moves[i] = new_move
randomize_abilities(self)
randomize_learnsets(self)
randomize_tm_hm_compatibility(self)
randomize_legendary_encounters(self)
randomize_misc_pokemon(self)
randomize_opponent_parties(self)
randomize_starters(self)
create_patch(self, output_directory)
del self.modified_trainers

View File

@@ -664,8 +664,10 @@ class PokemonEmeraldClient(BizHawkClient):
"cmd": "SetNotify",
"keys": [f"pokemon_wonder_trades_{ctx.team}"],
}, {
"cmd": "Get",
"keys": [f"pokemon_wonder_trades_{ctx.team}"],
"cmd": "Set",
"key": f"pokemon_wonder_trades_{ctx.team}",
"default": {"_lock": 0},
"operations": [{"operation": "default", "value": None}] # value is ignored
}]))
elif cmd == "SetReply":
if args.get("key", "") == f"pokemon_wonder_trades_{ctx.team}":

View File

@@ -646,7 +646,7 @@ def _init() -> None:
("SPECIES_CHIKORITA", "Chikorita", 152),
("SPECIES_BAYLEEF", "Bayleef", 153),
("SPECIES_MEGANIUM", "Meganium", 154),
("SPECIES_CYNDAQUIL", "Cindaquil", 155),
("SPECIES_CYNDAQUIL", "Cyndaquil", 155),
("SPECIES_QUILAVA", "Quilava", 156),
("SPECIES_TYPHLOSION", "Typhlosion", 157),
("SPECIES_TOTODILE", "Totodile", 158),

View File

@@ -1784,7 +1784,7 @@
"tags": ["BerryTree"]
},
"BERRY_TREE_65": {
"label": "Route 123 - Berry Master Berry Tree 9",
"label": "Route 123 - Berry Tree Berry Master 9",
"tags": ["BerryTree"]
},
"BERRY_TREE_66": {
@@ -2497,7 +2497,7 @@
"tags": ["Pokedex"]
},
"POKEDEX_REWARD_155": {
"label": "Pokedex - Cindaquil",
"label": "Pokedex - Cyndaquil",
"tags": ["Pokedex"]
},
"POKEDEX_REWARD_156": {

View File

@@ -80,7 +80,7 @@ def randomize_opponent_parties(world: "PokemonEmeraldWorld") -> None:
per_species_tmhm_moves[new_species.species_id] = sorted({
world.modified_tmhm_moves[i]
for i, is_compatible in enumerate(int_to_bool_array(new_species.tm_hm_compatibility))
if is_compatible
if is_compatible and world.modified_tmhm_moves[i] not in world.blacklisted_moves
})
# TMs and HMs compatible with the species

View File

@@ -51,7 +51,7 @@ opciones.
### ¿Dónde puedo obtener un archivo YAML?
Puedes generar un archivo YAML or descargar su plantilla en la [página de configuración de jugador de Pokémon Red and Blue](/games/Pokemon%20Red%20and%20Blue/player-settings)
Puedes generar un archivo YAML or descargar su plantilla en la [página de configuración de jugador de Pokémon Red and Blue](/games/Pokemon%20Red%20and%20Blue/player-options)
Es importante tener en cuenta que la opción `game_version` determina el ROM que será parcheado.
Tanto el jugador como la persona que genera (si está generando localmente) necesitarán el archivo del ROM

View File

@@ -661,11 +661,11 @@ item_table = {
description=RESOURCE_EFFICIENCY_DESCRIPTION_TEMPLATE.format("HERC")),
ItemNames.HERC_JUGGERNAUT_PLATING:
ItemData(285 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 16, SC2Race.TERRAN,
parent_item=ItemNames.WARHOUND, origin={"ext"},
parent_item=ItemNames.HERC, origin={"ext"},
description="Increases HERC armor by 2."),
ItemNames.HERC_KINETIC_FOAM:
ItemData(286 + SC2WOL_ITEM_ID_OFFSET, "Armory 6", 17, SC2Race.TERRAN,
parent_item=ItemNames.WARHOUND, origin={"ext"},
parent_item=ItemNames.HERC, origin={"ext"},
description="Increases HERC life by 50."),
ItemNames.HELLION_TWIN_LINKED_FLAMETHROWER:

View File

@@ -1620,7 +1620,7 @@ def get_locations(world: Optional[World]) -> Tuple[LocationData, ...]:
plando_locations = get_plando_locations(world)
exclude_locations = get_option_value(world, "exclude_locations")
location_table = [location for location in location_table
if (LocationType is LocationType.VICTORY or location.name not in exclude_locations)
if (location.type is LocationType.VICTORY or location.name not in exclude_locations)
and location.type not in excluded_location_types
or location.name in plando_locations]
for i, location_data in enumerate(location_table):

View File

@@ -26,5 +26,4 @@ To achieve the Help Everyone goal, the following characters will need to be help
## Can I have more than one save at a time?
No, unfortunately only one save slot is available for use in A Short Hike.
Starting a new save will erase the old one _permanently_.
You can have up to 3 saves at a time. To switch between them, use the Save Data button in the options menu.

View File

@@ -14,12 +14,13 @@
## Installation
1. Download the [Modding Tools](https://github.com/BrandenEK/AShortHike.ModdingTools/releases), and follow
the [installation instructions](https://github.com/BrandenEK/AShortHike.ModdingTools#a-short-hike-modding-tools) on the GitHub page.
1. Open the [Modding Tools GitHub page](https://github.com/BrandenEK/AShortHike.ModdingTools/), and follow
the installation instructions. After this step, your `A Short Hike/` folder should have an empty `Modding/` subfolder.
2. After the Modding Tools have been installed, download the
[Randomizer](https://github.com/BrandenEK/AShortHike.Randomizer/releases) and extract the contents of it
into the `Modding` folder.
[Randomizer](https://github.com/BrandenEK/AShortHike.Randomizer/releases) zip, extract it, and move the contents
of the `Randomizer/` folder into your `Modding/` folder. After this step, your `Modding/` folder should have
`data/` and `plugins/` subfolders.
## Connecting
@@ -29,4 +30,4 @@ Enter in the Server Port, Name, and Password (optional) in the popup menu that a
## Tracking
Install PopTracker from the link above and place the PopTracker pack into the packs folder.
Connect to Archipelago via the AP button in the top left.
Connect to Archipelago via the AP button in the top left.

View File

@@ -1,5 +1,6 @@
import typing
from Options import Option, DefaultOnToggle, Range, Toggle, DeathLink, Choice
from dataclasses import dataclass
from Options import DefaultOnToggle, Range, Toggle, DeathLink, Choice, PerGameCommonOptions, OptionSet
from .Items import action_item_table
class EnableCoinStars(DefaultOnToggle):
@@ -114,35 +115,37 @@ class StrictMoveRequirements(DefaultOnToggle):
if Move Randomization is enabled"""
display_name = "Strict Move Requirements"
def getMoveRandomizerOption(action: str):
class MoveRandomizerOption(Toggle):
"""Mario is unable to perform this action until a corresponding item is picked up.
This option is incompatible with builds using a 'nomoverando' branch."""
display_name = f"Randomize {action}"
return MoveRandomizerOption
class EnableMoveRandomizer(Toggle):
"""Mario is unable to perform some actions until a corresponding item is picked up.
This option is incompatible with builds using a 'nomoverando' branch.
Specific actions to randomize can be specified in the YAML."""
display_name = "Enable Move Randomizer"
class MoveRandomizerActions(OptionSet):
"""Which actions to randomize when Move Randomizer is enabled"""
display_name = "Randomized Moves"
# HACK: Disable randomization for double jump
valid_keys = [action for action in action_item_table if action != 'Double Jump']
default = valid_keys
sm64_options: typing.Dict[str, type(Option)] = {
"AreaRandomizer": AreaRandomizer,
"BuddyChecks": BuddyChecks,
"ExclamationBoxes": ExclamationBoxes,
"ProgressiveKeys": ProgressiveKeys,
"EnableCoinStars": EnableCoinStars,
"StrictCapRequirements": StrictCapRequirements,
"StrictCannonRequirements": StrictCannonRequirements,
"StrictMoveRequirements": StrictMoveRequirements,
"AmountOfStars": AmountOfStars,
"FirstBowserStarDoorCost": FirstBowserStarDoorCost,
"BasementStarDoorCost": BasementStarDoorCost,
"SecondFloorStarDoorCost": SecondFloorStarDoorCost,
"MIPS1Cost": MIPS1Cost,
"MIPS2Cost": MIPS2Cost,
"StarsToFinish": StarsToFinish,
"death_link": DeathLink,
"CompletionType": CompletionType,
}
for action in action_item_table:
# HACK: Disable randomization of double jump
if action == 'Double Jump': continue
sm64_options[f"MoveRandomizer{action.replace(' ','')}"] = getMoveRandomizerOption(action)
@dataclass
class SM64Options(PerGameCommonOptions):
area_rando: AreaRandomizer
buddy_checks: BuddyChecks
exclamation_boxes: ExclamationBoxes
progressive_keys: ProgressiveKeys
enable_coin_stars: EnableCoinStars
enable_move_rando: EnableMoveRandomizer
move_rando_actions: MoveRandomizerActions
strict_cap_requirements: StrictCapRequirements
strict_cannon_requirements: StrictCannonRequirements
strict_move_requirements: StrictMoveRequirements
amount_of_stars: AmountOfStars
first_bowser_star_door_cost: FirstBowserStarDoorCost
basement_star_door_cost: BasementStarDoorCost
second_floor_star_door_cost: SecondFloorStarDoorCost
mips1_cost: MIPS1Cost
mips2_cost: MIPS2Cost
stars_to_finish: StarsToFinish
death_link: DeathLink
completion_type: CompletionType

View File

@@ -2,6 +2,7 @@ import typing
from enum import Enum
from BaseClasses import MultiWorld, Region, Entrance, Location
from .Options import SM64Options
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, \
@@ -78,7 +79,7 @@ sm64_secrets_to_level = {secret: level for (level,secret) in sm64_level_to_secre
sm64_entrances_to_level = {**sm64_paintings_to_level, **sm64_secrets_to_level }
sm64_level_to_entrances = {**sm64_level_to_paintings, **sm64_level_to_secrets }
def create_regions(world: MultiWorld, player: int):
def create_regions(world: MultiWorld, options: SM64Options, player: int):
regSS = Region("Menu", player, world, "Castle Area")
create_default_locs(regSS, locSS_table)
world.regions.append(regSS)
@@ -88,7 +89,7 @@ def create_regions(world: MultiWorld, player: int):
"BoB: Mario Wings to the Sky", "BoB: Behind Chain Chomp's Gate", "BoB: Bob-omb Buddy")
bob_island = create_subregion(regBoB, "BoB: Island", "BoB: Shoot to the Island in the Sky", "BoB: Find the 8 Red Coins")
regBoB.subregions = [bob_island]
if (world.EnableCoinStars[player].value):
if options.enable_coin_stars:
create_locs(regBoB, "BoB: 100 Coins")
regWhomp = create_region("Whomp's Fortress", player, world)
@@ -96,7 +97,7 @@ def create_regions(world: MultiWorld, player: int):
"WF: Fall onto the Caged Island", "WF: Blast Away the Wall")
wf_tower = create_subregion(regWhomp, "WF: Tower", "WF: To the Top of the Fortress", "WF: Bob-omb Buddy")
regWhomp.subregions = [wf_tower]
if (world.EnableCoinStars[player].value):
if options.enable_coin_stars:
create_locs(regWhomp, "WF: 100 Coins")
regJRB = create_region("Jolly Roger Bay", player, world)
@@ -104,12 +105,12 @@ def create_regions(world: MultiWorld, player: int):
"JRB: Blast to the Stone Pillar", "JRB: Through the Jet Stream", "JRB: Bob-omb Buddy")
jrb_upper = create_subregion(regJRB, 'JRB: Upper', "JRB: Red Coins on the Ship Afloat")
regJRB.subregions = [jrb_upper]
if (world.EnableCoinStars[player].value):
if options.enable_coin_stars:
create_locs(jrb_upper, "JRB: 100 Coins")
regCCM = create_region("Cool, Cool Mountain", player, world)
create_default_locs(regCCM, locCCM_table)
if (world.EnableCoinStars[player].value):
if options.enable_coin_stars:
create_locs(regCCM, "CCM: 100 Coins")
regBBH = create_region("Big Boo's Haunt", player, world)
@@ -118,7 +119,7 @@ def create_regions(world: MultiWorld, player: int):
bbh_third_floor = create_subregion(regBBH, "BBH: Third Floor", "BBH: Eye to Eye in the Secret Room")
bbh_roof = create_subregion(bbh_third_floor, "BBH: Roof", "BBH: Big Boo's Balcony", "BBH: 1Up Block Top of Mansion")
regBBH.subregions = [bbh_third_floor, bbh_roof]
if (world.EnableCoinStars[player].value):
if options.enable_coin_stars:
create_locs(regBBH, "BBH: 100 Coins")
regPSS = create_region("The Princess's Secret Slide", player, world)
@@ -141,7 +142,7 @@ def create_regions(world: MultiWorld, player: int):
hmc_red_coin_area = create_subregion(regHMC, "HMC: Red Coin Area", "HMC: Elevate for 8 Red Coins")
hmc_pit_islands = create_subregion(regHMC, "HMC: Pit Islands", "HMC: A-Maze-Ing Emergency Exit", "HMC: 1Up Block above Pit")
regHMC.subregions = [hmc_red_coin_area, hmc_pit_islands]
if (world.EnableCoinStars[player].value):
if options.enable_coin_stars:
create_locs(hmc_red_coin_area, "HMC: 100 Coins")
regLLL = create_region("Lethal Lava Land", player, world)
@@ -149,7 +150,7 @@ def create_regions(world: MultiWorld, player: int):
"LLL: 8-Coin Puzzle with 15 Pieces", "LLL: Red-Hot Log Rolling")
lll_upper_volcano = create_subregion(regLLL, "LLL: Upper Volcano", "LLL: Hot-Foot-It into the Volcano", "LLL: Elevator Tour in the Volcano")
regLLL.subregions = [lll_upper_volcano]
if (world.EnableCoinStars[player].value):
if options.enable_coin_stars:
create_locs(regLLL, "LLL: 100 Coins")
regSSL = create_region("Shifting Sand Land", player, world)
@@ -159,7 +160,7 @@ def create_regions(world: MultiWorld, player: int):
ssl_upper_pyramid = create_subregion(regSSL, "SSL: Upper Pyramid", "SSL: Inside the Ancient Pyramid",
"SSL: Stand Tall on the Four Pillars", "SSL: Pyramid Puzzle")
regSSL.subregions = [ssl_upper_pyramid]
if (world.EnableCoinStars[player].value):
if options.enable_coin_stars:
create_locs(regSSL, "SSL: 100 Coins")
regDDD = create_region("Dire, Dire Docks", player, world)
@@ -167,7 +168,7 @@ def create_regions(world: MultiWorld, player: int):
"DDD: The Manta Ray's Reward", "DDD: Collect the Caps...")
ddd_moving_poles = create_subregion(regDDD, "DDD: Moving Poles", "DDD: Pole-Jumping for Red Coins")
regDDD.subregions = [ddd_moving_poles]
if (world.EnableCoinStars[player].value):
if options.enable_coin_stars:
create_locs(ddd_moving_poles, "DDD: 100 Coins")
regCotMC = create_region("Cavern of the Metal Cap", player, world)
@@ -184,7 +185,7 @@ def create_regions(world: MultiWorld, player: int):
regSL = create_region("Snowman's Land", player, world)
create_default_locs(regSL, locSL_table)
if (world.EnableCoinStars[player].value):
if options.enable_coin_stars:
create_locs(regSL, "SL: 100 Coins")
regWDW = create_region("Wet-Dry World", player, world)
@@ -193,7 +194,7 @@ def create_regions(world: MultiWorld, player: int):
"WDW: Secrets in the Shallows & Sky", "WDW: Bob-omb Buddy")
wdw_downtown = create_subregion(regWDW, "WDW: Downtown", "WDW: Go to Town for Red Coins", "WDW: Quick Race Through Downtown!", "WDW: 1Up Block in Downtown")
regWDW.subregions = [wdw_top, wdw_downtown]
if (world.EnableCoinStars[player].value):
if options.enable_coin_stars:
create_locs(wdw_top, "WDW: 100 Coins")
regTTM = create_region("Tall, Tall Mountain", player, world)
@@ -202,7 +203,7 @@ def create_regions(world: MultiWorld, player: int):
ttm_top = create_subregion(ttm_middle, "TTM: Top", "TTM: Scale the Mountain", "TTM: Mystery of the Monkey Cage",
"TTM: Mysterious Mountainside", "TTM: Breathtaking View from Bridge")
regTTM.subregions = [ttm_middle, ttm_top]
if (world.EnableCoinStars[player].value):
if options.enable_coin_stars:
create_locs(ttm_top, "TTM: 100 Coins")
create_region("Tiny-Huge Island (Huge)", player, world)
@@ -214,7 +215,7 @@ def create_regions(world: MultiWorld, player: int):
"THI: 1Up Block THI Large near Start", "THI: 1Up Block Windy Area")
thi_large_top = create_subregion(thi_pipes, "THI: Large Top", "THI: Make Wiggler Squirm")
regTHI.subregions = [thi_pipes, thi_large_top]
if (world.EnableCoinStars[player].value):
if options.enable_coin_stars:
create_locs(thi_large_top, "THI: 100 Coins")
regFloor3 = create_region("Third Floor", player, world)
@@ -225,7 +226,7 @@ def create_regions(world: MultiWorld, player: int):
ttc_upper = create_subregion(ttc_lower, "TTC: Upper", "TTC: Timed Jumps on Moving Bars", "TTC: The Pit and the Pendulums")
ttc_top = create_subregion(ttc_upper, "TTC: Top", "TTC: Stomp on the Thwomp", "TTC: 1Up Block at the Top")
regTTC.subregions = [ttc_lower, ttc_upper, ttc_top]
if (world.EnableCoinStars[player].value):
if options.enable_coin_stars:
create_locs(ttc_top, "TTC: 100 Coins")
regRR = create_region("Rainbow Ride", player, world)
@@ -235,7 +236,7 @@ def create_regions(world: MultiWorld, player: int):
rr_cruiser = create_subregion(regRR, "RR: Cruiser", "RR: Cruiser Crossing the Rainbow", "RR: Somewhere Over the Rainbow")
rr_house = create_subregion(regRR, "RR: House", "RR: The Big House in the Sky", "RR: 1Up Block On House in the Sky")
regRR.subregions = [rr_maze, rr_cruiser, rr_house]
if (world.EnableCoinStars[player].value):
if options.enable_coin_stars:
create_locs(rr_maze, "RR: 100 Coins")
regWMotR = create_region("Wing Mario over the Rainbow", player, world)

View File

@@ -3,6 +3,7 @@ from typing import Callable, Union, Dict, Set
from BaseClasses import MultiWorld
from ..generic.Rules import add_rule, set_rule
from .Locations import location_table
from .Options import SM64Options
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 .Items import action_item_table
@@ -24,7 +25,7 @@ def fix_reg(entrance_map: Dict[SM64Levels, str], entrance: SM64Levels, invalid_r
swapdict[entrance], swapdict[rand_entrance] = rand_region, old_dest
swapdict.pop(entrance)
def set_rules(world, player: int, area_connections: dict, star_costs: dict, move_rando_bitvec: int):
def set_rules(world, options: SM64Options, player: int, area_connections: dict, star_costs: dict, move_rando_bitvec: int):
randomized_level_to_paintings = sm64_level_to_paintings.copy()
randomized_level_to_secrets = sm64_level_to_secrets.copy()
valid_move_randomizer_start_courses = [
@@ -32,19 +33,19 @@ def set_rules(world, player: int, area_connections: dict, star_costs: dict, move
"Big Boo's Haunt", "Lethal Lava Land", "Shifting Sand Land",
"Dire, Dire Docks", "Snowman's Land"
] # Excluding WF, HMC, WDW, TTM, THI, TTC, and RR
if world.AreaRandomizer[player].value >= 1: # Some randomization is happening, randomize Courses
if options.area_rando >= 1: # Some randomization is happening, randomize Courses
randomized_level_to_paintings = shuffle_dict_keys(world,sm64_level_to_paintings)
# If not shuffling later, ensure a valid start course on move randomizer
if world.AreaRandomizer[player].value < 3 and move_rando_bitvec > 0:
if options.area_rando < 3 and move_rando_bitvec > 0:
swapdict = randomized_level_to_paintings.copy()
invalid_start_courses = {course for course in randomized_level_to_paintings.values() if course not in valid_move_randomizer_start_courses}
fix_reg(randomized_level_to_paintings, SM64Levels.BOB_OMB_BATTLEFIELD, invalid_start_courses, swapdict, world)
fix_reg(randomized_level_to_paintings, SM64Levels.WHOMPS_FORTRESS, invalid_start_courses, swapdict, world)
if world.AreaRandomizer[player].value == 2: # Randomize Secrets as well
if options.area_rando == 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}
if world.AreaRandomizer[player].value == 3: # Randomize Courses and Secrets in one pool
if options.area_rando == 3: # Randomize Courses and Secrets in one pool
randomized_entrances = shuffle_dict_keys(world, randomized_entrances)
# Guarantee first entrance is a course
swapdict = randomized_entrances.copy()
@@ -67,7 +68,7 @@ def set_rules(world, player: int, area_connections: dict, star_costs: dict, move
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()}
rf = RuleFactory(world, player, move_rando_bitvec)
rf = RuleFactory(world, options, player, move_rando_bitvec)
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))
@@ -199,7 +200,7 @@ def set_rules(world, player: int, area_connections: dict, star_costs: dict, move
# Bowser in the Sky
rf.assign_rule("BitS: Top", "CL+TJ | CL+SF+LG | MOVELESS & TJ+WK+LG")
# 100 Coin Stars
if world.EnableCoinStars[player]:
if options.enable_coin_stars:
rf.assign_rule("BoB: 100 Coins", "CANN & WC | CANNLESS & WC & TJ")
rf.assign_rule("WF: 100 Coins", "GP | MOVELESS")
rf.assign_rule("JRB: 100 Coins", "GP & {JRB: Upper}")
@@ -225,9 +226,9 @@ def set_rules(world, player: int, area_connections: dict, star_costs: dict, move
world.completion_condition[player] = lambda state: state.can_reach("BitS: Top", 'Region', player)
if world.CompletionType[player] == "last_bowser_stage":
if options.completion_type == "last_bowser_stage":
world.completion_condition[player] = lambda state: state.can_reach("BitS: Top", 'Region', player)
elif world.CompletionType[player] == "all_bowser_stages":
elif options.completion_type == "all_bowser_stages":
world.completion_condition[player] = lambda state: state.can_reach("Bowser in the Dark World", 'Region', player) and \
state.can_reach("BitFS: Upper", 'Region', player) and \
state.can_reach("BitS: Top", 'Region', player)
@@ -262,14 +263,14 @@ class RuleFactory:
class SM64LogicException(Exception):
pass
def __init__(self, world, player, move_rando_bitvec):
def __init__(self, world, options: SM64Options, player: int, move_rando_bitvec: int):
self.world = world
self.player = player
self.move_rando_bitvec = move_rando_bitvec
self.area_randomizer = world.AreaRandomizer[player].value > 0
self.capless = not world.StrictCapRequirements[player]
self.cannonless = not world.StrictCannonRequirements[player]
self.moveless = not world.StrictMoveRequirements[player] or not move_rando_bitvec > 0
self.area_randomizer = options.area_rando > 0
self.capless = not options.strict_cap_requirements
self.cannonless = not options.strict_cannon_requirements
self.moveless = not options.strict_move_requirements or not move_rando_bitvec > 0
def assign_rule(self, target_name: str, rule_expr: str):
target = self.world.get_location(target_name, self.player) if target_name in location_table else self.world.get_entrance(target_name, self.player)

View File

@@ -3,7 +3,7 @@ import os
import json
from .Items import item_table, action_item_table, cannon_item_table, SM64Item
from .Locations import location_table, SM64Location
from .Options import sm64_options
from .Options import SM64Options
from .Rules import set_rules
from .Regions import create_regions, sm64_level_to_entrances, SM64Levels
from BaseClasses import Item, Tutorial, ItemClassification, Region
@@ -40,7 +40,7 @@ class SM64World(World):
area_connections: typing.Dict[int, int]
option_definitions = sm64_options
options_dataclass = SM64Options
number_of_stars: int
move_rando_bitvec: int
@@ -49,38 +49,36 @@ class SM64World(World):
def generate_early(self):
max_stars = 120
if (not self.multiworld.EnableCoinStars[self.player].value):
if (not self.options.enable_coin_stars):
max_stars -= 15
self.move_rando_bitvec = 0
for action, itemid in action_item_table.items():
# HACK: Disable randomization of double jump
if action == 'Double Jump': continue
if getattr(self.multiworld, f"MoveRandomizer{action.replace(' ','')}")[self.player].value:
if self.options.enable_move_rando:
for action in self.options.move_rando_actions.value:
max_stars -= 1
self.move_rando_bitvec |= (1 << (itemid - action_item_table['Double Jump']))
if (self.multiworld.ExclamationBoxes[self.player].value > 0):
self.move_rando_bitvec |= (1 << (action_item_table[action] - action_item_table['Double Jump']))
if (self.options.exclamation_boxes > 0):
max_stars += 29
self.number_of_stars = min(self.multiworld.AmountOfStars[self.player].value, max_stars)
self.number_of_stars = min(self.options.amount_of_stars, max_stars)
self.filler_count = max_stars - self.number_of_stars
self.star_costs = {
'FirstBowserDoorCost': round(self.multiworld.FirstBowserStarDoorCost[self.player].value * self.number_of_stars / 100),
'BasementDoorCost': round(self.multiworld.BasementStarDoorCost[self.player].value * self.number_of_stars / 100),
'SecondFloorDoorCost': round(self.multiworld.SecondFloorStarDoorCost[self.player].value * self.number_of_stars / 100),
'MIPS1Cost': round(self.multiworld.MIPS1Cost[self.player].value * self.number_of_stars / 100),
'MIPS2Cost': round(self.multiworld.MIPS2Cost[self.player].value * self.number_of_stars / 100),
'StarsToFinish': round(self.multiworld.StarsToFinish[self.player].value * self.number_of_stars / 100)
'FirstBowserDoorCost': round(self.options.first_bowser_star_door_cost * self.number_of_stars / 100),
'BasementDoorCost': round(self.options.basement_star_door_cost * self.number_of_stars / 100),
'SecondFloorDoorCost': round(self.options.second_floor_star_door_cost * self.number_of_stars / 100),
'MIPS1Cost': round(self.options.mips1_cost * self.number_of_stars / 100),
'MIPS2Cost': round(self.options.mips2_cost * self.number_of_stars / 100),
'StarsToFinish': round(self.options.stars_to_finish * self.number_of_stars / 100)
}
# Nudge MIPS 1 to match vanilla on default percentage
if self.number_of_stars == 120 and self.multiworld.MIPS1Cost[self.player].value == 12:
if self.number_of_stars == 120 and self.options.mips1_cost == 12:
self.star_costs['MIPS1Cost'] = 15
self.topology_present = self.multiworld.AreaRandomizer[self.player].value
self.topology_present = self.options.area_rando
def create_regions(self):
create_regions(self.multiworld, self.player)
create_regions(self.multiworld, self.options, self.player)
def set_rules(self):
self.area_connections = {}
set_rules(self.multiworld, self.player, self.area_connections, self.star_costs, self.move_rando_bitvec)
set_rules(self.multiworld, self.options, self.player, self.area_connections, self.star_costs, self.move_rando_bitvec)
if self.topology_present:
# Write area_connections to spoiler log
for entrance, destination in self.area_connections.items():
@@ -107,7 +105,7 @@ class SM64World(World):
# Power Stars
self.multiworld.itempool += [self.create_item("Power Star") for i in range(0,self.number_of_stars)]
# Keys
if (not self.multiworld.ProgressiveKeys[self.player].value):
if (not self.options.progressive_keys):
key1 = self.create_item("Basement Key")
key2 = self.create_item("Second Floor Key")
self.multiworld.itempool += [key1, key2]
@@ -116,7 +114,7 @@ class SM64World(World):
# Caps
self.multiworld.itempool += [self.create_item(cap_name) for cap_name in ["Wing Cap", "Metal Cap", "Vanish Cap"]]
# Cannons
if (self.multiworld.BuddyChecks[self.player].value):
if (self.options.buddy_checks):
self.multiworld.itempool += [self.create_item(name) for name, id in cannon_item_table.items()]
# Moves
self.multiworld.itempool += [self.create_item(action)
@@ -124,7 +122,7 @@ class SM64World(World):
if self.move_rando_bitvec & (1 << itemid - action_item_table['Double Jump'])]
def generate_basic(self):
if not (self.multiworld.BuddyChecks[self.player].value):
if not (self.options.buddy_checks):
self.multiworld.get_location("BoB: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock BoB"))
self.multiworld.get_location("WF: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock WF"))
self.multiworld.get_location("JRB: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock JRB"))
@@ -136,7 +134,7 @@ class SM64World(World):
self.multiworld.get_location("THI: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock THI"))
self.multiworld.get_location("RR: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock RR"))
if (self.multiworld.ExclamationBoxes[self.player].value == 0):
if (self.options.exclamation_boxes == 0):
self.multiworld.get_location("CCM: 1Up Block Near Snowman", self.player).place_locked_item(self.create_item("1Up Mushroom"))
self.multiworld.get_location("CCM: 1Up Block Ice Pillar", self.player).place_locked_item(self.create_item("1Up Mushroom"))
self.multiworld.get_location("CCM: 1Up Block Secret Slide", self.player).place_locked_item(self.create_item("1Up Mushroom"))
@@ -174,8 +172,8 @@ class SM64World(World):
return {
"AreaRando": self.area_connections,
"MoveRandoVec": self.move_rando_bitvec,
"DeathLink": self.multiworld.death_link[self.player].value,
"CompletionType": self.multiworld.CompletionType[self.player].value,
"DeathLink": self.options.death_link.value,
"CompletionType": self.options.completion_type.value,
**self.star_costs
}

View File

@@ -25,10 +25,16 @@ There are two goals which can be chosen:
## What items and locations get shuffled?
Each unique level exit awards a location check. Optionally, collecting five Dragon Coins in each level can also award a location check.
Each unique level exit awards a location check. Additionally, the following in-level actions can be set to award a location check:
- Collecting Five Dragon Coins
- Collecting 3-Up Moons
- Activating Bonus Blocks
- Receiving Hidden 1-Ups
- Hitting Blocks containing coins or items
Mario's various abilities and powerups as described above are placed into the item pool.
If the player is playing Yoshi Egg Hunt, a certain number of Yoshi Eggs will be placed into the item pool.
Any additional items that are needed to fill out the item pool with be 1-Up Mushrooms.
Any additional items that are needed to fill out the item pool will be 1-Up Mushrooms, bundles of coins, or, if enabled, various trap items.
## Which items can be in another player's world?

View File

@@ -25,7 +25,7 @@ class SwordLocation(Choice):
Randomized - The sword can be placed anywhere.
Early - The sword will be placed in a location accessible from the start of
the game.
Unce assured - The sword will always be placed on Link's Uncle."""
Uncle - The sword will always be placed on Link's Uncle."""
display_name = "Sword Location"
option_Randomized = 0
option_Early = 1
@@ -48,7 +48,7 @@ class MorphLocation(Choice):
class Goal(Choice):
"""This option decides what goal is required to finish the randomizer.
Defeat Ganon and Mother Brain - Find the required crystals and boss tokens kill both bosses.
Defeat Ganon and Mother Brain - Find the required crystals and boss tokens to kill both bosses.
Fast Ganon and Defeat Mother Brain - The hole to ganon is open without having to defeat Agahnim in
Ganon's Tower and Ganon can be defeat as soon you have the required
crystals to make Ganon vulnerable. For keysanity, this mode also removes

View File

@@ -13,7 +13,7 @@ from Utils import output_path
from worlds.AutoWorld import WebWorld, World
from worlds.generic.Rules import add_item_rule, set_rule
from .logic import SoEPlayerLogic
from .options import Difficulty, EnergyCore, SoEOptions
from .options import Difficulty, EnergyCore, Sniffamizer, SniffIngredients, SoEOptions
from .patch import SoEDeltaPatch, get_base_rom_path
if typing.TYPE_CHECKING:
@@ -64,20 +64,28 @@ _id_offset: typing.Dict[int, int] = {
pyevermizer.CHECK_BOSS: _id_base + 50, # bosses 64050..6499
pyevermizer.CHECK_GOURD: _id_base + 100, # gourds 64100..64399
pyevermizer.CHECK_NPC: _id_base + 400, # npc 64400..64499
# TODO: sniff 64500..64799
# blank 64500..64799
pyevermizer.CHECK_EXTRA: _id_base + 800, # extra items 64800..64899
pyevermizer.CHECK_TRAP: _id_base + 900, # trap 64900..64999
pyevermizer.CHECK_SNIFF: _id_base + 1000 # sniff 65000..65592
}
# cache native evermizer items and locations
_items = pyevermizer.get_items()
_sniff_items = pyevermizer.get_sniff_items() # optional, not part of the default location pool
_traps = pyevermizer.get_traps()
_extras = pyevermizer.get_extra_items() # items that are not placed by default
_locations = pyevermizer.get_locations()
_sniff_locations = pyevermizer.get_sniff_locations() # optional, not part of the default location pool
# fix up texts for AP
for _loc in _locations:
if _loc.type == pyevermizer.CHECK_GOURD:
_loc.name = f'{_loc.name} #{_loc.index}'
_loc.name = f"{_loc.name} #{_loc.index}"
for _loc in _sniff_locations:
if _loc.type == pyevermizer.CHECK_SNIFF:
_loc.name = f"{_loc.name} Sniff #{_loc.index}"
del _loc
# item helpers
_ingredients = (
'Wax', 'Water', 'Vinegar', 'Root', 'Oil', 'Mushroom', 'Mud Pepper', 'Meteorite', 'Limestone', 'Iron',
@@ -97,7 +105,7 @@ def _match_item_name(item: pyevermizer.Item, substr: str) -> bool:
def _get_location_mapping() -> typing.Tuple[typing.Dict[str, int], typing.Dict[int, pyevermizer.Location]]:
name_to_id = {}
id_to_raw = {}
for loc in _locations:
for loc in itertools.chain(_locations, _sniff_locations):
ap_id = _id_offset[loc.type] + loc.index
id_to_raw[ap_id] = loc
name_to_id[loc.name] = ap_id
@@ -108,7 +116,7 @@ def _get_location_mapping() -> typing.Tuple[typing.Dict[str, int], typing.Dict[i
def _get_item_mapping() -> typing.Tuple[typing.Dict[str, int], typing.Dict[int, pyevermizer.Item]]:
name_to_id = {}
id_to_raw = {}
for item in itertools.chain(_items, _extras, _traps):
for item in itertools.chain(_items, _sniff_items, _extras, _traps):
if item.name in name_to_id:
continue
ap_id = _id_offset[item.type] + item.index
@@ -168,9 +176,9 @@ class SoEWorld(World):
options: SoEOptions
settings: typing.ClassVar[SoESettings]
topology_present = False
data_version = 4
data_version = 5
web = SoEWebWorld()
required_client_version = (0, 3, 5)
required_client_version = (0, 4, 4)
item_name_to_id, item_id_to_raw = _get_item_mapping()
location_name_to_id, location_id_to_raw = _get_location_mapping()
@@ -238,16 +246,26 @@ class SoEWorld(World):
spheres.setdefault(get_sphere_index(loc), {}).setdefault(loc.type, []).append(
SoELocation(self.player, loc.name, self.location_name_to_id[loc.name], ingame,
loc.difficulty > max_difficulty))
# extend pool if feature and setting enabled
if hasattr(Sniffamizer, "option_everywhere") and self.options.sniffamizer == Sniffamizer.option_everywhere:
for loc in _sniff_locations:
spheres.setdefault(get_sphere_index(loc), {}).setdefault(loc.type, []).append(
SoELocation(self.player, loc.name, self.location_name_to_id[loc.name], ingame,
loc.difficulty > max_difficulty))
# location balancing data
trash_fills: typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int, int]]] = {
0: {pyevermizer.CHECK_GOURD: (20, 40, 40, 40)}, # remove up to 40 gourds from sphere 1
1: {pyevermizer.CHECK_GOURD: (70, 90, 90, 90)}, # remove up to 90 gourds from sphere 2
0: {pyevermizer.CHECK_GOURD: (20, 40, 40, 40), # remove up to 40 gourds from sphere 1
pyevermizer.CHECK_SNIFF: (100, 130, 130, 130)}, # remove up to 130 sniff spots from sphere 1
1: {pyevermizer.CHECK_GOURD: (70, 90, 90, 90), # remove up to 90 gourds from sphere 2
pyevermizer.CHECK_SNIFF: (160, 200, 200, 200)}, # remove up to 200 sniff spots from sphere 2
}
# mark some as excluded based on numbers above
for trash_sphere, fills in trash_fills.items():
for typ, counts in fills.items():
if typ not in spheres[trash_sphere]:
continue # e.g. player does not have sniff locations
count = counts[self.options.difficulty.value]
for location in self.random.sample(spheres[trash_sphere][typ], count):
assert location.name != "Energy Core #285", "Error in sphere generation"
@@ -299,6 +317,15 @@ class SoEWorld(World):
# remove one pair of wings that will be placed in generate_basic
items.remove(self.create_item("Wings"))
# extend pool if feature and setting enabled
if hasattr(Sniffamizer, "option_everywhere") and self.options.sniffamizer == Sniffamizer.option_everywhere:
if self.options.sniff_ingredients == SniffIngredients.option_vanilla_ingredients:
# vanilla ingredients
items += list(map(lambda item: self.create_item(item), _sniff_items))
else:
# random ingredients
items += [self.create_item(self.get_filler_item_name()) for _ in _sniff_items]
def is_ingredient(item: pyevermizer.Item) -> bool:
for ingredient in _ingredients:
if _match_item_name(item, ingredient):
@@ -345,7 +372,12 @@ class SoEWorld(World):
set_rule(self.multiworld.get_location('Done', self.player),
lambda state: self.logic.has(state, pyevermizer.P_FINAL_BOSS))
set_rule(self.multiworld.get_entrance('New Game', self.player), lambda state: True)
for loc in _locations:
locations: typing.Iterable[pyevermizer.Location]
if hasattr(Sniffamizer, "option_everywhere") and self.options.sniffamizer == Sniffamizer.option_everywhere:
locations = itertools.chain(_locations, _sniff_locations)
else:
locations = _locations
for loc in locations:
location = self.multiworld.get_location(loc.name, self.player)
set_rule(location, self.make_rule(loc.requires))

View File

@@ -1,4 +1,5 @@
import typing
from itertools import chain
from typing import Callable, Set
from . import pyevermizer
@@ -11,10 +12,12 @@ if typing.TYPE_CHECKING:
# TODO: resolve/flatten/expand rules to get rid of recursion below where possible
# Logic.rules are all rules including locations, excluding those with no progress (i.e. locations that only drop items)
rules = [rule for rule in pyevermizer.get_logic() if len(rule.provides) > 0]
rules = pyevermizer.get_logic()
# Logic.items are all items and extra items excluding non-progression items and duplicates
# NOTE: we are skipping sniff items here because none of them is supposed to provide progression
item_names: Set[str] = set()
items = [item for item in filter(lambda item: item.progression, pyevermizer.get_items() + pyevermizer.get_extra_items())
items = [item for item in filter(lambda item: item.progression, # type: ignore[arg-type]
chain(pyevermizer.get_items(), pyevermizer.get_extra_items()))
if item.name not in item_names and not item_names.add(item.name)] # type: ignore[func-returns-value]

View File

@@ -1,4 +1,5 @@
from dataclasses import dataclass, fields
from datetime import datetime
from typing import Any, ClassVar, cast, Dict, Iterator, List, Tuple, Protocol
from Options import AssembleOptions, Choice, DeathLink, DefaultOnToggle, Option, PerGameCommonOptions, \
@@ -158,13 +159,30 @@ class Ingredienizer(EvermizerFlags, OffOnFullChoice):
flags = ['i', '', 'I']
class Sniffamizer(EvermizerFlags, OffOnFullChoice):
"""On Shuffles, Full randomizes drops in sniff locations"""
class Sniffamizer(EvermizerFlags, Choice):
"""
Off: all vanilla items in sniff spots
Shuffle: sniff items shuffled into random sniff spots
"""
display_name = "Sniffamizer"
option_off = 0
option_shuffle = 1
if datetime.today().year > 2024 or datetime.today().month > 3:
option_everywhere = 2
__doc__ = __doc__ + " Everywhere: add sniff spots to multiworld pool"
alias_true = 1
default = 1
flags = ['s', '', 'S']
class SniffIngredients(EvermizerFlag, Choice):
"""Select which items should be used as sniff items"""
display_name = "Sniff Ingredients"
option_vanilla_ingredients = 0
option_random_ingredients = 1
flag = 'v'
class Callbeadamizer(EvermizerFlags, OffOnFullChoice):
"""On Shuffles call bead characters, Full shuffles individual spells"""
display_name = "Callbeadamizer"
@@ -207,7 +225,7 @@ class ItemChanceMeta(AssembleOptions):
attrs["display_name"] = f"{attrs['item_name']} Chance"
attrs["range_start"] = 0
attrs["range_end"] = 100
cls = super(ItemChanceMeta, mcs).__new__(mcs, name, bases, attrs)
cls = super(ItemChanceMeta, mcs).__new__(mcs, name, bases, attrs) # type: ignore[no-untyped-call]
return cast(ItemChanceMeta, cls)
@@ -268,6 +286,7 @@ class SoEOptions(PerGameCommonOptions):
short_boss_rush: ShortBossRush
ingredienizer: Ingredienizer
sniffamizer: Sniffamizer
sniff_ingredients: SniffIngredients
callbeadamizer: Callbeadamizer
musicmizer: Musicmizer
doggomizer: Doggomizer

View File

@@ -1,36 +1,36 @@
pyevermizer==0.46.1 \
--hash=sha256:9fd71b5e4af26a5dd24a9cbf5320bf0111eef80320613401a1c03011b1515806 \
--hash=sha256:23f553ed0509d9a238b2832f775e0b5abd7741b38ab60d388294ee8a7b96c5fb \
--hash=sha256:7189b67766418a3e7e6c683f09c5e758aa1a5c24316dd9b714984bac099c4b75 \
--hash=sha256:befa930711e63d5d5892f67fd888b2e65e746363e74599c53e71ecefb90ae16a \
--hash=sha256:202933ce21e0f33859537bf3800d9a626c70262a9490962e3f450171758507ca \
--hash=sha256:c20ca69311c696528e1122ebc7d33775ee971f538c0e3e05dd3bfd4de10b82d4 \
--hash=sha256:74dc689a771ae5ffcd5257e763f571ee890e3e87bdb208233b7f451522c00d66 \
--hash=sha256:072296baef464daeb6304cf58827dcbae441ad0803039aee1c0caa10d56e0674 \
--hash=sha256:7921baf20d52d92d6aeb674125963c335b61abb7e1298bde4baf069d11a2d05e \
--hash=sha256:ca098034a84007038c2bff004582e6e6ac2fa9cc8b9251301d25d7e2adcee6da \
--hash=sha256:22ddb29823c19be9b15e1b3627db1babfe08b486aede7d5cc463a0a1ae4c75d8 \
--hash=sha256:bf1c441b49026d9000166be6e2f63fc351a3fda170aa3fdf18d44d5e5d044640 \
--hash=sha256:9710aa7957b4b1f14392006237eb95803acf27897377df3e85395f057f4316b9 \
--hash=sha256:8feb676c198bee17ab991ee015828345ac3f87c27dfdb3061d92d1fe47c184b4 \
--hash=sha256:597026dede72178ff3627a4eb3315de8444461c7f0f856f5773993c3f9790c53 \
--hash=sha256:70f9b964bdfb5191e8f264644c5d1af3041c66fe15261df8a99b3d719dc680d6 \
--hash=sha256:74655c0353ffb6cda30485091d0917ce703b128cd824b612b3110a85c79a93d0 \
--hash=sha256:0e9c74d105d4ec3af12404e85bb8776931c043657add19f798ee69465f92b999 \
--hash=sha256:d3c13446d3d482b9cce61ac73b38effd26fcdcf7f693a405868d3aaaa4d18ca6 \
--hash=sha256:371ac3360640ef439a5920ddfe11a34e9d2e546ed886bb8c9ed312611f9f4655 \
--hash=sha256:6e5cf63b036f24d2ae4375a88df8d0bc93208352939521d1fcac3c829ef2c363 \
--hash=sha256:edf28f5c4d1950d17343adf6d8d40d12c7e982d1e39535d55f7915e122cd8b0e \
--hash=sha256:b5ef6f3b4e04f677c296f60f7f4c320ac22cd5bc09c05574460116c8641c801a \
--hash=sha256:dd651f66720af4abe2ddae29944e299a57ff91e6fca1739e6dc1f8fd7a8c2b39 \
--hash=sha256:4e278f5f72c27f9703bce5514d2fead8c00361caac03e94b0bf9ad8a144f1eeb \
--hash=sha256:38f36ea1f545b835c3ecd6e081685a233ac2e3cf0eec8916adc92e4d791098a6 \
--hash=sha256:0a2e58ed6e7c42f006cc17d32cec1f432f01b3fe490e24d71471b36e0d0d8742 \
--hash=sha256:c1b658db76240596c03571c60635abe953f36fb55b363202971831c2872ea9a0 \
--hash=sha256:deb5a84a6a56325eb6701336cdbf70f72adaaeab33cbe953d0e551ecf2592f20 \
--hash=sha256:b1425c793e0825f58b3726e7afebaf5a296c07cb0d28580d0ee93dbe10dcdf63 \
--hash=sha256:11995fb4dfd14b5c359591baee2a864c5814650ba0084524d4ea0466edfaf029 \
--hash=sha256:5d2120b5c93ae322fe2a85d48e3eab4168a19e974a880908f1ac291c0300940f \
--hash=sha256:254912ea4bfaaffb0abe366e73bd9ecde622677d6afaf2ce8a0c330df99fefd9 \
--hash=sha256:540d8e4525f0b5255c1554b4589089dc58e15df22f343e9545ea00f7012efa07 \
--hash=sha256:f69b8ebded7eed181fabe30deabae89fd10c41964f38abb26b19664bbe55c1ae
pyevermizer==0.48.0 \
--hash=sha256:069ce348e480e04fd6208cfd0f789c600b18d7c34b5272375b95823be191ed57 \
--hash=sha256:58164dddaba2f340b0a8b4f39605e9dac46d8b0ffb16120e2e57bef2bfc1d683 \
--hash=sha256:115dd09d38a10f11d4629b340dfd75e2ba4089a1ff9e9748a11619829e02c876 \
--hash=sha256:b5e79cfe721e75cd7dec306b5eecd6385ce059e31ef7523ba7f677e22161ec6f \
--hash=sha256:382882fa9d641b9969a6c3ed89449a814bdabcb6b17b558872d95008a6cc908b \
--hash=sha256:92f67700e9132064a90858d391dd0b8fb111aff6dfd472befed57772d89ae567 \
--hash=sha256:fe4c453b7dbd5aa834b81f9a7aedb949a605455650b938b8b304d8e5a7edcbf7 \
--hash=sha256:c6bdbc45daf73818f763ed59ad079f16494593395d806f772dd62605c722b3e9 \
--hash=sha256:bb09f45448fdfd28566ae6fcc38c35a6632f4c31a9de2483848f6ce17b2359b5 \
--hash=sha256:00a8b9014744bd1528d0d39c33ede7c0d1713ad797a331cebb33d377a5bc1064 \
--hash=sha256:64ee69edc0a7d3b3caded78f2e46975f9beaff1ff8feaf29b87da44c45f38d7d \
--hash=sha256:9211bdb1313e9f4869ed5bdc61f3831d39679bd08bb4087f1c1e5475d9e3018b \
--hash=sha256:4a57821e422a1d75fe3307931a78db7a65e76955f8e401c4b347db6570390d09 \
--hash=sha256:04670cee0a0b913f24d2b9a1e771781560e2485bda31e6cd372a08421cf85cfa \
--hash=sha256:971fe77d0a20a1db984020ad253b613d0983f5e23ff22cba60ee5ac00d8128de \
--hash=sha256:127265fdb49f718f54706bf15604af1cec23590afd00d423089dea4331dcfc61 \
--hash=sha256:d47576360337c1a23f424cd49944a8d68fc4f3338e00719c9f89972c84604bef \
--hash=sha256:879659603e51130a0de8d9885d815a2fa1df8bd6cebe6d520d1c6002302adfdb \
--hash=sha256:6a91bfc53dd130db6424adf8ac97a1133e97b4157ed00f889d8cbd26a2a4b340 \
--hash=sha256:f3bf35fc5eef4cda49d2de77339fc201dd3206660a3dc15db005625b15bb806c \
--hash=sha256:e7c8d5bf59a3c16db20411bc5d8e9c9087a30b6b4edf1b5ed9f4c013291427e4 \
--hash=sha256:054a4d84ffe75448d41e88e1e0642ef719eb6111be5fe608e71e27a558c59069 \
--hash=sha256:e6f141ca367469c69ba7fbf65836c479ec6672c598cfcb6b39e8098c60d346bc \
--hash=sha256:6e65eb88f0c1ff4acde1c13b24ce649b0fe3d1d3916d02d96836c781a5022571 \
--hash=sha256:e61e8f476b6da809cf38912755ed8bb009665f589e913eb8df877e9fa763024b \
--hash=sha256:7e7c5484c0a2e3da6064de3f73d8d988d6703db58ab0be4730cbbf1a82319237 \
--hash=sha256:9033b954e5f4878fd94af6d2056c78e3316115521fb1c24a4416d5cbf2ad66ad \
--hash=sha256:824c623fff8ae4da176306c458ad63ad16a06a495a16db700665eca3c115924f \
--hash=sha256:8e31031409a8386c6a63b79d480393481badb3ba29f32ff7a0db2b4abed20ac8 \
--hash=sha256:7dbb7bb13e1e94f69f7ccdbcf4d35776424555fce5af1ca29d0256f91fdf087a \
--hash=sha256:3a24e331b259407b6912d6e0738aa8a675831db3b7493fcf54dc17cb0cb80d37 \
--hash=sha256:fdda06662a994271e96633cba100dd92b2fcd524acef8b2f664d1aaa14503cbd \
--hash=sha256:0f0fc81bef3dbb78ba6a7622dd4296f23c59825968a0bb0448beb16eb3397cc2 \
--hash=sha256:e07cbef776a7468669211546887357cc88e9afcf1578b23a4a4f2480517b15d9 \
--hash=sha256:e442212695bdf60e455673b7b9dd83a5d4b830d714376477093d2c9054d92832

View File

@@ -1,9 +1,11 @@
from test.bases import WorldTestBase
from typing import Iterable
from .. import SoEWorld
class SoETestBase(WorldTestBase):
game = "Secret of Evermore"
world: SoEWorld
def assertLocationReachability(self, reachable: Iterable[str] = (), unreachable: Iterable[str] = (),
satisfied: bool = True) -> None:

View File

@@ -0,0 +1,130 @@
import typing
from unittest import TestCase, skipUnless
from . import SoETestBase
from .. import pyevermizer
from ..options import Sniffamizer
class TestCount(TestCase):
"""
Test that counts line up for sniff spots
"""
def test_compare_counts(self) -> None:
self.assertEqual(len(pyevermizer.get_sniff_locations()), len(pyevermizer.get_sniff_items()),
"Sniff locations and sniff items don't line up")
class Bases:
# class in class to avoid running tests for helper class
class TestSniffamizerLocal(SoETestBase):
"""
Test that provided options do not add sniff items or locations
"""
def test_no_sniff_items(self) -> None:
self.assertLess(len(self.multiworld.itempool), 500,
"Unexpected number of items")
for item in self.multiworld.itempool:
if item.code is not None:
self.assertLess(item.code, 65000,
"Unexpected item type")
def test_no_sniff_locations(self) -> None:
location_count = sum(1 for location in self.multiworld.get_locations(self.player) if location.item is None)
self.assertLess(location_count, 500,
"Unexpected number of locations")
for location in self.multiworld.get_locations(self.player):
if location.address is not None:
self.assertLess(location.address, 65000,
"Unexpected location type")
self.assertEqual(location_count, len(self.multiworld.itempool),
"Locations and item counts do not line up")
class TestSniffamizerPool(SoETestBase):
"""
Test that provided options add sniff items and locations
"""
def test_sniff_items(self) -> None:
self.assertGreater(len(self.multiworld.itempool), 500,
"Unexpected number of items")
def test_sniff_locations(self) -> None:
location_count = sum(1 for location in self.multiworld.get_locations(self.player) if location.item is None)
self.assertGreater(location_count, 500,
"Unexpected number of locations")
self.assertTrue(any(location.address is not None and location.address >= 65000
for location in self.multiworld.get_locations(self.player)),
"No sniff locations")
self.assertEqual(location_count, len(self.multiworld.itempool),
"Locations and item counts do not line up")
class TestSniffamizerShuffle(Bases.TestSniffamizerLocal):
"""
Test that shuffle does not add extra items or locations
"""
options: typing.Dict[str, typing.Any] = {
"sniffamizer": "shuffle"
}
def test_flags(self) -> None:
# default -> no flags
flags = self.world.options.flags
self.assertNotIn("s", flags)
self.assertNotIn("S", flags)
self.assertNotIn("v", flags)
@skipUnless(hasattr(Sniffamizer, "option_everywhere"), "Feature disabled")
class TestSniffamizerEverywhereVanilla(Bases.TestSniffamizerPool):
"""
Test that everywhere + vanilla ingredients does add extra items and locations
"""
options: typing.Dict[str, typing.Any] = {
"sniffamizer": "everywhere",
"sniff_ingredients": "vanilla_ingredients",
}
def test_flags(self) -> None:
flags = self.world.options.flags
self.assertIn("S", flags)
self.assertNotIn("v", flags)
@skipUnless(hasattr(Sniffamizer, "option_everywhere"), "Feature disabled")
class TestSniffamizerEverywhereRandom(Bases.TestSniffamizerPool):
"""
Test that everywhere + random ingredients also adds extra items and locations
"""
options: typing.Dict[str, typing.Any] = {
"sniffamizer": "everywhere",
"sniff_ingredients": "random_ingredients",
}
def test_flags(self) -> None:
flags = self.world.options.flags
self.assertIn("S", flags)
self.assertIn("v", flags)
@skipUnless(hasattr(Sniffamizer, "option_everywhere"), "Feature disabled")
class EverywhereAccessTest(SoETestBase):
"""
Test that everywhere has certain rules
"""
options: typing.Dict[str, typing.Any] = {
"sniffamizer": "everywhere",
}
@staticmethod
def _resolve_numbers(spots: typing.Mapping[str, typing.Iterable[int]]) -> typing.List[str]:
return [f"{name} #{number}" for name, numbers in spots.items() for number in numbers]
def test_knight_basher(self) -> None:
locations = ["Mungola", "Lightning Storm"] + self._resolve_numbers({
"Gomi's Tower Sniff": range(473, 491),
"Gomi's Tower": range(195, 199),
})
items = [["Knight Basher"]]
self.assertAccessDependency(locations, items)

View File

@@ -13,9 +13,9 @@ from ..strings.villager_names import NPC, ModNPC
class Villager:
name: str
bachelor: bool
locations: Tuple[str]
locations: Tuple[str, ...]
birthday: str
gifts: Tuple[str]
gifts: Tuple[str, ...]
available: bool
mod_name: str
@@ -366,10 +366,11 @@ def villager(name: str, bachelor: bool, locations: Tuple[str, ...], birthday: st
return npc
def make_bachelor(mod_name: str, npc: Villager):
def adapt_wizard_to_sve(mod_name: str, npc: Villager):
if npc.mod_name:
mod_name = npc.mod_name
return Villager(npc.name, True, npc.locations, npc.birthday, npc.gifts, npc.available, mod_name)
# The wizard leaves his tower on sunday, for like 1 hour... Good enough to meet him!
return Villager(npc.name, True, npc.locations + forest, npc.birthday, npc.gifts, npc.available, mod_name)
def register_villager_modification(mod_name: str, npc: Villager, modification_function):
@@ -452,7 +453,7 @@ morris = villager(ModNPC.morris, False, jojamart, Season.spring, universal_loves
# Modified villagers; not included in all villagers
register_villager_modification(ModNames.sve, wizard, make_bachelor)
register_villager_modification(ModNames.sve, wizard, adapt_wizard_to_sve)
all_villagers_by_name: Dict[str, Villager] = {villager.name: villager for villager in all_villagers}
all_villagers_by_mod: Dict[str, List[Villager]] = {}

View File

@@ -81,22 +81,17 @@ For the locations which do not include a normal reward, Resource Packs and traps
A player can enable some options that will add some items to the pool that are relevant to progression
- Seasons Randomizer:
* All 4 seasons will be items, and one of them will be selected randomly and be added to the player's start inventory.
* At the end of each month, the player can choose the next season, instead of following the vanilla season order. On
Seasons Randomizer, they can only choose from the seasons they have received.
- All 4 seasons will be items, and one of them will be selected randomly and be added to the player's start inventory.
- At the end of each month, the player can choose the next season, instead of following the vanilla season order. On Seasons Randomizer, they can only choose from the seasons they have received.
- Cropsanity:
* Every single seed in the game starts off locked and cannot be purchased from any merchant. Their unlocks are received
as multiworld items. Growing each seed and harvesting the resulting crop sends a location check
* The way merchants sell seeds is considerably changed. Pierre sells fewer seeds at a high price, while Joja sells
unlimited seeds but in huge discount packs, not individually.
- Every single seed in the game starts off locked and cannot be purchased from any merchant. Their unlocks are received as multiworld items. Growing each seed and harvesting the resulting crop sends a location check
- The way merchants sell seeds is considerably changed. Pierre sells fewer seeds at a high price, while Joja sells unlimited seeds but in huge discount packs, not individually.
- Museumsanity:
* The items that are normally obtained from museum donation milestones are added to the item pool. Some items, like the
magic rock candy, are duplicated for convenience.
* The Traveling Merchant now sells artifacts and minerals, with a bias towards undonated ones, to mitigate randomness.
She will sell these items as the player receives "Traveling Merchant Metal Detector" items.
- The items that are normally obtained from museum donation milestones are added to the item pool. Some items, like the magic rock candy, are duplicated for convenience.
- The Traveling Merchant now sells artifacts and minerals, with a bias towards undonated ones, to mitigate randomness. She will sell these items as the player receives "Traveling Merchant Metal Detector" items.
- TV Channels
- Babies
* Only if Friendsanity is enabled
- Only if Friendsanity is enabled
There are a few extra vanilla items, which are added to the pool for convenience, but do not have a matching location. These include
- [Wizard Buildings](https://stardewvalleywiki.com/Wizard%27s_Tower#Buildings)
@@ -135,32 +130,32 @@ for these mods, the specifics will vary from mod to mod
List of supported mods:
- General
* [Stardew Valley Expanded](https://www.nexusmods.com/stardewvalley/mods/3753)
* [DeepWoods](https://www.nexusmods.com/stardewvalley/mods/2571)
* [Skull Cavern Elevator](https://www.nexusmods.com/stardewvalley/mods/963)
* [Bigger Backpack](https://www.nexusmods.com/stardewvalley/mods/1845)
* [Tractor Mod](https://www.nexusmods.com/stardewvalley/mods/1401)
* [Distant Lands - Witch Swamp Overhaul](https://www.nexusmods.com/stardewvalley/mods/18109)
- [Stardew Valley Expanded](https://www.nexusmods.com/stardewvalley/mods/3753)
- [DeepWoods](https://www.nexusmods.com/stardewvalley/mods/2571)
- [Skull Cavern Elevator](https://www.nexusmods.com/stardewvalley/mods/963)
- [Bigger Backpack](https://www.nexusmods.com/stardewvalley/mods/1845)
- [Tractor Mod](https://www.nexusmods.com/stardewvalley/mods/1401)
- [Distant Lands - Witch Swamp Overhaul](https://www.nexusmods.com/stardewvalley/mods/18109)
- Skills
* [Magic](https://www.nexusmods.com/stardewvalley/mods/2007)
* [Luck Skill](https://www.nexusmods.com/stardewvalley/mods/521)
* [Socializing Skill](https://www.nexusmods.com/stardewvalley/mods/14142)
* [Archaeology](https://www.nexusmods.com/stardewvalley/mods/15793)
* [Cooking Skill](https://www.nexusmods.com/stardewvalley/mods/522)
* [Binning Skill](https://www.nexusmods.com/stardewvalley/mods/14073)
- [Magic](https://www.nexusmods.com/stardewvalley/mods/2007)
- [Luck Skill](https://www.nexusmods.com/stardewvalley/mods/521)
- [Socializing Skill](https://www.nexusmods.com/stardewvalley/mods/14142)
- [Archaeology](https://www.nexusmods.com/stardewvalley/mods/15793)
- [Cooking Skill](https://www.nexusmods.com/stardewvalley/mods/522)
- [Binning Skill](https://www.nexusmods.com/stardewvalley/mods/14073)
- NPCs
* [Ayeisha - The Postal Worker (Custom NPC)](https://www.nexusmods.com/stardewvalley/mods/6427)
* [Mister Ginger (cat npc)](https://www.nexusmods.com/stardewvalley/mods/5295)
* [Juna - Roommate NPC](https://www.nexusmods.com/stardewvalley/mods/8606)
* [Professor Jasper Thomas](https://www.nexusmods.com/stardewvalley/mods/5599)
* [Alec Revisited](https://www.nexusmods.com/stardewvalley/mods/10697)
* [Custom NPC - Yoba](https://www.nexusmods.com/stardewvalley/mods/14871)
* [Custom NPC Eugene](https://www.nexusmods.com/stardewvalley/mods/9222)
* ['Prophet' Wellwick](https://www.nexusmods.com/stardewvalley/mods/6462)
* [Shiko - New Custom NPC](https://www.nexusmods.com/stardewvalley/mods/3732)
* [Delores - Custom NPC](https://www.nexusmods.com/stardewvalley/mods/5510)
* [Custom NPC - Riley](https://www.nexusmods.com/stardewvalley/mods/5811)
* [Alecto the Witch](https://www.nexusmods.com/stardewvalley/mods/10671)
- [Ayeisha - The Postal Worker (Custom NPC)](https://www.nexusmods.com/stardewvalley/mods/6427)
- [Mister Ginger (cat npc)](https://www.nexusmods.com/stardewvalley/mods/5295)
- [Juna - Roommate NPC](https://www.nexusmods.com/stardewvalley/mods/8606)
- [Professor Jasper Thomas](https://www.nexusmods.com/stardewvalley/mods/5599)
- [Alec Revisited](https://www.nexusmods.com/stardewvalley/mods/10697)
- [Custom NPC - Yoba](https://www.nexusmods.com/stardewvalley/mods/14871)
- [Custom NPC Eugene](https://www.nexusmods.com/stardewvalley/mods/9222)
- ['Prophet' Wellwick](https://www.nexusmods.com/stardewvalley/mods/6462)
- [Shiko - New Custom NPC](https://www.nexusmods.com/stardewvalley/mods/3732)
- [Delores - Custom NPC](https://www.nexusmods.com/stardewvalley/mods/5510)
- [Custom NPC - Riley](https://www.nexusmods.com/stardewvalley/mods/5811)
- [Alecto the Witch](https://www.nexusmods.com/stardewvalley/mods/10671)
Some of these mods might need a patch mod to tie the randomizer with the mod. These can be found
[here](https://github.com/Witchybun/SDV-Randomizer-Content-Patcher/releases)

View File

@@ -3,7 +3,11 @@
## Required Software
- Stardew Valley on PC (Recommended: [Steam version](https://store.steampowered.com/app/413150/Stardew_Valley/))
- SMAPI ([Mod loader for Stardew Valley](https://smapi.io/))
- You need version 1.5.6. It is available in a public beta branch on Steam ![image](https://i.imgur.com/uKAUmF0.png).
- If your Stardew is not on Steam, you are responsible for finding a way to downgrade it.
- This measure is temporary. We are working hard to bring the mod to Stardew 1.6 as soon as possible.
- SMAPI 3.x.x ([Mod loader for Stardew Valley](https://www.nexusmods.com/stardewvalley/mods/2400?tab=files))
- Same as Stardew Valley itself, SMAPI needs a slightly older version to be compatible with Stardew Valley 1.5.6 ![image](https://i.imgur.com/kzgObHy.png)
- [StardewArchipelago Mod Release 5.x.x](https://github.com/agilbert1412/StardewArchipelago/releases)
- It is important to use a mod release of version 5.x.x to play seeds that have been generated here. Later releases
can only be used with later releases of the world generator, that are not hosted on archipelago.gg yet.
@@ -34,11 +38,10 @@ You can customize your options by visiting the [Stardew Valley Player Options Pa
### Installing the mod
- Install [SMAPI](https://smapi.io/) by following the instructions on their website
- Install [SMAPI version 3.x.x](https://www.nexusmods.com/stardewvalley/mods/2400?tab=files) by following the instructions on the mod page
- Download and extract the [StardewArchipelago](https://github.com/agilbert1412/StardewArchipelago/releases) mod into
your Stardew Valley "Mods" folder
- *OPTIONAL*: If you want to launch your game through Steam, add the following to your Stardew Valley launch options:
- "[PATH TO STARDEW VALLEY]\Stardew Valley\StardewModdingAPI.exe" %command%
- *OPTIONAL*: If you want to launch your game through Steam, add the following to your Stardew Valley launch options: `"[PATH TO STARDEW VALLEY]\Stardew Valley\StardewModdingAPI.exe" %command%`
- Otherwise just launch "StardewModdingAPI.exe" in your installation folder directly
- Stardew Valley should launch itself alongside a console which allows you to read mod information and interact with some of them.

View File

@@ -73,7 +73,8 @@ class FishingLogic(BaseLogic[Union[FishingLogicMixin, ReceivedLogicMixin, Region
return rod_rule & self.logic.skill.has_level(Skill.fishing, 4)
if fish_quality == FishQuality.iridium:
return rod_rule & self.logic.skill.has_level(Skill.fishing, 10)
return False_()
raise ValueError(f"Quality {fish_quality} is unknown.")
def can_catch_every_fish(self) -> StardewRule:
rules = [self.has_max_fishing()]

View File

@@ -546,6 +546,7 @@ class StardewLogic(ReceivedLogicMixin, HasLogicMixin, RegionLogicMixin, BuffLogi
def can_succeed_grange_display(self) -> StardewRule:
if self.options.festival_locations != FestivalLocations.option_hard:
return True_()
animal_rule = self.animal.has_animal(Generic.any)
artisan_rule = self.artisan.can_keg(Generic.any) | self.artisan.can_preserves_jar(Generic.any)
cooking_rule = self.money.can_spend_at(Region.saloon, 220) # Salads at the bar are good enough

View File

@@ -44,10 +44,14 @@ CombatLogicMixin, CropLogicMixin, MagicLogicMixin]]):
tool_material = ToolMaterial.tiers[tool_level]
months = max(1, level - 1)
months_rule = self.logic.time.has_lived_months(months)
previous_level_rule = self.logic.skill.has_level(skill, level - 1)
if self.options.skill_progression != options.SkillProgression.option_vanilla:
previous_level_rule = self.logic.skill.has_level(skill, level - 1)
else:
previous_level_rule = True_()
if skill == Skill.fishing:
xp_rule = self.logic.tool.has_tool(Tool.fishing_rod, ToolMaterial.tiers[max(tool_level, 3)])
xp_rule = self.logic.tool.has_fishing_rod(max(tool_level, 1))
elif skill == Skill.farming:
xp_rule = self.logic.tool.has_tool(Tool.hoe, tool_material) & self.logic.tool.can_water(tool_level)
elif skill == Skill.foraging:
@@ -137,13 +141,17 @@ CombatLogicMixin, CropLogicMixin, MagicLogicMixin]]):
def can_fish(self, regions: Union[str, Tuple[str, ...]] = None, difficulty: int = 0) -> StardewRule:
if isinstance(regions, str):
regions = regions,
if regions is None or len(regions) == 0:
regions = fishing_regions
skill_required = min(10, max(0, int((difficulty / 10) - 1)))
if difficulty <= 40:
skill_required = 0
skill_rule = self.logic.skill.has_level(Skill.fishing, skill_required)
region_rule = self.logic.region.can_reach_any(regions)
# Training rod only works with fish < 50. Fiberglass does not help you to catch higher difficulty fish, so it's skipped in logic.
number_fishing_rod_required = 1 if difficulty < 50 else (2 if difficulty < 80 else 4)
return self.logic.tool.has_fishing_rod(number_fishing_rod_required) & skill_rule & region_rule

View File

@@ -12,10 +12,14 @@ from ..options import ToolProgression
from ..stardew_rule import StardewRule, True_, False_
from ..strings.ap_names.skill_level_names import ModSkillLevel
from ..strings.region_names import Region
from ..strings.skill_names import ModSkill
from ..strings.spells import MagicSpell
from ..strings.tool_names import ToolMaterial, Tool
fishing_rod_prices = {
3: 1800,
4: 7500,
}
tool_materials = {
ToolMaterial.copper: 1,
ToolMaterial.iron: 2,
@@ -40,27 +44,31 @@ class ToolLogicMixin(BaseLogicMixin):
class ToolLogic(BaseLogic[Union[ToolLogicMixin, HasLogicMixin, ReceivedLogicMixin, RegionLogicMixin, SeasonLogicMixin, MoneyLogicMixin, MagicLogicMixin]]):
# Should be cached
def has_tool(self, tool: str, material: str = ToolMaterial.basic) -> StardewRule:
assert tool != Tool.fishing_rod, "Use `has_fishing_rod` instead of `has_tool`."
if material == ToolMaterial.basic or tool == Tool.scythe:
return True_()
if self.options.tool_progression & ToolProgression.option_progressive:
return self.logic.received(f"Progressive {tool}", tool_materials[material])
return self.logic.has(f"{material} Bar") & self.logic.money.can_spend(tool_upgrade_prices[material])
return self.logic.has(f"{material} Bar") & self.logic.money.can_spend_at(Region.blacksmith, tool_upgrade_prices[material])
def can_use_tool_at(self, tool: str, material: str, region: str) -> StardewRule:
return self.has_tool(tool, material) & self.logic.region.can_reach(region)
@cache_self1
def has_fishing_rod(self, level: int) -> StardewRule:
assert 1 <= level <= 4, "Fishing rod 0 isn't real, it can't hurt you. Training is 1, Bamboo is 2, Fiberglass is 3 and Iridium is 4."
if self.options.tool_progression & ToolProgression.option_progressive:
return self.logic.received(f"Progressive {Tool.fishing_rod}", level)
if level <= 1:
if level <= 2:
# We assume you always have access to the Bamboo pole, because mod side there is a builtin way to get it back.
return self.logic.region.can_reach(Region.beach)
prices = {2: 500, 3: 1800, 4: 7500}
level = min(level, 4)
return self.logic.money.can_spend_at(Region.fish_shop, prices[level])
return self.logic.money.can_spend_at(Region.fish_shop, fishing_rod_prices[level])
# Should be cached
def can_forage(self, season: Union[str, Iterable[str]], region: str = Region.forest, need_hoe: bool = False) -> StardewRule:

View File

@@ -197,7 +197,7 @@ class Cropsanity(Choice):
"""Formerly named "Seed Shuffle"
Pierre now sells a random amount of seasonal seeds and Joja sells them without season requirements, but only in huge packs.
Disabled: All the seeds are unlocked from the start, there are no location checks for growing and harvesting crops
Shuffled: Seeds are unlocked as archipelago items, for each seed there is a location check for growing and harvesting that crop
Enabled: Seeds are unlocked as archipelago items, for each seed there is a location check for growing and harvesting that crop
"""
internal_name = "cropsanity"
display_name = "Cropsanity"

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