Compare commits

..

1 Commits

Author SHA1 Message Date
Fabian Dill
a79f71ad8b WebHost: turn Room.timeout from a database column into a flask app config field 2025-12-01 23:52:13 +01:00
31 changed files with 56 additions and 136 deletions

View File

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

View File

@@ -26,6 +26,7 @@ app.jinja_env.filters['get_file_safe_name'] = get_file_safe_name
app.config["SELFHOST"] = True # application process is in charge of running the websites
app.config["GENERATORS"] = 8 # maximum concurrent world gens
app.config["HOSTERS"] = 8 # maximum concurrent room hosters
app.config["ROOM_IDLE_TIMEOUT"] = 2 * 60 * 60 # seconds of idle before a Room spins down
app.config["SELFLAUNCH"] = True # application process is in charge of launching Rooms.
app.config["SELFLAUNCHCERT"] = None # can point to a SSL Certificate to encrypt Room websocket connections
app.config["SELFLAUNCHKEY"] = None # can point to a SSL Certificate Key to encrypt Room websocket connections

View File

@@ -38,6 +38,5 @@ def room_info(room_id: UUID) -> Dict[str, Any]:
"players": get_players(room.seed),
"last_port": room.last_port,
"last_activity": room.last_activity,
"timeout": room.timeout,
"downloads": downloads,
}

View File

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

View File

@@ -16,7 +16,6 @@ def get_rooms():
"creation_time": room.creation_time,
"last_activity": room.last_activity,
"last_port": room.last_port,
"timeout": room.timeout,
"tracker": to_url(room.tracker),
})
return jsonify(response)

View File

@@ -124,16 +124,14 @@ def autohost(config: dict):
hoster = MultiworldInstance(config, x)
hosters.append(hoster)
hoster.start()
activity_timedelta = timedelta(seconds=config["ROOM_IDLE_TIMEOUT"] + 5)
while not stop_event.wait(0.1):
with db_session:
rooms = select(
room for room in Room if
room.last_activity >= datetime.utcnow() - timedelta(days=3))
room.last_activity >= datetime.utcnow() - activity_timedelta)
for room in rooms:
# we have to filter twice, as the per-room timeout can't currently be PonyORM transpiled.
if room.last_activity >= datetime.utcnow() - timedelta(seconds=room.timeout + 5):
hosters[room.id.int % len(hosters)].start_room(room.id)
hosters[room.id.int % len(hosters)].start_room(room.id)
except AlreadyRunningException:
logging.info("Autohost reports as already running, not starting another.")
@@ -187,6 +185,7 @@ class MultiworldInstance():
self.cert = config["SELFLAUNCHCERT"]
self.key = config["SELFLAUNCHKEY"]
self.host = config["HOST_ADDRESS"]
self.room_idle_timer = config["ROOM_IDLE_TIMEOUT"]
self.rooms_to_start = multiprocessing.Queue()
self.rooms_shutting_down = multiprocessing.Queue()
self.name = f"MultiHoster{id}"
@@ -198,7 +197,7 @@ class MultiworldInstance():
process = multiprocessing.Process(group=None, target=run_server_process,
args=(self.name, self.ponyconfig, get_static_server_data(),
self.cert, self.key, self.host,
self.rooms_to_start, self.rooms_shutting_down),
self.rooms_to_start, self.rooms_shutting_down, self.room_idle_timer),
name=self.name)
process.start()
self.process = process

View File

@@ -231,7 +231,8 @@ def set_up_logging(room_id) -> logging.Logger:
def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str],
host: str, rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue):
host: str, rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue,
room_idle_timeout: int):
from setproctitle import setproctitle
setproctitle(name)
@@ -316,7 +317,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
else:
ctx.logger.exception("Could not determine port. Likely hosting failure.")
with db_session:
ctx.auto_shutdown = Room.get(id=room_id).timeout
ctx.auto_shutdown = room_idle_timeout
if ctx.saving:
setattr(asyncio.current_task(), "save", lambda: ctx._save(True))
assert ctx.shutdown_task is None
@@ -347,7 +348,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
# 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)
datetime.timedelta(minutes=1, seconds=room_idle_timeout)
del room
logging.info(f"Shutting down room {room_id} on {name}.")
finally:

View File

@@ -231,7 +231,7 @@ def host_room(room: UUID):
now = datetime.datetime.utcnow()
# indicate that the page should reload to get the assigned port
should_refresh = ((not room.last_port and now - room.creation_time < datetime.timedelta(seconds=3))
or room.last_activity < now - datetime.timedelta(seconds=room.timeout))
or room.last_activity < now - datetime.timedelta(seconds=app.config["ROOM_IDLE_TIMEOUT"]))
if now - room.last_activity > datetime.timedelta(minutes=1):
# we only set last_activity if needed, otherwise parallel access on /room will cause an internal server error

View File

@@ -27,7 +27,6 @@ class Room(db.Entity):
seed = Required('Seed', index=True)
multisave = Optional(buffer, lazy=True)
show_spoiler = Required(int, default=0) # 0 -> never, 1 -> after completion, -> 2 always
timeout = Required(int, default=lambda: 2 * 60 * 60) # seconds since last activity to shutdown
tracker = Optional(UUID, index=True)
# Port special value -1 means the server errored out. Another attempt can be made with a page refresh
last_port = Optional(int, default=lambda: 0)

View File

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

View File

@@ -29,7 +29,7 @@
and a <a href="{{ url_for("get_multiworld_sphere_tracker", tracker=room.tracker) }}">Sphere Tracker</a> enabled.
<br />
{% endif %}
The server for this room will be paused after {{ room.timeout//60//60 }} hours of inactivity.
The server for this room will be paused after {{ config["ROOM_IDLE_TIMEOUT"]//60//60 }} hours of inactivity.
Should you wish to continue later,
anyone can simply refresh this page and the server will resume.<br>
{% if room.last_port == -1 %}

View File

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

11
kvui.py
View File

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

View File

@@ -19,7 +19,6 @@ __all__ = [
"create_room",
"start_room",
"stop_room",
"set_room_timeout",
"get_multidata_for_room",
"set_multidata_for_room",
"stop_autogen",
@@ -139,7 +138,6 @@ def stop_room(app_client: "FlaskClient",
from pony.orm import db_session
from WebHostLib.models import Command, Room
from WebHostLib import app
poll_interval = 2
@@ -152,14 +150,12 @@ def stop_room(app_client: "FlaskClient",
with db_session:
room: Room = Room.get(id=room_uuid)
if simulate_idle:
new_last_activity = datetime.utcnow() - timedelta(seconds=room.timeout + 5)
new_last_activity = datetime.utcnow() - timedelta(seconds=2 * 60 * 60 + 5)
else:
new_last_activity = datetime.utcnow() - timedelta(days=3)
room.last_activity = new_last_activity
address = f"localhost:{room.last_port}" if room.last_port > 0 else None
if address:
original_timeout = room.timeout
room.timeout = 1 # avoid spinning it up again
Command(room=room, commandtext="/exit")
try:
@@ -186,28 +182,13 @@ def stop_room(app_client: "FlaskClient",
room = Room.get(id=room_uuid)
room.last_port = 0 # easier to detect when the host is up this way
if address:
room.timeout = original_timeout
room.last_activity = new_last_activity
print("timeout restored")
def set_room_timeout(room_id: str, timeout: float) -> None:
from pony.orm import db_session
from WebHostLib.models import Room
from WebHostLib import app
room_uuid = to_python(room_id)
with db_session:
room: Room = Room.get(id=room_uuid)
room.timeout = timeout
def get_multidata_for_room(webhost_client: "FlaskClient", room_id: str) -> bytes:
from pony.orm import db_session
from WebHostLib.models import Room
from WebHostLib import app
room_uuid = to_python(room_id)
with db_session:
@@ -219,7 +200,6 @@ def set_multidata_for_room(webhost_client: "FlaskClient", room_id: str, data: by
from pony.orm import db_session
from WebHostLib.models import Room
from WebHostLib import app
room_uuid = to_python(room_id)
with db_session:
@@ -258,9 +238,11 @@ def _stop_webhost_mp(name_filter: str, graceful: bool = True) -> None:
proc.kill()
proc.join()
def stop_autogen(graceful: bool = True) -> None:
# FIXME: this name filter is jank, but there seems to be no way to add a custom prefix for a Pool
_stop_webhost_mp("SpawnPoolWorker-", graceful)
def stop_autohost(graceful: bool = True) -> None:
_stop_webhost_mp("MultiHoster", graceful)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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