mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-09 09:03:46 -07:00
Compare commits
38 Commits
NewSoupVi-
...
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 | ||
|
|
d834ecec6a | ||
|
|
f3000a89d4 | ||
|
|
aa2774a5d5 |
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:
|
||||
|
||||
@@ -44,19 +44,19 @@ class TestOptions(unittest.TestCase):
|
||||
}],
|
||||
[{
|
||||
"name": "ItemLinkGroup",
|
||||
"item_pool": ["Hammer", "Bow"],
|
||||
"item_pool": ["Hammer", "Sword"],
|
||||
"link_replacement": False,
|
||||
"replacement_item": None,
|
||||
}]
|
||||
]
|
||||
# we really need some sort of test world but generic doesn't have enough items for this
|
||||
world = AutoWorldRegister.world_types["A Link to the Past"]
|
||||
world = AutoWorldRegister.world_types["APQuest"]
|
||||
plando_options = PlandoOptions.from_option_string("bosses")
|
||||
item_links = [ItemLinks.from_any(item_link_groups[0]), ItemLinks.from_any(item_link_groups[1])]
|
||||
for link in item_links:
|
||||
link.verify(world, "tester", plando_options)
|
||||
self.assertIn("Hammer", link.value[0]["item_pool"])
|
||||
self.assertIn("Bow", link.value[0]["item_pool"])
|
||||
self.assertIn("Sword", link.value[0]["item_pool"])
|
||||
|
||||
# TODO test that the group created using these options has the items
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -2,8 +2,8 @@ description: Almost blank test yaml
|
||||
name: Player{NUMBER}
|
||||
|
||||
game:
|
||||
Timespinner: 1 # what else
|
||||
APQuest: 1 # what else
|
||||
requires:
|
||||
version: 0.2.6
|
||||
Timespinner: {}
|
||||
APQuest: {}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,6 +16,10 @@ def make_data_directory(dir_name: str) -> Path:
|
||||
gitignore = specific_data_directory / ".gitignore"
|
||||
|
||||
with open(gitignore, "w") as f:
|
||||
f.write("*\n")
|
||||
f.write(
|
||||
"""*
|
||||
!.gitignore
|
||||
"""
|
||||
)
|
||||
|
||||
return specific_data_directory
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import binascii
|
||||
import importlib.util
|
||||
import importlib.machinery
|
||||
import os
|
||||
import random
|
||||
import pickle
|
||||
import Utils
|
||||
import settings
|
||||
from collections import defaultdict
|
||||
from typing import Dict
|
||||
|
||||
@@ -65,8 +63,27 @@ from .patches.aesthetics import rgb_to_bin, bin_to_rgb
|
||||
|
||||
from .. import Options
|
||||
|
||||
class VersionError(Exception):
|
||||
pass
|
||||
|
||||
# Function to generate a final rom, this patches the rom with all required patches
|
||||
def generateRom(base_rom: bytes, args, patch_data: Dict):
|
||||
from .. import LinksAwakeningWorld
|
||||
patcher_version = LinksAwakeningWorld.world_version
|
||||
generated_version = Utils.tuplize_version(patch_data.get("generated_world_version", "2.0.0"))
|
||||
if generated_version.major != patcher_version.major or generated_version.minor != patcher_version.minor:
|
||||
Utils.messagebox(
|
||||
"Error",
|
||||
"The apworld version that this patch was generated on is incompatible with your installed world.\n\n"
|
||||
f"Generated on {generated_version.as_simple_string()}\n"
|
||||
f"Installed version {patcher_version.as_simple_string()}",
|
||||
True
|
||||
)
|
||||
raise VersionError(
|
||||
f"The installed world ({patcher_version.as_simple_string()}) is incompatible with the world this patch "
|
||||
f"was generated on ({generated_version.as_simple_string()})"
|
||||
)
|
||||
|
||||
random.seed(patch_data["seed"] + patch_data["player"])
|
||||
multi_key = binascii.unhexlify(patch_data["multi_key"].encode())
|
||||
item_list = pickle.loads(binascii.unhexlify(patch_data["item_list"].encode()))
|
||||
@@ -85,9 +102,8 @@ def generateRom(base_rom: bytes, args, patch_data: Dict):
|
||||
pymod.prePatch(rom)
|
||||
|
||||
if options["gfxmod"]:
|
||||
user_settings = settings.get_settings()
|
||||
try:
|
||||
gfx_mod_file = user_settings["ladx_options"]["gfx_mod_file"]
|
||||
gfx_mod_file = LinksAwakeningWorld.settings.gfx_mod_file
|
||||
patches.aesthetics.gfxMod(rom, gfx_mod_file)
|
||||
except FileNotFoundError:
|
||||
pass # if user just doesnt provide gfxmod file, let patching continue
|
||||
|
||||
@@ -47,6 +47,10 @@ class BadRetroArchResponse(GameboyException):
|
||||
|
||||
class BadRetroArchResponse(GameboyException):
|
||||
pass
|
||||
|
||||
|
||||
class VersionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class LAClientConstants:
|
||||
@@ -518,7 +522,7 @@ class LinksAwakeningContext(CommonContext):
|
||||
class LADXManager(GameManager):
|
||||
logging_pairs = [
|
||||
("Client", "Archipelago"),
|
||||
("Tracker", "Tracker"),
|
||||
("Tracker", "Tracker"),
|
||||
]
|
||||
base_title = f"Links Awakening DX Client {LinksAwakeningWorld.world_version.as_simple_string()} | Archipelago"
|
||||
|
||||
@@ -614,11 +618,20 @@ class LinksAwakeningContext(CommonContext):
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd == "Connected":
|
||||
self.game = self.slot_info[self.slot].game
|
||||
self.slot_data = args.get("slot_data", {})
|
||||
generated_version = Utils.tuplize_version(self.slot_data.get("world_version", "2.0.0"))
|
||||
client_version = LinksAwakeningWorld.world_version
|
||||
if generated_version.major != client_version.major:
|
||||
self.disconnected_intentionally = True
|
||||
raise VersionError(
|
||||
f"The installed world ({client_version.as_simple_string()}) is incompatible with "
|
||||
f"the world this game was generated on ({generated_version.as_simple_string()})"
|
||||
)
|
||||
# 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,
|
||||
"client_version": client_version.as_simple_string(),
|
||||
})
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import settings
|
||||
import worlds.Files
|
||||
import hashlib
|
||||
import Utils
|
||||
@@ -59,6 +58,7 @@ class LADXProcedurePatch(worlds.Files.APProcedurePatch):
|
||||
def write_patch_data(world: "LinksAwakeningWorld", patch: LADXProcedurePatch):
|
||||
item_list = pickle.dumps([item for item in world.ladxr_logic.iteminfo_list if not isinstance(item, KeyLocation)])
|
||||
data_dict = {
|
||||
"generated_world_version": world.world_version.as_simple_string(),
|
||||
"out_base": world.multiworld.get_out_file_name_base(patch.player),
|
||||
"is_race": world.multiworld.is_race,
|
||||
"seed": world.multiworld.seed,
|
||||
@@ -125,9 +125,9 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
|
||||
|
||||
|
||||
def get_base_rom_path(file_name: str = "") -> str:
|
||||
options = settings.get_settings()
|
||||
from . import LinksAwakeningWorld
|
||||
if not file_name:
|
||||
file_name = options["ladx_options"]["rom_file"]
|
||||
file_name = LinksAwakeningWorld.settings.rom_file
|
||||
if not os.path.exists(file_name):
|
||||
file_name = Utils.user_path(file_name)
|
||||
return file_name
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,8 +4,10 @@ import os
|
||||
import typing
|
||||
import logging
|
||||
import re
|
||||
import struct
|
||||
|
||||
import settings
|
||||
import Utils
|
||||
from BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, Tutorial
|
||||
from Fill import fill_restrictive
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
@@ -50,6 +52,17 @@ class LinksAwakeningSettings(settings.Group):
|
||||
description = "LADX ROM File"
|
||||
md5s = [LADXProcedurePatch.hash]
|
||||
|
||||
@classmethod
|
||||
def validate(cls, path: str) -> None:
|
||||
try:
|
||||
super().validate(path)
|
||||
except ValueError:
|
||||
Utils.messagebox(
|
||||
"Error",
|
||||
"Provided rom does not match hash for English 1.0/revision-0 of Link's Awakening DX",
|
||||
True)
|
||||
raise
|
||||
|
||||
class RomStart(str):
|
||||
"""
|
||||
Set this to false to never autostart a rom (such as after patching)
|
||||
@@ -71,6 +84,24 @@ class LinksAwakeningSettings(settings.Group):
|
||||
Only .bin or .bdiff files
|
||||
The same directory will be checked for a matching text modification file
|
||||
"""
|
||||
def browse(self, filetypes=None, **kwargs):
|
||||
filetypes = [("Binary / Patch files", [".bin", ".bdiff"])]
|
||||
return super().browse(filetypes=filetypes, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def validate(cls, path: str) -> None:
|
||||
with open(path, "rb", buffering=0) as f:
|
||||
header, size = struct.unpack("<II", f.read()[:8])
|
||||
if path.endswith('.bin') and header == 0xDEADBEEF and size < 1024:
|
||||
# detect extended spritesheets from upstream ladxr
|
||||
Utils.messagebox(
|
||||
"Error",
|
||||
"Extended sprite sheets are not supported. Try again with a different gfxmod file, "
|
||||
"or provide no file to continue without modifying graphics.",
|
||||
True)
|
||||
raise ValueError("Provided gfxmod file is an extended sheet, which is not supported")
|
||||
|
||||
|
||||
|
||||
rom_file: RomFile = RomFile(RomFile.copy_to)
|
||||
rom_start: typing.Union[RomStart, bool] = True
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
"game": "Links Awakening DX",
|
||||
"authors": [ "zig", "threeandthree" ],
|
||||
"minimum_ap_version": "0.6.4",
|
||||
"world_version": "2.0.0"
|
||||
"world_version": "2.0.1"
|
||||
}
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -374,22 +374,32 @@ def create_and_flag_explicit_item_locks_and_excludes(world: SC2World) -> List[Fi
|
||||
Handles `excluded_items`, `locked_items`, and `start_inventory`
|
||||
Returns a list of all possible non-filler items that can be added, with an accompanying flags bitfield.
|
||||
"""
|
||||
excluded_items = world.options.excluded_items
|
||||
unexcluded_items = world.options.unexcluded_items
|
||||
locked_items = world.options.locked_items
|
||||
start_inventory = world.options.start_inventory
|
||||
excluded_items: dict[str, int] = world.options.excluded_items.value
|
||||
unexcluded_items: dict[str, int] = world.options.unexcluded_items.value
|
||||
locked_items: dict[str, int] = world.options.locked_items.value
|
||||
start_inventory: dict[str, int] = world.options.start_inventory.value
|
||||
key_items = world.custom_mission_order.get_items_to_lock()
|
||||
|
||||
def resolve_count(count: Optional[int], max_count: int) -> int:
|
||||
if count == 0:
|
||||
def resolve_exclude(count: int, max_count: int) -> int:
|
||||
if count < 0:
|
||||
return max_count
|
||||
if count is None:
|
||||
return 0
|
||||
if max_count == 0:
|
||||
return count
|
||||
return min(count, max_count)
|
||||
return count
|
||||
|
||||
def resolve_count(count: int, max_count: int, negative_value: int | None = None) -> int:
|
||||
"""
|
||||
Handles `count` being out of range.
|
||||
* If `count > max_count`, returns `max_count`.
|
||||
* If `count < 0`, returns `negative_value` (returns `max_count` if `negative_value` is unspecified)
|
||||
"""
|
||||
if count < 0:
|
||||
if negative_value is None:
|
||||
return max_count
|
||||
return negative_value
|
||||
if max_count and count > max_count:
|
||||
return max_count
|
||||
return count
|
||||
|
||||
auto_excludes = {item_name: 1 for item_name in item_groups.legacy_items}
|
||||
auto_excludes = Counter({item_name: 1 for item_name in item_groups.legacy_items})
|
||||
if world.options.exclude_overpowered_items.value == ExcludeOverpoweredItems.option_true:
|
||||
for item_name in item_groups.overpowered_items:
|
||||
auto_excludes[item_name] = 1
|
||||
@@ -402,28 +412,29 @@ def create_and_flag_explicit_item_locks_and_excludes(world: SC2World) -> List[Fi
|
||||
elif item_name in item_groups.nova_equipment:
|
||||
continue
|
||||
else:
|
||||
auto_excludes[item_name] = 0
|
||||
auto_excludes[item_name] = item_data.quantity
|
||||
|
||||
|
||||
result: List[FilterItem] = []
|
||||
for item_name, item_data in item_tables.item_table.items():
|
||||
max_count = item_data.quantity
|
||||
auto_excluded_count = auto_excludes.get(item_name)
|
||||
auto_excluded_count = auto_excludes.get(item_name, 0)
|
||||
excluded_count = excluded_items.get(item_name, auto_excluded_count)
|
||||
unexcluded_count = unexcluded_items.get(item_name)
|
||||
locked_count = locked_items.get(item_name)
|
||||
start_count: Optional[int] = start_inventory.get(item_name)
|
||||
unexcluded_count = unexcluded_items.get(item_name, 0)
|
||||
locked_count = locked_items.get(item_name, 0)
|
||||
start_count = start_inventory.get(item_name, 0)
|
||||
key_count = key_items.get(item_name, 0)
|
||||
# specifying 0 in the yaml means exclude / lock all
|
||||
# start_inventory doesn't allow specifying 0
|
||||
# not specifying means don't exclude/lock/start
|
||||
excluded_count = resolve_count(excluded_count, max_count)
|
||||
unexcluded_count = resolve_count(unexcluded_count, max_count)
|
||||
# Specifying a negative number in the yaml means exclude / lock / start all.
|
||||
# In the case of excluded/unexcluded, resolve negatives to max_count before subtracting them,
|
||||
# and after subtraction resolve negatives to just 0 (when unexcluded > excluded).
|
||||
excluded_count = resolve_count(
|
||||
resolve_exclude(excluded_count, max_count) - resolve_exclude(unexcluded_count, max_count),
|
||||
max_count,
|
||||
negative_value=0
|
||||
)
|
||||
locked_count = resolve_count(locked_count, max_count)
|
||||
start_count = resolve_count(start_count, max_count)
|
||||
|
||||
excluded_count = max(0, excluded_count - unexcluded_count)
|
||||
|
||||
# Priority: start_inventory >> locked_items >> excluded_items >> unspecified
|
||||
if max_count == 0:
|
||||
if excluded_count:
|
||||
@@ -486,8 +497,9 @@ def flag_excludes_by_faction_presence(world: SC2World, item_list: List[FilterIte
|
||||
item.flags |= ItemFilterFlags.FilterExcluded
|
||||
continue
|
||||
if not zerg_missions and item.data.race == SC2Race.ZERG:
|
||||
if item.data.type != item_tables.ZergItemType.Ability \
|
||||
and item.data.type != ZergItemType.Level:
|
||||
if (item.data.type != item_tables.ZergItemType.Ability
|
||||
and item.data.type != ZergItemType.Level
|
||||
):
|
||||
item.flags |= ItemFilterFlags.FilterExcluded
|
||||
continue
|
||||
if not protoss_missions and item.data.race == SC2Race.PROTOSS:
|
||||
@@ -641,7 +653,7 @@ def flag_mission_based_item_excludes(world: SC2World, item_list: List[FilterItem
|
||||
item.flags |= ItemFilterFlags.FilterExcluded
|
||||
|
||||
# Remove Spear of Adun passives
|
||||
if item.name in item_tables.spear_of_adun_castable_passives and not soa_passive_presence:
|
||||
if item.name in item_groups.spear_of_adun_passives and not soa_passive_presence:
|
||||
item.flags |= ItemFilterFlags.FilterExcluded
|
||||
|
||||
# Remove matchup-specific items if you don't play that matchup
|
||||
|
||||
@@ -40,6 +40,7 @@ from .options import (
|
||||
SpearOfAdunPassivesPresentInNoBuild, EnableVoidTrade, VoidTradeAgeLimit, void_trade_age_limits_ms, VoidTradeWorkers,
|
||||
DifficultyDamageModifier, MissionOrderScouting, GenericUpgradeResearchSpeedup, MercenaryHighlanders, WarCouncilNerfs,
|
||||
is_mission_in_soa_presence,
|
||||
upgrade_included_names,
|
||||
)
|
||||
from .mission_order.slot_data import CampaignSlotData, LayoutSlotData, MissionSlotData, MissionOrderObjectSlotData
|
||||
from .mission_order.entry_rules import SubRuleRuleData, CountMissionsRuleData, MissionEntryRules
|
||||
@@ -71,10 +72,12 @@ from .mission_tables import (
|
||||
)
|
||||
|
||||
import colorama
|
||||
from .options import Option, upgrade_included_names
|
||||
from NetUtils import ClientStatus, NetworkItem, JSONtoTextParser, JSONMessagePart, add_json_item, add_json_location, add_json_text, JSONTypes
|
||||
from MultiServer import mark_raw
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from Options import Option
|
||||
|
||||
pool = concurrent.futures.ThreadPoolExecutor(1)
|
||||
loop = asyncio.get_event_loop_policy().new_event_loop()
|
||||
nest_asyncio.apply(loop)
|
||||
|
||||
@@ -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"
|
||||
@@ -167,6 +172,7 @@ class ItemGroupNames:
|
||||
LOTV_UNITS = "LotV Units"
|
||||
LOTV_ITEMS = "LotV Items"
|
||||
LOTV_GLOBAL_UPGRADES = "LotV Global Upgrades"
|
||||
SOA_PASSIVES = "SOA Passive Abilities"
|
||||
SOA_ITEMS = "SOA"
|
||||
PROTOSS_GLOBAL_UPGRADES = "Protoss Global Upgrades"
|
||||
PROTOSS_BUILDINGS = "Protoss Buildings"
|
||||
@@ -175,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"
|
||||
@@ -287,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,
|
||||
@@ -316,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 = [
|
||||
@@ -596,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 = [
|
||||
@@ -777,11 +859,21 @@ item_name_groups[ItemGroupNames.PURIFIER_UNITS] = [
|
||||
item_names.MIRAGE, item_names.DAWNBRINGER, item_names.TRIREME, item_names.TEMPEST,
|
||||
item_names.CALADRIUS,
|
||||
]
|
||||
item_name_groups[ItemGroupNames.SOA_ITEMS] = soa_items = [
|
||||
item_name_groups[ItemGroupNames.SOA_PASSIVES] = spear_of_adun_passives = [
|
||||
item_names.RECONSTRUCTION_BEAM,
|
||||
item_names.OVERWATCH,
|
||||
item_names.GUARDIAN_SHELL,
|
||||
]
|
||||
spear_of_adun_actives = [
|
||||
*[item_name for item_name, item_data in item_tables.item_table.items() if item_data.type == item_tables.ProtossItemType.Spear_Of_Adun],
|
||||
item_names.SOA_PROGRESSIVE_PROXY_PYLON,
|
||||
]
|
||||
lotv_soa_items = [item_name for item_name in soa_items if item_name != item_names.SOA_PYLON_OVERCHARGE]
|
||||
item_name_groups[ItemGroupNames.SOA_ITEMS] = soa_items = spear_of_adun_actives + spear_of_adun_passives
|
||||
lotv_soa_items = [
|
||||
item_name
|
||||
for item_name in soa_items
|
||||
if item_name not in (item_names.SOA_PYLON_OVERCHARGE, item_names.OVERWATCH)
|
||||
]
|
||||
item_name_groups[ItemGroupNames.PROTOSS_GLOBAL_UPGRADES] = [
|
||||
item_name for item_name, item_data in item_tables.item_table.items() if item_data.type == item_tables.ProtossItemType.Solarite_Core
|
||||
]
|
||||
@@ -815,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
|
||||
@@ -2293,12 +2172,6 @@ spear_of_adun_calldowns = {
|
||||
item_names.SOA_SOLAR_BOMBARDMENT
|
||||
}
|
||||
|
||||
spear_of_adun_castable_passives = {
|
||||
item_names.RECONSTRUCTION_BEAM,
|
||||
item_names.OVERWATCH,
|
||||
item_names.GUARDIAN_SHELL,
|
||||
}
|
||||
|
||||
nova_equipment = {
|
||||
*[item_name for item_name, item_data in get_full_item_list().items()
|
||||
if item_data.type == TerranItemType.Nova_Gear],
|
||||
|
||||
@@ -5,14 +5,13 @@ from datetime import timedelta
|
||||
|
||||
from Options import (
|
||||
Choice, Toggle, DefaultOnToggle, OptionSet, Range,
|
||||
PerGameCommonOptions, Option, VerifyKeys, StartInventory,
|
||||
PerGameCommonOptions, VerifyKeys, StartInventory,
|
||||
is_iterable_except_str, OptionGroup, Visibility, ItemDict,
|
||||
Accessibility, ProgressionBalancing
|
||||
OptionCounter,
|
||||
)
|
||||
from Utils import get_fuzzy_results
|
||||
from BaseClasses import PlandoOptions
|
||||
from .item import item_names, item_tables
|
||||
from .item.item_groups import kerrigan_active_abilities, kerrigan_passives, nova_weapons, nova_gadgets
|
||||
from .item import item_names, item_tables, item_groups
|
||||
from .mission_tables import (
|
||||
SC2Campaign, SC2Mission, lookup_name_to_mission, MissionPools, get_missions_with_any_flags_in_list,
|
||||
campaign_mission_table, SC2Race, MissionFlag
|
||||
@@ -700,7 +699,7 @@ class KerriganMaxActiveAbilities(Range):
|
||||
"""
|
||||
display_name = "Kerrigan Maximum Active Abilities"
|
||||
range_start = 0
|
||||
range_end = len(kerrigan_active_abilities)
|
||||
range_end = len(item_groups.kerrigan_active_abilities)
|
||||
default = range_end
|
||||
|
||||
|
||||
@@ -711,7 +710,7 @@ class KerriganMaxPassiveAbilities(Range):
|
||||
"""
|
||||
display_name = "Kerrigan Maximum Passive Abilities"
|
||||
range_start = 0
|
||||
range_end = len(kerrigan_passives)
|
||||
range_end = len(item_groups.kerrigan_passives)
|
||||
default = range_end
|
||||
|
||||
|
||||
@@ -829,7 +828,7 @@ class SpearOfAdunMaxAutocastAbilities(Range):
|
||||
"""
|
||||
display_name = "Spear of Adun Maximum Passive Abilities"
|
||||
range_start = 0
|
||||
range_end = sum(item.quantity for item_name, item in item_tables.get_full_item_list().items() if item_name in item_tables.spear_of_adun_castable_passives)
|
||||
range_end = sum(item_tables.item_table[item_name].quantity for item_name in item_groups.spear_of_adun_passives)
|
||||
default = range_end
|
||||
|
||||
|
||||
@@ -883,7 +882,7 @@ class NovaMaxWeapons(Range):
|
||||
"""
|
||||
display_name = "Nova Maximum Weapons"
|
||||
range_start = 0
|
||||
range_end = len(nova_weapons)
|
||||
range_end = len(item_groups.nova_weapons)
|
||||
default = range_end
|
||||
|
||||
|
||||
@@ -897,7 +896,7 @@ class NovaMaxGadgets(Range):
|
||||
"""
|
||||
display_name = "Nova Maximum Gadgets"
|
||||
range_start = 0
|
||||
range_end = len(nova_gadgets)
|
||||
range_end = len(item_groups.nova_gadgets)
|
||||
default = range_end
|
||||
|
||||
|
||||
@@ -932,33 +931,48 @@ class TakeOverAIAllies(Toggle):
|
||||
display_name = "Take Over AI Allies"
|
||||
|
||||
|
||||
class Sc2ItemDict(Option[Dict[str, int]], VerifyKeys, Mapping[str, int]):
|
||||
"""A branch of ItemDict that supports item counts of 0"""
|
||||
class Sc2ItemDict(OptionCounter, VerifyKeys, Mapping[str, int]):
|
||||
"""A branch of ItemDict that supports negative item counts"""
|
||||
default = {}
|
||||
supports_weighting = False
|
||||
verify_item_name = True
|
||||
# convert_name_groups = True
|
||||
display_name = 'Unnamed dictionary'
|
||||
minimum_value: int = 0
|
||||
# Note(phaneros): Limiting minimum to -1 means that if two triggers add -1 to the same item,
|
||||
# the validation fails. So give trigger people space to stack a bunch of triggers.
|
||||
min: int = -1000
|
||||
max: int = 1000
|
||||
valid_keys = set(item_tables.item_table) | set(item_groups.item_name_groups)
|
||||
|
||||
def __init__(self, value: Dict[str, int]):
|
||||
def __init__(self, value: dict[str, int]):
|
||||
self.value = {key: val for key, val in value.items()}
|
||||
|
||||
@classmethod
|
||||
def from_any(cls, data: Union[List[str], Dict[str, int]]) -> 'Sc2ItemDict':
|
||||
def from_any(cls, data: list[str] | dict[str, int]) -> 'Sc2ItemDict':
|
||||
if isinstance(data, list):
|
||||
# This is a little default that gets us backwards compatibility with lists.
|
||||
# It doesn't play nice with trigger merging dicts and lists together, though, so best not to advertise it overmuch.
|
||||
data = {item: 0 for item in data}
|
||||
raise ValueError(
|
||||
f"{cls.display_name}: Cannot convert from list. "
|
||||
f"Use dict syntax (no dashes, 'value: number' synax)."
|
||||
)
|
||||
if isinstance(data, dict):
|
||||
for key, value in data.items():
|
||||
if not isinstance(value, int):
|
||||
raise ValueError(f"Invalid type in '{cls.display_name}': element '{key}' maps to '{value}', expected an integer")
|
||||
if value < cls.minimum_value:
|
||||
raise ValueError(f"Invalid value for '{cls.display_name}': element '{key}' maps to {value}, which is less than the minimum ({cls.minimum_value})")
|
||||
raise ValueError(
|
||||
f"Invalid type in '{cls.display_name}': "
|
||||
f"element '{key}' maps to '{value}', expected an integer"
|
||||
)
|
||||
if value < cls.min:
|
||||
raise ValueError(
|
||||
f"Invalid value for '{cls.display_name}': "
|
||||
f"element '{key}' maps to {value}, which is less than the minimum ({cls.min})"
|
||||
)
|
||||
if value > cls.max:
|
||||
raise ValueError(f"Invalid value for '{cls.display_name}': "
|
||||
f"element '{key}' maps to {value}, which is greater than the maximum ({cls.max})"
|
||||
)
|
||||
return cls(data)
|
||||
else:
|
||||
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
|
||||
raise NotImplementedError(f"{cls.display_name}: Cannot convert from non-dictionary, got {type(data)}")
|
||||
|
||||
def verify(self, world: Type['World'], player_name: str, plando_options: PlandoOptions) -> None:
|
||||
"""Overridden version of function from Options.VerifyKeys for a better error message"""
|
||||
@@ -974,15 +988,16 @@ class Sc2ItemDict(Option[Dict[str, int]], VerifyKeys, Mapping[str, int]):
|
||||
self.value = new_value
|
||||
for item_name in self.value:
|
||||
if item_name not in world.item_names:
|
||||
from .item import item_groups
|
||||
picks = get_fuzzy_results(
|
||||
item_name,
|
||||
list(world.item_names) + list(item_groups.ItemGroupNames.get_all_group_names()),
|
||||
limit=1,
|
||||
)
|
||||
raise Exception(f"Item {item_name} from option {self} "
|
||||
f"is not a valid item name from {world.game}. "
|
||||
f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure)")
|
||||
raise Exception(
|
||||
f"Item {item_name} from option {self} "
|
||||
f"is not a valid item name from {world.game}. "
|
||||
f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure)"
|
||||
)
|
||||
|
||||
def get_option_name(self, value):
|
||||
return ", ".join(f"{key}: {v}" for key, v in value.items())
|
||||
@@ -998,25 +1013,25 @@ class Sc2ItemDict(Option[Dict[str, int]], VerifyKeys, Mapping[str, int]):
|
||||
|
||||
|
||||
class Sc2StartInventory(Sc2ItemDict):
|
||||
"""Start with these items."""
|
||||
"""Start with these items. Use an amount of -1 to start with all copies of an item."""
|
||||
display_name = StartInventory.display_name
|
||||
|
||||
|
||||
class LockedItems(Sc2ItemDict):
|
||||
"""Guarantees that these items will be unlockable, in the amount specified.
|
||||
Specify an amount of 0 to lock all copies of an item."""
|
||||
Specify an amount of -1 to lock all copies of an item."""
|
||||
display_name = "Locked Items"
|
||||
|
||||
|
||||
class ExcludedItems(Sc2ItemDict):
|
||||
"""Guarantees that these items will not be unlockable, in the amount specified.
|
||||
Specify an amount of 0 to exclude all copies of an item."""
|
||||
Specify an amount of -1 to exclude all copies of an item."""
|
||||
display_name = "Excluded Items"
|
||||
|
||||
|
||||
class UnexcludedItems(Sc2ItemDict):
|
||||
"""Undoes an item exclusion; useful for whitelisting or fine-tuning a category.
|
||||
Specify an amount of 0 to unexclude all copies of an item."""
|
||||
Specify an amount of -1 to unexclude all copies of an item."""
|
||||
display_name = "Unexcluded Items"
|
||||
|
||||
|
||||
|
||||
@@ -3,8 +3,7 @@ from typing import Callable, Dict, List, Set, Tuple, TYPE_CHECKING, Iterable
|
||||
|
||||
from BaseClasses import Location, ItemClassification
|
||||
from .item import StarcraftItem, ItemFilterFlags, item_names, item_parents, item_groups
|
||||
from .item.item_tables import item_table, TerranItemType, ZergItemType, spear_of_adun_calldowns, \
|
||||
spear_of_adun_castable_passives
|
||||
from .item.item_tables import item_table, TerranItemType, ZergItemType, spear_of_adun_calldowns
|
||||
from .options import RequiredTactics
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -272,7 +271,7 @@ class ValidInventory:
|
||||
self.world.random.shuffle(spear_of_adun_actives)
|
||||
cull_items_over_maximum(spear_of_adun_actives, self.world.options.spear_of_adun_max_active_abilities.value)
|
||||
|
||||
spear_of_adun_autocasts = [item for item in inventory if item.name in spear_of_adun_castable_passives]
|
||||
spear_of_adun_autocasts = [item for item in inventory if item.name in item_groups.spear_of_adun_passives]
|
||||
self.world.random.shuffle(spear_of_adun_autocasts)
|
||||
cull_items_over_maximum(spear_of_adun_autocasts, self.world.options.spear_of_adun_max_passive_abilities.value)
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,19 +18,19 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
world_options = {
|
||||
**self.ALL_CAMPAIGNS,
|
||||
'locked_items': {
|
||||
item_names.MARINE: 0,
|
||||
item_names.MARAUDER: 0,
|
||||
item_names.MARINE: -1,
|
||||
item_names.MARAUDER: -1,
|
||||
item_names.MEDIVAC: 1,
|
||||
item_names.FIREBAT: 1,
|
||||
item_names.ZEALOT: 0,
|
||||
item_names.ZEALOT: -1,
|
||||
item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL: 2,
|
||||
},
|
||||
'excluded_items': {
|
||||
item_names.MARINE: 0,
|
||||
item_names.MARAUDER: 0,
|
||||
item_names.MEDIVAC: 0,
|
||||
item_names.MARINE: -1,
|
||||
item_names.MARAUDER: -1,
|
||||
item_names.MEDIVAC: -1,
|
||||
item_names.FIREBAT: 1,
|
||||
item_names.ZERGLING: 0,
|
||||
item_names.ZERGLING: -1,
|
||||
item_names.PROGRESSIVE_REGENERATIVE_BIO_STEEL: 2,
|
||||
}
|
||||
}
|
||||
@@ -50,38 +50,38 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
world_options = {
|
||||
'grant_story_tech': options.GrantStoryTech.option_grant,
|
||||
'excluded_items': {
|
||||
item_groups.ItemGroupNames.NOVA_EQUIPMENT: 15,
|
||||
item_groups.ItemGroupNames.NOVA_EQUIPMENT: -1,
|
||||
item_names.MARINE_PROGRESSIVE_STIMPACK: 1,
|
||||
item_names.MARAUDER_PROGRESSIVE_STIMPACK: 2,
|
||||
item_names.MARINE: 0,
|
||||
item_names.MARAUDER: 0,
|
||||
item_names.MARINE: -1,
|
||||
item_names.MARAUDER: -1,
|
||||
item_names.REAPER: 1,
|
||||
item_names.DIAMONDBACK: 0,
|
||||
item_names.DIAMONDBACK: -1,
|
||||
item_names.HELLION: 1,
|
||||
# Additional excludes to increase the likelihood that unexcluded items actually appear
|
||||
item_groups.ItemGroupNames.STARPORT_UNITS: 0,
|
||||
item_names.WARHOUND: 0,
|
||||
item_names.VULTURE: 0,
|
||||
item_names.WIDOW_MINE: 0,
|
||||
item_names.THOR: 0,
|
||||
item_names.GHOST: 0,
|
||||
item_names.SPECTRE: 0,
|
||||
item_groups.ItemGroupNames.MENGSK_UNITS: 0,
|
||||
item_groups.ItemGroupNames.TERRAN_VETERANCY_UNITS: 0,
|
||||
item_groups.ItemGroupNames.STARPORT_UNITS: -1,
|
||||
item_names.WARHOUND: -1,
|
||||
item_names.VULTURE: -1,
|
||||
item_names.WIDOW_MINE: -1,
|
||||
item_names.THOR: -1,
|
||||
item_names.GHOST: -1,
|
||||
item_names.SPECTRE: -1,
|
||||
item_groups.ItemGroupNames.MENGSK_UNITS: -1,
|
||||
item_groups.ItemGroupNames.TERRAN_VETERANCY_UNITS: -1,
|
||||
},
|
||||
'unexcluded_items': {
|
||||
item_names.NOVA_PLASMA_RIFLE: 1, # Necessary to pass logic
|
||||
item_names.NOVA_PULSE_GRENADES: 0, # Necessary to pass logic
|
||||
item_names.NOVA_JUMP_SUIT_MODULE: 0, # Necessary to pass logic
|
||||
item_groups.ItemGroupNames.BARRACKS_UNITS: 0,
|
||||
item_names.NOVA_PLASMA_RIFLE: 1, # Necessary to pass logic
|
||||
item_names.NOVA_PULSE_GRENADES: -1, # Necessary to pass logic
|
||||
item_names.NOVA_JUMP_SUIT_MODULE: -1, # Necessary to pass logic
|
||||
item_groups.ItemGroupNames.BARRACKS_UNITS: -1,
|
||||
item_names.NOVA_PROGRESSIVE_STEALTH_SUIT_MODULE: 1,
|
||||
item_names.HELLION: 1,
|
||||
item_names.MARINE_PROGRESSIVE_STIMPACK: 1,
|
||||
item_names.MARAUDER_PROGRESSIVE_STIMPACK: 0,
|
||||
item_names.MARAUDER_PROGRESSIVE_STIMPACK: -1,
|
||||
# Additional unexcludes for logic
|
||||
item_names.MEDIVAC: 0,
|
||||
item_names.BATTLECRUISER: 0,
|
||||
item_names.SCIENCE_VESSEL: 0,
|
||||
item_names.MEDIVAC: -1,
|
||||
item_names.BATTLECRUISER: -1,
|
||||
item_names.SCIENCE_VESSEL: -1,
|
||||
},
|
||||
# Terran-only
|
||||
'enabled_campaigns': {
|
||||
@@ -103,11 +103,29 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
self.assertNotIn(item_names.NOVA_BLAZEFIRE_GUNBLADE, itempool)
|
||||
self.assertNotIn(item_names.NOVA_ENERGY_SUIT_MODULE, itempool)
|
||||
|
||||
def test_exclude_2_beats_unexclude_1(self) -> None:
|
||||
world_options = {
|
||||
options.OPTION_NAME[options.ExcludedItems]: {
|
||||
item_names.MARINE: 2,
|
||||
},
|
||||
options.OPTION_NAME[options.UnexcludedItems]: {
|
||||
item_names.MARINE: 1,
|
||||
},
|
||||
# Ensure enough locations that marine doesn't get culled
|
||||
options.OPTION_NAME[options.SelectedRaces]: {
|
||||
SC2Race.TERRAN.get_title(),
|
||||
},
|
||||
options.OPTION_NAME[options.VictoryCache]: 9,
|
||||
}
|
||||
self.generate_world(world_options)
|
||||
itempool = [item.name for item in self.multiworld.itempool]
|
||||
self.assertNotIn(item_names.MARINE, itempool)
|
||||
|
||||
def test_excluding_groups_excludes_all_items_in_group(self):
|
||||
world_options = {
|
||||
'excluded_items': [
|
||||
item_groups.ItemGroupNames.BARRACKS_UNITS.lower(),
|
||||
]
|
||||
'excluded_items': {
|
||||
item_groups.ItemGroupNames.BARRACKS_UNITS.lower(): -1,
|
||||
},
|
||||
}
|
||||
self.generate_world(world_options)
|
||||
itempool = [item.name for item in self.multiworld.itempool]
|
||||
@@ -337,9 +355,9 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
# Options under test
|
||||
'vanilla_items_only': True,
|
||||
'unexcluded_items': {
|
||||
item_names.PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM: 0,
|
||||
item_names.PROGRESSIVE_FIRE_SUPPRESSION_SYSTEM: -1,
|
||||
item_names.WARHOUND: 1,
|
||||
item_groups.ItemGroupNames.TERRAN_STIMPACKS: 0,
|
||||
item_groups.ItemGroupNames.TERRAN_STIMPACKS: -1,
|
||||
},
|
||||
# Avoid options that lock non-vanilla items for logic
|
||||
'required_tactics': options.RequiredTactics.option_any_units,
|
||||
@@ -463,12 +481,12 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
},
|
||||
'required_tactics': options.RequiredTactics.option_no_logic,
|
||||
'enable_morphling': options.EnableMorphling.option_true,
|
||||
'excluded_items': [
|
||||
item_groups.ItemGroupNames.ZERG_UNITS.lower()
|
||||
],
|
||||
'unexcluded_items': [
|
||||
item_groups.ItemGroupNames.ZERG_MORPHS.lower()
|
||||
]
|
||||
'excluded_items': {
|
||||
item_groups.ItemGroupNames.ZERG_UNITS.lower(): -1,
|
||||
},
|
||||
'unexcluded_items': {
|
||||
item_groups.ItemGroupNames.ZERG_MORPHS.lower(): -1,
|
||||
},
|
||||
}
|
||||
self.generate_world(world_options)
|
||||
itempool = [item.name for item in self.multiworld.itempool]
|
||||
@@ -486,12 +504,12 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
},
|
||||
'required_tactics': options.RequiredTactics.option_no_logic,
|
||||
'enable_morphling': options.EnableMorphling.option_false,
|
||||
'excluded_items': [
|
||||
item_groups.ItemGroupNames.ZERG_UNITS.lower()
|
||||
],
|
||||
'unexcluded_items': [
|
||||
item_groups.ItemGroupNames.ZERG_MORPHS.lower()
|
||||
]
|
||||
'excluded_items': {
|
||||
item_groups.ItemGroupNames.ZERG_UNITS.lower(): -1,
|
||||
},
|
||||
'unexcluded_items': {
|
||||
item_groups.ItemGroupNames.ZERG_MORPHS.lower(): -1,
|
||||
},
|
||||
}
|
||||
self.generate_world(world_options)
|
||||
itempool = [item.name for item in self.multiworld.itempool]
|
||||
@@ -520,14 +538,14 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
|
||||
def test_planetary_orbital_module_not_present_without_cc_spells(self) -> None:
|
||||
world_options = {
|
||||
"excluded_items": [
|
||||
item_names.COMMAND_CENTER_MULE,
|
||||
item_names.COMMAND_CENTER_SCANNER_SWEEP,
|
||||
item_names.COMMAND_CENTER_EXTRA_SUPPLIES
|
||||
],
|
||||
"locked_items": [
|
||||
item_names.PLANETARY_FORTRESS
|
||||
]
|
||||
"excluded_items": {
|
||||
item_names.COMMAND_CENTER_MULE: -1,
|
||||
item_names.COMMAND_CENTER_SCANNER_SWEEP: -1,
|
||||
item_names.COMMAND_CENTER_EXTRA_SUPPLIES: -1,
|
||||
},
|
||||
"locked_items": {
|
||||
item_names.PLANETARY_FORTRESS: -1,
|
||||
}
|
||||
}
|
||||
|
||||
self.generate_world(world_options)
|
||||
@@ -931,10 +949,10 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
}
|
||||
},
|
||||
'grant_story_levels': options.GrantStoryLevels.option_additive,
|
||||
'excluded_items': [
|
||||
item_names.KERRIGAN_LEAPING_STRIKE,
|
||||
item_names.KERRIGAN_MEND,
|
||||
]
|
||||
'excluded_items': {
|
||||
item_names.KERRIGAN_LEAPING_STRIKE: -1,
|
||||
item_names.KERRIGAN_MEND: -1,
|
||||
}
|
||||
}
|
||||
self.generate_world(world_options)
|
||||
itempool = [item.name for item in self.multiworld.itempool]
|
||||
@@ -1208,7 +1226,7 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
'mission_order': MissionOrder.option_grid,
|
||||
'maximum_campaign_size': MaximumCampaignSize.range_end,
|
||||
'exclude_overpowered_items': ExcludeOverpoweredItems.option_true,
|
||||
'locked_items': [locked_item],
|
||||
'locked_items': {locked_item: -1},
|
||||
'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all,
|
||||
'selected_races': [SC2Race.TERRAN.get_title()],
|
||||
}
|
||||
@@ -1249,7 +1267,7 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
'maximum_campaign_size': MaximumCampaignSize.range_end,
|
||||
'exclude_overpowered_items': ExcludeOverpoweredItems.option_false,
|
||||
'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all,
|
||||
'locked_items': {item_name: 0 for item_name in unreleased_items},
|
||||
'locked_items': {item_name: -1 for item_name in unreleased_items},
|
||||
}
|
||||
|
||||
self.generate_world(world_options)
|
||||
@@ -1264,7 +1282,7 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
**self.ALL_CAMPAIGNS,
|
||||
'mission_order': MissionOrder.option_grid,
|
||||
'maximum_campaign_size': MaximumCampaignSize.range_end,
|
||||
'excluded_items': [item_name for item_name in item_groups.terran_mercenaries],
|
||||
'excluded_items': {item_name: -1 for item_name in item_groups.terran_mercenaries},
|
||||
'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all,
|
||||
'selected_races': [SC2Race.TERRAN.get_title()],
|
||||
}
|
||||
@@ -1280,7 +1298,7 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
'mission_order': MissionOrder.option_grid,
|
||||
'maximum_campaign_size': MaximumCampaignSize.range_end,
|
||||
'exclude_overpowered_items': ExcludeOverpoweredItems.option_true,
|
||||
'unexcluded_items': [item_names.SOA_TIME_STOP],
|
||||
'unexcluded_items': {item_names.SOA_TIME_STOP: -1},
|
||||
'enable_race_swap': options.EnableRaceSwapVariants.option_shuffle_all,
|
||||
}
|
||||
|
||||
@@ -1322,7 +1340,7 @@ class TestItemFiltering(Sc2SetupTestBase):
|
||||
'enabled_campaigns': {
|
||||
SC2Campaign.WOL.campaign_name
|
||||
},
|
||||
'excluded_items': [item_names.MARINE, item_names.MEDIC],
|
||||
'excluded_items': {item_names.MARINE: -1, item_names.MEDIC: -1},
|
||||
'shuffle_no_build': False,
|
||||
'required_tactics': RequiredTactics.option_standard
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ class ItemFilterTests(Sc2SetupTestBase):
|
||||
def test_excluding_all_barracks_units_excludes_infantry_upgrades(self) -> None:
|
||||
world_options = {
|
||||
'excluded_items': {
|
||||
item_groups.ItemGroupNames.BARRACKS_UNITS: 0
|
||||
item_groups.ItemGroupNames.BARRACKS_UNITS: -1,
|
||||
},
|
||||
'required_tactics': 'standard',
|
||||
'min_number_of_upgrades': 1,
|
||||
|
||||
@@ -35,10 +35,10 @@ class TestSupportedUseCases(Sc2SetupTestBase):
|
||||
SC2Campaign.NCO.campaign_name
|
||||
},
|
||||
'excluded_items': {
|
||||
item_groups.ItemGroupNames.TERRAN_UNITS: 0,
|
||||
item_groups.ItemGroupNames.TERRAN_UNITS: -1,
|
||||
},
|
||||
'unexcluded_items': {
|
||||
item_groups.ItemGroupNames.NCO_UNITS: 0,
|
||||
item_groups.ItemGroupNames.NCO_UNITS: -1,
|
||||
},
|
||||
'max_number_of_upgrades': 2,
|
||||
}
|
||||
@@ -81,10 +81,10 @@ class TestSupportedUseCases(Sc2SetupTestBase):
|
||||
},
|
||||
'mission_order': options.MissionOrder.option_vanilla_shuffled,
|
||||
'excluded_items': {
|
||||
item_groups.ItemGroupNames.TERRAN_ITEMS: 0,
|
||||
item_groups.ItemGroupNames.TERRAN_ITEMS: -1,
|
||||
},
|
||||
'unexcluded_items': {
|
||||
item_groups.ItemGroupNames.NCO_MAX_PROGRESSIVE_ITEMS: 0,
|
||||
item_groups.ItemGroupNames.NCO_MAX_PROGRESSIVE_ITEMS: -1,
|
||||
item_groups.ItemGroupNames.NCO_MIN_PROGRESSIVE_ITEMS: 1,
|
||||
},
|
||||
'excluded_missions': [
|
||||
@@ -398,7 +398,7 @@ class TestSupportedUseCases(Sc2SetupTestBase):
|
||||
|
||||
self.generate_world(world_options)
|
||||
world_item_names = [item.name for item in self.multiworld.itempool]
|
||||
spear_of_adun_actives = [item_name for item_name in world_item_names if item_name in item_tables.spear_of_adun_calldowns]
|
||||
spear_of_adun_actives = [item_name for item_name in world_item_names if item_name in item_groups.spear_of_adun_actives]
|
||||
|
||||
self.assertLessEqual(len(spear_of_adun_actives), target_number)
|
||||
|
||||
@@ -418,7 +418,9 @@ class TestSupportedUseCases(Sc2SetupTestBase):
|
||||
|
||||
self.generate_world(world_options)
|
||||
world_item_names = [item.name for item in self.multiworld.itempool]
|
||||
spear_of_adun_autocasts = [item_name for item_name in world_item_names if item_name in item_tables.spear_of_adun_castable_passives]
|
||||
spear_of_adun_autocasts = [
|
||||
item_name for item_name in world_item_names if item_name in item_groups.spear_of_adun_passives
|
||||
]
|
||||
|
||||
self.assertLessEqual(len(spear_of_adun_autocasts), target_number)
|
||||
|
||||
@@ -471,12 +473,12 @@ class TestSupportedUseCases(Sc2SetupTestBase):
|
||||
],
|
||||
'required_tactics': options.RequiredTactics.option_any_units,
|
||||
'excluded_items': {
|
||||
item_groups.ItemGroupNames.TERRAN_UNITS: 0,
|
||||
item_groups.ItemGroupNames.ZERG_UNITS: 0,
|
||||
item_groups.ItemGroupNames.TERRAN_UNITS: -1,
|
||||
item_groups.ItemGroupNames.ZERG_UNITS: -1,
|
||||
},
|
||||
'unexcluded_items': {
|
||||
item_groups.ItemGroupNames.TERRAN_MERCENARIES: 0,
|
||||
item_groups.ItemGroupNames.ZERG_MERCENARIES: 0,
|
||||
item_groups.ItemGroupNames.TERRAN_MERCENARIES: -1,
|
||||
item_groups.ItemGroupNames.ZERG_MERCENARIES: -1,
|
||||
},
|
||||
'start_inventory': {
|
||||
item_names.PROGRESSIVE_FAST_DELIVERY: 1,
|
||||
|
||||
@@ -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