rust-map-parser 0.1.0__py3-none-any.whl
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.
- rust_map_parser-0.1.0.dist-info/METADATA +538 -0
- rust_map_parser-0.1.0.dist-info/RECORD +110 -0
- rust_map_parser-0.1.0.dist-info/WHEEL +5 -0
- rust_map_parser-0.1.0.dist-info/licenses/LICENSE +21 -0
- rust_map_parser-0.1.0.dist-info/top_level.txt +1 -0
- rustmap/__init__.py +44 -0
- rustmap/config.py +236 -0
- rustmap/data/__init__.py +1 -0
- rustmap/data/monument_metadata.json +954 -0
- rustmap/data/no_build_zones.json +9543 -0
- rustmap/data/prefab_manifest.json +242637 -0
- rustmap/data/spawn_rules.json +3308 -0
- rustmap/data/tunnel_tiles/__init__.py +1 -0
- rustmap/data/tunnel_tiles/template_000__curve-nw-0.png +0 -0
- rustmap/data/tunnel_tiles/template_001__curve-nw-1.png +0 -0
- rustmap/data/tunnel_tiles/template_002__intersection-bb1-n.png +0 -0
- rustmap/data/tunnel_tiles/template_003__intersection-bb4-n.png +0 -0
- rustmap/data/tunnel_tiles/template_004__intersection-b5-n.png +0 -0
- rustmap/data/tunnel_tiles/template_005__intersection-b3-n.png +0 -0
- rustmap/data/tunnel_tiles/template_006__intersection-b2-n.png +0 -0
- rustmap/data/tunnel_tiles/template_007__intersection-bb2-n.png +0 -0
- rustmap/data/tunnel_tiles/template_008__intersection-b6-n.png +0 -0
- rustmap/data/tunnel_tiles/template_009__intersection-bb3-n.png +0 -0
- rustmap/data/tunnel_tiles/template_010__intersection-b4-n.png +0 -0
- rustmap/data/tunnel_tiles/template_011__intersection-bb6-n.png +0 -0
- rustmap/data/tunnel_tiles/template_012__intersection-n.png +0 -0
- rustmap/data/tunnel_tiles/template_013__intersection-b1-n.png +0 -0
- rustmap/data/tunnel_tiles/template_014__intersection-bb5-n.png +0 -0
- rustmap/data/tunnel_tiles/template_015__station-sn-0.png +0 -0
- rustmap/data/tunnel_tiles/template_016__straight-sn-3.png +0 -0
- rustmap/data/tunnel_tiles/template_017__straight-sn-2.png +0 -0
- rustmap/data/tunnel_tiles/template_018__station-sn-1.png +0 -0
- rustmap/data/tunnel_tiles/template_019__straight-sn-0.png +0 -0
- rustmap/data/tunnel_tiles/template_020__straight-sn-1.png +0 -0
- rustmap/data/tunnel_tiles/template_021__station-sn-3.png +0 -0
- rustmap/data/tunnel_tiles/template_022__straight-sn-4.png +0 -0
- rustmap/data/tunnel_tiles/template_023__station-sn-2.png +0 -0
- rustmap/data/tunnel_tiles/template_024__straight-sn-5.png +0 -0
- rustmap/data/tunnel_tiles/template_025__curve-ne-1.png +0 -0
- rustmap/data/tunnel_tiles/template_026__curve-ne-0.png +0 -0
- rustmap/data/tunnel_tiles/template_027__intersection.png +0 -0
- rustmap/data/tunnel_tiles/template_028__intersection-bb2-s.png +0 -0
- rustmap/data/tunnel_tiles/template_029__intersection-b2-s.png +0 -0
- rustmap/data/tunnel_tiles/template_030__intersection-b6-s.png +0 -0
- rustmap/data/tunnel_tiles/template_031__intersection-bb5-s.png +0 -0
- rustmap/data/tunnel_tiles/template_032__intersection-bb1-s.png +0 -0
- rustmap/data/tunnel_tiles/template_033__intersection-s.png +0 -0
- rustmap/data/tunnel_tiles/template_034__intersection-b1-s.png +0 -0
- rustmap/data/tunnel_tiles/template_035__intersection-bb6-s.png +0 -0
- rustmap/data/tunnel_tiles/template_036__intersection-b5-s.png +0 -0
- rustmap/data/tunnel_tiles/template_037__intersection-bb4-s.png +0 -0
- rustmap/data/tunnel_tiles/template_038__intersection-b3-s.png +0 -0
- rustmap/data/tunnel_tiles/template_039__intersection-bb3-s.png +0 -0
- rustmap/data/tunnel_tiles/template_040__intersection-b4-s.png +0 -0
- rustmap/data/tunnel_tiles/template_041__straight-we-3.png +0 -0
- rustmap/data/tunnel_tiles/template_042__station-we-1.png +0 -0
- rustmap/data/tunnel_tiles/template_043__straight-we-5.png +0 -0
- rustmap/data/tunnel_tiles/template_044__straight-we-4.png +0 -0
- rustmap/data/tunnel_tiles/template_045__station-we-2.png +0 -0
- rustmap/data/tunnel_tiles/template_046__station-we-3.png +0 -0
- rustmap/data/tunnel_tiles/template_047__station-we-0.png +0 -0
- rustmap/data/tunnel_tiles/template_048__straight-we-2.png +0 -0
- rustmap/data/tunnel_tiles/template_049__straight-we-1.png +0 -0
- rustmap/data/tunnel_tiles/template_050__straight-we-0.png +0 -0
- rustmap/data/tunnel_tiles/template_051__curve-se-0.png +0 -0
- rustmap/data/tunnel_tiles/template_052__curve-se-1.png +0 -0
- rustmap/data/tunnel_tiles/template_053__intersection-bb3-w.png +0 -0
- rustmap/data/tunnel_tiles/template_054__intersection-w.png +0 -0
- rustmap/data/tunnel_tiles/template_055__intersection-bb5-w.png +0 -0
- rustmap/data/tunnel_tiles/template_056__intersection-b5-w.png +0 -0
- rustmap/data/tunnel_tiles/template_057__intersection-b1-w.png +0 -0
- rustmap/data/tunnel_tiles/template_058__intersection-bb1-w.png +0 -0
- rustmap/data/tunnel_tiles/template_059__intersection-bb4-w.png +0 -0
- rustmap/data/tunnel_tiles/template_060__intersection-b4-w.png +0 -0
- rustmap/data/tunnel_tiles/template_061__intersection-b6-w.png +0 -0
- rustmap/data/tunnel_tiles/template_062__intersection-bb2-w.png +0 -0
- rustmap/data/tunnel_tiles/template_063__intersection-b3-w.png +0 -0
- rustmap/data/tunnel_tiles/template_064__intersection-b2-w.png +0 -0
- rustmap/data/tunnel_tiles/template_065__intersection-bb6-w.png +0 -0
- rustmap/data/tunnel_tiles/template_066__curve-sw-0.png +0 -0
- rustmap/data/tunnel_tiles/template_067__curve-sw-1.png +0 -0
- rustmap/data/tunnel_tiles/template_068__intersection-bb3-e.png +0 -0
- rustmap/data/tunnel_tiles/template_069__intersection-bb6-e.png +0 -0
- rustmap/data/tunnel_tiles/template_070__intersection-b4-e.png +0 -0
- rustmap/data/tunnel_tiles/template_071__intersection-b6-e.png +0 -0
- rustmap/data/tunnel_tiles/template_072__intersection-b5-e.png +0 -0
- rustmap/data/tunnel_tiles/template_073__intersection-b1-e.png +0 -0
- rustmap/data/tunnel_tiles/template_074__intersection-b3-e.png +0 -0
- rustmap/data/tunnel_tiles/template_075__intersection-bb2-e.png +0 -0
- rustmap/data/tunnel_tiles/template_076__intersection-e.png +0 -0
- rustmap/data/tunnel_tiles/template_077__intersection-bb1-e.png +0 -0
- rustmap/data/tunnel_tiles/template_078__intersection-bb4-e.png +0 -0
- rustmap/data/tunnel_tiles/template_079__intersection-b2-e.png +0 -0
- rustmap/data/tunnel_tiles/template_080__intersection-bb5-e.png +0 -0
- rustmap/data/tunnel_tiles/tiles.json +997 -0
- rustmap/exporter.py +409 -0
- rustmap/layers.py +323 -0
- rustmap/monument_assets.py +145 -0
- rustmap/monuments.py +226 -0
- rustmap/no_build.py +288 -0
- rustmap/no_build_assets.py +303 -0
- rustmap/parser.py +297 -0
- rustmap/png.py +18 -0
- rustmap/populations.py +361 -0
- rustmap/prefabs.py +281 -0
- rustmap/renderer.py +460 -0
- rustmap/resources.py +26 -0
- rustmap/tunnel_assets.py +398 -0
- rustmap/tunnels.py +417 -0
- rustmap/validation.py +106 -0
rustmap/tunnel_assets.py
ADDED
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"""Extract and cache Rust's final-LOD train-tunnel map geometry."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import math
|
|
8
|
+
import multiprocessing as mp
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import shutil
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
from PIL import Image, ImageDraw
|
|
18
|
+
|
|
19
|
+
from .png import save_png
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
CACHE_SCHEMA_VERSION = 5
|
|
23
|
+
TEMPLATE_PIXELS_PER_METER = 8.0
|
|
24
|
+
ASSET_SCENE_FILE = "BuildPlayer-AssetScene-monument.25"
|
|
25
|
+
TUNNEL_PREFIX = "assets/bundled/prefabs/autospawn/tunnel"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def default_tunnel_cache() -> Path:
|
|
29
|
+
if sys.platform == "win32":
|
|
30
|
+
base = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local"))
|
|
31
|
+
elif sys.platform == "darwin":
|
|
32
|
+
base = Path.home() / "Library" / "Caches"
|
|
33
|
+
else:
|
|
34
|
+
base = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache"))
|
|
35
|
+
return base / "rustmap" / "tunnel-geometry"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def find_rust_install(explicit: str | Path | None = None) -> Path | None:
|
|
39
|
+
candidates: list[Path] = []
|
|
40
|
+
if explicit is not None:
|
|
41
|
+
candidate = Path(explicit)
|
|
42
|
+
return candidate.resolve() if (
|
|
43
|
+
candidate / "Bundles" / "shared" / "content.bundle"
|
|
44
|
+
).is_file() else None
|
|
45
|
+
if os.environ.get("RUST_INSTALL_PATH"):
|
|
46
|
+
candidates.append(Path(os.environ["RUST_INSTALL_PATH"]))
|
|
47
|
+
if sys.platform == "win32":
|
|
48
|
+
for drive in "CDEF":
|
|
49
|
+
candidates.append(Path(f"{drive}:\\SteamLibrary\\steamapps\\common\\Rust"))
|
|
50
|
+
candidates.append(Path(os.environ.get("PROGRAMFILES(X86)", r"C:\Program Files (x86)")) /
|
|
51
|
+
"Steam" / "steamapps" / "common" / "Rust")
|
|
52
|
+
for candidate in candidates:
|
|
53
|
+
if (candidate / "Bundles" / "shared" / "content.bundle").is_file():
|
|
54
|
+
return candidate.resolve()
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _bundle_paths(install: Path) -> dict[str, Path]:
|
|
59
|
+
result = {
|
|
60
|
+
"content": install / "Bundles" / "shared" / "content.bundle",
|
|
61
|
+
"asset_scenes": install / "Bundles" / "shared" / "assetscenes.bundle",
|
|
62
|
+
"maps": install / "Bundles" / "maps" / "maps.bundle",
|
|
63
|
+
}
|
|
64
|
+
missing = [str(path) for path in result.values() if not path.is_file()]
|
|
65
|
+
if missing:
|
|
66
|
+
raise FileNotFoundError("Missing Rust bundle(s): " + ", ".join(missing))
|
|
67
|
+
return result
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _build_id(install: Path) -> str | None:
|
|
71
|
+
manifest = install.parent.parent / "appmanifest_252490.acf"
|
|
72
|
+
if not manifest.is_file():
|
|
73
|
+
return None
|
|
74
|
+
match = re.search(r'"buildid"\s+"([^"]+)"', manifest.read_text(encoding="utf-8", errors="ignore"))
|
|
75
|
+
return match.group(1) if match else None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def bundle_identity(install: Path) -> dict:
|
|
79
|
+
bundles = _bundle_paths(install)
|
|
80
|
+
return {
|
|
81
|
+
"schema_version": CACHE_SCHEMA_VERSION,
|
|
82
|
+
"rust_build_id": _build_id(install),
|
|
83
|
+
"bundles": {
|
|
84
|
+
name: {"path": str(path), "size": path.stat().st_size,
|
|
85
|
+
"mtime_ns": path.stat().st_mtime_ns}
|
|
86
|
+
for name, path in bundles.items()
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _cache_key(identity: dict) -> str:
|
|
92
|
+
stable = {
|
|
93
|
+
"schema_version": identity["schema_version"],
|
|
94
|
+
"rust_build_id": identity["rust_build_id"],
|
|
95
|
+
"bundles": {name: {"size": value["size"], "mtime_ns": value["mtime_ns"]}
|
|
96
|
+
for name, value in identity["bundles"].items()},
|
|
97
|
+
}
|
|
98
|
+
return hashlib.sha256(json.dumps(stable, sort_keys=True).encode()).hexdigest()[:20]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _vec(value, default: float = 0.0) -> np.ndarray:
|
|
102
|
+
return np.array([getattr(value, "x", default), getattr(value, "y", default),
|
|
103
|
+
getattr(value, "z", default)], dtype=np.float64)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _local_matrix(transform) -> np.ndarray:
|
|
107
|
+
p = _vec(transform.m_LocalPosition)
|
|
108
|
+
s = _vec(transform.m_LocalScale, 1.0)
|
|
109
|
+
q = transform.m_LocalRotation
|
|
110
|
+
x, y, z, w = float(q.x), float(q.y), float(q.z), float(q.w)
|
|
111
|
+
rotation = np.array([
|
|
112
|
+
[1-2*(y*y+z*z), 2*(x*y-z*w), 2*(x*z+y*w)],
|
|
113
|
+
[2*(x*y+z*w), 1-2*(x*x+z*z), 2*(y*z-x*w)],
|
|
114
|
+
[2*(x*z-y*w), 2*(y*z+x*w), 1-2*(x*x+y*y)],
|
|
115
|
+
], dtype=np.float64)
|
|
116
|
+
matrix = np.eye(4, dtype=np.float64)
|
|
117
|
+
matrix[:3, :3] = rotation @ np.diag(s)
|
|
118
|
+
matrix[:3, 3] = p
|
|
119
|
+
return matrix
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _world_matrix(transform, memo: dict[int, np.ndarray]) -> np.ndarray:
|
|
123
|
+
path_id = transform.object_reader.path_id
|
|
124
|
+
if path_id in memo:
|
|
125
|
+
return memo[path_id]
|
|
126
|
+
local = _local_matrix(transform)
|
|
127
|
+
parent = transform.m_Father
|
|
128
|
+
result = _world_matrix(parent.read(), memo) @ local if parent.path_id else local
|
|
129
|
+
memo[path_id] = result
|
|
130
|
+
return result
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _component(game_object, type_name: str):
|
|
134
|
+
for item in game_object.m_Component:
|
|
135
|
+
if item.component.type.name == type_name:
|
|
136
|
+
return item.component.read()
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _rasterize_template(vertices: np.ndarray, triangles: np.ndarray) -> tuple[np.ndarray, dict]:
|
|
141
|
+
"""Rasterize prefab-local X/Z triangles with stable world-grid pixel phase."""
|
|
142
|
+
started = time.perf_counter()
|
|
143
|
+
ppm = TEMPLATE_PIXELS_PER_METER
|
|
144
|
+
min_x, max_x = float(vertices[:, 0].min()), float(vertices[:, 0].max())
|
|
145
|
+
min_z, max_z = float(vertices[:, 2].min()), float(vertices[:, 2].max())
|
|
146
|
+
# Align bounds to the global 2 px/m lattice and retain one transparent pixel
|
|
147
|
+
# around the silhouette so Lanczos downsampling cannot clip edge coverage.
|
|
148
|
+
left_x = math.floor(min_x * ppm) / ppm - 1.0 / ppm
|
|
149
|
+
right_x = math.ceil(max_x * ppm) / ppm + 1.0 / ppm
|
|
150
|
+
bottom_z = math.floor(min_z * ppm) / ppm - 1.0 / ppm
|
|
151
|
+
top_z = math.ceil(max_z * ppm) / ppm + 1.0 / ppm
|
|
152
|
+
width = max(1, int(round((right_x - left_x) * ppm)) + 1)
|
|
153
|
+
height = max(1, int(round((top_z - bottom_z) * ppm)) + 1)
|
|
154
|
+
px = (vertices[:, 0] - left_x) * ppm
|
|
155
|
+
py = (top_z - vertices[:, 2]) * ppm
|
|
156
|
+
projected = np.round(np.column_stack((px, py)), 6)
|
|
157
|
+
image = Image.new("L", (width, height), 0)
|
|
158
|
+
draw = ImageDraw.Draw(image)
|
|
159
|
+
drawn = 0
|
|
160
|
+
for triangle in triangles:
|
|
161
|
+
points = projected[triangle]
|
|
162
|
+
first, second = points[1] - points[0], points[2] - points[0]
|
|
163
|
+
area = abs(first[0] * second[1] - first[1] * second[0])
|
|
164
|
+
if area <= 0.05:
|
|
165
|
+
continue
|
|
166
|
+
draw.polygon(tuple(map(tuple, points)), fill=255)
|
|
167
|
+
drawn += 1
|
|
168
|
+
return np.asarray(image, dtype=np.uint8), {
|
|
169
|
+
"mask_pixels_per_meter": ppm,
|
|
170
|
+
"mask_left_x": left_x,
|
|
171
|
+
"mask_top_z": top_z,
|
|
172
|
+
"mask_shape": [height, width],
|
|
173
|
+
"mask_triangle_count": drawn,
|
|
174
|
+
"mask_render_seconds": time.perf_counter() - started,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _class_type_index(asset_file, class_name: str) -> int:
|
|
179
|
+
representatives = {}
|
|
180
|
+
for obj in asset_file.objects.values():
|
|
181
|
+
if obj.type.name == "MonoBehaviour":
|
|
182
|
+
representatives.setdefault(id(obj.serialized_type), obj)
|
|
183
|
+
for obj in representatives.values():
|
|
184
|
+
try:
|
|
185
|
+
if obj.read().m_Script.read().m_ClassName == class_name:
|
|
186
|
+
return obj.serialized_type.script_type_index
|
|
187
|
+
except Exception:
|
|
188
|
+
continue
|
|
189
|
+
raise RuntimeError(f"{class_name} MonoBehaviour type was not found")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _extract_worker(asset_scenes: str, content: str, target: str, identity: dict, conn) -> None:
|
|
193
|
+
try:
|
|
194
|
+
import UnityPy
|
|
195
|
+
from UnityPy.helpers.MeshHelper import MeshHandler
|
|
196
|
+
|
|
197
|
+
started = time.perf_counter()
|
|
198
|
+
environment = UnityPy.load(asset_scenes, content)
|
|
199
|
+
scene = None
|
|
200
|
+
for root in environment.files.values():
|
|
201
|
+
files = getattr(root, "files", None) or {}
|
|
202
|
+
if ASSET_SCENE_FILE in files:
|
|
203
|
+
scene = files[ASSET_SCENE_FILE]
|
|
204
|
+
break
|
|
205
|
+
if scene is None:
|
|
206
|
+
raise RuntimeError(f"{ASSET_SCENE_FILE} was not found in assetscenes.bundle")
|
|
207
|
+
|
|
208
|
+
type_index = _class_type_index(scene, "DungeonGridCell")
|
|
209
|
+
arrays: dict[str, np.ndarray] = {}
|
|
210
|
+
templates = []
|
|
211
|
+
template_raster_seconds = 0.0
|
|
212
|
+
transform_memo: dict[int, np.ndarray] = {}
|
|
213
|
+
for component in scene.objects.values():
|
|
214
|
+
if (component.type.name != "MonoBehaviour" or
|
|
215
|
+
component.serialized_type.script_type_index != type_index):
|
|
216
|
+
continue
|
|
217
|
+
tree = component.read_typetree()
|
|
218
|
+
root_go = scene.objects[tree["m_GameObject"]["m_PathID"]].read()
|
|
219
|
+
path = root_go.m_Name.casefold().replace("\\", "/")
|
|
220
|
+
if not path.startswith(TUNNEL_PREFIX):
|
|
221
|
+
continue
|
|
222
|
+
root_transform = _component(root_go, "Transform")
|
|
223
|
+
root_inverse = np.linalg.inv(_world_matrix(root_transform, transform_memo))
|
|
224
|
+
vertices_parts, triangle_parts = [], []
|
|
225
|
+
vertex_offset = 0
|
|
226
|
+
mesh_count = 0
|
|
227
|
+
for lod_ptr in tree.get("MapRendererLods", []):
|
|
228
|
+
if not lod_ptr.get("m_PathID"):
|
|
229
|
+
continue
|
|
230
|
+
lod_tree = scene.objects[lod_ptr["m_PathID"]].read_typetree()
|
|
231
|
+
renderer = None
|
|
232
|
+
for state in reversed(lod_tree.get("States", [])):
|
|
233
|
+
pointer = state.get("renderer", {})
|
|
234
|
+
if pointer.get("m_PathID"):
|
|
235
|
+
renderer = scene.objects[pointer["m_PathID"]].read()
|
|
236
|
+
break
|
|
237
|
+
if renderer is None:
|
|
238
|
+
continue
|
|
239
|
+
renderer_go = renderer.m_GameObject.read()
|
|
240
|
+
mesh_filter = _component(renderer_go, "MeshFilter")
|
|
241
|
+
renderer_transform = _component(renderer_go, "Transform")
|
|
242
|
+
if mesh_filter is None or not mesh_filter.m_Mesh.path_id:
|
|
243
|
+
continue
|
|
244
|
+
mesh = mesh_filter.m_Mesh.read()
|
|
245
|
+
handler = MeshHandler(mesh)
|
|
246
|
+
handler.process()
|
|
247
|
+
if not handler.m_Vertices:
|
|
248
|
+
continue
|
|
249
|
+
local_to_root = root_inverse @ _world_matrix(renderer_transform, transform_memo)
|
|
250
|
+
vertices = np.asarray(handler.m_Vertices, dtype=np.float64)
|
|
251
|
+
homogeneous = np.column_stack((vertices, np.ones(len(vertices))))
|
|
252
|
+
vertices = (local_to_root @ homogeneous.T).T[:, :3].astype(np.float32)
|
|
253
|
+
triangles = [triangle for submesh in handler.get_triangles() for triangle in submesh]
|
|
254
|
+
if not triangles:
|
|
255
|
+
continue
|
|
256
|
+
indices = np.asarray(triangles, dtype=np.int32) + vertex_offset
|
|
257
|
+
vertices_parts.append(vertices)
|
|
258
|
+
triangle_parts.append(indices)
|
|
259
|
+
vertex_offset += len(vertices)
|
|
260
|
+
mesh_count += 1
|
|
261
|
+
if not vertices_parts:
|
|
262
|
+
continue
|
|
263
|
+
key = f"template_{len(templates):03d}"
|
|
264
|
+
combined_vertices = np.concatenate(vertices_parts)
|
|
265
|
+
combined_triangles = np.concatenate(triangle_parts)
|
|
266
|
+
arrays[key + "_vertices"] = combined_vertices
|
|
267
|
+
arrays[key + "_triangles"] = combined_triangles
|
|
268
|
+
template_mask, mask_metadata = _rasterize_template(
|
|
269
|
+
combined_vertices, combined_triangles
|
|
270
|
+
)
|
|
271
|
+
arrays[key + "_mask"] = template_mask
|
|
272
|
+
template_raster_seconds += mask_metadata["mask_render_seconds"]
|
|
273
|
+
templates.append({"key": key, "prefab_path": path, "mesh_count": mesh_count,
|
|
274
|
+
"vertex_count": len(combined_vertices),
|
|
275
|
+
"triangle_count": len(combined_triangles),
|
|
276
|
+
**mask_metadata})
|
|
277
|
+
|
|
278
|
+
destination = Path(target)
|
|
279
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
280
|
+
templates_directory = destination / "templates"
|
|
281
|
+
templates_directory.mkdir(parents=True, exist_ok=True)
|
|
282
|
+
for template in templates:
|
|
283
|
+
key = template["key"]
|
|
284
|
+
filename = f"{key}__{Path(template['prefab_path']).stem}.png"
|
|
285
|
+
save_png(
|
|
286
|
+
Image.fromarray(arrays.pop(key + "_mask"), mode="L"),
|
|
287
|
+
templates_directory / filename,
|
|
288
|
+
)
|
|
289
|
+
template["mask_file"] = f"templates/{filename}"
|
|
290
|
+
np.savez_compressed(destination / "geometry.npz", **arrays)
|
|
291
|
+
metadata = {"schema_version": CACHE_SCHEMA_VERSION, "identity": identity,
|
|
292
|
+
"template_count": len(templates), "templates": templates,
|
|
293
|
+
"template_pixels_per_meter": TEMPLATE_PIXELS_PER_METER,
|
|
294
|
+
"template_raster_seconds": template_raster_seconds,
|
|
295
|
+
"extraction_seconds": time.perf_counter() - started}
|
|
296
|
+
(destination / "metadata.json").write_text(
|
|
297
|
+
json.dumps(metadata, indent=2) + "\n", encoding="utf-8", newline="\n")
|
|
298
|
+
conn.send({"ok": True, "template_count": len(templates)})
|
|
299
|
+
except Exception as exc:
|
|
300
|
+
conn.send({"ok": False, "error": f"{type(exc).__name__}: {exc}"})
|
|
301
|
+
finally:
|
|
302
|
+
conn.close()
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def refresh_tunnel_geometry(rust_install_path: str | Path,
|
|
306
|
+
cache_path: str | Path | None = None) -> Path:
|
|
307
|
+
install = find_rust_install(rust_install_path)
|
|
308
|
+
if install is None:
|
|
309
|
+
raise FileNotFoundError(f"Rust installation not found: {rust_install_path}")
|
|
310
|
+
identity = bundle_identity(install)
|
|
311
|
+
cache_root = Path(cache_path) if cache_path is not None else default_tunnel_cache()
|
|
312
|
+
destination = cache_root / _cache_key(identity)
|
|
313
|
+
bundles = _bundle_paths(install)
|
|
314
|
+
parent, child = mp.get_context("spawn").Pipe(False)
|
|
315
|
+
process = mp.get_context("spawn").Process(
|
|
316
|
+
target=_extract_worker,
|
|
317
|
+
args=(str(bundles["asset_scenes"]), str(bundles["content"]),
|
|
318
|
+
str(destination), identity, child),
|
|
319
|
+
)
|
|
320
|
+
process.start()
|
|
321
|
+
result = parent.recv()
|
|
322
|
+
process.join()
|
|
323
|
+
if not result.get("ok"):
|
|
324
|
+
raise RuntimeError(result.get("error", "Tunnel geometry extraction failed"))
|
|
325
|
+
return destination
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def load_or_refresh_tunnel_geometry(rust_install_path: str | Path,
|
|
329
|
+
cache_path: str | Path | None = None) -> Path:
|
|
330
|
+
install = find_rust_install(rust_install_path)
|
|
331
|
+
if install is None:
|
|
332
|
+
raise FileNotFoundError("A matching local Rust client installation was not found")
|
|
333
|
+
identity = bundle_identity(install)
|
|
334
|
+
cache_root = Path(cache_path) if cache_path is not None else default_tunnel_cache()
|
|
335
|
+
destination = cache_root / _cache_key(identity)
|
|
336
|
+
metadata_path, geometry_path = destination / "metadata.json", destination / "geometry.npz"
|
|
337
|
+
if metadata_path.is_file() and geometry_path.is_file():
|
|
338
|
+
try:
|
|
339
|
+
metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
|
|
340
|
+
if metadata.get("identity") == identity and metadata.get("template_count", 0) > 0:
|
|
341
|
+
return destination
|
|
342
|
+
except (OSError, ValueError):
|
|
343
|
+
pass
|
|
344
|
+
return refresh_tunnel_geometry(install, cache_root)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def refresh_tunnel_templates(rust_install_path: str | Path,
|
|
348
|
+
cache_path: str | Path | None = None) -> Path:
|
|
349
|
+
"""Build named PNG tunnel pieces plus fallback geometry in a versioned cache."""
|
|
350
|
+
return refresh_tunnel_geometry(rust_install_path, cache_path)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def install_packaged_tunnel_templates(cache_path: str | Path,
|
|
354
|
+
destination: str | Path) -> Path:
|
|
355
|
+
"""Copy PNG pieces from a local cache and write sanitized package metadata."""
|
|
356
|
+
cache = Path(cache_path)
|
|
357
|
+
target = Path(destination)
|
|
358
|
+
metadata = json.loads((cache / "metadata.json").read_text(encoding="utf-8"))
|
|
359
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
360
|
+
expected = set()
|
|
361
|
+
templates = []
|
|
362
|
+
for item in metadata["templates"]:
|
|
363
|
+
filename = Path(item["mask_file"]).name
|
|
364
|
+
source = cache / item["mask_file"]
|
|
365
|
+
if not source.is_file():
|
|
366
|
+
raise FileNotFoundError(f"Missing generated tunnel tile: {source}")
|
|
367
|
+
shutil.copy2(source, target / filename)
|
|
368
|
+
expected.add(filename)
|
|
369
|
+
templates.append({
|
|
370
|
+
"key": item["key"], "prefab_path": item["prefab_path"],
|
|
371
|
+
"mask_file": filename,
|
|
372
|
+
"mask_pixels_per_meter": item["mask_pixels_per_meter"],
|
|
373
|
+
"mask_left_x": item["mask_left_x"], "mask_top_z": item["mask_top_z"],
|
|
374
|
+
"mask_shape": item["mask_shape"],
|
|
375
|
+
})
|
|
376
|
+
for old_tile in target.glob("*.png"):
|
|
377
|
+
if old_tile.name not in expected:
|
|
378
|
+
old_tile.unlink()
|
|
379
|
+
identity = metadata.get("identity", {})
|
|
380
|
+
sanitized_bundles = {
|
|
381
|
+
name: {"size": int(value["size"]), "mtime_ns": int(value["mtime_ns"])}
|
|
382
|
+
for name, value in identity.get("bundles", {}).items()
|
|
383
|
+
}
|
|
384
|
+
payload = {
|
|
385
|
+
"schema_version": int(metadata["schema_version"]),
|
|
386
|
+
"identity": {
|
|
387
|
+
"schema_version": int(identity.get("schema_version", metadata["schema_version"])),
|
|
388
|
+
"rust_build_id": identity.get("rust_build_id"),
|
|
389
|
+
"bundles": sanitized_bundles,
|
|
390
|
+
},
|
|
391
|
+
"template_count": len(templates),
|
|
392
|
+
"template_pixels_per_meter": metadata["template_pixels_per_meter"],
|
|
393
|
+
"templates": templates,
|
|
394
|
+
}
|
|
395
|
+
(target / "tiles.json").write_text(
|
|
396
|
+
json.dumps(payload, indent=2) + "\n", encoding="utf-8", newline="\n"
|
|
397
|
+
)
|
|
398
|
+
return target
|