maps4fs 1.2.3__py3-none-any.whl → 1.4.1__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.
Potentially problematic release.
This version of maps4fs might be problematic. Click here for more details.
- maps4fs/__init__.py +10 -1
- maps4fs/generator/background.py +196 -36
- maps4fs/generator/component.py +85 -23
- maps4fs/generator/config.py +1 -1
- maps4fs/generator/dem.py +11 -13
- maps4fs/generator/game.py +1 -1
- maps4fs/generator/grle.py +13 -14
- maps4fs/generator/i3d.py +169 -35
- maps4fs/generator/map.py +184 -7
- maps4fs/generator/texture.py +119 -33
- {maps4fs-1.2.3.dist-info → maps4fs-1.4.1.dist-info}/METADATA +44 -17
- maps4fs-1.4.1.dist-info/RECORD +21 -0
- maps4fs-1.2.3.dist-info/RECORD +0 -21
- {maps4fs-1.2.3.dist-info → maps4fs-1.4.1.dist-info}/LICENSE.md +0 -0
- {maps4fs-1.2.3.dist-info → maps4fs-1.4.1.dist-info}/WHEEL +0 -0
- {maps4fs-1.2.3.dist-info → maps4fs-1.4.1.dist-info}/top_level.txt +0 -0
maps4fs/generator/grle.py
CHANGED
@@ -40,13 +40,10 @@ class GRLE(Component):
|
|
40
40
|
"""Gets the path to the map I3D file from the game instance and saves it to the instance
|
41
41
|
attribute. If the game does not support I3D files, the attribute is set to None."""
|
42
42
|
|
43
|
-
self.farmland_margin = self.kwargs.get("farmland_margin", 0)
|
44
|
-
self.randomize_plants = self.kwargs.get("randomize_plants", True)
|
45
|
-
|
46
43
|
try:
|
47
44
|
grle_schema_path = self.game.grle_schema
|
48
45
|
except ValueError:
|
49
|
-
self.logger.
|
46
|
+
self.logger.warning("GRLE schema processing is not implemented for this game.")
|
50
47
|
return
|
51
48
|
|
52
49
|
try:
|
@@ -60,7 +57,7 @@ class GRLE(Component):
|
|
60
57
|
def process(self) -> None:
|
61
58
|
"""Generates InfoLayer PNG files based on the GRLE schema."""
|
62
59
|
if not self._grle_schema:
|
63
|
-
self.logger.
|
60
|
+
self.logger.debug("GRLE schema is not obtained, skipping the processing.")
|
64
61
|
return
|
65
62
|
|
66
63
|
for info_layer in self._grle_schema:
|
@@ -87,7 +84,7 @@ class GRLE(Component):
|
|
87
84
|
|
88
85
|
self._add_farmlands()
|
89
86
|
if self.game.code == "FS25":
|
90
|
-
self.logger.
|
87
|
+
self.logger.debug("Game is %s, plants will be added.", self.game.code)
|
91
88
|
self._add_plants()
|
92
89
|
else:
|
93
90
|
self.logger.warning("Adding plants it's not supported for the %s.", self.game.code)
|
@@ -123,7 +120,7 @@ class GRLE(Component):
|
|
123
120
|
self.game.weights_dir_path(self.map_directory), "infoLayer_farmlands.png"
|
124
121
|
)
|
125
122
|
|
126
|
-
self.logger.
|
123
|
+
self.logger.debug(
|
127
124
|
"Adding farmlands to the InfoLayer PNG file: %s.", info_layer_farmlands_path
|
128
125
|
)
|
129
126
|
|
@@ -147,8 +144,10 @@ class GRLE(Component):
|
|
147
144
|
|
148
145
|
for field in fields:
|
149
146
|
try:
|
150
|
-
fitted_field = self.
|
151
|
-
field,
|
147
|
+
fitted_field = self.fit_object_into_bounds(
|
148
|
+
polygon_points=field,
|
149
|
+
margin=self.map.grle_settings.farmland_margin,
|
150
|
+
angle=self.rotation,
|
152
151
|
)
|
153
152
|
except ValueError as e:
|
154
153
|
self.logger.warning(
|
@@ -193,10 +192,10 @@ class GRLE(Component):
|
|
193
192
|
|
194
193
|
tree.write(farmlands_xml_path)
|
195
194
|
|
196
|
-
self.logger.
|
195
|
+
self.logger.debug("Farmlands added to the farmlands XML file: %s.", farmlands_xml_path)
|
197
196
|
|
198
197
|
cv2.imwrite(info_layer_farmlands_path, image) # pylint: disable=no-member
|
199
|
-
self.logger.
|
198
|
+
self.logger.debug(
|
200
199
|
"Farmlands added to the InfoLayer PNG file: %s.", info_layer_farmlands_path
|
201
200
|
)
|
202
201
|
|
@@ -357,8 +356,8 @@ class GRLE(Component):
|
|
357
356
|
|
358
357
|
# Add islands of plants to the base image.
|
359
358
|
island_count = self.map_size
|
360
|
-
self.logger.
|
361
|
-
if self.
|
359
|
+
self.logger.debug("Adding %s islands of plants to the base image.", island_count)
|
360
|
+
if self.map.grle_settings.random_plants:
|
362
361
|
grass_image_copy = create_island_of_plants(grass_image_copy, island_count)
|
363
362
|
self.logger.debug("Islands of plants added to the base image.")
|
364
363
|
|
@@ -394,4 +393,4 @@ class GRLE(Component):
|
|
394
393
|
# Ensure that order of channels is correct because CV2 uses BGR and we need RGB.
|
395
394
|
density_map_fruits = cv2.cvtColor(density_map_fruits, cv2.COLOR_BGR2RGB)
|
396
395
|
cv2.imwrite(density_map_fruit_path, density_map_fruits)
|
397
|
-
self.logger.
|
396
|
+
self.logger.debug("Updated density map for fruits saved in %s.", density_map_fruit_path)
|
maps4fs/generator/i3d.py
CHANGED
@@ -19,7 +19,8 @@ DISPLACEMENT_LAYER_SIZE_FOR_BIG_MAPS = 32768
|
|
19
19
|
DEFAULT_MAX_LOD_DISTANCE = 10000
|
20
20
|
DEFAULT_MAX_LOD_OCCLUDER_DISTANCE = 10000
|
21
21
|
NODE_ID_STARTING_VALUE = 2000
|
22
|
-
|
22
|
+
SPLINES_NODE_ID_STARTING_VALUE = 5000
|
23
|
+
TREE_NODE_ID_STARTING_VALUE = 10000
|
23
24
|
DEFAULT_FOREST_DENSITY = 10
|
24
25
|
|
25
26
|
|
@@ -43,28 +44,25 @@ class I3d(Component):
|
|
43
44
|
def preprocess(self) -> None:
|
44
45
|
"""Gets the path to the map I3D file from the game instance and saves it to the instance
|
45
46
|
attribute. If the game does not support I3D files, the attribute is set to None."""
|
46
|
-
self.auto_process = self.kwargs.get("auto_process", False)
|
47
|
-
|
48
47
|
try:
|
49
48
|
self._map_i3d_path = self.game.i3d_file_path(self.map_directory)
|
50
49
|
self.logger.debug("Map I3D path: %s.", self._map_i3d_path)
|
51
50
|
except NotImplementedError:
|
52
|
-
self.logger.
|
51
|
+
self.logger.warning("I3D file processing is not implemented for this game.")
|
53
52
|
self._map_i3d_path = None
|
54
53
|
|
55
|
-
self.forest_density = self.kwargs.get("forest_density", DEFAULT_FOREST_DENSITY)
|
56
|
-
self.logger.info("Forest density: %s.", self.forest_density)
|
57
|
-
|
58
54
|
def process(self) -> None:
|
59
55
|
"""Updates the map I3D file with the default settings."""
|
60
56
|
self._update_i3d_file()
|
61
57
|
self._add_fields()
|
62
|
-
self.
|
58
|
+
if self.game.code == "FS25":
|
59
|
+
self._add_forests()
|
60
|
+
self._add_splines()
|
63
61
|
|
64
62
|
def _get_tree(self) -> ET.ElementTree | None:
|
65
63
|
"""Returns the ElementTree instance of the map I3D file."""
|
66
64
|
if not self._map_i3d_path:
|
67
|
-
self.logger.
|
65
|
+
self.logger.debug("I3D is not obtained, skipping the update.")
|
68
66
|
return None
|
69
67
|
if not os.path.isfile(self._map_i3d_path):
|
70
68
|
self.logger.warning("I3D file not found: %s.", self._map_i3d_path)
|
@@ -84,7 +82,7 @@ class I3d(Component):
|
|
84
82
|
root = tree.getroot()
|
85
83
|
for map_elem in root.iter("Scene"):
|
86
84
|
for terrain_elem in map_elem.iter("TerrainTransformGroup"):
|
87
|
-
if self.auto_process:
|
85
|
+
if self.map.dem_settings.auto_process:
|
88
86
|
terrain_elem.set("heightScale", str(DEFAULT_HEIGHT_SCALE))
|
89
87
|
self.logger.debug(
|
90
88
|
"heightScale attribute set to %s in TerrainTransformGroup element.",
|
@@ -122,7 +120,7 @@ class I3d(Component):
|
|
122
120
|
)
|
123
121
|
|
124
122
|
tree.write(self._map_i3d_path) # type: ignore
|
125
|
-
self.logger.
|
123
|
+
self.logger.debug("Map I3D file saved to: %s.", self._map_i3d_path)
|
126
124
|
|
127
125
|
def previews(self) -> list[str]:
|
128
126
|
"""Returns a list of paths to the preview images (empty list).
|
@@ -133,6 +131,134 @@ class I3d(Component):
|
|
133
131
|
"""
|
134
132
|
return []
|
135
133
|
|
134
|
+
# pylint: disable=R0914, R0915
|
135
|
+
def _add_splines(self) -> None:
|
136
|
+
"""Adds splines to the map I3D file."""
|
137
|
+
splines_i3d_path = os.path.join(self.map_directory, "map", "splines.i3d")
|
138
|
+
if not os.path.isfile(splines_i3d_path):
|
139
|
+
self.logger.warning("Splines I3D file not found: %s.", splines_i3d_path)
|
140
|
+
return
|
141
|
+
|
142
|
+
tree = ET.parse(splines_i3d_path)
|
143
|
+
|
144
|
+
textures_info_layer_path = self.get_infolayer_path("textures")
|
145
|
+
if not textures_info_layer_path:
|
146
|
+
return
|
147
|
+
|
148
|
+
with open(textures_info_layer_path, "r", encoding="utf-8") as textures_info_layer_file:
|
149
|
+
textures_info_layer = json.load(textures_info_layer_file)
|
150
|
+
|
151
|
+
roads_polylines: list[list[tuple[int, int]]] | None = textures_info_layer.get(
|
152
|
+
"roads_polylines"
|
153
|
+
)
|
154
|
+
|
155
|
+
if not roads_polylines:
|
156
|
+
self.logger.warning("Roads polylines data not found in textures info layer.")
|
157
|
+
return
|
158
|
+
|
159
|
+
self.logger.info("Found %s roads polylines in textures info layer.", len(roads_polylines))
|
160
|
+
self.logger.debug("Starging to add roads polylines to the I3D file.")
|
161
|
+
|
162
|
+
root = tree.getroot()
|
163
|
+
# Find <Shapes> element in the I3D file.
|
164
|
+
shapes_node = root.find(".//Shapes")
|
165
|
+
# Find <Scene> element in the I3D file.
|
166
|
+
scene_node = root.find(".//Scene")
|
167
|
+
|
168
|
+
# Read the not resized DEM to obtain Z values for spline points.
|
169
|
+
background_component = self.map.get_component("Background")
|
170
|
+
if not background_component:
|
171
|
+
self.logger.warning("Background component not found.")
|
172
|
+
return
|
173
|
+
|
174
|
+
# pylint: disable=no-member
|
175
|
+
not_resized_dem = cv2.imread(
|
176
|
+
background_component.not_resized_path, cv2.IMREAD_UNCHANGED # type: ignore
|
177
|
+
)
|
178
|
+
self.logger.debug(
|
179
|
+
"Not resized DEM loaded from: %s. Shape: %s.",
|
180
|
+
background_component.not_resized_path, # type: ignore
|
181
|
+
not_resized_dem.shape,
|
182
|
+
)
|
183
|
+
dem_x_size, dem_y_size = not_resized_dem.shape
|
184
|
+
|
185
|
+
if shapes_node is not None and scene_node is not None:
|
186
|
+
node_id = SPLINES_NODE_ID_STARTING_VALUE
|
187
|
+
|
188
|
+
for road_id, road in enumerate(roads_polylines, start=1):
|
189
|
+
# Add to scene node
|
190
|
+
# <Shape name="spline01_CSV" translation="0 0 0" nodeId="11" shapeId="11"/>
|
191
|
+
|
192
|
+
try:
|
193
|
+
fitted_road = self.fit_object_into_bounds(
|
194
|
+
linestring_points=road, angle=self.rotation
|
195
|
+
)
|
196
|
+
except ValueError as e:
|
197
|
+
self.logger.warning(
|
198
|
+
"Road %s could not be fitted into the map bounds with error: %s",
|
199
|
+
road_id,
|
200
|
+
e,
|
201
|
+
)
|
202
|
+
continue
|
203
|
+
|
204
|
+
self.logger.debug("Road %s has %s points.", road_id, len(fitted_road))
|
205
|
+
fitted_road = self.interpolate_points(
|
206
|
+
fitted_road, num_points=self.map.spline_settings.spline_density
|
207
|
+
)
|
208
|
+
self.logger.debug(
|
209
|
+
"Road %s has %s points after interpolation.", road_id, len(fitted_road)
|
210
|
+
)
|
211
|
+
|
212
|
+
spline_name = f"spline{road_id}"
|
213
|
+
|
214
|
+
shape_node = ET.Element("Shape")
|
215
|
+
shape_node.set("name", spline_name)
|
216
|
+
shape_node.set("translation", "0 0 0")
|
217
|
+
shape_node.set("nodeId", str(node_id))
|
218
|
+
shape_node.set("shapeId", str(node_id))
|
219
|
+
|
220
|
+
scene_node.append(shape_node)
|
221
|
+
|
222
|
+
road_ccs = [self.top_left_coordinates_to_center(point) for point in fitted_road]
|
223
|
+
|
224
|
+
# Add to shapes node
|
225
|
+
# <NurbsCurve name="spline01_CSV" shapeId="11" degree="3" form="open">
|
226
|
+
|
227
|
+
nurbs_curve_node = ET.Element("NurbsCurve")
|
228
|
+
nurbs_curve_node.set("name", spline_name)
|
229
|
+
nurbs_curve_node.set("shapeId", str(node_id))
|
230
|
+
nurbs_curve_node.set("degree", "3")
|
231
|
+
nurbs_curve_node.set("form", "open")
|
232
|
+
|
233
|
+
# Now for each point in the road add the following entry to nurbs_curve_node
|
234
|
+
# <cv c="-224.548401, 427.297546, -2047.570312" />
|
235
|
+
# The second coordinate (Z) will be 0 at the moment.
|
236
|
+
|
237
|
+
for point_ccs, point in zip(road_ccs, fitted_road):
|
238
|
+
cx, cy = point_ccs
|
239
|
+
x, y = point
|
240
|
+
|
241
|
+
x = int(x)
|
242
|
+
y = int(y)
|
243
|
+
|
244
|
+
x = max(0, min(x, dem_x_size - 1))
|
245
|
+
y = max(0, min(y, dem_y_size - 1))
|
246
|
+
|
247
|
+
z = not_resized_dem[y, x]
|
248
|
+
z /= 32 # Yes, it's a magic number here.
|
249
|
+
|
250
|
+
cv_node = ET.Element("cv")
|
251
|
+
cv_node.set("c", f"{cx}, {z}, {cy}")
|
252
|
+
|
253
|
+
nurbs_curve_node.append(cv_node)
|
254
|
+
|
255
|
+
shapes_node.append(nurbs_curve_node)
|
256
|
+
|
257
|
+
node_id += 1
|
258
|
+
|
259
|
+
tree.write(splines_i3d_path) # type: ignore
|
260
|
+
self.logger.debug("Splines I3D file saved to: %s.", splines_i3d_path)
|
261
|
+
|
136
262
|
# pylint: disable=R0914, R0915
|
137
263
|
def _add_fields(self) -> None:
|
138
264
|
"""Adds fields to the map I3D file."""
|
@@ -170,7 +296,9 @@ class I3d(Component):
|
|
170
296
|
|
171
297
|
for field in fields:
|
172
298
|
try:
|
173
|
-
fitted_field = self.
|
299
|
+
fitted_field = self.fit_object_into_bounds(
|
300
|
+
polygon_points=field, angle=self.rotation
|
301
|
+
)
|
174
302
|
except ValueError as e:
|
175
303
|
self.logger.warning(
|
176
304
|
"Field %s could not be fitted into the map bounds with error: %s",
|
@@ -239,7 +367,7 @@ class I3d(Component):
|
|
239
367
|
field_id += 1
|
240
368
|
|
241
369
|
tree.write(self._map_i3d_path) # type: ignore
|
242
|
-
self.logger.
|
370
|
+
self.logger.debug("Map I3D file saved to: %s.", self._map_i3d_path)
|
243
371
|
|
244
372
|
def get_name_indicator_node(self, node_id: int, field_id: int) -> tuple[ET.Element, int]:
|
245
373
|
"""Creates a name indicator node with given node ID and field ID.
|
@@ -332,24 +460,28 @@ class I3d(Component):
|
|
332
460
|
# pylint: disable=R0911
|
333
461
|
def _add_forests(self) -> None:
|
334
462
|
"""Adds forests to the map I3D file."""
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
463
|
+
custom_schema = self.kwargs.get("tree_custom_schema")
|
464
|
+
if custom_schema:
|
465
|
+
tree_schema = custom_schema
|
466
|
+
else:
|
467
|
+
try:
|
468
|
+
tree_schema_path = self.game.tree_schema
|
469
|
+
except ValueError:
|
470
|
+
self.logger.warning("Tree schema path not set for the Game %s.", self.game.code)
|
471
|
+
return
|
472
|
+
|
473
|
+
if not os.path.isfile(tree_schema_path):
|
474
|
+
self.logger.warning("Tree schema file was not found: %s.", tree_schema_path)
|
475
|
+
return
|
476
|
+
|
477
|
+
try:
|
478
|
+
with open(tree_schema_path, "r", encoding="utf-8") as tree_schema_file:
|
479
|
+
tree_schema = json.load(tree_schema_file) # type: ignore
|
480
|
+
except json.JSONDecodeError as e:
|
481
|
+
self.logger.warning(
|
482
|
+
"Could not load tree schema from %s with error: %s", tree_schema_path, e
|
483
|
+
)
|
484
|
+
return
|
353
485
|
|
354
486
|
texture_component: Texture | None = self.map.get_component("Texture") # type: ignore
|
355
487
|
if not texture_component:
|
@@ -395,14 +527,16 @@ class I3d(Component):
|
|
395
527
|
forest_image = cv2.imread(forest_image_path, cv2.IMREAD_UNCHANGED)
|
396
528
|
|
397
529
|
tree_count = 0
|
398
|
-
for x, y in self.non_empty_pixels(forest_image, step=self.forest_density):
|
530
|
+
for x, y in self.non_empty_pixels(forest_image, step=self.map.i3d_settings.forest_density):
|
399
531
|
xcs, ycs = self.top_left_coordinates_to_center((x, y))
|
400
532
|
node_id += 1
|
401
533
|
|
402
534
|
rotation = randint(-180, 180)
|
403
|
-
xcs, ycs = self.randomize_coordinates(
|
535
|
+
xcs, ycs = self.randomize_coordinates( # type: ignore
|
536
|
+
(xcs, ycs), self.map.i3d_settings.forest_density
|
537
|
+
)
|
404
538
|
|
405
|
-
random_tree = choice(tree_schema)
|
539
|
+
random_tree = choice(tree_schema) # type: ignore
|
406
540
|
tree_name = random_tree["name"]
|
407
541
|
tree_id = random_tree["reference_id"]
|
408
542
|
|
@@ -420,7 +554,7 @@ class I3d(Component):
|
|
420
554
|
self.logger.info("Added %s trees to the I3D file.", tree_count)
|
421
555
|
|
422
556
|
tree.write(self._map_i3d_path) # type: ignore
|
423
|
-
self.logger.
|
557
|
+
self.logger.debug("Map I3D file saved to: %s.", self._map_i3d_path)
|
424
558
|
|
425
559
|
@staticmethod
|
426
560
|
def randomize_coordinates(coordinates: tuple[int, int], density: int) -> tuple[float, float]:
|
maps4fs/generator/map.py
CHANGED
@@ -2,16 +2,135 @@
|
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
+
import json
|
5
6
|
import os
|
6
7
|
import shutil
|
7
8
|
from typing import Any, Generator
|
8
9
|
|
10
|
+
from pydantic import BaseModel
|
11
|
+
|
9
12
|
from maps4fs.generator.component import Component
|
10
13
|
from maps4fs.generator.game import Game
|
11
14
|
from maps4fs.logger import Logger
|
12
15
|
|
13
16
|
|
14
|
-
|
17
|
+
class SettingsModel(BaseModel):
|
18
|
+
"""Base class for settings models. It provides methods to convert settings to and from JSON."""
|
19
|
+
|
20
|
+
@classmethod
|
21
|
+
def all_settings_to_json(cls) -> dict[str, dict[str, Any]]:
|
22
|
+
"""Get all settings of the current class and its subclasses as a dictionary.
|
23
|
+
|
24
|
+
Returns:
|
25
|
+
dict[str, dict[str, Any]]: Dictionary with settings of the current class and its
|
26
|
+
subclasses.
|
27
|
+
"""
|
28
|
+
all_settings = {}
|
29
|
+
for subclass in cls.__subclasses__():
|
30
|
+
all_settings[subclass.__name__] = subclass().model_dump()
|
31
|
+
|
32
|
+
return all_settings
|
33
|
+
|
34
|
+
@classmethod
|
35
|
+
def all_settings_from_json(cls, data: dict) -> dict[str, SettingsModel]:
|
36
|
+
"""Create settings instances from JSON data.
|
37
|
+
|
38
|
+
Arguments:
|
39
|
+
data (dict): JSON data.
|
40
|
+
|
41
|
+
Returns:
|
42
|
+
dict[str, Type[SettingsModel]]: Dictionary with settings instances.
|
43
|
+
"""
|
44
|
+
settings = {}
|
45
|
+
for subclass in cls.__subclasses__():
|
46
|
+
settings[subclass.__name__] = subclass(**data[subclass.__name__])
|
47
|
+
|
48
|
+
return settings
|
49
|
+
|
50
|
+
|
51
|
+
class DEMSettings(SettingsModel):
|
52
|
+
"""Represents the advanced settings for DEM component.
|
53
|
+
|
54
|
+
Attributes:
|
55
|
+
auto_process (bool): use the auto preset to change the multiplier.
|
56
|
+
multiplier (int): multiplier for the heightmap, every pixel will be multiplied by this
|
57
|
+
value.
|
58
|
+
blur_radius (int): radius of the blur filter.
|
59
|
+
plateau (int): plateau height, will be added to each pixel.
|
60
|
+
water_depth (int): water depth, will be subtracted from each pixel where the water
|
61
|
+
is present.
|
62
|
+
"""
|
63
|
+
|
64
|
+
auto_process: bool = True
|
65
|
+
multiplier: int = 1
|
66
|
+
blur_radius: int = 35
|
67
|
+
plateau: int = 0
|
68
|
+
water_depth: int = 0
|
69
|
+
|
70
|
+
|
71
|
+
class BackgroundSettings(SettingsModel):
|
72
|
+
"""Represents the advanced settings for background component.
|
73
|
+
|
74
|
+
Attributes:
|
75
|
+
generate_background (bool): generate obj files for the background terrain.
|
76
|
+
generate_water (bool): generate obj files for the water.
|
77
|
+
resize_factor (int): resize factor for the background terrain and water.
|
78
|
+
It will be used as 1 / resize_factor of the original size.
|
79
|
+
"""
|
80
|
+
|
81
|
+
generate_background: bool = True
|
82
|
+
generate_water: bool = True
|
83
|
+
resize_factor: int = 8
|
84
|
+
|
85
|
+
|
86
|
+
class GRLESettings(SettingsModel):
|
87
|
+
"""Represents the advanced settings for GRLE component.
|
88
|
+
|
89
|
+
Attributes:
|
90
|
+
farmland_margin (int): margin around the farmland.
|
91
|
+
random_plants (bool): generate random plants on the map or use the default one.
|
92
|
+
"""
|
93
|
+
|
94
|
+
farmland_margin: int = 0
|
95
|
+
random_plants: bool = True
|
96
|
+
|
97
|
+
|
98
|
+
class I3DSettings(SettingsModel):
|
99
|
+
"""Represents the advanced settings for I3D component.
|
100
|
+
|
101
|
+
Attributes:
|
102
|
+
forest_density (int): density of the forest (distance between trees).
|
103
|
+
"""
|
104
|
+
|
105
|
+
forest_density: int = 10
|
106
|
+
|
107
|
+
|
108
|
+
class TextureSettings(SettingsModel):
|
109
|
+
"""Represents the advanced settings for texture component.
|
110
|
+
|
111
|
+
Attributes:
|
112
|
+
dissolve (bool): dissolve the texture into several images.
|
113
|
+
fields_padding (int): padding around the fields.
|
114
|
+
skip_drains (bool): skip drains generation.
|
115
|
+
"""
|
116
|
+
|
117
|
+
dissolve: bool = True
|
118
|
+
fields_padding: int = 0
|
119
|
+
skip_drains: bool = False
|
120
|
+
|
121
|
+
|
122
|
+
class SplineSettings(SettingsModel):
|
123
|
+
"""Represents the advanced settings for spline component.
|
124
|
+
|
125
|
+
Attributes:
|
126
|
+
spline_density (int): the number of extra points that will be added between each two
|
127
|
+
existing points.
|
128
|
+
"""
|
129
|
+
|
130
|
+
spline_density: int = 4
|
131
|
+
|
132
|
+
|
133
|
+
# pylint: disable=R0913, R0902, R0914
|
15
134
|
class Map:
|
16
135
|
"""Class used to generate map using all components.
|
17
136
|
|
@@ -23,7 +142,7 @@ class Map:
|
|
23
142
|
logger (Any): Logger instance
|
24
143
|
"""
|
25
144
|
|
26
|
-
def __init__( # pylint: disable=R0917
|
145
|
+
def __init__( # pylint: disable=R0917, R0915
|
27
146
|
self,
|
28
147
|
game: Game,
|
29
148
|
coordinates: tuple[float, float],
|
@@ -31,6 +150,13 @@ class Map:
|
|
31
150
|
rotation: int,
|
32
151
|
map_directory: str,
|
33
152
|
logger: Any = None,
|
153
|
+
custom_osm: str | None = None,
|
154
|
+
dem_settings: DEMSettings = DEMSettings(),
|
155
|
+
background_settings: BackgroundSettings = BackgroundSettings(),
|
156
|
+
grle_settings: GRLESettings = GRLESettings(),
|
157
|
+
i3d_settings: I3DSettings = I3DSettings(),
|
158
|
+
texture_settings: TextureSettings = TextureSettings(),
|
159
|
+
spline_settings: SplineSettings = SplineSettings(),
|
34
160
|
**kwargs,
|
35
161
|
):
|
36
162
|
if not logger:
|
@@ -53,12 +179,46 @@ class Map:
|
|
53
179
|
|
54
180
|
self.logger.info("Game was set to %s", game.code)
|
55
181
|
|
56
|
-
self.
|
57
|
-
self.logger.info("
|
182
|
+
self.custom_osm = custom_osm
|
183
|
+
self.logger.info("Custom OSM file: %s", custom_osm)
|
184
|
+
|
185
|
+
# Make a copy of a custom osm file to the map directory, so it will be
|
186
|
+
# included in the output archive.
|
187
|
+
if custom_osm:
|
188
|
+
copy_path = os.path.join(self.map_directory, "custom_osm.osm")
|
189
|
+
shutil.copyfile(custom_osm, copy_path)
|
190
|
+
self.logger.debug("Custom OSM file copied to %s", copy_path)
|
191
|
+
|
192
|
+
self.dem_settings = dem_settings
|
193
|
+
self.logger.info("DEM settings: %s", dem_settings)
|
194
|
+
self.background_settings = background_settings
|
195
|
+
self.logger.info("Background settings: %s", background_settings)
|
196
|
+
self.grle_settings = grle_settings
|
197
|
+
self.logger.info("GRLE settings: %s", grle_settings)
|
198
|
+
self.i3d_settings = i3d_settings
|
199
|
+
self.logger.info("I3D settings: %s", i3d_settings)
|
200
|
+
self.texture_settings = texture_settings
|
201
|
+
self.logger.info("Texture settings: %s", texture_settings)
|
202
|
+
self.spline_settings = spline_settings
|
203
|
+
self.logger.info("Spline settings: %s", spline_settings)
|
58
204
|
|
59
205
|
os.makedirs(self.map_directory, exist_ok=True)
|
60
206
|
self.logger.debug("Map directory created: %s", self.map_directory)
|
61
207
|
|
208
|
+
self.texture_custom_schema = kwargs.get("texture_custom_schema", None)
|
209
|
+
if self.texture_custom_schema:
|
210
|
+
save_path = os.path.join(self.map_directory, "texture_custom_schema.json")
|
211
|
+
with open(save_path, "w", encoding="utf-8") as file:
|
212
|
+
json.dump(self.texture_custom_schema, file, indent=4)
|
213
|
+
self.logger.debug("Texture custom schema saved to %s", save_path)
|
214
|
+
|
215
|
+
self.tree_custom_schema = kwargs.get("tree_custom_schema", None)
|
216
|
+
if self.tree_custom_schema:
|
217
|
+
save_path = os.path.join(self.map_directory, "tree_custom_schema.json")
|
218
|
+
with open(save_path, "w", encoding="utf-8") as file:
|
219
|
+
json.dump(self.tree_custom_schema, file, indent=4)
|
220
|
+
self.logger.debug("Tree custom schema saved to %s", save_path)
|
221
|
+
|
62
222
|
try:
|
63
223
|
shutil.unpack_archive(game.template_path, self.map_directory)
|
64
224
|
self.logger.debug("Map template unpacked to %s", self.map_directory)
|
@@ -71,6 +231,14 @@ class Map:
|
|
71
231
|
Yields:
|
72
232
|
Generator[str, None, None]: Component names.
|
73
233
|
"""
|
234
|
+
self.logger.info(
|
235
|
+
"Starting map generation. Game code: %s. Coordinates: %s, size: %s. Rotation: %s.",
|
236
|
+
self.game.code,
|
237
|
+
self.coordinates,
|
238
|
+
self.size,
|
239
|
+
self.rotation,
|
240
|
+
)
|
241
|
+
|
74
242
|
for game_component in self.game.components:
|
75
243
|
component = game_component(
|
76
244
|
self.game,
|
@@ -81,7 +249,8 @@ class Map:
|
|
81
249
|
self.rotation,
|
82
250
|
self.map_directory,
|
83
251
|
self.logger,
|
84
|
-
|
252
|
+
texture_custom_schema=self.texture_custom_schema,
|
253
|
+
tree_custom_schema=self.tree_custom_schema,
|
85
254
|
)
|
86
255
|
self.components.append(component)
|
87
256
|
|
@@ -107,6 +276,14 @@ class Map:
|
|
107
276
|
)
|
108
277
|
raise e
|
109
278
|
|
279
|
+
self.logger.info(
|
280
|
+
"Map generation completed. Game code: %s. Coordinates: %s, size: %s. Rotation: %s.",
|
281
|
+
self.game.code,
|
282
|
+
self.coordinates,
|
283
|
+
self.size,
|
284
|
+
self.rotation,
|
285
|
+
)
|
286
|
+
|
110
287
|
def get_component(self, component_name: str) -> Component | None:
|
111
288
|
"""Get component by name.
|
112
289
|
|
@@ -154,7 +331,7 @@ class Map:
|
|
154
331
|
if remove_source:
|
155
332
|
try:
|
156
333
|
shutil.rmtree(self.map_directory)
|
157
|
-
self.logger.
|
334
|
+
self.logger.debug("Map directory removed: %s", self.map_directory)
|
158
335
|
except Exception as e: # pylint: disable=W0718
|
159
|
-
self.logger.
|
336
|
+
self.logger.debug("Error removing map directory %s: %s", self.map_directory, e)
|
160
337
|
return archive_path
|