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
13 changed files with 203 additions and 47 deletions

View File

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

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

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

View File

@@ -31,3 +31,21 @@ components.append(
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:
return
if self.active_math_problem is not None:
if input_key in DIGIT_INPUTS_TO_DIGITS:
self.math_problem_input(DIGIT_INPUTS_TO_DIGITS[input_key])
if input_key == Input.BACKSPACE:
self.math_problem_delete()
if input_key in DIGIT_INPUTS_TO_DIGITS:
self.math_problem_input(DIGIT_INPUTS_TO_DIGITS[input_key])
return
if input_key == Input.BACKSPACE:
self.math_problem_delete()
return
if input_key == Input.LEFT:

View File

@@ -1,6 +1,6 @@
from typing import Dict, List
from .Technologies import factorio_base_id
from .Technologies import factorio_base_id, recipes
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)})
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
del pool

View File

@@ -112,7 +112,7 @@ def generate_mod(world: "Factorio", output_directory: str):
settings_template = template_env.get_template("settings.lua")
# get data for templates
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)}"
versioned_mod_name = mod_name + "_" + Utils.__version__

View File

@@ -6,7 +6,7 @@ import typing
from schema import Schema, Optional, And, Or, SchemaError
from Options import Choice, OptionDict, OptionSet, DefaultOnToggle, Range, DeathLink, Toggle, \
StartInventoryPool, PerGameCommonOptions, OptionGroup
StartInventoryPool, PerGameCommonOptions, OptionGroup, NamedRange
# schema helpers
@@ -60,6 +60,20 @@ class Goal(Choice):
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):
range_start = 1
range_end = 10000
@@ -475,6 +489,7 @@ class EnergyLink(Toggle):
class FactorioOptions(PerGameCommonOptions):
max_science_pack: MaxSciencePack
goal: Goal
craftsanity: CraftSanity
tech_tree_layout: TechTreeLayout
min_tech_cost: MinTechCost
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)))
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]:
techs = set()
if silo_recipe:
for ingredient in silo_recipe.ingredients:
techs |= recursively_get_unlocking_technologies(ingredient)
for ingredient in part_recipe.ingredients:
techs |= recursively_get_unlocking_technologies(ingredient)
if part_recipe:
for ingredient in part_recipe.ingredients:
techs |= recursively_get_unlocking_technologies(ingredient)
if cargo_landing_pad_recipe:
for ingredient in cargo_landing_pad_recipe.ingredients:
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.LauncherComponents import Component, components, Type, launch as launch_component
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 .Options import (FactorioOptions, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal,
TechCostDistribution, option_groups)
@@ -88,6 +88,7 @@ class Factorio(World):
skip_silo: bool = False
origin_region_name = "Nauvis"
science_locations: typing.List[FactorioScienceLocation]
craftsanity_locations: typing.List[FactorioCraftsanityLocation]
removed_technologies: typing.Set[str]
settings: typing.ClassVar[FactorioSettings]
trap_names: tuple[str] = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery",
@@ -100,6 +101,7 @@ class Factorio(World):
self.advancement_technologies = set()
self.custom_recipes = {}
self.science_locations = []
self.craftsanity_locations = []
self.tech_tree_layout_prerequisites = {}
generate_output = generate_mod
@@ -127,17 +129,42 @@ class Factorio(World):
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()):
location_pool.extend(location_pools[pack])
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:
# should be "ValueError: Sample larger than population or is negative"
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
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
min_cost = self.options.min_tech_cost.value
max_cost = self.options.max_tech_cost.value
@@ -159,6 +186,7 @@ class Factorio(World):
location.count = rand_values[i]
del rand_values
nauvis.locations.extend(self.science_locations)
nauvis.locations.extend(self.craftsanity_locations)
location = FactorioLocation(player, "Rocket Launch", None, nauvis)
nauvis.locations.append(location)
event = FactorioItem("Victory", ItemClassification.progression, None, player)
@@ -188,7 +216,7 @@ class Factorio(World):
loc: FactorioScienceLocation
if self.options.tech_tree_information == TechTreeInformation.option_full:
# mark all locations as pre-hinted
for loc in self.science_locations:
for loc in self.science_locations + self.craftsanity_locations:
loc.revealed = True
if self.skip_silo:
self.removed_technologies |= {"rocket-silo"}
@@ -236,6 +264,23 @@ class Factorio(World):
location.access_rule = lambda state, ingredient=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:
Rules.set_rule(location, lambda state, ingredients=frozenset(location.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")
cargo_pad_recipe = self.get_recipe("cargo-landing-pad")
part_recipe = self.custom_recipes["rocket-part"]
satellite_recipe = None
if self.options.goal == Goal.option_satellite:
satellite_recipe = self.get_recipe("satellite")
victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe, satellite_recipe, cargo_pad_recipe)
satellite_recipe = self.get_recipe("satellite")
victory_tech_names = get_rocket_requirements(
silo_recipe, part_recipe,
satellite_recipe if self.options.goal == Goal.option_satellite else None,
cargo_pad_recipe)
if self.options.silo == Silo.option_spawn:
victory_tech_names -= {"rocket-silo"}
else:
@@ -263,6 +309,46 @@ class Factorio(World):
victory_tech_names)
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:
return self.custom_recipes[name] if name in self.custom_recipes \
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"}
if self.options.silo != Silo.option_spawn:
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_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:
recipe = self.custom_recipes.get(recipe, recipes[recipe])
self.advancement_technologies |= {tech.name for tech in recipe.recursive_unlocking_technologies}
@@ -520,9 +614,23 @@ class FactorioLocation(Location):
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):
complexity: int
revealed: bool = False
crafted_item = None
# Factorio technology properties:
ingredients: typing.Dict[str, int]

View File

@@ -63,22 +63,6 @@ template_tech.upgrade = false
template_tech.effects = {}
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 %}
data.raw["recipe"]["rocket-silo"].enabled = true
{% endif %}
@@ -169,9 +153,16 @@ technologies["{{ original_tech_name }}"].hidden_in_factoriopedia = true
{#- the tech researched by the local player #}
new_tree_copy = table.deepcopy(template_tech)
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.ingredients = {{ variable_to_lua(location.factorio_ingredients) }}
{% endif %}
{%- if location.revealed and item.name in base_tech_table -%}
{#- copy Factorio Technology Icon #}
copy_factorio_icon(new_tree_copy, "{{ item.name }}")

View File

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