maps4fs 1.0.8__tar.gz → 1.0.9__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 (30) hide show
  1. {maps4fs-1.0.8 → maps4fs-1.0.9}/PKG-INFO +10 -3
  2. {maps4fs-1.0.8 → maps4fs-1.0.9}/README.md +9 -2
  3. {maps4fs-1.0.8 → maps4fs-1.0.9}/maps4fs/generator/component.py +96 -1
  4. maps4fs-1.0.9/maps4fs/generator/grle.py +154 -0
  5. {maps4fs-1.0.8 → maps4fs-1.0.9}/maps4fs/generator/i3d.py +4 -82
  6. {maps4fs-1.0.8 → maps4fs-1.0.9}/maps4fs.egg-info/PKG-INFO +10 -3
  7. {maps4fs-1.0.8 → maps4fs-1.0.9}/pyproject.toml +1 -1
  8. maps4fs-1.0.8/maps4fs/generator/grle.py +0 -74
  9. {maps4fs-1.0.8 → maps4fs-1.0.9}/LICENSE.md +0 -0
  10. {maps4fs-1.0.8 → maps4fs-1.0.9}/maps4fs/__init__.py +0 -0
  11. {maps4fs-1.0.8 → maps4fs-1.0.9}/maps4fs/generator/__init__.py +0 -0
  12. {maps4fs-1.0.8 → maps4fs-1.0.9}/maps4fs/generator/background.py +0 -0
  13. {maps4fs-1.0.8 → maps4fs-1.0.9}/maps4fs/generator/config.py +0 -0
  14. {maps4fs-1.0.8 → maps4fs-1.0.9}/maps4fs/generator/dem.py +0 -0
  15. {maps4fs-1.0.8 → maps4fs-1.0.9}/maps4fs/generator/game.py +0 -0
  16. {maps4fs-1.0.8 → maps4fs-1.0.9}/maps4fs/generator/map.py +0 -0
  17. {maps4fs-1.0.8 → maps4fs-1.0.9}/maps4fs/generator/path_steps.py +0 -0
  18. {maps4fs-1.0.8 → maps4fs-1.0.9}/maps4fs/generator/qgis.py +0 -0
  19. {maps4fs-1.0.8 → maps4fs-1.0.9}/maps4fs/generator/texture.py +0 -0
  20. {maps4fs-1.0.8 → maps4fs-1.0.9}/maps4fs/generator/tile.py +0 -0
  21. {maps4fs-1.0.8 → maps4fs-1.0.9}/maps4fs/logger.py +0 -0
  22. {maps4fs-1.0.8 → maps4fs-1.0.9}/maps4fs/toolbox/__init__.py +0 -0
  23. {maps4fs-1.0.8 → maps4fs-1.0.9}/maps4fs/toolbox/background.py +0 -0
  24. {maps4fs-1.0.8 → maps4fs-1.0.9}/maps4fs/toolbox/dem.py +0 -0
  25. {maps4fs-1.0.8 → maps4fs-1.0.9}/maps4fs.egg-info/SOURCES.txt +0 -0
  26. {maps4fs-1.0.8 → maps4fs-1.0.9}/maps4fs.egg-info/dependency_links.txt +0 -0
  27. {maps4fs-1.0.8 → maps4fs-1.0.9}/maps4fs.egg-info/requires.txt +0 -0
  28. {maps4fs-1.0.8 → maps4fs-1.0.9}/maps4fs.egg-info/top_level.txt +0 -0
  29. {maps4fs-1.0.8 → maps4fs-1.0.9}/setup.cfg +0 -0
  30. {maps4fs-1.0.8 → maps4fs-1.0.9}/tests/test_generator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: maps4fs
3
- Version: 1.0.8
3
+ Version: 1.0.9
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,8 +69,9 @@ Requires-Dist: pympler
69
69
  🚜 Supports Farming Simulator 22 and 25<br>
70
70
  🔷 Generates *.obj files for background terrain based on the real-world height map<br>
71
71
  📄 Generates scripts to download high-resolution satellite images from [QGIS](https://qgis.org/download/) in one click<br>
72
- 🧰 Modder Toolbox to help you with various tasks 🆕<br>
72
+ 🧰 Modder Toolbox to help you with various tasks <br>
73
73
  🌾 Automatically generates fields 🆕<br>
74
+ 🌽 Automatically generates farmlands 🆕<br>
74
75
 
75
76
  <p align="center">
76
77
  <img src="https://github.com/user-attachments/assets/cf8f5752-9c69-4018-bead-290f59ba6976"><br>
@@ -81,13 +82,15 @@ Requires-Dist: pympler
81
82
  🗒️ True-to-life blueprints for fast and precise modding.<br><br>
82
83
  <img width="480" src="https://github.com/user-attachments/assets/1a8802d2-6a3b-4bfa-af2b-7c09478e199b"><br>
83
84
  🌾 Field generation with one click.<br><br>
85
+ <img width="480" src="https://github.com/user-attachments/assets/4d1fa879-5d60-438b-a84e-16883bcef0ec"><br>
86
+ 🌽 Automatic farmlands generation based on the fields.<br><br>
84
87
  <img src="https://github.com/user-attachments/assets/cce45575-c917-4a1b-bdc0-6368e32ccdff"><br>
85
88
  📏 Almost any possible map sizes.
86
89
  </p>
87
90
 
88
91
  📹 A complete step-by-step video tutorial is here!
89
92
  <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>
90
- <i>How to Generate a Map for Farming Simulator 25 and 22 from a real place using maps4FS</i>
93
+ <p align="center"><i>How to Generate a Map for Farming Simulator 25 and 22 from a real place using maps4FS.</i></p>
91
94
 
92
95
  ## Quick Start
93
96
  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>
@@ -472,6 +475,10 @@ You can also apply some advanced settings to the map generation process. Note th
472
475
 
473
476
  - 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.
474
477
 
478
+ ### Farmlands Advanced settings
479
+
480
+ - 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.
481
+
475
482
  ## Resources
476
483
  In this section, you'll find a list of the resources that you need to create a map for the Farming Simulator.<br>
477
484
  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>
@@ -44,8 +44,9 @@
44
44
  🚜 Supports Farming Simulator 22 and 25<br>
45
45
  🔷 Generates *.obj files for background terrain based on the real-world height map<br>
46
46
  📄 Generates scripts to download high-resolution satellite images from [QGIS](https://qgis.org/download/) in one click<br>
47
- 🧰 Modder Toolbox to help you with various tasks 🆕<br>
47
+ 🧰 Modder Toolbox to help you with various tasks <br>
48
48
  🌾 Automatically generates fields 🆕<br>
49
+ 🌽 Automatically generates farmlands 🆕<br>
49
50
 
50
51
  <p align="center">
51
52
  <img src="https://github.com/user-attachments/assets/cf8f5752-9c69-4018-bead-290f59ba6976"><br>
@@ -56,13 +57,15 @@
56
57
  🗒️ True-to-life blueprints for fast and precise modding.<br><br>
57
58
  <img width="480" src="https://github.com/user-attachments/assets/1a8802d2-6a3b-4bfa-af2b-7c09478e199b"><br>
58
59
  🌾 Field generation with one click.<br><br>
60
+ <img width="480" src="https://github.com/user-attachments/assets/4d1fa879-5d60-438b-a84e-16883bcef0ec"><br>
61
+ 🌽 Automatic farmlands generation based on the fields.<br><br>
59
62
  <img src="https://github.com/user-attachments/assets/cce45575-c917-4a1b-bdc0-6368e32ccdff"><br>
60
63
  📏 Almost any possible map sizes.
61
64
  </p>
62
65
 
63
66
  📹 A complete step-by-step video tutorial is here!
64
67
  <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>
65
- <i>How to Generate a Map for Farming Simulator 25 and 22 from a real place using maps4FS</i>
68
+ <p align="center"><i>How to Generate a Map for Farming Simulator 25 and 22 from a real place using maps4FS.</i></p>
66
69
 
67
70
  ## Quick Start
68
71
  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>
@@ -447,6 +450,10 @@ You can also apply some advanced settings to the map generation process. Note th
447
450
 
448
451
  - 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.
449
452
 
453
+ ### Farmlands Advanced settings
454
+
455
+ - 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.
456
+
450
457
  ## Resources
451
458
  In this section, you'll find a list of the resources that you need to create a map for the Farming Simulator.<br>
452
459
  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>
@@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Any
9
9
 
10
10
  import osmnx as ox # type: ignore
11
11
  from pyproj import Transformer
12
+ from shapely.geometry import Polygon, box # type: ignore
12
13
 
13
14
  from maps4fs.generator.qgis import save_scripts
14
15
 
@@ -16,7 +17,7 @@ if TYPE_CHECKING:
16
17
  from maps4fs.generator.game import Game
17
18
 
18
19
 
19
- # pylint: disable=R0801, R0903, R0902
20
+ # pylint: disable=R0801, R0903, R0902, R0904
20
21
  class Component:
21
22
  """Base class for all map generation components.
22
23
 
@@ -281,3 +282,97 @@ class Component:
281
282
  """
282
283
  class_name = self.__class__.__name__.lower()
283
284
  save_scripts(qgis_layers, class_name, self.scripts_directory)
285
+
286
+ def get_polygon_center(self, polygon_points: list[tuple[int, int]]) -> tuple[int, int]:
287
+ """Calculates the center of a polygon defined by a list of points.
288
+
289
+ Arguments:
290
+ polygon_points (list[tuple[int, int]]): The points of the polygon.
291
+
292
+ Returns:
293
+ tuple[int, int]: The center of the polygon.
294
+ """
295
+ polygon = Polygon(polygon_points)
296
+ center = polygon.centroid
297
+ return int(center.x), int(center.y)
298
+
299
+ def absolute_to_relative(
300
+ self, point: tuple[int, int], center: tuple[int, int]
301
+ ) -> tuple[int, int]:
302
+ """Converts a pair of absolute coordinates to relative coordinates.
303
+
304
+ Arguments:
305
+ point (tuple[int, int]): The absolute coordinates.
306
+ center (tuple[int, int]): The center coordinates.
307
+
308
+ Returns:
309
+ tuple[int, int]: The relative coordinates.
310
+ """
311
+ cx, cy = center
312
+ x, y = point
313
+ return x - cx, y - cy
314
+
315
+ def top_left_coordinates_to_center(self, top_left: tuple[int, int]) -> tuple[int, int]:
316
+ """Converts a pair of coordinates from the top-left system to the center system.
317
+ In top-left system, the origin (0, 0) is in the top-left corner of the map, while in the
318
+ center system, the origin is in the center of the map.
319
+
320
+ Arguments:
321
+ top_left (tuple[int, int]): The coordinates in the top-left system.
322
+
323
+ Returns:
324
+ tuple[int, int]: The coordinates in the center system.
325
+ """
326
+ x, y = top_left
327
+ cs_x = x - self.map_width // 2
328
+ cs_y = y - self.map_height // 2
329
+
330
+ return cs_x, cs_y
331
+
332
+ def fit_polygon_into_bounds(
333
+ self, polygon_points: list[tuple[int, int]], margin: int = 0
334
+ ) -> list[tuple[int, int]]:
335
+ """Fits a polygon into the bounds of the map.
336
+
337
+ Arguments:
338
+ polygon_points (list[tuple[int, int]]): The points of the polygon.
339
+ margin (int, optional): The margin to add to the polygon. Defaults to 0.
340
+
341
+ Returns:
342
+ list[tuple[int, int]]: The points of the polygon fitted into the map bounds.
343
+ """
344
+ min_x = min_y = 0
345
+ max_x, max_y = self.map_width, self.map_height
346
+
347
+ # Create a polygon from the given points
348
+ polygon = Polygon(polygon_points)
349
+
350
+ if margin:
351
+ polygon = polygon.buffer(margin, join_style="mitre")
352
+
353
+ # Create a bounding box for the map bounds
354
+ bounds = box(min_x, min_y, max_x, max_y)
355
+
356
+ # Intersect the polygon with the bounds to fit it within the map
357
+ fitted_polygon = polygon.intersection(bounds)
358
+
359
+ if not isinstance(fitted_polygon, Polygon):
360
+ raise ValueError("The fitted polygon is not a valid polygon.")
361
+
362
+ # Return the fitted polygon points
363
+ return list(fitted_polygon.exterior.coords)
364
+
365
+ def get_infolayer_path(self, layer_name: str) -> str | None:
366
+ """Returns the path to the info layer file.
367
+
368
+ Arguments:
369
+ layer_name (str): The name of the layer.
370
+
371
+ Returns:
372
+ str | None: The path to the info layer file or None if the layer does not exist.
373
+ """
374
+ info_layer_path = os.path.join(self.info_layers_directory, f"{layer_name}.json")
375
+ if not os.path.isfile(info_layer_path):
376
+ self.logger.warning("Info layer %s does not exist", info_layer_path)
377
+ return None
378
+ return info_layer_path
@@ -0,0 +1,154 @@
1
+ """This module contains the GRLE class for generating InfoLayer PNG files based on GRLE schema."""
2
+
3
+ import json
4
+ import os
5
+ from xml.etree import ElementTree as ET
6
+
7
+ import cv2
8
+ import numpy as np
9
+
10
+ from maps4fs.generator.component import Component
11
+
12
+
13
+ # pylint: disable=W0223
14
+ class GRLE(Component):
15
+ """Component for to generate InfoLayer PNG files based on GRLE schema.
16
+
17
+ Arguments:
18
+ coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
19
+ map_height (int): The height of the map in pixels.
20
+ map_width (int): The width of the map in pixels.
21
+ map_directory (str): The directory where the map files are stored.
22
+ logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
23
+ info, warning. If not provided, default logging will be used.
24
+ """
25
+
26
+ _grle_schema: dict[str, float | int | str] | None = None
27
+
28
+ def preprocess(self) -> None:
29
+ """Gets the path to the map I3D file from the game instance and saves it to the instance
30
+ attribute. If the game does not support I3D files, the attribute is set to None."""
31
+
32
+ self.farmland_margin = self.kwargs.get("farmland_margin", 0)
33
+
34
+ try:
35
+ grle_schema_path = self.game.grle_schema
36
+ except ValueError:
37
+ self.logger.info("GRLE schema processing is not implemented for this game.")
38
+ return
39
+
40
+ try:
41
+ with open(grle_schema_path, "r", encoding="utf-8") as file:
42
+ self._grle_schema = json.load(file)
43
+ self.logger.debug("GRLE schema loaded from: %s.", grle_schema_path)
44
+ except (json.JSONDecodeError, FileNotFoundError) as error:
45
+ self.logger.error("Error loading GRLE schema from %s: %s.", grle_schema_path, error)
46
+ self._grle_schema = None
47
+
48
+ def process(self) -> None:
49
+ """Generates InfoLayer PNG files based on the GRLE schema."""
50
+ if not self._grle_schema:
51
+ self.logger.info("GRLE schema is not obtained, skipping the processing.")
52
+ return
53
+
54
+ for info_layer in self._grle_schema:
55
+ if isinstance(info_layer, dict):
56
+ file_path = os.path.join(
57
+ self.game.weights_dir_path(self.map_directory), info_layer["name"]
58
+ )
59
+
60
+ height = int(self.map_height * info_layer["height_multiplier"])
61
+ width = int(self.map_width * info_layer["width_multiplier"])
62
+ channels = info_layer["channels"]
63
+ data_type = info_layer["data_type"]
64
+
65
+ # Create the InfoLayer PNG file with zeros.
66
+ if channels == 1:
67
+ info_layer_data = np.zeros((height, width), dtype=data_type)
68
+ else:
69
+ info_layer_data = np.zeros((height, width, channels), dtype=data_type)
70
+ self.logger.debug("Shape of %s: %s.", info_layer["name"], info_layer_data.shape)
71
+ cv2.imwrite(file_path, info_layer_data) # pylint: disable=no-member
72
+ self.logger.debug("InfoLayer PNG file %s created.", file_path)
73
+ else:
74
+ self.logger.warning("Invalid InfoLayer schema: %s.", info_layer)
75
+
76
+ self._add_farmlands()
77
+
78
+ def previews(self) -> list[str]:
79
+ """Returns a list of paths to the preview images (empty list).
80
+ The component does not generate any preview images so it returns an empty list.
81
+
82
+ Returns:
83
+ list[str]: An empty list.
84
+ """
85
+ return []
86
+
87
+ # pylint: disable=R0801, R0914
88
+ def _add_farmlands(self) -> None:
89
+ """Adds farmlands to the InfoLayer PNG file."""
90
+
91
+ textures_info_layer_path = self.get_infolayer_path("textures")
92
+ if not textures_info_layer_path:
93
+ return
94
+
95
+ with open(textures_info_layer_path, "r", encoding="utf-8") as textures_info_layer_file:
96
+ textures_info_layer = json.load(textures_info_layer_file)
97
+
98
+ fields: list[list[tuple[int, int]]] | None = textures_info_layer.get("fields")
99
+ if not fields:
100
+ self.logger.warning("Fields data not found in textures info layer.")
101
+ return
102
+
103
+ self.logger.info("Found %s fields in textures info layer.", len(fields))
104
+
105
+ info_layer_farmlands_path = os.path.join(
106
+ self.game.weights_dir_path(self.map_directory), "infoLayer_farmlands.png"
107
+ )
108
+
109
+ if not os.path.isfile(info_layer_farmlands_path):
110
+ self.logger.warning("InfoLayer PNG file %s not found.", info_layer_farmlands_path)
111
+ return
112
+
113
+ # pylint: disable=no-member
114
+ image = cv2.imread(info_layer_farmlands_path, cv2.IMREAD_UNCHANGED)
115
+ farmlands_xml_path = os.path.join(self.map_directory, "map/config/farmlands.xml")
116
+ if not os.path.isfile(farmlands_xml_path):
117
+ self.logger.warning("Farmlands XML file %s not found.", farmlands_xml_path)
118
+ return
119
+
120
+ tree = ET.parse(farmlands_xml_path)
121
+ farmlands_xml = tree.find("farmlands")
122
+
123
+ for field_id, field in enumerate(fields, start=1):
124
+ try:
125
+ fitted_field = self.fit_polygon_into_bounds(field, self.farmland_margin)
126
+ except ValueError as e:
127
+ self.logger.warning("Field %s could not be fitted into the map bounds.", field_id)
128
+ self.logger.debug("Error: %s", e)
129
+ continue
130
+
131
+ field_np = np.array(fitted_field, np.int32)
132
+ field_np = field_np.reshape((-1, 1, 2))
133
+
134
+ # Infolayer image is 1/2 of the size of the map image, that's why we need to divide
135
+ # the coordinates by 2.
136
+ field_np = field_np // 2
137
+
138
+ # pylint: disable=no-member
139
+ cv2.fillPoly(image, [field_np], field_id) # type: ignore
140
+
141
+ # Add the field to the farmlands XML.
142
+ farmland = ET.SubElement(farmlands_xml, "farmland") # type: ignore
143
+ farmland.set("id", str(field_id))
144
+ farmland.set("priceScale", "1")
145
+ farmland.set("npcName", "FORESTER")
146
+
147
+ tree.write(farmlands_xml_path)
148
+
149
+ self.logger.info("Farmlands added to the farmlands XML file: %s.", farmlands_xml_path)
150
+
151
+ cv2.imwrite(info_layer_farmlands_path, image) # pylint: disable=no-member
152
+ self.logger.info(
153
+ "Farmlands added to the InfoLayer PNG file: %s.", info_layer_farmlands_path
154
+ )
@@ -6,8 +6,6 @@ import json
6
6
  import os
7
7
  from xml.etree import ElementTree as ET
8
8
 
9
- from shapely.geometry import Polygon, box # type: ignore
10
-
11
9
  from maps4fs.generator.component import Component
12
10
 
13
11
  DEFAULT_HEIGHT_SCALE = 2000
@@ -103,15 +101,14 @@ class I3d(Component):
103
101
  if tree is None:
104
102
  return
105
103
 
106
- textures_info_layer_path = os.path.join(self.info_layers_directory, "textures.json")
107
- if not os.path.isfile(textures_info_layer_path):
108
- self.logger.warning("Textures info layer not found: %s.", textures_info_layer_path)
104
+ textures_info_layer_path = self.get_infolayer_path("textures")
105
+ if not textures_info_layer_path:
109
106
  return
110
107
 
111
108
  with open(textures_info_layer_path, "r", encoding="utf-8") as textures_info_layer_file:
112
109
  textures_info_layer = json.load(textures_info_layer_file)
113
110
 
114
- fields: list[tuple[int, int]] | None = textures_info_layer.get("fields")
111
+ fields: list[list[tuple[int, int]]] | None = textures_info_layer.get("fields")
115
112
  if not fields:
116
113
  self.logger.warning("Fields data not found in textures info layer.")
117
114
  return
@@ -130,7 +127,7 @@ class I3d(Component):
130
127
  for field_id, field in enumerate(fields, start=1):
131
128
  # Convert the top-left coordinates to the center coordinates system.
132
129
  try:
133
- fitted_field = self.fit_polygon_into_bounds(field) # type: ignore
130
+ fitted_field = self.fit_polygon_into_bounds(field)
134
131
  except ValueError as e:
135
132
  self.logger.warning(
136
133
  "Field %s could not be fitted into the map bounds.", field_id
@@ -233,81 +230,6 @@ class I3d(Component):
233
230
 
234
231
  return teleport_indicator_node, node_id
235
232
 
236
- def get_polygon_center(self, polygon_points: list[tuple[int, int]]) -> tuple[int, int]:
237
- """Calculates the center of a polygon defined by a list of points.
238
-
239
- Arguments:
240
- polygon_points (list[tuple[int, int]]): The points of the polygon.
241
-
242
- Returns:
243
- tuple[int, int]: The center of the polygon.
244
- """
245
- polygon = Polygon(polygon_points)
246
- center = polygon.centroid
247
- return int(center.x), int(center.y)
248
-
249
- def absolute_to_relative(
250
- self, point: tuple[int, int], center: tuple[int, int]
251
- ) -> tuple[int, int]:
252
- """Converts a pair of absolute coordinates to relative coordinates.
253
-
254
- Arguments:
255
- point (tuple[int, int]): The absolute coordinates.
256
- center (tuple[int, int]): The center coordinates.
257
-
258
- Returns:
259
- tuple[int, int]: The relative coordinates.
260
- """
261
- cx, cy = center
262
- x, y = point
263
- return x - cx, y - cy
264
-
265
- def top_left_coordinates_to_center(self, top_left: tuple[int, int]) -> tuple[int, int]:
266
- """Converts a pair of coordinates from the top-left system to the center system.
267
- In top-left system, the origin (0, 0) is in the top-left corner of the map, while in the
268
- center system, the origin is in the center of the map.
269
-
270
- Arguments:
271
- top_left (tuple[int, int]): The coordinates in the top-left system.
272
-
273
- Returns:
274
- tuple[int, int]: The coordinates in the center system.
275
- """
276
- x, y = top_left
277
- cs_x = x - self.map_width // 2
278
- cs_y = y - self.map_height // 2
279
-
280
- return cs_x, cs_y
281
-
282
- def fit_polygon_into_bounds(
283
- self, polygon_points: list[tuple[int, int]]
284
- ) -> list[tuple[int, int]]:
285
- """Fits a polygon into the bounds of the map.
286
-
287
- Arguments:
288
- polygon_points (list[tuple[int, int]]): The points of the polygon.
289
-
290
- Returns:
291
- list[tuple[int, int]]: The points of the polygon fitted into the map bounds.
292
- """
293
- min_x = min_y = 0
294
- max_x, max_y = self.map_width, self.map_height
295
-
296
- # Create a polygon from the given points
297
- polygon = Polygon(polygon_points)
298
-
299
- # Create a bounding box for the map bounds
300
- bounds = box(min_x, min_y, max_x, max_y)
301
-
302
- # Intersect the polygon with the bounds to fit it within the map
303
- fitted_polygon = polygon.intersection(bounds)
304
-
305
- if not isinstance(fitted_polygon, Polygon):
306
- raise ValueError("The fitted polygon is not a valid polygon.")
307
-
308
- # Return the fitted polygon points
309
- return list(fitted_polygon.exterior.coords)
310
-
311
233
  @staticmethod
312
234
  def create_user_attribute_node(node_id: int) -> ET.Element:
313
235
  """Creates an XML user attribute node with given node ID.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: maps4fs
3
- Version: 1.0.8
3
+ Version: 1.0.9
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,8 +69,9 @@ Requires-Dist: pympler
69
69
  🚜 Supports Farming Simulator 22 and 25<br>
70
70
  🔷 Generates *.obj files for background terrain based on the real-world height map<br>
71
71
  📄 Generates scripts to download high-resolution satellite images from [QGIS](https://qgis.org/download/) in one click<br>
72
- 🧰 Modder Toolbox to help you with various tasks 🆕<br>
72
+ 🧰 Modder Toolbox to help you with various tasks <br>
73
73
  🌾 Automatically generates fields 🆕<br>
74
+ 🌽 Automatically generates farmlands 🆕<br>
74
75
 
75
76
  <p align="center">
76
77
  <img src="https://github.com/user-attachments/assets/cf8f5752-9c69-4018-bead-290f59ba6976"><br>
@@ -81,13 +82,15 @@ Requires-Dist: pympler
81
82
  🗒️ True-to-life blueprints for fast and precise modding.<br><br>
82
83
  <img width="480" src="https://github.com/user-attachments/assets/1a8802d2-6a3b-4bfa-af2b-7c09478e199b"><br>
83
84
  🌾 Field generation with one click.<br><br>
85
+ <img width="480" src="https://github.com/user-attachments/assets/4d1fa879-5d60-438b-a84e-16883bcef0ec"><br>
86
+ 🌽 Automatic farmlands generation based on the fields.<br><br>
84
87
  <img src="https://github.com/user-attachments/assets/cce45575-c917-4a1b-bdc0-6368e32ccdff"><br>
85
88
  📏 Almost any possible map sizes.
86
89
  </p>
87
90
 
88
91
  📹 A complete step-by-step video tutorial is here!
89
92
  <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>
90
- <i>How to Generate a Map for Farming Simulator 25 and 22 from a real place using maps4FS</i>
93
+ <p align="center"><i>How to Generate a Map for Farming Simulator 25 and 22 from a real place using maps4FS.</i></p>
91
94
 
92
95
  ## Quick Start
93
96
  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>
@@ -472,6 +475,10 @@ You can also apply some advanced settings to the map generation process. Note th
472
475
 
473
476
  - 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.
474
477
 
478
+ ### Farmlands Advanced settings
479
+
480
+ - 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.
481
+
475
482
  ## Resources
476
483
  In this section, you'll find a list of the resources that you need to create a map for the Farming Simulator.<br>
477
484
  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>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "maps4fs"
7
- version = "1.0.8"
7
+ version = "1.0.9"
8
8
  description = "Generate map templates for Farming Simulator from real places."
9
9
  authors = [{name = "iwatkot", email = "iwatkot@gmail.com"}]
10
10
  license = {text = "MIT License"}
@@ -1,74 +0,0 @@
1
- """This module contains the GRLE class for generating InfoLayer PNG files based on GRLE schema."""
2
-
3
- import json
4
- import os
5
-
6
- import cv2
7
- import numpy as np
8
-
9
- from maps4fs.generator.component import Component
10
-
11
-
12
- # pylint: disable=W0223
13
- class GRLE(Component):
14
- """Component for to generate InfoLayer PNG files based on GRLE schema.
15
-
16
- Arguments:
17
- coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
18
- map_height (int): The height of the map in pixels.
19
- map_width (int): The width of the map in pixels.
20
- map_directory (str): The directory where the map files are stored.
21
- logger (Any, optional): The logger to use. Must have at least three basic methods: debug,
22
- info, warning. If not provided, default logging will be used.
23
- """
24
-
25
- _grle_schema: dict[str, float | int | str] | None = None
26
-
27
- def preprocess(self) -> None:
28
- """Gets the path to the map I3D file from the game instance and saves it to the instance
29
- attribute. If the game does not support I3D files, the attribute is set to None."""
30
- try:
31
- grle_schema_path = self.game.grle_schema
32
- except ValueError:
33
- self.logger.info("GRLE schema processing is not implemented for this game.")
34
- return
35
-
36
- try:
37
- with open(grle_schema_path, "r", encoding="utf-8") as file:
38
- self._grle_schema = json.load(file)
39
- self.logger.debug("GRLE schema loaded from: %s.", grle_schema_path)
40
- except (json.JSONDecodeError, FileNotFoundError) as error:
41
- self.logger.error("Error loading GRLE schema from %s: %s.", grle_schema_path, error)
42
- self._grle_schema = None
43
-
44
- def process(self) -> None:
45
- """Generates InfoLayer PNG files based on the GRLE schema."""
46
- if not self._grle_schema:
47
- self.logger.info("GRLE schema is not obtained, skipping the processing.")
48
- return
49
-
50
- for info_layer in self._grle_schema:
51
- if isinstance(info_layer, dict):
52
- file_path = os.path.join(
53
- self.game.weights_dir_path(self.map_directory), info_layer["name"]
54
- )
55
-
56
- height = int(self.map_height * info_layer["height_multiplier"])
57
- width = int(self.map_width * info_layer["width_multiplier"])
58
- data_type = info_layer["data_type"]
59
-
60
- # Create the InfoLayer PNG file with zeros.
61
- info_layer_data = np.zeros((height, width), dtype=data_type)
62
- cv2.imwrite(file_path, info_layer_data) # pylint: disable=no-member
63
- self.logger.debug("InfoLayer PNG file %s created.", file_path)
64
- else:
65
- self.logger.warning("Invalid InfoLayer schema: %s.", info_layer)
66
-
67
- def previews(self) -> list[str]:
68
- """Returns a list of paths to the preview images (empty list).
69
- The component does not generate any preview images so it returns an empty list.
70
-
71
- Returns:
72
- list[str]: An empty list.
73
- """
74
- return []
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes