Compare commits

...

12 Commits

Author SHA1 Message Date
Fabian Dill
069405b217 Core: Bump version from 0.6.5 to 0.6.6 (#5753)
(cherry picked from commit 4477dc7a66)
2026-01-19 21:40:48 +01:00
Berserker
e78d92a4a8 LADX: no pickle 2026-01-19 21:40:36 +01:00
Fabian Dill
ff52318b75 WebHost: increase form upload limit (#5756)
(cherry picked from commit 01e1e1fe11)
2025-12-17 19:11:22 +01:00
Silvris
45994e344e Tests: test that every option in a preset is visible in either simple or complex UI (#5750) 2025-12-16 19:27:02 +01:00
Silvris
51d5e1afae Launcher: fix shortcuts on the AppImage (#5726)
* fix appimage executable reference

* adjust working dir

* use argv0 instead of appimage directly

* set noexe on frozen
2025-12-15 03:30:07 +01:00
Ziktofel
577b958c4d SC2: Fix Kerrigan logic for active spells (#5746) 2025-12-15 00:56:54 +01:00
Benny D
ce38d8ced6 Docs: Add 'silasary' to Mac tutorial contributors (#5745) 2025-12-14 17:01:32 +01:00
BeeFox-sys
d65fcf286d Launcher: Add workaround for kivy bug for linux touchpad devices (#5737)
* add code to fix touchpad on linux, courtesy of Snu of the kivy community

* Launcher: Update workaround to follow styleguide
2025-12-12 02:44:22 +01:00
Phaneros
5a6a0b37d6 sc2: Fixing typos in item descriptions (#5739) 2025-12-11 22:43:06 +01:00
Fabian Dill
4a0a65d604 WebHost: add played game to static tracker (#5731) 2025-12-09 00:45:02 +01:00
Emily
d25abfc305 Docs: update apsudoku docs / add links to web build (#5720) 2025-12-05 01:09:56 +01:00
Duck
0905e3ce32 WebHost/Game Guides: Change links to stay on current instance (#5699)
* Remove absolute links to archipelago.gg

* Fix other link issues
2025-12-02 00:40:05 +01:00
40 changed files with 298 additions and 217 deletions

View File

@@ -218,12 +218,17 @@ def launch(exe, in_terminal=False):
def create_shortcut(button: Any, component: Component) -> None:
from pyshortcuts import make_shortcut
script = sys.argv[0]
wkdir = Utils.local_path()
env = os.environ
if "APPIMAGE" in env:
script = env["ARGV0"]
wkdir = None # defaults to ~ on Linux
else:
script = sys.argv[0]
wkdir = Utils.local_path()
script = f"{script} \"{component.display_name}\""
make_shortcut(script, name=f"Archipelago {component.display_name}", icon=local_path("data", "icon.ico"),
startmenu=False, terminal=False, working_dir=wkdir)
startmenu=False, terminal=False, working_dir=wkdir, noexe=Utils.is_frozen())
button.menu.dismiss()

View File

@@ -48,7 +48,7 @@ class Version(typing.NamedTuple):
return ".".join(str(item) for item in self)
__version__ = "0.6.5"
__version__ = "0.6.6"
version_tuple = tuplize_version(__version__)
is_linux = sys.platform.startswith("linux")

View File

@@ -23,6 +23,17 @@ app.jinja_env.filters['any'] = any
app.jinja_env.filters['all'] = all
app.jinja_env.filters['get_file_safe_name'] = get_file_safe_name
# overwrites of flask default config
app.config["DEBUG"] = False
app.config["PORT"] = 80
app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER
app.config["MAX_CONTENT_LENGTH"] = 64 * 1024 * 1024 # 64 megabyte limit
# if you want to deploy, make sure you have a non-guessable secret key
app.config["SECRET_KEY"] = bytes(socket.gethostname(), encoding="utf-8")
app.config["SESSION_PERMANENT"] = True
app.config["MAX_FORM_MEMORY_SIZE"] = 2 * 1024 * 1024 # 2 MB, needed for large option pages such as SC2
# custom config
app.config["SELFHOST"] = True # application process is in charge of running the websites
app.config["GENERATORS"] = 8 # maximum concurrent world gens
app.config["HOSTERS"] = 8 # maximum concurrent room hosters
@@ -30,19 +41,12 @@ app.config["SELFLAUNCH"] = True # application process is in charge of launching
app.config["SELFLAUNCHCERT"] = None # can point to a SSL Certificate to encrypt Room websocket connections
app.config["SELFLAUNCHKEY"] = None # can point to a SSL Certificate Key to encrypt Room websocket connections
app.config["SELFGEN"] = True # application process is in charge of scheduling Generations.
app.config["DEBUG"] = False
app.config["PORT"] = 80
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAX_CONTENT_LENGTH'] = 64 * 1024 * 1024 # 64 megabyte limit
# if you want to deploy, make sure you have a non-guessable secret key
app.config["SECRET_KEY"] = bytes(socket.gethostname(), encoding="utf-8")
# at what amount of worlds should scheduling be used, instead of rolling in the web-thread
app.config["JOB_THRESHOLD"] = 1
# after what time in seconds should generation be aborted, freeing the queue slot. Can be set to None to disable.
app.config["JOB_TIME"] = 600
# memory limit for generator processes in bytes
app.config["GENERATOR_MEMORY_LIMIT"] = 4294967296
app.config['SESSION_PERMANENT'] = True
# waitress uses one thread for I/O, these are for processing of views that then get sent
# archipelago.gg uses gunicorn + nginx; ignoring this option

View File

@@ -58,6 +58,12 @@ class PlayerLocationsTotal(TypedDict):
total_locations: int
class PlayerGame(TypedDict):
team: int
player: int
game: str
@api_endpoints.route("/tracker/<suuid:tracker>")
@cache.memoize(timeout=60)
def tracker_data(tracker: UUID) -> dict[str, Any]:
@@ -80,7 +86,8 @@ def tracker_data(tracker: UUID) -> dict[str, Any]:
"""Slot aliases of all players."""
for team, players in all_players.items():
for player in players:
player_aliases.append({"team": team, "player": player, "alias": tracker_data.get_player_alias(team, player)})
player_aliases.append(
{"team": team, "player": player, "alias": tracker_data.get_player_alias(team, player)})
player_items_received: list[PlayerItemsReceived] = []
"""Items received by each player."""
@@ -94,7 +101,8 @@ def tracker_data(tracker: UUID) -> dict[str, Any]:
for team, players in all_players.items():
for player in players:
player_checks_done.append(
{"team": team, "player": player, "locations": sorted(tracker_data.get_player_checked_locations(team, player))})
{"team": team, "player": player,
"locations": sorted(tracker_data.get_player_checked_locations(team, player))})
total_checks_done: list[TeamTotalChecks] = [
{"team": team, "checks_done": checks_done}
@@ -144,7 +152,8 @@ def tracker_data(tracker: UUID) -> dict[str, Any]:
"""The current client status for each player."""
for team, players in all_players.items():
for player in players:
player_status.append({"team": team, "player": player, "status": tracker_data.get_player_client_status(team, player)})
player_status.append(
{"team": team, "player": player, "status": tracker_data.get_player_client_status(team, player)})
return {
"aliases": player_aliases,
@@ -207,12 +216,20 @@ def static_tracker_data(tracker: UUID) -> dict[str, Any]:
player_locations_total.append(
{"team": team, "player": player, "total_locations": len(tracker_data.get_player_locations(player))})
player_game: list[PlayerGame] = []
"""The played game per player slot."""
for team, players in all_players.items():
for player in players:
player_game.append({"team": team, "player": player, "game": tracker_data.get_player_game(player)})
return {
"groups": groups,
"datapackage": tracker_data._multidata["datapackage"],
"player_locations_total": player_locations_total,
"player_game": player_game,
}
# It should be exceedingly rare that slot data is needed, so it's separated out.
@api_endpoints.route("/slot_data_tracker/<suuid:tracker>")
@cache.memoize(timeout=300)

View File

@@ -23,7 +23,7 @@ players to rely upon each other to complete their game.
While a multiworld game traditionally requires all players to be playing the same game, a multi-game multiworld allows
players to randomize any of the supported games, and send items between them. This allows players of different
games to interact with one another in a single multiplayer environment. Archipelago supports multi-game multiworlds.
Here is a list of our [Supported Games](https://archipelago.gg/games).
Here is a list of our [Supported Games](/games).
## Can I generate a single-player game with Archipelago?
@@ -33,7 +33,7 @@ play, open the Settings Page, pick your settings, and click Generate Game.
## How do I get started?
We have a [Getting Started](https://archipelago.gg/tutorial/Archipelago/setup/en) guide that will help you get the
We have a [Getting Started](/tutorial/Archipelago/setup/en) guide that will help you get the
software set up. You can use that guide to learn how to generate multiworlds. There are also basic instructions for
including multiple games, and hosting multiworlds on the website for ease and convenience.
@@ -57,7 +57,7 @@ their multiworld.
If a player must leave early, they can use Archipelago's release system. When a player releases their game, all items
in that game belonging to other players are sent out automatically. This allows other players to continue to play
uninterrupted. Here is a list of all of our [Server Commands](https://archipelago.gg/tutorial/Archipelago/commands/en).
uninterrupted. Here is a list of all of our [Server Commands](/tutorial/Archipelago/commands/en).
## What happens if an item is placed somewhere it is impossible to get?

View File

@@ -385,6 +385,8 @@ Will provide a dict of static tracker data with the following keys:
- This hash can then be sent to the datapackage API to receive the appropriate datapackage as necessary
- The number of checks found vs. total checks available per player (`player_locations_total`)
- Same logic as the multitracker template: found = len(player_checks_done.locations) / total = player_locations_total.total_locations (all available checks).
- The game each player is playing (`player_game`)
- Provided as a list of objects with `team`, `player`, and `game`.
Example:
```json
@@ -409,10 +411,10 @@ Example:
],
"datapackage": {
"Archipelago": {
"checksum": "ac9141e9ad0318df2fa27da5f20c50a842afeecb",
"checksum": "ac9141e9ad0318df2fa27da5f20c50a842afeecb"
},
"The Messenger": {
"checksum": "6991cbcda7316b65bcb072667f3ee4c4cae71c0b",
"checksum": "6991cbcda7316b65bcb072667f3ee4c4cae71c0b"
}
},
"player_locations_total": [
@@ -427,6 +429,18 @@ Example:
"total_locations": 20
}
],
"player_game": [
{
"team": 0,
"player": 1,
"game": "Archipelago"
},
{
"team": 0,
"player": 2,
"game": "The Messenger"
}
]
}
```

11
kvui.py
View File

@@ -35,6 +35,17 @@ Config.set("input", "mouse", "mouse,disable_multitouch")
Config.set("kivy", "exit_on_escape", "0")
Config.set("graphics", "multisamples", "0") # multisamples crash old intel drivers
# Workaround for Kivy issue #9226.
# caused by kivy by default using probesysfs,
# which assumes all multi touch deviecs are touch screens.
# workaround provided by Snu of the kivy commmunity c:
from kivy.utils import platform
if platform == "linux":
options = Config.options("input")
for option in options:
if Config.get("input", option) == "probesysfs":
Config.remove_option("input", option)
# Workaround for an issue where importing kivy.core.window before loading sounds
# will hang the whole application on Linux once the first sound is loaded.
# kivymd imports kivy.core.window, so we have to do this before the first kivymd import.

View File

@@ -2,7 +2,7 @@ import unittest
from BaseClasses import PlandoOptions
from worlds import AutoWorldRegister
from Options import OptionCounter, NamedRange, NumericOption, OptionList, OptionSet
from Options import OptionCounter, NamedRange, NumericOption, OptionList, OptionSet, Visibility
class TestOptionPresets(unittest.TestCase):
@@ -19,6 +19,9 @@ class TestOptionPresets(unittest.TestCase):
# pass in all plando options in case a preset wants to require certain plando options
# for some reason
option.verify(world_type, "Test Player", PlandoOptions(sum(PlandoOptions)))
if not (Visibility.complex_ui in option.visibility or Visibility.simple_ui in option.visibility):
self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' is not "
f"visible in any supported UI.")
supported_types = [NumericOption, OptionSet, OptionList, OptionCounter]
if not any([issubclass(option.__class__, t) for t in supported_types]):
self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' "

View File

@@ -2,7 +2,7 @@
## Benötigte Software
- [Archipelago](github.com/ArchipelagoMW/Archipelago/releases/latest)
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest)
- Die [APQuest-apworld](https://github.com/NewSoupVi/Archipelago/releases),
falls diese nicht mit deiner Version von Archipelago gebündelt ist.
@@ -11,16 +11,16 @@
Zuerst brauchst du einen Raum, mit dem du dich verbinden kannst.
Dafür musst du oder jemand den du kennst ein Spiel generieren.
Dieser Schritt wird hier nicht erklärt, aber du kannst den
[Archipelago Setup Guide](https://archipelago.gg/tutorial/Archipelago/setup_en#generating-a-game) lesen.
[Archipelago Setup Guide](/tutorial/Archipelago/setup_en#generating-a-game) lesen.
Du musst außerdem [Archipelago](github.com/ArchipelagoMW/Archipelago/releases/latest) installiert haben
Du musst außerdem [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest) installiert haben
und die [APQuest apworld](https://github.com/NewSoupVi/Archipelago/releases) darin installieren.
Von hier ist es einfach, dich mit deinem Slot zu verbinden.
### Webhost-Raum
Wenn dein Raum auf einem WebHost läuft (z.B. [archipelago.gg](archipelago.gg))
Wenn dein Raum auf einem WebHost läuft (z.B. [archipelago.gg](https://archipelago.gg))
kannst du einfach auf deinen Namen in der Spielerliste klicken.
Dies öffnet den Archipelago Launcher, welcher dich dann fragt,
ob du den Text Client oder den APQuest Client öffnen willst.

View File

@@ -2,7 +2,7 @@
## Required Software
- [Archipelago](github.com/ArchipelagoMW/Archipelago/releases/latest)
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest)
- [The APQuest apworld](https://github.com/NewSoupVi/Archipelago/releases),
if not bundled with your version of Archipelago
@@ -10,16 +10,16 @@
First, you need a room to connect to. For this, you or someone you know has to generate a game.
This will not be explained here,
but you can check the [Archipelago Setup Guide](https://archipelago.gg/tutorial/Archipelago/setup_en#generating-a-game).
but you can check the [Archipelago Setup Guide](/tutorial/Archipelago/setup_en#generating-a-game).
You also need to have [Archipelago](github.com/ArchipelagoMW/Archipelago/releases/latest) installed
You also need to have [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest) installed
and the [The APQuest apworld](https://github.com/NewSoupVi/Archipelago/releases) installed into Archipelago.
From here, connecting to your APQuest slot is easy. There are two scenarios.
### Webhost Room
If your room is hosted on a WebHost (e.g. [archipelago.gg](archipelago.gg)),
If your room is hosted on a WebHost (e.g. [archipelago.gg](https://archipelago.gg)),
you should be able to simply click on your name in the player list.
This will open the Archipelago Launcher
and ask you whether you want to connect with the Text Client or the APQuest Client.

View File

@@ -11,3 +11,5 @@ Play Sudoku puzzles of varying difficulties, earning a hint for each puzzle corr
## Where is the options page?
There is no options page; this game cannot be used in your .yamls. Instead, the client can connect to any slot in a multiworld.
By using the connected room's Admin Password on the Admin Panel tab, you can configure some settings at any time to affect the entire room. This allows disabling hints entirely, as well as altering the hint odds for each difficulty.

View File

@@ -11,7 +11,11 @@ Does not need to be added at the start of a seed, as it does not create any slot
## Installation Procedures
Go to the latest release from the [APSudoku Releases page](https://github.com/APSudoku/APSudoku/releases/latest). Download and extract the appropriate file for your platform.
### Windows / Linux
Go to the latest release from the [github APSudoku Releases page](https://github.com/APSudoku/APSudoku/releases/latest). Download and extract the appropriate file for your platform.
### Web
Go to the [github pages](apsudoku.github.io) or [itch.io](https://emilyv99.itch.io/apsudoku) site, and play in the browser.
## Joining a MultiWorld Game
@@ -35,6 +39,14 @@ Info:
- You can also use the `Tracking` tab to view either a basic tracker or a valid [GodotAP tracker pack](https://github.com/EmilyV99/GodotAP/blob/main/tracker_packs/GET_PACKS.md)
- While connected, the number of "unhinted" locations for your slot is shown in the upper-left of the the `Sudoku` tab. (If this reads 0, no further hints can be earned for this slot, as every locations is already hinted)
- Click the various `?` buttons for information on controls/how to play
## Admin Settings
By using the connected room's Admin Password on the Admin Panel tab, you can configure some settings at any time to affect the entire room.
- You can disable APSudoku for the entire room, preventing any hints from being granted.
- You can customize the reward weights for each difficulty, making progression hints more or less likely, and/or adding a chance to get "no hint" after a solve.
## DeathLink Support
If `DeathLink` is enabled when you click `Connect`:

View File

@@ -19,7 +19,7 @@ The Mod can be installed and played by following these steps (see the [Mod Downl
2. Launch the game, if "OFFLINE" is visible in the upper-right corner of the screen, the Mod is working
### Create a Config (.yaml) File
The purpose of a YAML file is described in the [Basic Multiworld Setup Guide](https://archipelago.gg/tutorial/Archipelago/setup/en#generating-a-game).
The purpose of a YAML file is described in the [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en#generating-a-game).
The [Player Options page](/games/Choo-Choo%20Charles/player-options) allows to configure personal options and export a config YAML file.
@@ -38,7 +38,7 @@ Follow these steps to host a remote multiplayer or a local single-player session
1. Double-click the **cccharles.apworld** to automatically install the world randomization logic
2. Put the **CCCharles.yaml** to **Archipelago/Players/** with the YAML of each player to host
3. Launch the Archipelago launcher and click "Generate" to configure a game with the YAMLs in **Archipelago/output/**
4. For a multiplayer session, go to the [Archipelago HOST GAME page](https://archipelago.gg/uploads)
4. For a multiplayer session, go to the [Archipelago HOST GAME page](/uploads)
5. Click "Upload File" and select the generated **AP_\<seed\>.zip** in **Archipelago/output/**
6. Send the generated room page to each player

View File

@@ -19,7 +19,7 @@ Le Mod peut être installé et joué en suivant les étapes suivantes (voir la s
2. Lancer le jeu, si "OFFLINE" est visible dans le coin en haut à droite de l'écran, le Mod est actif
### Créer un Fichier de Configuration (.yaml)
L'objectif d'un fichier YAML est décrit dans le [Guide d'Installation Basique du Multiworld](https://archipelago.gg/tutorial/Archipelago/setup/en#generating-a-game) (en anglais).
L'objectif d'un fichier YAML est décrit dans le [Guide d'Installation Basique du Multiworld](/tutorial/Archipelago/setup/en#generating-a-game) (en anglais).
La [page d'Options Joueur](/games/Choo-Choo%20Charles/player-options) permet de configurer des options personnelles et exporter un fichier de configuration YAML.
@@ -38,7 +38,7 @@ Suivre ces étapes pour héberger une session multijoueur à distance ou locale
1. Double-cliquer sur **cccharles.apworld** pour installer automatiquement la logique de randomisation du monde
2. Placer le **CCCharles.yaml** dans **Archipelago/Players/** avec le YAML de chaque joueur à héberger
3. Exécuter le lanceur Archipelago et cliquer sur "Generate" pour configurer une partie avec les YAML dans **Archipelago/output/**
4. Pour une session multijoueur, aller à la [page Archipelago HOST GAME](https://archipelago.gg/uploads)
4. Pour une session multijoueur, aller à la [page Archipelago HOST GAME](/uploads)
5. Cliquer sur "Upload File" et sélectionner le **AP_\<seed\>.zip** généré dans **Archipelago/output/**
6. Envoyer la page de la partie générée à chaque joueur

View File

@@ -17,7 +17,7 @@ class GenericWeb(WebWorld):
'A guide detailing the commands available to the user when participating in an Archipelago session.',
'English', 'commands_en.md', 'commands/en', ['jat2980', 'Ijwu'])
mac = Tutorial('Archipelago Setup Guide for Mac', 'A guide detailing how to run Archipelago clients on macOS.',
'English', 'mac_en.md','mac/en', ['Bicoloursnake'])
'English', 'mac_en.md','mac/en', ['Bicoloursnake', 'silasary'])
plando = Tutorial('Archipelago Plando Guide', 'A guide to understanding and using plando for your game.',
'English', 'plando_en.md', 'plando/en', ['alwaysintreble', 'Alchav'])
setup = Tutorial('Getting Started',

View File

@@ -29,7 +29,7 @@ It is generally recommended that you use a virtual environment to run python bas
4. If the patching process needs a rom, but cannot find it, it will ask you to navigate to your legally obtained rom.
5. Your client should now be running and rom created (where applicable).
## Additional Steps for SNES Games
1. If using RetroArch, the instructions to set up your emulator [here in the Link to the Past setup guide](https://archipelago.gg/tutorial/A%20Link%20to%20the%20Past/multiworld/en) also work on the macOS version of RetroArch.
1. If using RetroArch, the instructions to set up your emulator [here in the Link to the Past setup guide](/tutorial/A%20Link%20to%20the%20Past/multiworld/en) also work on the macOS version of RetroArch.
2. Double click on the SNI tar.gz download to extract the files to an SNI directory. If it isn't already, rename this directory to SNI to make some steps easier.
3. Move the SNI directory out of the downloads directory, preferably into the Archipelago directory created earlier.
4. If the SNI directory is correctly named and moved into the Archipelago directory, it should auto run with the SNI client. If it doesn't automatically run, open up the SNI directory and run the SNI executable file manually.

View File

@@ -7,7 +7,7 @@ all_random = {
"game_language": "random",
"goal": "random",
"goal_speed": "random",
"total_heart_stars": "random",
"max_heart_stars": "random",
"heart_stars_required": "random",
"filler_percentage": "random",
"trap_percentage": "random",
@@ -34,7 +34,7 @@ all_random = {
beginner = {
"goal": "zero",
"goal_speed": "normal",
"total_heart_stars": 50,
"max_heart_stars": 50,
"heart_stars_required": 30,
"filler_percentage": 25,
"trap_percentage": 0,

View File

@@ -84,7 +84,7 @@ Enter The room's port number into the top box <b> where the x's are</b> and pres
- To fix this look over the guide at [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/). Specifically the Panacea and Lua Backend Steps.
- Using a seed from the standalone KH2 Randomizer Seed Generator.
- The Archipelago version of the KH2 Randomizer does not use this Seed Generator; refer to the [Archipelago Setup](https://archipelago.gg/tutorial/Archipelago/setup/en) to learn how to generate and play a seed through Archipelago.
- The Archipelago version of the KH2 Randomizer does not use this Seed Generator; refer to the [Archipelago Setup](/tutorial/Archipelago/setup/en) to learn how to generate and play a seed through Archipelago.
## Best Practices

View File

@@ -2,7 +2,6 @@ import binascii
import importlib.util
import importlib.machinery
import random
import pickle
import Utils
from collections import defaultdict
from typing import Dict
@@ -61,7 +60,11 @@ from .patches import bank34
from .roomEditor import RoomEditor, Object
from .patches.aesthetics import rgb_to_bin, bin_to_rgb
from .. import Options
from .logic import Logic as LADXRLogic
from .settings import Settings as LADXRSettings
from .worldSetup import WorldSetup as LADXRWorldSetup
from .locations.keyLocation import KeyLocation
class VersionError(Exception):
pass
@@ -86,8 +89,27 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
random.seed(patch_data["seed"] + patch_data["player"])
multi_key = binascii.unhexlify(patch_data["multi_key"].encode())
item_list = pickle.loads(binascii.unhexlify(patch_data["item_list"].encode()))
options = patch_data["options"]
ladxr_settings = LADXRSettings(patch_data["ladxr_settings_dict"])
world_setup = LADXRWorldSetup()
world_setup.goal = patch_data["world_setup"]["goal"]
world_setup.multichest = patch_data["world_setup"]["multichest"]
world_setup.entrance_mapping = patch_data["world_setup"]["entrance_mapping"]
world_setup.boss_mapping = patch_data["world_setup"]["boss_mapping"]
world_setup.miniboss_mapping = patch_data["world_setup"]["miniboss_mapping"]
ladxr_logic = LADXRLogic(configuration_options=ladxr_settings, world_setup=world_setup)
item_list = [item for item in ladxr_logic.iteminfo_list if not isinstance(item, KeyLocation)]
for spot in patch_data["rom_item_placements"]:
ladxr_item = next((item for item in item_list if item.nameId == spot["name_id"]), None)
if not ladxr_item:
continue
ladxr_item.item = spot["item"][1:] if spot["item"].startswith('*') else spot["item"]
ladxr_item.custom_item_name = spot["custom_item_name"]
mw = None
if patch_data["player"] != spot["item_owner"]:
mw = min(spot["item_owner"], 101)
ladxr_item.mw = mw
rom_patches = []
rom = ROMWithTables(base_rom, rom_patches)
rom.player_names = patch_data["other_player_names"]
@@ -101,7 +123,7 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
for pymod in pymods:
pymod.prePatch(rom)
if options["gfxmod"]:
if ladxr_settings.gfxmod:
try:
gfx_mod_file = LinksAwakeningWorld.settings.gfx_mod_file
patches.aesthetics.gfxMod(rom, gfx_mod_file)
@@ -136,7 +158,7 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
assembler.const("wLinkSpawnDelay", 0xDE13)
#assembler.const("HARDWARE_LINK", 1)
assembler.const("HARD_MODE", 1 if options["hard_mode"] else 0)
assembler.const("HARD_MODE", 1 if ladxr_settings.hardmode else 0)
patches.core.cleanup(rom)
patches.save.singleSaveSlot(rom)
@@ -159,17 +181,16 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
patches.core.alwaysAllowSecretBook(rom)
patches.core.injectMainLoop(rom)
if options["shuffle_small_keys"] != Options.ShuffleSmallKeys.option_original_dungeon or\
options["shuffle_nightmare_keys"] != Options.ShuffleNightmareKeys.option_original_dungeon:
if ladxr_settings.shufflesmallkeys != 'originaldungeon' or ladxr_settings.shufflenightmarekeys != 'originaldungeon':
patches.inventory.advancedInventorySubscreen(rom)
patches.inventory.moreSlots(rom)
# if ladxr_settings["witch"]:
# if ladxr_settings.witch:
patches.witch.updateWitch(rom)
patches.softlock.fixAll(rom)
if not options["rooster"]:
if not ladxr_settings.rooster:
patches.maptweaks.tweakMap(rom)
patches.maptweaks.tweakBirdKeyRoom(rom)
if options["overworld"] == Options.Overworld.option_open_mabe:
if ladxr_settings.overworld == 'openmabe':
patches.maptweaks.openMabe(rom)
patches.chest.fixChests(rom)
patches.shop.fixShop(rom)
@@ -181,9 +202,9 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
patches.tarin.updateTarin(rom)
patches.fishingMinigame.updateFinishingMinigame(rom)
patches.health.upgradeHealthContainers(rom)
# if ladxr_settings["owlstatues"] in ("dungeon", "both"):
# if ladxr_settings.owlstatues in ("dungeon", "both"):
# patches.owl.upgradeDungeonOwlStatues(rom)
# if ladxr_settings["owlstatues"] in ("overworld", "both"):
# if ladxr_settings.owlstatues in ("overworld", "both"):
# patches.owl.upgradeOverworldOwlStatues(rom)
patches.goldenLeaf.fixGoldenLeaf(rom)
patches.heartPiece.fixHeartPiece(rom)
@@ -194,17 +215,17 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
patches.songs.upgradeManbo(rom)
patches.songs.upgradeMamu(rom)
patches.tradeSequence.patchTradeSequence(rom, options)
patches.tradeSequence.patchTradeSequence(rom, ladxr_settings)
patches.bowwow.fixBowwow(rom, everywhere=False)
# if ladxr_settings["bowwow"] != 'normal':
# if ladxr_settings.bowwow != 'normal':
# patches.bowwow.bowwowMapPatches(rom)
patches.desert.desertAccess(rom)
# if ladxr_settings["overworld"] == 'dungeondive':
# if ladxr_settings.overworld == 'dungeondive':
# patches.overworld.patchOverworldTilesets(rom)
# patches.overworld.createDungeonOnlyOverworld(rom)
# elif ladxr_settings["overworld"] == 'nodungeons':
# elif ladxr_settings.overworld == 'nodungeons':
# patches.dungeon.patchNoDungeons(rom)
#elif world.ladxr_settings["overworld"] == 'random':
#elif ladxr_settings.overworld == 'random':
# patches.overworld.patchOverworldTilesets(rom)
# mapgen.store_map(rom, world.ladxr_logic.world.map)
#if settings.dungeon_items == 'keysy':
@@ -212,102 +233,94 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
# patches.reduceRNG.slowdownThreeOfAKind(rom)
patches.reduceRNG.fixHorseHeads(rom)
patches.bomb.onlyDropBombsWhenHaveBombs(rom)
if options["music_change_condition"] == Options.MusicChangeCondition.option_always:
if ladxr_settings.musicchange == 'always':
patches.aesthetics.noSwordMusic(rom)
patches.aesthetics.reduceMessageLengths(rom, random)
patches.aesthetics.allowColorDungeonSpritesEverywhere(rom)
if options["music"] == Options.Music.option_shuffled:
if ladxr_settings.music == 'shuffled':
patches.music.randomizeMusic(rom, random)
elif options["music"] == Options.Music.option_off:
elif ladxr_settings.music == 'off':
patches.music.noMusic(rom)
if options["no_flash"]:
if ladxr_settings.noflash:
patches.aesthetics.removeFlashingLights(rom)
if options["hard_mode"] == Options.HardMode.option_oracle:
if ladxr_settings.hardmode == 'oracle':
patches.hardMode.oracleMode(rom)
elif options["hard_mode"] == Options.HardMode.option_hero:
elif ladxr_settings.hardmode == 'hero':
patches.hardMode.heroMode(rom)
elif options["hard_mode"] == Options.HardMode.option_ohko:
elif ladxr_settings.hardmode == 'ohko':
patches.hardMode.oneHitKO(rom)
#if ladxr_settings["superweapons"]:
#if ladxr_settings.superweapons:
# patches.weapons.patchSuperWeapons(rom)
if options["text_mode"] == Options.TextMode.option_fast:
if ladxr_settings.textmode == 'fast':
patches.aesthetics.fastText(rom)
#if ladxr_settings["textmode"] == 'none':
#if ladxr_settings.textmode == 'none':
# patches.aesthetics.fastText(rom)
# patches.aesthetics.noText(rom)
if not options["nag_messages"]:
if not ladxr_settings.nagmessages:
patches.aesthetics.removeNagMessages(rom)
if options["low_hp_beep"] == Options.LowHpBeep.option_slow:
if ladxr_settings.lowhpbeep == 'slow':
patches.aesthetics.slowLowHPBeep(rom)
if options["low_hp_beep"] == Options.LowHpBeep.option_none:
if ladxr_settings.lowhpbeep == 'none':
patches.aesthetics.removeLowHPBeep(rom)
if 0 <= options["link_palette"]:
patches.aesthetics.forceLinksPalette(rom, options["link_palette"])
if 0 <= int(ladxr_settings.linkspalette):
patches.aesthetics.forceLinksPalette(rom, int(ladxr_settings.linkspalette))
if args.romdebugmode:
# The default rom has this build in, just need to set a flag and we get this save.
rom.patch(0, 0x0003, "00", "01")
# Patch the sword check on the shopkeeper turning around.
if options["stealing"] == Options.Stealing.option_disabled:
if ladxr_settings.steal == 'disabled':
rom.patch(4, 0x36F9, "FA4EDB", "3E0000")
rom.texts[0x2E] = utils.formatText("Hey! Welcome! Did you know that I have eyes on the back of my head?")
rom.texts[0x2F] = utils.formatText("Nothing escapes my gaze! Your thieving ways shall never prosper!")
#if ladxr_settings["hpmode"] == 'inverted':
#if ladxr_settings.hpmode == 'inverted':
# patches.health.setStartHealth(rom, 9)
#elif ladxr_settings["hpmode"] == '1':
#elif ladxr_settings.hpmode == '1':
# patches.health.setStartHealth(rom, 1)
patches.inventory.songSelectAfterOcarinaSelect(rom)
if options["quickswap"] == Options.Quickswap.option_a:
if ladxr_settings.quickswap == 'a':
patches.core.quickswap(rom, 1)
elif options["quickswap"] == Options.Quickswap.option_b:
elif ladxr_settings.quickswap == 'b':
patches.core.quickswap(rom, 0)
patches.core.addBootsControls(rom, options["boots_controls"])
patches.core.addBootsControls(rom, ladxr_settings.bootscontrols)
random.seed(patch_data["seed"] + patch_data["player"])
hints.addHints(rom, random, patch_data["hint_texts"])
if patch_data["world_setup"]["goal"] == "raft":
if world_setup.goal == "raft":
patches.goal.setRaftGoal(rom)
elif patch_data["world_setup"]["goal"] in ("bingo", "bingo-full"):
patches.bingo.setBingoGoal(rom, patch_data["world_setup"]["bingo_goals"], patch_data["world_setup"]["goal"])
elif patch_data["world_setup"]["goal"] == "seashells":
elif world_setup.goal in ("bingo", "bingo-full"):
patches.bingo.setBingoGoal(rom, world_setup.bingo_goals, world_setup.goal)
elif world_setup.goal == "seashells":
patches.goal.setSeashellGoal(rom, 20)
else:
patches.goal.setRequiredInstrumentCount(rom, patch_data["world_setup"]["goal"])
patches.goal.setRequiredInstrumentCount(rom, world_setup.goal)
# Patch the generated logic into the rom
patches.chest.setMultiChest(rom, patch_data["world_setup"]["multichest"])
#if ladxr_settings["overworld"] not in {"dungeondive", "random"}:
patches.entrances.changeEntrances(rom, patch_data["world_setup"]["entrance_mapping"])
for spot in item_list:
if spot.item and spot.item.startswith("*"):
spot.item = spot.item[1:]
mw = None
if spot.item_owner != spot.location_owner:
mw = spot.item_owner
if mw > 101:
# There are only 101 player name slots (99 + "The Server" + "another world"), so don't use more than that
mw = 101
spot.patch(rom, spot.item, multiworld=mw)
patches.enemies.changeBosses(rom, patch_data["world_setup"]["boss_mapping"])
patches.enemies.changeMiniBosses(rom, patch_data["world_setup"]["miniboss_mapping"])
patches.chest.setMultiChest(rom, world_setup.multichest)
#if ladxr_settings.overworld not in {"dungeondive", "random"}:
patches.entrances.changeEntrances(rom, world_setup.entrance_mapping)
for ladxr_item in item_list:
ladxr_item.patch(rom, ladxr_item.item, multiworld=ladxr_item.mw)
patches.enemies.changeBosses(rom, world_setup.boss_mapping)
patches.enemies.changeMiniBosses(rom, world_setup.miniboss_mapping)
if not args.romdebugmode:
patches.core.addFrameCounter(rom, len(item_list))
patches.core.warpHome(rom) # Needs to be done after setting the start location.
patches.titleScreen.setRomInfo(rom, patch_data)
if options["ap_title_screen"]:
if ladxr_settings.aptitlescreen:
patches.titleScreen.setTitleGraphics(rom)
patches.endscreen.updateEndScreen(rom)
patches.aesthetics.updateSpriteData(rom)
if args.doubletrouble:
patches.enemies.doubleTrouble(rom)
if options["text_shuffle"]:
if ladxr_settings.textshuffle:
excluded_ids = [
# Overworld owl statues
0x1B6, 0x1B7, 0x1B8, 0x1B9, 0x1BA, 0x1BB, 0x1BC, 0x1BD, 0x1BE, 0x22D,
@@ -366,14 +379,14 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
rom.texts[shuffled[bucket_idx][0]] = data
if options["trendy_game"] != Options.TrendyGame.option_normal:
if ladxr_settings.trendygame != 'normal':
# TODO: if 0 or 4, 5, remove inaccurate conveyor tiles
room_editor = RoomEditor(rom, 0x2A0)
if options["trendy_game"] == Options.TrendyGame.option_easy:
if ladxr_settings.trendygame == 'easy':
# Set physics flag on all objects
for i in range(0, 6):
rom.banks[0x4][0x6F1E + i -0x4000] = 0x4
@@ -384,7 +397,7 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
# Add new conveyor to "push" yoshi (it's only a visual)
room_editor.objects.append(Object(5, 3, 0xD0))
if options["trendy_game"] >= Options.TrendyGame.option_harder:
if ladxr_settings.trendygame in ('harder', 'hardest', 'impossible'):
"""
Data_004_76A0::
db $FC, $00, $04, $00, $00
@@ -393,18 +406,18 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
db $00, $04, $00, $FC, $00
"""
speeds = {
Options.TrendyGame.option_harder: (3, 8),
Options.TrendyGame.option_hardest: (3, 8),
Options.TrendyGame.option_impossible: (3, 16),
'harder': (3, 8),
'hardest': (3, 8),
'impossible': (3, 16),
}
def speed():
random.seed(patch_data["seed"] + patch_data["player"])
return random.randint(*speeds[options["trendy_game"]])
return random.randint(*speeds[ladxr_settings.trendygame])
rom.banks[0x4][0x76A0-0x4000] = 0xFF - speed()
rom.banks[0x4][0x76A2-0x4000] = speed()
rom.banks[0x4][0x76A6-0x4000] = speed()
rom.banks[0x4][0x76A8-0x4000] = 0xFF - speed()
if options["trendy_game"] >= Options.TrendyGame.option_hardest:
if ladxr_settings.trendygame in ('hardest', 'impossible'):
rom.banks[0x4][0x76A1-0x4000] = 0xFF - speed()
rom.banks[0x4][0x76A3-0x4000] = speed()
rom.banks[0x4][0x76A5-0x4000] = speed()
@@ -428,11 +441,11 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
for channel in range(3):
color[channel] = color[channel] * 31 // 0xbc
if options["warps"] != Options.Warps.option_vanilla:
patches.core.addWarpImprovements(rom, options["warps"] == Options.Warps.option_improved_additional)
if ladxr_settings.warps != 'vanilla':
patches.core.addWarpImprovements(rom, ladxr_settings.warps == 'improved_additional')
palette = options["palette"]
if palette != Options.Palette.option_normal:
palette = ladxr_settings.palette
if palette != 'normal':
ranges = {
# Object palettes
# Overworld palettes
@@ -462,22 +475,22 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
r,g,b = bin_to_rgb(packed)
# 1 bit
if palette == Options.Palette.option_1bit:
if palette == '1bit':
r &= 0b10000
g &= 0b10000
b &= 0b10000
# 2 bit
elif palette == Options.Palette.option_1bit:
elif palette == '1bit':
r &= 0b11000
g &= 0b11000
b &= 0b11000
# Invert
elif palette == Options.Palette.option_inverted:
elif palette == 'inverted':
r = 31 - r
g = 31 - g
b = 31 - b
# Pink
elif palette == Options.Palette.option_pink:
elif palette == 'pink':
r = r // 2
r += 16
r = int(r)
@@ -486,7 +499,7 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
b += 16
b = int(b)
b = clamp(b, 0, 0x1F)
elif palette == Options.Palette.option_greyscale:
elif palette == 'greyscale':
# gray=int(0.299*r+0.587*g+0.114*b)
gray = (r + g + b) // 3
r = g = b = gray

View File

@@ -20,7 +20,7 @@ class Dungeon1:
if options.owlstatues == "both" or options.owlstatues == "dungeon":
Location(dungeon=1).add(OwlStatue(0x10A)).connect(dungeon1_right_side, STONE_BEAK1)
dungeon1_3_of_a_kind = Location(dungeon=1).add(DungeonChest(0x10A)).connect(dungeon1_right_side, OR(r.attack_hookshot_no_bomb, SHIELD)) # three of a kind, shield stops the suit from changing
dungeon1_miniboss = Location(dungeon=1).connect(dungeon1_right_side, AND(r.miniboss_requirements[world_setup.miniboss_mapping[0]], FEATHER))
dungeon1_miniboss = Location(dungeon=1).connect(dungeon1_right_side, AND(r.miniboss_requirements[world_setup.miniboss_mapping['0']], FEATHER))
dungeon1_boss = Location(dungeon=1).connect(dungeon1_miniboss, NIGHTMARE_KEY1)
boss = Location(dungeon=1).add(HeartContainer(0x106), Instrument(0x102)).connect(dungeon1_boss, r.boss_requirements[world_setup.boss_mapping[0]])
@@ -30,7 +30,7 @@ class Dungeon1:
if options.logic == 'glitched' or options.logic == 'hell':
boss_key.connect(entrance, r.super_jump_feather) # super jump
dungeon1_miniboss.connect(dungeon1_right_side, r.miniboss_requirements[world_setup.miniboss_mapping[0]]) # damage boost or buffer pause over the pit to cross or mushroom
dungeon1_miniboss.connect(dungeon1_right_side, r.miniboss_requirements[world_setup.miniboss_mapping['0']]) # damage boost or buffer pause over the pit to cross or mushroom
if options.logic == 'hell':
feather_chest.connect(dungeon1_upper_left, SWORD) # keep slashing the spiked beetles until they keep moving 1 pixel close towards you and the pit, to get them to fall

View File

@@ -23,7 +23,7 @@ class Dungeon2:
dungeon2_r5 = Location(dungeon=2).connect(dungeon2_r4, AND(KEY2, FOUND(KEY2, 3))) # push two blocks together room with owl statue
if options.owlstatues == "both" or options.owlstatues == "dungeon":
Location(dungeon=2).add(OwlStatue(0x12F)).connect(dungeon2_r5, STONE_BEAK2) # owl statue is before miniboss
miniboss = Location(dungeon=2).add(DungeonChest(0x126)).add(DungeonChest(0x121)).connect(dungeon2_r5, AND(FEATHER, r.miniboss_requirements[world_setup.miniboss_mapping[1]])) # post hinox
miniboss = Location(dungeon=2).add(DungeonChest(0x126)).add(DungeonChest(0x121)).connect(dungeon2_r5, AND(FEATHER, r.miniboss_requirements[world_setup.miniboss_mapping['1']])) # post hinox
if options.owlstatues == "both" or options.owlstatues == "dungeon":
Location(dungeon=2).add(OwlStatue(0x129)).connect(miniboss, STONE_BEAK2) # owl statue after the miniboss
@@ -45,7 +45,7 @@ class Dungeon2:
dungeon2_map_chest.connect(dungeon2_l2, AND(r.attack_hookshot_powder, r.boots_bonk_pit)) # use boots to jump over the pits
dungeon2_r4.connect(dungeon2_r3, OR(r.boots_bonk_pit, r.hookshot_spam_pit)) # can use both pegasus boots bonks or hookshot spam to cross the pit room
dungeon2_r4.connect(shyguy_key_drop, r.rear_attack_range, one_way=True) # adjust for alternate requirements for dungeon2_r4
miniboss.connect(dungeon2_r5, AND(r.boots_dash_2d, r.miniboss_requirements[world_setup.miniboss_mapping[1]])) # use boots to dash over the spikes in the 2d section
miniboss.connect(dungeon2_r5, AND(r.boots_dash_2d, r.miniboss_requirements[world_setup.miniboss_mapping['1']])) # use boots to dash over the spikes in the 2d section
dungeon2_pre_stairs_boss.connect(dungeon2_r6, AND(HOOKSHOT, OR(BOW, BOMB, MAGIC_ROD, AND(OCARINA, SONG1)), FOUND(KEY2, 5))) # hookshot clip through the pot using both pol's voice
dungeon2_post_stairs_boss.connect(dungeon2_pre_stairs_boss, OR(BOMB, r.boots_jump)) # use a bomb to lower the last platform, or boots + feather to cross over top (only relevant in hell logic)
dungeon2_pre_boss.connect(dungeon2_post_stairs_boss, AND(r.boots_bonk_pit, r.hookshot_spam_pit)) # boots bonk off bottom wall + hookshot spam across the two 1 tile pits vertically

View File

@@ -33,7 +33,7 @@ class Dungeon3:
Location(dungeon=3).add(DroppedKey(0x14D)).connect(area_right, r.attack_hookshot_powder) # key after the stairs.
dungeon3_nightmare_key_chest = Location(dungeon=3).add(DungeonChest(0x147)).connect(area_right, AND(BOMB, FEATHER, PEGASUS_BOOTS)) # nightmare key chest
dungeon3_post_dodongo_chest = Location(dungeon=3).add(DungeonChest(0x146)).connect(area_right, AND(r.attack_hookshot_powder, r.miniboss_requirements[world_setup.miniboss_mapping[2]])) # boots after the miniboss
dungeon3_post_dodongo_chest = Location(dungeon=3).add(DungeonChest(0x146)).connect(area_right, AND(r.attack_hookshot_powder, r.miniboss_requirements[world_setup.miniboss_mapping['2']])) # boots after the miniboss
compass_chest = Location(dungeon=3).add(DungeonChest(0x142)).connect(area_right, OR(SWORD, BOMB, AND(SHIELD, r.attack_hookshot_powder))) # bomb only activates with sword, bomb or shield
dungeon3_3_bombite_room = Location(dungeon=3).add(DroppedKey(0x141)).connect(compass_chest, BOMB) # 3 bombite room
Location(dungeon=3).add(DroppedKey(0x148)).connect(area_right, r.attack_no_boomerang) # 2 zol 2 owl drop key

View File

@@ -29,7 +29,7 @@ class Dungeon4:
left_water_area = Location(dungeon=4).connect(before_miniboss, OR(FEATHER, FLIPPERS)) # area left with zol chest and 5 symbol puzzle (water area)
left_water_area.add(DungeonChest(0x16D)) # gel chest
left_water_area.add(DungeonChest(0x168)) # key chest near the puzzle
miniboss = Location(dungeon=4).connect(before_miniboss, AND(KEY4, FOUND(KEY4, 5), r.miniboss_requirements[world_setup.miniboss_mapping[3]]))
miniboss = Location(dungeon=4).connect(before_miniboss, AND(KEY4, FOUND(KEY4, 5), r.miniboss_requirements[world_setup.miniboss_mapping['3']]))
terrace_zols_chest = Location(dungeon=4).connect(before_miniboss, FLIPPERS) # flippers to move around miniboss through 5 tile room
miniboss = Location(dungeon=4).connect(terrace_zols_chest, POWER_BRACELET, one_way=True) # reach flippers chest through the miniboss room
terrace_zols_chest.add(DungeonChest(0x160)) # flippers chest

View File

@@ -15,7 +15,7 @@ class Dungeon5:
Location(dungeon=5).add(OwlStatue(0x19A)).connect(area2, STONE_BEAK5)
Location(dungeon=5).add(DungeonChest(0x19B)).connect(area2, r.attack_hookshot_powder) # map chest
blade_trap_chest = Location(dungeon=5).add(DungeonChest(0x197)).connect(area2, HOOKSHOT) # key chest on the left
post_gohma = Location(dungeon=5).connect(area2, AND(HOOKSHOT, r.miniboss_requirements[world_setup.miniboss_mapping[4]], KEY5, FOUND(KEY5,2))) # staircase after gohma
post_gohma = Location(dungeon=5).connect(area2, AND(HOOKSHOT, r.miniboss_requirements[world_setup.miniboss_mapping['4']], KEY5, FOUND(KEY5,2))) # staircase after gohma
staircase_before_boss = Location(dungeon=5).connect(post_gohma, AND(HOOKSHOT, FEATHER)) # bottom right section pits room before boss door. Path via gohma
after_keyblock_boss = Location(dungeon=5).connect(staircase_before_boss, AND(KEY5, FOUND(KEY5, 3))) # top right section pits room before boss door
after_stalfos = Location(dungeon=5).add(DungeonChest(0x196)).connect(area2, AND(SWORD, BOMB)) # Need to defeat master stalfos once for this empty chest; l2 sword beams kill but obscure

View File

@@ -24,7 +24,7 @@ class Dungeon6:
# right side
to_miniboss = Location(dungeon=6).connect(entrance, KEY6)
miniboss = Location(dungeon=6).connect(to_miniboss, AND(BOMB, r.miniboss_requirements[world_setup.miniboss_mapping[5]]))
miniboss = Location(dungeon=6).connect(to_miniboss, AND(BOMB, r.miniboss_requirements[world_setup.miniboss_mapping['5']]))
lower_right_side = Location(dungeon=6).add(DungeonChest(0x1BE)).connect(entrance, AND(r.attack_wizrobe, COUNT(POWER_BRACELET, 2))) # waterway key
medicine_chest = Location(dungeon=6).add(DungeonChest(0x1D1)).connect(lower_right_side, FEATHER) # ledge chest medicine
if options.owlstatues == "both" or options.owlstatues == "dungeon":

View File

@@ -22,7 +22,7 @@ class Dungeon7:
# Most of the dungeon can be accessed at this point.
if options.owlstatues == "both" or options.owlstatues == "dungeon":
bottomleft_owl = Location(dungeon=7).add(OwlStatue(0x21C)).connect(bottomleftF2_area, AND(BOMB, STONE_BEAK7))
nightmare_key = Location(dungeon=7).add(DungeonChest(0x224)).connect(bottomleftF2_area, r.miniboss_requirements[world_setup.miniboss_mapping[6]]) # nightmare key after the miniboss
nightmare_key = Location(dungeon=7).add(DungeonChest(0x224)).connect(bottomleftF2_area, r.miniboss_requirements[world_setup.miniboss_mapping['6']]) # nightmare key after the miniboss
mirror_shield_chest = Location(dungeon=7).add(DungeonChest(0x21A)).connect(bottomleftF2_area, r.hit_switch) # mirror shield chest, need to be able to hit a switch to reach or
bottomleftF2_area.connect(mirror_shield_chest, AND(KEY7, FOUND(KEY7, 3)), one_way = True) # reach mirror shield chest from hinox area by opening keyblock
toprightF1_chest = Location(dungeon=7).add(DungeonChest(0x204)).connect(bottomleftF2_area, r.hit_switch) # chest on the F1 right ledge. Added attack_hookshot since switch needs to be hit to get back up

View File

@@ -40,7 +40,7 @@ class Dungeon8:
middle_center_2 = Location(dungeon=8).connect(middle_center_1, AND(KEY8, FOUND(KEY8, 4)))
middle_center_3 = Location(dungeon=8).connect(middle_center_2, KEY8)
miniboss_entrance = Location(dungeon=8).connect(middle_center_3, AND(HOOKSHOT, KEY8, FOUND(KEY8, 7))) # hookshot to get across to keyblock, 7 to fix keylock issues if keys are used on other keyblocks
miniboss = Location(dungeon=8).connect(miniboss_entrance, AND(FEATHER, r.miniboss_requirements[world_setup.miniboss_mapping[7]])) # feather for 2d section, sword to kill
miniboss = Location(dungeon=8).connect(miniboss_entrance, AND(FEATHER, r.miniboss_requirements[world_setup.miniboss_mapping['7']])) # feather for 2d section, sword to kill
miniboss.add(DungeonChest(0x237)) # fire rod chest
up_left = Location(dungeon=8).connect(upper_center, AND(r.attack_hookshot_powder, AND(KEY8, FOUND(KEY8, 4))))
@@ -94,7 +94,7 @@ class Dungeon8:
entrance.connect(bottomright_pot_chest, r.shaq_jump, one_way=True) # use NW zamboni staircase backwards, and get a naked shaq jump off the bottom wall in the bottom right corner to pass by the pot
gibdos_drop_key.connect(upper_center, AND(FEATHER, SHIELD)) # lock gibdos into pits and crack the tile they stand on, then use shield to bump them into the pit
medicine_chest.connect(upper_center, AND(r.pit_buffer_boots, HOOKSHOT)) # boots bonk + lava buffer to the bottom wall, then bonk onto the middle section
miniboss.connect(miniboss_entrance, AND(r.boots_bonk_2d_hell, r.miniboss_requirements[world_setup.miniboss_mapping[7]])) # get through 2d section with boots bonks
miniboss.connect(miniboss_entrance, AND(r.boots_bonk_2d_hell, r.miniboss_requirements[world_setup.miniboss_mapping['7']])) # get through 2d section with boots bonks
top_left_stairs.connect(map_chest, AND(r.jesus_buffer, r.boots_bonk_2d_hell, MAGIC_ROD)) # boots bonk + lava buffer from map chest to entrance_up, then boots bonk through 2d section
nightmare_key.connect(top_left_stairs, AND(r.boots_bonk_pit, SWORD, FOUND(KEY8, 7))) # use a boots bonk to cross the 2d section + the lava in cueball room
bottom_right.connect(entrance_up, AND(POWER_BRACELET, r.jesus_buffer), one_way=True) # take staircase to NW zamboni room, boots bonk onto the lava and water buffer all the way down to push the zamboni

View File

@@ -541,8 +541,8 @@ OAMData:
rom.banks[0x38][0x1400+n*0x20:0x1410+n*0x20] = utils.createTileData(gfx_high)
rom.banks[0x38][0x1410+n*0x20:0x1420+n*0x20] = utils.createTileData(gfx_low)
def addBootsControls(rom, boots_controls: int):
if boots_controls == BootsControls.option_vanilla:
def addBootsControls(rom, bootscontrols):
if bootscontrols == 'vanilla':
return
consts = {
"INVENTORY_PEGASUS_BOOTS": 0x8,
@@ -560,25 +560,25 @@ def addBootsControls(rom, boots_controls: int):
BOOTS_START_ADDR = 0x11E8
condition = {
BootsControls.option_bracelet: """
'bracelet': """
ld a, [hl]
; Check if we are using the bracelet
cp INVENTORY_POWER_BRACELET
jr z, .yesBoots
""",
BootsControls.option_press_a: """
'pressa': """
; Check if we are using the A slot
cp J_A
jr z, .yesBoots
ld a, [hl]
""",
BootsControls.option_press_b: """
'pressb': """
; Check if we are using the B slot
cp J_B
jr z, .yesBoots
ld a, [hl]
"""
}[boots_controls]
}[bootscontrols]
# The new code fits exactly within Nintendo's poorly space optimzied code while having more features
boots_code = assembler.ASM("""

View File

@@ -387,7 +387,7 @@ def patchVarious(rom, settings):
# Boomerang trade guy
# if settings.boomerang not in {'trade', 'gift'} or settings.overworld in {'normal', 'nodungeons'}:
if settings["tradequest"]:
if settings.tradequest:
# Update magnifier checks
rom.patch(0x19, 0x05EC, ASM("ld a, [wTradeSequenceItem]\ncp $0E\njp nz, $7E61"), ASM("ld a, [wTradeSequenceItem2]\nand $20\njp z, $7E61")) # show the guy
rom.patch(0x00, 0x3199, ASM("ld a, [wTradeSequenceItem]\ncp $0E\njr nz, $06"), ASM("ld a, [wTradeSequenceItem2]\nand $20\njr z, $06")) # load the proper room layout

View File

@@ -68,7 +68,7 @@ class Setting:
class Settings:
def __init__(self, ap_options):
def __init__(self, settings_dict):
self.__all = [
Setting('seed', 'Main', '<', 'Seed', placeholder='Leave empty for random seed', default="", multiworld=False,
description="""For multiple people to generate the same randomization result, enter the generated seed number here.
@@ -178,6 +178,14 @@ Note, some entrances can lead into water, use the warp-to-home from the save&qui
description='Replaces the hints from owl statues with additional randomized items'),
Setting('superweapons', 'Special', 'q', 'Enable super weapons', default=False,
description='All items will be more powerful, faster, harder, bigger stronger. You name it.'),
Setting('trendygame', 'Special', 'a', 'Trendy Game', description="",
options=[('easy', 'e', 'Easy'), ('normal', 'n', 'Normal'), ('hard', 'h', 'Hard'), ('harder', 'r', 'Harder'), ('hardest', 't', 'Hardest'), ('impossible', 'i', 'Impossible')], default='normal'),
Setting('warps', 'Special', 'a', 'Warps', description="",
options=[('vanilla', 'v', 'Vanilla'), ('improved', 'i', 'Improved'), ('improvedadditional', 'a', 'Improved Additional')], default='vanilla'),
Setting('shufflenightmarekeys', 'Special', 'a', 'Shuffle Nightmare Keys', description="",
options=[('originaldungeon', '0', 'Original Dungeon'), ('owndungeons', '1', 'Own Dungeons'), ('ownworld', '2', 'Own World'), ('anyworld', '3', 'Any World'), ('differentworld', '4', 'Different World')], default="originaldungeon"),
Setting('shufflesmallkeys', 'Special', 'a', 'Shuffle Small Keys', description="",
options=[('originaldungeon', '0', 'Original Dungeon'), ('owndungeons', '1', 'Own Dungeons'), ('ownworld', '2', 'Own World'), ('anyworld', '3', 'Any World'), ('differentworld', '4', 'Different World')], default="originaldungeon"),
Setting('quickswap', 'User options', 'Q', 'Quickswap', options=[('none', '', 'Disabled'), ('a', 'a', 'Swap A button'), ('b', 'b', 'Swap B button')], default='none',
description='Adds that the select button swaps with either A or B. The item is swapped with the top inventory slot. The map is not available when quickswap is enabled.',
aesthetic=True),
@@ -192,7 +200,7 @@ Note, some entrances can lead into water, use the warp-to-home from the save&qui
Setting('nagmessages', 'User options', 'S', 'Show nag messages', default=False,
description='Enables the nag messages normally shown when touching stones and crystals',
aesthetic=True),
Setting('gfxmod', 'User options', 'c', 'Graphics', default='',
Setting('gfxmod', 'User options', 'c', 'Graphics', default=False,
description='Generally affects at least Link\'s sprite, but can alter any graphics in the game',
aesthetic=True),
Setting('linkspalette', 'User options', 'C', "Link's color",
@@ -202,25 +210,31 @@ Note, some entrances can lead into water, use the warp-to-home from the save&qui
[Normal] color of link depends on the tunic.
[Green/Yellow/Red/Blue] forces link into one of these colors.
[?? A/B/C/D] colors of link are usually inverted and color depends on the area you are in."""),
Setting('palette', 'User options', 'a', 'Palette', description="",
options=[('normal', 'n', 'Normal'), ('1bit', '1', '1 Bit'), ('2bit', '2', '2 Bit'), ('greyscale', 'g', 'Greyscale'), ('pink', 'p', 'Pink'), ('inverted', 'i', 'Inverted')], default='normal', aesthetic=True),
Setting('music', 'User options', 'M', 'Music', options=[('', '', 'Default'), ('random', 'r', 'Random'), ('off', 'o', 'Disable')], default='',
description="""
[Random] Randomizes overworld and dungeon music'
[Disable] no music in the whole game""",
aesthetic=True),
Setting('musicchange', 'User options', 'a', 'Music Change Condition', description="",
options=[('always', 'a', 'Always'), ('sword', 's', 'Sword')], default='always', aesthetic=True),
Setting('bootscontrols', 'User options', 'a', 'Boots Controls', description="",
options=[('vanilla', 'v', 'Vanilla'), ('bracelet', 'p', 'Bracelet'), ('pressa', 'a', 'Press A'), ('pressb', 'b', 'Press B')], default='vanilla', aesthetic=True),
Setting('foreignitemicons', 'User options', 'a', 'Foreign Item Icons', description="",
options=[('guessbyname', 'g', 'Guess By Name'), ('indicateprogression', 'p', 'Indicate Progression')], default="guessbyname", aesthetic=True),
Setting('aptitlescreen', 'User options', 'a', 'AP Title Screen', description="", default=True),
Setting('textshuffle', 'User options', 'a', 'Text Shuffle', description="", default=False),
]
self.__by_key = {s.key: s for s in self.__all}
# Make sure all short keys are unique
short_keys = set()
for s in self.__all:
assert s.short_key not in short_keys, s.label
short_keys.add(s.short_key)
self.ap_options = ap_options
# don't worry about unique short keys for AP
#short_keys = set()
#for s in self.__all:
# assert s.short_key not in short_keys, s.label
# short_keys.add(s.short_key)
for option in self.ap_options.values():
if not hasattr(option, 'to_ladxr_option'):
continue
name, value = option.to_ladxr_option(self.ap_options)
for name, value in settings_dict.items():
if value == "true":
value = 1
elif value == "false":

View File

@@ -28,7 +28,7 @@ class WorldSetup:
self.boss_mapping = list(range(9))
self.miniboss_mapping = {
# Main minibosses
0: "ROLLING_BONES", 1: "HINOX", 2: "DODONGO", 3: "CUE_BALL", 4: "GHOMA", 5: "SMASHER", 6: "GRIM_CREEPER", 7: "BLAINO",
'0': "ROLLING_BONES", '1': "HINOX", '2': "DODONGO", '3': "CUE_BALL", '4': "GHOMA", '5': "SMASHER", '6': "GRIM_CREEPER", '7': "BLAINO",
# Color dungeon needs to be special, as always.
"c1": "AVALAUNCH", "c2": "GIANT_BUZZ_BLOB",
# Overworld

View File

@@ -60,11 +60,12 @@ class TradeQuest(DefaultOffToggle, LADXROption):
ladxr_name = "tradequest"
class TextShuffle(DefaultOffToggle):
class TextShuffle(DefaultOffToggle, LADXROption):
"""
Shuffles all text in the game.
"""
display_name = "Text Shuffle"
ladxr_name = "textshuffle"
class Rooster(DefaultOnToggle, LADXROption):
@@ -112,11 +113,12 @@ class DungeonShuffle(DefaultOffToggle, LADXROption):
ladxr_name = "dungeonshuffle"
class APTitleScreen(DefaultOnToggle):
class APTitleScreen(DefaultOnToggle, LADXROption):
"""
Enables AP specific title screen and disables the intro cutscene.
"""
display_name = "AP Title Screen"
ladxr_name = "aptitlescreen"
class BossShuffle(Choice):
@@ -142,7 +144,7 @@ class DungeonItemShuffle(Choice):
ladxr_item: str
class ShuffleNightmareKeys(DungeonItemShuffle):
class ShuffleNightmareKeys(DungeonItemShuffle, LADXROption):
"""
**Original Dungeon:** The item will be within its original dungeon.
@@ -156,9 +158,10 @@ class ShuffleNightmareKeys(DungeonItemShuffle):
"""
display_name = "Shuffle Nightmare Keys"
ladxr_item = "NIGHTMARE_KEY"
ladxr_name = "shufflenightmarekeys"
class ShuffleSmallKeys(DungeonItemShuffle):
class ShuffleSmallKeys(DungeonItemShuffle, LADXROption):
"""
**Original Dungeon:** The item will be within its original dungeon.
@@ -172,6 +175,7 @@ class ShuffleSmallKeys(DungeonItemShuffle):
"""
display_name = "Shuffle Small Keys"
ladxr_item = "KEY"
ladxr_name = "shufflesmallkeys"
class ShuffleMaps(DungeonItemShuffle):
@@ -266,7 +270,7 @@ class Goal(Choice, LADXROption):
def to_ladxr_option(self, all_options):
if self.value == self.option_instruments:
return ("goal", all_options["instrument_count"])
return ("goal", int(all_options["instrument_count"]))
else:
return LADXROption.to_ladxr_option(self, all_options)
@@ -291,7 +295,7 @@ class NagMessages(DefaultOffToggle, LADXROption):
ladxr_name = "nagmessages"
class MusicChangeCondition(Choice):
class MusicChangeCondition(Choice, LADXROption):
"""
Controls how the music changes.
@@ -304,6 +308,7 @@ class MusicChangeCondition(Choice):
option_sword = 0
option_always = 1
default = option_always
ladxr_name = "musicchange"
class HardMode(Choice, LADXROption):
@@ -396,7 +401,7 @@ class NoFlash(DefaultOnToggle, LADXROption):
ladxr_name = "noflash"
class BootsControls(Choice):
class BootsControls(Choice, LADXROption):
"""
Adds an additional button to activate Pegasus Boots (does nothing if you
haven't picked up your boots!)
@@ -418,6 +423,7 @@ class BootsControls(Choice):
alias_a = 2
option_press_b = 3
alias_b = 3
ladxr_name = "bootscontrols"
class LinkPalette(Choice, LADXROption):
@@ -444,7 +450,7 @@ class LinkPalette(Choice, LADXROption):
return self.ladxr_name, str(self.value)
class TrendyGame(Choice):
class TrendyGame(Choice, LADXROption):
"""
**Easy:** All of the items hold still for you.
@@ -468,16 +474,18 @@ class TrendyGame(Choice):
option_hardest = 4
option_impossible = 5
default = option_normal
ladxr_name = "trendygame"
class GfxMod(DefaultOffToggle):
class GfxMod(DefaultOffToggle, LADXROption):
"""
If enabled, the patcher will prompt the user for a modification file to change sprites in the game and optionally some text.
"""
display_name = "GFX Modification"
ladxr_name = "gfxmod"
class Palette(Choice):
class Palette(Choice, LADXROption):
"""
Sets the palette for the game.
@@ -504,6 +512,7 @@ class Palette(Choice):
option_greyscale = 3
option_pink = 4
option_inverted = 5
ladxr_name = "palette"
class Music(Choice, LADXROption):
@@ -530,7 +539,7 @@ class Music(Choice, LADXROption):
return self.ladxr_name, s
class Warps(Choice):
class Warps(Choice, LADXROption):
"""
**Improved:** Adds remake style warp screen to the game. Choose your warp
destination on the map after jumping in a portal and press *B* to select.
@@ -544,6 +553,7 @@ class Warps(Choice):
option_improved = 1
option_improved_additional = 2
default = option_vanilla
ladxr_name = 'warps'
class InGameHints(DefaultOnToggle):
@@ -583,7 +593,7 @@ class StabilizeItemPool(DefaultOffToggle):
rich_text_doc = True
class ForeignItemIcons(Choice):
class ForeignItemIcons(Choice, LADXROption):
"""
Choose how to display foreign items.
@@ -597,6 +607,7 @@ class ForeignItemIcons(Choice):
option_guess_by_name = 0
option_indicate_progression = 1
default = option_guess_by_name
ladxr_name = 'foreignitemicons'
ladx_option_groups = [

View File

@@ -6,13 +6,11 @@ import json
import pkgutil
import bsdiff4
import binascii
import pickle
from typing import TYPE_CHECKING
from .Common import *
from .LADXR import generator
from .LADXR.main import get_parser
from .LADXR.hints import generate_hint_texts
from .LADXR.locations.keyLocation import KeyLocation
LADX_HASH = "07c211479386825042efb4ad31bb525f"
if TYPE_CHECKING:
@@ -35,7 +33,7 @@ class LADXPatchExtensions(worlds.Files.APPatchExtension):
@staticmethod
def patch_title_screen(caller: worlds.Files.APProcedurePatch, rom: bytes, data_file: str) -> bytes:
patch_data = json.loads(caller.get_file(data_file).decode("utf-8"))
if patch_data["options"]["ap_title_screen"]:
if patch_data["ladxr_settings_dict"]["aptitlescreen"] == 'true':
return bsdiff4.patch(rom, pkgutil.get_data(__name__, "LADXR/patches/title_screen.bdiff4"))
return rom
@@ -56,7 +54,6 @@ class LADXProcedurePatch(worlds.Files.APProcedurePatch):
def write_patch_data(world: "LinksAwakeningWorld", patch: LADXProcedurePatch):
item_list = pickle.dumps([item for item in world.ladxr_logic.iteminfo_list if not isinstance(item, KeyLocation)])
data_dict = {
"generated_world_version": world.world_version.as_simple_string(),
"out_base": world.multiworld.get_out_file_name_base(patch.player),
@@ -67,44 +64,16 @@ def write_patch_data(world: "LinksAwakeningWorld", patch: LADXProcedurePatch):
"player": patch.player,
"player_name": patch.player_name,
"other_player_names": list(world.multiworld.player_name.values()),
"item_list": binascii.hexlify(item_list).decode(),
"rom_item_placements": world.rom_item_placements,
"hint_texts": generate_hint_texts(world),
"world_setup": {
"goal": world.ladxr_logic.world_setup.goal,
"bingo_goals": world.ladxr_logic.world_setup.bingo_goals,
"multichest": world.ladxr_logic.world_setup.multichest,
"entrance_mapping": world.ladxr_logic.world_setup.entrance_mapping,
"boss_mapping": world.ladxr_logic.world_setup.boss_mapping,
"miniboss_mapping": world.ladxr_logic.world_setup.miniboss_mapping,
},
"options": world.options.as_dict(
"tradequest",
"rooster",
"experimental_dungeon_shuffle",
"experimental_entrance_shuffle",
"goal",
"instrument_count",
"link_palette",
"warps",
"trendy_game",
"gfxmod",
"palette",
"text_shuffle",
"shuffle_nightmare_keys",
"shuffle_small_keys",
"music",
"music_change_condition",
"nag_messages",
"ap_title_screen",
"boots_controls",
"stealing",
"quickswap",
"hard_mode",
"low_hp_beep",
"text_mode",
"no_flash",
"overworld",
),
"ladxr_settings_dict": world.ladxr_settings_dict,
}
patch.write_file("data.json", json.dumps(data_dict).encode('utf-8'))

View File

@@ -180,7 +180,17 @@ class LinksAwakeningWorld(World):
}
def convert_ap_options_to_ladxr_logic(self):
self.ladxr_settings = LADXRSettings(dataclasses.asdict(self.options))
# store a dict of ladxr settings as a middle step so that we can also create a
# ladxr settings object on the other side of the patch
options_dict = dataclasses.asdict(self.options)
self.ladxr_settings_dict = {}
for option in options_dict.values():
if not hasattr(option, 'to_ladxr_option'):
continue
name, value = option.to_ladxr_option(options_dict)
if name:
self.ladxr_settings_dict[name] = value
self.ladxr_settings = LADXRSettings(self.ladxr_settings_dict)
self.ladxr_settings.validate()
world_setup = LADXRWorldSetup()
@@ -503,36 +513,36 @@ class LinksAwakeningWorld(World):
return "TRADING_ITEM_LETTER"
def generate_output(self, output_directory: str):
# copy items back to locations
self.rom_item_placements = []
for r in self.multiworld.get_regions(self.player):
for loc in r.locations:
if isinstance(loc, LinksAwakeningLocation):
assert(loc.item)
spot = {}
# If we're a links awakening item, just use the item
if isinstance(loc.item, LinksAwakeningItem):
loc.ladxr_item.item = loc.item.item_data.ladxr_id
spot["item"] = loc.item.item_data.ladxr_id
# If the item name contains "sword", use a sword icon, etc
# Otherwise, use a cute letter as the icon
elif self.options.foreign_item_icons == 'guess_by_name':
loc.ladxr_item.item = self.guess_icon_for_other_world(loc.item)
loc.ladxr_item.setCustomItemName(loc.item.name)
spot["item"] = self.guess_icon_for_other_world(loc.item)
else:
if loc.item.advancement:
loc.ladxr_item.item = 'PIECE_OF_POWER'
spot["item"] = 'PIECE_OF_POWER'
else:
loc.ladxr_item.item = 'GUARDIAN_ACORN'
loc.ladxr_item.setCustomItemName(loc.item.name)
spot["item"] = 'GUARDIAN_ACORN'
spot["custom_item_name"] = loc.item.name
if loc.item:
loc.ladxr_item.item_owner = loc.item.player
spot["item_owner"] = loc.item.player
else:
loc.ladxr_item.item_owner = self.player
spot["item_owner"] = self.player
# Kind of kludge, make it possible for the location to differentiate between local and remote items
loc.ladxr_item.location_owner = self.player
spot["name_id"] = loc.ladxr_item.nameId
self.rom_item_placements.append(spot)
patch = LADXProcedurePatch(player=self.player, player_name=self.player_name)

View File

@@ -135,7 +135,7 @@ Additionally, if you get an item while already having the max for that item (for
It is likely that you do not have release or collect permissions, or that there is nothing to release or collect.
Another option is that your connection was interrupted.
If you would still like to use release or collect, refer to [this section of the server commands page](https://archipelago.gg/tutorial/Archipelago/commands/en#collect/release).
If you would still like to use release or collect, refer to [this section of the server commands page](/tutorial/Archipelago/commands/en#collectrelease).
You may use the in-game console to execute the commands, if your slot has permissions to do so.

View File

@@ -60,7 +60,7 @@
This is usage documentation for the `custom_mission_order` YAML option for Starcraft 2. You can enable Custom Mission Orders by setting `mission_order: custom` in your YAML.
You will need to know how to write a YAML before engaging with this feature, and should read the [Archipelago YAML documentation](https://archipelago.gg/tutorial/Archipelago/advanced_settings/en) before continuing here.
You will need to know how to write a YAML before engaging with this feature, and should read the [Archipelago YAML documentation](/tutorial/Archipelago/advanced_settings/en) before continuing here.
Every example in this document should be valid to generate.

View File

@@ -951,14 +951,14 @@ item_descriptions = {
item_names.TEMPEST_GRAVITY_SLING: "Tempests gain +8 range against air targets and +8 cast range.",
item_names.TEMPEST_INTERPLANETARY_RANGE: "Tempests gain +8 weapon range against all targets.",
item_names.PHOENIX_CLASS_IONIC_WAVELENGTH_FLUX: "Increases Phoenix, Mirage, and Skirmisher weapon damage by +2.",
item_names.PHOENIX_CLASS_ANION_PULSE_CRYSTALS: "Increases Phoenix, Mirage, and Skirmiser range by +2.",
item_names.PHOENIX_CLASS_ANION_PULSE_CRYSTALS: "Increases Phoenix, Mirage, and Skirmisher range by +2.",
item_names.CORSAIR_STEALTH_DRIVE: "Corsairs become permanently cloaked.",
item_names.CORSAIR_ARGUS_JEWEL: "Corsairs can store 2 charges of disruption web.",
item_names.CORSAIR_SUSTAINING_DISRUPTION: "Corsair disruption webs last longer.",
item_names.CORSAIR_NEUTRON_SHIELDS: "Increases corsair maximum shields by +20.",
item_names.ORACLE_STEALTH_DRIVE: "Oracles become permanently cloaked.",
item_names.ORACLE_SKYWARD_CHRONOANOMALY: "The Oracle's Stasis Ward can affect air units.",
item_names.ORACLE_TEMPORAL_ACCELERATION_BEAM: "Oracles no longer need to to spend energy to attack.",
item_names.ORACLE_TEMPORAL_ACCELERATION_BEAM: "Oracles no longer need to spend energy to attack.",
item_names.ORACLE_BOSONIC_CORE: "Increases starting energy by 150 and maximum energy by 50.",
item_names.ARBITER_CHRONOSTATIC_REINFORCEMENT: "Arbiters gain +50 maximum life and +1 armor.",
item_names.ARBITER_KHAYDARIN_CORE: _get_start_and_max_energy_desc("Arbiters"),

View File

@@ -1309,7 +1309,7 @@ class MaximumSupplyReductionPerItem(Range):
class LowestMaximumSupply(Range):
"""Controls how far max supply reduction traps can reduce maximum supply."""
display_name = "Lowest Maximum Supply"
range_start = 100
range_start = 50
range_end = 200
default = 180

View File

@@ -1168,11 +1168,7 @@ class SC2Logic:
def two_kerrigan_actives(self, state: CollectionState, story_tech_available=True) -> bool:
if story_tech_available and self.grant_story_tech == GrantStoryTech.option_grant:
return True
count = 0
for i in range(7):
if state.has_any(kerrigan_logic_active_abilities, self.player):
count += 1
return count >= 2
return state.count_from_list(item_groups.kerrigan_logic_active_abilities, self.player) >= 2
# Global Protoss
def protoss_power_rating(self, state: CollectionState) -> int:

View File

@@ -42,7 +42,7 @@ Weitere Informationen zum Randomizer findest du hier: [ReadMe](https://github.co
## Woher bekomme ich eine Konfigurationsdatei?
Die [Player Options](https://archipelago.gg/games/Timespinner/player-options) Seite auf der Website erlaubt dir,
Die [Player Options](/games/Timespinner/player-options) Seite auf der Website erlaubt dir,
persönliche Einstellungen zu definieren und diese in eine Konfigurationsdatei zu exportieren
* Die Timespinner Randomizer Option "StinkyMaw" ist in Archipelago Seeds aktuell immer an