Compare commits

..

10 Commits

Author SHA1 Message Date
NewSoupVi
ad8223998e Update components.py 2025-12-18 22:20:21 +01:00
NewSoupVi
b6e2c8129f Update components.py 2025-12-18 22:19:35 +01:00
NewSoupVi
fd4e47efab APQuest: Explain game_name and supports_uri more in components.py
Hopefully this can lead to more games implementing support for the "click on slot name -> everything launches automatically" functionality.
2025-12-18 22:05:55 +01:00
Alchav
b42fb77451 Factorio: Craftsanity (#5529) 2025-12-18 07:52:15 +01:00
Ziktofel
5a8e166289 SC2: New maintainership (#5752)
I (Ziktofel) stepped down but will remain as a mentor
2025-12-18 00:06:49 +01:00
Rosalie
5fa719143c TLOZ: Add manifest file (#5755)
* Added manifest file.

* Update archipelago.json

---------

Co-authored-by: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com>
2025-12-18 00:06:06 +01:00
Duck
a906f139c3 APQuest: Fix ValueError on typing numbers/backspace #5757 2025-12-18 00:02:11 +01:00
Katelyn Gigante
56363ea7e7 OptionsCreator: Respect World.hidden flag (#5754) 2025-12-17 20:09:35 +01:00
Fabian Dill
01e1e1fe11 WebHost: increase form upload limit (#5756) 2025-12-17 19:12:10 +01:00
Fabian Dill
4477dc7a66 Core: Bump version from 0.6.5 to 0.6.6 (#5753) 2025-12-17 03:33:29 +01:00
27 changed files with 359 additions and 224 deletions

View File

@@ -632,7 +632,7 @@ class OptionsCreator(ThemedApp):
self.create_options_panel(world_btn) self.create_options_panel(world_btn)
for world, cls in sorted(AutoWorldRegister.world_types.items(), key=lambda x: x[0]): for world, cls in sorted(AutoWorldRegister.world_types.items(), key=lambda x: x[0]):
if world == "Archipelago": if cls.hidden:
continue continue
world_text = MDButtonText(text=world, size_hint_y=None, width=dp(150), world_text = MDButtonText(text=world, size_hint_y=None, width=dp(150),
pos_hint={"x": 0.03, "center_y": 0.5}) pos_hint={"x": 0.03, "center_y": 0.5})

View File

@@ -177,7 +177,8 @@
/worlds/sa2b/ @PoryGone @RaspberrySpace /worlds/sa2b/ @PoryGone @RaspberrySpace
# Starcraft 2 # Starcraft 2
/worlds/sc2/ @Ziktofel # Note: @Ziktofel acts as a mentor
/worlds/sc2/ @MatthewMarinets @Snarkie @SirChuckOfTheChuckles
# Super Metroid # Super Metroid
/worlds/sm/ @lordlou /worlds/sm/ @lordlou

View File

@@ -31,3 +31,21 @@ components.append(
supports_uri=True, supports_uri=True,
) )
) )
# There are two optional parameters that are worth drawing attention to here: "game_name" and "supports_uri".
# As you might know, on a room page on WebHost, clicking a slot name opens your locally installed Launcher
# and asks you if you want to open a Text Client.
# If you have "game_name" set on your Component, your user also gets the option to open that instead.
# Furthermore, if you have "supports_uri" set to True, your Component will be passed a uri as an arg.
# This uri contains the room url + port, the slot name, and the password.
# You can process this uri arg to automatically connect the user to their slot without having to type anything.
# As you can see above, the APQuest client has both of these parameters set.
# This means a user can click on the slot name of an APQuest slot on WebHost,
# then click "APQuest Client" instead of "Text Client" in the Launcher popup, and after a few seconds,
# they will be connected and playing the game without having to touch their keyboard once.
# Since a Component is just Python code, this doesn't just work with CommonClient-derived clients.
# You could forward this uri arg to your standalone C++/Java/.NET/whatever client as well,
# meaning just about every client can support this "Click on slot name -> Everything happens automatically" action.
# The author would like to see more clients be aware of this feature and try to support it.

View File

@@ -158,11 +158,11 @@ class Game:
if not self.gameboard.ready: if not self.gameboard.ready:
return return
if self.active_math_problem is not None: if input_key in DIGIT_INPUTS_TO_DIGITS:
if input_key in DIGIT_INPUTS_TO_DIGITS: self.math_problem_input(DIGIT_INPUTS_TO_DIGITS[input_key])
self.math_problem_input(DIGIT_INPUTS_TO_DIGITS[input_key]) return
if input_key == Input.BACKSPACE: if input_key == Input.BACKSPACE:
self.math_problem_delete() self.math_problem_delete()
return return
if input_key == Input.LEFT: if input_key == Input.LEFT:

View File

@@ -1,6 +1,6 @@
from typing import Dict, List from typing import Dict, List
from .Technologies import factorio_base_id from .Technologies import factorio_base_id, recipes
from .Options import MaxSciencePack from .Options import MaxSciencePack
@@ -21,5 +21,18 @@ for pool in location_pools.values():
location_table.update({name: ap_id for ap_id, name in enumerate(pool, start=end_id)}) location_table.update({name: ap_id for ap_id, name in enumerate(pool, start=end_id)})
end_id += len(pool) end_id += len(pool)
craftsanity_locations = []
valid_items = []
item_category = {}
for recipe_name, recipe in recipes.items():
if not recipe_name.endswith(("-barrel", "-science-pack")):
for result in recipe.products:
if result not in valid_items:
valid_items.append(result)
for i, item in enumerate(valid_items, start=end_id):
location_table[f"Craft {item}"] = i
craftsanity_locations.append(f"Craft {item}")
end_id += 1
assert end_id - len(location_table) == factorio_base_id assert end_id - len(location_table) == factorio_base_id
del pool del pool

View File

@@ -112,7 +112,7 @@ def generate_mod(world: "Factorio", output_directory: str):
settings_template = template_env.get_template("settings.lua") settings_template = template_env.get_template("settings.lua")
# get data for templates # get data for templates
locations = [(location, location.item) locations = [(location, location.item)
for location in world.science_locations] for location in world.science_locations + world.craftsanity_locations]
mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.get_file_safe_player_name(player)}" mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.get_file_safe_player_name(player)}"
versioned_mod_name = mod_name + "_" + Utils.__version__ versioned_mod_name = mod_name + "_" + Utils.__version__

View File

@@ -6,7 +6,7 @@ import typing
from schema import Schema, Optional, And, Or, SchemaError from schema import Schema, Optional, And, Or, SchemaError
from Options import Choice, OptionDict, OptionSet, DefaultOnToggle, Range, DeathLink, Toggle, \ from Options import Choice, OptionDict, OptionSet, DefaultOnToggle, Range, DeathLink, Toggle, \
StartInventoryPool, PerGameCommonOptions, OptionGroup StartInventoryPool, PerGameCommonOptions, OptionGroup, NamedRange
# schema helpers # schema helpers
@@ -60,6 +60,20 @@ class Goal(Choice):
default = 0 default = 0
class CraftSanity(NamedRange):
"""Choose a number of researches to require crafting a specific item rather than with science packs.
May be capped based on the total number of locations.
There will always be at least 2 Science Pack research locations for automation and logistics, and 1 for rocket-silo
if the Rocket Silo option is not set to Spawn."""
display_name = "CraftSanity"
default = 0
range_start = 0
range_end = 183
special_range_names = {
"disabled": 0
}
class TechCost(Range): class TechCost(Range):
range_start = 1 range_start = 1
range_end = 10000 range_end = 10000
@@ -475,6 +489,7 @@ class EnergyLink(Toggle):
class FactorioOptions(PerGameCommonOptions): class FactorioOptions(PerGameCommonOptions):
max_science_pack: MaxSciencePack max_science_pack: MaxSciencePack
goal: Goal goal: Goal
craftsanity: CraftSanity
tech_tree_layout: TechTreeLayout tech_tree_layout: TechTreeLayout
min_tech_cost: MinTechCost min_tech_cost: MinTechCost
max_tech_cost: MaxTechCost max_tech_cost: MaxTechCost

View File

@@ -334,14 +334,15 @@ required_technologies: Dict[str, FrozenSet[Technology]] = Utils.KeyedDefaultDict
recursively_get_unlocking_technologies(ingredient_name, unlock_func=unlock))) recursively_get_unlocking_technologies(ingredient_name, unlock_func=unlock)))
def get_rocket_requirements(silo_recipe: Optional[Recipe], part_recipe: Recipe, def get_rocket_requirements(silo_recipe: Optional[Recipe], part_recipe: Optional[Recipe],
satellite_recipe: Optional[Recipe], cargo_landing_pad_recipe: Optional[Recipe]) -> Set[str]: satellite_recipe: Optional[Recipe], cargo_landing_pad_recipe: Optional[Recipe]) -> Set[str]:
techs = set() techs = set()
if silo_recipe: if silo_recipe:
for ingredient in silo_recipe.ingredients: for ingredient in silo_recipe.ingredients:
techs |= recursively_get_unlocking_technologies(ingredient) techs |= recursively_get_unlocking_technologies(ingredient)
for ingredient in part_recipe.ingredients: if part_recipe:
techs |= recursively_get_unlocking_technologies(ingredient) for ingredient in part_recipe.ingredients:
techs |= recursively_get_unlocking_technologies(ingredient)
if cargo_landing_pad_recipe: if cargo_landing_pad_recipe:
for ingredient in cargo_landing_pad_recipe.ingredients: for ingredient in cargo_landing_pad_recipe.ingredients:
techs |= recursively_get_unlocking_technologies(ingredient) techs |= recursively_get_unlocking_technologies(ingredient)

View File

@@ -9,7 +9,7 @@ from BaseClasses import Region, Location, Item, Tutorial, ItemClassification
from worlds.AutoWorld import World, WebWorld from worlds.AutoWorld import World, WebWorld
from worlds.LauncherComponents import Component, components, Type, launch as launch_component from worlds.LauncherComponents import Component, components, Type, launch as launch_component
from worlds.generic import Rules from worlds.generic import Rules
from .Locations import location_pools, location_table from .Locations import location_pools, location_table, craftsanity_locations
from .Mod import generate_mod from .Mod import generate_mod
from .Options import (FactorioOptions, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal, from .Options import (FactorioOptions, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal,
TechCostDistribution, option_groups) TechCostDistribution, option_groups)
@@ -88,6 +88,7 @@ class Factorio(World):
skip_silo: bool = False skip_silo: bool = False
origin_region_name = "Nauvis" origin_region_name = "Nauvis"
science_locations: typing.List[FactorioScienceLocation] science_locations: typing.List[FactorioScienceLocation]
craftsanity_locations: typing.List[FactorioCraftsanityLocation]
removed_technologies: typing.Set[str] removed_technologies: typing.Set[str]
settings: typing.ClassVar[FactorioSettings] settings: typing.ClassVar[FactorioSettings]
trap_names: tuple[str] = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery", trap_names: tuple[str] = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery",
@@ -100,6 +101,7 @@ class Factorio(World):
self.advancement_technologies = set() self.advancement_technologies = set()
self.custom_recipes = {} self.custom_recipes = {}
self.science_locations = [] self.science_locations = []
self.craftsanity_locations = []
self.tech_tree_layout_prerequisites = {} self.tech_tree_layout_prerequisites = {}
generate_output = generate_mod generate_output = generate_mod
@@ -127,17 +129,42 @@ class Factorio(World):
location_pool = [] location_pool = []
craftsanity_pool = [craft for craft in craftsanity_locations
if self.options.silo != Silo.option_spawn
or craft not in ["Craft rocket-silo", "Craft cargo-landing-pad"]]
# Ensure at least 2 science pack locations for automation and logistics, and 1 more for rocket-silo
# if it is not pre-spawned
craftsanity_count = min(self.options.craftsanity.value, len(craftsanity_pool),
location_count - (2 if self.options.silo == Silo.option_spawn else 3))
location_count -= craftsanity_count
for pack in sorted(self.options.max_science_pack.get_allowed_packs()): for pack in sorted(self.options.max_science_pack.get_allowed_packs()):
location_pool.extend(location_pools[pack]) location_pool.extend(location_pools[pack])
try: try:
location_names = random.sample(location_pool, location_count) # Ensure there are two "AP-1-" locations for automation and logistics, and one max science pack location
# for rocket-silo if it is not pre-spawned
max_science_pack_number = len(self.options.max_science_pack.get_allowed_packs())
science_location_names = None
while (not science_location_names or
len([location for location in science_location_names if location.startswith("AP-1-")]) < 2
or (self.options.silo != Silo.option_spawn and len([location for location in science_location_names
if location.startswith(f"AP-{max_science_pack_number}")]) < 1)):
science_location_names = random.sample(location_pool, location_count)
craftsanity_location_names = random.sample(craftsanity_pool, craftsanity_count)
except ValueError as e: except ValueError as e:
# should be "ValueError: Sample larger than population or is negative" # should be "ValueError: Sample larger than population or is negative"
raise Exception("Too many traps for too few locations. Either decrease the trap count, " raise Exception("Too many traps for too few locations. Either decrease the trap count, "
f"or increase the location count (higher max science pack). (Player {self.player})") from e f"or increase the location count (higher max science pack). (Player {self.player})") from e
self.science_locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis) self.science_locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis)
for loc_name in location_names] for loc_name in science_location_names]
self.craftsanity_locations = [FactorioCraftsanityLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis)
for loc_name in craftsanity_location_names]
distribution: TechCostDistribution = self.options.tech_cost_distribution distribution: TechCostDistribution = self.options.tech_cost_distribution
min_cost = self.options.min_tech_cost.value min_cost = self.options.min_tech_cost.value
max_cost = self.options.max_tech_cost.value max_cost = self.options.max_tech_cost.value
@@ -159,6 +186,7 @@ class Factorio(World):
location.count = rand_values[i] location.count = rand_values[i]
del rand_values del rand_values
nauvis.locations.extend(self.science_locations) nauvis.locations.extend(self.science_locations)
nauvis.locations.extend(self.craftsanity_locations)
location = FactorioLocation(player, "Rocket Launch", None, nauvis) location = FactorioLocation(player, "Rocket Launch", None, nauvis)
nauvis.locations.append(location) nauvis.locations.append(location)
event = FactorioItem("Victory", ItemClassification.progression, None, player) event = FactorioItem("Victory", ItemClassification.progression, None, player)
@@ -188,7 +216,7 @@ class Factorio(World):
loc: FactorioScienceLocation loc: FactorioScienceLocation
if self.options.tech_tree_information == TechTreeInformation.option_full: if self.options.tech_tree_information == TechTreeInformation.option_full:
# mark all locations as pre-hinted # mark all locations as pre-hinted
for loc in self.science_locations: for loc in self.science_locations + self.craftsanity_locations:
loc.revealed = True loc.revealed = True
if self.skip_silo: if self.skip_silo:
self.removed_technologies |= {"rocket-silo"} self.removed_technologies |= {"rocket-silo"}
@@ -236,6 +264,23 @@ class Factorio(World):
location.access_rule = lambda state, ingredient=ingredient: \ location.access_rule = lambda state, ingredient=ingredient: \
all(state.has(technology.name, player) for technology in required_technologies[ingredient]) all(state.has(technology.name, player) for technology in required_technologies[ingredient])
for location in self.craftsanity_locations:
if location.crafted_item == "crude-oil":
recipe = recipes["pumpjack"]
elif location.crafted_item in recipes:
recipe = recipes[location.crafted_item]
else:
for recipe_name, recipe in recipes.items():
if recipe_name.endswith("-barrel"):
continue
if location.crafted_item in recipe.products:
break
else:
raise Exception(
f"No recipe found for {location.crafted_item} for Craftsanity for player {self.player}")
location.access_rule = lambda state, recipe=recipe: \
state.has_all({technology.name for technology in recipe.recursive_unlocking_technologies}, player)
for location in self.science_locations: for location in self.science_locations:
Rules.set_rule(location, lambda state, ingredients=frozenset(location.ingredients): Rules.set_rule(location, lambda state, ingredients=frozenset(location.ingredients):
all(state.has(f"Automated {ingredient}", player) for ingredient in ingredients)) all(state.has(f"Automated {ingredient}", player) for ingredient in ingredients))
@@ -250,10 +295,11 @@ class Factorio(World):
silo_recipe = self.get_recipe("rocket-silo") silo_recipe = self.get_recipe("rocket-silo")
cargo_pad_recipe = self.get_recipe("cargo-landing-pad") cargo_pad_recipe = self.get_recipe("cargo-landing-pad")
part_recipe = self.custom_recipes["rocket-part"] part_recipe = self.custom_recipes["rocket-part"]
satellite_recipe = None satellite_recipe = self.get_recipe("satellite")
if self.options.goal == Goal.option_satellite: victory_tech_names = get_rocket_requirements(
satellite_recipe = self.get_recipe("satellite") silo_recipe, part_recipe,
victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe, satellite_recipe, cargo_pad_recipe) satellite_recipe if self.options.goal == Goal.option_satellite else None,
cargo_pad_recipe)
if self.options.silo == Silo.option_spawn: if self.options.silo == Silo.option_spawn:
victory_tech_names -= {"rocket-silo"} victory_tech_names -= {"rocket-silo"}
else: else:
@@ -263,6 +309,46 @@ class Factorio(World):
victory_tech_names) victory_tech_names)
self.multiworld.completion_condition[player] = lambda state: state.has('Victory', player) self.multiworld.completion_condition[player] = lambda state: state.has('Victory', player)
if "Craft rocket-silo" in self.multiworld.regions.location_cache[self.player]:
victory_tech_names_r = get_rocket_requirements(silo_recipe, None, None, None)
if self.options.silo == Silo.option_spawn:
victory_tech_names_r -= {"rocket-silo"}
else:
victory_tech_names_r |= {"rocket-silo"}
self.get_location("Craft rocket-silo").access_rule = lambda state: all(state.has(technology, player)
for technology in
victory_tech_names_r)
if "Craft rocket-part" in self.multiworld.regions.location_cache[self.player]:
victory_tech_names_p = get_rocket_requirements(silo_recipe, part_recipe, None, None)
if self.options.silo == Silo.option_spawn:
victory_tech_names_p -= {"rocket-silo"}
else:
victory_tech_names_p |= {"rocket-silo"}
self.get_location("Craft rocket-part").access_rule = lambda state: all(state.has(technology, player)
for technology in
victory_tech_names_p)
if "Craft satellite" in self.multiworld.regions.location_cache[self.player]:
victory_tech_names_s = get_rocket_requirements(None, None, satellite_recipe, None)
if self.options.silo == Silo.option_spawn:
victory_tech_names_s -= {"rocket-silo"}
else:
victory_tech_names_s |= {"rocket-silo"}
self.get_location("Craft satellite").access_rule = lambda state: all(state.has(technology, player)
for technology in
victory_tech_names_s)
if "Craft cargo-landing-pad" in self.multiworld.regions.location_cache[self.player]:
victory_tech_names_c = get_rocket_requirements(None, None, None, cargo_pad_recipe)
if self.options.silo == Silo.option_spawn:
victory_tech_names_c -= {"rocket-silo"}
else:
victory_tech_names_c |= {"rocket-silo"}
self.get_location("Craft cargo-landing-pad").access_rule = lambda state: all(state.has(technology, player)
for technology in
victory_tech_names_c)
def get_recipe(self, name: str) -> Recipe: def get_recipe(self, name: str) -> Recipe:
return self.custom_recipes[name] if name in self.custom_recipes \ return self.custom_recipes[name] if name in self.custom_recipes \
else next(iter(all_product_sources.get(name))) else next(iter(all_product_sources.get(name)))
@@ -486,9 +572,17 @@ class Factorio(World):
needed_recipes = self.options.max_science_pack.get_allowed_packs() | {"rocket-part"} needed_recipes = self.options.max_science_pack.get_allowed_packs() | {"rocket-part"}
if self.options.silo != Silo.option_spawn: if self.options.silo != Silo.option_spawn:
needed_recipes |= {"rocket-silo", "cargo-landing-pad"} needed_recipes |= {"rocket-silo", "cargo-landing-pad"}
if self.options.goal.value == Goal.option_satellite: if (self.options.goal.value == Goal.option_satellite
or "Craft satellite" in self.multiworld.regions.location_cache[self.player]):
needed_recipes |= {"satellite"} needed_recipes |= {"satellite"}
needed_items = {location.crafted_item for location in self.craftsanity_locations}
for recipe_name, recipe in recipes.items():
for product in recipe.products:
if product in needed_items:
self.advancement_technologies |= {tech.name for tech in recipe.recursive_unlocking_technologies}
break
for recipe in needed_recipes: for recipe in needed_recipes:
recipe = self.custom_recipes.get(recipe, recipes[recipe]) recipe = self.custom_recipes.get(recipe, recipes[recipe])
self.advancement_technologies |= {tech.name for tech in recipe.recursive_unlocking_technologies} self.advancement_technologies |= {tech.name for tech in recipe.recursive_unlocking_technologies}
@@ -520,9 +614,23 @@ class FactorioLocation(Location):
game: str = Factorio.game game: str = Factorio.game
class FactorioCraftsanityLocation(FactorioLocation):
ingredients = {}
count = 0
revealed = False
def __init__(self, player: int, name: str, address: int, parent: Region):
super(FactorioCraftsanityLocation, self).__init__(player, name, address, parent)
@property
def crafted_item(self):
return " ".join(self.name.split(" ")[1:])
class FactorioScienceLocation(FactorioLocation): class FactorioScienceLocation(FactorioLocation):
complexity: int complexity: int
revealed: bool = False revealed: bool = False
crafted_item = None
# Factorio technology properties: # Factorio technology properties:
ingredients: typing.Dict[str, int] ingredients: typing.Dict[str, int]

View File

@@ -63,22 +63,6 @@ template_tech.upgrade = false
template_tech.effects = {} template_tech.effects = {}
template_tech.prerequisites = {} template_tech.prerequisites = {}
{%- if max_science_pack < 6 %}
technologies["space-science-pack"].effects = {}
{%- if max_science_pack == 0 %}
table.insert (technologies["automation"].effects, {type = "unlock-recipe", recipe = "satellite"})
{%- elif max_science_pack == 1 %}
table.insert (technologies["logistic-science-pack"].effects, {type = "unlock-recipe", recipe = "satellite"})
{%- elif max_science_pack == 2 %}
table.insert (technologies["military-science-pack"].effects, {type = "unlock-recipe", recipe = "satellite"})
{%- elif max_science_pack == 3 %}
table.insert (technologies["chemical-science-pack"].effects, {type = "unlock-recipe", recipe = "satellite"})
{%- elif max_science_pack == 4 %}
table.insert (technologies["production-science-pack"].effects, {type = "unlock-recipe", recipe = "satellite"})
{%- elif max_science_pack == 5 %}
table.insert (technologies["utility-science-pack"].effects, {type = "unlock-recipe", recipe = "satellite"})
{% endif %}
{% endif %}
{%- if silo == 2 %} {%- if silo == 2 %}
data.raw["recipe"]["rocket-silo"].enabled = true data.raw["recipe"]["rocket-silo"].enabled = true
{% endif %} {% endif %}
@@ -169,9 +153,16 @@ technologies["{{ original_tech_name }}"].hidden_in_factoriopedia = true
{#- the tech researched by the local player #} {#- the tech researched by the local player #}
new_tree_copy = table.deepcopy(template_tech) new_tree_copy = table.deepcopy(template_tech)
new_tree_copy.name = "ap-{{ location.address }}-"{# use AP ID #} new_tree_copy.name = "ap-{{ location.address }}-"{# use AP ID #}
{% if location.crafted_item is not none %}
new_tree_copy.research_trigger = {
type = "{{ 'craft-fluid' if location.crafted_item in liquids else 'craft-item' }}",
{{ 'fluid' if location.crafted_item in liquids else 'item' }} = {{ variable_to_lua(location.crafted_item) }}
}
new_tree_copy.unit = nil
{% else %}
new_tree_copy.unit.count = {{ location.count }} new_tree_copy.unit.count = {{ location.count }}
new_tree_copy.unit.ingredients = {{ variable_to_lua(location.factorio_ingredients) }} new_tree_copy.unit.ingredients = {{ variable_to_lua(location.factorio_ingredients) }}
{% endif %}
{%- if location.revealed and item.name in base_tech_table -%} {%- if location.revealed and item.name in base_tech_table -%}
{#- copy Factorio Technology Icon #} {#- copy Factorio Technology Icon #}
copy_factorio_icon(new_tree_copy, "{{ item.name }}") copy_factorio_icon(new_tree_copy, "{{ item.name }}")

View File

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

View File

@@ -20,7 +20,7 @@ class Dungeon1:
if options.owlstatues == "both" or options.owlstatues == "dungeon": if options.owlstatues == "both" or options.owlstatues == "dungeon":
Location(dungeon=1).add(OwlStatue(0x10A)).connect(dungeon1_right_side, STONE_BEAK1) 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_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) 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]]) 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': if options.logic == 'glitched' or options.logic == 'hell':
boss_key.connect(entrance, r.super_jump_feather) # super jump 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': 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 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 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": if options.owlstatues == "both" or options.owlstatues == "dungeon":
Location(dungeon=2).add(OwlStatue(0x12F)).connect(dungeon2_r5, STONE_BEAK2) # owl statue is before miniboss 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": if options.owlstatues == "both" or options.owlstatues == "dungeon":
Location(dungeon=2).add(OwlStatue(0x129)).connect(miniboss, STONE_BEAK2) # owl statue after the miniboss 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_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(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 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_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_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 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. 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_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 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 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 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 = 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(0x16D)) # gel chest
left_water_area.add(DungeonChest(0x168)) # key chest near the puzzle 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 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 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 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(OwlStatue(0x19A)).connect(area2, STONE_BEAK5)
Location(dungeon=5).add(DungeonChest(0x19B)).connect(area2, r.attack_hookshot_powder) # map chest 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 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 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_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 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 # right side
to_miniboss = Location(dungeon=6).connect(entrance, KEY6) 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 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 medicine_chest = Location(dungeon=6).add(DungeonChest(0x1D1)).connect(lower_right_side, FEATHER) # ledge chest medicine
if options.owlstatues == "both" or options.owlstatues == "dungeon": 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. # Most of the dungeon can be accessed at this point.
if options.owlstatues == "both" or options.owlstatues == "dungeon": if options.owlstatues == "both" or options.owlstatues == "dungeon":
bottomleft_owl = Location(dungeon=7).add(OwlStatue(0x21C)).connect(bottomleftF2_area, AND(BOMB, STONE_BEAK7)) 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 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 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 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_2 = Location(dungeon=8).connect(middle_center_1, AND(KEY8, FOUND(KEY8, 4)))
middle_center_3 = Location(dungeon=8).connect(middle_center_2, KEY8) 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_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 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)))) 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 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 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 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 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 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 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][0x1400+n*0x20:0x1410+n*0x20] = utils.createTileData(gfx_high)
rom.banks[0x38][0x1410+n*0x20:0x1420+n*0x20] = utils.createTileData(gfx_low) rom.banks[0x38][0x1410+n*0x20:0x1420+n*0x20] = utils.createTileData(gfx_low)
def addBootsControls(rom, bootscontrols): def addBootsControls(rom, boots_controls: int):
if bootscontrols == 'vanilla': if boots_controls == BootsControls.option_vanilla:
return return
consts = { consts = {
"INVENTORY_PEGASUS_BOOTS": 0x8, "INVENTORY_PEGASUS_BOOTS": 0x8,
@@ -560,25 +560,25 @@ def addBootsControls(rom, bootscontrols):
BOOTS_START_ADDR = 0x11E8 BOOTS_START_ADDR = 0x11E8
condition = { condition = {
'bracelet': """ BootsControls.option_bracelet: """
ld a, [hl] ld a, [hl]
; Check if we are using the bracelet ; Check if we are using the bracelet
cp INVENTORY_POWER_BRACELET cp INVENTORY_POWER_BRACELET
jr z, .yesBoots jr z, .yesBoots
""", """,
'pressa': """ BootsControls.option_press_a: """
; Check if we are using the A slot ; Check if we are using the A slot
cp J_A cp J_A
jr z, .yesBoots jr z, .yesBoots
ld a, [hl] ld a, [hl]
""", """,
'pressb': """ BootsControls.option_press_b: """
; Check if we are using the B slot ; Check if we are using the B slot
cp J_B cp J_B
jr z, .yesBoots jr z, .yesBoots
ld a, [hl] ld a, [hl]
""" """
}[bootscontrols] }[boots_controls]
# The new code fits exactly within Nintendo's poorly space optimzied code while having more features # The new code fits exactly within Nintendo's poorly space optimzied code while having more features
boots_code = assembler.ASM(""" boots_code = assembler.ASM("""

View File

@@ -387,7 +387,7 @@ def patchVarious(rom, settings):
# Boomerang trade guy # Boomerang trade guy
# if settings.boomerang not in {'trade', 'gift'} or settings.overworld in {'normal', 'nodungeons'}: # if settings.boomerang not in {'trade', 'gift'} or settings.overworld in {'normal', 'nodungeons'}:
if settings.tradequest: if settings["tradequest"]:
# Update magnifier checks # 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(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 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: class Settings:
def __init__(self, settings_dict): def __init__(self, ap_options):
self.__all = [ self.__all = [
Setting('seed', 'Main', '<', 'Seed', placeholder='Leave empty for random seed', default="", multiworld=False, 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. description="""For multiple people to generate the same randomization result, enter the generated seed number here.
@@ -178,14 +178,6 @@ 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'), description='Replaces the hints from owl statues with additional randomized items'),
Setting('superweapons', 'Special', 'q', 'Enable super weapons', default=False, Setting('superweapons', 'Special', 'q', 'Enable super weapons', default=False,
description='All items will be more powerful, faster, harder, bigger stronger. You name it.'), 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', 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.', 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), aesthetic=True),
@@ -200,7 +192,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, Setting('nagmessages', 'User options', 'S', 'Show nag messages', default=False,
description='Enables the nag messages normally shown when touching stones and crystals', description='Enables the nag messages normally shown when touching stones and crystals',
aesthetic=True), aesthetic=True),
Setting('gfxmod', 'User options', 'c', 'Graphics', default=False, Setting('gfxmod', 'User options', 'c', 'Graphics', default='',
description='Generally affects at least Link\'s sprite, but can alter any graphics in the game', description='Generally affects at least Link\'s sprite, but can alter any graphics in the game',
aesthetic=True), aesthetic=True),
Setting('linkspalette', 'User options', 'C', "Link's color", Setting('linkspalette', 'User options', 'C', "Link's color",
@@ -210,31 +202,25 @@ Note, some entrances can lead into water, use the warp-to-home from the save&qui
[Normal] color of link depends on the tunic. [Normal] color of link depends on the tunic.
[Green/Yellow/Red/Blue] forces link into one of these colors. [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."""), [?? 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='', Setting('music', 'User options', 'M', 'Music', options=[('', '', 'Default'), ('random', 'r', 'Random'), ('off', 'o', 'Disable')], default='',
description=""" description="""
[Random] Randomizes overworld and dungeon music' [Random] Randomizes overworld and dungeon music'
[Disable] no music in the whole game""", [Disable] no music in the whole game""",
aesthetic=True), 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} self.__by_key = {s.key: s for s in self.__all}
# don't worry about unique short keys for AP # Make sure all short keys are unique
#short_keys = set() short_keys = set()
#for s in self.__all: for s in self.__all:
# assert s.short_key not in short_keys, s.label assert s.short_key not in short_keys, s.label
# short_keys.add(s.short_key) short_keys.add(s.short_key)
self.ap_options = ap_options
for name, value in settings_dict.items(): for option in self.ap_options.values():
if not hasattr(option, 'to_ladxr_option'):
continue
name, value = option.to_ladxr_option(self.ap_options)
if value == "true": if value == "true":
value = 1 value = 1
elif value == "false": elif value == "false":

View File

@@ -28,7 +28,7 @@ class WorldSetup:
self.boss_mapping = list(range(9)) self.boss_mapping = list(range(9))
self.miniboss_mapping = { self.miniboss_mapping = {
# Main minibosses # 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. # Color dungeon needs to be special, as always.
"c1": "AVALAUNCH", "c2": "GIANT_BUZZ_BLOB", "c1": "AVALAUNCH", "c2": "GIANT_BUZZ_BLOB",
# Overworld # Overworld

View File

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

View File

@@ -6,11 +6,13 @@ import json
import pkgutil import pkgutil
import bsdiff4 import bsdiff4
import binascii import binascii
import pickle
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from .Common import * from .Common import *
from .LADXR import generator from .LADXR import generator
from .LADXR.main import get_parser from .LADXR.main import get_parser
from .LADXR.hints import generate_hint_texts from .LADXR.hints import generate_hint_texts
from .LADXR.locations.keyLocation import KeyLocation
LADX_HASH = "07c211479386825042efb4ad31bb525f" LADX_HASH = "07c211479386825042efb4ad31bb525f"
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -33,7 +35,7 @@ class LADXPatchExtensions(worlds.Files.APPatchExtension):
@staticmethod @staticmethod
def patch_title_screen(caller: worlds.Files.APProcedurePatch, rom: bytes, data_file: str) -> bytes: 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")) patch_data = json.loads(caller.get_file(data_file).decode("utf-8"))
if patch_data["ladxr_settings_dict"]["aptitlescreen"] == 'true': if patch_data["options"]["ap_title_screen"]:
return bsdiff4.patch(rom, pkgutil.get_data(__name__, "LADXR/patches/title_screen.bdiff4")) return bsdiff4.patch(rom, pkgutil.get_data(__name__, "LADXR/patches/title_screen.bdiff4"))
return rom return rom
@@ -54,6 +56,7 @@ class LADXProcedurePatch(worlds.Files.APProcedurePatch):
def write_patch_data(world: "LinksAwakeningWorld", patch: LADXProcedurePatch): 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 = { data_dict = {
"generated_world_version": world.world_version.as_simple_string(), "generated_world_version": world.world_version.as_simple_string(),
"out_base": world.multiworld.get_out_file_name_base(patch.player), "out_base": world.multiworld.get_out_file_name_base(patch.player),
@@ -64,16 +67,44 @@ def write_patch_data(world: "LinksAwakeningWorld", patch: LADXProcedurePatch):
"player": patch.player, "player": patch.player,
"player_name": patch.player_name, "player_name": patch.player_name,
"other_player_names": list(world.multiworld.player_name.values()), "other_player_names": list(world.multiworld.player_name.values()),
"rom_item_placements": world.rom_item_placements, "item_list": binascii.hexlify(item_list).decode(),
"hint_texts": generate_hint_texts(world), "hint_texts": generate_hint_texts(world),
"world_setup": { "world_setup": {
"goal": world.ladxr_logic.world_setup.goal, "goal": world.ladxr_logic.world_setup.goal,
"bingo_goals": world.ladxr_logic.world_setup.bingo_goals,
"multichest": world.ladxr_logic.world_setup.multichest, "multichest": world.ladxr_logic.world_setup.multichest,
"entrance_mapping": world.ladxr_logic.world_setup.entrance_mapping, "entrance_mapping": world.ladxr_logic.world_setup.entrance_mapping,
"boss_mapping": world.ladxr_logic.world_setup.boss_mapping, "boss_mapping": world.ladxr_logic.world_setup.boss_mapping,
"miniboss_mapping": world.ladxr_logic.world_setup.miniboss_mapping, "miniboss_mapping": world.ladxr_logic.world_setup.miniboss_mapping,
}, },
"ladxr_settings_dict": world.ladxr_settings_dict, "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",
),
} }
patch.write_file("data.json", json.dumps(data_dict).encode('utf-8')) patch.write_file("data.json", json.dumps(data_dict).encode('utf-8'))

View File

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

View File

@@ -0,0 +1,5 @@
{
"game": "The Legend of Zelda",
"world_version": "1.0.0",
"authors": ["Rosalie"]
}