From 3f318bf6d58a9746af0dae37ba7441681716bf2e Mon Sep 17 00:00:00 2001 From: odidev Date: Wed, 17 Nov 2021 15:34:35 +0500 Subject: [PATCH 001/194] Add Linux AArch64 wheel build support Signed-off-by: odidev --- .github/workflows/python-package.yml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 1127e239..41d4c886 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -186,8 +186,14 @@ jobs: linux-wheels: runs-on: "ubuntu-20.04" + strategy: + matrix: + arch: ["x86_64", "aarch64"] steps: - uses: actions/checkout@v1 + - name: Set up QEMU + if: ${{ matrix.arch == 'aarch64' }} + uses: docker/setup-qemu-action@v1 - name: Checkout submodules run: | git submodule update --init --recursive --depth 1 @@ -204,10 +210,14 @@ jobs: python -m cibuildwheel --output-dir wheelhouse env: CIBW_BUILD: cp36-* pp* - CIBW_ARCHS_LINUX: "x86_64" - CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014 + CIBW_ARCHS_LINUX: ${{ matrix.arch }} + CIBW_MANYLINUX_*_IMAGE: manylinux2014 CIBW_MANYLINUX_PYPY_X86_64_IMAGE: manylinux2014 - CIBW_BEFORE_ALL_LINUX: yum install -y SDL2-devel + CIBW_MANYLINUX_PYPY_AARCH64_IMAGE: manylinux2014 + CIBW_BEFORE_ALL_LINUX: > + yum install -y epel-release && + yum-config-manager --enable epel && + yum install -y SDL2-devel CIBW_BEFORE_TEST: pip install numpy CIBW_TEST_COMMAND: python -c "import tcod" - name: Archive wheel From d11f17d0cbf6a2fb839e2f08f68b2d6289f1c5b9 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 28 Nov 2021 09:38:46 -0800 Subject: [PATCH 002/194] Add pickle test for v13 Random. The internal C type will have a slight change, so the old format needs to stay supported. --- MANIFEST.in | 2 +- tests/data/README.md | 3 +++ tests/data/random_v13.pkl | Bin 0 -> 12069 bytes tests/test_random.py | 8 ++++++++ 4 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 tests/data/README.md create mode 100644 tests/data/random_v13.pkl diff --git a/MANIFEST.in b/MANIFEST.in index 54b46459..dd211a1d 100755 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,6 +3,6 @@ include *.py *.cfg *.txt *.rst *.toml recursive-include tcod *.py *.c *.h recursive-include libtcod/src *.glsl* *.c *.h -include libtcod/*.txt libtcod/*.md +include libtcod/*.txt libtcod/*.md tests/data/*.pkl exclude tcod/*/SDL2.dll diff --git a/tests/data/README.md b/tests/data/README.md new file mode 100644 index 00000000..8a7a10d7 --- /dev/null +++ b/tests/data/README.md @@ -0,0 +1,3 @@ +Data files for tests, such as old pickle streams. + +Remember to add new file types to `MANIFEST.in`. diff --git a/tests/data/random_v13.pkl b/tests/data/random_v13.pkl new file mode 100644 index 0000000000000000000000000000000000000000..03fb7bf2e07c3c6951b3ecb7332a510a01a4cadd GIT binary patch literal 12069 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 None: assert rand.uniform(0, 1) == rand2.uniform(0, 1) assert rand.uniform(0, 1) == rand2.uniform(0, 1) assert rand.uniform(0, 1) == rand2.uniform(0, 1) + + +def test_load_rng_v13() -> None: + with open(pathlib.Path(__file__).parent / "data/random_v13.pkl", "rb") as f: + rand: tcod.random.Random = pickle.load(f) + assert rand.randint(0, 0xFFFF) == 56422 + assert rand.randint(0, 0xFFFF) == 15795 From 514cc496e1e5ae0c58bbc1b9c8d58fcb0bc5ebd2 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 16 Dec 2021 19:26:19 -0800 Subject: [PATCH 003/194] Fix breaking changes from libtcod. Private types in libtcod heaps and random were changed. --- build_libtcod.py | 4 ++-- libtcod | 2 +- tcod/cffi.h | 1 + tcod/libtcodpy.py | 7 ++----- tcod/path.c | 9 ++++----- tcod/random.py | 33 ++++++++++++++++----------------- tests/test_random.py | 2 +- 7 files changed, 27 insertions(+), 31 deletions(-) diff --git a/build_libtcod.py b/build_libtcod.py index a9de366b..0b670586 100644 --- a/build_libtcod.py +++ b/build_libtcod.py @@ -38,10 +38,10 @@ RE_PREPROCESSOR = re.compile(r"(?!#define\s+\w+\s+\d+$)#.*?(? tcod.random.Random: Returns: Random: A Random instance using the default random number generator. """ - return tcod.random.Random._new_from_cdata(ffi.cast("mersenne_data_t*", lib.TCOD_random_get_instance())) + return tcod.random.Random._new_from_cdata(lib.TCOD_random_get_instance()) @pending_deprecate() @@ -3809,10 +3809,7 @@ def random_save(rnd: Optional[tcod.random.Random]) -> tcod.random.Random: """ return tcod.random.Random._new_from_cdata( ffi.gc( - ffi.cast( - "mersenne_data_t*", - lib.TCOD_random_save(rnd.random_c if rnd else ffi.NULL), - ), + lib.TCOD_random_save(rnd.random_c if rnd else ffi.NULL), lib.TCOD_random_delete, ) ) diff --git a/tcod/path.c b/tcod/path.c index dac89091..873e2dae 100644 --- a/tcod/path.c +++ b/tcod/path.c @@ -440,9 +440,9 @@ int update_frontier_heuristic( for (int i = 0; i < frontier->heap.size; ++i) { unsigned char* heap_ptr = (unsigned char*)frontier->heap.heap; heap_ptr += frontier->heap.node_size * i; - struct TCOD_HeapNode* heap_node = (void*)heap_ptr; - struct FrontierNode* f_node = (struct FrontierNode*)heap_node->data; - heap_node->priority = (f_node->distance + compute_heuristic(heuristic, frontier->ndim, f_node->index)); + int* priority = (int*)heap_ptr; + struct FrontierNode* f_node = (struct FrontierNode*)(heap_ptr + frontier->heap.data_offset); + *priority = (f_node->distance + compute_heuristic(heuristic, frontier->ndim, f_node->index)); } TCOD_minheap_heapify(&frontier->heap); return 0; @@ -478,8 +478,7 @@ int frontier_has_index( for (int i = 0; i < frontier->heap.size; ++i) { const unsigned char* heap_ptr = (const unsigned char*)frontier->heap.heap; heap_ptr += frontier->heap.node_size * i; - const struct TCOD_HeapNode* heap_node = (const void*)heap_ptr; - const struct FrontierNode* f_node = (const void*)heap_node->data; + const struct FrontierNode* f_node = (const void*)(heap_ptr + frontier->heap.data_offset); bool found = 1; for (int j = 0; j < frontier->ndim; ++j) { if (index[j] != f_node->index[j]) { diff --git a/tcod/random.py b/tcod/random.py index eee4815c..ef64df2c 100644 --- a/tcod/random.py +++ b/tcod/random.py @@ -75,10 +75,7 @@ def __init__( seed = hash(seed) self.random_c = ffi.gc( - ffi.cast( - "mersenne_data_t*", - lib.TCOD_random_new_from_seed(algorithm, seed & 0xFFFFFFFF), - ), + lib.TCOD_random_new_from_seed(algorithm, seed & 0xFFFFFFFF), lib.TCOD_random_delete, ) @@ -141,22 +138,24 @@ def __getstate__(self) -> Any: """Pack the self.random_c attribute into a portable state.""" state = self.__dict__.copy() state["random_c"] = { - "algo": self.random_c.algo, - "distribution": self.random_c.distribution, - "mt": list(self.random_c.mt), - "cur_mt": self.random_c.cur_mt, - "Q": list(self.random_c.Q), - "c": self.random_c.c, - "cur": self.random_c.cur, + "mt_cmwc": { + "algorithm": self.random_c.mt_cmwc.algorithm, + "distribution": self.random_c.mt_cmwc.distribution, + "mt": list(self.random_c.mt_cmwc.mt), + "cur_mt": self.random_c.mt_cmwc.cur_mt, + "Q": list(self.random_c.mt_cmwc.Q), + "c": self.random_c.mt_cmwc.c, + "cur": self.random_c.mt_cmwc.cur, + } } return state def __setstate__(self, state: Any) -> None: """Create a new cdata object with the stored paramaters.""" - try: - cdata = state["random_c"] - except KeyError: # old/deprecated format - cdata = state["cdata"] - del state["cdata"] - state["random_c"] = ffi.new("mersenne_data_t*", cdata) + if "algo" in state["random_c"]: + # Handle old/deprecated format. Covert to libtcod's new union type. + state["random_c"]["algorithm"] = state["random_c"]["algo"] + del state["random_c"]["algo"] + state["random_c"] = {"mt_cmwc": state["random_c"]} + state["random_c"] = ffi.new("TCOD_Random*", state["random_c"]) self.__dict__.update(state) diff --git a/tests/test_random.py b/tests/test_random.py index 5984900e..ce5875e3 100644 --- a/tests/test_random.py +++ b/tests/test_random.py @@ -29,7 +29,7 @@ def test_tcod_random_pickle() -> None: assert rand.uniform(0, 1) == rand2.uniform(0, 1) -def test_load_rng_v13() -> None: +def test_load_rng_v13_1() -> None: with open(pathlib.Path(__file__).parent / "data/random_v13.pkl", "rb") as f: rand: tcod.random.Random = pickle.load(f) assert rand.randint(0, 0xFFFF) == 56422 From b3ff5fb69ccd1c5871ad1a33adee89d4702c0be1 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 16 Dec 2021 19:44:27 -0800 Subject: [PATCH 004/194] Update type hints. --- examples/samples_tcod.py | 4 ++-- tcod/loader.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/samples_tcod.py b/examples/samples_tcod.py index 3ca8b1a5..7a41f50b 100755 --- a/examples/samples_tcod.py +++ b/examples/samples_tcod.py @@ -621,8 +621,8 @@ def on_draw(self) -> None: light[~visible] = 0 # Set non-visible areas to darkness. # Setup background colors for floating point math. - light_bg = self.light_map_bg.astype(np.float16) - dark_bg = self.dark_map_bg.astype(np.float16) + light_bg: NDArray[np.float16] = self.light_map_bg.astype(np.float16) + dark_bg: NDArray[np.float16] = self.dark_map_bg.astype(np.float16) # Linear interpolation between colors. sample_console.tiles_rgb["bg"] = dark_bg + (light_bg - dark_bg) * light[..., np.newaxis] diff --git a/tcod/loader.py b/tcod/loader.py index 2469d8ce..645246ba 100644 --- a/tcod/loader.py +++ b/tcod/loader.py @@ -9,7 +9,7 @@ import cffi # type: ignore -from tcod import __path__ # type: ignore +from tcod import __path__ __sdl_version__ = "" From 32668013613e13f9d446aed062796a7caa0c0085 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 18 Dec 2021 16:30:52 -0800 Subject: [PATCH 005/194] Add console parameter to tcod.context.new. --- CHANGELOG.rst | 2 ++ tcod/context.py | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6c2b3cbc..64213b55 100755 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,8 @@ v2.0.0 Unreleased ------------------ +Added + - New `console` parameter in `tcod.context.new` which sets parameters from an existing Console. 13.1.0 - 2021-10-22 ------------------- diff --git a/tcod/context.py b/tcod/context.py index e2e69f12..b05c75b8 100644 --- a/tcod/context.py +++ b/tcod/context.py @@ -365,6 +365,7 @@ def new( sdl_window_flags: Optional[int] = None, title: Optional[str] = None, argv: Optional[Iterable[str]] = None, + console: Optional[tcod.Console] = None, ) -> Context: """Create a new context with the desired pixel size. @@ -376,6 +377,9 @@ def new( `columns` and `rows` is the desired size of the console. Can be left as `None` when you're setting a context by a window size instead of a console. + `console` automatically fills in the `columns` and `rows` parameters from an existing :any:`tcod.console.Console` + instance. + Providing no size information at all is also acceptable. `renderer` is the desired libtcod renderer to use. @@ -409,6 +413,9 @@ def new( the console which should be used. .. versionadded:: 11.16 + + .. versionchanged:: 13.2 + Added the `console` parameter. """ if renderer is None: renderer = RENDERER_OPENGL2 @@ -416,6 +423,9 @@ def new( sdl_window_flags = SDL_WINDOW_RESIZABLE if argv is None: argv = sys.argv + if console is not None: + columns = columns or console.width + rows = rows or console.height argv_encoded = [ffi.new("char[]", arg.encode("utf-8")) for arg in argv] # Needs to be kept alive for argv_c. argv_c = ffi.new("char*[]", argv_encoded) From ac6d8374f853b42befe00b2633caeee9ca6b031d Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 24 Dec 2021 12:07:57 -0800 Subject: [PATCH 006/194] Update libtcod and note fixes. --- CHANGELOG.rst | 10 ++++++++++ libtcod | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 64213b55..c50d6440 100755 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -11,10 +11,20 @@ Unreleased Added - New `console` parameter in `tcod.context.new` which sets parameters from an existing Console. +Changed + - Using `libtcod 1.20.0`. + +Fixed + - Fixed segfault when an OpenGL2 context fails to load. + - Gaussian number generation no longer affects the results of unrelated RNG's. + - Gaussian number generation is now reentrant and thread-safe. + - Fixed potential crash in PNG image loading. + 13.1.0 - 2021-10-22 ------------------- Added - Added the `tcod.tileset.procedural_block_elements` function. + Removed - Python 3.6 is no longer supported. diff --git a/libtcod b/libtcod index ec5c9168..4ff19abe 160000 --- a/libtcod +++ b/libtcod @@ -1 +1 @@ -Subproject commit ec5c91683f73e3dd5d2fab75501f98e92383dfe8 +Subproject commit 4ff19abea52c34eb07f2945b9657648a87820963 From 033c8621d27ab77c64854f2b3c745be007f4362d Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 24 Dec 2021 13:30:28 -0800 Subject: [PATCH 007/194] Prepare 13.2.0 release. --- CHANGELOG.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c50d6440..2f2bf05b 100755 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,9 @@ v2.0.0 Unreleased ------------------ + +13.2.0 - 2021-12-24 +------------------- Added - New `console` parameter in `tcod.context.new` which sets parameters from an existing Console. From 14f454906d2c9cc1ad620cf3ba38eb667f8012c1 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 31 Dec 2021 18:05:10 -0800 Subject: [PATCH 008/194] Use the more correct os.PathLike type hint. --- CHANGELOG.rst | 2 ++ tcod/console.py | 6 +++--- tcod/image.py | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2f2bf05b..89ebd6b6 100755 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,8 @@ v2.0.0 Unreleased ------------------ +Fixed + - Functions accepting `Path`-like parameters now accept the more correct `os.PathLike` type. 13.2.0 - 2021-12-24 ------------------- diff --git a/tcod/console.py b/tcod/console.py index b6e6862a..e1f3daee 100644 --- a/tcod/console.py +++ b/tcod/console.py @@ -7,7 +7,7 @@ import os import warnings -from pathlib import Path +from os import PathLike from typing import Any, Iterable, Optional, Sequence, Tuple, Union import numpy as np @@ -1267,7 +1267,7 @@ def recommended_size() -> Tuple[int, int]: return w, h -def load_xp(path: Union[str, Path], order: Literal["C", "F"] = "C") -> Tuple[Console, ...]: +def load_xp(path: Union[str, PathLike[str]], order: Literal["C", "F"] = "C") -> Tuple[Console, ...]: """Load a REXPaint file as a tuple of consoles. `path` is the name of the REXPaint file to load. @@ -1309,7 +1309,7 @@ def load_xp(path: Union[str, Path], order: Literal["C", "F"] = "C") -> Tuple[Con def save_xp( - path: Union[str, Path], + path: Union[str, PathLike[str]], consoles: Iterable[Console], compress_level: int = 9, ) -> None: diff --git a/tcod/image.py b/tcod/image.py index 82d489dc..e47ddc39 100644 --- a/tcod/image.py +++ b/tcod/image.py @@ -7,7 +7,7 @@ """ from __future__ import annotations -from pathlib import Path +from os import PathLike from typing import Any, Dict, Tuple, Union import numpy as np @@ -336,7 +336,7 @@ def _get_format_name(format: int) -> str: " It's recommended to load images with a more complete image library such as python-Pillow or python-imageio.", category=PendingDeprecationWarning, ) -def load(filename: Union[str, Path]) -> NDArray[np.uint8]: +def load(filename: Union[str, PathLike[str]]) -> NDArray[np.uint8]: """Load a PNG file as an RGBA array. `filename` is the name of the file to load. From c811aa8c4cddac97ed8d6b6a6d91fc456d5521f8 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 1 Jan 2022 01:36:56 -0800 Subject: [PATCH 009/194] Add XTERM renderer. --- CHANGELOG.rst | 3 +++ build_libtcod.py | 2 ++ tcod/__init__.py | 1 + tcod/constants.py | 4 +++- tcod/context.py | 14 ++++++++++++++ tcod/libtcodpy.py | 1 + 6 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 89ebd6b6..8bb930a3 100755 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -8,6 +8,9 @@ v2.0.0 Unreleased ------------------ +Added + - New experimental renderer `tcod.context.RENDERER_XTERM`. + Fixed - Functions accepting `Path`-like parameters now accept the more correct `os.PathLike` type. diff --git a/build_libtcod.py b/build_libtcod.py index 0b670586..a69933c6 100644 --- a/build_libtcod.py +++ b/build_libtcod.py @@ -388,6 +388,7 @@ def parse_sdl_attrs(prefix: str, all_names: List[str]) -> Tuple[str, str]: "TCOD_KEY_TEXT_SIZE", "TCOD_NOISE_MAX_DIMENSIONS", "TCOD_NOISE_MAX_OCTAVES", + "TCOD_FALLBACK_FONT_SIZE", ] EXCLUDE_CONSTANT_PREFIXES = [ @@ -395,6 +396,7 @@ def parse_sdl_attrs(prefix: str, all_names: List[str]) -> Tuple[str, str]: "TCOD_HEAP_", "TCOD_LEX_", "TCOD_CHARMAP_", + "TCOD_LOG_", ] diff --git a/tcod/__init__.py b/tcod/__init__.py index 98c489b1..91f41cc6 100644 --- a/tcod/__init__.py +++ b/tcod/__init__.py @@ -556,6 +556,7 @@ "RENDERER_OPENGL2", "RENDERER_SDL", "RENDERER_SDL2", + "RENDERER_XTERM", "RIGHT", "RNG_CMWC", "RNG_MT", diff --git a/tcod/constants.py b/tcod/constants.py index 41afe966..4cb84313 100644 --- a/tcod/constants.py +++ b/tcod/constants.py @@ -247,7 +247,7 @@ KEY_PRESSED = 1 KEY_RELEASED = 2 LEFT = 0 -NB_RENDERERS = 5 +NB_RENDERERS = 6 NOISE_DEFAULT = 0 NOISE_PERLIN = 1 NOISE_SIMPLEX = 2 @@ -257,6 +257,7 @@ RENDERER_OPENGL2 = 4 RENDERER_SDL = 2 RENDERER_SDL2 = 3 +RENDERER_XTERM = 5 RIGHT = 1 RNG_CMWC = 1 RNG_MT = 0 @@ -755,6 +756,7 @@ "RENDERER_OPENGL2", "RENDERER_SDL", "RENDERER_SDL2", + "RENDERER_XTERM", "RIGHT", "RNG_CMWC", "RNG_MT", diff --git a/tcod/context.py b/tcod/context.py index b05c75b8..e62ac042 100644 --- a/tcod/context.py +++ b/tcod/context.py @@ -80,6 +80,7 @@ "RENDERER_OPENGL2", "RENDERER_SDL", "RENDERER_SDL2", + "RENDERER_XTERM", ) SDL_WINDOW_FULLSCREEN = lib.SDL_WINDOW_FULLSCREEN @@ -126,6 +127,19 @@ Rendering is decided by SDL2 and can be changed by using an SDL2 hint: https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER """ +RENDERER_XTERM = lib.TCOD_RENDERER_XTERM +"""A renderer targeting modern terminals with 24-bit color support. + +This is an experimental renderer with partial support for XTerm and SSH. +This will work best on those terminals. + +Terminal inputs and events will be passed to SDL's event system. + +There is poor support for ANSI escapes on Windows 10. +It is not recommended to use this renderer on Windows. + +.. versionadded:: 13.3 +""" def _handle_tileset(tileset: Optional[tcod.tileset.Tileset]) -> Any: diff --git a/tcod/libtcodpy.py b/tcod/libtcodpy.py index d5b37ea2..ad5fee77 100644 --- a/tcod/libtcodpy.py +++ b/tcod/libtcodpy.py @@ -4713,6 +4713,7 @@ def _atexit_verify() -> None: "RENDERER_OPENGL2", "RENDERER_SDL", "RENDERER_SDL2", + "RENDERER_XTERM", "RIGHT", "RNG_CMWC", "RNG_MT", From 3a261cafc438c9aad36b8b909db44e76bcfbfaa5 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 1 Jan 2022 07:31:01 -0800 Subject: [PATCH 010/194] Update Numpy type hints. --- examples/samples_tcod.py | 22 +++++++++++----------- examples/ttf.py | 5 +++-- tcod/_internal.py | 3 ++- tcod/console.py | 2 +- tcod/event.py | 4 ++-- tcod/image.py | 10 +++++----- tcod/libtcodpy.py | 2 +- tcod/map.py | 4 ++-- tcod/noise.py | 10 +++++----- tcod/path.py | 6 +++--- tcod/sdl.py | 4 ++-- tcod/tileset.py | 9 +++++---- tests/test_libtcodpy.py | 11 ++++++----- tests/test_noise.py | 10 +++++----- tests/test_tcod.py | 4 ++-- 15 files changed, 55 insertions(+), 51 deletions(-) diff --git a/examples/samples_tcod.py b/examples/samples_tcod.py index 7a41f50b..2d95b315 100755 --- a/examples/samples_tcod.py +++ b/examples/samples_tcod.py @@ -14,7 +14,7 @@ import sys import time import warnings -from typing import List +from typing import Any, List import numpy as np import tcod @@ -101,14 +101,14 @@ class TrueColorSample(Sample): def __init__(self) -> None: self.name = "True colors" # corner colors - self.colors = np.array( + self.colors: NDArray[np.int16] = np.array( [(50, 40, 150), (240, 85, 5), (50, 35, 240), (10, 200, 130)], dtype=np.int16, ) # color shift direction - self.slide_dir = np.array([[1, 1, 1], [-1, -1, 1], [1, -1, 1], [1, 1, -1]], dtype=np.int16) + self.slide_dir: NDArray[np.int16] = np.array([[1, 1, 1], [-1, -1, 1], [1, -1, 1], [1, 1, -1]], dtype=np.int16) # corner indexes - self.corners = np.array([0, 1, 2, 3]) + self.corners: NDArray[np.int16] = np.array([0, 1, 2, 3], dtype=np.int16) def on_draw(self) -> None: self.slide_corner_colors() @@ -509,7 +509,7 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> None: "##############################################", ] -SAMPLE_MAP = np.array([list(line) for line in SAMPLE_MAP_]).transpose() +SAMPLE_MAP: NDArray[Any] = np.array([list(line) for line in SAMPLE_MAP_]).transpose() FOV_ALGO_NAMES = [ "BASIC ", @@ -545,17 +545,17 @@ def __init__(self) -> None: map_shape = (SAMPLE_SCREEN_WIDTH, SAMPLE_SCREEN_HEIGHT) - self.walkable = np.zeros(map_shape, dtype=bool, order="F") + self.walkable: NDArray[np.bool_] = np.zeros(map_shape, dtype=bool, order="F") self.walkable[:] = SAMPLE_MAP[:] == " " - self.transparent = np.zeros(map_shape, dtype=bool, order="F") + self.transparent: NDArray[np.bool_] = np.zeros(map_shape, dtype=bool, order="F") self.transparent[:] = self.walkable[:] | (SAMPLE_MAP == "=") # Lit background colors for the map. - self.light_map_bg = np.full(SAMPLE_MAP.shape, LIGHT_GROUND, dtype="3B") + self.light_map_bg: NDArray[np.uint8] = np.full(SAMPLE_MAP.shape, LIGHT_GROUND, dtype="3B") self.light_map_bg[SAMPLE_MAP[:] == "#"] = LIGHT_WALL # Dark background colors for the map. - self.dark_map_bg = np.full(SAMPLE_MAP.shape, DARK_GROUND, dtype="3B") + self.dark_map_bg: NDArray[np.uint8] = np.full(SAMPLE_MAP.shape, DARK_GROUND, dtype="3B") self.dark_map_bg[SAMPLE_MAP[:] == "#"] = DARK_WALL def draw_ui(self) -> None: @@ -948,7 +948,7 @@ class BSPSample(Sample): def __init__(self) -> None: self.name = "Bsp toolkit" self.bsp = tcod.bsp.BSP(1, 1, SAMPLE_SCREEN_WIDTH - 1, SAMPLE_SCREEN_HEIGHT - 1) - self.bsp_map = np.zeros((SAMPLE_SCREEN_WIDTH, SAMPLE_SCREEN_HEIGHT), dtype=bool, order="F") + self.bsp_map: NDArray[np.bool_] = np.zeros((SAMPLE_SCREEN_WIDTH, SAMPLE_SCREEN_HEIGHT), dtype=bool, order="F") self.bsp_generate() def bsp_generate(self) -> None: @@ -1214,7 +1214,7 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> None: # xc = [[1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4]] # yc = [[1, 1, 1, 1], [2, 2, 2, 2], [3, 3, 3, 3]] if numpy_available: - (xc, yc) = np.meshgrid(range(SCREEN_W), range(SCREEN_H)) # type: ignore + (xc, yc) = np.meshgrid(range(SCREEN_W), range(SCREEN_H)) # translate coordinates of all pixels to center xc = xc - HALF_W yc = yc - HALF_H diff --git a/examples/ttf.py b/examples/ttf.py index 36434916..249c771a 100644 --- a/examples/ttf.py +++ b/examples/ttf.py @@ -13,6 +13,7 @@ import freetype # type: ignore # pip install freetype-py import numpy as np import tcod +from numpy.typing import NDArray FONT = "VeraMono.ttf" @@ -36,10 +37,10 @@ def load_ttf(path: str, size: Tuple[int, int]) -> tcod.tileset.Tileset: ttf.load_glyph(glyph_index) bitmap = ttf.glyph.bitmap assert bitmap.pixel_mode == freetype.FT_PIXEL_MODE_GRAY - bitmap_array = np.asarray(bitmap.buffer).reshape((bitmap.width, bitmap.rows), order="F") + bitmap_array: NDArray[np.uint8] = np.asarray(bitmap.buffer).reshape((bitmap.width, bitmap.rows), order="F") if bitmap_array.size == 0: continue # Skip blank glyphs. - output_image = np.zeros(size, dtype=np.uint8, order="F") + output_image: NDArray[np.uint8] = np.zeros(size, dtype=np.uint8, order="F") out_slice = output_image # Adjust the position to center this glyph on the tile. diff --git a/tcod/_internal.py b/tcod/_internal.py index e7dc033b..bde088e9 100644 --- a/tcod/_internal.py +++ b/tcod/_internal.py @@ -7,6 +7,7 @@ from typing import Any, AnyStr, Callable, TypeVar, cast import numpy as np +from numpy.typing import NDArray from typing_extensions import Literal, NoReturn from tcod.loader import ffi, lib @@ -225,7 +226,7 @@ class TempImage(object): """An Image-like container for NumPy arrays.""" def __init__(self, array: Any): - self._array = np.ascontiguousarray(array, dtype=np.uint8) + self._array: NDArray[np.uint8] = np.ascontiguousarray(array, dtype=np.uint8) height, width, depth = self._array.shape if depth != 3: raise TypeError("Array must have RGB channels. Shape is: %r" % (self._array.shape,)) diff --git a/tcod/console.py b/tcod/console.py index e1f3daee..895c7e79 100644 --- a/tcod/console.py +++ b/tcod/console.py @@ -124,7 +124,7 @@ def __init__( if buffer is not None: if self._order == "F": buffer = buffer.transpose() - self._tiles = np.ascontiguousarray(buffer, self.DTYPE) + self._tiles: NDArray[Any] = np.ascontiguousarray(buffer, self.DTYPE) else: self._tiles = np.ndarray((height, width), dtype=self.DTYPE) diff --git a/tcod/event.py b/tcod/event.py index e430ae49..3cfc878b 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -1091,8 +1091,8 @@ def get_keyboard_state() -> NDArray[np.bool_]: """ numkeys = ffi.new("int[1]") keyboard_state = lib.SDL_GetKeyboardState(numkeys) - out: NDArray[np.bool_] = np.frombuffer(ffi.buffer(keyboard_state[0 : numkeys[0]]), dtype=np.bool_) # type: ignore - out.flags["WRITEABLE"] = False # This buffer is supposed to be const. + out: NDArray[np.bool_] = np.frombuffer(ffi.buffer(keyboard_state[0 : numkeys[0]]), dtype=np.bool_) + out.flags["WRITEABLE"] = False # type: ignore[index] # This buffer is supposed to be const. return out diff --git a/tcod/image.py b/tcod/image.py index e47ddc39..0a017530 100644 --- a/tcod/image.py +++ b/tcod/image.py @@ -11,7 +11,7 @@ from typing import Any, Dict, Tuple, Union import numpy as np -from numpy.typing import NDArray +from numpy.typing import ArrayLike, NDArray import tcod.console from tcod._internal import _console, deprecate @@ -41,7 +41,7 @@ def _from_cdata(cls, cdata: Any) -> Image: return self @classmethod - def from_array(cls, array: Any) -> Image: + def from_array(cls, array: ArrayLike) -> Image: """Create a new Image from a copy of an array-like object. Example: @@ -52,10 +52,10 @@ def from_array(cls, array: Any) -> Image: .. versionadded:: 11.4 """ - array = np.asarray(array) + array = np.asarray(array, dtype=np.uint8) height, width, depth = array.shape image = cls(width, height) - image_array = np.asarray(image) + image_array: NDArray[np.uint8] = np.asarray(image) image_array[...] = array return image @@ -349,7 +349,7 @@ def load(filename: Union[str, PathLike[str]]) -> NDArray[np.uint8]: array: NDArray[np.uint8] = np.asarray(image, dtype=np.uint8) height, width, depth = array.shape if depth == 3: - array = np.concatenate( # type: ignore + array = np.concatenate( ( array, np.full((height, width, 1), fill_value=255, dtype=np.uint8), diff --git a/tcod/libtcodpy.py b/tcod/libtcodpy.py index ad5fee77..626a05b6 100644 --- a/tcod/libtcodpy.py +++ b/tcod/libtcodpy.py @@ -2305,7 +2305,7 @@ def _heightmap_cdata(array: NDArray[np.float32]) -> ffi.CData: array = array.transpose() if not array.flags["C_CONTIGUOUS"]: raise ValueError("array must be a contiguous segment.") - if array.dtype != np.float32: # type: ignore + if array.dtype != np.float32: raise ValueError("array dtype must be float32, not %r" % array.dtype) height, width = array.shape pointer = ffi.from_buffer("float *", array) diff --git a/tcod/map.py b/tcod/map.py index 48ce9f9a..3ce4596b 100644 --- a/tcod/map.py +++ b/tcod/map.py @@ -87,7 +87,7 @@ def __init__( self.height = height self._order = tcod._internal.verify_order(order) - self.__buffer = np.zeros((height, width, 3), dtype=np.bool_) + self.__buffer: NDArray[np.bool_] = np.zeros((height, width, 3), dtype=np.bool_) self.map_c = self.__as_cdata() def __as_cdata(self) -> Any: @@ -251,7 +251,7 @@ def compute_fov( RuntimeWarning, stacklevel=2, ) - map_buffer = np.empty( + map_buffer: NDArray[np.bool_] = np.empty( transparency.shape, dtype=[("transparent", bool), ("walkable", bool), ("fov", bool)], ) diff --git a/tcod/noise.py b/tcod/noise.py index 2ee91c96..cae99be0 100644 --- a/tcod/noise.py +++ b/tcod/noise.py @@ -38,7 +38,7 @@ import enum import warnings -from typing import Any, Optional, Sequence, Tuple, Union +from typing import Any, List, Optional, Sequence, Tuple, Union import numpy as np from numpy.typing import ArrayLike, NDArray @@ -245,7 +245,7 @@ def __getitem__(self, indexes: Any) -> NDArray[np.float32]: raise IndexError( "This noise generator has %i dimensions, but was indexed with %i." % (self.dimensions, len(indexes)) ) - indexes = np.broadcast_arrays(*indexes) # type: ignore + indexes = np.broadcast_arrays(*indexes) c_input = [ffi.NULL, ffi.NULL, ffi.NULL, ffi.NULL] for i, index in enumerate(indexes): if index.dtype.type == np.object_: @@ -253,7 +253,7 @@ def __getitem__(self, indexes: Any) -> NDArray[np.float32]: indexes[i] = np.ascontiguousarray(index, dtype=np.float32) c_input[i] = ffi.from_buffer("float*", indexes[i]) - out = np.empty(indexes[0].shape, dtype=np.float32) + out: NDArray[np.float32] = np.empty(indexes[0].shape, dtype=np.float32) if self.implementation == Implementation.SIMPLE: lib.TCOD_noise_get_vectorized( self.noise_c, @@ -332,7 +332,7 @@ def sample_ogrid(self, ogrid: Sequence[ArrayLike]) -> NDArray[np.float32]: """ if len(ogrid) != self.dimensions: raise ValueError("len(ogrid) must equal self.dimensions, " "%r != %r" % (len(ogrid), self.dimensions)) - ogrids = [np.ascontiguousarray(array, np.float32) for array in ogrid] + ogrids: List[NDArray[np.float32]] = [np.ascontiguousarray(array, np.float32) for array in ogrid] out: np.ndarray[Any, np.dtype[np.float32]] = np.ndarray([array.size for array in ogrids], np.float32) lib.NoiseSampleOpenMeshGrid( self._tdl_noise_c, @@ -461,4 +461,4 @@ def grid( if len(shape) != len(origin): raise TypeError("shape must have the same length as origin") indexes = (np.arange(i_shape) * i_scale + i_origin for i_shape, i_scale, i_origin in zip(shape, scale, origin)) - return tuple(np.meshgrid(*indexes, copy=False, sparse=True, indexing=indexing)) # type: ignore + return tuple(np.meshgrid(*indexes, copy=False, sparse=True, indexing=indexing)) diff --git a/tcod/path.py b/tcod/path.py index 59b9b301..acc427af 100644 --- a/tcod/path.py +++ b/tcod/path.py @@ -473,7 +473,7 @@ def dijkstra2d( .. versionchanged:: 12.1 Added `out` parameter. Now returns the output array. """ - dist = np.asarray(distance) + dist: NDArray[Any] = np.asarray(distance) if out is ...: # type: ignore out = dist warnings.warn( @@ -560,7 +560,7 @@ def hillclimb2d( Added `edge_map` parameter. """ x, y = start - dist = np.asarray(distance) + dist: NDArray[Any] = np.asarray(distance) if not (0 <= x < dist.shape[0] and 0 <= y < dist.shape[1]): raise IndexError("Starting point %r not in shape %r" % (start, dist.shape)) c_dist = _export(dist) @@ -582,7 +582,7 @@ def _world_array(shape: Tuple[int, ...], dtype: Any = np.int32) -> NDArray[Any]: """Return an array where ``ij == arr[ij]``.""" return np.ascontiguousarray( np.transpose( - np.meshgrid( # type: ignore + np.meshgrid( *(np.arange(i, dtype=dtype) for i in shape), indexing="ij", copy=False, diff --git a/tcod/sdl.py b/tcod/sdl.py index 8aa38ecb..470011e6 100644 --- a/tcod/sdl.py +++ b/tcod/sdl.py @@ -8,7 +8,7 @@ from typing import Any, Tuple import numpy as np -from numpy.typing import ArrayLike +from numpy.typing import ArrayLike, NDArray from tcod.loader import ffi, lib @@ -19,7 +19,7 @@ class _TempSurface: """Holds a temporary surface derived from a NumPy array.""" def __init__(self, pixels: ArrayLike) -> None: - self._array = np.ascontiguousarray(pixels, dtype=np.uint8) + self._array: NDArray[np.uint8] = np.ascontiguousarray(pixels, dtype=np.uint8) if len(self._array) != 3: raise TypeError("NumPy shape must be 3D [y, x, ch] (got %r)" % (self._array.shape,)) if 3 <= self._array.shape[2] <= 4: diff --git a/tcod/tileset.py b/tcod/tileset.py index 98ea4520..cf26b0fb 100644 --- a/tcod/tileset.py +++ b/tcod/tileset.py @@ -74,7 +74,7 @@ def get_tile(self, codepoint: int) -> NDArray[np.uint8]: uint8. Note that most grey-scale tiles will only use the alpha channel and will usually have a solid white color channel. """ - tile = np.zeros(self.tile_shape + (4,), dtype=np.uint8) + tile: NDArray[np.uint8] = np.zeros(self.tile_shape + (4,), dtype=np.uint8) lib.TCOD_tileset_get_tile_( self._tileset_p, codepoint, @@ -133,7 +133,7 @@ def set_tile(self, codepoint: int, tile: Union[ArrayLike, NDArray[np.uint8]]) -> """ tile = np.ascontiguousarray(tile, dtype=np.uint8) if tile.shape == self.tile_shape: - full_tile = np.empty(self.tile_shape + (4,), dtype=np.uint8) + full_tile: NDArray[np.uint8] = np.empty(self.tile_shape + (4,), dtype=np.uint8) full_tile[:, :, :3] = 255 full_tile[:, :, 3] = tile return self.set_tile(codepoint, full_tile) @@ -167,7 +167,7 @@ def render(self, console: tcod.console.Console) -> NDArray[np.uint8]: raise ValueError("'console' must not be the root console.") width = console.width * self.tile_width height = console.height * self.tile_height - out = np.empty((height, width, 4), np.uint8) + out: NDArray[np.uint8] = np.empty((height, width, 4), np.uint8) out[:] = 9 surface_p = ffi.gc( lib.SDL_CreateRGBSurfaceWithFormatFrom( @@ -415,7 +415,8 @@ def procedural_block_elements(*, tileset: Tileset) -> None: (0x259E, 0b0110), # "▞" Quadrant upper right and lower left. (0x259F, 0b0111), # "▟" Quadrant upper right and lower left and lower right. ): - alpha: NDArray[np.uint8] = np.asarray((quadrants & quad_mask) != 0, dtype=np.uint8) * 255 + alpha: NDArray[np.uint8] = np.asarray((quadrants & quad_mask) != 0, dtype=np.uint8) + alpha *= 255 tileset.set_tile(codepoint, alpha) for codepoint, axis, fraction, negative in ( diff --git a/tests/test_libtcodpy.py b/tests/test_libtcodpy.py index 5db89034..bf12f9a1 100644 --- a/tests/test_libtcodpy.py +++ b/tests/test_libtcodpy.py @@ -5,6 +5,7 @@ import numpy import numpy as np import pytest +from numpy.typing import NDArray import tcod import tcod as libtcodpy @@ -253,7 +254,7 @@ def test_console_fill(console: tcod.console.Console) -> None: def test_console_fill_numpy(console: tcod.console.Console) -> None: width = libtcodpy.console_get_width(console) height = libtcodpy.console_get_height(console) - fill = numpy.zeros((height, width), dtype=numpy.intc) + fill: NDArray[np.intc] = numpy.zeros((height, width), dtype=np.intc) for y in range(height): fill[y, :] = y % 256 @@ -262,9 +263,9 @@ def test_console_fill_numpy(console: tcod.console.Console) -> None: libtcodpy.console_fill_char(console, fill) # type: ignore # verify fill - bg = numpy.zeros((height, width), dtype=numpy.intc) - fg = numpy.zeros((height, width), dtype=numpy.intc) - ch = numpy.zeros((height, width), dtype=numpy.intc) + bg: NDArray[np.intc] = numpy.zeros((height, width), dtype=numpy.intc) + fg: NDArray[np.intc] = numpy.zeros((height, width), dtype=numpy.intc) + ch: NDArray[np.intc] = numpy.zeros((height, width), dtype=numpy.intc) for y in range(height): for x in range(width): bg[y, x] = libtcodpy.console_get_char_background(console, x, y)[0] @@ -639,7 +640,7 @@ def test_heightmap() -> None: libtcodpy.heightmap_delete(h_map) -MAP = np.array( +MAP: NDArray[Any] = np.array( [ list(line) for line in ( diff --git a/tests/test_noise.py b/tests/test_noise.py index ed77dce6..2067b852 100644 --- a/tests/test_noise.py +++ b/tests/test_noise.py @@ -40,17 +40,17 @@ def test_noise_class( noise.sample_mgrid(np.mgrid[:2, :3]) noise.sample_ogrid(np.ogrid[:2, :3]) - np.testing.assert_equal( # type: ignore + np.testing.assert_equal( noise.sample_mgrid(np.mgrid[:2, :3]), noise.sample_ogrid(np.ogrid[:2, :3]), ) - np.testing.assert_equal(noise.sample_mgrid(np.mgrid[:2, :3]), noise[tuple(np.mgrid[:2, :3])]) # type: ignore + np.testing.assert_equal(noise.sample_mgrid(np.mgrid[:2, :3]), noise[tuple(np.mgrid[:2, :3])]) repr(noise) def test_noise_samples() -> None: noise = tcod.noise.Noise(2, tcod.noise.Algorithm.SIMPLEX, tcod.noise.Implementation.SIMPLE) - np.testing.assert_equal( # type: ignore + np.testing.assert_equal( noise.sample_mgrid(np.mgrid[:32, :24]), noise.sample_ogrid(np.ogrid[:32, :24]), ) @@ -77,7 +77,7 @@ def test_noise_pickle(implementation: tcod.noise.Implementation) -> None: rand = tcod.random.Random(tcod.random.MERSENNE_TWISTER, 42) noise = tcod.noise.Noise(2, implementation, seed=rand) noise2 = copy.copy(noise) - np.testing.assert_equal( # type: ignore + np.testing.assert_equal( noise.sample_ogrid(np.ogrid[:3, :1]), noise2.sample_ogrid(np.ogrid[:3, :1]), ) @@ -87,7 +87,7 @@ def test_noise_copy() -> None: rand = tcod.random.Random(tcod.random.MERSENNE_TWISTER, 42) noise = tcod.noise.Noise(2, seed=rand) noise2 = pickle.loads(pickle.dumps(noise)) - np.testing.assert_equal( # type: ignore + np.testing.assert_equal( noise.sample_ogrid(np.ogrid[:3, :1]), noise2.sample_ogrid(np.ogrid[:3, :1]), ) diff --git a/tests/test_tcod.py b/tests/test_tcod.py index bfb6a36e..88821924 100644 --- a/tests/test_tcod.py +++ b/tests/test_tcod.py @@ -6,7 +6,7 @@ import numpy as np import pytest -from numpy.typing import DTypeLike +from numpy.typing import DTypeLike, NDArray import tcod @@ -125,7 +125,7 @@ def test_color_class() -> None: @pytest.mark.parametrize("dtype", [np.int8, np.int16, np.int32, np.uint8, np.uint16, np.uint32, np.float32]) def test_path_numpy(dtype: DTypeLike) -> None: - map_np = np.ones((6, 6), dtype=dtype) + map_np: NDArray[Any] = np.ones((6, 6), dtype=dtype) map_np[1:4, 1:4] = 0 astar = tcod.path.AStar(map_np, 0) From 0ca94f0231e8b4922607c741940e47ca1db061e8 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 5 Jan 2022 21:13:25 -0800 Subject: [PATCH 011/194] Apply BDF fixes. Refactor more types to os.PathLike. --- CHANGELOG.rst | 1 + libtcod | 2 +- tcod/tileset.py | 12 +++++++----- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 8bb930a3..6e5ed581 100755 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -13,6 +13,7 @@ Added Fixed - Functions accepting `Path`-like parameters now accept the more correct `os.PathLike` type. + - BDF files with blank lines no longer fail to load with an "Unknown keyword" error. 13.2.0 - 2021-12-24 ------------------- diff --git a/libtcod b/libtcod index 4ff19abe..57f5216b 160000 --- a/libtcod +++ b/libtcod @@ -1 +1 @@ -Subproject commit 4ff19abea52c34eb07f2945b9657648a87820963 +Subproject commit 57f5216b66227f5af68a81e8d3118d6d15c58cff diff --git a/tcod/tileset.py b/tcod/tileset.py index cf26b0fb..fd4f2f21 100644 --- a/tcod/tileset.py +++ b/tcod/tileset.py @@ -14,7 +14,7 @@ import itertools import os -from pathlib import Path +from os import PathLike from typing import Any, Iterable, Optional, Tuple, Union import numpy as np @@ -250,7 +250,7 @@ def set_default(tileset: Tileset) -> None: lib.TCOD_set_default_tileset(tileset._tileset_p) -def load_truetype_font(path: Union[str, Path], tile_width: int, tile_height: int) -> Tileset: +def load_truetype_font(path: Union[str, PathLike[str]], tile_width: int, tile_height: int) -> Tileset: """Return a new Tileset from a `.ttf` or `.otf` file. Same as :any:`set_truetype_font`, but returns a :any:`Tileset` instead. @@ -267,7 +267,7 @@ def load_truetype_font(path: Union[str, Path], tile_width: int, tile_height: int @deprecate("Accessing the default tileset is deprecated.") -def set_truetype_font(path: Union[str, Path], tile_width: int, tile_height: int) -> None: +def set_truetype_font(path: Union[str, PathLike[str]], tile_width: int, tile_height: int) -> None: """Set the default tileset from a `.ttf` or `.otf` file. `path` is the file path for the font file. @@ -293,7 +293,7 @@ def set_truetype_font(path: Union[str, Path], tile_width: int, tile_height: int) raise RuntimeError(ffi.string(lib.TCOD_get_error())) -def load_bdf(path: Union[str, Path]) -> Tileset: +def load_bdf(path: Union[str, PathLike[str]]) -> Tileset: """Return a new Tileset from a `.bdf` file. For the best results the font should be monospace, cell-based, and @@ -314,7 +314,9 @@ def load_bdf(path: Union[str, Path]) -> Tileset: return Tileset._claim(cdata) -def load_tilesheet(path: Union[str, Path], columns: int, rows: int, charmap: Optional[Iterable[int]]) -> Tileset: +def load_tilesheet( + path: Union[str, PathLike[str]], columns: int, rows: int, charmap: Optional[Iterable[int]] +) -> Tileset: """Return a new Tileset from a simple tilesheet image. `path` is the file path to a PNG file with the tileset. From b9571b3f6e92045dfba76d48e31126d15d61b2b0 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 7 Jan 2022 17:53:18 -0800 Subject: [PATCH 012/194] Update changelog to markdown. Updates related references and scripts. --- CHANGELOG.md | 1176 ++++++++++++++++++++++++ CHANGELOG.rst | 1339 ---------------------------- README.rst | 2 +- docs/changelog.rst | 2 +- scripts/get_release_description.py | 9 +- scripts/tag_release.py | 25 +- setup.py | 2 +- 7 files changed, 1197 insertions(+), 1358 deletions(-) create mode 100644 CHANGELOG.md delete mode 100755 CHANGELOG.rst diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..cabd4854 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,1176 @@ +# Changelog +Changes relevant to the users of python-tcod are documented here. + +This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. + +## [Unreleased] +### Added +- New experimental renderer `tcod.context.RENDERER_XTERM`. +### Fixed +- Functions accepting `Path`-like parameters now accept the more correct `os.PathLike` type. +- BDF files with blank lines no longer fail to load with an "Unknown keyword" error. + +## [13.2.0] - 2021-12-24 +### Added +- New `console` parameter in `tcod.context.new` which sets parameters from an existing Console. + +### Changed +- Using `libtcod 1.20.0`. + +### Fixed +- Fixed segfault when an OpenGL2 context fails to load. +- Gaussian number generation no longer affects the results of unrelated RNG's. +- Gaussian number generation is now reentrant and thread-safe. +- Fixed potential crash in PNG image loading. + +## [13.1.0] - 2021-10-22 +### Added +- Added the `tcod.tileset.procedural_block_elements` function. + +### Removed +- Python 3.6 is no longer supported. + +## [13.0.0] - 2021-09-20 +### Changed +- Console print and drawing functions now always use absolute coordinates for negative numbers. + +## [12.7.3] - 2021-08-13 +### Deprecated +- `tcod.console_is_key_pressed` was replaced with `tcod.event.get_keyboard_state`. +- `tcod.console_from_file` is deprecated. +- The `.asc` and `.apf` formats are no longer actively supported. + +### Fixed +- Fixed the parsing of SDL 2.0.16 headers. + +## [12.7.2] - 2021-07-01 +### Fixed +- *Scancode* and *KeySym* enums no longer crash when SDL returns an unexpected value. + +## [12.7.1] - 2021-06-30 +### Added +- Started uploading wheels for ARM64 macOS. + +## [12.7.0] - 2021-06-29 +### Added +- *tcod.image* and *tcod.tileset* now support *pathlib*. + +### Fixed +- Wheels for 32-bit Windows now deploy again. + +## [12.6.2] - 2021-06-15 +### Fixed +- Git is no longer required to install from source. + +## [12.6.1] - 2021-06-09 +### Fixed +- Fixed version mismatch when building from sources. + +## [12.6.0] - 2021-06-09 +### Added +- Added the *decoration* parameter to *Console.draw_frame*. + You may use this parameter to designate custom glyphs as the frame border. + +### Deprecated +- The handling of negative indexes given to console drawing and printing + functions will be changed to be used as absolute coordinates in the future. + +## [12.5.1] - 2021-05-30 +### Fixed +- The setup script should no longer fail silently when cffi is unavailable. + +## [12.5.0] - 2021-05-21 +### Changed +- `KeyboardEvent`'s '`scancode`, `sym`, and `mod` attributes now use their respective enums. + +## [12.4.0] - 2021-05-21 +### Added +- Added modernized REXPaint saving/loading functions. + - `tcod.console.load_xp` + - `tcod.console.save_xp` + +### Changed +- Using `libtcod 1.18.1`. +- `tcod.event.KeySym` and `tcod.event.Scancode` can now be hashed. + +## [12.3.2] - 2021-05-15 +### Changed +- Using `libtcod 1.17.1`. + +### Fixed +- Fixed regression with loading PNG images. + +## [12.3.1] - 2021-05-13 +### Fixed +- Fix Windows deployment. + +## [12.3.0] - 2021-05-13 +### Added +- New keyboard enums: + - `tcod.event.KeySym` + - `tcod.event.Scancode` + - `tcod.event.Modifier` +- New functions: + - `tcod.event.get_keyboard_state` + - `tcod.event.get_modifier_state` +- Added `tcod.console.rgb_graphic` and `tcod.console.rgba_graphic` dtypes. +- Another name for the Console array attributes: `Console.rgb` and `Console.rgba`. + +### Changed +- Using `libtcod 1.17.0`. + +### Deprecated +- `Console_tiles_rgb` is being renamed to `Console.rgb`. +- `Console_tiles` being renamed to `Console.rgba`. + +### Fixed +- Contexts now give a more useful error when pickled. +- Fixed regressions with `tcod.console_print_frame` and `Console.print_frame` + when given empty strings as the banner. + +## [12.2.0] - 2021-04-09 +### Added +- Added `tcod.noise.Algorithm` and `tcod.noise.Implementation` enums. +- Added `tcod.noise.grid` helper function. + +### Deprecated +- The non-enum noise implementation names have been deprecated. + +### Fixed +- Indexing Noise classes now works with the FBM implementation. + +## [12.1.0] - 2021-04-01 +### Added +- Added package-level PyInstaller hook. + +### Changed +- Using `libtcod 1.16.7`. +- `tcod.path.dijkstra2d` now returns the output and accepts an `out` parameter. + +### Deprecated +- In the future `tcod.path.dijkstra2d` will no longer modify the input by default. Until then an `out` parameter must be given. + +### Fixed +- Fixed crashes from loading tilesets with non-square tile sizes. +- Tilesets with a size of 0 should no longer crash when used. +- Prevent division by zero from recommended-console-size functions. + +## [12.0.0] - 2021-03-05 +### Deprecated +- The Random class will now warn if the seed it's given will not used + deterministically. It will no longer accept non-integer seeds in the future. + +### Changed +- Now bundles SDL 2.0.14 for MacOS. +- `tcod.event` can now detect and will warn about uninitialized tile + attributes on mouse events. + +### Removed +- Python 3.5 is no longer supported. +- The `tdl` module has been dropped. + +## [11.19.3] - 2021-01-07 +### Fixed +- Some wheels had broken version metadata. + +## [11.19.2] - 2020-12-30 +### Changed +- Now bundles SDL 2.0.10 for MacOS and SDL 2.0.14 for Windows. + +### Fixed +- MacOS wheels were failing to bundle dependencies for SDL2. + +## [11.19.1] - 2020-12-29 +### Fixed +- MacOS wheels failed to deploy for the previous version. + +## [11.19.0] - 2020-12-29 +### Added +- Added the important `order` parameter to `Context.new_console`. + +## [11.18.3] - 2020-12-28 +### Changed +- Now bundles SDL 2.0.14 for Windows/MacOS. + +### Deprecated +- Support for Python 3.5 will be dropped. +- `tcod.console_load_xp` has been deprecated, `tcod.console_from_xp` can load + these files without modifying an existing console. + +### Fixed +- `tcod.console_from_xp` now has better error handling (instead of crashing.) +- Can now compile with SDL 2.0.14 headers. + +## [11.18.2] - 2020-12-03 +### Fixed +- Fixed missing `tcod.FOV_SYMMETRIC_SHADOWCAST` constant. +- Fixed regression in `tcod.sys_get_current_resolution` behavior. This + function now returns the monitor resolution as was previously expected. + +## [11.18.1] - 2020-11-30 +### Fixed +- Code points from the Private Use Area will now print correctly. + +## [11.18.0] - 2020-11-13 +### Added +- New context method `Context.new_console`. + +### Changed +- Using `libtcod 1.16.0-alpha.15`. + +## [11.17.0] - 2020-10-30 +### Added +- New FOV implementation: `tcod.FOV_SYMMETRIC_SHADOWCAST`. + +### Changed +- Using `libtcod 1.16.0-alpha.14`. + +## [11.16.1] - 2020-10-28 +### Deprecated +- Changed context deprecations to PendingDeprecationWarning to reduce mass + panic from tutorial followers. + +### Fixed +- Fixed garbled titles and crashing on some platforms. + +## [11.16.0] - 2020-10-23 +### Added +- Added `tcod.context.new` function. +- Contexts now support a CLI. +- You can now provide the window x,y position when making contexts. +- `tcod.noise.Noise` instances can now be indexed to generate noise maps. + +### Changed +- Using `libtcod 1.16.0-alpha.13`. +- The OpenGL 2 renderer can now use `SDL_HINT_RENDER_SCALE_QUALITY` to + determine the tileset upscaling filter. +- Improved performance of the FOV_BASIC algorithm. + +### Deprecated +- `tcod.context.new_window` and `tcod.context.new_terminal` have been replaced + by `tcod.context.new`. + +### Fixed +- Pathfinders will now work with boolean arrays. +- Console blits now ignore alpha compositing which would result in division by + zero. +- `tcod.console_is_key_pressed` should work even if libtcod events are ignored. +- The `TCOD_RENDERER` and `TCOD_VSYNC` environment variables should work now. +- `FOV_PERMISSIVE` algorithm is now reentrant. + +## [11.15.3] - 2020-07-30 +### Fixed +- `tcod.tileset.Tileset.remap`, codepoint and index were swapped. + +## [11.15.2] - 2020-07-27 +### Fixed +- `tcod.path.dijkstra2d`, fixed corrupted output with int8 arrays. + +## [11.15.1] - 2020-07-26 +### Changed +- `tcod.event.EventDispatch` now uses the absolute names for event type hints + so that IDE's can better auto-complete method overrides. + +### Fixed +- Fixed libtcodpy heightmap data alignment issues on non-square maps. + +## [11.15.0] - 2020-06-29 +### Added +- `tcod.path.SimpleGraph` for pathfinding on simple 2D arrays. + +### Changed +- `tcod.path.CustomGraph` now accepts an `order` parameter. + +## [11.14.0] - 2020-06-23 +### Added +- New `tcod.los` module for NumPy-based line-of-sight algorithms. + Includes `tcod.los.bresenham`. + +### Deprecated +- `tcod.line_where` and `tcod.line_iter` have been deprecated. + +## [11.13.6] - 2020-06-19 +### Deprecated +- `console_init_root` and `console_set_custom_font` have been replaced by the + modern API. +- All functions which handle SDL windows without a context are deprecated. +- All functions which modify a globally active tileset are deprecated. +- `tcod.map.Map` is deprecated, NumPy arrays should be passed to functions + directly instead of through this class. + +## [11.13.5] - 2020-06-15 +### Fixed +- Install requirements will no longer try to downgrade `cffi`. + +## [11.13.4] - 2020-06-15 + +## [11.13.3] - 2020-06-13 +### Fixed +- `cffi` requirement has been updated to version `1.13.0`. + The older versions raise TypeError's. + +## [11.13.2] - 2020-06-12 +### Fixed +- SDL related errors during package installation are now more readable. + +## [11.13.1] - 2020-05-30 +### Fixed +- `tcod.event.EventDispatch`: `ev_*` methods now allow `Optional[T]` return + types. + +## [11.13.0] - 2020-05-22 +### Added +- `tcod.path`: New `Pathfinder` and `CustomGraph` classes. + +### Changed +- Added `edge_map` parameter to `tcod.path.dijkstra2d` and + `tcod.path.hillclimb2d`. + +### Fixed +- tcod.console_init_root` and context initializing functions were not + raising exceptions on failure. + +## [11.12.1] - 2020-05-02 +### Fixed +- Prevent adding non-existent 2nd halves to potential double-wide charterers. + +## [11.12.0] - 2020-04-30 +### Added +- Added `tcod.context` module. You now have more options for making libtcod + controlled contexts. +- `tcod.tileset.load_tilesheet`: Load a simple tilesheet as a Tileset. +- `Tileset.remap`: Reassign codepoints to tiles on a Tileset. +- `tcod.tileset.CHARMAP_CP437`: Character mapping for `load_tilesheet`. +- `tcod.tileset.CHARMAP_TCOD`: Older libtcod layout. + +### Changed +- `EventDispatch.dispatch` can now return the values returned by the `ev_*` + methods. The class is now generic to support type checking these values. +- Event mouse coordinates are now strictly int types. +- Submodules are now implicitly imported. + +## [11.11.4] - 2020-04-26 +### Changed +- Using `libtcod 1.16.0-alpha.10`. + +### Fixed +- Fixed characters being dropped when color codes were used. + +## [11.11.3] - 2020-04-24 +### Changed +- Using `libtcod 1.16.0-alpha.9`. + +### Fixed +- `FOV_DIAMOND` and `FOV_RESTRICTIVE` algorithms are now reentrant. + [libtcod#48](https://github.com/libtcod/libtcod/pull/48) +- The `TCOD_VSYNC` environment variable was being ignored. + +## [11.11.2] - 2020-04-22 + +## [11.11.1] - 2020-04-03 +### Changed +- Using `libtcod 1.16.0-alpha.8`. + +### Fixed +- Changing the active tileset now redraws tiles correctly on the next frame. + +## [11.11.0] - 2020-04-02 +### Added +- Added `Console.close` as a more obvious way to close the active window of a + root console. + +### Changed +- GCC is no longer needed to compile the library on Windows. +- Using `libtcod 1.16.0-alpha.7`. +- `tcod.console_flush` will now accept an RGB tuple as a `clear_color`. + +### Fixed +- Changing the active tileset will now properly show it on the next render. + +## [11.10.0] - 2020-03-26 +### Added +- Added `tcod.tileset.load_bdf`, you can now load BDF fonts. +- `tcod.tileset.set_default` and `tcod.tileset.get_default` are now stable. + +### Changed +- Using `libtcod 1.16.0-alpha.6`. + +### Deprecated +- The `snap_to_integer` parameter in `tcod.console_flush` has been deprecated + since it can cause minor scaling issues which don't exist when using + `integer_scaling` instead. + +## [11.9.2] - 2020-03-17 +### Fixed +- Fixed segfault after the Tileset returned by `tcod.tileset.get_default` goes + out of scope. + +## [11.9.1] - 2020-02-28 +### Changed +- Using `libtcod 1.16.0-alpha.5`. +- Mouse tile coordinates are now always zero before the first call to + `tcod.console_flush`. + +## [11.9.0] - 2020-02-22 +### Added +- New method `Tileset.render` renders an RGBA NumPy array from a tileset and + a console. + +## [11.8.2] - 2020-02-22 +### Fixed +- Prevent KeyError when representing unusual keyboard symbol constants. + +## [11.8.1] - 2020-02-22 +### Changed +- Using `libtcod 1.16.0-alpha.4`. + +### Fixed +- Mouse tile coordinates are now correct on any resized window. + +## [11.8.0] - 2020-02-21 +### Added +- Added `tcod.console.recommended_size` for when you want to change your main + console size at runtime. +- Added `Console.tiles_rgb` as a replacement for `Console.tiles2`. + +### Changed +- Using `libtcod 1.16.0-alpha.3`. +- Added parameters to `tcod.console_flush`, you can now manually provide a + console and adjust how it is presented. + +### Deprecated +- `Console.tiles2` is deprecated in favour of `Console.tiles_rgb`. +- `Console.buffer` is now deprecated in favour of `Console.tiles`, instead of + the other way around. + +### Fixed +- Fixed keyboard state and mouse state functions losing state when events were + flushed. + +## [11.7.2] - 2020-02-16 +### Fixed +- Fixed regression in `tcod.console_clear`. + +## [11.7.1] - 2020-02-16 +### Fixed +- Fixed regression in `Console.draw_frame`. +- The wavelet noise generator now excludes -1.0f and 1.0f as return values. +- Fixed console fading color regression. + +## [11.7.0] - 2020-02-14 +### Changed +- Using `libtcod 1.16.0-alpha.2`. +- When a renderer fails to load it will now fallback to a different one. + The order is: OPENGL2 -> OPENGL -> SDL2. +- The default renderer is now SDL2. +- The SDL and OPENGL renderers are no longer deprecated, but they now point to + slightly different backward compatible implementations. + +### Deprecated +- The use of `libtcod.cfg` and `terminal.png` is deprecated. + +### Fixed +- `tcod.sys_update_char` now works with the newer renderers. +- Fixed buffer overflow in name generator. +- `tcod.image_from_console` now works with the newer renderers. +- New renderers now auto-load fonts from `libtcod.cfg` or `terminal.png`. + +## [11.6.0] - 2019-12-05 +### Changed +- Console blit operations now perform per-cell alpha transparency. + +## [11.5.1] - 2019-11-23 +### Fixed +- Python 3.8 wheels failed to deploy. + +## [11.5.0] - 2019-11-22 +### Changed +- Quarter block elements are now rendered using Unicode instead of a custom + encoding. + +### Fixed +- `OPENGL` and `GLSL` renderers were not properly clearing space characters. + +## [11.4.1] - 2019-10-15 +### Added +- Uploaded Python 3.8 wheels to PyPI. + +## [11.4.0] - 2019-09-20 +### Added +- Added `__array_interface__` to the Image class. +- Added `Console.draw_semigraphics` as a replacement for blit_2x functions. + `draw_semigraphics` can handle array-like objects. +- `Image.from_array` class method creates an Image from an array-like object. +- `tcod.image.load` loads a PNG file as an RGBA array. + +### Changed +- `Console.tiles` is now named `Console.buffer`. + +## [11.3.0] - 2019-09-06 +### Added +- New attribute `Console.tiles2` is similar to `Console.tiles` but without an + alpha channel. + +## [11.2.2] - 2019-08-25 +### Fixed +- Fixed a regression preventing PyInstaller distributions from loading SDL2. + +## [11.2.1] - 2019-08-25 + +## [11.2.0] - 2019-08-24 +### Added +- `tcod.path.dijkstra2d`: Computes Dijkstra from an arbitrary initial state. +- `tcod.path.hillclimb2d`: Returns a path from a distance array. +- `tcod.path.maxarray`: Creates arrays filled with maximum finite values. + +### Fixed +- Changing the tiles of an active tileset on OPENGL2 will no longer leave + temporary artifact tiles. +- It's now harder to accidentally import tcod's internal modules. + +## [11.1.2] - 2019-08-02 +### Changed +- Now bundles SDL 2.0.10 for Windows/MacOS. + +### Fixed +- Can now parse SDL 2.0.10 headers during installation without crashing. + +## [11.1.1] - 2019-08-01 +### Deprecated +- Using an out-of-bounds index for field-of-view operations now raises a + warning, which will later become an error. + +### Fixed +- Changing the tiles of an active tileset will now work correctly. + +## [11.1.0] - 2019-07-05 +### Added +- You can now set the `TCOD_RENDERER` and `TCOD_VSYNC` environment variables to + force specific options to be used. + Example: ``TCOD_RENDERER=sdl2 TCOD_VSYNC=1`` + +### Changed +- `tcod.sys_set_renderer` now raises an exception if it fails. + +### Fixed +- `tcod.console_map_ascii_code_to_font` functions will now work when called + before `tcod.console_init_root`. + +## [11.0.2] - 2019-06-21 +### Changed +- You no longer need OpenGL to build python-tcod. + +## [11.0.1] - 2019-06-21 +### Changed +- Better runtime checks for Windows dependencies should now give distinct + errors depending on if the issue is SDL2 or missing redistributables. + +### Fixed +- Changed NumPy type hints from `np.array` to `np.ndarray` which should + resolve issues. + +## [11.0.0] - 2019-06-14 +### Changed +- `tcod.map.compute_fov` now takes a 2-item tuple instead of separate `x` and + `y` parameters. This causes less confusion over how axes are aligned. + +## [10.1.1] - 2019-06-02 +### Changed +- Better string representations for `tcod.event.Event` subclasses. + +### Fixed +- Fixed regressions in text alignment for non-rectangle print functions. + +## [10.1.0] - 2019-05-24 +### Added +- `tcod.console_init_root` now has an optional `vsync` parameter. + +## [10.0.5] - 2019-05-17 +### Fixed +- Fixed shader compilation issues in the OPENGL2 renderer. +- Fallback fonts should fail less on Linux. + +## [10.0.4] - 2019-05-17 +### Changed +- Now depends on cffi 0.12 or later. + +### Fixed +- `tcod.console_init_root` and `tcod.console_set_custom_font` will raise + exceptions instead of terminating. +- Fixed issues preventing `tcod.event` from working on 32-bit Windows. + +## [10.0.3] - 2019-05-10 +### Fixed +- Corrected bounding box issues with the `Console.print_box` method. + +## [10.0.2] - 2019-04-26 +### Fixed +- Resolved Color warnings when importing tcod. +- When compiling, fixed a name conflict with endianness macros on FreeBSD. + +## [10.0.1] - 2019-04-19 +### Fixed +- Fixed horizontal alignment for TrueType fonts. +- Fixed taking screenshots with the older SDL renderer. + +## [10.0.0] - 2019-03-29 +### Added +- New `Console.tiles` array attribute. +### Changed +- `Console.DTYPE` changed to add alpha to its color types. +### Fixed +- Console printing was ignoring color codes at the beginning of a string. + +## [9.3.0] - 2019-03-15 +### Added +- The SDL2/OPENGL2 renderers can potentially use a fall-back font when none + are provided. +- New function `tcod.event.get_mouse_state`. +- New function `tcod.map.compute_fov` lets you get a visibility array directly + from a transparency array. +### Deprecated +- The following functions and classes have been deprecated. + - `tcod.Key` + - `tcod.Mouse` + - `tcod.mouse_get_status` + - `tcod.console_is_window_closed` + - `tcod.console_check_for_keypress` + - `tcod.console_wait_for_keypress` + - `tcod.console_delete` + - `tcod.sys_check_for_event` + - `tcod.sys_wait_for_event` +- The SDL, OPENGL, and GLSL renderers have been deprecated. +- Many libtcodpy functions have been marked with PendingDeprecationWarning's. +### Fixed +- To be more compatible with libtcodpy `tcod.console_init_root` will default + to the SDL render, but will raise warnings when an old renderer is used. + +## [9.2.5] - 2019-03-04 +### Fixed +- Fixed `tcod.namegen_generate_custom`. + +## [9.2.4] - 2019-03-02 +### Fixed +- The `tcod` package is has been marked as typed and will now work with MyPy. + +## [9.2.3] - 2019-03-01 +### Deprecated +- The behavior for negative indexes on the new print functions may change in + the future. +- Methods and functionality preventing `tcod.Color` from behaving like a tuple + have been deprecated. + +## [9.2.2] - 2019-02-26 +### Fixed +- `Console.print_box` wasn't setting the background color by default. + +## [9.2.1] - 2019-02-25 +### Fixed +- `tcod.sys_get_char_size` fixed on the new renderers. + +## [9.2.0] - 2019-02-24 +### Added +- New `tcod.console.get_height_rect` function, which can be used to get the + height of a print call without an existing console. +- New `tcod.tileset` module, with a `set_truetype_font` function. +### Fixed +- The new print methods now handle alignment according to how they were + documented. +- `SDL2` and `OPENGL2` now support screenshots. +- Windows and MacOS builds now restrict exported SDL2 symbols to only + SDL 2.0.5; This will avoid hard to debug import errors when the wrong + version of SDL is dynamically linked. +- The root console now starts with a white foreground. + +## [9.1.0] - 2019-02-23 +### Added +- Added the `tcod.random.MULTIPLY_WITH_CARRY` constant. +### Changed +- The overhead for warnings has been reduced when running Python with the + optimize `-O` flag. +- `tcod.random.Random` now provides a default algorithm. + +## [9.0.0] - 2019-02-17 +### Changed +- New console methods now default to an `fg` and `bg` of None instead of + white-on-black. + +## [8.5.0] - 2019-02-15 +### Added +- `tcod.console.Console` now supports `str` and `repr`. +- Added new Console methods which are independent from the console defaults. +- You can now give an array when initializing a `tcod.console.Console` + instance. +- `Console.clear` can now take `ch`, `fg`, and `bg` parameters. +### Changed +- Updated libtcod to 1.10.6 +- Printing generates more compact layouts. +### Deprecated +- Most libtcodpy console functions have been replaced by the tcod.console + module. +- Deprecated the `set_key_color` functions. You can pass key colors to + `Console.blit` instead. +- `Console.clear` should be given the colors to clear with as parameters, + rather than by using `default_fg` or `default_bg`. +- Most functions which depend on console default values have been deprecated. + The new deprecation warnings will give details on how to make default values + explicit. +### Fixed +- `tcod.console.Console.blit` was ignoring the key color set by + `Console.set_key_color`. +- The `SDL2` and `OPENGL2` renders can now large numbers of tiles. + +## [8.4.3] - 2019-02-06 +### Changed +- Updated libtcod to 1.10.5 +- The SDL2/OPENGL2 renderers will now auto-detect a custom fonts key-color. + +## [8.4.2] - 2019-02-05 +### Deprecated +- The tdl module has been deprecated. +- The libtcodpy parser functions have been deprecated. +### Fixed +- `tcod.image_is_pixel_transparent` and `tcod.image_get_alpha` now return + values. +- `Console.print_frame` was clearing tiles outside if its bounds. +- The `FONT_LAYOUT_CP437` layout was incorrect. + +## [8.4.1] - 2019-02-01 +### Fixed +- Window event types were not upper-case. +- Fixed regression where libtcodpy mouse wheel events unset mouse coordinates. + +## [8.4.0] - 2019-01-31 +### Added +- Added tcod.event module, based off of the sdlevent.py shim. +### Changed +- Updated libtcod to 1.10.3 +### Fixed +- Fixed libtcodpy `struct_add_value_list` function. +- Use correct math for tile-based delta in mouse events. +- New renderers now support tile-based mouse coordinates. +- SDL2 renderer will now properly refresh after the window is resized. + +## [8.3.2] - 2018-12-28 +### Fixed +- Fixed rare access violations for some functions which took strings as + parameters, such as `tcod.console_init_root`. + +## [8.3.1] - 2018-12-28 +### Fixed +- libtcodpy key and mouse functions will no longer accept the wrong types. +- The `new_struct` method was not being called for libtcodpy's custom parsers. + +## [8.3.0] - 2018-12-08 +### Added +- Added BSP traversal methods in tcod.bsp for parity with libtcodpy. +### Deprecated +- Already deprecated bsp functions are now even more deprecated. + +## [8.2.0] - 2018-11-27 +### Added +- New layout `tcod.FONT_LAYOUT_CP437`. +### Changed +- Updated libtcod to 1.10.2 +- `tcod.console_print_frame` and `Console.print_frame` now support Unicode + strings. +### Deprecated +- Deprecated using bytes strings for all printing functions. +### Fixed +- Console objects are now initialized with spaces. This fixes some blit + operations. +- Unicode code-points above U+FFFF will now work on all platforms. + +## [8.1.1] - 2018-11-16 +### Fixed +- Printing a frame with an empty string no longer displays a title bar. + +## [8.1.0] - 2018-11-15 +### Changed +- Heightmap functions now support 'F_CONTIGUOUS' arrays. +- `tcod.heightmap_new` now has an `order` parameter. +- Updated SDL to 2.0.9 +### Deprecated +- Deprecated heightmap functions which sample noise grids, this can be done + using the `Noise.sample_ogrid` method. + +## [8.0.0] - 2018-11-02 +### Changed +- The default renderer can now be anything if not set manually. +- Better error message for when a font file isn't found. + +## [7.0.1] - 2018-10-27 +### Fixed +- Building from source was failing because `console_2tris.glsl*` was missing + from source distributions. + +## [7.0.0] - 2018-10-25 +### Added +- New `RENDERER_SDL2` and `RENDERER_OPENGL2` renderers. +### Changed +- Updated libtcod to 1.9.0 +- Now requires SDL 2.0.5, which is not trivially installable on + Ubuntu 16.04 LTS. +### Removed +- Dropped support for Python versions before 3.5 +- Dropped support for MacOS versions before 10.9 Mavericks. + +## [6.0.7] - 2018-10-24 +### Fixed +- The root console no longer loses track of buffers and console defaults on a + renderer change. + +## [6.0.6] - 2018-10-01 +### Fixed +- Replaced missing wheels for older and 32-bit versions of MacOS. + +## [6.0.5] - 2018-09-28 +### Fixed +- Resolved CDefError error during source installs. + +## [6.0.4] - 2018-09-11 +### Fixed +- tcod.Key right-hand modifiers are now set independently at initialization, + instead of mirroring the left-hand modifier value. + +## [6.0.3] - 2018-09-05 +### Fixed +- tcod.Key and tcod.Mouse no longer ignore initiation parameters. + +## [6.0.2] - 2018-08-28 +### Fixed +- Fixed color constants missing at build-time. + +## [6.0.1] - 2018-08-24 +### Fixed +- Source distributions were missing C++ source files. + +## [6.0.0] - 2018-08-23 +### Changed +- Project renamed to tcod on PyPI. +### Deprecated +- Passing bytes strings to libtcodpy print functions is deprecated. +### Fixed +- Fixed libtcodpy print functions not accepting bytes strings. +- libtcod constants are now generated at build-time fixing static analysis + tools. + +## [5.0.1] - 2018-07-08 +### Fixed +- tdl.event no longer crashes with StopIteration on Python 3.7 + +## [5.0.0] - 2018-07-05 +### Changed +- tcod.path: all classes now use `shape` instead of `width` and `height`. +- tcod.path now respects NumPy array shape, instead of assuming that arrays + need to be transposed from C memory order. From now on `x` and `y` mean + 1st and 2nd axis. This doesn't affect non-NumPy code. +- tcod.path now has full support of non-contiguous memory. + +## [4.6.1] - 2018-06-30 +### Added +- New function `tcod.line_where` for indexing NumPy arrays using a Bresenham + line. +### Deprecated +- Python 2.7 support will be dropped in the near future. + +## [4.5.2] - 2018-06-29 +### Added +- New wheels for Python3.7 on Windows. +### Fixed +- Arrays from `tcod.heightmap_new` are now properly zeroed out. + +## [4.5.1] - 2018-06-23 +### Deprecated +- Deprecated all libtcodpy map functions. +### Fixed +- `tcod.map_copy` could break the `tcod.map.Map` class. +- `tcod.map_clear` `transparent` and `walkable` parameters were reversed. +- When multiple SDL2 headers were installed, the wrong ones would be used when + the library is built. +- Fails to build via pip unless Numpy is installed first. + +## [4.5.0] - 2018-06-12 +### Changed +- Updated libtcod to v1.7.0 +- Updated SDL to v2.0.8 +- Error messages when failing to create an SDL window should be a less vague. +- You no longer need to initialize libtcod before you can print to an + off-screen console. +### Fixed +- Avoid crashes if the root console has a character code higher than expected. +### Removed +- No more debug output when loading fonts. + +## [4.4.0] - 2018-05-02 +### Added +- Added the libtcodpy module as an alias for tcod. Actual use of it is + deprecated, it exists primarily for backward compatibility. +- Adding missing libtcodpy functions `console_has_mouse_focus` and + `console_is_active`. +### Changed +- Updated libtcod to v1.6.6 + +## [4.3.2] - 2018-03-18 +### Deprecated +- Deprecated the use of falsy console parameters with libtcodpy functions. +### Fixed +- Fixed libtcodpy image functions not supporting falsy console parameters. +- Fixed tdl `Window.get_char` method. (Kaczor2704) + +## [4.3.1] - 2018-03-07 +### Fixed +- Fixed cffi.api.FFIError "unsupported expression: expected a simple numeric + constant" error when building on platforms with an older cffi module and + newer SDL headers. +- tcod/tdl Map and Console objects were not saving stride data when pickled. + +## [4.3.0] - 2018-02-01 +### Added +- You can now set the numpy memory order on tcod.console.Console, + tcod.map.Map, and tdl.map.Map objects well as from the + tcod.console_init_root function. +### Changed +- The `console_init_root` `title` parameter is now optional. +### Fixed +- OpenGL renderer alpha blending is now consistent with all other render + modes. + +## [4.2.3] - 2018-01-06 +### Fixed +- Fixed setup.py regression that could prevent building outside of the git + repository. + +## [4.2.2] - 2018-01-06 +### Fixed +- The Windows dynamic linker will now prefer the bundled version of SDL. + This fixes: + "ImportError: DLL load failed: The specified procedure could not be found." +- `key.c` is no longer set when `key.vk == KEY_TEXT`, this fixes a regression + which was causing events to be heard twice in the libtcod/Python tutorial. + +## [4.2.0] - 2018-01-02 +### Changed +- Updated libtcod backend to v1.6.4 +- Updated SDL to v2.0.7 for Windows/MacOS. +### Removed +- Source distributions no longer include tests, examples, or fonts. + [Find these on GitHub.](https://github.com/libtcod/python-tcod) +### Fixed +- Fixed "final link failed: Nonrepresentable section on output" error + when compiling for Linux. +- `tcod.console_init_root` defaults to the SDL renderer, other renderers + cause issues with mouse movement events. + +## [4.1.1] - 2017-11-02 +### Fixed +- Fixed `ConsoleBuffer.blit` regression. +- Console defaults corrected, the root console's blend mode and alignment is + the default value for newly made Console's. +- You can give a byte string as a filename to load parsers. + +## [4.1.0] - 2017-07-19 +### Added +- tdl Map class can now be pickled. +### Changed +- Added protection to the `transparent`, `walkable`, and `fov` + attributes in tcod and tdl Map classes, to prevent them from being + accidentally overridden. +- tcod and tdl Map classes now use numpy arrays as their attributes. + +## [4.0.1] - 2017-07-12 +### Fixed +- tdl: Fixed NameError in `set_fps`. + +## [4.0.0] - 2017-07-08 +### Changed +- tcod.bsp: `BSP.split_recursive` parameter `random` is now `seed`. +- tcod.console: `Console.blit` parameters have been rearranged. + Most of the parameters are now optional. +- tcod.noise: `Noise.__init__` parameter `rand` is now named `seed`. +- tdl: Changed `set_fps` parameter name to `fps`. +### Fixed +- tcod.bsp: Corrected spelling of max_vertical_ratio. + +## [3.2.0] - 2017-07-04 +### Changed +- Merged libtcod-cffi dependency with TDL. +### Fixed +- Fixed boolean related crashes with Key 'text' events. +- tdl.noise: Fixed crash when given a negative seed. As well as cases + where an instance could lose its seed being pickled. + +## [3.1.0] - 2017-05-28 +### Added +- You can now pass tdl Console instances as parameters to libtcod-cffi + functions expecting a tcod Console. +### Changed +- Dependencies updated: `libtcod-cffi>=2.5.0,<3` +- The `Console.tcod_console` attribute is being renamed to + `Console.console_c`. +### Deprecated +- The tdl.noise and tdl.map modules will be deprecated in the future. +### Fixed +- Resolved crash-on-exit issues for Windows platforms. + +## [3.0.2] - 2017-04-13 +### Changed +- Dependencies updated: `libtcod-cffi>=2.4.3,<3` +- You can now create Console instances before a call to `tdl.init`. +### Removed +- Dropped support for Python 3.3 +### Fixed +- Resolved issues with MacOS builds. +- 'OpenGL' and 'GLSL' renderers work again. + +## [3.0.1] - 2017-03-22 +### Changed +- `KeyEvent`'s with `text` now have all their modifier keys set to False. +### Fixed +- Undefined behavior in text events caused crashes on 32-bit builds. + +## [3.0.0] - 2017-03-21 +### Added +- `KeyEvent` supports libtcod text and meta keys. +### Changed +- `KeyEvent` parameters have been moved. +- This version requires `libtcod-cffi>=2.3.0`. +### Deprecated +- `KeyEvent` camel capped attribute names are deprecated. +### Fixed +- Crashes with key-codes undefined by libtcod. +- `tdl.map` typedef issues with libtcod-cffi. + + +## [2.0.1] - 2017-02-22 +### Fixed +- `tdl.init` renderer was defaulted to OpenGL which is not supported in the + current version of libtcod. + +## [2.0.0] - 2017-02-15 +### Changed +- Dependencies updated, tdl now requires libtcod-cffi 2.x.x +- Some event behaviors have changed with SDL2, event keys might be different + than what you expect. +### Removed +- Key repeat functions were removed from SDL2. + `set_key_repeat` is now stubbed, and does nothing. + +## [1.6.0] - 2016-11-18 +- Console.blit methods can now take fg_alpha and bg_alpha parameters. + +## [1.5.3] - 2016-06-04 +- set_font no longer crashes when loading a file without the implied font + size in its name + +## [1.5.2] - 2016-03-11 +- Fixed non-square Map instances + +## [1.5.1] - 2015-12-20 +- Fixed errors with Unicode and non-Unicode literals on Python 2 +- Fixed attribute error in compute_fov + +## [1.5.0] - 2015-07-13 +- python-tdl distributions are now universal builds +- New Map class +- map.bresenham now returns a list +- This release will require libtcod-cffi v0.2.3 or later + +## [1.4.0] - 2015-06-22 +- The DLL's have been moved into another library which you can find at + https://github.com/HexDecimal/libtcod-cffi + You can use this library to have some raw access to libtcod if you want. + Plus it can be used alongside TDL. +- The libtcod console objects in Console instances have been made public. +- Added tdl.event.wait function. This function can called with a timeout and + can automatically call tdl.flush. + +## [1.3.1] - 2015-06-19 +- Fixed pathfinding regressions. + +## [1.3.0] - 2015-06-19 +- Updated backend to use python-cffi instead of ctypes. This gives decent + boost to speed in CPython and a drastic to boost in speed in PyPy. + +## [1.2.0] - 2015-06-06 +- The set_colors method now changes the default colors used by the draw_* + methods. You can use Python's Ellipsis to explicitly select default colors + this way. +- Functions and Methods renamed to match Python's style-guide PEP 8, the old + function names still exist and are depreciated. +- The fgcolor and bgcolor parameters have been shortened to fg and bg. + +## [1.1.7] - 2015-03-19 +- Noise generator now seeds properly. +- The OS event queue will now be handled during a call to tdl.flush. This + prevents a common newbie programmer hang where events are handled + infrequently during long animations, simulations, or early development. +- Fixed a major bug that would cause a crash in later versions of Python 3 + +## [1.1.6] - 2014-06-27 +- Fixed a race condition when importing on some platforms. +- Fixed a type issue with quickFOV on Linux. +- Added a bresenham function to the tdl.map module. + +## [1.1.5] - 2013-11-10 +- A for loop can iterate over all coordinates of a Console. +- drawStr can be configured to scroll or raise an error. +- You can now configure or disable key repeating with tdl.event.setKeyRepeat +- Typewriter class removed, use a Window instance for the same functionality. +- setColors method fixed. + +## [1.1.4] - 2013-03-06 +- Merged the Typewriter and MetaConsole classes, + You now have a virtual cursor with Console and Window objects. +- Fixed the clear method on the Window class. +- Fixed screenshot function. +- Fixed some drawing operations with unchanging backgrounds. +- Instances of Console and Noise can be pickled and copied. +- Added KeyEvent.keychar +- Fixed event.keyWait, and now converts window closed events into Alt+F4. + +## [1.1.3] - 2012-12-17 +- Some of the setFont parameters were incorrectly labeled and documented. +- setFont can auto-detect tilesets if the font sizes are in the filenames. +- Added some X11 unicode tilesets, including Unifont. + +## [1.1.2] - 2012-12-13 +- Window title now defaults to the running scripts filename. +- Fixed incorrect deltaTime for App.update +- App will no longer call tdl.flush on its own, you'll need to call this + yourself. +- tdl.noise module added. +- clear method now defaults to black on black. + +## [1.1.1] - 2012-12-05 +- Map submodule added with AStar class and quickFOV function. +- New Typewriter class. +- Most console functions can use Python-style negative indexes now. +- New App.runOnce method. +- Rectangle geometry is less strict. + +## [1.1.0] - 2012-10-04 +- KeyEvent.keyname is now KeyEvent.key +- MouseButtonEvent.button now behaves like KeyEvent.keyname does. +- event.App class added. +- Drawing methods no longer have a default for the character parameter. +- KeyEvent.ctrl is now KeyEvent.control + +## [1.0.8] - 2010-04-07 +- No longer works in Python 2.5 but now works in 3.x and has been partly + tested. +- Many bug fixes. + +## [1.0.5] - 2010-04-06 +- Got rid of setuptools dependency, this will make it much more compatible + with Python 3.x +- Fixed a typo with the MacOS library import. + +## [1.0.4] - 2010-04-06 +- All constant colors (C_*) have been removed, they may be put back in later. +- Made some type assertion failures show the value they received to help in + general debugging. Still working on it. +- Added MacOS and 64-bit Linux support. + +## [1.0.0] - 2009-01-31 +- First public release. diff --git a/CHANGELOG.rst b/CHANGELOG.rst deleted file mode 100755 index 6e5ed581..00000000 --- a/CHANGELOG.rst +++ /dev/null @@ -1,1339 +0,0 @@ -=========== - Changelog -=========== -Changes relevant to the users of python-tcod are documented here. - -This project adheres to `Semantic Versioning `_ since -v2.0.0 - -Unreleased ------------------- -Added - - New experimental renderer `tcod.context.RENDERER_XTERM`. - -Fixed - - Functions accepting `Path`-like parameters now accept the more correct `os.PathLike` type. - - BDF files with blank lines no longer fail to load with an "Unknown keyword" error. - -13.2.0 - 2021-12-24 -------------------- -Added - - New `console` parameter in `tcod.context.new` which sets parameters from an existing Console. - -Changed - - Using `libtcod 1.20.0`. - -Fixed - - Fixed segfault when an OpenGL2 context fails to load. - - Gaussian number generation no longer affects the results of unrelated RNG's. - - Gaussian number generation is now reentrant and thread-safe. - - Fixed potential crash in PNG image loading. - -13.1.0 - 2021-10-22 -------------------- -Added - - Added the `tcod.tileset.procedural_block_elements` function. - -Removed - - Python 3.6 is no longer supported. - -13.0.0 - 2021-09-20 -------------------- -Changed - - Console print and drawing functions now always use absolute coordinates for negative numbers. - -12.7.3 - 2021-08-13 -------------------- -Deprecated - - `tcod.console_is_key_pressed` was replaced with `tcod.event.get_keyboard_state`. - - `tcod.console_from_file` is deprecated. - - The `.asc` and `.apf` formats are no longer actively supported. - -Fixed - - Fixed the parsing of SDL 2.0.16 headers. - -12.7.2 - 2021-07-01 -------------------- -Fixed - - *Scancode* and *KeySym* enums no longer crash when SDL returns an unexpected value. - -12.7.1 - 2021-06-30 -------------------- -Added - - Started uploading wheels for ARM64 macOS. - -12.7.0 - 2021-06-29 -------------------- -Added - - *tcod.image* and *tcod.tileset* now support *pathlib*. - -Fixed - - Wheels for 32-bit Windows now deploy again. - -12.6.2 - 2021-06-15 -------------------- -Fixed - - Git is no longer required to install from source. - -12.6.1 - 2021-06-09 -------------------- -Fixed - - Fixed version mismatch when building from sources. - -12.6.0 - 2021-06-09 -------------------- -Added - - Added the *decoration* parameter to *Console.draw_frame*. - You may use this parameter to designate custom glyphs as the frame border. - -Deprecated - - The handling of negative indexes given to console drawing and printing - functions will be changed to be used as absolute coordinates in the future. - -12.5.1 - 2021-05-30 -------------------- -Fixed - - The setup script should no longer fail silently when cffi is unavailable. - -12.5.0 - 2021-05-21 -------------------- -Changed - - `KeyboardEvent`'s '`scancode`, `sym`, and `mod` attributes now use their respective enums. - -12.4.0 - 2021-05-21 -------------------- -Added - - Added modernized REXPaint saving/loading functions. - - `tcod.console.load_xp` - - `tcod.console.save_xp` - -Changed - - Using `libtcod 1.18.1`. - - `tcod.event.KeySym` and `tcod.event.Scancode` can now be hashed. - -12.3.2 - 2021-05-15 -------------------- -Changed - - Using `libtcod 1.17.1`. - -Fixed - - Fixed regression with loading PNG images. - -12.3.1 - 2021-05-13 -------------------- -Fixed - - Fix Windows deployment. - -12.3.0 - 2021-05-13 -------------------- -Added - - New keyboard enums: - - `tcod.event.KeySym` - - `tcod.event.Scancode` - - `tcod.event.Modifier` - - New functions: - - `tcod.event.get_keyboard_state` - - `tcod.event.get_modifier_state` - - Added `tcod.console.rgb_graphic` and `tcod.console.rgba_graphic` dtypes. - - Another name for the Console array attributes: `Console.rgb` and `Console.rgba`. - -Changed - - Using `libtcod 1.17.0`. - -Deprecated - - `Console_tiles_rgb` is being renamed to `Console.rgb`. - - `Console_tiles` being renamed to `Console.rgba`. - -Fixed - - Contexts now give a more useful error when pickled. - - Fixed regressions with `tcod.console_print_frame` and `Console.print_frame` - when given empty strings as the banner. - -12.2.0 - 2021-04-09 -------------------- -Added - - Added `tcod.noise.Algorithm` and `tcod.noise.Implementation` enums. - - Added `tcod.noise.grid` helper function. - -Deprecated - - The non-enum noise implementation names have been deprecated. - -Fixed - - Indexing Noise classes now works with the FBM implementation. - -12.1.0 - 2021-04-01 -------------------- -Added - - Added package-level PyInstaller hook. - -Changed - - Using `libtcod 1.16.7`. - - `tcod.path.dijkstra2d` now returns the output and accepts an `out` parameter. - -Deprecated - - In the future `tcod.path.dijkstra2d` will no longer modify the input by default. Until then an `out` parameter must be given. - -Fixed - - Fixed crashes from loading tilesets with non-square tile sizes. - - Tilesets with a size of 0 should no longer crash when used. - - Prevent division by zero from recommended-console-size functions. - -12.0.0 - 2021-03-05 -------------------- -Deprecated - - The Random class will now warn if the seed it's given will not used - deterministically. It will no longer accept non-integer seeds in the future. - -Changed - - Now bundles SDL 2.0.14 for MacOS. - - `tcod.event` can now detect and will warn about uninitialized tile - attributes on mouse events. - -Removed - - Python 3.5 is no longer supported. - - The `tdl` module has been dropped. - -11.19.3 - 2021-01-07 --------------------- -Fixed - - Some wheels had broken version metadata. - -11.19.2 - 2020-12-30 --------------------- -Changed - - Now bundles SDL 2.0.10 for MacOS and SDL 2.0.14 for Windows. - -Fixed - - MacOS wheels were failing to bundle dependencies for SDL2. - -11.19.1 - 2020-12-29 --------------------- -Fixed - - MacOS wheels failed to deploy for the previous version. - -11.19.0 - 2020-12-29 --------------------- -Added - - Added the important `order` parameter to `Context.new_console`. - -11.18.3 - 2020-12-28 --------------------- -Changed - - Now bundles SDL 2.0.14 for Windows/MacOS. - -Deprecated - - Support for Python 3.5 will be dropped. - - `tcod.console_load_xp` has been deprecated, `tcod.console_from_xp` can load - these files without modifying an existing console. - -Fixed - - `tcod.console_from_xp` now has better error handling (instead of crashing.) - - Can now compile with SDL 2.0.14 headers. - -11.18.2 - 2020-12-03 --------------------- -Fixed - - Fixed missing `tcod.FOV_SYMMETRIC_SHADOWCAST` constant. - - Fixed regression in `tcod.sys_get_current_resolution` behavior. This - function now returns the monitor resolution as was previously expected. - -11.18.1 - 2020-11-30 --------------------- -Fixed - - Code points from the Private Use Area will now print correctly. - -11.18.0 - 2020-11-13 --------------------- -Added - - New context method `Context.new_console`. - -Changed - - Using `libtcod 1.16.0-alpha.15`. - -11.17.0 - 2020-10-30 --------------------- -Added - - New FOV implementation: `tcod.FOV_SYMMETRIC_SHADOWCAST`. - -Changed - - Using `libtcod 1.16.0-alpha.14`. - -11.16.1 - 2020-10-28 --------------------- -Deprecated - - Changed context deprecations to PendingDeprecationWarning to reduce mass - panic from tutorial followers. - -Fixed - - Fixed garbled titles and crashing on some platforms. - -11.16.0 - 2020-10-23 --------------------- -Added - - Added `tcod.context.new` function. - - Contexts now support a CLI. - - You can now provide the window x,y position when making contexts. - - `tcod.noise.Noise` instances can now be indexed to generate noise maps. - -Changed - - Using `libtcod 1.16.0-alpha.13`. - - The OpenGL 2 renderer can now use `SDL_HINT_RENDER_SCALE_QUALITY` to - determine the tileset upscaling filter. - - Improved performance of the FOV_BASIC algorithm. - -Deprecated - - `tcod.context.new_window` and `tcod.context.new_terminal` have been replaced - by `tcod.context.new`. - -Fixed - - Pathfinders will now work with boolean arrays. - - Console blits now ignore alpha compositing which would result in division by - zero. - - `tcod.console_is_key_pressed` should work even if libtcod events are ignored. - - The `TCOD_RENDERER` and `TCOD_VSYNC` environment variables should work now. - - `FOV_PERMISSIVE` algorithm is now reentrant. - -11.15.3 - 2020-07-30 --------------------- -Fixed - - `tcod.tileset.Tileset.remap`, codepoint and index were swapped. - -11.15.2 - 2020-07-27 --------------------- -Fixed - - `tcod.path.dijkstra2d`, fixed corrupted output with int8 arrays. - -11.15.1 - 2020-07-26 --------------------- -Changed - - `tcod.event.EventDispatch` now uses the absolute names for event type hints - so that IDE's can better auto-complete method overrides. - -Fixed - - Fixed libtcodpy heightmap data alignment issues on non-square maps. - -11.15.0 - 2020-06-29 --------------------- -Added - - `tcod.path.SimpleGraph` for pathfinding on simple 2D arrays. - -Changed - - `tcod.path.CustomGraph` now accepts an `order` parameter. - -11.14.0 - 2020-06-23 --------------------- -Added - - New `tcod.los` module for NumPy-based line-of-sight algorithms. - Includes `tcod.los.bresenham`. - -Deprecated - - `tcod.line_where` and `tcod.line_iter` have been deprecated. - -11.13.6 - 2020-06-19 --------------------- -Deprecated - - `console_init_root` and `console_set_custom_font` have been replaced by the - modern API. - - All functions which handle SDL windows without a context are deprecated. - - All functions which modify a globally active tileset are deprecated. - - `tcod.map.Map` is deprecated, NumPy arrays should be passed to functions - directly instead of through this class. - -11.13.5 - 2020-06-15 --------------------- -Fixed - - Install requirements will no longer try to downgrade `cffi`. - -11.13.4 - 2020-06-15 --------------------- - -11.13.3 - 2020-06-13 --------------------- -Fixed - - `cffi` requirement has been updated to version `1.13.0`. - The older versions raise TypeError's. - -11.13.2 - 2020-06-12 --------------------- -Fixed - - SDL related errors during package installation are now more readable. - -11.13.1 - 2020-05-30 --------------------- -Fixed - - `tcod.event.EventDispatch`: `ev_*` methods now allow `Optional[T]` return - types. - -11.13.0 - 2020-05-22 --------------------- -Added - - `tcod.path`: New `Pathfinder` and `CustomGraph` classes. - -Changed - - Added `edge_map` parameter to `tcod.path.dijkstra2d` and - `tcod.path.hillclimb2d`. - -Fixed - - tcod.console_init_root` and context initializing functions were not - raising exceptions on failure. - -11.12.1 - 2020-05-02 --------------------- -Fixed - - Prevent adding non-existent 2nd halves to potential double-wide charterers. - -11.12.0 - 2020-04-30 --------------------- -Added - - Added `tcod.context` module. You now have more options for making libtcod - controlled contexts. - - `tcod.tileset.load_tilesheet`: Load a simple tilesheet as a Tileset. - - `Tileset.remap`: Reassign codepoints to tiles on a Tileset. - - `tcod.tileset.CHARMAP_CP437`: Character mapping for `load_tilesheet`. - - `tcod.tileset.CHARMAP_TCOD`: Older libtcod layout. - -Changed - - `EventDispatch.dispatch` can now return the values returned by the `ev_*` - methods. The class is now generic to support type checking these values. - - Event mouse coordinates are now strictly int types. - - Submodules are now implicitly imported. - -11.11.4 - 2020-04-26 --------------------- -Changed - - Using `libtcod 1.16.0-alpha.10`. - -Fixed - - Fixed characters being dropped when color codes were used. - -11.11.3 - 2020-04-24 --------------------- -Changed - - Using `libtcod 1.16.0-alpha.9`. - -Fixed - - `FOV_DIAMOND` and `FOV_RESTRICTIVE` algorithms are now reentrant. - `libtcod#48 `_ - - The `TCOD_VSYNC` environment variable was being ignored. - -11.11.2 - 2020-04-22 --------------------- - -11.11.1 - 2020-04-03 --------------------- -Changed - - Using `libtcod 1.16.0-alpha.8`. - -Fixed - - Changing the active tileset now redraws tiles correctly on the next frame. - -11.11.0 - 2020-04-02 --------------------- -Added - - Added `Console.close` as a more obvious way to close the active window of a - root console. - -Changed - - GCC is no longer needed to compile the library on Windows. - - Using `libtcod 1.16.0-alpha.7`. - - `tcod.console_flush` will now accept an RGB tuple as a `clear_color`. - -Fixed - - Changing the active tileset will now properly show it on the next render. - -11.10.0 - 2020-03-26 --------------------- -Added - - Added `tcod.tileset.load_bdf`, you can now load BDF fonts. - - `tcod.tileset.set_default` and `tcod.tileset.get_default` are now stable. - -Changed - - Using `libtcod 1.16.0-alpha.6`. - -Deprecated - - The `snap_to_integer` parameter in `tcod.console_flush` has been deprecated - since it can cause minor scaling issues which don't exist when using - `integer_scaling` instead. - -11.9.2 - 2020-03-17 -------------------- -Fixed - - Fixed segfault after the Tileset returned by `tcod.tileset.get_default` goes - out of scope. - -11.9.1 - 2020-02-28 -------------------- -Changed - - Using `libtcod 1.16.0-alpha.5`. - - Mouse tile coordinates are now always zero before the first call to - `tcod.console_flush`. - -11.9.0 - 2020-02-22 -------------------- -Added - - New method `Tileset.render` renders an RGBA NumPy array from a tileset and - a console. - -11.8.2 - 2020-02-22 -------------------- -Fixed - - Prevent KeyError when representing unusual keyboard symbol constants. - -11.8.1 - 2020-02-22 -------------------- -Changed - - Using `libtcod 1.16.0-alpha.4`. - -Fixed - - Mouse tile coordinates are now correct on any resized window. - -11.8.0 - 2020-02-21 -------------------- -Added - - Added `tcod.console.recommended_size` for when you want to change your main - console size at runtime. - - Added `Console.tiles_rgb` as a replacement for `Console.tiles2`. - -Changed - - Using `libtcod 1.16.0-alpha.3`. - - Added parameters to `tcod.console_flush`, you can now manually provide a - console and adjust how it is presented. - -Deprecated - - `Console.tiles2` is deprecated in favour of `Console.tiles_rgb`. - - `Console.buffer` is now deprecated in favour of `Console.tiles`, instead of - the other way around. - -Fixed - - Fixed keyboard state and mouse state functions losing state when events were - flushed. - -11.7.2 - 2020-02-16 -------------------- -Fixed - - Fixed regression in `tcod.console_clear`. - -11.7.1 - 2020-02-16 -------------------- -Fixed - - Fixed regression in `Console.draw_frame`. - - The wavelet noise generator now excludes -1.0f and 1.0f as return values. - - Fixed console fading color regression. - -11.7.0 - 2020-02-14 -------------------- -Changed - - Using `libtcod 1.16.0-alpha.2`. - - When a renderer fails to load it will now fallback to a different one. - The order is: OPENGL2 -> OPENGL -> SDL2. - - The default renderer is now SDL2. - - The SDL and OPENGL renderers are no longer deprecated, but they now point to - slightly different backward compatible implementations. - -Deprecated - - The use of `libtcod.cfg` and `terminal.png` is deprecated. - -Fixed - - `tcod.sys_update_char` now works with the newer renderers. - - Fixed buffer overflow in name generator. - - `tcod.image_from_console` now works with the newer renderers. - - New renderers now auto-load fonts from `libtcod.cfg` or `terminal.png`. - -11.6.0 - 2019-12-05 -------------------- -Changed - - Console blit operations now perform per-cell alpha transparency. - -11.5.1 - 2019-11-23 -------------------- -Fixed - - Python 3.8 wheels failed to deploy. - -11.5.0 - 2019-11-22 -------------------- -Changed - - Quarter block elements are now rendered using Unicode instead of a custom - encoding. - -Fixed - - `OPENGL` and `GLSL` renderers were not properly clearing space characters. - -11.4.1 - 2019-10-15 -------------------- -Added - - Uploaded Python 3.8 wheels to PyPI. - -11.4.0 - 2019-09-20 -------------------- -Added - - Added `__array_interface__` to the Image class. - - Added `Console.draw_semigraphics` as a replacement for blit_2x functions. - `draw_semigraphics` can handle array-like objects. - - `Image.from_array` class method creates an Image from an array-like object. - - `tcod.image.load` loads a PNG file as an RGBA array. - -Changed - - `Console.tiles` is now named `Console.buffer`. - -11.3.0 - 2019-09-06 -------------------- -Added - - New attribute `Console.tiles2` is similar to `Console.tiles` but without an - alpha channel. - -11.2.2 - 2019-08-25 -------------------- -Fixed - - Fixed a regression preventing PyInstaller distributions from loading SDL2. - -11.2.1 - 2019-08-25 -------------------- - -11.2.0 - 2019-08-24 -------------------- -Added - - `tcod.path.dijkstra2d`: Computes Dijkstra from an arbitrary initial state. - - `tcod.path.hillclimb2d`: Returns a path from a distance array. - - `tcod.path.maxarray`: Creates arrays filled with maximum finite values. - -Fixed - - Changing the tiles of an active tileset on OPENGL2 will no longer leave - temporary artifact tiles. - - It's now harder to accidentally import tcod's internal modules. - -11.1.2 - 2019-08-02 -------------------- -Changed - - Now bundles SDL 2.0.10 for Windows/MacOS. - -Fixed - - Can now parse SDL 2.0.10 headers during installation without crashing. - -11.1.1 - 2019-08-01 -------------------- -Deprecated - - Using an out-of-bounds index for field-of-view operations now raises a - warning, which will later become an error. - -Fixed - - Changing the tiles of an active tileset will now work correctly. - -11.1.0 - 2019-07-05 -------------------- -Added - - You can now set the `TCOD_RENDERER` and `TCOD_VSYNC` environment variables to - force specific options to be used. - Example: ``TCOD_RENDERER=sdl2 TCOD_VSYNC=1`` - -Changed - - `tcod.sys_set_renderer` now raises an exception if it fails. - -Fixed - - `tcod.console_map_ascii_code_to_font` functions will now work when called - before `tcod.console_init_root`. - -11.0.2 - 2019-06-21 -------------------- -Changed - - You no longer need OpenGL to build python-tcod. - -11.0.1 - 2019-06-21 -------------------- -Changed - - Better runtime checks for Windows dependencies should now give distinct - errors depending on if the issue is SDL2 or missing redistributables. - -Fixed - - Changed NumPy type hints from `np.array` to `np.ndarray` which should - resolve issues. - -11.0.0 - 2019-06-14 -------------------- -Changed - - `tcod.map.compute_fov` now takes a 2-item tuple instead of separate `x` and - `y` parameters. This causes less confusion over how axes are aligned. - -10.1.1 - 2019-06-02 -------------------- -Changed - - Better string representations for `tcod.event.Event` subclasses. - -Fixed - - Fixed regressions in text alignment for non-rectangle print functions. - -10.1.0 - 2019-05-24 -------------------- -Added - - `tcod.console_init_root` now has an optional `vsync` parameter. - -10.0.5 - 2019-05-17 -------------------- -Fixed - - Fixed shader compilation issues in the OPENGL2 renderer. - - Fallback fonts should fail less on Linux. - -10.0.4 - 2019-05-17 -------------------- -Changed - - Now depends on cffi 0.12 or later. - -Fixed - - `tcod.console_init_root` and `tcod.console_set_custom_font` will raise - exceptions instead of terminating. - - Fixed issues preventing `tcod.event` from working on 32-bit Windows. - -10.0.3 - 2019-05-10 -------------------- -Fixed - - Corrected bounding box issues with the `Console.print_box` method. - -10.0.2 - 2019-04-26 -------------------- -Fixed - - Resolved Color warnings when importing tcod. - - When compiling, fixed a name conflict with endianness macros on FreeBSD. - -10.0.1 - 2019-04-19 -------------------- -Fixed - - Fixed horizontal alignment for TrueType fonts. - - Fixed taking screenshots with the older SDL renderer. - -10.0.0 - 2019-03-29 -------------------- -Added - - New `Console.tiles` array attribute. -Changed - - `Console.DTYPE` changed to add alpha to its color types. -Fixed - - Console printing was ignoring color codes at the beginning of a string. - -9.3.0 - 2019-03-15 ------------------- -Added - - The SDL2/OPENGL2 renderers can potentially use a fall-back font when none - are provided. - - New function `tcod.event.get_mouse_state`. - - New function `tcod.map.compute_fov` lets you get a visibility array directly - from a transparency array. -Deprecated - - The following functions and classes have been deprecated. - - `tcod.Key` - - `tcod.Mouse` - - `tcod.mouse_get_status` - - `tcod.console_is_window_closed` - - `tcod.console_check_for_keypress` - - `tcod.console_wait_for_keypress` - - `tcod.console_delete` - - `tcod.sys_check_for_event` - - `tcod.sys_wait_for_event` - - The SDL, OPENGL, and GLSL renderers have been deprecated. - - Many libtcodpy functions have been marked with PendingDeprecationWarning's. -Fixed - - To be more compatible with libtcodpy `tcod.console_init_root` will default - to the SDL render, but will raise warnings when an old renderer is used. - -9.2.5 - 2019-03-04 ------------------- -Fixed - - Fixed `tcod.namegen_generate_custom`. - -9.2.4 - 2019-03-02 ------------------- -Fixed - - The `tcod` package is has been marked as typed and will now work with MyPy. - -9.2.3 - 2019-03-01 ------------------- -Deprecated - - The behavior for negative indexes on the new print functions may change in - the future. - - Methods and functionality preventing `tcod.Color` from behaving like a tuple - have been deprecated. - -9.2.2 - 2019-02-26 ------------------- -Fixed - - `Console.print_box` wasn't setting the background color by default. - -9.2.1 - 2019-02-25 ------------------- -Fixed - - `tcod.sys_get_char_size` fixed on the new renderers. - -9.2.0 - 2019-02-24 ------------------- -Added - - New `tcod.console.get_height_rect` function, which can be used to get the - height of a print call without an existing console. - - New `tcod.tileset` module, with a `set_truetype_font` function. -Fixed - - The new print methods now handle alignment according to how they were - documented. - - `SDL2` and `OPENGL2` now support screenshots. - - Windows and MacOS builds now restrict exported SDL2 symbols to only - SDL 2.0.5; This will avoid hard to debug import errors when the wrong - version of SDL is dynamically linked. - - The root console now starts with a white foreground. - -9.1.0 - 2019-02-23 ------------------- -Added - - Added the `tcod.random.MULTIPLY_WITH_CARRY` constant. -Changed - - The overhead for warnings has been reduced when running Python with the - optimize `-O` flag. - - `tcod.random.Random` now provides a default algorithm. - -9.0.0 - 2019-02-17 ------------------- -Changed - - New console methods now default to an `fg` and `bg` of None instead of - white-on-black. - -8.5.0 - 2019-02-15 ------------------- -Added - - `tcod.console.Console` now supports `str` and `repr`. - - Added new Console methods which are independent from the console defaults. - - You can now give an array when initializing a `tcod.console.Console` - instance. - - `Console.clear` can now take `ch`, `fg`, and `bg` parameters. -Changed - - Updated libtcod to 1.10.6 - - Printing generates more compact layouts. -Deprecated - - Most libtcodpy console functions have been replaced by the tcod.console - module. - - Deprecated the `set_key_color` functions. You can pass key colors to - `Console.blit` instead. - - `Console.clear` should be given the colors to clear with as parameters, - rather than by using `default_fg` or `default_bg`. - - Most functions which depend on console default values have been deprecated. - The new deprecation warnings will give details on how to make default values - explicit. -Fixed - - `tcod.console.Console.blit` was ignoring the key color set by - `Console.set_key_color`. - - The `SDL2` and `OPENGL2` renders can now large numbers of tiles. - -8.4.3 - 2019-02-06 ------------------- -Changed - - Updated libtcod to 1.10.5 - - The SDL2/OPENGL2 renderers will now auto-detect a custom fonts key-color. - -8.4.2 - 2019-02-05 ------------------- -Deprecated - - The tdl module has been deprecated. - - The libtcodpy parser functions have been deprecated. -Fixed - - `tcod.image_is_pixel_transparent` and `tcod.image_get_alpha` now return - values. - - `Console.print_frame` was clearing tiles outside if its bounds. - - The `FONT_LAYOUT_CP437` layout was incorrect. - -8.4.1 - 2019-02-01 ------------------- -Fixed - - Window event types were not upper-case. - - Fixed regression where libtcodpy mouse wheel events unset mouse coordinates. - -8.4.0 - 2019-01-31 ------------------- -Added - - Added tcod.event module, based off of the sdlevent.py shim. -Changed - - Updated libtcod to 1.10.3 -Fixed - - Fixed libtcodpy `struct_add_value_list` function. - - Use correct math for tile-based delta in mouse events. - - New renderers now support tile-based mouse coordinates. - - SDL2 renderer will now properly refresh after the window is resized. - -8.3.2 - 2018-12-28 ------------------- -Fixed - - Fixed rare access violations for some functions which took strings as - parameters, such as `tcod.console_init_root`. - -8.3.1 - 2018-12-28 ------------------- -Fixed - - libtcodpy key and mouse functions will no longer accept the wrong types. - - The `new_struct` method was not being called for libtcodpy's custom parsers. - -8.3.0 - 2018-12-08 ------------------- -Added - - Added BSP traversal methods in tcod.bsp for parity with libtcodpy. -Deprecated - - Already deprecated bsp functions are now even more deprecated. - -8.2.0 - 2018-11-27 ------------------- -Added - - New layout `tcod.FONT_LAYOUT_CP437`. -Changed - - Updated libtcod to 1.10.2 - - `tcod.console_print_frame` and `Console.print_frame` now support Unicode - strings. -Deprecated - - Deprecated using bytes strings for all printing functions. -Fixed - - Console objects are now initialized with spaces. This fixes some blit - operations. - - Unicode code-points above U+FFFF will now work on all platforms. - -8.1.1 - 2018-11-16 ------------------- -Fixed - - Printing a frame with an empty string no longer displays a title bar. - -8.1.0 - 2018-11-15 ------------------- -Changed - - Heightmap functions now support 'F_CONTIGUOUS' arrays. - - `tcod.heightmap_new` now has an `order` parameter. - - Updated SDL to 2.0.9 -Deprecated - - Deprecated heightmap functions which sample noise grids, this can be done - using the `Noise.sample_ogrid` method. - -8.0.0 - 2018-11-02 ------------------- -Changed - - The default renderer can now be anything if not set manually. - - Better error message for when a font file isn't found. - -7.0.1 - 2018-10-27 ------------------- -Fixed - - Building from source was failing because `console_2tris.glsl*` was missing - from source distributions. - -7.0.0 - 2018-10-25 ------------------- -Added - - New `RENDERER_SDL2` and `RENDERER_OPENGL2` renderers. -Changed - - Updated libtcod to 1.9.0 - - Now requires SDL 2.0.5, which is not trivially installable on - Ubuntu 16.04 LTS. -Removed - - Dropped support for Python versions before 3.5 - - Dropped support for MacOS versions before 10.9 Mavericks. - -6.0.7 - 2018-10-24 ------------------- -Fixed - - The root console no longer loses track of buffers and console defaults on a - renderer change. - -6.0.6 - 2018-10-01 ------------------- -Fixed - - Replaced missing wheels for older and 32-bit versions of MacOS. - -6.0.5 - 2018-09-28 ------------------- -Fixed - - Resolved CDefError error during source installs. - -6.0.4 - 2018-09-11 ------------------- -Fixed - - tcod.Key right-hand modifiers are now set independently at initialization, - instead of mirroring the left-hand modifier value. - -6.0.3 - 2018-09-05 ------------------- -Fixed - - tcod.Key and tcod.Mouse no longer ignore initiation parameters. - -6.0.2 - 2018-08-28 ------------------- -Fixed - - Fixed color constants missing at build-time. - -6.0.1 - 2018-08-24 ------------------- -Fixed - - Source distributions were missing C++ source files. - -6.0.0 - 2018-08-23 ------------------- -Changed - - Project renamed to tcod on PyPI. -Deprecated - - Passing bytes strings to libtcodpy print functions is deprecated. -Fixed - - Fixed libtcodpy print functions not accepting bytes strings. - - libtcod constants are now generated at build-time fixing static analysis - tools. - -5.0.1 - 2018-07-08 ------------------- -Fixed - - tdl.event no longer crashes with StopIteration on Python 3.7 - -5.0.0 - 2018-07-05 ------------------- -Changed - - tcod.path: all classes now use `shape` instead of `width` and `height`. - - tcod.path now respects NumPy array shape, instead of assuming that arrays - need to be transposed from C memory order. From now on `x` and `y` mean - 1st and 2nd axis. This doesn't affect non-NumPy code. - - tcod.path now has full support of non-contiguous memory. - -4.6.1 - 2018-06-30 ------------------- -Added - - New function `tcod.line_where` for indexing NumPy arrays using a Bresenham - line. -Deprecated - - Python 2.7 support will be dropped in the near future. - -4.5.2 - 2018-06-29 ------------------- -Added - - New wheels for Python3.7 on Windows. -Fixed - - Arrays from `tcod.heightmap_new` are now properly zeroed out. - -4.5.1 - 2018-06-23 ------------------- -Deprecated - - Deprecated all libtcodpy map functions. -Fixed - - `tcod.map_copy` could break the `tcod.map.Map` class. - - `tcod.map_clear` `transparent` and `walkable` parameters were reversed. - - When multiple SDL2 headers were installed, the wrong ones would be used when - the library is built. - - Fails to build via pip unless Numpy is installed first. - -4.5.0 - 2018-06-12 ------------------- -Changed - - Updated libtcod to v1.7.0 - - Updated SDL to v2.0.8 - - Error messages when failing to create an SDL window should be a less vague. - - You no longer need to initialize libtcod before you can print to an - off-screen console. -Fixed - - Avoid crashes if the root console has a character code higher than expected. -Removed - - No more debug output when loading fonts. - -4.4.0 - 2018-05-02 ------------------- -Added - - Added the libtcodpy module as an alias for tcod. Actual use of it is - deprecated, it exists primarily for backward compatibility. - - Adding missing libtcodpy functions `console_has_mouse_focus` and - `console_is_active`. -Changed - - Updated libtcod to v1.6.6 - -4.3.2 - 2018-03-18 ------------------- -Deprecated - - Deprecated the use of falsy console parameters with libtcodpy functions. -Fixed - - Fixed libtcodpy image functions not supporting falsy console parameters. - - Fixed tdl `Window.get_char` method. (Kaczor2704) - -4.3.1 - 2018-03-07 ------------------- -Fixed - - Fixed cffi.api.FFIError "unsupported expression: expected a simple numeric - constant" error when building on platforms with an older cffi module and - newer SDL headers. - - tcod/tdl Map and Console objects were not saving stride data when pickled. - -4.3.0 - 2018-02-01 ------------------- -Added - - You can now set the numpy memory order on tcod.console.Console, - tcod.map.Map, and tdl.map.Map objects well as from the - tcod.console_init_root function. -Changed - - The `console_init_root` `title` parameter is now optional. -Fixed - - OpenGL renderer alpha blending is now consistent with all other render - modes. - -4.2.3 - 2018-01-06 ------------------- -Fixed - - Fixed setup.py regression that could prevent building outside of the git - repository. - -4.2.2 - 2018-01-06 ------------------- -Fixed - - The Windows dynamic linker will now prefer the bundled version of SDL. - This fixes: - "ImportError: DLL load failed: The specified procedure could not be found." - - `key.c` is no longer set when `key.vk == KEY_TEXT`, this fixes a regression - which was causing events to be heard twice in the libtcod/Python tutorial. - -4.2.0 - 2018-01-02 ------------------- -Changed - - Updated libtcod backend to v1.6.4 - - Updated SDL to v2.0.7 for Windows/MacOS. -Removed - - Source distributions no longer include tests, examples, or fonts. - `Find these on GitHub. `_ -Fixed - - Fixed "final link failed: Nonrepresentable section on output" error - when compiling for Linux. - - `tcod.console_init_root` defaults to the SDL renderer, other renderers - cause issues with mouse movement events. - -4.1.1 - 2017-11-02 ------------------- -Fixed - - Fixed `ConsoleBuffer.blit` regression. - - Console defaults corrected, the root console's blend mode and alignment is - the default value for newly made Console's. - - You can give a byte string as a filename to load parsers. - -4.1.0 - 2017-07-19 ------------------- -Added - - tdl Map class can now be pickled. -Changed - - Added protection to the `transparent`, `walkable`, and `fov` - attributes in tcod and tdl Map classes, to prevent them from being - accidentally overridden. - - tcod and tdl Map classes now use numpy arrays as their attributes. - -4.0.1 - 2017-07-12 ------------------- -Fixed - - tdl: Fixed NameError in `set_fps`. - -4.0.0 - 2017-07-08 ------------------- -Changed - - tcod.bsp: `BSP.split_recursive` parameter `random` is now `seed`. - - tcod.console: `Console.blit` parameters have been rearranged. - Most of the parameters are now optional. - - tcod.noise: `Noise.__init__` parameter `rand` is now named `seed`. - - tdl: Changed `set_fps` parameter name to `fps`. -Fixed - - tcod.bsp: Corrected spelling of max_vertical_ratio. - -3.2.0 - 2017-07-04 ------------------- -Changed - - Merged libtcod-cffi dependency with TDL. -Fixed - - Fixed boolean related crashes with Key 'text' events. - - tdl.noise: Fixed crash when given a negative seed. As well as cases - where an instance could lose its seed being pickled. - -3.1.0 - 2017-05-28 ------------------- -Added - - You can now pass tdl Console instances as parameters to libtcod-cffi - functions expecting a tcod Console. -Changed - - Dependencies updated: `libtcod-cffi>=2.5.0,<3` - - The `Console.tcod_console` attribute is being renamed to - `Console.console_c`. -Deprecated - - The tdl.noise and tdl.map modules will be deprecated in the future. -Fixed - - Resolved crash-on-exit issues for Windows platforms. - -3.0.2 - 2017-04-13 ------------------- -Changed - - Dependencies updated: `libtcod-cffi>=2.4.3,<3` - - You can now create Console instances before a call to `tdl.init`. -Removed - - Dropped support for Python 3.3 -Fixed - - Resolved issues with MacOS builds. - - 'OpenGL' and 'GLSL' renderers work again. - -3.0.1 - 2017-03-22 ------------------- -Changed - - `KeyEvent`'s with `text` now have all their modifier keys set to False. -Fixed - - Undefined behavior in text events caused crashes on 32-bit builds. - -3.0.0 - 2017-03-21 ------------------- -Added - - `KeyEvent` supports libtcod text and meta keys. -Changed - - `KeyEvent` parameters have been moved. - - This version requires `libtcod-cffi>=2.3.0`. -Deprecated - - `KeyEvent` camel capped attribute names are deprecated. -Fixed - - Crashes with key-codes undefined by libtcod. - - `tdl.map` typedef issues with libtcod-cffi. - - -2.0.1 - 2017-02-22 ------------------- -Fixed - - `tdl.init` renderer was defaulted to OpenGL which is not supported in the - current version of libtcod. - -2.0.0 - 2017-02-15 ------------------- -Changed - - Dependencies updated, tdl now requires libtcod-cffi 2.x.x - - Some event behaviors have changed with SDL2, event keys might be different - than what you expect. -Removed - - Key repeat functions were removed from SDL2. - `set_key_repeat` is now stubbed, and does nothing. - -1.6.0 - 2016-11-18 ------------------- -- Console.blit methods can now take fg_alpha and bg_alpha parameters. - -1.5.3 - 2016-06-04 ------------------- -- set_font no longer crashes when loading a file without the implied font - size in its name - -1.5.2 - 2016-03-11 ------------------- -- Fixed non-square Map instances - -1.5.1 - 2015-12-20 ------------------- -- Fixed errors with Unicode and non-Unicode literals on Python 2 -- Fixed attribute error in compute_fov - -1.5.0 - 2015-07-13 ------------------- -- python-tdl distributions are now universal builds -- New Map class -- map.bresenham now returns a list -- This release will require libtcod-cffi v0.2.3 or later - -1.4.0 - 2015-06-22 ------------------- -- The DLL's have been moved into another library which you can find at - https://github.com/HexDecimal/libtcod-cffi - You can use this library to have some raw access to libtcod if you want. - Plus it can be used alongside TDL. -- The libtcod console objects in Console instances have been made public. -- Added tdl.event.wait function. This function can called with a timeout and - can automatically call tdl.flush. - -1.3.1 - 2015-06-19 ------------------- -- Fixed pathfinding regressions. - -1.3.0 - 2015-06-19 ------------------- -- Updated backend to use python-cffi instead of ctypes. This gives decent - boost to speed in CPython and a drastic to boost in speed in PyPy. - -1.2.0 - 2015-06-06 ------------------- -- The set_colors method now changes the default colors used by the draw_* - methods. You can use Python's Ellipsis to explicitly select default colors - this way. -- Functions and Methods renamed to match Python's style-guide PEP 8, the old - function names still exist and are depreciated. -- The fgcolor and bgcolor parameters have been shortened to fg and bg. - -1.1.7 - 2015-03-19 ------------------- -- Noise generator now seeds properly. -- The OS event queue will now be handled during a call to tdl.flush. This - prevents a common newbie programmer hang where events are handled - infrequently during long animations, simulations, or early development. -- Fixed a major bug that would cause a crash in later versions of Python 3 - -1.1.6 - 2014-06-27 ------------------- -- Fixed a race condition when importing on some platforms. -- Fixed a type issue with quickFOV on Linux. -- Added a bresenham function to the tdl.map module. - -1.1.5 - 2013-11-10 ------------------- -- A for loop can iterate over all coordinates of a Console. -- drawStr can be configured to scroll or raise an error. -- You can now configure or disable key repeating with tdl.event.setKeyRepeat -- Typewriter class removed, use a Window instance for the same functionality. -- setColors method fixed. - -1.1.4 - 2013-03-06 ------------------- -- Merged the Typewriter and MetaConsole classes, - You now have a virtual cursor with Console and Window objects. -- Fixed the clear method on the Window class. -- Fixed screenshot function. -- Fixed some drawing operations with unchanging backgrounds. -- Instances of Console and Noise can be pickled and copied. -- Added KeyEvent.keychar -- Fixed event.keyWait, and now converts window closed events into Alt+F4. - -1.1.3 - 2012-12-17 ------------------- -- Some of the setFont parameters were incorrectly labeled and documented. -- setFont can auto-detect tilesets if the font sizes are in the filenames. -- Added some X11 unicode tilesets, including Unifont. - -1.1.2 - 2012-12-13 ------------------- -- Window title now defaults to the running scripts filename. -- Fixed incorrect deltaTime for App.update -- App will no longer call tdl.flush on its own, you'll need to call this - yourself. -- tdl.noise module added. -- clear method now defaults to black on black. - -1.1.1 - 2012-12-05 ------------------- -- Map submodule added with AStar class and quickFOV function. -- New Typewriter class. -- Most console functions can use Python-style negative indexes now. -- New App.runOnce method. -- Rectangle geometry is less strict. - -1.1.0 - 2012-10-04 ------------------- -- KeyEvent.keyname is now KeyEvent.key -- MouseButtonEvent.button now behaves like KeyEvent.keyname does. -- event.App class added. -- Drawing methods no longer have a default for the character parameter. -- KeyEvent.ctrl is now KeyEvent.control - -1.0.8 - 2010-04-07 ------------------- -- No longer works in Python 2.5 but now works in 3.x and has been partly - tested. -- Many bug fixes. - -1.0.5 - 2010-04-06 ------------------- -- Got rid of setuptools dependency, this will make it much more compatible - with Python 3.x -- Fixed a typo with the MacOS library import. - -1.0.4 - 2010-04-06 ------------------- -- All constant colors (C_*) have been removed, they may be put back in later. -- Made some type assertion failures show the value they received to help in - general debugging. Still working on it. -- Added MacOS and 64-bit Linux support. - -1.0.0 - 2009-01-31 ------------------- -- First public release. diff --git a/README.rst b/README.rst index 3c38bea3..823e3ac0 100755 --- a/README.rst +++ b/README.rst @@ -55,7 +55,7 @@ For the most part it's just:: =========== You can find the most recent changelog -`here `_. +`here `_. ========= License diff --git a/docs/changelog.rst b/docs/changelog.rst index 1b4f389c..214182fa 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -3,4 +3,4 @@ =========== You can find the most recent changelog -`here `_. +`here `_. diff --git a/scripts/get_release_description.py b/scripts/get_release_description.py index 0cd97f66..d91b359c 100755 --- a/scripts/get_release_description.py +++ b/scripts/get_release_description.py @@ -2,21 +2,18 @@ """Print the description used for GitHub Releases.""" import re -TAG_BANNER = r"\d+\.\d+\.\d+\S* - \d+-\d+-\d+\n-+\n" +TAG_BANNER = r"## \[[\w.]*\] - \d+-\d+-\d+\n" RE_BODY = re.compile(fr".*?{TAG_BANNER}(.*?){TAG_BANNER}", re.DOTALL) -RE_SECTION = re.compile(r"^(\w+)$", re.MULTILINE) def main() -> None: - # Get the most recent tag. - with open("CHANGELOG.rst", "r", encoding="utf-8") as f: + """Output the most recently tagged changelog body to stdout.""" + with open("CHANGELOG.md", "r", encoding="utf-8") as f: match = RE_BODY.match(f.read()) assert match body = match.groups()[0].strip() - # Add Markdown formatting to sections. - body = RE_SECTION.sub(r"### \1", body) print(body) diff --git a/scripts/tag_release.py b/scripts/tag_release.py index 68dd07f4..ef4ce3f6 100644 --- a/scripts/tag_release.py +++ b/scripts/tag_release.py @@ -4,7 +4,7 @@ import re import subprocess import sys -from typing import Any, Tuple +from typing import Tuple parser = argparse.ArgumentParser(description="Tags and releases the next version of this project.") @@ -17,20 +17,23 @@ parser.add_argument("-v", "--verbose", action="store_true", help="Print debug information.") -def parse_changelog(args: Any) -> Tuple[str, str]: +def parse_changelog(args: argparse.Namespace) -> Tuple[str, str]: """Return an updated changelog and and the list of changes.""" - with open("CHANGELOG.rst", "r", encoding="utf-8") as file: + with open("CHANGELOG.md", "r", encoding="utf-8") as file: match = re.match( - pattern=r"(.*?Unreleased\n---+\n)(.+?)(\n*[^\n]+\n---+\n.*)", + pattern=r"(.*?## \[Unreleased]\n)(.+?\n)(\n*## \[.*)", string=file.read(), flags=re.DOTALL, ) assert match header, changes, tail = match.groups() - tag = "%s - %s" % (args.tag, datetime.date.today().isoformat()) - - tagged = "\n%s\n%s\n%s" % (tag, "-" * len(tag), changes) + tagged = "\n## [%s] - %s\n%s" % ( + args.tag, + datetime.date.today().isoformat(), + changes, + ) if args.verbose: + print("--- Tagged section:") print(tagged) return "".join((header, tagged, tail)), changes @@ -42,13 +45,15 @@ def main() -> None: sys.exit(1) args = parser.parse_args() - if args.verbose: - print(args) new_changelog, changes = parse_changelog(args) + if args.verbose: + print("--- New changelog:") + print(new_changelog) + if not args.dry_run: - with open("CHANGELOG.rst", "w", encoding="utf-8") as f: + with open("CHANGELOG.md", "w", encoding="utf-8") as f: f.write(new_changelog) edit = ["-e"] if args.edit else [] subprocess.check_call(["git", "commit", "-avm", "Prepare %s release." % args.tag] + edit) diff --git a/setup.py b/setup.py index f01db6f2..e0caa37b 100755 --- a/setup.py +++ b/setup.py @@ -111,7 +111,7 @@ def check_sdl_version() -> None: url="https://github.com/libtcod/python-tcod", project_urls={ "Documentation": "https://python-tcod.readthedocs.io", - "Changelog": "https://github.com/libtcod/python-tcod/blob/develop/CHANGELOG.rst", + "Changelog": "https://github.com/libtcod/python-tcod/blob/develop/CHANGELOG.md", "Source": "https://github.com/libtcod/python-tcod", "Tracker": "https://github.com/libtcod/python-tcod/issues", "Forum": "https://github.com/libtcod/python-tcod/discussions", From 28c2ffbd473498171b081785b69532cecb8c61cf Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 7 Jan 2022 22:30:10 -0800 Subject: [PATCH 013/194] Update libtcod. --- CHANGELOG.md | 2 ++ libtcod | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cabd4854..06da498e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version ## [Unreleased] ### Added - New experimental renderer `tcod.context.RENDERER_XTERM`. +### Changed +- Using `libtcod 1.20.1`. ### Fixed - Functions accepting `Path`-like parameters now accept the more correct `os.PathLike` type. - BDF files with blank lines no longer fail to load with an "Unknown keyword" error. diff --git a/libtcod b/libtcod index 57f5216b..d54f68bf 160000 --- a/libtcod +++ b/libtcod @@ -1 +1 @@ -Subproject commit 57f5216b66227f5af68a81e8d3118d6d15c58cff +Subproject commit d54f68bf10ee862f47d3668348279a13f489d028 From e12c4172baa9efdfd74aff6ee9bab8454a835248 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 7 Jan 2022 22:30:42 -0800 Subject: [PATCH 014/194] Prepare 13.3.0 release. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06da498e..8ba44236 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] + +## [13.3.0] - 2022-01-07 ### Added - New experimental renderer `tcod.context.RENDERER_XTERM`. ### Changed From 8ae52862c5b714ad6fdaebbd515a0169324740be Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 11 Jan 2022 03:02:01 -0800 Subject: [PATCH 015/194] Improve PathLike handling. --- CHANGELOG.md | 2 ++ tcod/console.py | 14 ++++++++------ tcod/image.py | 3 ++- tcod/tileset.py | 30 +++++++++++++++++------------- 4 files changed, 29 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ba44236..9a2ed57f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] +### Fixed +- Fixed handling of non-Path PathLike parameters and filepath encodings. ## [13.3.0] - 2022-01-07 ### Added diff --git a/tcod/console.py b/tcod/console.py index 895c7e79..f313b258 100644 --- a/tcod/console.py +++ b/tcod/console.py @@ -5,9 +5,9 @@ """ from __future__ import annotations -import os import warnings from os import PathLike +from pathlib import Path from typing import Any, Iterable, Optional, Sequence, Tuple, Union import numpy as np @@ -1300,11 +1300,12 @@ def load_xp(path: Union[str, PathLike[str]], order: Literal["C", "F"] = "C") -> is_transparent = (console.rgb["bg"] == KEY_COLOR).all(axis=-1) console.rgba[is_transparent] = (ord(" "), (0,), (0,)) """ - if not os.path.exists(path): - raise FileNotFoundError(f"File not found:\n\t{os.path.abspath(path)}") - layers = _check(tcod.lib.TCOD_load_xp(str(path).encode("utf-8"), 0, ffi.NULL)) + path = Path(path) + if not path.exists(): + raise FileNotFoundError(f"File not found:\n\t{path.resolve()}") + layers = _check(tcod.lib.TCOD_load_xp(bytes(path), 0, ffi.NULL)) consoles = ffi.new("TCOD_Console*[]", layers) - _check(tcod.lib.TCOD_load_xp(str(path).encode("utf-8"), layers, consoles)) + _check(tcod.lib.TCOD_load_xp(bytes(path), layers, consoles)) return tuple(Console._from_cdata(console_p, order=order) for console_p in consoles) @@ -1358,12 +1359,13 @@ def save_xp( tcod.console.save_xp("example.xp", [console]) """ + path = Path(path) consoles_c = ffi.new("TCOD_Console*[]", [c.console_c for c in consoles]) _check( tcod.lib.TCOD_save_xp( len(consoles_c), consoles_c, - str(path).encode("utf-8"), + bytes(path), compress_level, ) ) diff --git a/tcod/image.py b/tcod/image.py index 0a017530..d94441cb 100644 --- a/tcod/image.py +++ b/tcod/image.py @@ -8,6 +8,7 @@ from __future__ import annotations from os import PathLike +from pathlib import Path from typing import Any, Dict, Tuple, Union import numpy as np @@ -345,7 +346,7 @@ def load(filename: Union[str, PathLike[str]]) -> NDArray[np.uint8]: .. versionadded:: 11.4 """ - image = Image._from_cdata(ffi.gc(lib.TCOD_image_load(str(filename).encode()), lib.TCOD_image_delete)) + image = Image._from_cdata(ffi.gc(lib.TCOD_image_load(bytes(Path(filename))), lib.TCOD_image_delete)) array: NDArray[np.uint8] = np.asarray(image, dtype=np.uint8) height, width, depth = array.shape if depth == 3: diff --git a/tcod/tileset.py b/tcod/tileset.py index fd4f2f21..acff8176 100644 --- a/tcod/tileset.py +++ b/tcod/tileset.py @@ -13,8 +13,8 @@ from __future__ import annotations import itertools -import os from os import PathLike +from pathlib import Path from typing import Any, Iterable, Optional, Tuple, Union import numpy as np @@ -258,9 +258,10 @@ def load_truetype_font(path: Union[str, PathLike[str]], tile_width: int, tile_he This function is provisional. The API may change. """ - if not os.path.exists(path): - raise RuntimeError("File not found:\n\t%s" % (os.path.realpath(path),)) - cdata = lib.TCOD_load_truetype_font_(str(path).encode(), tile_width, tile_height) + path = Path(path) + if not path.exists(): + raise RuntimeError(f"File not found:\n\t{path.resolve()}") + cdata = lib.TCOD_load_truetype_font_(bytes(path), tile_width, tile_height) if not cdata: raise RuntimeError(ffi.string(lib.TCOD_get_error())) return Tileset._claim(cdata) @@ -287,9 +288,10 @@ def set_truetype_font(path: Union[str, PathLike[str]], tile_width: int, tile_hei This function does not support contexts. Use :any:`load_truetype_font` instead. """ - if not os.path.exists(path): - raise RuntimeError("File not found:\n\t%s" % (os.path.realpath(path),)) - if lib.TCOD_tileset_load_truetype_(str(path).encode(), tile_width, tile_height): + path = Path(path) + if not path.exists(): + raise RuntimeError(f"File not found:\n\t{path.resolve()}") + if lib.TCOD_tileset_load_truetype_(bytes(path), tile_width, tile_height): raise RuntimeError(ffi.string(lib.TCOD_get_error())) @@ -306,9 +308,10 @@ def load_bdf(path: Union[str, PathLike[str]]) -> Tileset: .. versionadded:: 11.10 """ # noqa: E501 - if not os.path.exists(path): - raise RuntimeError("File not found:\n\t%s" % (os.path.realpath(path),)) - cdata = lib.TCOD_load_bdf(str(path).encode()) + path = Path(path) + if not path.exists(): + raise RuntimeError(f"File not found:\n\t{path.resolve()}") + cdata = lib.TCOD_load_bdf(bytes(path)) if not cdata: raise RuntimeError(ffi.string(lib.TCOD_get_error()).decode()) return Tileset._claim(cdata) @@ -335,12 +338,13 @@ def load_tilesheet( .. versionadded:: 11.12 """ - if not os.path.exists(path): - raise RuntimeError("File not found:\n\t%s" % (os.path.realpath(path),)) + path = Path(path) + if not path.exists(): + raise RuntimeError(f"File not found:\n\t{path.resolve()}") mapping = [] if charmap is not None: mapping = list(itertools.islice(charmap, columns * rows)) - cdata = lib.TCOD_tileset_load(str(path).encode(), columns, rows, len(mapping), mapping) + cdata = lib.TCOD_tileset_load(bytes(path), columns, rows, len(mapping), mapping) if not cdata: _raise_tcod_error() return Tileset._claim(cdata) From b54412b9204221f93c3fac56b582eea72557831f Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 11 Jan 2022 03:58:07 -0800 Subject: [PATCH 016/194] Refactor and modernize scripts. Use pathlib and f-strings more often. Clean up some deprecated code. --- build_libtcod.py | 148 ++++++++++++++--------------- parse_sdl2.py | 21 ++-- scripts/generate_charmap_table.py | 5 +- scripts/get_release_description.py | 6 +- scripts/tag_release.py | 17 ++-- setup.py | 24 ++--- tcod/console.py | 2 +- 7 files changed, 109 insertions(+), 114 deletions(-) diff --git a/build_libtcod.py b/build_libtcod.py index a69933c6..1f65f122 100644 --- a/build_libtcod.py +++ b/build_libtcod.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +from __future__ import annotations import glob import os @@ -8,16 +9,13 @@ import subprocess import sys import zipfile +from pathlib import Path from typing import Any, Dict, Iterable, Iterator, List, Set, Tuple, Union - -try: - from urllib import urlretrieve # type: ignore -except ImportError: - from urllib.request import urlretrieve +from urllib.request import urlretrieve from cffi import FFI # type: ignore -sys.path.append(os.path.dirname(__file__)) +sys.path.append(str(Path(__file__).parent)) # Allow importing local modules. import parse_sdl2 # noqa: E402 @@ -52,18 +50,18 @@ class ParsedHeader: """ # Class dictionary of all parsed headers. - all_headers = {} # type: Dict[str, "ParsedHeader"] + all_headers: Dict[Path, ParsedHeader] = {} - def __init__(self, path: str) -> None: - self.path = path = os.path.normpath(path) - directory = os.path.dirname(path) + def __init__(self, path: Path) -> None: + self.path = path = path.resolve(True) + directory = path.parent depends = set() with open(self.path, "r", encoding="utf-8") as f: header = f.read() header = RE_COMMENT.sub("", header) header = RE_CPLUSPLUS.sub("", header) for dependency in RE_INCLUDE.findall(header): - depends.add(os.path.normpath(os.path.join(directory, dependency))) + depends.add((directory / dependency).resolve(True)) header = RE_PREPROCESSOR.sub("", header) header = RE_TAGS.sub("", header) header = RE_VAFUNC.sub("", header) @@ -87,17 +85,17 @@ def __str__(self) -> str: ) def __repr__(self) -> str: - return "ParsedHeader(%s)" % (self.path,) + return f"ParsedHeader({self.path})" def walk_includes(directory: str) -> Iterator[ParsedHeader]: """Parse all the include files in a directory and subdirectories.""" - for path, dirs, files in os.walk(directory): + for path, _dirs, files in os.walk(directory): for file in files: if file in HEADER_PARSE_EXCLUDES: continue if file.endswith(".h"): - yield ParsedHeader(os.path.join(path, file)) + yield ParsedHeader(Path(path, file).resolve(True)) def resolve_dependencies( @@ -105,7 +103,7 @@ def resolve_dependencies( ) -> List[ParsedHeader]: """Sort headers by their correct include order.""" unresolved = set(includes) - resolved = set() # type: Set[ParsedHeader] + resolved: Set[ParsedHeader] = set() result = [] while unresolved: for item in unresolved: @@ -115,7 +113,7 @@ def resolve_dependencies( if not unresolved & resolved: raise RuntimeError( "Could not resolve header load order.\n" - "Possible cyclic dependency with the unresolved headers:\n%s" % (unresolved,) + f"Possible cyclic dependency with the unresolved headers:\n{unresolved}" ) unresolved -= resolved return result @@ -124,49 +122,50 @@ def resolve_dependencies( def parse_includes() -> List[ParsedHeader]: """Collect all parsed header files and return them. - Reads HEADER_PARSE_PATHS and HEADER_PARSE_EXCLUDES.""" - includes = [] # type: List[ParsedHeader] + Reads HEADER_PARSE_PATHS and HEADER_PARSE_EXCLUDES. + """ + includes: List[ParsedHeader] = [] for dirpath in HEADER_PARSE_PATHS: includes.extend(walk_includes(dirpath)) return resolve_dependencies(includes) def walk_sources(directory: str) -> Iterator[str]: - for path, dirs, files in os.walk(directory): + for path, _dirs, files in os.walk(directory): for source in files: if source.endswith(".c"): - yield os.path.join(path, source) + yield str(Path(path, source)) -def get_sdl2_file(version: str) -> str: +def get_sdl2_file(version: str) -> Path: if sys.platform == "win32": - sdl2_file = "SDL2-devel-%s-VC.zip" % (version,) + sdl2_file = f"SDL2-devel-{version}-VC.zip" else: assert sys.platform == "darwin" - sdl2_file = "SDL2-%s.dmg" % (version,) - sdl2_local_file = os.path.join("dependencies", sdl2_file) - sdl2_remote_file = "https://www.libsdl.org/release/%s" % sdl2_file - if not os.path.exists(sdl2_local_file): - print("Downloading %s" % sdl2_remote_file) + sdl2_file = f"SDL2-{version}.dmg" + sdl2_local_file = Path("dependencies", sdl2_file) + sdl2_remote_file = f"https://www.libsdl.org/release/{sdl2_file}" + if not sdl2_local_file.exists(): + print(f"Downloading {sdl2_remote_file}") os.makedirs("dependencies/", exist_ok=True) urlretrieve(sdl2_remote_file, sdl2_local_file) return sdl2_local_file -def unpack_sdl2(version: str) -> str: - sdl2_path = "dependencies/SDL2-%s" % (version,) +def unpack_sdl2(version: str) -> Path: + sdl2_path = Path(f"dependencies/SDL2-{version}") if sys.platform == "darwin": sdl2_dir = sdl2_path - sdl2_path += "/SDL2.framework" - if os.path.exists(sdl2_path): + sdl2_path /= "SDL2.framework" + if sdl2_path.exists(): return sdl2_path sdl2_arc = get_sdl2_file(version) - print("Extracting %s" % sdl2_arc) - if sdl2_arc.endswith(".zip"): + print(f"Extracting {sdl2_arc}") + if sdl2_arc.suffix == ".zip": with zipfile.ZipFile(sdl2_arc) as zf: zf.extractall("dependencies/") elif sys.platform == "darwin": - assert sdl2_arc.endswith(".dmg") + assert sdl2_arc.suffix == ".dmg" subprocess.check_call(["hdiutil", "mount", sdl2_arc]) subprocess.check_call(["mkdir", "-p", sdl2_dir]) subprocess.check_call(["cp", "-r", "/Volumes/SDL2/SDL2.framework", sdl2_dir]) @@ -187,11 +186,11 @@ def unpack_sdl2(version: str) -> str: extra_parse_args = [] extra_compile_args = [] extra_link_args = [] -sources = [] # type: List[str] +sources: List[str] = [] libraries = [] library_dirs: List[str] = [] -define_macros = [("Py_LIMITED_API", 0x03060000)] # type: List[Tuple[str, Any]] +define_macros: List[Tuple[str, Any]] = [("Py_LIMITED_API", 0x03060000)] sources += walk_sources("tcod/") sources += walk_sources("libtcod/src/libtcod/") @@ -219,9 +218,9 @@ def unpack_sdl2(version: str) -> str: include_dirs.append("libtcod/src/zlib/") if sys.platform == "win32": - SDL2_INCLUDE = os.path.join(SDL2_PARSE_PATH, "include") + SDL2_INCLUDE = Path(SDL2_PARSE_PATH, "include") elif sys.platform == "darwin": - SDL2_INCLUDE = os.path.join(SDL2_PARSE_PATH, "Versions/A/Headers") + SDL2_INCLUDE = Path(SDL2_PARSE_PATH, "Versions/A/Headers") else: matches = re.findall( r"-I(\S+)", @@ -231,43 +230,40 @@ def unpack_sdl2(version: str) -> str: SDL2_INCLUDE = None for match in matches: - if os.path.isfile(os.path.join(match, "SDL_stdinc.h")): + if Path(match, "SDL_stdinc.h").is_file(): SDL2_INCLUDE = match assert SDL2_INCLUDE if sys.platform == "win32": - include_dirs.append(SDL2_INCLUDE) + include_dirs.append(str(SDL2_INCLUDE)) ARCH_MAPPING = {"32bit": "x86", "64bit": "x64"} - SDL2_LIB_DIR = os.path.join(SDL2_BUNDLE_PATH, "lib/", ARCH_MAPPING[BITSIZE]) - library_dirs.append(SDL2_LIB_DIR) - SDL2_LIB_DEST = os.path.join("tcod", ARCH_MAPPING[BITSIZE]) - if not os.path.exists(SDL2_LIB_DEST): + SDL2_LIB_DIR = Path(SDL2_BUNDLE_PATH, "lib/", ARCH_MAPPING[BITSIZE]) + library_dirs.append(str(SDL2_LIB_DIR)) + SDL2_LIB_DEST = Path("tcod", ARCH_MAPPING[BITSIZE]) + if not SDL2_LIB_DEST.exists(): os.mkdir(SDL2_LIB_DEST) - shutil.copy(os.path.join(SDL2_LIB_DIR, "SDL2.dll"), SDL2_LIB_DEST) + shutil.copy(Path(SDL2_LIB_DIR, "SDL2.dll"), SDL2_LIB_DEST) -def fix_header(filepath: str) -> None: +def fix_header(path: Path) -> None: """Removes leading whitespace from a MacOS header file. This whitespace is causing issues with directives on some platforms. """ - with open(filepath, "r+", encoding="utf-8") as f: - current = f.read() - fixed = "\n".join(line.strip() for line in current.split("\n")) - if current == fixed: - return - f.seek(0) - f.truncate() - f.write(fixed) + current = path.read_text(encoding="utf-8") + fixed = "\n".join(line.strip() for line in current.split("\n")) + if current == fixed: + return + path.write_text(fixed, encoding="utf-8") if sys.platform == "darwin": - HEADER_DIR = os.path.join(SDL2_PARSE_PATH, "Headers") - fix_header(os.path.join(HEADER_DIR, "SDL_assert.h")) - fix_header(os.path.join(HEADER_DIR, "SDL_config_macosx.h")) + HEADER_DIR = Path(SDL2_PARSE_PATH, "Headers") + fix_header(Path(HEADER_DIR, "SDL_assert.h")) + fix_header(Path(HEADER_DIR, "SDL_config_macosx.h")) include_dirs.append(HEADER_DIR) - extra_link_args += ["-F%s/.." % SDL2_BUNDLE_PATH] - extra_link_args += ["-rpath", "%s/.." % SDL2_BUNDLE_PATH] + extra_link_args += [f"-F{SDL2_BUNDLE_PATH}/.."] + extra_link_args += ["-rpath", f"{SDL2_BUNDLE_PATH}/.."] extra_link_args += ["-rpath", "/usr/local/opt/llvm/lib/"] # Fix "implicit declaration of function 'close'" in zlib. @@ -309,7 +305,7 @@ def fix_header(filepath: str) -> None: ffi.cdef(include.header) except Exception: # Print the source, for debugging. - print("Error with: %s" % include.path) + print(f"Error with: {include.path}") for i, line in enumerate(include.header.split("\n"), 1): print("%03i %s" % (i, line)) raise @@ -374,8 +370,8 @@ def parse_sdl_attrs(prefix: str, all_names: List[str]) -> Tuple[str, str]: lookup = [] for name, value in sorted(find_sdl_attrs(prefix), key=lambda item: item[1]): all_names.append(name) - names.append("%s = %s" % (name, value)) - lookup.append('%s: "%s"' % (value, name)) + names.append(f"{name} = {value}") + lookup.append(f'{value}: "{name}"') return "\n".join(names), "{\n %s,\n}" % (",\n ".join(lookup),) @@ -408,10 +404,10 @@ def update_module_all(filename: str, new_all: str) -> None: ) with open(filename, "r", encoding="utf-8") as f: match = RE_CONSTANTS_ALL.match(f.read()) - assert match, "Can't determine __all__ subsection in %s!" % (filename,) + assert match, f"Can't determine __all__ subsection in {filename}!" header, footer = match.groups() with open(filename, "w", encoding="utf-8") as f: - f.write("%s\n %s,\n %s" % (header, new_all, footer)) + f.write(f"{header}\n {new_all},\n {footer}") def generate_enums(prefix: str) -> Iterator[str]: @@ -446,14 +442,14 @@ def write_library_constants() -> None: value = getattr(lib, name) if name[:5] == "TCOD_": if name.isupper(): # const names - f.write("%s = %r\n" % (name[5:], value)) + f.write(f"{name[5:]} = {value!r}\n") all_names.append(name[5:]) elif name.startswith("FOV"): # fov const names - f.write("%s = %r\n" % (name, value)) + f.write(f"{name} = {value!r}\n") all_names.append(name) elif name[:6] == "TCODK_": # key name - f.write("KEY_%s = %r\n" % (name[6:], value)) - all_names.append("KEY_%s" % name[6:]) + f.write(f"KEY_{name[6:]} = {value!r}\n") + all_names.append(f"KEY_{name[6:]}") f.write("\n# --- colors ---\n") for name in dir(lib): @@ -465,11 +461,11 @@ def write_library_constants() -> None: if ffi.typeof(value) != ffi.typeof("TCOD_color_t"): continue color = tcod.color.Color._new_from_cdata(value) - f.write("%s = %r\n" % (name[5:], color)) + f.write(f"{name[5:]} = {color!r}\n") all_names.append(name[5:]) - all_names_merged = ",\n ".join('"%s"' % name for name in all_names) - f.write("\n__all__ = [\n %s,\n]\n" % (all_names_merged,)) + all_names_merged = ",\n ".join(f'"{name}"' for name in all_names) + f.write(f"\n__all__ = [\n {all_names_merged},\n]\n") update_module_all("tcod/__init__.py", all_names_merged) update_module_all("tcod/libtcodpy.py", all_names_merged) @@ -487,11 +483,10 @@ def write_library_constants() -> None: f.write("\n# --- SDL wheel ---\n") f.write("%s\n_REVERSE_WHEEL_TABLE = %s\n" % parse_sdl_attrs("SDL_MOUSEWHEEL", all_names)) - all_names_merged = ",\n ".join('"%s"' % name for name in all_names) - f.write("\n__all__ = [\n %s,\n]\n" % (all_names_merged,)) + all_names_merged = ",\n ".join(f'"{name}"' for name in all_names) + f.write(f"\n__all__ = [\n {all_names_merged},\n]\n") - with open("tcod/event.py", "r", encoding="utf-8") as f: - event_py = f.read() + event_py = Path("tcod/event.py").read_text(encoding="utf-8") event_py = re.sub( r"(?<=# --- SDL scancodes ---\n ).*?(?=\n # --- end ---\n)", @@ -506,8 +501,7 @@ def write_library_constants() -> None: flags=re.DOTALL, ) - with open("tcod/event.py", "w", encoding="utf-8") as f: - f.write(event_py) + Path("tcod/event.py").write_text(event_py, encoding="utf-8") if __name__ == "__main__": diff --git a/parse_sdl2.py b/parse_sdl2.py index bfdc0125..dd97940d 100644 --- a/parse_sdl2.py +++ b/parse_sdl2.py @@ -1,8 +1,11 @@ -import os.path +from __future__ import annotations + import platform import re import sys -from typing import Any, Dict, Iterator +from os import PathLike +from pathlib import Path +from typing import Any, Dict, Iterator, Union import cffi # type: ignore @@ -25,10 +28,9 @@ RE_EVENT_PADDING = re.compile(r"Uint8 padding\[[^]]+\];", re.MULTILINE | re.DOTALL) -def get_header(name: str) -> str: +def get_header(path: Path) -> str: """Return the source of a header in a partially preprocessed state.""" - with open(name, "r", encoding="utf-8") as f: - header = f.read() + header = path.read_text(encoding="utf-8") # Remove Doxygen code. header = RE_REMOVALS.sub("", header) # Remove comments. @@ -131,7 +133,8 @@ def parse(header: str, NEEDS_PACK4: bool) -> Iterator[str]: ] -def add_to_ffi(ffi: cffi.FFI, path: str) -> None: +def add_to_ffi(ffi: cffi.FFI, path: Union[str, PathLike[str]]) -> None: + path = Path(path) BITS, _ = platform.architecture() cdef_args: Dict[str, Any] = {} NEEDS_PACK4 = False @@ -142,12 +145,12 @@ def add_to_ffi(ffi: cffi.FFI, path: str) -> None: # cdef_args["pack"] = 4 ffi.cdef( - "\n".join(RE_TYPEDEF.findall(get_header(os.path.join(path, "SDL_stdinc.h")))).replace("SDLCALL ", ""), + "\n".join(RE_TYPEDEF.findall(get_header(path / "SDL_stdinc.h"))).replace("SDLCALL ", ""), **cdef_args, ) for header in HEADERS: try: - for code in parse(get_header(os.path.join(path, header)), NEEDS_PACK4): + for code in parse(get_header(path / header), NEEDS_PACK4): if "typedef struct SDL_AudioCVT" in code and sys.platform != "win32" and not NEEDS_PACK4: # This specific struct needs to be packed. ffi.cdef(code, packed=1) @@ -158,7 +161,7 @@ def add_to_ffi(ffi: cffi.FFI, path: str) -> None: raise -def get_ffi(path: str) -> cffi.FFI: +def get_ffi(path: Union[str, PathLike[str]]) -> cffi.FFI: """Return an ffi for SDL2, needs to be compiled.""" ffi = cffi.FFI() add_to_ffi(ffi, path) diff --git a/scripts/generate_charmap_table.py b/scripts/generate_charmap_table.py index d4bd0b77..1f9f4639 100755 --- a/scripts/generate_charmap_table.py +++ b/scripts/generate_charmap_table.py @@ -3,6 +3,8 @@ Uses the tabulate module from PyPI. """ +from __future__ import annotations + import argparse import unicodedata from typing import Iterable, Iterator @@ -71,8 +73,9 @@ def main() -> None: ) args = parser.parse_args() charmap = getattr(tcod.tileset, f"CHARMAP_{args.charmap.upper()}") + output = generate_table(charmap) with args.out_file as f: - f.write(generate_table(charmap)) + f.write(output) if __name__ == "__main__": diff --git a/scripts/get_release_description.py b/scripts/get_release_description.py index d91b359c..29844caf 100755 --- a/scripts/get_release_description.py +++ b/scripts/get_release_description.py @@ -1,6 +1,9 @@ #!/usr/bin/env python3 """Print the description used for GitHub Releases.""" +from __future__ import annotations + import re +from pathlib import Path TAG_BANNER = r"## \[[\w.]*\] - \d+-\d+-\d+\n" @@ -9,8 +12,7 @@ def main() -> None: """Output the most recently tagged changelog body to stdout.""" - with open("CHANGELOG.md", "r", encoding="utf-8") as f: - match = RE_BODY.match(f.read()) + match = RE_BODY.match(Path("CHANGELOG.md").read_text(encoding="utf-8")) assert match body = match.groups()[0].strip() diff --git a/scripts/tag_release.py b/scripts/tag_release.py index ef4ce3f6..4f900ba2 100644 --- a/scripts/tag_release.py +++ b/scripts/tag_release.py @@ -1,9 +1,12 @@ #!/usr/bin/env python3 +from __future__ import annotations + import argparse import datetime import re import subprocess import sys +from pathlib import Path from typing import Tuple parser = argparse.ArgumentParser(description="Tags and releases the next version of this project.") @@ -19,12 +22,11 @@ def parse_changelog(args: argparse.Namespace) -> Tuple[str, str]: """Return an updated changelog and and the list of changes.""" - with open("CHANGELOG.md", "r", encoding="utf-8") as file: - match = re.match( - pattern=r"(.*?## \[Unreleased]\n)(.+?\n)(\n*## \[.*)", - string=file.read(), - flags=re.DOTALL, - ) + match = re.match( + pattern=r"(.*?## \[Unreleased]\n)(.+?\n)(\n*## \[.*)", + string=Path("CHANGELOG.md").read_text(encoding="utf-8"), + flags=re.DOTALL, + ) assert match header, changes, tail = match.groups() tagged = "\n## [%s] - %s\n%s" % ( @@ -53,8 +55,7 @@ def main() -> None: print(new_changelog) if not args.dry_run: - with open("CHANGELOG.md", "w", encoding="utf-8") as f: - f.write(new_changelog) + Path("CHANGELOG.md").write_text(new_changelog, encoding="utf-8") edit = ["-e"] if args.edit else [] subprocess.check_call(["git", "commit", "-avm", "Prepare %s release." % args.tag] + edit) subprocess.check_call(["git", "tag", args.tag, "-am", "%s\n\n%s" % (args.tag, changes)] + edit) diff --git a/setup.py b/setup.py index e0caa37b..d78363f3 100755 --- a/setup.py +++ b/setup.py @@ -1,24 +1,24 @@ #!/usr/bin/env python3 +from __future__ import annotations -import os -import pathlib import platform import re import subprocess import sys import warnings +from pathlib import Path from typing import List from setuptools import setup SDL_VERSION_NEEDED = (2, 0, 5) -PATH = pathlib.Path(__file__).parent # setup.py current directory +SETUP_DIR = Path(__file__).parent # setup.py current directory def get_version() -> str: """Get the current version from a git tag, or by reading tcod/version.py""" - if (PATH / ".git").exists(): + if (SETUP_DIR / ".git").exists(): tag = subprocess.check_output(["git", "describe", "--abbrev=0"], universal_newlines=True).strip() assert not tag.startswith("v") version = tag @@ -30,13 +30,11 @@ def get_version() -> str: version += ".dev%i" % commits_since_tag # update tcod/version.py - with open(PATH / "tcod/version.py", "w", encoding="utf-8") as version_file: - version_file.write(f'__version__ = "{version}"\n') + (SETUP_DIR / "tcod/version.py").write_text(f'__version__ = "{version}"\n', encoding="utf-8") return version else: # Not a Git repository. try: - with open(PATH / "tcod/version.py", encoding="utf-8") as version_file: - match = re.match(r'__version__ = "(\S+)"', version_file.read()) + match = re.match(r'__version__ = "(\S+)"', (SETUP_DIR / "tcod/version.py").read_text(encoding="utf-8")) assert match return match.groups()[0] except FileNotFoundError: @@ -66,12 +64,6 @@ def get_package_data() -> List[str]: return files -def get_long_description() -> str: - """Return this projects description.""" - with open(PATH / "README.rst", "r", encoding="utf-8") as readme_file: - return readme_file.read() - - def check_sdl_version() -> None: """Check the local SDL version on Linux distributions.""" if not sys.platform.startswith("linux"): @@ -91,7 +83,7 @@ def check_sdl_version() -> None: raise RuntimeError("SDL version must be at least %s, (found %s)" % (needed_version, sdl_version_str)) -if not os.path.exists(PATH / "libtcod/src"): +if not (SETUP_DIR / "libtcod/src").exists(): print("Libtcod submodule is uninitialized.") print("Did you forget to run 'git submodule update --init'?") sys.exit(1) @@ -107,7 +99,7 @@ def check_sdl_version() -> None: author="Kyle Benesch", author_email="4b796c65+tcod@gmail.com", description="The official Python port of libtcod.", - long_description=get_long_description(), + long_description=(SETUP_DIR / "README.rst").read_text(encoding="utf-8"), url="https://github.com/libtcod/python-tcod", project_urls={ "Documentation": "https://python-tcod.readthedocs.io", diff --git a/tcod/console.py b/tcod/console.py index f313b258..56eea2ac 100644 --- a/tcod/console.py +++ b/tcod/console.py @@ -393,7 +393,7 @@ def __clear_warning(self, name: str, value: Tuple[int, int, int]) -> None: def clear( self, - ch: int = ord(" "), + ch: int = 0x20, fg: Tuple[int, int, int] = ..., # type: ignore bg: Tuple[int, int, int] = ..., # type: ignore ) -> None: From 858617bae4521af1883e4f983b53586c107a7b6d Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 13 Jan 2022 02:42:08 -0800 Subject: [PATCH 017/194] Manually add SDL_free to FFI exports. Also add SDL_calloc for later tests. --- parse_sdl2.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/parse_sdl2.py b/parse_sdl2.py index dd97940d..bd28167b 100644 --- a/parse_sdl2.py +++ b/parse_sdl2.py @@ -132,6 +132,12 @@ def parse(header: str, NEEDS_PACK4: bool) -> Iterator[str]: "SDL_version.h", ] +# It's easier to manually add these instead of parsing SDL_stdinc.h +CDEF_EXTRA = """ +void* SDL_calloc(size_t nmemb, size_t size); +void SDL_free(void *mem); +""" + def add_to_ffi(ffi: cffi.FFI, path: Union[str, PathLike[str]]) -> None: path = Path(path) @@ -148,6 +154,7 @@ def add_to_ffi(ffi: cffi.FFI, path: Union[str, PathLike[str]]) -> None: "\n".join(RE_TYPEDEF.findall(get_header(path / "SDL_stdinc.h"))).replace("SDLCALL ", ""), **cdef_args, ) + ffi.cdef(CDEF_EXTRA, **cdef_args) for header in HEADERS: try: for code in parse(get_header(path / header), NEEDS_PACK4): From 4385a4d65f20a6e1eefc8ab0a243c0d869adec45 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 25 Jan 2022 12:59:51 -0800 Subject: [PATCH 018/194] Update SDL CFFI parser. This is a backport of the SDL parser updates from the ESDL subproject. This may change what symbols are exported, but it will export more than before. It should also be more stable. --- .vscode/settings.json | 1 + build_libtcod.py | 120 ++---------------- build_sdl.py | 285 ++++++++++++++++++++++++++++++++++++++++++ parse_sdl2.py | 175 -------------------------- pyproject.toml | 8 +- requirements.txt | 3 +- setup.py | 5 +- 7 files changed, 309 insertions(+), 288 deletions(-) create mode 100644 build_sdl.py delete mode 100644 parse_sdl2.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 6e892bc5..ee28f37e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -216,6 +216,7 @@ "PAGEUP", "pathfinding", "pathlib", + "pcpp", "PILCROW", "pilmode", "PRINTF", diff --git a/build_libtcod.py b/build_libtcod.py index 1f65f122..93065ae3 100644 --- a/build_libtcod.py +++ b/build_libtcod.py @@ -5,19 +5,15 @@ import os import platform import re -import shutil -import subprocess import sys -import zipfile from pathlib import Path from typing import Any, Dict, Iterable, Iterator, List, Set, Tuple, Union -from urllib.request import urlretrieve from cffi import FFI # type: ignore sys.path.append(str(Path(__file__).parent)) # Allow importing local modules. -import parse_sdl2 # noqa: E402 +import build_sdl # noqa: E402 # The SDL2 version to parse and export symbols from. SDL2_PARSE_VERSION = os.environ.get("SDL_VERSION", "2.0.5") @@ -25,6 +21,8 @@ # The SDL2 version to include in binary distributions. SDL2_BUNDLE_VERSION = os.environ.get("SDL_VERSION", "2.0.14") +Py_LIMITED_API = 0x03060000 + HEADER_PARSE_PATHS = ("tcod/", "libtcod/src/libtcod/") HEADER_PARSE_EXCLUDES = ("gl2_ext_.h", "renderer_gl_internal.h", "event.h") @@ -137,60 +135,24 @@ def walk_sources(directory: str) -> Iterator[str]: yield str(Path(path, source)) -def get_sdl2_file(version: str) -> Path: - if sys.platform == "win32": - sdl2_file = f"SDL2-devel-{version}-VC.zip" - else: - assert sys.platform == "darwin" - sdl2_file = f"SDL2-{version}.dmg" - sdl2_local_file = Path("dependencies", sdl2_file) - sdl2_remote_file = f"https://www.libsdl.org/release/{sdl2_file}" - if not sdl2_local_file.exists(): - print(f"Downloading {sdl2_remote_file}") - os.makedirs("dependencies/", exist_ok=True) - urlretrieve(sdl2_remote_file, sdl2_local_file) - return sdl2_local_file - - -def unpack_sdl2(version: str) -> Path: - sdl2_path = Path(f"dependencies/SDL2-{version}") - if sys.platform == "darwin": - sdl2_dir = sdl2_path - sdl2_path /= "SDL2.framework" - if sdl2_path.exists(): - return sdl2_path - sdl2_arc = get_sdl2_file(version) - print(f"Extracting {sdl2_arc}") - if sdl2_arc.suffix == ".zip": - with zipfile.ZipFile(sdl2_arc) as zf: - zf.extractall("dependencies/") - elif sys.platform == "darwin": - assert sdl2_arc.suffix == ".dmg" - subprocess.check_call(["hdiutil", "mount", sdl2_arc]) - subprocess.check_call(["mkdir", "-p", sdl2_dir]) - subprocess.check_call(["cp", "-r", "/Volumes/SDL2/SDL2.framework", sdl2_dir]) - subprocess.check_call(["hdiutil", "unmount", "/Volumes/SDL2"]) - return sdl2_path - - includes = parse_includes() module_name = "tcod._libtcod" -include_dirs = [ +include_dirs: List[str] = [ ".", "libtcod/src/vendor/", "libtcod/src/vendor/utf8proc", "libtcod/src/vendor/zlib/", + *build_sdl.include_dirs, ] -extra_parse_args = [] -extra_compile_args = [] -extra_link_args = [] +extra_compile_args: List[str] = [*build_sdl.extra_compile_args] +extra_link_args: List[str] = [*build_sdl.extra_link_args] sources: List[str] = [] -libraries = [] -library_dirs: List[str] = [] -define_macros: List[Tuple[str, Any]] = [("Py_LIMITED_API", 0x03060000)] +libraries: List[str] = [*build_sdl.libraries] +library_dirs: List[str] = [*build_sdl.library_dirs] +define_macros: List[Tuple[str, Any]] = [("Py_LIMITED_API", Py_LIMITED_API)] sources += walk_sources("tcod/") sources += walk_sources("libtcod/src/libtcod/") @@ -205,74 +167,14 @@ def unpack_sdl2(version: str) -> Path: define_macros.append(("TCODLIB_API", "")) define_macros.append(("_CRT_SECURE_NO_WARNINGS", None)) -if sys.platform == "darwin": - extra_link_args += ["-framework", "SDL2"] -else: - libraries += ["SDL2"] - -# included SDL headers are for whatever OS's don't easily come with them - if sys.platform in ["win32", "darwin"]: - SDL2_PARSE_PATH = unpack_sdl2(SDL2_PARSE_VERSION) - SDL2_BUNDLE_PATH = unpack_sdl2(SDL2_BUNDLE_VERSION) include_dirs.append("libtcod/src/zlib/") -if sys.platform == "win32": - SDL2_INCLUDE = Path(SDL2_PARSE_PATH, "include") -elif sys.platform == "darwin": - SDL2_INCLUDE = Path(SDL2_PARSE_PATH, "Versions/A/Headers") -else: - matches = re.findall( - r"-I(\S+)", - subprocess.check_output(["sdl2-config", "--cflags"], universal_newlines=True), - ) - assert matches - - SDL2_INCLUDE = None - for match in matches: - if Path(match, "SDL_stdinc.h").is_file(): - SDL2_INCLUDE = match - assert SDL2_INCLUDE - -if sys.platform == "win32": - include_dirs.append(str(SDL2_INCLUDE)) - ARCH_MAPPING = {"32bit": "x86", "64bit": "x64"} - SDL2_LIB_DIR = Path(SDL2_BUNDLE_PATH, "lib/", ARCH_MAPPING[BITSIZE]) - library_dirs.append(str(SDL2_LIB_DIR)) - SDL2_LIB_DEST = Path("tcod", ARCH_MAPPING[BITSIZE]) - if not SDL2_LIB_DEST.exists(): - os.mkdir(SDL2_LIB_DEST) - shutil.copy(Path(SDL2_LIB_DIR, "SDL2.dll"), SDL2_LIB_DEST) - - -def fix_header(path: Path) -> None: - """Removes leading whitespace from a MacOS header file. - - This whitespace is causing issues with directives on some platforms. - """ - current = path.read_text(encoding="utf-8") - fixed = "\n".join(line.strip() for line in current.split("\n")) - if current == fixed: - return - path.write_text(fixed, encoding="utf-8") - if sys.platform == "darwin": - HEADER_DIR = Path(SDL2_PARSE_PATH, "Headers") - fix_header(Path(HEADER_DIR, "SDL_assert.h")) - fix_header(Path(HEADER_DIR, "SDL_config_macosx.h")) - include_dirs.append(HEADER_DIR) - extra_link_args += [f"-F{SDL2_BUNDLE_PATH}/.."] - extra_link_args += ["-rpath", f"{SDL2_BUNDLE_PATH}/.."] - extra_link_args += ["-rpath", "/usr/local/opt/llvm/lib/"] - # Fix "implicit declaration of function 'close'" in zlib. define_macros.append(("HAVE_UNISTD_H", 1)) -if sys.platform not in ["win32", "darwin"]: - extra_parse_args += subprocess.check_output(["sdl2-config", "--cflags"], universal_newlines=True).strip().split() - extra_compile_args += extra_parse_args - extra_link_args += subprocess.check_output(["sdl2-config", "--libs"], universal_newlines=True).strip().split() tdl_build = os.environ.get("TDL_BUILD", "RELEASE").upper() @@ -299,7 +201,7 @@ def fix_header(path: Path) -> None: extra_link_args.extend(GCC_CFLAGS[tdl_build]) ffi = FFI() -parse_sdl2.add_to_ffi(ffi, SDL2_INCLUDE) +ffi.cdef(build_sdl.get_cdef()) for include in includes: try: ffi.cdef(include.header) diff --git a/build_sdl.py b/build_sdl.py new file mode 100644 index 00000000..7bc0bd8d --- /dev/null +++ b/build_sdl.py @@ -0,0 +1,285 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import io +import os +import platform +import re +import shutil +import subprocess +import sys +import zipfile +from pathlib import Path +from typing import Any, Dict, List, Set +from urllib.request import urlretrieve + +import pcpp # type: ignore + +BITSIZE, LINKAGE = platform.architecture() + +# The SDL2 version to parse and export symbols from. +SDL2_PARSE_VERSION = os.environ.get("SDL_VERSION", "2.0.5") +# The SDL2 version to include in binary distributions. +SDL2_BUNDLE_VERSION = os.environ.get("SDL_VERSION", "2.0.14") + + +# Used to remove excessive newlines in debug outputs. +RE_NEWLINES = re.compile(r"\n\n+") +# Functions using va_list need to be culled. +RE_VAFUNC = re.compile(r"^.*?\([^()]*va_list[^()]*\);$", re.MULTILINE) +# Static inline functions need to be culled. +RE_INLINE = re.compile(r"^static inline.*?^}$", re.MULTILINE | re.DOTALL) +# Most SDL_PIXELFORMAT names need their values scrubbed. +RE_PIXELFORMAT = re.compile(r"(?PSDL_PIXELFORMAT_\w+) =[^,}]*") +# Most SDLK names need their values scrubbed. +RE_SDLK = re.compile(r"(?PSDLK_\w+) =.*?(?=,\n|}\n)") +# Remove compile time assertions from the cdef. +RE_ASSERT = re.compile(r"^.*SDL_compile_time_assert.*$", re.MULTILINE) +# Padding values need to be scrubbed. +RE_PADDING = re.compile(r"padding\[[^;]*\];") + +# These structs have an unusual size when packed by SDL on 32-bit platforms. +FLEXIBLE_STRUCTS = ( + "SDL_AudioCVT", + "SDL_TouchFingerEvent", + "SDL_MultiGestureEvent", + "SDL_DollarGestureEvent", +) + +# Other defined names which sometimes cause issues when parsed. +IGNORE_DEFINES = frozenset( + ( + "SDL_DEPRECATED", + "SDL_INLINE", + "SDL_FORCE_INLINE", + # Might show up in parsing and not in source. + "SDL_ANDROID_EXTERNAL_STORAGE_READ", + "SDL_ANDROID_EXTERNAL_STORAGE_WRITE", + "SDL_ASSEMBLY_ROUTINES", + "SDL_RWOPS_VITAFILE", + # Prevent double definition. + "SDL_FALSE", + "SDL_TRUE", + ) +) + + +def check_sdl_version() -> None: + """Check the local SDL version on Linux distributions.""" + if not sys.platform.startswith("linux"): + return + needed_version = SDL2_PARSE_VERSION + SDL_VERSION_NEEDED = tuple(int(n) for n in needed_version.split(".")) + try: + sdl_version_str = subprocess.check_output(["sdl2-config", "--version"], universal_newlines=True).strip() + except FileNotFoundError: + raise RuntimeError( + "libsdl2-dev or equivalent must be installed on your system" + f" and must be at least version {needed_version}." + "\nsdl2-config must be on PATH." + ) + print(f"Found SDL {sdl_version_str}.") + sdl_version = tuple(int(s) for s in sdl_version_str.split(".")) + if sdl_version < SDL_VERSION_NEEDED: + raise RuntimeError("SDL version must be at least %s, (found %s)" % (needed_version, sdl_version_str)) + + +def get_sdl2_file(version: str) -> Path: + if sys.platform == "win32": + sdl2_file = f"SDL2-devel-{version}-VC.zip" + else: + assert sys.platform == "darwin" + sdl2_file = f"SDL2-{version}.dmg" + sdl2_local_file = Path("dependencies", sdl2_file) + sdl2_remote_file = f"https://www.libsdl.org/release/{sdl2_file}" + if not sdl2_local_file.exists(): + print(f"Downloading {sdl2_remote_file}") + os.makedirs("dependencies/", exist_ok=True) + urlretrieve(sdl2_remote_file, sdl2_local_file) + return sdl2_local_file + + +def unpack_sdl2(version: str) -> Path: + sdl2_path = Path(f"dependencies/SDL2-{version}") + if sys.platform == "darwin": + sdl2_dir = sdl2_path + sdl2_path /= "SDL2.framework" + if sdl2_path.exists(): + return sdl2_path + sdl2_arc = get_sdl2_file(version) + print(f"Extracting {sdl2_arc}") + if sdl2_arc.suffix == ".zip": + with zipfile.ZipFile(sdl2_arc) as zf: + zf.extractall("dependencies/") + elif sys.platform == "darwin": + assert sdl2_arc.suffix == ".dmg" + subprocess.check_call(["hdiutil", "mount", sdl2_arc]) + subprocess.check_call(["mkdir", "-p", sdl2_dir]) + subprocess.check_call(["cp", "-r", "/Volumes/SDL2/SDL2.framework", sdl2_dir]) + subprocess.check_call(["hdiutil", "unmount", "/Volumes/SDL2"]) + return sdl2_path + + +class SDLParser(pcpp.Preprocessor): # type: ignore + """A modified preprocessor to output code in a format for CFFI.""" + + def __init__(self) -> None: + super().__init__() + self.line_directive = None # Don't output line directives. + self.known_string_defines: Dict[str, str] = {} + self.known_defines: Set[str] = set() + + def get_output(self) -> str: + """Return this objects current tokens as a string.""" + with io.StringIO() as buffer: + self.write(buffer) + for name in self.known_defines: + buffer.write(f"#define {name} ...\n") + return buffer.getvalue() + + def on_include_not_found(self, is_malformed: bool, is_system_include: bool, curdir: str, includepath: str) -> None: + """Remove bad includes such as stddef.h and stdarg.h.""" + raise pcpp.OutputDirective(pcpp.Action.IgnoreAndRemove) + + def _should_track_define(self, tokens: List[Any]) -> bool: + if len(tokens) < 3: + return False + if tokens[0].value in IGNORE_DEFINES: + return False + if not tokens[0].value.isupper(): + return False # Function-like name, such as SDL_snprintf. + if tokens[0].value.startswith("_") or tokens[0].value.endswith("_"): + return False # Private name. + if tokens[2].value.startswith("_") or tokens[2].value.endswith("_"): + return False # Likely calls a private function. + if tokens[1].type == "CPP_LPAREN": + return False # Function-like macro. + if len(tokens) >= 4 and tokens[2].type == "CPP_INTEGER" and tokens[3].type == "CPP_DOT": + return False # Value is a floating point number. + if tokens[0].value.startswith("SDL_PR") and (tokens[0].value.endswith("32") or tokens[0].value.endswith("64")): + return False # Data type for printing, which is not needed. + return bool( + tokens[0].value.startswith("KMOD_") + or tokens[0].value.startswith("SDL_") + or tokens[0].value.startswith("AUDIO_") + ) + + def on_directive_handle( + self, directive: Any, tokens: List[Any], if_passthru: bool, preceding_tokens: List[Any] + ) -> Any: + if directive.value == "define" and self._should_track_define(tokens): + if tokens[2].type == "CPP_STRING": + self.known_string_defines[tokens[0].value] = tokens[2].value + else: + self.known_defines.add(tokens[0].value) + return super().on_directive_handle(directive, tokens, if_passthru, preceding_tokens) + + +check_sdl_version() + +if sys.platform in ["win32", "darwin"]: + SDL2_PARSE_PATH = unpack_sdl2(SDL2_PARSE_VERSION) + SDL2_BUNDLE_PATH = unpack_sdl2(SDL2_BUNDLE_VERSION) + +SDL2_INCLUDE: Path +if sys.platform == "win32": + SDL2_INCLUDE = SDL2_PARSE_PATH / "include" +elif sys.platform == "darwin": + SDL2_INCLUDE = SDL2_PARSE_PATH / "Versions/A/Headers" +else: # Unix + matches = re.findall( + r"-I(\S+)", + subprocess.check_output(["sdl2-config", "--cflags"], universal_newlines=True), + ) + assert matches + + for match in matches: + if os.path.isfile(Path(match, "SDL_stdinc.h")): + SDL2_INCLUDE = match + assert SDL2_INCLUDE + + +EXTRA_CDEF = """ +#define SDLK_SCANCODE_MASK ... + +extern "Python" { +// SDL_AudioCallback callback. +void _sdl_audio_callback(void* userdata, Uint8* stream, int len); +} +""" + + +def get_cdef() -> str: + parser = SDLParser() + parser.add_path(SDL2_INCLUDE) + parser.parse( + """ + // Remove extern keyword. + #define extern + // Ignore some SDL assert statements. + #define DOXYGEN_SHOULD_IGNORE_THIS + + #define _SIZE_T_DEFINED_ + typedef int... size_t; + + // Skip these headers. + #define SDL_atomic_h_ + #define SDL_thread_h_ + + #include + """ + ) + sdl2_cdef = parser.get_output() + sdl2_cdef = RE_VAFUNC.sub("", sdl2_cdef) + sdl2_cdef = RE_INLINE.sub("", sdl2_cdef) + sdl2_cdef = RE_PIXELFORMAT.sub(r"\g = ...", sdl2_cdef) + sdl2_cdef = RE_SDLK.sub(r"\g = ...", sdl2_cdef) + sdl2_cdef = RE_NEWLINES.sub("\n", sdl2_cdef) + sdl2_cdef = RE_ASSERT.sub("", sdl2_cdef) + sdl2_cdef = RE_PADDING.sub("padding[...];", sdl2_cdef) + sdl2_cdef = ( + sdl2_cdef.replace("int SDL_main(int argc, char *argv[]);", "") + .replace("typedef unsigned int uintptr_t;", "typedef int... uintptr_t;") + .replace("typedef unsigned int size_t;", "typedef int... size_t;") + ) + for name in FLEXIBLE_STRUCTS: + sdl2_cdef = sdl2_cdef.replace(f"}} {name};", f"...;}} {name};") + return sdl2_cdef + EXTRA_CDEF + + +include_dirs: List[str] = [] +extra_compile_args: List[str] = [] +extra_link_args: List[str] = [] + +libraries: List[str] = [] +library_dirs: List[str] = [] + + +if sys.platform == "darwin": + extra_link_args += ["-framework", "SDL2"] +else: + libraries += ["SDL2"] + +# Bundle the Windows SDL2 DLL. +if sys.platform == "win32": + include_dirs.append(str(SDL2_INCLUDE)) + ARCH_MAPPING = {"32bit": "x86", "64bit": "x64"} + SDL2_LIB_DIR = Path(SDL2_BUNDLE_PATH, "lib/", ARCH_MAPPING[BITSIZE]) + library_dirs.append(str(SDL2_LIB_DIR)) + SDL2_LIB_DEST = Path("tcod", ARCH_MAPPING[BITSIZE]) + SDL2_LIB_DEST.mkdir(exist_ok=True) + shutil.copy(SDL2_LIB_DIR / "SDL2.dll", SDL2_LIB_DEST) + +# Link to the SDL2 framework on MacOS. +# Delocate will bundle the binaries in a later step. +if sys.platform == "darwin": + HEADER_DIR = os.path.join(SDL2_PARSE_PATH, "Headers") + include_dirs.append(HEADER_DIR) + extra_link_args += [f"-F{SDL2_BUNDLE_PATH}/.."] + extra_link_args += ["-rpath", f"{SDL2_BUNDLE_PATH}/.."] + extra_link_args += ["-rpath", "/usr/local/opt/llvm/lib/"] + +# Use sdl2-config to link to SDL2 on Linux. +if sys.platform not in ["win32", "darwin"]: + extra_compile_args += subprocess.check_output(["sdl2-config", "--cflags"], universal_newlines=True).strip().split() + extra_link_args += subprocess.check_output(["sdl2-config", "--libs"], universal_newlines=True).strip().split() diff --git a/parse_sdl2.py b/parse_sdl2.py deleted file mode 100644 index bd28167b..00000000 --- a/parse_sdl2.py +++ /dev/null @@ -1,175 +0,0 @@ -from __future__ import annotations - -import platform -import re -import sys -from os import PathLike -from pathlib import Path -from typing import Any, Dict, Iterator, Union - -import cffi # type: ignore - -# Various poorly made regular expressions, these will miss code which isn't -# supported by cffi. -RE_COMMENT = re.compile(r" */\*.*?\*/", re.DOTALL) -RE_REMOVALS = re.compile( - r"#ifndef DOXYGEN_SHOULD_IGNORE_THIS.*" r"#endif /\* DOXYGEN_SHOULD_IGNORE_THIS \*/", - re.DOTALL, -) -RE_DEFINE = re.compile(r"#define \w+(?!\() (?:.*?(?:\\\n)?)*$", re.MULTILINE) -RE_TYPEDEF = re.compile(r"^typedef[^{;#]*?(?:{[^}]*\n}[^;]*)?;", re.MULTILINE) -RE_ENUM = re.compile(r"^(?:typedef )?enum[^{;#]*?(?:{[^}]*\n}[^;]*)?;", re.MULTILINE) -RE_DECL = re.compile(r"^extern[^#(]*\([^#]*?\);$", re.MULTILINE | re.DOTALL) -RE_ENDIAN = re.compile(r"#if SDL_BYTEORDER == SDL_LIL_ENDIAN(.*?)#else(.*?)#endif", re.DOTALL) -RE_ENDIAN2 = re.compile(r"#if SDL_BYTEORDER == SDL_BIG_ENDIAN(.*?)#else(.*?)#endif", re.DOTALL) -RE_DEFINE_TRUNCATE = re.compile(r"(#define\s+\w+\s+).+$", flags=re.DOTALL) -RE_TYPEDEF_TRUNCATE = re.compile(r"(typedef\s+\w+\s+\w+)\s*{.*\n}(?=.*;$)", flags=re.DOTALL | re.MULTILINE) -RE_ENUM_TRUNCATE = re.compile(r"(\w+\s*=).+?(?=,$|})(?![^(']*\))", re.MULTILINE | re.DOTALL) -RE_EVENT_PADDING = re.compile(r"Uint8 padding\[[^]]+\];", re.MULTILINE | re.DOTALL) - - -def get_header(path: Path) -> str: - """Return the source of a header in a partially preprocessed state.""" - header = path.read_text(encoding="utf-8") - # Remove Doxygen code. - header = RE_REMOVALS.sub("", header) - # Remove comments. - header = RE_COMMENT.sub("", header) - # Deal with endianness in "SDL_audio.h". - header = RE_ENDIAN.sub(r"\1" if sys.byteorder == "little" else r"\2", header) - header = RE_ENDIAN2.sub(r"\1" if sys.byteorder != "little" else r"\2", header) - - # Ignore bad ARM compiler typedef. - header = header.replace("typedef int SDL_bool;", "") - return header - - -# Remove non-integer definitions. -DEFINE_BLACKLIST = [ - "SDL_AUDIOCVT_PACKED", - "SDL_BlitScaled", - "SDL_BlitSurface", - "SDL_Colour", - "SDL_IPHONE_MAX_GFORCE", -] - - -def parse(header: str, NEEDS_PACK4: bool) -> Iterator[str]: - """Pull individual sections from a header, processing them as needed.""" - for define in RE_DEFINE.findall(header): - if any(item in define for item in DEFINE_BLACKLIST): - continue # Remove non-integer definitions. - if '"' in define: - continue # Ignore definitions with strings. - # Replace various definitions with "..." since cffi is limited here. - yield RE_DEFINE_TRUNCATE.sub(r"\1 ...", define) - - for enum in RE_ENUM.findall(header): - yield RE_ENUM_TRUNCATE.sub(r"\1 ...", enum) - header = header.replace(enum, "") - - for typedef in RE_TYPEDEF.findall(header): - # Special case for SDL window flags enum. - if "SDL_WINDOW_FULLSCREEN_DESKTOP" in typedef: - typedef = typedef.replace("( SDL_WINDOW_FULLSCREEN | 0x00001000 )", "...") - # Detect array sizes at compile time. - typedef = typedef.replace("SDL_TEXTINPUTEVENT_TEXT_SIZE", "...") - typedef = typedef.replace("SDL_TEXTEDITINGEVENT_TEXT_SIZE", "...") - typedef = typedef.replace("SDL_AUDIOCVT_MAX_FILTERS + 1", "...") - - typedef = typedef.replace("SDLCALL", " ") - typedef = typedef.replace("SDL_AUDIOCVT_PACKED ", "") - typedef = RE_EVENT_PADDING.sub("Uint8 padding[...];", typedef) - - if NEEDS_PACK4 and "typedef struct SDL_AudioCVT" in typedef: - typedef = RE_TYPEDEF_TRUNCATE.sub(r"\1 { ...; }", typedef) - if NEEDS_PACK4 and "typedef struct SDL_TouchFingerEvent" in typedef: - typedef = RE_TYPEDEF_TRUNCATE.sub(r"\1 { ...; }", typedef) - if NEEDS_PACK4 and "typedef struct SDL_MultiGestureEvent" in typedef: - typedef = RE_TYPEDEF_TRUNCATE.sub(r"\1 { ...; }", typedef) - if NEEDS_PACK4 and "typedef struct SDL_DollarGestureEvent" in typedef: - typedef = RE_TYPEDEF_TRUNCATE.sub(r"\1 { ...; }", typedef) - yield typedef - - for decl in RE_DECL.findall(header): - if "SDL_RWops" in decl: - continue # Ignore SDL_RWops functions. - if "va_list" in decl: - continue - decl = re.sub(r"SDL_PRINTF_VARARG_FUNC\(\w*\)", "", decl) - decl = decl.replace("SDL_DEPRECATED", "") - decl = decl.replace("SDLCALL", "") - decl = re.sub(r"extern\s+DECLSPEC", "", decl) - yield decl.replace("SDL_PRINTF_FORMAT_STRING ", "") - - -# Parsed headers excluding "SDL_stdinc.h" -HEADERS = [ - "SDL_rect.h", - "SDL_pixels.h", - "SDL_blendmode.h", - "SDL_error.h", - "SDL_surface.h", - "SDL_video.h", - "SDL_render.h", - "SDL_audio.h", - "SDL_clipboard.h", - "SDL_touch.h", - "SDL_gesture.h", - "SDL_hints.h", - "SDL_joystick.h", - "SDL_haptic.h", - "SDL_power.h", - "SDL_log.h", - "SDL_messagebox.h", - "SDL_mouse.h", - "SDL_timer.h", - "SDL_keycode.h", - "SDL_scancode.h", - "SDL_keyboard.h", - "SDL_events.h", - "SDL.h", - "SDL_version.h", -] - -# It's easier to manually add these instead of parsing SDL_stdinc.h -CDEF_EXTRA = """ -void* SDL_calloc(size_t nmemb, size_t size); -void SDL_free(void *mem); -""" - - -def add_to_ffi(ffi: cffi.FFI, path: Union[str, PathLike[str]]) -> None: - path = Path(path) - BITS, _ = platform.architecture() - cdef_args: Dict[str, Any] = {} - NEEDS_PACK4 = False - if sys.platform == "win32" and BITS == "32bit": - NEEDS_PACK4 = True - # The following line is required but cffi does not currently support - # it for ABI mode. - # cdef_args["pack"] = 4 - - ffi.cdef( - "\n".join(RE_TYPEDEF.findall(get_header(path / "SDL_stdinc.h"))).replace("SDLCALL ", ""), - **cdef_args, - ) - ffi.cdef(CDEF_EXTRA, **cdef_args) - for header in HEADERS: - try: - for code in parse(get_header(path / header), NEEDS_PACK4): - if "typedef struct SDL_AudioCVT" in code and sys.platform != "win32" and not NEEDS_PACK4: - # This specific struct needs to be packed. - ffi.cdef(code, packed=1) - continue - ffi.cdef(code, **cdef_args) - except Exception: - print("Error parsing %r code:\n%s" % (header, code)) - raise - - -def get_ffi(path: Union[str, PathLike[str]]) -> cffi.FFI: - """Return an ffi for SDL2, needs to be compiled.""" - ffi = cffi.FFI() - add_to_ffi(ffi, path) - return ffi diff --git a/pyproject.toml b/pyproject.toml index df1a4a46..1fddcde9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,11 @@ [build-system] -requires = ["setuptools>=57.0.0", "wheel", "cffi~=1.13", "pycparser>=2.14"] +requires = [ + "setuptools>=57.0.0", + "wheel", + "cffi>=1.15", + "pycparser>=2.14", + "pcpp==1.30", +] build-backend = "setuptools.build_meta" [tool.black] diff --git a/requirements.txt b/requirements.txt index 3ce930b0..b5a68d21 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,8 @@ -cffi~=1.13 +cffi>=1.15 numpy>=1.20.3 pycparser>=2.14 setuptools>=36.0.1 types-setuptools types-tabulate typing_extensions +pcpp==1.30 diff --git a/setup.py b/setup.py index d78363f3..2f81769d 100755 --- a/setup.py +++ b/setup.py @@ -114,11 +114,12 @@ def check_sdl_version() -> None: python_requires=">=3.7", setup_requires=[ *pytest_runner, - "cffi~=1.13", + "cffi>=1.15", "pycparser>=2.14", + "pcpp==1.30", ], install_requires=[ - "cffi~=1.13", # Also required by pyproject.toml. + "cffi>=1.15", # Also required by pyproject.toml. "numpy>=1.20.3" if not is_pypy else "", "typing_extensions", ], From cbb56b4a84c9cd0ea9ea88332234f9c88298ae07 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 26 Jan 2022 15:40:27 -0800 Subject: [PATCH 019/194] Backport SDL audio features from ESDL project. Not making these public yet. --- tcod/sdl/__init__.py | 0 tcod/sdl/audio.py | 217 ++++++++++++++++++++++++++++++++++ tcod/sdl/sys.py | 72 +++++++++++ tcod/{sdl.py => sdl/video.py} | 0 4 files changed, 289 insertions(+) create mode 100644 tcod/sdl/__init__.py create mode 100644 tcod/sdl/audio.py create mode 100644 tcod/sdl/sys.py rename tcod/{sdl.py => sdl/video.py} (100%) diff --git a/tcod/sdl/__init__.py b/tcod/sdl/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tcod/sdl/audio.py b/tcod/sdl/audio.py new file mode 100644 index 00000000..787ccf0d --- /dev/null +++ b/tcod/sdl/audio.py @@ -0,0 +1,217 @@ +from __future__ import annotations + +import sys +import threading +import time +import weakref +from typing import Any, Iterator, List, Optional + +import numpy as np +from numpy.typing import ArrayLike, DTypeLike, NDArray + +import tcod.sdl.sys +from tcod.loader import ffi, lib + + +def _get_format(format: DTypeLike) -> int: + """Return a SDL_AudioFormat bitfield from a NumPy dtype.""" + dt: Any = np.dtype(format) + assert dt.fields is None + bitsize = dt.itemsize * 8 + assert 0 < bitsize <= lib.SDL_AUDIO_MASK_BITSIZE + assert dt.str[1] in "uif" + is_signed = dt.str[1] != "u" + is_float = dt.str[1] == "f" + byteorder = dt.byteorder + if byteorder == "=": + byteorder = "<" if sys.byteorder == "little" else ">" + + return ( # type: ignore + bitsize + | (lib.SDL_AUDIO_MASK_DATATYPE * is_float) + | (lib.SDL_AUDIO_MASK_ENDIAN * (byteorder == ">")) + | (lib.SDL_AUDIO_MASK_SIGNED * is_signed) + ) + + +def _dtype_from_format(format: int) -> np.dtype[Any]: + """Return a dtype from a SDL_AudioFormat.""" + bitsize = format & lib.SDL_AUDIO_MASK_BITSIZE + assert bitsize % 8 == 0 + bytesize = bitsize // 8 + byteorder = ">" if format & lib.SDL_AUDIO_MASK_ENDIAN else "<" + if format & lib.SDL_AUDIO_MASK_DATATYPE: + kind = "f" + elif format & lib.SDL_AUDIO_MASK_SIGNED: + kind = "i" + else: + kind = "u" + return np.dtype(f"{byteorder}{kind}{bytesize}") + + +class AudioDevice: + def __init__( + self, + device: Optional[str] = None, + capture: bool = False, + *, + frequency: int = 44100, + format: DTypeLike = np.float32, + channels: int = 2, + samples: int = 0, + allowed_changes: int = 0, + ): + self.__sdl_subsystems = tcod.sdl.sys._ScopeInit(tcod.sdl.sys.Subsystem.AUDIO) + self.__handle = ffi.new_handle(weakref.ref(self)) + desired = ffi.new( + "SDL_AudioSpec*", + { + "freq": frequency, + "format": _get_format(format), + "channels": channels, + "samples": samples, + "callback": ffi.NULL, + "userdata": self.__handle, + }, + ) + obtained = ffi.new("SDL_AudioSpec*") + self.device_id = lib.SDL_OpenAudioDevice( + ffi.NULL if device is None else device.encode("utf-8"), + capture, + desired, + obtained, + allowed_changes, + ) + assert self.device_id != 0, tcod.sdl.sys._get_error() + self.frequency = obtained.freq + self.is_capture = capture + self.format = _dtype_from_format(obtained.format) + self.channels = int(obtained.channels) + self.silence = int(obtained.silence) + self.samples = int(obtained.samples) + self.buffer_size = int(obtained.size) + self.unpause() + + @property + def _sample_size(self) -> int: + return self.format.itemsize * self.channels + + def pause(self) -> None: + lib.SDL_PauseAudioDevice(self.device_id, True) + + def unpause(self) -> None: + lib.SDL_PauseAudioDevice(self.device_id, False) + + def _verify_array_format(self, samples: NDArray[Any]) -> NDArray[Any]: + if samples.dtype != self.format: + raise TypeError(f"Expected an array of dtype {self.format}, got {samples.dtype} instead.") + return samples + + def _convert_array(self, samples_: ArrayLike) -> NDArray[Any]: + if isinstance(samples_, np.ndarray): + samples_ = self._verify_array_format(samples_) + samples: NDArray[Any] = np.asarray(samples_, dtype=self.format) + if len(samples.shape) < 2: + samples = samples[:, np.newaxis] + return np.ascontiguousarray(np.broadcast_to(samples, (samples.shape[0], self.channels)), dtype=self.format) + + @property + def queued_audio_bytes(self) -> int: + return int(lib.SDL_GetQueuedAudioSize(self.device_id)) + + def queue_audio(self, samples: ArrayLike) -> None: + assert not self.is_capture + samples = self._convert_array(samples) + buffer = ffi.from_buffer(samples) + lib.SDL_QueueAudio(self.device_id, buffer, len(buffer)) + + def dequeue_audio(self) -> NDArray[Any]: + assert self.is_capture + out_samples = self.queued_audio_bytes // self._sample_size + out = np.empty((out_samples, self.channels), self.format) + buffer = ffi.from_buffer(out) + bytes_returned = lib.SDL_DequeueAudio(self.device_id, buffer, len(buffer)) + samples_returned = bytes_returned // self._sample_size + assert samples_returned == out_samples + return out + + def __del__(self) -> None: + self.close() + + def close(self) -> None: + if not self.device_id: + return + lib.SDL_CloseAudioDevice(self.device_id) + self.device_id = 0 + + @staticmethod + def __default_callback(stream: NDArray[Any], silence: int) -> None: + stream[...] = silence + + +class Mixer(threading.Thread): + def __init__(self, device: AudioDevice): + super().__init__(daemon=True) + self.device = device + self.device.unpause() + self.start() + + def run(self) -> None: + buffer = np.full((self.device.samples, self.device.channels), self.device.silence, dtype=self.device.format) + while True: + time.sleep(0.001) + if self.device.queued_audio_bytes == 0: + self.on_stream(buffer) + self.device.queue_audio(buffer) + buffer[:] = self.device.silence + + def on_stream(self, stream: NDArray[Any]) -> None: + pass + + +class BasicMixer(Mixer): + def __init__(self, device: AudioDevice): + super().__init__(device) + self.play_buffers: List[List[NDArray[Any]]] = [] + + def play(self, sound: ArrayLike) -> None: + array = np.asarray(sound, dtype=self.device.format) + assert array.size + if len(array.shape) == 1: + array = array[:, np.newaxis] + chunks: List[NDArray[Any]] = np.split(array, range(0, len(array), self.device.samples)[1:])[::-1] + self.play_buffers.append(chunks) + + def on_stream(self, stream: NDArray[Any]) -> None: + super().on_stream(stream) + for chunks in self.play_buffers: + chunk = chunks.pop() + stream[: len(chunk)] += chunk + + self.play_buffers = [chunks for chunks in self.play_buffers if chunks] + + +@ffi.def_extern() # type: ignore +def _sdl_audio_callback(userdata: Any, stream: Any, length: int) -> None: + """Handle audio device callbacks.""" + device: Optional[AudioDevice] = ffi.from_handle(userdata)() + assert device is not None + _ = np.frombuffer(ffi.buffer(stream, length), dtype=device.format).reshape(-1, device.channels) + + +def _get_devices(capture: bool) -> Iterator[str]: + """Get audio devices from SDL_GetAudioDeviceName.""" + with tcod.sdl.sys._ScopeInit(tcod.sdl.sys.Subsystem.AUDIO): + device_count = lib.SDL_GetNumAudioDevices(capture) + for i in range(device_count): + yield str(ffi.string(lib.SDL_GetAudioDeviceName(i, capture)), encoding="utf-8") + + +def get_devices() -> Iterator[str]: + """Iterate over the available audio output devices.""" + yield from _get_devices(capture=False) + + +def get_capture_devices() -> Iterator[str]: + """Iterate over the available audio capture devices.""" + yield from _get_devices(capture=True) diff --git a/tcod/sdl/sys.py b/tcod/sdl/sys.py new file mode 100644 index 00000000..a47b2a70 --- /dev/null +++ b/tcod/sdl/sys.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +import enum +from typing import Any, Tuple + +from tcod.loader import ffi, lib + + +class Subsystem(enum.IntFlag): + TIMER = lib.SDL_INIT_TIMER + AUDIO = lib.SDL_INIT_AUDIO + VIDEO = lib.SDL_INIT_VIDEO + JOYSTICK = lib.SDL_INIT_JOYSTICK + HAPTIC = lib.SDL_INIT_HAPTIC + GAMECONTROLLER = lib.SDL_INIT_GAMECONTROLLER + EVENTS = lib.SDL_INIT_EVENTS + SENSOR = getattr(lib, "SDL_INIT_SENSOR", 0) + EVERYTHING = lib.SDL_INIT_EVERYTHING + + +def _check(result: int) -> int: + if result < 0: + raise RuntimeError(_get_error()) + return result + + +def init(flags: int = Subsystem.EVERYTHING) -> None: + _check(lib.SDL_InitSubSystem(flags)) + + +def quit(flags: int = Subsystem.EVERYTHING) -> None: + lib.SDL_QuitSubSystem(flags) + + +class _ScopeInit: + def __init__(self, flags: int) -> None: + init(flags) + self.flags = flags + + def close(self) -> None: + if self.flags: + quit(self.flags) + self.flags = 0 + + def __del__(self) -> None: + self.close() + + def __enter__(self) -> _ScopeInit: + return self + + def __exit__(self, *args: Any) -> None: + self.close() + + +def _get_error() -> str: + return str(ffi.string(lib.SDL_GetError()), encoding="utf-8") + + +class _PowerState(enum.IntEnum): + UNKNOWN = lib.SDL_POWERSTATE_UNKNOWN + ON_BATTERY = lib.SDL_POWERSTATE_ON_BATTERY + NO_BATTERY = lib.SDL_POWERSTATE_NO_BATTERY + CHARGING = lib.SDL_POWERSTATE_CHARGING + CHARGED = lib.SDL_POWERSTATE_CHARGED + + +def _get_power_info() -> Tuple[_PowerState, int, int]: + buffer = ffi.new("int[2]") + power_state = _PowerState(lib.SDL_GetPowerInfo(buffer, buffer + 1)) + seconds_of_power = buffer[0] + percenage = buffer[1] + return power_state, seconds_of_power, percenage diff --git a/tcod/sdl.py b/tcod/sdl/video.py similarity index 100% rename from tcod/sdl.py rename to tcod/sdl/video.py From 9715a4617ff812693a1b91a972ac40815e779f6b Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 27 Jan 2022 23:48:50 -0800 Subject: [PATCH 020/194] Catch logs from SDL. --- build_sdl.py | 1 + tcod/sdl/__init__.py | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/build_sdl.py b/build_sdl.py index 7bc0bd8d..9a26132e 100644 --- a/build_sdl.py +++ b/build_sdl.py @@ -205,6 +205,7 @@ def on_directive_handle( extern "Python" { // SDL_AudioCallback callback. void _sdl_audio_callback(void* userdata, Uint8* stream, int len); +void _sdl_log_output_function(void *userdata, int category, SDL_LogPriority priority, const char *message); } """ diff --git a/tcod/sdl/__init__.py b/tcod/sdl/__init__.py index e69de29b..cd77bcf2 100644 --- a/tcod/sdl/__init__.py +++ b/tcod/sdl/__init__.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +import logging +from typing import Any + +from tcod.loader import ffi, lib + +logger = logging.getLogger(__name__) + +_LOG_PRIORITY = { + int(lib.SDL_LOG_PRIORITY_VERBOSE): logging.DEBUG, + int(lib.SDL_LOG_PRIORITY_DEBUG): logging.DEBUG, + int(lib.SDL_LOG_PRIORITY_INFO): logging.INFO, + int(lib.SDL_LOG_PRIORITY_WARN): logging.WARNING, + int(lib.SDL_LOG_PRIORITY_ERROR): logging.ERROR, + int(lib.SDL_LOG_PRIORITY_CRITICAL): logging.CRITICAL, +} + + +@ffi.def_extern() # type: ignore +def _sdl_log_output_function(_userdata: Any, category: int, priority: int, message: Any) -> None: + """Pass logs sent by SDL to Python's logging system.""" + logger.log(_LOG_PRIORITY.get(priority, 0), "%i:%s", category, ffi.string(message).decode("utf-8")) + + +lib.SDL_LogSetOutputFunction(lib._sdl_log_output_function, ffi.NULL) From afdd1d4c58c5a24d939fd7df1187fcf6af5eaa9c Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 28 Jan 2022 13:39:15 -0800 Subject: [PATCH 021/194] Add SDL Renderer and Texture helpers. Perform some cleanups. --- .vscode/settings.json | 3 + tcod/sdl/__init__.py | 12 ++++ tcod/sdl/audio.py | 3 +- tcod/sdl/render.py | 127 ++++++++++++++++++++++++++++++++++++++++++ tcod/sdl/sys.py | 11 +--- 5 files changed, 145 insertions(+), 11 deletions(-) create mode 100644 tcod/sdl/render.py diff --git a/.vscode/settings.json b/.vscode/settings.json index ee28f37e..5b8556b3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -170,6 +170,7 @@ "LMASK", "lmeta", "lodepng", + "LPAREN", "LTCG", "lucida", "LWIN", @@ -250,6 +251,7 @@ "RRGGBB", "rtype", "RWIN", + "RWOPS", "scalex", "scaley", "Scancode", @@ -264,6 +266,7 @@ "setuptools", "SHADOWCAST", "SMILIE", + "snprintf", "stdeb", "struct", "structs", diff --git a/tcod/sdl/__init__.py b/tcod/sdl/__init__.py index cd77bcf2..66aa7885 100644 --- a/tcod/sdl/__init__.py +++ b/tcod/sdl/__init__.py @@ -23,4 +23,16 @@ def _sdl_log_output_function(_userdata: Any, category: int, priority: int, messa logger.log(_LOG_PRIORITY.get(priority, 0), "%i:%s", category, ffi.string(message).decode("utf-8")) +def _get_error() -> str: + """Return a message from SDL_GetError as a Unicode string.""" + return str(ffi.string(lib.SDL_GetError()), encoding="utf-8") + + +def _check(result: int) -> int: + """Check if an SDL function returned without errors, and raise an exception if it did.""" + if result < 0: + raise RuntimeError(_get_error()) + return result + + lib.SDL_LogSetOutputFunction(lib._sdl_log_output_function, ffi.NULL) diff --git a/tcod/sdl/audio.py b/tcod/sdl/audio.py index 787ccf0d..ac5cd0d1 100644 --- a/tcod/sdl/audio.py +++ b/tcod/sdl/audio.py @@ -11,6 +11,7 @@ import tcod.sdl.sys from tcod.loader import ffi, lib +from tcod.sdl import _get_error def _get_format(format: DTypeLike) -> int: @@ -82,7 +83,7 @@ def __init__( obtained, allowed_changes, ) - assert self.device_id != 0, tcod.sdl.sys._get_error() + assert self.device_id != 0, _get_error() self.frequency = obtained.freq self.is_capture = capture self.format = _dtype_from_format(obtained.format) diff --git a/tcod/sdl/render.py b/tcod/sdl/render.py new file mode 100644 index 00000000..fa5ae333 --- /dev/null +++ b/tcod/sdl/render.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +from typing import Any, Optional, Tuple + +import numpy as np +from numpy.typing import NDArray + +from tcod.loader import ffi, lib +from tcod.sdl import _check + + +class Texture: + def __init__(self, sdl_texture_p: Any, sdl_renderer_p: Any = None) -> None: + self.p = sdl_texture_p + self._sdl_renderer_p = sdl_renderer_p # Keep alive. + + def __eq__(self, other: Any) -> bool: + return bool(self.p == getattr(other, "p", None)) + + def _query(self) -> Tuple[int, int, int, int]: + """Return (format, access, width, height).""" + format = ffi.new("uint32_t*") + buffer = ffi.new("int[3]") + lib.SDL_QueryTexture(self.p, format, buffer, buffer + 1, buffer + 2) + return int(format), int(buffer[0]), int(buffer[1]), int(buffer[2]) + + @property + def format(self) -> int: + """Texture format, read only.""" + buffer = ffi.new("uint32_t*") + lib.SDL_QueryTexture(self.p, buffer, ffi.NULL, ffi.NULL, ffi.NULL) + return int(buffer[0]) + + @property + def access(self) -> int: + """Texture access mode, read only.""" + buffer = ffi.new("int*") + lib.SDL_QueryTexture(self.p, ffi.NULL, buffer, ffi.NULL, ffi.NULL) + return int(buffer[0]) + + @property + def width(self) -> int: + """Texture pixel width, read only.""" + buffer = ffi.new("int*") + lib.SDL_QueryTexture(self.p, ffi.NULL, ffi.NULL, buffer, ffi.NULL) + return int(buffer[0]) + + @property + def height(self) -> int: + """Texture pixel height, read only.""" + buffer = ffi.new("int*") + lib.SDL_QueryTexture(self.p, ffi.NULL, ffi.NULL, ffi.NULL, buffer) + return int(buffer[0]) + + @property + def alpha_mod(self) -> int: + """Texture alpha modulate value, can be set to: 0 - 255.""" + return int(lib.SDL_GetTextureAlphaMod(self.p)) + + @alpha_mod.setter + def alpha_mod(self, value: int) -> None: + _check(lib.SDL_SetTextureAlphaMod(self.p, value)) + + @property + def blend_mode(self) -> int: + """Texture blend mode, can be set.""" + return int(lib.SDL_GetTextureBlendMode(self.p)) + + @blend_mode.setter + def blend_mode(self, value: int) -> None: + _check(lib.SDL_SetTextureBlendMode(self.p, value)) + + @property + def rgb_mod(self) -> Tuple[int, int, int]: + """Texture RGB color modulate values, can be set.""" + rgb = ffi.new("uint8_t[3]") + _check(lib.SDL_GetTextureColorMod(self.p, rgb, rgb + 1, rgb + 2)) + return int(rgb[0]), int(rgb[1]), int(rgb[2]) + + @rgb_mod.setter + def rgb_mod(self, rgb: Tuple[int, int, int]) -> None: + _check(lib.SDL_SetTextureColorMod(self.p, rgb[0], rgb[1], rgb[2])) + + +class Renderer: + def __init__(self, sdl_renderer_p: Any) -> None: + if ffi.typeof(sdl_renderer_p) is not ffi.typeof("struct SDL_Renderer*"): + raise TypeError(f"Expected a {ffi.typeof('struct SDL_Window*')} type (was {ffi.typeof(sdl_renderer_p)}).") + self.p = sdl_renderer_p + + def __eq__(self, other: Any) -> bool: + return bool(self.p == getattr(other, "p", None)) + + def new_texture( + self, width: int, height: int, *, format: Optional[int] = None, access: Optional[int] = None + ) -> Texture: + """Allocate and return a new Texture for this renderer.""" + if format is None: + format = 0 + if access is None: + access = int(lib.SDL_TEXTUREACCESS_STATIC) + format = int(lib.SDL_PIXELFORMAT_RGBA32) + access = int(lib.SDL_TEXTUREACCESS_STATIC) + texture_p = ffi.gc(lib.SDL_CreateTexture(self.p, format, access, width, height), lib.SDL_DestroyTexture) + return Texture(texture_p, self.p) + + def upload_texture( + self, pixels: NDArray[Any], *, format: Optional[int] = None, access: Optional[int] = None + ) -> Texture: + """Return a new Texture from an array of pixels.""" + if format is None: + assert len(pixels.shape) == 3 + assert pixels.dtype == np.uint8 + if pixels.shape[2] == 4: + format = int(lib.SDL_PIXELFORMAT_RGBA32) + elif pixels.shape[2] == 3: + format = int(lib.SDL_PIXELFORMAT_RGB32) + else: + assert False + + texture = self.new_texture(pixels.shape[1], pixels.shape[0], format=format, access=access) + if not pixels[0].flags["C_CONTIGUOUS"]: + pixels = np.ascontiguousarray(pixels) + _check( + lib.SDL_UpdateTexture(texture.p, ffi.NULL, ffi.cast("const void*", pixels.ctypes.data), pixels.strides[0]) + ) + return texture diff --git a/tcod/sdl/sys.py b/tcod/sdl/sys.py index a47b2a70..a16d09a0 100644 --- a/tcod/sdl/sys.py +++ b/tcod/sdl/sys.py @@ -4,6 +4,7 @@ from typing import Any, Tuple from tcod.loader import ffi, lib +from tcod.sdl import _check class Subsystem(enum.IntFlag): @@ -18,12 +19,6 @@ class Subsystem(enum.IntFlag): EVERYTHING = lib.SDL_INIT_EVERYTHING -def _check(result: int) -> int: - if result < 0: - raise RuntimeError(_get_error()) - return result - - def init(flags: int = Subsystem.EVERYTHING) -> None: _check(lib.SDL_InitSubSystem(flags)) @@ -52,10 +47,6 @@ def __exit__(self, *args: Any) -> None: self.close() -def _get_error() -> str: - return str(ffi.string(lib.SDL_GetError()), encoding="utf-8") - - class _PowerState(enum.IntEnum): UNKNOWN = lib.SDL_POWERSTATE_UNKNOWN ON_BATTERY = lib.SDL_POWERSTATE_ON_BATTERY From 849201439ee85aea4a3c3966f67ad6b35d9f7bc3 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 29 Jan 2022 15:49:14 -0800 Subject: [PATCH 022/194] Port libtcod direct SDL rendering functions. Allow access to the fancy SDL classes from tcod contexts. --- tcod/context.py | 20 +++++++++++++++++ tcod/render.py | 56 ++++++++++++++++++++++++++++++++++++++++++++++ tcod/sdl/render.py | 2 +- tcod/sdl/video.py | 11 ++++++++- 4 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 tcod/render.py diff --git a/tcod/context.py b/tcod/context.py index e62ac042..3c8470b1 100644 --- a/tcod/context.py +++ b/tcod/context.py @@ -58,6 +58,8 @@ import tcod import tcod.event +import tcod.sdl.render +import tcod.sdl.video import tcod.tileset from tcod._internal import _check, _check_warn, pending_deprecate from tcod.loader import ffi, lib @@ -351,6 +353,24 @@ def toggle_fullscreen(context: tcod.context.Context) -> None: ''' # noqa: E501 return lib.TCOD_context_get_sdl_window(self._context_p) + @property + def sdl_window(self) -> Optional[tcod.sdl.video.Window]: + """Return a tcod.sdl.video.Window referencing this contexts SDL window if it exists. + + .. versionadded:: 13.4 + """ + p = self.sdl_window_p + return tcod.sdl.video.Window(p) if p else None + + @property + def sdl_renderer(self) -> Optional[tcod.sdl.render.Renderer]: + """Return a tcod.sdl.render.Renderer referencing this contexts SDL renderer if it exists. + + .. versionadded:: 13.4 + """ + p = lib.TCOD_context_get_sdl_renderer(self._context_p) + return tcod.sdl.render.Renderer(p) if p else None + def __reduce__(self) -> NoReturn: """Contexts can not be pickled, so this class will raise :class:`pickle.PicklingError`. diff --git a/tcod/render.py b/tcod/render.py new file mode 100644 index 00000000..1cfc9f9c --- /dev/null +++ b/tcod/render.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +from typing import Optional + +import tcod.console +import tcod.sdl.render +import tcod.tileset +from tcod._internal import _check, _check_p +from tcod.loader import ffi, lib + + +class SDLTilesetAtlas: + """Prepares a tileset for rendering using SDL.""" + + def __init__(self, renderer: tcod.sdl.render.Renderer, tileset: tcod.tileset.Tileset) -> None: + self._renderer = renderer + self.tileset = tileset + self.p = ffi.gc(_check_p(lib.TCOD_sdl2_atlas_new(renderer.p, tileset._tileset_p)), lib.TCOD_sdl2_atlas_delete) + + +class SDLConsoleRender: + """Holds an internal cache console and texture which are used to optimized console rendering.""" + + def __init__(self, atlas: SDLTilesetAtlas) -> None: + self._atlas = atlas + self._renderer = atlas._renderer + self._cache_console: Optional[tcod.console.Console] = None + self._texture: Optional[tcod.sdl.render.Texture] = None + + def render(self, console: tcod.console.Console) -> tcod.sdl.render.Texture: + """Render a console to a cached Texture and then return the Texture. + + You should not draw onto the returned Texture as only changed parts of it will be updated on the next call. + + This function requires the SDL renderer to have target texture support. + It will also change the SDL target texture for the duration of the call. + """ + if self._cache_console and ( + self._cache_console.width != console.width or self._cache_console.height != console.height + ): + self._cache_console = None + self._texture = None + if self._cache_console is None or self._texture is None: + self._cache_console = tcod.console.Console(console.width, console.height) + self._texture = self._renderer.new_texture( + self._atlas.tileset.tile_width * console.width, + self._atlas.tileset.tile_height * console.height, + format=int(lib.SDL_PIXELFORMAT_RGBA32), + access=int(lib.SDL_TEXTUREACCESS_TARGET), + ) + _check( + lib.TCOD_sdl2_render_texture( + self._atlas.p, console.console_c, self._cache_console.console_c, self._texture.p + ) + ) + return self._texture diff --git a/tcod/sdl/render.py b/tcod/sdl/render.py index fa5ae333..2fa3601e 100644 --- a/tcod/sdl/render.py +++ b/tcod/sdl/render.py @@ -116,7 +116,7 @@ def upload_texture( elif pixels.shape[2] == 3: format = int(lib.SDL_PIXELFORMAT_RGB32) else: - assert False + raise TypeError(f"Can't determine the format required for an array of shape {pixels.shape}.") texture = self.new_texture(pixels.shape[1], pixels.shape[0], format=format, access=access) if not pixels[0].flags["C_CONTIGUOUS"]: diff --git a/tcod/sdl/video.py b/tcod/sdl/video.py index 470011e6..635d5bda 100644 --- a/tcod/sdl/video.py +++ b/tcod/sdl/video.py @@ -114,8 +114,17 @@ def size(self, xy: Tuple[int, int]) -> None: x, y = xy lib.SDL_SetWindowSize(self.p, x, y) + @property + def title(self) -> str: + """The title of the window. You may set this attribute to change it.""" + return str(ffi.string(lib.SDL_GetWindowtitle(self.p)), encoding="utf-8") + + @title.setter + def title(self, value: str) -> None: + lib.SDL_SetWindowtitle(self.p, value.encode("utf-8")) + -def get_active_window() -> Window: +def _get_active_window() -> Window: """Return the SDL2 window current managed by libtcod. Will raise an error if libtcod does not currently have a window. From ebbac13a87069660d571601fb364a8bd1bc0f95c Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 29 Jan 2022 15:59:50 -0800 Subject: [PATCH 023/194] Port SDL_RenderCopy. This should be the minimum needed to render tcod tileset directly. --- tcod/sdl/render.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tcod/sdl/render.py b/tcod/sdl/render.py index 2fa3601e..641a0bf6 100644 --- a/tcod/sdl/render.py +++ b/tcod/sdl/render.py @@ -91,6 +91,20 @@ def __init__(self, sdl_renderer_p: Any) -> None: def __eq__(self, other: Any) -> bool: return bool(self.p == getattr(other, "p", None)) + def copy( + self, + texture: Texture, + source: Optional[Tuple[int, int, int, int]] = None, + dest: Optional[Tuple[int, int, int, int]] = None, + ) -> None: + """Copy a texture to the rendering target. + + `source` and `dest` are (x, y, width, height) regions of the texture parameter and target texture respectively. + """ + source_ = ffi.NULL if source is None else ffi.new("SDL_Rect*", source) + dest_ = ffi.NULL if dest is None else ffi.new("SDL_Rect*", dest) + _check(lib.SDL_RenderCopy(self.p, texture.p, source_, dest_)) + def new_texture( self, width: int, height: int, *, format: Optional[int] = None, access: Optional[int] = None ) -> Texture: From 957e4b778b7abfe6d6deada94317fab850343316 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 29 Jan 2022 16:29:32 -0800 Subject: [PATCH 024/194] Run the latest version of Black. --- examples/samples_libtcodpy.py | 8 ++++---- examples/samples_tcod.py | 6 +++--- scripts/get_release_description.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/samples_libtcodpy.py b/examples/samples_libtcodpy.py index 6b5b05a7..ba91aeca 100755 --- a/examples/samples_libtcodpy.py +++ b/examples/samples_libtcodpy.py @@ -1422,8 +1422,8 @@ def render_py(first, key, mouse): ) + libtcod.noise_get_fbm(noise2d, [1 - u / float(RES_U), tex_v], 32.0) if use_numpy: # squared distance from center, clipped to sensible minimum and maximum values - sqr_dist = xc ** 2 + yc ** 2 - sqr_dist = sqr_dist.clip(1.0 / RES_V, RES_V ** 2) + sqr_dist = xc**2 + yc**2 + sqr_dist = sqr_dist.clip(1.0 / RES_V, RES_V**2) # one coordinate into the texture, represents depth in the tunnel v = TEX_STRETCH * float(RES_V) / sqr_dist + frac_t @@ -1444,8 +1444,8 @@ def render_py(first, key, mouse): for y in range(-HALF_H, HALF_H): for x in range(-HALF_W, HALF_W): # squared distance from center, clipped to sensible minimum and maximum values - sqr_dist = x ** 2 + y ** 2 - sqr_dist = min(max(sqr_dist, 1.0 / RES_V), RES_V ** 2) + sqr_dist = x**2 + y**2 + sqr_dist = min(max(sqr_dist, 1.0 / RES_V), RES_V**2) # one coordinate into the texture, represents depth in the tunnel v = TEX_STRETCH * float(RES_V) / sqr_dist + frac_t diff --git a/examples/samples_tcod.py b/examples/samples_tcod.py index 2d95b315..d1fc0f79 100755 --- a/examples/samples_tcod.py +++ b/examples/samples_tcod.py @@ -608,7 +608,7 @@ def on_draw(self) -> None: x = x.astype(np.float32) - torch_x y = y.astype(np.float32) - torch_y - distance_squared = x ** 2 + y ** 2 # 2D squared distance array. + distance_squared = x**2 + y**2 # 2D squared distance array. # Get the currently visible cells. visible = (distance_squared < SQUARED_TORCH_RADIUS) & fov @@ -1299,8 +1299,8 @@ def on_draw(self) -> None: # squared distance from center, # clipped to sensible minimum and maximum values - sqr_dist = xc ** 2 + yc ** 2 - sqr_dist = sqr_dist.clip(1.0 / RES_V, RES_V ** 2) + sqr_dist = xc**2 + yc**2 + sqr_dist = sqr_dist.clip(1.0 / RES_V, RES_V**2) # one coordinate into the texture, represents depth in the tunnel vv = TEX_STRETCH * float(RES_V) / sqr_dist + self.frac_t diff --git a/scripts/get_release_description.py b/scripts/get_release_description.py index 29844caf..7d47d9f9 100755 --- a/scripts/get_release_description.py +++ b/scripts/get_release_description.py @@ -7,7 +7,7 @@ TAG_BANNER = r"## \[[\w.]*\] - \d+-\d+-\d+\n" -RE_BODY = re.compile(fr".*?{TAG_BANNER}(.*?){TAG_BANNER}", re.DOTALL) +RE_BODY = re.compile(rf".*?{TAG_BANNER}(.*?){TAG_BANNER}", re.DOTALL) def main() -> None: From c0a890c9a4d99480749cb9555bdff32644e31319 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 29 Jan 2022 16:31:04 -0800 Subject: [PATCH 025/194] Fix remaining issues with rendering. --- tcod/render.py | 10 ++++++---- tcod/sdl/render.py | 26 ++++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/tcod/render.py b/tcod/render.py index 1cfc9f9c..21e98db0 100644 --- a/tcod/render.py +++ b/tcod/render.py @@ -48,9 +48,11 @@ def render(self, console: tcod.console.Console) -> tcod.sdl.render.Texture: format=int(lib.SDL_PIXELFORMAT_RGBA32), access=int(lib.SDL_TEXTUREACCESS_TARGET), ) - _check( - lib.TCOD_sdl2_render_texture( - self._atlas.p, console.console_c, self._cache_console.console_c, self._texture.p + + with self._renderer.set_render_target(self._texture): + _check( + lib.TCOD_sdl2_render_texture( + self._atlas.p, console.console_c, self._cache_console.console_c, self._texture.p + ) ) - ) return self._texture diff --git a/tcod/sdl/render.py b/tcod/sdl/render.py index 641a0bf6..bc929bb5 100644 --- a/tcod/sdl/render.py +++ b/tcod/sdl/render.py @@ -82,6 +82,20 @@ def rgb_mod(self, rgb: Tuple[int, int, int]) -> None: _check(lib.SDL_SetTextureColorMod(self.p, rgb[0], rgb[1], rgb[2])) +class _RestoreTargetContext: + """A context manager which tracks the current render target and restores it on exiting.""" + + def __init__(self, renderer: Renderer) -> None: + self.renderer = renderer + self.old_texture_p = lib.SDL_GetRenderTarget(renderer.p) + + def __enter__(self) -> None: + pass + + def __exit__(self, *_: Any) -> None: + _check(lib.SDL_SetRenderTarget(self.renderer.p, self.old_texture_p)) + + class Renderer: def __init__(self, sdl_renderer_p: Any) -> None: if ffi.typeof(sdl_renderer_p) is not ffi.typeof("struct SDL_Renderer*"): @@ -105,6 +119,10 @@ def copy( dest_ = ffi.NULL if dest is None else ffi.new("SDL_Rect*", dest) _check(lib.SDL_RenderCopy(self.p, texture.p, source_, dest_)) + def present(self) -> None: + """Present the currently rendered image to the screen.""" + lib.SDL_RenderPresent(self.p) + def new_texture( self, width: int, height: int, *, format: Optional[int] = None, access: Optional[int] = None ) -> Texture: @@ -113,11 +131,15 @@ def new_texture( format = 0 if access is None: access = int(lib.SDL_TEXTUREACCESS_STATIC) - format = int(lib.SDL_PIXELFORMAT_RGBA32) - access = int(lib.SDL_TEXTUREACCESS_STATIC) texture_p = ffi.gc(lib.SDL_CreateTexture(self.p, format, access, width, height), lib.SDL_DestroyTexture) return Texture(texture_p, self.p) + def set_render_target(self, texture: Texture) -> _RestoreTargetContext: + """Change the render target to `texture`, returns a context that will restore the original target when exited.""" + restore = _RestoreTargetContext(self) + _check(lib.SDL_SetRenderTarget(self.p, texture.p)) + return restore + def upload_texture( self, pixels: NDArray[Any], *, format: Optional[int] = None, access: Optional[int] = None ) -> Texture: From 0165926ba139399f293d1875b4e624f9761490ef Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 29 Jan 2022 16:36:18 -0800 Subject: [PATCH 026/194] Have Flake8 ignore slightly too long lines. --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 4b26be03..faafa1fc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,7 +10,7 @@ test=pytest [flake8] ignore = E203 W503 -max-line-length = 120 +max-line-length = 130 [mypy] python_version = 3.8 From 1cf53b4988790d6c5366fb211eefbffb80ae4e2c Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 29 Jan 2022 16:53:47 -0800 Subject: [PATCH 027/194] Add tcod.sdl package to setup.py. --- examples/samples_tcod.py | 9 ++++++++- setup.py | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/examples/samples_tcod.py b/examples/samples_tcod.py index d1fc0f79..8eba7149 100755 --- a/examples/samples_tcod.py +++ b/examples/samples_tcod.py @@ -18,6 +18,7 @@ import numpy as np import tcod +import tcod.render from numpy.typing import NDArray if not sys.warnoptions: @@ -1417,6 +1418,10 @@ def main() -> None: global context, tileset tileset = tcod.tileset.load_tilesheet(FONT, 32, 8, tcod.tileset.CHARMAP_TCOD) context = init_context(tcod.RENDERER_SDL2) + sdl_renderer = context.sdl_renderer + assert sdl_renderer + atlas = tcod.render.SDLTilesetAtlas(sdl_renderer, tileset) + console_render = tcod.render.SDLConsoleRender(atlas) try: SAMPLES[cur_sample].on_enter() @@ -1429,7 +1434,9 @@ def main() -> None: SAMPLES[cur_sample].on_draw() sample_console.blit(root_console, SAMPLE_SCREEN_X, SAMPLE_SCREEN_Y) draw_stats() - context.present(root_console) + # context.present(root_console) + sdl_renderer.copy(console_render.render(root_console)) + sdl_renderer.present() handle_time() handle_events() finally: diff --git a/setup.py b/setup.py index 2f81769d..411653fd 100755 --- a/setup.py +++ b/setup.py @@ -109,7 +109,7 @@ def check_sdl_version() -> None: "Forum": "https://github.com/libtcod/python-tcod/discussions", }, py_modules=["libtcodpy"], - packages=["tcod", "tcod.__pyinstaller"], + packages=["tcod", "tcod.sdl", "tcod.__pyinstaller"], package_data={"tcod": get_package_data()}, python_requires=">=3.7", setup_requires=[ From ee8cde1e4e10440ed5771941a360706b113ed109 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 30 Jan 2022 19:27:34 -0800 Subject: [PATCH 028/194] Add support for creating plain SDL windows and renderers. --- .vscode/settings.json | 1 + tcod/sdl/__init__.py | 7 +++++++ tcod/sdl/render.py | 34 +++++++++++++++++++++++++++++++++- tcod/sdl/video.py | 31 ++++++++++++++++++++++++++++++- 4 files changed, 71 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 5b8556b3..8385c134 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -220,6 +220,7 @@ "pcpp", "PILCROW", "pilmode", + "PRESENTVSYNC", "PRINTF", "printn", "pycall", diff --git a/tcod/sdl/__init__.py b/tcod/sdl/__init__.py index 66aa7885..2415234d 100644 --- a/tcod/sdl/__init__.py +++ b/tcod/sdl/__init__.py @@ -35,4 +35,11 @@ def _check(result: int) -> int: return result +def _check_p(result: Any) -> Any: + """Check if an SDL function returned NULL, and raise an exception if it did.""" + if not result: + raise RuntimeError(_get_error()) + return result + + lib.SDL_LogSetOutputFunction(lib._sdl_log_output_function, ffi.NULL) diff --git a/tcod/sdl/render.py b/tcod/sdl/render.py index bc929bb5..36a56c52 100644 --- a/tcod/sdl/render.py +++ b/tcod/sdl/render.py @@ -5,8 +5,9 @@ import numpy as np from numpy.typing import NDArray +import tcod.sdl.video from tcod.loader import ffi, lib -from tcod.sdl import _check +from tcod.sdl import _check, _check_p class Texture: @@ -161,3 +162,34 @@ def upload_texture( lib.SDL_UpdateTexture(texture.p, ffi.NULL, ffi.cast("const void*", pixels.ctypes.data), pixels.strides[0]) ) return texture + + +def new_renderer( + window: tcod.sdl.video.Window, + *, + driver: Optional[int] = None, + software: bool = False, + vsync: bool = True, + target_textures: bool = False, +) -> Renderer: + """Initialize and return a new SDL Renderer. + + Example:: + + # Start by creating a window. + sdl_window = tcod.sdl.video.new_window(640, 480) + # Create a renderer with target texture support. + sdl_renderer = tcod.sdl.render.new_renderer(sdl_window, target_textures=True) + + .. seealso:: + :func:`tcod.sdl.video.new_window` + """ + driver = driver if driver is not None else -1 + flags = 0 + if vsync: + flags |= int(lib.SDL_RENDERER_PRESENTVSYNC) + if target_textures: + flags |= int(lib.SDL_RENDERER_TARGETTEXTURE) + flags |= int(lib.SDL_RENDERER_SOFTWARE) if software else int(lib.SDL_RENDERER_ACCELERATED) + renderer_p = _check_p(ffi.gc(lib.SDL_CreateRenderer(window.p, driver, flags), lib.SDL_DestroyRenderer)) + return Renderer(renderer_p) diff --git a/tcod/sdl/video.py b/tcod/sdl/video.py index 635d5bda..53b10da6 100644 --- a/tcod/sdl/video.py +++ b/tcod/sdl/video.py @@ -5,12 +5,14 @@ """ from __future__ import annotations -from typing import Any, Tuple +import sys +from typing import Any, Optional, Tuple import numpy as np from numpy.typing import ArrayLike, NDArray from tcod.loader import ffi, lib +from tcod.sdl import _check_p __all__ = ("Window",) @@ -124,6 +126,33 @@ def title(self, value: str) -> None: lib.SDL_SetWindowtitle(self.p, value.encode("utf-8")) +def new_window( + width: int, + height: int, + *, + x: Optional[int] = None, + y: Optional[int] = None, + title: Optional[str] = None, + flags: int = 0, +) -> Window: + """Initialize and return a new SDL Window. + + Example:: + + # Create a new resizable window with a custom title. + window = tcod.sdl.video.new_window(640, 480, title="Title bar text", flags=tcod.lib.SDL_WINDOW_RESIZABLE) + + .. seealso:: + :func:`tcod.sdl.render.new_renderer` + """ + x = x if x is not None else int(lib.SDL_WINDOWPOS_UNDEFINED) + y = y if y is not None else int(lib.SDL_WINDOWPOS_UNDEFINED) + if title is None: + title = sys.argv[0] + window_p = ffi.gc(lib.SDL_CreateWindow(title.encode("utf-8"), x, y, width, height, flags), lib.SDL_DestroyWindow) + return Window(_check_p(window_p)) + + def _get_active_window() -> Window: """Return the SDL2 window current managed by libtcod. From 9073a8ecf18b8c2121d649916f547b966438b76a Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 30 Jan 2022 20:18:16 -0800 Subject: [PATCH 029/194] Don't access tcod.lib at the top-level, for doc generation. --- tcod/sdl/__init__.py | 15 ++++++++------- tcod/sdl/sys.py | 28 ++++++++++++++-------------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/tcod/sdl/__init__.py b/tcod/sdl/__init__.py index 2415234d..0e260bde 100644 --- a/tcod/sdl/__init__.py +++ b/tcod/sdl/__init__.py @@ -8,12 +8,12 @@ logger = logging.getLogger(__name__) _LOG_PRIORITY = { - int(lib.SDL_LOG_PRIORITY_VERBOSE): logging.DEBUG, - int(lib.SDL_LOG_PRIORITY_DEBUG): logging.DEBUG, - int(lib.SDL_LOG_PRIORITY_INFO): logging.INFO, - int(lib.SDL_LOG_PRIORITY_WARN): logging.WARNING, - int(lib.SDL_LOG_PRIORITY_ERROR): logging.ERROR, - int(lib.SDL_LOG_PRIORITY_CRITICAL): logging.CRITICAL, + 1: logging.DEBUG, # SDL_LOG_PRIORITY_VERBOSE + 2: logging.DEBUG, # SDL_LOG_PRIORITY_DEBUG + 3: logging.INFO, # SDL_LOG_PRIORITY_INFO + 4: logging.WARNING, # SDL_LOG_PRIORITY_WARN + 5: logging.ERROR, # SDL_LOG_PRIORITY_ERROR + 6: logging.CRITICAL, # SDL_LOG_PRIORITY_CRITICAL } @@ -42,4 +42,5 @@ def _check_p(result: Any) -> Any: return result -lib.SDL_LogSetOutputFunction(lib._sdl_log_output_function, ffi.NULL) +if lib._sdl_log_output_function: + lib.SDL_LogSetOutputFunction(lib._sdl_log_output_function, ffi.NULL) diff --git a/tcod/sdl/sys.py b/tcod/sdl/sys.py index a16d09a0..4d9c0cf1 100644 --- a/tcod/sdl/sys.py +++ b/tcod/sdl/sys.py @@ -8,15 +8,15 @@ class Subsystem(enum.IntFlag): - TIMER = lib.SDL_INIT_TIMER - AUDIO = lib.SDL_INIT_AUDIO - VIDEO = lib.SDL_INIT_VIDEO - JOYSTICK = lib.SDL_INIT_JOYSTICK - HAPTIC = lib.SDL_INIT_HAPTIC - GAMECONTROLLER = lib.SDL_INIT_GAMECONTROLLER - EVENTS = lib.SDL_INIT_EVENTS - SENSOR = getattr(lib, "SDL_INIT_SENSOR", 0) - EVERYTHING = lib.SDL_INIT_EVERYTHING + TIMER = getattr(lib, "SDL_INIT_TIMER", 0x00000001) + AUDIO = getattr(lib, "SDL_INIT_AUDIO", 0x00000010) + VIDEO = getattr(lib, "SDL_INIT_VIDEO", 0x00000020) + JOYSTICK = getattr(lib, "SDL_INIT_JOYSTICK", 0x00000200) + HAPTIC = getattr(lib, "SDL_INIT_HAPTIC", 0x00001000) + GAMECONTROLLER = getattr(lib, "SDL_INIT_GAMECONTROLLER", 0x00002000) + EVENTS = getattr(lib, "SDL_INIT_EVENTS", 0x00004000) + SENSOR = getattr(lib, "SDL_INIT_SENSOR", 0x00008000) + EVERYTHING = getattr(lib, "SDL_INIT_EVERYTHING", 0) def init(flags: int = Subsystem.EVERYTHING) -> None: @@ -48,11 +48,11 @@ def __exit__(self, *args: Any) -> None: class _PowerState(enum.IntEnum): - UNKNOWN = lib.SDL_POWERSTATE_UNKNOWN - ON_BATTERY = lib.SDL_POWERSTATE_ON_BATTERY - NO_BATTERY = lib.SDL_POWERSTATE_NO_BATTERY - CHARGING = lib.SDL_POWERSTATE_CHARGING - CHARGED = lib.SDL_POWERSTATE_CHARGED + UNKNOWN = getattr(lib, "SDL_POWERSTATE_UNKNOWN", 0) + ON_BATTERY = getattr(lib, "SDL_POWERSTATE_ON_BATTERY", 0) + NO_BATTERY = getattr(lib, "SDL_POWERSTATE_NO_BATTERY", 0) + CHARGING = getattr(lib, "SDL_POWERSTATE_CHARGING", 0) + CHARGED = getattr(lib, "SDL_POWERSTATE_CHARGED", 0) def _get_power_info() -> Tuple[_PowerState, int, int]: From 65d6bf3cee47128652c4f9e31280d570ca9677fb Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 1 Feb 2022 13:30:02 -0800 Subject: [PATCH 030/194] Port more methods to SDL windows. --- tcod/render.py | 23 +++++ tcod/sdl/__init__.py | 28 +++++- tcod/sdl/sys.py | 18 ++-- tcod/sdl/video.py | 202 +++++++++++++++++++++++++++++++++++++++---- 4 files changed, 246 insertions(+), 25 deletions(-) diff --git a/tcod/render.py b/tcod/render.py index 21e98db0..f1bc87ce 100644 --- a/tcod/render.py +++ b/tcod/render.py @@ -1,3 +1,26 @@ +"""Handle the rendering of libtcod's tilesets. + +Example:: + + tileset = tcod.tileset.load_tilsheet("dejavu16x16_gs_tc.png", 32, 8, tcod.tileset.CHARMAP_TCOD) + console = tcod.Console(20, 8) + console.print(0, 0, "Hello World") + sdl_window = tcod.sdl.video.new_window( + console.width * tileset.tile_width, + console.height * tileset.tile_height, + flags=tcod.lib.SDL_WINDOW_RESIZABLE, + ) + sdl_renderer = tcod.sdl.render.new_renderer(sdl_window, target_textures=True) + atlas = tcod.render.SDLTilesetAtlas(sdl_renderer, tileset) + console_render = tcod.render.SDLConsoleRender(atlas) + while True: + sdl_renderer.copy(console_render.render(console)) + sdl_renderer.present() + for event in tcod.event.wait(): + if isinstance(event, tcod.event.Quit): + raise SystemExit() +""" + from __future__ import annotations from typing import Optional diff --git a/tcod/sdl/__init__.py b/tcod/sdl/__init__.py index 0e260bde..524f24b6 100644 --- a/tcod/sdl/__init__.py +++ b/tcod/sdl/__init__.py @@ -1,10 +1,12 @@ from __future__ import annotations import logging -from typing import Any +from typing import Any, Callable, Tuple, TypeVar from tcod.loader import ffi, lib +T = TypeVar("T") + logger = logging.getLogger(__name__) _LOG_PRIORITY = { @@ -44,3 +46,27 @@ def _check_p(result: Any) -> Any: if lib._sdl_log_output_function: lib.SDL_LogSetOutputFunction(lib._sdl_log_output_function, ffi.NULL) + + +def _compiled_version() -> Tuple[int, int, int]: + return int(lib.SDL_MAJOR_VERSION), int(lib.SDL_MINOR_VERSION), int(lib.SDL_PATCHLEVEL) + + +def _linked_version() -> Tuple[int, int, int]: + sdl_version = ffi.new("SDL_version*") + lib.SDL_GetVersion(sdl_version) + return int(sdl_version.major), int(sdl_version.minor), int(sdl_version.patch) + + +def _required_version(required: Tuple[int, int, int]) -> Callable[[T], T]: + if not lib: # Read the docs mock object. + return lambda x: x + if required <= _compiled_version(): + return lambda x: x + + def replacement(*_args: Any, **_kwargs: Any) -> Any: + raise RuntimeError( + f"This feature requires SDL version {required}, but tcod was compiled with version {_compiled_version()}" + ) + + return lambda x: replacement # type: ignore[return-value] diff --git a/tcod/sdl/sys.py b/tcod/sdl/sys.py index 4d9c0cf1..7569ed83 100644 --- a/tcod/sdl/sys.py +++ b/tcod/sdl/sys.py @@ -8,15 +8,15 @@ class Subsystem(enum.IntFlag): - TIMER = getattr(lib, "SDL_INIT_TIMER", 0x00000001) - AUDIO = getattr(lib, "SDL_INIT_AUDIO", 0x00000010) - VIDEO = getattr(lib, "SDL_INIT_VIDEO", 0x00000020) - JOYSTICK = getattr(lib, "SDL_INIT_JOYSTICK", 0x00000200) - HAPTIC = getattr(lib, "SDL_INIT_HAPTIC", 0x00001000) - GAMECONTROLLER = getattr(lib, "SDL_INIT_GAMECONTROLLER", 0x00002000) - EVENTS = getattr(lib, "SDL_INIT_EVENTS", 0x00004000) - SENSOR = getattr(lib, "SDL_INIT_SENSOR", 0x00008000) - EVERYTHING = getattr(lib, "SDL_INIT_EVERYTHING", 0) + TIMER = lib.SDL_INIT_TIMER or 0x00000001 + AUDIO = lib.SDL_INIT_AUDIO or 0x00000010 + VIDEO = lib.SDL_INIT_VIDEO or 0x00000020 + JOYSTICK = lib.SDL_INIT_JOYSTICK or 0x00000200 + HAPTIC = lib.SDL_INIT_HAPTIC or 0x00001000 + GAMECONTROLLER = lib.SDL_INIT_GAMECONTROLLER or 0x00002000 + EVENTS = lib.SDL_INIT_EVENTS or 0x00004000 + SENSOR = getattr(lib, "SDL_INIT_SENSOR", None) or 0x00008000 # SDL >= 2.0.9 + EVERYTHING = lib.SDL_INIT_EVERYTHING or 0 def init(flags: int = Subsystem.EVERYTHING) -> None: diff --git a/tcod/sdl/video.py b/tcod/sdl/video.py index 53b10da6..c77d91ad 100644 --- a/tcod/sdl/video.py +++ b/tcod/sdl/video.py @@ -5,6 +5,7 @@ """ from __future__ import annotations +import enum import sys from typing import Any, Optional, Tuple @@ -12,9 +13,52 @@ from numpy.typing import ArrayLike, NDArray from tcod.loader import ffi, lib -from tcod.sdl import _check_p +from tcod.sdl import _check, _check_p, _required_version -__all__ = ("Window",) +__all__ = ( + "WindowFlags", + "FlashOperation", + "Window", + "new_window", + "get_grabbed_window", +) + + +class WindowFlags(enum.IntFlag): + """Bit flags which make up a windows state. + + .. seealso:: + https://wiki.libsdl.org/SDL_WindowFlags + """ + + FULLSCREEN = lib.SDL_WINDOW_FULLSCREEN or 0 + FULLSCREEN_DESKTOP = lib.SDL_WINDOW_FULLSCREEN_DESKTOP or 0 + OPENGL = lib.SDL_WINDOW_OPENGL or 0 + SHOWN = lib.SDL_WINDOW_SHOWN or 0 + HIDDEN = lib.SDL_WINDOW_HIDDEN or 0 + BORDERLESS = lib.SDL_WINDOW_BORDERLESS or 0 + RESIZABLE = lib.SDL_WINDOW_RESIZABLE or 0 + MINIMIZED = lib.SDL_WINDOW_MINIMIZED or 0 + MAXIMIZED = lib.SDL_WINDOW_MAXIMIZED or 0 + MOUSE_GRABBED = lib.SDL_WINDOW_INPUT_GRABBED or 0 + INPUT_FOCUS = lib.SDL_WINDOW_INPUT_FOCUS or 0 + MOUSE_FOCUS = lib.SDL_WINDOW_MOUSE_FOCUS or 0 + FOREIGN = lib.SDL_WINDOW_FOREIGN or 0 + ALLOW_HIGHDPI = lib.SDL_WINDOW_ALLOW_HIGHDPI or 0 + MOUSE_CAPTURE = lib.SDL_WINDOW_MOUSE_CAPTURE or 0 + ALWAYS_ON_TOP = lib.SDL_WINDOW_ALWAYS_ON_TOP or 0 + SKIP_TASKBAR = lib.SDL_WINDOW_SKIP_TASKBAR or 0 + UTILITY = lib.SDL_WINDOW_UTILITY or 0 + TOOLTIP = lib.SDL_WINDOW_TOOLTIP or 0 + POPUP_MENU = lib.SDL_WINDOW_POPUP_MENU or 0 + VULKAN = lib.SDL_WINDOW_VULKAN or 0 + METAL = getattr(lib, "SDL_WINDOW_METAL", None) or 0x20000000 # SDL >= 2.0.14 + + +class FlashOperation(enum.IntEnum): + CANCEL = 0 + BRIEFLY = 1 + UNTIL_FOCUSED = 2 class _TempSurface: @@ -67,10 +111,7 @@ def set_icon(self, image: ArrayLike) -> None: @property def allow_screen_saver(self) -> bool: - """If True the operating system is allowed to display a screen saver. - - You can set this attribute to enable or disable the screen saver. - """ + """Get or set if the operating system is allowed to display a screen saver.""" return bool(lib.SDL_IsScreenSaverEnabled(self.p)) @allow_screen_saver.setter @@ -82,11 +123,10 @@ def allow_screen_saver(self, value: bool) -> None: @property def position(self) -> Tuple[int, int]: - """Return the (x, y) position of the window. + """Get or set the (x, y) position of the window. This attribute can be set the move the window. - The constants tcod.lib.SDL_WINDOWPOS_CENTERED or - tcod.lib.SDL_WINDOWPOS_UNDEFINED can be used. + The constants tcod.lib.SDL_WINDOWPOS_CENTERED or tcod.lib.SDL_WINDOWPOS_UNDEFINED may be used. """ xy = ffi.new("int[2]") lib.SDL_GetWindowPosition(self.p, xy, xy + 1) @@ -99,11 +139,10 @@ def position(self, xy: Tuple[int, int]) -> None: @property def size(self) -> Tuple[int, int]: - """Return the pixel (width, height) of the window. + """Get or set the pixel (width, height) of the window client area. - This attribute can be set to change the size of the window but the - given size must be greater than (1, 1) or else an exception will be - raised. + This attribute can be set to change the size of the window but the given size must be greater than (1, 1) or + else ValueError will be raised. """ xy = ffi.new("int[2]") lib.SDL_GetWindowSize(self.p, xy, xy + 1) @@ -112,19 +151,146 @@ def size(self) -> Tuple[int, int]: @size.setter def size(self, xy: Tuple[int, int]) -> None: if any(i <= 0 for i in xy): - raise ValueError("Window size must be greater than zero, not %r" % (xy,)) + raise ValueError(f"Window size must be greater than zero, not {xy}") x, y = xy lib.SDL_SetWindowSize(self.p, x, y) + @property + def min_size(self) -> Tuple[int, int]: + """Get or set this windows minimum client area.""" + xy = ffi.new("int[2]") + lib.SDL_GetWindowMinimumSize(self.p, xy, xy + 1) + return xy[0], xy[1] + + @min_size.setter + def min_size(self, xy: Tuple[int, int]) -> None: + lib.SDL_SetWindowMinimumSize(self.p, xy[0], xy[1]) + + @property + def max_size(self) -> Tuple[int, int]: + """Get or set this windows maximum client area.""" + xy = ffi.new("int[2]") + lib.SDL_GetWindowMaximumSize(self.p, xy, xy + 1) + return xy[0], xy[1] + + @max_size.setter + def max_size(self, xy: Tuple[int, int]) -> None: + lib.SDL_SetWindowMaximumSize(self.p, xy[0], xy[1]) + @property def title(self) -> str: - """The title of the window. You may set this attribute to change it.""" + """Get or set the title of the window.""" return str(ffi.string(lib.SDL_GetWindowtitle(self.p)), encoding="utf-8") @title.setter def title(self, value: str) -> None: lib.SDL_SetWindowtitle(self.p, value.encode("utf-8")) + @property + def flags(self) -> WindowFlags: + """The current flags of this window, read-only.""" + return WindowFlags(lib.SDL_GetWindowFlags(self.p)) + + @property + def fullscreen(self) -> int: + """Get or set the fullscreen status of this window. + + Can be set to :any:`WindowFlags.FULLSCREEN` or :any:`WindowFlags.FULLSCREEN_DESKTOP` flags + + Example:: + + # Toggle fullscreen. + window: tcod.sdl.video.Window + if window.fullscreen: + window.fullscreen = False # Set windowed mode. + else: + window.fullscreen = tcod.sdl.video.WindowFlags.FULLSCREEN_DESKTOP + """ + return self.flags & (WindowFlags.FULLSCREEN | WindowFlags.FULLSCREEN_DESKTOP) + + @fullscreen.setter + def fullscreen(self, value: int) -> None: + _check(lib.SDL_SetWindowFullscreen(self.p, value)) + + @property + def resizable(self) -> bool: + """Get or set if this window can be resized.""" + return bool(self.flags & WindowFlags.RESIZABLE) + + @resizable.setter + def resizable(self, value: bool) -> None: + lib.SDL_SetWindowResizable(self.p, value) + + @property + def border_size(self) -> Tuple[int, int, int, int]: + """Get the (top, left, bottom, right) size of the window decorations around the client area. + + If this fails or the window doesn't have decorations yet then the value will be (0, 0, 0, 0). + + .. seealso:: + https://wiki.libsdl.org/SDL_GetWindowBordersSize + """ + borders = ffi.new("int[4]") + # The return code is ignored. + _ = lib.SDL_GetWindowBordersSize(self.p, borders, borders + 1, borders + 2, borders + 3) + return borders[0], borders[1], borders[2], borders[3] + + @property + def opacity(self) -> float: + """Get or set this windows opacity. 0.0 is fully transarpent and 1.0 is fully opaque. + + Will error if you try to set this and opacity isn't supported. + """ + out = ffi.new("float*") + _check(lib.SDL_GetWindowOpacity(self.p, out)) + return float(out[0]) + + @opacity.setter + def opacity(self, value: float) -> None: + _check(lib.SDL_SetWindowOpacity(self.p, value)) + + @property + def grab(self) -> bool: + """Get or set this windows input grab mode. + + .. seealso:: + https://wiki.libsdl.org/SDL_SetWindowGrab + """ + return bool(lib.SDL_GetWindowGrab(self.p)) + + @grab.setter + def grab(self, value: bool) -> None: + lib.SDL_SetWindowGrab(self.p, value) + + @_required_version((2, 0, 16)) + def flash(self, operation: FlashOperation = FlashOperation.UNTIL_FOCUSED) -> None: + """Get the users attention.""" + _check(lib.SDL_FlashWindow(self.p, operation)) + + def raise_window(self) -> None: + """Raise the window and set input focus.""" + lib.SDL_RaiseWindow(self.p) + + def restore(self) -> None: + """Restore a minimized or maximized window to its original size and position.""" + lib.SDL_RestoreWindow(self.p) + + def maximize(self) -> None: + """Make the window as big as possible.""" + lib.SDL_MaximizeWindow(self.p) + + def minimize(self) -> None: + """Minimize the window to an iconic state.""" + lib.SDL_MinimizeWindow(self.p) + + def show(self) -> None: + """Show this window.""" + lib.SDL_ShowWindow(self.p) + + def hide(self) -> None: + """Hide this window.""" + lib.SDL_HideWindow(self.p) + def new_window( width: int, @@ -153,6 +319,12 @@ def new_window( return Window(_check_p(window_p)) +def get_grabbed_window() -> Optional[Window]: + """Return the window which has input grab enabled, if any.""" + sdl_window_p = lib.SDL_GetGrabbedWindow() + return Window(sdl_window_p) if sdl_window_p else None + + def _get_active_window() -> Window: """Return the SDL2 window current managed by libtcod. From 1d0eff457b403141335b9d1d482e18509e040f23 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 1 Feb 2022 17:48:40 -0800 Subject: [PATCH 031/194] Prefer compiling with SDL 2.0.20. --- build_sdl.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/build_sdl.py b/build_sdl.py index 9a26132e..0c374132 100644 --- a/build_sdl.py +++ b/build_sdl.py @@ -17,10 +17,11 @@ BITSIZE, LINKAGE = platform.architecture() +SDL_MIN_VERSION = (2, 0, 10) # The SDL2 version to parse and export symbols from. -SDL2_PARSE_VERSION = os.environ.get("SDL_VERSION", "2.0.5") +SDL2_PARSE_VERSION = os.environ.get("SDL_VERSION", "2.0.20") # The SDL2 version to include in binary distributions. -SDL2_BUNDLE_VERSION = os.environ.get("SDL_VERSION", "2.0.14") +SDL2_BUNDLE_VERSION = os.environ.get("SDL_VERSION", "2.0.20") # Used to remove excessive newlines in debug outputs. @@ -52,6 +53,7 @@ "SDL_DEPRECATED", "SDL_INLINE", "SDL_FORCE_INLINE", + "SDL_FALLTHROUGH", # Might show up in parsing and not in source. "SDL_ANDROID_EXTERNAL_STORAGE_READ", "SDL_ANDROID_EXTERNAL_STORAGE_WRITE", @@ -68,8 +70,7 @@ def check_sdl_version() -> None: """Check the local SDL version on Linux distributions.""" if not sys.platform.startswith("linux"): return - needed_version = SDL2_PARSE_VERSION - SDL_VERSION_NEEDED = tuple(int(n) for n in needed_version.split(".")) + needed_version = f"{SDL_MIN_VERSION[0]}.{SDL_MIN_VERSION[1]}.{SDL_MIN_VERSION[2]}" try: sdl_version_str = subprocess.check_output(["sdl2-config", "--version"], universal_newlines=True).strip() except FileNotFoundError: @@ -80,7 +81,7 @@ def check_sdl_version() -> None: ) print(f"Found SDL {sdl_version_str}.") sdl_version = tuple(int(s) for s in sdl_version_str.split(".")) - if sdl_version < SDL_VERSION_NEEDED: + if sdl_version < SDL_MIN_VERSION: raise RuntimeError("SDL version must be at least %s, (found %s)" % (needed_version, sdl_version_str)) From 5602fc4435c50a043c5e2b29ba434f95fa1bb84f Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 1 Feb 2022 18:52:04 -0800 Subject: [PATCH 032/194] Fix missing CPython builds on Linux. Update build packages. Split matrix jobs. --- .github/workflows/python-package.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 41d4c886..caf0f836 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -189,6 +189,7 @@ jobs: strategy: matrix: arch: ["x86_64", "aarch64"] + build: ["cp37-manylinux*", "pp37-manylinux*"] steps: - uses: actions/checkout@v1 - name: Set up QEMU @@ -204,12 +205,12 @@ jobs: - name: Install Python dependencies run: | python -m pip install --upgrade pip - pip install twine cibuildwheel==2.0.0 + pip install twine cibuildwheel==2.3.1 - name: Build wheels run: | python -m cibuildwheel --output-dir wheelhouse env: - CIBW_BUILD: cp36-* pp* + CIBW_BUILD: ${{ matrix.build }} CIBW_ARCHS_LINUX: ${{ matrix.arch }} CIBW_MANYLINUX_*_IMAGE: manylinux2014 CIBW_MANYLINUX_PYPY_X86_64_IMAGE: manylinux2014 From 20a190f443a6625ba8fa0b2b3ca16606cfb63fca Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 2 Feb 2022 10:06:49 -0800 Subject: [PATCH 033/194] Note SDL requirements have changed. --- README.rst | 2 +- build_sdl.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 823e3ac0..91444371 100755 --- a/README.rst +++ b/README.rst @@ -48,7 +48,7 @@ For the most part it's just:: ============== * Python 3.7+ * Windows, Linux, or MacOS X 10.9+. -* On Linux, requires libsdl2 (2.0.5+). +* On Linux, requires libsdl2 (2.0.10+). =========== Changelog diff --git a/build_sdl.py b/build_sdl.py index 0c374132..aadbff5c 100644 --- a/build_sdl.py +++ b/build_sdl.py @@ -17,6 +17,7 @@ BITSIZE, LINKAGE = platform.architecture() +# Reject versions of SDL older than this, update the requirements in the readme if you change this. SDL_MIN_VERSION = (2, 0, 10) # The SDL2 version to parse and export symbols from. SDL2_PARSE_VERSION = os.environ.get("SDL_VERSION", "2.0.20") From 858b630cd2773af8c81f2db5990a019c406b4661 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 2 Feb 2022 17:22:11 -0800 Subject: [PATCH 034/194] Add tests for SDL. --- tcod/sdl/render.py | 62 +++++++++++++++++++++++++++++++------- tcod/sdl/video.py | 61 +++++++++++++++++++------------------- tests/test_sdl.py | 74 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 155 insertions(+), 42 deletions(-) create mode 100644 tests/test_sdl.py diff --git a/tcod/sdl/render.py b/tcod/sdl/render.py index 36a56c52..228e0133 100644 --- a/tcod/sdl/render.py +++ b/tcod/sdl/render.py @@ -1,5 +1,6 @@ from __future__ import annotations +import enum from typing import Any, Optional, Tuple import numpy as np @@ -10,6 +11,17 @@ from tcod.sdl import _check, _check_p +class TextureAccess(enum.IntEnum): + """Determines how a texture is expected to be used.""" + + STATIC = lib.SDL_TEXTUREACCESS_STATIC or 0 + """Texture rarely changes.""" + STREAMING = lib.SDL_TEXTUREACCESS_STREAMING or 0 + """Texture frequently changes.""" + TARGET = lib.SDL_TEXTUREACCESS_TARGET or 0 + """Texture will be used as a render target.""" + + class Texture: def __init__(self, sdl_texture_p: Any, sdl_renderer_p: Any = None) -> None: self.p = sdl_texture_p @@ -56,7 +68,9 @@ def height(self) -> int: @property def alpha_mod(self) -> int: """Texture alpha modulate value, can be set to: 0 - 255.""" - return int(lib.SDL_GetTextureAlphaMod(self.p)) + out = ffi.new("uint8_t*") + _check(lib.SDL_GetTextureAlphaMod(self.p, out)) + return int(out[0]) @alpha_mod.setter def alpha_mod(self, value: int) -> None: @@ -65,7 +79,9 @@ def alpha_mod(self, value: int) -> None: @property def blend_mode(self) -> int: """Texture blend mode, can be set.""" - return int(lib.SDL_GetTextureBlendMode(self.p)) + out = ffi.new("SDL_BlendMode*") + _check(lib.SDL_GetTextureBlendMode(self.p, out)) + return int(out[0]) @blend_mode.setter def blend_mode(self, value: int) -> None: @@ -101,6 +117,8 @@ class Renderer: def __init__(self, sdl_renderer_p: Any) -> None: if ffi.typeof(sdl_renderer_p) is not ffi.typeof("struct SDL_Renderer*"): raise TypeError(f"Expected a {ffi.typeof('struct SDL_Window*')} type (was {ffi.typeof(sdl_renderer_p)}).") + if not sdl_renderer_p: + raise TypeError("C pointer must not be null.") self.p = sdl_renderer_p def __eq__(self, other: Any) -> bool: @@ -124,10 +142,24 @@ def present(self) -> None: """Present the currently rendered image to the screen.""" lib.SDL_RenderPresent(self.p) + def set_render_target(self, texture: Texture) -> _RestoreTargetContext: + """Change the render target to `texture`, returns a context that will restore the original target when exited.""" + restore = _RestoreTargetContext(self) + _check(lib.SDL_SetRenderTarget(self.p, texture.p)) + return restore + def new_texture( self, width: int, height: int, *, format: Optional[int] = None, access: Optional[int] = None ) -> Texture: - """Allocate and return a new Texture for this renderer.""" + """Allocate and return a new Texture for this renderer. + + Args: + width: The pixel width of the new texture. + height: The pixel height of the new texture. + format: The format the new texture. + access: The access mode of the texture. Defaults to :any:`TextureAccess.STATIC`. + See :any:`TextureAccess` for more options. + """ if format is None: format = 0 if access is None: @@ -135,23 +167,24 @@ def new_texture( texture_p = ffi.gc(lib.SDL_CreateTexture(self.p, format, access, width, height), lib.SDL_DestroyTexture) return Texture(texture_p, self.p) - def set_render_target(self, texture: Texture) -> _RestoreTargetContext: - """Change the render target to `texture`, returns a context that will restore the original target when exited.""" - restore = _RestoreTargetContext(self) - _check(lib.SDL_SetRenderTarget(self.p, texture.p)) - return restore - def upload_texture( self, pixels: NDArray[Any], *, format: Optional[int] = None, access: Optional[int] = None ) -> Texture: - """Return a new Texture from an array of pixels.""" + """Return a new Texture from an array of pixels. + + Args: + pixels: An RGB or RGBA array of pixels in row-major order. + format: The format of `pixels` when it isn't a simple RGB or RGBA array. + access: The access mode of the texture. Defaults to :any:`TextureAccess.STATIC`. + See :any:`TextureAccess` for more options. + """ if format is None: assert len(pixels.shape) == 3 assert pixels.dtype == np.uint8 if pixels.shape[2] == 4: format = int(lib.SDL_PIXELFORMAT_RGBA32) elif pixels.shape[2] == 3: - format = int(lib.SDL_PIXELFORMAT_RGB32) + format = int(lib.SDL_PIXELFORMAT_RGB24) else: raise TypeError(f"Can't determine the format required for an array of shape {pixels.shape}.") @@ -174,6 +207,13 @@ def new_renderer( ) -> Renderer: """Initialize and return a new SDL Renderer. + Args: + window: The window that this renderer will be attached to. + driver: Force SDL to use a specific video driver. + software: If True then a software renderer will be forced. By default a hardware renderer is used. + vsync: If True then Vsync will be enabled. + target_textures: If True then target textures can be used by the renderer. + Example:: # Start by creating a window. diff --git a/tcod/sdl/video.py b/tcod/sdl/video.py index c77d91ad..e12dcc57 100644 --- a/tcod/sdl/video.py +++ b/tcod/sdl/video.py @@ -21,6 +21,7 @@ "Window", "new_window", "get_grabbed_window", + "screen_saver_allowed", ) @@ -66,10 +67,10 @@ class _TempSurface: def __init__(self, pixels: ArrayLike) -> None: self._array: NDArray[np.uint8] = np.ascontiguousarray(pixels, dtype=np.uint8) - if len(self._array) != 3: - raise TypeError("NumPy shape must be 3D [y, x, ch] (got %r)" % (self._array.shape,)) - if 3 <= self._array.shape[2] <= 4: - raise TypeError("NumPy array must have RGB or RGBA channels. (got %r)" % (self._array.shape,)) + if len(self._array.shape) != 3: + raise TypeError(f"NumPy shape must be 3D [y, x, ch] (got {self._array.shape})") + if not (3 <= self._array.shape[2] <= 4): + raise TypeError(f"NumPy array must have RGB or RGBA channels. (got {self._array.shape})") self.p = ffi.gc( lib.SDL_CreateRGBSurfaceFrom( ffi.from_buffer("void*", self._array), @@ -95,32 +96,21 @@ def __init__(self, sdl_window_p: Any) -> None: "sdl_window_p must be %r type (was %r)." % (ffi.typeof("struct SDL_Window*"), ffi.typeof(sdl_window_p)) ) if not sdl_window_p: - raise ValueError("sdl_window_p can not be a null pointer.") + raise TypeError("sdl_window_p can not be a null pointer.") self.p = sdl_window_p def __eq__(self, other: Any) -> bool: return bool(self.p == other.p) - def set_icon(self, image: ArrayLike) -> None: + def set_icon(self, pixels: ArrayLike) -> None: """Set the window icon from an image. - `image` is a C memory order RGB or RGBA NumPy array. + Args: + pixels: A row-major array of RGB or RGBA pixel values. """ - surface = _TempSurface(image) + surface = _TempSurface(pixels) lib.SDL_SetWindowIcon(self.p, surface.p) - @property - def allow_screen_saver(self) -> bool: - """Get or set if the operating system is allowed to display a screen saver.""" - return bool(lib.SDL_IsScreenSaverEnabled(self.p)) - - @allow_screen_saver.setter - def allow_screen_saver(self, value: bool) -> None: - if value: - lib.SDL_EnableScreenSaver(self.p) - else: - lib.SDL_DisableScreenSaver(self.p) - @property def position(self) -> Tuple[int, int]: """Get or set the (x, y) position of the window. @@ -180,11 +170,11 @@ def max_size(self, xy: Tuple[int, int]) -> None: @property def title(self) -> str: """Get or set the title of the window.""" - return str(ffi.string(lib.SDL_GetWindowtitle(self.p)), encoding="utf-8") + return str(ffi.string(lib.SDL_GetWindowTitle(self.p)), encoding="utf-8") @title.setter def title(self, value: str) -> None: - lib.SDL_SetWindowtitle(self.p, value.encode("utf-8")) + lib.SDL_SetWindowTitle(self.p, value.encode("utf-8")) @property def flags(self) -> WindowFlags: @@ -303,6 +293,15 @@ def new_window( ) -> Window: """Initialize and return a new SDL Window. + Args: + width: The requested pixel width of the window. + height: The requested pixel height of the window. + x: The left-most position of the window. + y: The top-most position of the window. + title: The title text of the new window. If no option is given then `sys.arg[0]` will be used as the title. + flags: The SDL flags to use for this window, such as `tcod.sdl.video.WindowFlags.RESIZABLE`. + See :any:`WindowFlags` for more options. + Example:: # Create a new resizable window with a custom title. @@ -325,12 +324,12 @@ def get_grabbed_window() -> Optional[Window]: return Window(sdl_window_p) if sdl_window_p else None -def _get_active_window() -> Window: - """Return the SDL2 window current managed by libtcod. - - Will raise an error if libtcod does not currently have a window. - """ - sdl_window = lib.TCOD_sys_get_window() - if not sdl_window: - raise RuntimeError("TCOD does not have an active window.") - return Window(sdl_window) +def screen_saver_allowed(allow: Optional[bool] = None) -> bool: + """Allow or prevent a screen saver from being displayed and return the current allowed status.""" + if allow is None: + pass + elif allow: + lib.SDL_EnableScreenSaver() + else: + lib.SDL_DisableScreenSaver() + return bool(lib.SDL_IsScreenSaverEnabled()) diff --git a/tests/test_sdl.py b/tests/test_sdl.py new file mode 100644 index 00000000..348ebe60 --- /dev/null +++ b/tests/test_sdl.py @@ -0,0 +1,74 @@ +import sys + +import numpy as np +import pytest + +import tcod.sdl.render +import tcod.sdl.video + + +def test_sdl_window() -> None: + assert tcod.sdl.video.get_grabbed_window() is None + window = tcod.sdl.video.new_window(1, 1) + window.raise_window() + window.maximize() + window.restore() + window.minimize() + window.hide() + window.show() + assert window.title == sys.argv[0] + window.title = "Title" + assert window.title == "Title" + assert window.opacity == 1.0 + window.position = window.position + window.fullscreen = window.fullscreen + window.resizable = window.resizable + window.size = window.size + window.min_size = window.min_size + window.max_size = window.max_size + window.border_size + window.set_icon(np.zeros((32, 32, 3), dtype=np.uint8)) + with pytest.raises(TypeError): + window.set_icon(np.zeros((32, 32, 5), dtype=np.uint8)) + with pytest.raises(TypeError): + window.set_icon(np.zeros((32, 32), dtype=np.uint8)) + window.opacity = window.opacity + window.grab = window.grab + + +def test_sdl_window_bad_types() -> None: + with pytest.raises(TypeError): + tcod.sdl.video.Window(tcod.ffi.cast("SDL_Window*", tcod.ffi.NULL)) + with pytest.raises(TypeError): + tcod.sdl.video.Window(tcod.ffi.new("SDL_Rect*")) + + +def test_sdl_screen_saver() -> None: + assert tcod.sdl.video.screen_saver_allowed(False) is False + assert tcod.sdl.video.screen_saver_allowed(True) is True + assert tcod.sdl.video.screen_saver_allowed() is True + + +def test_sdl_render() -> None: + window = tcod.sdl.video.new_window(1, 1) + render = tcod.sdl.render.new_renderer(window, software=True, vsync=False, target_textures=True) + render.present() + rgb = render.upload_texture(np.zeros((8, 8, 3), np.uint8)) + assert (rgb.width, rgb.height) == (8, 8) + assert rgb.access == tcod.sdl.render.TextureAccess.STATIC + assert rgb.format == tcod.lib.SDL_PIXELFORMAT_RGB24 + rgb.alpha_mod = rgb.alpha_mod + rgb.blend_mode = rgb.blend_mode + rgb.rgb_mod = rgb.rgb_mod + rgba = render.upload_texture(np.zeros((8, 8, 4), np.uint8), access=tcod.sdl.render.TextureAccess.TARGET) + with render.set_render_target(rgba): + render.copy(rgb) + with pytest.raises(TypeError): + render.upload_texture(np.zeros((8, 8, 5), np.uint8)) + + +def test_sdl_render_bad_types() -> None: + with pytest.raises(TypeError): + tcod.sdl.render.Renderer(tcod.ffi.cast("SDL_Renderer*", tcod.ffi.NULL)) + with pytest.raises(TypeError): + tcod.sdl.render.Renderer(tcod.ffi.new("SDL_Rect*")) From ba2de759716184f7ee8f69c68cfdcfc45c84df7c Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 2 Feb 2022 23:10:59 -0800 Subject: [PATCH 035/194] Add some SDL modules to the documentation. --- docs/index.rst | 3 +++ docs/sdl/render.rst | 5 +++++ docs/sdl/video.rst | 5 +++++ docs/tcod/render.rst | 5 +++++ tcod/render.py | 8 +++++++- tcod/sdl/render.py | 8 ++++++++ tcod/sdl/video.py | 37 ++++++++++++++++++++++++++++++++----- 7 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 docs/sdl/render.rst create mode 100644 docs/sdl/video.rst create mode 100644 docs/tcod/render.rst diff --git a/docs/index.rst b/docs/index.rst index 1ae35b97..7ddc5583 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -36,8 +36,11 @@ Contents: tcod/noise tcod/path tcod/random + tcod/render tcod/tileset libtcodpy + sdl/render + sdl/video Indices and tables ================== diff --git a/docs/sdl/render.rst b/docs/sdl/render.rst new file mode 100644 index 00000000..0dc462cd --- /dev/null +++ b/docs/sdl/render.rst @@ -0,0 +1,5 @@ +tcod.sdl.render - SDL Rendering +=============================== + +.. automodule:: tcod.sdl.render + :members: diff --git a/docs/sdl/video.rst b/docs/sdl/video.rst new file mode 100644 index 00000000..56d43408 --- /dev/null +++ b/docs/sdl/video.rst @@ -0,0 +1,5 @@ +tcod.sdl.video - SDL Window and Display API +=========================================== + +.. automodule:: tcod.sdl.video + :members: diff --git a/docs/tcod/render.rst b/docs/tcod/render.rst new file mode 100644 index 00000000..605d52f2 --- /dev/null +++ b/docs/tcod/render.rst @@ -0,0 +1,5 @@ +tcod.render - Console Rendering Extension +========================================= + +.. automodule:: tcod.render + :members: diff --git a/tcod/render.py b/tcod/render.py index f1bc87ce..9e2918da 100644 --- a/tcod/render.py +++ b/tcod/render.py @@ -1,4 +1,8 @@ -"""Handle the rendering of libtcod's tilesets. +"""Handles the rendering of libtcod's tilesets. + +Using this module you can render a console to an SDL :any:`Texture` directly, letting you have full control over how +conoles are displayed. +This includes rendering multiple tilesets in a single frame and rendering consoles on top of each other. Example:: @@ -19,6 +23,8 @@ for event in tcod.event.wait(): if isinstance(event, tcod.event.Quit): raise SystemExit() + +.. versionadded:: 13.4 """ from __future__ import annotations diff --git a/tcod/sdl/render.py b/tcod/sdl/render.py index 228e0133..48d810fa 100644 --- a/tcod/sdl/render.py +++ b/tcod/sdl/render.py @@ -1,3 +1,7 @@ +"""SDL2 Rendering functionality. + +.. versionadded:: 13.4 +""" from __future__ import annotations import enum @@ -23,6 +27,8 @@ class TextureAccess(enum.IntEnum): class Texture: + """SDL hardware textures.""" + def __init__(self, sdl_texture_p: Any, sdl_renderer_p: Any = None) -> None: self.p = sdl_texture_p self._sdl_renderer_p = sdl_renderer_p # Keep alive. @@ -114,6 +120,8 @@ def __exit__(self, *_: Any) -> None: class Renderer: + """SDL Renderer.""" + def __init__(self, sdl_renderer_p: Any) -> None: if ffi.typeof(sdl_renderer_p) is not ffi.typeof("struct SDL_Renderer*"): raise TypeError(f"Expected a {ffi.typeof('struct SDL_Window*')} type (was {ffi.typeof(sdl_renderer_p)}).") diff --git a/tcod/sdl/video.py b/tcod/sdl/video.py index e12dcc57..4d1d3b7e 100644 --- a/tcod/sdl/video.py +++ b/tcod/sdl/video.py @@ -1,7 +1,6 @@ -"""SDL2 specific functionality. +"""SDL2 Window and Display handling. -Add the line ``import tcod.sdl`` to include this module, as importing this -module is not implied by ``import tcod``. +.. versionadded:: 13.4 """ from __future__ import annotations @@ -33,33 +32,60 @@ class WindowFlags(enum.IntFlag): """ FULLSCREEN = lib.SDL_WINDOW_FULLSCREEN or 0 + """""" FULLSCREEN_DESKTOP = lib.SDL_WINDOW_FULLSCREEN_DESKTOP or 0 + """""" OPENGL = lib.SDL_WINDOW_OPENGL or 0 + """""" SHOWN = lib.SDL_WINDOW_SHOWN or 0 + """""" HIDDEN = lib.SDL_WINDOW_HIDDEN or 0 + """""" BORDERLESS = lib.SDL_WINDOW_BORDERLESS or 0 + """""" RESIZABLE = lib.SDL_WINDOW_RESIZABLE or 0 + """""" MINIMIZED = lib.SDL_WINDOW_MINIMIZED or 0 + """""" MAXIMIZED = lib.SDL_WINDOW_MAXIMIZED or 0 + """""" MOUSE_GRABBED = lib.SDL_WINDOW_INPUT_GRABBED or 0 + """""" INPUT_FOCUS = lib.SDL_WINDOW_INPUT_FOCUS or 0 + """""" MOUSE_FOCUS = lib.SDL_WINDOW_MOUSE_FOCUS or 0 + """""" FOREIGN = lib.SDL_WINDOW_FOREIGN or 0 + """""" ALLOW_HIGHDPI = lib.SDL_WINDOW_ALLOW_HIGHDPI or 0 + """""" MOUSE_CAPTURE = lib.SDL_WINDOW_MOUSE_CAPTURE or 0 + """""" ALWAYS_ON_TOP = lib.SDL_WINDOW_ALWAYS_ON_TOP or 0 + """""" SKIP_TASKBAR = lib.SDL_WINDOW_SKIP_TASKBAR or 0 + """""" UTILITY = lib.SDL_WINDOW_UTILITY or 0 + """""" TOOLTIP = lib.SDL_WINDOW_TOOLTIP or 0 + """""" POPUP_MENU = lib.SDL_WINDOW_POPUP_MENU or 0 + """""" VULKAN = lib.SDL_WINDOW_VULKAN or 0 + """""" METAL = getattr(lib, "SDL_WINDOW_METAL", None) or 0x20000000 # SDL >= 2.0.14 + """""" class FlashOperation(enum.IntEnum): + """Values for :any:`Window.flash`.""" + CANCEL = 0 + """Stop flashing.""" BRIEFLY = 1 + """Flash breifly.""" UNTIL_FOCUSED = 2 + """Flash until focus is gained.""" class _TempSurface: @@ -185,7 +211,7 @@ def flags(self) -> WindowFlags: def fullscreen(self) -> int: """Get or set the fullscreen status of this window. - Can be set to :any:`WindowFlags.FULLSCREEN` or :any:`WindowFlags.FULLSCREEN_DESKTOP` flags + Can be set to the :any:`WindowFlags.FULLSCREEN` or :any:`WindowFlags.FULLSCREEN_DESKTOP` flags. Example:: @@ -304,8 +330,9 @@ def new_window( Example:: + import tcod.sdl.video # Create a new resizable window with a custom title. - window = tcod.sdl.video.new_window(640, 480, title="Title bar text", flags=tcod.lib.SDL_WINDOW_RESIZABLE) + window = tcod.sdl.video.new_window(640, 480, title="Title bar text", flags=tcod.sdl.video.WindowFlags.RESIZABLE) .. seealso:: :func:`tcod.sdl.render.new_renderer` From 8a678fd1fea56eff3521f1f6f3330269920fdf4b Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 3 Feb 2022 18:50:57 -0800 Subject: [PATCH 036/194] Add event watch functions. --- build_sdl.py | 3 +++ tcod/cdef.h | 2 -- tcod/event.py | 68 ++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/build_sdl.py b/build_sdl.py index aadbff5c..19a4d4f2 100644 --- a/build_sdl.py +++ b/build_sdl.py @@ -207,7 +207,10 @@ def on_directive_handle( extern "Python" { // SDL_AudioCallback callback. void _sdl_audio_callback(void* userdata, Uint8* stream, int len); +// SDL to Python log function. void _sdl_log_output_function(void *userdata, int category, SDL_LogPriority priority, const char *message); +// Generic event watcher callback. +int _sdl_event_watcher(void* userdata, SDL_Event* event); } """ diff --git a/tcod/cdef.h b/tcod/cdef.h index 70ed74f9..03db6cbc 100644 --- a/tcod/cdef.h +++ b/tcod/cdef.h @@ -16,7 +16,5 @@ float _pycall_path_dest_only(int x1, int y1, int x2, int y2, void* user_data); void _pycall_sdl_hook(struct SDL_Surface*); -int _pycall_event_watch(void* userdata, union SDL_Event* event); - void _pycall_cli_output(void* userdata, const char* output); } diff --git a/tcod/event.py b/tcod/event.py index 3cfc878b..73cc7409 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -23,7 +23,7 @@ import enum import warnings -from typing import Any, Callable, Dict, Generic, Iterator, Mapping, NamedTuple, Optional, Tuple, TypeVar, Union +from typing import Any, Callable, Dict, Generic, Iterator, Mapping, NamedTuple, Optional, Tuple, Type, TypeVar, Union import numpy as np from numpy.typing import NDArray @@ -238,7 +238,7 @@ def __init__(self, type: Optional[str] = None): self.sdl_event = None @classmethod - def from_sdl_event(cls, sdl_event: Any) -> Any: + def from_sdl_event(cls, sdl_event: Any) -> Event: """Return a class instance from a python-cffi 'SDL_Event*' pointer.""" raise NotImplementedError() @@ -739,7 +739,7 @@ def __str__(self) -> str: return "" -_SDL_TO_CLASS_TABLE: Dict[int, Any] = { +_SDL_TO_CLASS_TABLE: Dict[int, Type[Event]] = { lib.SDL_QUIT: Quit, lib.SDL_KEYDOWN: KeyDown, lib.SDL_KEYUP: KeyUp, @@ -752,6 +752,13 @@ def __str__(self) -> str: } +def _parse_event(sdl_event: Any) -> Event: + """Convert a C SDL_Event* type into a tcod Event sub-class.""" + if sdl_event.type not in _SDL_TO_CLASS_TABLE: + return Undefined.from_sdl_event(sdl_event) + return _SDL_TO_CLASS_TABLE[sdl_event.type].from_sdl_event(sdl_event) + + def get() -> Iterator[Any]: """Return an iterator for all pending events. @@ -1065,10 +1072,63 @@ def get_mouse_state() -> MouseState: @ffi.def_extern() # type: ignore -def _pycall_event_watch(userdata: Any, sdl_event: Any) -> int: +def _sdl_event_watcher(userdata: Any, sdl_event: Any) -> int: + callback: Callable[[Event], None] = ffi.from_handle(userdata) + callback(_parse_event(sdl_event)) return 0 +_EventCallback = TypeVar("_EventCallback", bound=Callable[[Event], None]) +_event_watch_handles: Dict[Callable[[Event], None], Any] = {} # Callbacks and their FFI handles. + + +def add_watch(callback: _EventCallback) -> _EventCallback: + """Add a callback for watching events. + + This function can be called with the callback to register, or be used as a decorator. + + Callbacks added as event watchers can later be removed with :any:`tcod.event.remove_watch`. + + Args: + callback (Callable[[Event], None]): + A function which accepts :any:`Event` parameters. + + Example:: + + import tcod.event + + @tcod.event.add_watch + def handle_events(event: tcod.event.Event) -> None: + if isinstance(event, tcod.event.KeyDown): + print(event) + + .. versionadded:: 13.4 + """ + if callback in _event_watch_handles: + warnings.warn(f"{callback} is already an active event watcher, nothing was added.", RuntimeWarning) + return callback + handle = _event_watch_handles[callback] = ffi.new_handle(callback) + lib.SDL_AddEventWatch(lib._sdl_event_watcher, handle) + return callback + + +def remove_watch(callback: Callable[[Event], None]) -> None: + """Remove a callback as an event wacher. + + Args: + callback (Callable[[Event], None]): + A function which has been previously registered with :any:`tcod.event.add_watch`. + + .. versionadded:: 13.4 + """ + if callback not in _event_watch_handles: + warnings.warn(f"{callback} is not an active event watcher, nothing was removed.", RuntimeWarning) + return + handle = _event_watch_handles[callback] + lib.SDL_DelEventWatch(lib._sdl_event_watcher, handle) + del _event_watch_handles[callback] + + def get_keyboard_state() -> NDArray[np.bool_]: """Return a boolean array with the current keyboard state. From 304eedb9cd5c20ffbb25d3a70827f8a31acac8df Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 4 Feb 2022 01:03:57 -0800 Subject: [PATCH 037/194] Clean up and refactor SDL audio module. --- tcod/sdl/audio.py | 135 +++++++++++++++++++++++++++------------------- 1 file changed, 81 insertions(+), 54 deletions(-) diff --git a/tcod/sdl/audio.py b/tcod/sdl/audio.py index ac5cd0d1..8e0e3c70 100644 --- a/tcod/sdl/audio.py +++ b/tcod/sdl/audio.py @@ -3,7 +3,6 @@ import sys import threading import time -import weakref from typing import Any, Iterator, List, Optional import numpy as np @@ -27,7 +26,7 @@ def _get_format(format: DTypeLike) -> int: if byteorder == "=": byteorder = "<" if sys.byteorder == "little" else ">" - return ( # type: ignore + return int( bitsize | (lib.SDL_AUDIO_MASK_DATATYPE * is_float) | (lib.SDL_AUDIO_MASK_ENDIAN * (byteorder == ">")) @@ -51,57 +50,45 @@ def _dtype_from_format(format: int) -> np.dtype[Any]: class AudioDevice: + """An SDL audio device.""" + def __init__( self, - device: Optional[str] = None, - capture: bool = False, - *, - frequency: int = 44100, - format: DTypeLike = np.float32, - channels: int = 2, - samples: int = 0, - allowed_changes: int = 0, + device_id: int, + capture: bool, + spec: Any, # SDL_AudioSpec* ): - self.__sdl_subsystems = tcod.sdl.sys._ScopeInit(tcod.sdl.sys.Subsystem.AUDIO) - self.__handle = ffi.new_handle(weakref.ref(self)) - desired = ffi.new( - "SDL_AudioSpec*", - { - "freq": frequency, - "format": _get_format(format), - "channels": channels, - "samples": samples, - "callback": ffi.NULL, - "userdata": self.__handle, - }, - ) - obtained = ffi.new("SDL_AudioSpec*") - self.device_id = lib.SDL_OpenAudioDevice( - ffi.NULL if device is None else device.encode("utf-8"), - capture, - desired, - obtained, - allowed_changes, - ) - assert self.device_id != 0, _get_error() - self.frequency = obtained.freq + assert device_id >= 0 + assert ffi.typeof(spec) is ffi.typeof("SDL_AudioSpec*") + assert spec + self.device_id = device_id + self.spec = spec + self.frequency = spec.freq self.is_capture = capture - self.format = _dtype_from_format(obtained.format) - self.channels = int(obtained.channels) - self.silence = int(obtained.silence) - self.samples = int(obtained.samples) - self.buffer_size = int(obtained.size) - self.unpause() + self.format = _dtype_from_format(spec.format) + self.channels = int(spec.channels) + self.silence = int(spec.silence) + self.samples = int(spec.samples) + self.buffer_size = int(spec.size) + self._callback = self.__default_callback @property def _sample_size(self) -> int: return self.format.itemsize * self.channels - def pause(self) -> None: - lib.SDL_PauseAudioDevice(self.device_id, True) + @property + def stopped(self) -> bool: + """Is True if the device has failed or was closed.""" + return bool(lib.SDL_GetAudioDeviceStatus(self.device_id) != lib.SDL_AUDIO_STOPPED) + + @property + def paused(self) -> bool: + """Get or set the device paused state.""" + return bool(lib.SDL_GetAudioDeviceStatus(self.device_id) != lib.SDL_AUDIO_PLAYING) - def unpause(self) -> None: - lib.SDL_PauseAudioDevice(self.device_id, False) + @paused.setter + def paused(self, value: bool) -> None: + lib.SDL_PauseAudioDevice(self.device_id, value) def _verify_array_format(self, samples: NDArray[Any]) -> NDArray[Any]: if samples.dtype != self.format: @@ -121,12 +108,14 @@ def queued_audio_bytes(self) -> int: return int(lib.SDL_GetQueuedAudioSize(self.device_id)) def queue_audio(self, samples: ArrayLike) -> None: + """Append audio samples to the audio data queue.""" assert not self.is_capture samples = self._convert_array(samples) buffer = ffi.from_buffer(samples) lib.SDL_QueueAudio(self.device_id, buffer, len(buffer)) def dequeue_audio(self) -> NDArray[Any]: + """Return the audio buffer from a capture stream.""" assert self.is_capture out_samples = self.queued_audio_bytes // self._sample_size out = np.empty((out_samples, self.channels), self.format) @@ -140,31 +129,30 @@ def __del__(self) -> None: self.close() def close(self) -> None: + """Close this audio device.""" if not self.device_id: return lib.SDL_CloseAudioDevice(self.device_id) self.device_id = 0 - @staticmethod - def __default_callback(stream: NDArray[Any], silence: int) -> None: - stream[...] = silence + def __default_callback(self, stream: NDArray[Any]) -> None: + stream[...] = self.silence class Mixer(threading.Thread): def __init__(self, device: AudioDevice): super().__init__(daemon=True) self.device = device - self.device.unpause() - self.start() def run(self) -> None: buffer = np.full((self.device.samples, self.device.channels), self.device.silence, dtype=self.device.format) while True: - time.sleep(0.001) - if self.device.queued_audio_bytes == 0: - self.on_stream(buffer) - self.device.queue_audio(buffer) - buffer[:] = self.device.silence + if self.device.queued_audio_bytes > 0: + time.sleep(0.001) + continue + self.on_stream(buffer) + self.device.queue_audio(buffer) + buffer[:] = self.device.silence def on_stream(self, stream: NDArray[Any]) -> None: pass @@ -197,7 +185,8 @@ def _sdl_audio_callback(userdata: Any, stream: Any, length: int) -> None: """Handle audio device callbacks.""" device: Optional[AudioDevice] = ffi.from_handle(userdata)() assert device is not None - _ = np.frombuffer(ffi.buffer(stream, length), dtype=device.format).reshape(-1, device.channels) + buffer = np.frombuffer(ffi.buffer(stream, length), dtype=device.format).reshape(-1, device.channels) + device._callback(buffer) def _get_devices(capture: bool) -> Iterator[str]: @@ -216,3 +205,41 @@ def get_devices() -> Iterator[str]: def get_capture_devices() -> Iterator[str]: """Iterate over the available audio capture devices.""" yield from _get_devices(capture=True) + + +def open( + name: Optional[str] = None, + capture: bool = False, + *, + frequency: int = 44100, + format: DTypeLike = np.float32, + channels: int = 2, + samples: int = 0, + allowed_changes: int = 0, + paused: bool = False, +) -> AudioDevice: + """Open an audio device for playback or capture.""" + tcod.sdl.sys.init(tcod.sdl.sys.Subsystem.AUDIO) + desired = ffi.new( + "SDL_AudioSpec*", + { + "freq": frequency, + "format": _get_format(format), + "channels": channels, + "samples": samples, + "callback": ffi.NULL, + "userdata": ffi.NULL, + }, + ) + obtained = ffi.new("SDL_AudioSpec*") + device_id: int = lib.SDL_OpenAudioDevice( + ffi.NULL if name is None else name.encode("utf-8"), + capture, + desired, + obtained, + allowed_changes, + ) + assert device_id >= 0, _get_error() + device = AudioDevice(device_id, capture, obtained) + device.paused = paused + return device From 45936e5ec217beb8e086b67a1f87f3a3b3120611 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 4 Feb 2022 17:48:34 -0800 Subject: [PATCH 038/194] Note recent additions to the changelog. --- .vscode/settings.json | 1 + CHANGELOG.md | 6 ++++++ tcod/context.py | 4 ++-- tcod/event.py | 6 ++++++ 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 8385c134..87df363c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -292,6 +292,7 @@ "truetype", "undoc", "Unifont", + "unraisablehook", "upscaling", "VAFUNC", "vcoef", diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a2ed57f..a0c57acc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] +### Added +- Adds `sdl_window` and `sdl_renderer` to tcod contexts. +- Adds `tcod.event.add_watch` and `tcod.event.remove_watch` to handle SDL events via callback. +- Adds the `tcod.sdl.video` module to handle SDL windows. +- Adds the `tcod.sdl.render` module to handle SDL renderers. +- Adds the `tcod.render` module which gives more control over the rendering of consoles and tilesets. ### Fixed - Fixed handling of non-Path PathLike parameters and filepath encodings. diff --git a/tcod/context.py b/tcod/context.py index 3c8470b1..14c81f6f 100644 --- a/tcod/context.py +++ b/tcod/context.py @@ -355,7 +355,7 @@ def toggle_fullscreen(context: tcod.context.Context) -> None: @property def sdl_window(self) -> Optional[tcod.sdl.video.Window]: - """Return a tcod.sdl.video.Window referencing this contexts SDL window if it exists. + """Return a :any:`tcod.sdl.video.Window` referencing this contexts SDL window if it exists. .. versionadded:: 13.4 """ @@ -364,7 +364,7 @@ def sdl_window(self) -> Optional[tcod.sdl.video.Window]: @property def sdl_renderer(self) -> Optional[tcod.sdl.render.Renderer]: - """Return a tcod.sdl.render.Renderer referencing this contexts SDL renderer if it exists. + """Return a :any:`tcod.sdl.render.Renderer` referencing this contexts SDL renderer if it exists. .. versionadded:: 13.4 """ diff --git a/tcod/event.py b/tcod/event.py index 73cc7409..7eccabd9 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -1089,6 +1089,10 @@ def add_watch(callback: _EventCallback) -> _EventCallback: Callbacks added as event watchers can later be removed with :any:`tcod.event.remove_watch`. + .. warning:: + How uncaught exceptions in a callback are handled is not currently defined by tcod. + They will likely be handled by :any:`sys.unraisablehook`. + Args: callback (Callable[[Event], None]): A function which accepts :any:`Event` parameters. @@ -2303,6 +2307,8 @@ def __repr__(self) -> str: "get", "wait", "get_mouse_state", + "add_watch", + "remove_watch", "EventDispatch", "get_keyboard_state", "get_modifier_state", From c6c746061c9caff0068d50bd84b68a3792583c8f Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 4 Feb 2022 17:57:29 -0800 Subject: [PATCH 039/194] Rename rgb_mod to color_mod. I've decided that this should match SDL names rather than tcod names. --- tcod/sdl/render.py | 6 +++--- tests/test_sdl.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tcod/sdl/render.py b/tcod/sdl/render.py index 48d810fa..25f0d211 100644 --- a/tcod/sdl/render.py +++ b/tcod/sdl/render.py @@ -94,14 +94,14 @@ def blend_mode(self, value: int) -> None: _check(lib.SDL_SetTextureBlendMode(self.p, value)) @property - def rgb_mod(self) -> Tuple[int, int, int]: + def color_mod(self) -> Tuple[int, int, int]: """Texture RGB color modulate values, can be set.""" rgb = ffi.new("uint8_t[3]") _check(lib.SDL_GetTextureColorMod(self.p, rgb, rgb + 1, rgb + 2)) return int(rgb[0]), int(rgb[1]), int(rgb[2]) - @rgb_mod.setter - def rgb_mod(self, rgb: Tuple[int, int, int]) -> None: + @color_mod.setter + def color_mod(self, rgb: Tuple[int, int, int]) -> None: _check(lib.SDL_SetTextureColorMod(self.p, rgb[0], rgb[1], rgb[2])) diff --git a/tests/test_sdl.py b/tests/test_sdl.py index 348ebe60..ebb05a81 100644 --- a/tests/test_sdl.py +++ b/tests/test_sdl.py @@ -59,7 +59,7 @@ def test_sdl_render() -> None: assert rgb.format == tcod.lib.SDL_PIXELFORMAT_RGB24 rgb.alpha_mod = rgb.alpha_mod rgb.blend_mode = rgb.blend_mode - rgb.rgb_mod = rgb.rgb_mod + rgb.color_mod = rgb.color_mod rgba = render.upload_texture(np.zeros((8, 8, 4), np.uint8), access=tcod.sdl.render.TextureAccess.TARGET) with render.set_render_target(rgba): render.copy(rgb) From c80f966c06180d9e96de03d84e6e72f39ff81feb Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 4 Feb 2022 18:06:19 -0800 Subject: [PATCH 040/194] Fix broken docstring, add note on add_watch errors. The `:` caused Sphinx to think this was a type description. --- tcod/event.py | 1 + tcod/sdl/render.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tcod/event.py b/tcod/event.py index 7eccabd9..b333b1fc 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -1092,6 +1092,7 @@ def add_watch(callback: _EventCallback) -> _EventCallback: .. warning:: How uncaught exceptions in a callback are handled is not currently defined by tcod. They will likely be handled by :any:`sys.unraisablehook`. + This may be later changed to pass the excpetion to a :any`tcod.event.get` or :any:`tcod.event.wait` call. Args: callback (Callable[[Event], None]): diff --git a/tcod/sdl/render.py b/tcod/sdl/render.py index 25f0d211..0a177596 100644 --- a/tcod/sdl/render.py +++ b/tcod/sdl/render.py @@ -73,7 +73,7 @@ def height(self) -> int: @property def alpha_mod(self) -> int: - """Texture alpha modulate value, can be set to: 0 - 255.""" + """Texture alpha modulate value, can be set to 0 - 255.""" out = ffi.new("uint8_t*") _check(lib.SDL_GetTextureAlphaMod(self.p, out)) return int(out[0]) From 1149f9a1d0f4a46e95c47abf12cb204ec3d0fc86 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 4 Feb 2022 19:59:18 -0800 Subject: [PATCH 041/194] Prepare 13.4.0 release. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0c57acc..1bff981d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] + +## [13.4.0] - 2022-02-04 ### Added - Adds `sdl_window` and `sdl_renderer` to tcod contexts. - Adds `tcod.event.add_watch` and `tcod.event.remove_watch` to handle SDL events via callback. From c0e7401b42c55564b733be74f1297f5c5e92fc5b Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 5 Feb 2022 00:46:32 -0800 Subject: [PATCH 042/194] Add many more renderer functions. --- .vscode/settings.json | 1 + tcod/sdl/render.py | 318 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 317 insertions(+), 2 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 87df363c..63947009 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -238,6 +238,7 @@ "randomizer", "rbutton", "RCTRL", + "rects", "redist", "Redistributable", "redistributables", diff --git a/tcod/sdl/render.py b/tcod/sdl/render.py index 0a177596..12dbf4e5 100644 --- a/tcod/sdl/render.py +++ b/tcod/sdl/render.py @@ -5,14 +5,14 @@ from __future__ import annotations import enum -from typing import Any, Optional, Tuple +from typing import Any, Optional, Tuple, Union import numpy as np from numpy.typing import NDArray import tcod.sdl.video from tcod.loader import ffi, lib -from tcod.sdl import _check, _check_p +from tcod.sdl import _check, _check_p, _required_version class TextureAccess(enum.IntEnum): @@ -43,6 +43,17 @@ def _query(self) -> Tuple[int, int, int, int]: lib.SDL_QueryTexture(self.p, format, buffer, buffer + 1, buffer + 2) return int(format), int(buffer[0]), int(buffer[1]), int(buffer[2]) + def update(self, pixels: NDArray[Any], rect: Optional[Tuple[int, int, int, int]] = None) -> None: + """Update the pixel data of this texture. + + .. versionadded:: unreleased + """ + if rect is None: + rect = (0, 0, self.width, self.height) + assert pixels.shape[:2] == rect[3], rect[2] + assert pixels[0].flags.c_contiguous + _check(lib.SDL_UpdateTexture(self.p, (rect,), ffi.cast("void*", pixels.ctypes.data), pixels.strides[0])) + @property def format(self) -> int: """Texture format, read only.""" @@ -204,6 +215,309 @@ def upload_texture( ) return texture + @property + def draw_color(self) -> Tuple[int, int, int, int]: + """Get or set the active RGBA draw color for this renderer. + + .. versionadded:: unreleased + """ + rgba = ffi.new("uint8_t[4]") + _check(lib.SDL_GetRenderDrawColor(self.p, rgba, rgba + 1, rgba + 2, rgba + 3)) + return tuple(rgba) # type: ignore[return-value] + + @draw_color.setter + def draw_color(self, rgba: Tuple[int, int, int, int]) -> None: + _check(lib.SDL_SetRenderDrawColor(self.p, *rgba)) + + @property + def draw_blend_mode(self) -> int: + """Get or set the active blend mode of this renderer. + + .. versionadded:: unreleased + """ + out = ffi.new("SDL_BlendMode*") + _check(lib.SDL_GetRenderDrawBlendMode(self.p, out)) + return int(out[0]) + + @draw_blend_mode.setter + def draw_blend_mode(self, value: int) -> None: + _check(lib.SDL_SetRenderDrawBlendMode(self.p, value)) + + @property + def output_size(self) -> Tuple[int, int]: + """Get the (width, height) pixel resolution of the rendering context. + + .. seealso:: + https://wiki.libsdl.org/SDL_GetRendererOutputSize + + .. versionadded:: unreleased + """ + out = ffi.new("int[2]") + _check(lib.SDL_GetRendererOutputSize(self.p, out, out + 1)) + return out[0], out[1] + + @property + def clip_rect(self) -> Optional[Tuple[int, int, int, int]]: + """Get or set the clipping rectangle of this renderer. + + Set to None to disable clipping. + + .. versionadded:: unreleased + """ + if not lib.SDL_RenderIsClipEnabled(self.p): + return None + rect = ffi.new("SDL_Rect*") + lib.SDL_RenderGetClipRect(self.p, rect) + return rect.x, rect.y, rect.w, rect.h + + @clip_rect.setter + def clip_rect(self, rect: Optional[Tuple[int, int, int, int]]) -> None: + rect_p = ffi.NULL if rect is None else ffi.new("SDL_Rect*", rect) + _check(lib.SDL_RenderSetClipRect(self.p, rect_p)) + + @property + def integer_scaling(self) -> bool: + """Get or set if this renderer enforces integer scaling. + + .. seealso:: + https://wiki.libsdl.org/SDL_RenderSetIntegerScale + + .. versionadded:: unreleased + """ + return bool(lib.SDL_RenderGetIntegerScale(self.p)) + + @integer_scaling.setter + def integer_scaling(self, enable: bool) -> None: + _check(lib.SDL_RenderSetIntegerScale(self.p, enable)) + + @property + def logical_size(self) -> Tuple[int, int]: + """Get or set a device independent (width, height) resolution. + + Might be (0, 0) if a resolution was never assigned. + + .. seealso:: + https://wiki.libsdl.org/SDL_RenderSetLogicalSize + + .. versionadded:: unreleased + """ + out = ffi.new("int[2]") + lib.SDL_RenderGetLogicalSize(self.p, out, out + 1) + return out[0], out[1] + + @logical_size.setter + def logical_size(self, size: Tuple[int, int]) -> None: + _check(lib.SDL_RenderSetLogicalSize(self.p, *size)) + + @property + def scale(self) -> Tuple[float, float]: + """Get or set an (x_scale, y_scale) multiplier for drawing. + + .. seealso:: + https://wiki.libsdl.org/SDL_RenderSetScale + + .. versionadded:: unreleased + """ + out = ffi.new("float[2]") + lib.SDL_RenderGetScale(self.p, out, out + 1) + return out[0], out[1] + + @scale.setter + def scale(self, scale: Tuple[float, float]) -> None: + _check(lib.SDL_RenderSetScale(self.p, *scale)) + + @property + def viewport(self) -> Optional[Tuple[int, int, int, int]]: + """Get or set the drawing area for the current rendering target. + + .. seealso:: + https://wiki.libsdl.org/SDL_RenderSetViewport + + .. versionadded:: unreleased + """ + rect = ffi.new("SDL_Rect*") + lib.SDL_RenderGetViewport(self.p, rect) + return rect.x, rect.y, rect.w, rect.h + + @viewport.setter + def viewport(self, rect: Optional[Tuple[int, int, int, int]]) -> None: + _check(lib.SDL_RenderSetViewport(self.p, (rect,))) + + def read_pixels( + self, + *, + rect: Optional[Tuple[int, int, int, int]] = None, + format: Optional[int] = None, + out: Optional[NDArray[Any]] = None, + ) -> NDArray[Any]: + """ + .. versionadded:: unreleased + """ + if format is None: + format = lib.SDL_PIXELFORMAT_RGBA32 + if rect is None: + texture_p = lib.SDL_GetRenderTarget(self.p) + if texture_p: + texture = Texture(texture_p) + rect = (0, 0, texture.width, texture.height) + else: + rect = (0, 0, *self.output_size) + width, height = rect[2:4] + if out is None: + if format == lib.SDL_PIXELFORMAT_RGBA32: + out = np.empty((height, width, 4), dtype=np.uint8) + elif format == lib.SDL_PIXELFORMAT_RGB24: + out = np.empty((height, width, 3), dtype=np.uint8) + else: + raise TypeError("Pixel format not supported yet.") + assert out.shape[:2] == height, width + assert out[0].flags.c_contiguous + _check(lib.SDL_RenderReadPixels(self.p, format, ffi.cast("void*", out.ctypes.data), out.strides[0])) + return out + + def clear(self) -> None: + """Clear the current render target with :any:`draw_color`. + + .. versionadded:: unreleased + """ + _check(lib.SDL_RenderClear(self.p)) + + def fill_rect(self, rect: Tuple[float, float, float, float]) -> None: + """Fill a rectangle with :any:`draw_color`. + .. versionadded:: unreleased + """ + _check(lib.SDL_RenderFillRectF(self.p, (rect,))) + + def draw_rect(self, rect: Tuple[float, float, float, float]) -> None: + """Draw a rectangle outline. + + .. versionadded:: unreleased + """ + _check(lib.SDL_RenderDrawRectF(self.p, (rect,))) + + def draw_point(self, xy: Tuple[float, float]) -> None: + """Draw a point. + + .. versionadded:: unreleased + """ + _check(lib.SDL_RenderDrawPointF(self.p, (xy,))) + + def draw_line(self, start: Tuple[float, float], end: Tuple[float, float]) -> None: + """Draw a single line. + + .. versionadded:: unreleased + """ + _check(lib.SDL_RenderDrawLineF(self.p, *start, *end)) + + def fill_rects(self, rects: NDArray[Union[np.intc, np.float32]]) -> None: + """Fill multiple rectangles from an array. + + .. versionadded:: unreleased + """ + assert len(rects.shape) == 2 + assert rects.shape[1] == 4 + rects = np.ascontiguousarray(rects) + if rects.dtype == np.intc: + _check(lib.SDL_RenderFillRects(self.p, tcod.ffi.from_buffer("SDL_Rect*", rects), rects.shape[0])) + elif rects.dtype == np.float32: + _check(lib.SDL_RenderFillRectsF(self.p, tcod.ffi.from_buffer("SDL_FRect*", rects), rects.shape[0])) + else: + raise TypeError(f"Array must be an np.intc or np.float32 type, got {rects.dtype}.") + + def draw_rects(self, rects: NDArray[Union[np.intc, np.float32]]) -> None: + """Draw multiple outlined rectangles from an array. + + .. versionadded:: unreleased + """ + assert len(rects.shape) == 2 + assert rects.shape[1] == 4 + rects = np.ascontiguousarray(rects) + if rects.dtype == np.intc: + _check(lib.SDL_RenderDrawRects(self.p, tcod.ffi.from_buffer("SDL_Rect*", rects), rects.shape[0])) + elif rects.dtype == np.float32: + _check(lib.SDL_RenderDrawRectsF(self.p, tcod.ffi.from_buffer("SDL_FRect*", rects), rects.shape[0])) + else: + raise TypeError(f"Array must be an np.intc or np.float32 type, got {rects.dtype}.") + + def draw_points(self, points: NDArray[Union[np.intc, np.float32]]) -> None: + """Draw an array of points. + + .. versionadded:: unreleased + """ + assert len(points.shape) == 2 + assert points.shape[1] == 2 + points = np.ascontiguousarray(points) + if points.dtype == np.intc: + _check(lib.SDL_RenderDrawRects(self.p, tcod.ffi.from_buffer("SDL_Point*", points), points.shape[0])) + elif points.dtype == np.float32: + _check(lib.SDL_RenderDrawRectsF(self.p, tcod.ffi.from_buffer("SDL_FPoint*", points), points.shape[0])) + else: + raise TypeError(f"Array must be an np.intc or np.float32 type, got {points.dtype}.") + + def draw_lines(self, points: NDArray[Union[np.intc, np.float32]]) -> None: + """Draw a connected series of lines from an array. + + .. versionadded:: unreleased + """ + assert len(points.shape) == 2 + assert points.shape[1] == 2 + points = np.ascontiguousarray(points) + if points.dtype == np.intc: + _check(lib.SDL_RenderDrawRects(self.p, tcod.ffi.from_buffer("SDL_Point*", points), points.shape[0] - 1)) + elif points.dtype == np.float32: + _check(lib.SDL_RenderDrawRectsF(self.p, tcod.ffi.from_buffer("SDL_FPoint*", points), points.shape[0] - 1)) + else: + raise TypeError(f"Array must be an np.intc or np.float32 type, got {points.dtype}.") + + @_required_version((2, 0, 18)) + def geometry( + self, + texture: Optional[Texture], + xy: NDArray[np.float32], + color: NDArray[np.uint8], + uv: NDArray[np.float32], + indices: Optional[NDArray[Union[np.uint8, np.uint16, np.uint32]]] = None, + ) -> None: + """Render triangles from texture and vertex data. + + .. versionadded:: unreleased + """ + assert xy.dtype == np.float32 + assert len(xy.shape) == 2 + assert xy.shape[1] == 2 + assert xy[0].flags.c_contiguous + + assert color.dtype == np.uint8 + assert len(color.shape) == 2 + assert color.shape[1] == 4 + assert color[0].flags.c_contiguous + + assert uv.dtype == np.float32 + assert len(uv.shape) == 2 + assert uv.shape[1] == 2 + assert uv[0].flags.c_contiguous + if indices is not None: + assert indices.dtype.type in (np.uint8, np.uint16, np.uint32, np.int8, np.int16, np.int32) + indices = np.ascontiguousarray(indices) + assert len(indices.shape) == 1 + assert xy.shape[0] == color.shape[0] == uv.shape[0] + _check( + lib.SDL_RenderGeometryRaw( + self.p, + texture.p if texture else ffi.NULL, + ffi.cast("float*", xy.ctypes.data), + xy.strides[0], + ffi.cast("uint8_t*", color.ctypes.data), + color.strides[0], + ffi.cast("float*", uv.ctypes.data), + uv.strides[0], + xy.shape[0], # Number of vertices. + ffi.cast("void*", indices.ctypes.data) if indices is not None else ffi.NULL, + indices.size if indices is not None else 0, + indices.itemsize if indices is not None else 0, + ) + ) + def new_renderer( window: tcod.sdl.video.Window, From 6f1b22c887c43d1c838928eb52e0c976ad6b9809 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 5 Feb 2022 01:04:34 -0800 Subject: [PATCH 043/194] Add extended copy parameters. Fix tests. --- tcod/sdl/render.py | 44 ++++++++++++++++++++++++++++++++++++++------ tests/test_sdl.py | 2 ++ 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/tcod/sdl/render.py b/tcod/sdl/render.py index 12dbf4e5..94b54f5e 100644 --- a/tcod/sdl/render.py +++ b/tcod/sdl/render.py @@ -26,6 +26,17 @@ class TextureAccess(enum.IntEnum): """Texture will be used as a render target.""" +class RendererFlip(enum.IntFlag): + """Flip parameter for :any:`Renderer.copy`.""" + + NONE = 0 + """Default value, no flip.""" + HORIZONTAL = 1 + """Flip the image horizontally.""" + VERTICAL = 2 + """Flip the image vertically.""" + + class Texture: """SDL hardware textures.""" @@ -146,16 +157,37 @@ def __eq__(self, other: Any) -> bool: def copy( self, texture: Texture, - source: Optional[Tuple[int, int, int, int]] = None, - dest: Optional[Tuple[int, int, int, int]] = None, + source: Optional[Tuple[float, float, float, float]] = None, + dest: Optional[Tuple[float, float, float, float]] = None, + angle: float = 0, + center: Optional[Tuple[float, float]] = None, + flip: RendererFlip = RendererFlip.NONE, ) -> None: """Copy a texture to the rendering target. - `source` and `dest` are (x, y, width, height) regions of the texture parameter and target texture respectively. + Args: + texture: The texture to copy onto the current texture target. + source: The (x, y, width, height) region of `texture` to copy. If None then the entire texture is copied. + dest: The (x, y, width, height) region of the target. If None then the entire target is drawn over. + angle: The angle in degrees to rotate the image clockwise. + center: The (x, y) point where rotation is applied. If None then the center of `dest` is used. + flip: Flips the `texture` when drawing it. + + .. versionchanged:: unreleased + `source` and `dest` can now be float tuples. + Added the `angle`, `center`, and `flip` parameters. """ - source_ = ffi.NULL if source is None else ffi.new("SDL_Rect*", source) - dest_ = ffi.NULL if dest is None else ffi.new("SDL_Rect*", dest) - _check(lib.SDL_RenderCopy(self.p, texture.p, source_, dest_)) + _check( + lib.SDL_RenderCopyExF( + self.p, + texture.p, + (source,) if source is not None else ffi.NULL, + (dest,) if dest is not None else ffi.NULL, + angle, + (center,) if center is not None else ffi.NULL, + flip, + ) + ) def present(self) -> None: """Present the currently rendered image to the screen.""" diff --git a/tests/test_sdl.py b/tests/test_sdl.py index ebb05a81..8dea7f33 100644 --- a/tests/test_sdl.py +++ b/tests/test_sdl.py @@ -4,6 +4,7 @@ import pytest import tcod.sdl.render +import tcod.sdl.sys import tcod.sdl.video @@ -44,6 +45,7 @@ def test_sdl_window_bad_types() -> None: def test_sdl_screen_saver() -> None: + tcod.sdl.sys.init() assert tcod.sdl.video.screen_saver_allowed(False) is False assert tcod.sdl.video.screen_saver_allowed(True) is True assert tcod.sdl.video.screen_saver_allowed() is True From 45d308b28d0a92bb314dea632ec909ad569852ff Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 5 Feb 2022 12:28:47 -0800 Subject: [PATCH 044/194] Experiment with mixer channels. --- tcod/sdl/audio.py | 97 ++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 83 insertions(+), 14 deletions(-) diff --git a/tcod/sdl/audio.py b/tcod/sdl/audio.py index 8e0e3c70..4edfe974 100644 --- a/tcod/sdl/audio.py +++ b/tcod/sdl/audio.py @@ -3,7 +3,7 @@ import sys import threading import time -from typing import Any, Iterator, List, Optional +from typing import Any, Callable, Dict, Hashable, Iterator, List, Optional, Tuple, Union import numpy as np from numpy.typing import ArrayLike, DTypeLike, NDArray @@ -139,8 +139,65 @@ def __default_callback(self, stream: NDArray[Any]) -> None: stream[...] = self.silence +class Channel: + mixer: Mixer + + def __init__(self) -> None: + self.volume: Union[float, Tuple[float, ...]] = 1.0 + self.sound_queue: List[NDArray[Any]] = [] + self.on_end_callback: Optional[Callable[[Channel], None]] = None + + @property + def busy(self) -> bool: + return bool(self.sound_queue) + + def play( + self, + sound: ArrayLike, + *, + on_end: Optional[Callable[[Channel], None]] = None, + ) -> None: + self.sound_queue[:] = [self._verify_audio_sample(sound)] + self.on_end_callback = on_end + + def _verify_audio_sample(self, sample: ArrayLike) -> NDArray[Any]: + """Verify an audio sample is valid and return it as a Numpy array.""" + array: NDArray[Any] = np.asarray(sample) + assert array.dtype == self.mixer.device.format + if len(array.shape) == 1: + array = array[:, np.newaxis] + return array + + def _on_mix(self, stream: NDArray[Any]) -> None: + while self.sound_queue and stream.size: + buffer = self.sound_queue[0] + if buffer.shape[0] > stream.shape[0]: + # Mix part of the buffer into the stream. + stream[:] += buffer[: stream.shape[0]] * self.volume + self.sound_queue[0] = buffer[stream.shape[0] :] + break # Stream was filled. + # Remaining buffer fits the stream array. + stream[: buffer.shape[0]] += buffer * self.volume + stream = stream[buffer.shape[0] :] + self.sound_queue.pop(0) + if not self.sound_queue and self.on_end_callback is not None: + self.on_end_callback(self) + + def fadeout(self, time: float) -> None: + assert time >= 0 + time_samples = round(time * self.mixer.device.frequency) + 1 + buffer: NDArray[np.float32] = np.zeros((time_samples, self.mixer.device.channels), np.float32) + self._on_mix(buffer) + buffer *= np.linspace(1.0, 0.0, time_samples + 1, endpoint=False)[1:] + self.sound_queue[:] = [buffer] + + def stop(self) -> None: + self.fadeout(0.0005) + + class Mixer(threading.Thread): def __init__(self, device: AudioDevice): + assert device.format == np.float32 super().__init__(daemon=True) self.device = device @@ -161,23 +218,35 @@ def on_stream(self, stream: NDArray[Any]) -> None: class BasicMixer(Mixer): def __init__(self, device: AudioDevice): super().__init__(device) - self.play_buffers: List[List[NDArray[Any]]] = [] + self.channels: Dict[Hashable, Channel] = {} - def play(self, sound: ArrayLike) -> None: - array = np.asarray(sound, dtype=self.device.format) - assert array.size - if len(array.shape) == 1: - array = array[:, np.newaxis] - chunks: List[NDArray[Any]] = np.split(array, range(0, len(array), self.device.samples)[1:])[::-1] - self.play_buffers.append(chunks) + def get_channel(self, key: Hashable) -> Channel: + if key not in self.channels: + self.channels[key] = Channel() + self.channels[key].mixer = self + return self.channels[key] + + def get_free_channel(self) -> Channel: + i = 0 + while True: + if not self.get_channel(i).busy: + return self.channels[i] + i += 1 + + def play( + self, + sound: ArrayLike, + *, + on_end: Optional[Callable[[Channel], None]] = None, + ) -> Channel: + channel = self.get_free_channel() + channel.play(sound, on_end=on_end) + return channel def on_stream(self, stream: NDArray[Any]) -> None: super().on_stream(stream) - for chunks in self.play_buffers: - chunk = chunks.pop() - stream[: len(chunk)] += chunk - - self.play_buffers = [chunks for chunks in self.play_buffers if chunks] + for channel in list(self.channels.values()): + channel._on_mix(stream) @ffi.def_extern() # type: ignore From 40435f5e5110a061b74e142c1c6e43fcf40a4b13 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 5 Feb 2022 15:16:54 -0800 Subject: [PATCH 045/194] Upload Windows wheel artifacts. Keep these for 7 days in case anyone is testing them this way. --- .github/workflows/python-package.yml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index caf0f836..dc00a1ae 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -152,7 +152,13 @@ jobs: with: name: sdist path: dist/tcod-*.tar.gz - retention-days: 3 + retention-days: 7 + - uses: actions/upload-artifact@v2 + if: runner.os == 'Windows' + with: + name: wheels-windows + path: dist/*.whl + retention-days: 7 isolated: # Test installing the package from source. needs: build @@ -224,9 +230,9 @@ jobs: - name: Archive wheel uses: actions/upload-artifact@v2 with: - name: wheel-linux + name: wheels-linux path: wheelhouse/*.whl - retention-days: 1 + retention-days: 7 - name: Upload to PyPI if: startsWith(github.ref, 'refs/tags/') env: @@ -266,9 +272,9 @@ jobs: - name: Archive wheel uses: actions/upload-artifact@v2 with: - name: wheel-macos + name: wheels-macos path: wheelhouse/*.whl - retention-days: 1 + retention-days: 7 - name: Upload to PyPI if: startsWith(github.ref, 'refs/tags/') env: From 5657e3b8daa8003e5d7fb094db28bc6c72ba96e9 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 5 Feb 2022 19:03:07 -0800 Subject: [PATCH 046/194] Add Renderer.set_vsync. --- tcod/sdl/render.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tcod/sdl/render.py b/tcod/sdl/render.py index 94b54f5e..ddb42b26 100644 --- a/tcod/sdl/render.py +++ b/tcod/sdl/render.py @@ -375,6 +375,14 @@ def viewport(self) -> Optional[Tuple[int, int, int, int]]: def viewport(self, rect: Optional[Tuple[int, int, int, int]]) -> None: _check(lib.SDL_RenderSetViewport(self.p, (rect,))) + @_required_version((2, 0, 18)) + def set_vsync(self, enable: bool) -> None: + """Enable or disable VSync for this renderer. + + .. versionadded:: unreleased + """ + _check(lib.SDL_RenderSetVSync(self.p, enable)) + def read_pixels( self, *, From 571eafd3b697dc7d088894911d2049e339b853a1 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 6 Feb 2022 12:21:29 -0800 Subject: [PATCH 047/194] Add compose_blend_mode and blend mode enums. Refactor TextureAccess enums to not need library lookup. --- tcod/sdl/render.py | 110 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 107 insertions(+), 3 deletions(-) diff --git a/tcod/sdl/render.py b/tcod/sdl/render.py index ddb42b26..8bec1880 100644 --- a/tcod/sdl/render.py +++ b/tcod/sdl/render.py @@ -18,11 +18,11 @@ class TextureAccess(enum.IntEnum): """Determines how a texture is expected to be used.""" - STATIC = lib.SDL_TEXTUREACCESS_STATIC or 0 + STATIC = 0 """Texture rarely changes.""" - STREAMING = lib.SDL_TEXTUREACCESS_STREAMING or 0 + STREAMING = 1 """Texture frequently changes.""" - TARGET = lib.SDL_TEXTUREACCESS_TARGET or 0 + TARGET = 2 """Texture will be used as a render target.""" @@ -37,6 +37,110 @@ class RendererFlip(enum.IntFlag): """Flip the image vertically.""" +class BlendFactor(enum.IntEnum): + """SDL blend factors. + + .. seealso:: + :any:`compose_blend_mode` + https://wiki.libsdl.org/SDL_BlendFactor + + .. versionadded:: unreleased + """ + + ZERO = 0x1 + """""" + ONE = 0x2 + """""" + SRC_COLOR = 0x3 + """""" + ONE_MINUS_SRC_COLOR = 0x4 + """""" + SRC_ALPHA = 0x5 + """""" + ONE_MINUS_SRC_ALPHA = 0x6 + """""" + DST_COLOR = 0x7 + """""" + ONE_MINUS_DST_COLOR = 0x8 + """""" + DST_ALPHA = 0x9 + """""" + ONE_MINUS_DST_ALPHA = 0xA + """""" + + +class BlendOperation(enum.IntEnum): + """SDL blend operations. + + .. seealso:: + :any:`compose_blend_mode` + https://wiki.libsdl.org/SDL_BlendOperation + + .. versionadded:: unreleased + """ + + ADD = 0x1 + """dest + source""" + SUBTRACT = 0x2 + """dest - source""" + REV_SUBTRACT = 0x3 + """source - dest""" + MINIMUM = 0x4 + """min(dest, source)""" + MAXIMUM = 0x5 + """max(dest, source)""" + + +class BlendMode(enum.IntEnum): + """SDL blend modes. + + .. seealso:: + :any:`Texture.blend_mode` + :any:`Renderer.draw_blend_mode` + :any:`compose_blend_mode` + + .. versionadded:: unreleased + """ + + NONE = 0x00000000 + """""" + BLEND = 0x00000001 + """""" + ADD = 0x00000002 + """""" + MOD = 0x00000004 + """""" + INVALID = 0x7FFFFFFF + """""" + + +def compose_blend_mode( + source_color_factor: BlendFactor, + dest_color_factor: BlendFactor, + color_operation: BlendOperation, + source_alpha_factor: BlendFactor, + dest_alpha_factor: BlendFactor, + alpha_operation: BlendOperation, +) -> BlendMode: + """Return a custom blend mode composed of the given factors and operations. + + .. seealso:: + https://wiki.libsdl.org/SDL_ComposeCustomBlendMode + + .. versionadded:: unreleased + """ + return BlendMode( + lib.SDL_ComposeCustomBlendMode( + source_color_factor, + dest_color_factor, + color_operation, + source_alpha_factor, + dest_alpha_factor, + alpha_operation, + ) + ) + + class Texture: """SDL hardware textures.""" From e3fc45fd1093fa59a0e7e26bd9f72c37d3173591 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 6 Feb 2022 12:33:03 -0800 Subject: [PATCH 048/194] Have enum properties return enum instances. --- CHANGELOG.md | 3 +++ tcod/sdl/render.py | 24 ++++++++++++++++-------- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bff981d..fef7ea70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] +### Changed +- `Texture.access` and `Texture.blend_mode` properties now return enum instances. + You can still set them with `int` but Mypy will complain. ## [13.4.0] - 2022-02-04 ### Added diff --git a/tcod/sdl/render.py b/tcod/sdl/render.py index 8bec1880..b005bcd5 100644 --- a/tcod/sdl/render.py +++ b/tcod/sdl/render.py @@ -177,11 +177,15 @@ def format(self) -> int: return int(buffer[0]) @property - def access(self) -> int: - """Texture access mode, read only.""" + def access(self) -> TextureAccess: + """Texture access mode, read only. + + .. versionadded:: unreleased + Property now returns a TextureAccess instance. + """ buffer = ffi.new("int*") lib.SDL_QueryTexture(self.p, ffi.NULL, buffer, ffi.NULL, ffi.NULL) - return int(buffer[0]) + return TextureAccess(buffer[0]) @property def width(self) -> int: @@ -209,11 +213,15 @@ def alpha_mod(self, value: int) -> None: _check(lib.SDL_SetTextureAlphaMod(self.p, value)) @property - def blend_mode(self) -> int: - """Texture blend mode, can be set.""" + def blend_mode(self) -> BlendMode: + """Texture blend mode, can be set. + + .. versionadded:: unreleased + Property now returns a BlendMode instance. + """ out = ffi.new("SDL_BlendMode*") _check(lib.SDL_GetTextureBlendMode(self.p, out)) - return int(out[0]) + return BlendMode(out[0]) @blend_mode.setter def blend_mode(self, value: int) -> None: @@ -366,14 +374,14 @@ def draw_color(self, rgba: Tuple[int, int, int, int]) -> None: _check(lib.SDL_SetRenderDrawColor(self.p, *rgba)) @property - def draw_blend_mode(self) -> int: + def draw_blend_mode(self) -> BlendMode: """Get or set the active blend mode of this renderer. .. versionadded:: unreleased """ out = ffi.new("SDL_BlendMode*") _check(lib.SDL_GetRenderDrawBlendMode(self.p, out)) - return int(out[0]) + return BlendMode(out[0]) @draw_blend_mode.setter def draw_blend_mode(self, value: int) -> None: From c8d0b7bcdd8a6131ff3d91a683499e08ebb7a0a6 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 6 Feb 2022 13:13:10 -0800 Subject: [PATCH 049/194] Add Window.mouse_rect. Fix docs of changed properties. --- tcod/sdl/__init__.py | 9 +++++++++ tcod/sdl/render.py | 4 ++-- tcod/sdl/video.py | 19 ++++++++++++++++++- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/tcod/sdl/__init__.py b/tcod/sdl/__init__.py index 524f24b6..81f04d12 100644 --- a/tcod/sdl/__init__.py +++ b/tcod/sdl/__init__.py @@ -58,6 +58,15 @@ def _linked_version() -> Tuple[int, int, int]: return int(sdl_version.major), int(sdl_version.minor), int(sdl_version.patch) +def _version_at_least(required: Tuple[int, int, int]) -> None: + """Raise an error if the compiled version is less than required. Used to guard recentally defined SDL functions.""" + if required <= _compiled_version(): + return + raise RuntimeError( + f"This feature requires SDL version {required}, but tcod was compiled with version {_compiled_version()}" + ) + + def _required_version(required: Tuple[int, int, int]) -> Callable[[T], T]: if not lib: # Read the docs mock object. return lambda x: x diff --git a/tcod/sdl/render.py b/tcod/sdl/render.py index b005bcd5..98017772 100644 --- a/tcod/sdl/render.py +++ b/tcod/sdl/render.py @@ -180,7 +180,7 @@ def format(self) -> int: def access(self) -> TextureAccess: """Texture access mode, read only. - .. versionadded:: unreleased + .. versionchanged:: unreleased Property now returns a TextureAccess instance. """ buffer = ffi.new("int*") @@ -216,7 +216,7 @@ def alpha_mod(self, value: int) -> None: def blend_mode(self) -> BlendMode: """Texture blend mode, can be set. - .. versionadded:: unreleased + .. versionchanged:: unreleased Property now returns a BlendMode instance. """ out = ffi.new("SDL_BlendMode*") diff --git a/tcod/sdl/video.py b/tcod/sdl/video.py index 4d1d3b7e..bdcb6b6d 100644 --- a/tcod/sdl/video.py +++ b/tcod/sdl/video.py @@ -12,7 +12,7 @@ from numpy.typing import ArrayLike, NDArray from tcod.loader import ffi, lib -from tcod.sdl import _check, _check_p, _required_version +from tcod.sdl import _check, _check_p, _required_version, _version_at_least __all__ = ( "WindowFlags", @@ -278,6 +278,23 @@ def grab(self) -> bool: def grab(self, value: bool) -> None: lib.SDL_SetWindowGrab(self.p, value) + @property + def mouse_rect(self) -> Optional[Tuple[int, int, int, int]]: + """Get or set the mouse confinement area when the window has mouse focus. + + Setting this will not automatically grab the cursor. + + .. versionadded:: unreleased + """ + _version_at_least((2, 0, 18)) + rect = lib.SDL_GetWindowMouseRect(self.p) + return (rect.x, rect.y, rect.w, rect.h) if rect else None + + @mouse_rect.setter + def mouse_rect(self, rect: Optional[Tuple[int, int, int, int]]) -> None: + _version_at_least((2, 0, 18)) + _check(lib.SDL_SetWindowMouseRect(self.p, (rect,) if rect else ffi.NULL)) + @_required_version((2, 0, 16)) def flash(self, operation: FlashOperation = FlashOperation.UNTIL_FOCUSED) -> None: """Get the users attention.""" From 9676735cff468b723e47dcd3ddf1cad6f78d2972 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 6 Feb 2022 13:27:12 -0800 Subject: [PATCH 050/194] Experiment with clipboard handing. --- tcod/sdl/sys.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tcod/sdl/sys.py b/tcod/sdl/sys.py index 7569ed83..34f5d4fc 100644 --- a/tcod/sdl/sys.py +++ b/tcod/sdl/sys.py @@ -1,10 +1,11 @@ from __future__ import annotations import enum +import warnings from typing import Any, Tuple from tcod.loader import ffi, lib -from tcod.sdl import _check +from tcod.sdl import _check, _get_error class Subsystem(enum.IntFlag): @@ -61,3 +62,16 @@ def _get_power_info() -> Tuple[_PowerState, int, int]: seconds_of_power = buffer[0] percenage = buffer[1] return power_state, seconds_of_power, percenage + + +def _get_clipboard() -> str: + """Return the text of the clipboard.""" + text = str(ffi.string(lib.SDL_GetClipboardText()), encoding="utf-8") + if not text: # Show the reason for an empty return, this should probably be logged instead. + warnings.warn(f"Return string is empty because: {_get_error()}") + return text + + +def _set_clipboard(text: str) -> None: + """Replace the clipboard with text.""" + _check(lib.SDL_SetClipboardText(text.encode("utf-8"))) From 3654e2f1d134748b58c31c21c13b6c216afa64d0 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 7 Feb 2022 13:05:29 -0800 Subject: [PATCH 051/194] Add public SDL mouse module. --- .vscode/settings.json | 5 + docs/index.rst | 1 + docs/sdl/mouse.rst | 5 + tcod/sdl/mouse.py | 212 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 223 insertions(+) create mode 100644 docs/sdl/mouse.rst create mode 100644 tcod/sdl/mouse.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 63947009..92284692 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -127,6 +127,7 @@ "howto", "htbp", "htmlzip", + "IBEAM", "ifdef", "ifndef", "iinfo", @@ -267,6 +268,10 @@ "servernum", "setuptools", "SHADOWCAST", + "SIZENESW", + "SIZENS", + "SIZENWSE", + "SIZEWE", "SMILIE", "snprintf", "stdeb", diff --git a/docs/index.rst b/docs/index.rst index 7ddc5583..3638a87c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -40,6 +40,7 @@ Contents: tcod/tileset libtcodpy sdl/render + sdl/mouse sdl/video Indices and tables diff --git a/docs/sdl/mouse.rst b/docs/sdl/mouse.rst new file mode 100644 index 00000000..932f4d9f --- /dev/null +++ b/docs/sdl/mouse.rst @@ -0,0 +1,5 @@ +tcod.sdl.mouse - SDL Mouse Functions +==================================== + +.. automodule:: tcod.sdl.mouse + :members: diff --git a/tcod/sdl/mouse.py b/tcod/sdl/mouse.py new file mode 100644 index 00000000..aa174ad9 --- /dev/null +++ b/tcod/sdl/mouse.py @@ -0,0 +1,212 @@ +"""SDL mouse and cursor functions. + +.. versionadded:: unreleased +""" +from __future__ import annotations + +import enum +from typing import Any, Optional, Tuple, Union + +import numpy as np +from numpy.typing import ArrayLike, NDArray + +import tcod.event +import tcod.sdl.video +from tcod.loader import ffi, lib +from tcod.sdl import _check, _check_p + + +class Cursor: + """A cursor icon for use with :any:`set_cursor`.""" + + def __init__(self, sdl_cursor_p: Any): + if ffi.typeof(sdl_cursor_p) is not ffi.typeof("struct SDL_Cursor*"): + raise TypeError(f"Expected a {ffi.typeof('struct SDL_Cursor*')} type (was {ffi.typeof(sdl_cursor_p)}).") + if not sdl_cursor_p: + raise TypeError("C pointer must not be null.") + self.p = sdl_cursor_p + + def __eq__(self, other: Any) -> bool: + return bool(self.p == getattr(other, "p", None)) + + @classmethod + def _claim(cls, sdl_cursor_p: Any) -> Cursor: + """Verify and wrap this pointer in a garbage collector before returning a Cursor.""" + return cls(ffi.gc(_check_p(sdl_cursor_p), lib.SDL_FreeCursor)) + + +class SystemCursor(enum.IntEnum): + """An enumerator of system cursor icons.""" + + ARROW = 0 + """""" + IBEAM = enum.auto() + """""" + WAIT = enum.auto() + """""" + CROSSHAIR = enum.auto() + """""" + WAITARROW = enum.auto() + """""" + SIZENWSE = enum.auto() + """""" + SIZENESW = enum.auto() + """""" + SIZEWE = enum.auto() + """""" + SIZENS = enum.auto() + """""" + SIZEALL = enum.auto() + """""" + NO = enum.auto() + """""" + HAND = enum.auto() + """""" + + +def new_cursor(data: NDArray[np.bool_], mask: NDArray[np.bool_], hot_xy: Tuple[int, int] = (0, 0)) -> Cursor: + """Return a new non-color Cursor from the provided parameters. + + Args: + data: A row-major boolean array for the data parameters. See the SDL docs for more info. + mask: A row-major boolean array for the mask parameters. See the SDL docs for more info. + hot_xy: The position of the pointer relative to the mouse sprite, starting from the upper-left at (0, 0). + + .. seealso:: + :any:`set_cursor` + https://wiki.libsdl.org/SDL_CreateCursor + """ + if len(data.shape) != 2: + raise TypeError("Data and mask arrays must be 2D.") + if data.shape != mask.shape: + raise TypeError("Data and mask arrays must have the same shape.") + height, width = data.shape + data_packed = np.packbits(data, axis=0, bitorder="big") + mask_packed = np.packbits(mask, axis=0, bitorder="big") + return Cursor._claim( + lib.SDL_CreateCursor( + ffi.from_buffer("uint8_t*", data_packed), ffi.from_buffer("uint8_t*", mask_packed), width, height, *hot_xy + ) + ) + + +def new_color_cursor(pixels: ArrayLike, hot_xy: Tuple[int, int]) -> Cursor: + """ + Args: + pixels: A row-major array of RGB or RGBA pixels. + hot_xy: The position of the pointer relative to the mouse sprite, starting from the upper-left at (0, 0). + + .. seealso:: + :any:`set_cursor` + """ + surface = tcod.sdl.video._TempSurface(pixels) + return Cursor._claim(lib.SDL_CreateColorCursor(surface.p, *hot_xy)) + + +def new_system_cursor(cursor: SystemCursor) -> Cursor: + """Return a new Cursor from one of the system cursors labeled by SystemCursor. + + .. seealso:: + :any:`set_cursor` + """ + return Cursor._claim(lib.SDL_CreateSystemCursor(cursor)) + + +def set_cursor(cursor: Optional[Union[Cursor, SystemCursor]]) -> None: + """Change the active cursor to the one provided. + + Args: + cursor: A cursor created from :any:`new_cursor`, :any:`new_color_cursor`, or :any:`new_system_cursor`. + Can also take values of :any:`SystemCursor` directly. + None will force the current cursor to be redrawn. + """ + if isinstance(cursor, SystemCursor): + cursor = new_system_cursor(cursor) + lib.SDL_SetCursor(cursor.p if cursor is not None else ffi.NULL) + + +def get_default_cursor() -> Cursor: + """Return the default cursor.""" + return Cursor(_check_p(lib.SDL_GetDefaultCursor())) + + +def get_cursor() -> Optional[Cursor]: + """Return the active cursor, or None if these is no mouse.""" + cursor_p = lib.SDL_GetCursor() + return Cursor(cursor_p) if cursor_p else None + + +def capture(enable: bool) -> None: + """Enable or disable mouse capture to track the mouse outside of a window. + + It is highly reccomended to read the related remarks section in the SDL docs before using this. + + .. seealso:: + :any:`tcod.sdl.mouse.set_relative_mode` + https://wiki.libsdl.org/SDL_CaptureMouse + """ + _check(lib.SDL_CaptureMouse(enable)) + + +def set_relative_mode(enable: bool) -> None: + """Enable or disable relative mouse mode which will lock and hide the mouse and only report mouse motion. + + .. seealso:: + :any:`tcod.sdl.mouse.capture` + https://wiki.libsdl.org/SDL_SetRelativeMouseMode + """ + _check(lib.SDL_SetRelativeMouseMode(enable)) + + +def get_relative_mode() -> bool: + """Return True if relative mouse mode is enabled.""" + return bool(lib.SDL_GetRelativeMouseMode()) + + +def get_global_state() -> tcod.event.MouseState: + """Return the mouse state relative to the desktop. + + .. seealso:: + https://wiki.libsdl.org/SDL_GetGlobalMouseState + """ + xy = ffi.new("int[2]") + state = lib.SDL_GetGlobalMouseState(xy, xy + 1) + return tcod.event.MouseState(pixel=(xy[0], xy[1]), state=state) + + +def get_relative_state() -> tcod.event.MouseState: + """Return the mouse state, the coordinates are relative to the last time this function was called. + + .. seealso:: + https://wiki.libsdl.org/SDL_GetRelativeMouseState + """ + xy = ffi.new("int[2]") + state = lib.SDL_GetRelativeMouseState(xy, xy + 1) + return tcod.event.MouseState(pixel=(xy[0], xy[1]), state=state) + + +def get_state() -> tcod.event.MouseState: + """Return the mouse state relative to the window with mouse focus. + + .. seealso:: + https://wiki.libsdl.org/SDL_GetMouseState + """ + xy = ffi.new("int[2]") + state = lib.SDL_GetMouseState(xy, xy + 1) + return tcod.event.MouseState(pixel=(xy[0], xy[1]), state=state) + + +def get_focus() -> Optional[tcod.sdl.video.Window]: + """Return the window which currently has mouse focus.""" + window_p = lib.SDL_GetMouseFocus() + return tcod.sdl.video.Window(window_p) if window_p else None + + +def warp_global(x: int, y: int) -> None: + """Move the mouse cursor to a position on the desktop.""" + _check(lib.SDL_WarpMouseGlobal(x, y)) + + +def warp_in_window(window: tcod.sdl.video.Window, x: int, y: int) -> None: + """Move the mouse cursor to a position within a window.""" + _check(lib.SDL_WarpMouseInWindow(window.p, x, y)) From 2d0a2e512c97a1fb21b32d202ac116a0a1abe170 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 9 Feb 2022 21:36:54 -0800 Subject: [PATCH 052/194] Add more audio callback support and document audio device opening. --- .vscode/settings.json | 1 + tcod/sdl/audio.py | 99 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 92 insertions(+), 8 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 92284692..e02d3ea2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -299,6 +299,7 @@ "undoc", "Unifont", "unraisablehook", + "unraiseable", "upscaling", "VAFUNC", "vcoef", diff --git a/tcod/sdl/audio.py b/tcod/sdl/audio.py index 4edfe974..c483fd15 100644 --- a/tcod/sdl/audio.py +++ b/tcod/sdl/audio.py @@ -1,5 +1,6 @@ from __future__ import annotations +import enum import sys import threading import time @@ -7,6 +8,7 @@ import numpy as np from numpy.typing import ArrayLike, DTypeLike, NDArray +from typing_extensions import Literal import tcod.sdl.sys from tcod.loader import ffi, lib @@ -70,7 +72,20 @@ def __init__( self.silence = int(spec.silence) self.samples = int(spec.samples) self.buffer_size = int(spec.size) - self._callback = self.__default_callback + self._handle: Optional[Any] = None + self._callback: Callable[[AudioDevice, NDArray[Any]], None] = self.__default_callback + + @property + def callback(self) -> Callable[[AudioDevice, NDArray[Any]], None]: + if self._handle is None: + raise TypeError("This AudioDevice was opened without a callback.") + return self._callback + + @callback.setter + def callback(self, new_callback: Callable[[AudioDevice, NDArray[Any]], None]) -> None: + if self._handle is None: + raise TypeError("This AudioDevice was opened without a callback.") + self._callback = new_callback @property def _sample_size(self) -> int: @@ -135,8 +150,9 @@ def close(self) -> None: lib.SDL_CloseAudioDevice(self.device_id) self.device_id = 0 - def __default_callback(self, stream: NDArray[Any]) -> None: - stream[...] = self.silence + @staticmethod + def __default_callback(device: AudioDevice, stream: NDArray[Any]) -> None: + stream[...] = device.silence class Channel: @@ -249,13 +265,17 @@ def on_stream(self, stream: NDArray[Any]) -> None: channel._on_mix(stream) +class _AudioCallbackUserdata: + device: AudioDevice + + @ffi.def_extern() # type: ignore def _sdl_audio_callback(userdata: Any, stream: Any, length: int) -> None: """Handle audio device callbacks.""" - device: Optional[AudioDevice] = ffi.from_handle(userdata)() - assert device is not None + data: _AudioCallbackUserdata = ffi.from_handle(userdata)() + device = data.device buffer = np.frombuffer(ffi.buffer(stream, length), dtype=device.format).reshape(-1, device.channels) - device._callback(buffer) + device._callback(device, buffer) def _get_devices(capture: bool) -> Iterator[str]: @@ -276,6 +296,23 @@ def get_capture_devices() -> Iterator[str]: yield from _get_devices(capture=True) +class AllowedChanges(enum.IntFlag): + """Which parameters are allowed to be changed when the values given are not supported.""" + + NONE = 0 + """""" + FREQUENCY = 0x01 + """""" + FORMAT = 0x02 + """""" + CHANNELS = 0x04 + """""" + SAMPLES = 0x08 + """""" + ANY = FREQUENCY | FORMAT | CHANNELS | SAMPLES + """""" + + def open( name: Optional[str] = None, capture: bool = False, @@ -284,10 +321,43 @@ def open( format: DTypeLike = np.float32, channels: int = 2, samples: int = 0, - allowed_changes: int = 0, + allowed_changes: AllowedChanges = AllowedChanges.NONE, paused: bool = False, + callback: Union[None, Literal[True], Callable[[AudioDevice, NDArray[Any]], None]] = None, ) -> AudioDevice: - """Open an audio device for playback or capture.""" + """Open an audio device for playback or capture and return it. + + Args: + name: The name of the device to open, or None for the most reasonable default. + capture: True if this is a recording device, or False if this is an output device. + frequency: The desired sample rate to open the device with. + format: The data format to use for samples as a NumPy dtype. + channels: The number of speakers for the device. 1, 2, 4, or 6 are typical options. + samples: The desired size of the audio buffer, must be a power of two. + allowed_changes: + By default if the hardware does not support the desired format than SDL will transparently convert between + formats for you. + Otherwise you can specify which parameters are allowed to be changed to fit the hardware better. + paused: + If True then the device will begin in a paused state. + It can then be unpaused by assigning False to :any:`AudioDevice.paused`. + callback: + If None then this device will be opened in push mode and you'll have to use :any:`AudioDevice.queue_audio` + to send audio data or :any:`AudioDevice.dequeue_audio` to receive it. + If a callback is given then you can change it later, but you can not enable or disable the callback on an + opened device. + If True then a default callback which plays silence will be used, this is useful if you need the audio + device before your callback is ready. + + If a callback is given then it will be called with the `AudioDevice` and a Numpy buffer of the data stream. + This callback will be run on a separate thread. + Exceptions not handled by the callback become unraiseable and will be handled by :any:`sys.unraisablehook`. + + .. seealso:: + https://wiki.libsdl.org/SDL_AudioSpec + https://wiki.libsdl.org/SDL_OpenAudioDevice + + """ tcod.sdl.sys.init(tcod.sdl.sys.Subsystem.AUDIO) desired = ffi.new( "SDL_AudioSpec*", @@ -300,6 +370,14 @@ def open( "userdata": ffi.NULL, }, ) + callback_data = _AudioCallbackUserdata() + if callback is not None: + handle = ffi.new_handle(callback_data) + desired.callback = lib._sdl_audio_callback + desired.userdata = handle + else: + handle = None + obtained = ffi.new("SDL_AudioSpec*") device_id: int = lib.SDL_OpenAudioDevice( ffi.NULL if name is None else name.encode("utf-8"), @@ -310,5 +388,10 @@ def open( ) assert device_id >= 0, _get_error() device = AudioDevice(device_id, capture, obtained) + if callback is not None: + callback_data.device = device + device._handle = handle + if callback is not True: + device._callback = callback device.paused = paused return device From e0da84a3554d35607fb00f90b3a83815d0bb788f Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 10 Feb 2022 18:27:49 -0800 Subject: [PATCH 053/194] Update loose unreleased tags with the release script. --- scripts/tag_release.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/scripts/tag_release.py b/scripts/tag_release.py index 4f900ba2..1479ec82 100644 --- a/scripts/tag_release.py +++ b/scripts/tag_release.py @@ -3,12 +3,15 @@ import argparse import datetime +import os import re import subprocess import sys from pathlib import Path from typing import Tuple +PROJECT_DIR = Path(__file__).parent.parent + parser = argparse.ArgumentParser(description="Tags and releases the next version of this project.") parser.add_argument("tag", help="Semantic version number to use as the tag.") @@ -24,7 +27,7 @@ def parse_changelog(args: argparse.Namespace) -> Tuple[str, str]: """Return an updated changelog and and the list of changes.""" match = re.match( pattern=r"(.*?## \[Unreleased]\n)(.+?\n)(\n*## \[.*)", - string=Path("CHANGELOG.md").read_text(encoding="utf-8"), + string=(PROJECT_DIR / "CHANGELOG.md").read_text(encoding="utf-8"), flags=re.DOTALL, ) assert match @@ -41,6 +44,23 @@ def parse_changelog(args: argparse.Namespace) -> Tuple[str, str]: return "".join((header, tagged, tail)), changes +def replace_unreleased_tags(tag: str, dry_run: bool) -> None: + match = re.match(r"\d+\.\d+", tag) + assert match + short_tag = match.group() + for directory, _, files in os.walk(PROJECT_DIR / "tcod"): + for filename in files: + file = Path(directory, filename) + if file.suffix != ".py": + continue + text = file.read_text(encoding="utf-8") + new_text = re.sub(r":: unreleased", rf":: {short_tag}", text) + if text != new_text: + print(f"Update tags in {file}") + if not dry_run: + file.write_text(new_text, encoding="utf-8") + + def main() -> None: if len(sys.argv) == 1: parser.print_help(sys.stderr) @@ -54,8 +74,10 @@ def main() -> None: print("--- New changelog:") print(new_changelog) + replace_unreleased_tags(args.tag, args.dry_run) + if not args.dry_run: - Path("CHANGELOG.md").write_text(new_changelog, encoding="utf-8") + (PROJECT_DIR / "CHANGELOG.md").write_text(new_changelog, encoding="utf-8") edit = ["-e"] if args.edit else [] subprocess.check_call(["git", "commit", "-avm", "Prepare %s release." % args.tag] + edit) subprocess.check_call(["git", "tag", args.tag, "-am", "%s\n\n%s" % (args.tag, changes)] + edit) From b0695329c37d118ca804438fc226a4865fe7e103 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 10 Feb 2022 18:28:31 -0800 Subject: [PATCH 054/194] Document the SDL audio module. --- docs/index.rst | 1 + docs/sdl/audio.rst | 5 ++++ tcod/sdl/audio.py | 62 ++++++++++++++++++++++++++++++++-------------- tcod/sdl/sys.py | 26 +++++++++---------- 4 files changed, 63 insertions(+), 31 deletions(-) create mode 100644 docs/sdl/audio.rst diff --git a/docs/index.rst b/docs/index.rst index 3638a87c..981ce6bf 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -39,6 +39,7 @@ Contents: tcod/render tcod/tileset libtcodpy + sdl/audio sdl/render sdl/mouse sdl/video diff --git a/docs/sdl/audio.rst b/docs/sdl/audio.rst new file mode 100644 index 00000000..702bd470 --- /dev/null +++ b/docs/sdl/audio.rst @@ -0,0 +1,5 @@ +tcod.sdl.audio - SDL Audio +========================== + +.. automodule:: tcod.sdl.audio + :members: diff --git a/tcod/sdl/audio.py b/tcod/sdl/audio.py index c483fd15..5edff741 100644 --- a/tcod/sdl/audio.py +++ b/tcod/sdl/audio.py @@ -1,3 +1,7 @@ +"""SDL2 audio playback and recording tools. + +.. versionadded:: unreleased +""" from __future__ import annotations import enum @@ -8,7 +12,7 @@ import numpy as np from numpy.typing import ArrayLike, DTypeLike, NDArray -from typing_extensions import Literal +from typing_extensions import Final, Literal import tcod.sdl.sys from tcod.loader import ffi, lib @@ -52,7 +56,10 @@ def _dtype_from_format(format: int) -> np.dtype[Any]: class AudioDevice: - """An SDL audio device.""" + """An SDL audio device. + + Open new audio devices using :any:`tcod.sdl.audio.open`. + """ def __init__( self, @@ -63,20 +70,30 @@ def __init__( assert device_id >= 0 assert ffi.typeof(spec) is ffi.typeof("SDL_AudioSpec*") assert spec - self.device_id = device_id - self.spec = spec - self.frequency = spec.freq - self.is_capture = capture - self.format = _dtype_from_format(spec.format) - self.channels = int(spec.channels) - self.silence = int(spec.silence) - self.samples = int(spec.samples) - self.buffer_size = int(spec.size) + self.device_id: Final[int] = device_id + """The SDL device identifier used for SDL C functions.""" + self.spec: Final[Any] = spec + """The SDL_AudioSpec as a CFFI object.""" + self.frequency: Final[int] = spec.freq + """The audio device sound frequency.""" + self.is_capture: Final[bool] = capture + """True if this is a recording device instead of an output device.""" + self.format: Final[np.dtype[Any]] = _dtype_from_format(spec.format) + """The format used for audio samples with this device.""" + self.channels: Final[int] = int(spec.channels) + """The number of audio channels for this device.""" + self.silence: float = int(spec.silence) + """The value of silence, according to SDL.""" + self.buffer_samples: Final[int] = int(spec.samples) + """The size of the audio buffer in samples.""" + self.buffer_bytes: Final[int] = int(spec.size) + """The size of the audio buffer in bytes.""" self._handle: Optional[Any] = None self._callback: Callable[[AudioDevice, NDArray[Any]], None] = self.__default_callback @property def callback(self) -> Callable[[AudioDevice, NDArray[Any]], None]: + """If the device was opened with a callback enabled, then you may get or set the callback with this attribute.""" if self._handle is None: raise TypeError("This AudioDevice was opened without a callback.") return self._callback @@ -89,6 +106,7 @@ def callback(self, new_callback: Callable[[AudioDevice, NDArray[Any]], None]) -> @property def _sample_size(self) -> int: + """The size of a sample in bytes.""" return self.format.itemsize * self.channels @property @@ -119,9 +137,15 @@ def _convert_array(self, samples_: ArrayLike) -> NDArray[Any]: return np.ascontiguousarray(np.broadcast_to(samples, (samples.shape[0], self.channels)), dtype=self.format) @property - def queued_audio_bytes(self) -> int: + def _queued_bytes(self) -> int: + """The current amount of bytes remaining in the audio queue.""" return int(lib.SDL_GetQueuedAudioSize(self.device_id)) + @property + def queued_samples(self) -> int: + """The current amount of samples remaining in the audio queue.""" + return self._queued_bytes // self._sample_size + def queue_audio(self, samples: ArrayLike) -> None: """Append audio samples to the audio data queue.""" assert not self.is_capture @@ -132,7 +156,7 @@ def queue_audio(self, samples: ArrayLike) -> None: def dequeue_audio(self) -> NDArray[Any]: """Return the audio buffer from a capture stream.""" assert self.is_capture - out_samples = self.queued_audio_bytes // self._sample_size + out_samples = self._queued_bytes // self._sample_size out = np.empty((out_samples, self.channels), self.format) buffer = ffi.from_buffer(out) bytes_returned = lib.SDL_DequeueAudio(self.device_id, buffer, len(buffer)) @@ -144,11 +168,11 @@ def __del__(self) -> None: self.close() def close(self) -> None: - """Close this audio device.""" - if not self.device_id: + """Close this audio device. Using this object after it has been closed is invalid.""" + if not hasattr(self, "device_id"): return lib.SDL_CloseAudioDevice(self.device_id) - self.device_id = 0 + del self.device_id @staticmethod def __default_callback(device: AudioDevice, stream: NDArray[Any]) -> None: @@ -218,9 +242,11 @@ def __init__(self, device: AudioDevice): self.device = device def run(self) -> None: - buffer = np.full((self.device.samples, self.device.channels), self.device.silence, dtype=self.device.format) + buffer = np.full( + (self.device.buffer_samples, self.device.channels), self.device.silence, dtype=self.device.format + ) while True: - if self.device.queued_audio_bytes > 0: + if self.device._queued_bytes > 0: time.sleep(0.001) continue self.on_stream(buffer) diff --git a/tcod/sdl/sys.py b/tcod/sdl/sys.py index 34f5d4fc..7987081d 100644 --- a/tcod/sdl/sys.py +++ b/tcod/sdl/sys.py @@ -9,14 +9,14 @@ class Subsystem(enum.IntFlag): - TIMER = lib.SDL_INIT_TIMER or 0x00000001 - AUDIO = lib.SDL_INIT_AUDIO or 0x00000010 - VIDEO = lib.SDL_INIT_VIDEO or 0x00000020 - JOYSTICK = lib.SDL_INIT_JOYSTICK or 0x00000200 - HAPTIC = lib.SDL_INIT_HAPTIC or 0x00001000 - GAMECONTROLLER = lib.SDL_INIT_GAMECONTROLLER or 0x00002000 - EVENTS = lib.SDL_INIT_EVENTS or 0x00004000 - SENSOR = getattr(lib, "SDL_INIT_SENSOR", None) or 0x00008000 # SDL >= 2.0.9 + TIMER = 0x00000001 + AUDIO = 0x00000010 + VIDEO = 0x00000020 + JOYSTICK = 0x00000200 + HAPTIC = 0x00001000 + GAMECONTROLLER = 0x00002000 + EVENTS = 0x00004000 + SENSOR = 0x00008000 EVERYTHING = lib.SDL_INIT_EVERYTHING or 0 @@ -49,11 +49,11 @@ def __exit__(self, *args: Any) -> None: class _PowerState(enum.IntEnum): - UNKNOWN = getattr(lib, "SDL_POWERSTATE_UNKNOWN", 0) - ON_BATTERY = getattr(lib, "SDL_POWERSTATE_ON_BATTERY", 0) - NO_BATTERY = getattr(lib, "SDL_POWERSTATE_NO_BATTERY", 0) - CHARGING = getattr(lib, "SDL_POWERSTATE_CHARGING", 0) - CHARGED = getattr(lib, "SDL_POWERSTATE_CHARGED", 0) + UNKNOWN = 0 + ON_BATTERY = enum.auto() + NO_BATTERY = enum.auto() + CHARGING = enum.auto() + CHARGED = enum.auto() def _get_power_info() -> Tuple[_PowerState, int, int]: From f78fe39181182cf60b325801f99151bcf056701e Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 11 Feb 2022 02:19:41 -0800 Subject: [PATCH 055/194] Add minimap to tcod samples. Update changelog. Add context atlas access. Refactor Texture attributes. --- .vscode/launch.json | 6 ++-- CHANGELOG.md | 10 +++++-- examples/samples_tcod.py | 64 ++++++++++++++++++++++++++++++---------- tcod/context.py | 12 ++++++++ tcod/render.py | 10 ++++++- tcod/sdl/render.py | 58 ++++++++++++++---------------------- tcod/tileset.py | 8 ++++- 7 files changed, 108 insertions(+), 60 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 4bd84cbe..c9892886 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,8 +9,7 @@ "type": "python", "request": "launch", "program": "${file}", - "console": "integratedTerminal", - "preLaunchTask": "develop python-tcod", + "console": "internalConsole", }, { // Run the Python samples. @@ -20,8 +19,7 @@ "request": "launch", "program": "${workspaceFolder}/examples/samples_tcod.py", "cwd": "${workspaceFolder}/examples", - "console": "integratedTerminal", - "preLaunchTask": "develop python-tcod", + "console": "internalConsole", }, { "name": "Python: Run tests", diff --git a/CHANGELOG.md b/CHANGELOG.md index fef7ea70..595c8e05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,19 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] +### Added +- `tcod.sdl.audio`, a new module exposing SDL audio devices. This does not include an audio mixer yet. +- `tcod.sdl.mouse`, for SDL mouse and cursor handing. +- `Context.sdl_atlas`, which provides the relevant `SDLTilesetAtlas` when one is being used by the context. +- Several missing features were added to `tcod.sdl.render`. +- `Window.mouse_rect` added to SDL windows to set the mouse confinement area. ### Changed - `Texture.access` and `Texture.blend_mode` properties now return enum instances. - You can still set them with `int` but Mypy will complain. + You can still set `blend_mode` with `int` but Mypy will complain. ## [13.4.0] - 2022-02-04 ### Added -- Adds `sdl_window` and `sdl_renderer` to tcod contexts. +- Adds `sdl_window` and `sdl_renderer` properties to tcod contexts. - Adds `tcod.event.add_watch` and `tcod.event.remove_watch` to handle SDL events via callback. - Adds the `tcod.sdl.video` module to handle SDL windows. - Adds the `tcod.sdl.render` module to handle SDL renderers. diff --git a/examples/samples_tcod.py b/examples/samples_tcod.py index 8eba7149..f92d0151 100755 --- a/examples/samples_tcod.py +++ b/examples/samples_tcod.py @@ -19,6 +19,7 @@ import numpy as np import tcod import tcod.render +import tcod.sdl.render from numpy.typing import NDArray if not sys.warnoptions: @@ -50,6 +51,8 @@ def get_data(path: str) -> str: # Mutable global names. context: tcod.context.Context tileset: tcod.tileset.Tileset +console_render: tcod.render.SDLConsoleRender # Optional SDL renderer. +sample_minimap: tcod.sdl.render.Texture # Optional minimap texture. root_console = tcod.Console(80, 50, order="F") sample_console = tcod.console.Console(SAMPLE_SCREEN_WIDTH, SAMPLE_SCREEN_HEIGHT, order="F") cur_sample = 0 # Current selected sample. @@ -68,7 +71,7 @@ def on_draw(self) -> None: pass def ev_keydown(self, event: tcod.event.KeyDown) -> None: - global cur_sample, context + global cur_sample if event.sym == tcod.event.K_DOWN: cur_sample = (cur_sample + 1) % len(SAMPLES) SAMPLES[cur_sample].on_enter() @@ -91,8 +94,7 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> None: raise SystemExit() elif event.sym in RENDERER_KEYS: # Swap the active context for one with a different renderer. - context.close() - context = init_context(RENDERER_KEYS[event.sym]) + init_context(RENDERER_KEYS[event.sym]) def ev_quit(self, event: tcod.event.Quit) -> None: raise SystemExit() @@ -541,7 +543,7 @@ def __init__(self) -> None: self.player_y = 10 self.torch = False self.light_walls = True - self.algo_num = 0 + self.algo_num = tcod.FOV_SYMMETRIC_SHADOWCAST self.noise = tcod.noise.Noise(1) # 1D noise for the torch flickering. map_shape = (SAMPLE_SCREEN_WIDTH, SAMPLE_SCREEN_HEIGHT) @@ -582,7 +584,7 @@ def on_draw(self) -> None: self.draw_ui() sample_console.print(self.player_x, self.player_y, "@") # Draw windows. - sample_console.tiles_rgb["ch"][SAMPLE_MAP == "="] = tcod.CHAR_DHLINE + sample_console.tiles_rgb["ch"][SAMPLE_MAP == "="] = 0x2550 # BOX DRAWINGS DOUBLE HORIZONTAL sample_console.tiles_rgb["fg"][SAMPLE_MAP == "="] = BLACK # Get a 2D boolean array of visible cells. @@ -1394,17 +1396,20 @@ def on_draw(self) -> None: ) -def init_context(renderer: int) -> tcod.context.Context: - """Return a new context with common parameters set. +def init_context(renderer: int) -> None: + """Setup or reset a global context with common parameters set. This function exists to more easily switch between renderers. """ + global context, console_render, sample_minimap + if "context" in globals(): + context.close() libtcod_version = "%i.%i.%i" % ( tcod.lib.TCOD_MAJOR_VERSION, tcod.lib.TCOD_MINOR_VERSION, tcod.lib.TCOD_PATCHLEVEL, ) - return tcod.context.new( + context = tcod.context.new( columns=root_console.width, rows=root_console.height, title=f"python-tcod samples" f" (python-tcod {tcod.__version__}, libtcod {libtcod_version})", @@ -1412,16 +1417,27 @@ def init_context(renderer: int) -> tcod.context.Context: vsync=False, # VSync turned off since this is for benchmarking. tileset=tileset, ) + if context.sdl_renderer: # If this context supports SDL rendering. + # Start by setting the logical size so that window resizing doesn't break anything. + context.sdl_renderer.logical_size = ( + tileset.tile_width * root_console.width, + tileset.tile_height * root_console.height, + ) + assert context.sdl_atlas + # Generate the console renderer and minimap. + console_render = tcod.render.SDLConsoleRender(context.sdl_atlas) + sample_minimap = context.sdl_renderer.new_texture( + SAMPLE_SCREEN_WIDTH, + SAMPLE_SCREEN_HEIGHT, + format=tcod.lib.SDL_PIXELFORMAT_RGB24, + access=tcod.sdl.render.TextureAccess.STREAMING, # Updated every frame. + ) def main() -> None: global context, tileset tileset = tcod.tileset.load_tilesheet(FONT, 32, 8, tcod.tileset.CHARMAP_TCOD) - context = init_context(tcod.RENDERER_SDL2) - sdl_renderer = context.sdl_renderer - assert sdl_renderer - atlas = tcod.render.SDLTilesetAtlas(sdl_renderer, tileset) - console_render = tcod.render.SDLConsoleRender(atlas) + init_context(tcod.RENDERER_SDL2) try: SAMPLES[cur_sample].on_enter() @@ -1434,9 +1450,25 @@ def main() -> None: SAMPLES[cur_sample].on_draw() sample_console.blit(root_console, SAMPLE_SCREEN_X, SAMPLE_SCREEN_Y) draw_stats() - # context.present(root_console) - sdl_renderer.copy(console_render.render(root_console)) - sdl_renderer.present() + if context.sdl_renderer: + # SDL renderer support, upload the sample console background to a minimap texture. + sample_minimap.update(sample_console.rgb.T["bg"]) + # Render the root_console normally, this is the drawing step of context.present without presenting. + context.sdl_renderer.copy(console_render.render(root_console)) + # Render the minimap to the screen. + context.sdl_renderer.copy( + sample_minimap, + dest=( + tileset.tile_width * 24, + tileset.tile_height * 36, + SAMPLE_SCREEN_WIDTH * 3, + SAMPLE_SCREEN_HEIGHT * 3, + ), + ) + context.sdl_renderer.present() + else: # No SDL renderer, just use plain context rendering. + context.present(root_console) + handle_time() handle_events() finally: diff --git a/tcod/context.py b/tcod/context.py index 14c81f6f..b26627e1 100644 --- a/tcod/context.py +++ b/tcod/context.py @@ -58,6 +58,7 @@ import tcod import tcod.event +import tcod.render import tcod.sdl.render import tcod.sdl.video import tcod.tileset @@ -371,6 +372,17 @@ def sdl_renderer(self) -> Optional[tcod.sdl.render.Renderer]: p = lib.TCOD_context_get_sdl_renderer(self._context_p) return tcod.sdl.render.Renderer(p) if p else None + @property + def sdl_atlas(self) -> Optional[tcod.render.SDLTilesetAtlas]: + """Return a :any:`tcod.render.SDLTilesetAtlas` referencing libtcod's SDL texture atlas if it exists. + + .. versionadded:: unreleased + """ + if self._context_p.type not in (lib.TCOD_RENDERER_SDL, lib.TCOD_RENDERER_SDL2): + return None + context_data = ffi.cast("struct TCOD_RendererSDL2*", self._context_p.contextdata_) + return tcod.render.SDLTilesetAtlas._from_ref(context_data.renderer, context_data.atlas) + def __reduce__(self) -> NoReturn: """Contexts can not be pickled, so this class will raise :class:`pickle.PicklingError`. diff --git a/tcod/render.py b/tcod/render.py index 9e2918da..79dc5060 100644 --- a/tcod/render.py +++ b/tcod/render.py @@ -29,7 +29,7 @@ from __future__ import annotations -from typing import Optional +from typing import Any, Optional import tcod.console import tcod.sdl.render @@ -46,6 +46,14 @@ def __init__(self, renderer: tcod.sdl.render.Renderer, tileset: tcod.tileset.Til self.tileset = tileset self.p = ffi.gc(_check_p(lib.TCOD_sdl2_atlas_new(renderer.p, tileset._tileset_p)), lib.TCOD_sdl2_atlas_delete) + @classmethod + def _from_ref(cls, renderer_p: Any, atlas_p: Any) -> SDLTilesetAtlas: + self = object.__new__(cls) + self._renderer = tcod.sdl.render.Renderer(renderer_p) + self.tileset = tcod.tileset.Tileset._from_ref(atlas_p.tileset) + self.p = atlas_p + return self + class SDLConsoleRender: """Holds an internal cache console and texture which are used to optimized console rendering.""" diff --git a/tcod/sdl/render.py b/tcod/sdl/render.py index 98017772..15ccb931 100644 --- a/tcod/sdl/render.py +++ b/tcod/sdl/render.py @@ -9,6 +9,7 @@ import numpy as np from numpy.typing import NDArray +from typing_extensions import Final import tcod.sdl.video from tcod.loader import ffi, lib @@ -142,11 +143,27 @@ def compose_blend_mode( class Texture: - """SDL hardware textures.""" + """SDL hardware textures. + + Create a new texture using :any:`Renderer.new_texture` or :any:`Renderer.upload_texture`. + """ def __init__(self, sdl_texture_p: Any, sdl_renderer_p: Any = None) -> None: self.p = sdl_texture_p self._sdl_renderer_p = sdl_renderer_p # Keep alive. + query = self._query() + self.format: Final[int] = query[0] + """Texture format, read only.""" + self.access: Final[TextureAccess] = TextureAccess(query[1]) + """Texture access mode, read only. + + .. versionchanged:: unreleased + Attribute is now a :any:`TextureAccess` value. + """ + self.width: Final[int] = query[2] + """Texture pixel width, read only.""" + self.height: Final[int] = query[3] + """Texture pixel height, read only.""" def __eq__(self, other: Any) -> bool: return bool(self.p == getattr(other, "p", None)) @@ -156,7 +173,7 @@ def _query(self) -> Tuple[int, int, int, int]: format = ffi.new("uint32_t*") buffer = ffi.new("int[3]") lib.SDL_QueryTexture(self.p, format, buffer, buffer + 1, buffer + 2) - return int(format), int(buffer[0]), int(buffer[1]), int(buffer[2]) + return int(format[0]), int(buffer[0]), int(buffer[1]), int(buffer[2]) def update(self, pixels: NDArray[Any], rect: Optional[Tuple[int, int, int, int]] = None) -> None: """Update the pixel data of this texture. @@ -165,42 +182,11 @@ def update(self, pixels: NDArray[Any], rect: Optional[Tuple[int, int, int, int]] """ if rect is None: rect = (0, 0, self.width, self.height) - assert pixels.shape[:2] == rect[3], rect[2] - assert pixels[0].flags.c_contiguous + assert pixels.shape[:2] == (self.height, self.width) + if not pixels[0].flags.c_contiguous: + pixels = np.ascontiguousarray(pixels) _check(lib.SDL_UpdateTexture(self.p, (rect,), ffi.cast("void*", pixels.ctypes.data), pixels.strides[0])) - @property - def format(self) -> int: - """Texture format, read only.""" - buffer = ffi.new("uint32_t*") - lib.SDL_QueryTexture(self.p, buffer, ffi.NULL, ffi.NULL, ffi.NULL) - return int(buffer[0]) - - @property - def access(self) -> TextureAccess: - """Texture access mode, read only. - - .. versionchanged:: unreleased - Property now returns a TextureAccess instance. - """ - buffer = ffi.new("int*") - lib.SDL_QueryTexture(self.p, ffi.NULL, buffer, ffi.NULL, ffi.NULL) - return TextureAccess(buffer[0]) - - @property - def width(self) -> int: - """Texture pixel width, read only.""" - buffer = ffi.new("int*") - lib.SDL_QueryTexture(self.p, ffi.NULL, ffi.NULL, buffer, ffi.NULL) - return int(buffer[0]) - - @property - def height(self) -> int: - """Texture pixel height, read only.""" - buffer = ffi.new("int*") - lib.SDL_QueryTexture(self.p, ffi.NULL, ffi.NULL, ffi.NULL, buffer) - return int(buffer[0]) - @property def alpha_mod(self) -> int: """Texture alpha modulate value, can be set to 0 - 255.""" diff --git a/tcod/tileset.py b/tcod/tileset.py index acff8176..14767e5b 100644 --- a/tcod/tileset.py +++ b/tcod/tileset.py @@ -40,12 +40,18 @@ def __init__(self, tile_width: int, tile_height: int) -> None: @classmethod def _claim(cls, cdata: Any) -> Tileset: """Return a new Tileset that owns the provided TCOD_Tileset* object.""" - self: Tileset = object.__new__(cls) + self = object.__new__(cls) if cdata == ffi.NULL: raise RuntimeError("Tileset initialized with nullptr.") self._tileset_p = ffi.gc(cdata, lib.TCOD_tileset_delete) return self + @classmethod + def _from_ref(cls, tileset_p: Any) -> Tileset: + self = object.__new__(cls) + self._tileset_p = tileset_p + return self + @property def tile_width(self) -> int: """The width of the tile in pixels.""" From 1efd2631b806e7fc511ca9806a824d8d060c767b Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 11 Feb 2022 15:58:04 -0800 Subject: [PATCH 056/194] Prepare 13.5.0 release. --- CHANGELOG.md | 2 ++ tcod/context.py | 2 +- tcod/sdl/audio.py | 2 +- tcod/sdl/mouse.py | 2 +- tcod/sdl/render.py | 56 +++++++++++++++++++++++----------------------- tcod/sdl/video.py | 2 +- 6 files changed, 34 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 595c8e05..b05c8459 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] + +## [13.5.0] - 2022-02-11 ### Added - `tcod.sdl.audio`, a new module exposing SDL audio devices. This does not include an audio mixer yet. - `tcod.sdl.mouse`, for SDL mouse and cursor handing. diff --git a/tcod/context.py b/tcod/context.py index b26627e1..8b324b83 100644 --- a/tcod/context.py +++ b/tcod/context.py @@ -376,7 +376,7 @@ def sdl_renderer(self) -> Optional[tcod.sdl.render.Renderer]: def sdl_atlas(self) -> Optional[tcod.render.SDLTilesetAtlas]: """Return a :any:`tcod.render.SDLTilesetAtlas` referencing libtcod's SDL texture atlas if it exists. - .. versionadded:: unreleased + .. versionadded:: 13.5 """ if self._context_p.type not in (lib.TCOD_RENDERER_SDL, lib.TCOD_RENDERER_SDL2): return None diff --git a/tcod/sdl/audio.py b/tcod/sdl/audio.py index 5edff741..cbc07b3d 100644 --- a/tcod/sdl/audio.py +++ b/tcod/sdl/audio.py @@ -1,6 +1,6 @@ """SDL2 audio playback and recording tools. -.. versionadded:: unreleased +.. versionadded:: 13.5 """ from __future__ import annotations diff --git a/tcod/sdl/mouse.py b/tcod/sdl/mouse.py index aa174ad9..13349ceb 100644 --- a/tcod/sdl/mouse.py +++ b/tcod/sdl/mouse.py @@ -1,6 +1,6 @@ """SDL mouse and cursor functions. -.. versionadded:: unreleased +.. versionadded:: 13.5 """ from __future__ import annotations diff --git a/tcod/sdl/render.py b/tcod/sdl/render.py index 15ccb931..f44acb6a 100644 --- a/tcod/sdl/render.py +++ b/tcod/sdl/render.py @@ -45,7 +45,7 @@ class BlendFactor(enum.IntEnum): :any:`compose_blend_mode` https://wiki.libsdl.org/SDL_BlendFactor - .. versionadded:: unreleased + .. versionadded:: 13.5 """ ZERO = 0x1 @@ -77,7 +77,7 @@ class BlendOperation(enum.IntEnum): :any:`compose_blend_mode` https://wiki.libsdl.org/SDL_BlendOperation - .. versionadded:: unreleased + .. versionadded:: 13.5 """ ADD = 0x1 @@ -100,7 +100,7 @@ class BlendMode(enum.IntEnum): :any:`Renderer.draw_blend_mode` :any:`compose_blend_mode` - .. versionadded:: unreleased + .. versionadded:: 13.5 """ NONE = 0x00000000 @@ -128,7 +128,7 @@ def compose_blend_mode( .. seealso:: https://wiki.libsdl.org/SDL_ComposeCustomBlendMode - .. versionadded:: unreleased + .. versionadded:: 13.5 """ return BlendMode( lib.SDL_ComposeCustomBlendMode( @@ -157,7 +157,7 @@ def __init__(self, sdl_texture_p: Any, sdl_renderer_p: Any = None) -> None: self.access: Final[TextureAccess] = TextureAccess(query[1]) """Texture access mode, read only. - .. versionchanged:: unreleased + .. versionchanged:: 13.5 Attribute is now a :any:`TextureAccess` value. """ self.width: Final[int] = query[2] @@ -178,7 +178,7 @@ def _query(self) -> Tuple[int, int, int, int]: def update(self, pixels: NDArray[Any], rect: Optional[Tuple[int, int, int, int]] = None) -> None: """Update the pixel data of this texture. - .. versionadded:: unreleased + .. versionadded:: 13.5 """ if rect is None: rect = (0, 0, self.width, self.height) @@ -202,7 +202,7 @@ def alpha_mod(self, value: int) -> None: def blend_mode(self) -> BlendMode: """Texture blend mode, can be set. - .. versionchanged:: unreleased + .. versionchanged:: 13.5 Property now returns a BlendMode instance. """ out = ffi.new("SDL_BlendMode*") @@ -271,7 +271,7 @@ def copy( center: The (x, y) point where rotation is applied. If None then the center of `dest` is used. flip: Flips the `texture` when drawing it. - .. versionchanged:: unreleased + .. versionchanged:: 13.5 `source` and `dest` can now be float tuples. Added the `angle`, `center`, and `flip` parameters. """ @@ -349,7 +349,7 @@ def upload_texture( def draw_color(self) -> Tuple[int, int, int, int]: """Get or set the active RGBA draw color for this renderer. - .. versionadded:: unreleased + .. versionadded:: 13.5 """ rgba = ffi.new("uint8_t[4]") _check(lib.SDL_GetRenderDrawColor(self.p, rgba, rgba + 1, rgba + 2, rgba + 3)) @@ -363,7 +363,7 @@ def draw_color(self, rgba: Tuple[int, int, int, int]) -> None: def draw_blend_mode(self) -> BlendMode: """Get or set the active blend mode of this renderer. - .. versionadded:: unreleased + .. versionadded:: 13.5 """ out = ffi.new("SDL_BlendMode*") _check(lib.SDL_GetRenderDrawBlendMode(self.p, out)) @@ -380,7 +380,7 @@ def output_size(self) -> Tuple[int, int]: .. seealso:: https://wiki.libsdl.org/SDL_GetRendererOutputSize - .. versionadded:: unreleased + .. versionadded:: 13.5 """ out = ffi.new("int[2]") _check(lib.SDL_GetRendererOutputSize(self.p, out, out + 1)) @@ -392,7 +392,7 @@ def clip_rect(self) -> Optional[Tuple[int, int, int, int]]: Set to None to disable clipping. - .. versionadded:: unreleased + .. versionadded:: 13.5 """ if not lib.SDL_RenderIsClipEnabled(self.p): return None @@ -412,7 +412,7 @@ def integer_scaling(self) -> bool: .. seealso:: https://wiki.libsdl.org/SDL_RenderSetIntegerScale - .. versionadded:: unreleased + .. versionadded:: 13.5 """ return bool(lib.SDL_RenderGetIntegerScale(self.p)) @@ -429,7 +429,7 @@ def logical_size(self) -> Tuple[int, int]: .. seealso:: https://wiki.libsdl.org/SDL_RenderSetLogicalSize - .. versionadded:: unreleased + .. versionadded:: 13.5 """ out = ffi.new("int[2]") lib.SDL_RenderGetLogicalSize(self.p, out, out + 1) @@ -446,7 +446,7 @@ def scale(self) -> Tuple[float, float]: .. seealso:: https://wiki.libsdl.org/SDL_RenderSetScale - .. versionadded:: unreleased + .. versionadded:: 13.5 """ out = ffi.new("float[2]") lib.SDL_RenderGetScale(self.p, out, out + 1) @@ -463,7 +463,7 @@ def viewport(self) -> Optional[Tuple[int, int, int, int]]: .. seealso:: https://wiki.libsdl.org/SDL_RenderSetViewport - .. versionadded:: unreleased + .. versionadded:: 13.5 """ rect = ffi.new("SDL_Rect*") lib.SDL_RenderGetViewport(self.p, rect) @@ -477,7 +477,7 @@ def viewport(self, rect: Optional[Tuple[int, int, int, int]]) -> None: def set_vsync(self, enable: bool) -> None: """Enable or disable VSync for this renderer. - .. versionadded:: unreleased + .. versionadded:: 13.5 """ _check(lib.SDL_RenderSetVSync(self.p, enable)) @@ -489,7 +489,7 @@ def read_pixels( out: Optional[NDArray[Any]] = None, ) -> NDArray[Any]: """ - .. versionadded:: unreleased + .. versionadded:: 13.5 """ if format is None: format = lib.SDL_PIXELFORMAT_RGBA32 @@ -516,41 +516,41 @@ def read_pixels( def clear(self) -> None: """Clear the current render target with :any:`draw_color`. - .. versionadded:: unreleased + .. versionadded:: 13.5 """ _check(lib.SDL_RenderClear(self.p)) def fill_rect(self, rect: Tuple[float, float, float, float]) -> None: """Fill a rectangle with :any:`draw_color`. - .. versionadded:: unreleased + .. versionadded:: 13.5 """ _check(lib.SDL_RenderFillRectF(self.p, (rect,))) def draw_rect(self, rect: Tuple[float, float, float, float]) -> None: """Draw a rectangle outline. - .. versionadded:: unreleased + .. versionadded:: 13.5 """ _check(lib.SDL_RenderDrawRectF(self.p, (rect,))) def draw_point(self, xy: Tuple[float, float]) -> None: """Draw a point. - .. versionadded:: unreleased + .. versionadded:: 13.5 """ _check(lib.SDL_RenderDrawPointF(self.p, (xy,))) def draw_line(self, start: Tuple[float, float], end: Tuple[float, float]) -> None: """Draw a single line. - .. versionadded:: unreleased + .. versionadded:: 13.5 """ _check(lib.SDL_RenderDrawLineF(self.p, *start, *end)) def fill_rects(self, rects: NDArray[Union[np.intc, np.float32]]) -> None: """Fill multiple rectangles from an array. - .. versionadded:: unreleased + .. versionadded:: 13.5 """ assert len(rects.shape) == 2 assert rects.shape[1] == 4 @@ -565,7 +565,7 @@ def fill_rects(self, rects: NDArray[Union[np.intc, np.float32]]) -> None: def draw_rects(self, rects: NDArray[Union[np.intc, np.float32]]) -> None: """Draw multiple outlined rectangles from an array. - .. versionadded:: unreleased + .. versionadded:: 13.5 """ assert len(rects.shape) == 2 assert rects.shape[1] == 4 @@ -580,7 +580,7 @@ def draw_rects(self, rects: NDArray[Union[np.intc, np.float32]]) -> None: def draw_points(self, points: NDArray[Union[np.intc, np.float32]]) -> None: """Draw an array of points. - .. versionadded:: unreleased + .. versionadded:: 13.5 """ assert len(points.shape) == 2 assert points.shape[1] == 2 @@ -595,7 +595,7 @@ def draw_points(self, points: NDArray[Union[np.intc, np.float32]]) -> None: def draw_lines(self, points: NDArray[Union[np.intc, np.float32]]) -> None: """Draw a connected series of lines from an array. - .. versionadded:: unreleased + .. versionadded:: 13.5 """ assert len(points.shape) == 2 assert points.shape[1] == 2 @@ -618,7 +618,7 @@ def geometry( ) -> None: """Render triangles from texture and vertex data. - .. versionadded:: unreleased + .. versionadded:: 13.5 """ assert xy.dtype == np.float32 assert len(xy.shape) == 2 diff --git a/tcod/sdl/video.py b/tcod/sdl/video.py index bdcb6b6d..3061ab58 100644 --- a/tcod/sdl/video.py +++ b/tcod/sdl/video.py @@ -284,7 +284,7 @@ def mouse_rect(self) -> Optional[Tuple[int, int, int, int]]: Setting this will not automatically grab the cursor. - .. versionadded:: unreleased + .. versionadded:: 13.5 """ _version_at_least((2, 0, 18)) rect = lib.SDL_GetWindowMouseRect(self.p) From b1048ca5d31550632c5c7a86d82b36af74cfc076 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 14 Feb 2022 18:52:37 -0800 Subject: [PATCH 057/194] Add locks to sound mixer. Add volume and Pygame style loop parameters to sound play functions. --- tcod/sdl/audio.py | 96 +++++++++++++++++++++++++++++++---------------- 1 file changed, 64 insertions(+), 32 deletions(-) diff --git a/tcod/sdl/audio.py b/tcod/sdl/audio.py index cbc07b3d..6f067765 100644 --- a/tcod/sdl/audio.py +++ b/tcod/sdl/audio.py @@ -179,10 +179,27 @@ def __default_callback(device: AudioDevice, stream: NDArray[Any]) -> None: stream[...] = device.silence +class _LoopSoundFunc: + def __init__(self, sound: NDArray[Any], loops: int, on_end: Optional[Callable[[Channel], None]]): + self.sound = sound + self.loops = loops + self.on_end = on_end + + def __call__(self, channel: Channel) -> None: + if not self.loops: + if self.on_end is not None: + self.on_end(channel) + return + channel.play(self.sound, volume=channel.volume, on_end=self) + if self.loops > 0: + self.loops -= 1 + + class Channel: mixer: Mixer def __init__(self) -> None: + self._lock = threading.RLock() self.volume: Union[float, Tuple[float, ...]] = 1.0 self.sound_queue: List[NDArray[Any]] = [] self.on_end_callback: Optional[Callable[[Channel], None]] = None @@ -195,10 +212,17 @@ def play( self, sound: ArrayLike, *, + volume: Union[float, Tuple[float, ...]] = 1.0, + loops: int = 0, on_end: Optional[Callable[[Channel], None]] = None, ) -> None: - self.sound_queue[:] = [self._verify_audio_sample(sound)] - self.on_end_callback = on_end + sound = self._verify_audio_sample(sound) + with self._lock: + self.volume = volume + self.sound_queue[:] = [sound] + self.on_end_callback = on_end + if loops: + self.on_end_callback = _LoopSoundFunc(sound, loops, on_end) def _verify_audio_sample(self, sample: ArrayLike) -> NDArray[Any]: """Verify an audio sample is valid and return it as a Numpy array.""" @@ -209,27 +233,29 @@ def _verify_audio_sample(self, sample: ArrayLike) -> NDArray[Any]: return array def _on_mix(self, stream: NDArray[Any]) -> None: - while self.sound_queue and stream.size: - buffer = self.sound_queue[0] - if buffer.shape[0] > stream.shape[0]: - # Mix part of the buffer into the stream. - stream[:] += buffer[: stream.shape[0]] * self.volume - self.sound_queue[0] = buffer[stream.shape[0] :] - break # Stream was filled. - # Remaining buffer fits the stream array. - stream[: buffer.shape[0]] += buffer * self.volume - stream = stream[buffer.shape[0] :] - self.sound_queue.pop(0) - if not self.sound_queue and self.on_end_callback is not None: - self.on_end_callback(self) + with self._lock: + while self.sound_queue and stream.size: + buffer = self.sound_queue[0] + if buffer.shape[0] > stream.shape[0]: + # Mix part of the buffer into the stream. + stream[:] += buffer[: stream.shape[0]] * self.volume + self.sound_queue[0] = buffer[stream.shape[0] :] + break # Stream was filled. + # Remaining buffer fits the stream array. + stream[: buffer.shape[0]] += buffer * self.volume + stream = stream[buffer.shape[0] :] + self.sound_queue.pop(0) + if not self.sound_queue and self.on_end_callback is not None: + self.on_end_callback(self) def fadeout(self, time: float) -> None: assert time >= 0 - time_samples = round(time * self.mixer.device.frequency) + 1 - buffer: NDArray[np.float32] = np.zeros((time_samples, self.mixer.device.channels), np.float32) - self._on_mix(buffer) - buffer *= np.linspace(1.0, 0.0, time_samples + 1, endpoint=False)[1:] - self.sound_queue[:] = [buffer] + with self._lock: + time_samples = round(time * self.mixer.device.frequency) + 1 + buffer: NDArray[np.float32] = np.zeros((time_samples, self.mixer.device.channels), np.float32) + self._on_mix(buffer) + buffer *= np.linspace(1.0, 0.0, time_samples + 1, endpoint=False)[1:] + self.sound_queue[:] = [buffer] def stop(self) -> None: self.fadeout(0.0005) @@ -240,6 +266,7 @@ def __init__(self, device: AudioDevice): assert device.format == np.float32 super().__init__(daemon=True) self.device = device + self._lock = threading.RLock() def run(self) -> None: buffer = np.full( @@ -263,32 +290,37 @@ def __init__(self, device: AudioDevice): self.channels: Dict[Hashable, Channel] = {} def get_channel(self, key: Hashable) -> Channel: - if key not in self.channels: - self.channels[key] = Channel() - self.channels[key].mixer = self - return self.channels[key] + with self._lock: + if key not in self.channels: + self.channels[key] = Channel() + self.channels[key].mixer = self + return self.channels[key] def get_free_channel(self) -> Channel: - i = 0 - while True: - if not self.get_channel(i).busy: - return self.channels[i] - i += 1 + with self._lock: + i = 0 + while True: + if not self.get_channel(i).busy: + return self.channels[i] + i += 1 def play( self, sound: ArrayLike, *, + volume: Union[float, Tuple[float, ...]] = 1.0, + loops: int = 0, on_end: Optional[Callable[[Channel], None]] = None, ) -> Channel: channel = self.get_free_channel() - channel.play(sound, on_end=on_end) + channel.play(sound, volume=volume, loops=loops, on_end=on_end) return channel def on_stream(self, stream: NDArray[Any]) -> None: super().on_stream(stream) - for channel in list(self.channels.values()): - channel._on_mix(stream) + with self._lock: + for channel in list(self.channels.values()): + channel._on_mix(stream) class _AudioCallbackUserdata: From 3b16ca81248abe1d1e44126f48c3451f2c324834 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 14 Feb 2022 19:22:50 -0800 Subject: [PATCH 058/194] Increase lower bound for setuptools. Add wheel version to requires. --- pyproject.toml | 4 ++-- requirements.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1fddcde9..02f90c15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [build-system] requires = [ - "setuptools>=57.0.0", - "wheel", + "setuptools>=60.9.0", + "wheel>=0.37.1", "cffi>=1.15", "pycparser>=2.14", "pcpp==1.30", diff --git a/requirements.txt b/requirements.txt index b5a68d21..a6a74931 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ cffi>=1.15 numpy>=1.20.3 pycparser>=2.14 -setuptools>=36.0.1 +setuptools>=60.9.0 types-setuptools types-tabulate typing_extensions From 9ccf7777f01530cb7cbd5d12f703ce884fbbbda4 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 14 Feb 2022 20:31:05 -0800 Subject: [PATCH 059/194] Pin setuptools==60.8.2. The latest version breaks in PyPy for an unknown reason. --- pyproject.toml | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 02f90c15..a861e465 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [build-system] requires = [ - "setuptools>=60.9.0", + "setuptools==60.8.2", "wheel>=0.37.1", "cffi>=1.15", "pycparser>=2.14", diff --git a/requirements.txt b/requirements.txt index a6a74931..5b572578 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ cffi>=1.15 numpy>=1.20.3 pycparser>=2.14 -setuptools>=60.9.0 +setuptools==60.8.2 types-setuptools types-tabulate typing_extensions From 463b5b0ccc8be02ea039dff063a8358436f635d9 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 15 Feb 2022 01:58:37 -0800 Subject: [PATCH 060/194] Automatically run the mixer thread. Making this not automatic caused usability issues. I'm defaulting it to run again. --- tcod/sdl/audio.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tcod/sdl/audio.py b/tcod/sdl/audio.py index 6f067765..3779c2bb 100644 --- a/tcod/sdl/audio.py +++ b/tcod/sdl/audio.py @@ -267,6 +267,7 @@ def __init__(self, device: AudioDevice): super().__init__(daemon=True) self.device = device self._lock = threading.RLock() + self.start() def run(self) -> None: buffer = np.full( @@ -286,8 +287,8 @@ def on_stream(self, stream: NDArray[Any]) -> None: class BasicMixer(Mixer): def __init__(self, device: AudioDevice): - super().__init__(device) self.channels: Dict[Hashable, Channel] = {} + super().__init__(device) def get_channel(self, key: Hashable) -> Channel: with self._lock: From b69720adca24ea3ba3d745f7fa0de81178b23f4c Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 17 Feb 2022 13:34:10 -0800 Subject: [PATCH 061/194] Add closing of Mixer instances. --- tcod/sdl/audio.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tcod/sdl/audio.py b/tcod/sdl/audio.py index 3779c2bb..fd6c0edb 100644 --- a/tcod/sdl/audio.py +++ b/tcod/sdl/audio.py @@ -267,13 +267,14 @@ def __init__(self, device: AudioDevice): super().__init__(daemon=True) self.device = device self._lock = threading.RLock() + self._running = True self.start() def run(self) -> None: buffer = np.full( (self.device.buffer_samples, self.device.channels), self.device.silence, dtype=self.device.format ) - while True: + while self._running: if self.device._queued_bytes > 0: time.sleep(0.001) continue @@ -281,6 +282,9 @@ def run(self) -> None: self.device.queue_audio(buffer) buffer[:] = self.device.silence + def close(self) -> None: + self._running = False + def on_stream(self, stream: NDArray[Any]) -> None: pass From efa027bcbccfeffdcc713b24b9421c07a7526f25 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 18 Feb 2022 22:51:49 -0800 Subject: [PATCH 062/194] Add audio conversion, make mixer features public. Add partial audio tests. --- .vscode/settings.json | 1 + CHANGELOG.md | 4 + tcod/sdl/audio.py | 167 +++++++++++++++++++++++++++++++++++++----- tests/test_sdl.py | 14 ++++ 4 files changed, 169 insertions(+), 17 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index e02d3ea2..1df05195 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -192,6 +192,7 @@ "msilib", "MSVC", "msvcr", + "mult", "mulx", "muly", "mypy", diff --git a/CHANGELOG.md b/CHANGELOG.md index b05c8459..65976d2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] +### Added +- `BasicMixer` and `Channel` classes added to `tcod.sdl.audio`. These handle simple audio mixing. +- `AudioDevice.convert` added to handle simple conversions to the active devices format. +- `tcod.sdl.audio.convert_audio` added to handle any other conversions needed. ## [13.5.0] - 2022-02-11 ### Added diff --git a/tcod/sdl/audio.py b/tcod/sdl/audio.py index fd6c0edb..13f10a5c 100644 --- a/tcod/sdl/audio.py +++ b/tcod/sdl/audio.py @@ -16,7 +16,7 @@ import tcod.sdl.sys from tcod.loader import ffi, lib -from tcod.sdl import _get_error +from tcod.sdl import _check, _get_error def _get_format(format: DTypeLike) -> int: @@ -55,10 +55,65 @@ def _dtype_from_format(format: int) -> np.dtype[Any]: return np.dtype(f"{byteorder}{kind}{bytesize}") +def convert_audio( + in_sound: ArrayLike, in_rate: int, *, out_rate: int, out_format: DTypeLike, out_channels: int +) -> NDArray[Any]: + """Convert an audio sample into a format supported by this device. + + Returns the converted array. This might be a reference to the input array if no conversion was needed. + + Args: + in_sound: The input ArrayLike sound sample. Input format and channels are derived from the array. + in_rate: The samplerate of the input array. + out_rate: The samplerate of the output array. + out_format: The output format of the converted array. + out_channels: The number of audio channels of the output array. + + .. versionadded:: unreleased + + .. seealso:: + :any:`AudioDevice.convert` + """ + in_array: NDArray[Any] = np.asarray(in_sound) + if len(in_array.shape) == 1: + in_array = in_array[:, np.newaxis] + if not len(in_array.shape) == 2: + raise TypeError(f"Expected a 1 or 2 ndim input, got {in_array.shape} instead.") + cvt = ffi.new("SDL_AudioCVT*") + in_channels = in_array.shape[1] + in_format = _get_format(in_array.dtype) + out_sdl_format = _get_format(out_format) + if _check(lib.SDL_BuildAudioCVT(cvt, in_format, in_channels, in_rate, out_sdl_format, out_channels, out_rate)) == 0: + return in_array # No conversion needed. + # Upload to the SDL_AudioCVT buffer. + cvt.len = in_array.itemsize * in_array.size + out_buffer = cvt.buf = ffi.new("uint8_t[]", cvt.len * cvt.len_mult) + np.frombuffer(ffi.buffer(out_buffer[0 : cvt.len]), dtype=in_array.dtype).reshape(in_array.shape)[:] = in_array + + _check(lib.SDL_ConvertAudio(cvt)) + out_array: NDArray[Any] = ( + np.frombuffer(ffi.buffer(out_buffer[0 : cvt.len_cvt]), dtype=out_format).reshape(-1, out_channels).copy() + ) + return out_array + + class AudioDevice: """An SDL audio device. Open new audio devices using :any:`tcod.sdl.audio.open`. + + Example:: + + import soundfile # pip install soundfile + import tcod.sdl.audio + + device = tcod.sdl.audio.open() + sound, samplerate = soundfile.read("example_sound.wav") + converted = device.convert(sound, samplerate) + device.queue_audio(converted) # Play the audio syncroniously. + + When you use this object directly the audio passed to :any:`queue_audio` is always played syncroniously. + For more typical asynchronous audio you should pass an AudioDevice to :any:`BasicMixer`. """ def __init__( @@ -136,6 +191,32 @@ def _convert_array(self, samples_: ArrayLike) -> NDArray[Any]: samples = samples[:, np.newaxis] return np.ascontiguousarray(np.broadcast_to(samples, (samples.shape[0], self.channels)), dtype=self.format) + def convert(self, sound: ArrayLike, rate: Optional[int] = None) -> NDArray[Any]: + """Convert an audio sample into a format supported by this device. + + Returns the converted array. This might be a reference to the input array if no conversion was needed. + + Args: + sound: An ArrayLike sound sample. + rate: The samplerate of the input array. + If None is given then it's assumed to be the same as the device. + + .. versionadded:: unreleased + + .. seealso:: + :any:`convert_audio` + """ + in_array: NDArray[Any] = np.asarray(sound) + if len(in_array.shape) == 1: + in_array = in_array[:, np.newaxis] + return convert_audio( + in_sound=sound, + in_rate=rate if rate is not None else self.frequency, + out_channels=self.channels if in_array.shape[1] > 1 else 1, + out_format=self.format, + out_rate=self.frequency, + ) + @property def _queued_bytes(self) -> int: """The current amount of bytes remaining in the audio queue.""" @@ -196,7 +277,13 @@ def __call__(self, channel: Channel) -> None: class Channel: - mixer: Mixer + """An audio channel for :any:`BasicMixer`. Use :any:`BasicMixer.get_channel` to initialize this object. + + .. versionadded:: unreleased + """ + + mixer: BasicMixer + """The :any:`BasicMixer` is channel belongs to.""" def __init__(self) -> None: self._lock = threading.RLock() @@ -206,6 +293,7 @@ def __init__(self) -> None: @property def busy(self) -> bool: + """Is True when this channel is playing audio.""" return bool(self.sound_queue) def play( @@ -216,6 +304,10 @@ def play( loops: int = 0, on_end: Optional[Callable[[Channel], None]] = None, ) -> None: + """Play an audio sample, stopping any audio currently playing on this channel. + + Parameters are the same as :any:`BasicMixer.play`. + """ sound = self._verify_audio_sample(sound) with self._lock: self.volume = volume @@ -233,6 +325,7 @@ def _verify_audio_sample(self, sample: ArrayLike) -> NDArray[Any]: return array def _on_mix(self, stream: NDArray[Any]) -> None: + """Mix the next part of this channels audio into an active audio stream.""" with self._lock: while self.sound_queue and stream.size: buffer = self.sound_queue[0] @@ -249,8 +342,10 @@ def _on_mix(self, stream: NDArray[Any]) -> None: self.on_end_callback(self) def fadeout(self, time: float) -> None: - assert time >= 0 + """Fadeout this channel then stop playing.""" with self._lock: + if not self.sound_queue: + return time_samples = round(time * self.mixer.device.frequency) + 1 buffer: NDArray[np.float32] = np.zeros((time_samples, self.mixer.device.channels), np.float32) self._on_mix(buffer) @@ -258,14 +353,36 @@ def fadeout(self, time: float) -> None: self.sound_queue[:] = [buffer] def stop(self) -> None: + """Stop audio on this channel.""" self.fadeout(0.0005) -class Mixer(threading.Thread): +class BasicMixer(threading.Thread): + """An SDL sound mixer implemented in Python and Numpy. + + Example:: + + import time + + import soundfile # pip install soundfile + import tcod.sdl.audio + + mixer = tcod.sdl.audio.BasicMixer(tcod.sdl.audio.open()) + sound, samplerate = soundfile.read("example_sound.wav") + sound = mixer.device.convert(sound, samplerate) # Needed if dtype or samplerate differs. + channel = mixer.play(sound) + while channel.busy: + time.sleep(0.001) + + .. versionadded:: unreleased + """ + def __init__(self, device: AudioDevice): + self.channels: Dict[Hashable, Channel] = {} assert device.format == np.float32 super().__init__(daemon=True) self.device = device + """The :any:`AudioDevice`""" self._lock = threading.RLock() self._running = True self.start() @@ -278,30 +395,30 @@ def run(self) -> None: if self.device._queued_bytes > 0: time.sleep(0.001) continue - self.on_stream(buffer) + self._on_stream(buffer) self.device.queue_audio(buffer) buffer[:] = self.device.silence def close(self) -> None: + """Shutdown this mixer, all playing audio will be abruptly stopped.""" self._running = False - def on_stream(self, stream: NDArray[Any]) -> None: - pass - + def get_channel(self, key: Hashable) -> Channel: + """Return a channel tied to with the given key. -class BasicMixer(Mixer): - def __init__(self, device: AudioDevice): - self.channels: Dict[Hashable, Channel] = {} - super().__init__(device) + Channels are initialized as you access them with this function. + :any:`int` channels starting from zero are used internally. - def get_channel(self, key: Hashable) -> Channel: + This can be used to generate a ``"music"`` channel for example. + """ with self._lock: if key not in self.channels: self.channels[key] = Channel() self.channels[key].mixer = self return self.channels[key] - def get_free_channel(self) -> Channel: + def _get_next_channel(self) -> Channel: + """Return the next available channel for the play method.""" with self._lock: i = 0 while True: @@ -317,12 +434,28 @@ def play( loops: int = 0, on_end: Optional[Callable[[Channel], None]] = None, ) -> Channel: - channel = self.get_free_channel() + """Play a sound, return the channel the sound is playing on. + + Args: + sound: The sound to play. This a Numpy array matching the format of the loaded audio device. + volume: The volume to play the sound at. + You can also pass a tuple of floats to set the volume for each channel/speaker. + loops: How many times to play the sound, `-1` can be used to loop the sound forever. + on_end: A function to call when this sound has ended. + This is called with the :any:`Channel` which was playing the sound. + """ + channel = self._get_next_channel() channel.play(sound, volume=volume, loops=loops, on_end=on_end) return channel - def on_stream(self, stream: NDArray[Any]) -> None: - super().on_stream(stream) + def stop(self) -> None: + """Stop playback on all channels from this mixer.""" + with self._lock: + for channel in self.channels.values(): + channel.stop() + + def _on_stream(self, stream: NDArray[Any]) -> None: + """Called to fill the audio buffer.""" with self._lock: for channel in list(self.channels.values()): channel._on_mix(stream) diff --git a/tests/test_sdl.py b/tests/test_sdl.py index 8dea7f33..3deec406 100644 --- a/tests/test_sdl.py +++ b/tests/test_sdl.py @@ -1,8 +1,10 @@ +import contextlib import sys import numpy as np import pytest +import tcod.sdl.audio import tcod.sdl.render import tcod.sdl.sys import tcod.sdl.video @@ -74,3 +76,15 @@ def test_sdl_render_bad_types() -> None: tcod.sdl.render.Renderer(tcod.ffi.cast("SDL_Renderer*", tcod.ffi.NULL)) with pytest.raises(TypeError): tcod.sdl.render.Renderer(tcod.ffi.new("SDL_Rect*")) + + +def test_sdl_audio_device() -> None: + with contextlib.closing(tcod.sdl.audio.open(frequency=44100, format=np.float32, channels=2, paused=True)) as device: + assert device.convert(np.zeros(4, dtype=np.float32), 22050).shape[0] == 8 + assert device.convert(np.zeros((4, 4), dtype=np.float32)).shape == (4, 2) + assert device.convert(np.zeros(4, dtype=np.int8)).shape[0] == 4 + device.paused = False + device.paused = True + assert device.queued_samples == 0 + with contextlib.closing(tcod.sdl.audio.BasicMixer(device)) as mixer: + assert mixer From 934d2dd0ba5bc4d2dd1e4435c826a6bf512c7a70 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 19 Feb 2022 00:42:42 -0800 Subject: [PATCH 063/194] Prepare 13.6.0 release. --- CHANGELOG.md | 2 ++ tcod/sdl/audio.py | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65976d2e..e6cf855e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] + +## [13.6.0] - 2022-02-19 ### Added - `BasicMixer` and `Channel` classes added to `tcod.sdl.audio`. These handle simple audio mixing. - `AudioDevice.convert` added to handle simple conversions to the active devices format. diff --git a/tcod/sdl/audio.py b/tcod/sdl/audio.py index 13f10a5c..65c66aef 100644 --- a/tcod/sdl/audio.py +++ b/tcod/sdl/audio.py @@ -69,7 +69,7 @@ def convert_audio( out_format: The output format of the converted array. out_channels: The number of audio channels of the output array. - .. versionadded:: unreleased + .. versionadded:: 13.6 .. seealso:: :any:`AudioDevice.convert` @@ -201,7 +201,7 @@ def convert(self, sound: ArrayLike, rate: Optional[int] = None) -> NDArray[Any]: rate: The samplerate of the input array. If None is given then it's assumed to be the same as the device. - .. versionadded:: unreleased + .. versionadded:: 13.6 .. seealso:: :any:`convert_audio` @@ -279,7 +279,7 @@ def __call__(self, channel: Channel) -> None: class Channel: """An audio channel for :any:`BasicMixer`. Use :any:`BasicMixer.get_channel` to initialize this object. - .. versionadded:: unreleased + .. versionadded:: 13.6 """ mixer: BasicMixer @@ -374,7 +374,7 @@ class BasicMixer(threading.Thread): while channel.busy: time.sleep(0.001) - .. versionadded:: unreleased + .. versionadded:: 13.6 """ def __init__(self, device: AudioDevice): From 807fdca464389e3febd8c86991bd4564d091f48d Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 8 Mar 2022 06:19:05 -0800 Subject: [PATCH 064/194] Explain non-blocking event loops in the getting started example. --- docs/tcod/getting-started.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/tcod/getting-started.rst b/docs/tcod/getting-started.rst index ddca121c..4954fe6a 100644 --- a/docs/tcod/getting-started.rst +++ b/docs/tcod/getting-started.rst @@ -45,6 +45,8 @@ Example:: console.print(x=0, y=0, string="Hello World!") context.present(console) # Show the console. + # This event loop will wait until at least one event is processed before exiting. + # For a non-blocking event loop replace `tcod.event.wait` with `tcod.event.get`. for event in tcod.event.wait(): context.convert_event(event) # Sets tile coordinates for mouse events. print(event) # Print event names and attributes. From 1a142ff09afab67fcd498e96193d80f2d899d943 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 29 Mar 2022 12:05:38 -0700 Subject: [PATCH 065/194] Fetch updates from libtcod and change the default renderer to SDL2. --- CHANGELOG.md | 3 +++ build_libtcod.py | 6 ------ libtcod | 2 +- tcod/context.py | 2 +- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6cf855e..009a9750 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] +### Changed +- The SDL2 renderer has had a major performance update when compiled with SDL 2.0.18. +- SDL2 is now the default renderer to avoid rare issues with the OpenGL 2 renderer. ## [13.6.0] - 2022-02-19 ### Added diff --git a/build_libtcod.py b/build_libtcod.py index 93065ae3..f84d7736 100644 --- a/build_libtcod.py +++ b/build_libtcod.py @@ -15,12 +15,6 @@ import build_sdl # noqa: E402 -# The SDL2 version to parse and export symbols from. -SDL2_PARSE_VERSION = os.environ.get("SDL_VERSION", "2.0.5") - -# The SDL2 version to include in binary distributions. -SDL2_BUNDLE_VERSION = os.environ.get("SDL_VERSION", "2.0.14") - Py_LIMITED_API = 0x03060000 HEADER_PARSE_PATHS = ("tcod/", "libtcod/src/libtcod/") diff --git a/libtcod b/libtcod index d54f68bf..577f83b3 160000 --- a/libtcod +++ b/libtcod @@ -1 +1 @@ -Subproject commit d54f68bf10ee862f47d3668348279a13f489d028 +Subproject commit 577f83b39ca25dcc50051751182ba1defe9b62c4 diff --git a/tcod/context.py b/tcod/context.py index 8b324b83..1c15382a 100644 --- a/tcod/context.py +++ b/tcod/context.py @@ -464,7 +464,7 @@ def new( Added the `console` parameter. """ if renderer is None: - renderer = RENDERER_OPENGL2 + renderer = RENDERER_SDL2 if sdl_window_flags is None: sdl_window_flags = SDL_WINDOW_RESIZABLE if argv is None: From d385df427d23510798d9ddb79219c05fd5b23597 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 29 Mar 2022 12:32:08 -0700 Subject: [PATCH 066/194] Prepare 13.6.1 release. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 009a9750..0874c558 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] + +## [13.6.1] - 2022-03-29 ### Changed - The SDL2 renderer has had a major performance update when compiled with SDL 2.0.18. - SDL2 is now the default renderer to avoid rare issues with the OpenGL 2 renderer. From 8f4d00c8338abc55c32fefbaacfbb3c2e6cbdb33 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 30 Mar 2022 13:23:48 -0700 Subject: [PATCH 067/194] Raise min version of NumPy dependency. Tcod always uses the type hinting features of the latest NumPy. This is the final version of NumPy with Python 3.7 support. Fixes #115 --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 5b572578..a7275c20 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ cffi>=1.15 -numpy>=1.20.3 +numpy>=1.21.4 pycparser>=2.14 setuptools==60.8.2 types-setuptools diff --git a/setup.py b/setup.py index 411653fd..a364e516 100755 --- a/setup.py +++ b/setup.py @@ -120,7 +120,7 @@ def check_sdl_version() -> None: ], install_requires=[ "cffi>=1.15", # Also required by pyproject.toml. - "numpy>=1.20.3" if not is_pypy else "", + "numpy>=1.21.4" if not is_pypy else "", "typing_extensions", ], cffi_modules=["build_libtcod.py:ffi"], From c2fb67ec1d275f6d105c9c6cdd705b100ad23660 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 22 Apr 2022 17:44:00 -0700 Subject: [PATCH 068/194] Improve docs. Focus more on showing examples in the page headers. --- tcod/context.py | 72 ++++++++++++++++++++++++--- tcod/event.py | 122 +++++++++++++++++++++++++++------------------- tcod/image.py | 13 +++-- tcod/sdl/audio.py | 58 +++++++++++++--------- tcod/sdl/mouse.py | 18 +++++++ tcod/sdl/video.py | 22 ++++++++- 6 files changed, 219 insertions(+), 86 deletions(-) diff --git a/tcod/context.py b/tcod/context.py index 1c15382a..a112d0d5 100644 --- a/tcod/context.py +++ b/tcod/context.py @@ -179,9 +179,10 @@ def __enter__(self) -> Context: return self def close(self) -> None: - """Delete the context, closing any windows opened by this context. + """Close this context, closing any windows opened by this context. - This instance is invalid after this call.""" + Afterwards doing anything with this instance other than closing it again is invalid. + """ if hasattr(self, "_context_p"): ffi.release(self._context_p) del self._context_p @@ -246,7 +247,21 @@ def pixel_to_subtile(self, x: int, y: int) -> Tuple[float, float]: return xy[0], xy[1] def convert_event(self, event: tcod.event.Event) -> None: - """Fill in the tile coordinates of a mouse event using this context.""" + """Fill in the tile coordinates of a mouse event using this context. + + Example:: + + context: tcod.context.Context + for event in tcod.event.get(): + if isinstance(event, tcod.event.MouseMotion): + # Pixel coordinates are always accessible. + print(f"{event.pixel=}, {event.pixel_motion=}") + context.convert_event(event) + if isinstance(event, tcod.event.MouseMotion): + # Now tile coordinate attributes can be accessed. + print(f"{event.tile=}, {event.tile_motion=}") + # A warning will be raised if you try to access these without convert_event. + """ if isinstance(event, (tcod.event.MouseState, tcod.event.MouseMotion)): event.tile = tcod.event.Point(*self.pixel_to_tile(*event.pixel)) if isinstance(event, tcod.event.MouseMotion): @@ -262,7 +277,17 @@ def save_screenshot(self, path: Optional[str] = None) -> None: _check(lib.TCOD_context_save_screenshot(self._context_p, c_path)) def change_tileset(self, tileset: Optional[tcod.tileset.Tileset]) -> None: - """Change the active tileset used by this context.""" + """Change the active tileset used by this context. + + The new tileset will take effect on the next call to :any:`present`. + Contexts not using a renderer with an emulated terminal will be unaffected by this method. + + This does not do anything to resize the window, keep this in mind if the tileset as a differing tile size. + Access the window with :any:`sdl_window` to resize it manually, if needed. + + Using this method only one tileset is active per-frame. + See :any:`tcod.render` if you want to renderer with multiple tilesets in a single frame. + """ _check(lib.TCOD_context_change_tileset(self._context_p, _handle_tileset(tileset))) def new_console( @@ -299,6 +324,25 @@ def new_console( .. seealso:: :any:`tcod.console.Console` + + Example:: + + scale = 1 # Tile size scale. This example uses integers but floating point numbers are also valid. + context = tcod.context.new() + while True: + # Create a cleared, dynamically-sized console for each frame. + console = context.new_console(magnification=scale) + # This printed output will wrap if the window is shrunk. + console.print_box(0, 0, console.width, console.height, "Hello world") + # Use integer_scaling to prevent subpixel distorsion. + # This may add padding around the rendered console. + context.present(console, integer_scaling=True) + for event in tcod.event.wait(): + if isinstance(event, tcod.event.Quit): + raise SystemExit() + elif isinstance(event, tcod.event.MouseWheel): + # Use the mouse wheel to change the rendered tile size. + scale = max(1, scale + event.y) """ if magnification < 0: raise ValueError("Magnification must be greater than zero. (Got %f)" % magnification) @@ -351,15 +395,31 @@ def toggle_fullscreen(context: tcod.context.Context) -> None: context.sdl_window_p, 0 if fullscreen else tcod.lib.SDL_WINDOW_FULLSCREEN_DESKTOP, ) + ''' # noqa: E501 return lib.TCOD_context_get_sdl_window(self._context_p) @property def sdl_window(self) -> Optional[tcod.sdl.video.Window]: - """Return a :any:`tcod.sdl.video.Window` referencing this contexts SDL window if it exists. + '''Return a :any:`tcod.sdl.video.Window` referencing this contexts SDL window if it exists. + + Example:: + + import tcod + improt tcod.sdl.video + + def toggle_fullscreen(context: tcod.context.Context) -> None: + """Toggle a context window between fullscreen and windowed modes.""" + window = context.sdl_window + if not window: + return + if window.fullscreen: + window.fullscreen = False + else: + window.fullscreen = tcod.sdl.video.WindowFlags.FULLSCREEN_DESKTOP .. versionadded:: 13.4 - """ + ''' p = self.sdl_window_p return tcod.sdl.video.Window(p) if p else None diff --git a/tcod/event.py b/tcod/event.py index b333b1fc..102ade1d 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -1,5 +1,4 @@ -""" -A light-weight implementation of event handling built on calls to SDL. +"""A light-weight implementation of event handling built on calls to SDL. Many event constants are derived directly from SDL. For example: ``tcod.event.K_UP`` and ``tcod.event.SCANCODE_A`` refer to @@ -8,14 +7,75 @@ `_ Printing any event will tell you its attributes in a human readable format. -An events type attribute if omitted is just the classes name with all letters -upper-case. +An events type attribute if omitted is just the classes name with all letters upper-case. + +As a general guideline, you should use :any:`KeyboardEvent.sym` for command inputs, +and :any:`TextInput.text` for name entry fields. + +Example:: + + import tcod + + KEY_COMMANDS = { + tcod.event.KeySym.UP: "move N", + tcod.event.KeySym.DOWN: "move S", + tcod.event.KeySym.LEFT: "move W", + tcod.event.KeySym.RIGHT: "move E", + } + + context = tcod.context.new() + while True: + console = context.new_console() + context.present(console, integer_scaling=True) + for event in tcod.event.wait(): + context.convert_event(event) # Adds tile coordinates to mouse events. + if isinstance(event, tcod.event.Quit): + print(event) + raise SystemExit() + elif isinstance(event, tcod.event.KeyDown): + print(event) # Prints the Scancode and KeySym enums for this event. + if event.sym in KEY_COMMANDS: + print(f"Command: {KEY_COMMANDS[event.sym]}") + elif isinstance(event, tcod.event.MouseButtonDown): + print(event) # Prints the mouse button constant names for this event. + elif isinstance(event, tcod.event.MouseMotion): + print(event) # Prints the mouse button mask bits in a readable format. + else: + print(event) # Print any unhandled events. + +Python 3.10 introduced `match statements `_ +which can be used to dispatch events more gracefully: + +Example:: -As a general guideline, you should use :any:`KeyboardEvent.sym` for command -inputs, and :any:`TextInput.text` for name entry fields. + import tcod -Remember to add the line ``import tcod.event``, as importing this module is not -implied by ``import tcod``. + KEY_COMMANDS = { + tcod.event.KeySym.UP: "move N", + tcod.event.KeySym.DOWN: "move S", + tcod.event.KeySym.LEFT: "move W", + tcod.event.KeySym.RIGHT: "move E", + } + + context = tcod.context.new() + while True: + console = context.new_console() + context.present(console, integer_scaling=True) + for event in tcod.event.wait(): + context.convert_event(event) # Adds tile coordinates to mouse events. + match event: + case tcod.event.Quit(): + raise SystemExit() + case tcod.event.KeyDown(sym) if sym in KEY_COMMANDS: + print(f"Command: {KEY_COMMANDS[sym]}") + case tcod.event.KeyDown(sym, scancode, mod, repeat): + print(f"KeyDown: {sym=}, {scancode=}, {mod=}, {repeat=}") + case tcod.event.MouseButtonDown(button, pixel, tile): + print(f"MouseButtonDown: {button=}, {pixel=}, {tile=}") + case tcod.event.MouseMotion(pixel, pixel_motion, tile, tile_motion): + print(f"MouseMotion: {pixel=}, {pixel_motion=}, {tile=}, {tile_motion=}") + case tcod.event.Event() as event: + print(event) # Show any unhandled events. .. versionadded:: 8.4 """ @@ -762,48 +822,10 @@ def _parse_event(sdl_event: Any) -> Event: def get() -> Iterator[Any]: """Return an iterator for all pending events. - Events are processed as the iterator is consumed. Breaking out of, or - discarding the iterator will leave the remaining events on the event queue. - It is also safe to call this function inside of a loop that is already - handling events (the event iterator is reentrant.) - - Example:: - - context: tcod.context.Context # Context object initialized earlier. - for event in tcod.event.get(): - context.convert_event(event) # Add tile coordinates to mouse events. - if isinstance(event, tcod.event.Quit): - print(event) - raise SystemExit() - elif isinstance(event, tcod.event.KeyDown): - print(event) # Prints the Scancode and KeySym enums for this event. - elif isinstance(event, tcod.event.MouseButtonDown): - print(event) # Prints the mouse button constant names for this event. - elif isinstance(event, tcod.event.MouseMotion): - print(event) # Prints the mouse button mask bits in a readable format. - else: - print(event) # Print any unhandled events. - # For loop exits after all current events are processed. - - Python 3.10 introduced `match statements `_ - which can be used to dispatch events more gracefully: - - Example:: - - context: tcod.context.Context # Context object initialized earlier. - for event in tcod.event.get(): - context.convert_event(event) # Add tile coordinates to mouse events. - match event: - case tcod.event.Quit(): - raise SystemExit() - case tcod.event.KeyDown(sym, scancode, mod, repeat): - print(f"KeyDown: {sym=}, {scancode=}, {mod=}, {repeat=}") - case tcod.event.MouseButtonDown(button, pixel, tile): - print(f"MouseButtonDown: {button=}, {pixel=}, {tile=}") - case tcod.event.MouseMotion(pixel, pixel_motion, tile, tile_motion): - print(f"MouseMotion: {pixel=}, {pixel_motion=}, {tile=}, {tile_motion=}") - case tcod.event.Event() as event: - print(event) # Show any unhandled events. + Events are processed as the iterator is consumed. + Breaking out of, or discarding the iterator will leave the remaining events on the event queue. + It is also safe to call this function inside of a loop that is already handling events + (the event iterator is reentrant.) """ sdl_event = ffi.new("SDL_Event*") while lib.SDL_PollEvent(sdl_event): diff --git a/tcod/image.py b/tcod/image.py index d94441cb..0f5760e9 100644 --- a/tcod/image.py +++ b/tcod/image.py @@ -1,9 +1,12 @@ -"""Functionality for handling images. +"""Libtcod functionality for handling images. -**Python-tcod is unable to render pixels to the screen directly.** -If your image can't be represented as tiles then you'll need to use -`an alternative library for graphics rendering -`_. +This module is generally seen as outdated. +To load images you should typically use `Pillow `_ or +`imageio `_ unless you need to use a feature exclusive to libtcod. + +**Python-tcod is unable to render pixels to consoles.** +The best it can do with consoles is convert an image into semigraphics which can be shown on non-emulated terminals. +For true pixel-based rendering you'll want to access the SDL rendering port at :any:`tcod.sdl.render`. """ from __future__ import annotations diff --git a/tcod/sdl/audio.py b/tcod/sdl/audio.py index 65c66aef..6bee2587 100644 --- a/tcod/sdl/audio.py +++ b/tcod/sdl/audio.py @@ -1,5 +1,39 @@ """SDL2 audio playback and recording tools. +This module includes SDL's low-level audio API and a naive implentation of an SDL mixer. +If you have experience with audio mixing then you might be better off writing your own mixer or +modifying the existing one which was written using Python/Numpy. + +This module is designed to integrate with the wider Python ecosystem. +It leaves the loading to sound samples to other libaries like +`SoundFile `_. + +Example:: + + # Synchronous audio example using SDL's low-level API. + import soundfile # pip install soundfile + import tcod.sdl.audio + + device = tcod.sdl.audio.open() # Open the default output device. + sound, samplerate = soundfile.read("example_sound.wav") # Load an audio sample using SoundFile. + converted = device.convert(sound, samplerate) # Convert this sample to the format expected by the device. + device.queue_audio(converted) # Play audio syncroniously by appending it to the device buffer. + +Example:: + + # Asynchronous audio example using BasicMixer. + import time + + import soundfile # pip install soundfile + import tcod.sdl.audio + + mixer = tcod.sdl.audio.BasicMixer(tcod.sdl.audio.open()) # Setup BasicMixer with the default audio output. + sound, samplerate = soundfile.read("example_sound.wav") # Load an audio sample using SoundFile. + sound = mixer.device.convert(sound, samplerate) # Convert this sample to the format expected by the device. + channel = mixer.play(sound) # Start asynchronous playback, audio is mixed on a separate Python thread. + while channel.busy: # Wait until the sample is done playing. + time.sleep(0.001) + .. versionadded:: 13.5 """ from __future__ import annotations @@ -102,16 +136,6 @@ class AudioDevice: Open new audio devices using :any:`tcod.sdl.audio.open`. - Example:: - - import soundfile # pip install soundfile - import tcod.sdl.audio - - device = tcod.sdl.audio.open() - sound, samplerate = soundfile.read("example_sound.wav") - converted = device.convert(sound, samplerate) - device.queue_audio(converted) # Play the audio syncroniously. - When you use this object directly the audio passed to :any:`queue_audio` is always played syncroniously. For more typical asynchronous audio you should pass an AudioDevice to :any:`BasicMixer`. """ @@ -360,20 +384,6 @@ def stop(self) -> None: class BasicMixer(threading.Thread): """An SDL sound mixer implemented in Python and Numpy. - Example:: - - import time - - import soundfile # pip install soundfile - import tcod.sdl.audio - - mixer = tcod.sdl.audio.BasicMixer(tcod.sdl.audio.open()) - sound, samplerate = soundfile.read("example_sound.wav") - sound = mixer.device.convert(sound, samplerate) # Needed if dtype or samplerate differs. - channel = mixer.play(sound) - while channel.busy: - time.sleep(0.001) - .. versionadded:: 13.6 """ diff --git a/tcod/sdl/mouse.py b/tcod/sdl/mouse.py index 13349ceb..a12f2bca 100644 --- a/tcod/sdl/mouse.py +++ b/tcod/sdl/mouse.py @@ -1,5 +1,9 @@ """SDL mouse and cursor functions. +You can use this module to move or capture the cursor. + +You can also set the cursor icon to an OS-defined or custom icon. + .. versionadded:: 13.5 """ from __future__ import annotations @@ -141,6 +145,20 @@ def capture(enable: bool) -> None: It is highly reccomended to read the related remarks section in the SDL docs before using this. + Example:: + + # Make mouse button presses capture the mouse until all buttons are released. + # This means that dragging the mouse outside of the window will not cause an interruption in motion events. + for event in tcod.event.get(): + match event: + case tcod.event.MouseButtonDown(button, pixel): # Clicking the window captures the mouse. + tcod.sdl.mouse.capture(True) + case tcod.event.MouseButtonUp(): # When all buttons are released then the mouse is released. + if tcod.event.mouse.get_global_state().state == 0: + tcod.sdl.mouse.capture(False) + case tcod.event.MouseMotion(pixel, pixel_motion, state): + pass # While a button is held this event is still captured outside of the window. + .. seealso:: :any:`tcod.sdl.mouse.set_relative_mode` https://wiki.libsdl.org/SDL_CaptureMouse diff --git a/tcod/sdl/video.py b/tcod/sdl/video.py index 3061ab58..7c65a374 100644 --- a/tcod/sdl/video.py +++ b/tcod/sdl/video.py @@ -1,5 +1,9 @@ """SDL2 Window and Display handling. +There are two main ways to access the SDL window. +Either you can use this module to open a window yourself bypassing libtcod's context, +or you can use :any:`Context.sdl_window` to get the window being controlled by that context (if the context has one.) + .. versionadded:: 13.4 """ from __future__ import annotations @@ -369,7 +373,23 @@ def get_grabbed_window() -> Optional[Window]: def screen_saver_allowed(allow: Optional[bool] = None) -> bool: - """Allow or prevent a screen saver from being displayed and return the current allowed status.""" + """Allow or prevent a screen saver from being displayed and return the current allowed status. + + If `allow` is `None` then only the current state is returned. + Otherwise it will change the state before checking it. + + SDL typically disables the screensaver by default. + If you're unsure, then don't touch this. + + Example:: + + import tcod.sdl.video + + print(f"Screen saver was allowed: {tcod.sdl.video.screen_saver_allowed()}") + # Allow the screen saver. + # Might be okay for some turn-based games which don't use a gamepad. + tcod.sdl.video.screen_saver_allowed(True) + """ if allow is None: pass elif allow: From 80afe770400e1887883db489e2ce779312f65534 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 2 May 2022 11:43:37 -0700 Subject: [PATCH 069/194] Fetch red background fix from libtcod. Fixes #116 --- CHANGELOG.md | 2 ++ libtcod | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0874c558..00dad55e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] +### Fixed +- SDL renderers were ignoring tiles where only the background red channel was changed. ## [13.6.1] - 2022-03-29 ### Changed diff --git a/libtcod b/libtcod index 577f83b3..6f0a9bc8 160000 --- a/libtcod +++ b/libtcod @@ -1 +1 @@ -Subproject commit 577f83b39ca25dcc50051751182ba1defe9b62c4 +Subproject commit 6f0a9bc8c31f769709299f248681b57a6f9659be From 2b0cbe66be4b8c5f2e932db9c0807081ed38a071 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 2 May 2022 11:52:21 -0700 Subject: [PATCH 070/194] Prepare 13.6.2 release. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00dad55e..8a214db1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] + +## [13.6.2] - 2022-05-02 ### Fixed - SDL renderers were ignoring tiles where only the background red channel was changed. From 6976563a676a2f7111254f49cee61c02f9dd2dd3 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 18 May 2022 14:39:54 -0700 Subject: [PATCH 071/194] Redirect links to the old develop branch to main. --- docs/changelog.rst | 2 +- docs/faq.rst | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 214182fa..2f021a4b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -3,4 +3,4 @@ =========== You can find the most recent changelog -`here `_. +`here `_. diff --git a/docs/faq.rst b/docs/faq.rst index 109ac981..ed7987fe 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -6,7 +6,7 @@ How do you set a frames-per-second while using contexts? You'll need to use an external tool to manage the framerate. This can either be your own custom tool or you can copy the Clock class from the -`framerate.py `_ +`framerate.py `_ example. diff --git a/setup.py b/setup.py index a364e516..c55ef0c2 100755 --- a/setup.py +++ b/setup.py @@ -103,7 +103,7 @@ def check_sdl_version() -> None: url="https://github.com/libtcod/python-tcod", project_urls={ "Documentation": "https://python-tcod.readthedocs.io", - "Changelog": "https://github.com/libtcod/python-tcod/blob/develop/CHANGELOG.md", + "Changelog": "https://github.com/libtcod/python-tcod/blob/main/CHANGELOG.md", "Source": "https://github.com/libtcod/python-tcod", "Tracker": "https://github.com/libtcod/python-tcod/issues", "Forum": "https://github.com/libtcod/python-tcod/discussions", From 751901e45ddf2b413d5c7f48ac2579d897472503 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 24 May 2022 15:45:44 -0700 Subject: [PATCH 072/194] Redirect broken link in readme. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 91444371..1f69e003 100755 --- a/README.rst +++ b/README.rst @@ -55,7 +55,7 @@ For the most part it's just:: =========== You can find the most recent changelog -`here `_. +`here `_. ========= License From 7a9a134a98cf9cb9d20dfd4a758e7ff8c0ffa2d0 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 24 May 2022 18:04:32 -0700 Subject: [PATCH 073/194] Update type hints for NumPy 1.22.4. Fix broken assert in Renderer.read_pixels. Show error codes by default in MyPy. --- examples/samples_tcod.py | 2 +- setup.cfg | 1 + tcod/console.py | 2 +- tcod/path.py | 8 ++++---- tcod/sdl/render.py | 2 +- tcod/tileset.py | 2 +- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/examples/samples_tcod.py b/examples/samples_tcod.py index f92d0151..bc7bc0af 100755 --- a/examples/samples_tcod.py +++ b/examples/samples_tcod.py @@ -140,7 +140,7 @@ def interpolate_corner_colors(self) -> None: def darken_background_characters(self) -> None: # darken background characters sample_console.fg[:] = sample_console.bg[:] - sample_console.fg[:] //= 2 + sample_console.fg[:] //= 2 # type: ignore[arg-type] # https://github.com/numpy/numpy/issues/21592 def randomize_sample_conole(self) -> None: # randomize sample console characters diff --git a/setup.cfg b/setup.cfg index faafa1fc..c70c33ef 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,6 +15,7 @@ max-line-length = 130 [mypy] python_version = 3.8 warn_unused_configs = True +show_error_codes = True disallow_subclassing_any = True disallow_any_generics = True disallow_untyped_calls = True diff --git a/tcod/console.py b/tcod/console.py index 56eea2ac..030f8bd1 100644 --- a/tcod/console.py +++ b/tcod/console.py @@ -243,7 +243,7 @@ def ch(self) -> NDArray[np.intc]: Index this array with ``console.ch[i, j] # order='C'`` or ``console.ch[x, y] # order='F'``. """ - return self._tiles["ch"].T if self._order == "F" else self._tiles["ch"] # type: ignore + return self._tiles["ch"].T if self._order == "F" else self._tiles["ch"] @property # type: ignore @deprecate("This attribute has been renamed to `rgba`.") diff --git a/tcod/path.py b/tcod/path.py index acc427af..d581e7da 100644 --- a/tcod/path.py +++ b/tcod/path.py @@ -344,7 +344,7 @@ def _compile_cost_edges(edge_map: Any) -> Tuple[Any, int]: edge_array = np.transpose(edge_nz) edge_array -= edge_center c_edges = ffi.new("int[]", len(edge_array) * 3) - edges = np.frombuffer(ffi.buffer(c_edges), dtype=np.intc).reshape(len(edge_array), 3) # type: ignore + edges = np.frombuffer(ffi.buffer(c_edges), dtype=np.intc).reshape(len(edge_array), 3) edges[:, :2] = edge_array edges[:, 2] = edge_map[edge_nz] return c_edges, len(edge_array) @@ -1148,7 +1148,7 @@ def traversal(self) -> NDArray[Any]: """ if self._order == "F": axes = range(self._travel.ndim) - return self._travel.transpose((*axes[-2::-1], axes[-1]))[..., ::-1] # type: ignore + return self._travel.transpose((*axes[-2::-1], axes[-1]))[..., ::-1] return self._travel def clear(self) -> None: @@ -1320,7 +1320,7 @@ def path_from(self, index: Tuple[int, ...]) -> NDArray[Any]: ffi.from_buffer("int*", path), ) ) - return path[:, ::-1] if self._order == "F" else path # type: ignore + return path[:, ::-1] if self._order == "F" else path def path_to(self, index: Tuple[int, ...]) -> NDArray[Any]: """Return the shortest path from the nearest root to `index`. @@ -1347,4 +1347,4 @@ def path_to(self, index: Tuple[int, ...]) -> NDArray[Any]: >>> pf.path_to((0, 0))[1:].tolist() # Exclude the starting point so that a blocked path is an empty list. [] """ # noqa: E501 - return self.path_from(index)[::-1] # type: ignore + return self.path_from(index)[::-1] diff --git a/tcod/sdl/render.py b/tcod/sdl/render.py index f44acb6a..3ea6bbd9 100644 --- a/tcod/sdl/render.py +++ b/tcod/sdl/render.py @@ -508,7 +508,7 @@ def read_pixels( out = np.empty((height, width, 3), dtype=np.uint8) else: raise TypeError("Pixel format not supported yet.") - assert out.shape[:2] == height, width + assert out.shape[:2] == (height, width) assert out[0].flags.c_contiguous _check(lib.SDL_RenderReadPixels(self.p, format, ffi.cast("void*", out.ctypes.data), out.strides[0])) return out diff --git a/tcod/tileset.py b/tcod/tileset.py index 14767e5b..93e8bc57 100644 --- a/tcod/tileset.py +++ b/tcod/tileset.py @@ -428,7 +428,7 @@ def procedural_block_elements(*, tileset: Tileset) -> None: (0x259F, 0b0111), # "▟" Quadrant upper right and lower left and lower right. ): alpha: NDArray[np.uint8] = np.asarray((quadrants & quad_mask) != 0, dtype=np.uint8) - alpha *= 255 + alpha *= 255 # type: ignore[arg-type] # https://github.com/numpy/numpy/issues/21592 tileset.set_tile(codepoint, alpha) for codepoint, axis, fraction, negative in ( From 52f8ba918ccf24639214ba0e89ce4c5839d2b3e0 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 27 May 2022 15:10:44 -0700 Subject: [PATCH 074/194] Add SDL TTF rendering example. --- examples/DejaVuSerif.ttf | Bin 0 -> 380132 bytes examples/sdl-hello-world.py | 47 ++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 examples/DejaVuSerif.ttf create mode 100644 examples/sdl-hello-world.py diff --git a/examples/DejaVuSerif.ttf b/examples/DejaVuSerif.ttf new file mode 100644 index 0000000000000000000000000000000000000000..0b803d206c1a4f19559d14f4d3cbd5bbfe0a86a1 GIT binary patch literal 380132 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%0A6)5fz^b{+9WlpW(OXq}w{QV`9e zQ>MSnKYOcSBsPPQ_DY|k|C-jEMd8XfDos`NFm5VT-)1j@L$Ca;B)r`O{vC=Uom^kt zz(O|9PaMxeZw0ndVrPt9=o$)hWEy1bSJ;8y4pwih;~z^!ZmwU~V!yHNhL^D47FkD{ zLF{-wTZqvK7sU9Y23ei5lj`KkBx}gJ{y5Lk;NgwntfEw&gw6%7LtIz5Imp385lOc6 z7E))R7_IDcD6=3x>(DX&`>fTgXEL{A%x%`H)wB5T$M8WxX1@2x8SLHZrRKYH*jk@O zlxb^`5Bmp?_gO^wwifw}mDig~r}NG;XlbxdaX7mJaCXB5o>9AoTJ*2 zL;7Ew=Qu;;q3Doa_5-@0`7}p`9#Wod_#{3q1~jMnL|bB!A~iU(`D8Yq>EC8|_m9a= z<=6h=NNf5P(C&?(-Qb7yk%An@Qg{r0fKTN5??t4j6*2dveDnAL78(s6Oj5-;G$aca z(9t+8f9K?p)hEx=*4M`}s&kiizxH%;^Ilg{>F$)$f}-a~NBf3*=Zy-;{k5mbr|q1Q z@g7Z2STafmK9jekTbc2pqWsKar;Z78AAa4(t3%4-RQ|zoQ*P~)Am@ zZB~$CVVbm}tms(zhy|lb7go`*h4p4@5#;{x@cT5PReUmA?-G^tpz!vjNWg$tX)RZr zui%3`W5W?*4lauTph7ijinvXi_*a{r{O#DD+SMpp{22>aRhw&~a4(;3`WZ z33v-^>6qI?ur1x7bD;;%jIe$1Z>T4v@o$Lc_w#LH@GPN1kt8343+J(uPE@rs9QM8<-Ri)Z$He9?2#zii?1l7lt( z%9Y>ONYaSm$60ZYY<}w<{=}Uf{FhCK)03+Af2Y#Ok-+gh&>0;(PIagiM_{Oc#H0cM zS%SsXoF*>~=64pca__`85_euCCB4dPq{2T>1)C3*7PZkMcTqThl-(Xu6~t^Qa>OoW z9zV)?D1T$cWL7+r)}_4hPeIRmItu=zVClk=q%zl} zvULw7&X`r&LMludzPedpJ)cJs| z6Pgz2jFD4|9Y%f%gRVbdZ?<#kf$b64n+Q*@a4(V*$XL!EkW)n8xgq>6yLMOaQ#qe| z&#&}hzdRtL$Tj{Ve*q?_GdGq$`|R=?4f7wIL!YJePw0M71yeIkr+YIKgQ>-^~ zWr-RqZe-7e1B1GjxD3_eIF1D*O>8%FM~PD(wh z@9AJXf6Ml{)Zt!NqHDTJ@JMCgPhd-PmXO~9gp4{u9B>e;VKqmg0^;vj`8%-r+pbFf z(Ei9nc_w@No=*QM-H{Jr9hL*{{6%Iu&_OtgQUqs9g#&^W9gz)HRhMDg zb)pyl`Vdd*g1Q=?S?_GBhoFp}4d^+$u(;k(pWV6MK&|vG>{sI<-do5waj3KE=j~zk z@bHZFh|!rs%+AriMw3glr&~NC07+eVnxau)Q@Relp+WTc9~L;T?K3#|hq4h{;vvO)Su(~zU9HBh=!j0o$HvR(3I=4ofBLVToc?9+9;z~5|}yC zSV=OFHxR~dGZhPk$s3yK0XJEeV05m!Ngvx1oPYg|4J;SMLHSp_l&#m_SR}ZdQ~Z~? z@V|PMb^Y{9_B1QU&vU@B1JD&-7WQ7a`l>kB!meG01N5PaYw{BU*Fa@-^XuE7{E-LB zAJzNPiyyuC(@O-sMB$~FodENcjpM7s?`RnFy;4#-53e`n4=HA^i;tQ=K)V+@G}D)Z z=p}?+LPa-t7FGgAoxn=qg+2hM-8TjF8MY~$@s!3NoO*#5iA0v8qHJG_yDrM);h{tu zOd-)>QGwA;9v=R2a96o@>0H(&VUiHln*7?=gfEdjJ-jEo85f2I)&y0!l}9TzGLpb! zOOjx^ICt8U1LZtL=9>r<-GB^BdJ91;yzGI_P+L+24t#q%FsEL&E4FhdY#$V*pRoYP z3j6h~3_qd#abh+bcHP!<&+5H<*6d;RywHkh;0w0R{Hu2&BcBN#dRm(F976&*TLb6Z zuyyaAr!X(9w$nG0}#*WSu5mg>0Z#nx>V&=ch48X)(A{Rb9~_DeCNqMnG1E z9|`trV79;Pp$qqj^oaH}xkbAKMC<$%Q&4ndNR&z>PG35Aso|1FB+TBEjZ%aXRufra zboKC;gKOM_60)xOC7o7j1S~m%X@+>$oSA^GJbRh~nu~G+WMJ8yJ2y|*-Z=K+!!NyN zCl8;Cv;T@D;qY%oDyA{VCZCzuL>RR5VF=J@K%Srh%k1aI@Q6yQ^)p2K64CCD`?21* z54xc>MBCkN=x(qXz~-s^q?mt@E;ri;Gxv@*oBRT!0~FcK8SCH@L8RkS5<;)Y1%{OH z*(N`;r|eea)270-FwnV1335uvItv`VMbl4=h?IPyQl#e4cX9<0JD2RE3>q8#MD$o2 z((g3w(h2^CB(d&RJ#uB;^Hh-k>TV-$Tbd*8ZhfQhVTguMDR7W{kbr|hx*^%VZXPZH zp3LZrdLHg(Xa8t-CsTj`Hwo}wgPl*sJCmEcQ;o+GKlg<}HO>_}{~A9K^4r(1<2ZSq zMO7nb4;K$t4>u1GI4x^RR0NDYfSGX2$6HZzO`!r~hnMzJb!qW*kt-oJ;;WHqbItQGa)_zmUZOoyKE>9 z=r$&OguA-r*E~V31+4ru4u$Gl2&e#=-N|ZMoo#`mlNHIpC&~W@etHq86bFgK7(%VyC0-~~> zzv=4aq1^DS20O(cl321E_M1J&o@K`-YIv+~^78^dV$Yo}1O5)WhHM|blR<9d>c!gl zuxPVv>fjgME&#;y|55keVNqRMAMosRrqG#T7=~U4P^r>8h=`zqiWN~&dQq_#P(;z# zB~fF=5@Sd))+EN5XiQ9Eim5lb$xXfKt*`QqT{|H>6l`s)2WLY)UHwOR82SYDK$=;1Hw0z_?~=pH1x1&gk@ z$~#na4ilYBqGPz|5Fu(JMfCttHAq{Wap(VWtZGTc%7>`TdMibw^-z`X2&{})LFw6A zO3$MCIOaj~Q$5m<`^XmFCk^Q_KlPvxBZWT1emX4FUQoT)Y+ffU!&qDI-tUn2Uw=gM zM-uU?BW6^Jir(xy7)Jb@gqJB=;FDJ(-W{MQ&hrfPckok&L=I4ds8!x-XOBpQZXk_M zkb#rD51tNwwE;RqZGcQ(Rie7iZm}i zBG<(mN>}~+P3HRBBi(0TlfM1wr=KIvYMxoY;nd?FRVI9R?CwV%xz`n4FH_DxwC;&u z-}jHc{n`1(^o^$;c;K<7u1y;^b;x)Kx@WO%^YgqtVbXooCI=5Ulb4fDt5lf09dtsU zy@1h>y%iy(5+_H&+sWD6$K6>YiY>0HDj$PRWG#9eL2_Bb7pgz9ij$ME#>vsivBulz z4YBzV|CQTi)a9Bm3rxh_pq7lf?4@X*;JLVG>a7+2^Dt=&_F|ve?O}@@2bNLnH55%W`rq__vUlfS(jbEC@$gQsJG5*-OGIdPPp29^$NlWyds0QSWAgM2FzCoIJJ7PrEyH2)Y)1l~Us(y0@s^LAd*QG2}FUvJpBFXhL(dSE4JvXxdi=|cmwnz=EnE^>L|&U6XpQD zev0{dE?T3j%3X9e`nV$Mc=v^mUqJ0G$}g}1UL_vRt`17kVUA*kzqhAObZs%XRRMR9 zxM5B2Ucv9lX4}VE>*1ky(du*_`Wh%`jkAkKjfc+1+lMT)!;k4Iu6z#AKa~v~=Q68! z$rcmExi1a8XTzS$ zZ7V7k!JfonJ>jUWO+}5dAbaL6&K-BY zclU$LMRD`Y{nE9MKES@Z0K?eVk`iV-4L%rw8NJ+0&Ki@G!l2ihoOK>*22m74ye$ZZ z0ES$;kq1oeD5_fA)Kwlri&KYz#a#iykTIiWnrAa<#KZa`l?rga@7~e-j21-oEfZ&&emb{JS=S+BX!< zb{r+2kGCo?@U(wE|T8dv>Um zGT8_aZb!GAlcT-9O~8OPsT+y#CR5GtkJ-_V^{Czh@mv7x7C?f5IH3@F>H&Fhz?t*# zJfpX}FK$@UxGJ?yCWHzIaa%mfu6^n3WFsO$9KZd%#mVg2}b{8Hk1j7M(19T_O)4}f@Z`7NUx8AJ9wLof<8ee zG;K!BG&^ctOa=#?TZj+1&cho{HcX3~ zrp2q%#Z!wMmBO{fg;R{ewrePY=4k-LGBTwW{Eqyo8D7taM5YR`8(@0N|TE66|!ykIQ{Co>13|G!{+$7>ly>*7-xw79WKrq z#Aq%|&c<8;|4~PZOViWSui8arkb@AE|Axq`!6~5_!t8oPm@PNK>iwU)h`K5qb4t^2 z#Q6G)ue(e8l_|enzy2HAMC53$+58;~c4cB7q7~O53BMhaa6SUb~0TH6jL8$Yzg!N zB1oV}4^T+q`)9ku&>t9Pa7QUsL`??wLdcWV*FQ*9!hx}83v%yv>h_o791f5 zB{)agu1<0-Ov3k#>J*+w%g@sqL;Mx4gM9r9!hH>xPKY4zg%>VD{N5#KfW_%F zTD{8P64qr1@!33}D`-Zv_P1-lrKeweiO!-i_{;s>&FVcIvx3eh0 z^JMe%hlV{iHpbsSUsrj-IVoY*@TZ%`j>gWSes0?~;X^6$s~>1GI+g_tQH=9O%wR{& zix1Y8mgjEp6#{)VI$yQBOSEfz7zrbl33J@wrqZ>B`nb1hyF-0lTFLs$;)j8EAXcqW zQU^Qbo-rTVn)S1c@sDD{PByi55gYLLFU{{i_#mr%xM{({#)q#gTG_t%3YrrCm_E-n z?8gH;>9QZGqP(DBO7F3`6%})!^QdSLKLCxciugP?b+k&1QK;0ej?qr>$V`Ad7){Cp zIWy&2msZzqwPULTNJHir=@fE9HcTmg&?KdFN&22sc;RO~ya{p~1vR+4Ncvj22k)Rf zMUkARh;~%FJ4Yi=5p#z;g}b|Rt75H7w`Q%@ztx(kfDllk;&gBrYIM=kp;7lwSD@%3 zB-9<1e~Ag`nq}uxyagv8_Nb)p5zP-g(4f`Um^ajDwGC+_rhALWk6)GkJ`-7oM0eaE zc1&v9vUTegw)Q9UBUTy3UF5*KREULoJ=pP~Si`NkifFy4jxt97Uvd?#;TlD^C0Eh< zf5}zkxOn8)=PC}W^auZcl&cUP6_h>f%pUgn8R-?)wa5I+Zs}E+hu)qp>DArlU-saF zfmhD3&-bu1J<7-?sfr!IRa76Daq${^Vx2^P#IBP@B6AVT!toRQob-z7{=O9d2WM;H z6!ZD}`iMcM7)6j;>7{n^P-r7yFRs{UDm*m4t$sRvmn&|b=@K^kxz32t{`SUi*FJqi zdkz1|c?wdozBGm9)Uq#25v&R8Jn=Q_dCzCZ4(aDt&;IPUBmbAS!zYeUjX&|=)@739 zHA}ocLi*;wWWjUZuATcITy}nC`IJ>nbz>^(juOqV-IiMRWqCfHF4@RWWFtQj?d7D_ zW+)ZW-WWn_sc9!8edh#4tE;M;Cn#F)oS-;J35tVu2?};P4rT3sm8K}{>7m{FmcG&! zNj|uw5m$qm4+vr<=?Au<%xoS_u`FMU>UV9V%g>7Lo7->}ct7^VS8+PeIH0%;MfJ!@ z-~;3)tcNG^`q4{(_;6F6kCV>BQK@#&x(TkHEL-$YJGlB9qrGYUUnN|*LJ2l5&uN#<)9Ko-&VC>w~M={Xl!*Or$*U6e4$nkO7{6X=V+aBbdI=u!^z1Rf9SN1 z9v%lp$Ae0xs6k#2i9UCC4WdSbkL3(b%^EOr=Vr^ig(V9kde}uSyf3gFTzH?qhd6Tw zSp~CbpH1*(uVI}gMQ)x}o2^qjMZ0FZXreu1f>hy_Yy_Y``RL%F&@O-dS|<%I2KJxf zs%>?LWwo6E#bZtr%_*^9IXQwyg&h1DA3_qxNJ!ZLk5D%!Zr;r19%+@6W`Mz?ZJaK@ zKs_pVVD7F#ml{f5en#FuvEcZ@MdRKaQ#4xRwIT?C6wx~Z@zlRTueYKzFA!DadW~pw zaL?8mvkf}6YqVpuvohM?P6hgaqbSgS*HWNQ*;pRY8xRh9Xq3!JY;{m|dud%zI8<6MzKS( zc%ED*o#GN|4i`S+;wh%|z9uI3zDhpFUf8`QH=tNh+-7Az`2)QS0hZXR}l*M4xHi4I*0&O?0* zd?+Zyp5Rl<428rzcK{8*4YiYNj62YVR{QZ+A0v%|FN8J(H>Jy3_imL1ZS>+Kz8~Bm z-YJ7nG0PeESz{mM$nRkx^lop|Xzr)pCB;+}qF{Oe>N#j0dl>R>*@~(>Lz=I%K3bLF zp*9Zk4HTjc(TaFu4$tRMph)=~4$Hz4bc0n2Q@poBh#%U^=H+BYb~%J>PLAnv^P7?5 z;niwbokMXNWw6K+Q4))vrpxGvgHGPk6=#(IUIvYiXwVB)iBjuEVnQy%S)9e~pzcU` z2^BFz;~Q7g-Kwh_|6aKx&rW zNhp+RAw&BL6IdJ*P(c=x@NnUPPrmu~lL3XoQS*et0iS;J?Z*R%U%)r0rzCz=$aVpk z?MK)JBl>lXZrRS7XrA|pc2;|2s2RjcjF%HX5UpDiKdo-+ZV#c=X>Gs650u7-O}l;i z2PL_zx42LEdwKxY8pZK2To=TAD?GF6^(WkqOwTiTM0;wYeX|{WWN~+o_EgImAd>a0 zuPjHHj%(!%P^+J|8yTQh`wS2&NgPpA;(gLD44&(hqxadH7f%YtG-TxZlmN4seY8eOzLke0(uz6cG+ZVLZ-p)3rL_6akTrgTlMj z$fK~{tSj29@p%S=s8?y#TD_;Ir&5a?wAKS>=F@By3?e26^THsCD@n|S8DG{eUQ+Yr z*BGIWfa5{~!QH^=t`e=n;Xta+y z#6LPvsdh9d)LK`#ER!{8{FqyGPPD<$>fjx$hsO%hbvyV3DB!aEWS3>jblS)_dOBLj z_tF~d$XE4G9a*gv!4GysUWhQ0ui&KMMyp{>91RIdzM|K$5bmZS7Ck`#Ar9CH=PK-a;kmrDb*vuuoZ@tIxSLpKE=djsrhL08{Jybv?eWks6 zN+A@V^7JYmGx;%vSDtJ?gpgnnGZm0@zzHNj{6xk9DKD|WApIcy9?*R5^DmfDI(zQO z50N{P{>4I_C6h3ft(AUZ+Dhpo>4t>aG$;RH#~O)`beGOv#5|PSLOleMHok^}2wyjU zXCc~^sU20(>iE9w778?wvI6Peu4@&_ur95y$L4@8?-@!LO{-(KplVfGl818QMEfbK zjcl%-D`LNNNhFi=i`-YSBed_Q1oxg_o!hs3FSnWhu&LFZNp$-+T?786v=0(6zOh-_ z_>awd?U5-?l#f$g(j4+x@qL(vgV(Ry^>S|{TWv6;1;m{Eb&G>x#AH&~55y=GBP&0aT z9eYQ19cv_yCU%d$W9yO~HvBXZv(zKRLsZ9uiR!55vEZtS>Ja&J9eTwYZeNM%eo}!o zgrAJER$_*u64U+mm6%bsO3WzTNzuL%6D+|a=uxP|4A-3$P>JbrGL*Q3O3YBI#0;fR z>0i;lGDQ@y*G^z96!PeK!YTxI5v7uJDYjODDV`qk!GD09NYq8WiS}mpwd7aN#7Ezd zjy#un=8cMV58ZW05_;$c-OeUS-z@F=@ZS;|RqtxASi&Z}_u|oWr{9E-(QGN6!=j!y z#o`P|j>yvlcKYp$Y_O}hk1obt5WReS9S8XlVtd!7-d?Rf;vPkpTD!~rUe7L{eJ-2T zGyM9gx0h$}DTNQIx0jEs-e~0JH~%UkJ^I>vgQ-^1LumR}(vFZ9-ueYy)FSiyYTk=e zo87mqkv>#mj=jmbQ2eb6&9)NLuz_?P)yCCQTcb1%>p=cpJk9a)M8406&4d}e9$fAb z)ZzP;=n;%W*C4V|*!lm0-y{0!BDGY(XyOF|7m#tx0}mAVGt^cHi4sRkg(H5BAl>qF z9;Tzn@lQ4A0>!gxT#k#UL@7q)=_A#_{w!Ni`$k9k8>3Y*E{47W$FJm*6|bmPgGH|z?2+I*{1pkJnEAtkZiFJ{dz9l zxZiwGUKdLO{eTYrqCn~NDd{1;C|TcNN3TKO!WB)Z>qE`~F`!R0Qz*UF!6yIckN~8_ z5Z@xNl68fj;E_-BT&od-T7wl~t>N>W-F-y=Rt;4(zJXjy@70g9db70GXzV)8gr|!K zo(v2M4pA^cdFO$I^_!&e=)#kB z(Nqm|`80ihsmr3!r0-Z3^nv2qZ)LqMBE5b9A6q~Fp4&y_ow!}>;GgrD&ytrqd~+NA z#c})trpQ7p^BeZvPcfVLqxHEj!9wbro9m^|ythi<+?4LV*`y5I>fO73-y;uq|~Ie`}h17TDK< zd&P0BU$HfflK!h!+E|=Nqn>&T&hMh}%^k|m>8!0EP@#i3b?RT#qrZL0Roc(%TyXTs zYCIo%MRl9%^tUguTsohk(@^tEEc(LVK@VZBQeZAr=vd7?`me0H5sNl+pkx{EAlrZC zK8yWdx6ruXwQ8ND-n=7*J@~>G!-bUUqJzCsCbjr_(MdsYUW=?NBFGK(z z-E`)AWL8NocjS^RJ-ue7FxAt;Om&GOVH1ZpV0#4g3=Fo>iRRxs?43QKAhU$HXTEOX zTfwmG+cK_qC&%M^Sh$ogGf{d$=AVcD&kU52`3vSBD7i>_hdNx%BJdJ;%^C%SP~I71 zM=e6m5XMknC)I1>dXDc&exxs@NAG20SXm7f6&zxbiHCD%j;S1kfLi>&OA|H0uq&`CcJI<9_J5OA*Vx~sWy{d+xvm

h-1mI`j?mn@#k|La->TpE!0$!%8zdVs3AcoHeV~ znBCnaH7;Fo?AJKVK0E(0+tb8$OPJ|P{8YaTJ7ysK3#;gO%c7XK5ZuF_VrLWXmpNAE zZT|I9^EFiy>d)>{wp#df{e+5yoj*@C0kq!vdF=Z~kQxsy+|kFDb<#30r5Wk*7i zpOO6}(Whxf{O~^cPuI1ob$m7*)! zXI9WTD0>;H<7EiM%P(8KjqG2VxL2{HSL(z`#6#E%m6Q59&bj0AT$!t*yUT3~@bjGQ zs`Pn;eC>)|-3QWBwrc#qwqDd!k;X&mW`Iq22gM;@;Jy&+S-K)d{6C$gU!}8jJlp2j z#AS`<=PhUHY_K$*XW7Ri55N@l;1Tr{a9QB`Njl2oD;aFozkESStpZF!!#1X~3Lmx! z2Aa!izR_mB8wP!@jn?;Jm+ttS%u}bt?nWtvEtl31DeU<+*3pEFyL3;R^bizlGJnoI z^xai~9`e~X`ei(jLVfbLw#Ml&dx^THtz8Y&T%OJpEcD> z=uY$8%@I*O^X>cN*onuJ$9lU}AXozL5p&XvEqKXW=?F>SWbi>C)@yNakveWV_ zKGi7su={OchmAIj5FRNwJE2dn?IKf<>=n3s>|<>Pyv zitTrT5Bu99Eu#Jc#OSTjR0X#h+#|a@MOs8>`Skf)^!Z`>9BZJuQ|fIAvi0;r|8LJv z``mO{AA?$s#zrAqiqmu_1N|JrL* zfVyLawEIte9pb-N?7*5}s}VDKqSA(PFWgd7pk`Z5zt<(4BVe8duyDJBZsobw#ZCWg zHP`;BE<=g=fOr?;Go0YQM|17|qOu<=E)*MYU)ztu$=g@=qkK~49pVqw{n#UV2hXnG zekEasEa7>(eX%>Q|3&m;F3pl{UtqfA>Jo7r7dBbesMa2Gz{99MLk!wsmMtKNS1Iq3 zj#}&Hu}zk?jwaKeS`Ke8xnh~~3GpY*G3+PHc8|4$i1gZiWf)xJzO$8Lpym-Lz;dOM zd@xLU0(?n082srg?~v^;`GvkiXIV=?F!v7R8LXWsX6liIgP`(%-Z^R6TlG?z?|=PV z8^z*w6o4HuOAlB}M?h~59nq5a;^)}1UJr9b4;aZQ`%4F5>z(ZLDuyuCYj^S$gh(y@ z3~KKS!mD@o7A@pdl-6bknw<#Wp>fArV?mRAIhma=h6spv^dQQ7M&XN`zd5CR>J+=P zACb?>$2T@gJ8$Pp2%}1iXs!YBd#wx^KUl-f+(y>HcG5XIY-A06`Y5Re==PMJ<#!oi zb_<0KHesb>G!$9WeFnvCs@OKEn5wFArGeaNe#6x&ns~-I$R^!uaO*QfJOgscQhWHE zKkOruZJn5psgLl8y%%HyXPtG-dpDu2!1mWM`(P!Wi4qf0#=l&HtbUoP39iPlfd^UF z5@`o#U>C=#fx;Jv6qZO^ATSHW4fL$*^nf_;dc z-Xi6)6zNqOrX{_KzaCV)Cv=47P)zoyVZ8%X+d*p&WdPcS_*dxnSb_-0f{UPs{IQcY+o44a;n@8DkY1AR= zRmo$;7uoj@NsHL~kJBCs2J^q5r3-{Da56#cb1dmO+^WKxBl!E^`#BDheVVfzWm;Zj znI{M$_}&UD*M0llMrqa-AlUPr?4y;Reb&YHY>~cXqqdRhX<|9j^WfDEb^$L#${Jsn zNU42(4f3a%fVKJEDt~sijpR`ItaPPK8QCViE_yvKWTSP|UK#`B<^;PnKqgglAwPI7 zkDrqJ8DWdH3^R-qUTr+Ya0$%U^0{ztLeC*_pd@wnzS&nZfEtxf+UJ+C4@+$OpbP2W z?m<(8VCpUVkoYu?-rlFCkKCuadzpi9K|pykXMfmN7g;*^aC!HA!hA$)7Ap^5XtD9| zyIdDhWNa%MKwXC&M+i-P1Cud6YapfkJhi!O4L_Kdc|Iv6yo90f;rxP_FaU0gz2|!M zGIU{w%x_D2mE2RX(rPLJX+y+BgWrXZgqcsXbcU&E)NJ0}EB!89;H?wp8(&?N&~?Zr z?Y_@Ew;48P1n>fK$Ef=l$!!h%NxxYPhzT9O*5DpF*VXE6e1_aA{(Gnw(G0~sdXXqS zewN#+`?&m3JV0{>2ZQODHhbp)z)G~tdD*KA??flxHWIMr?_^LUHn=$4DHh*B7~3ANX_jqU+ccx#0g1MtZIk;cHhpx6V4dMdsLBu z2#@dYVr(cwtCmMS@XJ+I3X4m#K@D4s)H*p_xZHor!S$DcRpYEe2*nWR1<}={;=xzX2kmYUw`hA)Kir<7rHl_35 zzxfGtZ8!I?>#fh#1oCsjhxqr+lkvHR?%Qj*vY($%S!=G#fKd+=o z&G07 z1mLE5Yz@o8zl6!hzoYY`?^VCi=L%pZe15;(?{j)pYxw)^eqTOc?f3hfU!XUFoI2kx zaK%Db6W_0g<^5{7g}-8@OfUGx>otW`{=Myal#9IG^OO;{K2LST!mp1xy}NDC4UqYj zNUJ+*-48zhgY!f2KIaDlQP3-&kIDSFi{3!zWAr)yea;W%`~3NKzi;8kpMSp@`@QBi zzmHj@!ajxl?*j*_Ub4tjCV&1m{@z0D&nK4WAL8_!#tL{m#&cXcTTH`qT(WmY9K$0R z?vG*UksG?9vw{c0@?Z0QmOuYn_}#*uQ{w(cDcOQ#d1j{%$5Je%nIqGwcII^MOQwjvYSlv7WER>#rTHs~A()v}#KE%Ja(} z+`n_zJWpZr0p?45N`gP3-uD6UC;j|JGTg}>n%GtA>|DrgTG``mKIAi1vi+o>zK(Xj zRug;+)w#o`s`@(-y!_TOb*f~y)v_zY?S0ChO4dU?biAOwr@`i1I+R}hD%VHURmoND z!~^vFZ2}LF>%st&euI*1jr)B#n>?TmgA41d!;-9x{QdCuA@fOlJU)rfII#^*>SKou zhUWs9TjBW%+8@+TXoi4CDcB!dF@FFH>%-whj}4;}+mIQ(ZF#>PHs2%}`f&SYX}*)j z)AP19|Ev$Q1I1gh{K?}77LTiMuyp}I{})JS<+Bk*5|?Y5t@4tA1ct zhWjn1tbc1uQPGyS)=$P&eS(7WgZn@O)ut;vv^fGF1b`+snFu=K=g=)AG?rPfi47DI zLuxAazB-3^GRKaL`8@93Kj+oGV)E%z$Iiq}Su>GUi#aU#&AW1kZg{6ln$V9JmYlzl zE&1DPZ+|iGf#yWMpRgzD%b;fzB|-^&L}i!Wc=h$Z9QS8pCs{Re9%Ad5i^IZBdWE)1St zMlb=l2;|_l%RsbYx+yhwq_A^_*7qOZTAh?Nd+)T;dlw8BmDSRF?rGCkY}}eZ=D?O^ zlM_;>FW+=e0SmC>Om8DAyplO<=b3lrEglpTh7>pP=^9WJCGNhwNTyUcr8~`?k0JS>l;8()Re? z*KL1oR^CLAd!Fyf5w0IVD$tLJuwWk)m{^qqFY2!tr%p@z`|ZMw!}c0ce&Et&X>o67 z-+RP4R;lQqTPsnc1)KQ|_90C%9BbvuC<{Mc#1#UUV)yBVEFkz1CS)YVG7-NI>ywZJ z`=Zppb7Dmy3E=E=zi#Nfc6PplFu-p8(IxHo^&D9}X3VN1<9{t&k&rUIATqLG`mBmP zlWG+yZSQ)krVj0VzJ0EZX6eagrxvEA%|HIbvH5BBP;v9Q0c9O0J10g(m9>sx^|VLO zX~^Rd5(?<;dywX}cYnkjLy zQ`Sy|5sIPImc2+C5cK^U`(JJDR&BO zqb#GU{df-|2fO^SRdNo=10Ga%`@xTDZuAK|f5w*%kKq~7cp%SiVsB+GS=@@nJXno1 zeJTtTc2A3$ytbUULsVPrgVbeNhN?+A-z=qAO*4BH)Q`8?W46B+xh`4}?cs%+U!z6o zC*V2YcDml)OGz!FZbC4PW${R*>V!mM7%r5@MUfWCgB+T|)PnNF{P(7vV}2KAR2{n} zefRUJ+Ql`}>nDcoJ#uuyrjtpVb(up8d;gukhAv=i2-Jw#%(4DQ%bUng8;RNzKyt;1l8NtKbu_ z-yj|Z>-vqMaufSpF56GcVbwF)rWwQ$+opn7U`;CVg>8(3vpj(`(f+`g05@_}?5Jg5 zXWv|kI}NB7K*3y?8`yzT7C3W8waz)rlqx2w4AM<$&*ybRGI1ME@2ckc&0nw?zyAAo zWn}G7NT(VCeL$v~>rbsAIcGHU-6NH!rAOhwcwkOafv8Pni`hSd~8q zNTHuu=5MvyWdR+$#&EJ3Tu=bAMb+4m@8#S(w&{cF;q%7~iXPpPpSEiK=7PitIiXo| z56mptHgA}c?6rd?b!yxCEknj0yt{2mV$!sgBV)#zdXIWXhj<1i4mFMG8Jp5nQ}JoZ z-(o8U;13;S!#dao@~6G2EDoo z8Wu>o4!U|iN0}-4|6jj z3ui7~SM-WoL`AGd!ttd{<-U_36PzgOBi1#llQ`fP;w6MNmxz!cdb1)G6Ty=SxQy-!&ACQgC)ueVwVaVcJ>=tPW+xwfdm z=VzT@6q#$$d|xcVaC$jU~}B<6HN(O^p3)!o9CuefgfUgz{y>NA6rUvg+t}cXeKSau&PNTu_rA z5;b{4<=CZT2Fzv|zg@aSoylCv9VLvl@~OPig$9!t=%+v?y1%x`76#K z@BJqHP?mZy=NY$}M2`%r@HH|O3!%)nm1`;wG7J_@S!ImSEqfa>CRDHm6y00B>|L3t57qUBY3=jxC^uss6H#WQhZ)MF= zC*jUyX+IfEs}$j{r&OzeW)=b_*icOGHhj1F(q;bK7lA2H=FUd? zbk@F_I70N3tqRlIgHvphzS%g+R2Z-ZWYnzu;91iPIA1rL~G(wGD{@0(Ca|x7GyRYGTR>zS-$fU z6Iit!2BN>78qYS0n<+xJkBTTV#?nYm;FH?%+`;(923UDVri>yi0|~6Uo66&O<(qkS zJ0KODkqReeVqQ zSLst1Wfd(RA3I>;U6oH=d1LIJPo-~uI#mlxTU0kBU|>;odh)n}bpOnN;nP=)o3v}* zkof6arVp>p3k)xqmX=bQo#daI{mTC7hZf}*u6kVN9l|s0$$u2Vx04G?vl9vRpM>Er z_WzCM=Bs?mp-Z#Ck8bcITQT}pj$rN$3`itu!)4Bp>FV!)6nHTSt8D4n7M5M8E?sN%>=9%hykf=U%C~G+v|5ZQfSia(rp-(3MXJ z-;(3<%bq!`pgJ!oXb6|PL%^_9gM)KLV0~+x&>z{epY5g0nYbpS*1nj&55A-J8m9{C zA$Vpmyk8QYODP4}AzQEWjupw(B!^MdXw zM9x%U7EZmb7^p>GjiDxfy*WV?DnsGk9jQHkLEoA*bf9G5M4BFD)D}w0vfD47<$tc*>K~ z)u&HLKR;Eae9N+_=J#`#wEmn6>CT*15al2?duKO}O)1NaFgi9%=bR^Qy)b{_%iG6| z?0hW!t$oc|j=g)ZtICGr@jY~&vPk?lTc`HKoyn`fGt4UzKVt8Jb_dX%!?GF8B%4w> z=|zWowke~AJ-s&k=&wkmGMt87W&cpuV6_VLc!V*Bx221F)HTw2{7j|%S=4L7DW2v& zhd?HWpox$gwz+iP2-B2|hOUVvbBn{Kr#Ez|E)U8b8JgE#ogJQ4GGJuK^kfn_rg%ho zRahXu18H%FyG)deq(|u`jN@ESyho${kaBSPNZCbmOG1ps_+}aQ#v-6}6H3yDFWFO; zw`6K+aOT960V6U8sK<()K?96_gZz-?%^DpueCgz5<wD4)oTaF3L#4Kk1L-qSd9RpGtlPYqMf3WxA-!_Lb2gmhs466Dfx^FlZsk@Ezy z3|n!*BOG48zZ1ftM012Vzxk5~kKjr_V?t@#@{U7nvT^We`7bA>-H8WkRy^?L)i>W3 zGMdcu$`%v_KlK>CFV_r&!4z>gBl{cnIRM{) zhq5_KMxlUh%jDfu?(F@|D{)-*s1h2Nl+iRMZGl9CPU_zaOk9vncX{&R0Vv% z^VRc|L*erftujNHn>_9~Trte*FsIDFB*{dXQo8*u@7Mr1#^C2m6FgzIUEJx|sG4_~r7 zk+HYcgrQZzVpVLU$bGi~v}a@jAVKOktXk5ilAw)KEUOU)E%X-13iGW+USu-O6N&zB zEn4+WlPp6^p{L8$0})@)$trw|Wjj*C^|&n-c{bRgK&Ipr^@=fhy_w*C$o86>h%pce{sA?0qGA?EJ#~`Qzez(kt?Z7tSAxK@_jtJ({*@{)H_Q zoj+&c*d*W^N@(}8Qnq+2Lgkg0F zoQUL$XA6qW;OEEcI9y{@M3fVWvTfqIHW+vuIN=P`h|^ygqNk)9!L~qAC2um9f=D+} z87*)0*1v7x{#V#2sl8KHgwO~V46wf_0dV7J(gU5nU7by#z8Z%y(M|b| zrE8yjS7uUC{gH+Fs^ldb_HjPJS8SKD6XKJ5BTHPVZ{q0;tFLo8Y zIs{(*tPX=0q}*gbULon8Y)QGxw@f1H0diImFu{o+2#MlJY2^@^P1Czua?+EgZBFdi zet1IB>fB1lab*RqvnouhzCK!G4pYaBs0DY5+wPkj5n{43Cw1PjB`cU~XLHk28%KwP zyg8vfclORn#p^D9ShadITu)JBcU?l`C(Q#%W?-}8u&-&L)Z(J8gkc{orI{Rx^hYU4 zu}a~eX;T+v6H(|m@=lHl@D3Xk=55tuxD8oyy+R^Gyi_q6l?CCrgLP)PXGB_>{79bt9>890Yv9-ZN za}ssh+_BT*a_fsDWmXI%5^sJ18@obTNIeC5IgK{E#Vye=fPd5nOJ6St!p-GGY5 zw&IotTT(M;E??bVo2O($iUe14d|1XH@7(&%4W0G5$>j@kCT~j+*Toja`{XokE*ZOP zbH}2_>Elc)2USdPlc#BLVAQnQSqoRJ+g4h(q%?dOTrKf#Jn+EpvTj(I(c8;B!5Iox?O%$#wD`=fY2s~V()hJWYFIx}F&eIQ#*fUSBA4J!{#7Nd^exKGh zqP=HoQAbsJk0g*ArkQ{HVuij~F`Ri}LGTzaH)(YRL$%cIK+8Zw8kv3>9_W7nhlxLM4)z=(( z`^(t8lKmN+TWmF;_x!(t=d$G{o6!P49tIhRQ;Y&>ed(dwNrHgO$CD!POL8g}5>8no zwFZgFEXKML>&7LOZ$Do>=TJjJ=FAnV+iQl1Bq1SYf8t`&q19EGIckQ-dwUkE}PTbj&7m_|Iy1BV2ZpDTzdAY4KGM{)FV#UR6USs;~!;K|7 zHm^cp&G(h`ANxxAaC=?nHpB)wlrdJ7wwWqBjA3t*WK6;3h2uN!yJe(&8_S6CdNFK? zd1cux4qWZ|`!R$uz2v{`o-Y1P_T7jNQ^6~Qxt8m*tlDlhRxkp$#)($usdpWc91c-r zxU55@RUN3wSvoz#7A=zBl0Ry0ZWf{_Iy`w)a%@6q7%_G40%1h6UC3zp5c)y!;x*Y8 z{3Xs$G>pHUEw~fRpG*0pIjFz;7oHs^YadS?S?ajq2W4o{7c$CKLvp~V%p9`(@r8do zKEq>e`{7-;MnBSSYuG40(_1nBbZg$I&l)SAe4(pf^eAoa3xZ^ur5=T)193zax8*3VfAHawU`ar!C=#3-Zk6&T{stmW%eqskiRS1DI)S*{Z(%vzd-t zwg!S?o+xja`Apw7_F}p*_~{KyqXItzsG3rR{DXX5YoCT$l5;+E;tdz7#eL%{Rqx$r zUwjcgl;C0&1>cK!@C^>`-6wss?P&9k9nH%6<{t2F2O{r#5qV1|g#R`^_wwiL+&^C- zyEO;8MmYt#=MMQa{#>n>(i;58d0ZyUz4y=|>F*FJEn%v}^2ftuV~(327_AD; zU-oeQKmTQ3QJ-9v9uSyTmRv7{HXVA{RjddpUNAOgVt!npTeH;WSlW4J{;Z4Jr#s{N z+$NUpIBmysa~ihKOmyfys{eldL?^f zv0|;VhfT#P0#CEpu>Pr|?059j9LKW-#d|luWS@!4Fq=PAp-6?M$oW_BUCY%*HmNAM zd(U8sPm4DhJi`WO3=Z=&gva~&4>oxM0Hh5zHQ=uXL-^nn`olC>*o4pMNB#lv^e5IZ zg&h>82n#4bpbwsvwEAw+Y^6?e_2@CuJ`~te^3o>A3(%+P?VBNl$i$@dZ zDR)g}Pu9ECs7MsaCX1#~ryg2u=x`pFgWG>2%&PIb^2Xg8j-F=iZ8vMo-yA(E_?|s` zmiafO78Im5Nng?5cNL8sF?^5O^ZMcY7H-rUj&?miwOo|`#$u)^E2o=3V6RGPj?t-0 zmZd~H+_+yzGruejDY$n>!4S$}Lw=zxbJUkXDV&Flzv+~Lv?tt{3h1LE5rz#G6S?!U z5RD`jWx4sGh}i2Im^^TAN=1J7e>+%We#pSN>CC&VHr1m;+S<|KpbPfyz0}dc{8h%Gc)w$K(Cd)EVn14HW40+Mt9Gwg-Rf zSU0)6R4T8%e^_A?n;KHIcmjKe)i+3iNyDS`!oG%v8wWZT9;qc6o^x}9x={Tx)#|A% z6$6C9E!{t|1f!0Km=vPC6jG2H8dgx1*V|$KdZpyn(V= z(WI<4pPMtA359si{?YsYwV(E(Qz59QVvR-&22hPDXTHPur1s`>Ue15oT{2dcJZ}0aX;)Eh=ZcoGgN2g|pIlv#({^TQ>ADx&vq!FfcG1?a4^1k+|I1CS za~@wa9#g629`3GVC_;X&uKTcbH*<_1Q8~K}=$-S(B11^xh5v~&keuaq+`#77s&1H{ zsIOO#~G&a)9%ufs|s>zUkr%uw1;YBoIJgg5fL>fu(4XSrN!ct69sUdb`$~99;n8(i#9FxWmnKL?C zdLm|6tak_dMOf+V9qBLluVC!g)cF(aa?AXO>Vr zIkLIFetJ+^tX~8BCS+LSa9Ji44v&LIom{^ri-R(gLFz0N8xca_*oH)cj!-mQDLI40 z57&OUZ$e~I!|J^g568s`mmU05YRJXpxJobK2|J=h{s?%zjRFGV?X@c}7P#f(V z_^9*&jX3wCy|=Q<5T>DPxUgj~9YXSQ>D1s+iGC~CPwgzdP;g(oSn{t`c9^nC2i;#^ zzcqho!;KCS1lbpppNCYV1!VwKx;RVUU0o6f9`gq!w0 z5l&7N?JYrAviNvPd0yBw>ACz=X(#Kc?5d1|Iq486M{l)bpn7w4vE~*`88oI3q!iN-jm7p-hA%Lo$w-gx<3)QDjP zrmSgYqhnvLIKF>PWlG|-)%zZ+JTc?+zLir4M-^A41}3Bz4mRb+`LjgvNH-IotXp`= zynf!WLx*SAu+a5VT^X-Z!AKKQOoz=Yk*mE^U z#-F%4ud9DamplIp#$`nQ4-QdW|6@%1ipJ zc+s}vPbYNMrwgm_Uj4wl*z=R8u+{7rX!Gp+rs7Cpm9$0N9hM#AXC6F#^OTsZhOOg- zvc?AUv5bb&_^m6HKh@Vu#la=*Q>E@%#1Fz0rcY&9eF6Di}m%X5t@H8>M4xWMWC8uk=T|Hey(k`B+DX zaH;L##@LvKN0v(`SxG}%uAY(ERUIoYH$Ukbl0Epw>6e3xvj;Ydf3SML^zrG^2SaKL z!=b|>XBn_@ub?{u>j_M~4 ztg1+K_Vm?GSX&XFC(RQNmmk~TIWr|?#+vTq<+W2zAKqA(vFtUb-rD=|#9@A^g9o@w zST`+U$lTqhpPPBE`k4o})TF1^Zh7$O>T}U8jb&~=*`-z4QyHE7gV!2&tjh%+5qUmt zrTK2^$dN=begIEC#L^XH)bsKSkAV!e7((NSnjBx>oSG4z2=50DsVn{)d$+i5NZ@}8 zI-hO*+iJ2V4na`yYVu$2Xquf7t!zdr-Git-U8`U*6%BuE@ ztMa8Ay*Ca(?qt2Acn1+G>=5AV)`)2J`158uEk0goXy2tx?U!^J(w<^qOP#)I=Q$SrU@vw^A+(_a+jn!!gby zL^Z3UV9ust|1H=uzc{{VLIivA+(Uv?IdfzApcU+Awo>Vn9MAT2GWI{}8P~6*&l;B$ zJ9>8R>WI>!EKh0C<=2FPp=Ec~vWH-25=*dx@9+4wkKH1a}NE4T= zW`_9D8Pa2{UGi9tt^H32V@F4r)SZdS0>J!rs&Cl=snT2$)Uxl@d4T+gvGPr}_lzZf`Bx!uXlr*Df zQbmz4ZFTtIAXoGK(^riSjUL}##T$3vJiL3uMU>9$d(RZtayR#(cJT5u{i?6=^i zKGTlG7@2u$h*M6`nlc7T!R*txtPt1p?xE=erGFrtTE1Ldl;2UClU}vD$b5sXny_G4 zkg!bJrux!1E?94V-P4p5Voo$B4DvVsP+hIM-Vi->Ot|!J`sAD-_GS2xX(=1(xlAYo zRX1p(emst4``O;AU}3Ex;1OH29~CNimN?QsZaVCJ252C#zTY&SyVt+`S4mXnS;)$sKV%%)=RB z1buB_Qt9GFj|$aR85;xMI4SaI-^dfl5x&~u6cXVS2;Eb}W7I>)@a{gaB%5d2NA>jI zpL?#bGQ;OTUw?SDgY5FP;pKxp+9mf6;kEZzsI;S^vn&Gk>z7sf#L7{^v(*nQ%2%d; zE@?gEMyBrBX?_6UxFUQ^M+GjeAb(N$$a#_ds2eyeaeG6^1o-+8eS~NPO^p;qokJq1sk?M&-v! zJFo%RmtR(~`W5%j4DXO!#evH2i>q_|Zj7j{VG!7cPjVL=tU3%B#-A^!E3htvSOn}z zP7z*m3`*y}uwU}AbY!e3i!X!$0)eGczC~S8_s~ZRPn7K3uxLWUGIp#qG;{Q{n!zn^ zy^^!=!G_xHGvhMr?kZ->zmvWR$euPNZ)4};$w};QRS&lpg=UYdo}I?VvY9iLec_=Tw2jOaG<}K~*l+P2ctz^Hj&JOdAI+$3osDUSSNa)E&Gy)H8g*}Zy{>&8_ z#$n*`|HIgOz_(ePkK?{q!;bfo_mX5wwrmY+%UZT<$wS_I@4e&Lu@gJaaP}Sq0)#9` z!b(Wkgs{q{lvP@27%i&-N*OJsK;zf{=kApp8~FY{|L`HUE~mTao_qH5+#T#75IJLh zE(vgHH7r#KgMSghvk~D9PQVr+SDXazGKjHw2;!Qf^tG02C_?Vk##{?d&Nm1m(w(ZR zwl4HhZfeoq{lHZDhFTd*)w^@|*z`JzqCXm<=u3U*9M%nl7FnOM4-%gW zV8yN!u40Em56IPm9>+|qmT{p#6h3lUpOtL7tG?pMcn(jVueQa>;sO=LT^dJ|K0Hoo z&9W-vjwYH4OA1Yi=*-~pUX`lraEGa{#>Ttg6(!b-ZJpTyW!-dvqq`_WqjNN7W?6HZ za@1;bHPm?$J;hGso`l>$7DKr%XxI?5y$%GN&VUjWPs}!0ptyi!Ct4=YkFI%WjVT~G zyLW#ZmlKt$Gw4#IejhK@O5?fK%|nil{ZySbjU95Y70SNW#u%+0uKvopWAsO)SK}`3_0G!f=dhcIB-&4Ks(YEpD_LrqL?rCpWZI3E% zG&oa(sTCoC1L5+da9Q(#j*^|-#v}V`&ul2Zl9bnPuUS(l5ICo*iiR8sDPxy5<>t*?4t(xjAEmlP!Cmj}cv`2OL%Q16|7Df(=&x+O1FlHZw8z0V-_b4=W^q5kkdMr!NP zv6{KMG-3G;Ob@UJw3%yWXL^)*bi*Fta|~h`$-6)yKT9X$xEBJqDUbm1(p4`_UUNc- zU|YIF-|Q2WtgTd)^OHF0(|H-rZ7ZO*Bp1Jz^*_rN=sMY3e++%4%78yshh= z8M_xx6dNVeWJ+^tl_C1RBW>2%vG%-_cK@^zTW+I<7p84+*hr_MTM5QdT{>ZAfEoF)E&JBb|9JGq{KXw+b{NY?*FPq^Ja)b(MPTb^3 z&fU7P#AE%>G;wZh=z;Z)cx7fzW>i?9AU0%jYj%?{N@%UoD9X)3rL{?tEsMxrd9bAS zjxkeE?;Wem#nU(CnVKEx2|=tJUujmA`sM>dM^6q*XDv<(2$$!pM<{4dh`T1tCzvKoR@V(SV6%J;cFKQVVD|vg5NQiqbBPdZ0=h zHh9Od-rT>hyr?fn>|^68OiqQfO&^w=-;=qeu*VwH6QQlO)(l(1gQO+q2!-!`tk^V- zMiE>zTbC{_TUS-Hx>O#>%cvSJ@xb{*@cTD&PecAG&!ZU@?a!PehTCggl8d2AD4f8B zli^T$!vh)wfkCXYCFHo|Go016Q0&c=ecgS71NTm4l~(3WH>eCPQ`VYIRg!G8pG_R4 z4)fKQbsN=om568Uv?tEr8JL`{O0-C$LX#~LS$0|&n&oGA+Lzj@F>K=CV)qE__;*y#pV;xC#k2p zzP(G%d*Vc@TPoo-4;O8^ySekB?AU_B+~|%wR@+K9o+{TV)HN1Cw6P)6SCL_k3XTo+ z$(g-4*ne&&hk83HyGd_tw!}voTXHj+Z9*z(_wP4nmX%4&$st)=e!p|;t2=WyI9AX0Z(hk7o-t z7HpF+U=o`eZurM%IPfANFz&l%%?w|c9M02s6z9}wc>a8?c>Bm?*5qBi{pY5$DZ~Br z2c6X`_>PID?K3A^l49%6>=1Hn@Q5EqL;lJNwBB4oz;wq)!1mh_?i*8EgMZC`q1SYlK_thFttx>MyBnWBxQ z|0}MmEj1^4^Ru)fV`6S-Y6{=`{!6;1>FgovRHL4=;rxJ{my%gMWGfr83%zoAGDDuM zv_l&rZQj?BuI{T;kMqQuto&k~Fp!t<2)wCA9o=E7&$GlAjPJ?D_@m8UQS3r|w-xqD zgyJQP4fw<5ZyJ2Q<2p*AiN6%fJUHMG$AS~WGBy~5P?-HINzVezV@`q}UQrTzZF6H1 zO13LwqE!X5^n&!rh^YTXMFyu87_@~lUR(^l;0L8{;p$3rPN~EfobJ)O3wvrdDvFw$ z$|LJn6>8(OPOCYkKe?c&Tw$N?uQDYRthu#ZQdeRQSYa%wDXerFRs@@h8`Dbj<(kOkjx&q)$SH^`LFx)y(k^?1kRtdnZf07h;K5IY(>_ZLlY;3JHES<#%r4$| zwm!MO#JB>*W_IW6>a&E-BO$RoAKSpToGnkSF}xWpNH^!DWHlI~;xjw#h3z^XdU5W2 ze_F}Dm)5L$YDZy|ND_!%OU_IU4@|PkQ}k;?R9ao66XMk=mFsJ+-QBUf)eyHr(R!e5 z>SUWFrG780VK?xXjr$l>TjVcU11^!o+lhq$R%5U*251(u&7Mol7a|;O#oHe4={hrM zODS7Zl2KtZ#0ks>U0eiDYb#2P*C=IDX@np>J^)3RR+q*!ot?@5Y-%*4zf{h*b=Zup z*-(PN?Tx(!8LRFYW36o6QIirG8yOIt#ETVG*rlOyiE*JkWto|d7n>zvmUT~j)&Jr{ z(#{jBW^XQ!mXxi*biSbDrUAbeGjt9@4PzbCHe+XC2b`g7OzbY(3yKqfa-sIsWq*Z* zYw@bNX%T7G;(Ddlp;aXoCgtShCHD?_iOchcR{`~2i{VWU+sh2+b`7|aD?0ax{1d7ntZJ00J^E9z5hCL`YpL>c_2QHzmU)bJDA1Bb2 znZqL!)8fLSlWiS&rh1z!Hi+$G=sU@*gH}PEqZLQJ-B$1~M5!hO)0jvMa)vBaB!+`= zn6Q}5^MrgHd}3>09I#@$ThGlEB{|27Lu18ZD|q_0LPv)+28o)x`jkobxa9U~W2Cje zOxchUouSu+r|RQ<lj1<3H5;w!-A{!@KF~wF9wBw9Aqx<(vKXGBp>c= zK0aZ?mu0kADwMnx$rS~Og_h)~$p1!!CK>YtGh?Eh25o=o)^=^6tZMu7+XqffI72O* zkYsE7&Zdf6w~dyhAr{XzT$(n%e*L(8?un_I`;V>@Zc1&tWu*S-DqDMi=U#a1(UUX$C~;CsOt2+4&zO{6IWoP`$o1v`t1WyC z@?!{SG;^8sAjfv$);LBl;|DbuGZ;KYMvDsY;@Rw>!i{$|wVoW$@(L4~5<@(5psH3k zy?b!7s52uxI2N9_i4FEkw5rmSYMG}?fK(}#pD$ixkEhT0SXuG95bN8ED(Z_Mzx|0YK3k%DANTD*0r4Vy|`quEGkT2YfJN$g_&~;b1mT#D$SS}kkWnk#;IdXqA+EtAvie6TBU&8bSx(! z$0E-Z2aWcPt_c!b<>iNGyYfV|mshkpEhUY|W``yx%Jgw7e7F(v6uCB<%MDKzSxb=L z3HtkI1}9%YXlLuW+ixpo3DdP9d!u6gSnL&kZPuMP94o*0(`~sGr@uc{dDE70RfH*C zoTZ40R@xjnqwqP1FeR7Bmx_I>+UZM?)N|G?Pfh+v^e5*eeC2x(iC z&Y+Ku(x^4jp@IniG~k6~xVAh5WH3a?F`qMWrNAUtr~s$9Ak1{#&>*50EDa6gL4tyT z*a9G*&=s?1Zy1#)nCn?e`s$@22| zdCkYqlR`+Qmm37aRBd8vxm8TvA0raSN+fcnE-tezJTT3WC$*1PDq_1I*jyB&E^J+=oQp&ye2g%cAfoYN;nNl9SJEv4T3Kl?IR&T+U zr>z<3vo+d?nhGUI(xmFWeIV~Bu(?#+aKJ;G$HXtJM}Ow{lLc3=;GoblKWm90?zU%L zR+Y=;T1seGIC3fH*2hPN&Q4cAkk;Co6YiB%yr$CFk((TpoRw}i>5AmMptK@$l3t~X z_lt=L$e+BWtmD*}#WHcWS5R6~5FMBAv`4lcAIsEtY%R`T-=vwRLKWqf+&X1QaC)sH zyGkCUX}od37$vnhibN8#RN$}ZY6c69Z>iI-h8%TNrMUjkU~a!d=*1I62BaFD^6huG z3UjB9*EOGBn;Vd5luC?ALDtcuO??NOHNF8!wtlL)X`M461uh<|Twf`Hd&qzlG0xZm z@Y^6Jxx|OY#s?IyGKR<^?GQ#=5;O@9@)Re**9{}5`iCChSuDf;s4-QX*OOJ;0(V}) zi4_R@^Mc}%!&~AVMzOYWD$Cedk}JOIL>s1p8&0lq*j62DvJaJ|uW+PnZF3IT6H-fu zb6C=(Y*k!<$fk}{h~xdvhw6&^9hQ*_1&Aaib?}y*n_;^N>=y0_>~||81F?PV4sI;7 zI^6ak5I)3F8H3s#J3&iMq%bxXcOxs1&cx@)lP%|0r_ofijHk8vrXR7K5~yJD6e%?-+arEi8@xf zZ)Q-@nvz>NSORzQh{H8i=D6fSkwhXYOpY^GX~HEHGvx(ar@B*8a&pTD_qEB2Y#AA5 zfhQ*_&BWhgGEhllH$62raYv7QVxe6a@atNb-NL5rBD-CePN2G$Y}X>OEy^S??3~h8gaNv65|yR9tZ`OX(6eOI$8Vn2p_e*M4U5Hocg9D zzss20pbibsXfMcW&5TEH;Lwz`WB^Xf^*yq+D8*dBr`Wr_FB2u06Kek>aTD?2;y>#1`8X z5kMG*fr|7$z4{3DJp}qZ5&O?RNm5mm0BS|zvImOkj?`>fc=pVF12spt^cN)e>c-AZ zmhb8^BxTmA=bfC;S6)iWtXHR(X2}B?*}!1|Z9$Ei+kOQ32~-5WC}SA{L0v!a#aNBk zmjPxYxt|Azc{m(a+tKIRH@?2VI9yYcm6l`6mHWho2&yY{L)*3HSkKh*siKnACCN#) zI@RVkQ;ecuu_4DG9!pT$>^5~mT;ax!#Ilk+T>@w5+*D@g`7gGfsyTD(E~9UhNR#{w zqP%$G`2K`}x&1lxW4#Y-c1kN&Im-9+8V5qt%S_Tdi@|J9P0dJ&j~A-*HEC52jh`<> zzRTdGDCiBKO)=O~38Z?O7{U~IV)&{!xak8EoCd4IUy+{JK6AeRyKmyO#jPtXnzr&R zUVNG=#5&V%{$pCnP?n)o6&`79%Qr$i27NvL$od=zfYX!kucAmVPS9Xi&djayHVKwWM{$tj(ULNKuMs+@J&*SQG;Lj zbxgcT;d#9Sp#z|MESkW=BykuOXc8F1GbtP@p_4S=^15ORU>~p}=yy^xotdfSTkfiE zKR;Kbt{OIk$BRNe6SEq0MJ<|OlKWu$g!mVVoCS&MYO_FKZ7{@S7ZfM^1^5L?i%e7~ zUmAT^WRAlcYEP~#$<=^qo#KZyvGjT5paX-oMq^k^LI9iXAEUIIwdmC!Zp-T~wU=}_ z)|~B?<*dD{3*wbhFLRiww8LbqwLD9F(3@Z>*O(($qzDNoi0OAT|{U z%tP(?5B$#S!@irccJ1KrgTmv=SGZ#ZNQlL&Zxg+3xopU#gE zL&+aSe340@QzgDde=!~t#CZFJQW-f0M@8`I34(8o(;p-QLiE>=((NB^?jT)yY2 zLDm;3=>c%R=*ppyj+m0&qjZzK!n<;HnJQ#sNV?w2&)oRCVET_cZj8yOw;+G(WTRe~ z)dWD}xFg37w#03VvW|_nTBpRIUndux+a_|UAtom0+I$?Y$2VItaVQH!?%ld-dGdio z2{>*y8(-GLu?sB~>EhC|vhwm2V_mi~B#g6y6K%`3rpIZ!57ehOI3#)TN^_j1K*kFe zDEXdn{KGRRz&X6DB-vugk%dVU!xXAolPM*;I;~;%aJ4A4slMEt7%Ei6`14~!BcuBL z!vZ|jSukEEiBdV!dC^JXK7q-0t+Ys=z+MsT7h;Pp{yZ->}7QWR+{my9a5IMt1g zl*~2fhr@)45`D2IlFwV=85j{9q71S6O5~~jP_G<;x4>9wkxSDPV#@04^Q1m8^2BI9 z&zHL~AX)Fk`Um8467Vf9+G6+>AAJ7>yHfD5hlaT@kHPn{k01lF7Q#D1HqmA4;~-5^ z{>-U5{p3i8GqrnI5D?_eN-G`7w%4gb6Vi>^@Yz}KfJn18DM6VL9Ue434zIXo8pPi6 zw(6?c%9Jhs!i==hJ61YGeu~0oxw5TDF0yx+GkOZ8=;Xj{J<7mDQ_bq4-n~GMN$UJ` zN3UG~v7g-9m9|K4^=T0+OrEDw7bwC!m8az_p9GUK6)w1;D4#@!g(r;k^N&r@*hGnX zsleAaNs}v;6=q2M0<)`I-TJDFdxwWV!Hk>D;K(Po;IFUHRaQ*SUBfl6_}_%goluM@ z6MAY$VKJuC>%Rgya;Wy~5`~fP>sL9P6-BXO7H>K;}K^$ua{*7iJpe1JrkTT9LffIeSvuoHoo6{HFPubW&( z1jnhs(-q@Lx`u>B7ES*+YH8vZJIoY2F(GfP9K}HLJk~$TM?N^U$?mONIX-3KCB!-M z>@n4AixY8{q3*7^lD~)R8**^^B`v!wF($P#E19aVKe}~H;e`!5&J#)coP;QAMk@D` zcR(nQqn_GvYt86GvpKmN9vmFqT@uT$*wuINiILRQkteV$!ai9N`#zj6#Gq(|I6MVD zIwLz5{W2XK(}8d!lwC5+0UeezhbyN@<>hSZGN*+d^0J*SDbHh z&h%D^=3*4Kf->=$yYgnwb#XCI;aaMI>V{*}0snS0;Ky0qF6%E+q)%9!a$b0inw)T<~ z#VG8O!d7za-D94zqM(055(o-O1axTEFAVFYZ zSq>FdjN=WS1dc_+;U}(8N3)0ao0vVAopQk zd!{W|Z_5(|pUWwtaGZM$NqpSW2Etc2+S6Zny0KV z^5ZiqG?b`ydxIw0YlXNhmoCXDl7_@L-a)tOH@tD=hSxV4y+dPzd$*K|S4e#0MX3>I z0waT07}vkD@7SL<>0@IaJA0-}UevB;IpsOhD1UZ*!|j{5o^FH+Si;9@_FT(N5S)(&Z`p{#ER1x58k_KFQPQc+${rllg=lAM)L+B|^QvI%5T z2FO&XR1JaJ;w4~$Y-OxzNFFSPo5^j#u4c`qKc^M6X-q|_LGhL*^TvkFmC25&6ZJKl zh0}YiHQI1)uprj6X|^z-c@n1 zWhF}TS~TZZiv65x?`*H%-=$IXoLqU|!}8X{SVn+cTo{AAO_m3|B?J{|U}6G|xGhN> z!(xRK*NzEci8N}R>mpBVQ?rFz$GE|Me(CL1hR*!dl+x8j$$6$UzsEQs@zQu+a!^#J zA&o0NL1p*mCOKyB>ub4ZQxWyl@ZHv0X_r+ySfh>087=GI-kc{1x7Du9Ma8WdG2WhD zLTzSDmby!s);PGSy!CL8*3h#T%QGl&VI{IYh4@Y~KGlogDsu6+YxfZVXdxEyVgr1} z{xlJ4q8<)A2+~PBaa?d597nYI`931Cv*cvKJiNF3C`+(Hr)LQPdPzOuS*$D-_P|Rq$Kt9}v z>jH4`#I+0AK`Jo*!_arzw`@`BqBuL~Z|Lum3kGc70#Q;xut_R3$BLEw5YH$@Qkb7j zyLXQD=eOT}BSBx7Mt_E)sTJuUVZ434LxOB^0xusgo=_UG;1RMN_Ly4u8T0^t_cQ>i zzdc;f42k)kxExZ2SbP?>9&KAghu>X6msT97S5BgB`%(Qlaxd4wEAS2h>+<{>bOzH4 zweTm9FHgW&@#KULu4Qx=%95WjB9#9#BMUX*P-4ls*iT2GP#->wr zFHExbrymhqors@qe`+gzY>ZwDZ@TvX9#2>X?}vU$#lW7!-D~(wIl!f!-1`H^&G98E z5SGA~%95k!LMU4WhSTEs5YQ>}l#d6C&ve593XEBtUDWE>RsY;+Tnp@)!rGEYN+Sljr;2zk%{!jq*mycGj z9oe+k(NO#FnbQyFPi^h!cqb*LroOfgNl=v6JMXPuci)AE`L{1U@#JHGD;CbSLv5fK z-r-BYHA@KWVmKiW*+6_?-c^f@ui3+psYRq*n*4;PD;qO?{a=5!^Ut5m|9lqypq9J( zSB>}Gb=SoD+0}Oe&VFhz`gr%-4<5U*AD!$+dHr-l-S+xx=SCj}rH}a<@W%QX@D_#n zn!yf<&4Mui*a#02;)*^lG9NiSw}&^-^->p<@}j8E;guzamJ0mf-V!)`NggB@B$I4mL6j z9_PeiaJb$;qk<3cVidXTfJ7ykU&SJ>rx5NyKnowe?JV53tA3zFh5UpCU8(BnVTyU5rDAalOK?|QM$lCj~Aaq@m> z2&|eE&8y%YdAz{@Fyiyr*=OLXP&goqUkjx~D4zAiwR3PCzc1yPzXQ*pK!u!s zoksNQas>)>^Gvr#F#82~nbHf(U?@&WfNN3{w7O6b4H`B4#qRu))_?gWdg;q{`ZbzO zzlN0P8d6fBH{5t5{qB*Q4`Kd{`0@)B32Xzfc6sR8FR;EL=hLws^iU-L=W*dq0j$Ac zc#w(V=$X6kM=ILmI{(t@&u7-@#UEbyd^zYh-8i`yLx2S;TB{T5NHCa89PY{b&S3cRlf2dGJ!a!SJ}F$I+FS6#YMTCoMtiUI z{-S4A(MYLMp0UE>M+*)e3GkODkKmsHla@>r`9<@I?0toY%@+D9{F=F|&?WolU_ZP- zdg8mh@YS*jAZ*Zb(aylY>=6M?$H;ol#v5+B2_@fjLg%AK2H#$1hWtJ0`b53Viv{F|3fA^impWL z50S9*#d)~eowfRKTSw2n0s24VGlw3<%KX8#9mn5?i*PU8(%gB&1uT20P)w)z-B&_6 zkX8hW;js?gM8A8}J17z^`G}-KKmFp1PXWb1J(xZSZBrrSJE4Fzh)iKpLuQPTcj&9M zjJ`^RkndnxgYUpS?k=7Uz!K8~t0)(D0c$DUN{XF$*RAw_@clX$sqtIs`bUPkx(6R& zt@`>w8m_CvqCIHh*cf~t?k_ z6u`}RAVtxuYt5F*s#~e%v(#Cdzj>{_RB?v>=)j)Dm_)Sc+<;o8QVvlO0ak1G_JV?* zYiHq@+qR5$`g{*c$d5{Jq5+ligi7^W^NX=$gzIjYLl``G9|i82V8DW?2btc8(5p(A zY0m)^e@0PiU%MIo72udZb!%0n#f)A#H>6al)C1?J0RM!je3Z~bpKs4V+j=k@1qIu? ztyY;zbwa7a@}&aSA5UcBIU--UAYble^id-0h)ZIHaub-lLb14|d{J=U8T8_rf6yEL zafafY>EHi5$^#_ri|@Sr@;elM;@jhQcz@>>o@c+v?!){a!dPJBt~juTgm4Olm`Hry z9P5ex%l#uSzcc^h05$Nt{g)r0W2p{Qd!D{Ub$kECvv3Vo>)H)lPXMpow(vanJs8gi z_8zQOd^n3St^^{I8e~+Rh`Aa9ZdfH2%+k4Tvny0Kn@VAK|3p9k#_T9W;jpQdHamJq zZnr5_Svgz=oRfnXoYrOn4y@?~2otN&%7s^p-$G0)R!F$+n71}g9tT{c5ui&Onv_h@Z3G}1uCw+#wX>A7IneMio zK}SGl6803h9ltRNpXXC<$Y%)c1KhnJ7eH4L>liFMvK>4`Oa*uqlCc#qmAh;O zimgOZ^WXKey6IPr+56T^|7~^ua93Y*-A#LIGzCQz3*Jb46bY;8ff#kk)|-k;fEtufN~A=6m#a_ImoW?APfx{&fEp8?vGz_#Jt|?_@n7=Qt08>;W|& zLi8W;y0G{GWwXStBsVOIP1txGFB*LoOg=UeKRa49?lLcr96EFaj7wC$dqa8qmly6q z9QuFvT==rRoG~)%>*Wf9Pi(WjOL*oO{(K$(6a=*Z|6mKiJ(k$cFp$qKnH9|Cun4?P zuoZ^bjK0vQ$s4y2vjl6+^<+V!7@xx;CK|MdV@sIW4IqNp(jAZFTU$yN&d+~&VZo!g z#hU-fj_lNJ7pSoFn-tm9&evb>?|Sd$8mIU1{{G`W1@$ky*VX^}>wEI$K~&yA&-{ad ziW~wHyswJipamGQ(?l)&NF8Ne;QS3F64w2E7MRL#k3Nxs#0^6fxLb671uE%BrH%C6 zeI@jGaS!YFXzzTsk+tI5e~rdR9|gqVvA(2^vR{U=ScJmh6er{ymUvu1Z_7P09=wL8 zw(2M8^Q}Gff6CGLHrC-Hx)^DTi|JR;+K!8e4?kNrRT(tQ&Z^8z56>` zCuc4^PQ6q6WVOjrSmb~?VZPp;f;w-%p~jijc&;q!Ue*zy=B~vK_f*-m+EZB14@l&_2>{IiN|NOfuPJsXx>Ru!2 zai9b|Zdy!WSez{V}?XY2u6OTOi?4uKS zb*sNe0ra>3rrx=9{@kO?x;}XB`42Jv5E&~M#sZD%zH9N9JCr*$Mg6CUE=B61BKlQy zshB$YTYIr=EHHt9e&DVwA_nnKLd7ap?z~4axVSOlK z;*x0|@~FrokJ`;T$@vC!GxN$CwimH+r@_!+D=JZ=h8Hu!!g;KWu-)rUoj83WcT>hi zlwSGF@So@fgszm{+s6KTU+1OEk3JC}|E`ICDK+(<&%N*!pZ}t3-RwA6cl45V!=YLi zm^ku4Zuo+=qmfB;brOwCP0BvcD^(OkSh;IU5jYiZ<9|7to>qig6nAi>?+`|lq ztdsNsG7j|)p<)x%tlogmp;w?U_%uMG#2~z`aKV}%%%$`?r|}@#IW@CO+KAqs)9EVL zQvZZ019r}UXW`nrcVG;#V;1)gJPnL#97Pdg=fkyt!`(>VPYbz#U(t4OePck zJUwhgd+77-HLx;ZOuwaL;>~mobk4Q=rg`)3HvxuB;P=m$!LWiT6=wbLQU-xxJqktz z$cfPG8hZaE{bU{e1Z+E|qWN=F1;HYMTcYY0#Bgn%_?Pe=rTv(}#&&a`4peHh< zb^ZG9DD^BVm@S$=v)(n-AO%U^YoAk3&+j5@SuzF+!U$RJj3EmZ_3rE}y>n{*-NN-~ zHRq1`Tc}gUc_nO8&wMc}!URD_%x{<$uK+Esz|`mgmJ#E>aKPunt_{4*1!4yqAaDse zT8*@~-A2E18`8I)Fz4Fsx#r_-f4FAPwH%{fy8PIs%a<=b_M={FJ>J!Q+^W-k{aois ziw-XK#Pg`Bqtug}1;%C%AvU|GMhU(=jLlB~<5Z+m!%;D8vx|Wrp83N~yZ-&~Rr&$? z+1JjY4_95d{MU<|*osv0djXo;b zk4EHOB%>cD+%i5;(-BsBC!DOqH17wpuN3ABVCcdnSU@JIct5P6|53!sE~dXCwDrQK z=bzs+pW&h>6VMX}zK7);!{o&4SZd=wv<8h%twsr}r;vH_G(CPN=MLbG(i7{~pFkJj zH~Uk^jz5WE;8I695*Q0dHH1D$$iy?Ek;?TzIqRLCci!#!p&8Y#e(s&_E6)$1fJWHb zJLhXzUckRMQfKBXsFj$yDBz#n)CW+-zJk0k2iYtv=s?cFdW5cD8!%c)>OV>=LsE#cx!Y#O4NUrj5MG4p2Vb-VKN z+3H!F-I8DW6EeWe$^CUr-6@cy((9(eeSFPk*Lqo@u--6cy$n;rI}FSWfm)R^JPeNI zieb5)fC|PErP4nc8Tmw@))dW@6dzqzq)`iGvf6s7OeU?bmC4u#n@n_~u?b4SYidx$ z`RHi+LnFK(ZnexzXIk}Oh_Nu9*)uS&H=IbcTwgUHm5I}0!vBphtq5Kcb)lXh#ucWe z9>bcHeb9XgkmJO-30J7nm1mE!r>1iq?HwxmrQZI59-xgRppBonERcZ-K(%;xNVq2~ z5OxEeyMkLNiB4ny(`PaA9N8XN0s8>x4w?~L?5z4ko)`FV}L z$@u2L%JtJT>&T-gg#K8e?l~~;GDL?rg4muaC?m$C$KZxK4ch>d+yI1UKHOi(L?$(wqd z_UrBdq&3AHUVHF?`}Q3M2pXWUZ+}O*kb|?3&$J; zFe+*5vw@>IAbyQ?*jMfNu6=xW= zu`x((&%>pKSeBvN2ye%c$Q%=!U4FN=h@mSGGoDMRN<*DKe$ci?Z}zDy>2A4BU!Q#9 zCNK5_^FM6Z%?n#+)N~J*x(Thx^oEPd2FUz!2<5M5|C_`HNqk5k!ql}GZWbbiBn}Q? z#K!dxp;C}26ojip>Lls_NcTjn>=E$XQC2wc1H>(`4NI6Vi6{(D9F5Va#NesHlR1y~ zjed%v+K_zgMY@fCxaFmWUTlSYXPkfes>yUah+gA=3SITia1XlJ@-F(RzLAgl9oFJL_OvZ~e1d)&3{iDMQEvzR z4g99BbGm-}z}RoH+R57F=r-y>fJ4maPWK$ZB+K^-s-y_Z17I3FOLxw&Ml(Gqtq_~o zTD3uIDk`n&g4Lq;P5owzLAQo>Hui-CPivJq`RInBvffUB!KK??g88s-=HY?UFc2XP z7XT3?fDA+8OD!#zS}q^F{~{7yyhuGe2buLv3m%v5MKSl@OMiMVff?(nn_vuxAHW-M z3`b*Kv6$1)24b$`dS*uM9ywT7TU&Q9b2c-_PnuTV+^UebIO_u0=9bRt>V12wtD>Th z6elLa-GNzIcJQR|erK{?pfHR~>=NXhPz})Ws7wHDI|hvP!iG(oHtfCkUP$p0p4&~m z#Cjh055bBW;+8l*_7dq%U%AM79$t4|zxSSdaCa=j(K+fT7~dC1C|nwbk*L@=WV9_K z&lxg6E8K#6e5pJiqVF4JeF8$2i0&>EJf+FVbNX9sM_{_$jjnO-blEp57#geAWs-JR zclX4VEBrL61l4fr8oDaj511Z%%tUW>rYbaL&XQ(4ju33!05*x+!=2cR`T+YTQ0fqd z4MwoQUw(J^BYt=GXkYIKroytSF7`L`pBwv(meDoHVxrFsmGyTbe}bu{j9ZV(Sw^f)!MK_Udf=>~{K=Y_w%I;dnbS8c1xC0S$6m44AI?75U&^f>=ap`D!)hrT2k@l2f|=5*P0%N=fc~cjS`< zU?h#2m5{|3j87n|i9XO+q*iyLKoh;E4nzd`b^fZc-)I%WK7#*t4Gg+J0iIx$a559S zT-f-7$ymtx-$JAwc;a&B|C~QSuW%49J?LgI{P3IeB9JdPbI9%KOX5|K6uTa;B3WW4 zUM0%*tE127uN1~&Y z%BX0dyE60ybr&}Z>?|ozD%m#>{!3DSpqyAiINnW^B$F;Ax{~+2sq{Vm+EA3 z>4v@Id!6dlPE%TH$(Swk;+bwS^*z6%WQZK<;1^Bh@kfkcw z6Fz_)31KwA0An`=>a@WwQFJvmG-762V6!=Lyrj~}R2fC_6qZ95FCIF0f1S98h$r^9 z0nzbAd4(&ygGT(7QIXNHNt$2@7Ed6is*~owcS$V_hXFmoD&bIYPXsu0Kn*ZC{!fPu z1!6>Fdls(agO6HlnP7Cle;WAcwp$<*LyxZ$rAgtbdz2y?a9AYa0{)&D#esEMlE9dh z^c2y|-gsd|giav{M^F5d3Jew}ng!(>^0QL;QHF#gy;h_5SF^Suidvbwsa#-Almz&* z&3f8(P1XD_m zJdvILea)AZ!J$z;qM%^5F(ED?RwPw~*={UFspVf+h6FSJ0JW*`4{7iZ2hmR^6WW42 zbbdi7C`dHs#bKkUnC##G7r0~f&w9V0z#!2|(iold{J&sZA(o1Hd_Ai$*wYh2HuF$J z2W>JBM72;|!JgE_Vw-T`TUG#75$wsycDKP@ogUPmNE--x*LTYn_|#pjS?C8fQLZ-m zLIU+H)d6i#4T0M*?<@pS4^y4M+fcg}!kDXggD{HlNhG{~Iu|*KpKq$IYmjQoDpj&^ zomrwUJ9*n}(?$IPlxrLuT|d1cBL`Uw^DR{9ZhK}(6k?<{vp&NDpH)J2!`#kAcmiSW z@2GKTD_n#}2Q?;L5aUk476gtuFqSijH+Uk*C6Yk`0W0ITDi&pa2xC-p)~tQKxZ;C# zIDgU7g7X)scH>juA-HS&sypjg&g2(k8)_SBNg8MVFwVxHPX@|{>38VQT**dio2zz& zf;Tq}>`wqDm#s&vczlZiw&P=ol@lBYr%Ldvyc(v!!{tYyuxnrRJzd)xymcMCqH=cn zxu1S~e)_DIK7x7=?cRIfz~0@5=x11`Kl@1}`fG11c|e7Jnf9e$!LM(k@uyz-+XwG| z{PNS779b9aV|amF4pUn$6svQDYnW-FS{ldY3+_Ykq|JTzNm3i{X>URw|=_6({p_`rwQ@BIot zjRL5d8TOrC>oYp@93bfY{1+F&O8S2OxkJ16eS z-Q7nKJe3j+6-s>g|LK=kb|S$8ZKGWbUAyypo;X$m89p=i#gZ0LbUpeBYX+mnj7YX` z{zLlY8Jy~)1ZN=ecZQ_?Anhj-{qA#&fB|@qmHa)>f^&dyq!G}#@HfcIgaIy7$*paC z5WudrLyQbokN<Hh?#(}A+!w6!qgYG#0 z>^*sN7X0E|Lux90>LTnA_@H+@4)3Y8)>}-QdhsiUL>KLL#W#|$bY%!o#e;=nMT;#x zu?R{oNo0j^1gJhCiEe1r#nZ(lM`xfcQXrQx-n&dr{nTVa@8j4;bZrg&`MIcQBrp!> z4YSiTvwA&Q1BoGF8yWj7jA&TKj^z0w9d%O#7|8eE59YBITNTYMEzQpQKvqd>$Gv;^ z-4hL_ao$Fn-sHfBG1jSkY9oy4i8CE2Ockb4!ydsRNT3Fvxr41!xKie@vZd}XVoPee z9AlLwr_QoiU$y3N@(>l^TUQ6Bxz!4jr%5ZCI~d*X-R_ zRqgLb%=FBxjdgf^7#6U(!6);_whSU7#x0aZEGGHt4kU!jkQi!t;BP3h2-k&EoETN) zP;W+#p*Lmn%LN=Nc9xY+sEya zi!(~_-IQPxQ>lO-Y-714FbJCK5{3z~gj1=IT2y9fE?+)O?-&^w8DA->lyD{`sn)V0 z96-YkowK9c&mZ_=GrA3}0s9vFU6_c8AS8Q_Juf$Vz`q=<|!^Iw=U8Z~O~71%EnzzetFrr^EAF#;Gjn#BLWY+mkqGEZ>fs1O(+?x>Rn)uG2n zeib4>Rp=om`o#Xm^o#hw?wND+_iwBlg$NWD?%>y;bHuNK2tN2Vm`i|miG$$cMe;kW zQjDG+#;%Q8)B8bO9CmKN*BM4%pctb?uXp)3$tl>uF|7bnub9gTV$G1q7 zKm(Y;|6{ySU@$1dLb(Dl3L2&_0OChJ9v;4d7nUJOF^1us(91^1Pkm)XU#mkxARZCS z>?zs^c!E3(aV&x`m{<$lhDJV)jxh{cGFWY>+(eHR@P#@J4oNX^z<0nmWuRbmpKDfB zI*jLnJvKn!Jb?KK^kwoZ04i(+b{XM+ka?QC;bG_xf9MOttP=p9;XQ!T_%h&R(UE-DTtf(Q&&@?HplLOw2j0>Iq*B;2J}AmwW^ zTLu^&jEYfZXyfCEZwuif{F^&ol&-XbdI=J!H1&F z1d!qrgn~>PUtly$yw9vxj?R&o7A8C<*T?Xb7*si!eb?g!zvYy(H6!$eA;#$r{y&^@ z^aVH!QtWSAv;JRRIU}oBQGmWbu%7~3yMm-Pco1F;7Z(YqdNTSm+pu{Wauk;`$Vv-{vu`o}_u7NJ`01HS5)|ePuWK!MZxAH5cI^9QCYC*V(E2^Qb8+%S2~Y z)qt}Mf9OwZsxbY)T;PLZosRQ{c;6VY0~5gvB*_n=2n<1($x|EzG!+PGp&+1ki1wkU zhUxo826F3aa|cII+33ad34>eix@gT&@ka+~7STS6)Hx z3h03ZV(hNGY*t-Oak;6jqPlq7q-MgB&8jNMcgDt6Ha4e94zAC#c!uHM2wAj-4Y8AMnM6E*HyDVR^*dy@^hL8r4p>HW1`fC=p=6=)n1zAR@71p z8bo7dR<+lPkZEmXlu&A+_b{@T#1~<1OkhCTpe)?+#bxpntY#tsmWCVG4iYH})`2!& zv^?!@U~CVVjbx^<8;0kH|Dmi9uK#CjX>gt(COOPRB|&w= zW#dTJ)kVLBRk$RD{SBzsmvMv=RCp@Uw(rva^chM{(Wo#z;@r*=>TeLc!L-N}@c`w) ze~hm}Vm4Su;+ufN!(7F2Ah4-BZ)r6cqN7z(qiO4EnJPNkU}$>_ML3HLhD-VkvB>!m z`vqZsUaeK5Pe}?56~o~tt)(_EUubHI9IxuNS}fMyYC}e36TrX*9r6vt%0RzCv>c02 z#y(=iDft~0oU`?z_`Vzdbwe+*_fl8OhENme{Id^k{CI-nESEp6m>4V?<|^IPe0N!Ozyn+Fe$M0HFfY0EQi= zSQZau5+y#s(=bm6I`l&!{p!HwC%UdV{|kX)EZ1+QdrT%O9M+6$9M}#$# z;vqGz6bW)lJB{R;krox7Vq_eE?@y0 z70odvr=&;+tmf;(P89GLEd+HF2mwroYD!7Xz#JS>y$F~R;`8<3G~T1svI2?{#?r=w&UN^?tVfYSX=U?ogt%Gw8dq7~q zL-kF1ee3WTBiHv+FLDE+<|7D4v=ER0wk|w5Al$>SpUc9oDKjOAU&_i(l{YskBpftJ z@8G1$8=FA59|GB4d*}doakcE>2OGM(8y+-0fPYX2rq|N_(`(mGqZ`*w!|>Ps#Qu8W zGj2EF4c@v@!EVy0UYnrl`G0XXeM)b?0n_KTXy}vb>H|+2Bh`E}`osLcXnKOP=>{}K z`Yas>q6A1tpQ2TiKcXf$Z{0xSvt!H0@f5<|%s0O_fhfwK^VX+mikV9ZbG-(0t$;5; zSO-^Zs6uGdonMZ>@+$s|+YN77u<2`PioS-&T-qP1I#~D{A@k3@`pWp1cjCXG|263E z`VN2rb)etV!JPLve+m{sZJC-oNl6B4iNri0nL;0%WdTY9A3K-yPAB(Kx6IZ*63 z$G(atpk6nLv>Tx|^HEQ@Mhlp!0GA_@PFchA20*$@I4p& z&>!Z7ez0CQ{M|HQmKzG~(2umc@JVmt;PFKsYa{%a(HVFE{-yoj<~-*bKa}(rQ&%u% zmcSG0g69f*89cCFl~;YB$nOR@?wt(V?p|-TX9Mix4GSk15c~_omRS7s>@Pgp zhv{<_#QUBl^aJ)3%zKej>!Kfs6LWq5c;K73o!#ih4|fMMK4%>nA2ctF1HIT^lkuI* z_}A&5yMv)0&N_kzSAU?NuU+(WlC;D3@Oq&Reurlxtap0Z_&#L(W`Kto|Dnb4C-C^t z4&wrxZ*zZU-~s%=_>2AD<^Vksd}c4}{~m@4!%t@~>;E1bpn`S>?WC@d@gazW_l0#M zZeREj+IMq4f$<^UPudy!X#xfGD$twQ?*RQSSr>c{#(_*awcay~wC|!X13mnkwBz+g zdXB+35U(TrTZm?$KUntctDYC3y_jidS<&Or4)TN>N#G&;K_m|0zzqi{D>@nYlQd!;DWsycNp<7DO7My@a&)fqaYiR0fJT zPh&VeRzig=rmJXridyNZg7#-z?G9un@V|w}hjqcYfVU6XKaz1`>9?p=1b!G7x3gUQ z2>ivsa|QPVyfN^sphKx`1fDCnC$uy0bVKO#WzPY?^9(nF&?n?xU|yh4&t`5nz<-{@ z$lFrV&h#hp{5SgZ0Z!-#@Z5wLIJ@a7&zBkea3jdPuJNJ0neh7?%f^Ru6x=f|e(53b zq`Tn(dgB8;H+dW-VgE!C@{hoe+=bIPq zq#uBB%1JxZzZ2ST`i=ex(7p~dQE;Fb?)~ZZSop>FFyGwZvFlg!Fmn1HBBz(mgZm%) zKl&e9SlWLj$i+y8E?jyW90-Tv-ilnuKx5C>yOMnyXYFe=kD*R3>I-d z#p8gSe2?&10q)QF0r>1QXylLe=pGEXPj@bJYYXb;CJ;0cjr@o_u!T!g0;=%<~~&zqcWq+L(?>i`e$!I@?;c#*&<(7%?nx6)4BALfOA0Otqz zTQI(kcH;if4*hUD{uZ>if}CK+zw(RmLtW!vSw23T z$AD!8{qJK3k@iN|Un^NJ^uz0A1Jwci<-m{l+@#2Z>Ho%J|F@R)|8TMYTg&=?=;}}O z=MwwzL&hFlxJKjywnG{IeTZcsa+uJGCwCFgk1%*<)=S!%^*-WSFZf&BLa<-l{^f@M zkFxgwjIwC{$DiHjdG1n4E``t~olZyyM+*s%AVPouAynx#kuEAyq=*QJ2q*#~gouDN zv4AL;&{PDwARySVV2QjIUSCM=Howp8^E`K#g5Uoi&E0eJ?9R;2?#|B6?#|Na1O8#B zke!b9_BvVUuA@LgXRnsJ_Z?iB_TIsR zZ@$+3v2J77z%Ub-*ds%dxsVed_b+;uvL7!22qn9XHT;U6M3ChxsN_K$>qX^iaIa; z@ccas+~4rP2F-d5S=49d-U-R+$t?+xBsoeA)a+2?TH{LC2F`w#RDe^RO}9!oQ{B zQ-dGW2gYy68=TAb26@6xc*K$u$yK&TDhJgE{426Ontm3{&yPs?wvh0{>INxa=tMr+ z_6g|yZNiOl3HCcbV%Qfdyp`Y^`M@{qaXwoAo+zlWOaaf9aM+=?hK9XNyBGmaHslMw z5Q9T5>@Oc=Uajg2zm-UTw1ULP9M*5DB<7T>-2{mZqhd#L`>rbXf z{T-I_@}_^TTKasS^v_is@=1S?>Zw}#ol>6|__tTWKL_czR7;;P>(5BP#Y#`|k@W|@ zNg7g0s6VURBqv#al#l9Ljw0Zcn>Y@FGy zz3CU$kbXSUcdwq_FAJ0p>xtFye?o9=0m+>xc=#dvZ9xtAo$%Xju8be$&#_@1WR%>? zKiYM3Z~O4z_uBuTkbRE!1iKz|E&JcH=ReUNlW-b)$IAANbh14ym+c9ARJALzJ;BbF z?Fp?Dc!pu`>QAOTkrH$7_iYr#IzUJ-s1MaAHvK z@MFlcdU`3(%J|`r`N;-95acQ6M;8B(=VGlZ!AUX}{tClhB|jYOQRt0@+II58nf7Ws zz{!5IDc!ZtNDdXh0M9UUeqoHT$Y-TD<`+6zCi)@rQF^k6{jy~{0cOB`(nG&c`vIhK zdV4;#A8jLBTqV5|=?{9-W6cY-W1pyBzd1X(t z`T05`zmz)=220N`cVHn4`C22TE62TX`%RM{yFH7%Z)jhVPapUIWQDK@a~v& z)CA9|Q!_jhQ*bN&H6ObD?=#?5xf0b7#RZP+_I3hemdr!%qL7E){UF~>^SmS3{%E}8 zugZ6oWqaFv_pU6ZAzR?H`2jfys zmE>!G#CWItBi~h$uYXtL9X}%9Rg$m&Q1jg-76+Zcq!8V?gP&;`&$O!Rc0^9Jr!tf@jXF2~Kia?@ce;ufb1Nf1mU|@Bvu{9PQWktt?j~_L!8Dw~b!b*znSZv)jkgj1Kq=_(b^3;^l9{ZlH9nfWc-TJ4^9_Q@RBC z+Zg{S9gSy%Z-)<@(xKM?ztpery<7TztZ8Ez;{c^=BGVaqhSC}OXRO)o4LGGk0|q{$ zKT$fPy^p2Ipa)LrjPb;1XOzyU@3E|nOlQC;-Q5OVG1#zed08?ncN_Xdjqv4nfRouK zZtdhon>hx9w|0N>?>6aOAO#yBPUtOS2ariUC1JI$WT zACYp{@vV_OZdwYUP}x%>@vW6OZY(#T-s!SqbGsxD-u2o^+7q4Sa6`@}Ki}Q~{+)zVJ0v|#?QnoB*EJ8E^fbXy<0$X<5)SzrdKz*# zCgDFwIOK2WXu!`%_>UGILr0??Nt4)YKY8F%KLfr{!mmp>;W_MGT)$V^ulRRfB}bZ!04wFrJjLaGxfu-h91CPHRv&|y9K9Q1|FFn zzB3~|X?ZVx8+#YlZB?3uWMl zS?}rG1;Y;7hBClGq8Stj`)KW%iS_(n(0Q+Z0d1la+M$U%V_Sq4t<-pON7W(WtFx2-zEITo7hQQJ-?OngIyu@5Gi;IeH?IQE-m6K%u| zpgGqqE4_Vm1$s1O=`i6oH9-EZC-IUDWq!NUocl>Q)tw1%Z^AJJ;pedbGC@UafnRWm z`n2?$P67Nl?Rzueo_%kSqn0V<7|t;Ml6B)NbFG$?qfPSixJO=NNlr*Rtpekre76km zXkM3KpBiVpG3x1XHF-^w=?&Y4hM5*4O$*@m=bVDPO z0Cd9`dhZg_=s1NOoz_8C&XjkqDRO7gX5Nckca(gvfA`%{}!>sOmo??GR6 zqMV7aJ{9&fJiF+#$n!qn17!J`{Jlpj<+DK#o?rRf7<*)2HpiHMym0CFHOE4-9f8kC zFZ;5Q-n1R1Eva7ueJATuu^lmeebN)$8dL2PB_2P_Dbr=S>#~@t?F#eSG+A!69#cjW zWZD=;(yr15ZG&Z6f0=dxje)c-WsQL}XMqk~q1{jiP&(xSR8%#cX&#KW<4A&&4%u*f zix_7@Oq+myy}15DKwS*0brXBEPl(k9X5&Jil|`W~VyS~G!`MYW3|5V*gdXCGhQl+V z=Ratp;3ElVf})3UMJRms7Rj5LHq2&+btLnCrM=C^B5$qNW!}~w`Q+5bl|RqNE;G`p zJMWlI&33)+NoTK|&X*H=_AN7=!oKoTD&aHJAwF)jpK=TDmXXZ-5C#3Fts(aEjAFg0 zx6nP&s1HRn)Rc;QF)pBvv3C!jXpcwln4*Ut!270qvozn`Lp>HeYTU8g&s zXg5FT&>;z-0dQ0W*N*b|K3ON(g2(m3j6I9f5BxctWU^Mh!mh*b@251(2v!w8Z7gu; zVTuYQr%S^;SP+COjiuCL_O*R`oMa1EXwB32ElJz|*Km@T_=R0iz6Xv5#D||&4aIq4 z1ot5rD29z1{Epw+b?;>zT^7Ihzr$qRc==Emq5>x3Y;J9Kz3+;J>|_tEdCu^^_NOh` zhZ!BpjF{C_W{1)`qfU@N${nmZ{HBCCg3Qtfh1Cip36KyRMhQB9kR0VB3EdKjdN4cd z6rcaNIOQvvm;V@^;}H>Pf;V-Rl#xS8$*3o3I{bpQv63cQQ<#4ci40UvyC5|Xtwh%& zyLd|irVv?1Hr2>-k1%u6_byHcd58pbAy;*@l&eEY&Zrxt`YR}Du=anEJ#Q4(BX18` zQ}C?Qs3E8AYu8&9kmIR{%4PM|gl<)dI?JdMalG&H1?*&&r)tu3hX3($`r^IRraDw~ zQs0BT0+n_d^#jx$f!tr);ioHOZ9%Gy_0+jt>FFQn7ZiHF^XVUwuXg?+`7|2q>6_>A zM8A@7_9i|uK0G#DPu1DZ*JIgJVn*!iv0?^$O1~g_G}zg&e6=>a;m!smCojE7Wkg1s z2z5s!562nhTK+qN148_EgxUkcKs&IupBf120d`&Gjv%FHc&*5=P&K%A==rG5r;~q3 zPQDri+R3MrB`>FwL%-?poiJWG_E zGsIf|R9&V;Lr$TG{M8_v0*RQKfyo2(#`#K?vVYaRD92_C^u-$sGS$rr+wNx`}TEqNSe{NFP^ABVf}WQ zOc#mRFy53tkLo-#mphn8?vj0{pkg8W+sb!;0Ql-ag^P_Mu~p*KrUpDzu4_r`@AU)%`2A$xojDa#*{QMS{` zrOBbjiw;S>2M)1^>c7+9&=?u7Qy!CT+b+rz=K`nv?(iI&C?mwt$@;eK!c%Y_&F8n@ z&8=@8eZKImquU0LA3r#6?6_M?hK!RB>MKLWBKG0fF@w07Bxa*)*v9%>j~(5%?ajB; z(5pv}Ub%Ag*p*?spIN_a*ZQYl;$Q82X5Frx>!04`j@~I@>vxZ^Qt@wZ|7{Dta`f#l zuN-^pDrD;yaDu)1Wb%6Ji2 zJ%tqx7T=sB#FcPvH*sP&fdMyh8t_E>c8S|&$9v!mgsri+;*>nA8Jma`lzi{gM~^+j z28f2YZ_7*dn6gz_&1Y+|icN{l2vuQg={Akqao-__3J<+`x|CGq`O~LQpFbV?ZW4?1 zXORpZCvlNA70tES*o%`^yP^N|=iHAr=`>->ihks-F@lDeielgIR`~9L`LvsQ8|L2)8G3N zqcLicEF%7C;DMF2AFb%5%|)MqKYOqLL!IkG;7J&)hDB&wcsj2l#@0 z`xk0q?pN5p1@re-PoL<(CD)`?a7W;Bq+hsy-vZ7~GTx7dShLR5Z`6V?Zyj;vWAO{0 ztj>kr!+c&(BD*MwY*mgpKcu6R`Q)5+YyI9C(x%UhK7D4qmox0Yd*hcKRS)17jkpHh zo9Z|A>C^_m_ai3$-SXYqif-yq*{&TGm$jZq<6uupX@)8AisK*d)?r`Sw_uH1558?LXWAC10TxAK~r`1Fw6Zkp!b z;hsJl2lvO5Uq{S1P*>1VdYq3gO0;iP9|ir&I*HE6uT~YbRevjLL0bg*ML3$qMrbeu z3&9kRC7sXDKcCMQhy$ZWjvU1X;FF&|R#JTISaHeUvu1tt;Vk_7@S|B&E^j;~KlEG$ zU6g_+#l{#>1t=`fg64>v)TrSU8BFP#{-g}z?4BlL8&Q6W#q*E;4W1lIPDUa>;U54U z5)k0uhXn*FCu?B{KB*oLm>kCav|u$PaD<;`!%;F=p{HH=Gi~;puFmISojA;qTeC)P z2QIk4c6Nj{G$T{6Q{n@jEBdYzv)EIsckEb;J8Pa5Q^cpaD{^_5yL7+`)Kw;5&3^(< zFoRemVl{dLuVPHMf@G8Wktg$G9ejg`k4tLPY4_MMgM8Z;>CmwW%T`#N?E3yWvygD80u(7T0t38wv|6v@TFjjD+v zMduS5U2goB0o*9E8X~*2Kb_jPZR*%@liT)A$htdy^YCFOi(a{Rvy zShsHcbjObOP3_omM*j59EI&RfD!ccRM>bB$KlRlge*U2=+jkx?km`~t{ZJQWC^tMQ z{us1mj00XK@+Yw+B4z;JtM7FE!>>;|Mfy4wJXe6{K%5@Ix`YC0fFBueBiwj4gKrpD zTiR`ABa>G?Sw-UaIh|*m_;OI@Ssqn?3&ijvdEmkA+-Q(B3(C zY(#RkA`ruI;UiE_948L$t%wwo$uy}FrY)h7_+TYb^2k+mp<5bjd`}XsjMz3o zY-3~e0`cx}@h}IlHbR6BA2Vpsn9Wu8Yic8pPJHkf_P_KqlUe6nl>9}MJd1Qt_~f% zp_`1DN>b0Yt@sn8Yc68UzH7u=h3;DqfoxE_Lmy*2Hsi#i^oYfPbPB8t6E?o$PyV5V z&GEp{Zuviy*M7zW%S5`LBD5>z%nZS z;9v3ah)vt766~miwXFm@BVlPC7;0UC5nWoLGGSORQlki_ zqal0Kv6V6%>?#x18b0$Kl5QstOyVP${c{g29x--r%XD2mFei8!NHD(51B_DyBXugH`{X0%N@;6%G+aV0#(hoSI`GNK;BrPDJ@jg4YO>+{9> zvwz^@iRO~{&+}~eQ=i9*5|7WZ{Bn*$!CZ2Z|1Wl1tJ_D1!5t7xli#l(JP;? zym^!QQ^hUtF%x>ItuiB{Q%l?I=6=y(vqRfVP}F*l)E(bwLhUK)=-ANImd)eTCb2DI z!u(Q0{exRYw+Ocd$FyitziwNliOpdTPKB+VTzZ}?_tU4tI&?7pcXS}fl|xQs;S#x7 z>Y^!b=#AvBI^Tf*gJNANYOFJbVhQ2;BRqxq-r8)BwnxLfhBXw4CyGyni>+rizrO!% z@ki_8mdhF9v+&nju5HaiipRY-KR;sA!;ilh$!@h4ZRizyu(dcIwPX4H&(>qBKD>g^ zIewEv>a?j9lN2?X`LR}Avc#U(Pu}=(a;>Nohks_>$v^#P7aD)zn|Kko zp*my506w)Z>;BJw#hXj095Z22-itOCCnG+iZy5WqULh-%2(SJqmvPRqC1My5Ff1M@pin3vJ@?w(s%m*!EmKP&F)%#REoo6(G> zIvDTviO)6T9Aq=2B)I*^f%fc>@#j(~_8eka0+2{-I~f(vfgp5f$M@g6Wc=pGPaL1O zdwln9 z9TDP%hay6wA6~RKX12a3-<@~gvEjVE>cBQxKuW*q=bF0 zE-U5%C8EqVwfHmjn?bJDB)IZ#)ZKIEmhI4-u5OUpfb!{6w1-J%nvxA)N^{`$$DB2i zV$+~nF*Bt5k~r?oi?K!2R?(JeJs^^%6?6?EX2q#Av!hyM_U2Dbn!IMsTPVui5Mz>6cG<^_vB&IRsw0KbumqJ!^G#|+18%bf zaru_G@#mj2mOQt4>5RE+*6?~0Cg$HgY2w`^l;7Vw^B)m1mw&&1?NfWvh`xB>$eRz< z?l|zt)dOgsGGvn{*4PR$=Fo}qWTgkViIbgAc0rx4%Z`Mo)Z{P+T{*xVp<%4HBh=nS zOKsaqXQa|=!5C?zt8>s5hvVYShuB=UPrM;MN!>U%rD3tiE_v&%lH#LBdFs4}TMr!A zFi^$>x+5wJB>+iT{lG2Lh1t0(8h300n2C4i}B(aaZ=phxk)BO`=D4nA{Lj_=`!x2J>z0CMh_a7JD|L%ge_!?j^8z4;P^qKy2i$J9W`j| z!2VQ5o%*QlRoO>ll|;nSfyIft<_!H^`8LJIlj}<;oQ#J z5v^3FM~07R)44@bM}9eY#9%RcE<5!^ANN;tsjfafzU5H#Mpkcw(c#(T^o9-mY_Yw> zBig5Rn~j{&)Y(72Zgc0v?qgz`Q@ObEm%84z1?8%P(dprgOcrbl397Hf-W3rVpxgWs z>+g$iSXD}oIpoTh+WMI%+;g9b3z72#^u&)>}=FD z^!yK}v3&GHGW~`Afd6iU{&1ewUW^r`)8-Oh=toh>0Cs>io5bLVmnJzWIu!@d@JpKO zP0@Bz$v&qSv5~Rb$ODJ2AW-f1d7n0aFZa*9Jof_LGILX&_%3ftS1daI#cFjE-MV#S zd&K+9De~}C+-=Wf2`Zj!lsOv}x*BCR&(M+6157b$7)Bdhm*8m2 zad`r1g4$u|fOtVJIQma+tH=}w=kT1@?;WvX1v|E4#gW%|-#OydPj7bUIBi^oBEEj) zk#W;IcAWRH-ev`BdAP@x=YAHy?0M;>JuK=M)~?54@%f6uqVS7nmoA+&9JgwXnDpq< z4Hpi+0v<62Q(Q2NLEy^TbBBFSHf6cwIS;aE0G=bkvrz#gYluV29Q^7Hii^uT&f_Nq zpEr+*|L7O;%asC_{VBV7l>Kp(FCl|XmA!t0yxT*bEHt&RfyH<10@@zWKC9S|M8W!$cJsy8bJ$B|Z?y<)6xC(% z2fZhYGY$I1J=WR%Osl3TPJ6A|wYU&NSZsqVw)=x4;*Yx@D8j+%Hu!X+imn~@vFu*%PZg9s7_YhrNcU@>SR}&Rx_wijfPCuTkXqp#!^;3e3G%e z=r`BrB_(>w3in3w&)T(fW!Dvj7t34H2yNH{$;cm$nNodEy_iyz{U8;?8tj2shdsHZ z#LY1L(`Sjkd5!DX8vTMC`nAu?TT09r&*b1=-VnG`#e@11#A+iy64iMuWCX{Q4yP$K zPU$;!uv?$=?i1Vv>c_5`;%)wyyO!Q(o_iMi%{^XCpQmTLlNXl%opVOrr6LCGDfc?p zsYgTsngRO23$VkEVl04df%B+nN$uijG6z#ECN41*b4~MyjTNw7?i+Jhy+SsUtrBxY z(K>N^`V7}9Ttys?S;mcm-I@^7ck;yp2Z>2yy;vtE_3UwR)M%E>DDHDIY)a6B-%@=E zveMy(k}fv&rL*p)xSG289{C>LGIh7{9{ivOs=HmI_(lCfS@S)*)PabNi$ToTTLT>A z1%;8NUB4{GiV2r_=s7;oy;-fr-*b1s8Wub?wgbTDFUQ(MIzx+FecGiW&Iw|vhQpvS zTNPh)mFJZj^6WxRll-`^akR&Rjxx(B{xf=xM1rJE{NzM0>pIj?aehewWj&^ z-18b6J1}f6Yr*b*YCaoZwu99A@fF_=orD4H5&VE=FFGWi5R2bjfr#*Z7#awT!L=cG zvbSo%!cUMr1+!#y6!Olt!(1;;X&gn|F<5v`(=iKW=g$(_I>VeuDE$HIsAs3mYgs>_ z!553;BEnte*a%e8Vcf)3ZZddu!Kj5Z~dmc8EixHF%s4 zS-!Z@pn)Skc<}t2HxBJtJ8^rDf$&^3*;4RIzY(IS}EnAr&V0*<`-P0 zBz;WeQ-*H}-Ua9|aF`g&Kjb|I5B_Q2#6xa)GgnNxu@cLutu{TElJw&{-CvtMv(}<% zlNM#(=XPcF3wT{5?sJSAxbHWu)csuFLlbv?clQfxhvbP{*OpzMkT$p5wxcmIj2Fz9 zIV^4NOx`%>%_#*1ORkN613ID&big%?A<%cUB;-4~8RJ}=eD<>_#f4K5ySQ!FE*8C$ zb$yJDoyDHb&v)(5r@7mD*#FCD?q=UhRf9FvdHA&3&uOdNm_kFclsWQ^MbAd`K!@Tl)cpQ_`JVWM-w}Mif5-eazc=zDj^!Ql&yo03 z@ARJh>82c(O8RGOq>uh?ukpPdzIQYKN}2zI8uJr>HNQ9bBm6bKH}UtoKt2tBu~rF>vGhG{cW=*ay*( z>htVPc4Rj@yw?5OKKG5{V)vWH#p-8>1o4>ma6v)Y20SSSW01QMcTbj&CUuN{+n+P= zTcslcL4y9AL7p$f znIb-h%W~qk?Ryj7QkmZa_n+khCqBY_-lL^)#DNB$ei9Fzv2BQWvNoFOLEiM6;6TPYrJcNO>R`QnKCX@UE)%N3T*?_)hJ zx)e7a?N#n6BUL4598FnjCoaj!du!wrye`a0=?7!2i}?V9uz19Mpx8_Ct+yUpz51cI z#&yWf?f{J@9uV6_p}0Tapg1OC@umu~c}awN(=|JC$ebU4x_>Y_ja7CH95Wp=-D+HLJYnQ{f{JySi%#5jQ9lPgY7O`z+ z{5S7Y`F34OI&yAAt^3&cF*8`h*VzcR7wZALkG(4POUL5=ZCl~Bp#ISoc1DeM8tr2M zKiSGnTe{XB_H%YdfIrv$RJ*3=0a)(6icR1q)i$`nQ3+|j|{dxZ}J1mz5RN#ezt!0ez+Ak zBY3#qa5l;|%05aR#eYUki&5!w2F89AsXZMCjtDuc-*U@u)Y4Yo=zZe%F#F zIuY?HcYv26h}`0cWbT*AGHg=jD0!}5F3Uj_I>?+#&K&We%u;oSy4OQaKkF604kv|G z=TvrBjJWs9VArfIeSsyY^0&OT=E3F>DB&^7@i*aVNmz)qDSRIeJb6x92(=y-5y=m0 zsr1*kX{R6$^a-XWs!lyh*SF1f&6xDWZU2VGExM!tOHyGr?>)Wf5~?eU56EMm6$q$jDCKg%_f zJ?Q5?B#ANBO3Bv?%{);IyO0yZo#A>b-&ERFUv6c;`W^i$9ji3(XGW`u;Z}c$4v#{g za39>hZjrm(k_hi#xPCG9j@?;sI$2$W4apL}%eMME+N#8b6<_4{G&10YyB4UpZi&?W zGNH;Ul9{~~M~K0SGK=-FO;ih|E;S`BSLVB5Z|hymPA(5zybE9E_a51g95QeIkS=?U z?dh^;_M*YJ=y6b98h_hUA0vkh>C&qg-Y%wx_=Ggj=?gmD3_9?98uw%2R-{BH5h9A| zX07R;-kX*;2*`)r8VvlB&XD=@hK#h(^S4K)CB*mYGGxeNyat^iLq?i)CFN}BFGJ2~ z#j$2fPLaB&{4a=j-v~?2BUrAbh5CENj7OtAoB9a2;KM^pPBmL|B-DwzmvmR6CDxcK#J&b0YwKZFO5g@*p?Nf4z zYnrJ)(f9oswlOqvg!4 zTW7lLURh=g%P87rPQXgFY&5$=Ybh8XR@GP|)R&i*uV^IxqJxND0)r-Fo}VbJteaXH&%Fd*9rm4(L3 z*7((!wthL=bz)YRhKm^6z@Rp>WBC}!g z_t>o>vBxaER#ZMxQ_K=l8)EFg$al%iXO$1x)!(f;6nly^>L!L|s}k5( zM9-{_X!LLXo+W6OSA3G)ZjBv z`BeW*`&QC3wE!*gXp23pxm&0Yuwxc^zQo|OhcZT6&_(?nPXwKsDNM2LSWy$sJ{)H3$4g&;l)F3 zR<`m)g|?xRrT2bO5#}&+DmK{C^$nqke!FqDy(rY~w+rmbI#Y_3wKjk43%S3i7K>=a zle=*E_f6BL5kqkQm{uH{PuV3Wm79dp)#;u0l*nmW9~1i+uPId@ z$Y`&!R{zJOhpXc~=uH#U%Yt@wmJf0dIxiFI-?Myid(2NCEq8^$Qu5g2HC zu-^kuXeZ0;ygx3deVLjPrK5QqzmBtaa#tX_SP%Vnd6yG`{7MiE7fOTU;56U5O=)oS z4x8>A*oZ_Z^xGml@I-kRyhAz~Q`NmjI$9@&C8Ta6B>L?#y9O(s=*_GXv@8CsK&fwi5 zN`q$_bN8aHEXMj0`L^Zs8>@j)bbW^WOLRuZW20LXyJgJBhw_BHjosKs?*C=-KJJI` zES_e>p~gQf^8UtLQi9>qW z3F?XJ8zA8~)G(2UiY`Ru)=-lHcCwRboRY$}{E^ANO-a!fgPzpplBU5!(a4d6UnukpvJg!ftG-CsVE$um<@_Ik=?P-7#B0*YhW zE$u0Gp(Q~`FA5^`NKGp&w}aQ)<)ck0fkQMSuPuta3+U=B-a=TVY{mHa3D6c6`lq?a&(~eb3ueCSWmV~{wDCL#Y@F;YTC?`HNmp389 z*i2rSp?#X3cWVpVUZw zbhhg#pF$wj-qv+=w)$wJNv`>`D5M-yHdS1Z{sY1zy$6Y~ZCW#Ys4Z*Aq8$MeI0RmC z9i62Z7wQ=A2GwxVby{v+AUQJz8{mpFXdBATLab4)9ruboRPyQTZ3m8WWtYFpeD0qzPD ziMJn-ZR(klJL{`-AR#eOElIrmu{g!j=!g>VwzRJ=a`^uUZ(?9oOJ< zqJN%J6cyoIyd?uKK4T9gYyrz7f~OQnp-NtU=9(+-n%WB#43CIw#5M3|)t46^u7$_? zF#TC}J$|Wa)pPSKNyAe`27jh3z4(kho3vHXvcCa=H0hsDUX@pufT?>e{{ziUps;a^ z0gNqMdihcaYa5VY=B+J|jdP?piuwIfHj;eUvTSBv>H9@asC)ANYUD~C1KDnaX|+^N zwJ?W=Q$kMdjq%#>UD>eEqG0!%Jqq4E+VV*HGE%=%{#$9O7CFBBw=`1RX=bbFMMU^b$waC)a^55{4Mm3a%_G8VX$(92j zIMeA&L=#7wl;cq9{1kquN*;Y; zmBOJNd&)xfS_?aY6*)t|QaN_+R4tb{OoHHH@l_&|sqD97PA00KGJ9 z@TOo56PQprRna>nS&%rwI#tI0&XKG$+T6Fw+xQ}P_0v9n3k0bQRvQYzt_XN*uxgH5 z=FJuqg+@fOaiTlOTF}O!{cSSEvuV?~%wK=a+#axp^`eSUyQFP4goLk7rKK;F_f)%- zmiovk9JVQTOj>(6YM=vG991@n(5#AT)C!3fq=-u4nN=x1tOlWiZpsRxw*3yqvPnjr zqesxbE3;ucWkXK|D3wk3iruVUw)%`(bjEDrMl*0Hzy?Qu!R*r(qc6pL7T!1?6t9|?~p|2~!sqT^Grc1*DWqGF|w-`Q>sdpJBf=ZBo+ zaJI-A6vQGEBp&0T*KGbxI<{@)f7N>6*VB`8amcX*zOm|Owk%H{GW z%?GuG`G7INT0juc{5ioGh z;Ymdtu0msAb)PNbS%$G-$%$}M`3-)^ruX#=R^iIWFW9O`w6eHkpMg7%c!O;@azx&LI4(DxVrlWBI?^qn8JPqQkD^AQnUf6~W#uWjRK7(<0eE zcK%oIq7t=W?|5wHbz)ejc^%Y(cB51rG`eW{Z|Wk^;@AG3@={)!pBujF5G71nf>u_O ze7wV{JM7v@vzuGp{&Z=1X{m3?EUR`CNc;*GC12z}I;UR%hXE*4bhye=u&kNF)MjGQ z@ow(#_8w*b9Np{wPHp4$E6qEWo-Dj_b(9QSfZ+(W&Yi zu6Ot*_ayqGJ^V#*WwZZD1*9tTE5g+`Y$u_8jB^tXa*e=X4u?ffUNC1=2-=_{kl_@- z8}3&#wzSQf`Plzh>|R?ngV+! zPu7P`>66ivk5@I7jaQh4uTai(8+=GMo8AXyc~Bdx>KdnzT&f*1rX%~Ue<-zr)=;=P zKl6{m&c*v026fsiVdz5$H$2;xNx|WyhAfBqxMq9vn4Hm1VxQ?x(2~4YXxuSm_5gj>hW8eMP*Ri=GdsWxZXNZq# zwMZ4~;v^r1g$=}o_RN|4#M6=s7SjG=p_pDImV^&y?Q*k+u`O&ZaaOs!u7J0K6A0#_@q#2`-4_JEx8bXqQGoz5nC8OLYzZ^akO!|EU@e+IM0JKXD86+(=uX0gRq(BwOY2< z!vgqg%BvCfJpaKu%Tt5%0te%stvp8r&gLbbcHvB3viJd~@mz@R=;~Y=l}tf-O&d*~ zGd2up{V$v@+IQUD?znp&P8Xf%b-P!uUbe%=C&)wQUixT_^28n?qKS}1XiF@sRZ_xp zkMpy~d2UGw&KuYc_W~Z{)4&mmGf7)ehT7PV6dMv48LDwD3eiAcx1$hIi2fN86zK0~ z*Hq4e9q8N?2};JkaFx0~aD+ucYhjO001r?b@CK^ZEE4;mup=lu7S$aa4HX{U1+F0s zlL%E8s;!*X_nyGCOm|7jUll2o~{r54AT*u?x(X4L#PvWko zYHPXz0DXzK(Xa5&(efj(L1`9dO?8wg4dEpz9A2r6X?$5n;3WRJF_uu@yMn$a_lFx6 zDh(u98l^6=hqrNmT|gT;<)wp`53W+U)}jZoVke)ECLVkuj2`GGf%Ja`!~!TPuvtdk0JIF79;g)ax3$0ke>`glZOr@vv6apU zkm4#u6G5z6xkVm9512OM?b+@F-NcCnb#d-gu`L*V`z>{L%bc<9PSV#e@!J>C84P7M zdn(N4chLuqcyKuD(A$Ee>|ZHgX{%L=sfrmEj;2A+$?>oq!rUd&21#SstW8H_BB29u z8b+<#X4#+#iyj!-e`;&r{xH@6BLCwMjHA+HjJ&*AR|78D1j?ygaV3sP(Tcm3{4_urBUtN9WoW4oF2Rmpn zUXcl^Zz^9>q+UIzEp)|IeUD*MzE{r_xsI#3W*X&kMSC{S_B^Bmy8#}dc}2G8Z#`$9 zK<(?5iuP(O-btGxg)UG>3VR~kR5WMy&-hb=)u-HHcy>+a|CuuzGDf?uXwNT8e0BpP zS+4vtR%Z;nKDf0pNEc42fSdjd{05B`$T4B|CHcumYU!?V$M|>d69APTSGOK>H{ux+ zW}}RtqMg_F(Rv%s>MOFdjpT69uKK!7OY2k+~^Rdy`5 zYrFJ>*pQy?XHHBL(PHetuI+JPK5QWLC3v%Kv(>^{;7|NTtP|S&ev&!>aQ{pLuA>?% zzo+&>7eTKL=zjgY6jXAydnFWwkL#cG3jo)=$&FYY^lLGrT2y7{f{s{$xLR;fr9 z3+fcJj1~YqLB5xjC#bBUiT;VKT=X)&Sxe~J->Ge5EcjZ#g4mhVW+QE}F$kRr2S;i< z88$N|S-(kp*+XFVf7tKy*DLpU{>k{lHYJUCm5O+%Ltygp%n z)XouyOFR#1;xk$P=?B)!PvD?&tv>kJirS&w>5zl|FOfW@1)%eBhwcS$0kzkU8nJ)q zgxBsJeIOt(V(pj_2i2{Go7WG^P50OQI^2`Je%OF+nu^e#2AzqZ^9$%SGU%8QLpj*3 z5--%GGWO6&Adap^gxd=F()B|JcGH3kB3)(tP)}KYfz94txi6zba+`Lo<6@gc*N&_g z-l$#c7EP4q(X3v0Xpq*hb-fp~CNH#Zrv<$b-tL8HB#lEjD_cXQPVvVi;iuyMZo$loBMwfqIq{BblX?ZU7a(cgM%UhR32uVr75)nW`+7i zgw+ZT^21!l&tI<-qC`db^d@vG3Tb=X=r!Tdw9CWTLxK(5m^?V6FH?B$1qWDb(eo6a z=PDCo!2xlB9X{n=u5M-N6ECBz;skxk@uV1h^uUvyY#!2Ha`HeDw2DFB{IfzHQRqJ6 znRjTrVSfZD2^kTF?*gOjES%@#N*%iv6(|`_CPzjoIW&^|If9|V5dqzJrTjbXE~9r4 zVvURM#I>Dl#vV4k{N@&^=V)F8{HlSU40asD(6K*0z^2sk)40SBA$ak}W97h+ifLRb z)32Ca@2)Ldwx|<#inV*hQ#%<{rW`ZOIT!kH6@N!97D}EDzR11EV~OBdS|Yw44r%>V z#Z|?Z(1nGzV(L)$BEHCDA>mo#s^mS8=8r5hd?i=lE2)bZCI)Awr-b}5(p3W4vD%3+ zWWXpK{Iv~MlanTF91|P;wDioJgwudI#38Xg@>SOFaggiUZCYeupxr}vQh4{e8zO~qKgVTJ-xsD>q(4>FP6b}ew)Lmg z3x=&5#-=LT^j6i*)m|2L?hT~f5rOyeIaP7N3Gh4X5WqV8o?o-^>!lSa>%&L)!0B9# zns@Z!ldI8MV(PiZHIwowmqANg^lB|Ttj|P z8@Q4d5!}(_pKOQU=#^2mFIgT}(-En&RfDppcWuENvQ=EnGv?!}J4F$gV9|6_(gCavj5Q`H%Tc^nGPTCl(TDufH!3oYWx>vD=-9 zRFjy{815Z^2983Zr2H9#h0t16c?#+p6wD&Uufc&Uv5hM#EMhv;U<7|s{LG?)gSvJN zd_?>regT+i(<7>~$%jy41gK+m!oxS>!mOy^K$+!d@vE7|J)6}Ac3^^Ltt>;jW2HL( zl5?9b*@bROOsLLrU@+en77j+g=D)bd;EATrT?1?6Ec6qJnz7j_6r4)%N94u7kZOA z)ce^ihplW+``2)LfzPk6W2bMkd;4ao%9NIPaU)l7LAK7f-a%_OQqyP9F z_`@A9d)#^SxB6eXxv%i)LU!N3llN&uDt0x{o)=I~_*csRN)-d)=l@31+y4z4HH7B- zq*UYaM^Pl8D`{(%1G?DVO3dI%bj4U9T`*P%n;!#35nCf>vNgO7ebQg0GDy3Z@BtNV zi;4!G@~!G|R|fr2ej_8W_~;^4v0)EG5bZLNykM1$E|tJ1Lw;8X|5~gSjoDQ8ojZ&ym-pyJL@362!(i;?B5if~>>{lBPAYOwtel4YpS0p&^D8Q!ts&rHS#!R*h;Q=X+OID$#yi5~ zz?|SbVVR^aDJpM_V>MONr2rR^z05ZOOOf_%d1KRW59M>PDqlR+eXcTJG7Zf`B=6Lw ziGNCGrWLJ=ZeUY{hc1=1T6!w4S!Pii?o@$&mt-ekPYRQUHa!#&LX1ZL*%AZSV1P(2`%Nqeq%e|f7P#A`*o(-09t%<(TW-P0gW zchgM1UPSWM$FS^0GBWY2=$VK$BxQu{AqKB%WUM{0K6AzzLSF6R_-6NM>x+uIcC{pK zaP9u@n$k=*oDF}1%1{212>V2oU&W4zI`B$a*iA_oWkB@$@K_w5h%{)how6Xjt$Wd6 ze$n+63exxfqBq`P>JagwL9S9^c8|pdYgrbqEK!zdi?l_chaTi_XR}x+t0n#roC#b< z%J~xU2jY{3B8I3C_zfP>=Xf{vgPM(Y81{z$7`ptAZvV4Z{e0|pk57WXXByx)?^ch9I1$GSd7S&Idvp=|ifu6Oda~|@!di%&x5vs` zp-OBK`L}x#U)EhvcrWWDl}|4bH*68NzP267$9v!WvezCgF)oz4-7~*xeo_)Cd^+7A zRU-b%w;iE!v%BDr90{#RGahSWvoxic|$eP(+$YQv)b? z=~fWCg54Mu5m6CQ5X%+Ot6~EcMXz`f@!GK5tCy19oqWG{=A5%rdX|x#vW3^tQk}eW;pGo18>Ml#ddPp^b+k<@G9S?Wz%v~IT;080%B2Xs{2}?H?y~* z!;F-G`z8)kfYh?r48W3`(*r(HZ0q^ z^_mqgiPI+yJblunf$P={#OLXg_=qP?pLFr6Nt0GxJn3|PB>yPQFNkTJ|+AeqAcmz`N z>6M50gje1pad!~S_&$WP4cY!i>q7AHV zW~Z^?h%lG1xoj!BiLGUiVppM;+1u3?nbk+`B*g-TwVX^Zoc9JlJSHIMU**yu7opUkHhB ztNc{y`hT8msgupWKP)_&P7|?(Dp?@}g&Y;Mn4k@mXsoy7>BOA5xn&M70(o z9ep-qTlH;htN1aTB!2vmS{oJgp~`HEM)VQBJE#oyDoY8Z1&TGCd(7tUEZ@GaTtvn# zjYfXb*Rq4?`smR=#P!i%BeXXS%CWhK*_h`*2O4eKxzo4u$XpwyNC(W75@rU0%;f-C z%H}S;^S$!jx0U-={uFs!oF~%7R5p&?Y^&n5Kw2u=f=kDVNcp<$ zS5g&hp!K zm%n%CQX=Mq=s$57!prP_c9=b>kB`wZTed&Ym`7`N?oM7Va-$Ld6+7$6zibOlL zk52ThJUVQrf4*1l3cl^yR-%wzL)HOUyQ`&`hx(|7fe6?o|Ib+x1q zZTHd~JdIAofsa=oxexU5N25QPV|}w}-y@(c(4qC34w4`D~Xh{k- zo=?>;?0NcBmL-1K_P5J?*B`sYW;x9g+q9+fM*&lO&jQ;QUIz9S-+LZl-u|y+H#smPk{03-1A3CHU}jEn zhR^U>lH*W!eO9k_;BhF^d-oRMv%zBrz9XmCJ!~6q8vRoYojmYmX7s;EoE80(H)XHh zN&6nHhn zk=e8V5SoAGk*n~NU4#zGJj_i&dm#sV$Ya)6YOyh|T;#|(W98$_&X_M2USWKRcFuM* zUiYGnk5(~?(wtmu;=1?vcbHGq`1p5Lb86p*d$D79sNEsDn5z&Y&;!9%bF!s(N4dVO zJe(vWLv$kTy~<}8-|3kocfB+R=j0scGwid(@hlzBX31wLPS4WWSg{_kc~kJS&9_|Y z3;gh=<=|)ek)?RX-a*?j$$18kc!%|~AL?2!;JaJmlPxs{oDiP8^fHzk9iT2Eg8raL zDwB>fM29LvfzJLHS4I*?1WF=x&^Q}3QXAhchYbZlD_x+M{*YltbdBCB_6dBTe>Uj% zlv86m3^*k(uY(`DY8E!C@khOFta`)=1@6`Bl?C+gRfj{#BbD>=NaaAK_n%uXj!UHu z%>b`+z$=}5OT3~yS((GFhvV{c^Kw=pCL9y=X>6p==mz>EpE1Du9Pl0o$HRSf2|x<9 zf|E8PZOe^~yea=6`U!dnnH$4vl#jxWq>ZUOfbZs+CDw-zBa^hJcsT4Gy`{+Y!XcZB zhvVjP;Zz+an}zKuTzVc z4hMNWaNImD9B>NXgP%xxXh;WE$vg8IW-xsBM)EQ8KhJZrT0BoTXNP|#FO;(J!hly~ zkcG1S4iGz!2M&3TohJrJ#sTTipapRNwv)~~mBU7jnUz0Ex$t!NIUv~2!Hbbmd8dja z(&z2+h!*TC*3SVz@YCJrDDOOX01%xNQzXBLS7g0Zn@Wz!Wc{L}hLSX(R?AC%N7&grBOq{PX~s;OfLLBhC&}EADCKksjpd8s zmz2iv+X|3oo{$FQ_s}rQ$RNcddknTigUm^c*)-Vsls!WRDxcJ>wE9kn*D!lyCWUOr z?%jNVMllkWFY`$)$=FWtq#h+th}_7D@kit&b&s%NR{lsbt(lkPN`3&SPS)_<rvzim;rOsml#`HG`KW)J@i&yK;4lc`ICBLtaG#v_MT=$|BD z99KrQ(Y8~I^xy}~ zxO|ze?m;KX)HqHy0(f=Osp6HgC*uTkDov@FTsNH_{2n^(7zTb+>_Iz(q7OnL3{=KK z)fQ2{tV=Dz>+D?7PpDY}-?)3@7-h?L+=Se1cxW^$h|yvN6Yde8mvAe%D)J%BN|?xK z!&I@_rjfi}MmP$!Hjy=O10LmbbE47jArv}~r&H!GwV_77^%5G(Ytu=29sC|bZ99UT zsEQof6{ksO98f9EgWc;p)M@rL;Hck*%;;(}?+wP@F*{1JC^P04QAB9I*wG&evnN`C> zeE`a@%3wJXNr9pkc@70RSL5+K$#S{2pHpexbc6~uHe2aqh%Z}!F}=F@5KCn|Au1`J z#ypj32^m$qys2`)C+jxgQ`|ZA8+ol;yLihL|&bA(93uZ~CQ%f{nKUdFdX@^fiZJXO=NmpC5RmZ#OqXv61Z^kTm+WWU2X zsdqwoV(oU<68Pin_wW|)B8F*FZ3ljR6UQl>^Sd&y@q<`4V701jSLrhYuI+`xSolbb?d2xA_K`j|>141XI?9PF}NvElz6G64xS;!wPrl!_h6nO)UaoD4TPcZ;(c83=77wB6>8r3e>Ujr?b zeQdkX$D&B~B>k0r zfC3Dl-AQtW{3NfEa!AH*dGgNeHFIaAKUsy8i(RMLd8OB7Fp@R9SvAgO2$H&9V8gX( zA;@GMfV~a5BX^eLWcf^ECV-3e=FZ9=(KjHS%l5e9RZ2y+$z|APBoqDbmA#2qjGsm{^e3OP^z>;DM$PLxuQpUy^;@E0okJ4lt!Oi+8JbTLi%>^R!K?XgVvO!qz&1^ zs{Dz7*p_75hm@J4aa(GxHBnnvnv?!_G;d6AxSQrYl?%1GH|0|i09I0hQR!A-(YjMXXK%?ywy3)6#(UrfA$u8^SB0F~R zkxW1%=J+6T+Hu2co8&0cS;#(>joP|*3{}w`gX5*!m9b5OBWUGJvZv?>+#s8fw2)1t z$~CssDx|s&94Xl;Y4B|_1j^WhdRB=Bg((i4 zV2%3?=Z}Z2T>dy?8<#AK+@|VIIR>KgtD#gHNo_j*d~qXNuzn-uU$O|12H92YaC`>* zW0@_50xLJ2EjWLC`Ns9o+mc246_H!lt9K0Um zv7SmST0-!jz<6;7jP26+BUTljZ|vC|?kdv#=O1}O%HQ*x<+5V410HXt=bo~J3yT-= zU#?6&90wg}!vQXmydXj@rm9r5NK2=WjSzv3_ z3%ZTO0&>xr9OIM+Ub{siqZ>v&sO2ir6XlBb7%Z}0{BvM|lriXe$uSAZgM@{?2ci&K zwQ5+v1xDw8@zDICWmL2mTS48}e41&{scKC=eIJxdyJ^fmXH z(xHn_#dQhOyu$O$yiV0|b~)n-&okrYxM%u)&oli$@y`O@X9aQ3_*tH3{KB|rY^moN zyEE>Y_JZ>azWc88sH#d2KrW~FmP2n}V6`OuVIMg7?GQ4Fjp|A3w*B}MyS~QX zvF(&*-Q{OUewNaG7LuRxKJn6yc(DpF=fxRAuZ@?}Fh}HU%osXXI}aycU4--JFTs3~ z%e1+eGx9fWDdvsbpxuPIBX??RF@NM?jOAlvm>_s`-UHwjR4#T$b1xI=QrdqJU$l8< zw`$)Zl+`^~H{{9iT>sbSZ@&2^q=~=qq_^H;t6~@Z#zQ=HE|stTpVpFJ?Z4mE-*3PF z{+r18P5u3Df3tu6_8a>e7ryD9x?k_Rv{#YT9n~W-J1o_DYQ44onEGE_3R#!tI^Rwj z5n65ymur8^9v-$6kz4!W7>?r7kX;JaCj}EZ91{+u#Z@F82!{_Nt9&c^oXiG$#KRZ& zkAu05{rY?uTIE@=Kz8fb>{j~RzyHV+WQo?{{rkt^Da?=JqBQ_8_wz4v=%n(5gXUJJ<{$z`_C>z% zEjzMIhJB*ftqVXe-K>t+cs<1;yiQ!U;Ypn7*aBYiZoGbkP~x5E_A1@lYvxgW#_lDl z+pXpiEBp8XJj1(F_Zey^6ZBA&#&Vue<-288^HpZA%5!Sum3yM(#U^fL=&32~S#}nq zPuA&>e%YZzk=qZIVdtC6*ch=Pa=X|t?$9B99&WGJ=gC{E#RlTb!Jm@v{}rLr=ReA5 z{}Ri&A7{Ig`aCoctN#N>mFE%3)i23s*`~71>Z%hiW*E>c1Mda6;+svI#8;a)i?3tf z66!p&N98;5mxetaz4D~q&dJ0}>67G-bg_6hlzc$9MpEN7EwX#}79_-pr8^VR?!h}-VSB|b=snq|$gY4RHbh(}r4j(dJ zTNoZtVYG<+tAZ~Tiz*IEm9bF=`O=73jQf#)VXKU#;Q?%^@oM-`M9+>ckFaYF9)#_k zfDXh-lh6j$MN1UzE0jz2vhiSac^H%&3hOzQ?=F;-7%jq)fAOVZvFMRl=n!DT>`v%e8^VwL#+D`5v;Y9L5ZoPYfcCj`U zZ9=MbH5SAH&)5yUz$a7mp~wBAJN5;ui2ZPG7O-MfxHNW0=E7}cckbes87#&x88?a+ zl`bq=vZRO&U$Uh14k4B-S|aE-KP-Pomn>c^URb<%iFjejk|p}e#oZQ)=%OWy#Pjs= z!s12vkUtkMUR0%BU9zaixN%9hBC!RzQ3SwVq}21Jca|*SFE1`C!c78Ja%brh*1F{C zqRN%nCB3-YqLSz#pzTIvl!z)6jJ)4%$>MHAMu~_j0ze8uV}}=YTdJtovKUmnxX7Vm z%OYf1Ub3hg|7A(hz1=)i5cOh9Ny!q{YDqUd0p&zR37{|SX4^C3u9j(RHfY{}7NWST zAIsDu5d*rVIV;XH#9dk$qvEcf9ywg5AG=X+8oAQWOK|S9_Ug9~9Lh;kTdd~y@jn$D z=b7wbU>{b>F&UxoNy^2Nc}n#6GRjN!fTcfewsGqL{zs+N;`kv&kNYgH9^g%)|0&~t z+=xIo#?Ffkn?O&FLl4evx+gq`tuk(x6cJ6a=YWOTILyS#V@#z^iJU8b(+{cK9?FO| z3-wSA>L?Cx$9hAWx?@*Tfag9#tr^Vm>(3c)MIVg(L77$=yDHz5@q_5f)CJ@X7+9`o zu?UOU0wE+RJd|jE$8z<)gONE><7a}DgJ+$^K^(W)4%JV2w z^giSL=zVrpvKv>g8o|+zVfa8Kq({NwSi3`+#M6A8c-qdY;5c%2=<&jd$yu&N!%pYM zYDG{Spcr59)qo#Ic#<9emw1c(4%3L)kbK_|@LLb@`TWZ0DrL-+GkPOmz!yYsv`e6w z=-R(($7}wE*b#Iu+bcRoZ>nCJeJd?p6J@GgjDxQD* ziFJ>QKJ12_JKx+%JJjK8=Wl%Q9fJqo5q%M#diN-Qed5H|xxi<*O278t6;~XLEW)Ss zC6TNDYRw0CKCMl^RBmZW1{Pz{9)p@?`jYIuI_2S@*cT0}g(}5TTGoQkKSi=gHkU^} z!v29hjlA$b!=D(PAzLqxSOr`=Pv6zeg??x4lvDeltcqDH%>vZQw9j$@dh{Xm=FdQM zruE}KT2yY*it->WGp8Ln3qtfDkA4==gFt2x9zwR=OYkV&$8d~V0e0MDRyvB8gxCcg zPwmp9+n}Cjm+D{k=u%QN<BIDP4LLp3!q~_eg$^E~j>!(rtLpLB9;SZ%F<{ zuTRtmr=-kDPmex=-vtw2zbLXz{=T(WxBToG?FxGjo}ZoHsekWbrxdm`3cNSF_3ATl zKw-PePct*;G;hu?I;;Gwg7Le?>x1R*=p*)TYLmz0OZ&v+$&n1Ldz`fdQi)TwzCI<$ zQpv1RVN^l6#6A_WKq}?(c&lj`%<7N7td&na#nyD{*tt`u&K-~R@7F9CY}W7F4d-|) z2hsMmCQb5!#*tuNliANjzk1FXU%A`oGYfrf!wqmR#psvPB(EZ}=sDg7G28IaOm-i_ zJ*;S_lk70F7q-sp?FancvYtIp=~-5WkDkA58hhT@v70uH#g_=yZB6_hacn?N4h{pt zr?`x*nOa;tRm{LAf3_|=W7_3a27ES~#<9I?@-N@RW@25<%sqR=-Qq%VH|x(T*vyYV z#;*zD?vFo~bych^M*F>`P210_L*cy~x2_KFV&%gCOFrh(R1Gd`<3q&AEUpO`Vd8lb zQFXY+{)w-X0II>oaHE%Arh<~8R|jnD70<6*_xL)og;j^l`>>@Hdc^X&Ve4rsMg*$E zzJReI>mG+dh9rP3mJ@8Yv>I&8sE(uCmIuV0no}J<@8t-EgdV-5Is=YqY&3ClNB_7w zp4t%bdlCVLRA5`8N?(ln04+4>!KrNuq*w0TDgEvEXNc0W&4_$&t9+;>tlZS1p#C<3 zJ^u91D4JUxWIW$qg{wCFL+l7X8hwWtDgx(&iAy54iUpEmrc2?aECoZyv-z{q$XMj7)g~62>ncBDLt%02*igJhNDm)o zZxCEmf=hNxoVY{5Bwl#BX2UGDYazV$U^%KnZM(gh6aJc2_A|TLR{T81tI|au0?6J7 zA4YnG0*d+&?T2*ezz-^kt_+w~szpG^iqPNz8CObcS9t?=Uew`=C2J>Y==rF!*O~1l zz3>id^FX%&&`NMi$xcD7Wvzon&Iiw;3NYDHBFWC1uJYoTWSKRySP$&nDHhHrb%-jW zn_Por3;W{fvb_Ool{CAw5bbXKCz_}kVKd>-X7eKJ8{ou_xq-r%ByBNXF7cB85VdH0 zh=pj@*}ka+Np#2By{L{D{W>ED_D|;! z#q8hbEKzQg&*rRpXVuEdQ*S!;=_QMvf=lqqDXUgZnY?o4WVpj3xCPlH=v8BN@Jt%5 zqaGi|70@(%AUZ{jXPLc7#EvUCUJb9gIpMP& zI#EA4lrlwav?HG!eK6XIPl`U|6smkv(H(2gHh{;|sW~1Ip<~9Ep4X?(m@$jTh@x0G zkIld>x-AB~9X=_8n10a*GEp2B4b;^;hc)vCu?(D}n^PkvwIj8pYY~s+IA>Z4!zMRc z3d80%_-vbv&E=Vqo%bL1)LH3q7^`Pi=FuI+aOy>qJGaAO~tcZvjt-^;z_?l~& zi5YL^HP=)=Xsl-))~peqg~y7|*5F-TOpY{%1M|_S_im2`;G4!_NsQ>wL8p)<#$%)H zqwT4`tDnWE)+|r*SLMlWSSS7|x^Xm;tIFfJ!}iXQZds!8G5mJ z5n(D`6Mw8cQ6lN@7<~x%o$^!p=c>F-f2=)`N;_}H)od2F9M3WBIvSz5Ahkw4s$-S= z`$2wT^wA(IQK4Bpzj9TLYVXU&!#Cq}Zn1GQ0yU597~VMA5jKH-k=veWS{*;y@OF>L zCL>rA|NK=esV^kkA)w=C#(}E+C&U*%qIhV-S|1`4)_(9oic2? z)HYC~9dQ90x^gAo9Te{q1?*Z-Qxc3;R7bDB5;edw-^)gU|8O3J!A8{!KlKVJ&iNR~ zs40KomqG{9!j&uagCL})@FjMz&PMUQjfY1oK?KDAAmNYohdI3wp|jLea~2qC3?tjP zgG?nsKDCR3(+9@@#T?@fIY3bjUNA?E(I|MU08a)Z6+0K2KN-(L1BkO^wQBs$7!ux6 zW)@}`=Y)Hi-79wiIgOh+d1aj2ZWi8PKcjfL-)v)?t>BQ)*yDKD;>1N``fzvDwfR1@Faq-Iha&A1yw5vJJ8e6Nur$5n zHce02rk%cfn`Ytr%-z~HP3yW%&2OrxiqiX`H~RyxkD{$sKrC4 z9P1s4MtbY-bmoP-&u{eF8C{;8l)Lq`=-KRC%Mwpp#(Z|Z&nKR+&d{$f>wPTxf`00e zKD@asZSJ*1g=Mh`_xIIB-(N+)w?oDY-%NaUO7Mxk7rbyubMcEc+;aYlA66{X`$UZn zy^zW6(9YC`X`{4p+C*)dHVb;81)12JLUsqu{7`ZC5c=rB z?xE7|!HmoxdhXb(u{hJ0Yx&ThFR*+WRzYcg0ebnNe01PL*qak>s?vChApkfEmR8~> zI{biRVho^r2sZ(T|5@1ev;h+eyY@e=YjkOk9+x+tIjYIza~8cZZ1{^Shfhl#eQAr! zaX0;n(W%phuX=IVuoqU1yf}5_^yVy_(>D2*No-mBmRqN+7SASSzBeSuk2bYfT3}>! zW_knhd*<1EOH$_h=cI~xIeE!9PZmpC7Hpexvz~u#*RB(;Ei5eDIc(V0MI$as88NLD zA<4XKWRr;_7rr!X*rsbnT+ndjB`xW0aQ4WC7mUCH*-n85tWoCB4GjYS7?vUWRBtyeX{P%L65r| zyLdYUk5C7@OitOZ6>6Pix)ABsFhsiF8fiKhfznn=|K`}~CK-FtC2QSr4Z<}B*EC#L z;<_H!8eHpey@=}{xIV+x=)(S{_8k)c!^P>xPXJko3qP7;bB#2RlZMydX>gV_Y;BUJ zU5aZVu9dj%#q}hvmvFs{>kC|sF5ISdf@?H~i?qgfJAAk6D(Nc(eTAT}5cCy-zCzGf z2>J>^Um@r#1bu~|uMqSVg1$nBzCzGf2$Ga{7Jr4&{U)gUiS;HBgO2tirFEpS-Lp)$ zL^>b7PWSUcMjKomKnkV1z-t33T28ixLxJ!aNY{#sw9*nB5TvnuEcVVbu>2h6*&UW_ znfCVv_9E+!7ilkuv&FkO2x#y`YzyoDKk+cT_bK4-){xqZw23P1>bK4-){xqZw23P z1>bK4-)|*-zY!p|0mL?_HVH#PO>fPX=*e4KU>+-x)dCrblw=f4M!{^Pn<7p3+g9^d zmYZg_4HWPK3mULcL3BqwA-`*QD7$HRklo43gXc`F`i|d|v~+cnb?wUN4r|%XKJ&U& zHN3J=Om3OU)^YJh<77T=WrL(!ulM^`-=MxBP1K8yoNKlv#%e!q)6NI+<8bL;fqPsU zn4pFPwJsRa4C(WcChKpFbQ63x!8b{20MY~Oyr&_3nw=hqG(A59-)G<+Mb#l+$%r}x zF>VMV>JUWKA&96$5K)I9q7Fes9fF8D1QB%zBI*!C)FFtdL!5{@1QB%z6peO{YTr2R zo$Z)GaGumjI`s4ZL7NB1&ZxoVQff`;bB9^hFpKx>i6&4u+J~Az-9Epd(VJ%$zA!cK z)j_c)&{b?4nn3HECeRlB)T6z5b6L9WimZW)vb$aH?|Vmqxz7@hpgDBW1AX)=r#W;G z&7r32`(3bQIyHwzSk8a(J(@#(qee$Ghx!;-*(LdyGyc}MQ(5Y1EBfCD8{pcrRfWbW z=0CCHS3h+0F+h$fo-pDN1V$6#DeXtP3nng7x(m|i?Q30dw!k*cfENAGpkwMykWSmZ z9WMu*96{V^y<6%x1pS7f-w^a0f__8LZwUGgLBApBHw68Lpx+Sm8-jjAj($VXZ%Edd zAwbL7jV3F;d*izcz6X2-y=TLe$Vofm>Wr%q+PDqS1|gCgfaC(LNTv&rJ_YS&x_^pf zrARA~tONim2_PHdn+&-DzEkm?3P{aSnsT9ijJF#ZXbk%@LK(hbZUN>e;_Z%X7o&>J z?~U_?U;#n2A-l^ow#!Ak(LRl+6m3lX(spgHyLQ&C`@X`vtX;eKowKjIu5CNqyJh_7 z`T6rkkH6)*w&H01{L$mLu2{Nt{AgoLv$of~`pTjyjrZ^=Y}b2}_B5Kh=#^KmY1=F- z@0wSV8mt(1uFrSwxD^eOUb!akB5Tz7D^_gSuwupe=-olLqRQ-GehE3W#daWQVKtVF zTVOm3Z7&i+7Sb4Sm+ABr88^HN;H~aZ^Lw z)DSl{+R%}dM(sBX+P0u=3);4zZ427Aplu7s6H26(lhiKGCpUw+dTwcso^M?B!y}J<|NUc+{1Ex(NjBm)9Af#z%IC35 z;ZOWAUZqv>?Rv#yhYmgV=)QeFKltl!Y>3$M+piBUNfSr;5-xC%BJ-zI(?xS?=H*Bd zsp1Hty-MT;k#24`1)Hl@unsBekfIJL>X4!iDe91-4k_x8q7EtQkfIJL>X4%DNKuCr zby6a=g8k5_-_fWa8ude?erVJWjryTcKQ!uxM*YyJ9~$*Tqkd@A4~=423T@S7h%E(T zOMx!Pr_+#5Lz=`Y^+A4Oq1q%y$iPxiWm7r@=~(lhes9wPC>$U!MLGlN45SG`kb0nw zLP|^W3*^C(#Xd75k6tTgq*J4o`k?$*rI8>0lp1)9V?*fQpI`Ze*mU?evFXW`{JGB` zd+gh9AA9WcN11;SV^ySzqiIVX{PkD7EgJIcuMaL$vG0Et_N;_|p#kgxY`1*D1oy*y!;^x)t*IUIsQ&RNOxeczEC$`Tk_+O?4Y53r{eE_~{#h5RLEh{#Lp|E9lH*^Vt*3%*|m-!s5>Ywc|n-g7_# zHKp8E2>93V+YcH|q;rsNf^_gJY4btYe9*D^AZ$Jen-9X~gRuD^Y(5B^55neyu=yZt zJ_wr+!sdgp`Jl9U$Hpm)v?M8z&64;HNJ2-bBT0}0C)<)B8>YsQwOf&vw3}+7W?HhU zJ5X8Be0Qve#_O##Y@11PvPFi|z<7rlGw#{E`LcU9|F(MV&G%n6=e~Q4o7ZmHvQ{jf zEyT;?zFWwz{q@YhJ-Oj(F$l;JQ;wmpT_pS3CE9z`T~zKz-NhJ5-nv^^PBx@O8m4$k zmedyC6c+RGO)O9^hHBW($X95mi||dgZFh>j0r20}4q$D8EZgpfWFwt#$KiZrZi}k` z1u5Mg-}IYCn@Cli@lERNif);1<08(%-FNg_|kf@YsgDf zvx1tG3`!s)U`Ut1#wc9^CO9r@_jdSIL#;%5R66bicdIAO@uY?QgtVseY)1#*$<2^E zcek|T_ORpjjvcp0vOVm$J?ywW?6^JbxIOH+J?ywW?6^JbxIOH+J?ywW>=@I15WCw0 z8i~Cl)Yi$xL3ENnwobBBC$o!zna_S>fponhZ)Y|Yh= zKl}2_FR#5R^XfI9kGf^`z4xxZWfWgKeRw+Gb@NU4-FMT?(NOxZ8Ee@2>e>0mCuI-{GgK{5NA%g@;KBt?u&w}=|p#3apKMUH=g7&kZebi#Kk2)D94Zh(;s!VH0 zXP|1Q`x#WbZGJdH0gFW zXvLW+Nfmh`&3(IvUL=lOkQ#kDX+=_!`SvxU?TTh29_vM~_Sk8V#SiAEs)lWB4{kM1 zfsLhteTt|ghBnk1%M7Fiv8}KgYtGF`3Fek|Pc6u$q35pHQy6osn10WMeZN=*6{Y>| zyQsstpO4n_yN+J^@(};a?9}sh^V289VewPX(IeVDF)?$0viNR|wU)Ki$6mMRtS(#M zV=Eq~pbeLONn_Sra}P$>inNgkIUt|}S0l(j2Ui2=paFgtLk~{#q}f;7(F4lR8o}Tt z?b|UAEMLkm3E4=U6y@X?dQl)VqbP*4xrv(;pXSSDvWK9fZ-Dp2Fia9^wr*wQs!?Z) zcldH%3#-xPKkvCZf4}J6%ZipSczfh`yV*Z=-e>x&XPhFAjcH(=IrY_NX6BiD#4jV7 z4q~OB3>(o%i0`9c4B)?Pe(~c)oyDyw|2!0Zg}?mB+Wd`|w-EjFn+@A@;5n)+I2#K+ zd<;D#!CN!H0ZErd`;Q5LrZ-{5*lij~*&uJDlRaeQ7IaT7&E?Lk2zp*jaR3s3Z%ca2FYnc2*MMFhOi#uj zx?emw+p?Cip}P3_-P2F`@!aGV`WXY~jBNbwbj)+YGTZ?gGk-!`v5kC(@HBNPRc%XM z%0{Ra$dzbx%EaM*ls2WirAv#U<}e1wCG%-+Di@{5p0;B_mh-faox=MYio>KOYI3JR zekHcQQjn%5Nz>ii;Z<-lfKZ{kBkuIJ?`+rl*eUWXru>SUt&U^CuT(2GM6MOGn1#0; znGk7+441y8p>n-Azo3mzU%P}Y9rHq)!pz$z!STj)=rQ`z%<_?0`iHE|MT^8UBOBb6 z**@i}iCcZG&%GqDvVVj3{(Vc|Mop{~bMH5z`%TyDdxoWa*+~3wKRhyb>ilO0^lhZS z^C`RJthaHP_H{3p`oA1>*Awkpwchj*YaPBkIPM(84zyM^>nhk)5TQ)Tf#l(bEbtEA zwpwW|)ZZqmVJldSA)Sr4dv9I`Y<7CVtQ+<$$=)6P&5C}w{_Robr$1lVtzzb^1LvMq z`EB$%{@e#&ezK@D;#AB(8oJ(5*bPVAWUzc#m%fG_9Tnk3;MqhuA+3v40$5|2PD;afton5c|i;n*UO=p)AO( zIb^136C`qq{ew|A5g~(-ZjV}?($q}w?Z$=tl=dT?hHvUE)9+6B?u2Jl|A&wcAw3xB z{z%ikk@y~IKR?&LHyYogfqM*agutxw?hJT$y__2rJUMoHYnG7@V{O9Z@EYEx;hiI1 z+DQxTic{NgE-7{zRlyYX2Vz30e*b&Zv&u4C_ilgef|QTjob{h+`Z;?j794r~lB}br z73cSEJ-NlcGk$+EdFGY=KYIIa>^B>VAFYoS~gwJTK*)@P{oX>XS2}6{c;hI>NUiwxnu# z)UimB>d+~ElD*S=TMIHwLH}AsYI;ix5mL2S(wAZ&mcO6rd$Tb2(-y`jIg`g3=H8^r z=%byD>sqg9YW6ceRXB{xfTN{WOzqMZ5(lZ5Q_sf1lMXzo2umtXx3E*t8X8%$A&!^B zQqTq1;x)Gov;((GEzodO3YWCoH^RglPVz>Sok^OHyU^ZM)E_dZuJW@PlM zHK#S(x1rzhBvU`dX!vCKpDTML-S4})jd7as33SJ`SE`O!B-f@8 z=}n4q>LPMb=x{*20czV3#ncA~;yH1Ug)|K}Hs6g_T891hfI>!7#5?jxdar}i#ATZL zM$||&P>8zy)a$14$|P`3J7{FcP!hCL;1}$gZ7@PX=0OuUax< zC$nxF{lx1=d=!G$M&R`+j60IZaixO>XSCbVFZG#eZ@7@S8>} zs1HCDIJNZyT|M4Mk(X*WIVVNx)1=ucGS$FMCA+BD*@l_v`(z|b}-*e znqL6oZETyU(yubdhG-Y-xZe;&QnyfPp9Vhs?%e=*4I*DtzzWEGE%3aR`>PqA)6i@X z-&EO9{ycn>tZ7Vw%)Y?>rhaTYT$G1~6ev#zK^%xbyHbxs+F5+?WXeGR0lWJ$xg|bi6iJUEMIDfKIF6-`RRirrsn!eL#gPy zA`sz7npA#67xOAzzv^nX(ClK00~ohozML;zj&GKYF^gB3 zu@K+wpB@rta<=su!yG+uz{29aU$1|>L4dVCr*FF0-0)m>&NHl%9h$z_@7qxlGj(IlMYPIB8S{>%SnrBNLcplx(= zx3Xp%#J};uSLnAzE{|NUEhoTSTlU^6_LyJ+w|L{9#*40+s^z0 z{U6M?Mo%!K0Ar)_@*zRjYW{8m{_e{at11Q##D{T4LsC)0{^142>s`aCWgb%u`cF|CSXvhW1u4yPPVvjWJZ`>H}*VXu~ z@{6WT&4O@hS8M~bK+-u?n`IRlKWKS?*@_kQ;U!&oo5nmNPa-f&`tYJwJiRe*lV8Ah zhwfc}@0|Yq=g2QMQW_E)IsU#+fBN*k@$zdZyFTP(K)z8s6#gCaoean~l;phvc!%3~ zgN<36E?l8<5lb@CTk)dq`k}I+F%0*OcM;o!$S-z%*-%A-q-4H>cvOtw{^%CNKhk~2 z%%*4?*5A`WSeG6Xpf`CqJ`}@F)cnLh2p5^~PmV=ldSou@S^78s zl_mr1^MJ<;XmW6e(&CPTxT_BA#oY=zICSW!@=-%^v}ohFAADfNvMX@nYD%1@zpAHryK2pNJcL}lKQ-0cW&dF|LY0%#-bR>q&EgZv2 z`gZ7SjFoeeNO{_J5c?>xW6DO(_fI);T5+-e)gur5De0}Km@uJ&$~rWoEO`>kmFII8 zU~35fG9~Wwk38UiwYd1mY5plj4{4UFFqe20{WMqKE^Qw+jFrlvf*hX<>6B!^Oo%(~DhriY5V~CV2S4WEI-y3hzA}sa#x9apW_PJbFZaWj#E?i1CB6sI)Ph zt!yqQb$*4vgDca|SR*}6yc3h>J5*;oa%_aQ5ct7nEzZ%&IjN;NzF-5OKibFTL7Xy_jQkY8eCse62X>>lzJP(HptKEHMk zzl43O?%~WU34`uA`L!o%<_CNU%X#MDsa7rlK7GDv8h=+b>(@Fo+xL*4?#1KdAIki3 z&m4T!o+ZG?T59*yEFa@D?wKs_@Z``|t$bV_hmKnEB%nv1Pc$q2=+`>sy?j*HkDVW| zD8G_TE#(s6Q~WD^#mXn*AF$(i^YZU0AD73W!^^)1P6B!q|H`%;dSZ4^O%5nuw>>Ca zu{;hvwZKV0PrQ9phgZ9tB+fWlc;)5c&*331uR~`|xy8XvK(9WZ$|!%;uT4Z>BKvjp z=R8YtIug^P_;=*1=pmUoaVzG}s(j2g>XWB- zdSZHkjrhFr)~mznIBS5b_fWkE~b_Ea7IiRB!F@5rz&fAYXTNx8@2k!7p%BQEBbRBco|3Up4X%|rK8OkkbF zihr^4j=U0;uP&cz_!G*b7C5!gY_{eMD=)5gPu5eb;-|xmq>mddw1YTex1Dap|dXed3?=*@AJmLTI4A6R&U?eC+~^r zjl~T|7j^lBl3mOv;$iDa3G-s<+@{QAh>fuBhJ zb|?xAIe@6*EhB4Tgi)bST8+D88|lhSIX3%pO{@aI{hp7>aV95K1cQ^iSIA! zEe@}Q5zntapJKdf|LWJq+P_YliSAWz|2plJ*t42)m(LR5Q|%L{|L?%ADaTm(MDkMh zT{BNzbR@Q4#lNy&ho40D>fn;{^d)Zp*345oJu$t+>qE)MLl0ya!&$qWV(=2lD*-&I z8z*ml=&VbA9(_PIiQ2z0*(BoEQ(p4x1OaTiu=%kM*-tPu@5g)|1z`GAF}FUG^Iv zj}ojQqAxK$HR7Yxu>-Fz`*q+sdA;(i?hn=T)`w1~OuRiLwqHd<-S(^C z#cak)Z*}{sp7+G`o}B$whks%@$KWeoW8-77K2w~{IKEnM`}NS|l#$kw5Y#ptpYT>mz>$ej@oNkOyd~ecZM#oq6yi;HOsmckuaC{at$; z_vk^EsZTtwtxWy(60=`Nr}gnaC5xEP)?ZICe2Mf`o4*Ph$J4s~zsx zi;RiB>iwAn@T7bk-1U(^@}3O6aq#OSe+Pad`PV_eq{%6BGJKq*{d@Q!9iG?^6hAQ= z{3~VZ&ri&*9G(6ZeD&8;44)(Wlf?HI^%jR$!brq#;&@wEKOp8miTJ6_{+zt^(NkUa zmVjPqZw~zW=*5BWl=14L4*E}84<0|hXd?`1l%~gNB z#Ox}OJ=GUq{q+>Xmq=gr!1ovR7KgW1{ncSF&_(^~AxD3SF z_)bQD4!%VCs|SCQR;TP=;Zw;sar^f~_M_xWwordRRQyW&@tv$cRxf+<{Ko87(Oy^o zJT8_y`jy{KTsV0>$M8Ek`-`&sKk7Y>4hhRE4>j*tu5-!4ykpvQM(z-TiPY`{ewG`^ z;onw>683%tE63d9DMOqI$V0>f4R?r%tP$%`fvMRv#rj!y&M_rD@N*=5I!;{SpiORQ zl5-p$ePYGf3h^8pjxUy*0M~xP1w=N!ggE8gZmTcm+S6`3nA07vU#sontTQt80$EE*#b)+&C}c9DBB{>T@H4bw4;?0Wj7UG5@;?pQ{Fb+}lj3OIidoE&A(rd*noM=(hz zCXzCqaWW3kA z@|c;_J6vj9OE9WF2Mone+&U&&!ema2RyBxLDHzWBrQXK1;nLn_QsuGU#(UvK9vxv9 zG_Zzst~j&SUV<joz{%df4Uu`-%$B5*Uv!$g+V_dFrjb24g-iTfx&Ema#aeqQZ>~ zkfE}5@=@BspY-8kg$U)jxqPm8(?M+}i3e7Q2fRyg01(R1UE6iwK#}$;5JgXh(Vz8l z=%_r#rmSF7K#og{180uy13IDx&QoCs7AAZFOD{Y@Du)$7Ss{cJeeoJTn4IF;YvUXu zvK2e#vo7M}GB&Ck=#>&1N$6s}A)cMfIy7J%=89*@o-ED5P3tPNv1Lkk4Xdd%=Y)sc zHf>KLsg~vhj3uS*@Km)`N2t zOBp-~OOdeoobCt96U5614KL&SSz|E-g+(>u;f6cH*HkZy^_&@uRG&lcs6PdPRj`3r z@iP$LBh6jmYhVix!-y_!xWibCnB~YU2qL^oe^A3XN+OT`*ipUcQn5Ftn!QGASzX0k z-J=wu&4o|4Pz^pD)WrcuE7q^CV540;PoeG_8^eSjHF3%K;2Hr%h&Y_YlgH|EDJI)Y z8IVKu8aVn~44qtb<~234JQq_!zT#?NFK_(u3h6r4EBrSru81ML!sf0T&Ug+zcxink z$t=F+r*;ATv$>Q~O{@5*Ca)h+1K}Op!+N-~O@RF!)c{-H%P79BU{Ch zv>D82EDQh7XvjvDiH}*AG8rD`iq9H|&*r*%QL#!R8&yQ|5~E@Ge~utzXtJ@%G7cV! zZ9$(x$!}=#oVuPDH70(vgJoK5WE7M`9NfZ z95dt(euH?!UyYsydcL{TAquy?|C*u;A z6#wrA1W{QjX8y@W0tC!)&M^^55|LwK4)NvCL3}yO{}dg?IL8DHgy=0^N*eCqWwZ=W zRXc&Av0*2!D&glFZx0fyDD;gnosxfvy;S19D8H#XWmF&rVJb_>9@j5>?uhaO zxJ&4yFeH7~YBS91&4r3SM{xNiIH=K=-^NPkgZOT@00^<^Gg?Hux#9!a2Z+9Yo z!T3tz12UXAYoqZqM>_0=69XGL7Yp`3nJYT8vpguoRu)JSSws>$%fUAT{7HO-Ea;-~ zJ0fE&gMfWZz`XQ|7h}K%8#BuA_!E4@-HsE8lxJrtPlXT2AW~a1u3(wj`NXh{uRxBH z`PP`s&+K0`%z6f$Q=h?G>6ZT6#&J;ZZ0b;bW9`FOSgFvbAJ92*9jlIfYn9%->LHAvSlVjIMm@ypo=F{Oc+ z#LhVYachj9S5;*^hdt`B<3S#6=m8)I8wE59N8J_V+B(O%nNOpw(E9bJS7dsCpG5jtOAFM5_~l`!MXqfxTg92N1?a*A9UjHi=@T7bnR; zo6h=L1ND0)T-$*f^K2!GqCU)=%|MT3yBS;9KczttXMm(w>jH5KwPm( zm8rc43~c%gz+P>`Zp`F2COc75p{;uqW5LvDF{zn7uL92-f1GYu27<(NYt;F@*>+VO^&pOx#Nd9<>G>Z2(PKE4OQ)Fq$F$owOBs`8WPM38TTs(cTz8=-PsILk_!BEiWk{0w8_zHM)F^;Xf zwd|X3%5G&}IiX%Kr9L z&V7qUj40~sT9W$dPio&j=_&aMU)ff5SdKyau%9h_mim7AZ9*UbT|xBUul2f>+_k)vB7dY%(AT~DKfjVIj z`u%gDOWGR^@gPSEXAt`ZLv#NT0Sxao;Un4i;@M=8CEo#g^4r>mqO3XLCJ;)%_|J}p zd}K1vf1gZUZ^XK7ReQkOae|Kxma!S1923lNL7K-E7KM@QE3OR25olZ=0?;1@NRb8L z=J6Od%C8OL1LJrg9p^|M^zGpHm*e{s53)Q=z#pT@#MlCnm6xGsBqtM9Y-Crf9nlmS zv;&8IH@A%D_N}8Yact8G17#hhNdQ(!BZMGhi8Ia?J-T_c?>J@Cr~Q zM_wV*J@P%rF4`e7Hm;kCE>aAt+0Gr&jmw`0$o7aX4a3?nE6jGzKrYFM#%uZ-trdJ3 zW{C?HjO9`z{&Ez#T7IG&r+o%;N@|DhBlJA!xFpnEa(5NSc%p8QB#Tl~39xBzLZ|AseH1c!pK%`V82Qu{o3+BPWh!DNSac0=?Idh9=0bOsHiZ}ikD@iol2fzr_i!| z%LPM~O2aGKuI70a72yM3JGFIyH#mxeK<*$X6aQzj!bBr%j?U&Noq66BD#H#-I&8C+ zj3}Cv9wZ;IYsH0fPJ2%;^SC1d$piVntv8dLKyjfzEZs%I?J8Yg%DAcu@u-v47u#DG zBrU`IgdQorBy7v6VZ0a5e#d*ZH_hXCwxX&B&SUT;EvF~)*?g3nBg0=I1avzZ zMyE_SfSnE3vy$O<(NABfNM>hKSub9CS4RQn^1P*K9hQm*@^Vq8^FusG!`5{M^tuabBa*Y=H|>dUhdqlU+3^ZVyjy;XA3T`oo&7zsPN@}7z-bTb=Dr$uKKC@;zVRSo_s6&2NPqXp_GPVjPJ$m6 zkIKUjbWnAfji&(T^=WwtBo|0fB%Y|lEjC!7;6QyL#C^G!8qH*%_9&}Ma8sz~E) z{-1hL<#F+a+Y~J@eUVS7iv4^8#yH)k;3pfz=gQ_l-*ksQ?7j*Du@S7(8verMmBMHo zzQt===IRZi>pIbO16vp!6k`_pCRipNkA~iHzy-qK4{h>Ox);+o-apLu0^_NAcI3eA zAAE2-eGi*62gmC=UcoCXK45J=sF?ErUL(OrhhRU_nPyYkf0gR)Hnioa%RX&OsxV-q zQ9PxtqwzCe7G3s?*oS>Z#k;OdLUI?G7&*Ysiz$?k1!EN-h_64e z-?NX?I-2uLMl+i7ATTuW(dmHPGDvEU^osem+dr6=jK66g+)lvxE78;GyCY;ZExAF1 z2-AMk zaC*4$DA5}z>5h|ff>1(sCXR2xYapE5l`6k~OGKAG@b(WbX_%bc@Djy@gG}oAH*Epn z-XUjprCZt1GJ0j%`K86w1u8}RMSh!aC8CRc77y5w4%ts^&tuK6V;AN>MY2eC_BTe| z(N)~s@JFjL>W+V#gV7$qH-8-8`APqlt%HC2ev|YepI&J-#-6wuD+*8ri>;uGF&Zk* z<0|Na69{xN!E_&|Lwa!T20f*rjL?k6TvRb%`Z*Wv8Zo6`s6&rr%bzl{F=t8Q50*ON zqBn<6J-v5MVUq?8L?z#}VgtMH^{+?u@7^~*x5=5&yC-i*OA$FAeROXB9(`N3NIMri zIkc33mTK6Fi|Cv{SW!X(K_B@EpVe%L2WwzTT9h%yO>vZEHt*%7a#A|e#c@MtjoC!; z*m{=;K5fImaU<6=zB)#!SpVqI4OHi{=Gy7zea4Oy3)|OI{rm;{+We=nFr~m>z{0F^ znw9j6y05Kv?rR^3!Sq}&aGt~W?x=d(yv}%8#ZkNs3Yb9_z&i^0KCgf$@o&3v@qD*v z%OAxm{_X5ee6WvqoIM)1@zNHz@$&X6{_U)eH167Q7SxFTb=Ru5tu`*q5DVo6@Pf!E z-vIlVvGK=f%WmR%-i?17Z8^ImcK9CLX*U0M^p6BoZ0Q!cl>pK^;EuDREr~|F7roA$ zplHN<37@Isl|)FsSq?FtBnuAZpav8IUi8IIv!gAI-Gqm+A>H&VqAh22r1#4mXYp@s z8nqc#8wV!%)$`GACDf%MuZR{q}w=mpiSBaGbmd2+g z<5d5vxqwaND5kUVIp$8Hazd{+RDd>?FIdVu@QG?G^k{i+8VKvn-#mOcvhVO={b74! z^n2ydLiDA9Km9b2E}}X1z8`&iMhDRm*bJ+0_-6%0e>nQ9DhQK67I&^4^0!APR}ztH zOm=fQJF?5X_WVAAjjoKG5?9Pex?8nl8j+1g(c;(*@|zxW@fu0bc*|g}T~Ha1Ie3kr zXPoN6v!i>I@v7XbXmdiGdV$B*EtTP}M!kA8C&Vi+_I?QMRP@*7pcNY3Qp7d&RZUMQ zV$YBKL(X|{#Al1#m>j27Bg0j@Z3ZZhHhfcSx*nGAWj!-1;f+qM9np*muDg}(7^>W; zTsO4Q=#W6N3G_nMu$37MPcSsQ%GKM8qzX319*R=>8v=ZNZG4{Toa{lPBBI$ixz3JN zhz&MMr(8F*aTPv$gp7*Q&ffNLn4yIy=#JcPm!5pc{!qkX67&*pfO<`Fw3mwtIS@N=Md<4 zxGFEEP}4yrldI>%Q_z|NT2(&3sR#I#(N&Q@^hSE)$bYBs1$;sDMm;6+djvgX%H_=; zV5xrnAwJ*9&eNke8o$?(AI(BPM*)6i?}zCIz=G6f?_8mv6~1|2rNu95k0+YO%+jiT?N;!F`Th zN?$W(%wQkS{9q>ge;E4?fTphX@tk`z*br8jApt^IAwbB2uyJJRGj$An7Y2#Pt&OXvV&=G&UG_+y+(;4uLR;qxlglG7`(dP_&>^IL2z$PJ34M2Dm zHWUt_aKrUu%a6d*J(>#o&4!Lr-|P_ z{^tRGFA}d`v0^=SpZMA?NRmS(>d+^PeSP(PAFr#f{fDRLr~hWfHeNro@im|2C=fa( z5WsVWy%6Am9!>+sG(U`d0se#$5RpSc3XpF-jk!Xy@&8Zjyz8h}PY*vmtz$778}-mo z-q`qzLFJPuN=MAq&(y)awba+lC%~IgY6`hg2*-K2ga$_y;u`}oajYwI$lHAwHOO{0 z9p1gG!N#VJ`e|Eb@9e?tI#!HMU(hu5y}SB%sJ^AkbOhtV^5d^lE;18wm#3lX982$)=|1rQ1 zGV6zE$pV`*c54F$h{Kf;TK({D5G{0e1N!19brG%EK;!WF5Osd#Zj8dPIoblk`>FmW zjRbmgK6MiiK#Vf#44mnC2m2ElbOzJP0A>)=y^P#3Jlx0JJX}G(1JI_hY#*YMl&4Rq z1Ryg0ZVvS|`2$n@x95Ke}X;< z8Qcn@YfLeqlTdC&;&TwWVYc3Ou*L}oSp~tth*ogOB%edPvh%}u)OYv{)#VynzqaDf zZCjTh;#$*|vVefHt&OEiid8(Wd!SUKh!17`)X{OR@fm7-Pnwz{zIO+eK0=ijo>r9Z zxic_(-tE1m@g4imb=U8oRi6;*Y6FKwB1Sz`z_|jn7?T&UnE;g`Es)FjAUu%2z&HZU z&@1Q-!h*0sLzmzAGDi)Tdh&Xi^!9p4chHH7duw4mkfKqYXNxh6ST zl+her$w|)RhYB5-|9IxWJpC+=r33Tv8uR!wHTfW>BFsrB{G_tfB_M#-5s(MYJirC? zPzYK*!X}8d?V^C5qxi5aHc(HgKUY(iy3rT=pCH>g$mW)-rZ&BNQUzNXrBLum*Ws^e z)Ffoo@OffJRP&`S}x1vn2LFI7SUz+rRYoI)NK-eReu4={yadW(Rq#zP9(92?ZE&X1Ki z$7x<^sfiaU!yZa9A6=dl=PZdWP`wS!8^jl(N|CrQUEz(~ZS!K9G^@Xz)t(h0tyfV$ z9&es(hhF}o>+%j)#pKyWp!vMIrc)1^T3gd7%IM3r&%YvXfn5aO3uo0}tporJ zmK@IE7%}lcK7bC}60MGvnYJJ$wXifNpHmstpYrO3!_<%>KfhNJ3fY%gR%S)zA>y++ zeI-?&2!$thJv%b_RI@_PfivM5_0PXz>X->|NEki`vXfhDjS5P&)OPlEV2)&d}27OnyPh;??2O@=AY&GHUbO zS#M9DkxN|zVH!rE)CZV{u&&Px1s?L~dNgenu{7XRY~<%0^0hsChU0Zm1Ni}B346*( z#}w;^59p9^HyjUyY6em4Zk(?NJtxqcWQHw==wLAPby!B8|BKj6zQy_+aAH6TPTL%# zP=5*aLr1t>8THx}>h%g#$l9f!NzBz~hW{W_zOT9013dk9(m+VIDcR_<$W zZhd+BmA9`RpuVU6nUQ51 zM`xvrpo`=P1i?HzUCQ20H?`WYF*X^VJ?;4_)>}*``s^_lDN@QvFC~lPWd}!H+Q~T4 zPz<1d$Gk*EtwCqG*V8`=qarPDo_k{bp6VUDe_GsjLKn<$6x=4)#_2<%HPq^dkzwR2 zX)&^F3XMP=5#barh`@8G2GhN*27QI>;Vf7u#M)>@Mf0v7?3!^p+c98qYn|JgGHM8pikHdeqYu{amxWIv z)mpWltsr7kky@{v4+S>-Y+Fjh9D-*37t2+i&m_^_5t+} zx}uvRlInJ#H*S168bAddL+W;k-Q!9*(>w?ljW09TBYmI*XX^X)X3XZHT8C}T7@S6uww_& z1phAl%$yKH^=qkB;9#Bzqdm|LD5-HGQFn*}$+maLL}f zu5F(A%?tP`F6g599XIrj+3j;3^UZFaqcm^s`F0h#I(6h}{aSUuR)vOJ5IlW#>hRIM zGq$W=1bikK(?GV?0F6oP4Mp$-mO+-4kn54qPXM!cGynh;6Wn3M5HaPJGo>-V{XL@o zynrqu&Q%f~ZE@>D+q?_JO+avddyI^kG6Pkj!_OILkeW_Ex*3KqjnCsKnLKzNWC-;0 z;F*R;isig$=qv{*$LfQrK>BW^BGxlK&r=acZt{z7B=X)r)TMMzrv z&2N@;4OrfE!@N7+IcNcQ{xB~L?Z8A-4>#jG0^O)c<{PZLfCrpFi1ENuLF=Uw=b|#0 zUI8S4U8MgI6}g^>-)c@pn&13^oHy?*>548`-1+>BfnNHf8|Qoy12oH~l4Gb?S}=y1 zPr@@GjpPIY%p~qNGVq3DAk~d9iE0 zxj{-?0jZm3+YL|7lVN&|3MCP4AQ~EOHt^~q=-)k%4O47^u=25^A>?y~S6J~BN)Z=? z1kz2cc}fS41Cf)(pXl55O@;4ZYVfDjUg|_$uIDf46)OK7bP2tBHa?zQX1qZ$I!ItTS^fe(NdV_3Cd#>k1 z0G)x#LYlrxmNl{y({^>lzT%%UP(*kdyX*#!iZr|HK*gHfy4*9Ld;J}BdTZ}u^PA_; zxeZ+lEN-6fT+kXNgTExXnx#d`;bskSYxE~5BGv}bd%*8CqfGwAq#GZ_ArX&?vA%J> z6KwZcbaAMo-{Qu(jzJ5WaUkE#QN&HH65r4$=j~JkyAx=JIp!k=Ne8RJ5gW`j?W#m} zbg@LnC3*Hd*1!QKRcdj27PZ#vdJD+% zu-B@ooTSf&|J0DnkUS-sB8S62A2ir7kX02t7cBLt4I7V>E5s$EYJs`OT6hll)IMN# z$BBxu`QSW==*{!!+}lKd{|CqoT{gXSu5-}RD4iqdwrI#aG&MDuBF2>4NJn@!*k&Pq zrtw+kh-RA6mJrY3IhHpcqrt)V5IksOXTRm`^BsexaBpTLN~N)Bi6CyKhL7oZ6muEa zDh`emkph+v!1wS#`Z@LyklzWoau5=5!W@x;L+;{NP&*Ex$|n0;ikB%Hbm_;hDBY>9 z0rT5d=NY9mo&+yd%+r{h~T*ZbxbCJhMvYVwoTbYXq(63)GI==>34VBo{M=ru}S zb80`Jh$*;&r6uLVEs?|x>{sBI$txQwNGp6ShVU4!N~ozVreJLlR5EXY+U`Y7yu z!T0y4%L${x(!QVh2HX?!YUIO}V|~SU>HmN1ok7X*hQ;bS^NveF02N$2^PM zm-kXpH<42sHsBCW47X^&5U7Xi^Qd1xH`0r?EpT5EA>_zqg7LI6*aIf9LjQxZORta)%de;qI1YK?)pkn~)>|7FEAlGYEU?U8s z7JTU;;N7PHw<)d5BfiWo8iC&;L9jgy4vj{H&}O=oD;GY_lTE)+QrsE(303`TY(c<% zbY|YXV`K}bm&$zR;BE0G5jE8^$0A#Q{egEJ9Af0Yq-803@}h`Q4uPpzNUUJT-`BXiAiVip#!>Tb>n=|BS#oHD%)5~$tXuE%G4h{UOq?f9doX8 zo)rk;1qfR#Z=Y)$Gy}Uuy~3M|fTJMiv?LmXFdTzv*f8l2mJ3G+N|r0mykH~^mLy1U zdN>d>k{B`n%+n9=`q%5$)VCM)+l~;o-=m&dzZU{d=XM}@*6D+ns19P;!%}LS{)>P& zqR??lwf7UK^q%m~V1Arnk>gQQ!d0Z;CemUpItOrg1i9Y~G-O(Kn4 zj;JE41T;hc3n(07YAqv6CPdFDEIloA88$cyoPziU@MSMG>k|Y z)5USNn$073-wSo1;w-=y&$9ya2A-?G|JE15zfn;SzNV>w!+7lXyHcYm^4YgvKfru| zd&3O1LBEt7Lt4be2{_Gk5;I1LUmHvYhrDb1)tb+nf3diIscXRG-uWNA&PbkY>sw@b z3(ST^sprSLW^`{u3?a6rzz4TOSKW!9M$Hn~k4w*=v2mUxguOx_8YPvGfMW6Wva@AD zU_2TTLqsZuJajZ#wqvcPenl1NskSsC-Qq^4g1Q;D(DxhaW$OLHGS@rk;_Ju)ojf`G zX$!B;fBMy#j>}j(;;CKM+0-CamKRM(5F^M+-*}3z5~EG%@C>H}!hK?lh`L8jvu>dR zFSGdevxp%eH)z3Ps)c+EJ9XHCK0pKwSuE^h0?jT;u+4T*YK<1%cy&2h3(lfhs z#K-ja;qzw0=k)hwL^CBtwr8kcbVLjAZZXssA484H46g)ukC6cU7G1c8+7^_#x$jQ~D}dN6T}bqC%T(eJhJzLqJrVG|%z@Ocu0VKNau z_aWEf{w78s>j2GH2tL?1pnhlrQ3BLR5iF@1YKH=@4J;%@z&Yb#u-u1NF-A(vXz#Dn zi1&71fWgr@`lE-BeKoPGyY4IUsTSw(HJzIKVaMtwk) zqZKF-$`f6q-rUO*muI153#v8>jEsesH3e)NM^^6d z*1e0r|BiZ$YqU#en*X+&uzn5WuD4h{w4VBC{d)A_`|~##inJS++(%uFlZW@~bl%^i zcfb3NQhg6}hqYB$p9qB-qX*8WfUUAs2v74b<1bDxnhW{=YB6n61SR)Q-J6K4-fqQq~ zK6GXQt7yTqU{=x7y%^8`<2t7Ig5kRVkL!q^7U+lmKdwV37f|p2$LnzI059qq;9IZ& zK*Z4f4L)q?@$M!>`@VhDKk#KVCjav~BPRdzIwL0k^Ex9Yf4dF`SHf}dVFBx5;?S>AgNEw|;QEgZ z*DD=ZULrYc{6$OXh-Hj&1JcV+Sdgm;7+B zw`WL*r*|;1Ga!?=D+mY>cmxDM160`3rN-}XhVTFM_wT#HvYdn6*!TT216aG^yB=^4 zy(4;m=~5~T^yV-CT}bBHa4Z&%tlQAN8@N{jZ2I4OCH}HVjg^0V!^F5H>KB_P#;s5j zCr0`v)J!e-SBMFW&l%~P_{&57L-WLiS5x|yZ81Bzb;D~%4_6|s{*yC0=C9vD%j@>1sVXG<*>JEw-hUg7pE+g1Er$Zs0fYLyR3U;6gaYb;dBU4=zp7;*Y?i(CeVE#svW4 zwNi2|eyC$uQc4`L*d$q+i9snYDlHuz+Ogwp@*W1We&W(40B3W{)MZg|Xb(W3FQ1>D zU%4_q(E6p_b+!91)lVcG8ewCVqcp%h)6RZBW4fE08fjQ@Nv{B`h*1QwLk7qfL{fHP z-q5E55s85vus#W$)*MG@I~Np`=YsSi7;N7Q!@4>99zJP4IhpRHK)+Q#<+Udmgu*RNi>c<+`KYgSO7$wEK4 zxaVCM-p6p@^-&Xm!w+IDL-2^hB6^vBELy_VuMtT5bfz+QfXK!wszNh}3ZSyZqpRw60c<)Gv7U`RdxQMJ8lk`G--(v@< zjg+WiRK^<&cWuPY$Kc(fpT{%r0=zb`G7q#uh6QvnJtBY?*#CwC7(E&VW@Lh|^Gn(+ z^AJxTg(o%icZ5(awS`6rnJc{luXVbo=P^Bf)JbAz^5_46Bfvu0KwS#QhrT#FVU34H z46um*Bice3r$x*Vg}{DJM6{{cm5%~l5B!w~C`U7KnogLk! zgR9mJKE66>xx^h!=3YoNK7j`s*BU{~mYDuOau^E@9nn}&C>ncN z|K$F-ND!H%SkVEYF=!mHPuVu((mirc(@x`69D-|%74=Hv3l#nG(UAx-!3v_p8-^fq znnzOhQS>}SMt`WGnhmjIn=yo>=?eKpIh|j4V0oqK3i&Bu16hVW0oz|-PQok^9afv7 zaY+VE;R8W)Ylb&Mg0W(1OYO4_L(i)xC5(pHJHEq@BiS`&lF88d}IUaIWztWE)T*1lVZ0%Yg1);Jt9iD*!@+gKgjodP;`M0^tZ zFo(aTUZuWSrn}cjW?;XHWm5t)GGjnZcs$a7r;!8psC1Vdwgni%ae_)pM}_JIDm{3N zh@jve=+;CZqlg4r(vGi1OK91aOtiybmYbG?+T&L*xlvub(yQZxB){ z0$m!>2H-XHdfuRQ0dn;hcx@0kJgkPBf?j0&O|<7M`g*T^ukFx0zm?jya%Em+#K9M2 zAc2|P*!bri>2XibJ#KaDHledGg6n!Spahv*mN!43jlo{+lWa*1)Kl_i7SpYkn(~8olR+iM zSr(|88FP8oE}YAghWZqa{oHXbPw45->!9ugz0fvn@;mx{C^#nQUxxR%K0^05vR#1Y zkWqo?abXv2n&(Q|=cA_6&*-lEVcc&|tjHivflyt^7=AX|YmZ|E4df-3kQ36cjoF5s|ScjEbR7*7kA5j0=VgWCUXv-9U{j_TNfjyz zj5Io5B@nD{CU(D35=j((Y4p6T6UG;qaHl0~Dju8<2=Q!la^y*qs2v3|wr(`X^l{FHSE2FhJ2G3Bk(vG+ATOF#$FYGmy9 zjevg4R*0YNjrW3a6RcaHS@gZah-Wa5Cd29h+Cey+hVZ$InoCW()Y!;;*7)p0)B?TH z9$=jTu4cH`a;T}`dvU#>GU|X!JTM20^%S-)WG-*Dp;jCrCh9j`J&gLa-#%r1M4dnU zLy!J~PVdCAZ@kjhj_;$T3Sdi4Y7lZRot0!Y*VUAT=_aK8|+BM*UIp!bkS8mWb&7Usj#xO%EDfUarXGnfNm{`eGi z(zrxW_CyZQ#T32+Br%v8n8TqqzDDp4QZsd|S9EyB0&r&Mvwop_GmUu4LT#N9SbqmR z0|Rp}o&>mL4~+2loAgf#O3$2GclzBM;6E_J9n=Hrs{Te_PxrAsge~&^n0o&2rw;)H z107&ZH38q-ZuH(TeKK-yWBKIi$lysq>8od|AdCUr>l;tunG@y3L@@!gN0(z_yP^TV_Y!}&e%&vEP-Hnc;A@&Q!y;r?NyUcXl=Okcl_E*X4w z7*uu(ZmxF=?V!TG#dQKCqmcO)d_P$I+(ozp%ur8+*at89x*{S0Z9{-IXh77CGx(!r}@ZUP>HJxPp84pVc#^;CvC4M_8qlQtQL%!~dFH+D*^>JlTfDXuA#qKw6B(MwdX0@aPhH9s|A_*Anpx z3Ai~>CfwDImCC3qBWh;&iKJg8B+sco(q4lWj*MV|<{1VOSi0iU#v)1zs2r zVPRwlMl!LP$%k4hNp}gsiXBN?C_I4&4+MK<_z7Nk+(S_`Y*gd?$29m4HFu-O-KZJA zrdD=S&fU!ZX8pbh!Zl&4Ml%)vOVEEE5rG=0p$K?N46%fH0vOGK=LG=-IN$PQ+dv)J z5Yy2jWQ?j{dj?}>=E$Knf`i1zD=UvzlK-MC>+901?aSTP_Tm{4X$Te3E)(K zJVQU}NKJKw&?Alg+TFXM$bEG?`eQq~iW&|do=oQHhO@~;oo@I9c}O?Bf3o3T1t5QJ zz&prN=(#Y=HEiQ(?%~%gH~oSiDWb0rQTnJp<~uMhN6bBeema4Ey@AqU=?wozFd#r; z{tWjqp}Um1u(<~&5EQwfw2y0fcs~xf8`(hpfd>nTmHHKrsmq_^4d*{6&O;sT529`R zhM|}ACOX3F(b;of(RYTLAmEDUsIdJvfCj}5gHwXH;4DEqIHylvp)&Np>VKsgE}tP% zP7{o!1mm>9jx)v2?l-bvf$!N&n3(#JITydNz zwRA0JEx-Wuh7yb+)qxkI;|}nG2%#?Rr*c-K`FJ#lo)1#*M+a9^J%-UCmshZuKu+T3b3Tg&&eNQ5Ia9s48^UdqW{zkFeQWbgFZi6tS?VxE~GDK$gvS6biJzlP@P zG=S}6_B#+?K=NmtpCd-V5{*I|#e^u&2qmODe1H%dkfG2`lGmN%+KS6NV|aGaoi*j{ zF^q5jO%1j-jv}d(AWrW2jTO&>>m>7av9l{XG(U$>OguDKtDSwI zrugOAnVGY9Cij25rLbW0C-b5g4fIsSm`D>`-26Rw=2oKU%2b}Go0EmDzdBtZu2Gt1 zum6&Uf9ms(Sjp^*0Dt!1;aA#QfsYAd%EO`vB_3t-5yNO32rg`*3-9ni79ZZSISxSY zG-buiCt5d5&Y(zO<}U8-R(_VC(Hb5UtU$SdwzYQmsdjV>`f(ekIb!< z_-AR8JoB@Wxht&Vs2mF%LW8_aG)*}n!MUxmb!#G7mj2NNtxIX#W<9^paRA=fGC-L} zc*9}S`?-=qkoUsynmCG72C)Da7)t@J;L$dW5-nwzXG+6$n4>srl=E2}wjEDhk(4qi z)&n8;n2F1(B3v$87ta{VlIyCAyawE2(n=I{J7$$?+??b&?dvMS3lhWmK1CHZ(xzjB z>6!E2nw&b=Rh{SqTb%gvLez)F>7 z8q)acu*f?;Ou!XI_|JEfcsa7YoBG#o-lommx@uO1!d|TO^i=vdQ`ZHE}n|lIlt{ zDKlTKZ8^In3vF!Q*Q`jIxxYSPN?xR&BeUL0nN}7yw6kM?{3UWiu)Vu7E5u7B@wOx_ zSsz&^)Xqt78mbDAPa1+`7r>-p#&!T~ve6D0B$I}D%n-nm6%1a2O@MF`ODmsHj=>J& z-N1Dyoqd?U`f_XR!r5*4p|Zl3s9+C~Jv%hNEhcVKvX8R3C0rEbTbe1GIY-@AqH+yX zxP;0oQUcQZ57vhFwA5+?)Fmy7)PZ^py4Q7TAlW5UTiF?(*jp8ERpq3LpCHYi9OW#? zU)x!;AWyWOpE{$mbFrDEEQjY}Nf2MNB&jvwwQFia?EPYjS`sSIBmcFKf6UUX|ob$1*Xppp$Aw0B$^D&mJF`kI?NYI_T%F^x;oD;E~| zx!PEIdV5&~Y@2CfYwZ=`?5>aqIB83oVp-FwbCH!eC0vu{Z*AkBu9wYxvqPbrc4k5G z!G(FnD~{?mKRr}YweKH`G8c8%`k=7V>B)tCS)$nL7HM>yJAnvva1Zf?sTtQx;p5)0 z@;I%-v!8#=sbyaR`;>vHLC|O*;HoXI*hC>)QJ5_qtQk}nvzKWQ0AwpSUQTcvZ5`|h z9L⪻^aYFO!99beHFX-Akj_(lfyO_-YO zV@o>9a{G2SO?_onfr3X4#Vj4#x$pJTRe>Ad|6_6V(9rU!aZ^tGxm2@e^X3JUUDuRc zJp1PQHusWqM|bunr}XW7^W=D#FxAJr7-6svW4Z=&{h?8yh4kDwULAl5d7}Y^P9Xe} z`EmK?_nRlbyQM0&x>@4q?CogIPj1gIZjEvBjm`0+9{4x4PAQNvZM}tp(BKfhxyb63 zxnY;gosFyqBE< zBGU?jpsUo{+<|Y;aWz36!sOdUXq=c{V%ka=K!<8=DK#xU5dk%28IIhv@@lb9Wm%@ZT}tW1;PB3=l}Ucgj^;xH<>9V_aeW7y zs<%vya8i}UZ}(4;YPHf7|DEmgJY&k#FZ-ng;r9VM+ItA(pIq70rd)Yrb$PhJ!YZI- zoLNv9YUdc7p;BdsIXj0G`}l(%i^EQ z*B-Cvf3Uq+I$=uRK#ZuOYyxlnP{fox84(zADnRC!Tt2BW1PPEUVYTkGPOO}=YfA6I zX88w0D=tn8D|zX&fyx~NlM=moUjDYICoRw1Lm9vg@!l9*qfu3+tHpM2U%>J}Fg610 zdhjQ9jJ&_X9z_^qHzo`_8w@J=K9H4+w= z=DNl&n%$BYOq9>rTEq{Tyt`xB8}%aZ$>+A5{&O|3v|`!WS;U>j4b?&BwhpFN4mR#$ zUWg<&+DGhYE3lVlMJTH)tCgw@sl7*3*^JF#Tjo4xaQMKVa)Zo+=fjR1EDX!WAo#dM zVi0`5l>QaAt{~~y=qO)^i;yjcFHldYni4LF*F=fu`KG32NrdTXiEAp2ag-3m z`o6nEU|O9jAvefI7#0^F>5v`PGNoD@fLy||8W-fMrc`D+J86rmB%Nm$YRTm*KH1w> zo#{hZ@D+huH90cJ#T;qgtV3TT!S6>V`lab&sHtMDzgFv?>Wfyz=+gYMj!`!rww+kt zHbH7K;F3ON`LVB;^nQMBOLL?krXnh`GFn*oK6Mq)9kr{lLi-mRhIJY5&VZAK(QqM% z4fz9n0GXK~MEH0l4i78sO|<8_*jbq}1Hc#LwI#Vj@RmO*WZUM@s`MafdQE*zy0jy_ zqGv`=MR-J6t2}eCMuonbePLcg+>F;-Qd%H6y*!4;6-LhA@IX9>W_gYf?AT%N<|F2l z94q)szvl8uI%vmC<&b=cY~eZ?^W&V=*Lqv7Z7!D=w?+E-$q3}BZOx2qEszl*jS9bf zKVhU4>NR4mF5X&Tt(X`hjE{|Q@(Xh}_s|rnl^IO`hW1`f?&fJJ(+TZ?AJ@diwrwiO z?a2?biuGSwmf0A~Cy+~YV>UCN%Y~sNRZ6K!UNb>$4$tvJ62IJ7ACe^uR=aymNR)UW zCmWu%iM3mRJ1wjCIMdm0;NDKmFYtq72AWB4s6h)VqemBm8UstBGrMe97}sE>T63|bCy6B7az@wua4#iPnfLkYj|mD zxFA8d_I!^=VnRGWeb%Hb*LqFc)F~?enu&Fi(iM$Kw(*B_>h}6v7w7D{W<~wcg%gM> z-Ie_VqD`sujy0FQG%d-DCk~$=?{)}-q))50m&wB%y5-psK0MQ;-rbcePPRxkc@DgG ztF`l|hv!%1ve|4qzQ`_HQ=oK#LqD_~%Mwx(oRr0}vBgTK$fe%^jZ7gvoDTYI@XLnm z%&51715k`MR=Dmt=A%Kz>0dh-qhau2tm%@h#_99p6Bo{Gnh?aX_jZR(KrK+Ii?UF& zaJ^qvSH^_a6i<)f2zRF-Z%09VeUgh(;^yS1^az=huOz%1U!7YX;pP#M-#9y!+>lf( zvvu(Fci8!1U{-|Z;e*4wJGWN(dDR_RkvchD&a?9iOOS-rWy|g{EnOWQe7r^W9x1hh z1{>T5_f{;aF6+~nyP12j?c6|8i179U9jzP6#nS3Eb*Y(IIjlSU02d2W z=KxQEAD?Gw?I!RUzRywRHYfv&^R#X@J|QvwNo6V?PgRzruJ!V6-*%<9?C{osDutqE z!OJJ>I*)ExkYZ+H&$}NPZDKOTE@#S&>23b||9B}qZNopdC$3r0B9L^d!iu570x}Q_ zG^+%f8TDi|Lm$h7u-7tZfKhEYIHVLCe+MQ4zC+02gb12GwAL6lbus3@tjhQ$AFoTA z-`kKLAgfwfh}^|4=AqdWWtoi$A}(R!CiM)I;d9okEuNTLaR_AU6yWLV>0r*Z69;R= z(h1=L^ok2X`1@G7J8=niV04Nww#MByK$R>_>#tT3R@eXuG{vn8v$9vWr;b z{muerd&eHAhD_ek+;Om7{g5JZTjJfU+yXTj(u(P+?)|}&_f9M6N^)a6_zHQMm6N1U z{V6Kh_Q{-#HS?OYgKR|6(!eOOZRE7qJF7Nz=Q>+PR4*tk-_#l%)&4S+4GsSBK6^hS z5#AWk#rd?c24J)wTtl#HsQKWKjjs9dxSl>e|EW_e_grPAfaIYpJ45(ll%3gvon4q5?7+A}9Hz`K6}%rTI*XY{&?Z=nl;- z-8|S*n&Qd}QYV*nrK`*1yek_wx~YP_O+6;fS-W8<;nnY!MRv5y3p9eTd5?^|y9af< zm{HzA!2@iigRy=CuAXaDbNp*Cj3Ck@Je4u;6>hxI*Zj`9LUrB3Y*7G@GjDC`w0x;^ zdQn9{^^rxn!i-rJrjhY;scRf}nb$s+f)o zzs6kR9Y#F7i9#cm)`1I49(-d_LQDr>BdBw|>E?q?y8(H zs_TNYvT(+($qk2>Ptl6zNZT*2?c7%FFUzi%>AjdgFF)4kZ^M_U6DPzg{H-zE*#DNW zk29*^jlr!F$}ni>1et+kUJj~Z^sHF0UWh92$DlW~n*|Ro0-!2t{YKod?%m%9BiRD-Tn-FvIH?O~{^^9~hi9QSO!NxjsBk`yW=c+EUtKg2+;m{DGSa>J;J;QSFHSCc^T6g9!e4QjBuKVa7vvlh>>~la;hcGt6#l9x?MX;)jOUBfTT(S`g>qI)!t@FiPm&lEk`!pKn6$c_4Awdah9aT_>q>>S zN3f8x^-75lWv;omaNT#?bG3`F%+6YQbdpHcdSp)5j=I2@_N@l|3^jxs5Cbj;`WqaM z5n?!acSNqnRwBmRGQfzAZxtY}Mt^NE=$IHTdeU#8<1_gW)>l+9!)b@ESs6-GI zT%|_i=qj}3aJYP)OH7oUOPEtfNWbU|U;hbFKFZS08PnyeS<_n!Wzn@g5kX#3cMD1S zM0IRiUWfoJjBALPM=&#%C4rpt`H|((n#~0~H!FKtvm&7om)`o7oQg(D~|IiNJ*Qc z@`)FOYjU!CY~AckO>B5JJSZ|)3j9?mQF5t+4c|PdM!Kt4kN{l(1jB^31Mk3kJ1$B7 zMP7KU5lJ#uaA;XGgaQsAY;s3-Lca=9I}0E|_V7q-&c?NKf~+su_O^1fxQg(@ z+rQtSov{4!G$!Hc>l&bNt*XyyjCPKkvc8zQ<|OlXBaU3Od&KmVs$2+XMd6$SVADFE zUu3_;zQ7owZ5j^aAf3hFjYf5_QwU^*quJ$1G(cC#0e4&?fz+8!9ib}=YOgqiU>jCJ zMuQ+jEC^FGu;UuITYQZjpJcxzE9i`?+B~yH6x&u`8j!!NUhUs-ctBB;>IVmFdaL5y zB~ds@t7XDJGfh)?r#_j9#Cyr5+1aY4Am1rdI!4YAp;Zl2}s72sQ1 zG$GQPD-0H>q7c%Azbg%lEUxiU*QENVthl$#!qLLQg)fq*JbhxNF7}c%MNVvq+67fb z)ocA)K14omnz?#Q`DEObHTkO9Q&XhbO_5A@nU5n1OezeJ)uacJEJ4lmY`JU9;LPTn zU|YT~&sF5=XnCEVmRsl-QJ)@I_UdozEiEkgK2l9;AU`tL!&B^}b{UB3j*rU=vGtiS zy&z|PTa|xcV?=m&b5)9;l|be#Q3)&%+bgxa7ZiOY-u#f!JjxeC%mQi1Sa5<6aYp9CWCT^_@pcC=(AXjRxevU*`S7b?>(JT;Es}ls_$vV{T^8H4Dn= zNKBfX=;_~5SMV2hIAle(f-Tzj5SfK9EJWW}0n0>98xs#95x2!MCLzvo?ubq_W zYtJIAJgpuE5GHQ&K!w_cu<)?>9QLy}vlB{%B0oDOLGaYInP!f@d|PK_X`FX>kjToy zLmDfAzwQ=}am6j8HjxzoHs6vl1*kPrk&Yr+^rG=NtU=jx2%VR{|Dv77HU1&nFI&XTmuamNL@Xao?{~B^TocYS0+^*sj5mmL}?OK7iDwZFd zCU-~HzShVr5!nhP?rutdS7a@+`a=WV4e`S2#gnsrlx|InVa*KJpa!nl?){XTry&kH=W`-%L-Z&T-~LSF;Pw-O(o&TL|Qy2+dnfk zK~S~4bCOC`+p@62Js~|OAa7o&)Hk($`tppdl{4y7#pLX($aHg5%vS36_a?ozvL)RI z5k>oc-B@w!%0V^P))f5{@fGP(X}a9)JHphCt2uP}!;;M1do@TQ~dOExrRW-2zf78gx>gRN~9$I;yME{FGuhYnx&&-<{9p3Y~3ywG*Qj+}P5R z5p3;K+r3b`>GoVzSnJ{GTTlA5Tg7DNc*M5mg@jZL7UXX1PR4x`BRIdqz%4r?&IJdZ zv~PzwKlp3RRC_D5TaU7{MY!t8jK zOoU`b-Era7$-a(JO$DSNM~GIYO^)|)mPCufdZJ@yssptO4@LRRWYkWr@K2VxM}(?Z zPLPzB6!?VXYJ_uVBvwVC-#*&Wq*OEv)vBwr6Znl5-hL8Ca|bh1Ta&bErGsZeePZL% zd>@kFifw)i_KJq}fG8hpM=L7_`;h$DqK*XDAscT^peD%A&6@9I?H1RZ+x4c(IjS13 z|Mq|%&IG-;h8|_?iH$Wo+7jB0t!T@T*sxb}5NNZvNbci}M*&AXvJCrUF>Hs9gB-GZ z@tN*0QOe|mfGmv{(bTM&lolZCKC>9zt8M<}$@KS@XQEy-?>zPS>59D7?{>^N+97Xo zP&w!tnwgH_JR;LIqC7sjNa4&ChQbLTnrQpbyqJvIaGw5SA@#@GZw6N^F8SaFc@;_P zHaC?lE)NzL%&(??G2xkfC%#IWyFv#M=;&={LX741*0px4vLWW&NWZNtF( zDxg|wSW|%&2b{#h=opp}rI8Aw`8*+Gi!7yO6T~GMVZrH@!SY-+f4FDrr8R|d(_e43 zi_OgOLT`B{CB+K{PES)1WWtOiZLtH1p}JjdsMb5k*$lxlZ&+xyiXWI(t0JEb++307 z9yeuz{w6GSIfZqeq)^({U-leB)~^>bo#df{RF}S>P#&}4I5oWaz#rSvUEP;f%t#Zg zcMXd1A)jPUNff9D?{8ahsxu7p61IV2FM@qZM)&OUqIW5y_a9M??ujLW-AZ7O66kwm zv6V1tC1M@-tEWCXf`V#o^d-yO>cz8>%>kQ%fwxgSa!}85Sx;rZOEujK?(rW&ACRKm z^5ORaohA@(gv_Trk6QYY5$JqMjQ+FGWa@4GiIv?gbi$3Dz_0SWIWIx*vHdLjC&OE?!%C~(w*B3C3J*9^R%G;k+=bM=4bD4Zs%GT zX`+t5R^XZ97pjcJQ&Z5hO?`83Xf+DZ?<4F{`Z`wK1Z3f5V&Y7$GiRBUdJENlPNwFD zo^9wBee^y00A0hjULJ;;6!%llqsQX{7Q0Q_wJ+qtg}IhiNR)$c6K&4!HJ1MlHi((P=N6=tDID`>!Euu zA7ErNT*hGJoiTG6ff3-tV7SoWxxoC75j<;v-47mZ}RT%Y*dKSiZbqx;oh9yofp0{^!u&e9Nop3vVQ~Ufm+aKmOKP9ZnqtH{iu`kduf@iD;Lfid)bITHPQllH^o2;PsWUx5RMze$)?wY;mKU? z(duYxMVj@xm~6JUKoW*!q_tfj$69P2$S`$w2;_3auDoC`AkGh=k3w%>eDs6OBlzqh>o$hsIIRlRFjLw)K3s)1Kb+4sKFQe6g%O;6lXUolYOjCvxE0QB71Cec@m5qfM zIoSugI0o}+?q3A%19sFSt{V70il7m_qc$E=kL@1h7;bzTH$Kex7;ziL#SB|QI`E(s{ad{$p`vd2HAf5jrhSh*n}%4OVq=_*L4Gaks%YlM+Sl5R&k77)`0)=89Fb|)q z4b-ys^a+s@EOu{@tJ!8z*cf4AY=V(#ubFSJu$5(J%Uf>UO6$Pz6Rh>Y|C2bQ#Ct(T z$ou*`f2T{pkH7v+X8t2hu3UUkiVX6T1$*j00D3?#IzA5+Wb!ZO2>cqH&^@}_1YIG* z$FIQV#PMw#V7Sewc%_Tt5h|f}8+VsjwsPA%I8}IF(5NOh0#_{UGpRX^#vLUFm%c*m zYy_RGgE=@`);DyYqESk)KgGMBLs-kk@eGtp@q#7Ps0`b=gPr7!oqcCer11^(G=y+9 zJ*x@BX~w~MNf1oV$H&9y{Kc9VX6xPE&Hgx2uqir5^EVt8H>A9p7(hPxmL9A3^@q9o zlq49e4-EX{h0%Kw74Cn@&o7{=rR_xw+a@k-o>e3P^8+ys|9PH?BPShZZiRTJ&fCyC ziU9?}M*)r)V`vJ4#wrL@0em}Cf?Q+V#EZH+ZWAx+Ic(zfCpkRK9A^F>dwHQn_yTem zW{7LRzE?uu`TryDyW^uQw!dedvYU{Qx=RQUNFfP5q!Cgep@rUi4-i^HNrV6~AP{<& zCLIwmA~hl#B#ZUiqaIlHuRz*BFR4ce$Uz65D@OYzxVy;{k(7BoSA)|d8V8* zbLPyM=DgQKl?k-RHr5=1(-X1h)-0bVl#>TLy%iAAz1i3IiL@aZ=Ep=<5Y{Zca7XDV>%k&TIJW{*BHn5 z8~co7t=q^SWdB)GU*1{^z|Nn8Mlp!JuW3^W0r7F?&hk~9x;|XKlAK~}#hwY&HFI^8 z1Fgte4l_2s6P@646{bsJouyIQT90R{rc6{R`2IX@vAt`;jN@FW$9Dpu`cV98 z2gHxjSGXfOj#}aqe9W~IQDGAYUFjsGQG|Vjs}LSF0X4t`bs2{XJ3e6%X}sF-8J{-& zrWVwf+ILnZ*NU?V8vk;&?NVbS-?*bEV#@|yz~e~UG#GJVPs+oxIU>Wh5Hk*At5~oJ z$}iik@{V<<%};5u%VNA@J2S;quJXH@vJ}TXO;j%JQ^Z}lV%$`#9DFDG{I2Y3T+ogS zR_g26(9HC38~8|`uurC()TrdnJe{}7H)J#A3%TBU_C9D;Vmbb$EqJx73Gri}sk6+|!AwWg^(^QAMAS2tjekF9RXnZdji2&uiC>&&{yFxg zJ;--#NmB&mfGG%d(u2?8k3kKD^_ePW0D6uHrJAJcdr?zOIQ&mj&ipP@($Y$Pzh}G) z=gaj!LMo*1n4c$o=opS>t;34u zg2k&1m!Q2BRC5NdL30U*zuo*iBHZ{8SEOZsYjIwM#KBc2q2d6{Hs4x_U%P<(Wgv_TvNrfyXE)_CpZTuaz1^BhIoI?u8Ha3q3pA+CyY?_mg!5KI)JC{H$=N#5Hn<<4UcwKZgnFrEud0QI zEylKGbuxzEnDnv5w!_iRe*gS*c+J1t3UOcC?&Lhjg>3^J_q8qV?mTkvk$c*3&Y&I) z`qag}Fa80Jw%CLf1)>oKR4P`;qboI=_}&-lhC(BfTTZI6>BNCgt62x`e)GLWA>lIN z?wEhgX_i}g?V+v5t6DRU?>n)uRz37XKNbA`pD%ZzF>eOAa|txSL@^HQ^+$7oOhUOq zfqRw{e;Dl>7PQtIwJvCAwEyFTrTplxQcvgj>jvuaJ$#Q&mk0dyLYxRwdYq-DUF~hlbD%RQMiC(A-Yrd&ssH`;VpRyQ!mOV-xqBR#v~DmA2Wdbhz57 z&a-Ld_waD9tccdICpFXBV4oFs(1x36t3O&a8Z?wzYAw=2y@`Kx93S#vQoft_X>tGN z9pVIe$`q74*V7 z96PK1(Smn} zH1~}*-ha1XZD??bwE6@$S-R})`j#3o%aOp8)}C%?sS*Fe)S_6Ky6eD(?2T=@;K=aksK&+}FVAU7Y2W^HY>$l8$b zNtC+|()`d>ZsIld%ns?v38TYN6P&^l!of)a?E~WbIW z@8I~Q%U$I*S^m6Tgkq0=zC-T^vdPd)rEXT~g|S^w@{u>MjqEbGS?+lk?K4pB8l>&J zsEqyRu}IJ<$pvhM793QKoIpsawPgb>Gm$eh>(7ZCNhBNu#dF+1nh z{G*Aogy3#4edHQt44KM*J^xvChY1Po?~OrlA znD9oc+0FGptAC>!k+30UmnjDh4{w^XN;R_Qy)j^5)j%q5JDgE>7Ud15lMy5ilScK9 zh;(R32~AS@9B1@W5men1nwTM$agGVDV&ck|Dz&p08m)Qh#@LeX4KmcpGqw*N98shF z(Y)gRO--6to8K?qy}3e;duR3PCf=GB7(EO1&w>rPcYdBUBO>C)n2bRKPOr!uI<@1N zX6?`%HcGiDl$t^JX3gvMJb!X5@;4fptgGLGofg^_A3DbEz)Nz)pw>rfh-~HFEviv< ztLol$NOeo)eHLTmo5faa!q08gI<#7_heuS)kicM%QauZs`l+rNi;P5NAma7qedOis z%T?pJ8(_tdXyIyWa*A7bBx6x3b#5G0!xFyFB1e4{{N3($)0@^Gss@Jk7&xKYV@Vo) zvrBXNAiqFs^N@xGvmuvzLB|^jKKnW4s5H}INoeMT%*qZKxxr#wF|P0KS|Y2xZjob8 zE4~{nvc826%T;PEFvg%2JFejbQ7Arz&a%N-s-z23EHZ>>;P@Ik@GkUNl2)IX-NEaq zij-s$x+)to;?hP;DYuMEV3BeXL|dEPAqsOY*NW=FTXH{?`W|_ zj;Y=DVCMAYYQMaxW15d_+0%9=OIgvSY4h4`jvlZTD=&0x(mbp+dDY`%-_FV@aO6qb zfIMML$fGDuj#>Oz#-4X1(5_@$aax4$#2psnVuxHIS@tZ_z#-D05RG+7eg!*4NPtVk;E)`;hfpY9Yfd+S+%i6c6BIv- zq*}D>E&nmGlXtYSJN<{R$8^s~&HMiAG2Mox<&9vmX2_TgSrBiy_0o}MKeS~!bHR^g zc)74D7hjPs(pC`bs0Ej;-VSN)Up;o&(xYohqz98oAKcu>DbgpGDDB2RBC!u;lVmFw z=qR>=I}rM4r;tA1Y#7eX`e8E;pizo{#g>eT8FgTrUcDxkx7^?V*39_rb*r)NI65t7 z^L=i2_z+o?**K~~i>>)!Lzcdr!(fZs8(}TQJ?PuEbT|{@U2-Zyj??H2Su_!8_^^(MSTRHAmjD-IFQwXo z5ArS2|KOy~+Y+C(O8@bTry<8>M>A(iw>#NJ=C|iK4(x0&6(bHvXk6LT#NSLhA->1( z6G|t3LFLr;&Z{|L+$O8^u*y-xR&3cY-Wa-i_B5;X%aL!){{DF^NJ*FD#u>}ru)pBA z*;E-{_BWO92G)_sQHUR%WGn`@JjJ&lAci4l*qA4EU55pmFMN{WiiIlDQBXiwlsQF72#+c;vM{eyvbqJ}oEGeq$V zY*D-BVcsC?$}$V2s`etm3}RCkXmH4|UR->EF_*GKtXd-#*;j%vRHo^*n~>kSEn zHVFGKnGR{nO zHa^N%!j$CwrQ@_)h}%TtCKI-VAm-|@Bi6x5jP1Y~$Dth9??@|nux?4iIEpR6xD4Fo zQR(y>wl12?w3va!g6fpVkyXr)z8NP@sCm*?`n`Sh%R9#F#wKIVo}!mtpR?jb$Ds{X<;yO3ZPK}e1mhgI=@|%YE4Om>eW-`%E5GL$I3_J zT!WgzF{0iH^tGl~wtdtVF=z}_-RXt~8miDiM!sxwB=W)P>#O=&@mS)DZl6>v62Q?< z0af(b4gYcCyDNXc+Uxn`gG#lX#_Rh*r1tWNoV86h*>W@~sjQd#^ukKrQx6|*rinAJ z<06T@Fh;%;5g|8Dv%S-$liVuD-~#ilFbc0US{S3v{#9k3G1lvCIa%f(^NLK_ZCo(k z-<{;7t(pNEc(8OSKUk)`Q zvf41&MpMpZYGMPpKrvL{gAI*w(W2h#`Z%?8*L_X)U!!Qvof?|S3N_D z6>1kH(P=?hxRV|YZyQT0@*egEq6y_0nwGOh!48Q?9CPL{Ur?U$GaTO7J~;PhjNB#n zD5FoDu!-YFw9SY;u6?s|C60Yn zCypLHar~{fjvEV%UUILTCiCSUqo=g_%Nf&?howdZVjSZg7?qZx)=kbxjjBpNt45_} zq{yI<@PM)n%1?u2OPuW6NwzfJ;>n1&^5tgblH6isqM5&7Wa@2X>8z6=WUU@OGkayU zNN+i`_lREIgYa`i@1bewX&Lz0qh@rs>F-T5W}Q^Pmj$qwbz*xN=vZuJwqYX(;J@lm_3 z3r3mHC+Rm(SG>uhOf`%u37=x+s??Lt7^4^I=N1^5XN@R`oR4p#5ihx2+R)Vqxz478 z_a7jhK)!Xz(fs&uw}&SX6Vp-UAX);jlvHZRRdHh9#~raQTZdBmbT$S$51d~bizQ6pLpeQ;fE6@UED8z-B%*J{y!MZea0-CNZO#vn~0`f77P-)gQ~ zz*yos?vx;105XZqmekvm?j3-xUz-#0<9oO);+%|=eiOgS%kg>cnKd(>U7S7J%XYDO zqr54NU>@pp_zko{59ucB)T?J)Sup3^AHRGwYnsxldV}0cpH8e_eJ);MHqgoAF`$)unAP0N39 z9TfX})u3k-EjOrYV5>=2KAYUynNN+*>&1#dSR3mhpIrBXq#oj=Qf#@l6fEeH=ePu@ zND@a}N2u{#h79bgd|?ZpzIf7dOHqBh_@{B(xB`W%G6+$v?0&289D~acjQ5qjV=i1J{-r_y-2m-s&hUX-E-ue^|pz)wCqnS~+*{$u60{tRqs%G6ffA$aP-BGJwdKsq*fKQ-{z7Kf|WKKEG zzN8#Ex*T+@#3?%MDFn121sfCevxy3F?@am4Y1|Qba*}Q94CBg(`Nqne9Qkp$9Fj)t zDGQXd^Yic7PGRU=(9gK0hnAfRF<{3~%6Z{Vvrx`c$B`zkx(`!QokadVCnrCj;(0Jq z4|VX*tsUA&W6=yZadbobcyd*Gq+ZbvM2vHm(NJ})IOo1g4R`r;oSNyYebHghq$7vc z+ZN1h)^o_1nYL{+oA($BcOezC`L+p#+vVS@SMybJs)WWhvem6YUd8NPJtE2Wd5)T& zHSD8{R14+3vtw4yx1ra4=tEn7VQ`-j)j|{J<{Ooi`fWz1MEYoCkSY6z!`KR$fu*^v&hLah}2Ua;`oNN_!T?&A)^)c;W zhi&-!@1Ye~yr_fgmppQ0GEB}+1%Nv^mttVFg9dk_G(`KUYEZ7N?3o&YfvqRY;z{V< zs2`?-w~@~(SfLz2w>CI*Extf1^l2g;Rh7)u4&A^h&)3;T?0A|O&(E=CJtDD}}FLk6^Z)H)nF?p_8X7b-da6uxJR?k=e-dOl$s$8&nFM9FyV9LGht zE=#4692ROg5Sh=Q*02!~8wP1mmyzrB!t8OA+K*lJ%9=Y5hFqSV>%M5#xQS^!S6iF$lCQ253FnuY7O@F)!NjOb*Ikgn4Xqi zExhiA!o9E63Nh}EpU|ddljhd&_{~cW?yjkBFq@($bgvs58g2>naI0RWQLVa-V(VA( ztkyN9U%iAz;T~S@zLhKMspGN0gUTHZTlr4d$^&Rll`h7`K<_V0R0A5xxJVNCHMI*H zH)16j)>Ah~N^7pHb>DLGi_Nx5?lQ;Z>lYTJ?#m~edA==gN$WlXw#{C2kLyW_rdZ(%YNA- zX3Okc+b{KMqW=wzYYba@4~H;O>gU^5pUK|3M}CoC)`NIlEqcj|@~-W2EADwwZu=Lg zPgjr!FPiBqAHh*Y3UgOtr95ve$Ww;ThQj8#M6M_>7TA80?ME9WC}F-WL)nI*VGoC# zB0BQ8(Cmr$Xncf`8BQJNxMY}zYt)HbsKdCI8}mw_|rZ{ zx;!4lvM(-xqlcu6h|$}W4-3Pj^3kz5a_chV7n!omHfYhg^DB%F>t!|Lqp8ZG*>bn- z;Ao|*QY76l44h}Aqp4Okz5`w+F)$>N%#B5{e*=5{r%02nUr;qG=n1LPS#C#8LST1S zW;R|pDSBGlT(woJ7TxOj7}1lIku#-N*)q9ecG-NnIL_F6CvSKa>vh?=L)#JI8b+SB zB?=UmvaCYYf-=IgqNO9f%EYT!T-QhD2rG7U#QHKZ+bP41_^Il3+ZK6yD%OK-%ZXDO zYPrdyAL5_Gf>Z?VHGk0C&y99Qd*gMvLit%fZ{*ngjNEgz8WbrTUu_V))aYb%R)4G2@KxKW zqK37WVu|5F(DVUldK!7FL@Su+a%DXpH$j1_G};OTg^q;=#& z`y8LV=a`R#K>?El4v9_KtB_YP14|Bz=iZ-!p-xEAws%hAPOvjmN*CpOY+k&+XzzU`-YE9cf<&`Df67!}eb;IgcQ#;-+&LN zxDqDPlX?$hV-c zoA4xCU6?~~=~&&ZeQR5PLFs3!wMc7I7N^#)Q?^(8w6v~v^ga2_Aq*tUv<{ccUj^lF zi2B?@bQFC-1+L0;!)FuPI*A)Wxy`BE(4c9?G~tc9PD>7r(?Al`X}#Po=3nlEt-^vO z9dvkvT5{k`7#$89=Z?OJM!&lx9s{LBw@pI>n7rrJVw@#h9 zbxU5J?edYui;ujyY?<;_(6COu@Z&UA(yk4D$m7N=TV8f7_)3XQ7m){Oq zcVKkHfmFMW4p1_Q6VN4Z!_uy1;>7vr{uJiQns!c zxO#T5UI1?I2iI+D;^U>qkN2ggu^h9XV~t@oYf86KV}F;4s0Baf^frE7Tdd5vSY|V9 zYXWQSmD#fCx(MV~+7@X)8_ThWQx!E1qTg2O){#r^9}uc0;A?zvnwTjR?DfJH1>4!P z&*zy&84FQ|%Fb(sJwg8#>c#)MRjRT z3-R*y^QbO0zY~FG?S$0>RoQB;UInt#Y#ms|JkSjrd~aX6jU9)+e)Jz353wb!>{7~Y z`001!v=|TJAw3lDO5Pscp59*G-rhdmzTSS`{@xaEt9KQT>OS78x8|)o1K^R{VBVzY z4i;4EHLzo`7n2(MgTV*KeqiE5T3u|pmDQALYBjBzUZYYCj~bphQ_s7GPYvH1I41|^ zfmmx*!KrW6Eku(_h|Pmyb1;r}9dm3>>L7%roaK)v<=pj&gXYM}K*JT9<##N6vHjNA zQr9;!PRme6^{(AKAv&OeZ{4V2%IG1j+Ud<}RQ5`2Br~V@)v8w6DBi}u(tft5B0UrKmbz=Y zBFvg%GNbx1!`_Waq&m;l#>|8-1SS^jJSd`{Y`(rur$3x>e*MEEs z@*);jrm*4-Yk6%ui@%+G;lgCf*-I~h4iED+BF$X|A~UunyX`lQZ$H_Oo=`zMbp*dEErLaG!b>+Mh)oH1qoW5Wnn_ zY`-CvZFAIW!=eUat*W#I9!4!x6vB?W=%|ml3#x-LRmspIeImqf zLim`J4GWm2bWR36r&;-Ot05csV44V%2OL{Eaq-ta>s(=R^v5m~$1-vgmOP#f35{wL654V|Xb6_9hT^Xg7RiPnDSI-;C9ym%!8{y{4q=g| zu;BQ(kQkmT@wL!q6EB$?fPgZ!GQibWdJ$#m6)x*~kpD-w5xzdS%D!PknVR^a^VTkD|Hb|bhDIr#*9 z-b3299yY9X+aWKcXJn+e9+m<6$$6%p>ThnbK9Q9Rv}3Y2EFih!ZKWB}7Vn1PT}#J1 zWuW67ba#5U%G6E$j=pn*_c2$VgYTglI^(?SHsE2Lm8WZWluUkC$mvyl_nfxW`EC`` zk<)iOfbNO7~P1n<$Wtd?ap*L_#!gte6b(Gfl zZf5y+c&GK`cl-?Rp3}clVDIEON1zOOj=U&KANg*IsSn=ucfQNPyTS^z?&F}DgI15c zD>U^1|A^L1t4H3=<99dlj^pw%D~Gk+ijS-8+8wuIymRJLIc)NgH|bs39DRtryJ?puN&V$p#+?D=Ns=Zd-B|RI@#E*)lT>wjc&+K@ zLbfhF^P|zVNIA7|{rZKA)~{c5@S~vY;TwQkv`jH>A6vbv^Qy@Wy7tMnR2ySxO~>_q zZu!tr4jF;n9;$`5L%^1d1+XP!TEwe9Nm+SO&wCUCjY$B1DmFf zXa}>)I$>*PDDU)C>pm#c7tqaLB!d=XT-+P+Lw-6IGGPHm2$_WCck+_0eSvntHbz-r z8egD#s*RKexDw44V8iJ!&tq&h%J@V>EyLIha+K!~=swT@x;hl583-NP1&E7llrhfB zKUecjp>#v&(u}1|Hjc0L+Ary@>pn-Ivni7?biB;Qu^}x}H@QuPtfsS#wHU~16(b+&)wHVGuU(9&2T@5p871LgyT{bFP zIcn>ct)vqz8VH`cg6 zwy*3dzg~WBIsQB95^AjWJm?=v>EZ||x{U>6XWWg7gFDHLYK^5KB~~TC24PZL-pVps zO?~!|@%f==rx>lWZf!UylfL*=Hb1zk<)sfA9oq2bnKN&0JlN=iOD)rW*z%>UWc=~f zmLD*}3qZvP!9AoN&=^9}e7xL;sbf8HG^Q_UAo|H=LcJ&8hvv3=*r8%OA@YY@&l}&fS;q|Frz?N8fC|A~DO&Ba#?U+iV9^wq^|^>Fs-97uM)4hs{h-PEv+_)*ElEfjI&F!D zfd`+jzOA}4*kLF4ebRckS|=2(;H_`IdFu^%8(ZCtGx+yA?HhgSepD5`wlGacIjoSj zVXn0etcW@jrDpc&*Nksw7>ACZ$tv0^LuTAryuv9Z3%s|Ba0PxL5w)^p_K zfwEd$Z1ZqSYqxx9hxT&K(&g>a+_JK;k4A1@{7LpFi#PEO!Ls=5gClY?Gf!q8e=leF zVbItDUe<)2s2=K1BN-N~_*%GI7fxzu>B`rtW6 z?uUa(j?Oba*l+BAe{G%fZ%MhHj#u#A=sI1YMztAdr@i*@e1%J!zL`Fg& zT?Z>o5<#ePTCiS9SD-#pk$EW%5sekqiX`%Pk%evwEiU_h?8$x^e)DH+U*{y-Cy&xe zUUv|vDBD<;Z6tp4C-3Du@-DXW5%nIS7|0|#+Ag`h;TA?KB>l;p3;>zR-HvW!j9)QV zb69$pc}btLL(&V!>xGTIH)rfv_Zbe1dhkd*u>a$Gl#}CexNdL!v7SCvI~sS?UyWbX z4iv5J5+WIk$Q+pB39wx<{|ou4LizCzQ^Cg&iLMEU3DQ2Ecz0Yolf8N3XrDeAWsS9I zrO#-$`p5c=nsD*{pStzIk8WYMGTEYWz0xsprFUfQIN8S5FG-uNV=yB{OWSYyHn-<# zrc=hyUK%WPcUrdi_#|vS!MP)B@}gRc)r2K4SX69y@RT)kmk;cc+|}2ua=EGB zcOUHbJaoG^F@`i(ep7wh^}KTR^5>C`=k(rO;Ck=#*hK>?_61R{`|*^Tk?z28phM4hXr2xOSLMt z0mfD1i-%>dkSCaOVTPqtBYCJx)Z zf4Piq+$XBF)yu2;@<42$%vMwrbc#h0y*bm38g@zU=Hq5I1uPDd6O5(0hVyZLH7q}K zjv6$q7a2H39eHQ?%t=4Vac5=Hl);@at^>DXV{LVR`sai@L_-bQpf=g$1#JWt1m7^N z+JFHjuRnivyhG0&XiDb0r7k@0I&qG8ITIt2UrBy z<8)^u>}5a)io*q32eAYhU!b40-_^eqxu#lTk||5%>YoCZ;ddr}KM=WUT|m6O7tXEB z)w_vYw`eg6CZfwNNBiBhwrafr1(MhK< zdVu{7g+aas0nWQ&Era@jn4x{+$TQKNp5fhPz&jKVRum2ZWNJb#!VZ~a#I+UtIga09 zc)nVFM-0LDr_86sYA0k8?V0XG`^Ugdv0o!zFdv8)PGCL|FY1cEfT5;5)DtHkmZ-K%k+2hf&3ML`Fl}x zB3@Jk;sx`XT$G!6|Cmc!Ky%n1J|3xOGRn$wU1aj&zU@mn>tN?G!zg z_4X7eefqlS1ey#$JPl0S#SG~Gx%zjIah*}8o#oW#qCGr>Ldb+A`g5okb44WD{&gMa zY$}7qHT4c)4~~C>-{Vh_Tj?c{>)uCe0@+Z zxAMB+s3ZT8SKK}xYi}fPsLh+sQTwp}kcQiym4w`0uKtF8Vn0^OtlVYlp7-u1VCLS5g0d18*S*%=^$!0;qhb(~Th0 z!XXbv>wWC+QQe~UpzRi&)We{~mtr*X(}%*kPLBuouuz&JoXpUnH-45rF0$ z)G$Z?t9pp`D(au|E!ydJ?t>H1o(MYV5r}IU-1?#o%cmZwOREs~BE+kpUnUrfxR7^- z7ey}>pcUac0cfE#uqRVHR=+_S&h}Q`m)}D?*AULtr&Hh29N(V>{R#lo&I&{!+SMi( zsDs5S=G$!1p31AY1@so_%1;hGLhFYzqWrG#=KE-Kt--G{dSts!LFRQt>WSB&$ z9Pd*RR!9#Ah*Quh$^rV{C5N6N7s1`-`VAfGGX_^YFF_v!Ak10BvjpzXa67?047jWI z#fl4E_{d+7-T{%IW;)VUe-)oR4hVlry&*1=>jcCl4hvCMEQAf~-pZi4lfUdT02*A` zSv-dpTlNmzmT+MjDZ}1F3TG>U`#GQm0O2_7F+5)Y7zb_rIXuJSH5+W%wt%Pb>dlhgEeoBwhtpmW=$_AT{trLLqN%=xvIG>bPXS$E%8TozyyS#0$ z5T%H>bfFM`eum#O5guum{RE)$gaT&6kMNXd`m@)E$~55PHXQ4*I!{K8*56a)X|ePHx8fOzOMv{cw7& zk48R8MqF^Td1`O`Y@3*kw!peJzwaY@QW$FIBpY;r@}qBs-kZ*CbPV2;UfvR64&qzV z=}8uWJ{;#p)YB^>(3CD_o907r4ii@Xj`T;QNzb_7c@5u{AnxZxGm{~_$<-%|&UoKI zvmx&;8SK;(h(78;dt0qUWTUO0fXv*iZGp~#u&#WNOy=*LJVBdgy@T`(qMg1=j6m4l zF8zaCm+o!Kw7;%*0$u4D^eLh{^-Cn9-S&z&w_ow@f&Fz;G`WcPoT$n?@)lKDuOS}N z_p^}>`V}{usH&BTi5!M_N#9XC$Ok{Gz&}ot_(yT>5P8Hu4ihCT?jg_@0H%Q=-FyRS zWWtX;x-W%01w0`g|Dx!tksQRXQtc=31-i0%5aj(}&>HoWf%s?@wM~iODb-!#A-!{s z%b0(MkbW!CmBv(hIKp*6nmN#^0MKtzOhZJ9HV{C%`yY^*o}vY%#b6WlIenr%$$J{p zVGO}MIe~P)LRyicKG!EoA8F}nh_fpEABz-sHYK}5@QUbV-U9wzMLRU?=k$4qlU(RV zRF7O`a_U>joEFiZ!oUTcl1->b#7md!(xqIw0+%1OcDb%Nsg4n!T{@d9?sC`FjvjYi zOGcK4XZsVw~=aINuS4h(nn6 z!Cff|xL>Xz2FOofrwA6h@`Ko=20*?GF~Gb|Bx=8)u0M~q@jP6Uh|*`FK3hbhUPUCD zdcqxoclfR3h$!9o#QKt1^^xExwtD z-}~?!dD503o|E8HF=#v*zuy47igCs!)ctSp%~Z&N)p+(Xfbu~8=aIi?)vLW2lVG6Vgwp{{l_5d-j_cOMXFlqkOpX zLiv?vuuAfheJ+;;@yi?d{TqPX8={^3%sy9+Mt(=xzgO;vc*-l6s|CJE;qt}1%B5^W z_}3BMRW__mRer^S1C)pJ{fdZFT=2bv7SJV0XT@5CayJJ47}Dw3K1R9&%Hgt)xLjA? z1G&a_u>a(``ilSPLYD$vJGjR4+*d&UJmz{l_M7@L^i}rzaLG>Q6FSd1$lcUiASKAz@s--kB!G?#4EOh>2t7yH4Z_Ol?;^GH{DqFcYhbM*Z# zyU>$dvNJ$_|3}*l_M+Be2v{}w=3qOIvS;SQ*T zzSR?YPi1<>H1CM<+Y`X_v8Zf%OH_7SB;4J$h)Qn0@VA9rr*YiB0oXz+o9~Fq?gxas z`+K62dpi76@a;L_<@Pt&!69EAVE&a1 zW)oi8j{xfbo&exOW$5pf-R>b>T~sp9L_QAVJF)@&8!&x{Z?>a+0}y_j@S?HgzlH5c z=N`)BiFB$WomKb_ahPXeWjM;`;zc=4owRb6&oL&Z{5$jHO3U@#Q{Df>$A7~U;@5xT ziz^>bb^pt9KQaH#yu0cg*E=C+v#o<&wmBVsQoS;F6mD$mm?hlE-a-8b^y+1R*8qn> z*BSs!^g<7S?E_Ga0sRTu`dExZ+{i|PunPh6=o=U9%U!ZL5IA|lwhgp5^AMy>c9kBe zM>OXM`wO{GqLKM^*wx7na}zG?Fk~lj*lehrj>%q^tGtXkTs)I~crKRbbL9p+cpvbD zKc2hA93u2|ve#iQkwGASnscP@$xgTuKy$n+wE)=S0^o)tJh=kn1A-D9Tnd2Be-XBw zi-_mq6Ot2@mrBP(Ze@(mFL z^T^H%eHCR)AiWM{Z;E;1`)+yAU5YTC#O?5SMsw{Jz!1DEg-he%K5oB>p>D&(Q1c4b z0fs{l7-8;)vE(Yz$wW4|`=XDjNMwL!zoNhE#Pdn0Z=^GE8(W4sfiEy#Z6{*%eImoW z9Pu0$@wl_`WsIHN0A}qL`tm#WkM#AT3es(@RN&7?903@duiI{(_S=l0Hg09QDz9O~|di0Nip;x{29hf8hSI{efo;{C;>wHWEJ(=>C!jMELTx z05regPBcbb>^kid6hHD8hk52srh~EsZl2S1{+12#+v0>qj&Kx4JOk+B`2F~I7)$;o zJ`RI8`P+&vrIP`;3i!Q(>j8f{!Vdr}gWucv?Qyuf0rU=_y5pTQ-xPQGZ%Tu{B^O}7 z2N&w;l~m>p4q!jUN9A|A9EQ@z??=9;FwSrlpL6&{2*D zQlM^LQ4y9v4}_iA1zF;nX%~RjLI9Lr2y;oit{y-*Kk>T$g)|c&zRw7tuSA*tE~4D? z;8sZEzx8AO=nBDq8=lfEhtv43B2;|$KlOi!xB##vG2rmVD+Kh+1#YPi{q&4}W1C^2 z@;08=5UX4=-{n%7FcyM&P5i`Ct4fDAcz<_Kbczo|}a7 zGVD-09d1Rx*r_3YJy4R|JWBb|E@EuNpj6c^%5%^g$%X=W+HZR9dPm`1-@DxZ>3fQ! zBDmuDztw%5K9o;6%}Eoub`tpe>+=12`{~ zA7`E_x;*}Hj(N(<=^Q)J7>eIh*#PpQ16*{i82*v>6d#4B@e6>*V|N_S1i8-i{^B~* z;V`c7u4k^i)9+5^&&$h@XTIqwpTw9>rs4g+2&kX-tcH0cj5&DR!DAHY-i~nxjiqP| z;&PqzDbKU(xy!|SE=$Gyboxu5Td|L&J{A2itw+RoyEf)VM$>$VYfh(p&c-=+Kx+_b zEKfEG^3z&Z_e!vt&c%A)+pyz30~-ac?V+`PWD^~a^=()ehxy3gX#RxOR$|S-G|Y`p z(|v@W-b%c!SB5>|fS6|T#Q0%9)+pY=+--gM-@qCkFY&I^KMnJf(@Z~MykCrO;U|DC zVVe1QvCs4!en$fE9lwhaJ25B!f$k4o;0y7QJ_*mqiV}?LrkOFnt9#>j9kEGo0N22? zZsIiAyNK@S*V)eHa_PBi?uTq3utj!o`vhwfs$o6KQZXNETn4+>L>Vv_>(LAOGcYId zgf(^l?K8}~PGT@&pM(>bAO4toJC3ys?y!q|Bl__i^a#w?reUv#8{97eL3oF`D)$xk zyRZ?Ba{mbKMz}Fp+nbN|J}1DpHWl1A@OwSxMqKt3=Xw`YN0c)k>p3Rd@9D+%d#-ZP zdS2}-PJbe;;Y7WOCOZxCH4*ghfPC?D#Mc7(i^HCl>R7LMp4N%D=IgLNDvjo5wNjA= zUUa~ktv;xC?WtZ7ecd-<9wQlZ**(A;tSK6tn7fsAxi&^{*?<;wWg;-ZM z4Qs+SnqR~mJAE@5{ItP;AM+z4M1fu^T7VWasScQDz|BRN*;to?Z}dxekMQ`tN$&+0 z=aiZKp_g@s{}n(_0L4jg25TT*7qiUC7?)Dr7Gw{``6c^NhCU9!99=uN=8zc;L|5|^ zk@<8h=l|Xtw>VJ~ZEpnD%ijczv#=)qrv52l8GdI%u0F8e#CqwQYP|8P4!_>be$y@5 zeiQ56X>C*%*0$e-TVOKbT><8r`oQ*hT-?B#!ZFDEeEopE05-;v`U?96%sIElJZWF> zu@BrjSijX5YjbDA-wr$gIa?9M^n804%XgenC<`qVbg?d({x$%!Tf)QdfuL8x{GJqU>E%Y z_EFeKk9GB0Jql}O)6ZOx&;q;i?ccx- zN@ZV-vLDh{B5y0P=V~SPSgl0)S0bL3kUJ|;_g0GSfGog7tQn>Cvge&HtxdfLIOPO9 z=XbQ0mDbDhdTXRX>*CJRI%S$mr{C0mWCQyz`ZPJu39FoLAzbp4I|VS~39joIy{GRf zEV(#|1m|tk663jj?i7o;ZqIY9;nv1szGVc~t9ijThxOOAM~&wNNG3*#Ry3z=S_Hc< z?H4!!yEj~S?1Lq6^A`d7*P^BScG0r(P|?yiNVKHi41qZO3m|EI6(&wJ?3%9V*9UO> zkU|7k6;kh871I}Pm4uH-upa?Vv}Xe+*`Eha<|ipwkLx2|BrGH7iA;jOHo`v-oM_(& zoMhh%oWi)h{VM!@8TaQW1MEBSWDxsbWPF10DZ;Wg#UsNhJmM#RB>S82+XVVnHf4Vz zV@^w6<&f7nIlX!kawVUzz=0?DuB>boS36Y+h)547`-E=dTox=WmSf0*9l- z5x^0Q>oJaDoWgGxv40KYwTxe2ypHjD#v2*G$l=ei|2@X>l#RmEl-osD!F}}l?avmYP!J`P_3yfRy7*jkrL5lr}2vdhkS66r&r*LYl z4=7Vt__99;Vd{#SjBDGwz~9=Q2iy+St}A*lF68HP7|&%qkMVrQ+d2HpjCU}8g>ez% zR~heQyo+Po&3F&vy^IesKE(Ji<0Fhq7$0T)H%{vvj^|zWpJx9V_Mc_{We)i%<1386 zV0?|R^k(eK*q^bLG3QotPUL=m@(XZ1rer;)TRo;^J=FHAcuxGS#~f*hFbhRPyd4G{ zggVv`l-w&Cfx_9qwb>tGKM#Kl!jxH*s2<8E;{{mGO)Gb{FH_jQ23!%lIJULyQkIKEk+! z@ljBqk$8)9dyMhhjE^%u!TCSM_#B7-knuIb(!$uvxH08KM$=cI=XntY3DY0ADd-;s zIa3)p3H*%$t!4lZLb_366TjWecnjmLj9=t1CpgS0!jLV9O*W>th#xUP0$72U0!M>) zGk|>=Q!9w(RuIEHjRD^~;)W7m@HYlIxeA;Nj9mrBFkm0V8H*M~e)=|+zl{aQTf?ElveV~5X!9#W84^dhy!Id0uyE8KpFVW z3n6FX%u9je!Mm%#iJ(S2xb`t{iXF3{qBVFH&%BFA+o31h`N_+ScQAg1aS`KJ8Si9# zlw&5I#tY(UJa|etiQ+LY1Q+7XOM#mpZ%2SBZ%vRl@;5=qCNh|DC~yLJ_c5>!&c8|k z@5oQQOJLq5fOpTspMVq-z`J3<>5SVk?!mYZ+I4~$z+V+|_&JQ{GM>kHKI857(+K%8 zf3<`CuP`oR{3_#}{Ps1*yX;#KemDE~uzxS(gZ9_){187s%>E;cOBf&J=jR!J#P}Lv zaFB2aF9#yc3l!nla>tBiLt-o8(0aWsWwNs`2pBpH-BB9c*y2vd7c=DL)OlG2kojOQ|*$9O*DMI4f9 zOR}Kak}RmUBnzr7$%1N2vY^_MEY30hkT7#91su6AQjl&fU|;qJ*;V*!GOmr*l)^bl zVabpJzI4G8;!6tnLjHb?3pwN*#&a3ZV?3WRvZn|tdkX3*at@Bv1&)Tl8RWxN zU|-<&EK%DtFWTeD5z!tzS_ixYlB7L2wN7-w+pD4nhwqK>wM1|DYl;5wR|fV49>D$q z>>tSC2Z7pm#30c5jws|YMInzV3VBRXh+bnap3|73kjE5-g2ohukh(j7X-rXweuOZ! zyFzYvg*>JxVib6?aibC`t|BJS>fzNA58~!yj=Od~J zim2jNDWTB@MO2WsX{DrXv?)Q^q)pm{w236upr|IPplnctek68_E=5peZI_6Mq6oSx z2*MUYQ3OHh`=7ZJWp|%_p7;5^ulis2IWyN>A9K!`GiOe6$4dK5u~^w>ip9!4Q!IAR z6pI7@&sUz&=wf4(v09VVI*;TsiDqVJ!F1t#XU8T#e(q&iava592Rl4jdU3Qf&yGoZ`rOU3;WmoC4t902_ zy6h@lc9kx>N|#-w%dXO8SLw2=blFw9>?&P$l`gwVmtCdHuF_>!>9VVI*;TsiDqVJ! zF1sq1U6sqO%4JvOva530Rk`e{Ty|A1yDFDmmCLTmWmo00t8&>@x$LT3c2zFBDwkcA z%dX00SLL#+a@ke6?5bRLRW7?KmtB?1uF7Rs<+7`C*;Tphs$6zeF1sq1U6sqO%4JvO zva530RlDq}U3S$jyK0wRwac#BWmoO8t9IE{yX>l6cGWJsYL{KL%dXmGSM9Q^cG*?C z?5bUM)h@egmtD2XuG(c+?Xs(O*;Tvjs$F)~F1u=%UA4=u+GSVmva5F4RlDq}U3S$j zyK0wRwaada%Z}fR;vTWYWw*p-x5Q<)#AUa{WyjC^a?USt*)4I|Epgc`aoH_#*)4I| zEpgc`aoH_#*)4I|Epgc`aoH_#*)4I|Epgc`aoH_#*)4I|Epgc`aoH_#*)4I|Epgc` zaoH_#*)4I|Epgc`aoH_#+10qGwyWI;b~Pi|AlV*tz0zv2(>0&dv&FXN9w~!r58j?5uEhRyaE=oShZU&I)H| zg|oB5*;(oAtaNr(Iy)TXJ?hOv&z|7 z)MFZq88e z<_z_2&QR~>4E1i#P>&a@Y3B^}c(KyX8S3$3rJXa>yE#L>dzz?sbB21cbB21cbB21c zbB21cbB201XQ<~nVlvxy&QLFQ&QLFQ&QQ;Dgq3#AP%n1QP%n1QP%n1QP|p*+m3Gch zFLusQFLusQ@8(POV&@F?Zq88e<_z`9&KVl$hx?HB`f8vb?vsb;)lOOGf~ci zHaYpYlTW&tz>{t!(9Ch^lxE6rm#19$Q?9(ljZ`h1nYT-e8>w2{NY&y-sunj=wYZV0 z#f?-gZlr2)BUOtVsao7f)#65~7B^D0xRI*GjZ`geq-t>^Rf`*`THHw0;zp_#H&V5@ zk*dXwR4s0#YH=e~iyNt0+(^~pMyeJ!Qnk2|s>O{|EpDW0aU)fW8>w2{NY&y-s#cd> ztIMv{W!LJmYjxSRy6jqAcC9YER+n9?%dXXB*Xpusb=kGL>{?xRtuDJ(mtCvNuGMAN z>auHf*|oatT3vRnF1uEjU8~Ek)n(V}vTJqOwYuzDU3RT5yH=N7tIMv{W!LJmYjxSR zx$N3pc5N=ZHkVzS%dX93*XFWobJ?}I?Aly*Z7#bumtC97uFYlF=CW&Z*|oXs+FW*R zF1t3DU7O3U&1Ki-vTJkMwYlutTy||PyEd0yo6D}vW!L7iYjfGPx$N3pc5N=ZHkVzS z%WeZ_>|?TlGxjlg-fchcwl})%jc)rTxBZgaZg<=5?n-IzE3xA(zYT$fS6%t5uKZOu z4tKheE?3*tw{3lOxw}#qz0@gP?!NOQ{d|`E=+@1Cr0p(j+jX-a(Yy?4*Uf%(>t;WS zT{rtt?7G>HV%N=n#E#XmYb-y~+emsCYs#*h{fK>P>4&|LcHL~d^R?ai+U|U9cfPhe zU)!Cp?atSB=WDz3wcYvJ?tE=`zP3AG+nulN&ewM5YrFHc-TG2mrflC|8S4?rsv45) zkYtBU*?ts~?2xRRAz3#=vTlZC-3(dpbjXfvvQx%-M6#lV9Bpf@N2K+*6TN*Ik~=t( z`yGvc`pEjSFcb*&C0xgf%WCYg|ayxR9)IAz9->vc`pE zjSI;d7t%(!w~KIZB4nG`uw&P_kgRd3eTeNqBu`jsu#04{i)65ig!kv@4|b6Zc99Hr zkqmZ`40e$Wc99HrkqmZ`40e$Wc99HrkqmZ`40e$Wc99HrkqmZ`40e(1=OWq9MG~>+ zr;5-&ry)5%k*vI_h+TO@vht>eIvx8(=%=x5b%r{fp-yM0)8Q?~&#$pc#p)cA)j6c? z|3h5=AHw~#mTgw&kZIdku9Fo;JB&%~sp23kk zgCltcM{++!az90KKSi=Shh%jQd5@E<&au7L$@`pK=On9hlt1d^V@@_X$?6>Ctj-}> zokOxZrw-xnX(g+3NLJ^Ntj?(Ku|87m^tlk{K70 z85fcnm$K`eFCus6uNJeCDf5sMWIFN`$wj8i8OVv!i9FSnWVr1~@+KvjavAb;C#Skr z(_|?nSx$4hD?dY)Q*x$!ip-W;WTgektO?1i3CXMpY0rPV0%o^aX+g5mf@Gxy$w~{7Srd|36SB%lW=(7}YeF(> zLfWG|+8yQ5?kJCTM|re6%A?&;9&Kk$=&;g)WY&aa)`Vo%gk;u)WY&aa)`Vo%gk;u) zWY&aa)`Vo%gk;u)WY&aar3J~X3CT)}vTI+5A(=I)lR4I%NM=g>^@{kPdyz-*+w7Og zVz*uCWR;WEPA+k>X8VER61kNY+%5jJ*TDvg>$A*71;b9WVM{8Xv!AD?ddrir=2($8yX-eyRZc z3Rulg{G%FV13zOCro&Qb;{|M?wus;&rk*y+0v^|nIj->7={2fiZQOUwfDUoA|^|(}#QOi4LcdMnUp3n znKn}>%PbH%9o^HrM5e|8b<;XUvhbHx3+PW@FLFi=5c8RvM6y$0A<*Wm44~}nY^Z=b zXc9SR1`zK#_&#SfVB;M0&dm}z4_oIo0D9-OL$}E91mYkG(jiAAhy5J(bJ)+Z`xQV; zIcr36hltEffKeFF?PLSkA}a`S?4(3EH7sqkKe^oabv3O0+>t`)f<1+oDD zHx2>p-ZTiXvn&-lMQ*0u&5J~CiG!ukA#!UrtONYqimf`@*L8{9mIlotx5vT^Ah+d| zEpHN8f!!4ZzhbM%$~@R4vMLdXaa9hi0rXdO3paAWUwsl#Ur&8~1=Iq1_4ugo0BkkH zKm?K@6AEAv)IlR`fG+42xg!=50Np##y#w7l(7glQJJ7uY-9KbNkH{YfLOi5E7Lel~ zt6&AJgN?8mwu!7B1c{IaGoT#scPH)cq}`pgyDJ{Lg$E#rgCt0Y9H@X=SOd+_0bBXC z${2`1GGsylEP^^{gbmOIy?p&bEF?fGWJ3wmKm#;EJ9P7P33))9duVenZSEz$wL@T; z$bB)833bp28vxz=(7hks`x5~B_h&;1)ByJHZ-RE{7I`2O_-xF99+3y>uZJjmXp_jp zw0W5RT8E8ww0i_Qk2LcY5cJKX*m`UrEP`5*rdU`n@@GE(oB$mnkI#TEk-wxtqsaPE zfbYMSz!s4w$l=KpXcuX&5P50{EQMZ?7WAKnR@y!j3;1~ky|#rw{j=D9wo_yS`x}V$ zIrg7N?|FPbZ?`v!ypRqJu$3=|KyM@MUc}E!$Fb zMKC9uSBQLo&WAD3B=S)Tpx-?Ru+@#y$LM^5?kBadP2|%oSSzvxn_C)zvd{9M9BBJl zJ6~%{`_EIM0@jIqF$5L?{qP0#U-XJ>jX;CQm*{?(1n7U62|2J3YGFNez!tuCK_C&* zf&H)f{CX>2n@gPEp#KfAeoOtgt6?L*{*0aPnnZf>-J1gJ_f`Qh^lpGozDqU&qX1jq z&wvs@=leQX3(a8l@biNQae$8>7Qr&W)(`8U1A6#=TI~Ip0!4t0AG`S3-2_O3Y@mD_ zd2MTiP5itu_I}EQa=_kC`1=WeKU4m578JljXa!=}P7K?r+dc}gx4j8A0)Dplh~npC z6+dC4Bm;7w94J?@kO-8kB4~hC*vg+fp-eA=F4!i@4D2JqC+*ETK-Zw_B|r`=ggWTt zXMYDlDr7=#hB z7jf=gA!?s^ST8EB1h5^4-#GlnwL`b4eKUaV!84#4I-!R@TS40)qeSg*LH~eQSSsp3 zfmCROEutcEKuiaD&?RcN2q7EAb_&t0ztcOj&{%~>|UMDIs17-kqM-;(AQ6pmjyGgaMQPh#m z{MnxZr~uj>MZ2TpAOVsgokv9W`NLQ0m`i2QVwb2>Yebz!45txK#u`zR7K)m@07Qb+AoTP9C(2%1wa=Q8V!~ld_r3 zqGrWGiKslv^01w^8rF--7f1tQ$fwP0_Gjk+cIIGb&N3j5f^=9bs*w7^ScpI}WWr`q z=NCbzs0)bqf<{qAg8;ilGoT#sS%lA`bjAoR69N6X=+8xeF8XuPpNsxn z^yi{K7yWqyAs$j73-SQ{dFanWf8Ki71Y1NEdoTn>K?a~(ybzWF`ITU+Bp$F;LVXEs zFC?c6n?;q*5LHI|vO$1O88*t&M3s|&d6lU7l+Rx(>Y^A}Cu#xxv7lB|MK){|buph8 zuMl;~5NH*3X%e8na3S=Hx@@he%M+kW)D`$zKf=2wWLDSRoJ|0ji{O|$OG)x*nRw8O}neny_)@N5`nsFW1s{!idvcq<*-TAb<|xi zfZyxc{(T%YimDw51)^>smK&Cdx{)?F;+y9UbrU%)%Y?O}Zcc#>qHZD1TdBLX2-vTi z0p!3lhPsWi+wp(#KlIo)J_7`po&&9qpq2 zkOf;r{SmvXM~S*K4%UdeD;_Ajo6o!XTvIFRPf37{d-%ME{d*&T-L;vpO%!uxb$=SH z6ZJp=bct$Ag{`6jU)#{xM%gy%w_$S|wzhSO`Y8>vp-9xv*!YIn&gkyERrJ6a!2T{-&>(tO+U}YTi(s?p-O^yK=-44Z+t@s)0bAB7dUy19PXOw6 z&xR79es^r`-UQg*y<7Ai0_3$v5~M>8R6s4PfoAA{t)lmgfe0i+CKSLTsDnn>0A0{4 zdQdDRKq_QI3DiIXG(kIbi{48h4w4`pa-afgVGT4x2W%C+cML=z88V>&7C{{}!UpJq zUeWu+LIR{hHk3dOG(Z!yL$~NSfjCHlbjX1UsDnn>0A0{4df!+`fK&7C{{}!UpJqUeQBhApue$8%m%C8lVZ< zp&7C{{}0eXj^cPM&?qIW2IhoW~VdWWKS zD0+uB!UpJqUeO7$kN~NW4JA+m4bTMb&@K8ffjCHlbjX1UsD(Aq3>~5mN9S;C9FC2{ zv2l14VB>IX49CWB>W5=vI5vi3V>riX_%dLu7~Tqu7sGo*Ck}*oNCC#3#3HByZ1U`( z6Y-U}RrC?)9f95v=pBI`&m{T?^p02o>tG{n7Ci#rBZfdSWCD7;tI;E{Hv)SjHo+Fr zBRxQGIfW49AGLl@9&`Uxu342LdfW0K_C9MGTcn;D@n_-*iBL@LFA4$$f zV(&=o9k~qFLMwDakLaTYLOi5EHk3dOtcLZl3ATtn+Jhl53Nj!M76LYpZiEf68McY$ zIZKa1Zxni?W+Qn^sxg0-DA-` zHVcZN3Rb{6*a(|po9N>PK_aBV3@C@Cuo~9GCfFi6*@Gc43Nj!M7Q!-E3$4%zJ)(I& z)W^p|3S>bMRKW^Z2OD8CY!f|t5F|nx%z$!O3aeo~pgS7f(LJJ1@E`-wI|02D&^rOW z6VN+h6KoNk5(D&Y$|y(!bWJu*9#;fiqSNAGnP{HN^>}p0 zliPUoC&aHf2EIx_)?H?vpt>5E_;knid2Peo^H z9uWW3PS`4X8Zl0bgG5LL+Ddu@2>}NNNJ}VQJiat9Qs$i?=bMSi(dgrVaJp&&zXg33&=c0S=7SZS7 z=XY%9B#6$XJ~t1_Mb9LrnQ=h*%o5ljdKR{3trwj~d0qu{i_S+cKM}G4f3w$!o|6UW z7vzX890=Gi#71E)G(iXSiatLMk|7%^pbna#1A0YY5C_RX`wOsjK^=67E=qz_z+MqK z%*}?4&?$Nzy7Srry?I+j7YoEf1h7?%ZgB}L0_+tx0lLM#qD#o71ig|}$b=j~w`37e zew&o9lWk4Jk#X~mKLMzaI0c{tszkuxuY*%2XqDS<_ zsjw6}L|;O?OQ^eK1+af9HZRQsbT6gd!Wn?Q%lN#kS@h)|q(C{KbNOb`R}6tHV1E(y zi_!p_i`qqBiLWaQ0Do7m1AJV$P4wb;NQV-jZt)sug)X4JG7d&TCKLg-SjW&+jj%;@ zbpqr;0}$sD^p?~C?XRNlsuIB8Rn*lGTMg~5jsehh_Krr;f56Tk z6QLaNxjGHjioTQbI~R(+3*Ebtp$>XP-;Le7mx^9Pylbj}Hh&rg*uDoJ_t54ZyS)Ln ziM}@ph~r+$*CxOk(f1Vrw(cjk2e9)%ljz1|pstZN4`T1Z8rTHAq92MtI?(Q+T4;u? zq92ZjJXj6gqSv9bj_pSV0o#w%0J@KKiGEZd0;!M#>qI}+2>579geq7s`mg1%2D*S) zo)`uAeF9rgGy~;NQ2t~D(jW&G0rr{)0%gsVHP-<#J{1qCfQ_dpdkP;(xC*f z-$q>3C=C<4kiYykA1qx?C_pTo{`*mb_eu`ekTr6AQK7zop;v2CeiOE0Dj&jhIe~Kzc&iji+-Q@HhU0(44~cSWk3v@ zJ7JsX50Zeo56YnyngE+03JigC$OGy=Tnio0Bl;t3ev|^({D}BJ!sbUC0sq}`fX!~& ze2m`5*!~!QpJ4aX8qr&bYs(_Q)@Mnu6uL!!o(T=G0k(?%A`w;tHn(DLYXz{swO#a= z0@(QydtXxaWiw#!OWJ*v40%usP0%I!YjXLz0G7de(LD*ke$N`{7X1zT-?05HpWpKN z9iQK20zSVZrtdaFx9HwMkObMHzbEb=dPV;fCwhB|7+EbwuMy)_i3wuGgzU$pi`gkv z%z!*GJ8u-T%Qi8)#lQ+Nv9+*8%1GBxc`cF@u}L3|R_% z?oZwRHP8yQIbaYJz%)yKLSjOiX z!0y2vfNp#&jDmE?19an8Lo;j^Gb{$EA4dH!>W5K3jQU~J52OAN>JOp*kW46tI%tJ% zF^3L0hnV9%AU>Yg&GE#?bGkXc9eTu!j)OEP zfMtNa(UhM+`3aPtPy(v~{S@q^Btto1KV=U1G)z0`eJy-k2un5i=HhW6J@1W6>MiA!eKh z_#c-A_#a34ILgx~PeVT~5719*gE%!dt*}+h#CXUA^d_P=5xr9*kOeh>-l@G}P8$U!unw@FF$l1qLAwmvW%P)d zg#Ah7um(1ZnT(yudC&mtPe}mkr)(0FN!!fTVon#RfOauc;~)oUKb5g&8uq54Kdlki z&l&}cHCe}zb&cxT5j6vCaX4k<+F=q{dBEZI3Jz~yI1@zDE5OYpE zVE>#}F*61M?PhEeb8Z@}6LTK*=M?~Le;0vT=oXWMjU38yDa$3E+-5N|Js_u<=*-+K zW>yL;1N8GU0G)hd$*&MITc8Fy#mpH6#50HOf<&Oakk7(3K#b?d0JhFAf)%h)%muW+ zAPI5+e-~i;f~{hTB9IA-pb@&n%#8)|nOhF40h{x%IWHNqpahlz{^k)k&#R_*2Gjw* zinoa=NrW6&2$Yw!i@9(JRKXT8rPwLmDyA$IYN1n1c?zr-GatSA_@0j~o=wd~3tBVs0d^8`p@rDG?SyhnQtaPz%_)IU6Xuh0j~oi@BA$ zThXt>AMevmU6YvG5@4g4+cO{!i0AgzfS=npi&;+ja&&o?G|LylQlNeX$6^JaD~AC7 zR$+TpDr7KeMm+>rp3-GR?Lh~p1g zK$}0JvpN>+XP1~elc812UHHALPR!jyfX_AMV*Z5QpVo`Hhkn0@wmegsd$GSZ5jKdq zkC^V;Cg%QuK)d@Xzdr>QLZg@mu=zl{n8vkY9wf#GYoSxjL-Bz9ht>e~57X}9MX+AX zIu8l}KkGWgJiM3M#PaBBF^?rf6>JpK6c6}p!rq@_fcnSt#QcT$*3S_0 z*I4Kg^P~seVw%x?DpO1gdQazwX-yOJ47zQ!ZQCkl1O7H_7V~_Zm=~zqh|Y@82ZC=IZrXn$aBc{KT&+FU7ypaHuzu76~Eqr&kz?=BV7 zMK15*_x*T4XEUE4Q1(HSm=9O~zlUc4^HB+qV|Na$7xVETAQ$HQ=93g4k5BObDdnGL zz*^`Pv!wz$#C*08*#CSHG>iEHn_pn-i$*b9hXC!jt`YNP9IO!YRV*wM^EIDeSBdE% z#~y6-VB?!~C=v6m2L;e6=DQidCv$ewyI#!q*#Dk(-)|7}Ljqv`2jchDG`n|L}6TE#PQP$!-j3+3YZ)cc#n3$n!v1(t~y z;{jzmh|F?jyHsjybOA+bQ+{&CO%#CAY7VB>%; z@eZW^zy!#HI@l^+BnBel9YhQVr2};bVT0!i@1Q2Y#zE)}9SF1?ivOWGun@2@6x&1F zVT*VNW9#5}NQD_t0kz`A(}w2@FTP8>VQdd8fll!bNrrO3{-N2hUc3ZsCs3BqE#6^` z;vJ69!|^d3o5LwfOakmCZW538irx`LuoTc4kq*7$jT{7{AQK9p2Jk-;`y=t2Op_HDbZkQX&^isT9f_DUfoB zNP^6g!}vT(lK6KtTQkv!$XImBr3^|XUvg!xBvLw2=Al1=tua!>zlcoz#a5Zy%co5~ zZ7xFcR|#c+q_K5A7S53QXw1TIE>`AHYFp$w`w^_nW3L3Anb<8vFGBlb{N}p40V3my zOD--goKsL9Ntkt5iqa!mfj*KlVFDoz2&z+kXnK*COh{%|tqR3RMRTi0=UzT5b zQGVWt0crW?=bkY?GOHkW-kkihNN#Cqn^KkInNT<0x3We!v zU-N1CrG@N`C9Gm$7CN@L82LYJ^*>ZQ!+9+u=VI3*N708z^BehcMt*5oVe!1kQ6r9K z@7LD;CD6V&f2|WC4QtRI(g-qoNc$6;T|AHODyI|0ofhRBh|zpC=tvGh^x$2@ff+&D zVm?bbIQhnkRrahJNiFYQlh}H`@Rqe_5+syvAr<6T82mG58J;JGKPvKlV{W|7)ZePf7biI+Wh~2N&dTw``0-a_Fb6vJh8oC&jEWGmU7NVI100o|9ZTBJv`XVGF>0e zyN?aZbmdgP&XS>SFtu&-oL`%JF*Xaa7tJp^tj*#&noE0|wXNyzpV@2`QEso&1j?f^?(h(i zY1o@h%>vrlUa}r{^ocDgp}ZLX^YLpV?2jhTWnhnKA$>F-8@9IJNB^1ruO6&*+Yhsd zY(94T`(y!IwzVA-?5)K1j6L4J&8Xl0j+;(&1hPkPzUxD4zyIjiZQCz9j`faPLK(hn z{lCk@mPhj(Nj#{W{&Uv-Jr_OSqUTvOLwl>TQAW?Y9T_cf z{V^9m|Gfv>tG4BA`k|EmwR`q>+cPKX(cS=UCdDrAGW2G0E6u03Y=1{}`u*7R*yd|T z$^Ii`Gq-1=JtomJtc-EpUY%LalkKya=-8X}j$^RnNcH!r_4ThK!8}@8yY?)zdF<$| z{%d7NkNtWK_;ov*ot+RwkCYwfe|xS)d&A~q?f=)u)8=X0Mvwhm_Zi){qg8(&{ntm^ z>RjxO>1_N&Gm73H`g`g>wY8(8ois(`x87}^+iSUhM6&;%D(J?L>sU2PI_}YKW zcbvyNrbPBSkM{F?tk}IB8QHdWbe%)~JI?Mo=-F%v`pRsJ{xQ^M7mdW~M|<|4`^VP& z=bUyN?Pvx&&M=#4gn0fng8rlP?<4uGzklwR9U1o@hcb6f*yCevd682f6=>`z54Cl#`dlqX?FB&f6VMr_ZX z-(CYd&X;I*|85Mo$7DXO?K%DH9{zW|x?{d#`*+7N{NG-)c8o1@eV6Nwq`m*!tIqa= zJ+JIsFnaCT2NxTiZSm{-u)QPLYwH4})v>)1y)W9cYRBl>Z}I=fi1p7`?T)OX4|VqZ zjoue$|8l;i%Xp%jAXE6F=`6OUu{D9MGkL_D>gpy^7GXX&m6|hn1Wcnejgp~g*cw|u z)Lo5dvYo}U%@n?(+S-cRn2MgYeHJCwwtaY>hT z##Hxt3iURE=#hx_)i|^=@ohCuA+9VJh4o~!vzj)lEb7P5BeuQudYUVZMrQlO9%UQL z4lDLK%KDDRVf+7#K6~~Yu=!{3&-ShD$7#;PcuFTx7PT9-8X+p1iH$qj8`IfOL(}$y z&CuGgb@r&({>=DAGum&_bHN_dF=$M3k=yn*6Wb#@&W2xS(r@jzN9W(29sOsUwPlZ+ z?WqjsWg0y@o?~U>ik8^+_PnxZ{x~w&amGc@d)wdry3t;+$7ixTvZr&Lqemoa&pxOA zIty#-Om`%99FOP`uo2qx!g}oQvmHm@`m{0k$79cCdzSSdhaJ6QuRnW^Sx@#XuzS{n zy+-UgW{;As>%TsuKKhU2bl0}us%`h%v)`U&_B!g<>u>j;#*Szo_4~HAcjRu*R@;}+ z2<=gh_Qrp2H#*9X=e}m|kR@)Mj*i>W8E^mdU-Uk|V~!KO_xyVPX?vspKDpyA(mzI@ z!kxuF4d%OE>(|>+YJ1f_{n&ffj;AqPlNV;{M+ZfGGes%ej7#9SN}bK zhb{Z8Wmk&qY{7af`z0FdE!vy^X@>C6Pl)!()INulVrPNdE_YGQV?Wp3E$!2!)wzWI z{%4lxdVtMg$FchFjzNDe|6Tvv`FI&;Q1qFq(DksLPmaKs&CEs`eLm~&-{|p}?dt5D zwLePRk2}sjJI5aF+WvYTT26_bsYlPL=t_Vsvwr3{pR7VoaEozviPA1}{<6FoNhr(D zkIc+3Dqe8dh{*q7L36}_0XrJAdYBuDTK;7jb-)q-@xK8Be$o8DUU23$u4j7P`Uplw2%q?-EU65az&*JW!(%gCF`FV+v*`*{;Ggfd} zPfU!I7e{jET^uQ4QIs{&;+f^F;1gbBnoN{$1LU@{3FIXXoOG>%j)~n@)+$&Ar&#pIe+) zIJ?lEXt_n@oO*0vG&e8LW$yaey)eivCFc1>xuq`ky!^7lIrCh1&nddNq=2nbcZTN9 z!bq8IVSC7(WB=?addB5NJ1n;-vcteX8R?6#Kb9R932|Q0#gW3_oa*#KY5u(2xzQtL zH_9S(rad1GZyU0$hzzxt?oiil9G#55Yi4FXSCh3m zpW|SU-9^QP?)0$j@+-=@Y9hHMC0t^;GmE$ciu>}%@^8DoydbwcQjl9l|L4#9OqmcFGx@B@X%i=>B}T?)XHFeIZCYf? z)X2n1nHdwuQ#Nt(xQyv(6DOY%8B4p#Q?eo%6DLi~!f4hM*R;>n#PPJ75ScW7>bP_) zj~P2LV`A1>iIE8tvnE@c6EHd^k~wB-*2HnsGsaAfWKN%&Ic3^-Y@}g!^2Es#rs8G% zr12zyp>b0(&zd^%l=Q4b8f8(K7|EJCCT;wrF;h=Vv=L4r<*5;;GJ+_v5gC7mZ9FY~ zOh!gz?8K~TSyRW4nPhcs@0>Du$|T!I(33q)`qJ6a9k#(K<0p@wIwm7AGA(obxQTXyew;XU{5YaTlfI`XD8&eZV}D;21YLO z-wp`B6jhB&`_mXq_Qg>tHtn$(Q(}nWL9NSae?-(&k=ZuIjDYGxj|95XiN4oX+3z*Z6 zgy`y41#>;SI$J{7Z`b+$^E=d$3kokN99hV4STUlcpk!peFdK;PJxuuN>ZUJN>-=}^W-8P8_RJ$~CIuEvXd15*X z7-zDGbQZasBQxY&IghW-&0&hZmha`Sl4|*@+#=olm8I+X8$~PSPqJERxkldPFNobF zU&+^U9pBPiBTvga@};bjd*mDGk#G5C|NG=Qc~_{8qU^{w|y3ZyexH%dvPiC!E%LcM&R;OU)M4syHC%p>*HxlALXA-T9anXvI!Ya_MyX@ev3#d|vO1ohta)Glp-xaK>O^&t zI$5QvF>0(Dr_$7TH9?)C($z$1ln2$R>NJ(1CaKA4ipo@{tEp<5{3zRGvwWbk)O2-* zI#XqMQVYnP#3F9)TL^nx=dZJu275kPW&s?VpXZCRJB^7u2MDXYITjeRxMT6 zsq5A6Rjs-~-KcI-%hb*47ImwtQ@5$x)pE5$tyHU2y=qW*s6VJbs@3XFb(gwZtx_8U>QHZ}H`QCJQ@yR;QSYiQ^`3fP z{X=b5AE*!2N2*(WtUghnsx9g>^||^&ZB<{YuhiG7M}4EdRo|&z^}YH*{iwF7pVZH4 zyB4jq)<%2U*MSaojNVBP&^zmadKbN`-c85q-Sr-NPd!NQrT5nR=s3Nv9<2A%L-hXo z0DYj2=!5i7eXx$#!}KBgP@SL;(}(NfI#C~?N9d6{Ngt_?(nsr2`WStzK29g=XY=zI#rL+WA!+lrpN0E`V^h6C+bu6X*xqs(v$TRovBaPQ}r~RrKjsN^qD$a zpQX>%=ja*wTz#JYozBs@dZwPG^K`zRt>@?hU8v937w95ASI^VMx-6>d_qtZ!pl{SS z>1Fz6eT%+T*Xi5z?RvRhp;zivx?VTvJM3j6Odab@s->)Cg zjru|TkbYP?J9oi{k(obZ`3d9m-NfJUB9AV)vxJI`fvL0`gPr*-_URBw{)j|Tfd{<)m{2M z{l5N(-mE{+AL@^ExBggvqCeGJ^k@2W{e|ADztmspuXT_9Mt`fn)4lq8{e%8dZ__{N zpEZ9R%P6CbF`n^FU_ujPb}|FZ&Ss$5#q4T!GqGlOvxnK!3^IF}z0E!*&g^RjoBhlX zv%fjO9B3luAT!h)Y~sx@bBHWN=1Q~JRGKPNZI+m;OpUqPTw|^^OU-rWdh>fz zYi=+%nw!irbF;a{+-mB~ZRU2f+^jGw%_>uG8q6K$59W_%wYk&WW$rd>%%99X=3cYb z+-L4L512;tpn1qVY}T1a%%kQp(`5c^9yfn6>&;)y6Xr?NY@RYL=4sPvo-u9aS+l`B zXP!4Nn2qK|^OAYlw3}DVtL8Pc$^6ay-Mnr(%p2xS^Oor}Z<}|_yQa&$XWlpeFq_Q> z=0o$5={6smPt2!gi}}oaZoV*E&6nmY^R?+Q-+96@b>fud3$+#d;55C-oD;oZ$EE{x4(CQ zcc2&X4)TV22Yd0}Fz*oWP%ptd%sbp0?j?Fhcq6=#{J`ds-cjDs-YD-F?^y3RFWEcZ z8||IorFbWLCwV7(soofGtT)a}^TvAe%Z=!dqcbb>sP4XstQ@l*?bZ@FR&CBwp zduMoOdfDDt-r3$c-VEz5!UXGXR&Gcq@d0xIZ+neJRc!l2i-UVKfH`klz6?-M# zg^$+&r{bBwg{-J(?f0%!`Kip6BkMKwM zBmE@*NdGARXn&M{jDM_uoS*C;?~nFR@KgK~{geEY{ZxO9Kh_`Tr}^Xk3H~X5x8q)*uTWT)L-ad=3nk#;V<&9 z^cVY;ewAPCFY&MPYy7MIYy4~drT%sP_5SbuTK@+BM*k*%nSZl?i+`(M=ilbv?l1RO z_$&QYe!bt|-{Jqk|D(U!ztg|VzuRBq|H;3{zt>;u-{;@&Kj1g|5Bd-J5BuxVN5f<$vw>_}}>7`rrAz{`dY5{*V4P|0n-ve|sQ-3UpusFYtpP2!ohl zr(i&^b1*R2CD=9CEr<z1$zhk1aZN>!Qf!OU`Vika6oWi5D5+nh6V=* z@xid*kl@fDAvi2JJQyA%21f)Vf{{T|aAa^)aC9&#I3_qYI4(#Ijt@o$Cj=?MiNQ(1 z$w6u`CKwxx3(|t|!Gz$HAU&8EoEn@KWCW9f$-$H$GdMk%8cYkag6Y8-!I?pJa8_`3 za858II5#*i_+5|_P!!A!<^{z;NpN9M8k7a)!TjK& zU_nq3TpU~yTpBD4E(4;}~_g9n3$f`@~3!6U(=!DB&F@aN$1;4i`Y;IF|G!IMFA@Kn$eJRP(K&jfA3 zv%!Yox#0QWgU61*C`7HkUs7W_SUJ?IGD2;L0d3Oa+ggLi^=gRbDc z;Qinq!RFwD;KSgfpgZ_D_$2r=*b;med>(ueYz@8)z6!n$dV+6)Z-eiG-r)P-hv3Ix zTkuoxbFe*>P=z`)p%?mL5Qbq)xKlVF+<=?h@`A?iR*|yN7#(dxnF;y~4f2eZshK z-*9laUpOS(KRh5jFpPu;g+s%G!}xGmcu06?m=GQo9v%)46T>6I5#h)%DLgVfDm*$I z6&@2F8y*)XhsTGb!xO@k@Wk+>@Z>Nx921TW$AxL(_;5maN|+u_3{MSD3p2t=;pA{i zm>HfPP7SApS>g2XjPT4bJ3K2qJ3J?x5uO{K7yd5H33J1l;jAz(%nxUWbHakKFg!oJ zAS?>!hV#PWuq3=NEDg)T@^F55QMe$i2rmvV2`>#7hL?qxhgXD)!YjkYVP#kqR)hPNI+Hh%jU3h)?`>-~=A-plXDO?ub9NrS%8rFrkg|~;x!xiDma8+0zHiUPC ze+d7`-`adQTpivS-X)L9W6~PlE$ieFc_v)L-^Ff`tHM8p_k{P#z2VyMzVQC=fv_=r zP};(W!iU3k;UnRr;bUP__-B42;K}gu@Gs%|@UOByd?I`@Y!06aTf(Qq*6^9IEqpfI z5Iz?^AHEQ73||ai3SSP}!&kyr!`H%1;ori)hp&en;Tz$b;ag#6_;&bC_-@!0z8Ah9 z{v+HReh_{beiU|xABUfWpN3n)&%)2cFT$>9IMOl-{VF?+=988awmub91M zY0N$`aWVVG4360^W=PEbF$csP7!!#(C}wEP!7=eM!(tAJIW#6A=CGK`wx_nf)$5%HtqBjZQM>*BF^JnqH)xQ?6nsQBpknE2TE zxcK<^G4ToUiSbGCW8;(KQ{q$O)8fa)r^k6u&usOZ?XOZSmXV zcf{|E-xa?*eoy?~`1bgY_+&$Ky}LpNu~h ze>(n5{Mq<(@#o_ xfR6n|NEGu;w@CH|`XG{)EBugBkrzZrjP*{$)nWp~W);_t-Y zjlUOvKmI}d!)33Ie-!^X{>ic%;-AJpi+>*fBK~FktN7RPZ{pv^zgu=&{QLM1@gL(q z#ea_f68|;+Tm1LB&@ z#u;xCv%;)2dze*bPqW&rF?*T&nERT&%|7OS=KkgZ=7DBkv!B`DJjguQ9AFMK2bqVM zgUv(D!_33YA?6Y0P_xz?W=2eEGLxIalxEa)O)!-?+&t18VIF0UG>{RR$>u3$ z!mKwN%%qtzXPb>?lbJT>n9XL!Y%yEStl4JHHQUXc*8tGe>Q(He>HzIe>eXy_nLp2f0=)q1@j+SA-tR2 z-9~oEcI>cSZewe#wa$8**cEoA-NUZ3d)n1@jor)M$KKcOZTGSFv-e+ihkby3pxxK* zXZN=cvJbWg*aPiB_96CQ`%wEZ`*3@ReS|&KuC<5R5u4h~=C-h<9kpE>Y-JC(kF-bF zN7*CoqwP97X2)&M_HAt&dz3xe9%GNS$JyiUW9$j`M0=8btUcMDVo$ZF*~i(_?c?nk z_DuT(`$T(|eUg2$eTtp1>+J?RX{YSjcB9>7r|mg*vz@V9>{dH#x7l;;b~|Tx*m--N zJ>OnnFSHlgr`o64i|y0xGwdbyQu|E%EcR{J7*n|-l;iG8VknSHr^g?*)cm3_5+jeV_soqfH1gMFiY zlYO&&i+!ton|-@|hkd7gmwmT=kA1Jb-QHo}XYaJ{w;!+{v>&o}*}LtB?MLiK?LGEm z_T%;w_LKHg_S5z=_OteL_Ve}&_KWsQ_RIGF>{skp?bqzr?KkWw?QiUF?eFaG?H}wP?Vs$Q?O*I)?ceO*?LX|j z_Mi4&_TP5F{>LqIySd$6K?xF5s?&0nb_Xu~WTk8&UBQABB%U$70 zH|n}BxXK;w9_fy7k8(%4N4s@y%#FLA>$}=D?kIP(JH{RBj&sMm$G8*RiS8u#Sa-5J z#hvO-bB}YUyT`jT+?nnP?uqU!_aygZ_Y^nb*1HXE(oMOu-A1>`O}lg4W;f%uxUFv1 zZFA?k?QYKPaP#gwcfPy8UFa@yPjydo7rUpsXShq;rS6&TS?<~HGWQ(!Tz9#aE za?f*DyKCIF?mBn9yTLu*-RNH6ZgMxfTigrXt?otcHuqxp68BQ~GWT-#3inF)D)(yl z8uwcFI`?|_2KPqyCiiCd7WY>7HurY-4);#?F86Nt9`{~%ySu}^&)w^u@=sx7` za(BBAyN|e!x_jKm+{fJ~+$Y_q+^5}V+-KeA+~?gF+!x)K+?U<|xv#jdy05vfyKlH} zx^KB}yYIN~y6?H~yC1k8x*xe8yPvq9x}Uk9yI;6px?j0pyWhCqy5G6qyFa);x<9!; zyT7=e$;n;@RdK@KhhuJALWnqkM`^Qm>>5&-}kj|{89dBe~drYALozv zkMSq?6a7j4vHoO#ia*t#<{#%z_mB5y_%r5Y<)7!T_Sg7p{dN9&e}jL%ztO+I-{f!hxA+(OTm6gtZT`jnCH|%U zW&Y*<75UiISnDlME-z zlQ=PnOYn0zR? zE4e%QaPpDlqscwV$C8gHpGZDw=FZ-+NtT*SZCyULWn#nj*{#bb)DDlW-#&HT)bK=W zmXFPDlobq{mrtm5y$#dbWlhuBGgBA14ZDtQPrl`x*dWWHcGwL&4~P33Cgj$`lWO`B zXJW_lnt-x~YY=$2)__y3vGvYR+o_$0%WH};rFOX1J5II69kr_;WXpsW)^A+mxTAM{)@|B#JaqK>iS0w1ogPWU2sXuL*c@A6OYA7Niw)QcJBA&{_ON|y zjcwFc#IJ~75w{|4Mcj(G6>%%#R>ZA{TM@S+ZbjURxK-0RQ5JK^-0k3BEyEaf9wYV` zvB!u#M(ivcTQ_#&hLsjPJPFTJx=U#VviGhoY>>U9w+uV zvB!x$PV8}Fj}v>GdXLlIJ=(iR{2uXp#P1QmNBkb~d&KV%zeoHY@q5JY5x++0UK70`dPDSv=nc^uqBqpOq4o{6Z;0Pic5HwoI>;w> z9xflN+f&OYrdsPxoST`DHB zmL?m~(qtpr8rg`pMmD0Yk&S3;WFy)d*@(7AHlnSOrNmE(pAtVMeoFk5_$l$zu0Lf7 zJo~vNj@BrpBq=3HDM?C6Qc99il8lmMlq91h870XmNk&OBN|MnM87+~~5*hI`;%CIq zh@TNZBYsZ&ocKBMbK>X3&xxOp#)9UuBz*21t#D2Oate@BfSdy46dA0_@M@kfb2O8imcj}m{B z_+8?6iQgrDm-t=ccZuI6ewX-N;&+MPC4QIqUE+6%-z9#R_{e}P5I+z<5I+z<5I+z< z5I+z<5I+z<5I+z<5I+z<5FZ(kAp^3C_{f0_IglX-GUPyp9LSIZ8FC;)4rIuI3^|Y? z2QuV9RuO-U_8+7ENP-MWkRb^&BteEG$dCjXk|0A8WJrPxNsu84G9*DZM*YW#KTdpP zL53{IkOdjCAVU^p$bt-6kRb~)WI={3$dCmYvLHhiWXOUHS&$(MGGsx9EXa@r8L}Wl z7G%hP3>lCi12SYlh78D%0U5NPLHilBpF#T>G@n898MK{2*BNx3LDv~{ok7=G&2w4v zT-H375OxM(XApD-F=r5S1~F$4an^K=Z2UQIu+d<>Y3Xxt)M9Nul9^WUFFB*5BZnw+ zh%$#L^O2Bjk{30#n^Bg0O2FpLy|cpnM0B}B$-2!IV71wk~t)qXPFlgYPW;!suWC_!<0EpnZuMhOqs)! zIZTTg<`7~Iape$C4uRwlNDiRq0D2Cf=Ky*RpyvR34xr}%dJcf+0C*07=Ky#Pfal$& zvtxGa>|8QAEt`hsWaHPsvtu*cWN*vh&`ikc`Kg(y=?PzNo12!@uRR^tvf-=!`!O>6 z(jNH}w@eSxadl2xa>MW>bS7jRz&MDIyw@gH* zPRuW-G|@?$rlX$xf70BvAKYiLBrCaK(YF&j8^BL3sYxsa&}t9r(H^at*S{}*v_08r zKR>iyF7Ip{RH`#MHM3)4Irkd6Nd6EF{wQntrAmXFbvCzEnrW+~Z7|;3ee3-07fdgo z)p|wSH_Z;qhOI3VX{R;O4yhaQqis^J4f4Mfrz4C1H%|D$Z5NBR@_&lyxu^CPe{3Dw zez-lXyCc~?%}m6Lbn z0C-N`l>_8CK%N7*IhjySCX@rfIhjySCX|y2KPPeH;{knj{F zJO#W_knj{FJOv3)LBdmz@DwCG1qn||~dz!dOX0lyXSTLHfn@LK`D74TaDzZE0|1qnex0#J|u6eIuz z2|z&tP>=u=Bme~oKtTdfkN^}U00jv^f#5F?`~`x)K=2m`{sO^YAovRee}UjH5c~xK zzd+y@2>b$pUm)-c1b%_QFA(?z0>41u7YO_UfnOl-3j}_Fpf3>g1%kamuonpS0>NG& z*bC8^|& z_@RIw3izRb9}0wifzU4y`UOJ2K=u=@KFID z74T639~JOX0Us6cQ2`$n@KFIDmEd~`zL(&83BH%$dkMam;CBgrm*95^ewW~62~L*a zWC>1|;A9C-mf&OwPL|+g2~L*aWC>1|;A9C-mf&OwPL|+g2~L*aWC>1|;A9C-mf&Ow zPL|+g2~L*aWC>1|;A9C-mf&OwPL|+g2~L*aWC>1|;A9C-mf&IuE|%b82`-l4VhJvm z;9`k>FTuwWd@RAo5_~L~04kXPDwzN(nE)!m&l3DB!Os%>ESUf*nE)!m(-J%_!P62v zEy2?gJT1Y~5B?1*hAmc+KHv)DF)7u)7*Vq4uQwoO{awmMI2 ztMkORI!|n?^Tf6~Pi(96#I`yw9Z}pBOMJy$@rkdvD?afRcf}{ZPOOMee8pk$iLVnY z;uBvdR>UX1;&eKqI4zd?D^80~{dHm`9Z?(>OL-NC#izWA!{Sq3#bNO&ui~)ylviOxN*LLnJKcs!-2eI1DedP!7wVnIQ58`V( z_f`LkukGAd{V%?@b6@#Gd~N5x@`w1^&VAMYXaY4= zeCn_IUwrDX`d@tNuliqn>aY4g?JKW|rT)rm;!}U+HSwvx@|yV6UwKV@>aV;eKJ`~# z6QBBnOMT_Fw6DA-miWqR;uBwaO?=`juZd55Hc@s-!aC%*Dp+E-o^OZzLY ziBJ10uZd6lE3b)9`>XyJpY~Ut6QA~1o)e$;SDq7}_SgBUw6A<8mijB-iBJ8N@5HD6 z%6H;Zf8{&zslW1F+E-o@%XP{_;?pk5L*mme%0p>i`6cbEZWqh-I&m*P*Q<_C`|wL2 ze(A$6efXshzx3gkKK#;$U;6M%AAae>FMar>55M%`mp=T`hhO^eOCNse!!LdKr4PUK z;g>%A(uZIA@JkBBF5_@xiO^x>C2{L+VC`tU~|{^;xX^=V&qzgXH|C+gE0+^fO8 z8r-YFy&BxB!Mz&XtHHe*+^fO88r-YFy&BxB!Mz&XtHHe*+^fO88r-YFy&BxB!Mz&X ztHHe*+^fO88r-YFy&BxB!Mz&XtHHe*+^fO88r-YFy&BxB!Mz&XtHHe*+^fO88r-YF zy&BxB!Mz&XtHHe*+^fO88r-YFy&BxB!Mz&XtHHe*+^fO88r-YFy&BxB!Mz&XThwDU zxL1RFHMm!Udo{RMgL^f&SA%;sxL1RFHMm!Udo{RMgL^f&SA%;sxL1RFHMm!Udo{RM zgL^f&SA%;sxL1RFHMm!Udo{RMgL^f&SA%;sxL1RFHMm!Udo{RMgL^f&SA%;sxL1RF zHMm!Ud$sEPv{s!jR`IG703XbqpQ=JfVkt>N=qV@_Y3W;W+99u4M>(^_@DSmNs!z2YnH z)T;NzSKg_0UPpZ8om#)>6<>L$)_I<^)_ESW$~(2r^N6p!Q|mmB_{uxA&hv<`cvkB? zkNDIdzNq1g8osFEiyFSD;for+sNstmzNq1g8osFEiyFSD;for+sNstmzNq1g8osFE ziyFSD;for+sNstmzNq1g8osFEiyFSD;for+sNstmzNq1g8oroXKh^f*)Oy;Ve558H zsmVub@{t-oso|3vKB?i88a}BP`9#0?O>3Q3603Y&>leS`^Ze^PQ(BYX)Z{lc`AtoJ zQO-+7Nli$?jH#PZ9O@338-_+zcHTg|Vep8d*)Z{lc`AtoJQ zO-+7Nli$?jH#PZ9O@338-_+zcHTg|Vep8d*)J-~kh7QCFTce|m?4~BWsmX3?vYVRh zrY5_o$!==0n+A4oVD|=gZ(#QZc5h(!26k^?_Xc)vVD|=gZ(#QZc5h(!26k^?_Xc)v zVD|=gZ(#QZc5h(!1~zYC^9D9=VDknxZ(#EV_HJPB2KH`X?*{g6VDAR@ZeZ^Q_HJPB z2KH`X?*{g6VDAR@ZeZ^Q_HJPB2KH`X?*{g6VDAR@ZeZ^Q_HJPB2KH`X?*{g6VDAR@ zZpd01*t>zf8`!&ny&KrOfxR2px`C}5*t&tO8`!#mtsB_6ft?%Jxq+P<*tvn78`!yl zof}xWfsGqjxPg5e*tdaw8`!sjeH+-ffqfg;w}E{d*tdaw8`!sjeH+-ffqfg;w}E{d z*tdaw8`!sjeH+-ffqfg;w}E{d*tdaw8`!sjeH+-ffqfgDRZAP4RTHah*XXR8_{w$- zSx*DoHn432+cvOm1KT#RZ3Ejjux$g|Hn432+cvOm1KT#RZ3Ejjux$g|Hn432+cvOm z1KT#RZ3Ejjux$g|Hn432+cvOm1KT#RZ3Ejjux$g|Hn432+cvOm1KT#RZ3Ejjux$g| zHn432+cvOm1KT#RZ3Ejjuxta%Hn401%QmoV1IsqBYy-}n>Mg%1DiImX#<-! zuxJB|Hn3;|i#D)m1B*7WXakEjuxJB|Hn3;|i#D)m1B*7WXakEjuxJB|Hn3>JtYE{e zU;}S9@MQyEHt=NwUpDY%v&fgs1~$4%AZ>J&fGmt|m*a_TRl3-A+=^{gy4ZHyifvW8 z*jA;AZB??^R;7we8i`{lzhaey_9^!C%%%8te)qgQnC@BdMeq7PkEGV#HXH0 zHsTXc$wqwQX{ska@sxaIl|J#5e8lG=R`L;__)0$F6JN4;B1kBtu1^V?_jAyUMbtp`+AkzIT$ zr-&{-l~F_&Ukjfy!c#_g%E*;3M%w$!7Zq4-+>~LM>ikres)7}(g-unlW~nMtv0BMg zm8ke!$6!lUNoJ`kK(WMSP^78=vs4wJSgmKuU`bU0%6V?U1-9BjqG}?nXZ3-2Eaw%0~9Hlx%Bj>fL({jwnEv5!Ph`Pz| z*d#j=^?1&mE$_wU*v|=aZN=gKG&SQfWjv;g$CUAyG9FVEjajOqQLH}4sftGNDG%c| zW!$EW+mvyeGHz4GZOXV!8Mmp5#w=CQD3&H?9H)%qlyRIgj#I{Q$~aCL$0_4DWgMrB zs~)YF zPSvCNZIkVexGZHzrVPoHA(=8HQ-);9kW3kpsp>%aJflT*V3w*56w6bqI#7ItqEvOD z_&Qos)q&!7Jr$VTx_)j-p9>X(@<)b{ib3%e7g802;!_BqAyqM$r9ecgVo-bvFOesy3ACD8?ePs5X@IJkF{OvsAUAScS(_ zwW0VtPC5f4KEq2jqWB7rscJ;=X$IAZ;uBvrqWCmJuhF&?-W@y{`b3B`zb$FoSPt9g zW9L=#uYB-n7uAw-o?)h1QhcQg`6`0zK{nNt@^{)rHD#8nrW8xN zsHPO3c2P|!KJiskichbv08Lt1^s(k`U$!H_UXCJ^5${vPr()b zsO6Ri6~E`@W$N2zXYvm50X z7zUfq4TrPa{j~g;iU?GBCHm~vj?`P;9x#x#8DcX-Y-T!BBDbehI#VM4`0_dVwUU{M z?fN-*GJ^**crb&^lVvV*PN zGgI4U=I8X5)#wU6wPk*0$Mm+D3pq_Mg3xS$k~`9yAT%4GDR%3Z8MZ3>EJ|rh7$?~CAn)!?pl(&mgKG_tzFXEC9PeKjLQur zGu;(IfviLtmm|&Kf_&En^aN`kWhf5HI3ii@Vi^(9B_g^^yMqg4ZT`*+bo8@2Qtr@} zAuPFoGU(U5a)FXbsT0LA>J`jOohTN+Ekd>H2K0Z4J}=SdrOp(~2Pgr0z0BGYj4rtW zB_KO4$&O2%Di*w<1PFXtxH+g}ch?O_lv%kcA5cDF<)-+Q00fkkn}ZvaOKw03NEKMR zDG0}tK&rsfO*zklK&rsfO*yZG%F<2ob=a|VQ+y>&TX{@AD4F4YoyD$c*zq? zQo(x9jP;%w>pjKtc#u@E-c!yiWwYK>d^(4ug7uzqo~MJPg7uzqp3WhuV7;fDmjSYE z{mkr!&58;n6|DEn2J1aVYA^Y_svXvQ%HO-4rGF(lQv;<;4a}Uq^zS^H@Lnmrw|$5H zeP)Nemtb{!Apj`smC7@l7Rl`S1+EwJODF>5vgWGQxd84fh zYL}pP3D1?JBBi7v@RAf@CW?dP`>PjZmfCyy)1 zK}vFvk{qNY2Pw%xN^+1=a*+1(GiPgg$U#bSkdhpvBnK&b)v$^YF6W^m^(cFF+c5g= zSXS!nv{;^bot+jRK+^Ai#pjtL!zlY*Grje^_3~?9qQc~8X-Kk+k}RVn%P4i`TJ8lX zk!6%*86{apNtRKPWt3zYWnEeke_J)E){aD@B+)2IG)fYUl0>5<(I`nYN)nBdM583p zC`mL*5{;5Xqa@KNNi<3ljgmy8B+)2IG)fYUl0>5<(I`nYN)nAyx4sQ9vaQc35_>eS z%$z9d_9yB@pP#qWiRR{2osZ_t@Y(XtEkyrlkq#|!wD(4#lTl`#Mxm3@qT89*g zGKn)fnhnXX5U%HB(b+U5zmB$ia(ZIR;!N@=^EacTmD@5uH%KK0iO-59$BR?QqfFt9 zg0`bf;fykc(?y+id7isG&t0B|E?wHCOS^PYmoDnkMP0h6OBZ$NqAp$3rHi_BQI{_2 z(nVdms7n`h>7p)O)TN7re%gi!eE$%boea!Q1~gni!v!>4K*I$zTtLGGG+f~Ohrss_ zf!WP~gbRHC5cvKfpx}ajk1ubB(7p~NlmP)35O6_P>B)5}9RjnWfmzW&ZW;7@eYsBg zAt2uZ@+~0W0`e^&-vZfYAlnS6w}5&JsJDQ63#hk%dJD|92C~gSwiys_0r3_PZvpWZ z5N`n_3w+5D&~AY*IRe@(Fth^NEuh_ke$Sr;W_bhIYe2dMW_bhBEg;B)UHs|vbGPkj1cSLunb@;xx+8$gS|ly6|lH!$TJnDPxw`34{(Fy$MV z@(oP+2Bv%icoD#h0A2*}A~59}P-g*k77z>pbrzWN4XCq#Itxts2Gm(Vodu?R1L`cG z&H_`ufhpg>S2qE57MSu4O!)?;d;?Ryfv;`?U)==0x(R%B6VPXYuWkZg-2~<(0{Sd4 zFACLK%oT`S|F zKnn=8fItffw17Yh2(&;}9muK!S#=<*4(PLhJ`3oxfIbUM6$hq@1NtnW&jR`^pw9yO zETGQ= zEuhi@yc|$z0hJa|X#tfMmn^0sas0e}Ml3{4XhLi?0>@U%~$s{9nQU75rbp{}udS z!T%NfU%~$s{9nQU75rbp{}udS!T%NfU%~$s{9nQU75rbp{}udS!T%NfU%~$s{9nQU z75rbp{}udS!T%NfU%~$s{9nQU75rbp{}udS!T%NfU%~$s{9nQU75rbp{}udS!T%Nf zU%~$s{9nQU75rbp{}udS!T%NfU%~$s{9nQU75rbp{}udS!T%NfU%~$s{9nQU75rbp z{}udS!T%NfU%~$s{9nQU75rbp{}udS!T%NfU%~$s{9nQU75rbp{}udS!T%NfU%~$s z{9nQU75rbp{}udS!T%NfU%~$s{9nQU75rbp{}udS!T%NfU%~$s{9nQU75rbp{}udS z!T%NfU%~$s{9nQU75rbp{}udS!T%NfU%~$s{9nQU75rbp{}udSfm36Q(=o>B7~^z| z=X8vEjZv?0aAO?Y7za1T!HscnV;tNV2RFvSjd5^e9NZWOH^#w@amq7JdB!QvIJhwm zZj6H)KReYThVHc|SI!DYdRPl8lj$NqYtIlQ@s`#q2*@Y^;>TGtQ%3Ilp zPk*rsRnDue<0T`^NTx0+$8k+OjaH_}Y>%sQB8F?5mZ9(OLrb)rzkrU|+5HS_1afimxrezFP6M z1=v?BzP14SYQ@(UU|+5H+5)U_ZdlC zQsb2xuhc-L1}ZgBsewujRBE771C<)6)Ig;M8r49f8fa7ljcTA#4K%718`XHDZ9Mtr zc2IZu;#N%?NM5v~UK>bWv(tLobn zo@`4kZ_H^uZ5Me@j(U2tvMsK>Bu7tbakY8mjX8Q!18MWf>vHs@mRg%f-j&my)aH>l z=BTHI*XEJe<>*O`r_Cep%F&bBJfj+KRO5}d6&oFEKQ}toey-bZuk7~QE4%&n%5J~C zvfFP<*6p_?>-O6w==R%^$+Z3;P`6Ial z?fhMmPH*s|BVI=Sh`eTX?S_fDsZNUurZ2BzY=5J~AHC$)DO`Vce*Mf~ZOM64EBUdG zwX%X`@Mi9ngS%v-VbOPTUT?8>TK`t{7ymZ!Wx?D3_`UJPzb}_x&R8q!SM2)j6YW=w zv+|=+gF=qTR{|pi#{%~wf#dFy^Mh;BCD)HExqk8d;IoV8^Z)s5kLw5L7w_MbQKRYUnG;`+g77msCu_DHwWHbzXNWbI|LK6B7Q z!{@G@+p=MsZC}@^hwVEr+r#A-OzWxk<(utcEN{HjbM0F%+ryP?P>}VH?IW=2-1(`w z!Gg8HipciN;0X~G8d_(itQXxPtK8Pl%(Wk1(f-uO+QCct2hV}k->zx9Z?gRvo57)$ zxh|F*mxJS#vgmAbe#4HnlhbqCW+pBiJQ;%peT)98xp{dT>7X!+XZBcJ-L!Z@Ka%zS z$Crm|$t@+d{2%{+|6VWteNa>>!r0oW3)*70$2(@XdENc7c5ZIRg{iFH9_#^8M|P&j zPL;g)FR%PBZ}h*su6k{=%6^R2Tej@>-7i`9{f{q1A6p&W^U(u4_e@6jtQ-31fzd~j z=)-p(-1+ciboas0U1vofx;T2ji|%yM`|eoNd0!UYu_n4bi{85sy=NhM_d@i}h3Fj% z(c9m4ROjvYMsGVRdh0^;mW!h|FGO!z5xwyZzVpU4(Hng9`Yd|gWc1p7qgQ3oD_=3$ zdF4X%ipl8ZH?8Qr{GjM%7ue3r4vJoST=bGXq8G1?Zo7Qn&TR|Pi*DVc^P&evx9${3sxP7;E>&(#g*V)eXNp$_~yDeLH%+Pf4j+1#L z&Pf+XC$5Z67+W_U9q*!JC!=E?9CwbnDLOifj*9l^9JM-X_KfP?ZKvKR>i1T6`kSNP z_#U0!>S%n-b;kFI#@3n6n2Xk#Xx*S4PZ@gjP0^7Kx)^2~Pf)lrt3PG+M_ z`zJNg$YFbTM*7iVYxn9Lws&;cT|;a4wVkzlMQbk^I@C;d4!tRQ#35@sk2o$mL@IH} zn&^;qLl2jtKYTKJ*h5D;4;zafDg}G!NOZ86gBPNQ93CB%?AT03dq?} tcod.sdl.render.Texture: + """Render text, upload it to VRAM, then return it as an SDL Texture.""" + # Use Pillow normally to render the font. This code is standard. + width, height = font.getsize(text) + image = Image.new("RGBA", (width, height)) + draw = ImageDraw.Draw(image) + draw.text((0, 0), text, font=font) + # Push to VRAM using SDL. + texture = renderer.upload_texture(np.asarray(image)) + texture.blend_mode = tcod.sdl.render.BlendMode.BLEND # Enable alpha blending by default. + return texture + + +def main() -> None: + """Show hello world until the window is closed.""" + # Open an SDL window and renderer. + window = tcod.sdl.video.new_window(720, 480, flags=tcod.sdl.video.WindowFlags.RESIZABLE) + renderer = tcod.sdl.render.new_renderer(window, target_textures=True) + # Render the text once, then reuse the texture. + hello_world = render_text(renderer, "Hello World") + hello_world.color_mod = (64, 255, 64) # Set the color when copied. + + while True: + renderer.draw_color = (0, 0, 0, 255) + renderer.clear() + renderer.copy(hello_world, dest=(0, 0, hello_world.width, hello_world.height)) + renderer.present() + for event in tcod.event.get(): + if isinstance(event, tcod.event.Quit): + raise SystemExit() + + +if __name__ == "__main__": + main() From f2d23ce247d35665cc4880c2a5e7eb33ce217bf1 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 3 Jul 2022 11:12:51 -0700 Subject: [PATCH 075/194] Use requests package instead of urlretrieve. Fixes certificate issues, at least on Windows. --- build_sdl.py | 6 ++++-- pyproject.toml | 1 + requirements.txt | 2 ++ setup.py | 1 + 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/build_sdl.py b/build_sdl.py index 19a4d4f2..2484ec7e 100644 --- a/build_sdl.py +++ b/build_sdl.py @@ -11,9 +11,9 @@ import zipfile from pathlib import Path from typing import Any, Dict, List, Set -from urllib.request import urlretrieve import pcpp # type: ignore +import requests BITSIZE, LINKAGE = platform.architecture() @@ -97,7 +97,9 @@ def get_sdl2_file(version: str) -> Path: if not sdl2_local_file.exists(): print(f"Downloading {sdl2_remote_file}") os.makedirs("dependencies/", exist_ok=True) - urlretrieve(sdl2_remote_file, sdl2_local_file) + with requests.get(sdl2_remote_file) as response: + response.raise_for_status() + sdl2_local_file.write_bytes(response.content) return sdl2_local_file diff --git a/pyproject.toml b/pyproject.toml index a861e465..63a4c899 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,7 @@ requires = [ "cffi>=1.15", "pycparser>=2.14", "pcpp==1.30", + "requests>=2.28.1", ] build-backend = "setuptools.build_meta" diff --git a/requirements.txt b/requirements.txt index a7275c20..a6fc227e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,9 @@ cffi>=1.15 numpy>=1.21.4 pycparser>=2.14 +requests>=2.28.1 setuptools==60.8.2 +types-requests types-setuptools types-tabulate typing_extensions diff --git a/setup.py b/setup.py index c55ef0c2..95ce8f6c 100755 --- a/setup.py +++ b/setup.py @@ -115,6 +115,7 @@ def check_sdl_version() -> None: setup_requires=[ *pytest_runner, "cffi>=1.15", + "requests>=2.28.1", "pycparser>=2.14", "pcpp==1.30", ], From a737db02f1ec00a08b91270604ed4d9d5622cd12 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 3 Jul 2022 11:13:56 -0700 Subject: [PATCH 076/194] Ignore new floating point definition in SDL headers. Fixes the parsing of SDL 2.0.22 headers. --- CHANGELOG.md | 2 ++ build_sdl.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a214db1..98a03373 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] +### Fixed +- Fixed the parsing of SDL 2.0.22 headers. Specifically `SDL_FLT_EPSILON`. ## [13.6.2] - 2022-05-02 ### Fixed diff --git a/build_sdl.py b/build_sdl.py index 2484ec7e..1fc6b788 100644 --- a/build_sdl.py +++ b/build_sdl.py @@ -63,6 +63,8 @@ # Prevent double definition. "SDL_FALSE", "SDL_TRUE", + # Ignore floating point symbols. + "SDL_FLT_EPSILON", ) ) From b4112b52d0042d2b640ec7e5fa5337d4f9b9c157 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 3 Jul 2022 11:19:09 -0700 Subject: [PATCH 077/194] Update Sphinx config language option. Fixes a warning from ReadTheDocs. --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 45d78195..cb508dc3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -85,7 +85,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: From 655e4f9cc0a313c8de8f146c21dc91a54a17cbcd Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 3 Jul 2022 12:10:05 -0700 Subject: [PATCH 078/194] Remove outdated type ignores. Were fixed upstream by Numpy. --- examples/samples_tcod.py | 2 +- tcod/tileset.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/samples_tcod.py b/examples/samples_tcod.py index bc7bc0af..f92d0151 100755 --- a/examples/samples_tcod.py +++ b/examples/samples_tcod.py @@ -140,7 +140,7 @@ def interpolate_corner_colors(self) -> None: def darken_background_characters(self) -> None: # darken background characters sample_console.fg[:] = sample_console.bg[:] - sample_console.fg[:] //= 2 # type: ignore[arg-type] # https://github.com/numpy/numpy/issues/21592 + sample_console.fg[:] //= 2 def randomize_sample_conole(self) -> None: # randomize sample console characters diff --git a/tcod/tileset.py b/tcod/tileset.py index 93e8bc57..14767e5b 100644 --- a/tcod/tileset.py +++ b/tcod/tileset.py @@ -428,7 +428,7 @@ def procedural_block_elements(*, tileset: Tileset) -> None: (0x259F, 0b0111), # "▟" Quadrant upper right and lower left and lower right. ): alpha: NDArray[np.uint8] = np.asarray((quadrants & quad_mask) != 0, dtype=np.uint8) - alpha *= 255 # type: ignore[arg-type] # https://github.com/numpy/numpy/issues/21592 + alpha *= 255 tileset.set_tile(codepoint, alpha) for codepoint, axis, fraction, negative in ( From db937cdf236a9c3b44cbb194a40e7fbcd3f2528e Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 12 Jul 2022 13:57:22 -0700 Subject: [PATCH 079/194] Add tcod and libtcod to the glossary. --- .vscode/settings.json | 1 + docs/glossary.rst | 22 +++++++++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 1df05195..ffdcb8ab 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -88,6 +88,7 @@ "devel", "DHLINE", "dlopen", + "Doryen", "DTEEE", "DTEEN", "DTEES", diff --git a/docs/glossary.rst b/docs/glossary.rst index 5380de58..3cfb22a7 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -10,6 +10,17 @@ Glossary These have been deprecated since version `8.5`. + tcod + `tcod` on its own is shorthand for both :term:`libtcod` and all of its bindings including :term:`python-tcod`. + + It originated as an acronym for the game the library was first created for: + `The Chronicles Of Doryen `_ + + libtcod + This is the original C library which contains the implementations and algorithms used by C programs. + + :term:`python-tcod` includes a statically linked version of this library. + libtcod-cffi This is the `cffi` implementation of libtcodpy, the original was made using `ctypes` which was more difficult to maintain. @@ -19,8 +30,10 @@ Glossary implemented. python-tcod - `python-tcod` is a superset of the :term:`libtcodpy` API. The major - additions include class functionality in returned objects, no manual + `python-tcod` is the main Python port of :term:`libtcod`. + + Originally a superset of the :term:`libtcodpy` API. The major + additions included class functionality in returned objects, no manual memory management, pickle-able objects, and `numpy` array attributes in most objects. @@ -38,14 +51,13 @@ Glossary This left it impractical for any real use as a roguelike library. Currently no new features are planned for `tdl`, instead new features - are added to `libtcod` itself and then ported to :term:`python-tcod`. + are added to :term:`libtcod` itself and then ported to :term:`python-tcod`. :term:`python-tdl` and :term:`libtcodpy` are included in installations of `python-tcod`. libtcodpy - `libtcodpy` is more or less a direct port of `libtcod`'s C API to - Python. + :term:`libtcodpy` is more or less a direct port of :term:`libtcod`'s C API to Python. This caused a handful of issues including instances needing to be freed manually or else a memory leak would occur, and many functions performing badly in Python due to the need to call them frequently. From 7dc1179da93218629b4df461f481efe9704a8882 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 25 Jul 2022 13:08:50 -0700 Subject: [PATCH 080/194] Always use keywords in structural pattern matching. The docs were erroneously using positional arguments, but this is not supported by the current classes. Related to #120. --- tcod/event.py | 6 +++--- tcod/sdl/mouse.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tcod/event.py b/tcod/event.py index 102ade1d..3044ef95 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -68,11 +68,11 @@ raise SystemExit() case tcod.event.KeyDown(sym) if sym in KEY_COMMANDS: print(f"Command: {KEY_COMMANDS[sym]}") - case tcod.event.KeyDown(sym, scancode, mod, repeat): + case tcod.event.KeyDown(sym=sym, scancode=scancode, mod=mod, repeat=repeat): print(f"KeyDown: {sym=}, {scancode=}, {mod=}, {repeat=}") - case tcod.event.MouseButtonDown(button, pixel, tile): + case tcod.event.MouseButtonDown(button=button, pixel=pixel, tile=tile): print(f"MouseButtonDown: {button=}, {pixel=}, {tile=}") - case tcod.event.MouseMotion(pixel, pixel_motion, tile, tile_motion): + case tcod.event.MouseMotion(pixel=pixel, pixel_motion=pixel_motion, tile=tile, tile_motion=tile_motion): print(f"MouseMotion: {pixel=}, {pixel_motion=}, {tile=}, {tile_motion=}") case tcod.event.Event() as event: print(event) # Show any unhandled events. diff --git a/tcod/sdl/mouse.py b/tcod/sdl/mouse.py index a12f2bca..94794be6 100644 --- a/tcod/sdl/mouse.py +++ b/tcod/sdl/mouse.py @@ -151,12 +151,12 @@ def capture(enable: bool) -> None: # This means that dragging the mouse outside of the window will not cause an interruption in motion events. for event in tcod.event.get(): match event: - case tcod.event.MouseButtonDown(button, pixel): # Clicking the window captures the mouse. + case tcod.event.MouseButtonDown(button=button, pixel=pixel): # Clicking the window captures the mouse. tcod.sdl.mouse.capture(True) case tcod.event.MouseButtonUp(): # When all buttons are released then the mouse is released. if tcod.event.mouse.get_global_state().state == 0: tcod.sdl.mouse.capture(False) - case tcod.event.MouseMotion(pixel, pixel_motion, state): + case tcod.event.MouseMotion(pixel=pixel, pixel_motion=pixel_motion, state=state): pass # While a button is held this event is still captured outside of the window. .. seealso:: From 79c8e382f53e0665d4a6842cf2a7f853d82fb7c4 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 25 Jul 2022 14:35:22 -0700 Subject: [PATCH 081/194] Avoid Sphinx 5.1.0. This version has a regression. --- docs/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 6cdabebb..5f954b04 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,2 @@ -sphinx>=4.2 +sphinx>=5.0.2,!=5.1.0 sphinx_rtd_theme From 90d3574cb2f1b4f1e11abae976f32ddead75f5e7 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 14 Jul 2022 11:29:10 -0700 Subject: [PATCH 082/194] Workaround GitHub's broken annotated tag handing. Update action/checkout versions. --- .github/workflows/python-package.yml | 35 +++++++++++++++++----------- .github/workflows/release-on-tag.yml | 2 +- setup.py | 4 +++- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index dc00a1ae..5e305f8c 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -11,11 +11,14 @@ defaults: run: shell: bash +env: + git-depth: 0 # Depth to search for tags. + jobs: black: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install Black run: pip install black - name: Run Black @@ -24,7 +27,7 @@ jobs: isort: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install isort run: pip install isort - name: isort @@ -41,7 +44,7 @@ jobs: flake8: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install Flake8 run: pip install Flake8 - name: Flake8 @@ -53,7 +56,7 @@ jobs: mypy: runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Checkout submodules run: git submodule update --init --recursive --depth 1 - name: Install Python dependencies @@ -76,7 +79,9 @@ jobs: sdl-version: ["2.0.14", "2.0.16"] fail-fast: true steps: - - uses: actions/checkout@v1 # v1 required to build package. + - uses: actions/checkout@v3 + with: + fetch-depth: ${{ env.git-depth }} - name: Checkout submodules run: git submodule update --init --recursive --depth 1 - name: Build package @@ -101,9 +106,9 @@ jobs: fail-fast: false steps: - # v2 breaks `git describe` so v1 is needed. - # https://github.com/actions/checkout/issues/272 - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 + with: + fetch-depth: ${{ env.git-depth }} - name: Checkout submodules run: | git submodule update --init --recursive --depth 1 @@ -197,7 +202,9 @@ jobs: arch: ["x86_64", "aarch64"] build: ["cp37-manylinux*", "pp37-manylinux*"] steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 + with: + fetch-depth: ${{ env.git-depth }} - name: Set up QEMU if: ${{ matrix.arch == 'aarch64' }} uses: docker/setup-qemu-action@v1 @@ -248,13 +255,15 @@ jobs: matrix: python: ["cp38-*_universal2", "cp38-*_x86_64", "cp38-*_arm64", "pp37-*"] steps: - # v2 breaks `git describe` so v1 is needed. - # https://github.com/actions/checkout/issues/272 - - uses: actions/checkout@v1 + - uses: actions/checkout@v3 + with: + fetch-depth: ${{ env.git-depth }} - name: Checkout submodules run: git submodule update --init --recursive --depth 1 - name: Print git describe - run: git describe + # "--tags" is required to workaround actions/checkout's broken annotated tag handing. + # https://github.com/actions/checkout/issues/290 + run: git describe --tags - name: Install Python dependencies run: pip3 install wheel twine -r requirements.txt - name: Prepare package diff --git a/.github/workflows/release-on-tag.yml b/.github/workflows/release-on-tag.yml index fd5e5377..cecfbda6 100644 --- a/.github/workflows/release-on-tag.yml +++ b/.github/workflows/release-on-tag.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Generate body run: | scripts/get_release_description.py | tee release_body.md diff --git a/setup.py b/setup.py index 95ce8f6c..c358930c 100755 --- a/setup.py +++ b/setup.py @@ -19,7 +19,9 @@ def get_version() -> str: """Get the current version from a git tag, or by reading tcod/version.py""" if (SETUP_DIR / ".git").exists(): - tag = subprocess.check_output(["git", "describe", "--abbrev=0"], universal_newlines=True).strip() + # "--tags" is required to workaround actions/checkout's broken annotated tag handing. + # https://github.com/actions/checkout/issues/290 + tag = subprocess.check_output(["git", "describe", "--abbrev=0", "--tags"], universal_newlines=True).strip() assert not tag.startswith("v") version = tag From efc3f5c4b5f0abf46311db33970d11e3319aabc7 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 15 Jul 2022 16:08:33 -0700 Subject: [PATCH 083/194] Substitute versions more accurately. --- scripts/tag_release.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/tag_release.py b/scripts/tag_release.py index 1479ec82..32d47d70 100644 --- a/scripts/tag_release.py +++ b/scripts/tag_release.py @@ -54,7 +54,7 @@ def replace_unreleased_tags(tag: str, dry_run: bool) -> None: if file.suffix != ".py": continue text = file.read_text(encoding="utf-8") - new_text = re.sub(r":: unreleased", rf":: {short_tag}", text) + new_text = re.sub(r":: *unreleased", rf":: {short_tag}", text, flags=re.IGNORECASE) if text != new_text: print(f"Update tags in {file}") if not dry_run: From f59050b2f290aa26bdda9df5c24803992b39fe36 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 7 Aug 2022 18:19:57 -0700 Subject: [PATCH 084/194] Make SDLConsoleRender.atlas public. Resolves #121 Also updates related docs and fixes typos. --- .vscode/settings.json | 2 ++ CHANGELOG.md | 4 ++++ tcod/render.py | 31 +++++++++++++++++++++---------- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index ffdcb8ab..76f51720 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -223,6 +223,7 @@ "pcpp", "PILCROW", "pilmode", + "PIXELFORMAT", "PRESENTVSYNC", "PRINTF", "printn", @@ -288,6 +289,7 @@ "TCODLIB", "TEEE", "TEEW", + "TEXTUREACCESS", "thirdparty", "Tileset", "tilesets", diff --git a/CHANGELOG.md b/CHANGELOG.md index 98a03373..880a3d5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] +### Added +- You can new use `SDLConsoleRender.atlas` to access the `SDLTilesetAtlas` used to create it. + [#121](https://github.com/libtcod/python-tcod/issues/121) + ### Fixed - Fixed the parsing of SDL 2.0.22 headers. Specifically `SDL_FLT_EPSILON`. diff --git a/tcod/render.py b/tcod/render.py index 79dc5060..a8aad4d5 100644 --- a/tcod/render.py +++ b/tcod/render.py @@ -1,12 +1,12 @@ """Handles the rendering of libtcod's tilesets. Using this module you can render a console to an SDL :any:`Texture` directly, letting you have full control over how -conoles are displayed. +consoles are displayed. This includes rendering multiple tilesets in a single frame and rendering consoles on top of each other. Example:: - tileset = tcod.tileset.load_tilsheet("dejavu16x16_gs_tc.png", 32, 8, tcod.tileset.CHARMAP_TCOD) + tileset = tcod.tileset.load_tilesheet("dejavu16x16_gs_tc.png", 32, 8, tcod.tileset.CHARMAP_TCOD) console = tcod.Console(20, 8) console.print(0, 0, "Hello World") sdl_window = tcod.sdl.video.new_window( @@ -31,6 +31,8 @@ from typing import Any, Optional +from typing_extensions import Final + import tcod.console import tcod.sdl.render import tcod.tileset @@ -43,15 +45,20 @@ class SDLTilesetAtlas: def __init__(self, renderer: tcod.sdl.render.Renderer, tileset: tcod.tileset.Tileset) -> None: self._renderer = renderer - self.tileset = tileset - self.p = ffi.gc(_check_p(lib.TCOD_sdl2_atlas_new(renderer.p, tileset._tileset_p)), lib.TCOD_sdl2_atlas_delete) + self.tileset: Final[tcod.tileset.Tileset] = tileset + """The tileset used to create this SDLTilesetAtlas.""" + self.p: Final = ffi.gc( + _check_p(lib.TCOD_sdl2_atlas_new(renderer.p, tileset._tileset_p)), lib.TCOD_sdl2_atlas_delete + ) @classmethod def _from_ref(cls, renderer_p: Any, atlas_p: Any) -> SDLTilesetAtlas: self = object.__new__(cls) + # Ignore Final reassignment type errors since this is an alternative constructor. + # This could be a sign that the current constructor was badly implemented. self._renderer = tcod.sdl.render.Renderer(renderer_p) - self.tileset = tcod.tileset.Tileset._from_ref(atlas_p.tileset) - self.p = atlas_p + self.tileset = tcod.tileset.Tileset._from_ref(atlas_p.tileset) # type: ignore[misc] + self.p = atlas_p # type: ignore[misc] return self @@ -59,7 +66,11 @@ class SDLConsoleRender: """Holds an internal cache console and texture which are used to optimized console rendering.""" def __init__(self, atlas: SDLTilesetAtlas) -> None: - self._atlas = atlas + self.atlas: Final[SDLTilesetAtlas] = atlas + """The SDLTilesetAtlas used to create this SDLConsoleRender. + + .. versionadded:: Unreleased + """ self._renderer = atlas._renderer self._cache_console: Optional[tcod.console.Console] = None self._texture: Optional[tcod.sdl.render.Texture] = None @@ -80,8 +91,8 @@ def render(self, console: tcod.console.Console) -> tcod.sdl.render.Texture: if self._cache_console is None or self._texture is None: self._cache_console = tcod.console.Console(console.width, console.height) self._texture = self._renderer.new_texture( - self._atlas.tileset.tile_width * console.width, - self._atlas.tileset.tile_height * console.height, + self.atlas.tileset.tile_width * console.width, + self.atlas.tileset.tile_height * console.height, format=int(lib.SDL_PIXELFORMAT_RGBA32), access=int(lib.SDL_TEXTUREACCESS_TARGET), ) @@ -89,7 +100,7 @@ def render(self, console: tcod.console.Console) -> tcod.sdl.render.Texture: with self._renderer.set_render_target(self._texture): _check( lib.TCOD_sdl2_render_texture( - self._atlas.p, console.console_c, self._cache_console.console_c, self._texture.p + self.atlas.p, console.console_c, self._cache_console.console_c, self._texture.p ) ) return self._texture From 8d9c72a99bb387a939b7f817722431a76fca493f Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 7 Aug 2022 19:32:53 -0700 Subject: [PATCH 085/194] Prepare 13.7.0 release. --- CHANGELOG.md | 2 ++ tcod/render.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 880a3d5b..129c8776 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] + +## [13.7.0] - 2022-08-07 ### Added - You can new use `SDLConsoleRender.atlas` to access the `SDLTilesetAtlas` used to create it. [#121](https://github.com/libtcod/python-tcod/issues/121) diff --git a/tcod/render.py b/tcod/render.py index a8aad4d5..e604d29f 100644 --- a/tcod/render.py +++ b/tcod/render.py @@ -69,7 +69,7 @@ def __init__(self, atlas: SDLTilesetAtlas) -> None: self.atlas: Final[SDLTilesetAtlas] = atlas """The SDLTilesetAtlas used to create this SDLConsoleRender. - .. versionadded:: Unreleased + .. versionadded:: 13.7 """ self._renderer = atlas._renderer self._cache_console: Optional[tcod.console.Console] = None From d5f8663a599dc0117aadd297c718c7b93fd881d5 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 21 Jul 2022 15:58:03 -0700 Subject: [PATCH 086/194] Add basic joystick port of SDL2. --- CHANGELOG.md | 3 + docs/index.rst | 1 + docs/sdl/joystick.rst | 6 ++ examples/eventget.py | 3 + tcod/event.py | 244 +++++++++++++++++++++++++++++++++++++++++- tcod/sdl/joystick.py | 119 ++++++++++++++++++++ 6 files changed, 375 insertions(+), 1 deletion(-) create mode 100644 docs/sdl/joystick.rst create mode 100644 tcod/sdl/joystick.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 129c8776..eef28017 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] +### Added +- Ported SDL2 joystick handing as `tcod.sdl.joystick`. +- New joystick related events. ## [13.7.0] - 2022-08-07 ### Added diff --git a/docs/index.rst b/docs/index.rst index 981ce6bf..7a138378 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -40,6 +40,7 @@ Contents: tcod/tileset libtcodpy sdl/audio + sdl/joystick sdl/render sdl/mouse sdl/video diff --git a/docs/sdl/joystick.rst b/docs/sdl/joystick.rst new file mode 100644 index 00000000..81628124 --- /dev/null +++ b/docs/sdl/joystick.rst @@ -0,0 +1,6 @@ +tcod.sdl.joystick - SDL Joystick Support +======================================== + +.. automodule:: tcod.sdl.joystick + :members: + :member-order: bysource diff --git a/examples/eventget.py b/examples/eventget.py index 55d6a58b..bb3534aa 100755 --- a/examples/eventget.py +++ b/examples/eventget.py @@ -8,6 +8,8 @@ from typing import List import tcod +import tcod.sdl.joystick +import tcod.sdl.sys WIDTH, HEIGHT = 720, 480 @@ -17,6 +19,7 @@ def main() -> None: event_log: List[str] = [] motion_desc = "" + joysticks = tcod.sdl.joystick.get_joysticks() with tcod.context.new(width=WIDTH, height=HEIGHT) as context: console = context.new_console() diff --git a/tcod/event.py b/tcod/event.py index 3044ef95..bd332c97 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -93,6 +93,7 @@ from tcod.event_constants import * # noqa: F4 from tcod.event_constants import KMOD_ALT, KMOD_CTRL, KMOD_GUI, KMOD_SHIFT from tcod.loader import ffi, lib +from tcod.sdl.joystick import _HAT_DIRECTIONS T = TypeVar("T") @@ -303,7 +304,7 @@ def from_sdl_event(cls, sdl_event: Any) -> Event: raise NotImplementedError() def __str__(self) -> str: - return "" % (self.type,) + return f"" class Quit(Event): @@ -779,6 +780,199 @@ def __str__(self) -> str: ) +class JoystickEvent(Event): + """A base class for joystick events. + + .. versionadded:: Unreleased + """ + + def __init__(self, type: str, which: int): + super().__init__(type) + self.which = which + """The ID of the joystick this event is for.""" + + def __repr__(self) -> str: + return f"tcod.event.{self.__class__.__name__}" f"(type={self.type!r}, which={self.which})" + + def __str__(self) -> str: + prefix = super().__str__().strip("<>") + return f"<{prefix}, which={self.which}>" + + +class JoystickAxis(JoystickEvent): + """When a joystick axis changes in value. + + .. versionadded:: Unreleased + + .. seealso:: + :any:`tcod.sdl.joystick` + """ + + which: int + """The ID of the joystick this event is for.""" + + def __init__(self, type: str, which: int, axis: int, value: int): + super().__init__(type, which) + self.axis = axis + """The index of the changed axis.""" + self.value = value + """The raw value of the axis in the range -32768 to 32767.""" + + @classmethod + def from_sdl_event(cls, sdl_event: Any) -> JoystickAxis: + return cls("JOYAXISMOTION", sdl_event.jaxis.which, sdl_event.jaxis.axis, sdl_event.jaxis.value) + + def __repr__(self) -> str: + return ( + f"tcod.event.{self.__class__.__name__}" + f"(type={self.type!r}, which={self.which}, axis={self.axis}, value={self.value})" + ) + + def __str__(self) -> str: + prefix = super().__str__().strip("<>") + return f"<{prefix}, axis={self.axis}, value={self.value}>" + + +class JoystickBall(JoystickEvent): + """When a joystick ball is moved. + + .. versionadded:: Unreleased + + .. seealso:: + :any:`tcod.sdl.joystick` + """ + + which: int + """The ID of the joystick this event is for.""" + + def __init__(self, type: str, which: int, ball: int, dx: int, dy: int): + super().__init__(type, which) + self.ball = ball + """The index of the moved ball.""" + self.dx = dx + """The X motion of the ball.""" + self.dy = dy + """The Y motion of the ball.""" + + @classmethod + def from_sdl_event(cls, sdl_event: Any) -> JoystickBall: + return cls( + "JOYBALLMOTION", sdl_event.jball.which, sdl_event.jball.ball, sdl_event.jball.xrel, sdl_event.jball.yrel + ) + + def __repr__(self) -> str: + return ( + f"tcod.event.{self.__class__.__name__}" + f"(type={self.type!r}, which={self.which}, ball={self.ball}, dx={self.dx}, dy={self.dy})" + ) + + def __str__(self) -> str: + prefix = super().__str__().strip("<>") + return f"<{prefix}, ball={self.ball}, dx={self.dx}, dy={self.dy}>" + + +class JoystickHat(JoystickEvent): + """When a joystick hat changes direction. + + .. versionadded:: Unreleased + + .. seealso:: + :any:`tcod.sdl.joystick` + """ + + which: int + """The ID of the joystick this event is for.""" + + def __init__(self, type: str, which: int, x: Literal[-1, 0, 1], y: Literal[-1, 0, 1]): + super().__init__(type, which) + self.x = x + """The new X direction of the hat.""" + self.y = y + """The new Y direction of the hat.""" + + @classmethod + def from_sdl_event(cls, sdl_event: Any) -> JoystickHat: + return cls("JOYHATMOTION", sdl_event.jhat.which, *_HAT_DIRECTIONS[sdl_event.jhat.hat]) + + def __repr__(self) -> str: + return ( + f"tcod.event.{self.__class__.__name__}" f"(type={self.type!r}, which={self.which}, x={self.x}, y={self.y})" + ) + + def __str__(self) -> str: + prefix = super().__str__().strip("<>") + return f"<{prefix}, x={self.x}, y={self.y}>" + + +class JoystickButton(JoystickEvent): + """When a joystick button is pressed or released. + + .. versionadded:: Unreleased + + Example:: + + for event in tcod.event.get(): + match event: + case JoystickButton(which=which, button=button, pressed=True): + print(f"Pressed {button=} on controller {which}.") + case JoystickButton(which=which, button=button, pressed=False): + print(f"Released {button=} on controller {which}.") + """ + + which: int + """The ID of the joystick this event is for.""" + + def __init__(self, type: str, which: int, button: int): + super().__init__(type, which) + self.button = button + """The index of the button this event is for.""" + + @property + def pressed(self) -> bool: + """True if the joystick button has been pressed, False when the button was released.""" + return self.type == "JOYBUTTONDOWN" + + @classmethod + def from_sdl_event(cls, sdl_event: Any) -> JoystickButton: + type = {lib.SDL_JOYBUTTONDOWN: "JOYBUTTONDOWN", lib.SDL_JOYBUTTONUP: "JOYBUTTONUP"}[sdl_event.type] + return cls(type, sdl_event.jbutton.which, sdl_event.jbutton.button) + + def __repr__(self) -> str: + return f"tcod.event.{self.__class__.__name__}" f"(type={self.type!r}, which={self.which}, button={self.button})" + + def __str__(self) -> str: + prefix = super().__str__().strip("<>") + return f"<{prefix}, button={self.button}>" + + +class JoystickDevice(JoystickEvent): + """An event for when a joystick is added or removed. + + .. versionadded:: Unreleased + + Example:: + + joysticks: dict[int, tcod.sdl.joystick.Joystick] = {} + for event in tcod.event.get(): + match event: + case tcod.event.JoystickDevice(type="JOYDEVICEADDED", which=device_id): + new_joystick = tcod.sdl.joystick.Joystick(device_id) + joysticks[new_joystick.id] = new_joystick + case tcod.event.JoystickDevice(type="JOYDEVICEREMOVED", which=which): + del joysticks[which] + """ + + which: int + """When type="JOYDEVICEADDED" this is the device ID. + When type="JOYDEVICEREMOVED" this is the instance ID. + """ + + @classmethod + def from_sdl_event(cls, sdl_event: Any) -> JoystickDevice: + type = {lib.SDL_JOYDEVICEADDED: "JOYDEVICEADDED", lib.SDL_JOYDEVICEREMOVED: "JOYDEVICEREMOVED"}[sdl_event.type] + return cls(type, sdl_event.jdevice.which) + + class Undefined(Event): """This class is a place holder for SDL events without their own tcod.event class. @@ -809,6 +1003,13 @@ def __str__(self) -> str: lib.SDL_MOUSEWHEEL: MouseWheel, lib.SDL_TEXTINPUT: TextInput, lib.SDL_WINDOWEVENT: WindowEvent, + lib.SDL_JOYAXISMOTION: JoystickAxis, + lib.SDL_JOYBALLMOTION: JoystickBall, + lib.SDL_JOYHATMOTION: JoystickHat, + lib.SDL_JOYBUTTONDOWN: JoystickButton, + lib.SDL_JOYBUTTONUP: JoystickButton, + lib.SDL_JOYDEVICEADDED: JoystickDevice, + lib.SDL_JOYDEVICEREMOVED: JoystickDevice, } @@ -1076,6 +1277,41 @@ def ev_windowtakefocus(self, event: tcod.event.WindowEvent) -> Optional[T]: def ev_windowhittest(self, event: tcod.event.WindowEvent) -> Optional[T]: pass + def ev_joyaxismotion(self, event: tcod.event.JoystickAxis) -> Optional[T]: + """ + .. versionadded:: Unreleased + """ + + def ev_joyballmotion(self, event: tcod.event.JoystickBall) -> Optional[T]: + """ + .. versionadded:: Unreleased + """ + + def ev_joyhatmotion(self, event: tcod.event.JoystickHat) -> Optional[T]: + """ + .. versionadded:: Unreleased + """ + + def ev_joybuttondown(self, event: tcod.event.JoystickButton) -> Optional[T]: + """ + .. versionadded:: Unreleased + """ + + def ev_joybuttonup(self, event: tcod.event.JoystickButton) -> Optional[T]: + """ + .. versionadded:: Unreleased + """ + + def ev_joydeviceadded(self, event: tcod.event.JoystickDevice) -> Optional[T]: + """ + .. versionadded:: Unreleased + """ + + def ev_joydeviceremoved(self, event: tcod.event.JoystickDevice) -> Optional[T]: + """ + .. versionadded:: Unreleased + """ + def ev_(self, event: Any) -> Optional[T]: pass @@ -2326,6 +2562,12 @@ def __repr__(self) -> str: "WindowEvent", "WindowMoved", "WindowResized", + "JoystickEvent", + "JoystickAxis", + "JoystickBall", + "JoystickHat", + "JoystickButton", + "JoystickDevice", "Undefined", "get", "wait", diff --git a/tcod/sdl/joystick.py b/tcod/sdl/joystick.py new file mode 100644 index 00000000..a2001cf4 --- /dev/null +++ b/tcod/sdl/joystick.py @@ -0,0 +1,119 @@ +"""SDL Joystick Support + +.. versionadded:: Unreleased +""" +from __future__ import annotations + +import enum +from typing import Dict, List, Optional, Tuple + +from typing_extensions import Final, Literal + +import tcod.sdl.sys +from tcod.loader import ffi, lib +from tcod.sdl import _check, _check_p + +_HAT_DIRECTIONS: Dict[int, Tuple[Literal[-1, 0, 1], Literal[-1, 0, 1]]] = { + lib.SDL_HAT_CENTERED or 0: (0, 0), + lib.SDL_HAT_UP or 0: (0, -1), + lib.SDL_HAT_RIGHT or 0: (1, 0), + lib.SDL_HAT_DOWN or 0: (0, 1), + lib.SDL_HAT_LEFT or 0: (-1, 0), + lib.SDL_HAT_RIGHTUP or 0: (1, -1), + lib.SDL_HAT_RIGHTDOWN or 0: (1, 1), + lib.SDL_HAT_LEFTUP or 0: (-1, -1), + lib.SDL_HAT_LEFTDOWN or 0: (-1, 1), +} + + +class Power(enum.IntEnum): + """The possible power states of a controller. + + .. seealso:: + :any:`Joystick.get_current_power` + """ + + UNKNOWN = lib.SDL_JOYSTICK_POWER_UNKNOWN or -1 + """Power state is unknown.""" + EMPTY = lib.SDL_JOYSTICK_POWER_EMPTY or 0 + """<= 5% power.""" + LOW = lib.SDL_JOYSTICK_POWER_LOW or 1 + """<= 20% power.""" + MEDIUM = lib.SDL_JOYSTICK_POWER_MEDIUM or 2 + """<= 70% power.""" + FULL = lib.SDL_JOYSTICK_POWER_FULL or 3 + """<= 100% power.""" + WIRED = lib.SDL_JOYSTICK_POWER_WIRED or 4 + """""" + MAX = lib.SDL_JOYSTICK_POWER_MAX or 5 + """""" + + +class Joystick: + """An SDL joystick. + + .. seealso:: + https://wiki.libsdl.org/CategoryJoystick + """ + + def __init__(self, device_index: int): + tcod.sdl.sys.init(tcod.sdl.sys.Subsystem.JOYSTICK) + self.sdl_joystick_p: Final = _check_p(ffi.gc(lib.SDL_JoystickOpen(device_index), lib.SDL_JoystickClose)) + self.axes: Final = _check(lib.SDL_JoystickNumAxes(self.sdl_joystick_p)) + self.balls: Final = _check(lib.SDL_JoystickNumBalls(self.sdl_joystick_p)) + self.buttons: Final = _check(lib.SDL_JoystickNumButtons(self.sdl_joystick_p)) + self.hats: Final = _check(lib.SDL_JoystickNumHats(self.sdl_joystick_p)) + self.name: Final = str(ffi.string(lib.SDL_JoystickName(self.sdl_joystick_p)), encoding="utf-8") + """The name of this joystick.""" + self.guid: Final = self._get_guid() + """The GUID of this joystick.""" + self.id: Final = _check(lib.SDL_JoystickInstanceID(self.sdl_joystick_p)) + """The instance ID of this joystick. This is not the same as the device ID.""" + + def _get_guid(self) -> str: + guid_str = ffi.new("char[33]") + lib.SDL_JoystickGetGUIDString(lib.SDL_JoystickGetGUID(self.sdl_joystick_p), guid_str, len(guid_str)) + return str(tcod.ffi.string(guid_str), encoding="utf-8") + + def get_current_power(self) -> Power: + """Return the power level/state of this joystick. See :any:`Power`.""" + return Power(lib.SDL_JoystickCurrentPowerLevel(self.sdl_joystick_p)) + + def get_axis(self, axis: int) -> int: + """Return the raw value of `axis` in the range -32768 to 32767.""" + return int(lib.SDL_JoystickGetAxis(self.sdl_joystick_p, axis)) + + def get_ball(self, ball: int) -> Tuple[int, int]: + """Return the values (delta_x, delta_y) of `ball` since the last poll.""" + xy = ffi.new("int[2]") + _check(lib.SDL_JoystickGetBall(ball, xy, xy + 1)) + return int(xy[0]), int(xy[1]) + + def get_button(self, button: int) -> bool: + """Return True if `button` is pressed.""" + return bool(lib.SDL_JoystickGetButton(self.sdl_joystick_p, button)) + + def get_hat(self, hat: int) -> Tuple[Literal[-1, 0, 1], Literal[-1, 0, 1]]: + """Return the direction of `hat` as (x, y). With (-1, -1) being in the upper-left.""" + return _HAT_DIRECTIONS[lib.SDL_JoystickGetHat(self.sdl_joystick_p, hat)] + + +def get_number() -> int: + """Return the number of attached joysticks.""" + tcod.sdl.sys.init(tcod.sdl.sys.Subsystem.JOYSTICK) + return _check(lib.SDL_NumJoysticks()) + + +def get_joysticks() -> List[Joystick]: + """Return a list of all connected joystick devices.""" + return [Joystick(i) for i in range(get_number())] + + +def event_state(new_state: Optional[bool] = None) -> bool: + """Check or set joystick event polling. + + .. seealso:: + https://wiki.libsdl.org/SDL_JoystickEventState + """ + _OPTIONS = {None: lib.SDL_QUERY, False: lib.SDL_IGNORE, True: lib.SDL_ENABLE} + return bool(_check(lib.SDL_JoystickEventState(_OPTIONS[new_state]))) From 9a9fa27718f75d966abafd78652e90dcefa5a6fe Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 21 Jul 2022 23:32:56 -0700 Subject: [PATCH 087/194] Clean up docs. --- docs/sdl/joystick.rst | 5 +++++ tcod/sdl/joystick.py | 19 ++++++++++++------- tcod/sdl/video.py | 2 +- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/docs/sdl/joystick.rst b/docs/sdl/joystick.rst index 81628124..e1b22ca2 100644 --- a/docs/sdl/joystick.rst +++ b/docs/sdl/joystick.rst @@ -2,5 +2,10 @@ tcod.sdl.joystick - SDL Joystick Support ======================================== .. automodule:: tcod.sdl.joystick + :members: + :exclude-members: + Power + +.. autoclass:: tcod.sdl.joystick.Power :members: :member-order: bysource diff --git a/tcod/sdl/joystick.py b/tcod/sdl/joystick.py index a2001cf4..e3abb8b6 100644 --- a/tcod/sdl/joystick.py +++ b/tcod/sdl/joystick.py @@ -59,15 +59,20 @@ class Joystick: def __init__(self, device_index: int): tcod.sdl.sys.init(tcod.sdl.sys.Subsystem.JOYSTICK) self.sdl_joystick_p: Final = _check_p(ffi.gc(lib.SDL_JoystickOpen(device_index), lib.SDL_JoystickClose)) - self.axes: Final = _check(lib.SDL_JoystickNumAxes(self.sdl_joystick_p)) - self.balls: Final = _check(lib.SDL_JoystickNumBalls(self.sdl_joystick_p)) - self.buttons: Final = _check(lib.SDL_JoystickNumButtons(self.sdl_joystick_p)) - self.hats: Final = _check(lib.SDL_JoystickNumHats(self.sdl_joystick_p)) - self.name: Final = str(ffi.string(lib.SDL_JoystickName(self.sdl_joystick_p)), encoding="utf-8") + """The CFFI pointer to an SDL_Joystick struct.""" + self.axes: Final[int] = _check(lib.SDL_JoystickNumAxes(self.sdl_joystick_p)) + """The total number of axes.""" + self.balls: Final[int] = _check(lib.SDL_JoystickNumBalls(self.sdl_joystick_p)) + """The total number of trackballs.""" + self.buttons: Final[int] = _check(lib.SDL_JoystickNumButtons(self.sdl_joystick_p)) + """The total number of buttons.""" + self.hats: Final[int] = _check(lib.SDL_JoystickNumHats(self.sdl_joystick_p)) + """The total number of hats.""" + self.name: Final[str] = str(ffi.string(lib.SDL_JoystickName(self.sdl_joystick_p)), encoding="utf-8") """The name of this joystick.""" - self.guid: Final = self._get_guid() + self.guid: Final[str] = self._get_guid() """The GUID of this joystick.""" - self.id: Final = _check(lib.SDL_JoystickInstanceID(self.sdl_joystick_p)) + self.id: Final[int] = _check(lib.SDL_JoystickInstanceID(self.sdl_joystick_p)) """The instance ID of this joystick. This is not the same as the device ID.""" def _get_guid(self) -> str: diff --git a/tcod/sdl/video.py b/tcod/sdl/video.py index 7c65a374..2ef4553e 100644 --- a/tcod/sdl/video.py +++ b/tcod/sdl/video.py @@ -87,7 +87,7 @@ class FlashOperation(enum.IntEnum): CANCEL = 0 """Stop flashing.""" BRIEFLY = 1 - """Flash breifly.""" + """Flash briefly.""" UNTIL_FOCUSED = 2 """Flash until focus is gained.""" From 6e71a35b91349fef367448844ea830a5e4a9b88e Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 25 Jul 2022 14:23:25 -0700 Subject: [PATCH 088/194] Update event type attributes. --- tcod/event.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tcod/event.py b/tcod/event.py index bd332c97..6caec3e7 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -656,7 +656,7 @@ class WindowEvent(Event): type (str): A window event could mean various event types. """ - type: Final[ # type: ignore[misc] # Narrowing contant type. + type: Final[ # type: ignore[misc] # Narrowing final type. Literal[ "WindowShown", "WindowHidden", @@ -726,7 +726,7 @@ class WindowMoved(WindowEvent): y (int): Movement on the y-axis. """ - type: Literal["WINDOWMOVED"] # type: ignore[assignment,misc] + type: Final[Literal["WINDOWMOVED"]] # type: ignore[assignment,misc] def __init__(self, x: int, y: int) -> None: super().__init__(None) @@ -757,7 +757,7 @@ class WindowResized(WindowEvent): height (int): The current height of the window. """ - type: Literal["WINDOWRESIZED", "WINDOWSIZECHANGED"] # type: ignore[assignment,misc] + type: Final[Literal["WINDOWRESIZED", "WINDOWSIZECHANGED"]] # type: ignore[assignment,misc] def __init__(self, type: str, width: int, height: int) -> None: super().__init__(type) @@ -962,6 +962,8 @@ class JoystickDevice(JoystickEvent): del joysticks[which] """ + type = Final[Literal["JOYDEVICEADDED", "JOYDEVICEREMOVED"]] # type: ignore[assignment,misc] + which: int """When type="JOYDEVICEADDED" this is the device ID. When type="JOYDEVICEREMOVED" this is the instance ID. From a0602f8d656b6c5c6ea62981b3efbfdca8709253 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 11 Sep 2022 14:44:06 -0700 Subject: [PATCH 089/194] Add GameController class. --- tcod/sdl/joystick.py | 289 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 280 insertions(+), 9 deletions(-) diff --git a/tcod/sdl/joystick.py b/tcod/sdl/joystick.py index e3abb8b6..7678e36d 100644 --- a/tcod/sdl/joystick.py +++ b/tcod/sdl/joystick.py @@ -5,7 +5,7 @@ from __future__ import annotations import enum -from typing import Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple, Union from typing_extensions import Final, Literal @@ -26,6 +26,72 @@ } +class ControllerAxis(enum.IntEnum): + """The standard axes for a game controller.""" + + INVALID = lib.SDL_CONTROLLER_AXIS_INVALID or -1 + LEFTX = lib.SDL_CONTROLLER_AXIS_LEFTX or 0 + """""" + LEFTY = lib.SDL_CONTROLLER_AXIS_LEFTY or 1 + """""" + RIGHTX = lib.SDL_CONTROLLER_AXIS_RIGHTX or 2 + """""" + RIGHTY = lib.SDL_CONTROLLER_AXIS_RIGHTY or 3 + """""" + TRIGGERLEFT = lib.SDL_CONTROLLER_AXIS_TRIGGERLEFT or 4 + """""" + TRIGGERRIGHT = lib.SDL_CONTROLLER_AXIS_TRIGGERRIGHT or 5 + """""" + + +class ControllerButton(enum.IntEnum): + """The standard buttons for a game controller.""" + + INVALID = lib.SDL_CONTROLLER_BUTTON_INVALID or -1 + A = lib.SDL_CONTROLLER_BUTTON_A or 0 + """""" + B = lib.SDL_CONTROLLER_BUTTON_B or 1 + """""" + X = lib.SDL_CONTROLLER_BUTTON_X or 2 + """""" + Y = lib.SDL_CONTROLLER_BUTTON_Y or 3 + """""" + BACK = lib.SDL_CONTROLLER_BUTTON_BACK or 4 + """""" + GUIDE = lib.SDL_CONTROLLER_BUTTON_GUIDE or 5 + """""" + START = lib.SDL_CONTROLLER_BUTTON_START or 6 + """""" + LEFTSTICK = lib.SDL_CONTROLLER_BUTTON_LEFTSTICK or 7 + """""" + RIGHTSTICK = lib.SDL_CONTROLLER_BUTTON_RIGHTSTICK or 8 + """""" + LEFTSHOULDER = lib.SDL_CONTROLLER_BUTTON_LEFTSHOULDER or 9 + """""" + RIGHTSHOULDER = lib.SDL_CONTROLLER_BUTTON_RIGHTSHOULDER or 10 + """""" + DPAD_UP = lib.SDL_CONTROLLER_BUTTON_DPAD_UP or 11 + """""" + DPAD_DOWN = lib.SDL_CONTROLLER_BUTTON_DPAD_DOWN or 12 + """""" + DPAD_LEFT = lib.SDL_CONTROLLER_BUTTON_DPAD_LEFT or 13 + """""" + DPAD_RIGHT = lib.SDL_CONTROLLER_BUTTON_DPAD_RIGHT or 14 + """""" + MISC1 = 15 + """""" + PADDLE1 = 16 + """""" + PADDLE2 = 17 + """""" + PADDLE3 = 18 + """""" + PADDLE4 = 19 + """""" + TOUCHPAD = 20 + """""" + + class Power(enum.IntEnum): """The possible power states of a controller. @@ -50,15 +116,14 @@ class Power(enum.IntEnum): class Joystick: - """An SDL joystick. + """A low-level SDL joystick. .. seealso:: https://wiki.libsdl.org/CategoryJoystick """ - def __init__(self, device_index: int): - tcod.sdl.sys.init(tcod.sdl.sys.Subsystem.JOYSTICK) - self.sdl_joystick_p: Final = _check_p(ffi.gc(lib.SDL_JoystickOpen(device_index), lib.SDL_JoystickClose)) + def __init__(self, sdl_joystick_p: Any): + self.sdl_joystick_p: Final = sdl_joystick_p """The CFFI pointer to an SDL_Joystick struct.""" self.axes: Final[int] = _check(lib.SDL_JoystickNumAxes(self.sdl_joystick_p)) """The total number of axes.""" @@ -74,6 +139,24 @@ def __init__(self, device_index: int): """The GUID of this joystick.""" self.id: Final[int] = _check(lib.SDL_JoystickInstanceID(self.sdl_joystick_p)) """The instance ID of this joystick. This is not the same as the device ID.""" + self._keep_alive: Any = None + """The owner of this objects memory if this object does not own itself.""" + + @classmethod + def _open(cls, device_index: int) -> Joystick: + tcod.sdl.sys.init(tcod.sdl.sys.Subsystem.JOYSTICK) + p = _check_p(ffi.gc(lib.SDL_JoystickOpen(device_index), lib.SDL_JoystickClose)) + return cls(p) + + def __eq__(self, other: object) -> bool: + if isinstance(other, GameController): + return self == other.joystick.id + if isinstance(other, Joystick): + return self.id == other.id + return NotImplemented + + def __hash__(self) -> int: + return hash(self.id) def _get_guid(self) -> str: guid_str = ffi.new("char[33]") @@ -95,7 +178,7 @@ def get_ball(self, ball: int) -> Tuple[int, int]: return int(xy[0]), int(xy[1]) def get_button(self, button: int) -> bool: - """Return True if `button` is pressed.""" + """Return True if `button` is currently held.""" return bool(lib.SDL_JoystickGetButton(self.sdl_joystick_p, button)) def get_hat(self, hat: int) -> Tuple[Literal[-1, 0, 1], Literal[-1, 0, 1]]: @@ -103,7 +186,168 @@ def get_hat(self, hat: int) -> Tuple[Literal[-1, 0, 1], Literal[-1, 0, 1]]: return _HAT_DIRECTIONS[lib.SDL_JoystickGetHat(self.sdl_joystick_p, hat)] -def get_number() -> int: +class GameController: + """A standard interface for an Xbox 360 style game controller.""" + + def __init__(self, sdl_controller_p: Any): + self.sdl_controller_p: Final = sdl_controller_p + self.joystick: Final = Joystick(lib.SDL_GameControllerGetJoystick(self.sdl_controller_p)) + """The :any:`Joystick` associated with this controller.""" + self.joystick._keep_alive = self.sdl_controller_p # This objects real owner needs to be kept alive. + + @classmethod + def _open(cls, joystick_index: int) -> GameController: + return cls(_check_p(ffi.gc(lib.SDL_GameControllerOpen(joystick_index), lib.SDL_GameControllerClose))) + + def get_button(self, button: ControllerButton) -> bool: + """Return True if `button` is currently held.""" + return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, button)) + + def get_axis(self, axis: ControllerAxis) -> int: + """Return the state of the given `axis`. + + The state is usually a value from -32768 to 32767, with positive values towards the lower-right direction. + Triggers have the range of 0 to 32767 instead. + """ + return int(lib.SDL_GameControllerGetAxis(self.sdl_controller_p, axis)) + + def __eq__(self, other: object) -> bool: + if isinstance(other, GameController): + return self.joystick.id == other.joystick.id + if isinstance(other, Joystick): + return self.joystick.id == other.id + return NotImplemented + + def __hash__(self) -> int: + return hash(self.joystick.id) + + # These could exist as convenience functions, but the get_X functions are probably better. + @property + def _left_x(self) -> int: + "Return the position of this axis. (-32768 to 32767)" + return int(lib.SDL_GameControllerGetAxis(self.sdl_controller_p, lib.SDL_CONTROLLER_AXIS_LEFTX)) + + @property + def _left_y(self) -> int: + "Return the position of this axis. (-32768 to 32767)" + return int(lib.SDL_GameControllerGetAxis(self.sdl_controller_p, lib.SDL_CONTROLLER_AXIS_LEFTY)) + + @property + def _right_x(self) -> int: + "Return the position of this axis. (-32768 to 32767)" + return int(lib.SDL_GameControllerGetAxis(self.sdl_controller_p, lib.SDL_CONTROLLER_AXIS_RIGHTX)) + + @property + def _right_y(self) -> int: + "Return the position of this axis. (-32768 to 32767)" + return int(lib.SDL_GameControllerGetAxis(self.sdl_controller_p, lib.SDL_CONTROLLER_AXIS_RIGHTY)) + + @property + def _trigger_left(self) -> int: + "Return the position of this trigger. (0 to 32767)" + return int(lib.SDL_GameControllerGetAxis(self.sdl_controller_p, lib.SDL_CONTROLLER_AXIS_TRIGGERLEFT)) + + @property + def _trigger_right(self) -> int: + "Return the position of this trigger. (0 to 32767)" + return int(lib.SDL_GameControllerGetAxis(self.sdl_controller_p, lib.SDL_CONTROLLER_AXIS_TRIGGERRIGHT)) + + @property + def _a(self) -> bool: + """Return True if this button is held.""" + return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_A)) + + @property + def _b(self) -> bool: + """Return True if this button is held.""" + return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_B)) + + @property + def _x(self) -> bool: + """Return True if this button is held.""" + return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_X)) + + @property + def _y(self) -> bool: + """Return True if this button is held.""" + return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_Y)) + + @property + def _back(self) -> bool: + """Return True if this button is held.""" + return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_BACK)) + + @property + def _guide(self) -> bool: + """Return True if this button is held.""" + return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_GUIDE)) + + @property + def _start(self) -> bool: + """Return True if this button is held.""" + return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_START)) + + @property + def _left_stick(self) -> bool: + """Return True if this button is held.""" + return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_LEFTSTICK)) + + @property + def _right_stick(self) -> bool: + """Return True if this button is held.""" + return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_RIGHTSTICK)) + + @property + def _left_shoulder(self) -> bool: + """Return True if this button is held.""" + return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_LEFTSHOULDER)) + + @property + def _right_shoulder(self) -> bool: + """Return True if this button is held.""" + return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_RIGHTSHOULDER)) + + @property + def _dpad(self) -> Tuple[Literal[-1, 0, 1], Literal[-1, 0, 1]]: + return ( + lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_DPAD_RIGHT) + - lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_DPAD_LEFT), + lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_DPAD_DOWN) + - lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_DPAD_UP), + ) + + @property + def _misc1(self) -> bool: + """Return True if this button is held.""" + return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_MISC1)) + + @property + def _paddle1(self) -> bool: + """Return True if this button is held.""" + return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_PADDLE1)) + + @property + def _paddle2(self) -> bool: + """Return True if this button is held.""" + return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_PADDLE2)) + + @property + def _paddle3(self) -> bool: + """Return True if this button is held.""" + return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_PADDLE3)) + + @property + def _paddle4(self) -> bool: + """Return True if this button is held.""" + return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_PADDLE4)) + + @property + def _touchpad(self) -> bool: + """Return True if this button is held.""" + return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_TOUCHPAD)) + + +def _get_number() -> int: """Return the number of attached joysticks.""" tcod.sdl.sys.init(tcod.sdl.sys.Subsystem.JOYSTICK) return _check(lib.SDL_NumJoysticks()) @@ -111,10 +355,27 @@ def get_number() -> int: def get_joysticks() -> List[Joystick]: """Return a list of all connected joystick devices.""" - return [Joystick(i) for i in range(get_number())] + return [Joystick._open(i) for i in range(_get_number())] + + +def get_controllers() -> List[GameController]: + """Return a list of all connected game controllers. + + This ignores joysticks without a game controller mapping. + """ + return [GameController._open(i) for i in range(_get_number()) if lib.SDL_IsGameController(i)] -def event_state(new_state: Optional[bool] = None) -> bool: +def get_all() -> List[Union[Joystick, GameController]]: + """Return a list of all connected joystick or controller devices. + + If the joystick has a controller mapping then it is returned as a :any:`GameController`. + Otherwise it is returned as a :any:`Joystick`. + """ + return [GameController._open(i) if lib.SDL_IsGameController(i) else Joystick._open(i) for i in range(_get_number())] + + +def joystick_event_state(new_state: Optional[bool] = None) -> bool: """Check or set joystick event polling. .. seealso:: @@ -122,3 +383,13 @@ def event_state(new_state: Optional[bool] = None) -> bool: """ _OPTIONS = {None: lib.SDL_QUERY, False: lib.SDL_IGNORE, True: lib.SDL_ENABLE} return bool(_check(lib.SDL_JoystickEventState(_OPTIONS[new_state]))) + + +def controller_event_state(new_state: Optional[bool] = None) -> bool: + """Check or set game controller event polling. + + .. seealso:: + https://wiki.libsdl.org/SDL_GameControllerEventState + """ + _OPTIONS = {None: lib.SDL_QUERY, False: lib.SDL_IGNORE, True: lib.SDL_ENABLE} + return bool(_check(lib.SDL_GameControllerEventState(_OPTIONS[new_state]))) From 2b8388f360fa9e75c98074cf23c1087974371d48 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 20 Sep 2022 15:01:35 -0700 Subject: [PATCH 090/194] Add controller events. --- examples/eventget.py | 18 +++++- tcod/event.py | 146 ++++++++++++++++++++++++++++++++++++++++--- tcod/sdl/joystick.py | 15 ++++- 3 files changed, 168 insertions(+), 11 deletions(-) diff --git a/examples/eventget.py b/examples/eventget.py index bb3534aa..52e26c09 100755 --- a/examples/eventget.py +++ b/examples/eventget.py @@ -5,7 +5,7 @@ # https://creativecommons.org/publicdomain/zero/1.0/ """An demonstration of event handling using the tcod.event module. """ -from typing import List +from typing import List, Set import tcod import tcod.sdl.joystick @@ -19,7 +19,9 @@ def main() -> None: event_log: List[str] = [] motion_desc = "" - joysticks = tcod.sdl.joystick.get_joysticks() + tcod.sdl.joystick.init() + controllers: Set[tcod.sdl.joystick.GameController] = set() + joysticks: Set[tcod.sdl.joystick.Joystick] = set() with tcod.context.new(width=WIDTH, height=HEIGHT) as context: console = context.new_console() @@ -42,9 +44,19 @@ def main() -> None: raise SystemExit() if isinstance(event, tcod.event.WindowResized) and event.type == "WINDOWRESIZED": console = context.new_console() + if isinstance(event, tcod.event.ControllerDevice): + if event.type == "CONTROLLERDEVICEADDED": + controllers.add(event.controller) + elif event.type == "CONTROLLERDEVICEREMOVED": + controllers.remove(event.controller) + if isinstance(event, tcod.event.JoystickDevice): + if event.type == "JOYDEVICEADDED": + joysticks.add(event.joystick) + elif event.type == "JOYDEVICEREMOVED": + joysticks.remove(event.joystick) if isinstance(event, tcod.event.MouseMotion): motion_desc = str(event) - else: + else: # Log all events other than MouseMotion. event_log.append(str(event)) diff --git a/tcod/event.py b/tcod/event.py index 6caec3e7..dba3054a 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -90,6 +90,7 @@ from typing_extensions import Final, Literal import tcod.event_constants +import tcod.sdl.joystick from tcod.event_constants import * # noqa: F4 from tcod.event_constants import KMOD_ALT, KMOD_CTRL, KMOD_GUI, KMOD_SHIFT from tcod.loader import ffi, lib @@ -791,6 +792,12 @@ def __init__(self, type: str, which: int): self.which = which """The ID of the joystick this event is for.""" + @property + def joystick(self) -> tcod.sdl.joystick.Joystick: + if self.type == "JOYDEVICEADDED": + return tcod.sdl.joystick.Joystick._open(self.which) + return tcod.sdl.joystick.Joystick._from_instance_id(self.which) + def __repr__(self) -> str: return f"tcod.event.{self.__class__.__name__}" f"(type={self.type!r}, which={self.which})" @@ -952,17 +959,16 @@ class JoystickDevice(JoystickEvent): Example:: - joysticks: dict[int, tcod.sdl.joystick.Joystick] = {} + joysticks: set[tcod.sdl.joystick.Joystick] = {} for event in tcod.event.get(): match event: - case tcod.event.JoystickDevice(type="JOYDEVICEADDED", which=device_id): - new_joystick = tcod.sdl.joystick.Joystick(device_id) - joysticks[new_joystick.id] = new_joystick - case tcod.event.JoystickDevice(type="JOYDEVICEREMOVED", which=which): - del joysticks[which] + case tcod.event.JoystickDevice(type="JOYDEVICEADDED", joystick=new_joystick): + joysticks.add(new_joystick) + case tcod.event.JoystickDevice(type="JOYDEVICEREMOVED", joystick=joystick): + joysticks.remove(joystick) """ - type = Final[Literal["JOYDEVICEADDED", "JOYDEVICEREMOVED"]] # type: ignore[assignment,misc] + type: Final[Literal["JOYDEVICEADDED", "JOYDEVICEREMOVED"]] # type: ignore[misc] which: int """When type="JOYDEVICEADDED" this is the device ID. @@ -975,6 +981,126 @@ def from_sdl_event(cls, sdl_event: Any) -> JoystickDevice: return cls(type, sdl_event.jdevice.which) +class ControllerEvent(Event): + """Base class for controller events. + + .. versionadded:: Unreleased + """ + + def __init__(self, type: str, which: int): + super().__init__(type) + self.which = which + """The ID of the joystick this event is for.""" + + @property + def controller(self) -> tcod.sdl.joystick.GameController: + """The :any:`GameController: for this event.""" + if self.type == "CONTROLLERDEVICEADDED": + return tcod.sdl.joystick.GameController._open(self.which) + return tcod.sdl.joystick.GameController._from_instance_id(self.which) + + def __repr__(self) -> str: + return f"tcod.event.{self.__class__.__name__}" f"(type={self.type!r}, which={self.which})" + + def __str__(self) -> str: + prefix = super().__str__().strip("<>") + return f"<{prefix}, which={self.which}>" + + +class ControllerAxis(ControllerEvent): + """When a controller axis is moved. + + .. versionadded:: Unreleased + """ + + type: Final[Literal["CONTROLLERAXISMOTION"]] # type: ignore[misc] + + def __init__(self, type: str, which: int, axis: tcod.sdl.joystick.ControllerAxis, value: int): + super().__init__(type, which) + self.axis = axis + """Which axis is being moved. One of :any:`ControllerAxis`.""" + self.value = value + """The new value of this events axis. + + This will be -32768 to 32767 for all axes except for triggers which are 0 to 32767 instead.""" + + @classmethod + def from_sdl_event(cls, sdl_event: Any) -> ControllerAxis: + return cls( + "CONTROLLERAXISMOTION", + sdl_event.caxis.which, + tcod.sdl.joystick.ControllerAxis(sdl_event.caxis.axis), + sdl_event.caxis.value, + ) + + def __repr__(self) -> str: + return ( + f"tcod.event.{self.__class__.__name__}" + f"(type={self.type!r}, which={self.which}, axis={self.axis}, value={self.value})" + ) + + def __str__(self) -> str: + prefix = super().__str__().strip("<>") + return f"<{prefix}, axis={self.axis}, value={self.value}>" + + +class ControllerButton(ControllerEvent): + """When a controller button is pressed or released. + + .. versionadded:: Unreleased + """ + + type: Final[Literal["CONTROLLERBUTTONDOWN", "CONTROLLERBUTTONUP"]] # type: ignore[misc] + + def __init__(self, type: str, which: int, button: tcod.sdl.joystick.ControllerButton, pressed: bool): + super().__init__(type, which) + self.button = button + """The button for this event. One of :any:`ControllerButton`.""" + self.pressed = pressed + """True if the button was pressed, False if it was released.""" + + @classmethod + def from_sdl_event(cls, sdl_event: Any) -> ControllerButton: + type = { + lib.SDL_CONTROLLERBUTTONDOWN: "CONTROLLERBUTTONDOWN", + lib.SDL_CONTROLLERBUTTONUP: "CONTROLLERBUTTONUP", + }[sdl_event.type] + return cls( + type, + sdl_event.cbutton.which, + tcod.sdl.joystick.ControllerButton(sdl_event.cbutton.button), + sdl_event.cbutton.state == lib.SDL_PRESSED, + ) + + def __repr__(self) -> str: + return ( + f"tcod.event.{self.__class__.__name__}" + f"(type={self.type!r}, which={self.which}, button={self.button}, pressed={self.pressed})" + ) + + def __str__(self) -> str: + prefix = super().__str__().strip("<>") + return f"<{prefix}, button={self.button}, pressed={self.pressed}>" + + +class ControllerDevice(ControllerEvent): + """When a controller is added, removed, or remapped. + + .. versionadded:: Unreleased + """ + + type: Final[Literal["CONTROLLERDEVICEADDED", "CONTROLLERDEVICEREMOVED", "CONTROLLERDEVICEREMAPPED"]] # type: ignore[misc] + + @classmethod + def from_sdl_event(cls, sdl_event: Any) -> ControllerDevice: + type = { + lib.SDL_CONTROLLERDEVICEADDED: "CONTROLLERDEVICEADDED", + lib.SDL_CONTROLLERDEVICEREMOVED: "CONTROLLERDEVICEREMOVED", + lib.SDL_CONTROLLERDEVICEREMAPPED: "CONTROLLERDEVICEREMAPPED", + }[sdl_event.type] + return cls(type, sdl_event.cdevice.which) + + class Undefined(Event): """This class is a place holder for SDL events without their own tcod.event class. @@ -1012,6 +1138,12 @@ def __str__(self) -> str: lib.SDL_JOYBUTTONUP: JoystickButton, lib.SDL_JOYDEVICEADDED: JoystickDevice, lib.SDL_JOYDEVICEREMOVED: JoystickDevice, + lib.SDL_CONTROLLERAXISMOTION: ControllerAxis, + lib.SDL_CONTROLLERBUTTONDOWN: ControllerButton, + lib.SDL_CONTROLLERBUTTONUP: ControllerButton, + lib.SDL_CONTROLLERDEVICEADDED: ControllerDevice, + lib.SDL_CONTROLLERDEVICEREMOVED: ControllerDevice, + lib.SDL_CONTROLLERDEVICEREMAPPED: ControllerDevice, } diff --git a/tcod/sdl/joystick.py b/tcod/sdl/joystick.py index 7678e36d..0ef32624 100644 --- a/tcod/sdl/joystick.py +++ b/tcod/sdl/joystick.py @@ -148,6 +148,10 @@ def _open(cls, device_index: int) -> Joystick: p = _check_p(ffi.gc(lib.SDL_JoystickOpen(device_index), lib.SDL_JoystickClose)) return cls(p) + @classmethod + def _from_instance_id(cls, instance_id: int) -> Joystick: + return cls(_check_p(ffi.gc(lib.SDL_JoystickFromInstanceID(instance_id), lib.SDL_JoystickClose))) + def __eq__(self, other: object) -> bool: if isinstance(other, GameController): return self == other.joystick.id @@ -199,6 +203,10 @@ def __init__(self, sdl_controller_p: Any): def _open(cls, joystick_index: int) -> GameController: return cls(_check_p(ffi.gc(lib.SDL_GameControllerOpen(joystick_index), lib.SDL_GameControllerClose))) + @classmethod + def _from_instance_id(cls, instance_id: int) -> GameController: + return cls(_check_p(ffi.gc(lib.SDL_GameControllerFromInstanceID(instance_id), lib.SDL_GameControllerClose))) + def get_button(self, button: ControllerButton) -> bool: """Return True if `button` is currently held.""" return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, button)) @@ -347,9 +355,14 @@ def _touchpad(self) -> bool: return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_TOUCHPAD)) +def init() -> None: + """Initialize SDL's joystick and game controller subsystems.""" + tcod.sdl.sys.init(tcod.sdl.sys.Subsystem.JOYSTICK | tcod.sdl.sys.Subsystem.GAMECONTROLLER) + + def _get_number() -> int: """Return the number of attached joysticks.""" - tcod.sdl.sys.init(tcod.sdl.sys.Subsystem.JOYSTICK) + init() return _check(lib.SDL_NumJoysticks()) From 8ce4b7e82f0069ea7c0ef5d54648d76fe6830600 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 20 Sep 2022 15:03:39 -0700 Subject: [PATCH 091/194] Make joystick class comparing more strict. I'm worried about comparing these across classes. I might relax this compare again in the future. --- tcod/sdl/joystick.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tcod/sdl/joystick.py b/tcod/sdl/joystick.py index 0ef32624..f9923920 100644 --- a/tcod/sdl/joystick.py +++ b/tcod/sdl/joystick.py @@ -153,8 +153,6 @@ def _from_instance_id(cls, instance_id: int) -> Joystick: return cls(_check_p(ffi.gc(lib.SDL_JoystickFromInstanceID(instance_id), lib.SDL_JoystickClose))) def __eq__(self, other: object) -> bool: - if isinstance(other, GameController): - return self == other.joystick.id if isinstance(other, Joystick): return self.id == other.id return NotImplemented @@ -222,8 +220,6 @@ def get_axis(self, axis: ControllerAxis) -> int: def __eq__(self, other: object) -> bool: if isinstance(other, GameController): return self.joystick.id == other.joystick.id - if isinstance(other, Joystick): - return self.joystick.id == other.id return NotImplemented def __hash__(self) -> int: From fd6a961772377582748bba0bac63005d5ce73460 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 22 Sep 2022 17:36:04 -0700 Subject: [PATCH 092/194] Hide get all function. This syntax is weird. I don't want it public for now. --- tcod/sdl/joystick.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tcod/sdl/joystick.py b/tcod/sdl/joystick.py index f9923920..d83aa9c7 100644 --- a/tcod/sdl/joystick.py +++ b/tcod/sdl/joystick.py @@ -375,7 +375,7 @@ def get_controllers() -> List[GameController]: return [GameController._open(i) for i in range(_get_number()) if lib.SDL_IsGameController(i)] -def get_all() -> List[Union[Joystick, GameController]]: +def _get_all() -> List[Union[Joystick, GameController]]: """Return a list of all connected joystick or controller devices. If the joystick has a controller mapping then it is returned as a :any:`GameController`. From a605cfd2354999517b7a14ac39abfe534efbc110 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 22 Sep 2022 17:42:03 -0700 Subject: [PATCH 093/194] Update libtcod. --- CHANGELOG.md | 7 +++++++ libtcod | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eef28017..b8454a44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,13 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version - Ported SDL2 joystick handing as `tcod.sdl.joystick`. - New joystick related events. +### Changed +- Using `libtcod 1.22.3`. + +### Fixed +- Fixed double present bug in non-context flush functions. + This was affecting performance and also caused a screen flicker whenever the global fade color was active. + ## [13.7.0] - 2022-08-07 ### Added - You can new use `SDLConsoleRender.atlas` to access the `SDLTilesetAtlas` used to create it. diff --git a/libtcod b/libtcod index 6f0a9bc8..38110b9d 160000 --- a/libtcod +++ b/libtcod @@ -1 +1 @@ -Subproject commit 6f0a9bc8c31f769709299f248681b57a6f9659be +Subproject commit 38110b9d785664392706b3de35450d64f565b37b From f1348726a4a3ebf407aa2861571d6bc73a6ff53c Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 22 Sep 2022 18:17:32 -0700 Subject: [PATCH 094/194] Bundle SDL 2.24.0. --- CHANGELOG.md | 2 ++ build_sdl.py | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8454a44..06007d98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,12 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version ### Changed - Using `libtcod 1.22.3`. +- Bundle `SDL 2.24.0` on Windows and MacOS. ### Fixed - Fixed double present bug in non-context flush functions. This was affecting performance and also caused a screen flicker whenever the global fade color was active. +- Fixed the parsing of SDL 2.24.0 headers on Windows. ## [13.7.0] - 2022-08-07 ### Added diff --git a/build_sdl.py b/build_sdl.py index 1fc6b788..77e46e68 100644 --- a/build_sdl.py +++ b/build_sdl.py @@ -22,7 +22,7 @@ # The SDL2 version to parse and export symbols from. SDL2_PARSE_VERSION = os.environ.get("SDL_VERSION", "2.0.20") # The SDL2 version to include in binary distributions. -SDL2_BUNDLE_VERSION = os.environ.get("SDL_VERSION", "2.0.20") +SDL2_BUNDLE_VERSION = os.environ.get("SDL_VERSION", "2.24.0") # Used to remove excessive newlines in debug outputs. @@ -65,6 +65,10 @@ "SDL_TRUE", # Ignore floating point symbols. "SDL_FLT_EPSILON", + # Conditional config flags which might be missing. + "SDL_VIDEO_RENDER_D3D12", + "SDL_SENSOR_WINDOWS", + "SDL_SENSOR_DUMMY", ) ) From 383eec53ebda6ce2126400a695b9748605ab9488 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 22 Sep 2022 20:34:19 -0700 Subject: [PATCH 095/194] Discourage the use of alternative renderers. I'll likely address the rare issues with the other renderers by removing them. --- CHANGELOG.md | 3 +++ tcod/context.py | 11 +++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06007d98..2c53986f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version - Using `libtcod 1.22.3`. - Bundle `SDL 2.24.0` on Windows and MacOS. +### Deprecated +- Renderers other than `tcod.RENDERER_SDL2` are now discouraged. + ### Fixed - Fixed double present bug in non-context flush functions. This was affecting performance and also caused a screen flicker whenever the global fade color was active. diff --git a/tcod/context.py b/tcod/context.py index a112d0d5..aacea6a1 100644 --- a/tcod/context.py +++ b/tcod/context.py @@ -52,6 +52,7 @@ import os import pickle import sys +import warnings from typing import Any, Iterable, List, Optional, Tuple from typing_extensions import Literal, NoReturn @@ -489,8 +490,8 @@ def new( Providing no size information at all is also acceptable. `renderer` is the desired libtcod renderer to use. - Typical options are :any:`tcod.context.RENDERER_OPENGL2` for a faster - renderer or :any:`tcod.context.RENDERER_SDL2` for a reliable renderer. + The default is :any:`tcod.context.RENDERER_SDL2` which is a fast renderer that runs reliably on all platforms. + If unsure then don't set this parameter. `tileset` is the font/tileset for the new context to render with. The fall-back tileset available from passing None is useful for @@ -525,6 +526,12 @@ def new( """ if renderer is None: renderer = RENDERER_SDL2 + if renderer != RENDERER_SDL2: + warnings.warn( + "In the future all renderers other than tcod.RENDERER_SDL2 may be removed or ignored.", + PendingDeprecationWarning, + stacklevel=2, + ) if sdl_window_flags is None: sdl_window_flags = SDL_WINDOW_RESIZABLE if argv is None: From 26f309b396ff8cd4fe276b163a8137983bda8999 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 22 Sep 2022 23:17:17 -0700 Subject: [PATCH 096/194] Prepare 13.8.0 release. --- CHANGELOG.md | 2 ++ tcod/event.py | 34 +++++++++++++++++----------------- tcod/sdl/joystick.py | 2 +- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c53986f..df42adef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] + +## [13.8.0] - 2022-09-22 ### Added - Ported SDL2 joystick handing as `tcod.sdl.joystick`. - New joystick related events. diff --git a/tcod/event.py b/tcod/event.py index dba3054a..f6783e4b 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -784,7 +784,7 @@ def __str__(self) -> str: class JoystickEvent(Event): """A base class for joystick events. - .. versionadded:: Unreleased + .. versionadded:: 13.8 """ def __init__(self, type: str, which: int): @@ -809,7 +809,7 @@ def __str__(self) -> str: class JoystickAxis(JoystickEvent): """When a joystick axis changes in value. - .. versionadded:: Unreleased + .. versionadded:: 13.8 .. seealso:: :any:`tcod.sdl.joystick` @@ -843,7 +843,7 @@ def __str__(self) -> str: class JoystickBall(JoystickEvent): """When a joystick ball is moved. - .. versionadded:: Unreleased + .. versionadded:: 13.8 .. seealso:: :any:`tcod.sdl.joystick` @@ -881,7 +881,7 @@ def __str__(self) -> str: class JoystickHat(JoystickEvent): """When a joystick hat changes direction. - .. versionadded:: Unreleased + .. versionadded:: 13.8 .. seealso:: :any:`tcod.sdl.joystick` @@ -914,7 +914,7 @@ def __str__(self) -> str: class JoystickButton(JoystickEvent): """When a joystick button is pressed or released. - .. versionadded:: Unreleased + .. versionadded:: 13.8 Example:: @@ -955,7 +955,7 @@ def __str__(self) -> str: class JoystickDevice(JoystickEvent): """An event for when a joystick is added or removed. - .. versionadded:: Unreleased + .. versionadded:: 13.8 Example:: @@ -984,7 +984,7 @@ def from_sdl_event(cls, sdl_event: Any) -> JoystickDevice: class ControllerEvent(Event): """Base class for controller events. - .. versionadded:: Unreleased + .. versionadded:: 13.8 """ def __init__(self, type: str, which: int): @@ -1010,7 +1010,7 @@ def __str__(self) -> str: class ControllerAxis(ControllerEvent): """When a controller axis is moved. - .. versionadded:: Unreleased + .. versionadded:: 13.8 """ type: Final[Literal["CONTROLLERAXISMOTION"]] # type: ignore[misc] @@ -1047,7 +1047,7 @@ def __str__(self) -> str: class ControllerButton(ControllerEvent): """When a controller button is pressed or released. - .. versionadded:: Unreleased + .. versionadded:: 13.8 """ type: Final[Literal["CONTROLLERBUTTONDOWN", "CONTROLLERBUTTONUP"]] # type: ignore[misc] @@ -1086,7 +1086,7 @@ def __str__(self) -> str: class ControllerDevice(ControllerEvent): """When a controller is added, removed, or remapped. - .. versionadded:: Unreleased + .. versionadded:: 13.8 """ type: Final[Literal["CONTROLLERDEVICEADDED", "CONTROLLERDEVICEREMOVED", "CONTROLLERDEVICEREMAPPED"]] # type: ignore[misc] @@ -1413,37 +1413,37 @@ def ev_windowhittest(self, event: tcod.event.WindowEvent) -> Optional[T]: def ev_joyaxismotion(self, event: tcod.event.JoystickAxis) -> Optional[T]: """ - .. versionadded:: Unreleased + .. versionadded:: 13.8 """ def ev_joyballmotion(self, event: tcod.event.JoystickBall) -> Optional[T]: """ - .. versionadded:: Unreleased + .. versionadded:: 13.8 """ def ev_joyhatmotion(self, event: tcod.event.JoystickHat) -> Optional[T]: """ - .. versionadded:: Unreleased + .. versionadded:: 13.8 """ def ev_joybuttondown(self, event: tcod.event.JoystickButton) -> Optional[T]: """ - .. versionadded:: Unreleased + .. versionadded:: 13.8 """ def ev_joybuttonup(self, event: tcod.event.JoystickButton) -> Optional[T]: """ - .. versionadded:: Unreleased + .. versionadded:: 13.8 """ def ev_joydeviceadded(self, event: tcod.event.JoystickDevice) -> Optional[T]: """ - .. versionadded:: Unreleased + .. versionadded:: 13.8 """ def ev_joydeviceremoved(self, event: tcod.event.JoystickDevice) -> Optional[T]: """ - .. versionadded:: Unreleased + .. versionadded:: 13.8 """ def ev_(self, event: Any) -> Optional[T]: diff --git a/tcod/sdl/joystick.py b/tcod/sdl/joystick.py index d83aa9c7..dde74192 100644 --- a/tcod/sdl/joystick.py +++ b/tcod/sdl/joystick.py @@ -1,6 +1,6 @@ """SDL Joystick Support -.. versionadded:: Unreleased +.. versionadded:: 13.8 """ from __future__ import annotations From 9ed914be72ca41b3a5292fa9ac89419017d67298 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 23 Sep 2022 11:36:13 -0700 Subject: [PATCH 097/194] Fix docs typo. --- tcod/event.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tcod/event.py b/tcod/event.py index f6783e4b..6cb8fe0b 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -1484,7 +1484,7 @@ def add_watch(callback: _EventCallback) -> _EventCallback: .. warning:: How uncaught exceptions in a callback are handled is not currently defined by tcod. They will likely be handled by :any:`sys.unraisablehook`. - This may be later changed to pass the excpetion to a :any`tcod.event.get` or :any:`tcod.event.wait` call. + This may be later changed to pass the excpetion to a :any:`tcod.event.get` or :any:`tcod.event.wait` call. Args: callback (Callable[[Event], None]): From 7e64ae4abdab621283e699cc1c742c24840765ca Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 23 Sep 2022 12:22:26 -0700 Subject: [PATCH 098/194] Add missing event names to EventDispatch. Change EventDispatch so that missing names will warn instead of crashing. --- CHANGELOG.md | 2 ++ tcod/event.py | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df42adef..9ac747ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] +### Fixed +- `EventDispatch` was missing new event names. ## [13.8.0] - 2022-09-22 ### Added diff --git a/tcod/event.py b/tcod/event.py index 6cb8fe0b..bc1062db 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -1325,7 +1325,11 @@ def dispatch(self, event: Any) -> Optional[T]: stacklevel=2, ) return None - func: Callable[[Any], Optional[T]] = getattr(self, "ev_%s" % (event.type.lower(),)) + func_name = f"ev_{event.type.lower()}" + func: Optional[Callable[[Any], Optional[T]]] = getattr(self, func_name, None) + if func is None: + warnings.warn(f"{func_name} is missing from this EventDispatch object.", RuntimeWarning, stacklevel=2) + return None return func(event) def event_get(self) -> None: @@ -1446,6 +1450,36 @@ def ev_joydeviceremoved(self, event: tcod.event.JoystickDevice) -> Optional[T]: .. versionadded:: 13.8 """ + def ev_controlleraxismotion(self, event: tcod.event.ControllerAxis) -> Optional[T]: + """ + .. versionadded:: Unreleased + """ + + def ev_controllerbuttondown(self, event: tcod.event.ControllerButton) -> Optional[T]: + """ + .. versionadded:: Unreleased + """ + + def ev_controllerbuttonup(self, event: tcod.event.ControllerButton) -> Optional[T]: + """ + .. versionadded:: Unreleased + """ + + def ev_controllerdeviceadded(self, event: tcod.event.ControllerDevice) -> Optional[T]: + """ + .. versionadded:: Unreleased + """ + + def ev_controllerdeviceremoved(self, event: ControllerDevice) -> Optional[T]: + """ + .. versionadded:: Unreleased + """ + + def ev_controllerdeviceremapped(self, event: ControllerDevice) -> Optional[T]: + """ + .. versionadded:: Unreleased + """ + def ev_(self, event: Any) -> Optional[T]: pass From d2f3c5b7a16864091f4e32ee5bb28f1695d2e31e Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 23 Sep 2022 12:31:10 -0700 Subject: [PATCH 099/194] Prepare 13.8.1 release. --- CHANGELOG.md | 2 ++ tcod/event.py | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ac747ed..5590ccb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] + +## [13.8.1] - 2022-09-23 ### Fixed - `EventDispatch` was missing new event names. diff --git a/tcod/event.py b/tcod/event.py index bc1062db..4d5b1536 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -1452,32 +1452,32 @@ def ev_joydeviceremoved(self, event: tcod.event.JoystickDevice) -> Optional[T]: def ev_controlleraxismotion(self, event: tcod.event.ControllerAxis) -> Optional[T]: """ - .. versionadded:: Unreleased + .. versionadded:: 13.8 """ def ev_controllerbuttondown(self, event: tcod.event.ControllerButton) -> Optional[T]: """ - .. versionadded:: Unreleased + .. versionadded:: 13.8 """ def ev_controllerbuttonup(self, event: tcod.event.ControllerButton) -> Optional[T]: """ - .. versionadded:: Unreleased + .. versionadded:: 13.8 """ def ev_controllerdeviceadded(self, event: tcod.event.ControllerDevice) -> Optional[T]: """ - .. versionadded:: Unreleased + .. versionadded:: 13.8 """ def ev_controllerdeviceremoved(self, event: ControllerDevice) -> Optional[T]: """ - .. versionadded:: Unreleased + .. versionadded:: 13.8 """ def ev_controllerdeviceremapped(self, event: ControllerDevice) -> Optional[T]: """ - .. versionadded:: Unreleased + .. versionadded:: 13.8 """ def ev_(self, event: Any) -> Optional[T]: From 52d102f37aeee7594d7f78ee50a571245e6e6c9b Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 23 Sep 2022 12:41:55 -0700 Subject: [PATCH 100/194] Fix inconsistent type annotations. --- tcod/event.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tcod/event.py b/tcod/event.py index 4d5b1536..3e9d9a69 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -1376,13 +1376,13 @@ def ev_windowexposed(self, event: tcod.event.WindowEvent) -> Optional[T]: This usually means a call to :any:`tcod.console_flush` is necessary. """ - def ev_windowmoved(self, event: "tcod.event.WindowMoved") -> Optional[T]: + def ev_windowmoved(self, event: tcod.event.WindowMoved) -> Optional[T]: """Called when the window is moved.""" - def ev_windowresized(self, event: "tcod.event.WindowResized") -> Optional[T]: + def ev_windowresized(self, event: tcod.event.WindowResized) -> Optional[T]: """Called when the window is resized.""" - def ev_windowsizechanged(self, event: "tcod.event.WindowResized") -> Optional[T]: + def ev_windowsizechanged(self, event: tcod.event.WindowResized) -> Optional[T]: """Called when the system or user changes the size of the window.""" def ev_windowminimized(self, event: tcod.event.WindowEvent) -> Optional[T]: @@ -1470,12 +1470,12 @@ def ev_controllerdeviceadded(self, event: tcod.event.ControllerDevice) -> Option .. versionadded:: 13.8 """ - def ev_controllerdeviceremoved(self, event: ControllerDevice) -> Optional[T]: + def ev_controllerdeviceremoved(self, event: tcod.event.ControllerDevice) -> Optional[T]: """ .. versionadded:: 13.8 """ - def ev_controllerdeviceremapped(self, event: ControllerDevice) -> Optional[T]: + def ev_controllerdeviceremapped(self, event: tcod.event.ControllerDevice) -> Optional[T]: """ .. versionadded:: 13.8 """ From 6f964d168a0d3ce4639d0895f81b12e9d8f16ca6 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 23 Sep 2022 12:42:56 -0700 Subject: [PATCH 101/194] Add missing controller events to __all__. --- tcod/event.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tcod/event.py b/tcod/event.py index 3e9d9a69..1ff5cfea 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -2736,6 +2736,10 @@ def __repr__(self) -> str: "JoystickHat", "JoystickButton", "JoystickDevice", + "ControllerEvent", + "ControllerAxis", + "ControllerButton", + "ControllerDevice", "Undefined", "get", "wait", From 6dafab584f60bdf268b5746aa1e7f1c8577c1e23 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 23 Sep 2022 12:45:03 -0700 Subject: [PATCH 102/194] Fix docs typo. --- tcod/event.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tcod/event.py b/tcod/event.py index 1ff5cfea..d663858c 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -994,7 +994,7 @@ def __init__(self, type: str, which: int): @property def controller(self) -> tcod.sdl.joystick.GameController: - """The :any:`GameController: for this event.""" + """The :any:`GameController` for this event.""" if self.type == "CONTROLLERDEVICEADDED": return tcod.sdl.joystick.GameController._open(self.which) return tcod.sdl.joystick.GameController._from_instance_id(self.which) From 30832182ca6b612e793ea2f73d189d1cf497d7f3 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 27 Sep 2022 12:16:53 -0700 Subject: [PATCH 103/194] Fix doc ref linked to the wrong function. --- tcod/map.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tcod/map.py b/tcod/map.py index 3ce4596b..8ccc3b1f 100644 --- a/tcod/map.py +++ b/tcod/map.py @@ -136,7 +136,7 @@ def compute_fov( algorithm (int): Defaults to tcod.FOV_RESTRICTIVE If you already have transparency in a NumPy array then you could use - :any:`tcod.map_compute_fov` instead. + :any:`tcod.map.compute_fov` instead. """ if not (0 <= x < self.width and 0 <= y < self.height): warnings.warn( From de0758e782e1492dd50846c57609ff4631f470d6 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 27 Sep 2022 13:24:21 -0700 Subject: [PATCH 104/194] Remove old type ignores for new Mypy version. --- tcod/color.py | 8 ++++---- tcod/console.py | 16 ++++++++-------- tcod/noise.py | 2 +- tests/conftest.py | 8 ++++---- tests/test_console.py | 8 ++++---- tests/test_tcod.py | 6 +++--- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/tcod/color.py b/tcod/color.py index 43fe0aab..fce63995 100644 --- a/tcod/color.py +++ b/tcod/color.py @@ -31,7 +31,7 @@ def r(self) -> int: """ return int(self[0]) - @r.setter # type: ignore + @r.setter @deprecate("Setting color attributes has been deprecated.") def r(self, value: int) -> None: self[0] = value & 0xFF @@ -45,7 +45,7 @@ def g(self) -> int: """ return int(self[1]) - @g.setter # type: ignore + @g.setter @deprecate("Setting color attributes has been deprecated.") def g(self, value: int) -> None: self[1] = value & 0xFF @@ -59,7 +59,7 @@ def b(self) -> int: """ return int(self[2]) - @b.setter # type: ignore + @b.setter @deprecate("Setting color attributes has been deprecated.") def b(self, value: int) -> None: self[2] = value & 0xFF @@ -102,7 +102,7 @@ def __eq__(self, other: Any) -> bool: return False @deprecate("Use NumPy instead for color math operations.") - def __add__(self, other: Any) -> Color: + def __add__(self, other: Any) -> Color: # type: ignore[override] """Add two colors together. .. deprecated:: 9.2 diff --git a/tcod/console.py b/tcod/console.py index 030f8bd1..c5dfbbff 100644 --- a/tcod/console.py +++ b/tcod/console.py @@ -245,7 +245,7 @@ def ch(self) -> NDArray[np.intc]: """ return self._tiles["ch"].T if self._order == "F" else self._tiles["ch"] - @property # type: ignore + @property @deprecate("This attribute has been renamed to `rgba`.") def tiles(self) -> NDArray[Any]: """An array of this consoles raw tile data. @@ -261,7 +261,7 @@ def tiles(self) -> NDArray[Any]: """ return self.rgba - @property # type: ignore + @property @deprecate("This attribute has been renamed to `rgba`.") def buffer(self) -> NDArray[Any]: """An array of this consoles raw tile data. @@ -273,7 +273,7 @@ def buffer(self) -> NDArray[Any]: """ return self.rgba - @property # type: ignore + @property @deprecate("This attribute has been renamed to `rgb`.") def tiles_rgb(self) -> NDArray[Any]: """An array of this consoles data without the alpha channel. @@ -285,7 +285,7 @@ def tiles_rgb(self) -> NDArray[Any]: """ return self.rgb - @property # type: ignore + @property @deprecate("This attribute has been renamed to `rgb`.") def tiles2(self) -> NDArray[Any]: """This name is deprecated in favour of :any:`rgb`. @@ -347,7 +347,7 @@ def default_bg(self) -> Tuple[int, int, int]: color = self._console_data.back return color.r, color.g, color.b - @default_bg.setter # type: ignore + @default_bg.setter @deprecate("Console defaults have been deprecated.") def default_bg(self, color: Tuple[int, int, int]) -> None: self._console_data.back = color @@ -358,7 +358,7 @@ def default_fg(self) -> Tuple[int, int, int]: color = self._console_data.fore return color.r, color.g, color.b - @default_fg.setter # type: ignore + @default_fg.setter @deprecate("Console defaults have been deprecated.") def default_fg(self, color: Tuple[int, int, int]) -> None: self._console_data.fore = color @@ -368,7 +368,7 @@ def default_bg_blend(self) -> int: """int: The default blending mode.""" return self._console_data.bkgnd_flag # type: ignore - @default_bg_blend.setter # type: ignore + @default_bg_blend.setter @deprecate("Console defaults have been deprecated.") def default_bg_blend(self, value: int) -> None: self._console_data.bkgnd_flag = value @@ -378,7 +378,7 @@ def default_alignment(self) -> int: """int: The default text alignment.""" return self._console_data.alignment # type: ignore - @default_alignment.setter # type: ignore + @default_alignment.setter @deprecate("Console defaults have been deprecated.") def default_alignment(self, value: int) -> None: self._console_data.alignment = value diff --git a/tcod/noise.py b/tcod/noise.py index cae99be0..7328965d 100644 --- a/tcod/noise.py +++ b/tcod/noise.py @@ -180,7 +180,7 @@ def __repr__(self) -> str: def dimensions(self) -> int: return int(self._tdl_noise_c.dimensions) - @property # type: ignore + @property @deprecate("This is a misspelling of 'dimensions'.") def dimentions(self) -> int: return self.dimensions diff --git a/tests/conftest.py b/tests/conftest.py index da22cd44..0b3fb63b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -33,10 +33,10 @@ def console(session_console: tcod.console.Console) -> tcod.console.Console: tcod.console_flush() with warnings.catch_warnings(): warnings.simplefilter("ignore") - console.default_fg = (255, 255, 255) # type: ignore - console.default_bg = (0, 0, 0) # type: ignore - console.default_bg_blend = tcod.BKGND_SET # type: ignore - console.default_alignment = tcod.LEFT # type: ignore + console.default_fg = (255, 255, 255) + console.default_bg = (0, 0, 0) + console.default_bg_blend = tcod.BKGND_SET + console.default_alignment = tcod.LEFT console.clear() return console diff --git a/tests/test_console.py b/tests/test_console.py index e4f15ef7..790ddcfe 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -44,16 +44,16 @@ def test_array_read_write() -> None: def test_console_defaults() -> None: console = tcod.console.Console(width=12, height=10) - console.default_bg = [2, 3, 4] # type: ignore + console.default_bg = [2, 3, 4] # type: ignore[assignment] assert console.default_bg == (2, 3, 4) - console.default_fg = (4, 5, 6) # type: ignore + console.default_fg = (4, 5, 6) assert console.default_fg == (4, 5, 6) - console.default_bg_blend = tcod.BKGND_ADD # type: ignore + console.default_bg_blend = tcod.BKGND_ADD assert console.default_bg_blend == tcod.BKGND_ADD - console.default_alignment = tcod.RIGHT # type: ignore + console.default_alignment = tcod.RIGHT assert console.default_alignment == tcod.RIGHT diff --git a/tests/test_tcod.py b/tests/test_tcod.py index 88821924..dbe2e89c 100644 --- a/tests/test_tcod.py +++ b/tests/test_tcod.py @@ -117,9 +117,9 @@ def test_color_class() -> None: assert tcod.black + (2, 2, 2) - (1, 1, 1) == (1, 1, 1) color = tcod.Color() - color.r = 1 # type: ignore - color.g = 2 # type: ignore - color.b = 3 # type: ignore + color.r = 1 + color.g = 2 + color.b = 3 assert color == (1, 2, 3) From 3eb49dfa951ebc0bb53ac960028a41da0bd9ef8b Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 30 Sep 2022 04:19:03 -0700 Subject: [PATCH 105/194] Set up sponsorship. --- .github/FUNDING.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..ac5a1634 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository + +github: HexDecimal From d3419a5b4593c7df1580427fc07616d798c85856 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 3 Oct 2022 22:47:29 -0700 Subject: [PATCH 106/194] Fix synchronous audio example. --- tcod/sdl/audio.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tcod/sdl/audio.py b/tcod/sdl/audio.py index 6bee2587..55b080c6 100644 --- a/tcod/sdl/audio.py +++ b/tcod/sdl/audio.py @@ -11,14 +11,19 @@ Example:: # Synchronous audio example using SDL's low-level API. + import time + import soundfile # pip install soundfile import tcod.sdl.audio device = tcod.sdl.audio.open() # Open the default output device. - sound, samplerate = soundfile.read("example_sound.wav") # Load an audio sample using SoundFile. + sound, samplerate = soundfile.read("example_sound.wav", dtype="float32") # Load an audio sample using SoundFile. converted = device.convert(sound, samplerate) # Convert this sample to the format expected by the device. device.queue_audio(converted) # Play audio syncroniously by appending it to the device buffer. + while device.queued_samples: # Wait until device is done playing. + time.sleep(0.001) + Example:: # Asynchronous audio example using BasicMixer. From 33ead26ab6bc8eb5f991216f0a34b4366e3ac961 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 30 Oct 2022 11:31:29 -0700 Subject: [PATCH 107/194] Don't run actions twice on pull requests. --- .github/workflows/python-package.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 5e305f8c..e0acab53 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -6,6 +6,7 @@ name: Package on: push: pull_request: + types: [opened, reopened] defaults: run: From ba4ac692e174aa9162fa320820f690ae1d5360f3 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 4 Dec 2022 19:04:58 -0800 Subject: [PATCH 108/194] Update libtcod and SDL versions. Apply new fixes from libtcod and warn for changes in renderers. Update parser to handle a more complex deprecation qualifier. Remove references to GLAD. Update Numpy typing. --- CHANGELOG.md | 8 ++++++++ build_libtcod.py | 4 +++- build_sdl.py | 2 +- docs/tcod/charmap-reference.rst | 2 +- libtcod | 2 +- tcod/context.py | 14 ++++++-------- tcod/event.py | 8 +++++++- tcod/event_constants.py | 18 +++++++++++++++--- tcod/tileset.py | 5 ++++- 9 files changed, 46 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5590ccb3..e86b05e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] +### Changed +- Using `libtcod 1.23.1`. +- Bundle `SDL 2.26.0` on Windows and MacOS. +- Code Page 437: Character 0x7F is now assigned to 0x2302 (HOUSE). +- Forced all renderers to ``RENDERER_SDL2`` to fix rare graphical artifacts with OpenGL. + +### Deprecated +- The `renderer` parameter of new contexts is now deprecated. ## [13.8.1] - 2022-09-23 ### Fixed diff --git a/build_libtcod.py b/build_libtcod.py index f84d7736..e8d800cf 100644 --- a/build_libtcod.py +++ b/build_libtcod.py @@ -29,6 +29,7 @@ RE_INCLUDE = re.compile(r'#include "([^"]*)"') RE_TAGS = re.compile( r"TCODLIB_C?API|TCOD_PUBLIC|TCOD_NODISCARD|TCOD_DEPRECATED_NOMESSAGE|TCOD_DEPRECATED_ENUM" + r"|(TCOD_DEPRECATED\(\".*?\"\))" r"|(TCOD_DEPRECATED|TCODLIB_FORMAT)\([^)]*\)|__restrict" ) RE_VAFUNC = re.compile(r"^[^;]*\([^;]*va_list.*\);", re.MULTILINE) @@ -151,7 +152,6 @@ def walk_sources(directory: str) -> Iterator[str]: sources += walk_sources("tcod/") sources += walk_sources("libtcod/src/libtcod/") sources += ["libtcod/src/vendor/stb.c"] -sources += ["libtcod/src/vendor/glad.c"] sources += ["libtcod/src/vendor/lodepng.c"] sources += ["libtcod/src/vendor/utf8proc/utf8proc.c"] sources += glob.glob("libtcod/src/vendor/zlib/*.c") @@ -265,6 +265,8 @@ def parse_sdl_attrs(prefix: str, all_names: List[str]) -> Tuple[str, str]: names = [] lookup = [] for name, value in sorted(find_sdl_attrs(prefix), key=lambda item: item[1]): + if name == "KMOD_RESERVED": + continue all_names.append(name) names.append(f"{name} = {value}") lookup.append(f'{value}: "{name}"') diff --git a/build_sdl.py b/build_sdl.py index 77e46e68..0bbe5241 100644 --- a/build_sdl.py +++ b/build_sdl.py @@ -22,7 +22,7 @@ # The SDL2 version to parse and export symbols from. SDL2_PARSE_VERSION = os.environ.get("SDL_VERSION", "2.0.20") # The SDL2 version to include in binary distributions. -SDL2_BUNDLE_VERSION = os.environ.get("SDL_VERSION", "2.24.0") +SDL2_BUNDLE_VERSION = os.environ.get("SDL_VERSION", "2.26.0") # Used to remove excessive newlines in debug outputs. diff --git a/docs/tcod/charmap-reference.rst b/docs/tcod/charmap-reference.rst index 0fa8150a..3ad42b16 100644 --- a/docs/tcod/charmap-reference.rst +++ b/docs/tcod/charmap-reference.rst @@ -173,7 +173,7 @@ https://en.wikipedia.org/wiki/Code_page_437 124 0x7C \'\|\' VERTICAL LINE 125 0x7D \'}\' RIGHT CURLY BRACKET 126 0x7E \'~\' TILDE - 127 0x7F \'\\x7f\' + 127 0x2302 \'⌂\' HOUSE 128 0xC7 \'Ç\' LATIN CAPITAL LETTER C WITH CEDILLA 129 0xFC \'ü\' LATIN SMALL LETTER U WITH DIAERESIS 130 0xE9 \'é\' LATIN SMALL LETTER E WITH ACUTE diff --git a/libtcod b/libtcod index 38110b9d..168ab8ce 160000 --- a/libtcod +++ b/libtcod @@ -1 +1 @@ -Subproject commit 38110b9d785664392706b3de35450d64f565b37b +Subproject commit 168ab8ce054f84087e04595a54ad02093c31a5a9 diff --git a/tcod/context.py b/tcod/context.py index aacea6a1..c2e57bee 100644 --- a/tcod/context.py +++ b/tcod/context.py @@ -489,9 +489,7 @@ def new( Providing no size information at all is also acceptable. - `renderer` is the desired libtcod renderer to use. - The default is :any:`tcod.context.RENDERER_SDL2` which is a fast renderer that runs reliably on all platforms. - If unsure then don't set this parameter. + `renderer` now does nothing and should not be set. It may be removed in the future. `tileset` is the font/tileset for the new context to render with. The fall-back tileset available from passing None is useful for @@ -524,14 +522,14 @@ def new( .. versionchanged:: 13.2 Added the `console` parameter. """ - if renderer is None: - renderer = RENDERER_SDL2 - if renderer != RENDERER_SDL2: + if renderer is not None: warnings.warn( - "In the future all renderers other than tcod.RENDERER_SDL2 may be removed or ignored.", - PendingDeprecationWarning, + "The renderer parameter was deprecated and will likely be removed in a future version of libtcod. " + "Remove the renderer parameter to fix this warning.", + FutureWarning, stacklevel=2, ) + renderer = RENDERER_SDL2 if sdl_window_flags is None: sdl_window_flags = SDL_WINDOW_RESIZABLE if argv is None: diff --git a/tcod/event.py b/tcod/event.py index d663858c..7be7eebd 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -1583,7 +1583,7 @@ def get_keyboard_state() -> NDArray[np.bool_]: numkeys = ffi.new("int[1]") keyboard_state = lib.SDL_GetKeyboardState(numkeys) out: NDArray[np.bool_] = np.frombuffer(ffi.buffer(keyboard_state[0 : numkeys[0]]), dtype=np.bool_) - out.flags["WRITEABLE"] = False # type: ignore[index] # This buffer is supposed to be const. + out.flags["WRITEABLE"] = False # This buffer is supposed to be const. return out @@ -2094,6 +2094,8 @@ class Scancode(enum.IntEnum): SLEEP = 282 APP1 = 283 APP2 = 284 + AUDIOREWIND = 285 + AUDIOFASTFORWARD = 286 # --- end --- @property @@ -2639,6 +2641,10 @@ class KeySym(enum.IntEnum): KBDILLUMUP = 1073742104 EJECT = 1073742105 SLEEP = 1073742106 + APP1 = 1073742107 + APP2 = 1073742108 + AUDIOREWIND = 1073742109 + AUDIOFASTFORWARD = 1073742110 # --- end --- @property diff --git a/tcod/event_constants.py b/tcod/event_constants.py index 6d6069be..aca5d5ec 100644 --- a/tcod/event_constants.py +++ b/tcod/event_constants.py @@ -245,6 +245,8 @@ SCANCODE_SLEEP = 282 SCANCODE_APP1 = 283 SCANCODE_APP2 = 284 +SCANCODE_AUDIOREWIND = 285 +SCANCODE_AUDIOFASTFORWARD = 286 # --- SDL keyboard symbols --- K_UNKNOWN = 0 @@ -484,6 +486,10 @@ K_KBDILLUMUP = 1073742104 K_EJECT = 1073742105 K_SLEEP = 1073742106 +K_APP1 = 1073742107 +K_APP2 = 1073742108 +K_AUDIOREWIND = 1073742109 +K_AUDIOFASTFORWARD = 1073742110 # --- SDL keyboard modifiers --- KMOD_NONE = 0 @@ -502,7 +508,7 @@ KMOD_NUM = 4096 KMOD_CAPS = 8192 KMOD_MODE = 16384 -KMOD_RESERVED = 32768 +KMOD_SCROLL = 32768 _REVERSE_MOD_TABLE = { 0: "KMOD_NONE", 1: "KMOD_LSHIFT", @@ -520,7 +526,7 @@ 4096: "KMOD_NUM", 8192: "KMOD_CAPS", 16384: "KMOD_MODE", - 32768: "KMOD_RESERVED", + 32768: "KMOD_SCROLL", } # --- SDL wheel --- @@ -775,6 +781,8 @@ "SCANCODE_SLEEP", "SCANCODE_APP1", "SCANCODE_APP2", + "SCANCODE_AUDIOREWIND", + "SCANCODE_AUDIOFASTFORWARD", "K_UNKNOWN", "K_BACKSPACE", "K_TAB", @@ -1012,6 +1020,10 @@ "K_KBDILLUMUP", "K_EJECT", "K_SLEEP", + "K_APP1", + "K_APP2", + "K_AUDIOREWIND", + "K_AUDIOFASTFORWARD", "KMOD_NONE", "KMOD_LSHIFT", "KMOD_RSHIFT", @@ -1028,7 +1040,7 @@ "KMOD_NUM", "KMOD_CAPS", "KMOD_MODE", - "KMOD_RESERVED", + "KMOD_SCROLL", "MOUSEWHEEL_NORMAL", "MOUSEWHEEL_FLIPPED", "MOUSEWHEEL", diff --git a/tcod/tileset.py b/tcod/tileset.py index 14767e5b..1e72559f 100644 --- a/tcod/tileset.py +++ b/tcod/tileset.py @@ -584,7 +584,7 @@ def procedural_block_elements(*, tileset: Tileset) -> None: 0x007C, 0x007D, 0x007E, - 0x007F, + 0x2302, 0x00C7, 0x00FC, 0x00E9, @@ -719,6 +719,9 @@ def procedural_block_elements(*, tileset: Tileset) -> None: See :ref:`code-page-437` for more info and a table of glyphs. .. versionadded:: 11.12 + +.. versionchanged:: Unreleased + Character at index ``0x7F`` was changed from value ``0x7F`` to the HOUSE ``⌂`` glyph ``0x2302``. """ CHARMAP_TCOD = [ From eccad15a7ec558d140296ab059ecf7cfd0b27c4e Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 6 Dec 2022 20:29:32 -0800 Subject: [PATCH 109/194] Fix mouse tile coords in samples when context.present is skipped. --- examples/samples_tcod.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/examples/samples_tcod.py b/examples/samples_tcod.py index f92d0151..f14c9c4b 100755 --- a/examples/samples_tcod.py +++ b/examples/samples_tcod.py @@ -1,7 +1,5 @@ #!/usr/bin/env python3 -""" -This code demonstrates various usages of python-tcod. -""" +"""This code demonstrates various usages of python-tcod.""" # To the extent possible under law, the libtcod maintainers have waived all # copyright and related or neighboring rights to these samples. # https://creativecommons.org/publicdomain/zero/1.0/ @@ -1412,7 +1410,7 @@ def init_context(renderer: int) -> None: context = tcod.context.new( columns=root_console.width, rows=root_console.height, - title=f"python-tcod samples" f" (python-tcod {tcod.__version__}, libtcod {libtcod_version})", + title=f"python-tcod samples (python-tcod {tcod.__version__}, libtcod {libtcod_version})", renderer=renderer, vsync=False, # VSync turned off since this is for benchmarking. tileset=tileset, @@ -1488,7 +1486,19 @@ def handle_time() -> None: def handle_events() -> None: for event in tcod.event.get(): - context.convert_event(event) + if context.sdl_renderer: + # Manual handing of tile coordinates since context.present is skipped. + if isinstance(event, (tcod.event.MouseState, tcod.event.MouseMotion)): + event.tile = tcod.event.Point(event.pixel.x // tileset.tile_width, event.pixel.y // tileset.tile_height) + if isinstance(event, tcod.event.MouseMotion): + prev_tile = ( + (event.pixel[0] - event.pixel_motion[0]) // tileset.tile_width, + (event.pixel[1] - event.pixel_motion[1]) // tileset.tile_height, + ) + event.tile_motion = tcod.event.Point(event.tile[0] - prev_tile[0], event.tile[1] - prev_tile[1]) + else: + context.convert_event(event) + SAMPLES[cur_sample].dispatch(event) if isinstance(event, tcod.event.Quit): raise SystemExit() From 5a661306ee75c3a4dd6a8120e50ff8bd2e3a7fcc Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 9 Dec 2022 13:45:54 -0800 Subject: [PATCH 110/194] Various doc and spelling fixes. --- .vscode/settings.json | 99 +++++++++++++++++++++++++++++++++++++++++++ tcod/_internal.py | 17 +++----- tcod/bsp.py | 9 ++-- tcod/color.py | 3 +- tcod/console.py | 21 +++++---- tcod/context.py | 25 ++++++----- tcod/event.py | 14 +++--- tcod/libtcodpy.py | 37 ++++++++-------- tcod/loader.py | 3 +- tcod/los.py | 3 +- tcod/noise.py | 4 +- tcod/path.py | 32 +++++++------- tcod/random.py | 2 +- tcod/tileset.py | 8 ++-- 14 files changed, 181 insertions(+), 96 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 76f51720..e49af1b9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,12 +20,14 @@ }, "cSpell.words": [ "ADDA", + "ADDALPHA", "addressof", "addsub", "addx", "addy", "algo", "ALPH", + "ALTERASE", "arange", "ARCHS", "asarray", @@ -34,7 +36,13 @@ "astype", "atexit", "AUDIOCVT", + "AUDIOFASTFORWARD", + "AUDIOMUTE", + "AUDIONEXT", + "AUDIOPLAY", "AUDIOPREV", + "AUDIOREWIND", + "AUDIOSTOP", "autoclass", "autofunction", "autogenerated", @@ -45,17 +53,24 @@ "bezier", "bfade", "bgcolor", + "bitmask", "BKGND", "Blit", "blits", "blitting", + "BORDERLESS", "bresenham", "Bresenham", + "BRIGHTNESSDOWN", "BRIGHTNESSUP", "bysource", "caeldera", + "CAPSLOCK", + "caxis", + "cbutton", "ccoef", "cdef", + "cdevice", "cffi", "cflags", "CFLAGS", @@ -63,6 +78,8 @@ "Chebyshev", "cibuildwheel", "CIBW", + "CLEARAGAIN", + "CLEARENTRY", "CMWC", "Codacy", "Codecov", @@ -71,15 +88,28 @@ "coef", "Coef", "COLCTRL", + "COMPILEDVERSION", "consolas", + "contextdata", + "CONTROLLERAXISMOTION", + "CONTROLLERBUTTONDOWN", + "CONTROLLERBUTTONUP", + "CONTROLLERDEVICEADDED", + "CONTROLLERDEVICEREMAPPED", + "CONTROLLERDEVICEREMOVED", "cplusplus", "CPLUSPLUS", "CRSEL", "ctypes", + "CURRENCYSUBUNIT", + "CURRENCYUNIT", "currentmodule", "datas", + "DBLAMPERSAND", + "DBLVERTICALBAR", "dcost", "DCROSS", + "DECIMALSEPARATOR", "dejavu", "delocate", "deque", @@ -87,6 +117,7 @@ "DESATURATED", "devel", "DHLINE", + "DISPLAYSWITCH", "dlopen", "Doryen", "DTEEE", @@ -101,6 +132,7 @@ "epub", "EQUALSAS", "errorvf", + "EXCLAM", "EXSEL", "favicon", "ffade", @@ -110,6 +142,7 @@ "fmean", "fontx", "fonty", + "frombuffer", "fullscreen", "fwidth", "genindex", @@ -122,6 +155,7 @@ "heapify", "heightmap", "hflip", + "HIGHDPI", "hillclimb", "hline", "horiz", @@ -138,17 +172,32 @@ "INROW", "isinstance", "isort", + "itemsize", "itleref", "ivar", + "jaxis", + "jball", + "jbutton", + "jdevice", + "jhat", "jice", "jieba", + "JOYAXISMOTION", + "JOYBALLMOTION", + "JOYBUTTONDOWN", + "JOYBUTTONUP", + "JOYDEVICEADDED", + "JOYDEVICEREMOVED", + "JOYHATMOTION", "Kaczor", "KBDILLUMDOWN", "KBDILLUMTOGGLE", "KBDILLUMUP", "keychar", + "KEYDOWN", "keyname", "keypress", + "keysym", "KEYUP", "KMOD", "KPADD", @@ -162,6 +211,8 @@ "lbutton", "LCTRL", "LDFLAGS", + "LEFTBRACE", + "LEFTBRACKET", "LEFTPAREN", "lerp", "LGUI", @@ -180,8 +231,17 @@ "maxarray", "maxdepth", "mbutton", + "MEDIASELECT", "MEIPASS", + "MEMADD", + "MEMCLEAR", + "MEMDIVIDE", + "MEMMULTIPLY", + "MEMRECALL", + "MEMSTORE", + "MEMSUBTRACT", "mersenne", + "meshgrid", "mgrid", "milli", "minmax", @@ -189,7 +249,10 @@ "mipmaps", "MMASK", "modindex", + "MOUSEBUTTONDOWN", "MOUSEBUTTONUP", + "MOUSEMOTION", + "MOUSESTATE", "msilib", "MSVC", "msvcr", @@ -200,6 +263,7 @@ "namegen", "ndarray", "ndim", + "newaxis", "newh", "neww", "noarchive", @@ -217,16 +281,21 @@ "onefile", "OPENGL", "OPER", + "PAGEDOWN", "PAGEUP", "pathfinding", "pathlib", "pcpp", + "PERLIN", "PILCROW", "pilmode", "PIXELFORMAT", + "PLUSMINUS", "PRESENTVSYNC", "PRINTF", "printn", + "PRINTSCREEN", + "propname", "pycall", "pycparser", "pyinstaller", @@ -238,6 +307,7 @@ "PYTHONOPTIMIZE", "Pyup", "quickstart", + "QUOTEDBL", "RALT", "randomizer", "rbutton", @@ -249,6 +319,8 @@ "repr", "rgba", "RGUI", + "RIGHTBRACE", + "RIGHTBRACKET", "RIGHTPAREN", "RMASK", "rmeta", @@ -261,13 +333,16 @@ "scalex", "scaley", "Scancode", + "scancodes", "scipy", "scoef", + "SCROLLLOCK", "sdist", "SDL's", "SDLCALL", "sdlevent", "SDLK", + "seealso", "servernum", "setuptools", "SHADOWCAST", @@ -291,32 +366,56 @@ "TEEW", "TEXTUREACCESS", "thirdparty", + "THOUSANDSSEPARATOR", "Tileset", "tilesets", "tilesheet", + "tilesheets", "timeit", "toctree", "todos", "tolist", "tris", "truetype", + "typestr", "undoc", "Unifont", "unraisablehook", "unraiseable", "upscaling", + "userdata", "VAFUNC", + "VALUELIST", "vcoef", "venv", "vertic", + "VERTICALBAR", "vflip", "vline", + "VOLUMEDOWN", "VOLUMEUP", "voronoi", "VRAM", "vsync", "WASD", + "windowclose", + "windowenter", + "WINDOWEVENT", + "windowexposed", + "windowfocusgained", + "windowfocuslost", + "windowhidden", + "windowhittest", + "windowleave", + "windowmaximized", + "windowminimized", + "windowmoved", + "WINDOWPOS", "WINDOWRESIZED", + "windowrestored", + "windowshown", + "windowsizechanged", + "windowtakefocus", "xdst", "xrel", "xvfb", diff --git a/tcod/_internal.py b/tcod/_internal.py index bde088e9..08730332 100644 --- a/tcod/_internal.py +++ b/tcod/_internal.py @@ -1,5 +1,4 @@ -"""This module internal helper functions used by the rest of the library. -""" +"""This module internal helper functions used by the rest of the library.""" from __future__ import annotations import functools @@ -39,8 +38,7 @@ def pending_deprecate( category: Any = PendingDeprecationWarning, stacklevel: int = 0, ) -> Callable[[F], F]: - """Like deprecate, but the default parameters are filled out for a generic - pending deprecation warning.""" + """Like deprecate, but the default parameters are filled out for a generic pending deprecation warning.""" return deprecate(message, category, stacklevel) @@ -88,7 +86,7 @@ def _unpack_char_p(char_p: Any) -> str: def _int(int_or_str: Any) -> int: - "return an integer where a single character string may be expected" + """Return an integer where a single character string may be expected.""" if isinstance(int_or_str, str): return ord(int_or_str) if isinstance(int_or_str, bytes): @@ -125,8 +123,9 @@ def _fmt(string: str, stacklevel: int = 2) -> bytes: class _PropagateException: - """Context manager designed to propagate exceptions outside of a cffi - callback context. Normally cffi suppresses the exception. + """Context manager designed to propagate exceptions outside of a cffi callback context. + + Normally cffi suppresses the exception. When propagate is called this class will hold onto the error until the control flow leaves the context, then the error will be raised. @@ -148,9 +147,7 @@ def propagate(self, *exc_info: Any) -> None: self.exc_info = exc_info def __enter__(self) -> Callable[[Any], None]: - """Once in context, only the propagate call is needed to use this - class effectively. - """ + """Once in context, only the propagate call is needed to use this class effectively.""" return self.propagate def __exit__(self, type: Any, value: Any, traceback: Any) -> None: diff --git a/tcod/bsp.py b/tcod/bsp.py index 42f82460..23c54ef0 100644 --- a/tcod/bsp.py +++ b/tcod/bsp.py @@ -1,4 +1,4 @@ -""" +r""" The following example shows how to traverse the BSP tree using Python. This assumes `create_room` and `connect_rooms` will be replaced by custom code. @@ -19,7 +19,7 @@ for node in bsp.pre_order(): if node.children: node1, node2 = node.children - print('Connect the rooms:\\n%s\\n%s' % (node1, node2)) + print('Connect the rooms:\n%s\n%s' % (node1, node2)) else: print('Dig a room for %s.' % node) """ @@ -33,8 +33,7 @@ class BSP(object): - """A binary space partitioning tree which can be used for simple dungeon - generation. + """A binary space partitioning tree which can be used for simple dungeon generation. Attributes: x (int): Rectangle left coordinate. @@ -242,7 +241,7 @@ def inverted_level_order(self) -> Iterator[BSP]: yield from levels.pop() def contains(self, x: int, y: int) -> bool: - """Returns True if this node contains these coordinates. + """Return True if this node contains these coordinates. Args: x (int): X position to check. diff --git a/tcod/color.py b/tcod/color.py index fce63995..470d7ac7 100644 --- a/tcod/color.py +++ b/tcod/color.py @@ -11,7 +11,7 @@ class Color(List[int]): - """ + """Old-style libtcodpy color class. Args: r (int): Red value, from 0 to 255. @@ -66,7 +66,6 @@ def b(self, value: int) -> None: @classmethod def _new_from_cdata(cls, cdata: Any) -> Color: - """new in libtcod-cffi""" return cls(cdata.r, cdata.g, cdata.b) def __getitem__(self, index: Any) -> Any: diff --git a/tcod/console.py b/tcod/console.py index c5dfbbff..81f209d9 100644 --- a/tcod/console.py +++ b/tcod/console.py @@ -45,8 +45,7 @@ def _fmt(string: str) -> bytes: class Console: - """A console object containing a grid of characters with - foreground/background colors. + """A console object containing a grid of characters with foreground/background colors. `width` and `height` are the size of the console (in tiles.) @@ -771,7 +770,7 @@ def blit( None, or a (red, green, blue) tuple with values of 0-255. .. versionchanged:: 4.0 - Parameters were rearraged and made optional. + Parameters were rearranged and made optional. Previously they were: `(x, y, width, height, dest, dest_x, dest_y, *)` @@ -841,7 +840,7 @@ def set_key_color(self, color: Optional[Tuple[int, int, int]]) -> None: self._key_color = color def __enter__(self) -> Console: - """Returns this console in a managed context. + """Return this console in a managed context. When the root console is used as a context, the graphical window will close once the context is left as if :any:`tcod.console_delete` was @@ -870,14 +869,14 @@ def close(self) -> None: lib.TCOD_console_delete(self.console_c) def __exit__(self, *args: Any) -> None: - """Closes the graphical window on exit. + """Close the graphical window on exit. Some tcod functions may have undefined behavior after this point. """ self.close() def __bool__(self) -> bool: - """Returns False if this is the root console. + """Return False if this is the root console. This mimics libtcodpy behavior. """ @@ -939,14 +938,14 @@ def print( bg_blend: int = tcod.constants.BKGND_SET, alignment: int = tcod.constants.LEFT, ) -> None: - """Print a string on a console with manual line breaks. + r"""Print a string on a console with manual line breaks. `x` and `y` are the starting tile, with ``0,0`` as the upper-left corner of the console. `string` is a Unicode string which may include color control characters. Strings which are too long will be truncated until the - next newline character ``"\\n"``. + next newline character ``"\n"``. `fg` and `bg` are the foreground text color and background tile color respectfully. This is a 3-item tuple with (r, g, b) color values from @@ -1051,7 +1050,7 @@ def draw_frame( *, decoration: Union[str, Tuple[int, int, int, int, int, int, int, int, int]] = "┌─┐│ │└─┘", ) -> None: - """Draw a framed rectangle with an optional title. + r"""Draw a framed rectangle with an optional title. `x` and `y` are the starting tile, with ``0,0`` as the upper-left corner of the console. @@ -1104,9 +1103,9 @@ def draw_frame( >>> console.print_box(x=0, y=5, width=12, height=1, string="┤Lower├", alignment=tcod.CENTER) 1 >>> print(console) - <┌─┐╔═╗123/-\\ + <┌─┐╔═╗123/-\ │ │║ ║456| | - └─┘╚═╝789\\-/ + └─┘╚═╝789\-/ ┌─ Title ──┐ │ │ └─┤Lower├──┘> diff --git a/tcod/context.py b/tcod/context.py index c2e57bee..a77562e4 100644 --- a/tcod/context.py +++ b/tcod/context.py @@ -168,7 +168,7 @@ class Context: """ def __init__(self, context_p: Any): - """Creates a context from a cffi pointer.""" + """Create a context from a cffi pointer.""" self._context_p = context_p @classmethod @@ -176,7 +176,7 @@ def _claim(cls, context_p: Any) -> Context: return cls(ffi.gc(context_p, lib.TCOD_context_delete)) def __enter__(self) -> Context: - """This context can be used as a context manager.""" + """Enter this context which will close on exiting.""" return self def close(self) -> None: @@ -189,7 +189,7 @@ def close(self) -> None: del self._context_p def __exit__(self, *args: Any) -> None: - """The libtcod context is closed as this context manager exits.""" + """Automatically close on the context on exit.""" self.close() def present( @@ -335,7 +335,7 @@ def new_console( console = context.new_console(magnification=scale) # This printed output will wrap if the window is shrunk. console.print_box(0, 0, console.width, console.height, "Hello world") - # Use integer_scaling to prevent subpixel distorsion. + # Use integer_scaling to prevent subpixel distortion. # This may add padding around the rendered console. context.present(console, integer_scaling=True) for event in tcod.event.wait(): @@ -353,13 +353,11 @@ def new_console( return tcod.console.Console(width, height, order=order) def recommended_console_size(self, min_columns: int = 1, min_rows: int = 1) -> Tuple[int, int]: - """Return the recommended (columns, rows) of a console for this - context. + """Return the recommended (columns, rows) of a console for this context. `min_columns`, `min_rows` are the lowest values which will be returned. - If result is only used to create a new console then you may want to - call :any:`Context.new_console` instead. + If result is only used to create a new console then you may want to call :any:`Context.new_console` instead. """ with ffi.new("int[2]") as size: _check(lib.TCOD_context_recommended_console_size(self._context_p, 1.0, size, size + 1)) @@ -407,7 +405,7 @@ def sdl_window(self) -> Optional[tcod.sdl.video.Window]: Example:: import tcod - improt tcod.sdl.video + import tcod.sdl.video def toggle_fullscreen(context: tcod.context.Context) -> None: """Toggle a context window between fullscreen and windowed modes.""" @@ -445,15 +443,16 @@ def sdl_atlas(self) -> Optional[tcod.render.SDLTilesetAtlas]: return tcod.render.SDLTilesetAtlas._from_ref(context_data.renderer, context_data.atlas) def __reduce__(self) -> NoReturn: - """Contexts can not be pickled, so this class will raise - :class:`pickle.PicklingError`. - """ + """Contexts can not be pickled, so this class will raise :class:`pickle.PicklingError`.""" raise pickle.PicklingError("Python-tcod contexts can not be pickled.") @ffi.def_extern() # type: ignore def _pycall_cli_output(catch_reference: Any, output: Any) -> None: - """Callback for the libtcod context CLI. Catches the CLI output.""" + """Callback for the libtcod context CLI. + + Catches the CLI output. + """ catch: List[str] = ffi.from_handle(catch_reference) catch.append(ffi.string(output).decode("utf-8")) diff --git a/tcod/event.py b/tcod/event.py index 7be7eebd..dbb8e6b0 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -185,7 +185,7 @@ def _init_sdl_video() -> None: class Modifier(enum.IntFlag): - """Keyboard modifier flags, a bitfield of held modifier keys. + """Keyboard modifier flags, a bit-field of held modifier keys. Use `bitwise and` to check if a modifier key is held. @@ -195,8 +195,6 @@ class Modifier(enum.IntFlag): Example:: >>> mod = tcod.event.Modifier(4098) - >>> mod - >>> mod & tcod.event.Modifier.SHIFT # Check if any shift key is held. >>> mod & tcod.event.Modifier.LSHIFT # Check if left shift key is held. @@ -1518,7 +1516,7 @@ def add_watch(callback: _EventCallback) -> _EventCallback: .. warning:: How uncaught exceptions in a callback are handled is not currently defined by tcod. They will likely be handled by :any:`sys.unraisablehook`. - This may be later changed to pass the excpetion to a :any:`tcod.event.get` or :any:`tcod.event.wait` call. + This may be later changed to pass the exception to a :any:`tcod.event.get` or :any:`tcod.event.wait` call. Args: callback (Callable[[Event], None]): @@ -1544,7 +1542,7 @@ def handle_events(event: tcod.event.Event) -> None: def remove_watch(callback: Callable[[Event], None]) -> None: - """Remove a callback as an event wacher. + """Remove a callback as an event watcher. Args: callback (Callable[[Event], None]): @@ -1580,9 +1578,9 @@ def get_keyboard_state() -> NDArray[np.bool_]: .. versionadded:: 12.3 """ - numkeys = ffi.new("int[1]") - keyboard_state = lib.SDL_GetKeyboardState(numkeys) - out: NDArray[np.bool_] = np.frombuffer(ffi.buffer(keyboard_state[0 : numkeys[0]]), dtype=np.bool_) + num_keys = ffi.new("int[1]") + keyboard_state = lib.SDL_GetKeyboardState(num_keys) + out: NDArray[np.bool_] = np.frombuffer(ffi.buffer(keyboard_state[0 : num_keys[0]]), dtype=np.bool_) out.flags["WRITEABLE"] = False # This buffer is supposed to be const. return out diff --git a/tcod/libtcodpy.py b/tcod/libtcodpy.py index 626a05b6..1271a3c1 100644 --- a/tcod/libtcodpy.py +++ b/tcod/libtcodpy.py @@ -1,5 +1,4 @@ -"""This module handles backward compatibility with the ctypes libtcodpy module. -""" +"""This module handles backward compatibility with the ctypes libtcodpy module.""" from __future__ import annotations import atexit @@ -72,8 +71,7 @@ def BKGND_ADDALPHA(a: int) -> int: class ConsoleBuffer(object): - """Simple console that allows direct (fast) access to cells. simplifies - use of the "fill" functions. + """Simple console that allows direct (fast) access to cells. Simplifies use of the "fill" functions. .. deprecated:: 6.0 Console array attributes perform better than this class. @@ -102,8 +100,9 @@ def __init__( fore_b: int = 0, char: str = " ", ) -> None: - """initialize with given width and height. values to fill the buffer - are optional, defaults to black with no characters. + """Initialize with given width and height. + + Values to fill the buffer are optional, defaults to black with no characters. """ warnings.warn( "Console array attributes perform better than this class.", @@ -124,8 +123,9 @@ def clear( fore_b: int = 0, char: str = " ", ) -> None: - """Clears the console. Values to fill it with are optional, defaults - to black with no characters. + """Clear the console. + + Values to fill it with are optional, defaults to black with no characters. Args: back_r (int): Red background color, from 0 to 255. @@ -146,7 +146,7 @@ def clear( self.char = [ord(char)] * n def copy(self) -> ConsoleBuffer: - """Returns a copy of this ConsoleBuffer. + """Return a copy of this ConsoleBuffer. Returns: ConsoleBuffer: A new ConsoleBuffer copy. @@ -332,7 +332,7 @@ def __repr__(self) -> str: class Key(_CDataWrapper): - """Key Event instance + r"""Key Event instance Attributes: vk (int): TCOD_keycode_t key code @@ -631,9 +631,7 @@ def _bsp_traverse( callback: Callable[[tcod.bsp.BSP, Any], None], userData: Any, ) -> None: - """pack callback into a handle for use with the callback - _pycall_bsp_callback - """ + """Pack callback into a handle for use with the callback _pycall_bsp_callback.""" for node in node_iter: callback(node, userData) @@ -1057,12 +1055,11 @@ def console_map_ascii_codes_to_font(firstAsciiCode: int, nbCodes: int, fontCharX @deprecate("Setup fonts using the tcod.tileset module.") def console_map_string_to_font(s: str, fontCharX: int, fontCharY: int) -> None: - """Remap a string of codes to a contiguous set of tiles. + r"""Remap a string of codes to a contiguous set of tiles. Args: s (AnyStr): A string of character codes to map to new values. - The null character `'\\x00'` will prematurely end this - function. + Any null character `'\x00'` will prematurely end the printed text. fontCharX (int): The starting X tile coordinate on the loaded tileset. 0 is the leftmost tile. fontCharY (int): The starting Y tile coordinate on the loaded tileset. @@ -3144,7 +3141,7 @@ def line(xo: int, yo: int, xd: int, yd: int, py_callback: Callable[[int, int], b A callback which takes x and y parameters and returns bool. Returns: - bool: False if the callback cancels the line interation by + bool: False if the callback cancels the line interaction by returning False or None, otherwise True. .. deprecated:: 2.0 @@ -3947,7 +3944,7 @@ def sys_elapsed_milli() -> int: """Get number of milliseconds since the start of the program. Returns: - int: Time since the progeam has started in milliseconds. + int: Time since the program has started in milliseconds. .. deprecated:: 2.0 Use Python's :mod:`time` module instead. @@ -3960,7 +3957,7 @@ def sys_elapsed_seconds() -> float: """Get number of seconds since the start of the program. Returns: - float: Time since the progeam has started in seconds. + float: Time since the program has started in seconds. .. deprecated:: 2.0 Use Python's :mod:`time` module instead. @@ -4097,7 +4094,7 @@ def sys_update_char( @deprecate("This function is not supported if contexts are being used.") def sys_register_SDL_renderer(callback: Callable[[Any], None]) -> None: - """Register a custom randering function with libtcod. + """Register a custom rendering function with libtcod. Note: This callback will only be called by the SDL renderer. diff --git a/tcod/loader.py b/tcod/loader.py index 645246ba..13efe66d 100644 --- a/tcod/loader.py +++ b/tcod/loader.py @@ -1,5 +1,4 @@ -"""This module handles loading of the libtcod cffi API. -""" +"""This module handles loading of the libtcod cffi API.""" from __future__ import annotations import os diff --git a/tcod/los.py b/tcod/los.py index ce48d23b..9cbd87da 100644 --- a/tcod/los.py +++ b/tcod/los.py @@ -1,5 +1,4 @@ -"""This modules holds functions for NumPy-based line of sight algorithms. -""" +"""This modules holds functions for NumPy-based line of sight algorithms.""" from __future__ import annotations from typing import Any, Tuple diff --git a/tcod/noise.py b/tcod/noise.py index 7328965d..92676c8a 100644 --- a/tcod/noise.py +++ b/tcod/noise.py @@ -181,7 +181,7 @@ def dimensions(self) -> int: return int(self._tdl_noise_c.dimensions) @property - @deprecate("This is a misspelling of 'dimensions'.") + @deprecate("This is a misspelling of 'dimensions'.", FutureWarning) def dimentions(self) -> int: return self.dimensions @@ -416,7 +416,7 @@ def grid( origin: Optional[Tuple[int, ...]] = None, indexing: Literal["ij", "xy"] = "xy", ) -> Tuple[NDArray[Any], ...]: - """A helper function for generating a grid of noise samples. + """Helper function for generating a grid of noise samples. `shape` is the shape of the returned mesh grid. This can be any number of dimensions, but :class:`Noise` classes only support up to 4. diff --git a/tcod/path.py b/tcod/path.py index d581e7da..d3c9a514 100644 --- a/tcod/path.py +++ b/tcod/path.py @@ -33,7 +33,7 @@ @ffi.def_extern() # type: ignore def _pycall_path_old(x1: int, y1: int, x2: int, y2: int, handle: Any) -> float: - """libtcodpy style callback, needs to preserve the old userData issue.""" + """Libtcodpy style callback, needs to preserve the old userData issue.""" func, userData = ffi.from_handle(handle) return func(x1, y1, x2, y2, userData) # type: ignore @@ -56,7 +56,7 @@ def _pycall_path_dest_only(x1: int, y1: int, x2: int, y2: int, handle: Any) -> f return ffi.from_handle(handle)(x2, y2) # type: ignore -def _get_pathcost_func( +def _get_path_cost_func( name: str, ) -> Callable[[int, int, int, int, Any], float]: """Return a properly cast PathCostArray callback.""" @@ -80,7 +80,7 @@ def __init__(self, userdata: Any, shape: Tuple[int, int]) -> None: self.shape = shape def get_tcod_path_ffi(self) -> Tuple[Any, Any, Tuple[int, int]]: - """Return (C callback, userdata handle, shape)""" + """Return (C callback, userdata handle, shape).""" return self._CALLBACK_P, ffi.new_handle(self._userdata), self.shape def __repr__(self) -> str: @@ -123,14 +123,14 @@ class NodeCostArray(np.ndarray): # type: ignore """ _C_ARRAY_CALLBACKS = { - np.float32: ("float*", _get_pathcost_func("PathCostArrayFloat32")), - np.bool_: ("int8_t*", _get_pathcost_func("PathCostArrayInt8")), - np.int8: ("int8_t*", _get_pathcost_func("PathCostArrayInt8")), - np.uint8: ("uint8_t*", _get_pathcost_func("PathCostArrayUInt8")), - np.int16: ("int16_t*", _get_pathcost_func("PathCostArrayInt16")), - np.uint16: ("uint16_t*", _get_pathcost_func("PathCostArrayUInt16")), - np.int32: ("int32_t*", _get_pathcost_func("PathCostArrayInt32")), - np.uint32: ("uint32_t*", _get_pathcost_func("PathCostArrayUInt32")), + np.float32: ("float*", _get_path_cost_func("PathCostArrayFloat32")), + np.bool_: ("int8_t*", _get_path_cost_func("PathCostArrayInt8")), + np.int8: ("int8_t*", _get_path_cost_func("PathCostArrayInt8")), + np.uint8: ("uint8_t*", _get_path_cost_func("PathCostArrayUInt8")), + np.int16: ("int16_t*", _get_path_cost_func("PathCostArrayInt16")), + np.uint16: ("uint16_t*", _get_path_cost_func("PathCostArrayUInt16")), + np.int32: ("int32_t*", _get_path_cost_func("PathCostArrayInt32")), + np.uint32: ("uint32_t*", _get_path_cost_func("PathCostArrayUInt32")), } def __new__(cls, array: ArrayLike) -> NodeCostArray: @@ -676,12 +676,12 @@ def __init__(self, shape: Tuple[int, ...], *, order: str = "C"): @property def ndim(self) -> int: - """The number of dimensions.""" + """Return the number of dimensions.""" return self._ndim @property def shape(self) -> Tuple[int, ...]: - """The shape of this graph.""" + """Return the shape of this graph.""" return self._shape def add_edge( @@ -895,7 +895,7 @@ def add_edges( self.add_edge(edge, edge_cost, cost=cost, condition=condition) def set_heuristic(self, *, cardinal: int = 0, diagonal: int = 0, z: int = 0, w: int = 0) -> None: - """Sets a pathfinder heuristic so that pathfinding can done with A*. + """Set a pathfinder heuristic so that pathfinding can done with A*. `cardinal`, `diagonal`, `z, and `w` are the lower-bound cost of movement in those directions. Values above the lower-bound can be @@ -1092,7 +1092,7 @@ def __init__(self, graph: Union[CustomGraph, SimpleGraph]): @property def distance(self) -> NDArray[Any]: - """The distance values of the pathfinder. + """Distance values of the pathfinder. The array returned from this property maintains the graphs `order`. @@ -1112,7 +1112,7 @@ def distance(self) -> NDArray[Any]: @property def traversal(self) -> NDArray[Any]: - """An array used to generate paths from any point to the nearest root. + """Array used to generate paths from any point to the nearest root. The array returned from this property maintains the graphs `order`. It has an extra dimension which includes the index of the next path. diff --git a/tcod/random.py b/tcod/random.py index ef64df2c..013dd6ab 100644 --- a/tcod/random.py +++ b/tcod/random.py @@ -151,7 +151,7 @@ def __getstate__(self) -> Any: return state def __setstate__(self, state: Any) -> None: - """Create a new cdata object with the stored paramaters.""" + """Create a new cdata object with the stored parameters.""" if "algo" in state["random_c"]: # Handle old/deprecated format. Covert to libtcod's new union type. state["random_c"]["algorithm"] = state["random_c"]["algo"] diff --git a/tcod/tileset.py b/tcod/tileset.py index 1e72559f..18879474 100644 --- a/tcod/tileset.py +++ b/tcod/tileset.py @@ -54,17 +54,17 @@ def _from_ref(cls, tileset_p: Any) -> Tileset: @property def tile_width(self) -> int: - """The width of the tile in pixels.""" + """Width of the tile in pixels.""" return int(lib.TCOD_tileset_get_tile_width_(self._tileset_p)) @property def tile_height(self) -> int: - """The height of the tile in pixels.""" + """Height of the tile in pixels.""" return int(lib.TCOD_tileset_get_tile_height_(self._tileset_p)) @property def tile_shape(self) -> Tuple[int, int]: - """The shape (height, width) of the tile in pixels.""" + """Shape (height, width) of the tile in pixels.""" return self.tile_height, self.tile_width def __contains__(self, codepoint: int) -> bool: @@ -357,7 +357,7 @@ def load_tilesheet( def procedural_block_elements(*, tileset: Tileset) -> None: - """Overwrites the block element codepoints in `tileset` with prodecually generated glyphs. + """Overwrite the block element codepoints in `tileset` with procedurally generated glyphs. Args: tileset (Tileset): A :any:`Tileset` with tiles of any shape. From 3ed5b15ada10ca4e7bb954a9dcd185fc704d7ed5 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 9 Dec 2022 14:54:01 -0800 Subject: [PATCH 111/194] Add explicit support for namespace packages. Fixes work that I'm trying out with new packages which I want to be in the tcod namespace. --- CHANGELOG.md | 3 +++ tcod/__init__.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e86b05e0..5626f7c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] +### Added +- Added explicit support for namespace packages. + ### Changed - Using `libtcod 1.23.1`. - Bundle `SDL 2.26.0` on Windows and MacOS. diff --git a/tcod/__init__.py b/tcod/__init__.py index 91f41cc6..92885a2f 100644 --- a/tcod/__init__.py +++ b/tcod/__init__.py @@ -10,6 +10,9 @@ import sys import warnings +from pkgutil import extend_path + +__path__ = extend_path(__path__, __name__) from tcod import bsp, color, console, context, event, image, los, map, noise, path, random, tileset from tcod.console import Console # noqa: F401 From 42083ba4753792537d8cef77797cf6fba8c0f1e3 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 9 Dec 2022 15:09:05 -0800 Subject: [PATCH 112/194] Update classifiers. --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index c358930c..4dadf34f 100755 --- a/setup.py +++ b/setup.py @@ -144,11 +144,13 @@ def check_sdl_version() -> None: "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Games/Entertainment", "Topic :: Multimedia :: Graphics", "Topic :: Software Development :: Libraries :: Python Modules", + "Typing :: Typed", ], keywords="roguelike cffi Unicode libtcod field-of-view pathfinding", platforms=["Windows", "MacOS", "Linux"], From b6fdc960af0d575b6996be4f32187398a89a26b5 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 9 Dec 2022 15:14:53 -0800 Subject: [PATCH 113/194] Arrange workflows to avoid needless job running. Skip main tests if linters fail. Do aarch64 last since it takes forever. --- .github/workflows/python-package.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index e0acab53..fafeff34 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -91,6 +91,7 @@ jobs: SDL_VERSION: ${{ matrix.sdl-version }} build: + needs: [black, isort, flake8, mypy] runs-on: ${{ matrix.os }} strategy: matrix: @@ -197,6 +198,7 @@ jobs: python -c "import tcod" linux-wheels: + needs: build # These take a while to build/test, so wait for normal tests to pass first. runs-on: "ubuntu-20.04" strategy: matrix: @@ -250,6 +252,7 @@ jobs: twine upload --skip-existing wheelhouse/* build-macos: + needs: [black, isort, flake8, mypy] runs-on: "macos-10.15" strategy: fail-fast: true From 117943a366dc09a642b975b801a1763cfaff9477 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 9 Dec 2022 16:18:20 -0800 Subject: [PATCH 114/194] Prepare 14.0.0 release. --- CHANGELOG.md | 2 ++ tcod/tileset.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5626f7c6..cccfb653 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] + +## [14.0.0] - 2022-12-09 ### Added - Added explicit support for namespace packages. diff --git a/tcod/tileset.py b/tcod/tileset.py index 18879474..ac418f1b 100644 --- a/tcod/tileset.py +++ b/tcod/tileset.py @@ -720,7 +720,7 @@ def procedural_block_elements(*, tileset: Tileset) -> None: .. versionadded:: 11.12 -.. versionchanged:: Unreleased +.. versionchanged:: 14.0 Character at index ``0x7F`` was changed from value ``0x7F`` to the HOUSE ``⌂`` glyph ``0x2302``. """ From d88e5924833f8a4b4039840a9e7258ce0620505d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Dec 2022 16:33:20 +0000 Subject: [PATCH 115/194] Bump setuptools from 60.8.2 to 65.5.1 Bumps [setuptools](https://github.com/pypa/setuptools) from 60.8.2 to 65.5.1. - [Release notes](https://github.com/pypa/setuptools/releases) - [Changelog](https://github.com/pypa/setuptools/blob/main/CHANGES.rst) - [Commits](https://github.com/pypa/setuptools/compare/v60.8.2...v65.5.1) --- updated-dependencies: - dependency-name: setuptools dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a6fc227e..7bb32eec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ cffi>=1.15 numpy>=1.21.4 pycparser>=2.14 requests>=2.28.1 -setuptools==60.8.2 +setuptools==65.5.1 types-requests types-setuptools types-tabulate From 15f9765e1de10c84bbc9f6e53281789b5b993077 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 30 Dec 2022 19:36:58 -0800 Subject: [PATCH 116/194] Fix window even case not matching its type hints. --- CHANGELOG.md | 3 +++ tcod/event.py | 12 +++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cccfb653..95120c50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] +### Changed +- Updated the case of window event types to match their type annotations. + This may cause regressions. Run Mypy to check for ``[comparison-overlap]`` errors. ## [14.0.0] - 2022-12-09 ### Added diff --git a/tcod/event.py b/tcod/event.py index dbb8e6b0..13bd8aa8 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -650,10 +650,7 @@ def __str__(self) -> str: class WindowEvent(Event): - """ - Attributes: - type (str): A window event could mean various event types. - """ + """A window event.""" type: Final[ # type: ignore[misc] # Narrowing final type. Literal[ @@ -675,12 +672,13 @@ class WindowEvent(Event): "WindowHitTest", ] ] + """The current window event. This can be one of various options.""" @classmethod def from_sdl_event(cls, sdl_event: Any) -> Union[WindowEvent, Undefined]: if sdl_event.window.event not in cls.__WINDOW_TYPES: return Undefined.from_sdl_event(sdl_event) - event_type: Final = cls.__WINDOW_TYPES[sdl_event.window.event].upper() + event_type: Final = cls.__WINDOW_TYPES[sdl_event.window.event] self: WindowEvent if sdl_event.window.event == lib.SDL_WINDOWEVENT_MOVED: self = WindowMoved(sdl_event.window.data1, sdl_event.window.data2) @@ -720,12 +718,12 @@ def __repr__(self) -> str: class WindowMoved(WindowEvent): """ Attributes: - type (str): Always "WINDOWMOVED". x (int): Movement on the x-axis. y (int): Movement on the y-axis. """ type: Final[Literal["WINDOWMOVED"]] # type: ignore[assignment,misc] + """Always "WINDOWMOVED".""" def __init__(self, x: int, y: int) -> None: super().__init__(None) @@ -751,12 +749,12 @@ def __str__(self) -> str: class WindowResized(WindowEvent): """ Attributes: - type (str): "WINDOWRESIZED" or "WINDOWSIZECHANGED" width (int): The current width of the window. height (int): The current height of the window. """ type: Final[Literal["WINDOWRESIZED", "WINDOWSIZECHANGED"]] # type: ignore[assignment,misc] + """WINDOWRESIZED" or "WINDOWSIZECHANGED""" def __init__(self, type: str, width: int, height: int) -> None: super().__init__(type) From 15241974baa5d3521301ee9711fdfe6908f49159 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 1 Jan 2023 02:17:50 -0800 Subject: [PATCH 117/194] Update copyright years. --- LICENSE.txt | 2 +- docs/conf.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index c00f298e..d91bf759 100755 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ BSD 2-Clause License -Copyright (c) 2009-2021, Kyle Benesch and the python-tcod contributors. +Copyright (c) 2009-2023, Kyle Benesch and the python-tcod contributors. All rights reserved. Redistribution and use in source and binary forms, with or without diff --git a/docs/conf.py b/docs/conf.py index cb508dc3..de4337ee 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -60,7 +60,7 @@ # General information about the project. project = "python-tcod" -copyright = "2009-2021, Kyle Benesch" +copyright = "2009-2023, Kyle Benesch" author = "Kyle Benesch" # The version info for the project you're documenting, acts as replacement for From d1d85a99e5872d96e2bbf1a298de78b4ece0b651 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 1 Jan 2023 22:15:46 -0800 Subject: [PATCH 118/194] Refactor events to deprecate mouse tile attributes. Renamed mouse pixel attributes to position and motion. Old names have been deprecated. Context.convert_event now returns an event with tile coordinates active. --- examples/samples_tcod.py | 12 +++-- tcod/context.py | 39 +++++++++----- tcod/event.py | 112 +++++++++++++++++++++++++++++++-------- tcod/sdl/mouse.py | 6 +-- 4 files changed, 124 insertions(+), 45 deletions(-) diff --git a/examples/samples_tcod.py b/examples/samples_tcod.py index f14c9c4b..60bd8416 100755 --- a/examples/samples_tcod.py +++ b/examples/samples_tcod.py @@ -1105,8 +1105,8 @@ def on_draw(self) -> None: "Right button : %s\n" "Middle button : %s\n" % ( - self.motion.pixel.x, - self.motion.pixel.y, + self.motion.position.x, + self.motion.position.y, self.motion.tile.x, self.motion.tile.y, self.motion.tile_motion.x, @@ -1489,11 +1489,13 @@ def handle_events() -> None: if context.sdl_renderer: # Manual handing of tile coordinates since context.present is skipped. if isinstance(event, (tcod.event.MouseState, tcod.event.MouseMotion)): - event.tile = tcod.event.Point(event.pixel.x // tileset.tile_width, event.pixel.y // tileset.tile_height) + event.tile = tcod.event.Point( + event.position.x // tileset.tile_width, event.position.y // tileset.tile_height + ) if isinstance(event, tcod.event.MouseMotion): prev_tile = ( - (event.pixel[0] - event.pixel_motion[0]) // tileset.tile_width, - (event.pixel[1] - event.pixel_motion[1]) // tileset.tile_height, + (event.position[0] - event.motion[0]) // tileset.tile_width, + (event.position[1] - event.motion[1]) // tileset.tile_height, ) event.tile_motion = tcod.event.Point(event.tile[0] - prev_tile[0], event.tile[1] - prev_tile[1]) else: diff --git a/tcod/context.py b/tcod/context.py index a77562e4..414f1bd2 100644 --- a/tcod/context.py +++ b/tcod/context.py @@ -49,11 +49,12 @@ """ # noqa: E501 from __future__ import annotations +import copy import os import pickle import sys import warnings -from typing import Any, Iterable, List, Optional, Tuple +from typing import Any, Iterable, List, Optional, Tuple, TypeVar from typing_extensions import Literal, NoReturn @@ -87,6 +88,8 @@ "RENDERER_XTERM", ) +_Event = TypeVar("_Event", bound=tcod.event.Event) + SDL_WINDOW_FULLSCREEN = lib.SDL_WINDOW_FULLSCREEN """Exclusive fullscreen mode. @@ -247,30 +250,38 @@ def pixel_to_subtile(self, x: int, y: int) -> Tuple[float, float]: _check(lib.TCOD_context_screen_pixel_to_tile_d(self._context_p, xy, xy + 1)) return xy[0], xy[1] - def convert_event(self, event: tcod.event.Event) -> None: - """Fill in the tile coordinates of a mouse event using this context. + def convert_event(self, event: _Event) -> _Event: + """Return an event with mouse pixel coordinates converted into tile coordinates. Example:: context: tcod.context.Context for event in tcod.event.get(): + event_tile = context.convert_event(event) if isinstance(event, tcod.event.MouseMotion): - # Pixel coordinates are always accessible. - print(f"{event.pixel=}, {event.pixel_motion=}") - context.convert_event(event) - if isinstance(event, tcod.event.MouseMotion): - # Now tile coordinate attributes can be accessed. - print(f"{event.tile=}, {event.tile_motion=}") - # A warning will be raised if you try to access these without convert_event. + # Events start with pixel coordinates and motion. + print(f"Pixels: {event.position=}, {event.motion=}") + if isinstance(event_tile, tcod.event.MouseMotion): + # Tile coordinates are used in the returned event. + print(f"Tiles: {event_tile.position=}, {event_tile.motion=}") + + .. versionchanged:: Unreleased + Now returns a new event with the coordinates converted into tiles. """ + event_copy = copy.copy(event) if isinstance(event, (tcod.event.MouseState, tcod.event.MouseMotion)): - event.tile = tcod.event.Point(*self.pixel_to_tile(*event.pixel)) + assert isinstance(event_copy, (tcod.event.MouseState, tcod.event.MouseMotion)) + event_copy.position = event.tile = tcod.event.Point(*self.pixel_to_tile(*event.position)) if isinstance(event, tcod.event.MouseMotion): + assert isinstance(event_copy, tcod.event.MouseMotion) prev_tile = self.pixel_to_tile( - event.pixel[0] - event.pixel_motion[0], - event.pixel[1] - event.pixel_motion[1], + event.position[0] - event.motion[0], + event.position[1] - event.motion[1], + ) + event_copy.motion = event.tile_motion = tcod.event.Point( + event.tile[0] - prev_tile[0], event.tile[1] - prev_tile[1] ) - event.tile_motion = tcod.event.Point(event.tile[0] - prev_tile[0], event.tile[1] - prev_tile[1]) + return event_copy def save_screenshot(self, path: Optional[str] = None) -> None: """Save a screen-shot to the given file path.""" diff --git a/tcod/event.py b/tcod/event.py index 13bd8aa8..58ca9bac 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -384,7 +384,7 @@ class MouseState(Event): """ Attributes: type (str): Always "MOUSESTATE". - pixel (Point): The pixel coordinates of the mouse. + position (Point): The position coordinates of the mouse. tile (Point): The integer tile coordinates of the mouse on the screen. state (int): A bitmask of which mouse buttons are currently held. @@ -397,39 +397,70 @@ class MouseState(Event): * tcod.event.BUTTON_X2MASK .. versionadded:: 9.3 + + .. versionchanged:: Unreleased + Renamed `pixel` attribute to `position`. """ def __init__( self, - pixel: Tuple[int, int] = (0, 0), + position: Tuple[int, int] = (0, 0), tile: Optional[Tuple[int, int]] = (0, 0), state: int = 0, ): super().__init__() - self.pixel = Point(*pixel) + self.position = Point(*position) self.__tile = Point(*tile) if tile is not None else None self.state = state + @property + def pixel(self) -> Point: + warnings.warn( + "The mouse.pixel attribute is deprecated. Use mouse.position instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.position + + @pixel.setter + def pixel(self, value: Point) -> None: + warnings.warn( + "The mouse.pixel attribute is deprecated. Use mouse.position instead.", + DeprecationWarning, + stacklevel=2, + ) + self.position = value + @property def tile(self) -> Point: + warnings.warn( + "The mouse.tile attribute is deprecated. Use mouse.position of the event returned by context.convert_event instead.", + DeprecationWarning, + stacklevel=2, + ) return _verify_tile_coordinates(self.__tile) @tile.setter def tile(self, xy: Tuple[int, int]) -> None: + warnings.warn( + "The mouse.tile attribute is deprecated. Use mouse.position of the event returned by context.convert_event instead.", + DeprecationWarning, + stacklevel=2, + ) self.__tile = Point(*xy) def __repr__(self) -> str: - return ("tcod.event.%s(pixel=%r, tile=%r, state=%s)") % ( + return ("tcod.event.%s(position=%r, tile=%r, state=%s)") % ( self.__class__.__name__, - tuple(self.pixel), + tuple(self.position), tuple(self.tile), _describe_bitmask(self.state, _REVERSE_BUTTON_MASK_TABLE_PREFIX), ) def __str__(self) -> str: - return ("<%s, pixel=(x=%i, y=%i), tile=(x=%i, y=%i), state=%s>") % ( + return ("<%s, position=(x=%i, y=%i), tile=(x=%i, y=%i), state=%s>") % ( super().__str__().strip("<>"), - *self.pixel, + *self.position, *self.tile, _describe_bitmask(self.state, _REVERSE_BUTTON_MASK_TABLE), ) @@ -439,8 +470,8 @@ class MouseMotion(MouseState): """ Attributes: type (str): Always "MOUSEMOTION". - pixel (Point): The pixel coordinates of the mouse. - pixel_motion (Point): The pixel delta. + position (Point): The pixel coordinates of the mouse. + motion (Point): The pixel delta. tile (Point): The integer tile coordinates of the mouse on the screen. tile_motion (Point): The integer tile delta. state (int): A bitmask of which mouse buttons are currently held. @@ -452,26 +483,60 @@ class MouseMotion(MouseState): * tcod.event.BUTTON_RMASK * tcod.event.BUTTON_X1MASK * tcod.event.BUTTON_X2MASK + + .. versionchanged:: Unreleased + Renamed `pixel` attribute to `position`. + Renamed `pixel_motion` attribute to `motion`. """ def __init__( self, - pixel: Tuple[int, int] = (0, 0), - pixel_motion: Tuple[int, int] = (0, 0), + position: Tuple[int, int] = (0, 0), + motion: Tuple[int, int] = (0, 0), tile: Optional[Tuple[int, int]] = (0, 0), tile_motion: Optional[Tuple[int, int]] = (0, 0), state: int = 0, ): - super().__init__(pixel, tile, state) - self.pixel_motion = Point(*pixel_motion) + super().__init__(position, tile, state) + self.motion = Point(*motion) self.__tile_motion = Point(*tile_motion) if tile_motion is not None else None + @property + def pixel_motion(self) -> Point: + warnings.warn( + "The mouse.pixel_motion attribute is deprecated. Use mouse.motion instead.", + DeprecationWarning, + stacklevel=2, + ) + return self.motion + + @pixel_motion.setter + def pixel_motion(self, value: Point) -> None: + warnings.warn( + "The mouse.pixel_motion attribute is deprecated. Use mouse.motion instead.", + DeprecationWarning, + stacklevel=2, + ) + self.motion = value + @property def tile_motion(self) -> Point: + warnings.warn( + "The mouse.tile_motion attribute is deprecated." + " Use mouse.motion of the event returned by context.convert_event instead.", + DeprecationWarning, + stacklevel=2, + ) return _verify_tile_coordinates(self.__tile_motion) @tile_motion.setter def tile_motion(self, xy: Tuple[int, int]) -> None: + warnings.warn( + "The mouse.tile_motion attribute is deprecated." + " Use mouse.motion of the event returned by context.convert_event instead.", + DeprecationWarning, + stacklevel=2, + ) self.__tile_motion = Point(*xy) @classmethod @@ -494,19 +559,19 @@ def from_sdl_event(cls, sdl_event: Any) -> MouseMotion: return self def __repr__(self) -> str: - return ("tcod.event.%s(pixel=%r, pixel_motion=%r, " "tile=%r, tile_motion=%r, state=%s)") % ( + return ("tcod.event.%s(position=%r, motion=%r, tile=%r, tile_motion=%r, state=%s)") % ( self.__class__.__name__, - tuple(self.pixel), - tuple(self.pixel_motion), + tuple(self.position), + tuple(self.motion), tuple(self.tile), tuple(self.tile_motion), _describe_bitmask(self.state, _REVERSE_BUTTON_MASK_TABLE_PREFIX), ) def __str__(self) -> str: - return ("<%s, pixel_motion=(x=%i, y=%i), tile_motion=(x=%i, y=%i)>") % ( + return ("<%s, motion=(x=%i, y=%i), tile_motion=(x=%i, y=%i)>") % ( super().__str__().strip("<>"), - *self.pixel_motion, + *self.motion, *self.tile_motion, ) @@ -516,7 +581,7 @@ class MouseButtonEvent(MouseState): Attributes: type (str): Will be "MOUSEBUTTONDOWN" or "MOUSEBUTTONUP", depending on the event. - pixel (Point): The pixel coordinates of the mouse. + position (Point): The pixel coordinates of the mouse. tile (Point): The integer tile coordinates of the mouse on the screen. button (int): Which mouse button. @@ -527,6 +592,7 @@ class MouseButtonEvent(MouseState): * tcod.event.BUTTON_RIGHT * tcod.event.BUTTON_X1 * tcod.event.BUTTON_X2 + """ def __init__( @@ -559,17 +625,17 @@ def from_sdl_event(cls, sdl_event: Any) -> Any: return self def __repr__(self) -> str: - return "tcod.event.%s(pixel=%r, tile=%r, button=%s)" % ( + return "tcod.event.%s(position=%r, tile=%r, button=%s)" % ( self.__class__.__name__, - tuple(self.pixel), + tuple(self.position), tuple(self.tile), _REVERSE_BUTTON_TABLE_PREFIX[self.button], ) def __str__(self) -> str: - return " tcod.event.MouseState: """ xy = ffi.new("int[2]") state = lib.SDL_GetGlobalMouseState(xy, xy + 1) - return tcod.event.MouseState(pixel=(xy[0], xy[1]), state=state) + return tcod.event.MouseState((xy[0], xy[1]), state=state) def get_relative_state() -> tcod.event.MouseState: @@ -200,7 +200,7 @@ def get_relative_state() -> tcod.event.MouseState: """ xy = ffi.new("int[2]") state = lib.SDL_GetRelativeMouseState(xy, xy + 1) - return tcod.event.MouseState(pixel=(xy[0], xy[1]), state=state) + return tcod.event.MouseState((xy[0], xy[1]), state=state) def get_state() -> tcod.event.MouseState: @@ -211,7 +211,7 @@ def get_state() -> tcod.event.MouseState: """ xy = ffi.new("int[2]") state = lib.SDL_GetMouseState(xy, xy + 1) - return tcod.event.MouseState(pixel=(xy[0], xy[1]), state=state) + return tcod.event.MouseState((xy[0], xy[1]), state=state) def get_focus() -> Optional[tcod.sdl.video.Window]: From 65ec5b9a705aeced7bd50b53779848e392f03846 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 4 Jan 2023 13:14:16 -0800 Subject: [PATCH 119/194] Prepare 15.0.0 release. --- CHANGELOG.md | 9 ++++++++- tcod/context.py | 2 +- tcod/event.py | 4 ++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95120c50..591b7289 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,16 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] + +## [15.0.0] - 2023-01-04 ### Changed -- Updated the case of window event types to match their type annotations. +- Modified the letter case of window event types to match their type annotations. This may cause regressions. Run Mypy to check for ``[comparison-overlap]`` errors. +- Mouse event attributes have been changed ``.pixel -> .position`` and ``.pixel_motion -> .motion``. +- `Context.convert_event` now returns copies of events with mouse coordinates converted into tile positions. + +### Deprecated +- Mouse event pixel and tile attributes have been deprecated. ## [14.0.0] - 2022-12-09 ### Added diff --git a/tcod/context.py b/tcod/context.py index 414f1bd2..cc82c90f 100644 --- a/tcod/context.py +++ b/tcod/context.py @@ -265,7 +265,7 @@ def convert_event(self, event: _Event) -> _Event: # Tile coordinates are used in the returned event. print(f"Tiles: {event_tile.position=}, {event_tile.motion=}") - .. versionchanged:: Unreleased + .. versionchanged:: 15.0 Now returns a new event with the coordinates converted into tiles. """ event_copy = copy.copy(event) diff --git a/tcod/event.py b/tcod/event.py index 58ca9bac..a54eeb61 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -398,7 +398,7 @@ class MouseState(Event): .. versionadded:: 9.3 - .. versionchanged:: Unreleased + .. versionchanged:: 15.0 Renamed `pixel` attribute to `position`. """ @@ -484,7 +484,7 @@ class MouseMotion(MouseState): * tcod.event.BUTTON_X1MASK * tcod.event.BUTTON_X2MASK - .. versionchanged:: Unreleased + .. versionchanged:: 15.0 Renamed `pixel` attribute to `position`. Renamed `pixel_motion` attribute to `motion`. """ From 3c9da8232743f6a36193db42e2f48fb400b6ec0d Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 24 Jan 2023 02:45:07 -0800 Subject: [PATCH 120/194] Extend namespace package support to tcod.sdl In case any SDL tools such as audio mixing are put in another package. Fix typo. --- CHANGELOG.md | 2 ++ tcod/sdl/__init__.py | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 591b7289..9d3f4b71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] +### Added +- Added support for `tcod.sdl` namespace packages. ## [15.0.0] - 2023-01-04 ### Changed diff --git a/tcod/sdl/__init__.py b/tcod/sdl/__init__.py index 81f04d12..e1cbde7a 100644 --- a/tcod/sdl/__init__.py +++ b/tcod/sdl/__init__.py @@ -1,10 +1,13 @@ from __future__ import annotations import logging +from pkgutil import extend_path from typing import Any, Callable, Tuple, TypeVar from tcod.loader import ffi, lib +__path__ = extend_path(__path__, __name__) + T = TypeVar("T") logger = logging.getLogger(__name__) @@ -59,7 +62,7 @@ def _linked_version() -> Tuple[int, int, int]: def _version_at_least(required: Tuple[int, int, int]) -> None: - """Raise an error if the compiled version is less than required. Used to guard recentally defined SDL functions.""" + """Raise an error if the compiled version is less than required. Used to guard recently defined SDL functions.""" if required <= _compiled_version(): return raise RuntimeError( From f544ea1ebfd09ec7aee9535a14adfbc2ffd509a2 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 16 Feb 2023 02:58:52 -0800 Subject: [PATCH 121/194] Update noise docs. --- tcod/noise.py | 115 ++++++++++++++++++++++++-------------------------- 1 file changed, 54 insertions(+), 61 deletions(-) diff --git a/tcod/noise.py b/tcod/noise.py index 92676c8a..94178b56 100644 --- a/tcod/noise.py +++ b/tcod/noise.py @@ -12,27 +12,24 @@ ... algorithm=tcod.noise.Algorithm.SIMPLEX, ... seed=42, ... ) - >>> samples = noise[tcod.noise.grid(shape=(5, 5), scale=0.25, origin=(0, 0))] + >>> samples = noise[tcod.noise.grid(shape=(5, 4), scale=0.25, origin=(0, 0))] >>> samples # Samples are a grid of floats between -1.0 and 1.0 array([[ 0. , -0.55046356, -0.76072866, -0.7088647 , -0.68165785], [-0.27523372, -0.7205134 , -0.74057037, -0.43919194, -0.29195625], [-0.40398532, -0.57662135, -0.33160293, 0.12860827, 0.2864191 ], - [-0.50773406, -0.2643614 , 0.24446318, 0.6390255 , 0.5922846 ], - [-0.64945626, -0.12529983, 0.5346834 , 0.80402255, 0.52655405]], + [-0.50773406, -0.2643614 , 0.24446318, 0.6390255 , 0.5922846 ]], dtype=float32) >>> (samples + 1.0) * 0.5 # You can normalize samples to 0.0 - 1.0 array([[0.5 , 0.22476822, 0.11963567, 0.14556766, 0.15917107], [0.36238313, 0.1397433 , 0.12971482, 0.28040403, 0.35402188], [0.29800734, 0.21168932, 0.33419853, 0.5643041 , 0.6432096 ], - [0.24613297, 0.3678193 , 0.6222316 , 0.8195127 , 0.79614234], - [0.17527187, 0.4373501 , 0.76734173, 0.9020113 , 0.76327705]], + [0.24613297, 0.3678193 , 0.6222316 , 0.8195127 , 0.79614234]], dtype=float32) >>> ((samples + 1.0) * (256 / 2)).astype(np.uint8) # Or as 8-bit unsigned bytes. array([[128, 57, 30, 37, 40], [ 92, 35, 33, 71, 90], [ 76, 54, 85, 144, 164], - [ 63, 94, 159, 209, 203], - [ 44, 111, 196, 230, 195]], dtype=uint8) + [ 63, 94, 159, 209, 203]], dtype=uint8) """ # noqa: E501 from __future__ import annotations @@ -103,26 +100,23 @@ def __getattr__(name: str) -> Implementation: class Noise(object): - """ + """A configurable noise sampler. The ``hurst`` exponent describes the raggedness of the resultant noise, with a higher value leading to a smoother noise. Not used with tcod.noise.SIMPLE. - ``lacunarity`` is a multiplier that determines how fast the noise - frequency increases for each successive octave. + ``lacunarity`` is a multiplier that determines how fast the noise frequency increases for each successive octave. Not used with tcod.noise.SIMPLE. Args: - dimensions (int): Must be from 1 to 4. - algorithm (int): Defaults to :any:`tcod.noise.Algorithm.SIMPLEX` - implementation (int): - Defaults to :any:`tcod.noise.Implementation.SIMPLE` - hurst (float): The hurst exponent. Should be in the 0.0-1.0 range. - lacunarity (float): The noise lacunarity. - octaves (float): The level of detail on fBm and turbulence - implementations. - seed (Optional[Random]): A Random instance, or None. + dimensions: Must be from 1 to 4. + algorithm: Defaults to :any:`tcod.noise.Algorithm.SIMPLEX` + implementation: Defaults to :any:`tcod.noise.Implementation.SIMPLE` + hurst: The hurst exponent. Should be in the 0.0-1.0 range. + lacunarity: The noise lacunarity. + octaves: The level of detail on fBm and turbulence implementations. + seed: A Random instance, or None. Attributes: noise_c (CData): A cffi pointer to a TCOD_noise_t object. @@ -224,18 +218,17 @@ def get_point(self, x: float = 0, y: float = 0, z: float = 0, w: float = 0) -> f """Return the noise value at the (x, y, z, w) point. Args: - x (float): The position on the 1st axis. - y (float): The position on the 2nd axis. - z (float): The position on the 3rd axis. - w (float): The position on the 4th axis. + x: The position on the 1st axis. + y: The position on the 2nd axis. + z: The position on the 3rd axis. + w: The position on the 4th axis. """ return float(lib.NoiseGetSample(self._tdl_noise_c, (x, y, z, w))) def __getitem__(self, indexes: Any) -> NDArray[np.float32]: """Sample a noise map through NumPy indexing. - This follows NumPy's advanced indexing rules, but allows for floating - point values. + This follows NumPy's advanced indexing rules, but allows for floating point values. .. versionadded:: 11.16 """ @@ -292,14 +285,14 @@ def sample_mgrid(self, mgrid: ArrayLike) -> NDArray[np.float32]: overhead when working with large mesh-grids. Args: - mgrid (numpy.ndarray): A mesh-grid array of points to sample. + mgrid: A mesh-grid array of points to sample. A contiguous array of type `numpy.float32` is preferred. Returns: - numpy.ndarray: An array of sampled points. + An array of sampled points. - This array has the shape: ``mgrid.shape[:-1]``. - The ``dtype`` is `numpy.float32`. + This array has the shape: ``mgrid.shape[:-1]``. + The ``dtype`` is `numpy.float32`. """ mgrid = np.ascontiguousarray(mgrid, np.float32) if mgrid.shape[0] != self.dimensions: @@ -320,15 +313,14 @@ def sample_mgrid(self, mgrid: ArrayLike) -> NDArray[np.float32]: def sample_ogrid(self, ogrid: Sequence[ArrayLike]) -> NDArray[np.float32]: """Sample an open mesh-grid array and return the result. - Args - ogrid (Sequence[ArrayLike]): An open mesh-grid. + Args: + ogrid: An open mesh-grid. Returns: - numpy.ndarray: An array of sampled points. + An array of sampled points. - The ``shape`` is based on the lengths of the open mesh-grid - arrays. - The ``dtype`` is `numpy.float32`. + The ``shape`` is based on the lengths of the open mesh-grid arrays. + The ``dtype`` is `numpy.float32`. """ if len(ogrid) != self.dimensions: raise ValueError("len(ogrid) must equal self.dimensions, " "%r != %r" % (len(ogrid), self.dimensions)) @@ -416,38 +408,39 @@ def grid( origin: Optional[Tuple[int, ...]] = None, indexing: Literal["ij", "xy"] = "xy", ) -> Tuple[NDArray[Any], ...]: - """Helper function for generating a grid of noise samples. - - `shape` is the shape of the returned mesh grid. This can be any number of - dimensions, but :class:`Noise` classes only support up to 4. - - `scale` is the step size of indexes away from `origin`. - This can be a single float, or it can be a tuple of floats with one float - for each axis in `shape`. A lower scale gives smoother transitions - between noise values. + """Generate a mesh-grid of sample points to use with noise sampling. - `origin` is the first sample of the grid. - If `None` then the `origin` will be zero on each axis. - `origin` is not scaled by the `scale` parameter. - - `indexing` is passed to :any:`numpy.meshgrid`. + Args: + shape: The shape of the grid. + This can be any number of dimensions, but :class:`Noise` classes only support up to 4. + scale: The step size between samples. + This can be a single float, or it can be a tuple of floats with one float for each axis in `shape`. + A lower scale gives smoother transitions between noise values. + origin: The position of the first sample. + If `None` then the `origin` will be zero on each axis. + `origin` is not scaled by the `scale` parameter. + indexing: Passed to :any:`numpy.meshgrid`. + + Returns: + A sparse mesh-grid to be passed into a :class:`Noise` instance. Example:: >>> noise = tcod.noise.Noise(dimensions=2, seed=42) - >>> noise[tcod.noise.grid(shape=(5, 5), scale=0.25)] - array([[ 0. , -0.55046356, -0.76072866, -0.7088647 , -0.68165785], - [-0.27523372, -0.7205134 , -0.74057037, -0.43919194, -0.29195625], - [-0.40398532, -0.57662135, -0.33160293, 0.12860827, 0.2864191 ], - [-0.50773406, -0.2643614 , 0.24446318, 0.6390255 , 0.5922846 ], - [-0.64945626, -0.12529983, 0.5346834 , 0.80402255, 0.52655405]], + + # Common case for ij-indexed arrays. + >>> noise[tcod.noise.grid(shape=(3, 5), scale=0.25, indexing="ij")] + array([[ 0. , -0.27523372, -0.40398532, -0.50773406, -0.64945626], + [-0.55046356, -0.7205134 , -0.57662135, -0.2643614 , -0.12529983], + [-0.76072866, -0.74057037, -0.33160293, 0.24446318, 0.5346834 ]], dtype=float32) - >>> noise[tcod.noise.grid(shape=(5, 5), scale=(0.5, 0.25), origin=(1, 1))] - array([[ 0.52655405, -0.5037453 , -0.81221616, -0.7057655 , 0.24630858], - [ 0.25038874, -0.75348294, -0.6379566 , -0.5817767 , -0.02789652], - [-0.03488023, -0.73630923, -0.12449139, -0.22774395, -0.22243626], - [-0.18455243, -0.35063767, 0.4495706 , 0.02399864, -0.42226675], - [-0.16333057, 0.18149695, 0.7547447 , -0.07006818, -0.6546707 ]], + + # Transpose an xy-indexed array to get a standard order="F" result. + >>> noise[tcod.noise.grid(shape=(4, 5), scale=(0.5, 0.25), origin=(1.0, 1.0))].T + array([[ 0.52655405, 0.25038874, -0.03488023, -0.18455243, -0.16333057], + [-0.5037453 , -0.75348294, -0.73630923, -0.35063767, 0.18149695], + [-0.81221616, -0.6379566 , -0.12449139, 0.4495706 , 0.7547447 ], + [-0.7057655 , -0.5817767 , -0.22774395, 0.02399864, -0.07006818]], dtype=float32) .. versionadded:: 12.2 From 992292c00de5a9a30372f025f259bcd55e6c7f16 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 16 Feb 2023 03:20:13 -0800 Subject: [PATCH 122/194] Updates sources for latest black/mypy versions. --- examples/samples_libtcodpy.py | 2 ++ examples/samples_tcod.py | 1 - tcod/console.py | 2 +- tcod/path.py | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/samples_libtcodpy.py b/examples/samples_libtcodpy.py index ba91aeca..c503cefb 100755 --- a/examples/samples_libtcodpy.py +++ b/examples/samples_libtcodpy.py @@ -930,6 +930,8 @@ def render_path(first, key, mouse): # if true, there is always a wall on north & west side of a room bsp_room_walls = True bsp_map = None + + # draw a vertical line def vline(m, x, y1, y2): if y1 > y2: diff --git a/examples/samples_tcod.py b/examples/samples_tcod.py index 60bd8416..6f81cd0d 100755 --- a/examples/samples_tcod.py +++ b/examples/samples_tcod.py @@ -230,7 +230,6 @@ def on_draw(self) -> None: class LineDrawingSample(Sample): - FLAG_NAMES = [ "BKGND_NONE", "BKGND_SET", diff --git a/tcod/console.py b/tcod/console.py index 81f209d9..3ffa437b 100644 --- a/tcod/console.py +++ b/tcod/console.py @@ -782,7 +782,7 @@ def blit( """ # The old syntax is easy to detect and correct. if hasattr(src_y, "console_c"): - (src_x, src_y, width, height, dest, dest_x, dest_y,) = ( + (src_x, src_y, width, height, dest, dest_x, dest_y) = ( dest, # type: ignore dest_x, dest_y, diff --git a/tcod/path.py b/tcod/path.py index d3c9a514..db42fba5 100644 --- a/tcod/path.py +++ b/tcod/path.py @@ -474,7 +474,7 @@ def dijkstra2d( Added `out` parameter. Now returns the output array. """ dist: NDArray[Any] = np.asarray(distance) - if out is ...: # type: ignore + if out is ...: out = dist warnings.warn( "No `out` parameter was given. " From c6bb7eed924508508f93a0fdc4d2101f42198a4e Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 16 Feb 2023 03:49:07 -0800 Subject: [PATCH 123/194] Update GitHub Actions versions. --- .github/workflows/python-package.yml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index fafeff34..c8749e3f 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -32,12 +32,12 @@ jobs: - name: Install isort run: pip install isort - name: isort - uses: liskin/gh-problem-matcher-wrap@v1 + uses: liskin/gh-problem-matcher-wrap@v2 with: linters: isort run: isort scripts/ tcod/ tests/ --check --diff - name: isort (examples) - uses: liskin/gh-problem-matcher-wrap@v1 + uses: liskin/gh-problem-matcher-wrap@v2 with: linters: isort run: isort examples/ --check --diff --thirdparty tcod @@ -49,7 +49,7 @@ jobs: - name: Install Flake8 run: pip install Flake8 - name: Flake8 - uses: liskin/gh-problem-matcher-wrap@v1 + uses: liskin/gh-problem-matcher-wrap@v2 with: linters: flake8 run: flake8 scripts/ tcod/ tests/ @@ -66,7 +66,7 @@ jobs: run: | echo '__version__ = ""' > tcod/version.py - name: Mypy - uses: liskin/gh-problem-matcher-wrap@v1 + uses: liskin/gh-problem-matcher-wrap@v2 with: linters: mypy run: mypy --show-column-numbers . @@ -115,7 +115,7 @@ jobs: run: | git submodule update --init --recursive --depth 1 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} architecture: ${{ matrix.architecture }} @@ -146,7 +146,7 @@ jobs: - name: Xvfb logs if: runner.os != 'Windows' run: cat /tmp/xvfb.log - - uses: codecov/codecov-action@v2 + - uses: codecov/codecov-action@v3 - name: Upload to PyPI if: startsWith(github.ref, 'refs/tags/') && runner.os != 'Linux' env: @@ -154,13 +154,13 @@ jobs: TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | twine upload --skip-existing dist/* - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 if: runner.os == 'Linux' with: name: sdist path: dist/tcod-*.tar.gz retention-days: 7 - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v3 if: runner.os == 'Windows' with: name: wheels-windows @@ -175,7 +175,7 @@ jobs: os: ["ubuntu-20.04", "windows-2019"] steps: - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.x - name: Install Python dependencies @@ -187,7 +187,7 @@ jobs: run: | sudo apt-get update sudo apt-get install libsdl2-dev - - uses: actions/download-artifact@v2 + - uses: actions/download-artifact@v3 with: name: sdist - name: Build package in isolation @@ -210,12 +210,12 @@ jobs: fetch-depth: ${{ env.git-depth }} - name: Set up QEMU if: ${{ matrix.arch == 'aarch64' }} - uses: docker/setup-qemu-action@v1 + uses: docker/setup-qemu-action@v2 - name: Checkout submodules run: | git submodule update --init --recursive --depth 1 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: "3.x" - name: Install Python dependencies @@ -238,7 +238,7 @@ jobs: CIBW_BEFORE_TEST: pip install numpy CIBW_TEST_COMMAND: python -c "import tcod" - name: Archive wheel - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: wheels-linux path: wheelhouse/*.whl @@ -283,7 +283,7 @@ jobs: CIBW_TEST_COMMAND: python -c "import tcod" CIBW_TEST_SKIP: "pp* *-macosx_arm64 *-macosx_universal2:arm64" - name: Archive wheel - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: wheels-macos path: wheelhouse/*.whl From a77ce55ab1ee3329aefe6ae0c90e4f67e68f6eb9 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 26 Mar 2023 22:08:25 -0700 Subject: [PATCH 124/194] Add Ruff config. Taking from another project, settings might need tweaking. Update ignore file for Ruff. --- .gitignore | 1 + pyproject.toml | 44 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/.gitignore b/.gitignore index 85bf65c5..6ec1f7fb 100644 --- a/.gitignore +++ b/.gitignore @@ -83,3 +83,4 @@ debian/python* .pytest_cache Thumbs.db .mypy_cache/ +.ruff_cache/ diff --git a/pyproject.toml b/pyproject.toml index 63a4c899..f27434f9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,3 +37,47 @@ filterwarnings = [ "ignore::PendingDeprecationWarning:tcod.libtcodpy", "ignore:This class may perform poorly and is no longer needed.::tcod.map", ] + +[tool.ruff] +# https://beta.ruff.rs/docs/rules/ +select = [ + "C90", # mccabe + "E", # pycodestyle + "W", # pycodestyle + "F", # Pyflakes + "I", # isort + "UP", # pyupgrade + "YTT", # flake8-2020 + "ANN", # flake8-annotations + "S", # flake8-bandit + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "DTZ", # flake8-datetimez + "EM", # flake8-errmsg + "EXE", # flake8-executable + "RET", # flake8-return + "ICN", # flake8-import-conventions + "INP", # flake8-no-pep420 + "PIE", # flake8-pie + "PT", # flake8-pytest-style + "SIM", # flake8-simplify + "PTH", # flake8-use-pathlib + "PL", # Pylint + "TRY", # tryceratops + "RUF", # NumPy-specific rules + "G", # flake8-logging-format + "D", # pydocstyle +] +ignore = [ + "E501", # line-too-long + "S101", # assert + "ANN101", # missing-type-self + "D203", # one-blank-line-before-class + "D204", # one-blank-line-after-class + "D213", # multi-line-summary-second-line + "D407", # dashed-underline-after-section + "D408", # section-underline-after-name + "D409", # section-underline-matches-section-length +] +line-length = 120 +target-version = "py37" From 849ac8f797597a2bf45c22ce53ffa7355814d8dc Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 26 Mar 2023 22:14:36 -0700 Subject: [PATCH 125/194] Docs: fix typo and clean up lines. --- tcod/event.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/tcod/event.py b/tcod/event.py index a54eeb61..0ef109d5 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -1264,15 +1264,14 @@ def wait(timeout: Optional[float] = None) -> Iterator[Any]: class EventDispatch(Generic[T]): - '''This class dispatches events to methods depending on the events type - attribute. + '''Dispatches events to methods depending on the events type attribute. - To use this class, make a sub-class and override the relevant `ev_*` - methods. Then send events to the dispatch method. + To use this class, make a sub-class and override the relevant `ev_*` methods. + Then send events to the dispatch method. .. versionchanged:: 11.12 - This is now a generic class. The type hists at the return value of - :any:`dispatch` and the `ev_*` methods. + This is now a generic class. + The type hints at the return value of :any:`dispatch` and the `ev_*` methods. Example:: From 14a80d450249872dc502f8a4ed91fc329845eb22 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 27 Mar 2023 18:04:56 -0700 Subject: [PATCH 126/194] Update keyboard examples to use enums. --- examples/samples_tcod.py | 96 ++++++++++++++++++++-------------------- tcod/event.py | 58 ++++++++++++------------ 2 files changed, 77 insertions(+), 77 deletions(-) diff --git a/examples/samples_tcod.py b/examples/samples_tcod.py index 6f81cd0d..bf20be3b 100755 --- a/examples/samples_tcod.py +++ b/examples/samples_tcod.py @@ -70,17 +70,17 @@ def on_draw(self) -> None: def ev_keydown(self, event: tcod.event.KeyDown) -> None: global cur_sample - if event.sym == tcod.event.K_DOWN: + if event.sym == tcod.event.KeySym.DOWN: cur_sample = (cur_sample + 1) % len(SAMPLES) SAMPLES[cur_sample].on_enter() draw_samples_menu() - elif event.sym == tcod.event.K_UP: + elif event.sym == tcod.event.KeySym.UP: cur_sample = (cur_sample - 1) % len(SAMPLES) SAMPLES[cur_sample].on_enter() draw_samples_menu() - elif event.sym == tcod.event.K_RETURN and event.mod & tcod.event.KMOD_LALT: + elif event.sym == tcod.event.KeySym.RETURN and event.mod & tcod.event.KMOD_LALT: tcod.console_set_fullscreen(not tcod.console_is_fullscreen()) - elif event.sym == tcod.event.K_PRINTSCREEN or event.sym == ord("p"): + elif event.sym == tcod.event.KeySym.PRINTSCREEN or event.sym == tcod.event.KeySym.p: print("screenshot") if event.mod & tcod.event.KMOD_LALT: tcod.console_save_apf(root_console, "samples.apf") @@ -88,7 +88,7 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> None: else: tcod.sys_save_screenshot() print("png") - elif event.sym == tcod.event.K_ESCAPE: + elif event.sym == tcod.event.KeySym.ESCAPE: raise SystemExit() elif event.sym in RENDERER_KEYS: # Swap the active context for one with a different renderer. @@ -259,7 +259,7 @@ def __init__(self) -> None: self.bk.ch[:] = ord(" ") def ev_keydown(self, event: tcod.event.KeyDown) -> None: - if event.sym in (tcod.event.K_RETURN, tcod.event.K_KP_ENTER): + if event.sym in (tcod.event.KeySym.RETURN, tcod.event.KeySym.KP_ENTER): self.bk_flag += 1 if (self.bk_flag & 0xFF) > tcod.BKGND_ALPH: self.bk_flag = tcod.BKGND_NONE @@ -449,30 +449,30 @@ def on_draw(self) -> None: ) def ev_keydown(self, event: tcod.event.KeyDown) -> None: - if ord("9") >= event.sym >= ord("1"): - self.func = event.sym - ord("1") + if tcod.event.KeySym.N9 >= event.sym >= tcod.event.KeySym.N1: + self.func = event.sym - tcod.event.KeySym.N1 self.noise = self.get_noise() - elif event.sym == ord("e"): + elif event.sym == tcod.event.KeySym.e: self.hurst += 0.1 self.noise = self.get_noise() - elif event.sym == ord("d"): + elif event.sym == tcod.event.KeySym.d: self.hurst -= 0.1 self.noise = self.get_noise() - elif event.sym == ord("r"): + elif event.sym == tcod.event.KeySym.r: self.lacunarity += 0.5 self.noise = self.get_noise() - elif event.sym == ord("f"): + elif event.sym == tcod.event.KeySym.f: self.lacunarity -= 0.5 self.noise = self.get_noise() - elif event.sym == ord("t"): + elif event.sym == tcod.event.KeySym.t: self.octaves += 0.5 self.noise.octaves = self.octaves - elif event.sym == ord("g"): + elif event.sym == tcod.event.KeySym.g: self.octaves -= 0.5 self.noise.octaves = self.octaves - elif event.sym == ord("y"): + elif event.sym == tcod.event.KeySym.y: self.zoom += 0.2 - elif event.sym == ord("h"): + elif event.sym == tcod.event.KeySym.h: self.zoom -= 0.2 else: super().ev_keydown(event) @@ -631,25 +631,25 @@ def on_draw(self) -> None: def ev_keydown(self, event: tcod.event.KeyDown) -> None: MOVE_KEYS = { - ord("i"): (0, -1), - ord("j"): (-1, 0), - ord("k"): (0, 1), - ord("l"): (1, 0), + tcod.event.KeySym.i: (0, -1), + tcod.event.KeySym.j: (-1, 0), + tcod.event.KeySym.k: (0, 1), + tcod.event.KeySym.l: (1, 0), } FOV_SELECT_KEYS = { - ord("-"): -1, - ord("="): 1, - tcod.event.K_KP_MINUS: -1, - tcod.event.K_KP_PLUS: 1, + tcod.event.KeySym.MINUS: -1, + tcod.event.KeySym.EQUALS: 1, + tcod.event.KeySym.KP_MINUS: -1, + tcod.event.KeySym.KP_PLUS: 1, } if event.sym in MOVE_KEYS: x, y = MOVE_KEYS[event.sym] if self.walkable[self.player_x + x, self.player_y + y]: self.player_x += x self.player_y += y - elif event.sym == ord("t"): + elif event.sym == tcod.event.KeySym.t: self.torch = not self.torch - elif event.sym == ord("w"): + elif event.sym == tcod.event.KeySym.w: self.light_walls = not self.light_walls elif event.sym in FOV_SELECT_KEYS: self.algo_num += FOV_SELECT_KEYS[event.sym] @@ -775,7 +775,7 @@ def on_draw(self) -> None: self.recalculate = True def ev_keydown(self, event: tcod.event.KeyDown) -> None: - if event.sym == ord("i") and self.dy > 0: + if event.sym == tcod.event.KeySym.i and self.dy > 0: # destination move north tcod.console_put_char(sample_console, self.dx, self.dy, self.oldchar, tcod.BKGND_NONE) self.dy -= 1 @@ -783,7 +783,7 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> None: tcod.console_put_char(sample_console, self.dx, self.dy, "+", tcod.BKGND_NONE) if SAMPLE_MAP[self.dx, self.dy] == " ": self.recalculate = True - elif event.sym == ord("k") and self.dy < SAMPLE_SCREEN_HEIGHT - 1: + elif event.sym == tcod.event.KeySym.k and self.dy < SAMPLE_SCREEN_HEIGHT - 1: # destination move south tcod.console_put_char(sample_console, self.dx, self.dy, self.oldchar, tcod.BKGND_NONE) self.dy += 1 @@ -791,7 +791,7 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> None: tcod.console_put_char(sample_console, self.dx, self.dy, "+", tcod.BKGND_NONE) if SAMPLE_MAP[self.dx, self.dy] == " ": self.recalculate = True - elif event.sym == ord("j") and self.dx > 0: + elif event.sym == tcod.event.KeySym.j and self.dx > 0: # destination move west tcod.console_put_char(sample_console, self.dx, self.dy, self.oldchar, tcod.BKGND_NONE) self.dx -= 1 @@ -799,7 +799,7 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> None: tcod.console_put_char(sample_console, self.dx, self.dy, "+", tcod.BKGND_NONE) if SAMPLE_MAP[self.dx, self.dy] == " ": self.recalculate = True - elif event.sym == ord("l") and self.dx < SAMPLE_SCREEN_WIDTH - 1: + elif event.sym == tcod.event.KeySym.l and self.dx < SAMPLE_SCREEN_WIDTH - 1: # destination move east tcod.console_put_char(sample_console, self.dx, self.dy, self.oldchar, tcod.BKGND_NONE) self.dx += 1 @@ -807,7 +807,7 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> None: tcod.console_put_char(sample_console, self.dx, self.dy, "+", tcod.BKGND_NONE) if SAMPLE_MAP[self.dx, self.dy] == " ": self.recalculate = True - elif event.sym == tcod.event.K_TAB: + elif event.sym == tcod.event.KeySym.TAB: self.using_astar = not self.using_astar if self.using_astar: tcod.console_print(sample_console, 1, 4, "Using : A* ") @@ -999,28 +999,28 @@ def on_draw(self) -> None: def ev_keydown(self, event: tcod.event.KeyDown) -> None: global bsp_random_room, bsp_room_walls, bsp_depth, bsp_min_room_size - if event.sym in (tcod.event.K_RETURN, tcod.event.K_KP_ENTER): + if event.sym in (tcod.event.KeySym.RETURN, tcod.event.KeySym.KP_ENTER): self.bsp_generate() - elif event.sym == ord(" "): + elif event.sym == tcod.event.KeySym.SPACE: self.bsp_refresh() - elif event.sym in (tcod.event.K_EQUALS, tcod.event.K_KP_PLUS): + elif event.sym in (tcod.event.KeySym.EQUALS, tcod.event.KeySym.KP_PLUS): bsp_depth += 1 self.bsp_generate() - elif event.sym in (tcod.event.K_MINUS, tcod.event.K_KP_MINUS): + elif event.sym in (tcod.event.KeySym.MINUS, tcod.event.KeySym.KP_MINUS): bsp_depth = max(1, bsp_depth - 1) self.bsp_generate() - elif event.sym in (tcod.event.K_8, tcod.event.K_KP_MULTIPLY): + elif event.sym in (tcod.event.KeySym.N8, tcod.event.KeySym.KP_MULTIPLY): bsp_min_room_size += 1 self.bsp_generate() - elif event.sym in (tcod.event.K_SLASH, tcod.event.K_KP_DIVIDE): + elif event.sym in (tcod.event.KeySym.SLASH, tcod.event.KeySym.KP_DIVIDE): bsp_min_room_size = max(2, bsp_min_room_size - 1) self.bsp_generate() - elif event.sym in (tcod.event.K_1, tcod.event.K_KP_1): + elif event.sym in (tcod.event.KeySym.N1, tcod.event.KeySym.KP_1): bsp_random_room = not bsp_random_room if not bsp_random_room: bsp_room_walls = True self.bsp_refresh() - elif event.sym in (tcod.event.K_2, tcod.event.K_KP_2): + elif event.sym in (tcod.event.KeySym.N2, tcod.event.KeySym.KP_2): bsp_room_walls = not bsp_room_walls self.bsp_refresh() else: @@ -1126,9 +1126,9 @@ def on_draw(self) -> None: ) def ev_keydown(self, event: tcod.event.KeyDown) -> None: - if event.sym == ord("1"): + if event.sym == tcod.event.KeySym.N1: tcod.mouse_show_cursor(False) - elif event.sym == ord("2"): + elif event.sym == tcod.event.KeySym.N2: tcod.mouse_show_cursor(True) else: super().ev_keydown(event) @@ -1177,10 +1177,10 @@ def on_draw(self) -> None: self.names.append(tcod.namegen_generate(self.sets[self.curset])) def ev_keydown(self, event: tcod.event.KeyDown) -> None: - if event.sym == ord("="): + if event.sym == tcod.event.KeySym.EQUALS: self.curset += 1 self.names.append("======") - elif event.sym == ord("-"): + elif event.sym == tcod.event.KeySym.MINUS: self.curset -= 1 self.names.append("======") else: @@ -1363,11 +1363,11 @@ def on_draw(self) -> None: ############################################# RENDERER_KEYS = { - tcod.event.K_F1: tcod.RENDERER_GLSL, - tcod.event.K_F2: tcod.RENDERER_OPENGL, - tcod.event.K_F3: tcod.RENDERER_SDL, - tcod.event.K_F4: tcod.RENDERER_SDL2, - tcod.event.K_F5: tcod.RENDERER_OPENGL2, + tcod.event.KeySym.F1: tcod.RENDERER_GLSL, + tcod.event.KeySym.F2: tcod.RENDERER_OPENGL, + tcod.event.KeySym.F3: tcod.RENDERER_SDL, + tcod.event.KeySym.F4: tcod.RENDERER_SDL2, + tcod.event.KeySym.F5: tcod.RENDERER_OPENGL2, } RENDERER_NAMES = ( diff --git a/tcod/event.py b/tcod/event.py index 0ef109d5..02d21f87 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -1,7 +1,7 @@ """A light-weight implementation of event handling built on calls to SDL. Many event constants are derived directly from SDL. -For example: ``tcod.event.K_UP`` and ``tcod.event.SCANCODE_A`` refer to +For example: ``tcod.event.KeySym.UP`` and ``tcod.event.Scancode.A`` refer to SDL's ``SDLK_UP`` and ``SDL_SCANCODE_A`` respectfully. `See this table for all of SDL's keyboard constants. `_ @@ -1279,35 +1279,35 @@ class EventDispatch(Generic[T]): MOVE_KEYS = { # key_symbol: (x, y) # Arrow keys. - tcod.event.K_LEFT: (-1, 0), - tcod.event.K_RIGHT: (1, 0), - tcod.event.K_UP: (0, -1), - tcod.event.K_DOWN: (0, 1), - tcod.event.K_HOME: (-1, -1), - tcod.event.K_END: (-1, 1), - tcod.event.K_PAGEUP: (1, -1), - tcod.event.K_PAGEDOWN: (1, 1), - tcod.event.K_PERIOD: (0, 0), + tcod.event.KeySym.LEFT: (-1, 0), + tcod.event.KeySym.RIGHT: (1, 0), + tcod.event.KeySym.UP: (0, -1), + tcod.event.KeySym.DOWN: (0, 1), + tcod.event.KeySym.HOME: (-1, -1), + tcod.event.KeySym.END: (-1, 1), + tcod.event.KeySym.PAGEUP: (1, -1), + tcod.event.KeySym.PAGEDOWN: (1, 1), + tcod.event.KeySym.PERIOD: (0, 0), # Numpad keys. - tcod.event.K_KP_1: (-1, 1), - tcod.event.K_KP_2: (0, 1), - tcod.event.K_KP_3: (1, 1), - tcod.event.K_KP_4: (-1, 0), - tcod.event.K_KP_5: (0, 0), - tcod.event.K_KP_6: (1, 0), - tcod.event.K_KP_7: (-1, -1), - tcod.event.K_KP_8: (0, -1), - tcod.event.K_KP_9: (1, -1), - tcod.event.K_CLEAR: (0, 0), # Numpad `clear` key. + tcod.event.KeySym.KP_1: (-1, 1), + tcod.event.KeySym.KP_2: (0, 1), + tcod.event.KeySym.KP_3: (1, 1), + tcod.event.KeySym.KP_4: (-1, 0), + tcod.event.KeySym.KP_5: (0, 0), + tcod.event.KeySym.KP_6: (1, 0), + tcod.event.KeySym.KP_7: (-1, -1), + tcod.event.KeySym.KP_8: (0, -1), + tcod.event.KeySym.KP_9: (1, -1), + tcod.event.KeySym.CLEAR: (0, 0), # Numpad `clear` key. # Vi Keys. - tcod.event.K_h: (-1, 0), - tcod.event.K_j: (0, 1), - tcod.event.K_k: (0, -1), - tcod.event.K_l: (1, 0), - tcod.event.K_y: (-1, -1), - tcod.event.K_u: (1, -1), - tcod.event.K_b: (-1, 1), - tcod.event.K_n: (1, 1), + tcod.event.KeySym.h: (-1, 0), + tcod.event.KeySym.j: (0, 1), + tcod.event.KeySym.k: (0, -1), + tcod.event.KeySym.l: (1, 0), + tcod.event.KeySym.y: (-1, -1), + tcod.event.KeySym.u: (1, -1), + tcod.event.KeySym.b: (-1, 1), + tcod.event.KeySym.n: (1, 1), } @@ -1333,7 +1333,7 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> None: if event.sym in MOVE_KEYS: # Send movement keys to the cmd_move method with parameters. self.cmd_move(*MOVE_KEYS[event.sym]) - elif event.sym == tcod.event.K_ESCAPE: + elif event.sym == tcod.event.KeySym.ESCAPE: self.cmd_escape() def ev_mousebuttondown(self, event: tcod.event.MouseButtonDown) -> None: From a756e389c8d4f547ce19b7cb7e99bb8f9b64d554 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 27 Mar 2023 18:15:28 -0700 Subject: [PATCH 127/194] Fix WASD typo. --- tcod/event.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tcod/event.py b/tcod/event.py index 02d21f87..1e6bd44a 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -1632,7 +1632,7 @@ def get_keyboard_state() -> NDArray[np.bool_]: state = tcod.event.get_keyboard_state() # Get a WASD movement vector: - x = int(state[tcod.event.Scancode.E]) - int(state[tcod.event.Scancode.A]) + x = int(state[tcod.event.Scancode.D]) - int(state[tcod.event.Scancode.A]) y = int(state[tcod.event.Scancode.S]) - int(state[tcod.event.Scancode.W]) # Key with 'z' glyph is held: From 2e447300d0b0993acf0ef5d8a2cd6c04d1988744 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 30 Mar 2023 19:26:54 -0700 Subject: [PATCH 128/194] Fix Renderer.read_pixels, add simple tests, update docs. --- CHANGELOG.md | 3 +++ tcod/sdl/render.py | 65 +++++++++++++++++++++++++++++++++++----------- tests/test_sdl.py | 4 +++ 3 files changed, 57 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d3f4b71..8cd60ae8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version ### Added - Added support for `tcod.sdl` namespace packages. +### Fixed +- ``Renderer.read_pixels`` method was completely broken. + ## [15.0.0] - 2023-01-04 ### Changed - Modified the letter case of window event types to match their type annotations. diff --git a/tcod/sdl/render.py b/tcod/sdl/render.py index 3ea6bbd9..b39a994e 100644 --- a/tcod/sdl/render.py +++ b/tcod/sdl/render.py @@ -9,7 +9,7 @@ import numpy as np from numpy.typing import NDArray -from typing_extensions import Final +from typing_extensions import Final, Literal import tcod.sdl.video from tcod.loader import ffi, lib @@ -484,15 +484,33 @@ def set_vsync(self, enable: bool) -> None: def read_pixels( self, *, - rect: Optional[Tuple[int, int, int, int]] = None, - format: Optional[int] = None, - out: Optional[NDArray[Any]] = None, - ) -> NDArray[Any]: - """ - .. versionadded:: 13.5 + rect: tuple[int, int, int, int] | None = None, + format: int | Literal["RGB", "RGBA"] = "RGBA", + out: NDArray[np.uint8] | None = None, + ) -> NDArray[np.uint8]: + """Fetch the pixel contents of the current rendering target to an array. + + By default returns an RGBA pixel array of the full target in the shape: ``(height, width, rgba)``. + The target can be changed with :any:`set_render_target` + + Args: + rect: The ``(left, top, width, height)`` region of the target to fetch, or None for the entire target. + format: The pixel format. Defaults to ``"RGBA"``. + out: The output array. + Can be None or must be an ``np.uint8`` array of shape: ``(height, width, channels)``. + Must be C contiguous along the ``(width, channels)`` axes. + + This operation is slow due to coping from VRAM to RAM. + When reading the main rendering target this should be called after rendering and before :any:`present`. + See https://wiki.libsdl.org/SDL2/SDL_RenderReadPixels + + Returns: + The output uint8 array of shape: ``(height, width, channels)`` with the fetched pixels. + + .. versionadded:: Unreleased """ - if format is None: - format = lib.SDL_PIXELFORMAT_RGBA32 + FORMATS: Final = {"RGB": lib.SDL_PIXELFORMAT_RGB24, "RGBA": lib.SDL_PIXELFORMAT_RGBA32} + sdl_format = FORMATS.get(format) if isinstance(format, str) else format if rect is None: texture_p = lib.SDL_GetRenderTarget(self.p) if texture_p: @@ -502,15 +520,31 @@ def read_pixels( rect = (0, 0, *self.output_size) width, height = rect[2:4] if out is None: - if format == lib.SDL_PIXELFORMAT_RGBA32: + if sdl_format == lib.SDL_PIXELFORMAT_RGBA32: out = np.empty((height, width, 4), dtype=np.uint8) - elif format == lib.SDL_PIXELFORMAT_RGB24: + elif sdl_format == lib.SDL_PIXELFORMAT_RGB24: out = np.empty((height, width, 3), dtype=np.uint8) else: - raise TypeError("Pixel format not supported yet.") - assert out.shape[:2] == (height, width) - assert out[0].flags.c_contiguous - _check(lib.SDL_RenderReadPixels(self.p, format, ffi.cast("void*", out.ctypes.data), out.strides[0])) + msg = f"Pixel format {format!r} not supported by tcod." + raise TypeError(msg) + if out.dtype != np.uint8: + msg = "`out` must be a uint8 array." + raise TypeError(msg) + expected_shape = (height, width, {lib.SDL_PIXELFORMAT_RGB24: 3, lib.SDL_PIXELFORMAT_RGBA32: 4}[sdl_format]) + if out.shape != expected_shape: + msg = f"Expected `out` to be an array of shape {expected_shape}, got {out.shape} instead." + raise TypeError(msg) + if not out[0].flags.c_contiguous: + msg = "`out` array must be C contiguous." + _check( + lib.SDL_RenderReadPixels( + self.p, + (rect,), + sdl_format, + ffi.cast("void*", out.ctypes.data), + out.strides[0], + ) + ) return out def clear(self) -> None: @@ -522,6 +556,7 @@ def clear(self) -> None: def fill_rect(self, rect: Tuple[float, float, float, float]) -> None: """Fill a rectangle with :any:`draw_color`. + .. versionadded:: 13.5 """ _check(lib.SDL_RenderFillRectF(self.p, (rect,))) diff --git a/tests/test_sdl.py b/tests/test_sdl.py index 3deec406..84dcf3a7 100644 --- a/tests/test_sdl.py +++ b/tests/test_sdl.py @@ -70,6 +70,10 @@ def test_sdl_render() -> None: with pytest.raises(TypeError): render.upload_texture(np.zeros((8, 8, 5), np.uint8)) + assert (render.read_pixels() == (0, 0, 0, 255)).all() + assert (render.read_pixels(format="RGB") == (0, 0, 0)).all() + assert render.read_pixels(rect=(1, 2, 3, 4)).shape == (4, 3, 4) + def test_sdl_render_bad_types() -> None: with pytest.raises(TypeError): From b81395a564eb9b742ac62be597a74be77816a4c6 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 30 Mar 2023 21:14:15 -0700 Subject: [PATCH 129/194] Prepare 15.0.1 release. --- CHANGELOG.md | 2 ++ tcod/sdl/render.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cd60ae8..8960156e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] + +## [15.0.1] - 2023-03-30 ### Added - Added support for `tcod.sdl` namespace packages. diff --git a/tcod/sdl/render.py b/tcod/sdl/render.py index b39a994e..d7e3226d 100644 --- a/tcod/sdl/render.py +++ b/tcod/sdl/render.py @@ -507,7 +507,7 @@ def read_pixels( Returns: The output uint8 array of shape: ``(height, width, channels)`` with the fetched pixels. - .. versionadded:: Unreleased + .. versionadded:: 15.0 """ FORMATS: Final = {"RGB": lib.SDL_PIXELFORMAT_RGB24, "RGBA": lib.SDL_PIXELFORMAT_RGBA32} sdl_format = FORMATS.get(format) if isinstance(format, str) else format From fadbd85c5296ac8311f5deada7eccf269f294a7c Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 21 May 2023 16:49:29 -0700 Subject: [PATCH 130/194] Update VSCode formatting config. --- .vscode/extensions.json | 1 + .vscode/settings.json | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 59e34caf..e9737cb3 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -6,6 +6,7 @@ "austin.code-gnu-global", "editorconfig.editorconfig", "ms-python.python", + "ms-python.black-formatter", "ms-python.vscode-pylance", "ms-vscode.cpptools", "redhat.vscode-yaml", diff --git a/.vscode/settings.json b/.vscode/settings.json index e49af1b9..f251f993 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,7 +14,7 @@ "--follow-imports=silent", "--show-column-numbers" ], - "python.formatting.provider": "black", + "python.formatting.provider": "none", "files.associations": { "*.spec": "python", }, @@ -424,5 +424,8 @@ ], "python.testing.pytestArgs": [], "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true + "python.testing.pytestEnabled": true, + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter" + } } \ No newline at end of file From 9bcc5c6afe9addd9281a44471e3bb9283c00726e Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 21 May 2023 18:18:23 -0700 Subject: [PATCH 131/194] Migrate setuptools meta data to pyproject.toml. Replaces version code with setuptools-scm. --- pyproject.toml | 65 ++++++++++++++++++++++++++- setup.cfg | 4 -- setup.py | 117 ++++++------------------------------------------- 3 files changed, 77 insertions(+), 109 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f27434f9..08fcbb7a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,7 @@ [build-system] requires = [ - "setuptools==60.8.2", + "setuptools>=61.0.0", + "setuptools_scm[toml]>=6.2", "wheel>=0.37.1", "cffi>=1.15", "pycparser>=2.14", @@ -9,6 +10,66 @@ requires = [ ] build-backend = "setuptools.build_meta" +[project] +name = "tcod" +dynamic = ["version"] +description = "The official Python port of libtcod." +authors = [{ name = "Kyle Benesch", email = "4b796c65+tcod@gmail.com" }] +readme = "README.rst" +requires-python = ">=3.7" +license = { text = "Simplified BSD License" } +dependencies = [ + "cffi>=1.15", + 'numpy>=1.21.4; implementation_name != "pypy"', + "typing_extensions", +] +keywords = [ + "roguelike", + "cffi", + "Unicode", + "libtcod", + "field-of-view", + "pathfinding", +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Win32 (MS Windows)", + "Environment :: MacOS X", + "Environment :: X11 Applications", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Natural Language :: English", + "Operating System :: POSIX", + "Operating System :: MacOS :: MacOS X", + "Operating System :: Microsoft :: Windows", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + "Topic :: Games/Entertainment", + "Topic :: Multimedia :: Graphics", + "Topic :: Software Development :: Libraries :: Python Modules", + "Typing :: Typed", +] + +[project.entry-points.pyinstaller40] +hook-dirs = "tcod.__pyinstaller:get_hook_dirs" + +[project.urls] +Homepage = "https://github.com/libtcod/python-tcod" +Documentation = "https://python-tcod.readthedocs.io" +Changelog = "https://github.com/libtcod/python-tcod/blob/main/CHANGELOG.md" +Source = "https://github.com/libtcod/python-tcod" +Tracker = "https://github.com/libtcod/python-tcod/issues" +Forum = "https://github.com/libtcod/python-tcod/discussions" + +[tool.setuptools_scm] +write_to = "tcod/version.py" + [tool.black] line-length = 120 target-version = ["py37"] @@ -21,7 +82,7 @@ line_length = 120 [tool.pytest.ini_options] minversion = "6.0" -required_plugins = ["pytest-cov"] +required_plugins = ["pytest-cov", "pytest-benchmark"] testpaths = ["tcod/", "tests/", "docs/"] addopts = [ "--doctest-modules", diff --git a/setup.cfg b/setup.cfg index c70c33ef..33d12878 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,3 @@ -[options.entry_points] -pyinstaller40 = - hook-dirs = tcod.__pyinstaller:get_hook_dirs - [bdist_wheel] py-limited-api = cp36 diff --git a/setup.py b/setup.py index 4dadf34f..dc2139e7 100755 --- a/setup.py +++ b/setup.py @@ -1,13 +1,11 @@ #!/usr/bin/env python3 +"""Python-tcod setup script.""" from __future__ import annotations import platform -import re import subprocess import sys -import warnings from pathlib import Path -from typing import List from setuptools import setup @@ -16,40 +14,9 @@ SETUP_DIR = Path(__file__).parent # setup.py current directory -def get_version() -> str: - """Get the current version from a git tag, or by reading tcod/version.py""" - if (SETUP_DIR / ".git").exists(): - # "--tags" is required to workaround actions/checkout's broken annotated tag handing. - # https://github.com/actions/checkout/issues/290 - tag = subprocess.check_output(["git", "describe", "--abbrev=0", "--tags"], universal_newlines=True).strip() - assert not tag.startswith("v") - version = tag - - # add .devNN if needed - log = subprocess.check_output(["git", "log", f"{tag}..HEAD", "--oneline"], universal_newlines=True) - commits_since_tag = log.count("\n") - if commits_since_tag: - version += ".dev%i" % commits_since_tag - - # update tcod/version.py - (SETUP_DIR / "tcod/version.py").write_text(f'__version__ = "{version}"\n', encoding="utf-8") - return version - else: # Not a Git repository. - try: - match = re.match(r'__version__ = "(\S+)"', (SETUP_DIR / "tcod/version.py").read_text(encoding="utf-8")) - assert match - return match.groups()[0] - except FileNotFoundError: - warnings.warn("Unknown version: Not in a Git repository and not from a sdist bundle or wheel.") - return "0.0.0" - - -is_pypy = platform.python_implementation() == "PyPy" - - -def get_package_data() -> List[str]: - """get data files which will be included in the main tcod/ directory""" - BITSIZE, _ = platform.architecture() +def get_package_data() -> list[str]: + """Get data files which will be included in the main tcod/ directory.""" + BIT_SIZE, _ = platform.architecture() files = [ "py.typed", "lib/LIBTCOD-CREDITS.txt", @@ -57,7 +24,7 @@ def get_package_data() -> List[str]: "lib/README-SDL.txt", ] if "win32" in sys.platform: - if BITSIZE == "32bit": + if BIT_SIZE == "32bit": files += ["x86/SDL2.dll"] else: files += ["x64/SDL2.dll"] @@ -70,19 +37,20 @@ def check_sdl_version() -> None: """Check the local SDL version on Linux distributions.""" if not sys.platform.startswith("linux"): return - needed_version = "%i.%i.%i" % SDL_VERSION_NEEDED + needed_version = "{}.{}.{}".format(*SDL_VERSION_NEEDED) try: sdl_version_str = subprocess.check_output(["sdl2-config", "--version"], universal_newlines=True).strip() - except FileNotFoundError: - raise RuntimeError( - "libsdl2-dev or equivalent must be installed on your system" - " and must be at least version %s." - "\nsdl2-config must be on PATH." % (needed_version,) + except FileNotFoundError as exc: + msg = ( + f"libsdl2-dev or equivalent must be installed on your system and must be at least version {needed_version}." + "\nsdl2-config must be on PATH." ) - print("Found SDL %s." % (sdl_version_str,)) + raise RuntimeError(msg) from exc + print(f"Found SDL {sdl_version_str}.") sdl_version = tuple(int(s) for s in sdl_version_str.split(".")) if sdl_version < SDL_VERSION_NEEDED: - raise RuntimeError("SDL version must be at least %s, (found %s)" % (needed_version, sdl_version_str)) + msg = f"SDL version must be at least {needed_version}, (found {sdl_version_str})" + raise RuntimeError(msg) if not (SETUP_DIR / "libtcod/src").exists(): @@ -92,67 +60,10 @@ def check_sdl_version() -> None: check_sdl_version() -needs_pytest = {"pytest", "test", "ptr"}.intersection(sys.argv) -pytest_runner = ["pytest-runner"] if needs_pytest else [] - setup( - name="tcod", - version=get_version(), - author="Kyle Benesch", - author_email="4b796c65+tcod@gmail.com", - description="The official Python port of libtcod.", - long_description=(SETUP_DIR / "README.rst").read_text(encoding="utf-8"), - url="https://github.com/libtcod/python-tcod", - project_urls={ - "Documentation": "https://python-tcod.readthedocs.io", - "Changelog": "https://github.com/libtcod/python-tcod/blob/main/CHANGELOG.md", - "Source": "https://github.com/libtcod/python-tcod", - "Tracker": "https://github.com/libtcod/python-tcod/issues", - "Forum": "https://github.com/libtcod/python-tcod/discussions", - }, py_modules=["libtcodpy"], packages=["tcod", "tcod.sdl", "tcod.__pyinstaller"], package_data={"tcod": get_package_data()}, - python_requires=">=3.7", - setup_requires=[ - *pytest_runner, - "cffi>=1.15", - "requests>=2.28.1", - "pycparser>=2.14", - "pcpp==1.30", - ], - install_requires=[ - "cffi>=1.15", # Also required by pyproject.toml. - "numpy>=1.21.4" if not is_pypy else "", - "typing_extensions", - ], cffi_modules=["build_libtcod.py:ffi"], - tests_require=["pytest", "pytest-cov", "pytest-benchmark"], - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Environment :: Win32 (MS Windows)", - "Environment :: MacOS X", - "Environment :: X11 Applications", - "Intended Audience :: Developers", - "License :: OSI Approved :: BSD License", - "Natural Language :: English", - "Operating System :: POSIX", - "Operating System :: MacOS :: MacOS X", - "Operating System :: Microsoft :: Windows", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", - "Topic :: Games/Entertainment", - "Topic :: Multimedia :: Graphics", - "Topic :: Software Development :: Libraries :: Python Modules", - "Typing :: Typed", - ], - keywords="roguelike cffi Unicode libtcod field-of-view pathfinding", platforms=["Windows", "MacOS", "Linux"], - license="Simplified BSD License", ) From 5b59572af6944eda81c6c0cb1549ce9197ca29cc Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 21 May 2023 18:36:43 -0700 Subject: [PATCH 132/194] Clean up PyInstaller hooks. Just general maintenance. --- .vscode/settings.json | 1 + CHANGELOG.md | 3 +++ tcod/__pyinstaller/__init__.py | 9 +++++---- tcod/__pyinstaller/hook-tcod.py | 4 ++-- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index f251f993..c2a007d0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -155,6 +155,7 @@ "heapify", "heightmap", "hflip", + "hiddenimports", "HIGHDPI", "hillclimb", "hline", diff --git a/CHANGELOG.md b/CHANGELOG.md index 8960156e..6593ee6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -255,6 +255,9 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version - Prevent division by zero from recommended-console-size functions. ## [12.0.0] - 2021-03-05 +### Added +- Now includes PyInstaller hooks within the package itself. + ### Deprecated - The Random class will now warn if the seed it's given will not used deterministically. It will no longer accept non-integer seeds in the future. diff --git a/tcod/__pyinstaller/__init__.py b/tcod/__pyinstaller/__init__.py index a2a36e88..7224bb10 100644 --- a/tcod/__pyinstaller/__init__.py +++ b/tcod/__pyinstaller/__init__.py @@ -1,8 +1,9 @@ """PyInstaller entry point for tcod.""" -import os -from typing import List +from __future__ import annotations +from pathlib import Path -def get_hook_dirs() -> List[str]: + +def get_hook_dirs() -> list[str]: """Return the current directory.""" - return [os.path.dirname(__file__)] + return [str(Path(__file__).parent)] diff --git a/tcod/__pyinstaller/hook-tcod.py b/tcod/__pyinstaller/hook-tcod.py index 2df121e6..9790d583 100644 --- a/tcod/__pyinstaller/hook-tcod.py +++ b/tcod/__pyinstaller/hook-tcod.py @@ -1,8 +1,8 @@ """PyInstaller hook for tcod. -Added here after tcod 12.0.0. +There were added since tcod 12.0.0. -If this hook is modified then the contributed hook needs to be removed from: +If this hook is ever modified then the contributed hook needs to be removed from: https://github.com/pyinstaller/pyinstaller-hooks-contrib """ from PyInstaller.utils.hooks import collect_dynamic_libs # type: ignore From 66da1b241bbe1eeb0336b8d0f8051f0c01d461c9 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 21 May 2023 19:41:15 -0700 Subject: [PATCH 133/194] Clean up internal functions. Mostly following Ruff notes to upgrade code. Add __init__.py to test package folder to resolve issues with pytest. --- setup.cfg | 2 +- tcod/_internal.py | 95 ++++++++++++++++++++++++----------------------- tests/__init__.py | 1 + 3 files changed, 50 insertions(+), 48 deletions(-) create mode 100644 tests/__init__.py diff --git a/setup.cfg b/setup.cfg index 33d12878..0e25efb1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,7 +5,7 @@ py-limited-api = cp36 test=pytest [flake8] -ignore = E203 W503 +ignore = E203 W503 TYP001 max-line-length = 130 [mypy] diff --git a/tcod/_internal.py b/tcod/_internal.py index 08730332..694c9aaf 100644 --- a/tcod/_internal.py +++ b/tcod/_internal.py @@ -1,21 +1,23 @@ -"""This module internal helper functions used by the rest of the library.""" +"""Internal helper functions used by the rest of the library.""" from __future__ import annotations import functools import warnings -from typing import Any, AnyStr, Callable, TypeVar, cast +from types import TracebackType +from typing import Any, AnyStr, Callable, NoReturn, SupportsInt, TypeVar, cast import numpy as np -from numpy.typing import NDArray -from typing_extensions import Literal, NoReturn +from numpy.typing import ArrayLike, NDArray +from typing_extensions import Literal from tcod.loader import ffi, lib FuncType = Callable[..., Any] F = TypeVar("F", bound=FuncType) +T = TypeVar("T") -def deprecate(message: str, category: Any = DeprecationWarning, stacklevel: int = 0) -> Callable[[F], F]: +def deprecate(message: str, category: type[Warning] = DeprecationWarning, stacklevel: int = 0) -> Callable[[F], F]: """Return a decorator which adds a warning to functions.""" def decorator(func: F) -> F: @@ -23,7 +25,7 @@ def decorator(func: F) -> F: return func @functools.wraps(func) - def wrapper(*args, **kwargs): # type: ignore + def wrapper(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401 warnings.warn(message, category, stacklevel=stacklevel + 2) return func(*args, **kwargs) @@ -35,7 +37,7 @@ def wrapper(*args, **kwargs): # type: ignore def pending_deprecate( message: str = "This function may be deprecated in the future." " Consider raising an issue on GitHub if you need this feature.", - category: Any = PendingDeprecationWarning, + category: type[Warning] = PendingDeprecationWarning, stacklevel: int = 0, ) -> Callable[[F], F]: """Like deprecate, but the default parameters are filled out for a generic pending deprecation warning.""" @@ -43,9 +45,11 @@ def pending_deprecate( def verify_order(order: Literal["C", "F"]) -> Literal["C", "F"]: + """Verify and return a Numpy order string.""" order = order.upper() # type: ignore if order not in ("C", "F"): - raise TypeError("order must be 'C' or 'F', not %r" % (order,)) + msg = f"order must be 'C' or 'F', not {order!r}" + raise TypeError(msg) return order @@ -61,7 +65,7 @@ def _check(error: int) -> int: return error -def _check_p(pointer: Any) -> Any: +def _check_p(pointer: T) -> T: """Treats NULL pointers as errors and raises a libtcod exception.""" if not pointer: _raise_tcod_error() @@ -79,19 +83,19 @@ def _check_warn(error: int, stacklevel: int = 2) -> int: return error -def _unpack_char_p(char_p: Any) -> str: +def _unpack_char_p(char_p: Any) -> str: # noqa: ANN401 if char_p == ffi.NULL: return "" return ffi.string(char_p).decode() # type: ignore -def _int(int_or_str: Any) -> int: +def _int(int_or_str: SupportsInt | str | bytes) -> int: """Return an integer where a single character string may be expected.""" if isinstance(int_or_str, str): return ord(int_or_str) if isinstance(int_or_str, bytes): return int_or_str[0] - return int(int_or_str) # check for __count__ + return int(int_or_str) def _bytes(string: AnyStr) -> bytes: @@ -103,8 +107,8 @@ def _bytes(string: AnyStr) -> bytes: def _unicode(string: AnyStr, stacklevel: int = 2) -> str: if isinstance(string, bytes): warnings.warn( - ("Passing byte strings as parameters to Unicode functions is " "deprecated."), - DeprecationWarning, + "Passing byte strings as parameters to Unicode functions is deprecated.", + FutureWarning, stacklevel=stacklevel + 1, ) return string.decode("latin-1") @@ -114,8 +118,8 @@ def _unicode(string: AnyStr, stacklevel: int = 2) -> str: def _fmt(string: str, stacklevel: int = 2) -> bytes: if isinstance(string, bytes): warnings.warn( - ("Passing byte strings as parameters to Unicode functions is " "deprecated."), - DeprecationWarning, + "Passing byte strings as parameters to Unicode functions is deprecated.", + FutureWarning, stacklevel=stacklevel + 1, ) string = string.decode("latin-1") @@ -135,51 +139,46 @@ class _PropagateException: """ def __init__(self) -> None: - # (exception, exc_value, traceback) - self.exc_info = None # type: Any + self.caught: BaseException | None = None - def propagate(self, *exc_info: Any) -> None: + def propagate(self, *exc_info: Any) -> None: # noqa: ANN401 """Set an exception to be raised once this context exits. If multiple errors are caught, only keep the first exception raised. """ - if not self.exc_info: - self.exc_info = exc_info + if self.caught is None: + self.caught = exc_info[1] def __enter__(self) -> Callable[[Any], None]: """Once in context, only the propagate call is needed to use this class effectively.""" return self.propagate - def __exit__(self, type: Any, value: Any, traceback: Any) -> None: + def __exit__( + self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None + ) -> None: """If we're holding on to an exception, raise it now. - Prefers our held exception over any current raising error. - - self.exc_info is reset now in case of nested manager shenanigans. + self.caught is reset now in case of nested manager shenanigans. """ - if self.exc_info: - type, value, traceback = self.exc_info - self.exc_info = None - if type: - # Python 2/3 compatible throw - exception = type(value) - exception.__traceback__ = traceback - raise exception - - -class _CDataWrapper(object): - def __init__(self, *args: Any, **kwargs: Any): + to_raise, self.caught = self.caught, None + if to_raise is not None: + raise to_raise from value + + +class _CDataWrapper: + """A generally deprecated CData wrapper class used by libtcodpy.""" + + def __init__(self, *args: Any, **kwargs: Any): # noqa: ANN401 self.cdata = self._get_cdata_from_args(*args, **kwargs) if self.cdata is None: self.cdata = ffi.NULL - super(_CDataWrapper, self).__init__() + super().__init__() @staticmethod - def _get_cdata_from_args(*args: Any, **kwargs: Any) -> Any: + def _get_cdata_from_args(*args: Any, **kwargs: Any) -> Any: # noqa: ANN401 if len(args) == 1 and isinstance(args[0], ffi.CData) and not kwargs: return args[0] - else: - return None + return None def __hash__(self) -> int: return hash(self.cdata) @@ -199,11 +198,11 @@ def __setattr__(self, attr: str, value: Any) -> None: if hasattr(self, "cdata") and hasattr(self.cdata, attr): setattr(self.cdata, attr, value) else: - super(_CDataWrapper, self).__setattr__(attr, value) + super().__setattr__(attr, value) -def _console(console: Any) -> Any: - """Return a cffi console.""" +def _console(console: Any) -> Any: # noqa: ANN401 + """Return a cffi console pointer.""" try: return console.console_c except AttributeError: @@ -219,14 +218,16 @@ def _console(console: Any) -> Any: return ffi.NULL -class TempImage(object): +class TempImage: """An Image-like container for NumPy arrays.""" - def __init__(self, array: Any): + def __init__(self, array: ArrayLike) -> None: + """Initialize an image from the given array. May copy or reference the array.""" self._array: NDArray[np.uint8] = np.ascontiguousarray(array, dtype=np.uint8) height, width, depth = self._array.shape if depth != 3: - raise TypeError("Array must have RGB channels. Shape is: %r" % (self._array.shape,)) + msg = f"Array must have RGB channels. Shape is: {self._array.shape!r}" + raise TypeError(msg) self._buffer = ffi.from_buffer("TCOD_color_t[]", self._array) self._mipmaps = ffi.new( "struct TCOD_mipmap_*", diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..38bb211b --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Test package.""" From 74d6bb8721b14b4905e90c1b5cb7afce156632fa Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 21 May 2023 22:44:04 -0700 Subject: [PATCH 134/194] Restrict setuptools version to fix editable installs. --- pyproject.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 08fcbb7a..6c4c9a34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,8 @@ [build-system] requires = [ - "setuptools>=61.0.0", + # Newer versions of setuptools break editable installs + # https://github.com/pypa/setuptools/issues/3548 + "setuptools >=61.0.0, <64.0.0", "setuptools_scm[toml]>=6.2", "wheel>=0.37.1", "cffi>=1.15", From 4570f56d791d53d24194321cae9ca1afa61bc994 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 21 May 2023 22:53:23 -0700 Subject: [PATCH 135/194] Fix DLL path issues on Windows. `__path__` is incorrect now that this project has namespace packages. --- CHANGELOG.md | 2 ++ tcod/loader.py | 15 +++------------ 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6593ee6b..13a11f93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] +### Fixed +- DLL loader could fail to load `SDL2.dll` when other tcod namespace packages were installed. ## [15.0.1] - 2023-03-30 ### Added diff --git a/tcod/loader.py b/tcod/loader.py index 13efe66d..33ff0dd9 100644 --- a/tcod/loader.py +++ b/tcod/loader.py @@ -4,12 +4,11 @@ import os import platform import sys +from pathlib import Path from typing import Any # noqa: F401 import cffi # type: ignore -from tcod import __path__ - __sdl_version__ = "" ffi_check = cffi.FFI() @@ -46,20 +45,12 @@ def get_architecture() -> str: def get_sdl_version() -> str: sdl_version = ffi.new("SDL_version*") lib.SDL_GetVersion(sdl_version) - return "%s.%s.%s" % ( - sdl_version.major, - sdl_version.minor, - sdl_version.patch, - ) + return f"{sdl_version.major}.{sdl_version.minor}.{sdl_version.patch}" if sys.platform == "win32": # add Windows dll's to PATH - _bits, _linkage = platform.architecture() - os.environ["PATH"] = "%s;%s" % ( - os.path.join(__path__[0], get_architecture()), - os.environ["PATH"], - ) + os.environ["PATH"] = f"""{Path(__file__).parent / get_architecture()}{os.pathsep}{os.environ["PATH"]}""" class _Mock(object): From 0ce1a310725203c606ac67f4e4678087be0a85fd Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 21 May 2023 22:54:43 -0700 Subject: [PATCH 136/194] Minor updates to libtcodpy samples. Mostly need to preserve this as a reference to older uses of the code. At least until a cull of the API. --- examples/samples_libtcodpy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/samples_libtcodpy.py b/examples/samples_libtcodpy.py index c503cefb..c59b5b46 100755 --- a/examples/samples_libtcodpy.py +++ b/examples/samples_libtcodpy.py @@ -1268,7 +1268,7 @@ def render_name(first, key, mouse): if ng_nbsets == 0: # parse all *.cfg files in data/namegen for file in os.listdir(get_data("namegen")): - if file.find(b".cfg") > 0: + if file.find(".cfg") > 0: libtcod.namegen_parse(get_data(os.path.join("namegen", file))) # get the sets list ng_sets = libtcod.namegen_get_sets() @@ -1597,7 +1597,7 @@ def __init__(self, name, func): libtcod.console_set_default_foreground(None, libtcod.grey) libtcod.console_set_default_background(None, libtcod.black) libtcod.console_print_ex(None, 42, 46 - (libtcod.NB_RENDERERS + 1), libtcod.BKGND_SET, libtcod.LEFT, "Renderer :") - for i in range(libtcod.NB_RENDERERS): + for i in range(len(renderer_name)): if i == cur_renderer: libtcod.console_set_default_foreground(None, libtcod.white) libtcod.console_set_default_background(None, libtcod.light_blue) From a828e13c96d71f2c29cbee7350668b7a9360757b Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 21 May 2023 23:12:23 -0700 Subject: [PATCH 137/194] Clean up and deprecate colors. Colors removed from __all__ and warnings are given when they're accessed. --- CHANGELOG.md | 3 + build_libtcod.py | 1 - pyproject.toml | 1 + tcod/__init__.py | 214 +++------------------------------------------- tcod/color.py | 48 +++++------ tcod/constants.py | 197 ------------------------------------------ tcod/libtcodpy.py | 197 ------------------------------------------ 7 files changed, 36 insertions(+), 625 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13a11f93..cf27a946 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] +### Deprecated +- Deprecated all color constants + ### Fixed - DLL loader could fail to load `SDL2.dll` when other tcod namespace packages were installed. diff --git a/build_libtcod.py b/build_libtcod.py index e8d800cf..4ff65329 100644 --- a/build_libtcod.py +++ b/build_libtcod.py @@ -360,7 +360,6 @@ def write_library_constants() -> None: continue color = tcod.color.Color._new_from_cdata(value) f.write(f"{name[5:]} = {color!r}\n") - all_names.append(name[5:]) all_names_merged = ",\n ".join(f'"{name}"' for name in all_names) f.write(f"\n__all__ = [\n {all_names_merged},\n]\n") diff --git a/pyproject.toml b/pyproject.toml index 6c4c9a34..495cacb9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -135,6 +135,7 @@ ignore = [ "E501", # line-too-long "S101", # assert "ANN101", # missing-type-self + "ANN102", # missing-type-cls "D203", # one-blank-line-before-class "D204", # one-blank-line-after-class "D213", # multi-line-summary-second-line diff --git a/tcod/__init__.py b/tcod/__init__.py index 92885a2f..4a4b5173 100644 --- a/tcod/__init__.py +++ b/tcod/__init__.py @@ -8,13 +8,12 @@ """ from __future__ import annotations -import sys import warnings from pkgutil import extend_path __path__ = extend_path(__path__, __name__) -from tcod import bsp, color, console, context, event, image, los, map, noise, path, random, tileset +from tcod import bsp, color, console, constants, context, event, image, los, map, noise, path, random, tileset from tcod.console import Console # noqa: F401 from tcod.constants import * # noqa: F4 from tcod.libtcodpy import * # noqa: F4 @@ -25,12 +24,20 @@ except ImportError: # Gets imported without version.py by ReadTheDocs __version__ = "" -if sys.version_info < (3, 6): + +def __getattr__(name: str) -> color.Color: + """Mark access to color constants as deprecated.""" + value: color.Color | None = getattr(constants, name, None) + if value is None: + msg = f"module {__name__!r} has no attribute {name!r}" + raise AttributeError(msg) warnings.warn( - "Support for Python 3.5 has been dropped from python-tcod.", - DeprecationWarning, + f"Color constants will be removed from future releases.\nReplace `tcod.{name}` with `{tuple(value)}`.", + FutureWarning, stacklevel=2, ) + return value + __all__ = [ # noqa: F405 "__version__", @@ -604,202 +611,5 @@ "TYPE_VALUELIST13", "TYPE_VALUELIST14", "TYPE_VALUELIST15", - "amber", - "azure", - "black", - "blue", - "brass", - "celadon", - "chartreuse", - "copper", - "crimson", - "cyan", - "dark_amber", - "dark_azure", - "dark_blue", - "dark_chartreuse", - "dark_crimson", - "dark_cyan", - "dark_flame", - "dark_fuchsia", - "dark_gray", - "dark_green", - "dark_grey", - "dark_han", - "dark_lime", - "dark_magenta", - "dark_orange", - "dark_pink", - "dark_purple", - "dark_red", - "dark_sea", - "dark_sepia", - "dark_sky", - "dark_turquoise", - "dark_violet", - "dark_yellow", - "darker_amber", - "darker_azure", - "darker_blue", - "darker_chartreuse", - "darker_crimson", - "darker_cyan", - "darker_flame", - "darker_fuchsia", - "darker_gray", - "darker_green", - "darker_grey", - "darker_han", - "darker_lime", - "darker_magenta", - "darker_orange", - "darker_pink", - "darker_purple", - "darker_red", - "darker_sea", - "darker_sepia", - "darker_sky", - "darker_turquoise", - "darker_violet", - "darker_yellow", - "darkest_amber", - "darkest_azure", - "darkest_blue", - "darkest_chartreuse", - "darkest_crimson", - "darkest_cyan", - "darkest_flame", - "darkest_fuchsia", - "darkest_gray", - "darkest_green", - "darkest_grey", - "darkest_han", - "darkest_lime", - "darkest_magenta", - "darkest_orange", - "darkest_pink", - "darkest_purple", - "darkest_red", - "darkest_sea", - "darkest_sepia", - "darkest_sky", - "darkest_turquoise", - "darkest_violet", - "darkest_yellow", - "desaturated_amber", - "desaturated_azure", - "desaturated_blue", - "desaturated_chartreuse", - "desaturated_crimson", - "desaturated_cyan", - "desaturated_flame", - "desaturated_fuchsia", - "desaturated_green", - "desaturated_han", - "desaturated_lime", - "desaturated_magenta", - "desaturated_orange", - "desaturated_pink", - "desaturated_purple", - "desaturated_red", - "desaturated_sea", - "desaturated_sky", - "desaturated_turquoise", - "desaturated_violet", - "desaturated_yellow", - "flame", - "fuchsia", - "gold", - "gray", - "green", - "grey", - "han", - "light_amber", - "light_azure", - "light_blue", - "light_chartreuse", - "light_crimson", - "light_cyan", - "light_flame", - "light_fuchsia", - "light_gray", - "light_green", - "light_grey", - "light_han", - "light_lime", - "light_magenta", - "light_orange", - "light_pink", - "light_purple", - "light_red", - "light_sea", - "light_sepia", - "light_sky", - "light_turquoise", - "light_violet", - "light_yellow", - "lighter_amber", - "lighter_azure", - "lighter_blue", - "lighter_chartreuse", - "lighter_crimson", - "lighter_cyan", - "lighter_flame", - "lighter_fuchsia", - "lighter_gray", - "lighter_green", - "lighter_grey", - "lighter_han", - "lighter_lime", - "lighter_magenta", - "lighter_orange", - "lighter_pink", - "lighter_purple", - "lighter_red", - "lighter_sea", - "lighter_sepia", - "lighter_sky", - "lighter_turquoise", - "lighter_violet", - "lighter_yellow", - "lightest_amber", - "lightest_azure", - "lightest_blue", - "lightest_chartreuse", - "lightest_crimson", - "lightest_cyan", - "lightest_flame", - "lightest_fuchsia", - "lightest_gray", - "lightest_green", - "lightest_grey", - "lightest_han", - "lightest_lime", - "lightest_magenta", - "lightest_orange", - "lightest_pink", - "lightest_purple", - "lightest_red", - "lightest_sea", - "lightest_sepia", - "lightest_sky", - "lightest_turquoise", - "lightest_violet", - "lightest_yellow", - "lime", - "magenta", - "orange", - "peach", - "pink", - "purple", - "red", - "sea", - "sepia", - "silver", - "sky", - "turquoise", - "violet", - "white", - "yellow", # --- End constants.py --- ] diff --git a/tcod/color.py b/tcod/color.py index 470d7ac7..f5fed4c2 100644 --- a/tcod/color.py +++ b/tcod/color.py @@ -1,6 +1,4 @@ -""" - -""" +"""Old libtcod color management.""" from __future__ import annotations import warnings @@ -32,7 +30,7 @@ def r(self) -> int: return int(self[0]) @r.setter - @deprecate("Setting color attributes has been deprecated.") + @deprecate("Setting color attributes has been deprecated.", FutureWarning) def r(self, value: int) -> None: self[0] = value & 0xFF @@ -46,7 +44,7 @@ def g(self) -> int: return int(self[1]) @g.setter - @deprecate("Setting color attributes has been deprecated.") + @deprecate("Setting color attributes has been deprecated.", FutureWarning) def g(self, value: int) -> None: self[1] = value & 0xFF @@ -60,35 +58,35 @@ def b(self) -> int: return int(self[2]) @b.setter - @deprecate("Setting color attributes has been deprecated.") + @deprecate("Setting color attributes has been deprecated.", FutureWarning) def b(self, value: int) -> None: self[2] = value & 0xFF @classmethod - def _new_from_cdata(cls, cdata: Any) -> Color: + def _new_from_cdata(cls, cdata: Any) -> Color: # noqa: ANN401 return cls(cdata.r, cdata.g, cdata.b) - def __getitem__(self, index: Any) -> Any: - """ + def __getitem__(self, index: Any) -> Any: # noqa: ANN401 + """Return a color channel. + .. deprecated:: 9.2 Accessing colors via a letter index is deprecated. """ - try: - return super().__getitem__(index) - except TypeError: + if isinstance(index, str): warnings.warn( "Accessing colors via a letter index is deprecated", DeprecationWarning, stacklevel=2, ) return super().__getitem__("rgb".index(index)) + return super().__getitem__(index) - @deprecate("This class will not be mutable in the future.") - def __setitem__(self, index: Any, value: Any) -> None: - try: - super().__setitem__(index, value) - except TypeError: + @deprecate("This class will not be mutable in the future.", FutureWarning) + def __setitem__(self, index: Any, value: Any) -> None: # noqa: ANN401 + if isinstance(index, str): super().__setitem__("rgb".index(index), value) + else: + super().__setitem__(index, value) def __eq__(self, other: Any) -> bool: """Compare equality between colors. @@ -100,7 +98,7 @@ def __eq__(self, other: Any) -> bool: except TypeError: return False - @deprecate("Use NumPy instead for color math operations.") + @deprecate("Use NumPy instead for color math operations.", FutureWarning) def __add__(self, other: Any) -> Color: # type: ignore[override] """Add two colors together. @@ -109,7 +107,7 @@ def __add__(self, other: Any) -> Color: # type: ignore[override] """ return Color._new_from_cdata(lib.TCOD_color_add(self, other)) - @deprecate("Use NumPy instead for color math operations.") + @deprecate("Use NumPy instead for color math operations.", FutureWarning) def __sub__(self, other: Any) -> Color: """Subtract one color from another. @@ -118,7 +116,7 @@ def __sub__(self, other: Any) -> Color: """ return Color._new_from_cdata(lib.TCOD_color_subtract(self, other)) - @deprecate("Use NumPy instead for color math operations.") + @deprecate("Use NumPy instead for color math operations.", FutureWarning) def __mul__(self, other: Any) -> Color: """Multiply with a scaler or another color. @@ -127,14 +125,8 @@ def __mul__(self, other: Any) -> Color: """ if isinstance(other, (Color, list, tuple)): return Color._new_from_cdata(lib.TCOD_color_multiply(self, other)) - else: - return Color._new_from_cdata(lib.TCOD_color_multiply_scalar(self, other)) + return Color._new_from_cdata(lib.TCOD_color_multiply_scalar(self, other)) def __repr__(self) -> str: """Return a printable representation of the current color.""" - return "%s(%r, %r, %r)" % ( - self.__class__.__name__, - self.r, - self.g, - self.b, - ) + return f"{self.__class__.__name__}({self.r!r}, {self.g!r}, {self.b!r})" diff --git a/tcod/constants.py b/tcod/constants.py index 4cb84313..f0a596dc 100644 --- a/tcod/constants.py +++ b/tcod/constants.py @@ -801,201 +801,4 @@ "TYPE_VALUELIST13", "TYPE_VALUELIST14", "TYPE_VALUELIST15", - "amber", - "azure", - "black", - "blue", - "brass", - "celadon", - "chartreuse", - "copper", - "crimson", - "cyan", - "dark_amber", - "dark_azure", - "dark_blue", - "dark_chartreuse", - "dark_crimson", - "dark_cyan", - "dark_flame", - "dark_fuchsia", - "dark_gray", - "dark_green", - "dark_grey", - "dark_han", - "dark_lime", - "dark_magenta", - "dark_orange", - "dark_pink", - "dark_purple", - "dark_red", - "dark_sea", - "dark_sepia", - "dark_sky", - "dark_turquoise", - "dark_violet", - "dark_yellow", - "darker_amber", - "darker_azure", - "darker_blue", - "darker_chartreuse", - "darker_crimson", - "darker_cyan", - "darker_flame", - "darker_fuchsia", - "darker_gray", - "darker_green", - "darker_grey", - "darker_han", - "darker_lime", - "darker_magenta", - "darker_orange", - "darker_pink", - "darker_purple", - "darker_red", - "darker_sea", - "darker_sepia", - "darker_sky", - "darker_turquoise", - "darker_violet", - "darker_yellow", - "darkest_amber", - "darkest_azure", - "darkest_blue", - "darkest_chartreuse", - "darkest_crimson", - "darkest_cyan", - "darkest_flame", - "darkest_fuchsia", - "darkest_gray", - "darkest_green", - "darkest_grey", - "darkest_han", - "darkest_lime", - "darkest_magenta", - "darkest_orange", - "darkest_pink", - "darkest_purple", - "darkest_red", - "darkest_sea", - "darkest_sepia", - "darkest_sky", - "darkest_turquoise", - "darkest_violet", - "darkest_yellow", - "desaturated_amber", - "desaturated_azure", - "desaturated_blue", - "desaturated_chartreuse", - "desaturated_crimson", - "desaturated_cyan", - "desaturated_flame", - "desaturated_fuchsia", - "desaturated_green", - "desaturated_han", - "desaturated_lime", - "desaturated_magenta", - "desaturated_orange", - "desaturated_pink", - "desaturated_purple", - "desaturated_red", - "desaturated_sea", - "desaturated_sky", - "desaturated_turquoise", - "desaturated_violet", - "desaturated_yellow", - "flame", - "fuchsia", - "gold", - "gray", - "green", - "grey", - "han", - "light_amber", - "light_azure", - "light_blue", - "light_chartreuse", - "light_crimson", - "light_cyan", - "light_flame", - "light_fuchsia", - "light_gray", - "light_green", - "light_grey", - "light_han", - "light_lime", - "light_magenta", - "light_orange", - "light_pink", - "light_purple", - "light_red", - "light_sea", - "light_sepia", - "light_sky", - "light_turquoise", - "light_violet", - "light_yellow", - "lighter_amber", - "lighter_azure", - "lighter_blue", - "lighter_chartreuse", - "lighter_crimson", - "lighter_cyan", - "lighter_flame", - "lighter_fuchsia", - "lighter_gray", - "lighter_green", - "lighter_grey", - "lighter_han", - "lighter_lime", - "lighter_magenta", - "lighter_orange", - "lighter_pink", - "lighter_purple", - "lighter_red", - "lighter_sea", - "lighter_sepia", - "lighter_sky", - "lighter_turquoise", - "lighter_violet", - "lighter_yellow", - "lightest_amber", - "lightest_azure", - "lightest_blue", - "lightest_chartreuse", - "lightest_crimson", - "lightest_cyan", - "lightest_flame", - "lightest_fuchsia", - "lightest_gray", - "lightest_green", - "lightest_grey", - "lightest_han", - "lightest_lime", - "lightest_magenta", - "lightest_orange", - "lightest_pink", - "lightest_purple", - "lightest_red", - "lightest_sea", - "lightest_sepia", - "lightest_sky", - "lightest_turquoise", - "lightest_violet", - "lightest_yellow", - "lime", - "magenta", - "orange", - "peach", - "pink", - "purple", - "red", - "sea", - "sepia", - "silver", - "sky", - "turquoise", - "violet", - "white", - "yellow", ] diff --git a/tcod/libtcodpy.py b/tcod/libtcodpy.py index 1271a3c1..51974108 100644 --- a/tcod/libtcodpy.py +++ b/tcod/libtcodpy.py @@ -4755,202 +4755,5 @@ def _atexit_verify() -> None: "TYPE_VALUELIST13", "TYPE_VALUELIST14", "TYPE_VALUELIST15", - "amber", - "azure", - "black", - "blue", - "brass", - "celadon", - "chartreuse", - "copper", - "crimson", - "cyan", - "dark_amber", - "dark_azure", - "dark_blue", - "dark_chartreuse", - "dark_crimson", - "dark_cyan", - "dark_flame", - "dark_fuchsia", - "dark_gray", - "dark_green", - "dark_grey", - "dark_han", - "dark_lime", - "dark_magenta", - "dark_orange", - "dark_pink", - "dark_purple", - "dark_red", - "dark_sea", - "dark_sepia", - "dark_sky", - "dark_turquoise", - "dark_violet", - "dark_yellow", - "darker_amber", - "darker_azure", - "darker_blue", - "darker_chartreuse", - "darker_crimson", - "darker_cyan", - "darker_flame", - "darker_fuchsia", - "darker_gray", - "darker_green", - "darker_grey", - "darker_han", - "darker_lime", - "darker_magenta", - "darker_orange", - "darker_pink", - "darker_purple", - "darker_red", - "darker_sea", - "darker_sepia", - "darker_sky", - "darker_turquoise", - "darker_violet", - "darker_yellow", - "darkest_amber", - "darkest_azure", - "darkest_blue", - "darkest_chartreuse", - "darkest_crimson", - "darkest_cyan", - "darkest_flame", - "darkest_fuchsia", - "darkest_gray", - "darkest_green", - "darkest_grey", - "darkest_han", - "darkest_lime", - "darkest_magenta", - "darkest_orange", - "darkest_pink", - "darkest_purple", - "darkest_red", - "darkest_sea", - "darkest_sepia", - "darkest_sky", - "darkest_turquoise", - "darkest_violet", - "darkest_yellow", - "desaturated_amber", - "desaturated_azure", - "desaturated_blue", - "desaturated_chartreuse", - "desaturated_crimson", - "desaturated_cyan", - "desaturated_flame", - "desaturated_fuchsia", - "desaturated_green", - "desaturated_han", - "desaturated_lime", - "desaturated_magenta", - "desaturated_orange", - "desaturated_pink", - "desaturated_purple", - "desaturated_red", - "desaturated_sea", - "desaturated_sky", - "desaturated_turquoise", - "desaturated_violet", - "desaturated_yellow", - "flame", - "fuchsia", - "gold", - "gray", - "green", - "grey", - "han", - "light_amber", - "light_azure", - "light_blue", - "light_chartreuse", - "light_crimson", - "light_cyan", - "light_flame", - "light_fuchsia", - "light_gray", - "light_green", - "light_grey", - "light_han", - "light_lime", - "light_magenta", - "light_orange", - "light_pink", - "light_purple", - "light_red", - "light_sea", - "light_sepia", - "light_sky", - "light_turquoise", - "light_violet", - "light_yellow", - "lighter_amber", - "lighter_azure", - "lighter_blue", - "lighter_chartreuse", - "lighter_crimson", - "lighter_cyan", - "lighter_flame", - "lighter_fuchsia", - "lighter_gray", - "lighter_green", - "lighter_grey", - "lighter_han", - "lighter_lime", - "lighter_magenta", - "lighter_orange", - "lighter_pink", - "lighter_purple", - "lighter_red", - "lighter_sea", - "lighter_sepia", - "lighter_sky", - "lighter_turquoise", - "lighter_violet", - "lighter_yellow", - "lightest_amber", - "lightest_azure", - "lightest_blue", - "lightest_chartreuse", - "lightest_crimson", - "lightest_cyan", - "lightest_flame", - "lightest_fuchsia", - "lightest_gray", - "lightest_green", - "lightest_grey", - "lightest_han", - "lightest_lime", - "lightest_magenta", - "lightest_orange", - "lightest_pink", - "lightest_purple", - "lightest_red", - "lightest_sea", - "lightest_sepia", - "lightest_sky", - "lightest_turquoise", - "lightest_violet", - "lightest_yellow", - "lime", - "magenta", - "orange", - "peach", - "pink", - "purple", - "red", - "sea", - "sepia", - "silver", - "sky", - "turquoise", - "violet", - "white", - "yellow", # --- End constants.py --- ] From cac2835bed5cfed8bb2d591696d1c82335c7d542 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 21 May 2023 23:42:11 -0700 Subject: [PATCH 138/194] Update Mypy config. Migrate to pyproject.toml. Add types-cffi stubs. --- .github/workflows/python-package.yml | 2 +- build_libtcod.py | 2 +- pyproject.toml | 33 ++++++++++++++++++++++++++++ requirements.txt | 1 + tcod/loader.py | 13 ++++++----- 5 files changed, 43 insertions(+), 8 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index c8749e3f..bac48070 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -69,7 +69,7 @@ jobs: uses: liskin/gh-problem-matcher-wrap@v2 with: linters: mypy - run: mypy --show-column-numbers . + run: mypy --show-column-numbers # This makes sure that the latest versions of the SDL headers parse correctly. parse_sdl: diff --git a/build_libtcod.py b/build_libtcod.py index 4ff65329..aaa60661 100644 --- a/build_libtcod.py +++ b/build_libtcod.py @@ -9,7 +9,7 @@ from pathlib import Path from typing import Any, Dict, Iterable, Iterator, List, Set, Tuple, Union -from cffi import FFI # type: ignore +from cffi import FFI sys.path.append(str(Path(__file__).parent)) # Allow importing local modules. diff --git a/pyproject.toml b/pyproject.toml index 495cacb9..d4baab4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,6 +101,39 @@ filterwarnings = [ "ignore:This class may perform poorly and is no longer needed.::tcod.map", ] +[tool.mypy] +files = ["."] +python_version = 3.8 +warn_unused_configs = true +show_error_codes = true +disallow_subclassing_any = true +disallow_any_generics = true +disallow_untyped_calls = true +disallow_untyped_defs = true +disallow_incomplete_defs = true +check_untyped_defs = true +disallow_untyped_decorators = true +no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_return_any = true +implicit_reexport = false +strict_equality = true +exclude = [ + "build/", + "venv/", + "libtcod/", + "docs/", + "distribution/", + "termbox/", + "samples_libtcodpy.py", +] + +[[tool.mypy.overrides]] +module = "numpy.*" +ignore_missing_imports = true + + [tool.ruff] # https://beta.ruff.rs/docs/rules/ select = [ diff --git a/requirements.txt b/requirements.txt index 7bb32eec..ee1ea8a0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ numpy>=1.21.4 pycparser>=2.14 requests>=2.28.1 setuptools==65.5.1 +types-cffi types-requests types-setuptools types-tabulate diff --git a/tcod/loader.py b/tcod/loader.py index 33ff0dd9..1a296b12 100644 --- a/tcod/loader.py +++ b/tcod/loader.py @@ -7,7 +7,7 @@ from pathlib import Path from typing import Any # noqa: F401 -import cffi # type: ignore +import cffi __sdl_version__ = "" @@ -29,12 +29,13 @@ def verify_dependencies() -> None: """Try to make sure dependencies exist on this system.""" if sys.platform == "win32": - lib_test = ffi_check.dlopen("SDL2.dll") # Make sure SDL2.dll is here. - version = ffi_check.new("struct SDL_version*") + lib_test: Any = ffi_check.dlopen("SDL2.dll") # Make sure SDL2.dll is here. + version: Any = ffi_check.new("struct SDL_version*") lib_test.SDL_GetVersion(version) # Need to check this version. - version = version.major, version.minor, version.patch - if version < (2, 0, 5): - raise RuntimeError("Tried to load an old version of SDL %r" % (version,)) + version_tuple = version.major, version.minor, version.patch + if version_tuple < (2, 0, 5): + msg = f"Tried to load an old version of SDL {version_tuple!r}" + raise RuntimeError(msg) def get_architecture() -> str: From 6c0b70fbbb74dc5546929341c75cba64766de920 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 22 May 2023 00:49:58 -0700 Subject: [PATCH 139/194] Deprecate older keyboard constants. Add warnings telling which enum replaces any key. Test deprecated keys and colors. --- CHANGELOG.md | 4 +- build_libtcod.py | 14 +- tcod/event.py | 507 +++------------------------------------ tcod/event_constants.py | 484 ------------------------------------- tests/test_deprecated.py | 23 ++ 5 files changed, 62 insertions(+), 970 deletions(-) create mode 100644 tests/test_deprecated.py diff --git a/CHANGELOG.md b/CHANGELOG.md index cf27a946..d9043af2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,9 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version ## [Unreleased] ### Deprecated -- Deprecated all color constants +- Deprecated all libtcod color constants. Replace these with your own manually defined colors. + Using a color will tell you the color values of the deprecated color in the warning. +- Deprecated older scancode and keysym constants. These were replaced with the Scancode and KeySym enums. ### Fixed - DLL loader could fail to load `SDL2.dll` when other tcod namespace packages were installed. diff --git a/build_libtcod.py b/build_libtcod.py index aaa60661..b0491e81 100644 --- a/build_libtcod.py +++ b/build_libtcod.py @@ -256,9 +256,8 @@ def find_sdl_attrs(prefix: str) -> Iterator[Tuple[str, Union[int, str, Any]]]: yield attr[name_starts_at:], getattr(lib, attr) -def parse_sdl_attrs(prefix: str, all_names: List[str]) -> Tuple[str, str]: - """Return the name/value pairs, and the final dictionary string for the - library attributes with `prefix`. +def parse_sdl_attrs(prefix: str, all_names: list[str] | None) -> tuple[str, str]: + """Return the name/value pairs, and the final dictionary string for the library attributes with `prefix`. Append matching names to the `all_names` list. """ @@ -267,10 +266,11 @@ def parse_sdl_attrs(prefix: str, all_names: List[str]) -> Tuple[str, str]: for name, value in sorted(find_sdl_attrs(prefix), key=lambda item: item[1]): if name == "KMOD_RESERVED": continue - all_names.append(name) + if all_names is not None: + all_names.append(name) names.append(f"{name} = {value}") lookup.append(f'{value}: "{name}"') - return "\n".join(names), "{\n %s,\n}" % (",\n ".join(lookup),) + return "\n".join(names), "{{\n {},\n}}".format(",\n ".join(lookup)) EXCLUDE_CONSTANTS = [ @@ -370,10 +370,10 @@ def write_library_constants() -> None: all_names = [] f.write(EVENT_CONSTANT_MODULE_HEADER) f.write("\n# --- SDL scancodes ---\n") - f.write(f"""{parse_sdl_attrs("SDL_SCANCODE", all_names)[0]}\n""") + f.write(f"""{parse_sdl_attrs("SDL_SCANCODE", None)[0]}\n""") f.write("\n# --- SDL keyboard symbols ---\n") - f.write(f"""{parse_sdl_attrs("SDLK", all_names)[0]}\n""") + f.write(f"""{parse_sdl_attrs("SDLK", None)[0]}\n""") f.write("\n# --- SDL keyboard modifiers ---\n") f.write("%s\n_REVERSE_MOD_TABLE = %s\n" % parse_sdl_attrs("KMOD", all_names)) diff --git a/tcod/event.py b/tcod/event.py index 1e6bd44a..19e0ee1e 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -2770,6 +2770,35 @@ def __repr__(self) -> str: return f"tcod.event.{self.__class__.__name__}.{self.name}" +def __getattr__(name: str) -> int: + """Migrate deprecated access of event constants.""" + value: int | None = getattr(tcod.event_constants, name, None) + if not value: + msg = f"module {__name__!r} has no attribute {name!r}" + raise AttributeError(msg) + if name.startswith("SCANCODE_"): + scancode = name[9:] + if scancode.isdigit(): + scancode = f"N{scancode}" + warnings.warn( + "Key constants have been replaced with enums.\n" + f"`tcod.event.{name}` should be replaced with `tcod.event.Scancode.{scancode}`", + FutureWarning, + stacklevel=2, + ) + elif name.startswith("K_"): + sym = name[2:] + if sym.isdigit(): + sym = f"N{sym}" + warnings.warn( + "Key constants have been replaced with enums.\n" + f"`tcod.event.{name}` should be replaced with `tcod.event.KeySym.{sym}`", + FutureWarning, + stacklevel=2, + ) + return value + + __all__ = [ # noqa: F405 "Modifier", "Point", @@ -2819,484 +2848,6 @@ def __repr__(self) -> str: "Scancode", "KeySym", # --- From event_constants.py --- - "SCANCODE_UNKNOWN", - "SCANCODE_A", - "SCANCODE_B", - "SCANCODE_C", - "SCANCODE_D", - "SCANCODE_E", - "SCANCODE_F", - "SCANCODE_G", - "SCANCODE_H", - "SCANCODE_I", - "SCANCODE_J", - "SCANCODE_K", - "SCANCODE_L", - "SCANCODE_M", - "SCANCODE_N", - "SCANCODE_O", - "SCANCODE_P", - "SCANCODE_Q", - "SCANCODE_R", - "SCANCODE_S", - "SCANCODE_T", - "SCANCODE_U", - "SCANCODE_V", - "SCANCODE_W", - "SCANCODE_X", - "SCANCODE_Y", - "SCANCODE_Z", - "SCANCODE_1", - "SCANCODE_2", - "SCANCODE_3", - "SCANCODE_4", - "SCANCODE_5", - "SCANCODE_6", - "SCANCODE_7", - "SCANCODE_8", - "SCANCODE_9", - "SCANCODE_0", - "SCANCODE_RETURN", - "SCANCODE_ESCAPE", - "SCANCODE_BACKSPACE", - "SCANCODE_TAB", - "SCANCODE_SPACE", - "SCANCODE_MINUS", - "SCANCODE_EQUALS", - "SCANCODE_LEFTBRACKET", - "SCANCODE_RIGHTBRACKET", - "SCANCODE_BACKSLASH", - "SCANCODE_NONUSHASH", - "SCANCODE_SEMICOLON", - "SCANCODE_APOSTROPHE", - "SCANCODE_GRAVE", - "SCANCODE_COMMA", - "SCANCODE_PERIOD", - "SCANCODE_SLASH", - "SCANCODE_CAPSLOCK", - "SCANCODE_F1", - "SCANCODE_F2", - "SCANCODE_F3", - "SCANCODE_F4", - "SCANCODE_F5", - "SCANCODE_F6", - "SCANCODE_F7", - "SCANCODE_F8", - "SCANCODE_F9", - "SCANCODE_F10", - "SCANCODE_F11", - "SCANCODE_F12", - "SCANCODE_PRINTSCREEN", - "SCANCODE_SCROLLLOCK", - "SCANCODE_PAUSE", - "SCANCODE_INSERT", - "SCANCODE_HOME", - "SCANCODE_PAGEUP", - "SCANCODE_DELETE", - "SCANCODE_END", - "SCANCODE_PAGEDOWN", - "SCANCODE_RIGHT", - "SCANCODE_LEFT", - "SCANCODE_DOWN", - "SCANCODE_UP", - "SCANCODE_NUMLOCKCLEAR", - "SCANCODE_KP_DIVIDE", - "SCANCODE_KP_MULTIPLY", - "SCANCODE_KP_MINUS", - "SCANCODE_KP_PLUS", - "SCANCODE_KP_ENTER", - "SCANCODE_KP_1", - "SCANCODE_KP_2", - "SCANCODE_KP_3", - "SCANCODE_KP_4", - "SCANCODE_KP_5", - "SCANCODE_KP_6", - "SCANCODE_KP_7", - "SCANCODE_KP_8", - "SCANCODE_KP_9", - "SCANCODE_KP_0", - "SCANCODE_KP_PERIOD", - "SCANCODE_NONUSBACKSLASH", - "SCANCODE_APPLICATION", - "SCANCODE_POWER", - "SCANCODE_KP_EQUALS", - "SCANCODE_F13", - "SCANCODE_F14", - "SCANCODE_F15", - "SCANCODE_F16", - "SCANCODE_F17", - "SCANCODE_F18", - "SCANCODE_F19", - "SCANCODE_F20", - "SCANCODE_F21", - "SCANCODE_F22", - "SCANCODE_F23", - "SCANCODE_F24", - "SCANCODE_EXECUTE", - "SCANCODE_HELP", - "SCANCODE_MENU", - "SCANCODE_SELECT", - "SCANCODE_STOP", - "SCANCODE_AGAIN", - "SCANCODE_UNDO", - "SCANCODE_CUT", - "SCANCODE_COPY", - "SCANCODE_PASTE", - "SCANCODE_FIND", - "SCANCODE_MUTE", - "SCANCODE_VOLUMEUP", - "SCANCODE_VOLUMEDOWN", - "SCANCODE_KP_COMMA", - "SCANCODE_KP_EQUALSAS400", - "SCANCODE_INTERNATIONAL1", - "SCANCODE_INTERNATIONAL2", - "SCANCODE_INTERNATIONAL3", - "SCANCODE_INTERNATIONAL4", - "SCANCODE_INTERNATIONAL5", - "SCANCODE_INTERNATIONAL6", - "SCANCODE_INTERNATIONAL7", - "SCANCODE_INTERNATIONAL8", - "SCANCODE_INTERNATIONAL9", - "SCANCODE_LANG1", - "SCANCODE_LANG2", - "SCANCODE_LANG3", - "SCANCODE_LANG4", - "SCANCODE_LANG5", - "SCANCODE_LANG6", - "SCANCODE_LANG7", - "SCANCODE_LANG8", - "SCANCODE_LANG9", - "SCANCODE_ALTERASE", - "SCANCODE_SYSREQ", - "SCANCODE_CANCEL", - "SCANCODE_CLEAR", - "SCANCODE_PRIOR", - "SCANCODE_RETURN2", - "SCANCODE_SEPARATOR", - "SCANCODE_OUT", - "SCANCODE_OPER", - "SCANCODE_CLEARAGAIN", - "SCANCODE_CRSEL", - "SCANCODE_EXSEL", - "SCANCODE_KP_00", - "SCANCODE_KP_000", - "SCANCODE_THOUSANDSSEPARATOR", - "SCANCODE_DECIMALSEPARATOR", - "SCANCODE_CURRENCYUNIT", - "SCANCODE_CURRENCYSUBUNIT", - "SCANCODE_KP_LEFTPAREN", - "SCANCODE_KP_RIGHTPAREN", - "SCANCODE_KP_LEFTBRACE", - "SCANCODE_KP_RIGHTBRACE", - "SCANCODE_KP_TAB", - "SCANCODE_KP_BACKSPACE", - "SCANCODE_KP_A", - "SCANCODE_KP_B", - "SCANCODE_KP_C", - "SCANCODE_KP_D", - "SCANCODE_KP_E", - "SCANCODE_KP_F", - "SCANCODE_KP_XOR", - "SCANCODE_KP_POWER", - "SCANCODE_KP_PERCENT", - "SCANCODE_KP_LESS", - "SCANCODE_KP_GREATER", - "SCANCODE_KP_AMPERSAND", - "SCANCODE_KP_DBLAMPERSAND", - "SCANCODE_KP_VERTICALBAR", - "SCANCODE_KP_DBLVERTICALBAR", - "SCANCODE_KP_COLON", - "SCANCODE_KP_HASH", - "SCANCODE_KP_SPACE", - "SCANCODE_KP_AT", - "SCANCODE_KP_EXCLAM", - "SCANCODE_KP_MEMSTORE", - "SCANCODE_KP_MEMRECALL", - "SCANCODE_KP_MEMCLEAR", - "SCANCODE_KP_MEMADD", - "SCANCODE_KP_MEMSUBTRACT", - "SCANCODE_KP_MEMMULTIPLY", - "SCANCODE_KP_MEMDIVIDE", - "SCANCODE_KP_PLUSMINUS", - "SCANCODE_KP_CLEAR", - "SCANCODE_KP_CLEARENTRY", - "SCANCODE_KP_BINARY", - "SCANCODE_KP_OCTAL", - "SCANCODE_KP_DECIMAL", - "SCANCODE_KP_HEXADECIMAL", - "SCANCODE_LCTRL", - "SCANCODE_LSHIFT", - "SCANCODE_LALT", - "SCANCODE_LGUI", - "SCANCODE_RCTRL", - "SCANCODE_RSHIFT", - "SCANCODE_RALT", - "SCANCODE_RGUI", - "SCANCODE_MODE", - "SCANCODE_AUDIONEXT", - "SCANCODE_AUDIOPREV", - "SCANCODE_AUDIOSTOP", - "SCANCODE_AUDIOPLAY", - "SCANCODE_AUDIOMUTE", - "SCANCODE_MEDIASELECT", - "SCANCODE_WWW", - "SCANCODE_MAIL", - "SCANCODE_CALCULATOR", - "SCANCODE_COMPUTER", - "SCANCODE_AC_SEARCH", - "SCANCODE_AC_HOME", - "SCANCODE_AC_BACK", - "SCANCODE_AC_FORWARD", - "SCANCODE_AC_STOP", - "SCANCODE_AC_REFRESH", - "SCANCODE_AC_BOOKMARKS", - "SCANCODE_BRIGHTNESSDOWN", - "SCANCODE_BRIGHTNESSUP", - "SCANCODE_DISPLAYSWITCH", - "SCANCODE_KBDILLUMTOGGLE", - "SCANCODE_KBDILLUMDOWN", - "SCANCODE_KBDILLUMUP", - "SCANCODE_EJECT", - "SCANCODE_SLEEP", - "SCANCODE_APP1", - "SCANCODE_APP2", - "K_UNKNOWN", - "K_BACKSPACE", - "K_TAB", - "K_RETURN", - "K_ESCAPE", - "K_SPACE", - "K_EXCLAIM", - "K_QUOTEDBL", - "K_HASH", - "K_DOLLAR", - "K_PERCENT", - "K_AMPERSAND", - "K_QUOTE", - "K_LEFTPAREN", - "K_RIGHTPAREN", - "K_ASTERISK", - "K_PLUS", - "K_COMMA", - "K_MINUS", - "K_PERIOD", - "K_SLASH", - "K_0", - "K_1", - "K_2", - "K_3", - "K_4", - "K_5", - "K_6", - "K_7", - "K_8", - "K_9", - "K_COLON", - "K_SEMICOLON", - "K_LESS", - "K_EQUALS", - "K_GREATER", - "K_QUESTION", - "K_AT", - "K_LEFTBRACKET", - "K_BACKSLASH", - "K_RIGHTBRACKET", - "K_CARET", - "K_UNDERSCORE", - "K_BACKQUOTE", - "K_a", - "K_b", - "K_c", - "K_d", - "K_e", - "K_f", - "K_g", - "K_h", - "K_i", - "K_j", - "K_k", - "K_l", - "K_m", - "K_n", - "K_o", - "K_p", - "K_q", - "K_r", - "K_s", - "K_t", - "K_u", - "K_v", - "K_w", - "K_x", - "K_y", - "K_z", - "K_DELETE", - "K_SCANCODE_MASK", - "K_CAPSLOCK", - "K_F1", - "K_F2", - "K_F3", - "K_F4", - "K_F5", - "K_F6", - "K_F7", - "K_F8", - "K_F9", - "K_F10", - "K_F11", - "K_F12", - "K_PRINTSCREEN", - "K_SCROLLLOCK", - "K_PAUSE", - "K_INSERT", - "K_HOME", - "K_PAGEUP", - "K_END", - "K_PAGEDOWN", - "K_RIGHT", - "K_LEFT", - "K_DOWN", - "K_UP", - "K_NUMLOCKCLEAR", - "K_KP_DIVIDE", - "K_KP_MULTIPLY", - "K_KP_MINUS", - "K_KP_PLUS", - "K_KP_ENTER", - "K_KP_1", - "K_KP_2", - "K_KP_3", - "K_KP_4", - "K_KP_5", - "K_KP_6", - "K_KP_7", - "K_KP_8", - "K_KP_9", - "K_KP_0", - "K_KP_PERIOD", - "K_APPLICATION", - "K_POWER", - "K_KP_EQUALS", - "K_F13", - "K_F14", - "K_F15", - "K_F16", - "K_F17", - "K_F18", - "K_F19", - "K_F20", - "K_F21", - "K_F22", - "K_F23", - "K_F24", - "K_EXECUTE", - "K_HELP", - "K_MENU", - "K_SELECT", - "K_STOP", - "K_AGAIN", - "K_UNDO", - "K_CUT", - "K_COPY", - "K_PASTE", - "K_FIND", - "K_MUTE", - "K_VOLUMEUP", - "K_VOLUMEDOWN", - "K_KP_COMMA", - "K_KP_EQUALSAS400", - "K_ALTERASE", - "K_SYSREQ", - "K_CANCEL", - "K_CLEAR", - "K_PRIOR", - "K_RETURN2", - "K_SEPARATOR", - "K_OUT", - "K_OPER", - "K_CLEARAGAIN", - "K_CRSEL", - "K_EXSEL", - "K_KP_00", - "K_KP_000", - "K_THOUSANDSSEPARATOR", - "K_DECIMALSEPARATOR", - "K_CURRENCYUNIT", - "K_CURRENCYSUBUNIT", - "K_KP_LEFTPAREN", - "K_KP_RIGHTPAREN", - "K_KP_LEFTBRACE", - "K_KP_RIGHTBRACE", - "K_KP_TAB", - "K_KP_BACKSPACE", - "K_KP_A", - "K_KP_B", - "K_KP_C", - "K_KP_D", - "K_KP_E", - "K_KP_F", - "K_KP_XOR", - "K_KP_POWER", - "K_KP_PERCENT", - "K_KP_LESS", - "K_KP_GREATER", - "K_KP_AMPERSAND", - "K_KP_DBLAMPERSAND", - "K_KP_VERTICALBAR", - "K_KP_DBLVERTICALBAR", - "K_KP_COLON", - "K_KP_HASH", - "K_KP_SPACE", - "K_KP_AT", - "K_KP_EXCLAM", - "K_KP_MEMSTORE", - "K_KP_MEMRECALL", - "K_KP_MEMCLEAR", - "K_KP_MEMADD", - "K_KP_MEMSUBTRACT", - "K_KP_MEMMULTIPLY", - "K_KP_MEMDIVIDE", - "K_KP_PLUSMINUS", - "K_KP_CLEAR", - "K_KP_CLEARENTRY", - "K_KP_BINARY", - "K_KP_OCTAL", - "K_KP_DECIMAL", - "K_KP_HEXADECIMAL", - "K_LCTRL", - "K_LSHIFT", - "K_LALT", - "K_LGUI", - "K_RCTRL", - "K_RSHIFT", - "K_RALT", - "K_RGUI", - "K_MODE", - "K_AUDIONEXT", - "K_AUDIOPREV", - "K_AUDIOSTOP", - "K_AUDIOPLAY", - "K_AUDIOMUTE", - "K_MEDIASELECT", - "K_WWW", - "K_MAIL", - "K_CALCULATOR", - "K_COMPUTER", - "K_AC_SEARCH", - "K_AC_HOME", - "K_AC_BACK", - "K_AC_FORWARD", - "K_AC_STOP", - "K_AC_REFRESH", - "K_AC_BOOKMARKS", - "K_BRIGHTNESSDOWN", - "K_BRIGHTNESSUP", - "K_DISPLAYSWITCH", - "K_KBDILLUMTOGGLE", - "K_KBDILLUMDOWN", - "K_KBDILLUMUP", - "K_EJECT", - "K_SLEEP", "KMOD_NONE", "KMOD_LSHIFT", "KMOD_RSHIFT", diff --git a/tcod/event_constants.py b/tcod/event_constants.py index aca5d5ec..d10eeb74 100644 --- a/tcod/event_constants.py +++ b/tcod/event_constants.py @@ -540,490 +540,6 @@ } __all__ = [ - "SCANCODE_UNKNOWN", - "SCANCODE_A", - "SCANCODE_B", - "SCANCODE_C", - "SCANCODE_D", - "SCANCODE_E", - "SCANCODE_F", - "SCANCODE_G", - "SCANCODE_H", - "SCANCODE_I", - "SCANCODE_J", - "SCANCODE_K", - "SCANCODE_L", - "SCANCODE_M", - "SCANCODE_N", - "SCANCODE_O", - "SCANCODE_P", - "SCANCODE_Q", - "SCANCODE_R", - "SCANCODE_S", - "SCANCODE_T", - "SCANCODE_U", - "SCANCODE_V", - "SCANCODE_W", - "SCANCODE_X", - "SCANCODE_Y", - "SCANCODE_Z", - "SCANCODE_1", - "SCANCODE_2", - "SCANCODE_3", - "SCANCODE_4", - "SCANCODE_5", - "SCANCODE_6", - "SCANCODE_7", - "SCANCODE_8", - "SCANCODE_9", - "SCANCODE_0", - "SCANCODE_RETURN", - "SCANCODE_ESCAPE", - "SCANCODE_BACKSPACE", - "SCANCODE_TAB", - "SCANCODE_SPACE", - "SCANCODE_MINUS", - "SCANCODE_EQUALS", - "SCANCODE_LEFTBRACKET", - "SCANCODE_RIGHTBRACKET", - "SCANCODE_BACKSLASH", - "SCANCODE_NONUSHASH", - "SCANCODE_SEMICOLON", - "SCANCODE_APOSTROPHE", - "SCANCODE_GRAVE", - "SCANCODE_COMMA", - "SCANCODE_PERIOD", - "SCANCODE_SLASH", - "SCANCODE_CAPSLOCK", - "SCANCODE_F1", - "SCANCODE_F2", - "SCANCODE_F3", - "SCANCODE_F4", - "SCANCODE_F5", - "SCANCODE_F6", - "SCANCODE_F7", - "SCANCODE_F8", - "SCANCODE_F9", - "SCANCODE_F10", - "SCANCODE_F11", - "SCANCODE_F12", - "SCANCODE_PRINTSCREEN", - "SCANCODE_SCROLLLOCK", - "SCANCODE_PAUSE", - "SCANCODE_INSERT", - "SCANCODE_HOME", - "SCANCODE_PAGEUP", - "SCANCODE_DELETE", - "SCANCODE_END", - "SCANCODE_PAGEDOWN", - "SCANCODE_RIGHT", - "SCANCODE_LEFT", - "SCANCODE_DOWN", - "SCANCODE_UP", - "SCANCODE_NUMLOCKCLEAR", - "SCANCODE_KP_DIVIDE", - "SCANCODE_KP_MULTIPLY", - "SCANCODE_KP_MINUS", - "SCANCODE_KP_PLUS", - "SCANCODE_KP_ENTER", - "SCANCODE_KP_1", - "SCANCODE_KP_2", - "SCANCODE_KP_3", - "SCANCODE_KP_4", - "SCANCODE_KP_5", - "SCANCODE_KP_6", - "SCANCODE_KP_7", - "SCANCODE_KP_8", - "SCANCODE_KP_9", - "SCANCODE_KP_0", - "SCANCODE_KP_PERIOD", - "SCANCODE_NONUSBACKSLASH", - "SCANCODE_APPLICATION", - "SCANCODE_POWER", - "SCANCODE_KP_EQUALS", - "SCANCODE_F13", - "SCANCODE_F14", - "SCANCODE_F15", - "SCANCODE_F16", - "SCANCODE_F17", - "SCANCODE_F18", - "SCANCODE_F19", - "SCANCODE_F20", - "SCANCODE_F21", - "SCANCODE_F22", - "SCANCODE_F23", - "SCANCODE_F24", - "SCANCODE_EXECUTE", - "SCANCODE_HELP", - "SCANCODE_MENU", - "SCANCODE_SELECT", - "SCANCODE_STOP", - "SCANCODE_AGAIN", - "SCANCODE_UNDO", - "SCANCODE_CUT", - "SCANCODE_COPY", - "SCANCODE_PASTE", - "SCANCODE_FIND", - "SCANCODE_MUTE", - "SCANCODE_VOLUMEUP", - "SCANCODE_VOLUMEDOWN", - "SCANCODE_KP_COMMA", - "SCANCODE_KP_EQUALSAS400", - "SCANCODE_INTERNATIONAL1", - "SCANCODE_INTERNATIONAL2", - "SCANCODE_INTERNATIONAL3", - "SCANCODE_INTERNATIONAL4", - "SCANCODE_INTERNATIONAL5", - "SCANCODE_INTERNATIONAL6", - "SCANCODE_INTERNATIONAL7", - "SCANCODE_INTERNATIONAL8", - "SCANCODE_INTERNATIONAL9", - "SCANCODE_LANG1", - "SCANCODE_LANG2", - "SCANCODE_LANG3", - "SCANCODE_LANG4", - "SCANCODE_LANG5", - "SCANCODE_LANG6", - "SCANCODE_LANG7", - "SCANCODE_LANG8", - "SCANCODE_LANG9", - "SCANCODE_ALTERASE", - "SCANCODE_SYSREQ", - "SCANCODE_CANCEL", - "SCANCODE_CLEAR", - "SCANCODE_PRIOR", - "SCANCODE_RETURN2", - "SCANCODE_SEPARATOR", - "SCANCODE_OUT", - "SCANCODE_OPER", - "SCANCODE_CLEARAGAIN", - "SCANCODE_CRSEL", - "SCANCODE_EXSEL", - "SCANCODE_KP_00", - "SCANCODE_KP_000", - "SCANCODE_THOUSANDSSEPARATOR", - "SCANCODE_DECIMALSEPARATOR", - "SCANCODE_CURRENCYUNIT", - "SCANCODE_CURRENCYSUBUNIT", - "SCANCODE_KP_LEFTPAREN", - "SCANCODE_KP_RIGHTPAREN", - "SCANCODE_KP_LEFTBRACE", - "SCANCODE_KP_RIGHTBRACE", - "SCANCODE_KP_TAB", - "SCANCODE_KP_BACKSPACE", - "SCANCODE_KP_A", - "SCANCODE_KP_B", - "SCANCODE_KP_C", - "SCANCODE_KP_D", - "SCANCODE_KP_E", - "SCANCODE_KP_F", - "SCANCODE_KP_XOR", - "SCANCODE_KP_POWER", - "SCANCODE_KP_PERCENT", - "SCANCODE_KP_LESS", - "SCANCODE_KP_GREATER", - "SCANCODE_KP_AMPERSAND", - "SCANCODE_KP_DBLAMPERSAND", - "SCANCODE_KP_VERTICALBAR", - "SCANCODE_KP_DBLVERTICALBAR", - "SCANCODE_KP_COLON", - "SCANCODE_KP_HASH", - "SCANCODE_KP_SPACE", - "SCANCODE_KP_AT", - "SCANCODE_KP_EXCLAM", - "SCANCODE_KP_MEMSTORE", - "SCANCODE_KP_MEMRECALL", - "SCANCODE_KP_MEMCLEAR", - "SCANCODE_KP_MEMADD", - "SCANCODE_KP_MEMSUBTRACT", - "SCANCODE_KP_MEMMULTIPLY", - "SCANCODE_KP_MEMDIVIDE", - "SCANCODE_KP_PLUSMINUS", - "SCANCODE_KP_CLEAR", - "SCANCODE_KP_CLEARENTRY", - "SCANCODE_KP_BINARY", - "SCANCODE_KP_OCTAL", - "SCANCODE_KP_DECIMAL", - "SCANCODE_KP_HEXADECIMAL", - "SCANCODE_LCTRL", - "SCANCODE_LSHIFT", - "SCANCODE_LALT", - "SCANCODE_LGUI", - "SCANCODE_RCTRL", - "SCANCODE_RSHIFT", - "SCANCODE_RALT", - "SCANCODE_RGUI", - "SCANCODE_MODE", - "SCANCODE_AUDIONEXT", - "SCANCODE_AUDIOPREV", - "SCANCODE_AUDIOSTOP", - "SCANCODE_AUDIOPLAY", - "SCANCODE_AUDIOMUTE", - "SCANCODE_MEDIASELECT", - "SCANCODE_WWW", - "SCANCODE_MAIL", - "SCANCODE_CALCULATOR", - "SCANCODE_COMPUTER", - "SCANCODE_AC_SEARCH", - "SCANCODE_AC_HOME", - "SCANCODE_AC_BACK", - "SCANCODE_AC_FORWARD", - "SCANCODE_AC_STOP", - "SCANCODE_AC_REFRESH", - "SCANCODE_AC_BOOKMARKS", - "SCANCODE_BRIGHTNESSDOWN", - "SCANCODE_BRIGHTNESSUP", - "SCANCODE_DISPLAYSWITCH", - "SCANCODE_KBDILLUMTOGGLE", - "SCANCODE_KBDILLUMDOWN", - "SCANCODE_KBDILLUMUP", - "SCANCODE_EJECT", - "SCANCODE_SLEEP", - "SCANCODE_APP1", - "SCANCODE_APP2", - "SCANCODE_AUDIOREWIND", - "SCANCODE_AUDIOFASTFORWARD", - "K_UNKNOWN", - "K_BACKSPACE", - "K_TAB", - "K_RETURN", - "K_ESCAPE", - "K_SPACE", - "K_EXCLAIM", - "K_QUOTEDBL", - "K_HASH", - "K_DOLLAR", - "K_PERCENT", - "K_AMPERSAND", - "K_QUOTE", - "K_LEFTPAREN", - "K_RIGHTPAREN", - "K_ASTERISK", - "K_PLUS", - "K_COMMA", - "K_MINUS", - "K_PERIOD", - "K_SLASH", - "K_0", - "K_1", - "K_2", - "K_3", - "K_4", - "K_5", - "K_6", - "K_7", - "K_8", - "K_9", - "K_COLON", - "K_SEMICOLON", - "K_LESS", - "K_EQUALS", - "K_GREATER", - "K_QUESTION", - "K_AT", - "K_LEFTBRACKET", - "K_BACKSLASH", - "K_RIGHTBRACKET", - "K_CARET", - "K_UNDERSCORE", - "K_BACKQUOTE", - "K_a", - "K_b", - "K_c", - "K_d", - "K_e", - "K_f", - "K_g", - "K_h", - "K_i", - "K_j", - "K_k", - "K_l", - "K_m", - "K_n", - "K_o", - "K_p", - "K_q", - "K_r", - "K_s", - "K_t", - "K_u", - "K_v", - "K_w", - "K_x", - "K_y", - "K_z", - "K_DELETE", - "K_SCANCODE_MASK", - "K_CAPSLOCK", - "K_F1", - "K_F2", - "K_F3", - "K_F4", - "K_F5", - "K_F6", - "K_F7", - "K_F8", - "K_F9", - "K_F10", - "K_F11", - "K_F12", - "K_PRINTSCREEN", - "K_SCROLLLOCK", - "K_PAUSE", - "K_INSERT", - "K_HOME", - "K_PAGEUP", - "K_END", - "K_PAGEDOWN", - "K_RIGHT", - "K_LEFT", - "K_DOWN", - "K_UP", - "K_NUMLOCKCLEAR", - "K_KP_DIVIDE", - "K_KP_MULTIPLY", - "K_KP_MINUS", - "K_KP_PLUS", - "K_KP_ENTER", - "K_KP_1", - "K_KP_2", - "K_KP_3", - "K_KP_4", - "K_KP_5", - "K_KP_6", - "K_KP_7", - "K_KP_8", - "K_KP_9", - "K_KP_0", - "K_KP_PERIOD", - "K_APPLICATION", - "K_POWER", - "K_KP_EQUALS", - "K_F13", - "K_F14", - "K_F15", - "K_F16", - "K_F17", - "K_F18", - "K_F19", - "K_F20", - "K_F21", - "K_F22", - "K_F23", - "K_F24", - "K_EXECUTE", - "K_HELP", - "K_MENU", - "K_SELECT", - "K_STOP", - "K_AGAIN", - "K_UNDO", - "K_CUT", - "K_COPY", - "K_PASTE", - "K_FIND", - "K_MUTE", - "K_VOLUMEUP", - "K_VOLUMEDOWN", - "K_KP_COMMA", - "K_KP_EQUALSAS400", - "K_ALTERASE", - "K_SYSREQ", - "K_CANCEL", - "K_CLEAR", - "K_PRIOR", - "K_RETURN2", - "K_SEPARATOR", - "K_OUT", - "K_OPER", - "K_CLEARAGAIN", - "K_CRSEL", - "K_EXSEL", - "K_KP_00", - "K_KP_000", - "K_THOUSANDSSEPARATOR", - "K_DECIMALSEPARATOR", - "K_CURRENCYUNIT", - "K_CURRENCYSUBUNIT", - "K_KP_LEFTPAREN", - "K_KP_RIGHTPAREN", - "K_KP_LEFTBRACE", - "K_KP_RIGHTBRACE", - "K_KP_TAB", - "K_KP_BACKSPACE", - "K_KP_A", - "K_KP_B", - "K_KP_C", - "K_KP_D", - "K_KP_E", - "K_KP_F", - "K_KP_XOR", - "K_KP_POWER", - "K_KP_PERCENT", - "K_KP_LESS", - "K_KP_GREATER", - "K_KP_AMPERSAND", - "K_KP_DBLAMPERSAND", - "K_KP_VERTICALBAR", - "K_KP_DBLVERTICALBAR", - "K_KP_COLON", - "K_KP_HASH", - "K_KP_SPACE", - "K_KP_AT", - "K_KP_EXCLAM", - "K_KP_MEMSTORE", - "K_KP_MEMRECALL", - "K_KP_MEMCLEAR", - "K_KP_MEMADD", - "K_KP_MEMSUBTRACT", - "K_KP_MEMMULTIPLY", - "K_KP_MEMDIVIDE", - "K_KP_PLUSMINUS", - "K_KP_CLEAR", - "K_KP_CLEARENTRY", - "K_KP_BINARY", - "K_KP_OCTAL", - "K_KP_DECIMAL", - "K_KP_HEXADECIMAL", - "K_LCTRL", - "K_LSHIFT", - "K_LALT", - "K_LGUI", - "K_RCTRL", - "K_RSHIFT", - "K_RALT", - "K_RGUI", - "K_MODE", - "K_AUDIONEXT", - "K_AUDIOPREV", - "K_AUDIOSTOP", - "K_AUDIOPLAY", - "K_AUDIOMUTE", - "K_MEDIASELECT", - "K_WWW", - "K_MAIL", - "K_CALCULATOR", - "K_COMPUTER", - "K_AC_SEARCH", - "K_AC_HOME", - "K_AC_BACK", - "K_AC_FORWARD", - "K_AC_STOP", - "K_AC_REFRESH", - "K_AC_BOOKMARKS", - "K_BRIGHTNESSDOWN", - "K_BRIGHTNESSUP", - "K_DISPLAYSWITCH", - "K_KBDILLUMTOGGLE", - "K_KBDILLUMDOWN", - "K_KBDILLUMUP", - "K_EJECT", - "K_SLEEP", - "K_APP1", - "K_APP2", - "K_AUDIOREWIND", - "K_AUDIOFASTFORWARD", "KMOD_NONE", "KMOD_LSHIFT", "KMOD_RSHIFT", diff --git a/tests/test_deprecated.py b/tests/test_deprecated.py new file mode 100644 index 00000000..8aa15303 --- /dev/null +++ b/tests/test_deprecated.py @@ -0,0 +1,23 @@ +"""Test deprecated features.""" +from __future__ import annotations + +import pytest + +import tcod +import tcod.event + +# ruff: noqa: D103 + + +@pytest.mark.filterwarnings("error") +def test_deprecate_color() -> None: + with pytest.raises(FutureWarning, match=r".*\(0, 0, 0\)"): + _ = tcod.black + + +@pytest.mark.filterwarnings("error") +def test_deprecate_key_constants() -> None: + with pytest.raises(FutureWarning, match=r".*KeySym.N1"): + _ = tcod.event.K_1 + with pytest.raises(FutureWarning, match=r".*Scancode.N1"): + _ = tcod.event.SCANCODE_1 From 1a59d17e44a8f58bb5704ff74cf167fc7676647f Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 22 May 2023 00:58:32 -0700 Subject: [PATCH 140/194] Add commits since last release badge. Mostly as a self-reminder for me to make a release. --- README.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 1f69e003..f84370ee 100755 --- a/README.rst +++ b/README.rst @@ -6,7 +6,7 @@ ======== |VersionsBadge| |ImplementationBadge| |LicenseBadge| -|PyPI| |RTD| |Codecov| |Pyup| +|PyPI| |RTD| |Codecov| |Pyup| |CommitsSinceLastRelease| ======= About @@ -100,3 +100,6 @@ python-tcod is distributed under the `Simplified 2-clause FreeBSD license .. |Pyup| image:: https://pyup.io/repos/github/libtcod/python-tcod/shield.svg :target: https://pyup.io/repos/github/libtcod/python-tcod/ :alt: Updates + +.. |CommitsSinceLastRelease| image:: https://img.shields.io/github/commits-since/libtcod/python-tcod/latest + :target: https://github.com/libtcod/python-tcod/blob/main/CHANGELOG.md From fe86ca6b7533d03cd846d8ae19cbe51e1a43119c Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 22 May 2023 00:52:08 -0700 Subject: [PATCH 141/194] Update MacOS runner to macos-11. The older runner is no longer available. --- .github/workflows/python-package.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index bac48070..4652840a 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -253,7 +253,7 @@ jobs: build-macos: needs: [black, isort, flake8, mypy] - runs-on: "macos-10.15" + runs-on: "macos-11" strategy: fail-fast: true matrix: @@ -274,7 +274,7 @@ jobs: # Downloads SDL2 for the later step. run: python3 setup.py check - name: Build wheels - uses: pypa/cibuildwheel@v2.0.0a4 + uses: pypa/cibuildwheel@v2.12.3 env: CIBW_BUILD: ${{ matrix.python }} CIBW_ARCHS_MACOS: x86_64 arm64 universal2 From f045dafe01a85af848544416a38f30e658c77a10 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 22 May 2023 01:18:34 -0700 Subject: [PATCH 142/194] Remove old Mypy config. --- setup.cfg | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/setup.cfg b/setup.cfg index 0e25efb1..75665f21 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,25 +7,3 @@ test=pytest [flake8] ignore = E203 W503 TYP001 max-line-length = 130 - -[mypy] -python_version = 3.8 -warn_unused_configs = True -show_error_codes = True -disallow_subclassing_any = True -disallow_any_generics = True -disallow_untyped_calls = True -disallow_untyped_defs = True -disallow_incomplete_defs = True -check_untyped_defs = True -disallow_untyped_decorators = True -no_implicit_optional = True -warn_redundant_casts = True -warn_unused_ignores = True -warn_return_any = True -implicit_reexport = False -strict_equality = True -exclude = (build/|venv/|libtcod/|docs/|distribution/|termbox/|samples_libtcodpy.py) - -[mypy-numpy] -ignore_missing_imports = True From f21b7501a1dee23affe29c94137689c3b577b524 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 22 May 2023 04:51:55 -0700 Subject: [PATCH 143/194] Update workflows to not invoke setup.py as much. Use latest OS versions. --- .github/workflows/python-package.yml | 35 ++++++++++++++++------------ .vscode/settings.json | 5 +++- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 4652840a..d63b1173 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -17,7 +17,7 @@ env: jobs: black: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Install Black @@ -26,7 +26,7 @@ jobs: run: black --check --diff examples/ scripts/ tcod/ tests/ *.py isort: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Install isort @@ -43,7 +43,7 @@ jobs: run: isort examples/ --check --diff --thirdparty tcod flake8: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Install Flake8 @@ -55,7 +55,7 @@ jobs: run: flake8 scripts/ tcod/ tests/ mypy: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Checkout submodules @@ -76,7 +76,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: ["windows-2019", "macos-11"] + os: ["windows-latest", "macos-latest"] sdl-version: ["2.0.14", "2.0.16"] fail-fast: true steps: @@ -85,8 +85,13 @@ jobs: fetch-depth: ${{ env.git-depth }} - name: Checkout submodules run: git submodule update --init --recursive --depth 1 + - uses: actions/setup-python@v4 + with: + python-version: "3.x" + - name: Install build dependencies + run: pip install build - name: Build package - run: ./setup.py build + run: python -m build env: SDL_VERSION: ${{ matrix.sdl-version }} @@ -95,14 +100,14 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: ["ubuntu-20.04", "windows-2019"] + os: ["ubuntu-latest", "windows-latest"] python-version: ["3.7", "3.8", "3.9", "pypy-3.7"] architecture: ["x64"] include: - - os: "windows-2019" + - os: "windows-latest" python-version: "3.7" architecture: "x86" - - os: "windows-2019" + - os: "windows-latest" python-version: "pypy-3.7" architecture: "x86" fail-fast: false @@ -127,14 +132,14 @@ jobs: - name: Install Python dependencies run: | python -m pip install --upgrade pip - pip install pytest pytest-cov pytest-benchmark wheel twine + pip install pytest pytest-cov pytest-benchmark build twine if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Initialize package run: | - python setup.py check # Creates tcod/version.py. + pip install -e . # Install the package in-place. - name: Build package. run: | - python setup.py build sdist develop bdist_wheel --py-limited-api cp36 # Install the package in-place. + python -m build - name: Test with pytest if: runner.os == 'Windows' run: | @@ -172,7 +177,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: ["ubuntu-20.04", "windows-2019"] + os: ["ubuntu-latest", "windows-latest"] steps: - name: Set up Python uses: actions/setup-python@v4 @@ -199,7 +204,7 @@ jobs: linux-wheels: needs: build # These take a while to build/test, so wait for normal tests to pass first. - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-latest" strategy: matrix: arch: ["x86_64", "aarch64"] @@ -272,7 +277,7 @@ jobs: run: pip3 install wheel twine -r requirements.txt - name: Prepare package # Downloads SDL2 for the later step. - run: python3 setup.py check + run: python3 setup.py || true - name: Build wheels uses: pypa/cibuildwheel@v2.12.3 env: diff --git a/.vscode/settings.json b/.vscode/settings.json index c2a007d0..25a75f16 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -428,5 +428,8 @@ "python.testing.pytestEnabled": true, "[python]": { "editor.defaultFormatter": "ms-python.black-formatter" - } + }, + "cSpell.enableFiletypes": [ + "github-actions-workflow" + ] } \ No newline at end of file From 5fd5b9e19ba44498970491f30d2cc57c44dae5d0 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 22 May 2023 07:19:08 -0700 Subject: [PATCH 144/194] Organize workflow for better performance. Skip long running wheel building unless on a tag release. Improve build speed of SDL parse tests, and only do those when linters pass. --- .github/workflows/python-package.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index d63b1173..d8ccdfc2 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -73,6 +73,7 @@ jobs: # This makes sure that the latest versions of the SDL headers parse correctly. parse_sdl: + needs: [black, isort, flake8, mypy] runs-on: ${{ matrix.os }} strategy: matrix: @@ -94,6 +95,7 @@ jobs: run: python -m build env: SDL_VERSION: ${{ matrix.sdl-version }} + TDL_BUILD: DEBUG build: needs: [black, isort, flake8, mypy] @@ -204,6 +206,7 @@ jobs: linux-wheels: needs: build # These take a while to build/test, so wait for normal tests to pass first. + if: startsWith(github.event.ref, 'refs/tags/') runs-on: "ubuntu-latest" strategy: matrix: From 498c50aa9c099ef2e665fbdbf50f1f75d77c222c Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 24 May 2023 23:14:10 -0700 Subject: [PATCH 145/194] Add pre-commit hooks. --- .pre-commit-config.yaml | 26 ++++++++++++++++++++++++++ CONTRIBUTING.md | 10 ++++++---- 2 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..54a9a88e --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,26 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: check-shebang-scripts-are-executable + - id: check-merge-conflict + - id: check-toml + - id: debug-statements + - id: fix-byte-order-marker + - id: detect-private-key + - repo: https://github.com/psf/black + rev: 23.3.0 + hooks: + - id: black + - repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort +default_language_version: + python: python3.11 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 58da55b3..c4f7d5ea 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,10 +1,12 @@ ## Code style -New and refactored Python code should follow the -[PEP 8](https://www.python.org/dev/peps/pep-0008/) guidelines. +Code styles are enforced using black and linters. +These are best enabled with a pre-commit which you can setup with: -It's recommended to use an editor supporting -[EditorConfig](https://editorconfig.org/). +```sh +pip install pre-commit +pre-commit install +``` ## Building python-tcod From 5a1d70cab1bf010cf78fb52d153505b7baf7898a Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 24 May 2023 23:22:04 -0700 Subject: [PATCH 146/194] Clean up minor formatting issues. Don't try to force import order in examples to look like external projects. Remove shebangs from test scripts. Add +x to examples and scripts. Fix all whitespace and final newlines. --- .editorconfig | 1 - .github/workflows/python-package.yml | 7 +------ .pyup.yml | 2 +- .vscode/extensions.json | 2 +- .vscode/launch.json | 2 +- .vscode/settings.json | 5 ++++- .vscode/tasks.json | 2 +- build_libtcod.py | 0 build_sdl.py | 0 docs/index.rst | 1 - examples/.isort.cfg | 5 ----- examples/cavegen.py | 0 examples/framerate.py | 0 examples/samples_libtcodpy.py | 1 + examples/samples_tcod.py | 3 ++- examples/sdl-hello-world.py | 3 ++- examples/ttf.py | 3 ++- fonts/libtcod/README.txt | 2 +- scripts/tag_release.py | 0 tests/test_libtcodpy.py | 2 -- tests/test_parser.py | 2 -- tests/test_tcod.py | 2 -- tests/test_testing.py | 2 -- 23 files changed, 17 insertions(+), 30 deletions(-) mode change 100644 => 100755 build_libtcod.py mode change 100644 => 100755 build_sdl.py delete mode 100644 examples/.isort.cfg mode change 100644 => 100755 examples/cavegen.py mode change 100644 => 100755 examples/framerate.py mode change 100644 => 100755 examples/ttf.py mode change 100644 => 100755 scripts/tag_release.py diff --git a/.editorconfig b/.editorconfig index 2cd81a2a..bd4f19f2 100644 --- a/.editorconfig +++ b/.editorconfig @@ -26,4 +26,3 @@ indent_size = 2 [*.json] indent_style = space indent_size = 4 -insert_final_newline = false diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index d8ccdfc2..0ec9701e 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -35,12 +35,7 @@ jobs: uses: liskin/gh-problem-matcher-wrap@v2 with: linters: isort - run: isort scripts/ tcod/ tests/ --check --diff - - name: isort (examples) - uses: liskin/gh-problem-matcher-wrap@v2 - with: - linters: isort - run: isort examples/ --check --diff --thirdparty tcod + run: isort scripts/ tcod/ tests/ examples/ --check --diff flake8: runs-on: ubuntu-latest diff --git a/.pyup.yml b/.pyup.yml index bf632f0b..fa58aa6e 100644 --- a/.pyup.yml +++ b/.pyup.yml @@ -1,4 +1,4 @@ -# autogenerated pyup.io config file +# autogenerated pyup.io config file # see https://pyup.io/docs/configuration/ for all available options update: false diff --git a/.vscode/extensions.json b/.vscode/extensions.json index e9737cb3..d589600c 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -16,4 +16,4 @@ ], // List of extensions recommended by VS Code that should not be recommended for users of this workspace. "unwantedRecommendations": [] -} \ No newline at end of file +} diff --git a/.vscode/launch.json b/.vscode/launch.json index c9892886..0f237387 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -45,4 +45,4 @@ "preLaunchTask": "build documentation", } ] -} \ No newline at end of file +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 25a75f16..12dc2d77 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,6 +7,9 @@ "editor.codeActionsOnSave": { "source.organizeImports": true }, + "files.trimFinalNewlines": true, + "files.insertFinalNewline": true, + "files.trimTrailingWhitespace": true, "python.linting.enabled": true, "python.linting.flake8Enabled": true, "python.linting.mypyEnabled": true, @@ -432,4 +435,4 @@ "cSpell.enableFiletypes": [ "github-actions-workflow" ] -} \ No newline at end of file +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ba6b05d3..5e1a0fb2 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -41,4 +41,4 @@ } } ] -} \ No newline at end of file +} diff --git a/build_libtcod.py b/build_libtcod.py old mode 100644 new mode 100755 diff --git a/build_sdl.py b/build_sdl.py old mode 100644 new mode 100755 diff --git a/docs/index.rst b/docs/index.rst index 7a138378..c9760f9f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -51,4 +51,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` - diff --git a/examples/.isort.cfg b/examples/.isort.cfg deleted file mode 100644 index 9e0911c7..00000000 --- a/examples/.isort.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[isort] -profile= black -py_version = 36 -skip_gitignore = true -line_length = 120 diff --git a/examples/cavegen.py b/examples/cavegen.py old mode 100644 new mode 100755 diff --git a/examples/framerate.py b/examples/framerate.py old mode 100644 new mode 100755 diff --git a/examples/samples_libtcodpy.py b/examples/samples_libtcodpy.py index c59b5b46..ab68eb91 100755 --- a/examples/samples_libtcodpy.py +++ b/examples/samples_libtcodpy.py @@ -4,6 +4,7 @@ # This code demonstrates various usages of libtcod modules # It's in the public domain. # +# ruff: noqa from __future__ import division import math diff --git a/examples/samples_tcod.py b/examples/samples_tcod.py index bf20be3b..bf0f7101 100755 --- a/examples/samples_tcod.py +++ b/examples/samples_tcod.py @@ -15,10 +15,11 @@ from typing import Any, List import numpy as np +from numpy.typing import NDArray + import tcod import tcod.render import tcod.sdl.render -from numpy.typing import NDArray if not sys.warnoptions: warnings.simplefilter("default") # Show all warnings. diff --git a/examples/sdl-hello-world.py b/examples/sdl-hello-world.py index e3d45245..d0ae93c0 100644 --- a/examples/sdl-hello-world.py +++ b/examples/sdl-hello-world.py @@ -2,10 +2,11 @@ from pathlib import Path import numpy as np +from PIL import Image, ImageDraw, ImageFont # type: ignore # pip install Pillow + import tcod import tcod.sdl.render import tcod.sdl.video -from PIL import Image, ImageDraw, ImageFont # type: ignore # pip install Pillow CURRENT_DIR = Path(__file__).parent # Directory of this script. font = ImageFont.truetype(bytes(CURRENT_DIR / "DejaVuSerif.ttf"), size=18) # Preloaded font file. diff --git a/examples/ttf.py b/examples/ttf.py old mode 100644 new mode 100755 index 249c771a..7268109f --- a/examples/ttf.py +++ b/examples/ttf.py @@ -12,9 +12,10 @@ import freetype # type: ignore # pip install freetype-py import numpy as np -import tcod from numpy.typing import NDArray +import tcod + FONT = "VeraMono.ttf" diff --git a/fonts/libtcod/README.txt b/fonts/libtcod/README.txt index c9726fbc..772f3efc 100755 --- a/fonts/libtcod/README.txt +++ b/fonts/libtcod/README.txt @@ -1,5 +1,5 @@ This directory contains antialiased fonts for libtcod. -These fonts are in public domain. +These fonts are in public domain. The file names are composed with : __.png diff --git a/scripts/tag_release.py b/scripts/tag_release.py old mode 100644 new mode 100755 diff --git a/tests/test_libtcodpy.py b/tests/test_libtcodpy.py index bf12f9a1..33cba999 100644 --- a/tests/test_libtcodpy.py +++ b/tests/test_libtcodpy.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python - from typing import Any, Callable, Iterator, List, Optional, Tuple, Union import numpy diff --git a/tests/test_parser.py b/tests/test_parser.py index 2e0ef5ed..9a1dad7f 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python - import os from typing import Any diff --git a/tests/test_tcod.py b/tests/test_tcod.py index dbe2e89c..d3216e02 100644 --- a/tests/test_tcod.py +++ b/tests/test_tcod.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python - import copy import pickle from typing import Any, NoReturn diff --git a/tests/test_testing.py b/tests/test_testing.py index 027da622..92604ae2 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python - import os curdir = os.path.dirname(__file__) From 1aaaf7c486f850e03a8ef2ef3f6456d9a86fdf63 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 24 May 2023 23:45:55 -0700 Subject: [PATCH 147/194] Ensure deprecated colors still work. --- libtcodpy.py | 6 ++++-- pyproject.toml | 1 + tcod/__init__.py | 4 ++-- tcod/libtcodpy.py | 9 +++++++++ tests/test_deprecated.py | 6 ++++++ 5 files changed, 22 insertions(+), 4 deletions(-) diff --git a/libtcodpy.py b/libtcodpy.py index bb1a8cc8..63e2e0b0 100644 --- a/libtcodpy.py +++ b/libtcodpy.py @@ -1,9 +1,11 @@ -"""This module just an alias for tcod""" +"""Module alias for tcod.""" import warnings +from tcod import * # noqa: F4 +from tcod.libtcodpy import __getattr__ # noqa: F401 + warnings.warn( "'import tcod as libtcodpy' is preferred.", DeprecationWarning, stacklevel=2, ) -from tcod import * # noqa: F4 diff --git a/pyproject.toml b/pyproject.toml index d4baab4e..06c30b87 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,6 +99,7 @@ filterwarnings = [ "ignore::DeprecationWarning:tcod.libtcodpy", "ignore::PendingDeprecationWarning:tcod.libtcodpy", "ignore:This class may perform poorly and is no longer needed.::tcod.map", + "ignore:'import tcod as libtcodpy' is preferred.", ] [tool.mypy] diff --git a/tcod/__init__.py b/tcod/__init__.py index 4a4b5173..b14581dd 100644 --- a/tcod/__init__.py +++ b/tcod/__init__.py @@ -25,7 +25,7 @@ __version__ = "" -def __getattr__(name: str) -> color.Color: +def __getattr__(name: str, stacklevel: int = 1) -> color.Color: """Mark access to color constants as deprecated.""" value: color.Color | None = getattr(constants, name, None) if value is None: @@ -34,7 +34,7 @@ def __getattr__(name: str) -> color.Color: warnings.warn( f"Color constants will be removed from future releases.\nReplace `tcod.{name}` with `{tuple(value)}`.", FutureWarning, - stacklevel=2, + stacklevel=stacklevel + 1, ) return value diff --git a/tcod/libtcodpy.py b/tcod/libtcodpy.py index 51974108..a14403c3 100644 --- a/tcod/libtcodpy.py +++ b/tcod/libtcodpy.py @@ -4201,6 +4201,15 @@ def _atexit_verify() -> None: lib.TCOD_console_delete(ffi.NULL) +def __getattr__(name: str) -> Color: + """Mark access to color constants as deprecated.""" + try: + return tcod.__getattr__(name, stacklevel=2) # type: ignore[call-arg] + except AttributeError: + msg = f"module {__name__!r} has no attribute {name!r}" + raise AttributeError(msg) from None + + __all__ = [ # noqa: F405 "Color", "Bsp", diff --git a/tests/test_deprecated.py b/tests/test_deprecated.py index 8aa15303..425cafb1 100644 --- a/tests/test_deprecated.py +++ b/tests/test_deprecated.py @@ -3,8 +3,10 @@ import pytest +import libtcodpy import tcod import tcod.event +import tcod.libtcodpy # ruff: noqa: D103 @@ -13,6 +15,10 @@ def test_deprecate_color() -> None: with pytest.raises(FutureWarning, match=r".*\(0, 0, 0\)"): _ = tcod.black + with pytest.raises(FutureWarning, match=r".*\(0, 0, 0\)"): + _ = tcod.libtcodpy.black + with pytest.raises(FutureWarning, match=r".*\(0, 0, 0\)"): + _ = libtcodpy.black @pytest.mark.filterwarnings("error") From 00ebb839fff1e6fceb0200be65f63d609fc1b223 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 25 May 2023 00:18:56 -0700 Subject: [PATCH 148/194] Set Sphinx version to not break the RTD theme. --- docs/requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/requirements.txt b/docs/requirements.txt index 5f954b04..f25589d5 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,2 @@ -sphinx>=5.0.2,!=5.1.0 -sphinx_rtd_theme +sphinx>=5.0.2,!=5.1.0,<6.1 # https://github.com/readthedocs/sphinx_rtd_theme/issues/1463 +sphinx_rtd_theme>=1.2.1 From 2749f2ef454d132645bd1c211676160f825cd9c1 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 25 May 2023 00:31:15 -0700 Subject: [PATCH 149/194] Update ReadTheDocs config file. --- .readthedocs.yaml | 31 +++++++++++++++++++++++++++++++ .readthedocs.yml | 24 ------------------------ 2 files changed, 31 insertions(+), 24 deletions(-) create mode 100644 .readthedocs.yaml delete mode 100644 .readthedocs.yml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..390d6115 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,31 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +version: 2 + +build: + os: ubuntu-22.04 + tools: + python: "3.11" + apt_packages: + - libsdl2-dev + +submodules: + include: all + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/conf.py + fail_on_warning: true + +# If using Sphinx, optionally build your docs in additional formats such as PDF +# formats: +# - pdf + +# Optionally declare the Python requirements required to build your docs +python: + install: + - requirements: docs/requirements.txt + - method: pip + path: . diff --git a/.readthedocs.yml b/.readthedocs.yml deleted file mode 100644 index 2521036c..00000000 --- a/.readthedocs.yml +++ /dev/null @@ -1,24 +0,0 @@ -# .readthedocs.yml -# Read the Docs configuration file -# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details - -version: 2 - -build: - image: latest - -sphinx: - builder: html - configuration: docs/conf.py - fail_on_warning: true - -formats: - - htmlzip - - pdf - - epub - -python: - version: "3.8" - install: - - requirements: requirements.txt - - requirements: docs/requirements.txt From be8dfe67e7223c4d369792adaaa3261d264ff432 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 25 May 2023 01:29:24 -0700 Subject: [PATCH 150/194] Setup trusted publishing for testing. The old deployment is kept in case anything breaks. That will be removed once this looks good. --- .github/workflows/python-package.yml | 58 ++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 0ec9701e..0dd25329 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -297,3 +297,61 @@ jobs: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: twine upload --skip-existing wheelhouse/* + + publish: + needs: [build] + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + environment: + name: release + url: https://pypi.org/p/tcod + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v3 + with: + name: sdist + path: dist/ + - uses: actions/download-artifact@v3 + with: + name: wheels-windows + path: dist/ + - uses: pypa/gh-action-pypi-publish@release/v1 + with: + skip-existing: true + + publish-macos: + needs: [build-macos] + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + environment: + name: release + url: https://pypi.org/p/tcod + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v3 + with: + name: wheels-macos + path: dist/ + - uses: pypa/gh-action-pypi-publish@release/v1 + with: + skip-existing: true + + publish-linux: + needs: [linux-wheels] + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + environment: + name: release + url: https://pypi.org/p/tcod + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v3 + with: + name: wheels-linux + path: dist/ + - uses: pypa/gh-action-pypi-publish@release/v1 + with: + skip-existing: true From dc2f84f61408fbe0d847a90ef847126c73f7237d Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 25 May 2023 01:38:52 -0700 Subject: [PATCH 151/194] Don't build package just to test SDL parsing. --- .github/workflows/python-package.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 0dd25329..71976b35 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -85,12 +85,11 @@ jobs: with: python-version: "3.x" - name: Install build dependencies - run: pip install build - - name: Build package - run: python -m build + run: pip install -r requirements.txt + - name: Test SDL parsing + run: python build_sdl.py env: SDL_VERSION: ${{ matrix.sdl-version }} - TDL_BUILD: DEBUG build: needs: [black, isort, flake8, mypy] From f2c72095b247e033820e93d23f43761ddb23539d Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 25 May 2023 01:58:24 -0700 Subject: [PATCH 152/194] Modernize SDL build script. --- build_sdl.py | 57 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/build_sdl.py b/build_sdl.py index 0bbe5241..2b7c19e7 100755 --- a/build_sdl.py +++ b/build_sdl.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +"""Build script to parse SDL headers and generate CFFI bindings.""" from __future__ import annotations import io @@ -10,12 +11,14 @@ import sys import zipfile from pathlib import Path -from typing import Any, Dict, List, Set +from typing import Any import pcpp # type: ignore import requests -BITSIZE, LINKAGE = platform.architecture() +# ruff: noqa: S603, S607 # This script calls a lot of programs. + +BIT_SIZE, LINKAGE = platform.architecture() # Reject versions of SDL older than this, update the requirements in the readme if you change this. SDL_MIN_VERSION = (2, 0, 10) @@ -80,19 +83,21 @@ def check_sdl_version() -> None: needed_version = f"{SDL_MIN_VERSION[0]}.{SDL_MIN_VERSION[1]}.{SDL_MIN_VERSION[2]}" try: sdl_version_str = subprocess.check_output(["sdl2-config", "--version"], universal_newlines=True).strip() - except FileNotFoundError: - raise RuntimeError( - "libsdl2-dev or equivalent must be installed on your system" - f" and must be at least version {needed_version}." - "\nsdl2-config must be on PATH." + except FileNotFoundError as exc: + msg = ( + "libsdl2-dev or equivalent must be installed on your system and must be at least version" + f" {needed_version}.\nsdl2-config must be on PATH." ) + raise RuntimeError(msg) from exc print(f"Found SDL {sdl_version_str}.") sdl_version = tuple(int(s) for s in sdl_version_str.split(".")) if sdl_version < SDL_MIN_VERSION: - raise RuntimeError("SDL version must be at least %s, (found %s)" % (needed_version, sdl_version_str)) + msg = f"SDL version must be at least {needed_version}, (found {sdl_version_str})" + raise RuntimeError(msg) def get_sdl2_file(version: str) -> Path: + """Return a path to an SDL2 archive for the current platform. The archive is downloaded if missing.""" if sys.platform == "win32": sdl2_file = f"SDL2-devel-{version}-VC.zip" else: @@ -102,14 +107,15 @@ def get_sdl2_file(version: str) -> Path: sdl2_remote_file = f"https://www.libsdl.org/release/{sdl2_file}" if not sdl2_local_file.exists(): print(f"Downloading {sdl2_remote_file}") - os.makedirs("dependencies/", exist_ok=True) - with requests.get(sdl2_remote_file) as response: + Path("dependencies/").mkdir(parents=True, exist_ok=True) + with requests.get(sdl2_remote_file) as response: # noqa: S113 response.raise_for_status() sdl2_local_file.write_bytes(response.content) return sdl2_local_file def unpack_sdl2(version: str) -> Path: + """Return the path to an extracted SDL distribution. Creates it if missing.""" sdl2_path = Path(f"dependencies/SDL2-{version}") if sys.platform == "darwin": sdl2_dir = sdl2_path @@ -134,10 +140,11 @@ class SDLParser(pcpp.Preprocessor): # type: ignore """A modified preprocessor to output code in a format for CFFI.""" def __init__(self) -> None: + """Initialise the object with empty values.""" super().__init__() self.line_directive = None # Don't output line directives. - self.known_string_defines: Dict[str, str] = {} - self.known_defines: Set[str] = set() + self.known_string_defines: dict[str, str] = {} + self.known_defines: set[str] = set() def get_output(self) -> str: """Return this objects current tokens as a string.""" @@ -151,7 +158,7 @@ def on_include_not_found(self, is_malformed: bool, is_system_include: bool, curd """Remove bad includes such as stddef.h and stdarg.h.""" raise pcpp.OutputDirective(pcpp.Action.IgnoreAndRemove) - def _should_track_define(self, tokens: List[Any]) -> bool: + def _should_track_define(self, tokens: list[Any]) -> bool: if len(tokens) < 3: return False if tokens[0].value in IGNORE_DEFINES: @@ -175,8 +182,9 @@ def _should_track_define(self, tokens: List[Any]) -> bool: ) def on_directive_handle( - self, directive: Any, tokens: List[Any], if_passthru: bool, preceding_tokens: List[Any] - ) -> Any: + self, directive: Any, tokens: list[Any], if_passthru: bool, preceding_tokens: list[Any] # noqa: ANN401 + ) -> Any: # noqa: ANN401 + """Catch and store definitions.""" if directive.value == "define" and self._should_track_define(tokens): if tokens[2].type == "CPP_STRING": self.known_string_defines[tokens[0].value] = tokens[2].value @@ -204,7 +212,7 @@ def on_directive_handle( assert matches for match in matches: - if os.path.isfile(Path(match, "SDL_stdinc.h")): + if Path(match, "SDL_stdinc.h").is_file(): SDL2_INCLUDE = match assert SDL2_INCLUDE @@ -224,6 +232,7 @@ def on_directive_handle( def get_cdef() -> str: + """Return the parsed code of SDL for CFFI.""" parser = SDLParser() parser.add_path(SDL2_INCLUDE) parser.parse( @@ -261,12 +270,12 @@ def get_cdef() -> str: return sdl2_cdef + EXTRA_CDEF -include_dirs: List[str] = [] -extra_compile_args: List[str] = [] -extra_link_args: List[str] = [] +include_dirs: list[str] = [] +extra_compile_args: list[str] = [] +extra_link_args: list[str] = [] -libraries: List[str] = [] -library_dirs: List[str] = [] +libraries: list[str] = [] +library_dirs: list[str] = [] if sys.platform == "darwin": @@ -278,16 +287,16 @@ def get_cdef() -> str: if sys.platform == "win32": include_dirs.append(str(SDL2_INCLUDE)) ARCH_MAPPING = {"32bit": "x86", "64bit": "x64"} - SDL2_LIB_DIR = Path(SDL2_BUNDLE_PATH, "lib/", ARCH_MAPPING[BITSIZE]) + SDL2_LIB_DIR = Path(SDL2_BUNDLE_PATH, "lib/", ARCH_MAPPING[BIT_SIZE]) library_dirs.append(str(SDL2_LIB_DIR)) - SDL2_LIB_DEST = Path("tcod", ARCH_MAPPING[BITSIZE]) + SDL2_LIB_DEST = Path("tcod", ARCH_MAPPING[BIT_SIZE]) SDL2_LIB_DEST.mkdir(exist_ok=True) shutil.copy(SDL2_LIB_DIR / "SDL2.dll", SDL2_LIB_DEST) # Link to the SDL2 framework on MacOS. # Delocate will bundle the binaries in a later step. if sys.platform == "darwin": - HEADER_DIR = os.path.join(SDL2_PARSE_PATH, "Headers") + HEADER_DIR = Path(SDL2_PARSE_PATH, "Headers") include_dirs.append(HEADER_DIR) extra_link_args += [f"-F{SDL2_BUNDLE_PATH}/.."] extra_link_args += ["-rpath", f"{SDL2_BUNDLE_PATH}/.."] From 30a7fa401b7e3257553dddc75938780b6553f1ac Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 25 May 2023 02:50:33 -0700 Subject: [PATCH 153/194] Modernize libtcod build script. --- build_libtcod.py | 87 +++++++++++++++++++++++++----------------------- 1 file changed, 45 insertions(+), 42 deletions(-) diff --git a/build_libtcod.py b/build_libtcod.py index b0491e81..98571002 100755 --- a/build_libtcod.py +++ b/build_libtcod.py @@ -1,13 +1,15 @@ #!/usr/bin/env python3 +"""Parse and compile libtcod and SDL sources for CFFI.""" from __future__ import annotations +import contextlib import glob import os import platform import re import sys from pathlib import Path -from typing import Any, Dict, Iterable, Iterator, List, Set, Tuple, Union +from typing import Any, Iterable, Iterator from cffi import FFI @@ -20,7 +22,7 @@ HEADER_PARSE_PATHS = ("tcod/", "libtcod/src/libtcod/") HEADER_PARSE_EXCLUDES = ("gl2_ext_.h", "renderer_gl_internal.h", "event.h") -BITSIZE, LINKAGE = platform.architecture() +BIT_SIZE, LINKAGE = platform.architecture() # Regular expressions to parse the headers for cffi. RE_COMMENT = re.compile(r"\s*/\*.*?\*/|\s*//*?$", re.DOTALL | re.MULTILINE) @@ -43,18 +45,18 @@ class ParsedHeader: """ # Class dictionary of all parsed headers. - all_headers: Dict[Path, ParsedHeader] = {} + all_headers: dict[Path, ParsedHeader] = {} def __init__(self, path: Path) -> None: + """Initialize and organize a header file.""" self.path = path = path.resolve(True) directory = path.parent depends = set() - with open(self.path, "r", encoding="utf-8") as f: - header = f.read() + header = self.path.read_text(encoding="utf-8") header = RE_COMMENT.sub("", header) header = RE_CPLUSPLUS.sub("", header) for dependency in RE_INCLUDE.findall(header): - depends.add((directory / dependency).resolve(True)) + depends.add((directory / str(dependency)).resolve(True)) header = RE_PREPROCESSOR.sub("", header) header = RE_TAGS.sub("", header) header = RE_VAFUNC.sub("", header) @@ -63,22 +65,22 @@ def __init__(self, path: Path) -> None: self.depends = frozenset(depends) self.all_headers[self.path] = self - def parsed_depends(self) -> Iterator["ParsedHeader"]: + def parsed_depends(self) -> Iterator[ParsedHeader]: """Return dependencies excluding ones that were not loaded.""" for dep in self.depends: - try: + with contextlib.suppress(KeyError): yield self.all_headers[dep] - except KeyError: - pass def __str__(self) -> str: - return "Parsed harder at '%s'\n Depends on: %s" % ( + """Return useful info on this object.""" + return "Parsed harder at '{}'\n Depends on: {}".format( self.path, - "\n\t".join(self.depends), + "\n\t".join(str(d) for d in self.depends), ) def __repr__(self) -> str: - return f"ParsedHeader({self.path})" + """Return the representation of this object.""" + return f"ParsedHeader({self.path!r})" def walk_includes(directory: str) -> Iterator[ParsedHeader]: @@ -93,10 +95,10 @@ def walk_includes(directory: str) -> Iterator[ParsedHeader]: def resolve_dependencies( includes: Iterable[ParsedHeader], -) -> List[ParsedHeader]: +) -> list[ParsedHeader]: """Sort headers by their correct include order.""" unresolved = set(includes) - resolved: Set[ParsedHeader] = set() + resolved: set[ParsedHeader] = set() result = [] while unresolved: for item in unresolved: @@ -104,26 +106,29 @@ def resolve_dependencies( resolved.add(item) result.append(item) if not unresolved & resolved: - raise RuntimeError( - "Could not resolve header load order.\n" - f"Possible cyclic dependency with the unresolved headers:\n{unresolved}" + msg = ( + "Could not resolve header load order." + "\nPossible cyclic dependency with the unresolved headers:" + f"\n{unresolved}" ) + raise RuntimeError(msg) unresolved -= resolved return result -def parse_includes() -> List[ParsedHeader]: +def parse_includes() -> list[ParsedHeader]: """Collect all parsed header files and return them. Reads HEADER_PARSE_PATHS and HEADER_PARSE_EXCLUDES. """ - includes: List[ParsedHeader] = [] + includes: list[ParsedHeader] = [] for dirpath in HEADER_PARSE_PATHS: includes.extend(walk_includes(dirpath)) return resolve_dependencies(includes) def walk_sources(directory: str) -> Iterator[str]: + """Iterate over the C sources of a directory recursively.""" for path, _dirs, files in os.walk(directory): for source in files: if source.endswith(".c"): @@ -133,7 +138,7 @@ def walk_sources(directory: str) -> Iterator[str]: includes = parse_includes() module_name = "tcod._libtcod" -include_dirs: List[str] = [ +include_dirs: list[str] = [ ".", "libtcod/src/vendor/", "libtcod/src/vendor/utf8proc", @@ -141,13 +146,13 @@ def walk_sources(directory: str) -> Iterator[str]: *build_sdl.include_dirs, ] -extra_compile_args: List[str] = [*build_sdl.extra_compile_args] -extra_link_args: List[str] = [*build_sdl.extra_link_args] -sources: List[str] = [] +extra_compile_args: list[str] = [*build_sdl.extra_compile_args] +extra_link_args: list[str] = [*build_sdl.extra_link_args] +sources: list[str] = [] -libraries: List[str] = [*build_sdl.libraries] -library_dirs: List[str] = [*build_sdl.library_dirs] -define_macros: List[Tuple[str, Any]] = [("Py_LIMITED_API", Py_LIMITED_API)] +libraries: list[str] = [*build_sdl.libraries] +library_dirs: list[str] = [*build_sdl.library_dirs] +define_macros: list[tuple[str, Any]] = [("Py_LIMITED_API", Py_LIMITED_API)] sources += walk_sources("tcod/") sources += walk_sources("libtcod/src/libtcod/") @@ -173,7 +178,7 @@ def walk_sources(directory: str) -> Iterator[str]: tdl_build = os.environ.get("TDL_BUILD", "RELEASE").upper() MSVC_CFLAGS = {"DEBUG": ["/Od"], "RELEASE": ["/GL", "/O2", "/GS-", "/wd4996"]} -MSVC_LDFLAGS: Dict[str, List[str]] = {"DEBUG": [], "RELEASE": ["/LTCG"]} +MSVC_LDFLAGS: dict[str, list[str]] = {"DEBUG": [], "RELEASE": ["/LTCG"]} GCC_CFLAGS = { "DEBUG": ["-std=c99", "-Og", "-g", "-fPIC"], "RELEASE": [ @@ -238,7 +243,7 @@ def walk_sources(directory: str) -> Iterator[str]: ''' -def find_sdl_attrs(prefix: str) -> Iterator[Tuple[str, Union[int, str, Any]]]: +def find_sdl_attrs(prefix: str) -> Iterator[tuple[str, int | str | Any]]: """Return names and values from `tcod.lib`. `prefix` is used to filter out which names to copy. @@ -294,24 +299,22 @@ def parse_sdl_attrs(prefix: str, all_names: list[str] | None) -> tuple[str, str] ] -def update_module_all(filename: str, new_all: str) -> None: +def update_module_all(filename: Path, new_all: str) -> None: """Update the __all__ of a file with the constants from new_all.""" RE_CONSTANTS_ALL = re.compile( r"(.*# --- From constants.py ---).*(# --- End constants.py ---.*)", re.DOTALL, ) - with open(filename, "r", encoding="utf-8") as f: - match = RE_CONSTANTS_ALL.match(f.read()) + match = RE_CONSTANTS_ALL.match(filename.read_text(encoding="utf-8")) assert match, f"Can't determine __all__ subsection in {filename}!" header, footer = match.groups() - with open(filename, "w", encoding="utf-8") as f: - f.write(f"{header}\n {new_all},\n {footer}") + filename.write_text(f"{header}\n {new_all},\n {footer}", encoding="utf-8") def generate_enums(prefix: str) -> Iterator[str]: """Generate attribute assignments suitable for a Python enum.""" - for name, value in sorted(find_sdl_attrs(prefix), key=lambda item: item[1]): - name = name.split("_", 1)[1] + for symbol, value in sorted(find_sdl_attrs(prefix), key=lambda item: item[1]): + _, name = symbol.split("_", 1) if name.isdigit(): name = f"N{name}" if name in "IOl": # Handle Flake8 warnings. @@ -325,7 +328,7 @@ def write_library_constants() -> None: import tcod.color from tcod._libtcod import ffi, lib - with open("tcod/constants.py", "w", encoding="utf-8") as f: + with Path("tcod/constants.py").open("w", encoding="utf-8") as f: all_names = [] f.write(CONSTANT_MODULE_HEADER) for name in dir(lib): @@ -363,10 +366,10 @@ def write_library_constants() -> None: all_names_merged = ",\n ".join(f'"{name}"' for name in all_names) f.write(f"\n__all__ = [\n {all_names_merged},\n]\n") - update_module_all("tcod/__init__.py", all_names_merged) - update_module_all("tcod/libtcodpy.py", all_names_merged) + update_module_all(Path("tcod/__init__.py"), all_names_merged) + update_module_all(Path("tcod/libtcodpy.py"), all_names_merged) - with open("tcod/event_constants.py", "w", encoding="utf-8") as f: + with Path("tcod/event_constants.py").open("w", encoding="utf-8") as f: all_names = [] f.write(EVENT_CONSTANT_MODULE_HEADER) f.write("\n# --- SDL scancodes ---\n") @@ -376,10 +379,10 @@ def write_library_constants() -> None: f.write(f"""{parse_sdl_attrs("SDLK", None)[0]}\n""") f.write("\n# --- SDL keyboard modifiers ---\n") - f.write("%s\n_REVERSE_MOD_TABLE = %s\n" % parse_sdl_attrs("KMOD", all_names)) + f.write("{}\n_REVERSE_MOD_TABLE = {}\n".format(*parse_sdl_attrs("KMOD", all_names))) f.write("\n# --- SDL wheel ---\n") - f.write("%s\n_REVERSE_WHEEL_TABLE = %s\n" % parse_sdl_attrs("SDL_MOUSEWHEEL", all_names)) + f.write("{}\n_REVERSE_WHEEL_TABLE = {}\n".format(*parse_sdl_attrs("SDL_MOUSEWHEEL", all_names))) all_names_merged = ",\n ".join(f'"{name}"' for name in all_names) f.write(f"\n__all__ = [\n {all_names_merged},\n]\n") From 38e6ca9478493ce2b32f1c578468467888679445 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 25 May 2023 03:20:31 -0700 Subject: [PATCH 154/194] Have Ruff inherit the target version from the project. --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 06c30b87..dd068d98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -178,4 +178,3 @@ ignore = [ "D409", # section-underline-matches-section-length ] line-length = 120 -target-version = "py37" From 71a015220c2641757131f2c53abee7aad9171111 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 25 May 2023 03:33:52 -0700 Subject: [PATCH 155/194] Modernize tag release script. --- scripts/tag_release.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/scripts/tag_release.py b/scripts/tag_release.py index 32d47d70..92e896fe 100755 --- a/scripts/tag_release.py +++ b/scripts/tag_release.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +"""Automate tagged releases of this project.""" from __future__ import annotations import argparse @@ -8,7 +9,8 @@ import subprocess import sys from pathlib import Path -from typing import Tuple + +# ruff: noqa: INP001, S603, S607 PROJECT_DIR = Path(__file__).parent.parent @@ -23,7 +25,7 @@ parser.add_argument("-v", "--verbose", action="store_true", help="Print debug information.") -def parse_changelog(args: argparse.Namespace) -> Tuple[str, str]: +def parse_changelog(args: argparse.Namespace) -> tuple[str, str]: """Return an updated changelog and and the list of changes.""" match = re.match( pattern=r"(.*?## \[Unreleased]\n)(.+?\n)(\n*## \[.*)", @@ -32,9 +34,9 @@ def parse_changelog(args: argparse.Namespace) -> Tuple[str, str]: ) assert match header, changes, tail = match.groups() - tagged = "\n## [%s] - %s\n%s" % ( + tagged = "\n## [{}] - {}\n{}".format( args.tag, - datetime.date.today().isoformat(), + datetime.date.today().isoformat(), # Local timezone is fine, probably. # noqa: DTZ011 changes, ) if args.verbose: @@ -45,6 +47,7 @@ def parse_changelog(args: argparse.Namespace) -> Tuple[str, str]: def replace_unreleased_tags(tag: str, dry_run: bool) -> None: + """Walk though sources and replace pending tags with the new tag.""" match = re.match(r"\d+\.\d+", tag) assert match short_tag = match.group() @@ -62,7 +65,8 @@ def replace_unreleased_tags(tag: str, dry_run: bool) -> None: def main() -> None: - if len(sys.argv) == 1: + """Entry function.""" + if len(sys.argv) <= 1: parser.print_help(sys.stderr) sys.exit(1) @@ -79,8 +83,8 @@ def main() -> None: if not args.dry_run: (PROJECT_DIR / "CHANGELOG.md").write_text(new_changelog, encoding="utf-8") edit = ["-e"] if args.edit else [] - subprocess.check_call(["git", "commit", "-avm", "Prepare %s release." % args.tag] + edit) - subprocess.check_call(["git", "tag", args.tag, "-am", "%s\n\n%s" % (args.tag, changes)] + edit) + subprocess.check_call(["git", "commit", "-avm", f"Prepare {args.tag} release.", *edit]) + subprocess.check_call(["git", "tag", args.tag, "-am", f"{args.tag}\n\n{changes}", *edit]) if __name__ == "__main__": From aff2fefed7c2919abf755c73281dd4858394a4ff Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 25 May 2023 03:36:07 -0700 Subject: [PATCH 156/194] Prepare 15.0.2 release. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9043af2..f0a2ab97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] + +## [15.0.2] - 2023-05-25 ### Deprecated - Deprecated all libtcod color constants. Replace these with your own manually defined colors. Using a color will tell you the color values of the deprecated color in the warning. From dc244d4c8d86fd27e9a891a814de5d6bdfdfe9ab Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 25 May 2023 03:47:13 -0700 Subject: [PATCH 157/194] Remove Twine uploads from CI. The Twine command failed to run. I'll have to fix this and redeploy. --- .github/workflows/python-package.yml | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 71976b35..56bd1aa2 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -128,7 +128,7 @@ jobs: - name: Install Python dependencies run: | python -m pip install --upgrade pip - pip install pytest pytest-cov pytest-benchmark build twine + pip install pytest pytest-cov pytest-benchmark build if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Initialize package run: | @@ -148,13 +148,6 @@ jobs: if: runner.os != 'Windows' run: cat /tmp/xvfb.log - uses: codecov/codecov-action@v3 - - name: Upload to PyPI - if: startsWith(github.ref, 'refs/tags/') && runner.os != 'Linux' - env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - twine upload --skip-existing dist/* - uses: actions/upload-artifact@v3 if: runner.os == 'Linux' with: @@ -223,7 +216,7 @@ jobs: - name: Install Python dependencies run: | python -m pip install --upgrade pip - pip install twine cibuildwheel==2.3.1 + pip install cibuildwheel==2.3.1 - name: Build wheels run: | python -m cibuildwheel --output-dir wheelhouse @@ -245,13 +238,6 @@ jobs: name: wheels-linux path: wheelhouse/*.whl retention-days: 7 - - name: Upload to PyPI - if: startsWith(github.ref, 'refs/tags/') - env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - twine upload --skip-existing wheelhouse/* build-macos: needs: [black, isort, flake8, mypy] @@ -271,7 +257,7 @@ jobs: # https://github.com/actions/checkout/issues/290 run: git describe --tags - name: Install Python dependencies - run: pip3 install wheel twine -r requirements.txt + run: pip3 install -r requirements.txt - name: Prepare package # Downloads SDL2 for the later step. run: python3 setup.py || true @@ -290,12 +276,6 @@ jobs: name: wheels-macos path: wheelhouse/*.whl retention-days: 7 - - name: Upload to PyPI - if: startsWith(github.ref, 'refs/tags/') - env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: twine upload --skip-existing wheelhouse/* publish: needs: [build] From b4a04365a63b5fd3896e51b5e2e3dfa4d7f35d06 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 25 May 2023 03:57:40 -0700 Subject: [PATCH 158/194] Replace deprecated release action. --- .github/workflows/release-on-tag.yml | 13 +++++-------- .vscode/settings.json | 1 + 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release-on-tag.yml b/.github/workflows/release-on-tag.yml index cecfbda6..95cf02d3 100644 --- a/.github/workflows/release-on-tag.yml +++ b/.github/workflows/release-on-tag.yml @@ -9,6 +9,8 @@ jobs: build: name: Create Release runs-on: ubuntu-latest + permissions: + contents: write steps: - name: Checkout code uses: actions/checkout@v3 @@ -17,12 +19,7 @@ jobs: scripts/get_release_description.py | tee release_body.md - name: Create Release id: create_release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} + uses: ncipollo/release-action@v1 with: - tag_name: ${{ github.ref }} - release_name: "" - body_path: release_body.md - draft: false - prerelease: false + name: "" + bodyFile: release_body.md diff --git a/.vscode/settings.json b/.vscode/settings.json index 12dc2d77..6351b6e7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -265,6 +265,7 @@ "muly", "mypy", "namegen", + "ncipollo", "ndarray", "ndim", "newaxis", From 814f54f55c424ff8330868fa8c844be8010324d2 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 25 May 2023 03:58:44 -0700 Subject: [PATCH 159/194] Prepare 15.0.3 release. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0a2ab97..d73c8da5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version ## [Unreleased] +## [15.0.3] - 2023-05-25 + ## [15.0.2] - 2023-05-25 ### Deprecated - Deprecated all libtcod color constants. Replace these with your own manually defined colors. From 2539d296678dc0bcaeaaa163a9cda716bf1bc345 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 25 May 2023 05:18:43 -0700 Subject: [PATCH 160/194] Reorganize workflows. Skip tests on emulated architectures. Make an sdist in its own job so that isolated tests can start sooner. Let more builds/tests run in parallel. Merge publish jobs so that PyPI updates are done all at once. --- .github/workflows/python-package.yml | 95 ++++++++++++---------------- pyproject.toml | 4 ++ 2 files changed, 43 insertions(+), 56 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 56bd1aa2..2f4960a9 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -55,17 +55,37 @@ jobs: - uses: actions/checkout@v3 - name: Checkout submodules run: git submodule update --init --recursive --depth 1 - - name: Install Python dependencies + - name: Install typing dependencies run: pip install mypy pytest -r requirements.txt - - name: Fake initialize package - run: | - echo '__version__ = ""' > tcod/version.py - name: Mypy uses: liskin/gh-problem-matcher-wrap@v2 with: linters: mypy run: mypy --show-column-numbers + sdist: + runs-on: ubuntu-latest + steps: + - name: APT update + run: sudo apt-get update + - name: Install APT dependencies + run: sudo apt-get install libsdl2-dev + - uses: actions/checkout@v3 + with: + fetch-depth: ${{ env.git-depth }} + - name: Checkout submodules + run: git submodule update --init --recursive --depth 1 + - name: Install build + run: pip install build + - name: Build source distribution + run: python -m build --sdist + - uses: actions/upload-artifact@v3 + with: + name: sdist + path: dist/tcod-*.tar.gz + retention-days: 7 + + # This makes sure that the latest versions of the SDL headers parse correctly. parse_sdl: needs: [black, isort, flake8, mypy] @@ -133,7 +153,7 @@ jobs: - name: Initialize package run: | pip install -e . # Install the package in-place. - - name: Build package. + - name: Build package run: | python -m build - name: Test with pytest @@ -148,12 +168,6 @@ jobs: if: runner.os != 'Windows' run: cat /tmp/xvfb.log - uses: codecov/codecov-action@v3 - - uses: actions/upload-artifact@v3 - if: runner.os == 'Linux' - with: - name: sdist - path: dist/tcod-*.tar.gz - retention-days: 7 - uses: actions/upload-artifact@v3 if: runner.os == 'Windows' with: @@ -162,7 +176,7 @@ jobs: retention-days: 7 isolated: # Test installing the package from source. - needs: build + needs: [black, isort, flake8, mypy, sdist] runs-on: ${{ matrix.os }} strategy: matrix: @@ -192,8 +206,7 @@ jobs: python -c "import tcod" linux-wheels: - needs: build # These take a while to build/test, so wait for normal tests to pass first. - if: startsWith(github.event.ref, 'refs/tags/') + needs: [black, isort, flake8, mypy] runs-on: "ubuntu-latest" strategy: matrix: @@ -232,6 +245,8 @@ jobs: yum install -y SDL2-devel CIBW_BEFORE_TEST: pip install numpy CIBW_TEST_COMMAND: python -c "import tcod" + # Skip test on emulated architectures + CIBW_TEST_SKIP: "*_aarch64" - name: Archive wheel uses: actions/upload-artifact@v3 with: @@ -252,15 +267,11 @@ jobs: fetch-depth: ${{ env.git-depth }} - name: Checkout submodules run: git submodule update --init --recursive --depth 1 - - name: Print git describe - # "--tags" is required to workaround actions/checkout's broken annotated tag handing. - # https://github.com/actions/checkout/issues/290 - run: git describe --tags - name: Install Python dependencies run: pip3 install -r requirements.txt - name: Prepare package # Downloads SDL2 for the later step. - run: python3 setup.py || true + run: python3 build_sdl.py - name: Build wheels uses: pypa/cibuildwheel@v2.12.3 env: @@ -278,7 +289,7 @@ jobs: retention-days: 7 publish: - needs: [build] + needs: [sdist, build, build-macos, linux-wheels] runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/') environment: @@ -295,42 +306,14 @@ jobs: with: name: wheels-windows path: dist/ + - uses: actions/download-artifact@v3 + with: + name: wheels-macos + path: dist/ + - uses: actions/download-artifact@v3 + with: + name: wheels-linux + path: dist/ - uses: pypa/gh-action-pypi-publish@release/v1 with: skip-existing: true - - publish-macos: - needs: [build-macos] - runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/') - environment: - name: release - url: https://pypi.org/p/tcod - permissions: - id-token: write - steps: - - uses: actions/download-artifact@v3 - with: - name: wheels-macos - path: dist/ - - uses: pypa/gh-action-pypi-publish@release/v1 - with: - skip-existing: true - - publish-linux: - needs: [linux-wheels] - runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/') - environment: - name: release - url: https://pypi.org/p/tcod - permissions: - id-token: write - steps: - - uses: actions/download-artifact@v3 - with: - name: wheels-linux - path: dist/ - - uses: pypa/gh-action-pypi-publish@release/v1 - with: - skip-existing: true diff --git a/pyproject.toml b/pyproject.toml index dd068d98..e181a747 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -134,6 +134,10 @@ exclude = [ module = "numpy.*" ignore_missing_imports = true +[[tool.mypy.overrides]] +module = "tcod.version" +ignore_missing_imports = true + [tool.ruff] # https://beta.ruff.rs/docs/rules/ From 531e72ded1243f5e8a6e644a2781dcb51f502790 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 25 May 2023 05:45:24 -0700 Subject: [PATCH 161/194] Remove missing version from changelog. It failed to deploy. 15.0.2 only exists as a tag at this point. --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d73c8da5..e5730d8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,6 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version ## [Unreleased] ## [15.0.3] - 2023-05-25 - -## [15.0.2] - 2023-05-25 ### Deprecated - Deprecated all libtcod color constants. Replace these with your own manually defined colors. Using a color will tell you the color values of the deprecated color in the warning. From 5692a258d09dc6d77d687146697ec3635494601d Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 26 May 2023 21:55:16 -0700 Subject: [PATCH 162/194] Disable and remove Flake8. Converting over to Ruff which has slightly different warnings for the same things, and the Flake8 warnings are being distracting. --- .github/workflows/python-package.yml | 22 +++++----------------- .vscode/settings.json | 2 +- build_libtcod.py | 2 +- setup.cfg | 4 ---- 4 files changed, 7 insertions(+), 23 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 2f4960a9..9161d910 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -37,18 +37,6 @@ jobs: linters: isort run: isort scripts/ tcod/ tests/ examples/ --check --diff - flake8: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: Install Flake8 - run: pip install Flake8 - - name: Flake8 - uses: liskin/gh-problem-matcher-wrap@v2 - with: - linters: flake8 - run: flake8 scripts/ tcod/ tests/ - mypy: runs-on: ubuntu-latest steps: @@ -88,7 +76,7 @@ jobs: # This makes sure that the latest versions of the SDL headers parse correctly. parse_sdl: - needs: [black, isort, flake8, mypy] + needs: [black, isort, mypy] runs-on: ${{ matrix.os }} strategy: matrix: @@ -112,7 +100,7 @@ jobs: SDL_VERSION: ${{ matrix.sdl-version }} build: - needs: [black, isort, flake8, mypy] + needs: [black, isort, mypy] runs-on: ${{ matrix.os }} strategy: matrix: @@ -176,7 +164,7 @@ jobs: retention-days: 7 isolated: # Test installing the package from source. - needs: [black, isort, flake8, mypy, sdist] + needs: [black, isort, mypy, sdist] runs-on: ${{ matrix.os }} strategy: matrix: @@ -206,7 +194,7 @@ jobs: python -c "import tcod" linux-wheels: - needs: [black, isort, flake8, mypy] + needs: [black, isort, mypy] runs-on: "ubuntu-latest" strategy: matrix: @@ -255,7 +243,7 @@ jobs: retention-days: 7 build-macos: - needs: [black, isort, flake8, mypy] + needs: [black, isort, mypy] runs-on: "macos-11" strategy: fail-fast: true diff --git a/.vscode/settings.json b/.vscode/settings.json index 6351b6e7..4274b1dd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,7 +11,7 @@ "files.insertFinalNewline": true, "files.trimTrailingWhitespace": true, "python.linting.enabled": true, - "python.linting.flake8Enabled": true, + "python.linting.flake8Enabled": false, "python.linting.mypyEnabled": true, "python.linting.mypyArgs": [ "--follow-imports=silent", diff --git a/build_libtcod.py b/build_libtcod.py index 98571002..3f5d2772 100755 --- a/build_libtcod.py +++ b/build_libtcod.py @@ -317,7 +317,7 @@ def generate_enums(prefix: str) -> Iterator[str]: _, name = symbol.split("_", 1) if name.isdigit(): name = f"N{name}" - if name in "IOl": # Handle Flake8 warnings. + if name in "IOl": # Ignore ambiguous variable name warnings. yield f"{name} = {value} # noqa: E741" else: yield f"{name} = {value}" diff --git a/setup.cfg b/setup.cfg index 75665f21..9cc8e393 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,7 +3,3 @@ py-limited-api = cp36 [aliases] test=pytest - -[flake8] -ignore = E203 W503 TYP001 -max-line-length = 130 From 756722f8c1a636e46a384d20c9e926dbace76ed2 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 26 May 2023 23:04:41 -0700 Subject: [PATCH 163/194] Use Ruff to clean up tcod, bsp, console, context, event, and image modules. --- pyproject.toml | 4 + tcod/__init__.py | 9 +- tcod/bsp.py | 42 ++++---- tcod/console.py | 127 +++++++++++------------ tcod/context.py | 85 ++++++++-------- tcod/event.py | 259 +++++++++++++++++++++++++---------------------- tcod/image.py | 31 +++--- 7 files changed, 289 insertions(+), 268 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e181a747..4df86fb1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -182,3 +182,7 @@ ignore = [ "D409", # section-underline-matches-section-length ] line-length = 120 + +[tool.ruff.pydocstyle] +# Use Google-style docstrings. +convention = "google" diff --git a/tcod/__init__.py b/tcod/__init__.py index b14581dd..0082d521 100644 --- a/tcod/__init__.py +++ b/tcod/__init__.py @@ -14,10 +14,10 @@ __path__ = extend_path(__path__, __name__) from tcod import bsp, color, console, constants, context, event, image, los, map, noise, path, random, tileset -from tcod.console import Console # noqa: F401 -from tcod.constants import * # noqa: F4 -from tcod.libtcodpy import * # noqa: F4 -from tcod.loader import __sdl_version__, ffi, lib # noqa: F4 +from tcod.console import Console +from tcod.constants import * # noqa: F403 +from tcod.libtcodpy import * # noqa: F403 +from tcod.loader import __sdl_version__, ffi, lib try: from tcod.version import __version__ @@ -41,6 +41,7 @@ def __getattr__(name: str, stacklevel: int = 1) -> color.Color: __all__ = [ # noqa: F405 "__version__", + "__sdl_version__", "lib", "ffi", "bsp", diff --git a/tcod/bsp.py b/tcod/bsp.py index 23c54ef0..8734c340 100644 --- a/tcod/bsp.py +++ b/tcod/bsp.py @@ -1,10 +1,11 @@ -r""" +r"""Libtcod's Binary Space Partitioning. + The following example shows how to traverse the BSP tree using Python. This assumes `create_room` and `connect_rooms` will be replaced by custom code. Example:: - import tcod + import tcod.bsp bsp = tcod.bsp.BSP(x=0, y=0, width=80, height=60) bsp.split_recursive( @@ -25,14 +26,14 @@ """ from __future__ import annotations -from typing import Any, Iterator, List, Optional, Tuple, Union # noqa: F401 +from typing import Any, Iterator import tcod.random from tcod._internal import deprecate from tcod.loader import ffi, lib -class BSP(object): +class BSP: """A binary space partitioning tree which can be used for simple dungeon generation. Attributes: @@ -55,7 +56,8 @@ class BSP(object): height (int): Rectangle height. """ - def __init__(self, x: int, y: int, width: int, height: int): + def __init__(self, x: int, y: int, width: int, height: int) -> None: + """Initialize a root node of a BSP tree.""" self.x = x self.y = y self.width = width @@ -65,11 +67,12 @@ def __init__(self, x: int, y: int, width: int, height: int): self.position = 0 self.horizontal = False - self.parent: Optional[BSP] = None - self.children: Union[Tuple[()], Tuple[BSP, BSP]] = () + self.parent: BSP | None = None + self.children: tuple[()] | tuple[BSP, BSP] = () @property - def w(self) -> int: + @deprecate("This attribute has been renamed to `width`.", FutureWarning) + def w(self) -> int: # noqa: D102 return self.width @w.setter @@ -77,7 +80,8 @@ def w(self, value: int) -> None: self.width = value @property - def h(self) -> int: + @deprecate("This attribute has been renamed to `height`.", FutureWarning) + def h(self) -> int: # noqa: D102 return self.height @h.setter @@ -92,7 +96,7 @@ def _as_cdata(self) -> Any: cdata.level = self.level return cdata - def __str__(self) -> str: + def __repr__(self) -> str: """Provide a useful readout when printed.""" status = "leaf" if self.children: @@ -101,7 +105,7 @@ def __str__(self) -> str: self.horizontal, ) - return "<%s(x=%i,y=%i,width=%i,height=%i)level=%i,%s>" % ( + return "<%s(x=%i,y=%i,width=%i,height=%i) level=%i %s>" % ( self.__class__.__name__, self.x, self.y, @@ -131,21 +135,21 @@ def split_once(self, horizontal: bool, position: int) -> None: """Split this partition into 2 sub-partitions. Args: - horizontal (bool): - position (int): + horizontal (bool): If True then the sub-partition is split into an upper and bottom half. + position (int): The position of where to put the divider relative to the current node. """ cdata = self._as_cdata() lib.TCOD_bsp_split_once(cdata, horizontal, position) self._unpack_bsp_tree(cdata) - def split_recursive( + def split_recursive( # noqa: PLR0913 self, depth: int, min_width: int, min_height: int, max_horizontal_ratio: float, max_vertical_ratio: float, - seed: Optional[tcod.random.Random] = None, + seed: tcod.random.Random | None = None, ) -> None: """Divide this partition recursively. @@ -229,7 +233,7 @@ def inverted_level_order(self) -> Iterator[BSP]: .. versionadded:: 8.3 """ - levels: List[List[BSP]] = [] + levels: list[list[BSP]] = [] next = [self] while next: levels.append(next) @@ -253,16 +257,16 @@ def contains(self, x: int, y: int) -> bool: """ return self.x <= x < self.x + self.width and self.y <= y < self.y + self.height - def find_node(self, x: int, y: int) -> Optional[BSP]: + def find_node(self, x: int, y: int) -> BSP | None: """Return the deepest node which contains these coordinates. Returns: - Optional[BSP]: BSP object or None. + BSP object or None. """ if not self.contains(x, y): return None for child in self.children: - found: Optional[BSP] = child.find_node(x, y) + found = child.find_node(x, y) if found: return found return self diff --git a/tcod/console.py b/tcod/console.py index 3ffa437b..77a75c0e 100644 --- a/tcod/console.py +++ b/tcod/console.py @@ -8,7 +8,7 @@ import warnings from os import PathLike from pathlib import Path -from typing import Any, Iterable, Optional, Sequence, Tuple, Union +from typing import Any, Iterable import numpy as np from numpy.typing import NDArray @@ -116,9 +116,9 @@ def __init__( width: int, height: int, order: Literal["C", "F"] = "C", - buffer: Optional[NDArray[Any]] = None, + buffer: NDArray[Any] | None = None, ): - self._key_color: Optional[Tuple[int, int, int]] = None + self._key_color: tuple[int, int, int] | None = None self._order = tcod._internal.verify_order(order) if buffer is not None: if self._order == "F": @@ -152,7 +152,7 @@ def __init__( self.clear() @classmethod - def _from_cdata(cls, cdata: Any, order: Literal["C", "F"] = "C") -> Console: + def _from_cdata(cls, cdata: Any, order: Literal["C", "F"] = "C") -> Console: # noqa: ANN401 """Return a Console instance which wraps this `TCOD_Console*` object.""" if isinstance(cdata, cls): return cdata @@ -162,7 +162,7 @@ def _from_cdata(cls, cdata: Any, order: Literal["C", "F"] = "C") -> Console: return self @classmethod - def _get_root(cls, order: Optional[Literal["C", "F"]] = None) -> Console: + def _get_root(cls, order: Literal["C", "F"] | None = None) -> Console: """Return a root console singleton with valid buffers. This function will also update an already active root console. @@ -196,12 +196,12 @@ def _init_setup_console_data(self, order: Literal["C", "F"] = "C") -> None: @property def width(self) -> int: - """int: The width of this Console. (read-only)""" + """The width of this Console.""" return lib.TCOD_console_get_width(self.console_c) # type: ignore @property def height(self) -> int: - """int: The height of this Console. (read-only)""" + """The height of this Console.""" return lib.TCOD_console_get_height(self.console_c) # type: ignore @property @@ -341,25 +341,25 @@ def rgb(self) -> NDArray[Any]: return self.rgba.view(self._DTYPE_RGB) @property - def default_bg(self) -> Tuple[int, int, int]: + def default_bg(self) -> tuple[int, int, int]: """Tuple[int, int, int]: The default background color.""" color = self._console_data.back return color.r, color.g, color.b @default_bg.setter @deprecate("Console defaults have been deprecated.") - def default_bg(self, color: Tuple[int, int, int]) -> None: + def default_bg(self, color: tuple[int, int, int]) -> None: self._console_data.back = color @property - def default_fg(self) -> Tuple[int, int, int]: + def default_fg(self) -> tuple[int, int, int]: """Tuple[int, int, int]: The default foreground color.""" color = self._console_data.fore return color.r, color.g, color.b @default_fg.setter @deprecate("Console defaults have been deprecated.") - def default_fg(self, color: Tuple[int, int, int]) -> None: + def default_fg(self, color: tuple[int, int, int]) -> None: self._console_data.fore = color @property @@ -382,10 +382,10 @@ def default_alignment(self) -> int: def default_alignment(self, value: int) -> None: self._console_data.alignment = value - def __clear_warning(self, name: str, value: Tuple[int, int, int]) -> None: + def __clear_warning(self, name: str, value: tuple[int, int, int]) -> None: """Raise a warning for bad default values during calls to clear.""" warnings.warn( - "Clearing with the console default values is deprecated.\n" "Add %s=%r to this call." % (name, value), + f"Clearing with the console default values is deprecated.\nAdd {name}={value!r} to this call.", DeprecationWarning, stacklevel=3, ) @@ -393,8 +393,8 @@ def __clear_warning(self, name: str, value: Tuple[int, int, int]) -> None: def clear( self, ch: int = 0x20, - fg: Tuple[int, int, int] = ..., # type: ignore - bg: Tuple[int, int, int] = ..., # type: ignore + fg: tuple[int, int, int] = ..., # type: ignore + bg: tuple[int, int, int] = ..., # type: ignore ) -> None: """Reset all values in this console to a single value. @@ -459,25 +459,22 @@ def put_char( 13: "tcod.BKGND_DEFAULT", } - def __deprecate_defaults( + def __deprecate_defaults( # noqa: C901, PLR0912 self, new_func: str, - bg_blend: Any, - alignment: Any = ..., - clear: Any = ..., + bg_blend: Any, # noqa: ANN401 + alignment: Any = ..., # noqa: ANN401 + clear: Any = ..., # noqa: ANN401 ) -> None: """Return the parameters needed to recreate the current default state.""" if not __debug__: return - fg: Optional[Tuple[int, int, int]] = self.default_fg - bg: Optional[Tuple[int, int, int]] = self.default_bg + fg: tuple[int, int, int] | None = self.default_fg + bg: tuple[int, int, int] | None = self.default_bg if bg_blend == tcod.constants.BKGND_NONE: bg = None - if bg_blend == tcod.constants.BKGND_DEFAULT: - bg_blend = self.default_bg_blend - else: - bg_blend = None + bg_blend = self.default_bg_blend if bg_blend == tcod.constants.BKGND_DEFAULT else None if bg_blend == tcod.constants.BKGND_NONE: bg = None bg_blend = None @@ -497,22 +494,19 @@ def __deprecate_defaults( if clear is False: params.append("ch=0") if fg is not None: - params.append("fg=%s" % (fg,)) + params.append(f"fg={fg}") if bg is not None: - params.append("bg=%s" % (bg,)) + params.append(f"bg={bg}") if bg_blend is not None: - params.append("bg_blend=%s" % (self.__BG_BLEND_LOOKUP[bg_blend],)) + params.append(f"bg_blend={self.__BG_BLEND_LOOKUP[bg_blend]}") if alignment is not None: - params.append("alignment=%s" % (self.__ALIGNMENT_LOOKUP[alignment],)) + params.append(f"alignment={self.__ALIGNMENT_LOOKUP[alignment]}") param_str = ", ".join(params) - if not param_str: - param_str = "." - else: - param_str = " and add the following parameters:\n%s" % (param_str,) + param_str = "." if not param_str else f" and add the following parameters:\n{param_str}" warnings.warn( "Console functions using default values have been deprecated.\n" - "Replace this method with `Console.%s`%s" % (new_func, param_str), - DeprecationWarning, + f"Replace this method with `Console.{new_func}`{param_str}", + FutureWarning, stacklevel=3, ) @@ -522,7 +516,7 @@ def print_( y: int, string: str, bg_blend: int = tcod.constants.BKGND_DEFAULT, - alignment: Optional[int] = None, + alignment: int | None = None, ) -> None: """Print a color formatted string on a console. @@ -551,7 +545,7 @@ def print_rect( height: int, string: str, bg_blend: int = tcod.constants.BKGND_DEFAULT, - alignment: Optional[int] = None, + alignment: int | None = None, ) -> int: """Print a string constrained to a rectangle. @@ -748,7 +742,7 @@ def blit( height: int = 0, fg_alpha: float = 1.0, bg_alpha: float = 1.0, - key_color: Optional[Tuple[int, int, int]] = None, + key_color: tuple[int, int, int] | None = None, ) -> None: """Blit from this console onto the ``dest`` console. @@ -828,7 +822,7 @@ def blit( ) @deprecate("Pass the key color to Console.blit instead of calling this function.") - def set_key_color(self, color: Optional[Tuple[int, int, int]]) -> None: + def set_key_color(self, color: tuple[int, int, int] | None) -> None: """Set a consoles blit transparent color. `color` is the (r, g, b) color, or None to disable key color. @@ -853,7 +847,8 @@ def __enter__(self) -> Console: :any:`tcod.console_init_root` """ if self.console_c != ffi.NULL: - raise NotImplementedError("Only the root console has a context.") + msg = "Only the root console has a context." + raise NotImplementedError(msg) return self def close(self) -> None: @@ -865,7 +860,8 @@ def close(self) -> None: .. versionadded:: 11.11 """ if self.console_c != ffi.NULL: - raise NotImplementedError("Only the root console can be used to close libtcod's window.") + msg = "Only the root console can be used to close libtcod's window." + raise NotImplementedError(msg) lib.TCOD_console_delete(self.console_c) def __exit__(self, *args: Any) -> None: @@ -882,7 +878,7 @@ def __bool__(self) -> bool: """ return bool(self.console_c != ffi.NULL) - def __getstate__(self) -> Any: + def __getstate__(self) -> dict[str, Any]: state = self.__dict__.copy() del state["console_c"] state["_console_data"] = { @@ -897,7 +893,7 @@ def __getstate__(self) -> Any: state["_tiles"] = np.array(self._tiles, copy=True) return state - def __setstate__(self, state: Any) -> None: + def __setstate__(self, state: dict[str, Any]) -> None: self._key_color = None if "_tiles" not in state: tiles: NDArray[Any] = np.ndarray((self.height, self.width), dtype=self.DTYPE) @@ -933,8 +929,8 @@ def print( x: int, y: int, string: str, - fg: Optional[Tuple[int, int, int]] = None, - bg: Optional[Tuple[int, int, int]] = None, + fg: tuple[int, int, int] | None = None, + bg: tuple[int, int, int] | None = None, bg_blend: int = tcod.constants.BKGND_SET, alignment: int = tcod.constants.LEFT, ) -> None: @@ -984,8 +980,8 @@ def print_box( width: int, height: int, string: str, - fg: Optional[Tuple[int, int, int]] = None, - bg: Optional[Tuple[int, int, int]] = None, + fg: tuple[int, int, int] | None = None, + bg: tuple[int, int, int] | None = None, bg_blend: int = tcod.constants.BKGND_SET, alignment: int = tcod.constants.LEFT, ) -> int: @@ -1044,11 +1040,11 @@ def draw_frame( height: int, title: str = "", clear: bool = True, - fg: Optional[Tuple[int, int, int]] = None, - bg: Optional[Tuple[int, int, int]] = None, + fg: tuple[int, int, int] | None = None, + bg: tuple[int, int, int] | None = None, bg_blend: int = tcod.constants.BKGND_SET, *, - decoration: Union[str, Tuple[int, int, int, int, int, int, int, int, int]] = "┌─┐│ │└─┘", + decoration: str | tuple[int, int, int, int, int, int, int, int, int] = "┌─┐│ │└─┘", ) -> None: r"""Draw a framed rectangle with an optional title. @@ -1111,9 +1107,8 @@ def draw_frame( └─┤Lower├──┘> """ if title and decoration != "┌─┐│ │└─┘": - raise TypeError( - "The title and decoration parameters are mutually exclusive. You should print the title manually." - ) + msg = "The title and decoration parameters are mutually exclusive. You should print the title manually." + raise TypeError(msg) if title: warnings.warn( "The title parameter will be removed in the future since the style is hard-coded.", @@ -1135,13 +1130,10 @@ def draw_frame( clear, ) return - decoration_: Sequence[int] - if isinstance(decoration, str): - decoration_ = [ord(c) for c in decoration] - else: - decoration_ = decoration + decoration_ = [ord(c) for c in decoration] if isinstance(decoration, str) else decoration if len(decoration_) != 9: - raise TypeError(f"Decoration must have a length of 9 (len(decoration) is {len(decoration_)}.)") + msg = f"Decoration must have a length of 9 (len(decoration) is {len(decoration_)}.)" + raise TypeError(msg) _check( lib.TCOD_console_draw_frame_rgb( self.console_c, @@ -1164,8 +1156,8 @@ def draw_rect( width: int, height: int, ch: int, - fg: Optional[Tuple[int, int, int]] = None, - bg: Optional[Tuple[int, int, int]] = None, + fg: tuple[int, int, int] | None = None, + bg: tuple[int, int, int] | None = None, bg_blend: int = tcod.constants.BKGND_SET, ) -> None: """Draw characters and colors over a rectangular region. @@ -1234,7 +1226,7 @@ def get_height_rect(width: int, string: str) -> int: @deprecate("This function does not support contexts.") -def recommended_size() -> Tuple[int, int]: +def recommended_size() -> tuple[int, int]: """Return the recommended size of a console for the current active window. The return is determined from the active tileset size and active window @@ -1253,7 +1245,8 @@ def recommended_size() -> Tuple[int, int]: Use :any:`Context.recommended_console_size` instead. """ if not lib.TCOD_ctx.engine: - raise RuntimeError("The libtcod engine was not initialized first.") + msg = "The libtcod engine was not initialized first." + raise RuntimeError(msg) window = lib.TCOD_sys_get_sdl_window() renderer = lib.TCOD_sys_get_sdl_renderer() with ffi.new("int[2]") as xy: @@ -1266,7 +1259,7 @@ def recommended_size() -> Tuple[int, int]: return w, h -def load_xp(path: Union[str, PathLike[str]], order: Literal["C", "F"] = "C") -> Tuple[Console, ...]: +def load_xp(path: str | PathLike[str], order: Literal["C", "F"] = "C") -> tuple[Console, ...]: """Load a REXPaint file as a tuple of consoles. `path` is the name of the REXPaint file to load. @@ -1299,9 +1292,7 @@ def load_xp(path: Union[str, PathLike[str]], order: Literal["C", "F"] = "C") -> is_transparent = (console.rgb["bg"] == KEY_COLOR).all(axis=-1) console.rgba[is_transparent] = (ord(" "), (0,), (0,)) """ - path = Path(path) - if not path.exists(): - raise FileNotFoundError(f"File not found:\n\t{path.resolve()}") + path = Path(path).resolve(strict=True) layers = _check(tcod.lib.TCOD_load_xp(bytes(path), 0, ffi.NULL)) consoles = ffi.new("TCOD_Console*[]", layers) _check(tcod.lib.TCOD_load_xp(bytes(path), layers, consoles)) @@ -1309,7 +1300,7 @@ def load_xp(path: Union[str, PathLike[str]], order: Literal["C", "F"] = "C") -> def save_xp( - path: Union[str, PathLike[str]], + path: str | PathLike[str], consoles: Iterable[Console], compress_level: int = 9, ) -> None: diff --git a/tcod/context.py b/tcod/context.py index cc82c90f..b8f359d4 100644 --- a/tcod/context.py +++ b/tcod/context.py @@ -46,17 +46,17 @@ :any:`tcod.mouse_get_status`. .. versionadded:: 11.12 -""" # noqa: E501 +""" from __future__ import annotations import copy -import os import pickle import sys import warnings -from typing import Any, Iterable, List, Optional, Tuple, TypeVar +from pathlib import Path +from typing import Any, Iterable, NoReturn, TypeVar -from typing_extensions import Literal, NoReturn +from typing_extensions import Literal import tcod import tcod.event @@ -149,18 +149,18 @@ """ -def _handle_tileset(tileset: Optional[tcod.tileset.Tileset]) -> Any: +def _handle_tileset(tileset: tcod.tileset.Tileset | None) -> Any: """Get the TCOD_Tileset pointer from a Tileset or return a NULL pointer.""" return tileset._tileset_p if tileset else ffi.NULL -def _handle_title(title: Optional[str]) -> Any: +def _handle_title(title: str | None) -> Any: """Return title as a CFFI string. If title is None then return a decent default title is returned. """ if title is None: - title = os.path.basename(sys.argv[0]) + title = Path(sys.argv[0]).name return ffi.new("char[]", title.encode("utf-8")) @@ -170,7 +170,7 @@ class Context: Use :any:`tcod.context.new` to create a new context. """ - def __init__(self, context_p: Any): + def __init__(self, context_p: Any) -> None: """Create a context from a cffi pointer.""" self._context_p = context_p @@ -201,8 +201,8 @@ def present( *, keep_aspect: bool = False, integer_scaling: bool = False, - clear_color: Tuple[int, int, int] = (0, 0, 0), - align: Tuple[float, float] = (0.5, 0.5), + clear_color: tuple[int, int, int] = (0, 0, 0), + align: tuple[float, float] = (0.5, 0.5), ) -> None: """Present a console to this context's display. @@ -238,13 +238,13 @@ def present( ) _check(lib.TCOD_context_present(self._context_p, console.console_c, viewport_args)) - def pixel_to_tile(self, x: int, y: int) -> Tuple[int, int]: + def pixel_to_tile(self, x: int, y: int) -> tuple[int, int]: """Convert window pixel coordinates to tile coordinates.""" with ffi.new("int[2]", (x, y)) as xy: _check(lib.TCOD_context_screen_pixel_to_tile_i(self._context_p, xy, xy + 1)) return xy[0], xy[1] - def pixel_to_subtile(self, x: int, y: int) -> Tuple[float, float]: + def pixel_to_subtile(self, x: int, y: int) -> tuple[float, float]: """Convert window pixel coordinates to sub-tile coordinates.""" with ffi.new("double[2]", (x, y)) as xy: _check(lib.TCOD_context_screen_pixel_to_tile_d(self._context_p, xy, xy + 1)) @@ -283,12 +283,12 @@ def convert_event(self, event: _Event) -> _Event: ) return event_copy - def save_screenshot(self, path: Optional[str] = None) -> None: + def save_screenshot(self, path: str | None = None) -> None: """Save a screen-shot to the given file path.""" c_path = path.encode("utf-8") if path is not None else ffi.NULL _check(lib.TCOD_context_save_screenshot(self._context_p, c_path)) - def change_tileset(self, tileset: Optional[tcod.tileset.Tileset]) -> None: + def change_tileset(self, tileset: tcod.tileset.Tileset | None) -> None: """Change the active tileset used by this context. The new tileset will take effect on the next call to :any:`present`. @@ -363,7 +363,7 @@ def new_console( width, height = max(min_columns, size[0]), max(min_rows, size[1]) return tcod.console.Console(width, height, order=order) - def recommended_console_size(self, min_columns: int = 1, min_rows: int = 1) -> Tuple[int, int]: + def recommended_console_size(self, min_columns: int = 1, min_rows: int = 1) -> tuple[int, int]: """Return the recommended (columns, rows) of a console for this context. `min_columns`, `min_rows` are the lowest values which will be returned. @@ -406,11 +406,11 @@ def toggle_fullscreen(context: tcod.context.Context) -> None: 0 if fullscreen else tcod.lib.SDL_WINDOW_FULLSCREEN_DESKTOP, ) - ''' # noqa: E501 + ''' return lib.TCOD_context_get_sdl_window(self._context_p) @property - def sdl_window(self) -> Optional[tcod.sdl.video.Window]: + def sdl_window(self) -> tcod.sdl.video.Window | None: '''Return a :any:`tcod.sdl.video.Window` referencing this contexts SDL window if it exists. Example:: @@ -434,7 +434,7 @@ def toggle_fullscreen(context: tcod.context.Context) -> None: return tcod.sdl.video.Window(p) if p else None @property - def sdl_renderer(self) -> Optional[tcod.sdl.render.Renderer]: + def sdl_renderer(self) -> tcod.sdl.render.Renderer | None: """Return a :any:`tcod.sdl.render.Renderer` referencing this contexts SDL renderer if it exists. .. versionadded:: 13.4 @@ -443,7 +443,7 @@ def sdl_renderer(self) -> Optional[tcod.sdl.render.Renderer]: return tcod.sdl.render.Renderer(p) if p else None @property - def sdl_atlas(self) -> Optional[tcod.render.SDLTilesetAtlas]: + def sdl_atlas(self) -> tcod.render.SDLTilesetAtlas | None: """Return a :any:`tcod.render.SDLTilesetAtlas` referencing libtcod's SDL texture atlas if it exists. .. versionadded:: 13.5 @@ -455,7 +455,8 @@ def sdl_atlas(self) -> Optional[tcod.render.SDLTilesetAtlas]: def __reduce__(self) -> NoReturn: """Contexts can not be pickled, so this class will raise :class:`pickle.PicklingError`.""" - raise pickle.PicklingError("Python-tcod contexts can not be pickled.") + msg = "Python-tcod contexts can not be pickled." + raise pickle.PicklingError(msg) @ffi.def_extern() # type: ignore @@ -464,25 +465,25 @@ def _pycall_cli_output(catch_reference: Any, output: Any) -> None: Catches the CLI output. """ - catch: List[str] = ffi.from_handle(catch_reference) + catch: list[str] = ffi.from_handle(catch_reference) catch.append(ffi.string(output).decode("utf-8")) def new( *, - x: Optional[int] = None, - y: Optional[int] = None, - width: Optional[int] = None, - height: Optional[int] = None, - columns: Optional[int] = None, - rows: Optional[int] = None, - renderer: Optional[int] = None, - tileset: Optional[tcod.tileset.Tileset] = None, + x: int | None = None, + y: int | None = None, + width: int | None = None, + height: int | None = None, + columns: int | None = None, + rows: int | None = None, + renderer: int | None = None, + tileset: tcod.tileset.Tileset | None = None, vsync: bool = True, - sdl_window_flags: Optional[int] = None, - title: Optional[str] = None, - argv: Optional[Iterable[str]] = None, - console: Optional[tcod.Console] = None, + sdl_window_flags: int | None = None, + title: str | None = None, + argv: Iterable[str] | None = None, + console: tcod.Console | None = None, ) -> Context: """Create a new context with the desired pixel size. @@ -550,7 +551,7 @@ def new( argv_encoded = [ffi.new("char[]", arg.encode("utf-8")) for arg in argv] # Needs to be kept alive for argv_c. argv_c = ffi.new("char*[]", argv_encoded) - catch_msg: List[str] = [] + catch_msg: list[str] = [] catch_handle = ffi.new_handle(catch_msg) # Keep alive. title_p = _handle_title(title) # Keep alive. @@ -590,11 +591,11 @@ def new_window( width: int, height: int, *, - renderer: Optional[int] = None, - tileset: Optional[tcod.tileset.Tileset] = None, + renderer: int | None = None, + tileset: tcod.tileset.Tileset | None = None, vsync: bool = True, - sdl_window_flags: Optional[int] = None, - title: Optional[str] = None, + sdl_window_flags: int | None = None, + title: str | None = None, ) -> Context: """Create a new context with the desired pixel size. @@ -617,11 +618,11 @@ def new_terminal( columns: int, rows: int, *, - renderer: Optional[int] = None, - tileset: Optional[tcod.tileset.Tileset] = None, + renderer: int | None = None, + tileset: tcod.tileset.Tileset | None = None, vsync: bool = True, - sdl_window_flags: Optional[int] = None, - title: Optional[str] = None, + sdl_window_flags: int | None = None, + title: str | None = None, ) -> Context: """Create a new context with the desired console size. diff --git a/tcod/event.py b/tcod/event.py index 19e0ee1e..c415cb34 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -83,7 +83,7 @@ import enum import warnings -from typing import Any, Callable, Dict, Generic, Iterator, Mapping, NamedTuple, Optional, Tuple, Type, TypeVar, Union +from typing import Any, Callable, Generic, Iterator, Mapping, NamedTuple, TypeVar import numpy as np from numpy.typing import NDArray @@ -91,7 +91,7 @@ import tcod.event_constants import tcod.sdl.joystick -from tcod.event_constants import * # noqa: F4 +from tcod.event_constants import * # noqa: F403 from tcod.event_constants import KMOD_ALT, KMOD_CTRL, KMOD_GUI, KMOD_SHIFT from tcod.loader import ffi, lib from tcod.sdl.joystick import _HAT_DIRECTIONS @@ -100,7 +100,7 @@ class _ConstantsWithPrefix(Mapping[int, str]): - def __init__(self, constants: Mapping[int, str]): + def __init__(self, constants: Mapping[int, str]) -> None: self.constants = constants def __getitem__(self, key: int) -> str: @@ -133,7 +133,7 @@ def _describe_bitmask(bits: int, table: Mapping[int, str], default: str = "0") - return "|".join(result) -def _pixel_to_tile(x: float, y: float) -> Optional[Tuple[float, float]]: +def _pixel_to_tile(x: float, y: float) -> tuple[float, float] | None: """Convert pixel coordinates to tile coordinates.""" if not lib.TCOD_ctx.engine: return None @@ -155,7 +155,7 @@ class Point(NamedTuple): """A pixel or tile coordinate starting with zero as the top-most position.""" -def _verify_tile_coordinates(xy: Optional[Point]) -> Point: +def _verify_tile_coordinates(xy: Point | None) -> Point: """Check if an events tile coordinate is initialized and warn if not. Always returns a valid Point object for backwards compatibility. @@ -291,7 +291,7 @@ class Event: pointer. All sub-classes have this attribute. """ - def __init__(self, type: Optional[str] = None): + def __init__(self, type: str | None = None) -> None: if type is None: type = self.__class__.__name__.upper() self.type: Final = type @@ -323,11 +323,12 @@ def from_sdl_event(cls, sdl_event: Any) -> Quit: return self def __repr__(self) -> str: - return "tcod.event.%s()" % (self.__class__.__name__,) + return f"tcod.event.{self.__class__.__name__}()" class KeyboardEvent(Event): - """ + """Base keyboard event. + Attributes: type (str): Will be "KEYDOWN" or "KEYUP", depending on the event. scancode (Scancode): The keyboard scan-code, this is the physical location @@ -345,7 +346,7 @@ class KeyboardEvent(Event): `scancode`, `sym`, and `mod` now use their respective enums. """ - def __init__(self, scancode: int, sym: int, mod: int, repeat: bool = False): + def __init__(self, scancode: int, sym: int, mod: int, repeat: bool = False) -> None: super().__init__() self.scancode = Scancode(scancode) self.sym = KeySym(sym) @@ -360,7 +361,7 @@ def from_sdl_event(cls, sdl_event: Any) -> Any: return self def __repr__(self) -> str: - return "tcod.event.%s(scancode=%r, sym=%r, mod=%s%s)" % ( + return "tcod.event.{}(scancode={!r}, sym={!r}, mod={}{})".format( self.__class__.__name__, self.scancode, self.sym, @@ -381,7 +382,8 @@ class KeyUp(KeyboardEvent): class MouseState(Event): - """ + """Mouse state. + Attributes: type (str): Always "MOUSESTATE". position (Point): The position coordinates of the mouse. @@ -404,10 +406,10 @@ class MouseState(Event): def __init__( self, - position: Tuple[int, int] = (0, 0), - tile: Optional[Tuple[int, int]] = (0, 0), + position: tuple[int, int] = (0, 0), + tile: tuple[int, int] | None = (0, 0), state: int = 0, - ): + ) -> None: super().__init__() self.position = Point(*position) self.__tile = Point(*tile) if tile is not None else None @@ -441,7 +443,7 @@ def tile(self) -> Point: return _verify_tile_coordinates(self.__tile) @tile.setter - def tile(self, xy: Tuple[int, int]) -> None: + def tile(self, xy: tuple[int, int]) -> None: warnings.warn( "The mouse.tile attribute is deprecated. Use mouse.position of the event returned by context.convert_event instead.", DeprecationWarning, @@ -450,7 +452,7 @@ def tile(self, xy: Tuple[int, int]) -> None: self.__tile = Point(*xy) def __repr__(self) -> str: - return ("tcod.event.%s(position=%r, tile=%r, state=%s)") % ( + return ("tcod.event.{}(position={!r}, tile={!r}, state={})").format( self.__class__.__name__, tuple(self.position), tuple(self.tile), @@ -467,7 +469,8 @@ def __str__(self) -> str: class MouseMotion(MouseState): - """ + """Mouse motion event. + Attributes: type (str): Always "MOUSEMOTION". position (Point): The pixel coordinates of the mouse. @@ -491,12 +494,12 @@ class MouseMotion(MouseState): def __init__( self, - position: Tuple[int, int] = (0, 0), - motion: Tuple[int, int] = (0, 0), - tile: Optional[Tuple[int, int]] = (0, 0), - tile_motion: Optional[Tuple[int, int]] = (0, 0), + position: tuple[int, int] = (0, 0), + motion: tuple[int, int] = (0, 0), + tile: tuple[int, int] | None = (0, 0), + tile_motion: tuple[int, int] | None = (0, 0), state: int = 0, - ): + ) -> None: super().__init__(position, tile, state) self.motion = Point(*motion) self.__tile_motion = Point(*tile_motion) if tile_motion is not None else None @@ -530,7 +533,7 @@ def tile_motion(self) -> Point: return _verify_tile_coordinates(self.__tile_motion) @tile_motion.setter - def tile_motion(self, xy: Tuple[int, int]) -> None: + def tile_motion(self, xy: tuple[int, int]) -> None: warnings.warn( "The mouse.tile_motion attribute is deprecated." " Use mouse.motion of the event returned by context.convert_event instead.", @@ -559,7 +562,7 @@ def from_sdl_event(cls, sdl_event: Any) -> MouseMotion: return self def __repr__(self) -> str: - return ("tcod.event.%s(position=%r, motion=%r, tile=%r, tile_motion=%r, state=%s)") % ( + return ("tcod.event.{}(position={!r}, motion={!r}, tile={!r}, tile_motion={!r}, state={})").format( self.__class__.__name__, tuple(self.position), tuple(self.motion), @@ -577,7 +580,8 @@ def __str__(self) -> str: class MouseButtonEvent(MouseState): - """ + """Mouse button event. + Attributes: type (str): Will be "MOUSEBUTTONDOWN" or "MOUSEBUTTONUP", depending on the event. @@ -597,10 +601,10 @@ class MouseButtonEvent(MouseState): def __init__( self, - pixel: Tuple[int, int] = (0, 0), - tile: Optional[Tuple[int, int]] = (0, 0), + pixel: tuple[int, int] = (0, 0), + tile: tuple[int, int] | None = (0, 0), button: int = 0, - ): + ) -> None: super().__init__(pixel, tile, button) @property @@ -617,7 +621,7 @@ def from_sdl_event(cls, sdl_event: Any) -> Any: pixel = button.x, button.y subtile = _pixel_to_tile(*pixel) if subtile is None: - tile: Optional[Tuple[int, int]] = None + tile: tuple[int, int] | None = None else: tile = int(subtile[0]), int(subtile[1]) self = cls(pixel, tile, button.button) @@ -625,7 +629,7 @@ def from_sdl_event(cls, sdl_event: Any) -> Any: return self def __repr__(self) -> str: - return "tcod.event.%s(position=%r, tile=%r, button=%s)" % ( + return "tcod.event.{}(position={!r}, tile={!r}, button={})".format( self.__class__.__name__, tuple(self.position), tuple(self.tile), @@ -650,7 +654,8 @@ class MouseButtonUp(MouseButtonEvent): class MouseWheel(Event): - """ + """Mouse wheel event. + Attributes: type (str): Always "MOUSEWHEEL". x (int): Horizontal scrolling. A positive value means scrolling right. @@ -661,7 +666,7 @@ class MouseWheel(Event): the Operating System. """ - def __init__(self, x: int, y: int, flipped: bool = False): + def __init__(self, x: int, y: int, flipped: bool = False) -> None: super().__init__() self.x = x self.y = y @@ -692,13 +697,14 @@ def __str__(self) -> str: class TextInput(Event): - """ + """SDL text input event. + Attributes: type (str): Always "TEXTINPUT". text (str): A Unicode string with the input. """ - def __init__(self, text: str): + def __init__(self, text: str) -> None: super().__init__() self.text = text @@ -709,10 +715,10 @@ def from_sdl_event(cls, sdl_event: Any) -> TextInput: return self def __repr__(self) -> str: - return "tcod.event.%s(text=%r)" % (self.__class__.__name__, self.text) + return f"tcod.event.{self.__class__.__name__}(text={self.text!r})" def __str__(self) -> str: - return "<%s, text=%r)" % (super().__str__().strip("<>"), self.text) + return "<{}, text={!r})".format(super().__str__().strip("<>"), self.text) class WindowEvent(Event): @@ -741,7 +747,7 @@ class WindowEvent(Event): """The current window event. This can be one of various options.""" @classmethod - def from_sdl_event(cls, sdl_event: Any) -> Union[WindowEvent, Undefined]: + def from_sdl_event(cls, sdl_event: Any) -> WindowEvent | Undefined: if sdl_event.window.event not in cls.__WINDOW_TYPES: return Undefined.from_sdl_event(sdl_event) event_type: Final = cls.__WINDOW_TYPES[sdl_event.window.event] @@ -759,7 +765,7 @@ def from_sdl_event(cls, sdl_event: Any) -> Union[WindowEvent, Undefined]: return self def __repr__(self) -> str: - return "tcod.event.%s(type=%r)" % (self.__class__.__name__, self.type) + return f"tcod.event.{self.__class__.__name__}(type={self.type!r})" __WINDOW_TYPES = { lib.SDL_WINDOWEVENT_SHOWN: "WindowShown", @@ -782,7 +788,8 @@ def __repr__(self) -> str: class WindowMoved(WindowEvent): - """ + """Window moved event. + Attributes: x (int): Movement on the x-axis. y (int): Movement on the y-axis. @@ -797,7 +804,7 @@ def __init__(self, x: int, y: int) -> None: self.y = y def __repr__(self) -> str: - return "tcod.event.%s(type=%r, x=%r, y=%r)" % ( + return "tcod.event.{}(type={!r}, x={!r}, y={!r})".format( self.__class__.__name__, self.type, self.x, @@ -805,7 +812,7 @@ def __repr__(self) -> str: ) def __str__(self) -> str: - return "<%s, x=%r, y=%r)" % ( + return "<{}, x={!r}, y={!r})".format( super().__str__().strip("<>"), self.x, self.y, @@ -813,7 +820,8 @@ def __str__(self) -> str: class WindowResized(WindowEvent): - """ + """Window resized event. + Attributes: width (int): The current width of the window. height (int): The current height of the window. @@ -828,7 +836,7 @@ def __init__(self, type: str, width: int, height: int) -> None: self.height = height def __repr__(self) -> str: - return "tcod.event.%s(type=%r, width=%r, height=%r)" % ( + return "tcod.event.{}(type={!r}, width={!r}, height={!r})".format( self.__class__.__name__, self.type, self.width, @@ -836,7 +844,7 @@ def __repr__(self) -> str: ) def __str__(self) -> str: - return "<%s, width=%r, height=%r)" % ( + return "<{}, width={!r}, height={!r})".format( super().__str__().strip("<>"), self.width, self.height, @@ -849,7 +857,7 @@ class JoystickEvent(Event): .. versionadded:: 13.8 """ - def __init__(self, type: str, which: int): + def __init__(self, type: str, which: int) -> None: super().__init__(type) self.which = which """The ID of the joystick this event is for.""" @@ -880,7 +888,7 @@ class JoystickAxis(JoystickEvent): which: int """The ID of the joystick this event is for.""" - def __init__(self, type: str, which: int, axis: int, value: int): + def __init__(self, type: str, which: int, axis: int, value: int) -> None: super().__init__(type, which) self.axis = axis """The index of the changed axis.""" @@ -914,7 +922,7 @@ class JoystickBall(JoystickEvent): which: int """The ID of the joystick this event is for.""" - def __init__(self, type: str, which: int, ball: int, dx: int, dy: int): + def __init__(self, type: str, which: int, ball: int, dx: int, dy: int) -> None: super().__init__(type, which) self.ball = ball """The index of the moved ball.""" @@ -952,7 +960,7 @@ class JoystickHat(JoystickEvent): which: int """The ID of the joystick this event is for.""" - def __init__(self, type: str, which: int, x: Literal[-1, 0, 1], y: Literal[-1, 0, 1]): + def __init__(self, type: str, which: int, x: Literal[-1, 0, 1], y: Literal[-1, 0, 1]) -> None: super().__init__(type, which) self.x = x """The new X direction of the hat.""" @@ -991,7 +999,7 @@ class JoystickButton(JoystickEvent): which: int """The ID of the joystick this event is for.""" - def __init__(self, type: str, which: int, button: int): + def __init__(self, type: str, which: int, button: int) -> None: super().__init__(type, which) self.button = button """The index of the button this event is for.""" @@ -1049,7 +1057,7 @@ class ControllerEvent(Event): .. versionadded:: 13.8 """ - def __init__(self, type: str, which: int): + def __init__(self, type: str, which: int) -> None: super().__init__(type) self.which = which """The ID of the joystick this event is for.""" @@ -1077,7 +1085,7 @@ class ControllerAxis(ControllerEvent): type: Final[Literal["CONTROLLERAXISMOTION"]] # type: ignore[misc] - def __init__(self, type: str, which: int, axis: tcod.sdl.joystick.ControllerAxis, value: int): + def __init__(self, type: str, which: int, axis: tcod.sdl.joystick.ControllerAxis, value: int) -> None: super().__init__(type, which) self.axis = axis """Which axis is being moved. One of :any:`ControllerAxis`.""" @@ -1114,7 +1122,7 @@ class ControllerButton(ControllerEvent): type: Final[Literal["CONTROLLERBUTTONDOWN", "CONTROLLERBUTTONUP"]] # type: ignore[misc] - def __init__(self, type: str, which: int, button: tcod.sdl.joystick.ControllerButton, pressed: bool): + def __init__(self, type: str, which: int, button: tcod.sdl.joystick.ControllerButton, pressed: bool) -> None: super().__init__(type, which) self.button = button """The button for this event. One of :any:`ControllerButton`.""" @@ -1164,9 +1172,7 @@ def from_sdl_event(cls, sdl_event: Any) -> ControllerDevice: class Undefined(Event): - """This class is a place holder for SDL events without their own tcod.event - class. - """ + """This class is a place holder for SDL events without their own tcod.event class.""" def __init__(self) -> None: super().__init__("") @@ -1183,7 +1189,7 @@ def __str__(self) -> str: return "" -_SDL_TO_CLASS_TABLE: Dict[int, Type[Event]] = { +_SDL_TO_CLASS_TABLE: dict[int, type[Event]] = { lib.SDL_QUIT: Quit, lib.SDL_KEYDOWN: KeyDown, lib.SDL_KEYUP: KeyUp, @@ -1232,7 +1238,7 @@ def get() -> Iterator[Any]: yield Undefined.from_sdl_event(sdl_event) -def wait(timeout: Optional[float] = None) -> Iterator[Any]: +def wait(timeout: float | None = None) -> Iterator[Any]: """Block until events exist, then return an event iterator. `timeout` is the maximum number of seconds to wait as a floating point @@ -1365,9 +1371,9 @@ def cmd_quit(self) -> None: tcod.console_flush() for event in tcod.event.wait(): state.dispatch(event) - ''' # noqa: E501 + ''' - def dispatch(self, event: Any) -> Optional[T]: + def dispatch(self, event: Any) -> T | None: """Send an event to an `ev_*` method. `*` will be the `event.type` attribute converted to lower-case. @@ -1387,7 +1393,7 @@ def dispatch(self, event: Any) -> Optional[T]: ) return None func_name = f"ev_{event.type.lower()}" - func: Optional[Callable[[Any], Optional[T]]] = getattr(self, func_name, None) + func: Callable[[Any], T | None] | None = getattr(self, func_name, None) if func is None: warnings.warn(f"{func_name} is missing from this EventDispatch object.", RuntimeWarning, stacklevel=2) return None @@ -1397,151 +1403,164 @@ def event_get(self) -> None: for event in get(): self.dispatch(event) - def event_wait(self, timeout: Optional[float]) -> None: + def event_wait(self, timeout: float | None) -> None: wait(timeout) self.event_get() - def ev_quit(self, event: tcod.event.Quit) -> Optional[T]: + def ev_quit(self, event: tcod.event.Quit) -> T | None: """Called when the termination of the program is requested.""" - def ev_keydown(self, event: tcod.event.KeyDown) -> Optional[T]: + def ev_keydown(self, event: tcod.event.KeyDown) -> T | None: """Called when a keyboard key is pressed or repeated.""" - def ev_keyup(self, event: tcod.event.KeyUp) -> Optional[T]: + def ev_keyup(self, event: tcod.event.KeyUp) -> T | None: """Called when a keyboard key is released.""" - def ev_mousemotion(self, event: tcod.event.MouseMotion) -> Optional[T]: + def ev_mousemotion(self, event: tcod.event.MouseMotion) -> T | None: """Called when the mouse is moved.""" - def ev_mousebuttondown(self, event: tcod.event.MouseButtonDown) -> Optional[T]: + def ev_mousebuttondown(self, event: tcod.event.MouseButtonDown) -> T | None: """Called when a mouse button is pressed.""" - def ev_mousebuttonup(self, event: tcod.event.MouseButtonUp) -> Optional[T]: + def ev_mousebuttonup(self, event: tcod.event.MouseButtonUp) -> T | None: """Called when a mouse button is released.""" - def ev_mousewheel(self, event: tcod.event.MouseWheel) -> Optional[T]: + def ev_mousewheel(self, event: tcod.event.MouseWheel) -> T | None: """Called when the mouse wheel is scrolled.""" - def ev_textinput(self, event: tcod.event.TextInput) -> Optional[T]: + def ev_textinput(self, event: tcod.event.TextInput) -> T | None: """Called to handle Unicode input.""" - def ev_windowshown(self, event: tcod.event.WindowEvent) -> Optional[T]: + def ev_windowshown(self, event: tcod.event.WindowEvent) -> T | None: """Called when the window is shown.""" - def ev_windowhidden(self, event: tcod.event.WindowEvent) -> Optional[T]: + def ev_windowhidden(self, event: tcod.event.WindowEvent) -> T | None: """Called when the window is hidden.""" - def ev_windowexposed(self, event: tcod.event.WindowEvent) -> Optional[T]: + def ev_windowexposed(self, event: tcod.event.WindowEvent) -> T | None: """Called when a window is exposed, and needs to be refreshed. This usually means a call to :any:`tcod.console_flush` is necessary. """ - def ev_windowmoved(self, event: tcod.event.WindowMoved) -> Optional[T]: + def ev_windowmoved(self, event: tcod.event.WindowMoved) -> T | None: """Called when the window is moved.""" - def ev_windowresized(self, event: tcod.event.WindowResized) -> Optional[T]: + def ev_windowresized(self, event: tcod.event.WindowResized) -> T | None: """Called when the window is resized.""" - def ev_windowsizechanged(self, event: tcod.event.WindowResized) -> Optional[T]: + def ev_windowsizechanged(self, event: tcod.event.WindowResized) -> T | None: """Called when the system or user changes the size of the window.""" - def ev_windowminimized(self, event: tcod.event.WindowEvent) -> Optional[T]: + def ev_windowminimized(self, event: tcod.event.WindowEvent) -> T | None: """Called when the window is minimized.""" - def ev_windowmaximized(self, event: tcod.event.WindowEvent) -> Optional[T]: + def ev_windowmaximized(self, event: tcod.event.WindowEvent) -> T | None: """Called when the window is maximized.""" - def ev_windowrestored(self, event: tcod.event.WindowEvent) -> Optional[T]: + def ev_windowrestored(self, event: tcod.event.WindowEvent) -> T | None: """Called when the window is restored.""" - def ev_windowenter(self, event: tcod.event.WindowEvent) -> Optional[T]: + def ev_windowenter(self, event: tcod.event.WindowEvent) -> T | None: """Called when the window gains mouse focus.""" - def ev_windowleave(self, event: tcod.event.WindowEvent) -> Optional[T]: + def ev_windowleave(self, event: tcod.event.WindowEvent) -> T | None: """Called when the window loses mouse focus.""" - def ev_windowfocusgained(self, event: tcod.event.WindowEvent) -> Optional[T]: + def ev_windowfocusgained(self, event: tcod.event.WindowEvent) -> T | None: """Called when the window gains keyboard focus.""" - def ev_windowfocuslost(self, event: tcod.event.WindowEvent) -> Optional[T]: + def ev_windowfocuslost(self, event: tcod.event.WindowEvent) -> T | None: """Called when the window loses keyboard focus.""" - def ev_windowclose(self, event: tcod.event.WindowEvent) -> Optional[T]: + def ev_windowclose(self, event: tcod.event.WindowEvent) -> T | None: """Called when the window manager requests the window to be closed.""" - def ev_windowtakefocus(self, event: tcod.event.WindowEvent) -> Optional[T]: + def ev_windowtakefocus(self, event: tcod.event.WindowEvent) -> T | None: pass - def ev_windowhittest(self, event: tcod.event.WindowEvent) -> Optional[T]: + def ev_windowhittest(self, event: tcod.event.WindowEvent) -> T | None: pass - def ev_joyaxismotion(self, event: tcod.event.JoystickAxis) -> Optional[T]: - """ + def ev_joyaxismotion(self, event: tcod.event.JoystickAxis) -> T | None: + """Called when a joystick analog is moved. + .. versionadded:: 13.8 """ - def ev_joyballmotion(self, event: tcod.event.JoystickBall) -> Optional[T]: - """ + def ev_joyballmotion(self, event: tcod.event.JoystickBall) -> T | None: + """Called when a joystick ball is moved. + .. versionadded:: 13.8 """ - def ev_joyhatmotion(self, event: tcod.event.JoystickHat) -> Optional[T]: - """ + def ev_joyhatmotion(self, event: tcod.event.JoystickHat) -> T | None: + """Called when a joystick hat is moved. + .. versionadded:: 13.8 """ - def ev_joybuttondown(self, event: tcod.event.JoystickButton) -> Optional[T]: - """ + def ev_joybuttondown(self, event: tcod.event.JoystickButton) -> T | None: + """Called when a joystick button is pressed. + .. versionadded:: 13.8 """ - def ev_joybuttonup(self, event: tcod.event.JoystickButton) -> Optional[T]: - """ + def ev_joybuttonup(self, event: tcod.event.JoystickButton) -> T | None: + """Called when a joystick button is released. + .. versionadded:: 13.8 """ - def ev_joydeviceadded(self, event: tcod.event.JoystickDevice) -> Optional[T]: - """ + def ev_joydeviceadded(self, event: tcod.event.JoystickDevice) -> T | None: + """Called when a joystick is added. + .. versionadded:: 13.8 """ - def ev_joydeviceremoved(self, event: tcod.event.JoystickDevice) -> Optional[T]: - """ + def ev_joydeviceremoved(self, event: tcod.event.JoystickDevice) -> T | None: + """Called when a joystick is removed. + .. versionadded:: 13.8 """ - def ev_controlleraxismotion(self, event: tcod.event.ControllerAxis) -> Optional[T]: - """ + def ev_controlleraxismotion(self, event: tcod.event.ControllerAxis) -> T | None: + """Called when a controller analog is moved. + .. versionadded:: 13.8 """ - def ev_controllerbuttondown(self, event: tcod.event.ControllerButton) -> Optional[T]: - """ + def ev_controllerbuttondown(self, event: tcod.event.ControllerButton) -> T | None: + """Called when a controller button is pressed. + .. versionadded:: 13.8 """ - def ev_controllerbuttonup(self, event: tcod.event.ControllerButton) -> Optional[T]: - """ + def ev_controllerbuttonup(self, event: tcod.event.ControllerButton) -> T | None: + """Called when a controller button is released. + .. versionadded:: 13.8 """ - def ev_controllerdeviceadded(self, event: tcod.event.ControllerDevice) -> Optional[T]: - """ + def ev_controllerdeviceadded(self, event: tcod.event.ControllerDevice) -> T | None: + """Called when a standard controller is added. + .. versionadded:: 13.8 """ - def ev_controllerdeviceremoved(self, event: tcod.event.ControllerDevice) -> Optional[T]: - """ + def ev_controllerdeviceremoved(self, event: tcod.event.ControllerDevice) -> T | None: + """Called when a standard controller is removed. + .. versionadded:: 13.8 """ - def ev_controllerdeviceremapped(self, event: tcod.event.ControllerDevice) -> Optional[T]: - """ + def ev_controllerdeviceremapped(self, event: tcod.event.ControllerDevice) -> T | None: + """Called when a standard controller is remapped. + .. versionadded:: 13.8 """ - def ev_(self, event: Any) -> Optional[T]: + def ev_(self, event: Any) -> T | None: pass @@ -1566,7 +1585,7 @@ def _sdl_event_watcher(userdata: Any, sdl_event: Any) -> int: _EventCallback = TypeVar("_EventCallback", bound=Callable[[Event], None]) -_event_watch_handles: Dict[Callable[[Event], None], Any] = {} # Callbacks and their FFI handles. +_event_watch_handles: dict[Callable[[Event], None], Any] = {} # Callbacks and their FFI handles. def add_watch(callback: _EventCallback) -> _EventCallback: @@ -2192,7 +2211,7 @@ def scancode(self) -> Scancode: return self @classmethod - def _missing_(cls, value: object) -> Optional[Scancode]: + def _missing_(cls, value: object) -> Scancode | None: if not isinstance(value, int): return None result = cls(0) @@ -2201,9 +2220,8 @@ def _missing_(cls, value: object) -> Optional[Scancode]: def __eq__(self, other: Any) -> bool: if isinstance(other, KeySym): - raise TypeError( - "Scancode and KeySym enums can not be compared directly." " Convert one or the other to the same type." - ) + msg = "Scancode and KeySym enums can not be compared directly. Convert one or the other to the same type." + raise TypeError(msg) return super().__eq__(other) def __hash__(self) -> int: @@ -2747,7 +2765,7 @@ def scancode(self) -> Scancode: return Scancode(lib.SDL_GetScancodeFromKey(self.value)) @classmethod - def _missing_(cls, value: object) -> Optional[KeySym]: + def _missing_(cls, value: object) -> KeySym | None: if not isinstance(value, int): return None result = cls(0) @@ -2756,9 +2774,8 @@ def _missing_(cls, value: object) -> Optional[KeySym]: def __eq__(self, other: Any) -> bool: if isinstance(other, Scancode): - raise TypeError( - "Scancode and KeySym enums can not be compared directly." " Convert one or the other to the same type." - ) + msg = "Scancode and KeySym enums can not be compared directly. Convert one or the other to the same type." + raise TypeError(msg) return super().__eq__(other) def __hash__(self) -> int: diff --git a/tcod/image.py b/tcod/image.py index 0f5760e9..85e315ea 100644 --- a/tcod/image.py +++ b/tcod/image.py @@ -12,7 +12,7 @@ from os import PathLike from pathlib import Path -from typing import Any, Dict, Tuple, Union +from typing import Any import numpy as np from numpy.typing import ArrayLike, NDArray @@ -22,8 +22,9 @@ from tcod.loader import ffi, lib -class Image(object): - """ +class Image: + """A libtcod image. + Args: width (int): Width of the new Image. height (int): Height of the new Image. @@ -33,7 +34,8 @@ class Image(object): height (int): Read only height of this Image. """ - def __init__(self, width: int, height: int): + def __init__(self, width: int, height: int) -> None: + """Initialize a blank image.""" self.width, self.height = width, height self.image_c = ffi.gc(lib.TCOD_image_new(width, height), lib.TCOD_image_delete) @@ -63,7 +65,7 @@ def from_array(cls, array: ArrayLike) -> Image: image_array[...] = array return image - def clear(self, color: Tuple[int, int, int]) -> None: + def clear(self, color: tuple[int, int, int]) -> None: """Fill this entire Image with color. Args: @@ -102,7 +104,7 @@ def scale(self, width: int, height: int) -> None: lib.TCOD_image_scale(self.image_c, width, height) self.width, self.height = width, height - def set_key_color(self, color: Tuple[int, int, int]) -> None: + def set_key_color(self, color: tuple[int, int, int]) -> None: """Set a color to be transparent during blitting functions. Args: @@ -138,7 +140,7 @@ def refresh_console(self, console: tcod.console.Console) -> None: """ lib.TCOD_image_refresh_console(self.image_c, _console(console)) - def _get_size(self) -> Tuple[int, int]: + def _get_size(self) -> tuple[int, int]: """Return the (width, height) for this Image. Returns: @@ -149,7 +151,7 @@ def _get_size(self) -> Tuple[int, int]: lib.TCOD_image_get_size(self.image_c, w, h) return w[0], h[0] - def get_pixel(self, x: int, y: int) -> Tuple[int, int, int]: + def get_pixel(self, x: int, y: int) -> tuple[int, int, int]: """Get the color of a pixel in this Image. Args: @@ -164,7 +166,7 @@ def get_pixel(self, x: int, y: int) -> Tuple[int, int, int]: color = lib.TCOD_image_get_pixel(self.image_c, x, y) return color.r, color.g, color.b - def get_mipmap_pixel(self, left: float, top: float, right: float, bottom: float) -> Tuple[int, int, int]: + def get_mipmap_pixel(self, left: float, top: float, right: float, bottom: float) -> tuple[int, int, int]: """Get the average color of a rectangle in this Image. Parameters should stay within the following limits: @@ -185,7 +187,7 @@ def get_mipmap_pixel(self, left: float, top: float, right: float, bottom: float) color = lib.TCOD_image_get_mipmap_pixel(self.image_c, left, top, right, bottom) return (color.r, color.g, color.b) - def put_pixel(self, x: int, y: int, color: Tuple[int, int, int]) -> None: + def put_pixel(self, x: int, y: int, color: tuple[int, int, int]) -> None: """Change a pixel on this Image. Args: @@ -295,7 +297,7 @@ def save_as(self, filename: str) -> None: lib.TCOD_image_save(self.image_c, filename.encode("utf-8")) @property - def __array_interface__(self) -> Dict[str, Any]: + def __array_interface__(self) -> dict[str, Any]: """Return an interface for this images pixel buffer. Use :any:`numpy.asarray` to get the read-write array of this Image. @@ -314,7 +316,8 @@ def __array_interface__(self) -> Dict[str, Any]: depth = 3 data = int(ffi.cast("size_t", self.image_c.mipmaps[0].buf)) else: - raise TypeError("Image has no initialized data.") + msg = "Image has no initialized data." + raise TypeError(msg) return { "shape": (self.height, self.width, depth), "typestr": "|u1", @@ -329,7 +332,7 @@ def _get_format_name(format: int) -> str: for attr in dir(lib): if not attr.startswith("SDL_PIXELFORMAT"): continue - if not getattr(lib, attr) == format: + if getattr(lib, attr) != format: continue return attr return str(format) @@ -340,7 +343,7 @@ def _get_format_name(format: int) -> str: " It's recommended to load images with a more complete image library such as python-Pillow or python-imageio.", category=PendingDeprecationWarning, ) -def load(filename: Union[str, PathLike[str]]) -> NDArray[np.uint8]: +def load(filename: str | PathLike[str]) -> NDArray[np.uint8]: """Load a PNG file as an RGBA array. `filename` is the name of the file to load. From 77f4a434f9a7eb2d88b030795d80d4d0156c7f0d Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 26 May 2023 23:14:57 -0700 Subject: [PATCH 164/194] Use Ruff to clean up libtcodpy. --- .vscode/settings.json | 3 + tcod/libtcodpy.py | 333 ++++++++++++++++++++++-------------------- 2 files changed, 178 insertions(+), 158 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 4274b1dd..2ab94f02 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -157,6 +157,7 @@ "guass", "heapify", "heightmap", + "heightmaps", "hflip", "hiddenimports", "HIGHDPI", @@ -197,6 +198,7 @@ "KBDILLUMDOWN", "KBDILLUMTOGGLE", "KBDILLUMUP", + "kernelsize", "keychar", "KEYDOWN", "keyname", @@ -403,6 +405,7 @@ "VRAM", "vsync", "WASD", + "waterlevel", "windowclose", "windowenter", "WINDOWEVENT", diff --git a/tcod/libtcodpy.py b/tcod/libtcodpy.py index a14403c3..b06572ec 100644 --- a/tcod/libtcodpy.py +++ b/tcod/libtcodpy.py @@ -2,11 +2,11 @@ from __future__ import annotations import atexit -import os import sys import threading import warnings -from typing import Any, AnyStr, Callable, Hashable, Iterable, Iterator, List, Optional, Sequence, Tuple, Union +from pathlib import Path +from typing import Any, AnyStr, Callable, Hashable, Iterable, Iterator, Sequence import numpy as np from numpy.typing import NDArray @@ -36,7 +36,7 @@ pending_deprecate, ) from tcod.color import Color -from tcod.constants import * # noqa: F4 +from tcod.constants import * # noqa: F403 from tcod.constants import ( BKGND_ADDA, BKGND_ALPH, @@ -50,6 +50,8 @@ ) from tcod.loader import ffi, lib +# ruff: noqa: ANN401 PLR0913 # Functions are too deprecated to make changes. + Bsp = tcod.bsp.BSP NB_FOV_ALGORITHMS = 13 @@ -70,7 +72,7 @@ def BKGND_ADDALPHA(a: int) -> int: return BKGND_ADDA | (int(a * 255) << 8) -class ConsoleBuffer(object): +class ConsoleBuffer: """Simple console that allows direct (fast) access to cells. Simplifies use of the "fill" functions. .. deprecated:: 6.0 @@ -247,7 +249,8 @@ def blit( if not dest: dest = tcod.console.Console._from_cdata(ffi.NULL) if dest.width != self.width or dest.height != self.height: - raise ValueError("ConsoleBuffer.blit: " "Destination console has an incorrect size.") + msg = "ConsoleBuffer.blit: Destination console has an incorrect size." + raise ValueError(msg) if fill_back: bg = dest.bg.ravel() @@ -264,7 +267,7 @@ def blit( class Dice(_CDataWrapper): - """ + """A libtcod dice object. Args: nb_dices (int): Number of dice. @@ -283,7 +286,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: DeprecationWarning, stacklevel=2, ) - super(Dice, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) if self.cdata == ffi.NULL: self._init(*args, **kwargs) @@ -318,7 +321,7 @@ def __str__(self) -> str: ) def __repr__(self) -> str: - return "%s(nb_dices=%r,nb_faces=%r,multiplier=%r,addsub=%r)" % ( + return "{}(nb_dices={!r},nb_faces={!r},multiplier={!r},addsub={!r})".format( self.__class__.__name__, self.nb_dices, self.nb_faces, @@ -332,7 +335,7 @@ def __repr__(self) -> str: class Key(_CDataWrapper): - r"""Key Event instance + r"""Key Event instance. Attributes: vk (int): TCOD_keycode_t key code @@ -377,7 +380,7 @@ def __init__( rctrl: bool = False, rmeta: bool = False, shift: bool = False, - ): + ) -> None: if isinstance(vk, ffi.CData): self.cdata = vk return @@ -401,7 +404,7 @@ def __getattr__(self, attr: str) -> Any: return ord(self.cdata.c) if attr == "text": return ffi.string(self.cdata.text).decode() - return super(Key, self).__getattr__(attr) + return super().__getattr__(attr) def __setattr__(self, attr: str, value: Any) -> None: if attr == "c": @@ -409,12 +412,12 @@ def __setattr__(self, attr: str, value: Any) -> None: elif attr == "text": self.cdata.text = value.encode() else: - super(Key, self).__setattr__(attr, value) + super().__setattr__(attr, value) def __repr__(self) -> str: """Return a representation of this Key object.""" params = [] - params.append("pressed=%r, vk=tcod.%s" % (self.pressed, _LOOKUP_VK[self.vk])) + params.append(f"pressed={self.pressed!r}, vk=tcod.{_LOOKUP_VK[self.vk]}") if self.c: params.append("c=ord(%r)" % chr(self.c)) if self.text: @@ -429,7 +432,7 @@ def __repr__(self) -> str: "rmeta", ]: if getattr(self, attr): - params.append("%s=%r" % (attr, getattr(self, attr))) + params.append(f"{attr}={getattr(self, attr)!r}") return "tcod.Key(%s)" % ", ".join(params) @property @@ -438,7 +441,7 @@ def key_p(self) -> Any: class Mouse(_CDataWrapper): - """Mouse event instance + """Mouse event instance. Attributes: x (int): Absolute mouse position at pixel x. @@ -473,7 +476,7 @@ def __init__( dcx: int = 0, dcy: int = 0, **kwargs: Any, - ): + ) -> None: if isinstance(x, ffi.CData): self.cdata = x return @@ -495,7 +498,7 @@ def __repr__(self) -> str: for attr in ["x", "y", "dx", "dy", "cx", "cy", "dcx", "dcy"]: if getattr(self, attr) == 0: continue - params.append("%s=%r" % (attr, getattr(self, attr))) + params.append(f"{attr}={getattr(self, attr)!r}") for attr in [ "lbutton", "rbutton", @@ -507,7 +510,7 @@ def __repr__(self) -> str: "wheel_down", ]: if getattr(self, attr): - params.append("%s=%r" % (attr, getattr(self, attr))) + params.append(f"{attr}={getattr(self, attr)!r}") return "tcod.Mouse(%s)" % ", ".join(params) @property @@ -515,7 +518,7 @@ def mouse_p(self) -> Any: return self.cdata -@deprecate("Call tcod.bsp.BSP(x, y, width, height) instead.") +@deprecate("Call tcod.bsp.BSP(x, y, width, height) instead.", FutureWarning) def bsp_new_with_size(x: int, y: int, w: int, h: int) -> tcod.bsp.BSP: """Create a new BSP instance with the given rectangle. @@ -534,35 +537,38 @@ def bsp_new_with_size(x: int, y: int, w: int, h: int) -> tcod.bsp.BSP: return Bsp(x, y, w, h) -@deprecate("Call node.split_once instead.") +@deprecate("Call node.split_once instead.", FutureWarning) def bsp_split_once(node: tcod.bsp.BSP, horizontal: bool, position: int) -> None: - """ + """Deprecated function. + .. deprecated:: 2.0 Use :any:`BSP.split_once` instead. """ node.split_once(horizontal, position) -@deprecate("Call node.split_recursive instead.") +@deprecate("Call node.split_recursive instead.", FutureWarning) def bsp_split_recursive( node: tcod.bsp.BSP, - randomizer: Optional[tcod.random.Random], + randomizer: tcod.random.Random | None, nb: int, minHSize: int, minVSize: int, maxHRatio: float, maxVRatio: float, ) -> None: - """ + """Deprecated function. + .. deprecated:: 2.0 Use :any:`BSP.split_recursive` instead. """ node.split_recursive(nb, minHSize, minVSize, maxHRatio, maxVRatio, randomizer) -@deprecate("Assign values via attribute instead.") +@deprecate("Assign values via attribute instead.", FutureWarning) def bsp_resize(node: tcod.bsp.BSP, x: int, y: int, w: int, h: int) -> None: - """ + """Deprecated function. + .. deprecated:: 2.0 Assign directly to :any:`BSP` attributes instead. """ @@ -573,8 +579,9 @@ def bsp_resize(node: tcod.bsp.BSP, x: int, y: int, w: int, h: int) -> None: @deprecate("Access children with 'node.children' instead.") -def bsp_left(node: tcod.bsp.BSP) -> Optional[tcod.bsp.BSP]: - """ +def bsp_left(node: tcod.bsp.BSP) -> tcod.bsp.BSP | None: + """Deprecated function. + .. deprecated:: 2.0 Use :any:`BSP.children` instead. """ @@ -582,44 +589,49 @@ def bsp_left(node: tcod.bsp.BSP) -> Optional[tcod.bsp.BSP]: @deprecate("Access children with 'node.children' instead.") -def bsp_right(node: tcod.bsp.BSP) -> Optional[tcod.bsp.BSP]: - """ +def bsp_right(node: tcod.bsp.BSP) -> tcod.bsp.BSP | None: + """Deprecated function. + .. deprecated:: 2.0 Use :any:`BSP.children` instead. """ return None if not node.children else node.children[1] -@deprecate("Get the parent with 'node.parent' instead.") -def bsp_father(node: tcod.bsp.BSP) -> Optional[tcod.bsp.BSP]: - """ +@deprecate("Get the parent with 'node.parent' instead.", FutureWarning) +def bsp_father(node: tcod.bsp.BSP) -> tcod.bsp.BSP | None: + """Deprecated function. + .. deprecated:: 2.0 Use :any:`BSP.parent` instead. """ return node.parent -@deprecate("Check for children with 'bool(node.children)' instead.") +@deprecate("Check for children with 'bool(node.children)' instead.", FutureWarning) def bsp_is_leaf(node: tcod.bsp.BSP) -> bool: - """ + """Deprecated function. + .. deprecated:: 2.0 Use :any:`BSP.children` instead. """ return not node.children -@deprecate("Use 'node.contains' instead.") +@deprecate("Use 'node.contains' instead.", FutureWarning) def bsp_contains(node: tcod.bsp.BSP, cx: int, cy: int) -> bool: - """ + """Deprecated function. + .. deprecated:: 2.0 Use :any:`BSP.contains` instead. """ return node.contains(cx, cy) -@deprecate("Use 'node.find_node' instead.") -def bsp_find_node(node: tcod.bsp.BSP, cx: int, cy: int) -> Optional[tcod.bsp.BSP]: - """ +@deprecate("Use 'node.find_node' instead.", FutureWarning) +def bsp_find_node(node: tcod.bsp.BSP, cx: int, cy: int) -> tcod.bsp.BSP | None: + """Deprecated function. + .. deprecated:: 2.0 Use :any:`BSP.find_node` instead. """ @@ -720,7 +732,7 @@ def bsp_remove_sons(node: tcod.bsp.BSP) -> None: node.children = () -@deprecate("libtcod objects are deleted automatically.") +@deprecate("libtcod objects are deleted automatically.", FutureWarning) def bsp_delete(node: tcod.bsp.BSP) -> None: """Exists for backward compatibility. Does nothing. @@ -734,7 +746,7 @@ def bsp_delete(node: tcod.bsp.BSP) -> None: @pending_deprecate() -def color_lerp(c1: Tuple[int, int, int], c2: Tuple[int, int, int], a: float) -> Color: +def color_lerp(c1: tuple[int, int, int], c2: tuple[int, int, int], a: float) -> Color: """Return the linear interpolation between two colors. ``a`` is the interpolation value, with 0 returning ``c1``, @@ -771,7 +783,7 @@ def color_set_hsv(c: Color, h: float, s: float, v: float) -> None: @pending_deprecate() -def color_get_hsv(c: Tuple[int, int, int]) -> Tuple[float, float, float]: +def color_get_hsv(c: tuple[int, int, int]) -> tuple[float, float, float]: """Return the (hue, saturation, value) of a color. Args: @@ -807,7 +819,7 @@ def color_scale_HSV(c: Color, scoef: float, vcoef: float) -> None: @pending_deprecate() -def color_gen_map(colors: Iterable[Tuple[int, int, int]], indexes: Iterable[int]) -> List[Color]: +def color_gen_map(colors: Iterable[tuple[int, int, int]], indexes: Iterable[int]) -> list[Color]: """Return a smoothly defined scale of colors. If ``indexes`` is [0, 3, 9] for example, the first color from ``colors`` @@ -842,11 +854,11 @@ def color_gen_map(colors: Iterable[Tuple[int, int, int]], indexes: Iterable[int] def console_init_root( w: int, h: int, - title: Optional[str] = None, + title: str | None = None, fullscreen: bool = False, - renderer: Optional[int] = None, + renderer: int | None = None, order: Literal["C", "F"] = "C", - vsync: Optional[bool] = None, + vsync: bool | None = None, ) -> tcod.console.Console: """Set up the primary display and return the root console. @@ -913,7 +925,7 @@ def console_init_root( """ if title is None: # Use the scripts filename as the title. - title = os.path.basename(sys.argv[0]) + title = Path(sys.argv[0]).name if renderer is None: renderer = tcod.constants.RENDERER_SDL2 elif renderer == tcod.constants.RENDERER_GLSL: @@ -972,9 +984,8 @@ def console_set_custom_font( Load fonts using :any:`tcod.tileset.load_tilesheet` instead. See :ref:`getting-started` for more info. """ - if not os.path.exists(_unicode(fontFile)): - raise RuntimeError("File not found:\n\t%s" % (str(os.path.realpath(fontFile)),)) - _check(lib.TCOD_console_set_custom_font(_bytes(fontFile), flags, nb_char_horiz, nb_char_vertic)) + path = Path(_unicode(fontFile)).resolve(strict=True) + _check(lib.TCOD_console_set_custom_font(path, flags, nb_char_horiz, nb_char_vertic)) @deprecate("Check `con.width` instead.") @@ -1157,17 +1168,13 @@ def console_credits_render(x: int, y: int, alpha: bool) -> bool: @deprecate("This function is not supported if contexts are being used.") def console_flush( - console: Optional[tcod.console.Console] = None, + console: tcod.console.Console | None = None, *, keep_aspect: bool = False, integer_scaling: bool = False, - snap_to_integer: Optional[bool] = None, - clear_color: Union[Tuple[int, int, int], Tuple[int, int, int, int]] = ( - 0, - 0, - 0, - ), - align: Tuple[float, float] = (0.5, 0.5), + snap_to_integer: bool | None = None, + clear_color: tuple[int, int, int] | tuple[int, int, int, int] = (0, 0, 0), + align: tuple[float, float] = (0.5, 0.5), ) -> None: """Update the display to represent the root consoles current state. @@ -1221,17 +1228,14 @@ def console_flush( "align_x": align[0], "align_y": align[1], } - if console is None: - console_p = ffi.NULL - else: - console_p = _console(console) + console_p = ffi.NULL if console is None else _console(console) with ffi.new("struct TCOD_ViewportOptions*", options) as viewport_opts: _check(lib.TCOD_console_flush_ex(console_p, viewport_opts)) # drawing on a console @deprecate("Set the `con.default_bg` attribute instead.") -def console_set_default_background(con: tcod.console.Console, col: Tuple[int, int, int]) -> None: +def console_set_default_background(con: tcod.console.Console, col: tuple[int, int, int]) -> None: """Change the default background color for a console. Args: @@ -1246,7 +1250,7 @@ def console_set_default_background(con: tcod.console.Console, col: Tuple[int, in @deprecate("Set the `con.default_fg` attribute instead.") -def console_set_default_foreground(con: tcod.console.Console, col: Tuple[int, int, int]) -> None: +def console_set_default_foreground(con: tcod.console.Console, col: tuple[int, int, int]) -> None: """Change the default foreground color for a console. Args: @@ -1282,7 +1286,7 @@ def console_put_char( con: tcod.console.Console, x: int, y: int, - c: Union[int, str], + c: int | str, flag: int = BKGND_DEFAULT, ) -> None: """Draw the character c at x,y using the default colors and a blend mode. @@ -1302,9 +1306,9 @@ def console_put_char_ex( con: tcod.console.Console, x: int, y: int, - c: Union[int, str], - fore: Tuple[int, int, int], - back: Tuple[int, int, int], + c: int | str, + fore: tuple[int, int, int], + back: tuple[int, int, int], ) -> None: """Draw the character c at x,y using the colors fore and back. @@ -1326,7 +1330,7 @@ def console_set_char_background( con: tcod.console.Console, x: int, y: int, - col: Tuple[int, int, int], + col: tuple[int, int, int], flag: int = BKGND_SET, ) -> None: """Change the background color of x,y to col using a blend mode. @@ -1343,7 +1347,7 @@ def console_set_char_background( @deprecate("Directly access a consoles foreground color with `console.fg`") -def console_set_char_foreground(con: tcod.console.Console, x: int, y: int, col: Tuple[int, int, int]) -> None: +def console_set_char_foreground(con: tcod.console.Console, x: int, y: int, col: tuple[int, int, int]) -> None: """Change the foreground color of x,y to col. Args: @@ -1361,7 +1365,7 @@ def console_set_char_foreground(con: tcod.console.Console, x: int, y: int, col: @deprecate("Directly access a consoles characters with `console.ch`") -def console_set_char(con: tcod.console.Console, x: int, y: int, c: Union[int, str]) -> None: +def console_set_char(con: tcod.console.Console, x: int, y: int, c: int | str) -> None: """Change the character at x,y to c, keeping the current colors. Args: @@ -1414,7 +1418,7 @@ def console_set_alignment(con: tcod.console.Console, alignment: int) -> None: Args: con (Console): Any Console instance. - alignment (int): + alignment (int): The libtcod alignment constant. .. deprecated:: 8.5 Set :any:`Console.default_alignment` instead. @@ -1613,7 +1617,7 @@ def console_print_frame( @pending_deprecate() -def console_set_color_control(con: int, fore: Tuple[int, int, int], back: Tuple[int, int, int]) -> None: +def console_set_color_control(con: int, fore: tuple[int, int, int], back: tuple[int, int, int]) -> None: """Configure :term:`color controls`. Args: @@ -1679,27 +1683,30 @@ def console_get_char(con: tcod.console.Console, x: int, y: int) -> int: return lib.TCOD_console_get_char(_console(con), x, y) # type: ignore -@deprecate("This function is not supported if contexts are being used.") -def console_set_fade(fade: int, fadingColor: Tuple[int, int, int]) -> None: - """ +@deprecate("This function is not supported if contexts are being used.", FutureWarning) +def console_set_fade(fade: int, fadingColor: tuple[int, int, int]) -> None: + """Deprecated function. + .. deprecated:: 11.13 This function is not supported by contexts. """ lib.TCOD_console_set_fade(fade, fadingColor) -@deprecate("This function is not supported if contexts are being used.") +@deprecate("This function is not supported if contexts are being used.", FutureWarning) def console_get_fade() -> int: - """ + """Deprecated function. + .. deprecated:: 11.13 This function is not supported by contexts. """ return int(lib.TCOD_console_get_fade()) -@deprecate("This function is not supported if contexts are being used.") +@deprecate("This function is not supported if contexts are being used.", FutureWarning) def console_get_fading_color() -> Color: - """ + """Deprecated function. + .. deprecated:: 11.13 This function is not supported by contexts. """ @@ -1734,7 +1741,8 @@ def console_wait_for_keypress(flush: bool) -> Key: @deprecate("Use the tcod.event.get function to check for events.") def console_check_for_keypress(flags: int = KEY_RELEASED) -> Key: - """ + """Return a recently pressed key. + .. deprecated:: 9.3 Use the :any:`tcod.event.get` function to check for events. @@ -1749,9 +1757,10 @@ def console_check_for_keypress(flags: int = KEY_RELEASED) -> Key: return key -@deprecate("Use tcod.event.get_keyboard_state to see if a key is held.") +@deprecate("Use tcod.event.get_keyboard_state to see if a key is held.", FutureWarning) def console_is_key_pressed(key: int) -> bool: - """ + """Return True if a key is held. + .. deprecated:: 12.7 Use :any:`tcod.event.get_keyboard_state` to check if a key is held. """ @@ -1787,9 +1796,8 @@ def console_from_file(filename: str) -> tcod.console.Console: Other formats are not actively supported. """ - if not os.path.exists(filename): - raise RuntimeError("File not found:\n\t%s" % (os.path.realpath(filename),)) - return tcod.console.Console._from_cdata(_check_p(lib.TCOD_console_from_file(filename.encode("utf-8")))) + path = Path(filename).resolve(strict=True) + return tcod.console.Console._from_cdata(_check_p(lib.TCOD_console_from_file(bytes(path)))) @deprecate("Call the `Console.blit` method instead.") @@ -1814,7 +1822,7 @@ def console_blit( @deprecate("Pass the key color to `Console.blit` instead of calling this function.") -def console_set_key_color(con: tcod.console.Console, col: Tuple[int, int, int]) -> None: +def console_set_key_color(con: tcod.console.Console, col: tuple[int, int, int]) -> None: """Set a consoles blit transparent color. .. deprecated:: 8.5 @@ -1879,7 +1887,8 @@ def console_fill_foreground( You should assign to :any:`tcod.console.Console.fg` instead. """ if len(r) != len(g) or len(r) != len(b): - raise TypeError("R, G and B must all have the same size.") + msg = "R, G and B must all have the same size." + raise TypeError(msg) if isinstance(r, np.ndarray) and isinstance(g, np.ndarray) and isinstance(b, np.ndarray): # numpy arrays, use numpy's ctypes functions r_ = np.ascontiguousarray(r, dtype=np.intc) @@ -1916,7 +1925,8 @@ def console_fill_background( You should assign to :any:`tcod.console.Console.bg` instead. """ if len(r) != len(g) or len(r) != len(b): - raise TypeError("R, G and B must all have the same size.") + msg = "R, G and B must all have the same size." + raise TypeError(msg) if isinstance(r, np.ndarray) and isinstance(g, np.ndarray) and isinstance(b, np.ndarray): # numpy arrays, use numpy's ctypes functions r_ = np.ascontiguousarray(r, dtype=np.intc) @@ -2015,19 +2025,17 @@ def console_save_xp(con: tcod.console.Console, filename: str, compress_level: in @deprecate("Use tcod.console.load_xp to load this file.") def console_from_xp(filename: str) -> tcod.console.Console: """Return a single console from a REXPaint `.xp` file.""" - if not os.path.exists(filename): - raise RuntimeError("File not found:\n\t%s" % (os.path.realpath(filename),)) - return tcod.console.Console._from_cdata(_check_p(lib.TCOD_console_from_xp(filename.encode("utf-8")))) + path = Path(filename).resolve(strict=True) + return tcod.console.Console._from_cdata(_check_p(lib.TCOD_console_from_xp(bytes(path)))) @deprecate("Use tcod.console.load_xp to load this file.") def console_list_load_xp( filename: str, -) -> Optional[List[tcod.console.Console]]: +) -> list[tcod.console.Console] | None: """Return a list of consoles from a REXPaint `.xp` file.""" - if not os.path.exists(filename): - raise RuntimeError("File not found:\n\t%s" % (os.path.realpath(filename),)) - tcod_list = lib.TCOD_console_list_from_xp(filename.encode("utf-8")) + path = Path(filename).resolve(strict=True) + tcod_list = lib.TCOD_console_list_from_xp(bytes(path)) if tcod_list == ffi.NULL: return None try: @@ -2064,6 +2072,7 @@ def path_new_using_map(m: tcod.map.Map, dcost: float = 1.41) -> tcod.path.AStar: m (Map): A Map instance. dcost (float): The path-finding cost of diagonal movement. Can be set to 0 to disable diagonal movement. + Returns: AStar: A new AStar instance. """ @@ -2087,6 +2096,7 @@ def path_new_using_function( userData (Any): dcost (float): A multiplier for the cost of diagonal movement. Can be set to 0 to disable diagonal movement. + Returns: AStar: A new AStar instance. """ @@ -2103,6 +2113,7 @@ def path_compute(p: tcod.path.AStar, ox: int, oy: int, dx: int, dy: int) -> bool oy (int): Starting y position. dx (int): Destination x position. dy (int): Destination y position. + Returns: bool: True if a valid path was found. Otherwise False. """ @@ -2110,13 +2121,14 @@ def path_compute(p: tcod.path.AStar, ox: int, oy: int, dx: int, dy: int) -> bool @pending_deprecate() -def path_get_origin(p: tcod.path.AStar) -> Tuple[int, int]: +def path_get_origin(p: tcod.path.AStar) -> tuple[int, int]: """Get the current origin position. This point moves when :any:`path_walk` returns the next x,y step. Args: p (AStar): An AStar instance. + Returns: Tuple[int, int]: An (x, y) point. """ @@ -2127,11 +2139,12 @@ def path_get_origin(p: tcod.path.AStar) -> Tuple[int, int]: @pending_deprecate() -def path_get_destination(p: tcod.path.AStar) -> Tuple[int, int]: +def path_get_destination(p: tcod.path.AStar) -> tuple[int, int]: """Get the current destination position. Args: p (AStar): An AStar instance. + Returns: Tuple[int, int]: An (x, y) point. """ @@ -2147,6 +2160,7 @@ def path_size(p: tcod.path.AStar) -> int: Args: p (AStar): An AStar instance. + Returns: int: Length of the path. """ @@ -2166,7 +2180,7 @@ def path_reverse(p: tcod.path.AStar) -> None: @pending_deprecate() -def path_get(p: tcod.path.AStar, idx: int) -> Tuple[int, int]: +def path_get(p: tcod.path.AStar, idx: int) -> tuple[int, int]: """Get a point on a path. Args: @@ -2185,6 +2199,7 @@ def path_is_empty(p: tcod.path.AStar) -> bool: Args: p (AStar): An AStar instance. + Returns: bool: True if a path is empty. Otherwise False. """ @@ -2192,7 +2207,7 @@ def path_is_empty(p: tcod.path.AStar) -> bool: @pending_deprecate() -def path_walk(p: tcod.path.AStar, recompute: bool) -> Union[Tuple[int, int], Tuple[None, None]]: +def path_walk(p: tcod.path.AStar, recompute: bool) -> tuple[int, int] | tuple[None, None]: """Return the next (x, y) point in a path, or (None, None) if it's empty. When ``recompute`` is True and a previously valid path reaches a point @@ -2201,6 +2216,7 @@ def path_walk(p: tcod.path.AStar, recompute: bool) -> Union[Tuple[int, int], Tup Args: p (AStar): An AStar instance. recompute (bool): Recompute the path automatically. + Returns: Union[Tuple[int, int], Tuple[None, None]]: A single (x, y) point, or (None, None) @@ -2262,7 +2278,7 @@ def dijkstra_reverse(p: tcod.path.Dijkstra) -> None: @pending_deprecate() -def dijkstra_get(p: tcod.path.Dijkstra, idx: int) -> Tuple[int, int]: +def dijkstra_get(p: tcod.path.Dijkstra, idx: int) -> tuple[int, int]: x = ffi.new("int *") y = ffi.new("int *") lib.TCOD_dijkstra_get(p._path_c, idx, x, y) @@ -2277,7 +2293,7 @@ def dijkstra_is_empty(p: tcod.path.Dijkstra) -> bool: @pending_deprecate() def dijkstra_path_walk( p: tcod.path.Dijkstra, -) -> Union[Tuple[int, int], Tuple[None, None]]: +) -> tuple[int, int] | tuple[None, None]: x = ffi.new("int *") y = ffi.new("int *") if lib.TCOD_dijkstra_path_walk(p._path_c, x, y): @@ -2301,7 +2317,8 @@ def _heightmap_cdata(array: NDArray[np.float32]) -> ffi.CData: if array.flags["F_CONTIGUOUS"]: array = array.transpose() if not array.flags["C_CONTIGUOUS"]: - raise ValueError("array must be a contiguous segment.") + msg = "array must be a contiguous segment." + raise ValueError(msg) if array.dtype != np.float32: raise ValueError("array dtype must be float32, not %r" % array.dtype) height, width = array.shape @@ -2333,7 +2350,8 @@ def heightmap_new(w: int, h: int, order: str = "C") -> NDArray[np.float32]: elif order == "F": return np.zeros((w, h), np.float32, order="F") else: - raise ValueError("Invalid order parameter, should be 'C' or 'F'.") + msg = "Invalid order parameter, should be 'C' or 'F'." + raise ValueError(msg) @deprecate("Assign to heightmaps as a NumPy array instead.") @@ -2358,7 +2376,8 @@ def heightmap_set_value(hm: NDArray[np.float32], x: int, y: int, value: float) - ) hm[x, y] = value else: - raise ValueError("This array is not contiguous.") + msg = "This array is not contiguous." + raise ValueError(msg) @deprecate("Add a scalar to an array using `hm[:] += value`") @@ -2404,7 +2423,7 @@ def heightmap_clear(hm: NDArray[np.float32]) -> None: @deprecate("Clamp array values using `hm.clip(mi, ma)`") def heightmap_clamp(hm: NDArray[np.float32], mi: float, ma: float) -> None: - """Clamp all values on this heightmap between ``mi`` and ``ma`` + """Clamp all values on this heightmap between ``mi`` and ``ma``. Args: hm (numpy.ndarray): A numpy.ndarray formatted for heightmap functions. @@ -2449,8 +2468,7 @@ def heightmap_lerp_hm( hm3: NDArray[np.float32], coef: float, ) -> None: - """Perform linear interpolation between two heightmaps storing the result - in ``hm3``. + """Perform linear interpolation between two heightmaps storing the result in ``hm3``. This is the same as doing ``hm3[:] = hm1[:] + (hm2[:] - hm1[:]) * coef`` @@ -2525,13 +2543,11 @@ def heightmap_add_hill(hm: NDArray[np.float32], x: float, y: float, radius: floa @pending_deprecate() def heightmap_dig_hill(hm: NDArray[np.float32], x: float, y: float, radius: float, height: float) -> None: - """ + """Dig a hill in a heightmap. - This function takes the highest value (if height > 0) or the lowest - (if height < 0) between the map and the hill. + This function takes the highest value (if height > 0) or the lowest (if height < 0) between the map and the hill. - It's main goal is to carve things in maps (like rivers) by digging hills - along a curve. + It's main goal is to carve things in maps (like rivers) by digging hills along a curve. Args: hm (numpy.ndarray): A numpy.ndarray formatted for heightmap functions. @@ -2549,7 +2565,7 @@ def heightmap_rain_erosion( nbDrops: int, erosionCoef: float, sedimentationCoef: float, - rnd: Optional[tcod.random.Random] = None, + rnd: tcod.random.Random | None = None, ) -> None: """Simulate the effect of rain drops on the terrain, resulting in erosion. @@ -2582,8 +2598,7 @@ def heightmap_kernel_transform( minLevel: float, maxLevel: float, ) -> None: - """Apply a generic transformation on the map, so that each resulting cell - value is the weighted sum of several neighbor cells. + """Apply a generic transformation on the map, so that each resulting cell value is the weighted sum of several neighbor cells. This can be used to smooth/sharpen the map. @@ -2635,7 +2650,7 @@ def heightmap_add_voronoi( nbPoints: Any, nbCoef: int, coef: Sequence[float], - rnd: Optional[tcod.random.Random] = None, + rnd: tcod.random.Random | None = None, ) -> None: """Add values from a Voronoi diagram to the heightmap. @@ -2721,7 +2736,7 @@ def heightmap_scale_fbm( delta: float, scale: float, ) -> None: - """Multiply the heighmap values with FBM noise. + """Multiply the heightmap values with FBM noise. Args: hm (numpy.ndarray): A numpy.ndarray formatted for heightmap functions. @@ -2755,8 +2770,8 @@ def heightmap_scale_fbm( @pending_deprecate() def heightmap_dig_bezier( hm: NDArray[np.float32], - px: Tuple[int, int, int, int], - py: Tuple[int, int, int, int], + px: tuple[int, int, int, int], + py: tuple[int, int, int, int], startRadius: float, startDepth: float, endRadius: float, @@ -2808,7 +2823,8 @@ def heightmap_get_value(hm: NDArray[np.float32], x: int, y: int) -> float: ) return hm[x, y] # type: ignore else: - raise ValueError("This array is not contiguous.") + msg = "This array is not contiguous." + raise ValueError(msg) @pending_deprecate() @@ -2842,7 +2858,7 @@ def heightmap_get_slope(hm: NDArray[np.float32], x: int, y: int) -> float: @pending_deprecate() -def heightmap_get_normal(hm: NDArray[np.float32], x: float, y: float, waterLevel: float) -> Tuple[float, float, float]: +def heightmap_get_normal(hm: NDArray[np.float32], x: float, y: float, waterLevel: float) -> tuple[float, float, float]: """Return the map normal at given coordinates. Args: @@ -2893,7 +2909,7 @@ def heightmap_has_land_on_border(hm: NDArray[np.float32], waterlevel: float) -> @deprecate("Use `hm.min()` and `hm.max()` instead.") -def heightmap_get_minmax(hm: NDArray[np.float32]) -> Tuple[float, float]: +def heightmap_get_minmax(hm: NDArray[np.float32]) -> tuple[float, float]: """Return the min and max values of this heightmap. Args: @@ -2928,7 +2944,7 @@ def image_new(width: int, height: int) -> tcod.image.Image: @pending_deprecate() -def image_clear(image: tcod.image.Image, col: Tuple[int, int, int]) -> None: +def image_clear(image: tcod.image.Image, col: tuple[int, int, int]) -> None: image.clear(col) @@ -2958,7 +2974,7 @@ def image_scale(image: tcod.image.Image, neww: int, newh: int) -> None: @pending_deprecate() -def image_set_key_color(image: tcod.image.Image, col: Tuple[int, int, int]) -> None: +def image_set_key_color(image: tcod.image.Image, col: tuple[int, int, int]) -> None: image.set_key_color(col) @@ -3008,22 +3024,22 @@ def image_refresh_console(image: tcod.image.Image, console: tcod.console.Console @pending_deprecate() -def image_get_size(image: tcod.image.Image) -> Tuple[int, int]: +def image_get_size(image: tcod.image.Image) -> tuple[int, int]: return image.width, image.height @pending_deprecate() -def image_get_pixel(image: tcod.image.Image, x: int, y: int) -> Tuple[int, int, int]: +def image_get_pixel(image: tcod.image.Image, x: int, y: int) -> tuple[int, int, int]: return image.get_pixel(x, y) @pending_deprecate() -def image_get_mipmap_pixel(image: tcod.image.Image, x0: float, y0: float, x1: float, y1: float) -> Tuple[int, int, int]: +def image_get_mipmap_pixel(image: tcod.image.Image, x0: float, y0: float, x1: float, y1: float) -> tuple[int, int, int]: return image.get_mipmap_pixel(x0, y0, x1, y1) @pending_deprecate() -def image_put_pixel(image: tcod.image.Image, x: int, y: int, col: Tuple[int, int, int]) -> None: +def image_put_pixel(image: tcod.image.Image, x: int, y: int, col: tuple[int, int, int]) -> None: image.put_pixel(x, y, col) @@ -3102,7 +3118,7 @@ def line_init(xo: int, yo: int, xd: int, yd: int) -> None: @deprecate("Use tcod.line_iter instead.") -def line_step() -> Union[Tuple[int, int], Tuple[None, None]]: +def line_step() -> tuple[int, int] | tuple[None, None]: """After calling line_init returns (x, y) points of the line. Once all points are exhausted this function will return (None, None) @@ -3156,8 +3172,8 @@ def line(xo: int, yo: int, xd: int, yd: int, py_callback: Callable[[int, int], b @deprecate("This function has been replaced by tcod.los.bresenham.") -def line_iter(xo: int, yo: int, xd: int, yd: int) -> Iterator[Tuple[int, int]]: - """returns an Iterable +def line_iter(xo: int, yo: int, xd: int, yd: int) -> Iterator[tuple[int, int]]: + """Returns an Iterable over a Bresenham line. This Iterable does not include the origin point. @@ -3183,7 +3199,7 @@ def line_iter(xo: int, yo: int, xd: int, yd: int) -> Iterator[Tuple[int, int]]: @deprecate("This function has been replaced by tcod.los.bresenham.") -def line_where(x1: int, y1: int, x2: int, y2: int, inclusive: bool = True) -> Tuple[NDArray[np.intc], NDArray[np.intc]]: +def line_where(x1: int, y1: int, x2: int, y2: int, inclusive: bool = True) -> tuple[NDArray[np.intc], NDArray[np.intc]]: """Return a NumPy index array following a Bresenham line. If `inclusive` is true then the start point is included in the result. @@ -3281,8 +3297,7 @@ def map_compute_fov( @deprecate("Use map.fov to check for this property.") def map_is_in_fov(m: tcod.map.Map, x: int, y: int) -> bool: - """Return True if the cell at x,y is lit by the last field-of-view - algorithm. + """Return True if the cell at x,y is lit by the last field-of-view algorithm. .. note:: This function is slow. @@ -3294,7 +3309,8 @@ def map_is_in_fov(m: tcod.map.Map, x: int, y: int) -> bool: @deprecate("Use map.transparent to check for this property.") def map_is_transparent(m: tcod.map.Map, x: int, y: int) -> bool: - """ + """Return True is a map cell is transparent. + .. note:: This function is slow. .. deprecated:: 4.5 @@ -3305,7 +3321,8 @@ def map_is_transparent(m: tcod.map.Map, x: int, y: int) -> bool: @deprecate("Use map.walkable to check for this property.") def map_is_walkable(m: tcod.map.Map, x: int, y: int) -> bool: - """ + """Return True is a map cell is walkable. + .. note:: This function is slow. .. deprecated:: 4.5 @@ -3365,7 +3382,7 @@ def mouse_get_status() -> Mouse: @pending_deprecate() -def namegen_parse(filename: str, random: Optional[tcod.random.Random] = None) -> None: +def namegen_parse(filename: str, random: tcod.random.Random | None = None) -> None: lib.TCOD_namegen_parse(_bytes(filename), random or ffi.NULL) @@ -3380,7 +3397,7 @@ def namegen_generate_custom(name: str, rule: str) -> str: @pending_deprecate() -def namegen_get_sets() -> List[str]: +def namegen_get_sets() -> list[str]: sets = lib.TCOD_namegen_get_sets() try: lst = [] @@ -3401,7 +3418,7 @@ def noise_new( dim: int, h: float = NOISE_DEFAULT_HURST, l: float = NOISE_DEFAULT_LACUNARITY, # noqa: E741 - random: Optional[tcod.random.Random] = None, + random: tcod.random.Random | None = None, ) -> tcod.noise.Noise: """Return a new Noise instance. @@ -3499,7 +3516,7 @@ def noise_delete(n: tcod.noise.Noise) -> None: def _unpack_union(type_: int, union: Any) -> Any: - """Unpack items from parser new_property (value_converter)""" + """Unpack items from parser new_property (value_converter).""" if type_ == lib.TCOD_TYPE_BOOL: return bool(union.b) elif type_ == lib.TCOD_TYPE_CHAR: @@ -3692,7 +3709,7 @@ def random_new_from_seed(seed: Hashable, algo: int = RNG_CMWC) -> tcod.random.Ra @pending_deprecate() -def random_set_distribution(rnd: Optional[tcod.random.Random], dist: int) -> None: +def random_set_distribution(rnd: tcod.random.Random | None, dist: int) -> None: """Change the distribution mode of a random number generator. Args: @@ -3703,7 +3720,7 @@ def random_set_distribution(rnd: Optional[tcod.random.Random], dist: int) -> Non @pending_deprecate() -def random_get_int(rnd: Optional[tcod.random.Random], mi: int, ma: int) -> int: +def random_get_int(rnd: tcod.random.Random | None, mi: int, ma: int) -> int: """Return a random integer in the range: ``mi`` <= n <= ``ma``. The result is affected by calls to :any:`random_set_distribution`. @@ -3720,7 +3737,7 @@ def random_get_int(rnd: Optional[tcod.random.Random], mi: int, ma: int) -> int: @pending_deprecate() -def random_get_float(rnd: Optional[tcod.random.Random], mi: float, ma: float) -> float: +def random_get_float(rnd: tcod.random.Random | None, mi: float, ma: float) -> float: """Return a random float in the range: ``mi`` <= n <= ``ma``. The result is affected by calls to :any:`random_set_distribution`. @@ -3738,7 +3755,7 @@ def random_get_float(rnd: Optional[tcod.random.Random], mi: float, ma: float) -> @deprecate("Call tcod.random_get_float instead.") -def random_get_double(rnd: Optional[tcod.random.Random], mi: float, ma: float) -> float: +def random_get_double(rnd: tcod.random.Random | None, mi: float, ma: float) -> float: """Return a random float in the range: ``mi`` <= n <= ``ma``. .. deprecated:: 2.0 @@ -3749,7 +3766,7 @@ def random_get_double(rnd: Optional[tcod.random.Random], mi: float, ma: float) - @pending_deprecate() -def random_get_int_mean(rnd: Optional[tcod.random.Random], mi: int, ma: int, mean: int) -> int: +def random_get_int_mean(rnd: tcod.random.Random | None, mi: int, ma: int, mean: int) -> int: """Return a random weighted integer in the range: ``mi`` <= n <= ``ma``. The result is affected by calls to :any:`random_set_distribution`. @@ -3767,7 +3784,7 @@ def random_get_int_mean(rnd: Optional[tcod.random.Random], mi: int, ma: int, mea @pending_deprecate() -def random_get_float_mean(rnd: Optional[tcod.random.Random], mi: float, ma: float, mean: float) -> float: +def random_get_float_mean(rnd: tcod.random.Random | None, mi: float, ma: float, mean: float) -> float: """Return a random weighted float in the range: ``mi`` <= n <= ``ma``. The result is affected by calls to :any:`random_set_distribution`. @@ -3786,7 +3803,7 @@ def random_get_float_mean(rnd: Optional[tcod.random.Random], mi: float, ma: floa @deprecate("Call tcod.random_get_float_mean instead.") -def random_get_double_mean(rnd: Optional[tcod.random.Random], mi: float, ma: float, mean: float) -> float: +def random_get_double_mean(rnd: tcod.random.Random | None, mi: float, ma: float, mean: float) -> float: """Return a random weighted float in the range: ``mi`` <= n <= ``ma``. .. deprecated:: 2.0 @@ -3797,7 +3814,7 @@ def random_get_double_mean(rnd: Optional[tcod.random.Random], mi: float, ma: flo @deprecate("Use the standard library 'copy' module instead.") -def random_save(rnd: Optional[tcod.random.Random]) -> tcod.random.Random: +def random_save(rnd: tcod.random.Random | None) -> tcod.random.Random: """Return a copy of a random number generator. .. deprecated:: 8.4 @@ -3813,7 +3830,7 @@ def random_save(rnd: Optional[tcod.random.Random]) -> tcod.random.Random: @deprecate("This function is deprecated.") -def random_restore(rnd: Optional[tcod.random.Random], backup: tcod.random.Random) -> None: +def random_restore(rnd: tcod.random.Random | None, backup: tcod.random.Random) -> None: """Restore a random number generator from a backed up copy. Args: @@ -3990,7 +4007,7 @@ def sys_get_renderer() -> int: # easy screenshots @deprecate("This function is not supported if contexts are being used.") -def sys_save_screenshot(name: Optional[str] = None) -> None: +def sys_save_screenshot(name: str | None = None) -> None: """Save a screenshot to a file. By default this will automatically save screenshots in the working @@ -4006,7 +4023,7 @@ def sys_save_screenshot(name: Optional[str] = None) -> None: This function is not supported by contexts. Use :any:`Context.save_screenshot` instead. """ - lib.TCOD_sys_save_screenshot(_bytes(name) if name is not None else ffi.NULL) + lib.TCOD_sys_save_screenshot(bytes(Path(name)) if name is not None else ffi.NULL) # custom fullscreen resolution @@ -4032,7 +4049,7 @@ def sys_force_fullscreen_resolution(width: int, height: int) -> None: @deprecate("This function is deprecated, which monitor is detected is ambiguous.") -def sys_get_current_resolution() -> Tuple[int, int]: +def sys_get_current_resolution() -> tuple[int, int]: """Return a monitors pixel resolution as (width, height). .. deprecated:: 11.13 @@ -4045,8 +4062,8 @@ def sys_get_current_resolution() -> Tuple[int, int]: @deprecate("This function is not supported if contexts are being used.") -def sys_get_char_size() -> Tuple[int, int]: - """Return the current fonts character size as (width, height) +def sys_get_char_size() -> tuple[int, int]: + """Return the current fonts character size as (width, height). Returns: Tuple[int,int]: The current font glyph size in (width, height) @@ -4121,7 +4138,7 @@ def _pycall_sdl_hook(sdl_surface: Any) -> None: @deprecate("Use tcod.event.get to check for events.") -def sys_check_for_event(mask: int, k: Optional[Key], m: Optional[Mouse]) -> int: +def sys_check_for_event(mask: int, k: Key | None, m: Mouse | None) -> int: """Check for and return an event. Args: @@ -4138,7 +4155,7 @@ def sys_check_for_event(mask: int, k: Optional[Key], m: Optional[Mouse]) -> int: @deprecate("Use tcod.event.wait to wait for events.") -def sys_wait_for_event(mask: int, k: Optional[Key], m: Optional[Mouse], flush: bool) -> int: +def sys_wait_for_event(mask: int, k: Key | None, m: Mouse | None, flush: bool) -> int: """Wait for an event then return. If flush is True then the buffer will be cleared before waiting. Otherwise From e0233a7b0e4d2733809ae7fe75c511166e12d494 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 26 May 2023 23:54:45 -0700 Subject: [PATCH 165/194] Clean up the remaining auto-fixes for the tcod package and scripts. --- scripts/generate_charmap_table.py | 3 + scripts/get_release_description.py | 2 + tcod/_internal.py | 2 +- tcod/console.py | 5 +- tcod/loader.py | 7 +- tcod/los.py | 4 +- tcod/map.py | 33 +++--- tcod/noise.py | 66 ++++++------ tcod/path.py | 163 +++++++++++++++-------------- tcod/random.py | 8 +- tcod/render.py | 6 +- tcod/sdl/__init__.py | 20 ++-- tcod/sdl/audio.py | 46 ++++---- tcod/sdl/joystick.py | 38 +++---- tcod/sdl/mouse.py | 29 ++--- tcod/sdl/render.py | 89 ++++++++-------- tcod/sdl/sys.py | 6 +- tcod/sdl/video.py | 51 +++++---- tcod/tileset.py | 67 ++++++------ 19 files changed, 330 insertions(+), 315 deletions(-) diff --git a/scripts/generate_charmap_table.py b/scripts/generate_charmap_table.py index 1f9f4639..2e2931b5 100755 --- a/scripts/generate_charmap_table.py +++ b/scripts/generate_charmap_table.py @@ -13,6 +13,8 @@ import tcod.tileset +# ruff: noqa: INP001 + def get_charmaps() -> Iterator[str]: """Return an iterator of the current character maps from tcod.tilest.""" @@ -53,6 +55,7 @@ def generate_table(charmap: Iterable[int]) -> str: def main() -> None: + """Main entry point.""" parser = argparse.ArgumentParser( description="Generate an RST table for a tcod character map.", ) diff --git a/scripts/get_release_description.py b/scripts/get_release_description.py index 7d47d9f9..0a2dffb3 100755 --- a/scripts/get_release_description.py +++ b/scripts/get_release_description.py @@ -5,6 +5,8 @@ import re from pathlib import Path +# ruff: noqa: INP001 + TAG_BANNER = r"## \[[\w.]*\] - \d+-\d+-\d+\n" RE_BODY = re.compile(rf".*?{TAG_BANNER}(.*?){TAG_BANNER}", re.DOTALL) diff --git a/tcod/_internal.py b/tcod/_internal.py index 694c9aaf..a9d2ba35 100644 --- a/tcod/_internal.py +++ b/tcod/_internal.py @@ -168,7 +168,7 @@ def __exit__( class _CDataWrapper: """A generally deprecated CData wrapper class used by libtcodpy.""" - def __init__(self, *args: Any, **kwargs: Any): # noqa: ANN401 + def __init__(self, *args: Any, **kwargs: Any) -> None: # noqa: ANN401 self.cdata = self._get_cdata_from_args(*args, **kwargs) if self.cdata is None: self.cdata = ffi.NULL diff --git a/tcod/console.py b/tcod/console.py index 77a75c0e..19114077 100644 --- a/tcod/console.py +++ b/tcod/console.py @@ -1,5 +1,4 @@ -""" -Libtcod consoles are a strictly tile-based representation of text and color. +"""Libtcod consoles are a strictly tile-based representation of text and color. To render a console you need a tileset and a window to render to. See :ref:`getting-started` for info on how to set those up. """ @@ -117,7 +116,7 @@ def __init__( height: int, order: Literal["C", "F"] = "C", buffer: NDArray[Any] | None = None, - ): + ) -> None: self._key_color: tuple[int, int, int] | None = None self._order = tcod._internal.verify_order(order) if buffer is not None: diff --git a/tcod/loader.py b/tcod/loader.py index 1a296b12..0027fb8c 100644 --- a/tcod/loader.py +++ b/tcod/loader.py @@ -5,7 +5,7 @@ import platform import sys from pathlib import Path -from typing import Any # noqa: F401 +from typing import Any import cffi @@ -54,7 +54,7 @@ def get_sdl_version() -> str: os.environ["PATH"] = f"""{Path(__file__).parent / get_architecture()}{os.pathsep}{os.environ["PATH"]}""" -class _Mock(object): +class _Mock: """Mock object needed for ReadTheDocs.""" @staticmethod @@ -64,7 +64,6 @@ def def_extern() -> Any: def __getattr__(self, attr: str) -> None: """Return None on any attribute.""" - return None def __bool__(self) -> bool: """Allow checking for this mock object at import time.""" @@ -80,7 +79,7 @@ def __bool__(self) -> bool: lib = ffi = _Mock() else: verify_dependencies() - from tcod._libtcod import ffi, lib # type: ignore # noqa: F401 + from tcod._libtcod import ffi, lib # type: ignore __sdl_version__ = get_sdl_version() diff --git a/tcod/los.py b/tcod/los.py index 9cbd87da..d815504f 100644 --- a/tcod/los.py +++ b/tcod/los.py @@ -1,7 +1,7 @@ """This modules holds functions for NumPy-based line of sight algorithms.""" from __future__ import annotations -from typing import Any, Tuple +from typing import Any import numpy as np from numpy.typing import NDArray @@ -9,7 +9,7 @@ from tcod.loader import ffi, lib -def bresenham(start: Tuple[int, int], end: Tuple[int, int]) -> NDArray[np.intc]: +def bresenham(start: tuple[int, int], end: tuple[int, int]) -> NDArray[np.intc]: """Return a thin Bresenham line as a NumPy array of shape (length, 2). `start` and `end` are the endpoints of the line. diff --git a/tcod/map.py b/tcod/map.py index 8ccc3b1f..e33a0984 100644 --- a/tcod/map.py +++ b/tcod/map.py @@ -1,11 +1,8 @@ -"""libtcod map attributes and field-of-view functions. - - -""" +"""libtcod map attributes and field-of-view functions.""" from __future__ import annotations import warnings -from typing import Any, Tuple +from typing import Any import numpy as np from numpy.typing import ArrayLike, NDArray @@ -16,7 +13,7 @@ from tcod.loader import ffi, lib -class Map(object): +class Map: """A map containing libtcod attributes. .. versionchanged:: 4.1 @@ -77,7 +74,7 @@ def __init__( width: int, height: int, order: Literal["C", "F"] = "C", - ): + ) -> None: warnings.warn( "This class may perform poorly and is no longer needed.", DeprecationWarning, @@ -140,15 +137,15 @@ def compute_fov( """ if not (0 <= x < self.width and 0 <= y < self.height): warnings.warn( - "Index (%r, %r) is outside of this maps shape (%r, %r)." - "\nThis will raise an error in future versions." % (x, y, self.width, self.height), + "Index ({}, {}) is outside of this maps shape ({}, {})." + "\nThis will raise an error in future versions.".format(x, y, self.width, self.height), RuntimeWarning, stacklevel=2, ) lib.TCOD_map_compute_fov(self.map_c, x, y, radius, light_walls, algorithm) - def __setstate__(self, state: Any) -> None: + def __setstate__(self, state: dict[str, Any]) -> None: if "_Map__buffer" not in state: # deprecated # remove this check on major version update self.__buffer = np.zeros((state["height"], state["width"], 3), dtype=np.bool_) @@ -157,12 +154,10 @@ def __setstate__(self, state: Any) -> None: self.__buffer[:, :, 2] = state["buffer"] & 0x04 del state["buffer"] state["_order"] = "F" - if "_order" not in state: # remove this check on major version update - raise RuntimeError("This Map was saved with a bad version of tdl.") self.__dict__.update(state) self.map_c = self.__as_cdata() - def __getstate__(self) -> Any: + def __getstate__(self) -> dict[str, Any]: state = self.__dict__.copy() del state["map_c"] return state @@ -170,7 +165,7 @@ def __getstate__(self) -> Any: def compute_fov( transparency: ArrayLike, - pov: Tuple[int, int], + pov: tuple[int, int], radius: int = 0, light_walls: bool = True, algorithm: int = tcod.constants.FOV_RESTRICTIVE, @@ -240,14 +235,12 @@ def compute_fov( if len(transparency.shape) != 2: raise TypeError("transparency must be an array of 2 dimensions" " (shape is %r)" % transparency.shape) if isinstance(pov, int): - raise TypeError( - "The tcod.map.compute_fov function has changed. The `x` and `y`" - " parameters should now be given as a single tuple." - ) + msg = "The tcod.map.compute_fov function has changed. The `x` and `y` parameters should now be given as a single tuple." + raise TypeError(msg) if not (0 <= pov[0] < transparency.shape[0] and 0 <= pov[1] < transparency.shape[1]): warnings.warn( - "Given pov index %r is outside the array of shape %r." - "\nThis will raise an error in future versions." % (pov, transparency.shape), + "Given pov index {!r} is outside the array of shape {!r}." + "\nThis will raise an error in future versions.".format(pov, transparency.shape), RuntimeWarning, stacklevel=2, ) diff --git a/tcod/noise.py b/tcod/noise.py index 94178b56..843f60b9 100644 --- a/tcod/noise.py +++ b/tcod/noise.py @@ -30,12 +30,12 @@ [ 92, 35, 33, 71, 90], [ 76, 54, 85, 144, 164], [ 63, 94, 159, 209, 203]], dtype=uint8) -""" # noqa: E501 +""" from __future__ import annotations import enum import warnings -from typing import Any, List, Optional, Sequence, Tuple, Union +from typing import Any, Sequence import numpy as np from numpy.typing import ArrayLike, NDArray @@ -43,7 +43,6 @@ import tcod.constants import tcod.random -from tcod._internal import deprecate from tcod.loader import ffi, lib @@ -92,14 +91,15 @@ def __getattr__(name: str) -> Implementation: if name in Implementation.__members__: warnings.warn( f"'tcod.noise.{name}' is deprecated," f" use 'tcod.noise.Implementation.{name}' instead.", - DeprecationWarning, + FutureWarning, stacklevel=2, ) return Implementation[name] - raise AttributeError(f"module {__name__} has no attribute {name}") + msg = f"module {__name__} has no attribute {name}" + raise AttributeError(msg) -class Noise(object): +class Noise: """A configurable noise sampler. The ``hurst`` exponent describes the raggedness of the resultant noise, @@ -130,10 +130,11 @@ def __init__( hurst: float = 0.5, lacunarity: float = 2.0, octaves: float = 4, - seed: Optional[Union[int, tcod.random.Random]] = None, - ): + seed: int | tcod.random.Random | None = None, + ) -> None: if not 0 < dimensions <= 4: - raise ValueError("dimensions must be in range 0 < n <= 4, got %r" % (dimensions,)) + msg = f"dimensions must be in range 0 < n <= 4, got {dimensions}" + raise ValueError(msg) self._seed = seed self._random = self.__rng_from_seed(seed) _random_c = self._random.random_c @@ -149,7 +150,7 @@ def __init__( self.implementation = implementation # sanity check @staticmethod - def __rng_from_seed(seed: Union[None, int, tcod.random.Random]) -> tcod.random.Random: + def __rng_from_seed(seed: None | int | tcod.random.Random) -> tcod.random.Random: if seed is None or isinstance(seed, int): return tcod.random.Random(seed=seed, algorithm=tcod.random.MERSENNE_TWISTER) return seed @@ -174,11 +175,6 @@ def __repr__(self) -> str: def dimensions(self) -> int: return int(self._tdl_noise_c.dimensions) - @property - @deprecate("This is a misspelling of 'dimensions'.", FutureWarning) - def dimentions(self) -> int: - return self.dimensions - @property def algorithm(self) -> int: noise_type = self.noise_c.noise_type @@ -195,7 +191,8 @@ def implementation(self) -> int: @implementation.setter def implementation(self, value: int) -> None: if not 0 <= value < 3: - raise ValueError("%r is not a valid implementation. " % (value,)) + msg = f"{value!r} is not a valid implementation. " + raise ValueError(msg) self._tdl_noise_c.implementation = value @property @@ -242,7 +239,8 @@ def __getitem__(self, indexes: Any) -> NDArray[np.float32]: c_input = [ffi.NULL, ffi.NULL, ffi.NULL, ffi.NULL] for i, index in enumerate(indexes): if index.dtype.type == np.object_: - raise TypeError("Index arrays can not be of dtype np.object_.") + msg = "Index arrays can not be of dtype np.object_." + raise TypeError(msg) indexes[i] = np.ascontiguousarray(index, dtype=np.float32) c_input[i] = ffi.from_buffer("float*", indexes[i]) @@ -296,12 +294,12 @@ def sample_mgrid(self, mgrid: ArrayLike) -> NDArray[np.float32]: """ mgrid = np.ascontiguousarray(mgrid, np.float32) if mgrid.shape[0] != self.dimensions: - raise ValueError( - "mgrid.shape[0] must equal self.dimensions, " "%r[0] != %r" % (mgrid.shape, self.dimensions) - ) + msg = f"mgrid.shape[0] must equal self.dimensions, {mgrid.shape!r}[0] != {self.dimensions!r}" + raise ValueError(msg) out: np.ndarray[Any, np.dtype[np.float32]] = np.ndarray(mgrid.shape[1:], np.float32) if mgrid.shape[1:] != out.shape: - raise ValueError("mgrid.shape[1:] must equal out.shape, " "%r[1:] != %r" % (mgrid.shape, out.shape)) + msg = f"mgrid.shape[1:] must equal out.shape, {mgrid.shape!r}[1:] != {out.shape!r}" + raise ValueError(msg) lib.NoiseSampleMeshGrid( self._tdl_noise_c, out.size, @@ -323,8 +321,9 @@ def sample_ogrid(self, ogrid: Sequence[ArrayLike]) -> NDArray[np.float32]: The ``dtype`` is `numpy.float32`. """ if len(ogrid) != self.dimensions: - raise ValueError("len(ogrid) must equal self.dimensions, " "%r != %r" % (len(ogrid), self.dimensions)) - ogrids: List[NDArray[np.float32]] = [np.ascontiguousarray(array, np.float32) for array in ogrid] + msg = f"len(ogrid) must equal self.dimensions, {len(ogrid)!r} != {self.dimensions!r}" + raise ValueError(msg) + ogrids: list[NDArray[np.float32]] = [np.ascontiguousarray(array, np.float32) for array in ogrid] out: np.ndarray[Any, np.dtype[np.float32]] = np.ndarray([array.size for array in ogrids], np.float32) lib.NoiseSampleOpenMeshGrid( self._tdl_noise_c, @@ -335,7 +334,7 @@ def sample_ogrid(self, ogrid: Sequence[ArrayLike]) -> NDArray[np.float32]: ) return out - def __getstate__(self) -> Any: + def __getstate__(self) -> dict[str, Any]: state = self.__dict__.copy() if self.dimensions < 4 and self.noise_c.waveletTileData == ffi.NULL: # Trigger a side effect of wavelet, so that copies will be synced. @@ -366,7 +365,7 @@ def __getstate__(self) -> Any: } return state - def __setstate__(self, state: Any) -> None: + def __setstate__(self, state: dict[str, Any]) -> None: if isinstance(state, tuple): # deprecated format return self._setstate_old(state) # unpack wavelet tile data if it exists @@ -384,6 +383,7 @@ def __setstate__(self, state: Any) -> None: state["_tdl_noise_c"]["noise"] = state["noise_c"] state["_tdl_noise_c"] = ffi.new("TDLNoise*", state["_tdl_noise_c"]) self.__dict__.update(state) + return None def _setstate_old(self, state: Any) -> None: self._random = state[0] @@ -403,11 +403,11 @@ def _setstate_old(self, state: Any) -> None: def grid( - shape: Tuple[int, ...], - scale: Union[Tuple[float, ...], float], - origin: Optional[Tuple[int, ...]] = None, + shape: tuple[int, ...], + scale: tuple[float, ...] | float, + origin: tuple[int, ...] | None = None, indexing: Literal["ij", "xy"] = "xy", -) -> Tuple[NDArray[Any], ...]: +) -> tuple[NDArray[Any], ...]: """Generate a mesh-grid of sample points to use with noise sampling. Args: @@ -444,14 +444,16 @@ def grid( dtype=float32) .. versionadded:: 12.2 - """ # noqa: E501 + """ if isinstance(scale, float): scale = (scale,) * len(shape) if origin is None: origin = (0,) * len(shape) if len(shape) != len(scale): - raise TypeError("shape must have the same length as scale") + msg = "shape must have the same length as scale" + raise TypeError(msg) if len(shape) != len(origin): - raise TypeError("shape must have the same length as origin") + msg = "shape must have the same length as origin" + raise TypeError(msg) indexes = (np.arange(i_shape) * i_scale + i_origin for i_shape, i_scale, i_origin in zip(shape, scale, origin)) return tuple(np.meshgrid(*indexes, copy=False, sparse=True, indexing=indexing)) diff --git a/tcod/path.py b/tcod/path.py index db42fba5..e7b648c5 100644 --- a/tcod/path.py +++ b/tcod/path.py @@ -20,13 +20,12 @@ import functools import itertools import warnings -from typing import Any, Callable, Dict, List, Optional, Tuple, Union +from typing import Any, Callable import numpy as np from numpy.typing import ArrayLike, NDArray from typing_extensions import Literal -import tcod.map # noqa: F401 from tcod._internal import _check from tcod.loader import ffi, lib @@ -65,7 +64,7 @@ def _get_path_cost_func( return ffi.cast("TCOD_path_func_t", ffi.addressof(lib, name)) # type: ignore -class _EdgeCostFunc(object): +class _EdgeCostFunc: """Generic edge-cost function factory. `userdata` is the custom userdata to send to the C call. @@ -75,16 +74,16 @@ class _EdgeCostFunc(object): _CALLBACK_P = lib._pycall_path_old - def __init__(self, userdata: Any, shape: Tuple[int, int]) -> None: + def __init__(self, userdata: Any, shape: tuple[int, int]) -> None: self._userdata = userdata self.shape = shape - def get_tcod_path_ffi(self) -> Tuple[Any, Any, Tuple[int, int]]: + def get_tcod_path_ffi(self) -> tuple[Any, Any, tuple[int, int]]: """Return (C callback, userdata handle, shape).""" return self._CALLBACK_P, ffi.new_handle(self._userdata), self.shape def __repr__(self) -> str: - return "%s(%r, shape=%r)" % ( + return "{}({!r}, shape={!r})".format( self.__class__.__name__, self._userdata, self.shape, @@ -109,10 +108,10 @@ class EdgeCostCallback(_EdgeCostFunc): def __init__( self, callback: Callable[[int, int, int, int], float], - shape: Tuple[int, int], - ): + shape: tuple[int, int], + ) -> None: self.callback = callback - super(EdgeCostCallback, self).__init__(callback, shape) + super().__init__(callback, shape) class NodeCostArray(np.ndarray): # type: ignore @@ -139,16 +138,15 @@ def __new__(cls, array: ArrayLike) -> NodeCostArray: return self def __repr__(self) -> str: - return "%s(%r)" % ( - self.__class__.__name__, - repr(self.view(np.ndarray)), - ) + return f"{self.__class__.__name__}({repr(self.view(np.ndarray))!r})" - def get_tcod_path_ffi(self) -> Tuple[Any, Any, Tuple[int, int]]: + def get_tcod_path_ffi(self) -> tuple[Any, Any, tuple[int, int]]: if len(self.shape) != 2: - raise ValueError("Array must have a 2d shape, shape is %r" % (self.shape,)) + msg = f"Array must have a 2d shape, shape is {self.shape!r}" + raise ValueError(msg) if self.dtype.type not in self._C_ARRAY_CALLBACKS: - raise ValueError("dtype must be one of %r, dtype is %r" % (self._C_ARRAY_CALLBACKS.keys(), self.dtype.type)) + msg = f"dtype must be one of {self._C_ARRAY_CALLBACKS.keys()!r}, dtype is {self.dtype.type!r}" + raise ValueError(msg) array_type, callback = self._C_ARRAY_CALLBACKS[self.dtype.type] userdata = ffi.new( @@ -158,10 +156,10 @@ def get_tcod_path_ffi(self) -> Tuple[Any, Any, Tuple[int, int]]: return callback, userdata, (self.shape[0], self.shape[1]) -class _PathFinder(object): +class _PathFinder: """A class sharing methods used by AStar and Dijkstra.""" - def __init__(self, cost: Any, diagonal: float = 1.41): + def __init__(self, cost: Any, diagonal: float = 1.41) -> None: self.cost = cost self.diagonal = diagonal self._path_c: Any = None @@ -198,11 +196,7 @@ def __init__(self, cost: Any, diagonal: float = 1.41): ) def __repr__(self) -> str: - return "%s(cost=%r, diagonal=%r)" % ( - self.__class__.__name__, - self.cost, - self.diagonal, - ) + return f"{self.__class__.__name__}(cost={self.cost!r}, diagonal={self.diagonal!r})" def __getstate__(self) -> Any: state = self.__dict__.copy() @@ -222,14 +216,15 @@ def __setstate__(self, state: Any) -> None: class AStar(_PathFinder): - """ + """The older libtcod A* pathfinder. + Args: cost (Union[tcod.map.Map, numpy.ndarray, Any]): diagonal (float): Multiplier for diagonal movement. A value of 0 will disable diagonal movement entirely. """ - def get_path(self, start_x: int, start_y: int, goal_x: int, goal_y: int) -> List[Tuple[int, int]]: + def get_path(self, start_x: int, start_y: int, goal_x: int, goal_y: int) -> list[tuple[int, int]]: """Return a list of (x, y) steps to reach the goal point, if possible. Args: @@ -237,6 +232,7 @@ def get_path(self, start_x: int, start_y: int, goal_x: int, goal_y: int) -> List start_y (int): Starting Y position. goal_x (int): Destination X position. goal_y (int): Destination Y position. + Returns: List[Tuple[int, int]]: A list of points, or an empty list if there is no valid path. @@ -251,7 +247,8 @@ def get_path(self, start_x: int, start_y: int, goal_x: int, goal_y: int) -> List class Dijkstra(_PathFinder): - """ + """The older libtcod Dijkstra pathfinder. + Args: cost (Union[tcod.map.Map, numpy.ndarray, Any]): diagonal (float): Multiplier for diagonal movement. @@ -266,7 +263,7 @@ def set_goal(self, x: int, y: int) -> None: """Set the goal point and recompute the Dijkstra path-finder.""" lib.TCOD_dijkstra_compute(self._path_c, x, y) - def get_path(self, x: int, y: int) -> List[Tuple[int, int]]: + def get_path(self, x: int, y: int) -> list[tuple[int, int]]: """Return a list of (x, y) steps to reach the goal point, if possible.""" lib.TCOD_dijkstra_path_set(self._path_c, x, y) path = [] @@ -292,7 +289,7 @@ def get_path(self, x: int, y: int) -> List[Tuple[int, int]]: def maxarray( - shape: Tuple[int, ...], + shape: tuple[int, ...], dtype: Any = np.int32, order: Literal["C", "F"] = "C", ) -> NDArray[Any]: @@ -314,10 +311,11 @@ def maxarray( return np.full(shape, np.iinfo(dtype).max, dtype, order) -def _export_dict(array: NDArray[Any]) -> Dict[str, Any]: +def _export_dict(array: NDArray[Any]) -> dict[str, Any]: """Convert a NumPy array into a format compatible with CFFI.""" if array.dtype.type not in _INT_TYPES: - raise TypeError("dtype was %s, but must be one of %s." % (array.dtype.type, tuple(_INT_TYPES.keys()))) + msg = f"dtype was {array.dtype.type}, but must be one of {tuple(_INT_TYPES.keys())}." + raise TypeError(msg) return { "type": _INT_TYPES[array.dtype.type], "ndim": array.ndim, @@ -332,7 +330,7 @@ def _export(array: NDArray[Any]) -> Any: return ffi.new("struct NArray*", _export_dict(array)) -def _compile_cost_edges(edge_map: Any) -> Tuple[Any, int]: +def _compile_cost_edges(edge_map: Any) -> tuple[Any, int]: """Return an edge_cost array using an integer map.""" edge_map = np.array(edge_map, copy=True) if edge_map.ndim != 2: @@ -353,11 +351,11 @@ def _compile_cost_edges(edge_map: Any) -> Tuple[Any, int]: def dijkstra2d( distance: ArrayLike, cost: ArrayLike, - cardinal: Optional[int] = None, - diagonal: Optional[int] = None, + cardinal: int | None = None, + diagonal: int | None = None, *, edge_map: Any = None, - out: Optional[np.ndarray] = ..., # type: ignore + out: np.ndarray | None = ..., # type: ignore ) -> NDArray[Any]: """Return the computed distance of all nodes on a 2D Dijkstra grid. @@ -491,14 +489,17 @@ def dijkstra2d( out[...] = dist if dist.shape != out.shape: - raise TypeError("distance and output must have the same shape %r != %r" % (dist.shape, out.shape)) + msg = f"distance and output must have the same shape {dist.shape!r} != {out.shape!r}" + raise TypeError(msg) cost = np.asarray(cost) if dist.shape != cost.shape: - raise TypeError("output and cost must have the same shape %r != %r" % (out.shape, cost.shape)) + msg = f"output and cost must have the same shape {out.shape!r} != {cost.shape!r}" + raise TypeError(msg) c_dist = _export(out) if edge_map is not None: if cardinal is not None or diagonal is not None: - raise TypeError("`edge_map` can not be set at the same time as" " `cardinal` or `diagonal`.") + msg = "`edge_map` can not be set at the same time as `cardinal` or `diagonal`." + raise TypeError(msg) c_edges, n_edges = _compile_cost_edges(edge_map) _check(lib.dijkstra2d(c_dist, _export(cost), n_edges, c_edges)) else: @@ -510,7 +511,7 @@ def dijkstra2d( return out -def _compile_bool_edges(edge_map: ArrayLike) -> Tuple[Any, int]: +def _compile_bool_edges(edge_map: ArrayLike) -> tuple[Any, int]: """Return an edge array using a boolean map.""" edge_map = np.array(edge_map, copy=True) edge_center = edge_map.shape[0] // 2, edge_map.shape[1] // 2 @@ -522,9 +523,9 @@ def _compile_bool_edges(edge_map: ArrayLike) -> Tuple[Any, int]: def hillclimb2d( distance: ArrayLike, - start: Tuple[int, int], - cardinal: Optional[bool] = None, - diagonal: Optional[bool] = None, + start: tuple[int, int], + cardinal: bool | None = None, + diagonal: bool | None = None, *, edge_map: Any = None, ) -> NDArray[Any]: @@ -562,11 +563,13 @@ def hillclimb2d( x, y = start dist: NDArray[Any] = np.asarray(distance) if not (0 <= x < dist.shape[0] and 0 <= y < dist.shape[1]): - raise IndexError("Starting point %r not in shape %r" % (start, dist.shape)) + msg = f"Starting point {start!r} not in shape {dist.shape!r}" + raise IndexError(msg) c_dist = _export(dist) if edge_map is not None: if cardinal is not None or diagonal is not None: - raise TypeError("`edge_map` can not be set at the same time as" " `cardinal` or `diagonal`.") + msg = "`edge_map` can not be set at the same time as `cardinal` or `diagonal`." + raise TypeError(msg) c_edges, n_edges = _compile_bool_edges(edge_map) func = functools.partial(lib.hillclimb2d, c_dist, x, y, n_edges, c_edges) else: @@ -578,7 +581,7 @@ def hillclimb2d( return path -def _world_array(shape: Tuple[int, ...], dtype: Any = np.int32) -> NDArray[Any]: +def _world_array(shape: tuple[int, ...], dtype: Any = np.int32) -> NDArray[Any]: """Return an array where ``ij == arr[ij]``.""" return np.ascontiguousarray( np.transpose( @@ -592,7 +595,7 @@ def _world_array(shape: Tuple[int, ...], dtype: Any = np.int32) -> NDArray[Any]: ) -def _as_hashable(obj: Optional[np.ndarray[Any, Any]]) -> Optional[Any]: +def _as_hashable(obj: np.ndarray[Any, Any] | None) -> Any | None: """Return NumPy arrays as a more hashable form.""" if obj is None: return obj @@ -661,18 +664,19 @@ class CustomGraph: Added the `order` parameter. """ - def __init__(self, shape: Tuple[int, ...], *, order: str = "C"): + def __init__(self, shape: tuple[int, ...], *, order: str = "C") -> None: self._shape = self._shape_c = tuple(shape) self._ndim = len(self._shape) self._order = order if self._order == "F": self._shape_c = self._shape_c[::-1] if not 0 < self._ndim <= 4: - raise TypeError("Graph dimensions must be 1 <= n <= 4.") - self._graph: Dict[Tuple[Any, ...], Dict[str, Any]] = {} - self._edge_rules_keep_alive: List[Any] = [] + msg = "Graph dimensions must be 1 <= n <= 4." + raise TypeError(msg) + self._graph: dict[tuple[Any, ...], dict[str, Any]] = {} + self._edge_rules_keep_alive: list[Any] = [] self._edge_rules_p: Any = None - self._heuristic: Optional[Tuple[int, int, int, int]] = None + self._heuristic: tuple[int, int, int, int] | None = None @property def ndim(self) -> int: @@ -680,17 +684,17 @@ def ndim(self) -> int: return self._ndim @property - def shape(self) -> Tuple[int, ...]: + def shape(self) -> tuple[int, ...]: """Return the shape of this graph.""" return self._shape def add_edge( self, - edge_dir: Tuple[int, ...], + edge_dir: tuple[int, ...], edge_cost: int = 1, *, cost: NDArray[Any], - condition: Optional[ArrayLike] = None, + condition: ArrayLike | None = None, ) -> None: """Add a single edge rule. @@ -742,20 +746,23 @@ def add_edge( but bidirectional edges are not a requirement for the graph. One directional edges such as pits can be added which will only allow movement outwards from the root nodes of the pathfinder. - """ # noqa: E501 + """ self._edge_rules_p = None edge_dir = tuple(edge_dir) cost = np.asarray(cost) if len(edge_dir) != self._ndim: raise TypeError("edge_dir must have exactly %i items, got %r" % (self._ndim, edge_dir)) if edge_cost <= 0: - raise ValueError("edge_cost must be greater than zero, got %r" % (edge_cost,)) + msg = f"edge_cost must be greater than zero, got {edge_cost!r}" + raise ValueError(msg) if cost.shape != self._shape: - raise TypeError("cost array must be shape %r, got %r" % (self._shape, cost.shape)) + msg = f"cost array must be shape {self._shape!r}, got {cost.shape!r}" + raise TypeError(msg) if condition is not None: condition = np.asarray(condition) if condition.shape != self._shape: - raise TypeError("condition array must be shape %r, got %r" % (self._shape, condition.shape)) + msg = f"condition array must be shape {self._shape!r}, got {condition.shape!r}" + raise TypeError(msg) if self._order == "F": # Inputs need to be converted to C. edge_dir = edge_dir[::-1] @@ -772,7 +779,7 @@ def add_edge( } if condition is not None: rule["condition"] = condition - edge = edge_dir + (edge_cost,) + edge = (*edge_dir, edge_cost) if edge not in rule["edge_list"]: rule["edge_list"].append(edge) @@ -781,7 +788,7 @@ def add_edges( *, edge_map: ArrayLike, cost: NDArray[Any], - condition: Optional[ArrayLike] = None, + condition: ArrayLike | None = None, ) -> None: """Add a rule with multiple edges. @@ -943,13 +950,15 @@ def set_heuristic(self, *, cardinal: int = 0, diagonal: int = 0, z: int = 0, w: that's because those nodes are only partially evaluated, but pathfinding to those nodes will work correctly as long as the heuristic isn't greedy. - """ # noqa: E501 + """ if 0 == cardinal == diagonal == z == w: self._heuristic = None if diagonal and cardinal > diagonal: - raise ValueError("Diagonal parameter can not be lower than cardinal.") + msg = "Diagonal parameter can not be lower than cardinal." + raise ValueError(msg) if cardinal < 0 or diagonal < 0 or z < 0 or w < 0: - raise ValueError("Parameters can not be set to negative values..") + msg = "Parameters can not be set to negative values." + raise ValueError(msg) self._heuristic = (cardinal, diagonal, z, w) def _compile_rules(self) -> Any: @@ -1022,12 +1031,14 @@ class SimpleGraph: .. versionadded:: 11.15 """ - def __init__(self, *, cost: ArrayLike, cardinal: int, diagonal: int, greed: int = 1): + def __init__(self, *, cost: ArrayLike, cardinal: int, diagonal: int, greed: int = 1) -> None: cost = np.asarray(cost) if cost.ndim != 2: - raise TypeError("The cost array must e 2 dimensional, array of shape %r given." % (cost.shape,)) + msg = f"The cost array must e 2 dimensional, array of shape {cost.shape!r} given." + raise TypeError(msg) if greed <= 0: - raise ValueError("Greed must be greater than zero, got %r" % (greed,)) + msg = f"Greed must be greater than zero, got {greed}" + raise ValueError(msg) edge_map = ( (diagonal, cardinal, diagonal), (cardinal, 0, cardinal), @@ -1046,11 +1057,11 @@ def ndim(self) -> int: return 2 @property - def shape(self) -> Tuple[int, int]: + def shape(self) -> tuple[int, int]: return self._shape @property - def _heuristic(self) -> Optional[Tuple[int, int, int, int]]: + def _heuristic(self) -> tuple[int, int, int, int] | None: return self._subgraph._heuristic def set_heuristic(self, *, cardinal: int, diagonal: int) -> None: @@ -1079,7 +1090,7 @@ class Pathfinder: .. versionadded:: 11.13 """ - def __init__(self, graph: Union[CustomGraph, SimpleGraph]): + def __init__(self, graph: CustomGraph | SimpleGraph) -> None: self._graph = graph self._order = graph._order self._frontier_p = ffi.gc(lib.TCOD_frontier_new(self._graph._ndim), lib.TCOD_frontier_delete) @@ -1087,7 +1098,7 @@ def __init__(self, graph: Union[CustomGraph, SimpleGraph]): self._travel = _world_array(self._graph._shape_c) self._distance_p = _export(self._distance) self._travel_p = _export(self._travel) - self._heuristic: Optional[Tuple[int, int, int, int, Tuple[int, ...]]] = None + self._heuristic: tuple[int, int, int, int, tuple[int, ...]] | None = None self._heuristic_p: Any = ffi.NULL @property @@ -1161,7 +1172,7 @@ def clear(self) -> None: self._travel = _world_array(self._graph._shape_c) lib.TCOD_frontier_clear(self._frontier_p) - def add_root(self, index: Tuple[int, ...], value: int = 0) -> None: + def add_root(self, index: tuple[int, ...], value: int = 0) -> None: """Add a root node and insert it into the pathfinder frontier. `index` is the root point to insert. The length of `index` must match @@ -1179,7 +1190,7 @@ def add_root(self, index: Tuple[int, ...], value: int = 0) -> None: self._update_heuristic(None) lib.TCOD_frontier_push(self._frontier_p, index, value, value) - def _update_heuristic(self, goal_ij: Optional[Tuple[int, ...]]) -> bool: + def _update_heuristic(self, goal_ij: tuple[int, ...] | None) -> bool: """Update the active heuristic. Return True if the heuristic changed.""" if goal_ij is None: heuristic = None @@ -1212,7 +1223,7 @@ def rebuild_frontier(self) -> None: self._update_heuristic(None) _check(lib.rebuild_frontier_from_distance(self._frontier_p, self._distance_p)) - def resolve(self, goal: Optional[Tuple[int, ...]] = None) -> None: + def resolve(self, goal: tuple[int, ...] | None = None) -> None: """Manually run the pathfinder algorithm. The :any:`path_from` and :any:`path_to` methods will automatically @@ -1270,7 +1281,7 @@ def resolve(self, goal: Optional[Tuple[int, ...]] = None) -> None: self._update_heuristic(goal) self._graph._resolve(self) - def path_from(self, index: Tuple[int, ...]) -> NDArray[Any]: + def path_from(self, index: tuple[int, ...]) -> NDArray[Any]: """Return the shortest path from `index` to the nearest root. The returned array is of shape `(length, ndim)` where `length` is the @@ -1303,7 +1314,7 @@ def path_from(self, index: Tuple[int, ...]) -> NDArray[Any]: >>> pf.path_from((4, 4))[1:].tolist() # Exclude the starting point so that a blocked path is an empty list. [] - """ # noqa: E501 + """ index = tuple(index) # Check for bad input. if len(index) != self._graph._ndim: raise TypeError("Index must be %i items, got %r" % (self._distance.ndim, index)) @@ -1322,7 +1333,7 @@ def path_from(self, index: Tuple[int, ...]) -> NDArray[Any]: ) return path[:, ::-1] if self._order == "F" else path - def path_to(self, index: Tuple[int, ...]) -> NDArray[Any]: + def path_to(self, index: tuple[int, ...]) -> NDArray[Any]: """Return the shortest path from the nearest root to `index`. See :any:`path_from`. @@ -1346,5 +1357,5 @@ def path_to(self, index: Tuple[int, ...]) -> NDArray[Any]: [[1, 1], [2, 2], [3, 3]] >>> pf.path_to((0, 0))[1:].tolist() # Exclude the starting point so that a blocked path is an empty list. [] - """ # noqa: E501 + """ return self.path_from(index)[::-1] diff --git a/tcod/random.py b/tcod/random.py index 013dd6ab..12169141 100644 --- a/tcod/random.py +++ b/tcod/random.py @@ -11,7 +11,7 @@ import os import random import warnings -from typing import Any, Hashable, Optional +from typing import Any, Hashable import tcod.constants from tcod.loader import ffi, lib @@ -21,7 +21,7 @@ MULTIPLY_WITH_CARRY = tcod.constants.RNG_CMWC -class Random(object): +class Random: """The libtcod random number generator. `algorithm` defaults to Mersenne Twister, it can be one of: @@ -49,8 +49,8 @@ class Random(object): def __init__( self, algorithm: int = MERSENNE_TWISTER, - seed: Optional[Hashable] = None, - ): + seed: Hashable | None = None, + ) -> None: """Create a new instance using this algorithm and seed.""" if seed is None: seed = random.getrandbits(32) diff --git a/tcod/render.py b/tcod/render.py index e604d29f..71a98686 100644 --- a/tcod/render.py +++ b/tcod/render.py @@ -29,7 +29,7 @@ from __future__ import annotations -from typing import Any, Optional +from typing import Any from typing_extensions import Final @@ -72,8 +72,8 @@ def __init__(self, atlas: SDLTilesetAtlas) -> None: .. versionadded:: 13.7 """ self._renderer = atlas._renderer - self._cache_console: Optional[tcod.console.Console] = None - self._texture: Optional[tcod.sdl.render.Texture] = None + self._cache_console: tcod.console.Console | None = None + self._texture: tcod.sdl.render.Texture | None = None def render(self, console: tcod.console.Console) -> tcod.sdl.render.Texture: """Render a console to a cached Texture and then return the Texture. diff --git a/tcod/sdl/__init__.py b/tcod/sdl/__init__.py index e1cbde7a..84ee2d06 100644 --- a/tcod/sdl/__init__.py +++ b/tcod/sdl/__init__.py @@ -2,7 +2,7 @@ import logging from pkgutil import extend_path -from typing import Any, Callable, Tuple, TypeVar +from typing import Any, Callable, TypeVar from tcod.loader import ffi, lib @@ -51,34 +51,32 @@ def _check_p(result: Any) -> Any: lib.SDL_LogSetOutputFunction(lib._sdl_log_output_function, ffi.NULL) -def _compiled_version() -> Tuple[int, int, int]: +def _compiled_version() -> tuple[int, int, int]: return int(lib.SDL_MAJOR_VERSION), int(lib.SDL_MINOR_VERSION), int(lib.SDL_PATCHLEVEL) -def _linked_version() -> Tuple[int, int, int]: +def _linked_version() -> tuple[int, int, int]: sdl_version = ffi.new("SDL_version*") lib.SDL_GetVersion(sdl_version) return int(sdl_version.major), int(sdl_version.minor), int(sdl_version.patch) -def _version_at_least(required: Tuple[int, int, int]) -> None: +def _version_at_least(required: tuple[int, int, int]) -> None: """Raise an error if the compiled version is less than required. Used to guard recently defined SDL functions.""" if required <= _compiled_version(): return - raise RuntimeError( - f"This feature requires SDL version {required}, but tcod was compiled with version {_compiled_version()}" - ) + msg = f"This feature requires SDL version {required}, but tcod was compiled with version {_compiled_version()}" + raise RuntimeError(msg) -def _required_version(required: Tuple[int, int, int]) -> Callable[[T], T]: +def _required_version(required: tuple[int, int, int]) -> Callable[[T], T]: if not lib: # Read the docs mock object. return lambda x: x if required <= _compiled_version(): return lambda x: x def replacement(*_args: Any, **_kwargs: Any) -> Any: - raise RuntimeError( - f"This feature requires SDL version {required}, but tcod was compiled with version {_compiled_version()}" - ) + msg = f"This feature requires SDL version {required}, but tcod was compiled with version {_compiled_version()}" + raise RuntimeError(msg) return lambda x: replacement # type: ignore[return-value] diff --git a/tcod/sdl/audio.py b/tcod/sdl/audio.py index 55b080c6..8234a2a6 100644 --- a/tcod/sdl/audio.py +++ b/tcod/sdl/audio.py @@ -47,7 +47,7 @@ import sys import threading import time -from typing import Any, Callable, Dict, Hashable, Iterator, List, Optional, Tuple, Union +from typing import Any, Callable, Hashable, Iterator import numpy as np from numpy.typing import ArrayLike, DTypeLike, NDArray @@ -116,8 +116,9 @@ def convert_audio( in_array: NDArray[Any] = np.asarray(in_sound) if len(in_array.shape) == 1: in_array = in_array[:, np.newaxis] - if not len(in_array.shape) == 2: - raise TypeError(f"Expected a 1 or 2 ndim input, got {in_array.shape} instead.") + if len(in_array.shape) != 2: + msg = f"Expected a 1 or 2 ndim input, got {in_array.shape} instead." + raise TypeError(msg) cvt = ffi.new("SDL_AudioCVT*") in_channels = in_array.shape[1] in_format = _get_format(in_array.dtype) @@ -150,7 +151,7 @@ def __init__( device_id: int, capture: bool, spec: Any, # SDL_AudioSpec* - ): + ) -> None: assert device_id >= 0 assert ffi.typeof(spec) is ffi.typeof("SDL_AudioSpec*") assert spec @@ -172,20 +173,22 @@ def __init__( """The size of the audio buffer in samples.""" self.buffer_bytes: Final[int] = int(spec.size) """The size of the audio buffer in bytes.""" - self._handle: Optional[Any] = None + self._handle: Any | None = None self._callback: Callable[[AudioDevice, NDArray[Any]], None] = self.__default_callback @property def callback(self) -> Callable[[AudioDevice, NDArray[Any]], None]: """If the device was opened with a callback enabled, then you may get or set the callback with this attribute.""" if self._handle is None: - raise TypeError("This AudioDevice was opened without a callback.") + msg = "This AudioDevice was opened without a callback." + raise TypeError(msg) return self._callback @callback.setter def callback(self, new_callback: Callable[[AudioDevice, NDArray[Any]], None]) -> None: if self._handle is None: - raise TypeError("This AudioDevice was opened without a callback.") + msg = "This AudioDevice was opened without a callback." + raise TypeError(msg) self._callback = new_callback @property @@ -209,7 +212,8 @@ def paused(self, value: bool) -> None: def _verify_array_format(self, samples: NDArray[Any]) -> NDArray[Any]: if samples.dtype != self.format: - raise TypeError(f"Expected an array of dtype {self.format}, got {samples.dtype} instead.") + msg = f"Expected an array of dtype {self.format}, got {samples.dtype} instead." + raise TypeError(msg) return samples def _convert_array(self, samples_: ArrayLike) -> NDArray[Any]: @@ -220,7 +224,7 @@ def _convert_array(self, samples_: ArrayLike) -> NDArray[Any]: samples = samples[:, np.newaxis] return np.ascontiguousarray(np.broadcast_to(samples, (samples.shape[0], self.channels)), dtype=self.format) - def convert(self, sound: ArrayLike, rate: Optional[int] = None) -> NDArray[Any]: + def convert(self, sound: ArrayLike, rate: int | None = None) -> NDArray[Any]: """Convert an audio sample into a format supported by this device. Returns the converted array. This might be a reference to the input array if no conversion was needed. @@ -290,7 +294,7 @@ def __default_callback(device: AudioDevice, stream: NDArray[Any]) -> None: class _LoopSoundFunc: - def __init__(self, sound: NDArray[Any], loops: int, on_end: Optional[Callable[[Channel], None]]): + def __init__(self, sound: NDArray[Any], loops: int, on_end: Callable[[Channel], None] | None) -> None: self.sound = sound self.loops = loops self.on_end = on_end @@ -316,9 +320,9 @@ class Channel: def __init__(self) -> None: self._lock = threading.RLock() - self.volume: Union[float, Tuple[float, ...]] = 1.0 - self.sound_queue: List[NDArray[Any]] = [] - self.on_end_callback: Optional[Callable[[Channel], None]] = None + self.volume: float | tuple[float, ...] = 1.0 + self.sound_queue: list[NDArray[Any]] = [] + self.on_end_callback: Callable[[Channel], None] | None = None @property def busy(self) -> bool: @@ -329,9 +333,9 @@ def play( self, sound: ArrayLike, *, - volume: Union[float, Tuple[float, ...]] = 1.0, + volume: float | tuple[float, ...] = 1.0, loops: int = 0, - on_end: Optional[Callable[[Channel], None]] = None, + on_end: Callable[[Channel], None] | None = None, ) -> None: """Play an audio sample, stopping any audio currently playing on this channel. @@ -392,8 +396,8 @@ class BasicMixer(threading.Thread): .. versionadded:: 13.6 """ - def __init__(self, device: AudioDevice): - self.channels: Dict[Hashable, Channel] = {} + def __init__(self, device: AudioDevice) -> None: + self.channels: dict[Hashable, Channel] = {} assert device.format == np.float32 super().__init__(daemon=True) self.device = device @@ -445,9 +449,9 @@ def play( self, sound: ArrayLike, *, - volume: Union[float, Tuple[float, ...]] = 1.0, + volume: float | tuple[float, ...] = 1.0, loops: int = 0, - on_end: Optional[Callable[[Channel], None]] = None, + on_end: Callable[[Channel], None] | None = None, ) -> Channel: """Play a sound, return the channel the sound is playing on. @@ -525,7 +529,7 @@ class AllowedChanges(enum.IntFlag): def open( - name: Optional[str] = None, + name: str | None = None, capture: bool = False, *, frequency: int = 44100, @@ -534,7 +538,7 @@ def open( samples: int = 0, allowed_changes: AllowedChanges = AllowedChanges.NONE, paused: bool = False, - callback: Union[None, Literal[True], Callable[[AudioDevice, NDArray[Any]], None]] = None, + callback: None | Literal[True] | Callable[[AudioDevice, NDArray[Any]], None] = None, ) -> AudioDevice: """Open an audio device for playback or capture and return it. diff --git a/tcod/sdl/joystick.py b/tcod/sdl/joystick.py index dde74192..5d24d353 100644 --- a/tcod/sdl/joystick.py +++ b/tcod/sdl/joystick.py @@ -1,11 +1,11 @@ -"""SDL Joystick Support +"""SDL Joystick Support. .. versionadded:: 13.8 """ from __future__ import annotations import enum -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any from typing_extensions import Final, Literal @@ -13,7 +13,7 @@ from tcod.loader import ffi, lib from tcod.sdl import _check, _check_p -_HAT_DIRECTIONS: Dict[int, Tuple[Literal[-1, 0, 1], Literal[-1, 0, 1]]] = { +_HAT_DIRECTIONS: dict[int, tuple[Literal[-1, 0, 1], Literal[-1, 0, 1]]] = { lib.SDL_HAT_CENTERED or 0: (0, 0), lib.SDL_HAT_UP or 0: (0, -1), lib.SDL_HAT_RIGHT or 0: (1, 0), @@ -122,7 +122,7 @@ class Joystick: https://wiki.libsdl.org/CategoryJoystick """ - def __init__(self, sdl_joystick_p: Any): + def __init__(self, sdl_joystick_p: Any) -> None: self.sdl_joystick_p: Final = sdl_joystick_p """The CFFI pointer to an SDL_Joystick struct.""" self.axes: Final[int] = _check(lib.SDL_JoystickNumAxes(self.sdl_joystick_p)) @@ -173,7 +173,7 @@ def get_axis(self, axis: int) -> int: """Return the raw value of `axis` in the range -32768 to 32767.""" return int(lib.SDL_JoystickGetAxis(self.sdl_joystick_p, axis)) - def get_ball(self, ball: int) -> Tuple[int, int]: + def get_ball(self, ball: int) -> tuple[int, int]: """Return the values (delta_x, delta_y) of `ball` since the last poll.""" xy = ffi.new("int[2]") _check(lib.SDL_JoystickGetBall(ball, xy, xy + 1)) @@ -183,7 +183,7 @@ def get_button(self, button: int) -> bool: """Return True if `button` is currently held.""" return bool(lib.SDL_JoystickGetButton(self.sdl_joystick_p, button)) - def get_hat(self, hat: int) -> Tuple[Literal[-1, 0, 1], Literal[-1, 0, 1]]: + def get_hat(self, hat: int) -> tuple[Literal[-1, 0, 1], Literal[-1, 0, 1]]: """Return the direction of `hat` as (x, y). With (-1, -1) being in the upper-left.""" return _HAT_DIRECTIONS[lib.SDL_JoystickGetHat(self.sdl_joystick_p, hat)] @@ -191,7 +191,7 @@ def get_hat(self, hat: int) -> Tuple[Literal[-1, 0, 1], Literal[-1, 0, 1]]: class GameController: """A standard interface for an Xbox 360 style game controller.""" - def __init__(self, sdl_controller_p: Any): + def __init__(self, sdl_controller_p: Any) -> None: self.sdl_controller_p: Final = sdl_controller_p self.joystick: Final = Joystick(lib.SDL_GameControllerGetJoystick(self.sdl_controller_p)) """The :any:`Joystick` associated with this controller.""" @@ -228,32 +228,32 @@ def __hash__(self) -> int: # These could exist as convenience functions, but the get_X functions are probably better. @property def _left_x(self) -> int: - "Return the position of this axis. (-32768 to 32767)" + """Return the position of this axis (-32768 to 32767).""" return int(lib.SDL_GameControllerGetAxis(self.sdl_controller_p, lib.SDL_CONTROLLER_AXIS_LEFTX)) @property def _left_y(self) -> int: - "Return the position of this axis. (-32768 to 32767)" + """Return the position of this axis (-32768 to 32767).""" return int(lib.SDL_GameControllerGetAxis(self.sdl_controller_p, lib.SDL_CONTROLLER_AXIS_LEFTY)) @property def _right_x(self) -> int: - "Return the position of this axis. (-32768 to 32767)" + """Return the position of this axis (-32768 to 32767).""" return int(lib.SDL_GameControllerGetAxis(self.sdl_controller_p, lib.SDL_CONTROLLER_AXIS_RIGHTX)) @property def _right_y(self) -> int: - "Return the position of this axis. (-32768 to 32767)" + """Return the position of this axis (-32768 to 32767).""" return int(lib.SDL_GameControllerGetAxis(self.sdl_controller_p, lib.SDL_CONTROLLER_AXIS_RIGHTY)) @property def _trigger_left(self) -> int: - "Return the position of this trigger. (0 to 32767)" + """Return the position of this trigger (0 to 32767).""" return int(lib.SDL_GameControllerGetAxis(self.sdl_controller_p, lib.SDL_CONTROLLER_AXIS_TRIGGERLEFT)) @property def _trigger_right(self) -> int: - "Return the position of this trigger. (0 to 32767)" + """Return the position of this trigger (0 to 32767).""" return int(lib.SDL_GameControllerGetAxis(self.sdl_controller_p, lib.SDL_CONTROLLER_AXIS_TRIGGERRIGHT)) @property @@ -312,7 +312,7 @@ def _right_shoulder(self) -> bool: return bool(lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_RIGHTSHOULDER)) @property - def _dpad(self) -> Tuple[Literal[-1, 0, 1], Literal[-1, 0, 1]]: + def _dpad(self) -> tuple[Literal[-1, 0, 1], Literal[-1, 0, 1]]: return ( lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_DPAD_RIGHT) - lib.SDL_GameControllerGetButton(self.sdl_controller_p, lib.SDL_CONTROLLER_BUTTON_DPAD_LEFT), @@ -362,12 +362,12 @@ def _get_number() -> int: return _check(lib.SDL_NumJoysticks()) -def get_joysticks() -> List[Joystick]: +def get_joysticks() -> list[Joystick]: """Return a list of all connected joystick devices.""" return [Joystick._open(i) for i in range(_get_number())] -def get_controllers() -> List[GameController]: +def get_controllers() -> list[GameController]: """Return a list of all connected game controllers. This ignores joysticks without a game controller mapping. @@ -375,7 +375,7 @@ def get_controllers() -> List[GameController]: return [GameController._open(i) for i in range(_get_number()) if lib.SDL_IsGameController(i)] -def _get_all() -> List[Union[Joystick, GameController]]: +def _get_all() -> list[Joystick | GameController]: """Return a list of all connected joystick or controller devices. If the joystick has a controller mapping then it is returned as a :any:`GameController`. @@ -384,7 +384,7 @@ def _get_all() -> List[Union[Joystick, GameController]]: return [GameController._open(i) if lib.SDL_IsGameController(i) else Joystick._open(i) for i in range(_get_number())] -def joystick_event_state(new_state: Optional[bool] = None) -> bool: +def joystick_event_state(new_state: bool | None = None) -> bool: """Check or set joystick event polling. .. seealso:: @@ -394,7 +394,7 @@ def joystick_event_state(new_state: Optional[bool] = None) -> bool: return bool(_check(lib.SDL_JoystickEventState(_OPTIONS[new_state]))) -def controller_event_state(new_state: Optional[bool] = None) -> bool: +def controller_event_state(new_state: bool | None = None) -> bool: """Check or set game controller event polling. .. seealso:: diff --git a/tcod/sdl/mouse.py b/tcod/sdl/mouse.py index bce0b07f..aaffdeec 100644 --- a/tcod/sdl/mouse.py +++ b/tcod/sdl/mouse.py @@ -9,7 +9,7 @@ from __future__ import annotations import enum -from typing import Any, Optional, Tuple, Union +from typing import Any import numpy as np from numpy.typing import ArrayLike, NDArray @@ -23,11 +23,13 @@ class Cursor: """A cursor icon for use with :any:`set_cursor`.""" - def __init__(self, sdl_cursor_p: Any): + def __init__(self, sdl_cursor_p: Any) -> None: if ffi.typeof(sdl_cursor_p) is not ffi.typeof("struct SDL_Cursor*"): - raise TypeError(f"Expected a {ffi.typeof('struct SDL_Cursor*')} type (was {ffi.typeof(sdl_cursor_p)}).") + msg = f"Expected a {ffi.typeof('struct SDL_Cursor*')} type (was {ffi.typeof(sdl_cursor_p)})." + raise TypeError(msg) if not sdl_cursor_p: - raise TypeError("C pointer must not be null.") + msg = "C pointer must not be null." + raise TypeError(msg) self.p = sdl_cursor_p def __eq__(self, other: Any) -> bool: @@ -68,7 +70,7 @@ class SystemCursor(enum.IntEnum): """""" -def new_cursor(data: NDArray[np.bool_], mask: NDArray[np.bool_], hot_xy: Tuple[int, int] = (0, 0)) -> Cursor: +def new_cursor(data: NDArray[np.bool_], mask: NDArray[np.bool_], hot_xy: tuple[int, int] = (0, 0)) -> Cursor: """Return a new non-color Cursor from the provided parameters. Args: @@ -81,9 +83,11 @@ def new_cursor(data: NDArray[np.bool_], mask: NDArray[np.bool_], hot_xy: Tuple[i https://wiki.libsdl.org/SDL_CreateCursor """ if len(data.shape) != 2: - raise TypeError("Data and mask arrays must be 2D.") + msg = "Data and mask arrays must be 2D." + raise TypeError(msg) if data.shape != mask.shape: - raise TypeError("Data and mask arrays must have the same shape.") + msg = "Data and mask arrays must have the same shape." + raise TypeError(msg) height, width = data.shape data_packed = np.packbits(data, axis=0, bitorder="big") mask_packed = np.packbits(mask, axis=0, bitorder="big") @@ -94,9 +98,8 @@ def new_cursor(data: NDArray[np.bool_], mask: NDArray[np.bool_], hot_xy: Tuple[i ) -def new_color_cursor(pixels: ArrayLike, hot_xy: Tuple[int, int]) -> Cursor: - """ - Args: +def new_color_cursor(pixels: ArrayLike, hot_xy: tuple[int, int]) -> Cursor: + """Args: pixels: A row-major array of RGB or RGBA pixels. hot_xy: The position of the pointer relative to the mouse sprite, starting from the upper-left at (0, 0). @@ -116,7 +119,7 @@ def new_system_cursor(cursor: SystemCursor) -> Cursor: return Cursor._claim(lib.SDL_CreateSystemCursor(cursor)) -def set_cursor(cursor: Optional[Union[Cursor, SystemCursor]]) -> None: +def set_cursor(cursor: Cursor | SystemCursor | None) -> None: """Change the active cursor to the one provided. Args: @@ -134,7 +137,7 @@ def get_default_cursor() -> Cursor: return Cursor(_check_p(lib.SDL_GetDefaultCursor())) -def get_cursor() -> Optional[Cursor]: +def get_cursor() -> Cursor | None: """Return the active cursor, or None if these is no mouse.""" cursor_p = lib.SDL_GetCursor() return Cursor(cursor_p) if cursor_p else None @@ -214,7 +217,7 @@ def get_state() -> tcod.event.MouseState: return tcod.event.MouseState((xy[0], xy[1]), state=state) -def get_focus() -> Optional[tcod.sdl.video.Window]: +def get_focus() -> tcod.sdl.video.Window | None: """Return the window which currently has mouse focus.""" window_p = lib.SDL_GetMouseFocus() return tcod.sdl.video.Window(window_p) if window_p else None diff --git a/tcod/sdl/render.py b/tcod/sdl/render.py index d7e3226d..65cc6d41 100644 --- a/tcod/sdl/render.py +++ b/tcod/sdl/render.py @@ -5,7 +5,7 @@ from __future__ import annotations import enum -from typing import Any, Optional, Tuple, Union +from typing import Any import numpy as np from numpy.typing import NDArray @@ -168,14 +168,14 @@ def __init__(self, sdl_texture_p: Any, sdl_renderer_p: Any = None) -> None: def __eq__(self, other: Any) -> bool: return bool(self.p == getattr(other, "p", None)) - def _query(self) -> Tuple[int, int, int, int]: + def _query(self) -> tuple[int, int, int, int]: """Return (format, access, width, height).""" format = ffi.new("uint32_t*") buffer = ffi.new("int[3]") lib.SDL_QueryTexture(self.p, format, buffer, buffer + 1, buffer + 2) return int(format[0]), int(buffer[0]), int(buffer[1]), int(buffer[2]) - def update(self, pixels: NDArray[Any], rect: Optional[Tuple[int, int, int, int]] = None) -> None: + def update(self, pixels: NDArray[Any], rect: tuple[int, int, int, int] | None = None) -> None: """Update the pixel data of this texture. .. versionadded:: 13.5 @@ -214,14 +214,14 @@ def blend_mode(self, value: int) -> None: _check(lib.SDL_SetTextureBlendMode(self.p, value)) @property - def color_mod(self) -> Tuple[int, int, int]: + def color_mod(self) -> tuple[int, int, int]: """Texture RGB color modulate values, can be set.""" rgb = ffi.new("uint8_t[3]") _check(lib.SDL_GetTextureColorMod(self.p, rgb, rgb + 1, rgb + 2)) return int(rgb[0]), int(rgb[1]), int(rgb[2]) @color_mod.setter - def color_mod(self, rgb: Tuple[int, int, int]) -> None: + def color_mod(self, rgb: tuple[int, int, int]) -> None: _check(lib.SDL_SetTextureColorMod(self.p, rgb[0], rgb[1], rgb[2])) @@ -244,9 +244,11 @@ class Renderer: def __init__(self, sdl_renderer_p: Any) -> None: if ffi.typeof(sdl_renderer_p) is not ffi.typeof("struct SDL_Renderer*"): - raise TypeError(f"Expected a {ffi.typeof('struct SDL_Window*')} type (was {ffi.typeof(sdl_renderer_p)}).") + msg = f"Expected a {ffi.typeof('struct SDL_Window*')} type (was {ffi.typeof(sdl_renderer_p)})." + raise TypeError(msg) if not sdl_renderer_p: - raise TypeError("C pointer must not be null.") + msg = "C pointer must not be null." + raise TypeError(msg) self.p = sdl_renderer_p def __eq__(self, other: Any) -> bool: @@ -255,10 +257,10 @@ def __eq__(self, other: Any) -> bool: def copy( self, texture: Texture, - source: Optional[Tuple[float, float, float, float]] = None, - dest: Optional[Tuple[float, float, float, float]] = None, + source: tuple[float, float, float, float] | None = None, + dest: tuple[float, float, float, float] | None = None, angle: float = 0, - center: Optional[Tuple[float, float]] = None, + center: tuple[float, float] | None = None, flip: RendererFlip = RendererFlip.NONE, ) -> None: """Copy a texture to the rendering target. @@ -297,9 +299,7 @@ def set_render_target(self, texture: Texture) -> _RestoreTargetContext: _check(lib.SDL_SetRenderTarget(self.p, texture.p)) return restore - def new_texture( - self, width: int, height: int, *, format: Optional[int] = None, access: Optional[int] = None - ) -> Texture: + def new_texture(self, width: int, height: int, *, format: int | None = None, access: int | None = None) -> Texture: """Allocate and return a new Texture for this renderer. Args: @@ -316,9 +316,7 @@ def new_texture( texture_p = ffi.gc(lib.SDL_CreateTexture(self.p, format, access, width, height), lib.SDL_DestroyTexture) return Texture(texture_p, self.p) - def upload_texture( - self, pixels: NDArray[Any], *, format: Optional[int] = None, access: Optional[int] = None - ) -> Texture: + def upload_texture(self, pixels: NDArray[Any], *, format: int | None = None, access: int | None = None) -> Texture: """Return a new Texture from an array of pixels. Args: @@ -335,7 +333,8 @@ def upload_texture( elif pixels.shape[2] == 3: format = int(lib.SDL_PIXELFORMAT_RGB24) else: - raise TypeError(f"Can't determine the format required for an array of shape {pixels.shape}.") + msg = f"Can't determine the format required for an array of shape {pixels.shape}." + raise TypeError(msg) texture = self.new_texture(pixels.shape[1], pixels.shape[0], format=format, access=access) if not pixels[0].flags["C_CONTIGUOUS"]: @@ -346,7 +345,7 @@ def upload_texture( return texture @property - def draw_color(self) -> Tuple[int, int, int, int]: + def draw_color(self) -> tuple[int, int, int, int]: """Get or set the active RGBA draw color for this renderer. .. versionadded:: 13.5 @@ -356,7 +355,7 @@ def draw_color(self) -> Tuple[int, int, int, int]: return tuple(rgba) # type: ignore[return-value] @draw_color.setter - def draw_color(self, rgba: Tuple[int, int, int, int]) -> None: + def draw_color(self, rgba: tuple[int, int, int, int]) -> None: _check(lib.SDL_SetRenderDrawColor(self.p, *rgba)) @property @@ -374,7 +373,7 @@ def draw_blend_mode(self, value: int) -> None: _check(lib.SDL_SetRenderDrawBlendMode(self.p, value)) @property - def output_size(self) -> Tuple[int, int]: + def output_size(self) -> tuple[int, int]: """Get the (width, height) pixel resolution of the rendering context. .. seealso:: @@ -387,7 +386,7 @@ def output_size(self) -> Tuple[int, int]: return out[0], out[1] @property - def clip_rect(self) -> Optional[Tuple[int, int, int, int]]: + def clip_rect(self) -> tuple[int, int, int, int] | None: """Get or set the clipping rectangle of this renderer. Set to None to disable clipping. @@ -401,7 +400,7 @@ def clip_rect(self) -> Optional[Tuple[int, int, int, int]]: return rect.x, rect.y, rect.w, rect.h @clip_rect.setter - def clip_rect(self, rect: Optional[Tuple[int, int, int, int]]) -> None: + def clip_rect(self, rect: tuple[int, int, int, int] | None) -> None: rect_p = ffi.NULL if rect is None else ffi.new("SDL_Rect*", rect) _check(lib.SDL_RenderSetClipRect(self.p, rect_p)) @@ -421,7 +420,7 @@ def integer_scaling(self, enable: bool) -> None: _check(lib.SDL_RenderSetIntegerScale(self.p, enable)) @property - def logical_size(self) -> Tuple[int, int]: + def logical_size(self) -> tuple[int, int]: """Get or set a device independent (width, height) resolution. Might be (0, 0) if a resolution was never assigned. @@ -436,11 +435,11 @@ def logical_size(self) -> Tuple[int, int]: return out[0], out[1] @logical_size.setter - def logical_size(self, size: Tuple[int, int]) -> None: + def logical_size(self, size: tuple[int, int]) -> None: _check(lib.SDL_RenderSetLogicalSize(self.p, *size)) @property - def scale(self) -> Tuple[float, float]: + def scale(self) -> tuple[float, float]: """Get or set an (x_scale, y_scale) multiplier for drawing. .. seealso:: @@ -453,11 +452,11 @@ def scale(self) -> Tuple[float, float]: return out[0], out[1] @scale.setter - def scale(self, scale: Tuple[float, float]) -> None: + def scale(self, scale: tuple[float, float]) -> None: _check(lib.SDL_RenderSetScale(self.p, *scale)) @property - def viewport(self) -> Optional[Tuple[int, int, int, int]]: + def viewport(self) -> tuple[int, int, int, int] | None: """Get or set the drawing area for the current rendering target. .. seealso:: @@ -470,7 +469,7 @@ def viewport(self) -> Optional[Tuple[int, int, int, int]]: return rect.x, rect.y, rect.w, rect.h @viewport.setter - def viewport(self, rect: Optional[Tuple[int, int, int, int]]) -> None: + def viewport(self, rect: tuple[int, int, int, int] | None) -> None: _check(lib.SDL_RenderSetViewport(self.p, (rect,))) @_required_version((2, 0, 18)) @@ -554,35 +553,35 @@ def clear(self) -> None: """ _check(lib.SDL_RenderClear(self.p)) - def fill_rect(self, rect: Tuple[float, float, float, float]) -> None: + def fill_rect(self, rect: tuple[float, float, float, float]) -> None: """Fill a rectangle with :any:`draw_color`. .. versionadded:: 13.5 """ _check(lib.SDL_RenderFillRectF(self.p, (rect,))) - def draw_rect(self, rect: Tuple[float, float, float, float]) -> None: + def draw_rect(self, rect: tuple[float, float, float, float]) -> None: """Draw a rectangle outline. .. versionadded:: 13.5 """ _check(lib.SDL_RenderDrawRectF(self.p, (rect,))) - def draw_point(self, xy: Tuple[float, float]) -> None: + def draw_point(self, xy: tuple[float, float]) -> None: """Draw a point. .. versionadded:: 13.5 """ _check(lib.SDL_RenderDrawPointF(self.p, (xy,))) - def draw_line(self, start: Tuple[float, float], end: Tuple[float, float]) -> None: + def draw_line(self, start: tuple[float, float], end: tuple[float, float]) -> None: """Draw a single line. .. versionadded:: 13.5 """ _check(lib.SDL_RenderDrawLineF(self.p, *start, *end)) - def fill_rects(self, rects: NDArray[Union[np.intc, np.float32]]) -> None: + def fill_rects(self, rects: NDArray[np.intc | np.float32]) -> None: """Fill multiple rectangles from an array. .. versionadded:: 13.5 @@ -595,9 +594,10 @@ def fill_rects(self, rects: NDArray[Union[np.intc, np.float32]]) -> None: elif rects.dtype == np.float32: _check(lib.SDL_RenderFillRectsF(self.p, tcod.ffi.from_buffer("SDL_FRect*", rects), rects.shape[0])) else: - raise TypeError(f"Array must be an np.intc or np.float32 type, got {rects.dtype}.") + msg = f"Array must be an np.intc or np.float32 type, got {rects.dtype}." + raise TypeError(msg) - def draw_rects(self, rects: NDArray[Union[np.intc, np.float32]]) -> None: + def draw_rects(self, rects: NDArray[np.intc | np.float32]) -> None: """Draw multiple outlined rectangles from an array. .. versionadded:: 13.5 @@ -610,9 +610,10 @@ def draw_rects(self, rects: NDArray[Union[np.intc, np.float32]]) -> None: elif rects.dtype == np.float32: _check(lib.SDL_RenderDrawRectsF(self.p, tcod.ffi.from_buffer("SDL_FRect*", rects), rects.shape[0])) else: - raise TypeError(f"Array must be an np.intc or np.float32 type, got {rects.dtype}.") + msg = f"Array must be an np.intc or np.float32 type, got {rects.dtype}." + raise TypeError(msg) - def draw_points(self, points: NDArray[Union[np.intc, np.float32]]) -> None: + def draw_points(self, points: NDArray[np.intc | np.float32]) -> None: """Draw an array of points. .. versionadded:: 13.5 @@ -625,9 +626,10 @@ def draw_points(self, points: NDArray[Union[np.intc, np.float32]]) -> None: elif points.dtype == np.float32: _check(lib.SDL_RenderDrawRectsF(self.p, tcod.ffi.from_buffer("SDL_FPoint*", points), points.shape[0])) else: - raise TypeError(f"Array must be an np.intc or np.float32 type, got {points.dtype}.") + msg = f"Array must be an np.intc or np.float32 type, got {points.dtype}." + raise TypeError(msg) - def draw_lines(self, points: NDArray[Union[np.intc, np.float32]]) -> None: + def draw_lines(self, points: NDArray[np.intc | np.float32]) -> None: """Draw a connected series of lines from an array. .. versionadded:: 13.5 @@ -640,16 +642,17 @@ def draw_lines(self, points: NDArray[Union[np.intc, np.float32]]) -> None: elif points.dtype == np.float32: _check(lib.SDL_RenderDrawRectsF(self.p, tcod.ffi.from_buffer("SDL_FPoint*", points), points.shape[0] - 1)) else: - raise TypeError(f"Array must be an np.intc or np.float32 type, got {points.dtype}.") + msg = f"Array must be an np.intc or np.float32 type, got {points.dtype}." + raise TypeError(msg) @_required_version((2, 0, 18)) def geometry( self, - texture: Optional[Texture], + texture: Texture | None, xy: NDArray[np.float32], color: NDArray[np.uint8], uv: NDArray[np.float32], - indices: Optional[NDArray[Union[np.uint8, np.uint16, np.uint32]]] = None, + indices: NDArray[np.uint8 | np.uint16 | np.uint32] | None = None, ) -> None: """Render triangles from texture and vertex data. @@ -695,7 +698,7 @@ def geometry( def new_renderer( window: tcod.sdl.video.Window, *, - driver: Optional[int] = None, + driver: int | None = None, software: bool = False, vsync: bool = True, target_textures: bool = False, diff --git a/tcod/sdl/sys.py b/tcod/sdl/sys.py index 7987081d..39bda95d 100644 --- a/tcod/sdl/sys.py +++ b/tcod/sdl/sys.py @@ -2,7 +2,7 @@ import enum import warnings -from typing import Any, Tuple +from typing import Any from tcod.loader import ffi, lib from tcod.sdl import _check, _get_error @@ -56,7 +56,7 @@ class _PowerState(enum.IntEnum): CHARGED = enum.auto() -def _get_power_info() -> Tuple[_PowerState, int, int]: +def _get_power_info() -> tuple[_PowerState, int, int]: buffer = ffi.new("int[2]") power_state = _PowerState(lib.SDL_GetPowerInfo(buffer, buffer + 1)) seconds_of_power = buffer[0] @@ -68,7 +68,7 @@ def _get_clipboard() -> str: """Return the text of the clipboard.""" text = str(ffi.string(lib.SDL_GetClipboardText()), encoding="utf-8") if not text: # Show the reason for an empty return, this should probably be logged instead. - warnings.warn(f"Return string is empty because: {_get_error()}") + warnings.warn(f"Return string is empty because: {_get_error()}", RuntimeWarning, stacklevel=2) return text diff --git a/tcod/sdl/video.py b/tcod/sdl/video.py index 2ef4553e..4cf6a87b 100644 --- a/tcod/sdl/video.py +++ b/tcod/sdl/video.py @@ -10,7 +10,7 @@ import enum import sys -from typing import Any, Optional, Tuple +from typing import Any import numpy as np from numpy.typing import ArrayLike, NDArray @@ -98,9 +98,11 @@ class _TempSurface: def __init__(self, pixels: ArrayLike) -> None: self._array: NDArray[np.uint8] = np.ascontiguousarray(pixels, dtype=np.uint8) if len(self._array.shape) != 3: - raise TypeError(f"NumPy shape must be 3D [y, x, ch] (got {self._array.shape})") + msg = f"NumPy shape must be 3D [y, x, ch] (got {self._array.shape})" + raise TypeError(msg) if not (3 <= self._array.shape[2] <= 4): - raise TypeError(f"NumPy array must have RGB or RGBA channels. (got {self._array.shape})") + msg = f"NumPy array must have RGB or RGBA channels. (got {self._array.shape})" + raise TypeError(msg) self.p = ffi.gc( lib.SDL_CreateRGBSurfaceFrom( ffi.from_buffer("void*", self._array), @@ -122,11 +124,13 @@ class Window: def __init__(self, sdl_window_p: Any) -> None: if ffi.typeof(sdl_window_p) is not ffi.typeof("struct SDL_Window*"): - raise TypeError( - "sdl_window_p must be %r type (was %r)." % (ffi.typeof("struct SDL_Window*"), ffi.typeof(sdl_window_p)) + msg = "sdl_window_p must be {!r} type (was {!r}).".format( + ffi.typeof("struct SDL_Window*"), ffi.typeof(sdl_window_p) ) + raise TypeError(msg) if not sdl_window_p: - raise TypeError("sdl_window_p can not be a null pointer.") + msg = "sdl_window_p can not be a null pointer." + raise TypeError(msg) self.p = sdl_window_p def __eq__(self, other: Any) -> bool: @@ -142,7 +146,7 @@ def set_icon(self, pixels: ArrayLike) -> None: lib.SDL_SetWindowIcon(self.p, surface.p) @property - def position(self) -> Tuple[int, int]: + def position(self) -> tuple[int, int]: """Get or set the (x, y) position of the window. This attribute can be set the move the window. @@ -153,12 +157,12 @@ def position(self) -> Tuple[int, int]: return xy[0], xy[1] @position.setter - def position(self, xy: Tuple[int, int]) -> None: + def position(self, xy: tuple[int, int]) -> None: x, y = xy lib.SDL_SetWindowPosition(self.p, x, y) @property - def size(self) -> Tuple[int, int]: + def size(self) -> tuple[int, int]: """Get or set the pixel (width, height) of the window client area. This attribute can be set to change the size of the window but the given size must be greater than (1, 1) or @@ -169,32 +173,33 @@ def size(self) -> Tuple[int, int]: return xy[0], xy[1] @size.setter - def size(self, xy: Tuple[int, int]) -> None: + def size(self, xy: tuple[int, int]) -> None: if any(i <= 0 for i in xy): - raise ValueError(f"Window size must be greater than zero, not {xy}") + msg = f"Window size must be greater than zero, not {xy}" + raise ValueError(msg) x, y = xy lib.SDL_SetWindowSize(self.p, x, y) @property - def min_size(self) -> Tuple[int, int]: + def min_size(self) -> tuple[int, int]: """Get or set this windows minimum client area.""" xy = ffi.new("int[2]") lib.SDL_GetWindowMinimumSize(self.p, xy, xy + 1) return xy[0], xy[1] @min_size.setter - def min_size(self, xy: Tuple[int, int]) -> None: + def min_size(self, xy: tuple[int, int]) -> None: lib.SDL_SetWindowMinimumSize(self.p, xy[0], xy[1]) @property - def max_size(self) -> Tuple[int, int]: + def max_size(self) -> tuple[int, int]: """Get or set this windows maximum client area.""" xy = ffi.new("int[2]") lib.SDL_GetWindowMaximumSize(self.p, xy, xy + 1) return xy[0], xy[1] @max_size.setter - def max_size(self, xy: Tuple[int, int]) -> None: + def max_size(self, xy: tuple[int, int]) -> None: lib.SDL_SetWindowMaximumSize(self.p, xy[0], xy[1]) @property @@ -242,7 +247,7 @@ def resizable(self, value: bool) -> None: lib.SDL_SetWindowResizable(self.p, value) @property - def border_size(self) -> Tuple[int, int, int, int]: + def border_size(self) -> tuple[int, int, int, int]: """Get the (top, left, bottom, right) size of the window decorations around the client area. If this fails or the window doesn't have decorations yet then the value will be (0, 0, 0, 0). @@ -283,7 +288,7 @@ def grab(self, value: bool) -> None: lib.SDL_SetWindowGrab(self.p, value) @property - def mouse_rect(self) -> Optional[Tuple[int, int, int, int]]: + def mouse_rect(self) -> tuple[int, int, int, int] | None: """Get or set the mouse confinement area when the window has mouse focus. Setting this will not automatically grab the cursor. @@ -295,7 +300,7 @@ def mouse_rect(self) -> Optional[Tuple[int, int, int, int]]: return (rect.x, rect.y, rect.w, rect.h) if rect else None @mouse_rect.setter - def mouse_rect(self, rect: Optional[Tuple[int, int, int, int]]) -> None: + def mouse_rect(self, rect: tuple[int, int, int, int] | None) -> None: _version_at_least((2, 0, 18)) _check(lib.SDL_SetWindowMouseRect(self.p, (rect,) if rect else ffi.NULL)) @@ -333,9 +338,9 @@ def new_window( width: int, height: int, *, - x: Optional[int] = None, - y: Optional[int] = None, - title: Optional[str] = None, + x: int | None = None, + y: int | None = None, + title: str | None = None, flags: int = 0, ) -> Window: """Initialize and return a new SDL Window. @@ -366,13 +371,13 @@ def new_window( return Window(_check_p(window_p)) -def get_grabbed_window() -> Optional[Window]: +def get_grabbed_window() -> Window | None: """Return the window which has input grab enabled, if any.""" sdl_window_p = lib.SDL_GetGrabbedWindow() return Window(sdl_window_p) if sdl_window_p else None -def screen_saver_allowed(allow: Optional[bool] = None) -> bool: +def screen_saver_allowed(allow: bool | None = None) -> bool: """Allow or prevent a screen saver from being displayed and return the current allowed status. If `allow` is `None` then only the current state is returned. diff --git a/tcod/tileset.py b/tcod/tileset.py index ac418f1b..9f13f691 100644 --- a/tcod/tileset.py +++ b/tcod/tileset.py @@ -15,7 +15,7 @@ import itertools from os import PathLike from pathlib import Path -from typing import Any, Iterable, Optional, Tuple, Union +from typing import Any, Iterable import numpy as np from numpy.typing import ArrayLike, NDArray @@ -42,7 +42,8 @@ def _claim(cls, cdata: Any) -> Tileset: """Return a new Tileset that owns the provided TCOD_Tileset* object.""" self = object.__new__(cls) if cdata == ffi.NULL: - raise RuntimeError("Tileset initialized with nullptr.") + msg = "Tileset initialized with nullptr." + raise RuntimeError(msg) self._tileset_p = ffi.gc(cdata, lib.TCOD_tileset_delete) return self @@ -63,7 +64,7 @@ def tile_height(self) -> int: return int(lib.TCOD_tileset_get_tile_height_(self._tileset_p)) @property - def tile_shape(self) -> Tuple[int, int]: + def tile_shape(self) -> tuple[int, int]: """Shape (height, width) of the tile in pixels.""" return self.tile_height, self.tile_width @@ -80,7 +81,7 @@ def get_tile(self, codepoint: int) -> NDArray[np.uint8]: uint8. Note that most grey-scale tiles will only use the alpha channel and will usually have a solid white color channel. """ - tile: NDArray[np.uint8] = np.zeros(self.tile_shape + (4,), dtype=np.uint8) + tile: NDArray[np.uint8] = np.zeros((*self.tile_shape, 4), dtype=np.uint8) lib.TCOD_tileset_get_tile_( self._tileset_p, codepoint, @@ -88,7 +89,7 @@ def get_tile(self, codepoint: int) -> NDArray[np.uint8]: ) return tile - def set_tile(self, codepoint: int, tile: Union[ArrayLike, NDArray[np.uint8]]) -> None: + def set_tile(self, codepoint: int, tile: ArrayLike | NDArray[np.uint8]) -> None: """Upload a tile into this array. Args: @@ -139,11 +140,11 @@ def set_tile(self, codepoint: int, tile: Union[ArrayLike, NDArray[np.uint8]]) -> """ tile = np.ascontiguousarray(tile, dtype=np.uint8) if tile.shape == self.tile_shape: - full_tile: NDArray[np.uint8] = np.empty(self.tile_shape + (4,), dtype=np.uint8) + full_tile: NDArray[np.uint8] = np.empty((*self.tile_shape, 4), dtype=np.uint8) full_tile[:, :, :3] = 255 full_tile[:, :, 3] = tile return self.set_tile(codepoint, full_tile) - required = self.tile_shape + (4,) + required = (*self.tile_shape, 4) if tile.shape != required: note = "" if len(tile.shape) == 3 and tile.shape[2] == 3: @@ -151,12 +152,14 @@ def set_tile(self, codepoint: int, tile: Union[ArrayLike, NDArray[np.uint8]]) -> "\nNote: An RGB array is too ambiguous," " an alpha channel must be added to this array to divide the background/foreground areas." ) - raise ValueError(f"Tile shape must be {required} or {self.tile_shape}, got {tile.shape}.{note}") + msg = f"Tile shape must be {required} or {self.tile_shape}, got {tile.shape}.{note}" + raise ValueError(msg) lib.TCOD_tileset_set_tile_( self._tileset_p, codepoint, ffi.from_buffer("struct TCOD_ColorRGBA*", tile), ) + return None def render(self, console: tcod.console.Console) -> NDArray[np.uint8]: """Render an RGBA array, using console with this tileset. @@ -170,7 +173,8 @@ def render(self, console: tcod.console.Console) -> NDArray[np.uint8]: .. versionadded:: 11.9 """ if not console: - raise ValueError("'console' must not be the root console.") + msg = "'console' must not be the root console." + raise ValueError(msg) width = console.width * self.tile_width height = console.height * self.tile_height out: NDArray[np.uint8] = np.empty((height, width, 4), np.uint8) @@ -186,16 +190,15 @@ def render(self, console: tcod.console.Console) -> NDArray[np.uint8]: ), lib.SDL_FreeSurface, ) - with surface_p: - with ffi.new("SDL_Surface**", surface_p) as surface_p_p: - _check( - lib.TCOD_tileset_render_to_surface( - self._tileset_p, - _console(console), - ffi.NULL, - surface_p_p, - ) + with surface_p, ffi.new("SDL_Surface**", surface_p) as surface_p_p: + _check( + lib.TCOD_tileset_render_to_surface( + self._tileset_p, + _console(console), + ffi.NULL, + surface_p_p, ) + ) return out def remap(self, codepoint: int, x: int, y: int = 0) -> None: @@ -256,7 +259,7 @@ def set_default(tileset: Tileset) -> None: lib.TCOD_set_default_tileset(tileset._tileset_p) -def load_truetype_font(path: Union[str, PathLike[str]], tile_width: int, tile_height: int) -> Tileset: +def load_truetype_font(path: str | PathLike[str], tile_width: int, tile_height: int) -> Tileset: """Return a new Tileset from a `.ttf` or `.otf` file. Same as :any:`set_truetype_font`, but returns a :any:`Tileset` instead. @@ -264,9 +267,7 @@ def load_truetype_font(path: Union[str, PathLike[str]], tile_width: int, tile_he This function is provisional. The API may change. """ - path = Path(path) - if not path.exists(): - raise RuntimeError(f"File not found:\n\t{path.resolve()}") + path = Path(path).resolve(strict=True) cdata = lib.TCOD_load_truetype_font_(bytes(path), tile_width, tile_height) if not cdata: raise RuntimeError(ffi.string(lib.TCOD_get_error())) @@ -274,7 +275,7 @@ def load_truetype_font(path: Union[str, PathLike[str]], tile_width: int, tile_he @deprecate("Accessing the default tileset is deprecated.") -def set_truetype_font(path: Union[str, PathLike[str]], tile_width: int, tile_height: int) -> None: +def set_truetype_font(path: str | PathLike[str], tile_width: int, tile_height: int) -> None: """Set the default tileset from a `.ttf` or `.otf` file. `path` is the file path for the font file. @@ -294,14 +295,12 @@ def set_truetype_font(path: Union[str, PathLike[str]], tile_width: int, tile_hei This function does not support contexts. Use :any:`load_truetype_font` instead. """ - path = Path(path) - if not path.exists(): - raise RuntimeError(f"File not found:\n\t{path.resolve()}") + path = Path(path).resolve(strict=True) if lib.TCOD_tileset_load_truetype_(bytes(path), tile_width, tile_height): raise RuntimeError(ffi.string(lib.TCOD_get_error())) -def load_bdf(path: Union[str, PathLike[str]]) -> Tileset: +def load_bdf(path: str | PathLike[str]) -> Tileset: """Return a new Tileset from a `.bdf` file. For the best results the font should be monospace, cell-based, and @@ -313,19 +312,15 @@ def load_bdf(path: Union[str, PathLike[str]]) -> Tileset: take effect when `tcod.console_init_root` is called. .. versionadded:: 11.10 - """ # noqa: E501 - path = Path(path) - if not path.exists(): - raise RuntimeError(f"File not found:\n\t{path.resolve()}") + """ + path = Path(path).resolve(strict=True) cdata = lib.TCOD_load_bdf(bytes(path)) if not cdata: raise RuntimeError(ffi.string(lib.TCOD_get_error()).decode()) return Tileset._claim(cdata) -def load_tilesheet( - path: Union[str, PathLike[str]], columns: int, rows: int, charmap: Optional[Iterable[int]] -) -> Tileset: +def load_tilesheet(path: str | PathLike[str], columns: int, rows: int, charmap: Iterable[int] | None) -> Tileset: """Return a new Tileset from a simple tilesheet image. `path` is the file path to a PNG file with the tileset. @@ -344,9 +339,7 @@ def load_tilesheet( .. versionadded:: 11.12 """ - path = Path(path) - if not path.exists(): - raise RuntimeError(f"File not found:\n\t{path.resolve()}") + path = Path(path).resolve(strict=True) mapping = [] if charmap is not None: mapping = list(itertools.islice(charmap, columns * rows)) From 26f24067302510a53d68b2da3da9238e106bff78 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 26 May 2023 23:57:31 -0700 Subject: [PATCH 166/194] Remove no-pep420 linter rule. --- pyproject.toml | 1 - scripts/generate_charmap_table.py | 2 -- scripts/get_release_description.py | 2 -- scripts/tag_release.py | 2 +- 4 files changed, 1 insertion(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4df86fb1..e1f8bead 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -158,7 +158,6 @@ select = [ "EXE", # flake8-executable "RET", # flake8-return "ICN", # flake8-import-conventions - "INP", # flake8-no-pep420 "PIE", # flake8-pie "PT", # flake8-pytest-style "SIM", # flake8-simplify diff --git a/scripts/generate_charmap_table.py b/scripts/generate_charmap_table.py index 2e2931b5..0591cb9b 100755 --- a/scripts/generate_charmap_table.py +++ b/scripts/generate_charmap_table.py @@ -13,8 +13,6 @@ import tcod.tileset -# ruff: noqa: INP001 - def get_charmaps() -> Iterator[str]: """Return an iterator of the current character maps from tcod.tilest.""" diff --git a/scripts/get_release_description.py b/scripts/get_release_description.py index 0a2dffb3..7d47d9f9 100755 --- a/scripts/get_release_description.py +++ b/scripts/get_release_description.py @@ -5,8 +5,6 @@ import re from pathlib import Path -# ruff: noqa: INP001 - TAG_BANNER = r"## \[[\w.]*\] - \d+-\d+-\d+\n" RE_BODY = re.compile(rf".*?{TAG_BANNER}(.*?){TAG_BANNER}", re.DOTALL) diff --git a/scripts/tag_release.py b/scripts/tag_release.py index 92e896fe..70b77fb7 100755 --- a/scripts/tag_release.py +++ b/scripts/tag_release.py @@ -10,7 +10,7 @@ import sys from pathlib import Path -# ruff: noqa: INP001, S603, S607 +# ruff: noqa: S603, S607 PROJECT_DIR = Path(__file__).parent.parent From f07e6847500cbbf42525e7387f622c346b7ea3d7 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 27 May 2023 00:25:49 -0700 Subject: [PATCH 167/194] Add PathLike support to more libtcodpy functions. Remove long deprecated bytes to Unicode conversion. --- CHANGELOG.md | 5 ++ tcod/image.py | 7 ++- tcod/libtcodpy.py | 130 +++++++++++++++++++++++++++++++--------------- 3 files changed, 99 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5730d8a..e280b173 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] +### Added +- Added PathLike support to more libtcodpy functions. + +### Removed +- `tcod.console_set_custom_font` can no longer take bytes. ## [15.0.3] - 2023-05-25 ### Deprecated diff --git a/tcod/image.py b/tcod/image.py index 85e315ea..ced114d7 100644 --- a/tcod/image.py +++ b/tcod/image.py @@ -288,13 +288,16 @@ def blit_2x( img_height, ) - def save_as(self, filename: str) -> None: + def save_as(self, filename: str | PathLike[str]) -> None: """Save the Image to a 32-bit .bmp or .png file. Args: filename (Text): File path to same this Image. + + .. versionchanged:: Unreleased + Added PathLike support. """ - lib.TCOD_image_save(self.image_c, filename.encode("utf-8")) + lib.TCOD_image_save(self.image_c, bytes(Path(filename))) @property def __array_interface__(self) -> dict[str, Any]: diff --git a/tcod/libtcodpy.py b/tcod/libtcodpy.py index b06572ec..cb4d2c8a 100644 --- a/tcod/libtcodpy.py +++ b/tcod/libtcodpy.py @@ -5,8 +5,9 @@ import sys import threading import warnings +from os import PathLike from pathlib import Path -from typing import Any, AnyStr, Callable, Hashable, Iterable, Iterator, Sequence +from typing import Any, Callable, Hashable, Iterable, Iterator, Sequence import numpy as np from numpy.typing import NDArray @@ -954,7 +955,7 @@ def console_init_root( https://python-tcod.readthedocs.io/en/latest/tcod/getting-started.html""" ) def console_set_custom_font( - fontFile: AnyStr, + fontFile: str | PathLike[str], flags: int = FONT_LAYOUT_ASCII_INCOL, nb_char_horiz: int = 0, nb_char_vertic: int = 0, @@ -983,9 +984,12 @@ def console_set_custom_font( .. deprecated:: 11.13 Load fonts using :any:`tcod.tileset.load_tilesheet` instead. See :ref:`getting-started` for more info. + + .. versionchanged:: Unreleased + Added PathLike support. `fontFile` no longer takes bytes. """ - path = Path(_unicode(fontFile)).resolve(strict=True) - _check(lib.TCOD_console_set_custom_font(path, flags, nb_char_horiz, nb_char_vertic)) + fontFile = Path(fontFile).resolve(strict=True) + _check(lib.TCOD_console_set_custom_font(bytes(fontFile), flags, nb_char_horiz, nb_char_vertic)) @deprecate("Check `con.width` instead.") @@ -1780,7 +1784,7 @@ def console_new(w: int, h: int) -> tcod.console.Console: @deprecate("This loading method is no longer supported, use tcod.console_load_xp instead.") -def console_from_file(filename: str) -> tcod.console.Console: +def console_from_file(filename: str | PathLike[str]) -> tcod.console.Console: """Return a new console object from a filename. The file format is automatically determined. This can load REXPaint `.xp`, @@ -1795,9 +1799,12 @@ def console_from_file(filename: str) -> tcod.console.Console: Use :any:`tcod.console_load_xp` to load REXPaint consoles. Other formats are not actively supported. + + .. versionchanged:: Unreleased + Added PathLike support. """ - path = Path(filename).resolve(strict=True) - return tcod.console.Console._from_cdata(_check_p(lib.TCOD_console_from_file(bytes(path)))) + filename = Path(filename).resolve(strict=True) + return tcod.console.Console._from_cdata(_check_p(lib.TCOD_console_from_file(bytes(filename)))) @deprecate("Call the `Console.blit` method instead.") @@ -1966,76 +1973,106 @@ def console_fill_char(con: tcod.console.Console, arr: Sequence[int]) -> None: @deprecate("This format is not actively supported") -def console_load_asc(con: tcod.console.Console, filename: str) -> bool: +def console_load_asc(con: tcod.console.Console, filename: str | PathLike[str]) -> bool: """Update a console from a non-delimited ASCII `.asc` file. .. deprecated:: 12.7 This format is no longer supported. + + .. versionchanged:: Unreleased + Added PathLike support. """ - return bool(lib.TCOD_console_load_asc(_console(con), filename.encode("utf-8"))) + filename = Path(filename).resolve(strict=True) + return bool(lib.TCOD_console_load_asc(_console(con), bytes(filename))) @deprecate("This format is not actively supported") -def console_save_asc(con: tcod.console.Console, filename: str) -> bool: +def console_save_asc(con: tcod.console.Console, filename: str | PathLike[str]) -> bool: """Save a console to a non-delimited ASCII `.asc` file. .. deprecated:: 12.7 This format is no longer supported. + + .. versionchanged:: Unreleased + Added PathLike support. """ - return bool(lib.TCOD_console_save_asc(_console(con), filename.encode("utf-8"))) + return bool(lib.TCOD_console_save_asc(_console(con), bytes(Path(filename)))) @deprecate("This format is not actively supported") -def console_load_apf(con: tcod.console.Console, filename: str) -> bool: +def console_load_apf(con: tcod.console.Console, filename: str | PathLike[str]) -> bool: """Update a console from an ASCII Paint `.apf` file. .. deprecated:: 12.7 This format is no longer supported. + + .. versionchanged:: Unreleased + Added PathLike support. """ - return bool(lib.TCOD_console_load_apf(_console(con), filename.encode("utf-8"))) + filename = Path(filename).resolve(strict=True) + return bool(lib.TCOD_console_load_apf(_console(con), bytes(filename))) @deprecate("This format is not actively supported") -def console_save_apf(con: tcod.console.Console, filename: str) -> bool: +def console_save_apf(con: tcod.console.Console, filename: str | PathLike[str]) -> bool: """Save a console to an ASCII Paint `.apf` file. .. deprecated:: 12.7 This format is no longer supported. + + .. versionchanged:: Unreleased + Added PathLike support. """ - return bool(lib.TCOD_console_save_apf(_console(con), filename.encode("utf-8"))) + return bool(lib.TCOD_console_save_apf(_console(con), bytes(Path(filename)))) @deprecate("Use tcod.console.load_xp to load this file.") -def console_load_xp(con: tcod.console.Console, filename: str) -> bool: +def console_load_xp(con: tcod.console.Console, filename: str | PathLike[str]) -> bool: """Update a console from a REXPaint `.xp` file. .. deprecated:: 11.18 Functions modifying console objects in-place are deprecated. Use :any:`tcod.console_from_xp` to load a Console from a file. + + .. versionchanged:: Unreleased + Added PathLike support. """ - return bool(lib.TCOD_console_load_xp(_console(con), filename.encode("utf-8"))) + filename = Path(filename).resolve(strict=True) + return bool(lib.TCOD_console_load_xp(_console(con), bytes(filename))) @deprecate("Use tcod.console.save_xp to save this console.") -def console_save_xp(con: tcod.console.Console, filename: str, compress_level: int = 9) -> bool: - """Save a console to a REXPaint `.xp` file.""" - return bool(lib.TCOD_console_save_xp(_console(con), filename.encode("utf-8"), compress_level)) +def console_save_xp(con: tcod.console.Console, filename: str | PathLike[str], compress_level: int = 9) -> bool: + """Save a console to a REXPaint `.xp` file. + + .. versionchanged:: Unreleased + Added PathLike support. + """ + return bool(lib.TCOD_console_save_xp(_console(con), bytes(Path(filename)), compress_level)) @deprecate("Use tcod.console.load_xp to load this file.") -def console_from_xp(filename: str) -> tcod.console.Console: - """Return a single console from a REXPaint `.xp` file.""" - path = Path(filename).resolve(strict=True) - return tcod.console.Console._from_cdata(_check_p(lib.TCOD_console_from_xp(bytes(path)))) +def console_from_xp(filename: str | PathLike[str]) -> tcod.console.Console: + """Return a single console from a REXPaint `.xp` file. + + .. versionchanged:: Unreleased + Added PathLike support. + """ + filename = Path(filename).resolve(strict=True) + return tcod.console.Console._from_cdata(_check_p(lib.TCOD_console_from_xp(bytes(filename)))) @deprecate("Use tcod.console.load_xp to load this file.") def console_list_load_xp( - filename: str, + filename: str | PathLike[str], ) -> list[tcod.console.Console] | None: - """Return a list of consoles from a REXPaint `.xp` file.""" - path = Path(filename).resolve(strict=True) - tcod_list = lib.TCOD_console_list_from_xp(bytes(path)) + """Return a list of consoles from a REXPaint `.xp` file. + + .. versionchanged:: Unreleased + Added PathLike support. + """ + filename = Path(filename).resolve(strict=True) + tcod_list = lib.TCOD_console_list_from_xp(bytes(filename)) if tcod_list == ffi.NULL: return None try: @@ -2051,15 +2088,19 @@ def console_list_load_xp( @deprecate("Use tcod.console.save_xp to save these consoles.") def console_list_save_xp( console_list: Sequence[tcod.console.Console], - filename: str, + filename: str | PathLike[str], compress_level: int = 9, ) -> bool: - """Save a list of consoles to a REXPaint `.xp` file.""" + """Save a list of consoles to a REXPaint `.xp` file. + + .. versionchanged:: Unreleased + Added PathLike support. + """ tcod_list = lib.TCOD_list_new() try: for console in console_list: lib.TCOD_list_push(tcod_list, _console(console)) - return bool(lib.TCOD_console_list_save_xp(tcod_list, filename.encode("utf-8"), compress_level)) + return bool(lib.TCOD_console_list_save_xp(tcod_list, bytes(Path(filename)), compress_level)) finally: lib.TCOD_list_delete(tcod_list) @@ -2992,13 +3033,17 @@ def image_is_pixel_transparent(image: tcod.image.Image, x: int, y: int) -> bool: "This function may be removed in the future." " It's recommended to load images with a more complete image library such as python-Pillow or python-imageio." ) -def image_load(filename: str) -> tcod.image.Image: +def image_load(filename: str | PathLike[str]) -> tcod.image.Image: """Load an image file into an Image instance and return it. Args: - filename (AnyStr): Path to a .bmp or .png image file. + filename: Path to a .bmp or .png image file. + + .. versionchanged:: Unreleased + Added PathLike support. """ - return tcod.image.Image._from_cdata(ffi.gc(lib.TCOD_image_load(_bytes(filename)), lib.TCOD_image_delete)) + filename = Path(filename).resolve(strict=True) + return tcod.image.Image._from_cdata(ffi.gc(lib.TCOD_image_load(bytes(filename)), lib.TCOD_image_delete)) @pending_deprecate() @@ -3085,7 +3130,7 @@ def image_blit_2x( @pending_deprecate() -def image_save(image: tcod.image.Image, filename: str) -> None: +def image_save(image: tcod.image.Image, filename: str | PathLike[str]) -> None: image.save_as(filename) @@ -3382,8 +3427,8 @@ def mouse_get_status() -> Mouse: @pending_deprecate() -def namegen_parse(filename: str, random: tcod.random.Random | None = None) -> None: - lib.TCOD_namegen_parse(_bytes(filename), random or ffi.NULL) +def namegen_parse(filename: str | PathLike[str], random: tcod.random.Random | None = None) -> None: + lib.TCOD_namegen_parse(bytes(Path(filename)), random or ffi.NULL) @pending_deprecate() @@ -3583,10 +3628,10 @@ def _pycall_parser_error(msg: Any) -> None: @deprecate("Parser functions have been deprecated.") -def parser_run(parser: Any, filename: str, listener: Any = None) -> None: +def parser_run(parser: Any, filename: str | PathLike[str], listener: Any = None) -> None: global _parser_listener if not listener: - lib.TCOD_parser_run(parser, _bytes(filename), ffi.NULL) + lib.TCOD_parser_run(parser, bytes(Path(filename)), ffi.NULL) return propagate_manager = _PropagateException() @@ -3605,7 +3650,7 @@ def parser_run(parser: Any, filename: str, listener: Any = None) -> None: with _parser_callback_lock: _parser_listener = listener with propagate_manager: - lib.TCOD_parser_run(parser, _bytes(filename), c_listener) + lib.TCOD_parser_run(parser, bytes(Path(filename)), c_listener) @deprecate("libtcod objects are deleted automatically.") @@ -4007,7 +4052,7 @@ def sys_get_renderer() -> int: # easy screenshots @deprecate("This function is not supported if contexts are being used.") -def sys_save_screenshot(name: str | None = None) -> None: +def sys_save_screenshot(name: str | PathLike[str] | None = None) -> None: """Save a screenshot to a file. By default this will automatically save screenshots in the working @@ -4022,6 +4067,9 @@ def sys_save_screenshot(name: str | None = None) -> None: .. deprecated:: 11.13 This function is not supported by contexts. Use :any:`Context.save_screenshot` instead. + + .. versionchanged:: Unreleased + Added PathLike support. """ lib.TCOD_sys_save_screenshot(bytes(Path(name)) if name is not None else ffi.NULL) From 684ae5386eaeadfb501d8369349b9b66024f2312 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 27 May 2023 00:45:05 -0700 Subject: [PATCH 168/194] Cleanup pass of examples directory. Switch samples_tcod to use Path objects. --- .vscode/settings.json | 1 + examples/distribution/PyInstaller/main.py | 8 ++-- examples/distribution/cx_Freeze/main.py | 2 + examples/eventget.py | 6 +-- examples/framerate.py | 6 +-- examples/samples_tcod.py | 45 ++++++++++------------- examples/termbox/termbox.py | 17 +++++---- examples/termbox/termboxtest.py | 5 +-- examples/thread_jobs.py | 7 ++-- examples/ttf.py | 1 + 10 files changed, 48 insertions(+), 50 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 2ab94f02..18967232 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -145,6 +145,7 @@ "fmean", "fontx", "fonty", + "freetype", "frombuffer", "fullscreen", "fwidth", diff --git a/examples/distribution/PyInstaller/main.py b/examples/distribution/PyInstaller/main.py index b396d536..7f715351 100755 --- a/examples/distribution/PyInstaller/main.py +++ b/examples/distribution/PyInstaller/main.py @@ -3,20 +3,22 @@ # copyright and related or neighboring rights for the "hello world" PyInstaller # example script. This work is published from: United States. # https://creativecommons.org/publicdomain/zero/1.0/ -import os.path +"""PyInstaller main script example.""" import sys +from pathlib import Path import tcod WIDTH, HEIGHT = 80, 60 # The base directory, this is sys._MEIPASS when in one-file mode. -BASE_DIR = getattr(sys, "_MEIPASS", ".") +BASE_DIR = Path(getattr(sys, "_MEIPASS", ".")) -FONT_PATH = os.path.join(BASE_DIR, "data/terminal8x8_gs_ro.png") +FONT_PATH = BASE_DIR / "data/terminal8x8_gs_ro.png" def main() -> None: + """Entry point function.""" tileset = tcod.tileset.load_tilesheet(FONT_PATH, 16, 16, tcod.tileset.CHARMAP_CP437) with tcod.context.new(columns=WIDTH, rows=HEIGHT, tileset=tileset) as context: while True: diff --git a/examples/distribution/cx_Freeze/main.py b/examples/distribution/cx_Freeze/main.py index 6323407a..b1f51acb 100755 --- a/examples/distribution/cx_Freeze/main.py +++ b/examples/distribution/cx_Freeze/main.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +"""cx_Freeze main script example.""" import tcod WIDTH, HEIGHT = 80, 60 @@ -6,6 +7,7 @@ def main() -> None: + """Entry point function.""" tileset = tcod.tileset.load_tilesheet("data/terminal8x8_gs_ro.png", 16, 16, tcod.tileset.CHARMAP_CP437) with tcod.context.new(columns=WIDTH, rows=HEIGHT, tileset=tileset) as context: while True: diff --git a/examples/eventget.py b/examples/eventget.py index 52e26c09..32a12658 100755 --- a/examples/eventget.py +++ b/examples/eventget.py @@ -3,8 +3,7 @@ # copyright and related or neighboring rights for this example. This work is # published from: United States. # https://creativecommons.org/publicdomain/zero/1.0/ -"""An demonstration of event handling using the tcod.event module. -""" +"""An demonstration of event handling using the tcod.event module.""" from typing import List, Set import tcod @@ -15,8 +14,7 @@ def main() -> None: - """Example program for tcod.event""" - + """Example program for tcod.event.""" event_log: List[str] = [] motion_desc = "" tcod.sdl.joystick.init() diff --git a/examples/framerate.py b/examples/framerate.py index 39ef6ee6..3a9e9f81 100755 --- a/examples/framerate.py +++ b/examples/framerate.py @@ -3,8 +3,7 @@ # copyright and related or neighboring rights for this example. This work is # published from: United States. # https://creativecommons.org/publicdomain/zero/1.0/ -"""A system to control time since the original libtcod tools are deprecated. -""" +"""A system to control time since the original libtcod tools are deprecated.""" import statistics import time from collections import deque @@ -23,6 +22,7 @@ class Clock: """ def __init__(self) -> None: + """Initialize this object with empty data.""" self.last_time = time.perf_counter() # Last time this was synced. self.time_samples: Deque[float] = deque() # Delta time samples. self.max_samples = 64 # Number of fps samples to log. Can be changed. @@ -138,7 +138,7 @@ def main() -> None: context.convert_event(event) # Set tile coordinates for event. if isinstance(event, tcod.event.Quit): raise SystemExit() - elif isinstance(event, tcod.event.MouseWheel): + if isinstance(event, tcod.event.MouseWheel): desired_fps = max(1, desired_fps + event.y) diff --git a/examples/samples_tcod.py b/examples/samples_tcod.py index bf0f7101..8b1d375d 100755 --- a/examples/samples_tcod.py +++ b/examples/samples_tcod.py @@ -7,12 +7,12 @@ import copy import math -import os import random import sys import time import warnings -from typing import Any, List +from pathlib import Path +from typing import Any import numpy as np from numpy.typing import NDArray @@ -21,19 +21,15 @@ import tcod.render import tcod.sdl.render +# ruff: noqa: S311 + if not sys.warnoptions: warnings.simplefilter("default") # Show all warnings. +DATA_DIR = Path(__file__).parent / "../libtcod/data" +"""Path of the samples data directory.""" -def get_data(path: str) -> str: - """Return the path to a resource in the libtcod data directory,""" - SCRIPT_DIR = os.path.dirname(__file__) - DATA_DIR = os.path.join(SCRIPT_DIR, "../libtcod/data") - assert os.path.exists(DATA_DIR), ( - "Data directory is missing," " did you forget to run `git submodule update --init`?" - ) - return os.path.join(DATA_DIR, path) - +assert DATA_DIR.exists(), "Data directory is missing, did you forget to run `git submodule update --init`?" WHITE = (255, 255, 255) GREY = (127, 127, 127) @@ -45,7 +41,7 @@ def get_data(path: str) -> str: SAMPLE_SCREEN_HEIGHT = 20 SAMPLE_SCREEN_X = 20 SAMPLE_SCREEN_Y = 10 -FONT = get_data("fonts/dejavu10x10_gs_tc.png") +FONT = DATA_DIR / "fonts/dejavu10x10_gs_tc.png" # Mutable global names. context: tcod.context.Context @@ -564,10 +560,9 @@ def draw_ui(self) -> None: 1, 1, "IJKL : move around\n" - "T : torch fx %s\n" - "W : light walls %s\n" - "+-: algo %s" - % ( + "T : torch fx {}\n" + "W : light walls {}\n" + "+-: algo {}".format( "on " if self.torch else "off", "on " if self.light_walls else "off", FOV_ALGO_NAMES[self.algo_num], @@ -1032,9 +1027,9 @@ class ImageSample(Sample): def __init__(self) -> None: self.name = "Image toolkit" - self.img = tcod.image_load(get_data("img/skull.png")) + self.img = tcod.image_load(DATA_DIR / "img/skull.png") self.img.set_key_color(BLACK) - self.circle = tcod.image_load(get_data("img/circle.png")) + self.circle = tcod.image_load(DATA_DIR / "img/circle.png") def on_draw(self) -> None: sample_console.clear() @@ -1068,7 +1063,7 @@ def __init__(self) -> None: self.motion = tcod.event.MouseMotion() self.mouse_left = self.mouse_middle = self.mouse_right = 0 - self.log: List[str] = [] + self.log: list[str] = [] def on_enter(self) -> None: tcod.mouse_move(320, 200) @@ -1141,15 +1136,15 @@ def __init__(self) -> None: self.curset = 0 self.delay = 0.0 - self.names: List[str] = [] - self.sets: List[str] = [] + self.names: list[str] = [] + self.sets: list[str] = [] def on_draw(self) -> None: if not self.sets: # parse all *.cfg files in data/namegen - for file in os.listdir(get_data("namegen")): - if file.find(".cfg") > 0: - tcod.namegen_parse(get_data(os.path.join("namegen", file))) + for file in (DATA_DIR / "namegen").iterdir(): + if file.suffix == ".cfg": + tcod.namegen_parse(file) # get the sets list self.sets = tcod.namegen_get_sets() print(self.sets) @@ -1254,7 +1249,7 @@ def on_enter(self) -> None: self.frac_t: float = RES_V - 1 self.abs_t: float = RES_V - 1 # light and current color of the tunnel texture - self.lights: List[Light] = [] + self.lights: list[Light] = [] self.tex_r = 0.0 self.tex_g = 0.0 self.tex_b = 0.0 diff --git a/examples/termbox/termbox.py b/examples/termbox/termbox.py index d97d9110..26ab2d7a 100755 --- a/examples/termbox/termbox.py +++ b/examples/termbox/termbox.py @@ -1,5 +1,4 @@ -""" -Implementation of Termbox Python API in tdl. +"""Implementation of Termbox Python API in tdl. See README.md for details. """ @@ -18,10 +17,10 @@ class TermboxException(Exception): - def __init__(self, msg): + def __init__(self, msg) -> None: self.msg = msg - def __str__(self): + def __str__(self) -> str: return self.msg @@ -153,7 +152,7 @@ def __str__(self): class Event: - """Aggregate for Termbox Event structure""" + """Aggregate for Termbox Event structure.""" type = None ch = None @@ -169,10 +168,11 @@ def gettuple(self): class Termbox: - def __init__(self, width=132, height=60): + def __init__(self, width=132, height=60) -> None: global _instance if _instance: - raise TermboxException("It is possible to create only one instance of Termbox") + msg = "It is possible to create only one instance of Termbox" + raise TermboxException(msg) try: self.console = tdl.init(width, height) @@ -183,7 +183,7 @@ def __init__(self, width=132, height=60): _instance = self - def __del__(self): + def __del__(self) -> None: self.close() def __exit__(self, *args): # t, value, traceback): @@ -289,5 +289,6 @@ def poll_event(self): if e.type == "KEYDOWN": self.e.key = e.key return self.e.gettuple() + return None # return (e.type, uch, e.key, e.mod, e.w, e.h, e.x, e.y) diff --git a/examples/termbox/termboxtest.py b/examples/termbox/termboxtest.py index 9b43cfae..c0d57065 100755 --- a/examples/termbox/termboxtest.py +++ b/examples/termbox/termboxtest.py @@ -1,5 +1,4 @@ #!/usr/bin/python -# -*- encoding: utf-8 -*- import termbox @@ -17,8 +16,8 @@ def print_line(t, msg, y, fg, bg): t.change_cell(x + i, y, c, fg, bg) -class SelectBox(object): - def __init__(self, tb, choices, active=-1): +class SelectBox: + def __init__(self, tb, choices, active=-1) -> None: self.tb = tb self.active = active self.choices = choices diff --git a/examples/thread_jobs.py b/examples/thread_jobs.py index 82d88358..b935c91f 100755 --- a/examples/thread_jobs.py +++ b/examples/thread_jobs.py @@ -42,7 +42,7 @@ def test_fov_single(maps: List[tcod.map.Map]) -> None: def test_fov_threads(executor: concurrent.futures.Executor, maps: List[tcod.map.Map]) -> None: - for result in executor.map(test_fov, maps): + for _result in executor.map(test_fov, maps): pass @@ -57,7 +57,7 @@ def test_astar_single(maps: List[tcod.map.Map]) -> None: def test_astar_threads(executor: concurrent.futures.Executor, maps: List[tcod.map.Map]) -> None: - for result in executor.map(test_astar, maps): + for _result in executor.map(test_astar, maps): pass @@ -66,8 +66,7 @@ def run_test( single_func: Callable[[List[tcod.map.Map]], None], multi_func: Callable[[concurrent.futures.Executor, List[tcod.map.Map]], None], ) -> None: - """Run a function designed for a single thread and compare it to a threaded - version. + """Run a function designed for a single thread and compare it to a threaded version. This prints the results of these tests. """ diff --git a/examples/ttf.py b/examples/ttf.py index 7268109f..de134165 100755 --- a/examples/ttf.py +++ b/examples/ttf.py @@ -59,6 +59,7 @@ def load_ttf(path: str, size: Tuple[int, int]) -> tcod.tileset.Tileset: def main() -> None: + """True-type font example script.""" console = tcod.Console(16, 12, order="F") with tcod.context.new( columns=console.width, From 74c7bd1bf13ed2d48dfaabcc625edaafbc34cfb5 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 27 May 2023 01:09:46 -0700 Subject: [PATCH 169/194] Fix missing imports in docstrings. These were being included in the module imports before, but I'd rather have them here or in conftest.py. --- tcod/path.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tcod/path.py b/tcod/path.py index e7b648c5..9e2439c8 100644 --- a/tcod/path.py +++ b/tcod/path.py @@ -1131,6 +1131,7 @@ def traversal(self) -> NDArray[Any]: Example:: # This example demonstrates the purpose of the traversal array. + >>> import tcod.path >>> graph = tcod.path.SimpleGraph( ... cost=np.ones((5, 5), np.int8), cardinal=2, diagonal=3, ... ) @@ -1240,6 +1241,7 @@ def resolve(self, goal: tuple[int, ...] | None = None) -> None: Example:: + >>> import tcod.path >>> graph = tcod.path.SimpleGraph( ... cost=np.ones((4, 4), np.int8), cardinal=2, diagonal=3, ... ) @@ -1300,6 +1302,7 @@ def path_from(self, index: tuple[int, ...]) -> NDArray[Any]: Example:: + >>> import tcod.path >>> cost = np.ones((5, 5), dtype=np.int8) >>> cost[:, 3:] = 0 >>> graph = tcod.path.SimpleGraph(cost=cost, cardinal=2, diagonal=3) @@ -1344,6 +1347,7 @@ def path_to(self, index: tuple[int, ...]) -> NDArray[Any]: Example:: + >>> import tcod.path >>> graph = tcod.path.SimpleGraph( ... cost=np.ones((5, 5), np.int8), cardinal=2, diagonal=3, ... ) From 3a9122731e2e9117c1e6e9f85f09e9679696f257 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 27 May 2023 01:28:12 -0700 Subject: [PATCH 170/194] Remove setup.cfg, config moved to pyproject.toml --- pyproject.toml | 3 +++ setup.cfg | 5 ----- 2 files changed, 3 insertions(+), 5 deletions(-) delete mode 100644 setup.cfg diff --git a/pyproject.toml b/pyproject.toml index e1f8bead..81bab905 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,6 +69,9 @@ Source = "https://github.com/libtcod/python-tcod" Tracker = "https://github.com/libtcod/python-tcod/issues" Forum = "https://github.com/libtcod/python-tcod/discussions" +[tool.distutils.bdist_wheel] +py-limited-api = "cp37" + [tool.setuptools_scm] write_to = "tcod/version.py" diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 9cc8e393..00000000 --- a/setup.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[bdist_wheel] -py-limited-api = cp36 - -[aliases] -test=pytest From 49ddf4c4527e921d41508b0a40a15390bad4b909 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 27 May 2023 01:35:23 -0700 Subject: [PATCH 171/194] Update Codecov settings Allow comment, but only on coverage changes. --- codecov.yml => .codecov.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) rename codecov.yml => .codecov.yml (60%) diff --git a/codecov.yml b/.codecov.yml similarity index 60% rename from codecov.yml rename to .codecov.yml index dd31e005..79c5a87d 100644 --- a/codecov.yml +++ b/.codecov.yml @@ -3,6 +3,7 @@ coverage: project: default: target: auto - threshold: 0.1 + threshold: "0.1" base: auto -comment: off +comment: + require_changes: true From 1613480f5d8475fc515fbc8931ef25739c5fd1a7 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 27 May 2023 02:14:11 -0700 Subject: [PATCH 172/194] Fix or suppress all Ruff warnings in test scripts. --- pyproject.toml | 2 ++ tests/conftest.py | 15 +++++---- tests/test_console.py | 7 +++-- tests/test_libtcodpy.py | 70 +++++++++++++++++++---------------------- tests/test_noise.py | 13 +++++--- tests/test_parser.py | 15 +++++---- tests/test_random.py | 18 ++++++----- tests/test_sdl.py | 11 ++++--- tests/test_tcod.py | 33 ++++++++++--------- tests/test_testing.py | 10 ------ tests/test_tileset.py | 3 ++ 11 files changed, 103 insertions(+), 94 deletions(-) delete mode 100644 tests/test_testing.py diff --git a/pyproject.toml b/pyproject.toml index 81bab905..4c1c47a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -174,6 +174,8 @@ select = [ ignore = [ "E501", # line-too-long "S101", # assert + "S301", # suspicious-pickle-usage + "S311", # suspicious-non-cryptographic-random-usage "ANN101", # missing-type-self "ANN102", # missing-type-cls "D203", # one-blank-line-before-class diff --git a/tests/conftest.py b/tests/conftest.py index 0b3fb63b..163d0918 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,18 +1,21 @@ +"""Test directory configuration.""" import random import warnings -from typing import Any, Callable, Iterator, Union +from typing import Callable, Iterator, Union import pytest import tcod +# ruff: noqa: D103 -def pytest_addoption(parser: Any) -> None: + +def pytest_addoption(parser: pytest.Parser) -> None: parser.addoption("--no-window", action="store_true", help="Skip tests which need a rendering context.") @pytest.fixture(scope="session", params=["SDL", "SDL2"]) -def session_console(request: Any) -> Iterator[tcod.console.Console]: +def session_console(request: pytest.FixtureRequest) -> Iterator[tcod.console.Console]: if request.config.getoption("--no-window"): pytest.skip("This test needs a rendering context.") FONT_FILE = "libtcod/terminal.png" @@ -27,7 +30,7 @@ def session_console(request: Any) -> Iterator[tcod.console.Console]: yield con -@pytest.fixture(scope="function") +@pytest.fixture() def console(session_console: tcod.console.Console) -> tcod.console.Console: console = session_console tcod.console_flush() @@ -81,6 +84,6 @@ def ch_latin1_str() -> str: "latin1_str", ] ) -def ch(request: Any) -> Callable[[], Union[int, str]]: - """Test with multiple types of ascii/latin1 characters""" +def ch(request: pytest.FixtureRequest) -> Callable[[], Union[int, str]]: + """Test with multiple types of ascii/latin1 characters.""" return globals()["ch_%s" % request.param]() # type: ignore diff --git a/tests/test_console.py b/tests/test_console.py index 790ddcfe..5ddc6f8e 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -1,12 +1,14 @@ +"""Tests for tcod.console.""" import pickle from pathlib import Path import numpy as np import pytest -from numpy import array import tcod +# ruff: noqa: D103 + @pytest.mark.filterwarnings("ignore:Directly access a consoles") @pytest.mark.filterwarnings("ignore:This function may be deprecated in the fu") @@ -95,7 +97,8 @@ def test_console_pickle_fortran() -> None: def test_console_repr() -> None: - array # Needed for eval. + from numpy import array # noqa: F401 # Used for eval + eval(repr(tcod.console.Console(10, 2))) diff --git a/tests/test_libtcodpy.py b/tests/test_libtcodpy.py index 33cba999..5803c625 100644 --- a/tests/test_libtcodpy.py +++ b/tests/test_libtcodpy.py @@ -1,6 +1,7 @@ +"""Tests for the libtcodpy API.""" +from pathlib import Path from typing import Any, Callable, Iterator, List, Optional, Tuple, Union -import numpy import numpy as np import pytest from numpy.typing import NDArray @@ -8,6 +9,8 @@ import tcod import tcod as libtcodpy +# ruff: noqa: D103 + pytestmark = [ pytest.mark.filterwarnings("ignore::DeprecationWarning"), pytest.mark.filterwarnings("ignore::PendingDeprecationWarning"), @@ -29,7 +32,7 @@ def test_credits(console: tcod.console.Console) -> None: libtcodpy.console_credits_reset() -def assert_char( +def assert_char( # noqa: PLR0913 console: tcod.console.Console, x: int, y: int, @@ -143,20 +146,20 @@ def test_console_blit(console: tcod.console.Console, offscreen: tcod.console.Con @pytest.mark.filterwarnings("ignore") -def test_console_asc_read_write(console: tcod.console.Console, offscreen: tcod.console.Console, tmpdir: Any) -> None: +def test_console_asc_read_write(console: tcod.console.Console, offscreen: tcod.console.Console, tmp_path: Path) -> None: libtcodpy.console_print(console, 0, 0, "test") - asc_file = tmpdir.join("test.asc").strpath + asc_file = tmp_path / "test.asc" assert libtcodpy.console_save_asc(console, asc_file) assert libtcodpy.console_load_asc(offscreen, asc_file) assertConsolesEqual(console, offscreen) @pytest.mark.filterwarnings("ignore") -def test_console_apf_read_write(console: tcod.console.Console, offscreen: tcod.console.Console, tmpdir: Any) -> None: +def test_console_apf_read_write(console: tcod.console.Console, offscreen: tcod.console.Console, tmp_path: Path) -> None: libtcodpy.console_print(console, 0, 0, "test") - apf_file = tmpdir.join("test.apf").strpath + apf_file = tmp_path / "test.apf" assert libtcodpy.console_save_apf(console, apf_file) assert libtcodpy.console_load_apf(offscreen, apf_file) assertConsolesEqual(console, offscreen) @@ -176,14 +179,14 @@ def test_console_rexpaint_load_test_file(console: tcod.console.Console) -> None: @pytest.mark.filterwarnings("ignore") def test_console_rexpaint_save_load( console: tcod.console.Console, - tmpdir: Any, + tmp_path: Path, ch: int, fg: Tuple[int, int, int], bg: Tuple[int, int, int], ) -> None: libtcodpy.console_print(console, 0, 0, "test") libtcodpy.console_put_char_ex(console, 1, 1, ch, fg, bg) - xp_file = tmpdir.join("test.xp").strpath + xp_file = tmp_path / "test.xp" assert libtcodpy.console_save_xp(console, xp_file, 1) xp_console = libtcodpy.console_from_xp(xp_file) assert xp_console @@ -193,12 +196,12 @@ def test_console_rexpaint_save_load( @pytest.mark.filterwarnings("ignore") -def test_console_rexpaint_list_save_load(console: tcod.console.Console, tmpdir: Any) -> None: +def test_console_rexpaint_list_save_load(console: tcod.console.Console, tmp_path: Path) -> None: con1 = libtcodpy.console_new(8, 2) con2 = libtcodpy.console_new(8, 2) libtcodpy.console_print(con1, 0, 0, "hello") libtcodpy.console_print(con2, 0, 0, "world") - xp_file = tmpdir.join("test.xp").strpath + xp_file = tmp_path / "test.xp" assert libtcodpy.console_list_save_xp([con1, con2], xp_file, 1) loaded_consoles = libtcodpy.console_list_load_xp(xp_file) assert loaded_consoles @@ -252,7 +255,7 @@ def test_console_fill(console: tcod.console.Console) -> None: def test_console_fill_numpy(console: tcod.console.Console) -> None: width = libtcodpy.console_get_width(console) height = libtcodpy.console_get_height(console) - fill: NDArray[np.intc] = numpy.zeros((height, width), dtype=np.intc) + fill: NDArray[np.intc] = np.zeros((height, width), dtype=np.intc) for y in range(height): fill[y, :] = y % 256 @@ -261,9 +264,9 @@ def test_console_fill_numpy(console: tcod.console.Console) -> None: libtcodpy.console_fill_char(console, fill) # type: ignore # verify fill - bg: NDArray[np.intc] = numpy.zeros((height, width), dtype=numpy.intc) - fg: NDArray[np.intc] = numpy.zeros((height, width), dtype=numpy.intc) - ch: NDArray[np.intc] = numpy.zeros((height, width), dtype=numpy.intc) + bg: NDArray[np.intc] = np.zeros((height, width), dtype=np.intc) + fg: NDArray[np.intc] = np.zeros((height, width), dtype=np.intc) + ch: NDArray[np.intc] = np.zeros((height, width), dtype=np.intc) for y in range(height): for x in range(width): bg[y, x] = libtcodpy.console_get_char_background(console, x, y)[0] @@ -291,7 +294,7 @@ def test_console_buffer(console: tcod.console.Console) -> None: @pytest.mark.filterwarnings("ignore:Console array attributes perform better") def test_console_buffer_error(console: tcod.console.Console) -> None: buffer = libtcodpy.ConsoleBuffer(0, 0) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=r".*Destination console has an incorrect size."): buffer.blit(console) @@ -322,8 +325,8 @@ def test_sys_time(console: tcod.console.Console) -> None: @pytest.mark.filterwarnings("ignore") -def test_sys_screenshot(console: tcod.console.Console, tmpdir: Any) -> None: - libtcodpy.sys_save_screenshot(tmpdir.join("test.png").strpath) +def test_sys_screenshot(console: tcod.console.Console, tmp_path: Path) -> None: + libtcodpy.sys_save_screenshot(tmp_path / "test.png") @pytest.mark.filterwarnings("ignore") @@ -333,7 +336,7 @@ def test_sys_custom_render(console: tcod.console.Console) -> None: escape = [] - def sdl_callback(sdl_surface: Any) -> None: + def sdl_callback(sdl_surface: object) -> None: escape.append(True) libtcodpy.sys_register_SDL_renderer(sdl_callback) @@ -342,7 +345,7 @@ def sdl_callback(sdl_surface: Any) -> None: @pytest.mark.filterwarnings("ignore") -def test_image(console: tcod.console.Console, tmpdir: Any) -> None: +def test_image(console: tcod.console.Console, tmp_path: Path) -> None: img = libtcodpy.image_new(16, 16) libtcodpy.image_clear(img, (0, 0, 0)) libtcodpy.image_invert(img) @@ -360,7 +363,7 @@ def test_image(console: tcod.console.Console, tmpdir: Any) -> None: libtcodpy.image_blit(img, console, 0, 0, libtcodpy.BKGND_SET, 1, 1, 0) libtcodpy.image_blit_rect(img, console, 0, 0, 16, 16, libtcodpy.BKGND_SET) libtcodpy.image_blit_2x(img, console, 0, 0) - libtcodpy.image_save(img, tmpdir.join("test.png").strpath) + libtcodpy.image_save(img, tmp_path / "test.png") libtcodpy.image_delete(img) img = libtcodpy.image_from_console(console) @@ -385,14 +388,12 @@ def test_clipboard(console: tcod.console.Console, sample: str) -> None: # arguments to test with and the results expected from these arguments LINE_ARGS = (-5, 0, 5, 10) EXCLUSIVE_RESULTS = [(-4, 1), (-3, 2), (-2, 3), (-1, 4), (0, 5), (1, 6), (2, 7), (3, 8), (4, 9), (5, 10)] -INCLUSIVE_RESULTS = [(-5, 0)] + EXCLUSIVE_RESULTS +INCLUSIVE_RESULTS = [(-5, 0), *EXCLUSIVE_RESULTS] @pytest.mark.filterwarnings("ignore") def test_line_step() -> None: - """ - libtcodpy.line_init and libtcodpy.line_step - """ + """libtcodpy.line_init and libtcodpy.line_step.""" libtcodpy.line_init(*LINE_ARGS) for expected_xy in EXCLUSIVE_RESULTS: assert libtcodpy.line_step() == expected_xy @@ -401,9 +402,7 @@ def test_line_step() -> None: @pytest.mark.filterwarnings("ignore") def test_line() -> None: - """ - tests normal use, lazy evaluation, and error propagation - """ + """Tests normal use, lazy evaluation, and error propagation.""" # test normal results test_result: List[Tuple[int, int]] = [] @@ -427,17 +426,13 @@ def return_false(x: int, y: int) -> bool: @pytest.mark.filterwarnings("ignore") def test_line_iter() -> None: - """ - libtcodpy.line_iter - """ + """libtcodpy.line_iter.""" assert list(libtcodpy.line_iter(*LINE_ARGS)) == INCLUSIVE_RESULTS @pytest.mark.filterwarnings("ignore") def test_bsp() -> None: - """ - commented out statements work in libtcod-cffi - """ + """Commented out statements work in libtcod-cffi.""" bsp = libtcodpy.bsp_new_with_size(0, 0, 64, 64) repr(bsp) # test __repr__ on leaf libtcodpy.bsp_resize(bsp, 0, 0, 32, 32) @@ -479,7 +474,7 @@ def test_bsp() -> None: libtcodpy.bsp_split_recursive(bsp, None, 4, 2, 2, 1.0, 1.0) # cover bsp_traverse - def traverse(node: tcod.bsp.BSP, user_data: Any) -> None: + def traverse(node: tcod.bsp.BSP, user_data: object) -> None: return None libtcodpy.bsp_traverse_pre_order(bsp, traverse) @@ -498,9 +493,10 @@ def traverse(node: tcod.bsp.BSP, user_data: Any) -> None: @pytest.mark.filterwarnings("ignore") def test_map() -> None: - map = libtcodpy.map_new(16, 16) - assert libtcodpy.map_get_width(map) == 16 - assert libtcodpy.map_get_height(map) == 16 + WIDTH, HEIGHT = 13, 17 + map = libtcodpy.map_new(WIDTH, HEIGHT) + assert libtcodpy.map_get_width(map) == WIDTH + assert libtcodpy.map_get_height(map) == HEIGHT libtcodpy.map_copy(map, map) libtcodpy.map_clear(map) libtcodpy.map_set_properties(map, 0, 0, True, True) diff --git a/tests/test_noise.py b/tests/test_noise.py index 2067b852..6a653f06 100644 --- a/tests/test_noise.py +++ b/tests/test_noise.py @@ -1,3 +1,4 @@ +"""Tests for the tcod.noise module.""" import copy import pickle @@ -6,6 +7,8 @@ import tcod +# ruff: noqa: D103 + @pytest.mark.parametrize("implementation", tcod.noise.Implementation) @pytest.mark.parametrize("algorithm", tcod.noise.Algorithm) @@ -28,7 +31,7 @@ def test_noise_class( octaves=octaves, ) # cover attributes - assert noise.dimensions == 2 + assert noise.dimensions == 2 # noqa: PLR2004 noise.algorithm = noise.algorithm noise.implementation = noise.implementation noise.octaves = noise.octaves @@ -57,14 +60,14 @@ def test_noise_samples() -> None: def test_noise_errors() -> None: - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=r"dimensions must be in range"): tcod.noise.Noise(0) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=r"-1 is not a valid implementation"): tcod.noise.Noise(1, implementation=-1) noise = tcod.noise.Noise(2) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=r"mgrid.shape\[0\] must equal self.dimensions"): noise.sample_mgrid(np.mgrid[:2, :2, :2]) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=r"len\(ogrid\) must equal self.dimensions"): noise.sample_ogrid(np.ogrid[:2, :2, :2]) with pytest.raises(IndexError): noise[0, 0, 0, 0, 0] diff --git a/tests/test_parser.py b/tests/test_parser.py index 9a1dad7f..ce200166 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,10 +1,13 @@ -import os +"""Test old libtcodpy parser.""" +from pathlib import Path from typing import Any import pytest import tcod as libtcod +# ruff: noqa: D103 + @pytest.mark.filterwarnings("ignore") def test_parser() -> None: @@ -27,7 +30,7 @@ def test_parser() -> None: # default listener print("***** Default listener *****") - libtcod.parser_run(parser, os.path.join("libtcod", "data", "cfg", "sample.cfg")) + libtcod.parser_run(parser, Path("libtcod/data/cfg/sample.cfg")) print("bool_field : ", libtcod.parser_get_bool_property(parser, "myStruct.bool_field")) print("char_field : ", libtcod.parser_get_char_property(parser, "myStruct.char_field")) print("int_field : ", libtcod.parser_get_int_property(parser, "myStruct.int_field")) @@ -46,7 +49,7 @@ def test_parser() -> None: print("***** Custom listener *****") class MyListener: - def new_struct(self, struct: Any, name: str) -> bool: + def new_struct(self, struct: Any, name: str) -> bool: # noqa: ANN401 print("new structure type", libtcod.struct_get_name(struct), " named ", name) return True @@ -54,7 +57,7 @@ def new_flag(self, name: str) -> bool: print("new flag named ", name) return True - def new_property(self, name: str, typ: int, value: Any) -> bool: + def new_property(self, name: str, typ: int, value: Any) -> bool: # noqa: ANN401 type_names = ["NONE", "BOOL", "CHAR", "INT", "FLOAT", "STRING", "COLOR", "DICE"] type_name = type_names[typ & 0xFF] if typ & libtcod.TYPE_LIST: @@ -62,7 +65,7 @@ def new_property(self, name: str, typ: int, value: Any) -> bool: print("new property named ", name, " type ", type_name, " value ", value) return True - def end_struct(self, struct: Any, name: str) -> bool: + def end_struct(self, struct: Any, name: str) -> bool: # noqa: ANN401 print("end structure type", libtcod.struct_get_name(struct), " named ", name) return True @@ -70,7 +73,7 @@ def error(self, msg: str) -> bool: print("error : ", msg) return True - libtcod.parser_run(parser, os.path.join("libtcod", "data", "cfg", "sample.cfg"), MyListener()) + libtcod.parser_run(parser, Path("libtcod/data/cfg/sample.cfg"), MyListener()) if __name__ == "__main__": diff --git a/tests/test_random.py b/tests/test_random.py index ce5875e3..c56c6678 100644 --- a/tests/test_random.py +++ b/tests/test_random.py @@ -1,14 +1,19 @@ +"""Test random number generators.""" import copy -import pathlib import pickle +from pathlib import Path import tcod +# ruff: noqa: D103 + +SCRIPT_DIR = Path(__file__).parent + def test_tcod_random() -> None: rand = tcod.random.Random(tcod.random.COMPLEMENTARY_MULTIPLY_WITH_CARRY) - assert 0 <= rand.randint(0, 100) <= 100 - assert 0 <= rand.uniform(0, 100) <= 100 + assert 0 <= rand.randint(0, 100) <= 100 # noqa: PLR2004 + assert 0 <= rand.uniform(0, 100) <= 100 # noqa: PLR2004 rand.guass(0, 1) rand.inverse_guass(0, 1) @@ -30,7 +35,6 @@ def test_tcod_random_pickle() -> None: def test_load_rng_v13_1() -> None: - with open(pathlib.Path(__file__).parent / "data/random_v13.pkl", "rb") as f: - rand: tcod.random.Random = pickle.load(f) - assert rand.randint(0, 0xFFFF) == 56422 - assert rand.randint(0, 0xFFFF) == 15795 + rand: tcod.random.Random = pickle.loads((SCRIPT_DIR / "data/random_v13.pkl").read_bytes()) + assert rand.randint(0, 0xFFFF) == 56422 # noqa: PLR2004 + assert rand.randint(0, 0xFFFF) == 15795 # noqa: PLR2004 diff --git a/tests/test_sdl.py b/tests/test_sdl.py index 84dcf3a7..b7bf8571 100644 --- a/tests/test_sdl.py +++ b/tests/test_sdl.py @@ -1,3 +1,4 @@ +"""Test SDL specific features.""" import contextlib import sys @@ -9,6 +10,8 @@ import tcod.sdl.sys import tcod.sdl.video +# ruff: noqa: D103 + def test_sdl_window() -> None: assert tcod.sdl.video.get_grabbed_window() is None @@ -22,14 +25,14 @@ def test_sdl_window() -> None: assert window.title == sys.argv[0] window.title = "Title" assert window.title == "Title" - assert window.opacity == 1.0 + assert window.opacity == 1.0 # noqa: PLR2004 window.position = window.position window.fullscreen = window.fullscreen window.resizable = window.resizable window.size = window.size window.min_size = window.min_size window.max_size = window.max_size - window.border_size + window.border_size # noqa: B018 window.set_icon(np.zeros((32, 32, 3), dtype=np.uint8)) with pytest.raises(TypeError): window.set_icon(np.zeros((32, 32, 5), dtype=np.uint8)) @@ -84,9 +87,9 @@ def test_sdl_render_bad_types() -> None: def test_sdl_audio_device() -> None: with contextlib.closing(tcod.sdl.audio.open(frequency=44100, format=np.float32, channels=2, paused=True)) as device: - assert device.convert(np.zeros(4, dtype=np.float32), 22050).shape[0] == 8 + assert device.convert(np.zeros(4, dtype=np.float32), 22050).shape[0] == 8 # noqa: PLR2004 assert device.convert(np.zeros((4, 4), dtype=np.float32)).shape == (4, 2) - assert device.convert(np.zeros(4, dtype=np.int8)).shape[0] == 4 + assert device.convert(np.zeros(4, dtype=np.int8)).shape[0] == 4 # noqa: PLR2004 device.paused = False device.paused = True assert device.queued_samples == 0 diff --git a/tests/test_tcod.py b/tests/test_tcod.py index d3216e02..5c731f48 100644 --- a/tests/test_tcod.py +++ b/tests/test_tcod.py @@ -1,3 +1,4 @@ +"""Tests for newer tcod API.""" import copy import pickle from typing import Any, NoReturn @@ -8,15 +9,15 @@ import tcod +# ruff: noqa: D103 -def raise_Exception(*args: Any) -> NoReturn: - raise RuntimeError("testing exception") + +def raise_Exception(*args: object) -> NoReturn: + raise RuntimeError("testing exception") # noqa: TRY003, EM101 def test_line_error() -> None: - """ - test exception propagation - """ + """Test exception propagation.""" with pytest.raises(RuntimeError): tcod.line(0, 0, 10, 10, py_callback=raise_Exception) @@ -24,9 +25,7 @@ def test_line_error() -> None: @pytest.mark.filterwarnings("ignore:Iterate over nodes using") @pytest.mark.filterwarnings("ignore:Use pre_order method instead of walk.") def test_tcod_bsp() -> None: - """ - test tcod additions to BSP - """ + """Test tcod additions to BSP.""" bsp = tcod.bsp.BSP(0, 0, 32, 32) assert bsp.level == 0 @@ -45,7 +44,7 @@ def test_tcod_bsp() -> None: # test that operations on deep BSP nodes preserve depth sub_bsp = bsp.children[0] sub_bsp.split_recursive(3, 2, 2, 1, 1) - assert sub_bsp.children[0].level == 2 + assert sub_bsp.children[0].level == 2 # noqa: PLR2004 # cover find_node method assert bsp.find_node(0, 0) @@ -112,7 +111,7 @@ def test_color_class() -> None: assert tcod.white * 1 == tcod.white assert tcod.white * tcod.black == tcod.black assert tcod.white - tcod.white == tcod.black - assert tcod.black + (2, 2, 2) - (1, 1, 1) == (1, 1, 1) + assert tcod.black + (2, 2, 2) - (1, 1, 1) == (1, 1, 1) # noqa: RUF005 color = tcod.Color() color.r = 1 @@ -129,17 +128,17 @@ def test_path_numpy(dtype: DTypeLike) -> None: astar = tcod.path.AStar(map_np, 0) astar = pickle.loads(pickle.dumps(astar)) # test pickle astar = tcod.path.AStar(astar.cost, 0) # use existing cost attribute - assert len(astar.get_path(0, 0, 5, 5)) == 10 + assert len(astar.get_path(0, 0, 5, 5)) == 10 # noqa: PLR2004 dijkstra = tcod.path.Dijkstra(map_np, 0) dijkstra.set_goal(0, 0) - assert len(dijkstra.get_path(5, 5)) == 10 + assert len(dijkstra.get_path(5, 5)) == 10 # noqa: PLR2004 repr(dijkstra) # cover __repr__ methods # cover errors - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=r"Array must have a 2d shape, shape is \(3, 3, 3\)"): tcod.path.AStar(np.ones((3, 3, 3), dtype=dtype)) - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=r"dtype must be one of dict_keys"): tcod.path.AStar(np.ones((2, 2), dtype=np.float64)) @@ -158,7 +157,7 @@ def test_key_repr() -> None: Key = tcod.Key key = Key(vk=1, c=2, shift=True) assert key.vk == 1 - assert key.c == 2 + assert key.c == 2 # noqa: PLR2004 assert key.shift key_copy = eval(repr(key)) assert key.vk == key_copy.vk @@ -193,8 +192,8 @@ def test_context() -> None: with tcod.context.new_terminal(columns=WIDTH, rows=HEIGHT, renderer=tcod.RENDERER_SDL2) as context: console = tcod.Console(*context.recommended_console_size()) context.present(console) - context.sdl_window_p - context.renderer_type + assert context.sdl_window_p is not None + assert context.renderer_type >= 0 context.change_tileset(tcod.tileset.Tileset(16, 16)) context.pixel_to_tile(0, 0) context.pixel_to_subtile(0, 0) diff --git a/tests/test_testing.py b/tests/test_testing.py deleted file mode 100644 index 92604ae2..00000000 --- a/tests/test_testing.py +++ /dev/null @@ -1,10 +0,0 @@ -import os - -curdir = os.path.dirname(__file__) - -FONT_FILE = os.path.join(curdir, "data/fonts/consolas10x10_gs_tc.png") - -# def test_console(): -# libtcod.console_set_custom_font(FONT_FILE, libtcod.FONT_LAYOUT_TCOD) -# libtcod.console_init_root(40, 30, 'test', False, libtcod.RENDERER_SDL) -# libtcod.console_flush() diff --git a/tests/test_tileset.py b/tests/test_tileset.py index 36a3de58..c0c24c75 100644 --- a/tests/test_tileset.py +++ b/tests/test_tileset.py @@ -1,5 +1,8 @@ +"""Test for tcod.tileset module.""" import tcod +# ruff: noqa: D103 + def test_proc_block_elements() -> None: tileset = tcod.tileset.Tileset(8, 8) From 9cb25f75dc95046bffeb079c535a535e7266839d Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 27 May 2023 06:22:31 -0700 Subject: [PATCH 173/194] Skip DLL copy if it already exists at the destination. Should fix minor crashes when creating development installs. --- build_sdl.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/build_sdl.py b/build_sdl.py index 2b7c19e7..ef3dd4be 100755 --- a/build_sdl.py +++ b/build_sdl.py @@ -291,7 +291,10 @@ def get_cdef() -> str: library_dirs.append(str(SDL2_LIB_DIR)) SDL2_LIB_DEST = Path("tcod", ARCH_MAPPING[BIT_SIZE]) SDL2_LIB_DEST.mkdir(exist_ok=True) - shutil.copy(SDL2_LIB_DIR / "SDL2.dll", SDL2_LIB_DEST) + SDL2_LIB_DEST_FILE = SDL2_LIB_DEST / "SDL2.dll" + SDL2_LIB_FILE = SDL2_LIB_DIR / "SDL2.dll" + if not SDL2_LIB_DEST_FILE.exists() or SDL2_LIB_FILE.read_bytes() != SDL2_LIB_DEST_FILE.read_bytes(): + shutil.copy(SDL2_LIB_FILE, SDL2_LIB_DEST_FILE) # Link to the SDL2 framework on MacOS. # Delocate will bundle the binaries in a later step. From c6967a8cffd198c6f9e3e7c35ec11290f527bf02 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 27 May 2023 11:44:41 -0700 Subject: [PATCH 174/194] More libtcodpy deprecation. Update samples to work with namespaces a little better. Add missing SDL mouse feature. --- CHANGELOG.md | 7 +++ examples/samples_tcod.py | 108 ++++++++++++++++--------------- tcod/image.py | 5 ++ tcod/libtcodpy.py | 133 ++++++++++++++++++++++----------------- tcod/sdl/mouse.py | 21 ++++++- 5 files changed, 164 insertions(+), 110 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e280b173..5831f695 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,17 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version ## [Unreleased] ### Added - Added PathLike support to more libtcodpy functions. +- New `tcod.sdl.mouse.show` function for querying or setting mouse visibility. + +### Deprecated +- Deprecated the libtcodpy functions for images and noise generators. ### Removed - `tcod.console_set_custom_font` can no longer take bytes. +### Fixed +- Fix `tcod.sdl.mouse.warp_in_window` function. + ## [15.0.3] - 2023-05-25 ### Deprecated - Deprecated all libtcod color constants. Replace these with your own manually defined colors. diff --git a/examples/samples_tcod.py b/examples/samples_tcod.py index 8b1d375d..35e1832d 100755 --- a/examples/samples_tcod.py +++ b/examples/samples_tcod.py @@ -18,7 +18,12 @@ from numpy.typing import NDArray import tcod +import tcod.constants +import tcod.event +import tcod.libtcodpy +import tcod.noise import tcod.render +import tcod.sdl.mouse import tcod.sdl.render # ruff: noqa: S311 @@ -48,7 +53,7 @@ tileset: tcod.tileset.Tileset console_render: tcod.render.SDLConsoleRender # Optional SDL renderer. sample_minimap: tcod.sdl.render.Texture # Optional minimap texture. -root_console = tcod.Console(80, 50, order="F") +root_console = tcod.console.Console(80, 50, order="F") sample_console = tcod.console.Console(SAMPLE_SCREEN_WIDTH, SAMPLE_SCREEN_HEIGHT, order="F") cur_sample = 0 # Current selected sample. frame_times = [time.perf_counter()] @@ -191,7 +196,7 @@ def __init__(self) -> None: "You can render to an offscreen console and blit in on another " "one, simulating alpha transparency.", fg=WHITE, bg=None, - alignment=tcod.CENTER, + alignment=tcod.constants.CENTER, ) def on_enter(self) -> None: @@ -245,8 +250,8 @@ class LineDrawingSample(Sample): def __init__(self) -> None: self.name = "Line drawing" - self.mk_flag = tcod.BKGND_SET - self.bk_flag = tcod.BKGND_SET + self.mk_flag = tcod.constants.BKGND_SET + self.bk_flag = tcod.constants.BKGND_SET self.bk = tcod.console.Console(sample_console.width, sample_console.height, order="F") # initialize the colored background @@ -291,7 +296,7 @@ def on_draw(self) -> None: yd = int(sample_console.height // 2 - sin_angle * sample_console.width // 2) # draw the line # in python the easiest way is to use the line iterator - for x, y in tcod.line_iter(xo, yo, xd, yd): + for x, y in tcod.los.bresenham((xo, yo), (xd, yd)).tolist(): if 0 <= x < sample_console.width and 0 <= y < sample_console.height: tcod.console_set_char_background(sample_console, x, y, LIGHT_BLUE, self.bk_flag) sample_console.print( @@ -359,10 +364,10 @@ def __init__(self) -> None: self.dy = 0.0 self.octaves = 4.0 self.zoom = 3.0 - self.hurst = tcod.NOISE_DEFAULT_HURST - self.lacunarity = tcod.NOISE_DEFAULT_LACUNARITY + self.hurst = tcod.libtcodpy.NOISE_DEFAULT_HURST + self.lacunarity = tcod.libtcodpy.NOISE_DEFAULT_LACUNARITY self.noise = self.get_noise() - self.img = tcod.image_new(SAMPLE_SCREEN_WIDTH * 2, SAMPLE_SCREEN_HEIGHT * 2) + self.img = tcod.image.Image(SAMPLE_SCREEN_WIDTH * 2, SAMPLE_SCREEN_HEIGHT * 2) @property def algorithm(self) -> int: @@ -537,7 +542,7 @@ def __init__(self) -> None: self.player_y = 10 self.torch = False self.light_walls = True - self.algo_num = tcod.FOV_SYMMETRIC_SHADOWCAST + self.algo_num = tcod.constants.FOV_SYMMETRIC_SHADOWCAST self.noise = tcod.noise.Noise(1) # 1D noise for the torch flickering. map_shape = (SAMPLE_SCREEN_WIDTH, SAMPLE_SCREEN_HEIGHT) @@ -668,17 +673,19 @@ def __init__(self) -> None: self.busy = 0.0 self.oldchar = " " - self.map = tcod.map_new(SAMPLE_SCREEN_WIDTH, SAMPLE_SCREEN_HEIGHT) + self.map = tcod.map.Map(SAMPLE_SCREEN_WIDTH, SAMPLE_SCREEN_HEIGHT) for y in range(SAMPLE_SCREEN_HEIGHT): for x in range(SAMPLE_SCREEN_WIDTH): if SAMPLE_MAP[x, y] == " ": # ground - tcod.map_set_properties(self.map, x, y, True, True) + self.map.walkable[y, x] = True + self.map.transparent[y, x] = True elif SAMPLE_MAP[x, y] == "=": # window - tcod.map_set_properties(self.map, x, y, True, False) - self.path = tcod.path_new_using_map(self.map) - self.dijkstra = tcod.dijkstra_new(self.map) + self.map.walkable[y, x] = False + self.map.transparent[y, x] = True + self.path = tcod.path.AStar(self.map) + self.dijkstra = tcod.path.Dijkstra(self.map) def on_enter(self) -> None: # we draw the foreground only the first time. @@ -901,43 +908,41 @@ def traverse_node(bsp_map: NDArray[np.bool_], node: tcod.bsp.BSP) -> None: left, right = node.children node.x = min(left.x, right.x) node.y = min(left.y, right.y) - node.w = max(left.x + left.w, right.x + right.w) - node.x - node.h = max(left.y + left.h, right.y + right.h) - node.y + node.width = max(left.x + left.width, right.x + right.width) - node.x + node.height = max(left.y + left.height, right.y + right.height) - node.y # create a corridor between the two lower nodes if node.horizontal: # vertical corridor - if left.x + left.w - 1 < right.x or right.x + right.w - 1 < left.x: + if left.x + left.width - 1 < right.x or right.x + right.width - 1 < left.x: # no overlapping zone. we need a Z shaped corridor - x1 = random.randint(left.x, left.x + left.w - 1) - x2 = random.randint(right.x, right.x + right.w - 1) - y = random.randint(left.y + left.h, right.y) + x1 = random.randint(left.x, left.x + left.width - 1) + x2 = random.randint(right.x, right.x + right.width - 1) + y = random.randint(left.y + left.height, right.y) vline_up(bsp_map, x1, y - 1) hline(bsp_map, x1, y, x2) vline_down(bsp_map, x2, y + 1) else: # straight vertical corridor min_x = max(left.x, right.x) - max_x = min(left.x + left.w - 1, right.x + right.w - 1) + max_x = min(left.x + left.width - 1, right.x + right.width - 1) x = random.randint(min_x, max_x) vline_down(bsp_map, x, right.y) vline_up(bsp_map, x, right.y - 1) + elif left.y + left.height - 1 < right.y or right.y + right.height - 1 < left.y: # horizontal corridor + # no overlapping zone. we need a Z shaped corridor + y1 = random.randint(left.y, left.y + left.height - 1) + y2 = random.randint(right.y, right.y + right.height - 1) + x = random.randint(left.x + left.width, right.x) + hline_left(bsp_map, x - 1, y1) + vline(bsp_map, x, y1, y2) + hline_right(bsp_map, x + 1, y2) else: - # horizontal corridor - if left.y + left.h - 1 < right.y or right.y + right.h - 1 < left.y: - # no overlapping zone. we need a Z shaped corridor - y1 = random.randint(left.y, left.y + left.h - 1) - y2 = random.randint(right.y, right.y + right.h - 1) - x = random.randint(left.x + left.w, right.x) - hline_left(bsp_map, x - 1, y1) - vline(bsp_map, x, y1, y2) - hline_right(bsp_map, x + 1, y2) - else: - # straight horizontal corridor - min_y = max(left.y, right.y) - max_y = min(left.y + left.h - 1, right.y + right.h - 1) - y = random.randint(min_y, max_y) - hline_left(bsp_map, right.x - 1, y) - hline_right(bsp_map, right.x, y) + # straight horizontal corridor + min_y = max(left.y, right.y) + max_y = min(left.y + left.height - 1, right.y + right.height - 1) + y = random.randint(min_y, max_y) + hline_left(bsp_map, right.x - 1, y) + hline_right(bsp_map, right.x, y) class BSPSample(Sample): @@ -1027,9 +1032,9 @@ class ImageSample(Sample): def __init__(self) -> None: self.name = "Image toolkit" - self.img = tcod.image_load(DATA_DIR / "img/skull.png") + self.img = tcod.image.Image.from_file(DATA_DIR / "img/skull.png") self.img.set_key_color(BLACK) - self.circle = tcod.image_load(DATA_DIR / "img/circle.png") + self.circle = tcod.image.Image.from_file(DATA_DIR / "img/circle.png") def on_draw(self) -> None: sample_console.clear() @@ -1066,8 +1071,10 @@ def __init__(self) -> None: self.log: list[str] = [] def on_enter(self) -> None: - tcod.mouse_move(320, 200) - tcod.mouse_show_cursor(True) + sdl_window = context.sdl_window + if sdl_window: + tcod.sdl.mouse.warp_in_window(sdl_window, 320, 200) + tcod.sdl.mouse.show(True) def ev_mousemotion(self, event: tcod.event.MouseMotion) -> None: self.motion = event @@ -1123,9 +1130,9 @@ def on_draw(self) -> None: def ev_keydown(self, event: tcod.event.KeyDown) -> None: if event.sym == tcod.event.KeySym.N1: - tcod.mouse_show_cursor(False) + tcod.sdl.mouse.show(False) elif event.sym == tcod.event.KeySym.N2: - tcod.mouse_show_cursor(True) + tcod.sdl.mouse.show(True) else: super().ev_keydown(event) @@ -1215,7 +1222,7 @@ def ev_keydown(self, event: tcod.event.KeyDown) -> None: xc = xc - HALF_W yc = yc - HALF_H -noise2d = tcod.noise_new(2, 0.5, 2.0) +noise2d = tcod.noise.Noise(2, hurst=0.5, lacunarity=2.0) if numpy_available: # the texture starts empty texture = np.zeros((RES_U, RES_V)) @@ -1359,11 +1366,11 @@ def on_draw(self) -> None: ############################################# RENDERER_KEYS = { - tcod.event.KeySym.F1: tcod.RENDERER_GLSL, - tcod.event.KeySym.F2: tcod.RENDERER_OPENGL, - tcod.event.KeySym.F3: tcod.RENDERER_SDL, - tcod.event.KeySym.F4: tcod.RENDERER_SDL2, - tcod.event.KeySym.F5: tcod.RENDERER_OPENGL2, + tcod.event.KeySym.F1: tcod.constants.RENDERER_GLSL, + tcod.event.KeySym.F2: tcod.constants.RENDERER_OPENGL, + tcod.event.KeySym.F3: tcod.constants.RENDERER_SDL, + tcod.event.KeySym.F4: tcod.constants.RENDERER_SDL2, + tcod.event.KeySym.F5: tcod.constants.RENDERER_OPENGL2, } RENDERER_NAMES = ( @@ -1406,7 +1413,6 @@ def init_context(renderer: int) -> None: columns=root_console.width, rows=root_console.height, title=f"python-tcod samples (python-tcod {tcod.__version__}, libtcod {libtcod_version})", - renderer=renderer, vsync=False, # VSync turned off since this is for benchmarking. tileset=tileset, ) @@ -1430,7 +1436,7 @@ def init_context(renderer: int) -> None: def main() -> None: global context, tileset tileset = tcod.tileset.load_tilesheet(FONT, 32, 8, tcod.tileset.CHARMAP_TCOD) - init_context(tcod.RENDERER_SDL2) + init_context(tcod.constants.RENDERER_SDL2) try: SAMPLES[cur_sample].on_enter() diff --git a/tcod/image.py b/tcod/image.py index ced114d7..d1a98d88 100644 --- a/tcod/image.py +++ b/tcod/image.py @@ -65,6 +65,11 @@ def from_array(cls, array: ArrayLike) -> Image: image_array[...] = array return image + @classmethod + def from_file(cls, path: str | PathLike[str]) -> Image: + path = Path(path).resolve(strict=True) + return cls._from_cdata(ffi.gc(lib.TCOD_image_load(bytes(path)), lib.TCOD_image_delete)) + def clear(self, color: tuple[int, int, int]) -> None: """Fill this entire Image with color. diff --git a/tcod/libtcodpy.py b/tcod/libtcodpy.py index cb4d2c8a..d34de56c 100644 --- a/tcod/libtcodpy.py +++ b/tcod/libtcodpy.py @@ -2979,59 +2979,60 @@ def heightmap_delete(hm: Any) -> None: """ -@pending_deprecate() +@deprecate("Use `tcod.image.Image(width, height)` instead.", FutureWarning) def image_new(width: int, height: int) -> tcod.image.Image: return tcod.image.Image(width, height) -@pending_deprecate() +@deprecate("Use the `image.clear()` method instead.", FutureWarning) def image_clear(image: tcod.image.Image, col: tuple[int, int, int]) -> None: image.clear(col) -@pending_deprecate() +@deprecate("Use the `image.invert()` method instead.", FutureWarning) def image_invert(image: tcod.image.Image) -> None: image.invert() -@pending_deprecate() +@deprecate("Use the `image.hflip()` method instead.", FutureWarning) def image_hflip(image: tcod.image.Image) -> None: image.hflip() -@pending_deprecate() +@deprecate("Use the `image.rotate90(n)` method instead.", FutureWarning) def image_rotate90(image: tcod.image.Image, num: int = 1) -> None: image.rotate90(num) -@pending_deprecate() +@deprecate("Use the `image.vflip()` method instead.", FutureWarning) def image_vflip(image: tcod.image.Image) -> None: image.vflip() -@pending_deprecate() +@deprecate("Use the `image.scale(new_width, new_height)` method instead.", FutureWarning) def image_scale(image: tcod.image.Image, neww: int, newh: int) -> None: image.scale(neww, newh) -@pending_deprecate() +@deprecate("Use the `image.image_set_key_color(rgb)` method instead.", FutureWarning) def image_set_key_color(image: tcod.image.Image, col: tuple[int, int, int]) -> None: image.set_key_color(col) -@pending_deprecate() +@deprecate("Use `np.asarray(image)[y, x, 3]` instead.", FutureWarning) def image_get_alpha(image: tcod.image.Image, x: int, y: int) -> int: return image.get_alpha(x, y) -@pending_deprecate() +@deprecate("Use the Numpy array interface to check alpha or color keys.", FutureWarning) def image_is_pixel_transparent(image: tcod.image.Image, x: int, y: int) -> bool: return bool(lib.TCOD_image_is_pixel_transparent(image.image_c, x, y)) -@pending_deprecate( - "This function may be removed in the future." - " It's recommended to load images with a more complete image library such as python-Pillow or python-imageio." +@deprecate( + "Call the classmethod `tcod.image.Image.from_file` instead to load images." + "\nIt's recommended to load images with a more complete image library such as python-Pillow or python-imageio.", + FutureWarning, ) def image_load(filename: str | PathLike[str]) -> tcod.image.Image: """Load an image file into an Image instance and return it. @@ -3041,12 +3042,14 @@ def image_load(filename: str | PathLike[str]) -> tcod.image.Image: .. versionchanged:: Unreleased Added PathLike support. + + .. deprecated:: Unreleased + Use :any:`tcod.image.Image.from_file` instead. """ - filename = Path(filename).resolve(strict=True) - return tcod.image.Image._from_cdata(ffi.gc(lib.TCOD_image_load(bytes(filename)), lib.TCOD_image_delete)) + return tcod.image.Image.from_file(filename) -@pending_deprecate() +@deprecate("Use `Tileset.render` instead of this function.", FutureWarning) def image_from_console(console: tcod.console.Console) -> tcod.image.Image: """Return an Image with a Consoles pixel data. @@ -3054,6 +3057,9 @@ def image_from_console(console: tcod.console.Console) -> tcod.image.Image: Args: console (Console): Any Console instance. + + .. deprecated:: Unreleased + :any:`Tileset.render` is a better alternative. """ return tcod.image.Image._from_cdata( ffi.gc( @@ -3063,32 +3069,37 @@ def image_from_console(console: tcod.console.Console) -> tcod.image.Image: ) -@pending_deprecate() +@deprecate("Use `Tileset.render` instead of this function.", FutureWarning) def image_refresh_console(image: tcod.image.Image, console: tcod.console.Console) -> None: + """Update an image made with :any:`image_from_console`. + + .. deprecated:: Unreleased + This function is unnecessary, use :any:`Tileset.render` instead. + """ image.refresh_console(console) -@pending_deprecate() +@deprecate("Access an images size with `image.width` or `image.height`.", FutureWarning) def image_get_size(image: tcod.image.Image) -> tuple[int, int]: return image.width, image.height -@pending_deprecate() +@deprecate("Use `np.asarray(image)[y, x, :3]` instead.", FutureWarning) def image_get_pixel(image: tcod.image.Image, x: int, y: int) -> tuple[int, int, int]: return image.get_pixel(x, y) -@pending_deprecate() +@deprecate("Use the `image.get_mipmap_pixel(...)` method instead.", FutureWarning) def image_get_mipmap_pixel(image: tcod.image.Image, x0: float, y0: float, x1: float, y1: float) -> tuple[int, int, int]: return image.get_mipmap_pixel(x0, y0, x1, y1) -@pending_deprecate() +@deprecate("Use `np.asarray(image)[y, x, :3] = rgb` instead.", FutureWarning) def image_put_pixel(image: tcod.image.Image, x: int, y: int, col: tuple[int, int, int]) -> None: image.put_pixel(x, y, col) -@pending_deprecate() +@deprecate("Use the `image.blit(...)` method instead.", FutureWarning) def image_blit( image: tcod.image.Image, console: tcod.console.Console, @@ -3102,7 +3113,7 @@ def image_blit( image.blit(console, x, y, bkgnd_flag, scalex, scaley, angle) -@pending_deprecate() +@deprecate("Use the `image.blit_rect(...)` method instead.", FutureWarning) def image_blit_rect( image: tcod.image.Image, console: tcod.console.Console, @@ -3115,7 +3126,7 @@ def image_blit_rect( image.blit_rect(console, x, y, w, h, bkgnd_flag) -@pending_deprecate() +@deprecate("Use `Console.draw_semigraphics(image, ...)` instead.", FutureWarning) def image_blit_2x( image: tcod.image.Image, console: tcod.console.Console, @@ -3129,12 +3140,12 @@ def image_blit_2x( image.blit_2x(console, dx, dy, sx, sy, w, h) -@pending_deprecate() +@deprecate("Use the `image.save_as` method instead.", FutureWarning) def image_save(image: tcod.image.Image, filename: str | PathLike[str]) -> None: image.save_as(filename) -@deprecate("libtcod objects are deleted automatically.") +@deprecate("libtcod objects are deleted automatically.", FutureWarning) def image_delete(image: tcod.image.Image) -> None: """Does nothing. libtcod objects are managed by Python's garbage collector. @@ -3142,7 +3153,7 @@ def image_delete(image: tcod.image.Image) -> None: """ -@deprecate("Use tcod.line_iter instead.") +@deprecate("Use tcod.los.bresenham instead.", FutureWarning) def line_init(xo: int, yo: int, xd: int, yd: int) -> None: """Initialize a line whose points will be returned by `line_step`. @@ -3157,12 +3168,12 @@ def line_init(xo: int, yo: int, xd: int, yd: int) -> None: yd (int): Y destination point. .. deprecated:: 2.0 - Use `line_iter` instead. + This function was replaced by :any:`tcod.los.bresenham`. """ lib.TCOD_line_init(xo, yo, xd, yd) -@deprecate("Use tcod.line_iter instead.") +@deprecate("Use tcod.los.bresenham instead.", FutureWarning) def line_step() -> tuple[int, int] | tuple[None, None]: """After calling line_init returns (x, y) points of the line. @@ -3174,7 +3185,7 @@ def line_step() -> tuple[int, int] | tuple[None, None]: or (None, None) if there are no more points. .. deprecated:: 2.0 - Use `line_iter` instead. + This function was replaced by :any:`tcod.los.bresenham`. """ x = ffi.new("int *") y = ffi.new("int *") @@ -3184,7 +3195,7 @@ def line_step() -> tuple[int, int] | tuple[None, None]: return None, None -@deprecate("Use tcod.line_iter instead.") +@deprecate("Use tcod.los.bresenham instead.", FutureWarning) def line(xo: int, yo: int, xd: int, yd: int, py_callback: Callable[[int, int], bool]) -> bool: """Iterate over a line using a callback function. @@ -3206,7 +3217,7 @@ def line(xo: int, yo: int, xd: int, yd: int, py_callback: Callable[[int, int], b returning False or None, otherwise True. .. deprecated:: 2.0 - Use `line_iter` instead. + This function was replaced by :any:`tcod.los.bresenham`. """ for x, y in line_iter(xo, yo, xd, yd): if not py_callback(x, y): @@ -3216,7 +3227,7 @@ def line(xo: int, yo: int, xd: int, yd: int, py_callback: Callable[[int, int], b return False -@deprecate("This function has been replaced by tcod.los.bresenham.") +@deprecate("This function has been replaced by tcod.los.bresenham.", FutureWarning) def line_iter(xo: int, yo: int, xd: int, yd: int) -> Iterator[tuple[int, int]]: """Returns an Iterable over a Bresenham line. @@ -3243,7 +3254,7 @@ def line_iter(xo: int, yo: int, xd: int, yd: int) -> Iterator[tuple[int, int]]: yield (x[0], y[0]) -@deprecate("This function has been replaced by tcod.los.bresenham.") +@deprecate("This function has been replaced by tcod.los.bresenham.", FutureWarning) def line_where(x1: int, y1: int, x2: int, y2: int, inclusive: bool = True) -> tuple[NDArray[np.intc], NDArray[np.intc]]: """Return a NumPy index array following a Bresenham line. @@ -3274,7 +3285,7 @@ def line_where(x1: int, y1: int, x2: int, y2: int, inclusive: bool = True) -> tu return i, j -@deprecate("Call tcod.map.Map(width, height) instead.") +@deprecate("Call tcod.map.Map(width, height) instead.", FutureWarning) def map_new(w: int, h: int) -> tcod.map.Map: """Return a :any:`tcod.map.Map` with a width and height. @@ -3285,7 +3296,7 @@ def map_new(w: int, h: int) -> tcod.map.Map: return tcod.map.Map(w, h) -@deprecate("Use Python's standard copy module instead.") +@deprecate("Use Python's standard copy module instead.", FutureWarning) def map_copy(source: tcod.map.Map, dest: tcod.map.Map) -> None: """Copy map data from `source` to `dest`. @@ -3298,7 +3309,7 @@ def map_copy(source: tcod.map.Map, dest: tcod.map.Map) -> None: dest._Map__buffer[:] = source._Map__buffer[:] # type: ignore -@deprecate("Set properties using the m.transparent and m.walkable arrays.") +@deprecate("Set properties using the m.transparent and m.walkable arrays.", FutureWarning) def map_set_properties(m: tcod.map.Map, x: int, y: int, isTrans: bool, isWalk: bool) -> None: """Set the properties of a single cell. @@ -3311,7 +3322,7 @@ def map_set_properties(m: tcod.map.Map, x: int, y: int, isTrans: bool, isWalk: b lib.TCOD_map_set_properties(m.map_c, x, y, isTrans, isWalk) -@deprecate("Clear maps using NumPy broadcast rules instead.") +@deprecate("Clear maps using NumPy broadcast rules instead.", FutureWarning) def map_clear(m: tcod.map.Map, transparent: bool = False, walkable: bool = False) -> None: """Change all map cells to a specific value. @@ -3323,7 +3334,7 @@ def map_clear(m: tcod.map.Map, transparent: bool = False, walkable: bool = False m.walkable[:] = walkable -@deprecate("Call the map.compute_fov method instead.") +@deprecate("Call the map.compute_fov method instead.", FutureWarning) def map_compute_fov( m: tcod.map.Map, x: int, @@ -3340,7 +3351,7 @@ def map_compute_fov( m.compute_fov(x, y, radius, light_walls, algo) -@deprecate("Use map.fov to check for this property.") +@deprecate("Use map.fov to check for this property.", FutureWarning) def map_is_in_fov(m: tcod.map.Map, x: int, y: int) -> bool: """Return True if the cell at x,y is lit by the last field-of-view algorithm. @@ -3352,7 +3363,7 @@ def map_is_in_fov(m: tcod.map.Map, x: int, y: int) -> bool: return bool(lib.TCOD_map_is_in_fov(m.map_c, x, y)) -@deprecate("Use map.transparent to check for this property.") +@deprecate("Use map.transparent to check for this property.", FutureWarning) def map_is_transparent(m: tcod.map.Map, x: int, y: int) -> bool: """Return True is a map cell is transparent. @@ -3364,7 +3375,7 @@ def map_is_transparent(m: tcod.map.Map, x: int, y: int) -> bool: return bool(lib.TCOD_map_is_transparent(m.map_c, x, y)) -@deprecate("Use map.walkable to check for this property.") +@deprecate("Use map.walkable to check for this property.", FutureWarning) def map_is_walkable(m: tcod.map.Map, x: int, y: int) -> bool: """Return True is a map cell is walkable. @@ -3376,7 +3387,7 @@ def map_is_walkable(m: tcod.map.Map, x: int, y: int) -> bool: return bool(lib.TCOD_map_is_walkable(m.map_c, x, y)) -@deprecate("libtcod objects are deleted automatically.") +@deprecate("libtcod objects are deleted automatically.", FutureWarning) def map_delete(m: tcod.map.Map) -> None: """Does nothing. libtcod objects are managed by Python's garbage collector. @@ -3384,7 +3395,7 @@ def map_delete(m: tcod.map.Map) -> None: """ -@deprecate("Check the map.width attribute instead.") +@deprecate("Check the map.width attribute instead.", FutureWarning) def map_get_width(map: tcod.map.Map) -> int: """Return the width of a map. @@ -3394,7 +3405,7 @@ def map_get_width(map: tcod.map.Map) -> int: return map.width -@deprecate("Check the map.height attribute instead.") +@deprecate("Check the map.height attribute instead.", FutureWarning) def map_get_height(map: tcod.map.Map) -> int: """Return the height of a map. @@ -3404,24 +3415,32 @@ def map_get_height(map: tcod.map.Map) -> int: return map.height -@pending_deprecate() +@deprecate("Use `tcod.sdl.mouse.show(visible)` instead.", FutureWarning) def mouse_show_cursor(visible: bool) -> None: - """Change the visibility of the mouse cursor.""" + """Change the visibility of the mouse cursor. + + .. deprecated:: Unreleased + Use :any:`tcod.sdl.mouse.show` instead. + """ lib.TCOD_mouse_show_cursor(visible) -@pending_deprecate() +@deprecate("Use `is_visible = tcod.sdl.mouse.show()` instead.", FutureWarning) def mouse_is_cursor_visible() -> bool: - """Return True if the mouse cursor is visible.""" + """Return True if the mouse cursor is visible. + + .. deprecated:: Unreleased + Use :any:`tcod.sdl.mouse.show` instead. + """ return bool(lib.TCOD_mouse_is_cursor_visible()) -@pending_deprecate() +@deprecate("Use `tcod.sdl.mouse.warp_in_window` instead.", FutureWarning) def mouse_move(x: int, y: int) -> None: lib.TCOD_mouse_move(x, y) -@deprecate("Use tcod.event.get_mouse_state() instead.") +@deprecate("Use tcod.event.get_mouse_state() instead.", FutureWarning) def mouse_get_status() -> Mouse: return Mouse(lib.TCOD_mouse_get_status()) @@ -3458,7 +3477,7 @@ def namegen_destroy() -> None: lib.TCOD_namegen_destroy() -@pending_deprecate() +@deprecate("Use `tcod.noise.Noise(dimensions, hurst=, lacunarity=)` instead.", FutureWarning) def noise_new( dim: int, h: float = NOISE_DEFAULT_HURST, @@ -3479,7 +3498,7 @@ def noise_new( return tcod.noise.Noise(dim, hurst=h, lacunarity=l, seed=random) -@pending_deprecate() +@deprecate("Use `noise.algorithm = x` instead.", FutureWarning) def noise_set_type(n: tcod.noise.Noise, typ: int) -> None: """Set a Noise objects default noise algorithm. @@ -3489,7 +3508,7 @@ def noise_set_type(n: tcod.noise.Noise, typ: int) -> None: n.algorithm = typ -@pending_deprecate() +@deprecate("Use `value = noise[x]` instead.", FutureWarning) def noise_get(n: tcod.noise.Noise, f: Sequence[float], typ: int = NOISE_DEFAULT) -> float: """Return the noise value sampled from the ``f`` coordinate. @@ -3509,7 +3528,7 @@ def noise_get(n: tcod.noise.Noise, f: Sequence[float], typ: int = NOISE_DEFAULT) return float(lib.TCOD_noise_get_ex(n.noise_c, ffi.new("float[4]", f), typ)) -@pending_deprecate() +@deprecate("Configure a Noise instance for FBM and then sample it like normal.", FutureWarning) def noise_get_fbm( n: tcod.noise.Noise, f: Sequence[float], @@ -3530,7 +3549,7 @@ def noise_get_fbm( return float(lib.TCOD_noise_get_fbm_ex(n.noise_c, ffi.new("float[4]", f), oc, typ)) -@pending_deprecate() +@deprecate("Configure a Noise instance for FBM and then sample it like normal.", FutureWarning) def noise_get_turbulence( n: tcod.noise.Noise, f: Sequence[float], @@ -3551,7 +3570,7 @@ def noise_get_turbulence( return float(lib.TCOD_noise_get_turbulence_ex(n.noise_c, ffi.new("float[4]", f), oc, typ)) -@deprecate("libtcod objects are deleted automatically.") +@deprecate("libtcod objects are deleted automatically.", FutureWarning) def noise_delete(n: tcod.noise.Noise) -> None: # type (Any) -> None """Does nothing. libtcod objects are managed by Python's garbage collector. diff --git a/tcod/sdl/mouse.py b/tcod/sdl/mouse.py index aaffdeec..cfb0be3f 100644 --- a/tcod/sdl/mouse.py +++ b/tcod/sdl/mouse.py @@ -99,7 +99,9 @@ def new_cursor(data: NDArray[np.bool_], mask: NDArray[np.bool_], hot_xy: tuple[i def new_color_cursor(pixels: ArrayLike, hot_xy: tuple[int, int]) -> Cursor: - """Args: + """Create a new color cursor. + + Args: pixels: A row-major array of RGB or RGBA pixels. hot_xy: The position of the pointer relative to the mouse sprite, starting from the upper-left at (0, 0). @@ -230,4 +232,19 @@ def warp_global(x: int, y: int) -> None: def warp_in_window(window: tcod.sdl.video.Window, x: int, y: int) -> None: """Move the mouse cursor to a position within a window.""" - _check(lib.SDL_WarpMouseInWindow(window.p, x, y)) + lib.SDL_WarpMouseInWindow(window.p, x, y) + + +def show(visible: bool | None = None) -> bool: + """Optionally show or hide the mouse cursor then return the state of the cursor. + + Args: + visible: If None then only return the current state. Otherwise set the mouse visibility. + + Returns: + True if the cursor is visible. + + .. versionadded:: Unreleased + """ + _OPTIONS = {None: lib.SDL_QUERY, False: lib.SDL_DISABLE, True: lib.SDL_ENABLE} + return _check(lib.SDL_ShowCursor(_OPTIONS[visible])) == int(lib.SDL_ENABLE) From 7d994eeb1bbdac92c301295679ce19395adb14fd Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 27 May 2023 18:44:36 -0700 Subject: [PATCH 175/194] Add missing docstring to Image.from_file class method. --- CHANGELOG.md | 1 + tcod/image.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5831f695..1a0c7312 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version ### Added - Added PathLike support to more libtcodpy functions. - New `tcod.sdl.mouse.show` function for querying or setting mouse visibility. +- New class method `tcod.image.Image.from_file` to load images with. This replaces `tcod.image_load`. ### Deprecated - Deprecated the libtcodpy functions for images and noise generators. diff --git a/tcod/image.py b/tcod/image.py index d1a98d88..927be5da 100644 --- a/tcod/image.py +++ b/tcod/image.py @@ -67,6 +67,10 @@ def from_array(cls, array: ArrayLike) -> Image: @classmethod def from_file(cls, path: str | PathLike[str]) -> Image: + """Return a new Image loaded from the given `path`. + + .. versionadded:: Unreleased + """ path = Path(path).resolve(strict=True) return cls._from_cdata(ffi.gc(lib.TCOD_image_load(bytes(path)), lib.TCOD_image_delete)) From 37ec7c0ef139e2ac134d35f305a1d98f7f45a217 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 27 May 2023 18:49:46 -0700 Subject: [PATCH 176/194] Fixed many typos in the tcod.sdl package. --- .vscode/settings.json | 26 ++++++++++++++++++++++++++ tcod/sdl/audio.py | 28 ++++++++++++++-------------- tcod/sdl/mouse.py | 2 +- tcod/sdl/sys.py | 4 ++-- tcod/sdl/video.py | 2 +- 5 files changed, 44 insertions(+), 18 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 18967232..8eb6e93e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -57,6 +57,8 @@ "bfade", "bgcolor", "bitmask", + "bitorder", + "BITSIZE", "BKGND", "Blit", "blits", @@ -123,6 +125,7 @@ "DISPLAYSWITCH", "dlopen", "Doryen", + "DPAD", "DTEEE", "DTEEN", "DTEES", @@ -149,6 +152,8 @@ "frombuffer", "fullscreen", "fwidth", + "GAMECONTROLLER", + "gamepad", "genindex", "GFORCE", "GLES", @@ -220,12 +225,18 @@ "LDFLAGS", "LEFTBRACE", "LEFTBRACKET", + "LEFTDOWN", "LEFTPAREN", + "LEFTSHOULDER", + "LEFTSTICK", + "LEFTUP", + "LEFTX", "lerp", "LGUI", "libsdl", "libtcod", "libtcodpy", + "linspace", "liskin", "LMASK", "lmeta", @@ -289,8 +300,10 @@ "onefile", "OPENGL", "OPER", + "packbits", "PAGEDOWN", "PAGEUP", + "PATCHLEVEL", "pathfinding", "pathlib", "pcpp", @@ -329,7 +342,12 @@ "RGUI", "RIGHTBRACE", "RIGHTBRACKET", + "RIGHTDOWN", "RIGHTPAREN", + "RIGHTSHOULDER", + "RIGHTSTICK", + "RIGHTUP", + "RIGHTX", "RMASK", "rmeta", "roguelike", @@ -354,18 +372,21 @@ "servernum", "setuptools", "SHADOWCAST", + "SIZEALL", "SIZENESW", "SIZENS", "SIZENWSE", "SIZEWE", "SMILIE", "snprintf", + "soundfile", "stdeb", "struct", "structs", "SUBP", "SYSREQ", "tablefmt", + "TARGETTEXTURE", "tcod", "tcoddoc", "TCODK", @@ -383,6 +404,9 @@ "toctree", "todos", "tolist", + "touchpad", + "TRIGGERLEFT", + "TRIGGERRIGHT", "tris", "truetype", "typestr", @@ -405,6 +429,8 @@ "voronoi", "VRAM", "vsync", + "VULKAN", + "WAITARROW", "WASD", "waterlevel", "windowclose", diff --git a/tcod/sdl/audio.py b/tcod/sdl/audio.py index 8234a2a6..69ca5143 100644 --- a/tcod/sdl/audio.py +++ b/tcod/sdl/audio.py @@ -1,11 +1,11 @@ """SDL2 audio playback and recording tools. -This module includes SDL's low-level audio API and a naive implentation of an SDL mixer. +This module includes SDL's low-level audio API and a naive implementation of an SDL mixer. If you have experience with audio mixing then you might be better off writing your own mixer or modifying the existing one which was written using Python/Numpy. This module is designed to integrate with the wider Python ecosystem. -It leaves the loading to sound samples to other libaries like +It leaves the loading to sound samples to other libraries like `SoundFile `_. Example:: @@ -17,9 +17,9 @@ import tcod.sdl.audio device = tcod.sdl.audio.open() # Open the default output device. - sound, samplerate = soundfile.read("example_sound.wav", dtype="float32") # Load an audio sample using SoundFile. - converted = device.convert(sound, samplerate) # Convert this sample to the format expected by the device. - device.queue_audio(converted) # Play audio syncroniously by appending it to the device buffer. + sound, sample_rate = soundfile.read("example_sound.wav", dtype="float32") # Load an audio sample using SoundFile. + converted = device.convert(sound, sample_rate) # Convert this sample to the format expected by the device. + device.queue_audio(converted) # Play audio synchronously by appending it to the device buffer. while device.queued_samples: # Wait until device is done playing. time.sleep(0.001) @@ -33,8 +33,8 @@ import tcod.sdl.audio mixer = tcod.sdl.audio.BasicMixer(tcod.sdl.audio.open()) # Setup BasicMixer with the default audio output. - sound, samplerate = soundfile.read("example_sound.wav") # Load an audio sample using SoundFile. - sound = mixer.device.convert(sound, samplerate) # Convert this sample to the format expected by the device. + sound, sample_rate = soundfile.read("example_sound.wav") # Load an audio sample using SoundFile. + sound = mixer.device.convert(sound, sample_rate) # Convert this sample to the format expected by the device. channel = mixer.play(sound) # Start asynchronous playback, audio is mixed on a separate Python thread. while channel.busy: # Wait until the sample is done playing. time.sleep(0.001) @@ -59,7 +59,7 @@ def _get_format(format: DTypeLike) -> int: - """Return a SDL_AudioFormat bitfield from a NumPy dtype.""" + """Return a SDL_AudioFormat bit-field from a NumPy dtype.""" dt: Any = np.dtype(format) assert dt.fields is None bitsize = dt.itemsize * 8 @@ -83,7 +83,7 @@ def _dtype_from_format(format: int) -> np.dtype[Any]: """Return a dtype from a SDL_AudioFormat.""" bitsize = format & lib.SDL_AUDIO_MASK_BITSIZE assert bitsize % 8 == 0 - bytesize = bitsize // 8 + byte_size = bitsize // 8 byteorder = ">" if format & lib.SDL_AUDIO_MASK_ENDIAN else "<" if format & lib.SDL_AUDIO_MASK_DATATYPE: kind = "f" @@ -91,7 +91,7 @@ def _dtype_from_format(format: int) -> np.dtype[Any]: kind = "i" else: kind = "u" - return np.dtype(f"{byteorder}{kind}{bytesize}") + return np.dtype(f"{byteorder}{kind}{byte_size}") def convert_audio( @@ -103,8 +103,8 @@ def convert_audio( Args: in_sound: The input ArrayLike sound sample. Input format and channels are derived from the array. - in_rate: The samplerate of the input array. - out_rate: The samplerate of the output array. + in_rate: The sample-rate of the input array. + out_rate: The sample-rate of the output array. out_format: The output format of the converted array. out_channels: The number of audio channels of the output array. @@ -142,7 +142,7 @@ class AudioDevice: Open new audio devices using :any:`tcod.sdl.audio.open`. - When you use this object directly the audio passed to :any:`queue_audio` is always played syncroniously. + When you use this object directly the audio passed to :any:`queue_audio` is always played synchronously. For more typical asynchronous audio you should pass an AudioDevice to :any:`BasicMixer`. """ @@ -231,7 +231,7 @@ def convert(self, sound: ArrayLike, rate: int | None = None) -> NDArray[Any]: Args: sound: An ArrayLike sound sample. - rate: The samplerate of the input array. + rate: The sample-rate of the input array. If None is given then it's assumed to be the same as the device. .. versionadded:: 13.6 diff --git a/tcod/sdl/mouse.py b/tcod/sdl/mouse.py index cfb0be3f..3498c7d8 100644 --- a/tcod/sdl/mouse.py +++ b/tcod/sdl/mouse.py @@ -148,7 +148,7 @@ def get_cursor() -> Cursor | None: def capture(enable: bool) -> None: """Enable or disable mouse capture to track the mouse outside of a window. - It is highly reccomended to read the related remarks section in the SDL docs before using this. + It is highly recommended to read the related remarks section in the SDL docs before using this. Example:: diff --git a/tcod/sdl/sys.py b/tcod/sdl/sys.py index 39bda95d..f2b65fb4 100644 --- a/tcod/sdl/sys.py +++ b/tcod/sdl/sys.py @@ -60,8 +60,8 @@ def _get_power_info() -> tuple[_PowerState, int, int]: buffer = ffi.new("int[2]") power_state = _PowerState(lib.SDL_GetPowerInfo(buffer, buffer + 1)) seconds_of_power = buffer[0] - percenage = buffer[1] - return power_state, seconds_of_power, percenage + percentage = buffer[1] + return power_state, seconds_of_power, percentage def _get_clipboard() -> str: diff --git a/tcod/sdl/video.py b/tcod/sdl/video.py index 4cf6a87b..0a4ad7a0 100644 --- a/tcod/sdl/video.py +++ b/tcod/sdl/video.py @@ -262,7 +262,7 @@ def border_size(self) -> tuple[int, int, int, int]: @property def opacity(self) -> float: - """Get or set this windows opacity. 0.0 is fully transarpent and 1.0 is fully opaque. + """Get or set this windows opacity. 0.0 is fully transparent and 1.0 is fully opaque. Will error if you try to set this and opacity isn't supported. """ From eaf4571d137d8569001422a7fc8a799eb96ddf72 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 27 May 2023 20:32:28 -0700 Subject: [PATCH 177/194] Fix audio device callback. Closes #128 Allow audio conversions of floating types other than float32. Setup AudioDevice as a context manager and add a `__repr__` method. --- CHANGELOG.md | 6 +++++ tcod/sdl/audio.py | 60 +++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a0c7312..ba10dc37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version - Added PathLike support to more libtcodpy functions. - New `tcod.sdl.mouse.show` function for querying or setting mouse visibility. - New class method `tcod.image.Image.from_file` to load images with. This replaces `tcod.image_load`. +- `tcod.sdl.audio.AudioDevice` is now a context manager. + +### Changed +- SDL audio conversion will now pass unconvertible floating types as float32 instead of raising. ### Deprecated - Deprecated the libtcodpy functions for images and noise generators. @@ -17,6 +21,8 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version ### Fixed - Fix `tcod.sdl.mouse.warp_in_window` function. +- Fix `TypeError: '_AudioCallbackUserdata' object is not callable` when using an SDL audio device callback. + [#128](https://github.com/libtcod/python-tcod/issues/128) ## [15.0.3] - 2023-05-25 ### Deprecated diff --git a/tcod/sdl/audio.py b/tcod/sdl/audio.py index 69ca5143..b995e336 100644 --- a/tcod/sdl/audio.py +++ b/tcod/sdl/audio.py @@ -47,11 +47,12 @@ import sys import threading import time +from types import TracebackType from typing import Any, Callable, Hashable, Iterator import numpy as np from numpy.typing import ArrayLike, DTypeLike, NDArray -from typing_extensions import Final, Literal +from typing_extensions import Final, Literal, Self import tcod.sdl.sys from tcod.loader import ffi, lib @@ -110,6 +111,9 @@ def convert_audio( .. versionadded:: 13.6 + .. versionchanged:: Unreleased + Now converts floating types to `np.float32` when SDL doesn't support the specific format. + .. seealso:: :any:`AudioDevice.convert` """ @@ -123,8 +127,26 @@ def convert_audio( in_channels = in_array.shape[1] in_format = _get_format(in_array.dtype) out_sdl_format = _get_format(out_format) - if _check(lib.SDL_BuildAudioCVT(cvt, in_format, in_channels, in_rate, out_sdl_format, out_channels, out_rate)) == 0: - return in_array # No conversion needed. + try: + if ( + _check(lib.SDL_BuildAudioCVT(cvt, in_format, in_channels, in_rate, out_sdl_format, out_channels, out_rate)) + == 0 + ): + return in_array # No conversion needed. + except RuntimeError as exc: + if ( # SDL now only supports float32, but later versions may add more support for more formats. + exc.args[0] == "Invalid source format" + and np.issubdtype(in_array.dtype, np.floating) + and in_array.dtype != np.float32 + ): + return convert_audio( # Try again with float32 + in_array.astype(np.float32), + in_rate, + out_rate=out_rate, + out_format=out_format, + out_channels=out_channels, + ) + raise # Upload to the SDL_AudioCVT buffer. cvt.len = in_array.itemsize * in_array.size out_buffer = cvt.buf = ffi.new("uint8_t[]", cvt.len * cvt.len_mult) @@ -144,6 +166,9 @@ class AudioDevice: When you use this object directly the audio passed to :any:`queue_audio` is always played synchronously. For more typical asynchronous audio you should pass an AudioDevice to :any:`BasicMixer`. + + .. versionchanged:: Unreleased + Can now be used as a context which will close the device on exit. """ def __init__( @@ -176,6 +201,23 @@ def __init__( self._handle: Any | None = None self._callback: Callable[[AudioDevice, NDArray[Any]], None] = self.__default_callback + def __repr__(self) -> str: + """Return a representation of this device.""" + items = [ + f"{self.__class__.__name__}(device_id={self.device_id})", + f"frequency={self.frequency}", + f"is_capture={self.is_capture}", + f"format={self.format}", + f"channels={self.channels}", + f"buffer_samples={self.buffer_samples}", + f"buffer_bytes={self.buffer_bytes}", + ] + if self.silence: + items.append(f"silence={self.silence}") + if self._handle is not None: + items.append(f"callback={self._callback}") + return f"""<{" ".join(items)}>""" + @property def callback(self) -> Callable[[AudioDevice, NDArray[Any]], None]: """If the device was opened with a callback enabled, then you may get or set the callback with this attribute.""" @@ -288,6 +330,16 @@ def close(self) -> None: lib.SDL_CloseAudioDevice(self.device_id) del self.device_id + def __enter__(self) -> Self: + """Return self and enter a managed context.""" + return self + + def __exit__( + self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None + ) -> None: + """Close the device when exiting the context.""" + self.close() + @staticmethod def __default_callback(device: AudioDevice, stream: NDArray[Any]) -> None: stream[...] = device.silence @@ -487,7 +539,7 @@ class _AudioCallbackUserdata: @ffi.def_extern() # type: ignore def _sdl_audio_callback(userdata: Any, stream: Any, length: int) -> None: """Handle audio device callbacks.""" - data: _AudioCallbackUserdata = ffi.from_handle(userdata)() + data: _AudioCallbackUserdata = ffi.from_handle(userdata) device = data.device buffer = np.frombuffer(ffi.buffer(stream, length), dtype=device.format).reshape(-1, device.channels) device._callback(device, buffer) From 98531c5fee187633fc84640c844ac1bfb0e88c0a Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 27 May 2023 20:53:44 -0700 Subject: [PATCH 178/194] Prepare 16.0.0 release. --- CHANGELOG.md | 4 +++- tcod/image.py | 4 ++-- tcod/libtcodpy.py | 36 ++++++++++++++++++------------------ tcod/sdl/audio.py | 4 ++-- tcod/sdl/mouse.py | 2 +- 5 files changed, 26 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba10dc37..5db385e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] + +## [16.0.0] - 2023-05-27 ### Added - Added PathLike support to more libtcodpy functions. - New `tcod.sdl.mouse.show` function for querying or setting mouse visibility. @@ -17,7 +19,7 @@ This project adheres to [Semantic Versioning](https://semver.org/) since version - Deprecated the libtcodpy functions for images and noise generators. ### Removed -- `tcod.console_set_custom_font` can no longer take bytes. +- `tcod.console_set_custom_font` can no longer take bytes as the file path. ### Fixed - Fix `tcod.sdl.mouse.warp_in_window` function. diff --git a/tcod/image.py b/tcod/image.py index 927be5da..700c6290 100644 --- a/tcod/image.py +++ b/tcod/image.py @@ -69,7 +69,7 @@ def from_array(cls, array: ArrayLike) -> Image: def from_file(cls, path: str | PathLike[str]) -> Image: """Return a new Image loaded from the given `path`. - .. versionadded:: Unreleased + .. versionadded:: 16.0 """ path = Path(path).resolve(strict=True) return cls._from_cdata(ffi.gc(lib.TCOD_image_load(bytes(path)), lib.TCOD_image_delete)) @@ -303,7 +303,7 @@ def save_as(self, filename: str | PathLike[str]) -> None: Args: filename (Text): File path to same this Image. - .. versionchanged:: Unreleased + .. versionchanged:: 16.0 Added PathLike support. """ lib.TCOD_image_save(self.image_c, bytes(Path(filename))) diff --git a/tcod/libtcodpy.py b/tcod/libtcodpy.py index d34de56c..6bb1c762 100644 --- a/tcod/libtcodpy.py +++ b/tcod/libtcodpy.py @@ -985,7 +985,7 @@ def console_set_custom_font( Load fonts using :any:`tcod.tileset.load_tilesheet` instead. See :ref:`getting-started` for more info. - .. versionchanged:: Unreleased + .. versionchanged:: 16.0 Added PathLike support. `fontFile` no longer takes bytes. """ fontFile = Path(fontFile).resolve(strict=True) @@ -1800,7 +1800,7 @@ def console_from_file(filename: str | PathLike[str]) -> tcod.console.Console: Other formats are not actively supported. - .. versionchanged:: Unreleased + .. versionchanged:: 16.0 Added PathLike support. """ filename = Path(filename).resolve(strict=True) @@ -1979,7 +1979,7 @@ def console_load_asc(con: tcod.console.Console, filename: str | PathLike[str]) - .. deprecated:: 12.7 This format is no longer supported. - .. versionchanged:: Unreleased + .. versionchanged:: 16.0 Added PathLike support. """ filename = Path(filename).resolve(strict=True) @@ -1993,7 +1993,7 @@ def console_save_asc(con: tcod.console.Console, filename: str | PathLike[str]) - .. deprecated:: 12.7 This format is no longer supported. - .. versionchanged:: Unreleased + .. versionchanged:: 16.0 Added PathLike support. """ return bool(lib.TCOD_console_save_asc(_console(con), bytes(Path(filename)))) @@ -2006,7 +2006,7 @@ def console_load_apf(con: tcod.console.Console, filename: str | PathLike[str]) - .. deprecated:: 12.7 This format is no longer supported. - .. versionchanged:: Unreleased + .. versionchanged:: 16.0 Added PathLike support. """ filename = Path(filename).resolve(strict=True) @@ -2020,7 +2020,7 @@ def console_save_apf(con: tcod.console.Console, filename: str | PathLike[str]) - .. deprecated:: 12.7 This format is no longer supported. - .. versionchanged:: Unreleased + .. versionchanged:: 16.0 Added PathLike support. """ return bool(lib.TCOD_console_save_apf(_console(con), bytes(Path(filename)))) @@ -2034,7 +2034,7 @@ def console_load_xp(con: tcod.console.Console, filename: str | PathLike[str]) -> Functions modifying console objects in-place are deprecated. Use :any:`tcod.console_from_xp` to load a Console from a file. - .. versionchanged:: Unreleased + .. versionchanged:: 16.0 Added PathLike support. """ filename = Path(filename).resolve(strict=True) @@ -2045,7 +2045,7 @@ def console_load_xp(con: tcod.console.Console, filename: str | PathLike[str]) -> def console_save_xp(con: tcod.console.Console, filename: str | PathLike[str], compress_level: int = 9) -> bool: """Save a console to a REXPaint `.xp` file. - .. versionchanged:: Unreleased + .. versionchanged:: 16.0 Added PathLike support. """ return bool(lib.TCOD_console_save_xp(_console(con), bytes(Path(filename)), compress_level)) @@ -2055,7 +2055,7 @@ def console_save_xp(con: tcod.console.Console, filename: str | PathLike[str], co def console_from_xp(filename: str | PathLike[str]) -> tcod.console.Console: """Return a single console from a REXPaint `.xp` file. - .. versionchanged:: Unreleased + .. versionchanged:: 16.0 Added PathLike support. """ filename = Path(filename).resolve(strict=True) @@ -2068,7 +2068,7 @@ def console_list_load_xp( ) -> list[tcod.console.Console] | None: """Return a list of consoles from a REXPaint `.xp` file. - .. versionchanged:: Unreleased + .. versionchanged:: 16.0 Added PathLike support. """ filename = Path(filename).resolve(strict=True) @@ -2093,7 +2093,7 @@ def console_list_save_xp( ) -> bool: """Save a list of consoles to a REXPaint `.xp` file. - .. versionchanged:: Unreleased + .. versionchanged:: 16.0 Added PathLike support. """ tcod_list = lib.TCOD_list_new() @@ -3040,10 +3040,10 @@ def image_load(filename: str | PathLike[str]) -> tcod.image.Image: Args: filename: Path to a .bmp or .png image file. - .. versionchanged:: Unreleased + .. versionchanged:: 16.0 Added PathLike support. - .. deprecated:: Unreleased + .. deprecated:: 16.0 Use :any:`tcod.image.Image.from_file` instead. """ return tcod.image.Image.from_file(filename) @@ -3058,7 +3058,7 @@ def image_from_console(console: tcod.console.Console) -> tcod.image.Image: Args: console (Console): Any Console instance. - .. deprecated:: Unreleased + .. deprecated:: 16.0 :any:`Tileset.render` is a better alternative. """ return tcod.image.Image._from_cdata( @@ -3073,7 +3073,7 @@ def image_from_console(console: tcod.console.Console) -> tcod.image.Image: def image_refresh_console(image: tcod.image.Image, console: tcod.console.Console) -> None: """Update an image made with :any:`image_from_console`. - .. deprecated:: Unreleased + .. deprecated:: 16.0 This function is unnecessary, use :any:`Tileset.render` instead. """ image.refresh_console(console) @@ -3419,7 +3419,7 @@ def map_get_height(map: tcod.map.Map) -> int: def mouse_show_cursor(visible: bool) -> None: """Change the visibility of the mouse cursor. - .. deprecated:: Unreleased + .. deprecated:: 16.0 Use :any:`tcod.sdl.mouse.show` instead. """ lib.TCOD_mouse_show_cursor(visible) @@ -3429,7 +3429,7 @@ def mouse_show_cursor(visible: bool) -> None: def mouse_is_cursor_visible() -> bool: """Return True if the mouse cursor is visible. - .. deprecated:: Unreleased + .. deprecated:: 16.0 Use :any:`tcod.sdl.mouse.show` instead. """ return bool(lib.TCOD_mouse_is_cursor_visible()) @@ -4087,7 +4087,7 @@ def sys_save_screenshot(name: str | PathLike[str] | None = None) -> None: This function is not supported by contexts. Use :any:`Context.save_screenshot` instead. - .. versionchanged:: Unreleased + .. versionchanged:: 16.0 Added PathLike support. """ lib.TCOD_sys_save_screenshot(bytes(Path(name)) if name is not None else ffi.NULL) diff --git a/tcod/sdl/audio.py b/tcod/sdl/audio.py index b995e336..d1f221df 100644 --- a/tcod/sdl/audio.py +++ b/tcod/sdl/audio.py @@ -111,7 +111,7 @@ def convert_audio( .. versionadded:: 13.6 - .. versionchanged:: Unreleased + .. versionchanged:: 16.0 Now converts floating types to `np.float32` when SDL doesn't support the specific format. .. seealso:: @@ -167,7 +167,7 @@ class AudioDevice: When you use this object directly the audio passed to :any:`queue_audio` is always played synchronously. For more typical asynchronous audio you should pass an AudioDevice to :any:`BasicMixer`. - .. versionchanged:: Unreleased + .. versionchanged:: 16.0 Can now be used as a context which will close the device on exit. """ diff --git a/tcod/sdl/mouse.py b/tcod/sdl/mouse.py index 3498c7d8..0de8ddbc 100644 --- a/tcod/sdl/mouse.py +++ b/tcod/sdl/mouse.py @@ -244,7 +244,7 @@ def show(visible: bool | None = None) -> bool: Returns: True if the cursor is visible. - .. versionadded:: Unreleased + .. versionadded:: 16.0 """ _OPTIONS = {None: lib.SDL_QUERY, False: lib.SDL_DISABLE, True: lib.SDL_ENABLE} return _check(lib.SDL_ShowCursor(_OPTIONS[visible])) == int(lib.SDL_ENABLE) From 93114e2df88c36251d0b5b54486e921668914564 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 28 May 2023 03:52:32 -0700 Subject: [PATCH 179/194] Fix multiple issues with tcod.sdl.audio Adds much needed testing of the tcod.sdl.audio module. Convert some asserts into real errors. Handle exceptions raised in audio callback as unraisable. --- .vscode/settings.json | 1 + CHANGELOG.md | 4 ++ tcod/sdl/audio.py | 67 +++++++++++++++++++--- tests/test_sdl.py | 14 ----- tests/test_sdl_audio.py | 121 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 186 insertions(+), 21 deletions(-) create mode 100644 tests/test_sdl_audio.py diff --git a/.vscode/settings.json b/.vscode/settings.json index 8eb6e93e..6ddcc5ef 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -412,6 +412,7 @@ "typestr", "undoc", "Unifont", + "unraisable", "unraisablehook", "unraiseable", "upscaling", diff --git a/CHANGELOG.md b/CHANGELOG.md index 5db385e8..166c2af1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] +### Fixed +- `AudioDevice.stopped` was inverted. +- Fixed the audio mixer stop and fadeout methods. +- Exceptions raised in the audio mixer callback no longer cause a messy crash, they now go to `sys.unraisablehook`. ## [16.0.0] - 2023-05-27 ### Added diff --git a/tcod/sdl/audio.py b/tcod/sdl/audio.py index d1f221df..c909aa73 100644 --- a/tcod/sdl/audio.py +++ b/tcod/sdl/audio.py @@ -47,6 +47,7 @@ import sys import threading import time +from dataclasses import dataclass from types import TracebackType from typing import Any, Callable, Hashable, Iterator @@ -65,7 +66,9 @@ def _get_format(format: DTypeLike) -> int: assert dt.fields is None bitsize = dt.itemsize * 8 assert 0 < bitsize <= lib.SDL_AUDIO_MASK_BITSIZE - assert dt.str[1] in "uif" + if not dt.str[1] in "uif": + msg = f"Unexpected dtype: {dt}" + raise TypeError(msg) is_signed = dt.str[1] != "u" is_float = dt.str[1] == "f" byteorder = dt.byteorder @@ -81,7 +84,21 @@ def _get_format(format: DTypeLike) -> int: def _dtype_from_format(format: int) -> np.dtype[Any]: - """Return a dtype from a SDL_AudioFormat.""" + """Return a dtype from a SDL_AudioFormat. + + >>> _dtype_from_format(tcod.lib.AUDIO_F32LSB) + dtype('float32') + >>> _dtype_from_format(tcod.lib.AUDIO_F32MSB) + dtype('>f4') + >>> _dtype_from_format(tcod.lib.AUDIO_S16LSB) + dtype('int16') + >>> _dtype_from_format(tcod.lib.AUDIO_S16MSB) + dtype('>i2') + >>> _dtype_from_format(tcod.lib.AUDIO_U16LSB) + dtype('uint16') + >>> _dtype_from_format(tcod.lib.AUDIO_U16MSB) + dtype('>u2') + """ bitsize = format & lib.SDL_AUDIO_MASK_BITSIZE assert bitsize % 8 == 0 byte_size = bitsize // 8 @@ -203,6 +220,8 @@ def __init__( def __repr__(self) -> str: """Return a representation of this device.""" + if self.stopped: + return f"<{self.__class__.__name__}() stopped=True>" items = [ f"{self.__class__.__name__}(device_id={self.device_id})", f"frequency={self.frequency}", @@ -211,7 +230,9 @@ def __repr__(self) -> str: f"channels={self.channels}", f"buffer_samples={self.buffer_samples}", f"buffer_bytes={self.buffer_bytes}", + f"paused={self.paused}", ] + if self.silence: items.append(f"silence={self.silence}") if self._handle is not None: @@ -241,7 +262,9 @@ def _sample_size(self) -> int: @property def stopped(self) -> bool: """Is True if the device has failed or was closed.""" - return bool(lib.SDL_GetAudioDeviceStatus(self.device_id) != lib.SDL_AUDIO_STOPPED) + if not hasattr(self, "device_id"): + return True + return bool(lib.SDL_GetAudioDeviceStatus(self.device_id) == lib.SDL_AUDIO_STOPPED) @property def paused(self) -> bool: @@ -404,7 +427,9 @@ def play( def _verify_audio_sample(self, sample: ArrayLike) -> NDArray[Any]: """Verify an audio sample is valid and return it as a Numpy array.""" array: NDArray[Any] = np.asarray(sample) - assert array.dtype == self.mixer.device.format + if array.dtype != self.mixer.device.format: + msg = f"Audio sample must be dtype={self.mixer.device.format}, input was dtype={array.dtype}" + raise TypeError(msg) if len(array.shape) == 1: array = array[:, np.newaxis] return array @@ -434,7 +459,7 @@ def fadeout(self, time: float) -> None: time_samples = round(time * self.mixer.device.frequency) + 1 buffer: NDArray[np.float32] = np.zeros((time_samples, self.mixer.device.channels), np.float32) self._on_mix(buffer) - buffer *= np.linspace(1.0, 0.0, time_samples + 1, endpoint=False)[1:] + buffer *= np.linspace(1.0, 0.0, time_samples + 1, endpoint=False)[1:, np.newaxis] self.sound_queue[:] = [buffer] def stop(self) -> None: @@ -536,13 +561,41 @@ class _AudioCallbackUserdata: device: AudioDevice +@dataclass +class _UnraisableHookArgs: + exc_type: type[BaseException] + exc_value: BaseException | None + exc_traceback: TracebackType | None + err_msg: str | None + object: object + + +class _ProtectedContext: + def __init__(self, obj: object = None) -> None: + self.obj = obj + + def __enter__(self) -> None: + pass + + def __exit__( + self, exc_type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None + ) -> bool: + if exc_type is None: + return False + if sys.version_info < (3, 8): + return False + sys.unraisablehook(_UnraisableHookArgs(exc_type, value, traceback, None, self.obj)) # type: ignore[arg-type] + return True + + @ffi.def_extern() # type: ignore -def _sdl_audio_callback(userdata: Any, stream: Any, length: int) -> None: +def _sdl_audio_callback(userdata: Any, stream: Any, length: int) -> None: # noqa: ANN401 """Handle audio device callbacks.""" data: _AudioCallbackUserdata = ffi.from_handle(userdata) device = data.device buffer = np.frombuffer(ffi.buffer(stream, length), dtype=device.format).reshape(-1, device.channels) - device._callback(device, buffer) + with _ProtectedContext(device): + device._callback(device, buffer) def _get_devices(capture: bool) -> Iterator[str]: diff --git a/tests/test_sdl.py b/tests/test_sdl.py index b7bf8571..fa2ac29a 100644 --- a/tests/test_sdl.py +++ b/tests/test_sdl.py @@ -1,11 +1,9 @@ """Test SDL specific features.""" -import contextlib import sys import numpy as np import pytest -import tcod.sdl.audio import tcod.sdl.render import tcod.sdl.sys import tcod.sdl.video @@ -83,15 +81,3 @@ def test_sdl_render_bad_types() -> None: tcod.sdl.render.Renderer(tcod.ffi.cast("SDL_Renderer*", tcod.ffi.NULL)) with pytest.raises(TypeError): tcod.sdl.render.Renderer(tcod.ffi.new("SDL_Rect*")) - - -def test_sdl_audio_device() -> None: - with contextlib.closing(tcod.sdl.audio.open(frequency=44100, format=np.float32, channels=2, paused=True)) as device: - assert device.convert(np.zeros(4, dtype=np.float32), 22050).shape[0] == 8 # noqa: PLR2004 - assert device.convert(np.zeros((4, 4), dtype=np.float32)).shape == (4, 2) - assert device.convert(np.zeros(4, dtype=np.int8)).shape[0] == 4 # noqa: PLR2004 - device.paused = False - device.paused = True - assert device.queued_samples == 0 - with contextlib.closing(tcod.sdl.audio.BasicMixer(device)) as mixer: - assert mixer diff --git a/tests/test_sdl_audio.py b/tests/test_sdl_audio.py new file mode 100644 index 00000000..90af5fd2 --- /dev/null +++ b/tests/test_sdl_audio.py @@ -0,0 +1,121 @@ +"""Test tcod.sdl.audio module.""" +import contextlib +import sys +import time +from typing import Any + +import numpy as np +import pytest +from numpy.typing import NDArray + +import tcod.sdl.audio + +# ruff: noqa: D103 + + +def test_devices() -> None: + list(tcod.sdl.audio.get_devices()) + list(tcod.sdl.audio.get_capture_devices()) + + +def test_audio_device() -> None: + with tcod.sdl.audio.open(frequency=44100, format=np.float32, channels=2, paused=True) as device: + assert not device.stopped + assert device.convert(np.zeros(4, dtype=np.float32), 22050).shape[0] == 8 # noqa: PLR2004 + assert device.convert(np.zeros((4, 4), dtype=np.float32)).shape == (4, 2) + assert device.convert(np.zeros(4, dtype=np.int8)).shape[0] == 4 # noqa: PLR2004 + assert device.paused is True + device.paused = False + assert device.paused is False + device.paused = True + assert device.queued_samples == 0 + with pytest.raises(TypeError): + device.callback # noqa: B018 + with pytest.raises(TypeError): + device.callback = lambda _device, _stream: None + with contextlib.closing(tcod.sdl.audio.BasicMixer(device)) as mixer: + assert mixer.daemon + assert mixer.play(np.zeros(4, np.float32)).busy + mixer.play(np.zeros(0, np.float32)) + mixer.play(np.full(1, 0.01, np.float32), on_end=lambda _: None) + mixer.play(np.full(1, 0.01, np.float32), loops=2, on_end=lambda _: None) + mixer.play(np.full(4, 0.01, np.float32), loops=2).stop() + mixer.play(np.full(100000, 0.01, np.float32)) + with pytest.raises(TypeError, match=r".*must be dtype=float32.*was dtype=int32"): + mixer.play(np.zeros(1, np.int32)) + time.sleep(0.001) + mixer.stop() + + +def test_audio_capture() -> None: + with tcod.sdl.audio.open(capture=True) as device: + assert not device.stopped + assert isinstance(device.dequeue_audio(), np.ndarray) + + +def test_audio_device_repr() -> None: + with tcod.sdl.audio.open(format=np.uint16, paused=True, callback=True) as device: + assert "silence=" in repr(device) + assert "callback=" in repr(device) + assert "stopped=" in repr(device) + + +def test_convert_bad_shape() -> None: + with pytest.raises(TypeError): + tcod.sdl.audio.convert_audio( + np.zeros((1, 1, 1), np.float32), 8000, out_rate=8000, out_format=np.float32, out_channels=1 + ) + + +def test_convert_bad_type() -> None: + with pytest.raises(TypeError, match=r".*bool"): + tcod.sdl.audio.convert_audio(np.zeros(8, bool), 8000, out_rate=8000, out_format=np.float32, out_channels=1) + with pytest.raises(RuntimeError, match=r"Invalid source format"): + tcod.sdl.audio.convert_audio(np.zeros(8, np.int64), 8000, out_rate=8000, out_format=np.float32, out_channels=1) + + +def test_convert_float64() -> None: + np.testing.assert_array_equal( + tcod.sdl.audio.convert_audio( + np.ones(8, np.float64), 8000, out_rate=8000, out_format=np.float32, out_channels=1 + ), + np.ones((8, 1), np.float32), + ) + + +def test_audio_callback() -> None: + class CheckCalled: + was_called: bool = False + + def __call__(self, device: tcod.sdl.audio.AudioDevice, stream: NDArray[Any]) -> None: + self.was_called = True + assert isinstance(device, tcod.sdl.audio.AudioDevice) + assert isinstance(stream, np.ndarray) + assert len(stream.shape) == 2 # noqa: PLR2004 + + check_called = CheckCalled() + with tcod.sdl.audio.open(callback=check_called, paused=False) as device: + device.callback = device.callback + while not check_called.was_called: + time.sleep(0.001) + + +@pytest.mark.skipif(sys.version_info < (3, 8), reason="Needs sys.unraisablehook support") +@pytest.mark.filterwarnings("ignore::pytest.PytestUnraisableExceptionWarning") +def test_audio_callback_unraisable() -> None: + """Test unraisable error in audio callback. + + This can't be checked with pytest very well, so at least make sure this doesn't crash. + """ + + class CheckCalled: + was_called: bool = False + + def __call__(self, device: tcod.sdl.audio.AudioDevice, stream: NDArray[Any]) -> None: + self.was_called = True + raise Exception("Test unraisable error") # noqa + + check_called = CheckCalled() + with tcod.sdl.audio.open(callback=check_called, paused=False): + while not check_called.was_called: + time.sleep(0.001) From 499059247f69ab3f55168db068ccb5a3e428e551 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 28 May 2023 04:36:37 -0700 Subject: [PATCH 180/194] Run tests in CI with a timeout. --- .github/workflows/python-package.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 9161d910..3c571b40 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -136,7 +136,7 @@ jobs: - name: Install Python dependencies run: | python -m pip install --upgrade pip - pip install pytest pytest-cov pytest-benchmark build + pip install pytest pytest-cov pytest-benchmark pytest-timeout build if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Initialize package run: | @@ -147,11 +147,11 @@ jobs: - name: Test with pytest if: runner.os == 'Windows' run: | - pytest --cov-report=xml + pytest --cov-report=xml --timeout=30 - name: Test with pytest (Xvfb) if: always() && runner.os != 'Windows' run: | - xvfb-run -e /tmp/xvfb.log --server-num=$RANDOM --auto-servernum pytest --cov-report=xml + xvfb-run -e /tmp/xvfb.log --server-num=$RANDOM --auto-servernum pytest --cov-report=xml --timeout=30 - name: Xvfb logs if: runner.os != 'Windows' run: cat /tmp/xvfb.log From 66a0bd91c9ab03b7dcd75f29e5fe24d45e783476 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 28 May 2023 04:52:34 -0700 Subject: [PATCH 181/194] Xfail tests when audio devices are missing. --- tests/test_sdl_audio.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/test_sdl_audio.py b/tests/test_sdl_audio.py index 90af5fd2..6441bda1 100644 --- a/tests/test_sdl_audio.py +++ b/tests/test_sdl_audio.py @@ -13,11 +13,20 @@ # ruff: noqa: D103 +needs_audio_device = pytest.mark.xfail( + not list(tcod.sdl.audio.get_devices()), reason="This test requires an audio device" +) +needs_audio_capture = pytest.mark.xfail( + not list(tcod.sdl.audio.get_capture_devices()), reason="This test requires an audio capture device" +) + + def test_devices() -> None: list(tcod.sdl.audio.get_devices()) list(tcod.sdl.audio.get_capture_devices()) +@needs_audio_device def test_audio_device() -> None: with tcod.sdl.audio.open(frequency=44100, format=np.float32, channels=2, paused=True) as device: assert not device.stopped @@ -47,14 +56,17 @@ def test_audio_device() -> None: mixer.stop() +@needs_audio_capture def test_audio_capture() -> None: with tcod.sdl.audio.open(capture=True) as device: assert not device.stopped assert isinstance(device.dequeue_audio(), np.ndarray) +@needs_audio_device def test_audio_device_repr() -> None: with tcod.sdl.audio.open(format=np.uint16, paused=True, callback=True) as device: + assert not device.stopped assert "silence=" in repr(device) assert "callback=" in repr(device) assert "stopped=" in repr(device) @@ -83,6 +95,7 @@ def test_convert_float64() -> None: ) +@needs_audio_device def test_audio_callback() -> None: class CheckCalled: was_called: bool = False @@ -95,6 +108,7 @@ def __call__(self, device: tcod.sdl.audio.AudioDevice, stream: NDArray[Any]) -> check_called = CheckCalled() with tcod.sdl.audio.open(callback=check_called, paused=False) as device: + assert not device.stopped device.callback = device.callback while not check_called.was_called: time.sleep(0.001) @@ -102,6 +116,7 @@ def __call__(self, device: tcod.sdl.audio.AudioDevice, stream: NDArray[Any]) -> @pytest.mark.skipif(sys.version_info < (3, 8), reason="Needs sys.unraisablehook support") @pytest.mark.filterwarnings("ignore::pytest.PytestUnraisableExceptionWarning") +@needs_audio_device def test_audio_callback_unraisable() -> None: """Test unraisable error in audio callback. @@ -116,6 +131,7 @@ def __call__(self, device: tcod.sdl.audio.AudioDevice, stream: NDArray[Any]) -> raise Exception("Test unraisable error") # noqa check_called = CheckCalled() - with tcod.sdl.audio.open(callback=check_called, paused=False): + with tcod.sdl.audio.open(callback=check_called, paused=False) as device: + assert not device.stopped while not check_called.was_called: time.sleep(0.001) From 07bf13c21ff3903a5fc4a5ae548047799d4faa6f Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sun, 28 May 2023 05:06:50 -0700 Subject: [PATCH 182/194] Prepare 16.0.1 release. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 166c2af1..872d4904 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] + +## [16.0.1] - 2023-05-28 ### Fixed - `AudioDevice.stopped` was inverted. - Fixed the audio mixer stop and fadeout methods. From 35144454d26852e3992b6ad29f638b9e40f7b6a9 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Mon, 29 May 2023 06:37:43 -0700 Subject: [PATCH 183/194] Remove unnecessary files from source distributions. Right now GitHub is the main place to track the source and the sources uploaded to PyPI are just to build with. I don't need to upload fonts, tools, or tool configs to PyPI. --- MANIFEST.in | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index dd211a1d..9fc63cae 100755 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,8 +1,23 @@ -include *.py *.cfg *.txt *.rst *.toml +global-exclude .* +prune .* + +include *.py +include *.txt +include *.rst +include *.toml +include *.md recursive-include tcod *.py *.c *.h +prune libtcod recursive-include libtcod/src *.glsl* *.c *.h -include libtcod/*.txt libtcod/*.md tests/data/*.pkl +include libtcod/*.txt +include libtcod/*.md + +prune docs +prune examples +prune fonts +prune scripts +prune tests -exclude tcod/*/SDL2.dll +global-exclude *.dll From ffd21bba370ae8362257e9607271d4b76dec4cb6 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 30 May 2023 23:31:21 -0700 Subject: [PATCH 184/194] Convert examples and testing to be more namespace friendly. --- .github/workflows/python-package.yml | 6 +++--- docs/installation.rst | 4 ++-- docs/tcod/getting-started.rst | 10 +++++++--- examples/distribution/PyInstaller/main.py | 5 ++++- examples/distribution/cx_Freeze/main.py | 5 ++++- examples/eventget.py | 3 ++- examples/sdl-hello-world.py | 2 +- examples/thread_jobs.py | 2 +- examples/ttf.py | 7 +++++-- tcod/console.py | 10 ++++++---- tcod/context.py | 4 ++-- tcod/event.py | 2 +- tcod/render.py | 2 +- tests/test_console.py | 22 +++++++++++++--------- tests/test_noise.py | 2 +- tests/test_random.py | 2 +- tests/test_tcod.py | 3 ++- tests/test_tileset.py | 2 +- 18 files changed, 57 insertions(+), 36 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 3c571b40..587ef2d5 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -191,7 +191,7 @@ jobs: pip install tcod-*.tar.gz - name: Confirm package import run: | - python -c "import tcod" + python -c "import tcod.context" linux-wheels: needs: [black, isort, mypy] @@ -232,7 +232,7 @@ jobs: yum-config-manager --enable epel && yum install -y SDL2-devel CIBW_BEFORE_TEST: pip install numpy - CIBW_TEST_COMMAND: python -c "import tcod" + CIBW_TEST_COMMAND: python -c "import tcod.context" # Skip test on emulated architectures CIBW_TEST_SKIP: "*_aarch64" - name: Archive wheel @@ -267,7 +267,7 @@ jobs: CIBW_ARCHS_MACOS: x86_64 arm64 universal2 CIBW_BEFORE_BUILD_MACOS: pip install --upgrade delocate CIBW_BEFORE_TEST: pip install numpy - CIBW_TEST_COMMAND: python -c "import tcod" + CIBW_TEST_COMMAND: python -c "import tcod.context" CIBW_TEST_SKIP: "pp* *-macosx_arm64 *-macosx_universal2:arm64" - name: Archive wheel uses: actions/upload-artifact@v3 diff --git a/docs/installation.rst b/docs/installation.rst index 109601cc..4b8e04ce 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -50,9 +50,9 @@ You can then verify that ``tcod`` is importable from the Python interpreter:: >python - >>> import tcod + >>> import tcod.context -If ``import tcod`` doesn't throw an ``ImportError`` then ``tcod`` has been installed correctly to your system libraries. +If ``import tcod.context`` doesn't throw an ``ImportError`` then ``tcod`` has been installed correctly to your system libraries. Some IDE's such as PyCharm will create a virtual environment which will ignore your system libraries and require tcod to be installed again in that new environment. diff --git a/docs/tcod/getting-started.rst b/docs/tcod/getting-started.rst index 4954fe6a..b277f864 100644 --- a/docs/tcod/getting-started.rst +++ b/docs/tcod/getting-started.rst @@ -23,7 +23,10 @@ Example:: #!/usr/bin/env python3 # Make sure 'dejavu10x10_gs_tc.png' is in the same directory as this script. - import tcod + import tcod.console + import tcod.context + import tcod.event + import tcod.tileset WIDTH, HEIGHT = 80, 60 # Console width and height in tiles. @@ -35,7 +38,7 @@ Example:: "dejavu10x10_gs_tc.png", 32, 8, tcod.tileset.CHARMAP_TCOD, ) # Create the main console. - console = tcod.Console(WIDTH, HEIGHT, order="F") + console = tcod.console.Console(WIDTH, HEIGHT, order="F") # Create a window based on this console and tileset. with tcod.context.new( # New window for a console of size columns×rows. columns=console.width, rows=console.height, tileset=tileset, @@ -87,7 +90,8 @@ clearing the console every frame and replacing it only on resizing the window. Example:: #!/usr/bin/env python3 - import tcod + import tcod.context + import tcod.event WIDTH, HEIGHT = 720, 480 # Window pixel resolution (when not maximized.) FLAGS = tcod.context.SDL_WINDOW_RESIZABLE | tcod.context.SDL_WINDOW_MAXIMIZED diff --git a/examples/distribution/PyInstaller/main.py b/examples/distribution/PyInstaller/main.py index 7f715351..e1c6090d 100755 --- a/examples/distribution/PyInstaller/main.py +++ b/examples/distribution/PyInstaller/main.py @@ -7,7 +7,10 @@ import sys from pathlib import Path -import tcod +import tcod.console +import tcod.context +import tcod.event +import tcod.tileset WIDTH, HEIGHT = 80, 60 diff --git a/examples/distribution/cx_Freeze/main.py b/examples/distribution/cx_Freeze/main.py index b1f51acb..8f608af7 100755 --- a/examples/distribution/cx_Freeze/main.py +++ b/examples/distribution/cx_Freeze/main.py @@ -1,6 +1,9 @@ #!/usr/bin/env python3 """cx_Freeze main script example.""" -import tcod +import tcod.console +import tcod.context +import tcod.event +import tcod.tileset WIDTH, HEIGHT = 80, 60 console = None diff --git a/examples/eventget.py b/examples/eventget.py index 32a12658..774ad3b1 100755 --- a/examples/eventget.py +++ b/examples/eventget.py @@ -6,7 +6,8 @@ """An demonstration of event handling using the tcod.event module.""" from typing import List, Set -import tcod +import tcod.context +import tcod.event import tcod.sdl.joystick import tcod.sdl.sys diff --git a/examples/sdl-hello-world.py b/examples/sdl-hello-world.py index d0ae93c0..b9c07547 100644 --- a/examples/sdl-hello-world.py +++ b/examples/sdl-hello-world.py @@ -4,7 +4,7 @@ import numpy as np from PIL import Image, ImageDraw, ImageFont # type: ignore # pip install Pillow -import tcod +import tcod.event import tcod.sdl.render import tcod.sdl.video diff --git a/examples/thread_jobs.py b/examples/thread_jobs.py index b935c91f..d6fa54fd 100755 --- a/examples/thread_jobs.py +++ b/examples/thread_jobs.py @@ -20,7 +20,7 @@ import timeit from typing import Callable, List, Tuple -import tcod +import tcod.map THREADS = multiprocessing.cpu_count() diff --git a/examples/ttf.py b/examples/ttf.py index de134165..e1c585d5 100755 --- a/examples/ttf.py +++ b/examples/ttf.py @@ -14,7 +14,10 @@ import numpy as np from numpy.typing import NDArray -import tcod +import tcod.console +import tcod.context +import tcod.event +import tcod.tileset FONT = "VeraMono.ttf" @@ -60,7 +63,7 @@ def load_ttf(path: str, size: Tuple[int, int]) -> tcod.tileset.Tileset: def main() -> None: """True-type font example script.""" - console = tcod.Console(16, 12, order="F") + console = tcod.console.Console(16, 12, order="F") with tcod.context.new( columns=console.width, rows=console.height, diff --git a/tcod/console.py b/tcod/console.py index 19114077..f14f8bca 100644 --- a/tcod/console.py +++ b/tcod/console.py @@ -1087,7 +1087,7 @@ def draw_frame( Example:: - >>> console = tcod.Console(12, 6) + >>> console = tcod.console.Console(12, 6) >>> console.draw_frame(x=0, y=0, width=3, height=3) >>> console.draw_frame(x=3, y=0, width=3, height=3, decoration="╔═╗║ ║╚═╝") >>> console.draw_frame(x=6, y=0, width=3, height=3, decoration="123456789") @@ -1272,7 +1272,8 @@ def load_xp(path: str | PathLike[str], order: Literal["C", "F"] = "C") -> tuple[ Example:: import numpy as np - import tcod + import tcod.console + import tcod.tileset path = "example.xp" # REXPaint file with one layer. @@ -1321,9 +1322,10 @@ def save_xp( Example:: import numpy as np - import tcod + import tcod.console + import tcod.tileset - console = tcod.Console(80, 24) # Example console. + console = tcod.console.Console(80, 24) # Example console. # Convert from Unicode to REXPaint's encoding. # Required to load this console correctly in the REXPaint tool. diff --git a/tcod/context.py b/tcod/context.py index b8f359d4..019097c2 100644 --- a/tcod/context.py +++ b/tcod/context.py @@ -58,7 +58,7 @@ from typing_extensions import Literal -import tcod +import tcod.console import tcod.event import tcod.render import tcod.sdl.render @@ -483,7 +483,7 @@ def new( sdl_window_flags: int | None = None, title: str | None = None, argv: Iterable[str] | None = None, - console: tcod.Console | None = None, + console: tcod.console.Console | None = None, ) -> Context: """Create a new context with the desired pixel size. diff --git a/tcod/event.py b/tcod/event.py index c415cb34..1379c40c 100644 --- a/tcod/event.py +++ b/tcod/event.py @@ -1253,7 +1253,7 @@ def wait(timeout: float | None = None) -> Iterator[Any]: context: tcod.context.Context # Context object initialized earlier. while True: # Main game-loop. - console: tcod.Console # Console used for rendering. + console: tcod.console.Console # Console used for rendering. ... # Render the frame to `console` and then: context.present(console) # Show the console to the display. # The ordering to draw first before waiting for events is important. diff --git a/tcod/render.py b/tcod/render.py index 71a98686..184d8abd 100644 --- a/tcod/render.py +++ b/tcod/render.py @@ -7,7 +7,7 @@ Example:: tileset = tcod.tileset.load_tilesheet("dejavu16x16_gs_tc.png", 32, 8, tcod.tileset.CHARMAP_TCOD) - console = tcod.Console(20, 8) + console = tcod.console.Console(20, 8) console.print(0, 0, "Hello World") sdl_window = tcod.sdl.video.new_window( console.width * tileset.tile_width, diff --git a/tests/test_console.py b/tests/test_console.py index 5ddc6f8e..a5af3d31 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -6,23 +6,24 @@ import pytest import tcod +import tcod.console # ruff: noqa: D103 -@pytest.mark.filterwarnings("ignore:Directly access a consoles") -@pytest.mark.filterwarnings("ignore:This function may be deprecated in the fu") def test_array_read_write() -> None: console = tcod.console.Console(width=12, height=10) FG = (255, 254, 253) BG = (1, 2, 3) CH = ord("&") - tcod.console_put_char_ex(console, 0, 0, CH, FG, BG) + with pytest.warns(): + tcod.console_put_char_ex(console, 0, 0, CH, FG, BG) assert console.ch[0, 0] == CH assert tuple(console.fg[0, 0]) == FG assert tuple(console.bg[0, 0]) == BG - tcod.console_put_char_ex(console, 1, 2, CH, FG, BG) + with pytest.warns(): + tcod.console_put_char_ex(console, 1, 2, CH, FG, BG) assert console.ch[2, 1] == CH assert tuple(console.fg[2, 1]) == FG assert tuple(console.bg[2, 1]) == BG @@ -37,9 +38,12 @@ def test_array_read_write() -> None: console.fg[1, ::2] = FG console.bg[...] = BG - assert tcod.console_get_char(console, 2, 1) == CH - assert tuple(tcod.console_get_char_foreground(console, 2, 1)) == FG - assert tuple(tcod.console_get_char_background(console, 2, 1)) == BG + with pytest.warns(): + assert tcod.console_get_char(console, 2, 1) == CH + with pytest.warns(): + assert tuple(tcod.console_get_char_foreground(console, 2, 1)) == FG + with pytest.warns(): + assert tuple(tcod.console_get_char_background(console, 2, 1)) == BG @pytest.mark.filterwarnings("ignore") @@ -137,7 +141,7 @@ def test_console_semigraphics() -> None: def test_rexpaint(tmp_path: Path) -> None: xp_path = tmp_path / "test.xp" - consoles = tcod.Console(80, 24, order="F"), tcod.Console(8, 8, order="F") + consoles = tcod.console.Console(80, 24, order="F"), tcod.console.Console(8, 8, order="F") tcod.console.save_xp(xp_path, consoles, compress_level=0) loaded = tcod.console.load_xp(xp_path, order="F") assert len(consoles) == len(loaded) @@ -149,7 +153,7 @@ def test_rexpaint(tmp_path: Path) -> None: def test_draw_frame() -> None: - console = tcod.Console(3, 3, order="C") + console = tcod.console.Console(3, 3, order="C") with pytest.raises(TypeError): console.draw_frame(0, 0, 3, 3, title="test", decoration="123456789") with pytest.raises(TypeError): diff --git a/tests/test_noise.py b/tests/test_noise.py index 6a653f06..0f64d5fe 100644 --- a/tests/test_noise.py +++ b/tests/test_noise.py @@ -5,7 +5,7 @@ import numpy as np import pytest -import tcod +import tcod.noise # ruff: noqa: D103 diff --git a/tests/test_random.py b/tests/test_random.py index c56c6678..e6b64e1e 100644 --- a/tests/test_random.py +++ b/tests/test_random.py @@ -3,7 +3,7 @@ import pickle from pathlib import Path -import tcod +import tcod.random # ruff: noqa: D103 diff --git a/tests/test_tcod.py b/tests/test_tcod.py index 5c731f48..b736e787 100644 --- a/tests/test_tcod.py +++ b/tests/test_tcod.py @@ -8,6 +8,7 @@ from numpy.typing import DTypeLike, NDArray import tcod +import tcod.console # ruff: noqa: D103 @@ -190,7 +191,7 @@ def test_context() -> None: pass WIDTH, HEIGHT = 16, 4 with tcod.context.new_terminal(columns=WIDTH, rows=HEIGHT, renderer=tcod.RENDERER_SDL2) as context: - console = tcod.Console(*context.recommended_console_size()) + console = tcod.console.Console(*context.recommended_console_size()) context.present(console) assert context.sdl_window_p is not None assert context.renderer_type >= 0 diff --git a/tests/test_tileset.py b/tests/test_tileset.py index c0c24c75..aba88b98 100644 --- a/tests/test_tileset.py +++ b/tests/test_tileset.py @@ -1,5 +1,5 @@ """Test for tcod.tileset module.""" -import tcod +import tcod.tileset # ruff: noqa: D103 From af6d8ea6b827e96cbd9a859e369d3f985d54c11f Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Tue, 30 May 2023 23:45:27 -0700 Subject: [PATCH 185/194] Fix readme typo. --- examples/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/README.md b/examples/README.md index 48fc5209..0594efe1 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,7 +1,7 @@ This directory contains a few example scripts for using python-tcod. `samples_tcod.py` is the mail example which uses most of the newer API. This -can be compared to `samokes_libtcodpy.py` which mostly uses deprecated +can be compared to `samples_libtcodpy.py` which mostly uses deprecated functions from the old API. Examples in the `distribution/` folder show how to distribute projects made From b28789cfa8ac5dcc4f5b2d244e5c5f484eaa47f2 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 31 May 2023 23:10:04 -0700 Subject: [PATCH 186/194] Give a better error when a context is used after it's closed. --- tcod/context.py | 32 +++++++++++++++++++++----------- tests/test_tcod.py | 2 ++ 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/tcod/context.py b/tcod/context.py index 019097c2..79eb7df1 100644 --- a/tcod/context.py +++ b/tcod/context.py @@ -176,8 +176,18 @@ def __init__(self, context_p: Any) -> None: @classmethod def _claim(cls, context_p: Any) -> Context: + """Return a new instance wrapping a context pointer.""" return cls(ffi.gc(context_p, lib.TCOD_context_delete)) + @property + def _p(self) -> Any: # noqa: ANN401 + """Return the context pointer or raise if it is missing.""" + try: + return self._context_p + except AttributeError: + msg = "This context has been closed can no longer be used." + raise RuntimeError(msg) from None + def __enter__(self) -> Context: """Enter this context which will close on exiting.""" return self @@ -236,18 +246,18 @@ def present( "align_y": align[1], }, ) - _check(lib.TCOD_context_present(self._context_p, console.console_c, viewport_args)) + _check(lib.TCOD_context_present(self._p, console.console_c, viewport_args)) def pixel_to_tile(self, x: int, y: int) -> tuple[int, int]: """Convert window pixel coordinates to tile coordinates.""" with ffi.new("int[2]", (x, y)) as xy: - _check(lib.TCOD_context_screen_pixel_to_tile_i(self._context_p, xy, xy + 1)) + _check(lib.TCOD_context_screen_pixel_to_tile_i(self._p, xy, xy + 1)) return xy[0], xy[1] def pixel_to_subtile(self, x: int, y: int) -> tuple[float, float]: """Convert window pixel coordinates to sub-tile coordinates.""" with ffi.new("double[2]", (x, y)) as xy: - _check(lib.TCOD_context_screen_pixel_to_tile_d(self._context_p, xy, xy + 1)) + _check(lib.TCOD_context_screen_pixel_to_tile_d(self._p, xy, xy + 1)) return xy[0], xy[1] def convert_event(self, event: _Event) -> _Event: @@ -286,7 +296,7 @@ def convert_event(self, event: _Event) -> _Event: def save_screenshot(self, path: str | None = None) -> None: """Save a screen-shot to the given file path.""" c_path = path.encode("utf-8") if path is not None else ffi.NULL - _check(lib.TCOD_context_save_screenshot(self._context_p, c_path)) + _check(lib.TCOD_context_save_screenshot(self._p, c_path)) def change_tileset(self, tileset: tcod.tileset.Tileset | None) -> None: """Change the active tileset used by this context. @@ -300,7 +310,7 @@ def change_tileset(self, tileset: tcod.tileset.Tileset | None) -> None: Using this method only one tileset is active per-frame. See :any:`tcod.render` if you want to renderer with multiple tilesets in a single frame. """ - _check(lib.TCOD_context_change_tileset(self._context_p, _handle_tileset(tileset))) + _check(lib.TCOD_context_change_tileset(self._p, _handle_tileset(tileset))) def new_console( self, @@ -359,7 +369,7 @@ def new_console( if magnification < 0: raise ValueError("Magnification must be greater than zero. (Got %f)" % magnification) size = ffi.new("int[2]") - _check(lib.TCOD_context_recommended_console_size(self._context_p, magnification, size, size + 1)) + _check(lib.TCOD_context_recommended_console_size(self._p, magnification, size, size + 1)) width, height = max(min_columns, size[0]), max(min_rows, size[1]) return tcod.console.Console(width, height, order=order) @@ -371,13 +381,13 @@ def recommended_console_size(self, min_columns: int = 1, min_rows: int = 1) -> t If result is only used to create a new console then you may want to call :any:`Context.new_console` instead. """ with ffi.new("int[2]") as size: - _check(lib.TCOD_context_recommended_console_size(self._context_p, 1.0, size, size + 1)) + _check(lib.TCOD_context_recommended_console_size(self._p, 1.0, size, size + 1)) return max(min_columns, size[0]), max(min_rows, size[1]) @property def renderer_type(self) -> int: """Return the libtcod renderer type used by this context.""" - return _check(lib.TCOD_context_get_renderer_type(self._context_p)) + return _check(lib.TCOD_context_get_renderer_type(self._p)) @property def sdl_window_p(self) -> Any: @@ -407,7 +417,7 @@ def toggle_fullscreen(context: tcod.context.Context) -> None: ) ''' - return lib.TCOD_context_get_sdl_window(self._context_p) + return lib.TCOD_context_get_sdl_window(self._p) @property def sdl_window(self) -> tcod.sdl.video.Window | None: @@ -439,7 +449,7 @@ def sdl_renderer(self) -> tcod.sdl.render.Renderer | None: .. versionadded:: 13.4 """ - p = lib.TCOD_context_get_sdl_renderer(self._context_p) + p = lib.TCOD_context_get_sdl_renderer(self._p) return tcod.sdl.render.Renderer(p) if p else None @property @@ -448,7 +458,7 @@ def sdl_atlas(self) -> tcod.render.SDLTilesetAtlas | None: .. versionadded:: 13.5 """ - if self._context_p.type not in (lib.TCOD_RENDERER_SDL, lib.TCOD_RENDERER_SDL2): + if self._p.type not in (lib.TCOD_RENDERER_SDL, lib.TCOD_RENDERER_SDL2): return None context_data = ffi.cast("struct TCOD_RendererSDL2*", self._context_p.contextdata_) return tcod.render.SDLTilesetAtlas._from_ref(context_data.renderer, context_data.atlas) diff --git a/tests/test_tcod.py b/tests/test_tcod.py index b736e787..0f9ddf15 100644 --- a/tests/test_tcod.py +++ b/tests/test_tcod.py @@ -198,3 +198,5 @@ def test_context() -> None: context.change_tileset(tcod.tileset.Tileset(16, 16)) context.pixel_to_tile(0, 0) context.pixel_to_subtile(0, 0) + with pytest.raises(RuntimeError, match=".*context has been closed"): + context.present(console) From 21c93c12129927ad5df479bcde6da1403cbf4bcc Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 31 May 2023 23:29:57 -0700 Subject: [PATCH 187/194] Increase test timeout. Now that hanging issues are fixed the timeout should be far more relaxed. --- .github/workflows/python-package.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 587ef2d5..95e391c2 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -147,11 +147,11 @@ jobs: - name: Test with pytest if: runner.os == 'Windows' run: | - pytest --cov-report=xml --timeout=30 + pytest --cov-report=xml --timeout=300 - name: Test with pytest (Xvfb) if: always() && runner.os != 'Windows' run: | - xvfb-run -e /tmp/xvfb.log --server-num=$RANDOM --auto-servernum pytest --cov-report=xml --timeout=30 + xvfb-run -e /tmp/xvfb.log --server-num=$RANDOM --auto-servernum pytest --cov-report=xml --timeout=300 - name: Xvfb logs if: runner.os != 'Windows' run: cat /tmp/xvfb.log From fc2ba4f888c880ff7dfe8fe901dcbe8eae52dfd3 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 31 May 2023 23:35:03 -0700 Subject: [PATCH 188/194] Handle warnings better in deprecated tests. Remove outdated doctest which was fully deprecated. --- tcod/console.py | 12 +++++++----- tcod/libtcodpy.py | 13 ------------- tests/test_console.py | 7 ++++--- tests/test_deprecated.py | 4 +++- tests/test_tcod.py | 2 +- 5 files changed, 15 insertions(+), 23 deletions(-) diff --git a/tcod/console.py b/tcod/console.py index f14f8bca..f5147e23 100644 --- a/tcod/console.py +++ b/tcod/console.py @@ -304,10 +304,11 @@ def rgba(self) -> NDArray[Any]: Example: >>> con = tcod.console.Console(10, 2) + >>> WHITE, BLACK = (255, 255, 255), (0, 0, 0) >>> con.rgba[0, 0] = ( ... ord("X"), - ... (*tcod.white, 255), - ... (*tcod.black, 255), + ... (*WHITE, 255), + ... (*BLACK, 255), ... ) >>> con.rgba[0, 0] (88, [255, 255, 255, 255], [ 0, 0, 0, 255]) @@ -328,10 +329,11 @@ def rgb(self) -> NDArray[Any]: Example: >>> con = tcod.console.Console(10, 2) - >>> con.rgb[0, 0] = ord("@"), tcod.yellow, tcod.black + >>> BLUE, YELLOW, BLACK = (0, 0, 255), (255, 255, 0), (0, 0, 0) + >>> con.rgb[0, 0] = ord("@"), YELLOW, BLACK >>> con.rgb[0, 0] (64, [255, 255, 0], [0, 0, 0]) - >>> con.rgb["bg"] = tcod.blue + >>> con.rgb["bg"] = BLUE >>> con.rgb[0, 0] (64, [255, 255, 0], [ 0, 0, 255]) @@ -916,7 +918,7 @@ def __repr__(self) -> str: self.width, self.height, self._order, - self.tiles, + self.rgba, ) def __str__(self) -> str: diff --git a/tcod/libtcodpy.py b/tcod/libtcodpy.py index 6bb1c762..4a258081 100644 --- a/tcod/libtcodpy.py +++ b/tcod/libtcodpy.py @@ -3260,19 +3260,6 @@ def line_where(x1: int, y1: int, x2: int, y2: int, inclusive: bool = True) -> tu If `inclusive` is true then the start point is included in the result. - Example: - >>> where = tcod.line_where(1, 0, 3, 4) - >>> where - (array([1, 1, 2, 2, 3]...), array([0, 1, 2, 3, 4]...)) - >>> array = np.zeros((5, 5), dtype=np.int32) - >>> array[where] = np.arange(len(where[0])) + 1 - >>> array - array([[0, 0, 0, 0, 0], - [1, 2, 0, 0, 0], - [0, 0, 3, 4, 0], - [0, 0, 0, 0, 5], - [0, 0, 0, 0, 0]]...) - .. versionadded:: 4.6 .. deprecated:: 11.14 diff --git a/tests/test_console.py b/tests/test_console.py index a5af3d31..f8fa5d91 100644 --- a/tests/test_console.py +++ b/tests/test_console.py @@ -106,11 +106,11 @@ def test_console_repr() -> None: eval(repr(tcod.console.Console(10, 2))) -@pytest.mark.filterwarnings("ignore") def test_console_str() -> None: console = tcod.console.Console(10, 2) console.ch[:] = ord(".") - console.print_(0, 0, "Test") + with pytest.warns(): + console.print_(0, 0, "Test") assert str(console) == ("") @@ -161,4 +161,5 @@ def test_draw_frame() -> None: console.draw_frame(0, 0, 3, 3, decoration=(49, 50, 51, 52, 53, 54, 55, 56, 57)) assert console.ch.tolist() == [[49, 50, 51], [52, 53, 54], [55, 56, 57]] - console.draw_frame(0, 0, 3, 3, title="T") + with pytest.warns(): + console.draw_frame(0, 0, 3, 3, title="T") diff --git a/tests/test_deprecated.py b/tests/test_deprecated.py index 425cafb1..ec4894c9 100644 --- a/tests/test_deprecated.py +++ b/tests/test_deprecated.py @@ -3,11 +3,13 @@ import pytest -import libtcodpy import tcod import tcod.event import tcod.libtcodpy +with pytest.warns(): + import libtcodpy + # ruff: noqa: D103 diff --git a/tests/test_tcod.py b/tests/test_tcod.py index 0f9ddf15..33e50195 100644 --- a/tests/test_tcod.py +++ b/tests/test_tcod.py @@ -19,7 +19,7 @@ def raise_Exception(*args: object) -> NoReturn: def test_line_error() -> None: """Test exception propagation.""" - with pytest.raises(RuntimeError): + with pytest.raises(RuntimeError), pytest.warns(): tcod.line(0, 0, 10, 10, py_callback=raise_Exception) From 9e1291de40cb211fe68dca653b39dffa41f33541 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Thu, 1 Jun 2023 00:30:05 -0700 Subject: [PATCH 189/194] Backport deprecated line_where test. So that this is still covered since the doctest has been removed. --- tests/test_deprecated.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_deprecated.py b/tests/test_deprecated.py index ec4894c9..9d871ba7 100644 --- a/tests/test_deprecated.py +++ b/tests/test_deprecated.py @@ -1,6 +1,7 @@ """Test deprecated features.""" from __future__ import annotations +import numpy as np import pytest import tcod @@ -29,3 +30,9 @@ def test_deprecate_key_constants() -> None: _ = tcod.event.K_1 with pytest.raises(FutureWarning, match=r".*Scancode.N1"): _ = tcod.event.SCANCODE_1 + + +def test_line_where() -> None: + with pytest.warns(): + where = tcod.libtcodpy.line_where(1, 0, 3, 4) + np.testing.assert_array_equal(where, [[1, 1, 2, 2, 3], [0, 1, 2, 3, 4]]) From 3bed09fa4d395f89d4fed63c0cfa12189362fbee Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 2 Jun 2023 07:37:17 -0700 Subject: [PATCH 190/194] Fix joystick removal access. Joysticks can not be accessed by their instance id during removal. This change stores them in a weak dictionary so that they may be accessed that way. --- CHANGELOG.md | 2 ++ tcod/sdl/joystick.py | 18 ++++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 872d4904..c23fd3dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] +### Fixed +- Joystick/controller device events would raise `RuntimeError` when accessed after removal. ## [16.0.1] - 2023-05-28 ### Fixed diff --git a/tcod/sdl/joystick.py b/tcod/sdl/joystick.py index 5d24d353..5e2390ec 100644 --- a/tcod/sdl/joystick.py +++ b/tcod/sdl/joystick.py @@ -5,7 +5,8 @@ from __future__ import annotations import enum -from typing import Any +from typing import Any, ClassVar +from weakref import WeakValueDictionary from typing_extensions import Final, Literal @@ -122,6 +123,9 @@ class Joystick: https://wiki.libsdl.org/CategoryJoystick """ + _by_instance_id: ClassVar[WeakValueDictionary[int, Joystick]] = WeakValueDictionary() + """Currently opened joysticks.""" + def __init__(self, sdl_joystick_p: Any) -> None: self.sdl_joystick_p: Final = sdl_joystick_p """The CFFI pointer to an SDL_Joystick struct.""" @@ -142,6 +146,8 @@ def __init__(self, sdl_joystick_p: Any) -> None: self._keep_alive: Any = None """The owner of this objects memory if this object does not own itself.""" + self._by_instance_id[self.id] = self + @classmethod def _open(cls, device_index: int) -> Joystick: tcod.sdl.sys.init(tcod.sdl.sys.Subsystem.JOYSTICK) @@ -150,7 +156,7 @@ def _open(cls, device_index: int) -> Joystick: @classmethod def _from_instance_id(cls, instance_id: int) -> Joystick: - return cls(_check_p(ffi.gc(lib.SDL_JoystickFromInstanceID(instance_id), lib.SDL_JoystickClose))) + return cls._by_instance_id[instance_id] def __eq__(self, other: object) -> bool: if isinstance(other, Joystick): @@ -163,7 +169,7 @@ def __hash__(self) -> int: def _get_guid(self) -> str: guid_str = ffi.new("char[33]") lib.SDL_JoystickGetGUIDString(lib.SDL_JoystickGetGUID(self.sdl_joystick_p), guid_str, len(guid_str)) - return str(tcod.ffi.string(guid_str), encoding="utf-8") + return str(ffi.string(guid_str), encoding="ascii") def get_current_power(self) -> Power: """Return the power level/state of this joystick. See :any:`Power`.""" @@ -191,11 +197,15 @@ def get_hat(self, hat: int) -> tuple[Literal[-1, 0, 1], Literal[-1, 0, 1]]: class GameController: """A standard interface for an Xbox 360 style game controller.""" + _by_instance_id: ClassVar[WeakValueDictionary[int, GameController]] = WeakValueDictionary() + """Currently opened controllers.""" + def __init__(self, sdl_controller_p: Any) -> None: self.sdl_controller_p: Final = sdl_controller_p self.joystick: Final = Joystick(lib.SDL_GameControllerGetJoystick(self.sdl_controller_p)) """The :any:`Joystick` associated with this controller.""" self.joystick._keep_alive = self.sdl_controller_p # This objects real owner needs to be kept alive. + self._by_instance_id[self.joystick.id] = self @classmethod def _open(cls, joystick_index: int) -> GameController: @@ -203,7 +213,7 @@ def _open(cls, joystick_index: int) -> GameController: @classmethod def _from_instance_id(cls, instance_id: int) -> GameController: - return cls(_check_p(ffi.gc(lib.SDL_GameControllerFromInstanceID(instance_id), lib.SDL_GameControllerClose))) + return cls._by_instance_id[instance_id] def get_button(self, button: ControllerButton) -> bool: """Return True if `button` is currently held.""" From c061679b18cfaac162c576ef5d6513cf123eee75 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Fri, 2 Jun 2023 08:08:18 -0700 Subject: [PATCH 191/194] Prepare 16.0.2 release. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c23fd3dc..c9f0e36e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ Changes relevant to the users of python-tcod are documented here. This project adheres to [Semantic Versioning](https://semver.org/) since version `2.0.0`. ## [Unreleased] + +## [16.0.2] - 2023-06-02 ### Fixed - Joystick/controller device events would raise `RuntimeError` when accessed after removal. From 49ebc65ddf0fb0292f667dca9d43b7d5fe1ca7bd Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 3 Jun 2023 00:11:55 -0700 Subject: [PATCH 192/194] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 28 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..dc89cbfd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,28 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior. A minimal code example is recommended. + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. Windows 11] + - Python version [e.g. Python 3.11, PyPy 3.9] + - tcod version [e.g. 16.0.2] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..11fc491e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From ce0ea6408dda735c731c405d4153a02267f6e8a5 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 3 Jun 2023 08:08:28 -0700 Subject: [PATCH 193/194] Add more logging for libtcod and SDL. Move protected context closer to tcod.sdl internals. --- tcod/cdef.h | 3 +++ tcod/loader.py | 17 +++++++++++++++ tcod/sdl/__init__.py | 52 +++++++++++++++++++++++++++++++++++++++++--- tcod/sdl/audio.py | 30 +------------------------ 4 files changed, 70 insertions(+), 32 deletions(-) diff --git a/tcod/cdef.h b/tcod/cdef.h index 03db6cbc..aecccc9c 100644 --- a/tcod/cdef.h +++ b/tcod/cdef.h @@ -17,4 +17,7 @@ float _pycall_path_dest_only(int x1, int y1, int x2, int y2, void* user_data); void _pycall_sdl_hook(struct SDL_Surface*); void _pycall_cli_output(void* userdata, const char* output); + +// Libtcod log watch function. +void _libtcod_log_watcher(const TCOD_LogMessage* message, void* userdata); } diff --git a/tcod/loader.py b/tcod/loader.py index 0027fb8c..36e6d17b 100644 --- a/tcod/loader.py +++ b/tcod/loader.py @@ -1,6 +1,7 @@ """This module handles loading of the libtcod cffi API.""" from __future__ import annotations +import logging import os import platform import sys @@ -9,6 +10,8 @@ import cffi +logger = logging.getLogger("tcod") + __sdl_version__ = "" ffi_check = cffi.FFI() @@ -83,4 +86,18 @@ def __bool__(self) -> bool: __sdl_version__ = get_sdl_version() + +@ffi.def_extern() # type: ignore +def _libtcod_log_watcher(message: Any, userdata: None) -> None: + text = str(ffi.string(message.message), encoding="utf-8") + source = str(ffi.string(message.source), encoding="utf-8") + level = int(message.level) + lineno = int(message.lineno) + logger.log(level, "%s:%d:%s", source, lineno, text) + + +if lib: + lib.TCOD_set_log_callback(lib._libtcod_log_watcher, ffi.NULL) + lib.TCOD_set_log_level(0) + __all__ = ["ffi", "lib"] diff --git a/tcod/sdl/__init__.py b/tcod/sdl/__init__.py index 84ee2d06..b99ad675 100644 --- a/tcod/sdl/__init__.py +++ b/tcod/sdl/__init__.py @@ -1,7 +1,10 @@ from __future__ import annotations import logging +import sys as _sys +from dataclasses import dataclass from pkgutil import extend_path +from types import TracebackType from typing import Any, Callable, TypeVar from tcod.loader import ffi, lib @@ -10,7 +13,7 @@ T = TypeVar("T") -logger = logging.getLogger(__name__) +logger = logging.getLogger("tcod.sdl") _LOG_PRIORITY = { 1: logging.DEBUG, # SDL_LOG_PRIORITY_VERBOSE @@ -21,11 +24,52 @@ 6: logging.CRITICAL, # SDL_LOG_PRIORITY_CRITICAL } +_LOG_CATEGORY = { + int(lib.SDL_LOG_CATEGORY_APPLICATION): "APPLICATION", + int(lib.SDL_LOG_CATEGORY_ERROR): "ERROR", + int(lib.SDL_LOG_CATEGORY_ASSERT): "ASSERT", + int(lib.SDL_LOG_CATEGORY_SYSTEM): "SYSTEM", + int(lib.SDL_LOG_CATEGORY_AUDIO): "AUDIO", + int(lib.SDL_LOG_CATEGORY_VIDEO): "VIDEO", + int(lib.SDL_LOG_CATEGORY_RENDER): "RENDER", + int(lib.SDL_LOG_CATEGORY_INPUT): "INPUT", + int(lib.SDL_LOG_CATEGORY_TEST): "TEST", + int(lib.SDL_LOG_CATEGORY_CUSTOM): "", +} + + +@dataclass +class _UnraisableHookArgs: + exc_type: type[BaseException] + exc_value: BaseException | None + exc_traceback: TracebackType | None + err_msg: str | None + object: object + + +class _ProtectedContext: + def __init__(self, obj: object = None) -> None: + self.obj = obj + + def __enter__(self) -> None: + pass + + def __exit__( + self, exc_type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None + ) -> bool: + if exc_type is None: + return False + if _sys.version_info < (3, 8): + return False + _sys.unraisablehook(_UnraisableHookArgs(exc_type, value, traceback, None, self.obj)) # type: ignore[arg-type] + return True + @ffi.def_extern() # type: ignore -def _sdl_log_output_function(_userdata: Any, category: int, priority: int, message: Any) -> None: +def _sdl_log_output_function(_userdata: None, category: int, priority: int, message_p: Any) -> None: # noqa: ANN401 """Pass logs sent by SDL to Python's logging system.""" - logger.log(_LOG_PRIORITY.get(priority, 0), "%i:%s", category, ffi.string(message).decode("utf-8")) + message = str(ffi.string(message_p), encoding="utf-8") + logger.log(_LOG_PRIORITY.get(priority, 0), "%s:%s", _LOG_CATEGORY.get(category, ""), message) def _get_error() -> str: @@ -49,6 +93,8 @@ def _check_p(result: Any) -> Any: if lib._sdl_log_output_function: lib.SDL_LogSetOutputFunction(lib._sdl_log_output_function, ffi.NULL) + if __debug__: + lib.SDL_LogSetAllPriority(lib.SDL_LOG_PRIORITY_VERBOSE) def _compiled_version() -> tuple[int, int, int]: diff --git a/tcod/sdl/audio.py b/tcod/sdl/audio.py index c909aa73..88d9323a 100644 --- a/tcod/sdl/audio.py +++ b/tcod/sdl/audio.py @@ -47,7 +47,6 @@ import sys import threading import time -from dataclasses import dataclass from types import TracebackType from typing import Any, Callable, Hashable, Iterator @@ -57,7 +56,7 @@ import tcod.sdl.sys from tcod.loader import ffi, lib -from tcod.sdl import _check, _get_error +from tcod.sdl import _check, _get_error, _ProtectedContext def _get_format(format: DTypeLike) -> int: @@ -561,33 +560,6 @@ class _AudioCallbackUserdata: device: AudioDevice -@dataclass -class _UnraisableHookArgs: - exc_type: type[BaseException] - exc_value: BaseException | None - exc_traceback: TracebackType | None - err_msg: str | None - object: object - - -class _ProtectedContext: - def __init__(self, obj: object = None) -> None: - self.obj = obj - - def __enter__(self) -> None: - pass - - def __exit__( - self, exc_type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None - ) -> bool: - if exc_type is None: - return False - if sys.version_info < (3, 8): - return False - sys.unraisablehook(_UnraisableHookArgs(exc_type, value, traceback, None, self.obj)) # type: ignore[arg-type] - return True - - @ffi.def_extern() # type: ignore def _sdl_audio_callback(userdata: Any, stream: Any, length: int) -> None: # noqa: ANN401 """Handle audio device callbacks.""" From 01c9bb631c215e262334a246b36253d1f29c072f Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Sat, 3 Jun 2023 08:51:01 -0700 Subject: [PATCH 194/194] Move tcod.sdl internals to a sub-module. To keep things out of a potential namespace. --- tcod/sdl/__init__.py | 126 +----------------------------------------- tcod/sdl/_internal.py | 126 ++++++++++++++++++++++++++++++++++++++++++ tcod/sdl/audio.py | 2 +- tcod/sdl/joystick.py | 2 +- tcod/sdl/mouse.py | 2 +- tcod/sdl/render.py | 2 +- tcod/sdl/sys.py | 2 +- tcod/sdl/video.py | 2 +- 8 files changed, 133 insertions(+), 131 deletions(-) create mode 100644 tcod/sdl/_internal.py diff --git a/tcod/sdl/__init__.py b/tcod/sdl/__init__.py index b99ad675..8133948b 100644 --- a/tcod/sdl/__init__.py +++ b/tcod/sdl/__init__.py @@ -1,128 +1,4 @@ -from __future__ import annotations - -import logging -import sys as _sys -from dataclasses import dataclass +"""tcod.sdl package.""" from pkgutil import extend_path -from types import TracebackType -from typing import Any, Callable, TypeVar - -from tcod.loader import ffi, lib __path__ = extend_path(__path__, __name__) - -T = TypeVar("T") - -logger = logging.getLogger("tcod.sdl") - -_LOG_PRIORITY = { - 1: logging.DEBUG, # SDL_LOG_PRIORITY_VERBOSE - 2: logging.DEBUG, # SDL_LOG_PRIORITY_DEBUG - 3: logging.INFO, # SDL_LOG_PRIORITY_INFO - 4: logging.WARNING, # SDL_LOG_PRIORITY_WARN - 5: logging.ERROR, # SDL_LOG_PRIORITY_ERROR - 6: logging.CRITICAL, # SDL_LOG_PRIORITY_CRITICAL -} - -_LOG_CATEGORY = { - int(lib.SDL_LOG_CATEGORY_APPLICATION): "APPLICATION", - int(lib.SDL_LOG_CATEGORY_ERROR): "ERROR", - int(lib.SDL_LOG_CATEGORY_ASSERT): "ASSERT", - int(lib.SDL_LOG_CATEGORY_SYSTEM): "SYSTEM", - int(lib.SDL_LOG_CATEGORY_AUDIO): "AUDIO", - int(lib.SDL_LOG_CATEGORY_VIDEO): "VIDEO", - int(lib.SDL_LOG_CATEGORY_RENDER): "RENDER", - int(lib.SDL_LOG_CATEGORY_INPUT): "INPUT", - int(lib.SDL_LOG_CATEGORY_TEST): "TEST", - int(lib.SDL_LOG_CATEGORY_CUSTOM): "", -} - - -@dataclass -class _UnraisableHookArgs: - exc_type: type[BaseException] - exc_value: BaseException | None - exc_traceback: TracebackType | None - err_msg: str | None - object: object - - -class _ProtectedContext: - def __init__(self, obj: object = None) -> None: - self.obj = obj - - def __enter__(self) -> None: - pass - - def __exit__( - self, exc_type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None - ) -> bool: - if exc_type is None: - return False - if _sys.version_info < (3, 8): - return False - _sys.unraisablehook(_UnraisableHookArgs(exc_type, value, traceback, None, self.obj)) # type: ignore[arg-type] - return True - - -@ffi.def_extern() # type: ignore -def _sdl_log_output_function(_userdata: None, category: int, priority: int, message_p: Any) -> None: # noqa: ANN401 - """Pass logs sent by SDL to Python's logging system.""" - message = str(ffi.string(message_p), encoding="utf-8") - logger.log(_LOG_PRIORITY.get(priority, 0), "%s:%s", _LOG_CATEGORY.get(category, ""), message) - - -def _get_error() -> str: - """Return a message from SDL_GetError as a Unicode string.""" - return str(ffi.string(lib.SDL_GetError()), encoding="utf-8") - - -def _check(result: int) -> int: - """Check if an SDL function returned without errors, and raise an exception if it did.""" - if result < 0: - raise RuntimeError(_get_error()) - return result - - -def _check_p(result: Any) -> Any: - """Check if an SDL function returned NULL, and raise an exception if it did.""" - if not result: - raise RuntimeError(_get_error()) - return result - - -if lib._sdl_log_output_function: - lib.SDL_LogSetOutputFunction(lib._sdl_log_output_function, ffi.NULL) - if __debug__: - lib.SDL_LogSetAllPriority(lib.SDL_LOG_PRIORITY_VERBOSE) - - -def _compiled_version() -> tuple[int, int, int]: - return int(lib.SDL_MAJOR_VERSION), int(lib.SDL_MINOR_VERSION), int(lib.SDL_PATCHLEVEL) - - -def _linked_version() -> tuple[int, int, int]: - sdl_version = ffi.new("SDL_version*") - lib.SDL_GetVersion(sdl_version) - return int(sdl_version.major), int(sdl_version.minor), int(sdl_version.patch) - - -def _version_at_least(required: tuple[int, int, int]) -> None: - """Raise an error if the compiled version is less than required. Used to guard recently defined SDL functions.""" - if required <= _compiled_version(): - return - msg = f"This feature requires SDL version {required}, but tcod was compiled with version {_compiled_version()}" - raise RuntimeError(msg) - - -def _required_version(required: tuple[int, int, int]) -> Callable[[T], T]: - if not lib: # Read the docs mock object. - return lambda x: x - if required <= _compiled_version(): - return lambda x: x - - def replacement(*_args: Any, **_kwargs: Any) -> Any: - msg = f"This feature requires SDL version {required}, but tcod was compiled with version {_compiled_version()}" - raise RuntimeError(msg) - - return lambda x: replacement # type: ignore[return-value] diff --git a/tcod/sdl/_internal.py b/tcod/sdl/_internal.py new file mode 100644 index 00000000..79d59339 --- /dev/null +++ b/tcod/sdl/_internal.py @@ -0,0 +1,126 @@ +"""tcod.sdl private functions.""" +from __future__ import annotations + +import logging +import sys as _sys +from dataclasses import dataclass +from types import TracebackType +from typing import Any, Callable, TypeVar + +from tcod.loader import ffi, lib + +T = TypeVar("T") + +logger = logging.getLogger("tcod.sdl") + +_LOG_PRIORITY = { + 1: logging.DEBUG, # SDL_LOG_PRIORITY_VERBOSE + 2: logging.DEBUG, # SDL_LOG_PRIORITY_DEBUG + 3: logging.INFO, # SDL_LOG_PRIORITY_INFO + 4: logging.WARNING, # SDL_LOG_PRIORITY_WARN + 5: logging.ERROR, # SDL_LOG_PRIORITY_ERROR + 6: logging.CRITICAL, # SDL_LOG_PRIORITY_CRITICAL +} + +_LOG_CATEGORY = { + int(lib.SDL_LOG_CATEGORY_APPLICATION): "APPLICATION", + int(lib.SDL_LOG_CATEGORY_ERROR): "ERROR", + int(lib.SDL_LOG_CATEGORY_ASSERT): "ASSERT", + int(lib.SDL_LOG_CATEGORY_SYSTEM): "SYSTEM", + int(lib.SDL_LOG_CATEGORY_AUDIO): "AUDIO", + int(lib.SDL_LOG_CATEGORY_VIDEO): "VIDEO", + int(lib.SDL_LOG_CATEGORY_RENDER): "RENDER", + int(lib.SDL_LOG_CATEGORY_INPUT): "INPUT", + int(lib.SDL_LOG_CATEGORY_TEST): "TEST", + int(lib.SDL_LOG_CATEGORY_CUSTOM): "", +} + + +@dataclass +class _UnraisableHookArgs: + exc_type: type[BaseException] + exc_value: BaseException | None + exc_traceback: TracebackType | None + err_msg: str | None + object: object + + +class _ProtectedContext: + def __init__(self, obj: object = None) -> None: + self.obj = obj + + def __enter__(self) -> None: + pass + + def __exit__( + self, exc_type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None + ) -> bool: + if exc_type is None: + return False + if _sys.version_info < (3, 8): + return False + _sys.unraisablehook(_UnraisableHookArgs(exc_type, value, traceback, None, self.obj)) # type: ignore[arg-type] + return True + + +@ffi.def_extern() # type: ignore +def _sdl_log_output_function(_userdata: None, category: int, priority: int, message_p: Any) -> None: # noqa: ANN401 + """Pass logs sent by SDL to Python's logging system.""" + message = str(ffi.string(message_p), encoding="utf-8") + logger.log(_LOG_PRIORITY.get(priority, 0), "%s:%s", _LOG_CATEGORY.get(category, ""), message) + + +def _get_error() -> str: + """Return a message from SDL_GetError as a Unicode string.""" + return str(ffi.string(lib.SDL_GetError()), encoding="utf-8") + + +def _check(result: int) -> int: + """Check if an SDL function returned without errors, and raise an exception if it did.""" + if result < 0: + raise RuntimeError(_get_error()) + return result + + +def _check_p(result: Any) -> Any: + """Check if an SDL function returned NULL, and raise an exception if it did.""" + if not result: + raise RuntimeError(_get_error()) + return result + + +if lib._sdl_log_output_function: + lib.SDL_LogSetOutputFunction(lib._sdl_log_output_function, ffi.NULL) + if __debug__: + lib.SDL_LogSetAllPriority(lib.SDL_LOG_PRIORITY_VERBOSE) + + +def _compiled_version() -> tuple[int, int, int]: + return int(lib.SDL_MAJOR_VERSION), int(lib.SDL_MINOR_VERSION), int(lib.SDL_PATCHLEVEL) + + +def _linked_version() -> tuple[int, int, int]: + sdl_version = ffi.new("SDL_version*") + lib.SDL_GetVersion(sdl_version) + return int(sdl_version.major), int(sdl_version.minor), int(sdl_version.patch) + + +def _version_at_least(required: tuple[int, int, int]) -> None: + """Raise an error if the compiled version is less than required. Used to guard recently defined SDL functions.""" + if required <= _compiled_version(): + return + msg = f"This feature requires SDL version {required}, but tcod was compiled with version {_compiled_version()}" + raise RuntimeError(msg) + + +def _required_version(required: tuple[int, int, int]) -> Callable[[T], T]: + if not lib: # Read the docs mock object. + return lambda x: x + if required <= _compiled_version(): + return lambda x: x + + def replacement(*_args: Any, **_kwargs: Any) -> Any: + msg = f"This feature requires SDL version {required}, but tcod was compiled with version {_compiled_version()}" + raise RuntimeError(msg) + + return lambda x: replacement # type: ignore[return-value] diff --git a/tcod/sdl/audio.py b/tcod/sdl/audio.py index 88d9323a..f8261097 100644 --- a/tcod/sdl/audio.py +++ b/tcod/sdl/audio.py @@ -56,7 +56,7 @@ import tcod.sdl.sys from tcod.loader import ffi, lib -from tcod.sdl import _check, _get_error, _ProtectedContext +from tcod.sdl._internal import _check, _get_error, _ProtectedContext def _get_format(format: DTypeLike) -> int: diff --git a/tcod/sdl/joystick.py b/tcod/sdl/joystick.py index 5e2390ec..2a00d0cc 100644 --- a/tcod/sdl/joystick.py +++ b/tcod/sdl/joystick.py @@ -12,7 +12,7 @@ import tcod.sdl.sys from tcod.loader import ffi, lib -from tcod.sdl import _check, _check_p +from tcod.sdl._internal import _check, _check_p _HAT_DIRECTIONS: dict[int, tuple[Literal[-1, 0, 1], Literal[-1, 0, 1]]] = { lib.SDL_HAT_CENTERED or 0: (0, 0), diff --git a/tcod/sdl/mouse.py b/tcod/sdl/mouse.py index 0de8ddbc..5c7543a5 100644 --- a/tcod/sdl/mouse.py +++ b/tcod/sdl/mouse.py @@ -17,7 +17,7 @@ import tcod.event import tcod.sdl.video from tcod.loader import ffi, lib -from tcod.sdl import _check, _check_p +from tcod.sdl._internal import _check, _check_p class Cursor: diff --git a/tcod/sdl/render.py b/tcod/sdl/render.py index 65cc6d41..06962f93 100644 --- a/tcod/sdl/render.py +++ b/tcod/sdl/render.py @@ -13,7 +13,7 @@ import tcod.sdl.video from tcod.loader import ffi, lib -from tcod.sdl import _check, _check_p, _required_version +from tcod.sdl._internal import _check, _check_p, _required_version class TextureAccess(enum.IntEnum): diff --git a/tcod/sdl/sys.py b/tcod/sdl/sys.py index f2b65fb4..b0cbd08b 100644 --- a/tcod/sdl/sys.py +++ b/tcod/sdl/sys.py @@ -5,7 +5,7 @@ from typing import Any from tcod.loader import ffi, lib -from tcod.sdl import _check, _get_error +from tcod.sdl._internal import _check, _get_error class Subsystem(enum.IntFlag): diff --git a/tcod/sdl/video.py b/tcod/sdl/video.py index 0a4ad7a0..21af4ecc 100644 --- a/tcod/sdl/video.py +++ b/tcod/sdl/video.py @@ -16,7 +16,7 @@ from numpy.typing import ArrayLike, NDArray from tcod.loader import ffi, lib -from tcod.sdl import _check, _check_p, _required_version, _version_at_least +from tcod.sdl._internal import _check, _check_p, _required_version, _version_at_least __all__ = ( "WindowFlags",