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,
|
"reportMissingImports": true,
|
||||||
"reportMissingTypeStubs": true,
|
"reportMissingTypeStubs": true,
|
||||||
|
|
||||||
"pythonVersion": "3.8",
|
"pythonVersion": "3.10",
|
||||||
"pythonPlatform": "Windows",
|
"pythonPlatform": "Windows",
|
||||||
|
|
||||||
"executionEnvironments": [
|
"executionEnvironments": [
|
||||||
|
|
|
@ -53,7 +53,7 @@ jobs:
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@v5
|
||||||
if: env.diff != ''
|
if: env.diff != ''
|
||||||
with:
|
with:
|
||||||
python-version: 3.8
|
python-version: '3.10'
|
||||||
|
|
||||||
- name: "Install dependencies"
|
- name: "Install dependencies"
|
||||||
if: env.diff != ''
|
if: env.diff != ''
|
||||||
|
|
|
@ -24,14 +24,14 @@ env:
|
||||||
jobs:
|
jobs:
|
||||||
# build-release-macos: # LF volunteer
|
# 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
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Install python
|
- name: Install python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: '3.8'
|
python-version: '3.10'
|
||||||
- name: Download run-time dependencies
|
- name: Download run-time dependencies
|
||||||
run: |
|
run: |
|
||||||
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip
|
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:
|
matrix:
|
||||||
os: [ubuntu-latest]
|
os: [ubuntu-latest]
|
||||||
python:
|
python:
|
||||||
- {version: '3.8'}
|
|
||||||
- {version: '3.9'}
|
|
||||||
- {version: '3.10'}
|
- {version: '3.10'}
|
||||||
- {version: '3.11'}
|
- {version: '3.11'}
|
||||||
- {version: '3.12'}
|
- {version: '3.12'}
|
||||||
include:
|
include:
|
||||||
- python: {version: '3.8'} # win7 compat
|
- python: {version: '3.10'} # old compat
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
- python: {version: '3.12'} # current
|
- python: {version: '3.12'} # current
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
import itertools
|
|
||||||
import functools
|
import functools
|
||||||
import logging
|
import logging
|
||||||
import random
|
import random
|
||||||
import secrets
|
import secrets
|
||||||
import typing # this can go away when Python 3.8 support is dropped
|
|
||||||
from argparse import Namespace
|
from argparse import Namespace
|
||||||
from collections import Counter, deque
|
from collections import Counter, deque
|
||||||
from collections.abc import Collection, MutableSequence
|
from collections.abc import Collection, MutableSequence
|
||||||
from enum import IntEnum, IntFlag
|
from enum import IntEnum, IntFlag
|
||||||
from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Mapping, NamedTuple,
|
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
|
from typing_extensions import NotRequired, TypedDict
|
||||||
|
|
||||||
|
@ -20,7 +18,7 @@ import NetUtils
|
||||||
import Options
|
import Options
|
||||||
import Utils
|
import Utils
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from worlds import AutoWorld
|
from worlds import AutoWorld
|
||||||
|
|
||||||
|
|
||||||
|
@ -231,7 +229,7 @@ class MultiWorld():
|
||||||
for player in self.player_ids:
|
for player in self.player_ids:
|
||||||
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
|
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
|
||||||
self.worlds[player] = world_type(self, 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]
|
self.worlds[player].options = options_dataclass(**{option_key: getattr(args, option_key)[player]
|
||||||
for option_key in options_dataclass.type_hints})
|
for option_key in options_dataclass.type_hints})
|
||||||
|
|
||||||
|
@ -975,7 +973,7 @@ class Region:
|
||||||
entrances: List[Entrance]
|
entrances: List[Entrance]
|
||||||
exits: List[Entrance]
|
exits: List[Entrance]
|
||||||
locations: List[Location]
|
locations: List[Location]
|
||||||
entrance_type: ClassVar[Type[Entrance]] = Entrance
|
entrance_type: ClassVar[type[Entrance]] = Entrance
|
||||||
|
|
||||||
class Register(MutableSequence):
|
class Register(MutableSequence):
|
||||||
region_manager: MultiWorld.RegionManager
|
region_manager: MultiWorld.RegionManager
|
||||||
|
@ -1075,7 +1073,7 @@ class Region:
|
||||||
return entrance.parent_region.get_connecting_entrance(is_main_entrance)
|
return entrance.parent_region.get_connecting_entrance(is_main_entrance)
|
||||||
|
|
||||||
def add_locations(self, locations: Dict[str, Optional[int]],
|
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
|
Adds locations to the Region object, where location_type is your Location class and locations is a dict of
|
||||||
location names to address.
|
location names to address.
|
||||||
|
|
|
@ -5,8 +5,8 @@ import multiprocessing
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
|
||||||
if sys.version_info < (3, 8, 6):
|
if sys.version_info < (3, 10, 11):
|
||||||
raise RuntimeError("Incompatible Python Version. 3.8.7+ is supported.")
|
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)
|
# 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())
|
_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 argparse import Namespace
|
||||||
from settings import Settings, get_settings
|
from settings import Settings, get_settings
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union
|
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union, TypeGuard
|
||||||
from typing_extensions import TypeGuard
|
|
||||||
from yaml import load, load_all, dump
|
from yaml import load, load_all, dump
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -48,7 +47,7 @@ class Version(typing.NamedTuple):
|
||||||
return ".".join(str(item) for item in self)
|
return ".".join(str(item) for item in self)
|
||||||
|
|
||||||
|
|
||||||
__version__ = "0.5.1"
|
__version__ = "0.6.0"
|
||||||
version_tuple = tuplize_version(__version__)
|
version_tuple = tuplize_version(__version__)
|
||||||
|
|
||||||
is_linux = sys.platform.startswith("linux")
|
is_linux = sys.platform.startswith("linux")
|
||||||
|
|
|
@ -17,7 +17,7 @@ from Utils import get_file_safe_name
|
||||||
if typing.TYPE_CHECKING:
|
if typing.TYPE_CHECKING:
|
||||||
from flask import Flask
|
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
|
settings.no_gui = True
|
||||||
configpath = os.path.abspath("config.yaml")
|
configpath = os.path.abspath("config.yaml")
|
||||||
if not os.path.exists(configpath): # fall back to config.yaml in home
|
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-Caching>=2.3.0
|
||||||
Flask-Compress>=1.15
|
Flask-Compress>=1.15
|
||||||
Flask-Limiter>=3.8.0
|
Flask-Limiter>=3.8.0
|
||||||
bokeh>=3.1.1; python_version <= '3.8'
|
bokeh>=3.5.2
|
||||||
bokeh>=3.4.3; python_version == '3.9'
|
|
||||||
bokeh>=3.5.2; python_version >= '3.10'
|
|
||||||
markupsafe>=2.1.5
|
markupsafe>=2.1.5
|
||||||
Markdown>=3.7
|
Markdown>=3.7
|
||||||
mdx-breakless-lists>=1.0.1
|
mdx-breakless-lists>=1.0.1
|
||||||
|
|
|
@ -16,7 +16,7 @@ game contributions:
|
||||||
* **Do not introduce unit test failures/regressions.**
|
* **Do not introduce unit test failures/regressions.**
|
||||||
Archipelago supports multiple versions of Python. You may need to download older Python versions to fully test
|
Archipelago supports multiple versions of Python. You may need to download older Python versions to fully test
|
||||||
your changes. Currently, the oldest supported version
|
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
|
It is recommended that automated github actions are turned on in your fork to have github run unit tests after
|
||||||
pushing.
|
pushing.
|
||||||
You can turn them on here:
|
You can turn them on here:
|
||||||
|
|
|
@ -7,7 +7,7 @@ use that version. These steps are for developers or platforms without compiled r
|
||||||
## General
|
## General
|
||||||
|
|
||||||
What you'll need:
|
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
|
* Python 3.12.x is currently the newest supported version
|
||||||
* pip: included in downloads from python.org, separate in many Linux distributions
|
* pip: included in downloads from python.org, separate in many Linux distributions
|
||||||
* Matching C compiler
|
* 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
|
# 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
|
# 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)
|
||||||
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_CONSOLELOG"] = "1"
|
||||||
os.environ["KIVY_NO_FILELOG"] = "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",
|
"excludes": ["numpy", "Cython", "PySide2", "PIL",
|
||||||
"pandas", "zstandard"],
|
"pandas", "zstandard"],
|
||||||
"zip_include_packages": ["*"],
|
"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_files": [], # broken in cx 6.14.0, we use more special sauce now
|
||||||
"include_msvcr": False,
|
"include_msvcr": False,
|
||||||
"replace_paths": ["*."],
|
"replace_paths": ["*."],
|
||||||
|
|
|
@ -2,9 +2,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import abc
|
import abc
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, ClassVar, Dict, Iterable, Tuple, Any, Optional, Union
|
from typing import TYPE_CHECKING, ClassVar, Dict, Iterable, Tuple, Any, Optional, Union, TypeGuard
|
||||||
|
|
||||||
from typing_extensions import TypeGuard
|
|
||||||
|
|
||||||
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components
|
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components
|
||||||
|
|
||||||
|
|
|
@ -66,19 +66,12 @@ class WorldSource:
|
||||||
start = time.perf_counter()
|
start = time.perf_counter()
|
||||||
if self.is_zip:
|
if self.is_zip:
|
||||||
importer = zipimport.zipimporter(self.resolved_path)
|
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])
|
||||||
spec = importer.find_spec(os.path.basename(self.path).rsplit(".", 1)[0])
|
assert spec, f"{self.path} is not a loadable module"
|
||||||
assert spec, f"{self.path} is not a loadable module"
|
mod = importlib.util.module_from_spec(spec)
|
||||||
mod = importlib.util.module_from_spec(spec)
|
|
||||||
else: # TODO: remove with 3.8 support
|
mod.__package__ = f"worlds.{mod.__package__}"
|
||||||
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__}"
|
mod.__name__ = f"worlds.{mod.__name__}"
|
||||||
sys.modules[mod.__name__] = mod
|
sys.modules[mod.__name__] = mod
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
|
|
|
@ -9,11 +9,7 @@ import ast
|
||||||
|
|
||||||
import jinja2
|
import jinja2
|
||||||
|
|
||||||
try:
|
from ast import unparse
|
||||||
from ast import unparse
|
|
||||||
except ImportError:
|
|
||||||
# Py 3.8 and earlier compatibility module
|
|
||||||
from astunparse import unparse
|
|
||||||
|
|
||||||
from Utils import get_text_between
|
from Utils import get_text_between
|
||||||
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
astunparse>=1.6.3; python_version <= '3.8'
|
|
|
@ -1,5 +1,5 @@
|
||||||
import logging
|
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 BaseClasses import CollectionState, Entrance, Item, ItemClassification, MultiWorld, Tutorial
|
||||||
from Options import Accessibility
|
from Options import Accessibility
|
||||||
|
@ -120,16 +120,16 @@ class MessengerWorld(World):
|
||||||
required_seals: int = 0
|
required_seals: int = 0
|
||||||
created_seals: int = 0
|
created_seals: int = 0
|
||||||
total_shards: int = 0
|
total_shards: int = 0
|
||||||
shop_prices: Dict[str, int]
|
shop_prices: dict[str, int]
|
||||||
figurine_prices: Dict[str, int]
|
figurine_prices: dict[str, int]
|
||||||
_filler_items: List[str]
|
_filler_items: list[str]
|
||||||
starting_portals: List[str]
|
starting_portals: list[str]
|
||||||
plando_portals: List[str]
|
plando_portals: list[str]
|
||||||
spoiler_portal_mapping: Dict[str, str]
|
spoiler_portal_mapping: dict[str, str]
|
||||||
portal_mapping: List[int]
|
portal_mapping: list[int]
|
||||||
transitions: List[Entrance]
|
transitions: list[Entrance]
|
||||||
reachable_locs: int = 0
|
reachable_locs: int = 0
|
||||||
filler: Dict[str, int]
|
filler: dict[str, int]
|
||||||
|
|
||||||
def generate_early(self) -> None:
|
def generate_early(self) -> None:
|
||||||
if self.options.goal == Goal.option_power_seal_hunt:
|
if self.options.goal == Goal.option_power_seal_hunt:
|
||||||
|
@ -178,7 +178,7 @@ class MessengerWorld(World):
|
||||||
for reg_name in sub_region]
|
for reg_name in sub_region]
|
||||||
|
|
||||||
for region in complex_regions:
|
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]
|
connection_data = CONNECTIONS[region.parent][region_name]
|
||||||
for exit_region in connection_data:
|
for exit_region in connection_data:
|
||||||
region.connect(self.multiworld.get_region(exit_region, self.player))
|
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
|
# create items that are always in the item pool
|
||||||
main_movement_items = ["Rope Dart", "Wingsuit"]
|
main_movement_items = ["Rope Dart", "Wingsuit"]
|
||||||
precollected_names = [item.name for item in self.multiworld.precollected_items[self.player]]
|
precollected_names = [item.name for item in self.multiworld.precollected_items[self.player]]
|
||||||
itempool: List[MessengerItem] = [
|
itempool: list[MessengerItem] = [
|
||||||
self.create_item(item)
|
self.create_item(item)
|
||||||
for item in self.item_name_to_id
|
for item in self.item_name_to_id
|
||||||
if item not in {
|
if item not in {
|
||||||
|
@ -290,7 +290,7 @@ class MessengerWorld(World):
|
||||||
for portal, output in portal_info:
|
for portal, output in portal_info:
|
||||||
spoiler.set_entrance(f"{portal} Portal", output, "I can write anything I want here lmao", self.player)
|
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 = {
|
slot_data = {
|
||||||
"shop": {SHOP_ITEMS[item].internal_name: price for item, price in self.shop_prices.items()},
|
"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()},
|
"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)
|
return self._filler_items.pop(0)
|
||||||
|
|
||||||
def create_item(self, name: str) -> MessengerItem:
|
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(
|
return MessengerItem(
|
||||||
name,
|
name,
|
||||||
ItemClassification.progression if item_id is None else self.get_item_classification(name),
|
ItemClassification.progression if item_id is None else self.get_item_classification(name),
|
||||||
|
@ -351,7 +351,7 @@ class MessengerWorld(World):
|
||||||
return ItemClassification.filler
|
return ItemClassification.filler
|
||||||
|
|
||||||
@classmethod
|
@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)
|
group = super().create_group(multiworld, new_player_id, players)
|
||||||
assert isinstance(group, MessengerWorld)
|
assert isinstance(group, MessengerWorld)
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ import os.path
|
||||||
import subprocess
|
import subprocess
|
||||||
import urllib.request
|
import urllib.request
|
||||||
from shutil import which
|
from shutil import which
|
||||||
from typing import Any, Optional
|
from typing import Any
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
from Utils import open_file
|
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"
|
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.
|
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
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def launch_game(*args) -> None:
|
def launch_game(*args) -> None:
|
||||||
"""Check the game installation, then launch it"""
|
"""Check the game installation, then launch it"""
|
||||||
def courier_installed() -> bool:
|
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": {
|
"Ninja Village": {
|
||||||
"Right": [
|
"Right": [
|
||||||
"Autumn Hills - Left",
|
"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",
|
"Ninja Village - Right": "Autumn Hills - Left",
|
||||||
"Autumn Hills - Left": "Ninja Village - Right",
|
"Autumn Hills - Left": "Ninja Village - Right",
|
||||||
"Autumn Hills - Right": "Forlorn Temple - Left",
|
"Autumn Hills - Right": "Forlorn Temple - Left",
|
||||||
|
@ -680,7 +678,7 @@ RANDOMIZED_CONNECTIONS: Dict[str, str] = {
|
||||||
"Sunken Shrine - Left": "Howling Grotto - Bottom",
|
"Sunken Shrine - Left": "Howling Grotto - Bottom",
|
||||||
}
|
}
|
||||||
|
|
||||||
TRANSITIONS: List[str] = [
|
TRANSITIONS: list[str] = [
|
||||||
"Ninja Village - Right",
|
"Ninja Village - Right",
|
||||||
"Autumn Hills - Left",
|
"Autumn Hills - Left",
|
||||||
"Autumn Hills - Right",
|
"Autumn Hills - Right",
|
||||||
|
|
|
@ -2,7 +2,7 @@ from .shop import FIGURINES, SHOP_ITEMS
|
||||||
|
|
||||||
# items
|
# items
|
||||||
# listing individual groups first for easy lookup
|
# listing individual groups first for easy lookup
|
||||||
NOTES = [
|
NOTES: list[str] = [
|
||||||
"Key of Hope",
|
"Key of Hope",
|
||||||
"Key of Chaos",
|
"Key of Chaos",
|
||||||
"Key of Courage",
|
"Key of Courage",
|
||||||
|
@ -11,7 +11,7 @@ NOTES = [
|
||||||
"Key of Symbiosis",
|
"Key of Symbiosis",
|
||||||
]
|
]
|
||||||
|
|
||||||
PROG_ITEMS = [
|
PROG_ITEMS: list[str] = [
|
||||||
"Wingsuit",
|
"Wingsuit",
|
||||||
"Rope Dart",
|
"Rope Dart",
|
||||||
"Lightfoot Tabi",
|
"Lightfoot Tabi",
|
||||||
|
@ -28,18 +28,18 @@ PROG_ITEMS = [
|
||||||
"Seashell",
|
"Seashell",
|
||||||
]
|
]
|
||||||
|
|
||||||
PHOBEKINS = [
|
PHOBEKINS: list[str] = [
|
||||||
"Necro",
|
"Necro",
|
||||||
"Pyro",
|
"Pyro",
|
||||||
"Claustro",
|
"Claustro",
|
||||||
"Acro",
|
"Acro",
|
||||||
]
|
]
|
||||||
|
|
||||||
USEFUL_ITEMS = [
|
USEFUL_ITEMS: list[str] = [
|
||||||
"Windmill Shuriken",
|
"Windmill Shuriken",
|
||||||
]
|
]
|
||||||
|
|
||||||
FILLER = {
|
FILLER: dict[str, int] = {
|
||||||
"Time Shard": 5,
|
"Time Shard": 5,
|
||||||
"Time Shard (10)": 10,
|
"Time Shard (10)": 10,
|
||||||
"Time Shard (50)": 20,
|
"Time Shard (50)": 20,
|
||||||
|
@ -48,13 +48,13 @@ FILLER = {
|
||||||
"Time Shard (500)": 5,
|
"Time Shard (500)": 5,
|
||||||
}
|
}
|
||||||
|
|
||||||
TRAPS = {
|
TRAPS: dict[str, int] = {
|
||||||
"Teleport Trap": 5,
|
"Teleport Trap": 5,
|
||||||
"Prophecy Trap": 10,
|
"Prophecy Trap": 10,
|
||||||
}
|
}
|
||||||
|
|
||||||
# item_name_to_id needs to be deterministic and match upstream
|
# item_name_to_id needs to be deterministic and match upstream
|
||||||
ALL_ITEMS = [
|
ALL_ITEMS: list[str] = [
|
||||||
*NOTES,
|
*NOTES,
|
||||||
"Windmill Shuriken",
|
"Windmill Shuriken",
|
||||||
"Wingsuit",
|
"Wingsuit",
|
||||||
|
@ -83,7 +83,7 @@ ALL_ITEMS = [
|
||||||
# locations
|
# locations
|
||||||
# the names of these don't actually matter, but using the upstream's names for now
|
# the names of these don't actually matter, but using the upstream's names for now
|
||||||
# order must be exactly the same as upstream
|
# order must be exactly the same as upstream
|
||||||
ALWAYS_LOCATIONS = [
|
ALWAYS_LOCATIONS: list[str] = [
|
||||||
# notes
|
# notes
|
||||||
"Sunken Shrine - Key of Love",
|
"Sunken Shrine - Key of Love",
|
||||||
"Corrupted Future - Key of Courage",
|
"Corrupted Future - Key of Courage",
|
||||||
|
@ -160,7 +160,7 @@ ALWAYS_LOCATIONS = [
|
||||||
"Elemental Skylands Seal - Fire",
|
"Elemental Skylands Seal - Fire",
|
||||||
]
|
]
|
||||||
|
|
||||||
BOSS_LOCATIONS = [
|
BOSS_LOCATIONS: list[str] = [
|
||||||
"Autumn Hills - Leaf Golem",
|
"Autumn Hills - Leaf Golem",
|
||||||
"Catacombs - Ruxxtin",
|
"Catacombs - Ruxxtin",
|
||||||
"Howling Grotto - Emerald Golem",
|
"Howling Grotto - Emerald Golem",
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
from schema import And, Optional, Or, Schema
|
from schema import And, Optional, Or, Schema
|
||||||
|
|
||||||
|
@ -167,7 +166,7 @@ class ShopPrices(Range):
|
||||||
default = 100
|
default = 100
|
||||||
|
|
||||||
|
|
||||||
def planned_price(location: str) -> Dict[Optional, Or]:
|
def planned_price(location: str) -> dict[Optional, Or]:
|
||||||
return {
|
return {
|
||||||
Optional(location): Or(
|
Optional(location): Or(
|
||||||
And(int, lambda n: n >= 0),
|
And(int, lambda n: n >= 0),
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from typing import List, TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from BaseClasses import CollectionState, PlandoOptions
|
from BaseClasses import CollectionState, PlandoOptions
|
||||||
from Options import PlandoConnection
|
from Options import PlandoConnection
|
||||||
|
@ -8,7 +8,7 @@ if TYPE_CHECKING:
|
||||||
from . import MessengerWorld
|
from . import MessengerWorld
|
||||||
|
|
||||||
|
|
||||||
PORTALS = [
|
PORTALS: list[str] = [
|
||||||
"Autumn Hills",
|
"Autumn Hills",
|
||||||
"Riviere Turquoise",
|
"Riviere Turquoise",
|
||||||
"Howling Grotto",
|
"Howling Grotto",
|
||||||
|
@ -18,7 +18,7 @@ PORTALS = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
SHOP_POINTS = {
|
SHOP_POINTS: dict[str, list[str]] = {
|
||||||
"Autumn Hills": [
|
"Autumn Hills": [
|
||||||
"Climbing Claws",
|
"Climbing Claws",
|
||||||
"Hope Path",
|
"Hope Path",
|
||||||
|
@ -113,7 +113,7 @@ SHOP_POINTS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
CHECKPOINTS = {
|
CHECKPOINTS: dict[str, list[str]] = {
|
||||||
"Autumn Hills": [
|
"Autumn Hills": [
|
||||||
"Hope Latch",
|
"Hope Latch",
|
||||||
"Key of Hope",
|
"Key of Hope",
|
||||||
|
@ -186,7 +186,7 @@ CHECKPOINTS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
REGION_ORDER = [
|
REGION_ORDER: list[str] = [
|
||||||
"Autumn Hills",
|
"Autumn Hills",
|
||||||
"Forlorn Temple",
|
"Forlorn Temple",
|
||||||
"Catacombs",
|
"Catacombs",
|
||||||
|
@ -228,7 +228,7 @@ def shuffle_portals(world: "MessengerWorld") -> None:
|
||||||
|
|
||||||
return parent
|
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"""
|
"""checks the provided plando connections for portals and connects them"""
|
||||||
nonlocal available_portals
|
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 - Nest": [
|
||||||
"Ninja Village - Candle",
|
"Ninja Village - Candle",
|
||||||
"Ninja Village - Astral Seed",
|
"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": [
|
"Ninja Village": [
|
||||||
"Right",
|
"Right",
|
||||||
],
|
],
|
||||||
|
@ -385,7 +382,7 @@ SUB_REGIONS: Dict[str, List[str]] = {
|
||||||
|
|
||||||
|
|
||||||
# order is slightly funky here for back compat
|
# 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"],
|
"Autumn Hills - Lakeside Checkpoint": ["Autumn Hills Mega Shard"],
|
||||||
"Forlorn Temple - Outside Shop": ["Hidden Entrance Mega Shard"],
|
"Forlorn Temple - Outside Shop": ["Hidden Entrance Mega Shard"],
|
||||||
"Catacombs - Top Left": ["Catacombs 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"},
|
"Menu": {"Tower HQ": "Start Game"},
|
||||||
"Tower HQ": {
|
"Tower HQ": {
|
||||||
"Autumn Hills - Portal": "ToTHQ Autumn Hills Portal",
|
"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
|
# regions that don't have sub-regions
|
||||||
LEVELS: List[str] = [
|
LEVELS: list[str] = [
|
||||||
"Menu",
|
"Menu",
|
||||||
"Tower HQ",
|
"Tower HQ",
|
||||||
"The Shop",
|
"The Shop",
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import Dict, TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from BaseClasses import CollectionState
|
from BaseClasses import CollectionState
|
||||||
from worlds.generic.Rules import CollectionRule, add_rule, allow_self_locking_items
|
from worlds.generic.Rules import CollectionRule, add_rule, allow_self_locking_items
|
||||||
|
@ -12,9 +12,9 @@ if TYPE_CHECKING:
|
||||||
class MessengerRules:
|
class MessengerRules:
|
||||||
player: int
|
player: int
|
||||||
world: "MessengerWorld"
|
world: "MessengerWorld"
|
||||||
connection_rules: Dict[str, CollectionRule]
|
connection_rules: dict[str, CollectionRule]
|
||||||
region_rules: Dict[str, CollectionRule]
|
region_rules: dict[str, CollectionRule]
|
||||||
location_rules: Dict[str, CollectionRule]
|
location_rules: dict[str, CollectionRule]
|
||||||
maximum_price: int
|
maximum_price: int
|
||||||
required_seals: 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:
|
if TYPE_CHECKING:
|
||||||
from . import MessengerWorld
|
from . import MessengerWorld
|
||||||
else:
|
else:
|
||||||
MessengerWorld = object
|
MessengerWorld = object
|
||||||
|
|
||||||
PROG_SHOP_ITEMS: List[str] = [
|
PROG_SHOP_ITEMS: list[str] = [
|
||||||
"Path of Resilience",
|
"Path of Resilience",
|
||||||
"Meditation",
|
"Meditation",
|
||||||
"Strike of the Ninja",
|
"Strike of the Ninja",
|
||||||
|
@ -14,7 +14,7 @@ PROG_SHOP_ITEMS: List[str] = [
|
||||||
"Aerobatics Warrior",
|
"Aerobatics Warrior",
|
||||||
]
|
]
|
||||||
|
|
||||||
USEFUL_SHOP_ITEMS: List[str] = [
|
USEFUL_SHOP_ITEMS: list[str] = [
|
||||||
"Karuta Plates",
|
"Karuta Plates",
|
||||||
"Serendipitous Bodies",
|
"Serendipitous Bodies",
|
||||||
"Kusari Jacket",
|
"Kusari Jacket",
|
||||||
|
@ -29,10 +29,10 @@ class ShopData(NamedTuple):
|
||||||
internal_name: str
|
internal_name: str
|
||||||
min_price: int
|
min_price: int
|
||||||
max_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),
|
"Karuta Plates": ShopData("HP_UPGRADE_1", 20, 200),
|
||||||
"Serendipitous Bodies": ShopData("ENEMY_DROP_HP", 20, 300, "The Shop - Karuta Plates"),
|
"Serendipitous Bodies": ShopData("ENEMY_DROP_HP", 20, 300, "The Shop - Karuta Plates"),
|
||||||
"Path of Resilience": ShopData("DAMAGE_REDUCTION", 100, 500, "The Shop - Serendipitous Bodies"),
|
"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"),
|
"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),
|
"Green Kappa Figurine": ShopData("GREEN_KAPPA", 100, 500),
|
||||||
"Blue Kappa Figurine": ShopData("BLUE_KAPPA", 100, 500),
|
"Blue Kappa Figurine": ShopData("BLUE_KAPPA", 100, 500),
|
||||||
"Ountarde Figurine": ShopData("OUNTARDE", 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_mod = world.options.shop_price.value
|
||||||
shop_price_planned = world.options.shop_price_plan
|
shop_price_planned = world.options.shop_price_plan
|
||||||
|
|
||||||
shop_prices: Dict[str, int] = {}
|
shop_prices: dict[str, int] = {}
|
||||||
figurine_prices: Dict[str, int] = {}
|
figurine_prices: dict[str, int] = {}
|
||||||
for item, price in shop_price_planned.value.items():
|
for item, price in shop_price_planned.value.items():
|
||||||
if not isinstance(price, int):
|
if not isinstance(price, int):
|
||||||
price = world.random.choices(list(price.keys()), weights=list(price.values()))[0]
|
price = world.random.choices(list(price.keys()), weights=list(price.values()))[0]
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
from functools import cached_property
|
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 BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, Region
|
||||||
from .regions import LOCATIONS, MEGA_SHARDS
|
from .regions import LOCATIONS, MEGA_SHARDS
|
||||||
|
@ -10,14 +10,14 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
|
|
||||||
class MessengerEntrance(Entrance):
|
class MessengerEntrance(Entrance):
|
||||||
world: Optional["MessengerWorld"] = None
|
world: "MessengerWorld | None" = None
|
||||||
|
|
||||||
|
|
||||||
class MessengerRegion(Region):
|
class MessengerRegion(Region):
|
||||||
parent: str
|
parent: str
|
||||||
entrance_type = MessengerEntrance
|
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)
|
super().__init__(name, world.player, world.multiworld)
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
locations = []
|
locations = []
|
||||||
|
@ -48,7 +48,7 @@ class MessengerRegion(Region):
|
||||||
class MessengerLocation(Location):
|
class MessengerLocation(Location):
|
||||||
game = "The Messenger"
|
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)
|
super().__init__(player, name, loc_id, parent)
|
||||||
if loc_id is None:
|
if loc_id is None:
|
||||||
if name == "Rescue Phantom":
|
if name == "Rescue Phantom":
|
||||||
|
@ -59,7 +59,7 @@ class MessengerLocation(Location):
|
||||||
class MessengerShopLocation(MessengerLocation):
|
class MessengerShopLocation(MessengerLocation):
|
||||||
@cached_property
|
@cached_property
|
||||||
def cost(self) -> int:
|
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]
|
world = self.parent_region.multiworld.worlds[self.player]
|
||||||
shop_data = SHOP_ITEMS[name]
|
shop_data = SHOP_ITEMS[name]
|
||||||
if shop_data.prerequisite:
|
if shop_data.prerequisite:
|
||||||
|
|
|
@ -77,7 +77,7 @@ class PlandoTest(MessengerTestBase):
|
||||||
|
|
||||||
loc = f"The Shop - {loc}"
|
loc = f"The Shop - {loc}"
|
||||||
self.assertLessEqual(price, self.multiworld.get_location(loc, self.player).cost)
|
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))
|
self.assertEqual(len(prices), len(SHOP_ITEMS))
|
||||||
|
|
||||||
figures = self.world.figurine_prices
|
figures = self.world.figurine_prices
|
||||||
|
|
|
@ -1,16 +1,12 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from graphlib import TopologicalSorter
|
||||||
from typing import Iterable, Mapping, Callable
|
from typing import Iterable, Mapping, Callable
|
||||||
|
|
||||||
from .game_content import StardewContent, ContentPack, StardewFeatures
|
from .game_content import StardewContent, ContentPack, StardewFeatures
|
||||||
from .vanilla.base import base_game as base_game_content_pack
|
from .vanilla.base import base_game as base_game_content_pack
|
||||||
from ..data.game_item import GameItem, ItemSource
|
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:
|
def unpack_content(features: StardewFeatures, packs: Iterable[ContentPack]) -> StardewContent:
|
||||||
# Base game is always registered first.
|
# Base game is always registered first.
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
from dataclasses import dataclass
|
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):
|
class MachineSource(ItemSource):
|
||||||
item: str # this should be optional (worm bin)
|
item: str # this should be optional (worm bin)
|
||||||
machine: str
|
machine: str
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import enum
|
import enum
|
||||||
import sys
|
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from types import MappingProxyType
|
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
|
from ..stardew_rule.protocol import StardewRule
|
||||||
|
|
||||||
if sys.version_info >= (3, 10):
|
|
||||||
kw_only = {"kw_only": True}
|
|
||||||
else:
|
|
||||||
kw_only = {}
|
|
||||||
|
|
||||||
DEFAULT_REQUIREMENT_TAGS = MappingProxyType({})
|
DEFAULT_REQUIREMENT_TAGS = MappingProxyType({})
|
||||||
|
|
||||||
|
|
||||||
|
@ -36,21 +30,17 @@ class ItemTag(enum.Enum):
|
||||||
class ItemSource(ABC):
|
class ItemSource(ABC):
|
||||||
add_tags: ClassVar[Tuple[ItemTag]] = ()
|
add_tags: ClassVar[Tuple[ItemTag]] = ()
|
||||||
|
|
||||||
|
other_requirements: Tuple[Requirement, ...] = field(kw_only=True, default_factory=tuple)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def requirement_tags(self) -> Mapping[str, Tuple[ItemTag, ...]]:
|
def requirement_tags(self) -> Mapping[str, Tuple[ItemTag, ...]]:
|
||||||
return DEFAULT_REQUIREMENT_TAGS
|
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):
|
class GenericSource(ItemSource):
|
||||||
regions: Tuple[str, ...] = ()
|
regions: Tuple[str, ...] = ()
|
||||||
"""No region means it's available everywhere."""
|
"""No region means it's available everywhere."""
|
||||||
other_requirements: Tuple[Requirement, ...] = ()
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
@ -59,7 +49,7 @@ class CustomRuleSource(ItemSource):
|
||||||
create_rule: Callable[[Any], StardewRule]
|
create_rule: Callable[[Any], StardewRule]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, **kw_only)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class CompoundSource(ItemSource):
|
class CompoundSource(ItemSource):
|
||||||
sources: Tuple[ItemSource, ...] = ()
|
sources: Tuple[ItemSource, ...] = ()
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Tuple, Sequence, Mapping
|
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
|
from ..strings.season_names import Season
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, **kw_only)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class ForagingSource(ItemSource):
|
class ForagingSource(ItemSource):
|
||||||
regions: Tuple[str, ...]
|
regions: Tuple[str, ...]
|
||||||
seasons: Tuple[str, ...] = Season.all
|
seasons: Tuple[str, ...] = Season.all
|
||||||
other_requirements: Tuple[Requirement, ...] = ()
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, **kw_only)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class SeasonalForagingSource(ItemSource):
|
class SeasonalForagingSource(ItemSource):
|
||||||
season: str
|
season: str
|
||||||
days: Sequence[int]
|
days: Sequence[int]
|
||||||
|
@ -22,17 +21,17 @@ class SeasonalForagingSource(ItemSource):
|
||||||
return ForagingSource(seasons=(self.season,), regions=self.regions)
|
return ForagingSource(seasons=(self.season,), regions=self.regions)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, **kw_only)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class FruitBatsSource(ItemSource):
|
class FruitBatsSource(ItemSource):
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, **kw_only)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class MushroomCaveSource(ItemSource):
|
class MushroomCaveSource(ItemSource):
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, **kw_only)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class HarvestFruitTreeSource(ItemSource):
|
class HarvestFruitTreeSource(ItemSource):
|
||||||
add_tags = (ItemTag.CROPSANITY,)
|
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):
|
class HarvestCropSource(ItemSource):
|
||||||
add_tags = (ItemTag.CROPSANITY,)
|
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):
|
class ArtifactSpotSource(ItemSource):
|
||||||
amount: int
|
amount: int
|
||||||
|
|
|
@ -1,40 +1,39 @@
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Tuple, Optional
|
from typing import Tuple, Optional
|
||||||
|
|
||||||
from .game_item import ItemSource, kw_only, Requirement
|
from .game_item import ItemSource
|
||||||
from ..strings.season_names import Season
|
from ..strings.season_names import Season
|
||||||
|
|
||||||
ItemPrice = Tuple[int, str]
|
ItemPrice = Tuple[int, str]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, **kw_only)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class ShopSource(ItemSource):
|
class ShopSource(ItemSource):
|
||||||
shop_region: str
|
shop_region: str
|
||||||
money_price: Optional[int] = None
|
money_price: Optional[int] = None
|
||||||
items_price: Optional[Tuple[ItemPrice, ...]] = None
|
items_price: Optional[Tuple[ItemPrice, ...]] = None
|
||||||
seasons: Tuple[str, ...] = Season.all
|
seasons: Tuple[str, ...] = Season.all
|
||||||
other_requirements: Tuple[Requirement, ...] = ()
|
|
||||||
|
|
||||||
def __post_init__(self):
|
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.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."
|
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):
|
class MysteryBoxSource(ItemSource):
|
||||||
amount: int
|
amount: int
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, **kw_only)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class ArtifactTroveSource(ItemSource):
|
class ArtifactTroveSource(ItemSource):
|
||||||
amount: int
|
amount: int
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, **kw_only)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class PrizeMachineSource(ItemSource):
|
class PrizeMachineSource(ItemSource):
|
||||||
amount: int
|
amount: int
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, **kw_only)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class FishingTreasureChestSource(ItemSource):
|
class FishingTreasureChestSource(ItemSource):
|
||||||
amount: int
|
amount: int
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
from ..data.game_item import kw_only
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class Skill:
|
class Skill:
|
||||||
name: str
|
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():
|
def load_item_csv():
|
||||||
try:
|
from importlib.resources import files
|
||||||
from importlib.resources import files
|
|
||||||
except ImportError:
|
|
||||||
from importlib_resources import files # noqa
|
|
||||||
|
|
||||||
items = []
|
items = []
|
||||||
with files(data).joinpath("items.csv").open() as file:
|
with files(data).joinpath("items.csv").open() as file:
|
||||||
|
|
|
@ -130,10 +130,7 @@ class StardewLocationCollector(Protocol):
|
||||||
|
|
||||||
|
|
||||||
def load_location_csv() -> List[LocationData]:
|
def load_location_csv() -> List[LocationData]:
|
||||||
try:
|
from importlib.resources import files
|
||||||
from importlib.resources import files
|
|
||||||
except ImportError:
|
|
||||||
from importlib_resources import files
|
|
||||||
|
|
||||||
with files(data).joinpath("locations.csv").open() as file:
|
with files(data).joinpath("locations.csv").open() as file:
|
||||||
reader = csv.DictReader(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>
|
# <function Location.<lambda> at 0x102ca98a0>
|
||||||
lambda_regex = re.compile(r"^<function Location\.<lambda> at (.*)>$")
|
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):
|
class TestGenerationIsStable(SVTestCase):
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import ClassVar, Dict, Literal, Tuple
|
from typing import ClassVar, Dict, Literal, Tuple, TypeGuard
|
||||||
from typing_extensions import TypeGuard # remove when Python >= 3.10
|
|
||||||
|
|
||||||
from Options import Choice, DefaultOnToggle, NamedRange, OptionGroup, PerGameCommonOptions, Range, Removed, Toggle
|
from Options import Choice, DefaultOnToggle, NamedRange, OptionGroup, PerGameCommonOptions, Range, Removed, Toggle
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue