Core: purge py3.8 and py3.9 (#3973)
Co-authored-by: Remy Jette <remy@remyjette.com> Co-authored-by: Jouramie <16137441+Jouramie@users.noreply.github.com> Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
This commit is contained in:
parent
6c939d2d59
commit
334781e976
|
@ -16,7 +16,7 @@
|
|||
"reportMissingImports": true,
|
||||
"reportMissingTypeStubs": true,
|
||||
|
||||
"pythonVersion": "3.8",
|
||||
"pythonVersion": "3.10",
|
||||
"pythonPlatform": "Windows",
|
||||
|
||||
"executionEnvironments": [
|
||||
|
|
|
@ -53,7 +53,7 @@ jobs:
|
|||
- uses: actions/setup-python@v5
|
||||
if: env.diff != ''
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: '3.10'
|
||||
|
||||
- name: "Install dependencies"
|
||||
if: env.diff != ''
|
||||
|
|
|
@ -24,14 +24,14 @@ env:
|
|||
jobs:
|
||||
# build-release-macos: # LF volunteer
|
||||
|
||||
build-win-py38: # RCs will still be built and signed by hand
|
||||
build-win-py310: # 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.8'
|
||||
python-version: '3.10'
|
||||
- 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
|
||||
|
|
|
@ -33,13 +33,11 @@ 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.8'} # win7 compat
|
||||
- python: {version: '3.10'} # old compat
|
||||
os: windows-latest
|
||||
- python: {version: '3.12'} # current
|
||||
os: windows-latest
|
||||
|
|
|
@ -1,18 +1,16 @@
|
|||
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)
|
||||
Optional, Protocol, Set, Tuple, Union, TYPE_CHECKING)
|
||||
|
||||
from typing_extensions import NotRequired, TypedDict
|
||||
|
||||
|
@ -20,7 +18,7 @@ import NetUtils
|
|||
import Options
|
||||
import Utils
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
if TYPE_CHECKING:
|
||||
from worlds import AutoWorld
|
||||
|
||||
|
||||
|
@ -231,7 +229,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: typing.Type[Options.PerGameCommonOptions] = world_type.options_dataclass
|
||||
options_dataclass: 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})
|
||||
|
||||
|
@ -975,7 +973,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
|
||||
|
@ -1075,7 +1073,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.
|
||||
|
|
|
@ -5,8 +5,8 @@ import multiprocessing
|
|||
import warnings
|
||||
|
||||
|
||||
if sys.version_info < (3, 8, 6):
|
||||
raise RuntimeError("Incompatible Python Version. 3.8.7+ is supported.")
|
||||
if sys.version_info < (3, 10, 11):
|
||||
raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. 3.10.11+ 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())
|
||||
|
|
5
Utils.py
5
Utils.py
|
@ -19,8 +19,7 @@ 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
|
||||
from typing_extensions import TypeGuard
|
||||
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union, TypeGuard
|
||||
from yaml import load, load_all, dump
|
||||
|
||||
try:
|
||||
|
@ -48,7 +47,7 @@ class Version(typing.NamedTuple):
|
|||
return ".".join(str(item) for item in self)
|
||||
|
||||
|
||||
__version__ = "0.5.1"
|
||||
__version__ = "0.6.0"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
is_linux = sys.platform.startswith("linux")
|
||||
|
|
|
@ -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__) or "." # py3.8 is not abs. remove "." when dropping 3.8
|
||||
Utils.local_path.cached_path = os.path.dirname(__file__)
|
||||
settings.no_gui = True
|
||||
configpath = os.path.abspath("config.yaml")
|
||||
if not os.path.exists(configpath): # fall back to config.yaml in home
|
||||
|
|
|
@ -5,9 +5,7 @@ waitress>=3.0.0
|
|||
Flask-Caching>=2.3.0
|
||||
Flask-Compress>=1.15
|
||||
Flask-Limiter>=3.8.0
|
||||
bokeh>=3.1.1; python_version <= '3.8'
|
||||
bokeh>=3.4.3; python_version == '3.9'
|
||||
bokeh>=3.5.2; python_version >= '3.10'
|
||||
bokeh>=3.5.2
|
||||
markupsafe>=2.1.5
|
||||
Markdown>=3.7
|
||||
mdx-breakless-lists>=1.0.1
|
||||
|
|
|
@ -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.8](https://www.python.org/downloads/release/python-380/).
|
||||
is [Python 3.10](https://www.python.org/downloads/release/python-31015/).
|
||||
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:
|
||||
|
|
|
@ -7,7 +7,7 @@ use that version. These steps are for developers or platforms without compiled r
|
|||
## General
|
||||
|
||||
What you'll need:
|
||||
* [Python 3.8.7 or newer](https://www.python.org/downloads/), not the Windows Store version
|
||||
* [Python 3.10.15 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
|
||||
|
|
5
kvui.py
5
kvui.py
|
@ -12,10 +12,7 @@ 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
|
||||
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.
|
||||
ctypes.windll.shcore.SetProcessDpiAwareness(0)
|
||||
|
||||
os.environ["KIVY_NO_CONSOLELOG"] = "1"
|
||||
os.environ["KIVY_NO_FILELOG"] = "1"
|
||||
|
|
2
setup.py
2
setup.py
|
@ -634,7 +634,7 @@ cx_Freeze.setup(
|
|||
"excludes": ["numpy", "Cython", "PySide2", "PIL",
|
||||
"pandas", "zstandard"],
|
||||
"zip_include_packages": ["*"],
|
||||
"zip_exclude_packages": ["worlds", "sc2", "orjson"], # TODO: remove orjson here once we drop py3.8 support
|
||||
"zip_exclude_packages": ["worlds", "sc2"],
|
||||
"include_files": [], # broken in cx 6.14.0, we use more special sauce now
|
||||
"include_msvcr": False,
|
||||
"replace_paths": ["*."],
|
||||
|
|
|
@ -2,9 +2,7 @@
|
|||
from __future__ import annotations
|
||||
import abc
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, ClassVar, Dict, Iterable, Tuple, Any, Optional, Union
|
||||
|
||||
from typing_extensions import TypeGuard
|
||||
from typing import TYPE_CHECKING, ClassVar, Dict, Iterable, Tuple, Any, Optional, Union, TypeGuard
|
||||
|
||||
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components
|
||||
|
||||
|
|
|
@ -66,19 +66,12 @@ class WorldSource:
|
|||
start = time.perf_counter()
|
||||
if self.is_zip:
|
||||
importer = zipimport.zipimporter(self.resolved_path)
|
||||
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])
|
||||
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 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():
|
||||
|
|
|
@ -9,11 +9,7 @@ import ast
|
|||
|
||||
import jinja2
|
||||
|
||||
try:
|
||||
from ast import unparse
|
||||
except ImportError:
|
||||
# Py 3.8 and earlier compatibility module
|
||||
from astunparse import unparse
|
||||
from ast import unparse
|
||||
|
||||
from Utils import get_text_between
|
||||
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
astunparse>=1.6.3; python_version <= '3.8'
|
|
@ -1,5 +1,5 @@
|
|||
import logging
|
||||
from typing import Any, ClassVar, Dict, List, Optional, Set, TextIO
|
||||
from typing import Any, ClassVar, 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.replace(f"{region.parent} - ", "")
|
||||
region_name = region.name.removeprefix(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: Optional[int] = self.item_name_to_id.get(name, None)
|
||||
item_id: int | None = 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)
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import os.path
|
|||
import subprocess
|
||||
import urllib.request
|
||||
from shutil import which
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
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) -> Optional[bool]:
|
||||
def ask_yes_no_cancel(title: str, text: str) -> bool | None:
|
||||
"""
|
||||
Wrapper for tkinter.messagebox.askyesnocancel, that creates a popup dialog box with yes, no, and cancel buttons.
|
||||
|
||||
|
@ -33,7 +33,6 @@ def ask_yes_no_cancel(title: str, text: str) -> Optional[bool]:
|
|||
return ret
|
||||
|
||||
|
||||
|
||||
def launch_game(*args) -> None:
|
||||
"""Check the game installation, then launch it"""
|
||||
def courier_installed() -> bool:
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
from typing import Dict, List
|
||||
|
||||
CONNECTIONS: Dict[str, Dict[str, List[str]]] = {
|
||||
CONNECTIONS: dict[str, dict[str, list[str]]] = {
|
||||
"Ninja Village": {
|
||||
"Right": [
|
||||
"Autumn Hills - Left",
|
||||
|
@ -640,7 +638,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",
|
||||
|
@ -680,7 +678,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",
|
||||
|
|
|
@ -2,7 +2,7 @@ from .shop import FIGURINES, SHOP_ITEMS
|
|||
|
||||
# items
|
||||
# listing individual groups first for easy lookup
|
||||
NOTES = [
|
||||
NOTES: list[str] = [
|
||||
"Key of Hope",
|
||||
"Key of Chaos",
|
||||
"Key of Courage",
|
||||
|
@ -11,7 +11,7 @@ NOTES = [
|
|||
"Key of Symbiosis",
|
||||
]
|
||||
|
||||
PROG_ITEMS = [
|
||||
PROG_ITEMS: list[str] = [
|
||||
"Wingsuit",
|
||||
"Rope Dart",
|
||||
"Lightfoot Tabi",
|
||||
|
@ -28,18 +28,18 @@ PROG_ITEMS = [
|
|||
"Seashell",
|
||||
]
|
||||
|
||||
PHOBEKINS = [
|
||||
PHOBEKINS: list[str] = [
|
||||
"Necro",
|
||||
"Pyro",
|
||||
"Claustro",
|
||||
"Acro",
|
||||
]
|
||||
|
||||
USEFUL_ITEMS = [
|
||||
USEFUL_ITEMS: list[str] = [
|
||||
"Windmill Shuriken",
|
||||
]
|
||||
|
||||
FILLER = {
|
||||
FILLER: dict[str, int] = {
|
||||
"Time Shard": 5,
|
||||
"Time Shard (10)": 10,
|
||||
"Time Shard (50)": 20,
|
||||
|
@ -48,13 +48,13 @@ FILLER = {
|
|||
"Time Shard (500)": 5,
|
||||
}
|
||||
|
||||
TRAPS = {
|
||||
TRAPS: dict[str, int] = {
|
||||
"Teleport Trap": 5,
|
||||
"Prophecy Trap": 10,
|
||||
}
|
||||
|
||||
# item_name_to_id needs to be deterministic and match upstream
|
||||
ALL_ITEMS = [
|
||||
ALL_ITEMS: list[str] = [
|
||||
*NOTES,
|
||||
"Windmill Shuriken",
|
||||
"Wingsuit",
|
||||
|
@ -83,7 +83,7 @@ ALL_ITEMS = [
|
|||
# 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 = [
|
||||
ALWAYS_LOCATIONS: list[str] = [
|
||||
# notes
|
||||
"Sunken Shrine - Key of Love",
|
||||
"Corrupted Future - Key of Courage",
|
||||
|
@ -160,7 +160,7 @@ ALWAYS_LOCATIONS = [
|
|||
"Elemental Skylands Seal - Fire",
|
||||
]
|
||||
|
||||
BOSS_LOCATIONS = [
|
||||
BOSS_LOCATIONS: list[str] = [
|
||||
"Autumn Hills - Leaf Golem",
|
||||
"Catacombs - Ruxxtin",
|
||||
"Howling Grotto - Emerald Golem",
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import Dict
|
||||
|
||||
from schema import And, Optional, Or, Schema
|
||||
|
||||
|
@ -167,7 +166,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),
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from copy import deepcopy
|
||||
from typing import List, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from BaseClasses import CollectionState, PlandoOptions
|
||||
from Options import PlandoConnection
|
||||
|
@ -8,7 +8,7 @@ if TYPE_CHECKING:
|
|||
from . import MessengerWorld
|
||||
|
||||
|
||||
PORTALS = [
|
||||
PORTALS: list[str] = [
|
||||
"Autumn Hills",
|
||||
"Riviere Turquoise",
|
||||
"Howling Grotto",
|
||||
|
@ -18,7 +18,7 @@ PORTALS = [
|
|||
]
|
||||
|
||||
|
||||
SHOP_POINTS = {
|
||||
SHOP_POINTS: dict[str, list[str]] = {
|
||||
"Autumn Hills": [
|
||||
"Climbing Claws",
|
||||
"Hope Path",
|
||||
|
@ -113,7 +113,7 @@ SHOP_POINTS = {
|
|||
}
|
||||
|
||||
|
||||
CHECKPOINTS = {
|
||||
CHECKPOINTS: dict[str, list[str]] = {
|
||||
"Autumn Hills": [
|
||||
"Hope Latch",
|
||||
"Key of Hope",
|
||||
|
@ -186,7 +186,7 @@ CHECKPOINTS = {
|
|||
}
|
||||
|
||||
|
||||
REGION_ORDER = [
|
||||
REGION_ORDER: list[str] = [
|
||||
"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
|
||||
|
||||
|
|
|
@ -1,7 +1,4 @@
|
|||
from typing import Dict, List
|
||||
|
||||
|
||||
LOCATIONS: Dict[str, List[str]] = {
|
||||
LOCATIONS: dict[str, list[str]] = {
|
||||
"Ninja Village - Nest": [
|
||||
"Ninja Village - Candle",
|
||||
"Ninja Village - Astral Seed",
|
||||
|
@ -201,7 +198,7 @@ LOCATIONS: Dict[str, List[str]] = {
|
|||
}
|
||||
|
||||
|
||||
SUB_REGIONS: Dict[str, List[str]] = {
|
||||
SUB_REGIONS: dict[str, list[str]] = {
|
||||
"Ninja Village": [
|
||||
"Right",
|
||||
],
|
||||
|
@ -385,7 +382,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"],
|
||||
|
@ -414,7 +411,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",
|
||||
|
@ -436,7 +433,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",
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from typing import Dict, TYPE_CHECKING
|
||||
from typing import 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
|
||||
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
from typing import Dict, List, NamedTuple, Optional, Set, TYPE_CHECKING, Tuple, Union
|
||||
from typing import NamedTuple, TYPE_CHECKING
|
||||
|
||||
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: Optional[Union[str, Set[str]]] = None
|
||||
prerequisite: str | set[str] | None = 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]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from functools import cached_property
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from typing import 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: Optional["MessengerWorld"] = None
|
||||
world: "MessengerWorld | None" = None
|
||||
|
||||
|
||||
class MessengerRegion(Region):
|
||||
parent: str
|
||||
entrance_type = MessengerEntrance
|
||||
|
||||
def __init__(self, name: str, world: "MessengerWorld", parent: Optional[str] = None) -> None:
|
||||
def __init__(self, name: str, world: "MessengerWorld", parent: str | None = 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: Optional[int], parent: MessengerRegion) -> None:
|
||||
def __init__(self, player: int, name: str, loc_id: int | None, 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.replace("The Shop - ", "") # TODO use `remove_prefix` when 3.8 finally gets dropped
|
||||
name = self.name.removeprefix("The Shop - ")
|
||||
world = self.parent_region.multiworld.worlds[self.player]
|
||||
shop_data = SHOP_ITEMS[name]
|
||||
if shop_data.prerequisite:
|
||||
|
|
|
@ -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.replace("The Shop - ", "") in SHOP_ITEMS)
|
||||
self.assertTrue(loc.removeprefix("The Shop - ") in SHOP_ITEMS)
|
||||
self.assertEqual(len(prices), len(SHOP_ITEMS))
|
||||
|
||||
figures = self.world.figurine_prices
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
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.
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
from .game_item import kw_only, ItemSource
|
||||
from .game_item import ItemSource
|
||||
|
||||
|
||||
@dataclass(frozen=True, **kw_only)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class MachineSource(ItemSource):
|
||||
item: str # this should be optional (worm bin)
|
||||
machine: str
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import enum
|
||||
import sys
|
||||
from abc import ABC
|
||||
from dataclasses import dataclass, field
|
||||
from types import MappingProxyType
|
||||
|
@ -7,11 +6,6 @@ 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({})
|
||||
|
||||
|
||||
|
@ -36,21 +30,17 @@ 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)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class GenericSource(ItemSource):
|
||||
regions: Tuple[str, ...] = ()
|
||||
"""No region means it's available everywhere."""
|
||||
other_requirements: Tuple[Requirement, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
@ -59,7 +49,7 @@ class CustomRuleSource(ItemSource):
|
|||
create_rule: Callable[[Any], StardewRule]
|
||||
|
||||
|
||||
@dataclass(frozen=True, **kw_only)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class CompoundSource(ItemSource):
|
||||
sources: Tuple[ItemSource, ...] = ()
|
||||
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import Tuple, Sequence, Mapping
|
||||
|
||||
from .game_item import ItemSource, kw_only, ItemTag, Requirement
|
||||
from .game_item import ItemSource, ItemTag
|
||||
from ..strings.season_names import Season
|
||||
|
||||
|
||||
@dataclass(frozen=True, **kw_only)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class ForagingSource(ItemSource):
|
||||
regions: Tuple[str, ...]
|
||||
seasons: Tuple[str, ...] = Season.all
|
||||
other_requirements: Tuple[Requirement, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True, **kw_only)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class SeasonalForagingSource(ItemSource):
|
||||
season: str
|
||||
days: Sequence[int]
|
||||
|
@ -22,17 +21,17 @@ class SeasonalForagingSource(ItemSource):
|
|||
return ForagingSource(seasons=(self.season,), regions=self.regions)
|
||||
|
||||
|
||||
@dataclass(frozen=True, **kw_only)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class FruitBatsSource(ItemSource):
|
||||
...
|
||||
|
||||
|
||||
@dataclass(frozen=True, **kw_only)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class MushroomCaveSource(ItemSource):
|
||||
...
|
||||
|
||||
|
||||
@dataclass(frozen=True, **kw_only)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class HarvestFruitTreeSource(ItemSource):
|
||||
add_tags = (ItemTag.CROPSANITY,)
|
||||
|
||||
|
@ -46,7 +45,7 @@ class HarvestFruitTreeSource(ItemSource):
|
|||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, **kw_only)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class HarvestCropSource(ItemSource):
|
||||
add_tags = (ItemTag.CROPSANITY,)
|
||||
|
||||
|
@ -61,6 +60,6 @@ class HarvestCropSource(ItemSource):
|
|||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, **kw_only)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class ArtifactSpotSource(ItemSource):
|
||||
amount: int
|
||||
|
|
|
@ -1,40 +1,39 @@
|
|||
from dataclasses import dataclass
|
||||
from typing import Tuple, Optional
|
||||
|
||||
from .game_item import ItemSource, kw_only, Requirement
|
||||
from .game_item import ItemSource
|
||||
from ..strings.season_names import Season
|
||||
|
||||
ItemPrice = Tuple[int, str]
|
||||
|
||||
|
||||
@dataclass(frozen=True, **kw_only)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
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)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class MysteryBoxSource(ItemSource):
|
||||
amount: int
|
||||
|
||||
|
||||
@dataclass(frozen=True, **kw_only)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class ArtifactTroveSource(ItemSource):
|
||||
amount: int
|
||||
|
||||
|
||||
@dataclass(frozen=True, **kw_only)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class PrizeMachineSource(ItemSource):
|
||||
amount: int
|
||||
|
||||
|
||||
@dataclass(frozen=True, **kw_only)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class FishingTreasureChestSource(ItemSource):
|
||||
amount: int
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
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)
|
||||
has_mastery: bool = field(kw_only=True)
|
||||
|
|
|
@ -125,10 +125,7 @@ class StardewItemDeleter(Protocol):
|
|||
|
||||
|
||||
def load_item_csv():
|
||||
try:
|
||||
from importlib.resources import files
|
||||
except ImportError:
|
||||
from importlib_resources import files # noqa
|
||||
from importlib.resources import files
|
||||
|
||||
items = []
|
||||
with files(data).joinpath("items.csv").open() as file:
|
||||
|
|
|
@ -130,10 +130,7 @@ class StardewLocationCollector(Protocol):
|
|||
|
||||
|
||||
def load_location_csv() -> List[LocationData]:
|
||||
try:
|
||||
from importlib.resources import files
|
||||
except ImportError:
|
||||
from importlib_resources import files
|
||||
from importlib.resources import files
|
||||
|
||||
with files(data).joinpath("locations.csv").open() as file:
|
||||
reader = csv.DictReader(file)
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
importlib_resources; python_version <= '3.8'
|
||||
graphlib_backport; python_version <= '3.8'
|
|
@ -12,8 +12,6 @@ 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):
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
from collections import Counter
|
||||
from dataclasses import dataclass
|
||||
from typing import ClassVar, Dict, Literal, Tuple
|
||||
from typing_extensions import TypeGuard # remove when Python >= 3.10
|
||||
from typing import ClassVar, Dict, Literal, Tuple, TypeGuard
|
||||
|
||||
from Options import Choice, DefaultOnToggle, NamedRange, OptionGroup, PerGameCommonOptions, Range, Removed, Toggle
|
||||
|
||||
|
|
Loading…
Reference in New Issue