Skip to content

Commit

Permalink
Merge branch 'main' into patch-1
Browse files Browse the repository at this point in the history
  • Loading branch information
ScipioWright authored Nov 29, 2024
2 parents a636d3c + 492e3a3 commit b0df227
Show file tree
Hide file tree
Showing 57 changed files with 1,201 additions and 352 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
worlds/blasphemous/region_data.py linguist-generated=true
worlds/yachtdice/YachtWeights.py linguist-generated=true
8 changes: 4 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ env:
jobs:
# build-release-macos: # LF volunteer

build-win-py310: # RCs will still be built and signed by hand
build-win: # RCs will still be built and signed by hand
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Install python
uses: actions/setup-python@v5
with:
python-version: '3.10'
python-version: '3.12'
- name: Download run-time dependencies
run: |
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip
Expand Down Expand Up @@ -111,10 +111,10 @@ jobs:
- name: Get a recent python
uses: actions/setup-python@v5
with:
python-version: '3.11'
python-version: '3.12'
- name: Install build-time dependencies
run: |
echo "PYTHON=python3.11" >> $GITHUB_ENV
echo "PYTHON=python3.12" >> $GITHUB_ENV
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
chmod a+rx appimagetool-x86_64.AppImage
./appimagetool-x86_64.AppImage --appimage-extract
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ jobs:
- name: Get a recent python
uses: actions/setup-python@v5
with:
python-version: '3.11'
python-version: '3.12'
- name: Install build-time dependencies
run: |
echo "PYTHON=python3.11" >> $GITHUB_ENV
echo "PYTHON=python3.12" >> $GITHUB_ENV
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
chmod a+rx appimagetool-x86_64.AppImage
./appimagetool-x86_64.AppImage --appimage-extract
Expand Down
2 changes: 1 addition & 1 deletion BaseClasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -1545,7 +1545,7 @@ def write_option(option_key: str, option_obj: Options.AssembleOptions) -> None:
[f" {location}: {item}" for (location, item) in sphere.items()] if isinstance(sphere, dict) else
[f" {item}" for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()]))
if self.unreachables:
outfile.write('\n\nUnreachable Items:\n\n')
outfile.write('\n\nUnreachable Progression Items:\n\n')
outfile.write(
'\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables]))

Expand Down
67 changes: 30 additions & 37 deletions Main.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,45 +153,38 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No

# remove starting inventory from pool items.
# Because some worlds don't actually create items during create_items this has to be as late as possible.
if any(getattr(multiworld.worlds[player].options, "start_inventory_from_pool", None) for player in multiworld.player_ids):
new_items: List[Item] = []
old_items: List[Item] = []
depletion_pool: Dict[int, Dict[str, int]] = {
player: getattr(multiworld.worlds[player].options,
"start_inventory_from_pool",
StartInventoryPool({})).value.copy()
for player in multiworld.player_ids
}
for player, items in depletion_pool.items():
player_world: AutoWorld.World = multiworld.worlds[player]
for count in items.values():
for _ in range(count):
new_items.append(player_world.create_filler())
target: int = sum(sum(items.values()) for items in depletion_pool.values())
for i, item in enumerate(multiworld.itempool):
fallback_inventory = StartInventoryPool({})
depletion_pool: Dict[int, Dict[str, int]] = {
player: getattr(multiworld.worlds[player].options, "start_inventory_from_pool", fallback_inventory).value.copy()
for player in multiworld.player_ids
}
target_per_player = {
player: sum(target_items.values()) for player, target_items in depletion_pool.items() if target_items
}

if target_per_player:
new_itempool: List[Item] = []

# Make new itempool with start_inventory_from_pool items removed
for item in multiworld.itempool:
if depletion_pool[item.player].get(item.name, 0):
target -= 1
depletion_pool[item.player][item.name] -= 1
# quick abort if we have found all items
if not target:
old_items.extend(multiworld.itempool[i+1:])
break
else:
old_items.append(item)

# leftovers?
if target:
for player, remaining_items in depletion_pool.items():
remaining_items = {name: count for name, count in remaining_items.items() if count}
if remaining_items:
logger.warning(f"{multiworld.get_player_name(player)}"
f" is trying to remove items from their pool that don't exist: {remaining_items}")
# find all filler we generated for the current player and remove until it matches
removables = [item for item in new_items if item.player == player]
for _ in range(sum(remaining_items.values())):
new_items.remove(removables.pop())
assert len(multiworld.itempool) == len(new_items + old_items), "Item Pool amounts should not change."
multiworld.itempool[:] = new_items + old_items
new_itempool.append(item)

# Create filler in place of the removed items, warn if any items couldn't be found in the multiworld itempool
for player, target in target_per_player.items():
unfound_items = {item: count for item, count in depletion_pool[player].items() if count}

if unfound_items:
player_name = multiworld.get_player_name(player)
logger.warning(f"{player_name} tried to remove items from their pool that don't exist: {unfound_items}")

needed_items = target_per_player[player] - sum(unfound_items.values())
new_itempool += [multiworld.worlds[player].create_filler() for _ in range(needed_items)]

assert len(multiworld.itempool) == len(new_itempool), "Item Pool amounts should not change."
multiworld.itempool[:] = new_itempool

multiworld.link_items()

Expand Down Expand Up @@ -289,7 +282,7 @@ def precollect_hint(location):
if type(location.address) == int:
assert location.item.code is not None, "item code None should be event, " \
"location.address should then also be None. Location: " \
f" {location}"
f" {location}, Item: {location.item}"
assert location.address not in locations_data[location.player], (
f"Locations with duplicate address. {location} and "
f"{locations_data[location.player][location.address]}")
Expand Down
34 changes: 19 additions & 15 deletions Options.py
Original file line number Diff line number Diff line change
Expand Up @@ -1463,22 +1463,26 @@ class OptionGroup(typing.NamedTuple):
def get_option_groups(world: typing.Type[World], visibility_level: Visibility = Visibility.template) -> typing.Dict[
str, typing.Dict[str, typing.Type[Option[typing.Any]]]]:
"""Generates and returns a dictionary for the option groups of a specified world."""
option_groups = {option: option_group.name
for option_group in world.web.option_groups
for option in option_group.options}
option_to_name = {option: option_name for option_name, option in world.options_dataclass.type_hints.items()}

ordered_groups = {group.name: group.options for group in world.web.option_groups}

# add a default option group for uncategorized options to get thrown into
ordered_groups = ["Game Options"]
[ordered_groups.append(group) for group in option_groups.values() if group not in ordered_groups]
grouped_options = {group: {} for group in ordered_groups}
for option_name, option in world.options_dataclass.type_hints.items():
if visibility_level & option.visibility:
grouped_options[option_groups.get(option, "Game Options")][option_name] = option

# if the world doesn't have any ungrouped options, this group will be empty so just remove it
if not grouped_options["Game Options"]:
del grouped_options["Game Options"]

return grouped_options
if "Game Options" not in ordered_groups:
grouped_options = set(option for group in ordered_groups.values() for option in group)
ungrouped_options = [option for option in option_to_name if option not in grouped_options]
# only add the game options group if we have ungrouped options
if ungrouped_options:
ordered_groups = {**{"Game Options": ungrouped_options}, **ordered_groups}

return {
group: {
option_to_name[option]: option
for option in group_options
if (visibility_level in option.visibility and option in option_to_name)
}
for group, group_options in ordered_groups.items()
}


def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True) -> None:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ Currently, the following games are supported:
* Kingdom Hearts 1
* Mega Man 2
* Yacht Dice
* Faxanadu

For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
Expand Down
10 changes: 4 additions & 6 deletions Utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -858,11 +858,10 @@ def async_start(co: Coroutine[None, None, typing.Any], name: Optional[str] = Non
task.add_done_callback(_faf_tasks.discard)


def deprecate(message: str):
def deprecate(message: str, add_stacklevels: int = 0):
if __debug__:
raise Exception(message)
import warnings
warnings.warn(message)
warnings.warn(message, stacklevel=2 + add_stacklevels)


class DeprecateDict(dict):
Expand All @@ -876,10 +875,9 @@ def __init__(self, message: str, error: bool = False) -> None:

def __getitem__(self, item: Any) -> Any:
if self.should_error:
deprecate(self.log_message)
deprecate(self.log_message, add_stacklevels=1)
elif __debug__:
import warnings
warnings.warn(self.log_message)
warnings.warn(self.log_message, stacklevel=2)
return super().__getitem__(item)


Expand Down
3 changes: 0 additions & 3 deletions WebHostLib/templates/templates.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@
{% include 'header/grassHeader.html' %}
<title>Option Templates (YAML)</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.1/showdown.min.js"
integrity="sha512-L03kznCrNOfVxOUovR6ESfCz9Gfny7gihUX/huVbQB9zjODtYpxaVtIaAkpetoiyV2eqWbvxMH9fiSv5enX7bw=="
crossorigin="anonymous"></script>
{% endblock %}

{% block body %}
Expand Down
9 changes: 6 additions & 3 deletions docs/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -55,19 +55,22 @@
/worlds/dlcquest/ @axe-y @agilbert1412

# DOOM 1993
/worlds/doom_1993/ @Daivuk
/worlds/doom_1993/ @Daivuk @KScl

# DOOM II
/worlds/doom_ii/ @Daivuk
/worlds/doom_ii/ @Daivuk @KScl

# Factorio
/worlds/factorio/ @Berserker66

# Faxanadu
/worlds/faxanadu/ @Daivuk

# Final Fantasy Mystic Quest
/worlds/ffmq/ @Alchav @wildham0

# Heretic
/worlds/heretic/ @Daivuk
/worlds/heretic/ @Daivuk @KScl

# Hollow Knight
/worlds/hk/ @BadMagic100 @qwint
Expand Down
10 changes: 8 additions & 2 deletions settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import os.path
import shutil
import sys
import types
import typing
import warnings
from enum import IntEnum
Expand Down Expand Up @@ -162,8 +163,13 @@ def update(self, dct: Dict[str, Any]) -> None:
else:
# assign value, try to upcast to type hint
annotation = self.get_type_hints().get(k, None)
candidates = [] if annotation is None else \
typing.get_args(annotation) if typing.get_origin(annotation) is Union else [annotation]
candidates = (
[] if annotation is None else (
typing.get_args(annotation)
if typing.get_origin(annotation) in (Union, types.UnionType)
else [annotation]
)
)
none_type = type(None)
for cls in candidates:
assert isinstance(cls, type), f"{self.__class__.__name__}.{k}: type {cls} not supported in settings"
Expand Down
18 changes: 18 additions & 0 deletions test/general/test_items.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,21 @@ def test_itempool_not_modified(self):
call_all(multiworld, step)
self.assertEqual(created_items, multiworld.itempool,
f"{game_name} modified the itempool during {step}")

def test_locality_not_modified(self):
"""Test that worlds don't modify the locality of items after duplicates are resolved"""
gen_steps = ("generate_early", "create_regions", "create_items")
additional_steps = ("set_rules", "generate_basic", "pre_fill")
worlds_to_test = {game: world for game, world in AutoWorldRegister.world_types.items()}
for game_name, world_type in worlds_to_test.items():
with self.subTest("Game", game=game_name):
multiworld = setup_solo_multiworld(world_type, gen_steps)
local_items = multiworld.worlds[1].options.local_items.value.copy()
non_local_items = multiworld.worlds[1].options.non_local_items.value.copy()
for step in additional_steps:
with self.subTest("step", step=step):
call_all(multiworld, step)
self.assertEqual(local_items, multiworld.worlds[1].options.local_items.value,
f"{game_name} modified local_items during {step}")
self.assertEqual(non_local_items, multiworld.worlds[1].options.non_local_items.value,
f"{game_name} modified non_local_items during {step}")
16 changes: 16 additions & 0 deletions test/general/test_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from unittest import TestCase

from settings import Group
from worlds.AutoWorld import AutoWorldRegister


class TestSettings(TestCase):
def test_settings_can_update(self) -> None:
"""
Test that world settings can update.
"""
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=game_name):
if world_type.settings is not None:
assert isinstance(world_type.settings, Group)
world_type.settings.update({}) # a previous bug had a crash in this call to update
5 changes: 4 additions & 1 deletion worlds/AutoWorld.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ def settings(cls) -> Any: # actual type is defined in World
# lazy loading + caching to minimize runtime cost
if cls.__settings is None:
from settings import get_settings
cls.__settings = get_settings()[cls.settings_key]
try:
cls.__settings = get_settings()[cls.settings_key]
except AttributeError:
return None
return cls.__settings

def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoWorldRegister:
Expand Down
2 changes: 1 addition & 1 deletion worlds/doom_1993/Options.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ class StartWithComputerAreaMaps(Toggle):
class ResetLevelOnDeath(DefaultOnToggle):
"""When dying, levels are reset and monsters respawned. But inventory and checks are kept.
Turning this setting off is considered easy mode. Good for new players that don't know the levels well."""
display_name="Reset Level on Death"
display_name = "Reset Level on Death"


class Episode1(DefaultOnToggle):
Expand Down
2 changes: 1 addition & 1 deletion worlds/doom_ii/Options.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ class StartWithComputerAreaMaps(Toggle):
class ResetLevelOnDeath(DefaultOnToggle):
"""When dying, levels are reset and monsters respawned. But inventory and checks are kept.
Turning this setting off is considered easy mode. Good for new players that don't know the levels well."""
display_message="Reset level on death"
display_name = "Reset Level on Death"


class Episode1(DefaultOnToggle):
Expand Down
Loading

0 comments on commit b0df227

Please sign in to comment.