mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-26 11:13:30 -07:00
Compare commits
118 Commits
empty-deat
...
NewSoupVi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
117295fd29 | ||
|
|
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 |
1
.github/pyright-config.json
vendored
1
.github/pyright-config.json
vendored
@@ -2,6 +2,7 @@
|
|||||||
"include": [
|
"include": [
|
||||||
"../BizHawkClient.py",
|
"../BizHawkClient.py",
|
||||||
"../Patch.py",
|
"../Patch.py",
|
||||||
|
"../test/param.py",
|
||||||
"../test/general/test_groups.py",
|
"../test/general/test_groups.py",
|
||||||
"../test/general/test_helpers.py",
|
"../test/general/test_helpers.py",
|
||||||
"../test/general/test_memory.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
|
continue-on-error: false
|
||||||
if: env.diff != '' && matrix.task == 'flake8'
|
if: env.diff != '' && matrix.task == 'flake8'
|
||||||
run: |
|
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"
|
- name: "flake8: Lint modified files"
|
||||||
continue-on-error: true
|
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
|
if-no-files-found: error
|
||||||
retention-days: 7 # keep for 7 days, should be enough
|
retention-days: 7 # keep for 7 days, should be enough
|
||||||
|
|
||||||
build-ubuntu2004:
|
build-ubuntu2204:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
# - copy code below to release.yml -
|
# - copy code below to release.yml -
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|||||||
4
.github/workflows/ctest.yml
vendored
4
.github/workflows/ctest.yml
vendored
@@ -36,9 +36,9 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: ilammy/msvc-dev-cmd@v1
|
- uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756
|
||||||
if: startsWith(matrix.os,'windows')
|
if: startsWith(matrix.os,'windows')
|
||||||
- uses: Bacondish2023/setup-googletest@v1
|
- uses: Bacondish2023/setup-googletest@49065d1f7a6d21f6134864dd65980fe5dbe06c73
|
||||||
with:
|
with:
|
||||||
build-type: 'Release'
|
build-type: 'Release'
|
||||||
- name: Build tests
|
- 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-windows: # this is done by hand because of signing
|
||||||
# build-release-macos: # LF volunteer
|
# build-release-macos: # LF volunteer
|
||||||
|
|
||||||
build-release-ubuntu2004:
|
build-release-ubuntu2204:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Set env
|
- name: Set env
|
||||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -10,6 +10,7 @@
|
|||||||
*.apmc
|
*.apmc
|
||||||
*.apz5
|
*.apz5
|
||||||
*.aptloz
|
*.aptloz
|
||||||
|
*.aptww
|
||||||
*.apemerald
|
*.apemerald
|
||||||
*.pyc
|
*.pyc
|
||||||
*.pyd
|
*.pyd
|
||||||
|
|||||||
@@ -511,7 +511,7 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
import colorama
|
import colorama
|
||||||
|
|
||||||
colorama.init()
|
colorama.just_fix_windows_console()
|
||||||
|
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
colorama.deinit()
|
colorama.deinit()
|
||||||
|
|||||||
@@ -616,7 +616,7 @@ class MultiWorld():
|
|||||||
locations: Set[Location] = set()
|
locations: Set[Location] = set()
|
||||||
events: Set[Location] = set()
|
events: Set[Location] = set()
|
||||||
for location in self.get_filled_locations():
|
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)
|
locations.add(location)
|
||||||
else:
|
else:
|
||||||
events.add(location)
|
events.add(location)
|
||||||
@@ -1106,6 +1106,9 @@ class Region:
|
|||||||
def __len__(self) -> int:
|
def __len__(self) -> int:
|
||||||
return self._list.__len__()
|
return self._list.__len__()
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self._list)
|
||||||
|
|
||||||
# This seems to not be needed, but that's a bit suspicious.
|
# This seems to not be needed, but that's a bit suspicious.
|
||||||
# def __del__(self):
|
# def __del__(self):
|
||||||
# self.clear()
|
# self.clear()
|
||||||
@@ -1310,9 +1313,6 @@ class Location:
|
|||||||
multiworld = self.parent_region.multiworld if self.parent_region and self.parent_region.multiworld else None
|
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})'
|
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):
|
def __lt__(self, other: Location):
|
||||||
return (self.player, self.name) < (other.player, other.name)
|
return (self.player, self.name) < (other.player, other.name)
|
||||||
|
|
||||||
@@ -1416,6 +1416,10 @@ class Item:
|
|||||||
def flags(self) -> int:
|
def flags(self) -> int:
|
||||||
return self.classification.as_flag()
|
return self.classification.as_flag()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_event(self) -> bool:
|
||||||
|
return self.code is None
|
||||||
|
|
||||||
def __eq__(self, other: object) -> bool:
|
def __eq__(self, other: object) -> bool:
|
||||||
if not isinstance(other, Item):
|
if not isinstance(other, Item):
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
|
|||||||
@@ -413,7 +413,8 @@ class CommonContext:
|
|||||||
await self.server.socket.close()
|
await self.server.socket.close()
|
||||||
if self.server_task is not None:
|
if self.server_task is not None:
|
||||||
await self.server_task
|
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:
|
async def send_msgs(self, msgs: typing.List[typing.Any]) -> None:
|
||||||
""" `msgs` JSON serializable """
|
""" `msgs` JSON serializable """
|
||||||
@@ -624,9 +625,6 @@ class CommonContext:
|
|||||||
|
|
||||||
def consume_network_data_package(self, data_package: dict):
|
def consume_network_data_package(self, data_package: dict):
|
||||||
self.update_data_package(data_package)
|
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'])}")
|
logger.info(f"Got new ID/Name DataPackage for {', '.join(data_package['games'])}")
|
||||||
for game, game_data in data_package["games"].items():
|
for game, game_data in data_package["games"].items():
|
||||||
Utils.store_data_package_for_checksum(game, game_data)
|
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)
|
args = handle_url_arg(args, parser=parser)
|
||||||
|
|
||||||
# use colorama to display colored text highlighting on windows
|
# use colorama to display colored text highlighting on windows
|
||||||
colorama.init()
|
colorama.just_fix_windows_console()
|
||||||
|
|
||||||
asyncio.run(main(args))
|
asyncio.run(main(args))
|
||||||
colorama.deinit()
|
colorama.deinit()
|
||||||
|
|||||||
@@ -261,7 +261,7 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
parser = get_base_parser()
|
parser = get_base_parser()
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
colorama.init()
|
colorama.just_fix_windows_console()
|
||||||
|
|
||||||
asyncio.run(main(args))
|
asyncio.run(main(args))
|
||||||
colorama.deinit()
|
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())
|
items_to_place.append(reachable_items[next_player].pop())
|
||||||
|
|
||||||
for item in items_to_place:
|
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:
|
if pool_item is item:
|
||||||
item_pool.pop(p)
|
del item_pool[-p]
|
||||||
break
|
break
|
||||||
|
|
||||||
maximum_exploration_state = sweep_from_pool(
|
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
|
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):
|
location.locked and location.item.player not in minimal_players):
|
||||||
pool.append(location.item)
|
pool.append(location.item)
|
||||||
state.remove(location.item)
|
|
||||||
location.item = None
|
location.item = None
|
||||||
if location in state.advancements:
|
if location in state.advancements:
|
||||||
state.advancements.remove(location)
|
state.advancements.remove(location)
|
||||||
|
state.remove(location.item)
|
||||||
locations.append(location)
|
locations.append(location)
|
||||||
if pool and locations:
|
if pool and locations:
|
||||||
locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY)
|
locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY)
|
||||||
@@ -500,13 +502,15 @@ def distribute_items_restrictive(multiworld: MultiWorld,
|
|||||||
|
|
||||||
if prioritylocations:
|
if prioritylocations:
|
||||||
# "priority fill"
|
# "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,
|
single_player_placement=single_player, swap=False, on_place=mark_for_locking,
|
||||||
name="Priority", one_item_per_player=True, allow_partial=True)
|
name="Priority", one_item_per_player=True, allow_partial=True)
|
||||||
|
|
||||||
if prioritylocations:
|
if prioritylocations:
|
||||||
# retry with one_item_per_player off because some priority fills can fail to fill with that optimization
|
# 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,
|
single_player_placement=single_player, swap=False, on_place=mark_for_locking,
|
||||||
name="Priority Retry", one_item_per_player=False)
|
name="Priority Retry", one_item_per_player=False)
|
||||||
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
|
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
|
||||||
@@ -514,14 +518,15 @@ def distribute_items_restrictive(multiworld: MultiWorld,
|
|||||||
|
|
||||||
if progitempool:
|
if progitempool:
|
||||||
# "advancement/progression fill"
|
# "advancement/progression fill"
|
||||||
|
maximum_exploration_state = sweep_from_pool(multiworld.state)
|
||||||
if panic_method == "swap":
|
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)
|
name="Progression", single_player_placement=single_player)
|
||||||
elif panic_method == "raise":
|
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)
|
name="Progression", single_player_placement=single_player)
|
||||||
elif panic_method == "start_inventory":
|
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)
|
allow_partial=True, name="Progression", single_player_placement=single_player)
|
||||||
if progitempool:
|
if progitempool:
|
||||||
for item in 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",
|
parser.add_argument("--skip_output", action="store_true",
|
||||||
help="Skips generation assertion and output stages and skips multidata and spoiler output. "
|
help="Skips generation assertion and output stages and skips multidata and spoiler output. "
|
||||||
"Intended for debugging and testing purposes.")
|
"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()
|
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):
|
if not os.path.isabs(args.weights_file_path):
|
||||||
args.weights_file_path = os.path.join(args.player_files_path, 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):
|
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.meta_file_path = os.path.join(args.player_files_path, args.meta_file_path)
|
||||||
args.plando: PlandoOptions = PlandoOptions.from_option_string(args.plando)
|
args.plando: PlandoOptions = PlandoOptions.from_option_string(args.plando)
|
||||||
|
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
|
||||||
@@ -108,6 +118,8 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
|
|||||||
raise Exception("Cannot mix --sameoptions with --meta")
|
raise Exception("Cannot mix --sameoptions with --meta")
|
||||||
else:
|
else:
|
||||||
meta_weights = None
|
meta_weights = None
|
||||||
|
|
||||||
|
|
||||||
player_id = 1
|
player_id = 1
|
||||||
player_files = {}
|
player_files = {}
|
||||||
for file in os.scandir(args.player_files_path):
|
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.outputpath = args.outputpath
|
||||||
erargs.skip_prog_balancing = args.skip_prog_balancing
|
erargs.skip_prog_balancing = args.skip_prog_balancing
|
||||||
erargs.skip_output = args.skip_output
|
erargs.skip_output = args.skip_output
|
||||||
|
erargs.spoiler_only = args.spoiler_only
|
||||||
erargs.name = {}
|
erargs.name = {}
|
||||||
erargs.csv_output = args.csv_output
|
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.")
|
raise RuntimeError(f"All options specified in \"{option}\" are weighted as zero.")
|
||||||
|
|
||||||
|
|
||||||
class SafeDict(dict):
|
class SafeFormatter(string.Formatter):
|
||||||
def __missing__(self, key):
|
def get_value(self, key, args, kwargs):
|
||||||
return '{' + key + '}'
|
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):
|
def handle_name(name: str, player: int, name_counter: Counter):
|
||||||
name_counter[name.lower()] += 1
|
name_counter[name.lower()] += 1
|
||||||
number = name_counter[name.lower()]
|
number = name_counter[name.lower()]
|
||||||
new_name = "%".join([x.replace("%number%", "{number}").replace("%player%", "{player}") for x in name.split("%%")])
|
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 ''),
|
new_name = SafeFormatter().vformat(new_name, (), {"number": number,
|
||||||
player=player,
|
"NUMBER": (number if number > 1 else ''),
|
||||||
PLAYER=(player if player > 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.
|
# 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.
|
# Could cause issues for some clients that cannot handle the additional whitespace.
|
||||||
new_name = new_name.strip()[:16].strip()
|
new_name = new_name.strip()[:16].strip()
|
||||||
|
|
||||||
if new_name == "Archipelago":
|
if new_name == "Archipelago":
|
||||||
raise Exception(f"You cannot name yourself \"{new_name}\"")
|
raise Exception(f"You cannot name yourself \"{new_name}\"")
|
||||||
return 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 APBP as argument, launch corresponding client.
|
||||||
* if run with executable as argument, run it passing argv[2:] as arguments
|
* 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
|
Scroll down to components= to add components to the launcher as well as setup.py
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import itertools
|
|
||||||
import logging
|
import logging
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import shlex
|
import shlex
|
||||||
@@ -20,10 +18,11 @@ import urllib.parse
|
|||||||
import webbrowser
|
import webbrowser
|
||||||
from os.path import isfile
|
from os.path import isfile
|
||||||
from shutil import which
|
from shutil import which
|
||||||
from typing import Callable, Optional, Sequence, Tuple, Union
|
from typing import Callable, Optional, Sequence, Tuple, Union, Any
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import ModuleUpdate
|
import ModuleUpdate
|
||||||
|
|
||||||
ModuleUpdate.update()
|
ModuleUpdate.update()
|
||||||
|
|
||||||
import settings
|
import settings
|
||||||
@@ -105,7 +104,8 @@ components.extend([
|
|||||||
Component("Generate Template Options", func=generate_yamls),
|
Component("Generate Template Options", func=generate_yamls),
|
||||||
Component("Archipelago Website", func=lambda: webbrowser.open("https://archipelago.gg/")),
|
Component("Archipelago Website", func=lambda: webbrowser.open("https://archipelago.gg/")),
|
||||||
Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")),
|
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),
|
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)
|
url = urllib.parse.urlparse(path)
|
||||||
queries = urllib.parse.parse_qs(url.query)
|
queries = urllib.parse.parse_qs(url.query)
|
||||||
launch_args = (path, *launch_args)
|
launch_args = (path, *launch_args)
|
||||||
client_component = None
|
client_component = []
|
||||||
text_client_component = None
|
text_client_component = None
|
||||||
if "game" in queries:
|
if "game" in queries:
|
||||||
game = queries["game"][0]
|
game = queries["game"][0]
|
||||||
@@ -122,49 +122,40 @@ def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None:
|
|||||||
game = "Archipelago"
|
game = "Archipelago"
|
||||||
for component in components:
|
for component in components:
|
||||||
if component.supports_uri and component.game_name == game:
|
if component.supports_uri and component.game_name == game:
|
||||||
client_component = component
|
client_component.append(component)
|
||||||
elif component.display_name == "Text Client":
|
elif component.display_name == "Text Client":
|
||||||
text_client_component = component
|
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)
|
run_component(text_client_component, *launch_args)
|
||||||
return
|
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):
|
).open()
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
def identify(path: Union[None, str]) -> Tuple[Union[None, str], Union[None, Component]]:
|
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)
|
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
|
refresh_components: Optional[Callable[[], None]] = None
|
||||||
|
|
||||||
|
|
||||||
def run_gui():
|
def run_gui(path: str, args: Any) -> None:
|
||||||
from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget, ApAsyncImage
|
from kvui import (ThemedApp, MDFloatLayout, MDGridLayout, ScrollBox)
|
||||||
|
from kivy.properties import ObjectProperty
|
||||||
from kivy.core.window import Window
|
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"
|
base_title: str = "Archipelago Launcher"
|
||||||
container: ContainerLayout
|
top_screen: MDFloatLayout = ObjectProperty(None)
|
||||||
grid: GridLayout
|
navigation: MDGridLayout = ObjectProperty(None)
|
||||||
_tool_layout: Optional[ScrollBox] = None
|
grid: MDGridLayout = ObjectProperty(None)
|
||||||
_client_layout: Optional[ScrollBox] = 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.title = self.base_title + " " + Utils.__version__
|
||||||
self.ctx = ctx
|
self.ctx = ctx
|
||||||
self.icon = r"data/icon.png"
|
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__()
|
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:
|
def open_menu(caller):
|
||||||
component (Component): The component associated with the button.
|
caller.menu.open()
|
||||||
|
|
||||||
Returns:
|
menu_items = [
|
||||||
None. The button is added to the parent grid layout.
|
{
|
||||||
|
"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)
|
||||||
|
|
||||||
"""
|
return button_card
|
||||||
button = Button(text=component.display_name, size_hint_y=None, height=40)
|
|
||||||
button.component = component
|
def _refresh_components(self, type_filter: Sequence[str | Type] | None = None) -> None:
|
||||||
button.bind(on_release=self.component_action)
|
if not type_filter:
|
||||||
if component.icon != "icon":
|
type_filter = [Type.CLIENT, Type.ADJUSTER, Type.TOOL, Type.MISC]
|
||||||
image = ApAsyncImage(source=icon_paths[component.icon],
|
favorites = "favorites" in type_filter
|
||||||
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
|
|
||||||
|
|
||||||
# clear before repopulating
|
# clear before repopulating
|
||||||
assert self._tool_layout and self._client_layout, "must call `build` first"
|
assert self.button_layout, "must call `build` first"
|
||||||
tool_children = reversed(self._tool_layout.layout.children)
|
tool_children = reversed(self.button_layout.layout.children)
|
||||||
for child in tool_children:
|
for child in tool_children:
|
||||||
self._tool_layout.layout.remove_widget(child)
|
self.button_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)
|
|
||||||
|
|
||||||
_tools = {c.display_name: c for c in components if c.type == Type.TOOL}
|
cards = [card for card in self.cards if card.component.type in type_filter
|
||||||
_clients = {c.display_name: c for c in components if c.type == Type.CLIENT}
|
or favorites and card.component.display_name in self.favorites]
|
||||||
_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}
|
|
||||||
|
|
||||||
for (tool, client) in itertools.zip_longest(itertools.chain(
|
self.current_filter = type_filter
|
||||||
_tools.items(), _miscs.items(), _adjusters.items()
|
|
||||||
), _clients.items()):
|
for card in cards:
|
||||||
# column 1
|
self.button_layout.layout.add_widget(card)
|
||||||
if tool:
|
|
||||||
self._tool_layout.layout.add_widget(build_button(tool[1]))
|
top = self.button_layout.children[0].y + self.button_layout.children[0].height \
|
||||||
# column 2
|
- self.button_layout.height
|
||||||
if client:
|
scroll_percent = self.button_layout.convert_distance_to_scroll(0, top)
|
||||||
self._client_layout.layout.add_widget(build_button(client[1]))
|
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):
|
def build(self):
|
||||||
self.container = ContainerLayout()
|
self.top_screen = Builder.load_file(Utils.local_path("data/launcher.kv"))
|
||||||
self.grid = GridLayout(cols=2)
|
self.grid = self.top_screen.ids.grid
|
||||||
self.container.add_widget(self.grid)
|
self.navigation = self.top_screen.ids.navigation
|
||||||
self.grid.add_widget(Label(text="General", size_hint_y=None, height=40))
|
self.button_layout = self.top_screen.ids.button_layout
|
||||||
self.grid.add_widget(Label(text="Clients", size_hint_y=None, height=40))
|
self.set_colors()
|
||||||
self._tool_layout = ScrollBox()
|
self.top_screen.md_bg_color = self.theme_cls.backgroundColor
|
||||||
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()
|
|
||||||
|
|
||||||
global refresh_components
|
global refresh_components
|
||||||
refresh_components = self._refresh_components
|
refresh_components = self._refresh_components
|
||||||
|
|
||||||
Window.bind(on_drop_file=self._on_drop_file)
|
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
|
@staticmethod
|
||||||
def component_action(button):
|
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:
|
if button.component.func:
|
||||||
button.component.func()
|
button.component.func()
|
||||||
else:
|
else:
|
||||||
@@ -333,7 +390,13 @@ def run_gui():
|
|||||||
self.root_window.close()
|
self.root_window.close()
|
||||||
super()._stop(*largs)
|
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
|
# avoiding Launcher reference leak
|
||||||
# and don't try to do something with widgets after window closed
|
# 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)
|
path = args.get("Patch|Game|Component|url", None)
|
||||||
if path is not None:
|
if path is not None:
|
||||||
if path.startswith("archipelago://"):
|
if not path.startswith("archipelago://"):
|
||||||
handle_uri(path, args.get("args", ()))
|
file, component = identify(path)
|
||||||
return
|
if file:
|
||||||
file, component = identify(path)
|
args['file'] = file
|
||||||
if file:
|
if component:
|
||||||
args['file'] = file
|
args['component'] = component
|
||||||
if component:
|
if not component:
|
||||||
args['component'] = component
|
logging.warning(f"Could not identify Component responsible for {path}")
|
||||||
if not component:
|
|
||||||
logging.warning(f"Could not identify Component responsible for {path}")
|
|
||||||
|
|
||||||
if args["update_settings"]:
|
if args["update_settings"]:
|
||||||
update_settings()
|
update_settings()
|
||||||
@@ -378,7 +439,7 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None):
|
|||||||
elif "component" in args:
|
elif "component" in args:
|
||||||
run_component(args["component"], *args["args"])
|
run_component(args["component"], *args["args"])
|
||||||
elif not args["update_settings"]:
|
elif not args["update_settings"]:
|
||||||
run_gui()
|
run_gui(path, args.get("args", ()))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
@@ -400,6 +461,7 @@ if __name__ == '__main__':
|
|||||||
main(parser.parse_args())
|
main(parser.parse_args())
|
||||||
|
|
||||||
from worlds.LauncherComponents import processes
|
from worlds.LauncherComponents import processes
|
||||||
|
|
||||||
for process in processes:
|
for process in processes:
|
||||||
# we await all child processes to close before we tear down the process host
|
# 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
|
# 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,
|
from CommonClient import (CommonContext, get_base_parser, gui_enabled, logger,
|
||||||
server_loop)
|
server_loop)
|
||||||
from NetUtils import ClientStatus
|
from NetUtils import ClientStatus
|
||||||
|
from worlds.ladx import LinksAwakeningWorld
|
||||||
from worlds.ladx.Common import BASE_ID as LABaseID
|
from worlds.ladx.Common import BASE_ID as LABaseID
|
||||||
from worlds.ladx.GpsTracker import GpsTracker
|
from worlds.ladx.GpsTracker import GpsTracker
|
||||||
from worlds.ladx.TrackerConsts import storage_key
|
from worlds.ladx.TrackerConsts import storage_key
|
||||||
@@ -139,7 +140,7 @@ class RAGameboy():
|
|||||||
def set_checks_range(self, checks_start, checks_size):
|
def set_checks_range(self, checks_start, checks_size):
|
||||||
self.checks_start = checks_start
|
self.checks_start = checks_start
|
||||||
self.checks_size = checks_size
|
self.checks_size = checks_size
|
||||||
|
|
||||||
def set_location_range(self, location_start, location_size, critical_addresses):
|
def set_location_range(self, location_start, location_size, critical_addresses):
|
||||||
self.location_start = location_start
|
self.location_start = location_start
|
||||||
self.location_size = location_size
|
self.location_size = location_size
|
||||||
@@ -237,7 +238,7 @@ class RAGameboy():
|
|||||||
self.cache[start:start + len(hram_block)] = hram_block
|
self.cache[start:start + len(hram_block)] = hram_block
|
||||||
|
|
||||||
self.last_cache_read = time.time()
|
self.last_cache_read = time.time()
|
||||||
|
|
||||||
async def read_memory_block(self, address: int, size: int):
|
async def read_memory_block(self, address: int, size: int):
|
||||||
block = bytearray()
|
block = bytearray()
|
||||||
remaining_size = size
|
remaining_size = size
|
||||||
@@ -245,7 +246,7 @@ class RAGameboy():
|
|||||||
chunk = await self.async_read_memory(address + len(block), remaining_size)
|
chunk = await self.async_read_memory(address + len(block), remaining_size)
|
||||||
remaining_size -= len(chunk)
|
remaining_size -= len(chunk)
|
||||||
block += chunk
|
block += chunk
|
||||||
|
|
||||||
return block
|
return block
|
||||||
|
|
||||||
async def read_memory_cache(self, addresses):
|
async def read_memory_cache(self, addresses):
|
||||||
@@ -506,7 +507,7 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
la_task = None
|
la_task = None
|
||||||
client = None
|
client = None
|
||||||
# TODO: does this need to re-read on reset?
|
# TODO: does this need to re-read on reset?
|
||||||
found_checks = []
|
found_checks = set()
|
||||||
last_resend = time.time()
|
last_resend = time.time()
|
||||||
|
|
||||||
magpie_enabled = False
|
magpie_enabled = False
|
||||||
@@ -514,8 +515,8 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
magpie_task = None
|
magpie_task = None
|
||||||
won = False
|
won = False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def slot_storage_key(self):
|
def slot_storage_key(self):
|
||||||
return f"{self.slot_info[self.slot].name}_{storage_key}"
|
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:
|
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:
|
def run_gui(self) -> None:
|
||||||
import webbrowser
|
import webbrowser
|
||||||
import kvui
|
from kvui import GameManager, ImageButton
|
||||||
from kvui import Button, GameManager
|
|
||||||
from kivy.uix.image import Image
|
|
||||||
|
|
||||||
class LADXManager(GameManager):
|
class LADXManager(GameManager):
|
||||||
logging_pairs = [
|
logging_pairs = [
|
||||||
@@ -544,25 +543,15 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
b = super().build()
|
b = super().build()
|
||||||
|
|
||||||
if self.ctx.magpie_enabled:
|
if self.ctx.magpie_enabled:
|
||||||
button = Button(text="", size=(30, 30), size_hint_x=None,
|
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'))
|
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)
|
|
||||||
|
|
||||||
self.connect_layout.add_widget(button)
|
self.connect_layout.add_widget(button)
|
||||||
|
|
||||||
return b
|
return b
|
||||||
|
|
||||||
self.ui = LADXManager(self)
|
self.ui = LADXManager(self)
|
||||||
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
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]):
|
async def send_new_entrances(self, entrances: typing.Dict[str, str]):
|
||||||
# Store the entrances we find on the server for future sessions
|
# Store the entrances we find on the server for future sessions
|
||||||
message = [{
|
message = [{
|
||||||
@@ -601,20 +590,20 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
logger.info("victory!")
|
logger.info("victory!")
|
||||||
await self.send_msgs(message)
|
await self.send_msgs(message)
|
||||||
self.won = True
|
self.won = True
|
||||||
|
|
||||||
async def request_found_entrances(self):
|
async def request_found_entrances(self):
|
||||||
await self.send_msgs([{"cmd": "Get", "keys": [self.slot_storage_key]}])
|
await self.send_msgs([{"cmd": "Get", "keys": [self.slot_storage_key]}])
|
||||||
|
|
||||||
# Ask for updates so that players can co-op entrances in a seed
|
# Ask for updates so that players can co-op entrances in a seed
|
||||||
await self.send_msgs([{"cmd": "SetNotify", "keys": [self.slot_storage_key]}])
|
await self.send_msgs([{"cmd": "SetNotify", "keys": [self.slot_storage_key]}])
|
||||||
|
|
||||||
async def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None:
|
async def on_deathlink(self, data: typing.Dict[str, typing.Any]) -> None:
|
||||||
if self.ENABLE_DEATHLINK:
|
if self.ENABLE_DEATHLINK:
|
||||||
self.client.pending_deathlink = True
|
self.client.pending_deathlink = True
|
||||||
|
|
||||||
def new_checks(self, item_ids, ladxr_ids):
|
def new_checks(self, item_ids, ladxr_ids):
|
||||||
self.found_checks += item_ids
|
self.found_checks.update(item_ids)
|
||||||
create_task_log_exception(self.send_checks())
|
create_task_log_exception(self.check_locations(self.found_checks))
|
||||||
if self.magpie_enabled:
|
if self.magpie_enabled:
|
||||||
create_task_log_exception(self.magpie.send_new_checks(ladxr_ids))
|
create_task_log_exception(self.magpie.send_new_checks(ladxr_ids))
|
||||||
|
|
||||||
@@ -642,12 +631,18 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
if cmd == "Connected":
|
if cmd == "Connected":
|
||||||
self.game = self.slot_info[self.slot].game
|
self.game = self.slot_info[self.slot].game
|
||||||
self.slot_data = args.get("slot_data", {})
|
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
|
# TODO - use watcher_event
|
||||||
if cmd == "ReceivedItems":
|
if cmd == "ReceivedItems":
|
||||||
for index, item in enumerate(args["items"], start=args["index"]):
|
for index, item in enumerate(args["items"], start=args["index"]):
|
||||||
self.client.recvd_checks[index] = item
|
self.client.recvd_checks[index] = item
|
||||||
|
|
||||||
if cmd == "Retrieved" and self.magpie_enabled and self.slot_storage_key in args["keys"]:
|
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])
|
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:
|
if self.last_resend + 5.0 < now:
|
||||||
self.last_resend = now
|
self.last_resend = now
|
||||||
await self.send_checks()
|
await self.check_locations(self.found_checks)
|
||||||
if self.magpie_enabled:
|
if self.magpie_enabled:
|
||||||
try:
|
try:
|
||||||
self.magpie.set_checks(self.client.tracker.all_checks)
|
self.magpie.set_checks(self.client.tracker.all_checks)
|
||||||
await self.magpie.set_item_tracker(self.client.item_tracker)
|
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:
|
if self.client.gps_tracker.needs_found_entrances:
|
||||||
await self.request_found_entrances()
|
await self.request_found_entrances()
|
||||||
self.client.gps_tracker.needs_found_entrances = False
|
self.client.gps_tracker.needs_found_entrances = False
|
||||||
@@ -745,8 +742,8 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
await asyncio.sleep(1.0)
|
await asyncio.sleep(1.0)
|
||||||
|
|
||||||
def run_game(romfile: str) -> None:
|
def run_game(romfile: str) -> None:
|
||||||
auto_start = typing.cast(typing.Union[bool, str],
|
auto_start = LinksAwakeningWorld.settings.rom_start
|
||||||
Utils.get_options()["ladx_options"].get("rom_start", True))
|
|
||||||
if auto_start is True:
|
if auto_start is True:
|
||||||
import webbrowser
|
import webbrowser
|
||||||
webbrowser.open(romfile)
|
webbrowser.open(romfile)
|
||||||
@@ -803,6 +800,6 @@ async def main():
|
|||||||
await ctx.shutdown()
|
await ctx.shutdown()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
colorama.init()
|
colorama.just_fix_windows_console()
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
colorama.deinit()
|
colorama.deinit()
|
||||||
|
|||||||
@@ -370,7 +370,7 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
import colorama
|
import colorama
|
||||||
|
|
||||||
colorama.init()
|
colorama.just_fix_windows_console()
|
||||||
|
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
colorama.deinit()
|
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
|
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.
|
# 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_stage(multiworld, "assert_generate")
|
||||||
|
|
||||||
AutoWorld.call_all(multiworld, "generate_early")
|
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...')
|
logger.info(f'Beginning output...')
|
||||||
outfilebase = 'AP_' + multiworld.seed_name
|
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()
|
output = tempfile.TemporaryDirectory()
|
||||||
with output as temp_dir:
|
with output as temp_dir:
|
||||||
output_players = [player for player in multiworld.player_ids if AutoWorld.World.generate_output.__code__
|
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
|
from BaseClasses import ItemClassification
|
||||||
|
|
||||||
min_client_version = Version(0, 1, 6)
|
min_client_version = Version(0, 1, 6)
|
||||||
colorama.init()
|
colorama.just_fix_windows_console()
|
||||||
|
|
||||||
|
|
||||||
def remove_from_list(container, value):
|
def remove_from_list(container, value):
|
||||||
@@ -66,9 +66,13 @@ def pop_from_container(container, value):
|
|||||||
return container
|
return container
|
||||||
|
|
||||||
|
|
||||||
def update_dict(dictionary, entries):
|
def update_container_unique(container, entries):
|
||||||
dictionary.update(entries)
|
if isinstance(container, list):
|
||||||
return dictionary
|
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():
|
def queue_gc():
|
||||||
@@ -109,7 +113,7 @@ modify_functions = {
|
|||||||
# lists/dicts:
|
# lists/dicts:
|
||||||
"remove": remove_from_list,
|
"remove": remove_from_list,
|
||||||
"pop": pop_from_container,
|
"pop": pop_from_container,
|
||||||
"update": update_dict,
|
"update": update_container_unique,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1978,11 +1982,13 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
|||||||
new_hint = new_hint.re_prioritize(ctx, status)
|
new_hint = new_hint.re_prioritize(ctx, status)
|
||||||
if hint == new_hint:
|
if hint == new_hint:
|
||||||
return
|
return
|
||||||
ctx.replace_hint(client.team, hint.finding_player, hint, new_hint)
|
|
||||||
ctx.replace_hint(client.team, hint.receiving_player, hint, new_hint)
|
concerning_slots = ctx.slot_set(hint.receiving_player) | {hint.finding_player}
|
||||||
|
for slot in concerning_slots:
|
||||||
|
ctx.replace_hint(client.team, slot, hint, new_hint)
|
||||||
ctx.save()
|
ctx.save()
|
||||||
ctx.on_changed_hints(client.team, hint.finding_player)
|
for slot in concerning_slots:
|
||||||
ctx.on_changed_hints(client.team, hint.receiving_player)
|
ctx.on_changed_hints(client.team, slot)
|
||||||
|
|
||||||
elif cmd == 'StatusUpdate':
|
elif cmd == 'StatusUpdate':
|
||||||
update_client_status(ctx, client, args["status"])
|
update_client_status(ctx, client, args["status"])
|
||||||
@@ -2037,7 +2043,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
|||||||
value = func(value, operation["value"])
|
value = func(value, operation["value"])
|
||||||
ctx.stored_data[args["key"]] = args["value"] = value
|
ctx.stored_data[args["key"]] = args["value"] = value
|
||||||
targets = set(ctx.stored_data_notification_clients[args["key"]])
|
targets = set(ctx.stored_data_notification_clients[args["key"]])
|
||||||
if args.get("want_reply", True):
|
if args.get("want_reply", False):
|
||||||
targets.add(client)
|
targets.add(client)
|
||||||
if targets:
|
if targets:
|
||||||
ctx.broadcast(targets, [args])
|
ctx.broadcast(targets, [args])
|
||||||
|
|||||||
@@ -346,7 +346,7 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
import colorama
|
import colorama
|
||||||
|
|
||||||
colorama.init()
|
colorama.just_fix_windows_console()
|
||||||
|
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
colorama.deinit()
|
colorama.deinit()
|
||||||
|
|||||||
@@ -1579,6 +1579,7 @@ def dump_player_options(multiworld: MultiWorld) -> None:
|
|||||||
player_output = {
|
player_output = {
|
||||||
"Game": multiworld.game[player],
|
"Game": multiworld.game[player],
|
||||||
"Name": multiworld.get_player_name(player),
|
"Name": multiworld.get_player_name(player),
|
||||||
|
"ID": player,
|
||||||
}
|
}
|
||||||
output.append(player_output)
|
output.append(player_output)
|
||||||
for option_key, option in world.options_dataclass.type_hints.items():
|
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)
|
game_option_names.append(display_name)
|
||||||
|
|
||||||
with open(output_path(f"generate_{multiworld.seed_name}.csv"), mode="w", newline="") as file:
|
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 = DictWriter(file, fields)
|
||||||
writer.writeheader()
|
writer.writeheader()
|
||||||
writer.writerows(output)
|
writer.writerows(output)
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ Currently, the following games are supported:
|
|||||||
* Castlevania: Circle of the Moon
|
* Castlevania: Circle of the Moon
|
||||||
* Inscryption
|
* Inscryption
|
||||||
* Civilization VI
|
* Civilization VI
|
||||||
|
* The Legend of Zelda: The Wind Waker
|
||||||
|
|
||||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
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
|
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__':
|
if __name__ == '__main__':
|
||||||
colorama.init()
|
colorama.just_fix_windows_console()
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
colorama.deinit()
|
colorama.deinit()
|
||||||
|
|||||||
@@ -500,7 +500,7 @@ def main():
|
|||||||
|
|
||||||
import colorama
|
import colorama
|
||||||
|
|
||||||
colorama.init()
|
colorama.just_fix_windows_console()
|
||||||
|
|
||||||
asyncio.run(_main())
|
asyncio.run(_main())
|
||||||
colorama.deinit()
|
colorama.deinit()
|
||||||
|
|||||||
2
Utils.py
2
Utils.py
@@ -47,7 +47,7 @@ class Version(typing.NamedTuple):
|
|||||||
return ".".join(str(item) for item in self)
|
return ".".join(str(item) for item in self)
|
||||||
|
|
||||||
|
|
||||||
__version__ = "0.6.0"
|
__version__ = "0.6.2"
|
||||||
version_tuple = tuplize_version(__version__)
|
version_tuple = tuplize_version(__version__)
|
||||||
|
|
||||||
is_linux = sys.platform.startswith("linux")
|
is_linux = sys.platform.startswith("linux")
|
||||||
|
|||||||
@@ -214,17 +214,11 @@ class WargrooveContext(CommonContext):
|
|||||||
def run_gui(self):
|
def run_gui(self):
|
||||||
"""Import kivy UI system and start running it as self.ui_task."""
|
"""Import kivy UI system and start running it as self.ui_task."""
|
||||||
from kvui import GameManager, HoverBehavior, ServerToolTip
|
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.lang import Builder
|
||||||
from kivy.uix.button import Button
|
|
||||||
from kivy.uix.togglebutton import ToggleButton
|
from kivy.uix.togglebutton import ToggleButton
|
||||||
from kivy.uix.boxlayout import BoxLayout
|
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.uix.label import Label
|
||||||
from kivy.properties import ColorProperty
|
|
||||||
from kivy.uix.image import Image
|
|
||||||
import pkgutil
|
import pkgutil
|
||||||
|
|
||||||
class TrackerLayout(BoxLayout):
|
class TrackerLayout(BoxLayout):
|
||||||
@@ -446,6 +440,6 @@ if __name__ == '__main__':
|
|||||||
parser = get_base_parser(description="Wargroove Client, for text interfacing.")
|
parser = get_base_parser(description="Wargroove Client, for text interfacing.")
|
||||||
|
|
||||||
args, rest = parser.parse_known_args()
|
args, rest = parser.parse_known_args()
|
||||||
colorama.init()
|
colorama.just_fix_windows_console()
|
||||||
asyncio.run(main(args))
|
asyncio.run(main(args))
|
||||||
colorama.deinit()
|
colorama.deinit()
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from threading import Event, Thread
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
from uuid import UUID
|
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 Utils import restricted_loads
|
||||||
from .locker import Locker, AlreadyRunningException
|
from .locker import Locker, AlreadyRunningException
|
||||||
@@ -36,12 +36,21 @@ def handle_generation_failure(result: BaseException):
|
|||||||
logging.exception(e)
|
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):
|
def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
|
||||||
try:
|
try:
|
||||||
meta = json.loads(generation.meta)
|
meta = json.loads(generation.meta)
|
||||||
options = restricted_loads(generation.options)
|
options = restricted_loads(generation.options)
|
||||||
logging.info(f"Generating {generation.id} for {len(options)} players")
|
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,
|
{"meta": meta,
|
||||||
"sid": generation.id,
|
"sid": generation.id,
|
||||||
"owner": generation.owner},
|
"owner": generation.owner},
|
||||||
@@ -55,6 +64,10 @@ def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
|
|||||||
|
|
||||||
|
|
||||||
def init_generator(config: dict[str, Any]) -> None:
|
def init_generator(config: dict[str, Any]) -> None:
|
||||||
|
from setproctitle import setproctitle
|
||||||
|
|
||||||
|
setproctitle("Generator (idle)")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import resource
|
import resource
|
||||||
except ModuleNotFoundError:
|
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,
|
def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
||||||
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str],
|
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str],
|
||||||
host: str, rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue):
|
host: str, rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue):
|
||||||
|
from setproctitle import setproctitle
|
||||||
|
|
||||||
|
setproctitle(name)
|
||||||
Utils.init_logging(name)
|
Utils.init_logging(name)
|
||||||
try:
|
try:
|
||||||
import resource
|
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.")
|
raise Exception("Worlds system should not be loaded in the custom server.")
|
||||||
|
|
||||||
import gc
|
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
|
gc.collect() # free intermediate objects used during setup
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
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
|
assert ctx.server is None
|
||||||
try:
|
try:
|
||||||
ctx.server = websockets.serve(
|
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
|
await ctx.server
|
||||||
except OSError: # likely port in use
|
except OSError: # likely port in use
|
||||||
ctx.server = websockets.serve(
|
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
|
await ctx.server
|
||||||
port = 0
|
port = 0
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=Non
|
|||||||
{"bosses", "items", "connections", "texts"}))
|
{"bosses", "items", "connections", "texts"}))
|
||||||
erargs.skip_prog_balancing = False
|
erargs.skip_prog_balancing = False
|
||||||
erargs.skip_output = False
|
erargs.skip_output = False
|
||||||
|
erargs.spoiler_only = False
|
||||||
erargs.csv_output = False
|
erargs.csv_output = False
|
||||||
|
|
||||||
name_counter = Counter()
|
name_counter = Counter()
|
||||||
|
|||||||
@@ -35,6 +35,12 @@ def start_playing():
|
|||||||
@app.route('/games/<string:game>/info/<string:lang>')
|
@app.route('/games/<string:game>/info/<string:lang>')
|
||||||
@cache.cached()
|
@cache.cached()
|
||||||
def game_info(game, lang):
|
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))
|
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>')
|
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
|
||||||
@cache.cached()
|
@cache.cached()
|
||||||
def tutorial(game, file, lang):
|
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))
|
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
|
from docutils.core import publish_parts
|
||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
from flask import redirect, render_template, request, Response
|
from flask import redirect, render_template, request, Response, abort
|
||||||
|
|
||||||
import Options
|
import Options
|
||||||
from Utils import local_path
|
from Utils import local_path
|
||||||
@@ -142,7 +142,10 @@ def weighted_options_old():
|
|||||||
@app.route("/games/<string:game>/weighted-options")
|
@app.route("/games/<string:game>/weighted-options")
|
||||||
@cache.cached()
|
@cache.cached()
|
||||||
def weighted_options(game: str):
|
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"])
|
@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")
|
@app.route("/games/<string:game>/player-options")
|
||||||
@cache.cached()
|
@cache.cached()
|
||||||
def player_options(game: str):
|
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
|
# YAML generator for player-options
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
flask>=3.0.3
|
flask>=3.1.0
|
||||||
werkzeug>=3.0.6
|
werkzeug>=3.1.3
|
||||||
pony>=0.7.19
|
pony>=0.7.19
|
||||||
waitress>=3.0.0
|
waitress>=3.0.2
|
||||||
Flask-Caching>=2.3.0
|
Flask-Caching>=2.3.0
|
||||||
Flask-Compress>=1.15
|
Flask-Compress>=1.17
|
||||||
Flask-Limiter>=3.8.0
|
Flask-Limiter>=3.12
|
||||||
bokeh>=3.5.2
|
bokeh>=3.6.3
|
||||||
markupsafe>=2.1.5
|
markupsafe>=3.0.2
|
||||||
Markdown>=3.7
|
Markdown>=3.7
|
||||||
mdx-breakless-lists>=1.0.1
|
mdx-breakless-lists>=1.0.1
|
||||||
|
setproctitle>=1.3.5
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ window.addEventListener('load', () => {
|
|||||||
showdown.setOption('strikethrough', true);
|
showdown.setOption('strikethrough', true);
|
||||||
showdown.setOption('literalMidWordUnderscores', true);
|
showdown.setOption('literalMidWordUnderscores', true);
|
||||||
gameInfo.innerHTML += (new showdown.Converter()).makeHtml(results);
|
gameInfo.innerHTML += (new showdown.Converter()).makeHtml(results);
|
||||||
adjustHeaderWidth();
|
|
||||||
|
|
||||||
// Reset the id of all header divs to something nicer
|
// Reset the id of all header divs to something nicer
|
||||||
for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) {
|
for (const header of document.querySelectorAll('h1, h2, h3, h4, h5, h6')) {
|
||||||
@@ -42,10 +41,5 @@ window.addEventListener('load', () => {
|
|||||||
scrollTarget?.scrollIntoView();
|
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('file-input').addEventListener('change', () => {
|
||||||
document.getElementById('host-game-form').submit();
|
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('literalMidWordUnderscores', true);
|
||||||
showdown.setOption('disableForced4SpacesIndentedSublists', true);
|
showdown.setOption('disableForced4SpacesIndentedSublists', true);
|
||||||
tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results);
|
tutorialWrapper.innerHTML += (new showdown.Converter()).makeHtml(results);
|
||||||
adjustHeaderWidth();
|
|
||||||
|
|
||||||
const title = document.querySelector('h1')
|
const title = document.querySelector('h1')
|
||||||
if (title) {
|
if (title) {
|
||||||
@@ -49,10 +48,5 @@ window.addEventListener('load', () => {
|
|||||||
scrollTarget?.scrollIntoView();
|
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{
|
body{
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: calc(100vh - 110px);
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
a{
|
a{
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{% extends 'pageWrapper.html' %}
|
{% extends 'pageWrapper.html' %}
|
||||||
{% import "macros.html" as macros %}
|
{% import "macros.html" as macros %}
|
||||||
|
{% set show_footer = True %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<title>Page Not Found (404)</title>
|
<title>Page Not Found (404)</title>
|
||||||
@@ -13,5 +14,4 @@
|
|||||||
The page you're looking for doesn't exist.<br />
|
The page you're looking for doesn't exist.<br />
|
||||||
<a href="/">Click here to return to safety.</a>
|
<a href="/">Click here to return to safety.</a>
|
||||||
</div>
|
</div>
|
||||||
{% include 'islandFooter.html' %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends 'pageWrapper.html' %}
|
{% extends 'pageWrapper.html' %}
|
||||||
|
{% set show_footer = True %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<title>Upload Multidata</title>
|
<title>Upload Multidata</title>
|
||||||
@@ -27,6 +28,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% include 'islandFooter.html' %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends 'pageWrapper.html' %}
|
{% extends 'pageWrapper.html' %}
|
||||||
|
{% set show_footer = True %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<title>Archipelago</title>
|
<title>Archipelago</title>
|
||||||
@@ -57,5 +58,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include 'islandFooter.html' %}
|
|
||||||
{% endblock %}
|
{% 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/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/cookieNotice.css") }}" />
|
||||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/globalStyles.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>
|
<script type="application/ecmascript" src="{{ url_for('static', filename="assets/cookieNotice.js") }}"></script>
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<title>Archipelago</title>
|
<title>Archipelago</title>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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() %}
|
{% block body %}
|
||||||
{% if messages %}
|
{% endblock %}
|
||||||
<div>
|
</main>
|
||||||
{% for message in messages | unique %}
|
|
||||||
<div class="user-message">{{ message }}</div>
|
{% if show_footer %}
|
||||||
{% endfor %}
|
{% include "islandFooter.html" %}
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -213,7 +213,7 @@
|
|||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro RandomizeButton(option_name, option) %}
|
{% 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 }}">
|
<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" }} />
|
<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' %}
|
{% extends 'pageWrapper.html' %}
|
||||||
{% import "macros.html" as macros %}
|
{% import "macros.html" as macros %}
|
||||||
|
{% set show_footer = True %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<title>Generation failed, please retry.</title>
|
<title>Generation failed, please retry.</title>
|
||||||
@@ -15,5 +16,4 @@
|
|||||||
{{ seed_error }}
|
{{ seed_error }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include 'islandFooter.html' %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{% extends 'pageWrapper.html' %}
|
{% extends 'pageWrapper.html' %}
|
||||||
|
{% set show_footer = True %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<title>Start Playing</title>
|
<title>Start Playing</title>
|
||||||
@@ -26,6 +27,4 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% include 'islandFooter.html' %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{% extends 'pageWrapper.html' %}
|
{% extends 'pageWrapper.html' %}
|
||||||
{% import "macros.html" as macros %}
|
{% import "macros.html" as macros %}
|
||||||
|
{% set show_footer = True %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<title>View Seed {{ seed.id|suuid }}</title>
|
<title>View Seed {{ seed.id|suuid }}</title>
|
||||||
@@ -50,5 +51,4 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% include 'islandFooter.html' %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
{% extends 'pageWrapper.html' %}
|
{% extends 'pageWrapper.html' %}
|
||||||
{% import "macros.html" as macros %}
|
{% import "macros.html" as macros %}
|
||||||
|
{% set show_footer = True %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<title>Generation in Progress</title>
|
<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") }}"/>
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/waitSeed.css") }}"/>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -15,5 +18,34 @@
|
|||||||
Waiting for game to generate, this page auto-refreshes to check.
|
Waiting for game to generate, this page auto-refreshes to check.
|
||||||
</div>
|
</div>
|
||||||
</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 %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -100,7 +100,7 @@
|
|||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="unsupported-option">
|
<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>
|
</div>
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -386,7 +386,7 @@ if __name__ == '__main__':
|
|||||||
parser.add_argument('diff_file', default="", type=str, nargs="?",
|
parser.add_argument('diff_file', default="", type=str, nargs="?",
|
||||||
help='Path to a Archipelago Binary Patch file')
|
help='Path to a Archipelago Binary Patch file')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
colorama.init()
|
colorama.just_fix_windows_console()
|
||||||
|
|
||||||
asyncio.run(main(args))
|
asyncio.run(main(args))
|
||||||
colorama.deinit()
|
colorama.deinit()
|
||||||
|
|||||||
@@ -14,23 +14,51 @@
|
|||||||
salmon: "FA8072" # typically trap item
|
salmon: "FA8072" # typically trap item
|
||||||
white: "FFFFFF" # not used, if you want to change the generic text color change color in Label
|
white: "FFFFFF" # not used, if you want to change the generic text color change color in Label
|
||||||
orange: "FF7700" # Used for command echo
|
orange: "FF7700" # Used for command echo
|
||||||
<Label>:
|
# KivyMD theming parameters
|
||||||
color: "FFFFFF"
|
theme_style: "Dark" # Light/Dark
|
||||||
<TabbedPanel>:
|
primary_palette: "Green" # Many options
|
||||||
tab_width: root.width / app.tab_count
|
dynamic_scheme_name: "TONAL_SPOT"
|
||||||
|
dynamic_scheme_contrast: 0.0
|
||||||
|
<MDLabel>:
|
||||||
|
color: self.theme_cls.primaryColor
|
||||||
<TooltipLabel>:
|
<TooltipLabel>:
|
||||||
text_size: self.width, None
|
adaptive_height: True
|
||||||
size_hint_y: None
|
|
||||||
height: self.texture_size[1]
|
|
||||||
font_size: dp(20)
|
font_size: dp(20)
|
||||||
markup: True
|
markup: True
|
||||||
|
halign: "left"
|
||||||
<SelectableLabel>:
|
<SelectableLabel>:
|
||||||
|
size_hint: 1, None
|
||||||
canvas.before:
|
canvas.before:
|
||||||
Color:
|
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:
|
Rectangle:
|
||||||
size: self.size
|
size: self.size
|
||||||
pos: self.pos
|
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>:
|
<UILog>:
|
||||||
messages: 1000 # amount of messages stored in client logs.
|
messages: 1000 # amount of messages stored in client logs.
|
||||||
cols: 1
|
cols: 1
|
||||||
@@ -49,7 +77,7 @@
|
|||||||
<HintLabel>:
|
<HintLabel>:
|
||||||
canvas.before:
|
canvas.before:
|
||||||
Color:
|
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:
|
Rectangle:
|
||||||
size: self.size
|
size: self.size
|
||||||
pos: self.pos
|
pos: self.pos
|
||||||
@@ -152,3 +180,16 @@
|
|||||||
height: dp(30)
|
height: dp(30)
|
||||||
multiline: False
|
multiline: False
|
||||||
write_tab: 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
|
# Wargroove
|
||||||
/worlds/wargroove/ @FlySniper
|
/worlds/wargroove/ @FlySniper
|
||||||
|
|
||||||
|
# The Wind Waker
|
||||||
|
/worlds/tww/ @tanjo3
|
||||||
|
|
||||||
# The Witness
|
# The Witness
|
||||||
/worlds/witness/ @NewSoupVi @blastron
|
/worlds/witness/ @NewSoupVi @blastron
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
# Adding Games
|
# 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:
|
Adding a new game to Archipelago has two major parts:
|
||||||
|
|
||||||
* Game Modification to communicate with Archipelago server (hereafter referred to as "client")
|
* 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
|
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
|
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
|
must fulfill a few requirements in order to function as expected. Libraries for most modern languages and the spec for
|
||||||
to behave as expected are:
|
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
|
* 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
|
* Reconnect if the connection is unstable and lost while playing
|
||||||
* 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
|
|
||||||
* Be able to change the port for saved connection info
|
* 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
|
* 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
|
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
|
|
||||||
* Send a status update packet alerting the server that the player has completed their goal
|
* 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
|
Regarding items and locations, the game client must be able to handle these tasks:
|
||||||
[network protocol](/docs/network%20protocol.md) API reference document.
|
|
||||||
|
#### 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
|
## 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
|
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
|
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
|
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
|
repository and creating a new world package in `/worlds/`.
|
||||||
following requirements:
|
|
||||||
|
|
||||||
* A folder within `/worlds/` that contains an `__init__.py`
|
The base World class can be found in [AutoWorld](/worlds/AutoWorld.py). Methods available for your world to call
|
||||||
* A `World` subclass where you create your world and define all of its rules
|
during generation can be found in [BaseClasses](/BaseClasses.py) and [Fill](/Fill.py). Some examples and documentation
|
||||||
* A unique game name
|
regarding the API can be found in the [world api doc](/docs/world%20api.md). Before publishing, make sure to also
|
||||||
* For webhost documentation and behaviors, a `WebWorld` subclass that must be instantiated in the `World` class
|
check out [world maintainer.md](/docs/world%20maintainer.md).
|
||||||
definition
|
|
||||||
* The game_info doc must follow the format `{language_code}_{game_name}.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
|
* 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.
|
`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 implementation of `create_item` that can create an item when called by either your code or by another process
|
||||||
* An `options_dataclass` defining the options players have available to them
|
within Archipelago
|
||||||
* A `Region` for your player with the name "Menu" to start from
|
* At least one `Region` for your player to start from (i.e. the Origin Region)
|
||||||
* Create a non-zero number of locations and add them to your regions
|
* The default name of this region is "Menu" but you may configure a different name with
|
||||||
* Create a non-zero number of items **equal** to the number of locations and add them to the multiworld itempool
|
[origin_region_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L298-L299)
|
||||||
* All items submitted to the multiworld itempool must not be manually placed by the World. If you need to place specific
|
* A non-zero number of locations, added to your regions
|
||||||
items, there are multiple ways to do so, but they should not be added to the multiworld itempool.
|
* 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:
|
### Encouraged Features
|
||||||
* 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
|
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
|
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
|
* 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.
|
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.
|
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.
|
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.
|
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`. |
|
| 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. |
|
| 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`. |
|
| 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
|
### 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.
|
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
|
### 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:
|
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 |
|
| Name | Type | Notes |
|
||||||
|--------|-------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|--------|-------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| time | float | Unix Time Stamp of time of death. |
|
| 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." |
|
| 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. |
|
| 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
|
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.
|
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
|
## Running Tests
|
||||||
|
|
||||||
#### Using Pycharm
|
#### Using Pycharm
|
||||||
@@ -100,3 +132,11 @@ next to the run and debug buttons.
|
|||||||
#### Running Tests without Pycharm
|
#### Running Tests without Pycharm
|
||||||
|
|
||||||
Run `pip install pytest pytest-subtests`, then use your IDE to run tests or run `pytest` from the source folder.
|
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:
|
def set_rules(self) -> None:
|
||||||
# For some worlds this step can be omitted if either a Logic mixin
|
# 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
|
# (see below) is used or it's easier to apply the rules from data during
|
||||||
# location generation or everything is in generate_basic
|
# location generation
|
||||||
|
|
||||||
# set a simple rule for an region
|
# set a simple rule for an region
|
||||||
set_rule(self.multiworld.get_entrance("Boss Door", self.player),
|
set_rule(self.multiworld.get_entrance("Boss Door", self.player),
|
||||||
|
|||||||
@@ -50,13 +50,15 @@ class EntranceLookup:
|
|||||||
_random: random.Random
|
_random: random.Random
|
||||||
_expands_graph_cache: dict[Entrance, bool]
|
_expands_graph_cache: dict[Entrance, bool]
|
||||||
_coupled: 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.dead_ends = EntranceLookup.GroupLookup()
|
||||||
self.others = EntranceLookup.GroupLookup()
|
self.others = EntranceLookup.GroupLookup()
|
||||||
self._random = rng
|
self._random = rng
|
||||||
self._expands_graph_cache = {}
|
self._expands_graph_cache = {}
|
||||||
self._coupled = coupled
|
self._coupled = coupled
|
||||||
|
self._usable_exits = usable_exits
|
||||||
|
|
||||||
def _can_expand_graph(self, entrance: Entrance) -> bool:
|
def _can_expand_graph(self, entrance: Entrance) -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -95,7 +97,8 @@ class EntranceLookup:
|
|||||||
# randomizable exits which are not reverse of the incoming entrance.
|
# 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
|
# uncoupled mode is an exception because in this case going back in the door you just came in could
|
||||||
# actually lead somewhere new
|
# 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
|
self._expands_graph_cache[entrance] = True
|
||||||
return True
|
return True
|
||||||
elif exit_.connected_region and exit_.connected_region not in visited:
|
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 }
|
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
|
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 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
|
:param target_group: The group to assign to the created ER target. If not specified, the group from
|
||||||
the original entrance will be copied.
|
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
|
child_region = entrance.connected_region
|
||||||
parent_region = entrance.parent_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
|
# targets in the child region will be created when the other direction edge is disconnected
|
||||||
target = parent_region.create_er_target(entrance.name)
|
target = parent_region.create_er_target(entrance.name)
|
||||||
else:
|
else:
|
||||||
# for 1-ways, the child region needs a target and coupling/naming is not a concern
|
# for 1-ways, the child region needs a target. naming is not a concern for coupling so we
|
||||||
target = child_region.create_er_target(child_region.name)
|
# 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_type = entrance.randomization_type
|
||||||
target.randomization_group = target_group or entrance.randomization_group
|
target.randomization_group = target_group or entrance.randomization_group
|
||||||
|
|
||||||
@@ -325,7 +336,6 @@ def randomize_entrances(
|
|||||||
|
|
||||||
start_time = time.perf_counter()
|
start_time = time.perf_counter()
|
||||||
er_state = ERPlacementState(world, coupled)
|
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
|
# similar to fill, skip validity checks on entrances if the game is beatable on minimal accessibility
|
||||||
perform_validity_check = True
|
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
|
# used when membership checks are needed on the exit list, e.g. speculative sweep
|
||||||
exits_set = set(exits)
|
exits_set = set(exits)
|
||||||
|
entrance_lookup = EntranceLookup(world.random, coupled, exits_set)
|
||||||
for entrance in er_targets:
|
for entrance in er_targets:
|
||||||
entrance_lookup.add(entrance)
|
entrance_lookup.add(entrance)
|
||||||
|
|
||||||
@@ -358,6 +369,34 @@ def randomize_entrances(
|
|||||||
if on_connect:
|
if on_connect:
|
||||||
on_connect(er_state, placed_exits)
|
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:
|
def find_pairing(dead_end: bool, require_new_exits: bool) -> bool:
|
||||||
nonlocal perform_validity_check
|
nonlocal perform_validity_check
|
||||||
placeable_exits = er_state.find_placeable_exits(perform_validity_check, exits)
|
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.
|
# 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.
|
# 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
|
exit_requirement_satisfied = (not perform_validity_check or not require_new_exits
|
||||||
or target_entrance.connected_region not in er_state.placed_regions)
|
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)
|
|
||||||
if exit_requirement_satisfied and source_exit.can_connect_to(target_entrance, dead_end, er_state):
|
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)):
|
and not er_state.test_speculative_connection(source_exit, target_entrance, exits_set)):
|
||||||
continue
|
continue
|
||||||
do_placement(source_exit, target_entrance)
|
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("input", "mouse", "mouse,disable_multitouch")
|
||||||
Config.set("kivy", "exit_on_escape", "0")
|
Config.set("kivy", "exit_on_escape", "0")
|
||||||
Config.set("graphics", "multisamples", "0") # multisamples crash old intel drivers
|
Config.set("graphics", "multisamples", "0") # multisamples crash old intel drivers
|
||||||
|
from kivymd.uix.divider import MDDivider
|
||||||
from kivy.app import App
|
|
||||||
from kivy.core.window import Window
|
from kivy.core.window import Window
|
||||||
from kivy.core.clipboard import Clipboard
|
from kivy.core.clipboard import Clipboard
|
||||||
from kivy.core.text.markup import MarkupLabel
|
from kivy.core.text.markup import MarkupLabel
|
||||||
@@ -46,30 +45,32 @@ from kivy.clock import Clock
|
|||||||
from kivy.factory import Factory
|
from kivy.factory import Factory
|
||||||
from kivy.properties import BooleanProperty, ObjectProperty, NumericProperty
|
from kivy.properties import BooleanProperty, ObjectProperty, NumericProperty
|
||||||
from kivy.metrics import dp
|
from kivy.metrics import dp
|
||||||
from kivy.effects.scroll import ScrollEffect
|
|
||||||
from kivy.uix.widget import Widget
|
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.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.utils import escape_markup
|
||||||
from kivy.lang import Builder
|
from kivy.lang import Builder
|
||||||
from kivy.uix.recycleview.views import RecycleDataViewBehavior
|
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.recycleboxlayout import RecycleBoxLayout
|
||||||
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
|
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
|
||||||
from kivy.animation import Animation
|
from kivy.animation import Animation
|
||||||
from kivy.uix.popup import Popup
|
from kivy.uix.popup import Popup
|
||||||
from kivy.uix.dropdown import DropDown
|
|
||||||
from kivy.uix.image import AsyncImage
|
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)
|
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"\[.*?]")
|
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 :(
|
# I was surprised to find this didn't already exist in kivy :(
|
||||||
class HoverBehavior(object):
|
class HoverBehavior(object):
|
||||||
"""originally from https://stackoverflow.com/a/605348110"""
|
"""originally from https://stackoverflow.com/a/605348110"""
|
||||||
@@ -125,7 +205,7 @@ class HoverBehavior(object):
|
|||||||
Factory.register("HoverBehavior", HoverBehavior)
|
Factory.register("HoverBehavior", HoverBehavior)
|
||||||
|
|
||||||
|
|
||||||
class ToolTip(Label):
|
class ToolTip(MDTooltipPlain):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -133,49 +213,30 @@ class ServerToolTip(ToolTip):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ScrollBox(ScrollView):
|
class HovererableLabel(HoverBehavior, MDLabel):
|
||||||
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):
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class TooltipLabel(HovererableLabel):
|
class TooltipLabel(HovererableLabel, MDTooltip):
|
||||||
tooltip = None
|
tooltip_display_delay = 0.1
|
||||||
|
|
||||||
def create_tooltip(self, text, x, y):
|
def create_tooltip(self, text, x, y):
|
||||||
text = text.replace("<br>", "\n").replace("&", "&").replace("&bl;", "[").replace("&br;", "]")
|
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
|
# position float layout
|
||||||
self.tooltip.x = x - self.tooltip.width / 2
|
center_x, center_y = self.to_window(self.center_x, self.center_y)
|
||||||
self.tooltip.y = y - self.tooltip.height / 2 + 48
|
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:
|
||||||
if self.tooltip:
|
# update
|
||||||
App.get_running_app().root.remove_widget(self.tooltip)
|
self._tooltip.text = text
|
||||||
self.tooltip = None
|
else:
|
||||||
|
self._tooltip = ToolTip(text=text, pos_hint={})
|
||||||
|
self.display_tooltip()
|
||||||
|
|
||||||
def on_mouse_pos(self, window, pos):
|
def on_mouse_pos(self, window, pos):
|
||||||
if not self.get_root_window():
|
if not self.get_root_window():
|
||||||
@@ -202,26 +263,26 @@ class TooltipLabel(HovererableLabel):
|
|||||||
|
|
||||||
def on_leave(self):
|
def on_leave(self):
|
||||||
self.remove_tooltip()
|
self.remove_tooltip()
|
||||||
|
self._tooltip = None
|
||||||
|
|
||||||
|
|
||||||
class ServerLabel(HovererableLabel):
|
class ServerLabel(HovererableLabel, MDTooltip):
|
||||||
|
tooltip_display_delay = 0.1
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(HovererableLabel, self).__init__(*args, **kwargs)
|
super(HovererableLabel, self).__init__(*args, **kwargs)
|
||||||
self.layout = FloatLayout()
|
self._tooltip = ServerToolTip(text="Test")
|
||||||
self.popuplabel = ServerToolTip(text="Test")
|
|
||||||
self.layout.add_widget(self.popuplabel)
|
|
||||||
|
|
||||||
def on_enter(self):
|
def on_enter(self):
|
||||||
self.popuplabel.text = self.get_text()
|
self._tooltip.text = self.get_text()
|
||||||
App.get_running_app().root.add_widget(self.layout)
|
self.display_tooltip()
|
||||||
fade_in_animation.start(self.layout)
|
|
||||||
|
|
||||||
def on_leave(self):
|
def on_leave(self):
|
||||||
App.get_running_app().root.remove_widget(self.layout)
|
self.animation_tooltip_dismiss()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ctx(self) -> context_type:
|
def ctx(self) -> context_type:
|
||||||
return App.get_running_app().ctx
|
return MDApp.get_running_app().ctx
|
||||||
|
|
||||||
def get_text(self):
|
def get_text(self):
|
||||||
if self.ctx.server:
|
if self.ctx.server:
|
||||||
@@ -262,11 +323,11 @@ class ServerLabel(HovererableLabel):
|
|||||||
return "No current server connection. \nPlease connect to an Archipelago server."
|
return "No current server connection. \nPlease connect to an Archipelago server."
|
||||||
|
|
||||||
|
|
||||||
class MainLayout(GridLayout):
|
class MainLayout(MDGridLayout):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ContainerLayout(FloatLayout):
|
class ContainerLayout(MDFloatLayout):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@@ -286,6 +347,11 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
|
|||||||
return super(SelectableLabel, self).refresh_view_attrs(
|
return super(SelectableLabel, self).refresh_view_attrs(
|
||||||
rv, index, data)
|
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):
|
def on_touch_down(self, touch):
|
||||||
""" Add selection on touch down """
|
""" Add selection on touch down """
|
||||||
if super(SelectableLabel, self).on_touch_down(touch):
|
if super(SelectableLabel, self).on_touch_down(touch):
|
||||||
@@ -296,10 +362,10 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
|
|||||||
else:
|
else:
|
||||||
# Not a fan of the following few lines, but they work.
|
# Not a fan of the following few lines, but they work.
|
||||||
temp = MarkupLabel(text=self.text).markup
|
temp = MarkupLabel(text=self.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("["))
|
||||||
cmdinput = App.get_running_app().textinput
|
cmdinput = MDApp.get_running_app().textinput
|
||||||
if not cmdinput.text:
|
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:
|
if input_text is not None:
|
||||||
cmdinput.text = input_text
|
cmdinput.text = input_text
|
||||||
|
|
||||||
@@ -310,30 +376,115 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
|
|||||||
""" Respond to the selection of items in the view. """
|
""" Respond to the selection of items in the view. """
|
||||||
self.selected = is_selected
|
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)
|
min_chars = NumericProperty(3)
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**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.dropdown.bind(on_select=lambda instance, x: setattr(self, 'text', x))
|
||||||
self.bind(on_text_validate=self.on_message)
|
self.bind(on_text_validate=self.on_message)
|
||||||
|
self.bind(width=lambda instance, x: setattr(self.dropdown, "width", x))
|
||||||
|
|
||||||
def on_message(self, instance):
|
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):
|
def on_text(self, instance, value):
|
||||||
if len(value) >= self.min_chars:
|
if len(value) >= self.min_chars:
|
||||||
self.dropdown.clear_widgets()
|
self.dropdown.items.clear()
|
||||||
ctx: context_type = App.get_running_app().ctx
|
ctx: context_type = MDApp.get_running_app().ctx
|
||||||
if not ctx.game:
|
if not ctx.game:
|
||||||
return
|
return
|
||||||
item_names = ctx.item_names._game_store[ctx.game].values()
|
item_names = ctx.item_names._game_store[ctx.game].values()
|
||||||
|
|
||||||
def on_press(button: Button):
|
def on_press(text):
|
||||||
split_text = MarkupLabel(text=button.text).markup
|
split_text = MarkupLabel(text=text).markup
|
||||||
return self.dropdown.select("".join(text_frag for text_frag in split_text
|
return self.dropdown.select("".join(text_frag for text_frag in split_text
|
||||||
if not text_frag.startswith("[")))
|
if not text_frag.startswith("[")))
|
||||||
lowered = value.lower()
|
lowered = value.lower()
|
||||||
@@ -345,20 +496,29 @@ class AutocompleteHintInput(TextInput):
|
|||||||
else:
|
else:
|
||||||
text = escape_markup(item_name)
|
text = escape_markup(item_name)
|
||||||
text = text[:index] + "[b]" + text[index:index+len(value)]+"[/b]"+text[index+len(value):]
|
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)
|
self.dropdown.items.append({
|
||||||
btn.bind(on_release=on_press)
|
"text": text,
|
||||||
self.dropdown.add_widget(btn)
|
"on_release": lambda: on_press(text),
|
||||||
if not self.dropdown.attach_to:
|
"markup": True
|
||||||
self.dropdown.open(self)
|
})
|
||||||
|
if not self.dropdown.parent:
|
||||||
|
self.dropdown.open()
|
||||||
else:
|
else:
|
||||||
self.dropdown.dismiss()
|
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)
|
selected = BooleanProperty(False)
|
||||||
striped = BooleanProperty(False)
|
striped = BooleanProperty(False)
|
||||||
index = None
|
index = None
|
||||||
dropdown: DropDown
|
dropdown: MDDropdownMenu
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super(HintLabel, self).__init__()
|
super(HintLabel, self).__init__()
|
||||||
@@ -369,29 +529,28 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
|||||||
self.entrance_text = ""
|
self.entrance_text = ""
|
||||||
self.status_text = ""
|
self.status_text = ""
|
||||||
self.hint = {}
|
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
|
for status in (HintStatus.HINT_NO_PRIORITY, HintStatus.HINT_PRIORITY, HintStatus.HINT_AVOID):
|
||||||
self.dropdown = DropDown()
|
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 = MDDropdownMenu(caller=self.ids["status"], items=menu_items)
|
||||||
self.dropdown.select(button.status)
|
|
||||||
|
|
||||||
def select(instance, data):
|
def select(instance, data):
|
||||||
ctx.update_hint(self.hint["location"],
|
ctx.update_hint(self.hint["location"],
|
||||||
self.hint["finding_player"],
|
self.hint["finding_player"],
|
||||||
data)
|
data)
|
||||||
|
|
||||||
for status in (HintStatus.HINT_NO_PRIORITY, HintStatus.HINT_PRIORITY, HintStatus.HINT_AVOID):
|
self.dropdown.bind(on_release=self.dropdown.dismiss)
|
||||||
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)
|
|
||||||
|
|
||||||
def set_height(self, instance, value):
|
def set_height(self, instance, value):
|
||||||
self.height = max([child.texture_size[1] for child in self.children])
|
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.entrance_text = data["entrance"]["text"]
|
||||||
self.status_text = data["status"]["text"]
|
self.status_text = data["status"]["text"]
|
||||||
self.hint = data["status"]["hint"]
|
self.hint = data["status"]["hint"]
|
||||||
self.height = self.minimum_height
|
|
||||||
return super(HintLabel, self).refresh_view_attrs(rv, index, data)
|
return super(HintLabel, self).refresh_view_attrs(rv, index, data)
|
||||||
|
|
||||||
def on_touch_down(self, touch):
|
def on_touch_down(self, touch):
|
||||||
@@ -419,10 +577,10 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
|||||||
if status_label.collide_point(*touch.pos):
|
if status_label.collide_point(*touch.pos):
|
||||||
if self.hint["status"] == HintStatus.HINT_FOUND:
|
if self.hint["status"] == HintStatus.HINT_FOUND:
|
||||||
return
|
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
|
if ctx.slot_concerns_self(self.hint["receiving_player"]): # If this player owns this hint
|
||||||
# open a dropdown
|
# open a dropdown
|
||||||
self.dropdown.open(self.ids["status"])
|
self.dropdown.open()
|
||||||
elif self.selected:
|
elif self.selected:
|
||||||
self.parent.clear_selection()
|
self.parent.clear_selection()
|
||||||
else:
|
else:
|
||||||
@@ -431,8 +589,7 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
|||||||
if self.entrance_text != "Vanilla"
|
if self.entrance_text != "Vanilla"
|
||||||
else "", ". (", self.status_text.lower(), ")"))
|
else "", ". (", self.status_text.lower(), ")"))
|
||||||
temp = MarkupLabel(text).markup
|
temp = MarkupLabel(text).markup
|
||||||
text = "".join(
|
text = "".join(part for part in temp if not part.startswith("["))
|
||||||
part for part in temp if not part.startswith(("[color", "[/color]", "[ref=", "[/ref]")))
|
|
||||||
Clipboard.copy(escape_markup(text).replace("&", "&").replace("&bl;", "[").replace("&br;", "]"))
|
Clipboard.copy(escape_markup(text).replace("&", "&").replace("&bl;", "[").replace("&br;", "]"))
|
||||||
return self.parent.select_with_touch(self.index, touch)
|
return self.parent.select_with_touch(self.index, touch)
|
||||||
else:
|
else:
|
||||||
@@ -455,7 +612,7 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
|||||||
else:
|
else:
|
||||||
parent.sort_key = key
|
parent.sort_key = key
|
||||||
parent.reversed = False
|
parent.reversed = False
|
||||||
App.get_running_app().update_hints()
|
MDApp.get_running_app().update_hints()
|
||||||
|
|
||||||
def apply_selection(self, rv, index, is_selected):
|
def apply_selection(self, rv, index, is_selected):
|
||||||
""" Respond to the selection of items in the view. """
|
""" Respond to the selection of items in the view. """
|
||||||
@@ -463,7 +620,7 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
|||||||
self.selected = is_selected
|
self.selected = is_selected
|
||||||
|
|
||||||
|
|
||||||
class ConnectBarTextInput(TextInput):
|
class ConnectBarTextInput(MDTextField):
|
||||||
def insert_text(self, substring, from_undo=False):
|
def insert_text(self, substring, from_undo=False):
|
||||||
s = substring.replace("\n", "").replace("\r", "")
|
s = substring.replace("\n", "").replace("\r", "")
|
||||||
return super(ConnectBarTextInput, self).insert_text(s, from_undo=from_undo)
|
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 "/!"
|
return len(string) > 0 and string[0] in "/!"
|
||||||
|
|
||||||
|
|
||||||
class CommandPromptTextInput(TextInput):
|
class CommandPromptTextInput(MDTextField):
|
||||||
MAXIMUM_HISTORY_MESSAGES = 50
|
MAXIMUM_HISTORY_MESSAGES = 50
|
||||||
|
|
||||||
def __init__(self, **kwargs) -> None:
|
def __init__(self, **kwargs) -> None:
|
||||||
@@ -521,7 +678,7 @@ class CommandPromptTextInput(TextInput):
|
|||||||
|
|
||||||
|
|
||||||
class MessageBox(Popup):
|
class MessageBox(Popup):
|
||||||
class MessageBoxLabel(Label):
|
class MessageBoxLabel(MDLabel):
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self._label.refresh()
|
self._label.refresh()
|
||||||
@@ -539,14 +696,31 @@ class MessageBox(Popup):
|
|||||||
self.height += max(0, label.height - 18)
|
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 = [
|
logging_pairs = [
|
||||||
("Client", "Archipelago"),
|
("Client", "Archipelago"),
|
||||||
]
|
]
|
||||||
base_title: str = "Archipelago Client"
|
base_title: str = "Archipelago Client"
|
||||||
last_autofillable_command: str
|
last_autofillable_command: str
|
||||||
|
|
||||||
main_area_container: GridLayout
|
main_area_container: MDGridLayout
|
||||||
""" subclasses can add more columns beside the tabs """
|
""" subclasses can add more columns beside the tabs """
|
||||||
|
|
||||||
def __init__(self, ctx: context_type):
|
def __init__(self, ctx: context_type):
|
||||||
@@ -581,18 +755,26 @@ class GameManager(App):
|
|||||||
return max(1, len(self.tabs.tab_list))
|
return max(1, len(self.tabs.tab_list))
|
||||||
return 1
|
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:
|
def build(self) -> Layout:
|
||||||
|
self.set_colors()
|
||||||
self.container = ContainerLayout()
|
self.container = ContainerLayout()
|
||||||
|
|
||||||
self.grid = MainLayout()
|
self.grid = MainLayout()
|
||||||
self.grid.cols = 1
|
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
|
# top part
|
||||||
server_label = ServerLabel()
|
server_label = ServerLabel(halign="center")
|
||||||
self.connect_layout.add_widget(server_label)
|
self.connect_layout.add_widget(server_label)
|
||||||
self.server_connect_bar = ConnectBarTextInput(text=self.ctx.suggested_address or "archipelago.gg:",
|
self.server_connect_bar = ConnectBarTextInput(text=self.ctx.suggested_address or "archipelago.gg:",
|
||||||
size_hint_y=None,
|
size_hint_y=None, role="medium",
|
||||||
height=dp(30), multiline=False, write_tab=False)
|
height=dp(70), multiline=False, write_tab=False)
|
||||||
|
|
||||||
def connect_bar_validate(sender):
|
def connect_bar_validate(sender):
|
||||||
if not self.ctx.server:
|
if not self.ctx.server:
|
||||||
@@ -600,26 +782,31 @@ class GameManager(App):
|
|||||||
|
|
||||||
self.server_connect_bar.bind(on_text_validate=connect_bar_validate)
|
self.server_connect_bar.bind(on_text_validate=connect_bar_validate)
|
||||||
self.connect_layout.add_widget(self.server_connect_bar)
|
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.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.connect_layout.add_widget(self.server_connect_button)
|
||||||
self.grid.add_widget(self.connect_layout)
|
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)
|
self.grid.add_widget(self.progressbar)
|
||||||
|
|
||||||
# middle part
|
# middle part
|
||||||
self.tabs = TabbedPanel(size_hint_y=1)
|
self.tabs = ClientTabs()
|
||||||
self.tabs.default_tab_text = "All"
|
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)
|
self.log_panels["All"] = self.tabs.default_tab_content = UILog(*(logging.getLogger(logger_name)
|
||||||
for logger_name, name in
|
for logger_name, name in
|
||||||
self.logging_pairs))
|
self.logging_pairs))
|
||||||
|
self.tabs.carousel.add_widget(self.tabs.default_tab_content)
|
||||||
|
|
||||||
for logger_name, display_name in self.logging_pairs:
|
for logger_name, display_name in self.logging_pairs:
|
||||||
bridge_logger = logging.getLogger(logger_name)
|
bridge_logger = logging.getLogger(logger_name)
|
||||||
panel = TabbedPanelItem(text=display_name)
|
self.log_panels[display_name] = UILog(bridge_logger)
|
||||||
self.log_panels[display_name] = panel.content = UILog(bridge_logger)
|
|
||||||
if len(self.logging_pairs) > 1:
|
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
|
# show Archipelago tab if other logging is present
|
||||||
|
self.tabs.carousel.add_widget(panel.content)
|
||||||
self.tabs.add_widget(panel)
|
self.tabs.add_widget(panel)
|
||||||
|
|
||||||
hint_panel = self.add_client_tab("Hints", HintLayout())
|
hint_panel = self.add_client_tab("Hints", HintLayout())
|
||||||
@@ -627,21 +814,20 @@ class GameManager(App):
|
|||||||
self.log_panels["Hints"] = hint_panel.content
|
self.log_panels["Hints"] = hint_panel.content
|
||||||
hint_panel.content.add_widget(self.hint_log)
|
hint_panel.content.add_widget(self.hint_log)
|
||||||
|
|
||||||
if len(self.logging_pairs) == 1:
|
self.main_area_container = MDGridLayout(size_hint_y=1, rows=1)
|
||||||
self.tabs.default_tab_text = "Archipelago"
|
|
||||||
|
|
||||||
self.main_area_container = GridLayout(size_hint_y=1, rows=1)
|
|
||||||
self.main_area_container.add_widget(self.tabs)
|
self.main_area_container.add_widget(self.tabs)
|
||||||
|
|
||||||
self.grid.add_widget(self.main_area_container)
|
self.grid.add_widget(self.main_area_container)
|
||||||
|
|
||||||
# bottom part
|
# bottom part
|
||||||
bottom_layout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30))
|
bottom_layout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(70), spacing=5, padding=(5, 10))
|
||||||
info_button = Button(size=(dp(100), dp(30)), text="Command:", size_hint_x=None)
|
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)
|
info_button.bind(on_release=self.command_button_action)
|
||||||
bottom_layout.add_widget(info_button)
|
bottom_layout.add_widget(info_button)
|
||||||
self.textinput = CommandPromptTextInput(size_hint_y=None, height=dp(30), multiline=False, write_tab=False)
|
self.textinput = CommandPromptTextInput(size_hint_y=None, height=dp(30), multiline=False, write_tab=False)
|
||||||
self.textinput.bind(on_text_validate=self.on_message)
|
self.textinput.bind(on_text_validate=self.on_message)
|
||||||
|
info_button.height = self.textinput.height
|
||||||
self.textinput.text_validate_unfocus = False
|
self.textinput.text_validate_unfocus = False
|
||||||
bottom_layout.add_widget(self.textinput)
|
bottom_layout.add_widget(self.textinput)
|
||||||
self.grid.add_widget(bottom_layout)
|
self.grid.add_widget(bottom_layout)
|
||||||
@@ -662,24 +848,26 @@ class GameManager(App):
|
|||||||
def add_client_tab(self, title: str, content: Widget) -> Widget:
|
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.
|
"""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."""
|
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
|
new_tab.content = content
|
||||||
self.tabs.add_widget(new_tab)
|
self.tabs.add_widget(new_tab)
|
||||||
|
self.tabs.carousel.add_widget(new_tab.content)
|
||||||
return new_tab
|
return new_tab
|
||||||
|
|
||||||
def update_texts(self, dt):
|
def update_texts(self, dt):
|
||||||
if hasattr(self.tabs.content.children[0], "fix_heights"):
|
for slide in self.tabs.carousel.slides:
|
||||||
self.tabs.content.children[0].fix_heights() # TODO: remove this when Kivy fixes this upstream
|
if hasattr(slide, "fix_heights"):
|
||||||
|
slide.fix_heights() # TODO: remove this when Kivy fixes this upstream
|
||||||
if self.ctx.server:
|
if self.ctx.server:
|
||||||
self.title = self.base_title + " " + Utils.__version__ + \
|
self.title = self.base_title + " " + Utils.__version__ + \
|
||||||
f" | Connected to: {self.ctx.server_address} " \
|
f" | Connected to: {self.ctx.server_address} " \
|
||||||
f"{'.'.join(str(e) for e in self.ctx.server_version)}"
|
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.server_connect_bar.readonly = True
|
||||||
self.progressbar.max = len(self.ctx.checked_locations) + len(self.ctx.missing_locations)
|
self.progressbar.max = len(self.ctx.checked_locations) + len(self.ctx.missing_locations)
|
||||||
self.progressbar.value = len(self.ctx.checked_locations)
|
self.progressbar.value = len(self.ctx.checked_locations)
|
||||||
else:
|
else:
|
||||||
self.server_connect_button.text = "Connect"
|
self.server_connect_button._button_text.text = "Connect"
|
||||||
self.server_connect_bar.readonly = False
|
self.server_connect_bar.readonly = False
|
||||||
self.title = self.base_title + " " + Utils.__version__
|
self.title = self.base_title + " " + Utils.__version__
|
||||||
self.progressbar.value = 0
|
self.progressbar.value = 0
|
||||||
@@ -742,8 +930,8 @@ class GameManager(App):
|
|||||||
|
|
||||||
def enable_energy_link(self):
|
def enable_energy_link(self):
|
||||||
if not hasattr(self, "energy_link_label"):
|
if not hasattr(self, "energy_link_label"):
|
||||||
self.energy_link_label = Label(text="Energy Link: Standby",
|
self.energy_link_label = MDLabel(text="Energy Link: Standby",
|
||||||
size_hint_x=None, width=150)
|
size_hint_x=None, width=150, halign="center")
|
||||||
self.connect_layout.add_widget(self.energy_link_label)
|
self.connect_layout.add_widget(self.energy_link_label)
|
||||||
|
|
||||||
def set_new_energy_link_value(self):
|
def set_new_energy_link_value(self):
|
||||||
@@ -779,8 +967,9 @@ class LogtoUI(logging.Handler):
|
|||||||
self.on_log(self.format(record))
|
self.on_log(self.format(record))
|
||||||
|
|
||||||
|
|
||||||
class UILog(RecycleView):
|
class UILog(MDRecycleView):
|
||||||
messages: typing.ClassVar[int] # comes from kv file
|
messages: typing.ClassVar[int] # comes from kv file
|
||||||
|
adaptive_height = True
|
||||||
|
|
||||||
def __init__(self, *loggers_to_handle, **kwargs):
|
def __init__(self, *loggers_to_handle, **kwargs):
|
||||||
super(UILog, self).__init__(**kwargs)
|
super(UILog, self).__init__(**kwargs)
|
||||||
@@ -807,16 +996,22 @@ class UILog(RecycleView):
|
|||||||
element.height = element.texture_size[1]
|
element.height = element.texture_size[1]
|
||||||
|
|
||||||
|
|
||||||
class HintLayout(BoxLayout):
|
class HintLayout(MDBoxLayout):
|
||||||
orientation = "vertical"
|
orientation = "vertical"
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
boxlayout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30))
|
boxlayout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(55))
|
||||||
boxlayout.add_widget(Label(text="New Hint:", size_hint_x=None, size_hint_y=None, height=dp(30)))
|
boxlayout.add_widget(MDLabel(text="New Hint:", size_hint_x=None, size_hint_y=None, height=dp(55)))
|
||||||
boxlayout.add_widget(AutocompleteHintInput())
|
boxlayout.add_widget(AutocompleteHintInput())
|
||||||
self.add_widget(boxlayout)
|
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] = {
|
status_names: typing.Dict[HintStatus, str] = {
|
||||||
HintStatus.HINT_FOUND: "Found",
|
HintStatus.HINT_FOUND: "Found",
|
||||||
@@ -840,8 +1035,7 @@ status_sort_weights: dict[HintStatus, int] = {
|
|||||||
HintStatus.HINT_PRIORITY: 4,
|
HintStatus.HINT_PRIORITY: 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class HintLog(MDRecycleView):
|
||||||
class HintLog(RecycleView):
|
|
||||||
header = {
|
header = {
|
||||||
"receiving": {"text": "[u]Receiving Player[/u]"},
|
"receiving": {"text": "[u]Receiving Player[/u]"},
|
||||||
"item": {"text": "[u]Item[/u]"},
|
"item": {"text": "[u]Item[/u]"},
|
||||||
@@ -852,7 +1046,7 @@ class HintLog(RecycleView):
|
|||||||
"hint": {"receiving_player": -1, "location": -1, "finding_player": -1, "status": ""}},
|
"hint": {"receiving_player": -1, "location": -1, "finding_player": -1, "status": ""}},
|
||||||
"striped": True,
|
"striped": True,
|
||||||
}
|
}
|
||||||
|
data: list[typing.Any]
|
||||||
sort_key: str = ""
|
sort_key: str = ""
|
||||||
reversed: bool = True
|
reversed: bool = True
|
||||||
|
|
||||||
@@ -865,7 +1059,7 @@ class HintLog(RecycleView):
|
|||||||
if not hints: # Fix the scrolling looking visually wrong in some edge cases
|
if not hints: # Fix the scrolling looking visually wrong in some edge cases
|
||||||
self.scroll_y = 1.0
|
self.scroll_y = 1.0
|
||||||
data = []
|
data = []
|
||||||
ctx = App.get_running_app().ctx
|
ctx = MDApp.get_running_app().ctx
|
||||||
for hint in hints:
|
for hint in hints:
|
||||||
if not hint.get("status"): # Allows connecting to old servers
|
if not hint.get("status"): # Allows connecting to old servers
|
||||||
hint["status"] = HintStatus.HINT_FOUND if hint["found"] else HintStatus.HINT_UNSPECIFIED
|
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)
|
data = pkgutil.get_data(module, path)
|
||||||
return self._bytes_to_data(data)
|
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())
|
loader = next(loader for loader in ImageLoader.loaders if loader.can_load_memory())
|
||||||
return loader.load(loader, io.BytesIO(data))
|
return loader.load(loader, io.BytesIO(data))
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
colorama>=0.4.6
|
colorama>=0.4.6
|
||||||
websockets>=13.0.1,<14
|
websockets>=13.0.1,<14
|
||||||
PyYAML>=6.0.2
|
PyYAML>=6.0.2
|
||||||
jellyfish>=1.1.0
|
jellyfish>=1.1.3
|
||||||
jinja2>=3.1.4
|
jinja2>=3.1.6
|
||||||
schema>=0.7.7
|
schema>=0.7.7
|
||||||
kivy>=2.3.0
|
kivy>=2.3.1
|
||||||
bsdiff4>=1.2.4
|
bsdiff4>=1.2.6
|
||||||
platformdirs>=4.2.2
|
platformdirs>=4.3.6
|
||||||
certifi>=2024.12.14
|
certifi>=2025.1.31
|
||||||
cython>=3.0.11
|
cython>=3.0.12
|
||||||
cymem>=2.0.8
|
cymem>=2.0.11
|
||||||
orjson>=3.10.7
|
orjson>=3.10.15
|
||||||
typing_extensions>=4.12.2
|
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
|
# 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:
|
try:
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
try:
|
try:
|
||||||
@@ -629,12 +629,13 @@ cx_Freeze.setup(
|
|||||||
ext_modules=cythonize("_speedups.pyx"),
|
ext_modules=cythonize("_speedups.pyx"),
|
||||||
options={
|
options={
|
||||||
"build_exe": {
|
"build_exe": {
|
||||||
"packages": ["worlds", "kivy", "cymem", "websockets"],
|
"packages": ["worlds", "kivy", "cymem", "websockets", "kivymd"],
|
||||||
"includes": [],
|
"includes": [],
|
||||||
"excludes": ["numpy", "Cython", "PySide2", "PIL",
|
"excludes": ["numpy", "Cython", "PySide2", "PIL",
|
||||||
"pandas", "zstandard"],
|
"pandas"],
|
||||||
|
"zip_includes": [],
|
||||||
"zip_include_packages": ["*"],
|
"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_files": [], # broken in cx 6.14.0, we use more special sauce now
|
||||||
"include_msvcr": False,
|
"include_msvcr": False,
|
||||||
"replace_paths": ["*."],
|
"replace_paths": ["*."],
|
||||||
|
|||||||
@@ -65,8 +65,10 @@ class TestEntranceLookup(unittest.TestCase):
|
|||||||
"""tests that get_targets shuffles targets between groups when requested"""
|
"""tests that get_targets shuffles targets between groups when requested"""
|
||||||
multiworld = generate_test_multiworld()
|
multiworld = generate_test_multiworld()
|
||||||
generate_disconnected_region_grid(multiworld, 5)
|
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)
|
er_targets = [entrance for region in multiworld.get_regions(1)
|
||||||
for entrance in region.entrances if not entrance.parent_region]
|
for entrance in region.entrances if not entrance.parent_region]
|
||||||
for entrance in er_targets:
|
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"""
|
"""tests that get_targets does not shuffle targets between groups when requested"""
|
||||||
multiworld = generate_test_multiworld()
|
multiworld = generate_test_multiworld()
|
||||||
generate_disconnected_region_grid(multiworld, 5)
|
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)
|
er_targets = [entrance for region in multiworld.get_regions(1)
|
||||||
for entrance in region.entrances if not entrance.parent_region]
|
for entrance in region.entrances if not entrance.parent_region]
|
||||||
for entrance in er_targets:
|
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]
|
group_order = [prev := group.randomization_group for group in retrieved_targets if prev != group.randomization_group]
|
||||||
self.assertEqual([ERTestGroups.TOP, ERTestGroups.BOTTOM], group_order)
|
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):
|
class TestBakeTargetGroupLookup(unittest.TestCase):
|
||||||
def test_lookup_generation(self):
|
def test_lookup_generation(self):
|
||||||
@@ -148,7 +176,7 @@ class TestDisconnectForRandomization(unittest.TestCase):
|
|||||||
e.randomization_group = 1
|
e.randomization_group = 1
|
||||||
e.connect(r2)
|
e.connect(r2)
|
||||||
|
|
||||||
disconnect_entrance_for_randomization(e)
|
disconnect_entrance_for_randomization(e, one_way_target_name="foo")
|
||||||
|
|
||||||
self.assertIsNone(e.connected_region)
|
self.assertIsNone(e.connected_region)
|
||||||
self.assertEqual([], r1.entrances)
|
self.assertEqual([], r1.entrances)
|
||||||
@@ -158,10 +186,22 @@ class TestDisconnectForRandomization(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(1, len(r2.entrances))
|
self.assertEqual(1, len(r2.entrances))
|
||||||
self.assertIsNone(r2.entrances[0].parent_region)
|
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(EntranceType.ONE_WAY, r2.entrances[0].randomization_type)
|
||||||
self.assertEqual(1, r2.entrances[0].randomization_group)
|
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):
|
def test_disconnect_uses_alternate_group(self):
|
||||||
multiworld = generate_test_multiworld()
|
multiworld = generate_test_multiworld()
|
||||||
r1 = Region("r1", 1, multiworld)
|
r1 = Region("r1", 1, multiworld)
|
||||||
@@ -171,7 +211,7 @@ class TestDisconnectForRandomization(unittest.TestCase):
|
|||||||
e.randomization_group = 1
|
e.randomization_group = 1
|
||||||
e.connect(r2)
|
e.connect(r2)
|
||||||
|
|
||||||
disconnect_entrance_for_randomization(e, 2)
|
disconnect_entrance_for_randomization(e, 2, "foo")
|
||||||
|
|
||||||
self.assertIsNone(e.connected_region)
|
self.assertIsNone(e.connected_region)
|
||||||
self.assertEqual([], r1.entrances)
|
self.assertEqual([], r1.entrances)
|
||||||
@@ -181,7 +221,7 @@ class TestDisconnectForRandomization(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(1, len(r2.entrances))
|
self.assertEqual(1, len(r2.entrances))
|
||||||
self.assertIsNone(r2.entrances[0].parent_region)
|
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(EntranceType.ONE_WAY, r2.entrances[0].randomization_type)
|
||||||
self.assertEqual(2, r2.entrances[0].randomization_group)
|
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.pairings))
|
||||||
self.assertEqual(80, len(result.placements))
|
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"""
|
"""tests that in coupled mode, all 2 way transitions have an inverse"""
|
||||||
multiworld = generate_test_multiworld()
|
multiworld = generate_test_multiworld()
|
||||||
generate_disconnected_region_grid(multiworld, 5)
|
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
|
# if we didn't visit every placement the verification on_connect doesn't really mean much
|
||||||
self.assertEqual(len(result.placements), seen_placement_count)
|
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):
|
def test_uncoupled(self):
|
||||||
"""tests that in uncoupled mode, no transitions have an (intentional) inverse"""
|
"""tests that in uncoupled mode, no transitions have an (intentional) inverse"""
|
||||||
multiworld = generate_test_multiworld()
|
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
|
import unittest
|
||||||
from typing import List, Tuple
|
from typing import ClassVar, List, Tuple
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
from BaseClasses import CollectionState, Location, MultiWorld
|
from BaseClasses import CollectionState, Location, MultiWorld
|
||||||
@@ -7,6 +7,7 @@ from Fill import distribute_items_restrictive
|
|||||||
from Options import Accessibility
|
from Options import Accessibility
|
||||||
from worlds.AutoWorld import AutoWorldRegister, call_all, call_single
|
from worlds.AutoWorld import AutoWorldRegister, call_all, call_single
|
||||||
from ..general import gen_steps, setup_multiworld
|
from ..general import gen_steps, setup_multiworld
|
||||||
|
from ..param import classvar_matrix
|
||||||
|
|
||||||
|
|
||||||
class MultiworldTestBase(TestCase):
|
class MultiworldTestBase(TestCase):
|
||||||
@@ -63,15 +64,18 @@ class TestAllGamesMultiworld(MultiworldTestBase):
|
|||||||
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
|
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
|
||||||
|
|
||||||
|
|
||||||
|
@classvar_matrix(game=AutoWorldRegister.world_types.keys())
|
||||||
class TestTwoPlayerMulti(MultiworldTestBase):
|
class TestTwoPlayerMulti(MultiworldTestBase):
|
||||||
|
game: ClassVar[str]
|
||||||
|
|
||||||
def test_two_player_single_game_fills(self) -> None:
|
def test_two_player_single_game_fills(self) -> None:
|
||||||
"""Tests that a multiworld of two players for each registered game world can generate."""
|
"""Tests that a multiworld of two players for each registered game world can generate."""
|
||||||
for world_type in AutoWorldRegister.world_types.values():
|
world_type = AutoWorldRegister.world_types[self.game]
|
||||||
self.multiworld = setup_multiworld([world_type, world_type], ())
|
self.multiworld = setup_multiworld([world_type, world_type], ())
|
||||||
for world in self.multiworld.worlds.values():
|
for world in self.multiworld.worlds.values():
|
||||||
world.options.accessibility.value = Accessibility.option_full
|
world.options.accessibility.value = Accessibility.option_full
|
||||||
self.assertSteps(gen_steps)
|
self.assertSteps(gen_steps)
|
||||||
with self.subTest("filling multiworld", games=world_type.game, seed=self.multiworld.seed):
|
with self.subTest("filling multiworld", games=world_type.game, seed=self.multiworld.seed):
|
||||||
distribute_items_restrictive(self.multiworld)
|
distribute_items_restrictive(self.multiworld)
|
||||||
call_all(self.multiworld, "post_fill")
|
call_all(self.multiworld, "post_fill")
|
||||||
self.assertTrue(self.fulfills_accessibility(), "Collected all locations, but can't beat the game")
|
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:
|
if TYPE_CHECKING:
|
||||||
from SNIClient import SNIContext
|
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)
|
components.append(component)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,16 @@ class AutoLogicRegister(type):
|
|||||||
elif not item_name.startswith("__"):
|
elif not item_name.startswith("__"):
|
||||||
if hasattr(CollectionState, item_name):
|
if hasattr(CollectionState, item_name):
|
||||||
raise Exception(f"Name conflict on Logic Mixin {name} trying to overwrite {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)
|
setattr(CollectionState, item_name, function)
|
||||||
return new_class
|
return new_class
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ class Component:
|
|||||||
"""
|
"""
|
||||||
display_name: str
|
display_name: str
|
||||||
"""Used as the GUI button label and the component name in the CLI args"""
|
"""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
|
type: Type
|
||||||
"""
|
"""
|
||||||
Enum "Type" classification of component intent, for filtering in the Launcher GUI
|
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,
|
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,
|
cli: bool = False, icon: str = 'icon', component_type: Optional[Type] = None,
|
||||||
func: Optional[Callable] = None, file_identifier: Optional[Callable[[str], bool]] = 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.display_name = display_name
|
||||||
|
self.description = description
|
||||||
self.script_name = script_name
|
self.script_name = script_name
|
||||||
self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None
|
self.frozen_name = frozen_name or f'Archipelago{script_name}' if script_name else None
|
||||||
self.icon = icon
|
self.icon = icon
|
||||||
@@ -88,7 +91,6 @@ processes = weakref.WeakSet()
|
|||||||
|
|
||||||
|
|
||||||
def launch_subprocess(func: Callable, name: str | None = None, args: Tuple[str, ...] = ()) -> None:
|
def launch_subprocess(func: Callable, name: str | None = None, args: Tuple[str, ...] = ()) -> None:
|
||||||
global processes
|
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
process = multiprocessing.Process(target=func, name=name, args=args)
|
process = multiprocessing.Process(target=func, name=name, args=args)
|
||||||
process.start()
|
process.start()
|
||||||
|
|||||||
@@ -276,6 +276,6 @@ def launch(*launch_args: str) -> None:
|
|||||||
|
|
||||||
Utils.init_logging("BizHawkClient", exception_logger="Client")
|
Utils.init_logging("BizHawkClient", exception_logger="Client")
|
||||||
import colorama
|
import colorama
|
||||||
colorama.init()
|
colorama.just_fix_windows_console()
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
colorama.deinit()
|
colorama.deinit()
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ mpyq>=0.2.5
|
|||||||
portpicker>=1.5.2
|
portpicker>=1.5.2
|
||||||
aiohttp>=3.8.4
|
aiohttp>=3.8.4
|
||||||
loguru>=0.7.0
|
loguru>=0.7.0
|
||||||
protobuf==3.20.3
|
protobuf==3.20.3
|
||||||
|
|||||||
@@ -238,14 +238,12 @@ class AdventureWorld(World):
|
|||||||
|
|
||||||
def create_regions(self) -> None:
|
def create_regions(self) -> None:
|
||||||
create_regions(self.options, self.multiworld, self.player, self.dragon_rooms)
|
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.multiworld.get_location("Chalice Home", self.player).place_locked_item(
|
||||||
self.create_event("Victory", ItemClassification.progression))
|
self.create_event("Victory", ItemClassification.progression))
|
||||||
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
|
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
|
||||||
|
|
||||||
|
set_rules = set_rules
|
||||||
|
|
||||||
def pre_fill(self):
|
def pre_fill(self):
|
||||||
# Place empty items in filler locations here, to limit
|
# Place empty items in filler locations here, to limit
|
||||||
# the number of exported empty items and the density of stuff in overworld.
|
# the number of exported empty items and the density of stuff in overworld.
|
||||||
|
|||||||
@@ -261,6 +261,6 @@ def launch():
|
|||||||
# options = Utils.get_options()
|
# options = Utils.get_options()
|
||||||
|
|
||||||
import colorama
|
import colorama
|
||||||
colorama.init()
|
colorama.just_fix_windows_console()
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
colorama.deinit()
|
colorama.deinit()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from typing import List, TYPE_CHECKING, Dict, Any
|
|||||||
from schema import Schema, Optional
|
from schema import Schema, Optional
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from worlds.AutoWorld import PerGameCommonOptions
|
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:
|
if TYPE_CHECKING:
|
||||||
from . import HatInTimeWorld
|
from . import HatInTimeWorld
|
||||||
@@ -625,6 +625,8 @@ class ParadeTrapWeight(Range):
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AHITOptions(PerGameCommonOptions):
|
class AHITOptions(PerGameCommonOptions):
|
||||||
|
start_inventory_from_pool: StartInventoryPool
|
||||||
|
|
||||||
EndGoal: EndGoal
|
EndGoal: EndGoal
|
||||||
ActRandomizer: ActRandomizer
|
ActRandomizer: ActRandomizer
|
||||||
ActPlando: ActPlando
|
ActPlando: ActPlando
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from BaseClasses import Item, ItemClassification, Tutorial, Location, MultiWorld
|
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, \
|
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 .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, \
|
from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID, \
|
||||||
get_total_locations
|
get_total_locations
|
||||||
@@ -78,6 +78,9 @@ class HatInTimeWorld(World):
|
|||||||
self.nyakuza_thug_items: Dict[str, int] = {}
|
self.nyakuza_thug_items: Dict[str, int] = {}
|
||||||
self.badge_seller_count: int = 0
|
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):
|
def generate_early(self):
|
||||||
adjust_options(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.")
|
logging.debug(f"Spritefile {file} could not be loaded as a valid sprite.")
|
||||||
|
|
||||||
with concurrent.futures.ThreadPoolExecutor() as pool:
|
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):
|
for file in os.listdir(dir):
|
||||||
pool.submit(load_sprite_from_file, os.path.join(dir, file))
|
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():
|
class Sprite():
|
||||||
sprite_size = 28672
|
sprite_size = 28672
|
||||||
@@ -554,6 +559,11 @@ class Sprite():
|
|||||||
self.sprite = filedata[0x80000:0x87000]
|
self.sprite = filedata[0x80000:0x87000]
|
||||||
self.palette = filedata[0xDD308:0xDD380]
|
self.palette = filedata[0xDD308:0xDD380]
|
||||||
self.glove_palette = filedata[0xDEDF5:0xDEDF9]
|
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'):
|
elif filedata.startswith(b'ZSPR'):
|
||||||
self.from_zspr(filedata, filename)
|
self.from_zspr(filedata, filename)
|
||||||
else:
|
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]
|
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):
|
class ALTTPWorld(World):
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
maseya-z3pr>=1.0.0rc1
|
maseya-z3pr>=1.0.0rc1
|
||||||
xxtea>=3.0.0
|
xxtea>=3.0.0
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ class AquariaWeb(WebWorld):
|
|||||||
)
|
)
|
||||||
|
|
||||||
tutorials = [setup, setup_fr]
|
tutorials = [setup, setup_fr]
|
||||||
|
game_info_languages = ["en", "fr"]
|
||||||
|
|
||||||
|
|
||||||
class AquariaWorld(World):
|
class AquariaWorld(World):
|
||||||
|
|||||||
@@ -1,6 +1,31 @@
|
|||||||
# Celeste 64 - Changelog
|
# 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
|
## v1.2
|
||||||
|
|
||||||
### Features:
|
### Features:
|
||||||
|
|||||||
@@ -39,6 +39,22 @@ move_item_data_table: Dict[str, Celeste64ItemData] = {
|
|||||||
ItemName.climb: Celeste64ItemData(celeste_64_base_id + 0xD, ItemClassification.progression),
|
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}
|
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 typing import Dict, NamedTuple, Optional
|
||||||
|
|
||||||
from BaseClasses import Location
|
from BaseClasses import Location
|
||||||
from .Names import LocationName
|
from .Names import LocationName, RegionName
|
||||||
|
|
||||||
|
|
||||||
celeste_64_base_id: int = 0xCA0000
|
celeste_64_base_id: int = 0xCA0000
|
||||||
@@ -17,66 +17,80 @@ class Celeste64LocationData(NamedTuple):
|
|||||||
|
|
||||||
|
|
||||||
strawberry_location_data_table: Dict[str, Celeste64LocationData] = {
|
strawberry_location_data_table: Dict[str, Celeste64LocationData] = {
|
||||||
LocationName.strawberry_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x00),
|
LocationName.strawberry_1: Celeste64LocationData(RegionName.intro_islands, celeste_64_base_id + 0x00),
|
||||||
LocationName.strawberry_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x01),
|
LocationName.strawberry_2: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x01),
|
||||||
LocationName.strawberry_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x02),
|
LocationName.strawberry_3: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x02),
|
||||||
LocationName.strawberry_4: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x03),
|
LocationName.strawberry_4: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x03),
|
||||||
LocationName.strawberry_5: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x04),
|
LocationName.strawberry_5: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x04),
|
||||||
LocationName.strawberry_6: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x05),
|
LocationName.strawberry_6: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x05),
|
||||||
LocationName.strawberry_7: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x06),
|
LocationName.strawberry_7: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x06),
|
||||||
LocationName.strawberry_8: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x07),
|
LocationName.strawberry_8: Celeste64LocationData(RegionName.nw_girders_island, celeste_64_base_id + 0x07),
|
||||||
LocationName.strawberry_9: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x08),
|
LocationName.strawberry_9: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x08),
|
||||||
LocationName.strawberry_10: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x09),
|
LocationName.strawberry_10: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x09),
|
||||||
LocationName.strawberry_11: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0A),
|
LocationName.strawberry_11: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x0A),
|
||||||
LocationName.strawberry_12: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0B),
|
LocationName.strawberry_12: Celeste64LocationData(RegionName.badeline_tower_lower, celeste_64_base_id + 0x0B),
|
||||||
LocationName.strawberry_13: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0C),
|
LocationName.strawberry_13: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x0C),
|
||||||
LocationName.strawberry_14: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0D),
|
LocationName.strawberry_14: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x0D),
|
||||||
LocationName.strawberry_15: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0E),
|
LocationName.strawberry_15: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x0E),
|
||||||
LocationName.strawberry_16: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x0F),
|
LocationName.strawberry_16: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x0F),
|
||||||
LocationName.strawberry_17: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x10),
|
LocationName.strawberry_17: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x10),
|
||||||
LocationName.strawberry_18: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x11),
|
LocationName.strawberry_18: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x11),
|
||||||
LocationName.strawberry_19: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x12),
|
LocationName.strawberry_19: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x12),
|
||||||
LocationName.strawberry_20: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x13),
|
LocationName.strawberry_20: Celeste64LocationData(RegionName.badeline_tower_lower, celeste_64_base_id + 0x13),
|
||||||
LocationName.strawberry_21: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x14),
|
LocationName.strawberry_21: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x14),
|
||||||
LocationName.strawberry_22: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x15),
|
LocationName.strawberry_22: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x15),
|
||||||
LocationName.strawberry_23: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x16),
|
LocationName.strawberry_23: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x16),
|
||||||
LocationName.strawberry_24: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x17),
|
LocationName.strawberry_24: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x17),
|
||||||
LocationName.strawberry_25: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x18),
|
LocationName.strawberry_25: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x18),
|
||||||
LocationName.strawberry_26: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x19),
|
LocationName.strawberry_26: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x19),
|
||||||
LocationName.strawberry_27: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x1A),
|
LocationName.strawberry_27: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x1A),
|
||||||
LocationName.strawberry_28: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x1B),
|
LocationName.strawberry_28: Celeste64LocationData(RegionName.ne_feathers_island, celeste_64_base_id + 0x1B),
|
||||||
LocationName.strawberry_29: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x1C),
|
LocationName.strawberry_29: Celeste64LocationData(RegionName.badeline_tower_upper, celeste_64_base_id + 0x1C),
|
||||||
LocationName.strawberry_30: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x1D),
|
LocationName.strawberry_30: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x1D),
|
||||||
}
|
}
|
||||||
|
|
||||||
friend_location_data_table: Dict[str, Celeste64LocationData] = {
|
friend_location_data_table: Dict[str, Celeste64LocationData] = {
|
||||||
LocationName.granny_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x00),
|
LocationName.granny_1: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x00),
|
||||||
LocationName.granny_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x01),
|
LocationName.granny_2: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x01),
|
||||||
LocationName.granny_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x02),
|
LocationName.granny_3: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x02),
|
||||||
LocationName.theo_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x03),
|
LocationName.theo_1: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x03),
|
||||||
LocationName.theo_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x04),
|
LocationName.theo_2: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x04),
|
||||||
LocationName.theo_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x05),
|
LocationName.theo_3: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x100 + 0x05),
|
||||||
LocationName.badeline_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x06),
|
LocationName.badeline_1: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x100 + 0x06),
|
||||||
LocationName.badeline_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x07),
|
LocationName.badeline_2: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x100 + 0x07),
|
||||||
LocationName.badeline_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x100 + 0x08),
|
LocationName.badeline_3: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x100 + 0x08),
|
||||||
}
|
}
|
||||||
|
|
||||||
sign_location_data_table: Dict[str, Celeste64LocationData] = {
|
sign_location_data_table: Dict[str, Celeste64LocationData] = {
|
||||||
LocationName.sign_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x00),
|
LocationName.sign_1: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x200 + 0x00),
|
||||||
LocationName.sign_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x01),
|
LocationName.sign_2: Celeste64LocationData(RegionName.granny_island, celeste_64_base_id + 0x200 + 0x01),
|
||||||
LocationName.sign_3: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x02),
|
LocationName.sign_3: Celeste64LocationData(RegionName.highway_island, celeste_64_base_id + 0x200 + 0x02),
|
||||||
LocationName.sign_4: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x03),
|
LocationName.sign_4: Celeste64LocationData(RegionName.se_house_island, celeste_64_base_id + 0x200 + 0x03),
|
||||||
LocationName.sign_5: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x200 + 0x04),
|
LocationName.sign_5: Celeste64LocationData(RegionName.badeline_island, celeste_64_base_id + 0x200 + 0x04),
|
||||||
}
|
}
|
||||||
|
|
||||||
car_location_data_table: Dict[str, Celeste64LocationData] = {
|
car_location_data_table: Dict[str, Celeste64LocationData] = {
|
||||||
LocationName.car_1: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x300 + 0x00),
|
LocationName.car_1: Celeste64LocationData(RegionName.intro_islands, celeste_64_base_id + 0x300 + 0x00),
|
||||||
LocationName.car_2: Celeste64LocationData("Forsaken City", celeste_64_base_id + 0x300 + 0x01),
|
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,
|
location_data_table: Dict[str, Celeste64LocationData] = {**strawberry_location_data_table,
|
||||||
**friend_location_data_table,
|
**friend_location_data_table,
|
||||||
**sign_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}
|
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"
|
air_dash = "Air Dash"
|
||||||
skid_jump = "Skid Jump"
|
skid_jump = "Skid Jump"
|
||||||
climb = "Climb"
|
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_9 = "South-West Dash Refills Strawberry"
|
||||||
strawberry_10 = "South-East Tower Side Strawberry"
|
strawberry_10 = "South-East Tower Side Strawberry"
|
||||||
strawberry_11 = "Girders Strawberry"
|
strawberry_11 = "Girders Strawberry"
|
||||||
strawberry_12 = "North-East Tower Bottom Strawberry"
|
strawberry_12 = "Badeline Tower Bottom Strawberry"
|
||||||
strawberry_13 = "Breakable Blocks Strawberry"
|
strawberry_13 = "Breakable Blocks Strawberry"
|
||||||
strawberry_14 = "Feather Maze Strawberry"
|
strawberry_14 = "Feather Maze Strawberry"
|
||||||
strawberry_15 = "Feather Chain Strawberry"
|
strawberry_15 = "Feather Chain Strawberry"
|
||||||
@@ -18,7 +18,7 @@ strawberry_16 = "Feather Hidden Strawberry"
|
|||||||
strawberry_17 = "Double Dash Puzzle Strawberry"
|
strawberry_17 = "Double Dash Puzzle Strawberry"
|
||||||
strawberry_18 = "Double Dash Spike Climb Strawberry"
|
strawberry_18 = "Double Dash Spike Climb Strawberry"
|
||||||
strawberry_19 = "Double Dash Spring 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_21 = "Theo Tower Lower Cassette Strawberry"
|
||||||
strawberry_22 = "Theo Tower Upper Cassette Strawberry"
|
strawberry_22 = "Theo Tower Upper Cassette Strawberry"
|
||||||
strawberry_23 = "South End of Bridge 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_26 = "North End of Bridge Cassette Strawberry"
|
||||||
strawberry_27 = "Distant Feather Cassette Strawberry"
|
strawberry_27 = "Distant Feather Cassette Strawberry"
|
||||||
strawberry_28 = "Feather Arches Cassette Strawberry"
|
strawberry_28 = "Feather Arches Cassette Strawberry"
|
||||||
strawberry_29 = "North-East Tower Cassette Strawberry"
|
strawberry_29 = "Badeline Tower Cassette Strawberry"
|
||||||
strawberry_30 = "Badeline Cassette Strawberry"
|
strawberry_30 = "Badeline Island Cassette Strawberry"
|
||||||
|
|
||||||
# Friend Locations
|
# Friend Locations
|
||||||
granny_1 = "Granny Conversation 1"
|
granny_1 = "Granny Conversation 1"
|
||||||
@@ -51,3 +51,15 @@ sign_5 = "Credits Sign"
|
|||||||
# Car Locations
|
# Car Locations
|
||||||
car_1 = "Intro Car"
|
car_1 = "Intro Car"
|
||||||
car_2 = "Secret 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
|
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):
|
class DeathLinkAmnesty(Range):
|
||||||
@@ -18,7 +20,7 @@ class TotalStrawberries(Range):
|
|||||||
"""
|
"""
|
||||||
display_name = "Total Strawberries"
|
display_name = "Total Strawberries"
|
||||||
range_start = 0
|
range_start = 0
|
||||||
range_end = 46
|
range_end = 55
|
||||||
default = 20
|
default = 20
|
||||||
|
|
||||||
class StrawberriesRequiredPercentage(Range):
|
class StrawberriesRequiredPercentage(Range):
|
||||||
@@ -73,6 +75,93 @@ class Carsanity(Toggle):
|
|||||||
"""
|
"""
|
||||||
display_name = "Carsanity"
|
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):
|
class BadelineChaserSource(Choice):
|
||||||
"""
|
"""
|
||||||
@@ -119,6 +208,13 @@ celeste_64_option_groups = [
|
|||||||
Friendsanity,
|
Friendsanity,
|
||||||
Signsanity,
|
Signsanity,
|
||||||
Carsanity,
|
Carsanity,
|
||||||
|
Checkpointsanity,
|
||||||
|
]),
|
||||||
|
OptionGroup("Aesthetic Options", [
|
||||||
|
MadelineOneDashHairColor,
|
||||||
|
MadelineTwoDashHairColor,
|
||||||
|
MadelineNoDashHairColor,
|
||||||
|
MadelineFeatherHairColor,
|
||||||
]),
|
]),
|
||||||
OptionGroup("Badeline Chasers", [
|
OptionGroup("Badeline Chasers", [
|
||||||
BadelineChaserSource,
|
BadelineChaserSource,
|
||||||
@@ -142,7 +238,68 @@ class Celeste64Options(PerGameCommonOptions):
|
|||||||
friendsanity: Friendsanity
|
friendsanity: Friendsanity
|
||||||
signsanity: Signsanity
|
signsanity: Signsanity
|
||||||
carsanity: Carsanity
|
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_source: BadelineChaserSource
|
||||||
badeline_chaser_frequency: BadelineChaserFrequency
|
badeline_chaser_frequency: BadelineChaserFrequency
|
||||||
badeline_chaser_speed: BadelineChaserSpeed
|
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 typing import Dict, List, NamedTuple
|
||||||
|
|
||||||
|
from .Names import RegionName
|
||||||
|
|
||||||
class Celeste64RegionData(NamedTuple):
|
class Celeste64RegionData(NamedTuple):
|
||||||
connecting_regions: List[str] = []
|
connecting_regions: List[str] = []
|
||||||
|
|
||||||
|
|
||||||
region_data_table: Dict[str, Celeste64RegionData] = {
|
region_data_table: Dict[str, Celeste64RegionData] = {
|
||||||
"Menu": Celeste64RegionData(["Forsaken City"]),
|
"Menu": Celeste64RegionData([RegionName.forsaken_city]),
|
||||||
"Forsaken City": Celeste64RegionData(),
|
|
||||||
|
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 worlds.generic.Rules import set_rule
|
||||||
|
|
||||||
from . import Celeste64World
|
from . import Celeste64World
|
||||||
from .Names import ItemName, LocationName
|
from .Names import ItemName, LocationName, RegionName
|
||||||
|
|
||||||
|
|
||||||
def set_rules(world: Celeste64World):
|
def set_rules(world: Celeste64World):
|
||||||
if world.options.logic_difficulty == "standard":
|
if world.options.logic_difficulty == "standard":
|
||||||
if world.options.move_shuffle:
|
world.active_logic_mapping = location_standard_moves_logic
|
||||||
world.active_logic_mapping = location_standard_moves_logic
|
world.active_region_logic_mapping = region_standard_moves_logic
|
||||||
else:
|
|
||||||
world.active_logic_mapping = location_standard_logic
|
|
||||||
else:
|
else:
|
||||||
if world.options.move_shuffle:
|
world.active_logic_mapping = location_hard_moves_logic
|
||||||
world.active_logic_mapping = location_hard_moves_logic
|
world.active_region_logic_mapping = region_hard_moves_logic
|
||||||
else:
|
|
||||||
world.active_logic_mapping = location_hard_logic
|
|
||||||
|
|
||||||
for location in world.multiworld.get_locations(world.player):
|
for location in world.multiworld.get_locations(world.player):
|
||||||
set_rule(location, lambda state, location=location: location_rule(state, world, location.name))
|
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.
|
# Completion condition.
|
||||||
world.multiworld.completion_condition[world.player] = lambda state: goal_rule(state, world)
|
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]]] = {
|
location_standard_moves_logic: Dict[str, List[List[str]]] = {
|
||||||
LocationName.strawberry_1: [[ItemName.ground_dash],
|
LocationName.strawberry_1: [[ItemName.ground_dash],
|
||||||
[ItemName.air_dash],
|
[ItemName.air_dash],
|
||||||
[ItemName.skid_jump],
|
|
||||||
[ItemName.climb]],
|
|
||||||
LocationName.strawberry_2: [[ItemName.ground_dash],
|
|
||||||
[ItemName.air_dash],
|
|
||||||
[ItemName.skid_jump],
|
|
||||||
[ItemName.climb]],
|
[ItemName.climb]],
|
||||||
|
LocationName.strawberry_2: [[ItemName.air_dash],
|
||||||
|
[ItemName.skid_jump]],
|
||||||
LocationName.strawberry_3: [[ItemName.air_dash],
|
LocationName.strawberry_3: [[ItemName.air_dash],
|
||||||
[ItemName.skid_jump]],
|
[ItemName.skid_jump]],
|
||||||
LocationName.strawberry_4: [[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
|
LocationName.strawberry_4: [[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
|
||||||
LocationName.strawberry_5: [[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_9: [[ItemName.dash_refill, ItemName.air_dash]],
|
||||||
LocationName.strawberry_10: [[ItemName.climb]],
|
LocationName.strawberry_10: [[ItemName.climb]],
|
||||||
LocationName.strawberry_11: [[ItemName.dash_refill, ItemName.air_dash, ItemName.climb],
|
LocationName.strawberry_11: [[ItemName.air_dash, ItemName.climb]],
|
||||||
[ItemName.traffic_block, ItemName.climb]],
|
LocationName.strawberry_13: [[ItemName.breakables, ItemName.air_dash],
|
||||||
LocationName.strawberry_12: [[ItemName.dash_refill, ItemName.double_dash_refill, ItemName.air_dash],
|
[ItemName.breakables, ItemName.ground_dash]],
|
||||||
[ItemName.traffic_block, ItemName.double_dash_refill, ItemName.air_dash]],
|
LocationName.strawberry_14: [[ItemName.feather, ItemName.air_dash]],
|
||||||
LocationName.strawberry_13: [[ItemName.dash_refill, ItemName.breakables, ItemName.air_dash],
|
LocationName.strawberry_15: [[ItemName.feather, ItemName.air_dash, ItemName.climb]],
|
||||||
[ItemName.traffic_block, ItemName.breakables, ItemName.ground_dash],
|
LocationName.strawberry_16: [[ItemName.feather]],
|
||||||
[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
|
LocationName.strawberry_17: [[ItemName.double_dash_refill, ItemName.feather, ItemName.traffic_block]],
|
||||||
LocationName.strawberry_14: [[ItemName.dash_refill, ItemName.feather, ItemName.air_dash],
|
LocationName.strawberry_18: [[ItemName.double_dash_refill, ItemName.air_dash, ItemName.climb]],
|
||||||
[ItemName.traffic_block, ItemName.feather, ItemName.air_dash]],
|
LocationName.strawberry_19: [[ItemName.double_dash_refill, ItemName.spring, ItemName.air_dash, ItemName.skid_jump]],
|
||||||
LocationName.strawberry_15: [[ItemName.dash_refill, ItemName.feather, ItemName.air_dash, ItemName.climb],
|
LocationName.strawberry_20: [[ItemName.feather, ItemName.breakables, ItemName.air_dash]],
|
||||||
[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_21: [[ItemName.cassette, ItemName.traffic_block, 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_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],
|
LocationName.strawberry_23: [[ItemName.cassette, ItemName.coin, ItemName.air_dash, ItemName.climb]],
|
||||||
[ItemName.cassette, ItemName.traffic_block, ItemName.coin, ItemName.air_dash, ItemName.climb]],
|
|
||||||
LocationName.strawberry_24: [[ItemName.cassette, ItemName.dash_refill, ItemName.traffic_block, ItemName.air_dash]],
|
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],
|
LocationName.strawberry_25: [[ItemName.cassette, 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.air_dash, ItemName.climb]],
|
||||||
LocationName.strawberry_26: [[ItemName.cassette, ItemName.dash_refill, ItemName.air_dash, ItemName.climb],
|
LocationName.strawberry_27: [[ItemName.cassette, ItemName.feather, ItemName.coin, ItemName.air_dash, ItemName.climb]],
|
||||||
[ItemName.cassette, ItemName.traffic_block, ItemName.air_dash, ItemName.climb]],
|
LocationName.strawberry_28: [[ItemName.cassette, ItemName.feather, ItemName.coin, ItemName.air_dash, ItemName.climb]],
|
||||||
LocationName.strawberry_27: [[ItemName.cassette, ItemName.dash_refill, ItemName.feather, ItemName.coin, ItemName.air_dash],
|
LocationName.strawberry_29: [[ItemName.cassette, ItemName.dash_refill, ItemName.coin, ItemName.air_dash, ItemName.skid_jump]],
|
||||||
[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_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.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_1: [[ItemName.traffic_block, ItemName.breakables, ItemName.air_dash]],
|
||||||
LocationName.theo_2: [[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.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],
|
LocationName.sign_2: [[ItemName.breakables, ItemName.ground_dash],
|
||||||
[ItemName.breakables, ItemName.air_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],
|
LocationName.car_2: [[ItemName.breakables, ItemName.ground_dash, ItemName.climb],
|
||||||
[ItemName.breakables, ItemName.air_dash]],
|
[ItemName.breakables, ItemName.air_dash, ItemName.climb]],
|
||||||
}
|
}
|
||||||
|
|
||||||
location_hard_moves_logic: Dict[str, List[List[str]]] = {
|
location_hard_moves_logic: Dict[str, List[List[str]]] = {
|
||||||
LocationName.strawberry_3: [[ItemName.air_dash],
|
|
||||||
[ItemName.skid_jump]],
|
|
||||||
LocationName.strawberry_5: [[ItemName.ground_dash],
|
LocationName.strawberry_5: [[ItemName.ground_dash],
|
||||||
[ItemName.air_dash]],
|
[ItemName.air_dash]],
|
||||||
LocationName.strawberry_8: [[ItemName.traffic_block],
|
|
||||||
[ItemName.ground_dash, ItemName.air_dash]],
|
|
||||||
LocationName.strawberry_10: [[ItemName.air_dash],
|
LocationName.strawberry_10: [[ItemName.air_dash],
|
||||||
[ItemName.climb]],
|
[ItemName.climb]],
|
||||||
LocationName.strawberry_11: [[ItemName.ground_dash],
|
LocationName.strawberry_11: [[ItemName.ground_dash],
|
||||||
[ItemName.air_dash],
|
[ItemName.air_dash],
|
||||||
[ItemName.skid_jump]],
|
[ItemName.skid_jump]],
|
||||||
LocationName.strawberry_12: [[ItemName.feather],
|
|
||||||
[ItemName.ground_dash],
|
|
||||||
[ItemName.air_dash]],
|
|
||||||
LocationName.strawberry_13: [[ItemName.breakables, ItemName.ground_dash],
|
LocationName.strawberry_13: [[ItemName.breakables, ItemName.ground_dash],
|
||||||
[ItemName.breakables, ItemName.air_dash]],
|
[ItemName.breakables, ItemName.air_dash]],
|
||||||
LocationName.strawberry_14: [[ItemName.feather, 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],
|
LocationName.strawberry_15: [[ItemName.feather],
|
||||||
[ItemName.ground_dash, ItemName.air_dash]],
|
[ItemName.ground_dash, ItemName.air_dash]],
|
||||||
LocationName.strawberry_17: [[ItemName.double_dash_refill, ItemName.traffic_block]],
|
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]],
|
[ItemName.cassette, ItemName.feather, ItemName.climb]],
|
||||||
LocationName.strawberry_29: [[ItemName.cassette, ItemName.dash_refill, ItemName.air_dash, ItemName.skid_jump],
|
LocationName.strawberry_29: [[ItemName.cassette, ItemName.dash_refill, ItemName.air_dash, ItemName.skid_jump],
|
||||||
[ItemName.cassette, ItemName.ground_dash, ItemName.air_dash]],
|
[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],
|
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.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.air_dash, ItemName.climb]],
|
||||||
[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.sign_2: [[ItemName.breakables, ItemName.ground_dash],
|
LocationName.sign_2: [[ItemName.breakables, ItemName.ground_dash],
|
||||||
[ItemName.breakables, ItemName.air_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],
|
LocationName.car_2: [[ItemName.breakables, ItemName.ground_dash],
|
||||||
[ItemName.breakables, ItemName.air_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:
|
if loc not in world.active_logic_mapping:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -332,12 +204,28 @@ def location_rule(state: CollectionState, world: Celeste64World, loc: str) -> bo
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def goal_rule(state: CollectionState, world: Celeste64World) -> bool:
|
def region_connection_rule(state: CollectionState, world: Celeste64World, region_connection: Tuple[str]) -> bool:
|
||||||
if not state.has(ItemName.strawberry, world.player, world.strawberries_required):
|
if region_connection not in world.active_region_logic_mapping:
|
||||||
return False
|
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):
|
if state.has_all(possible_access, world.player):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
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 copy import deepcopy
|
||||||
from typing import Dict, List
|
from typing import Dict, List, Tuple
|
||||||
|
|
||||||
from BaseClasses import ItemClassification, Location, Region, Tutorial
|
from BaseClasses import ItemClassification, Location, Region, Tutorial
|
||||||
from worlds.AutoWorld import WebWorld, World
|
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,\
|
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 .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):
|
class Celeste64WebWorld(WebWorld):
|
||||||
@@ -42,8 +44,15 @@ class Celeste64World(World):
|
|||||||
# Instance Data
|
# Instance Data
|
||||||
strawberries_required: int
|
strawberries_required: int
|
||||||
active_logic_mapping: Dict[str, List[List[str]]]
|
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:
|
def create_item(self, name: str) -> Celeste64Item:
|
||||||
# Only make required amount of strawberries be Progression
|
# Only make required amount of strawberries be Progression
|
||||||
@@ -76,25 +85,49 @@ class Celeste64World(World):
|
|||||||
for name in unlockable_item_data_table.keys()
|
for name in unlockable_item_data_table.keys()
|
||||||
if name not in self.options.start_inventory]
|
if name not in self.options.start_inventory]
|
||||||
|
|
||||||
if self.options.move_shuffle:
|
chosen_start_item: str = ""
|
||||||
move_items_for_itempool: List[str] = deepcopy(list(move_item_data_table.keys()))
|
|
||||||
|
|
||||||
|
if self.options.move_shuffle:
|
||||||
if self.options.logic_difficulty == "standard":
|
if self.options.logic_difficulty == "standard":
|
||||||
# If the start_inventory already includes a move, don't worry about giving it one
|
possible_unwalls: List[str] = [name for name in move_item_data_table.keys()
|
||||||
if not [move for move in move_items_for_itempool if move in self.options.start_inventory]:
|
if name != ItemName.skid_jump]
|
||||||
chosen_start_move = self.random.choice(move_items_for_itempool)
|
|
||||||
move_items_for_itempool.remove(chosen_start_move)
|
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:
|
if self.options.carsanity:
|
||||||
intro_car_loc: Location = self.multiworld.get_location(LocationName.car_1, self.player)
|
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
|
location_count -= 1
|
||||||
else:
|
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)
|
item_pool += [self.create_item(name)
|
||||||
for name in move_items_for_itempool
|
for name in move_item_data_table.keys()
|
||||||
if name not in self.options.start_inventory]
|
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))
|
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))
|
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
|
if location_data.region == region_name
|
||||||
}, Celeste64Location)
|
}, 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:
|
def get_filler_item_name(self) -> str:
|
||||||
return ItemName.raspberry
|
return ItemName.raspberry
|
||||||
|
|
||||||
|
|
||||||
def set_rules(self) -> None:
|
|
||||||
from .Rules import set_rules
|
|
||||||
set_rules(self)
|
|
||||||
|
|
||||||
|
|
||||||
def fill_slot_data(self):
|
def fill_slot_data(self):
|
||||||
return {
|
return {
|
||||||
"death_link": self.options.death_link.value,
|
"death_link": self.options.death_link.value,
|
||||||
@@ -161,6 +199,11 @@ class Celeste64World(World):
|
|||||||
"friendsanity": self.options.friendsanity.value,
|
"friendsanity": self.options.friendsanity.value,
|
||||||
"signsanity": self.options.signsanity.value,
|
"signsanity": self.options.signsanity.value,
|
||||||
"carsanity": self.options.carsanity.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_source": self.options.badeline_chaser_source.value,
|
||||||
"badeline_chaser_frequency": self.options.badeline_chaser_frequency.value,
|
"badeline_chaser_frequency": self.options.badeline_chaser_frequency.value,
|
||||||
"badeline_chaser_speed": self.options.badeline_chaser_speed.value,
|
"badeline_chaser_speed": self.options.badeline_chaser_speed.value,
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import os
|
import os
|
||||||
|
import io
|
||||||
from typing import TYPE_CHECKING, Dict, List, Optional, cast
|
from typing import TYPE_CHECKING, Dict, List, Optional, cast
|
||||||
import zipfile
|
import zipfile
|
||||||
from BaseClasses import Location
|
from BaseClasses import Location
|
||||||
from worlds.Files import APContainer
|
from worlds.Files import APContainer, AutoPatchRegister
|
||||||
|
|
||||||
from .Enum import CivVICheckType
|
from .Enum import CivVICheckType
|
||||||
from .Locations import CivVILocation, CivVILocationData
|
from .Locations import CivVILocation, CivVILocationData
|
||||||
@@ -25,24 +26,32 @@ class CivTreeItem:
|
|||||||
ui_tree_row: int
|
ui_tree_row: int
|
||||||
|
|
||||||
|
|
||||||
class CivVIContainer(APContainer):
|
class CivVIContainer(APContainer, metaclass=AutoPatchRegister):
|
||||||
"""
|
"""
|
||||||
Responsible for generating the dynamic mod files for the Civ VI multiworld
|
Responsible for generating the dynamic mod files for the Civ VI multiworld
|
||||||
"""
|
"""
|
||||||
game: Optional[str] = "Civilization VI"
|
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 = ""):
|
player: Optional[int] = None, player_name: str = "", server: str = ""):
|
||||||
self.patch_data = patch_data
|
if isinstance(patch_data, io.BytesIO):
|
||||||
self.file_path = base_path
|
super().__init__(patch_data, player, player_name, server)
|
||||||
container_path = os.path.join(output_directory, base_path + ".apcivvi")
|
else:
|
||||||
super().__init__(container_path, player, player_name, server)
|
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:
|
def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
|
||||||
for filename, yml in self.patch_data.items():
|
for filename, yml in self.patch_data.items():
|
||||||
opened_zipfile.writestr(filename, yml)
|
opened_zipfile.writestr(filename, yml)
|
||||||
super().write_contents(opened_zipfile)
|
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:
|
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
|
Returns the name of the player in the world
|
||||||
"""
|
"""
|
||||||
if player != world.player:
|
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"
|
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!"/>
|
<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}" '
|
{"".join([f'{tab}<Row TechnologyType="{location.name}" '
|
||||||
f'Name="{get_formatted_player_name(world, location.item.player)} '
|
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'EraType="{world.location_table[location.name].era_type}" '
|
||||||
f'UITreeRow="{world.location_table[location.name].uiTreeRow}" '
|
f'UITreeRow="{world.location_table[location.name].uiTreeRow}" '
|
||||||
f'Cost="{get_cost(world, world.location_table[location.name])}" '
|
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!"/>
|
<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}" '
|
{"".join([f'{tab}<Row CivicType="{location.name}" '
|
||||||
f'Name="{get_formatted_player_name(world, location.item.player)} '
|
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'EraType="{world.location_table[location.name].era_type}" '
|
||||||
f'UITreeRow="{world.location_table[location.name].uiTreeRow}" '
|
f'UITreeRow="{world.location_table[location.name].uiTreeRow}" '
|
||||||
f'Cost="{get_cost(world, world.location_table[location.name])}" '
|
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?
|
- 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.
|
- 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!
|
- 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!
|
- 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.
|
- 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?!
|
- 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
|
## Enabling the tuner
|
||||||
|
|
||||||
Depending on how you installed Civ 6 you will have to navigate to one of the following:
|
In the main menu, navigate to the "Game Options" page. On the "Game" menu, make sure that "Tuner (disables achievements)" is enabled.
|
||||||
|
|
||||||
- `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.
|
|
||||||
|
|
||||||
## Mod Installation
|
## Mod Installation
|
||||||
|
|
||||||
1. Download and unzip the latest release of the mod from [GitHub](https://github.com/hesto2/civilization_archipelago_mod/releases/latest).
|
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.
|
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:
|
5. Your finished mod folder should look something like this:
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class CliqueWebWorld(WebWorld):
|
|||||||
)
|
)
|
||||||
|
|
||||||
tutorials = [setup_en, setup_de]
|
tutorials = [setup_en, setup_de]
|
||||||
|
game_info_languages = ["en", "de"]
|
||||||
|
|
||||||
|
|
||||||
class CliqueWorld(World):
|
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
|
# 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"]:
|
if not options["multi_hit_breakables"]:
|
||||||
rom_data.write_byte(0x10C7A1, 0x03)
|
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
|
# Change the appearance of the Pot-Pourri to that of a larger PowerUp regardless of the above setting, so other
|
||||||
# game PermaUps are distinguishable.
|
# game PermaUps are distinguishable.
|
||||||
rom_data.write_int32s(0xEE558, [0x06005F08, 0x3FB00000, 0xFFFFFF00])
|
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(0x10CF38, 0x8000FF4D) # CT final room door slab
|
||||||
rom_data.write_int32(0x10CF44, 0x1000FF4D) # CT Renon slab
|
rom_data.write_int32(0x10CF44, 0x1000FF4D) # CT Renon slab
|
||||||
rom_data.write_int32(0x10C908, 0x0008FF4D) # Villa foyer chandelier
|
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
|
# Once-per-frame gameplay checks
|
||||||
rom_data.write_int32(0x6C848, 0x080FF40D) # J 0x803FD034
|
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,
|
"multi_hit_breakables": world.options.multi_hit_breakables.value,
|
||||||
"drop_previous_sub_weapon": world.options.drop_previous_sub_weapon.value,
|
"drop_previous_sub_weapon": world.options.drop_previous_sub_weapon.value,
|
||||||
"countdown": world.options.countdown.value,
|
"countdown": world.options.countdown.value,
|
||||||
|
"lizard_locker_items": world.options.lizard_locker_items.value,
|
||||||
"shopsanity": world.options.shopsanity.value,
|
"shopsanity": world.options.shopsanity.value,
|
||||||
"panther_dash": world.options.panther_dash.value,
|
"panther_dash": world.options.panther_dash.value,
|
||||||
"big_toss": world.options.big_toss.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, \
|
from .aesthetics import shuffle_sub_weapons, get_location_data, get_countdown_flags, populate_enemy_drops, \
|
||||||
get_start_inventory_data
|
get_start_inventory_data
|
||||||
from .rom import RomData, patch_rom, get_base_rom_path, CVCotMProcedurePatch, CVCOTM_CT_US_HASH, CVCOTM_AC_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
|
# CVCOTM_VC_US_HASH
|
||||||
from .client import CastlevaniaCotMClient
|
from .client import CastlevaniaCotMClient
|
||||||
|
|
||||||
|
|
||||||
@@ -29,7 +29,8 @@ class CVCotMSettings(settings.Group):
|
|||||||
"""File name of the Castlevania CotM US rom"""
|
"""File name of the Castlevania CotM US rom"""
|
||||||
copy_to = "Castlevania - Circle of the Moon (USA).gba"
|
copy_to = "Castlevania - Circle of the Moon (USA).gba"
|
||||||
description = "Castlevania CotM (US) ROM File"
|
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)
|
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 .locations import BASE_ID, get_location_names_to_ids
|
||||||
from .items import cvcotm_item_info, MAJORS_CLASSIFICATIONS
|
from .items import cvcotm_item_info, MAJORS_CLASSIFICATIONS
|
||||||
from .locations import cvcotm_location_info
|
from .locations import cvcotm_location_info
|
||||||
@@ -91,6 +91,7 @@ class CastlevaniaCotMClient(BizHawkClient):
|
|||||||
patch_suffix = ".apcvcotm"
|
patch_suffix = ".apcvcotm"
|
||||||
sent_initial_packets: bool
|
sent_initial_packets: bool
|
||||||
self_induced_death: bool
|
self_induced_death: bool
|
||||||
|
time_of_sent_death: Optional[float]
|
||||||
local_checked_locations: Set[int]
|
local_checked_locations: Set[int]
|
||||||
client_set_events = {flag_name: False for flag, flag_name in EVENT_FLAG_MAP.items()}
|
client_set_events = {flag_name: False for flag, flag_name in EVENT_FLAG_MAP.items()}
|
||||||
killed_dracula_2: bool
|
killed_dracula_2: bool
|
||||||
@@ -139,6 +140,7 @@ class CastlevaniaCotMClient(BizHawkClient):
|
|||||||
self.sent_initial_packets = False
|
self.sent_initial_packets = False
|
||||||
self.local_checked_locations = set()
|
self.local_checked_locations = set()
|
||||||
self.self_induced_death = False
|
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.client_set_events = {flag_name: False for flag, flag_name in EVENT_FLAG_MAP.items()}
|
||||||
self.killed_dracula_2 = False
|
self.killed_dracula_2 = False
|
||||||
self.won_battle_arena = False
|
self.won_battle_arena = False
|
||||||
@@ -156,14 +158,16 @@ class CastlevaniaCotMClient(BizHawkClient):
|
|||||||
return
|
return
|
||||||
if ctx.slot is None:
|
if ctx.slot is None:
|
||||||
return
|
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"]:
|
if "cause" in args["data"]:
|
||||||
cause = args["data"]["cause"]
|
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 == "":
|
if cause == "":
|
||||||
cause = f"{args['data']['source']} killed you without a word!"
|
cause = f"{args['data']['source']} killed you without a word!"
|
||||||
if len(cause) > ITEM_NAME_LIMIT + PLAYER_NAME_LIMIT:
|
if len(cause) > ITEM_NAME_LIMIT + PLAYER_NAME_LIMIT:
|
||||||
cause = cause[:ITEM_NAME_LIMIT + PLAYER_NAME_LIMIT]
|
cause = cause[:ITEM_NAME_LIMIT + PLAYER_NAME_LIMIT]
|
||||||
else:
|
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!"
|
cause = f"{args['data']['source']} killed you without a word!"
|
||||||
|
|
||||||
# Highlight the player that killed us in the game's orange text.
|
# Highlight the player that killed us in the game's orange text.
|
||||||
@@ -259,8 +263,13 @@ class CastlevaniaCotMClient(BizHawkClient):
|
|||||||
else:
|
else:
|
||||||
area_of_death = DEATHLINK_AREA_NAMES[area]
|
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!")
|
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
|
# 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.
|
# player is running the Battle Arena and Dracula goal.
|
||||||
if f"castlevania_cotm_events_{ctx.team}_{ctx.slot}" in ctx.stored_data:
|
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
|
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.
|
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.
|
The Wii U Virtual Console version does not work due to changes in the code in that version.
|
||||||
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 is currently untested. If you happen to have purchased it before the Wii U eShop shut down, you can try
|
Due to the reasons mentioned above, it is most recommended to obtain the ROM by dumping it from an original cartridge of the
|
||||||
dumping and playing with it. However, at the moment, we cannot guarantee that it will work well due to it being untested.
|
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.
|
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).
|
- [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.
|
- 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.
|
- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) 2.7 or later.
|
||||||
|
|
||||||
### Configuring BizHawk
|
### Configuring BizHawk
|
||||||
|
|||||||
@@ -22,11 +22,9 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
CVCOTM_CT_US_HASH = "50a1089600603a94e15ecf287f8d5a1f" # Original GBA cartridge ROM
|
CVCOTM_CT_US_HASH = "50a1089600603a94e15ecf287f8d5a1f" # Original GBA cartridge ROM
|
||||||
CVCOTM_AC_US_HASH = "87a1bd6577b6702f97a60fc55772ad74" # Castlevania Advance Collection 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
|
# The Wii U VC version is not currently supported. See the Game Page for more info.
|
||||||
# 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.
|
|
||||||
|
|
||||||
ARCHIPELAGO_IDENTIFIER_START = 0x7FFF00
|
ARCHIPELAGO_IDENTIFIER_START = 0x7FFF00
|
||||||
ARCHIPELAGO_IDENTIFIER = "ARCHIPELAG03"
|
ARCHIPELAGO_IDENTIFIER = "ARCHIPELAG03"
|
||||||
@@ -518,7 +516,8 @@ class CVCotMPatchExtensions(APPatchExtension):
|
|||||||
|
|
||||||
|
|
||||||
class CVCotMProcedurePatch(APProcedurePatch, APTokenMixin):
|
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"
|
patch_file_ending: str = ".apcvcotm"
|
||||||
result_file_ending: str = ".gba"
|
result_file_ending: str = ".gba"
|
||||||
|
|
||||||
@@ -585,7 +584,8 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
|
|||||||
|
|
||||||
basemd5 = hashlib.md5()
|
basemd5 = hashlib.md5()
|
||||||
basemd5.update(base_rom_bytes)
|
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."
|
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.")
|
"Get the correct game and version, then dump it.")
|
||||||
setattr(get_base_rom_bytes, "base_rom_bytes", base_rom_bytes)
|
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