maps4fs 1.6.4__py3-none-any.whl → 1.6.7__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.
@@ -58,6 +58,10 @@ class Background(Component):
58
58
  os.makedirs(self.water_directory, exist_ok=True)
59
59
 
60
60
  self.output_path = os.path.join(self.background_directory, f"{FULL_NAME}.png")
61
+ if self.map.custom_background_path:
62
+ self.check_custom_background(self.map.custom_background_path)
63
+ shutil.copyfile(self.map.custom_background_path, self.output_path)
64
+
61
65
  self.not_substracted_path = os.path.join(self.background_directory, "not_substracted.png")
62
66
  self.not_resized_path = os.path.join(self.background_directory, "not_resized.png")
63
67
 
@@ -75,6 +79,28 @@ class Background(Component):
75
79
  self.dem.set_output_resolution((self.rotated_size, self.rotated_size))
76
80
  self.dem.set_dem_path(self.output_path)
77
81
 
82
+ def check_custom_background(self, image_path: str) -> None:
83
+ """Checks if the custom background image meets the requirements.
84
+
85
+ Arguments:
86
+ image_path (str): The path to the custom background image.
87
+
88
+ Raises:
89
+ ValueError: If the custom background image does not meet the requirements.
90
+ """
91
+ image = cv2.imread(image_path, cv2.IMREAD_UNCHANGED) # pylint: disable=no-member
92
+ if image.shape[0] != image.shape[1]:
93
+ raise ValueError("The custom background image must be a square.")
94
+
95
+ if image.shape[0] != self.map_size + DEFAULT_DISTANCE * 2:
96
+ raise ValueError("The custom background image must have the size of the map + 4096.")
97
+
98
+ if len(image.shape) != 2:
99
+ raise ValueError("The custom background image must be a grayscale image.")
100
+
101
+ if image.dtype != np.uint16:
102
+ raise ValueError("The custom background image must be a 16-bit grayscale image.")
103
+
78
104
  def is_preview(self, name: str) -> bool:
79
105
  """Checks if the DEM is a preview.
80
106
 
@@ -91,7 +117,9 @@ class Background(Component):
91
117
  as a result the DEM files will be saved, then based on them the obj files will be
92
118
  generated."""
93
119
  self.create_background_textures()
94
- self.dem.process()
120
+
121
+ if not self.map.custom_background_path:
122
+ self.dem.process()
95
123
 
96
124
  shutil.copyfile(self.dem.dem_path, self.not_substracted_path)
97
125
  self.cutout(self.dem.dem_path, save_path=self.not_resized_path)
@@ -365,29 +393,6 @@ class Background(Component):
365
393
  mesh.apply_scale([0.5, 0.5, 0.5])
366
394
  self.mesh_to_stl(mesh)
367
395
 
368
- def get_z_scaling_factor(self) -> float:
369
- """Calculates the scaling factor for the Z axis based on the map settings.
370
-
371
- Returns:
372
- float -- The scaling factor for the Z axis.
373
- """
374
-
375
- scaling_factor = 1 / self.map.dem_settings.multiplier
376
- self.logger.debug("Z scaling factor including DEM multiplier: %s", scaling_factor)
377
-
378
- if self.map.shared_settings.height_scale_multiplier:
379
- scaling_factor *= self.map.shared_settings.height_scale_multiplier
380
- self.logger.debug(
381
- "Z scaling factor including height scale multiplier: %s", scaling_factor
382
- )
383
- if self.map.shared_settings.mesh_z_scaling_factor:
384
- scaling_factor *= 1 / self.map.shared_settings.mesh_z_scaling_factor
385
- self.logger.debug(
386
- "Z scaling factor including mesh z scaling factor: %s", scaling_factor
387
- )
388
-
389
- return scaling_factor
390
-
391
396
  def mesh_to_stl(self, mesh: trimesh.Trimesh) -> None:
392
397
  """Converts the mesh to an STL file and saves it in the previews directory.
393
398
  Uses powerful simplification to reduce the size of the file since it will be used
@@ -535,3 +535,26 @@ class Component:
535
535
  interpolated_polyline.append(polyline[-1])
536
536
 
537
537
  return interpolated_polyline
538
+
539
+ def get_z_scaling_factor(self) -> float:
540
+ """Calculates the scaling factor for the Z axis based on the map settings.
541
+
542
+ Returns:
543
+ float -- The scaling factor for the Z axis.
544
+ """
545
+
546
+ scaling_factor = 1 / self.map.dem_settings.multiplier
547
+ self.logger.debug("Z scaling factor including DEM multiplier: %s", scaling_factor)
548
+
549
+ if self.map.shared_settings.height_scale_multiplier:
550
+ scaling_factor *= self.map.shared_settings.height_scale_multiplier
551
+ self.logger.debug(
552
+ "Z scaling factor including height scale multiplier: %s", scaling_factor
553
+ )
554
+ if self.map.shared_settings.mesh_z_scaling_factor:
555
+ scaling_factor *= 1 / self.map.shared_settings.mesh_z_scaling_factor
556
+ self.logger.debug(
557
+ "Z scaling factor including mesh z scaling factor: %s", scaling_factor
558
+ )
559
+
560
+ return scaling_factor
maps4fs/generator/game.py CHANGED
@@ -40,7 +40,7 @@ class Game:
40
40
  _tree_schema: str | None = None
41
41
 
42
42
  # Order matters! Some components depend on others.
43
- components = [Texture, GRLE, Background, I3d, Config, Satellite]
43
+ components = [Texture, Background, GRLE, I3d, Config, Satellite]
44
44
 
45
45
  def __init__(self, map_template_path: str | None = None):
46
46
  if map_template_path:
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:
@@ -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
@@ -15,12 +15,9 @@ from maps4fs.generator.component import Component
15
15
  from maps4fs.generator.texture import Texture
16
16
 
17
17
  DISPLACEMENT_LAYER_SIZE_FOR_BIG_MAPS = 32768
18
- DEFAULT_MAX_LOD_DISTANCE = 10000
19
- DEFAULT_MAX_LOD_OCCLUDER_DISTANCE = 10000
20
18
  NODE_ID_STARTING_VALUE = 2000
21
19
  SPLINES_NODE_ID_STARTING_VALUE = 5000
22
20
  TREE_NODE_ID_STARTING_VALUE = 10000
23
- DEFAULT_FOREST_DENSITY = 10
24
21
 
25
22
 
26
23
  # pylint: disable=R0903
@@ -197,7 +194,7 @@ class I3d(Component):
197
194
  linestring_points=road, angle=self.rotation
198
195
  )
199
196
  except ValueError as e:
200
- self.logger.warning(
197
+ self.logger.debug(
201
198
  "Road %s could not be fitted into the map bounds with error: %s",
202
199
  road_id,
203
200
  e,
@@ -248,7 +245,7 @@ class I3d(Component):
248
245
  y = max(0, min(y, dem_y_size - 1))
249
246
 
250
247
  z = not_resized_dem[y, x]
251
- z /= 32 # Yes, it's a magic number here.
248
+ z *= self.get_z_scaling_factor() # type: ignore
252
249
 
253
250
  cv_node = ET.Element("cv")
254
251
  cv_node.set("c", f"{cx}, {z}, {cy}")
@@ -322,7 +319,7 @@ class I3d(Component):
322
319
  polygon_points=field, angle=self.rotation
323
320
  )
324
321
  except ValueError as e:
325
- self.logger.warning(
322
+ self.logger.debug(
326
323
  "Field %s could not be fitted into the map bounds with error: %s",
327
324
  field_id,
328
325
  e,
@@ -336,7 +333,7 @@ class I3d(Component):
336
333
  try:
337
334
  cx, cy = self.get_polygon_center(field_ccs)
338
335
  except Exception as e: # pylint: disable=W0718
339
- self.logger.warning(
336
+ self.logger.debug(
340
337
  "Field %s could not be fitted into the map bounds.", field_id
341
338
  )
342
339
  self.logger.debug("Error: %s", e)
maps4fs/generator/map.py CHANGED
@@ -89,6 +89,17 @@ class Map:
89
89
 
90
90
  self.dem_settings = dem_settings
91
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
+
92
103
  self.background_settings = background_settings
93
104
  self.logger.info("Background settings: %s", background_settings)
94
105
  self.grle_settings = grle_settings
@@ -140,6 +151,11 @@ class Map:
140
151
  json.dump(self.tree_custom_schema, file, indent=4)
141
152
  self.logger.debug("Tree custom schema saved to %s", save_path)
142
153
 
154
+ self.custom_background_path = kwargs.get("custom_background_path", None)
155
+ if self.custom_background_path:
156
+ save_path = os.path.join(self.map_directory, "custom_background.png")
157
+ shutil.copyfile(self.custom_background_path, save_path)
158
+
143
159
  try:
144
160
  shutil.unpack_archive(game.template_path, self.map_directory)
145
161
  self.logger.debug("Map template unpacked to %s", self.map_directory)
@@ -23,6 +23,10 @@ class SharedSettings(BaseModel):
23
23
  class SettingsModel(BaseModel):
24
24
  """Base class for settings models. It provides methods to convert settings to and from JSON."""
25
25
 
26
+ model_config = ConfigDict(
27
+ frozen=False,
28
+ )
29
+
26
30
  @classmethod
27
31
  def all_settings_to_json(cls) -> dict[str, dict[str, Any]]:
28
32
  """Get all settings of the current class and its subclasses as a dictionary.
@@ -38,18 +42,28 @@ class SettingsModel(BaseModel):
38
42
  return all_settings
39
43
 
40
44
  @classmethod
41
- 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]:
42
48
  """Create settings instances from JSON data.
43
49
 
44
50
  Arguments:
45
51
  data (dict): JSON data.
52
+ flattening (bool): if set to True will flattet iterables to use the first element
53
+ of it.
46
54
 
47
55
  Returns:
48
56
  dict[str, Type[SettingsModel]]: Dictionary with settings instances.
49
57
  """
50
58
  settings = {}
51
59
  for subclass in cls.__subclasses__():
52
- 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)
53
67
 
54
68
  return settings
55
69
 
@@ -117,6 +131,12 @@ class GRLESettings(SettingsModel):
117
131
  farmland_margin: int = 0
118
132
  random_plants: bool = True
119
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
120
140
 
121
141
 
122
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}
@@ -212,6 +217,10 @@ class Texture(Component):
212
217
 
213
218
  self._weights_dir = self.game.weights_dir_path(self.map_directory)
214
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
+
215
224
  self.info_save_path = os.path.join(self.map_directory, "generation_info.json")
216
225
  self.logger.debug("Generation info save path: %s.", self.info_save_path)
217
226
 
@@ -251,11 +260,56 @@ class Texture(Component):
251
260
  return layer
252
261
  return None
253
262
 
254
- def process(self):
263
+ def process(self) -> None:
264
+ """Processes the data to generate textures."""
255
265
  self._prepare_weights()
256
266
  self._read_parameters()
257
267
  self.draw()
258
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
+ )
259
313
 
260
314
  def rotate_textures(self) -> None:
261
315
  """Rotates textures of the layers which have tags."""
@@ -274,8 +328,6 @@ class Texture(Component):
274
328
  output_height=self.map_size,
275
329
  output_width=self.map_size,
276
330
  )
277
- else:
278
- self.logger.warning("Layer path %s not found.", layer_path)
279
331
  else:
280
332
  self.logger.debug(
281
333
  "Skipping rotation of layer %s because it has no tags.", layer.name
@@ -664,7 +716,9 @@ class Texture(Component):
664
716
  is_fieds = info_layer == "fields"
665
717
  try:
666
718
  if self.map.custom_osm is not None:
667
- 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)
668
722
  else:
669
723
  objects = ox.features_from_bbox(bbox=self.new_bbox, tags=tags)
670
724
  except Exception as e: # pylint: disable=W0718
@@ -723,6 +777,8 @@ class Texture(Component):
723
777
 
724
778
  if not isinstance(padded_polygon, shapely.geometry.polygon.Polygon):
725
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.")
726
782
  else:
727
783
  polygon = padded_polygon
728
784
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: maps4fs
3
- Version: 1.6.4
3
+ Version: 1.6.7
4
4
  Summary: Generate map templates for Farming Simulator from real places.
5
5
  Author-email: iwatkot <iwatkot@gmail.com>
6
6
  License: MIT License
@@ -131,6 +131,7 @@ docker run -d -p 8501:8501 --name maps4fs iwatkot/maps4fs
131
131
  ```
132
132
  And open [http://localhost:8501](http://localhost:8501) in your browser.<br>
133
133
  If you don't know how to use Docker, navigate to the [Docker version](#option-2-docker-version), it's really simple.<br>
134
+ Check out the [Docker FAQ](docs/FAQ_docker.md) if you have any questions.<br>
134
135
 
135
136
  ### 🤯 For developers
136
137
  **Option 3:** Python package. Install the package using the following command:
@@ -185,6 +186,7 @@ Using it is easy and doesn't require any guides. Enjoy!
185
186
  🗺️ Supported map sizes: 2x2, 4x4, 8x8, 16x16 km and any custom size.
186
187
  ⚙️ Advanced settings: enabled.
187
188
  🖼️ Texture dissolving: enabled.
189
+ Check out the [Docker FAQ](docs/FAQ_docker.md) if you have any questions.<br>
188
190
  You can launch the project with minimalistic UI in your browser using Docker. Follow these steps:
189
191
 
190
192
  1. Install [Docker](https://docs.docker.com/get-docker/) for your OS.
@@ -421,6 +423,7 @@ Let's have a closer look at the fields:
421
423
  - `background` - set it to True for the textures, which should have impact on the Background Terrain, by default it's used to subtract the water depth from the DEM and background terrain.
422
424
  - `info_layer` - if the layer is saving some data in JSON format, this section will describe it's name in the JSON file. Used to find the needed JSON data, for example for fields it will be `fields` and as a value - list of polygon coordinates.
423
425
  - `invisible` - set it to True for the textures, which should not be drawn in the files, but only to save the data in the JSON file (related to the previous field).
426
+ - `procedural` - is a list of corresponding files, that will be used for a procedural generation. For example: `"procedural": ["PG_meadow", "PG_acres"]` - means that the texture will be used for two procedural generation files: `masks/PG_meadow.png` and `masks/PG_acres.png`. Note, that the one procuderal name can be applied to multiple textures, in this case they will be merged into one mask.
424
427
 
425
428
  ## Background terrain
426
429
  The tool now supports the generation of the background terrain. If you don't know what it is, here's a brief explanation. The background terrain is the world around the map. It's important to create it because if you don't, the map will look like it's floating in the void. The background terrain is a simple plane that can (and should) be textured to look fine.<br>
@@ -472,7 +475,7 @@ You can also apply some advanced settings to the map generation process.<br>
472
475
 
473
476
  ### DEM Advanced settings
474
477
 
475
- - Multiplier: the height of the map is multiplied by this value. So the DEM map is just a 16-bit grayscale image, which means that the maximum available value there is 65535, while the actual difference between the deepest and the highest point on Earth is about 20 km. Just note that this setting mostly does not matter, because you can always adjust it in the Giants Editor, learn more about the DEM file and the heightScale parameter in [docs](docs/dem.md). By default, it's set to 1.
478
+ - Multiplier: the height of the map is multiplied by this value. So the DEM map is just a 16-bit grayscale image, which means that the maximum available value there is 65535, while the actual difference between the deepest and the highest point on Earth is about 20 km. Just note that this setting mostly does not matter, because you can always adjust it in the Giants Editor, learn more about the DEM file and the heightScale parameter in [docs](docs/dem.md). To match the in-game heights with SRTM Data provider, the recommended value is 255 (if easy mode is disabled), but depending on the place, you will need to play with both multiplier and the height scale in Giants Editor to find the best values.
476
479
 
477
480
  - Blur radius: the radius of the Gaussian blur filter applied to the DEM map. By default, it's set to 21. This filter just makes the DEM map smoother, so the height transitions will be more natural. You can set it to 1 to disable the filter, but it will result in a Minecraft-like map.
478
481
 
@@ -505,6 +508,18 @@ decimation will be, which means the higher it will affect the geometry. It's not
505
508
 
506
509
  - Add Farmyards - if enabled, the tool will create farmlands from the regions that are marked as farmyards in the OSM data. Those farmlands will not have fields and also will not be drawn on textures. By default, it's turned off.
507
510
 
511
+ - Base grass - you can select which plant will be used as a base grass on the map.
512
+
513
+ - Plants island minimum size - when random plants are enabled, the generator will add islands of differents plants to the map and choose the random size of those island between the minimum and maximum values. This one is the minimum size of the island in meters.
514
+
515
+ - Plants island maximum size - it's the same as above, but for the maximum size of the island in meters.
516
+
517
+ - Plants island vertex count - the number of vertices in the island. The higher the value, the more detailed the island will be. Note, that high values will turn the smoothed island into geometric madness.
518
+
519
+ - Plants insland rounding radius - used to round the vertices of the island. The higher the value, the more rounded the island will be.
520
+
521
+ - Plants island percent - defines the relation between the map size and the number of islands of plants. For example, if set to 100% for map size of 2048 will be added 2048 islands of plants.
522
+
508
523
  ### I3D Advanced settings
509
524
 
510
525
  - Forest density - the density of the forest in meters. The lower the value, the lower the distance between the trees, which makes the forest denser. Note, that low values will lead to enormous number of trees, which may cause the Giants Editor to crash or lead to performance issues. By default, it's set to 10.
@@ -538,6 +553,8 @@ The tool also supports the expert settings. Do not use them until you read the d
538
553
 
539
554
  - Show schemas - you'll be able to edit or define your own texture or tree schemas. It's useful if you want to add some custom textures or trees to the map. Refer to the [Texture schema](#texture-schema) section to learn more about the schema structure. Any incorrect value here will lead to the completely broken map.
540
555
 
556
+ - Upload custom background image - if you have an image, which represents the map and background terrain you can use it for generation. Note, that the image should meet the following requirements: 1:1 aspect ratio, size = map size + 2048 * 2, it should be uint16 (unsigned 16-bit integer) grayscale (single channel) image. The image should be in the PNG format. If any of the requirements are not met, the tool raises an error. If you're using rotation, the image should already be rotated.
557
+
541
558
  ## Resources
542
559
  In this section, you'll find a list of the resources that you need to create a map for the Farming Simulator.<br>
543
560
  To create a basic map, you only need the Giants Editor. But if you want to create a background terrain - the world around the map, so it won't look like it's floating in the void - you also need Blender and the Blender Exporter Plugins. To create realistic textures for the background terrain, the QGIS is required to obtain high-resolution satellite images.<br>
@@ -1,18 +1,18 @@
1
1
  maps4fs/__init__.py,sha256=WbT36EzJ_74GN0RUUrLIYECdSdtRiZaxKl17KUt7pjA,492
2
2
  maps4fs/logger.py,sha256=B-NEYpMjPAAqlV4VpfTi6nbBFnEABVtQOaYe6nMpidg,1489
3
3
  maps4fs/generator/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
4
- maps4fs/generator/background.py,sha256=JZNSiPZzA6_Kihu83qdCbsJc9bGeeSBY0SP6jTtdvU4,24595
5
- maps4fs/generator/component.py,sha256=GrTI7803gOQFhqocdjFG-wh0HOkC6HWoyKr8pR2Xp28,20409
4
+ maps4fs/generator/background.py,sha256=tV4UXvtkNN-OSvv6ujp4jFWRU1xGBgEvSakVGZ1H4nc,24877
5
+ maps4fs/generator/component.py,sha256=vn_ThQw3OTcloqYuJWC7vghAvIAnwJsybEm7qMwvsZk,21356
6
6
  maps4fs/generator/config.py,sha256=0QmK052B8bxyHVhg3jzCORLfOBMMmqVfhhbqXKf6OMk,4383
7
7
  maps4fs/generator/dem.py,sha256=20gx0dzX0LyO6ipvDitst-BwGfcKogFqgQf9Q2qMH5U,10933
8
- maps4fs/generator/game.py,sha256=QHgVnyGYvEnfwGZ84-u-dpbCRr3UeVVqBbrwr5WG8dE,7992
9
- maps4fs/generator/grle.py,sha256=rU84Q1PBHr-V-JzdHJ7BXLHC_LztGw6Z1FS2w_1HIF0,17848
10
- maps4fs/generator/i3d.py,sha256=D1QBHCFygTkml6GZmLRlUagWEVQb0tNeyZ-MAY-Uf0Q,24945
11
- maps4fs/generator/map.py,sha256=8CYUs7NNVoQBvABqtoKtnbj280JuvseNORDCsebkQ_c,9357
8
+ maps4fs/generator/game.py,sha256=Nf5r2ubV4YVAVHGzJyhbF2GnOC0qV3HlHYIZBCWciHs,7992
9
+ maps4fs/generator/grle.py,sha256=hcbVBJ4j_Zr2QvEVo2cYNh2jARVXp_X3onifBtp9Zxs,20922
10
+ maps4fs/generator/i3d.py,sha256=pUyHKWKcw43xVCf3Y8iabtbQba05LYxMHi8vziGksIA,24843
11
+ maps4fs/generator/map.py,sha256=P8wHrCLhLcv2W5zJmMGjpM1TAMR8c7yVFzm_n-5ZTHQ,10084
12
12
  maps4fs/generator/qgis.py,sha256=Es8hLuqN_KH8lDfnJE6He2rWYbAKJ3RGPn-o87S6CPI,6116
13
13
  maps4fs/generator/satellite.py,sha256=_7RcuNmR1mjxEJWMDsjnzKUIqWxnGUn50XtjB7HmSPg,3661
14
- maps4fs/generator/settings.py,sha256=zPdewt348ulparOAlWo-TmfyF77bm1567fcliL6YP6s,4951
15
- maps4fs/generator/texture.py,sha256=C3gjAd5yAF0uw7QP3hU2In9iHAXg-oXzYAuBVWd3FD8,31135
14
+ maps4fs/generator/settings.py,sha256=3ASf3hW1nkGt8_3IOvKIKNUd6XAHYTAA8FquuhpSUlU,5668
15
+ maps4fs/generator/texture.py,sha256=gIXCHU1vT3evbkaXAV9gLUrgI1wH3xJLgWAtZgFruj0,34013
16
16
  maps4fs/generator/dtm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  maps4fs/generator/dtm/dtm.py,sha256=azy-RWsc5PgenKDtgG0lrddMwWEw1hYzdng9V8zphMk,9167
18
18
  maps4fs/generator/dtm/srtm.py,sha256=2-pX6bWrJX6gr8IM7ueX6mm_PW7_UQ58MtdzDHae2OQ,9030
@@ -20,8 +20,8 @@ maps4fs/generator/dtm/usgs.py,sha256=ZTi10RNDA3EBrsVg2ZoYrdN4uqiG1Jvk7FzdcKdgNkU
20
20
  maps4fs/toolbox/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
21
21
  maps4fs/toolbox/background.py,sha256=9BXWNqs_n3HgqDiPztWylgYk_QM4YgBpe6_ZNQAWtSc,2154
22
22
  maps4fs/toolbox/dem.py,sha256=z9IPFNmYbjiigb3t02ZenI3Mo8odd19c5MZbjDEovTo,3525
23
- maps4fs-1.6.4.dist-info/LICENSE.md,sha256=pTKD_oUexcn-yccFCTrMeLkZy0ifLRa-VNcDLqLZaIw,10749
24
- maps4fs-1.6.4.dist-info/METADATA,sha256=huMhHVyVy7sduTij2eOj0GAAVxSCgUSX4unrmkEeW-s,36425
25
- maps4fs-1.6.4.dist-info/WHEEL,sha256=A3WOREP4zgxI0fKrHUG8DC8013e3dK3n7a6HDbcEIwE,91
26
- maps4fs-1.6.4.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
27
- maps4fs-1.6.4.dist-info/RECORD,,
23
+ maps4fs-1.6.7.dist-info/LICENSE.md,sha256=pTKD_oUexcn-yccFCTrMeLkZy0ifLRa-VNcDLqLZaIw,10749
24
+ maps4fs-1.6.7.dist-info/METADATA,sha256=uNK0tEiWExntm4F61lyfM4q4qY-qw4naYg7iFE6Kb5M,38680
25
+ maps4fs-1.6.7.dist-info/WHEEL,sha256=A3WOREP4zgxI0fKrHUG8DC8013e3dK3n7a6HDbcEIwE,91
26
+ maps4fs-1.6.7.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
27
+ maps4fs-1.6.7.dist-info/RECORD,,