mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-09 09:03:46 -07:00
Compare commits
119 Commits
core_multi
...
NewSoupVi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2fcaec6ad | ||
|
|
ec1e113b4c | ||
|
|
347efac0cd | ||
|
|
b7b5bf58aa | ||
|
|
a324c97815 | ||
|
|
f263a0bc91 | ||
|
|
6a9299018c | ||
|
|
ee471a48bd | ||
|
|
879d7c23b7 | ||
|
|
934b09238e | ||
|
|
1fd8e4435e | ||
|
|
50fd42d0c2 | ||
|
|
399958c881 | ||
|
|
78c93d7e39 | ||
|
|
e3b8a60584 | ||
|
|
b7263edfd0 | ||
|
|
1ee749b352 | ||
|
|
f93734f9e3 | ||
|
|
e211dfa1c2 | ||
|
|
0f7deb1d2a | ||
|
|
f2cb16a5be | ||
|
|
98477e27aa | ||
|
|
4149db1a01 | ||
|
|
9ac921380f | ||
|
|
286e24629f | ||
|
|
ab2efc0c5c | ||
|
|
60d6078e1f | ||
|
|
f94492b2d3 | ||
|
|
f03bb61747 | ||
|
|
dc4e8bae98 | ||
|
|
ac26f8be8b | ||
|
|
8c79499573 | ||
|
|
63fbcc5fc8 | ||
|
|
cad217af19 | ||
|
|
a6ad4a8293 | ||
|
|
503999cb32 | ||
|
|
c2d8f2443e | ||
|
|
4571ed7e2f | ||
|
|
ef5cbd3ba3 | ||
|
|
5c162bd7ce | ||
|
|
7bdaaa25c1 | ||
|
|
9a5a02b654 | ||
|
|
4fea6b6e9b | ||
|
|
bd8b8822ac | ||
|
|
0a44c3ec49 | ||
|
|
3262984386 | ||
|
|
180265c8f4 | ||
|
|
a9b4d33cd2 | ||
|
|
5dfb9b28f7 | ||
|
|
ec75793ac3 | ||
|
|
cd4da36863 | ||
|
|
1749e22569 | ||
|
|
0cce88cfbc | ||
|
|
61e83a300b | ||
|
|
136a13aac7 | ||
|
|
2c90db9ae7 | ||
|
|
507e051a5a | ||
|
|
b5bf9ed1d7 | ||
|
|
215eb7e473 | ||
|
|
f42233699a | ||
|
|
1bec68df4d | ||
|
|
d8576e72eb | ||
|
|
7265468e8d | ||
|
|
d07f36dedd | ||
|
|
364a1b71ec | ||
|
|
daee6d210f | ||
|
|
96be0071e6 | ||
|
|
ff8e1dfb47 | ||
|
|
d26db6f213 | ||
|
|
bb6c753583 | ||
|
|
ca08e4b950 | ||
|
|
5a6b02dbd3 | ||
|
|
14416b1050 | ||
|
|
da4e6fc532 | ||
|
|
57d8b69a6d | ||
|
|
c9d8a8661c | ||
|
|
4a3d23e0e6 | ||
|
|
a3666f2ae5 | ||
|
|
c3e000e574 | ||
|
|
dd5481930a | ||
|
|
842328c661 | ||
|
|
8f75384e2e | ||
|
|
193faa00ce | ||
|
|
5e5383b399 | ||
|
|
cb6b29dbe3 | ||
|
|
82b0819051 | ||
|
|
e12ab4afa4 | ||
|
|
1416f631cc | ||
|
|
dbaac47d1e | ||
|
|
cf0ae5e31b | ||
|
|
8891f07362 | ||
|
|
d78974ec59 | ||
|
|
32be26c4d7 | ||
|
|
9de49aa419 | ||
|
|
294a67a4b4 | ||
|
|
0e99888926 | ||
|
|
74cbf10930 | ||
|
|
08d2909b0e | ||
|
|
0949b11436 | ||
|
|
9cdffe7f63 | ||
|
|
8b2a883669 | ||
|
|
b7fc96100c | ||
|
|
63cbc00a40 | ||
|
|
57b94dba6f | ||
|
|
0dd188e108 | ||
|
|
bf8c840293 | ||
|
|
c0244f3018 | ||
|
|
8af8502202 | ||
|
|
42eaeb92f0 | ||
|
|
7f35eb8867 | ||
|
|
785569c40c | ||
|
|
a9eb70a881 | ||
|
|
5d3d0c8625 | ||
|
|
7e32feeea3 | ||
|
|
0d1935e757 | ||
|
|
9b3ee018e9 | ||
|
|
1de411ec89 | ||
|
|
3192799bbf | ||
|
|
2c8dded52f |
1
.github/pyright-config.json
vendored
1
.github/pyright-config.json
vendored
@@ -2,6 +2,7 @@
|
||||
"include": [
|
||||
"../BizHawkClient.py",
|
||||
"../Patch.py",
|
||||
"../test/param.py",
|
||||
"../test/general/test_groups.py",
|
||||
"../test/general/test_helpers.py",
|
||||
"../test/general/test_memory.py",
|
||||
|
||||
2
.github/workflows/analyze-modified-files.yml
vendored
2
.github/workflows/analyze-modified-files.yml
vendored
@@ -65,7 +65,7 @@ jobs:
|
||||
continue-on-error: false
|
||||
if: env.diff != '' && matrix.task == 'flake8'
|
||||
run: |
|
||||
flake8 --count --select=E9,F63,F7,F82 --show-source --statistics ${{ env.diff }}
|
||||
flake8 --count --select=E9,F63,F7,F82 --ignore F824 --show-source --statistics ${{ env.diff }}
|
||||
|
||||
- name: "flake8: Lint modified files"
|
||||
continue-on-error: true
|
||||
|
||||
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -99,8 +99,8 @@ jobs:
|
||||
if-no-files-found: error
|
||||
retention-days: 7 # keep for 7 days, should be enough
|
||||
|
||||
build-ubuntu2004:
|
||||
runs-on: ubuntu-20.04
|
||||
build-ubuntu2204:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
# - copy code below to release.yml -
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
4
.github/workflows/ctest.yml
vendored
4
.github/workflows/ctest.yml
vendored
@@ -36,9 +36,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ilammy/msvc-dev-cmd@v1
|
||||
- uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756
|
||||
if: startsWith(matrix.os,'windows')
|
||||
- uses: Bacondish2023/setup-googletest@v1
|
||||
- uses: Bacondish2023/setup-googletest@49065d1f7a6d21f6134864dd65980fe5dbe06c73
|
||||
with:
|
||||
build-type: 'Release'
|
||||
- name: Build tests
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -29,8 +29,8 @@ jobs:
|
||||
# build-release-windows: # this is done by hand because of signing
|
||||
# build-release-macos: # LF volunteer
|
||||
|
||||
build-release-ubuntu2004:
|
||||
runs-on: ubuntu-20.04
|
||||
build-release-ubuntu2204:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Set env
|
||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,6 +10,7 @@
|
||||
*.apmc
|
||||
*.apz5
|
||||
*.aptloz
|
||||
*.aptww
|
||||
*.apemerald
|
||||
*.pyc
|
||||
*.pyd
|
||||
|
||||
@@ -511,7 +511,7 @@ if __name__ == '__main__':
|
||||
|
||||
import colorama
|
||||
|
||||
colorama.init()
|
||||
colorama.just_fix_windows_console()
|
||||
|
||||
asyncio.run(main())
|
||||
colorama.deinit()
|
||||
|
||||
@@ -616,7 +616,7 @@ class MultiWorld():
|
||||
locations: Set[Location] = set()
|
||||
events: Set[Location] = set()
|
||||
for location in self.get_filled_locations():
|
||||
if type(location.item.code) is int:
|
||||
if type(location.item.code) is int and type(location.address) is int:
|
||||
locations.add(location)
|
||||
else:
|
||||
events.add(location)
|
||||
@@ -1106,6 +1106,9 @@ class Region:
|
||||
def __len__(self) -> int:
|
||||
return self._list.__len__()
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._list)
|
||||
|
||||
# This seems to not be needed, but that's a bit suspicious.
|
||||
# def __del__(self):
|
||||
# self.clear()
|
||||
@@ -1310,9 +1313,6 @@ class Location:
|
||||
multiworld = self.parent_region.multiworld if self.parent_region and self.parent_region.multiworld else None
|
||||
return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.name, self.player))
|
||||
|
||||
def __lt__(self, other: Location):
|
||||
return (self.player, self.name) < (other.player, other.name)
|
||||
|
||||
@@ -1416,6 +1416,10 @@ class Item:
|
||||
def flags(self) -> int:
|
||||
return self.classification.as_flag()
|
||||
|
||||
@property
|
||||
def is_event(self) -> bool:
|
||||
return self.code is None
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, Item):
|
||||
return NotImplemented
|
||||
|
||||
@@ -413,7 +413,8 @@ class CommonContext:
|
||||
await self.server.socket.close()
|
||||
if self.server_task is not None:
|
||||
await self.server_task
|
||||
self.ui.update_hints()
|
||||
if self.ui:
|
||||
self.ui.update_hints()
|
||||
|
||||
async def send_msgs(self, msgs: typing.List[typing.Any]) -> None:
|
||||
""" `msgs` JSON serializable """
|
||||
@@ -624,9 +625,6 @@ class CommonContext:
|
||||
|
||||
def consume_network_data_package(self, data_package: dict):
|
||||
self.update_data_package(data_package)
|
||||
current_cache = Utils.persistent_load().get("datapackage", {}).get("games", {})
|
||||
current_cache.update(data_package["games"])
|
||||
Utils.persistent_store("datapackage", "games", current_cache)
|
||||
logger.info(f"Got new ID/Name DataPackage for {', '.join(data_package['games'])}")
|
||||
for game, game_data in data_package["games"].items():
|
||||
Utils.store_data_package_for_checksum(game, game_data)
|
||||
@@ -1128,7 +1126,7 @@ def run_as_textclient(*args):
|
||||
args = handle_url_arg(args, parser=parser)
|
||||
|
||||
# use colorama to display colored text highlighting on windows
|
||||
colorama.init()
|
||||
colorama.just_fix_windows_console()
|
||||
|
||||
asyncio.run(main(args))
|
||||
colorama.deinit()
|
||||
|
||||
@@ -261,7 +261,7 @@ if __name__ == '__main__':
|
||||
|
||||
parser = get_base_parser()
|
||||
args = parser.parse_args()
|
||||
colorama.init()
|
||||
colorama.just_fix_windows_console()
|
||||
|
||||
asyncio.run(main(args))
|
||||
colorama.deinit()
|
||||
|
||||
21
Fill.py
21
Fill.py
@@ -75,9 +75,11 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
||||
items_to_place.append(reachable_items[next_player].pop())
|
||||
|
||||
for item in items_to_place:
|
||||
for p, pool_item in enumerate(item_pool):
|
||||
# The items added into `reachable_items` are placed starting from the end of each deque in
|
||||
# `reachable_items`, so the items being placed are more likely to be found towards the end of `item_pool`.
|
||||
for p, pool_item in enumerate(reversed(item_pool), start=1):
|
||||
if pool_item is item:
|
||||
item_pool.pop(p)
|
||||
del item_pool[-p]
|
||||
break
|
||||
|
||||
maximum_exploration_state = sweep_from_pool(
|
||||
@@ -348,10 +350,10 @@ def accessibility_corrections(multiworld: MultiWorld, state: CollectionState, lo
|
||||
if (location.item is not None and location.item.advancement and location.address is not None and not
|
||||
location.locked and location.item.player not in minimal_players):
|
||||
pool.append(location.item)
|
||||
state.remove(location.item)
|
||||
location.item = None
|
||||
if location in state.advancements:
|
||||
state.advancements.remove(location)
|
||||
state.remove(location.item)
|
||||
locations.append(location)
|
||||
if pool and locations:
|
||||
locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY)
|
||||
@@ -500,13 +502,15 @@ def distribute_items_restrictive(multiworld: MultiWorld,
|
||||
|
||||
if prioritylocations:
|
||||
# "priority fill"
|
||||
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool,
|
||||
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", one_item_per_player=True, allow_partial=True)
|
||||
|
||||
if prioritylocations:
|
||||
# retry with one_item_per_player off because some priority fills can fail to fill with that optimization
|
||||
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool,
|
||||
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)
|
||||
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
|
||||
@@ -514,14 +518,15 @@ def distribute_items_restrictive(multiworld: MultiWorld,
|
||||
|
||||
if progitempool:
|
||||
# "advancement/progression fill"
|
||||
maximum_exploration_state = sweep_from_pool(multiworld.state)
|
||||
if panic_method == "swap":
|
||||
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=True,
|
||||
fill_restrictive(multiworld, maximum_exploration_state, defaultlocations, progitempool, swap=True,
|
||||
name="Progression", single_player_placement=single_player)
|
||||
elif panic_method == "raise":
|
||||
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=False,
|
||||
fill_restrictive(multiworld, maximum_exploration_state, defaultlocations, progitempool, swap=False,
|
||||
name="Progression", single_player_placement=single_player)
|
||||
elif panic_method == "start_inventory":
|
||||
fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=False,
|
||||
fill_restrictive(multiworld, maximum_exploration_state, defaultlocations, progitempool, swap=False,
|
||||
allow_partial=True, name="Progression", single_player_placement=single_player)
|
||||
if progitempool:
|
||||
for item in progitempool:
|
||||
|
||||
35
Generate.py
35
Generate.py
@@ -54,12 +54,22 @@ def mystery_argparse():
|
||||
parser.add_argument("--skip_output", action="store_true",
|
||||
help="Skips generation assertion and output stages and skips multidata and spoiler output. "
|
||||
"Intended for debugging and testing purposes.")
|
||||
parser.add_argument("--spoiler_only", action="store_true",
|
||||
help="Skips generation assertion and multidata, outputting only a spoiler log. "
|
||||
"Intended for debugging and testing purposes.")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.skip_output and args.spoiler_only:
|
||||
parser.error("Cannot mix --skip_output and --spoiler_only")
|
||||
elif args.spoiler == 0 and args.spoiler_only:
|
||||
parser.error("Cannot use --spoiler_only when --spoiler=0. Use --skip_output or set --spoiler to a different value")
|
||||
|
||||
if not os.path.isabs(args.weights_file_path):
|
||||
args.weights_file_path = os.path.join(args.player_files_path, args.weights_file_path)
|
||||
if not os.path.isabs(args.meta_file_path):
|
||||
args.meta_file_path = os.path.join(args.player_files_path, args.meta_file_path)
|
||||
args.plando: PlandoOptions = PlandoOptions.from_option_string(args.plando)
|
||||
|
||||
return args
|
||||
|
||||
|
||||
@@ -108,6 +118,8 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
|
||||
raise Exception("Cannot mix --sameoptions with --meta")
|
||||
else:
|
||||
meta_weights = None
|
||||
|
||||
|
||||
player_id = 1
|
||||
player_files = {}
|
||||
for file in os.scandir(args.player_files_path):
|
||||
@@ -164,6 +176,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
|
||||
erargs.outputpath = args.outputpath
|
||||
erargs.skip_prog_balancing = args.skip_prog_balancing
|
||||
erargs.skip_output = args.skip_output
|
||||
erargs.spoiler_only = args.spoiler_only
|
||||
erargs.name = {}
|
||||
erargs.csv_output = args.csv_output
|
||||
|
||||
@@ -279,22 +292,30 @@ def get_choice(option, root, value=None) -> Any:
|
||||
raise RuntimeError(f"All options specified in \"{option}\" are weighted as zero.")
|
||||
|
||||
|
||||
class SafeDict(dict):
|
||||
def __missing__(self, key):
|
||||
return '{' + key + '}'
|
||||
class SafeFormatter(string.Formatter):
|
||||
def get_value(self, key, args, kwargs):
|
||||
if isinstance(key, int):
|
||||
if key < len(args):
|
||||
return args[key]
|
||||
else:
|
||||
return "{" + str(key) + "}"
|
||||
else:
|
||||
return kwargs.get(key, "{" + key + "}")
|
||||
|
||||
|
||||
def handle_name(name: str, player: int, name_counter: Counter):
|
||||
name_counter[name.lower()] += 1
|
||||
number = name_counter[name.lower()]
|
||||
new_name = "%".join([x.replace("%number%", "{number}").replace("%player%", "{player}") for x in name.split("%%")])
|
||||
new_name = string.Formatter().vformat(new_name, (), SafeDict(number=number,
|
||||
NUMBER=(number if number > 1 else ''),
|
||||
player=player,
|
||||
PLAYER=(player if player > 1 else '')))
|
||||
|
||||
new_name = SafeFormatter().vformat(new_name, (), {"number": number,
|
||||
"NUMBER": (number if number > 1 else ''),
|
||||
"player": player,
|
||||
"PLAYER": (player if player > 1 else '')})
|
||||
# Run .strip twice for edge case where after the initial .slice new_name has a leading whitespace.
|
||||
# Could cause issues for some clients that cannot handle the additional whitespace.
|
||||
new_name = new_name.strip()[:16].strip()
|
||||
|
||||
if new_name == "Archipelago":
|
||||
raise Exception(f"You cannot name yourself \"{new_name}\"")
|
||||
return new_name
|
||||
|
||||
292
Launcher.py
292
Launcher.py
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Archipelago launcher for bundled app.
|
||||
Archipelago Launcher
|
||||
|
||||
* if run with APBP as argument, launch corresponding client.
|
||||
* if run with executable as argument, run it passing argv[2:] as arguments
|
||||
@@ -8,9 +8,7 @@ Archipelago launcher for bundled app.
|
||||
Scroll down to components= to add components to the launcher as well as setup.py
|
||||
"""
|
||||
|
||||
|
||||
import argparse
|
||||
import itertools
|
||||
import logging
|
||||
import multiprocessing
|
||||
import shlex
|
||||
@@ -20,10 +18,11 @@ import urllib.parse
|
||||
import webbrowser
|
||||
from os.path import isfile
|
||||
from shutil import which
|
||||
from typing import Callable, Optional, Sequence, Tuple, Union
|
||||
from typing import Callable, Optional, Sequence, Tuple, Union, Any
|
||||
|
||||
if __name__ == "__main__":
|
||||
import ModuleUpdate
|
||||
|
||||
ModuleUpdate.update()
|
||||
|
||||
import settings
|
||||
@@ -105,7 +104,8 @@ components.extend([
|
||||
Component("Generate Template Options", func=generate_yamls),
|
||||
Component("Archipelago Website", func=lambda: webbrowser.open("https://archipelago.gg/")),
|
||||
Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")),
|
||||
Component("Unrated/18+ Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")),
|
||||
Component("Unrated/18+ Discord Server", icon="discord",
|
||||
func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")),
|
||||
Component("Browse Files", func=browse_files),
|
||||
])
|
||||
|
||||
@@ -114,7 +114,7 @@ def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None:
|
||||
url = urllib.parse.urlparse(path)
|
||||
queries = urllib.parse.parse_qs(url.query)
|
||||
launch_args = (path, *launch_args)
|
||||
client_component = None
|
||||
client_component = []
|
||||
text_client_component = None
|
||||
if "game" in queries:
|
||||
game = queries["game"][0]
|
||||
@@ -122,49 +122,40 @@ def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None:
|
||||
game = "Archipelago"
|
||||
for component in components:
|
||||
if component.supports_uri and component.game_name == game:
|
||||
client_component = component
|
||||
client_component.append(component)
|
||||
elif component.display_name == "Text Client":
|
||||
text_client_component = component
|
||||
|
||||
if client_component is None:
|
||||
from kvui import MDButton, MDButtonText
|
||||
from kivymd.uix.dialog import MDDialog, MDDialogHeadlineText, MDDialogContentContainer, MDDialogSupportingText
|
||||
from kivymd.uix.divider import MDDivider
|
||||
|
||||
if not client_component:
|
||||
run_component(text_client_component, *launch_args)
|
||||
return
|
||||
else:
|
||||
popup_text = MDDialogSupportingText(text="Select client to open and connect with.")
|
||||
component_buttons = [MDDivider()]
|
||||
for component in [text_client_component, *client_component]:
|
||||
component_buttons.append(MDButton(
|
||||
MDButtonText(text=component.display_name),
|
||||
on_release=lambda *args, comp=component: run_component(comp, *launch_args),
|
||||
style="text"
|
||||
))
|
||||
component_buttons.append(MDDivider())
|
||||
|
||||
from kvui import App, Button, BoxLayout, Label, Window
|
||||
MDDialog(
|
||||
# Headline
|
||||
MDDialogHeadlineText(text="Connect to Multiworld"),
|
||||
# Text
|
||||
popup_text,
|
||||
# Content
|
||||
MDDialogContentContainer(
|
||||
*component_buttons,
|
||||
orientation="vertical"
|
||||
),
|
||||
|
||||
class Popup(App):
|
||||
def __init__(self):
|
||||
self.title = "Connect to Multiworld"
|
||||
self.icon = r"data/icon.png"
|
||||
super().__init__()
|
||||
|
||||
def build(self):
|
||||
layout = BoxLayout(orientation="vertical")
|
||||
layout.add_widget(Label(text="Select client to open and connect with."))
|
||||
button_row = BoxLayout(orientation="horizontal", size_hint=(1, 0.4))
|
||||
|
||||
text_client_button = Button(
|
||||
text=text_client_component.display_name,
|
||||
on_release=lambda *args: run_component(text_client_component, *launch_args)
|
||||
)
|
||||
button_row.add_widget(text_client_button)
|
||||
|
||||
game_client_button = Button(
|
||||
text=client_component.display_name,
|
||||
on_release=lambda *args: run_component(client_component, *launch_args)
|
||||
)
|
||||
button_row.add_widget(game_client_button)
|
||||
|
||||
layout.add_widget(button_row)
|
||||
|
||||
return layout
|
||||
|
||||
def _stop(self, *largs):
|
||||
# see run_gui Launcher _stop comment for details
|
||||
self.root_window.close()
|
||||
super()._stop(*largs)
|
||||
|
||||
Popup().run()
|
||||
).open()
|
||||
|
||||
|
||||
def identify(path: Union[None, str]) -> Tuple[Union[None, str], Union[None, Component]]:
|
||||
@@ -220,100 +211,166 @@ def launch(exe, in_terminal=False):
|
||||
subprocess.Popen(exe)
|
||||
|
||||
|
||||
def create_shortcut(button: Any, component: Component) -> None:
|
||||
from pyshortcuts import make_shortcut
|
||||
script = sys.argv[0]
|
||||
wkdir = Utils.local_path()
|
||||
|
||||
script = f"{script} \"{component.display_name}\""
|
||||
make_shortcut(script, name=f"Archipelago {component.display_name}", icon=local_path("data", "icon.ico"),
|
||||
startmenu=False, terminal=False, working_dir=wkdir)
|
||||
button.menu.dismiss()
|
||||
|
||||
|
||||
refresh_components: Optional[Callable[[], None]] = None
|
||||
|
||||
|
||||
def run_gui():
|
||||
from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget, ApAsyncImage
|
||||
def run_gui(path: str, args: Any) -> None:
|
||||
from kvui import (ThemedApp, MDFloatLayout, MDGridLayout, ScrollBox)
|
||||
from kivy.properties import ObjectProperty
|
||||
from kivy.core.window import Window
|
||||
from kivy.uix.relativelayout import RelativeLayout
|
||||
from kivy.metrics import dp
|
||||
from kivymd.uix.button import MDIconButton
|
||||
from kivymd.uix.card import MDCard
|
||||
from kivymd.uix.menu import MDDropdownMenu
|
||||
from kivymd.uix.snackbar import MDSnackbar, MDSnackbarText
|
||||
|
||||
class Launcher(App):
|
||||
from kivy.lang.builder import Builder
|
||||
|
||||
class LauncherCard(MDCard):
|
||||
component: Component | None
|
||||
image: str
|
||||
context_button: MDIconButton = ObjectProperty(None)
|
||||
|
||||
def __init__(self, *args, component: Component | None = None, image_path: str = "", **kwargs):
|
||||
self.component = component
|
||||
self.image = image_path
|
||||
super().__init__(args, kwargs)
|
||||
|
||||
class Launcher(ThemedApp):
|
||||
base_title: str = "Archipelago Launcher"
|
||||
container: ContainerLayout
|
||||
grid: GridLayout
|
||||
_tool_layout: Optional[ScrollBox] = None
|
||||
_client_layout: Optional[ScrollBox] = None
|
||||
top_screen: MDFloatLayout = ObjectProperty(None)
|
||||
navigation: MDGridLayout = ObjectProperty(None)
|
||||
grid: MDGridLayout = ObjectProperty(None)
|
||||
button_layout: ScrollBox = ObjectProperty(None)
|
||||
cards: list[LauncherCard]
|
||||
current_filter: Sequence[str | Type] | None
|
||||
|
||||
def __init__(self, ctx=None):
|
||||
def __init__(self, ctx=None, path=None, args=None):
|
||||
self.title = self.base_title + " " + Utils.__version__
|
||||
self.ctx = ctx
|
||||
self.icon = r"data/icon.png"
|
||||
self.favorites = []
|
||||
self.launch_uri = path
|
||||
self.launch_args = args
|
||||
self.cards = []
|
||||
self.current_filter = (Type.CLIENT, Type.TOOL, Type.ADJUSTER, Type.MISC)
|
||||
persistent = Utils.persistent_load()
|
||||
if "launcher" in persistent:
|
||||
if "favorites" in persistent["launcher"]:
|
||||
self.favorites.extend(persistent["launcher"]["favorites"])
|
||||
if "filter" in persistent["launcher"]:
|
||||
if persistent["launcher"]["filter"]:
|
||||
filters = []
|
||||
for filter in persistent["launcher"]["filter"].split(", "):
|
||||
if filter == "favorites":
|
||||
filters.append(filter)
|
||||
else:
|
||||
filters.append(Type[filter])
|
||||
self.current_filter = filters
|
||||
super().__init__()
|
||||
|
||||
def _refresh_components(self) -> None:
|
||||
def set_favorite(self, caller):
|
||||
if caller.component.display_name in self.favorites:
|
||||
self.favorites.remove(caller.component.display_name)
|
||||
caller.icon = "star-outline"
|
||||
else:
|
||||
self.favorites.append(caller.component.display_name)
|
||||
caller.icon = "star"
|
||||
|
||||
def build_button(component: Component) -> Widget:
|
||||
def build_card(self, component: Component) -> LauncherCard:
|
||||
"""
|
||||
Builds a card widget for a given component.
|
||||
|
||||
:param component: The component associated with the button.
|
||||
|
||||
:return: The created Card Widget.
|
||||
"""
|
||||
Builds a button widget for a given component.
|
||||
button_card = LauncherCard(component=component,
|
||||
image_path=icon_paths[component.icon])
|
||||
|
||||
Args:
|
||||
component (Component): The component associated with the button.
|
||||
def open_menu(caller):
|
||||
caller.menu.open()
|
||||
|
||||
Returns:
|
||||
None. The button is added to the parent grid layout.
|
||||
menu_items = [
|
||||
{
|
||||
"text": "Add shortcut on desktop",
|
||||
"leading_icon": "laptop",
|
||||
"on_release": lambda: create_shortcut(button_card.context_button, component)
|
||||
}
|
||||
]
|
||||
button_card.context_button.menu = MDDropdownMenu(caller=button_card.context_button, items=menu_items)
|
||||
button_card.context_button.bind(on_release=open_menu)
|
||||
|
||||
"""
|
||||
button = Button(text=component.display_name, size_hint_y=None, height=40)
|
||||
button.component = component
|
||||
button.bind(on_release=self.component_action)
|
||||
if component.icon != "icon":
|
||||
image = ApAsyncImage(source=icon_paths[component.icon],
|
||||
size=(38, 38), size_hint=(None, 1), pos=(5, 0))
|
||||
box_layout = RelativeLayout(size_hint_y=None, height=40)
|
||||
box_layout.add_widget(button)
|
||||
box_layout.add_widget(image)
|
||||
return box_layout
|
||||
return button
|
||||
return button_card
|
||||
|
||||
def _refresh_components(self, type_filter: Sequence[str | Type] | None = None) -> None:
|
||||
if not type_filter:
|
||||
type_filter = [Type.CLIENT, Type.ADJUSTER, Type.TOOL, Type.MISC]
|
||||
favorites = "favorites" in type_filter
|
||||
|
||||
# clear before repopulating
|
||||
assert self._tool_layout and self._client_layout, "must call `build` first"
|
||||
tool_children = reversed(self._tool_layout.layout.children)
|
||||
assert self.button_layout, "must call `build` first"
|
||||
tool_children = reversed(self.button_layout.layout.children)
|
||||
for child in tool_children:
|
||||
self._tool_layout.layout.remove_widget(child)
|
||||
client_children = reversed(self._client_layout.layout.children)
|
||||
for child in client_children:
|
||||
self._client_layout.layout.remove_widget(child)
|
||||
self.button_layout.layout.remove_widget(child)
|
||||
|
||||
_tools = {c.display_name: c for c in components if c.type == Type.TOOL}
|
||||
_clients = {c.display_name: c for c in components if c.type == Type.CLIENT}
|
||||
_adjusters = {c.display_name: c for c in components if c.type == Type.ADJUSTER}
|
||||
_miscs = {c.display_name: c for c in components if c.type == Type.MISC}
|
||||
cards = [card for card in self.cards if card.component.type in type_filter
|
||||
or favorites and card.component.display_name in self.favorites]
|
||||
|
||||
for (tool, client) in itertools.zip_longest(itertools.chain(
|
||||
_tools.items(), _miscs.items(), _adjusters.items()
|
||||
), _clients.items()):
|
||||
# column 1
|
||||
if tool:
|
||||
self._tool_layout.layout.add_widget(build_button(tool[1]))
|
||||
# column 2
|
||||
if client:
|
||||
self._client_layout.layout.add_widget(build_button(client[1]))
|
||||
self.current_filter = type_filter
|
||||
|
||||
for card in cards:
|
||||
self.button_layout.layout.add_widget(card)
|
||||
|
||||
top = self.button_layout.children[0].y + self.button_layout.children[0].height \
|
||||
- self.button_layout.height
|
||||
scroll_percent = self.button_layout.convert_distance_to_scroll(0, top)
|
||||
self.button_layout.scroll_y = max(0, min(1, scroll_percent[1]))
|
||||
|
||||
def filter_clients(self, caller):
|
||||
self._refresh_components(caller.type)
|
||||
|
||||
def build(self):
|
||||
self.container = ContainerLayout()
|
||||
self.grid = GridLayout(cols=2)
|
||||
self.container.add_widget(self.grid)
|
||||
self.grid.add_widget(Label(text="General", size_hint_y=None, height=40))
|
||||
self.grid.add_widget(Label(text="Clients", size_hint_y=None, height=40))
|
||||
self._tool_layout = ScrollBox()
|
||||
self._tool_layout.layout.orientation = "vertical"
|
||||
self.grid.add_widget(self._tool_layout)
|
||||
self._client_layout = ScrollBox()
|
||||
self._client_layout.layout.orientation = "vertical"
|
||||
self.grid.add_widget(self._client_layout)
|
||||
|
||||
self._refresh_components()
|
||||
self.top_screen = Builder.load_file(Utils.local_path("data/launcher.kv"))
|
||||
self.grid = self.top_screen.ids.grid
|
||||
self.navigation = self.top_screen.ids.navigation
|
||||
self.button_layout = self.top_screen.ids.button_layout
|
||||
self.set_colors()
|
||||
self.top_screen.md_bg_color = self.theme_cls.backgroundColor
|
||||
|
||||
global refresh_components
|
||||
refresh_components = self._refresh_components
|
||||
|
||||
Window.bind(on_drop_file=self._on_drop_file)
|
||||
|
||||
return self.container
|
||||
for component in components:
|
||||
self.cards.append(self.build_card(component))
|
||||
|
||||
self._refresh_components(self.current_filter)
|
||||
|
||||
return self.top_screen
|
||||
|
||||
def on_start(self):
|
||||
if self.launch_uri:
|
||||
handle_uri(self.launch_uri, self.launch_args)
|
||||
self.launch_uri = None
|
||||
self.launch_args = None
|
||||
|
||||
@staticmethod
|
||||
def component_action(button):
|
||||
MDSnackbar(MDSnackbarText(text="Opening in a new window..."), y=dp(24), pos_hint={"center_x": 0.5},
|
||||
size_hint_x=0.5).open()
|
||||
if button.component.func:
|
||||
button.component.func()
|
||||
else:
|
||||
@@ -333,7 +390,13 @@ def run_gui():
|
||||
self.root_window.close()
|
||||
super()._stop(*largs)
|
||||
|
||||
Launcher().run()
|
||||
def on_stop(self):
|
||||
Utils.persistent_store("launcher", "favorites", self.favorites)
|
||||
Utils.persistent_store("launcher", "filter", ", ".join(filter.name if isinstance(filter, Type) else filter
|
||||
for filter in self.current_filter))
|
||||
super().on_stop()
|
||||
|
||||
Launcher(path=path, args=args).run()
|
||||
|
||||
# avoiding Launcher reference leak
|
||||
# and don't try to do something with widgets after window closed
|
||||
@@ -360,16 +423,14 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
|
||||
|
||||
path = args.get("Patch|Game|Component|url", None)
|
||||
if path is not None:
|
||||
if path.startswith("archipelago://"):
|
||||
handle_uri(path, args.get("args", ()))
|
||||
return
|
||||
file, component = identify(path)
|
||||
if file:
|
||||
args['file'] = file
|
||||
if component:
|
||||
args['component'] = component
|
||||
if not component:
|
||||
logging.warning(f"Could not identify Component responsible for {path}")
|
||||
if not path.startswith("archipelago://"):
|
||||
file, component = identify(path)
|
||||
if file:
|
||||
args['file'] = file
|
||||
if component:
|
||||
args['component'] = component
|
||||
if not component:
|
||||
logging.warning(f"Could not identify Component responsible for {path}")
|
||||
|
||||
if args["update_settings"]:
|
||||
update_settings()
|
||||
@@ -378,7 +439,7 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
|
||||
elif "component" in args:
|
||||
run_component(args["component"], *args["args"])
|
||||
elif not args["update_settings"]:
|
||||
run_gui()
|
||||
run_gui(path, args.get("args", ()))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
@@ -400,6 +461,7 @@ if __name__ == '__main__':
|
||||
main(parser.parse_args())
|
||||
|
||||
from worlds.LauncherComponents import processes
|
||||
|
||||
for process in processes:
|
||||
# we await all child processes to close before we tear down the process host
|
||||
# this makes it feel like each one is its own program, as the Launcher is closed now
|
||||
|
||||
@@ -26,6 +26,7 @@ import typing
|
||||
from CommonClient import (CommonContext, get_base_parser, gui_enabled, logger,
|
||||
server_loop)
|
||||
from NetUtils import ClientStatus
|
||||
from worlds.ladx import LinksAwakeningWorld
|
||||
from worlds.ladx.Common import BASE_ID as LABaseID
|
||||
from worlds.ladx.GpsTracker import GpsTracker
|
||||
from worlds.ladx.TrackerConsts import storage_key
|
||||
@@ -139,7 +140,7 @@ class RAGameboy():
|
||||
def set_checks_range(self, checks_start, checks_size):
|
||||
self.checks_start = checks_start
|
||||
self.checks_size = checks_size
|
||||
|
||||
|
||||
def set_location_range(self, location_start, location_size, critical_addresses):
|
||||
self.location_start = location_start
|
||||
self.location_size = location_size
|
||||
@@ -237,7 +238,7 @@ class RAGameboy():
|
||||
self.cache[start:start + len(hram_block)] = hram_block
|
||||
|
||||
self.last_cache_read = time.time()
|
||||
|
||||
|
||||
async def read_memory_block(self, address: int, size: int):
|
||||
block = bytearray()
|
||||
remaining_size = size
|
||||
@@ -245,7 +246,7 @@ class RAGameboy():
|
||||
chunk = await self.async_read_memory(address + len(block), remaining_size)
|
||||
remaining_size -= len(chunk)
|
||||
block += chunk
|
||||
|
||||
|
||||
return block
|
||||
|
||||
async def read_memory_cache(self, addresses):
|
||||
@@ -506,7 +507,7 @@ class LinksAwakeningContext(CommonContext):
|
||||
la_task = None
|
||||
client = None
|
||||
# TODO: does this need to re-read on reset?
|
||||
found_checks = []
|
||||
found_checks = set()
|
||||
last_resend = time.time()
|
||||
|
||||
magpie_enabled = False
|
||||
@@ -514,8 +515,8 @@ class LinksAwakeningContext(CommonContext):
|
||||
magpie_task = None
|
||||
won = False
|
||||
|
||||
@property
|
||||
def slot_storage_key(self):
|
||||
@property
|
||||
def slot_storage_key(self):
|
||||
return f"{self.slot_info[self.slot].name}_{storage_key}"
|
||||
|
||||
def __init__(self, server_address: typing.Optional[str], password: typing.Optional[str], magpie: typing.Optional[bool]) -> None:
|
||||
@@ -529,9 +530,7 @@ class LinksAwakeningContext(CommonContext):
|
||||
|
||||
def run_gui(self) -> None:
|
||||
import webbrowser
|
||||
import kvui
|
||||
from kvui import Button, GameManager
|
||||
from kivy.uix.image import Image
|
||||
from kvui import GameManager, ImageButton
|
||||
|
||||
class LADXManager(GameManager):
|
||||
logging_pairs = [
|
||||
@@ -544,25 +543,15 @@ class LinksAwakeningContext(CommonContext):
|
||||
b = super().build()
|
||||
|
||||
if self.ctx.magpie_enabled:
|
||||
button = Button(text="", size=(30, 30), size_hint_x=None,
|
||||
on_press=lambda _: webbrowser.open('https://magpietracker.us/?enable_autotracker=1'))
|
||||
image = Image(size=(16, 16), texture=magpie_logo())
|
||||
button.add_widget(image)
|
||||
|
||||
def set_center(_, center):
|
||||
image.center = center
|
||||
button.bind(center=set_center)
|
||||
|
||||
button = ImageButton(texture=magpie_logo(), fit_mode="cover", image_size=(32, 32), size_hint_x=None,
|
||||
on_press=lambda _: webbrowser.open('https://magpietracker.us/?enable_autotracker=1'))
|
||||
self.connect_layout.add_widget(button)
|
||||
|
||||
return b
|
||||
|
||||
self.ui = LADXManager(self)
|
||||
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||
|
||||
async def send_checks(self):
|
||||
message = [{"cmd": "LocationChecks", "locations": self.found_checks}]
|
||||
await self.send_msgs(message)
|
||||
|
||||
async def send_new_entrances(self, entrances: typing.Dict[str, str]):
|
||||
# Store the entrances we find on the server for future sessions
|
||||
message = [{
|
||||
@@ -601,20 +590,20 @@ class LinksAwakeningContext(CommonContext):
|
||||
logger.info("victory!")
|
||||
await self.send_msgs(message)
|
||||
self.won = True
|
||||
|
||||
|
||||
async def request_found_entrances(self):
|
||||
await self.send_msgs([{"cmd": "Get", "keys": [self.slot_storage_key]}])
|
||||
|
||||
# Ask for updates so that players can co-op entrances in a seed
|
||||
await self.send_msgs([{"cmd": "SetNotify", "keys": [self.slot_storage_key]}])
|
||||
# Ask for updates so that players can co-op entrances in a seed
|
||||
await self.send_msgs([{"cmd": "SetNotify", "keys": [self.slot_storage_key]}])
|
||||
|
||||
async def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None:
|
||||
if self.ENABLE_DEATHLINK:
|
||||
self.client.pending_deathlink = True
|
||||
|
||||
def new_checks(self, item_ids, ladxr_ids):
|
||||
self.found_checks += item_ids
|
||||
create_task_log_exception(self.send_checks())
|
||||
self.found_checks.update(item_ids)
|
||||
create_task_log_exception(self.check_locations(self.found_checks))
|
||||
if self.magpie_enabled:
|
||||
create_task_log_exception(self.magpie.send_new_checks(ladxr_ids))
|
||||
|
||||
@@ -642,12 +631,18 @@ class LinksAwakeningContext(CommonContext):
|
||||
if cmd == "Connected":
|
||||
self.game = self.slot_info[self.slot].game
|
||||
self.slot_data = args.get("slot_data", {})
|
||||
|
||||
# This is sent to magpie over local websocket to make its own connection
|
||||
self.slot_data.update({
|
||||
"server_address": self.server_address,
|
||||
"slot_name": self.player_names[self.slot],
|
||||
"password": self.password,
|
||||
})
|
||||
|
||||
# TODO - use watcher_event
|
||||
if cmd == "ReceivedItems":
|
||||
for index, item in enumerate(args["items"], start=args["index"]):
|
||||
self.client.recvd_checks[index] = item
|
||||
|
||||
|
||||
if cmd == "Retrieved" and self.magpie_enabled and self.slot_storage_key in args["keys"]:
|
||||
self.client.gps_tracker.receive_found_entrances(args["keys"][self.slot_storage_key])
|
||||
|
||||
@@ -721,13 +716,15 @@ class LinksAwakeningContext(CommonContext):
|
||||
|
||||
if self.last_resend + 5.0 < now:
|
||||
self.last_resend = now
|
||||
await self.send_checks()
|
||||
await self.check_locations(self.found_checks)
|
||||
if self.magpie_enabled:
|
||||
try:
|
||||
self.magpie.set_checks(self.client.tracker.all_checks)
|
||||
await self.magpie.set_item_tracker(self.client.item_tracker)
|
||||
self.magpie.slot_data = self.slot_data
|
||||
|
||||
if self.slot_data and "slot_data" in self.magpie.features and not self.magpie.has_sent_slot_data:
|
||||
self.magpie.slot_data = self.slot_data
|
||||
await self.magpie.send_slot_data()
|
||||
|
||||
if self.client.gps_tracker.needs_found_entrances:
|
||||
await self.request_found_entrances()
|
||||
self.client.gps_tracker.needs_found_entrances = False
|
||||
@@ -745,8 +742,8 @@ class LinksAwakeningContext(CommonContext):
|
||||
await asyncio.sleep(1.0)
|
||||
|
||||
def run_game(romfile: str) -> None:
|
||||
auto_start = typing.cast(typing.Union[bool, str],
|
||||
Utils.get_options()["ladx_options"].get("rom_start", True))
|
||||
auto_start = LinksAwakeningWorld.settings.rom_start
|
||||
|
||||
if auto_start is True:
|
||||
import webbrowser
|
||||
webbrowser.open(romfile)
|
||||
@@ -803,6 +800,6 @@ async def main():
|
||||
await ctx.shutdown()
|
||||
|
||||
if __name__ == '__main__':
|
||||
colorama.init()
|
||||
colorama.just_fix_windows_console()
|
||||
asyncio.run(main())
|
||||
colorama.deinit()
|
||||
|
||||
@@ -370,7 +370,7 @@ if __name__ == "__main__":
|
||||
|
||||
import colorama
|
||||
|
||||
colorama.init()
|
||||
colorama.just_fix_windows_console()
|
||||
|
||||
asyncio.run(main())
|
||||
colorama.deinit()
|
||||
|
||||
11
Main.py
11
Main.py
@@ -81,7 +81,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
del item_digits, location_digits, item_count, location_count
|
||||
|
||||
# This assertion method should not be necessary to run if we are not outputting any multidata.
|
||||
if not args.skip_output:
|
||||
if not args.skip_output and not args.spoiler_only:
|
||||
AutoWorld.call_stage(multiworld, "assert_generate")
|
||||
|
||||
AutoWorld.call_all(multiworld, "generate_early")
|
||||
@@ -224,6 +224,15 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
logger.info(f'Beginning output...')
|
||||
outfilebase = 'AP_' + multiworld.seed_name
|
||||
|
||||
if args.spoiler_only:
|
||||
if args.spoiler > 1:
|
||||
logger.info('Calculating playthrough.')
|
||||
multiworld.spoiler.create_playthrough(create_paths=args.spoiler > 2)
|
||||
|
||||
multiworld.spoiler.to_file(output_path('%s_Spoiler.txt' % outfilebase))
|
||||
logger.info('Done. Skipped multidata modification. Total time: %s', time.perf_counter() - start)
|
||||
return multiworld
|
||||
|
||||
output = tempfile.TemporaryDirectory()
|
||||
with output as temp_dir:
|
||||
output_players = [player for player in multiworld.player_ids if AutoWorld.World.generate_output.__code__
|
||||
|
||||
@@ -47,7 +47,7 @@ from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, Networ
|
||||
from BaseClasses import ItemClassification
|
||||
|
||||
min_client_version = Version(0, 1, 6)
|
||||
colorama.init()
|
||||
colorama.just_fix_windows_console()
|
||||
|
||||
|
||||
def remove_from_list(container, value):
|
||||
@@ -66,9 +66,13 @@ def pop_from_container(container, value):
|
||||
return container
|
||||
|
||||
|
||||
def update_dict(dictionary, entries):
|
||||
dictionary.update(entries)
|
||||
return dictionary
|
||||
def update_container_unique(container, entries):
|
||||
if isinstance(container, list):
|
||||
existing_container_as_set = set(container)
|
||||
container.extend([entry for entry in entries if entry not in existing_container_as_set])
|
||||
else:
|
||||
container.update(entries)
|
||||
return container
|
||||
|
||||
|
||||
def queue_gc():
|
||||
@@ -109,7 +113,7 @@ modify_functions = {
|
||||
# lists/dicts:
|
||||
"remove": remove_from_list,
|
||||
"pop": pop_from_container,
|
||||
"update": update_dict,
|
||||
"update": update_container_unique,
|
||||
}
|
||||
|
||||
|
||||
@@ -788,9 +792,10 @@ class Context:
|
||||
return None
|
||||
|
||||
def replace_hint(self, team: int, slot: int, old_hint: Hint, new_hint: Hint) -> None:
|
||||
if old_hint in self.hints[team, slot]:
|
||||
self.hints[team, slot].remove(old_hint)
|
||||
self.hints[team, slot].add(new_hint)
|
||||
for real_slot in self.slot_set(slot):
|
||||
if old_hint in self.hints[team, real_slot]:
|
||||
self.hints[team, real_slot].remove(old_hint)
|
||||
self.hints[team, real_slot].add(new_hint)
|
||||
|
||||
# "events"
|
||||
|
||||
@@ -2037,7 +2042,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
value = func(value, operation["value"])
|
||||
ctx.stored_data[args["key"]] = args["value"] = value
|
||||
targets = set(ctx.stored_data_notification_clients[args["key"]])
|
||||
if args.get("want_reply", True):
|
||||
if args.get("want_reply", False):
|
||||
targets.add(client)
|
||||
if targets:
|
||||
ctx.broadcast(targets, [args])
|
||||
@@ -2536,25 +2541,6 @@ async def main(args: argparse.Namespace):
|
||||
logging.info("No file selected. Exiting.")
|
||||
import sys
|
||||
sys.exit(1)
|
||||
elif not args.disable_save:
|
||||
import tempfile
|
||||
import os
|
||||
import sys
|
||||
try:
|
||||
common = os.path.commonpath((tempfile.gettempdir(), data_filename))
|
||||
if not os.path.samefile(tempfile.gettempdir(), common):
|
||||
raise ValueError
|
||||
|
||||
except ValueError:
|
||||
# win32 built-in zip-folder handling, uses "temporary internet files" to store
|
||||
if (sys.platform == "win32" and
|
||||
"/AppData/Local/Microsoft/Windows/INetCache/IE/" in data_filename):
|
||||
logging.info("File inside temporary directory (likely a zip file, just load the zip directly). "
|
||||
"Exiting.")
|
||||
sys.exit(1)
|
||||
else:
|
||||
logging.info("File inside temporary directory. Exiting.")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
ctx.load(data_filename, args.use_embedded_options)
|
||||
|
||||
@@ -346,7 +346,7 @@ if __name__ == '__main__':
|
||||
|
||||
import colorama
|
||||
|
||||
colorama.init()
|
||||
colorama.just_fix_windows_console()
|
||||
|
||||
asyncio.run(main())
|
||||
colorama.deinit()
|
||||
|
||||
@@ -1579,6 +1579,7 @@ def dump_player_options(multiworld: MultiWorld) -> None:
|
||||
player_output = {
|
||||
"Game": multiworld.game[player],
|
||||
"Name": multiworld.get_player_name(player),
|
||||
"ID": player,
|
||||
}
|
||||
output.append(player_output)
|
||||
for option_key, option in world.options_dataclass.type_hints.items():
|
||||
@@ -1591,7 +1592,7 @@ def dump_player_options(multiworld: MultiWorld) -> None:
|
||||
game_option_names.append(display_name)
|
||||
|
||||
with open(output_path(f"generate_{multiworld.seed_name}.csv"), mode="w", newline="") as file:
|
||||
fields = ["Game", "Name", *all_option_names]
|
||||
fields = ["ID", "Game", "Name", *all_option_names]
|
||||
writer = DictWriter(file, fields)
|
||||
writer.writeheader()
|
||||
writer.writerows(output)
|
||||
|
||||
@@ -81,6 +81,7 @@ Currently, the following games are supported:
|
||||
* Castlevania: Circle of the Moon
|
||||
* Inscryption
|
||||
* Civilization VI
|
||||
* The Legend of Zelda: The Wind Waker
|
||||
|
||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
||||
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||
|
||||
@@ -735,6 +735,6 @@ async def main() -> None:
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
colorama.init()
|
||||
colorama.just_fix_windows_console()
|
||||
asyncio.run(main())
|
||||
colorama.deinit()
|
||||
|
||||
@@ -500,7 +500,7 @@ def main():
|
||||
|
||||
import colorama
|
||||
|
||||
colorama.init()
|
||||
colorama.just_fix_windows_console()
|
||||
|
||||
asyncio.run(_main())
|
||||
colorama.deinit()
|
||||
|
||||
2
Utils.py
2
Utils.py
@@ -47,7 +47,7 @@ class Version(typing.NamedTuple):
|
||||
return ".".join(str(item) for item in self)
|
||||
|
||||
|
||||
__version__ = "0.6.0"
|
||||
__version__ = "0.6.2"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
is_linux = sys.platform.startswith("linux")
|
||||
|
||||
@@ -214,17 +214,11 @@ class WargrooveContext(CommonContext):
|
||||
def run_gui(self):
|
||||
"""Import kivy UI system and start running it as self.ui_task."""
|
||||
from kvui import GameManager, HoverBehavior, ServerToolTip
|
||||
from kivy.uix.tabbedpanel import TabbedPanelItem
|
||||
from kivymd.uix.tab import MDTabsItem, MDTabsItemText
|
||||
from kivy.lang import Builder
|
||||
from kivy.uix.button import Button
|
||||
from kivy.uix.togglebutton import ToggleButton
|
||||
from kivy.uix.boxlayout import BoxLayout
|
||||
from kivy.uix.gridlayout import GridLayout
|
||||
from kivy.uix.image import AsyncImage, Image
|
||||
from kivy.uix.stacklayout import StackLayout
|
||||
from kivy.uix.label import Label
|
||||
from kivy.properties import ColorProperty
|
||||
from kivy.uix.image import Image
|
||||
import pkgutil
|
||||
|
||||
class TrackerLayout(BoxLayout):
|
||||
@@ -446,6 +440,6 @@ if __name__ == '__main__':
|
||||
parser = get_base_parser(description="Wargroove Client, for text interfacing.")
|
||||
|
||||
args, rest = parser.parse_known_args()
|
||||
colorama.init()
|
||||
colorama.just_fix_windows_console()
|
||||
asyncio.run(main(args))
|
||||
colorama.deinit()
|
||||
|
||||
@@ -9,7 +9,7 @@ from threading import Event, Thread
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from pony.orm import db_session, select, commit
|
||||
from pony.orm import db_session, select, commit, PrimaryKey
|
||||
|
||||
from Utils import restricted_loads
|
||||
from .locker import Locker, AlreadyRunningException
|
||||
@@ -36,12 +36,21 @@ def handle_generation_failure(result: BaseException):
|
||||
logging.exception(e)
|
||||
|
||||
|
||||
def _mp_gen_game(gen_options: dict, meta: dict[str, Any] | None = None, owner=None, sid=None) -> PrimaryKey | None:
|
||||
from setproctitle import setproctitle
|
||||
|
||||
setproctitle(f"Generator ({sid})")
|
||||
res = gen_game(gen_options, meta=meta, owner=owner, sid=sid)
|
||||
setproctitle(f"Generator (idle)")
|
||||
return res
|
||||
|
||||
|
||||
def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
|
||||
try:
|
||||
meta = json.loads(generation.meta)
|
||||
options = restricted_loads(generation.options)
|
||||
logging.info(f"Generating {generation.id} for {len(options)} players")
|
||||
pool.apply_async(gen_game, (options,),
|
||||
pool.apply_async(_mp_gen_game, (options,),
|
||||
{"meta": meta,
|
||||
"sid": generation.id,
|
||||
"owner": generation.owner},
|
||||
@@ -55,6 +64,10 @@ def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
|
||||
|
||||
|
||||
def init_generator(config: dict[str, Any]) -> None:
|
||||
from setproctitle import setproctitle
|
||||
|
||||
setproctitle("Generator (idle)")
|
||||
|
||||
try:
|
||||
import resource
|
||||
except ModuleNotFoundError:
|
||||
|
||||
@@ -227,6 +227,9 @@ def set_up_logging(room_id) -> logging.Logger:
|
||||
def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
||||
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str],
|
||||
host: str, rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue):
|
||||
from setproctitle import setproctitle
|
||||
|
||||
setproctitle(name)
|
||||
Utils.init_logging(name)
|
||||
try:
|
||||
import resource
|
||||
@@ -247,8 +250,23 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
||||
raise Exception("Worlds system should not be loaded in the custom server.")
|
||||
|
||||
import gc
|
||||
ssl_context = load_server_cert(cert_file, cert_key_file) if cert_file else None
|
||||
del cert_file, cert_key_file, ponyconfig
|
||||
|
||||
if not cert_file:
|
||||
def get_ssl_context():
|
||||
return None
|
||||
else:
|
||||
load_date = None
|
||||
ssl_context = load_server_cert(cert_file, cert_key_file)
|
||||
|
||||
def get_ssl_context():
|
||||
nonlocal load_date, ssl_context
|
||||
today = datetime.date.today()
|
||||
if load_date != today:
|
||||
ssl_context = load_server_cert(cert_file, cert_key_file)
|
||||
load_date = today
|
||||
return ssl_context
|
||||
|
||||
del ponyconfig
|
||||
gc.collect() # free intermediate objects used during setup
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
@@ -263,12 +281,12 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
||||
assert ctx.server is None
|
||||
try:
|
||||
ctx.server = websockets.serve(
|
||||
functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=ssl_context)
|
||||
functools.partial(server, ctx=ctx), ctx.host, ctx.port, ssl=get_ssl_context())
|
||||
|
||||
await ctx.server
|
||||
except OSError: # likely port in use
|
||||
ctx.server = websockets.serve(
|
||||
functools.partial(server, ctx=ctx), ctx.host, 0, ssl=ssl_context)
|
||||
functools.partial(server, ctx=ctx), ctx.host, 0, ssl=get_ssl_context())
|
||||
|
||||
await ctx.server
|
||||
port = 0
|
||||
|
||||
@@ -135,6 +135,7 @@ def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=Non
|
||||
{"bosses", "items", "connections", "texts"}))
|
||||
erargs.skip_prog_balancing = False
|
||||
erargs.skip_output = False
|
||||
erargs.spoiler_only = False
|
||||
erargs.csv_output = False
|
||||
|
||||
name_counter = Counter()
|
||||
|
||||
@@ -35,6 +35,12 @@ def start_playing():
|
||||
@app.route('/games/<string:game>/info/<string:lang>')
|
||||
@cache.cached()
|
||||
def game_info(game, lang):
|
||||
try:
|
||||
world = AutoWorldRegister.world_types[game]
|
||||
if lang not in world.web.game_info_languages:
|
||||
raise KeyError("Sorry, this game's info page is not available in that language yet.")
|
||||
except KeyError:
|
||||
return abort(404)
|
||||
return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game))
|
||||
|
||||
|
||||
@@ -52,6 +58,12 @@ def games():
|
||||
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
|
||||
@cache.cached()
|
||||
def tutorial(game, file, lang):
|
||||
try:
|
||||
world = AutoWorldRegister.world_types[game]
|
||||
if lang not in [tut.link.split("/")[1] for tut in world.web.tutorials]:
|
||||
raise KeyError("Sorry, the tutorial is not available in that language yet.")
|
||||
except KeyError:
|
||||
return abort(404)
|
||||
return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game))
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing import Dict, Union
|
||||
from docutils.core import publish_parts
|
||||
|
||||
import yaml
|
||||
from flask import redirect, render_template, request, Response
|
||||
from flask import redirect, render_template, request, Response, abort
|
||||
|
||||
import Options
|
||||
from Utils import local_path
|
||||
@@ -142,7 +142,10 @@ def weighted_options_old():
|
||||
@app.route("/games/<string:game>/weighted-options")
|
||||
@cache.cached()
|
||||
def weighted_options(game: str):
|
||||
return render_options_page("weightedOptions/weightedOptions.html", game, is_complex=True)
|
||||
try:
|
||||
return render_options_page("weightedOptions/weightedOptions.html", game, is_complex=True)
|
||||
except KeyError:
|
||||
return abort(404)
|
||||
|
||||
|
||||
@app.route("/games/<string:game>/generate-weighted-yaml", methods=["POST"])
|
||||
@@ -197,7 +200,10 @@ def generate_weighted_yaml(game: str):
|
||||
@app.route("/games/<string:game>/player-options")
|
||||
@cache.cached()
|
||||
def player_options(game: str):
|
||||
return render_options_page("playerOptions/playerOptions.html", game, is_complex=False)
|
||||
try:
|
||||
return render_options_page("playerOptions/playerOptions.html", game, is_complex=False)
|
||||
except KeyError:
|
||||
return abort(404)
|
||||
|
||||
|
||||
# YAML generator for player-options
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
flask>=3.0.3
|
||||
werkzeug>=3.0.6
|
||||
flask>=3.1.0
|
||||
werkzeug>=3.1.3
|
||||
pony>=0.7.19
|
||||
waitress>=3.0.0
|
||||
waitress>=3.0.2
|
||||
Flask-Caching>=2.3.0
|
||||
Flask-Compress>=1.15
|
||||
Flask-Limiter>=3.8.0
|
||||
bokeh>=3.5.2
|
||||
markupsafe>=2.1.5
|
||||
Flask-Compress>=1.17
|
||||
Flask-Limiter>=3.12
|
||||
bokeh>=3.6.3
|
||||
markupsafe>=3.0.2
|
||||
Markdown>=3.7
|
||||
mdx-breakless-lists>=1.0.1
|
||||
setproctitle>=1.3.5
|
||||
|
||||
@@ -23,7 +23,6 @@ window.addEventListener('load', () => {
|
||||
showdown.setOption('strikethrough', true);
|
||||
showdown.setOption('literalMidWordUnderscores', true);
|
||||
gameInfo.innerHTML += (new showdown.Converter()).makeHtml(results);
|
||||
adjustHeaderWidth();
|
||||
|
||||
// Reset the id of all header divs to something nicer
|
||||
for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) {
|
||||
@@ -42,10 +41,5 @@ window.addEventListener('load', () => {
|
||||
scrollTarget?.scrollIntoView();
|
||||
}
|
||||
});
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
gameInfo.innerHTML =
|
||||
`<h2>This page is out of logic!</h2>
|
||||
<h3>Click <a href="${window.location.origin}">here</a> to return to safety.</h3>`;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,4 @@ window.addEventListener('load', () => {
|
||||
document.getElementById('file-input').addEventListener('change', () => {
|
||||
document.getElementById('host-game-form').submit();
|
||||
});
|
||||
|
||||
adjustFooterHeight();
|
||||
});
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
const adjustFooterHeight = () => {
|
||||
// If there is no footer on this page, do nothing
|
||||
const footer = document.getElementById('island-footer');
|
||||
if (!footer) { return; }
|
||||
|
||||
// If the body is taller than the window, also do nothing
|
||||
if (document.body.offsetHeight > window.innerHeight) {
|
||||
footer.style.marginTop = '0';
|
||||
return;
|
||||
}
|
||||
|
||||
// Add a margin-top to the footer to position it at the bottom of the screen
|
||||
const sibling = footer.previousElementSibling;
|
||||
const margin = (window.innerHeight - sibling.offsetTop - sibling.offsetHeight - footer.offsetHeight);
|
||||
if (margin < 1) {
|
||||
footer.style.marginTop = '0';
|
||||
return;
|
||||
}
|
||||
footer.style.marginTop = `${margin}px`;
|
||||
};
|
||||
|
||||
const adjustHeaderWidth = () => {
|
||||
// If there is no header, do nothing
|
||||
const header = document.getElementById('base-header');
|
||||
if (!header) { return; }
|
||||
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.style.width = '100px';
|
||||
tempDiv.style.height = '100px';
|
||||
tempDiv.style.overflow = 'scroll';
|
||||
tempDiv.style.position = 'absolute';
|
||||
tempDiv.style.top = '-500px';
|
||||
document.body.appendChild(tempDiv);
|
||||
const scrollbarWidth = tempDiv.offsetWidth - tempDiv.clientWidth;
|
||||
document.body.removeChild(tempDiv);
|
||||
|
||||
const documentRoot = document.compatMode === 'BackCompat' ? document.body : document.documentElement;
|
||||
const margin = (documentRoot.scrollHeight > documentRoot.clientHeight) ? 0-scrollbarWidth : 0;
|
||||
document.getElementById('base-header-right').style.marginRight = `${margin}px`;
|
||||
};
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
window.addEventListener('resize', adjustFooterHeight);
|
||||
window.addEventListener('resize', adjustHeaderWidth);
|
||||
adjustFooterHeight();
|
||||
adjustHeaderWidth();
|
||||
});
|
||||
@@ -25,7 +25,6 @@ window.addEventListener('load', () => {
|
||||
showdown.setOption('literalMidWordUnderscores', true);
|
||||
showdown.setOption('disableForced4SpacesIndentedSublists', true);
|
||||
tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results);
|
||||
adjustHeaderWidth();
|
||||
|
||||
const title = document.querySelector('h1')
|
||||
if (title) {
|
||||
@@ -49,10 +48,5 @@ window.addEventListener('load', () => {
|
||||
scrollTarget?.scrollIntoView();
|
||||
}
|
||||
});
|
||||
}).catch((error) => {
|
||||
console.error(error);
|
||||
tutorialWrapper.innerHTML =
|
||||
`<h2>This page is out of logic!</h2>
|
||||
<h3>Click <a href="${window.location.origin}/tutorial">here</a> to return to safety.</h3>`;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,6 +36,13 @@ html{
|
||||
|
||||
body{
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: calc(100vh - 110px);
|
||||
}
|
||||
|
||||
main {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
a{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
{% import "macros.html" as macros %}
|
||||
{% set show_footer = True %}
|
||||
|
||||
{% block head %}
|
||||
<title>Page Not Found (404)</title>
|
||||
@@ -13,5 +14,4 @@
|
||||
The page you're looking for doesn't exist.<br />
|
||||
<a href="/">Click here to return to safety.</a>
|
||||
</div>
|
||||
{% include 'islandFooter.html' %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
{% set show_footer = True %}
|
||||
|
||||
{% block head %}
|
||||
<title>Upload Multidata</title>
|
||||
@@ -27,6 +28,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'islandFooter.html' %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
{% set show_footer = True %}
|
||||
|
||||
{% block head %}
|
||||
<title>Archipelago</title>
|
||||
@@ -57,5 +58,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'islandFooter.html' %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -5,26 +5,29 @@
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/tooltip.css") }}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/cookieNotice.css") }}" />
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/globalStyles.css") }}" />
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/styleController.js") }}"></script>
|
||||
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/cookieNotice.js") }}"></script>
|
||||
{% block head %}
|
||||
<title>Archipelago</title>
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for message in messages | unique %}
|
||||
<div class="user-message">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
<div>
|
||||
{% for message in messages | unique %}
|
||||
<div class="user-message">{{ message }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% block body %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
|
||||
{% if show_footer %}
|
||||
{% include "islandFooter.html" %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% block body %}
|
||||
{% endblock %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -213,7 +213,7 @@
|
||||
{% endmacro %}
|
||||
|
||||
{% macro RandomizeButton(option_name, option) %}
|
||||
<div class="randomize-button" data-tooltip="Toggle randomization for this option!">
|
||||
<div class="randomize-button" data-tooltip="Pick a random value for this option.">
|
||||
<label for="random-{{ option_name }}">
|
||||
<input type="checkbox" id="random-{{ option_name }}" name="random-{{ option_name }}" class="randomize-checkbox" data-option-name="{{ option_name }}" {{ "checked" if option.default == "random" }} />
|
||||
🎲
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
{% import "macros.html" as macros %}
|
||||
{% set show_footer = True %}
|
||||
|
||||
{% block head %}
|
||||
<title>Generation failed, please retry.</title>
|
||||
@@ -15,5 +16,4 @@
|
||||
{{ seed_error }}
|
||||
</div>
|
||||
</div>
|
||||
{% include 'islandFooter.html' %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
{% set show_footer = True %}
|
||||
|
||||
{% block head %}
|
||||
<title>Start Playing</title>
|
||||
@@ -26,6 +27,4 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include 'islandFooter.html' %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
{% import "macros.html" as macros %}
|
||||
{% set show_footer = True %}
|
||||
|
||||
{% block head %}
|
||||
<title>View Seed {{ seed.id|suuid }}</title>
|
||||
@@ -50,5 +51,4 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% include 'islandFooter.html' %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
{% import "macros.html" as macros %}
|
||||
{% set show_footer = True %}
|
||||
|
||||
{% block head %}
|
||||
<title>Generation in Progress</title>
|
||||
<meta http-equiv="refresh" content="1">
|
||||
<noscript>
|
||||
<meta http-equiv="refresh" content="1">
|
||||
</noscript>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/waitSeed.css") }}"/>
|
||||
{% endblock %}
|
||||
|
||||
@@ -15,5 +18,34 @@
|
||||
Waiting for game to generate, this page auto-refreshes to check.
|
||||
</div>
|
||||
</div>
|
||||
{% include 'islandFooter.html' %}
|
||||
<script>
|
||||
const waitSeedDiv = document.getElementById("wait-seed");
|
||||
async function checkStatus() {
|
||||
try {
|
||||
const response = await fetch("{{ url_for('api.wait_seed_api', seed=seed_id) }}");
|
||||
if (response.status !== 202) {
|
||||
// Seed is ready; reload page to load seed page.
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
waitSeedDiv.innerHTML = `
|
||||
<h1>Generation in Progress</h1>
|
||||
<p>${data.text}</p>
|
||||
`;
|
||||
|
||||
setTimeout(checkStatus, 1000); // Continue polling.
|
||||
} catch (error) {
|
||||
waitSeedDiv.innerHTML = `
|
||||
<h1>Progress Unknown</h1>
|
||||
<p>${error.message}<br />(Last checked: ${new Date().toLocaleTimeString()})</p>
|
||||
`;
|
||||
|
||||
setTimeout(checkStatus, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(checkStatus, 1000);
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
|
||||
{% else %}
|
||||
<div class="unsupported-option">
|
||||
This option is not supported. Please edit your .yaml file manually.
|
||||
This option cannot be modified here. Please edit your .yaml file manually.
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
@@ -386,7 +386,7 @@ if __name__ == '__main__':
|
||||
parser.add_argument('diff_file', default="", type=str, nargs="?",
|
||||
help='Path to a Archipelago Binary Patch file')
|
||||
args = parser.parse_args()
|
||||
colorama.init()
|
||||
colorama.just_fix_windows_console()
|
||||
|
||||
asyncio.run(main(args))
|
||||
colorama.deinit()
|
||||
|
||||
@@ -14,23 +14,51 @@
|
||||
salmon: "FA8072" # typically trap item
|
||||
white: "FFFFFF" # not used, if you want to change the generic text color change color in Label
|
||||
orange: "FF7700" # Used for command echo
|
||||
<Label>:
|
||||
color: "FFFFFF"
|
||||
<TabbedPanel>:
|
||||
tab_width: root.width / app.tab_count
|
||||
# KivyMD theming parameters
|
||||
theme_style: "Dark" # Light/Dark
|
||||
primary_palette: "Green" # Many options
|
||||
dynamic_scheme_name: "TONAL_SPOT"
|
||||
dynamic_scheme_contrast: 0.0
|
||||
<MDLabel>:
|
||||
color: self.theme_cls.primaryColor
|
||||
<TooltipLabel>:
|
||||
text_size: self.width, None
|
||||
size_hint_y: None
|
||||
height: self.texture_size[1]
|
||||
adaptive_height: True
|
||||
font_size: dp(20)
|
||||
markup: True
|
||||
halign: "left"
|
||||
<SelectableLabel>:
|
||||
size_hint: 1, None
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: (.0, 0.9, .1, .3) if self.selected else (0.2, 0.2, 0.2, 1)
|
||||
rgba: (.0, 0.9, .1, .3) if self.selected else self.theme_cls.surfaceContainerLowColor
|
||||
Rectangle:
|
||||
size: self.size
|
||||
pos: self.pos
|
||||
<MarkupDropdownItem>
|
||||
orientation: "vertical"
|
||||
|
||||
MDLabel:
|
||||
text: root.text
|
||||
valign: "center"
|
||||
padding_x: "12dp"
|
||||
shorten: True
|
||||
shorten_from: "right"
|
||||
theme_text_color: "Custom"
|
||||
markup: True
|
||||
text_color:
|
||||
app.theme_cls.onSurfaceVariantColor \
|
||||
if not root.text_color else \
|
||||
root.text_color
|
||||
|
||||
MDDivider:
|
||||
md_bg_color:
|
||||
( \
|
||||
app.theme_cls.outlineVariantColor \
|
||||
if not root.divider_color \
|
||||
else root.divider_color \
|
||||
) \
|
||||
if root.divider else \
|
||||
(0, 0, 0, 0)
|
||||
<UILog>:
|
||||
messages: 1000 # amount of messages stored in client logs.
|
||||
cols: 1
|
||||
@@ -49,7 +77,7 @@
|
||||
<HintLabel>:
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: (.0, 0.9, .1, .3) if self.selected else (0.2, 0.2, 0.2, 1) if self.striped else (0.18, 0.18, 0.18, 1)
|
||||
rgba: (.0, 0.9, .1, .3) if self.selected else self.theme_cls.surfaceContainerHighColor if self.striped else self.theme_cls.surfaceContainerLowColor
|
||||
Rectangle:
|
||||
size: self.size
|
||||
pos: self.pos
|
||||
@@ -152,3 +180,16 @@
|
||||
height: dp(30)
|
||||
multiline: False
|
||||
write_tab: False
|
||||
<ScrollBox>:
|
||||
layout: layout
|
||||
bar_width: "12dp"
|
||||
scroll_wheel_distance: 40
|
||||
do_scroll_x: False
|
||||
scroll_type: ['bars', 'content']
|
||||
|
||||
MDBoxLayout:
|
||||
id: layout
|
||||
orientation: "vertical"
|
||||
spacing: 10
|
||||
size_hint_y: None
|
||||
height: self.minimum_height
|
||||
|
||||
142
data/launcher.kv
Normal file
142
data/launcher.kv
Normal file
@@ -0,0 +1,142 @@
|
||||
<LauncherCard>:
|
||||
id: main
|
||||
style: "filled"
|
||||
padding: "4dp"
|
||||
size_hint: 1, None
|
||||
height: "75dp"
|
||||
context_button: context
|
||||
|
||||
MDRelativeLayout:
|
||||
ApAsyncImage:
|
||||
source: main.image
|
||||
size: (48, 48)
|
||||
size_hint_y: None
|
||||
pos_hint: {"center_x": 0.1, "center_y": 0.5}
|
||||
|
||||
MDLabel:
|
||||
text: main.component.display_name
|
||||
pos_hint:{"center_x": 0.5, "center_y": 0.75 if main.component.description else 0.65}
|
||||
halign: "center"
|
||||
font_style: "Title"
|
||||
role: "medium"
|
||||
theme_text_color: "Custom"
|
||||
text_color: app.theme_cls.primaryColor
|
||||
|
||||
MDLabel:
|
||||
text: main.component.description
|
||||
pos_hint: {"center_x": 0.5, "center_y": 0.35}
|
||||
halign: "center"
|
||||
role: "small"
|
||||
theme_text_color: "Custom"
|
||||
text_color: app.theme_cls.primaryColor
|
||||
|
||||
MDIconButton:
|
||||
component: main.component
|
||||
icon: "star" if self.component.display_name in app.favorites else "star-outline"
|
||||
style: "standard"
|
||||
pos_hint:{"center_x": 0.85, "center_y": 0.8}
|
||||
theme_text_color: "Custom"
|
||||
text_color: app.theme_cls.primaryColor
|
||||
on_release: app.set_favorite(self)
|
||||
|
||||
MDIconButton:
|
||||
id: context
|
||||
icon: "menu"
|
||||
style: "standard"
|
||||
pos_hint:{"center_x": 0.95, "center_y": 0.8}
|
||||
theme_text_color: "Custom"
|
||||
text_color: app.theme_cls.primaryColor
|
||||
|
||||
MDButton:
|
||||
pos_hint:{"center_x": 0.9, "center_y": 0.25}
|
||||
size_hint_y: None
|
||||
height: "25dp"
|
||||
component: main.component
|
||||
on_release: app.component_action(self)
|
||||
|
||||
MDButtonText:
|
||||
text: "Open"
|
||||
|
||||
|
||||
#:import Type worlds.LauncherComponents.Type
|
||||
MDFloatLayout:
|
||||
id: top_screen
|
||||
|
||||
MDGridLayout:
|
||||
id: grid
|
||||
cols: 2
|
||||
spacing: "5dp"
|
||||
padding: "10dp"
|
||||
|
||||
MDGridLayout:
|
||||
id: navigation
|
||||
cols: 1
|
||||
size_hint_x: 0.25
|
||||
|
||||
MDButton:
|
||||
id: all
|
||||
style: "text"
|
||||
type: (Type.CLIENT, Type.TOOL, Type.ADJUSTER, Type.MISC)
|
||||
on_release: app.filter_clients(self)
|
||||
|
||||
MDButtonIcon:
|
||||
icon: "asterisk"
|
||||
MDButtonText:
|
||||
text: "All"
|
||||
MDButton:
|
||||
id: client
|
||||
style: "text"
|
||||
type: (Type.CLIENT, )
|
||||
on_release: app.filter_clients(self)
|
||||
|
||||
MDButtonIcon:
|
||||
icon: "controller"
|
||||
MDButtonText:
|
||||
text: "Client"
|
||||
MDButton:
|
||||
id: Tool
|
||||
style: "text"
|
||||
type: (Type.TOOL, )
|
||||
on_release: app.filter_clients(self)
|
||||
|
||||
MDButtonIcon:
|
||||
icon: "desktop-classic"
|
||||
MDButtonText:
|
||||
text: "Tool"
|
||||
MDButton:
|
||||
id: adjuster
|
||||
style: "text"
|
||||
type: (Type.ADJUSTER, )
|
||||
on_release: app.filter_clients(self)
|
||||
|
||||
MDButtonIcon:
|
||||
icon: "wrench"
|
||||
MDButtonText:
|
||||
text: "Adjuster"
|
||||
MDButton:
|
||||
id: misc
|
||||
style: "text"
|
||||
type: (Type.MISC, )
|
||||
on_release: app.filter_clients(self)
|
||||
|
||||
MDButtonIcon:
|
||||
icon: "dots-horizontal-circle-outline"
|
||||
MDButtonText:
|
||||
text: "Misc"
|
||||
|
||||
MDButton:
|
||||
id: favorites
|
||||
style: "text"
|
||||
type: ("favorites", )
|
||||
on_release: app.filter_clients(self)
|
||||
|
||||
MDButtonIcon:
|
||||
icon: "star"
|
||||
MDButtonText:
|
||||
text: "Favorites"
|
||||
|
||||
MDNavigationDrawerDivider:
|
||||
|
||||
|
||||
ScrollBox:
|
||||
id: button_layout
|
||||
@@ -214,6 +214,9 @@
|
||||
# Wargroove
|
||||
/worlds/wargroove/ @FlySniper
|
||||
|
||||
# The Wind Waker
|
||||
/worlds/tww/ @tanjo3
|
||||
|
||||
# The Witness
|
||||
/worlds/witness/ @NewSoupVi @blastron
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# Adding Games
|
||||
|
||||
Like all contributions to Archipelago, New Game implementations should follow the [Contributing](/docs/contributing.md)
|
||||
guide.
|
||||
|
||||
Adding a new game to Archipelago has two major parts:
|
||||
|
||||
* Game Modification to communicate with Archipelago server (hereafter referred to as "client")
|
||||
@@ -13,30 +16,51 @@ it will not be detailed here.
|
||||
|
||||
The client is an intermediary program between the game and the Archipelago server. This can either be a direct
|
||||
modification to the game, an external program, or both. This can be implemented in nearly any modern language, but it
|
||||
must fulfill a few requirements in order to function as expected. The specific requirements the game client must follow
|
||||
to behave as expected are:
|
||||
must fulfill a few requirements in order to function as expected. Libraries for most modern languages and the spec for
|
||||
various packets can be found in the [network protocol](/docs/network%20protocol.md) API reference document.
|
||||
|
||||
### Hard Requirements
|
||||
|
||||
In order for the game client to behave as expected, it must be able to perform these functions:
|
||||
|
||||
* Handle both secure and unsecure websocket connections
|
||||
* Detect and react when a location has been "checked" by the player by sending a network packet to the server
|
||||
* Receive and parse network packets when the player receives an item from the server, and reward it to the player on
|
||||
demand
|
||||
* **Any** of your items can be received any number of times, up to and far surpassing those that the game might
|
||||
normally expect from features such as starting inventory, item link replacement, or item cheating
|
||||
* Players and the admin can cheat items to the player at any time with a server command, and these items may not have
|
||||
a player or location attributed to them
|
||||
* Reconnect if the connection is unstable and lost while playing
|
||||
* Be able to change the port for saved connection info
|
||||
* Rooms hosted on the website attempt to reserve their port, but since there are a limited number of ports, this
|
||||
privilege can be lost, requiring the room to be moved to a new port
|
||||
* Reconnect if the connection is unstable and lost while playing
|
||||
* Keep an index for items received in order to resync. The ItemsReceived Packets are a single list with guaranteed
|
||||
order.
|
||||
* Receive items that were sent to the player while they were not connected to the server
|
||||
* The player being able to complete checks while offline and sending them when reconnecting is a good bonus, but not
|
||||
strictly required
|
||||
privilege can be lost, requiring the room to be moved to a new port
|
||||
* Send a status update packet alerting the server that the player has completed their goal
|
||||
|
||||
Libraries for most modern languages and the spec for various packets can be found in the
|
||||
[network protocol](/docs/network%20protocol.md) API reference document.
|
||||
Regarding items and locations, the game client must be able to handle these tasks:
|
||||
|
||||
#### Location Handling
|
||||
|
||||
Send a network packet to the server when it detects a location has been "checked" by the player in-game.
|
||||
|
||||
* If actions were taken in game that would usually trigger a location check, and those actions can only ever be taken
|
||||
once, but the client was not connected when they happened: The client must send those location checks on connection
|
||||
so that they are not permanently lost, e.g. by reading flags in the game state or save file.
|
||||
|
||||
#### Item Handling
|
||||
|
||||
Receive and parse network packets from the server when the player receives an item.
|
||||
|
||||
* It must reward items to the player on demand, as items can come from other players at any time.
|
||||
* It must be able to reward copies of an item, up to and beyond the number the game normally expects. This may happen
|
||||
due to features such as starting inventory, item link replacement, admin commands, or item cheating. **Any** of
|
||||
your items can be received **any** number of times.
|
||||
* Admins and players may use server commands to create items without a player or location attributed to them. The
|
||||
client must be able to handle these items.
|
||||
* It must keep an index for items received in order to resync. The ItemsReceived Packets are a single list with a
|
||||
guaranteed order.
|
||||
* It must be able to receive items that were sent to the player while they were not connected to the server.
|
||||
|
||||
### Encouraged Features
|
||||
|
||||
These are "nice to have" features for a client, but they are not strictly required. It is encouraged to add them
|
||||
if possible.
|
||||
|
||||
* If your client appears in the Archipelago Launcher, you may define an icon for it that differentiates it from
|
||||
other clients. The icon size is 48x48 pixels, but smaller or larger images will scale to that size.
|
||||
|
||||
## World
|
||||
|
||||
@@ -44,35 +68,94 @@ The world is your game integration for the Archipelago generator, webhost, and m
|
||||
information necessary for creating the items and locations to be randomized, the logic for item placement, the
|
||||
datapackage information so other game clients can recognize your game data, and documentation. Your world must be
|
||||
written as a Python package to be loaded by Archipelago. This is currently done by creating a fork of the Archipelago
|
||||
repository and creating a new world package in `/worlds/`. A bare minimum world implementation must satisfy the
|
||||
following requirements:
|
||||
repository and creating a new world package in `/worlds/`.
|
||||
|
||||
* A folder within `/worlds/` that contains an `__init__.py`
|
||||
* A `World` subclass where you create your world and define all of its rules
|
||||
* A unique game name
|
||||
* For webhost documentation and behaviors, a `WebWorld` subclass that must be instantiated in the `World` class
|
||||
definition
|
||||
* The game_info doc must follow the format `{language_code}_{game_name}.md`
|
||||
The base World class can be found in [AutoWorld](/worlds/AutoWorld.py). Methods available for your world to call
|
||||
during generation can be found in [BaseClasses](/BaseClasses.py) and [Fill](/Fill.py). Some examples and documentation
|
||||
regarding the API can be found in the [world api doc](/docs/world%20api.md). Before publishing, make sure to also
|
||||
check out [world maintainer.md](/docs/world%20maintainer.md).
|
||||
|
||||
### Hard Requirements
|
||||
|
||||
A bare minimum world implementation must satisfy the following requirements:
|
||||
|
||||
* It has a folder with the name of your game (or an abbreviation) under `/worlds/`
|
||||
* The `/worlds/{game}` folder contains an `__init__.py`
|
||||
* Any subfolders within `/worlds/{game}` that contain `*.py` files also contain an `__init__.py` for frozen build
|
||||
packaging
|
||||
* The game folder has at least one game_info doc named with follow the format `{language_code}_{game_name}.md`
|
||||
* The game folder has at least one setup doc
|
||||
* There must be a `World` subclass in your game folder (typically in `/worlds/{game}/__init__.py`) where you create
|
||||
your world and define all of its rules and features
|
||||
|
||||
Within the `World` subclass you should also have:
|
||||
|
||||
* A [unique game name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L260)
|
||||
* An [instance](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L295) of a `WebWorld`
|
||||
subclass for webhost documentation and behaviors
|
||||
* In your `WebWorld`, if you wrote a game_info doc in more than one language, override the list of
|
||||
[game info languages](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L210) with the
|
||||
ones you include.
|
||||
* In your `WebWorld`, override the list of
|
||||
[tutorials](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L213) with each tutorial
|
||||
or setup doc you included in the game folder.
|
||||
* A mapping for items and locations defining their names and ids for clients to be able to identify them. These are
|
||||
`item_name_to_id` and `location_name_to_id`, respectively.
|
||||
* Create an item when `create_item` is called both by your code and externally
|
||||
* An `options_dataclass` defining the options players have available to them
|
||||
* A `Region` for your player with the name "Menu" to start from
|
||||
* Create a non-zero number of locations and add them to your regions
|
||||
* Create a non-zero number of items **equal** to the number of locations and add them to the multiworld itempool
|
||||
* All items submitted to the multiworld itempool must not be manually placed by the World. If you need to place specific
|
||||
items, there are multiple ways to do so, but they should not be added to the multiworld itempool.
|
||||
`item_name_to_id` and `location_name_to_id`, respectively.
|
||||
* An implementation of `create_item` that can create an item when called by either your code or by another process
|
||||
within Archipelago
|
||||
* At least one `Region` for your player to start from (i.e. the Origin Region)
|
||||
* The default name of this region is "Menu" but you may configure a different name with
|
||||
[origin_region_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L298-L299)
|
||||
* A non-zero number of locations, added to your regions
|
||||
* A non-zero number of items **equal** to the number of locations, added to the multiworld itempool
|
||||
* In rare cases, there may be 0-location-0-item games, but this is extremely atypical.
|
||||
* A set
|
||||
[completion condition](https://github.com/ArchipelagoMW/Archipelago/blob/main/BaseClasses.py#L77) (aka "goal") for
|
||||
the player.
|
||||
* Use your player as the index (`multiworld.completion_condition[player]`) for your world's completion goal.
|
||||
|
||||
Notable caveats:
|
||||
* The "Menu" region will always be considered the "start" for the player
|
||||
* The "Menu" region is *always* considered accessible; i.e. the player is expected to always be able to return to the
|
||||
### Encouraged Features
|
||||
|
||||
These are "nice to have" features for a world, but they are not strictly required. It is encouraged to add them
|
||||
if possible.
|
||||
|
||||
* An implementation of
|
||||
[get_filler_item_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L473)
|
||||
* By default, this function chooses any item name from `item_name_to_id`, so you want to limit it to only the true
|
||||
filler items.
|
||||
* An `options_dataclass` defining the options players have available to them
|
||||
* This should be accompanied by a type hint for `options` with the same class name
|
||||
* A [bug report page](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L220)
|
||||
* A list of [option groups](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L226)
|
||||
for better organization on the webhost
|
||||
* A dictionary of [options presets](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L223)
|
||||
for player convenience
|
||||
* A dictionary of [item name groups](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L273)
|
||||
for player convenience
|
||||
* A dictionary of
|
||||
[location name groups](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L276)
|
||||
for player convenience
|
||||
* Other games may also benefit from your name group dictionaries for hints, features, etc.
|
||||
|
||||
### Discouraged or Prohibited Behavior
|
||||
|
||||
These are behaviors or implementations that are known to cause various issues. Some of these points have notable
|
||||
workarounds or preferred methods which should be used instead:
|
||||
|
||||
* All items submitted to the multiworld itempool must not be manually placed by the World.
|
||||
* If you need to place specific items, there are multiple ways to do so, but they should not be added to the
|
||||
multiworld itempool.
|
||||
* It is not allowed to use `eval` for most reasons, chiefly due to security concerns.
|
||||
* It is discouraged to use PyYAML (i.e. `yaml.load`) directly due to security concerns.
|
||||
* When possible, use `Utils.parse_yaml` instead, as this defaults to the safe loader and the faster C parser.
|
||||
* When submitting regions or items to the multiworld (`multiworld.regions` and `multiworld.itempool` respectively),
|
||||
do **not** use `=` as this will overwrite all elements for all games in the seed.
|
||||
* Instead, use `append`, `extend`, or `+=`.
|
||||
|
||||
### Notable Caveats
|
||||
|
||||
* The Origin Region will always be considered the "start" for the player
|
||||
* The Origin Region is *always* considered accessible; i.e. the player is expected to always be able to return to the
|
||||
start of the game from anywhere
|
||||
* When submitting regions or items to the multiworld (multiworld.regions and multiworld.itempool respectively), use
|
||||
`append`, `extend`, or `+=`. **Do not use `=`**
|
||||
* Regions are simply containers for locations that share similar access rules. They do not have to map to
|
||||
concrete, physical areas within your game and can be more abstract like tech trees or a questline.
|
||||
|
||||
The base World class can be found in [AutoWorld](/worlds/AutoWorld.py). Methods available for your world to call during
|
||||
generation can be found in [BaseClasses](/BaseClasses.py) and [Fill](/Fill.py). Some examples and documentation
|
||||
regarding the API can be found in the [world api doc](/docs/world%20api.md).
|
||||
Before publishing, make sure to also check out [world maintainer.md](/docs/world%20maintainer.md).
|
||||
|
||||
@@ -66,3 +66,22 @@ The reason entrance access rules using `location.can_reach` and `entrance.can_re
|
||||
We recognize it can feel like a trap since it will not alert you when you are missing an indirect condition, and that some games have very complex access rules.
|
||||
As of [PR #3682 (Core: Region handling customization)](https://github.com/ArchipelagoMW/Archipelago/pull/3682) being merged, it is possible for a world to opt out of indirect conditions entirely, instead using the system of checking each entrance whenever a region has been reached, although this does come with a performance cost.
|
||||
Opting out of using indirect conditions should only be used by games that *really* need it. For most games, it should be reasonable to know all entrance → region dependencies, making indirect conditions preferred because they are much faster.
|
||||
|
||||
---
|
||||
|
||||
### I uploaded the generated output of my world to the webhost and webhost is erroring on corrupted multidata
|
||||
|
||||
The error `Could not load multidata. File may be corrupted or incompatible.` occurs when uploading a locally generated
|
||||
file where there is an issue with the multidata contained within it. It may come with a description like
|
||||
`(No module named 'worlds.myworld')` or `(global 'worlds.myworld.names.ItemNames' is forbidden)`
|
||||
|
||||
Pickling is a way to compress python objects such that they can be decompressed and be used to rebuild the
|
||||
python objects. This means that if one of your custom class instances ends up in the multidata, the server would not
|
||||
be able to load that custom class to decompress the data, which can fail either because the custom class is unknown
|
||||
(because it cannot load your world module) or the class it's attempting to import to decompress is deemed unsafe.
|
||||
|
||||
Common situations where this can happen include:
|
||||
* Using Option instances directly in slot_data. Ex: using `options.option_name` instead of `options.option_name.value`.
|
||||
Also, consider using the `options.as_dict("option_name", "option_two")` helper.
|
||||
* Using enums as Location/Item names in the datapackage. When building out `location_name_to_id` and `item_name_to_id`,
|
||||
make sure that you are not using your enum class for either the names or ids in these mappings.
|
||||
|
||||
@@ -470,7 +470,7 @@ The following operations can be applied to a datastorage key
|
||||
| right_shift | Applies a bitwise right-shift to the current value of the key by `value`. |
|
||||
| remove | List only: removes the first instance of `value` found in the list. |
|
||||
| pop | List or Dict: for lists it will remove the index of the `value` given. for dicts it removes the element with the specified key of `value`. |
|
||||
| update | Dict only: Updates the dictionary with the specified elements given in `value` creating new keys, or updating old ones if they previously existed. |
|
||||
| update | List or Dict: Adds the elements of `value` to the container if they weren't already present. In the case of a Dict, already present keys will have their corresponding values updated. |
|
||||
|
||||
### SetNotify
|
||||
Used to register your current session for receiving all [SetReply](#SetReply) packages of certain keys to allow your client to keep track of changes.
|
||||
@@ -756,8 +756,8 @@ Tags are represented as a list of strings, the common client tags follow:
|
||||
### DeathLink
|
||||
A special kind of Bounce packet that can be supported by any AP game. It targets the tag "DeathLink" and carries the following data:
|
||||
|
||||
| Name | Type | Notes |
|
||||
|--------|-------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| time | float | Unix Time Stamp of time of death. |
|
||||
| cause | str | Optional. Text to explain the cause of death. When provided, or checked, this should contain the player name, ex. "Berserker was run over by a train." |
|
||||
| source | str | Name of the player who first died. Can be a slot name, but can also be a name from within a multiplayer game. |
|
||||
| Name | Type | Notes |
|
||||
|--------|-------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| time | float | Unix Time Stamp of time of death. |
|
||||
| cause | str | Optional. Text to explain the cause of death. When provided, or checked, if the string is non-empty, it should contain the player name, ex. "Berserker was run over by a train." |
|
||||
| source | str | Name of the player who first died. Can be a slot name, but can also be a name from within a multiplayer game. |
|
||||
|
||||
@@ -82,6 +82,38 @@ Unit tests can also be created using [TestBase](/test/bases.py#L16) or
|
||||
may be useful for generating a multiworld under very specific constraints without using the generic world setup, or for
|
||||
testing portions of your code that can be tested without relying on a multiworld to be created first.
|
||||
|
||||
#### Parametrization
|
||||
|
||||
When defining a test that needs to cover a range of inputs it is useful to parameterize (to run the same test
|
||||
for multiple inputs) the base test. Some important things to consider when attempting to parametrize your test are:
|
||||
|
||||
* [Subtests](https://docs.python.org/3/library/unittest.html#distinguishing-test-iterations-using-subtests)
|
||||
can be used to have parametrized assertions that show up similar to individual tests but without the overhead
|
||||
of needing to instantiate multiple tests; however, subtests can not be multithreaded and do not have individual
|
||||
timing data, so they are not suitable for slow tests.
|
||||
|
||||
* Archipelago's tests are test-runner-agnostic. That means tests are not allowed to use e.g. `@pytest.mark.parametrize`.
|
||||
Instead, we define our own parametrization helpers in [test.param](/test/param.py).
|
||||
|
||||
* Classes inheriting from `WorldTestBase`, including those created by the helpers in `test.param`, will run all
|
||||
base tests by default, make sure the produced tests actually do what you aim for and do not waste a lot of
|
||||
extra CPU time. Consider using `TestBase` or `unittest.TestCase` directly
|
||||
or setting `WorldTestBase.run_default_tests` to False.
|
||||
|
||||
#### Performance Considerations
|
||||
|
||||
Archipelago is big enough that the runtime of unittests can have an impact on productivity.
|
||||
|
||||
Individual tests should take less than a second, so they can be properly multithreaded.
|
||||
|
||||
Ideally, thorough tests are directed at actual code/functionality. Do not just create and/or fill a ton of individual
|
||||
Multiworlds that spend most of the test time outside what you actually want to test.
|
||||
|
||||
Consider generating/validating "random" games as part of your APWorld release workflow rather than having that be part
|
||||
of continuous integration, and add minimal reproducers to the "normal" tests for problems that were found.
|
||||
You can use [@unittest.skipIf](https://docs.python.org/3/library/unittest.html#unittest.skipIf) with an environment
|
||||
variable to keep all the benefits of the test framework while not running the marked tests by default.
|
||||
|
||||
## Running Tests
|
||||
|
||||
#### Using Pycharm
|
||||
@@ -100,3 +132,11 @@ next to the run and debug buttons.
|
||||
#### Running Tests without Pycharm
|
||||
|
||||
Run `pip install pytest pytest-subtests`, then use your IDE to run tests or run `pytest` from the source folder.
|
||||
|
||||
#### Running Tests Multithreaded
|
||||
|
||||
pytest can run multiple test runners in parallel with the pytest-xdist extension.
|
||||
|
||||
Install with `pip install pytest-xdist`.
|
||||
|
||||
Run with `pytest -n12` to spawn 12 process that each run 1/12th of the tests.
|
||||
|
||||
@@ -606,8 +606,8 @@ from .items import get_item_type
|
||||
|
||||
def set_rules(self) -> None:
|
||||
# For some worlds this step can be omitted if either a Logic mixin
|
||||
# (see below) is used, it's easier to apply the rules from data during
|
||||
# location generation or everything is in generate_basic
|
||||
# (see below) is used or it's easier to apply the rules from data during
|
||||
# location generation
|
||||
|
||||
# set a simple rule for an region
|
||||
set_rule(self.multiworld.get_entrance("Boss Door", self.player),
|
||||
|
||||
@@ -50,13 +50,15 @@ class EntranceLookup:
|
||||
_random: random.Random
|
||||
_expands_graph_cache: dict[Entrance, bool]
|
||||
_coupled: bool
|
||||
_usable_exits: set[Entrance]
|
||||
|
||||
def __init__(self, rng: random.Random, coupled: bool):
|
||||
def __init__(self, rng: random.Random, coupled: bool, usable_exits: set[Entrance]):
|
||||
self.dead_ends = EntranceLookup.GroupLookup()
|
||||
self.others = EntranceLookup.GroupLookup()
|
||||
self._random = rng
|
||||
self._expands_graph_cache = {}
|
||||
self._coupled = coupled
|
||||
self._usable_exits = usable_exits
|
||||
|
||||
def _can_expand_graph(self, entrance: Entrance) -> bool:
|
||||
"""
|
||||
@@ -95,7 +97,8 @@ class EntranceLookup:
|
||||
# randomizable exits which are not reverse of the incoming entrance.
|
||||
# uncoupled mode is an exception because in this case going back in the door you just came in could
|
||||
# actually lead somewhere new
|
||||
if not exit_.connected_region and (not self._coupled or exit_.name != entrance.name):
|
||||
if (not exit_.connected_region and (not self._coupled or exit_.name != entrance.name)
|
||||
and exit_ in self._usable_exits):
|
||||
self._expands_graph_cache[entrance] = True
|
||||
return True
|
||||
elif exit_.connected_region and exit_.connected_region not in visited:
|
||||
@@ -265,14 +268,19 @@ def bake_target_group_lookup(world: World, get_target_groups: Callable[[int], li
|
||||
return { group: get_target_groups(group) for group in unique_groups }
|
||||
|
||||
|
||||
def disconnect_entrance_for_randomization(entrance: Entrance, target_group: int | None = None) -> None:
|
||||
def disconnect_entrance_for_randomization(entrance: Entrance, target_group: int | None = None,
|
||||
one_way_target_name: str | None = None) -> None:
|
||||
"""
|
||||
Given an entrance in a "vanilla" region graph, splits that entrance to prepare it for randomization
|
||||
in randomize_entrances. This should be done after setting the type and group of the entrance.
|
||||
in randomize_entrances. This should be done after setting the type and group of the entrance. Because it attempts
|
||||
to meet strict entrance naming requirements for coupled mode, this function may produce unintuitive results when
|
||||
called only on a single entrance; it produces eventually-correct outputs only after calling it on all entrances.
|
||||
|
||||
:param entrance: The entrance which will be disconnected in preparation for randomization.
|
||||
:param target_group: The group to assign to the created ER target. If not specified, the group from
|
||||
the original entrance will be copied.
|
||||
:param one_way_target_name: The name of the created ER target if `entrance` is one-way. This argument
|
||||
is required for one-way entrances and is ignored otherwise.
|
||||
"""
|
||||
child_region = entrance.connected_region
|
||||
parent_region = entrance.parent_region
|
||||
@@ -287,8 +295,11 @@ def disconnect_entrance_for_randomization(entrance: Entrance, target_group: int
|
||||
# targets in the child region will be created when the other direction edge is disconnected
|
||||
target = parent_region.create_er_target(entrance.name)
|
||||
else:
|
||||
# for 1-ways, the child region needs a target and coupling/naming is not a concern
|
||||
target = child_region.create_er_target(child_region.name)
|
||||
# for 1-ways, the child region needs a target. naming is not a concern for coupling so we
|
||||
# allow it to be user provided (and require it, to prevent an unhelpful assumed name in pairings)
|
||||
if not one_way_target_name:
|
||||
raise ValueError("Cannot disconnect a one-way entrance without a target name specified")
|
||||
target = child_region.create_er_target(one_way_target_name)
|
||||
target.randomization_type = entrance.randomization_type
|
||||
target.randomization_group = target_group or entrance.randomization_group
|
||||
|
||||
@@ -325,7 +336,6 @@ def randomize_entrances(
|
||||
|
||||
start_time = time.perf_counter()
|
||||
er_state = ERPlacementState(world, coupled)
|
||||
entrance_lookup = EntranceLookup(world.random, coupled)
|
||||
# similar to fill, skip validity checks on entrances if the game is beatable on minimal accessibility
|
||||
perform_validity_check = True
|
||||
|
||||
@@ -341,6 +351,7 @@ def randomize_entrances(
|
||||
|
||||
# used when membership checks are needed on the exit list, e.g. speculative sweep
|
||||
exits_set = set(exits)
|
||||
entrance_lookup = EntranceLookup(world.random, coupled, exits_set)
|
||||
for entrance in er_targets:
|
||||
entrance_lookup.add(entrance)
|
||||
|
||||
@@ -358,6 +369,34 @@ def randomize_entrances(
|
||||
if on_connect:
|
||||
on_connect(er_state, placed_exits)
|
||||
|
||||
def needs_speculative_sweep(dead_end: bool, require_new_exits: bool, placeable_exits: list[Entrance]) -> bool:
|
||||
# speculative sweep is expensive. We currently only do it as a last resort, if we might cap off the graph
|
||||
# entirely
|
||||
if len(placeable_exits) > 1:
|
||||
return False
|
||||
|
||||
# in certain stages of randomization we either expect or don't care if the search space shrinks.
|
||||
# we should never speculative sweep here.
|
||||
if dead_end or not require_new_exits or not perform_validity_check:
|
||||
return False
|
||||
|
||||
# edge case - if all dead ends have pre-placed progression or indirect connections, they are pulled forward
|
||||
# into the non dead end stage. In this case, and only this case, it's possible that the last connection may
|
||||
# actually be placeable in stage 1. We need to skip speculative sweep in this case because we expect the graph
|
||||
# to get capped off.
|
||||
|
||||
# check to see if we are proposing the last placement
|
||||
if not coupled:
|
||||
# in uncoupled, this check is easy as there will only be one target.
|
||||
is_last_placement = len(entrance_lookup) == 1
|
||||
else:
|
||||
# a bit harder, there may be 1 or 2 targets depending on if the exit to place is one way or two way.
|
||||
# if it is two way, we can safely assume that one of the targets is the logical pair of the exit.
|
||||
desired_target_count = 2 if placeable_exits[0].randomization_type == EntranceType.TWO_WAY else 1
|
||||
is_last_placement = len(entrance_lookup) == desired_target_count
|
||||
# if it's not the last placement, we need a sweep
|
||||
return not is_last_placement
|
||||
|
||||
def find_pairing(dead_end: bool, require_new_exits: bool) -> bool:
|
||||
nonlocal perform_validity_check
|
||||
placeable_exits = er_state.find_placeable_exits(perform_validity_check, exits)
|
||||
@@ -371,11 +410,9 @@ def randomize_entrances(
|
||||
# very last exit and check whatever exits we open up are functionally accessible.
|
||||
# this requirement can be ignored on a beaten minimal, islands are no issue there.
|
||||
exit_requirement_satisfied = (not perform_validity_check or not require_new_exits
|
||||
or target_entrance.connected_region not in er_state.placed_regions)
|
||||
needs_speculative_sweep = (not dead_end and require_new_exits and perform_validity_check
|
||||
and len(placeable_exits) == 1)
|
||||
or target_entrance.connected_region not in er_state.placed_regions)
|
||||
if exit_requirement_satisfied and source_exit.can_connect_to(target_entrance, dead_end, er_state):
|
||||
if (needs_speculative_sweep
|
||||
if (needs_speculative_sweep(dead_end, require_new_exits, placeable_exits)
|
||||
and not er_state.test_speculative_connection(source_exit, target_entrance, exits_set)):
|
||||
continue
|
||||
do_placement(source_exit, target_entrance)
|
||||
|
||||
475
kvui.py
475
kvui.py
@@ -35,8 +35,7 @@ from kivy.config import Config
|
||||
Config.set("input", "mouse", "mouse,disable_multitouch")
|
||||
Config.set("kivy", "exit_on_escape", "0")
|
||||
Config.set("graphics", "multisamples", "0") # multisamples crash old intel drivers
|
||||
|
||||
from kivy.app import App
|
||||
from kivymd.uix.divider import MDDivider
|
||||
from kivy.core.window import Window
|
||||
from kivy.core.clipboard import Clipboard
|
||||
from kivy.core.text.markup import MarkupLabel
|
||||
@@ -46,30 +45,32 @@ from kivy.clock import Clock
|
||||
from kivy.factory import Factory
|
||||
from kivy.properties import BooleanProperty, ObjectProperty, NumericProperty
|
||||
from kivy.metrics import dp
|
||||
from kivy.effects.scroll import ScrollEffect
|
||||
from kivy.uix.widget import Widget
|
||||
from kivy.uix.button import Button
|
||||
from kivy.uix.gridlayout import GridLayout
|
||||
from kivy.uix.layout import Layout
|
||||
from kivy.uix.textinput import TextInput
|
||||
from kivy.uix.scrollview import ScrollView
|
||||
from kivy.uix.recycleview import RecycleView
|
||||
from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem
|
||||
from kivy.uix.boxlayout import BoxLayout
|
||||
from kivy.uix.floatlayout import FloatLayout
|
||||
from kivy.uix.label import Label
|
||||
from kivy.uix.progressbar import ProgressBar
|
||||
from kivy.uix.dropdown import DropDown
|
||||
from kivy.utils import escape_markup
|
||||
from kivy.lang import Builder
|
||||
from kivy.uix.recycleview.views import RecycleDataViewBehavior
|
||||
from kivy.uix.behaviors import FocusBehavior
|
||||
from kivy.uix.behaviors import FocusBehavior, ToggleButtonBehavior
|
||||
from kivy.uix.recycleboxlayout import RecycleBoxLayout
|
||||
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
|
||||
from kivy.animation import Animation
|
||||
from kivy.uix.popup import Popup
|
||||
from kivy.uix.dropdown import DropDown
|
||||
from kivy.uix.image import AsyncImage
|
||||
from kivymd.app import MDApp
|
||||
from kivymd.uix.gridlayout import MDGridLayout
|
||||
from kivymd.uix.floatlayout import MDFloatLayout
|
||||
from kivymd.uix.boxlayout import MDBoxLayout
|
||||
from kivymd.uix.tab.tab import MDTabsPrimary, MDTabsItem, MDTabsItemText, MDTabsCarousel
|
||||
from kivymd.uix.menu import MDDropdownMenu
|
||||
from kivymd.uix.menu.menu import MDDropdownTextItem
|
||||
from kivymd.uix.dropdownitem import MDDropDownItem, MDDropDownItemText
|
||||
from kivymd.uix.button import MDButton, MDButtonText, MDButtonIcon, MDIconButton
|
||||
from kivymd.uix.label import MDLabel, MDIcon
|
||||
from kivymd.uix.recycleview import MDRecycleView
|
||||
from kivymd.uix.textfield.textfield import MDTextField
|
||||
from kivymd.uix.progressindicator import MDLinearProgressIndicator
|
||||
from kivymd.uix.scrollview import MDScrollView
|
||||
from kivymd.uix.tooltip import MDTooltip, MDTooltipPlain
|
||||
|
||||
fade_in_animation = Animation(opacity=0, duration=0) + Animation(opacity=1, duration=0.25)
|
||||
|
||||
@@ -86,6 +87,85 @@ else:
|
||||
remove_between_brackets = re.compile(r"\[.*?]")
|
||||
|
||||
|
||||
class ThemedApp(MDApp):
|
||||
def set_colors(self):
|
||||
text_colors = KivyJSONtoTextParser.TextColors()
|
||||
self.theme_cls.theme_style = getattr(text_colors, "theme_style", "Dark")
|
||||
self.theme_cls.primary_palette = getattr(text_colors, "primary_palette", "Green")
|
||||
self.theme_cls.dynamic_scheme_name = getattr(text_colors, "dynamic_scheme_name", "TONAL_SPOT")
|
||||
self.theme_cls.dynamic_scheme_contrast = getattr(text_colors, "dynamic_scheme_contrast", 0.0)
|
||||
|
||||
|
||||
class ImageIcon(MDButtonIcon, AsyncImage):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(args, kwargs)
|
||||
self.image = ApAsyncImage(**kwargs)
|
||||
self.add_widget(self.image)
|
||||
|
||||
def add_widget(self, widget, index=0, canvas=None):
|
||||
return super(MDIcon, self).add_widget(widget)
|
||||
|
||||
|
||||
class ImageButton(MDIconButton):
|
||||
def __init__(self, **kwargs):
|
||||
image_args = dict()
|
||||
for kwarg in ("fit_mode", "image_size", "color", "source", "texture"):
|
||||
val = kwargs.pop(kwarg, "None")
|
||||
if val != "None":
|
||||
image_args[kwarg.replace("image_", "")] = val
|
||||
super().__init__()
|
||||
self.image = ApAsyncImage(**image_args)
|
||||
|
||||
def set_center(button, center):
|
||||
self.image.center_x = self.center_x
|
||||
self.image.center_y = self.center_y
|
||||
|
||||
self.bind(center=set_center)
|
||||
self.add_widget(self.image)
|
||||
|
||||
def add_widget(self, widget, index=0, canvas=None):
|
||||
return super(MDIcon, self).add_widget(widget)
|
||||
|
||||
|
||||
class ScrollBox(MDScrollView):
|
||||
layout: MDBoxLayout = ObjectProperty(None)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
# thanks kivymd
|
||||
class ToggleButton(MDButton, ToggleButtonBehavior):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ToggleButton, self).__init__(*args, **kwargs)
|
||||
self.bind(state=self._update_bg)
|
||||
|
||||
def _update_bg(self, _, state: str):
|
||||
if self.disabled:
|
||||
return
|
||||
if self.theme_bg_color == "Primary":
|
||||
self.theme_bg_color = "Custom"
|
||||
|
||||
if state == "down":
|
||||
self.md_bg_color = self.theme_cls.primaryColor
|
||||
for child in self.children:
|
||||
if child.theme_text_color == "Primary":
|
||||
child.theme_text_color = "Custom"
|
||||
if child.theme_icon_color == "Primary":
|
||||
child.theme_icon_color = "Custom"
|
||||
child.text_color = self.theme_cls.onPrimaryColor
|
||||
child.icon_color = self.theme_cls.onPrimaryColor
|
||||
else:
|
||||
self.md_bg_color = self.theme_cls.surfaceContainerLowestColor
|
||||
for child in self.children:
|
||||
if child.theme_text_color == "Primary":
|
||||
child.theme_text_color = "Custom"
|
||||
if child.theme_icon_color == "Primary":
|
||||
child.theme_icon_color = "Custom"
|
||||
child.text_color = self.theme_cls.primaryColor
|
||||
child.icon_color = self.theme_cls.primaryColor
|
||||
|
||||
|
||||
# I was surprised to find this didn't already exist in kivy :(
|
||||
class HoverBehavior(object):
|
||||
"""originally from https://stackoverflow.com/a/605348110"""
|
||||
@@ -125,7 +205,7 @@ class HoverBehavior(object):
|
||||
Factory.register("HoverBehavior", HoverBehavior)
|
||||
|
||||
|
||||
class ToolTip(Label):
|
||||
class ToolTip(MDTooltipPlain):
|
||||
pass
|
||||
|
||||
|
||||
@@ -133,49 +213,30 @@ class ServerToolTip(ToolTip):
|
||||
pass
|
||||
|
||||
|
||||
class ScrollBox(ScrollView):
|
||||
layout: BoxLayout
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.layout = BoxLayout(size_hint_y=None)
|
||||
self.layout.bind(minimum_height=self.layout.setter("height"))
|
||||
self.add_widget(self.layout)
|
||||
self.effect_cls = ScrollEffect
|
||||
self.bar_width = dp(12)
|
||||
self.scroll_type = ["content", "bars"]
|
||||
|
||||
|
||||
class HovererableLabel(HoverBehavior, Label):
|
||||
class HovererableLabel(HoverBehavior, MDLabel):
|
||||
pass
|
||||
|
||||
|
||||
class TooltipLabel(HovererableLabel):
|
||||
tooltip = None
|
||||
class TooltipLabel(HovererableLabel, MDTooltip):
|
||||
tooltip_display_delay = 0.1
|
||||
|
||||
def create_tooltip(self, text, x, y):
|
||||
text = text.replace("<br>", "\n").replace("&", "&").replace("&bl;", "[").replace("&br;", "]")
|
||||
if self.tooltip:
|
||||
# update
|
||||
self.tooltip.children[0].text = text
|
||||
else:
|
||||
self.tooltip = FloatLayout()
|
||||
tooltip_label = ToolTip(text=text)
|
||||
self.tooltip.add_widget(tooltip_label)
|
||||
fade_in_animation.start(self.tooltip)
|
||||
App.get_running_app().root.add_widget(self.tooltip)
|
||||
|
||||
# handle left-side boundary to not render off-screen
|
||||
x = max(x, 3 + self.tooltip.children[0].texture_size[0] / 2)
|
||||
|
||||
# position float layout
|
||||
self.tooltip.x = x - self.tooltip.width / 2
|
||||
self.tooltip.y = y - self.tooltip.height / 2 + 48
|
||||
center_x, center_y = self.to_window(self.center_x, self.center_y)
|
||||
self.shift_y = y - center_y
|
||||
shift_x = center_x - x
|
||||
if shift_x > 0:
|
||||
self.shift_left = shift_x
|
||||
else:
|
||||
self.shift_right = shift_x
|
||||
|
||||
def remove_tooltip(self):
|
||||
if self.tooltip:
|
||||
App.get_running_app().root.remove_widget(self.tooltip)
|
||||
self.tooltip = None
|
||||
if self._tooltip:
|
||||
# update
|
||||
self._tooltip.text = text
|
||||
else:
|
||||
self._tooltip = ToolTip(text=text, pos_hint={})
|
||||
self.display_tooltip()
|
||||
|
||||
def on_mouse_pos(self, window, pos):
|
||||
if not self.get_root_window():
|
||||
@@ -202,26 +263,26 @@ class TooltipLabel(HovererableLabel):
|
||||
|
||||
def on_leave(self):
|
||||
self.remove_tooltip()
|
||||
self._tooltip = None
|
||||
|
||||
|
||||
class ServerLabel(HovererableLabel):
|
||||
class ServerLabel(HovererableLabel, MDTooltip):
|
||||
tooltip_display_delay = 0.1
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(HovererableLabel, self).__init__(*args, **kwargs)
|
||||
self.layout = FloatLayout()
|
||||
self.popuplabel = ServerToolTip(text="Test")
|
||||
self.layout.add_widget(self.popuplabel)
|
||||
self._tooltip = ServerToolTip(text="Test")
|
||||
|
||||
def on_enter(self):
|
||||
self.popuplabel.text = self.get_text()
|
||||
App.get_running_app().root.add_widget(self.layout)
|
||||
fade_in_animation.start(self.layout)
|
||||
self._tooltip.text = self.get_text()
|
||||
self.display_tooltip()
|
||||
|
||||
def on_leave(self):
|
||||
App.get_running_app().root.remove_widget(self.layout)
|
||||
self.animation_tooltip_dismiss()
|
||||
|
||||
@property
|
||||
def ctx(self) -> context_type:
|
||||
return App.get_running_app().ctx
|
||||
return MDApp.get_running_app().ctx
|
||||
|
||||
def get_text(self):
|
||||
if self.ctx.server:
|
||||
@@ -262,11 +323,11 @@ class ServerLabel(HovererableLabel):
|
||||
return "No current server connection. \nPlease connect to an Archipelago server."
|
||||
|
||||
|
||||
class MainLayout(GridLayout):
|
||||
class MainLayout(MDGridLayout):
|
||||
pass
|
||||
|
||||
|
||||
class ContainerLayout(FloatLayout):
|
||||
class ContainerLayout(MDFloatLayout):
|
||||
pass
|
||||
|
||||
|
||||
@@ -286,6 +347,11 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
|
||||
return super(SelectableLabel, self).refresh_view_attrs(
|
||||
rv, index, data)
|
||||
|
||||
def on_size(self, instance_label, size: list) -> None:
|
||||
super().on_size(instance_label, size)
|
||||
if self.parent:
|
||||
self.width = self.parent.width
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
""" Add selection on touch down """
|
||||
if super(SelectableLabel, self).on_touch_down(touch):
|
||||
@@ -296,10 +362,10 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
|
||||
else:
|
||||
# Not a fan of the following few lines, but they work.
|
||||
temp = MarkupLabel(text=self.text).markup
|
||||
text = "".join(part for part in temp if not part.startswith(("[color", "[/color]", "[ref=", "[/ref]")))
|
||||
cmdinput = App.get_running_app().textinput
|
||||
text = "".join(part for part in temp if not part.startswith("["))
|
||||
cmdinput = MDApp.get_running_app().textinput
|
||||
if not cmdinput.text:
|
||||
input_text = get_input_text_from_response(text, App.get_running_app().last_autofillable_command)
|
||||
input_text = get_input_text_from_response(text, MDApp.get_running_app().last_autofillable_command)
|
||||
if input_text is not None:
|
||||
cmdinput.text = input_text
|
||||
|
||||
@@ -310,30 +376,115 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
|
||||
""" Respond to the selection of items in the view. """
|
||||
self.selected = is_selected
|
||||
|
||||
|
||||
class AutocompleteHintInput(TextInput):
|
||||
|
||||
class MarkupDropdownTextItem(MDDropdownTextItem):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
for child in self.children:
|
||||
if child.__class__ == MDLabel:
|
||||
child.markup = True
|
||||
print(self.text)
|
||||
# Currently, this only lets us do markup on text that does not have any icons
|
||||
# Create new TextItems as needed
|
||||
|
||||
|
||||
class MarkupDropdown(MDDropdownMenu):
|
||||
def on_items(self, instance, value: list) -> None:
|
||||
"""
|
||||
The method sets the class that will be used to create the menu item.
|
||||
"""
|
||||
|
||||
items = []
|
||||
viewclass = "MarkupDropdownTextItem"
|
||||
|
||||
for data in value:
|
||||
if "viewclass" not in data:
|
||||
if (
|
||||
"leading_icon" not in data
|
||||
and "trailing_icon" not in data
|
||||
and "trailing_text" not in data
|
||||
):
|
||||
viewclass = "MarkupDropdownTextItem"
|
||||
elif (
|
||||
"leading_icon" in data
|
||||
and "trailing_icon" not in data
|
||||
and "trailing_text" not in data
|
||||
):
|
||||
viewclass = "MDDropdownLeadingIconItem"
|
||||
elif (
|
||||
"leading_icon" not in data
|
||||
and "trailing_icon" in data
|
||||
and "trailing_text" not in data
|
||||
):
|
||||
viewclass = "MDDropdownTrailingIconItem"
|
||||
elif (
|
||||
"leading_icon" not in data
|
||||
and "trailing_icon" in data
|
||||
and "trailing_text" in data
|
||||
):
|
||||
viewclass = "MDDropdownTrailingIconTextItem"
|
||||
elif (
|
||||
"leading_icon" in data
|
||||
and "trailing_icon" in data
|
||||
and "trailing_text" in data
|
||||
):
|
||||
viewclass = "MDDropdownLeadingTrailingIconTextItem"
|
||||
elif (
|
||||
"leading_icon" in data
|
||||
and "trailing_icon" in data
|
||||
and "trailing_text" not in data
|
||||
):
|
||||
viewclass = "MDDropdownLeadingTrailingIconItem"
|
||||
elif (
|
||||
"leading_icon" not in data
|
||||
and "trailing_icon" not in data
|
||||
and "trailing_text" in data
|
||||
):
|
||||
viewclass = "MDDropdownTrailingTextItem"
|
||||
elif (
|
||||
"leading_icon" in data
|
||||
and "trailing_icon" not in data
|
||||
and "trailing_text" in data
|
||||
):
|
||||
viewclass = "MDDropdownLeadingIconTrailingTextItem"
|
||||
|
||||
data["viewclass"] = viewclass
|
||||
|
||||
if "height" not in data:
|
||||
data["height"] = dp(48)
|
||||
|
||||
items.append(data)
|
||||
|
||||
self._items = items
|
||||
# Update items in view
|
||||
if hasattr(self, "menu"):
|
||||
self.menu.data = self._items
|
||||
|
||||
|
||||
class AutocompleteHintInput(MDTextField):
|
||||
min_chars = NumericProperty(3)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.dropdown = DropDown()
|
||||
self.dropdown = MarkupDropdown(caller=self, position="bottom", border_margin=dp(24), width=self.width)
|
||||
self.dropdown.bind(on_select=lambda instance, x: setattr(self, 'text', x))
|
||||
self.bind(on_text_validate=self.on_message)
|
||||
self.bind(width=lambda instance, x: setattr(self.dropdown, "width", x))
|
||||
|
||||
def on_message(self, instance):
|
||||
App.get_running_app().commandprocessor("!hint "+instance.text)
|
||||
MDApp.get_running_app().commandprocessor("!hint "+instance.text)
|
||||
|
||||
def on_text(self, instance, value):
|
||||
if len(value) >= self.min_chars:
|
||||
self.dropdown.clear_widgets()
|
||||
ctx: context_type = App.get_running_app().ctx
|
||||
self.dropdown.items.clear()
|
||||
ctx: context_type = MDApp.get_running_app().ctx
|
||||
if not ctx.game:
|
||||
return
|
||||
item_names = ctx.item_names._game_store[ctx.game].values()
|
||||
|
||||
def on_press(button: Button):
|
||||
split_text = MarkupLabel(text=button.text).markup
|
||||
def on_press(text):
|
||||
split_text = MarkupLabel(text=text).markup
|
||||
return self.dropdown.select("".join(text_frag for text_frag in split_text
|
||||
if not text_frag.startswith("[")))
|
||||
lowered = value.lower()
|
||||
@@ -345,20 +496,29 @@ class AutocompleteHintInput(TextInput):
|
||||
else:
|
||||
text = escape_markup(item_name)
|
||||
text = text[:index] + "[b]" + text[index:index+len(value)]+"[/b]"+text[index+len(value):]
|
||||
btn = Button(text=text, size_hint_y=None, height=dp(30), markup=True)
|
||||
btn.bind(on_release=on_press)
|
||||
self.dropdown.add_widget(btn)
|
||||
if not self.dropdown.attach_to:
|
||||
self.dropdown.open(self)
|
||||
self.dropdown.items.append({
|
||||
"text": text,
|
||||
"on_release": lambda: on_press(text),
|
||||
"markup": True
|
||||
})
|
||||
if not self.dropdown.parent:
|
||||
self.dropdown.open()
|
||||
else:
|
||||
self.dropdown.dismiss()
|
||||
|
||||
|
||||
class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
status_icons = {
|
||||
HintStatus.HINT_NO_PRIORITY: "information",
|
||||
HintStatus.HINT_PRIORITY: "exclamation-thick",
|
||||
HintStatus.HINT_AVOID: "alert"
|
||||
}
|
||||
|
||||
|
||||
class HintLabel(RecycleDataViewBehavior, MDBoxLayout):
|
||||
selected = BooleanProperty(False)
|
||||
striped = BooleanProperty(False)
|
||||
index = None
|
||||
dropdown: DropDown
|
||||
dropdown: MDDropdownMenu
|
||||
|
||||
def __init__(self):
|
||||
super(HintLabel, self).__init__()
|
||||
@@ -369,29 +529,28 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
self.entrance_text = ""
|
||||
self.status_text = ""
|
||||
self.hint = {}
|
||||
for child in self.children:
|
||||
child.bind(texture_size=self.set_height)
|
||||
|
||||
ctx = MDApp.get_running_app().ctx
|
||||
menu_items = []
|
||||
|
||||
ctx = App.get_running_app().ctx
|
||||
self.dropdown = DropDown()
|
||||
for status in (HintStatus.HINT_NO_PRIORITY, HintStatus.HINT_PRIORITY, HintStatus.HINT_AVOID):
|
||||
name = status_names[status]
|
||||
status_button = MDDropDownItem(MDDropDownItemText(text=name), size_hint_y=None, height=dp(50))
|
||||
status_button.status = status
|
||||
menu_items.append({
|
||||
"text": name,
|
||||
"leading_icon": status_icons[status],
|
||||
"on_release": lambda x=status: select(self, x)
|
||||
})
|
||||
|
||||
def set_value(button):
|
||||
self.dropdown.select(button.status)
|
||||
self.dropdown = MDDropdownMenu(caller=self.ids["status"], items=menu_items)
|
||||
|
||||
def select(instance, data):
|
||||
ctx.update_hint(self.hint["location"],
|
||||
self.hint["finding_player"],
|
||||
data)
|
||||
|
||||
for status in (HintStatus.HINT_NO_PRIORITY, HintStatus.HINT_PRIORITY, HintStatus.HINT_AVOID):
|
||||
name = status_names[status]
|
||||
status_button = Button(text=name, size_hint_y=None, height=dp(50))
|
||||
status_button.status = status
|
||||
status_button.bind(on_release=set_value)
|
||||
self.dropdown.add_widget(status_button)
|
||||
|
||||
self.dropdown.bind(on_select=select)
|
||||
self.dropdown.bind(on_release=self.dropdown.dismiss)
|
||||
|
||||
def set_height(self, instance, value):
|
||||
self.height = max([child.texture_size[1] for child in self.children])
|
||||
@@ -406,7 +565,6 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
self.entrance_text = data["entrance"]["text"]
|
||||
self.status_text = data["status"]["text"]
|
||||
self.hint = data["status"]["hint"]
|
||||
self.height = self.minimum_height
|
||||
return super(HintLabel, self).refresh_view_attrs(rv, index, data)
|
||||
|
||||
def on_touch_down(self, touch):
|
||||
@@ -419,10 +577,10 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
if status_label.collide_point(*touch.pos):
|
||||
if self.hint["status"] == HintStatus.HINT_FOUND:
|
||||
return
|
||||
ctx = App.get_running_app().ctx
|
||||
ctx = MDApp.get_running_app().ctx
|
||||
if ctx.slot_concerns_self(self.hint["receiving_player"]): # If this player owns this hint
|
||||
# open a dropdown
|
||||
self.dropdown.open(self.ids["status"])
|
||||
self.dropdown.open()
|
||||
elif self.selected:
|
||||
self.parent.clear_selection()
|
||||
else:
|
||||
@@ -431,8 +589,7 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
if self.entrance_text != "Vanilla"
|
||||
else "", ". (", self.status_text.lower(), ")"))
|
||||
temp = MarkupLabel(text).markup
|
||||
text = "".join(
|
||||
part for part in temp if not part.startswith(("[color", "[/color]", "[ref=", "[/ref]")))
|
||||
text = "".join(part for part in temp if not part.startswith("["))
|
||||
Clipboard.copy(escape_markup(text).replace("&", "&").replace("&bl;", "[").replace("&br;", "]"))
|
||||
return self.parent.select_with_touch(self.index, touch)
|
||||
else:
|
||||
@@ -455,7 +612,7 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
else:
|
||||
parent.sort_key = key
|
||||
parent.reversed = False
|
||||
App.get_running_app().update_hints()
|
||||
MDApp.get_running_app().update_hints()
|
||||
|
||||
def apply_selection(self, rv, index, is_selected):
|
||||
""" Respond to the selection of items in the view. """
|
||||
@@ -463,7 +620,7 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
self.selected = is_selected
|
||||
|
||||
|
||||
class ConnectBarTextInput(TextInput):
|
||||
class ConnectBarTextInput(MDTextField):
|
||||
def insert_text(self, substring, from_undo=False):
|
||||
s = substring.replace("\n", "").replace("\r", "")
|
||||
return super(ConnectBarTextInput, self).insert_text(s, from_undo=from_undo)
|
||||
@@ -473,7 +630,7 @@ def is_command_input(string: str) -> bool:
|
||||
return len(string) > 0 and string[0] in "/!"
|
||||
|
||||
|
||||
class CommandPromptTextInput(TextInput):
|
||||
class CommandPromptTextInput(MDTextField):
|
||||
MAXIMUM_HISTORY_MESSAGES = 50
|
||||
|
||||
def __init__(self, **kwargs) -> None:
|
||||
@@ -521,7 +678,7 @@ class CommandPromptTextInput(TextInput):
|
||||
|
||||
|
||||
class MessageBox(Popup):
|
||||
class MessageBoxLabel(Label):
|
||||
class MessageBoxLabel(MDLabel):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._label.refresh()
|
||||
@@ -539,14 +696,31 @@ class MessageBox(Popup):
|
||||
self.height += max(0, label.height - 18)
|
||||
|
||||
|
||||
class GameManager(App):
|
||||
class ClientTabs(MDTabsPrimary):
|
||||
carousel: MDTabsCarousel
|
||||
lock_swiping = True
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.carousel = MDTabsCarousel(lock_swiping=True)
|
||||
super().__init__(*args, MDDivider(size_hint_y=None, height=dp(4)), self.carousel, **kwargs)
|
||||
self.size_hint_y = 1
|
||||
|
||||
def remove_tab(self, tab, content=None):
|
||||
if content is None:
|
||||
content = tab.content
|
||||
self.ids.container.remove_widget(tab)
|
||||
self.carousel.remove_widget(content)
|
||||
self.on_size(self, self.size)
|
||||
|
||||
|
||||
class GameManager(ThemedApp):
|
||||
logging_pairs = [
|
||||
("Client", "Archipelago"),
|
||||
]
|
||||
base_title: str = "Archipelago Client"
|
||||
last_autofillable_command: str
|
||||
|
||||
main_area_container: GridLayout
|
||||
main_area_container: MDGridLayout
|
||||
""" subclasses can add more columns beside the tabs """
|
||||
|
||||
def __init__(self, ctx: context_type):
|
||||
@@ -581,18 +755,26 @@ class GameManager(App):
|
||||
return max(1, len(self.tabs.tab_list))
|
||||
return 1
|
||||
|
||||
def on_start(self):
|
||||
def on_start(*args):
|
||||
self.root.md_bg_color = self.theme_cls.backgroundColor
|
||||
super().on_start()
|
||||
Clock.schedule_once(on_start)
|
||||
|
||||
def build(self) -> Layout:
|
||||
self.set_colors()
|
||||
self.container = ContainerLayout()
|
||||
|
||||
self.grid = MainLayout()
|
||||
self.grid.cols = 1
|
||||
self.connect_layout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30))
|
||||
self.connect_layout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(70),
|
||||
spacing=5, padding=(5, 10))
|
||||
# top part
|
||||
server_label = ServerLabel()
|
||||
server_label = ServerLabel(halign="center")
|
||||
self.connect_layout.add_widget(server_label)
|
||||
self.server_connect_bar = ConnectBarTextInput(text=self.ctx.suggested_address or "archipelago.gg:",
|
||||
size_hint_y=None,
|
||||
height=dp(30), multiline=False, write_tab=False)
|
||||
size_hint_y=None, role="medium",
|
||||
height=dp(70), multiline=False, write_tab=False)
|
||||
|
||||
def connect_bar_validate(sender):
|
||||
if not self.ctx.server:
|
||||
@@ -600,26 +782,31 @@ class GameManager(App):
|
||||
|
||||
self.server_connect_bar.bind(on_text_validate=connect_bar_validate)
|
||||
self.connect_layout.add_widget(self.server_connect_bar)
|
||||
self.server_connect_button = Button(text="Connect", size=(dp(100), dp(30)), size_hint_y=None, size_hint_x=None)
|
||||
self.server_connect_button = MDButton(MDButtonText(text="Connect"), style="filled", size=(dp(100), dp(70)),
|
||||
size_hint_x=None, size_hint_y=None, radius=5, pos_hint={"center_y": 0.55})
|
||||
self.server_connect_button.bind(on_press=self.connect_button_action)
|
||||
self.server_connect_button.height = self.server_connect_bar.height
|
||||
self.connect_layout.add_widget(self.server_connect_button)
|
||||
self.grid.add_widget(self.connect_layout)
|
||||
self.progressbar = ProgressBar(size_hint_y=None, height=3)
|
||||
self.progressbar = MDLinearProgressIndicator(size_hint_y=None, height=3)
|
||||
self.grid.add_widget(self.progressbar)
|
||||
|
||||
# middle part
|
||||
self.tabs = TabbedPanel(size_hint_y=1)
|
||||
self.tabs.default_tab_text = "All"
|
||||
self.tabs = ClientTabs()
|
||||
self.tabs.add_widget(MDTabsItem(MDTabsItemText(text="All" if len(self.logging_pairs) > 1 else "Archipelago")))
|
||||
self.log_panels["All"] = self.tabs.default_tab_content = UILog(*(logging.getLogger(logger_name)
|
||||
for logger_name, name in
|
||||
self.logging_pairs))
|
||||
for logger_name, name in
|
||||
self.logging_pairs))
|
||||
self.tabs.carousel.add_widget(self.tabs.default_tab_content)
|
||||
|
||||
for logger_name, display_name in self.logging_pairs:
|
||||
bridge_logger = logging.getLogger(logger_name)
|
||||
panel = TabbedPanelItem(text=display_name)
|
||||
self.log_panels[display_name] = panel.content = UILog(bridge_logger)
|
||||
self.log_panels[display_name] = UILog(bridge_logger)
|
||||
if len(self.logging_pairs) > 1:
|
||||
panel = MDTabsItem(MDTabsItemText(text=display_name))
|
||||
panel.content = self.log_panels[display_name]
|
||||
# show Archipelago tab if other logging is present
|
||||
self.tabs.carousel.add_widget(panel.content)
|
||||
self.tabs.add_widget(panel)
|
||||
|
||||
hint_panel = self.add_client_tab("Hints", HintLayout())
|
||||
@@ -627,21 +814,20 @@ class GameManager(App):
|
||||
self.log_panels["Hints"] = hint_panel.content
|
||||
hint_panel.content.add_widget(self.hint_log)
|
||||
|
||||
if len(self.logging_pairs) == 1:
|
||||
self.tabs.default_tab_text = "Archipelago"
|
||||
|
||||
self.main_area_container = GridLayout(size_hint_y=1, rows=1)
|
||||
self.main_area_container = MDGridLayout(size_hint_y=1, rows=1)
|
||||
self.main_area_container.add_widget(self.tabs)
|
||||
|
||||
self.grid.add_widget(self.main_area_container)
|
||||
|
||||
# bottom part
|
||||
bottom_layout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30))
|
||||
info_button = Button(size=(dp(100), dp(30)), text="Command:", size_hint_x=None)
|
||||
bottom_layout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(70), spacing=5, padding=(5, 10))
|
||||
info_button = MDButton(MDButtonText(text="Command:"), radius=5, style="filled", size=(dp(100), dp(70)),
|
||||
size_hint_x=None, size_hint_y=None, pos_hint={"center_y": 0.575})
|
||||
info_button.bind(on_release=self.command_button_action)
|
||||
bottom_layout.add_widget(info_button)
|
||||
self.textinput = CommandPromptTextInput(size_hint_y=None, height=dp(30), multiline=False, write_tab=False)
|
||||
self.textinput.bind(on_text_validate=self.on_message)
|
||||
info_button.height = self.textinput.height
|
||||
self.textinput.text_validate_unfocus = False
|
||||
bottom_layout.add_widget(self.textinput)
|
||||
self.grid.add_widget(bottom_layout)
|
||||
@@ -662,24 +848,26 @@ class GameManager(App):
|
||||
def add_client_tab(self, title: str, content: Widget) -> Widget:
|
||||
"""Adds a new tab to the client window with a given title, and provides a given Widget as its content.
|
||||
Returns the new tab widget, with the provided content being placed on the tab as content."""
|
||||
new_tab = TabbedPanelItem(text=title)
|
||||
new_tab = MDTabsItem(MDTabsItemText(text=title))
|
||||
new_tab.content = content
|
||||
self.tabs.add_widget(new_tab)
|
||||
self.tabs.carousel.add_widget(new_tab.content)
|
||||
return new_tab
|
||||
|
||||
def update_texts(self, dt):
|
||||
if hasattr(self.tabs.content.children[0], "fix_heights"):
|
||||
self.tabs.content.children[0].fix_heights() # TODO: remove this when Kivy fixes this upstream
|
||||
for slide in self.tabs.carousel.slides:
|
||||
if hasattr(slide, "fix_heights"):
|
||||
slide.fix_heights() # TODO: remove this when Kivy fixes this upstream
|
||||
if self.ctx.server:
|
||||
self.title = self.base_title + " " + Utils.__version__ + \
|
||||
f" | Connected to: {self.ctx.server_address} " \
|
||||
f"{'.'.join(str(e) for e in self.ctx.server_version)}"
|
||||
self.server_connect_button.text = "Disconnect"
|
||||
self.server_connect_button._button_text.text = "Disconnect"
|
||||
self.server_connect_bar.readonly = True
|
||||
self.progressbar.max = len(self.ctx.checked_locations) + len(self.ctx.missing_locations)
|
||||
self.progressbar.value = len(self.ctx.checked_locations)
|
||||
else:
|
||||
self.server_connect_button.text = "Connect"
|
||||
self.server_connect_button._button_text.text = "Connect"
|
||||
self.server_connect_bar.readonly = False
|
||||
self.title = self.base_title + " " + Utils.__version__
|
||||
self.progressbar.value = 0
|
||||
@@ -742,8 +930,8 @@ class GameManager(App):
|
||||
|
||||
def enable_energy_link(self):
|
||||
if not hasattr(self, "energy_link_label"):
|
||||
self.energy_link_label = Label(text="Energy Link: Standby",
|
||||
size_hint_x=None, width=150)
|
||||
self.energy_link_label = MDLabel(text="Energy Link: Standby",
|
||||
size_hint_x=None, width=150, halign="center")
|
||||
self.connect_layout.add_widget(self.energy_link_label)
|
||||
|
||||
def set_new_energy_link_value(self):
|
||||
@@ -779,8 +967,9 @@ class LogtoUI(logging.Handler):
|
||||
self.on_log(self.format(record))
|
||||
|
||||
|
||||
class UILog(RecycleView):
|
||||
class UILog(MDRecycleView):
|
||||
messages: typing.ClassVar[int] # comes from kv file
|
||||
adaptive_height = True
|
||||
|
||||
def __init__(self, *loggers_to_handle, **kwargs):
|
||||
super(UILog, self).__init__(**kwargs)
|
||||
@@ -807,16 +996,22 @@ class UILog(RecycleView):
|
||||
element.height = element.texture_size[1]
|
||||
|
||||
|
||||
class HintLayout(BoxLayout):
|
||||
class HintLayout(MDBoxLayout):
|
||||
orientation = "vertical"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
boxlayout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30))
|
||||
boxlayout.add_widget(Label(text="New Hint:", size_hint_x=None, size_hint_y=None, height=dp(30)))
|
||||
boxlayout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(55))
|
||||
boxlayout.add_widget(MDLabel(text="New Hint:", size_hint_x=None, size_hint_y=None, height=dp(55)))
|
||||
boxlayout.add_widget(AutocompleteHintInput())
|
||||
self.add_widget(boxlayout)
|
||||
|
||||
def fix_heights(self):
|
||||
for child in self.children:
|
||||
fix_func = getattr(child, "fix_heights", None)
|
||||
if fix_func:
|
||||
fix_func()
|
||||
|
||||
|
||||
status_names: typing.Dict[HintStatus, str] = {
|
||||
HintStatus.HINT_FOUND: "Found",
|
||||
@@ -840,8 +1035,7 @@ status_sort_weights: dict[HintStatus, int] = {
|
||||
HintStatus.HINT_PRIORITY: 4,
|
||||
}
|
||||
|
||||
|
||||
class HintLog(RecycleView):
|
||||
class HintLog(MDRecycleView):
|
||||
header = {
|
||||
"receiving": {"text": "[u]Receiving Player[/u]"},
|
||||
"item": {"text": "[u]Item[/u]"},
|
||||
@@ -852,7 +1046,7 @@ class HintLog(RecycleView):
|
||||
"hint": {"receiving_player": -1, "location": -1, "finding_player": -1, "status": ""}},
|
||||
"striped": True,
|
||||
}
|
||||
|
||||
data: list[typing.Any]
|
||||
sort_key: str = ""
|
||||
reversed: bool = True
|
||||
|
||||
@@ -865,7 +1059,7 @@ class HintLog(RecycleView):
|
||||
if not hints: # Fix the scrolling looking visually wrong in some edge cases
|
||||
self.scroll_y = 1.0
|
||||
data = []
|
||||
ctx = App.get_running_app().ctx
|
||||
ctx = MDApp.get_running_app().ctx
|
||||
for hint in hints:
|
||||
if not hint.get("status"): # Allows connecting to old servers
|
||||
hint["status"] = HintStatus.HINT_FOUND if hint["found"] else HintStatus.HINT_UNSPECIFIED
|
||||
@@ -929,7 +1123,8 @@ class ImageLoaderPkgutil(ImageLoaderBase):
|
||||
data = pkgutil.get_data(module, path)
|
||||
return self._bytes_to_data(data)
|
||||
|
||||
def _bytes_to_data(self, data: typing.Union[bytes, bytearray]) -> typing.List[ImageData]:
|
||||
@staticmethod
|
||||
def _bytes_to_data(data: typing.Union[bytes, bytearray]) -> typing.List[ImageData]:
|
||||
loader = next(loader for loader in ImageLoader.loaders if loader.can_load_memory())
|
||||
return loader.load(loader, io.BytesIO(data))
|
||||
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
colorama>=0.4.6
|
||||
websockets>=13.0.1,<14
|
||||
PyYAML>=6.0.2
|
||||
jellyfish>=1.1.0
|
||||
jinja2>=3.1.4
|
||||
jellyfish>=1.1.3
|
||||
jinja2>=3.1.6
|
||||
schema>=0.7.7
|
||||
kivy>=2.3.0
|
||||
bsdiff4>=1.2.4
|
||||
platformdirs>=4.2.2
|
||||
certifi>=2024.12.14
|
||||
cython>=3.0.11
|
||||
cymem>=2.0.8
|
||||
orjson>=3.10.7
|
||||
kivy>=2.3.1
|
||||
bsdiff4>=1.2.6
|
||||
platformdirs>=4.3.6
|
||||
certifi>=2025.1.31
|
||||
cython>=3.0.12
|
||||
cymem>=2.0.11
|
||||
orjson>=3.10.15
|
||||
typing_extensions>=4.12.2
|
||||
pyshortcuts>=1.9.1
|
||||
kivymd @ git+https://github.com/kivymd/KivyMD@5ff9d0d
|
||||
kivymd>=2.0.1.dev0
|
||||
|
||||
9
setup.py
9
setup.py
@@ -19,7 +19,7 @@ from typing import Dict, Iterable, List, Optional, Sequence, Set, Tuple, Union
|
||||
|
||||
|
||||
# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it
|
||||
requirement = 'cx-Freeze==7.2.0'
|
||||
requirement = 'cx-Freeze==8.0.0'
|
||||
try:
|
||||
import pkg_resources
|
||||
try:
|
||||
@@ -629,12 +629,13 @@ cx_Freeze.setup(
|
||||
ext_modules=cythonize("_speedups.pyx"),
|
||||
options={
|
||||
"build_exe": {
|
||||
"packages": ["worlds", "kivy", "cymem", "websockets"],
|
||||
"packages": ["worlds", "kivy", "cymem", "websockets", "kivymd"],
|
||||
"includes": [],
|
||||
"excludes": ["numpy", "Cython", "PySide2", "PIL",
|
||||
"pandas", "zstandard"],
|
||||
"pandas"],
|
||||
"zip_includes": [],
|
||||
"zip_include_packages": ["*"],
|
||||
"zip_exclude_packages": ["worlds", "sc2"],
|
||||
"zip_exclude_packages": ["worlds", "sc2", "kivymd"],
|
||||
"include_files": [], # broken in cx 6.14.0, we use more special sauce now
|
||||
"include_msvcr": False,
|
||||
"replace_paths": ["*."],
|
||||
|
||||
@@ -65,8 +65,10 @@ class TestEntranceLookup(unittest.TestCase):
|
||||
"""tests that get_targets shuffles targets between groups when requested"""
|
||||
multiworld = generate_test_multiworld()
|
||||
generate_disconnected_region_grid(multiworld, 5)
|
||||
exits_set = set([ex for region in multiworld.get_regions(1)
|
||||
for ex in region.exits if not ex.connected_region])
|
||||
|
||||
lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True)
|
||||
lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True, usable_exits=exits_set)
|
||||
er_targets = [entrance for region in multiworld.get_regions(1)
|
||||
for entrance in region.entrances if not entrance.parent_region]
|
||||
for entrance in er_targets:
|
||||
@@ -86,8 +88,10 @@ class TestEntranceLookup(unittest.TestCase):
|
||||
"""tests that get_targets does not shuffle targets between groups when requested"""
|
||||
multiworld = generate_test_multiworld()
|
||||
generate_disconnected_region_grid(multiworld, 5)
|
||||
exits_set = set([ex for region in multiworld.get_regions(1)
|
||||
for ex in region.exits if not ex.connected_region])
|
||||
|
||||
lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True)
|
||||
lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True, usable_exits=exits_set)
|
||||
er_targets = [entrance for region in multiworld.get_regions(1)
|
||||
for entrance in region.entrances if not entrance.parent_region]
|
||||
for entrance in er_targets:
|
||||
@@ -99,6 +103,30 @@ class TestEntranceLookup(unittest.TestCase):
|
||||
group_order = [prev := group.randomization_group for group in retrieved_targets if prev != group.randomization_group]
|
||||
self.assertEqual([ERTestGroups.TOP, ERTestGroups.BOTTOM], group_order)
|
||||
|
||||
def test_selective_dead_ends(self):
|
||||
"""test that entrances that EntranceLookup has not been told to consider are ignored when finding dead-ends"""
|
||||
multiworld = generate_test_multiworld()
|
||||
generate_disconnected_region_grid(multiworld, 5)
|
||||
exits_set = set([ex for region in multiworld.get_regions(1)
|
||||
for ex in region.exits if not ex.connected_region
|
||||
and ex.name != "region20_right" and ex.name != "region21_left"])
|
||||
|
||||
lookup = EntranceLookup(multiworld.worlds[1].random, coupled=True, usable_exits=exits_set)
|
||||
er_targets = [entrance for region in multiworld.get_regions(1)
|
||||
for entrance in region.entrances if not entrance.parent_region and
|
||||
entrance.name != "region20_right" and entrance.name != "region21_left"]
|
||||
for entrance in er_targets:
|
||||
lookup.add(entrance)
|
||||
# region 20 is the bottom left corner of the grid, and therefore only has a right entrance from region 21
|
||||
# and a top entrance from region 15; since we've told lookup to ignore the right entrance from region 21,
|
||||
# the top entrance from region 15 should be considered a dead-end
|
||||
dead_end_region = multiworld.get_region("region20", 1)
|
||||
for dead_end in dead_end_region.entrances:
|
||||
if dead_end.name == "region20_top":
|
||||
break
|
||||
# there should be only this one dead-end
|
||||
self.assertTrue(dead_end in lookup.dead_ends)
|
||||
self.assertEqual(len(lookup.dead_ends), 1)
|
||||
|
||||
class TestBakeTargetGroupLookup(unittest.TestCase):
|
||||
def test_lookup_generation(self):
|
||||
@@ -148,7 +176,7 @@ class TestDisconnectForRandomization(unittest.TestCase):
|
||||
e.randomization_group = 1
|
||||
e.connect(r2)
|
||||
|
||||
disconnect_entrance_for_randomization(e)
|
||||
disconnect_entrance_for_randomization(e, one_way_target_name="foo")
|
||||
|
||||
self.assertIsNone(e.connected_region)
|
||||
self.assertEqual([], r1.entrances)
|
||||
@@ -158,10 +186,22 @@ class TestDisconnectForRandomization(unittest.TestCase):
|
||||
|
||||
self.assertEqual(1, len(r2.entrances))
|
||||
self.assertIsNone(r2.entrances[0].parent_region)
|
||||
self.assertEqual("r2", r2.entrances[0].name)
|
||||
self.assertEqual("foo", r2.entrances[0].name)
|
||||
self.assertEqual(EntranceType.ONE_WAY, r2.entrances[0].randomization_type)
|
||||
self.assertEqual(1, r2.entrances[0].randomization_group)
|
||||
|
||||
def test_disconnect_default_1way_no_vanilla_target_raises(self):
|
||||
multiworld = generate_test_multiworld()
|
||||
r1 = Region("r1", 1, multiworld)
|
||||
r2 = Region("r2", 1, multiworld)
|
||||
e = r1.create_exit("e")
|
||||
e.randomization_type = EntranceType.ONE_WAY
|
||||
e.randomization_group = 1
|
||||
e.connect(r2)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
disconnect_entrance_for_randomization(e)
|
||||
|
||||
def test_disconnect_uses_alternate_group(self):
|
||||
multiworld = generate_test_multiworld()
|
||||
r1 = Region("r1", 1, multiworld)
|
||||
@@ -171,7 +211,7 @@ class TestDisconnectForRandomization(unittest.TestCase):
|
||||
e.randomization_group = 1
|
||||
e.connect(r2)
|
||||
|
||||
disconnect_entrance_for_randomization(e, 2)
|
||||
disconnect_entrance_for_randomization(e, 2, "foo")
|
||||
|
||||
self.assertIsNone(e.connected_region)
|
||||
self.assertEqual([], r1.entrances)
|
||||
@@ -181,7 +221,7 @@ class TestDisconnectForRandomization(unittest.TestCase):
|
||||
|
||||
self.assertEqual(1, len(r2.entrances))
|
||||
self.assertIsNone(r2.entrances[0].parent_region)
|
||||
self.assertEqual("r2", r2.entrances[0].name)
|
||||
self.assertEqual("foo", r2.entrances[0].name)
|
||||
self.assertEqual(EntranceType.ONE_WAY, r2.entrances[0].randomization_type)
|
||||
self.assertEqual(2, r2.entrances[0].randomization_group)
|
||||
|
||||
@@ -218,7 +258,7 @@ class TestRandomizeEntrances(unittest.TestCase):
|
||||
self.assertEqual(80, len(result.pairings))
|
||||
self.assertEqual(80, len(result.placements))
|
||||
|
||||
def test_coupling(self):
|
||||
def test_coupled(self):
|
||||
"""tests that in coupled mode, all 2 way transitions have an inverse"""
|
||||
multiworld = generate_test_multiworld()
|
||||
generate_disconnected_region_grid(multiworld, 5)
|
||||
@@ -236,6 +276,36 @@ class TestRandomizeEntrances(unittest.TestCase):
|
||||
# if we didn't visit every placement the verification on_connect doesn't really mean much
|
||||
self.assertEqual(len(result.placements), seen_placement_count)
|
||||
|
||||
def test_uncoupled_succeeds_stage1_indirect_condition(self):
|
||||
multiworld = generate_test_multiworld()
|
||||
menu = multiworld.get_region("Menu", 1)
|
||||
generate_entrance_pair(menu, "_right", ERTestGroups.RIGHT)
|
||||
end = Region("End", 1, multiworld)
|
||||
multiworld.regions.append(end)
|
||||
generate_entrance_pair(end, "_left", ERTestGroups.LEFT)
|
||||
multiworld.register_indirect_condition(end, None)
|
||||
|
||||
result = randomize_entrances(multiworld.worlds[1], False, directionally_matched_group_lookup)
|
||||
self.assertSetEqual({
|
||||
("Menu_right", "End_left"),
|
||||
("End_left", "Menu_right")
|
||||
}, set(result.pairings))
|
||||
|
||||
def test_coupled_succeeds_stage1_indirect_condition(self):
|
||||
multiworld = generate_test_multiworld()
|
||||
menu = multiworld.get_region("Menu", 1)
|
||||
generate_entrance_pair(menu, "_right", ERTestGroups.RIGHT)
|
||||
end = Region("End", 1, multiworld)
|
||||
multiworld.regions.append(end)
|
||||
generate_entrance_pair(end, "_left", ERTestGroups.LEFT)
|
||||
multiworld.register_indirect_condition(end, None)
|
||||
|
||||
result = randomize_entrances(multiworld.worlds[1], True, directionally_matched_group_lookup)
|
||||
self.assertSetEqual({
|
||||
("Menu_right", "End_left"),
|
||||
("End_left", "Menu_right")
|
||||
}, set(result.pairings))
|
||||
|
||||
def test_uncoupled(self):
|
||||
"""tests that in uncoupled mode, no transitions have an (intentional) inverse"""
|
||||
multiworld = generate_test_multiworld()
|
||||
|
||||
14
test/general/test_packages.py
Normal file
14
test/general/test_packages.py
Normal file
@@ -0,0 +1,14 @@
|
||||
import unittest
|
||||
import os
|
||||
|
||||
|
||||
class TestPackages(unittest.TestCase):
|
||||
def test_packages_have_init(self):
|
||||
"""Test that all world folders containing .py files also have a __init__.py file,
|
||||
to indicate full package rather than namespace package."""
|
||||
import Utils
|
||||
|
||||
worlds_path = Utils.local_path("worlds")
|
||||
for dirpath, dirnames, filenames in os.walk(worlds_path):
|
||||
with self.subTest(directory=dirpath):
|
||||
self.assertEqual("__init__.py" in filenames, any(file.endswith(".py") for file in filenames))
|
||||
11
test/general/test_patches.py
Normal file
11
test/general/test_patches.py
Normal file
@@ -0,0 +1,11 @@
|
||||
import unittest
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
from worlds.Files import AutoPatchRegister
|
||||
|
||||
|
||||
class TestPatches(unittest.TestCase):
|
||||
def test_patch_name_matches_game(self) -> None:
|
||||
for game_name in AutoPatchRegister.patch_types:
|
||||
with self.subTest(game=game_name):
|
||||
self.assertIn(game_name, AutoWorldRegister.world_types.keys(),
|
||||
f"Patch '{game_name}' does not match the name of any world.")
|
||||
19
test/general/test_requirements.py
Normal file
19
test/general/test_requirements.py
Normal file
@@ -0,0 +1,19 @@
|
||||
import unittest
|
||||
import os
|
||||
|
||||
|
||||
class TestBase(unittest.TestCase):
|
||||
def test_requirements_file_ends_on_newline(self):
|
||||
"""Test that all requirements files end on a newline"""
|
||||
import Utils
|
||||
requirements_files = [Utils.local_path("requirements.txt"),
|
||||
Utils.local_path("WebHostLib", "requirements.txt")]
|
||||
worlds_path = Utils.local_path("worlds")
|
||||
for entry in os.listdir(worlds_path):
|
||||
requirements_path = os.path.join(worlds_path, entry, "requirements.txt")
|
||||
if os.path.isfile(requirements_path):
|
||||
requirements_files.append(requirements_path)
|
||||
for requirements_file in requirements_files:
|
||||
with self.subTest(path=requirements_file):
|
||||
with open(requirements_file) as f:
|
||||
self.assertEqual(f.read()[-1], "\n")
|
||||
@@ -1,5 +1,5 @@
|
||||
import unittest
|
||||
from typing import List, Tuple
|
||||
from typing import ClassVar, List, Tuple
|
||||
from unittest import TestCase
|
||||
|
||||
from BaseClasses import CollectionState, Location, MultiWorld
|
||||
@@ -7,6 +7,7 @@ from Fill import distribute_items_restrictive
|
||||
from Options import Accessibility
|
||||
from worlds.AutoWorld import AutoWorldRegister, call_all, call_single
|
||||
from ..general import gen_steps, setup_multiworld
|
||||
from ..param import classvar_matrix
|
||||
|
||||
|
||||
class MultiworldTestBase(TestCase):
|
||||
@@ -63,15 +64,18 @@ class TestAllGamesMultiworld(MultiworldTestBase):
|
||||
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
|
||||
|
||||
|
||||
@classvar_matrix(game=AutoWorldRegister.world_types.keys())
|
||||
class TestTwoPlayerMulti(MultiworldTestBase):
|
||||
game: ClassVar[str]
|
||||
|
||||
def test_two_player_single_game_fills(self) -> None:
|
||||
"""Tests that a multiworld of two players for each registered game world can generate."""
|
||||
for world_type in AutoWorldRegister.world_types.values():
|
||||
self.multiworld = setup_multiworld([world_type, world_type], ())
|
||||
for world in self.multiworld.worlds.values():
|
||||
world.options.accessibility.value = Accessibility.option_full
|
||||
self.assertSteps(gen_steps)
|
||||
with self.subTest("filling multiworld", games=world_type.game, seed=self.multiworld.seed):
|
||||
distribute_items_restrictive(self.multiworld)
|
||||
call_all(self.multiworld, "post_fill")
|
||||
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
|
||||
world_type = AutoWorldRegister.world_types[self.game]
|
||||
self.multiworld = setup_multiworld([world_type, world_type], ())
|
||||
for world in self.multiworld.worlds.values():
|
||||
world.options.accessibility.value = Accessibility.option_full
|
||||
self.assertSteps(gen_steps)
|
||||
with self.subTest("filling multiworld", games=world_type.game, seed=self.multiworld.seed):
|
||||
distribute_items_restrictive(self.multiworld)
|
||||
call_all(self.multiworld, "post_fill")
|
||||
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
|
||||
|
||||
46
test/param.py
Normal file
46
test/param.py
Normal file
@@ -0,0 +1,46 @@
|
||||
import itertools
|
||||
import sys
|
||||
from typing import Any, Callable, Iterable
|
||||
|
||||
|
||||
def classvar_matrix(**kwargs: Iterable[Any]) -> Callable[[type], None]:
|
||||
"""
|
||||
Create a new class for each variation of input, allowing to generate a TestCase matrix / parametrization that
|
||||
supports multi-threading and has better reporting for ``unittest --durations=...`` and ``pytest --durations=...``
|
||||
than subtests.
|
||||
|
||||
The kwargs will be set as ClassVars in the newly created classes. Use as ::
|
||||
|
||||
@classvar_matrix(var_name=[value1, value2])
|
||||
class MyTestCase(unittest.TestCase):
|
||||
var_name: typing.ClassVar[...]
|
||||
|
||||
:param kwargs: A dict of ClassVars to set, where key is the variable name and value is a list of all values.
|
||||
:return: A decorator to be applied to a class.
|
||||
"""
|
||||
keys: tuple[str]
|
||||
values: Iterable[Iterable[Any]]
|
||||
keys, values = zip(*kwargs.items())
|
||||
values = map(lambda v: sorted(v) if isinstance(v, (set, frozenset)) else v, values)
|
||||
permutations_dicts = [dict(zip(keys, v)) for v in itertools.product(*values)]
|
||||
|
||||
def decorator(cls: type) -> None:
|
||||
mod = sys.modules[cls.__module__]
|
||||
|
||||
for permutation in permutations_dicts:
|
||||
|
||||
class Unrolled(cls): # type: ignore
|
||||
pass
|
||||
|
||||
for k, v in permutation.items():
|
||||
setattr(Unrolled, k, v)
|
||||
params = ", ".join([f"{k}={repr(v)}" for k, v in permutation.items()])
|
||||
params = f"{{{params}}}"
|
||||
|
||||
Unrolled.__module__ = cls.__module__
|
||||
Unrolled.__qualname__ = f"{cls.__qualname__}{params}"
|
||||
setattr(mod, f"{cls.__name__}{params}", Unrolled)
|
||||
|
||||
return None
|
||||
|
||||
return decorator
|
||||
@@ -9,7 +9,8 @@ from worlds.LauncherComponents import Component, SuffixIdentifier, Type, compone
|
||||
if TYPE_CHECKING:
|
||||
from SNIClient import SNIContext
|
||||
|
||||
component = Component('SNI Client', 'SNIClient', component_type=Type.CLIENT, file_identifier=SuffixIdentifier(".apsoe"))
|
||||
component = Component('SNI Client', 'SNIClient', component_type=Type.CLIENT, file_identifier=SuffixIdentifier(".apsoe"),
|
||||
description="A client for connecting to SNES consoles via Super Nintendo Interface.")
|
||||
components.append(component)
|
||||
|
||||
|
||||
|
||||
@@ -110,6 +110,16 @@ class AutoLogicRegister(type):
|
||||
elif not item_name.startswith("__"):
|
||||
if hasattr(CollectionState, item_name):
|
||||
raise Exception(f"Name conflict on Logic Mixin {name} trying to overwrite {item_name}")
|
||||
|
||||
assert callable(function) or "init_mixin" in dct, (
|
||||
f"{name} defined class variable {item_name} without also having init_mixin.\n\n"
|
||||
"Explanation:\n"
|
||||
"Class variables that will be mutated need to be inintialized as instance variables in init_mixin.\n"
|
||||
"If your LogicMixin variables aren't actually mutable / you don't intend to mutate them, "
|
||||
"there is no point in using LogixMixin.\n"
|
||||
"LogicMixin exists to track custom state variables that change when items are collected/removed."
|
||||
)
|
||||
|
||||
setattr(CollectionState, item_name, function)
|
||||
return new_class
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@ class Component:
|
||||
"""
|
||||
display_name: str
|
||||
"""Used as the GUI button label and the component name in the CLI args"""
|
||||
description: str
|
||||
"""Optional description displayed on the GUI underneath the display name"""
|
||||
type: Type
|
||||
"""
|
||||
Enum "Type" classification of component intent, for filtering in the Launcher GUI
|
||||
@@ -58,8 +60,9 @@ class Component:
|
||||
def __init__(self, display_name: str, script_name: Optional[str] = None, frozen_name: Optional[str] = None,
|
||||
cli: bool = False, icon: str = 'icon', component_type: Optional[Type] = None,
|
||||
func: Optional[Callable] = None, file_identifier: Optional[Callable[[str], bool]] = None,
|
||||
game_name: Optional[str] = None, supports_uri: Optional[bool] = False):
|
||||
game_name: Optional[str] = None, supports_uri: Optional[bool] = False, description: str = "") -> None:
|
||||
self.display_name = display_name
|
||||
self.description = description
|
||||
self.script_name = script_name
|
||||
self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None
|
||||
self.icon = icon
|
||||
@@ -88,7 +91,6 @@ processes = weakref.WeakSet()
|
||||
|
||||
|
||||
def launch_subprocess(func: Callable, name: str | None = None, args: Tuple[str, ...] = ()) -> None:
|
||||
global processes
|
||||
import multiprocessing
|
||||
process = multiprocessing.Process(target=func, name=name, args=args)
|
||||
process.start()
|
||||
|
||||
@@ -276,6 +276,6 @@ def launch(*launch_args: str) -> None:
|
||||
|
||||
Utils.init_logging("BizHawkClient", exception_logger="Client")
|
||||
import colorama
|
||||
colorama.init()
|
||||
colorama.just_fix_windows_console()
|
||||
asyncio.run(main())
|
||||
colorama.deinit()
|
||||
|
||||
@@ -3,4 +3,4 @@ mpyq>=0.2.5
|
||||
portpicker>=1.5.2
|
||||
aiohttp>=3.8.4
|
||||
loguru>=0.7.0
|
||||
protobuf==3.20.3
|
||||
protobuf==3.20.3
|
||||
|
||||
@@ -238,14 +238,12 @@ class AdventureWorld(World):
|
||||
|
||||
def create_regions(self) -> None:
|
||||
create_regions(self.options, self.multiworld, self.player, self.dragon_rooms)
|
||||
|
||||
set_rules = set_rules
|
||||
|
||||
def generate_basic(self) -> None:
|
||||
self.multiworld.get_location("Chalice Home", self.player).place_locked_item(
|
||||
self.create_event("Victory", ItemClassification.progression))
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
|
||||
|
||||
set_rules = set_rules
|
||||
|
||||
def pre_fill(self):
|
||||
# Place empty items in filler locations here, to limit
|
||||
# the number of exported empty items and the density of stuff in overworld.
|
||||
|
||||
@@ -261,6 +261,6 @@ def launch():
|
||||
# options = Utils.get_options()
|
||||
|
||||
import colorama
|
||||
colorama.init()
|
||||
colorama.just_fix_windows_console()
|
||||
asyncio.run(main())
|
||||
colorama.deinit()
|
||||
|
||||
@@ -2,7 +2,7 @@ from typing import List, TYPE_CHECKING, Dict, Any
|
||||
from schema import Schema, Optional
|
||||
from dataclasses import dataclass
|
||||
from worlds.AutoWorld import PerGameCommonOptions
|
||||
from Options import Range, Toggle, DeathLink, Choice, OptionDict, DefaultOnToggle, OptionGroup
|
||||
from Options import Range, Toggle, DeathLink, Choice, OptionDict, DefaultOnToggle, OptionGroup, StartInventoryPool
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import HatInTimeWorld
|
||||
@@ -625,6 +625,8 @@ class ParadeTrapWeight(Range):
|
||||
|
||||
@dataclass
|
||||
class AHITOptions(PerGameCommonOptions):
|
||||
start_inventory_from_pool: StartInventoryPool
|
||||
|
||||
EndGoal: EndGoal
|
||||
ActRandomizer: ActRandomizer
|
||||
ActPlando: ActPlando
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from BaseClasses import Item, ItemClassification, Tutorial, Location, MultiWorld
|
||||
from .Items import item_table, create_item, relic_groups, act_contracts, create_itempool, get_shop_trap_name, \
|
||||
calculate_yarn_costs, alps_hooks
|
||||
calculate_yarn_costs, alps_hooks, junk_weights
|
||||
from .Regions import create_regions, randomize_act_entrances, chapter_act_info, create_events, get_shuffled_region
|
||||
from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID, \
|
||||
get_total_locations
|
||||
@@ -78,6 +78,9 @@ class HatInTimeWorld(World):
|
||||
self.nyakuza_thug_items: Dict[str, int] = {}
|
||||
self.badge_seller_count: int = 0
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return self.random.choices(list(junk_weights.keys()), weights=junk_weights.values(), k=1)[0]
|
||||
|
||||
def generate_early(self):
|
||||
adjust_options(self)
|
||||
|
||||
|
||||
@@ -515,10 +515,15 @@ def _populate_sprite_table():
|
||||
logging.debug(f"Spritefile {file} could not be loaded as a valid sprite.")
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor() as pool:
|
||||
for dir in [user_path('data', 'sprites', 'alttpr'), user_path('data', 'sprites', 'custom')]:
|
||||
sprite_paths = [user_path('data', 'sprites', 'alttpr'), user_path('data', 'sprites', 'custom')]
|
||||
for dir in [dir for dir in sprite_paths if os.path.isdir(dir)]:
|
||||
for file in os.listdir(dir):
|
||||
pool.submit(load_sprite_from_file, os.path.join(dir, file))
|
||||
|
||||
if "link" not in _sprite_table:
|
||||
logging.info("Link sprite was not loaded. Loading link from base rom")
|
||||
load_sprite_from_file(get_base_rom_path())
|
||||
|
||||
|
||||
class Sprite():
|
||||
sprite_size = 28672
|
||||
@@ -554,6 +559,11 @@ class Sprite():
|
||||
self.sprite = filedata[0x80000:0x87000]
|
||||
self.palette = filedata[0xDD308:0xDD380]
|
||||
self.glove_palette = filedata[0xDEDF5:0xDEDF9]
|
||||
h = hashlib.md5()
|
||||
h.update(filedata)
|
||||
if h.hexdigest() == LTTPJPN10HASH:
|
||||
self.name = "Link"
|
||||
self.author_name = "Nintendo"
|
||||
elif filedata.startswith(b'ZSPR'):
|
||||
self.from_zspr(filedata, filename)
|
||||
else:
|
||||
|
||||
@@ -121,6 +121,7 @@ class ALTTPWeb(WebWorld):
|
||||
)
|
||||
|
||||
tutorials = [setup_en, setup_de, setup_es, setup_fr, msu, msu_es, msu_fr, plando, oof_sound]
|
||||
game_info_languages = ["en", "fr"]
|
||||
|
||||
|
||||
class ALTTPWorld(World):
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
maseya-z3pr>=1.0.0rc1
|
||||
xxtea>=3.0.0
|
||||
xxtea>=3.0.0
|
||||
|
||||
@@ -41,6 +41,7 @@ class AquariaWeb(WebWorld):
|
||||
)
|
||||
|
||||
tutorials = [setup, setup_fr]
|
||||
game_info_languages = ["en", "fr"]
|
||||
|
||||
|
||||
class AquariaWorld(World):
|
||||
|
||||
@@ -1,6 +1,31 @@
|
||||
# Celeste 64 - Changelog
|
||||
|
||||
|
||||
## v1.3
|
||||
|
||||
### Features:
|
||||
|
||||
- New optional Location Checks
|
||||
- Checkpointsanity
|
||||
- Hair Color
|
||||
- Allows for setting of Maddy's hair color in each of No Dash, One Dash, Two Dash, and Feather states
|
||||
- Other Player Ghosts
|
||||
- A game config option allows you to see ghosts of other Celeste 64 players in the multiworld
|
||||
|
||||
### Quality of Life:
|
||||
|
||||
- Checkpoint Warping
|
||||
- Received Checkpoint items allow for warping to their respective checkpoint
|
||||
- These items are on their respective checkpoint location if Checkpointsanity is disabled
|
||||
- Logic accounts for being able to warp to otherwise inaccessible areas
|
||||
- Checkpoints are a possible option for a starting item on Standard Logic + Move Shuffle + Checkpointsanity
|
||||
- New Options toggle to enable/disable background input
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
- Traffic Blocks now correctly appear disabled within Cassettes
|
||||
|
||||
|
||||
## v1.2
|
||||
|
||||
### Features:
|
||||
|
||||
@@ -39,6 +39,22 @@ move_item_data_table: Dict[str, Celeste64ItemData] = {
|
||||
ItemName.climb: Celeste64ItemData(celeste_64_base_id + 0xD, ItemClassification.progression),
|
||||
}
|
||||
|
||||
item_data_table: Dict[str, Celeste64ItemData] = {**collectable_item_data_table, **unlockable_item_data_table, **move_item_data_table}
|
||||
checkpoint_item_data_table: Dict[str, Celeste64ItemData] = {
|
||||
ItemName.checkpoint_1: Celeste64ItemData(celeste_64_base_id + 0x20, ItemClassification.progression),
|
||||
ItemName.checkpoint_2: Celeste64ItemData(celeste_64_base_id + 0x21, ItemClassification.progression),
|
||||
ItemName.checkpoint_3: Celeste64ItemData(celeste_64_base_id + 0x22, ItemClassification.progression),
|
||||
ItemName.checkpoint_4: Celeste64ItemData(celeste_64_base_id + 0x23, ItemClassification.progression),
|
||||
ItemName.checkpoint_5: Celeste64ItemData(celeste_64_base_id + 0x24, ItemClassification.progression),
|
||||
ItemName.checkpoint_6: Celeste64ItemData(celeste_64_base_id + 0x25, ItemClassification.progression),
|
||||
ItemName.checkpoint_7: Celeste64ItemData(celeste_64_base_id + 0x26, ItemClassification.progression),
|
||||
ItemName.checkpoint_8: Celeste64ItemData(celeste_64_base_id + 0x27, ItemClassification.progression),
|
||||
ItemName.checkpoint_9: Celeste64ItemData(celeste_64_base_id + 0x28, ItemClassification.progression),
|
||||
ItemName.checkpoint_10: Celeste64ItemData(celeste_64_base_id + 0x29, ItemClassification.progression),
|
||||
}
|
||||
|
||||
item_data_table: Dict[str, Celeste64ItemData] = {**collectable_item_data_table,
|
||||
**unlockable_item_data_table,
|
||||
**move_item_data_table,
|
||||
**checkpoint_item_data_table}
|
||||
|
||||
item_table = {name: data.code for name, data in item_data_table.items() if data.code is not None}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import Dict, NamedTuple, Optional
|
||||
|
||||
from BaseClasses import Location
|
||||
from .Names import LocationName
|
||||
from .Names import LocationName, RegionName
|
||||
|
||||
|
||||
celeste_64_base_id: int = 0xCA0000
|
||||
@@ -17,66 +17,80 @@ class Celeste64LocationData(NamedTuple):
|
||||
|
||||
|
||||
strawberry_location_data_table: Dict[str, Celeste64LocationData] = {
|
||||
LocationName.strawberry_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x00),
|
||||
LocationName.strawberry_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x01),
|
||||
LocationName.strawberry_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x02),
|
||||
LocationName.strawberry_4: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x03),
|
||||
LocationName.strawberry_5: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x04),
|
||||
LocationName.strawberry_6: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x05),
|
||||
LocationName.strawberry_7: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x06),
|
||||
LocationName.strawberry_8: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x07),
|
||||
LocationName.strawberry_9: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x08),
|
||||
LocationName.strawberry_10: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x09),
|
||||
LocationName.strawberry_11: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0A),
|
||||
LocationName.strawberry_12: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0B),
|
||||
LocationName.strawberry_13: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0C),
|
||||
LocationName.strawberry_14: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0D),
|
||||
LocationName.strawberry_15: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0E),
|
||||
LocationName.strawberry_16: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0F),
|
||||
LocationName.strawberry_17: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x10),
|
||||
LocationName.strawberry_18: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x11),
|
||||
LocationName.strawberry_19: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x12),
|
||||
LocationName.strawberry_20: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x13),
|
||||
LocationName.strawberry_21: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x14),
|
||||
LocationName.strawberry_22: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x15),
|
||||
LocationName.strawberry_23: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x16),
|
||||
LocationName.strawberry_24: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x17),
|
||||
LocationName.strawberry_25: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x18),
|
||||
LocationName.strawberry_26: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x19),
|
||||
LocationName.strawberry_27: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x1A),
|
||||
LocationName.strawberry_28: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x1B),
|
||||
LocationName.strawberry_29: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x1C),
|
||||
LocationName.strawberry_30: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x1D),
|
||||
LocationName.strawberry_1: Celeste64LocationData(RegionName.intro_islands, celeste_64_base_id + 0x00),
|
||||
LocationName.strawberry_2: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x01),
|
||||
LocationName.strawberry_3: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x02),
|
||||
LocationName.strawberry_4: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x03),
|
||||
LocationName.strawberry_5: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x04),
|
||||
LocationName.strawberry_6: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x05),
|
||||
LocationName.strawberry_7: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x06),
|
||||
LocationName.strawberry_8: Celeste64LocationData(RegionName.nw_girders_island, celeste_64_base_id + 0x07),
|
||||
LocationName.strawberry_9: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x08),
|
||||
LocationName.strawberry_10: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x09),
|
||||
LocationName.strawberry_11: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x0A),
|
||||
LocationName.strawberry_12: Celeste64LocationData(RegionName.badeline_tower_lower, celeste_64_base_id + 0x0B),
|
||||
LocationName.strawberry_13: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x0C),
|
||||
LocationName.strawberry_14: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x0D),
|
||||
LocationName.strawberry_15: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x0E),
|
||||
LocationName.strawberry_16: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x0F),
|
||||
LocationName.strawberry_17: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x10),
|
||||
LocationName.strawberry_18: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x11),
|
||||
LocationName.strawberry_19: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x12),
|
||||
LocationName.strawberry_20: Celeste64LocationData(RegionName.badeline_tower_lower, celeste_64_base_id + 0x13),
|
||||
LocationName.strawberry_21: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x14),
|
||||
LocationName.strawberry_22: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x15),
|
||||
LocationName.strawberry_23: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x16),
|
||||
LocationName.strawberry_24: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x17),
|
||||
LocationName.strawberry_25: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x18),
|
||||
LocationName.strawberry_26: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x19),
|
||||
LocationName.strawberry_27: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x1A),
|
||||
LocationName.strawberry_28: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x1B),
|
||||
LocationName.strawberry_29: Celeste64LocationData(RegionName.badeline_tower_upper, celeste_64_base_id + 0x1C),
|
||||
LocationName.strawberry_30: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x1D),
|
||||
}
|
||||
|
||||
friend_location_data_table: Dict[str, Celeste64LocationData] = {
|
||||
LocationName.granny_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x00),
|
||||
LocationName.granny_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x01),
|
||||
LocationName.granny_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x02),
|
||||
LocationName.theo_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x03),
|
||||
LocationName.theo_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x04),
|
||||
LocationName.theo_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x05),
|
||||
LocationName.badeline_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x06),
|
||||
LocationName.badeline_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x07),
|
||||
LocationName.badeline_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x08),
|
||||
LocationName.granny_1: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x00),
|
||||
LocationName.granny_2: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x01),
|
||||
LocationName.granny_3: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x02),
|
||||
LocationName.theo_1: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x03),
|
||||
LocationName.theo_2: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x04),
|
||||
LocationName.theo_3: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x05),
|
||||
LocationName.badeline_1: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x100 + 0x06),
|
||||
LocationName.badeline_2: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x100 + 0x07),
|
||||
LocationName.badeline_3: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x100 + 0x08),
|
||||
}
|
||||
|
||||
sign_location_data_table: Dict[str, Celeste64LocationData] = {
|
||||
LocationName.sign_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x00),
|
||||
LocationName.sign_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x01),
|
||||
LocationName.sign_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x02),
|
||||
LocationName.sign_4: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x03),
|
||||
LocationName.sign_5: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x04),
|
||||
LocationName.sign_1: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x200 + 0x00),
|
||||
LocationName.sign_2: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x200 + 0x01),
|
||||
LocationName.sign_3: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x200 + 0x02),
|
||||
LocationName.sign_4: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x200 + 0x03),
|
||||
LocationName.sign_5: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x200 + 0x04),
|
||||
}
|
||||
|
||||
car_location_data_table: Dict[str, Celeste64LocationData] = {
|
||||
LocationName.car_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x300 + 0x00),
|
||||
LocationName.car_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x300 + 0x01),
|
||||
LocationName.car_1: Celeste64LocationData(RegionName.intro_islands, celeste_64_base_id + 0x300 + 0x00),
|
||||
LocationName.car_2: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x300 + 0x01),
|
||||
}
|
||||
|
||||
checkpoint_location_data_table: Dict[str, Celeste64LocationData] = {
|
||||
LocationName.checkpoint_1: Celeste64LocationData(RegionName.intro_islands, celeste_64_base_id + 0x400 + 0x00),
|
||||
LocationName.checkpoint_2: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x400 + 0x01),
|
||||
LocationName.checkpoint_3: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x400 + 0x02),
|
||||
LocationName.checkpoint_4: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x400 + 0x03),
|
||||
LocationName.checkpoint_5: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x400 + 0x04),
|
||||
LocationName.checkpoint_6: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x400 + 0x05),
|
||||
LocationName.checkpoint_7: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x400 + 0x06),
|
||||
LocationName.checkpoint_8: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x400 + 0x07),
|
||||
LocationName.checkpoint_9: Celeste64LocationData(RegionName.badeline_tower_upper, celeste_64_base_id + 0x400 + 0x08),
|
||||
LocationName.checkpoint_10: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x400 + 0x09),
|
||||
}
|
||||
|
||||
location_data_table: Dict[str, Celeste64LocationData] = {**strawberry_location_data_table,
|
||||
**friend_location_data_table,
|
||||
**sign_location_data_table,
|
||||
**car_location_data_table}
|
||||
**car_location_data_table,
|
||||
**checkpoint_location_data_table}
|
||||
|
||||
location_table = {name: data.address for name, data in location_data_table.items() if data.address is not None}
|
||||
|
||||
@@ -15,3 +15,18 @@ ground_dash = "Ground Dash"
|
||||
air_dash = "Air Dash"
|
||||
skid_jump = "Skid Jump"
|
||||
climb = "Climb"
|
||||
|
||||
# Checkpoint Items
|
||||
checkpoint_1 = "Intro Checkpoint"
|
||||
checkpoint_2 = "Granny Checkpoint"
|
||||
checkpoint_3 = "South-East Tower Checkpoint"
|
||||
checkpoint_4 = "Climb Sign Checkpoint"
|
||||
checkpoint_5 = "Freeway Checkpoint"
|
||||
checkpoint_6 = "Freeway Feather Checkpoint"
|
||||
checkpoint_7 = "Feather Maze Checkpoint"
|
||||
checkpoint_8 = "Double Dash House Checkpoint"
|
||||
checkpoint_9 = "Badeline Tower Checkpoint"
|
||||
checkpoint_10 = "Badeline Island Checkpoint"
|
||||
|
||||
# Item used for logic definitions that are not possible with the given options
|
||||
cannot_access = "CANNOT ACCESS"
|
||||
|
||||
@@ -10,7 +10,7 @@ strawberry_8 = "Traffic Block Strawberry"
|
||||
strawberry_9 = "South-West Dash Refills Strawberry"
|
||||
strawberry_10 = "South-East Tower Side Strawberry"
|
||||
strawberry_11 = "Girders Strawberry"
|
||||
strawberry_12 = "North-East Tower Bottom Strawberry"
|
||||
strawberry_12 = "Badeline Tower Bottom Strawberry"
|
||||
strawberry_13 = "Breakable Blocks Strawberry"
|
||||
strawberry_14 = "Feather Maze Strawberry"
|
||||
strawberry_15 = "Feather Chain Strawberry"
|
||||
@@ -18,7 +18,7 @@ strawberry_16 = "Feather Hidden Strawberry"
|
||||
strawberry_17 = "Double Dash Puzzle Strawberry"
|
||||
strawberry_18 = "Double Dash Spike Climb Strawberry"
|
||||
strawberry_19 = "Double Dash Spring Strawberry"
|
||||
strawberry_20 = "North-East Tower Breakable Bottom Strawberry"
|
||||
strawberry_20 = "Badeline Tower Breakable Bottom Strawberry"
|
||||
strawberry_21 = "Theo Tower Lower Cassette Strawberry"
|
||||
strawberry_22 = "Theo Tower Upper Cassette Strawberry"
|
||||
strawberry_23 = "South End of Bridge Cassette Strawberry"
|
||||
@@ -27,8 +27,8 @@ strawberry_25 = "Cassette Hidden in the House Strawberry"
|
||||
strawberry_26 = "North End of Bridge Cassette Strawberry"
|
||||
strawberry_27 = "Distant Feather Cassette Strawberry"
|
||||
strawberry_28 = "Feather Arches Cassette Strawberry"
|
||||
strawberry_29 = "North-East Tower Cassette Strawberry"
|
||||
strawberry_30 = "Badeline Cassette Strawberry"
|
||||
strawberry_29 = "Badeline Tower Cassette Strawberry"
|
||||
strawberry_30 = "Badeline Island Cassette Strawberry"
|
||||
|
||||
# Friend Locations
|
||||
granny_1 = "Granny Conversation 1"
|
||||
@@ -51,3 +51,15 @@ sign_5 = "Credits Sign"
|
||||
# Car Locations
|
||||
car_1 = "Intro Car"
|
||||
car_2 = "Secret Car"
|
||||
|
||||
# Checkpoint Locations
|
||||
checkpoint_1 = "Intro Checkpoint"
|
||||
checkpoint_2 = "Granny Checkpoint"
|
||||
checkpoint_3 = "South-East Tower Checkpoint"
|
||||
checkpoint_4 = "Climb Sign Checkpoint"
|
||||
checkpoint_5 = "Freeway Checkpoint"
|
||||
checkpoint_6 = "Freeway Feather Checkpoint"
|
||||
checkpoint_7 = "Feather Maze Checkpoint"
|
||||
checkpoint_8 = "Double Dash House Checkpoint"
|
||||
checkpoint_9 = "Badeline Tower Checkpoint"
|
||||
checkpoint_10 = "Badeline Island Checkpoint"
|
||||
|
||||
13
worlds/celeste64/Names/RegionName.py
Normal file
13
worlds/celeste64/Names/RegionName.py
Normal file
@@ -0,0 +1,13 @@
|
||||
# Level Base Regions
|
||||
forsaken_city = "Forsaken City"
|
||||
|
||||
# Forsaken City Regions
|
||||
intro_islands = "Intro Islands"
|
||||
granny_island = "Granny Island"
|
||||
highway_island = "Freeway Island"
|
||||
nw_girders_island = "North-West Girders Island"
|
||||
ne_feathers_island = "North-East Feathers Island"
|
||||
se_house_island = "South-East House Island"
|
||||
badeline_tower_lower = "Badeline Tower Lower"
|
||||
badeline_tower_upper = "Badeline Tower Upper"
|
||||
badeline_island = "Badeline Island"
|
||||
@@ -1,6 +1,8 @@
|
||||
from dataclasses import dataclass
|
||||
import random
|
||||
|
||||
from Options import Choice, Range, Toggle, DeathLink, OptionGroup, PerGameCommonOptions
|
||||
from Options import Choice, TextChoice, Range, Toggle, DeathLink, OptionGroup, PerGameCommonOptions, OptionError
|
||||
from worlds.AutoWorld import World
|
||||
|
||||
|
||||
class DeathLinkAmnesty(Range):
|
||||
@@ -18,7 +20,7 @@ class TotalStrawberries(Range):
|
||||
"""
|
||||
display_name = "Total Strawberries"
|
||||
range_start = 0
|
||||
range_end = 46
|
||||
range_end = 55
|
||||
default = 20
|
||||
|
||||
class StrawberriesRequiredPercentage(Range):
|
||||
@@ -73,6 +75,93 @@ class Carsanity(Toggle):
|
||||
"""
|
||||
display_name = "Carsanity"
|
||||
|
||||
class Checkpointsanity(Toggle):
|
||||
"""
|
||||
Whether activating Checkpoints grants location checks
|
||||
|
||||
Activating this will also shuffle items into the pool which allow usage and warping to each Checkpoint
|
||||
"""
|
||||
display_name = "Checkpointsanity"
|
||||
|
||||
|
||||
class ColorChoice(TextChoice):
|
||||
option_strawberry = 0xDB2C00
|
||||
option_empty = 0x6EC0FF
|
||||
option_double = 0xFA91FF
|
||||
option_golden = 0xF2D450
|
||||
option_baddy = 0x9B3FB5
|
||||
option_fire_red = 0xFF0000
|
||||
option_maroon = 0x800000
|
||||
option_salmon = 0xFF3A65
|
||||
option_orange = 0xD86E0A
|
||||
option_lime_green = 0x8DF920
|
||||
option_bright_green = 0x0DAF05
|
||||
option_forest_green = 0x132818
|
||||
option_royal_blue = 0x0036BF
|
||||
option_brown = 0xB78726
|
||||
option_black = 0x000000
|
||||
option_white = 0xFFFFFF
|
||||
option_grey = 0x808080
|
||||
option_any_color = -1
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str) -> Choice:
|
||||
text = text.lower()
|
||||
if text == "random":
|
||||
choice_list = list(cls.name_lookup)
|
||||
choice_list.remove(cls.option_any_color)
|
||||
return cls(random.choice(choice_list))
|
||||
return super().from_text(text)
|
||||
|
||||
|
||||
class MadelineOneDashHairColor(ColorChoice):
|
||||
"""
|
||||
What color Madeline's hair is when she has one dash
|
||||
|
||||
The `any_color` option will choose a fully random color
|
||||
|
||||
A custom color entry may be supplied as a 6-character RGB hex color code
|
||||
e.g. F542C8
|
||||
"""
|
||||
display_name = "Madeline One Dash Hair Color"
|
||||
default = ColorChoice.option_strawberry
|
||||
|
||||
class MadelineTwoDashHairColor(ColorChoice):
|
||||
"""
|
||||
What color Madeline's hair is when she has two dashes
|
||||
|
||||
The `any_color` option will choose a fully random color
|
||||
|
||||
A custom color entry may be supplied as a 6-character RGB hex color code
|
||||
e.g. F542C8
|
||||
"""
|
||||
display_name = "Madeline Two Dash Hair Color"
|
||||
default = ColorChoice.option_double
|
||||
|
||||
class MadelineNoDashHairColor(ColorChoice):
|
||||
"""
|
||||
What color Madeline's hair is when she has no dashes
|
||||
|
||||
The `any_color` option will choose a fully random color
|
||||
|
||||
A custom color entry may be supplied as a 6-character RGB hex color code
|
||||
e.g. F542C8
|
||||
"""
|
||||
display_name = "Madeline No Dash Hair Color"
|
||||
default = ColorChoice.option_empty
|
||||
|
||||
class MadelineFeatherHairColor(ColorChoice):
|
||||
"""
|
||||
What color Madeline's hair is when she has a feather
|
||||
|
||||
The `any_color` option will choose a fully random color
|
||||
|
||||
A custom color entry may be supplied as a 6-character RGB hex color code
|
||||
e.g. F542C8
|
||||
"""
|
||||
display_name = "Madeline Feather Hair Color"
|
||||
default = ColorChoice.option_golden
|
||||
|
||||
|
||||
class BadelineChaserSource(Choice):
|
||||
"""
|
||||
@@ -119,6 +208,13 @@ celeste_64_option_groups = [
|
||||
Friendsanity,
|
||||
Signsanity,
|
||||
Carsanity,
|
||||
Checkpointsanity,
|
||||
]),
|
||||
OptionGroup("Aesthetic Options", [
|
||||
MadelineOneDashHairColor,
|
||||
MadelineTwoDashHairColor,
|
||||
MadelineNoDashHairColor,
|
||||
MadelineFeatherHairColor,
|
||||
]),
|
||||
OptionGroup("Badeline Chasers", [
|
||||
BadelineChaserSource,
|
||||
@@ -142,7 +238,68 @@ class Celeste64Options(PerGameCommonOptions):
|
||||
friendsanity: Friendsanity
|
||||
signsanity: Signsanity
|
||||
carsanity: Carsanity
|
||||
checkpointsanity: Checkpointsanity
|
||||
|
||||
madeline_one_dash_hair_color: MadelineOneDashHairColor
|
||||
madeline_two_dash_hair_color: MadelineTwoDashHairColor
|
||||
madeline_no_dash_hair_color: MadelineNoDashHairColor
|
||||
madeline_feather_hair_color: MadelineFeatherHairColor
|
||||
|
||||
badeline_chaser_source: BadelineChaserSource
|
||||
badeline_chaser_frequency: BadelineChaserFrequency
|
||||
badeline_chaser_speed: BadelineChaserSpeed
|
||||
|
||||
|
||||
def resolve_options(world: World):
|
||||
# One Dash Hair
|
||||
if isinstance(world.options.madeline_one_dash_hair_color.value, str):
|
||||
try:
|
||||
world.madeline_one_dash_hair_color = int(world.options.madeline_one_dash_hair_color.value.strip("#")[:6], 16)
|
||||
except ValueError:
|
||||
raise OptionError(f"Invalid input for option `madeline_one_dash_hair_color`:"
|
||||
f"{world.options.madeline_one_dash_hair_color.value} for "
|
||||
f"{world.player_name}")
|
||||
elif world.options.madeline_one_dash_hair_color.value == ColorChoice.option_any_color:
|
||||
world.madeline_one_dash_hair_color = world.random.randint(0, 0xFFFFFF)
|
||||
else:
|
||||
world.madeline_one_dash_hair_color = world.options.madeline_one_dash_hair_color.value
|
||||
|
||||
# Two Dash Hair
|
||||
if isinstance(world.options.madeline_two_dash_hair_color.value, str):
|
||||
try:
|
||||
world.madeline_two_dash_hair_color = int(world.options.madeline_two_dash_hair_color.value.strip("#")[:6], 16)
|
||||
except ValueError:
|
||||
raise OptionError(f"Invalid input for option `madeline_two_dash_hair_color`:"
|
||||
f"{world.options.madeline_two_dash_hair_color.value} for "
|
||||
f"{world.player_name}")
|
||||
elif world.options.madeline_two_dash_hair_color.value == ColorChoice.option_any_color:
|
||||
world.madeline_two_dash_hair_color = world.random.randint(0, 0xFFFFFF)
|
||||
else:
|
||||
world.madeline_two_dash_hair_color = world.options.madeline_two_dash_hair_color.value
|
||||
|
||||
# No Dash Hair
|
||||
if isinstance(world.options.madeline_no_dash_hair_color.value, str):
|
||||
try:
|
||||
world.madeline_no_dash_hair_color = int(world.options.madeline_no_dash_hair_color.value.strip("#")[:6], 16)
|
||||
except ValueError:
|
||||
raise OptionError(f"Invalid input for option `madeline_no_dash_hair_color`:"
|
||||
f"{world.options.madeline_no_dash_hair_color.value} for "
|
||||
f"{world.player_name}")
|
||||
elif world.options.madeline_no_dash_hair_color.value == ColorChoice.option_any_color:
|
||||
world.madeline_no_dash_hair_color = world.random.randint(0, 0xFFFFFF)
|
||||
else:
|
||||
world.madeline_no_dash_hair_color = world.options.madeline_no_dash_hair_color.value
|
||||
|
||||
# Feather Hair
|
||||
if isinstance(world.options.madeline_feather_hair_color.value, str):
|
||||
try:
|
||||
world.madeline_feather_hair_color = int(world.options.madeline_feather_hair_color.value.strip("#")[:6], 16)
|
||||
except ValueError:
|
||||
raise OptionError(f"Invalid input for option `madeline_feather_hair_color`:"
|
||||
f"{world.options.madeline_feather_hair_color.value} for "
|
||||
f"{world.player_name}")
|
||||
elif world.options.madeline_feather_hair_color.value == ColorChoice.option_any_color:
|
||||
world.madeline_feather_hair_color = world.random.randint(0, 0xFFFFFF)
|
||||
else:
|
||||
world.madeline_feather_hair_color = world.options.madeline_feather_hair_color.value
|
||||
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
from typing import Dict, List, NamedTuple
|
||||
|
||||
from .Names import RegionName
|
||||
|
||||
class Celeste64RegionData(NamedTuple):
|
||||
connecting_regions: List[str] = []
|
||||
|
||||
|
||||
region_data_table: Dict[str, Celeste64RegionData] = {
|
||||
"Menu": Celeste64RegionData(["Forsaken City"]),
|
||||
"Forsaken City": Celeste64RegionData(),
|
||||
"Menu": Celeste64RegionData([RegionName.forsaken_city]),
|
||||
|
||||
RegionName.forsaken_city: Celeste64RegionData([RegionName.intro_islands, RegionName.granny_island, RegionName.highway_island, RegionName.ne_feathers_island, RegionName.se_house_island, RegionName.badeline_tower_upper, RegionName.badeline_island]),
|
||||
|
||||
RegionName.intro_islands: Celeste64RegionData([RegionName.granny_island]),
|
||||
RegionName.granny_island: Celeste64RegionData([RegionName.highway_island, RegionName.nw_girders_island, RegionName.badeline_tower_lower, RegionName.se_house_island]),
|
||||
RegionName.highway_island: Celeste64RegionData([RegionName.granny_island, RegionName.ne_feathers_island, RegionName.nw_girders_island]),
|
||||
RegionName.nw_girders_island: Celeste64RegionData([RegionName.highway_island]),
|
||||
RegionName.ne_feathers_island: Celeste64RegionData([RegionName.se_house_island, RegionName.highway_island, RegionName.badeline_tower_lower, RegionName.badeline_tower_upper]),
|
||||
RegionName.se_house_island: Celeste64RegionData([RegionName.ne_feathers_island, RegionName.granny_island, RegionName.badeline_tower_lower]),
|
||||
RegionName.badeline_tower_lower: Celeste64RegionData([RegionName.se_house_island, RegionName.ne_feathers_island, RegionName.granny_island, RegionName.badeline_tower_upper]),
|
||||
RegionName.badeline_tower_upper: Celeste64RegionData([RegionName.badeline_island, RegionName.badeline_tower_lower, RegionName.se_house_island, RegionName.ne_feathers_island, RegionName.granny_island]),
|
||||
RegionName.badeline_island: Celeste64RegionData([RegionName.badeline_tower_upper, RegionName.granny_island, RegionName.highway_island]),
|
||||
}
|
||||
|
||||
@@ -1,265 +1,85 @@
|
||||
from typing import Dict, List
|
||||
from typing import Dict, List, Tuple, Callable
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
from BaseClasses import CollectionState, Region
|
||||
from worlds.generic.Rules import set_rule
|
||||
|
||||
from . import Celeste64World
|
||||
from .Names import ItemName, LocationName
|
||||
from .Names import ItemName, LocationName, RegionName
|
||||
|
||||
|
||||
def set_rules(world: Celeste64World):
|
||||
if world.options.logic_difficulty == "standard":
|
||||
if world.options.move_shuffle:
|
||||
world.active_logic_mapping = location_standard_moves_logic
|
||||
else:
|
||||
world.active_logic_mapping = location_standard_logic
|
||||
world.active_logic_mapping = location_standard_moves_logic
|
||||
world.active_region_logic_mapping = region_standard_moves_logic
|
||||
else:
|
||||
if world.options.move_shuffle:
|
||||
world.active_logic_mapping = location_hard_moves_logic
|
||||
else:
|
||||
world.active_logic_mapping = location_hard_logic
|
||||
world.active_logic_mapping = location_hard_moves_logic
|
||||
world.active_region_logic_mapping = region_hard_moves_logic
|
||||
|
||||
for location in world.multiworld.get_locations(world.player):
|
||||
set_rule(location, lambda state, location=location: location_rule(state, world, location.name))
|
||||
|
||||
if world.options.logic_difficulty == "standard":
|
||||
if world.options.move_shuffle:
|
||||
world.goal_logic_mapping = goal_standard_moves_logic
|
||||
else:
|
||||
world.goal_logic_mapping = goal_standard_logic
|
||||
else:
|
||||
if world.options.move_shuffle:
|
||||
world.goal_logic_mapping = goal_hard_moves_logic
|
||||
else:
|
||||
world.goal_logic_mapping = goal_hard_logic
|
||||
|
||||
# Completion condition.
|
||||
world.multiworld.completion_condition[world.player] = lambda state: goal_rule(state, world)
|
||||
|
||||
|
||||
goal_standard_logic: List[List[str]] = [[ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.double_dash_refill]]
|
||||
goal_hard_logic: List[List[str]] = [[]]
|
||||
goal_standard_moves_logic: List[List[str]] = [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb]]
|
||||
goal_hard_moves_logic: List[List[str]] = [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb],
|
||||
[ItemName.traffic_block, ItemName.air_dash, ItemName.skid_jump],
|
||||
[ItemName.ground_dash, ItemName.air_dash, ItemName.skid_jump],
|
||||
[ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.ground_dash, ItemName.air_dash]]
|
||||
|
||||
|
||||
location_standard_logic: Dict[str, List[List[str]]] = {
|
||||
LocationName.strawberry_4: [[ItemName.traffic_block, ItemName.breakables]],
|
||||
LocationName.strawberry_6: [[ItemName.dash_refill],
|
||||
[ItemName.traffic_block]],
|
||||
LocationName.strawberry_7: [[ItemName.dash_refill],
|
||||
[ItemName.traffic_block]],
|
||||
LocationName.strawberry_8: [[ItemName.traffic_block]],
|
||||
LocationName.strawberry_9: [[ItemName.dash_refill]],
|
||||
LocationName.strawberry_11: [[ItemName.dash_refill],
|
||||
[ItemName.traffic_block]],
|
||||
LocationName.strawberry_12: [[ItemName.dash_refill, ItemName.double_dash_refill],
|
||||
[ItemName.traffic_block, ItemName.double_dash_refill]],
|
||||
LocationName.strawberry_13: [[ItemName.dash_refill, ItemName.breakables],
|
||||
[ItemName.traffic_block, ItemName.breakables]],
|
||||
LocationName.strawberry_14: [[ItemName.dash_refill, ItemName.feather],
|
||||
[ItemName.traffic_block, ItemName.feather]],
|
||||
LocationName.strawberry_15: [[ItemName.dash_refill, ItemName.feather],
|
||||
[ItemName.traffic_block, ItemName.feather]],
|
||||
LocationName.strawberry_16: [[ItemName.dash_refill, ItemName.feather],
|
||||
[ItemName.traffic_block, ItemName.feather]],
|
||||
LocationName.strawberry_17: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block]],
|
||||
LocationName.strawberry_18: [[ItemName.dash_refill, ItemName.double_dash_refill],
|
||||
[ItemName.traffic_block, ItemName.feather, ItemName.double_dash_refill]],
|
||||
LocationName.strawberry_19: [[ItemName.dash_refill, ItemName.double_dash_refill, ItemName.spring],
|
||||
[ItemName.traffic_block, ItemName.double_dash_refill, ItemName.feather, ItemName.spring]],
|
||||
LocationName.strawberry_20: [[ItemName.dash_refill, ItemName.feather, ItemName.breakables],
|
||||
[ItemName.traffic_block, ItemName.feather, ItemName.breakables]],
|
||||
|
||||
LocationName.strawberry_21: [[ItemName.cassette, ItemName.traffic_block, ItemName.breakables]],
|
||||
LocationName.strawberry_22: [[ItemName.cassette, ItemName.dash_refill, ItemName.breakables]],
|
||||
LocationName.strawberry_23: [[ItemName.cassette, ItemName.dash_refill, ItemName.coin],
|
||||
[ItemName.cassette, ItemName.traffic_block, ItemName.coin]],
|
||||
LocationName.strawberry_24: [[ItemName.cassette, ItemName.dash_refill, ItemName.traffic_block]],
|
||||
LocationName.strawberry_25: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill],
|
||||
[ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.double_dash_refill]],
|
||||
LocationName.strawberry_26: [[ItemName.cassette, ItemName.dash_refill],
|
||||
[ItemName.cassette, ItemName.traffic_block]],
|
||||
LocationName.strawberry_27: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin],
|
||||
[ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.coin]],
|
||||
LocationName.strawberry_28: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin],
|
||||
[ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.coin]],
|
||||
LocationName.strawberry_29: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin]],
|
||||
LocationName.strawberry_30: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.spring, ItemName.breakables]],
|
||||
|
||||
LocationName.theo_1: [[ItemName.traffic_block, ItemName.breakables]],
|
||||
LocationName.theo_2: [[ItemName.traffic_block, ItemName.breakables]],
|
||||
LocationName.theo_3: [[ItemName.traffic_block, ItemName.breakables]],
|
||||
LocationName.badeline_1: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables]],
|
||||
LocationName.badeline_2: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables]],
|
||||
LocationName.badeline_3: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables]],
|
||||
|
||||
LocationName.sign_2: [[ItemName.breakables]],
|
||||
LocationName.sign_3: [[ItemName.dash_refill],
|
||||
[ItemName.traffic_block]],
|
||||
LocationName.sign_4: [[ItemName.dash_refill, ItemName.double_dash_refill],
|
||||
[ItemName.dash_refill, ItemName.feather],
|
||||
[ItemName.traffic_block, ItemName.feather]],
|
||||
LocationName.sign_5: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables]],
|
||||
|
||||
LocationName.car_2: [[ItemName.breakables]],
|
||||
}
|
||||
|
||||
location_hard_logic: Dict[str, List[List[str]]] = {
|
||||
LocationName.strawberry_13: [[ItemName.breakables]],
|
||||
LocationName.strawberry_17: [[ItemName.double_dash_refill, ItemName.traffic_block]],
|
||||
LocationName.strawberry_20: [[ItemName.breakables]],
|
||||
|
||||
LocationName.strawberry_21: [[ItemName.cassette, ItemName.traffic_block, ItemName.breakables]],
|
||||
LocationName.strawberry_22: [[ItemName.cassette]],
|
||||
LocationName.strawberry_23: [[ItemName.cassette, ItemName.coin]],
|
||||
LocationName.strawberry_24: [[ItemName.cassette]],
|
||||
LocationName.strawberry_25: [[ItemName.cassette, ItemName.double_dash_refill]],
|
||||
LocationName.strawberry_26: [[ItemName.cassette]],
|
||||
LocationName.strawberry_27: [[ItemName.cassette]],
|
||||
LocationName.strawberry_28: [[ItemName.cassette, ItemName.feather]],
|
||||
LocationName.strawberry_29: [[ItemName.cassette]],
|
||||
LocationName.strawberry_30: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables]],
|
||||
|
||||
LocationName.sign_2: [[ItemName.breakables]],
|
||||
|
||||
LocationName.car_2: [[ItemName.breakables]],
|
||||
}
|
||||
|
||||
location_standard_moves_logic: Dict[str, List[List[str]]] = {
|
||||
LocationName.strawberry_1: [[ItemName.ground_dash],
|
||||
[ItemName.air_dash],
|
||||
[ItemName.skid_jump],
|
||||
[ItemName.climb]],
|
||||
LocationName.strawberry_2: [[ItemName.ground_dash],
|
||||
[ItemName.air_dash],
|
||||
[ItemName.skid_jump],
|
||||
[ItemName.climb]],
|
||||
LocationName.strawberry_2: [[ItemName.air_dash],
|
||||
[ItemName.skid_jump]],
|
||||
LocationName.strawberry_3: [[ItemName.air_dash],
|
||||
[ItemName.skid_jump]],
|
||||
LocationName.strawberry_4: [[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
|
||||
LocationName.strawberry_5: [[ItemName.air_dash]],
|
||||
LocationName.strawberry_6: [[ItemName.dash_refill, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.ground_dash],
|
||||
[ItemName.traffic_block, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.skid_jump],
|
||||
[ItemName.traffic_block, ItemName.climb]],
|
||||
LocationName.strawberry_7: [[ItemName.dash_refill, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.ground_dash],
|
||||
[ItemName.traffic_block, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.skid_jump],
|
||||
[ItemName.traffic_block, ItemName.climb]],
|
||||
LocationName.strawberry_8: [[ItemName.traffic_block, ItemName.ground_dash],
|
||||
[ItemName.traffic_block, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.skid_jump],
|
||||
[ItemName.traffic_block, ItemName.climb]],
|
||||
LocationName.strawberry_9: [[ItemName.dash_refill, ItemName.air_dash]],
|
||||
LocationName.strawberry_10: [[ItemName.climb]],
|
||||
LocationName.strawberry_11: [[ItemName.dash_refill, ItemName.air_dash, ItemName.climb],
|
||||
[ItemName.traffic_block, ItemName.climb]],
|
||||
LocationName.strawberry_12: [[ItemName.dash_refill, ItemName.double_dash_refill, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.double_dash_refill, ItemName.air_dash]],
|
||||
LocationName.strawberry_13: [[ItemName.dash_refill, ItemName.breakables, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.breakables, ItemName.ground_dash],
|
||||
[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
|
||||
LocationName.strawberry_14: [[ItemName.dash_refill, ItemName.feather, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.feather, ItemName.air_dash]],
|
||||
LocationName.strawberry_15: [[ItemName.dash_refill, ItemName.feather, ItemName.air_dash, ItemName.climb],
|
||||
[ItemName.traffic_block, ItemName.feather, ItemName.climb]],
|
||||
LocationName.strawberry_16: [[ItemName.dash_refill, ItemName.feather, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.feather]],
|
||||
LocationName.strawberry_17: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.ground_dash],
|
||||
[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
|
||||
[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.skid_jump],
|
||||
[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.climb]],
|
||||
LocationName.strawberry_18: [[ItemName.dash_refill, ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb],
|
||||
[ItemName.traffic_block, ItemName.feather, ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb]],
|
||||
LocationName.strawberry_19: [[ItemName.dash_refill, ItemName.double_dash_refill, ItemName.spring, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.double_dash_refill, ItemName.feather, ItemName.spring, ItemName.air_dash]],
|
||||
LocationName.strawberry_20: [[ItemName.dash_refill, ItemName.feather, ItemName.breakables, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.feather, ItemName.breakables, ItemName.air_dash]],
|
||||
LocationName.strawberry_11: [[ItemName.air_dash, ItemName.climb]],
|
||||
LocationName.strawberry_13: [[ItemName.breakables, ItemName.air_dash],
|
||||
[ItemName.breakables, ItemName.ground_dash]],
|
||||
LocationName.strawberry_14: [[ItemName.feather, ItemName.air_dash]],
|
||||
LocationName.strawberry_15: [[ItemName.feather, ItemName.air_dash, ItemName.climb]],
|
||||
LocationName.strawberry_16: [[ItemName.feather]],
|
||||
LocationName.strawberry_17: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block]],
|
||||
LocationName.strawberry_18: [[ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb]],
|
||||
LocationName.strawberry_19: [[ItemName.double_dash_refill, ItemName.spring, ItemName.air_dash, ItemName.skid_jump]],
|
||||
LocationName.strawberry_20: [[ItemName.feather, ItemName.breakables, ItemName.air_dash]],
|
||||
|
||||
LocationName.strawberry_21: [[ItemName.cassette, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
|
||||
LocationName.strawberry_22: [[ItemName.cassette, ItemName.dash_refill, ItemName.breakables, ItemName.air_dash]],
|
||||
LocationName.strawberry_23: [[ItemName.cassette, ItemName.dash_refill, ItemName.coin, ItemName.air_dash, ItemName.climb],
|
||||
[ItemName.cassette, ItemName.traffic_block, ItemName.coin, ItemName.air_dash, ItemName.climb]],
|
||||
LocationName.strawberry_23: [[ItemName.cassette, ItemName.coin, ItemName.air_dash, ItemName.climb]],
|
||||
LocationName.strawberry_24: [[ItemName.cassette, ItemName.dash_refill, ItemName.traffic_block, ItemName.air_dash]],
|
||||
LocationName.strawberry_25: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb],
|
||||
[ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb]],
|
||||
LocationName.strawberry_26: [[ItemName.cassette, ItemName.dash_refill, ItemName.air_dash, ItemName.climb],
|
||||
[ItemName.cassette, ItemName.traffic_block, ItemName.air_dash, ItemName.climb]],
|
||||
LocationName.strawberry_27: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin, ItemName.air_dash],
|
||||
[ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.coin, ItemName.air_dash]],
|
||||
LocationName.strawberry_28: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin, ItemName.air_dash, ItemName.climb],
|
||||
[ItemName.cassette, ItemName.traffic_block, ItemName.feather, ItemName.coin, ItemName.air_dash, ItemName.climb]],
|
||||
LocationName.strawberry_29: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin, ItemName.air_dash, ItemName.skid_jump]],
|
||||
LocationName.strawberry_25: [[ItemName.cassette, ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb]],
|
||||
LocationName.strawberry_26: [[ItemName.cassette, ItemName.air_dash, ItemName.climb]],
|
||||
LocationName.strawberry_27: [[ItemName.cassette, ItemName.feather, ItemName.coin, ItemName.air_dash, ItemName.climb]],
|
||||
LocationName.strawberry_28: [[ItemName.cassette, ItemName.feather, ItemName.coin, ItemName.air_dash, ItemName.climb]],
|
||||
LocationName.strawberry_29: [[ItemName.cassette, ItemName.dash_refill, ItemName.coin, ItemName.air_dash, ItemName.skid_jump]],
|
||||
LocationName.strawberry_30: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.spring, ItemName.breakables, ItemName.air_dash, ItemName.climb]],
|
||||
|
||||
LocationName.granny_1: [[ItemName.ground_dash],
|
||||
[ItemName.air_dash],
|
||||
[ItemName.skid_jump],
|
||||
[ItemName.climb]],
|
||||
LocationName.granny_2: [[ItemName.ground_dash],
|
||||
[ItemName.air_dash],
|
||||
[ItemName.skid_jump],
|
||||
[ItemName.climb]],
|
||||
LocationName.granny_3: [[ItemName.ground_dash],
|
||||
[ItemName.air_dash],
|
||||
[ItemName.skid_jump],
|
||||
[ItemName.climb]],
|
||||
LocationName.theo_1: [[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
|
||||
LocationName.theo_2: [[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
|
||||
LocationName.theo_3: [[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
|
||||
LocationName.badeline_1: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb]],
|
||||
LocationName.badeline_2: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb]],
|
||||
LocationName.badeline_3: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb]],
|
||||
|
||||
LocationName.sign_1: [[ItemName.ground_dash],
|
||||
[ItemName.air_dash],
|
||||
[ItemName.skid_jump],
|
||||
[ItemName.climb]],
|
||||
LocationName.sign_2: [[ItemName.breakables, ItemName.ground_dash],
|
||||
[ItemName.breakables, ItemName.air_dash]],
|
||||
LocationName.sign_3: [[ItemName.dash_refill, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.ground_dash],
|
||||
[ItemName.traffic_block, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.skid_jump],
|
||||
[ItemName.traffic_block, ItemName.climb]],
|
||||
LocationName.sign_4: [[ItemName.dash_refill, ItemName.double_dash_refill, ItemName.air_dash],
|
||||
[ItemName.dash_refill, ItemName.feather, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.feather, ItemName.ground_dash],
|
||||
[ItemName.traffic_block, ItemName.feather, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.feather, ItemName.skid_jump],
|
||||
[ItemName.traffic_block, ItemName.feather, ItemName.climb]],
|
||||
LocationName.sign_5: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb]],
|
||||
|
||||
LocationName.car_2: [[ItemName.breakables, ItemName.ground_dash],
|
||||
[ItemName.breakables, ItemName.air_dash]],
|
||||
LocationName.car_2: [[ItemName.breakables, ItemName.ground_dash, ItemName.climb],
|
||||
[ItemName.breakables, ItemName.air_dash, ItemName.climb]],
|
||||
}
|
||||
|
||||
location_hard_moves_logic: Dict[str, List[List[str]]] = {
|
||||
LocationName.strawberry_3: [[ItemName.air_dash],
|
||||
[ItemName.skid_jump]],
|
||||
LocationName.strawberry_5: [[ItemName.ground_dash],
|
||||
[ItemName.air_dash]],
|
||||
LocationName.strawberry_8: [[ItemName.traffic_block],
|
||||
[ItemName.ground_dash, ItemName.air_dash]],
|
||||
LocationName.strawberry_10: [[ItemName.air_dash],
|
||||
[ItemName.climb]],
|
||||
LocationName.strawberry_11: [[ItemName.ground_dash],
|
||||
[ItemName.air_dash],
|
||||
[ItemName.skid_jump]],
|
||||
LocationName.strawberry_12: [[ItemName.feather],
|
||||
[ItemName.ground_dash],
|
||||
[ItemName.air_dash]],
|
||||
LocationName.strawberry_13: [[ItemName.breakables, ItemName.ground_dash],
|
||||
[ItemName.breakables, ItemName.air_dash]],
|
||||
LocationName.strawberry_14: [[ItemName.feather, ItemName.air_dash],
|
||||
[ItemName.air_dash, ItemName.climb]],
|
||||
[ItemName.air_dash, ItemName.climb],
|
||||
[ItemName.double_dash_refill, ItemName.air_dash]],
|
||||
LocationName.strawberry_15: [[ItemName.feather],
|
||||
[ItemName.ground_dash, ItemName.air_dash]],
|
||||
LocationName.strawberry_17: [[ItemName.double_dash_refill, ItemName.traffic_block]],
|
||||
@@ -287,42 +107,94 @@ location_hard_moves_logic: Dict[str, List[List[str]]] = {
|
||||
[ItemName.cassette, ItemName.feather, ItemName.climb]],
|
||||
LocationName.strawberry_29: [[ItemName.cassette, ItemName.dash_refill, ItemName.air_dash, ItemName.skid_jump],
|
||||
[ItemName.cassette, ItemName.ground_dash, ItemName.air_dash]],
|
||||
LocationName.strawberry_30: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables, ItemName.ground_dash, ItemName.air_dash, ItemName.climb, ItemName.skid_jump],
|
||||
[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables, ItemName.feather, ItemName.air_dash, ItemName.climb, ItemName.skid_jump],
|
||||
[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables, ItemName.spring, ItemName.ground_dash, ItemName.air_dash, ItemName.climb],
|
||||
[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables, ItemName.spring, ItemName.feather, ItemName.air_dash, ItemName.climb]],
|
||||
|
||||
LocationName.badeline_1: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb],
|
||||
[ItemName.traffic_block, ItemName.air_dash, ItemName.skid_jump],
|
||||
[ItemName.ground_dash, ItemName.air_dash, ItemName.skid_jump],
|
||||
[ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.ground_dash, ItemName.air_dash]],
|
||||
LocationName.badeline_2: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb],
|
||||
[ItemName.traffic_block, ItemName.air_dash, ItemName.skid_jump],
|
||||
[ItemName.ground_dash, ItemName.air_dash, ItemName.skid_jump],
|
||||
[ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.ground_dash, ItemName.air_dash]],
|
||||
LocationName.badeline_3: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb],
|
||||
[ItemName.traffic_block, ItemName.air_dash, ItemName.skid_jump],
|
||||
[ItemName.ground_dash, ItemName.air_dash, ItemName.skid_jump],
|
||||
[ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.ground_dash, ItemName.air_dash]],
|
||||
LocationName.strawberry_30: [[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb, ItemName.skid_jump],
|
||||
[ItemName.cassette, ItemName.dash_refill, ItemName.double_dash_refill, ItemName.traffic_block, ItemName.breakables, ItemName.spring, ItemName.air_dash, ItemName.climb]],
|
||||
|
||||
LocationName.sign_2: [[ItemName.breakables, ItemName.ground_dash],
|
||||
[ItemName.breakables, ItemName.air_dash]],
|
||||
LocationName.sign_5: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables, ItemName.air_dash, ItemName.climb],
|
||||
[ItemName.traffic_block, ItemName.air_dash, ItemName.skid_jump],
|
||||
[ItemName.ground_dash, ItemName.air_dash, ItemName.skid_jump],
|
||||
[ItemName.feather, ItemName.traffic_block, ItemName.air_dash],
|
||||
[ItemName.traffic_block, ItemName.ground_dash, ItemName.air_dash]],
|
||||
|
||||
LocationName.car_2: [[ItemName.breakables, ItemName.ground_dash],
|
||||
[ItemName.breakables, ItemName.air_dash]],
|
||||
}
|
||||
|
||||
|
||||
def location_rule(state: CollectionState, world: Celeste64World, loc: str) -> bool:
|
||||
region_standard_moves_logic: Dict[Tuple[str], List[List[str]]] = {
|
||||
(RegionName.forsaken_city, RegionName.granny_island): [[ItemName.checkpoint_2], [ItemName.checkpoint_3], [ItemName.checkpoint_4]],
|
||||
(RegionName.forsaken_city, RegionName.highway_island): [[ItemName.checkpoint_5], [ItemName.checkpoint_6]],
|
||||
(RegionName.forsaken_city, RegionName.ne_feathers_island): [[ItemName.checkpoint_7]],
|
||||
(RegionName.forsaken_city, RegionName.se_house_island): [[ItemName.checkpoint_8]],
|
||||
(RegionName.forsaken_city, RegionName.badeline_tower_upper): [[ItemName.checkpoint_9]],
|
||||
(RegionName.forsaken_city, RegionName.badeline_island): [[ItemName.checkpoint_10]],
|
||||
|
||||
(RegionName.intro_islands, RegionName.granny_island): [[ItemName.ground_dash],
|
||||
[ItemName.air_dash],
|
||||
[ItemName.skid_jump],
|
||||
[ItemName.climb]],
|
||||
|
||||
(RegionName.granny_island, RegionName.highway_island): [[ItemName.air_dash, ItemName.dash_refill]],
|
||||
(RegionName.granny_island, RegionName.nw_girders_island): [[ItemName.traffic_block]],
|
||||
(RegionName.granny_island, RegionName.badeline_tower_lower): [[ItemName.air_dash, ItemName.climb, ItemName.dash_refill]],
|
||||
(RegionName.granny_island, RegionName.se_house_island): [[ItemName.air_dash, ItemName.climb, ItemName.double_dash_refill]],
|
||||
|
||||
(RegionName.highway_island, RegionName.granny_island): [[ItemName.traffic_block], [ItemName.air_dash, ItemName.dash_refill]],
|
||||
(RegionName.highway_island, RegionName.ne_feathers_island): [[ItemName.feather]],
|
||||
(RegionName.highway_island, RegionName.nw_girders_island): [[ItemName.cannot_access]],
|
||||
|
||||
(RegionName.nw_girders_island, RegionName.highway_island): [[ItemName.traffic_block]],
|
||||
|
||||
(RegionName.ne_feathers_island, RegionName.highway_island): [[ItemName.feather]],
|
||||
(RegionName.ne_feathers_island, RegionName.badeline_tower_lower): [[ItemName.feather]],
|
||||
(RegionName.ne_feathers_island, RegionName.badeline_tower_upper): [[ItemName.climb, ItemName.air_dash, ItemName.feather]],
|
||||
|
||||
(RegionName.se_house_island, RegionName.granny_island): [[ItemName.air_dash, ItemName.traffic_block, ItemName.double_dash_refill]],
|
||||
(RegionName.se_house_island, RegionName.badeline_tower_lower): [[ItemName.air_dash, ItemName.double_dash_refill]],
|
||||
|
||||
(RegionName.badeline_tower_lower, RegionName.se_house_island): [[ItemName.cannot_access]],
|
||||
(RegionName.badeline_tower_lower, RegionName.ne_feathers_island): [[ItemName.air_dash, ItemName.breakables, ItemName.feather]],
|
||||
(RegionName.badeline_tower_lower, RegionName.granny_island): [[ItemName.cannot_access]],
|
||||
(RegionName.badeline_tower_lower, RegionName.badeline_tower_upper): [[ItemName.cannot_access]],
|
||||
|
||||
(RegionName.badeline_tower_upper, RegionName.badeline_island): [[ItemName.air_dash, ItemName.climb, ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block, ItemName.breakables]],
|
||||
(RegionName.badeline_tower_upper, RegionName.se_house_island): [[ItemName.air_dash], [ItemName.ground_dash]],
|
||||
(RegionName.badeline_tower_upper, RegionName.ne_feathers_island): [[ItemName.air_dash], [ItemName.ground_dash]],
|
||||
(RegionName.badeline_tower_upper, RegionName.granny_island): [[ItemName.dash_refill]],
|
||||
|
||||
(RegionName.badeline_island, RegionName.badeline_tower_upper): [[ItemName.air_dash], [ItemName.ground_dash]],
|
||||
}
|
||||
|
||||
region_hard_moves_logic: Dict[Tuple[str], List[List[str]]] = {
|
||||
(RegionName.forsaken_city, RegionName.granny_island): [[ItemName.checkpoint_2], [ItemName.checkpoint_3], [ItemName.checkpoint_4]],
|
||||
(RegionName.forsaken_city, RegionName.highway_island): [[ItemName.checkpoint_5], [ItemName.checkpoint_6]],
|
||||
(RegionName.forsaken_city, RegionName.ne_feathers_island): [[ItemName.checkpoint_7]],
|
||||
(RegionName.forsaken_city, RegionName.se_house_island): [[ItemName.checkpoint_8]],
|
||||
(RegionName.forsaken_city, RegionName.badeline_tower_upper): [[ItemName.checkpoint_9]],
|
||||
(RegionName.forsaken_city, RegionName.badeline_island): [[ItemName.checkpoint_10]],
|
||||
|
||||
(RegionName.granny_island, RegionName.nw_girders_island): [[ItemName.traffic_block]],
|
||||
(RegionName.granny_island, RegionName.badeline_tower_lower): [[ItemName.air_dash], [ItemName.ground_dash]],
|
||||
(RegionName.granny_island, RegionName.se_house_island): [[ItemName.air_dash, ItemName.double_dash_refill], [ItemName.ground_dash]],
|
||||
|
||||
(RegionName.highway_island, RegionName.nw_girders_island): [[ItemName.air_dash, ItemName.ground_dash]],
|
||||
|
||||
(RegionName.nw_girders_island, RegionName.highway_island): [[ItemName.traffic_block], [ItemName.air_dash, ItemName.ground_dash]],
|
||||
|
||||
(RegionName.ne_feathers_island, RegionName.highway_island): [[ItemName.feather], [ItemName.air_dash], [ItemName.ground_dash], [ItemName.skid_jump]],
|
||||
(RegionName.ne_feathers_island, RegionName.badeline_tower_lower): [[ItemName.feather], [ItemName.air_dash], [ItemName.ground_dash]],
|
||||
(RegionName.ne_feathers_island, RegionName.badeline_tower_upper): [[ItemName.feather]],
|
||||
|
||||
(RegionName.se_house_island, RegionName.granny_island): [[ItemName.traffic_block]],
|
||||
(RegionName.se_house_island, RegionName.badeline_tower_lower): [[ItemName.air_dash], [ItemName.ground_dash]],
|
||||
|
||||
(RegionName.badeline_tower_upper, RegionName.badeline_island): [[ItemName.air_dash, ItemName.climb, ItemName.feather, ItemName.traffic_block],
|
||||
[ItemName.air_dash, ItemName.climb, ItemName.feather, ItemName.skid_jump],
|
||||
[ItemName.air_dash, ItemName.climb, ItemName.ground_dash, ItemName.traffic_block],
|
||||
[ItemName.air_dash, ItemName.climb, ItemName.ground_dash, ItemName.skid_jump]],
|
||||
|
||||
(RegionName.badeline_island, RegionName.badeline_tower_upper): [[ItemName.air_dash], [ItemName.ground_dash]],
|
||||
}
|
||||
|
||||
|
||||
def location_rule(state: CollectionState, world: Celeste64World, loc: str) -> bool:
|
||||
if loc not in world.active_logic_mapping:
|
||||
return True
|
||||
|
||||
@@ -332,12 +204,28 @@ def location_rule(state: CollectionState, world: Celeste64World, loc: str) -> bo
|
||||
|
||||
return False
|
||||
|
||||
def goal_rule(state: CollectionState, world: Celeste64World) -> bool:
|
||||
if not state.has(ItemName.strawberry, world.player, world.strawberries_required):
|
||||
return False
|
||||
def region_connection_rule(state: CollectionState, world: Celeste64World, region_connection: Tuple[str]) -> bool:
|
||||
if region_connection not in world.active_region_logic_mapping:
|
||||
return True
|
||||
|
||||
for possible_access in world.goal_logic_mapping:
|
||||
for possible_access in world.active_region_logic_mapping[region_connection]:
|
||||
if state.has_all(possible_access, world.player):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def goal_rule(state: CollectionState, world: Celeste64World) -> bool:
|
||||
if not state.has(ItemName.strawberry, world.player, world.strawberries_required):
|
||||
return False
|
||||
|
||||
goal_region: Region = world.multiworld.get_region(RegionName.badeline_island, world.player)
|
||||
return state.can_reach(goal_region)
|
||||
|
||||
def connect_region(world: Celeste64World, region: Region, dest_regions: List[str]):
|
||||
rules: Dict[str, Callable[[CollectionState], bool]] = {}
|
||||
|
||||
for dest_region in dest_regions:
|
||||
region_connection: Tuple[str] = (region.name, dest_region)
|
||||
rules[dest_region] = lambda state, region_connection=region_connection: region_connection_rule(state, world, region_connection)
|
||||
|
||||
region.add_exits(dest_regions, rules)
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
from copy import deepcopy
|
||||
from typing import Dict, List
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
from BaseClasses import ItemClassification, Location, Region, Tutorial
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from .Items import Celeste64Item, unlockable_item_data_table, move_item_data_table, item_data_table, item_table
|
||||
from .Items import Celeste64Item, unlockable_item_data_table, move_item_data_table, item_data_table,\
|
||||
checkpoint_item_data_table, item_table
|
||||
from .Locations import Celeste64Location, strawberry_location_data_table, friend_location_data_table,\
|
||||
sign_location_data_table, car_location_data_table, location_table
|
||||
sign_location_data_table, car_location_data_table, checkpoint_location_data_table,\
|
||||
location_table
|
||||
from .Names import ItemName, LocationName
|
||||
from .Options import Celeste64Options, celeste_64_option_groups
|
||||
from .Options import Celeste64Options, celeste_64_option_groups, resolve_options
|
||||
|
||||
|
||||
class Celeste64WebWorld(WebWorld):
|
||||
@@ -42,8 +44,15 @@ class Celeste64World(World):
|
||||
# Instance Data
|
||||
strawberries_required: int
|
||||
active_logic_mapping: Dict[str, List[List[str]]]
|
||||
goal_logic_mapping: Dict[str, List[List[str]]]
|
||||
active_region_logic_mapping: Dict[Tuple[str], List[List[str]]]
|
||||
|
||||
madeline_one_dash_hair_color: int
|
||||
madeline_two_dash_hair_color: int
|
||||
madeline_no_dash_hair_color: int
|
||||
madeline_feather_hair_color: int
|
||||
|
||||
def generate_early(self) -> None:
|
||||
resolve_options(self)
|
||||
|
||||
def create_item(self, name: str) -> Celeste64Item:
|
||||
# Only make required amount of strawberries be Progression
|
||||
@@ -76,25 +85,49 @@ class Celeste64World(World):
|
||||
for name in unlockable_item_data_table.keys()
|
||||
if name not in self.options.start_inventory]
|
||||
|
||||
if self.options.move_shuffle:
|
||||
move_items_for_itempool: List[str] = deepcopy(list(move_item_data_table.keys()))
|
||||
chosen_start_item: str = ""
|
||||
|
||||
if self.options.move_shuffle:
|
||||
if self.options.logic_difficulty == "standard":
|
||||
# If the start_inventory already includes a move, don't worry about giving it one
|
||||
if not [move for move in move_items_for_itempool if move in self.options.start_inventory]:
|
||||
chosen_start_move = self.random.choice(move_items_for_itempool)
|
||||
move_items_for_itempool.remove(chosen_start_move)
|
||||
possible_unwalls: List[str] = [name for name in move_item_data_table.keys()
|
||||
if name != ItemName.skid_jump]
|
||||
|
||||
if self.options.checkpointsanity:
|
||||
possible_unwalls.extend([name for name in checkpoint_item_data_table.keys()
|
||||
if name != ItemName.checkpoint_1 and name != ItemName.checkpoint_10])
|
||||
|
||||
# If the start_inventory already includes a move or checkpoint, don't worry about giving it one
|
||||
if not [item for item in possible_unwalls if item in self.multiworld.precollected_items[self.player]]:
|
||||
chosen_start_item = self.random.choice(possible_unwalls)
|
||||
|
||||
if self.options.carsanity:
|
||||
intro_car_loc: Location = self.multiworld.get_location(LocationName.car_1, self.player)
|
||||
intro_car_loc.place_locked_item(self.create_item(chosen_start_move))
|
||||
intro_car_loc.place_locked_item(self.create_item(chosen_start_item))
|
||||
location_count -= 1
|
||||
else:
|
||||
self.multiworld.push_precollected(self.create_item(chosen_start_move))
|
||||
self.multiworld.push_precollected(self.create_item(chosen_start_item))
|
||||
|
||||
item_pool += [self.create_item(name)
|
||||
for name in move_items_for_itempool
|
||||
if name not in self.options.start_inventory]
|
||||
for name in move_item_data_table.keys()
|
||||
if name not in self.multiworld.precollected_items[self.player]
|
||||
and name != chosen_start_item]
|
||||
else:
|
||||
for start_move in move_item_data_table.keys():
|
||||
self.multiworld.push_precollected(self.create_item(start_move))
|
||||
|
||||
if self.options.checkpointsanity:
|
||||
location_count += 9
|
||||
goal_checkpoint_loc: Location = self.multiworld.get_location(LocationName.checkpoint_10, self.player)
|
||||
goal_checkpoint_loc.place_locked_item(self.create_item(ItemName.checkpoint_10))
|
||||
item_pool += [self.create_item(name)
|
||||
for name in checkpoint_item_data_table.keys()
|
||||
if name not in self.multiworld.precollected_items[self.player]
|
||||
and name != ItemName.checkpoint_10
|
||||
and name != chosen_start_item]
|
||||
else:
|
||||
for item_name in checkpoint_item_data_table.keys():
|
||||
checkpoint_loc: Location = self.multiworld.get_location(item_name, self.player)
|
||||
checkpoint_loc.place_locked_item(self.create_item(item_name))
|
||||
|
||||
real_total_strawberries: int = min(self.options.total_strawberries.value, location_count - len(item_pool))
|
||||
self.strawberries_required = int(real_total_strawberries * (self.options.strawberries_required_percentage / 100))
|
||||
@@ -140,18 +173,23 @@ class Celeste64World(World):
|
||||
if location_data.region == region_name
|
||||
}, Celeste64Location)
|
||||
|
||||
region.add_exits(region_data_table[region_name].connecting_regions)
|
||||
region.add_locations({
|
||||
location_name: location_data.address for location_name, location_data in checkpoint_location_data_table.items()
|
||||
if location_data.region == region_name
|
||||
}, Celeste64Location)
|
||||
|
||||
from .Rules import connect_region
|
||||
connect_region(self, region, region_data_table[region_name].connecting_regions)
|
||||
|
||||
# Have to do this here because of other games using State in a way that's bad
|
||||
from .Rules import set_rules
|
||||
set_rules(self)
|
||||
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return ItemName.raspberry
|
||||
|
||||
|
||||
def set_rules(self) -> None:
|
||||
from .Rules import set_rules
|
||||
set_rules(self)
|
||||
|
||||
|
||||
def fill_slot_data(self):
|
||||
return {
|
||||
"death_link": self.options.death_link.value,
|
||||
@@ -161,6 +199,11 @@ class Celeste64World(World):
|
||||
"friendsanity": self.options.friendsanity.value,
|
||||
"signsanity": self.options.signsanity.value,
|
||||
"carsanity": self.options.carsanity.value,
|
||||
"checkpointsanity": self.options.checkpointsanity.value,
|
||||
"madeline_one_dash_hair_color": self.madeline_one_dash_hair_color,
|
||||
"madeline_two_dash_hair_color": self.madeline_two_dash_hair_color,
|
||||
"madeline_no_dash_hair_color": self.madeline_no_dash_hair_color,
|
||||
"madeline_feather_hair_color": self.madeline_feather_hair_color,
|
||||
"badeline_chaser_source": self.options.badeline_chaser_source.value,
|
||||
"badeline_chaser_frequency": self.options.badeline_chaser_frequency.value,
|
||||
"badeline_chaser_speed": self.options.badeline_chaser_speed.value,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from dataclasses import dataclass
|
||||
import os
|
||||
import io
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, cast
|
||||
import zipfile
|
||||
from BaseClasses import Location
|
||||
from worlds.Files import APContainer
|
||||
from worlds.Files import APContainer, AutoPatchRegister
|
||||
|
||||
from .Enum import CivVICheckType
|
||||
from .Locations import CivVILocation, CivVILocationData
|
||||
@@ -25,24 +26,32 @@ class CivTreeItem:
|
||||
ui_tree_row: int
|
||||
|
||||
|
||||
class CivVIContainer(APContainer):
|
||||
class CivVIContainer(APContainer, metaclass=AutoPatchRegister):
|
||||
"""
|
||||
Responsible for generating the dynamic mod files for the Civ VI multiworld
|
||||
"""
|
||||
game: Optional[str] = "Civilization VI"
|
||||
patch_file_ending = ".apcivvi"
|
||||
|
||||
def __init__(self, patch_data: Dict[str, str], base_path: str, output_directory: str,
|
||||
def __init__(self, patch_data: Dict[str, str] | io.BytesIO, base_path: str = "", output_directory: str = "",
|
||||
player: Optional[int] = None, player_name: str = "", server: str = ""):
|
||||
self.patch_data = patch_data
|
||||
self.file_path = base_path
|
||||
container_path = os.path.join(output_directory, base_path + ".apcivvi")
|
||||
super().__init__(container_path, player, player_name, server)
|
||||
if isinstance(patch_data, io.BytesIO):
|
||||
super().__init__(patch_data, player, player_name, server)
|
||||
else:
|
||||
self.patch_data = patch_data
|
||||
self.file_path = base_path
|
||||
container_path = os.path.join(output_directory, base_path + ".apcivvi")
|
||||
super().__init__(container_path, player, player_name, server)
|
||||
|
||||
def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
|
||||
for filename, yml in self.patch_data.items():
|
||||
opened_zipfile.writestr(filename, yml)
|
||||
super().write_contents(opened_zipfile)
|
||||
|
||||
def sanitize_value(value: str) -> str:
|
||||
"""Removes values that can cause issues in XML"""
|
||||
return value.replace('"', "'").replace('&', 'and')
|
||||
|
||||
|
||||
def get_cost(world: 'CivVIWorld', location: CivVILocationData) -> int:
|
||||
"""
|
||||
@@ -58,7 +67,7 @@ def get_formatted_player_name(world: 'CivVIWorld', player: int) -> str:
|
||||
Returns the name of the player in the world
|
||||
"""
|
||||
if player != world.player:
|
||||
return f"{world.multiworld.player_name[player]}{apo}s"
|
||||
return sanitize_value(f"{world.multiworld.player_name[player]}{apo}s")
|
||||
return "Your"
|
||||
|
||||
|
||||
@@ -101,7 +110,7 @@ def generate_new_items(world: 'CivVIWorld') -> str:
|
||||
<Row TechnologyType="TECH_BLOCKER" Name="TECH_BLOCKER" EraType="ERA_ANCIENT" UITreeRow="0" Cost="99999" AdvisorType="ADVISOR_GENERIC" Description="Archipelago Tech created to prevent players from researching their own tech. If you can read this, then congrats you have reached the end of your tree before beating the game!"/>
|
||||
{"".join([f'{tab}<Row TechnologyType="{location.name}" '
|
||||
f'Name="{get_formatted_player_name(world, location.item.player)} '
|
||||
f'{location.item.name}" '
|
||||
f'{sanitize_value(location.item.name)}" '
|
||||
f'EraType="{world.location_table[location.name].era_type}" '
|
||||
f'UITreeRow="{world.location_table[location.name].uiTreeRow}" '
|
||||
f'Cost="{get_cost(world, world.location_table[location.name])}" '
|
||||
@@ -117,7 +126,7 @@ def generate_new_items(world: 'CivVIWorld') -> str:
|
||||
<Row CivicType="CIVIC_BLOCKER" Name="CIVIC_BLOCKER" EraType="ERA_ANCIENT" UITreeRow="0" Cost="99999" AdvisorType="ADVISOR_GENERIC" Description="Archipelago Civic created to prevent players from researching their own civics. If you can read this, then congrats you have reached the end of your tree before beating the game!"/>
|
||||
{"".join([f'{tab}<Row CivicType="{location.name}" '
|
||||
f'Name="{get_formatted_player_name(world, location.item.player)} '
|
||||
f'{location.item.name}" '
|
||||
f'{sanitize_value(location.item.name)}" '
|
||||
f'EraType="{world.location_table[location.name].era_type}" '
|
||||
f'UITreeRow="{world.location_table[location.name].uiTreeRow}" '
|
||||
f'Cost="{get_cost(world, world.location_table[location.name])}" '
|
||||
|
||||
0
worlds/civ_6/data/__init__.py
Normal file
0
worlds/civ_6/data/__init__.py
Normal file
@@ -51,7 +51,7 @@ Boosts have logic associated with them in order to verify you can always reach t
|
||||
- I need to kill a unit with a slinger/archer/musketman or some other obsolete unit I can't build anymore, how can I do this?
|
||||
- Don't forget you can go into the Tech Tree and click on a Vanilla tech you've received in order to toggle it on/off. This is necessary in order to pursue some of the boosts if you receive techs in certain orders.
|
||||
- Something happened, and I'm not able to unlock the boost due to game rules!
|
||||
- A few scenarios you may worry about: "Found a religion", "Make an alliance with another player", "Develop an alliance to level 2", "Build a wonder from X Era", to name a few. Any boost that is "miss-able" has been flagged as an "Excluded" location and will not ever receive a progression item. For a list of how each boost is flagged, take a look [here](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/civ_6/data/boosts.json).
|
||||
- A few scenarios you may worry about: "Found a religion", "Make an alliance with another player", "Develop an alliance to level 2", "Build a wonder from X Era", to name a few. Any boost that is "miss-able" has been flagged as an "Excluded" location and will not ever receive a progression item. For a list of how each boost is flagged, take a look [here](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/civ_6/data/boosts.py).
|
||||
- I'm worried that my `PROGRESSIVE_ERA` item is going to be stuck in a boost I won't have time to complete before my maximum unlocked era ends!
|
||||
- The unpredictable timing of boosts and unlocking them can occasionally lead to scenarios where you'll have to first encounter a locked era defeat and then load a previous save. To help reduce the frequency of this, local `PROGRESSIVE_ERA` items will never be located at a boost check.
|
||||
- There's too many boosts, how will I know which one's I should focus on?!
|
||||
|
||||
@@ -14,22 +14,17 @@ The following are required in order to play Civ VI in Archipelago:
|
||||
|
||||
## Enabling the tuner
|
||||
|
||||
Depending on how you installed Civ 6 you will have to navigate to one of the following:
|
||||
|
||||
- `YOUR_USER/Documents/My Games/Sid Meier's Civilization VI/AppOptions.txt`
|
||||
- `YOUR_USER/AppData/Local/Firaxis Games/Sid Meier's Civilization VI/AppOptions.txt`
|
||||
|
||||
Once you have located your `AppOptions.txt`, do a search for `Enable FireTuner`. Set `EnableTuner` to `1` instead of `0`. **NOTE**: While this is active, achievements will be disabled.
|
||||
In the main menu, navigate to the "Game Options" page. On the "Game" menu, make sure that "Tuner (disables achievements)" is enabled.
|
||||
|
||||
## Mod Installation
|
||||
|
||||
1. Download and unzip the latest release of the mod from [GitHub](https://github.com/hesto2/civilization_archipelago_mod/releases/latest).
|
||||
|
||||
2. Copy the folder containing the mod files to your Civ VI mods folder. On Windows, this is usually located at `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods`.
|
||||
2. Copy the folder containing the mod files to your Civ VI mods folder. On Windows, this is usually located at `C:\Users\YOUR_USER\Documents\My Games\Sid Meier's Civilization VI\Mods`. If you use OneDrive, check if the folder is instead located in your OneDrive file structure.
|
||||
|
||||
3. After the Archipelago host generates a game, you should be given a `.apcivvi` file. Associate the file with the Archipelago Launcher and double click it.
|
||||
|
||||
4. Copy the contents of the new folder it generates (it will have the same name as the `.apcivvi` file) into your Civilization VI Archipelago Mod folder.
|
||||
4. Copy the contents of the new folder it generates (it will have the same name as the `.apcivvi` file) into your Civilization VI Archipelago Mod folder. If double clicking the `.apcivvi` file doesn't generate a folder, you can just rename it to a file ending with `.zip` and extract its contents to a new folder. To do this, right click the `.apcivvi` file and click "Rename", make sure it ends in `.zip`, then right click it again and select "Extract All".
|
||||
|
||||
5. Your finished mod folder should look something like this:
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ class CliqueWebWorld(WebWorld):
|
||||
)
|
||||
|
||||
tutorials = [setup_en, setup_de]
|
||||
game_info_languages = ["en", "de"]
|
||||
|
||||
|
||||
class CliqueWorld(World):
|
||||
|
||||
0
worlds/cv64/data/__init__.py
Normal file
0
worlds/cv64/data/__init__.py
Normal file
@@ -644,6 +644,9 @@ class CV64PatchExtensions(APPatchExtension):
|
||||
# Replace the PowerUp in the Forest Special1 Bridge 3HB rock with an L jewel if 3HBs aren't randomized
|
||||
if not options["multi_hit_breakables"]:
|
||||
rom_data.write_byte(0x10C7A1, 0x03)
|
||||
# Replace the PowerUp in one of the lizard lockers if the lizard locker items aren't randomized.
|
||||
if not options["lizard_locker_items"]:
|
||||
rom_data.write_byte(0xBFCA07, 0x03)
|
||||
# Change the appearance of the Pot-Pourri to that of a larger PowerUp regardless of the above setting, so other
|
||||
# game PermaUps are distinguishable.
|
||||
rom_data.write_int32s(0xEE558, [0x06005F08, 0x3FB00000, 0xFFFFFF00])
|
||||
@@ -714,7 +717,11 @@ class CV64PatchExtensions(APPatchExtension):
|
||||
rom_data.write_int32(0x10CF38, 0x8000FF4D) # CT final room door slab
|
||||
rom_data.write_int32(0x10CF44, 0x1000FF4D) # CT Renon slab
|
||||
rom_data.write_int32(0x10C908, 0x0008FF4D) # Villa foyer chandelier
|
||||
rom_data.write_byte(0x10CF37, 0x04) # pointer for CT final room door slab item data
|
||||
|
||||
# Change the pointer to the Clock Tower final room 3HB door slab drops to not share its values with those of the
|
||||
# 3HB slab near Renon at the top of the room.
|
||||
if options["multi_hit_breakables"]:
|
||||
rom_data.write_byte(0x10CF37, 0x04)
|
||||
|
||||
# Once-per-frame gameplay checks
|
||||
rom_data.write_int32(0x6C848, 0x080FF40D) # J 0x803FD034
|
||||
@@ -1000,6 +1007,7 @@ def write_patch(world: "CV64World", patch: CV64ProcedurePatch, offset_data: Dict
|
||||
"multi_hit_breakables": world.options.multi_hit_breakables.value,
|
||||
"drop_previous_sub_weapon": world.options.drop_previous_sub_weapon.value,
|
||||
"countdown": world.options.countdown.value,
|
||||
"lizard_locker_items": world.options.lizard_locker_items.value,
|
||||
"shopsanity": world.options.shopsanity.value,
|
||||
"panther_dash": world.options.panther_dash.value,
|
||||
"big_toss": world.options.big_toss.value,
|
||||
|
||||
@@ -19,8 +19,8 @@ from worlds.AutoWorld import WebWorld, World
|
||||
|
||||
from .aesthetics import shuffle_sub_weapons, get_location_data, get_countdown_flags, populate_enemy_drops, \
|
||||
get_start_inventory_data
|
||||
from .rom import RomData, patch_rom, get_base_rom_path, CVCotMProcedurePatch, CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, \
|
||||
CVCOTM_VC_US_HASH
|
||||
from .rom import RomData, patch_rom, get_base_rom_path, CVCotMProcedurePatch, CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH
|
||||
# CVCOTM_VC_US_HASH
|
||||
from .client import CastlevaniaCotMClient
|
||||
|
||||
|
||||
@@ -29,7 +29,8 @@ class CVCotMSettings(settings.Group):
|
||||
"""File name of the Castlevania CotM US rom"""
|
||||
copy_to = "Castlevania - Circle of the Moon (USA).gba"
|
||||
description = "Castlevania CotM (US) ROM File"
|
||||
md5s = [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, CVCOTM_VC_US_HASH]
|
||||
# md5s = [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, CVCOTM_VC_US_HASH]
|
||||
md5s = [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH]
|
||||
|
||||
rom_file: RomFile = RomFile(RomFile.copy_to)
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import TYPE_CHECKING, Set
|
||||
from typing import TYPE_CHECKING, Set, Optional
|
||||
from .locations import BASE_ID, get_location_names_to_ids
|
||||
from .items import cvcotm_item_info, MAJORS_CLASSIFICATIONS
|
||||
from .locations import cvcotm_location_info
|
||||
@@ -91,6 +91,7 @@ class CastlevaniaCotMClient(BizHawkClient):
|
||||
patch_suffix = ".apcvcotm"
|
||||
sent_initial_packets: bool
|
||||
self_induced_death: bool
|
||||
time_of_sent_death: Optional[float]
|
||||
local_checked_locations: Set[int]
|
||||
client_set_events = {flag_name: False for flag, flag_name in EVENT_FLAG_MAP.items()}
|
||||
killed_dracula_2: bool
|
||||
@@ -139,6 +140,7 @@ class CastlevaniaCotMClient(BizHawkClient):
|
||||
self.sent_initial_packets = False
|
||||
self.local_checked_locations = set()
|
||||
self.self_induced_death = False
|
||||
self.time_of_sent_death = None
|
||||
self.client_set_events = {flag_name: False for flag, flag_name in EVENT_FLAG_MAP.items()}
|
||||
self.killed_dracula_2 = False
|
||||
self.won_battle_arena = False
|
||||
@@ -156,14 +158,16 @@ class CastlevaniaCotMClient(BizHawkClient):
|
||||
return
|
||||
if ctx.slot is None:
|
||||
return
|
||||
if "DeathLink" in args["tags"] and args["data"]["source"] != ctx.slot_info[ctx.slot].name:
|
||||
if "DeathLink" in args["tags"] and args["data"]["time"] != self.time_of_sent_death:
|
||||
if "cause" in args["data"]:
|
||||
cause = args["data"]["cause"]
|
||||
# If the other game sent a death with a blank string for the cause, use the default death message.
|
||||
if cause == "":
|
||||
cause = f"{args['data']['source']} killed you without a word!"
|
||||
if len(cause) > ITEM_NAME_LIMIT + PLAYER_NAME_LIMIT:
|
||||
cause = cause[:ITEM_NAME_LIMIT + PLAYER_NAME_LIMIT]
|
||||
else:
|
||||
# If the other game sent a death with no cause at all, use the default death message.
|
||||
cause = f"{args['data']['source']} killed you without a word!"
|
||||
|
||||
# Highlight the player that killed us in the game's orange text.
|
||||
@@ -259,8 +263,13 @@ class CastlevaniaCotMClient(BizHawkClient):
|
||||
else:
|
||||
area_of_death = DEATHLINK_AREA_NAMES[area]
|
||||
|
||||
# Send the death.
|
||||
await ctx.send_death(f"{ctx.player_names[ctx.slot]} perished in {area_of_death}. Dracula has won!")
|
||||
|
||||
# Record the time in which the death was sent so when we receive the packet we can tell it wasn't our
|
||||
# own death. ctx.on_deathlink overwrites it later, so it MUST be grabbed now.
|
||||
self.time_of_sent_death = ctx.last_death_link
|
||||
|
||||
# Update the Dracula II and Battle Arena events already being done on past separate sessions for if the
|
||||
# player is running the Battle Arena and Dracula goal.
|
||||
if f"castlevania_cotm_events_{ctx.team}_{ctx.slot}" in ctx.stored_data:
|
||||
|
||||
0
worlds/cvcotm/data/__init__.py
Normal file
0
worlds/cvcotm/data/__init__.py
Normal file
@@ -153,11 +153,10 @@ Advance Collection ROM; most notably the fact that the audio does not function w
|
||||
which is currently a requirement to connect to a multiworld. This happens because all audio code was stripped
|
||||
from the ROM, and all sound is instead played by the collection through external means.
|
||||
|
||||
For this reason, it is most recommended to obtain the ROM by dumping it from an original cartridge of the game that you legally own.
|
||||
Though, the Advance Collection *can* still technically be an option if you cannot do that and don't mind the lack of sound.
|
||||
The Wii U Virtual Console version does not work due to changes in the code in that version.
|
||||
|
||||
The Wii U Virtual Console version is currently untested. If you happen to have purchased it before the Wii U eShop shut down, you can try
|
||||
dumping and playing with it. However, at the moment, we cannot guarantee that it will work well due to it being untested.
|
||||
Due to the reasons mentioned above, it is most recommended to obtain the ROM by dumping it from an original cartridge of the
|
||||
game that you legally own. However, the Advance Collection *is* an option if you cannot do that and don't mind the lack of sound.
|
||||
|
||||
Regardless of which released ROM you intend to try playing with, the US version of the game is required.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases/latest).
|
||||
- A Castlevania: Circle of the Moon ROM of the US version specifically. The Archipelago community cannot provide this.
|
||||
The Castlevania Advance Collection ROM can technically be used, but it has no audio. The Wii U Virtual Console ROM is untested.
|
||||
The Castlevania Advance Collection ROM can be used, but it has no audio. The Wii U Virtual Console ROM does not work.
|
||||
- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) 2.7 or later.
|
||||
|
||||
### Configuring BizHawk
|
||||
|
||||
@@ -22,11 +22,9 @@ if TYPE_CHECKING:
|
||||
|
||||
CVCOTM_CT_US_HASH = "50a1089600603a94e15ecf287f8d5a1f" # Original GBA cartridge ROM
|
||||
CVCOTM_AC_US_HASH = "87a1bd6577b6702f97a60fc55772ad74" # Castlevania Advance Collection ROM
|
||||
CVCOTM_VC_US_HASH = "2cc38305f62b337281663bad8c901cf9" # Wii U Virtual Console ROM
|
||||
# CVCOTM_VC_US_HASH = "2cc38305f62b337281663bad8c901cf9" # Wii U Virtual Console ROM
|
||||
|
||||
# NOTE: The Wii U VC version is untested as of when this comment was written. I am only including its hash in case it
|
||||
# does work. If someone who has it can confirm it does indeed work, this comment should be removed. If it doesn't, the
|
||||
# hash should be removed in addition. See the Game Page for more information about supported versions.
|
||||
# The Wii U VC version is not currently supported. See the Game Page for more info.
|
||||
|
||||
ARCHIPELAGO_IDENTIFIER_START = 0x7FFF00
|
||||
ARCHIPELAGO_IDENTIFIER = "ARCHIPELAG03"
|
||||
@@ -518,7 +516,8 @@ class CVCotMPatchExtensions(APPatchExtension):
|
||||
|
||||
|
||||
class CVCotMProcedurePatch(APProcedurePatch, APTokenMixin):
|
||||
hash = [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, CVCOTM_VC_US_HASH]
|
||||
# hash = [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, CVCOTM_VC_US_HASH]
|
||||
hash = [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH]
|
||||
patch_file_ending: str = ".apcvcotm"
|
||||
result_file_ending: str = ".gba"
|
||||
|
||||
@@ -585,7 +584,8 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
|
||||
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(base_rom_bytes)
|
||||
if basemd5.hexdigest() not in [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, CVCOTM_VC_US_HASH]:
|
||||
# if basemd5.hexdigest() not in [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, CVCOTM_VC_US_HASH]:
|
||||
if basemd5.hexdigest() not in [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH]:
|
||||
raise Exception("Supplied Base ROM does not match known MD5s for Castlevania: Circle of the Moon USA."
|
||||
"Get the correct game and version, then dump it.")
|
||||
setattr(get_base_rom_bytes, "base_rom_bytes", base_rom_bytes)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user