Compare commits

..

1 Commits

Author SHA1 Message Date
NewSoupVi
9a351be44b The Witness: Rename "Panel Hunt Settings" to "Panel Hunt Options"
Who let me get away with this lmao
2024-11-26 21:17:24 +01:00
49 changed files with 184 additions and 126 deletions

View File

@@ -16,7 +16,7 @@
"reportMissingImports": true,
"reportMissingTypeStubs": true,
"pythonVersion": "3.10",
"pythonVersion": "3.8",
"pythonPlatform": "Windows",
"executionEnvironments": [

View File

@@ -53,7 +53,7 @@ jobs:
- uses: actions/setup-python@v5
if: env.diff != ''
with:
python-version: '3.10'
python-version: 3.8
- name: "Install dependencies"
if: env.diff != ''

View File

@@ -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-py38: # 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.8'
- 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

View File

@@ -33,11 +33,13 @@ jobs:
matrix:
os: [ubuntu-latest]
python:
- {version: '3.8'}
- {version: '3.9'}
- {version: '3.10'}
- {version: '3.11'}
- {version: '3.12'}
include:
- python: {version: '3.10'} # old compat
- python: {version: '3.8'} # win7 compat
os: windows-latest
- python: {version: '3.12'} # current
os: windows-latest

View File

@@ -1,16 +1,18 @@
from __future__ import annotations
import collections
import itertools
import functools
import logging
import random
import secrets
import typing # this can go away when Python 3.8 support is dropped
from argparse import Namespace
from collections import Counter, deque
from collections.abc import Collection, MutableSequence
from enum import IntEnum, IntFlag
from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Mapping, NamedTuple,
Optional, Protocol, Set, Tuple, Union, TYPE_CHECKING)
Optional, Protocol, Set, Tuple, Union, Type)
from typing_extensions import NotRequired, TypedDict
@@ -18,7 +20,7 @@ import NetUtils
import Options
import Utils
if TYPE_CHECKING:
if typing.TYPE_CHECKING:
from worlds import AutoWorld
@@ -229,7 +231,7 @@ class MultiWorld():
for player in self.player_ids:
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
self.worlds[player] = world_type(self, player)
options_dataclass: type[Options.PerGameCommonOptions] = world_type.options_dataclass
options_dataclass: typing.Type[Options.PerGameCommonOptions] = world_type.options_dataclass
self.worlds[player].options = options_dataclass(**{option_key: getattr(args, option_key)[player]
for option_key in options_dataclass.type_hints})
@@ -973,7 +975,7 @@ class Region:
entrances: List[Entrance]
exits: List[Entrance]
locations: List[Location]
entrance_type: ClassVar[type[Entrance]] = Entrance
entrance_type: ClassVar[Type[Entrance]] = Entrance
class Register(MutableSequence):
region_manager: MultiWorld.RegionManager
@@ -1073,7 +1075,7 @@ class Region:
return entrance.parent_region.get_connecting_entrance(is_main_entrance)
def add_locations(self, locations: Dict[str, Optional[int]],
location_type: Optional[type[Location]] = None) -> None:
location_type: Optional[Type[Location]] = None) -> None:
"""
Adds locations to the Region object, where location_type is your Location class and locations is a dict of
location names to address.

View File

@@ -5,8 +5,8 @@ import multiprocessing
import warnings
if sys.version_info < (3, 10, 11):
raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. 3.10.11+ is supported.")
if sys.version_info < (3, 8, 6):
raise RuntimeError("Incompatible Python Version. 3.8.7+ is supported.")
# don't run update if environment is frozen/compiled or if not the parent process (skip in subprocess)
_skip_update = bool(getattr(sys, "frozen", False) or multiprocessing.parent_process())

View File

@@ -19,7 +19,8 @@ import warnings
from argparse import Namespace
from settings import Settings, get_settings
from time import sleep
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union, TypeGuard
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union
from typing_extensions import TypeGuard
from yaml import load, load_all, dump
try:
@@ -47,7 +48,7 @@ class Version(typing.NamedTuple):
return ".".join(str(item) for item in self)
__version__ = "0.6.0"
__version__ = "0.5.1"
version_tuple = tuplize_version(__version__)
is_linux = sys.platform.startswith("linux")

View File

@@ -17,7 +17,7 @@ from Utils import get_file_safe_name
if typing.TYPE_CHECKING:
from flask import Flask
Utils.local_path.cached_path = os.path.dirname(__file__)
Utils.local_path.cached_path = os.path.dirname(__file__) or "." # py3.8 is not abs. remove "." when dropping 3.8
settings.no_gui = True
configpath = os.path.abspath("config.yaml")
if not os.path.exists(configpath): # fall back to config.yaml in home

View File

@@ -5,7 +5,9 @@ waitress>=3.0.0
Flask-Caching>=2.3.0
Flask-Compress>=1.15
Flask-Limiter>=3.8.0
bokeh>=3.5.2
bokeh>=3.1.1; python_version <= '3.8'
bokeh>=3.4.3; python_version == '3.9'
bokeh>=3.5.2; python_version >= '3.10'
markupsafe>=2.1.5
Markdown>=3.7
mdx-breakless-lists>=1.0.1

View File

@@ -53,7 +53,7 @@
<table class="range-rows" data-option="{{ option_name }}">
<tbody>
{{ RangeRow(option_name, option, option.range_start, option.range_start, True) }}
{% if option.default is number and option.range_start < option.default < option.range_end %}
{% if option.range_start < option.default < option.range_end %}
{{ RangeRow(option_name, option, option.default, option.default, True) }}
{% endif %}
{{ RangeRow(option_name, option, option.range_end, option.range_end, True) }}

View File

@@ -16,7 +16,7 @@ game contributions:
* **Do not introduce unit test failures/regressions.**
Archipelago supports multiple versions of Python. You may need to download older Python versions to fully test
your changes. Currently, the oldest supported version
is [Python 3.10](https://www.python.org/downloads/release/python-31015/).
is [Python 3.8](https://www.python.org/downloads/release/python-380/).
It is recommended that automated github actions are turned on in your fork to have github run unit tests after
pushing.
You can turn them on here:

View File

@@ -7,7 +7,7 @@ use that version. These steps are for developers or platforms without compiled r
## General
What you'll need:
* [Python 3.10.15 or newer](https://www.python.org/downloads/), not the Windows Store version
* [Python 3.8.7 or newer](https://www.python.org/downloads/), not the Windows Store version
* Python 3.12.x is currently the newest supported version
* pip: included in downloads from python.org, separate in many Linux distributions
* Matching C compiler

View File

@@ -12,7 +12,10 @@ if sys.platform == "win32":
# kivy 2.2.0 introduced DPI awareness on Windows, but it makes the UI enter an infinitely recursive re-layout
# by setting the application to not DPI Aware, Windows handles scaling the entire window on its own, ignoring kivy's
ctypes.windll.shcore.SetProcessDpiAwareness(0)
try:
ctypes.windll.shcore.SetProcessDpiAwareness(0)
except FileNotFoundError: # shcore may not be found on <= Windows 7
pass # TODO: remove silent except when Python 3.8 is phased out.
os.environ["KIVY_NO_CONSOLELOG"] = "1"
os.environ["KIVY_NO_FILELOG"] = "1"

View File

@@ -634,7 +634,7 @@ cx_Freeze.setup(
"excludes": ["numpy", "Cython", "PySide2", "PIL",
"pandas", "zstandard"],
"zip_include_packages": ["*"],
"zip_exclude_packages": ["worlds", "sc2"],
"zip_exclude_packages": ["worlds", "sc2", "orjson"], # TODO: remove orjson here once we drop py3.8 support
"include_files": [], # broken in cx 6.14.0, we use more special sauce now
"include_msvcr": False,
"replace_paths": ["*."],

View File

@@ -2,7 +2,9 @@
from __future__ import annotations
import abc
import logging
from typing import TYPE_CHECKING, ClassVar, Dict, Iterable, Tuple, Any, Optional, Union, TypeGuard
from typing import TYPE_CHECKING, ClassVar, Dict, Iterable, Tuple, Any, Optional, Union
from typing_extensions import TypeGuard
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components

View File

@@ -66,12 +66,19 @@ class WorldSource:
start = time.perf_counter()
if self.is_zip:
importer = zipimport.zipimporter(self.resolved_path)
spec = importer.find_spec(os.path.basename(self.path).rsplit(".", 1)[0])
assert spec, f"{self.path} is not a loadable module"
mod = importlib.util.module_from_spec(spec)
mod.__package__ = f"worlds.{mod.__package__}"
if hasattr(importer, "find_spec"): # new in Python 3.10
spec = importer.find_spec(os.path.basename(self.path).rsplit(".", 1)[0])
assert spec, f"{self.path} is not a loadable module"
mod = importlib.util.module_from_spec(spec)
else: # TODO: remove with 3.8 support
mod = importer.load_module(os.path.basename(self.path).rsplit(".", 1)[0])
if mod.__package__ is not None:
mod.__package__ = f"worlds.{mod.__package__}"
else:
# load_module does not populate package, we'll have to assume mod.__name__ is correct here
# probably safe to remove with 3.8 support
mod.__package__ = f"worlds.{mod.__name__}"
mod.__name__ = f"worlds.{mod.__name__}"
sys.modules[mod.__name__] = mod
with warnings.catch_warnings():

View File

@@ -9,7 +9,11 @@ import ast
import jinja2
from ast import unparse
try:
from ast import unparse
except ImportError:
# Py 3.8 and earlier compatibility module
from astunparse import unparse
from Utils import get_text_between

View File

@@ -0,0 +1 @@
astunparse>=1.6.3; python_version <= '3.8'

View File

@@ -1,5 +1,5 @@
import logging
from typing import Any, ClassVar, TextIO
from typing import Any, ClassVar, Dict, List, Optional, Set, TextIO
from BaseClasses import CollectionState, Entrance, Item, ItemClassification, MultiWorld, Tutorial
from Options import Accessibility
@@ -120,16 +120,16 @@ class MessengerWorld(World):
required_seals: int = 0
created_seals: int = 0
total_shards: int = 0
shop_prices: dict[str, int]
figurine_prices: dict[str, int]
_filler_items: list[str]
starting_portals: list[str]
plando_portals: list[str]
spoiler_portal_mapping: dict[str, str]
portal_mapping: list[int]
transitions: list[Entrance]
shop_prices: Dict[str, int]
figurine_prices: Dict[str, int]
_filler_items: List[str]
starting_portals: List[str]
plando_portals: List[str]
spoiler_portal_mapping: Dict[str, str]
portal_mapping: List[int]
transitions: List[Entrance]
reachable_locs: int = 0
filler: dict[str, int]
filler: Dict[str, int]
def generate_early(self) -> None:
if self.options.goal == Goal.option_power_seal_hunt:
@@ -178,7 +178,7 @@ class MessengerWorld(World):
for reg_name in sub_region]
for region in complex_regions:
region_name = region.name.removeprefix(f"{region.parent} - ")
region_name = region.name.replace(f"{region.parent} - ", "")
connection_data = CONNECTIONS[region.parent][region_name]
for exit_region in connection_data:
region.connect(self.multiworld.get_region(exit_region, self.player))
@@ -191,7 +191,7 @@ class MessengerWorld(World):
# create items that are always in the item pool
main_movement_items = ["Rope Dart", "Wingsuit"]
precollected_names = [item.name for item in self.multiworld.precollected_items[self.player]]
itempool: list[MessengerItem] = [
itempool: List[MessengerItem] = [
self.create_item(item)
for item in self.item_name_to_id
if item not in {
@@ -290,7 +290,7 @@ class MessengerWorld(World):
for portal, output in portal_info:
spoiler.set_entrance(f"{portal} Portal", output, "I can write anything I want here lmao", self.player)
def fill_slot_data(self) -> dict[str, Any]:
def fill_slot_data(self) -> Dict[str, Any]:
slot_data = {
"shop": {SHOP_ITEMS[item].internal_name: price for item, price in self.shop_prices.items()},
"figures": {FIGURINES[item].internal_name: price for item, price in self.figurine_prices.items()},
@@ -316,7 +316,7 @@ class MessengerWorld(World):
return self._filler_items.pop(0)
def create_item(self, name: str) -> MessengerItem:
item_id: int | None = self.item_name_to_id.get(name, None)
item_id: Optional[int] = self.item_name_to_id.get(name, None)
return MessengerItem(
name,
ItemClassification.progression if item_id is None else self.get_item_classification(name),
@@ -351,7 +351,7 @@ class MessengerWorld(World):
return ItemClassification.filler
@classmethod
def create_group(cls, multiworld: "MultiWorld", new_player_id: int, players: set[int]) -> World:
def create_group(cls, multiworld: "MultiWorld", new_player_id: int, players: Set[int]) -> World:
group = super().create_group(multiworld, new_player_id, players)
assert isinstance(group, MessengerWorld)

View File

@@ -5,7 +5,7 @@ import os.path
import subprocess
import urllib.request
from shutil import which
from typing import Any
from typing import Any, Optional
from zipfile import ZipFile
from Utils import open_file
@@ -17,7 +17,7 @@ from Utils import is_windows, messagebox, tuplize_version
MOD_URL = "https://api.github.com/repos/alwaysintreble/TheMessengerRandomizerModAP/releases/latest"
def ask_yes_no_cancel(title: str, text: str) -> bool | None:
def ask_yes_no_cancel(title: str, text: str) -> Optional[bool]:
"""
Wrapper for tkinter.messagebox.askyesnocancel, that creates a popup dialog box with yes, no, and cancel buttons.
@@ -33,6 +33,7 @@ def ask_yes_no_cancel(title: str, text: str) -> bool | None:
return ret
def launch_game(*args) -> None:
"""Check the game installation, then launch it"""
def courier_installed() -> bool:

View File

@@ -1,4 +1,6 @@
CONNECTIONS: dict[str, dict[str, list[str]]] = {
from typing import Dict, List
CONNECTIONS: Dict[str, Dict[str, List[str]]] = {
"Ninja Village": {
"Right": [
"Autumn Hills - Left",
@@ -638,7 +640,7 @@ CONNECTIONS: dict[str, dict[str, list[str]]] = {
},
}
RANDOMIZED_CONNECTIONS: dict[str, str] = {
RANDOMIZED_CONNECTIONS: Dict[str, str] = {
"Ninja Village - Right": "Autumn Hills - Left",
"Autumn Hills - Left": "Ninja Village - Right",
"Autumn Hills - Right": "Forlorn Temple - Left",
@@ -678,7 +680,7 @@ RANDOMIZED_CONNECTIONS: dict[str, str] = {
"Sunken Shrine - Left": "Howling Grotto - Bottom",
}
TRANSITIONS: list[str] = [
TRANSITIONS: List[str] = [
"Ninja Village - Right",
"Autumn Hills - Left",
"Autumn Hills - Right",

View File

@@ -2,7 +2,7 @@ from .shop import FIGURINES, SHOP_ITEMS
# items
# listing individual groups first for easy lookup
NOTES: list[str] = [
NOTES = [
"Key of Hope",
"Key of Chaos",
"Key of Courage",
@@ -11,7 +11,7 @@ NOTES: list[str] = [
"Key of Symbiosis",
]
PROG_ITEMS: list[str] = [
PROG_ITEMS = [
"Wingsuit",
"Rope Dart",
"Lightfoot Tabi",
@@ -28,18 +28,18 @@ PROG_ITEMS: list[str] = [
"Seashell",
]
PHOBEKINS: list[str] = [
PHOBEKINS = [
"Necro",
"Pyro",
"Claustro",
"Acro",
]
USEFUL_ITEMS: list[str] = [
USEFUL_ITEMS = [
"Windmill Shuriken",
]
FILLER: dict[str, int] = {
FILLER = {
"Time Shard": 5,
"Time Shard (10)": 10,
"Time Shard (50)": 20,
@@ -48,13 +48,13 @@ FILLER: dict[str, int] = {
"Time Shard (500)": 5,
}
TRAPS: dict[str, int] = {
TRAPS = {
"Teleport Trap": 5,
"Prophecy Trap": 10,
}
# item_name_to_id needs to be deterministic and match upstream
ALL_ITEMS: list[str] = [
ALL_ITEMS = [
*NOTES,
"Windmill Shuriken",
"Wingsuit",
@@ -83,7 +83,7 @@ ALL_ITEMS: list[str] = [
# locations
# the names of these don't actually matter, but using the upstream's names for now
# order must be exactly the same as upstream
ALWAYS_LOCATIONS: list[str] = [
ALWAYS_LOCATIONS = [
# notes
"Sunken Shrine - Key of Love",
"Corrupted Future - Key of Courage",
@@ -160,7 +160,7 @@ ALWAYS_LOCATIONS: list[str] = [
"Elemental Skylands Seal - Fire",
]
BOSS_LOCATIONS: list[str] = [
BOSS_LOCATIONS = [
"Autumn Hills - Leaf Golem",
"Catacombs - Ruxxtin",
"Howling Grotto - Emerald Golem",

View File

@@ -1,4 +1,5 @@
from dataclasses import dataclass
from typing import Dict
from schema import And, Optional, Or, Schema
@@ -166,7 +167,7 @@ class ShopPrices(Range):
default = 100
def planned_price(location: str) -> dict[Optional, Or]:
def planned_price(location: str) -> Dict[Optional, Or]:
return {
Optional(location): Or(
And(int, lambda n: n >= 0),

View File

@@ -1,5 +1,5 @@
from copy import deepcopy
from typing import TYPE_CHECKING
from typing import List, TYPE_CHECKING
from BaseClasses import CollectionState, PlandoOptions
from Options import PlandoConnection
@@ -8,7 +8,7 @@ if TYPE_CHECKING:
from . import MessengerWorld
PORTALS: list[str] = [
PORTALS = [
"Autumn Hills",
"Riviere Turquoise",
"Howling Grotto",
@@ -18,7 +18,7 @@ PORTALS: list[str] = [
]
SHOP_POINTS: dict[str, list[str]] = {
SHOP_POINTS = {
"Autumn Hills": [
"Climbing Claws",
"Hope Path",
@@ -113,7 +113,7 @@ SHOP_POINTS: dict[str, list[str]] = {
}
CHECKPOINTS: dict[str, list[str]] = {
CHECKPOINTS = {
"Autumn Hills": [
"Hope Latch",
"Key of Hope",
@@ -186,7 +186,7 @@ CHECKPOINTS: dict[str, list[str]] = {
}
REGION_ORDER: list[str] = [
REGION_ORDER = [
"Autumn Hills",
"Forlorn Temple",
"Catacombs",
@@ -228,7 +228,7 @@ def shuffle_portals(world: "MessengerWorld") -> None:
return parent
def handle_planned_portals(plando_connections: list[PlandoConnection]) -> None:
def handle_planned_portals(plando_connections: List[PlandoConnection]) -> None:
"""checks the provided plando connections for portals and connects them"""
nonlocal available_portals

View File

@@ -1,4 +1,7 @@
LOCATIONS: dict[str, list[str]] = {
from typing import Dict, List
LOCATIONS: Dict[str, List[str]] = {
"Ninja Village - Nest": [
"Ninja Village - Candle",
"Ninja Village - Astral Seed",
@@ -198,7 +201,7 @@ LOCATIONS: dict[str, list[str]] = {
}
SUB_REGIONS: dict[str, list[str]] = {
SUB_REGIONS: Dict[str, List[str]] = {
"Ninja Village": [
"Right",
],
@@ -382,7 +385,7 @@ SUB_REGIONS: dict[str, list[str]] = {
# order is slightly funky here for back compat
MEGA_SHARDS: dict[str, list[str]] = {
MEGA_SHARDS: Dict[str, List[str]] = {
"Autumn Hills - Lakeside Checkpoint": ["Autumn Hills Mega Shard"],
"Forlorn Temple - Outside Shop": ["Hidden Entrance Mega Shard"],
"Catacombs - Top Left": ["Catacombs Mega Shard"],
@@ -411,7 +414,7 @@ MEGA_SHARDS: dict[str, list[str]] = {
}
REGION_CONNECTIONS: dict[str, dict[str, str]] = {
REGION_CONNECTIONS: Dict[str, Dict[str, str]] = {
"Menu": {"Tower HQ": "Start Game"},
"Tower HQ": {
"Autumn Hills - Portal": "ToTHQ Autumn Hills Portal",
@@ -433,7 +436,7 @@ REGION_CONNECTIONS: dict[str, dict[str, str]] = {
# regions that don't have sub-regions
LEVELS: list[str] = [
LEVELS: List[str] = [
"Menu",
"Tower HQ",
"The Shop",

View File

@@ -1,4 +1,4 @@
from typing import TYPE_CHECKING
from typing import Dict, TYPE_CHECKING
from BaseClasses import CollectionState
from worlds.generic.Rules import CollectionRule, add_rule, allow_self_locking_items
@@ -12,9 +12,9 @@ if TYPE_CHECKING:
class MessengerRules:
player: int
world: "MessengerWorld"
connection_rules: dict[str, CollectionRule]
region_rules: dict[str, CollectionRule]
location_rules: dict[str, CollectionRule]
connection_rules: Dict[str, CollectionRule]
region_rules: Dict[str, CollectionRule]
location_rules: Dict[str, CollectionRule]
maximum_price: int
required_seals: int

View File

@@ -1,11 +1,11 @@
from typing import NamedTuple, TYPE_CHECKING
from typing import Dict, List, NamedTuple, Optional, Set, TYPE_CHECKING, Tuple, Union
if TYPE_CHECKING:
from . import MessengerWorld
else:
MessengerWorld = object
PROG_SHOP_ITEMS: list[str] = [
PROG_SHOP_ITEMS: List[str] = [
"Path of Resilience",
"Meditation",
"Strike of the Ninja",
@@ -14,7 +14,7 @@ PROG_SHOP_ITEMS: list[str] = [
"Aerobatics Warrior",
]
USEFUL_SHOP_ITEMS: list[str] = [
USEFUL_SHOP_ITEMS: List[str] = [
"Karuta Plates",
"Serendipitous Bodies",
"Kusari Jacket",
@@ -29,10 +29,10 @@ class ShopData(NamedTuple):
internal_name: str
min_price: int
max_price: int
prerequisite: str | set[str] | None = None
prerequisite: Optional[Union[str, Set[str]]] = None
SHOP_ITEMS: dict[str, ShopData] = {
SHOP_ITEMS: Dict[str, ShopData] = {
"Karuta Plates": ShopData("HP_UPGRADE_1", 20, 200),
"Serendipitous Bodies": ShopData("ENEMY_DROP_HP", 20, 300, "The Shop - Karuta Plates"),
"Path of Resilience": ShopData("DAMAGE_REDUCTION", 100, 500, "The Shop - Serendipitous Bodies"),
@@ -56,7 +56,7 @@ SHOP_ITEMS: dict[str, ShopData] = {
"Focused Power Sense": ShopData("POWER_SEAL_WORLD_MAP", 300, 600, "The Shop - Power Sense"),
}
FIGURINES: dict[str, ShopData] = {
FIGURINES: Dict[str, ShopData] = {
"Green Kappa Figurine": ShopData("GREEN_KAPPA", 100, 500),
"Blue Kappa Figurine": ShopData("BLUE_KAPPA", 100, 500),
"Ountarde Figurine": ShopData("OUNTARDE", 100, 500),
@@ -73,12 +73,12 @@ FIGURINES: dict[str, ShopData] = {
}
def shuffle_shop_prices(world: MessengerWorld) -> tuple[dict[str, int], dict[str, int]]:
def shuffle_shop_prices(world: MessengerWorld) -> Tuple[Dict[str, int], Dict[str, int]]:
shop_price_mod = world.options.shop_price.value
shop_price_planned = world.options.shop_price_plan
shop_prices: dict[str, int] = {}
figurine_prices: dict[str, int] = {}
shop_prices: Dict[str, int] = {}
figurine_prices: Dict[str, int] = {}
for item, price in shop_price_planned.value.items():
if not isinstance(price, int):
price = world.random.choices(list(price.keys()), weights=list(price.values()))[0]

View File

@@ -1,5 +1,5 @@
from functools import cached_property
from typing import TYPE_CHECKING
from typing import Optional, TYPE_CHECKING
from BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, Region
from .regions import LOCATIONS, MEGA_SHARDS
@@ -10,14 +10,14 @@ if TYPE_CHECKING:
class MessengerEntrance(Entrance):
world: "MessengerWorld | None" = None
world: Optional["MessengerWorld"] = None
class MessengerRegion(Region):
parent: str
entrance_type = MessengerEntrance
def __init__(self, name: str, world: "MessengerWorld", parent: str | None = None) -> None:
def __init__(self, name: str, world: "MessengerWorld", parent: Optional[str] = None) -> None:
super().__init__(name, world.player, world.multiworld)
self.parent = parent
locations = []
@@ -48,7 +48,7 @@ class MessengerRegion(Region):
class MessengerLocation(Location):
game = "The Messenger"
def __init__(self, player: int, name: str, loc_id: int | None, parent: MessengerRegion) -> None:
def __init__(self, player: int, name: str, loc_id: Optional[int], parent: MessengerRegion) -> None:
super().__init__(player, name, loc_id, parent)
if loc_id is None:
if name == "Rescue Phantom":
@@ -59,7 +59,7 @@ class MessengerLocation(Location):
class MessengerShopLocation(MessengerLocation):
@cached_property
def cost(self) -> int:
name = self.name.removeprefix("The Shop - ")
name = self.name.replace("The Shop - ", "") # TODO use `remove_prefix` when 3.8 finally gets dropped
world = self.parent_region.multiworld.worlds[self.player]
shop_data = SHOP_ITEMS[name]
if shop_data.prerequisite:

View File

@@ -77,7 +77,7 @@ class PlandoTest(MessengerTestBase):
loc = f"The Shop - {loc}"
self.assertLessEqual(price, self.multiworld.get_location(loc, self.player).cost)
self.assertTrue(loc.removeprefix("The Shop - ") in SHOP_ITEMS)
self.assertTrue(loc.replace("The Shop - ", "") in SHOP_ITEMS)
self.assertEqual(len(prices), len(SHOP_ITEMS))
figures = self.world.figurine_prices

View File

@@ -319,7 +319,7 @@ class StardewValleyWorld(World):
if override_classification is None:
override_classification = item.classification
if override_classification & ItemClassification.progression:
if override_classification == ItemClassification.progression:
self.total_progression_items += 1
return StardewItem(item.name, override_classification, item.code, self.player)

View File

@@ -1,12 +1,16 @@
from __future__ import annotations
from graphlib import TopologicalSorter
from typing import Iterable, Mapping, Callable
from .game_content import StardewContent, ContentPack, StardewFeatures
from .vanilla.base import base_game as base_game_content_pack
from ..data.game_item import GameItem, ItemSource
try:
from graphlib import TopologicalSorter
except ImportError:
from graphlib_backport import TopologicalSorter # noqa
def unpack_content(features: StardewFeatures, packs: Iterable[ContentPack]) -> StardewContent:
# Base game is always registered first.

View File

@@ -1,9 +1,9 @@
from dataclasses import dataclass
from .game_item import ItemSource
from .game_item import kw_only, ItemSource
@dataclass(frozen=True, kw_only=True)
@dataclass(frozen=True, **kw_only)
class MachineSource(ItemSource):
item: str # this should be optional (worm bin)
machine: str

View File

@@ -1,4 +1,5 @@
import enum
import sys
from abc import ABC
from dataclasses import dataclass, field
from types import MappingProxyType
@@ -6,6 +7,11 @@ from typing import List, Iterable, Set, ClassVar, Tuple, Mapping, Callable, Any
from ..stardew_rule.protocol import StardewRule
if sys.version_info >= (3, 10):
kw_only = {"kw_only": True}
else:
kw_only = {}
DEFAULT_REQUIREMENT_TAGS = MappingProxyType({})
@@ -30,17 +36,21 @@ class ItemTag(enum.Enum):
class ItemSource(ABC):
add_tags: ClassVar[Tuple[ItemTag]] = ()
other_requirements: Tuple[Requirement, ...] = field(kw_only=True, default_factory=tuple)
@property
def requirement_tags(self) -> Mapping[str, Tuple[ItemTag, ...]]:
return DEFAULT_REQUIREMENT_TAGS
# FIXME this should just be an optional field, but kw_only requires python 3.10...
@property
def other_requirements(self) -> Iterable[Requirement]:
return ()
@dataclass(frozen=True, kw_only=True)
@dataclass(frozen=True, **kw_only)
class GenericSource(ItemSource):
regions: Tuple[str, ...] = ()
"""No region means it's available everywhere."""
other_requirements: Tuple[Requirement, ...] = ()
@dataclass(frozen=True)
@@ -49,7 +59,7 @@ class CustomRuleSource(ItemSource):
create_rule: Callable[[Any], StardewRule]
@dataclass(frozen=True, kw_only=True)
@dataclass(frozen=True, **kw_only)
class CompoundSource(ItemSource):
sources: Tuple[ItemSource, ...] = ()

View File

@@ -1,17 +1,18 @@
from dataclasses import dataclass
from typing import Tuple, Sequence, Mapping
from .game_item import ItemSource, ItemTag
from .game_item import ItemSource, kw_only, ItemTag, Requirement
from ..strings.season_names import Season
@dataclass(frozen=True, kw_only=True)
@dataclass(frozen=True, **kw_only)
class ForagingSource(ItemSource):
regions: Tuple[str, ...]
seasons: Tuple[str, ...] = Season.all
other_requirements: Tuple[Requirement, ...] = ()
@dataclass(frozen=True, kw_only=True)
@dataclass(frozen=True, **kw_only)
class SeasonalForagingSource(ItemSource):
season: str
days: Sequence[int]
@@ -21,17 +22,17 @@ class SeasonalForagingSource(ItemSource):
return ForagingSource(seasons=(self.season,), regions=self.regions)
@dataclass(frozen=True, kw_only=True)
@dataclass(frozen=True, **kw_only)
class FruitBatsSource(ItemSource):
...
@dataclass(frozen=True, kw_only=True)
@dataclass(frozen=True, **kw_only)
class MushroomCaveSource(ItemSource):
...
@dataclass(frozen=True, kw_only=True)
@dataclass(frozen=True, **kw_only)
class HarvestFruitTreeSource(ItemSource):
add_tags = (ItemTag.CROPSANITY,)
@@ -45,7 +46,7 @@ class HarvestFruitTreeSource(ItemSource):
}
@dataclass(frozen=True, kw_only=True)
@dataclass(frozen=True, **kw_only)
class HarvestCropSource(ItemSource):
add_tags = (ItemTag.CROPSANITY,)
@@ -60,6 +61,6 @@ class HarvestCropSource(ItemSource):
}
@dataclass(frozen=True, kw_only=True)
@dataclass(frozen=True, **kw_only)
class ArtifactSpotSource(ItemSource):
amount: int

View File

@@ -7,7 +7,7 @@ id,name,classification,groups,mod_name
19,Glittering Boulder Removed,progression,COMMUNITY_REWARD,
20,Minecarts Repair,useful,COMMUNITY_REWARD,
21,Bus Repair,progression,COMMUNITY_REWARD,
22,Progressive Movie Theater,"progression,trap",COMMUNITY_REWARD,
22,Progressive Movie Theater,progression,COMMUNITY_REWARD,
23,Stardrop,progression,,
24,Progressive Backpack,progression,,
25,Rusty Sword,filler,"WEAPON,DEPRECATED",
1 id name classification groups mod_name
7 19 Glittering Boulder Removed progression COMMUNITY_REWARD
8 20 Minecarts Repair useful COMMUNITY_REWARD
9 21 Bus Repair progression COMMUNITY_REWARD
10 22 Progressive Movie Theater progression,trap progression COMMUNITY_REWARD
11 23 Stardrop progression
12 24 Progressive Backpack progression
13 25 Rusty Sword filler WEAPON,DEPRECATED

View File

@@ -1,39 +1,40 @@
from dataclasses import dataclass
from typing import Tuple, Optional
from .game_item import ItemSource
from .game_item import ItemSource, kw_only, Requirement
from ..strings.season_names import Season
ItemPrice = Tuple[int, str]
@dataclass(frozen=True, kw_only=True)
@dataclass(frozen=True, **kw_only)
class ShopSource(ItemSource):
shop_region: str
money_price: Optional[int] = None
items_price: Optional[Tuple[ItemPrice, ...]] = None
seasons: Tuple[str, ...] = Season.all
other_requirements: Tuple[Requirement, ...] = ()
def __post_init__(self):
assert self.money_price is not None or self.items_price is not None, "At least money price or items price need to be defined."
assert self.items_price is None or all(isinstance(p, tuple) for p in self.items_price), "Items price should be a tuple."
@dataclass(frozen=True, kw_only=True)
@dataclass(frozen=True, **kw_only)
class MysteryBoxSource(ItemSource):
amount: int
@dataclass(frozen=True, kw_only=True)
@dataclass(frozen=True, **kw_only)
class ArtifactTroveSource(ItemSource):
amount: int
@dataclass(frozen=True, kw_only=True)
@dataclass(frozen=True, **kw_only)
class PrizeMachineSource(ItemSource):
amount: int
@dataclass(frozen=True, kw_only=True)
@dataclass(frozen=True, **kw_only)
class FishingTreasureChestSource(ItemSource):
amount: int

View File

@@ -1,7 +1,9 @@
from dataclasses import dataclass, field
from ..data.game_item import kw_only
@dataclass(frozen=True)
class Skill:
name: str
has_mastery: bool = field(kw_only=True)
has_mastery: bool = field(**kw_only)

View File

@@ -138,7 +138,7 @@ This means that, for these specific mods, if you decide to include them in your
with the assumption that you will install and play with these mods. The multiworld will contain related items and locations
for these mods, the specifics will vary from mod to mod
[Supported Mods Documentation](https://github.com/agilbert1412/StardewArchipelago/blob/6.x.x/Documentation/Supported%20Mods.md)
[Supported Mods Documentation](https://github.com/agilbert1412/StardewArchipelago/blob/5.x.x/Documentation/Supported%20Mods.md)
List of supported mods:

View File

@@ -12,7 +12,7 @@
- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases)
* (Only for the TextClient)
- Other Stardew Valley Mods [Nexus Mods](https://www.nexusmods.com/stardewvalley)
* There are [supported mods](https://github.com/agilbert1412/StardewArchipelago/blob/6.x.x/Documentation/Supported%20Mods.md)
* There are [supported mods](https://github.com/agilbert1412/StardewArchipelago/blob/5.x.x/Documentation/Supported%20Mods.md)
that you can add to your yaml to include them with the Archipelago randomization
* It is **not** recommended to further mod Stardew Valley with unsupported mods, although it is possible to do so.

View File

@@ -2,7 +2,6 @@ import csv
import enum
import logging
from dataclasses import dataclass, field
from functools import reduce
from pathlib import Path
from random import Random
from typing import Dict, List, Protocol, Union, Set, Optional
@@ -125,14 +124,17 @@ class StardewItemDeleter(Protocol):
def load_item_csv():
from importlib.resources import files
try:
from importlib.resources import files
except ImportError:
from importlib_resources import files # noqa
items = []
with files(data).joinpath("items.csv").open() as file:
item_reader = csv.DictReader(file)
for item in item_reader:
id = int(item["id"]) if item["id"] else None
classification = reduce((lambda a, b: a | b), {ItemClassification[str_classification] for str_classification in item["classification"].split(",")})
classification = ItemClassification[item["classification"]]
groups = {Group[group] for group in item["groups"].split(",") if group}
mod_name = str(item["mod_name"]) if item["mod_name"] else None
items.append(ItemData(id, item["name"], classification, mod_name, groups))

View File

@@ -130,7 +130,10 @@ class StardewLocationCollector(Protocol):
def load_location_csv() -> List[LocationData]:
from importlib.resources import files
try:
from importlib.resources import files
except ImportError:
from importlib_resources import files
with files(data).joinpath("locations.csv").open() as file:
reader = csv.DictReader(file)

View File

@@ -0,0 +1,2 @@
importlib_resources; python_version <= '3.8'
graphlib_backport; python_version <= '3.8'

View File

@@ -35,7 +35,7 @@ class TestBaseItemGeneration(SVTestBase):
items_to_ignore.extend(baby.name for baby in items.items_by_group[Group.BABY])
items_to_ignore.extend(resource_pack.name for resource_pack in items.items_by_group[Group.RESOURCE_PACK])
items_to_ignore.append("The Gateway Gazette")
progression_items = [item for item in items.all_items if item.classification & ItemClassification.progression and item.name not in items_to_ignore]
progression_items = [item for item in items.all_items if item.classification is ItemClassification.progression and item.name not in items_to_ignore]
for progression_item in progression_items:
with self.subTest(f"{progression_item.name}"):
self.assertIn(progression_item.name, all_created_items)
@@ -86,7 +86,7 @@ class TestNoGingerIslandItemGeneration(SVTestBase):
items_to_ignore.extend(season.name for season in items.items_by_group[Group.WEAPON])
items_to_ignore.extend(baby.name for baby in items.items_by_group[Group.BABY])
items_to_ignore.append("The Gateway Gazette")
progression_items = [item for item in items.all_items if item.classification & ItemClassification.progression and item.name not in items_to_ignore]
progression_items = [item for item in items.all_items if item.classification is ItemClassification.progression and item.name not in items_to_ignore]
for progression_item in progression_items:
with self.subTest(f"{progression_item.name}"):
if Group.GINGER_ISLAND in progression_item.groups:

View File

@@ -306,7 +306,7 @@ class SVTestBase(RuleAssertMixin, WorldTestBase, SVTestCase):
def create_item(self, item: str) -> StardewItem:
created_item = self.world.create_item(item)
if created_item.classification & ItemClassification.progression:
if created_item.classification == ItemClassification.progression:
self.multiworld.worlds[self.player].total_progression_items -= 1
return created_item

View File

@@ -75,7 +75,7 @@ class TestBaseItemGeneration(SVTestBase):
items_to_ignore.extend(baby.name for baby in items.items_by_group[Group.BABY])
items_to_ignore.extend(resource_pack.name for resource_pack in items.items_by_group[Group.RESOURCE_PACK])
items_to_ignore.append("The Gateway Gazette")
progression_items = [item for item in items.all_items if item.classification & ItemClassification.progression
progression_items = [item for item in items.all_items if item.classification is ItemClassification.progression
and item.name not in items_to_ignore]
for progression_item in progression_items:
with self.subTest(f"{progression_item.name}"):
@@ -105,7 +105,7 @@ class TestNoGingerIslandModItemGeneration(SVTestBase):
items_to_ignore.extend(baby.name for baby in items.items_by_group[Group.BABY])
items_to_ignore.extend(resource_pack.name for resource_pack in items.items_by_group[Group.RESOURCE_PACK])
items_to_ignore.append("The Gateway Gazette")
progression_items = [item for item in items.all_items if item.classification & ItemClassification.progression
progression_items = [item for item in items.all_items if item.classification is ItemClassification.progression
and item.name not in items_to_ignore]
for progression_item in progression_items:
with self.subTest(f"{progression_item.name}"):

View File

@@ -8,5 +8,5 @@ class TestHasProgressionPercent(unittest.TestCase):
def test_max_item_amount_is_full_collection(self):
# Not caching because it fails too often for some reason
with solo_multiworld(world_caching=False) as (multiworld, world):
progression_item_count = sum(1 for i in multiworld.get_items() if i.classification & ItemClassification.progression)
progression_item_count = sum(1 for i in multiworld.get_items() if ItemClassification.progression in i.classification)
self.assertEqual(world.total_progression_items, progression_item_count - 1) # -1 to skip Victory

View File

@@ -12,6 +12,8 @@ BYTES_TO_REMOVE = 4
# <function Location.<lambda> at 0x102ca98a0>
lambda_regex = re.compile(r"^<function Location\.<lambda> at (.*)>$")
# Python 3.10.2\r\n
python_version_regex = re.compile(r"^Python (\d+)\.(\d+)\.(\d+)\s*$")
class TestGenerationIsStable(SVTestCase):

View File

@@ -406,7 +406,6 @@ class PuzzleRandomizationSeed(Range):
Sigma Rando, which is the basis for all puzzle randomization in this randomizer, uses a seed from 1 to 9999999 for the puzzle randomization.
This option lets you set this seed yourself.
"""
display_name = "Puzzle Randomization Seed"
range_start = 1
range_end = 9999999
default = "random"

View File

@@ -1,6 +1,7 @@
from collections import Counter
from dataclasses import dataclass
from typing import ClassVar, Dict, Literal, Tuple, TypeGuard
from typing import ClassVar, Dict, Literal, Tuple
from typing_extensions import TypeGuard # remove when Python >= 3.10
from Options import Choice, DefaultOnToggle, NamedRange, OptionGroup, PerGameCommonOptions, Range, Removed, Toggle