Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
0de09cd
Core: Better scaling explicit indirect conditions (#4582)
Mysteryem Feb 21, 2026
d83da1b
WebHost: memory leak fixes (#5966)
Berserker66 Feb 22, 2026
fefd790
ALTTP: remove `world: MultiWorld` and typing (#5974)
Silvris Feb 24, 2026
699ca8a
WebHost: Add CORS headers to API Endpoints (#5777)
DrAwesome4333 Feb 25, 2026
b30b2ec
Return new state man (Vi's note: I have chosen not to change this tit…
duckboycool Feb 25, 2026
eeb022f
The Messenger: minor maintenance (#5965)
alwaysintreble Feb 26, 2026
2db5435
CI: upgrade InnoSetup to 6.7.0 (#5979)
black-sliver Feb 26, 2026
fcccbfc
MultiServer: don't keep multidata alive for race_mode (#5980)
black-sliver Feb 26, 2026
ff5402c
Fix(undertale): prevent massive bounce msg spam for position updates …
TreZc0 Feb 28, 2026
61d5120
Core: use typing_extensions `deprecated` (#5989)
beauxq Feb 28, 2026
e49ba2f
Undertale: Use check_locations helper to avoid redundant sends (#5993)
duckboycool Mar 1, 2026
922c7fe
Core: allow async def functions as commands (#5859)
Berserker66 Mar 1, 2026
a3e8f69
Core: introduce finalize_multiworld and pre_output stages (#5700)
Berserker66 Mar 1, 2026
f263133
MultiServer: graceful shutdown for ctrl+c and sigterm (#5996)
black-sliver Mar 3, 2026
b372b02
OptionCreator: 0.6.6 reported issues (#5949)
Silvris Mar 4, 2026
3ecd856
MultiServer: fix Windows compatibility (#6010)
Silvris Mar 6, 2026
b53f9d3
Docs: Better document state.locations_checked (#6018)
qwint Mar 7, 2026
366fd37
MM2: Fix /request command help (#5805)
Suyooo Mar 8, 2026
9f29859
MLSS: Fix client auto-connect bug + Client cleanup (#5895)
jamesbrq Mar 8, 2026
9efcba5
FF1: Added manifest (#5911)
Rosalie-A Mar 8, 2026
fc2cb3c
OoT: change setup-guides to have 2.10 be the minimum version recommen…
StripesOO7 Mar 8, 2026
a8ac828
Pokemon Emerald: Fix rare fuzzer errors (#5914)
Zunawe Mar 8, 2026
b38548f
Shivers: Adds Manifest File (#5918)
GodlFire Mar 8, 2026
53956b7
OOT: UTC deprecation warning fix (#5983)
josephwhite Mar 8, 2026
99601cc
Saving Princess: add manifest (#6008)
LeonarthCG Mar 8, 2026
4bb6cac
Lingo: Add archipelago.json (#6017)
hatkirby Mar 8, 2026
5b99118
Mega Man 3: Implement new game (#5237)
Silvris Mar 8, 2026
371db53
Stardew Valley: morel doesn't spawn in fall secret woods (#6003)
itepastra Mar 8, 2026
44e4243
Docs: Don't serve non-static files in example_nginx.conf (#5971)
remyjette Mar 9, 2026
123e1f5
Lingo: Fix logic for Near Eight Painting (#6014)
hatkirby Mar 9, 2026
0b6ba10
The Messenger: Universal Tracker support (#5344)
Jouramie Mar 10, 2026
07a1ec0
Test: Defaults for Options test (#5428)
josephwhite Mar 10, 2026
2c279ce
Muse Dash: Adds 3 new music packs plus fixes being able to roll songs…
DeamonHunter Mar 10, 2026
fd81553
Fix missing } in example_nginx.conf (#6027)
remyjette Mar 10, 2026
c255ea8
Pokemon Emerald: Dexsanity Encounter Type Option (#6016)
Goo-Dang Mar 10, 2026
1a8a71f
Dark Souls 3: Update location descriptions for Red Tearstone Ring and…
richarm4 Mar 10, 2026
c3659fb
TUNIC: Refactor entrance hint generation (#5620)
ScipioWright Mar 10, 2026
4b37283
WebHost: Update UTC datetime usage (timezone-naive) (#4906)
josephwhite Mar 10, 2026
72ff9b1
Saving Princess: Security fixes for issues detected by Bandit (#6013)
LeonarthCG Mar 10, 2026
94136ac
Docs: Add references to running from source (#6022)
duckboycool Mar 10, 2026
d000c0f
Docs: Update plando_en.md with item group example (#6024)
Gryphonlady Mar 10, 2026
f00d29e
Tests: fix race in test hosting shutdown (#5987)
black-sliver Mar 10, 2026
3235863
WebHost: add stats show cli command (#5995)
black-sliver Mar 10, 2026
47e581b
LttP: add manifest (#6005)
Berserker66 Mar 10, 2026
56c2272
RoR2: Seekers of the Storm (SOTS) DLC Support (#5569)
kindasneaki Mar 10, 2026
a8e926a
Core: Make Generic ER only consider the current world in isolation (#…
Mysteryem Mar 10, 2026
3c802d0
DS3: Use remaining_fill instead of custom fill (#4397)
Exempt-Medic Mar 10, 2026
03b638d
Docs: Reword 'could be generated from json' to avoid encouraging slow…
Ixrec Mar 10, 2026
3016379
KH2: Fix nondeterministic generation when CasualBounties is enabled (…
Mysteryem Mar 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:
run: |
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip
Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force
choco install innosetup --version=6.2.2 --allow-downgrade
choco install innosetup --version=6.7.0 --allow-downgrade
- name: Build
run: |
python -m pip install --upgrade pip
Expand Down
9 changes: 6 additions & 3 deletions BaseClasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,7 @@ class CollectionState():
advancements: Set[Location]
path: Dict[Union[Region, Entrance], PathValue]
locations_checked: Set[Location]
"""Internal cache for Advancement Locations already checked by this CollectionState. Not for use in logic."""
stale: Dict[int, bool]
allow_partial_entrances: bool
additional_init_functions: List[Callable[[CollectionState, MultiWorld], None]] = []
Expand Down Expand Up @@ -788,9 +789,11 @@ def _update_reachable_regions_explicit_indirect_conditions(self, player: int, qu
self.multiworld.worlds[player].reached_region(self, new_region)

# Retry connections if the new region can unblock them
for new_entrance in self.multiworld.indirect_connections.get(new_region, set()):
if new_entrance in blocked_connections and new_entrance not in queue:
queue.append(new_entrance)
entrances = self.multiworld.indirect_connections.get(new_region)
if entrances is not None:
relevant_entrances = entrances.intersection(blocked_connections)
relevant_entrances.difference_update(queue)
queue.extend(relevant_entrances)

def _update_reachable_regions_auto_indirect_conditions(self, player: int, queue: deque[Entrance]):
reachable_regions = self.reachable_regions[player]
Expand Down
1 change: 1 addition & 0 deletions Fill.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ def location_can_fill_item(location_to_fill: Location, item_to_fill: Item):
item_to_place = itempool.pop()
spot_to_fill: typing.Optional[Location] = None

# going through locations in the same order as the provided `locations` argument
for i, location in enumerate(locations):
if location_can_fill_item(location, item_to_place):
# popping by index is faster than removing by content,
Expand Down
3 changes: 3 additions & 0 deletions Main.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,9 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None)
else:
logger.info("Progression balancing skipped.")

AutoWorld.call_all(multiworld, "finalize_multiworld")
AutoWorld.call_all(multiworld, "pre_output")

# we're about to output using multithreading, so we're removing the global random state to prevent accidental use
multiworld.random.passthrough = False

Expand Down
33 changes: 32 additions & 1 deletion MultiServer.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import typing
import weakref
import zlib
from signal import SIGINT, SIGTERM, signal

import ModuleUpdate

Expand Down Expand Up @@ -496,7 +497,8 @@ def _load(self, decoded_obj: MultiData, game_data_packages: typing.Dict[str, typ

self.read_data = {}
# there might be a better place to put this.
self.read_data["race_mode"] = lambda: decoded_obj.get("race_mode", 0)
race_mode = decoded_obj.get("race_mode", 0)
self.read_data["race_mode"] = lambda: race_mode
mdata_ver = decoded_obj["minimum_versions"]["server"]
if mdata_ver > version_tuple:
raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver}, "
Expand Down Expand Up @@ -1301,6 +1303,13 @@ def __new__(cls, name, bases, attrs):
commands.update(base.commands)
commands.update({command_name[5:]: method for command_name, method in attrs.items() if
command_name.startswith("_cmd_")})
for command_name, method in commands.items():
# wrap async def functions so they run on default asyncio loop
if inspect.iscoroutinefunction(method):
def _wrapper(self, *args, _method=method, **kwargs):
return async_start(_method(self, *args, **kwargs))
functools.update_wrapper(_wrapper, method)
commands[command_name] = _wrapper
return super(CommandMeta, cls).__new__(cls, name, bases, attrs)


Expand Down Expand Up @@ -2563,6 +2572,8 @@ async def console(ctx: Context):
input_text = await queue.get()
queue.task_done()
ctx.commandprocessor(input_text)
except asyncio.exceptions.CancelledError:
ctx.logger.info("ConsoleTask cancelled")
except:
import traceback
traceback.print_exc()
Expand Down Expand Up @@ -2729,6 +2740,26 @@ async def main(args: argparse.Namespace):
console_task = asyncio.create_task(console(ctx))
if ctx.auto_shutdown:
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, [console_task]))

def stop():
try:
for remove_signal in [SIGINT, SIGTERM]:
asyncio.get_event_loop().remove_signal_handler(remove_signal)
except NotImplementedError:
pass
ctx.commandprocessor._cmd_exit()

def shutdown(signum, frame):
stop()

try:
for sig in [SIGINT, SIGTERM]:
asyncio.get_event_loop().add_signal_handler(sig, stop)
except NotImplementedError:
# add_signal_handler is only implemented for UNIX platforms
for sig in [SIGINT, SIGTERM]:
signal(sig, shutdown)

await ctx.exit_event.wait()
console_task.cancel()
if ctx.shutdown_task:
Expand Down
58 changes: 35 additions & 23 deletions OptionsCreator.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
import re
from urllib.parse import urlparse
from worlds.AutoWorld import AutoWorldRegister, World
from Options import (Option, Toggle, TextChoice, Choice, FreeText, NamedRange, Range, OptionSet, OptionList, Removed,
from Options import (Option, Toggle, TextChoice, Choice, FreeText, NamedRange, Range, OptionSet, OptionList,
OptionCounter, Visibility)


Expand Down Expand Up @@ -318,26 +318,28 @@ def export_options(self, button: Widget) -> None:
else:
self.show_result_snack("Name cannot be longer than 16 characters.")

def create_range(self, option: typing.Type[Range], name: str):
def create_range(self, option: typing.Type[Range], name: str, bind=True):
def update_text(range_box: VisualRange):
self.options[name] = int(range_box.slider.value)
range_box.tag.text = str(int(range_box.slider.value))
return

box = VisualRange(option=option, name=name)
box.slider.bind(on_touch_move=lambda _, _1: update_text(box))
if bind:
box.slider.bind(value=lambda _, _1: update_text(box))
self.options[name] = option.default
return box

def create_named_range(self, option: typing.Type[NamedRange], name: str):
def set_to_custom(range_box: VisualNamedRange):
if (not self.options[name] == range_box.range.slider.value) \
and (not self.options[name] in option.special_range_names or
range_box.range.slider.value != option.special_range_names[self.options[name]]):
# we should validate the touch here,
# but this is much cheaper
range_box.range.tag.text = str(int(range_box.range.slider.value))
if range_box.range.slider.value in option.special_range_names.values():
value = next(key for key, val in option.special_range_names.items()
if val == range_box.range.slider.value)
self.options[name] = value
set_button_text(box.choice, value.title())
else:
self.options[name] = int(range_box.range.slider.value)
range_box.range.tag.text = str(int(range_box.range.slider.value))
set_button_text(range_box.choice, "Custom")

def set_button_text(button: MDButton, text: str):
Expand All @@ -346,7 +348,7 @@ def set_button_text(button: MDButton, text: str):
def set_value(text: str, range_box: VisualNamedRange):
range_box.range.slider.value = min(max(option.special_range_names[text.lower()], option.range_start),
option.range_end)
range_box.range.tag.text = str(int(range_box.range.slider.value))
range_box.range.tag.text = str(option.special_range_names[text.lower()])
set_button_text(range_box.choice, text)
self.options[name] = text.lower()
range_box.range.slider.dropdown.dismiss()
Expand All @@ -355,13 +357,18 @@ def open_dropdown(button):
# for some reason this fixes an issue causing some to not open
box.range.slider.dropdown.open()

box = VisualNamedRange(option=option, name=name, range_widget=self.create_range(option, name))
if option.default in option.special_range_names:
box = VisualNamedRange(option=option, name=name, range_widget=self.create_range(option, name, bind=False))
default: int | str = option.default
if default in option.special_range_names:
# value can get mismatched in this case
box.range.slider.value = min(max(option.special_range_names[option.default], option.range_start),
box.range.slider.value = min(max(option.special_range_names[default], option.range_start),
option.range_end)
box.range.tag.text = str(int(box.range.slider.value))
box.range.slider.bind(on_touch_move=lambda _, _2: set_to_custom(box))
elif default in option.special_range_names.values():
# better visual
default = next(key for key, val in option.special_range_names.items() if val == option.default)
set_button_text(box.choice, default.title())
box.range.slider.bind(value=lambda _, _2: set_to_custom(box))
items = [
{
"text": choice.title(),
Expand All @@ -371,7 +378,7 @@ def open_dropdown(button):
]
box.range.slider.dropdown = MDDropdownMenu(caller=box.choice, items=items)
box.choice.bind(on_release=open_dropdown)
self.options[name] = option.default
self.options[name] = default
return box

def create_free_text(self, option: typing.Type[FreeText] | typing.Type[TextChoice], name: str):
Expand Down Expand Up @@ -447,8 +454,12 @@ def create_popup(self, option: typing.Type[OptionList] | typing.Type[OptionSet]
valid_keys = sorted(option.valid_keys)
if option.verify_item_name:
valid_keys += list(world.item_name_to_id.keys())
if option.convert_name_groups:
valid_keys += list(world.item_name_groups.keys())
if option.verify_location_name:
valid_keys += list(world.location_name_to_id.keys())
if option.convert_name_groups:
valid_keys += list(world.location_name_groups.keys())

if not issubclass(option, OptionCounter):
def apply_changes(button):
Expand All @@ -470,14 +481,6 @@ def apply_changes(button):
dialog.scrollbox.layout.spacing = dp(5)
dialog.scrollbox.layout.padding = [0, dp(5), 0, 0]

if name not in self.options:
# convert from non-mutable to mutable
# We use list syntax even for sets, set behavior is enforced through GUI
if issubclass(option, OptionCounter):
self.options[name] = deepcopy(option.default)
else:
self.options[name] = sorted(option.default)

if issubclass(option, OptionCounter):
for value in sorted(self.options[name]):
dialog.add_set_item(value, self.options[name].get(value, None))
Expand All @@ -491,6 +494,15 @@ def apply_changes(button):
def create_option_set_list_counter(self, option: typing.Type[OptionList] | typing.Type[OptionSet] |
typing.Type[OptionCounter], name: str, world: typing.Type[World]):
main_button = MDButton(MDButtonText(text="Edit"), on_release=lambda x: self.create_popup(option, name, world))

if name not in self.options:
# convert from non-mutable to mutable
# We use list syntax even for sets, set behavior is enforced through GUI
if issubclass(option, OptionCounter):
self.options[name] = deepcopy(option.default)
else:
self.options[name] = sorted(option.default)

return main_button

def create_option(self, option: typing.Type[Option], name: str, world: typing.Type[World]) -> Widget:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ Currently, the following games are supported:
* APQuest
* Satisfactory
* EarthBound
* Mega Man 3

For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
Expand Down
Loading
Loading