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/exporter.py
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
"""Composable Rust map export pipeline."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
8
|
+
from contextlib import ExitStack
|
|
9
|
+
from importlib import resources
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
from PIL import Image
|
|
14
|
+
|
|
15
|
+
from .config import ExportConfig, ExportResult
|
|
16
|
+
from .layers import generate_diagnostics
|
|
17
|
+
from .monuments import save_monuments
|
|
18
|
+
from .no_build import save_no_build_zones
|
|
19
|
+
from .parser import load_map
|
|
20
|
+
from .png import save_png
|
|
21
|
+
from .populations import SpawnFilterEvaluator
|
|
22
|
+
from .renderer import save_map_render
|
|
23
|
+
from .tunnels import save_tunnel_render
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
ORIENTATION = "flip_vertical (reverse Z rows; preserve X left/right)"
|
|
27
|
+
HEATMAP_PREVIEW_DIRECTORY = "Heatmap-previews"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def export_orientation(values: np.ndarray) -> np.ndarray:
|
|
31
|
+
"""Convert native ``[z,x]`` arrays to image rows without mirroring X."""
|
|
32
|
+
return np.ascontiguousarray(values[::-1, :])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _stats(values: np.ndarray, populations: list[str]) -> dict:
|
|
36
|
+
return {
|
|
37
|
+
"populations": populations,
|
|
38
|
+
"nonzero_pixels": int(np.count_nonzero(values)),
|
|
39
|
+
"minimum": int(values.min()), "maximum": int(values.max()),
|
|
40
|
+
"mean": float(values.mean()),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _write_metadata(path: Path, metadata: dict, artifacts: dict[str, int]) -> None:
|
|
45
|
+
artifacts[path.name] = 0
|
|
46
|
+
for _ in range(4):
|
|
47
|
+
rendered = json.dumps(metadata, indent=2) + "\n"
|
|
48
|
+
size = len(rendered.encode("utf-8"))
|
|
49
|
+
if artifacts[path.name] == size:
|
|
50
|
+
break
|
|
51
|
+
artifacts[path.name] = size
|
|
52
|
+
path.write_text(json.dumps(metadata, indent=2) + "\n", encoding="utf-8", newline="\n")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _generate(config: ExportConfig, rules_path: Path | None,
|
|
56
|
+
manifest_path: Path | None) -> dict:
|
|
57
|
+
started = time.perf_counter()
|
|
58
|
+
output = config.output_dir
|
|
59
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
exports = config.exports
|
|
61
|
+
timings: dict[str, object] = {"heatmap_categories": {}}
|
|
62
|
+
|
|
63
|
+
stage = time.perf_counter()
|
|
64
|
+
world = load_map(config.map_path)
|
|
65
|
+
timings["map_parse_seconds"] = time.perf_counter() - stage
|
|
66
|
+
|
|
67
|
+
diagnostics_dir = output / "diagnostics"
|
|
68
|
+
diagnostics_pool = ThreadPoolExecutor(max_workers=1) if exports.diagnostics else None
|
|
69
|
+
diagnostics_started = time.perf_counter()
|
|
70
|
+
diagnostics_future = (
|
|
71
|
+
diagnostics_pool.submit(generate_diagnostics, world, diagnostics_dir)
|
|
72
|
+
if diagnostics_pool else None
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
heatmap_options = exports.heatmaps
|
|
76
|
+
heatmap_categories: dict[str, dict] = {}
|
|
77
|
+
npz_path: Path | None = None
|
|
78
|
+
preview_dir: Path | None = None
|
|
79
|
+
heatmap_resolution: int | None = None
|
|
80
|
+
rule_database_metadata = None
|
|
81
|
+
if heatmap_options is not None:
|
|
82
|
+
heatmap_resolution = heatmap_options.resolved_resolution(world.size)
|
|
83
|
+
if rules_path is None:
|
|
84
|
+
raise RuntimeError("Heatmap export requires a spawn-rule database")
|
|
85
|
+
stage = time.perf_counter()
|
|
86
|
+
database = json.loads(rules_path.read_text(encoding="utf-8"))
|
|
87
|
+
timings["rule_database_load_seconds"] = time.perf_counter() - stage
|
|
88
|
+
rules = {rule["asset_path"]: rule for rule in database["rules"]}
|
|
89
|
+
rule_database_metadata = {
|
|
90
|
+
"schema_version": database.get("schema_version"),
|
|
91
|
+
"sources": database.get("sources", {}),
|
|
92
|
+
}
|
|
93
|
+
stage = time.perf_counter()
|
|
94
|
+
evaluator = SpawnFilterEvaluator(world, heatmap_resolution)
|
|
95
|
+
timings["heatmap_evaluator_setup_seconds"] = time.perf_counter() - stage
|
|
96
|
+
arrays: dict[str, np.ndarray] = {}
|
|
97
|
+
preview_dir = output / HEATMAP_PREVIEW_DIRECTORY
|
|
98
|
+
preview_pool = ThreadPoolExecutor(max_workers=4) if heatmap_options.previews else None
|
|
99
|
+
preview_futures = []
|
|
100
|
+
if preview_pool:
|
|
101
|
+
preview_dir.mkdir(parents=True, exist_ok=True)
|
|
102
|
+
for category, asset_paths in database["heatmap_categories"].items():
|
|
103
|
+
category_started = time.perf_counter()
|
|
104
|
+
combined = np.zeros(
|
|
105
|
+
(heatmap_resolution, heatmap_resolution), dtype=np.uint8
|
|
106
|
+
)
|
|
107
|
+
used = []
|
|
108
|
+
for asset_path in asset_paths:
|
|
109
|
+
rule = rules.get(asset_path)
|
|
110
|
+
if rule is None or not rule.get("active"):
|
|
111
|
+
continue
|
|
112
|
+
np.maximum(combined, evaluator.evaluate(rule), out=combined)
|
|
113
|
+
used.append(rule["name"])
|
|
114
|
+
exported = export_orientation(combined)
|
|
115
|
+
arrays[category] = exported
|
|
116
|
+
heatmap_categories[category] = _stats(exported, used)
|
|
117
|
+
if preview_pool:
|
|
118
|
+
preview_futures.append(preview_pool.submit(
|
|
119
|
+
save_png,
|
|
120
|
+
Image.fromarray(exported, mode="L"),
|
|
121
|
+
preview_dir / f"{category}.png",
|
|
122
|
+
))
|
|
123
|
+
timings["heatmap_categories"][category] = time.perf_counter() - category_started
|
|
124
|
+
timings["heatmaps_total_seconds"] = sum(timings["heatmap_categories"].values())
|
|
125
|
+
npz_path = output / f"heatmaps_{heatmap_resolution}.npz"
|
|
126
|
+
stage = time.perf_counter()
|
|
127
|
+
np.savez_compressed(npz_path, **arrays)
|
|
128
|
+
timings["heatmap_npz_encode_seconds"] = time.perf_counter() - stage
|
|
129
|
+
if preview_pool:
|
|
130
|
+
for future in preview_futures:
|
|
131
|
+
future.result()
|
|
132
|
+
preview_pool.shutdown()
|
|
133
|
+
else:
|
|
134
|
+
timings.update({
|
|
135
|
+
"rule_database_load_seconds": 0.0,
|
|
136
|
+
"heatmap_evaluator_setup_seconds": 0.0,
|
|
137
|
+
"heatmaps_total_seconds": 0.0,
|
|
138
|
+
"heatmap_npz_encode_seconds": 0.0,
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
if diagnostics_future:
|
|
142
|
+
diagnostic_stats = diagnostics_future.result()
|
|
143
|
+
diagnostics_pool.shutdown()
|
|
144
|
+
timings["diagnostics_seconds"] = time.perf_counter() - diagnostics_started
|
|
145
|
+
else:
|
|
146
|
+
diagnostic_stats = {}
|
|
147
|
+
timings["diagnostics_seconds"] = 0.0
|
|
148
|
+
|
|
149
|
+
monuments_path = output / "monuments.json"
|
|
150
|
+
stage = time.perf_counter()
|
|
151
|
+
if exports.monuments:
|
|
152
|
+
if manifest_path is None:
|
|
153
|
+
raise RuntimeError("Monument export requires a prefab manifest")
|
|
154
|
+
monument_data = save_monuments(world, manifest_path, monuments_path)
|
|
155
|
+
else:
|
|
156
|
+
monument_data = {"monument_count": 0, "unique_prefab_count": 0}
|
|
157
|
+
timings["monuments_seconds"] = time.perf_counter() - stage
|
|
158
|
+
|
|
159
|
+
terrain_options = exports.terrain
|
|
160
|
+
render_metadata = None
|
|
161
|
+
if terrain_options is not None:
|
|
162
|
+
stage = time.perf_counter()
|
|
163
|
+
render_metadata = save_map_render(
|
|
164
|
+
world, output, terrain_options.scale, terrain_options.ocean_margin,
|
|
165
|
+
terrain_options.formats, terrain_options.debug, terrain_options.full_size,
|
|
166
|
+
terrain_options.tiles is not None,
|
|
167
|
+
terrain_options.tiles.size if terrain_options.tiles else 512,
|
|
168
|
+
)
|
|
169
|
+
timings["map_render_seconds"] = time.perf_counter() - stage
|
|
170
|
+
else:
|
|
171
|
+
timings["map_render_seconds"] = 0.0
|
|
172
|
+
|
|
173
|
+
terrain_path = (
|
|
174
|
+
output / "map_render_full.png"
|
|
175
|
+
if terrain_options is not None and terrain_options.full_size else None
|
|
176
|
+
)
|
|
177
|
+
terrain_image = None
|
|
178
|
+
if terrain_path is not None and terrain_path.is_file():
|
|
179
|
+
with Image.open(terrain_path) as source:
|
|
180
|
+
terrain_image = source.convert("RGBA")
|
|
181
|
+
|
|
182
|
+
no_build_options = exports.no_build_zones
|
|
183
|
+
tunnel_options = exports.tunnels
|
|
184
|
+
|
|
185
|
+
def run_no_build():
|
|
186
|
+
if manifest_path is None:
|
|
187
|
+
raise RuntimeError("No-build export requires a prefab manifest")
|
|
188
|
+
stage_started = time.perf_counter()
|
|
189
|
+
value = save_no_build_zones(
|
|
190
|
+
world, manifest_path, output,
|
|
191
|
+
resolution=no_build_options.resolution,
|
|
192
|
+
fill_color=no_build_options.fill_color,
|
|
193
|
+
outline_color=no_build_options.outline_color,
|
|
194
|
+
outline_width=no_build_options.outline_width,
|
|
195
|
+
terrain_image=terrain_image,
|
|
196
|
+
export_images=no_build_options.export_images,
|
|
197
|
+
export_json=no_build_options.export_json,
|
|
198
|
+
)
|
|
199
|
+
return value, time.perf_counter() - stage_started
|
|
200
|
+
|
|
201
|
+
def run_tunnels():
|
|
202
|
+
if manifest_path is None:
|
|
203
|
+
raise RuntimeError("Tunnel export requires a prefab manifest")
|
|
204
|
+
stage_started = time.perf_counter()
|
|
205
|
+
value = save_tunnel_render(
|
|
206
|
+
world, manifest_path, output,
|
|
207
|
+
resolution=tunnel_options.resolution,
|
|
208
|
+
overlay_opacity=tunnel_options.overlay_opacity,
|
|
209
|
+
tint_color=tunnel_options.tint_color,
|
|
210
|
+
terrain_image=terrain_image,
|
|
211
|
+
export_layer=tunnel_options.export_layer,
|
|
212
|
+
export_overlay=tunnel_options.export_overlay,
|
|
213
|
+
)
|
|
214
|
+
return value, time.perf_counter() - stage_started
|
|
215
|
+
|
|
216
|
+
if no_build_options is not None and tunnel_options is not None:
|
|
217
|
+
with ThreadPoolExecutor(max_workers=2) as pool:
|
|
218
|
+
no_build_future = pool.submit(run_no_build)
|
|
219
|
+
tunnel_future = pool.submit(run_tunnels)
|
|
220
|
+
no_build_metadata, timings["no_build_zones_seconds"] = no_build_future.result()
|
|
221
|
+
tunnel_metadata, timings["tunnel_render_seconds"] = tunnel_future.result()
|
|
222
|
+
elif no_build_options is not None:
|
|
223
|
+
no_build_metadata, timings["no_build_zones_seconds"] = run_no_build()
|
|
224
|
+
tunnel_metadata, timings["tunnel_render_seconds"] = {"status": "disabled"}, 0.0
|
|
225
|
+
elif tunnel_options is not None:
|
|
226
|
+
tunnel_metadata, timings["tunnel_render_seconds"] = run_tunnels()
|
|
227
|
+
no_build_metadata, timings["no_build_zones_seconds"] = {
|
|
228
|
+
"status": "disabled", "zone_count": 0
|
|
229
|
+
}, 0.0
|
|
230
|
+
else:
|
|
231
|
+
no_build_metadata = {"status": "disabled", "zone_count": 0}
|
|
232
|
+
tunnel_metadata = {"status": "disabled"}
|
|
233
|
+
timings["no_build_zones_seconds"] = 0.0
|
|
234
|
+
timings["tunnel_render_seconds"] = 0.0
|
|
235
|
+
if terrain_image is not None:
|
|
236
|
+
terrain_image.close()
|
|
237
|
+
|
|
238
|
+
metadata_path = output / "export_metadata.json"
|
|
239
|
+
metadata = {
|
|
240
|
+
"schema_version": 2,
|
|
241
|
+
"map": {
|
|
242
|
+
"path": str(config.map_path),
|
|
243
|
+
"world_size": int(world.size),
|
|
244
|
+
"serialization_version": int(world.serialization_version),
|
|
245
|
+
"timestamp": int(world.timestamp),
|
|
246
|
+
},
|
|
247
|
+
"orientation": ORIENTATION,
|
|
248
|
+
"enabled_outputs": {
|
|
249
|
+
"heatmaps": heatmap_options is not None,
|
|
250
|
+
"diagnostics": exports.diagnostics,
|
|
251
|
+
"monuments": exports.monuments,
|
|
252
|
+
"terrain": terrain_options is not None,
|
|
253
|
+
"tunnels": tunnel_options is not None,
|
|
254
|
+
"no_build_zones": no_build_options is not None,
|
|
255
|
+
},
|
|
256
|
+
"heatmaps": {
|
|
257
|
+
"file": npz_path.name if npz_path else None,
|
|
258
|
+
"resolution": heatmap_resolution,
|
|
259
|
+
"requested_resolution": heatmap_options.resolution if heatmap_options else None,
|
|
260
|
+
"resolution_mode": (
|
|
261
|
+
"world_size" if heatmap_options and heatmap_options.resolution is None
|
|
262
|
+
else "explicit" if heatmap_options else None
|
|
263
|
+
),
|
|
264
|
+
"dtype": "uint8" if heatmap_options else None,
|
|
265
|
+
"format": "npz_compressed" if heatmap_options else None,
|
|
266
|
+
"preview_directory": preview_dir.name if preview_dir else None,
|
|
267
|
+
"rule_database": rule_database_metadata,
|
|
268
|
+
"categories": heatmap_categories,
|
|
269
|
+
},
|
|
270
|
+
"diagnostics": {
|
|
271
|
+
"directory": diagnostics_dir.name if exports.diagnostics else None,
|
|
272
|
+
"native_shapes": {
|
|
273
|
+
name: details.get("shape")
|
|
274
|
+
for name, details in diagnostic_stats.get("layers", {}).items()
|
|
275
|
+
if isinstance(details, dict) and details.get("shape") is not None
|
|
276
|
+
},
|
|
277
|
+
"orientation_validation": diagnostic_stats.get("orientation_validation"),
|
|
278
|
+
},
|
|
279
|
+
"monuments": {
|
|
280
|
+
"file": monuments_path.name if exports.monuments else None,
|
|
281
|
+
"count": int(monument_data["monument_count"]),
|
|
282
|
+
"unique_prefab_count": int(monument_data["unique_prefab_count"]),
|
|
283
|
+
},
|
|
284
|
+
"terrain": render_metadata,
|
|
285
|
+
"tunnels": tunnel_metadata,
|
|
286
|
+
"no_build_zones": no_build_metadata,
|
|
287
|
+
"timings": timings,
|
|
288
|
+
}
|
|
289
|
+
artifacts: dict[str, int] = {}
|
|
290
|
+
if npz_path:
|
|
291
|
+
artifacts[npz_path.name] = npz_path.stat().st_size
|
|
292
|
+
if preview_dir:
|
|
293
|
+
artifacts[f"{preview_dir.name}-total"] = sum(p.stat().st_size for p in preview_dir.glob("*.png"))
|
|
294
|
+
if exports.diagnostics:
|
|
295
|
+
artifacts["diagnostics-total"] = sum(p.stat().st_size for p in diagnostics_dir.glob("*.*"))
|
|
296
|
+
if exports.monuments:
|
|
297
|
+
artifacts[monuments_path.name] = monuments_path.stat().st_size
|
|
298
|
+
if render_metadata:
|
|
299
|
+
artifacts.update({str(name): int(size) for name, size in render_metadata.get("artifacts", {}).items()})
|
|
300
|
+
for enabled, names in (
|
|
301
|
+
(tunnel_options is not None, ("tunnels.png", "tunnels_on_map.png", "tunnels_metadata.json")),
|
|
302
|
+
(no_build_options is not None, ("no_build_zones.png", "no_build_zones_on_map.png", "no_build_zones.json")),
|
|
303
|
+
):
|
|
304
|
+
if enabled:
|
|
305
|
+
for name in names:
|
|
306
|
+
path = output / name
|
|
307
|
+
if path.is_file():
|
|
308
|
+
artifacts[name] = path.stat().st_size
|
|
309
|
+
elapsed = time.perf_counter() - started
|
|
310
|
+
metadata["generation"] = {"elapsed_seconds": elapsed, "artifact_sizes_bytes": artifacts}
|
|
311
|
+
_write_metadata(metadata_path, metadata, artifacts)
|
|
312
|
+
elapsed = time.perf_counter() - started
|
|
313
|
+
metadata["generation"]["elapsed_seconds"] = elapsed
|
|
314
|
+
_write_metadata(metadata_path, metadata, artifacts)
|
|
315
|
+
|
|
316
|
+
if config.timing_debug:
|
|
317
|
+
print("\nRust map export timing breakdown")
|
|
318
|
+
print("-" * 48)
|
|
319
|
+
labels = (
|
|
320
|
+
"map_parse_seconds", "rule_database_load_seconds",
|
|
321
|
+
"heatmap_evaluator_setup_seconds", "heatmaps_total_seconds",
|
|
322
|
+
"heatmap_npz_encode_seconds", "diagnostics_seconds",
|
|
323
|
+
"monuments_seconds", "map_render_seconds",
|
|
324
|
+
"no_build_zones_seconds", "tunnel_render_seconds",
|
|
325
|
+
)
|
|
326
|
+
for label in labels:
|
|
327
|
+
print(f"{label.removesuffix('_seconds').replace('_', ' '):32s} {float(timings[label]):8.3f}s")
|
|
328
|
+
if timings["heatmap_categories"]:
|
|
329
|
+
print(" Slowest heatmap categories:")
|
|
330
|
+
for name, seconds in sorted(
|
|
331
|
+
timings["heatmap_categories"].items(), key=lambda item: item[1], reverse=True
|
|
332
|
+
)[:8]:
|
|
333
|
+
print(f" {name:28s} {seconds:8.3f}s")
|
|
334
|
+
print("-" * 48)
|
|
335
|
+
print(f"{'total':32s} {elapsed:8.3f}s\n")
|
|
336
|
+
return metadata
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
class RustMapExporter:
|
|
340
|
+
"""Run selected map export stages from a validated configuration."""
|
|
341
|
+
|
|
342
|
+
def __init__(self, config: ExportConfig):
|
|
343
|
+
self.config = config.validated()
|
|
344
|
+
|
|
345
|
+
def run(self) -> ExportResult:
|
|
346
|
+
config = self.config
|
|
347
|
+
exports = config.exports
|
|
348
|
+
needs_rules = exports.heatmaps is not None
|
|
349
|
+
needs_manifest = bool(exports.monuments or exports.tunnels or exports.no_build_zones)
|
|
350
|
+
with ExitStack() as stack:
|
|
351
|
+
rules = None
|
|
352
|
+
if needs_rules:
|
|
353
|
+
rules = config.data.spawn_rules_path
|
|
354
|
+
if rules is None:
|
|
355
|
+
rules = stack.enter_context(resources.as_file(
|
|
356
|
+
resources.files("rustmap.data").joinpath("spawn_rules.json")
|
|
357
|
+
))
|
|
358
|
+
manifest = None
|
|
359
|
+
if needs_manifest:
|
|
360
|
+
manifest = config.data.prefab_manifest_path
|
|
361
|
+
if manifest is None:
|
|
362
|
+
manifest = stack.enter_context(resources.as_file(
|
|
363
|
+
resources.files("rustmap.data").joinpath("prefab_manifest.json")
|
|
364
|
+
))
|
|
365
|
+
metadata = _generate(config, Path(rules) if rules else None,
|
|
366
|
+
Path(manifest) if manifest else None)
|
|
367
|
+
|
|
368
|
+
output = config.output_dir
|
|
369
|
+
heatmaps = exports.heatmaps
|
|
370
|
+
terrain = exports.terrain
|
|
371
|
+
tiles = terrain.tiles if terrain else None
|
|
372
|
+
tunnel_options = exports.tunnels
|
|
373
|
+
no_build_options = exports.no_build_zones
|
|
374
|
+
tunnel_metadata = metadata["tunnels"]
|
|
375
|
+
no_build_metadata = metadata["no_build_zones"]
|
|
376
|
+
return ExportResult(
|
|
377
|
+
output_dir=output,
|
|
378
|
+
world_size=int(metadata["map"]["world_size"]),
|
|
379
|
+
elapsed_seconds=float(metadata["generation"]["elapsed_seconds"]),
|
|
380
|
+
metadata_file=output / "export_metadata.json",
|
|
381
|
+
metadata=metadata,
|
|
382
|
+
heatmap_categories=tuple(sorted(metadata["heatmaps"]["categories"])),
|
|
383
|
+
heatmaps_file=(output / metadata["heatmaps"]["file"]) if heatmaps else None,
|
|
384
|
+
monuments_file=(output / "monuments.json") if exports.monuments else None,
|
|
385
|
+
monument_count=int(metadata["monuments"]["count"]),
|
|
386
|
+
map_image=(output / "map_render.png")
|
|
387
|
+
if terrain and not terrain.full_size and "png" in terrain.formats else None,
|
|
388
|
+
full_map_image=(output / "map_render_full.png")
|
|
389
|
+
if terrain and terrain.full_size else None,
|
|
390
|
+
map_tiles_dir=(output / "map_render_tiles") if tiles else None,
|
|
391
|
+
map_tiles_metadata_file=(output / "map_render_tiles" / "tiles.json") if tiles else None,
|
|
392
|
+
map_tile_count=int(((metadata["terrain"] or {}).get("full_size_tiles") or {}).get("tile_count", 0)),
|
|
393
|
+
diagnostics_dir=(output / "diagnostics") if exports.diagnostics else None,
|
|
394
|
+
tunnels_image=(output / "tunnels.png")
|
|
395
|
+
if (tunnel_metadata.get("status") == "rendered" and
|
|
396
|
+
tunnel_options and tunnel_options.export_layer) else None,
|
|
397
|
+
tunnels_overlay_image=(output / "tunnels_on_map.png")
|
|
398
|
+
if tunnel_metadata.get("overlay_file") else None,
|
|
399
|
+
tunnels_metadata_file=(output / "tunnels_metadata.json") if exports.tunnels else None,
|
|
400
|
+
tunnel_render_status=str(tunnel_metadata.get("status", "disabled")),
|
|
401
|
+
no_build_zones_image=(output / "no_build_zones.png")
|
|
402
|
+
if no_build_options and no_build_options.export_images else None,
|
|
403
|
+
no_build_zones_overlay_image=(output / "no_build_zones_on_map.png")
|
|
404
|
+
if no_build_metadata.get("overlay_file") else None,
|
|
405
|
+
no_build_zones_file=(output / "no_build_zones.json")
|
|
406
|
+
if no_build_options and no_build_options.export_json else None,
|
|
407
|
+
no_build_zone_count=int(no_build_metadata.get("zone_count", 0)),
|
|
408
|
+
no_build_zone_status=str(no_build_metadata.get("status", "disabled")),
|
|
409
|
+
)
|