maps4fs 1.0.7__py3-none-any.whl → 1.0.9__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.
Potentially problematic release.
This version of maps4fs might be problematic. Click here for more details.
- maps4fs/generator/background.py +27 -6
- maps4fs/generator/component.py +96 -1
- maps4fs/generator/grle.py +81 -1
- maps4fs/generator/i3d.py +16 -98
- maps4fs/generator/texture.py +34 -7
- {maps4fs-1.0.7.dist-info → maps4fs-1.0.9.dist-info}/METADATA +21 -8
- {maps4fs-1.0.7.dist-info → maps4fs-1.0.9.dist-info}/RECORD +10 -10
- {maps4fs-1.0.7.dist-info → maps4fs-1.0.9.dist-info}/LICENSE.md +0 -0
- {maps4fs-1.0.7.dist-info → maps4fs-1.0.9.dist-info}/WHEEL +0 -0
- {maps4fs-1.0.7.dist-info → maps4fs-1.0.9.dist-info}/top_level.txt +0 -0
maps4fs/generator/background.py
CHANGED
@@ -184,6 +184,7 @@ class Background(Component):
|
|
184
184
|
dem_data = cv2.imread(tile.dem_path, cv2.IMREAD_UNCHANGED) # pylint: disable=no-member
|
185
185
|
self.plane_from_np(tile.code, dem_data, save_path) # type: ignore
|
186
186
|
|
187
|
+
# pylint: disable=too-many-locals
|
187
188
|
def cutout(self, dem_path: str) -> str:
|
188
189
|
"""Cuts out the center of the DEM (the actual map) and saves it as a separate file.
|
189
190
|
|
@@ -205,20 +206,40 @@ class Background(Component):
|
|
205
206
|
|
206
207
|
output_size = self.map_height + 1
|
207
208
|
|
208
|
-
# pylint: disable=no-member
|
209
|
-
dem_data = cv2.resize(dem_data, (output_size, output_size), interpolation=cv2.INTER_LINEAR)
|
210
|
-
|
211
209
|
main_dem_path = self.game.dem_file_path(self.map_directory)
|
210
|
+
dem_directory = os.path.dirname(main_dem_path)
|
212
211
|
|
213
212
|
try:
|
214
213
|
os.remove(main_dem_path)
|
215
214
|
except FileNotFoundError:
|
216
215
|
pass
|
217
216
|
|
218
|
-
|
219
|
-
|
217
|
+
# pylint: disable=no-member
|
218
|
+
resized_dem_data = cv2.resize(
|
219
|
+
dem_data, (output_size, output_size), interpolation=cv2.INTER_LINEAR
|
220
|
+
)
|
221
|
+
|
222
|
+
# Giant Editor contains a bug for large maps, where the DEM should not match
|
223
|
+
# the UnitsPerPixel value. For example, for map 8192x8192, without bug
|
224
|
+
# the DEM image should be 8193x8193, but it does not work, so we need to
|
225
|
+
# resize the DEM to 4097x4097.
|
226
|
+
if self.map_height > 4096:
|
227
|
+
correct_dem_path = os.path.join(dem_directory, "correct_dem.png")
|
228
|
+
save_path = correct_dem_path
|
229
|
+
|
230
|
+
output_size = self.map_height // 2 + 1
|
231
|
+
bugged_dem_data = cv2.resize(
|
232
|
+
dem_data, (output_size, output_size), interpolation=cv2.INTER_LINEAR
|
233
|
+
)
|
234
|
+
# pylint: disable=no-member
|
235
|
+
cv2.imwrite(main_dem_path, bugged_dem_data)
|
236
|
+
else:
|
237
|
+
save_path = main_dem_path
|
238
|
+
|
239
|
+
cv2.imwrite(save_path, resized_dem_data) # pylint: disable=no-member
|
240
|
+
self.logger.info("DEM cutout saved: %s", save_path)
|
220
241
|
|
221
|
-
return
|
242
|
+
return save_path
|
222
243
|
|
223
244
|
# pylint: disable=too-many-locals
|
224
245
|
def plane_from_np(self, tile_code: str, dem_data: np.ndarray, save_path: str) -> None:
|
maps4fs/generator/component.py
CHANGED
@@ -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
|
maps4fs/generator/grle.py
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
import json
|
4
4
|
import os
|
5
|
+
from xml.etree import ElementTree as ET
|
5
6
|
|
6
7
|
import cv2
|
7
8
|
import numpy as np
|
@@ -27,6 +28,9 @@ class GRLE(Component):
|
|
27
28
|
def preprocess(self) -> None:
|
28
29
|
"""Gets the path to the map I3D file from the game instance and saves it to the instance
|
29
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
|
+
|
30
34
|
try:
|
31
35
|
grle_schema_path = self.game.grle_schema
|
32
36
|
except ValueError:
|
@@ -55,15 +59,22 @@ class GRLE(Component):
|
|
55
59
|
|
56
60
|
height = int(self.map_height * info_layer["height_multiplier"])
|
57
61
|
width = int(self.map_width * info_layer["width_multiplier"])
|
62
|
+
channels = info_layer["channels"]
|
58
63
|
data_type = info_layer["data_type"]
|
59
64
|
|
60
65
|
# Create the InfoLayer PNG file with zeros.
|
61
|
-
|
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)
|
62
71
|
cv2.imwrite(file_path, info_layer_data) # pylint: disable=no-member
|
63
72
|
self.logger.debug("InfoLayer PNG file %s created.", file_path)
|
64
73
|
else:
|
65
74
|
self.logger.warning("Invalid InfoLayer schema: %s.", info_layer)
|
66
75
|
|
76
|
+
self._add_farmlands()
|
77
|
+
|
67
78
|
def previews(self) -> list[str]:
|
68
79
|
"""Returns a list of paths to the preview images (empty list).
|
69
80
|
The component does not generate any preview images so it returns an empty list.
|
@@ -72,3 +83,72 @@ class GRLE(Component):
|
|
72
83
|
list[str]: An empty list.
|
73
84
|
"""
|
74
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
|
+
)
|
maps4fs/generator/i3d.py
CHANGED
@@ -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
|
@@ -34,6 +32,8 @@ class I3d(Component):
|
|
34
32
|
def preprocess(self) -> None:
|
35
33
|
"""Gets the path to the map I3D file from the game instance and saves it to the instance
|
36
34
|
attribute. If the game does not support I3D files, the attribute is set to None."""
|
35
|
+
self.auto_process = self.kwargs.get("auto_process", False)
|
36
|
+
|
37
37
|
try:
|
38
38
|
self._map_i3d_path = self.game.i3d_file_path(self.map_directory)
|
39
39
|
self.logger.debug("Map I3D path: %s.", self._map_i3d_path)
|
@@ -69,22 +69,16 @@ class I3d(Component):
|
|
69
69
|
root = tree.getroot()
|
70
70
|
for map_elem in root.iter("Scene"):
|
71
71
|
for terrain_elem in map_elem.iter("TerrainTransformGroup"):
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
terrain_elem.set("occMaxLODDistance", str(DEFAULT_MAX_LOD_OCCLUDER_DISTANCE))
|
84
|
-
self.logger.debug(
|
85
|
-
"occMaxLODDistance attribute set to %s in TerrainTransformGroup element.",
|
86
|
-
DEFAULT_MAX_LOD_OCCLUDER_DISTANCE,
|
87
|
-
)
|
72
|
+
if self.auto_process:
|
73
|
+
terrain_elem.set("heightScale", str(DEFAULT_HEIGHT_SCALE))
|
74
|
+
self.logger.debug(
|
75
|
+
"heightScale attribute set to %s in TerrainTransformGroup element.",
|
76
|
+
DEFAULT_HEIGHT_SCALE,
|
77
|
+
)
|
78
|
+
else:
|
79
|
+
self.logger.debug(
|
80
|
+
"Auto process is disabled, skipping the heightScale attribute update."
|
81
|
+
)
|
88
82
|
|
89
83
|
self.logger.debug("TerrainTransformGroup element updated in I3D file.")
|
90
84
|
|
@@ -107,15 +101,14 @@ class I3d(Component):
|
|
107
101
|
if tree is None:
|
108
102
|
return
|
109
103
|
|
110
|
-
textures_info_layer_path =
|
111
|
-
if not
|
112
|
-
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:
|
113
106
|
return
|
114
107
|
|
115
108
|
with open(textures_info_layer_path, "r", encoding="utf-8") as textures_info_layer_file:
|
116
109
|
textures_info_layer = json.load(textures_info_layer_file)
|
117
110
|
|
118
|
-
fields: list[tuple[int, int]] | None = textures_info_layer.get("fields")
|
111
|
+
fields: list[list[tuple[int, int]]] | None = textures_info_layer.get("fields")
|
119
112
|
if not fields:
|
120
113
|
self.logger.warning("Fields data not found in textures info layer.")
|
121
114
|
return
|
@@ -134,7 +127,7 @@ class I3d(Component):
|
|
134
127
|
for field_id, field in enumerate(fields, start=1):
|
135
128
|
# Convert the top-left coordinates to the center coordinates system.
|
136
129
|
try:
|
137
|
-
fitted_field = self.fit_polygon_into_bounds(field)
|
130
|
+
fitted_field = self.fit_polygon_into_bounds(field)
|
138
131
|
except ValueError as e:
|
139
132
|
self.logger.warning(
|
140
133
|
"Field %s could not be fitted into the map bounds.", field_id
|
@@ -237,81 +230,6 @@ class I3d(Component):
|
|
237
230
|
|
238
231
|
return teleport_indicator_node, node_id
|
239
232
|
|
240
|
-
def get_polygon_center(self, polygon_points: list[tuple[int, int]]) -> tuple[int, int]:
|
241
|
-
"""Calculates the center of a polygon defined by a list of points.
|
242
|
-
|
243
|
-
Arguments:
|
244
|
-
polygon_points (list[tuple[int, int]]): The points of the polygon.
|
245
|
-
|
246
|
-
Returns:
|
247
|
-
tuple[int, int]: The center of the polygon.
|
248
|
-
"""
|
249
|
-
polygon = Polygon(polygon_points)
|
250
|
-
center = polygon.centroid
|
251
|
-
return int(center.x), int(center.y)
|
252
|
-
|
253
|
-
def absolute_to_relative(
|
254
|
-
self, point: tuple[int, int], center: tuple[int, int]
|
255
|
-
) -> tuple[int, int]:
|
256
|
-
"""Converts a pair of absolute coordinates to relative coordinates.
|
257
|
-
|
258
|
-
Arguments:
|
259
|
-
point (tuple[int, int]): The absolute coordinates.
|
260
|
-
center (tuple[int, int]): The center coordinates.
|
261
|
-
|
262
|
-
Returns:
|
263
|
-
tuple[int, int]: The relative coordinates.
|
264
|
-
"""
|
265
|
-
cx, cy = center
|
266
|
-
x, y = point
|
267
|
-
return x - cx, y - cy
|
268
|
-
|
269
|
-
def top_left_coordinates_to_center(self, top_left: tuple[int, int]) -> tuple[int, int]:
|
270
|
-
"""Converts a pair of coordinates from the top-left system to the center system.
|
271
|
-
In top-left system, the origin (0, 0) is in the top-left corner of the map, while in the
|
272
|
-
center system, the origin is in the center of the map.
|
273
|
-
|
274
|
-
Arguments:
|
275
|
-
top_left (tuple[int, int]): The coordinates in the top-left system.
|
276
|
-
|
277
|
-
Returns:
|
278
|
-
tuple[int, int]: The coordinates in the center system.
|
279
|
-
"""
|
280
|
-
x, y = top_left
|
281
|
-
cs_x = x - self.map_width // 2
|
282
|
-
cs_y = y - self.map_height // 2
|
283
|
-
|
284
|
-
return cs_x, cs_y
|
285
|
-
|
286
|
-
def fit_polygon_into_bounds(
|
287
|
-
self, polygon_points: list[tuple[int, int]]
|
288
|
-
) -> list[tuple[int, int]]:
|
289
|
-
"""Fits a polygon into the bounds of the map.
|
290
|
-
|
291
|
-
Arguments:
|
292
|
-
polygon_points (list[tuple[int, int]]): The points of the polygon.
|
293
|
-
|
294
|
-
Returns:
|
295
|
-
list[tuple[int, int]]: The points of the polygon fitted into the map bounds.
|
296
|
-
"""
|
297
|
-
min_x = min_y = 0
|
298
|
-
max_x, max_y = self.map_width, self.map_height
|
299
|
-
|
300
|
-
# Create a polygon from the given points
|
301
|
-
polygon = Polygon(polygon_points)
|
302
|
-
|
303
|
-
# Create a bounding box for the map bounds
|
304
|
-
bounds = box(min_x, min_y, max_x, max_y)
|
305
|
-
|
306
|
-
# Intersect the polygon with the bounds to fit it within the map
|
307
|
-
fitted_polygon = polygon.intersection(bounds)
|
308
|
-
|
309
|
-
if not isinstance(fitted_polygon, Polygon):
|
310
|
-
raise ValueError("The fitted polygon is not a valid polygon.")
|
311
|
-
|
312
|
-
# Return the fitted polygon points
|
313
|
-
return list(fitted_polygon.exterior.coords)
|
314
|
-
|
315
233
|
@staticmethod
|
316
234
|
def create_user_attribute_node(node_id: int) -> ET.Element:
|
317
235
|
"""Creates an XML user attribute node with given node ID.
|
maps4fs/generator/texture.py
CHANGED
@@ -159,6 +159,7 @@ class Texture(Component):
|
|
159
159
|
|
160
160
|
def preprocess(self) -> None:
|
161
161
|
self.light_version = self.kwargs.get("light_version", False)
|
162
|
+
self.fields_padding = self.kwargs.get("fields_padding", 0)
|
162
163
|
self.logger.debug("Light version: %s.", self.light_version)
|
163
164
|
|
164
165
|
if not os.path.isfile(self.game.texture_schema):
|
@@ -476,7 +477,9 @@ class Texture(Component):
|
|
476
477
|
pairs = list(zip(xs, ys))
|
477
478
|
return np.array(pairs, dtype=np.int32).reshape((-1, 1, 2))
|
478
479
|
|
479
|
-
def _to_polygon(
|
480
|
+
def _to_polygon(
|
481
|
+
self, obj: pd.core.series.Series, width: int | None
|
482
|
+
) -> shapely.geometry.polygon.Polygon:
|
480
483
|
"""Converts OSM object to numpy array of polygon points.
|
481
484
|
|
482
485
|
Arguments:
|
@@ -484,7 +487,7 @@ class Texture(Component):
|
|
484
487
|
width (int | None): Width of the polygon in meters.
|
485
488
|
|
486
489
|
Returns:
|
487
|
-
|
490
|
+
shapely.geometry.polygon.Polygon: Polygon geometry.
|
488
491
|
"""
|
489
492
|
geometry = obj["geometry"]
|
490
493
|
geometry_type = geometry.geom_type
|
@@ -498,7 +501,7 @@ class Texture(Component):
|
|
498
501
|
self,
|
499
502
|
geometry: shapely.geometry.linestring.LineString | shapely.geometry.point.Point,
|
500
503
|
width: int | None,
|
501
|
-
) ->
|
504
|
+
) -> shapely.geometry.polygon.Polygon:
|
502
505
|
"""Converts LineString or Point geometry to numpy array of polygon points.
|
503
506
|
|
504
507
|
Arguments:
|
@@ -507,10 +510,23 @@ class Texture(Component):
|
|
507
510
|
width (int | None): Width of the polygon in meters.
|
508
511
|
|
509
512
|
Returns:
|
510
|
-
|
513
|
+
shapely.geometry.polygon.Polygon: Polygon geometry.
|
511
514
|
"""
|
512
515
|
polygon = geometry.buffer(width)
|
513
|
-
return
|
516
|
+
return polygon
|
517
|
+
|
518
|
+
def _skip(
|
519
|
+
self, geometry: shapely.geometry.polygon.Polygon, *args, **kwargs
|
520
|
+
) -> shapely.geometry.polygon.Polygon:
|
521
|
+
"""Returns the same geometry.
|
522
|
+
|
523
|
+
Arguments:
|
524
|
+
geometry (shapely.geometry.polygon.Polygon): Polygon geometry.
|
525
|
+
|
526
|
+
Returns:
|
527
|
+
shapely.geometry.polygon.Polygon: Polygon geometry.
|
528
|
+
"""
|
529
|
+
return geometry
|
514
530
|
|
515
531
|
def _converters(
|
516
532
|
self, geom_type: str
|
@@ -523,7 +539,7 @@ class Texture(Component):
|
|
523
539
|
Returns:
|
524
540
|
Callable[[shapely.geometry, int | None], np.ndarray]: Converter function.
|
525
541
|
"""
|
526
|
-
converters = {"Polygon": self.
|
542
|
+
converters = {"Polygon": self._skip, "LineString": self._sequence, "Point": self._sequence}
|
527
543
|
return converters.get(geom_type) # type: ignore
|
528
544
|
|
529
545
|
def polygons(
|
@@ -538,6 +554,7 @@ class Texture(Component):
|
|
538
554
|
Yields:
|
539
555
|
Generator[np.ndarray, None, None]: Numpy array of polygon points.
|
540
556
|
"""
|
557
|
+
is_fieds = "farmland" in tags.values()
|
541
558
|
try:
|
542
559
|
objects = ox.features_from_bbox(bbox=self.new_bbox, tags=tags)
|
543
560
|
except Exception as e: # pylint: disable=W0718
|
@@ -551,7 +568,17 @@ class Texture(Component):
|
|
551
568
|
polygon = self._to_polygon(obj, width)
|
552
569
|
if polygon is None:
|
553
570
|
continue
|
554
|
-
|
571
|
+
|
572
|
+
if is_fieds and self.fields_padding > 0:
|
573
|
+
padded_polygon = polygon.buffer(-self.fields_padding)
|
574
|
+
|
575
|
+
if not isinstance(padded_polygon, shapely.geometry.polygon.Polygon):
|
576
|
+
self.logger.warning("The padding value is too high, field will not padded.")
|
577
|
+
else:
|
578
|
+
polygon = padded_polygon
|
579
|
+
|
580
|
+
polygon_np = self._to_np(polygon)
|
581
|
+
yield polygon_np
|
555
582
|
|
556
583
|
def previews(self) -> list[str]:
|
557
584
|
"""Invokes methods to generate previews. Returns list of paths to previews.
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: maps4fs
|
3
|
-
Version: 1.0.
|
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
|
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
|
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>
|
@@ -452,20 +455,30 @@ The tool supports the custom size of the map. To use this feature select `Custom
|
|
452
455
|
|
453
456
|
⛔️ 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>
|
454
457
|
|
455
|
-

|
456
459
|
|
457
460
|
You can also apply some advanced settings to the map generation process. Note that they're ADVANCED, so you don't need to use them if you're not sure what they do.<br>
|
458
461
|
|
459
|
-
|
462
|
+
### DEM Advanced settings
|
460
463
|
|
461
|
-
-
|
464
|
+
- Multiplier: the height of the map is multiplied by this value. So the DEM map is just a 16-bit grayscale image, which means that the maximum available value there is 65535, while the actual difference between the deepest and the highest point on Earth is about 20 km. Just note that this setting mostly does not matter, because you can always adjust it in the Giants Editor, learn more about the DEM file and the heightScale parameter in [docs](docs/dem.md). By default, it's set to 1.
|
462
465
|
|
463
|
-
-
|
466
|
+
- Blur radius: the radius of the Gaussian blur filter applied to the DEM map. By default, it's set to 21. This filter just makes the DEM map smoother, so the height transitions will be more natural. You can set it to 1 to disable the filter, but it will result in a Minecraft-like map.
|
464
467
|
|
465
|
-
-
|
468
|
+
- 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.
|
469
|
+
|
470
|
+
### Background Terrain Advanced settings
|
466
471
|
|
467
472
|
- Background Terrain Generate only full tiles: if checked (by default) the small tiles (N, NE, E, and so on) will not be generated, only the full tile will be created. It's useful when you don't want to work with separate tiles, but with one big file. Since the new method of cutting the map from the background terrain added to the documentation, and now it's possible to perfectly align the map with the background terrain, this option will remain just as a legacy one.
|
468
473
|
|
474
|
+
### Texture Advanced settings
|
475
|
+
|
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.
|
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
|
+
|
469
482
|
## Resources
|
470
483
|
In this section, you'll find a list of the resources that you need to create a map for the Farming Simulator.<br>
|
471
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>
|
@@ -1,23 +1,23 @@
|
|
1
1
|
maps4fs/__init__.py,sha256=da4jmND2Ths9AffnkAKgzLHNkvKFOc_l21gJisPXqWY,155
|
2
2
|
maps4fs/logger.py,sha256=B-NEYpMjPAAqlV4VpfTi6nbBFnEABVtQOaYe6nMpidg,1489
|
3
3
|
maps4fs/generator/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
|
4
|
-
maps4fs/generator/background.py,sha256=
|
5
|
-
maps4fs/generator/component.py,sha256=
|
4
|
+
maps4fs/generator/background.py,sha256=ogd5TmAWL5zhZtTCOH8YHGKqc0SGQqOsWuVg3AaZO0I,14015
|
5
|
+
maps4fs/generator/component.py,sha256=ac0l1nUeRnMejNgJ26PO0Jjb2ELYSaagM0mzNjM_w5o,14582
|
6
6
|
maps4fs/generator/config.py,sha256=kspXIT2o-_28EU0RQsROHCjkgQdqQnvreAKP5QAC5Ws,4279
|
7
7
|
maps4fs/generator/dem.py,sha256=cCJLE20-XKaQx5lwIFNEgmQ5kfhE24QmVrAyMVwsU_A,16459
|
8
8
|
maps4fs/generator/game.py,sha256=4I6edxTeZf41Vgvx6BaucEflMEHomRRvdMZRJAPm0d4,7450
|
9
|
-
maps4fs/generator/grle.py,sha256=
|
10
|
-
maps4fs/generator/i3d.py,sha256=
|
9
|
+
maps4fs/generator/grle.py,sha256=5Ck44CKI-yxnxgy8K_18L0pFAfln5e9e7DVkpNZVwC0,6404
|
10
|
+
maps4fs/generator/i3d.py,sha256=CPscvhuoBRfGJSmcCGHyjM5FKlDXAbYALJkvhGv4UrA,11091
|
11
11
|
maps4fs/generator/map.py,sha256=gDZUZ2wimoeA8mHVOCnZvrIBeK7b99OIWFd_LjruqBc,4677
|
12
12
|
maps4fs/generator/path_steps.py,sha256=twhoP0KOYWOpOJfYrSWPHygtIeM-r5cIlePg1SHVyHk,3589
|
13
13
|
maps4fs/generator/qgis.py,sha256=Es8hLuqN_KH8lDfnJE6He2rWYbAKJ3RGPn-o87S6CPI,6116
|
14
|
-
maps4fs/generator/texture.py,sha256=
|
14
|
+
maps4fs/generator/texture.py,sha256=2c2x99xnqKZoXDB4fdQBESFMPMiGrbx_fADFTdx4ZGY,24638
|
15
15
|
maps4fs/generator/tile.py,sha256=z1-xEVjgFNf2WzLkgwoGGq8nREJpjPljeC9lmb5xPKA,1997
|
16
16
|
maps4fs/toolbox/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
|
17
17
|
maps4fs/toolbox/background.py,sha256=9BXWNqs_n3HgqDiPztWylgYk_QM4YgBpe6_ZNQAWtSc,2154
|
18
18
|
maps4fs/toolbox/dem.py,sha256=z9IPFNmYbjiigb3t02ZenI3Mo8odd19c5MZbjDEovTo,3525
|
19
|
-
maps4fs-1.0.
|
20
|
-
maps4fs-1.0.
|
21
|
-
maps4fs-1.0.
|
22
|
-
maps4fs-1.0.
|
23
|
-
maps4fs-1.0.
|
19
|
+
maps4fs-1.0.9.dist-info/LICENSE.md,sha256=pTKD_oUexcn-yccFCTrMeLkZy0ifLRa-VNcDLqLZaIw,10749
|
20
|
+
maps4fs-1.0.9.dist-info/METADATA,sha256=2cnIMdJN45eVlg9tBVPAwJsoBWVz1qavS6U9B4-iVAU,28327
|
21
|
+
maps4fs-1.0.9.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
22
|
+
maps4fs-1.0.9.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
|
23
|
+
maps4fs-1.0.9.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|