Compare commits

..

3 Commits

Author SHA1 Message Date
Fabian Dill
1169e62191 pickle is no longer used directly 2025-10-02 00:18:33 +02:00
Fabian Dill
d0bd1d29b1 Merge branch 'Archipelago_Main' into webhost_queue_display
# Conflicts:
#	WebHostLib/api/generate.py
2025-10-02 00:14:27 +02:00
Fabian Dill
d463faa9d9 WebHost: notify of current generation queue length 2025-05-11 15:03:04 +02:00
89 changed files with 377 additions and 1834 deletions

View File

@@ -9,14 +9,12 @@ on:
- 'setup.py'
- 'requirements.txt'
- '*.iss'
- 'worlds/*/archipelago.json'
pull_request:
paths:
- '.github/workflows/build.yml'
- 'setup.py'
- 'requirements.txt'
- '*.iss'
- 'worlds/*/archipelago.json'
workflow_dispatch:
env:

View File

@@ -1,24 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Build APWorld" type="PythonConfigurationType" factoryName="Python">
<module name="Archipelago" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<envs>
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<option name="SCRIPT_NAME" value="$ContentRoot$/Launcher.py" />
<option name="PARAMETERS" value="\&quot;Build APWorlds\&quot;" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
<option name="MODULE_MODE" value="false" />
<option name="REDIRECT_INPUT" value="false" />
<option name="INPUT_FILE" value="" />
<method v="2" />
</configuration>
</component>

View File

@@ -1346,7 +1346,8 @@ class Region:
for entrance in self.entrances: # BFS might be better here, trying DFS for now.
return entrance.parent_region.get_connecting_entrance(is_main_entrance)
def add_locations(self, locations: Mapping[str, int | None], location_type: type[Location] | None = None) -> None:
def add_locations(self, locations: Dict[str, Optional[int]],
location_type: Optional[type[Location]] = None) -> None:
"""
Adds locations to the Region object, where location_type is your Location class and locations is a dict of
location names to address.
@@ -1434,8 +1435,8 @@ class Region:
entrance.connect(self)
return entrance
def add_exits(self, exits: Iterable[str] | Mapping[str, str | None],
rules: Mapping[str, Callable[[CollectionState], bool]] | None = None) -> List[Entrance]:
def add_exits(self, exits: Union[Iterable[str], Dict[str, Optional[str]]],
rules: Dict[str, Callable[[CollectionState], bool]] = None) -> List[Entrance]:
"""
Connects current region to regions in exit dictionary. Passed region names must exist first.
@@ -1443,7 +1444,7 @@ class Region:
created entrances will be named "self.name -> connecting_region"
:param rules: rules for the exits from this region. format is {"connecting_region": rule}
"""
if not isinstance(exits, Mapping):
if not isinstance(exits, Dict):
exits = dict.fromkeys(exits)
return [
self.connect(
@@ -1857,9 +1858,6 @@ class Spoiler:
Utils.__version__, self.multiworld.seed))
outfile.write('Filling Algorithm: %s\n' % self.multiworld.algorithm)
outfile.write('Players: %d\n' % self.multiworld.players)
if self.multiworld.players > 1:
loc_count = len([loc for loc in self.multiworld.get_locations() if not loc.is_event])
outfile.write('Total Location Count: %d\n' % loc_count)
outfile.write(f'Plando Options: {self.multiworld.plando_options}\n')
AutoWorld.call_stage(self.multiworld, "write_spoiler_header", outfile)
@@ -1868,9 +1866,6 @@ class Spoiler:
outfile.write('\nPlayer %d: %s\n' % (player, self.multiworld.get_player_name(player)))
outfile.write('Game: %s\n' % self.multiworld.game[player])
loc_count = len([loc for loc in self.multiworld.get_locations(player) if not loc.is_event])
outfile.write('Location Count: %d\n' % loc_count)
for f_option, option in self.multiworld.worlds[player].options_dataclass.type_hints.items():
write_option(f_option, option)

View File

@@ -856,9 +856,9 @@ async def server_loop(ctx: CommonContext, address: typing.Optional[str] = None)
server_url = urllib.parse.urlparse(address)
if server_url.username:
ctx.username = urllib.parse.unquote(server_url.username)
ctx.username = server_url.username
if server_url.password:
ctx.password = urllib.parse.unquote(server_url.password)
ctx.password = server_url.password
def reconnect_hint() -> str:
return ", type /connect to reconnect" if ctx.server_address else ""

View File

@@ -129,10 +129,6 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
for i, location in enumerate(placements))
for (i, location, unsafe) in swap_attempts:
placed_item = location.item
if item_to_place == placed_item:
# The number of allowed swaps is limited, so do not allow a swap of an item with a copy of
# itself.
continue
# Unplaceable items can sometimes be swapped infinitely. Limit the
# number of times we will swap an individual item to prevent this
swap_count = swapped_items[placed_item.player, placed_item.name, unsafe]

View File

@@ -3,6 +3,9 @@ ModuleUpdate.update()
import Utils
if __name__ == "__main__":
Utils.init_logging("LinksAwakeningContext", exception_logger="Client")
import asyncio
import base64
import binascii
@@ -23,14 +26,16 @@ import typing
from CommonClient import (CommonContext, get_base_parser, gui_enabled, logger,
server_loop)
from NetUtils import ClientStatus
from . import LinksAwakeningWorld
from .Common import BASE_ID as LABaseID
from .GpsTracker import GpsTracker
from .TrackerConsts import storage_key
from .ItemTracker import ItemTracker
from .LADXR.checkMetadata import checkMetadataTable
from .Locations import get_locations_to_id, meta_to_name
from .Tracker import LocationTracker, MagpieBridge, Check
from worlds.ladx import LinksAwakeningWorld
from worlds.ladx.Common import BASE_ID as LABaseID
from worlds.ladx.GpsTracker import GpsTracker
from worlds.ladx.TrackerConsts import storage_key
from worlds.ladx.ItemTracker import ItemTracker
from worlds.ladx.LADXR.checkMetadata import checkMetadataTable
from worlds.ladx.Locations import get_locations_to_id, meta_to_name
from worlds.ladx.Tracker import LocationTracker, MagpieBridge, Check
class GameboyException(Exception):
pass
@@ -755,44 +760,42 @@ def run_game(romfile: str) -> None:
except FileNotFoundError:
logger.error(f"Couldn't launch ROM, {args[0]} is missing")
def launch(*launch_args):
async def main():
parser = get_base_parser(description="Link's Awakening Client.")
parser.add_argument("--url", help="Archipelago connection url")
parser.add_argument("--no-magpie", dest='magpie', default=True, action='store_false', help="Disable magpie bridge")
parser.add_argument('diff_file', default="", type=str, nargs="?",
help='Path to a .apladx Archipelago Binary Patch file')
async def main():
parser = get_base_parser(description="Link's Awakening Client.")
parser.add_argument("--url", help="Archipelago connection url")
parser.add_argument("--no-magpie", dest='magpie', default=True, action='store_false', help="Disable magpie bridge")
parser.add_argument('diff_file', default="", type=str, nargs="?",
help='Path to a .apladx Archipelago Binary Patch file')
args = parser.parse_args(launch_args)
args = parser.parse_args()
if args.diff_file:
import Patch
logger.info("patch file was supplied - creating rom...")
meta, rom_file = Patch.create_rom_file(args.diff_file)
if "server" in meta and not args.connect:
args.connect = meta["server"]
logger.info(f"wrote rom file to {rom_file}")
if args.diff_file:
import Patch
logger.info("patch file was supplied - creating rom...")
meta, rom_file = Patch.create_rom_file(args.diff_file)
if "server" in meta and not args.connect:
args.connect = meta["server"]
logger.info(f"wrote rom file to {rom_file}")
ctx = LinksAwakeningContext(args.connect, args.password, args.magpie)
ctx = LinksAwakeningContext(args.connect, args.password, args.magpie)
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
# TODO: nothing about the lambda about has to be in a lambda
ctx.la_task = create_task_log_exception(ctx.run_game_loop())
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
# TODO: nothing about the lambda about has to be in a lambda
ctx.la_task = create_task_log_exception(ctx.run_game_loop())
if gui_enabled:
ctx.run_gui()
ctx.run_cli()
# Down below run_gui so that we get errors out of the process
if args.diff_file:
run_game(rom_file)
# Down below run_gui so that we get errors out of the process
if args.diff_file:
run_game(rom_file)
await ctx.exit_event.wait()
await ctx.shutdown()
Utils.init_logging("LinksAwakeningContext", exception_logger="Client")
await ctx.exit_event.wait()
await ctx.shutdown()
if __name__ == '__main__':
colorama.just_fix_windows_console()
asyncio.run(main())
colorama.deinit()

View File

@@ -54,16 +54,13 @@ def main(args, seed=None, baked_server_options: dict[str, object] | None = None)
logger.info(f"Found {len(AutoWorld.AutoWorldRegister.world_types)} World Types:")
longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types)
world_classes = AutoWorld.AutoWorldRegister.world_types.values()
version_count = max(len(cls.world_version.as_simple_string()) for cls in world_classes)
item_count = len(str(max(len(cls.item_names) for cls in world_classes)))
location_count = len(str(max(len(cls.location_names) for cls in world_classes)))
item_count = len(str(max(len(cls.item_names) for cls in AutoWorld.AutoWorldRegister.world_types.values())))
location_count = len(str(max(len(cls.location_names) for cls in AutoWorld.AutoWorldRegister.world_types.values())))
for name, cls in AutoWorld.AutoWorldRegister.world_types.items():
if not cls.hidden and len(cls.item_names) > 0:
logger.info(f" {name:{longest_name}}: "
f"v{cls.world_version.as_simple_string():{version_count}} | "
f"v{cls.world_version.as_simple_string()} |"
f"Items: {len(cls.item_names):{item_count}} | "
f"Locations: {len(cls.location_names):{location_count}}")

View File

@@ -32,7 +32,7 @@ if typing.TYPE_CHECKING:
import colorama
import websockets
from websockets.extensions.permessage_deflate import PerMessageDeflate, ServerPerMessageDeflateFactory
from websockets.extensions.permessage_deflate import PerMessageDeflate
try:
# ponyorm is a requirement for webhost, not default server, so may not be importable
from pony.orm.dbapiprovider import OperationalError
@@ -50,15 +50,6 @@ from BaseClasses import ItemClassification
min_client_version = Version(0, 5, 0)
colorama.just_fix_windows_console()
no_version = Version(0, 0, 0)
assert isinstance(no_version, tuple) # assert immutable
server_per_message_deflate_factory = ServerPerMessageDeflateFactory(
server_max_window_bits=11,
client_max_window_bits=11,
compress_settings={"memLevel": 4},
)
def remove_from_list(container, value):
try:
@@ -134,31 +125,8 @@ def get_saving_second(seed_name: str, interval: int = 60) -> int:
class Client(Endpoint):
__slots__ = (
"__weakref__",
"version",
"auth",
"team",
"slot",
"send_index",
"tags",
"messageprocessor",
"ctx",
"remote_items",
"remote_start_inventory",
"no_items",
"no_locations",
"no_text",
)
version: Version
auth: bool
team: int | None
slot: int | None
send_index: int
tags: list[str]
messageprocessor: ClientMessageProcessor
ctx: weakref.ref[Context]
version = Version(0, 0, 0)
tags: typing.List[str]
remote_items: bool
remote_start_inventory: bool
no_items: bool
@@ -167,7 +135,6 @@ class Client(Endpoint):
def __init__(self, socket: "ServerConnection", ctx: Context) -> None:
super().__init__(socket)
self.version = no_version
self.auth = False
self.team = None
self.slot = None
@@ -175,11 +142,6 @@ class Client(Endpoint):
self.tags = []
self.messageprocessor = client_message_processor(ctx, self)
self.ctx = weakref.ref(ctx)
self.remote_items = False
self.remote_start_inventory = False
self.no_items = False
self.no_locations = False
self.no_text = False
@property
def items_handling(self):
@@ -217,7 +179,6 @@ class Context:
"release_mode": str,
"remaining_mode": str,
"collect_mode": str,
"countdown_mode": str,
"item_cheat": bool,
"compatibility": int}
# team -> slot id -> list of clients authenticated to slot.
@@ -247,8 +208,8 @@ class Context:
def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int,
hint_cost: int, item_cheat: bool, release_mode: str = "disabled", collect_mode="disabled",
countdown_mode: str = "auto", remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0,
compatibility: int = 2, log_network: bool = False, logger: logging.Logger = logging.getLogger()):
remaining_mode: str = "disabled", auto_shutdown: typing.SupportsFloat = 0, compatibility: int = 2,
log_network: bool = False, logger: logging.Logger = logging.getLogger()):
self.logger = logger
super(Context, self).__init__()
self.slot_info = {}
@@ -281,7 +242,6 @@ class Context:
self.release_mode: str = release_mode
self.remaining_mode: str = remaining_mode
self.collect_mode: str = collect_mode
self.countdown_mode: str = countdown_mode
self.item_cheat = item_cheat
self.exit_event = asyncio.Event()
self.client_activity_timers: typing.Dict[
@@ -667,7 +627,6 @@ class Context:
"server_password": self.server_password, "password": self.password,
"release_mode": self.release_mode,
"remaining_mode": self.remaining_mode, "collect_mode": self.collect_mode,
"countdown_mode": self.countdown_mode,
"item_cheat": self.item_cheat, "compatibility": self.compatibility}
}
@@ -702,7 +661,6 @@ class Context:
self.release_mode = savedata["game_options"]["release_mode"]
self.remaining_mode = savedata["game_options"]["remaining_mode"]
self.collect_mode = savedata["game_options"]["collect_mode"]
self.countdown_mode = savedata["game_options"].get("countdown_mode", self.countdown_mode)
self.item_cheat = savedata["game_options"]["item_cheat"]
self.compatibility = savedata["game_options"]["compatibility"]
@@ -1200,17 +1158,16 @@ def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, st
found = location_id in ctx.location_checks[team, finding_player]
entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "")
hint_status = status # Assign again because we're in a for loop
if found:
hint_status = HintStatus.HINT_FOUND
elif hint_status is None:
status = HintStatus.HINT_FOUND
elif status is None:
if item_flags & ItemClassification.trap:
hint_status = HintStatus.HINT_AVOID
status = HintStatus.HINT_AVOID
else:
hint_status = HintStatus.HINT_PRIORITY
status = HintStatus.HINT_PRIORITY
hints.append(
Hint(receiving_player, finding_player, location_id, item_id, found, entrance, item_flags, hint_status)
Hint(receiving_player, finding_player, location_id, item_id, found, entrance, item_flags, status)
)
return hints
@@ -1535,23 +1492,6 @@ class ClientMessageProcessor(CommonCommandProcessor):
" You can ask the server admin for a /collect")
return False
def _cmd_countdown(self, seconds: str = "10") -> bool:
"""Start a countdown in seconds"""
if self.ctx.countdown_mode == "disabled" or \
self.ctx.countdown_mode == "auto" and len(self.ctx.player_names) >= 30:
self.output("Sorry, client countdowns have been disabled on this server. You can ask the server admin for a /countdown")
return False
try:
timer = int(seconds, 10)
except ValueError:
timer = 10
else:
if timer > 60 * 60:
raise ValueError(f"{timer} is invalid. Maximum is 1 hour.")
async_start(countdown(self.ctx, timer))
return True
def _cmd_remaining(self) -> bool:
"""List remaining items in your game, but not their location or recipient"""
if self.ctx.remaining_mode == "enabled":
@@ -2512,11 +2452,6 @@ class ServerCommandProcessor(CommonCommandProcessor):
elif value_type == str and option_name.endswith("password"):
def value_type(input_text: str):
return None if input_text.lower() in {"null", "none", '""', "''"} else input_text
elif option_name == "countdown_mode":
valid_values = {"enabled", "disabled", "auto"}
if option_value.lower() not in valid_values:
self.output(f"Unrecognized {option_name} value '{option_value}', known: {', '.join(valid_values)}")
return False
elif value_type == str and option_name.endswith("mode"):
valid_values = {"goal", "enabled", "disabled"}
valid_values.update(("auto", "auto_enabled") if option_name != "remaining_mode" else [])
@@ -2604,13 +2539,6 @@ def parse_args() -> argparse.Namespace:
goal: !collect can be used after goal completion
auto-enabled: !collect is available and automatically triggered on goal completion
''')
parser.add_argument('--countdown_mode', default=defaults["countdown_mode"], nargs='?',
choices=['enabled', 'disabled', "auto"], help='''\
Select !countdown Accessibility. (default: %(default)s)
enabled: !countdown is always available
disabled: !countdown is never available
auto: !countdown is available for rooms with less than 30 players
''')
parser.add_argument('--remaining_mode', default=defaults["remaining_mode"], nargs='?',
choices=['enabled', 'disabled', "goal"], help='''\
Select !remaining Accessibility. (default: %(default)s)
@@ -2676,7 +2604,7 @@ async def main(args: argparse.Namespace):
ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points,
args.hint_cost, not args.disable_item_cheat, args.release_mode, args.collect_mode,
args.countdown_mode, args.remaining_mode,
args.remaining_mode,
args.auto_shutdown, args.compatibility, args.log_network)
data_filename = args.multidata
@@ -2711,13 +2639,7 @@ async def main(args: argparse.Namespace):
ssl_context = load_server_cert(args.cert, args.cert_key) if args.cert else None
ctx.server = websockets.serve(
functools.partial(server, ctx=ctx),
host=ctx.host,
port=ctx.port,
ssl=ssl_context,
extensions=[server_per_message_deflate_factory],
)
ctx.server = websockets.serve(functools.partial(server, ctx=ctx), host=ctx.host, port=ctx.port, ssl=ssl_context)
ip = args.host if args.host else Utils.get_public_ipv4()
logging.info('Hosting game at %s:%d (%s)' % (ip, ctx.port,
'No password' if not ctx.password else 'Password: %s' % ctx.password))

View File

@@ -174,8 +174,6 @@ decode = JSONDecoder(object_hook=_object_hook).decode
class Endpoint:
__slots__ = ("socket",)
socket: "ServerConnection"
def __init__(self, socket):

View File

@@ -1380,7 +1380,7 @@ class NonLocalItems(ItemSet):
class StartInventory(ItemDict):
"""Start with the specified amount of these items. Example: "Bomb: 1" """
"""Start with these items."""
verify_item_name = True
display_name = "Start Inventory"
rich_text_doc = True
@@ -1388,7 +1388,7 @@ class StartInventory(ItemDict):
class StartInventoryPool(StartInventory):
"""Start with the specified amount of these items and don't place them in the world. Example: "Bomb: 1"
"""Start with these items and don't place them in the world.
The game decides what the replacement items will be.
"""
@@ -1474,10 +1474,8 @@ class ItemLinks(OptionList):
super(ItemLinks, self).verify(world, player_name, plando_options)
existing_links = set()
for link in self.value:
link["name"] = link["name"].strip()[:16].strip()
if link["name"] in existing_links:
raise Exception(f"Item link names are limited to their first 16 characters and must be unique. "
f"You have more than one link named '{link['name']}'.")
raise Exception(f"You cannot have more than one link named {link['name']}.")
existing_links.add(link["name"])
pool = self.verify_items(link["item_pool"], link["name"], "item_pool", world)
@@ -1712,7 +1710,7 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
from jinja2 import Template
from worlds import AutoWorldRegister
from Utils import local_path, __version__
from Utils import local_path, __version__, tuplize_version
full_path: str

View File

@@ -18,7 +18,7 @@ from json import loads, dumps
from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser
import Utils
import settings
from settings import Settings
from Utils import async_start
from MultiServer import mark_raw
if typing.TYPE_CHECKING:
@@ -286,7 +286,7 @@ class SNESState(enum.IntEnum):
def launch_sni() -> None:
sni_path = settings.get_settings().sni_options.sni_path
sni_path = Settings.sni_options.sni_path
if not os.path.isdir(sni_path):
sni_path = Utils.local_path(sni_path)
@@ -669,7 +669,7 @@ async def game_watcher(ctx: SNIContext) -> None:
async def run_game(romfile: str) -> None:
auto_start = settings.get_settings().sni_options.snes_rom_start
auto_start = Settings.sni_options.snes_rom_start
if auto_start is True:
import webbrowser
webbrowser.open(romfile)

View File

@@ -1,7 +1,6 @@
from __future__ import annotations
import asyncio
import concurrent.futures
import json
import typing
import builtins
@@ -36,7 +35,7 @@ if typing.TYPE_CHECKING:
def tuplize_version(version: str) -> Version:
return Version(*(int(piece) for piece in version.split(".")))
return Version(*(int(piece, 10) for piece in version.split(".")))
class Version(typing.NamedTuple):
@@ -50,6 +49,7 @@ class Version(typing.NamedTuple):
__version__ = "0.6.4"
version_tuple = tuplize_version(__version__)
version = Version(*version_tuple)
is_linux = sys.platform.startswith("linux")
is_macos = sys.platform == "darwin"
@@ -478,7 +478,7 @@ class RestrictedUnpickler(pickle.Unpickler):
mod = importlib.import_module(module)
obj = getattr(mod, name)
if issubclass(obj, (self.options_module.Option, self.options_module.PlandoConnection,
self.options_module.PlandoItem, self.options_module.PlandoText)):
self.options_module.PlandoText)):
return obj
# Forbid everything else.
raise pickle.UnpicklingError(f"global '{module}.{name}' is forbidden")
@@ -721,22 +721,13 @@ def get_intended_text(input_text: str, possible_answers) -> typing.Tuple[str, bo
def get_input_text_from_response(text: str, command: str) -> typing.Optional[str]:
"""
Parses the response text from `get_intended_text` to find the suggested input and autocomplete the command in
arguments with it.
:param text: The response text from `get_intended_text`.
:param command: The command to which the input text should be added. Must contain the prefix used by the command
(`!` or `/`).
:return: The command with the suggested input text appended, or None if no suggestion was found.
"""
if "did you mean " in text:
for question in ("Didn't find something that closely matches",
"Too many close matches"):
if text.startswith(question):
name = get_text_between(text, "did you mean '",
"'? (")
return f"{command} {name}"
return f"!{command} {name}"
elif text.startswith("Missing: "):
return text.replace("Missing: ", "!hint_location ")
return None
@@ -1139,40 +1130,3 @@ def is_iterable_except_str(obj: object) -> TypeGuard[typing.Iterable[typing.Any]
if isinstance(obj, str):
return False
return isinstance(obj, typing.Iterable)
class DaemonThreadPoolExecutor(concurrent.futures.ThreadPoolExecutor):
"""
ThreadPoolExecutor that uses daemonic threads that do not keep the program alive.
NOTE: use this with caution because killed threads will not properly clean up.
"""
def _adjust_thread_count(self):
# see upstream ThreadPoolExecutor for details
import threading
import weakref
from concurrent.futures.thread import _worker
if self._idle_semaphore.acquire(timeout=0):
return
def weakref_cb(_, q=self._work_queue):
q.put(None)
num_threads = len(self._threads)
if num_threads < self._max_workers:
thread_name = f"{self._thread_name_prefix or self}_{num_threads}"
t = threading.Thread(
name=thread_name,
target=_worker,
args=(
weakref.ref(self, weakref_cb),
self._work_queue,
self._initializer,
self._initargs,
),
daemon=True,
)
t.start()
self._threads.add(t)
# NOTE: don't add to _threads_queues so we don't block on shutdown

View File

@@ -109,13 +109,6 @@ if __name__ == "__main__":
logging.exception(e)
logging.warning("Could not update LttP sprites.")
app = get_app()
from worlds import AutoWorldRegister
# Update to only valid WebHost worlds
invalid_worlds = {name for name, world in AutoWorldRegister.world_types.items()
if not hasattr(world.web, "tutorials")}
if invalid_worlds:
logging.error(f"Following worlds not loaded as they are invalid for WebHost: {invalid_worlds}")
AutoWorldRegister.world_types = {k: v for k, v in AutoWorldRegister.world_types.items() if k not in invalid_worlds}
create_options_files()
copy_tutorials_files_to_static()
if app.config["SELFLAUNCH"]:

View File

@@ -1,15 +1,16 @@
import json
import typing
from uuid import UUID
from flask import request, session, url_for
from markupsafe import Markup
from pony.orm import commit
from pony.orm import commit, select
from Utils import restricted_dumps
from WebHostLib import app
from WebHostLib import app, cache
from WebHostLib.check import get_yaml_data, roll_options
from WebHostLib.generate import get_meta
from WebHostLib.models import Generation, STATE_QUEUED, Seed, STATE_ERROR
from WebHostLib.models import Generation, STATE_QUEUED, STATE_STARTED, Seed, STATE_ERROR
from . import api_endpoints
@@ -74,12 +75,23 @@ def generate_api():
def wait_seed_api(seed: UUID):
seed_id = seed
seed = Seed.get(id=seed_id)
reply_dict: dict[str, typing.Any] = {"queue_len": get_queue_length()}
if seed:
return {"text": "Generation done"}, 201
reply_dict["text"] = "Generation done"
return reply_dict, 201
generation = Generation.get(id=seed_id)
if not generation:
return {"text": "Generation not found"}, 404
reply_dict["text"] = "Generation not found"
return reply_dict, 404
elif generation.state == STATE_ERROR:
return {"text": "Generation failed"}, 500
return {"text": "Generation running"}, 202
reply_dict["text"] = "Generation failed"
return reply_dict, 500
reply_dict["text"] = "Generation running"
return reply_dict, 202
@cache.memoize(timeout=5)
def get_queue_length() -> int:
return select(generation for generation in Generation if
generation.state == STATE_STARTED or generation.state == STATE_QUEUED).count()

View File

@@ -36,39 +36,25 @@ def handle_generation_failure(result: BaseException):
logging.exception(e)
def _mp_gen_game(
gen_options: dict,
meta: dict[str, Any] | None = None,
owner=None,
sid=None,
timeout: int|None = None,
) -> PrimaryKey | None:
def _mp_gen_game(gen_options: dict, meta: dict[str, Any] | None = None, owner=None, sid=None) -> PrimaryKey | None:
from setproctitle import setproctitle
setproctitle(f"Generator ({sid})")
try:
return gen_game(gen_options, meta=meta, owner=owner, sid=sid, timeout=timeout)
finally:
setproctitle(f"Generator (idle)")
res = gen_game(gen_options, meta=meta, owner=owner, sid=sid)
setproctitle(f"Generator (idle)")
return res
def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation, timeout: int|None) -> None:
def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
try:
meta = json.loads(generation.meta)
options = restricted_loads(generation.options)
logging.info(f"Generating {generation.id} for {len(options)} players")
pool.apply_async(
_mp_gen_game,
(options,),
{
"meta": meta,
"sid": generation.id,
"owner": generation.owner,
"timeout": timeout,
},
handle_generation_success,
handle_generation_failure,
)
pool.apply_async(_mp_gen_game, (options,),
{"meta": meta,
"sid": generation.id,
"owner": generation.owner},
handle_generation_success, handle_generation_failure)
except Exception as e:
generation.state = STATE_ERROR
commit()
@@ -149,7 +135,6 @@ def autogen(config: dict):
with multiprocessing.Pool(config["GENERATORS"], initializer=init_generator,
initargs=(config,), maxtasksperchild=10) as generator_pool:
job_time = config["JOB_TIME"]
with db_session:
to_start = select(generation for generation in Generation if generation.state == STATE_STARTED)
@@ -160,7 +145,7 @@ def autogen(config: dict):
if sid:
generation.delete()
else:
launch_generator(generator_pool, generation, timeout=job_time)
launch_generator(generator_pool, generation)
commit()
select(generation for generation in Generation if generation.state == STATE_ERROR).delete()
@@ -172,7 +157,7 @@ def autogen(config: dict):
generation for generation in Generation
if generation.state == STATE_QUEUED).for_update()
for generation in to_start:
launch_generator(generator_pool, generation, timeout=job_time)
launch_generator(generator_pool, generation)
except AlreadyRunningException:
logging.info("Autogen reports as already running, not starting another.")

View File

@@ -19,10 +19,7 @@ from pony.orm import commit, db_session, select
import Utils
from MultiServer import (
Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor, load_server_cert,
server_per_message_deflate_factory,
)
from MultiServer import Context, server, auto_shutdown, ServerCommandProcessor, ClientMessageProcessor, load_server_cert
from Utils import restricted_loads, cache_argsless
from .locker import Locker
from .models import Command, GameDataPackage, Room, db
@@ -100,7 +97,6 @@ class WebHostContext(Context):
self.main_loop.call_soon_threadsafe(cmdprocessor, command.commandtext)
command.delete()
commit()
del commands
time.sleep(5)
@db_session
@@ -150,13 +146,13 @@ class WebHostContext(Context):
self.location_name_groups = static_location_name_groups
return self._load(multidata, game_data_packages, True)
@db_session
def init_save(self, enabled: bool = True):
self.saving = enabled
if self.saving:
with db_session:
savegame_data = Room.get(id=self.room_id).multisave
if savegame_data:
self.set_save(restricted_loads(Room.get(id=self.room_id).multisave))
savegame_data = Room.get(id=self.room_id).multisave
if savegame_data:
self.set_save(restricted_loads(Room.get(id=self.room_id).multisave))
self._start_async_saving(atexit_save=False)
threading.Thread(target=self.listen_to_db_commands, daemon=True).start()
@@ -286,12 +282,8 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
assert ctx.server is None
try:
ctx.server = websockets.serve(
functools.partial(server, ctx=ctx),
ctx.host,
ctx.port,
ssl=get_ssl_context(),
extensions=[server_per_message_deflate_factory],
)
functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=get_ssl_context())
await ctx.server
except OSError: # likely port in use
ctx.server = websockets.serve(
@@ -312,7 +304,6 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
with db_session:
room = Room.get(id=ctx.room_id)
room.last_port = port
del room
else:
ctx.logger.exception("Could not determine port. Likely hosting failure.")
with db_session:
@@ -331,7 +322,6 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
with db_session:
room = Room.get(id=room_id)
room.last_port = -1
del room
logger.exception(e)
raise
else:
@@ -343,12 +333,11 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
ctx.save_dirty = False # make sure the saving thread does not write to DB after final wakeup
ctx.exit_event.set() # make sure the saving thread stops at some point
# NOTE: async saving should probably be an async task and could be merged with shutdown_task
with db_session:
with (db_session):
# ensure the Room does not spin up again on its own, minute of safety buffer
room = Room.get(id=room_id)
room.last_activity = datetime.datetime.utcnow() - \
datetime.timedelta(minutes=1, seconds=room.timeout)
del room
logging.info(f"Shutting down room {room_id} on {name}.")
finally:
await asyncio.sleep(5)

View File

@@ -14,7 +14,7 @@ from pony.orm import commit, db_session
from BaseClasses import get_seed, seeddigits
from Generate import PlandoOptions, handle_name, mystery_argparse
from Main import main as ERmain
from Utils import __version__, restricted_dumps, DaemonThreadPoolExecutor
from Utils import __version__, restricted_dumps
from WebHostLib import app
from settings import ServerOptions, GeneratorOptions
from .check import get_yaml_data, roll_options
@@ -33,7 +33,6 @@ def get_meta(options_source: dict, race: bool = False) -> dict[str, list[str] |
"release_mode": str(options_source.get("release_mode", ServerOptions.release_mode)),
"remaining_mode": str(options_source.get("remaining_mode", ServerOptions.remaining_mode)),
"collect_mode": str(options_source.get("collect_mode", ServerOptions.collect_mode)),
"countdown_mode": str(options_source.get("countdown_mode", ServerOptions.countdown_mode)),
"item_cheat": bool(int(options_source.get("item_cheat", not ServerOptions.disable_item_cheat))),
"server_password": str(options_source.get("server_password", None)),
}
@@ -73,10 +72,6 @@ def generate(race=False):
return render_template("generate.html", race=race, version=__version__)
def format_exception(e: BaseException) -> str:
return f"{e.__class__.__name__}: {e}"
def start_generation(options: dict[str, dict | str], meta: dict[str, Any]):
results, gen_options = roll_options(options, set(meta["plando_options"]))
@@ -97,9 +92,7 @@ def start_generation(options: dict[str, dict | str], meta: dict[str, Any]):
except PicklingError as e:
from .autolauncher import handle_generation_failure
handle_generation_failure(e)
meta["error"] = format_exception(e)
details = json.dumps(meta, indent=4).strip()
return render_template("seedError.html", seed_error=meta["error"], details=details)
return render_template("seedError.html", seed_error=("PicklingError: " + str(e)))
commit()
@@ -107,18 +100,16 @@ def start_generation(options: dict[str, dict | str], meta: dict[str, Any]):
else:
try:
seed_id = gen_game({name: vars(options) for name, options in gen_options.items()},
meta=meta, owner=session["_id"].int, timeout=app.config["JOB_TIME"])
meta=meta, owner=session["_id"].int)
except BaseException as e:
from .autolauncher import handle_generation_failure
handle_generation_failure(e)
meta["error"] = format_exception(e)
details = json.dumps(meta, indent=4).strip()
return render_template("seedError.html", seed_error=meta["error"], details=details)
return render_template("seedError.html", seed_error=(e.__class__.__name__ + ": " + str(e)))
return redirect(url_for("view_seed", seed=seed_id))
def gen_game(gen_options: dict, meta: dict[str, Any] | None = None, owner=None, sid=None, timeout: int|None = None):
def gen_game(gen_options: dict, meta: dict[str, Any] | None = None, owner=None, sid=None):
if meta is None:
meta = {}
@@ -172,12 +163,11 @@ def gen_game(gen_options: dict, meta: dict[str, Any] | None = None, owner=None,
ERmain(args, seed, baked_server_options=meta["server_options"])
return upload_to_db(target.name, sid, owner, race)
thread_pool = DaemonThreadPoolExecutor(max_workers=1)
thread_pool = concurrent.futures.ThreadPoolExecutor(max_workers=1)
thread = thread_pool.submit(task)
try:
return thread.result(timeout)
return thread.result(app.config["JOB_TIME"])
except concurrent.futures.TimeoutError as e:
if sid:
with db_session:
@@ -185,14 +175,11 @@ def gen_game(gen_options: dict, meta: dict[str, Any] | None = None, owner=None,
if gen is not None:
gen.state = STATE_ERROR
meta = json.loads(gen.meta)
meta["error"] = ("Allowed time for Generation exceeded, " +
"please consider generating locally instead. " +
format_exception(e))
meta["error"] = (
"Allowed time for Generation exceeded, please consider generating locally instead. " +
e.__class__.__name__ + ": " + str(e))
gen.meta = json.dumps(meta)
commit()
except (KeyboardInterrupt, SystemExit):
# don't update db, retry next time
raise
except BaseException as e:
if sid:
with db_session:
@@ -200,15 +187,10 @@ def gen_game(gen_options: dict, meta: dict[str, Any] | None = None, owner=None,
if gen is not None:
gen.state = STATE_ERROR
meta = json.loads(gen.meta)
meta["error"] = format_exception(e)
meta["error"] = (e.__class__.__name__ + ": " + str(e))
gen.meta = json.dumps(meta)
commit()
raise
finally:
# free resources claimed by thread pool, if possible
# NOTE: Timeout depends on the process being killed at some point
# since we can't actually cancel a running gen at the moment.
thread_pool.shutdown(wait=False, cancel_futures=True)
@app.route('/wait/<suuid:seed>')
@@ -222,9 +204,7 @@ def wait_seed(seed: UUID):
if not generation:
return "Generation not found."
elif generation.state == STATE_ERROR:
meta = json.loads(generation.meta)
details = json.dumps(meta, indent=4).strip()
return render_template("seedError.html", seed_error=meta["error"], details=details)
return render_template("seedError.html", seed_error=generation.meta)
return render_template("waitSeed.html", seed_id=seed_id)

View File

@@ -271,9 +271,9 @@ def host_room(room: UUID):
or "Discordbot" in request.user_agent.string
or not any(browser_token in request.user_agent.string for browser_token in browser_tokens))
def get_log(max_size: int = 0 if automated else 1024000) -> Tuple[str, int]:
def get_log(max_size: int = 0 if automated else 1024000) -> str:
if max_size == 0:
return "", 0
return ""
try:
with open(os.path.join("logs", str(room.id) + ".txt"), "rb") as log:
raw_size = 0
@@ -284,9 +284,9 @@ def host_room(room: UUID):
break
raw_size += len(block)
fragments.append(block.decode("utf-8"))
return "".join(fragments), raw_size
return "".join(fragments)
except FileNotFoundError:
return "", 0
return ""
return render_template("hostRoom.html", room=room, should_refresh=should_refresh, get_log=get_log)

View File

@@ -76,7 +76,7 @@ def filter_rst_to_html(text: str) -> str:
lines = text.splitlines()
text = lines[0] + "\n" + dedent("\n".join(lines[1:]))
return publish_parts(text, writer='html', settings=None, settings_overrides={
return publish_parts(text, writer_name='html', settings=None, settings_overrides={
'raw_enable': False,
'file_insertion_enabled': False,
'output_encoding': 'unicode'
@@ -231,7 +231,7 @@ def generate_yaml(game: str):
if key_parts[-1] == "qty":
if key_parts[0] not in options:
options[key_parts[0]] = {}
if val and val != "0":
if val != "0":
options[key_parts[0]][key_parts[1]] = int(val)
del options[key]

View File

@@ -4,8 +4,7 @@ pony>=0.7.19; python_version <= '3.12'
pony @ git+https://github.com/black-sliver/pony@7feb1221953b7fa4a6735466bf21a8b4d35e33ba#0.7.19; python_version >= '3.13'
waitress>=3.0.2
Flask-Caching>=2.3.0
Flask-Compress>=1.17; python_version >= '3.12'
Flask-Compress==1.18; python_version <= '3.11' # 3.11's pkg_resources can't resolve the new "backports.zstd" dependency
Flask-Compress>=1.17
Flask-Limiter>=3.12
bokeh>=3.6.3
markupsafe>=3.0.2

View File

@@ -66,7 +66,7 @@ is to ensure items necessary to complete the game will be accessible to the play
rules allowing certain items to be placed in normally unreachable locations, provided the player has indicated they are
comfortable exploiting certain glitches in the game.
## I want to develop a game implementation for Archipelago. How do I do that?
## I want to add a game to the Archipelago randomizer. How do I do that?
The best way to get started is to take a look at our code on GitHub:
[Archipelago GitHub Page](https://github.com/ArchipelagoMW/Archipelago).
@@ -77,5 +77,4 @@ There, you will find examples of games in the `worlds` folder:
You may also find developer documentation in the `docs` folder:
[/docs Folder in Archipelago Code](https://github.com/ArchipelagoMW/Archipelago/tree/main/docs).
If you have more questions regarding development of a game implementation, feel free to ask in the **#ap-world-dev**
channel on our Discord.
If you have more questions, feel free to ask in the **#ap-world-dev** channel on our Discord.

View File

@@ -72,13 +72,3 @@ code{
padding-right: 0.25rem;
color: #000000;
}
code.grassy {
background-color: #b5e9a4;
border: 1px solid #2a6c2f;
white-space: preserve;
text-align: left;
display: block;
font-size: 14px;
line-height: 20px;
}

View File

@@ -13,7 +13,3 @@
min-height: 360px;
text-align: center;
}
h2, h4 {
color: #ffffff;
}

View File

@@ -58,7 +58,8 @@
Open Log File...
</a>
</div>
{% set log, log_len = get_log() -%}
{% set log = get_log() -%}
{%- set log_len = log | length - 1 if log.endswith("…") else log | length -%}
<div id="logger" style="white-space: pre">{{ log }}</div>
<script>
let url = '{{ url_for('display_log', room = room.id) }}';

View File

@@ -4,20 +4,16 @@
{% block head %}
<title>Generation failed, please retry.</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles/waitSeed.css') }}"/>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/waitSeed.css") }}"/>
{% endblock %}
{% block body %}
{% include 'header/oceanIslandHeader.html' %}
<div id="wait-seed-wrapper" class="grass-island">
<div id="wait-seed">
<h1>Generation Failed</h1>
<h2>Please try again!</h2>
<p>{{ seed_error }}</p>
<h4>More details:</h4>
<p>
<code class="grassy">{{ details }}</code>
</p>
<h1>Generation failed</h1>
<h2>please retry</h2>
{{ seed_error }}
</div>
</div>
{% endblock %}

View File

@@ -31,9 +31,6 @@
{% include 'header/oceanHeader.html' %}
<div id="games" class="markdown">
<h1>Currently Supported Games</h1>
<p>Below are the games that are currently included with the Archipelago software. To play a game that is not on
this page, please refer to the <a href="/tutorial/Archipelago/setup/en#playing-with-custom-worlds">playing with
custom worlds</a> section of the setup guide.</p>
<div class="js-only">
<label for="game-search">Search for your game below!</label><br />
<div class="page-controls">

View File

@@ -30,10 +30,21 @@
}
const data = await response.json();
waitSeedDiv.innerHTML = `
<h1>Generation in Progress</h1>
<p>${data.text}</p>
`;
if (data.queue_len === 1){
waitSeedDiv.innerHTML = `
<h1>Generation in Progress</h1>
<p>${data.text}</p>
<p>This is the only generation in the queue.</p>
`;
}
else {
waitSeedDiv.innerHTML = `
<h1>Generation in Progress</h1>
<p>${data.text}</p>
<p>There are ${data.queue_len} generations in the queue.</p>
`;
}
setTimeout(checkStatus, 1000); // Continue polling.
} catch (error) {

View File

@@ -1,83 +1,40 @@
# APWorld Specification
# apworld Specification
Archipelago depends on worlds to provide game-specific details like items, locations and output generation.
These are called "APWorlds".
They are located in the `worlds/` folder (source) or `<install dir>/lib/worlds/` (when installed).
Those are located in the `worlds/` folder (source) or `<install dir>/lib/worlds/` (when installed).
See [world api.md](world%20api.md) for details.
APWorlds can either be a folder, or they can be packaged as an .apworld file.
## .apworld File Format
apworld provides a way to package and ship a world that is not part of the main distribution by placing a `*.apworld`
file into the worlds folder.
The `.apworld` file format provides a way to package and ship an APWorld that is not part of the main distribution
by placing a `*.apworld` file into the worlds folder.
**Warning:** apworlds have to be all lower case, otherwise they raise a bogus Exception when trying to import in frozen python 3.10+!
`.apworld` files are zip archives, all lower case, with the file ending `.apworld`.
## File Format
apworld files are zip archives, all lower case, with the file ending `.apworld`.
The zip has to contain a folder with the same name as the zip, case-sensitive, that contains what would normally be in
the world's folder in `worlds/`. I.e. `worlds/ror2.apworld` containing `ror2/__init__.py`.
**Warning:** `.apworld` files have to be all lower case,
otherwise they raise a bogus Exception when trying to import in frozen python 3.10+!
## Metadata
Metadata about the APWorld is defined in an `archipelago.json` file.
If the APWorld is a folder, the only required field is "game":
Metadata about the apworld is defined in an `archipelago.json` file inside the zip archive.
The current format version has at minimum:
```json
{
"game": "Game Name"
}
```
There are also the following optional fields:
* `minimum_ap_version` and `maximum_ap_version` - which if present will each be compared against the current
Archipelago version respectively to filter those files from being loaded.
* `world_version` - an arbitrary version for that world in order to only load the newest valid world.
An APWorld without a world_version is always treated as older than one with a version
(**Must** use exactly the format `"major.minor.build"`, e.g. `1.0.0`)
* `authors` - a list of authors, to eventually be displayed in various user-facing places such as WebHost and
package managers. Should always be a list of strings.
If the APWorld is packaged as an `.apworld` zip file, it also needs to have `version` and `compatible_version`,
which refer to the version of the APContainer packaging scheme defined in [Files.py](../worlds/Files.py).
These get automatically added to the `archipelago.json` of an .apworld if it is packaged using the
["Build apworlds" launcher component](#build-apworlds-launcher-component),
which is the correct way to package your `.apworld` as a world developer. Do not write these fields yourself.
### "Build apworlds" Launcher Component
In the Archipelago Launcher, there is a "Build apworlds" component that will package all world folders to `.apworld`,
and add `archipelago.json` manifest files to them.
These .apworld files will be output to `build/apworlds` (relative to the Archipelago root directory).
The `archipelago.json` file in each .apworld will automatically include the appropriate
`version` and `compatible_version`.
If a world folder has an `archipelago.json` in its root, any fields it contains will be carried over.
So, a world folder with an `archipelago.json` that looks like this:
```json
{
"game": "Game Name",
"minimum_ap_version": "0.6.4",
"world_version": "2.1.4",
"authors": ["NewSoupVi"]
}
```
will be packaged into an `.apworld` with a manifest file inside of it that looks like this:
```json
{
"minimum_ap_version": "0.6.4",
"world_version": "2.1.4",
"authors": ["NewSoupVi"],
"version": 7,
"compatible_version": 7,
"version": 6,
"compatible_version": 5,
"game": "Game Name"
}
```
This is the recommended workflow for packaging your world to an `.apworld`.
with the following optional version fields using the format `"1.0.0"` to represent major.minor.build:
* `minimum_ap_version` and `maximum_ap_version` - which if present will each be compared against the current
Archipelago version respectively to filter those files from being loaded
* `world_version` - an arbitrary version for that world in order to only load the newest valid world.
An apworld without a world_version is always treated as older than one with a version
## Extra Data
@@ -86,7 +43,7 @@ The zip can contain arbitrary files in addition what was specified above.
## Caveats
Imports from other files inside the APWorld have to use relative imports. e.g. `from .options import MyGameOptions`
Imports from other files inside the apworld have to use relative imports. e.g. `from .options import MyGameOptions`
Imports from AP base have to use absolute imports, e.g. `from Options import Toggle` or
`from worlds.AutoWorld import World`

View File

@@ -180,8 +180,8 @@ Root: HKCR; Subkey: "{#MyAppName}mm2patch\shell\open\command"; ValueData: """{a
Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLauncher.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\shell\open\command"; ValueData: """{app}\ArchipelagoLauncher.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\shell\open\command"; ValueData: """{app}\ArchipelagoLinksAwakeningClient.exe"" ""%1"""; ValueType: string; ValueName: "";
Root: HKCR; Subkey: ".aptloz"; ValueData: "{#MyAppName}tlozpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
Root: HKCR; Subkey: "{#MyAppName}tlozpatch"; ValueData: "Archipelago The Legend of Zelda Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";

10
kvui.py
View File

@@ -838,15 +838,15 @@ class GameManager(ThemedApp):
self.log_panels: typing.Dict[str, Widget] = {}
# keep track of last used command to autofill on click
self.last_autofillable_command = "!hint"
autofillable_commands = ("!hint_location", "!hint", "!getitem")
self.last_autofillable_command = "hint"
autofillable_commands = ("hint_location", "hint", "getitem")
original_say = ctx.on_user_say
def intercept_say(text):
text = original_say(text)
if text:
for command in autofillable_commands:
if text.startswith(command):
if text.startswith("!" + command):
self.last_autofillable_command = command
break
return text
@@ -1099,6 +1099,10 @@ class GameManager(ThemedApp):
hints = self.ctx.stored_data.get(f"_read_hints_{self.ctx.team}_{self.ctx.slot}", [])
self.hint_log.refresh_hints(hints)
# default F1 keybind, opens a settings menu, that seems to break the layout engine once closed
def open_settings(self, *largs):
pass
class LogtoUI(logging.Handler):
def __init__(self, on_log):

View File

@@ -579,17 +579,6 @@ class ServerOptions(Group):
"goal" -> Client can ask for remaining items after goal completion
"""
class CountdownMode(str):
"""
Countdown modes
Determines whether or not a player can initiate a countdown with !countdown
Note that /countdown is always available to the host.
"enabled" -> Client can always initiate a countdown with !countdown.
"disabled" -> Client can never initiate a countdown with !countdown.
"auto" -> !countdown will be available for any room with less than 30 slots.
"""
class AutoShutdown(int):
"""Automatically shut down the server after this many seconds without new location checks, 0 to keep running"""
@@ -624,7 +613,6 @@ class ServerOptions(Group):
release_mode: ReleaseMode = ReleaseMode("auto")
collect_mode: CollectMode = CollectMode("auto")
remaining_mode: RemainingMode = RemainingMode("goal")
countdown_mode: CountdownMode = CountdownMode("auto")
auto_shutdown: AutoShutdown = AutoShutdown(0)
compatibility: Compatibility = Compatibility(2)
log_network: LogNetwork = LogNetwork(0)

View File

@@ -372,6 +372,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
from Options import generate_yaml_templates
from worlds.AutoWorld import AutoWorldRegister
from worlds.Files import APWorldContainer
from Utils import version
assert not non_apworlds - set(AutoWorldRegister.world_types), \
f"Unknown world {non_apworlds - set(AutoWorldRegister.world_types)} designated for .apworld"
folders_to_remove: list[str] = []
@@ -381,25 +382,15 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
file_name = os.path.split(os.path.dirname(worldtype.__file__))[1]
world_directory = self.libfolder / "worlds" / file_name
if os.path.isfile(world_directory / "archipelago.json"):
with open(os.path.join(world_directory, "archipelago.json"), mode="r", encoding="utf-8") as manifest_file:
manifest = json.load(manifest_file)
assert "game" in manifest, (
f"World directory {world_directory} has an archipelago.json manifest file, but it"
"does not define a \"game\"."
)
assert manifest["game"] == worldtype.game, (
f"World directory {world_directory} has an archipelago.json manifest file, but value of the"
f"\"game\" field ({manifest['game']} does not equal the World class's game ({worldtype.game})."
)
manifest = json.load(open(world_directory / "archipelago.json"))
else:
manifest = {}
# this method creates an apworld that cannot be moved to a different OS or minor python version,
# which should be ok
zip_path = self.libfolder / "worlds" / (file_name + ".apworld")
apworld = APWorldContainer(str(zip_path))
apworld.minimum_ap_version = version_tuple
apworld.maximum_ap_version = version_tuple
apworld.minimum_ap_version = version
apworld.maximum_ap_version = version
apworld.game = worldtype.game
manifest.update(apworld.get_manifest())
apworld.manifest_path = f"{file_name}/archipelago.json"

View File

@@ -1,227 +0,0 @@
#!/usr/bin/env python
# based on python-websockets compression benchmark (c) Aymeric Augustin and contributors
# https://github.com/python-websockets/websockets/blob/main/experiments/compression/benchmark.py
import collections
import time
import zlib
from typing import Iterable
REPEAT = 10
WB, ML = 12, 5 # defaults used as a reference
WBITS = range(9, 16)
MEMLEVELS = range(1, 10)
def benchmark(data: Iterable[bytes]) -> None:
size: dict[int, dict[int, float]] = collections.defaultdict(dict)
duration: dict[int, dict[int, float]] = collections.defaultdict(dict)
for wbits in WBITS:
for memLevel in MEMLEVELS:
encoder = zlib.compressobj(wbits=-wbits, memLevel=memLevel)
encoded = []
print(f"Compressing {REPEAT} times with {wbits=} and {memLevel=}")
t0 = time.perf_counter()
for _ in range(REPEAT):
for item in data:
# Taken from PerMessageDeflate.encode
item = encoder.compress(item) + encoder.flush(zlib.Z_SYNC_FLUSH)
if item.endswith(b"\x00\x00\xff\xff"):
item = item[:-4]
encoded.append(item)
t1 = time.perf_counter()
size[wbits][memLevel] = sum(len(item) for item in encoded) / REPEAT
duration[wbits][memLevel] = (t1 - t0) / REPEAT
raw_size = sum(len(item) for item in data)
print("=" * 79)
print("Compression ratio")
print("=" * 79)
print("\t".join(["wb \\ ml"] + [str(memLevel) for memLevel in MEMLEVELS]))
for wbits in WBITS:
print(
"\t".join(
[str(wbits)]
+ [
f"{100 * (1 - size[wbits][memLevel] / raw_size):.1f}%"
for memLevel in MEMLEVELS
]
)
)
print("=" * 79)
print()
print("=" * 79)
print("CPU time")
print("=" * 79)
print("\t".join(["wb \\ ml"] + [str(memLevel) for memLevel in MEMLEVELS]))
for wbits in WBITS:
print(
"\t".join(
[str(wbits)]
+ [
f"{1000 * duration[wbits][memLevel]:.1f}ms"
for memLevel in MEMLEVELS
]
)
)
print("=" * 79)
print()
print("=" * 79)
print(f"Size vs. {WB} \\ {ML}")
print("=" * 79)
print("\t".join(["wb \\ ml"] + [str(memLevel) for memLevel in MEMLEVELS]))
for wbits in WBITS:
print(
"\t".join(
[str(wbits)]
+ [
f"{100 * (size[wbits][memLevel] / size[WB][ML] - 1):.1f}%"
for memLevel in MEMLEVELS
]
)
)
print("=" * 79)
print()
print("=" * 79)
print(f"Time vs. {WB} \\ {ML}")
print("=" * 79)
print("\t".join(["wb \\ ml"] + [str(memLevel) for memLevel in MEMLEVELS]))
for wbits in WBITS:
print(
"\t".join(
[str(wbits)]
+ [
f"{100 * (duration[wbits][memLevel] / duration[WB][ML] - 1):.1f}%"
for memLevel in MEMLEVELS
]
)
)
print("=" * 79)
print()
def generate_data_package_corpus() -> list[bytes]:
# compared to default 12, 5:
# 11, 4 saves 16K RAM, gives +4.6% size, -5.0% time .. +1.1% time
# 10, 4 saves 20K RAM, gives +10.2% size, -3.8% time .. +0.6% time
# 11, 3 saves 20K RAM, gives +6.5% size, +14.2% time
# 10, 3 saves 24K RAM, gives +12.8% size, +0.5% time .. +6.9% time
# NOTE: time delta is highly unstable; time is ~100ms
import warnings
with warnings.catch_warnings():
warnings.simplefilter("ignore")
from NetUtils import encode
from worlds import network_data_package
return [encode(network_data_package).encode("utf-8")]
def generate_solo_release_corpus() -> list[bytes]:
# compared to default 12, 5:
# 11, 4 saves 16K RAM, gives +0.9% size, +3.9% time
# 10, 4 saves 20K RAM, gives +1.4% size, +3.4% time
# 11, 3 saves 20K RAM, gives +1.8% size, +13.9% time
# 10, 3 saves 24K RAM, gives +2.1% size, +4.8% time
# NOTE: time delta is highly unstable; time is ~0.4ms
from random import Random
from MultiServer import json_format_send_event
from NetUtils import encode, NetworkItem
r = Random()
r.seed(0)
solo_release = []
solo_release_locations = [r.randint(1000, 1999) for _ in range(200)]
solo_release_items = sorted([r.randint(1000, 1999) for _ in range(200)]) # currently sorted by item
solo_player = 1
for location, item in zip(solo_release_locations, solo_release_items):
flags = r.choice((0, 0, 0, 0, 0, 0, 0, 1, 2, 3))
network_item = NetworkItem(item, location, solo_player, flags)
solo_release.append(json_format_send_event(network_item, solo_player))
solo_release.append({
"cmd": "ReceivedItems",
"index": 0,
"items": solo_release_items,
})
solo_release.append({
"cmd": "RoomUpdate",
"hint_points": 200,
"checked_locations": solo_release_locations,
})
return [encode(solo_release).encode("utf-8")]
def generate_gameplay_corpus() -> list[bytes]:
# compared to default 12, 5:
# 11, 4 saves 16K RAM, gives +13.6% size, +4.1% time
# 10, 4 saves 20K RAM, gives +22.3% size, +2.2% time
# 10, 3 saves 24K RAM, gives +26.2% size, +1.6% time
# NOTE: time delta is highly unstable; time is 4ms
from copy import copy
from random import Random
from MultiServer import json_format_send_event
from NetUtils import encode, NetworkItem
r = Random()
r.seed(0)
gameplay = []
observer = 1
hint_points = 0
index = 0
players = list(range(1, 10))
player_locations = {player: [r.randint(1000, 1999) for _ in range(200)] for player in players}
player_items = {player: [r.randint(1000, 1999) for _ in range(200)] for player in players}
player_receiver = {player: [r.randint(1, len(players)) for _ in range(200)] for player in players}
for i in range(0, len(player_locations[1])):
player_sequence = copy(players)
r.shuffle(player_sequence)
for finder in player_sequence:
flags = r.choice((0, 0, 0, 0, 0, 0, 0, 1, 2, 3))
receiver = player_receiver[finder][i]
item = player_items[finder][i]
location = player_locations[finder][i]
network_item = NetworkItem(item, location, receiver, flags)
gameplay.append(json_format_send_event(network_item, observer))
if finder == observer:
hint_points += 1
gameplay.append({
"cmd": "RoomUpdate",
"hint_points": hint_points,
"checked_locations": [location],
})
if receiver == observer:
gameplay.append({
"cmd": "ReceivedItems",
"index": index,
"items": [item],
})
index += 1
return [encode(gameplay).encode("utf-8")]
def main() -> None:
#corpus = generate_data_package_corpus()
#corpus = generate_solo_release_corpus()
#corpus = generate_gameplay_corpus()
corpus = generate_data_package_corpus() + generate_solo_release_corpus() + generate_gameplay_corpus()
benchmark(corpus)
print(f"raw size: {sum(len(data) for data in corpus)}")
if __name__ == "__main__":
main()

View File

@@ -1,12 +1,4 @@
def run_locations_benchmark(freeze_gc: bool = True) -> None:
"""
Run a benchmark of location access rule performance against an empty_state and an all_state.
:param freeze_gc: Whether to freeze gc before benchmarking and unfreeze gc afterward. Freezing gc moves all objects
tracked by the garbage collector to a permanent generation, ignoring them in all future collections. Freezing
greatly reduces the duration of running gc.collect() within benchmarks, which otherwise often takes much longer
than running all iterations for the location rule being benchmarked.
"""
def run_locations_benchmark():
import argparse
import logging
import gc
@@ -42,8 +34,6 @@ def run_locations_benchmark(freeze_gc: bool = True) -> None:
return "\n".join(f" {time:.4f} in {name}" for name, time in counter.most_common(top))
def location_test(self, test_location: Location, state: CollectionState, state_name: str) -> float:
if freeze_gc:
gc.freeze()
with TimeIt(f"{test_location.game} {self.rule_iterations} "
f"runs of {test_location}.access_rule({state_name})", logger) as t:
for _ in range(self.rule_iterations):
@@ -51,8 +41,6 @@ def run_locations_benchmark(freeze_gc: bool = True) -> None:
# if time is taken to disentangle complex ref chains,
# this time should be attributed to the rule.
gc.collect()
if freeze_gc:
gc.unfreeze()
return t.dif
def main(self):
@@ -76,13 +64,9 @@ def run_locations_benchmark(freeze_gc: bool = True) -> None:
gc.collect()
for step in self.gen_steps:
if freeze_gc:
gc.freeze()
with TimeIt(f"{game} step {step}", logger):
call_all(multiworld, step)
gc.collect()
if freeze_gc:
gc.unfreeze()
locations = sorted(multiworld.get_unfilled_locations())
if not locations:

View File

@@ -6,9 +6,9 @@ from Utils import get_intended_text, get_input_text_from_response
class TestClient(unittest.TestCase):
def test_autofill_hint_from_fuzzy_hint(self) -> None:
tests = (
("item", ["item1", "item2"]), # Multiple close matches
("itm", ["item1", "item21"]), # No close match, multiple option
("item", ["item1"]), # No close match, single option
("item", ["item1", "item2"]), # Multiple close matches
("itm", ["item1", "item21"]), # No close match, multiple option
("item", ["item1"]), # No close match, single option
("item", ["\"item\" 'item' (item)"]), # Testing different special characters
)
@@ -16,7 +16,7 @@ class TestClient(unittest.TestCase):
item_name, usable, response = get_intended_text(input_text, possible_answers)
self.assertFalse(usable, "This test must be updated, it seems get_fuzzy_results behavior changed")
hint_command = get_input_text_from_response(response, "!hint")
hint_command = get_input_text_from_response(response, "hint")
self.assertIsNotNone(hint_command,
"The response to fuzzy hints is no longer recognized by the hint autofill")
self.assertEqual(hint_command, f"!hint {item_name}",

View File

@@ -1,7 +1,7 @@
import unittest
from BaseClasses import PlandoOptions
from Options import Choice, ItemLinks, PlandoConnections, PlandoItems, PlandoTexts
from Options import ItemLinks, Choice
from Utils import restricted_dumps
from worlds.AutoWorld import AutoWorldRegister
@@ -72,8 +72,8 @@ class TestOptions(unittest.TestCase):
for link in item_links.values():
self.assertEqual(link.value[0], item_link_group[0])
def test_pickle_dumps_default(self):
"""Test that default option values can be pickled into database for WebHost generation"""
def test_pickle_dumps(self):
"""Test options can be pickled into database for WebHost generation"""
for gamename, world_type in AutoWorldRegister.world_types.items():
if not world_type.hidden:
for option_key, option in world_type.options_dataclass.type_hints.items():
@@ -81,23 +81,3 @@ class TestOptions(unittest.TestCase):
restricted_dumps(option.from_any(option.default))
if issubclass(option, Choice) and option.default in option.name_lookup:
restricted_dumps(option.from_text(option.name_lookup[option.default]))
def test_pickle_dumps_plando(self):
"""Test that plando options using containers of a custom type can be pickled"""
# The base PlandoConnections class can't be instantiated directly, create a subclass and then cast it
class TestPlandoConnections(PlandoConnections):
entrances = {"An Entrance"}
exits = {"An Exit"}
plando_connection_value = PlandoConnections(
TestPlandoConnections.from_any([{"entrance": "An Entrance", "exit": "An Exit"}])
)
plando_values = {
"PlandoConnections": plando_connection_value,
"PlandoItems": PlandoItems.from_any([{"item": "Something", "location": "Somewhere"}]),
"PlandoTexts": PlandoTexts.from_any([{"text": "Some text.", "at": "text_box"}]),
}
for option_key, value in plando_values.items():
with self.subTest(option=option_key):
restricted_dumps(value)

View File

@@ -1,14 +0,0 @@
import unittest
from Utils import DaemonThreadPoolExecutor
class DaemonThreadPoolExecutorTest(unittest.TestCase):
def test_is_daemon(self) -> None:
def run() -> None:
pass
with DaemonThreadPoolExecutor(1) as executor:
executor.submit(run)
self.assertTrue(next(iter(executor._threads)).daemon)

View File

@@ -175,12 +175,12 @@ class APWorldContainer(APContainer):
maximum_ap_version: "Version | None" = None
def read_contents(self, opened_zipfile: zipfile.ZipFile) -> Dict[str, Any]:
from Utils import tuplize_version
from Utils import tuplize_version, Version
manifest = super().read_contents(opened_zipfile)
self.game = manifest["game"]
for version_key in ("world_version", "minimum_ap_version", "maximum_ap_version"):
if version_key in manifest:
setattr(self, version_key, tuplize_version(manifest[version_key]))
setattr(self, version_key, Version(*tuplize_version(manifest[version_key])))
return manifest
def get_manifest(self) -> Dict[str, Any]:

View File

@@ -180,7 +180,7 @@ def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, path
if found_already_loaded and is_kivy_running():
raise Exception(f"Installed APWorld successfully, but '{module_name}' is already loaded, "
"so a Launcher restart is required to use the new installation.")
world_source = worlds.WorldSource(str(target), is_zip=True, relative=False)
world_source = worlds.WorldSource(str(target), is_zip=True)
bisect.insort(worlds.world_sources, world_source)
world_source.load()
@@ -217,6 +217,8 @@ components: List[Component] = [
description="Install an APWorld to play games not included with Archipelago by default."),
Component('Text Client', 'CommonClient', 'ArchipelagoTextClient', func=launch_textclient,
description="Connect to a multiworld using the text client."),
Component('Links Awakening DX Client', 'LinksAwakeningClient',
file_identifier=SuffixIdentifier('.apladx')),
Component('LttP Adjuster', 'LttPAdjuster'),
# Ocarina of Time
Component('OoT Client', 'OoTClient',
@@ -242,46 +244,21 @@ icon_paths = {
}
if not is_frozen():
def _build_apworlds(*launch_args: str):
def _build_apworlds():
import json
import os
import zipfile
from worlds import AutoWorldRegister
from worlds.Files import APWorldContainer
from Launcher import open_folder
import argparse
parser = argparse.ArgumentParser("Build script for APWorlds")
parser.add_argument("worlds", type=str, default=(), nargs="*", help="Names of APWorlds to build.")
args = parser.parse_args(launch_args)
if args.worlds:
games = [(game, AutoWorldRegister.world_types.get(game, None)) for game in args.worlds]
else:
games = [(worldname, worldtype) for worldname, worldtype in AutoWorldRegister.world_types.items()
if not worldtype.zip_path]
apworlds_folder = os.path.join("build", "apworlds")
os.makedirs(apworlds_folder, exist_ok=True)
for worldname, worldtype in games:
if not worldtype:
logging.error(f"Requested APWorld \"{worldname}\" does not exist.")
continue
for worldname, worldtype in AutoWorldRegister.world_types.items():
file_name = os.path.split(os.path.dirname(worldtype.__file__))[1]
world_directory = os.path.join("worlds", file_name)
if os.path.isfile(os.path.join(world_directory, "archipelago.json")):
with open(os.path.join(world_directory, "archipelago.json"), mode="r", encoding="utf-8") as manifest_file:
manifest = json.load(manifest_file)
assert "game" in manifest, (
f"World directory {world_directory} has an archipelago.json manifest file, but it"
"does not define a \"game\"."
)
assert manifest["game"] == worldtype.game, (
f"World directory {world_directory} has an archipelago.json manifest file, but value of the"
f"\"game\" field ({manifest['game']} does not equal the World class's game ({worldtype.game})."
)
manifest = json.load(open(os.path.join(world_directory, "archipelago.json")))
else:
manifest = {}
@@ -292,15 +269,12 @@ if not is_frozen():
apworld.manifest_path = f"{file_name}/archipelago.json"
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED,
compresslevel=9) as zf:
for path in pathlib.Path(world_directory).rglob("*"):
for path in pathlib.Path(world_directory).rglob("*.*"):
relative_path = os.path.join(*path.parts[path.parts.index("worlds") + 1:])
if "__MACOSX" in relative_path or ".DS_STORE" in relative_path or "__pycache__" in relative_path:
continue
if not relative_path.endswith("archipelago.json"):
zf.write(path, relative_path)
zf.writestr(apworld.manifest_path, json.dumps(manifest))
open_folder(apworlds_folder)
components.append(Component('Build APWorlds', func=_build_apworlds, cli=True,
description="Build APWorlds from loose-file world folders."))
components.append(Component('Build apworlds', func=_build_apworlds, cli=True,))

View File

@@ -122,14 +122,14 @@ for world_source in world_sources:
for dirpath, dirnames, filenames in os.walk(world_source.resolved_path):
for file in filenames:
if file.endswith("archipelago.json"):
with open(os.path.join(dirpath, file), mode="r", encoding="utf-8") as manifest_file:
manifest = json.load(manifest_file)
manifest = json.load(open(os.path.join(dirpath, file), "r"))
break
if manifest:
break
game = manifest.get("game")
if game in AutoWorldRegister.world_types:
AutoWorldRegister.world_types[game].world_version = tuplize_version(manifest.get("world_version", "0.0.0"))
AutoWorldRegister.world_types[game].world_version = Version(*tuplize_version(manifest.get("world_version",
"0.0.0")))
if apworlds:
# encapsulation for namespace / gc purposes

View File

@@ -23,7 +23,7 @@ game you play will make sure that every game has its own save game.
Unzip the Aquaria randomizer release and copy all unzipped files in the Aquaria game folder. The unzipped files are:
- aquaria_randomizer.exe
- OpenAL32.dll
- randomizer_files (directory)
- override (directory)
- SDL2.dll
- usersettings.xml
- wrap_oal.dll
@@ -32,10 +32,7 @@ Unzip the Aquaria randomizer release and copy all unzipped files in the Aquaria
If there is a conflict between files in the original game folder and the unzipped files, you should overwrite
the original files with the ones from the unzipped randomizer.
There is multiple way to start the game. The easiest one is using the launcher. To do that, just run
the `aquaria_randomizer.exe` file.
You can also launch the randomizer using the command line interface (you can open the command line interface
Finally, to launch the randomizer, you must use the command line interface (you can open the command line interface
by typing `cmd` in the address bar of the Windows File Explorer). Here is the command line used to start the
randomizer:
@@ -52,17 +49,15 @@ aquaria_randomizer.exe --name YourName --server theServer:thePort --password th
### Linux when using the AppImage
If you use the AppImage, just copy it into the Aquaria game folder. You then have to make it executable. You
can do that from the command line by using:
can do that from command line by using:
```bash
chmod +x Aquaria_Randomizer-*.AppImage
```
or by using the Graphical file Explorer of your system (the permission can generally be set in the file properties).
or by using the Graphical Explorer of your system.
To launch the randomizer using the integrated launcher, just execute the AppImage file.
You can also use command line arguments to set the server and slot of your game:
To launch the randomizer, just launch in command line:
```bash
./Aquaria_Randomizer-*.AppImage --name YourName --server theServer:thePort
@@ -84,7 +79,7 @@ the original game will stop working. Copying the folder will guarantee that the
Untar the Aquaria randomizer release and copy all extracted files in the Aquaria game folder. The extracted files are:
- aquaria_randomizer
- randomizer_files (directory)
- override (directory)
- usersettings.xml
- cacert.pem
@@ -92,7 +87,7 @@ If there is a conflict between files in the original game folder and the extract
the original files with the ones from the extracted randomizer files.
Then, you should use your system package manager to install `liblua5`, `libogg`, `libvorbis`, `libopenal` and `libsdl2`.
On Debian base systems (like Ubuntu), you can use the following command:
On Debian base system (like Ubuntu), you can use the following command:
```bash
sudo apt install liblua5.1-0-dev libogg-dev libvorbis-dev libopenal-dev libsdl2-dev
@@ -102,9 +97,7 @@ Also, if there are certain `.so` files in the original Aquaria game folder (`lib
`libSDL-1.2.so.0` and `libstdc++.so.6`), you should remove them from the Aquaria Randomizer game folder. Those are
old libraries that will not work on the recent build of the randomizer.
To launch the randomizer using the integrated launcher, just execute the `aquaria_randomizer` file.
You can also use command line arguments to set the server and slot of your game:
To launch the randomizer, just launch in command line:
```bash
./aquaria_randomizer --name YourName --server theServer:thePort
@@ -122,20 +115,6 @@ sure that your executable has executable permission:
```bash
chmod +x aquaria_randomizer
```
### Steam deck
On the Steamdeck, go in desktop mode and follow the same procedure as the Linux Appimage.
### No sound on Linux/Steam deck
If your game play without problems, but with no sound, the game probably does not use the correct
driver for the sound system. To fix that, you can use `ALSOFT_DRIVERS=pulse` before your command
line to make it work. Something like this (depending on the way you launch the randomizer):
```bash
ALSOFT_DRIVERS=pulse ./Aquaria_Randomizer-*.AppImage --name YourName --server theServer:thePort
```
## Auto-Tracking

View File

@@ -2,12 +2,12 @@
## Logiciels nécessaires
- Une copie du jeu Aquaria non modifiée (disponible sur la majorité des sites de ventes de jeux vidéos en ligne)
- Une copie du jeu Aquaria non-modifiée (disponible sur la majorité des sites de ventes de jeux vidéos en ligne)
- Le client du Randomizer d'Aquaria [Aquaria randomizer](https://github.com/tioui/Aquaria_Randomizer/releases/latest)
## Logiciels optionnels
- De manière optionnelle, pour pouvoir envoyer des [commandes](/tutorial/Archipelago/commands/en) comme `!hint`: utilisez le client texte de [la version la plus récente d'Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest)
- De manière optionnel, pour pouvoir envoyer des [commandes](/tutorial/Archipelago/commands/en) comme `!hint`: utilisez le client texte de [la version la plus récente d'Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest)
- [Aquaria AP Tracker](https://github.com/palex00/aquaria-ap-tracker/releases/latest), pour utiliser avec [PopTracker](https://github.com/black-sliver/PopTracker/releases/latest)
## Procédures d'installation et d'exécution
@@ -25,7 +25,7 @@ Désarchiver le randomizer d'Aquaria et copier tous les fichiers de l'archive da
fichier d'archive devrait contenir les fichiers suivants:
- aquaria_randomizer.exe
- OpenAL32.dll
- randomizer_files (directory)
- override (directory)
- SDL2.dll
- usersettings.xml
- wrap_oal.dll
@@ -34,10 +34,7 @@ fichier d'archive devrait contenir les fichiers suivants:
S'il y a des conflits entre les fichiers de l'archive zip et les fichiers du jeu original, vous devez utiliser
les fichiers contenus dans l'archive zip.
Il y a plusieurs manières de lancer le randomizer. Le plus simple consiste à utiliser le lanceur intégré en
exécutant simplement le fichier `aquaria_randomizer.exe`.
Il est également possible de lancer le randomizer en utilisant la ligne de commande (vous pouvez ouvrir une interface de
Finalement, pour lancer le randomizer, vous devez utiliser la ligne de commande (vous pouvez ouvrir une interface de
ligne de commande, entrez l'adresse `cmd` dans la barre d'adresse de l'explorateur de fichier de Windows). Voici
la ligne de commande à utiliser pour lancer le randomizer:
@@ -60,12 +57,9 @@ le mettre exécutable. Vous pouvez mettre le fichier exécutable avec la command
chmod +x Aquaria_Randomizer-*.AppImage
```
ou bien en utilisant l'explorateur de fichier graphique de votre système (la permission d'exécution est
généralement dans les propriétés du fichier).
ou bien en utilisant l'explorateur graphique de votre système.
Pour lancer le randomizer en utilisant le lanceur intégré, seulement exécuter le fichier AppImage.
Vous pouvez également lancer le randomizer en spécifiant les informations de connexion dans les arguments de la ligne de commande:
Pour lancer le randomizer, utiliser la commande suivante:
```bash
./Aquaria_Randomizer-*.AppImage --name VotreNom --server LeServeur:LePort
@@ -89,7 +83,7 @@ avant de déposer le randomizer à l'intérieur permet de vous assurer de garder
Désarchiver le fichier tar et copier tous les fichiers qu'il contient dans le répertoire du jeu d'origine d'Aquaria. Les
fichiers extraient du fichier tar devraient être les suivants:
- aquaria_randomizer
- randomizer_files (directory)
- override (directory)
- usersettings.xml
- cacert.pem
@@ -108,10 +102,7 @@ Notez également que s'il y a des fichiers ".so" dans le répertoire d'Aquaria (
`libSDL-1.2.so.0` and `libstdc++.so.6`), vous devriez les retirer. Il s'agit de vieille version des librairies qui
ne sont plus fonctionnelles dans les systèmes modernes et qui pourrait empêcher le randomizer de fonctionner.
Pour lancer le randomizer en utilisant le lanceur intégré, seulement exécuter le fichier `aquaria_randomizer`.
Vous pouvez également lancer le randomizer en spécifiant les information de connexion dans les arguments de la
ligne de commande:
Pour lancer le randomizer, utiliser la commande suivante:
```bash
./aquaria_randomizer --name VotreNom --server LeServeur:LePort
@@ -129,21 +120,6 @@ pour vous assurer que votre fichier est exécutable:
```bash
chmod +x aquaria_randomizer
```
### Steam Deck
Pour installer le randomizer sur la Steam Deck, seulement suivre la procédure pour les fichiers AppImage
indiquée précédemment.
### Aucun son sur Linux/Steam Deck
Si le jeu fonctionne sans problème, mais qu'il n'y a aucun son, c'est probablement parce que le jeu
n'arrive pas à utiliser le bon pilote de son. Généralement, le problème est réglé en ajoutant la
variable d'environnement `ALSOFT_DRIVERS=pulse`. Voici un exemple (peut varier en fonction de la manière
que le randomizer est lancé):
```bash
ALSOFT_DRIVERS=pulse ./Aquaria_Randomizer-*.AppImage --name VotreNom --server LeServeur:LePort
```
## Tracking automatique

View File

@@ -1,27 +0,0 @@
Modified MIT License
Copyright (c) 2025 PoryGone
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, and/or distribute copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to
the following conditions:
No copy or substantial portion of the Software shall be sublicensed or relicensed
without the express written permission of the copyright holder(s)
No copy or substantial portion of the Software shall be sold without the express
written permission of the copyright holder(s)
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,6 +0,0 @@
{
"game": "Celeste 64",
"authors": [ "PoryGone" ],
"minimum_ap_version": "0.6.3",
"world_version": "1.3.1"
}

View File

@@ -1,27 +0,0 @@
Modified MIT License
Copyright (c) 2025 PoryGone
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, and/or distribute copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to
the following conditions:
No copy or substantial portion of the Software shall be sublicensed or relicensed
without the express written permission of the copyright holder(s)
No copy or substantial portion of the Software shall be sold without the express
written permission of the copyright holder(s)
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,6 +0,0 @@
{
"game": "Celeste (Open World)",
"authors": [ "PoryGone" ],
"minimum_ap_version": "0.6.3",
"world_version": "1.0.5"
}

View File

@@ -20,7 +20,6 @@ class CivVIBoostData:
Prereq: List[str]
PrereqRequiredCount: int
Classification: str
EraRequired: bool = False
class GoodyHutRewardData(TypedDict):

View File

@@ -150,10 +150,7 @@ def generate_era_location_table() -> Dict[str, Dict[str, CivVILocationData]]:
location = CivVILocationData(
boost.Type, 0, 0, id_base, boost.EraType, CivVICheckType.BOOST
)
# If EraRequired is True, place the boost in its actual era
# Otherwise, place it in ERA_ANCIENT for early access
target_era = boost.EraType if boost.EraRequired else "ERA_ANCIENT"
era_locations[target_era][boost.Type] = location
era_locations["ERA_ANCIENT"][boost.Type] = location
id_base += 1
return era_locations

View File

@@ -210,8 +210,8 @@ boosts: List[CivVIBoostData] = [
CivVIBoostData(
"BOOST_TECH_SQUARE_RIGGING",
"ERA_RENAISSANCE",
["TECH_GUNPOWDER", "TECH_MILITARY_ENGINEERING", "TECH_MINING"],
3,
["TECH_GUNPOWDER"],
1,
"DEFAULT",
),
CivVIBoostData(
@@ -252,15 +252,15 @@ boosts: List[CivVIBoostData] = [
CivVIBoostData(
"BOOST_TECH_BALLISTICS",
"ERA_INDUSTRIAL",
["TECH_SIEGE_TACTICS", "TECH_MILITARY_ENGINEERING", "TECH_BRONZE_WORKING"],
3,
["TECH_SIEGE_TACTICS", "TECH_MILITARY_ENGINEERING"],
2,
"DEFAULT",
),
CivVIBoostData(
"BOOST_TECH_MILITARY_SCIENCE",
"ERA_INDUSTRIAL",
["TECH_BRONZE_WORKING", "TECH_STIRRUPS", "TECH_MINING"],
3,
["TECH_STIRRUPS"],
1,
"DEFAULT",
),
CivVIBoostData(
@@ -301,8 +301,8 @@ boosts: List[CivVIBoostData] = [
CivVIBoostData(
"BOOST_TECH_REPLACEABLE_PARTS",
"ERA_MODERN",
["TECH_MILITARY_SCIENCE", "TECH_MINING"],
2,
["TECH_MILITARY_SCIENCE"],
1,
"DEFAULT",
),
CivVIBoostData(
@@ -343,8 +343,8 @@ boosts: List[CivVIBoostData] = [
CivVIBoostData(
"BOOST_TECH_ADVANCED_FLIGHT",
"ERA_ATOMIC",
["TECH_FLIGHT", "TECH_REFINING", "TECH_MINING"],
3,
["TECH_FLIGHT"],
1,
"DEFAULT",
),
CivVIBoostData(
@@ -436,8 +436,8 @@ boosts: List[CivVIBoostData] = [
CivVIBoostData(
"BOOST_TECH_COMPOSITES",
"ERA_INFORMATION",
["TECH_COMBUSTION", "TECH_REFINING", "TECH_MINING"],
3,
["TECH_COMBUSTION"],
1,
"DEFAULT",
),
CivVIBoostData(
@@ -470,7 +470,7 @@ boosts: List[CivVIBoostData] = [
"TECH_ELECTRICITY",
"TECH_NUCLEAR_FISSION",
],
4,
1,
"DEFAULT",
),
CivVIBoostData(
@@ -651,11 +651,10 @@ boosts: List[CivVIBoostData] = [
),
CivVIBoostData(
"BOOST_CIVIC_FEUDALISM",
"ERA_CLASSICAL",
"ERA_MEDIEVAL",
[],
0,
"DEFAULT",
True,
),
CivVIBoostData(
"BOOST_CIVIC_CIVIL_SERVICE",
@@ -663,7 +662,6 @@ boosts: List[CivVIBoostData] = [
[],
0,
"DEFAULT",
True,
),
CivVIBoostData(
"BOOST_CIVIC_MERCENARIES",
@@ -792,7 +790,6 @@ boosts: List[CivVIBoostData] = [
[],
0,
"DEFAULT",
True
),
CivVIBoostData(
"BOOST_CIVIC_CONSERVATION",
@@ -888,7 +885,6 @@ boosts: List[CivVIBoostData] = [
["TECH_ROCKETRY"],
1,
"DEFAULT",
True
),
CivVIBoostData(
"BOOST_CIVIC_GLOBALIZATION",

View File

@@ -14,27 +14,25 @@ The following are required in order to play Civ VI in Archipelago:
- A copy of the game `Civilization VI` including the two expansions `Rise & Fall` and `Gathering Storm` (both the Steam and Epic version should work).
## Enabling the tuner
In the main menu, navigate to the "Game Options" page. On the "Game" menu, make sure that "Tuner (disables achievements)" is enabled.
## Mod Installation
1. Download and unzip the latest release of the mod from [GitHub](https://github.com/hesto2/civilization_archipelago_mod/releases/latest).
2. Copy the folder containing the mod files to your Civ VI mods folder. On Windows, this is usually located at `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods`. If you use OneDrive, check if the folder is instead located in your OneDrive file structure, and use that path when relevant in future steps.
3. After the Archipelago host generates a game, you should be given a `.apcivvi` file. You can open it as a zip file, you can do this by either right clicking it and opening it with a program that handles zip files (if you associate that file with the program it will open it with that program in the future by double clicking it), or by right clicking and renaming the file extension from `apcivvi` to `zip` (only works if you are displaying file extensions). You can also associate the file with the Archipelago Launcher and double click it and it will create a folder with the mod files inside of it.
3. After the Archipelago host generates a game, you should be given a `.apcivvi` file. Associate the file with the Archipelago Launcher and double click it.
4. Copy the contents of the zip file or folder it generated (the name of the folder should be the same as the apcivvi file) into your Civilization VI Archipelago Mod folder (there should be five files placed there from the `.apcivvi` file, overwrite if asked).
4. Copy the contents of the new folder it generates (it will have the same name as the `.apcivvi` file) into your Civilization VI Archipelago Mod folder. If double clicking the `.apcivvi` file doesn't generate a folder, you can instead open it as a zip file. You can do this by either right clicking it and opening it with a program that handles zip files, or by right clicking and renaming the file extension from `apcivvi` to `zip`.
5. Your mod path should look something like `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods\civilization_archipelago_mod`. If everything was done correctly you can now connect to the game.
5. Place the files generated from the `.apcivvi` in your archipelago mod folder (there should be five files placed there from the apcivvi file, overwrite if asked). Your mod path should look something like `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods\civilization_archipelago_mod`.
## Connecting to a game
## Configuring your game
1. In the main menu, navigate to the "Game Options" page. On the "Game" menu, make sure that "Tuner (disables achievements)" is enabled.
2. In the main menu, navigate to the "Additional Content" page, then go to "Mods" and make sure the Archipelago mod is enabled.
3. When starting the game make sure you are on the Gathering Storm ruleset in a Single Player game. Additionally you must start in the ancient era, other settings and game modes can be customised to your own liking. An important thing to note is that settings preset saves the mod list from when you created it, so if you want to use a setting preset with this you must create it after installing the Archipelago mod.
4. To connect to the room open the Archipelago Launcher, from within the launcher open the Civ6 client and connect to the room. Once connected to the room enter your slot name and if everything went right you should now be connected.
Make sure you enable the mod in the main title under Additional Content > Mods. When configuring your game, make sure to start the game in the Ancient Era and leave all settings related to starting technologies and civics as the defaults. Other than that, configure difficulty, AI, etc. as you normally would.
## Troubleshooting
@@ -53,8 +51,3 @@ The following are required in order to play Civ VI in Archipelago:
- If you still have any errors make sure the two expansions Rise & Fall and Gathering Storm are active in the mod selector (all the official DLC works without issues but Rise & Fall and Gathering Storm are required for the mod).
- If boostsanity is enabled and those items are not being sent out but regular techs are, make sure you placed the files from your new room in the mod folder.
- If you are neither receiving or sending items, make sure you have the correct client open. The client should be the Civ6 and NOT the Text Client.
- This should be compatible with a lot of other mods, but if you are having issues try disabling all mods other than the Archipelago mod and see if the problem still persists.

View File

@@ -105,78 +105,3 @@ class TestBoostsanityExcluded(CivVITestBase):
if "BOOST" in location.name:
found_locations += 1
self.assertEqual(found_locations, 0)
class TestBoostsanityEraRequired(CivVITestBase):
options = {
"boostsanity": "true",
"progression_style": "none",
"shuffle_goody_hut_rewards": "false",
}
def test_era_required_boosts_not_accessible_early(self) -> None:
# BOOST_CIVIC_FEUDALISM has EraRequired=True and ERA_CLASSICAL
# It should NOT be accessible in Ancient era
self.assertFalse(self.can_reach_location("BOOST_CIVIC_FEUDALISM"))
# BOOST_CIVIC_URBANIZATION has EraRequired=True and ERA_INDUSTRIAL
# It should NOT be accessible in Ancient era
self.assertFalse(self.can_reach_location("BOOST_CIVIC_URBANIZATION"))
# BOOST_CIVIC_SPACE_RACE has EraRequired=True and ERA_ATOMIC
# It should NOT be accessible in Ancient era
self.assertFalse(self.can_reach_location("BOOST_CIVIC_SPACE_RACE"))
# Regular boosts without EraRequired should be accessible
self.assertTrue(self.can_reach_location("BOOST_TECH_SAILING"))
self.assertTrue(self.can_reach_location("BOOST_CIVIC_MILITARY_TRADITION"))
def test_era_required_boosts_accessible_in_correct_era(self) -> None:
# Collect items to reach Classical era
self.collect_by_name(["Mining", "Bronze Working", "Astrology", "Writing",
"Irrigation", "Sailing", "Animal Husbandry",
"State Workforce", "Foreign Trade"])
# BOOST_CIVIC_FEUDALISM should now be accessible in Classical era
self.assertTrue(self.can_reach_location("BOOST_CIVIC_FEUDALISM"))
# BOOST_CIVIC_URBANIZATION still not accessible (requires Industrial)
self.assertFalse(self.can_reach_location("BOOST_CIVIC_URBANIZATION"))
# Collect more items to reach Industrial era
self.collect_all_but(["TECH_ROCKETRY"])
# Now BOOST_CIVIC_URBANIZATION should be accessible
self.assertTrue(self.can_reach_location("BOOST_CIVIC_URBANIZATION"))
class TestBoostsanityEraRequiredWithProgression(CivVITestBase):
options = {
"boostsanity": "true",
"progression_style": "eras_and_districts",
"shuffle_goody_hut_rewards": "false",
}
def test_era_required_with_progressive_eras(self) -> None:
# Collect all items except Progressive Era
self.collect_all_but(["Progressive Era"])
# Even with all other items, era-required boosts should not be accessible
self.assertFalse(self.can_reach_location("BOOST_CIVIC_FEUDALISM"))
self.assertFalse(self.can_reach_location("BOOST_CIVIC_URBANIZATION"))
# Collect enough Progressive Era items to reach Classical (needs 2)
self.collect(self.get_item_by_name("Progressive Era"))
self.collect(self.get_item_by_name("Progressive Era"))
# BOOST_CIVIC_FEUDALISM should now be accessible
self.assertTrue(self.can_reach_location("BOOST_CIVIC_FEUDALISM"))
# But BOOST_CIVIC_URBANIZATION still requires Industrial era (needs 5 total)
self.assertFalse(self.can_reach_location("BOOST_CIVIC_URBANIZATION"))
# Collect 3 more Progressive Era items to reach Industrial
self.collect_by_name(["Progressive Era", "Progressive Era", "Progressive Era"])
# Now BOOST_CIVIC_URBANIZATION should be accessible
self.assertTrue(self.can_reach_location("BOOST_CIVIC_URBANIZATION"))

View File

@@ -1,27 +0,0 @@
Modified MIT License
Copyright (c) 2025 PoryGone
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, and/or distribute copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to
the following conditions:
No copy or substantial portion of the Software shall be sublicensed or relicensed
without the express written permission of the copyright holder(s)
No copy or substantial portion of the Software shall be sold without the express
written permission of the copyright holder(s)
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,6 +0,0 @@
{
"game": "Donkey Kong Country 3",
"authors": [ "PoryGone" ],
"minimum_ap_version": "0.6.3",
"world_version": "1.1.0"
}

View File

@@ -2,7 +2,6 @@ import csv
import enum
import math
from dataclasses import dataclass, field
from functools import reduce
from random import Random
from typing import Dict, List, Set
@@ -62,7 +61,7 @@ def load_item_csv():
item_reader = csv.DictReader(file)
for item in item_reader:
id = int(item["id"]) if item["id"] else None
classification = reduce((lambda a, b: a | b), {ItemClassification[str_classification] for str_classification in item["classification"].split(",")})
classification = ItemClassification[item["classification"]]
groups = {Group[group] for group in item["groups"].split(",") if group}
items.append(ItemData(id, item["name"], classification, groups))
return items

View File

@@ -22,7 +22,7 @@ id,name,classification,groups
20,Wall Jump Pack,progression,"DLC,Freemium"
21,Health Bar Pack,useful,"DLC,Freemium"
22,Parallax Pack,filler,"DLC,Freemium"
23,Harmless Plants Pack,"progression,trap","DLC,Freemium"
23,Harmless Plants Pack,progression,"DLC,Freemium"
24,Death of Comedy Pack,progression,"DLC,Freemium"
25,Canadian Dialog Pack,filler,"DLC,Freemium"
26,DLC NPC Pack,progression,"DLC,Freemium"
1 id name classification groups
22 20 Wall Jump Pack progression DLC,Freemium
23 21 Health Bar Pack useful DLC,Freemium
24 22 Parallax Pack filler DLC,Freemium
25 23 Harmless Plants Pack progression,trap progression DLC,Freemium
26 24 Death of Comedy Pack progression DLC,Freemium
27 25 Canadian Dialog Pack filler DLC,Freemium
28 26 DLC NPC Pack progression DLC,Freemium

View File

@@ -16,7 +16,6 @@ logger = logging.getLogger("Client")
rom_name_location = 0x07FFE3
player_name_location = 0x07BCC0
locations_array_start = 0x200
locations_array_length = 0x100
items_obtained = 0x03
@@ -112,12 +111,6 @@ class FF1Client(BizHawkClient):
return True
async def set_auth(self, ctx: "BizHawkClientContext") -> None:
auth_raw = (await bizhawk.read(
ctx.bizhawk_ctx,
[(player_name_location, 0x40, self.rom)]))[0]
ctx.auth = str(auth_raw, "utf-8").replace("\x00", "").strip()
async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
if ctx.server is None:
return
@@ -211,7 +204,7 @@ class FF1Client(BizHawkClient):
write_list.append((location, [0], self.sram))
elif current_item_name in no_overworld_items:
if current_item_name == "Sigil":
location = 0x2B
location = 0x28
else:
location = 0x12
write_list.append((location, [1], self.sram))

View File

@@ -253,17 +253,5 @@
"CubeBot": 529,
"Sarda": 525,
"Fairy": 531,
"Lefein": 527,
"DeepDungeon32B_Chest144": 401,
"DeepDungeon30B_Chest145": 402,
"DeepDungeon29B_Chest146": 403,
"DeepDungeon29B_Chest147": 404,
"DeepDungeon40B_Chest186": 443,
"DeepDungeon38B_Chest188": 445,
"DeepDungeon36B_Chest189": 446,
"DeepDungeon33B_Chest190": 447,
"DeepDungeon40B_Chest191": 448,
"DeepDungeon41B_Chest192": 449,
"DeepDungeon34B_Chest193": 450,
"DeepDungeon39B_Chest194": 451
"Lefein": 527
}

View File

@@ -1,5 +1,4 @@
from typing import NamedTuple, Union
from typing_extensions import deprecated
import logging
from BaseClasses import Item, Tutorial, ItemClassification
@@ -50,8 +49,7 @@ class GenericWorld(World):
return Item(name, ItemClassification.filler, -1, self.player)
raise InvalidItemError(name)
@deprecated("worlds.generic.PlandoItem is deprecated and will be removed in the next version. "
"Use Options.PlandoItem(s) instead.")
class PlandoItem(NamedTuple):
item: str
location: str

View File

@@ -136,27 +136,6 @@ are rolling locally, ensure this file is edited to your liking **before** rollin
when running the Archipelago Installation software. If you have changed settings in this file, and would like to retain
them, you may rename the file to `options.yaml`.
### Playing with custom worlds
If you are generating locally, you can play with worlds that are not included in the Archipelago installation.
These worlds are packaged as `.apworld` files. To add a world to your installation, click the "Install APWorld" button
in the launcher and select the `.apworld` file you wish to install. Alternatively, you can drag the `.apworld` file
onto the launcher or double-click the file itself (if on Windows). Once the world is installed, it will function like
the worlds that are already packaged with Archipelago. Also note that while generation with custom worlds must be done
locally, these games can then be uploaded to the website for hosting and played as normal.
We strongly recommend that you ensure the source of the `.apworld` is safe and trustworthy before playing with a
custom world. Installed APWorlds are able to run custom code on your computer whenever you open Archipelago.
#### Alternate versions of included worlds
If you want to play with an alternate version of a game that is already included in Archipelago, you should also
remove the original APWorld after completing the above installation. To do so, go to your Archipelago installation
folder and navigate to the `lib/worlds` directory. Then move the `.apworld` or the folder corresponding to the game you
want to play an alternate version of to somewhere else as a backup. If you want to play this original again, then
restore the original version to `lib/worlds` and remove the alternate version, which is in the `custom_worlds` folder.
Note: Currently, this cannot be done on the Linux AppImage release.
## Hosting an Archipelago Server

View File

@@ -5,8 +5,6 @@
* A legal copy of Hollow Knight.
* Steam, Gog, and Xbox Game Pass versions of the game are supported.
* Windows, Mac, and Linux (including Steam Deck) are supported.
**Do NOT** install BepInEx, it is not required and is incompatible with most mods. Archipelago, along with the majority of mods use custom tooling pre-dating BepInEx, and they are only available through Lumafly and similar installers rather than sites like Nexus Mods.
## Installing the Archipelago Mod using Lumafly
1. Launch Lumafly and ensure it locates your Hollow Knight installation directory.

View File

@@ -5,10 +5,6 @@
* Tener una copia legal de Hollow Knight.
* Las versiones de Steam, GOG y Xbox Game Pass son compatibles
* Las versiones de Windows, Mac y Linux (Incluyendo Steam Deck) son compatibles
**NO** instales BepInEx, **no** es necesario y es incompatible con varios mods. Archipelago (y la mayoría de los mods)
usan herramientas más antiguas que BepInEx, que solo están disponibles por medio de instaladores de mods como Lumafly y
similares, en vez de sitios web como Nexus Mods.
## Instalación del mod de Archipelago con Lumafly
1. Ejecuta Lumafly y asegurate de localizar la carpeta de instalación de Hollow Knight
@@ -65,4 +61,4 @@ de Archipelago para generar un YAML usando una interfaz gráfica.
## Consejos y otros comandos
Mientras juegas en un multiworld, puedes interactuar con el servidor usando varios comandos listados en la
[guía de comandos](/tutorial/Archipelago/commands/en). Puedes usar el Cliente de Texto Archipelago para hacer esto,
que está incluido en la última versión del [software de Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest).
que está incluido en la última versión del [software de Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest).

View File

@@ -5,10 +5,6 @@
* Uma cópia legal de Hollow Knight.
* Versões Steam, Gog, e Xbox Game Pass do jogo são suportadas.
* Windows, Mac, e Linux (incluindo Steam Deck) são suportados.
**NÃO** instale o BepInEx, ele **não** é necessário e é incompatível com vários mods. O Archipelago (e a maioria dos mods)
usam ferramentas mais antigas do que o BepInEx, disponíveis apenas a partir de gerenciadores como o Lumafly e semelhantes,
ao invés de sites como o Nexus Mods.
## Instalando o mod Archipelago Mod usando Lumafly
1. Abra o Lumafly e confirme que ele localizou sua pasta de instalação do Hollow Knight.

View File

@@ -1,6 +0,0 @@
{
"game": "Kingdom Hearts 2",
"authors": [ "JaredWeakStrike" ],
"minimum_ap_version": "0.6.3",
"world_version": "2.0.0"
}

View File

@@ -3,7 +3,7 @@ from dataclasses import dataclass
import os.path
import typing
import logging
from Options import Choice, Toggle, DefaultOnToggle, Range, FreeText, PerGameCommonOptions, OptionGroup, Removed, StartInventoryPool
from Options import Choice, Toggle, DefaultOnToggle, Range, FreeText, PerGameCommonOptions, OptionGroup, Removed
from collections import defaultdict
import Utils
@@ -665,7 +665,6 @@ class LinksAwakeningOptions(PerGameCommonOptions):
tarins_gift: TarinsGift
overworld: Overworld
stabilize_item_pool: StabilizeItemPool
start_inventory_from_pool: StartInventoryPool
warp_improvements: Removed
additional_warp_points: Removed

View File

@@ -9,7 +9,6 @@ import settings
from BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, Tutorial
from Fill import fill_restrictive
from worlds.AutoWorld import WebWorld, World
from worlds.LauncherComponents import Component, components, SuffixIdentifier, Type, launch, icon_paths
from .Common import *
from . import ItemIconGuessing
from .Items import (DungeonItemData, DungeonItemType, ItemName, LinksAwakeningItem, TradeItemData,
@@ -30,19 +29,6 @@ from .Rom import LADXProcedurePatch, write_patch_data
DEVELOPER_MODE = False
def launch_client(*args):
from .LinksAwakeningClient import launch as ladx_launch
launch(ladx_launch, name=f"{LINKS_AWAKENING} Client", args=args)
components.append(Component(f"{LINKS_AWAKENING} Client",
func=launch_client,
component_type=Type.CLIENT,
icon=LINKS_AWAKENING,
file_identifier=SuffixIdentifier('.apladx')))
icon_paths[LINKS_AWAKENING] = "ap:worlds.ladx/assets/MarinV-3_small.png"
class LinksAwakeningSettings(settings.Group):
class RomFile(settings.UserFilePath):
"""File name of the Link's Awakening DX rom"""
@@ -225,6 +211,8 @@ class LinksAwakeningWorld(World):
def create_items(self) -> None:
itempool = []
exclude = [item.name for item in self.multiworld.precollected_items[self.player]]
self.prefill_original_dungeon = [ [], [], [], [], [], [], [], [], [] ]
self.prefill_own_dungeons = []
self.pre_fill_items = []
@@ -241,46 +229,50 @@ class LinksAwakeningWorld(World):
continue
item_name = ladxr_item_to_la_item_name[ladx_item_name]
for _ in range(count):
item = self.create_item(item_name)
if not self.options.tradequest and isinstance(item.item_data, TradeItemData):
location = self.multiworld.get_location(item.item_data.vanilla_location, self.player)
location.place_locked_item(item)
location.show_in_spoiler = False
continue
if isinstance(item.item_data, DungeonItemData):
item_type = item.item_data.ladxr_id[:-1]
shuffle_type = self.dungeon_item_types[item_type]
if item.item_data.dungeon_item_type == DungeonItemType.INSTRUMENT and shuffle_type == ShuffleInstruments.option_vanilla:
# Find instrument, lock
# TODO: we should be able to pinpoint the region we want, save a lookup table please
found = False
for r in self.multiworld.get_regions(self.player):
if r.dungeon_index != item.item_data.dungeon_index:
continue
for loc in r.locations:
if not isinstance(loc, LinksAwakeningLocation):
continue
if not isinstance(loc.ladxr_item, Instrument):
continue
loc.place_locked_item(item)
found = True
break
if found:
break
else:
if shuffle_type == DungeonItemShuffle.option_original_dungeon:
self.prefill_original_dungeon[item.item_data.dungeon_index - 1].append(item)
self.pre_fill_items.append(item)
elif shuffle_type == DungeonItemShuffle.option_own_dungeons:
self.prefill_own_dungeons.append(item)
self.pre_fill_items.append(item)
else:
itempool.append(item)
if item_name in exclude:
exclude.remove(item_name) # this is destructive. create unique list above
self.multiworld.itempool.append(self.create_item(self.get_filler_item_name()))
else:
itempool.append(item)
item = self.create_item(item_name)
if not self.options.tradequest and isinstance(item.item_data, TradeItemData):
location = self.multiworld.get_location(item.item_data.vanilla_location, self.player)
location.place_locked_item(item)
location.show_in_spoiler = False
continue
if isinstance(item.item_data, DungeonItemData):
item_type = item.item_data.ladxr_id[:-1]
shuffle_type = self.dungeon_item_types[item_type]
if item.item_data.dungeon_item_type == DungeonItemType.INSTRUMENT and shuffle_type == ShuffleInstruments.option_vanilla:
# Find instrument, lock
# TODO: we should be able to pinpoint the region we want, save a lookup table please
found = False
for r in self.multiworld.get_regions(self.player):
if r.dungeon_index != item.item_data.dungeon_index:
continue
for loc in r.locations:
if not isinstance(loc, LinksAwakeningLocation):
continue
if not isinstance(loc.ladxr_item, Instrument):
continue
loc.place_locked_item(item)
found = True
break
if found:
break
else:
if shuffle_type == DungeonItemShuffle.option_original_dungeon:
self.prefill_original_dungeon[item.item_data.dungeon_index - 1].append(item)
self.pre_fill_items.append(item)
elif shuffle_type == DungeonItemShuffle.option_own_dungeons:
self.prefill_own_dungeons.append(item)
self.pre_fill_items.append(item)
else:
itempool.append(item)
else:
itempool.append(item)
self.multi_key = self.generate_multi_key()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -106,38 +106,26 @@ def tree_zone_4_midway_bell(state, player):
def tree_zone_4_coins(state, player, coins):
auto_scroll = is_auto_scroll(state, player, "Tree Zone 4")
entryway = 14
hall = 4
first_trip_downstairs = 31
second_trip_downstairs = 15
downstairs_with_auto_scroll = 12
final_room = 10
reachable_coins_from_start = 0
reachable_coins_from_bell = 0
reachable_coins = 0
if has_pipe_up(state, player):
reachable_coins_from_start += entryway
reachable_coins += 14
if has_pipe_right(state, player):
reachable_coins_from_start += hall
reachable_coins += 4
if has_pipe_down(state, player):
if auto_scroll:
reachable_coins_from_start += downstairs_with_auto_scroll
else:
reachable_coins_from_start += final_room + first_trip_downstairs + second_trip_downstairs
if state.has("Tree Zone 4 Midway Bell", player):
if has_pipe_down(state, player) and (auto_scroll or not has_pipe_left(state, player)):
reachable_coins_from_bell += final_room
elif has_pipe_left(state, player) and not auto_scroll:
if has_pipe_down(state, player):
reachable_coins_from_bell += first_trip_downstairs
if has_pipe_right(state, player):
reachable_coins_from_bell += entryway + hall
reachable_coins += 10
if not auto_scroll:
reachable_coins += 46
elif state.has("Tree Zone 4 Midway Bell", player):
if not auto_scroll:
if has_pipe_left(state, player):
reachable_coins += 18
if has_pipe_down(state, player):
reachable_coins += 10
if has_pipe_up(state, player):
reachable_coins_from_bell += second_trip_downstairs + final_room
else:
reachable_coins_from_bell += entryway + hall
return coins <= max(reachable_coins_from_start, reachable_coins_from_bell)
reachable_coins += 46
elif has_pipe_down(state, player):
reachable_coins += 10
return coins <= reachable_coins
def tree_zone_5_boss(state, player):
@@ -251,9 +239,12 @@ def pumpkin_zone_4_coins(state, player, coins):
def mario_zone_1_normal_exit(state, player):
return has_pipe_right(state, player) and (not is_auto_scroll(state, player, "Mario Zone 1")
or state.has_any(["Mushroom", "Fire Flower", "Carrot",
"Mario Zone 1 Midway Bell"], player))
if has_pipe_right(state, player):
if state.has_any(["Mushroom", "Fire Flower", "Carrot", "Mario Zone 1 Midway Bell"], player):
return True
if is_auto_scroll(state, player, "Mario Zone 1"):
return True
return False
def mario_zone_1_midway_bell(state, player):

View File

@@ -1,5 +1,5 @@
{
"game": "Mega Man 2",
"world_version": "0.3.3",
"world_version": "0.3.2",
"minimum_ap_version": "0.6.4"
}

View File

@@ -327,6 +327,8 @@ def patch_rom(world: "MM2World", patch: MM2ProcedurePatch) -> None:
patch.write_byte(0x36089, pool[18]) # Intro
patch.write_byte(0x361F1, pool[19]) # Title
from Utils import __version__
patch.name = bytearray(f'MM2{__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0',
'utf8')[:21]

View File

@@ -58,10 +58,6 @@ FlashFixTarget1:
%org($808D, $0B)
FlashFixTarget2:
%org($A65C, $0B)
HeatFix:
CMP #$FF
%org($8015, $0D)
ClearRefreshHook:
; if we're already doing a fresh load of the stage select

View File

@@ -1,6 +0,0 @@
{
"game": "Ocarina of Time",
"authors": ["espeon65536"],
"world_version": "7.0.0",
"minimum_ap_version": "0.6.4"
}

View File

@@ -36,26 +36,6 @@ DATA_LOCATIONS = {
"CrashCheck4": (0x16DD, 1),
}
TRACKER_EVENT_FLAGS = [
0x77, # EVENT_BEAT_BROCK
0xbf, # EVENT_BEAT_MISTY
0x167, # EVENT_BEAT_LT_SURGE
0x1a9, # EVENT_BEAT_ERIKA
0x259, # EVENT_BEAT_KOGA
0x361, # EVENT_BEAT_SABRINA
0x299, # EVENT_BEAT_BLAINE
0x51, # EVENT_BEAT_VIRIDIAN_GYM_GIOVANNI
0x38, # EVENT_OAK_GOT_PARCEL
0x525, # EVENT_BEAT_ROUTE22_RIVAL_1ST_BATTLE
0x117, # EVENT_RESCUED_MR_FUJI
0x55c, # EVENT_GOT_SS_TICKET
0x78f, # EVENT_BEAT_SILPH_CO_GIOVANNI
0x901, # EVENT_BEAT_CHAMPION_RIVAL
]
assert len(TRACKER_EVENT_FLAGS) <= 32
location_map = {"Rod": {}, "EventFlag": {}, "Missable": {}, "Hidden": {}, "list": {}, "DexSanityFlag": {}}
location_bytes_bits = {}
for location in location_data:
@@ -81,7 +61,6 @@ class PokemonRBClient(BizHawkClient):
super().__init__()
self.auto_hints = set()
self.locations_array = None
self.tracker_bitfield = 0
self.disconnect_pending = False
self.set_deathlink = False
self.banking_command = None
@@ -257,22 +236,6 @@ class PokemonRBClient(BizHawkClient):
await ctx.send_msgs([{"cmd": "Bounce", "slots": [ctx.slot], "data": {"currentMap": data["CurrentMap"][0]}}])
self.current_map = data["CurrentMap"][0]
# TRACKER
tracker_bitfield = 0
for i, flag in enumerate(TRACKER_EVENT_FLAGS):
if data["EventFlag"][flag // 8] & (1 << (flag % 8)):
tracker_bitfield |= 1 << i
if tracker_bitfield != self.tracker_bitfield:
await ctx.send_msgs([{
"cmd": "Set",
"key": f"pokemon_rb_events_{ctx.team}_{ctx.slot}",
"default": 0,
"want_reply": False,
"operations": [{"operation": "or", "value": tracker_bitfield}],
}])
self.tracker_bitfield = tracker_bitfield
# VICTORY
if data["EventFlag"][280] & 1 and not ctx.finished_game:

View File

@@ -1,28 +0,0 @@
Modified MIT License
Copyright (c) 2025 PoryGone
Copyright (c) 2025 RaspberrySpaceJam
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, and/or distribute copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to
the following conditions:
No copy or substantial portion of the Software shall be sublicensed or relicensed
without the express written permission of the copyright holder(s)
No copy or substantial portion of the Software shall be sold without the express
written permission of the copyright holder(s)
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,6 +0,0 @@
{
"game": "Sonic Adventure 2 Battle",
"authors": [ "PoryGone", "RaspberrySpaceJam" ],
"minimum_ap_version": "0.6.3",
"world_version": "2.4.2"
}

View File

@@ -35,7 +35,6 @@ from .mission_tables import SC2Campaign, SC2Mission, SC2Race, MissionFlag
from .regions import create_mission_order
from .mission_order import SC2MissionOrder
from worlds.LauncherComponents import components, Component, launch as launch_component
from .mission_order.presets import sc2_options_presets
logger = logging.getLogger("Starcraft 2")
VICTORY_MODULO = 100
@@ -76,7 +75,6 @@ class Starcraft2WebWorld(WebWorld):
tutorials = [setup_en, setup_fr, custom_mission_orders_en]
game_info_languages = ["en", "fr"]
options_presets = sc2_options_presets
option_groups = option_groups

View File

@@ -1,447 +0,0 @@
from typing import Any, Dict
import Options as ap_options
from .. import options
from Options import Accessibility, ProgressionBalancing
from .. import item_names
from ..mission_tables import SC2Race, SC2Campaign
from ..options import (
# avoid import *
GameDifficulty, DifficultyDamageModifier, GameSpeed, DisableForcedCamera, SkipCutscenes, AllInMap, MissionOrder,
MaximumCampaignSize, TwoStartPositions, KeyMode, PlayerColorTerranRaynor, PlayerColorProtoss, PlayerColorZerg,
PlayerColorZergPrimal, PlayerColorNova, SelectedRaces, EnabledCampaigns, EnableRaceSwapVariants, EnableMissionRaceBalancing,
ShuffleCampaigns, ShuffleNoBuild, StarterUnit, RequiredTactics, EnableVoidTrade, VoidTradeAgeLimit, VoidTradeWorkers,
EnsureGenericItems, MinNumberOfUpgrades, MaxNumberOfUpgrades, MercenaryHighlanders, MaxUpgradeLevel, GenericUpgradeMissions,
GenericUpgradeResearch, GenericUpgradeResearchSpeedup, GenericUpgradeItems, KerriganPresence, KerriganLevelsPerMissionCompleted,
KerriganLevelsPerMissionCompletedCap, KerriganLevelItemSum, KerriganLevelItemDistribution, KerriganTotalLevelCap, StartPrimaryAbilities,
KerriganPrimalStatus, KerriganMaxActiveAbilities, KerriganMaxPassiveAbilities, EnableMorphling, WarCouncilNerfs, SpearOfAdunPresence,
SpearOfAdunPresentInNoBuild, SpearOfAdunPassiveAbilityPresence, SpearOfAdunPassivesPresentInNoBuild, SpearOfAdunMaxActiveAbilities,
SpearOfAdunMaxAutocastAbilities, GrantStoryTech, GrantStoryLevels, NovaMaxWeapons, NovaMaxGadgets, NovaGhostOfAChanceVariant,
TakeOverAIAllies, LockedItems, ExcludedItems, UnexcludedItems, ExcludedMissions, DifficultyCurve, ExcludeVeryHardMissions, VanillaItemsOnly,
ExcludeOverpoweredItems, VictoryCache, VanillaLocations, ExtraLocations, ChallengeLocations, MasteryLocations, BasebustLocations,
SpeedrunLocations, PreventativeLocations, FillerPercentage, MineralsPerItem, VespenePerItem, StartingSupplyPerItem, MaximumSupplyPerItem,
MaximumSupplyReductionPerItem, LowestMaximumSupply, ResearchCostReductionPerItem, FillerItemsDistribution, MissionOrderScouting,
CustomMissionOrder, OPTION_NAME
)
template_settings = {
# Free to play, 25 WoL missions, Terran only Golden Path. Matches the template.yaml.
OPTION_NAME[Accessibility]: Accessibility.option_full,
OPTION_NAME[ProgressionBalancing]: ProgressionBalancing.default,
OPTION_NAME[GameDifficulty]: GameDifficulty.option_normal,
OPTION_NAME[SelectedRaces]: {SC2Race.TERRAN.get_title()},
OPTION_NAME[MissionOrder]: MissionOrder.option_golden_path,
OPTION_NAME[RequiredTactics]: RequiredTactics.option_standard,
OPTION_NAME[EnabledCampaigns]: {SC2Campaign.WOL.campaign_name},
OPTION_NAME[EnableRaceSwapVariants]: EnableRaceSwapVariants.option_disabled,
OPTION_NAME[KeyMode]: KeyMode.option_disabled,
OPTION_NAME[MaximumCampaignSize]: 25,
OPTION_NAME[StarterUnit]: StarterUnit.option_balanced,
OPTION_NAME[NovaGhostOfAChanceVariant]: NovaGhostOfAChanceVariant.option_wol,
OPTION_NAME[TakeOverAIAllies]: TakeOverAIAllies.option_false,
OPTION_NAME[DifficultyCurve]: DifficultyCurve.option_standard,
OPTION_NAME[MasteryLocations]: MasteryLocations.option_disabled,
OPTION_NAME[MissionOrderScouting]: MissionOrderScouting.option_completed,
OPTION_NAME[MineralsPerItem]: 10,
OPTION_NAME[VespenePerItem]: 10,
OPTION_NAME[StartingSupplyPerItem]: 2,
OPTION_NAME[MaximumSupplyPerItem]: 1,
OPTION_NAME[ResearchCostReductionPerItem]: 2,
}
f2p_big_settings = {
# Free to play, all race 7x7 Grid
OPTION_NAME[Accessibility]: Accessibility.option_full,
OPTION_NAME[ProgressionBalancing]: ProgressionBalancing.default,
OPTION_NAME[GameDifficulty]: GameDifficulty.option_normal,
OPTION_NAME[SelectedRaces]: SelectedRaces.valid_keys,
OPTION_NAME[MissionOrder]: MissionOrder.option_grid,
OPTION_NAME[RequiredTactics]: RequiredTactics.option_standard,
OPTION_NAME[EnabledCampaigns]: {
SC2Campaign.WOL.campaign_name,
SC2Campaign.PROPHECY.campaign_name,
SC2Campaign.PROLOGUE.campaign_name
},
OPTION_NAME[EnableRaceSwapVariants]: EnableRaceSwapVariants.option_shuffle_all,
OPTION_NAME[EnableMissionRaceBalancing]: EnableMissionRaceBalancing.option_semi_balanced,
OPTION_NAME[KeyMode]: KeyMode.option_disabled,
OPTION_NAME[MaximumCampaignSize]: 48,
OPTION_NAME[TwoStartPositions]: TwoStartPositions.option_true,
OPTION_NAME[StarterUnit]: StarterUnit.option_balanced,
OPTION_NAME[EnableMorphling]: EnableMorphling.option_true,
OPTION_NAME[TakeOverAIAllies]: TakeOverAIAllies.option_false,
OPTION_NAME[DifficultyCurve]: DifficultyCurve.option_standard,
OPTION_NAME[VanillaLocations]: VanillaLocations.option_enabled,
OPTION_NAME[ExtraLocations]: ExtraLocations.option_enabled,
OPTION_NAME[ChallengeLocations]: ChallengeLocations.option_enabled,
OPTION_NAME[MasteryLocations]: MasteryLocations.option_disabled,
OPTION_NAME[KerriganPrimalStatus]: KerriganPrimalStatus.option_item,
OPTION_NAME[WarCouncilNerfs]: WarCouncilNerfs.option_false,
OPTION_NAME[SpearOfAdunPresence]: SpearOfAdunPresence.option_everywhere,
OPTION_NAME[SpearOfAdunPresentInNoBuild]: SpearOfAdunPresentInNoBuild.option_false,
OPTION_NAME[SpearOfAdunPassiveAbilityPresence]: SpearOfAdunPassiveAbilityPresence.option_everywhere,
OPTION_NAME[SpearOfAdunPassivesPresentInNoBuild]: SpearOfAdunPassivesPresentInNoBuild.option_false,
OPTION_NAME[MissionOrderScouting]: MissionOrderScouting.option_completed,
OPTION_NAME[MineralsPerItem]: 5,
OPTION_NAME[VespenePerItem]: 5,
OPTION_NAME[StartingSupplyPerItem]: 2,
OPTION_NAME[MaximumSupplyPerItem]: 1,
OPTION_NAME[ResearchCostReductionPerItem]: 2,
}
zerg_rush_settings = {
# Zerg only, Blitz, short (5 required, 15 total). Sync-friendly
OPTION_NAME[Accessibility]: Accessibility.option_full,
OPTION_NAME[ProgressionBalancing]: ProgressionBalancing.default,
OPTION_NAME[GameDifficulty]: GameDifficulty.option_hard,
OPTION_NAME[SelectedRaces]: {SC2Race.ZERG.get_title()},
OPTION_NAME[MissionOrder]: MissionOrder.option_blitz,
OPTION_NAME[RequiredTactics]: RequiredTactics.option_advanced,
OPTION_NAME[EnabledCampaigns]: EnabledCampaigns.valid_keys,
OPTION_NAME[EnableRaceSwapVariants]: EnableRaceSwapVariants.option_shuffle_all,
OPTION_NAME[KeyMode]: KeyMode.option_disabled,
OPTION_NAME[MaximumCampaignSize]: 15,
OPTION_NAME[StarterUnit]: StarterUnit.option_balanced,
OPTION_NAME[EnableMorphling]: EnableMorphling.option_true,
OPTION_NAME[TakeOverAIAllies]: TakeOverAIAllies.option_false,
OPTION_NAME[DifficultyCurve]: DifficultyCurve.option_standard,
OPTION_NAME[VictoryCache]: 3,
OPTION_NAME[GenericUpgradeItems]: GenericUpgradeItems.option_bundle_all,
OPTION_NAME[VanillaLocations]: VanillaLocations.option_enabled,
OPTION_NAME[ExtraLocations]: ExtraLocations.option_enabled,
OPTION_NAME[ChallengeLocations]: ChallengeLocations.option_enabled,
OPTION_NAME[MasteryLocations]: MasteryLocations.option_disabled,
OPTION_NAME[ExcludeVeryHardMissions]: ExcludeVeryHardMissions.option_true,
OPTION_NAME[KerriganPrimalStatus]: KerriganPrimalStatus.option_item,
OPTION_NAME[MissionOrderScouting]: MissionOrderScouting.option_all,
OPTION_NAME[GrantStoryTech]: GrantStoryTech.option_grant,
OPTION_NAME[GrantStoryLevels]: GrantStoryLevels.option_minimum,
OPTION_NAME[MineralsPerItem]: 25,
OPTION_NAME[VespenePerItem]: 25,
OPTION_NAME[StartingSupplyPerItem]: 5,
OPTION_NAME[MaximumSupplyPerItem]: 2,
OPTION_NAME[ResearchCostReductionPerItem]: 5,
}
classic_grid_settings = {
# Short-ish 5x5 Grid (8 required, 24 total), all races. Sync-friendly
OPTION_NAME[Accessibility]: Accessibility.option_full,
OPTION_NAME[ProgressionBalancing]: ProgressionBalancing.default,
OPTION_NAME[GameDifficulty]: GameDifficulty.option_hard,
OPTION_NAME[SelectedRaces]: SelectedRaces.valid_keys,
OPTION_NAME[MissionOrder]: MissionOrder.option_grid,
OPTION_NAME[RequiredTactics]: RequiredTactics.option_standard,
OPTION_NAME[EnabledCampaigns]: EnabledCampaigns.valid_keys,
OPTION_NAME[EnableRaceSwapVariants]: EnableRaceSwapVariants.option_shuffle_all,
OPTION_NAME[EnableMissionRaceBalancing]: EnableMissionRaceBalancing.option_fully_balanced,
OPTION_NAME[KeyMode]: KeyMode.option_disabled,
OPTION_NAME[MaximumCampaignSize]: 24,
OPTION_NAME[TwoStartPositions]: TwoStartPositions.option_true,
OPTION_NAME[StarterUnit]: StarterUnit.option_balanced,
OPTION_NAME[EnableMorphling]: EnableMorphling.option_true,
OPTION_NAME[TakeOverAIAllies]: TakeOverAIAllies.option_false,
OPTION_NAME[DifficultyCurve]: DifficultyCurve.option_standard,
OPTION_NAME[VictoryCache]: 5,
OPTION_NAME[GenericUpgradeItems]: GenericUpgradeItems.option_bundle_all,
OPTION_NAME[VanillaLocations]: VanillaLocations.option_enabled,
OPTION_NAME[ExtraLocations]: ExtraLocations.option_enabled,
OPTION_NAME[ChallengeLocations]: ChallengeLocations.option_enabled,
OPTION_NAME[MasteryLocations]: MasteryLocations.option_disabled,
OPTION_NAME[ExcludeVeryHardMissions]: ExcludeVeryHardMissions.option_true,
OPTION_NAME[KerriganPrimalStatus]: KerriganPrimalStatus.option_item,
OPTION_NAME[WarCouncilNerfs]: WarCouncilNerfs.option_false,
OPTION_NAME[SpearOfAdunPresence]: SpearOfAdunPresence.option_any_race_lotv,
OPTION_NAME[SpearOfAdunPresentInNoBuild]: SpearOfAdunPresentInNoBuild.option_false,
OPTION_NAME[SpearOfAdunPassiveAbilityPresence]: SpearOfAdunPassiveAbilityPresence.option_any_race_lotv,
OPTION_NAME[SpearOfAdunPassivesPresentInNoBuild]: SpearOfAdunPassivesPresentInNoBuild.option_false,
OPTION_NAME[MissionOrderScouting]: MissionOrderScouting.option_all,
OPTION_NAME[GrantStoryTech]: GrantStoryTech.option_grant,
OPTION_NAME[GrantStoryLevels]: GrantStoryLevels.option_minimum,
OPTION_NAME[MineralsPerItem]: 25,
OPTION_NAME[VespenePerItem]: 25,
OPTION_NAME[StartingSupplyPerItem]: 5,
OPTION_NAME[MaximumSupplyPerItem]: 2,
OPTION_NAME[ResearchCostReductionPerItem]: 5,
}
raceswap_blitz_settings = {
# medium-sized Blitz (10 required, 50 total), all races, forced raceswaps
OPTION_NAME[Accessibility]: Accessibility.option_full,
OPTION_NAME[ProgressionBalancing]: ProgressionBalancing.default,
OPTION_NAME[GameDifficulty]: GameDifficulty.option_hard,
OPTION_NAME[SelectedRaces]: SelectedRaces.valid_keys,
OPTION_NAME[MissionOrder]: MissionOrder.option_blitz,
OPTION_NAME[RequiredTactics]: RequiredTactics.option_advanced,
OPTION_NAME[EnabledCampaigns]: EnabledCampaigns.valid_keys,
OPTION_NAME[EnableRaceSwapVariants]: EnableRaceSwapVariants.option_pick_one_non_vanilla,
OPTION_NAME[EnableMissionRaceBalancing]: EnableMissionRaceBalancing.option_semi_balanced,
OPTION_NAME[KeyMode]: KeyMode.option_disabled,
OPTION_NAME[MaximumCampaignSize]: 50,
OPTION_NAME[StarterUnit]: StarterUnit.option_off,
OPTION_NAME[EnableMorphling]: EnableMorphling.option_true,
OPTION_NAME[TakeOverAIAllies]: TakeOverAIAllies.option_false,
OPTION_NAME[DifficultyCurve]: DifficultyCurve.option_standard,
OPTION_NAME[VanillaLocations]: VanillaLocations.option_enabled,
OPTION_NAME[ExtraLocations]: ExtraLocations.option_enabled,
OPTION_NAME[ChallengeLocations]: ChallengeLocations.option_enabled,
OPTION_NAME[MasteryLocations]: MasteryLocations.option_disabled,
OPTION_NAME[KerriganPrimalStatus]: KerriganPrimalStatus.option_item,
OPTION_NAME[WarCouncilNerfs]: WarCouncilNerfs.option_true,
OPTION_NAME[SpearOfAdunPresence]: SpearOfAdunPresence.option_protoss,
OPTION_NAME[SpearOfAdunPresentInNoBuild]: SpearOfAdunPresentInNoBuild.option_false,
OPTION_NAME[SpearOfAdunPassiveAbilityPresence]: SpearOfAdunPassiveAbilityPresence.option_protoss,
OPTION_NAME[SpearOfAdunPassivesPresentInNoBuild]: SpearOfAdunPassivesPresentInNoBuild.option_false,
OPTION_NAME[ExcludeOverpoweredItems]: ExcludeOverpoweredItems.option_true,
OPTION_NAME[MissionOrderScouting]: MissionOrderScouting.option_completed,
OPTION_NAME[MineralsPerItem]: 10,
OPTION_NAME[VespenePerItem]: 10,
OPTION_NAME[StartingSupplyPerItem]: 2,
OPTION_NAME[MaximumSupplyPerItem]: 1,
OPTION_NAME[ResearchCostReductionPerItem]: 2,
}
bread_and_butter_settings = {
# 50 mission Golden Path, all races, limits on Upgrades per Unit/Kerrigan/Nova/SoA
OPTION_NAME[Accessibility]: Accessibility.option_full,
OPTION_NAME[ProgressionBalancing]: ProgressionBalancing.default,
OPTION_NAME[GameDifficulty]: GameDifficulty.option_normal,
OPTION_NAME[SelectedRaces]: SelectedRaces.valid_keys,
OPTION_NAME[MissionOrder]: MissionOrder.option_golden_path,
OPTION_NAME[RequiredTactics]: RequiredTactics.option_standard,
OPTION_NAME[EnabledCampaigns]: EnabledCampaigns.valid_keys,
OPTION_NAME[EnableRaceSwapVariants]: EnableRaceSwapVariants.option_pick_one,
OPTION_NAME[EnableMissionRaceBalancing]: EnableMissionRaceBalancing.option_semi_balanced,
OPTION_NAME[KeyMode]: KeyMode.option_progressive_questlines,
OPTION_NAME[MaximumCampaignSize]: 50,
OPTION_NAME[StarterUnit]: StarterUnit.option_off,
OPTION_NAME[EnableMorphling]: EnableMorphling.option_true,
OPTION_NAME[TakeOverAIAllies]: TakeOverAIAllies.option_false,
OPTION_NAME[DifficultyCurve]: DifficultyCurve.option_standard,
OPTION_NAME[VanillaLocations]: VanillaLocations.option_enabled,
OPTION_NAME[ExtraLocations]: ExtraLocations.option_enabled,
OPTION_NAME[ChallengeLocations]: ChallengeLocations.option_enabled,
OPTION_NAME[MasteryLocations]: MasteryLocations.option_disabled,
OPTION_NAME[WarCouncilNerfs]: WarCouncilNerfs.option_true,
OPTION_NAME[NovaGhostOfAChanceVariant]: NovaGhostOfAChanceVariant.option_nco,
OPTION_NAME[GenericUpgradeItems]: GenericUpgradeItems.option_individual_items,
OPTION_NAME[MinNumberOfUpgrades]: 1,
OPTION_NAME[MaxNumberOfUpgrades]: 4,
OPTION_NAME[NovaMaxWeapons]: 2,
OPTION_NAME[NovaMaxGadgets]: 2,
OPTION_NAME[KerriganPrimalStatus]: KerriganPrimalStatus.option_item,
OPTION_NAME[KerriganMaxActiveAbilities]: 4,
OPTION_NAME[KerriganMaxPassiveAbilities]: 2,
OPTION_NAME[SpearOfAdunPresence]: SpearOfAdunPresence.option_protoss,
OPTION_NAME[SpearOfAdunPresentInNoBuild]: SpearOfAdunPresentInNoBuild.option_false,
OPTION_NAME[SpearOfAdunPassiveAbilityPresence]: SpearOfAdunPassiveAbilityPresence.option_protoss,
OPTION_NAME[SpearOfAdunPassivesPresentInNoBuild]: SpearOfAdunPassivesPresentInNoBuild.option_false,
OPTION_NAME[SpearOfAdunMaxActiveAbilities]: 3,
OPTION_NAME[SpearOfAdunMaxAutocastAbilities]: 1,
OPTION_NAME[MissionOrderScouting]: MissionOrderScouting.option_completed,
OPTION_NAME[ExcludeOverpoweredItems]: ExcludeOverpoweredItems.option_true,
OPTION_NAME[GrantStoryTech]: GrantStoryTech.option_allow_substitutes,
OPTION_NAME[MineralsPerItem]: 10,
OPTION_NAME[VespenePerItem]: 10,
OPTION_NAME[StartingSupplyPerItem]: 2,
OPTION_NAME[MaximumSupplyPerItem]: 1,
OPTION_NAME[ResearchCostReductionPerItem]: 2,
OPTION_NAME[FillerPercentage]: 30,
OPTION_NAME[FillerItemsDistribution]: {
item_names.STARTING_MINERALS: 10,
item_names.STARTING_VESPENE: 10,
item_names.STARTING_SUPPLY: 10,
item_names.MAX_SUPPLY: 10,
item_names.SHIELD_REGENERATION: 5,
item_names.BUILDING_CONSTRUCTION_SPEED: 10,
item_names.KERRIGAN_LEVELS_1: 0,
item_names.UPGRADE_RESEARCH_SPEED: 10,
item_names.UPGRADE_RESEARCH_COST: 10,
item_names.REDUCED_MAX_SUPPLY: 0,
}
}
all_protoss_settings = {
# Vanilla campaign, but full Protoss. 62 missions, huge for one race, reduced locations, low values for fillers
OPTION_NAME[Accessibility]: Accessibility.option_full,
OPTION_NAME[ProgressionBalancing]: ProgressionBalancing.default,
OPTION_NAME[GameDifficulty]: GameDifficulty.option_normal,
OPTION_NAME[SelectedRaces]: {SC2Race.PROTOSS.get_title()},
OPTION_NAME[MissionOrder]: MissionOrder.option_vanilla_shuffled,
OPTION_NAME[RequiredTactics]: RequiredTactics.option_standard,
OPTION_NAME[EnabledCampaigns]: {
SC2Campaign.WOL.campaign_name,
SC2Campaign.PROPHECY.campaign_name,
SC2Campaign.HOTS.campaign_name,
SC2Campaign.PROLOGUE.campaign_name,
SC2Campaign.LOTV.campaign_name,
# SC2Campaign.EPILOGUE.campaign_name, enable once Epilogue gets race-swaps
# SC2Campaign.NCO.campaign_name, enable once NCO gets race-swaps
},
OPTION_NAME[EnableRaceSwapVariants]: EnableRaceSwapVariants.option_shuffle_all,
OPTION_NAME[KeyMode]: KeyMode.option_progressive_questlines,
OPTION_NAME[MaximumCampaignSize]: 61,
OPTION_NAME[StarterUnit]: StarterUnit.option_off,
OPTION_NAME[GrantStoryTech]: GrantStoryTech.option_grant,
OPTION_NAME[GrantStoryLevels]: GrantStoryLevels.option_minimum,
OPTION_NAME[TakeOverAIAllies]: TakeOverAIAllies.option_false,
OPTION_NAME[DifficultyCurve]: DifficultyCurve.option_standard,
OPTION_NAME[VanillaLocations]: VanillaLocations.option_enabled,
OPTION_NAME[ExtraLocations]: ExtraLocations.option_enabled,
OPTION_NAME[ChallengeLocations]: ChallengeLocations.option_disabled,
OPTION_NAME[MasteryLocations]: MasteryLocations.option_disabled,
OPTION_NAME[WarCouncilNerfs]: WarCouncilNerfs.option_true,
OPTION_NAME[GenericUpgradeItems]: GenericUpgradeItems.option_individual_items,
OPTION_NAME[MinNumberOfUpgrades]: 2,
OPTION_NAME[MaxNumberOfUpgrades]: 4,
OPTION_NAME[SpearOfAdunPresence]: SpearOfAdunPresence.option_protoss,
OPTION_NAME[SpearOfAdunPresentInNoBuild]: SpearOfAdunPresentInNoBuild.option_false,
OPTION_NAME[SpearOfAdunPassiveAbilityPresence]: SpearOfAdunPassiveAbilityPresence.option_protoss,
OPTION_NAME[SpearOfAdunPassivesPresentInNoBuild]: SpearOfAdunPassivesPresentInNoBuild.option_false,
OPTION_NAME[SpearOfAdunMaxActiveAbilities]: 3,
OPTION_NAME[SpearOfAdunMaxAutocastAbilities]: 1,
OPTION_NAME[MissionOrderScouting]: MissionOrderScouting.option_completed,
OPTION_NAME[MineralsPerItem]: 5,
OPTION_NAME[VespenePerItem]: 5,
OPTION_NAME[StartingSupplyPerItem]: 1,
OPTION_NAME[MaximumSupplyPerItem]: 1,
OPTION_NAME[ResearchCostReductionPerItem]: 1,
OPTION_NAME[FillerPercentage]: 30,
OPTION_NAME[FillerItemsDistribution]: {
item_names.STARTING_MINERALS: 1,
item_names.STARTING_VESPENE: 1,
item_names.STARTING_SUPPLY: 1,
item_names.MAX_SUPPLY: 1,
item_names.SHIELD_REGENERATION: 1,
item_names.BUILDING_CONSTRUCTION_SPEED: 1,
item_names.KERRIGAN_LEVELS_1: 0,
item_names.UPGRADE_RESEARCH_SPEED: 1,
item_names.UPGRADE_RESEARCH_COST: 1,
item_names.REDUCED_MAX_SUPPLY: 0,
}
}
evil_logic_settings = {
# 6x6 grid on any_unit. High difficulty, disabled Kerrigan, harsh limits, trap items. any_unit has a chance to be unbeatable
OPTION_NAME[Accessibility]: Accessibility.option_full,
OPTION_NAME[ProgressionBalancing]: ProgressionBalancing.default,
OPTION_NAME[GameDifficulty]: GameDifficulty.option_brutal,
OPTION_NAME[SelectedRaces]: SelectedRaces.valid_keys,
OPTION_NAME[MissionOrder]: MissionOrder.option_grid,
OPTION_NAME[RequiredTactics]: RequiredTactics.option_any_units,
OPTION_NAME[EnabledCampaigns]: EnabledCampaigns.valid_keys,
OPTION_NAME[EnableRaceSwapVariants]: EnableRaceSwapVariants.option_pick_one,
OPTION_NAME[EnableMissionRaceBalancing]: EnableMissionRaceBalancing.option_semi_balanced,
OPTION_NAME[KeyMode]: KeyMode.option_progressive_questlines,
OPTION_NAME[MaximumCampaignSize]: 35,
OPTION_NAME[TwoStartPositions]: TwoStartPositions.option_true,
OPTION_NAME[StarterUnit]: StarterUnit.option_off,
OPTION_NAME[EnableMorphling]: EnableMorphling.option_true,
OPTION_NAME[TakeOverAIAllies]: TakeOverAIAllies.option_false,
OPTION_NAME[DifficultyCurve]: DifficultyCurve.option_standard,
OPTION_NAME[VanillaLocations]: VanillaLocations.option_enabled,
OPTION_NAME[ExtraLocations]: ExtraLocations.option_enabled,
OPTION_NAME[ChallengeLocations]: ChallengeLocations.option_enabled,
OPTION_NAME[MasteryLocations]: MasteryLocations.option_enabled,
OPTION_NAME[WarCouncilNerfs]: WarCouncilNerfs.option_true,
OPTION_NAME[NovaGhostOfAChanceVariant]: NovaGhostOfAChanceVariant.option_nco,
OPTION_NAME[GenericUpgradeItems]: GenericUpgradeItems.option_individual_items,
OPTION_NAME[MinNumberOfUpgrades]: 1,
OPTION_NAME[MaxNumberOfUpgrades]: 2,
OPTION_NAME[NovaMaxWeapons]: 1,
OPTION_NAME[NovaMaxGadgets]: 1,
OPTION_NAME[KerriganPresence]: KerriganPresence.option_not_present,
OPTION_NAME[SpearOfAdunPresence]: SpearOfAdunPresence.option_any_race_lotv,
OPTION_NAME[SpearOfAdunPresentInNoBuild]: SpearOfAdunPresentInNoBuild.option_false,
OPTION_NAME[SpearOfAdunPassiveAbilityPresence]: SpearOfAdunPassiveAbilityPresence.option_any_race_lotv,
OPTION_NAME[SpearOfAdunPassivesPresentInNoBuild]: SpearOfAdunPassivesPresentInNoBuild.option_false,
OPTION_NAME[SpearOfAdunMaxActiveAbilities]: 2,
OPTION_NAME[SpearOfAdunMaxAutocastAbilities]: 1,
OPTION_NAME[MissionOrderScouting]: MissionOrderScouting.option_completed,
OPTION_NAME[ExcludeOverpoweredItems]: ExcludeOverpoweredItems.option_true,
OPTION_NAME[GrantStoryTech]: GrantStoryTech.option_allow_substitutes,
OPTION_NAME[MineralsPerItem]: 10,
OPTION_NAME[VespenePerItem]: 10,
OPTION_NAME[StartingSupplyPerItem]: 2,
OPTION_NAME[MaximumSupplyPerItem]: 1,
OPTION_NAME[ResearchCostReductionPerItem]: 2,
OPTION_NAME[MaximumSupplyReductionPerItem]: 10,
OPTION_NAME[FillerPercentage]: 20,
OPTION_NAME[FillerItemsDistribution]: {
item_names.STARTING_MINERALS: 10,
item_names.STARTING_VESPENE: 10,
item_names.STARTING_SUPPLY: 10,
item_names.MAX_SUPPLY: 0,
item_names.SHIELD_REGENERATION: 5,
item_names.BUILDING_CONSTRUCTION_SPEED: 10,
item_names.KERRIGAN_LEVELS_1: 0,
item_names.UPGRADE_RESEARCH_SPEED: 10,
item_names.UPGRADE_RESEARCH_COST: 10,
item_names.REDUCED_MAX_SUPPLY: 2,
}
}
full_campaign_settings = {
# mandatory full campaign, not recommended, but will be expected. 195 mission grid, all races. Reduced locations and filler values
OPTION_NAME[Accessibility]: Accessibility.option_full,
OPTION_NAME[ProgressionBalancing]: ProgressionBalancing.default,
OPTION_NAME[GameDifficulty]: GameDifficulty.option_normal,
OPTION_NAME[SelectedRaces]: SelectedRaces.valid_keys,
OPTION_NAME[MissionOrder]: MissionOrder.option_grid,
OPTION_NAME[RequiredTactics]: RequiredTactics.option_standard,
OPTION_NAME[EnabledCampaigns]: EnabledCampaigns.valid_keys,
OPTION_NAME[EnableRaceSwapVariants]: EnableRaceSwapVariants.option_shuffle_all,
OPTION_NAME[KeyMode]: KeyMode.option_progressive_missions,
OPTION_NAME[MaximumCampaignSize]: MaximumCampaignSize.range_end,
OPTION_NAME[TwoStartPositions]: TwoStartPositions.option_true,
OPTION_NAME[StarterUnit]: StarterUnit.option_off,
OPTION_NAME[EnableMorphling]: EnableMorphling.option_true,
OPTION_NAME[NovaGhostOfAChanceVariant]: NovaGhostOfAChanceVariant.option_nco,
OPTION_NAME[GrantStoryTech]: GrantStoryTech.option_allow_substitutes,
OPTION_NAME[TakeOverAIAllies]: TakeOverAIAllies.option_false,
OPTION_NAME[DifficultyCurve]: DifficultyCurve.option_standard,
OPTION_NAME[VanillaLocations]: VanillaLocations.option_enabled,
OPTION_NAME[ExtraLocations]: ExtraLocations.option_half_chance,
OPTION_NAME[ChallengeLocations]: ChallengeLocations.option_disabled,
OPTION_NAME[MasteryLocations]: MasteryLocations.option_disabled,
OPTION_NAME[KerriganPrimalStatus]: KerriganPrimalStatus.option_item,
OPTION_NAME[WarCouncilNerfs]: WarCouncilNerfs.option_true,
OPTION_NAME[MaxUpgradeLevel]: 5,
OPTION_NAME[SpearOfAdunPresence]: SpearOfAdunPresence.option_everywhere,
OPTION_NAME[SpearOfAdunPresentInNoBuild]: SpearOfAdunPresentInNoBuild.option_false,
OPTION_NAME[SpearOfAdunPassiveAbilityPresence]: SpearOfAdunPassiveAbilityPresence.option_everywhere,
OPTION_NAME[SpearOfAdunPassivesPresentInNoBuild]: SpearOfAdunPassivesPresentInNoBuild.option_false,
OPTION_NAME[MissionOrderScouting]: MissionOrderScouting.option_completed,
OPTION_NAME[MineralsPerItem]: 5,
OPTION_NAME[VespenePerItem]: 5,
OPTION_NAME[StartingSupplyPerItem]: 1,
OPTION_NAME[MaximumSupplyPerItem]: 1,
OPTION_NAME[ResearchCostReductionPerItem]: 1,
OPTION_NAME[FillerItemsDistribution]: {
item_names.STARTING_MINERALS: 10,
item_names.STARTING_VESPENE: 10,
item_names.STARTING_SUPPLY: 10,
item_names.MAX_SUPPLY: 10,
item_names.SHIELD_REGENERATION: 5,
item_names.BUILDING_CONSTRUCTION_SPEED: 10,
item_names.KERRIGAN_LEVELS_1: 5,
item_names.UPGRADE_RESEARCH_SPEED: 10,
item_names.UPGRADE_RESEARCH_COST: 10,
item_names.REDUCED_MAX_SUPPLY: 0,
}
}
sc2_options_presets: Dict[str, Dict[str, Any]] = {
"F2P Terran [~7 hours]": template_settings,
"F2P Big [~10 hours]": f2p_big_settings,
"Zerg Rush [~3 hours]": zerg_rush_settings,
"Classic Grid [~4 hours]": classic_grid_settings,
"Raceswap Blitz [~8 hours]": raceswap_blitz_settings,
"Bread and Butter [~12 hours]": bread_and_butter_settings,
"Pure Protoss [~15 hours]": all_protoss_settings,
"Evil Logic [~6 hours]": evil_logic_settings,
"Giant Grid Game [30+ hours]": full_campaign_settings,
}

View File

@@ -6,8 +6,7 @@ from datetime import timedelta
from Options import (
Choice, Toggle, DefaultOnToggle, OptionSet, Range,
PerGameCommonOptions, Option, VerifyKeys, StartInventory,
is_iterable_except_str, OptionGroup, Visibility, ItemDict,
Accessibility, ProgressionBalancing
is_iterable_except_str, OptionGroup, Visibility, ItemDict
)
from Utils import get_fuzzy_results
from BaseClasses import PlandoOptions
@@ -1757,6 +1756,3 @@ void_trade_age_limits_ms: Dict[int, int] = {
VoidTradeAgeLimit.option_1_day: 1000 * int(timedelta(days = 1).total_seconds()),
VoidTradeAgeLimit.option_1_week: 1000 * int(timedelta(weeks = 1).total_seconds()),
}
# Store the names of all options
OPTION_NAME = {option_type: name for name, option_type in Starcraft2Options.type_hints.items()}

View File

@@ -101,7 +101,7 @@ def has_x_belt_multiplier(state: CollectionState, player: int, needed: float) ->
def has_logic_list_building(state: CollectionState, player: int, buildings: list[str], index: int,
includeuseful: bool, floating: bool) -> bool:
includeuseful: bool) -> bool:
# Includes balancer, tunnel, and trash in logic in order to make them appear in earlier spheres
if includeuseful and not (state.has(ITEMS.trash, player) and has_balancer(state, player) and
@@ -109,7 +109,7 @@ def has_logic_list_building(state: CollectionState, player: int, buildings: list
return False
if buildings[index] == ITEMS.cutter:
if buildings.index(ITEMS.stacker) < index and not floating:
if buildings.index(ITEMS.stacker) < index:
return state.has_any((ITEMS.cutter, ITEMS.cutter_quad), player)
else:
return can_cut_half(state, player)
@@ -195,38 +195,38 @@ def create_shapez_regions(player: int, multiworld: MultiWorld, floating: bool,
# Progressively connect level and upgrade regions
regions[REGIONS.main].connect(
regions[REGIONS.levels_1], "Using first level building",
lambda state: has_logic_list_building(state, player, level_logic_buildings, 0, False, floating))
lambda state: has_logic_list_building(state, player, level_logic_buildings, 0, False))
regions[REGIONS.levels_1].connect(
regions[REGIONS.levels_2], "Using second level building",
lambda state: has_logic_list_building(state, player, level_logic_buildings, 1, False, floating))
lambda state: has_logic_list_building(state, player, level_logic_buildings, 1, False))
regions[REGIONS.levels_2].connect(
regions[REGIONS.levels_3], "Using third level building",
lambda state: has_logic_list_building(state, player, level_logic_buildings, 2,
early_useful == OPTIONS.buildings_3, floating))
early_useful == OPTIONS.buildings_3))
regions[REGIONS.levels_3].connect(
regions[REGIONS.levels_4], "Using fourth level building",
lambda state: has_logic_list_building(state, player, level_logic_buildings, 3, False, floating))
lambda state: has_logic_list_building(state, player, level_logic_buildings, 3, False))
regions[REGIONS.levels_4].connect(
regions[REGIONS.levels_5], "Using fifth level building",
lambda state: has_logic_list_building(state, player, level_logic_buildings, 4,
early_useful == OPTIONS.buildings_5, floating))
early_useful == OPTIONS.buildings_5))
regions[REGIONS.main].connect(
regions[REGIONS.upgrades_1], "Using first upgrade building",
lambda state: has_logic_list_building(state, player, upgrade_logic_buildings, 0, False, floating))
lambda state: has_logic_list_building(state, player, upgrade_logic_buildings, 0, False))
regions[REGIONS.upgrades_1].connect(
regions[REGIONS.upgrades_2], "Using second upgrade building",
lambda state: has_logic_list_building(state, player, upgrade_logic_buildings, 1, False, floating))
lambda state: has_logic_list_building(state, player, upgrade_logic_buildings, 1, False))
regions[REGIONS.upgrades_2].connect(
regions[REGIONS.upgrades_3], "Using third upgrade building",
lambda state: has_logic_list_building(state, player, upgrade_logic_buildings, 2,
early_useful == OPTIONS.buildings_3, floating))
early_useful == OPTIONS.buildings_3))
regions[REGIONS.upgrades_3].connect(
regions[REGIONS.upgrades_4], "Using fourth upgrade building",
lambda state: has_logic_list_building(state, player, upgrade_logic_buildings, 3, False, floating))
lambda state: has_logic_list_building(state, player, upgrade_logic_buildings, 3, False))
regions[REGIONS.upgrades_4].connect(
regions[REGIONS.upgrades_5], "Using fifth upgrade building",
lambda state: has_logic_list_building(state, player, upgrade_logic_buildings, 4,
early_useful == OPTIONS.buildings_5, floating))
early_useful == OPTIONS.buildings_5))
# Connect Uncolored shapesanity regions to Main
regions[REGIONS.main].connect(

View File

@@ -13,8 +13,8 @@
## Installation
1. Read the [Randomizer readme](https://github.com/BrandenEK/AShortHike.Randomizer) to see all required dependencies
1. Read the [Mod Installer readme](https://github.com/BrandenEK/AShortHike.Modding.Installer) to see how to download the required mods
Open the [Randomizer Repository](https://github.com/BrandenEK/AShortHike.Randomizer) and follow
the installation instructions listed there.
## Connecting

View File

@@ -1,28 +0,0 @@
Modified MIT License
Copyright (c) 2025 PoryGone
Copyright (c) 2025 lx5
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, and/or distribute copies of the Software,
and to permit persons to whom the Software is furnished to do so, subject to
the following conditions:
No copy or substantial portion of the Software shall be sublicensed or relicensed
without the express written permission of the copyright holder(s)
No copy or substantial portion of the Software shall be sold without the express
written permission of the copyright holder(s)
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,6 +0,0 @@
{
"game": "Super Mario World",
"authors": [ "PoryGone", "lx5" ],
"minimum_ap_version": "0.6.3",
"world_version": "2.1.1"
}

View File

@@ -424,6 +424,7 @@ class Item:
]
for item in itemPool:
item.Progression = True
item.World = world
return itemPool
@@ -438,6 +439,7 @@ class Item:
]
for item in itemPool:
item.Progression = True
item.World = world
return itemPool

View File

@@ -145,12 +145,8 @@ class GanonsTower(Z3Region):
def CanFill(self, item: Item):
if (self.Config.Multiworld):
# changed for AP becuase upstream only uses CanFill for filling progression-related items
# note that item.Progression does not include all items with progression classification
# item.World will be None for item created by create_item for item links
if (item.World is not None and item.World != self.world and (item.Progression or item.IsDungeonItem() or item.IsKeycard() or item.IsSmMap())):
return False
if (item.World is not None and item.World == self.world and item.Progression):
if (item.World is not None and (item.World != self.world or item.Progression)):
return False
if (self.Config.Keysanity and not ((item.Type == ItemType.BigKeyGT or item.Type == ItemType.KeyGT) and item.World == self.world) and (item.IsKey() or item.IsBigKey() or item.IsKeycard())):
return False

View File

@@ -260,19 +260,13 @@ class SMZ3World(World):
l.always_allow = lambda state, item, loc=loc: \
item.game == "SMZ3" and \
loc.alwaysAllow(item.item, state.smz3state[self.player])
l.item_rule = lambda item, loc=loc, region=region, old_rule=l.item_rule: (\
old_rule = l.item_rule
l.item_rule = lambda item, loc=loc, region=region: (\
item.game != "SMZ3" or \
loc.allow(item.item, None) and \
region.CanFill(item.item)) and old_rule(item)
set_rule(l, lambda state, loc=loc: loc.Available(state.smz3state[self.player]))
# In multiworlds, GT is disallowed from having progression items.
# This item rule replicates this behavior for non-SMZ3 games
for loc in self.smz3World.GetRegion("Ganon's Tower").Locations:
l = self.locations[loc.Name]
l.item_rule = lambda item, old_rule=l.item_rule: \
(item.game == "SMZ3" or not item.advancement) and old_rule(item)
def create_regions(self):
self.create_locations(self.player)
startRegion = self.create_region(self.multiworld, self.player, 'Menu')
@@ -595,18 +589,29 @@ class SMZ3World(World):
]))
def JunkFillGT(self, factor):
junkPoolIdx = [idx for idx, i in enumerate(self.multiworld.itempool) if i.excludable]
self.random.shuffle(junkPoolIdx)
junkLocations = [loc for loc in self.locations.values() if loc.name in self.locationNamesGT and loc.item is None]
self.random.shuffle(junkLocations)
poolLength = len(self.multiworld.itempool)
junkPoolIdx = [i for i in range(0, poolLength)
if self.multiworld.itempool[i].classification in (ItemClassification.filler, ItemClassification.trap)]
toRemove = []
for loc in junkLocations:
# Note: Upstream GT junk fill uses FastFill, which ignores item rules
if len(junkPoolIdx) == 0 or len(toRemove) >= int(len(junkLocations) * factor * self.smz3World.TowerCrystals / 7):
break
itemFromPool = self.multiworld.itempool[junkPoolIdx[0]]
toRemove.append(junkPoolIdx.pop(0))
loc.place_locked_item(itemFromPool)
for loc in self.locations.values():
# commenting this for now since doing a partial GT pre fill would allow for non SMZ3 progression in GT
# which isnt desirable (SMZ3 logic only filters for SMZ3 items). Having progression in GT can only happen in Single Player.
# if len(toRemove) >= int(len(self.locationNamesGT) * factor * self.smz3World.TowerCrystals / 7):
# break
if loc.name in self.locationNamesGT and loc.item is None:
poolLength = len(junkPoolIdx)
# start looking at a random starting index and loop at start if no match found
start = self.multiworld.random.randint(0, poolLength)
itemFromPool = None
for off in range(0, poolLength):
i = (start + off) % poolLength
candidate = self.multiworld.itempool[junkPoolIdx[i]]
if junkPoolIdx[i] not in toRemove and loc.can_fill(self.multiworld.state, candidate, False):
itemFromPool = candidate
toRemove.append(junkPoolIdx[i])
break
assert itemFromPool is not None, "Can't find anymore item(s) to pre fill GT"
self.multiworld.push_item(loc, itemFromPool, False)
toRemove.sort(reverse = True)
for i in toRemove:
self.multiworld.itempool.pop(i)
@@ -617,15 +622,15 @@ class SMZ3World(World):
raise Exception(f"Tried to place item {itemType} at {location.Name}, but there is no such item in the item pool")
else:
location.Item = itemToPlace
itemPoolIdx = next((idx for idx, i in enumerate(self.multiworld.itempool) if i.player == self.player and i.name == itemToPlace.Type.name), None)
if itemPoolIdx is not None:
itemFromPool = self.multiworld.itempool.pop(itemPoolIdx)
itemFromPool = next((i for i in self.multiworld.itempool if i.player == self.player and i.name == itemToPlace.Type.name), None)
if itemFromPool is not None:
self.multiworld.get_location(location.Name, self.player).place_locked_item(itemFromPool)
self.multiworld.itempool.remove(itemFromPool)
else:
itemPoolIdx = next((idx for idx, i in enumerate(self.smz3DungeonItems) if i.player == self.player and i.name == itemToPlace.Type.name), None)
if itemPoolIdx is not None:
itemFromPool = self.smz3DungeonItems.pop(itemPoolIdx)
itemFromPool = next((i for i in self.smz3DungeonItems if i.player == self.player and i.name == itemToPlace.Type.name), None)
if itemFromPool is not None:
self.multiworld.get_location(location.Name, self.player).place_locked_item(itemFromPool)
self.smz3DungeonItems.remove(itemFromPool)
itemPool.remove(itemToPlace)
def FrontFillItemInOwnWorld(self, itemPool, itemType):
@@ -635,10 +640,10 @@ class SMZ3World(World):
raise Exception(f"Tried to front fill {item.Name} in, but no location was available")
location.Item = item
itemPoolIdx = next((idx for idx, i in enumerate(self.multiworld.itempool) if i.player == self.player and i.name == item.Type.name and i.advancement == item.Progression), None)
if itemPoolIdx is not None:
itemFromPool = self.multiworld.itempool.pop(itemPoolIdx)
itemFromPool = next((i for i in self.multiworld.itempool if i.player == self.player and i.name == item.Type.name and i.advancement == item.Progression), None)
if itemFromPool is not None:
self.multiworld.get_location(location.Name, self.player).place_locked_item(itemFromPool)
self.multiworld.itempool.remove(itemFromPool)
itemPool.remove(item)
def InitialFillInOwnWorld(self):

View File

@@ -1,6 +0,0 @@
{
"game": "Stardew Valley",
"authors": ["KaitoKid", "Jouramie", "Witchybun (Mod Support)", "Exempt-Medic (Proofreading)"],
"minimum_ap_version": "0.6.4",
"world_version": "6.0.0"
}

View File

@@ -50,6 +50,7 @@ on the Archipelago website to generate a YAML using a graphical interface.
significantly more difficult with this mod, so it is recommended to choose a lower difficulty than you normally would
play on.
4. Open the world in single player or multiplayer.
5. When you're ready, open chat, and enter `/apstart` to start the game.
## Commands