maps4fs 1.5.7__py3-none-any.whl → 1.6.91__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.
maps4fs/generator/grle.py CHANGED
@@ -10,13 +10,26 @@ import numpy as np
10
10
  from shapely.geometry import Polygon # type: ignore
11
11
 
12
12
  from maps4fs.generator.component import Component
13
- from maps4fs.generator.texture import Texture
13
+ from maps4fs.generator.texture import PREVIEW_MAXIMUM_SIZE, Texture
14
14
 
15
- ISLAND_SIZE_MIN = 10
16
- ISLAND_SIZE_MAX = 200
17
15
  ISLAND_DISTORTION = 0.3
18
- ISLAND_VERTEX_COUNT = 30
19
- ISLAND_ROUNDING_RADIUS = 15
16
+
17
+
18
+ def plant_to_pixel_value(plant_name: str) -> int | None:
19
+ """Returns the pixel value representation of the plant.
20
+ If not found, returns None.
21
+
22
+ Arguments:
23
+ plant_name (str): name of the plant
24
+
25
+ Returns:
26
+ int | None: pixel value of the plant or None if not found.
27
+ """
28
+ plants = {
29
+ "smallDenseMix": 33,
30
+ "meadow": 131,
31
+ }
32
+ return plants.get(plant_name)
20
33
 
21
34
 
22
35
  # pylint: disable=W0223
@@ -39,6 +52,7 @@ class GRLE(Component):
39
52
  def preprocess(self) -> None:
40
53
  """Gets the path to the map I3D file from the game instance and saves it to the instance
41
54
  attribute. If the game does not support I3D files, the attribute is set to None."""
55
+ self.preview_paths: dict[str, str] = {}
42
56
 
43
57
  try:
44
58
  grle_schema_path = self.game.grle_schema
@@ -89,6 +103,7 @@ class GRLE(Component):
89
103
  else:
90
104
  self.logger.warning("Adding plants it's not supported for the %s.", self.game.code)
91
105
 
106
+ # pylint: disable=no-member
92
107
  def previews(self) -> list[str]:
93
108
  """Returns a list of paths to the preview images (empty list).
94
109
  The component does not generate any preview images so it returns an empty list.
@@ -96,7 +111,57 @@ class GRLE(Component):
96
111
  Returns:
97
112
  list[str]: An empty list.
98
113
  """
99
- return []
114
+ preview_paths = []
115
+ for preview_name, preview_path in self.preview_paths.items():
116
+ save_path = os.path.join(self.previews_directory, f"{preview_name}.png")
117
+ # Resize the preview image to the maximum size allowed for previews.
118
+ image = cv2.imread(preview_path, cv2.IMREAD_GRAYSCALE)
119
+ if image.shape[0] > PREVIEW_MAXIMUM_SIZE or image.shape[1] > PREVIEW_MAXIMUM_SIZE:
120
+ image = cv2.resize(image, (PREVIEW_MAXIMUM_SIZE, PREVIEW_MAXIMUM_SIZE))
121
+ image_normalized = np.empty_like(image)
122
+ cv2.normalize(image, image_normalized, 0, 255, cv2.NORM_MINMAX)
123
+ image_colored = cv2.applyColorMap(image_normalized, cv2.COLORMAP_JET)
124
+ cv2.imwrite(save_path, image_colored)
125
+ preview_paths.append(save_path)
126
+
127
+ with_fields_save_path = os.path.join(
128
+ self.previews_directory, f"{preview_name}_with_fields.png"
129
+ )
130
+ image_with_fields = self.overlay_fields(image_colored)
131
+ if image_with_fields is None:
132
+ continue
133
+ cv2.imwrite(with_fields_save_path, image_with_fields) # pylint: disable=no-member
134
+ preview_paths.append(with_fields_save_path)
135
+
136
+ return preview_paths
137
+
138
+ def overlay_fields(self, farmlands_np: np.ndarray) -> np.ndarray | None:
139
+ """Overlay fields on the farmlands preview image.
140
+
141
+ Arguments:
142
+ farmlands_np (np.ndarray): The farmlands preview image.
143
+
144
+ Returns:
145
+ np.ndarray | None: The farmlands preview image with fields overlayed on top of it.
146
+ """
147
+ texture_component: Texture | None = self.map.get_component("Texture") # type: ignore
148
+ if not texture_component:
149
+ self.logger.warning("Texture component not found in the map.")
150
+ return None
151
+
152
+ fields_layer = texture_component.get_layer_by_usage("field")
153
+ fields_layer_path = fields_layer.get_preview_or_path( # type: ignore
154
+ self.game.weights_dir_path(self.map_directory)
155
+ )
156
+ if not fields_layer_path or not os.path.isfile(fields_layer_path):
157
+ self.logger.warning("Fields layer not found in the texture component.")
158
+ return None
159
+ fields_np = cv2.imread(fields_layer_path)
160
+ # Resize fields_np to the same size as farmlands_np.
161
+ fields_np = cv2.resize(fields_np, (farmlands_np.shape[1], farmlands_np.shape[0]))
162
+
163
+ # use fields_np as base layer and overlay farmlands_np on top of it with 50% alpha blending.
164
+ return cv2.addWeighted(fields_np, 0.5, farmlands_np, 0.5, 0)
100
165
 
101
166
  # pylint: disable=R0801, R0914
102
167
  def _add_farmlands(self) -> None:
@@ -114,12 +179,12 @@ class GRLE(Component):
114
179
  self.logger.warning("Fields data not found in textures info layer.")
115
180
  return
116
181
 
117
- self.logger.info("Found %s fields in textures info layer.", len(fields))
182
+ self.logger.debug("Found %s fields in textures info layer.", len(fields))
118
183
 
119
184
  farmyards: list[list[tuple[int, int]]] | None = textures_info_layer.get("farmyards")
120
185
  if farmyards and self.map.grle_settings.add_farmyards:
121
186
  fields.extend(farmyards)
122
- self.logger.info("Found %s farmyards in textures info layer.", len(farmyards))
187
+ self.logger.debug("Found %s farmyards in textures info layer.", len(farmyards))
123
188
 
124
189
  info_layer_farmlands_path = os.path.join(
125
190
  self.game.weights_dir_path(self.map_directory), "infoLayer_farmlands.png"
@@ -155,7 +220,7 @@ class GRLE(Component):
155
220
  angle=self.rotation,
156
221
  )
157
222
  except ValueError as e:
158
- self.logger.warning(
223
+ self.logger.debug(
159
224
  "Farmland %s could not be fitted into the map bounds with error: %s",
160
225
  farmland_id,
161
226
  e,
@@ -180,7 +245,7 @@ class GRLE(Component):
180
245
  try:
181
246
  cv2.fillPoly(image, [field_np], farmland_id) # type: ignore
182
247
  except Exception as e: # pylint: disable=W0718
183
- self.logger.warning(
248
+ self.logger.debug(
184
249
  "Farmland %s could not be added to the InfoLayer PNG file with error: %s",
185
250
  farmland_id,
186
251
  e,
@@ -204,6 +269,8 @@ class GRLE(Component):
204
269
  "Farmlands added to the InfoLayer PNG file: %s.", info_layer_farmlands_path
205
270
  )
206
271
 
272
+ self.preview_paths["farmlands"] = info_layer_farmlands_path # type: ignore
273
+
207
274
  # pylint: disable=R0915
208
275
  def _add_plants(self) -> None:
209
276
  """Adds plants to the InfoLayer PNG file."""
@@ -270,10 +337,13 @@ class GRLE(Component):
270
337
  grass_image[forest_image != 0] = 255
271
338
 
272
339
  # B and G channels remain the same (zeros), while we change the R channel.
273
- possible_R_values = [33, 65, 97, 129, 161, 193, 225] # pylint: disable=C0103
340
+ possible_R_values = [65, 97, 129, 161, 193, 225] # pylint: disable=C0103
274
341
 
275
- # 1st approach: Change the non zero values in the base image to 33 (for debug).
276
- # And use the base image as R channel in the density map.
342
+ base_layer_pixel_value = plant_to_pixel_value(
343
+ self.map.grle_settings.base_grass # type:ignore
344
+ )
345
+ if not base_layer_pixel_value:
346
+ base_layer_pixel_value = 131
277
347
 
278
348
  # pylint: disable=no-member
279
349
  def create_island_of_plants(image: np.ndarray, count: int) -> np.ndarray:
@@ -290,23 +360,20 @@ class GRLE(Component):
290
360
  # Randomly choose the value for the island.
291
361
  plant_value = choice(possible_R_values)
292
362
  # Randomly choose the size of the island.
293
- island_size = randint(ISLAND_SIZE_MIN, ISLAND_SIZE_MAX)
363
+ island_size = randint(
364
+ self.map.grle_settings.plants_island_minimum_size, # type:ignore
365
+ self.map.grle_settings.plants_island_maximum_size, # type:ignore
366
+ )
294
367
  # Randomly choose the position of the island.
295
- # x = np.random.randint(0, image.shape[1] - island_size)
296
- # y = np.random.randint(0, image.shape[0] - island_size)
297
368
  x = randint(0, image.shape[1] - island_size)
298
369
  y = randint(0, image.shape[0] - island_size)
299
370
 
300
- # Randomly choose the shape of the island.
301
- # shapes = ["circle", "ellipse", "polygon"]
302
- # shape = choice(shapes)
303
-
304
371
  try:
305
372
  polygon_points = get_rounded_polygon(
306
- num_vertices=ISLAND_VERTEX_COUNT,
373
+ num_vertices=self.map.grle_settings.plants_island_vertex_count,
307
374
  center=(x + island_size // 2, y + island_size // 2),
308
375
  radius=island_size // 2,
309
- rounding_radius=ISLAND_ROUNDING_RADIUS,
376
+ rounding_radius=self.map.grle_settings.plants_island_rounding_radius,
310
377
  )
311
378
  if not polygon_points:
312
379
  continue
@@ -355,16 +422,16 @@ class GRLE(Component):
355
422
  grass_image_copy = grass_image.copy()
356
423
  if forest_image is not None:
357
424
  # Add the forest layer to the base image, to merge the masks.
358
- grass_image_copy[forest_image != 0] = 33
359
- # Set all the non-zero values to 33.
360
- grass_image_copy[grass_image != 0] = 33
425
+ grass_image_copy[forest_image != 0] = base_layer_pixel_value
426
+
427
+ grass_image_copy[grass_image != 0] = base_layer_pixel_value
361
428
 
362
429
  # Add islands of plants to the base image.
363
- island_count = self.map_size
430
+ island_count = int(self.map_size * self.map.grle_settings.plants_island_percent // 100)
364
431
  self.logger.debug("Adding %s islands of plants to the base image.", island_count)
365
432
  if self.map.grle_settings.random_plants:
366
433
  grass_image_copy = create_island_of_plants(grass_image_copy, island_count)
367
- self.logger.debug("Islands of plants added to the base image.")
434
+ self.logger.info("Added %s islands of plants to the base image.", island_count)
368
435
 
369
436
  # Sligtly reduce the size of the grass_image, that we'll use as mask.
370
437
  kernel = np.ones((3, 3), np.uint8)
@@ -380,7 +447,6 @@ class GRLE(Component):
380
447
  grass_image_copy[:, 0] = 0 # Left side
381
448
  grass_image_copy[:, -1] = 0 # Right side
382
449
 
383
- # Value of 33 represents the base grass plant.
384
450
  # After painting it with base grass, we'll create multiple islands of different plants.
385
451
  # On the final step, we'll remove all the values which in pixels
386
452
  # where zerons in the original base image (so we don't paint grass where it should not be).
maps4fs/generator/i3d.py CHANGED
@@ -14,14 +14,10 @@ import numpy as np
14
14
  from maps4fs.generator.component import Component
15
15
  from maps4fs.generator.texture import Texture
16
16
 
17
- DEFAULT_HEIGHT_SCALE = 2000
18
17
  DISPLACEMENT_LAYER_SIZE_FOR_BIG_MAPS = 32768
19
- DEFAULT_MAX_LOD_DISTANCE = 10000
20
- DEFAULT_MAX_LOD_OCCLUDER_DISTANCE = 10000
21
18
  NODE_ID_STARTING_VALUE = 2000
22
19
  SPLINES_NODE_ID_STARTING_VALUE = 5000
23
20
  TREE_NODE_ID_STARTING_VALUE = 10000
24
- DEFAULT_FOREST_DENSITY = 10
25
21
 
26
22
 
27
23
  # pylint: disable=R0903
@@ -81,6 +77,20 @@ class I3d(Component):
81
77
 
82
78
  root = tree.getroot()
83
79
  for map_elem in root.iter("Scene"):
80
+ for terrain_elem in map_elem.iter("TerrainTransformGroup"):
81
+ if self.map.shared_settings.change_height_scale:
82
+ suggested_height_scale = self.map.shared_settings.height_scale_value
83
+ if suggested_height_scale is not None and suggested_height_scale > 255:
84
+ new_height_scale = int(
85
+ self.map.shared_settings.height_scale_value # type: ignore
86
+ )
87
+ terrain_elem.set("heightScale", str(new_height_scale))
88
+ self.logger.info(
89
+ "heightScale attribute set to %s in TerrainTransformGroup element.",
90
+ new_height_scale,
91
+ )
92
+
93
+ self.logger.debug("TerrainTransformGroup element updated in I3D file.")
84
94
  sun_elem = map_elem.find(".//Light[@name='sun']")
85
95
 
86
96
  if sun_elem is not None:
@@ -97,10 +107,6 @@ class I3d(Component):
97
107
  )
98
108
 
99
109
  if self.map_size > 4096:
100
- terrain_elem = root.find(".//TerrainTransformGroup")
101
- if terrain_elem is None:
102
- self.logger.warning("TerrainTransformGroup element not found in I3D file.")
103
- return
104
110
  displacement_layer = terrain_elem.find(".//DisplacementLayer") # pylint: disable=W0631
105
111
 
106
112
  if displacement_layer is not None:
@@ -146,7 +152,7 @@ class I3d(Component):
146
152
  self.logger.warning("Roads polylines data not found in textures info layer.")
147
153
  return
148
154
 
149
- self.logger.info("Found %s roads polylines in textures info layer.", len(roads_polylines))
155
+ self.logger.debug("Found %s roads polylines in textures info layer.", len(roads_polylines))
150
156
  self.logger.debug("Starging to add roads polylines to the I3D file.")
151
157
 
152
158
  root = tree.getroot()
@@ -188,7 +194,7 @@ class I3d(Component):
188
194
  linestring_points=road, angle=self.rotation
189
195
  )
190
196
  except ValueError as e:
191
- self.logger.warning(
197
+ self.logger.debug(
192
198
  "Road %s could not be fitted into the map bounds with error: %s",
193
199
  road_id,
194
200
  e,
@@ -239,7 +245,7 @@ class I3d(Component):
239
245
  y = max(0, min(y, dem_y_size - 1))
240
246
 
241
247
  z = not_resized_dem[y, x]
242
- z /= 32 # Yes, it's a magic number here.
248
+ z *= self.get_z_scaling_factor() # type: ignore
243
249
 
244
250
  cv_node = ET.Element("cv")
245
251
  cv_node.set("c", f"{cx}, {z}, {cy}")
@@ -291,7 +297,7 @@ class I3d(Component):
291
297
  self.logger.warning("Fields data not found in textures info layer.")
292
298
  return
293
299
 
294
- self.logger.info("Found %s fields in textures info layer.", len(fields))
300
+ self.logger.debug("Found %s fields in textures info layer.", len(fields))
295
301
  self.logger.debug("Starging to add fields to the I3D file.")
296
302
 
297
303
  root = tree.getroot()
@@ -313,7 +319,7 @@ class I3d(Component):
313
319
  polygon_points=field, angle=self.rotation
314
320
  )
315
321
  except ValueError as e:
316
- self.logger.warning(
322
+ self.logger.debug(
317
323
  "Field %s could not be fitted into the map bounds with error: %s",
318
324
  field_id,
319
325
  e,
@@ -327,7 +333,7 @@ class I3d(Component):
327
333
  try:
328
334
  cx, cy = self.get_polygon_center(field_ccs)
329
335
  except Exception as e: # pylint: disable=W0718
330
- self.logger.warning(
336
+ self.logger.debug(
331
337
  "Field %s could not be fitted into the map bounds.", field_id
332
338
  )
333
339
  self.logger.debug("Error: %s", e)
maps4fs/generator/map.py CHANGED
@@ -8,7 +8,7 @@ import shutil
8
8
  from typing import Any, Generator
9
9
 
10
10
  from maps4fs.generator.component import Component
11
- from maps4fs.generator.dtm import DTMProvider, DTMProviderSettings
11
+ from maps4fs.generator.dtm.dtm import DTMProvider, DTMProviderSettings
12
12
  from maps4fs.generator.game import Game
13
13
  from maps4fs.generator.settings import (
14
14
  BackgroundSettings,
@@ -16,6 +16,7 @@ from maps4fs.generator.settings import (
16
16
  GRLESettings,
17
17
  I3DSettings,
18
18
  SatelliteSettings,
19
+ SharedSettings,
19
20
  SplineSettings,
20
21
  TextureSettings,
21
22
  )
@@ -88,6 +89,17 @@ class Map:
88
89
 
89
90
  self.dem_settings = dem_settings
90
91
  self.logger.info("DEM settings: %s", dem_settings)
92
+ if self.dem_settings.water_depth > 0:
93
+ # Make sure that the plateau value is >= water_depth
94
+ self.dem_settings.plateau = max(
95
+ self.dem_settings.plateau, self.dem_settings.water_depth
96
+ )
97
+ self.logger.info(
98
+ "Plateau value was set to %s to be >= water_depth value %s",
99
+ self.dem_settings.plateau,
100
+ self.dem_settings.water_depth,
101
+ )
102
+
91
103
  self.background_settings = background_settings
92
104
  self.logger.info("Background settings: %s", background_settings)
93
105
  self.grle_settings = grle_settings
@@ -123,6 +135,8 @@ class Map:
123
135
  with open(save_path, "w", encoding="utf-8") as file:
124
136
  json.dump(settings_json, file, indent=4)
125
137
 
138
+ self.shared_settings = SharedSettings()
139
+
126
140
  self.texture_custom_schema = kwargs.get("texture_custom_schema", None)
127
141
  if self.texture_custom_schema:
128
142
  save_path = os.path.join(self.map_directory, "texture_custom_schema.json")
@@ -132,11 +146,17 @@ class Map:
132
146
 
133
147
  self.tree_custom_schema = kwargs.get("tree_custom_schema", None)
134
148
  if self.tree_custom_schema:
149
+ self.logger.info("Custom tree schema contains %s trees", len(self.tree_custom_schema))
135
150
  save_path = os.path.join(self.map_directory, "tree_custom_schema.json")
136
151
  with open(save_path, "w", encoding="utf-8") as file:
137
152
  json.dump(self.tree_custom_schema, file, indent=4)
138
153
  self.logger.debug("Tree custom schema saved to %s", save_path)
139
154
 
155
+ self.custom_background_path = kwargs.get("custom_background_path", None)
156
+ if self.custom_background_path:
157
+ save_path = os.path.join(self.map_directory, "custom_background.png")
158
+ shutil.copyfile(self.custom_background_path, save_path)
159
+
140
160
  try:
141
161
  shutil.unpack_archive(game.template_path, self.map_directory)
142
162
  self.logger.debug("Map template unpacked to %s", self.map_directory)
@@ -245,7 +265,7 @@ class Map:
245
265
  str: Path to the archive.
246
266
  """
247
267
  archive_path = shutil.make_archive(archive_path, "zip", self.map_directory)
248
- self.logger.info("Map packed to %s.zip", archive_path)
268
+ self.logger.debug("Map packed to %s.zip", archive_path)
249
269
  if remove_source:
250
270
  try:
251
271
  shutil.rmtree(self.map_directory)
@@ -34,7 +34,7 @@ class Satellite(Component):
34
34
  """Downloads the satellite images for the map."""
35
35
  self.image_paths = [] # pylint: disable=W0201
36
36
  if not self.map.satellite_settings.download_images:
37
- self.logger.info("Satellite images download is disabled.")
37
+ self.logger.debug("Satellite images download is disabled.")
38
38
  return
39
39
 
40
40
  margin = self.map.satellite_settings.satellite_margin
@@ -4,12 +4,29 @@ from __future__ import annotations
4
4
 
5
5
  from typing import Any
6
6
 
7
- from pydantic import BaseModel
7
+ from pydantic import BaseModel, ConfigDict
8
+
9
+
10
+ class SharedSettings(BaseModel):
11
+ """Represents the shared settings for all components."""
12
+
13
+ mesh_z_scaling_factor: float | None = None
14
+ height_scale_multiplier: float | None = None
15
+ height_scale_value: float | None = None
16
+ change_height_scale: bool = False
17
+
18
+ model_config = ConfigDict(
19
+ frozen=False,
20
+ )
8
21
 
9
22
 
10
23
  class SettingsModel(BaseModel):
11
24
  """Base class for settings models. It provides methods to convert settings to and from JSON."""
12
25
 
26
+ model_config = ConfigDict(
27
+ frozen=False,
28
+ )
29
+
13
30
  @classmethod
14
31
  def all_settings_to_json(cls) -> dict[str, dict[str, Any]]:
15
32
  """Get all settings of the current class and its subclasses as a dictionary.
@@ -25,18 +42,28 @@ class SettingsModel(BaseModel):
25
42
  return all_settings
26
43
 
27
44
  @classmethod
28
- def all_settings_from_json(cls, data: dict) -> dict[str, SettingsModel]:
45
+ def all_settings_from_json(
46
+ cls, data: dict, flattening: bool = True
47
+ ) -> dict[str, SettingsModel]:
29
48
  """Create settings instances from JSON data.
30
49
 
31
50
  Arguments:
32
51
  data (dict): JSON data.
52
+ flattening (bool): if set to True will flattet iterables to use the first element
53
+ of it.
33
54
 
34
55
  Returns:
35
56
  dict[str, Type[SettingsModel]]: Dictionary with settings instances.
36
57
  """
37
58
  settings = {}
38
59
  for subclass in cls.__subclasses__():
39
- settings[subclass.__name__] = subclass(**data[subclass.__name__])
60
+ subclass_data = data[subclass.__name__]
61
+ if flattening:
62
+ for key, value in subclass_data.items():
63
+ if isinstance(value, (list, tuple)):
64
+ subclass_data[key] = value[0]
65
+
66
+ settings[subclass.__name__] = subclass(**subclass_data)
40
67
 
41
68
  return settings
42
69
 
@@ -85,6 +112,10 @@ class BackgroundSettings(SettingsModel):
85
112
  generate_background: bool = False
86
113
  generate_water: bool = False
87
114
  resize_factor: int = 8
115
+ remove_center: bool = False
116
+ apply_decimation: bool = False
117
+ decimation_percent: int = 25
118
+ decimation_agression: int = 3
88
119
 
89
120
 
90
121
  class GRLESettings(SettingsModel):
@@ -100,6 +131,12 @@ class GRLESettings(SettingsModel):
100
131
  farmland_margin: int = 0
101
132
  random_plants: bool = True
102
133
  add_farmyards: bool = False
134
+ base_grass: tuple | str = ("smallDenseMix", "meadow")
135
+ plants_island_minimum_size: int = 10
136
+ plants_island_maximum_size: int = 200
137
+ plants_island_vertex_count: int = 30
138
+ plants_island_rounding_radius: int = 15
139
+ plants_island_percent: int = 100
103
140
 
104
141
 
105
142
  class I3DSettings(SettingsModel):
@@ -5,6 +5,8 @@ from __future__ import annotations
5
5
  import json
6
6
  import os
7
7
  import re
8
+ import shutil
9
+ import warnings
8
10
  from collections import defaultdict
9
11
  from typing import Any, Callable, Generator, Optional
10
12
 
@@ -69,6 +71,7 @@ class Texture(Component):
69
71
  usage: str | None = None,
70
72
  background: bool = False,
71
73
  invisible: bool = False,
74
+ procedural: list[str] | None = None,
72
75
  ):
73
76
  self.name = name
74
77
  self.count = count
@@ -81,6 +84,7 @@ class Texture(Component):
81
84
  self.usage = usage
82
85
  self.background = background
83
86
  self.invisible = invisible
87
+ self.procedural = procedural
84
88
 
85
89
  def to_json(self) -> dict[str, str | list[str] | bool]: # type: ignore
86
90
  """Returns dictionary with layer data.
@@ -99,6 +103,7 @@ class Texture(Component):
99
103
  "usage": self.usage,
100
104
  "background": self.background,
101
105
  "invisible": self.invisible,
106
+ "procedural": self.procedural,
102
107
  }
103
108
 
104
109
  data = {k: v for k, v in data.items() if v is not None}
@@ -187,7 +192,7 @@ class Texture(Component):
187
192
  custom_schema = self.kwargs.get("texture_custom_schema")
188
193
  if custom_schema:
189
194
  layers_schema = custom_schema # type: ignore
190
- self.logger.info("Custom schema loaded with %s layers.", len(layers_schema))
195
+ self.logger.debug("Custom schema loaded with %s layers.", len(layers_schema))
191
196
  else:
192
197
  if not os.path.isfile(self.game.texture_schema):
193
198
  raise FileNotFoundError(
@@ -202,18 +207,20 @@ class Texture(Component):
202
207
 
203
208
  try:
204
209
  self.layers = [self.Layer.from_json(layer) for layer in layers_schema] # type: ignore
205
- self.logger.info("Loaded %s layers.", len(self.layers))
210
+ self.logger.debug("Loaded %s layers.", len(self.layers))
206
211
  except Exception as e: # pylint: disable=W0703
207
212
  raise ValueError(f"Error loading texture layers: {e}") from e
208
213
 
209
214
  base_layer = self.get_base_layer()
210
215
  if base_layer:
211
216
  self.logger.debug("Base layer found: %s.", base_layer.name)
212
- else:
213
- self.logger.warning("No base layer found.")
214
217
 
215
218
  self._weights_dir = self.game.weights_dir_path(self.map_directory)
216
219
  self.logger.debug("Weights directory: %s.", self._weights_dir)
220
+ self.procedural_dir = os.path.join(self._weights_dir, "masks")
221
+ os.makedirs(self.procedural_dir, exist_ok=True)
222
+ self.logger.debug("Procedural directory: %s.", self.procedural_dir)
223
+
217
224
  self.info_save_path = os.path.join(self.map_directory, "generation_info.json")
218
225
  self.logger.debug("Generation info save path: %s.", self.info_save_path)
219
226
 
@@ -253,11 +260,56 @@ class Texture(Component):
253
260
  return layer
254
261
  return None
255
262
 
256
- def process(self):
263
+ def process(self) -> None:
264
+ """Processes the data to generate textures."""
257
265
  self._prepare_weights()
258
266
  self._read_parameters()
259
267
  self.draw()
260
268
  self.rotate_textures()
269
+ self.copy_procedural()
270
+
271
+ def copy_procedural(self) -> None:
272
+ """Copies some of the textures to use them as mask for procedural generation.
273
+ Creates an empty blockmask if it does not exist."""
274
+ blockmask_path = os.path.join(self.procedural_dir, "BLOCKMASK.png")
275
+ if not os.path.isfile(blockmask_path):
276
+ self.logger.debug("BLOCKMASK.png not found, creating an empty file.")
277
+ img = np.zeros((self.map_size, self.map_size), dtype=np.uint8)
278
+ cv2.imwrite(blockmask_path, img) # pylint: disable=no-member
279
+
280
+ pg_layers_by_type = defaultdict(list)
281
+ for layer in self.layers:
282
+ if layer.procedural:
283
+ # Get path to the original file.
284
+ texture_path = layer.get_preview_or_path(self._weights_dir)
285
+ for procedural_layer_name in layer.procedural:
286
+ pg_layers_by_type[procedural_layer_name].append(texture_path)
287
+
288
+ if not pg_layers_by_type:
289
+ self.logger.debug("No procedural layers found.")
290
+ return
291
+
292
+ for procedural_layer_name, texture_paths in pg_layers_by_type.items():
293
+ procedural_save_path = os.path.join(self.procedural_dir, f"{procedural_layer_name}.png")
294
+ if len(texture_paths) > 1:
295
+ # If there are more than one texture, merge them.
296
+ merged_texture = np.zeros((self.map_size, self.map_size), dtype=np.uint8)
297
+ for texture_path in texture_paths:
298
+ # pylint: disable=E1101
299
+ texture = cv2.imread(texture_path, cv2.IMREAD_UNCHANGED)
300
+ merged_texture[texture == 255] = 255
301
+ cv2.imwrite(procedural_save_path, merged_texture) # pylint: disable=no-member
302
+ self.logger.debug(
303
+ "Procedural file %s merged from %s textures.",
304
+ procedural_save_path,
305
+ len(texture_paths),
306
+ )
307
+ elif len(texture_paths) == 1:
308
+ # Otherwise, copy the texture.
309
+ shutil.copyfile(texture_paths[0], procedural_save_path)
310
+ self.logger.debug(
311
+ "Procedural file %s copied from %s.", procedural_save_path, texture_paths[0]
312
+ )
261
313
 
262
314
  def rotate_textures(self) -> None:
263
315
  """Rotates textures of the layers which have tags."""
@@ -276,8 +328,6 @@ class Texture(Component):
276
328
  output_height=self.map_size,
277
329
  output_width=self.map_size,
278
330
  )
279
- else:
280
- self.logger.warning("Layer path %s not found.", layer_path)
281
331
  else:
282
332
  self.logger.debug(
283
333
  "Skipping rotation of layer %s because it has no tags.", layer.name
@@ -305,8 +355,7 @@ class Texture(Component):
305
355
  useful_attributes = [
306
356
  "coordinates",
307
357
  "bbox",
308
- "map_height",
309
- "map_width",
358
+ "map_size",
310
359
  "rotation",
311
360
  "minimum_x",
312
361
  "minimum_y",
@@ -510,7 +559,7 @@ class Texture(Component):
510
559
  cv2.imwrite(sublayer_path, sublayer)
511
560
  self.logger.debug("Sublayer %s saved.", sublayer_path)
512
561
 
513
- self.logger.info("Dissolved layer %s.", layer.name)
562
+ self.logger.debug("Dissolved layer %s.", layer.name)
514
563
 
515
564
  def draw_base_layer(self, cumulative_image: np.ndarray) -> None:
516
565
  """Draws base layer and saves it into the png file.
@@ -596,7 +645,7 @@ class Texture(Component):
596
645
  geometry_type = geometry.geom_type
597
646
  converter = self._converters(geometry_type)
598
647
  if not converter:
599
- self.logger.warning("Geometry type %s not supported.", geometry_type)
648
+ self.logger.debug("Geometry type %s not supported.", geometry_type)
600
649
  return None
601
650
  return converter(geometry, width)
602
651
 
@@ -667,7 +716,9 @@ class Texture(Component):
667
716
  is_fieds = info_layer == "fields"
668
717
  try:
669
718
  if self.map.custom_osm is not None:
670
- objects = ox.features_from_xml(self.map.custom_osm, tags=tags)
719
+ with warnings.catch_warnings():
720
+ warnings.simplefilter("ignore", FutureWarning)
721
+ objects = ox.features_from_xml(self.map.custom_osm, tags=tags)
671
722
  else:
672
723
  objects = ox.features_from_bbox(bbox=self.new_bbox, tags=tags)
673
724
  except Exception as e: # pylint: disable=W0718
@@ -726,6 +777,8 @@ class Texture(Component):
726
777
 
727
778
  if not isinstance(padded_polygon, shapely.geometry.polygon.Polygon):
728
779
  self.logger.warning("The padding value is too high, field will not padded.")
780
+ elif not list(padded_polygon.exterior.coords):
781
+ self.logger.warning("The padding value is too high, field will not padded.")
729
782
  else:
730
783
  polygon = padded_polygon
731
784