Compare commits

...

30 Commits

Author SHA1 Message Date
Fabian Dill
6f12caf55e Update MultiServer.py 2025-08-26 21:04:50 +02:00
Fabian Dill
ad7d2157c3 add notify_client_without_auth to send messages to not authenticated clients 2025-08-26 13:57:14 +02:00
Fabian Dill
27ed50ff1b MultiServer: prevent GetDataPackage if PerMessageDeflate is not supported. 2025-08-22 18:03:23 +02:00
Duck
bead81b64b Core: Fix get_unique_identifier failing on missing cache folder (#5322) 2025-08-21 07:46:06 +02:00
black-sliver
16d5b453a7 Core: require setuptools>=75 (#5346)
Setuptools 70.3.0 seems to not work for us.
2025-08-19 19:35:50 +02:00
massimilianodelliubaldini
48906de873 Jak and Daxter: fix checks getting lost if player disconnects. (#5280) 2025-08-19 18:08:39 +02:00
Nicholas Saylor
9a64b8c5ce Webhost: Remove showdown.js Remnants (#4984) 2025-08-18 02:48:56 +02:00
Silvris
6ba2b7f8c3 Tests: implement pattern for filtering unittests locally (#5080) 2025-08-18 02:46:48 +02:00
Flit
6f7ca082f2 Docker: use python:3.12-slim-bookworm (#5343) 2025-08-17 20:47:01 +02:00
Faris
eb09be3594 OSRS: Fix UT Integration and Various Gen Failures (#5331) 2025-08-16 17:08:44 -04:00
Fabian Dill
9d654b7e3b Core: drop Python 3.10 (#5324)
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2025-08-15 18:45:40 +02:00
Doug Hoskisson
8f7fcd4889 Zillion: Move completion_condition Definition Earlier (#5279) 2025-08-15 08:55:11 -04:00
black-sliver
b85887241f CI: update appimagetool hash (#5333) 2025-08-15 12:36:13 +02:00
Fabian Dill
5110676c76 Core: 0.6.4 (#5314) 2025-08-15 11:44:24 +02:00
JaredWeakStrike
0020e6c3d3 KH2: Fix html headers to be markdown (#5305)
* update setup guide

* Update worlds/kh2/docs/setup_en.md

Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>

* Update worlds/kh2/docs/setup_en.md

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Update en_Kingdom Hearts 2.md

---------

Co-authored-by: Fabian Dill <Berserker66@users.noreply.github.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
2025-08-12 23:35:25 +02:00
LiquidCat64
6e6fd0e9bc CV64 and CotM: Correct Archipleago (#5323) 2025-08-12 22:01:29 +02:00
black-sliver
85c26f9740 WebHost: redirect old tutorials to new URL (#5319)
* WebHost: redirect old tutorials to new URL

* WebHost: make comment in tutorial_redirect more accurate
2025-08-12 15:38:22 +00:00
Fabian Dill
9057ce0ce3 WebHost: fix links on sitemap, switch to url_for and add test to prevent future breakage (#5318) 2025-08-12 16:52:34 +02:00
black-sliver
378cc91a4d CI: update appimage runtime (#5315) 2025-08-12 02:41:43 +02:00
qwint
cdde38fdc9 Settings: warn for broken worlds instead of crashing (#4438)
note: i swear the issue was an importerror but i could only get attributeerrors on the getattr() call, maybe we want to check for both?
2025-08-10 17:23:39 +02:00
Adrian Priestley
c34c00baa4 fix(deps): Lock setuptools version to <81 (#5284)
- Update Dockerfile to specify "setuptools<81"
- Modify ModuleUpdate.py to install setuptools with version constraint
2025-08-10 17:09:31 +02:00
Mysteryem
9bd535752e Core: Sort Unreachable Locations Written to the Spoiler (#5269) 2025-08-10 11:03:12 -04:00
Duck
ecb22642af Tests: Handle optional args for get_all_state patch (#5297)
* Make `use_cache` optional

* Pass all kwargs
2025-08-09 00:24:19 +02:00
Exempt-Medic
17ccfdc266 DS3: Don't Create Disabled Locations (#5292) 2025-08-08 15:07:36 -04:00
Scipio Wright
4633f12972 Docs: Use / instead of . for the reference to lttp's options.py (#5300)
* Update options api.md

* o -> O
2025-08-07 20:14:09 +02:00
Silvris
1f6c99635e FF1: fix client breaking other NES games (#5293) 2025-08-05 22:25:11 +02:00
threeandthreee
4e92cac171 LADX: Update Docs (#5290)
* convert ladxr section to markdown, other adjustments
make links clickable
crow icon -> open tracker
adjust for removed sprite sheets
some adjustments in ladxr section for differences in the ap version:
we don't have a casual logic
we don't have stealing options

* fix link, and another correction
2025-08-04 11:46:05 -04:00
Scipio Wright
3b88630b0d TUNIC: Fix zig skip showing up in decoupled + fixed shop #5289 2025-08-04 14:21:58 +02:00
Ishigh1
e6d2d8f455 Core: Added a leading 0 to classification.as_flag #5291 2025-08-04 14:19:51 +02:00
massimilianodelliubaldini
84c2d70d9a Fix regression on 404 redirects 2025-08-03 03:06:42 +00:00
39 changed files with 399 additions and 211 deletions

View File

@@ -29,7 +29,7 @@
"reportMissingImports": true,
"reportMissingTypeStubs": true,
"pythonVersion": "3.10",
"pythonVersion": "3.11",
"pythonPlatform": "Windows",
"executionEnvironments": [

View File

@@ -53,7 +53,7 @@ jobs:
- uses: actions/setup-python@v5
if: env.diff != ''
with:
python-version: '3.10'
python-version: '3.11'
- name: "Install dependencies"
if: env.diff != ''

View File

@@ -22,9 +22,9 @@ env:
# NOTE: since appimage/appimagetool and appimage/type2-runtime does not have tags anymore,
# we check the sha256 and require manual intervention if it was updated.
APPIMAGETOOL_VERSION: continuous
APPIMAGETOOL_X86_64_HASH: '363dafac070b65cc36ca024b74db1f043c6f5cd7be8fca760e190dce0d18d684'
APPIMAGETOOL_X86_64_HASH: '29348a20b80827cd261c28e95172ff828b69d43d4e4e18e3fd069e2c8693c94e'
APPIMAGE_RUNTIME_VERSION: continuous
APPIMAGE_RUNTIME_X86_64_HASH: 'e3c4dfb70eddf42e7e5a1d28dff396d30563aa9a901970aebe6f01f3fecf9f8e'
APPIMAGE_RUNTIME_X86_64_HASH: 'e70ffa9b69b211574d0917adc482dd66f25a0083427b5945783965d55b0b0a8b'
permissions: # permissions required for attestation
id-token: 'write'

View File

@@ -12,9 +12,9 @@ env:
# NOTE: since appimage/appimagetool and appimage/type2-runtime does not have tags anymore,
# we check the sha256 and require manual intervention if it was updated.
APPIMAGETOOL_VERSION: continuous
APPIMAGETOOL_X86_64_HASH: '363dafac070b65cc36ca024b74db1f043c6f5cd7be8fca760e190dce0d18d684'
APPIMAGETOOL_X86_64_HASH: '29348a20b80827cd261c28e95172ff828b69d43d4e4e18e3fd069e2c8693c94e'
APPIMAGE_RUNTIME_VERSION: continuous
APPIMAGE_RUNTIME_X86_64_HASH: 'e3c4dfb70eddf42e7e5a1d28dff396d30563aa9a901970aebe6f01f3fecf9f8e'
APPIMAGE_RUNTIME_X86_64_HASH: 'e70ffa9b69b211574d0917adc482dd66f25a0083427b5945783965d55b0b0a8b'
permissions: # permissions required for attestation
id-token: 'write'

View File

@@ -39,11 +39,10 @@ jobs:
matrix:
os: [ubuntu-latest]
python:
- {version: '3.10'}
- {version: '3.11'}
- {version: '3.11.2'} # Change to '3.11' around 2026-06-10
- {version: '3.12'}
include:
- python: {version: '3.10'} # old compat
- python: {version: '3.11'} # old compat
os: windows-latest
- python: {version: '3.12'} # current
os: windows-latest

View File

@@ -1571,7 +1571,7 @@ class ItemClassification(IntFlag):
def as_flag(self) -> int:
"""As Network API flag int."""
return int(self & 0b0111)
return int(self & 0b00111)
class Item:
@@ -1899,7 +1899,8 @@ class Spoiler:
if self.unreachables:
outfile.write('\n\nUnreachable Progression Items:\n\n')
outfile.write(
'\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables]))
'\n'.join(['%s: %s' % (unreachable.item, unreachable)
for unreachable in sorted(self.unreachables)]))
if self.paths:
outfile.write('\n\nPaths:\n\n')

View File

@@ -28,7 +28,7 @@ COPY requirements.txt WebHostLib/requirements.txt
RUN pip install --no-cache-dir -r \
WebHostLib/requirements.txt \
setuptools
"setuptools>=75,<81"
COPY _speedups.pyx .
COPY intset.h .
@@ -36,7 +36,7 @@ COPY intset.h .
RUN cythonize -b -i _speedups.pyx
# Archipelago
FROM python:3.12-slim AS archipelago
FROM python:3.12-slim-bookworm AS archipelago
ARG TARGETARCH
ENV VIRTUAL_ENV=/opt/venv
ENV PYTHONUNBUFFERED=1

View File

@@ -5,15 +5,15 @@ import multiprocessing
import warnings
if sys.platform in ("win32", "darwin") and sys.version_info < (3, 10, 11):
if sys.platform in ("win32", "darwin") and sys.version_info < (3, 11, 9):
# Official micro version updates. This should match the number in docs/running from source.md.
raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. Official 3.10.15+ is supported.")
elif sys.platform in ("win32", "darwin") and sys.version_info < (3, 10, 15):
raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. Official 3.11.9+ is supported.")
elif sys.platform in ("win32", "darwin") and sys.version_info < (3, 11, 13):
# There are known security issues, but no easy way to install fixed versions on Windows for testing.
warnings.warn(f"Python Version {sys.version_info} has security issues. Don't use in production.")
elif sys.version_info < (3, 10, 1):
elif sys.version_info < (3, 11, 0):
# Other platforms may get security backports instead of micro updates, so the number is unreliable.
raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. 3.10.1+ is supported.")
raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. 3.11.0+ is supported.")
# don't run update if environment is frozen/compiled or if not the parent process (skip in subprocess)
_skip_update = bool(
@@ -74,11 +74,11 @@ def update_command():
def install_pkg_resources(yes=False):
try:
import pkg_resources # noqa: F401
except ImportError:
except (AttributeError, ImportError):
check_pip()
if not yes:
confirm("pkg_resources not found, press enter to install it")
subprocess.call([sys.executable, "-m", "pip", "install", "--upgrade", "setuptools"])
subprocess.call([sys.executable, "-m", "pip", "install", "--upgrade", "setuptools>=75,<81"])
def update(yes: bool = False, force: bool = False) -> None:

View File

@@ -413,6 +413,12 @@ class Context:
self.logger.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text))
async_start(self.send_msgs(client, [{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments}]))
def notify_client_without_auth(self, client: Client, text: str, additional_arguments: dict = {}):
if client.auth:
return self.notify_client(client, text, additional_arguments)
self.logger.info("Notice (Unauthenticated Player): " + text)
async_start(self.send_msgs(client, [{"cmd": "PrintJSON", "data": [{ "text": text }], **additional_arguments}]))
def notify_client_multiple(self, client: Client, texts: typing.List[str], additional_arguments: dict = {}):
if not client.auth or client.no_text:
return
@@ -1858,6 +1864,13 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
await ctx.send_msgs(client, reply)
elif cmd == "GetDataPackage":
if not any(isinstance(extension, PerMessageDeflate) for extension in client.socket.extensions):
ctx.notify_client_without_auth(
client,
"Warning: your client does not support compressed websocket connections! "
"DataPackage (item and location names) were rejected to be transferred."
)
return
exclusions = args.get("exclusions", [])
if "games" in args:
games = {name: game_data for name, game_data in ctx.gamespackage.items()

View File

@@ -47,7 +47,7 @@ class Version(typing.NamedTuple):
return ".".join(str(item) for item in self)
__version__ = "0.6.3"
__version__ = "0.6.4"
version_tuple = tuplize_version(__version__)
is_linux = sys.platform.startswith("linux")
@@ -414,11 +414,11 @@ def get_adjuster_settings(game_name: str) -> Namespace:
@cache_argsless
def get_unique_identifier():
common_path = cache_path("common.json")
if os.path.exists(common_path):
try:
with open(common_path) as f:
common_file = json.load(f)
uuid = common_file.get("uuid", None)
else:
except FileNotFoundError:
common_file = {}
uuid = None
@@ -428,6 +428,9 @@ def get_unique_identifier():
from uuid import uuid4
uuid = str(uuid4())
common_file["uuid"] = uuid
cache_folder = os.path.dirname(common_path)
os.makedirs(cache_folder, exist_ok=True)
with open(common_path, "w") as f:
json.dump(common_file, f, separators=(",", ":"))
return uuid
@@ -900,7 +903,7 @@ def async_start(co: Coroutine[None, None, typing.Any], name: Optional[str] = Non
Use this to start a task when you don't keep a reference to it or immediately await it,
to prevent early garbage collection. "fire-and-forget"
"""
# https://docs.python.org/3.10/library/asyncio-task.html#asyncio.create_task
# https://docs.python.org/3.11/library/asyncio-task.html#asyncio.create_task
# Python docs:
# ```
# Important: Save a reference to the result of [asyncio.create_task],

View File

@@ -87,19 +87,22 @@ def start_playing():
@cache.cached()
def game_info(game, lang):
"""Game Info Pages"""
theme = get_world_theme(game)
secure_game_name = secure_filename(game)
lang = secure_filename(lang)
document = render_markdown(os.path.join(
app.static_folder, "generated", "docs",
secure_game_name, f"{lang}_{secure_game_name}.md"
))
return render_template(
"markdown_document.html",
title=f"{game} Guide",
html_from_markdown=document,
theme=theme,
)
try:
theme = get_world_theme(game)
secure_game_name = secure_filename(game)
lang = secure_filename(lang)
document = render_markdown(os.path.join(
app.static_folder, "generated", "docs",
secure_game_name, f"{lang}_{secure_game_name}.md"
))
return render_template(
"markdown_document.html",
title=f"{game} Guide",
html_from_markdown=document,
theme=theme,
)
except FileNotFoundError:
return abort(404)
@app.route('/games')
@@ -112,19 +115,31 @@ def games():
@app.route('/tutorial/<string:game>/<string:file>')
@cache.cached()
def tutorial(game: str, file: str):
theme = get_world_theme(game)
secure_game_name = secure_filename(game)
file = secure_filename(file)
document = render_markdown(os.path.join(
app.static_folder, "generated", "docs",
secure_game_name, file+".md"
))
return render_template(
"markdown_document.html",
title=f"{game} Guide",
html_from_markdown=document,
theme=theme,
)
try:
theme = get_world_theme(game)
secure_game_name = secure_filename(game)
file = secure_filename(file)
document = render_markdown(os.path.join(
app.static_folder, "generated", "docs",
secure_game_name, file+".md"
))
return render_template(
"markdown_document.html",
title=f"{game} Guide",
html_from_markdown=document,
theme=theme,
)
except FileNotFoundError:
return abort(404)
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
def tutorial_redirect(game: str, file: str, lang: str):
"""
Permanent redirect old tutorial URLs to new ones to keep search engines happy.
e.g. /tutorial/Archipelago/setup/en -> /tutorial/Archipelago/setup_en
"""
return redirect(url_for("tutorial", game=game, file=f"{file}_{lang}"), code=301)
@app.route('/tutorial/')

View File

@@ -28,7 +28,6 @@
font-weight: normal;
font-family: LondrinaSolid-Regular, sans-serif;
text-transform: uppercase;
cursor: pointer; /* TODO: remove once we drop showdown.js */
width: 100%;
text-shadow: 1px 1px 4px #000000;
}
@@ -37,7 +36,6 @@
font-size: 38px;
font-weight: normal;
font-family: LondrinaSolid-Light, sans-serif;
cursor: pointer; /* TODO: remove once we drop showdown.js */
width: 100%;
margin-top: 20px;
margin-bottom: 0.5rem;
@@ -50,7 +48,6 @@
font-family: LexendDeca-Regular, sans-serif;
text-transform: none;
text-align: left;
cursor: pointer; /* TODO: remove once we drop showdown.js */
width: 100%;
margin-bottom: 0.5rem;
}
@@ -59,7 +56,6 @@
font-family: LexendDeca-Regular, sans-serif;
text-transform: none;
font-size: 24px;
cursor: pointer; /* TODO: remove once we drop showdown.js */
margin-bottom: 24px;
}
@@ -67,14 +63,12 @@
font-family: LexendDeca-Regular, sans-serif;
text-transform: none;
font-size: 22px;
cursor: pointer; /* TODO: remove once we drop showdown.js */
}
.markdown h6, .markdown details summary.h6{
font-family: LexendDeca-Regular, sans-serif;
text-transform: none;
font-size: 20px;
cursor: pointer; /* TODO: remove once we drop showdown.js */
}
.markdown h4, .markdown h5, .markdown h6{

View File

@@ -11,32 +11,32 @@
<h1>Site Map</h1>
<h2>Base Pages</h2>
<ul>
<li><a href="/discord">Discord Link</a></li>
<li><a href="/faq/en">F.A.Q. Page</a></li>
<li><a href="/favicon.ico">Favicon</a></li>
<li><a href="/generate">Generate Game Page</a></li>
<li><a href="/">Homepage</a></li>
<li><a href="/uploads">Host Game Page</a></li>
<li><a href="/datapackage">Raw Data Package</a></li>
<li><a href="{{ url_for('check')}}">Settings Validator</a></li>
<li><a href="/sitemap">Site Map</a></li>
<li><a href="/start-playing">Start Playing</a></li>
<li><a href="/games">Supported Games Page</a></li>
<li><a href="/tutorial">Tutorials Page</a></li>
<li><a href="/user-content">User Content</a></li>
<li><a href="{{url_for('stats')}}">Game Statistics</a></li>
<li><a href="/glossary/en">Glossary</a></li>
<li><a href="{{url_for("show_session")}}">Session / Login</a></li>
<li><a href="{{ url_for('discord') }}">Discord Link</a></li>
<li><a href="{{ url_for('faq', lang='en') }}">F.A.Q. Page</a></li>
<li><a href="{{ url_for('favicon') }}">Favicon</a></li>
<li><a href="{{ url_for('generate') }}">Generate Game Page</a></li>
<li><a href="{{ url_for('landing') }}">Homepage</a></li>
<li><a href="{{ url_for('uploads') }}">Host Game Page</a></li>
<li><a href="{{ url_for('get_datapackage') }}">Raw Data Package</a></li>
<li><a href="{{ url_for('check') }}">Settings Validator</a></li>
<li><a href="{{ url_for('get_sitemap') }}">Site Map</a></li>
<li><a href="{{ url_for('start_playing') }}">Start Playing</a></li>
<li><a href="{{ url_for('games') }}">Supported Games Page</a></li>
<li><a href="{{ url_for('tutorial_landing') }}">Tutorials Page</a></li>
<li><a href="{{ url_for('user_content') }}">User Content</a></li>
<li><a href="{{ url_for('stats') }}">Game Statistics</a></li>
<li><a href="{{ url_for('glossary', lang='en') }}">Glossary</a></li>
<li><a href="{{ url_for('show_session') }}">Session / Login</a></li>
</ul>
<h2>Tutorials</h2>
<ul>
<li><a href="/tutorial/Archipelago/setup/en">Multiworld Setup Tutorial</a></li>
<li><a href="/tutorial/Archipelago/mac/en">Setup Guide for Mac</a></li>
<li><a href="/tutorial/Archipelago/commands/en">Server and Client Commands</a></li>
<li><a href="/tutorial/Archipelago/advanced_settings/en">Advanced YAML Guide</a></li>
<li><a href="/tutorial/Archipelago/triggers/en">Triggers Guide</a></li>
<li><a href="/tutorial/Archipelago/plando/en">Plando Guide</a></li>
<li><a href="{{ url_for('tutorial', game='Archipelago', file='setup_en') }}">Multiworld Setup Tutorial</a></li>
<li><a href="{{ url_for('tutorial', game='Archipelago', file='mac_en') }}">Setup Guide for Mac</a></li>
<li><a href="{{ url_for('tutorial', game='Archipelago', file='commands_en') }}">Server and Client Commands</a></li>
<li><a href="{{ url_for('tutorial', game='Archipelago', file='advanced_settings_en') }}">Advanced YAML Guide</a></li>
<li><a href="{{ url_for('tutorial', game='Archipelago', file='triggers_en') }}">Triggers Guide</a></li>
<li><a href="{{ url_for('tutorial', game='Archipelago', file='plando_en') }}">Plando Guide</a></li>
</ul>
<h2>Game Info Pages</h2>

View File

@@ -16,7 +16,7 @@ game contributions:
* **Do not introduce unit test failures/regressions.**
Archipelago supports multiple versions of Python. You may need to download older Python versions to fully test
your changes. Currently, the oldest supported version
is [Python 3.10](https://www.python.org/downloads/release/python-31015/).
is [Python 3.11](https://www.python.org/downloads/release/python-31113/).
It is recommended that automated github actions are turned on in your fork to have github run unit tests after
pushing.
You can turn them on here:

View File

@@ -344,7 +344,7 @@ names, and `def can_place_boss`, which passes a boss and location, allowing you
your game. When this function is called, `bosses`, `locations`, and the passed strings will all be lowercase. There is
also a `duplicate_bosses` attribute allowing you to define if a boss can be placed multiple times in your world. False
by default, and will reject duplicate boss names from the user. For an example of using this class, refer to
`worlds.alttp.options.py`
`worlds/alttp/Options.py`
### OptionDict
This option returns a dictionary. Setting a default here is recommended as it will output the dictionary to the

View File

@@ -7,7 +7,7 @@ use that version. These steps are for developers or platforms without compiled r
## General
What you'll need:
* [Python 3.10.11 or newer](https://www.python.org/downloads/), not the Windows Store version
* [Python 3.11.9 or newer](https://www.python.org/downloads/), not the Windows Store version
* On Windows, please consider only using the latest supported version in production environments since security
updates for older versions are not easily available.
* Python 3.12.x is currently the newest supported version

View File

@@ -754,7 +754,12 @@ class Settings(Group):
return super().__getattribute__(key)
# directly import world and grab settings class
world_mod, world_cls_name = _world_settings_name_cache[key].rsplit(".", 1)
world = cast(type, getattr(__import__(world_mod, fromlist=[world_cls_name]), world_cls_name))
try:
world = cast(type, getattr(__import__(world_mod, fromlist=[world_cls_name]), world_cls_name))
except AttributeError:
import warnings
warnings.warn(f"World {world_cls_name} failed to initialize properly.")
return super().__getattribute__(key)
assert getattr(world, "settings_key") == key
try:
cls_or_name = world.__annotations__["settings"]

View File

@@ -30,7 +30,7 @@ try:
install_cx_freeze = False
except pkg_resources.ResolutionError:
install_cx_freeze = True
except ImportError:
except (AttributeError, ImportError):
install_cx_freeze = True
pkg_resources = None # type: ignore[assignment]

View File

@@ -3,7 +3,7 @@ from typing import List, Optional, Tuple, Type, Union
from BaseClasses import CollectionState, Item, ItemClassification, Location, MultiWorld, Region
from worlds import network_data_package
from worlds.AutoWorld import World, call_all
from worlds.AutoWorld import World, WebWorld, call_all
gen_steps = (
"generate_early",
@@ -17,7 +17,7 @@ gen_steps = (
def setup_solo_multiworld(
world_type: Type[World], steps: Tuple[str, ...] = gen_steps, seed: Optional[int] = None
world_type: Type[World], steps: Tuple[str, ...] = gen_steps, seed: Optional[int] = None
) -> MultiWorld:
"""
Creates a multiworld with a single player of `world_type`, sets default options, and calls provided gen steps.
@@ -62,11 +62,16 @@ def setup_multiworld(worlds: Union[List[Type[World]], Type[World]], steps: Tuple
return multiworld
class TestWebWorld(WebWorld):
tutorials = []
class TestWorld(World):
game = f"Test Game"
item_name_to_id = {}
location_name_to_id = {}
hidden = True
web = TestWebWorld()
# add our test world to the data package, so we can test it later

View File

@@ -48,13 +48,14 @@ class TestBase(unittest.TestCase):
original_get_all_state = multiworld.get_all_state
def patched_get_all_state(use_cache: bool, allow_partial_entrances: bool = False):
def patched_get_all_state(use_cache: bool | None = None, allow_partial_entrances: bool = False,
**kwargs):
self.assertTrue(allow_partial_entrances, (
"Before the connect_entrances step finishes, other worlds might still have partial entrances. "
"As such, any call to get_all_state must use allow_partial_entrances = True."
))
return original_get_all_state(use_cache, allow_partial_entrances)
return original_get_all_state(use_cache, allow_partial_entrances, **kwargs)
multiworld.get_all_state = patched_get_all_state

View File

@@ -0,0 +1,63 @@
import urllib.parse
import html
import re
from flask import url_for
import WebHost
from . import TestBase
class TestSitemap(TestBase):
# Codes for OK and some redirects that we use
valid_status_codes = [200, 302, 308]
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
WebHost.copy_tutorials_files_to_static()
def test_sitemap_route(self) -> None:
"""Verify that the sitemap route works correctly and renders the template without errors."""
with self.app.test_request_context():
# Test the /sitemap route
with self.client.open("/sitemap") as response:
self.assertEqual(response.status_code, 200)
self.assertIn(b"Site Map", response.data)
# Test the /index route which should also serve the sitemap
with self.client.open("/index") as response:
self.assertEqual(response.status_code, 200)
self.assertIn(b"Site Map", response.data)
# Test using url_for with the function name
with self.client.open(url_for('get_sitemap')) as response:
self.assertEqual(response.status_code, 200)
self.assertIn(b'Site Map', response.data)
def test_sitemap_links(self) -> None:
"""
Verify that all links in the sitemap are valid by making a request to each one.
"""
with self.app.test_request_context():
with self.client.open(url_for("get_sitemap")) as response:
self.assertEqual(response.status_code, 200)
html_content = response.data.decode()
# Extract all href links using regex
href_pattern = re.compile(r'href=["\'](.*?)["\']')
links = href_pattern.findall(html_content)
self.assertTrue(len(links) > 0, "No links found in sitemap")
# Test each link
for link in links:
# Skip external links
if link.startswith(("http://", "https://")):
continue
link = urllib.parse.unquote(html.unescape(link))
with self.client.open(link) as response, self.subTest(link=link):
self.assertIn(response.status_code, self.valid_status_codes,
f"Link {link} returned invalid status code {response.status_code}")

View File

@@ -1,17 +1,46 @@
def load_tests(loader, standard_tests, pattern):
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from unittest import TestLoader, TestSuite
def load_tests(loader: "TestLoader", standard_tests: "TestSuite", pattern: str):
import os
import unittest
import fnmatch
from .. import file_path
from worlds.AutoWorld import AutoWorldRegister
suite = unittest.TestSuite()
suite.addTests(standard_tests)
# pattern hack
# all tests from within __init__ are always imported, so we need to filter out the folder earlier
# if the pattern isn't matching a specific world, we don't have much of a solution
if pattern.startswith("worlds."):
if pattern.endswith(".py"):
pattern = pattern[:-3]
components = pattern.split(".")
world_glob = f"worlds.{components[1]}"
pattern = components[-1]
elif pattern.startswith(f"worlds{os.path.sep}") or pattern.startswith(f"worlds{os.path.altsep}"):
components = pattern.split(os.path.sep)
if len(components) == 1:
components = pattern.split(os.path.altsep)
world_glob = f"worlds.{components[1]}"
pattern = components[-1]
else:
world_glob = "*"
folders = [os.path.join(os.path.split(world.__file__)[0], "test")
for world in AutoWorldRegister.world_types.values()]
for world in AutoWorldRegister.world_types.values()
if fnmatch.fnmatch(world.__module__, world_glob)]
all_tests = [
test_case for folder in folders if os.path.exists(folder)
for test_collection in loader.discover(folder, top_level_dir=file_path)
for test_collection in loader.discover(folder, top_level_dir=file_path, pattern=pattern)
for test_suite in test_collection if isinstance(test_suite, unittest.suite.TestSuite)
for test_case in test_suite
]

View File

@@ -37,7 +37,7 @@ class CV64Web(WebWorld):
tutorials = [Tutorial(
"Multiworld Setup Guide",
"A guide to setting up the Archipleago Castlevania 64 randomizer on your computer and connecting it to a "
"A guide to setting up the Archipelago Castlevania 64 randomizer on your computer and connecting it to a "
"multiworld.",
"English",
"setup_en.md",

View File

@@ -41,7 +41,7 @@ class CVCotMWeb(WebWorld):
tutorials = [Tutorial(
"Multiworld Setup Guide",
"A guide to setting up the Archipleago Castlevania: Circle of the Moon randomizer on your computer and "
"A guide to setting up the Archipelago Castlevania: Circle of the Moon randomizer on your computer and "
"connecting it to a multiworld.",
"English",
"setup_en.md",

View File

@@ -267,6 +267,10 @@ class DarkSouls3World(World):
# Don't allow missable duplicates of progression items to be expected progression.
if location.name in self.missable_dupe_prog_locs: continue
# Don't create DLC and NGP locations if those are disabled
if location.dlc and not self.options.enable_dlc: continue
if location.ngp and not self.options.enable_ngp: continue
# Replace non-randomized items with events that give the default item
event_item = (
self.create_item(location.default_item_name) if location.default_item_name

View File

@@ -89,11 +89,15 @@ class FF1Client(BizHawkClient):
async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
try:
if (await bizhawk.get_memory_size(ctx.bizhawk_ctx, self.rom)) < rom_name_location + 0x0D:
return False # ROM is not large enough to be a Final Fantasy 1 ROM
# Check ROM name/patch version
rom_name = ((await bizhawk.read(ctx.bizhawk_ctx, [(rom_name_location, 0x0D, self.rom)]))[0])
rom_name = rom_name.decode("ascii")
if rom_name != "FINAL FANTASY":
return False # Not a Final Fantasy 1 ROM
except UnicodeDecodeError:
return False # rom_name returned invalid text
except bizhawk.RequestFailedError:
return False # Not able to get a response, say no for now

View File

@@ -2,7 +2,7 @@
Archipelago does not have a compiled release on macOS. However, it is possible to run from source code on macOS. This guide expects you to have some experience with running software from the terminal.
## Prerequisite Software
Here is a list of software to install and source code to download.
1. Python 3.10 "universal2" or newer from the [macOS Python downloads page](https://www.python.org/downloads/macos/).
1. Python 3.11 "universal2" or newer from the [macOS Python downloads page](https://www.python.org/downloads/macos/).
**Python 3.13 is not supported yet.**
2. Xcode from the [macOS App Store](https://apps.apple.com/us/app/xcode/id497799835).
3. The source code from the [Archipelago releases page](https://github.com/ArchipelagoMW/Archipelago/releases).

View File

@@ -135,6 +135,10 @@ class JakAndDaxterContext(CommonContext):
self.tags = set()
await self.send_connect()
async def disconnect(self, allow_autoreconnect: bool = False):
self.locations_checked = set() # Clear this set to gracefully handle server disconnects.
await super(JakAndDaxterContext, self).disconnect(allow_autoreconnect)
def on_package(self, cmd: str, args: dict):
if cmd == "RoomInfo":
@@ -177,6 +181,10 @@ class JakAndDaxterContext(CommonContext):
create_task_log_exception(get_orb_balance())
# If there were any locations checked while the client wasn't connected, we want to make sure the server
# knows about them. To do that, replay the whole location_outbox (no duplicates will be sent).
self.memr.outbox_index = 0
# Tell the server if Deathlink is enabled or disabled in the in-game options.
# This allows us to "remember" the user's choice.
self.on_deathlink_toggle()
@@ -254,6 +262,7 @@ class JakAndDaxterContext(CommonContext):
# We don't need an ap_inform function because check_locations solves that need.
def on_location_check(self, location_ids: list[int]):
self.locations_checked.update(location_ids) # Populate this set to gracefully handle server disconnects.
create_task_log_exception(self.check_locations(location_ids))
# CommonClient has no finished_game function, so we will have to craft our own. TODO - Update if that changes.

View File

@@ -1,15 +1,15 @@
# Kingdom Hearts 2
<h2 style="text-transform:none";>Changes from the vanilla game</h2>
## Changes from the vanilla game
This randomizer creates a more dynamic play experience by randomizing the locations of most items in Kingdom Hearts 2. Currently all items within Chests, Popups, Get Bonuses, Form Levels, and Sora's Levels are randomized. This allows abilities that Sora would normally have to be placed on Keyblades with random stats. Additionally, there are several options for ways to finish the game, allowing for different goals beyond beating the final boss.
<h2 style="text-transform:none";>Where is the options page</h2>
## Where is the options page
The [player options page for this game](../player-options) contains all the options you need to configure and export a config file.
<h2 style="text-transform:none";>What is randomized in this game?</h2>
## What is randomized in this game?
- Chests
@@ -21,27 +21,27 @@ The [player options page for this game](../player-options) contains all the opti
- Keyblade Stats
- Keyblade Abilities
<h2 style="text-transform:none";>What Kingdom Hearts 2 items can appear in other players' worlds?</h2>
## What Kingdom Hearts 2 items can appear in other players' worlds?
Every item in the game except for abilities on weapons.
<h2 style="text-transform:none";>What is The Garden of Assemblage "GoA"?</h2>
## What is The Garden of Assemblage "GoA"?
The Garden of Assemblage Mod made by Sonicshadowsilver2 and Num turns the Garden of Assemblage into a “World Hub” where each portal takes you to one of the game worlds (as opposed to having a world map). This allows you to enter worlds at any time, and world progression is maintained for each world individually.
<h2 style="text-transform:none";>What does another world's item look like in Kingdom Hearts 2?</h2>
## What does another world's item look like in Kingdom Hearts 2?
In Kingdom Hearts 2, items which need to be sent to other worlds appear in any location that has a item in the vanilla game. They are represented by the Archipelago icon, and must be "picked up" as if it were a normal item. Upon obtaining the item, it will be sent to its home world.
<h2 style="text-transform:none";>When the player receives an item, what happens?</h2>
## When the player receives an item, what happens?
It is added to your inventory. If you obtain magic, you will need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable.
<h2 style="text-transform:none";>What Happens if I die before Room Saving?</h2>
## What Happens if I die before Room Saving?
When you die in vanilla Kingdom Hearts 2, you are reverted to the last non-boss room you entered and your status is reverted to what it was at that time. However, in archipelago, any item that you have sent/received will not be taken away from the player, any chest you have opened will remain open, and you will keep your level, but lose the experience.
@@ -49,7 +49,7 @@ When you die in vanilla Kingdom Hearts 2, you are reverted to the last non-boss
For example, if you are fighting Roxas, receive Reflect Element, then die mid-fight, you will keep that Reflect Element. You will still need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable.
<h2 style="text-transform:none";>Customization options:</h2>
## Customization options:
- Choose a goal from the list below (with an additional option to Kill Final Xemnas alongside your goal).
@@ -64,11 +64,11 @@ For example, if you are fighting Roxas, receive Reflect Element, then die mid-fi
- Customize the amount and level of progressive movement (Growth Abilities) you start with.
- Customize start inventory, i.e., begin every run with certain items or spells of your choice.
<h2 style="text-transform:none";>What are Lucky Emblems?</h2>
## What are Lucky Emblems?
Lucky Emblems are items that are required to beat the game if your goal is "Lucky Emblem Hunt".<br>
You can think of these as requiring X number of Proofs of Nonexistence to open the final door.
<h2 style="text-transform:none";>What is Hitlist/Bounties?</h2>
## What is Hitlist/Bounties?
The Hitlist goal adds "bounty" items to select late-game fights and locations, and you need to collect X number of them to win.<br>
The list of possible locations that can contain a bounty:
@@ -82,7 +82,7 @@ The list of possible locations that can contain a bounty:
- Transport to Remembrance
- Godess of Fate cup and Hades Paradox cup
<h2 style="text-transform:none";>Quality of life:</h2>
## Quality of life:
With the help of Shananas, Num, and ZakTheRobot we have many QoL features such are:

View File

@@ -1,11 +1,11 @@
# Kingdom Hearts 2 Archipelago Setup Guide
<h2 style="text-transform:none";>Quick Links</h2>
## Quick Links
- [Game Info Page](../../../../games/Kingdom%20Hearts%202/info/en)
- [Player Options Page](../../../../games/Kingdom%20Hearts%202/player-options)
<h2 style="text-transform:none";>Required Software:</h2>
## Required Software:
Kingdom Hearts II Final Mix from the [Epic Games Store](https://store.epicgames.com/en-US/discover/kingdom-hearts) or [Steam](https://store.steampowered.com/app/2552430/KINGDOM_HEARTS_HD_1525_ReMIX/)
@@ -23,39 +23,39 @@ Kingdom Hearts II Final Mix from the [Epic Games Store](https://store.epicgames.
1. Optionally Install the Archipelago Quality Of Life mod from `JaredWeakStrike/AP_QOL` using OpenKH Mod Manager
2. Optionally Install the Quality Of Life mod from `shananas/BearSkip` using OpenKH Mod Manager
<h3 style="text-transform:none";>Required: Archipelago Companion Mod</h3>
### Required: Archipelago Companion Mod
Load this mod just like the <b>GoA ROM</b> you did during the KH2 Rando setup. `JaredWeakStrike/APCompanion`<br>
Have this mod second-highest priority below the .zip seed.<br>
This mod is based upon Num's Garden of Assemblage Mod and requires it to work. Without Num this could not be possible.
<h3 style="text-transform:none";>Required: Auto Save Mod and KH2 Lua Library</h3>
### Required: Auto Save Mod and KH2 Lua Library
Load these mods just like you loaded the GoA ROM mod during the KH2 Rando setup. `KH2FM-Mods-equations19/auto-save` and `KH2FM-Mods-equations19/KH2-Lua-Library` Location doesn't matter, required in case of crashes. See [Best Practices](en#best-practices) on how to load the auto save
Load these mods just like you loaded the GoA ROM mod during the KH2 Rando setup. `KH2FM-Mods-equations19/auto-save` and `KH2FM-Mods-equations19/KH2-Lua-Library` Location doesn't matter, required in case of crashes. See [Best Practices](#best-practices) on how to load the auto save
<h3 style="text-transform:none";>Optional QoL Mods: AP QoL and Bear Skip</h3>
### Optional QoL Mods: AP QoL and Bear Skip
`JaredWeakStrike/AP_QOL` Makes the urns minigames much faster, makes Cavern of Remembrance orbs drop significantly more drive orbs for refilling drive/leveling master form, skips the animation when using the bulky vendor RC, skips carpet escape auto-scroller in Agrabah 2, and prevents the wardrobe in the Beasts Castle wardrobe push minigame from waking up while being pushed.
`shananas/BearSkip` Skips all minigames in 100 Acre Woods except the Spooky Cave minigame since there are chests in Spooky Cave you can only get during the minigame. For Spooky Cave, Pooh is moved to the other side of the invisible wall that prevents you from using his RC to finish the minigame.
<h3 style="text-transform:none";>Installing A Seed</h3>
### Installing A Seed
When you generate a game you will see a download link for a KH2 .zip seed on the room page. Download the seed then open OpenKH Mod Manager and click the green plus and "Select and install Mod Archive".<br>
Make sure the seed is on the top of the list (Highest Priority)<br>
After Installing the seed click "Mod Loader -> Build/Build and Run". Every slot is a unique mod to install and will be needed be repatched for different slots/rooms.
<h2 style="text-transform:none";>Optional Software:</h2>
## Optional Software:
- [Kingdom Hearts 2 AP Tracker](https://github.com/palex00/kh2-ap-tracker/releases/latest/), for use with
[PopTracker](https://github.com/black-sliver/PopTracker/releases)
<h2 style="text-transform:none";>What the Mod Manager Should Look Like.</h2>
## What the Mod Manager Should Look Like.
![image](https://i.imgur.com/N0WJ8Qn.png)
<h2 style="text-transform:none";>Using the KH2 Client</h2>
## Using the KH2 Client
Start the game through OpenKH Mod Manager. If starting a new run, enter the Garden of Assemblage from a new save. If returning to a run, load the save and enter the Garden of Assemblage. Then run the [ArchipelagoKH2Client.exe](https://github.com/ArchipelagoMW/Archipelago/releases).<br>
When you successfully connect to the server the client will automatically hook into the game to send/receive checks. <br>
@@ -67,13 +67,13 @@ Most checks will be sent to you anywhere outside a load or cutscene.<br>
If you obtain magic, you will need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable.
<h2 style="text-transform:none";>KH2 Client should look like this: </h2>
## KH2 Client should look like this:
![image](https://i.imgur.com/qP6CmV8.png)
Enter The room's port number into the top box <b> where the x's are</b> and press "Connect". Follow the prompts there and you should be connected
<h2 style="text-transform:none";>Common Pitfalls</h2>
## Common Pitfalls
- Having an old GOA Lua Script in your `C:\Users\*YourName*\Documents\KINGDOM HEARTS HD 1.5+2.5 ReMIX\scripts\kh2` folder.
- Pressing F2 while in game should look like this. ![image](https://i.imgur.com/ABSdtPC.png)
@@ -86,7 +86,7 @@ Enter The room's port number into the top box <b> where the x's are</b> and pres
- Using a seed from the standalone KH2 Randomizer Seed Generator.
- The Archipelago version of the KH2 Randomizer does not use this Seed Generator; refer to the [Archipelago Setup](https://archipelago.gg/tutorial/Archipelago/setup/en) to learn how to generate and play a seed through Archipelago.
<h2 style="text-transform:none"; >Best Practices</h2>
## Best Practices
- Make a save at the start of the GoA before opening anything. This will be the file to select when loading an autosave if/when your game crashes.
- If you don't want to have a save in the GoA. Disconnect the client, load the auto save, and then reconnect the client after it loads the auto save.
@@ -94,13 +94,13 @@ Enter The room's port number into the top box <b> where the x's are</b> and pres
- Run the game in windows/borderless windowed mode. Fullscreen is stable but the game can crash if you alt-tab out.
- Make sure to save in a different save slot when playing in an async or disconnecting from the server to play a different seed
<h2 style="text-transform:none";>Logic Sheet & PopTracker Autotracking</h2>
## Logic Sheet & PopTracker Autotracking
Have any questions on what's in logic? This spreadsheet made by Bulcon has the answer [Requirements/logic sheet](https://docs.google.com/spreadsheets/d/1nNi8ohEs1fv-sDQQRaP45o6NoRcMlLJsGckBonweDMY/edit?usp=sharing)
Alternatively you can use the Kingdom Hearts 2 PopTracker Pack that is based off of the logic sheet above and does all the work for you.
<h3 style="text-transform:none";>PopTracker Pack</h3>
### PopTracker Pack
1. Download [Kingdom Hearts 2 AP Tracker](https://github.com/palex00/kh2-ap-tracker/releases/latest/) and
[PopTracker](https://github.com/black-sliver/PopTracker/releases).
@@ -112,7 +112,7 @@ Alternatively you can use the Kingdom Hearts 2 PopTracker Pack that is based off
This pack will handle logic, received items, checked locations and autotabbing for you!
<h2 style="text-transform:none";>F.A.Q.</h2>
## F.A.Q.
- Why is my Client giving me a "Cannot Open Process: " error?
- Due to how the client reads kingdom hearts 2 memory some people's computer flags it as a virus. Run the client as admin.

View File

@@ -34,62 +34,75 @@ business!
## I don't know what to do!
That's not a question - but I'd suggest clicking the crow icon on your client, which will load an AP compatible autotracker for LADXR.
That's not a question - but I'd suggest clicking the **Open Tracker** button in your client, which will load an AP compatible autotracker for LADXR.
## What is this randomizer based on?
This randomizer is based on (forked from) the wonderful work daid did on LADXR - https://github.com/daid/LADXR
This randomizer is based on (forked from) the wonderful work daid did on [LADXR](https://github.com/daid/LADXR)
The autotracker code for communication with magpie tracker is directly copied from kbranch's repo - https://github.com/kbranch/Magpie/tree/master/autotracking
The autotracker code for communication with magpie tracker is directly copied from [kbranch's repo](https://github.com/kbranch/Magpie)
### Graphics
The following sprite sheets have been included with permission of their respective authors:
* by Madam Materia (https://www.twitch.tv/isabelle_zephyr)
* by [Madam Materia](https://www.twitch.tv/isabelle_zephyr)
* Matty_LA
* by Linker (https://twitter.com/BenjaminMaksym)
* Bowwow
* Bunny
* Luigi
* Mario
* Richard
* Tarin
Title screen graphics by toomanyteeth✨ (https://instagram.com/toomanyyyteeth)
Title screen graphics by [toomanyteeth✨](https://instagram.com/toomanyyyteeth)
## Some tips from LADXR...
<h3>Locations</h3>
<p>All chests and dungeon keys are always randomized. Also, the 3 songs (Marin, Mambo, and Manu) give a you an item if you present them the Ocarina. The seashell mansion 20 shells reward is also shuffled, but the 5 and 10 shell reward is not, as those can be missed.</p>
<p>The moblin cave with Bowwow contains a chest instead. The color dungeon gives 2 items at the end instead of a choice of tunic. Other item locations are: The toadstool, the reward for delivering the toadstool, hidden seashells, heart pieces, heart containers, golden leaves, the Mad Batters (capacity upgrades), the shovel/bow in the shop, the rooster's grave, and all of the keys' (tail,slime,angler,face,bird) locations.</p>
<p>Finally, new players often forget the following locations: the heart piece hidden in the water at the castle, the heart piece hidden in the bomb cave (screen before the honey), bonk seashells (run with pegasus boots against the tree in at the Tail Cave, and the tree right of Mabe Village, next to the phone booth), and the hookshop drop from Master Stalfos in D5.</p>
### Locations
<h3>Color Dungeon</h3>
<p>The Color Dungeon is part of the item shuffle, and the red/blue tunics are shuffled in the item pool. Which means the fairy at the end of the color dungeon gives out two random items.</p>
<p>To access the color dungeon, you need the power bracelet, and you need to push the gravestones in the right order: "down, left, up, right, up", going from the lower right gravestone, to the one left of it, above it, and then to the right.</p>
All chests and dungeon keys are always randomized. Also, the 3 songs (Marin, Mambo, and Manu) give a you an item if you present them the Ocarina. The seashell mansion 20 shells reward is also shuffled, but the 5 and 10 shell reward is not, as those can be missed.
<h3>Bowwow</h3>
<p>Bowwow is in a chest, somewhere. After you find him, he will always be in the swamp with you, but not anywhere else.</p>
The moblin cave with Bowwow contains a chest instead. The color dungeon gives 2 items at the end instead of a choice of tunic. Other item locations are: The toadstool, the reward for delivering the toadstool, hidden seashells, heart pieces, heart containers, golden leaves, the Mad Batters (capacity upgrades), the shovel/bow in the shop, the rooster's grave, and all of the keys' (tail,slime,angler,face,bird) locations.
<h3>Added things</h3>
<p>In your save and quit menu, there is a 3rd option to return to your home. This has two main uses: it speeds up the game, and prevents softlocks (common in entrance rando).</p>
<p>If you have weapons that require ammunition (bombs, powder, arrows), a ghost will show up inside Marin's house. He will refill you up to 10 ammunition, so you do not run out.</p>
<p>The flying rooster is (optionally) available as an item.</p>
<p>You can access the Bird Key cave item with the L2 Power Bracelet.</p>
<p>Boomerang cave is now a random item gift by default (available post-bombs), and boomerang is in the item pool.</p>
<p>Your inventory has been increased by four, to accommodate these items now coexisting with eachother.</p>
Finally, new players often forget the following locations: the heart piece hidden in the water at the castle, the heart piece hidden in the bomb cave (screen before the honey), bonk seashells (run with pegasus boots against the tree in at the Tail Cave, and the tree right of Mabe Village, next to the phone booth), and the hookshop drop from Master Stalfos in D5.
<h3>Removed things</h3>
<p>The ghost mini-quest after D4 never shows up, his seashell reward is always available.</p>
<p>The walrus is moved a bit, so that you can access the desert without taking Marin on a date.</p>
### Color Dungeon
<h3>Logic</h3>
<p>Depending on your options, you can only steal after you find the sword, always, or never.</p>
<p>Do not forget that there are two items in the rafting ride. You can access this with just Hookshot or Flippers.</p>
<p>Killing enemies with bombs is in normal logic. You can switch to casual logic if you do not want this.</p>
<p>D7 confuses some people, but by dropping down pits on the 2nd floor you can access almost all of this dungeon, even without feather and power bracelet.</p>
The Color Dungeon is part of the item shuffle, and the red/blue tunics are shuffled in the item pool. Which means the fairy at the end of the color dungeon gives out two random items.
<h3>Tech</h3>
<p>The toadstool and magic powder used to be the same type of item. LADXR turns this into two items that you can have a the same time. 4 extra item slots in your inventory were added to support this extra item, and have the ability to own the boomerang.</p>
<p>The glitch where the slime key is effectively a 6th golden leaf is fixed, and golden leaves can be collected fine next to the slime key.</p>
To access the color dungeon, you need the power bracelet, and you need to push the gravestones in the right order: "down, left, up, right, up", going from the lower right gravestone, to the one left of it, above it, and then to the right.
### Bowwow
Bowwow is in a chest, somewhere. After you find him, he will always be in the swamp with you, but not anywhere else.
### Added things
In your save and quit menu, there is a 3rd option to return to your home. This has two main uses: it speeds up the game, and prevents softlocks (common in entrance rando).
If you have weapons that require ammunition (bombs, powder, arrows), a ghost will show up inside Marin's house. He will refill you up to 10 ammunition, so you do not run out.
The flying rooster is (optionally) available as an item.
If the rooster is disabled, you can access the Bird Key cave item with the L2 Power Bracelet.
Boomerang cave is now a random item gift by default (available post-bombs), and boomerang is in the item pool.
Your inventory has been increased by four, to accommodate these items now coexisting with eachother.
### Removed things
The ghost mini-quest after D4 never shows up, his seashell reward is always available.
The walrus is moved a bit, so that you can access the desert without taking Marin on a date.
### Logic
You can only steal after you find the sword.
Do not forget that there are two items in the rafting ride. You can access this with just Hookshot or Flippers.
Killing enemies with bombs is in logic.
D7 confuses some people, but by dropping down pits on the 2nd floor you can access almost all of this dungeon, even without feather and power bracelet.
### Tech
The toadstool and magic powder used to be the same type of item. LADXR turns this into two items that you can have a the same time. 4 extra item slots in your inventory were added to support this extra item, and have the ability to own the boomerang.
The glitch where the slime key is effectively a 6th golden leaf is fixed, and golden leaves can be collected fine next to the slime key.

View File

@@ -3,6 +3,8 @@ import typing
from BaseClasses import Location
task_types = ["prayer", "magic", "runecraft", "mining", "crafting", "smithing", "fishing", "cooking", "firemaking", "woodcutting", "combat"]
class SkillRequirement(typing.NamedTuple):
skill: str
level: int

View File

@@ -8,7 +8,7 @@ import requests
# The CSVs are updated at this repository to be shared between generator and client.
data_repository_address = "https://raw.githubusercontent.com/digiholic/osrs-archipelago-logic/"
# The Github tag of the CSVs this was generated with
data_csv_tag = "v2.0.4"
data_csv_tag = "v2.0.5"
# If true, generate using file names in the repository
debug = False

View File

@@ -77,7 +77,7 @@ location_rows = [
LocationRow('Bake a Redberry Pie', 'cooking', ['Redberry Bush', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 10), ], [], 0),
LocationRow('Cook some Stew', 'cooking', ['Bowl', 'Meat', 'Potato', ], [SkillRequirement('Cooking', 25), ], [], 0),
LocationRow('Bake an Apple Pie', 'cooking', ['Cooking Apple', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 32), ], [], 2),
LocationRow('Enter the Cook\'s Guild', 'cooking', ['Cook\'s Guild', ], [], [], 0),
LocationRow('Enter the Cook\'s Guild', 'cooking', ['Cook\'s Guild', ], [SkillRequirement('Cooking', 32), ], [], 0),
LocationRow('Bake a Cake', 'cooking', ['Wheat', 'Windmill', 'Egg', 'Milk', 'Cake Tin', ], [SkillRequirement('Cooking', 40), ], [], 6),
LocationRow('Bake a Meat Pizza', 'cooking', ['Wheat', 'Windmill', 'Cheese', 'Tomato', 'Meat', ], [SkillRequirement('Cooking', 45), ], [], 8),
LocationRow('Burn a Log', 'firemaking', [], [SkillRequirement('Firemaking', 1), SkillRequirement('Woodcutting', 1), ], [], 0),

View File

@@ -2,7 +2,7 @@ from dataclasses import dataclass
from Options import Choice, Toggle, Range, PerGameCommonOptions
MAX_COMBAT_TASKS = 16
MAX_COMBAT_TASKS = 17
MAX_PRAYER_TASKS = 5
MAX_MAGIC_TASKS = 7

View File

@@ -190,6 +190,8 @@ def get_firemaking_skill_rule(level, player, options) -> CollectionRule:
def get_skill_rule(skill, level, player, options) -> CollectionRule:
if level <= 1:
return lambda state: True
if skill.lower() == "fishing":
return get_fishing_skill_rule(level, player, options)
if skill.lower() == "mining":

View File

@@ -1,11 +1,11 @@
import typing
from BaseClasses import Item, Tutorial, ItemClassification, Region, MultiWorld, CollectionState
from Fill import fill_restrictive, FillError
from BaseClasses import Item, Tutorial, ItemClassification, Region, MultiWorld
from worlds.AutoWorld import WebWorld, World
from Options import OptionError
from .Items import OSRSItem, starting_area_dict, chunksanity_starting_chunks, QP_Items, ItemRow, \
chunksanity_special_region_names
from .Locations import OSRSLocation, LocationRow
from .Locations import OSRSLocation, LocationRow, task_types
from .Rules import *
from .Options import OSRSOptions, StartingArea
from .Names import LocationNames, ItemNames, RegionNames
@@ -47,6 +47,7 @@ class OSRSWorld(World):
base_id = 0x070000
data_version = 1
explicit_indirect_conditions = False
ut_can_gen_without_yaml = True
item_name_to_id = {item_rows[i].name: 0x070000 + i for i in range(len(item_rows))}
location_name_to_id = {location_rows[i].name: 0x070000 + i for i in range(len(location_rows))}
@@ -105,6 +106,18 @@ class OSRSWorld(World):
# Set Starting Chunk
self.multiworld.push_precollected(self.create_item(self.starting_area_item))
elif hasattr(self.multiworld,"re_gen_passthrough") and self.game in self.multiworld.re_gen_passthrough:
re_gen_passthrough = self.multiworld.re_gen_passthrough[self.game] # UT passthrough
if "starting_area" in re_gen_passthrough:
self.starting_area_item = re_gen_passthrough["starting_area"]
for task_type in task_types:
if f"max_{task_type}_level" in re_gen_passthrough:
getattr(self.options,f"max_{task_type}_level").value = re_gen_passthrough[f"max_{task_type}_level"]
max_count = getattr(self.options,f"max_{task_type}_tasks")
max_count.value = max_count.range_end
self.options.brutal_grinds.value = re_gen_passthrough["brutal_grinds"]
"""
This function pulls from LogicCSVToPython so that it sends the correct tag of the repository to the client.
@@ -115,20 +128,13 @@ class OSRSWorld(World):
data = self.options.as_dict("brutal_grinds")
data["data_csv_tag"] = data_csv_tag
data["starting_area"] = str(self.starting_area_item) #these aren't actually strings, they just play them on tv
for task_type in task_types:
data[f"max_{task_type}_level"] = getattr(self.options,f"max_{task_type}_level").value
return data
def interpret_slot_data(self, slot_data: typing.Dict[str, typing.Any]) -> None:
if "starting_area" in slot_data:
self.starting_area_item = slot_data["starting_area"]
menu_region = self.multiworld.get_region("Menu",self.player)
menu_region.exits.clear() #prevent making extra exits if players just reconnect to a differnet slot
if self.starting_area_item in chunksanity_special_region_names:
starting_area_region = chunksanity_special_region_names[self.starting_area_item]
else:
starting_area_region = self.starting_area_item[6:] # len("Area: ")
starting_entrance = menu_region.create_exit(f"Start->{starting_area_region}")
starting_entrance.access_rule = lambda state: state.has(self.starting_area_item, self.player)
starting_entrance.connect(self.region_name_to_data[starting_area_region])
@staticmethod
def interpret_slot_data(slot_data: typing.Dict[str, typing.Any]) -> typing.Dict[str, typing.Any]:
return slot_data
def create_regions(self) -> None:
"""
@@ -195,6 +201,8 @@ class OSRSWorld(World):
generation_is_fake = hasattr(self.multiworld, "generation_is_fake") # UT specific override
locations_required = 0
for item_row in item_rows:
if item_row.name == self.starting_area_item:
continue #skip starting area
# If it's a filler item, set it aside for later
if item_row.progression == ItemClassification.filler:
continue
@@ -206,15 +214,18 @@ class OSRSWorld(World):
locations_required += item_row.amount
if self.options.enable_duds: locations_required += self.options.dud_count
locations_added = 1 # At this point we've already added the starting area, so we start at 1 instead of 0
locations_added = 0 # Keep track of the number of locations we add so we don't add more the number of items we're going to make
# Quests are always added first, before anything else is rolled
for i, location_row in enumerate(location_rows):
if location_row.category in {"quest", "points", "goal"}:
if location_row.category in {"quest"}:
if self.task_within_skill_levels(location_row.skills):
self.create_and_add_location(i)
if location_row.category == "quest":
locations_added += 1
locations_added += 1
elif location_row.category in {"goal"}:
if not self.task_within_skill_levels(location_row.skills):
raise OptionError(f"Goal location for {self.player_name} not allowed in skill levels") #it doesn't actually have any, but just in case for future
self.create_and_add_location(i)
# Build up the weighted Task Pool
rnd = self.random
@@ -225,18 +236,28 @@ class OSRSWorld(World):
rnd.shuffle(general_tasks)
else:
general_tasks.reverse()
for i in range(self.options.minimum_general_tasks):
general_tasks_added = 0
while general_tasks_added<self.options.minimum_general_tasks and general_tasks:
task = general_tasks.pop()
self.add_location(task)
locations_added += 1
if self.task_within_skill_levels(task.skills):
self.add_location(task)
locations_added += 1
general_tasks_added += 1
while generation_is_fake and len(general_tasks)>0:
task = general_tasks.pop()
if self.task_within_skill_levels(task.skills):
self.add_location(task)
locations_added += 1
general_tasks_added += 1
if general_tasks_added < self.options.minimum_general_tasks:
raise OptionError(f"{self.plyaer_name} doesn't have enough general tasks to create required minimum count"+
f", raise maximum skill levels or lower minimum general tasks")
general_weight = self.options.general_task_weight if len(general_tasks) > 0 else 0
general_weight = self.options.general_task_weight.value if len(general_tasks) > 0 else 0
tasks_per_task_type: typing.Dict[str, typing.List[LocationRow]] = {}
weights_per_task_type: typing.Dict[str, int] = {}
task_types = ["prayer", "magic", "runecraft", "mining", "crafting",
"smithing", "fishing", "cooking", "firemaking", "woodcutting", "combat"]
for task_type in task_types:
max_amount_for_task_type = getattr(self.options, f"max_{task_type}_tasks")
tasks_for_this_type = [task for task in self.locations_by_category[task_type]
@@ -263,10 +284,13 @@ class OSRSWorld(World):
all_weights.append(weights_per_task_type[task_type])
# Even after the initial forced generals, they can still be rolled randomly
if general_weight > 0:
if general_weight > 0 and len(general_tasks)>0:
all_tasks.append(general_tasks)
all_weights.append(general_weight)
if not generation_is_fake and locations_added > locations_required: #due to minimum general tasks we already have more than needed
raise OptionError(f"Too many locations created for {self.player_name}, lower the minimum general tasks")
while locations_added < locations_required or (generation_is_fake and len(all_tasks) > 0):
if all_tasks:
chosen_task = rnd.choices(all_tasks, all_weights)[0]
@@ -282,9 +306,9 @@ class OSRSWorld(World):
del all_tasks[index]
del all_weights[index]
else:
else: # We can ignore general tasks in UT because they will have been cleared already
if len(general_tasks) == 0:
raise Exception(f"There are not enough available tasks to fill the remaining pool for OSRS " +
raise OptionError(f"There are not enough available tasks to fill the remaining pool for OSRS " +
f"Please adjust {self.player_name}'s settings to be less restrictive of tasks.")
task = general_tasks.pop()
self.add_location(task)
@@ -296,7 +320,7 @@ class OSRSWorld(World):
self.create_and_add_location(index)
def create_items(self) -> None:
filler_items = []
filler_items:list[ItemRow] = []
for item_row in item_rows:
if item_row.name != self.starting_area_item:
# If it's a filler item, set it aside for later
@@ -321,7 +345,7 @@ class OSRSWorld(World):
def get_filler_item_name(self) -> str:
if self.options.enable_duds:
return self.random.choice([item for item in item_rows if item.progression == ItemClassification.filler])
return self.random.choice([item.name for item in item_rows if item.progression == ItemClassification.filler])
else:
return self.random.choice([ItemNames.Progressive_Weapons, ItemNames.Progressive_Magic,
ItemNames.Progressive_Range_Weapon, ItemNames.Progressive_Armor,
@@ -388,6 +412,12 @@ class OSRSWorld(World):
# Set the access rule for the QP Location
add_rule(qp_loc, lambda state, loc=q_loc: (loc.can_reach(state)))
qp = 0
for qp_event in self.available_QP_locations:
qp += int(qp_event[0])
if qp < self.location_rows_by_name[LocationNames.Q_Dragon_Slayer].qp:
raise OptionError(f"{self.player_name} doesn't have enough quests for reach goal, increase maximum skill levels")
# place "Victory" at "Dragon Slayer" and set collection as win condition
self.multiworld.get_location(LocationNames.Q_Dragon_Slayer, self.player) \
.place_locked_item(self.create_event("Victory"))

View File

@@ -255,8 +255,10 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
else:
dead_ends.append(portal)
dead_end_direction_tracker[portal.direction] += 1
if portal.region == "Zig Skip Exit" and entrance_layout == EntranceLayout.option_fixed_shop:
if (portal.region == "Zig Skip Exit" and entrance_layout == EntranceLayout.option_fixed_shop
and not decoupled):
# direction isn't meaningful here since zig skip cannot be in direction pairs mode
# don't add it in decoupled
two_plus.append(portal)
# now we generate the shops and add them to the dead ends list

View File

@@ -168,8 +168,8 @@ class ZillionWorld(World):
def create_regions(self) -> None:
assert self.zz_system.randomizer, "generate_early hasn't been called"
assert self.id_to_zz_item, "generate_early hasn't been called"
p = self.player
logic_cache = ZillionLogicCache(p, self.zz_system.randomizer, self.id_to_zz_item)
player = self.player
logic_cache = ZillionLogicCache(player, self.zz_system.randomizer, self.id_to_zz_item)
self.logic_cache = logic_cache
w = self.multiworld
self.my_locations = []
@@ -192,7 +192,7 @@ class ZillionWorld(World):
all_regions: dict[str, ZillionRegion] = {}
for here_zz_name, zz_r in self.zz_system.randomizer.regions.items():
here_name = "Menu" if here_zz_name == "start" else zz_reg_name_to_reg_name(here_zz_name)
all_regions[here_name] = ZillionRegion(zz_r, here_name, here_name, p, w)
all_regions[here_name] = ZillionRegion(zz_r, here_name, here_name, player, w)
self.multiworld.regions.append(all_regions[here_name])
limited_skill = Req(gun=3, jump=3, skill=self.zz_system.randomizer.options.skill, hp=940, red=1, floppy=126)
@@ -239,7 +239,7 @@ class ZillionWorld(World):
for zz_dest in zz_here.connections.keys():
dest_name = "Menu" if zz_dest.name == "start" else zz_reg_name_to_reg_name(zz_dest.name)
dest = all_regions[dest_name]
exit_ = Entrance(p, f"{here_name} to {dest_name}", here)
exit_ = Entrance(player, f"{here_name} to {dest_name}", here)
here.exits.append(exit_)
exit_.connect(dest)
@@ -248,6 +248,11 @@ class ZillionWorld(World):
if self.options.priority_dead_ends.value:
self.options.priority_locations.value |= {loc.name for loc in dead_end_locations}
# main location name is an alias
main_loc_name = self.zz_system.randomizer.loc_name_2_pretty[self.zz_system.randomizer.locations["main"].name]
self.multiworld.get_location(main_loc_name, player).place_locked_item(self.create_item("Win"))
self.multiworld.completion_condition[player] = lambda state: state.has("Win", player)
@override
def create_items(self) -> None:
if not self.id_to_zz_item:
@@ -272,17 +277,6 @@ class ZillionWorld(World):
self.logger.debug(f"Zillion Items: {item_name} 1")
self.multiworld.itempool.append(self.create_item(item_name))
@override
def generate_basic(self) -> None:
assert self.zz_system.randomizer, "generate_early hasn't been called"
# main location name is an alias
main_loc_name = self.zz_system.randomizer.loc_name_2_pretty[self.zz_system.randomizer.locations["main"].name]
self.multiworld.get_location(main_loc_name, self.player)\
.place_locked_item(self.create_item("Win"))
self.multiworld.completion_condition[self.player] = \
lambda state: state.has("Win", self.player)
@staticmethod
def stage_generate_basic(multiworld: MultiWorld, *args: Any) -> None: # noqa: ANN401
# item link pools are about to be created in main