maps4fs 1.6.4__py3-none-any.whl → 1.6.7__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.
- maps4fs/generator/background.py +29 -24
- maps4fs/generator/component.py +23 -0
- maps4fs/generator/game.py +1 -1
- maps4fs/generator/grle.py +92 -26
- maps4fs/generator/i3d.py +4 -7
- maps4fs/generator/map.py +16 -0
- maps4fs/generator/settings.py +22 -2
- maps4fs/generator/texture.py +60 -4
- {maps4fs-1.6.4.dist-info → maps4fs-1.6.7.dist-info}/METADATA +19 -2
- {maps4fs-1.6.4.dist-info → maps4fs-1.6.7.dist-info}/RECORD +13 -13
- {maps4fs-1.6.4.dist-info → maps4fs-1.6.7.dist-info}/LICENSE.md +0 -0
- {maps4fs-1.6.4.dist-info → maps4fs-1.6.7.dist-info}/WHEEL +0 -0
- {maps4fs-1.6.4.dist-info → maps4fs-1.6.7.dist-info}/top_level.txt +0 -0
maps4fs/generator/background.py
CHANGED
@@ -58,6 +58,10 @@ class Background(Component):
|
|
58
58
|
os.makedirs(self.water_directory, exist_ok=True)
|
59
59
|
|
60
60
|
self.output_path = os.path.join(self.background_directory, f"{FULL_NAME}.png")
|
61
|
+
if self.map.custom_background_path:
|
62
|
+
self.check_custom_background(self.map.custom_background_path)
|
63
|
+
shutil.copyfile(self.map.custom_background_path, self.output_path)
|
64
|
+
|
61
65
|
self.not_substracted_path = os.path.join(self.background_directory, "not_substracted.png")
|
62
66
|
self.not_resized_path = os.path.join(self.background_directory, "not_resized.png")
|
63
67
|
|
@@ -75,6 +79,28 @@ class Background(Component):
|
|
75
79
|
self.dem.set_output_resolution((self.rotated_size, self.rotated_size))
|
76
80
|
self.dem.set_dem_path(self.output_path)
|
77
81
|
|
82
|
+
def check_custom_background(self, image_path: str) -> None:
|
83
|
+
"""Checks if the custom background image meets the requirements.
|
84
|
+
|
85
|
+
Arguments:
|
86
|
+
image_path (str): The path to the custom background image.
|
87
|
+
|
88
|
+
Raises:
|
89
|
+
ValueError: If the custom background image does not meet the requirements.
|
90
|
+
"""
|
91
|
+
image = cv2.imread(image_path, cv2.IMREAD_UNCHANGED) # pylint: disable=no-member
|
92
|
+
if image.shape[0] != image.shape[1]:
|
93
|
+
raise ValueError("The custom background image must be a square.")
|
94
|
+
|
95
|
+
if image.shape[0] != self.map_size + DEFAULT_DISTANCE * 2:
|
96
|
+
raise ValueError("The custom background image must have the size of the map + 4096.")
|
97
|
+
|
98
|
+
if len(image.shape) != 2:
|
99
|
+
raise ValueError("The custom background image must be a grayscale image.")
|
100
|
+
|
101
|
+
if image.dtype != np.uint16:
|
102
|
+
raise ValueError("The custom background image must be a 16-bit grayscale image.")
|
103
|
+
|
78
104
|
def is_preview(self, name: str) -> bool:
|
79
105
|
"""Checks if the DEM is a preview.
|
80
106
|
|
@@ -91,7 +117,9 @@ class Background(Component):
|
|
91
117
|
as a result the DEM files will be saved, then based on them the obj files will be
|
92
118
|
generated."""
|
93
119
|
self.create_background_textures()
|
94
|
-
|
120
|
+
|
121
|
+
if not self.map.custom_background_path:
|
122
|
+
self.dem.process()
|
95
123
|
|
96
124
|
shutil.copyfile(self.dem.dem_path, self.not_substracted_path)
|
97
125
|
self.cutout(self.dem.dem_path, save_path=self.not_resized_path)
|
@@ -365,29 +393,6 @@ class Background(Component):
|
|
365
393
|
mesh.apply_scale([0.5, 0.5, 0.5])
|
366
394
|
self.mesh_to_stl(mesh)
|
367
395
|
|
368
|
-
def get_z_scaling_factor(self) -> float:
|
369
|
-
"""Calculates the scaling factor for the Z axis based on the map settings.
|
370
|
-
|
371
|
-
Returns:
|
372
|
-
float -- The scaling factor for the Z axis.
|
373
|
-
"""
|
374
|
-
|
375
|
-
scaling_factor = 1 / self.map.dem_settings.multiplier
|
376
|
-
self.logger.debug("Z scaling factor including DEM multiplier: %s", scaling_factor)
|
377
|
-
|
378
|
-
if self.map.shared_settings.height_scale_multiplier:
|
379
|
-
scaling_factor *= self.map.shared_settings.height_scale_multiplier
|
380
|
-
self.logger.debug(
|
381
|
-
"Z scaling factor including height scale multiplier: %s", scaling_factor
|
382
|
-
)
|
383
|
-
if self.map.shared_settings.mesh_z_scaling_factor:
|
384
|
-
scaling_factor *= 1 / self.map.shared_settings.mesh_z_scaling_factor
|
385
|
-
self.logger.debug(
|
386
|
-
"Z scaling factor including mesh z scaling factor: %s", scaling_factor
|
387
|
-
)
|
388
|
-
|
389
|
-
return scaling_factor
|
390
|
-
|
391
396
|
def mesh_to_stl(self, mesh: trimesh.Trimesh) -> None:
|
392
397
|
"""Converts the mesh to an STL file and saves it in the previews directory.
|
393
398
|
Uses powerful simplification to reduce the size of the file since it will be used
|
maps4fs/generator/component.py
CHANGED
@@ -535,3 +535,26 @@ class Component:
|
|
535
535
|
interpolated_polyline.append(polyline[-1])
|
536
536
|
|
537
537
|
return interpolated_polyline
|
538
|
+
|
539
|
+
def get_z_scaling_factor(self) -> float:
|
540
|
+
"""Calculates the scaling factor for the Z axis based on the map settings.
|
541
|
+
|
542
|
+
Returns:
|
543
|
+
float -- The scaling factor for the Z axis.
|
544
|
+
"""
|
545
|
+
|
546
|
+
scaling_factor = 1 / self.map.dem_settings.multiplier
|
547
|
+
self.logger.debug("Z scaling factor including DEM multiplier: %s", scaling_factor)
|
548
|
+
|
549
|
+
if self.map.shared_settings.height_scale_multiplier:
|
550
|
+
scaling_factor *= self.map.shared_settings.height_scale_multiplier
|
551
|
+
self.logger.debug(
|
552
|
+
"Z scaling factor including height scale multiplier: %s", scaling_factor
|
553
|
+
)
|
554
|
+
if self.map.shared_settings.mesh_z_scaling_factor:
|
555
|
+
scaling_factor *= 1 / self.map.shared_settings.mesh_z_scaling_factor
|
556
|
+
self.logger.debug(
|
557
|
+
"Z scaling factor including mesh z scaling factor: %s", scaling_factor
|
558
|
+
)
|
559
|
+
|
560
|
+
return scaling_factor
|
maps4fs/generator/game.py
CHANGED
@@ -40,7 +40,7 @@ class Game:
|
|
40
40
|
_tree_schema: str | None = None
|
41
41
|
|
42
42
|
# Order matters! Some components depend on others.
|
43
|
-
components = [Texture,
|
43
|
+
components = [Texture, Background, GRLE, I3d, Config, Satellite]
|
44
44
|
|
45
45
|
def __init__(self, map_template_path: str | None = None):
|
46
46
|
if map_template_path:
|
maps4fs/generator/grle.py
CHANGED
@@ -10,13 +10,26 @@ import numpy as np
|
|
10
10
|
from shapely.geometry import Polygon # type: ignore
|
11
11
|
|
12
12
|
from maps4fs.generator.component import Component
|
13
|
-
from maps4fs.generator.texture import Texture
|
13
|
+
from maps4fs.generator.texture import PREVIEW_MAXIMUM_SIZE, Texture
|
14
14
|
|
15
|
-
ISLAND_SIZE_MIN = 10
|
16
|
-
ISLAND_SIZE_MAX = 200
|
17
15
|
ISLAND_DISTORTION = 0.3
|
18
|
-
|
19
|
-
|
16
|
+
|
17
|
+
|
18
|
+
def plant_to_pixel_value(plant_name: str) -> int | None:
|
19
|
+
"""Returns the pixel value representation of the plant.
|
20
|
+
If not found, returns None.
|
21
|
+
|
22
|
+
Arguments:
|
23
|
+
plant_name (str): name of the plant
|
24
|
+
|
25
|
+
Returns:
|
26
|
+
int | None: pixel value of the plant or None if not found.
|
27
|
+
"""
|
28
|
+
plants = {
|
29
|
+
"smallDenseMix": 33,
|
30
|
+
"meadow": 131,
|
31
|
+
}
|
32
|
+
return plants.get(plant_name)
|
20
33
|
|
21
34
|
|
22
35
|
# pylint: disable=W0223
|
@@ -39,6 +52,7 @@ class GRLE(Component):
|
|
39
52
|
def preprocess(self) -> None:
|
40
53
|
"""Gets the path to the map I3D file from the game instance and saves it to the instance
|
41
54
|
attribute. If the game does not support I3D files, the attribute is set to None."""
|
55
|
+
self.preview_paths: dict[str, str] = {}
|
42
56
|
|
43
57
|
try:
|
44
58
|
grle_schema_path = self.game.grle_schema
|
@@ -89,6 +103,7 @@ class GRLE(Component):
|
|
89
103
|
else:
|
90
104
|
self.logger.warning("Adding plants it's not supported for the %s.", self.game.code)
|
91
105
|
|
106
|
+
# pylint: disable=no-member
|
92
107
|
def previews(self) -> list[str]:
|
93
108
|
"""Returns a list of paths to the preview images (empty list).
|
94
109
|
The component does not generate any preview images so it returns an empty list.
|
@@ -96,7 +111,57 @@ class GRLE(Component):
|
|
96
111
|
Returns:
|
97
112
|
list[str]: An empty list.
|
98
113
|
"""
|
99
|
-
|
114
|
+
preview_paths = []
|
115
|
+
for preview_name, preview_path in self.preview_paths.items():
|
116
|
+
save_path = os.path.join(self.previews_directory, f"{preview_name}.png")
|
117
|
+
# Resize the preview image to the maximum size allowed for previews.
|
118
|
+
image = cv2.imread(preview_path, cv2.IMREAD_GRAYSCALE)
|
119
|
+
if image.shape[0] > PREVIEW_MAXIMUM_SIZE or image.shape[1] > PREVIEW_MAXIMUM_SIZE:
|
120
|
+
image = cv2.resize(image, (PREVIEW_MAXIMUM_SIZE, PREVIEW_MAXIMUM_SIZE))
|
121
|
+
image_normalized = np.empty_like(image)
|
122
|
+
cv2.normalize(image, image_normalized, 0, 255, cv2.NORM_MINMAX)
|
123
|
+
image_colored = cv2.applyColorMap(image_normalized, cv2.COLORMAP_JET)
|
124
|
+
cv2.imwrite(save_path, image_colored)
|
125
|
+
preview_paths.append(save_path)
|
126
|
+
|
127
|
+
with_fields_save_path = os.path.join(
|
128
|
+
self.previews_directory, f"{preview_name}_with_fields.png"
|
129
|
+
)
|
130
|
+
image_with_fields = self.overlay_fields(image_colored)
|
131
|
+
if image_with_fields is None:
|
132
|
+
continue
|
133
|
+
cv2.imwrite(with_fields_save_path, image_with_fields) # pylint: disable=no-member
|
134
|
+
preview_paths.append(with_fields_save_path)
|
135
|
+
|
136
|
+
return preview_paths
|
137
|
+
|
138
|
+
def overlay_fields(self, farmlands_np: np.ndarray) -> np.ndarray | None:
|
139
|
+
"""Overlay fields on the farmlands preview image.
|
140
|
+
|
141
|
+
Arguments:
|
142
|
+
farmlands_np (np.ndarray): The farmlands preview image.
|
143
|
+
|
144
|
+
Returns:
|
145
|
+
np.ndarray | None: The farmlands preview image with fields overlayed on top of it.
|
146
|
+
"""
|
147
|
+
texture_component: Texture | None = self.map.get_component("Texture") # type: ignore
|
148
|
+
if not texture_component:
|
149
|
+
self.logger.warning("Texture component not found in the map.")
|
150
|
+
return None
|
151
|
+
|
152
|
+
fields_layer = texture_component.get_layer_by_usage("field")
|
153
|
+
fields_layer_path = fields_layer.get_preview_or_path( # type: ignore
|
154
|
+
self.game.weights_dir_path(self.map_directory)
|
155
|
+
)
|
156
|
+
if not fields_layer_path or not os.path.isfile(fields_layer_path):
|
157
|
+
self.logger.warning("Fields layer not found in the texture component.")
|
158
|
+
return None
|
159
|
+
fields_np = cv2.imread(fields_layer_path)
|
160
|
+
# Resize fields_np to the same size as farmlands_np.
|
161
|
+
fields_np = cv2.resize(fields_np, (farmlands_np.shape[1], farmlands_np.shape[0]))
|
162
|
+
|
163
|
+
# use fields_np as base layer and overlay farmlands_np on top of it with 50% alpha blending.
|
164
|
+
return cv2.addWeighted(fields_np, 0.5, farmlands_np, 0.5, 0)
|
100
165
|
|
101
166
|
# pylint: disable=R0801, R0914
|
102
167
|
def _add_farmlands(self) -> None:
|
@@ -155,7 +220,7 @@ class GRLE(Component):
|
|
155
220
|
angle=self.rotation,
|
156
221
|
)
|
157
222
|
except ValueError as e:
|
158
|
-
self.logger.
|
223
|
+
self.logger.debug(
|
159
224
|
"Farmland %s could not be fitted into the map bounds with error: %s",
|
160
225
|
farmland_id,
|
161
226
|
e,
|
@@ -180,7 +245,7 @@ class GRLE(Component):
|
|
180
245
|
try:
|
181
246
|
cv2.fillPoly(image, [field_np], farmland_id) # type: ignore
|
182
247
|
except Exception as e: # pylint: disable=W0718
|
183
|
-
self.logger.
|
248
|
+
self.logger.debug(
|
184
249
|
"Farmland %s could not be added to the InfoLayer PNG file with error: %s",
|
185
250
|
farmland_id,
|
186
251
|
e,
|
@@ -204,6 +269,8 @@ class GRLE(Component):
|
|
204
269
|
"Farmlands added to the InfoLayer PNG file: %s.", info_layer_farmlands_path
|
205
270
|
)
|
206
271
|
|
272
|
+
self.preview_paths["farmlands"] = info_layer_farmlands_path # type: ignore
|
273
|
+
|
207
274
|
# pylint: disable=R0915
|
208
275
|
def _add_plants(self) -> None:
|
209
276
|
"""Adds plants to the InfoLayer PNG file."""
|
@@ -270,10 +337,13 @@ class GRLE(Component):
|
|
270
337
|
grass_image[forest_image != 0] = 255
|
271
338
|
|
272
339
|
# B and G channels remain the same (zeros), while we change the R channel.
|
273
|
-
possible_R_values = [
|
340
|
+
possible_R_values = [65, 97, 129, 161, 193, 225] # pylint: disable=C0103
|
274
341
|
|
275
|
-
|
276
|
-
|
342
|
+
base_layer_pixel_value = plant_to_pixel_value(
|
343
|
+
self.map.grle_settings.base_grass # type:ignore
|
344
|
+
)
|
345
|
+
if not base_layer_pixel_value:
|
346
|
+
base_layer_pixel_value = 131
|
277
347
|
|
278
348
|
# pylint: disable=no-member
|
279
349
|
def create_island_of_plants(image: np.ndarray, count: int) -> np.ndarray:
|
@@ -290,23 +360,20 @@ class GRLE(Component):
|
|
290
360
|
# Randomly choose the value for the island.
|
291
361
|
plant_value = choice(possible_R_values)
|
292
362
|
# Randomly choose the size of the island.
|
293
|
-
island_size = randint(
|
363
|
+
island_size = randint(
|
364
|
+
self.map.grle_settings.plants_island_minimum_size, # type:ignore
|
365
|
+
self.map.grle_settings.plants_island_maximum_size, # type:ignore
|
366
|
+
)
|
294
367
|
# Randomly choose the position of the island.
|
295
|
-
# x = np.random.randint(0, image.shape[1] - island_size)
|
296
|
-
# y = np.random.randint(0, image.shape[0] - island_size)
|
297
368
|
x = randint(0, image.shape[1] - island_size)
|
298
369
|
y = randint(0, image.shape[0] - island_size)
|
299
370
|
|
300
|
-
# Randomly choose the shape of the island.
|
301
|
-
# shapes = ["circle", "ellipse", "polygon"]
|
302
|
-
# shape = choice(shapes)
|
303
|
-
|
304
371
|
try:
|
305
372
|
polygon_points = get_rounded_polygon(
|
306
|
-
num_vertices=
|
373
|
+
num_vertices=self.map.grle_settings.plants_island_vertex_count,
|
307
374
|
center=(x + island_size // 2, y + island_size // 2),
|
308
375
|
radius=island_size // 2,
|
309
|
-
rounding_radius=
|
376
|
+
rounding_radius=self.map.grle_settings.plants_island_rounding_radius,
|
310
377
|
)
|
311
378
|
if not polygon_points:
|
312
379
|
continue
|
@@ -355,16 +422,16 @@ class GRLE(Component):
|
|
355
422
|
grass_image_copy = grass_image.copy()
|
356
423
|
if forest_image is not None:
|
357
424
|
# Add the forest layer to the base image, to merge the masks.
|
358
|
-
grass_image_copy[forest_image != 0] =
|
359
|
-
|
360
|
-
grass_image_copy[grass_image != 0] =
|
425
|
+
grass_image_copy[forest_image != 0] = base_layer_pixel_value
|
426
|
+
|
427
|
+
grass_image_copy[grass_image != 0] = base_layer_pixel_value
|
361
428
|
|
362
429
|
# Add islands of plants to the base image.
|
363
|
-
island_count = self.map_size
|
430
|
+
island_count = int(self.map_size * self.map.grle_settings.plants_island_percent // 100)
|
364
431
|
self.logger.debug("Adding %s islands of plants to the base image.", island_count)
|
365
432
|
if self.map.grle_settings.random_plants:
|
366
433
|
grass_image_copy = create_island_of_plants(grass_image_copy, island_count)
|
367
|
-
|
434
|
+
self.logger.info("Added %s islands of plants to the base image.", island_count)
|
368
435
|
|
369
436
|
# Sligtly reduce the size of the grass_image, that we'll use as mask.
|
370
437
|
kernel = np.ones((3, 3), np.uint8)
|
@@ -380,7 +447,6 @@ class GRLE(Component):
|
|
380
447
|
grass_image_copy[:, 0] = 0 # Left side
|
381
448
|
grass_image_copy[:, -1] = 0 # Right side
|
382
449
|
|
383
|
-
# Value of 33 represents the base grass plant.
|
384
450
|
# After painting it with base grass, we'll create multiple islands of different plants.
|
385
451
|
# On the final step, we'll remove all the values which in pixels
|
386
452
|
# where zerons in the original base image (so we don't paint grass where it should not be).
|
maps4fs/generator/i3d.py
CHANGED
@@ -15,12 +15,9 @@ from maps4fs.generator.component import Component
|
|
15
15
|
from maps4fs.generator.texture import Texture
|
16
16
|
|
17
17
|
DISPLACEMENT_LAYER_SIZE_FOR_BIG_MAPS = 32768
|
18
|
-
DEFAULT_MAX_LOD_DISTANCE = 10000
|
19
|
-
DEFAULT_MAX_LOD_OCCLUDER_DISTANCE = 10000
|
20
18
|
NODE_ID_STARTING_VALUE = 2000
|
21
19
|
SPLINES_NODE_ID_STARTING_VALUE = 5000
|
22
20
|
TREE_NODE_ID_STARTING_VALUE = 10000
|
23
|
-
DEFAULT_FOREST_DENSITY = 10
|
24
21
|
|
25
22
|
|
26
23
|
# pylint: disable=R0903
|
@@ -197,7 +194,7 @@ class I3d(Component):
|
|
197
194
|
linestring_points=road, angle=self.rotation
|
198
195
|
)
|
199
196
|
except ValueError as e:
|
200
|
-
self.logger.
|
197
|
+
self.logger.debug(
|
201
198
|
"Road %s could not be fitted into the map bounds with error: %s",
|
202
199
|
road_id,
|
203
200
|
e,
|
@@ -248,7 +245,7 @@ class I3d(Component):
|
|
248
245
|
y = max(0, min(y, dem_y_size - 1))
|
249
246
|
|
250
247
|
z = not_resized_dem[y, x]
|
251
|
-
z
|
248
|
+
z *= self.get_z_scaling_factor() # type: ignore
|
252
249
|
|
253
250
|
cv_node = ET.Element("cv")
|
254
251
|
cv_node.set("c", f"{cx}, {z}, {cy}")
|
@@ -322,7 +319,7 @@ class I3d(Component):
|
|
322
319
|
polygon_points=field, angle=self.rotation
|
323
320
|
)
|
324
321
|
except ValueError as e:
|
325
|
-
self.logger.
|
322
|
+
self.logger.debug(
|
326
323
|
"Field %s could not be fitted into the map bounds with error: %s",
|
327
324
|
field_id,
|
328
325
|
e,
|
@@ -336,7 +333,7 @@ class I3d(Component):
|
|
336
333
|
try:
|
337
334
|
cx, cy = self.get_polygon_center(field_ccs)
|
338
335
|
except Exception as e: # pylint: disable=W0718
|
339
|
-
self.logger.
|
336
|
+
self.logger.debug(
|
340
337
|
"Field %s could not be fitted into the map bounds.", field_id
|
341
338
|
)
|
342
339
|
self.logger.debug("Error: %s", e)
|
maps4fs/generator/map.py
CHANGED
@@ -89,6 +89,17 @@ class Map:
|
|
89
89
|
|
90
90
|
self.dem_settings = dem_settings
|
91
91
|
self.logger.info("DEM settings: %s", dem_settings)
|
92
|
+
if self.dem_settings.water_depth > 0:
|
93
|
+
# Make sure that the plateau value is >= water_depth
|
94
|
+
self.dem_settings.plateau = max(
|
95
|
+
self.dem_settings.plateau, self.dem_settings.water_depth
|
96
|
+
)
|
97
|
+
self.logger.info(
|
98
|
+
"Plateau value was set to %s to be >= water_depth value %s",
|
99
|
+
self.dem_settings.plateau,
|
100
|
+
self.dem_settings.water_depth,
|
101
|
+
)
|
102
|
+
|
92
103
|
self.background_settings = background_settings
|
93
104
|
self.logger.info("Background settings: %s", background_settings)
|
94
105
|
self.grle_settings = grle_settings
|
@@ -140,6 +151,11 @@ class Map:
|
|
140
151
|
json.dump(self.tree_custom_schema, file, indent=4)
|
141
152
|
self.logger.debug("Tree custom schema saved to %s", save_path)
|
142
153
|
|
154
|
+
self.custom_background_path = kwargs.get("custom_background_path", None)
|
155
|
+
if self.custom_background_path:
|
156
|
+
save_path = os.path.join(self.map_directory, "custom_background.png")
|
157
|
+
shutil.copyfile(self.custom_background_path, save_path)
|
158
|
+
|
143
159
|
try:
|
144
160
|
shutil.unpack_archive(game.template_path, self.map_directory)
|
145
161
|
self.logger.debug("Map template unpacked to %s", self.map_directory)
|
maps4fs/generator/settings.py
CHANGED
@@ -23,6 +23,10 @@ class SharedSettings(BaseModel):
|
|
23
23
|
class SettingsModel(BaseModel):
|
24
24
|
"""Base class for settings models. It provides methods to convert settings to and from JSON."""
|
25
25
|
|
26
|
+
model_config = ConfigDict(
|
27
|
+
frozen=False,
|
28
|
+
)
|
29
|
+
|
26
30
|
@classmethod
|
27
31
|
def all_settings_to_json(cls) -> dict[str, dict[str, Any]]:
|
28
32
|
"""Get all settings of the current class and its subclasses as a dictionary.
|
@@ -38,18 +42,28 @@ class SettingsModel(BaseModel):
|
|
38
42
|
return all_settings
|
39
43
|
|
40
44
|
@classmethod
|
41
|
-
def all_settings_from_json(
|
45
|
+
def all_settings_from_json(
|
46
|
+
cls, data: dict, flattening: bool = True
|
47
|
+
) -> dict[str, SettingsModel]:
|
42
48
|
"""Create settings instances from JSON data.
|
43
49
|
|
44
50
|
Arguments:
|
45
51
|
data (dict): JSON data.
|
52
|
+
flattening (bool): if set to True will flattet iterables to use the first element
|
53
|
+
of it.
|
46
54
|
|
47
55
|
Returns:
|
48
56
|
dict[str, Type[SettingsModel]]: Dictionary with settings instances.
|
49
57
|
"""
|
50
58
|
settings = {}
|
51
59
|
for subclass in cls.__subclasses__():
|
52
|
-
|
60
|
+
subclass_data = data[subclass.__name__]
|
61
|
+
if flattening:
|
62
|
+
for key, value in subclass_data.items():
|
63
|
+
if isinstance(value, (list, tuple)):
|
64
|
+
subclass_data[key] = value[0]
|
65
|
+
|
66
|
+
settings[subclass.__name__] = subclass(**subclass_data)
|
53
67
|
|
54
68
|
return settings
|
55
69
|
|
@@ -117,6 +131,12 @@ class GRLESettings(SettingsModel):
|
|
117
131
|
farmland_margin: int = 0
|
118
132
|
random_plants: bool = True
|
119
133
|
add_farmyards: bool = False
|
134
|
+
base_grass: tuple | str = ("smallDenseMix", "meadow")
|
135
|
+
plants_island_minimum_size: int = 10
|
136
|
+
plants_island_maximum_size: int = 200
|
137
|
+
plants_island_vertex_count: int = 30
|
138
|
+
plants_island_rounding_radius: int = 15
|
139
|
+
plants_island_percent: int = 100
|
120
140
|
|
121
141
|
|
122
142
|
class I3DSettings(SettingsModel):
|
maps4fs/generator/texture.py
CHANGED
@@ -5,6 +5,8 @@ from __future__ import annotations
|
|
5
5
|
import json
|
6
6
|
import os
|
7
7
|
import re
|
8
|
+
import shutil
|
9
|
+
import warnings
|
8
10
|
from collections import defaultdict
|
9
11
|
from typing import Any, Callable, Generator, Optional
|
10
12
|
|
@@ -69,6 +71,7 @@ class Texture(Component):
|
|
69
71
|
usage: str | None = None,
|
70
72
|
background: bool = False,
|
71
73
|
invisible: bool = False,
|
74
|
+
procedural: list[str] | None = None,
|
72
75
|
):
|
73
76
|
self.name = name
|
74
77
|
self.count = count
|
@@ -81,6 +84,7 @@ class Texture(Component):
|
|
81
84
|
self.usage = usage
|
82
85
|
self.background = background
|
83
86
|
self.invisible = invisible
|
87
|
+
self.procedural = procedural
|
84
88
|
|
85
89
|
def to_json(self) -> dict[str, str | list[str] | bool]: # type: ignore
|
86
90
|
"""Returns dictionary with layer data.
|
@@ -99,6 +103,7 @@ class Texture(Component):
|
|
99
103
|
"usage": self.usage,
|
100
104
|
"background": self.background,
|
101
105
|
"invisible": self.invisible,
|
106
|
+
"procedural": self.procedural,
|
102
107
|
}
|
103
108
|
|
104
109
|
data = {k: v for k, v in data.items() if v is not None}
|
@@ -212,6 +217,10 @@ class Texture(Component):
|
|
212
217
|
|
213
218
|
self._weights_dir = self.game.weights_dir_path(self.map_directory)
|
214
219
|
self.logger.debug("Weights directory: %s.", self._weights_dir)
|
220
|
+
self.procedural_dir = os.path.join(self._weights_dir, "masks")
|
221
|
+
os.makedirs(self.procedural_dir, exist_ok=True)
|
222
|
+
self.logger.debug("Procedural directory: %s.", self.procedural_dir)
|
223
|
+
|
215
224
|
self.info_save_path = os.path.join(self.map_directory, "generation_info.json")
|
216
225
|
self.logger.debug("Generation info save path: %s.", self.info_save_path)
|
217
226
|
|
@@ -251,11 +260,56 @@ class Texture(Component):
|
|
251
260
|
return layer
|
252
261
|
return None
|
253
262
|
|
254
|
-
def process(self):
|
263
|
+
def process(self) -> None:
|
264
|
+
"""Processes the data to generate textures."""
|
255
265
|
self._prepare_weights()
|
256
266
|
self._read_parameters()
|
257
267
|
self.draw()
|
258
268
|
self.rotate_textures()
|
269
|
+
self.copy_procedural()
|
270
|
+
|
271
|
+
def copy_procedural(self) -> None:
|
272
|
+
"""Copies some of the textures to use them as mask for procedural generation.
|
273
|
+
Creates an empty blockmask if it does not exist."""
|
274
|
+
blockmask_path = os.path.join(self.procedural_dir, "BLOCKMASK.png")
|
275
|
+
if not os.path.isfile(blockmask_path):
|
276
|
+
self.logger.debug("BLOCKMASK.png not found, creating an empty file.")
|
277
|
+
img = np.zeros((self.map_size, self.map_size), dtype=np.uint8)
|
278
|
+
cv2.imwrite(blockmask_path, img) # pylint: disable=no-member
|
279
|
+
|
280
|
+
pg_layers_by_type = defaultdict(list)
|
281
|
+
for layer in self.layers:
|
282
|
+
if layer.procedural:
|
283
|
+
# Get path to the original file.
|
284
|
+
texture_path = layer.get_preview_or_path(self._weights_dir)
|
285
|
+
for procedural_layer_name in layer.procedural:
|
286
|
+
pg_layers_by_type[procedural_layer_name].append(texture_path)
|
287
|
+
|
288
|
+
if not pg_layers_by_type:
|
289
|
+
self.logger.debug("No procedural layers found.")
|
290
|
+
return
|
291
|
+
|
292
|
+
for procedural_layer_name, texture_paths in pg_layers_by_type.items():
|
293
|
+
procedural_save_path = os.path.join(self.procedural_dir, f"{procedural_layer_name}.png")
|
294
|
+
if len(texture_paths) > 1:
|
295
|
+
# If there are more than one texture, merge them.
|
296
|
+
merged_texture = np.zeros((self.map_size, self.map_size), dtype=np.uint8)
|
297
|
+
for texture_path in texture_paths:
|
298
|
+
# pylint: disable=E1101
|
299
|
+
texture = cv2.imread(texture_path, cv2.IMREAD_UNCHANGED)
|
300
|
+
merged_texture[texture == 255] = 255
|
301
|
+
cv2.imwrite(procedural_save_path, merged_texture) # pylint: disable=no-member
|
302
|
+
self.logger.debug(
|
303
|
+
"Procedural file %s merged from %s textures.",
|
304
|
+
procedural_save_path,
|
305
|
+
len(texture_paths),
|
306
|
+
)
|
307
|
+
elif len(texture_paths) == 1:
|
308
|
+
# Otherwise, copy the texture.
|
309
|
+
shutil.copyfile(texture_paths[0], procedural_save_path)
|
310
|
+
self.logger.debug(
|
311
|
+
"Procedural file %s copied from %s.", procedural_save_path, texture_paths[0]
|
312
|
+
)
|
259
313
|
|
260
314
|
def rotate_textures(self) -> None:
|
261
315
|
"""Rotates textures of the layers which have tags."""
|
@@ -274,8 +328,6 @@ class Texture(Component):
|
|
274
328
|
output_height=self.map_size,
|
275
329
|
output_width=self.map_size,
|
276
330
|
)
|
277
|
-
else:
|
278
|
-
self.logger.warning("Layer path %s not found.", layer_path)
|
279
331
|
else:
|
280
332
|
self.logger.debug(
|
281
333
|
"Skipping rotation of layer %s because it has no tags.", layer.name
|
@@ -664,7 +716,9 @@ class Texture(Component):
|
|
664
716
|
is_fieds = info_layer == "fields"
|
665
717
|
try:
|
666
718
|
if self.map.custom_osm is not None:
|
667
|
-
|
719
|
+
with warnings.catch_warnings():
|
720
|
+
warnings.simplefilter("ignore", FutureWarning)
|
721
|
+
objects = ox.features_from_xml(self.map.custom_osm, tags=tags)
|
668
722
|
else:
|
669
723
|
objects = ox.features_from_bbox(bbox=self.new_bbox, tags=tags)
|
670
724
|
except Exception as e: # pylint: disable=W0718
|
@@ -723,6 +777,8 @@ class Texture(Component):
|
|
723
777
|
|
724
778
|
if not isinstance(padded_polygon, shapely.geometry.polygon.Polygon):
|
725
779
|
self.logger.warning("The padding value is too high, field will not padded.")
|
780
|
+
elif not list(padded_polygon.exterior.coords):
|
781
|
+
self.logger.warning("The padding value is too high, field will not padded.")
|
726
782
|
else:
|
727
783
|
polygon = padded_polygon
|
728
784
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: maps4fs
|
3
|
-
Version: 1.6.
|
3
|
+
Version: 1.6.7
|
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
|
@@ -131,6 +131,7 @@ docker run -d -p 8501:8501 --name maps4fs iwatkot/maps4fs
|
|
131
131
|
```
|
132
132
|
And open [http://localhost:8501](http://localhost:8501) in your browser.<br>
|
133
133
|
If you don't know how to use Docker, navigate to the [Docker version](#option-2-docker-version), it's really simple.<br>
|
134
|
+
Check out the [Docker FAQ](docs/FAQ_docker.md) if you have any questions.<br>
|
134
135
|
|
135
136
|
### 🤯 For developers
|
136
137
|
**Option 3:** Python package. Install the package using the following command:
|
@@ -185,6 +186,7 @@ Using it is easy and doesn't require any guides. Enjoy!
|
|
185
186
|
🗺️ Supported map sizes: 2x2, 4x4, 8x8, 16x16 km and any custom size.
|
186
187
|
⚙️ Advanced settings: enabled.
|
187
188
|
🖼️ Texture dissolving: enabled.
|
189
|
+
Check out the [Docker FAQ](docs/FAQ_docker.md) if you have any questions.<br>
|
188
190
|
You can launch the project with minimalistic UI in your browser using Docker. Follow these steps:
|
189
191
|
|
190
192
|
1. Install [Docker](https://docs.docker.com/get-docker/) for your OS.
|
@@ -421,6 +423,7 @@ Let's have a closer look at the fields:
|
|
421
423
|
- `background` - set it to True for the textures, which should have impact on the Background Terrain, by default it's used to subtract the water depth from the DEM and background terrain.
|
422
424
|
- `info_layer` - if the layer is saving some data in JSON format, this section will describe it's name in the JSON file. Used to find the needed JSON data, for example for fields it will be `fields` and as a value - list of polygon coordinates.
|
423
425
|
- `invisible` - set it to True for the textures, which should not be drawn in the files, but only to save the data in the JSON file (related to the previous field).
|
426
|
+
- `procedural` - is a list of corresponding files, that will be used for a procedural generation. For example: `"procedural": ["PG_meadow", "PG_acres"]` - means that the texture will be used for two procedural generation files: `masks/PG_meadow.png` and `masks/PG_acres.png`. Note, that the one procuderal name can be applied to multiple textures, in this case they will be merged into one mask.
|
424
427
|
|
425
428
|
## Background terrain
|
426
429
|
The tool now supports the generation of the background terrain. If you don't know what it is, here's a brief explanation. The background terrain is the world around the map. It's important to create it because if you don't, the map will look like it's floating in the void. The background terrain is a simple plane that can (and should) be textured to look fine.<br>
|
@@ -472,7 +475,7 @@ You can also apply some advanced settings to the map generation process.<br>
|
|
472
475
|
|
473
476
|
### DEM Advanced settings
|
474
477
|
|
475
|
-
- 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).
|
478
|
+
- 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). To match the in-game heights with SRTM Data provider, the recommended value is 255 (if easy mode is disabled), but depending on the place, you will need to play with both multiplier and the height scale in Giants Editor to find the best values.
|
476
479
|
|
477
480
|
- 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.
|
478
481
|
|
@@ -505,6 +508,18 @@ decimation will be, which means the higher it will affect the geometry. It's not
|
|
505
508
|
|
506
509
|
- Add Farmyards - if enabled, the tool will create farmlands from the regions that are marked as farmyards in the OSM data. Those farmlands will not have fields and also will not be drawn on textures. By default, it's turned off.
|
507
510
|
|
511
|
+
- Base grass - you can select which plant will be used as a base grass on the map.
|
512
|
+
|
513
|
+
- Plants island minimum size - when random plants are enabled, the generator will add islands of differents plants to the map and choose the random size of those island between the minimum and maximum values. This one is the minimum size of the island in meters.
|
514
|
+
|
515
|
+
- Plants island maximum size - it's the same as above, but for the maximum size of the island in meters.
|
516
|
+
|
517
|
+
- Plants island vertex count - the number of vertices in the island. The higher the value, the more detailed the island will be. Note, that high values will turn the smoothed island into geometric madness.
|
518
|
+
|
519
|
+
- Plants insland rounding radius - used to round the vertices of the island. The higher the value, the more rounded the island will be.
|
520
|
+
|
521
|
+
- Plants island percent - defines the relation between the map size and the number of islands of plants. For example, if set to 100% for map size of 2048 will be added 2048 islands of plants.
|
522
|
+
|
508
523
|
### I3D Advanced settings
|
509
524
|
|
510
525
|
- 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.
|
@@ -538,6 +553,8 @@ The tool also supports the expert settings. Do not use them until you read the d
|
|
538
553
|
|
539
554
|
- Show schemas - you'll be able to edit or define your own texture or tree schemas. It's useful if you want to add some custom textures or trees to the map. Refer to the [Texture schema](#texture-schema) section to learn more about the schema structure. Any incorrect value here will lead to the completely broken map.
|
540
555
|
|
556
|
+
- Upload custom background image - if you have an image, which represents the map and background terrain you can use it for generation. Note, that the image should meet the following requirements: 1:1 aspect ratio, size = map size + 2048 * 2, it should be uint16 (unsigned 16-bit integer) grayscale (single channel) image. The image should be in the PNG format. If any of the requirements are not met, the tool raises an error. If you're using rotation, the image should already be rotated.
|
557
|
+
|
541
558
|
## Resources
|
542
559
|
In this section, you'll find a list of the resources that you need to create a map for the Farming Simulator.<br>
|
543
560
|
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,18 +1,18 @@
|
|
1
1
|
maps4fs/__init__.py,sha256=WbT36EzJ_74GN0RUUrLIYECdSdtRiZaxKl17KUt7pjA,492
|
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=tV4UXvtkNN-OSvv6ujp4jFWRU1xGBgEvSakVGZ1H4nc,24877
|
5
|
+
maps4fs/generator/component.py,sha256=vn_ThQw3OTcloqYuJWC7vghAvIAnwJsybEm7qMwvsZk,21356
|
6
6
|
maps4fs/generator/config.py,sha256=0QmK052B8bxyHVhg3jzCORLfOBMMmqVfhhbqXKf6OMk,4383
|
7
7
|
maps4fs/generator/dem.py,sha256=20gx0dzX0LyO6ipvDitst-BwGfcKogFqgQf9Q2qMH5U,10933
|
8
|
-
maps4fs/generator/game.py,sha256=
|
9
|
-
maps4fs/generator/grle.py,sha256=
|
10
|
-
maps4fs/generator/i3d.py,sha256=
|
11
|
-
maps4fs/generator/map.py,sha256=
|
8
|
+
maps4fs/generator/game.py,sha256=Nf5r2ubV4YVAVHGzJyhbF2GnOC0qV3HlHYIZBCWciHs,7992
|
9
|
+
maps4fs/generator/grle.py,sha256=hcbVBJ4j_Zr2QvEVo2cYNh2jARVXp_X3onifBtp9Zxs,20922
|
10
|
+
maps4fs/generator/i3d.py,sha256=pUyHKWKcw43xVCf3Y8iabtbQba05LYxMHi8vziGksIA,24843
|
11
|
+
maps4fs/generator/map.py,sha256=P8wHrCLhLcv2W5zJmMGjpM1TAMR8c7yVFzm_n-5ZTHQ,10084
|
12
12
|
maps4fs/generator/qgis.py,sha256=Es8hLuqN_KH8lDfnJE6He2rWYbAKJ3RGPn-o87S6CPI,6116
|
13
13
|
maps4fs/generator/satellite.py,sha256=_7RcuNmR1mjxEJWMDsjnzKUIqWxnGUn50XtjB7HmSPg,3661
|
14
|
-
maps4fs/generator/settings.py,sha256=
|
15
|
-
maps4fs/generator/texture.py,sha256=
|
14
|
+
maps4fs/generator/settings.py,sha256=3ASf3hW1nkGt8_3IOvKIKNUd6XAHYTAA8FquuhpSUlU,5668
|
15
|
+
maps4fs/generator/texture.py,sha256=gIXCHU1vT3evbkaXAV9gLUrgI1wH3xJLgWAtZgFruj0,34013
|
16
16
|
maps4fs/generator/dtm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
17
17
|
maps4fs/generator/dtm/dtm.py,sha256=azy-RWsc5PgenKDtgG0lrddMwWEw1hYzdng9V8zphMk,9167
|
18
18
|
maps4fs/generator/dtm/srtm.py,sha256=2-pX6bWrJX6gr8IM7ueX6mm_PW7_UQ58MtdzDHae2OQ,9030
|
@@ -20,8 +20,8 @@ maps4fs/generator/dtm/usgs.py,sha256=ZTi10RNDA3EBrsVg2ZoYrdN4uqiG1Jvk7FzdcKdgNkU
|
|
20
20
|
maps4fs/toolbox/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
|
21
21
|
maps4fs/toolbox/background.py,sha256=9BXWNqs_n3HgqDiPztWylgYk_QM4YgBpe6_ZNQAWtSc,2154
|
22
22
|
maps4fs/toolbox/dem.py,sha256=z9IPFNmYbjiigb3t02ZenI3Mo8odd19c5MZbjDEovTo,3525
|
23
|
-
maps4fs-1.6.
|
24
|
-
maps4fs-1.6.
|
25
|
-
maps4fs-1.6.
|
26
|
-
maps4fs-1.6.
|
27
|
-
maps4fs-1.6.
|
23
|
+
maps4fs-1.6.7.dist-info/LICENSE.md,sha256=pTKD_oUexcn-yccFCTrMeLkZy0ifLRa-VNcDLqLZaIw,10749
|
24
|
+
maps4fs-1.6.7.dist-info/METADATA,sha256=uNK0tEiWExntm4F61lyfM4q4qY-qw4naYg7iFE6Kb5M,38680
|
25
|
+
maps4fs-1.6.7.dist-info/WHEEL,sha256=A3WOREP4zgxI0fKrHUG8DC8013e3dK3n7a6HDbcEIwE,91
|
26
|
+
maps4fs-1.6.7.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
|
27
|
+
maps4fs-1.6.7.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|