maps4fs 1.1.6__py3-none-any.whl → 1.2.3__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.
@@ -63,6 +63,7 @@ class Background(Component):
63
63
  for name, autoprocess in zip(ELEMENTS, autoprocesses):
64
64
  dem = DEM(
65
65
  self.game,
66
+ self.map,
66
67
  self.coordinates,
67
68
  background_size,
68
69
  rotated_size,
@@ -17,6 +17,7 @@ from maps4fs.generator.qgis import save_scripts
17
17
 
18
18
  if TYPE_CHECKING:
19
19
  from maps4fs.generator.game import Game
20
+ from maps4fs.generator.map import Map
20
21
 
21
22
 
22
23
  # pylint: disable=R0801, R0903, R0902, R0904
@@ -25,6 +26,7 @@ class Component:
25
26
 
26
27
  Arguments:
27
28
  game (Game): The game instance for which the map is generated.
29
+ map (Map): The map instance for which the component is generated.
28
30
  coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
29
31
  map_size (int): The size of the map in pixels.
30
32
  map_rotated_size (int): The size of the map in pixels after rotation.
@@ -37,6 +39,7 @@ class Component:
37
39
  def __init__(
38
40
  self,
39
41
  game: Game,
42
+ map: Map, # pylint: disable=W0622
40
43
  coordinates: tuple[float, float],
41
44
  map_size: int,
42
45
  map_rotated_size: int,
@@ -46,6 +49,7 @@ class Component:
46
49
  **kwargs, # pylint: disable=W0613, R0913, R0917
47
50
  ):
48
51
  self.game = game
52
+ self.map = map
49
53
  self.coordinates = coordinates
50
54
  self.map_size = map_size
51
55
  self.map_rotated_size = map_rotated_size
maps4fs/generator/game.py CHANGED
@@ -36,6 +36,7 @@ class Game:
36
36
  _map_template_path: str | None = None
37
37
  _texture_schema: str | None = None
38
38
  _grle_schema: str | None = None
39
+ _tree_schema: str | None = None
39
40
 
40
41
  # Order matters! Some components depend on others.
41
42
  components = [Texture, I3d, GRLE, Background, Config]
@@ -109,6 +110,19 @@ class Game:
109
110
  raise ValueError("GRLE layers schema path not set.")
110
111
  return self._grle_schema
111
112
 
113
+ @property
114
+ def tree_schema(self) -> str:
115
+ """Returns the path to the tree layers schema file.
116
+
117
+ Raises:
118
+ ValueError: If the tree layers schema path is not set.
119
+
120
+ Returns:
121
+ str: The path to the tree layers schema file."""
122
+ if not self._tree_schema:
123
+ raise ValueError("Tree layers schema path not set.")
124
+ return self._tree_schema
125
+
112
126
  def dem_file_path(self, map_directory: str) -> str:
113
127
  """Returns the path to the DEM file.
114
128
 
@@ -187,6 +201,7 @@ class FS25(Game):
187
201
  _map_template_path = os.path.join(working_directory, "data", "fs25-map-template.zip")
188
202
  _texture_schema = os.path.join(working_directory, "data", "fs25-texture-schema.json")
189
203
  _grle_schema = os.path.join(working_directory, "data", "fs25-grle-schema.json")
204
+ _tree_schema = os.path.join(working_directory, "data", "fs25-tree-schema.json")
190
205
 
191
206
  def dem_file_path(self, map_directory: str) -> str:
192
207
  """Returns the path to the DEM file.
maps4fs/generator/grle.py CHANGED
@@ -2,12 +2,21 @@
2
2
 
3
3
  import json
4
4
  import os
5
+ from random import choice, randint
5
6
  from xml.etree import ElementTree as ET
6
7
 
7
8
  import cv2
8
9
  import numpy as np
10
+ from shapely.geometry import Polygon # type: ignore
9
11
 
10
12
  from maps4fs.generator.component import Component
13
+ from maps4fs.generator.texture import Texture
14
+
15
+ ISLAND_SIZE_MIN = 10
16
+ ISLAND_SIZE_MAX = 200
17
+ ISLAND_DISTORTION = 0.3
18
+ ISLAND_VERTEX_COUNT = 30
19
+ ISLAND_ROUNDING_RADIUS = 15
11
20
 
12
21
 
13
22
  # pylint: disable=W0223
@@ -32,6 +41,7 @@ class GRLE(Component):
32
41
  attribute. If the game does not support I3D files, the attribute is set to None."""
33
42
 
34
43
  self.farmland_margin = self.kwargs.get("farmland_margin", 0)
44
+ self.randomize_plants = self.kwargs.get("randomize_plants", True)
35
45
 
36
46
  try:
37
47
  grle_schema_path = self.game.grle_schema
@@ -76,6 +86,11 @@ class GRLE(Component):
76
86
  self.logger.warning("Invalid InfoLayer schema: %s.", info_layer)
77
87
 
78
88
  self._add_farmlands()
89
+ if self.game.code == "FS25":
90
+ self.logger.info("Game is %s, plants will be added.", self.game.code)
91
+ self._add_plants()
92
+ else:
93
+ self.logger.warning("Adding plants it's not supported for the %s.", self.game.code)
79
94
 
80
95
  def previews(self) -> list[str]:
81
96
  """Returns a list of paths to the preview images (empty list).
@@ -184,3 +199,199 @@ class GRLE(Component):
184
199
  self.logger.info(
185
200
  "Farmlands added to the InfoLayer PNG file: %s.", info_layer_farmlands_path
186
201
  )
202
+
203
+ # pylint: disable=R0915
204
+ def _add_plants(self) -> None:
205
+ """Adds plants to the InfoLayer PNG file."""
206
+ # 1. Get the path to the densityMap_fruits.png.
207
+ # 2. Get the path to the base layer (grass).
208
+ # 3. Detect non-zero areas in the base layer (it's where the plants will be placed).
209
+ texture_component: Texture | None = self.map.get_component("Texture") # type: ignore
210
+ if not texture_component:
211
+ self.logger.warning("Texture component not found in the map.")
212
+ return
213
+
214
+ grass_layer = texture_component.get_layer_by_usage("grass")
215
+ if not grass_layer:
216
+ self.logger.warning("Grass layer not found in the texture component.")
217
+ return
218
+
219
+ weights_directory = self.game.weights_dir_path(self.map_directory)
220
+ grass_image_path = grass_layer.get_preview_or_path(weights_directory)
221
+ self.logger.debug("Grass image path: %s.", grass_image_path)
222
+
223
+ forest_layer = texture_component.get_layer_by_usage("forest")
224
+ forest_image = None
225
+ if forest_layer:
226
+ forest_image_path = forest_layer.get_preview_or_path(weights_directory)
227
+ self.logger.debug("Forest image path: %s.", forest_image_path)
228
+ if forest_image_path:
229
+ # pylint: disable=no-member
230
+ forest_image = cv2.imread(forest_image_path, cv2.IMREAD_UNCHANGED)
231
+
232
+ if not grass_image_path or not os.path.isfile(grass_image_path):
233
+ self.logger.warning("Base image not found in %s.", grass_image_path)
234
+ return
235
+
236
+ density_map_fruit_path = os.path.join(
237
+ self.game.weights_dir_path(self.map_directory), "densityMap_fruits.png"
238
+ )
239
+
240
+ self.logger.debug("Density map for fruits path: %s.", density_map_fruit_path)
241
+
242
+ if not os.path.isfile(density_map_fruit_path):
243
+ self.logger.warning("Density map for fruits not found in %s.", density_map_fruit_path)
244
+ return
245
+
246
+ # Single channeled 8-bit image, where non-zero values (255) are where the grass is.
247
+ grass_image = cv2.imread( # pylint: disable=no-member
248
+ grass_image_path, cv2.IMREAD_UNCHANGED # pylint: disable=no-member
249
+ )
250
+
251
+ # Density map of the fruits is 2X size of the base image, so we need to resize it.
252
+ # We'll resize the base image to make it bigger, so we can compare the values.
253
+ grass_image = cv2.resize( # pylint: disable=no-member
254
+ grass_image,
255
+ (grass_image.shape[1] * 2, grass_image.shape[0] * 2),
256
+ interpolation=cv2.INTER_NEAREST, # pylint: disable=no-member
257
+ )
258
+ if forest_image is not None:
259
+ forest_image = cv2.resize( # pylint: disable=no-member
260
+ forest_image,
261
+ (forest_image.shape[1] * 2, forest_image.shape[0] * 2),
262
+ interpolation=cv2.INTER_NEAREST, # pylint: disable=no-member
263
+ )
264
+
265
+ # Add non zero values from the forest image to the grass image.
266
+ grass_image[forest_image != 0] = 255
267
+
268
+ # B and G channels remain the same (zeros), while we change the R channel.
269
+ possible_R_values = [33, 65, 97, 129, 161, 193, 225] # pylint: disable=C0103
270
+
271
+ # 1st approach: Change the non zero values in the base image to 33 (for debug).
272
+ # And use the base image as R channel in the density map.
273
+
274
+ # pylint: disable=no-member
275
+ def create_island_of_plants(image: np.ndarray, count: int) -> np.ndarray:
276
+ """Create an island of plants in the image.
277
+
278
+ Arguments:
279
+ image (np.ndarray): The image where the island of plants will be created.
280
+ count (int): The number of islands of plants to create.
281
+
282
+ Returns:
283
+ np.ndarray: The image with the islands of plants.
284
+ """
285
+ for _ in range(count):
286
+ # Randomly choose the value for the island.
287
+ plant_value = choice(possible_R_values)
288
+ # Randomly choose the size of the island.
289
+ island_size = randint(ISLAND_SIZE_MIN, ISLAND_SIZE_MAX)
290
+ # Randomly choose the position of the island.
291
+ # x = np.random.randint(0, image.shape[1] - island_size)
292
+ # y = np.random.randint(0, image.shape[0] - island_size)
293
+ x = randint(0, image.shape[1] - island_size)
294
+ y = randint(0, image.shape[0] - island_size)
295
+
296
+ # Randomly choose the shape of the island.
297
+ # shapes = ["circle", "ellipse", "polygon"]
298
+ # shape = choice(shapes)
299
+
300
+ try:
301
+ polygon_points = get_rounded_polygon(
302
+ num_vertices=ISLAND_VERTEX_COUNT,
303
+ center=(x + island_size // 2, y + island_size // 2),
304
+ radius=island_size // 2,
305
+ rounding_radius=ISLAND_ROUNDING_RADIUS,
306
+ )
307
+ if not polygon_points:
308
+ continue
309
+
310
+ nodes = np.array(polygon_points, np.int32) # type: ignore
311
+ cv2.fillPoly(image, [nodes], plant_value) # type: ignore
312
+ except Exception: # pylint: disable=W0703
313
+ continue
314
+
315
+ return image
316
+
317
+ def get_rounded_polygon(
318
+ num_vertices: int, center: tuple[int, int], radius: int, rounding_radius: int
319
+ ) -> list[tuple[int, int]] | None:
320
+ """Get a randomly rounded polygon.
321
+
322
+ Arguments:
323
+ num_vertices (int): The number of vertices of the polygon.
324
+ center (tuple[int, int]): The center of the polygon.
325
+ radius (int): The radius of the polygon.
326
+ rounding_radius (int): The rounding radius of the polygon.
327
+
328
+ Returns:
329
+ list[tuple[int, int]] | None: The rounded polygon.
330
+ """
331
+ angle_offset = np.pi / num_vertices
332
+ angles = np.linspace(0, 2 * np.pi, num_vertices, endpoint=False) + angle_offset
333
+ random_angles = angles + np.random.uniform(
334
+ -ISLAND_DISTORTION, ISLAND_DISTORTION, num_vertices
335
+ ) # Add randomness to angles
336
+ random_radii = radius + np.random.uniform(
337
+ -radius * ISLAND_DISTORTION, radius * ISLAND_DISTORTION, num_vertices
338
+ ) # Add randomness to radii
339
+
340
+ points = [
341
+ (center[0] + np.cos(a) * r, center[1] + np.sin(a) * r)
342
+ for a, r in zip(random_angles, random_radii)
343
+ ]
344
+ polygon = Polygon(points)
345
+ buffered_polygon = polygon.buffer(rounding_radius, resolution=16)
346
+ rounded_polygon = list(buffered_polygon.exterior.coords)
347
+ if not rounded_polygon:
348
+ return None
349
+ return rounded_polygon
350
+
351
+ grass_image_copy = grass_image.copy()
352
+ if forest_image is not None:
353
+ # Add the forest layer to the base image, to merge the masks.
354
+ grass_image_copy[forest_image != 0] = 33
355
+ # Set all the non-zero values to 33.
356
+ grass_image_copy[grass_image != 0] = 33
357
+
358
+ # Add islands of plants to the base image.
359
+ 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:
362
+ grass_image_copy = create_island_of_plants(grass_image_copy, island_count)
363
+ self.logger.debug("Islands of plants added to the base image.")
364
+
365
+ # Sligtly reduce the size of the grass_image, that we'll use as mask.
366
+ kernel = np.ones((3, 3), np.uint8)
367
+ grass_image = cv2.erode(grass_image, kernel, iterations=1)
368
+
369
+ # Remove the values where the base image has zeros.
370
+ grass_image_copy[grass_image == 0] = 0
371
+ self.logger.debug("Removed the values where the base image has zeros.")
372
+
373
+ # Set zeros on all sides of the image
374
+ grass_image_copy[0, :] = 0 # Top side
375
+ grass_image_copy[-1, :] = 0 # Bottom side
376
+ grass_image_copy[:, 0] = 0 # Left side
377
+ grass_image_copy[:, -1] = 0 # Right side
378
+
379
+ # Value of 33 represents the base grass plant.
380
+ # After painting it with base grass, we'll create multiple islands of different plants.
381
+ # On the final step, we'll remove all the values which in pixels
382
+ # where zerons in the original base image (so we don't paint grass where it should not be).
383
+
384
+ # Three channeled 8-bit image, where non-zero values are the
385
+ # different types of plants (only in the R channel).
386
+ density_map_fruits = cv2.imread(density_map_fruit_path, cv2.IMREAD_UNCHANGED)
387
+ self.logger.debug("Density map for fruits loaded, shape: %s.", density_map_fruits.shape)
388
+
389
+ # Put the updated base image as the B channel in the density map.
390
+ density_map_fruits[:, :, 0] = grass_image_copy
391
+ self.logger.debug("Updated base image added as the B channel in the density map.")
392
+
393
+ # Save the updated density map.
394
+ # Ensure that order of channels is correct because CV2 uses BGR and we need RGB.
395
+ density_map_fruits = cv2.cvtColor(density_map_fruits, cv2.COLOR_BGR2RGB)
396
+ 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)
maps4fs/generator/i3d.py CHANGED
@@ -4,15 +4,23 @@ from __future__ import annotations
4
4
 
5
5
  import json
6
6
  import os
7
+ from random import choice, randint, uniform
8
+ from typing import Generator
7
9
  from xml.etree import ElementTree as ET
8
10
 
11
+ import cv2
12
+ import numpy as np
13
+
9
14
  from maps4fs.generator.component import Component
15
+ from maps4fs.generator.texture import Texture
10
16
 
11
17
  DEFAULT_HEIGHT_SCALE = 2000
12
18
  DISPLACEMENT_LAYER_SIZE_FOR_BIG_MAPS = 32768
13
19
  DEFAULT_MAX_LOD_DISTANCE = 10000
14
20
  DEFAULT_MAX_LOD_OCCLUDER_DISTANCE = 10000
15
- NODE_ID_STARTING_VALUE = 500
21
+ NODE_ID_STARTING_VALUE = 2000
22
+ TREE_NODE_ID_STARTING_VALUE = 4000
23
+ DEFAULT_FOREST_DENSITY = 10
16
24
 
17
25
 
18
26
  # pylint: disable=R0903
@@ -44,10 +52,14 @@ class I3d(Component):
44
52
  self.logger.info("I3D file processing is not implemented for this game.")
45
53
  self._map_i3d_path = None
46
54
 
55
+ self.forest_density = self.kwargs.get("forest_density", DEFAULT_FOREST_DENSITY)
56
+ self.logger.info("Forest density: %s.", self.forest_density)
57
+
47
58
  def process(self) -> None:
48
59
  """Updates the map I3D file with the default settings."""
49
60
  self._update_i3d_file()
50
61
  self._add_fields()
62
+ self._add_forests()
51
63
 
52
64
  def _get_tree(self) -> ET.ElementTree | None:
53
65
  """Returns the ElementTree instance of the map I3D file."""
@@ -85,6 +97,21 @@ class I3d(Component):
85
97
 
86
98
  self.logger.debug("TerrainTransformGroup element updated in I3D file.")
87
99
 
100
+ sun_elem = map_elem.find(".//Light[@name='sun']")
101
+
102
+ if sun_elem is not None:
103
+ self.logger.debug("Sun element found in I3D file.")
104
+
105
+ distance = self.map_size // 2
106
+
107
+ sun_elem.set("lastShadowMapSplitBboxMin", f"-{distance},-128,-{distance}")
108
+ sun_elem.set("lastShadowMapSplitBboxMax", f"{distance},148,{distance}")
109
+
110
+ self.logger.debug(
111
+ "Sun BBOX updated with half of the map size: %s.",
112
+ distance,
113
+ )
114
+
88
115
  if self.map_size > 4096:
89
116
  displacement_layer = terrain_elem.find(".//DisplacementLayer") # pylint: disable=W0631
90
117
 
@@ -301,3 +328,138 @@ class I3d(Component):
301
328
  attribute_node.set("type", attr_type)
302
329
  attribute_node.set("value", value)
303
330
  return attribute_node
331
+
332
+ # pylint: disable=R0911
333
+ def _add_forests(self) -> None:
334
+ """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
353
+
354
+ texture_component: Texture | None = self.map.get_component("Texture") # type: ignore
355
+ if not texture_component:
356
+ self.logger.warning("Texture component not found.")
357
+ return
358
+
359
+ forest_layer = texture_component.get_layer_by_usage("forest")
360
+
361
+ if not forest_layer:
362
+ self.logger.warning("Forest layer not found.")
363
+ return
364
+
365
+ weights_directory = self.game.weights_dir_path(self.map_directory)
366
+ forest_image_path = forest_layer.get_preview_or_path(weights_directory)
367
+
368
+ if not forest_image_path or not os.path.isfile(forest_image_path):
369
+ self.logger.warning("Forest image not found.")
370
+ return
371
+
372
+ tree = self._get_tree()
373
+ if tree is None:
374
+ return
375
+
376
+ # Find the <Scene> element in the I3D file.
377
+ root = tree.getroot()
378
+ scene_node = root.find(".//Scene")
379
+ if scene_node is None:
380
+ self.logger.warning("Scene element not found in I3D file.")
381
+ return
382
+
383
+ self.logger.debug("Scene element found in I3D file, starting to add forests.")
384
+
385
+ node_id = TREE_NODE_ID_STARTING_VALUE
386
+
387
+ # Create <TransformGroup name="trees" translation="0 400 0" nodeId="{node_id}"> element.
388
+ trees_node = ET.Element("TransformGroup")
389
+ trees_node.set("name", "trees")
390
+ trees_node.set("translation", "0 400 0")
391
+ trees_node.set("nodeId", str(node_id))
392
+ node_id += 1
393
+
394
+ # pylint: disable=no-member
395
+ forest_image = cv2.imread(forest_image_path, cv2.IMREAD_UNCHANGED)
396
+
397
+ tree_count = 0
398
+ for x, y in self.non_empty_pixels(forest_image, step=self.forest_density):
399
+ xcs, ycs = self.top_left_coordinates_to_center((x, y))
400
+ node_id += 1
401
+
402
+ rotation = randint(-180, 180)
403
+ xcs, ycs = self.randomize_coordinates((xcs, ycs), self.forest_density) # type: ignore
404
+
405
+ random_tree = choice(tree_schema)
406
+ tree_name = random_tree["name"]
407
+ tree_id = random_tree["reference_id"]
408
+
409
+ reference_node = ET.Element("ReferenceNode")
410
+ reference_node.set("name", tree_name) # type: ignore
411
+ reference_node.set("translation", f"{xcs} 0 {ycs}")
412
+ reference_node.set("rotation", f"0 {rotation} 0")
413
+ reference_node.set("referenceId", str(tree_id))
414
+ reference_node.set("nodeId", str(node_id))
415
+
416
+ trees_node.append(reference_node)
417
+ tree_count += 1
418
+
419
+ scene_node.append(trees_node)
420
+ self.logger.info("Added %s trees to the I3D file.", tree_count)
421
+
422
+ tree.write(self._map_i3d_path) # type: ignore
423
+ self.logger.info("Map I3D file saved to: %s.", self._map_i3d_path)
424
+
425
+ @staticmethod
426
+ def randomize_coordinates(coordinates: tuple[int, int], density: int) -> tuple[float, float]:
427
+ """Randomizes the coordinates of the point with the given density.
428
+
429
+ Arguments:
430
+ coordinates (tuple[int, int]): The coordinates of the point.
431
+ density (int): The density of the randomization.
432
+
433
+ Returns:
434
+ tuple[float, float]: The randomized coordinates of the point.
435
+ """
436
+ MAXIMUM_RELATIVE_SHIFT = 0.2 # pylint: disable=C0103
437
+ shift_range = density * MAXIMUM_RELATIVE_SHIFT
438
+
439
+ x_shift = uniform(-shift_range, shift_range)
440
+ y_shift = uniform(-shift_range, shift_range)
441
+
442
+ x, y = coordinates
443
+ x += x_shift # type: ignore
444
+ y += y_shift # type: ignore
445
+
446
+ return x, y
447
+
448
+ @staticmethod
449
+ def non_empty_pixels(
450
+ image: np.ndarray, step: int = 1
451
+ ) -> Generator[tuple[int, int], None, None]:
452
+ """Receives numpy array, which represents single-channeled image of uint8 type.
453
+ Yield coordinates of non-empty pixels (pixels with value greater than 0).
454
+
455
+ Arguments:
456
+ image (np.ndarray): The image to get non-empty pixels from.
457
+ step (int, optional): The step to iterate through the image. Defaults to 1.
458
+
459
+ Yields:
460
+ tuple[int, int]: The coordinates of non-empty pixels.
461
+ """
462
+ for y, row in enumerate(image[::step]):
463
+ for x, value in enumerate(row[::step]):
464
+ if value > 0:
465
+ yield x * step, y * step
maps4fs/generator/map.py CHANGED
@@ -74,6 +74,7 @@ class Map:
74
74
  for game_component in self.game.components:
75
75
  component = game_component(
76
76
  self.game,
77
+ self,
77
78
  self.coordinates,
78
79
  self.size,
79
80
  self.rotated_size,
@@ -82,6 +83,7 @@ class Map:
82
83
  self.logger,
83
84
  **self.kwargs,
84
85
  )
86
+ self.components.append(component)
85
87
 
86
88
  yield component.__class__.__name__
87
89
 
@@ -104,7 +106,20 @@ class Map:
104
106
  e,
105
107
  )
106
108
  raise e
107
- self.components.append(component)
109
+
110
+ def get_component(self, component_name: str) -> Component | None:
111
+ """Get component by name.
112
+
113
+ Arguments:
114
+ component_name (str): Name of the component.
115
+
116
+ Returns:
117
+ Component | None: Component instance or None if not found.
118
+ """
119
+ for component in self.components:
120
+ if component.__class__.__name__ == component_name:
121
+ return component
122
+ return None
108
123
 
109
124
  def previews(self) -> list[str]:
110
125
  """Get list of preview images.
@@ -63,6 +63,7 @@ class Texture(Component):
63
63
  exclude_weight: bool = False,
64
64
  priority: int | None = None,
65
65
  info_layer: str | None = None,
66
+ usage: str | None = None,
66
67
  ):
67
68
  self.name = name
68
69
  self.count = count
@@ -72,6 +73,7 @@ class Texture(Component):
72
73
  self.exclude_weight = exclude_weight
73
74
  self.priority = priority
74
75
  self.info_layer = info_layer
76
+ self.usage = usage
75
77
 
76
78
  def to_json(self) -> dict[str, str | list[str] | bool]: # type: ignore
77
79
  """Returns dictionary with layer data.
@@ -87,6 +89,7 @@ class Texture(Component):
87
89
  "exclude_weight": self.exclude_weight,
88
90
  "priority": self.priority,
89
91
  "info_layer": self.info_layer,
92
+ "usage": self.usage,
90
93
  }
91
94
 
92
95
  data = {k: v for k, v in data.items() if v is not None}
@@ -117,29 +120,29 @@ class Texture(Component):
117
120
  weight_postfix = "_weight" if not self.exclude_weight else ""
118
121
  return os.path.join(weights_directory, f"{self.name}{idx}{weight_postfix}.png")
119
122
 
120
- def path_preview(self, previews_directory: str) -> str:
123
+ def path_preview(self, weights_directory: str) -> str:
121
124
  """Returns path to the preview of the first texture of the layer.
122
125
 
123
126
  Arguments:
124
- previews_directory (str): Path to the directory with previews.
127
+ weights_directory (str): Path to the directory with weights.
125
128
 
126
129
  Returns:
127
130
  str: Path to the preview.
128
131
  """
129
- return self.path(previews_directory).replace(".png", "_preview.png")
132
+ return self.path(weights_directory).replace(".png", "_preview.png")
130
133
 
131
- def get_preview_or_path(self, previews_directory: str) -> str:
134
+ def get_preview_or_path(self, weights_directory: str) -> str:
132
135
  """Returns path to the preview of the first texture of the layer if it exists,
133
136
  otherwise returns path to the texture.
134
137
 
135
138
  Arguments:
136
- previews_directory (str): Path to the directory with previews.
139
+ weights_directory (str): Path to the directory with weights.
137
140
 
138
141
  Returns:
139
142
  str: Path to the preview or texture.
140
143
  """
141
- preview_path = self.path_preview(previews_directory)
142
- return preview_path if os.path.isfile(preview_path) else self.path(previews_directory)
144
+ preview_path = self.path_preview(weights_directory)
145
+ return preview_path if os.path.isfile(preview_path) else self.path(weights_directory)
143
146
 
144
147
  def paths(self, weights_directory: str) -> list[str]:
145
148
  """Returns a list of paths to the textures of the layer.
@@ -212,6 +215,20 @@ class Texture(Component):
212
215
  return layer
213
216
  return None
214
217
 
218
+ def get_layer_by_usage(self, usage: str) -> Layer | None:
219
+ """Returns layer by usage.
220
+
221
+ Arguments:
222
+ usage (str): Usage of the layer.
223
+
224
+ Returns:
225
+ Layer | None: Layer.
226
+ """
227
+ for layer in self.layers:
228
+ if layer.usage == usage:
229
+ return layer
230
+ return None
231
+
215
232
  def process(self):
216
233
  self._prepare_weights()
217
234
  self._read_parameters()
@@ -287,7 +304,10 @@ class Texture(Component):
287
304
  Arguments:
288
305
  layer (Layer): Layer with textures and tags.
289
306
  """
290
- size = (self.map_rotated_size, self.map_rotated_size)
307
+ if layer.tags is None:
308
+ size = (self.map_size, self.map_size)
309
+ else:
310
+ size = (self.map_rotated_size, self.map_rotated_size)
291
311
  postfix = "_weight.png" if not layer.exclude_weight else ".png"
292
312
  if layer.count == 0:
293
313
  filepaths = [os.path.join(self._weights_dir, layer.name + postfix)]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: maps4fs
3
- Version: 1.1.6
3
+ Version: 1.2.3
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
@@ -43,7 +43,7 @@ Requires-Dist: pympler
43
43
  <a href="#Background-terrain">Background terrain</a> •
44
44
  <a href="#Overview-image">Overview image</a><br>
45
45
  <a href="#DDS-conversion">DDS conversion</a> •
46
- <a href="#For-advanced-users">For advanced users</a> •
46
+ <a href="#Advanced-settings">Advanced settings</a> •
47
47
  <a href="#Resources">Resources</a> •
48
48
  <a href="#Bugs-and-feature-requests">Bugs and feature requests</a><br>
49
49
  <a href="#Special-thanks">Special thanks</a>
@@ -68,6 +68,8 @@ Requires-Dist: pympler
68
68
  🔄 Support map rotation 🆕<br>
69
69
  🌾 Automatically generates fields 🆕<br>
70
70
  🌽 Automatically generates farmlands 🆕<br>
71
+ 🌿 Automatically generates decorative foliage 🆕<br>
72
+ 🌲 Automatically generates forests 🆕<br>
71
73
  🌍 Based on real-world data from OpenStreetMap<br>
72
74
  🏞️ Generates height map using SRTM dataset<br>
73
75
  📦 Provides a ready-to-use map template for the Giants Editor<br>
@@ -84,6 +86,10 @@ Requires-Dist: pympler
84
86
  🛰️ Realistic background terrain with satellite images.<br><br>
85
87
  <img src="https://github.com/user-attachments/assets/6e3c0e99-2cce-46ac-82db-5cb60bba7a30"><br>
86
88
  📐 Perfectly aligned background terrain.<br><br>
89
+ <img src="https://github.com/user-attachments/assets/5764b2ec-e626-426f-9f5d-beb12ba95133"><br>
90
+ 🌿 Automatically generates decorative foliage.<br><br>
91
+ <img src="https://github.com/user-attachments/assets/27a5e541-a9f5-4504-b8d2-64aae9fb3e52"><br>
92
+ 🌲 Automatically generates forests.<br><br>
87
93
  <img src="https://github.com/user-attachments/assets/80e5923c-22c7-4dc0-8906-680902511f3a"><br>
88
94
  🗒️ True-to-life blueprints for fast and precise modding.<br><br>
89
95
  <img width="480" src="https://github.com/user-attachments/assets/1a8802d2-6a3b-4bfa-af2b-7c09478e199b"><br>
@@ -453,7 +459,7 @@ List of the important DDS files:
453
459
  - `preview.dds` - 2048x2048 pixels, the preview image of the map on the loading screen,
454
460
  - `mapsUS/overview.dds` - 4096x4096 pixels, the overview image of the map (in-game map)
455
461
 
456
- ## For advanced users
462
+ ## Advanced settings
457
463
  The tool supports the custom size of the map. To use this feature select `Custom` in the `Map size` dropdown and enter the desired size. The tool will generate a map with the size you entered.<br>
458
464
 
459
465
  ⛔️ Do not use this feature, if you don't know what you're doing. In most cases, the Giants Editor will just crash on opening the file, because you need to enter specific values for the map size.<br><br>
@@ -478,6 +484,12 @@ You can also apply some advanced settings to the map generation process. Note th
478
484
 
479
485
  - Farmlands margin - this value (in meters) will be applied to each farmland, making it bigger. You can use the value to adjust how much the farmland should be bigger than the actual field. By default, it's set to 3.
480
486
 
487
+ ### Vegetation Advanced settings
488
+
489
+ - 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.
490
+
491
+ - Random plants - when adding decorative foliage, enabling this option will add different species of plants to the map. If unchecked only basic grass (smallDenseMix) will be added. Defaults to True.
492
+
481
493
  ## Resources
482
494
  In this section, you'll find a list of the resources that you need to create a map for the Farming Simulator.<br>
483
495
  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>
@@ -0,0 +1,21 @@
1
+ maps4fs/__init__.py,sha256=da4jmND2Ths9AffnkAKgzLHNkvKFOc_l21gJisPXqWY,155
2
+ maps4fs/logger.py,sha256=B-NEYpMjPAAqlV4VpfTi6nbBFnEABVtQOaYe6nMpidg,1489
3
+ maps4fs/generator/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
4
+ maps4fs/generator/background.py,sha256=fLWk7FSNL08gk3skHfi0iVchnKrYjnLLKAT1g_7sRzc,15907
5
+ maps4fs/generator/component.py,sha256=_d9rHmGh348KOMrLWR8rRDVsbZ2xwJQwZGIGvMIYXPM,17533
6
+ maps4fs/generator/config.py,sha256=b7qY0luC-_WM_c72Ohtlf4FrB37X5cALInbestSdUsw,4382
7
+ maps4fs/generator/dem.py,sha256=rc7ADzjvlZzStOqagsWW0Vrm9-X86aPpoR1RhBF_-OE,16025
8
+ maps4fs/generator/game.py,sha256=ZQeYzPzPB3CG41avdhNCyTZpHEeedqNBuAbNevTZuXg,7931
9
+ maps4fs/generator/grle.py,sha256=3hcr5e2YLXemFi-_x2cLHWbMVb06591k0PZxaBVovH8,17600
10
+ maps4fs/generator/i3d.py,sha256=oK5pKjzvT-gydma5Q6CcDYTVODGxK7MIGajLrAV9JkU,18370
11
+ maps4fs/generator/map.py,sha256=lA1MNAcMwsDtsYxbwwm7DjwP3zraHKnri_xnLUu30j0,5326
12
+ maps4fs/generator/qgis.py,sha256=Es8hLuqN_KH8lDfnJE6He2rWYbAKJ3RGPn-o87S6CPI,6116
13
+ maps4fs/generator/texture.py,sha256=MSkM-rH_836l8zgq1WVcYJeYrUofWBpC8OKJglSmGGQ,26558
14
+ maps4fs/toolbox/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
15
+ maps4fs/toolbox/background.py,sha256=9BXWNqs_n3HgqDiPztWylgYk_QM4YgBpe6_ZNQAWtSc,2154
16
+ maps4fs/toolbox/dem.py,sha256=z9IPFNmYbjiigb3t02ZenI3Mo8odd19c5MZbjDEovTo,3525
17
+ maps4fs-1.2.3.dist-info/LICENSE.md,sha256=pTKD_oUexcn-yccFCTrMeLkZy0ifLRa-VNcDLqLZaIw,10749
18
+ maps4fs-1.2.3.dist-info/METADATA,sha256=F3YcAPR8CA5160fvsWtnxEq3CtObEpiAG5B1DS6iSkg,29885
19
+ maps4fs-1.2.3.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
20
+ maps4fs-1.2.3.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
21
+ maps4fs-1.2.3.dist-info/RECORD,,
@@ -1,21 +0,0 @@
1
- maps4fs/__init__.py,sha256=da4jmND2Ths9AffnkAKgzLHNkvKFOc_l21gJisPXqWY,155
2
- maps4fs/logger.py,sha256=B-NEYpMjPAAqlV4VpfTi6nbBFnEABVtQOaYe6nMpidg,1489
3
- maps4fs/generator/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
4
- maps4fs/generator/background.py,sha256=dnUkS1atEqqz_ryKkcfP_K9ZcwMHZVs97y-twZMpD44,15881
5
- maps4fs/generator/component.py,sha256=sihh2S35q0o38leEU-dpi0is6kYCuxXiWUISAtiQErM,17351
6
- maps4fs/generator/config.py,sha256=b7qY0luC-_WM_c72Ohtlf4FrB37X5cALInbestSdUsw,4382
7
- maps4fs/generator/dem.py,sha256=rc7ADzjvlZzStOqagsWW0Vrm9-X86aPpoR1RhBF_-OE,16025
8
- maps4fs/generator/game.py,sha256=2oA77i9lyNYOlhtOZft6KiPTxgGJbX1Gam1TosMNLis,7407
9
- maps4fs/generator/grle.py,sha256=IVbvzF_azItxLE_ZxaM_suQflEZjJuMgHmv1En51G7A,7592
10
- maps4fs/generator/i3d.py,sha256=SzjAxYacbBQ030N2sHh9c-dhWoG3iADJqCrHBox6vWI,12268
11
- maps4fs/generator/map.py,sha256=7UqLjDZgoY6M0ZxX5Q4Rjee2UGWZ64a3tGyr8B24UO0,4863
12
- maps4fs/generator/qgis.py,sha256=Es8hLuqN_KH8lDfnJE6He2rWYbAKJ3RGPn-o87S6CPI,6116
13
- maps4fs/generator/texture.py,sha256=uSt563KomSVUndl25IgEIi0YuhBQbnhPIoQKa-4A3_E,26016
14
- maps4fs/toolbox/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
15
- maps4fs/toolbox/background.py,sha256=9BXWNqs_n3HgqDiPztWylgYk_QM4YgBpe6_ZNQAWtSc,2154
16
- maps4fs/toolbox/dem.py,sha256=z9IPFNmYbjiigb3t02ZenI3Mo8odd19c5MZbjDEovTo,3525
17
- maps4fs-1.1.6.dist-info/LICENSE.md,sha256=pTKD_oUexcn-yccFCTrMeLkZy0ifLRa-VNcDLqLZaIw,10749
18
- maps4fs-1.1.6.dist-info/METADATA,sha256=W5x23vIOjkx-xxq4L0VIWVIA9pCgOniTQvbFqiTUVFs,28942
19
- maps4fs-1.1.6.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
20
- maps4fs-1.1.6.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
21
- maps4fs-1.1.6.dist-info/RECORD,,