mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-15 20:13:48 -07:00
Compare commits
35 Commits
0.6.5-rc1
...
NewSoupVi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9657f92932 | ||
|
|
35e667791f | ||
|
|
ce7c54ef9d | ||
|
|
ac84b272c5 | ||
|
|
e8a63abfa4 | ||
|
|
3fa2745c37 | ||
|
|
775065715d | ||
|
|
4e608b13ae | ||
|
|
886cc68051 | ||
|
|
146a314d22 | ||
|
|
18cf1bce36 | ||
|
|
f7e3f4e589 | ||
|
|
9f9765b78d | ||
|
|
8ae1a7da32 | ||
|
|
08ea3fe225 | ||
|
|
b81be6b4fc | ||
|
|
f1aca0fc46 | ||
|
|
d88fe99780 | ||
|
|
360a1384f2 | ||
|
|
d089b00ad5 | ||
|
|
c05a2adc38 | ||
|
|
7631242621 | ||
|
|
df48c3e718 | ||
|
|
9a755e64b2 | ||
|
|
34d362a003 | ||
|
|
b75cce5d41 | ||
|
|
a07faca2d9 | ||
|
|
8a1a715dc4 | ||
|
|
60a192b1b6 | ||
|
|
3b721e0365 | ||
|
|
3e16c20fce | ||
|
|
ec2c39e82f | ||
|
|
23d319247f | ||
|
|
c2c488410f | ||
|
|
8ea49e76db |
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
@@ -24,10 +24,10 @@ env:
|
||||
# NOTE: since appimage/appimagetool and appimage/type2-runtime does not have tags anymore,
|
||||
# we check the sha256 and require manual intervention if it was updated.
|
||||
APPIMAGE_FORK: 'PopTracker'
|
||||
APPIMAGETOOL_VERSION: 'r-2025-10-19'
|
||||
APPIMAGETOOL_X86_64_HASH: '9493a6b253a01f84acb9c624c38810ecfa11d99daa829b952b0bff43113080f9'
|
||||
APPIMAGE_RUNTIME_VERSION: 'r-2025-08-11'
|
||||
APPIMAGE_RUNTIME_X86_64_HASH: 'e70ffa9b69b211574d0917adc482dd66f25a0083427b5945783965d55b0b0a8b'
|
||||
APPIMAGETOOL_VERSION: 'r-2025-11-18'
|
||||
APPIMAGETOOL_X86_64_HASH: '4577a452b30af2337123fbb383aea154b618e51ad5448c3b62085cbbbfbfd9a2'
|
||||
APPIMAGE_RUNTIME_VERSION: 'r-2025-11-07'
|
||||
APPIMAGE_RUNTIME_X86_64_HASH: '27ddd3f78e483fc5f7856e413d7c17092917f8c35bfe3318a0d378aa9435ad17'
|
||||
|
||||
permissions: # permissions required for attestation
|
||||
id-token: 'write'
|
||||
|
||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -12,10 +12,10 @@ env:
|
||||
# NOTE: since appimage/appimagetool and appimage/type2-runtime does not have tags anymore,
|
||||
# we check the sha256 and require manual intervention if it was updated.
|
||||
APPIMAGE_FORK: 'PopTracker'
|
||||
APPIMAGETOOL_VERSION: 'r-2025-10-19'
|
||||
APPIMAGETOOL_X86_64_HASH: '9493a6b253a01f84acb9c624c38810ecfa11d99daa829b952b0bff43113080f9'
|
||||
APPIMAGE_RUNTIME_VERSION: 'r-2025-08-11'
|
||||
APPIMAGE_RUNTIME_X86_64_HASH: 'e70ffa9b69b211574d0917adc482dd66f25a0083427b5945783965d55b0b0a8b'
|
||||
APPIMAGETOOL_VERSION: 'r-2025-11-18'
|
||||
APPIMAGETOOL_X86_64_HASH: '4577a452b30af2337123fbb383aea154b618e51ad5448c3b62085cbbbfbfd9a2'
|
||||
APPIMAGE_RUNTIME_VERSION: 'r-2025-11-07'
|
||||
APPIMAGE_RUNTIME_X86_64_HASH: '27ddd3f78e483fc5f7856e413d7c17092917f8c35bfe3318a0d378aa9435ad17'
|
||||
|
||||
permissions: # permissions required for attestation
|
||||
id-token: 'write'
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
<option name="IS_MODULE_SDK" value="true" />
|
||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||
<option name="SCRIPT_NAME" value="$ContentRoot$/Launcher.py" />
|
||||
<option name="PARAMETERS" value="\"Build APWorlds\"" />
|
||||
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/Launcher.py" />
|
||||
<option name="PARAMETERS" value=""Build APWorlds"" />
|
||||
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||
<option name="EMULATE_TERMINAL" value="false" />
|
||||
<option name="MODULE_MODE" value="false" />
|
||||
|
||||
@@ -323,7 +323,7 @@ class CommonContext:
|
||||
hint_cost: int | None
|
||||
"""Current Hint Cost per Hint from the server"""
|
||||
hint_points: int | None
|
||||
"""Current avaliable Hint Points from the server"""
|
||||
"""Current available Hint Points from the server"""
|
||||
player_names: dict[int, str]
|
||||
"""Current lookup of slot number to player display name from server (includes aliases)"""
|
||||
|
||||
|
||||
@@ -347,7 +347,9 @@ def update_weights(weights: dict, new_weights: dict, update_type: str, name: str
|
||||
elif isinstance(new_value, list):
|
||||
cleaned_value.extend(new_value)
|
||||
elif isinstance(new_value, dict):
|
||||
cleaned_value = dict(Counter(cleaned_value) + Counter(new_value))
|
||||
counter_value = Counter(cleaned_value)
|
||||
counter_value.update(new_value)
|
||||
cleaned_value = dict(counter_value)
|
||||
else:
|
||||
raise Exception(f"Cannot apply merge to non-dict, set, or list type {option_name},"
|
||||
f" received {type(new_value).__name__}.")
|
||||
@@ -361,7 +363,9 @@ def update_weights(weights: dict, new_weights: dict, update_type: str, name: str
|
||||
for element in new_value:
|
||||
cleaned_value.remove(element)
|
||||
elif isinstance(new_value, dict):
|
||||
cleaned_value = dict(Counter(cleaned_value) - Counter(new_value))
|
||||
counter_value = Counter(cleaned_value)
|
||||
counter_value.subtract(new_value)
|
||||
cleaned_value = dict(counter_value)
|
||||
else:
|
||||
raise Exception(f"Cannot apply remove to non-dict, set, or list type {option_name},"
|
||||
f" received {type(new_value).__name__}.")
|
||||
|
||||
@@ -493,7 +493,7 @@ class Context:
|
||||
self.read_data["race_mode"] = lambda: decoded_obj.get("race_mode", 0)
|
||||
mdata_ver = decoded_obj["minimum_versions"]["server"]
|
||||
if mdata_ver > version_tuple:
|
||||
raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver},"
|
||||
raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver}, "
|
||||
f"however this server is of version {version_tuple}")
|
||||
self.generator_version = Version(*decoded_obj["version"])
|
||||
clients_ver = decoded_obj["minimum_versions"].get("clients", {})
|
||||
|
||||
@@ -1545,6 +1545,7 @@ class PlandoItems(Option[typing.List[PlandoItem]]):
|
||||
default = ()
|
||||
supports_weighting = False
|
||||
display_name = "Plando Items"
|
||||
visibility = Visibility.template | Visibility.spoiler
|
||||
|
||||
def __init__(self, value: typing.Iterable[PlandoItem]) -> None:
|
||||
self.value = list(deepcopy(value))
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
if __name__ == "__main__":
|
||||
import ModuleUpdate
|
||||
|
||||
ModuleUpdate.update()
|
||||
|
||||
|
||||
from kvui import (ThemedApp, ScrollBox, MainLayout, ContainerLayout, dp, Widget, MDBoxLayout, TooltipLabel, MDLabel,
|
||||
ToggleButton, MarkupDropdown, ResizableTextField)
|
||||
from kivy.uix.behaviors.button import ButtonBehavior
|
||||
@@ -330,6 +336,11 @@ class OptionsCreator(ThemedApp):
|
||||
box.range.slider.dropdown.open()
|
||||
|
||||
box = VisualNamedRange(option=option, name=name, range_widget=self.create_range(option, name))
|
||||
if option.default in option.special_range_names:
|
||||
# value can get mismatched in this case
|
||||
box.range.slider.value = min(max(option.special_range_names[option.default], option.range_start),
|
||||
option.range_end)
|
||||
box.range.tag.text = str(int(box.range.slider.value))
|
||||
box.range.slider.bind(on_touch_move=lambda _, _2: set_to_custom(box))
|
||||
items = [
|
||||
{
|
||||
@@ -365,7 +376,7 @@ class OptionsCreator(ThemedApp):
|
||||
# for some reason this fixes an issue causing some to not open
|
||||
dropdown.open()
|
||||
|
||||
default_random = option.default == "random"
|
||||
default_string = isinstance(option.default, str)
|
||||
main_button = VisualChoice(option=option, name=name)
|
||||
main_button.bind(on_release=open_dropdown)
|
||||
|
||||
@@ -377,7 +388,7 @@ class OptionsCreator(ThemedApp):
|
||||
for choice in option.name_lookup
|
||||
]
|
||||
dropdown = MDDropdownMenu(caller=main_button, items=items)
|
||||
self.options[name] = option.name_lookup[option.default] if not default_random else option.default
|
||||
self.options[name] = option.name_lookup[option.default] if not default_string else option.default
|
||||
return main_button
|
||||
|
||||
def create_text_choice(self, option: typing.Type[TextChoice], name: str):
|
||||
@@ -560,8 +571,11 @@ class OptionsCreator(ThemedApp):
|
||||
groups[group].append((name, option))
|
||||
|
||||
for group, options in groups.items():
|
||||
options = [(name, option) for name, option in options
|
||||
if name and option.visibility & Visibility.simple_ui]
|
||||
if not options:
|
||||
continue # Game Options can be empty if every other option is in another group
|
||||
# Can also have an option group of options that should not render on simple ui
|
||||
group_item = MDExpansionPanel(size_hint_y=None)
|
||||
group_header = MDExpansionPanelHeader(MDListItem(MDListItemSupportingText(text=group),
|
||||
TrailingPressedIconButton(icon="chevron-right",
|
||||
@@ -583,8 +597,7 @@ class OptionsCreator(ThemedApp):
|
||||
group_box.layout.orientation = "vertical"
|
||||
group_box.layout.spacing = dp(3)
|
||||
for name, option in options:
|
||||
if name and option is not Removed and option.visibility & Visibility.simple_ui:
|
||||
group_content.add_widget(self.create_option(option, name, cls))
|
||||
group_content.add_widget(self.create_option(option, name, cls))
|
||||
expansion_box.layout.add_widget(group_item)
|
||||
self.option_layout.add_widget(expansion_box)
|
||||
self.game_label.text = f"Game: {self.current_game}"
|
||||
|
||||
@@ -959,7 +959,7 @@ if "Timespinner" in network_data_package["games"]:
|
||||
|
||||
timespinner_location_ids = {
|
||||
"Present": list(range(1337000, 1337085)),
|
||||
"Past": list(range(1337086, 1337175)),
|
||||
"Past": list(range(1337086, 1337157)) + list(range(1337159, 1337175)),
|
||||
"Ancient Pyramid": [
|
||||
1337236,
|
||||
1337246, 1337247, 1337248, 1337249]
|
||||
|
||||
@@ -289,7 +289,7 @@ async def nes_sync_task(ctx: ZeldaContext):
|
||||
if not ctx.auth:
|
||||
ctx.auth = ''.join([chr(i) for i in data_decoded['playerName'] if i != 0])
|
||||
if ctx.auth == '':
|
||||
logger.info("Invalid ROM detected. No player name built into the ROM. Please regenerate"
|
||||
logger.info("Invalid ROM detected. No player name built into the ROM. Please regenerate "
|
||||
"the ROM using the same link but adding your slot name")
|
||||
if ctx.awaiting_rom:
|
||||
await ctx.server_auth(False)
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
tag: tag
|
||||
MDLabel:
|
||||
id: tag
|
||||
text: str(this.option.default) if this.option.default != "random" else this.option.range_start
|
||||
text: str(this.option.default) if not isinstance(this.option.default, str) else str(this.option.range_start)
|
||||
MDSlider:
|
||||
id: slider
|
||||
min: this.option.range_start
|
||||
max: this.option.range_end
|
||||
value: min(max(this.option.default, this.option.range_start), this.option.range_end) if this.option.default != "random" else this.option.range_start
|
||||
value: min(max(this.option.default, this.option.range_start), this.option.range_end) if not isinstance(this.option.default, str) else this.option.range_start
|
||||
step: 1
|
||||
step_point_size: 0
|
||||
MDSliderHandle:
|
||||
@@ -23,7 +23,7 @@
|
||||
text: text
|
||||
MDButtonText:
|
||||
id: text
|
||||
text: this.option.get_option_name(this.option.default if this.option.default != "random" else list(this.option.options.values())[0])
|
||||
text: this.option.get_option_name(this.option.default if not isinstance(this.option.default, str) else list(this.option.options.values())[0])
|
||||
theme_text_color: "Primary"
|
||||
|
||||
<VisualNamedRange>:
|
||||
@@ -38,7 +38,7 @@
|
||||
text: text
|
||||
MDButtonText:
|
||||
id: text
|
||||
text: this.option.special_range_names.get(list(this.option.special_range_names.values()).index(this.option.default)) if this.option.default in this.option.special_range_names else "Custom"
|
||||
text: this.option.default.title() if this.option.default in this.option.special_range_names else "Custom"
|
||||
|
||||
<VisualFreeText>:
|
||||
multiline: False
|
||||
|
||||
@@ -44,9 +44,9 @@ These get automatically added to the `archipelago.json` of an .apworld if it is
|
||||
["Build apworlds" launcher component](#build-apworlds-launcher-component),
|
||||
which is the correct way to package your `.apworld` as a world developer. Do not write these fields yourself.
|
||||
|
||||
### "Build apworlds" Launcher Component
|
||||
### "Build APWorlds" Launcher Component
|
||||
|
||||
In the Archipelago Launcher, there is a "Build apworlds" component that will package all world folders to `.apworld`,
|
||||
In the Archipelago Launcher, there is a "Build APWorlds" component that will package all world folders to `.apworld`,
|
||||
and add `archipelago.json` manifest files to them.
|
||||
These .apworld files will be output to `build/apworlds` (relative to the Archipelago root directory).
|
||||
The `archipelago.json` file in each .apworld will automatically include the appropriate
|
||||
|
||||
@@ -269,7 +269,8 @@ placed on them.
|
||||
|
||||
### PriorityLocations
|
||||
Marks locations given here as `LocationProgressType.Priority` forcing progression items on them if any are available in
|
||||
the pool.
|
||||
the pool. Progression items without a deprioritized flag will be used first when filling priority_locations. Progression items with
|
||||
a deprioritized flag will be used next.
|
||||
|
||||
### ItemLinks
|
||||
Allows users to share their item pool with other players. Currently item links are per game. A link of one game between
|
||||
|
||||
@@ -21,6 +21,29 @@ Unless these are shared between multiple people, we expect the following from ea
|
||||
of development.
|
||||
* Let us know of long periods of unavailability.
|
||||
|
||||
## Authority
|
||||
|
||||
For a Pull Request into a world to be merged, one of the world maintainers of that world has to approve it.
|
||||
This applies to all Pull Requests, no matter how small, with the sole exception of patching security vulnerabilities.
|
||||
|
||||
World maintainers can partially opt out of this,
|
||||
allowing core maintainers to merge pull requests which they deem critical and "obvious" enough.
|
||||
There is no one singular definition of what Pull Requests fit this criteria -
|
||||
You are trusting the core maintainers of Archipelago to be reasonable about their judgement.
|
||||
|
||||
Some examples of Pull Requests like this include:
|
||||
- Fixing a broken link in documentation
|
||||
- Correcting a typo
|
||||
- Fixing a crash where the intent of the code is obvious (e.g. an indentation error due to typing 3 spaces instead of 4)
|
||||
|
||||
To do this, they can add a comment in [CODEOWNERS](./CODEOWNERS) under their game:
|
||||
|
||||
```
|
||||
# APQuest
|
||||
# Core is allowed to merge some types of PRs without my approval as described in "world maintainer.md"
|
||||
/worlds/apquest/ @NewSoupVi
|
||||
```
|
||||
|
||||
## Becoming a World Maintainer
|
||||
|
||||
### Adding a World
|
||||
|
||||
@@ -525,7 +525,7 @@ def randomize_entrances(
|
||||
|
||||
running_time = time.perf_counter() - start_time
|
||||
if running_time > 1.0:
|
||||
logging.info(f"Took {running_time:.4f} seconds during entrance randomization for player {world.player},"
|
||||
logging.info(f"Took {running_time:.4f} seconds during entrance randomization for player {world.player}, "
|
||||
f"named {world.multiworld.player_name[world.player]}")
|
||||
|
||||
return er_state
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
colorama>=0.4.6
|
||||
websockets>=13.0.1,<14
|
||||
PyYAML>=6.0.2
|
||||
jellyfish>=1.1.3
|
||||
PyYAML>=6.0.3
|
||||
jellyfish>=1.2.1
|
||||
jinja2>=3.1.6
|
||||
schema>=0.7.7
|
||||
schema>=0.7.8
|
||||
kivy>=2.3.1
|
||||
bsdiff4>=1.2.6
|
||||
platformdirs>=4.3.6
|
||||
certifi>=2025.4.26
|
||||
cython>=3.0.12
|
||||
cymem>=2.0.11
|
||||
orjson>=3.10.15
|
||||
typing_extensions>=4.12.2
|
||||
pyshortcuts>=1.9.1
|
||||
platformdirs>=4.5.0
|
||||
certifi>=2025.11.12
|
||||
cython>=3.2.1
|
||||
cymem>=2.0.13
|
||||
orjson>=3.11.4
|
||||
typing_extensions>=4.15.0
|
||||
pyshortcuts>=1.9.6
|
||||
kivymd @ git+https://github.com/kivymd/KivyMD@5ff9d0d
|
||||
kivymd>=2.0.1.dev0
|
||||
|
||||
4
setup.py
4
setup.py
@@ -394,11 +394,11 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
|
||||
manifest = json.load(manifest_file)
|
||||
|
||||
assert "game" in manifest, (
|
||||
f"World directory {world_directory} has an archipelago.json manifest file, but it"
|
||||
f"World directory {world_directory} has an archipelago.json manifest file, but it "
|
||||
"does not define a \"game\"."
|
||||
)
|
||||
assert manifest["game"] == worldtype.game, (
|
||||
f"World directory {world_directory} has an archipelago.json manifest file, but value of the"
|
||||
f"World directory {world_directory} has an archipelago.json manifest file, but value of the "
|
||||
f"\"game\" field ({manifest['game']} does not equal the World class's game ({worldtype.game})."
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -37,3 +37,23 @@ class TestPlayerOptions(unittest.TestCase):
|
||||
self.assertEqual(new_weights["dict_2"]["option_g"], 50)
|
||||
self.assertEqual(len(new_weights["set_1"]), 2)
|
||||
self.assertIn("option_d", new_weights["set_1"])
|
||||
|
||||
def test_update_dict_supports_negatives_and_zeroes(self):
|
||||
original_options = {
|
||||
"dict_1": {"a": 1, "b": -1},
|
||||
"dict_2": {"a": 1, "b": -1},
|
||||
}
|
||||
new_weights = Generate.update_weights(
|
||||
original_options,
|
||||
{
|
||||
"+dict_1": {"a": -2, "b": 2},
|
||||
"-dict_2": {"a": 1, "b": 2},
|
||||
},
|
||||
"Tested",
|
||||
"",
|
||||
)
|
||||
self.assertEqual(new_weights["dict_1"]["a"], -1)
|
||||
self.assertEqual(new_weights["dict_1"]["b"], 1)
|
||||
self.assertEqual(new_weights["dict_2"]["a"], 0)
|
||||
self.assertEqual(new_weights["dict_2"]["b"], -3)
|
||||
self.assertIn("a", new_weights["dict_2"])
|
||||
|
||||
@@ -70,13 +70,13 @@ if __name__ == "__main__":
|
||||
empty_file = str(Path(tempdir) / "empty")
|
||||
open(empty_file, "w").close()
|
||||
sys.argv += ["--config_override", empty_file] # tests #5541
|
||||
multis = [["VVVVVV"], ["Temp World"], ["VVVVVV", "Temp World"]]
|
||||
multis = [["APQuest"], ["Temp World"], ["APQuest", "Temp World"]]
|
||||
p1_games: list[str] = []
|
||||
data_paths: list[Path | None] = []
|
||||
rooms: list[str] = []
|
||||
multidata: Path | None
|
||||
|
||||
copy_world("VVVVVV", "Temp World")
|
||||
copy_world("APQuest", "Temp World")
|
||||
try:
|
||||
for n, games in enumerate(multis, 1):
|
||||
print(f"Generating [{n}] {', '.join(games)} offline")
|
||||
|
||||
@@ -20,7 +20,7 @@ def copy(src: str, dst: str) -> None:
|
||||
src_cls = AutoWorldRegister.world_types[src]
|
||||
src_folder = Path(src_cls.__file__).parent
|
||||
worlds_folder = src_folder.parent
|
||||
if (not src_cls.__file__.endswith("__init__.py") or not src_folder.is_dir()
|
||||
if (not src_cls.__file__.endswith(("__init__.py", "world.py")) or not src_folder.is_dir()
|
||||
or not (worlds_folder / "generic").is_dir()):
|
||||
raise ValueError(f"Unsupported layout for copy_world from {src}")
|
||||
dst_folder = worlds_folder / dst_folder_name
|
||||
@@ -28,11 +28,14 @@ def copy(src: str, dst: str) -> None:
|
||||
raise ValueError(f"Destination {dst_folder} already exists")
|
||||
shutil.copytree(src_folder, dst_folder)
|
||||
_new_worlds[dst] = str(dst_folder)
|
||||
with open(dst_folder / "__init__.py", "r", encoding="utf-8-sig") as f:
|
||||
contents = f.read()
|
||||
contents = re.sub(r'game\s*(:\s*[a-zA-Z\[\]]+)?\s*=\s*[\'"]' + re.escape(src) + r'[\'"]', f'game = "{dst}"', contents)
|
||||
with open(dst_folder / "__init__.py", "w", encoding="utf-8") as f:
|
||||
f.write(contents)
|
||||
|
||||
for potential_world_class_file in ("__init__.py", "world.py"):
|
||||
with open(dst_folder / potential_world_class_file, "r", encoding="utf-8-sig") as f:
|
||||
contents = f.read()
|
||||
r_src = re.escape(src)
|
||||
contents = re.sub(r'game\s*(:\s*[a-zA-Z\[\]]+)?\s*=\s*[\'"]' + r_src + r'[\'"]', f'game = "{dst}"', contents)
|
||||
with open(dst_folder / "__init__.py", "w", encoding="utf-8") as f:
|
||||
f.write(contents)
|
||||
|
||||
|
||||
def delete(name: str) -> None:
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
|
||||
from __future__ import annotations
|
||||
import abc
|
||||
from bisect import bisect_right
|
||||
from dataclasses import dataclass
|
||||
import enum
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, ClassVar, Dict, Iterable, Tuple, Any, Optional, Union, TypeGuard
|
||||
from typing import (TYPE_CHECKING, Any, ClassVar, Dict, Generic, Iterable,
|
||||
Optional, Sequence, Tuple, TypeGuard, TypeVar, Union)
|
||||
|
||||
|
||||
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from SNIClient import SNIContext
|
||||
|
||||
SNES_READ_CHUNK_SIZE = 2048
|
||||
"""
|
||||
note: SNI v0.0.101 currently has a bug where reads from
|
||||
RetroArch >2048 bytes will only return the last ~2048 bytes read.
|
||||
https://github.com/alttpo/sni/issues/51
|
||||
"""
|
||||
|
||||
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)
|
||||
@@ -91,3 +103,119 @@ class SNIClient(abc.ABC, metaclass=AutoSNIClientRegister):
|
||||
def on_package(self, ctx: SNIContext, cmd: str, args: Dict[str, Any]) -> None:
|
||||
""" override this with code to handle packages from the server """
|
||||
pass
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True, order=True)
|
||||
class Read:
|
||||
""" snes memory read - address and size in bytes """
|
||||
address: int
|
||||
size: int
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class _MemRead:
|
||||
location: Read
|
||||
data: bytes
|
||||
|
||||
|
||||
_T_Enum = TypeVar("_T_Enum", bound=enum.Enum)
|
||||
|
||||
|
||||
class SnesData(Generic[_T_Enum]):
|
||||
_ranges: Sequence[_MemRead]
|
||||
""" sorted by address """
|
||||
|
||||
def __init__(self, ranges: Sequence[tuple[Read, bytes]]) -> None:
|
||||
self._ranges = [_MemRead(r, d) for r, d in ranges]
|
||||
|
||||
def get(self, read: _T_Enum) -> bytes:
|
||||
assert isinstance(read.value, Read), read.value
|
||||
address = read.value.address
|
||||
index = bisect_right(self._ranges, address, key=lambda r: r.location.address) - 1
|
||||
assert index >= 0, (self._ranges, read.value)
|
||||
mem_read = self._ranges[index]
|
||||
sub_index = address - mem_read.location.address
|
||||
return mem_read.data[sub_index:sub_index + read.value.size]
|
||||
|
||||
|
||||
class SnesReader(Generic[_T_Enum]):
|
||||
"""
|
||||
how to use:
|
||||
```
|
||||
from enum import Enum
|
||||
from worlds.AutoSNIClient import Read, SNIClient, SnesReader
|
||||
|
||||
class MyGameMemory(Enum):
|
||||
game_mode = Read(WRAM_START + 0x0998, 1)
|
||||
send_queue = Read(SEND_QUEUE_START, 8 * 127)
|
||||
...
|
||||
|
||||
snes_reader = SnesReader(MyGameMemory)
|
||||
|
||||
snes_data = await snes_reader.read(ctx)
|
||||
if snes_data is None:
|
||||
snes_logger.info("error reading from snes")
|
||||
return
|
||||
|
||||
game_mode = snes_data.get(MyGameMemory.game_mode)
|
||||
```
|
||||
"""
|
||||
_ranges: Sequence[Read]
|
||||
""" sorted by address """
|
||||
|
||||
def __init__(self, reads: type[_T_Enum]) -> None:
|
||||
self._ranges = self._make_ranges(reads)
|
||||
|
||||
@staticmethod
|
||||
def _make_ranges(reads: type[enum.Enum]) -> Sequence[Read]:
|
||||
|
||||
unprocessed_reads: list[Read] = []
|
||||
for e in reads:
|
||||
assert isinstance(e.value, Read), (reads.__name__, e, e.value)
|
||||
unprocessed_reads.append(e.value)
|
||||
unprocessed_reads.sort()
|
||||
|
||||
ranges: list[Read] = []
|
||||
for read in unprocessed_reads:
|
||||
# v end of the previous range
|
||||
if len(ranges) == 0 or read.address - (ranges[-1].address + ranges[-1].size) > 255:
|
||||
ranges.append(read)
|
||||
else: # combine with previous range
|
||||
chunk_address = ranges[-1].address
|
||||
assert read.address >= chunk_address, "sort() didn't work? or something"
|
||||
original_chunk_size = ranges[-1].size
|
||||
new_size = max((read.address + read.size) - chunk_address,
|
||||
original_chunk_size)
|
||||
ranges[-1] = Read(chunk_address, new_size)
|
||||
logging.debug(f"{len(ranges)=} {max(r.size for r in ranges)=}")
|
||||
return ranges
|
||||
|
||||
async def read(self, ctx: "SNIContext") -> SnesData[_T_Enum] | None:
|
||||
"""
|
||||
returns `None` if reading fails,
|
||||
otherwise returns the data for the registered `Enum`
|
||||
"""
|
||||
from SNIClient import snes_read
|
||||
|
||||
reads: list[tuple[Read, bytes]] = []
|
||||
for r in self._ranges:
|
||||
if r.size < SNES_READ_CHUNK_SIZE: # most common
|
||||
response = await snes_read(ctx, r.address, r.size)
|
||||
if response is None:
|
||||
return None
|
||||
reads.append((r, response))
|
||||
else: # big read
|
||||
# Problems were reported with big reads,
|
||||
# so we chunk it into smaller pieces.
|
||||
read_so_far = 0
|
||||
collection: list[bytes] = []
|
||||
while read_so_far < r.size:
|
||||
remaining_size = r.size - read_so_far
|
||||
chunk_size = min(SNES_READ_CHUNK_SIZE, remaining_size)
|
||||
response = await snes_read(ctx, r.address + read_so_far, chunk_size)
|
||||
if response is None:
|
||||
return None
|
||||
collection.append(response)
|
||||
read_so_far += chunk_size
|
||||
reads.append((r, b"".join(collection)))
|
||||
return SnesData(reads)
|
||||
|
||||
@@ -291,11 +291,11 @@ if not is_frozen():
|
||||
manifest = json.load(manifest_file)
|
||||
|
||||
assert "game" in manifest, (
|
||||
f"World directory {world_directory} has an archipelago.json manifest file, but it"
|
||||
f"World directory {world_directory} has an archipelago.json manifest file, but it "
|
||||
"does not define a \"game\"."
|
||||
)
|
||||
assert manifest["game"] == worldtype.game, (
|
||||
f"World directory {world_directory} has an archipelago.json manifest file, but value of the"
|
||||
f"World directory {world_directory} has an archipelago.json manifest file, but value of the "
|
||||
f"\"game\" field ({manifest['game']} does not equal the World class's game ({worldtype.game})."
|
||||
)
|
||||
else:
|
||||
@@ -318,5 +318,5 @@ if not is_frozen():
|
||||
open_folder(apworlds_folder)
|
||||
|
||||
|
||||
components.append(Component('Build APWorlds', func=_build_apworlds, cli=True,
|
||||
components.append(Component("Build APWorlds", func=_build_apworlds, cli=True,
|
||||
description="Build APWorlds from loose-file world folders."))
|
||||
|
||||
@@ -263,7 +263,6 @@ def generate_itempool(world):
|
||||
('Frog', 'Get Frog'),
|
||||
('Missing Smith', 'Return Smith'),
|
||||
('Floodgate', 'Open Floodgate'),
|
||||
('Agahnim 1', 'Beat Agahnim 1'),
|
||||
('Flute Activation Spot', 'Activated Flute'),
|
||||
('Capacity Upgrade Shop', 'Capacity Upgrade Shop')
|
||||
]
|
||||
|
||||
@@ -183,7 +183,7 @@ def check_enemizer(enemizercli):
|
||||
if getattr(check_enemizer, "done", None):
|
||||
return
|
||||
if not os.path.exists(enemizercli) and not os.path.exists(enemizercli + ".exe"):
|
||||
raise Exception(f"Enemizer not found at {enemizercli}, please install it."
|
||||
raise Exception(f"Enemizer not found at {enemizercli}, please install it. "
|
||||
f"Such as https://github.com/Ijwu/Enemizer/releases")
|
||||
|
||||
with check_lock:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"game": "APQuest",
|
||||
"minimum_ap_version": "0.6.4",
|
||||
"world_version": "1.0.0",
|
||||
"world_version": "1.0.1",
|
||||
"authors": ["NewSoupVi"]
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from math import sqrt
|
||||
from random import choice, random
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import Any
|
||||
|
||||
from kivy.core.window import Keyboard, Window
|
||||
from kivy.graphics import Color, Triangle
|
||||
|
||||
@@ -2,9 +2,8 @@ import pkgutil
|
||||
from collections.abc import Buffer
|
||||
from enum import Enum
|
||||
from io import BytesIO
|
||||
from typing import Literal, NamedTuple, cast
|
||||
from typing import Literal, NamedTuple, Protocol, cast
|
||||
|
||||
from bokeh.protocol import Protocol
|
||||
from kivy.uix.image import CoreImage
|
||||
|
||||
from CommonClient import logger
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from collections import Counter
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from .events import Event, LocationClearedEvent, VictoryEvent
|
||||
from .gameboard import Gameboard
|
||||
|
||||
@@ -44,9 +44,10 @@ class CivVIContainer(APPlayerContainer):
|
||||
opened_zipfile.writestr(filename, yml)
|
||||
super().write_contents(opened_zipfile)
|
||||
|
||||
|
||||
def sanitize_value(value: str) -> str:
|
||||
"""Removes values that can cause issues in XML"""
|
||||
return value.replace('"', "'").replace('&', 'and')
|
||||
"""Removes values that can cause issues in XML"""
|
||||
return value.replace('"', "'").replace('&', 'and').replace('{', '').replace('}', '')
|
||||
|
||||
|
||||
def get_cost(world: 'CivVIWorld', location: CivVILocationData) -> int:
|
||||
@@ -87,8 +88,10 @@ def generate_new_items(world: 'CivVIWorld') -> str:
|
||||
boost_civics = []
|
||||
|
||||
if world.options.boostsanity:
|
||||
boost_techs = [location for location in locations if location.location_type == CivVICheckType.BOOST and location.name.split("_")[1] == "TECH"]
|
||||
boost_civics = [location for location in locations if location.location_type == CivVICheckType.BOOST and location.name.split("_")[1] == "CIVIC"]
|
||||
boost_techs = [location for location in locations if location.location_type ==
|
||||
CivVICheckType.BOOST and location.name.split("_")[1] == "TECH"]
|
||||
boost_civics = [location for location in locations if location.location_type ==
|
||||
CivVICheckType.BOOST and location.name.split("_")[1] == "CIVIC"]
|
||||
techs += boost_techs
|
||||
civics += boost_civics
|
||||
|
||||
|
||||
@@ -214,7 +214,7 @@ renon_item_dialogue = {
|
||||
"\"Banshee Boomerang.\"",
|
||||
0x10: "No weapon triangle\n"
|
||||
"advantages with this.",
|
||||
0x12: "It looks sus? Trust me,"
|
||||
0x12: "It looks sus? Trust me,\n"
|
||||
"my wares are genuine.",
|
||||
0x15: "This non-volatile kind\n"
|
||||
"is safe to handle.",
|
||||
|
||||
@@ -1030,7 +1030,7 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(base_rom_bytes)
|
||||
if CV64_US_10_HASH != basemd5.hexdigest():
|
||||
raise Exception("Supplied Base Rom does not match known MD5 for Castlevania 64 US 1.0."
|
||||
raise Exception("Supplied Base Rom does not match known MD5 for Castlevania 64 US 1.0. "
|
||||
"Get the correct game and version, then dump it.")
|
||||
setattr(get_base_rom_bytes, "base_rom_bytes", base_rom_bytes)
|
||||
return base_rom_bytes
|
||||
|
||||
@@ -247,6 +247,10 @@ class CastlevaniaCotMClient(BizHawkClient):
|
||||
await bizhawk.write(ctx.bizhawk_ctx, [(QUEUED_TEXTBOX_1_ADDRESS, [0 for _ in range(12)], "EWRAM")])
|
||||
return
|
||||
|
||||
# If the player doesn't have Dash Boots for whatever reason, put them in their inventory now.
|
||||
if not magic_items_array[0]:
|
||||
await bizhawk.write(ctx.bizhawk_ctx, [(MAGIC_ITEMS_ARRAY_START, [1], "EWRAM")])
|
||||
|
||||
# Enable DeathLink if it's in our slot_data.
|
||||
if "DeathLink" not in ctx.tags and ctx.slot_data["death_link"]:
|
||||
await ctx.update_death_link(True)
|
||||
|
||||
@@ -586,7 +586,7 @@ def get_base_rom_bytes(file_name: str = "") -> 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]:
|
||||
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.")
|
||||
setattr(get_base_rom_bytes, "base_rom_bytes", base_rom_bytes)
|
||||
return base_rom_bytes
|
||||
|
||||
@@ -90,6 +90,22 @@ class DarkSouls3World(World):
|
||||
self.created_regions = set()
|
||||
self.all_excluded_locations.update(self.options.exclude_locations.value)
|
||||
|
||||
# This code doesn't work because tests don't verify options
|
||||
# Don't consider disabled locations to be AP-excluded
|
||||
# if not self.options.enable_dlc:
|
||||
# self.options.exclude_locations.value = {
|
||||
# location
|
||||
# for location in self.options.exclude_locations
|
||||
# if not location_dictionary[location].dlc
|
||||
# }
|
||||
|
||||
# if not self.options.enable_ngp:
|
||||
# self.options.exclude_locations.value = {
|
||||
# location for
|
||||
# location in self.options.exclude_locations
|
||||
# if not location_dictionary[location].ngp
|
||||
# }
|
||||
|
||||
# Inform Universal Tracker where Yhorm is being randomized to.
|
||||
if hasattr(self.multiworld, "re_gen_passthrough"):
|
||||
if "Dark Souls III" in self.multiworld.re_gen_passthrough:
|
||||
@@ -264,6 +280,13 @@ class DarkSouls3World(World):
|
||||
):
|
||||
new_location.progress_type = LocationProgressType.EXCLUDED
|
||||
else:
|
||||
# Don't consider non-randomized locations to be AP-excluded
|
||||
if location.name in excluded:
|
||||
excluded.remove(location.name)
|
||||
# Only remove from all_excluded if excluded does not have priority over missable
|
||||
if not (self.options.missable_location_behavior < self.options.excluded_location_behavior):
|
||||
self.all_excluded_locations.remove(location.name)
|
||||
|
||||
# Don't allow missable duplicates of progression items to be expected progression.
|
||||
if location.name in self.missable_dupe_prog_locs: continue
|
||||
|
||||
@@ -283,11 +306,6 @@ class DarkSouls3World(World):
|
||||
parent = new_region,
|
||||
)
|
||||
new_location.place_locked_item(event_item)
|
||||
if location.name in excluded:
|
||||
excluded.remove(location.name)
|
||||
# Only remove from all_excluded if excluded does not have priority over missable
|
||||
if not (self.options.missable_location_behavior < self.options.excluded_location_behavior):
|
||||
self.all_excluded_locations.remove(location.name)
|
||||
|
||||
new_region.locations.append(new_location)
|
||||
|
||||
@@ -1357,7 +1375,7 @@ class DarkSouls3World(World):
|
||||
if self.yhorm_location != default_yhorm_location:
|
||||
text += f"\nYhorm takes the place of {self.yhorm_location.name} in {self.player_name}'s world\n"
|
||||
|
||||
if self.options.excluded_location_behavior == "allow_useful":
|
||||
if self.options.excluded_location_behavior != "forbid_useful":
|
||||
text += f"\n{self.player_name}'s world excluded: {sorted(self.all_excluded_locations)}\n"
|
||||
|
||||
if text:
|
||||
|
||||
@@ -584,7 +584,7 @@ def launch(*new_args: str):
|
||||
|
||||
# args handling
|
||||
parser = get_base_parser(description="Optional arguments to Factorio Client follow. "
|
||||
"Remaining arguments get passed into bound Factorio instance."
|
||||
"Remaining arguments get passed into bound Factorio instance. "
|
||||
"Refer to Factorio --help for those.")
|
||||
parser.add_argument('--rcon-port', default='24242', type=int, help='Port to use to communicate with Factorio')
|
||||
parser.add_argument('--rcon-password', help='Password to authenticate with RCON.')
|
||||
|
||||
@@ -155,6 +155,10 @@ def set_rules(self) -> None:
|
||||
return True
|
||||
check_foresta(loc.parent_region)
|
||||
|
||||
if self.options.map_shuffle or self.options.crest_shuffle:
|
||||
process_rules(self.multiworld.get_entrance("Subregion Frozen Fields to Subregion Aquaria", self.player),
|
||||
["SummerAquaria"])
|
||||
|
||||
if self.options.logic == "friendly":
|
||||
process_rules(self.multiworld.get_entrance("Overworld - Ice Pyramid", self.player),
|
||||
["MagicMirror"])
|
||||
|
||||
@@ -601,7 +601,7 @@ async def run_game(ctx: JakAndDaxterContext):
|
||||
f"Please check your host.yaml file.\n"
|
||||
f"If the value of 'jakanddaxter_options > auto_detect_root_directory' is true, verify that OpenGOAL "
|
||||
f"is installed properly.\n"
|
||||
f"If it is false, check the value of 'jakanddaxter_options > root_directory'."
|
||||
f"If it is false, check the value of 'jakanddaxter_options > root_directory'. "
|
||||
f"Verify it is a valid existing path, and all backslashes have been replaced with forward slashes.")
|
||||
ctx.on_log_error(logger, msg)
|
||||
return
|
||||
|
||||
@@ -7,7 +7,9 @@ class TradesCostNothingTest(JakAndDaxterTestBase):
|
||||
"global_orbsanity_bundle_size": 10,
|
||||
"citizen_orb_trade_amount": 0,
|
||||
"oracle_orb_trade_amount": 0,
|
||||
"start_inventory": {"Power Cell": 100},
|
||||
"fire_canyon_cell_count": 0,
|
||||
"mountain_pass_cell_count": 0,
|
||||
"lava_tube_cell_count": 0,
|
||||
}
|
||||
|
||||
def test_orb_items_are_filler(self):
|
||||
@@ -26,7 +28,9 @@ class TradesCostEverythingTest(JakAndDaxterTestBase):
|
||||
"global_orbsanity_bundle_size": 10,
|
||||
"citizen_orb_trade_amount": 120,
|
||||
"oracle_orb_trade_amount": 150,
|
||||
"start_inventory": {"Power Cell": 100},
|
||||
"fire_canyon_cell_count": 0,
|
||||
"mountain_pass_cell_count": 0,
|
||||
"lava_tube_cell_count": 0,
|
||||
}
|
||||
|
||||
def test_orb_items_are_progression(self):
|
||||
|
||||
@@ -30,7 +30,7 @@ class KH1Web(WebWorld):
|
||||
theme = "ocean"
|
||||
tutorials = [Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
"A guide to setting up the Kingdom Hearts Randomizer software on your computer."
|
||||
"A guide to setting up the Kingdom Hearts Randomizer software on your computer. "
|
||||
"This guide covers single-player, multiworld, and related software.",
|
||||
"English",
|
||||
"kh1_en.md",
|
||||
|
||||
@@ -195,21 +195,24 @@ class MagpieBridge:
|
||||
async def handler(self, websocket):
|
||||
self.ws = websocket
|
||||
while True:
|
||||
message = json.loads(await websocket.recv())
|
||||
if message["type"] == "handshake":
|
||||
logger.info(
|
||||
f"Connected, supported features: {message['features']}")
|
||||
self.features = message["features"]
|
||||
try:
|
||||
message = json.loads(await websocket.recv())
|
||||
if message["type"] == "handshake":
|
||||
logger.info(
|
||||
f"Connected, supported features: {message['features']}")
|
||||
self.features = message["features"]
|
||||
|
||||
await self.send_handshAck()
|
||||
await self.send_handshAck()
|
||||
|
||||
if message["type"] == "sendFull":
|
||||
if "items" in self.features:
|
||||
await self.send_all_inventory()
|
||||
if "checks" in self.features:
|
||||
await self.send_all_checks()
|
||||
if self.use_entrance_tracker():
|
||||
await self.send_gps(diff=False)
|
||||
if message["type"] == "sendFull":
|
||||
if "items" in self.features:
|
||||
await self.send_all_inventory()
|
||||
if "checks" in self.features:
|
||||
await self.send_all_checks()
|
||||
if self.use_entrance_tracker():
|
||||
await self.send_gps(diff=False)
|
||||
except websockets.exceptions.ConnectionClosedOK:
|
||||
pass
|
||||
|
||||
# Translate renamed IDs back to LADXR IDs
|
||||
@staticmethod
|
||||
|
||||
@@ -88,7 +88,7 @@ class MLSSClient(BizHawkClient):
|
||||
if seed not in ctx.seed_name:
|
||||
logger.info(
|
||||
"ERROR: The ROM you loaded is for a different game of AP. "
|
||||
"Please make sure the host has sent you the correct patch file,"
|
||||
"Please make sure the host has sent you the correct patch file, "
|
||||
"and that you have opened the correct ROM."
|
||||
)
|
||||
raise bizhawk.ConnectorError("Loaded ROM is for Incorrect lobby.")
|
||||
|
||||
@@ -775,7 +775,7 @@ item_descriptions = {
|
||||
item_names.BULLFROG_BROODLINGS: "Bullfrogs spawn two broodlings on impact, in addition to unloading their cargo.",
|
||||
item_names.BULLFROG_HARD_IMPACT: "Bullfrogs deal more damage and stun longer on impact.",
|
||||
item_names.INFESTED_BANSHEE_BRACED_EXOSKELETON: "Infested Banshees gain +100 life.",
|
||||
item_names.INFESTED_BANSHEE_RAPID_HIBERNATION: "Infested Banshees regenerate 20 life and energy per second while burrowed.",
|
||||
item_names.INFESTED_BANSHEE_RAPID_HIBERNATION: "Allows Infested Banshees to Burrow. Infested Banshees regenerate 20 life and energy per second while burrowed.",
|
||||
item_names.INFESTED_BANSHEE_FLESHFUSED_TARGETING_OPTICS: "Infested Banshees gain +2 range while cloaked.",
|
||||
item_names.INFESTED_LIBERATOR_CLOUD_DISPERSAL: "Infested Liberators instantly transform into a cloud of microscopic organisms while attacking, reducing the damage they take by 85%.",
|
||||
item_names.INFESTED_LIBERATOR_VIRAL_CONTAMINATION: "Increases the damage Infested Liberators deal to their primary target by 100%.",
|
||||
|
||||
@@ -111,6 +111,9 @@ class ItemGroupNames:
|
||||
TERRAN_ORIGINAL_PROGRESSIVE_UPGRADES = "Terran Original Progressive Upgrades"
|
||||
"""Progressive items where level 1 appeared in WoL"""
|
||||
MENGSK_UNITS = "Mengsk Units"
|
||||
TERRAN_SC1_UNITS = "Terran SC1 Units"
|
||||
TERRAN_SC1_BUILDINGS = "Terran SC1 Buildings"
|
||||
TERRAN_LADDER_UNITS = "Terran Ladder Units"
|
||||
TERRAN_VETERANCY_UNITS = "Terran Veterancy Units"
|
||||
ORBITAL_COMMAND_ABILITIES = "Orbital Command Abilities"
|
||||
WOL_ORBITAL_COMMAND_ABILITIES = "WoL Command Center Abilities"
|
||||
@@ -154,6 +157,8 @@ class ItemGroupNames:
|
||||
"""All items from Stukov co-op subfaction"""
|
||||
INF_TERRAN_UNITS = "Infested Terran Units"
|
||||
INF_TERRAN_UPGRADES = "Infested Terran Upgrades"
|
||||
ZERG_SC1_UNITS = "Zerg SC1 Units"
|
||||
ZERG_LADDER_UNITS = "Zerg Ladder Units"
|
||||
|
||||
PROTOSS_ITEMS = "Protoss Items"
|
||||
PROTOSS_UNITS = "Protoss Units"
|
||||
@@ -176,6 +181,9 @@ class ItemGroupNames:
|
||||
NERAZIM_UNITS = "Nerazim"
|
||||
TAL_DARIM_UNITS = "Tal'Darim"
|
||||
PURIFIER_UNITS = "Purifier"
|
||||
PROTOSS_SC1_UNITS = "Protoss SC1 Units"
|
||||
PROTOSS_SC1_BUILDINGS = "Protoss SC1 Buildings"
|
||||
PROTOSS_LADDER_UNITS = "Protoss Ladder Units"
|
||||
|
||||
VANILLA_ITEMS = "Vanilla Items"
|
||||
OVERPOWERED_ITEMS = "Overpowered Items"
|
||||
@@ -288,8 +296,14 @@ item_name_groups[ItemGroupNames.WOL_BUILDINGS] = wol_buildings = [
|
||||
item_names.HIVE_MIND_EMULATOR, item_names.PSI_DISRUPTER,
|
||||
]
|
||||
item_name_groups[ItemGroupNames.TERRAN_BUILDINGS] = terran_buildings = [
|
||||
item_name for item_name, item_data in item_tables.item_table.items()
|
||||
if item_data.type == item_tables.TerranItemType.Building or item_name in wol_buildings
|
||||
*[
|
||||
item_name for item_name, item_data in item_tables.item_table.items()
|
||||
if item_data.type == item_tables.TerranItemType.Building or item_name in wol_buildings
|
||||
],
|
||||
item_names.PSI_SCREEN,
|
||||
item_names.SONIC_DISRUPTER,
|
||||
item_names.PSI_INDOCTRINATOR,
|
||||
item_names.ARGUS_AMPLIFIER,
|
||||
]
|
||||
item_name_groups[ItemGroupNames.MENGSK_UNITS] = [
|
||||
item_names.AEGIS_GUARD, item_names.EMPERORS_SHADOW,
|
||||
@@ -317,6 +331,41 @@ spider_mine_sources = [
|
||||
item_names.SIEGE_TANK_SPIDER_MINES,
|
||||
item_names.RAVEN_SPIDER_MINES,
|
||||
]
|
||||
item_name_groups[ItemGroupNames.TERRAN_SC1_UNITS] = [
|
||||
item_names.MARINE,
|
||||
item_names.FIREBAT,
|
||||
item_names.GHOST,
|
||||
item_names.MEDIC,
|
||||
item_names.VULTURE,
|
||||
item_names.SIEGE_TANK,
|
||||
item_names.GOLIATH,
|
||||
item_names.WRAITH,
|
||||
# No dropship
|
||||
item_names.SCIENCE_VESSEL,
|
||||
item_names.BATTLECRUISER,
|
||||
item_names.VALKYRIE,
|
||||
]
|
||||
item_name_groups[ItemGroupNames.TERRAN_SC1_BUILDINGS] = [
|
||||
item_names.BUNKER,
|
||||
item_names.MISSILE_TURRET,
|
||||
]
|
||||
item_name_groups[ItemGroupNames.TERRAN_LADDER_UNITS] = [
|
||||
item_names.MARINE,
|
||||
item_names.MARAUDER,
|
||||
item_names.REAPER,
|
||||
item_names.GHOST,
|
||||
item_names.HELLION,
|
||||
item_names.WIDOW_MINE,
|
||||
item_names.SIEGE_TANK,
|
||||
item_names.THOR,
|
||||
item_names.CYCLONE,
|
||||
item_names.VIKING,
|
||||
item_names.MEDIVAC,
|
||||
item_names.LIBERATOR,
|
||||
item_names.RAVEN,
|
||||
item_names.BANSHEE,
|
||||
item_names.BATTLECRUISER,
|
||||
]
|
||||
|
||||
# Terran Upgrades
|
||||
item_name_groups[ItemGroupNames.WOL_UPGRADES] = wol_upgrades = [
|
||||
@@ -597,6 +646,38 @@ item_name_groups[ItemGroupNames.OVERLORD_UPGRADES] = [
|
||||
item_names.OVERLORD_IMPROVED_OVERLORDS,
|
||||
item_names.OVERLORD_OVERSEER_ASPECT,
|
||||
]
|
||||
item_name_groups[ItemGroupNames.ZERG_SC1_UNITS] = [
|
||||
item_names.ZERGLING,
|
||||
item_names.HYDRALISK,
|
||||
item_names.MUTALISK,
|
||||
item_names.SCOURGE,
|
||||
item_names.BROOD_QUEEN,
|
||||
item_names.DEFILER,
|
||||
item_names.ULTRALISK,
|
||||
item_names.HYDRALISK_LURKER_ASPECT,
|
||||
item_names.MUTALISK_CORRUPTOR_DEVOURER_ASPECT,
|
||||
item_names.MUTALISK_CORRUPTOR_GUARDIAN_ASPECT,
|
||||
item_names.DEVOURING_ONES,
|
||||
item_names.HUNTER_KILLERS,
|
||||
item_names.TORRASQUE_MERC,
|
||||
]
|
||||
item_name_groups[ItemGroupNames.ZERG_LADDER_UNITS] = [
|
||||
item_names.ZERGLING,
|
||||
item_names.SWARM_QUEEN, # Replace: Hive Queen
|
||||
item_names.ZERGLING_BANELING_ASPECT,
|
||||
item_names.ROACH,
|
||||
item_names.ROACH_RAVAGER_ASPECT,
|
||||
item_names.OVERLORD_OVERSEER_ASPECT,
|
||||
item_names.HYDRALISK,
|
||||
item_names.HYDRALISK_LURKER_ASPECT,
|
||||
item_names.MUTALISK,
|
||||
item_names.CORRUPTOR,
|
||||
item_names.MUTALISK_CORRUPTOR_VIPER_ASPECT,
|
||||
item_names.MUTALISK_CORRUPTOR_BROOD_LORD_ASPECT,
|
||||
item_names.INFESTOR,
|
||||
item_names.SWARM_HOST,
|
||||
item_names.ULTRALISK,
|
||||
]
|
||||
|
||||
# Zerg Upgrades
|
||||
item_name_groups[ItemGroupNames.HOTS_STRAINS] = hots_strains = [
|
||||
@@ -826,6 +907,45 @@ item_name_groups[ItemGroupNames.LOTV_ITEMS] = vanilla_lotv_items = (
|
||||
+ protoss_generic_upgrades
|
||||
+ lotv_war_council_upgrades
|
||||
)
|
||||
item_name_groups[ItemGroupNames.PROTOSS_SC1_UNITS] = [
|
||||
item_names.ZEALOT,
|
||||
item_names.DRAGOON,
|
||||
item_names.HIGH_TEMPLAR,
|
||||
item_names.DARK_TEMPLAR,
|
||||
item_names.DARK_ARCHON,
|
||||
item_names.DARK_TEMPLAR_DARK_ARCHON_MELD,
|
||||
# No shuttle
|
||||
item_names.REAVER,
|
||||
item_names.OBSERVER,
|
||||
item_names.SCOUT,
|
||||
item_names.CARRIER,
|
||||
item_names.ARBITER,
|
||||
item_names.CORSAIR,
|
||||
]
|
||||
item_name_groups[ItemGroupNames.PROTOSS_SC1_BUILDINGS] = [
|
||||
item_names.PHOTON_CANNON,
|
||||
item_names.SHIELD_BATTERY,
|
||||
]
|
||||
item_name_groups[ItemGroupNames.PROTOSS_LADDER_UNITS] = [
|
||||
item_names.ZEALOT,
|
||||
item_names.STALKER,
|
||||
item_names.SENTRY,
|
||||
item_names.ADEPT,
|
||||
item_names.HIGH_TEMPLAR,
|
||||
item_names.DARK_TEMPLAR,
|
||||
item_names.DARK_TEMPLAR_ARCHON_MERGE,
|
||||
item_names.OBSERVER,
|
||||
item_names.WARP_PRISM,
|
||||
item_names.IMMORTAL,
|
||||
item_names.COLOSSUS,
|
||||
item_names.DISRUPTOR,
|
||||
item_names.PHOENIX,
|
||||
item_names.VOID_RAY,
|
||||
item_names.ORACLE,
|
||||
item_names.CARRIER,
|
||||
item_names.TEMPEST,
|
||||
item_names.MOTHERSHIP, # Replace: Aiur Mothership
|
||||
]
|
||||
|
||||
item_name_groups[ItemGroupNames.VANILLA_ITEMS] = vanilla_items = (
|
||||
vanilla_wol_items + vanilla_hots_items + vanilla_lotv_items
|
||||
|
||||
@@ -2151,127 +2151,6 @@ not_balanced_starting_units = {
|
||||
item_names.TEMPEST,
|
||||
}
|
||||
|
||||
|
||||
# Defense rating table
|
||||
# Commented defense ratings are handled in LogicMixin
|
||||
tvx_defense_ratings = {
|
||||
item_names.SIEGE_TANK: 5,
|
||||
# "Graduating Range": 1,
|
||||
item_names.PLANETARY_FORTRESS: 3,
|
||||
# Bunker w/ Marine/Marauder: 3,
|
||||
item_names.PERDITION_TURRET: 2,
|
||||
item_names.DEVASTATOR_TURRET: 2,
|
||||
item_names.VULTURE: 1,
|
||||
item_names.BANSHEE: 1,
|
||||
item_names.BATTLECRUISER: 1,
|
||||
item_names.LIBERATOR: 4,
|
||||
item_names.WIDOW_MINE: 1,
|
||||
# "Concealment (Widow Mine)": 1
|
||||
}
|
||||
tvz_defense_ratings = {
|
||||
item_names.PERDITION_TURRET: 2,
|
||||
# Bunker w/ Firebat: 2,
|
||||
item_names.LIBERATOR: -2,
|
||||
item_names.HIVE_MIND_EMULATOR: 3,
|
||||
item_names.PSI_DISRUPTER: 3,
|
||||
}
|
||||
tvx_air_defense_ratings = {
|
||||
item_names.MISSILE_TURRET: 2,
|
||||
}
|
||||
zvx_defense_ratings = {
|
||||
# Note that this doesn't include Kerrigan because this is just for race swaps, which doesn't involve her (for now)
|
||||
item_names.SPINE_CRAWLER: 3,
|
||||
# w/ Twin Drones: 1
|
||||
item_names.SWARM_QUEEN: 1,
|
||||
item_names.SWARM_HOST: 1,
|
||||
# impaler: 3
|
||||
# "Hardened Tentacle Spines (Impaler)": 2
|
||||
# lurker: 1
|
||||
# "Seismic Spines (Lurker)": 2
|
||||
# "Adapted Spines (Lurker)": 1
|
||||
# brood lord : 2
|
||||
# corpser roach: 1
|
||||
# creep tumors (swarm queen or overseer): 1
|
||||
# w/ malignant creep: 1
|
||||
# tanks with ammo: 5
|
||||
item_names.INFESTED_BUNKER: 3,
|
||||
item_names.BILE_LAUNCHER: 2,
|
||||
}
|
||||
# zvz_defense_ratings = {
|
||||
# corpser roach: 1
|
||||
# primal igniter: 2
|
||||
# lurker: 1
|
||||
# w/ adapted spines: -1
|
||||
# impaler: -1
|
||||
# }
|
||||
zvx_air_defense_ratings = {
|
||||
item_names.SPORE_CRAWLER: 2,
|
||||
# w/ Twin Drones: 1
|
||||
item_names.INFESTED_MISSILE_TURRET: 2,
|
||||
}
|
||||
pvx_defense_ratings = {
|
||||
item_names.PHOTON_CANNON: 2,
|
||||
item_names.KHAYDARIN_MONOLITH: 3,
|
||||
item_names.SHIELD_BATTERY: 1,
|
||||
item_names.NEXUS_OVERCHARGE: 2,
|
||||
item_names.SKYLORD: 1,
|
||||
item_names.MATRIX_OVERLOAD: 1,
|
||||
item_names.COLOSSUS: 1,
|
||||
item_names.VANGUARD: 1,
|
||||
item_names.REAVER: 1,
|
||||
}
|
||||
pvz_defense_ratings = {
|
||||
item_names.KHAYDARIN_MONOLITH: -2,
|
||||
item_names.COLOSSUS: 1,
|
||||
}
|
||||
|
||||
terran_passive_ratings = {
|
||||
item_names.AUTOMATED_REFINERY: 4,
|
||||
item_names.COMMAND_CENTER_MULE: 4,
|
||||
item_names.ORBITAL_DEPOTS: 2,
|
||||
item_names.COMMAND_CENTER_COMMAND_CENTER_REACTOR: 2,
|
||||
item_names.COMMAND_CENTER_EXTRA_SUPPLIES: 2,
|
||||
item_names.MICRO_FILTERING: 2,
|
||||
item_names.TECH_REACTOR: 2
|
||||
}
|
||||
|
||||
zerg_passive_ratings = {
|
||||
item_names.TWIN_DRONES: 7,
|
||||
item_names.AUTOMATED_EXTRACTORS: 4,
|
||||
item_names.VESPENE_EFFICIENCY: 3,
|
||||
item_names.OVERLORD_IMPROVED_OVERLORDS: 4,
|
||||
item_names.MALIGNANT_CREEP: 2
|
||||
}
|
||||
|
||||
protoss_passive_ratings = {
|
||||
item_names.QUATRO: 4,
|
||||
item_names.ORBITAL_ASSIMILATORS: 4,
|
||||
item_names.AMPLIFIED_ASSIMILATORS: 3,
|
||||
item_names.PROBE_WARPIN: 2,
|
||||
item_names.ELDER_PROBES: 2,
|
||||
item_names.MATRIX_OVERLOAD: 2
|
||||
}
|
||||
|
||||
soa_energy_ratings = {
|
||||
item_names.SOA_SOLAR_LANCE: 8,
|
||||
item_names.SOA_DEPLOY_FENIX: 7,
|
||||
item_names.SOA_TEMPORAL_FIELD: 6,
|
||||
item_names.SOA_PROGRESSIVE_PROXY_PYLON: 5, # Requires Lvl 2 (Warp in Reinforcements)
|
||||
item_names.SOA_SHIELD_OVERCHARGE: 5,
|
||||
item_names.SOA_ORBITAL_STRIKE: 4
|
||||
}
|
||||
|
||||
soa_passive_ratings = {
|
||||
item_names.GUARDIAN_SHELL: 4,
|
||||
item_names.OVERWATCH: 2
|
||||
}
|
||||
|
||||
soa_ultimate_ratings = {
|
||||
item_names.SOA_TIME_STOP: 4,
|
||||
item_names.SOA_PURIFIER_BEAM: 3,
|
||||
item_names.SOA_SOLAR_BOMBARDMENT: 3
|
||||
}
|
||||
|
||||
kerrigan_levels = [
|
||||
item_name for item_name, item_data in item_table.items()
|
||||
if item_data.type == ZergItemType.Level and item_data.race == SC2Race.ZERG
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -306,8 +306,8 @@ class ShapezWorld(World):
|
||||
self.location_count = len(self.included_locations)
|
||||
|
||||
# Create regions and entrances based on included locations and player options
|
||||
self.multiworld.regions.extend(create_shapez_regions(self.player, self.multiworld,
|
||||
bool(self.options.allow_floating_layers.value),
|
||||
has_floating = self.options.allow_floating_layers.value or not (self.options.randomize_level_requirements and self.options.randomize_upgrade_requirements)
|
||||
self.multiworld.regions.extend(create_shapez_regions(self.player, self.multiworld, has_floating,
|
||||
self.included_locations, self.location_name_to_id,
|
||||
self.level_logic, self.upgrade_logic,
|
||||
self.options.early_balancer_tunnel_and_trash.current_key,
|
||||
|
||||
@@ -1047,7 +1047,7 @@ def set_er_region_rules(world: "TunicWorld", regions: dict[str, Region], portal_
|
||||
connecting_region=regions["Rooted Ziggurat Portal Room"])
|
||||
regions["Rooted Ziggurat Portal Room"].connect(
|
||||
connecting_region=regions["Rooted Ziggurat Portal"],
|
||||
rule=lambda state: has_fuses("Activate Ziggurat Fuse", state, world) and has_ability(prayer, state, world))
|
||||
rule=lambda state: has_ability(prayer, state, world))
|
||||
|
||||
regions["Rooted Ziggurat Portal Room"].connect(
|
||||
connecting_region=regions["Rooted Ziggurat Portal Room Exit"],
|
||||
|
||||
@@ -178,7 +178,7 @@ class WargrooveContext(CommonContext):
|
||||
self.remove_communication_files()
|
||||
atexit.register(self.remove_communication_files)
|
||||
if not os.path.isdir(appdata_wargroove):
|
||||
print_error_and_close("WargrooveClient couldn't find Wargoove in appdata!"
|
||||
print_error_and_close("WargrooveClient couldn't find Wargoove in appdata! "
|
||||
"Boot Wargroove and then close it to attempt to fix this error")
|
||||
mods_directory = os.path.join(appdata_wargroove, "mods", "ArchipelagoMod")
|
||||
save_directory = os.path.join(appdata_wargroove, "save")
|
||||
|
||||
Reference in New Issue
Block a user