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/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.info("GRLE schema processing is not implemented for this game.")
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.info("GRLE schema is not obtained, skipping the processing.")
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.info("Game is %s, plants will be added.", self.game.code)
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.info(
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.fit_polygon_into_bounds(
151
- field, self.farmland_margin, angle=self.rotation
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.info("Farmlands added to the farmlands XML file: %s.", farmlands_xml_path)
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.info(
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.info("Adding %s islands of plants to the base image.", island_count)
361
- if self.randomize_plants:
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.info("Updated density map for fruits saved in %s.", density_map_fruit_path)
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
- TREE_NODE_ID_STARTING_VALUE = 4000
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.info("I3D file processing is not implemented for this game.")
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._add_forests()
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.info("I3D is not obtained, skipping the update.")
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.info("Map I3D file saved to: %s.", self._map_i3d_path)
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.fit_polygon_into_bounds(field, angle=self.rotation)
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.info("Map I3D file saved to: %s.", self._map_i3d_path)
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
- try:
336
- tree_schema_path = self.game.tree_schema
337
- except ValueError:
338
- self.logger.warning("Tree schema path not set for the Game %s.", self.game.code)
339
- return
340
-
341
- if not os.path.isfile(tree_schema_path):
342
- self.logger.warning("Tree schema file was not found: %s.", tree_schema_path)
343
- return
344
-
345
- try:
346
- with open(tree_schema_path, "r", encoding="utf-8") as tree_schema_file:
347
- tree_schema: list[dict[str, str | int]] = json.load(tree_schema_file)
348
- except json.JSONDecodeError as e:
349
- self.logger.warning(
350
- "Could not load tree schema from %s with error: %s", tree_schema_path, e
351
- )
352
- return
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((xcs, ycs), self.forest_density) # type: ignore
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.info("Map I3D file saved to: %s.", self._map_i3d_path)
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
- # pylint: disable=R0913, R0902
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.kwargs = kwargs
57
- self.logger.info("Additional arguments: %s", kwargs)
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
- **self.kwargs,
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.info("Map directory removed: %s", self.map_directory)
334
+ self.logger.debug("Map directory removed: %s", self.map_directory)
158
335
  except Exception as e: # pylint: disable=W0718
159
- self.logger.error("Error removing map directory %s: %s", self.map_directory, e)
336
+ self.logger.debug("Error removing map directory %s: %s", self.map_directory, e)
160
337
  return archive_path