diff --git a/.github/workflows/python-build.yml b/.github/workflows/python-build.yml index 0a3de40b..bd69796e 100644 --- a/.github/workflows/python-build.yml +++ b/.github/workflows/python-build.yml @@ -18,9 +18,9 @@ jobs: fail-fast: false matrix: cfg: - - {os: windows-latest, python-version: '3.10', architecture: x64} - - {os: macos-latest, python-version: '3.10', architecture: x64} - - {os: ubuntu-latest, python-version: '3.10', architecture: x64} + - {os: windows-latest, python-version: '3.12', architecture: x64} + - {os: macos-latest, python-version: '3.12', architecture: x64} + - {os: ubuntu-latest, python-version: '3.12', architecture: x64} steps: - uses: actions/checkout@v3 diff --git a/.github/workflows/python-stylecheck.yml b/.github/workflows/python-stylecheck.yml index b9f34cc0..659eeba6 100644 --- a/.github/workflows/python-stylecheck.yml +++ b/.github/workflows/python-stylecheck.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.10'] + python-version: ['3.12'] steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/python-unittests.yml b/.github/workflows/python-unittests.yml index ec355e88..b9b3b90a 100644 --- a/.github/workflows/python-unittests.yml +++ b/.github/workflows/python-unittests.yml @@ -16,8 +16,9 @@ jobs: unittests: runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: - python-version: ['3.10', '3.11'] + python-version: ['3.11', '3.12'] os: [windows-latest, macos-latest, ubuntu-latest] steps: - uses: actions/checkout@v2 diff --git a/.gitignore b/.gitignore index 1f016580..4c632a2d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,10 +6,6 @@ __pycache__/ # C extensions *.so -# Cython -*.c -*.html - # Distribution / packaging .Python /build/ @@ -116,6 +112,11 @@ venv.bak/ *.user *.pyproj *.sln +src/sln/ +*.exp +*.lib +*.pdb +*.ilk # Visual Studio Code extensions .qt_for_python diff --git a/MANIFEST.in b/MANIFEST.in index edca1482..5d747ea9 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,2 @@ -# Python files -recursive-include src/amulet_editor *.py *.pyi *.pyx *.pxd -# Images -recursive-include src/amulet_editor *.jpg *.png *.svg *.ico -# Misc -recursive-include src/amulet_editor *.mcmeta *.lang *.json *.qss +recursive-include src/amulet_editor *.pyi py.typed *.py *.cpp *.hpp *.jpg *.png *.svg *.ico *.lang *.json *.qss +recursive-include src/builtin_plugins *.pyi py.typed *.py *.cpp *.hpp *.jpg *.png *.svg *.ico *.lang *.json *.qss diff --git a/build_system/build/settings/base.json b/build_system/build/settings/base.json index e1e61dca..3671087d 100644 --- a/build_system/build/settings/base.json +++ b/build_system/build/settings/base.json @@ -11,8 +11,6 @@ "hidden_imports": [ "amulet", "amulet_nbt", - "PyMCTranslate", - "minecraft_model_reader", "PySide6", "pkg_resources", "OpenGL", diff --git a/pyproject.toml b/pyproject.toml index b497dc0a..28469164 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,8 +2,9 @@ requires = [ "setuptools >= 42", "wheel", - "cython >= 3.0.0a9", "versioneer", - "numpy ~= 1.17" + "pybind11 ~= 2.12", + "amulet_nbt ~= 4.0a2", + "amulet_core == 2.0a7" ] build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg index 3340fd61..d1335c94 100644 --- a/setup.cfg +++ b/setup.cfg @@ -12,26 +12,40 @@ long_description_content_type = text/markdown platforms = any [options] -include_package_data = True python_requires = >=3.11 install_requires = PySide6_Essentials~=6.5 - numpy~=1.17 + numpy~=2.0 pyopengl~=3.0 packaging - amulet_core~=2.0a0 - amulet_nbt~=3.0a2 - minecraft_resource_pack~=1.3 + amulet_nbt~=4.0a2 + amulet_core==2.0a7 amulet_runtime_final~=1.1 Pillow package_dir= =src -packages = find: +packages = find_namespace: [options.packages.find] where=src +[options.package_data] +* = + **/*.hpp + **/*.cpp + **/*.jpg + **/*.png + **/*.svg + **/*.ico + **/*.lang + **/*.json + **/*.qss + +[options.exclude_package_data] +* = + **/*.py.cpp + [options.extras_require] docs = @@ -42,9 +56,12 @@ dev = black>=22.3 pre_commit>=1.11.1 pylint>=2.12.2 + isort autoflake mypy types-pyinstaller + wheel + versioneer [options.entry_points] diff --git a/setup.py b/setup.py index 57fb804c..ec052565 100644 --- a/setup.py +++ b/setup.py @@ -1,168 +1,163 @@ -from typing import List, Tuple -from setuptools import setup -from wheel.bdist_wheel import bdist_wheel -from Cython.Build import cythonize import glob +import os import sys -import numpy -import subprocess -import logging + +# import subprocess +# import logging import re -import versioneer +import sysconfig +from setuptools import setup, Extension +from distutils import ccompiler +from distutils.sysconfig import get_python_inc +from wheel.bdist_wheel import bdist_wheel -first_party = { - "amulet_core", - "amulet_nbt", - "pymctranslate", - "minecraft_resource_pack", -} - - -def freeze_requirements(packages: List[str]) -> List[str]: - # Pip install the requirements to find the newest compatible versions - # This makes sure that the source versions are using the same dependencies as the compiled version. - # This also makes sure that the source version is using the newest version of the dependency. - if any( - "~=" in r and r.split("~=", 1)[0].lower().replace("-", "_") in first_party - for r in packages - ): - print("pip-install") - try: - # make sure pip is up to date - subprocess.run([sys.executable, "-m", "pip", "install", "--upgrade", "pip"]) - # run pip install - subprocess.run( - [sys.executable, "-m", "pip", "install", *packages, "--upgrade"] - ) - # run pip freeze - installed = ( - subprocess.check_output( - [sys.executable, "-m", "pip", "freeze"], encoding="utf-8" - ) - .strip() - .split("\n") - ) - requirements_map = { - r.split("==")[0].lower().replace("-", "_"): r for r in installed - } - - print(installed, requirements_map) - for index, requirement in enumerate(packages): - if "~=" in requirement: - lib = requirement.split("~=")[0].strip().lower().replace("-", "_") - if lib in first_party and lib in requirements_map: - packages[index] = requirements_map[lib] - print(f"Modified packages to {packages}") - except Exception as e: - print("Failed to bake versions:", e) - return packages - - -GCCPattern = re.compile(r"gcc version (?P\d+)\.(?P\d+)") -ClangPattern = re.compile(r"clang(?:-|\s+version\s+)(?P\d+)\.(?P\d+)") - - -def get_openmp_args() -> Tuple[List[str], List[str], List[str], List[str]]: - # This has been lifted from here https://github.com/cython/cython/blob/606bd8cf235149c3be6876d0f5ae60032c8aab6c/runtests.py - import sysconfig - from distutils import ccompiler - - def get_openmp_args_for(arg) -> Tuple[List[str], List[str]]: - """arg == 'CC' or 'CXX'""" - cc = ( - sysconfig.get_config_var(arg) or ccompiler.get_default_compiler() - ).split()[0] - if cc == "msvc": - # Microsoft Visual C - return ["/openmp"], [] - elif cc: - # Try GCC and Clang - try: - out = subprocess.check_output([cc, "-v"]).decode() - except ChildProcessError: - logging.exception(f"Could not resolve unknown compiler {cc}") - else: - gcc_match = GCCPattern.search(out) - if gcc_match: - if (gcc_match.group("major"), gcc_match.group("minor")) >= (4, 2): - return ["-fopenmp"], ["-fopenmp"] - return [], [] - clang_match = ClangPattern.search(out) - if clang_match: - # if (clang_match.group("major"), clang_match.group("minor")) >= (3, 7): - # return ['-fopenmp'], ['-fopenmp'] - return [], [] - # If all else fails disable openmp - return [], [] - - omp_ccargs, omp_clargs = get_openmp_args_for("CC") - omp_cppcargs, omp_cpplargs = get_openmp_args_for("CXX") - - return omp_ccargs, omp_clargs, omp_cppcargs, omp_cpplargs - - -# build cython extensions -if next(glob.iglob("src/**/*.pyx", recursive=True), None): - # This throws an error if it does not match any files - omp_ccargs, omp_clargs, omp_cppcargs, omp_cpplargs = get_openmp_args() - ext = cythonize( - "src/**/*.pyx", - aliases={ - "OPENMP_CCARGS": omp_ccargs, - "OPENMP_CLARGS": omp_clargs, - "OPENMP_CPPCARGS": omp_cppcargs, - "OPENMP_CPPLARGS": omp_cpplargs, - }, - ) -else: - ext = () +import versioneer +import pybind11 + +import amulet_nbt +import amulet + + +def get_compile_args() -> list[str]: + compiler = sysconfig.get_config_var("CXX") or ccompiler.get_default_compiler() + compile_args = [] + if compiler.split()[0] == "msvc": + compile_args.append("/std:c++20") + else: + compile_args.append("-std=c++20") + + if sys.platform == "darwin": + compile_args.append("-mmacosx-version-min=10.13") + return compile_args + + +CompileArgs = get_compile_args() + + +# def get_openmp_args() -> tuple[list[str], list[str], list[str], list[str]]: +# # This has been lifted from here https://github.com/cython/cython/blob/606bd8cf235149c3be6876d0f5ae60032c8aab6c/runtests.py +# import sysconfig +# from distutils import ccompiler +# GCCPattern = re.compile(r"gcc version (?P\d+)\.(?P\d+)") +# ClangPattern = re.compile(r"clang(?:-|\s+version\s+)(?P\d+)\.(?P\d+)") +# +# def get_openmp_args_for(arg: str) -> tuple[list[str], list[str]]: +# """arg == 'CC' or 'CXX'""" +# cc = ( +# sysconfig.get_config_var(arg) or ccompiler.get_default_compiler() +# ).split()[0] +# if cc == "msvc": +# # Microsoft Visual C +# return ["/openmp"], [] +# elif cc: +# # Try GCC and Clang +# try: +# out = subprocess.check_output([cc, "-v"]).decode() +# except ChildProcessError: +# logging.exception(f"Could not resolve unknown compiler {cc}") +# else: +# gcc_match = GCCPattern.search(out) +# if gcc_match: +# if (gcc_match.group("major"), gcc_match.group("minor")) >= (4, 2): +# return ["-fopenmp"], ["-fopenmp"] +# return [], [] +# clang_match = ClangPattern.search(out) +# if clang_match: +# # if (clang_match.group("major"), clang_match.group("minor")) >= (3, 7): +# # return ['-fopenmp'], ['-fopenmp'] +# return [], [] +# # If all else fails disable openmp +# return [], [] +# +# omp_ccargs, omp_clargs = get_openmp_args_for("CC") +# omp_cppcargs, omp_cpplargs = get_openmp_args_for("CXX") +# +# return omp_ccargs, omp_clargs, omp_cppcargs, omp_cpplargs cmdclass = versioneer.get_cmdclass() +BDistWheelOriginal: type[bdist_wheel] = cmdclass.get("bdist_wheel", bdist_wheel) -# There might be a better way of doing this -# The extra argument needs to be defined in the sdist -# so that it doesn't error. It doesn't actually use it. -class SDist(cmdclass["sdist"]): - user_options = cmdclass["sdist"].user_options + [ - ("freeze-first=", None, ""), - ] - - def initialize_options(self) -> None: - super().initialize_options() - self.freeze_first = None - - -class BDistWheel(bdist_wheel): - user_options = bdist_wheel.user_options + [ - ( - "freeze-first=", - None, - "Find and freeze the newest version of first party libraries. Only used internally.", - ), - ] - - def initialize_options(self) -> None: - super().initialize_options() - self.freeze_first = None - +class BDistWheel(BDistWheelOriginal): def finalize_options(self) -> None: - if self.freeze_first: - self.distribution.install_requires = freeze_requirements( - list(self.distribution.install_requires) - ) + # Freeze requirements so that the same version is installed as was compiled against. + frozen_requirements = { + "amulet_nbt": amulet_nbt.__version__, + "amulet_core": amulet.__version__, + } + install_requires = list(self.distribution.install_requires) + for i, requirement in enumerate(self.distribution.install_requires): + match = re.match(r"[a-zA-Z0-9_-]+", requirement) + if match is None: + continue + name = match.group().lower().replace("-", "_") + if name not in frozen_requirements: + continue + install_requires[i] = f"{name}=={frozen_requirements.pop(name)}" + + if frozen_requirements: + raise RuntimeError(f"{frozen_requirements} {install_requires}") + + self.distribution.install_requires = install_requires super().finalize_options() -cmdclass["sdist"] = SDist cmdclass["bdist_wheel"] = BDistWheel +AmuletNBTLib = ( + "amulet_nbt", + dict( + sources=glob.glob( + os.path.join(glob.escape(amulet_nbt.get_source()), "**", "*.cpp"), + recursive=True, + ), + include_dirs=[amulet_nbt.get_include()], + cflags=CompileArgs, + ), +) + +AmuletCoreLib = ( + "amulet_core", + dict( + sources=glob.glob( + os.path.join(glob.escape(amulet.__path__[0]), "**", "*.cpp"), + recursive=True, + ), + include_dirs=[ + get_python_inc(), + pybind11.get_include(), + amulet_nbt.get_include(), + os.path.dirname(amulet.__path__[0]), + ], + cflags=CompileArgs, + ), +) + + setup( version=versioneer.get_version(), cmdclass=cmdclass, - include_dirs=[numpy.get_include()], - ext_modules=ext, + libraries=[AmuletNBTLib, AmuletCoreLib], + ext_modules=[ + Extension( + name="builtin_plugins.amulet_team_3d_viewer._view_3d._chunk_builder", + sources=[ + "src/builtin_plugins/amulet_team_3d_viewer/_view_3d/_chunk_builder.cpp", + "src/builtin_plugins/amulet_team_3d_viewer/_view_3d/_chunk_builder.py.cpp", + ], + include_dirs=[ + pybind11.get_include(), + amulet_nbt.get_include(), + os.path.dirname(amulet.__path__[0]), + "src", + "src/builtin_plugins", + ], + libraries=["amulet_nbt", "amulet_core"], + define_macros=[("PYBIND11_DETAILED_ERROR_MESSAGES", None)], + extra_compile_args=CompileArgs, + ), + ], ) diff --git a/src/amulet_editor/__pyinstaller/hook-amulet_editor.py b/src/amulet_editor/__pyinstaller/hook-amulet_editor.py index 1aea0606..1f086198 100644 --- a/src/amulet_editor/__pyinstaller/hook-amulet_editor.py +++ b/src/amulet_editor/__pyinstaller/hook-amulet_editor.py @@ -5,11 +5,13 @@ ) datas = [ - *collect_data_files("amulet_editor", excludes=["**/*.ui", "**/*.c", "**/*.pyx"]), + *collect_data_files( + "amulet_editor", excludes=["**/*.ui", "**/*.py.cpp", "**/*.pyc"] + ), *collect_data_files( "builtin_plugins", include_py_files=True, - excludes=["**/*.ui", "**/*.c", "**/*.pyx", "**/*.pyc"], + excludes=["**/*.ui", "**/*.py.cpp", "**/*.pyc"], ), *copy_metadata("amulet_editor", recursive=True), ] diff --git a/src_/builtin_plugins_/amulet_team_3d_viewer/_resources/lang/en.lang b/src/amulet_editor/py.typed similarity index 100% rename from src_/builtin_plugins_/amulet_team_3d_viewer/_resources/lang/en.lang rename to src/amulet_editor/py.typed diff --git a/src_/builtin_plugins_/amulet_team_3d_viewer/__init__.py b/src/builtin_plugins/amulet_team_3d_viewer/__init__.py similarity index 100% rename from src_/builtin_plugins_/amulet_team_3d_viewer/__init__.py rename to src/builtin_plugins/amulet_team_3d_viewer/__init__.py diff --git a/src_/builtin_plugins_/amulet_team_3d_viewer/_plugin.py b/src/builtin_plugins/amulet_team_3d_viewer/_plugin.py similarity index 59% rename from src_/builtin_plugins_/amulet_team_3d_viewer/_plugin.py rename to src/builtin_plugins/amulet_team_3d_viewer/_plugin.py index da451ff8..ff26b93d 100644 --- a/src_/builtin_plugins_/amulet_team_3d_viewer/_plugin.py +++ b/src/builtin_plugins/amulet_team_3d_viewer/_plugin.py @@ -1,6 +1,7 @@ from __future__ import annotations import os from typing import Optional +from contextlib import suppress from PySide6.QtCore import QLocale, QCoreApplication @@ -9,8 +10,7 @@ from amulet_editor.models.plugin import PluginV1 import amulet_team_locale -import amulet_team_main_window -import tablericons +from amulet_team_main_window import register_widget, unregister_widget import amulet_team_3d_viewer from ._view_3d import View3D @@ -18,29 +18,20 @@ # Qt only weekly references this. We must hold a strong reference to stop it getting garbage collected _translator: Optional[ATranslator] = None -view_3d_button: Optional[amulet_team_main_window.ButtonProxy] = None -def _set_view_3d_layout(): - amulet_team_main_window.get_main_window().set_layout(View3D) - - -def load_plugin(): - global _translator, view_3d_button +def load_plugin() -> None: + global _translator if get_level() is not None: _translator = ATranslator() _locale_changed() QCoreApplication.installTranslator(_translator) amulet_team_locale.locale_changed.connect(_locale_changed) - amulet_team_main_window.register_widget(View3D) - view_3d_button = amulet_team_main_window.add_toolbar_button(sticky=True) - view_3d_button.set_icon(tablericons.three_d_cube_sphere) - view_3d_button.set_name("3D Editor") - view_3d_button.set_callback(_set_view_3d_layout) + register_widget(View3D) -def _locale_changed(): +def _locale_changed() -> None: assert _translator is not None _translator.load_lang( QLocale(), @@ -49,8 +40,11 @@ def _locale_changed(): ) -def unload_plugin(): - QCoreApplication.removeTranslator(_translator) +def unload_plugin() -> None: + with suppress(ValueError): + unregister_widget(View3D) + if _translator is not None: + QCoreApplication.removeTranslator(_translator) plugin = PluginV1(load=load_plugin, unload=unload_plugin) diff --git a/src_/builtin_plugins_/amulet_team_selection/__init__.py b/src/builtin_plugins/amulet_team_3d_viewer/_resources/lang/en.lang similarity index 100% rename from src_/builtin_plugins_/amulet_team_selection/__init__.py rename to src/builtin_plugins/amulet_team_3d_viewer/_resources/lang/en.lang diff --git a/src_/builtin_plugins_/amulet_team_3d_viewer/_view_3d/__init__.py b/src/builtin_plugins/amulet_team_3d_viewer/_view_3d/__init__.py similarity index 100% rename from src_/builtin_plugins_/amulet_team_3d_viewer/_view_3d/__init__.py rename to src/builtin_plugins/amulet_team_3d_viewer/_view_3d/__init__.py diff --git a/src_/builtin_plugins_/amulet_team_3d_viewer/_view_3d/_camera.py b/src/builtin_plugins/amulet_team_3d_viewer/_view_3d/_camera.py similarity index 100% rename from src_/builtin_plugins_/amulet_team_3d_viewer/_view_3d/_camera.py rename to src/builtin_plugins/amulet_team_3d_viewer/_view_3d/_camera.py diff --git a/src/builtin_plugins/amulet_team_3d_viewer/_view_3d/_chunk_builder.cpp b/src/builtin_plugins/amulet_team_3d_viewer/_view_3d/_chunk_builder.cpp new file mode 100644 index 00000000..e69de29b diff --git a/src/builtin_plugins/amulet_team_3d_viewer/_view_3d/_chunk_builder.hpp b/src/builtin_plugins/amulet_team_3d_viewer/_view_3d/_chunk_builder.hpp new file mode 100644 index 00000000..6f70f09b --- /dev/null +++ b/src/builtin_plugins/amulet_team_3d_viewer/_view_3d/_chunk_builder.hpp @@ -0,0 +1 @@ +#pragma once diff --git a/src/builtin_plugins/amulet_team_3d_viewer/_view_3d/_chunk_builder.py.cpp b/src/builtin_plugins/amulet_team_3d_viewer/_view_3d/_chunk_builder.py.cpp new file mode 100644 index 00000000..d95c2b7d --- /dev/null +++ b/src/builtin_plugins/amulet_team_3d_viewer/_view_3d/_chunk_builder.py.cpp @@ -0,0 +1,26 @@ +#include +#include + +#include + +#include +#include +#include +#include + +namespace py = pybind11; + + +PYBIND11_MODULE(_chunk_builder, m) { + py::module::import("amulet.palette.block_palette"); + m.def( + "create_lod0_chunk", + []( + Amulet::pybind11::type_hints::PyObjectStr<"amulet_team_3d_viewer._view_32._resource_pack"> resource_pack, + std::shared_ptr arr, + std::shared_ptr palette + ) { + throw std::runtime_error("NotImplemented"); + } + ); +} diff --git a/src_/builtin_plugins_/amulet_team_3d_viewer/_view_3d/_drawable.py b/src/builtin_plugins/amulet_team_3d_viewer/_view_3d/_drawable.py similarity index 100% rename from src_/builtin_plugins_/amulet_team_3d_viewer/_view_3d/_drawable.py rename to src/builtin_plugins/amulet_team_3d_viewer/_view_3d/_drawable.py diff --git a/src_/builtin_plugins_/amulet_team_3d_viewer/_view_3d/_key_catcher.py b/src/builtin_plugins/amulet_team_3d_viewer/_view_3d/_key_catcher.py similarity index 93% rename from src_/builtin_plugins_/amulet_team_3d_viewer/_view_3d/_key_catcher.py rename to src/builtin_plugins/amulet_team_3d_viewer/_view_3d/_key_catcher.py index fa59b83a..e5358dbd 100644 --- a/src_/builtin_plugins_/amulet_team_3d_viewer/_view_3d/_key_catcher.py +++ b/src/builtin_plugins/amulet_team_3d_viewer/_view_3d/_key_catcher.py @@ -27,11 +27,11 @@ class KeySrc(IntEnum): ] ModifierT = frozenset[KeyT] Number = Union[float, int] -ReceiverT = Union[Slot, Signal, Callable[[], None], WeakMethod] +ReceiverType = Union[Slot, Signal, Callable[[float], None], WeakMethod] class TimerData(QObject): - receivers: set[ReceiverT] + event_receivers: set[ReceiverType] interval: float _timer: QTimer @@ -39,9 +39,9 @@ class TimerData(QObject): delta_timeout = Signal(float) - def __init__(self, msec: int): + def __init__(self, msec: int) -> None: super().__init__() - self.receivers = set() + self.event_receivers = set() self.interval = msec / 1000 self._last_timeout = 0.0 self._timer = QTimer() @@ -63,7 +63,7 @@ def stop(self) -> None: class EventStorage(QObject): one_shot = Signal() - def __init__(self, key: KeyT, modifiers: frozenset[KeyT]): + def __init__(self, key: KeyT, modifiers: frozenset[KeyT]) -> None: super().__init__() self.key = key self.modifiers = modifiers @@ -132,7 +132,7 @@ def _key_pressed(self, key: KeyT) -> None: for timer_data in storage.timers.values(): timer_data.stop() for interval, timer_data in list(best_storage.timers.items()): - if timer_data.receivers: + if timer_data.event_receivers: timer_data.delta_timeout.emit(interval // 1000) timer_data.start() else: @@ -163,7 +163,7 @@ def _clean_storage(self, key: KeyT, modifiers: frozenset[KeyT]) -> None: storage = self._get_storage(key, modifiers) # Remove all unused timers. for interval in list(storage.timers.keys()): - if not storage.timers[interval].receivers: + if not storage.timers[interval].event_receivers: del storage.timers[interval] # Remove storage if there are no bound receivers if not storage.bound_one_shot and not storage.timers: @@ -234,9 +234,9 @@ def connect_repeating( timer_data.delta_timeout.connect(receiver) if ismethod(receiver): # If this class strongly stores references to methods they can't get garbage collected - timer_data.receivers.add(WeakMethod(receiver)) + timer_data.event_receivers.add(WeakMethod(receiver)) else: - timer_data.receivers.add(receiver) + timer_data.event_receivers.add(receiver) def disconnect_repeating( self, @@ -260,13 +260,13 @@ def disconnect_repeating( if timer_data is not None: timer_data.delta_timeout.disconnect(receiver) if ismethod(receiver): - timer_data.receivers.remove(WeakMethod(receiver)) + timer_data.event_receivers.remove(WeakMethod(receiver)) else: - timer_data.receivers.remove(receiver) + timer_data.event_receivers.remove(receiver) self._clean_storage(key, modifiers) -def _demo(): +def _demo() -> None: from PySide6.QtWidgets import ( QApplication, QLabel, @@ -296,22 +296,22 @@ def _demo(): b = 0 c = 0 - def update_a(): + def update_a(dt: float) -> None: nonlocal a a += 1 label_a.setText(str(a)) - def update_b(): + def update_b(dt: float) -> None: nonlocal b b += 1 label_b.setText(str(b)) - def update_c(): + def update_c() -> None: nonlocal c c += 1 label_c.setText(str(c)) - def connect(): + def connect() -> None: key_catcher.connect_repeating( update_a, (KeySrc.Keyboard, Qt.Key.Key_A), frozenset(), 100 ) @@ -324,7 +324,7 @@ def connect(): connect() - def disconnect(): + def disconnect() -> None: key_catcher.disconnect_repeating( update_a, (KeySrc.Keyboard, Qt.Key.Key_A), frozenset(), 100 ) diff --git a/src_/builtin_plugins_/amulet_team_3d_viewer/_view_3d/_level_geometry.py b/src/builtin_plugins/amulet_team_3d_viewer/_view_3d/_level_geometry.py similarity index 77% rename from src_/builtin_plugins_/amulet_team_3d_viewer/_view_3d/_level_geometry.py rename to src/builtin_plugins/amulet_team_3d_viewer/_view_3d/_level_geometry.py index 1eb7d0c6..9b3fa5ce 100644 --- a/src_/builtin_plugins_/amulet_team_3d_viewer/_view_3d/_level_geometry.py +++ b/src/builtin_plugins/amulet_team_3d_viewer/_view_3d/_level_geometry.py @@ -1,15 +1,14 @@ from __future__ import annotations import logging -from typing import Optional, Generator, Callable, Iterator +from typing import Optional, Generator, Callable, Iterator, Any, TypeVar import ctypes from threading import Lock -from weakref import WeakKeyDictionary, WeakValueDictionary, ref, proxy +from weakref import WeakKeyDictionary, WeakValueDictionary, ref from collections.abc import MutableMapping import bisect -from contextlib import contextmanager import numpy -from PySide6.QtCore import QThread, Signal, QObject, Slot, QTimer, QCoreApplication, Qt +from PySide6.QtCore import QThread, Signal, QObject, Slot, QTimer, QCoreApplication from PySide6.QtGui import QMatrix4x4, QOpenGLContext, QOffscreenSurface from PySide6.QtOpenGL import ( QOpenGLVertexArrayObject, @@ -19,23 +18,25 @@ QOpenGLTexture, ) from shiboken6 import VoidPtr, isValid +from OpenGL.constant import IntConstant from OpenGL.GL import ( - GL_FLOAT, - GL_FALSE, - GL_TRIANGLES, - GL_CULL_FACE, - GL_BACK, - GL_DEPTH_TEST, - GL_LEQUAL, - GL_BLEND, - GL_SRC_ALPHA, - GL_ONE_MINUS_SRC_ALPHA, + GL_FLOAT as _GL_FLOAT, + GL_FALSE as _GL_FALSE, + GL_TRIANGLES as _GL_TRIANGLES, + GL_CULL_FACE as _GL_CULL_FACE, + GL_BACK as _GL_BACK, + GL_DEPTH_TEST as _GL_DEPTH_TEST, + GL_LEQUAL as _GL_LEQUAL, + GL_BLEND as _GL_BLEND, + GL_SRC_ALPHA as _GL_SRC_ALPHA, + GL_ONE_MINUS_SRC_ALPHA as _GL_ONE_MINUS_SRC_ALPHA, ) from amulet.data_types import DimensionId from amulet.level.abc import Level from amulet.errors import ChunkLoadError, ChunkDoesNotExist from amulet.chunk import Chunk +from amulet.chunk_components import BlockComponent import thread_manager @@ -47,12 +48,28 @@ from amulet_editor.application._invoke import invoke from amulet_editor.models.widgets.traceback_dialog import CatchException -try: - from .chunk_builder_cy import create_lod0_chunk -except: - raise ImportError( - "Could not import cython chunk mesher. The cython code must be compiled first." - ) +from ._chunk_builder import create_lod0_chunk + +T = TypeVar("T") + + +def dynamic_cast(obj: Any, new_type: type[T]) -> T: + if not isinstance(obj, new_type): + raise TypeError(f"{obj} is not an instance of {new_type}") + return obj + + +# This should really be typed better in PyOpenGL +GL_FLOAT = dynamic_cast(_GL_FLOAT, IntConstant) +GL_FALSE = dynamic_cast(_GL_FALSE, IntConstant) +GL_TRIANGLES = dynamic_cast(_GL_TRIANGLES, IntConstant) +GL_CULL_FACE = dynamic_cast(_GL_CULL_FACE, IntConstant) +GL_BACK = dynamic_cast(_GL_BACK, IntConstant) +GL_DEPTH_TEST = dynamic_cast(_GL_DEPTH_TEST, IntConstant) +GL_LEQUAL = dynamic_cast(_GL_LEQUAL, IntConstant) +GL_BLEND = dynamic_cast(_GL_BLEND, IntConstant) +GL_SRC_ALPHA = dynamic_cast(_GL_SRC_ALPHA, IntConstant) +GL_ONE_MINUS_SRC_ALPHA = dynamic_cast(_GL_ONE_MINUS_SRC_ALPHA, IntConstant) FloatSize = ctypes.sizeof(ctypes.c_float) @@ -76,9 +93,12 @@ class SharedVBOManager(QObject): _surface: QOffscreenSurface _vbos: set[QOpenGLBuffer] - def __init__(self): + def __init__(self) -> None: """Use new class method""" - if QThread.currentThread() is not QCoreApplication.instance().thread(): + app_instance = QCoreApplication.instance() + if app_instance is None: + raise RuntimeError("Qt App does not exist.") + if QThread.currentThread() is not app_instance.thread(): raise RuntimeError( "SharedVBOManager must be constructed on the main thread." ) @@ -98,7 +118,7 @@ def __init__(self): lock = self._lock = Lock() vbos = self._vbos = set() - def destroy(): + def destroy() -> None: with CatchException(), lock: if not context.makeCurrent(surface): raise ContextException("Could not make context current.") @@ -117,7 +137,7 @@ def create_vbo(self, buffer: bytes) -> QOpenGLBuffer: There will be no active OpenGL context in the main thread when this is finished. """ - def create_vbo(): + def create_vbo() -> QOpenGLBuffer: with self._lock: if not self._context.makeCurrent(self._surface): raise ContextException("Could not make context current.") @@ -134,14 +154,14 @@ def create_vbo(): return invoke(create_vbo) - def destroy_vbo(self, vbo: QOpenGLBuffer): + def destroy_vbo(self, vbo: QOpenGLBuffer) -> None: """ Destroy a shared VBO. There will be no active OpenGL context in the main thread when this is finished. """ log.debug("destroy_vbo") - def destroy_vbo(): + def destroy_vbo() -> None: with self._lock: if vbo not in self._vbos: raise RuntimeError( @@ -198,7 +218,7 @@ class SharedChunkData(QObject): # Emitted when the geometry has been modified. geometry_changed = Signal() - def __init__(self, model_transform: QMatrix4x4): + def __init__(self, model_transform: QMatrix4x4) -> None: super().__init__() self.model_transform: QMatrix4x4 = model_transform self.geometry = None @@ -210,7 +230,7 @@ class ChunkGeneratorWorker(QObject): The OpenGL calls need to be done on the main thread. """ - def __init__(self): + def __init__(self) -> None: super().__init__() self.generate_chunk.connect(self._generate_chunk) @@ -223,7 +243,7 @@ def _generate_chunk( vbo_manager: SharedVBOManager, chunk_key: ChunkKey, chunk_data: SharedChunkData, - ): + ) -> None: with CatchException(): resource_pack_container = get_gl_resource_pack_container(level) if not resource_pack_container.loaded: @@ -242,7 +262,8 @@ def _generate_chunk( buffer = b"" try: - chunk = level.get_chunk(cx, cz, dimension) + chunk_handle = level.get_dimension(dimension).get_chunk_handle(cx, cz) + chunk = chunk_handle.get([BlockComponent.ComponentID]) except ChunkDoesNotExist: # TODO: Add void geometry pass @@ -271,7 +292,7 @@ def _generate_chunk( chunk_data.geometry = SharedChunkGeometry(vbo, vertex_count, resource_pack) # When the container gets garbage collected, destroy the vbo - def destroy_vbo(): + def destroy_vbo() -> None: with CatchException(): vbo_manager.destroy_vbo(vbo) @@ -292,19 +313,23 @@ def _sub_chunks( :param chunk: The chunk object :return: A list of tuples containing the larger block array and the location of the sub-chunk """ - blocks = chunk.blocks + if not isinstance(chunk, BlockComponent): + return [] + sections = chunk.block.sections sub_chunks = [] neighbour_chunks = {} for dx, dz in ((-1, 0), (1, 0), (0, -1), (0, 1)): try: - neighbour_chunks[(dx, dz)] = level.get_chunk( - cx + dx, cz + dz, dimension - ).blocks + chunk_handle = level.get_dimension(dimension).get_chunk_handle( + cx + dx, cz + dz + ) + chunk = chunk_handle.get([BlockComponent.ComponentID]) + if isinstance(chunk, BlockComponent): + neighbour_chunks[(dx, dz)] = chunk.block except ChunkLoadError: continue - for cy in blocks.sub_chunks: - sub_chunk = blocks.get_sub_chunk(cy) + for cy, sub_chunk in sections.items(): larger_blocks = numpy.zeros( sub_chunk.shape + numpy.array((2, 2, 2)), sub_chunk.dtype ) @@ -324,28 +349,21 @@ def _sub_chunks( # larger_blocks[1:-1, 1:-1, 1:-1] = sub_chunk larger_blocks[1:-1, 1:-1, 1:-1] = sub_chunk for chunk_offset, neighbour_blocks in neighbour_chunks.items(): - if cy not in neighbour_blocks: + neighbour_sections = neighbour_blocks.sections + if cy not in neighbour_sections: continue if chunk_offset == (-1, 0): - larger_blocks[0, 1:-1, 1:-1] = neighbour_blocks.get_sub_chunk(cy)[ - -1, :, : - ] + larger_blocks[0, 1:-1, 1:-1] = neighbour_sections[cy][-1, :, :] elif chunk_offset == (1, 0): - larger_blocks[-1, 1:-1, 1:-1] = neighbour_blocks.get_sub_chunk(cy)[ - 0, :, : - ] + larger_blocks[-1, 1:-1, 1:-1] = neighbour_sections[cy][0, :, :] elif chunk_offset == (0, -1): - larger_blocks[1:-1, 1:-1, 0] = neighbour_blocks.get_sub_chunk(cy)[ - :, :, -1 - ] + larger_blocks[1:-1, 1:-1, 0] = neighbour_sections[cy][:, :, -1] elif chunk_offset == (0, 1): - larger_blocks[1:-1, 1:-1, -1] = neighbour_blocks.get_sub_chunk(cy)[ - :, :, 0 - ] - if cy - 1 in blocks: - larger_blocks[1:-1, 0, 1:-1] = blocks.get_sub_chunk(cy - 1)[:, -1, :] - if cy + 1 in blocks: - larger_blocks[1:-1, -1, 1:-1] = blocks.get_sub_chunk(cy + 1)[:, 0, :] + larger_blocks[1:-1, 1:-1, -1] = neighbour_sections[cy][:, :, 0] + if cy - 1 in sections: + larger_blocks[1:-1, 0, 1:-1] = sections[cy - 1][:, -1, :] + if cy + 1 in sections: + larger_blocks[1:-1, -1, 1:-1] = sections[cy + 1][:, 0, :] sub_chunks.append((larger_blocks, cy * 16)) return sub_chunks @@ -354,21 +372,24 @@ class ChunkGenerator(QObject): _thread: QThread _worker: ChunkGeneratorWorker - def __init__(self): + def __init__(self) -> None: super().__init__() thread = self._thread = thread_manager.new_thread("ChunkGeneratorThread") self._thread.start() worker = self._worker = ChunkGeneratorWorker() self._worker.moveToThread(self._thread) - def destroy(): + def destroy() -> None: with CatchException(): worker.deleteLater() log.debug("Quitting chunk generation thread.") thread.quit() self.destroyed.connect(destroy) - QCoreApplication.instance().aboutToQuit.connect(self.deleteLater) + app_instance = QCoreApplication.instance() + if app_instance is None: + raise RuntimeError("Qt App is None") + app_instance.aboutToQuit.connect(self.deleteLater) def generate_chunk( self, @@ -376,7 +397,7 @@ def generate_chunk( vbo_manager: SharedVBOManager, chunk_key: ChunkKey, chunk: SharedChunkData, - ): + ) -> None: """ Async function to generate the chunk VBO. @@ -397,7 +418,7 @@ class SharedLevelGeometry(QObject): # Class variables _instances_lock = Lock() - _instances: WeakKeyDictionary[Level, SharedLevelGeometry] = {} + _instances = WeakKeyDictionary[Level, "SharedLevelGeometry"]() # Instance variables _level: Callable[[], Optional[Level]] @@ -415,7 +436,7 @@ def instance(cls, level: Level) -> SharedLevelGeometry: cls._instances[level] = invoke(lambda: SharedLevelGeometry(level)) return cls._instances[level] - def __init__(self, level: Level): + def __init__(self, level: Level) -> None: """To get an instance of this class you should use :classmethod:`instance`""" super().__init__() self._level = ref(level) @@ -428,7 +449,16 @@ def __init__(self, level: Level): self._resource_pack_container = get_gl_resource_pack_container(level) self._resource_pack_container.changed.connect(self._resource_pack_changed) - QCoreApplication.instance().aboutToQuit.connect(self.deleteLater) + app_instance = QCoreApplication.instance() + if app_instance is None: + raise RuntimeError("Qt App is None") + app_instance.aboutToQuit.connect(self.deleteLater) + + def _get_level(self) -> Level: + level = self._level() + if level is None: + raise RuntimeError("Level does not exist.") + return level def get_chunk(self, chunk_key: ChunkKey) -> SharedChunkData: """Get the geometry for a chunk.""" @@ -440,16 +470,16 @@ def get_chunk(self, chunk_key: ChunkKey) -> SharedChunkData: transform.translate(cx * 16, 0, cz * 16) chunk = self._chunks[chunk_key] = SharedChunkData(transform) self._chunk_generator.generate_chunk( - self._level(), self._vbo_manager, chunk_key, chunk + self._get_level(), self._vbo_manager, chunk_key, chunk ) return chunk - def _resource_pack_changed(self): + def _resource_pack_changed(self) -> None: # The geometry of all loaded chunks needs to be rebuilt. with CatchException(), self._chunks_lock: for chunk_key, chunk in self._chunks.items(): self._chunk_generator.generate_chunk( - self._level(), self._vbo_manager, chunk_key, chunk + self._get_level(), self._vbo_manager, chunk_key, chunk ) @@ -460,19 +490,19 @@ class WidgetChunkData(QObject): geometry_changed = Signal() - def __init__(self, shared: SharedChunkData): + def __init__(self, shared: SharedChunkData) -> None: super().__init__() self.shared = shared self.geometry = None self.vao = None self.shared.geometry_changed.connect(self.geometry_changed) - def __del__(self): + def __del__(self) -> None: if self.vao is not None: log.warning("VAO has not been destroyed.") -def empty_iterator(): +def empty_iterator() -> Iterator[ChunkKey]: yield from () @@ -494,24 +524,24 @@ def get_grid_spiral( class ChunkContainer(MutableMapping[ChunkKey, WidgetChunkData]): - def __init__(self): + def __init__(self) -> None: self._chunks: dict[ChunkKey, WidgetChunkData] = {} self._order: list[ChunkKey] = [] - self._x = 0 - self._z = 0 + self._x: int = 0 + self._z: int = 0 - def set_position(self, cx, cz): + def set_position(self, cx: int, cz: int) -> None: self._x = cx self._z = cz self._order = sorted(self._order, key=self._dist) - def __contains__(self, k: ChunkKey): + def __contains__(self, k: ChunkKey | Any) -> bool: return k in self._chunks - def _dist(self, k: ChunkKey): + def _dist(self, k: ChunkKey) -> int: return -abs(k[1] - self._x) - abs(k[2] - self._z) - def __setitem__(self, k: ChunkKey, v: WidgetChunkData): + def __setitem__(self, k: ChunkKey, v: WidgetChunkData) -> None: if k not in self._chunks: self._order.insert( bisect.bisect_left(self._order, self._dist(k), key=self._dist), k @@ -532,6 +562,25 @@ def __iter__(self) -> Iterator[ChunkKey]: yield from self._order +class LevelGeometryGLData: + context: QOpenGLContext + program: QOpenGLShaderProgram + matrix_location: int + texture_location: int + + def __init__( + self, + context: QOpenGLContext, + program: QOpenGLShaderProgram, + matrix_location: int, + texture_location: int, + ): + self.context = context + self.program = program + self.matrix_location = matrix_location + self.texture_location = texture_location + + class WidgetLevelGeometry(QObject, Drawable): """ A class holding the level geometry data relating to one widget. @@ -547,24 +596,19 @@ class WidgetLevelGeometry(QObject, Drawable): _unload_radius: int _dimension: Optional[DimensionId] _camera_chunk: Optional[tuple[int, int]] - _chunk_finder: Generator[ChunkKey, None, None] + _chunk_finder: Iterator[ChunkKey] _generation_count: int # OpenGL attributes - _context: Optional[ - QOpenGLContext - ] # The presence of a context dictates that the state is active _surface: QOffscreenSurface - _program: Optional[QOpenGLShaderProgram] - _matrix_location: Optional[int] - _texture_location: Optional[int] + _gl_data_: Optional[LevelGeometryGLData] _chunks: ChunkContainer _pending_chunks: dict[ChunkKey, WidgetChunkData] # The geometry has changed and needs repainting. geometry_changed = Signal() - def __init__(self, level: BaseLevel): + def __init__(self, level: Level) -> None: super().__init__() self._shared = SharedLevelGeometry.instance(level) @@ -575,17 +619,20 @@ def __init__(self, level: BaseLevel): self._chunk_finder = empty_iterator() self._generation_count = 0 - self._context = None + self._gl_data_ = None self._surface = QOffscreenSurface() self._surface.create() - self._program = None - self._matrix_location = None - self._texture_location = None self._chunks = ChunkContainer() self._pending_chunks = {} self._queue_chunk.connect(self._process_chunk) + @property + def _gl_data(self) -> LevelGeometryGLData: + if self._gl_data_ is None: + raise RuntimeError("GL state has not been initialised.") + return self._gl_data_ + def initializeGL(self) -> None: """ Initialise the opengl state. @@ -598,14 +645,14 @@ def initializeGL(self) -> None: "The widget context is not sharing with the global context." ) - if self._context is not None: + if self._gl_data_ is not None: raise RuntimeError( "This has been initialised before without being destroyed." ) # Initialise the shader - self._program = QOpenGLShaderProgram() - self._program.addShaderFromSourceCode( + program = QOpenGLShaderProgram() + program.addShaderFromSourceCode( QOpenGLShader.ShaderTypeBit.Vertex, """#version 150 in vec3 position; @@ -627,7 +674,7 @@ def initializeGL(self) -> None: }""", ) - self._program.addShaderFromSourceCode( + program.addShaderFromSourceCode( QOpenGLShader.ShaderTypeBit.Fragment, """#version 150 in vec2 fTexCoord; @@ -653,21 +700,23 @@ def initializeGL(self) -> None: }""", ) - self._program.bindAttributeLocation("position", 0) - self._program.bindAttributeLocation("vTexCoord", 1) - self._program.bindAttributeLocation("vTexOffset", 2) - self._program.bindAttributeLocation("vTint", 3) - self._program.link() - self._program.bind() - self._matrix_location = self._program.uniformLocation("transformation_matrix") - self._texture_location = self._program.uniformLocation("image") - self._program.release() - - self._context = context + program.bindAttributeLocation("position", 0) + program.bindAttributeLocation("vTexCoord", 1) + program.bindAttributeLocation("vTexOffset", 2) + program.bindAttributeLocation("vTint", 3) + program.link() + program.bind() + matrix_location = program.uniformLocation("transformation_matrix") + texture_location = program.uniformLocation("image") + program.release() + + self._gl_data_ = LevelGeometryGLData( + context, program, matrix_location, texture_location + ) self._init_geometry_no_context() self._update_chunk_finder() - def destroyGL(self): + def destroyGL(self) -> None: """ Destroy all the data associated with this context. Once finished the context may be destroyed. @@ -675,13 +724,10 @@ def destroyGL(self): The caller must activate the context. """ with CatchException(): - self._context = None - self._program = None - self._matrix_location = None - self._texture_location = None + self._gl_data_ = None self._destroy_geometry_no_context() - def __del__(self): + def __del__(self) -> None: log.debug("__del__ WidgetLevelGeometry") def paintGL(self, projection_matrix: QMatrix4x4, view_matrix: QMatrix4x4) -> None: @@ -692,9 +738,10 @@ def paintGL(self, projection_matrix: QMatrix4x4, view_matrix: QMatrix4x4) -> Non :param projection_matrix: The camera internal projection matrix. :param view_matrix: The camera external matrix. """ - if self._context is None: + gl_data = self._gl_data_ + if gl_data is None: return - if QOpenGLContext.currentContext() is not self._context: + if QOpenGLContext.currentContext() is not gl_data.context: raise ContextException("Context is not valid") f = QOpenGLContext.currentContext().functions() @@ -706,24 +753,28 @@ def paintGL(self, projection_matrix: QMatrix4x4, view_matrix: QMatrix4x4) -> Non f.glCullFace(GL_BACK) # Draw the geometry - self._program.bind() + gl_data.program.bind() # Init the texture - self._program.setUniformValue1i(self._texture_location, 0) + gl_data.program.setUniformValue1i(gl_data.texture_location, 0) transform = projection_matrix * view_matrix for chunk_data in self._chunks.values(): - self._program.setUniformValue( - self._matrix_location, transform * chunk_data.shared.model_transform + gl_data.program.setUniformValue( + gl_data.matrix_location, transform * chunk_data.shared.model_transform ) - chunk_data.geometry.texture.bind(0) - chunk_data.vao.bind() - f.glDrawArrays(GL_TRIANGLES, 0, chunk_data.geometry.vertex_count) - chunk_data.vao.release() - - self._program.release() - - def _update_chunk_finder(self): + geometry = chunk_data.geometry + vao = chunk_data.vao + if geometry is None or vao is None: + raise RuntimeError + geometry.texture.bind(0) + vao.bind() + f.glDrawArrays(GL_TRIANGLES, 0, geometry.vertex_count) + vao.release() + + gl_data.program.release() + + def _update_chunk_finder(self) -> None: if self._dimension is None or self._camera_chunk is None: empty_iterator() else: @@ -733,7 +784,7 @@ def _update_chunk_finder(self): ) self._queue_next_chunk() - def _init_geometry_no_context(self): + def _init_geometry_no_context(self) -> None: """ Initialise the OpenGL data for all existing chunk objects. This is the opposite of _destroy_vao_no_context @@ -744,7 +795,7 @@ def _init_geometry_no_context(self): # Most of the chunks are not valid yet and errors will occur if we try and draw them. self._create_vao(chunk, False) - def _destroy_geometry_no_context(self): + def _destroy_geometry_no_context(self) -> None: """ Destroy all Vertex Array Objects. The VBOs are defined in the shared context so do not need to be destroyed here. @@ -765,7 +816,7 @@ def _destroy_geometry_no_context(self): chunk.vao = None log.debug("cleared VAOs") - def _clear_chunks_no_context(self): + def _clear_chunks_no_context(self) -> None: """ Clears all geometry data. The context must be current before calling this. @@ -774,24 +825,26 @@ def _clear_chunks_no_context(self): self._chunks.clear() self._pending_chunks.clear() - def _clear_chunks(self): + def _clear_chunks(self) -> None: """Unload all chunk data. It is the job of the caller to handle the chunk generator.""" - if self._context is None: + gl_data = self._gl_data_ + if gl_data is None: return - if not self._context.makeCurrent(self._surface): + if not gl_data.context.makeCurrent(self._surface): raise ContextException("Could not make context current.") self._clear_chunks_no_context() - self._context.doneCurrent() + gl_data.context.doneCurrent() - def _clear_far_chunks(self): + def _clear_far_chunks(self) -> None: """ Unload all chunk data outside the unload render distance. It is the job of the caller to handle the chunk generator. """ - if self._context is None or self._camera_chunk is None: + gl_data = self._gl_data_ + if gl_data is None or self._camera_chunk is None: return - if not self._context.makeCurrent(self._surface): + if not gl_data.context.makeCurrent(self._surface): raise ContextException("Could not make context current.") camera_cx, camera_cz = self._camera_chunk @@ -817,15 +870,15 @@ def _clear_far_chunks(self): for chunk_key in remove_chunk_keys: del container[chunk_key] - self._context.doneCurrent() + gl_data.context.doneCurrent() - def set_dimension(self, dimension: DimensionId): + def set_dimension(self, dimension: DimensionId) -> None: if dimension != self._dimension: self._dimension = dimension self._clear_chunks() self._update_chunk_finder() - def set_location(self, cx: int, cz: int): + def set_location(self, cx: int, cz: int) -> None: """Set the chunk the camera is in.""" cx = int(cx) cz = int(cz) @@ -836,7 +889,7 @@ def set_location(self, cx: int, cz: int): self._update_chunk_finder() self._chunks.set_position(cx, cz) - def set_render_distance(self, load_distance: int, unload_distance: int): + def set_render_distance(self, load_distance: int, unload_distance: int) -> None: """ Set the render distance attributes. @@ -856,16 +909,17 @@ def set_render_distance(self, load_distance: int, unload_distance: int): self._clear_far_chunks() self._update_chunk_finder() - def _queue_next_chunk(self): - if self._context and self._generation_count == 0: + def _queue_next_chunk(self) -> None: + if self._gl_data_ and self._generation_count == 0: # The instance is running and there are no existing generation calls running. self._generation_count += 1 self._queue_chunk.emit() _queue_chunk = Signal() - def _create_vao(self, chunk: WidgetChunkData, signal=True): - if self._context is None or not self._context.makeCurrent(self._surface): + def _create_vao(self, chunk: WidgetChunkData, signal: bool = True) -> None: + gl_data = self._gl_data_ + if gl_data is None or not gl_data.context.makeCurrent(self._surface): raise ContextException("Could not make context current.") f = QOpenGLContext.currentContext().functions() @@ -878,6 +932,8 @@ def _create_vao(self, chunk: WidgetChunkData, signal=True): # Associate the vbo with the vao vbo_container = chunk.shared.geometry + if vbo_container is None: + raise RuntimeError vbo_container.vbo.bind() # vertex coord @@ -901,7 +957,7 @@ def _create_vao(self, chunk: WidgetChunkData, signal=True): chunk.vao.release() vbo_container.vbo.release() - self._context.doneCurrent() + gl_data.context.doneCurrent() # Update the vbo attribute. # If a VBO was previously stored it will get automatically deleted when the last reference is lost. @@ -910,10 +966,10 @@ def _create_vao(self, chunk: WidgetChunkData, signal=True): if signal: self.geometry_changed.emit() - def _process_chunk(self): + def _process_chunk(self) -> None: """Generate a chunk. This must not be called directly.""" with CatchException(): - if self._context is None: + if self._gl_data_ is None: return while True: # Find a chunk key to process @@ -954,11 +1010,11 @@ def _process_chunk(self): @staticmethod def get_on_change_callback( weak_self: Callable[[], Optional[WidgetLevelGeometry]], chunk_key: ChunkKey - ): - def on_change(): + ) -> Callable[[], None]: + def on_change() -> None: with CatchException(): self_: Optional[WidgetLevelGeometry] = weak_self() - if self_ is None or self_._context is None: + if self_ is None or self_._gl_data_ is None: return self_._generation_count += 1 if chunk_key in self_._pending_chunks: diff --git a/src_/builtin_plugins_/amulet_team_3d_viewer/_view_3d/_renderer.py b/src/builtin_plugins/amulet_team_3d_viewer/_view_3d/_renderer.py similarity index 80% rename from src_/builtin_plugins_/amulet_team_3d_viewer/_view_3d/_renderer.py rename to src/builtin_plugins/amulet_team_3d_viewer/_view_3d/_renderer.py index 416349b1..868ba77b 100644 --- a/src_/builtin_plugins_/amulet_team_3d_viewer/_view_3d/_renderer.py +++ b/src/builtin_plugins/amulet_team_3d_viewer/_view_3d/_renderer.py @@ -1,4 +1,5 @@ from __future__ import annotations +from typing import Any, TypeVar import logging from math import sin, cos, radians @@ -13,12 +14,14 @@ QCursor, QGuiApplication, ) +from PySide6.QtWidgets import QWidget from PySide6.QtOpenGLWidgets import QOpenGLWidget +from OpenGL.constant import IntConstant from OpenGL.GL import ( - GL_COLOR_BUFFER_BIT, - GL_DEPTH_BUFFER_BIT, - GL_DEPTH_TEST, + GL_COLOR_BUFFER_BIT as _GL_COLOR_BUFFER_BIT, + GL_DEPTH_BUFFER_BIT as _GL_DEPTH_BUFFER_BIT, + GL_DEPTH_TEST as _GL_DEPTH_TEST, ) from amulet_editor.data.level import get_level @@ -32,6 +35,19 @@ log = logging.getLogger(__name__) +T = TypeVar("T") + + +def dynamic_cast(obj: Any, new_type: type[T]) -> T: + if not isinstance(obj, new_type): + raise TypeError(f"{obj} is not an instance of {new_type}") + return obj + + +GL_COLOR_BUFFER_BIT = dynamic_cast(_GL_COLOR_BUFFER_BIT, IntConstant) +GL_DEPTH_BUFFER_BIT = dynamic_cast(_GL_DEPTH_BUFFER_BIT, IntConstant) +GL_DEPTH_TEST = dynamic_cast(_GL_DEPTH_TEST, IntConstant) + """ GPU Memory Deallocation @@ -48,7 +64,7 @@ class GlData: data_valid: bool render_level: WidgetLevelGeometry - def __init__(self, render_level: WidgetLevelGeometry): + def __init__(self, render_level: WidgetLevelGeometry) -> None: self.context_valid = False self.data_valid = False self.render_level = render_level @@ -56,19 +72,19 @@ def __init__(self, render_level: WidgetLevelGeometry): def is_valid(self) -> bool: return self.context_valid and self.data_valid - def init_context(self): + def init_context(self) -> None: if self.context_valid: raise RuntimeError("Context was not destroyed.") self.context_valid = True - def destroy_context(self): + def destroy_context(self) -> None: if not self.context_valid: raise RuntimeError("Context was not initialised") if self.data_valid: raise RuntimeError("Data has not been destroyed.") self.context_valid = False - def init_context_data(self): + def init_context_data(self) -> None: """ Initialise the context data. The caller is responsible for managing the context. @@ -80,10 +96,10 @@ def init_context_data(self): self.data_valid = True self._init_context_data() - def _init_context_data(self): + def _init_context_data(self) -> None: self.render_level.initializeGL() - def destroy_context_data(self): + def destroy_context_data(self) -> None: """ Destroy the context data. The caller is responsible for managing the context. @@ -95,10 +111,10 @@ def destroy_context_data(self): self._destroy_context_data() log.debug("Destroyed GL") - def _destroy_context_data(self): + def _destroy_context_data(self) -> None: self.render_level.destroyGL() - def __del__(self): + def __del__(self) -> None: with CatchException(): log.debug("__del__ GlData") if self.data_valid: @@ -115,11 +131,16 @@ class FirstPersonCanvas(QOpenGLWidget, QOpenGLFunctions): # Having a pointer to self would stop self being garbage collected. _gl_data: GlData - def __init__(self, parent=None): + def __init__(self, parent: QWidget | None = None) -> None: QOpenGLWidget.__init__(self, parent) QOpenGLFunctions.__init__(self) - self._level = get_level() + level = get_level() + if level is None: + raise RuntimeError( + "FirstPersonCanvas cannot be constructed when a level does not exist." + ) + self._level = level self._gl_data = GlData(WidgetLevelGeometry(self._level)) self._gl_data.render_level.geometry_changed.connect(self.update) @@ -129,7 +150,7 @@ def __init__(self, parent=None): self._start_pos = QPoint() self._mouse_captured = False - self._speed = 1 + self._speed = 1.0 self._key_catcher = KeyCatcher() self.installEventFilter(self._key_catcher) @@ -166,7 +187,7 @@ def __init__(self, parent=None): ) self._resource_pack_container.init() - def initializeGL(self): + def initializeGL(self) -> None: """Private initialisation method called by the QOpenGLWidget""" # You must only put calls that do not need destructing here. # Normal initialisation must go in the showEvent and destruction in the hideEvent @@ -175,7 +196,7 @@ def initializeGL(self): gl_data = self._gl_data gl_data.init_context() - def destroy_context(): + def destroy_context() -> None: nonlocal gl_data # This is required for gl_data to be garbage collected with CatchException(): log.debug("Context aboutToBeDestroyed") @@ -192,31 +213,31 @@ def destroy_context(): gl_data.init_context_data() # TODO: pull this data from somewhere # Set the start position after OpenGL has been initialised - gl_data.render_level.set_dimension(self._level.dimensions[0]) + gl_data.render_level.set_dimension(next(iter(self._level.dimension_ids()))) self.camera.location = Location(0, 0, 0) - def __del__(self): + def __del__(self) -> None: log.debug("__del__ FirstPersonCanvas") @property def camera(self) -> Camera: return self._camera - def showEvent(self, event: QShowEvent): + def showEvent(self, event: QShowEvent) -> None: with CatchException(): log.debug("show") self.makeCurrent() self._gl_data.init_context_data() self.doneCurrent() - def hideEvent(self, event: QHideEvent): + def hideEvent(self, event: QHideEvent) -> None: with CatchException(): log.debug("hide") self.makeCurrent() self._gl_data.destroy_context_data() self.doneCurrent() - def paintGL(self): + def paintGL(self) -> None: """Private paint method called by the QOpenGLWidget""" with CatchException(): if ( @@ -236,18 +257,18 @@ def paintGL(self): self.camera.intrinsic_matrix, self.camera.extrinsic_matrix ) - def resizeGL(self, width, height): + def resizeGL(self, width: float, height: float) -> None: """Private resize method called by the QOpenGLWidget""" self.camera.set_perspective_projection(45, width / height, 0.01, 10_000) - def mousePressEvent(self, event: QMouseEvent): + def mousePressEvent(self, event: QMouseEvent) -> None: if event.buttons() & Qt.MouseButton.RightButton: self._mouse_captured = True self._start_pos = event.globalPosition().toPoint() self.setFocus() QGuiApplication.setOverrideCursor(QCursor(Qt.CursorShape.BlankCursor)) - def mouseMoveEvent(self, event: QMouseEvent): + def mouseMoveEvent(self, event: QMouseEvent) -> None: if event.buttons() & Qt.MouseButton.RightButton: pos = event.globalPosition().toPoint() dx = pos.x() - self._start_pos.x() @@ -262,7 +283,7 @@ def mouseMoveEvent(self, event: QMouseEvent): # On some systems setPos does not work. We must reset _start_pos to the new pos self._start_pos = QCursor.pos() - def mouseReleaseEvent(self, event: QMouseEvent): + def mouseReleaseEvent(self, event: QMouseEvent) -> None: if self._mouse_captured: self._mouse_captured = False QGuiApplication.restoreOverrideCursor() @@ -273,11 +294,11 @@ def wheelEvent(self, event: QWheelEvent) -> None: else: self._slower() - def _on_move(self): + def _on_move(self) -> None: x, _, z = self.camera.location - self._gl_data.render_level.set_location(x // 16, z // 16) + self._gl_data.render_level.set_location(int(x // 16), int(z // 16)) - def _move_relative(self, angle: int, dt: float): + def _move_relative(self, angle: int, dt: float) -> None: x, y, z = self.camera.location azimuth = radians(self.camera.rotation.azimuth + angle) self.camera.location = Location( @@ -285,35 +306,35 @@ def _move_relative(self, angle: int, dt: float): ) @Slot() - def _forwards(self, dt: float): + def _forwards(self, dt: float) -> None: self._move_relative(180, dt) @Slot() - def _right(self, dt: float): + def _right(self, dt: float) -> None: self._move_relative(270, dt) @Slot() - def _backwards(self, dt: float): + def _backwards(self, dt: float) -> None: self._move_relative(0, dt) @Slot() - def _left(self, dt: float): + def _left(self, dt: float) -> None: self._move_relative(90, dt) @Slot() - def _up(self, dt: float): + def _up(self, dt: float) -> None: x, y, z = self.camera.location self.camera.location = Location(x, y + self._speed * dt, z) @Slot() - def _down(self, dt: float): + def _down(self, dt: float) -> None: x, y, z = self.camera.location self.camera.location = Location(x, y - self._speed * dt, z) @Slot() - def _faster(self): + def _faster(self) -> None: self._speed *= 1.1 @Slot() - def _slower(self): + def _slower(self) -> None: self._speed /= 1.1 diff --git a/src_/builtin_plugins_/amulet_team_3d_viewer/_view_3d/_resource_pack.py b/src/builtin_plugins/amulet_team_3d_viewer/_view_3d/_resource_pack.py similarity index 76% rename from src_/builtin_plugins_/amulet_team_3d_viewer/_view_3d/_resource_pack.py rename to src/builtin_plugins/amulet_team_3d_viewer/_view_3d/_resource_pack.py index 64f581e0..97471894 100644 --- a/src_/builtin_plugins_/amulet_team_3d_viewer/_view_3d/_resource_pack.py +++ b/src/builtin_plugins/amulet_team_3d_viewer/_view_3d/_resource_pack.py @@ -15,11 +15,14 @@ from PySide6.QtGui import QImage, QOpenGLContext, QOffscreenSurface from PySide6.QtOpenGL import QOpenGLTexture -from minecraft_model_reader.api.resource_pack.base import BaseResourcePackManager -from minecraft_model_reader import BlockMesh -import PyMCTranslate -from amulet.block import Block -from amulet.level.abc import Level +from amulet.version import VersionNumber +from amulet.block import Block, BlockStack +from amulet.level.abc import Level, DiskLevel +from amulet.game.abc import GameVersion +from amulet.game import get_game_version +from amulet.mesh.block import BlockMesh +from amulet.mesh.block.missing_block import get_missing_block +from amulet.resource_pack.abc import BaseResourcePackManager from ._textureatlas import create_atlas @@ -35,7 +38,7 @@ class OpenGLResourcePack: """ - This class will take a minecraft_model_reader resource pack and load the textures into a texture atlas. + This class will take a resource pack and load the textures into a texture atlas. After creating an instance, initialise must be called. """ @@ -43,9 +46,9 @@ class OpenGLResourcePack: _lock = Lock() _resource_pack: BaseResourcePackManager # The translator to look up the version block - _translator: PyMCTranslate.Version + _game_version: GameVersion # Loaded block models - _block_models: Dict[Block, BlockMesh] + _block_models: Dict[BlockStack, BlockMesh] # Texture coordinates _texture_bounds: Dict[str, Tuple[float, float, float, float]] @@ -54,20 +57,22 @@ class OpenGLResourcePack: _context: Optional[QOpenGLContext] _surface: Optional[QOffscreenSurface] - def __init__( - self, resource_pack: BaseResourcePackManager, translator: PyMCTranslate.Version - ): + def __init__(self, resource_pack: BaseResourcePackManager, translator: GameVersion): self._lock = Lock() self._resource_pack = resource_pack - self._translator = translator + self._game_version = translator self._block_models = {} self._texture_bounds = {} self._texture = None self._context = None self._surface = None - def __del__(self): - if self._texture is not None: + def __del__(self) -> None: + if ( + self._context is not None + and self._surface is not None + and self._texture is not None + ): self._context.makeCurrent(self._surface) self._texture.destroy() self._context.doneCurrent() @@ -77,7 +82,7 @@ def initialise(self) -> Promise[None]: Create the atlas texture. """ - def func(promise_data: Promise.Data): + def func(promise_data: Promise.Data) -> None: with self._lock: if self._texture is None: cache_id = struct.unpack( @@ -131,7 +136,7 @@ def func(promise_data: Promise.Data): self._texture_bounds = bounds - def init_gl(): + def init_gl() -> None: self._context = QOpenGLContext() self._context.setShareContext( QOpenGLContext.globalShareContext() @@ -177,7 +182,7 @@ def get_texture(self) -> QOpenGLTexture: raise RuntimeError("The OpenGLResourcePack has not been initialised.") return self._texture - def get_texture_path(self, namespace: Optional[str], relative_path: str): + def get_texture_path(self, namespace: Optional[str], relative_path: str) -> str: """Get the absolute path of the image from the relative components. Useful for getting the id of textures for hard coded textures not connected to a resource pack. """ @@ -190,23 +195,34 @@ def texture_bounds(self, texture_path: str) -> Tuple[float, float, float, float] else: return self._texture_bounds[self._resource_pack.missing_no] - def get_block_model(self, universal_block: Block) -> BlockMesh: - """Get the BlockMesh class for a given universal Block. + def get_block_model(self, block_stack: BlockStack) -> BlockMesh: + """Get the BlockMesh class for a given BlockStack. The Block will be translated to the version format using the previously specified translator.""" - if universal_block not in self._block_models: - version_block = self._translator.block.from_universal( - universal_block.base_block - )[0] - if universal_block.extra_blocks: - for block_ in universal_block.extra_blocks: - version_block += self._translator.block.from_universal(block_)[0] - - self._block_models[universal_block] = self._resource_pack.get_block_model( - version_block - ) + if block_stack not in self._block_models: + blocks = list[Block]() + for block in block_stack: + if self._game_version.supports_version(block.platform, block.version): + blocks.append(block) + else: + # Translate to the required format. + converted_block, _, _ = get_game_version( + block.platform, block.version + ).block.translate( + self._game_version.platform, + self._game_version.max_version, + block, + ) + if isinstance(converted_block, Block): + blocks.append(converted_block) + if blocks: + self._block_models[block_stack] = self._resource_pack.get_block_model( + BlockStack(*blocks) + ) + else: + self._block_models[block_stack] = get_missing_block(self._resource_pack) - return self._block_models[universal_block] + return self._block_models[block_stack] class RenderResourcePackContainer(QObject): @@ -215,9 +231,9 @@ class RenderResourcePackContainer(QObject): # Emitted when the resource pack has changed. changed = Signal() - def __init__(self, level: Level): + def __init__(self, level: Level) -> None: super().__init__() - self._level = ref(level) + self._level = ref[Level](level) self._lock = RLock() self._resource_pack: Optional[OpenGLResourcePack] = None self._loader: Optional[Promise[None]] = None @@ -242,16 +258,21 @@ def resource_pack(self) -> OpenGLResourcePack: ) return rp - def _reload(self): - def func(promise_data: Promise.Data): + def _reload(self) -> None: + def func(promise_data: Promise.Data) -> None: with self._lock, DisplayException( "Error initialising the OpenGL resource pack." ): - level: BaseLevel = self._level() - log.debug(f"Loading OpenGL resource pack for level {level.level_path}") + level = self._level() + if level is None: + raise Exception("Level is None") + if isinstance(level, DiskLevel): + log.debug(f"Loading OpenGL resource pack for level {level.path}") + else: + log.debug(f"Loading OpenGL resource pack.") resource_pack = self._resource_pack_container.resource_pack # TODO: modify the resource pack library to expose the desired translator - translator = level.translation_manager.get_version("java", (999, 0, 0)) + translator = get_game_version("java", VersionNumber(999, 0, 0)) rp = OpenGLResourcePack(resource_pack, translator) promise = rp.initialise() @@ -278,12 +299,10 @@ def func(promise_data: Promise.Data): _lock = Lock() -_level_data: WeakKeyDictionary[BaseLevel, RenderResourcePackContainer] = ( - WeakKeyDictionary() -) +_level_data: WeakKeyDictionary[Level, RenderResourcePackContainer] = WeakKeyDictionary() -def get_gl_resource_pack_container(level: BaseLevel) -> RenderResourcePackContainer: +def get_gl_resource_pack_container(level: Level) -> RenderResourcePackContainer: with _lock: if level not in _level_data: _level_data[level] = invoke(lambda: RenderResourcePackContainer(level)) diff --git a/src_/builtin_plugins_/amulet_team_3d_viewer/_view_3d/_textureatlas.py b/src/builtin_plugins/amulet_team_3d_viewer/_view_3d/_textureatlas.py similarity index 89% rename from src_/builtin_plugins_/amulet_team_3d_viewer/_view_3d/_textureatlas.py rename to src/builtin_plugins/amulet_team_3d_viewer/_view_3d/_textureatlas.py index 42e2517f..d8c54bf7 100644 --- a/src_/builtin_plugins_/amulet_team_3d_viewer/_view_3d/_textureatlas.py +++ b/src/builtin_plugins/amulet_team_3d_viewer/_view_3d/_textureatlas.py @@ -25,7 +25,6 @@ import logging from PIL import Image import math -from typing import Dict, Tuple, List from collections.abc import Collection from amulet_editor.models.generic._promise import Promise @@ -43,7 +42,7 @@ class AtlasTooSmall(Exception): class Packable: """A two-dimensional object with position information.""" - def __init__(self, width: int, height: int): + def __init__(self, width: int, height: int) -> None: self._x = 0 self._y = 0 self._width = width @@ -78,7 +77,7 @@ def perimeter(self) -> int: return 2 * self._width + 2 * self._height -class PackRegion(object): +class PackRegion: """A region that two-dimensional Packable objects can be packed into.""" def __init__(self, x: int, y: int, width: int, height: int) -> None: @@ -187,19 +186,19 @@ def draw(self, image: Image.Image, border: int) -> None: image.paste(self._image, (self.x, self.y)) -class Texture(object): +class Texture: """A collection of one or more frames.""" - def __init__(self, name: str, frames: List[Frame]) -> None: + def __init__(self, name: str, frames: list[Frame]) -> None: self._name = name - self._frames: List[Frame] = frames + self._frames: list[Frame] = frames @property def name(self) -> str: return self._name @property - def frames(self) -> List[Frame]: + def frames(self) -> list[Frame]: return self._frames @@ -208,21 +207,21 @@ class TextureAtlas(PackRegion): def __init__(self, width: int, height: int, border: int = 0) -> None: super(TextureAtlas, self).__init__(0, 0, width, height) - self._textures: List[Texture] = [] + self._textures: list[Texture] = [] self._border = border @property - def textures(self) -> List[Texture]: + def textures(self) -> list[Texture]: return self._textures - def pack(self, texture: Texture) -> None: + def pack_texture(self, texture: Texture) -> None: """Pack a Texture into this atlas.""" self._textures.append(texture) for frame in texture.frames: - if not super(TextureAtlas, self).pack(frame, self._border): + if not self.pack(frame, self._border): raise AtlasTooSmall("Failed to pack frame %s" % frame.filename) - def to_dict(self) -> Dict[str, Tuple[int, int, int, int]]: + def to_dict(self) -> dict[str, tuple[float, float, float, float]]: return { tex.name: ( tex.frames[0].x / self.width, @@ -250,24 +249,24 @@ def write(self, filename: str, mode: str) -> None: def create_atlas( texture_tuple: Collection[str], -) -> Promise[Tuple[Image.Image, Dict[str, Tuple[float, float, float, float]]]]: - def func(promise_data: Promise.Data): +) -> Promise[tuple[Image.Image, dict[str, tuple[float, float, float, float]]]]: + def func( + promise_data: Promise.Data, + ) -> tuple[Image.Image, dict[str, tuple[float, float, float, float]]]: log.info("Creating texture atlas") # Parse texture names textures = [] - for texture_index, texture in enumerate(texture_tuple): + for texture_index, texture_path in enumerate(texture_tuple): if not texture_index % 100: promise_data.progress_change.emit( 0.5 * texture_index / (len(texture_tuple)) ) - # Look for a texture name - name, frames = texture, [texture] # Build frame objects - frames = [Frame(f) for f in frames] + frames = [Frame(texture_path)] # Add frames to texture object list - textures.append(Texture(name, frames)) + textures.append(Texture(texture_path, frames)) # Sort textures by perimeter size in non-increasing order textures = sorted(textures, key=lambda i: i.frames[0].perimeter, reverse=True) @@ -283,9 +282,7 @@ def func(promise_data: Promise.Data): size = max(height, width, 1 << (math.ceil(pixels**0.5) - 1).bit_length()) - atlas_created = False - atlas = None - while not atlas_created: + while True: try: # Create the atlas and pack textures in log.info(f"Trying to pack textures into image of size {size}x{size}") @@ -296,8 +293,8 @@ def func(promise_data: Promise.Data): promise_data.progress_change.emit( 0.5 + 0.5 * texture_index / len(textures) ) - atlas.pack(texture) - atlas_created = True + atlas.pack_texture(texture) + break except AtlasTooSmall: log.info(f"Image was too small. Trying with a larger area") size *= 2 diff --git a/src_/builtin_plugins_/amulet_team_3d_viewer/_view_3d/_view.py b/src/builtin_plugins/amulet_team_3d_viewer/_view_3d/_view.py similarity index 87% rename from src_/builtin_plugins_/amulet_team_3d_viewer/_view_3d/_view.py rename to src/builtin_plugins/amulet_team_3d_viewer/_view_3d/_view.py index 89f188f9..713d07e6 100644 --- a/src_/builtin_plugins_/amulet_team_3d_viewer/_view_3d/_view.py +++ b/src/builtin_plugins/amulet_team_3d_viewer/_view_3d/_view.py @@ -5,12 +5,12 @@ from PySide6.QtCore import Qt from PySide6.QtWidgets import QWidget, QVBoxLayout -from amulet_team_main_window import Widget +from amulet_team_main_window import TabWidget from ._renderer import FirstPersonCanvas -class View3D(QWidget, Widget): +class View3D(TabWidget): name = "3D View" def __init__( diff --git a/src_/builtin_plugins_/amulet_team_3d_viewer/_view_3d/chunk_builder_cy.pyx b/src/builtin_plugins/amulet_team_3d_viewer/_view_3d/chunk_builder_cy.pyx similarity index 95% rename from src_/builtin_plugins_/amulet_team_3d_viewer/_view_3d/chunk_builder_cy.pyx rename to src/builtin_plugins/amulet_team_3d_viewer/_view_3d/chunk_builder_cy.pyx index 678233d0..5e695917 100644 --- a/src_/builtin_plugins_/amulet_team_3d_viewer/_view_3d/chunk_builder_cy.pyx +++ b/src/builtin_plugins/amulet_team_3d_viewer/_view_3d/chunk_builder_cy.pyx @@ -14,7 +14,7 @@ from libc.stdint cimport ( ) cdef extern from "stdlib.h": - void *memcpy(void *dest, void *src, size_t n) nogil + void *memcpy(void *dest, void *src, size_t n) noexcept nogil from cpython cimport array import array @@ -72,7 +72,7 @@ cdef struct BlockArray: int32_t dy # the displacement in the y axis int32_t dz # the displacement in the z axis -cdef BlockArray* BlockArray_new(int32_t sx, int32_t sy, int32_t sz) nogil: +cdef BlockArray* BlockArray_new(int32_t sx, int32_t sy, int32_t sz) noexcept nogil: self = calloc(1, sizeof(BlockArray)) self.arr = malloc(sx * sy * sz * sizeof(uint32_t)) self.sx = sx @@ -91,7 +91,7 @@ cdef BlockArray* BlockArray_init( int32_t dx, int32_t dy, int32_t dz, -) nogil: +) noexcept nogil: self = BlockArray_new(sx, sy, sz) memcpy(self.arr, arr, sx * sy * sz * sizeof(uint32_t)) self.dx = dx @@ -99,7 +99,7 @@ cdef BlockArray* BlockArray_init( self.dz = dz return self -cdef void BlockArray_free(BlockArray* self) nogil: +cdef void BlockArray_free(BlockArray* self) noexcept nogil: free(self.arr) free(self) @@ -108,19 +108,19 @@ cdef struct VertArray: float* arr # pointer to the array int32_t size # the number of floats in the array -cdef VertArray* VertArray_new(uint32_t size) nogil: +cdef VertArray* VertArray_new(uint32_t size) noexcept nogil: # assert size and size % (ATTR_COUNT*3) == 0, "arr must have a multiple of 36 values" vert_array = calloc(1, sizeof(VertArray)) vert_array.arr = malloc(size * sizeof(float)) vert_array.size = size return vert_array -cdef VertArray* VertArray_init(float* arr, uint32_t size) nogil: +cdef VertArray* VertArray_init(float* arr, uint32_t size) noexcept nogil: vert_array = VertArray_new(size) memcpy(vert_array.arr, arr, size * sizeof(float)) return vert_array -cdef void VertArray_free(VertArray* vert_array) nogil: +cdef void VertArray_free(VertArray* vert_array) noexcept nogil: free(vert_array.arr) free(vert_array) @@ -148,7 +148,7 @@ cdef BlockModel* BlockModel_init(dict face_data, int8_t is_transparent): block_model.faces[index] = NULL return block_model -cdef void BlockModel_free(BlockModel* block_model) nogil: +cdef void BlockModel_free(BlockModel* block_model) noexcept nogil: cdef Py_ssize_t i for i in range(7): if block_model.faces[i]: @@ -197,27 +197,27 @@ cdef struct VertArrayContainer: int32_t size # The current size of the array int32_t used # The number of elements of the array that are used -cdef VertArrayContainer* VertArrayContainer_init() nogil: +cdef VertArrayContainer* VertArrayContainer_init() noexcept nogil: self = calloc(1, sizeof(VertArrayContainer)) self.arrays = calloc(10, sizeof(VertArray*)) self.size = 0 self.used = 0 return self -cdef void VertArrayContainer_free(VertArrayContainer* self) nogil: +cdef void VertArrayContainer_free(VertArrayContainer* self) noexcept nogil: cdef int32_t i for i in range(self.used): VertArray_free(self.arrays[i]) free(self.arrays) free(self) -cdef void VertArrayContainer_append(VertArrayContainer* self, VertArray* vert_array) nogil: +cdef void VertArrayContainer_append(VertArrayContainer* self, VertArray* vert_array) noexcept nogil: if self.used == self.size: _VertArrayContainer_extend(self) self.arrays[self.used] = vert_array self.used += 1 -cdef void _VertArrayContainer_extend(VertArrayContainer* self) nogil: +cdef void _VertArrayContainer_extend(VertArrayContainer* self) noexcept nogil: arr = calloc(self.size + 5, sizeof(VertArray*)) memcpy(arr, self.arrays, self.size * sizeof(VertArray*)) free(self.arrays) @@ -229,13 +229,13 @@ cdef struct VertArrayContainerTuple: VertArrayContainer* verts VertArrayContainer* verts_translucent -cdef VertArrayContainerTuple* VertArrayContainerTuple_init() nogil: +cdef VertArrayContainerTuple* VertArrayContainerTuple_init() noexcept nogil: self = calloc(1, sizeof(VertArrayContainerTuple)) self.verts = VertArrayContainer_init() self.verts_translucent = VertArrayContainer_init() return self -cdef void VertArrayContainerTuple_free(VertArrayContainerTuple* self) nogil: +cdef void VertArrayContainerTuple_free(VertArrayContainerTuple* self) noexcept nogil: VertArrayContainer_free(self.verts) VertArrayContainer_free(self.verts_translucent) free(self) @@ -245,13 +245,13 @@ cdef uint32_t get_block( int32_t x, int32_t y, int32_t z -) nogil: +) noexcept nogil: return block_array.arr[x * block_array.sz * block_array.sy + y * block_array.sz + z] cdef VertArrayContainerTuple* create_lod0_sub_chunk( BlockArray* block_array, BlockModelManager block_model_manager, -) nogil: +) noexcept nogil: cdef int32_t x, y, z, x_, y_, z_, dx, dy, dz # location variables # float counters diff --git a/src_/builtin_plugins_/amulet_team_3d_viewer/plugin.json b/src/builtin_plugins/amulet_team_3d_viewer/plugin.json similarity index 85% rename from src_/builtin_plugins_/amulet_team_3d_viewer/plugin.json rename to src/builtin_plugins/amulet_team_3d_viewer/plugin.json index 76103209..96f67967 100644 --- a/src_/builtin_plugins_/amulet_team_3d_viewer/plugin.json +++ b/src/builtin_plugins/amulet_team_3d_viewer/plugin.json @@ -11,9 +11,7 @@ "PyOpenGL~=3.0", "Pillow", "amulet_editor~=1.0a0", - "amulet_core~=1.9", - "minecraft_resource_pack~=1.3", - "pymctranslate~=1.2" + "amulet_core~=1.9" ], "plugin": [ "amulet_team_locale~=1.0", diff --git a/src/builtin_plugins/amulet_team_app/plugin.json b/src/builtin_plugins/amulet_team_app/plugin.json index ed7029c9..6326b88e 100644 --- a/src/builtin_plugins/amulet_team_app/plugin.json +++ b/src/builtin_plugins/amulet_team_app/plugin.json @@ -3,7 +3,7 @@ "version": "1.0.0", "name": "Amulet App", "depends": { - "python": "~=3.9", + "python": "~=3.11", "library": [ "amulet_editor~=1.0a0" ] diff --git a/src/builtin_plugins/amulet_team_default_layout/_plugin.py b/src/builtin_plugins/amulet_team_default_layout/_plugin.py index 100afaaa..19b82894 100644 --- a/src/builtin_plugins/amulet_team_default_layout/_plugin.py +++ b/src/builtin_plugins/amulet_team_default_layout/_plugin.py @@ -13,28 +13,28 @@ create_layout_button, ) from amulet_team_home_page import HomeWidget +from amulet_team_level_info import LevelInfoWidget import tablericons HomeLayoutID = "073bfd20-249e-4e0c-ad41-0bcb0c9db89f" home_button: ButtonProxy | None = None +LevelInfoLayoutID = "4de0ebcd-f789-440f-9526-e6cc5d77caff" +level_info_button: ButtonProxy | None = None def load_plugin() -> None: - global home_button - - register_layout(HomeLayoutID, LayoutConfig( - WindowConfig( - None, - None, - WidgetStackConfig( - ( - WidgetConfig(HomeWidget.__qualname__), - ) - ) + global home_button, level_info_button + + register_layout( + HomeLayoutID, + LayoutConfig( + WindowConfig( + None, None, WidgetStackConfig((WidgetConfig(HomeWidget.__qualname__),)) + ), + (), ), - (), - )) + ) # Set up the home button home_button = create_layout_button(HomeLayoutID) @@ -45,7 +45,23 @@ def load_plugin() -> None: # Make the home layout active by clicking the button home_button.click() else: - pass + register_layout( + LevelInfoLayoutID, + LayoutConfig( + WindowConfig( + None, + None, + WidgetStackConfig((WidgetConfig(LevelInfoWidget.__qualname__),)), + ), + (), + ), + ) + + # Set up the home button + level_info_button = create_layout_button(LevelInfoLayoutID) + level_info_button.set_icon(tablericons.file_info) + level_info_button.set_name("Level Info") + level_info_button.click() # register_view(View3D, tablericons.three_d_cube_sphere, "3D Editor") # get_active_window().activate_view(View3D) @@ -53,7 +69,10 @@ def load_plugin() -> None: def unload_plugin() -> None: if home_button is not None: home_button.delete() - unregister_layout(HomeLayoutID) + unregister_layout(HomeLayoutID) + if level_info_button is not None: + level_info_button.delete() + unregister_layout(LevelInfoLayoutID) plugin = PluginV1(load=load_plugin, unload=unload_plugin) diff --git a/src/builtin_plugins/amulet_team_default_layout/plugin.json b/src/builtin_plugins/amulet_team_default_layout/plugin.json index 17c8941c..d53423bd 100644 --- a/src/builtin_plugins/amulet_team_default_layout/plugin.json +++ b/src/builtin_plugins/amulet_team_default_layout/plugin.json @@ -3,13 +3,14 @@ "version": "1.0.0", "name": "Amulet Default Layout", "depends": { - "python": "~=3.9", + "python": "~=3.11", "library": [ "amulet_editor~=1.0a0" ], "plugin": [ "amulet_team_main_window~=1.0", "amulet_team_home_page~=1.0", + "amulet_team_level_info~=1.0", "tablericons~=1.0" ] }, diff --git a/src/builtin_plugins/amulet_team_home_page/_plugin.py b/src/builtin_plugins/amulet_team_home_page/_plugin.py index 77330267..4e5f01f2 100644 --- a/src/builtin_plugins/amulet_team_home_page/_plugin.py +++ b/src/builtin_plugins/amulet_team_home_page/_plugin.py @@ -8,7 +8,7 @@ import amulet_team_locale import amulet_team_main_window -from amulet_team_main_window import register_widget +from amulet_team_main_window import register_widget, unregister_widget import amulet_team_home_page from .home import HomeWidget @@ -38,7 +38,7 @@ def _locale_changed() -> None: def unload_plugin() -> None: - amulet_team_main_window.unregister_widget(HomeWidget) + unregister_widget(HomeWidget) if _translator is not None: QCoreApplication.removeTranslator(_translator) diff --git a/src/builtin_plugins/amulet_team_home_page/plugin.json b/src/builtin_plugins/amulet_team_home_page/plugin.json index 51d083e7..233769d6 100644 --- a/src/builtin_plugins/amulet_team_home_page/plugin.json +++ b/src/builtin_plugins/amulet_team_home_page/plugin.json @@ -3,7 +3,7 @@ "version": "1.0.0", "name": "Amulet Home Page", "depends": { - "python": "~=3.9", + "python": "~=3.11", "library": [ "PySide6_Essentials~=6.2", "amulet_editor~=1.0a0" diff --git a/src/builtin_plugins/amulet_team_inspector/plugin.json b/src/builtin_plugins/amulet_team_inspector/plugin.json index 2db212b3..33769e8d 100644 --- a/src/builtin_plugins/amulet_team_inspector/plugin.json +++ b/src/builtin_plugins/amulet_team_inspector/plugin.json @@ -3,7 +3,7 @@ "version": "1.0.0", "name": "Amulet Inspector", "depends": { - "python": "~=3.9", + "python": "~=3.11", "library": [ "PySide6_Essentials~=6.2", "amulet_editor~=1.0a0" diff --git a/src/builtin_plugins/amulet_team_level_info/__init__.py b/src/builtin_plugins/amulet_team_level_info/__init__.py new file mode 100644 index 00000000..09ca1dbd --- /dev/null +++ b/src/builtin_plugins/amulet_team_level_info/__init__.py @@ -0,0 +1,2 @@ +from ._plugin import plugin +from .level_info import LevelInfoWidget diff --git a/src/builtin_plugins/amulet_team_level_info/_plugin.py b/src/builtin_plugins/amulet_team_level_info/_plugin.py new file mode 100644 index 00000000..e83d1363 --- /dev/null +++ b/src/builtin_plugins/amulet_team_level_info/_plugin.py @@ -0,0 +1,46 @@ +from __future__ import annotations +import os + +from PySide6.QtCore import QLocale, QCoreApplication + +from amulet_editor.models.localisation import ATranslator +from amulet_editor.models.plugin import PluginV1 + +import amulet_team_locale +import amulet_team_main_window +from amulet_team_main_window import register_widget + +import amulet_team_level_info +from .level_info import LevelInfoWidget + + +# Qt only weekly references this. We must hold a strong reference to stop it getting garbage collected +_translator: ATranslator | None = None + + +def load_plugin() -> None: + global _translator + _translator = ATranslator() + _locale_changed() + QCoreApplication.installTranslator(_translator) + amulet_team_locale.locale_changed.connect(_locale_changed) + + register_widget(LevelInfoWidget) + + +def _locale_changed() -> None: + assert _translator is not None + _translator.load_lang( + QLocale(), + "", + directory=os.path.join(*amulet_team_level_info.__path__, "resources", "lang"), + ) + + +def unload_plugin() -> None: + amulet_team_main_window.unregister_widget(LevelInfoWidget) + if _translator is not None: + QCoreApplication.removeTranslator(_translator) + + +plugin = PluginV1(load=load_plugin, unload=unload_plugin) diff --git a/src/builtin_plugins/amulet_team_level_info/level_info/__init__.py b/src/builtin_plugins/amulet_team_level_info/level_info/__init__.py new file mode 100644 index 00000000..028a388c --- /dev/null +++ b/src/builtin_plugins/amulet_team_level_info/level_info/__init__.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from typing import Optional + +from PySide6.QtCore import Qt +from PySide6.QtWidgets import QWidget, QVBoxLayout + +from amulet_team_main_window import TabWidget + + +class LevelInfoWidget(TabWidget): + name = "LevelInfo" + + def __init__( + self, parent: Optional[QWidget] = None, f: Qt.WindowType = Qt.WindowType.Widget + ): + super().__init__(parent, f) + self._layout = QVBoxLayout(self) + self._layout.setContentsMargins(0, 0, 0, 0) diff --git a/src/builtin_plugins/amulet_team_level_info/plugin.json b/src/builtin_plugins/amulet_team_level_info/plugin.json new file mode 100644 index 00000000..1dfdae2e --- /dev/null +++ b/src/builtin_plugins/amulet_team_level_info/plugin.json @@ -0,0 +1,18 @@ +{ + "identifier": "amulet_team_level_info", + "version": "1.0.0", + "name": "Amulet Level Info Page", + "depends": { + "python": "~=3.11", + "library": [ + "PySide6_Essentials~=6.2", + "amulet_editor~=1.0a0" + ], + "plugin": [ + "amulet_team_locale~=1.0", + "amulet_team_main_window~=1.0", + "tablericons~=1.0" + ] + }, + "locked": "true" +} diff --git a/src/builtin_plugins/amulet_team_level_info/resources/lang/en.lang b/src/builtin_plugins/amulet_team_level_info/resources/lang/en.lang new file mode 100644 index 00000000..e69de29b diff --git a/src/builtin_plugins/amulet_team_locale/plugin.json b/src/builtin_plugins/amulet_team_locale/plugin.json index d029931f..1b5ba50e 100644 --- a/src/builtin_plugins/amulet_team_locale/plugin.json +++ b/src/builtin_plugins/amulet_team_locale/plugin.json @@ -3,7 +3,7 @@ "version": "1.0.0", "name": "Amulet Locale", "depends": { - "python": "~=3.9", + "python": "~=3.11", "library": [ "amulet_editor~=1.0a0" ] diff --git a/src/builtin_plugins/amulet_team_main_window/plugin.json b/src/builtin_plugins/amulet_team_main_window/plugin.json index a3e39b7f..b9d489f5 100644 --- a/src/builtin_plugins/amulet_team_main_window/plugin.json +++ b/src/builtin_plugins/amulet_team_main_window/plugin.json @@ -3,7 +3,7 @@ "version": "1.0.0", "name": "Amulet Main Window", "depends": { - "python": "~=3.9", + "python": "~=3.11", "library": [ "shiboken6~=6.2", "PySide6_Essentials~=6.2", diff --git a/src_/builtin_plugins_/amulet_team_resource_pack/__init__.py b/src/builtin_plugins/amulet_team_resource_pack/__init__.py similarity index 100% rename from src_/builtin_plugins_/amulet_team_resource_pack/__init__.py rename to src/builtin_plugins/amulet_team_resource_pack/__init__.py diff --git a/src_/builtin_plugins_/amulet_team_resource_pack/_api.py b/src/builtin_plugins/amulet_team_resource_pack/_api.py similarity index 96% rename from src_/builtin_plugins_/amulet_team_resource_pack/_api.py rename to src/builtin_plugins/amulet_team_resource_pack/_api.py index 9fbb2683..cfaafd11 100644 --- a/src_/builtin_plugins_/amulet_team_resource_pack/_api.py +++ b/src/builtin_plugins/amulet_team_resource_pack/_api.py @@ -13,11 +13,9 @@ from amulet_editor.models.generic._promise import Promise from amulet_editor.models.widgets.traceback_dialog import DisplayException -from minecraft_model_reader import BaseResourcePackManager -from minecraft_model_reader.api.resource_pack import ( - load_resource_pack_manager, -) -from minecraft_model_reader.api.resource_pack.java.download_resources import ( +from amulet.resource_pack.abc import BaseResourcePackManager +from amulet.resource_pack import load_resource_pack_manager +from amulet.resource_pack.java.download_resources import ( get_java_vanilla_latest_iter, get_java_vanilla_fix, ) diff --git a/src_/builtin_plugins_/amulet_team_resource_pack/_plugin.py b/src/builtin_plugins/amulet_team_resource_pack/_plugin.py similarity index 100% rename from src_/builtin_plugins_/amulet_team_resource_pack/_plugin.py rename to src/builtin_plugins/amulet_team_resource_pack/_plugin.py diff --git a/src_/builtin_plugins_/amulet_team_resource_pack/plugin.json b/src/builtin_plugins/amulet_team_resource_pack/plugin.json similarity index 89% rename from src_/builtin_plugins_/amulet_team_resource_pack/plugin.json rename to src/builtin_plugins/amulet_team_resource_pack/plugin.json index 87958fca..c24361de 100644 --- a/src_/builtin_plugins_/amulet_team_resource_pack/plugin.json +++ b/src/builtin_plugins/amulet_team_resource_pack/plugin.json @@ -6,7 +6,6 @@ "python": "~=3.9", "library": [ "PySide6_Essentials~=6.2", - "minecraft_resource_pack~=1.3", "amulet_editor~=1.0a0", "amulet_core~=1.9" ], diff --git a/src_/builtin_plugins_/amulet_team_resource_pack/resources/lang/en.lang b/src/builtin_plugins/amulet_team_resource_pack/resources/lang/en.lang similarity index 100% rename from src_/builtin_plugins_/amulet_team_resource_pack/resources/lang/en.lang rename to src/builtin_plugins/amulet_team_resource_pack/resources/lang/en.lang diff --git a/src/builtin_plugins/amulet_team_selection/__init__.py b/src/builtin_plugins/amulet_team_selection/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src_/builtin_plugins_/amulet_team_selection/plugin.json b/src/builtin_plugins/amulet_team_selection/plugin.json similarity index 100% rename from src_/builtin_plugins_/amulet_team_selection/plugin.json rename to src/builtin_plugins/amulet_team_selection/plugin.json diff --git a/src/builtin_plugins/tablericons/plugin.json b/src/builtin_plugins/tablericons/plugin.json index a5f7b95c..c381d85e 100644 --- a/src/builtin_plugins/tablericons/plugin.json +++ b/src/builtin_plugins/tablericons/plugin.json @@ -3,7 +3,7 @@ "version": "1.0.0", "name": "Tabler Icons", "depends": { - "python": "~=3.9", + "python": "~=3.11", "plugin": [] }, "locked": "true" diff --git a/src/builtin_plugins/thread_manager/plugin.json b/src/builtin_plugins/thread_manager/plugin.json index 322d3519..e8d4193d 100644 --- a/src/builtin_plugins/thread_manager/plugin.json +++ b/src/builtin_plugins/thread_manager/plugin.json @@ -3,7 +3,7 @@ "version": "1.0.0", "name": "Thread Manager", "depends": { - "python": "~=3.9", + "python": "~=3.11", "library": [ "shiboken6~=6.2", "PySide6_Essentials~=6.2", diff --git a/tools/generate_pybind_stubs.py b/tools/generate_pybind_stubs.py new file mode 100644 index 00000000..100fb1de --- /dev/null +++ b/tools/generate_pybind_stubs.py @@ -0,0 +1,123 @@ +import os +import glob +import importlib.util +import sys +import subprocess +import re +import pybind11_stubgen +from pybind11_stubgen.structs import Identifier +from pybind11_stubgen.parser.mixins.filter import FilterClassMembers + +UnionPattern = re.compile( + r"^(?P[a-zA-Z_][a-zA-Z0-9_]*): types\.UnionType\s*#\s*value = (?P.*)$", + flags=re.MULTILINE, +) +VersionPattern = re.compile(r"(?P[a-zA-Z0-9_].*): str = '.*?'") + + +def union_sub_func(match: re.Match) -> str: + return f'{match.group("variable")}: typing.TypeAlias = {match.group("value")}' + + +def str_sub_func(match: re.Match) -> str: + return f"{match.group('var')}: str" + + +def get_module_path(name: str) -> str: + spec = importlib.util.find_spec(name) + assert spec is not None + module_path = spec.origin + assert module_path is not None + return module_path + + +def get_package_dir(name: str) -> str: + return os.path.dirname(get_module_path(name)) + + +def patch_stubgen(): + # Is there a better way to add items to the blacklist? + # ABC + FilterClassMembers._FilterClassMembers__attribute_blacklist.add( + Identifier("__abstractmethods__") + ) + FilterClassMembers._FilterClassMembers__attribute_blacklist.add( + Identifier("__orig_bases__") + ) + FilterClassMembers._FilterClassMembers__attribute_blacklist.add( + Identifier("__parameters__") + ) + FilterClassMembers._FilterClassMembers__attribute_blacklist.add( + Identifier("_abc_impl") + ) + # Protocol + FilterClassMembers._FilterClassMembers__attribute_blacklist.add( + Identifier("__protocol_attrs__") + ) + FilterClassMembers._FilterClassMembers__attribute_blacklist.add( + Identifier("_is_protocol") + ) + # dataclass + FilterClassMembers._FilterClassMembers__attribute_blacklist.add( + Identifier("__dataclass_fields__") + ) + FilterClassMembers._FilterClassMembers__attribute_blacklist.add( + Identifier("__dataclass_params__") + ) + FilterClassMembers._FilterClassMembers__attribute_blacklist.add( + Identifier("__match_args__") + ) + + +def main() -> None: + src_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "src") + + for stub_path in glob.iglob( + os.path.join(glob.escape(src_path), "**", "*.pyi"), recursive=True + ): + os.remove(stub_path) + + patch_stubgen() + sys.argv = [ + "pybind11_stubgen", + f"--output-dir={os.path.join(src_path, 'builtin_plugins', 'amulet_team_3d_viewer', '_view_3d')}", + "amulet_team_3d_viewer._view_3d._chunk_builder", + ] + pybind11_stubgen.main() + # If pybind11_stubgen adds args to main + # pybind11_stubgen.main([ + # f"--output-dir={src_path}", + # "amulet", + # ]) + + for stub_path in glob.iglob( + os.path.join(glob.escape(src_path), "**", "*.pyi"), recursive=True + ): + with open(stub_path, encoding="utf-8") as f: + pyi = f.read() + pyi = UnionPattern.sub(union_sub_func, pyi) + pyi = VersionPattern.sub(str_sub_func, pyi) + with open(stub_path, "w", encoding="utf-8") as f: + f.write(pyi) + + subprocess.run( + [ + "isort", + stub_path, + ] + ) + + subprocess.run( + [ + "autoflake", + "--in-place", + "--remove-unused-variables", + stub_path, + ] + ) + + subprocess.run([sys.executable, "-m", "black", src_path]) + + +if __name__ == "__main__": + main() diff --git a/tools/generate_vs_sln.py b/tools/generate_vs_sln.py new file mode 100644 index 00000000..31cd4f2d --- /dev/null +++ b/tools/generate_vs_sln.py @@ -0,0 +1,577 @@ +"""This generates a Visual Studio solution file and projects for each module.""" + +from __future__ import annotations +import os +import re +import uuid +from dataclasses import dataclass, field +from enum import Enum +import pybind11 +import sys +import glob +import sysconfig +from collections.abc import Iterable +from hashlib import md5 +import importlib.util + +SrcDir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "src") + +ProjectPattern = re.compile( + r'Project\("{(?P[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})}"\) = "(?P[a-zA-Z0-9_-]+)", "(?P[a-zA-Z0-9\\/_-]+\.vcxproj)", "{(?P[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})}"' +) + +PythonExtensionModuleSuffix = ( + f".cp{sys.version_info.major}{sys.version_info.minor}-win_amd64.pyd" +) + +PythonIncludeDir = sysconfig.get_paths()["include"] +PythonLibraryDir = os.path.join(os.path.dirname(PythonIncludeDir), "libs") + + +VCXProjSource = """\ + """ +VCXProjInclude = """\ + """ + +VCXProj = r""" + + + + Debug + x64 + + + Release + x64 + + + + 17.0 + {{{project_guid}}} + Win32Proj + 10.0 + + + + {library_type} + true + v143 + + + {library_type} + false + v143 + + + + + + + + + + + + + + + $(SolutionDir)$(Platform)\$(Configuration)\int\{project_name}\ + {out_dir} + {file_extension} + $(VC_LibraryPath_x64);$(WindowsSDK_LibraryPath_x64);$(WindowsSDK_LibraryPath_x64);{library_path} + {ext_name} + + + $(SolutionDir)$(Platform)\$(Configuration)\int\{project_name}\ + {out_dir} + {file_extension} + $(VC_LibraryPath_x64);$(WindowsSDK_LibraryPath_x64);$(WindowsSDK_LibraryPath_x64);{library_path} + {ext_name} + + + + stdcpp20 + {include_dirs}%(AdditionalIncludeDirectories) + + + $(CoreLibraryDependencies);%(AdditionalDependencies);{libraries} + + + + + stdcpp20 + {include_dirs}%(AdditionalIncludeDirectories) + + + $(CoreLibraryDependencies);%(AdditionalDependencies);{libraries} + + + +{source_files} + + +{include_files} + + + + +""" + + +VCXProjFiltersSource = """\ + + {rel_path} + """ + + +VCXProjFiltersSourceGroup = """\ + + {{{uuid}}} + +""" + + +VCXProjFiltersInclude = """\ + + {rel_path} + """ + + +VCXProjFiltersIncludeGroup = """\ + + {{{uuid}}} + +""" + + +VCXProjFilters = r""" + + +{filter_groups} + +{source_files} + + +{include_files} + +""" + + +VCXProjUser = r""" + + +""" + + +SolutionHeader = """Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32630.192 +MinimumVisualStudioVersion = 10.0.40219.1 +""" + +SolutionProjectDependency = """\ + {{{dependency_guid}}} = {{{dependency_guid}}} +""" +SolutionProjectDependencies = """\ + ProjectSection(ProjectDependencies) = postProject +{project_dependencies}\ + EndProjectSection +""" + +SolutionProject = """Project("{{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}}") = "{project_name}", "{project_name}.vcxproj", "{{{project_guid}}}" +{project_dependencies}EndProject +""" + +SolutionGlobalConfigurationPlatforms = """\ + {{{project_guid}}}.Debug|x64.ActiveCfg = Debug|x64 + {{{project_guid}}}.Debug|x64.Build.0 = Debug|x64 + {{{project_guid}}}.Debug|x86.ActiveCfg = Debug|x64 + {{{project_guid}}}.Debug|x86.Build.0 = Debug|x64 + {{{project_guid}}}.Release|x64.ActiveCfg = Release|x64 + {{{project_guid}}}.Release|x64.Build.0 = Release|x64 + {{{project_guid}}}.Release|x86.ActiveCfg = Release|x64 + {{{project_guid}}}.Release|x86.Build.0 = Release|x64""" + + +SolutionGlobal = """\ +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution +{configuration_platforms} + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {{6B72B1FC-8248-4021-B089-D05ED8CBCE73}} + EndGlobalSection +EndGlobal +""" + + +class CompileMode(Enum): + DynamicLibrary = "DynamicLibrary" + StaticLibrary = "StaticLibrary" + PythonExtension = "pyd" + + +@dataclass(kw_only=True) +class ProjectData: + name: str + compile_mode: CompileMode + source_files: list[tuple[str, str, str]] = field(default_factory=list) + include_files: list[tuple[str, str, str]] = field(default_factory=list) + include_dirs: list[str] = field(default_factory=list) + library_dirs: list[str] = field(default_factory=list) + dependencies: list[ProjectData] = field(default_factory=list) + py_package: str | None = None + package_dir: str | None = None + + def project_guid(self) -> str: + encoded_name = self.name.encode() + digest = md5(encoded_name).hexdigest() + return ( + digest[0:8].upper() + + "-" + + digest[8:12].upper() + + "-" + + digest[12:16].upper() + + "-" + + digest[16:20].upper() + + "-" + + digest[20:32].upper() + ) + + +def write( + src_dir: str, solution_dir: str, solution_name: str, projects: list[ProjectData] +) -> None: + os.makedirs(solution_dir, exist_ok=True) + for project in projects: + project_guid = project.project_guid() + vcxproj_sources = "\n".join( + VCXProjSource.format(path=os.path.join(*path)) + for path in project.source_files + ) + vcxproj_includes = "\n".join( + VCXProjInclude.format(path=os.path.join(*path)) + for path in project.include_files + ) + library_type = project.compile_mode + project_name = project.name + if library_type == CompileMode.PythonExtension: + extension = PythonExtensionModuleSuffix + library_type = CompileMode.DynamicLibrary + if project.py_package: + module_path = project.py_package.replace(".", "\\") + out_dir = f"{project.package_dir or src_dir}\\{module_path}\\" + project_name = f"{project.py_package}.{project_name}" + else: + out_dir = f"{project.package_dir or src_dir}\\" + elif library_type == CompileMode.StaticLibrary: + extension = ".lib" + out_dir = ( + f"$(SolutionDir)$(Platform)\\$(Configuration)\\out\\{project.name}\\" + ) + # elif library_type == CompileMode.DynamicLibrary: + # extension = ".dll" + # out_dir = f"$(SolutionDir)$(Platform)\\$(Configuration)\\out\\{project.name}\\" + else: + raise RuntimeError + with open( + os.path.join(solution_dir, f"{project_name}.vcxproj"), "w", encoding="utf8" + ) as f: + f.write( + VCXProj.format( + project_name=project_name, + ext_name=project.name, + source_files=vcxproj_sources, + include_files=vcxproj_includes, + include_dirs="".join(f"{path};" for path in project.include_dirs), + library_path="".join( + [f"{path};" for path in project.library_dirs] + + [ + f"$(SolutionDir)$(Platform)\\$(Configuration)\\out\\{dep.name}\\;" + for dep in project.dependencies + if dep.compile_mode == CompileMode.StaticLibrary + and dep.source_files + ] + ), + libraries="".join( + f"{dep.name}.lib;" + for dep in project.dependencies + if dep.compile_mode == CompileMode.StaticLibrary + and dep.source_files + ), + library_type=library_type.value, + project_guid=project_guid, + file_extension=extension, + out_dir=out_dir, + ) + ) + filter_sources = [] + filter_includes = [] + filter_sources_groups = dict[str, str]() + filter_includes_groups = dict[str, str]() + for path in project.source_files: + filter_sources.append( + VCXProjFiltersSource.format( + path=os.path.join(*path), rel_path=path[1] or "" + ) + ) + rel_path = path[1] + while rel_path: + if rel_path not in filter_sources_groups: + filter_sources_groups[rel_path] = VCXProjFiltersSourceGroup.format( + path=rel_path, uuid=str(uuid.uuid4()) + ) + rel_path = os.path.dirname(rel_path) + for path in project.include_files: + filter_includes.append( + VCXProjFiltersInclude.format( + path=os.path.join(*path), rel_path=path[1] or "" + ) + ) + rel_path = path[1] + while rel_path: + if rel_path not in filter_includes_groups: + filter_includes_groups[rel_path] = ( + VCXProjFiltersIncludeGroup.format( + path=rel_path, uuid=str(uuid.uuid4()) + ) + ) + rel_path = os.path.dirname(rel_path) + with open( + os.path.join(solution_dir, f"{project_name}.vcxproj.filters"), + "w", + encoding="utf8", + ) as f: + f.write( + VCXProjFilters.format( + source_files="\n".join(filter_sources), + include_files="\n".join(filter_includes), + filter_groups="".join(filter_sources_groups.values()) + + "".join(filter_includes_groups.values()), + ) + ) + with open( + os.path.join(solution_dir, f"{project_name}.vcxproj.user"), + "w", + encoding="utf8", + ) as f: + f.write(VCXProjUser) + + # write solution file + with open( + os.path.join(solution_dir, f"{solution_name}.sln"), "w", encoding="utf8" + ) as f: + f.write(SolutionHeader) + global_configuration_platforms = [] + for project in projects: + project_guid = project.project_guid() + project_name = ( + f"{project.py_package}.{project.name}" + if project.py_package + else project.name + ) + if project.dependencies: + project_dependencies = "".join( + SolutionProjectDependency.format(dependency_guid=dep.project_guid()) + for dep in project.dependencies + ) + project_dependencies = SolutionProjectDependencies.format( + project_dependencies=project_dependencies + ) + else: + project_dependencies = "" + f.write( + SolutionProject.format( + project_name=project_name, + project_guid=project_guid, + project_dependencies=project_dependencies, + ) + ) + global_configuration_platforms.append( + SolutionGlobalConfigurationPlatforms.format(project_guid=project_guid) + ) + f.write( + SolutionGlobal.format( + configuration_platforms="\n".join(global_configuration_platforms) + ) + ) + + +def get_files( + *, + root_dir: str, + ext: str, + root_dir_suffix: str = "", + exclude_dirs: Iterable[str] = (), + exclude_exts: Iterable[str] = (), +) -> list[tuple[str, str, str]]: + """ + Get file paths split into + 1) containing folder ("your/path") + 2) relative path to parent directory within containing folder ("amulet/io") + 3) file name. ("binary_reader.hpp") + get_files("your/path", "hpp") + """ + paths = list[tuple[str, str, str]]() + search_path = root_dir + if root_dir_suffix: + search_path = os.path.join(search_path, root_dir_suffix) + for path in glob.iglob( + os.path.join(glob.escape(search_path), "**", f"*.{ext}"), recursive=True + ): + if any(map(path.startswith, exclude_dirs)): + continue + if any(map(path.endswith, exclude_exts)): + continue + rel_path = os.path.relpath(path, root_dir) + paths.append((root_dir, os.path.dirname(rel_path), os.path.basename(rel_path))) + return paths + + +def get_package_path(name: str) -> str: + spec = importlib.util.find_spec(name) + if spec is None: + raise RuntimeError(f"Could not find {name}") + search_path = spec.submodule_search_locations + if search_path is None: + raise RuntimeError(f"{name} search path is None") + return search_path[0] + + +def main() -> None: + amulet_nbt_path = get_package_path("amulet_nbt") + amulet_nbt_lib = ProjectData( + name="amulet_nbt", + compile_mode=CompileMode.StaticLibrary, + include_files=get_files( + root_dir=amulet_nbt_path, root_dir_suffix="include", ext="hpp" + ), + source_files=get_files( + root_dir=amulet_nbt_path, root_dir_suffix="cpp", ext="cpp" + ), + include_dirs=[os.path.join(amulet_nbt_path, "include")], + ) + amulet_nbt_py = ProjectData( + name="__init__", + compile_mode=CompileMode.PythonExtension, + source_files=get_files( + root_dir=amulet_nbt_path, + root_dir_suffix="pybind", + ext="cpp", + ), + include_dirs=[ + PythonIncludeDir, + pybind11.get_include(), + os.path.join(amulet_nbt_path, "include"), + ], + library_dirs=[ + PythonLibraryDir, + ], + dependencies=[ + amulet_nbt_lib, + ], + py_package="amulet_nbt", + package_dir=os.path.dirname(amulet_nbt_path), + ) + + amulet_core_path = get_package_path("amulet") + amulet_core_lib = ProjectData( + name="amulet", + compile_mode=CompileMode.StaticLibrary, + include_files=get_files( + root_dir=os.path.dirname(amulet_core_path), + root_dir_suffix="amulet", + ext="hpp", + exclude_exts=[".py.hpp"], + ), + source_files=get_files( + root_dir=os.path.dirname(amulet_core_path), + root_dir_suffix="amulet", + ext="cpp", + exclude_exts=[".py.cpp"], + ), + include_dirs=[ + PythonIncludeDir, + pybind11.get_include(), + os.path.join(amulet_nbt_path, "include"), + os.path.dirname(amulet_core_path), + ], + ) + amulet_core_py = ProjectData( + name="__init__", + compile_mode=CompileMode.PythonExtension, + include_files=get_files( + root_dir=os.path.dirname(amulet_core_path), + root_dir_suffix="amulet", + ext="py.hpp", + ), + source_files=get_files( + root_dir=os.path.dirname(amulet_core_path), + root_dir_suffix="amulet", + ext="py.cpp", + ), + include_dirs=[ + PythonIncludeDir, + pybind11.get_include(), + os.path.join(amulet_nbt_path, "include"), + os.path.dirname(amulet_core_path), + ], + library_dirs=[ + PythonLibraryDir, + ], + dependencies=[amulet_nbt_lib, amulet_core_lib], + py_package="amulet", + package_dir=os.path.dirname(amulet_core_path), + ) + + view_3d_path = os.path.join( + SrcDir, "builtin_plugins", "amulet_team_3d_viewer", "_view_3d" + ) + chunk_builder_py = ProjectData( + name="_chunk_builder", + compile_mode=CompileMode.PythonExtension, + include_files=get_files(root_dir=view_3d_path, ext="hpp"), + source_files=get_files( + root_dir=view_3d_path, + ext="cpp", + ), + include_dirs=[ + PythonIncludeDir, + pybind11.get_include(), + os.path.join(amulet_nbt_path, "include"), + os.path.dirname(amulet_core_path), + view_3d_path, + ], + library_dirs=[ + PythonLibraryDir, + ], + dependencies=[amulet_nbt_lib, amulet_core_lib], + py_package="builtin_plugins.amulet_team_3d_viewer._view_3d", + ) + projects = [ + amulet_nbt_lib, + amulet_nbt_py, + amulet_core_lib, + amulet_core_py, + chunk_builder_py, + ] + + write( + SrcDir, + os.path.join(SrcDir, "sln"), + "Amulet-Editor", + projects, + ) + + +if __name__ == "__main__": + main()