OpenRCT2-ObjectCommon 0.1.3__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/PKG-INFO +5 -4
- {openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/README.md +3 -2
- {openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/openrct2_object_common/blender/lights.py +1 -1
- {openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/openrct2_object_common/blender/mesh_extract.py +1 -2
- {openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/openrct2_object_common/parkobj.py +7 -0
- {openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/openrct2_object_common/placement.py +10 -11
- {openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/pyproject.toml +4 -4
- {openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/tests/test_parkobj.py +21 -0
- {openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/tests/test_placement.py +32 -6
- {openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/.github/workflows/lint.yml +0 -0
- {openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/.github/workflows/publish.yml +0 -0
- {openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/.github/workflows/pytest.yml +0 -0
- {openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/.gitignore +0 -0
- {openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/.yamllint.yaml +0 -0
- {openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/LICENSE +0 -0
- {openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/openrct2_object_common/__init__.py +0 -0
- {openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/openrct2_object_common/blender/__init__.py +0 -0
- {openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/openrct2_object_common/blender/modal.py +0 -0
- {openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/openrct2_object_common/blender/props.py +0 -0
- {openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/openrct2_object_common/cli.py +0 -0
- {openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/openrct2_object_common/config.py +0 -0
- {openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/openrct2_object_common/objectjson.py +0 -0
- {openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/openrct2_object_common/py.typed +0 -0
- {openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/openrct2_object_common/testing.py +0 -0
- {openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/scripts/ci/set_version.py +0 -0
- {openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/tests/conftest.py +0 -0
- {openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/tests/test_blender.py +0 -0
- {openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/tests/test_blender_shared.py +0 -0
- {openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/tests/test_cli.py +0 -0
- {openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/tests/test_config.py +0 -0
- {openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/tests/test_objectjson.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: OpenRCT2-ObjectCommon
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Shared config, CLI, .parkobj, and object.json scaffolding for OpenRCT2 object generators.
|
|
5
5
|
Project-URL: Homepage, https://github.com/alex-parisi/OpenRCT2-ObjectCommon
|
|
6
6
|
Project-URL: Repository, https://github.com/alex-parisi/OpenRCT2-ObjectCommon
|
|
@@ -9,7 +9,7 @@ License-Expression: GPL-3.0-or-later
|
|
|
9
9
|
License-File: LICENSE
|
|
10
10
|
Requires-Python: >=3.11
|
|
11
11
|
Requires-Dist: numpy>=1.26
|
|
12
|
-
Requires-Dist: openrct2-x7-renderer>=0.3.
|
|
12
|
+
Requires-Dist: openrct2-x7-renderer>=0.3.4
|
|
13
13
|
Requires-Dist: pillow>=10.0
|
|
14
14
|
Requires-Dist: pyyaml>=6.0
|
|
15
15
|
Provides-Extra: blender
|
|
@@ -109,8 +109,9 @@ def export_to(obj, context, parkobj_path, work_dir, skip_render=False):
|
|
|
109
109
|
|
|
110
110
|
### Blender helpers
|
|
111
111
|
|
|
112
|
-
The `blender` subpackage imports `bpy` (
|
|
113
|
-
does not), so install the extra when
|
|
112
|
+
The `blender` subpackage imports `bpy` (`blender.modal`, `blender.props`, and
|
|
113
|
+
`blender.mesh_extract` — `blender.lights` does not), so install the extra when
|
|
114
|
+
working with it outside Blender:
|
|
114
115
|
|
|
115
116
|
```bash
|
|
116
117
|
pip install "OpenRCT2-ObjectCommon[blender]"
|
|
@@ -91,8 +91,9 @@ def export_to(obj, context, parkobj_path, work_dir, skip_render=False):
|
|
|
91
91
|
|
|
92
92
|
### Blender helpers
|
|
93
93
|
|
|
94
|
-
The `blender` subpackage imports `bpy` (
|
|
95
|
-
does not), so install the extra when
|
|
94
|
+
The `blender` subpackage imports `bpy` (`blender.modal`, `blender.props`, and
|
|
95
|
+
`blender.mesh_extract` — `blender.lights` does not), so install the extra when
|
|
96
|
+
working with it outside Blender:
|
|
96
97
|
|
|
97
98
|
```bash
|
|
98
99
|
pip install "OpenRCT2-ObjectCommon[blender]"
|
{openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/openrct2_object_common/blender/lights.py
RENAMED
|
@@ -43,7 +43,7 @@ def lights_from_items(items: Iterable[_LightItem]) -> list[Light]:
|
|
|
43
43
|
rig = [
|
|
44
44
|
Light(
|
|
45
45
|
type=LIGHT_TYPE_MAP[item.type],
|
|
46
|
-
shadow=
|
|
46
|
+
shadow=bool(item.shadow),
|
|
47
47
|
direction=normalize_direction(list(item.direction)),
|
|
48
48
|
intensity=item.strength,
|
|
49
49
|
)
|
|
@@ -16,6 +16,7 @@ from collections.abc import Callable
|
|
|
16
16
|
import bpy
|
|
17
17
|
import numpy as np
|
|
18
18
|
from mathutils import Matrix, Vector
|
|
19
|
+
from openrct2_x7_renderer.constants import MaterialFlag
|
|
19
20
|
from openrct2_x7_renderer.image import quantize_to_indexed, read_png
|
|
20
21
|
from openrct2_x7_renderer.mesh import Material, Mesh
|
|
21
22
|
from openrct2_x7_renderer.types import IndexedImage
|
|
@@ -65,8 +66,6 @@ def material_base(
|
|
|
65
66
|
*prop_attr*: attribute name on the bpy material (e.g. ``"vg_material"``).
|
|
66
67
|
*region_map*: maps region enum string → ``(flag_bits, region_id)``.
|
|
67
68
|
"""
|
|
68
|
-
from openrct2_x7_renderer.constants import MaterialFlag
|
|
69
|
-
|
|
70
69
|
m = Material()
|
|
71
70
|
if bmat is None:
|
|
72
71
|
return m, None
|
{openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/openrct2_object_common/parkobj.py
RENAMED
|
@@ -73,6 +73,8 @@ def write_images_dat_lgx(
|
|
|
73
73
|
``note`` is appended to the log line (e.g. ``" for 4 tiles"``). Returns the
|
|
74
74
|
single-element ``["$LGX:images.dat[0..N-1]"]`` list OpenRCT2 expects.
|
|
75
75
|
"""
|
|
76
|
+
if not images:
|
|
77
|
+
raise ValueError("Cannot write images.dat with no sprites")
|
|
76
78
|
out_path = work_dir / "images.dat"
|
|
77
79
|
write_images_dat(images, out_path)
|
|
78
80
|
log.info(
|
|
@@ -123,6 +125,11 @@ def assemble_parkobj(
|
|
|
123
125
|
parkobj_path.parent.mkdir(parents=True, exist_ok=True)
|
|
124
126
|
tmp_fd, tmp_path = tempfile.mkstemp(suffix=".parkobj", dir=parkobj_path.parent)
|
|
125
127
|
os.close(tmp_fd)
|
|
128
|
+
# mkstemp creates the file 0o600; widen to the umask default so the
|
|
129
|
+
# delivered .parkobj has normal file permissions after os.replace.
|
|
130
|
+
umask = os.umask(0)
|
|
131
|
+
os.umask(umask)
|
|
132
|
+
os.chmod(tmp_path, 0o666 & ~umask)
|
|
126
133
|
try:
|
|
127
134
|
with zipfile.ZipFile(tmp_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
|
|
128
135
|
zf.write(work_dir / "object.json", "object.json")
|
{openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/openrct2_object_common/placement.py
RENAMED
|
@@ -13,7 +13,7 @@ import math
|
|
|
13
13
|
|
|
14
14
|
import numpy as np
|
|
15
15
|
from numpy.typing import NDArray
|
|
16
|
-
from openrct2_x7_renderer.geometry import rotate_x, rotate_y, rotate_z
|
|
16
|
+
from openrct2_x7_renderer.geometry import rotate_x, rotate_y, rotate_z, split_mesh_by_ghost
|
|
17
17
|
from openrct2_x7_renderer.mesh import Mesh
|
|
18
18
|
from openrct2_x7_renderer.ray_trace import SceneBuilder
|
|
19
19
|
from openrct2_x7_renderer.types import Model
|
|
@@ -24,8 +24,8 @@ __all__ = ["add_model_to_scene", "orientation_to_matrix"]
|
|
|
24
24
|
def orientation_to_matrix(orientation_deg: NDArray[np.float64]) -> NDArray[np.float64]:
|
|
25
25
|
"""A MeshFrame orientation ``(angle_y, angle_z, angle_x)`` in degrees as a
|
|
26
26
|
``(3, 3)`` rotation matrix, applied as ``rotate_y @ rotate_z @ rotate_x``."""
|
|
27
|
-
|
|
28
|
-
return rotate_y(
|
|
27
|
+
ry, rz, rx = orientation_deg * (math.pi / 180.0)
|
|
28
|
+
return rotate_y(ry) @ rotate_z(rz) @ rotate_x(rx)
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
def add_model_to_scene(
|
|
@@ -42,17 +42,16 @@ def add_model_to_scene(
|
|
|
42
42
|
Placements whose ``mesh_index`` is ``-1`` (an empty slot) are skipped. With
|
|
43
43
|
``clamp_frame=True`` a placement with fewer frames falls back to its last
|
|
44
44
|
frame (scenery's animated poses); with ``False`` the frame is indexed exactly
|
|
45
|
-
(vehicle frames are uniform across placements). ``mask`` is the per-model
|
|
46
|
-
``MeshFlag`` bitmask (e.g. ghost / mask geometry)
|
|
45
|
+
(vehicle frames are uniform across placements). ``mask`` is the base per-model
|
|
46
|
+
``MeshFlag`` bitmask (e.g. ghost / mask geometry); faces whose material is a
|
|
47
|
+
ghost are additionally split into their own ``MeshFlag.GHOST`` model.
|
|
47
48
|
"""
|
|
48
49
|
for mesh_frames in model.meshes:
|
|
49
50
|
idx = min(frame, len(mesh_frames) - 1) if clamp_frame else frame
|
|
50
51
|
mf = mesh_frames[idx]
|
|
51
52
|
if mf.mesh_index == -1:
|
|
52
53
|
continue
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
mask,
|
|
58
|
-
)
|
|
54
|
+
matrix = orientation_to_matrix(mf.orientation)
|
|
55
|
+
position = mf.position.astype(np.float64)
|
|
56
|
+
for sub_mesh, sub_mask in split_mesh_by_ghost(meshes[mf.mesh_index], mask):
|
|
57
|
+
builder.add_model(sub_mesh, matrix, position, sub_mask)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "OpenRCT2-ObjectCommon"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "Shared config, CLI, .parkobj, and object.json scaffolding for OpenRCT2 object generators."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -9,7 +9,7 @@ authors = [
|
|
|
9
9
|
{ name = "Alex Parisi", email = "alex@atparisi.com" }
|
|
10
10
|
]
|
|
11
11
|
dependencies = [
|
|
12
|
-
"openrct2-x7-renderer>=0.3.
|
|
12
|
+
"openrct2-x7-renderer>=0.3.4",
|
|
13
13
|
"numpy>=1.26",
|
|
14
14
|
"pillow>=10.0",
|
|
15
15
|
"pyyaml>=6.0",
|
|
@@ -19,8 +19,8 @@ dependencies = [
|
|
|
19
19
|
Homepage = "https://github.com/alex-parisi/OpenRCT2-ObjectCommon"
|
|
20
20
|
Repository = "https://github.com/alex-parisi/OpenRCT2-ObjectCommon"
|
|
21
21
|
|
|
22
|
-
# The Blender helpers (openrct2_object_common.blender.
|
|
23
|
-
# of the package does not. Only the add-ons need this extra.
|
|
22
|
+
# The Blender helpers (openrct2_object_common.blender, except .lights) import
|
|
23
|
+
# bpy; the rest of the package does not. Only the add-ons need this extra.
|
|
24
24
|
[project.optional-dependencies]
|
|
25
25
|
blender = ["bpy>=4.0"]
|
|
26
26
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Tests for .parkobj assembly and the images.dat helper."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import os
|
|
4
5
|
import zipfile
|
|
5
6
|
|
|
6
7
|
import numpy as np
|
|
@@ -26,6 +27,13 @@ def test_write_images_dat_lgx_writes_blob_and_ref(tmp_path):
|
|
|
26
27
|
assert (tmp_path / "images.dat").exists()
|
|
27
28
|
|
|
28
29
|
|
|
30
|
+
def test_write_images_dat_lgx_rejects_empty_list(tmp_path):
|
|
31
|
+
# An empty list would otherwise emit a malformed "$LGX:images.dat[0..-1]".
|
|
32
|
+
with pytest.raises(ValueError, match="no sprites"):
|
|
33
|
+
write_images_dat_lgx([], tmp_path)
|
|
34
|
+
assert not (tmp_path / "images.dat").exists()
|
|
35
|
+
|
|
36
|
+
|
|
29
37
|
def test_assemble_renders_and_zips(tmp_path):
|
|
30
38
|
work = tmp_path / "object"
|
|
31
39
|
parkobj = tmp_path / "out" / "thing.parkobj"
|
|
@@ -58,6 +66,19 @@ def test_assemble_cleans_up_temp_on_zip_failure(tmp_path):
|
|
|
58
66
|
assert list(out.glob("*.parkobj")) == []
|
|
59
67
|
|
|
60
68
|
|
|
69
|
+
def test_assemble_parkobj_respects_umask(tmp_path):
|
|
70
|
+
# The temp file behind the atomic replace is mkstemp'd (0o600); the final
|
|
71
|
+
# .parkobj must carry normal umask-derived permissions instead.
|
|
72
|
+
work = tmp_path / "object"
|
|
73
|
+
parkobj = tmp_path / "thing.parkobj"
|
|
74
|
+
old_umask = os.umask(0o022)
|
|
75
|
+
try:
|
|
76
|
+
assemble_parkobj({"id": "rct2.thing"}, parkobj, work, _render_two_pixels)
|
|
77
|
+
finally:
|
|
78
|
+
os.umask(old_umask)
|
|
79
|
+
assert parkobj.stat().st_mode & 0o777 == 0o644
|
|
80
|
+
|
|
81
|
+
|
|
61
82
|
def test_skip_render_reuses_previous_images(tmp_path):
|
|
62
83
|
work = tmp_path / "object"
|
|
63
84
|
work.mkdir()
|
|
@@ -7,6 +7,8 @@ scene is needed.
|
|
|
7
7
|
|
|
8
8
|
import numpy as np
|
|
9
9
|
from openrct2_object_common.placement import add_model_to_scene, orientation_to_matrix
|
|
10
|
+
from openrct2_x7_renderer.constants import MeshFlag
|
|
11
|
+
from openrct2_x7_renderer.mesh import Material, Mesh
|
|
10
12
|
from openrct2_x7_renderer.types import MeshFrame, Model
|
|
11
13
|
|
|
12
14
|
|
|
@@ -20,6 +22,18 @@ class _StubBuilder:
|
|
|
20
22
|
self.calls.append((mesh, matrix, translation, mask))
|
|
21
23
|
|
|
22
24
|
|
|
25
|
+
def _mesh(*, is_ghost: bool = False) -> Mesh:
|
|
26
|
+
"""A one-triangle mesh with a single (optionally ghost) material."""
|
|
27
|
+
return Mesh(
|
|
28
|
+
vertices=np.zeros((3, 3), dtype=np.float32),
|
|
29
|
+
normals=np.zeros((3, 3), dtype=np.float32),
|
|
30
|
+
uvs=np.zeros((3, 2), dtype=np.float32),
|
|
31
|
+
faces=np.array([[0, 1, 2]], dtype=np.uint32),
|
|
32
|
+
face_materials=np.array([0], dtype=np.uint32),
|
|
33
|
+
materials=[Material(is_ghost=is_ghost)],
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
23
37
|
def test_orientation_to_matrix_zero_is_identity():
|
|
24
38
|
m = orientation_to_matrix(np.zeros(3, dtype=np.float64))
|
|
25
39
|
assert m.shape == (3, 3)
|
|
@@ -33,7 +47,7 @@ def test_orientation_to_matrix_is_orthonormal():
|
|
|
33
47
|
|
|
34
48
|
|
|
35
49
|
def test_add_model_places_each_referenced_mesh():
|
|
36
|
-
meshes = [
|
|
50
|
+
meshes = [_mesh(), _mesh()]
|
|
37
51
|
model = Model(meshes=[
|
|
38
52
|
[MeshFrame(mesh_index=0, position=np.array([1.0, 2.0, 3.0]))],
|
|
39
53
|
[MeshFrame(mesh_index=1)],
|
|
@@ -41,21 +55,33 @@ def test_add_model_places_each_referenced_mesh():
|
|
|
41
55
|
builder = _StubBuilder()
|
|
42
56
|
add_model_to_scene(builder, meshes, model, mask=7)
|
|
43
57
|
|
|
44
|
-
assert [c[0] for c in builder.calls] ==
|
|
58
|
+
assert [c[0] for c in builder.calls] == meshes
|
|
45
59
|
np.testing.assert_allclose(builder.calls[0][2], [1.0, 2.0, 3.0])
|
|
46
60
|
assert all(c[3] == 7 for c in builder.calls)
|
|
47
61
|
|
|
48
62
|
|
|
49
63
|
def test_add_model_skips_empty_slots():
|
|
64
|
+
only = _mesh()
|
|
50
65
|
model = Model(meshes=[[MeshFrame(mesh_index=-1)], [MeshFrame(mesh_index=0)]])
|
|
51
66
|
builder = _StubBuilder()
|
|
52
|
-
add_model_to_scene(builder, [
|
|
53
|
-
assert [c[0] for c in builder.calls] == [
|
|
67
|
+
add_model_to_scene(builder, [only], model)
|
|
68
|
+
assert [c[0] for c in builder.calls] == [only]
|
|
54
69
|
|
|
55
70
|
|
|
56
71
|
def test_clamp_frame_falls_back_to_last_pose():
|
|
57
72
|
# Placement has 2 frames; requesting frame 5 with clamp uses the last (idx 1).
|
|
73
|
+
a, b = _mesh(), _mesh()
|
|
58
74
|
model = Model(meshes=[[MeshFrame(mesh_index=0), MeshFrame(mesh_index=1)]])
|
|
59
75
|
builder = _StubBuilder()
|
|
60
|
-
add_model_to_scene(builder, [
|
|
61
|
-
assert [c[0] for c in builder.calls] == [
|
|
76
|
+
add_model_to_scene(builder, [a, b], model, frame=5, clamp_frame=True)
|
|
77
|
+
assert [c[0] for c in builder.calls] == [b]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_add_model_marks_ghost_material_geometry():
|
|
81
|
+
# A wholly ghost placement is added once, carrying the base mask OR'd with
|
|
82
|
+
# MeshFlag.GHOST so the renderer traces through it.
|
|
83
|
+
model = Model(meshes=[[MeshFrame(mesh_index=0)]])
|
|
84
|
+
builder = _StubBuilder()
|
|
85
|
+
add_model_to_scene(builder, [_mesh(is_ghost=True)], model, mask=2)
|
|
86
|
+
assert len(builder.calls) == 1
|
|
87
|
+
assert builder.calls[0][3] == 2 | int(MeshFlag.GHOST)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/openrct2_object_common/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/openrct2_object_common/blender/modal.py
RENAMED
|
File without changes
|
{openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/openrct2_object_common/blender/props.py
RENAMED
|
File without changes
|
|
File without changes
|
{openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/openrct2_object_common/config.py
RENAMED
|
File without changes
|
{openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/openrct2_object_common/objectjson.py
RENAMED
|
File without changes
|
|
File without changes
|
{openrct2_objectcommon-0.1.3 → openrct2_objectcommon-0.2.0}/openrct2_object_common/testing.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|