Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .vscode/dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,6 @@ marm
maskable
maxcolors
maxidx
maxpane
maxpayne
mbin
medi
Expand Down
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ Run `dade --help` to list the games, and `dade <game> --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 |
Expand Down Expand Up @@ -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
Expand All @@ -480,20 +480,20 @@ 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,
naming each value's type. The walk stops where a level leaves tagged territory, which is where its
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.
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions dade/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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')
Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',)

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
File renamed without changes.
20 changes: 14 additions & 6 deletions dade/maxpane/decals.py → dade/maxpayne/decals.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.

Expand All @@ -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
-------
Expand All @@ -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)
Expand All @@ -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:
Expand Down
14 changes: 7 additions & 7 deletions dade/maxpane/gltf.py → dade/maxpayne/gltf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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] = []
Expand Down Expand Up @@ -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
----------
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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
Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion dade/maxpane/main.py → dade/maxpayne/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
4 changes: 2 additions & 2 deletions dade/maxpane/typing.py → dade/maxpayne/typing.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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."""
Expand Down
2 changes: 1 addition & 1 deletion docs/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ and :doc:`../utilities`.
incoming
jubeatplus
marmalade
maxpane
maxpayne
monopoly08
rbplus
rhythmin
Expand Down
Loading