maps4fs 1.1.6__tar.gz → 1.2.4__tar.gz

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.
Files changed (28) hide show
  1. {maps4fs-1.1.6 → maps4fs-1.2.4}/PKG-INFO +23 -3
  2. {maps4fs-1.1.6 → maps4fs-1.2.4}/README.md +22 -2
  3. {maps4fs-1.1.6 → maps4fs-1.2.4}/maps4fs/generator/background.py +178 -15
  4. {maps4fs-1.1.6 → maps4fs-1.2.4}/maps4fs/generator/component.py +12 -2
  5. {maps4fs-1.1.6 → maps4fs-1.2.4}/maps4fs/generator/game.py +15 -0
  6. maps4fs-1.2.4/maps4fs/generator/grle.py +397 -0
  7. {maps4fs-1.1.6 → maps4fs-1.2.4}/maps4fs/generator/i3d.py +163 -1
  8. {maps4fs-1.1.6 → maps4fs-1.2.4}/maps4fs/generator/map.py +16 -1
  9. {maps4fs-1.1.6 → maps4fs-1.2.4}/maps4fs/generator/texture.py +61 -17
  10. {maps4fs-1.1.6 → maps4fs-1.2.4}/maps4fs.egg-info/PKG-INFO +23 -3
  11. {maps4fs-1.1.6 → maps4fs-1.2.4}/pyproject.toml +1 -1
  12. maps4fs-1.1.6/maps4fs/generator/grle.py +0 -186
  13. {maps4fs-1.1.6 → maps4fs-1.2.4}/LICENSE.md +0 -0
  14. {maps4fs-1.1.6 → maps4fs-1.2.4}/maps4fs/__init__.py +0 -0
  15. {maps4fs-1.1.6 → maps4fs-1.2.4}/maps4fs/generator/__init__.py +0 -0
  16. {maps4fs-1.1.6 → maps4fs-1.2.4}/maps4fs/generator/config.py +0 -0
  17. {maps4fs-1.1.6 → maps4fs-1.2.4}/maps4fs/generator/dem.py +0 -0
  18. {maps4fs-1.1.6 → maps4fs-1.2.4}/maps4fs/generator/qgis.py +0 -0
  19. {maps4fs-1.1.6 → maps4fs-1.2.4}/maps4fs/logger.py +0 -0
  20. {maps4fs-1.1.6 → maps4fs-1.2.4}/maps4fs/toolbox/__init__.py +0 -0
  21. {maps4fs-1.1.6 → maps4fs-1.2.4}/maps4fs/toolbox/background.py +0 -0
  22. {maps4fs-1.1.6 → maps4fs-1.2.4}/maps4fs/toolbox/dem.py +0 -0
  23. {maps4fs-1.1.6 → maps4fs-1.2.4}/maps4fs.egg-info/SOURCES.txt +0 -0
  24. {maps4fs-1.1.6 → maps4fs-1.2.4}/maps4fs.egg-info/dependency_links.txt +0 -0
  25. {maps4fs-1.1.6 → maps4fs-1.2.4}/maps4fs.egg-info/requires.txt +0 -0
  26. {maps4fs-1.1.6 → maps4fs-1.2.4}/maps4fs.egg-info/top_level.txt +0 -0
  27. {maps4fs-1.1.6 → maps4fs-1.2.4}/setup.cfg +0 -0
  28. {maps4fs-1.1.6 → maps4fs-1.2.4}/tests/test_generator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: maps4fs
3
- Version: 1.1.6
3
+ Version: 1.2.4
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,9 @@ 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>
73
+ 🌊 Automatically generates water planes 🆕<br>
71
74
  🌍 Based on real-world data from OpenStreetMap<br>
72
75
  🏞️ Generates height map using SRTM dataset<br>
73
76
  📦 Provides a ready-to-use map template for the Giants Editor<br>
@@ -84,6 +87,12 @@ Requires-Dist: pympler
84
87
  🛰️ Realistic background terrain with satellite images.<br><br>
85
88
  <img src="https://github.com/user-attachments/assets/6e3c0e99-2cce-46ac-82db-5cb60bba7a30"><br>
86
89
  📐 Perfectly aligned background terrain.<br><br>
90
+ <img src="https://github.com/user-attachments/assets/5764b2ec-e626-426f-9f5d-beb12ba95133"><br>
91
+ 🌿 Automatically generates decorative foliage.<br><br>
92
+ <img src="https://github.com/user-attachments/assets/27a5e541-a9f5-4504-b8d2-64aae9fb3e52"><br>
93
+ 🌲 Automatically generates forests.<br><br>
94
+ <img src="https://github.com/user-attachments/assets/cce7d4e0-cba2-4dd2-b22d-03137fb2e860"><br>
95
+ 🌊 Automatically generates water planes.<br><br>
87
96
  <img src="https://github.com/user-attachments/assets/80e5923c-22c7-4dc0-8906-680902511f3a"><br>
88
97
  🗒️ True-to-life blueprints for fast and precise modding.<br><br>
89
98
  <img width="480" src="https://github.com/user-attachments/assets/1a8802d2-6a3b-4bfa-af2b-7c09478e199b"><br>
@@ -95,6 +104,9 @@ Requires-Dist: pympler
95
104
  <a href="https://www.youtube.com/watch?v=Nl_aqXJ5nAk" target="_blank"><img src="https://github.com/user-attachments/assets/4845e030-0e73-47ab-a5a3-430308913060"/></a>
96
105
  <p align="center"><i>How to Generate a Map for Farming Simulator 25 and 22 from a real place using maps4FS.</i></p>
97
106
 
107
+ ![Map example](https://github.com/user-attachments/assets/c46a3581-dd17-462f-b815-e36d4f724947)
108
+ <p align="center"><i>Map example generated with maps4fs.</i></p>
109
+
98
110
  ## Quick Start
99
111
  There are several ways to use the tool. You obviously need the **first one**, but you can choose any of the others depending on your needs.<br>
100
112
  ### 🚜 For most users
@@ -453,7 +465,7 @@ List of the important DDS files:
453
465
  - `preview.dds` - 2048x2048 pixels, the preview image of the map on the loading screen,
454
466
  - `mapsUS/overview.dds` - 4096x4096 pixels, the overview image of the map (in-game map)
455
467
 
456
- ## For advanced users
468
+ ## Advanced settings
457
469
  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
470
 
459
471
  ⛔️ 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>
@@ -470,6 +482,8 @@ You can also apply some advanced settings to the map generation process. Note th
470
482
 
471
483
  - Plateau height: this value will be added to each pixel of the DEM image, making it "higher". It's useful when you want to add some negative heights on the map, that appear to be in a "low" place. By default, it's set to 0.
472
484
 
485
+ - Water depth: this value will be subtracted from each pixel of the DEM image, where water resources are located. Pay attention that it's not in meters, instead it in the pixel value of DEM, which is 16 bit image with possible values from 0 to 65535. When this value is set, the same value will be added to the plateau setting to avoid negative heights.
486
+
473
487
  ### Texture Advanced settings
474
488
 
475
489
  - Fields padding - this value (in meters) will be applied to each field, making it smaller. It's useful when the fields are too close to each other and you want to make them smaller. By default, it's set to 0.
@@ -478,6 +492,12 @@ You can also apply some advanced settings to the map generation process. Note th
478
492
 
479
493
  - 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
494
 
495
+ ### Vegetation Advanced settings
496
+
497
+ - 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.
498
+
499
+ - 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.
500
+
481
501
  ## Resources
482
502
  In this section, you'll find a list of the resources that you need to create a map for the Farming Simulator.<br>
483
503
  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>
@@ -18,7 +18,7 @@
18
18
  <a href="#Background-terrain">Background terrain</a> •
19
19
  <a href="#Overview-image">Overview image</a><br>
20
20
  <a href="#DDS-conversion">DDS conversion</a> •
21
- <a href="#For-advanced-users">For advanced users</a> •
21
+ <a href="#Advanced-settings">Advanced settings</a> •
22
22
  <a href="#Resources">Resources</a> •
23
23
  <a href="#Bugs-and-feature-requests">Bugs and feature requests</a><br>
24
24
  <a href="#Special-thanks">Special thanks</a>
@@ -43,6 +43,9 @@
43
43
  🔄 Support map rotation 🆕<br>
44
44
  🌾 Automatically generates fields 🆕<br>
45
45
  🌽 Automatically generates farmlands 🆕<br>
46
+ 🌿 Automatically generates decorative foliage 🆕<br>
47
+ 🌲 Automatically generates forests 🆕<br>
48
+ 🌊 Automatically generates water planes 🆕<br>
46
49
  🌍 Based on real-world data from OpenStreetMap<br>
47
50
  🏞️ Generates height map using SRTM dataset<br>
48
51
  📦 Provides a ready-to-use map template for the Giants Editor<br>
@@ -59,6 +62,12 @@
59
62
  🛰️ Realistic background terrain with satellite images.<br><br>
60
63
  <img src="https://github.com/user-attachments/assets/6e3c0e99-2cce-46ac-82db-5cb60bba7a30"><br>
61
64
  📐 Perfectly aligned background terrain.<br><br>
65
+ <img src="https://github.com/user-attachments/assets/5764b2ec-e626-426f-9f5d-beb12ba95133"><br>
66
+ 🌿 Automatically generates decorative foliage.<br><br>
67
+ <img src="https://github.com/user-attachments/assets/27a5e541-a9f5-4504-b8d2-64aae9fb3e52"><br>
68
+ 🌲 Automatically generates forests.<br><br>
69
+ <img src="https://github.com/user-attachments/assets/cce7d4e0-cba2-4dd2-b22d-03137fb2e860"><br>
70
+ 🌊 Automatically generates water planes.<br><br>
62
71
  <img src="https://github.com/user-attachments/assets/80e5923c-22c7-4dc0-8906-680902511f3a"><br>
63
72
  🗒️ True-to-life blueprints for fast and precise modding.<br><br>
64
73
  <img width="480" src="https://github.com/user-attachments/assets/1a8802d2-6a3b-4bfa-af2b-7c09478e199b"><br>
@@ -70,6 +79,9 @@
70
79
  <a href="https://www.youtube.com/watch?v=Nl_aqXJ5nAk" target="_blank"><img src="https://github.com/user-attachments/assets/4845e030-0e73-47ab-a5a3-430308913060"/></a>
71
80
  <p align="center"><i>How to Generate a Map for Farming Simulator 25 and 22 from a real place using maps4FS.</i></p>
72
81
 
82
+ ![Map example](https://github.com/user-attachments/assets/c46a3581-dd17-462f-b815-e36d4f724947)
83
+ <p align="center"><i>Map example generated with maps4fs.</i></p>
84
+
73
85
  ## Quick Start
74
86
  There are several ways to use the tool. You obviously need the **first one**, but you can choose any of the others depending on your needs.<br>
75
87
  ### 🚜 For most users
@@ -428,7 +440,7 @@ List of the important DDS files:
428
440
  - `preview.dds` - 2048x2048 pixels, the preview image of the map on the loading screen,
429
441
  - `mapsUS/overview.dds` - 4096x4096 pixels, the overview image of the map (in-game map)
430
442
 
431
- ## For advanced users
443
+ ## Advanced settings
432
444
  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>
433
445
 
434
446
  ⛔️ 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>
@@ -445,6 +457,8 @@ You can also apply some advanced settings to the map generation process. Note th
445
457
 
446
458
  - Plateau height: this value will be added to each pixel of the DEM image, making it "higher". It's useful when you want to add some negative heights on the map, that appear to be in a "low" place. By default, it's set to 0.
447
459
 
460
+ - Water depth: this value will be subtracted from each pixel of the DEM image, where water resources are located. Pay attention that it's not in meters, instead it in the pixel value of DEM, which is 16 bit image with possible values from 0 to 65535. When this value is set, the same value will be added to the plateau setting to avoid negative heights.
461
+
448
462
  ### Texture Advanced settings
449
463
 
450
464
  - Fields padding - this value (in meters) will be applied to each field, making it smaller. It's useful when the fields are too close to each other and you want to make them smaller. By default, it's set to 0.
@@ -453,6 +467,12 @@ You can also apply some advanced settings to the map generation process. Note th
453
467
 
454
468
  - 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.
455
469
 
470
+ ### Vegetation Advanced settings
471
+
472
+ - 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.
473
+
474
+ - 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.
475
+
456
476
  ## Resources
457
477
  In this section, you'll find a list of the resources that you need to create a map for the Farming Simulator.<br>
458
478
  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>
@@ -3,8 +3,10 @@ around the map."""
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
+ import json
6
7
  import os
7
8
  import shutil
9
+ from copy import deepcopy
8
10
 
9
11
  import cv2
10
12
  import numpy as np
@@ -17,6 +19,7 @@ from maps4fs.generator.dem import (
17
19
  DEFAULT_PLATEAU,
18
20
  DEM,
19
21
  )
22
+ from maps4fs.generator.texture import Texture
20
23
 
21
24
  DEFAULT_DISTANCE = 2048
22
25
  RESIZE_FACTOR = 1 / 8
@@ -25,6 +28,7 @@ FULL_PREVIEW_NAME = "PREVIEW"
25
28
  ELEMENTS = [FULL_NAME, FULL_PREVIEW_NAME]
26
29
 
27
30
 
31
+ # pylint: disable=R0902
28
32
  class Background(Component):
29
33
  """Component for creating 3D obj files based on DEM data around the map.
30
34
 
@@ -43,7 +47,9 @@ class Background(Component):
43
47
  def preprocess(self) -> None:
44
48
  """Registers the DEMs for the background terrain."""
45
49
  self.light_version = self.kwargs.get("light_version", False)
50
+ self.water_depth = self.kwargs.get("water_depth", 0)
46
51
  self.stl_preview_path: str | None = None
52
+ self.water_resources_path: str | None = None
47
53
 
48
54
  if self.rotation:
49
55
  self.logger.debug("Rotation is enabled: %s.", self.rotation)
@@ -51,21 +57,29 @@ class Background(Component):
51
57
  else:
52
58
  output_size_multiplier = 1
53
59
 
54
- background_size = self.map_size + DEFAULT_DISTANCE * 2
55
- rotated_size = int(background_size * output_size_multiplier)
60
+ self.background_size = self.map_size + DEFAULT_DISTANCE * 2
61
+ self.rotated_size = int(self.background_size * output_size_multiplier)
56
62
 
57
63
  self.background_directory = os.path.join(self.map_directory, "background")
64
+ self.water_directory = os.path.join(self.map_directory, "water")
58
65
  os.makedirs(self.background_directory, exist_ok=True)
66
+ os.makedirs(self.water_directory, exist_ok=True)
59
67
 
60
68
  autoprocesses = [self.kwargs.get("auto_process", False), False]
69
+ self.output_paths = [
70
+ os.path.join(self.background_directory, f"{name}.png") for name in ELEMENTS
71
+ ]
72
+ self.not_substracted_path = os.path.join(self.background_directory, "not_substracted.png")
73
+
61
74
  dems = []
62
75
 
63
- for name, autoprocess in zip(ELEMENTS, autoprocesses):
76
+ for name, autoprocess, output_path in zip(ELEMENTS, autoprocesses, self.output_paths):
64
77
  dem = DEM(
65
78
  self.game,
79
+ self.map,
66
80
  self.coordinates,
67
- background_size,
68
- rotated_size,
81
+ self.background_size,
82
+ self.rotated_size,
69
83
  self.rotation,
70
84
  self.map_directory,
71
85
  self.logger,
@@ -76,8 +90,8 @@ class Background(Component):
76
90
  )
77
91
  dem.preprocess()
78
92
  dem.is_preview = self.is_preview(name) # type: ignore
79
- dem.set_output_resolution((rotated_size, rotated_size))
80
- dem.set_dem_path(os.path.join(self.background_directory, f"{name}.png"))
93
+ dem.set_output_resolution((self.rotated_size, self.rotated_size))
94
+ dem.set_dem_path(output_path)
81
95
  dems.append(dem)
82
96
 
83
97
  self.dems = dems
@@ -97,8 +111,17 @@ class Background(Component):
97
111
  """Launches the component processing. Iterates over all tiles and processes them
98
112
  as a result the DEM files will be saved, then based on them the obj files will be
99
113
  generated."""
114
+ self.create_background_textures()
115
+
100
116
  for dem in self.dems:
101
117
  dem.process()
118
+ if not dem.is_preview: # type: ignore
119
+ shutil.copyfile(dem.dem_path, self.not_substracted_path)
120
+
121
+ if self.water_depth:
122
+ self.subtraction()
123
+
124
+ for dem in self.dems:
102
125
  if not dem.is_preview: # type: ignore
103
126
  cutted_dem_path = self.cutout(dem.dem_path)
104
127
  if self.game.additional_dem_name is not None:
@@ -106,6 +129,7 @@ class Background(Component):
106
129
 
107
130
  if not self.light_version:
108
131
  self.generate_obj_files()
132
+ self.generate_water_resources_obj()
109
133
  else:
110
134
  self.logger.info("Light version is enabled, obj files will not be generated.")
111
135
 
@@ -222,13 +246,20 @@ class Background(Component):
222
246
  return main_dem_path
223
247
 
224
248
  # pylint: disable=too-many-locals
225
- def plane_from_np(self, dem_data: np.ndarray, save_path: str, is_preview: bool = False) -> None:
249
+ def plane_from_np(
250
+ self,
251
+ dem_data: np.ndarray,
252
+ save_path: str,
253
+ is_preview: bool = False,
254
+ include_zeros: bool = True,
255
+ ) -> None:
226
256
  """Generates a 3D obj file based on DEM data.
227
257
 
228
258
  Arguments:
229
259
  dem_data (np.ndarray) -- The DEM data as a numpy array.
230
260
  save_path (str) -- The path where the obj file will be saved.
231
261
  is_preview (bool, optional) -- If True, the preview mesh will be generated.
262
+ include_zeros (bool, optional) -- If True, the mesh will include the zero height values.
232
263
  """
233
264
  dem_data = cv2.resize( # pylint: disable=no-member
234
265
  dem_data, (0, 0), fx=RESIZE_FACTOR, fy=RESIZE_FACTOR
@@ -246,6 +277,9 @@ class Background(Component):
246
277
  x, y = np.meshgrid(x, y)
247
278
  z = dem_data
248
279
 
280
+ ground = z.max()
281
+ self.logger.debug("Ground level: %s", ground)
282
+
249
283
  self.logger.debug(
250
284
  "Starting to generate a mesh for with shape: %s x %s. This may take a while...",
251
285
  cols,
@@ -255,6 +289,8 @@ class Background(Component):
255
289
  vertices = np.column_stack([x.ravel(), y.ravel(), z.ravel()])
256
290
  faces = []
257
291
 
292
+ skipped = 0
293
+
258
294
  for i in range(rows - 1):
259
295
  for j in range(cols - 1):
260
296
  top_left = i * cols + j
@@ -262,9 +298,15 @@ class Background(Component):
262
298
  bottom_left = top_left + cols
263
299
  bottom_right = bottom_left + 1
264
300
 
301
+ if ground in [z[i, j], z[i, j + 1], z[i + 1, j], z[i + 1, j + 1]]:
302
+ skipped += 1
303
+ continue
304
+
265
305
  faces.append([top_left, bottom_left, bottom_right])
266
306
  faces.append([top_left, bottom_right, top_right])
267
307
 
308
+ self.logger.debug("Skipped faces: %s", skipped)
309
+
268
310
  faces = np.array(faces) # type: ignore
269
311
  mesh = trimesh.Trimesh(vertices=vertices, faces=faces)
270
312
 
@@ -282,13 +324,14 @@ class Background(Component):
282
324
  mesh.apply_scale([0.5, 0.5, 0.5])
283
325
  self.mesh_to_stl(mesh)
284
326
  else:
285
- multiplier = self.kwargs.get("multiplier", DEFAULT_MULTIPLIER)
286
- if multiplier != 1:
287
- z_scaling_factor = 1 / multiplier
288
- else:
289
- z_scaling_factor = 1 / 2**5
290
- self.logger.debug("Z scaling factor: %s", z_scaling_factor)
291
- mesh.apply_scale([1 / RESIZE_FACTOR, 1 / RESIZE_FACTOR, z_scaling_factor])
327
+ if not include_zeros:
328
+ multiplier = self.kwargs.get("multiplier", DEFAULT_MULTIPLIER)
329
+ if multiplier != 1:
330
+ z_scaling_factor = 1 / multiplier
331
+ else:
332
+ z_scaling_factor = 1 / 2**5
333
+ self.logger.debug("Z scaling factor: %s", z_scaling_factor)
334
+ mesh.apply_scale([1 / RESIZE_FACTOR, 1 / RESIZE_FACTOR, z_scaling_factor])
292
335
 
293
336
  mesh.export(save_path)
294
337
  self.logger.debug("Obj file saved: %s", save_path)
@@ -412,3 +455,123 @@ class Background(Component):
412
455
 
413
456
  cv2.imwrite(colored_dem_path, dem_data_colored)
414
457
  return colored_dem_path
458
+
459
+ def create_background_textures(self) -> None:
460
+ """Creates background textures for the map."""
461
+ if not os.path.isfile(self.game.texture_schema):
462
+ self.logger.warning("Texture schema file not found: %s", self.game.texture_schema)
463
+ return
464
+
465
+ with open(self.game.texture_schema, "r", encoding="utf-8") as f:
466
+ layers_schema = json.load(f)
467
+
468
+ background_layers = []
469
+ for layer in layers_schema:
470
+ if layer.get("background") is True:
471
+ layer_copy = deepcopy(layer)
472
+ layer_copy["count"] = 1
473
+ layer_copy["name"] = f"{layer['name']}_background"
474
+ background_layers.append(layer_copy)
475
+
476
+ if not background_layers:
477
+ return
478
+
479
+ self.background_texture = Texture( # pylint: disable=W0201
480
+ self.game,
481
+ self.map,
482
+ self.coordinates,
483
+ self.background_size,
484
+ self.rotated_size,
485
+ rotation=self.rotation,
486
+ map_directory=self.map_directory,
487
+ logger=self.logger,
488
+ light_version=self.light_version,
489
+ custom_schema=background_layers,
490
+ )
491
+
492
+ self.background_texture.preprocess()
493
+ self.background_texture.process()
494
+
495
+ processed_layers = self.background_texture.get_background_layers()
496
+ weights_directory = self.game.weights_dir_path(self.map_directory)
497
+ background_paths = [layer.path(weights_directory) for layer in processed_layers]
498
+ self.logger.debug("Found %s background textures.", len(background_paths))
499
+
500
+ if not background_paths:
501
+ self.logger.warning("No background textures found.")
502
+ return
503
+
504
+ # Merge all images into one.
505
+ background_image = np.zeros((self.background_size, self.background_size), dtype=np.uint8)
506
+ for path in background_paths:
507
+ layer = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
508
+ background_image = cv2.add(background_image, layer) # type: ignore
509
+
510
+ background_save_path = os.path.join(self.water_directory, "water_resources.png")
511
+ cv2.imwrite(background_save_path, background_image)
512
+ self.logger.info("Background texture saved: %s", background_save_path)
513
+ self.water_resources_path = background_save_path # pylint: disable=W0201
514
+
515
+ def subtraction(self) -> None:
516
+ """Subtracts the water depth from the DEM data where the water resources are located."""
517
+ if not self.water_resources_path:
518
+ self.logger.warning("Water resources texture not found.")
519
+ return
520
+
521
+ # Single channeled 8 bit image, where the water have values of 255, and the rest 0.
522
+ water_resources_image = cv2.imread(self.water_resources_path, cv2.IMREAD_UNCHANGED)
523
+ mask = water_resources_image == 255
524
+
525
+ # Make mask a little bit smaller (1 pixel).
526
+ mask = cv2.erode(mask.astype(np.uint8), np.ones((3, 3), np.uint8), iterations=1).astype(
527
+ bool
528
+ )
529
+
530
+ for output_path in self.output_paths:
531
+ if FULL_PREVIEW_NAME in output_path:
532
+ continue
533
+ dem_image = cv2.imread(output_path, cv2.IMREAD_UNCHANGED)
534
+
535
+ # Create a mask where water_resources_image is 255 (or not 0)
536
+ # Subtract water_depth from dem_image where mask is True
537
+ dem_image[mask] = dem_image[mask] - self.water_depth
538
+
539
+ # Save the modified dem_image back to the output path
540
+ cv2.imwrite(output_path, dem_image)
541
+ self.logger.debug("Water depth subtracted from DEM data: %s", output_path)
542
+
543
+ def generate_water_resources_obj(self) -> None:
544
+ """Generates 3D obj files based on water resources data."""
545
+ if not self.water_resources_path:
546
+ self.logger.warning("Water resources texture not found.")
547
+ return
548
+
549
+ # Single channeled 8 bit image, where the water have values of 255, and the rest 0.
550
+ plane_water = cv2.imread(self.water_resources_path, cv2.IMREAD_UNCHANGED)
551
+ dilated_plane_water = cv2.dilate(
552
+ plane_water.astype(np.uint8), np.ones((5, 5), np.uint8), iterations=5
553
+ ).astype(np.uint8)
554
+ plane_save_path = os.path.join(self.water_directory, "plane_water.obj")
555
+ self.plane_from_np(
556
+ dilated_plane_water, plane_save_path, is_preview=False, include_zeros=False
557
+ )
558
+
559
+ # Single channeled 16 bit DEM image of terrain.
560
+ background_dem = cv2.imread(self.not_substracted_path, cv2.IMREAD_UNCHANGED)
561
+
562
+ # Remove all the values from the background dem where the plane_water is 0.
563
+ background_dem[plane_water == 0] = 0
564
+
565
+ # Dilate the background dem to make the water more smooth.
566
+ elevated_water = cv2.dilate(background_dem, np.ones((3, 3), np.uint16), iterations=10)
567
+
568
+ # Use the background dem as a mask to prevent the original values from being overwritten.
569
+ mask = background_dem > 0
570
+
571
+ # Combine the dilated background dem with non-dilated background dem.
572
+ elevated_water = np.where(mask, background_dem, elevated_water)
573
+ elevated_save_path = os.path.join(self.water_directory, "elevated_water.obj")
574
+
575
+ self.plane_from_np(
576
+ elevated_water, elevated_save_path, is_preview=False, include_zeros=False
577
+ )
@@ -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
@@ -326,6 +330,7 @@ class Component:
326
330
 
327
331
  return cs_x, cs_y
328
332
 
333
+ # pylint: disable=R0914
329
334
  def fit_polygon_into_bounds(
330
335
  self, polygon_points: list[tuple[int, int]], margin: int = 0, angle: int = 0
331
336
  ) -> list[tuple[int, int]]:
@@ -367,8 +372,13 @@ class Component:
367
372
  bounds = box(min_x, min_y, max_x, max_y)
368
373
 
369
374
  # Intersect the polygon with the bounds to fit it within the map
370
- fitted_polygon = polygon.intersection(bounds)
371
- self.logger.debug("Fitted the polygon into the bounds: %s", bounds)
375
+ try:
376
+ fitted_polygon = polygon.intersection(bounds)
377
+ self.logger.debug("Fitted the polygon into the bounds: %s", bounds)
378
+ except Exception as e:
379
+ raise ValueError( # pylint: disable=W0707
380
+ f"Could not fit the polygon into the bounds: {e}"
381
+ )
372
382
 
373
383
  if not isinstance(fitted_polygon, Polygon):
374
384
  raise ValueError("The fitted polygon is not a valid polygon.")
@@ -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.