diff --git a/.vscode/dictionary.txt b/.vscode/dictionary.txt index 82f9fca..a547507 100644 --- a/.vscode/dictionary.txt +++ b/.vscode/dictionary.txt @@ -344,7 +344,6 @@ marm maskable maxcolors maxidx -maxpane maxpayne mbin medi diff --git a/README.md b/README.md index 26c8aa2..4279d95 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Run `dade --help` to list the games, and `dade --help` to list a game's s | `dade incoming` | _Incoming_ (PC and Dreamcast) | Rage Software / Interplay | | `dade jubeatplus` | _jubeat plus_ (iOS) | Konami | | `dade marmalade` | Any Marmalade SDK title (Derbh, IwResGroup) | Marmalade / Ideaworks | -| `dade maxpane` | _Max Payne_ (PC) | Remedy Entertainment | +| `dade maxpayne` | _Max Payne_ (PC) | Remedy Entertainment | | `dade misc` | Formats belonging to no single game | — | | `dade monopoly08` | _Monopoly_ (2008, multi-platform) | Electronic Arts | | `dade rbplus` | _REFLEC BEAT plus_ (iOS) | Konami | @@ -459,8 +459,8 @@ an asset that will not convert instead of stopping. ## Max Payne ```shell -dade maxpane ras-list MAXPAYNE.ISO -dade maxpane ras-extract MAXPAYNE.ISO -o extracted +dade maxpayne ras-list MAXPAYNE.ISO +dade maxpayne ras-extract MAXPAYNE.ISO -o extracted ``` Readers for the RAS (Remedy Archive System) containers the game loads everything from. The argument @@ -480,7 +480,7 @@ Every member is LZSS-compressed and the archive tables are encrypted, both handl Pass `--raw` to `ras-extract` to keep the `RA->` and `RC->` wrappers. ```shell -dade maxpane inspect-tags extracted/data/database/levels/part1/Part1_Level6.ldb +dade maxpayne inspect-tags extracted/data/database/levels/part1/Part1_Level6.ldb ``` `inspect-tags` decodes the tagged `R_MemoryFile` stream that every custom asset is built from, @@ -488,12 +488,12 @@ naming each value's type. The walk stops where a level leaves tagged territory, first untagged string begins. ```shell -dade maxpane ldb2glb extracted/data/database/levels -o glb -dade maxpane ldb-textures extracted/data/database/levels -o textures +dade maxpayne ldb2glb extracted/data/database/levels -o glb +dade maxpayne ldb-textures extracted/data/database/levels -o textures ``` ```shell -dade maxpane ldb2glb extracted/data/database/levels -D extracted/data/database -o glb +dade maxpayne ldb2glb extracted/data/database/levels -D extracted/data/database -o glb ``` `ldb2glb` converts levels to binary glTF, one `.glb` per `.ldb`, in parallel across every core. @@ -520,7 +520,7 @@ never used, and neither is `dummy`'s, which stays dropped. Graffiti, signage and switchable surfaces come off their walls slightly. A level lays each of them in exactly the plane of what it covers, and nothing in the file marks which is which, because the engine walked its BSP and never drew both at once. A viewer draws the whole level and has only a -depth buffer, so `dade.maxpane.decals` works the layering out from the geometry and lifts each +depth buffer, so `dade.maxpayne.decals` works the layering out from the geometry and lifts each covered face about eight millimetres along its normal. Four things about the format are easy to get backwards. A face's corner count is not its number of diff --git a/dade/main.py b/dade/main.py index 9c181cb..b97977c 100644 --- a/dade/main.py +++ b/dade/main.py @@ -23,7 +23,7 @@ from .incoming.main import main as incoming_extract from .jubeatplus.main import jubeatplus as jubeatplus_group from .marmalade.main import marm as marmalade_group -from .maxpane.main import cli as maxpane_group +from .maxpayne.main import cli as maxpayne_group from .misc.main import misc as misc_group from .monopoly08.main import main as monopoly_extract from .rbplus.main import rbplus as rbplus_group @@ -95,7 +95,7 @@ def main() -> None: main.add_command(_incoming()) main.add_command(jubeatplus_group, name='jubeatplus') main.add_command(marmalade_group, name='marmalade') -main.add_command(maxpane_group, name='maxpane') +main.add_command(maxpayne_group, name='maxpayne') main.add_command(misc_group, name='misc') main.add_command(_monopoly08()) main.add_command(rbplus_group, name='rbplus') diff --git a/dade/maxpane/__init__.py b/dade/maxpayne/__init__.py similarity index 100% rename from dade/maxpane/__init__.py rename to dade/maxpayne/__init__.py diff --git a/dade/maxpane/blocks.py b/dade/maxpayne/blocks.py similarity index 100% rename from dade/maxpane/blocks.py rename to dade/maxpayne/blocks.py diff --git a/dade/maxpane/commands/__init__.py b/dade/maxpayne/commands/__init__.py similarity index 100% rename from dade/maxpane/commands/__init__.py rename to dade/maxpayne/commands/__init__.py diff --git a/dade/maxpane/commands/inspect_tags.py b/dade/maxpayne/commands/inspect_tags.py similarity index 90% rename from dade/maxpane/commands/inspect_tags.py rename to dade/maxpayne/commands/inspect_tags.py index 32ceb82..7d85995 100644 --- a/dade/maxpane/commands/inspect_tags.py +++ b/dade/maxpayne/commands/inspect_tags.py @@ -1,4 +1,4 @@ -"""``dade maxpane inspect-tags`` - summarise the tagged stream inside a Max Payne asset.""" +"""``dade maxpayne inspect-tags`` - summarise the tagged stream inside a Max Payne asset.""" from __future__ import annotations from collections import Counter @@ -7,8 +7,8 @@ import click -from dade.maxpane.blocks import unwrap -from dade.maxpane.memoryfile import BasicType, iter_values +from dade.maxpayne.blocks import unwrap +from dade.maxpayne.memoryfile import BasicType, iter_values from .utils import debug_option diff --git a/dade/maxpane/commands/ldb2glb.py b/dade/maxpayne/commands/ldb2glb.py similarity index 96% rename from dade/maxpane/commands/ldb2glb.py rename to dade/maxpayne/commands/ldb2glb.py index 9417f57..a38aef3 100644 --- a/dade/maxpane/commands/ldb2glb.py +++ b/dade/maxpayne/commands/ldb2glb.py @@ -1,4 +1,4 @@ -"""``dade maxpane ldb2glb`` - convert levels to binary glTF.""" +"""``dade maxpayne ldb2glb`` - convert levels to binary glTF.""" from __future__ import annotations from concurrent.futures import ProcessPoolExecutor @@ -9,9 +9,9 @@ import click from dade.common.workers import default_jobs -from dade.maxpane.blocks import unwrap -from dade.maxpane.gltf import build_glb -from dade.maxpane.ldb import InvalidLevelError, read_level +from dade.maxpayne.blocks import unwrap +from dade.maxpayne.gltf import build_glb +from dade.maxpayne.ldb import InvalidLevelError, read_level from .models import load_models from .utils import debug_option diff --git a/dade/maxpane/commands/ldb_textures.py b/dade/maxpayne/commands/ldb_textures.py similarity index 94% rename from dade/maxpane/commands/ldb_textures.py rename to dade/maxpayne/commands/ldb_textures.py index 85135ca..6a74175 100644 --- a/dade/maxpane/commands/ldb_textures.py +++ b/dade/maxpayne/commands/ldb_textures.py @@ -1,4 +1,4 @@ -"""``dade maxpane ldb-textures`` - write out the images a level embeds.""" +"""``dade maxpayne ldb-textures`` - write out the images a level embeds.""" from __future__ import annotations from pathlib import PurePosixPath, PureWindowsPath @@ -7,8 +7,8 @@ import click -from dade.maxpane.blocks import unwrap -from dade.maxpane.ldb import InvalidLevelError, read_textures +from dade.maxpayne.blocks import unwrap +from dade.maxpayne.ldb import InvalidLevelError, read_textures from .utils import debug_option diff --git a/dade/maxpane/commands/models.py b/dade/maxpayne/commands/models.py similarity index 95% rename from dade/maxpane/commands/models.py rename to dade/maxpayne/commands/models.py index fa8f3fb..e514ef3 100644 --- a/dade/maxpane/commands/models.py +++ b/dade/maxpayne/commands/models.py @@ -5,14 +5,14 @@ import logging import re -from dade.maxpane.blocks import unwrap -from dade.maxpane.model import InvalidModelError, read_model -from dade.maxpane.typing import TextureImage +from dade.maxpayne.blocks import unwrap +from dade.maxpayne.model import InvalidModelError, read_model +from dade.maxpayne.typing import TextureImage if TYPE_CHECKING: from pathlib import Path - from dade.maxpane.typing import Level, Model + from dade.maxpayne.typing import Level, Model __all__ = ('load_models',) diff --git a/dade/maxpane/commands/ras_extract.py b/dade/maxpayne/commands/ras_extract.py similarity index 95% rename from dade/maxpane/commands/ras_extract.py rename to dade/maxpayne/commands/ras_extract.py index 77566c9..ddeca3d 100644 --- a/dade/maxpane/commands/ras_extract.py +++ b/dade/maxpayne/commands/ras_extract.py @@ -1,4 +1,4 @@ -"""``dade maxpane ras-extract`` - unpack a RAS archive, MPM package, or disc image.""" +"""``dade maxpayne ras-extract`` - unpack a RAS archive, MPM package, or disc image.""" from __future__ import annotations from fnmatch import fnmatch @@ -8,7 +8,7 @@ import click from dade.common.exceptions import InvalidFormatError -from dade.maxpane.ras import InvalidArchiveError, member_bytes, read_directory +from dade.maxpayne.ras import InvalidArchiveError, member_bytes, read_directory from .sources import NoArchivesFoundError, iter_archives from .utils import debug_option diff --git a/dade/maxpane/commands/ras_list.py b/dade/maxpayne/commands/ras_list.py similarity index 93% rename from dade/maxpane/commands/ras_list.py rename to dade/maxpayne/commands/ras_list.py index 4245e14..57a70ce 100644 --- a/dade/maxpane/commands/ras_list.py +++ b/dade/maxpayne/commands/ras_list.py @@ -1,4 +1,4 @@ -"""``dade maxpane ras-list`` - show the directory of a RAS archive, MPM package, or disc image.""" +"""``dade maxpayne ras-list`` - show the directory of a RAS archive, MPM package, or disc image.""" from __future__ import annotations from collections import Counter @@ -9,7 +9,7 @@ import click from dade.common.exceptions import InvalidFormatError -from dade.maxpane.ras import InvalidArchiveError, read_directory +from dade.maxpayne.ras import InvalidArchiveError, read_directory from .sources import NoArchivesFoundError, iter_archives from .utils import debug_option diff --git a/dade/maxpane/commands/sources.py b/dade/maxpayne/commands/sources.py similarity index 99% rename from dade/maxpane/commands/sources.py rename to dade/maxpayne/commands/sources.py index 212ea34..224cc12 100644 --- a/dade/maxpane/commands/sources.py +++ b/dade/maxpayne/commands/sources.py @@ -10,7 +10,7 @@ from dade.common.disc import open_image from dade.common.tools import ToolNotFoundError, run_unshield -from dade.maxpane.ras import MAGIC +from dade.maxpayne.ras import MAGIC if TYPE_CHECKING: from collections.abc import Iterator diff --git a/dade/maxpane/commands/utils.py b/dade/maxpayne/commands/utils.py similarity index 96% rename from dade/maxpane/commands/utils.py rename to dade/maxpayne/commands/utils.py index 9260937..936df84 100644 --- a/dade/maxpane/commands/utils.py +++ b/dade/maxpayne/commands/utils.py @@ -5,7 +5,7 @@ __all__ = ('debug_option',) -debug_option = bascom.debug_option({'dade.common': {}, 'dade.maxpane': {}}) +debug_option = bascom.debug_option({'dade.common': {}, 'dade.maxpayne': {}}) """Attach ``-d/--debug`` to a leaf command and route it through :py:func:`bascom.setup_logging`. :meta hide-value: diff --git a/dade/maxpane/crypto.py b/dade/maxpayne/crypto.py similarity index 100% rename from dade/maxpane/crypto.py rename to dade/maxpayne/crypto.py diff --git a/dade/maxpane/decals.py b/dade/maxpayne/decals.py similarity index 87% rename from dade/maxpane/decals.py rename to dade/maxpayne/decals.py index 6a83932..14ccbde 100644 --- a/dade/maxpane/decals.py +++ b/dade/maxpayne/decals.py @@ -5,7 +5,7 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: # pragma: no cover - from collections.abc import Sequence + from collections.abc import Hashable, Sequence from .typing import Vector3 @@ -137,7 +137,7 @@ def _cells(box: Sequence[float]) -> list[tuple[int, int]]: int(box[3] // _CELL) + 1)] -def layer_faces(surfaces: Sequence[tuple[Vector3, Sequence[Vector3]]]) -> list[int]: +def layer_faces(surfaces: Sequence[tuple[Vector3, Sequence[Vector3], Hashable]]) -> list[int]: """ Work out how far off its plane each face has to sit to stop fighting the ones behind it. @@ -150,10 +150,17 @@ def layer_faces(surfaces: Sequence[tuple[Vector3, Sequence[Vector3]]]) -> list[i Faces are considered in order of the area they cover, so the surface underneath keeps the plane the level gave it and only the smaller things laid over it move. + Faces are stacked only when they belong to different surfaces, which is what the key says. + Two faces of one mesh drawing with one material are two pieces of the same thing: a quad split + along its diagonal gives two triangles with the same bounding box, and lifting either off the + other opens a hairline crack down the middle of every table top in the level. Two copies of a + prop, or a tag and the wall behind it, differ in one or the other and do stack. + Parameters ---------- - surfaces : collections.abc.Sequence[tuple[Vector3, collections.abc.Sequence[Vector3]]] - Each face's outward normal and its corners, both in the space they will be drawn in. + surfaces : collections.abc.Sequence + One entry per face: its outward normal, its corners in the space they will be drawn in, + and a hashable key naming the surface it belongs to. Returns ------- @@ -163,7 +170,8 @@ def layer_faces(surfaces: Sequence[tuple[Vector3, Sequence[Vector3]]]) -> list[i """ boxes: dict[int, tuple[float, ...]] = {} planes: defaultdict[tuple[int, ...], list[int]] = defaultdict(list) - for index, (normal, corners) in enumerate(surfaces): + drawn = [key for _normal, _corners, key in surfaces] + for index, (normal, corners, _key) in enumerate(surfaces): found = _profile(normal, corners) if found is not None: planes[found[0]].append(index) @@ -181,7 +189,7 @@ def layer_faces(surfaces: Sequence[tuple[Vector3, Sequence[Vector3]]]) -> list[i below = -1 for square in squares: for other in grid[square]: - if _stacked(box, boxes[other]): + if drawn[other] != drawn[index] and _stacked(box, boxes[other]): below = max(below, out[other]) out[index] = below + 1 for square in squares: diff --git a/dade/maxpane/gltf.py b/dade/maxpayne/gltf.py similarity index 98% rename from dade/maxpane/gltf.py rename to dade/maxpayne/gltf.py index a082412..380859f 100644 --- a/dade/maxpane/gltf.py +++ b/dade/maxpayne/gltf.py @@ -910,7 +910,7 @@ def finish(self, name: str) -> bytes: document: dict[str, Any] = { 'accessors': self.accessors, 'asset': { - 'generator': 'dade maxpane', + 'generator': 'dade maxpayne', 'version': '2.0' }, 'bufferViews': self.views, @@ -1079,7 +1079,7 @@ def _add_static_mesh( Animations the mesh can play. A mesh with any is written so it can be animated. lifts : collections.abc.Mapping[int, int] How far off its plane each face has to sit, in steps of - :py:data:`dade.maxpane.decals.DECAL_STEP`, keyed by the face's index. Faces that stay put + :py:data:`dade.maxpayne.decals.DECAL_STEP`, keyed by the face's index. Faces that stay put may be left out. """ positions: list[Vector3] = [] @@ -1237,7 +1237,7 @@ def _lift_decals(level: Level, containers: Sequence[RenderMesh]) -> list[list[di Whether two surfaces fight depends on where they end up in the scene, not on which mesh they were stored in, so every drawn face is gathered in scene space first and layered together. See - :py:func:`dade.maxpane.decals.layer_faces` for what the layering means. + :py:func:`dade.maxpayne.decals.layer_faces` for what the layering means. Parameters ---------- @@ -1250,10 +1250,10 @@ def _lift_decals(level: Level, containers: Sequence[RenderMesh]) -> list[list[di ------- list[list[dict[int, int]]] One entry per container, one per mesh within it, mapping a face's index to how many steps - of :py:data:`dade.maxpane.decals.DECAL_STEP` it has to rise. Faces that stay put are left + of :py:data:`dade.maxpayne.decals.DECAL_STEP` it has to rise. Faces that stay put are left out. """ - surfaces: list[tuple[Vector3, list[Vector3]]] = [] + surfaces: list[tuple[Vector3, list[Vector3], tuple[int, int, int]]] = [] origins: list[tuple[int, int, int]] = [] for group, container in enumerate(containers): for index, mesh in enumerate(container.meshes): @@ -1269,7 +1269,7 @@ def _lift_decals(level: Level, containers: Sequence[RenderMesh]) -> list[list[di surfaces.append((_turn(matrix, _mirror(face.normal)), [ _place(matrix, _mirror(mesh.positions[corner.position])) for corner in container.corners[face.first_corner:end] - ])) + ], (group, index, face.material))) origins.append((group, index, at)) out: list[list[dict[int, int]]] = [[{} for _ in c.meshes] for c in containers] for layer, (group, index, at) in zip(layer_faces(surfaces), origins, strict=True): @@ -1288,7 +1288,7 @@ def build_glb(level: Level, Parameters ---------- level : Level - A level from :py:func:`dade.maxpane.ldb.read_level`. + A level from :py:func:`dade.maxpayne.ldb.read_level`. name : str Name given to the scene and its nodes. models : collections.abc.Mapping[str, Model] | None diff --git a/dade/maxpane/ldb.py b/dade/maxpayne/ldb.py similarity index 100% rename from dade/maxpane/ldb.py rename to dade/maxpayne/ldb.py diff --git a/dade/maxpane/main.py b/dade/maxpayne/main.py similarity index 87% rename from dade/maxpane/main.py rename to dade/maxpayne/main.py index b7497c6..bef3ae8 100644 --- a/dade/maxpane/main.py +++ b/dade/maxpayne/main.py @@ -12,7 +12,7 @@ __all__ = ('cli',) -@click.group(name='maxpane', context_settings={'help_option_names': ('-h', '--help')}) +@click.group(name='maxpayne', context_settings={'help_option_names': ('-h', '--help')}) def cli() -> None: """Extract and decode Max Payne (Remedy Entertainment) assets.""" diff --git a/dade/maxpane/memoryfile.py b/dade/maxpayne/memoryfile.py similarity index 100% rename from dade/maxpane/memoryfile.py rename to dade/maxpayne/memoryfile.py diff --git a/dade/maxpane/model.py b/dade/maxpayne/model.py similarity index 100% rename from dade/maxpane/model.py rename to dade/maxpayne/model.py diff --git a/dade/maxpane/ras.py b/dade/maxpayne/ras.py similarity index 100% rename from dade/maxpane/ras.py rename to dade/maxpayne/ras.py diff --git a/dade/maxpane/typing.py b/dade/maxpayne/typing.py similarity index 98% rename from dade/maxpane/typing.py rename to dade/maxpayne/typing.py index 8e66130..119c1fa 100644 --- a/dade/maxpane/typing.py +++ b/dade/maxpayne/typing.py @@ -1,4 +1,4 @@ -"""Typed data structures shared across :py:mod:`dade.maxpane`.""" +"""Typed data structures shared across :py:mod:`dade.maxpayne`.""" from __future__ import annotations from typing import NamedTuple, TypeAlias @@ -95,7 +95,7 @@ class Polygon(NamedTuple): mesh_index: int """Index into a smaller table that is not decoded yet, also provisionally named. It takes 5 values on ``Part1_Level6.ldb`` and 39 on ``Part1_Level1.ldb``, splitting a level into a handful - of large face sets, which is what :py:func:`dade.maxpane.gltf.build_glb` groups primitives + of large face sets, which is what :py:func:`dade.maxpayne.gltf.build_glb` groups primitives by.""" normal: Vector3 """Outward face normal.""" diff --git a/docs/api/index.rst b/docs/api/index.rst index 8fa946f..db26064 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -25,7 +25,7 @@ and :doc:`../utilities`. incoming jubeatplus marmalade - maxpane + maxpayne monopoly08 rbplus rhythmin diff --git a/docs/api/maxpane.rst b/docs/api/maxpane.rst deleted file mode 100644 index 5345087..0000000 --- a/docs/api/maxpane.rst +++ /dev/null @@ -1,60 +0,0 @@ -Max Payne -========= - -Readers for the formats Remedy Entertainment built on its ``rl`` library for *Max Payne*: the RAS -archives and MPM mod packages, the seeded stream cipher and LZSS block wrappers guarding them, the -tagged ``R_MemoryFile`` streams every custom asset is written as, and the levels themselves. - -``dade.maxpane.blocks`` ------------------------ - -.. automodule:: dade.maxpane.blocks - :members: - -``dade.maxpane.crypto`` ------------------------ - -.. automodule:: dade.maxpane.crypto - :members: - -``dade.maxpane.decals`` ------------------------ - -.. automodule:: dade.maxpane.decals - :members: - -``dade.maxpane.gltf`` ---------------------- - -.. automodule:: dade.maxpane.gltf - :members: - -``dade.maxpane.ldb`` --------------------- - -.. automodule:: dade.maxpane.ldb - :members: - -``dade.maxpane.memoryfile`` ---------------------------- - -.. automodule:: dade.maxpane.memoryfile - :members: - -``dade.maxpane.model`` ----------------------- - -.. automodule:: dade.maxpane.model - :members: - -``dade.maxpane.ras`` --------------------- - -.. automodule:: dade.maxpane.ras - :members: - -``dade.maxpane.typing`` ------------------------ - -.. automodule:: dade.maxpane.typing - :members: diff --git a/docs/api/maxpayne.rst b/docs/api/maxpayne.rst new file mode 100644 index 0000000..710c77f --- /dev/null +++ b/docs/api/maxpayne.rst @@ -0,0 +1,60 @@ +Max Payne +========= + +Readers for the formats Remedy Entertainment built on its ``rl`` library for *Max Payne*: the RAS +archives and MPM mod packages, the seeded stream cipher and LZSS block wrappers guarding them, the +tagged ``R_MemoryFile`` streams every custom asset is written as, and the levels themselves. + +``dade.maxpayne.blocks`` +------------------------ + +.. automodule:: dade.maxpayne.blocks + :members: + +``dade.maxpayne.crypto`` +------------------------ + +.. automodule:: dade.maxpayne.crypto + :members: + +``dade.maxpayne.decals`` +------------------------ + +.. automodule:: dade.maxpayne.decals + :members: + +``dade.maxpayne.gltf`` +---------------------- + +.. automodule:: dade.maxpayne.gltf + :members: + +``dade.maxpayne.ldb`` +--------------------- + +.. automodule:: dade.maxpayne.ldb + :members: + +``dade.maxpayne.memoryfile`` +---------------------------- + +.. automodule:: dade.maxpayne.memoryfile + :members: + +``dade.maxpayne.model`` +----------------------- + +.. automodule:: dade.maxpayne.model + :members: + +``dade.maxpayne.ras`` +--------------------- + +.. automodule:: dade.maxpayne.ras + :members: + +``dade.maxpayne.typing`` +------------------------ + +.. automodule:: dade.maxpayne.typing + :members: diff --git a/tests/maxpane/conftest.py b/tests/maxpayne/conftest.py similarity index 99% rename from tests/maxpane/conftest.py rename to tests/maxpayne/conftest.py index 0546dc9..6e6ccbf 100644 --- a/tests/maxpane/conftest.py +++ b/tests/maxpayne/conftest.py @@ -9,7 +9,7 @@ import pytest -from dade.maxpane.crypto import next_seed +from dade.maxpayne.crypto import next_seed if TYPE_CHECKING: from collections.abc import Callable, Sequence @@ -23,7 +23,7 @@ def _encrypt(data: bytes, seed: int) -> bytes: """ - Invert :py:func:`dade.maxpane.crypto.decrypt` so tests can build archives. + Invert :py:func:`dade.maxpayne.crypto.decrypt` so tests can build archives. Parameters ---------- diff --git a/tests/maxpane/test_blocks.py b/tests/maxpayne/test_blocks.py similarity index 98% rename from tests/maxpane/test_blocks.py rename to tests/maxpayne/test_blocks.py index d50f625..50c72fb 100644 --- a/tests/maxpane/test_blocks.py +++ b/tests/maxpayne/test_blocks.py @@ -5,7 +5,7 @@ import pytest -from dade.maxpane.blocks import ( +from dade.maxpayne.blocks import ( RING_FILL, decompress, decrypt_block, diff --git a/tests/maxpane/test_commands.py b/tests/maxpayne/test_commands.py similarity index 94% rename from tests/maxpane/test_commands.py rename to tests/maxpayne/test_commands.py index 87afafd..1dfd8fb 100644 --- a/tests/maxpane/test_commands.py +++ b/tests/maxpayne/test_commands.py @@ -8,14 +8,14 @@ import pytest from dade.common.tools import ToolNotFoundError -from dade.maxpane.commands.inspect_tags import inspect_tags -from dade.maxpane.commands.ldb2glb import ldb2glb -from dade.maxpane.commands.ldb_textures import ldb_textures -from dade.maxpane.commands.ras_extract import ras_extract -from dade.maxpane.commands.ras_list import ras_list -from dade.maxpane.commands.sources import NoArchivesFoundError, iter_archives -from dade.maxpane.main import cli -from dade.maxpane.memoryfile import BasicType +from dade.maxpayne.commands.inspect_tags import inspect_tags +from dade.maxpayne.commands.ldb2glb import ldb2glb +from dade.maxpayne.commands.ldb_textures import ldb_textures +from dade.maxpayne.commands.ras_extract import ras_extract +from dade.maxpayne.commands.ras_list import ras_list +from dade.maxpayne.commands.sources import NoArchivesFoundError, iter_archives +from dade.maxpayne.main import cli +from dade.maxpayne.memoryfile import BasicType if TYPE_CHECKING: from collections.abc import Callable, Iterator, Mapping @@ -182,7 +182,7 @@ def test_iter_archives_raises_when_nothing_is_found(tmp_path: Path) -> None: def test_iter_archives_reads_loose_archives_from_an_image(tmp_path: Path, mocker: MockerFixture, make_ras: Callable[..., bytes]) -> None: archive = make_ras() - mocker.patch('dade.maxpane.commands.sources.open_image', + mocker.patch('dade.maxpayne.commands.sources.open_image', return_value=_StubImage({ 'DISK1/LEVELS/X_LEVEL1.RAS': archive, 'DISK1/SETUP.EXE': b'stub' @@ -200,8 +200,8 @@ def fake_unshield(cabinet: Path, output_dir: Path) -> None: staged['siblings'] = sorted(path.name for path in cabinet.parent.iterdir()) (output_dir / 'x_data.ras').write_bytes(make_ras()) - mocker.patch('dade.maxpane.commands.sources.run_unshield', side_effect=fake_unshield) - mocker.patch('dade.maxpane.commands.sources.open_image', + mocker.patch('dade.maxpayne.commands.sources.run_unshield', side_effect=fake_unshield) + mocker.patch('dade.maxpayne.commands.sources.open_image', return_value=_StubImage({ 'DISK1/DATA1.CAB': b'ISc(', 'DISK1/DATA1.HDR': b'hdr', @@ -218,7 +218,7 @@ def fake_unshield(cabinet: Path, output_dir: Path) -> None: def test_iter_archives_skips_a_cabinet_without_unshield(tmp_path: Path, mocker: MockerFixture) -> None: - mocker.patch('dade.maxpane.commands.sources.run_unshield', + mocker.patch('dade.maxpayne.commands.sources.run_unshield', side_effect=ToolNotFoundError('missing')) cabinet = tmp_path / 'data1.cab' cabinet.write_bytes(b'ISc(') @@ -228,7 +228,7 @@ def test_iter_archives_skips_a_cabinet_without_unshield(tmp_path: Path, def test_iter_archives_skips_a_cabinet_unshield_cannot_read(tmp_path: Path, mocker: MockerFixture) -> None: - mocker.patch('dade.maxpane.commands.sources.run_unshield', + mocker.patch('dade.maxpayne.commands.sources.run_unshield', side_effect=sp.CalledProcessError(1, 'unshield')) cabinet = tmp_path / 'data1.cab' cabinet.write_bytes(b'ISc(') @@ -241,7 +241,7 @@ def test_iter_archives_reads_a_cabinet(tmp_path: Path, mocker: MockerFixture, def fake_unshield(cabinet: Path, output_dir: Path) -> None: (output_dir / 'x_data.ras').write_bytes(make_ras()) - mocker.patch('dade.maxpane.commands.sources.run_unshield', side_effect=fake_unshield) + mocker.patch('dade.maxpayne.commands.sources.run_unshield', side_effect=fake_unshield) cabinet = tmp_path / 'data1.cab' cabinet.write_bytes(b'ISc(') assert [label for label, _ in iter_archives(cabinet)] == ['x_data.ras'] diff --git a/tests/maxpane/test_crypto.py b/tests/maxpayne/test_crypto.py similarity index 95% rename from tests/maxpane/test_crypto.py rename to tests/maxpayne/test_crypto.py index 1783d38..72a13d6 100644 --- a/tests/maxpane/test_crypto.py +++ b/tests/maxpayne/test_crypto.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING -from dade.maxpane.crypto import decrypt, next_seed +from dade.maxpayne.crypto import decrypt, next_seed if TYPE_CHECKING: from collections.abc import Callable diff --git a/tests/maxpane/test_decals.py b/tests/maxpayne/test_decals.py similarity index 53% rename from tests/maxpane/test_decals.py rename to tests/maxpayne/test_decals.py index 1b8b95c..db0a59b 100644 --- a/tests/maxpane/test_decals.py +++ b/tests/maxpayne/test_decals.py @@ -1,52 +1,65 @@ from __future__ import annotations -from dade.maxpane.decals import layer_faces +from dade.maxpayne.decals import layer_faces _FLOOR = ((0.0, 0.0, 0.0), (8.0, 0.0, 0.0), (8.0, 0.0, 8.0), (0.0, 0.0, 8.0)) _RUG = ((2.0, 0.0, 2.0), (5.0, 0.0, 2.0), (5.0, 0.0, 5.0), (2.0, 0.0, 5.0)) _UP = (0.0, 1.0, 0.0) +_BOARDS = 1 +_WOOL = 2 +_CORK = 3 def test_layer_faces_leaves_a_lone_face_alone() -> None: - assert layer_faces([(_UP, _FLOOR)]) == [0] + assert layer_faces([(_UP, _FLOOR, _BOARDS)]) == [0] def test_layer_faces_lifts_the_smaller_of_two_stacked_faces() -> None: - assert layer_faces([(_UP, _FLOOR), (_UP, _RUG)]) == [0, 1] + assert layer_faces([(_UP, _FLOOR, _BOARDS), (_UP, _RUG, _WOOL)]) == [0, 1] def test_layer_faces_keeps_the_wider_face_on_its_plane_whichever_comes_first() -> None: # The rug is given first, but the floor is what the level built and has to stay put. - assert layer_faces([(_UP, _RUG), (_UP, _FLOOR)]) == [1, 0] + assert layer_faces([(_UP, _RUG, _WOOL), (_UP, _FLOOR, _BOARDS)]) == [1, 0] def test_layer_faces_stacks_three_deep() -> None: coaster = ((3.0, 0.0, 3.0), (4.0, 0.0, 3.0), (4.0, 0.0, 4.0), (3.0, 0.0, 4.0)) - assert layer_faces([(_UP, _FLOOR), (_UP, _RUG), (_UP, coaster)]) == [0, 1, 2] + surfaces = [(_UP, _FLOOR, _BOARDS), (_UP, _RUG, _WOOL), (_UP, coaster, _CORK)] + assert layer_faces(surfaces) == [0, 1, 2] + + +def test_layer_faces_leaves_two_halves_of_one_surface_alone() -> None: + # A quad split along its diagonal gives two triangles with the same bounding box. They draw + # with one material because they are one surface, and lifting either cracks it open. + lower = ((0.0, 0.0, 0.0), (8.0, 0.0, 0.0), (8.0, 0.0, 8.0)) + upper = ((0.0, 0.0, 0.0), (8.0, 0.0, 8.0), (0.0, 0.0, 8.0)) + assert layer_faces([(_UP, lower, _BOARDS), (_UP, upper, _BOARDS)]) == [0, 0] def test_layer_faces_ignores_faces_that_only_meet_along_an_edge() -> None: # Architecture is tiled, so neighbouring wall panels share a seam and nothing more. right = ((8.0, 0.0, 0.0), (16.0, 0.0, 0.0), (16.0, 0.0, 8.0), (8.0, 0.0, 8.0)) - assert layer_faces([(_UP, _FLOOR), (_UP, right)]) == [0, 0] + assert layer_faces([(_UP, _FLOOR, _BOARDS), (_UP, right, _WOOL)]) == [0, 0] def test_layer_faces_ignores_a_face_on_a_different_plane() -> None: ceiling = tuple((x, 4.0, z) for x, _y, z in _RUG) - assert layer_faces([(_UP, _FLOOR), (_UP, ceiling)]) == [0, 0] + assert layer_faces([(_UP, _FLOOR, _BOARDS), (_UP, ceiling, _WOOL)]) == [0, 0] def test_layer_faces_ignores_a_face_turned_the_other_way() -> None: # A viewer never sees both sides of one surface at once, so back to back is not a clash. - assert layer_faces([(_UP, _FLOOR), ((0.0, -1.0, 0.0), _RUG)]) == [0, 0] + assert layer_faces([(_UP, _FLOOR, _BOARDS), ((0.0, -1.0, 0.0), _RUG, _WOOL)]) == [0, 0] def test_layer_faces_skips_a_face_with_no_plane() -> None: - assert layer_faces([((0.0, 0.0, 0.0), _FLOOR), (_UP, ()), (_UP, _FLOOR)]) == [0, 0, 0] + surfaces = [((0.0, 0.0, 0.0), _FLOOR, _BOARDS), (_UP, (), _WOOL), (_UP, _FLOOR, _CORK)] + assert layer_faces(surfaces) == [0, 0, 0] def test_layer_faces_handles_a_wall_that_leans_on_z() -> None: # The in-plane frame picks a different helper axis for a normal that leans on Z. wall = ((0.0, 0.0, 0.0), (0.0, 8.0, 0.0), (8.0, 8.0, 0.0), (8.0, 0.0, 0.0)) tag = ((2.0, 2.0, 0.0), (2.0, 5.0, 0.0), (5.0, 5.0, 0.0), (5.0, 2.0, 0.0)) - assert layer_faces([((0.0, 0.0, 1.0), wall), ((0.0, 0.0, 1.0), tag)]) == [0, 1] + assert layer_faces([((0.0, 0.0, 1.0), wall, _BOARDS), ((0.0, 0.0, 1.0), tag, _WOOL)]) == [0, 1] diff --git a/tests/maxpane/test_gltf.py b/tests/maxpayne/test_gltf.py similarity index 96% rename from tests/maxpane/test_gltf.py rename to tests/maxpayne/test_gltf.py index cf62cb4..f56732c 100644 --- a/tests/maxpane/test_gltf.py +++ b/tests/maxpayne/test_gltf.py @@ -7,10 +7,10 @@ import pytest -from dade.maxpane.decals import DECAL_STEP -from dade.maxpane.gltf import GLB_MAGIC, build_glb -from dade.maxpane.ldb import read_level -from dade.maxpane.typing import Level, LevelGeometry, Model +from dade.maxpayne.decals import DECAL_STEP +from dade.maxpayne.gltf import GLB_MAGIC, build_glb +from dade.maxpayne.ldb import read_level +from dade.maxpayne.typing import Level, LevelGeometry, Model if TYPE_CHECKING: from collections.abc import Callable @@ -324,8 +324,8 @@ def test_build_glb_reuses_one_composed_texture(make_ldb: Callable[..., bytes]) - def _model(make_model: Callable[..., bytes], **kwargs: object) -> Model: - from dade.maxpane.model import read_model - from dade.maxpane.typing import TextureImage + from dade.maxpayne.model import read_model + from dade.maxpayne.typing import TextureImage model = read_model(make_model(**kwargs)) return model._replace(textures=tuple( TextureImage(data=_png((4, 4), (10, 20, 30)), kind=0, path=name) @@ -375,7 +375,7 @@ def test_build_glb_shares_one_model_material_between_placements( def test_build_glb_falls_back_when_a_model_names_no_image(make_ldb: Callable[..., bytes], make_model: Callable[..., bytes]) -> None: - from dade.maxpane.model import read_model + from dade.maxpayne.model import read_model bare = read_model(make_model()) document, _ = _parse(build_glb(read_level(make_ldb()), models={'character:transit_cop': bare})) node = next(n for n in document['nodes'] if n['name'].startswith('character:')) @@ -385,7 +385,7 @@ def test_build_glb_falls_back_when_a_model_names_no_image(make_ldb: Callable[... def test_build_glb_names_each_mesh_of_a_multipart_model(make_ldb: Callable[..., bytes], make_model: Callable[..., bytes]) -> None: - from dade.maxpane.model import read_model + from dade.maxpayne.model import read_model one = read_model(make_model()) both = one._replace(meshes=one.meshes + one.meshes) document, _ = _parse(build_glb(read_level(make_ldb()), models={'character:transit_cop': both})) @@ -395,7 +395,7 @@ def test_build_glb_names_each_mesh_of_a_multipart_model(make_ldb: Callable[..., def test_build_glb_skips_a_models_empty_mesh(make_ldb: Callable[..., bytes], make_model: Callable[..., bytes]) -> None: - from dade.maxpane.model import read_model + from dade.maxpayne.model import read_model one = read_model(make_model()) hollow = read_model(make_model(faces=(), coord_faces=(), face_materials=())) mixed = one._replace(meshes=one.meshes + hollow.meshes) @@ -585,15 +585,18 @@ def test_build_glb_skips_an_undecodable_atlas(make_ldb: Callable[..., bytes]) -> def test_build_glb_lifts_a_face_off_the_one_it_covers(make_ldb: Callable[..., bytes]) -> None: - # Both triangles lie in one plane over the same ground, the way a level lays graffiti on a - # wall, so the second has to come off it or a depth buffer cannot tell which is in front. - document, binary = _parse(build_glb(read_level(make_ldb(face_materials=(7,), - layout='stacked')))) - position = document['meshes'][0]['primitives'][0]['attributes']['POSITION'] - raw = _accessor_bytes(document, binary, position) - heights = [struct.unpack_from('<3f', raw, corner * 12)[1] for corner in range(6)] - assert heights[:3] == [0.0, 0.0, 0.0] - assert heights[3:] == pytest.approx([DECAL_STEP] * 3) + # The triangles lie in one plane over the same ground, the way a level lays graffiti on a + # wall, and draw with different materials, so one has to come off the other or a depth buffer + # cannot tell which is in front. + document, binary = _parse( + build_glb(read_level(make_ldb(face_materials=(7, 9), layout='stacked')))) + heights: set[float] = set() + for primitive in document['meshes'][0]['primitives']: + raw = _accessor_bytes(document, binary, primitive['attributes']['POSITION']) + heights.update( + struct.unpack_from('<3f', raw, corner * 12)[1] for corner in range(len(raw) // 12)) + # One face keeps the plane and each face laid over it rises another step. + assert sorted(heights)[:3] == pytest.approx([0.0, DECAL_STEP, 2 * DECAL_STEP]) def test_build_glb_leaves_a_face_nothing_covers_alone(make_ldb: Callable[..., bytes]) -> None: diff --git a/tests/maxpane/test_ldb.py b/tests/maxpayne/test_ldb.py similarity index 99% rename from tests/maxpane/test_ldb.py rename to tests/maxpayne/test_ldb.py index cc4f005..ad9e5ad 100644 --- a/tests/maxpane/test_ldb.py +++ b/tests/maxpayne/test_ldb.py @@ -5,7 +5,7 @@ import pytest -from dade.maxpane.ldb import InvalidLevelError, read_geometry, read_level, read_textures +from dade.maxpayne.ldb import InvalidLevelError, read_geometry, read_level, read_textures if TYPE_CHECKING: from collections.abc import Callable diff --git a/tests/maxpane/test_memoryfile.py b/tests/maxpayne/test_memoryfile.py similarity index 99% rename from tests/maxpane/test_memoryfile.py rename to tests/maxpayne/test_memoryfile.py index c1eb903..9a649f6 100644 --- a/tests/maxpane/test_memoryfile.py +++ b/tests/maxpayne/test_memoryfile.py @@ -4,7 +4,7 @@ import pytest -from dade.maxpane.memoryfile import ( +from dade.maxpayne.memoryfile import ( TAG_SIZES, BasicType, iter_values, diff --git a/tests/maxpane/test_model.py b/tests/maxpayne/test_model.py similarity index 99% rename from tests/maxpane/test_model.py rename to tests/maxpayne/test_model.py index 92b3396..f3620c8 100644 --- a/tests/maxpane/test_model.py +++ b/tests/maxpayne/test_model.py @@ -5,7 +5,7 @@ import pytest -from dade.maxpane.model import InvalidModelError, read_model +from dade.maxpayne.model import InvalidModelError, read_model if TYPE_CHECKING: from collections.abc import Callable diff --git a/tests/maxpane/test_models_command.py b/tests/maxpayne/test_models_command.py similarity index 98% rename from tests/maxpane/test_models_command.py rename to tests/maxpayne/test_models_command.py index 9cec879..f52de23 100644 --- a/tests/maxpane/test_models_command.py +++ b/tests/maxpayne/test_models_command.py @@ -3,8 +3,8 @@ from typing import TYPE_CHECKING -from dade.maxpane.commands.models import load_models -from dade.maxpane.ldb import read_level +from dade.maxpayne.commands.models import load_models +from dade.maxpayne.ldb import read_level if TYPE_CHECKING: from collections.abc import Callable diff --git a/tests/maxpane/test_ras.py b/tests/maxpayne/test_ras.py similarity index 98% rename from tests/maxpane/test_ras.py rename to tests/maxpayne/test_ras.py index b9d1f8f..0df8c77 100644 --- a/tests/maxpane/test_ras.py +++ b/tests/maxpayne/test_ras.py @@ -5,7 +5,7 @@ import pytest -from dade.maxpane.ras import ( +from dade.maxpayne.ras import ( HEADER_SIZE, InvalidArchiveError, is_intact, @@ -94,7 +94,7 @@ def test_read_header_reads_the_table_checksums(make_ras: Callable[..., bytes]) - # on all five shipped archives. import zlib - from dade.maxpane.crypto import decrypt + from dade.maxpayne.crypto import decrypt archive = make_ras() header = read_header(archive) start = 0x2C diff --git a/tests/test_cli.py b/tests/test_cli.py index aba9bfa..20014f4 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -20,7 +20,7 @@ 'incoming', 'jubeatplus', 'marmalade', - 'maxpane', + 'maxpayne', 'misc', 'monopoly08', 'rbplus',