Merge remote-tracking branch 'remotes/origin/main' into Satisfactory_ToBeVerified

# Conflicts:
#	README.md
This commit is contained in:
Jarno
2025-07-24 11:31:17 +02:00
66 changed files with 2311 additions and 362 deletions

210
.dockerignore Normal file
View File

@@ -0,0 +1,210 @@
.git
.github
.run
docs
test
typings
*Client.py
.idea
.vscode
*_Spoiler.txt
*.bmbp
*.apbp
*.apl2ac
*.apm3
*.apmc
*.apz5
*.aptloz
*.apemerald
*.pyc
*.pyd
*.sfc
*.z64
*.n64
*.nes
*.smc
*.sms
*.gb
*.gbc
*.gba
*.wixobj
*.lck
*.db3
*multidata
*multisave
*.archipelago
*.apsave
*.BIN
*.puml
setups
build
bundle/components.wxs
dist
/prof/
README.html
.vs/
EnemizerCLI/
/Players/
/SNI/
/sni-*/
/appimagetool*
/host.yaml
/options.yaml
/config.yaml
/logs/
_persistent_storage.yaml
mystery_result_*.yaml
*-errors.txt
success.txt
output/
Output Logs/
/factorio/
/Minecraft Forge Server/
/WebHostLib/static/generated
/freeze_requirements.txt
/Archipelago.zip
/setup.ini
/installdelete.iss
/data/user.kv
/datapackage
/custom_worlds
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
*.dll
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
installer.log
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# vim editor
*.swp
# SageMath parsed files
*.sage.py
# Environments
.env
.venv*
env/
venv/
/venv*/
ENV/
env.bak/
venv.bak/
*.code-workspace
shell.nix
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# Cython intermediates
_speedups.c
_speedups.cpp
_speedups.html
# minecraft server stuff
jdk*/
minecraft*/
minecraft_versions.json
!worlds/minecraft/
# pyenv
.python-version
#undertale stuff
/Undertale/
# OS General Files
.DS_Store
.AppleDouble
.LSOverride
Thumbs.db
[Dd]esktop.ini

View File

@@ -8,18 +8,24 @@ on:
paths:
- '**'
- '!docs/**'
- '!deploy/**'
- '!setup.py'
- '!Dockerfile'
- '!*.iss'
- '!.gitignore'
- '!.dockerignore'
- '!.github/workflows/**'
- '.github/workflows/unittests.yml'
pull_request:
paths:
- '**'
- '!docs/**'
- '!deploy/**'
- '!setup.py'
- '!Dockerfile'
- '!*.iss'
- '!.gitignore'
- '!.dockerignore'
- '!.github/workflows/**'
- '.github/workflows/unittests.yml'

View File

@@ -5,6 +5,7 @@ import functools
import logging
import random
import secrets
import warnings
from argparse import Namespace
from collections import Counter, deque
from collections.abc import Collection, MutableSequence
@@ -438,12 +439,27 @@ class MultiWorld():
def get_location(self, location_name: str, player: int) -> Location:
return self.regions.location_cache[player][location_name]
def get_all_state(self, use_cache: bool, allow_partial_entrances: bool = False,
def get_all_state(self, use_cache: bool | None = None, allow_partial_entrances: bool = False,
collect_pre_fill_items: bool = True, perform_sweep: bool = True) -> CollectionState:
cached = getattr(self, "_all_state", None)
if use_cache and cached:
return cached.copy()
"""
Creates a new CollectionState, and collects all precollected items, all items in the multiworld itempool, those
specified in each worlds' `get_pre_fill_items()`, and then sweeps the multiworld collecting any other items
it is able to reach, building as complete of a completed game state as possible.
:param use_cache: Deprecated and unused.
:param allow_partial_entrances: Whether the CollectionState should allow for disconnected entrances while
sweeping, such as before entrance randomization is complete.
:param collect_pre_fill_items: Whether the items in each worlds' `get_pre_fill_items()` should be added to this
state.
:param perform_sweep: Whether this state should perform a sweep for reachable locations, collecting any placed
items it can.
:return: The completed CollectionState.
"""
if __debug__ and use_cache is not None:
# TODO swap to Utils.deprecate when we want this to crash on source and warn on frozen
warnings.warn("multiworld.get_all_state no longer caches all_state and this argument will be removed.",
DeprecationWarning)
ret = CollectionState(self, allow_partial_entrances)
for item in self.itempool:
@@ -456,8 +472,6 @@ class MultiWorld():
if perform_sweep:
ret.sweep_for_advancements()
if use_cache:
self._all_state = ret
return ret
def get_items(self) -> List[Item]:
@@ -706,6 +720,12 @@ class MultiWorld():
sphere.append(locations.pop(n))
if not sphere:
if __debug__:
from Fill import FillError
raise FillError(
f"Could not access required locations for accessibility check. Missing: {locations}",
multiworld=self,
)
# ran out of places and did not finish yet, quit
logging.warning(f"Could not access required locations for accessibility check."
f" Missing: {locations}")
@@ -1150,13 +1170,13 @@ class Region:
self.region_manager = region_manager
def __getitem__(self, index: int) -> Location:
return self._list.__getitem__(index)
return self._list[index]
def __setitem__(self, index: int, value: Location) -> None:
raise NotImplementedError()
def __len__(self) -> int:
return self._list.__len__()
return len(self._list)
def __iter__(self):
return iter(self._list)
@@ -1170,8 +1190,8 @@ class Region:
class LocationRegister(Register):
def __delitem__(self, index: int) -> None:
location: Location = self._list.__getitem__(index)
self._list.__delitem__(index)
location: Location = self._list[index]
del self._list[index]
del(self.region_manager.location_cache[location.player][location.name])
def insert(self, index: int, value: Location) -> None:
@@ -1182,8 +1202,8 @@ class Region:
class EntranceRegister(Register):
def __delitem__(self, index: int) -> None:
entrance: Entrance = self._list.__getitem__(index)
self._list.__delitem__(index)
entrance: Entrance = self._list[index]
del self._list[index]
del(self.region_manager.entrance_cache[entrance.player][entrance.name])
def insert(self, index: int, value: Entrance) -> None:
@@ -1430,27 +1450,43 @@ class Location:
class ItemClassification(IntFlag):
filler = 0b0000
filler = 0b00000
""" aka trash, as in filler items like ammo, currency etc """
progression = 0b0001
progression = 0b00001
""" Item that is logically relevant.
Protects this item from being placed on excluded or unreachable locations. """
useful = 0b0010
useful = 0b00010
""" Item that is especially useful.
Protects this item from being placed on excluded or unreachable locations.
When combined with another flag like "progression", it means "an especially useful progression item". """
trap = 0b0100
trap = 0b00100
""" Item that is detrimental in some way. """
skip_balancing = 0b1000
skip_balancing = 0b01000
""" should technically never occur on its own
Item that is logically relevant, but progression balancing should not touch.
Typically currency or other counted items. """
Possible reasons for why an item should not be pulled ahead by progression balancing:
1. This item is quite insignificant, so pulling it earlier doesn't help (currency/etc.)
2. It is important for the player experience that this item is evenly distributed in the seed (e.g. goal items) """
progression_skip_balancing = 0b1001 # only progression gets balanced
deprioritized = 0b10000
""" Should technically never occur on its own.
Will not be considered for priority locations,
unless Priority Locations Fill runs out of regular progression items before filling all priority locations.
Should be used for items that would feel bad for the player to find on a priority location.
Usually, these are items that are plentiful or insignificant. """
progression_deprioritized_skip_balancing = 0b11001
""" Since a common case of both skip_balancing and deprioritized is "insignificant progression",
these items often want both flags. """
progression_skip_balancing = 0b01001 # only progression gets balanced
progression_deprioritized = 0b10001 # only progression can be placed during priority fill
def as_flag(self) -> int:
"""As Network API flag int."""
@@ -1498,6 +1534,10 @@ class Item:
def trap(self) -> bool:
return ItemClassification.trap in self.classification
@property
def deprioritized(self) -> bool:
return ItemClassification.deprioritized in self.classification
@property
def filler(self) -> bool:
return not (self.advancement or self.useful or self.trap)

View File

@@ -201,6 +201,7 @@ class CommonContext:
# noinspection PyTypeChecker
def __getitem__(self, key: str) -> typing.Mapping[int, str]:
assert isinstance(key, str), f"ctx.{self.lookup_type}_names used with an id, use the lookup_in_ helpers instead"
return self._game_store[key]
def __len__(self) -> int:
@@ -210,7 +211,7 @@ class CommonContext:
return iter(self._game_store)
def __repr__(self) -> str:
return self._game_store.__repr__()
return repr(self._game_store)
def lookup_in_game(self, code: int, game_name: typing.Optional[str] = None) -> str:
"""Returns the name for an item/location id in the context of a specific game or own game if `game` is

98
Dockerfile Normal file
View File

@@ -0,0 +1,98 @@
# hadolint global ignore=SC1090,SC1091
# Source
FROM scratch AS release
WORKDIR /release
ADD https://github.com/Ijwu/Enemizer/releases/latest/download/ubuntu.16.04-x64.zip Enemizer.zip
# Enemizer
FROM alpine:3.21 AS enemizer
ARG TARGETARCH
WORKDIR /release
COPY --from=release /release/Enemizer.zip .
# No release for arm architecture. Skip.
RUN if [ "$TARGETARCH" = "amd64" ]; then \
apk add unzip=6.0-r15 --no-cache && \
unzip -u Enemizer.zip -d EnemizerCLI && \
chmod -R 777 EnemizerCLI; \
else touch EnemizerCLI; fi
# Cython builder stage
FROM python:3.12 AS cython-builder
WORKDIR /build
# Copy and install requirements first (better caching)
COPY requirements.txt WebHostLib/requirements.txt
RUN pip install --no-cache-dir -r \
WebHostLib/requirements.txt \
setuptools
COPY _speedups.pyx .
COPY intset.h .
RUN cythonize -b -i _speedups.pyx
# Archipelago
FROM python:3.12-slim AS archipelago
ARG TARGETARCH
ENV VIRTUAL_ENV=/opt/venv
ENV PYTHONUNBUFFERED=1
WORKDIR /app
# Install requirements
# hadolint ignore=DL3008
RUN apt-get update && \
apt-get install -y --no-install-recommends \
git \
gcc=4:12.2.0-3 \
libc6-dev \
libtk8.6=8.6.13-2 \
g++=4:12.2.0-3 \
curl && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Create and activate venv
RUN python -m venv $VIRTUAL_ENV; \
. $VIRTUAL_ENV/bin/activate
# Copy and install requirements first (better caching)
COPY WebHostLib/requirements.txt WebHostLib/requirements.txt
RUN pip install --no-cache-dir -r \
WebHostLib/requirements.txt \
gunicorn==23.0.0
COPY . .
COPY --from=cython-builder /build/*.so ./
# Run ModuleUpdate
RUN python ModuleUpdate.py -y
# Purge unneeded packages
RUN apt-get purge -y \
git \
gcc \
libc6-dev \
g++ && \
apt-get autoremove -y
# Copy necessary components
COPY --from=enemizer /release/EnemizerCLI /tmp/EnemizerCLI
# No release for arm architecture. Skip.
RUN if [ "$TARGETARCH" = "amd64" ]; then \
cp /tmp/EnemizerCLI EnemizerCLI; \
fi; \
rm -rf /tmp/EnemizerCLI
# Define health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:${PORT:-80} || exit 1
ENV SKIP_REQUIREMENTS_UPDATE=true
ENTRYPOINT [ "python", "WebHost.py" ]

84
Fill.py
View File

@@ -116,6 +116,13 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
else:
# we filled all reachable spots.
if swap:
# Keep a cache of previous safe swap states that might be usable to sweep from to produce the next
# swap state, instead of sweeping from `base_state` each time.
previous_safe_swap_state_cache: typing.Deque[CollectionState] = deque()
# Almost never are more than 2 states needed. The rare cases that do are usually highly restrictive
# single_player_placement=True pre-fills which can go through more than 10 states in some seeds.
max_swap_base_state_cache_length = 3
# try swapping this item with previously placed items in a safe way then in an unsafe way
swap_attempts = ((i, location, unsafe)
for unsafe in (False, True)
@@ -130,9 +137,30 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
location.item = None
placed_item.location = None
swap_state = sweep_from_pool(base_state, [placed_item, *item_pool] if unsafe else item_pool,
multiworld.get_filled_locations(item.player)
if single_player_placement else None)
for previous_safe_swap_state in previous_safe_swap_state_cache:
# If a state has already checked the location of the swap, then it cannot be used.
if location not in previous_safe_swap_state.advancements:
# Previous swap states will have collected all items in `item_pool`, so the new
# `swap_state` can skip having to collect them again.
# Previous swap states will also have already checked many locations, making the sweep
# faster.
swap_state = sweep_from_pool(previous_safe_swap_state, (placed_item,) if unsafe else (),
multiworld.get_filled_locations(item.player)
if single_player_placement else None)
break
else:
# No previous swap_state was usable as a base state to sweep from, so create a new one.
swap_state = sweep_from_pool(base_state, [placed_item, *item_pool] if unsafe else item_pool,
multiworld.get_filled_locations(item.player)
if single_player_placement else None)
# Unsafe states should not be added to the cache because they have collected `placed_item`.
if not unsafe:
if len(previous_safe_swap_state_cache) >= max_swap_base_state_cache_length:
# Remove the oldest cached state.
previous_safe_swap_state_cache.pop()
# Add the new state to the start of the cache.
previous_safe_swap_state_cache.appendleft(swap_state)
# unsafe means swap_state assumes we can somehow collect placed_item before item_to_place
# by continuing to swap, which is not guaranteed. This is unsafe because there is no mechanic
# to clean that up later, so there is a chance generation fails.
@@ -450,6 +478,12 @@ def distribute_early_items(multiworld: MultiWorld,
def distribute_items_restrictive(multiworld: MultiWorld,
panic_method: typing.Literal["swap", "raise", "start_inventory"] = "swap") -> None:
assert all(item.location is None for item in multiworld.itempool), (
"At the start of distribute_items_restrictive, "
"there are items in the multiworld itempool that are already placed on locations:\n"
f"{[(item.location, item) for item in multiworld.itempool if item.location is not None]}"
)
fill_locations = sorted(multiworld.get_unfilled_locations())
multiworld.random.shuffle(fill_locations)
# get items to distribute
@@ -492,18 +526,48 @@ def distribute_items_restrictive(multiworld: MultiWorld,
single_player = multiworld.players == 1 and not multiworld.groups
if prioritylocations:
regular_progression = []
deprioritized_progression = []
for item in progitempool:
if item.deprioritized:
deprioritized_progression.append(item)
else:
regular_progression.append(item)
# "priority fill"
maximum_exploration_state = sweep_from_pool(multiworld.state)
fill_restrictive(multiworld, maximum_exploration_state, prioritylocations, progitempool,
# try without deprioritized items in the mix at all. This means they need to be collected into state first.
priority_fill_state = sweep_from_pool(multiworld.state, deprioritized_progression)
fill_restrictive(multiworld, priority_fill_state, prioritylocations, regular_progression,
single_player_placement=single_player, swap=False, on_place=mark_for_locking,
name="Priority", one_item_per_player=True, allow_partial=True)
if prioritylocations:
if prioritylocations and regular_progression:
# retry with one_item_per_player off because some priority fills can fail to fill with that optimization
maximum_exploration_state = sweep_from_pool(multiworld.state)
fill_restrictive(multiworld, maximum_exploration_state, prioritylocations, progitempool,
single_player_placement=single_player, swap=False, on_place=mark_for_locking,
name="Priority Retry", one_item_per_player=False)
# deprioritized items are still not in the mix, so they need to be collected into state first.
priority_retry_state = sweep_from_pool(multiworld.state, deprioritized_progression)
fill_restrictive(multiworld, priority_retry_state, prioritylocations, regular_progression,
single_player_placement=single_player, swap=False, on_place=mark_for_locking,
name="Priority Retry", one_item_per_player=False, allow_partial=True)
if prioritylocations and deprioritized_progression:
# There are no more regular progression items that can be placed on any priority locations.
# We'd still prefer to place deprioritized progression items on priority locations over filler items.
# Since we're leaving out the remaining regular progression now, we need to collect it into state first.
priority_retry_2_state = sweep_from_pool(multiworld.state, regular_progression)
fill_restrictive(multiworld, priority_retry_2_state, prioritylocations, deprioritized_progression,
single_player_placement=single_player, swap=False, on_place=mark_for_locking,
name="Priority Retry 2", one_item_per_player=True, allow_partial=True)
if prioritylocations and deprioritized_progression:
# retry with deprioritized items AND without one_item_per_player optimisation
# Since we're leaving out the remaining regular progression now, we need to collect it into state first.
priority_retry_3_state = sweep_from_pool(multiworld.state, regular_progression)
fill_restrictive(multiworld, priority_retry_3_state, prioritylocations, deprioritized_progression,
single_player_placement=single_player, swap=False, on_place=mark_for_locking,
name="Priority Retry 3", one_item_per_player=False)
# restore original order of progitempool
progitempool[:] = [item for item in progitempool if not item.location]
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
defaultlocations = prioritylocations + defaultlocations

View File

@@ -16,7 +16,11 @@ elif sys.version_info < (3, 10, 1):
raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. 3.10.1+ is supported.")
# don't run update if environment is frozen/compiled or if not the parent process (skip in subprocess)
_skip_update = bool(getattr(sys, "frozen", False) or multiprocessing.parent_process())
_skip_update = bool(
getattr(sys, "frozen", False) or
multiprocessing.parent_process() or
os.environ.get("SKIP_REQUIREMENTS_UPDATE", "").lower() in ("1", "true", "yes")
)
update_ran = _skip_update

View File

@@ -1950,6 +1950,48 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
if locs and create_as_hint:
ctx.save()
await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}])
elif cmd == 'CreateHints':
location_player = args.get("player", client.slot)
locations = args["locations"]
status = args.get("status", HintStatus.HINT_UNSPECIFIED)
if not locations:
await ctx.send_msgs(client, [{"cmd": "InvalidPacket", "type": "arguments",
"text": "CreateHints: No locations specified.", "original_cmd": cmd}])
hints = []
for location in locations:
if location_player != client.slot and location not in ctx.locations[location_player]:
error_text = (
"CreateHints: One or more of the locations do not exist for the specified off-world player. "
"Please refrain from hinting other slot's locations that you don't know contain your items."
)
await ctx.send_msgs(client, [{"cmd": "InvalidPacket", "type": "arguments",
"text": error_text, "original_cmd": cmd}])
return
target_item, item_player, flags = ctx.locations[location_player][location]
if client.slot not in ctx.slot_set(item_player):
if status != HintStatus.HINT_UNSPECIFIED:
error_text = 'CreateHints: Must use "unspecified"/None status for items from other players.'
await ctx.send_msgs(client, [{"cmd": "InvalidPacket", "type": "arguments",
"text": error_text, "original_cmd": cmd}])
return
if client.slot != location_player:
error_text = "CreateHints: Can only create hints for own items or own locations."
await ctx.send_msgs(client, [{"cmd": "InvalidPacket", "type": "arguments",
"text": error_text, "original_cmd": cmd}])
return
hints += collect_hint_location_id(ctx, client.team, location_player, location, status)
# As of writing this code, only_new=True does not update status for existing hints
ctx.notify_hints(client.team, hints, only_new=True)
ctx.save()
elif cmd == 'UpdateHint':
location = args["location"]

View File

@@ -865,13 +865,13 @@ class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mappin
return ", ".join(f"{key}: {v}" for key, v in value.items())
def __getitem__(self, item: str) -> typing.Any:
return self.value.__getitem__(item)
return self.value[item]
def __iter__(self) -> typing.Iterator[str]:
return self.value.__iter__()
return iter(self.value)
def __len__(self) -> int:
return self.value.__len__()
return len(self.value)
# __getitem__ fallback fails for Counters, so we define this explicitly
def __contains__(self, item) -> bool:
@@ -1067,10 +1067,10 @@ class PlandoTexts(Option[typing.List[PlandoText]], VerifyKeys):
yield from self.value
def __getitem__(self, index: typing.SupportsIndex) -> PlandoText:
return self.value.__getitem__(index)
return self.value[index]
def __len__(self) -> int:
return self.value.__len__()
return len(self.value)
class ConnectionsMeta(AssembleOptions):
@@ -1217,7 +1217,7 @@ class PlandoConnections(Option[typing.List[PlandoConnection]], metaclass=Connect
connection.exit) for connection in value])
def __getitem__(self, index: typing.SupportsIndex) -> PlandoConnection:
return self.value.__getitem__(index)
return self.value[index]
def __iter__(self) -> typing.Iterator[PlandoConnection]:
yield from self.value
@@ -1315,6 +1315,7 @@ class CommonOptions(metaclass=OptionsMetaProperty):
will be returned as a sorted list.
"""
assert option_names, "options.as_dict() was used without any option names."
assert len(option_names) < len(self.__class__.type_hints), "Specify only options you need."
option_results = {}
for option_name in option_names:
if option_name not in type(self).type_hints:

View File

@@ -80,6 +80,7 @@ Currently, the following games are supported:
* Jak and Daxter: The Precursor Legacy
* Super Mario Land 2: 6 Golden Coins
* shapez
* Paint
* Satisfactory
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).

View File

@@ -47,7 +47,7 @@ class Version(typing.NamedTuple):
return ".".join(str(item) for item in self)
__version__ = "0.6.2"
__version__ = "0.6.3"
version_tuple = tuplize_version(__version__)
is_linux = sys.platform.startswith("linux")
@@ -413,13 +413,23 @@ def get_adjuster_settings(game_name: str) -> Namespace:
@cache_argsless
def get_unique_identifier():
uuid = persistent_load().get("client", {}).get("uuid", None)
common_path = cache_path("common.json")
if os.path.exists(common_path):
with open(common_path) as f:
common_file = json.load(f)
uuid = common_file.get("uuid", None)
else:
common_file = {}
uuid = None
if uuid:
return uuid
import uuid
uuid = uuid.getnode()
persistent_store("client", "uuid", uuid)
from uuid import uuid4
uuid = str(uuid4())
common_file["uuid"] = uuid
with open(common_path, "w") as f:
json.dump(common_file, f, separators=(",", ":"))
return uuid

View File

@@ -61,18 +61,26 @@ cache = Cache()
Compress(app)
def to_python(value):
return uuid.UUID(bytes=base64.urlsafe_b64decode(value + '=='))
def to_url(value):
return base64.urlsafe_b64encode(value.bytes).rstrip(b'=').decode('ascii')
class B64UUIDConverter(BaseConverter):
def to_python(self, value):
return uuid.UUID(bytes=base64.urlsafe_b64decode(value + '=='))
return to_python(value)
def to_url(self, value):
return base64.urlsafe_b64encode(value.bytes).rstrip(b'=').decode('ascii')
return to_url(value)
# short UUID
app.url_map.converters["suuid"] = B64UUIDConverter
app.jinja_env.filters['suuid'] = lambda value: base64.urlsafe_b64encode(value.bytes).rstrip(b'=').decode('ascii')
app.jinja_env.filters["suuid"] = to_url
app.jinja_env.filters["title_sorted"] = title_sorted

View File

@@ -3,6 +3,7 @@ from uuid import UUID
from flask import abort, url_for
from WebHostLib import to_url
import worlds.Files
from . import api_endpoints, get_players
from ..models import Room
@@ -33,7 +34,7 @@ def room_info(room_id: UUID) -> Dict[str, Any]:
downloads.append(slot_download)
return {
"tracker": room.tracker,
"tracker": to_url(room.tracker),
"players": get_players(room.seed),
"last_port": room.last_port,
"last_activity": room.last_activity,

View File

@@ -1,6 +1,7 @@
from flask import session, jsonify
from pony.orm import select
from WebHostLib import to_url
from WebHostLib.models import Room, Seed
from . import api_endpoints, get_players
@@ -10,13 +11,13 @@ def get_rooms():
response = []
for room in select(room for room in Room if room.owner == session["_id"]):
response.append({
"room_id": room.id,
"seed_id": room.seed.id,
"room_id": to_url(room.id),
"seed_id": to_url(room.seed.id),
"creation_time": room.creation_time,
"last_activity": room.last_activity,
"last_port": room.last_port,
"timeout": room.timeout,
"tracker": room.tracker,
"tracker": to_url(room.tracker),
})
return jsonify(response)
@@ -26,7 +27,7 @@ def get_seeds():
response = []
for seed in select(seed for seed in Seed if seed.owner == session["_id"]):
response.append({
"seed_id": seed.id,
"seed_id": to_url(seed.id),
"creation_time": seed.creation_time,
"players": get_players(seed),
})

View File

@@ -32,6 +32,9 @@
{% elif patch.game == "Super Mario 64" and room.seed.slots|length == 1 %}
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
Download APSM64EX File...</a>
{% elif patch.game == "Factorio" %}
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
Download Factorio Mod...</a>
{% elif patch.game | is_applayercontainer(patch.data, patch.player_id) %}
<a href="{{ url_for("download_patch", patch_id=patch.id, room_id=room.id) }}" download>
Download Patch File...</a>

View File

@@ -46,7 +46,9 @@ requires:
{{ yaml_dump(game) }}:
{%- for group_name, group_options in option_groups.items() %}
# {{ group_name }}
##{% for _ in group_name %}#{% endfor %}##
# {{ group_name }} #
##{% for _ in group_name %}#{% endfor %}##
{%- for option_key, option in group_options.items() %}
{{ option_key }}:

61
deploy/docker-compose.yml Normal file
View File

@@ -0,0 +1,61 @@
services:
multiworld:
# Build only once. Web service uses the same image build
build:
context: ..
# Name image for use in web service
image: archipelago-base
# Use locally-built image
pull_policy: never
# Launch main process without website hosting (config override)
entrypoint: python WebHost.py --config_override selflaunch.yaml
volumes:
# Mount application volume
- app_volume:/app
# Mount configs
- ./example_config.yaml:/app/config.yaml
- ./example_selflaunch.yaml:/app/selflaunch.yaml
# Expose on host network for access to dynamically mapped ports
network_mode: host
# No Healthcheck in place yet for multiworld
healthcheck:
test: ["NONE"]
web:
# Use image build by multiworld service
image: archipelago-base
# Use locally-built image
pull_policy: never
# Launch gunicorn targeting WebHost application
entrypoint: gunicorn -c gunicorn.conf.py
volumes:
# Mount application volume
- app_volume:/app
# Mount configs
- ./example_config.yaml:/app/config.yaml
- ./example_gunicorn.conf.py:/app/gunicorn.conf.py
environment:
# Bind gunicorn on 8000
- PORT=8000
nginx:
image: nginx:stable-alpine
volumes:
# Mount application volume
- app_volume:/app
# Mount config
- ./example_nginx.conf:/etc/nginx/nginx.conf
ports:
# Nginx listening internally on port 80 -- mapped to 8080 on host
- 8080:80
depends_on:
- web
volumes:
# Share application directory amongst multiworld and web services
# (for access to log files and the like), and nginx (for static files)
app_volume:

View File

@@ -0,0 +1,10 @@
# Refer to ../docs/webhost configuration sample.yaml
# We'll be hosting VIA gunicorn
SELFHOST: false
# We'll start a separate process for rooms and generators
SELFLAUNCH: false
# Host Address. This is the address encoded into the patch that will be used for client auto-connect.
# Set as your local IP (192.168.x.x) to serve over LAN.
HOST_ADDRESS: localhost

View File

@@ -0,0 +1,19 @@
workers = 2
threads = 2
wsgi_app = "WebHost:get_app()"
accesslog = "-"
access_log_format = (
'%({x-forwarded-for}i)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
)
worker_class = "gthread" # "sync" | "gthread"
forwarded_allow_ips = "*"
loglevel = "info"
"""
You can programatically set values.
For example, set number of workers to half of the cpu count:
import multiprocessing
workers = multiprocessing.cpu_count() / 2
"""

64
deploy/example_nginx.conf Normal file
View File

@@ -0,0 +1,64 @@
worker_processes 1;
user nobody nogroup;
# 'user nobody nobody;' for systems with 'nobody' as a group instead
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024; # increase if you have lots of clients
accept_mutex off; # set to 'on' if nginx worker_processes > 1
# 'use epoll;' to enable for Linux 2.6+
# 'use kqueue;' to enable for FreeBSD, OSX
use epoll;
}
http {
include mime.types;
# fallback in case we can't determine a type
default_type application/octet-stream;
access_log /var/log/nginx/access.log combined;
sendfile on;
upstream app_server {
# fail_timeout=0 means we always retry an upstream even if it failed
# to return a good HTTP response
# for UNIX domain socket setups
# server unix:/tmp/gunicorn.sock fail_timeout=0;
# for a TCP configuration
server web:8000 fail_timeout=0;
}
server {
# use 'listen 80 deferred;' for Linux
# use 'listen 80 accept_filter=httpready;' for FreeBSD
listen 80 deferred;
client_max_body_size 4G;
# set the correct host(s) for your site
# server_name example.com www.example.com;
keepalive_timeout 5;
# path for static files
root /app/WebHostLib;
location / {
# checks for static file, if not found proxy to app
try_files $uri @proxy_to_app;
}
location @proxy_to_app {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;
# we don't want nginx trying to do something clever with
# redirects, we set the Host: header above already.
proxy_redirect off;
proxy_pass http://app_server;
}
}
}

View File

@@ -0,0 +1,13 @@
# Refer to ../docs/webhost configuration sample.yaml
# We'll be hosting VIA gunicorn
SELFHOST: false
# Start room and generator processes
SELFLAUNCH: true
JOB_THRESHOLD: 0
# Maximum concurrent world gens
GENERATORS: 3
# Rooms will be spread across multiple processes
HOSTERS: 4

View File

@@ -136,6 +136,9 @@
# Overcooked! 2
/worlds/overcooked2/ @toasterparty
# Paint
/worlds/paint/ @MarioManTAW
# Pokemon Emerald
/worlds/pokemon_emerald/ @Zunawe

View File

@@ -0,0 +1,91 @@
# Deploy Using Containers
If you just want to play and there is a compiled version available on the [Archipelago releases page](https://github.com/ArchipelagoMW/Archipelago/releases), use that version.
To build the full Archipelago software stack, refer to [Running From Source](running%20from%20source.md).
Follow these steps to build and deploy a containerized instance of the web host software, optionally integrating [Gunicorn](https://gunicorn.org/) WSGI HTTP Server running behind the [nginx](https://nginx.org/) reverse proxy.
## Building the Container Image
What you'll need:
* A container runtime engine such as:
* [Docker](https://www.docker.com/)
* [Podman](https://podman.io/)
* For running with rootless podman, you need to ensure all ports used are usable rootless, by default ports less than 1024 are root only. See [the official tutorial](https://github.com/containers/podman/blob/main/docs/tutorials/rootless_tutorial.md) for details.
Starting from the root repository directory, the standalone Archipelago image can be built and run with the command:
`docker build -t archipelago .`
Or:
`podman build -t archipelago .`
It is recommended to tag the image using `-t` to more easily identify the image and run it.
## Running the Container
Running the container can be performed using:
`docker run --network host archipelago`
Or:
`podman run --network host archipelago`
The Archipelago web host requires access to multiple ports in order to host game servers simultaneously. To simplify configuration for this purpose, specify `--network host`.
Given the default configuration, the website will be accessible at the hostname/IP address (localhost if run locally) of the machine being deployed to, at port 80. It can be configured by creating a YAML file and mapping a volume to the container when running initially:
`docker run archipelago --network host -v /path/to/config.yaml:/app/config.yaml`
See `docs/webhost configuration sample.yaml` for example.
## Using Docker Compose
An example [docker compose](../deploy/docker-compose.yml) file can be found in [deploy](../deploy), along with example configuration files used by the services it orchestrates. Using these files as-is will spin up two separate archipelago containers with special modifications to their runtime arguments, in addition to deploying an `nginx` reverse proxy container.
To deploy in this manner, from the ["deploy"](../deploy) directory, run:
`docker compose up -d`
### Services
The `docker-compose.yaml` file defines three services:
* multiworld:
* Executes the main `WebHost` process, using the [example config](../deploy/example_config.yaml), and overriding with a secondary [selflaunch example config](../deploy/example_selflaunch.yaml). This is because we do not want to launch the website through this service.
* web:
* Executes `gunicorn` using its [example config](../deploy/example_gunicorn.conf.py), which will bind it to the `WebHost` application, in effect launching it.
* We mount the main [config](../deploy/example_config.yaml) without an override to specify that we are launching the website through this service.
* No ports are exposed through to the host.
* nginx:
* Serves as a reverse proxy with `web` as its upstream.
* Directs all HTTP traffic from port 80 to the upstream service.
* Exposed to the host on port 8080. This is where we can reach the website.
### Configuration
As these are examples, they can be copied and modified. For instance setting the value of `HOST_ADDRESS` in [example config](../deploy/example_config.yaml) to host machines local IP address, will expose the service to its local area network.
The configuration files may be modified to handle for machine-specific optimizations, such as:
* Web pages responding too slowly
* Edit [the gunicorn config](../deploy/example_gunicorn.conf.py) to increase thread and/or worker count.
* Game generation stalls
* Increase the generator count in [selflaunch config](../deploy/example_selflaunch.yaml)
* Gameplay lags
* Increase the hoster count in [selflaunch config](../deploy/example_selflaunch.yaml)
Changes made to `docker-compose.yaml` can be applied by running `docker compose up -d`, while those made to other files are applied by running `docker compose restart`.
## Windows
It is possible to carry out these deployment steps on Windows under [Windows Subsystem for Linux](https://learn.microsoft.com/en-us/windows/wsl/install).
## Optional: A Link to the Past Enemizer
Only required to generate seeds that include A Link to the Past with certain options enabled. You will receive an
error if it is required.
Enemizer can be enabled on `x86_64` platform architecture, and is included in the image build process. Enemizer requires a version 1.0 Japanese "Zelda no Densetsu" `.sfc` rom file to be placed in the application directory:
`docker run archipelago -v "/path/to/zelda.sfc:/app/Zelda no Densetsu - Kamigami no Triforce (Japan).sfc"`.
Enemizer is not currently available for `aarch64`.
## Optional: Git
Building the image requires a local copy of the ArchipelagoMW source code.
Refer to [Running From Source](running%20from%20source.md#optional-git).

View File

@@ -276,6 +276,7 @@ These packets are sent purely from client to server. They are not accepted by cl
* [Sync](#Sync)
* [LocationChecks](#LocationChecks)
* [LocationScouts](#LocationScouts)
* [CreateHints](#CreateHints)
* [UpdateHint](#UpdateHint)
* [StatusUpdate](#StatusUpdate)
* [Say](#Say)
@@ -294,7 +295,7 @@ Sent by the client to initiate a connection to an Archipelago game session.
| password | str | If the game session requires a password, it should be passed here. |
| game | str | The name of the game the client is playing. Example: `A Link to the Past` |
| name | str | The player name for this client. |
| uuid | str | Unique identifier for player client. |
| uuid | str | Unique identifier for player. Cached in the user cache \Archipelago\Cache\common.json |
| version | [NetworkVersion](#NetworkVersion) | An object representing the Archipelago version this client supports. |
| items_handling | int | Flags configuring which items should be sent by the server. Read below for individual flags. |
| tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. [Tags](#Tags) |
@@ -347,6 +348,21 @@ This is useful in cases where an item appears in the game world, such as 'ledge
| locations | list\[int\] | The ids of the locations seen by the client. May contain any number of locations, even ones sent before; duplicates do not cause issues with the Archipelago server. |
| create_as_hint | int | If non-zero, the scouted locations get created and broadcasted as a player-visible hint. <br/>If 2 only new hints are broadcast, however this does not remove them from the LocationInfo reply. |
### CreateHints
Sent to the server to create hints for a specified list of locations.
Hints that already exist will be silently skipped and their status will not be updated.
When creating hints for another slot's locations, the packet will fail if any of those locations don't contain items for the requesting slot.
When creating hints for your own slot's locations, non-existing locations will silently be skipped.
#### Arguments
| Name | Type | Notes |
| ---- | ---- | ----- |
| locations | list\[int\] | The ids of the locations to create hints for. |
| player | int | The ID of the player whose locations are being hinted for. Defaults to the requesting slot. |
| status | [HintStatus](#HintStatus) | If included, sets the status of the hint to this status. Defaults to `HINT_UNSPECIFIED`. Cannot set `HINT_FOUND`. |
### UpdateHint
Sent to the server to update the status of a Hint. The client must be the 'receiving_player' of the Hint, or the update fails.

347
docs/webhost api.md Normal file
View File

@@ -0,0 +1,347 @@
# API Guide
Archipelago has a rudimentary API that can be queried by endpoints. The API is a work-in-progress and should be improved over time.
The following API requests are formatted as: `https://<Archipelago URL>/api/<endpoint>`
The returned data will be formated in a combination of JSON lists or dicts, with their keys or values being notated in `blocks` (if applicable)
Current endpoints:
- Datapackage API
- [`/datapackage`](#datapackage)
- [`/datapackage/<string:checksum>`](#datapackagestringchecksum)
- [`/datapackage_checksum`](#datapackagechecksum)
- Generation API
- [`/generate`](#generate)
- [`/status/<suuid:seed>`](#status)
- Room API
- [`/room_status/<suuid:room_id>`](#roomstatus)
- User API
- [`/get_rooms`](#getrooms)
- [`/get_seeds`](#getseeds)
## Datapackage Endpoints
These endpoints are used by applications to acquire a room's datapackage, and validate that they have the correct datapackage for use. Datapackages normally include, item IDs, location IDs, and name groupings, for a given room, and are essential for mapping IDs received from Archipelago to their correct items or locations.
### `/datapackage`
<a name="datapackage"></a>
Fetches the current datapackage from the WebHost.
You'll receive a dict named `games` that contains a named dict of every game and its data currently supported by Archipelago.
Each game will have:
- A checksum `checksum`
- A dict of item groups `item_name_groups`
- Item name to AP ID dict `item_name_to_id`
- A dict of location groups `location_name_groups`
- Location name to AP ID dict `location_name_to_id`
Example:
```
{
"games": {
...
"Clique": {
"checksum": "0271f7a80b44ba72187f92815c2bc8669cb464c7",
"item_name_groups": {
"Everything": [
"A Cool Filler Item (No Satisfaction Guaranteed)",
"Button Activation",
"Feeling of Satisfaction"
]
},
"item_name_to_id": {
"A Cool Filler Item (No Satisfaction Guaranteed)": 69696967,
"Button Activation": 69696968,
"Feeling of Satisfaction": 69696969
},
"location_name_groups": {
"Everywhere": [
"The Big Red Button",
"The Item on the Desk"
]
},
"location_name_to_id": {
"The Big Red Button": 69696969,
"The Item on the Desk": 69696968
}
},
...
}
}
```
### `/datapackage/<string:checksum>`
<a name="datapackagestringchecksum"></a>
Fetches a single datapackage by checksum.
Returns a dict of the game's data with:
- A checksum `checksum`
- A dict of item groups `item_name_groups`
- Item name to AP ID dict `item_name_to_id`
- A dict of location groups `location_name_groups`
- Location name to AP ID dict `location_name_to_id`
Its format will be identical to the whole-datapackage endpoint (`/datapackage`), except you'll only be returned the single game's data in a dict.
### `/datapackage_checksum`
<a name="datapackagechecksum"></a>
Fetches the checksums of the current static datapackages on the WebHost.
You'll receive a dict with `game:checksum` key-value pairs for all the current officially supported games.
Example:
```
{
...
"Donkey Kong Country 3":"f90acedcd958213f483a6a4c238e2a3faf92165e",
"Factorio":"a699194a9589db3ebc0d821915864b422c782f44",
...
}
```
## Generation Endpoint
These endpoints are used internally for the WebHost to generate games and validate their generation. They are also used by external applications to generate games automatically.
### `/generate`
<a name="generate"></a>
Submits a game to the WebHost for generation.
**This endpoint only accepts a POST HTTP request.**
There are two ways to submit data for generation: With a file and with JSON.
#### With a file:
Have your ZIP of yaml(s) or a single yaml, and submit a POST request to the `/generate` endpoint.
If the options are valid, you'll be returned a successful generation response. (see [Generation Response](#generation-response))
Example using the python requests library:
```
file = {'file': open('Games.zip', 'rb')}
req = requests.post("https://archipelago.gg/api/generate", files=file)
```
#### With JSON:
Compile your weights/yaml data into a dict. Then insert that into a dict with the key `"weights"`.
Finally, submit a POST request to the `/generate` endpoint.
If the weighted options are valid, you'll be returned a successful generation response (see [Generation Response](#generation-response))
Example using the python requests library:
```
data = {"Test":{"game": "Factorio","name": "Test","Factorio": {}},}
weights={"weights": data}
req = requests.post("https://archipelago.gg/api/generate", json=weights)
```
#### Generation Response:
##### Successful Generation:
Upon successful generation, you'll be sent a JSON dict response detailing the generation:
- The UUID of the generation `detail`
- The SUUID of the generation `encoded`
- The response text `text`
- The page that will resolve to the seed/room generation page once generation has completed `url`
- The API status page of the generation `wait_api_url` (see [Status Endpoint](#status))
Example:
```
{
"detail": "19878f16-5a58-4b76-aab7-d6bf38be9463",
"encoded": "GYePFlpYS3aqt9a_OL6UYw",
"text": "Generation of seed 19878f16-5a58-4b76-aab7-d6bf38be9463 started successfully.",
"url": "http://archipelago.gg/wait/GYePFlpYS3aqt9a_OL6UYw",
"wait_api_url": "http://archipelago.gg/api/status/GYePFlpYS3aqt9a_OL6UYw"
}
```
##### Failed Generation:
Upon failed generation, you'll be returned a single key-value pair. The key will always be `text`
The value will give you a hint as to what may have gone wrong.
- Options without tags, and a 400 status code
- Options in a string, and a 400 status code
- Invalid file/weight string, `No options found. Expected file attachment or json weights.` with a 400 status code
- Too many slots for the server to process, `Max size of multiworld exceeded` with a 409 status code
If the generation detects a issue in generation, you'll be sent a dict with two key-value pairs (`text` and `detail`) and a 400 status code. The values will be:
- Summary of issue in `text`
- Detailed issue in `detail`
In the event of an unhandled server exception, you'll be provided a dict with a single key `text`:
- Exception, `Uncought Exception: <error>` with a 500 status code
### `/status/<suuid:seed>`
<a name="status"></a>
Retrieves the status of the seed's generation.
This endpoint will return a dict with a single key-vlaue pair. The key will always be `text`
The value will tell you the status of the generation:
- Generation was completed: `Generation done` with a 201 status code
- Generation request was not found: `Generation not found` with a 404 status code
- Generation of the seed failed: `Generation failed` with a 500 status code
- Generation is in progress still: `Generation running` with a 202 status code
## Room Endpoints
Endpoints to fetch information of the active WebHost room with the supplied room_ID.
### `/room_status/<suuid:room_id>`
<a name="roomstatus"></a>
Will provide a dict of room data with the following keys:
- Tracker SUUID (`tracker`)
- A list of players (`players`)
- Each item containing a list with the Slot name and Game
- Last known hosted port (`last_port`)
- Last activity timestamp (`last_activity`)
- The room timeout counter (`timeout`)
- A list of downloads for files required for gameplay (`downloads`)
- Each item is a dict containings the download URL and slot (`slot`, `download`)
Example:
```
{
"downloads": [
{
"download": "/slot_file/kK5fmxd8TfisU5Yp_eg/1",
"slot": 1
},
{
"download": "/slot_file/kK5fmxd8TfisU5Yp_eg/2",
"slot": 2
},
{
"download": "/slot_file/kK5fmxd8TfisU5Yp_eg/3",
"slot": 3
},
{
"download": "/slot_file/kK5fmxd8TfisU5Yp_eg/4",
"slot": 4
},
{
"download": "/slot_file/kK5fmxd8TfisU5Yp_eg/5",
"slot": 5
}
],
"last_activity": "Fri, 18 Apr 2025 20:35:45 GMT",
"last_port": 52122,
"players": [
[
"Slot_Name_1",
"Ocarina of Time"
],
[
"Slot_Name_2",
"Ocarina of Time"
],
[
"Slot_Name_3",
"Ocarina of Time"
],
[
"Slot_Name_4",
"Ocarina of Time"
],
[
"Slot_Name_5",
"Ocarina of Time"
]
],
"timeout": 7200,
"tracker": "cf6989c0-4703-45d7-a317-2e5158431171"
}
```
## User Endpoints
User endpoints can get room and seed details from the current session tokens (cookies)
### `/get_rooms`
<a name="getrooms"></a>
Retreives a list of all rooms currently owned by the session token.
Each list item will contain a dict with the room's details:
- Room SUUID (`room_id`)
- Seed SUUID (`seed_id`)
- Creation timestamp (`creation_time`)
- Last activity timestamp (`last_activity`)
- Last known AP port (`last_port`)
- Room timeout counter in seconds (`timeout`)
- Room tracker SUUID (`tracker`)
Example:
```
[
{
"creation_time": "Fri, 18 Apr 2025 19:46:53 GMT",
"last_activity": "Fri, 18 Apr 2025 21:16:02 GMT",
"last_port": 52122,
"room_id": "90ae5f9b-177c-4df8-ac53-9629fc3bff7a",
"seed_id": "efbd62c2-aaeb-4dda-88c3-f461c029cef6",
"timeout": 7200,
"tracker": "cf6989c0-4703-45d7-a317-2e5158431171"
},
{
"creation_time": "Fri, 18 Apr 2025 20:36:42 GMT",
"last_activity": "Fri, 18 Apr 2025 20:36:46 GMT",
"last_port": 56884,
"room_id": "14465c05-d08e-4d28-96bd-916f994609d8",
"seed_id": "a528e34c-3b4f-42a9-9f8f-00a4fd40bacb",
"timeout": 7200,
"tracker": "4e624bd8-32b6-42e4-9178-aa407f72751c"
}
]
```
### `/get_seeds`
<a name="getseeds"></a>
Retreives a list of all seeds currently owned by the session token.
Each item in the list will contain a dict with the seed's details:
- Seed SUUID (`seed_id`)
- Creation timestamp (`creation_time`)
- A list of player slots (`players`)
- Each item in the list will contain a list of the slot name and game
Example:
```
[
{
"creation_time": "Fri, 18 Apr 2025 19:46:52 GMT",
"players": [
[
"Slot_Name_1",
"Ocarina of Time"
],
[
"Slot_Name_2",
"Ocarina of Time"
],
[
"Slot_Name_3",
"Ocarina of Time"
],
[
"Slot_Name_4",
"Ocarina of Time"
],
[
"Slot_Name_5",
"Ocarina of Time"
]
],
"seed_id": "efbd62c2-aaeb-4dda-88c3-f461c029cef6"
},
{
"creation_time": "Fri, 18 Apr 2025 20:36:39 GMT",
"players": [
[
"Slot_Name_1",
"Clique"
],
[
"Slot_Name_2",
"Clique"
],
[
"Slot_Name_3",
"Clique"
],
[
"Slot_Name_4",
"Archipelago"
]
],
"seed_id": "a528e34c-3b4f-42a9-9f8f-00a4fd40bacb"
}
]
```

View File

@@ -603,6 +603,28 @@ class TestDistributeItemsRestrictive(unittest.TestCase):
self.assertTrue(player3.locations[2].item.advancement)
self.assertTrue(player3.locations[3].item.advancement)
def test_deprioritized_does_not_land_on_priority(self):
multiworld = generate_test_multiworld(1)
player1 = generate_player_data(multiworld, 1, 2, prog_item_count=2)
player1.prog_items[0].classification |= ItemClassification.deprioritized
player1.locations[0].progress_type = LocationProgressType.PRIORITY
distribute_items_restrictive(multiworld)
self.assertFalse(player1.locations[0].item.deprioritized)
def test_deprioritized_still_goes_on_priority_ahead_of_filler(self):
multiworld = generate_test_multiworld(1)
player1 = generate_player_data(multiworld, 1, 2, prog_item_count=1, basic_item_count=1)
player1.prog_items[0].classification |= ItemClassification.deprioritized
player1.locations[0].progress_type = LocationProgressType.PRIORITY
distribute_items_restrictive(multiworld)
self.assertTrue(player1.locations[0].item.advancement)
def test_can_remove_locations_in_fill_hook(self):
"""Test that distribute_items_restrictive calls the fill hook and allows for item and location removal"""
multiworld = generate_test_multiworld()

View File

@@ -2,6 +2,8 @@ import re
from pathlib import Path
from typing import TYPE_CHECKING, Optional, cast
from WebHostLib import to_python
if TYPE_CHECKING:
from flask import Flask
from werkzeug.test import Client as FlaskClient
@@ -103,7 +105,7 @@ def stop_room(app_client: "FlaskClient",
poll_interval = 2
print(f"Stopping room {room_id}")
room_uuid = app.url_map.converters["suuid"].to_python(None, room_id) # type: ignore[arg-type]
room_uuid = to_python(room_id)
if timeout is not None:
sleep(.1) # should not be required, but other things might use threading
@@ -156,7 +158,7 @@ def set_room_timeout(room_id: str, timeout: float) -> None:
from WebHostLib.models import Room
from WebHostLib import app
room_uuid = app.url_map.converters["suuid"].to_python(None, room_id) # type: ignore[arg-type]
room_uuid = to_python(room_id)
with db_session:
room: Room = Room.get(id=room_uuid)
room.timeout = timeout
@@ -168,7 +170,7 @@ def get_multidata_for_room(webhost_client: "FlaskClient", room_id: str) -> bytes
from WebHostLib.models import Room
from WebHostLib import app
room_uuid = app.url_map.converters["suuid"].to_python(None, room_id) # type: ignore[arg-type]
room_uuid = to_python(room_id)
with db_session:
room: Room = Room.get(id=room_uuid)
return cast(bytes, room.seed.multidata)
@@ -180,7 +182,7 @@ def set_multidata_for_room(webhost_client: "FlaskClient", room_id: str, data: by
from WebHostLib.models import Room
from WebHostLib import app
room_uuid = app.url_map.converters["suuid"].to_python(None, room_id) # type: ignore[arg-type]
room_uuid = to_python(room_id)
with db_session:
room: Room = Room.get(id=room_uuid)
room.seed.multidata = data

View File

@@ -260,11 +260,7 @@ class HatInTimeWorld(World):
f"{item_name} ({self.multiworld.get_player_name(loc.item.player)})")
slot_data["ShopItemNames"] = shop_item_names
for name, value in self.options.as_dict(*self.options_dataclass.type_hints).items():
if name in slot_data_options:
slot_data[name] = value
slot_data.update(self.options.as_dict(*slot_data_options))
return slot_data
def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]):

View File

@@ -505,10 +505,11 @@ class ALTTPWorld(World):
def pre_fill(self):
from Fill import fill_restrictive, FillError
attempts = 5
all_state = self.multiworld.get_all_state(use_cache=False)
all_state = self.multiworld.get_all_state(perform_sweep=False)
crystals = [self.create_item(name) for name in ['Red Pendant', 'Blue Pendant', 'Green Pendant', 'Crystal 1', 'Crystal 2', 'Crystal 3', 'Crystal 4', 'Crystal 7', 'Crystal 5', 'Crystal 6']]
for crystal in crystals:
all_state.remove(crystal)
all_state.sweep_for_advancements()
crystal_locations = [self.get_location('Turtle Rock - Prize'),
self.get_location('Eastern Palace - Prize'),
self.get_location('Desert Palace - Prize'),

View File

@@ -207,11 +207,7 @@ class EnemyScaling(DefaultOnToggle):
class BlasphemousDeathLink(DeathLink):
"""
When you die, everyone dies. The reverse is also true.
Note that Guilt Fragments will not appear when killed by Death Link.
"""
__doc__ = DeathLink.__doc__ + "\n\n Note that Guilt Fragments will not appear when killed by death link."
@dataclass

View File

@@ -175,11 +175,7 @@ class DamageMultiplier(Range):
class BRCDeathLink(DeathLink):
"""
When you die, everyone dies. The reverse is also true.
This can be changed later in the options menu inside the Archipelago phone app.
"""
__doc__ = DeathLink.__doc__ + "\n\n This can be changed later in the options menu inside the Archipelago phone app."
@dataclass

View File

@@ -1,6 +1,6 @@
from dataclasses import dataclass
from Options import (OptionGroup, Choice, DefaultOnToggle, ItemsAccessibility, PerGameCommonOptions, Range, Toggle,
StartInventoryPool)
StartInventoryPool, DeathLink)
class CharacterStages(Choice):
@@ -507,12 +507,11 @@ class WindowColorA(Range):
default = 8
class DeathLink(Choice):
"""
When you die, everyone dies. Of course the reverse is true too.
Explosive: Makes received DeathLinks kill you via the Magical Nitro explosion instead of the normal death animation.
"""
display_name = "DeathLink"
class CV64DeathLink(Choice):
__doc__ = (DeathLink.__doc__ + "\n\n Explosive: Makes received death links kill you via the Magical Nitro " +
"explosion instead of the normal death animation.")
display_name = "Death Link"
option_off = 0
alias_no = 0
alias_true = 1
@@ -575,7 +574,7 @@ class CV64Options(PerGameCommonOptions):
map_lighting: MapLighting
fall_guard: FallGuard
cinematic_experience: CinematicExperience
death_link: DeathLink
death_link: CV64DeathLink
cv64_option_groups = [
@@ -584,7 +583,7 @@ cv64_option_groups = [
RenonFightCondition, VincentFightCondition, BadEndingCondition, IncreaseItemLimit, NerfHealingItems,
LoadingZoneHeals, InvisibleItems, DropPreviousSubWeapon, PermanentPowerUps, IceTrapPercentage,
IceTrapAppearance, DisableTimeRestrictions, SkipGondolas, SkipWaterwayBlocks, Countdown, BigToss, PantherDash,
IncreaseShimmySpeed, FallGuard, DeathLink
IncreaseShimmySpeed, FallGuard, CV64DeathLink
]),
OptionGroup("cosmetics", [
WindowColorR, WindowColorG, WindowColorB, WindowColorA, BackgroundMusic, MapLighting, CinematicExperience

View File

@@ -16,7 +16,7 @@ from .text import cv64_string_to_bytearray, cv64_text_truncate, cv64_text_wrap
from .aesthetics import renon_item_dialogue, get_item_text_color
from .locations import get_location_info
from .options import CharacterStages, VincentFightCondition, RenonFightCondition, PostBehemothBoss, RoomOfClocksBoss, \
BadEndingCondition, DeathLink, DraculasCondition, InvisibleItems, Countdown, PantherDash
BadEndingCondition, CV64DeathLink, DraculasCondition, InvisibleItems, Countdown, PantherDash
from settings import get_settings
if TYPE_CHECKING:
@@ -356,7 +356,7 @@ class CV64PatchExtensions(APPatchExtension):
rom_data.write_int32s(0xBFE190, patches.subweapon_surface_checker)
# Make received DeathLinks blow you to smithereens instead of kill you normally.
if options["death_link"] == DeathLink.option_explosive:
if options["death_link"] == CV64DeathLink.option_explosive:
rom_data.write_int32s(0xBFC0D0, patches.deathlink_nitro_edition)
rom_data.write_int32(0x27A70, 0x10000008) # B [forward 0x08]
rom_data.write_int32(0x27AA0, 0x0C0FFA78) # JAL 0x803FE9E0
@@ -365,7 +365,7 @@ class CV64PatchExtensions(APPatchExtension):
rom_data.write_int32(0x32DBC, 0x00000000)
# Set the DeathLink ROM flag if it's on at all.
if options["death_link"] != DeathLink.option_off:
if options["death_link"] != CV64DeathLink.option_off:
rom_data.write_byte(0xBFBFDE, 0x01)
# DeathLink counter decrementer code

View File

@@ -39,16 +39,14 @@ randomized item and (optionally) enemy locations. You only need to do this once
To run _Dark Souls III_ in Archipelago mode:
1. Start Steam. **Do not run in offline mode.** Running Steam in offline mode will make certain
scripted invaders fail to spawn. Instead, change the game itself to offline mode on the menu
screen.
1. Start Steam. **Do not run Steam in offline mode.** Running Steam in offline mode will make certain
scripted invaders fail to spawn.
2. Run `launchmod_darksouls3.bat`. This will start _Dark Souls III_ as well as a command prompt that
2. To prevent you from getting penalized, **make sure to set _Dark Souls III_ to offline mode in the game options.**
3. Run `launchmod_darksouls3.bat`. This will start _Dark Souls III_ as well as a command prompt that
you can use to interact with the Archipelago server.
3. Type `/connect {SERVER_IP}:{SERVER_PORT} {SLOT_NAME}` into the command prompt, with the
appropriate values filled in. For example: `/connect archipelago.gg:24242 PlayerName`.
4. Start playing as normal. An "Archipelago connected" message will appear onscreen once you have
control of your character and the connection is established.

View File

@@ -154,7 +154,17 @@ class HKWeb(WebWorld):
["JoaoVictor-FA"]
)
tutorials = [setup_en, setup_pt_br]
setup_es = Tutorial(
setup_en.tutorial_name,
setup_en.description,
"Español",
"setup_es.md",
"setup/es",
["GreenMarco", "Panto UwUr"]
)
tutorials = [setup_en, setup_pt_br, setup_es]
game_info_languages = ["en", "es"]
bug_report_page = "https://github.com/Ijwu/Archipelago.HollowKnight/issues/new?assignees=&labels=bug%2C+needs+investigation&template=bug_report.md&title="
@@ -218,6 +228,11 @@ class HKWorld(World):
wp = self.options.WhitePalace
if wp <= WhitePalace.option_nopathofpain:
exclusions.update(path_of_pain_locations)
exclusions.update((
"Soul_Totem-Path_of_Pain",
"Lore_Tablet-Path_of_Pain_Entrance",
"Journal_Entry-Seal_of_Binding",
))
if wp <= WhitePalace.option_kingfragment:
exclusions.update(white_palace_checks)
if wp == WhitePalace.option_exclude:
@@ -226,6 +241,9 @@ class HKWorld(World):
# If charms are randomized, this will be junk-filled -- so transitions and events are not progression
exclusions.update(white_palace_transitions)
exclusions.update(white_palace_events)
exclusions.update(item_name_groups["PalaceJournal"])
exclusions.update(item_name_groups["PalaceLore"])
exclusions.update(item_name_groups["PalaceTotem"])
return exclusions
def create_regions(self):

View File

@@ -0,0 +1,25 @@
# Hollow Knight
## ¿Dónde está la página de opciones?
La [página de opciones de jugador para este juego](../player-options) contiene todas las opciones que necesitas para
configurar y exportar un archivo de configuración.
## ¿Qué se randomiza en este juego?
El randomizer cambia la ubicación de los objetos. Los objetos que se intercambian se eligen dentro de tu YAML.
Los costes de las tiendas son aleatorios. Los objetos que podrían ser aleatorios, pero no lo son, permanecerán sin
modificar en sus ubicaciones habituales. En particular, cuando los ítems con el PadreLarva y la Vidente están
parcialmente randomizados, los ítems randomizados se obtendrán de un cofre en la habitación, mientras que los ítems no
randomizados serán dados por el NPC de forma normal.
## ¿Qué objetos de Hollow Knight pueden aparecer en los mundos de otros jugadores?
Esto depende enteramente de tus opciones YAML. Algunos ejemplos son: amuletos, larvas, capullos de saviavida, geo, etc.
## ¿Qué aspecto tienen los objetos de otro mundo en Hollow Knight?
Cuando el jugador de Hollow Knight recoja un objeto de un lugar y sea un objeto para otro juego, aparecerá en la
pantalla de objetos recientes de ese jugador como un objeto enviado a otro jugador. Si el objeto es para otro jugador
de Hollow Knight entonces el sprite será el del sprite original del objeto. Si el objeto pertenece a un jugador que no
está jugando a Hollow Knight, el sprite será el logo del Archipiélago.

View File

@@ -0,0 +1,64 @@
# Hollow Knight Archipelago
## Software requerido
* Descarga y descomprime Lumafly Mod manager desde el [sitio web de Lumafly](https://themulhima.github.io/Lumafly/)
* 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
## Instalación del mod de Archipelago con Lumafly
1. Ejecuta Lumafly y asegurate de localizar la carpeta de instalación de Hollow Knight
2. Instala el mod de Archipiélago haciendo click en cualquiera de los siguientes:
* Haz clic en uno de los enlaces de abajo para permitir Lumafly para instalar los mods. Lumafly pedirá
confirmación.
* [Archipiélago y dependencias solamente](https://themulhima.github.io/Lumafly/commands/download/?mods=Archipelago)
* [Archipelago con rando essentials](https://themulhima.github.io/Lumafly/commands/download/?mods=Archipelago/Archipelago%20Map%20Mod/RecentItemsDisplay/DebugMod/RandoStats/Additional%20Timelines/CompassAlwaysOn/AdditionalMaps/)
(incluye Archipelago Map Mod, RecentItemsDisplay, DebugMod, RandoStats, AdditionalTimelines, CompassAlwaysOn,
y AdditionalMaps).
* Haz clic en el botón "Instalar" situado junto a la entrada del mod "Archipiélago". Si lo deseas, instala también
"Archipelago Map Mod" para utilizarlo como rastreador en el juego.
Si lo requieres (Y recomiendo hacerlo) busca e instala Archipelago Map Mod para usar un tracker in-game
3. Ejecuta el juego desde el apartado de inicio haciendo click en el botón Launch with Mods
## Que hago si Lumafly no encontro la ruta de instalación de mi juego?
1. Busca el directorio manualmente
* En Xbox Game pass:
1. Entra a la Xbox App y dirigete sobre el icono de Hollow Knight que esta a la izquierda.
2. Haz click en los 3 puntitos y elige el apartado Administrar
3. Dirigete al apartado Archivos Locales y haz click en Buscar
4. Abre en Hollow Knight, luego Content y copia la ruta de archivos que esta en la barra de navegación.
* En Steam:
1. Si instalaste Hollow Knight en algún otro disco que no sea el predeterminado, ya sabrás donde se encuentra
el juego, ve a esa carpeta, abrela y copia la ruta de archivos que se encuentra en la barra de navegación.
* En Windows, la ruta predeterminada suele ser:`C:\Program Files (x86)\Steam\steamapps\common\Hollow Knight`
* En linux/Steam Deck suele ser: ~/.local/share/Steam/steamapps/common/Hollow Knight
* En Mac suele ser: ~/Library/Application Support/Steam/steamapps/common/Hollow Knight/hollow_knight.app
2. Ejecuta Lumafly como administrador y, cuando te pregunte por la ruta de instalación, pega la ruta que copeaste
anteriormente.
## Configuración de tu fichero YAML
### ¿Qué es un YAML y por qué necesito uno?
Un archivo YAML es la forma en la que proporcionas tus opciones de jugador a Archipelago.
Mira la [guía básica de configuración multiworld](/tutorial/Archipelago/setup/en) aquí en la web de Archipelago para
aprender más, (solo se encuentra en Inglés).
### ¿Dónde consigo un YAML?
Puedes usar la [página de opciones de juego para Hollow Knight](/games/Hollow%20Knight/player-options) aquí en la web
de Archipelago para generar un YAML usando una interfaz gráfica.
## Unete a una partida de Archipelago en Hollow Knight
1. Inicia el juego con los mods necesarios indicados anteriormente.
2. Crea una **nueva partida.**
3. Elige el modo **Archipelago** en la selección de modos de partida.
4. Introduce la configuración correcta para tu servidor de Archipelago.
5. Pulsa **Iniciar** para iniciar la partida. El juego se quedará con la pantalla en negro unos segundos mientras
coloca todos los objetos.
6. El juego debera comenzar y ya estaras dentro del servidor.
* Si estas esperando a que termine un contador/timer, procura presionar el boton Start cuando el contador/timer
termine.
* Otra manera es pausar el juego y esperar a que el contador/timer termine cuando ingreses a la partida.
## 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).

View File

@@ -57,13 +57,8 @@ class ExtraLogic(DefaultOnToggle):
class Hylics2DeathLink(DeathLink):
"""
When you die, everyone dies. The reverse is also true.
Note that this also includes death by using the PERISH gesture.
Can be toggled via in-game console command "/deathlink".
"""
__doc__ = (DeathLink.__doc__ + "\n\n Note that this also includes death by using the PERISH gesture." +
"\n\n Can be toggled via in-game console command \"/deathlink\".")
@dataclass

View File

@@ -287,13 +287,13 @@ class BadStartingWeapons(Toggle):
class DonaldDeathLink(Toggle):
"""
If Donald is KO'ed, so is Sora. If Death Link is toggled on in your client, this will send a death to everyone.
If Donald is KO'ed, so is Sora. If Death Link is toggled on in your client, this will send a death to everyone who enabled death link.
"""
display_name = "Donald Death Link"
class GoofyDeathLink(Toggle):
"""
If Goofy is KO'ed, so is Sora. If Death Link is toggled on in your client, this will send a death to everyone.
If Goofy is KO'ed, so is Sora. If Death Link is toggled on in your client, this will send a death to everyone who enabled death link.
"""
display_name = "Goofy Death Link"

View File

@@ -394,7 +394,7 @@ class LingoPlayerLogic:
or painting.room in required_painting_rooms:
return False
if world.options.shuffle_doors == ShuffleDoors.option_none:
if world.options.shuffle_doors != ShuffleDoors.option_doors:
if painting.req_blocked_when_no_doors:
return False

View File

@@ -121,10 +121,8 @@ class ShopPrice(Choice):
class NoitaDeathLink(DeathLink):
"""
When you die, everyone dies. Of course, the reverse is true too.
You can disable this in the in-game mod options.
"""
__doc__ = (DeathLink.__doc__ + "\n\n You can disable this or set it to give yourself a trap effect when " +
"another player dies in the in-game mod options.")
@dataclass

View File

@@ -1324,10 +1324,20 @@ class OOTWorld(World):
state.prog_items[self.player][alt_item_name] -= count
if state.prog_items[self.player][alt_item_name] < 1:
del (state.prog_items[self.player][alt_item_name])
# invalidate caches, nothing can be trusted anymore now
state.child_reachable_regions[self.player] = set()
state.child_blocked_connections[self.player] = set()
state.adult_reachable_regions[self.player] = set()
state.adult_blocked_connections[self.player] = set()
state._oot_stale[self.player] = True
return True
changed = super().remove(state, item)
if changed:
# invalidate caches, nothing can be trusted anymore now
state.child_reachable_regions[self.player] = set()
state.child_blocked_connections[self.player] = set()
state.adult_reachable_regions[self.player] = set()
state.adult_blocked_connections[self.player] = set()
state._oot_stale[self.player] = True
return changed

128
worlds/paint/__init__.py Normal file
View File

@@ -0,0 +1,128 @@
from typing import Dict, Any
from BaseClasses import CollectionState, Item, MultiWorld, Tutorial, Region
from Options import OptionError
from worlds.AutoWorld import LogicMixin, World, WebWorld
from .items import item_table, PaintItem, item_data_table, traps, deathlink_traps
from .locations import location_table, PaintLocation, location_data_table
from .options import PaintOptions
class PaintWebWorld(WebWorld):
theme = "partyTime"
setup_en = Tutorial(
tutorial_name="Start Guide",
description="A guide to playing Paint in Archipelago.",
language="English",
file_name="guide_en.md",
link="guide/en",
authors=["MarioManTAW"]
)
tutorials = [setup_en]
class PaintWorld(World):
"""
The classic Microsoft app, reimagined as an Archipelago game! Find your tools, expand your canvas, and paint the
greatest image the world has ever seen.
"""
game = "Paint"
options_dataclass = PaintOptions
options: PaintOptions
web = PaintWebWorld()
location_name_to_id = location_table
item_name_to_id = item_table
origin_region_name = "Canvas"
def generate_early(self) -> None:
if self.options.canvas_size_increment < 50 and self.options.logic_percent <= 55:
if self.multiworld.players == 1:
raise OptionError("Logic Percent must be greater than 55 when generating a single-player world with "
"Canvas Size Increment below 50.")
def get_filler_item_name(self) -> str:
if self.random.randint(0, 99) >= self.options.trap_count:
return "Additional Palette Color"
elif self.options.death_link:
return self.random.choice(deathlink_traps)
else:
return self.random.choice(traps)
def create_item(self, name: str) -> PaintItem:
item = PaintItem(name, item_data_table[name].type, item_data_table[name].code, self.player)
return item
def create_items(self) -> None:
starting_tools = ["Brush", "Pencil", "Eraser/Color Eraser", "Airbrush", "Line", "Rectangle", "Ellipse",
"Rounded Rectangle"]
self.push_precollected(self.create_item("Magnifier"))
self.push_precollected(self.create_item(starting_tools.pop(self.options.starting_tool)))
items_to_create = ["Free-Form Select", "Select", "Fill With Color", "Pick Color", "Text", "Curve", "Polygon"]
items_to_create += starting_tools
items_to_create += ["Progressive Canvas Width"] * (400 // self.options.canvas_size_increment)
items_to_create += ["Progressive Canvas Height"] * (300 // self.options.canvas_size_increment)
depth_items = ["Progressive Color Depth (Red)", "Progressive Color Depth (Green)",
"Progressive Color Depth (Blue)"]
for item in depth_items:
self.push_precollected(self.create_item(item))
items_to_create += depth_items * 6
pre_filled = len(items_to_create)
to_fill = len(self.get_region("Canvas").locations)
if pre_filled > to_fill:
raise OptionError(f"{self.player_name}'s Paint world has too few locations for its required items. "
"Consider adding more locations by raising logic percent or adding fractional checks. "
"Alternatively, increasing the canvas size increment will require fewer items.")
while len(items_to_create) < (to_fill - pre_filled) * (self.options.trap_count / 100) + pre_filled:
if self.options.death_link:
items_to_create += [self.random.choice(deathlink_traps)]
else:
items_to_create += [self.random.choice(traps)]
while len(items_to_create) < to_fill:
items_to_create += ["Additional Palette Color"]
self.multiworld.itempool += [self.create_item(item) for item in items_to_create]
def create_regions(self) -> None:
canvas = Region("Canvas", self.player, self.multiworld)
canvas.locations += [PaintLocation(self.player, loc_name, loc_data.address, canvas)
for loc_name, loc_data in location_data_table.items()
if location_exists_with_options(self, loc_data.address)]
self.multiworld.regions += [canvas]
def set_rules(self) -> None:
from .rules import set_completion_rules
set_completion_rules(self, self.player)
def fill_slot_data(self) -> Dict[str, Any]:
return dict(self.options.as_dict("logic_percent", "goal_percent", "goal_image", "death_link",
"canvas_size_increment"), version="0.5.2")
def collect(self, state: CollectionState, item: Item) -> bool:
change = super().collect(state, item)
if change:
state.paint_percent_stale[self.player] = True
return change
def remove(self, state: CollectionState, item: Item) -> bool:
change = super().remove(state, item)
if change:
state.paint_percent_stale[self.player] = True
return change
def location_exists_with_options(world: PaintWorld, location: int):
l = location % 198600
return l <= world.options.logic_percent * 4 and (l % 4 == 0 or
(l > world.options.half_percent_checks * 4 and l % 2 == 0) or
l > world.options.quarter_percent_checks * 4)
class PaintState(LogicMixin):
paint_percent_available: dict[int, float] # per player
paint_percent_stale: dict[int, bool]
def init_mixin(self, multiworld: MultiWorld) -> None:
self.paint_percent_available = {player: 0 for player in multiworld.get_game_players("Paint")}
self.paint_percent_stale = {player: True for player in multiworld.get_game_players("Paint")}

View File

@@ -0,0 +1,35 @@
# Paint
## Where is the options page?
You can read through all the options and generate a YAML [here](../player-options).
## What does randomization do to this game?
Most tools are locked from the start, leaving only the Magnifier and one drawing tool, specified in the game options.
Canvas size is locked and will only expand when the Progressive Canvas Width and Progressive Canvas Height items are
obtained. Additionally, color selection is limited, starting with only a few possible colors but gaining more options
when Progressive Color Depth items are obtained in each of the red, green, and blue components.
Location checks are sent out based on similarity to a target image, measured as a percentage. Every percentage point up
to a maximum set in the game options will send a new check, and the game will be considered done when a certain target
percentage (also set in the game options) is reached.
## What other changes are made to the game?
This project is based on [JS Paint](https://jspaint.app), an open-source remake of Microsoft Paint. Most features will
work similarly to this version but some features have also been removed. Most notably, pasting functionality has been
completely removed to prevent cheating.
With the addition of a second canvas to display the target image, there are some additional features that may not be
intuitive. There are two special functions in the Extras menu to help visualize how to improve your score. Similarity
Mode (shortcut Ctrl+Shift+M) shows the similarity of each portion of the image in grayscale, with white representing
perfect similarity and black representing no similarity. Conversely, Difference Mode (shortcut Ctrl+M) visualizes the
differences between what has been drawn and the target image in full color, showing the direction both hue and
lightness need to shift to match the target. Additionally, once unlocked, the Pick Color tool can be used on both the
main and target canvases.
Custom colors have been streamlined for Archipelago play. The only starting palette options are black and white, but
additional palette slots can be unlocked as Archipelago items. Double-clicking on any palette slot will allow you to
edit the color in that slot directly and shift-clicking a palette slot will allow you to override the slot with your
currently selected color.

View File

@@ -0,0 +1,8 @@
# Paint Randomizer Start Guide
After rolling your seed, go to the [Archipelago Paint](https://mariomantaw.github.io/jspaint/) site and enter the
server details, your slot name, and a room password if one is required. Then click "Connect". If desired, you may then
load a custom target image with File->Open Goal Image. If playing asynchronously, note that progress is saved using the
hash that will appear at the end of the URL so it is recommended to leave the tab open or save the URL with the hash to
avoid losing progress.

48
worlds/paint/items.py Normal file
View File

@@ -0,0 +1,48 @@
from typing import NamedTuple, Dict
from BaseClasses import Item, ItemClassification
class PaintItem(Item):
game = "Paint"
class PaintItemData(NamedTuple):
code: int
type: ItemClassification
item_data_table: Dict[str, PaintItemData] = {
"Progressive Canvas Width": PaintItemData(198501, ItemClassification.progression),
"Progressive Canvas Height": PaintItemData(198502, ItemClassification.progression),
"Progressive Color Depth (Red)": PaintItemData(198503, ItemClassification.progression),
"Progressive Color Depth (Green)": PaintItemData(198504, ItemClassification.progression),
"Progressive Color Depth (Blue)": PaintItemData(198505, ItemClassification.progression),
"Free-Form Select": PaintItemData(198506, ItemClassification.useful),
"Select": PaintItemData(198507, ItemClassification.useful),
"Eraser/Color Eraser": PaintItemData(198508, ItemClassification.useful),
"Fill With Color": PaintItemData(198509, ItemClassification.useful),
"Pick Color": PaintItemData(198510, ItemClassification.progression),
"Magnifier": PaintItemData(198511, ItemClassification.useful),
"Pencil": PaintItemData(198512, ItemClassification.useful),
"Brush": PaintItemData(198513, ItemClassification.useful),
"Airbrush": PaintItemData(198514, ItemClassification.useful),
"Text": PaintItemData(198515, ItemClassification.useful),
"Line": PaintItemData(198516, ItemClassification.useful),
"Curve": PaintItemData(198517, ItemClassification.useful),
"Rectangle": PaintItemData(198518, ItemClassification.useful),
"Polygon": PaintItemData(198519, ItemClassification.useful),
"Ellipse": PaintItemData(198520, ItemClassification.useful),
"Rounded Rectangle": PaintItemData(198521, ItemClassification.useful),
# "Change Background Color": PaintItemData(198522, ItemClassification.useful),
"Additional Palette Color": PaintItemData(198523, ItemClassification.filler),
"Undo Trap": PaintItemData(198524, ItemClassification.trap),
"Clear Image Trap": PaintItemData(198525, ItemClassification.trap),
"Invert Colors Trap": PaintItemData(198526, ItemClassification.trap),
"Flip Horizontal Trap": PaintItemData(198527, ItemClassification.trap),
"Flip Vertical Trap": PaintItemData(198528, ItemClassification.trap),
}
item_table = {name: data.code for name, data in item_data_table.items()}
traps = ["Undo Trap", "Clear Image Trap", "Invert Colors Trap", "Flip Horizontal Trap", "Flip Vertical Trap"]
deathlink_traps = ["Invert Colors Trap", "Flip Horizontal Trap", "Flip Vertical Trap"]

24
worlds/paint/locations.py Normal file
View File

@@ -0,0 +1,24 @@
from typing import NamedTuple, Dict
from BaseClasses import CollectionState, Location
class PaintLocation(Location):
game = "Paint"
def access_rule(self, state: CollectionState):
from .rules import paint_percent_available
return paint_percent_available(state, state.multiworld.worlds[self.player], self.player) >=\
(self.address % 198600) / 4
class PaintLocationData(NamedTuple):
region: str
address: int
location_data_table: Dict[str, PaintLocationData] = {
# f"Similarity: {i}%": PaintLocationData("Canvas", 198500 + i) for i in range(1, 96)
f"Similarity: {i/4}%": PaintLocationData("Canvas", 198600 + i) for i in range(1, 381)
}
location_table = {name: data.address for name, data in location_data_table.items()}

107
worlds/paint/options.py Normal file
View File

@@ -0,0 +1,107 @@
from dataclasses import dataclass
from Options import Range, PerGameCommonOptions, StartInventoryPool, Toggle, Choice, Visibility
class LogicPercent(Range):
"""Sets the maximum percent similarity required for a check to be in logic.
Higher values are more difficult and items/locations will not be generated beyond this number."""
display_name = "Logic Percent"
range_start = 50
range_end = 95
default = 80
class GoalPercent(Range):
"""Sets the percent similarity required to achieve your goal.
If this number is higher than the value for logic percent,
reaching goal will be in logic upon obtaining all progression items."""
display_name = "Goal Percent"
range_start = 50
range_end = 95
default = 80
class HalfPercentChecks(Range):
"""Sets the lowest percent at which locations will be created for each 0.5% of similarity.
Below this number, there will be a check every 1%.
Above this number, there will be a check every 0.5%."""
display_name = "Half Percent Checks"
range_start = 0
range_end = 95
default = 50
class QuarterPercentChecks(Range):
"""Sets the lowest percent at which locations will be created for each 0.25% of similarity.
This number will override Half Percent Checks if it is lower."""
display_name = "Quarter Percent Checks"
range_start = 0
range_end = 95
default = 70
class CanvasSizeIncrement(Choice):
"""Sets the number of pixels the canvas will expand for each width/height item received.
Ensure an adequate number of locations are generated if setting this below 50."""
display_name = "Canvas Size Increment"
# option_10 = 10
# option_20 = 20
option_25 = 25
option_50 = 50
option_100 = 100
default = 100
class GoalImage(Range):
"""Sets the numbered image you will be required to match.
See https://github.com/MarioManTAW/jspaint/tree/master/images/archipelago
for a list of possible images or choose random.
This can also be overwritten client-side by using File->Open."""
display_name = "Goal Image"
range_start = 1
range_end = 1
default = 1
visibility = Visibility.none
class StartingTool(Choice):
"""Sets which tool (other than Magnifier) you will be able to use from the start."""
option_brush = 0
option_pencil = 1
option_eraser = 2
option_airbrush = 3
option_line = 4
option_rectangle = 5
option_ellipse = 6
option_rounded_rectangle = 7
default = 0
class TrapCount(Range):
"""Sets the percentage of filler items to be replaced by random traps."""
display_name = "Trap Fill Percent"
range_start = 0
range_end = 100
default = 0
class DeathLink(Toggle):
"""If on, using the Undo or Clear Image functions will send a death to all other players with death link on.
Receiving a death will clear the image and reset the history.
This option also prevents Undo and Clear Image traps from being generated in the item pool."""
display_name = "Death Link"
@dataclass
class PaintOptions(PerGameCommonOptions):
logic_percent: LogicPercent
goal_percent: GoalPercent
half_percent_checks: HalfPercentChecks
quarter_percent_checks: QuarterPercentChecks
canvas_size_increment: CanvasSizeIncrement
goal_image: GoalImage
starting_tool: StartingTool
trap_count: TrapCount
death_link: DeathLink
start_inventory_from_pool: StartInventoryPool

40
worlds/paint/rules.py Normal file
View File

@@ -0,0 +1,40 @@
from math import sqrt
from BaseClasses import CollectionState
from . import PaintWorld
def paint_percent_available(state: CollectionState, world: PaintWorld, player: int) -> bool:
if state.paint_percent_stale[player]:
state.paint_percent_available[player] = calculate_paint_percent_available(state, world, player)
state.paint_percent_stale[player] = False
return state.paint_percent_available[player]
def calculate_paint_percent_available(state: CollectionState, world: PaintWorld, player: int) -> float:
p = state.has("Pick Color", player)
r = min(state.count("Progressive Color Depth (Red)", player), 7)
g = min(state.count("Progressive Color Depth (Green)", player), 7)
b = min(state.count("Progressive Color Depth (Blue)", player), 7)
if not p:
r = min(r, 2)
g = min(g, 2)
b = min(b, 2)
w = state.count("Progressive Canvas Width", player)
h = state.count("Progressive Canvas Height", player)
# This code looks a little messy but it's a mathematical formula derived from the similarity calculations in the
# client. The first line calculates the maximum score achievable for a single pixel with the current items in the
# worst possible case. This per-pixel score is then multiplied by the number of pixels currently available (the
# starting canvas is 400x300) over the total number of pixels with everything unlocked (800x600) to get the
# total score achievable assuming the worst possible target image. Finally, this is multiplied by the logic percent
# option which restricts the logic so as to not require pixel perfection.
return ((1 - ((sqrt(((2 ** (7 - r) - 1) ** 2 + (2 ** (7 - g) - 1) ** 2 + (2 ** (7 - b) - 1) ** 2) * 12)) / 765)) *
min(400 + w * world.options.canvas_size_increment, 800) *
min(300 + h * world.options.canvas_size_increment, 600) *
world.options.logic_percent / 480000)
def set_completion_rules(world: PaintWorld, player: int) -> None:
world.multiworld.completion_condition[player] = \
lambda state: (paint_percent_available(state, world, player) >=
min(world.options.logic_percent, world.options.goal_percent))

View File

@@ -93,7 +93,7 @@ class RaftWorld(World):
dupeItemPool = list(dupeItemPool)
# Finally, add items as necessary
for item in dupeItemPool:
self.extraItemNamePool.append(self.replace_item_name_as_necessary(item))
self.extraItemNamePool.append(self.replace_item_name_as_necessary(item["name"]))
assert self.extraItemNamePool, f"Don't know what extra items to create for {self.player_name}."

View File

@@ -1,5 +1,5 @@
import typing
from Options import Choice, PerGameCommonOptions, Range, OptionDict, OptionList, OptionSet, Option, Toggle, DefaultOnToggle
from Options import Choice, PerGameCommonOptions, Range, OptionDict, OptionList, OptionSet, OptionGroup, Toggle, DefaultOnToggle
from .variaRandomizer.utils.objectives import _goals
from dataclasses import dataclass
@@ -8,8 +8,15 @@ class StartItemsRemovesFromPool(Toggle):
display_name = "StartItems Removes From Item Pool"
class Preset(Choice):
"""Choose one of the presets or specify "varia_custom" to use varia_custom_preset option or specify "custom" to use
custom_preset option."""
"""Determines the general difficulty of the item placements by adjusting the list of tricks that logic allows.
- Newbie: New to randomizers, but completed Super Metroid 100% and knows basic techniques (Wall Jump, Shinespark, Mid-air Morph)
- Casual: Occasional rando player. No hell runs or suitless Maridia, some easy to learn tricks in logic.
- Regular: Plays rando regularly. Knows many tricks that open up the game.
- Veteran: Experienced rando player. Harder everything, some tougher tricks in logic.
- Expert: Knows almost all tricks: full suitless Maridia, Lower Norfair hell runs, etc.
- Master: Everything on hardest, all tricks known.
In-depth details on each preset can be found on the VARIA website: https://varia.run/presets
You may also specify "varia_custom" to use varia_custom_preset option, or specify "custom" to use custom_preset option."""
display_name = "Preset"
option_newbie = 0
option_casual = 1
@@ -46,7 +53,8 @@ class StartLocation(Choice):
default = 1
class DeathLink(Choice):
"""When DeathLink is enabled and someone dies, you will die. With survive reserve tanks can save you."""
"""When DeathLink is enabled and someone else with DeathLink dies, you will die.
If "Enable Survive" is selected, reserve tanks can save you."""
display_name = "Death Link"
option_disable = 0
option_enable = 1
@@ -56,11 +64,13 @@ class DeathLink(Choice):
default = 0
class RemoteItems(Toggle):
"""Indicates you get items sent from your own world. This allows coop play of a world."""
display_name = "Remote Items"
"""Items from your own world are sent via the Archipelago server. This allows co-op play of a world and means that
you will not lose items on death or save file loss."""
display_name = "Remote Items"
class MaxDifficulty(Choice):
"""Depending on the perceived difficulties of the techniques, bosses, hell runs etc. from the preset, it will
"""Maximum difficulty of tricks that are allowed from the seed's Preset.
Depending on the perceived difficulties of the techniques, bosses, hell runs etc. from the preset, it will
prevent the Randomizer from placing an item in a location too difficult to reach with the current items."""
display_name = "Maximum Difficulty"
option_easy = 0
@@ -73,7 +83,7 @@ class MaxDifficulty(Choice):
default = 4
class MorphPlacement(Choice):
"""Influences where the Morphing Ball with be placed."""
"""Influences where the Morphing Ball will be placed."""
display_name = "Morph Placement"
option_early = 0
option_normal = 1
@@ -85,21 +95,21 @@ class StrictMinors(Toggle):
display_name = "Strict Minors"
class MissileQty(Range):
"""The higher the number the higher the probability of choosing missles when placing a minor."""
"""The higher the number, the higher the probability of choosing Missiles when placing a minor."""
display_name = "Missile Quantity"
range_start = 10
range_end = 90
default = 30
class SuperQty(Range):
"""The higher the number the higher the probability of choosing super missles when placing a minor."""
"""The higher the number, the higher the probability of choosing Super Missiles when placing a minor."""
display_name = "Super Quantity"
range_start = 10
range_end = 90
default = 20
class PowerBombQty(Range):
"""The higher the number the higher the probability of choosing power bombs when placing a minor."""
"""The higher the number, the higher the probability of choosing Power Bombs when placing a minor."""
display_name = "Power Bomb Quantity"
range_start = 10
range_end = 90
@@ -123,7 +133,13 @@ class EnergyQty(Choice):
default = 3
class AreaRandomization(Choice):
"""Randomize areas together using bidirectional access portals."""
"""Randomize areas together using bidirectional access portals.
- Off: No change. All rooms are connected the same as in the original game.
- Full: All doors connecting areas will be randomized. "Areas" are roughly determined, but generally are regions
with different tilesets or music. For example, red Brinstar and green/pink Brinstar are different areas, Crocomire
and upper Norfair are different areas, etc.
- Light: Keep the same number of transitions between areas as in vanilla. So Crocomire area will always be connected
to upper Norfair, there'll always be two transitions between Crateria/blue Brinstar and green/pink Brinstar, etc."""
display_name = "Area Randomization"
option_off = 0
option_light = 1
@@ -136,13 +152,13 @@ class AreaLayout(Toggle):
display_name = "Area Layout"
class DoorsColorsRando(Toggle):
"""Randomize the color of Red/Green/Yellow doors. Add four new type of doors which require Ice/Wave/Spazer/Plasma
beams to open them."""
"""Randomize the color of Red/Green/Yellow doors. Add four new types of doors which require Ice/Wave/Spazer/Plasma
Beams to open them."""
display_name = "Doors Colors Rando"
class AllowGreyDoors(Toggle):
"""When randomizing the color of Red/Green/Yellow doors, some doors can be randomized to Grey. Grey doors will never
open, you will have to go around them."""
open; you will have to go around them."""
display_name = "Allow Grey Doors"
class BossRandomization(Toggle):
@@ -169,7 +185,10 @@ class LayoutPatches(DefaultOnToggle):
display_name = "Layout Patches"
class VariaTweaks(Toggle):
"""Include minor tweaks for the game to behave 'as it should' in a randomizer context"""
"""Include minor tweaks for the game to behave 'as it should' in a randomizer context:
- Bomb Torizo always activates after picking up its item and does not require Bomb to activate
- Wrecked Ship item on the Energy Tank Chozo statue is present before defeating Phantoon
- Lower Norfair Chozo statue that lowers the acid toward Gold Torizo does not require Space Jump to activate"""
display_name = "Varia Tweaks"
class NerfedCharge(Toggle):
@@ -179,7 +198,12 @@ class NerfedCharge(Toggle):
display_name = "Nerfed Charge"
class GravityBehaviour(Choice):
"""Modify the heat damage and enemy damage reduction qualities of the Gravity and Varia Suits."""
"""Modify the heat damage and enemy damage reduction qualities of the Gravity and Varia Suits.
- Vanilla: Gravity provides full protection against all environmental damage (heat, spikes, etc.)
- Balanced: Removes Gravity environmental protection. Doubles Varia environmental protection. Enemy damage protection
is vanilla (50% Varia, 75% Gravity).
- Progressive: Gravity provides 50% heat reduction, Varia provides full heat reduction. Each suit adds 50% enemy
and environmental reduction, stacking to 75% reduction if you have both."""
display_name = "Gravity Behaviour"
option_Vanilla = 0
option_Balanced = 1
@@ -233,7 +257,7 @@ class RandomMusic(Toggle):
class CustomPreset(OptionDict):
"""
see https://randommetroidsolver.pythonanywhere.com/presets for detailed info on each preset settings
see https://varia.run/presets for detailed info on each preset settings
knows: each skill (know) has a pair [can use, perceived difficulty using one of 1, 5, 10, 25, 50 or 100 each one
matching a max_difficulty]
settings: hard rooms, hellruns and bosses settings
@@ -246,7 +270,7 @@ class CustomPreset(OptionDict):
}
class VariaCustomPreset(OptionList):
"""use an entry from the preset list on https://randommetroidsolver.pythonanywhere.com/presets"""
"""use an entry from the preset list on https://varia.run/presets"""
display_name = "Varia Custom Preset"
default = {}
@@ -259,7 +283,7 @@ class EscapeRando(Toggle):
During the escape sequence:
- All doors are opened
- Maridia tube is opened
- The Hyper Beam can destroy Bomb , Power Bomb and Super Missile blocks and open blue/green gates from both sides
- The Hyper Beam can destroy Bomb, Power Bomb and Super Missile blocks and open blue/green gates from both sides
- All mini bosses are defeated
- All minor enemies are removed to allow you to move faster and remove lag
@@ -281,9 +305,9 @@ class RemoveEscapeEnemies(Toggle):
class Tourian(Choice):
"""
Choose endgame Tourian behaviour:
Vanilla: regular vanilla Tourian
Fast: speed up Tourian to skip Metroids, Zebetites, and all cutscenes (including Mother Brain 3 fight). Golden Four statues are replaced by an invincible Gadora until all objectives are completed.
Disabled: skip Tourian entirely, ie. escape sequence is triggered as soon as all objectives are completed.
- Vanilla: regular vanilla Tourian
- Fast: speed up Tourian to skip Metroids, Zebetites, and all cutscenes (including Mother Brain 3 fight). Golden Four statues are replaced by an invincible Gadora until all objectives are completed.
- Disabled: skip Tourian entirely; the escape sequence is triggered as soon as all objectives are completed.
"""
display_name = "Endgame behavior with Tourian"
option_Vanilla = 0
@@ -373,10 +397,71 @@ class RelaxedRoundRobinCF(Toggle):
"""
display_name = "Relaxed round robin Crystal Flash"
sm_option_groups = [
OptionGroup("Logic", [
Preset,
MaxDifficulty,
StartLocation,
VariaCustomPreset,
CustomPreset,
]),
OptionGroup("Objectives and Endgame", [
Objective,
CustomObjective,
CustomObjectiveCount,
CustomObjectiveList,
Tourian,
EscapeRando,
RemoveEscapeEnemies,
Animals,
]),
OptionGroup("Areas and Layout", [
AreaRandomization,
AreaLayout,
DoorsColorsRando,
AllowGreyDoors,
BossRandomization,
LayoutPatches,
]),
OptionGroup("Item Pool", [
MorphPlacement,
StrictMinors,
MissileQty,
SuperQty,
PowerBombQty,
MinorQty,
EnergyQty,
FunCombat,
FunMovement,
FunSuits,
]),
OptionGroup("Misc Tweaks", [
VariaTweaks,
GravityBehaviour,
NerfedCharge,
SpinJumpRestart,
SpeedKeep,
InfiniteSpaceJump,
RelaxedRoundRobinCF,
]),
OptionGroup("Quality of Life", [
ElevatorsSpeed,
DoorsSpeed,
RefillBeforeSave,
]),
OptionGroup("Cosmetic", [
Hud,
HideItems,
NoMusic,
RandomMusic,
]),
]
@dataclass
class SMOptions(PerGameCommonOptions):
start_inventory_removes_from_pool: StartItemsRemovesFromPool
preset: Preset
max_difficulty: MaxDifficulty
start_location: StartLocation
remote_items: RemoteItems
death_link: DeathLink
@@ -384,7 +469,6 @@ class SMOptions(PerGameCommonOptions):
#scav_num_locs: "10"
#scav_randomized: "off"
#scav_escape: "off"
max_difficulty: MaxDifficulty
#progression_speed": "medium"
#progression_difficulty": "normal"
morph_placement: MorphPlacement

View File

@@ -15,7 +15,7 @@ from worlds.generic.Rules import add_rule, set_rule
logger = logging.getLogger("Super Metroid")
from .Options import SMOptions
from .Options import SMOptions, sm_option_groups
from .Client import SMSNIClient
from .Rom import SM_ROM_MAX_PLAYERID, SM_ROM_PLAYERDATA_COUNT, SMProcedurePatch, get_sm_symbols
import Utils
@@ -78,6 +78,7 @@ class SMWeb(WebWorld):
"multiworld/en",
["Farrak Kilhn"]
)]
option_groups = sm_option_groups
class ByteEdit(TypedDict):
@@ -852,7 +853,7 @@ class SMWorld(World):
def fill_slot_data(self):
slot_data = {}
if not self.multiworld.is_race:
slot_data = self.options.as_dict(*self.options_dataclass.type_hints)
slot_data = self.options.as_dict("start_location", "max_difficulty", "area_randomization", "doors_colors_rando", "boss_randomization")
slot_data["Preset"] = { "Knows": {},
"Settings": {"hardRooms": Settings.SettingsDict[self.player].hardRooms,
"bossesDifficulty": Settings.SettingsDict[self.player].bossesDifficulty,

View File

@@ -500,7 +500,14 @@ class SMZ3World(World):
multidata["connect_names"][new_name] = payload
def fill_slot_data(self):
slot_data = {}
slot_data = {
"goal": self.options.goal.value,
"open_tower": self.options.open_tower.value,
"ganon_vulnerable": self.options.ganon_vulnerable.value,
"open_tourian": self.options.open_tourian.value,
"sm_logic": self.options.sm_logic.value,
"key_shuffle": self.options.key_shuffle.value,
}
return slot_data
def collect(self, state: CollectionState, item: Item) -> bool:

View File

@@ -50,15 +50,25 @@ class StardewWebWorld(WebWorld):
options_presets = sv_options_presets
option_groups = sv_option_groups
tutorials = [
Tutorial(
"Multiworld Setup Guide",
"A guide to playing Stardew Valley with Archipelago.",
"English",
"setup_en.md",
"setup/en",
["KaitoKid", "Jouramie", "Witchybun (Mod Support)", "Exempt-Medic (Proofreading)"]
)]
setup_en = Tutorial(
"Multiworld Setup Guide",
"A guide to playing Stardew Valley with Archipelago.",
"English",
"setup_en.md",
"setup/en",
["KaitoKid", "Jouramie", "Witchybun (Mod Support)", "Exempt-Medic (Proofreading)"]
)
setup_fr = Tutorial(
"Guide de configuration MultiWorld",
"Un guide pour configurer Stardew Valley sur Archipelago",
"Français",
"setup_fr.md",
"setup/fr",
["Eindall"]
)
tutorials = [setup_en, setup_fr]
class StardewValleyWorld(World):

View File

@@ -2316,100 +2316,100 @@ id,region,name,tags,mod_name
4069,Museum,Read Note From Gunther,"BOOKSANITY,BOOKSANITY_LOST",
4070,Museum,Read Goblins by M. Jasper,"BOOKSANITY,BOOKSANITY_LOST",
4071,Museum,Read Secret Statues Acrostics,"BOOKSANITY,BOOKSANITY_LOST",
4101,Clint's Blacksmith,Open Golden Coconut,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4102,Island West,Fishing Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4103,Island West,Fishing Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4104,Island North,Fishing Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4105,Island North,Fishing Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4106,Island Southeast,Fishing Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4107,Island East,Jungle Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
4108,Island East,Banana Altar,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4109,Leo's Hut,Leo's Tree,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4110,Island Shrine,Gem Birds Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
4111,Island Shrine,Gem Birds Shrine,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4112,Island West,Harvesting Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4113,Island West,Harvesting Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4114,Island West,Harvesting Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4115,Island West,Harvesting Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4116,Island West,Harvesting Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4117,Gourmand Frog Cave,Gourmand Frog Melon,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4118,Gourmand Frog Cave,Gourmand Frog Wheat,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4119,Gourmand Frog Cave,Gourmand Frog Garlic,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4120,Island West,Journal Scrap #6,"WALNUTSANITY,WALNUTSANITY_DIG",
4121,Island West,Mussel Node Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4122,Island West,Mussel Node Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4123,Island West,Mussel Node Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4124,Island West,Mussel Node Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4125,Island West,Mussel Node Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4126,Shipwreck,Shipwreck Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
4127,Island West,Whack A Mole,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4128,Island West,Starfish Triangle,"WALNUTSANITY,WALNUTSANITY_DIG",
4129,Island West,Starfish Diamond,"WALNUTSANITY,WALNUTSANITY_DIG",
4130,Island West,X in the sand,"WALNUTSANITY,WALNUTSANITY_DIG",
4131,Island West,Diamond Of Indents,"WALNUTSANITY,WALNUTSANITY_DIG",
4132,Island West,Bush Behind Coconut Tree,"WALNUTSANITY,WALNUTSANITY_BUSH",
4133,Island West,Journal Scrap #4,"WALNUTSANITY,WALNUTSANITY_DIG",
4134,Island West,Walnut Room Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
4135,Island West,Coast Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
4136,Island West,Tiger Slime Walnut,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4137,Island West,Bush Behind Mahogany Tree,"WALNUTSANITY,WALNUTSANITY_BUSH",
4138,Island West,Circle Of Grass,"WALNUTSANITY,WALNUTSANITY_DIG",
4139,Island West,Below Colored Crystals Cave Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
4140,Colored Crystals Cave,Colored Crystals,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4141,Island West,Cliff Edge Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
4142,Island West,Diamond Of Pebbles,"WALNUTSANITY,WALNUTSANITY_DIG",
4143,Island West,Farm Parrot Express Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
4144,Island West,Farmhouse Cliff Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
4145,Island North,Big Circle Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG",
4146,Island North,Grove Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
4147,Island North,Diamond Of Grass,"WALNUTSANITY,WALNUTSANITY_DIG",
4148,Island North,Small Circle Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG",
4149,Island North,Patch Of Sand,"WALNUTSANITY,WALNUTSANITY_DIG",
4150,Dig Site,Crooked Circle Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG",
4151,Dig Site,Above Dig Site Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
4152,Dig Site,Above Field Office Bush 1,"WALNUTSANITY,WALNUTSANITY_BUSH",
4153,Dig Site,Above Field Office Bush 2,"WALNUTSANITY,WALNUTSANITY_BUSH",
4154,Field Office,Complete Large Animal Collection,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4155,Field Office,Complete Snake Collection,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4156,Field Office,Complete Mummified Frog Collection,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4157,Field Office,Complete Mummified Bat Collection,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4158,Field Office,Purple Flowers Island Survey,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4159,Field Office,Purple Starfish Island Survey,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4160,Island North,Bush Behind Volcano Tree,"WALNUTSANITY,WALNUTSANITY_BUSH",
4161,Island North,Arc Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG",
4162,Island North,Protruding Tree Walnut,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4163,Island North,Journal Scrap #10,"WALNUTSANITY,WALNUTSANITY_DIG",
4164,Island North,Northmost Point Circle Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG",
4165,Island North,Hidden Passage Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
4166,Volcano Secret Beach,Secret Beach Bush 1,"WALNUTSANITY,WALNUTSANITY_BUSH",
4167,Volcano Secret Beach,Secret Beach Bush 2,"WALNUTSANITY,WALNUTSANITY_BUSH",
4168,Volcano - Floor 5,Volcano Rocks Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4169,Volcano - Floor 5,Volcano Rocks Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4170,Volcano - Floor 10,Volcano Rocks Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4171,Volcano - Floor 10,Volcano Rocks Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4172,Volcano - Floor 10,Volcano Rocks Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4173,Volcano - Floor 5,Volcano Monsters Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4174,Volcano - Floor 5,Volcano Monsters Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4175,Volcano - Floor 10,Volcano Monsters Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4176,Volcano - Floor 10,Volcano Monsters Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4177,Volcano - Floor 10,Volcano Monsters Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4178,Volcano - Floor 5,Volcano Crates Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4179,Volcano - Floor 5,Volcano Crates Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4180,Volcano - Floor 10,Volcano Crates Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4181,Volcano - Floor 10,Volcano Crates Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4182,Volcano - Floor 10,Volcano Crates Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4183,Volcano - Floor 5,Volcano Common Chest Walnut,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4184,Volcano - Floor 10,Volcano Rare Chest Walnut,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4185,Volcano - Floor 10,Forge Entrance Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
4186,Volcano - Floor 10,Forge Exit Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
4187,Island North,Cliff Over Island South Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
4188,Island Southeast,Starfish Tide Pool,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4189,Island Southeast,Diamond Of Yellow Starfish,"WALNUTSANITY,WALNUTSANITY_DIG",
4190,Island Southeast,Mermaid Song,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4191,Pirate Cove,Pirate Darts 1,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4192,Pirate Cove,Pirate Darts 2,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4193,Pirate Cove,Pirate Darts 3,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4194,Pirate Cove,Pirate Cove Patch Of Sand,"WALNUTSANITY,WALNUTSANITY_DIG",
4101,Clint's Blacksmith,Walnutsanity: Open Golden Coconut,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4102,Island West,Walnutsanity: Fishing Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4103,Island West,Walnutsanity: Fishing Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4104,Island North,Walnutsanity: Fishing Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4105,Island North,Walnutsanity: Fishing Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4106,Island Southeast,Walnutsanity: Fishing Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4107,Island East,Walnutsanity: Jungle Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
4108,Island East,Walnutsanity: Banana Altar,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4109,Leo's Hut,Walnutsanity: Leo's Tree,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4110,Island Shrine,Walnutsanity: Gem Birds Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
4111,Island Shrine,Walnutsanity: Gem Birds Shrine,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4112,Island West,Walnutsanity: Harvesting Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4113,Island West,Walnutsanity: Harvesting Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4114,Island West,Walnutsanity: Harvesting Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4115,Island West,Walnutsanity: Harvesting Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4116,Island West,Walnutsanity: Harvesting Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4117,Gourmand Frog Cave,Walnutsanity: Gourmand Frog Melon,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4118,Gourmand Frog Cave,Walnutsanity: Gourmand Frog Wheat,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4119,Gourmand Frog Cave,Walnutsanity: Gourmand Frog Garlic,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4120,Island West,Walnutsanity: Journal Scrap #6,"WALNUTSANITY,WALNUTSANITY_DIG",
4121,Island West,Walnutsanity: Mussel Node Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4122,Island West,Walnutsanity: Mussel Node Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4123,Island West,Walnutsanity: Mussel Node Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4124,Island West,Walnutsanity: Mussel Node Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4125,Island West,Walnutsanity: Mussel Node Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4126,Shipwreck,Walnutsanity: Shipwreck Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
4127,Island West,Walnutsanity: Whack A Mole,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4128,Island West,Walnutsanity: Starfish Triangle,"WALNUTSANITY,WALNUTSANITY_DIG",
4129,Island West,Walnutsanity: Starfish Diamond,"WALNUTSANITY,WALNUTSANITY_DIG",
4130,Island West,Walnutsanity: X in the sand,"WALNUTSANITY,WALNUTSANITY_DIG",
4131,Island West,Walnutsanity: Diamond Of Indents,"WALNUTSANITY,WALNUTSANITY_DIG",
4132,Island West,Walnutsanity: Bush Behind Coconut Tree,"WALNUTSANITY,WALNUTSANITY_BUSH",
4133,Island West,Walnutsanity: Journal Scrap #4,"WALNUTSANITY,WALNUTSANITY_DIG",
4134,Island West,Walnutsanity: Walnut Room Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
4135,Island West,Walnutsanity: Coast Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
4136,Island West,Walnutsanity: Tiger Slime Walnut,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4137,Island West,Walnutsanity: Bush Behind Mahogany Tree,"WALNUTSANITY,WALNUTSANITY_BUSH",
4138,Island West,Walnutsanity: Circle Of Grass,"WALNUTSANITY,WALNUTSANITY_DIG",
4139,Island West,Walnutsanity: Below Colored Crystals Cave Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
4140,Colored Crystals Cave,Walnutsanity: Colored Crystals,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4141,Island West,Walnutsanity: Cliff Edge Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
4142,Island West,Walnutsanity: Diamond Of Pebbles,"WALNUTSANITY,WALNUTSANITY_DIG",
4143,Island West,Walnutsanity: Farm Parrot Express Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
4144,Island West,Walnutsanity: Farmhouse Cliff Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
4145,Island North,Walnutsanity: Big Circle Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG",
4146,Island North,Walnutsanity: Grove Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
4147,Island North,Walnutsanity: Diamond Of Grass,"WALNUTSANITY,WALNUTSANITY_DIG",
4148,Island North,Walnutsanity: Small Circle Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG",
4149,Island North,Walnutsanity: Patch Of Sand,"WALNUTSANITY,WALNUTSANITY_DIG",
4150,Dig Site,Walnutsanity: Crooked Circle Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG",
4151,Dig Site,Walnutsanity: Above Dig Site Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
4152,Dig Site,Walnutsanity: Above Field Office Bush 1,"WALNUTSANITY,WALNUTSANITY_BUSH",
4153,Dig Site,Walnutsanity: Above Field Office Bush 2,"WALNUTSANITY,WALNUTSANITY_BUSH",
4154,Field Office,Walnutsanity: Complete Large Animal Collection,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4155,Field Office,Walnutsanity: Complete Snake Collection,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4156,Field Office,Walnutsanity: Complete Mummified Frog Collection,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4157,Field Office,Walnutsanity: Complete Mummified Bat Collection,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4158,Field Office,Walnutsanity: Purple Flowers Island Survey,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4159,Field Office,Walnutsanity: Purple Starfish Island Survey,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4160,Island North,Walnutsanity: Bush Behind Volcano Tree,"WALNUTSANITY,WALNUTSANITY_BUSH",
4161,Island North,Walnutsanity: Arc Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG",
4162,Island North,Walnutsanity: Protruding Tree Walnut,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4163,Island North,Walnutsanity: Journal Scrap #10,"WALNUTSANITY,WALNUTSANITY_DIG",
4164,Island North,Walnutsanity: Northmost Point Circle Of Stones,"WALNUTSANITY,WALNUTSANITY_DIG",
4165,Island North,Walnutsanity: Hidden Passage Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
4166,Volcano Secret Beach,Walnutsanity: Secret Beach Bush 1,"WALNUTSANITY,WALNUTSANITY_BUSH",
4167,Volcano Secret Beach,Walnutsanity: Secret Beach Bush 2,"WALNUTSANITY,WALNUTSANITY_BUSH",
4168,Volcano - Floor 5,Walnutsanity: Volcano Rocks Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4169,Volcano - Floor 5,Walnutsanity: Volcano Rocks Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4170,Volcano - Floor 10,Walnutsanity: Volcano Rocks Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4171,Volcano - Floor 10,Walnutsanity: Volcano Rocks Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4172,Volcano - Floor 10,Walnutsanity: Volcano Rocks Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4173,Volcano - Floor 5,Walnutsanity: Volcano Monsters Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4174,Volcano - Floor 5,Walnutsanity: Volcano Monsters Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4175,Volcano - Floor 10,Walnutsanity: Volcano Monsters Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4176,Volcano - Floor 10,Walnutsanity: Volcano Monsters Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4177,Volcano - Floor 10,Walnutsanity: Volcano Monsters Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4178,Volcano - Floor 5,Walnutsanity: Volcano Crates Walnut 1,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4179,Volcano - Floor 5,Walnutsanity: Volcano Crates Walnut 2,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4180,Volcano - Floor 10,Walnutsanity: Volcano Crates Walnut 3,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4181,Volcano - Floor 10,Walnutsanity: Volcano Crates Walnut 4,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4182,Volcano - Floor 10,Walnutsanity: Volcano Crates Walnut 5,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4183,Volcano - Floor 5,Walnutsanity: Volcano Common Chest Walnut,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4184,Volcano - Floor 10,Walnutsanity: Volcano Rare Chest Walnut,"WALNUTSANITY,WALNUTSANITY_REPEATABLE",
4185,Volcano - Floor 10,Walnutsanity: Forge Entrance Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
4186,Volcano - Floor 10,Walnutsanity: Forge Exit Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
4187,Island North,Walnutsanity: Cliff Over Island South Bush,"WALNUTSANITY,WALNUTSANITY_BUSH",
4188,Island Southeast,Walnutsanity: Starfish Tide Pool,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4189,Island Southeast,Walnutsanity: Diamond Of Yellow Starfish,"WALNUTSANITY,WALNUTSANITY_DIG",
4190,Island Southeast,Walnutsanity: Mermaid Song,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4191,Pirate Cove,Walnutsanity: Pirate Darts 1,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4192,Pirate Cove,Walnutsanity: Pirate Darts 2,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4193,Pirate Cove,Walnutsanity: Pirate Darts 3,"WALNUTSANITY,WALNUTSANITY_PUZZLE",
4194,Pirate Cove,Walnutsanity: Pirate Cove Patch Of Sand,"WALNUTSANITY,WALNUTSANITY_DIG",
5001,Stardew Valley,Level 1 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill
5002,Stardew Valley,Level 2 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill
5003,Stardew Valley,Level 3 Luck,"LUCK_LEVEL,SKILL_LEVEL",Luck Skill
1 id region name tags mod_name
2316 4069 Museum Read Note From Gunther BOOKSANITY,BOOKSANITY_LOST
2317 4070 Museum Read Goblins by M. Jasper BOOKSANITY,BOOKSANITY_LOST
2318 4071 Museum Read Secret Statues Acrostics BOOKSANITY,BOOKSANITY_LOST
2319 4101 Clint's Blacksmith Open Golden Coconut Walnutsanity: Open Golden Coconut WALNUTSANITY,WALNUTSANITY_PUZZLE
2320 4102 Island West Fishing Walnut 1 Walnutsanity: Fishing Walnut 1 WALNUTSANITY,WALNUTSANITY_REPEATABLE
2321 4103 Island West Fishing Walnut 2 Walnutsanity: Fishing Walnut 2 WALNUTSANITY,WALNUTSANITY_REPEATABLE
2322 4104 Island North Fishing Walnut 3 Walnutsanity: Fishing Walnut 3 WALNUTSANITY,WALNUTSANITY_REPEATABLE
2323 4105 Island North Fishing Walnut 4 Walnutsanity: Fishing Walnut 4 WALNUTSANITY,WALNUTSANITY_REPEATABLE
2324 4106 Island Southeast Fishing Walnut 5 Walnutsanity: Fishing Walnut 5 WALNUTSANITY,WALNUTSANITY_REPEATABLE
2325 4107 Island East Jungle Bush Walnutsanity: Jungle Bush WALNUTSANITY,WALNUTSANITY_BUSH
2326 4108 Island East Banana Altar Walnutsanity: Banana Altar WALNUTSANITY,WALNUTSANITY_PUZZLE
2327 4109 Leo's Hut Leo's Tree Walnutsanity: Leo's Tree WALNUTSANITY,WALNUTSANITY_PUZZLE
2328 4110 Island Shrine Gem Birds Bush Walnutsanity: Gem Birds Bush WALNUTSANITY,WALNUTSANITY_BUSH
2329 4111 Island Shrine Gem Birds Shrine Walnutsanity: Gem Birds Shrine WALNUTSANITY,WALNUTSANITY_PUZZLE
2330 4112 Island West Harvesting Walnut 1 Walnutsanity: Harvesting Walnut 1 WALNUTSANITY,WALNUTSANITY_REPEATABLE
2331 4113 Island West Harvesting Walnut 2 Walnutsanity: Harvesting Walnut 2 WALNUTSANITY,WALNUTSANITY_REPEATABLE
2332 4114 Island West Harvesting Walnut 3 Walnutsanity: Harvesting Walnut 3 WALNUTSANITY,WALNUTSANITY_REPEATABLE
2333 4115 Island West Harvesting Walnut 4 Walnutsanity: Harvesting Walnut 4 WALNUTSANITY,WALNUTSANITY_REPEATABLE
2334 4116 Island West Harvesting Walnut 5 Walnutsanity: Harvesting Walnut 5 WALNUTSANITY,WALNUTSANITY_REPEATABLE
2335 4117 Gourmand Frog Cave Gourmand Frog Melon Walnutsanity: Gourmand Frog Melon WALNUTSANITY,WALNUTSANITY_PUZZLE
2336 4118 Gourmand Frog Cave Gourmand Frog Wheat Walnutsanity: Gourmand Frog Wheat WALNUTSANITY,WALNUTSANITY_PUZZLE
2337 4119 Gourmand Frog Cave Gourmand Frog Garlic Walnutsanity: Gourmand Frog Garlic WALNUTSANITY,WALNUTSANITY_PUZZLE
2338 4120 Island West Journal Scrap #6 Walnutsanity: Journal Scrap #6 WALNUTSANITY,WALNUTSANITY_DIG
2339 4121 Island West Mussel Node Walnut 1 Walnutsanity: Mussel Node Walnut 1 WALNUTSANITY,WALNUTSANITY_REPEATABLE
2340 4122 Island West Mussel Node Walnut 2 Walnutsanity: Mussel Node Walnut 2 WALNUTSANITY,WALNUTSANITY_REPEATABLE
2341 4123 Island West Mussel Node Walnut 3 Walnutsanity: Mussel Node Walnut 3 WALNUTSANITY,WALNUTSANITY_REPEATABLE
2342 4124 Island West Mussel Node Walnut 4 Walnutsanity: Mussel Node Walnut 4 WALNUTSANITY,WALNUTSANITY_REPEATABLE
2343 4125 Island West Mussel Node Walnut 5 Walnutsanity: Mussel Node Walnut 5 WALNUTSANITY,WALNUTSANITY_REPEATABLE
2344 4126 Shipwreck Shipwreck Bush Walnutsanity: Shipwreck Bush WALNUTSANITY,WALNUTSANITY_BUSH
2345 4127 Island West Whack A Mole Walnutsanity: Whack A Mole WALNUTSANITY,WALNUTSANITY_PUZZLE
2346 4128 Island West Starfish Triangle Walnutsanity: Starfish Triangle WALNUTSANITY,WALNUTSANITY_DIG
2347 4129 Island West Starfish Diamond Walnutsanity: Starfish Diamond WALNUTSANITY,WALNUTSANITY_DIG
2348 4130 Island West X in the sand Walnutsanity: X in the sand WALNUTSANITY,WALNUTSANITY_DIG
2349 4131 Island West Diamond Of Indents Walnutsanity: Diamond Of Indents WALNUTSANITY,WALNUTSANITY_DIG
2350 4132 Island West Bush Behind Coconut Tree Walnutsanity: Bush Behind Coconut Tree WALNUTSANITY,WALNUTSANITY_BUSH
2351 4133 Island West Journal Scrap #4 Walnutsanity: Journal Scrap #4 WALNUTSANITY,WALNUTSANITY_DIG
2352 4134 Island West Walnut Room Bush Walnutsanity: Walnut Room Bush WALNUTSANITY,WALNUTSANITY_BUSH
2353 4135 Island West Coast Bush Walnutsanity: Coast Bush WALNUTSANITY,WALNUTSANITY_BUSH
2354 4136 Island West Tiger Slime Walnut Walnutsanity: Tiger Slime Walnut WALNUTSANITY,WALNUTSANITY_REPEATABLE
2355 4137 Island West Bush Behind Mahogany Tree Walnutsanity: Bush Behind Mahogany Tree WALNUTSANITY,WALNUTSANITY_BUSH
2356 4138 Island West Circle Of Grass Walnutsanity: Circle Of Grass WALNUTSANITY,WALNUTSANITY_DIG
2357 4139 Island West Below Colored Crystals Cave Bush Walnutsanity: Below Colored Crystals Cave Bush WALNUTSANITY,WALNUTSANITY_BUSH
2358 4140 Colored Crystals Cave Colored Crystals Walnutsanity: Colored Crystals WALNUTSANITY,WALNUTSANITY_PUZZLE
2359 4141 Island West Cliff Edge Bush Walnutsanity: Cliff Edge Bush WALNUTSANITY,WALNUTSANITY_BUSH
2360 4142 Island West Diamond Of Pebbles Walnutsanity: Diamond Of Pebbles WALNUTSANITY,WALNUTSANITY_DIG
2361 4143 Island West Farm Parrot Express Bush Walnutsanity: Farm Parrot Express Bush WALNUTSANITY,WALNUTSANITY_BUSH
2362 4144 Island West Farmhouse Cliff Bush Walnutsanity: Farmhouse Cliff Bush WALNUTSANITY,WALNUTSANITY_BUSH
2363 4145 Island North Big Circle Of Stones Walnutsanity: Big Circle Of Stones WALNUTSANITY,WALNUTSANITY_DIG
2364 4146 Island North Grove Bush Walnutsanity: Grove Bush WALNUTSANITY,WALNUTSANITY_BUSH
2365 4147 Island North Diamond Of Grass Walnutsanity: Diamond Of Grass WALNUTSANITY,WALNUTSANITY_DIG
2366 4148 Island North Small Circle Of Stones Walnutsanity: Small Circle Of Stones WALNUTSANITY,WALNUTSANITY_DIG
2367 4149 Island North Patch Of Sand Walnutsanity: Patch Of Sand WALNUTSANITY,WALNUTSANITY_DIG
2368 4150 Dig Site Crooked Circle Of Stones Walnutsanity: Crooked Circle Of Stones WALNUTSANITY,WALNUTSANITY_DIG
2369 4151 Dig Site Above Dig Site Bush Walnutsanity: Above Dig Site Bush WALNUTSANITY,WALNUTSANITY_BUSH
2370 4152 Dig Site Above Field Office Bush 1 Walnutsanity: Above Field Office Bush 1 WALNUTSANITY,WALNUTSANITY_BUSH
2371 4153 Dig Site Above Field Office Bush 2 Walnutsanity: Above Field Office Bush 2 WALNUTSANITY,WALNUTSANITY_BUSH
2372 4154 Field Office Complete Large Animal Collection Walnutsanity: Complete Large Animal Collection WALNUTSANITY,WALNUTSANITY_PUZZLE
2373 4155 Field Office Complete Snake Collection Walnutsanity: Complete Snake Collection WALNUTSANITY,WALNUTSANITY_PUZZLE
2374 4156 Field Office Complete Mummified Frog Collection Walnutsanity: Complete Mummified Frog Collection WALNUTSANITY,WALNUTSANITY_PUZZLE
2375 4157 Field Office Complete Mummified Bat Collection Walnutsanity: Complete Mummified Bat Collection WALNUTSANITY,WALNUTSANITY_PUZZLE
2376 4158 Field Office Purple Flowers Island Survey Walnutsanity: Purple Flowers Island Survey WALNUTSANITY,WALNUTSANITY_PUZZLE
2377 4159 Field Office Purple Starfish Island Survey Walnutsanity: Purple Starfish Island Survey WALNUTSANITY,WALNUTSANITY_PUZZLE
2378 4160 Island North Bush Behind Volcano Tree Walnutsanity: Bush Behind Volcano Tree WALNUTSANITY,WALNUTSANITY_BUSH
2379 4161 Island North Arc Of Stones Walnutsanity: Arc Of Stones WALNUTSANITY,WALNUTSANITY_DIG
2380 4162 Island North Protruding Tree Walnut Walnutsanity: Protruding Tree Walnut WALNUTSANITY,WALNUTSANITY_PUZZLE
2381 4163 Island North Journal Scrap #10 Walnutsanity: Journal Scrap #10 WALNUTSANITY,WALNUTSANITY_DIG
2382 4164 Island North Northmost Point Circle Of Stones Walnutsanity: Northmost Point Circle Of Stones WALNUTSANITY,WALNUTSANITY_DIG
2383 4165 Island North Hidden Passage Bush Walnutsanity: Hidden Passage Bush WALNUTSANITY,WALNUTSANITY_BUSH
2384 4166 Volcano Secret Beach Secret Beach Bush 1 Walnutsanity: Secret Beach Bush 1 WALNUTSANITY,WALNUTSANITY_BUSH
2385 4167 Volcano Secret Beach Secret Beach Bush 2 Walnutsanity: Secret Beach Bush 2 WALNUTSANITY,WALNUTSANITY_BUSH
2386 4168 Volcano - Floor 5 Volcano Rocks Walnut 1 Walnutsanity: Volcano Rocks Walnut 1 WALNUTSANITY,WALNUTSANITY_REPEATABLE
2387 4169 Volcano - Floor 5 Volcano Rocks Walnut 2 Walnutsanity: Volcano Rocks Walnut 2 WALNUTSANITY,WALNUTSANITY_REPEATABLE
2388 4170 Volcano - Floor 10 Volcano Rocks Walnut 3 Walnutsanity: Volcano Rocks Walnut 3 WALNUTSANITY,WALNUTSANITY_REPEATABLE
2389 4171 Volcano - Floor 10 Volcano Rocks Walnut 4 Walnutsanity: Volcano Rocks Walnut 4 WALNUTSANITY,WALNUTSANITY_REPEATABLE
2390 4172 Volcano - Floor 10 Volcano Rocks Walnut 5 Walnutsanity: Volcano Rocks Walnut 5 WALNUTSANITY,WALNUTSANITY_REPEATABLE
2391 4173 Volcano - Floor 5 Volcano Monsters Walnut 1 Walnutsanity: Volcano Monsters Walnut 1 WALNUTSANITY,WALNUTSANITY_REPEATABLE
2392 4174 Volcano - Floor 5 Volcano Monsters Walnut 2 Walnutsanity: Volcano Monsters Walnut 2 WALNUTSANITY,WALNUTSANITY_REPEATABLE
2393 4175 Volcano - Floor 10 Volcano Monsters Walnut 3 Walnutsanity: Volcano Monsters Walnut 3 WALNUTSANITY,WALNUTSANITY_REPEATABLE
2394 4176 Volcano - Floor 10 Volcano Monsters Walnut 4 Walnutsanity: Volcano Monsters Walnut 4 WALNUTSANITY,WALNUTSANITY_REPEATABLE
2395 4177 Volcano - Floor 10 Volcano Monsters Walnut 5 Walnutsanity: Volcano Monsters Walnut 5 WALNUTSANITY,WALNUTSANITY_REPEATABLE
2396 4178 Volcano - Floor 5 Volcano Crates Walnut 1 Walnutsanity: Volcano Crates Walnut 1 WALNUTSANITY,WALNUTSANITY_REPEATABLE
2397 4179 Volcano - Floor 5 Volcano Crates Walnut 2 Walnutsanity: Volcano Crates Walnut 2 WALNUTSANITY,WALNUTSANITY_REPEATABLE
2398 4180 Volcano - Floor 10 Volcano Crates Walnut 3 Walnutsanity: Volcano Crates Walnut 3 WALNUTSANITY,WALNUTSANITY_REPEATABLE
2399 4181 Volcano - Floor 10 Volcano Crates Walnut 4 Walnutsanity: Volcano Crates Walnut 4 WALNUTSANITY,WALNUTSANITY_REPEATABLE
2400 4182 Volcano - Floor 10 Volcano Crates Walnut 5 Walnutsanity: Volcano Crates Walnut 5 WALNUTSANITY,WALNUTSANITY_REPEATABLE
2401 4183 Volcano - Floor 5 Volcano Common Chest Walnut Walnutsanity: Volcano Common Chest Walnut WALNUTSANITY,WALNUTSANITY_REPEATABLE
2402 4184 Volcano - Floor 10 Volcano Rare Chest Walnut Walnutsanity: Volcano Rare Chest Walnut WALNUTSANITY,WALNUTSANITY_REPEATABLE
2403 4185 Volcano - Floor 10 Forge Entrance Bush Walnutsanity: Forge Entrance Bush WALNUTSANITY,WALNUTSANITY_BUSH
2404 4186 Volcano - Floor 10 Forge Exit Bush Walnutsanity: Forge Exit Bush WALNUTSANITY,WALNUTSANITY_BUSH
2405 4187 Island North Cliff Over Island South Bush Walnutsanity: Cliff Over Island South Bush WALNUTSANITY,WALNUTSANITY_BUSH
2406 4188 Island Southeast Starfish Tide Pool Walnutsanity: Starfish Tide Pool WALNUTSANITY,WALNUTSANITY_PUZZLE
2407 4189 Island Southeast Diamond Of Yellow Starfish Walnutsanity: Diamond Of Yellow Starfish WALNUTSANITY,WALNUTSANITY_DIG
2408 4190 Island Southeast Mermaid Song Walnutsanity: Mermaid Song WALNUTSANITY,WALNUTSANITY_PUZZLE
2409 4191 Pirate Cove Pirate Darts 1 Walnutsanity: Pirate Darts 1 WALNUTSANITY,WALNUTSANITY_PUZZLE
2410 4192 Pirate Cove Pirate Darts 2 Walnutsanity: Pirate Darts 2 WALNUTSANITY,WALNUTSANITY_PUZZLE
2411 4193 Pirate Cove Pirate Darts 3 Walnutsanity: Pirate Darts 3 WALNUTSANITY,WALNUTSANITY_PUZZLE
2412 4194 Pirate Cove Pirate Cove Patch Of Sand Walnutsanity: Pirate Cove Patch Of Sand WALNUTSANITY,WALNUTSANITY_DIG
2413 5001 Stardew Valley Level 1 Luck LUCK_LEVEL,SKILL_LEVEL Luck Skill
2414 5002 Stardew Valley Level 2 Luck LUCK_LEVEL,SKILL_LEVEL Luck Skill
2415 5003 Stardew Valley Level 3 Luck LUCK_LEVEL,SKILL_LEVEL Luck Skill

View File

@@ -0,0 +1,87 @@
# Guide de configuration du Randomizer Stardew Valley
## Logiciels nécessaires
- Stardew Valley 1.6 sur PC (Recommandé: [Steam](https://store.steampowered.com/app/413150/Stardew_Valley/))
- SMAPI ([Mod loader pour Stardew Valley](https://www.nexusmods.com/stardewvalley/mods/2400?tab=files))
- [StardewArchipelago Version 6.x.x](https://github.com/agilbert1412/StardewArchipelago/releases)
- Il est important d'utiliser une release en 6.x.x pour jouer sur des seeds générées ici. Les versions ultérieures peuvent uniquement être utilisées pour des release ultérieures du générateur de mondes, qui ne sont pas encore hébergées sur archipelago.gg
## Logiciels optionnels
- Launcher Archipelago à partir de la [page des versions d'Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
- (Uniquement pour le client textuel)
- Autres [mods supportés](https://github.com/agilbert1412/StardewArchipelago/blob/6.x.x/Documentation/Supported%20Mods.md) que vous pouvez ajouter au yaml pour les inclure dans la randomization d'Archipelago
- Il n'est **pas** recommandé de modder Stardew Valley avec des mods non supportés, même s'il est possible de le faire.
Les interactions entre mods peuvent être imprévisibles, et aucune aide ne sera fournie pour les bugs qui y sont liés.
- Plus vous avez de mods non supportés, et plus ils sont gros, plus vous avez de chances de casser des choses.
## Configuration du fichier YAML
### Qu'est qu'un fichier YAML et pourquoi en ai-je besoin ?
Voir le guide pour paramètrer un fichier YAML dans le guide de configuration d'Archipelago (en anglais): [Guide de configuration d'un MultiWorld basique](/tutorial/Archipelago/setup/en)
### Où puis-je récupèrer un fichier YAML
Vous pouvez personnaliser vos options en visitant la [Page d'options de joueur pour Stardew Valley](/games/Stardew%20Valley/player-options)
## Rejoindre une partie en MultiWorld
### Installation du mod
- Installer [SMAPI](https://www.nexusmods.com/stardewvalley/mods/2400?tab=files) en suivant les instructions sur la page du mod.
- Télécharger et extraire le mod [StardewArchipelago](https://github.com/agilbert1412/StardewArchipelago/releases) dans le dossier "Mods" de Stardew Valley.
- *Optionnel*: Si vous voulez lancer le jeu depuis Steam, ajouter l'option de lancement suivante à Stardew Valley : `"[PATH TO STARDEW VALLEY]\Stardew Valley\StardewModdingAPI.exe" %command%`
- Sinon, exécutez juste "StardewModdingAPI.exe" dans le dossier d'installation de Stardew Valley.
- Stardew Valley devrait se lancer avec une console qui liste les informations des mods installés, et intéragit avec certains d'entre eux.
### Se connecter au MultiServer
Lancer Stardew Valley avec SMAPI. Une fois que vous avez atteint l'écran titre du jeu, créez une nouvelle ferme.
Dans la fenêtre de création de personnage, vous verrez 3 nouveaux champs, qui permettent de relier votre personnage à un MultiWorld Archipelago.
![image](https://i.imgur.com/b8KZy2F.png)
Vous pouvez personnaliser votre personnage comme vous le souhaitez.
Le champ "Server" nécessite l'adresse **et** le port, et le "Slotname" est le nom que vous avez spécifié dans votre YAML.
`archipelago.gg:12345`
`StardewPlayer`
Le mot de passe est optionnel.
Votre jeu se connectera automatiquement à Archipelago, et se reconnectera automatiquement également quand vous chargerez votre sauvegarde, plus tard.
Vous n'aurez plus besoin d'entrer ces informations à nouveau pour ce personnage, à moins que votre session ne change d'ip ou de port.
Si l'ip ou le port de la session **change**, vous pouvez suivre ces instructions pour modifier les informations de connexion liées à votre sauvegarde :
- Lancer Stardew Valley moddé
- Dans le **menu principal** du jeu, entrer la commande suivante **dans la console de SMAPI** :
- `connect_override ip:port slot password`
- Par exemple : `connect_override archipelago.gg:54321 StardewPlayer`
- Chargez votre partie. Les nouvelles informations de connexion seront utilisées à la place de celles enregistrées initialement.
- Jouez une journée, dormez et sauvegarder la partie. Les nouvelles informations de connexion iront écraser les précédentes, et deviendront permanentes.
### Intéragir avec le MultiWorld depuis le jeu
Quand vous vous connectez, vous devriez voir un message dans le chat vous informant de l'existence de la commande `!!help`. Cette commande liste les autres commandes exclusives à Stardew Valley que vous pouvez utiliser.
De plus, vous pouvez utiliser le chat en jeu pour parler aux autres joueurs du MultiWorld, pour peu qu'ils aient un jeu qui supporte le chat.
Enfin, vous pouvez également utiliser les commandes Archipelago (`!help` pour les lister) depuis le chat du jeu, permettant de demander des indices (via la commande `!hint`) sur certains objets.
Il est important de préciser que le chat de Stardew Valley est assez limité. Par exemple, il ne permet pas de remonter l'historique de conversation. La console SMAPI qui tourne à côté aura quant à elle l'historique complet et sera plus pratique pour consulter des messages moins récents.
Pour une meilleure expérience avec le chat, vous pouvez aussi utiliser le client textuel d'Archipelago, bien qu'il ne permettra pas de lancer les commandes exclusives à Stardew Valley.
### Jouer avec des mods supportés
Voir la [documentation des mods supportés](https://github.com/agilbert1412/StardewArchipelago/blob/6.x.x/Documentation/Supported%20Mods.md) (en Anglais).
### Multijoueur
Vous ne pouvez pas jouer à Stardew Valley en mode multijoueur pour le moment. Il n'y a aucun plan d'action pour ajouter cette fonctionalité à court terme.

View File

@@ -443,27 +443,27 @@ def set_walnut_puzzle_rules(logic: StardewLogic, multiworld, player, world_optio
if WalnutsanityOptionName.puzzles not in world_options.walnutsanity:
return
set_rule(multiworld.get_location("Open Golden Coconut", player), logic.has(Geode.golden_coconut))
set_rule(multiworld.get_location("Banana Altar", player), logic.has(Fruit.banana))
set_rule(multiworld.get_location("Leo's Tree", player), logic.tool.has_tool(Tool.axe))
set_rule(multiworld.get_location("Gem Birds Shrine", player), logic.has(Mineral.amethyst) & logic.has(Mineral.aquamarine) &
set_rule(multiworld.get_location("Walnutsanity: Open Golden Coconut", player), logic.has(Geode.golden_coconut))
set_rule(multiworld.get_location("Walnutsanity: Banana Altar", player), logic.has(Fruit.banana))
set_rule(multiworld.get_location("Walnutsanity: Leo's Tree", player), logic.tool.has_tool(Tool.axe))
set_rule(multiworld.get_location("Walnutsanity: Gem Birds Shrine", player), logic.has(Mineral.amethyst) & logic.has(Mineral.aquamarine) &
logic.has(Mineral.emerald) & logic.has(Mineral.ruby) & logic.has(Mineral.topaz) &
logic.region.can_reach_all((Region.island_north, Region.island_west, Region.island_east, Region.island_south)))
set_rule(multiworld.get_location("Gourmand Frog Melon", player), logic.has(Fruit.melon) & logic.region.can_reach(Region.island_west))
set_rule(multiworld.get_location("Gourmand Frog Wheat", player), logic.has(Vegetable.wheat) &
logic.region.can_reach(Region.island_west) & logic.region.can_reach_location("Gourmand Frog Melon"))
set_rule(multiworld.get_location("Gourmand Frog Garlic", player), logic.has(Vegetable.garlic) &
logic.region.can_reach(Region.island_west) & logic.region.can_reach_location("Gourmand Frog Wheat"))
set_rule(multiworld.get_location("Whack A Mole", player), logic.tool.has_tool(Tool.watering_can, ToolMaterial.iridium))
set_rule(multiworld.get_location("Complete Large Animal Collection", player), logic.walnut.can_complete_large_animal_collection())
set_rule(multiworld.get_location("Complete Snake Collection", player), logic.walnut.can_complete_snake_collection())
set_rule(multiworld.get_location("Complete Mummified Frog Collection", player), logic.walnut.can_complete_frog_collection())
set_rule(multiworld.get_location("Complete Mummified Bat Collection", player), logic.walnut.can_complete_bat_collection())
set_rule(multiworld.get_location("Purple Flowers Island Survey", player), logic.walnut.can_start_field_office)
set_rule(multiworld.get_location("Purple Starfish Island Survey", player), logic.walnut.can_start_field_office)
set_rule(multiworld.get_location("Protruding Tree Walnut", player), logic.combat.has_slingshot)
set_rule(multiworld.get_location("Starfish Tide Pool", player), logic.tool.has_fishing_rod(1))
set_rule(multiworld.get_location("Mermaid Song", player), logic.has(Furniture.flute_block))
set_rule(multiworld.get_location("Walnutsanity: Gourmand Frog Melon", player), logic.has(Fruit.melon) & logic.region.can_reach(Region.island_west))
set_rule(multiworld.get_location("Walnutsanity: Gourmand Frog Wheat", player), logic.has(Vegetable.wheat) &
logic.region.can_reach(Region.island_west) & logic.region.can_reach_location("Walnutsanity: Gourmand Frog Melon"))
set_rule(multiworld.get_location("Walnutsanity: Gourmand Frog Garlic", player), logic.has(Vegetable.garlic) &
logic.region.can_reach(Region.island_west) & logic.region.can_reach_location("Walnutsanity: Gourmand Frog Wheat"))
set_rule(multiworld.get_location("Walnutsanity: Whack A Mole", player), logic.tool.has_tool(Tool.watering_can, ToolMaterial.iridium))
set_rule(multiworld.get_location("Walnutsanity: Complete Large Animal Collection", player), logic.walnut.can_complete_large_animal_collection())
set_rule(multiworld.get_location("Walnutsanity: Complete Snake Collection", player), logic.walnut.can_complete_snake_collection())
set_rule(multiworld.get_location("Walnutsanity: Complete Mummified Frog Collection", player), logic.walnut.can_complete_frog_collection())
set_rule(multiworld.get_location("Walnutsanity: Complete Mummified Bat Collection", player), logic.walnut.can_complete_bat_collection())
set_rule(multiworld.get_location("Walnutsanity: Purple Flowers Island Survey", player), logic.walnut.can_start_field_office)
set_rule(multiworld.get_location("Walnutsanity: Purple Starfish Island Survey", player), logic.walnut.can_start_field_office)
set_rule(multiworld.get_location("Walnutsanity: Protruding Tree Walnut", player), logic.combat.has_slingshot)
set_rule(multiworld.get_location("Walnutsanity: Starfish Tide Pool", player), logic.tool.has_fishing_rod(1))
set_rule(multiworld.get_location("Walnutsanity: Mermaid Song", player), logic.has(Furniture.flute_block))
def set_walnut_bushes_rules(logic, multiworld, player, world_options):
@@ -490,13 +490,13 @@ def set_walnut_repeatable_rules(logic, multiworld, player, world_options):
if WalnutsanityOptionName.repeatables not in world_options.walnutsanity:
return
for i in range(1, 6):
set_rule(multiworld.get_location(f"Fishing Walnut {i}", player), logic.tool.has_fishing_rod(1))
set_rule(multiworld.get_location(f"Harvesting Walnut {i}", player), logic.skill.can_get_farming_xp)
set_rule(multiworld.get_location(f"Mussel Node Walnut {i}", player), logic.tool.has_tool(Tool.pickaxe))
set_rule(multiworld.get_location(f"Volcano Rocks Walnut {i}", player), logic.tool.has_tool(Tool.pickaxe))
set_rule(multiworld.get_location(f"Volcano Monsters Walnut {i}", player), logic.combat.has_galaxy_weapon)
set_rule(multiworld.get_location(f"Volcano Crates Walnut {i}", player), logic.combat.has_any_weapon)
set_rule(multiworld.get_location(f"Tiger Slime Walnut", player), logic.monster.can_kill(Monster.tiger_slime))
set_rule(multiworld.get_location(f"Walnutsanity: Fishing Walnut {i}", player), logic.tool.has_fishing_rod(1))
set_rule(multiworld.get_location(f"Walnutsanity: Harvesting Walnut {i}", player), logic.skill.can_get_farming_xp)
set_rule(multiworld.get_location(f"Walnutsanity: Mussel Node Walnut {i}", player), logic.tool.has_tool(Tool.pickaxe))
set_rule(multiworld.get_location(f"Walnutsanity: Volcano Rocks Walnut {i}", player), logic.tool.has_tool(Tool.pickaxe))
set_rule(multiworld.get_location(f"Walnutsanity: Volcano Monsters Walnut {i}", player), logic.combat.has_galaxy_weapon)
set_rule(multiworld.get_location(f"Walnutsanity: Volcano Crates Walnut {i}", player), logic.combat.has_any_weapon)
set_rule(multiworld.get_location(f"Walnutsanity: Tiger Slime Walnut", player), logic.monster.can_kill(Monster.tiger_slime))
def set_cropsanity_rules(logic: StardewLogic, multiworld, player, world_content: StardewContent):

View File

@@ -1,26 +1,46 @@
import unittest
from .bases import SVTestBase
from ..options import ExcludeGingerIsland, Walnutsanity, ToolProgression, SkillProgression
from ..strings.ap_names.ap_option_names import WalnutsanityOptionName
class TestWalnutsanityNone(SVTestBase):
class SVWalnutsanityTestBase(SVTestBase):
expected_walnut_locations: set[str] = set()
unexpected_walnut_locations: set[str] = set()
@classmethod
def setUpClass(cls) -> None:
if cls is SVWalnutsanityTestBase:
raise unittest.SkipTest("Base tests disabled")
super().setUpClass()
def test_walnut_locations(self):
location_names = {location.name for location in self.multiworld.get_locations()}
for location in self.expected_walnut_locations:
self.assertIn(location, location_names, f"{location} should be in the location names")
for location in self.unexpected_walnut_locations:
self.assertNotIn(location, location_names, f"{location} should not be in the location names")
class TestWalnutsanityNone(SVWalnutsanityTestBase):
options = {
ExcludeGingerIsland: ExcludeGingerIsland.option_false,
Walnutsanity: Walnutsanity.preset_none,
SkillProgression: ToolProgression.option_progressive,
ToolProgression: ToolProgression.option_progressive,
}
def test_no_walnut_locations(self):
location_names = {location.name for location in self.multiworld.get_locations()}
self.assertNotIn("Open Golden Coconut", location_names)
self.assertNotIn("Fishing Walnut 4", location_names)
self.assertNotIn("Journal Scrap #6", location_names)
self.assertNotIn("Starfish Triangle", location_names)
self.assertNotIn("Bush Behind Coconut Tree", location_names)
self.assertNotIn("Purple Starfish Island Survey", location_names)
self.assertNotIn("Volcano Monsters Walnut 3", location_names)
self.assertNotIn("Cliff Over Island South Bush", location_names)
unexpected_walnut_locations = {
"Walnutsanity: Open Golden Coconut",
"Walnutsanity: Fishing Walnut 4",
"Walnutsanity: Journal Scrap #6",
"Walnutsanity: Starfish Triangle",
"Walnutsanity: Bush Behind Coconut Tree",
"Walnutsanity: Purple Starfish Island Survey",
"Walnutsanity: Volcano Monsters Walnut 3",
"Walnutsanity: Cliff Over Island South Bush",
}
def test_logic_received_walnuts(self):
# You need to receive 0, and collect 40
@@ -48,28 +68,30 @@ class TestWalnutsanityNone(SVTestBase):
self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player))
class TestWalnutsanityPuzzles(SVTestBase):
class TestWalnutsanityPuzzles(SVWalnutsanityTestBase):
options = {
ExcludeGingerIsland: ExcludeGingerIsland.option_false,
Walnutsanity: frozenset({WalnutsanityOptionName.puzzles}),
SkillProgression: ToolProgression.option_progressive,
ToolProgression: ToolProgression.option_progressive,
}
def test_only_puzzle_walnut_locations(self):
location_names = {location.name for location in self.multiworld.get_locations()}
self.assertIn("Open Golden Coconut", location_names)
self.assertNotIn("Fishing Walnut 4", location_names)
self.assertNotIn("Journal Scrap #6", location_names)
self.assertNotIn("Starfish Triangle", location_names)
self.assertNotIn("Bush Behind Coconut Tree", location_names)
self.assertIn("Purple Starfish Island Survey", location_names)
self.assertNotIn("Volcano Monsters Walnut 3", location_names)
self.assertNotIn("Cliff Over Island South Bush", location_names)
expected_walnut_locations = {
"Walnutsanity: Open Golden Coconut",
"Walnutsanity: Purple Starfish Island Survey",
}
unexpected_walnut_locations = {
"Walnutsanity: Fishing Walnut 4",
"Walnutsanity: Journal Scrap #6",
"Walnutsanity: Starfish Triangle",
"Walnutsanity: Bush Behind Coconut Tree",
"Walnutsanity: Volcano Monsters Walnut 3",
"Walnutsanity: Cliff Over Island South Bush",
}
def test_field_office_locations_require_professor_snail(self):
location_names = ["Complete Large Animal Collection", "Complete Snake Collection", "Complete Mummified Frog Collection",
"Complete Mummified Bat Collection", "Purple Flowers Island Survey", "Purple Starfish Island Survey", ]
location_names = ["Walnutsanity: Complete Large Animal Collection", "Walnutsanity: Complete Snake Collection",
"Walnutsanity: Complete Mummified Frog Collection", "Walnutsanity: Complete Mummified Bat Collection",
"Walnutsanity: Purple Flowers Island Survey", "Walnutsanity: Purple Starfish Island Survey", ]
self.collect("Island Obelisk")
self.collect("Island North Turtle")
self.collect("Island West Turtle")
@@ -90,40 +112,42 @@ class TestWalnutsanityPuzzles(SVTestBase):
self.assert_can_reach_location(location)
class TestWalnutsanityBushes(SVTestBase):
class TestWalnutsanityBushes(SVWalnutsanityTestBase):
options = {
ExcludeGingerIsland: ExcludeGingerIsland.option_false,
Walnutsanity: frozenset({WalnutsanityOptionName.bushes}),
}
def test_only_bush_walnut_locations(self):
location_names = {location.name for location in self.multiworld.get_locations()}
self.assertNotIn("Open Golden Coconut", location_names)
self.assertNotIn("Fishing Walnut 4", location_names)
self.assertNotIn("Journal Scrap #6", location_names)
self.assertNotIn("Starfish Triangle", location_names)
self.assertIn("Bush Behind Coconut Tree", location_names)
self.assertNotIn("Purple Starfish Island Survey", location_names)
self.assertNotIn("Volcano Monsters Walnut 3", location_names)
self.assertIn("Cliff Over Island South Bush", location_names)
expected_walnut_locations = {
"Walnutsanity: Bush Behind Coconut Tree",
"Walnutsanity: Cliff Over Island South Bush",
}
unexpected_walnut_locations = {
"Walnutsanity: Open Golden Coconut",
"Walnutsanity: Fishing Walnut 4",
"Walnutsanity: Journal Scrap #6",
"Walnutsanity: Starfish Triangle",
"Walnutsanity: Purple Starfish Island Survey",
"Walnutsanity: Volcano Monsters Walnut 3",
}
class TestWalnutsanityPuzzlesAndBushes(SVTestBase):
class TestWalnutsanityPuzzlesAndBushes(SVWalnutsanityTestBase):
options = {
ExcludeGingerIsland: ExcludeGingerIsland.option_false,
Walnutsanity: frozenset({WalnutsanityOptionName.puzzles, WalnutsanityOptionName.bushes}),
}
def test_only_bush_walnut_locations(self):
location_names = {location.name for location in self.multiworld.get_locations()}
self.assertIn("Open Golden Coconut", location_names)
self.assertNotIn("Fishing Walnut 4", location_names)
self.assertNotIn("Journal Scrap #6", location_names)
self.assertNotIn("Starfish Triangle", location_names)
self.assertIn("Bush Behind Coconut Tree", location_names)
self.assertIn("Purple Starfish Island Survey", location_names)
self.assertNotIn("Volcano Monsters Walnut 3", location_names)
self.assertIn("Cliff Over Island South Bush", location_names)
expected_walnut_locations = {
"Walnutsanity: Open Golden Coconut",
"Walnutsanity: Bush Behind Coconut Tree",
"Walnutsanity: Purple Starfish Island Survey",
"Walnutsanity: Cliff Over Island South Bush",
}
unexpected_walnut_locations = {
"Walnutsanity: Fishing Walnut 4",
"Walnutsanity: Journal Scrap #6",
"Walnutsanity: Starfish Triangle",
"Walnutsanity: Volcano Monsters Walnut 3",
}
def test_logic_received_walnuts(self):
# You need to receive 25, and collect 15
@@ -136,58 +160,59 @@ class TestWalnutsanityPuzzlesAndBushes(SVTestBase):
self.assertTrue(self.multiworld.state.can_reach_location("Parrot Express", self.player))
class TestWalnutsanityDigSpots(SVTestBase):
class TestWalnutsanityDigSpots(SVWalnutsanityTestBase):
options = {
ExcludeGingerIsland: ExcludeGingerIsland.option_false,
Walnutsanity: frozenset({WalnutsanityOptionName.dig_spots}),
}
def test_only_dig_spots_walnut_locations(self):
location_names = {location.name for location in self.multiworld.get_locations()}
self.assertNotIn("Open Golden Coconut", location_names)
self.assertNotIn("Fishing Walnut 4", location_names)
self.assertIn("Journal Scrap #6", location_names)
self.assertIn("Starfish Triangle", location_names)
self.assertNotIn("Bush Behind Coconut Tree", location_names)
self.assertNotIn("Purple Starfish Island Survey", location_names)
self.assertNotIn("Volcano Monsters Walnut 3", location_names)
self.assertNotIn("Cliff Over Island South Bush", location_names)
expected_walnut_locations = {
"Walnutsanity: Journal Scrap #6",
"Walnutsanity: Starfish Triangle",
}
unexpected_walnut_locations = {
"Walnutsanity: Open Golden Coconut",
"Walnutsanity: Fishing Walnut 4",
"Walnutsanity: Bush Behind Coconut Tree",
"Walnutsanity: Purple Starfish Island Survey",
"Walnutsanity: Volcano Monsters Walnut 3",
"Walnutsanity: Cliff Over Island South Bush",
}
class TestWalnutsanityRepeatables(SVTestBase):
class TestWalnutsanityRepeatables(SVWalnutsanityTestBase):
options = {
ExcludeGingerIsland: ExcludeGingerIsland.option_false,
Walnutsanity: frozenset({WalnutsanityOptionName.repeatables}),
}
def test_only_repeatable_walnut_locations(self):
location_names = {location.name for location in self.multiworld.get_locations()}
self.assertNotIn("Open Golden Coconut", location_names)
self.assertIn("Fishing Walnut 4", location_names)
self.assertNotIn("Journal Scrap #6", location_names)
self.assertNotIn("Starfish Triangle", location_names)
self.assertNotIn("Bush Behind Coconut Tree", location_names)
self.assertNotIn("Purple Starfish Island Survey", location_names)
self.assertIn("Volcano Monsters Walnut 3", location_names)
self.assertNotIn("Cliff Over Island South Bush", location_names)
expected_walnut_locations = {
"Walnutsanity: Fishing Walnut 4",
"Walnutsanity: Volcano Monsters Walnut 3",
}
unexpected_walnut_locations = {
"Walnutsanity: Open Golden Coconut",
"Walnutsanity: Journal Scrap #6",
"Walnutsanity: Starfish Triangle",
"Walnutsanity: Bush Behind Coconut Tree",
"Walnutsanity: Purple Starfish Island Survey",
"Walnutsanity: Cliff Over Island South Bush",
}
class TestWalnutsanityAll(SVTestBase):
class TestWalnutsanityAll(SVWalnutsanityTestBase):
options = {
ExcludeGingerIsland: ExcludeGingerIsland.option_false,
Walnutsanity: Walnutsanity.preset_all,
}
def test_all_walnut_locations(self):
location_names = {location.name for location in self.multiworld.get_locations()}
self.assertIn("Open Golden Coconut", location_names)
self.assertIn("Fishing Walnut 4", location_names)
self.assertIn("Journal Scrap #6", location_names)
self.assertIn("Starfish Triangle", location_names)
self.assertIn("Bush Behind Coconut Tree", location_names)
self.assertIn("Purple Starfish Island Survey", location_names)
self.assertIn("Volcano Monsters Walnut 3", location_names)
self.assertIn("Cliff Over Island South Bush", location_names)
expected_walnut_locations = {
"Walnutsanity: Open Golden Coconut",
"Walnutsanity: Fishing Walnut 4",
"Walnutsanity: Journal Scrap #6",
"Walnutsanity: Starfish Triangle",
"Walnutsanity: Bush Behind Coconut Tree",
"Walnutsanity: Purple Starfish Island Survey",
"Walnutsanity: Volcano Monsters Walnut 3",
"Walnutsanity: Cliff Over Island South Bush",
}
def test_logic_received_walnuts(self):
# You need to receive 40, and collect 4

View File

@@ -1,6 +0,0 @@
from test.bases import WorldTestBase
class TunicTestBase(WorldTestBase):
game = "TUNIC"
player = 1

View File

@@ -0,0 +1,5 @@
from test.bases import WorldTestBase
class TunicTestBase(WorldTestBase):
game = "TUNIC"

View File

@@ -1,5 +1,5 @@
from . import TunicTestBase
from .. import options
from .bases import TunicTestBase
class TestAccess(TunicTestBase):

View File

@@ -1,17 +1,15 @@
from BaseClasses import ItemClassification
from collections import Counter
from . import TunicTestBase
from .. import options
from .. import options, TunicWorld
from .bases import TunicTestBase
from ..combat_logic import (check_combat_reqs, area_data, get_money_count, calc_effective_hp, get_potion_level,
get_hp_level, get_def_level, get_sp_level, has_combat_reqs)
from ..items import item_table
from .. import TunicWorld
class TestCombat(TunicTestBase):
options = {options.CombatLogic.internal_name: options.CombatLogic.option_on}
player = 1
world: TunicWorld
combat_items = []
# these are items that are progression that do not contribute to combat logic

View File

@@ -27,14 +27,32 @@ from .rules import set_rules
class WitnessWebWorld(WebWorld):
theme = "jungle"
tutorials = [Tutorial(
setup_en = Tutorial(
"Multiworld Setup Guide",
"A guide to playing The Witness with Archipelago.",
"English",
"setup_en.md",
"setup/en",
["NewSoupVi", "Jarno"]
)]
)
setup_de = Tutorial(
setup_en.tutorial_name,
setup_en.description,
"German",
"setup_de.md",
"setup/de",
["NewSoupVi"]
)
setup_fr = Tutorial(
setup_en.tutorial_name,
setup_en.description,
"Français",
"setup_fr.md",
"setup/fr",
["Rever"]
)
tutorials = [setup_en, setup_de, setup_fr]
options_presets = witness_option_presets
option_groups = witness_option_groups

View File

@@ -18,6 +18,7 @@ GENERAL_LOCATIONS = {
"Outside Tutorial Outpost Entry Panel",
"Outside Tutorial Outpost Exit Panel",
"Glass Factory Entry Panel",
"Glass Factory Discard",
"Glass Factory Back Wall 5",
"Glass Factory Front 3",

View File

@@ -0,0 +1,46 @@
# The Witness Randomizer Setup
## Benötigte Software
- [The Witness für ein 64-bit-Windows-Betriebssystem (z.B. Steam-Version)](https://store.steampowered.com/app/210970/The_Witness/)
- [The Witness Archipelago Randomizer](https://github.com/NewSoupVi/The-Witness-Randomizer-for-Archipelago/releases/latest)
## Optionale Software
- [ArchipelagoTextClient](https://github.com/ArchipelagoMW/Archipelago/releases)
- [The Witness Auto-Tracker mit Kartenansicht](https://github.com/NewSoupVi/witness_archipelago_tracker/releases), benutzbar mit [PopTracker](https://github.com/black-sliver/PopTracker/releases)
## Verbindung mit einem Multiworld-Spiel
1. Öffne The Witness.
2. Erstelle einen neuen Speicherstand.
3. Öffne [The Witness Archipelago Randomizer](https://github.com/NewSoupVi/The-Witness-Randomizer-for-Archipelago/releases/latest).
4. Gib die Archipelago-Adresse, deinen Namen und evtl. das Passwort ein.
5. Drücke "Connect".
6. Viel Spaß!
Wenn du ein vorheriges Spiel fortsetzen willst, ist das auch möüglich:
1. Öffne The Witness.
2. Lade den Speicherstand für das Multiworld-Spiel, das du weiterspielen willst - Wenn das nicht sowieso schon der ist, den das Spiel automatisch geladen hat.
3. Öffne [The Witness Archipelago Randomizer](https://github.com/NewSoupVi/The-Witness-Randomizer-for-Archipelago/releases/latest).
4. Drücke "Load Credentials", um Adresse, Namen und Passwort automatisch zu laden (oder tippe diese manuell ein).
5. Drücke "Connect".
## Archipelago Text Client
Es ist empfehlenswert, den "Archipelago Text Client", der eine Textansicht für gesendete und erhaltene Items liefert, beim Spielen nebenbei sichtbar zu haben.
<br/>Diese Nachrichten werden zwar auch im Spiel angezeigt, jedoch nur für ein paar Sekunden. Es ist leicht, eine dieser Nachrichten zu übersehen.
<br/><br/>Alternativ gibt es den visuellen Auto-Tracker mit Kartenansicht, der im nächsten Kapitel beschrieben wird.
## Auto-Tracking
The Witness hat einen voll funktionsfähigen Tracker mit Kartenansicht und Autotracking.
1. Installiere [PopTracker](https://github.com/black-sliver/PopTracker/releases) und lade den [The Witness Auto-Tracker mit Kartenansicht](https://github.com/NewSoupVi/witness_archipelago_tracker/releases) herunter.
2. Öffne PopTracker, und lade das "The Witness"-Packet.
3. Klicke auf das "AP"-Symbol am oberen Fensterrand.
4. Gib die Archipelago-Adresse, deinen Namen und evtl. das Passwort ein.
Der Rest sollte vollautomatisch ohne weitere Eingabe funktionieren. Der Tracker wird deine momentanen Items anzeigen und lösbare Rätsel grün auf der Karte anzeigen. Sobald du eine Rätselsequenz abschließt, wird sie grau markiert.

View File

@@ -0,0 +1,47 @@
# Guide d'installation du Witness randomizer
## Logiciels Requis
- [The Witness pour Windows 64-bit (par exemple, la version Steam)](https://store.steampowered.com/app/210970/The_Witness/)
- [The Witness Archipelago Randomizer](https://github.com/NewSoupVi/The-Witness-Randomizer-for-Archipelago/releases/latest)
## Logiciels Facultatifs
- [ArchipelagoTextClient](https://github.com/ArchipelagoMW/Archipelago/releases)
- [The Witness Map- et Auto-Tracker](https://github.com/NewSoupVi/witness_archipelago_tracker/releases), pour usage avec [PopTracker](https://github.com/black-sliver/PopTracker/releases)
## Rejoindre un jeu multimonde
1. Lancez The Witness
2. Commencez une nouvelle partie
3. Lancez [The Witness Archipelago Randomizer](https://github.com/NewSoupVi/The-Witness-Randomizer-for-Archipelago/releases/latest)
4. Inscrivez l'adresse Archipelago, votre nom de joueur et le mot de passe du jeu multimonde
5. Cliquez sur "Connect"
6. Jouez!
Pour continuer un jeu multimonde précedemment commencé:
1. Lancez The Witness
2. Chargez la sauvegarde sur laquelle vous avez dernièrement joué ce monde, si ce n'est pas celle qui a été chargée automatiquement
3. Lancez [The Witness Archipelago Randomizer](https://github.com/NewSoupVi/The-Witness-Randomizer-for-Archipelago/releases/latest)
4. Cliquez sur "Load Credentials" (ou tapez les manuellement)
5. Cliquez sur "Connect"
## Archipelago Text Client
Il est recommandé d'utiliser le "Archipelago Text Client" en parallèle afin de suivre quels items vous envoyez et recevez.
<br/>The Witness affiche également ces informations en jeu, mais seulement pour une courte période et donc il est facile de manquer ces messages.
<br/><br/>Bien sûr, vous pouvez également utiliser l'auto-tracker!
## Auto-Tracking
The Witness a un tracker fonctionnel qui supporte l'auto-tracking.
1. Téléchargez [The Witness Map- and Auto-Tracker](https://github.com/NewSoupVi/witness_archipelago_tracker/releases) et [PopTracker](https://github.com/black-sliver/PopTracker/releases).
2. Ouvrez Poptracker, puis chargez le pack Witness.
3. Cliquez sur l'icone "AP" qui se situe au dessus de la carte.
4. Inscrivez l'adresse Archipelago, votre nom de joueur et le mot de passe du jeu multimonde.
Le reste devrait être pris en charge par Poptracker - les items que vous recevrez et les puzzles que vous résolverez seront automatiquement indiqués. De plus, Poptracker est en mesure de détecter
vos paramètres de jeu - les puzzles accessibles seront alors masqués ou affichés en fonction de vos paramètres de randomization et de logique. Veuillez noter que le tracker peut être obsolète.