maps4fs 1.2.0__py3-none-any.whl → 1.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,7 +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
- _base_image: str | None = None
39
+ _tree_schema: str | None = None
40
40
 
41
41
  # Order matters! Some components depend on others.
42
42
  components = [Texture, I3d, GRLE, Background, Config]
@@ -110,6 +110,19 @@ class Game:
110
110
  raise ValueError("GRLE layers schema path not set.")
111
111
  return self._grle_schema
112
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
+
113
126
  def dem_file_path(self, map_directory: str) -> str:
114
127
  """Returns the path to the DEM file.
115
128
 
@@ -131,19 +144,6 @@ class Game:
131
144
  str: The path to the weights directory."""
132
145
  raise NotImplementedError
133
146
 
134
- def base_image_path(self, map_directory: str) -> str | None:
135
- """Returns the path to the base density map image.
136
-
137
- Arguments:
138
- map_directory (str): The path to the map directory.
139
-
140
- Returns:
141
- str: The path to the base density map image or None if not set.
142
- """
143
- if self._base_image:
144
- return os.path.join(self.weights_dir_path(map_directory), self._base_image)
145
- return None
146
-
147
147
  def i3d_file_path(self, map_directory: str) -> str:
148
148
  """Returns the path to the i3d file.
149
149
 
@@ -201,7 +201,7 @@ class FS25(Game):
201
201
  _map_template_path = os.path.join(working_directory, "data", "fs25-map-template.zip")
202
202
  _texture_schema = os.path.join(working_directory, "data", "fs25-texture-schema.json")
203
203
  _grle_schema = os.path.join(working_directory, "data", "fs25-grle-schema.json")
204
- _base_image = "base.png"
204
+ _tree_schema = os.path.join(working_directory, "data", "fs25-tree-schema.json")
205
205
 
206
206
  def dem_file_path(self, map_directory: str) -> str:
207
207
  """Returns the path to the DEM file.
maps4fs/generator/grle.py CHANGED
@@ -7,11 +7,16 @@ from xml.etree import ElementTree as ET
7
7
 
8
8
  import cv2
9
9
  import numpy as np
10
+ from shapely.geometry import Polygon # type: ignore
10
11
 
11
12
  from maps4fs.generator.component import Component
13
+ from maps4fs.generator.texture import Texture
12
14
 
13
15
  ISLAND_SIZE_MIN = 10
14
16
  ISLAND_SIZE_MAX = 200
17
+ ISLAND_DISTORTION = 0.3
18
+ ISLAND_VERTEX_COUNT = 30
19
+ ISLAND_ROUNDING_RADIUS = 15
15
20
 
16
21
 
17
22
  # pylint: disable=W0223
@@ -194,34 +199,70 @@ class GRLE(Component):
194
199
  "Farmlands added to the InfoLayer PNG file: %s.", info_layer_farmlands_path
195
200
  )
196
201
 
202
+ # pylint: disable=R0915
197
203
  def _add_plants(self) -> None:
198
204
  """Adds plants to the InfoLayer PNG file."""
199
205
  # 1. Get the path to the densityMap_fruits.png.
200
206
  # 2. Get the path to the base layer (grass).
201
207
  # 3. Detect non-zero areas in the base layer (it's where the plants will be placed).
202
- base_image_path = self.game.base_image_path(self.map_directory)
203
- if not base_image_path or not os.path.isfile(base_image_path):
204
- self.logger.warning("Base image not found in %s.", base_image_path)
208
+ texture_component: Texture | None = self.map.get_component("Texture") # type: ignore
209
+ if not texture_component:
210
+ self.logger.warning("Texture component not found in the map.")
211
+ return
212
+
213
+ grass_layer = texture_component.get_layer_by_usage("grass")
214
+ if not grass_layer:
215
+ self.logger.warning("Grass layer not found in the texture component.")
216
+ return
217
+
218
+ weights_directory = self.game.weights_dir_path(self.map_directory)
219
+ grass_image_path = grass_layer.get_preview_or_path(weights_directory)
220
+ self.logger.debug("Grass image path: %s.", grass_image_path)
221
+
222
+ forest_layer = texture_component.get_layer_by_usage("forest")
223
+ forest_image = None
224
+ if forest_layer:
225
+ forest_image_path = forest_layer.get_preview_or_path(weights_directory)
226
+ self.logger.debug("Forest image path: %s.", forest_image_path)
227
+ if forest_image_path:
228
+ # pylint: disable=no-member
229
+ forest_image = cv2.imread(forest_image_path, cv2.IMREAD_UNCHANGED)
230
+
231
+ if not grass_image_path or not os.path.isfile(grass_image_path):
232
+ self.logger.warning("Base image not found in %s.", grass_image_path)
205
233
  return
206
234
 
207
235
  density_map_fruit_path = os.path.join(
208
236
  self.game.weights_dir_path(self.map_directory), "densityMap_fruits.png"
209
237
  )
210
238
 
239
+ self.logger.debug("Density map for fruits path: %s.", density_map_fruit_path)
240
+
211
241
  if not os.path.isfile(density_map_fruit_path):
212
242
  self.logger.warning("Density map for fruits not found in %s.", density_map_fruit_path)
213
243
  return
214
244
 
215
245
  # Single channeled 8-bit image, where non-zero values (255) are where the grass is.
216
- base_image = cv2.imread(base_image_path, cv2.IMREAD_UNCHANGED) # pylint: disable=no-member
246
+ grass_image = cv2.imread( # pylint: disable=no-member
247
+ grass_image_path, cv2.IMREAD_UNCHANGED # pylint: disable=no-member
248
+ )
217
249
 
218
250
  # Density map of the fruits is 2X size of the base image, so we need to resize it.
219
251
  # We'll resize the base image to make it bigger, so we can compare the values.
220
- base_image = cv2.resize( # pylint: disable=no-member
221
- base_image,
222
- (base_image.shape[1] * 2, base_image.shape[0] * 2),
252
+ grass_image = cv2.resize( # pylint: disable=no-member
253
+ grass_image,
254
+ (grass_image.shape[1] * 2, grass_image.shape[0] * 2),
223
255
  interpolation=cv2.INTER_NEAREST, # pylint: disable=no-member
224
256
  )
257
+ if forest_image is not None:
258
+ forest_image = cv2.resize( # pylint: disable=no-member
259
+ forest_image,
260
+ (forest_image.shape[1] * 2, forest_image.shape[0] * 2),
261
+ interpolation=cv2.INTER_NEAREST, # pylint: disable=no-member
262
+ )
263
+
264
+ # Add non zero values from the forest image to the grass image.
265
+ grass_image[forest_image != 0] = 255
225
266
 
226
267
  # B and G channels remain the same (zeros), while we change the R channel.
227
268
  possible_R_values = [33, 65, 97, 129, 161, 193, 225] # pylint: disable=C0103
@@ -252,46 +293,79 @@ class GRLE(Component):
252
293
  y = randint(0, image.shape[0] - island_size)
253
294
 
254
295
  # Randomly choose the shape of the island.
255
- shapes = ["circle", "ellipse", "polygon"]
256
- shape = choice(shapes)
296
+ # shapes = ["circle", "ellipse", "polygon"]
297
+ # shape = choice(shapes)
257
298
 
258
299
  try:
259
- if shape == "circle":
260
- center = (x + island_size // 2, y + island_size // 2)
261
- radius = island_size // 2
262
- cv2.circle(image, center, radius, plant_value, -1) # type: ignore
263
- elif shape == "ellipse":
264
- center = (x + island_size // 2, y + island_size // 2)
265
- axes = (island_size // 2, island_size // 4)
266
- angle = 0
267
- cv2.ellipse( # type: ignore
268
- image, center, axes, angle, 0, 360, plant_value, -1
269
- )
270
- elif shape == "polygon":
271
- nodes_count = randint(20, 50)
272
- nodes = []
273
- for _ in range(nodes_count):
274
- node = (randint(x, x + island_size), randint(y, y + island_size))
275
- nodes.append(node)
276
- nodes = np.array(nodes, np.int32) # type: ignore
277
- cv2.fillPoly(image, [nodes], plant_value) # type: ignore
300
+ polygon_points = get_rounded_polygon(
301
+ num_vertices=ISLAND_VERTEX_COUNT,
302
+ center=(x + island_size // 2, y + island_size // 2),
303
+ radius=island_size // 2,
304
+ rounding_radius=ISLAND_ROUNDING_RADIUS,
305
+ )
306
+ if not polygon_points:
307
+ continue
308
+
309
+ nodes = np.array(polygon_points, np.int32) # type: ignore
310
+ cv2.fillPoly(image, [nodes], plant_value) # type: ignore
278
311
  except Exception: # pylint: disable=W0703
279
312
  continue
280
313
 
281
314
  return image
282
315
 
283
- updated_base_image = base_image.copy()
316
+ def get_rounded_polygon(
317
+ num_vertices: int, center: tuple[int, int], radius: int, rounding_radius: int
318
+ ) -> list[tuple[int, int]] | None:
319
+ """Get a randomly rounded polygon.
320
+
321
+ Arguments:
322
+ num_vertices (int): The number of vertices of the polygon.
323
+ center (tuple[int, int]): The center of the polygon.
324
+ radius (int): The radius of the polygon.
325
+ rounding_radius (int): The rounding radius of the polygon.
326
+
327
+ Returns:
328
+ list[tuple[int, int]] | None: The rounded polygon.
329
+ """
330
+ angle_offset = np.pi / num_vertices
331
+ angles = np.linspace(0, 2 * np.pi, num_vertices, endpoint=False) + angle_offset
332
+ random_angles = angles + np.random.uniform(
333
+ -ISLAND_DISTORTION, ISLAND_DISTORTION, num_vertices
334
+ ) # Add randomness to angles
335
+ random_radii = radius + np.random.uniform(
336
+ -radius * ISLAND_DISTORTION, radius * ISLAND_DISTORTION, num_vertices
337
+ ) # Add randomness to radii
338
+
339
+ points = [
340
+ (center[0] + np.cos(a) * r, center[1] + np.sin(a) * r)
341
+ for a, r in zip(random_angles, random_radii)
342
+ ]
343
+ polygon = Polygon(points)
344
+ buffered_polygon = polygon.buffer(rounding_radius, resolution=16)
345
+ rounded_polygon = list(buffered_polygon.exterior.coords)
346
+ if not rounded_polygon:
347
+ return None
348
+ return rounded_polygon
349
+
350
+ grass_image_copy = grass_image.copy()
351
+ if forest_image is not None:
352
+ # Add the forest layer to the base image, to merge the masks.
353
+ grass_image_copy[forest_image != 0] = 33
284
354
  # Set all the non-zero values to 33.
285
- updated_base_image[base_image != 0] = 33
355
+ grass_image_copy[grass_image != 0] = 33
286
356
 
287
357
  # Add islands of plants to the base image.
288
358
  island_count = self.map_size
289
359
  self.logger.info("Adding %s islands of plants to the base image.", island_count)
290
- updated_base_image = create_island_of_plants(updated_base_image, island_count)
360
+ grass_image_copy = create_island_of_plants(grass_image_copy, island_count)
291
361
  self.logger.debug("Islands of plants added to the base image.")
292
362
 
363
+ # Sligtly reduce the size of the grass_image, that we'll use as mask.
364
+ kernel = np.ones((3, 3), np.uint8)
365
+ grass_image = cv2.erode(grass_image, kernel, iterations=1)
366
+
293
367
  # Remove the values where the base image has zeros.
294
- updated_base_image[base_image == 0] = 0
368
+ grass_image_copy[grass_image == 0] = 0
295
369
  self.logger.debug("Removed the values where the base image has zeros.")
296
370
 
297
371
  # Value of 33 represents the base grass plant.
@@ -305,7 +379,7 @@ class GRLE(Component):
305
379
  self.logger.debug("Density map for fruits loaded, shape: %s.", density_map_fruits.shape)
306
380
 
307
381
  # Put the updated base image as the B channel in the density map.
308
- density_map_fruits[:, :, 0] = updated_base_image
382
+ density_map_fruits[:, :, 0] = grass_image_copy
309
383
  self.logger.debug("Updated base image added as the B channel in the density map.")
310
384
 
311
385
  # Save the updated density map.
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."""
@@ -316,3 +328,138 @@ class I3d(Component):
316
328
  attribute_node.set("type", attr_type)
317
329
  attribute_node.set("value", value)
318
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.
@@ -5,7 +5,6 @@ from __future__ import annotations
5
5
  import json
6
6
  import os
7
7
  import re
8
- import shutil
9
8
  from collections import defaultdict
10
9
  from typing import Any, Callable, Generator, Optional
11
10
 
@@ -64,6 +63,7 @@ class Texture(Component):
64
63
  exclude_weight: bool = False,
65
64
  priority: int | None = None,
66
65
  info_layer: str | None = None,
66
+ usage: str | None = None,
67
67
  ):
68
68
  self.name = name
69
69
  self.count = count
@@ -73,6 +73,7 @@ class Texture(Component):
73
73
  self.exclude_weight = exclude_weight
74
74
  self.priority = priority
75
75
  self.info_layer = info_layer
76
+ self.usage = usage
76
77
 
77
78
  def to_json(self) -> dict[str, str | list[str] | bool]: # type: ignore
78
79
  """Returns dictionary with layer data.
@@ -88,6 +89,7 @@ class Texture(Component):
88
89
  "exclude_weight": self.exclude_weight,
89
90
  "priority": self.priority,
90
91
  "info_layer": self.info_layer,
92
+ "usage": self.usage,
91
93
  }
92
94
 
93
95
  data = {k: v for k, v in data.items() if v is not None}
@@ -118,29 +120,29 @@ class Texture(Component):
118
120
  weight_postfix = "_weight" if not self.exclude_weight else ""
119
121
  return os.path.join(weights_directory, f"{self.name}{idx}{weight_postfix}.png")
120
122
 
121
- def path_preview(self, previews_directory: str) -> str:
123
+ def path_preview(self, weights_directory: str) -> str:
122
124
  """Returns path to the preview of the first texture of the layer.
123
125
 
124
126
  Arguments:
125
- previews_directory (str): Path to the directory with previews.
127
+ weights_directory (str): Path to the directory with weights.
126
128
 
127
129
  Returns:
128
130
  str: Path to the preview.
129
131
  """
130
- return self.path(previews_directory).replace(".png", "_preview.png")
132
+ return self.path(weights_directory).replace(".png", "_preview.png")
131
133
 
132
- def get_preview_or_path(self, previews_directory: str) -> str:
134
+ def get_preview_or_path(self, weights_directory: str) -> str:
133
135
  """Returns path to the preview of the first texture of the layer if it exists,
134
136
  otherwise returns path to the texture.
135
137
 
136
138
  Arguments:
137
- previews_directory (str): Path to the directory with previews.
139
+ weights_directory (str): Path to the directory with weights.
138
140
 
139
141
  Returns:
140
142
  str: Path to the preview or texture.
141
143
  """
142
- preview_path = self.path_preview(previews_directory)
143
- 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)
144
146
 
145
147
  def paths(self, weights_directory: str) -> list[str]:
146
148
  """Returns a list of paths to the textures of the layer.
@@ -213,6 +215,20 @@ class Texture(Component):
213
215
  return layer
214
216
  return None
215
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
+
216
232
  def process(self):
217
233
  self._prepare_weights()
218
234
  self._read_parameters()
@@ -243,17 +259,6 @@ class Texture(Component):
243
259
  "Skipping rotation of layer %s because it has no tags.", layer.name
244
260
  )
245
261
 
246
- base_path = self.game.base_image_path(self.map_directory)
247
- if base_path:
248
- base_layer = self.get_base_layer()
249
- if base_layer:
250
- base_layer_path = base_layer.get_preview_or_path(self._weights_dir)
251
- self.logger.debug(
252
- "Copying base layer to use it later for density map to %s.", base_path
253
- )
254
- # Make a copy of a base layer to the fruits density map.
255
- shutil.copy(base_layer_path, base_path)
256
-
257
262
  # pylint: disable=W0201
258
263
  def _read_parameters(self) -> None:
259
264
  """Reads map parameters from OSM data, such as:
@@ -299,7 +304,10 @@ class Texture(Component):
299
304
  Arguments:
300
305
  layer (Layer): Layer with textures and tags.
301
306
  """
302
- 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)
303
311
  postfix = "_weight.png" if not layer.exclude_weight else ".png"
304
312
  if layer.count == 0:
305
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.2.0
3
+ Version: 1.2.1
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
@@ -69,6 +69,7 @@ Requires-Dist: pympler
69
69
  🌾 Automatically generates fields 🆕<br>
70
70
  🌽 Automatically generates farmlands 🆕<br>
71
71
  🌿 Automatically generates decorative foliage 🆕<br>
72
+ 🌲 Automatically generates forests 🆕<br>
72
73
  🌍 Based on real-world data from OpenStreetMap<br>
73
74
  🏞️ Generates height map using SRTM dataset<br>
74
75
  📦 Provides a ready-to-use map template for the Giants Editor<br>
@@ -87,6 +88,8 @@ Requires-Dist: pympler
87
88
  📐 Perfectly aligned background terrain.<br><br>
88
89
  <img src="https://github.com/user-attachments/assets/5764b2ec-e626-426f-9f5d-beb12ba95133"><br>
89
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>
90
93
  <img src="https://github.com/user-attachments/assets/80e5923c-22c7-4dc0-8906-680902511f3a"><br>
91
94
  🗒️ True-to-life blueprints for fast and precise modding.<br><br>
92
95
  <img width="480" src="https://github.com/user-attachments/assets/1a8802d2-6a3b-4bfa-af2b-7c09478e199b"><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=QNb1WAnTiGcFdCatu2XOWuLt2NwNBidkq6s8sY7bBgY,17245
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.1.dist-info/LICENSE.md,sha256=pTKD_oUexcn-yccFCTrMeLkZy0ifLRa-VNcDLqLZaIw,10749
18
+ maps4fs-1.2.1.dist-info/METADATA,sha256=43v3vGKA2z9uEd4r2M0wcebMv3fKEJESFbkUOshkZHU,29340
19
+ maps4fs-1.2.1.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
20
+ maps4fs-1.2.1.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
21
+ maps4fs-1.2.1.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=M_tN1oYrQd14kWYQnbzutHpb3sT8s3V_7Lxi1IPw8VE,7923
9
- maps4fs/generator/grle.py,sha256=HBeD5ETAz8GxJte6BuXpIBhsUpFcCul4eBMoQXRTQnw,14110
10
- maps4fs/generator/i3d.py,sha256=66GTg4e6-RlT0q1JFVd_4BB-aEXrxAWZgGz4YABbgxA,12819
11
- maps4fs/generator/map.py,sha256=7UqLjDZgoY6M0ZxX5Q4Rjee2UGWZ64a3tGyr8B24UO0,4863
12
- maps4fs/generator/qgis.py,sha256=Es8hLuqN_KH8lDfnJE6He2rWYbAKJ3RGPn-o87S6CPI,6116
13
- maps4fs/generator/texture.py,sha256=OaubSDheXlZOSfCFsROLDCXDNPeSrCja7ahldLBArWs,26550
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.0.dist-info/LICENSE.md,sha256=pTKD_oUexcn-yccFCTrMeLkZy0ifLRa-VNcDLqLZaIw,10749
18
- maps4fs-1.2.0.dist-info/METADATA,sha256=-qsScy3CYrOynYBooNpoKs60FT0z50830j39yqlX46Q,29152
19
- maps4fs-1.2.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
20
- maps4fs-1.2.0.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
21
- maps4fs-1.2.0.dist-info/RECORD,,