maps4fs 1.2.0__py3-none-any.whl → 1.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- maps4fs/generator/background.py +1 -0
- maps4fs/generator/component.py +4 -0
- maps4fs/generator/game.py +15 -15
- maps4fs/generator/grle.py +107 -33
- maps4fs/generator/i3d.py +148 -1
- maps4fs/generator/map.py +16 -1
- maps4fs/generator/texture.py +28 -20
- {maps4fs-1.2.0.dist-info → maps4fs-1.2.1.dist-info}/METADATA +4 -1
- maps4fs-1.2.1.dist-info/RECORD +21 -0
- maps4fs-1.2.0.dist-info/RECORD +0 -21
- {maps4fs-1.2.0.dist-info → maps4fs-1.2.1.dist-info}/LICENSE.md +0 -0
- {maps4fs-1.2.0.dist-info → maps4fs-1.2.1.dist-info}/WHEEL +0 -0
- {maps4fs-1.2.0.dist-info → maps4fs-1.2.1.dist-info}/top_level.txt +0 -0
maps4fs/generator/background.py
CHANGED
maps4fs/generator/component.py
CHANGED
@@ -17,6 +17,7 @@ from maps4fs.generator.qgis import save_scripts
|
|
17
17
|
|
18
18
|
if TYPE_CHECKING:
|
19
19
|
from maps4fs.generator.game import Game
|
20
|
+
from maps4fs.generator.map import Map
|
20
21
|
|
21
22
|
|
22
23
|
# pylint: disable=R0801, R0903, R0902, R0904
|
@@ -25,6 +26,7 @@ class Component:
|
|
25
26
|
|
26
27
|
Arguments:
|
27
28
|
game (Game): The game instance for which the map is generated.
|
29
|
+
map (Map): The map instance for which the component is generated.
|
28
30
|
coordinates (tuple[float, float]): The latitude and longitude of the center of the map.
|
29
31
|
map_size (int): The size of the map in pixels.
|
30
32
|
map_rotated_size (int): The size of the map in pixels after rotation.
|
@@ -37,6 +39,7 @@ class Component:
|
|
37
39
|
def __init__(
|
38
40
|
self,
|
39
41
|
game: Game,
|
42
|
+
map: Map, # pylint: disable=W0622
|
40
43
|
coordinates: tuple[float, float],
|
41
44
|
map_size: int,
|
42
45
|
map_rotated_size: int,
|
@@ -46,6 +49,7 @@ class Component:
|
|
46
49
|
**kwargs, # pylint: disable=W0613, R0913, R0917
|
47
50
|
):
|
48
51
|
self.game = game
|
52
|
+
self.map = map
|
49
53
|
self.coordinates = coordinates
|
50
54
|
self.map_size = map_size
|
51
55
|
self.map_rotated_size = map_rotated_size
|
maps4fs/generator/game.py
CHANGED
@@ -36,7 +36,7 @@ class Game:
|
|
36
36
|
_map_template_path: str | None = None
|
37
37
|
_texture_schema: str | None = None
|
38
38
|
_grle_schema: str | None = None
|
39
|
-
|
39
|
+
_tree_schema: str | None = None
|
40
40
|
|
41
41
|
# Order matters! Some components depend on others.
|
42
42
|
components = [Texture, I3d, GRLE, Background, Config]
|
@@ -110,6 +110,19 @@ class Game:
|
|
110
110
|
raise ValueError("GRLE layers schema path not set.")
|
111
111
|
return self._grle_schema
|
112
112
|
|
113
|
+
@property
|
114
|
+
def tree_schema(self) -> str:
|
115
|
+
"""Returns the path to the tree layers schema file.
|
116
|
+
|
117
|
+
Raises:
|
118
|
+
ValueError: If the tree layers schema path is not set.
|
119
|
+
|
120
|
+
Returns:
|
121
|
+
str: The path to the tree layers schema file."""
|
122
|
+
if not self._tree_schema:
|
123
|
+
raise ValueError("Tree layers schema path not set.")
|
124
|
+
return self._tree_schema
|
125
|
+
|
113
126
|
def dem_file_path(self, map_directory: str) -> str:
|
114
127
|
"""Returns the path to the DEM file.
|
115
128
|
|
@@ -131,19 +144,6 @@ class Game:
|
|
131
144
|
str: The path to the weights directory."""
|
132
145
|
raise NotImplementedError
|
133
146
|
|
134
|
-
def base_image_path(self, map_directory: str) -> str | None:
|
135
|
-
"""Returns the path to the base density map image.
|
136
|
-
|
137
|
-
Arguments:
|
138
|
-
map_directory (str): The path to the map directory.
|
139
|
-
|
140
|
-
Returns:
|
141
|
-
str: The path to the base density map image or None if not set.
|
142
|
-
"""
|
143
|
-
if self._base_image:
|
144
|
-
return os.path.join(self.weights_dir_path(map_directory), self._base_image)
|
145
|
-
return None
|
146
|
-
|
147
147
|
def i3d_file_path(self, map_directory: str) -> str:
|
148
148
|
"""Returns the path to the i3d file.
|
149
149
|
|
@@ -201,7 +201,7 @@ class FS25(Game):
|
|
201
201
|
_map_template_path = os.path.join(working_directory, "data", "fs25-map-template.zip")
|
202
202
|
_texture_schema = os.path.join(working_directory, "data", "fs25-texture-schema.json")
|
203
203
|
_grle_schema = os.path.join(working_directory, "data", "fs25-grle-schema.json")
|
204
|
-
|
204
|
+
_tree_schema = os.path.join(working_directory, "data", "fs25-tree-schema.json")
|
205
205
|
|
206
206
|
def dem_file_path(self, map_directory: str) -> str:
|
207
207
|
"""Returns the path to the DEM file.
|
maps4fs/generator/grle.py
CHANGED
@@ -7,11 +7,16 @@ from xml.etree import ElementTree as ET
|
|
7
7
|
|
8
8
|
import cv2
|
9
9
|
import numpy as np
|
10
|
+
from shapely.geometry import Polygon # type: ignore
|
10
11
|
|
11
12
|
from maps4fs.generator.component import Component
|
13
|
+
from maps4fs.generator.texture import Texture
|
12
14
|
|
13
15
|
ISLAND_SIZE_MIN = 10
|
14
16
|
ISLAND_SIZE_MAX = 200
|
17
|
+
ISLAND_DISTORTION = 0.3
|
18
|
+
ISLAND_VERTEX_COUNT = 30
|
19
|
+
ISLAND_ROUNDING_RADIUS = 15
|
15
20
|
|
16
21
|
|
17
22
|
# pylint: disable=W0223
|
@@ -194,34 +199,70 @@ class GRLE(Component):
|
|
194
199
|
"Farmlands added to the InfoLayer PNG file: %s.", info_layer_farmlands_path
|
195
200
|
)
|
196
201
|
|
202
|
+
# pylint: disable=R0915
|
197
203
|
def _add_plants(self) -> None:
|
198
204
|
"""Adds plants to the InfoLayer PNG file."""
|
199
205
|
# 1. Get the path to the densityMap_fruits.png.
|
200
206
|
# 2. Get the path to the base layer (grass).
|
201
207
|
# 3. Detect non-zero areas in the base layer (it's where the plants will be placed).
|
202
|
-
|
203
|
-
if not
|
204
|
-
self.logger.warning("
|
208
|
+
texture_component: Texture | None = self.map.get_component("Texture") # type: ignore
|
209
|
+
if not texture_component:
|
210
|
+
self.logger.warning("Texture component not found in the map.")
|
211
|
+
return
|
212
|
+
|
213
|
+
grass_layer = texture_component.get_layer_by_usage("grass")
|
214
|
+
if not grass_layer:
|
215
|
+
self.logger.warning("Grass layer not found in the texture component.")
|
216
|
+
return
|
217
|
+
|
218
|
+
weights_directory = self.game.weights_dir_path(self.map_directory)
|
219
|
+
grass_image_path = grass_layer.get_preview_or_path(weights_directory)
|
220
|
+
self.logger.debug("Grass image path: %s.", grass_image_path)
|
221
|
+
|
222
|
+
forest_layer = texture_component.get_layer_by_usage("forest")
|
223
|
+
forest_image = None
|
224
|
+
if forest_layer:
|
225
|
+
forest_image_path = forest_layer.get_preview_or_path(weights_directory)
|
226
|
+
self.logger.debug("Forest image path: %s.", forest_image_path)
|
227
|
+
if forest_image_path:
|
228
|
+
# pylint: disable=no-member
|
229
|
+
forest_image = cv2.imread(forest_image_path, cv2.IMREAD_UNCHANGED)
|
230
|
+
|
231
|
+
if not grass_image_path or not os.path.isfile(grass_image_path):
|
232
|
+
self.logger.warning("Base image not found in %s.", grass_image_path)
|
205
233
|
return
|
206
234
|
|
207
235
|
density_map_fruit_path = os.path.join(
|
208
236
|
self.game.weights_dir_path(self.map_directory), "densityMap_fruits.png"
|
209
237
|
)
|
210
238
|
|
239
|
+
self.logger.debug("Density map for fruits path: %s.", density_map_fruit_path)
|
240
|
+
|
211
241
|
if not os.path.isfile(density_map_fruit_path):
|
212
242
|
self.logger.warning("Density map for fruits not found in %s.", density_map_fruit_path)
|
213
243
|
return
|
214
244
|
|
215
245
|
# Single channeled 8-bit image, where non-zero values (255) are where the grass is.
|
216
|
-
|
246
|
+
grass_image = cv2.imread( # pylint: disable=no-member
|
247
|
+
grass_image_path, cv2.IMREAD_UNCHANGED # pylint: disable=no-member
|
248
|
+
)
|
217
249
|
|
218
250
|
# Density map of the fruits is 2X size of the base image, so we need to resize it.
|
219
251
|
# We'll resize the base image to make it bigger, so we can compare the values.
|
220
|
-
|
221
|
-
|
222
|
-
(
|
252
|
+
grass_image = cv2.resize( # pylint: disable=no-member
|
253
|
+
grass_image,
|
254
|
+
(grass_image.shape[1] * 2, grass_image.shape[0] * 2),
|
223
255
|
interpolation=cv2.INTER_NEAREST, # pylint: disable=no-member
|
224
256
|
)
|
257
|
+
if forest_image is not None:
|
258
|
+
forest_image = cv2.resize( # pylint: disable=no-member
|
259
|
+
forest_image,
|
260
|
+
(forest_image.shape[1] * 2, forest_image.shape[0] * 2),
|
261
|
+
interpolation=cv2.INTER_NEAREST, # pylint: disable=no-member
|
262
|
+
)
|
263
|
+
|
264
|
+
# Add non zero values from the forest image to the grass image.
|
265
|
+
grass_image[forest_image != 0] = 255
|
225
266
|
|
226
267
|
# B and G channels remain the same (zeros), while we change the R channel.
|
227
268
|
possible_R_values = [33, 65, 97, 129, 161, 193, 225] # pylint: disable=C0103
|
@@ -252,46 +293,79 @@ class GRLE(Component):
|
|
252
293
|
y = randint(0, image.shape[0] - island_size)
|
253
294
|
|
254
295
|
# Randomly choose the shape of the island.
|
255
|
-
shapes = ["circle", "ellipse", "polygon"]
|
256
|
-
shape = choice(shapes)
|
296
|
+
# shapes = ["circle", "ellipse", "polygon"]
|
297
|
+
# shape = choice(shapes)
|
257
298
|
|
258
299
|
try:
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
elif shape == "polygon":
|
271
|
-
nodes_count = randint(20, 50)
|
272
|
-
nodes = []
|
273
|
-
for _ in range(nodes_count):
|
274
|
-
node = (randint(x, x + island_size), randint(y, y + island_size))
|
275
|
-
nodes.append(node)
|
276
|
-
nodes = np.array(nodes, np.int32) # type: ignore
|
277
|
-
cv2.fillPoly(image, [nodes], plant_value) # type: ignore
|
300
|
+
polygon_points = get_rounded_polygon(
|
301
|
+
num_vertices=ISLAND_VERTEX_COUNT,
|
302
|
+
center=(x + island_size // 2, y + island_size // 2),
|
303
|
+
radius=island_size // 2,
|
304
|
+
rounding_radius=ISLAND_ROUNDING_RADIUS,
|
305
|
+
)
|
306
|
+
if not polygon_points:
|
307
|
+
continue
|
308
|
+
|
309
|
+
nodes = np.array(polygon_points, np.int32) # type: ignore
|
310
|
+
cv2.fillPoly(image, [nodes], plant_value) # type: ignore
|
278
311
|
except Exception: # pylint: disable=W0703
|
279
312
|
continue
|
280
313
|
|
281
314
|
return image
|
282
315
|
|
283
|
-
|
316
|
+
def get_rounded_polygon(
|
317
|
+
num_vertices: int, center: tuple[int, int], radius: int, rounding_radius: int
|
318
|
+
) -> list[tuple[int, int]] | None:
|
319
|
+
"""Get a randomly rounded polygon.
|
320
|
+
|
321
|
+
Arguments:
|
322
|
+
num_vertices (int): The number of vertices of the polygon.
|
323
|
+
center (tuple[int, int]): The center of the polygon.
|
324
|
+
radius (int): The radius of the polygon.
|
325
|
+
rounding_radius (int): The rounding radius of the polygon.
|
326
|
+
|
327
|
+
Returns:
|
328
|
+
list[tuple[int, int]] | None: The rounded polygon.
|
329
|
+
"""
|
330
|
+
angle_offset = np.pi / num_vertices
|
331
|
+
angles = np.linspace(0, 2 * np.pi, num_vertices, endpoint=False) + angle_offset
|
332
|
+
random_angles = angles + np.random.uniform(
|
333
|
+
-ISLAND_DISTORTION, ISLAND_DISTORTION, num_vertices
|
334
|
+
) # Add randomness to angles
|
335
|
+
random_radii = radius + np.random.uniform(
|
336
|
+
-radius * ISLAND_DISTORTION, radius * ISLAND_DISTORTION, num_vertices
|
337
|
+
) # Add randomness to radii
|
338
|
+
|
339
|
+
points = [
|
340
|
+
(center[0] + np.cos(a) * r, center[1] + np.sin(a) * r)
|
341
|
+
for a, r in zip(random_angles, random_radii)
|
342
|
+
]
|
343
|
+
polygon = Polygon(points)
|
344
|
+
buffered_polygon = polygon.buffer(rounding_radius, resolution=16)
|
345
|
+
rounded_polygon = list(buffered_polygon.exterior.coords)
|
346
|
+
if not rounded_polygon:
|
347
|
+
return None
|
348
|
+
return rounded_polygon
|
349
|
+
|
350
|
+
grass_image_copy = grass_image.copy()
|
351
|
+
if forest_image is not None:
|
352
|
+
# Add the forest layer to the base image, to merge the masks.
|
353
|
+
grass_image_copy[forest_image != 0] = 33
|
284
354
|
# Set all the non-zero values to 33.
|
285
|
-
|
355
|
+
grass_image_copy[grass_image != 0] = 33
|
286
356
|
|
287
357
|
# Add islands of plants to the base image.
|
288
358
|
island_count = self.map_size
|
289
359
|
self.logger.info("Adding %s islands of plants to the base image.", island_count)
|
290
|
-
|
360
|
+
grass_image_copy = create_island_of_plants(grass_image_copy, island_count)
|
291
361
|
self.logger.debug("Islands of plants added to the base image.")
|
292
362
|
|
363
|
+
# Sligtly reduce the size of the grass_image, that we'll use as mask.
|
364
|
+
kernel = np.ones((3, 3), np.uint8)
|
365
|
+
grass_image = cv2.erode(grass_image, kernel, iterations=1)
|
366
|
+
|
293
367
|
# Remove the values where the base image has zeros.
|
294
|
-
|
368
|
+
grass_image_copy[grass_image == 0] = 0
|
295
369
|
self.logger.debug("Removed the values where the base image has zeros.")
|
296
370
|
|
297
371
|
# Value of 33 represents the base grass plant.
|
@@ -305,7 +379,7 @@ class GRLE(Component):
|
|
305
379
|
self.logger.debug("Density map for fruits loaded, shape: %s.", density_map_fruits.shape)
|
306
380
|
|
307
381
|
# Put the updated base image as the B channel in the density map.
|
308
|
-
density_map_fruits[:, :, 0] =
|
382
|
+
density_map_fruits[:, :, 0] = grass_image_copy
|
309
383
|
self.logger.debug("Updated base image added as the B channel in the density map.")
|
310
384
|
|
311
385
|
# Save the updated density map.
|
maps4fs/generator/i3d.py
CHANGED
@@ -4,15 +4,23 @@ from __future__ import annotations
|
|
4
4
|
|
5
5
|
import json
|
6
6
|
import os
|
7
|
+
from random import choice, randint, uniform
|
8
|
+
from typing import Generator
|
7
9
|
from xml.etree import ElementTree as ET
|
8
10
|
|
11
|
+
import cv2
|
12
|
+
import numpy as np
|
13
|
+
|
9
14
|
from maps4fs.generator.component import Component
|
15
|
+
from maps4fs.generator.texture import Texture
|
10
16
|
|
11
17
|
DEFAULT_HEIGHT_SCALE = 2000
|
12
18
|
DISPLACEMENT_LAYER_SIZE_FOR_BIG_MAPS = 32768
|
13
19
|
DEFAULT_MAX_LOD_DISTANCE = 10000
|
14
20
|
DEFAULT_MAX_LOD_OCCLUDER_DISTANCE = 10000
|
15
|
-
NODE_ID_STARTING_VALUE =
|
21
|
+
NODE_ID_STARTING_VALUE = 2000
|
22
|
+
TREE_NODE_ID_STARTING_VALUE = 4000
|
23
|
+
DEFAULT_FOREST_DENSITY = 10
|
16
24
|
|
17
25
|
|
18
26
|
# pylint: disable=R0903
|
@@ -44,10 +52,14 @@ class I3d(Component):
|
|
44
52
|
self.logger.info("I3D file processing is not implemented for this game.")
|
45
53
|
self._map_i3d_path = None
|
46
54
|
|
55
|
+
self.forest_density = self.kwargs.get("forest_density", DEFAULT_FOREST_DENSITY)
|
56
|
+
self.logger.info("Forest density: %s.", self.forest_density)
|
57
|
+
|
47
58
|
def process(self) -> None:
|
48
59
|
"""Updates the map I3D file with the default settings."""
|
49
60
|
self._update_i3d_file()
|
50
61
|
self._add_fields()
|
62
|
+
self._add_forests()
|
51
63
|
|
52
64
|
def _get_tree(self) -> ET.ElementTree | None:
|
53
65
|
"""Returns the ElementTree instance of the map I3D file."""
|
@@ -316,3 +328,138 @@ class I3d(Component):
|
|
316
328
|
attribute_node.set("type", attr_type)
|
317
329
|
attribute_node.set("value", value)
|
318
330
|
return attribute_node
|
331
|
+
|
332
|
+
# pylint: disable=R0911
|
333
|
+
def _add_forests(self) -> None:
|
334
|
+
"""Adds forests to the map I3D file."""
|
335
|
+
try:
|
336
|
+
tree_schema_path = self.game.tree_schema
|
337
|
+
except ValueError:
|
338
|
+
self.logger.warning("Tree schema path not set for the Game %s.", self.game.code)
|
339
|
+
return
|
340
|
+
|
341
|
+
if not os.path.isfile(tree_schema_path):
|
342
|
+
self.logger.warning("Tree schema file was not found: %s.", tree_schema_path)
|
343
|
+
return
|
344
|
+
|
345
|
+
try:
|
346
|
+
with open(tree_schema_path, "r", encoding="utf-8") as tree_schema_file:
|
347
|
+
tree_schema: list[dict[str, str | int]] = json.load(tree_schema_file)
|
348
|
+
except json.JSONDecodeError as e:
|
349
|
+
self.logger.warning(
|
350
|
+
"Could not load tree schema from %s with error: %s", tree_schema_path, e
|
351
|
+
)
|
352
|
+
return
|
353
|
+
|
354
|
+
texture_component: Texture | None = self.map.get_component("Texture") # type: ignore
|
355
|
+
if not texture_component:
|
356
|
+
self.logger.warning("Texture component not found.")
|
357
|
+
return
|
358
|
+
|
359
|
+
forest_layer = texture_component.get_layer_by_usage("forest")
|
360
|
+
|
361
|
+
if not forest_layer:
|
362
|
+
self.logger.warning("Forest layer not found.")
|
363
|
+
return
|
364
|
+
|
365
|
+
weights_directory = self.game.weights_dir_path(self.map_directory)
|
366
|
+
forest_image_path = forest_layer.get_preview_or_path(weights_directory)
|
367
|
+
|
368
|
+
if not forest_image_path or not os.path.isfile(forest_image_path):
|
369
|
+
self.logger.warning("Forest image not found.")
|
370
|
+
return
|
371
|
+
|
372
|
+
tree = self._get_tree()
|
373
|
+
if tree is None:
|
374
|
+
return
|
375
|
+
|
376
|
+
# Find the <Scene> element in the I3D file.
|
377
|
+
root = tree.getroot()
|
378
|
+
scene_node = root.find(".//Scene")
|
379
|
+
if scene_node is None:
|
380
|
+
self.logger.warning("Scene element not found in I3D file.")
|
381
|
+
return
|
382
|
+
|
383
|
+
self.logger.debug("Scene element found in I3D file, starting to add forests.")
|
384
|
+
|
385
|
+
node_id = TREE_NODE_ID_STARTING_VALUE
|
386
|
+
|
387
|
+
# Create <TransformGroup name="trees" translation="0 400 0" nodeId="{node_id}"> element.
|
388
|
+
trees_node = ET.Element("TransformGroup")
|
389
|
+
trees_node.set("name", "trees")
|
390
|
+
trees_node.set("translation", "0 400 0")
|
391
|
+
trees_node.set("nodeId", str(node_id))
|
392
|
+
node_id += 1
|
393
|
+
|
394
|
+
# pylint: disable=no-member
|
395
|
+
forest_image = cv2.imread(forest_image_path, cv2.IMREAD_UNCHANGED)
|
396
|
+
|
397
|
+
tree_count = 0
|
398
|
+
for x, y in self.non_empty_pixels(forest_image, step=self.forest_density):
|
399
|
+
xcs, ycs = self.top_left_coordinates_to_center((x, y))
|
400
|
+
node_id += 1
|
401
|
+
|
402
|
+
rotation = randint(-180, 180)
|
403
|
+
xcs, ycs = self.randomize_coordinates((xcs, ycs), self.forest_density) # type: ignore
|
404
|
+
|
405
|
+
random_tree = choice(tree_schema)
|
406
|
+
tree_name = random_tree["name"]
|
407
|
+
tree_id = random_tree["reference_id"]
|
408
|
+
|
409
|
+
reference_node = ET.Element("ReferenceNode")
|
410
|
+
reference_node.set("name", tree_name) # type: ignore
|
411
|
+
reference_node.set("translation", f"{xcs} 0 {ycs}")
|
412
|
+
reference_node.set("rotation", f"0 {rotation} 0")
|
413
|
+
reference_node.set("referenceId", str(tree_id))
|
414
|
+
reference_node.set("nodeId", str(node_id))
|
415
|
+
|
416
|
+
trees_node.append(reference_node)
|
417
|
+
tree_count += 1
|
418
|
+
|
419
|
+
scene_node.append(trees_node)
|
420
|
+
self.logger.info("Added %s trees to the I3D file.", tree_count)
|
421
|
+
|
422
|
+
tree.write(self._map_i3d_path) # type: ignore
|
423
|
+
self.logger.info("Map I3D file saved to: %s.", self._map_i3d_path)
|
424
|
+
|
425
|
+
@staticmethod
|
426
|
+
def randomize_coordinates(coordinates: tuple[int, int], density: int) -> tuple[float, float]:
|
427
|
+
"""Randomizes the coordinates of the point with the given density.
|
428
|
+
|
429
|
+
Arguments:
|
430
|
+
coordinates (tuple[int, int]): The coordinates of the point.
|
431
|
+
density (int): The density of the randomization.
|
432
|
+
|
433
|
+
Returns:
|
434
|
+
tuple[float, float]: The randomized coordinates of the point.
|
435
|
+
"""
|
436
|
+
MAXIMUM_RELATIVE_SHIFT = 0.2 # pylint: disable=C0103
|
437
|
+
shift_range = density * MAXIMUM_RELATIVE_SHIFT
|
438
|
+
|
439
|
+
x_shift = uniform(-shift_range, shift_range)
|
440
|
+
y_shift = uniform(-shift_range, shift_range)
|
441
|
+
|
442
|
+
x, y = coordinates
|
443
|
+
x += x_shift # type: ignore
|
444
|
+
y += y_shift # type: ignore
|
445
|
+
|
446
|
+
return x, y
|
447
|
+
|
448
|
+
@staticmethod
|
449
|
+
def non_empty_pixels(
|
450
|
+
image: np.ndarray, step: int = 1
|
451
|
+
) -> Generator[tuple[int, int], None, None]:
|
452
|
+
"""Receives numpy array, which represents single-channeled image of uint8 type.
|
453
|
+
Yield coordinates of non-empty pixels (pixels with value greater than 0).
|
454
|
+
|
455
|
+
Arguments:
|
456
|
+
image (np.ndarray): The image to get non-empty pixels from.
|
457
|
+
step (int, optional): The step to iterate through the image. Defaults to 1.
|
458
|
+
|
459
|
+
Yields:
|
460
|
+
tuple[int, int]: The coordinates of non-empty pixels.
|
461
|
+
"""
|
462
|
+
for y, row in enumerate(image[::step]):
|
463
|
+
for x, value in enumerate(row[::step]):
|
464
|
+
if value > 0:
|
465
|
+
yield x * step, y * step
|
maps4fs/generator/map.py
CHANGED
@@ -74,6 +74,7 @@ class Map:
|
|
74
74
|
for game_component in self.game.components:
|
75
75
|
component = game_component(
|
76
76
|
self.game,
|
77
|
+
self,
|
77
78
|
self.coordinates,
|
78
79
|
self.size,
|
79
80
|
self.rotated_size,
|
@@ -82,6 +83,7 @@ class Map:
|
|
82
83
|
self.logger,
|
83
84
|
**self.kwargs,
|
84
85
|
)
|
86
|
+
self.components.append(component)
|
85
87
|
|
86
88
|
yield component.__class__.__name__
|
87
89
|
|
@@ -104,7 +106,20 @@ class Map:
|
|
104
106
|
e,
|
105
107
|
)
|
106
108
|
raise e
|
107
|
-
|
109
|
+
|
110
|
+
def get_component(self, component_name: str) -> Component | None:
|
111
|
+
"""Get component by name.
|
112
|
+
|
113
|
+
Arguments:
|
114
|
+
component_name (str): Name of the component.
|
115
|
+
|
116
|
+
Returns:
|
117
|
+
Component | None: Component instance or None if not found.
|
118
|
+
"""
|
119
|
+
for component in self.components:
|
120
|
+
if component.__class__.__name__ == component_name:
|
121
|
+
return component
|
122
|
+
return None
|
108
123
|
|
109
124
|
def previews(self) -> list[str]:
|
110
125
|
"""Get list of preview images.
|
maps4fs/generator/texture.py
CHANGED
@@ -5,7 +5,6 @@ from __future__ import annotations
|
|
5
5
|
import json
|
6
6
|
import os
|
7
7
|
import re
|
8
|
-
import shutil
|
9
8
|
from collections import defaultdict
|
10
9
|
from typing import Any, Callable, Generator, Optional
|
11
10
|
|
@@ -64,6 +63,7 @@ class Texture(Component):
|
|
64
63
|
exclude_weight: bool = False,
|
65
64
|
priority: int | None = None,
|
66
65
|
info_layer: str | None = None,
|
66
|
+
usage: str | None = None,
|
67
67
|
):
|
68
68
|
self.name = name
|
69
69
|
self.count = count
|
@@ -73,6 +73,7 @@ class Texture(Component):
|
|
73
73
|
self.exclude_weight = exclude_weight
|
74
74
|
self.priority = priority
|
75
75
|
self.info_layer = info_layer
|
76
|
+
self.usage = usage
|
76
77
|
|
77
78
|
def to_json(self) -> dict[str, str | list[str] | bool]: # type: ignore
|
78
79
|
"""Returns dictionary with layer data.
|
@@ -88,6 +89,7 @@ class Texture(Component):
|
|
88
89
|
"exclude_weight": self.exclude_weight,
|
89
90
|
"priority": self.priority,
|
90
91
|
"info_layer": self.info_layer,
|
92
|
+
"usage": self.usage,
|
91
93
|
}
|
92
94
|
|
93
95
|
data = {k: v for k, v in data.items() if v is not None}
|
@@ -118,29 +120,29 @@ class Texture(Component):
|
|
118
120
|
weight_postfix = "_weight" if not self.exclude_weight else ""
|
119
121
|
return os.path.join(weights_directory, f"{self.name}{idx}{weight_postfix}.png")
|
120
122
|
|
121
|
-
def path_preview(self,
|
123
|
+
def path_preview(self, weights_directory: str) -> str:
|
122
124
|
"""Returns path to the preview of the first texture of the layer.
|
123
125
|
|
124
126
|
Arguments:
|
125
|
-
|
127
|
+
weights_directory (str): Path to the directory with weights.
|
126
128
|
|
127
129
|
Returns:
|
128
130
|
str: Path to the preview.
|
129
131
|
"""
|
130
|
-
return self.path(
|
132
|
+
return self.path(weights_directory).replace(".png", "_preview.png")
|
131
133
|
|
132
|
-
def get_preview_or_path(self,
|
134
|
+
def get_preview_or_path(self, weights_directory: str) -> str:
|
133
135
|
"""Returns path to the preview of the first texture of the layer if it exists,
|
134
136
|
otherwise returns path to the texture.
|
135
137
|
|
136
138
|
Arguments:
|
137
|
-
|
139
|
+
weights_directory (str): Path to the directory with weights.
|
138
140
|
|
139
141
|
Returns:
|
140
142
|
str: Path to the preview or texture.
|
141
143
|
"""
|
142
|
-
preview_path = self.path_preview(
|
143
|
-
return preview_path if os.path.isfile(preview_path) else self.path(
|
144
|
+
preview_path = self.path_preview(weights_directory)
|
145
|
+
return preview_path if os.path.isfile(preview_path) else self.path(weights_directory)
|
144
146
|
|
145
147
|
def paths(self, weights_directory: str) -> list[str]:
|
146
148
|
"""Returns a list of paths to the textures of the layer.
|
@@ -213,6 +215,20 @@ class Texture(Component):
|
|
213
215
|
return layer
|
214
216
|
return None
|
215
217
|
|
218
|
+
def get_layer_by_usage(self, usage: str) -> Layer | None:
|
219
|
+
"""Returns layer by usage.
|
220
|
+
|
221
|
+
Arguments:
|
222
|
+
usage (str): Usage of the layer.
|
223
|
+
|
224
|
+
Returns:
|
225
|
+
Layer | None: Layer.
|
226
|
+
"""
|
227
|
+
for layer in self.layers:
|
228
|
+
if layer.usage == usage:
|
229
|
+
return layer
|
230
|
+
return None
|
231
|
+
|
216
232
|
def process(self):
|
217
233
|
self._prepare_weights()
|
218
234
|
self._read_parameters()
|
@@ -243,17 +259,6 @@ class Texture(Component):
|
|
243
259
|
"Skipping rotation of layer %s because it has no tags.", layer.name
|
244
260
|
)
|
245
261
|
|
246
|
-
base_path = self.game.base_image_path(self.map_directory)
|
247
|
-
if base_path:
|
248
|
-
base_layer = self.get_base_layer()
|
249
|
-
if base_layer:
|
250
|
-
base_layer_path = base_layer.get_preview_or_path(self._weights_dir)
|
251
|
-
self.logger.debug(
|
252
|
-
"Copying base layer to use it later for density map to %s.", base_path
|
253
|
-
)
|
254
|
-
# Make a copy of a base layer to the fruits density map.
|
255
|
-
shutil.copy(base_layer_path, base_path)
|
256
|
-
|
257
262
|
# pylint: disable=W0201
|
258
263
|
def _read_parameters(self) -> None:
|
259
264
|
"""Reads map parameters from OSM data, such as:
|
@@ -299,7 +304,10 @@ class Texture(Component):
|
|
299
304
|
Arguments:
|
300
305
|
layer (Layer): Layer with textures and tags.
|
301
306
|
"""
|
302
|
-
|
307
|
+
if layer.tags is None:
|
308
|
+
size = (self.map_size, self.map_size)
|
309
|
+
else:
|
310
|
+
size = (self.map_rotated_size, self.map_rotated_size)
|
303
311
|
postfix = "_weight.png" if not layer.exclude_weight else ".png"
|
304
312
|
if layer.count == 0:
|
305
313
|
filepaths = [os.path.join(self._weights_dir, layer.name + postfix)]
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: maps4fs
|
3
|
-
Version: 1.2.
|
3
|
+
Version: 1.2.1
|
4
4
|
Summary: Generate map templates for Farming Simulator from real places.
|
5
5
|
Author-email: iwatkot <iwatkot@gmail.com>
|
6
6
|
License: MIT License
|
@@ -69,6 +69,7 @@ Requires-Dist: pympler
|
|
69
69
|
🌾 Automatically generates fields 🆕<br>
|
70
70
|
🌽 Automatically generates farmlands 🆕<br>
|
71
71
|
🌿 Automatically generates decorative foliage 🆕<br>
|
72
|
+
🌲 Automatically generates forests 🆕<br>
|
72
73
|
🌍 Based on real-world data from OpenStreetMap<br>
|
73
74
|
🏞️ Generates height map using SRTM dataset<br>
|
74
75
|
📦 Provides a ready-to-use map template for the Giants Editor<br>
|
@@ -87,6 +88,8 @@ Requires-Dist: pympler
|
|
87
88
|
📐 Perfectly aligned background terrain.<br><br>
|
88
89
|
<img src="https://github.com/user-attachments/assets/5764b2ec-e626-426f-9f5d-beb12ba95133"><br>
|
89
90
|
🌿 Automatically generates decorative foliage.<br><br>
|
91
|
+
<img src="https://github.com/user-attachments/assets/27a5e541-a9f5-4504-b8d2-64aae9fb3e52"><br>
|
92
|
+
🌲 Automatically generates forests.<br><br>
|
90
93
|
<img src="https://github.com/user-attachments/assets/80e5923c-22c7-4dc0-8906-680902511f3a"><br>
|
91
94
|
🗒️ True-to-life blueprints for fast and precise modding.<br><br>
|
92
95
|
<img width="480" src="https://github.com/user-attachments/assets/1a8802d2-6a3b-4bfa-af2b-7c09478e199b"><br>
|
@@ -0,0 +1,21 @@
|
|
1
|
+
maps4fs/__init__.py,sha256=da4jmND2Ths9AffnkAKgzLHNkvKFOc_l21gJisPXqWY,155
|
2
|
+
maps4fs/logger.py,sha256=B-NEYpMjPAAqlV4VpfTi6nbBFnEABVtQOaYe6nMpidg,1489
|
3
|
+
maps4fs/generator/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
|
4
|
+
maps4fs/generator/background.py,sha256=fLWk7FSNL08gk3skHfi0iVchnKrYjnLLKAT1g_7sRzc,15907
|
5
|
+
maps4fs/generator/component.py,sha256=_d9rHmGh348KOMrLWR8rRDVsbZ2xwJQwZGIGvMIYXPM,17533
|
6
|
+
maps4fs/generator/config.py,sha256=b7qY0luC-_WM_c72Ohtlf4FrB37X5cALInbestSdUsw,4382
|
7
|
+
maps4fs/generator/dem.py,sha256=rc7ADzjvlZzStOqagsWW0Vrm9-X86aPpoR1RhBF_-OE,16025
|
8
|
+
maps4fs/generator/game.py,sha256=ZQeYzPzPB3CG41avdhNCyTZpHEeedqNBuAbNevTZuXg,7931
|
9
|
+
maps4fs/generator/grle.py,sha256=QNb1WAnTiGcFdCatu2XOWuLt2NwNBidkq6s8sY7bBgY,17245
|
10
|
+
maps4fs/generator/i3d.py,sha256=oK5pKjzvT-gydma5Q6CcDYTVODGxK7MIGajLrAV9JkU,18370
|
11
|
+
maps4fs/generator/map.py,sha256=lA1MNAcMwsDtsYxbwwm7DjwP3zraHKnri_xnLUu30j0,5326
|
12
|
+
maps4fs/generator/qgis.py,sha256=Es8hLuqN_KH8lDfnJE6He2rWYbAKJ3RGPn-o87S6CPI,6116
|
13
|
+
maps4fs/generator/texture.py,sha256=MSkM-rH_836l8zgq1WVcYJeYrUofWBpC8OKJglSmGGQ,26558
|
14
|
+
maps4fs/toolbox/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
|
15
|
+
maps4fs/toolbox/background.py,sha256=9BXWNqs_n3HgqDiPztWylgYk_QM4YgBpe6_ZNQAWtSc,2154
|
16
|
+
maps4fs/toolbox/dem.py,sha256=z9IPFNmYbjiigb3t02ZenI3Mo8odd19c5MZbjDEovTo,3525
|
17
|
+
maps4fs-1.2.1.dist-info/LICENSE.md,sha256=pTKD_oUexcn-yccFCTrMeLkZy0ifLRa-VNcDLqLZaIw,10749
|
18
|
+
maps4fs-1.2.1.dist-info/METADATA,sha256=43v3vGKA2z9uEd4r2M0wcebMv3fKEJESFbkUOshkZHU,29340
|
19
|
+
maps4fs-1.2.1.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
20
|
+
maps4fs-1.2.1.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
|
21
|
+
maps4fs-1.2.1.dist-info/RECORD,,
|
maps4fs-1.2.0.dist-info/RECORD
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
maps4fs/__init__.py,sha256=da4jmND2Ths9AffnkAKgzLHNkvKFOc_l21gJisPXqWY,155
|
2
|
-
maps4fs/logger.py,sha256=B-NEYpMjPAAqlV4VpfTi6nbBFnEABVtQOaYe6nMpidg,1489
|
3
|
-
maps4fs/generator/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
|
4
|
-
maps4fs/generator/background.py,sha256=dnUkS1atEqqz_ryKkcfP_K9ZcwMHZVs97y-twZMpD44,15881
|
5
|
-
maps4fs/generator/component.py,sha256=sihh2S35q0o38leEU-dpi0is6kYCuxXiWUISAtiQErM,17351
|
6
|
-
maps4fs/generator/config.py,sha256=b7qY0luC-_WM_c72Ohtlf4FrB37X5cALInbestSdUsw,4382
|
7
|
-
maps4fs/generator/dem.py,sha256=rc7ADzjvlZzStOqagsWW0Vrm9-X86aPpoR1RhBF_-OE,16025
|
8
|
-
maps4fs/generator/game.py,sha256=M_tN1oYrQd14kWYQnbzutHpb3sT8s3V_7Lxi1IPw8VE,7923
|
9
|
-
maps4fs/generator/grle.py,sha256=HBeD5ETAz8GxJte6BuXpIBhsUpFcCul4eBMoQXRTQnw,14110
|
10
|
-
maps4fs/generator/i3d.py,sha256=66GTg4e6-RlT0q1JFVd_4BB-aEXrxAWZgGz4YABbgxA,12819
|
11
|
-
maps4fs/generator/map.py,sha256=7UqLjDZgoY6M0ZxX5Q4Rjee2UGWZ64a3tGyr8B24UO0,4863
|
12
|
-
maps4fs/generator/qgis.py,sha256=Es8hLuqN_KH8lDfnJE6He2rWYbAKJ3RGPn-o87S6CPI,6116
|
13
|
-
maps4fs/generator/texture.py,sha256=OaubSDheXlZOSfCFsROLDCXDNPeSrCja7ahldLBArWs,26550
|
14
|
-
maps4fs/toolbox/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
|
15
|
-
maps4fs/toolbox/background.py,sha256=9BXWNqs_n3HgqDiPztWylgYk_QM4YgBpe6_ZNQAWtSc,2154
|
16
|
-
maps4fs/toolbox/dem.py,sha256=z9IPFNmYbjiigb3t02ZenI3Mo8odd19c5MZbjDEovTo,3525
|
17
|
-
maps4fs-1.2.0.dist-info/LICENSE.md,sha256=pTKD_oUexcn-yccFCTrMeLkZy0ifLRa-VNcDLqLZaIw,10749
|
18
|
-
maps4fs-1.2.0.dist-info/METADATA,sha256=-qsScy3CYrOynYBooNpoKs60FT0z50830j39yqlX46Q,29152
|
19
|
-
maps4fs-1.2.0.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
20
|
-
maps4fs-1.2.0.dist-info/top_level.txt,sha256=Ue9DSRlejRQRCaJueB0uLcKrWwsEq9zezfv5dI5mV1M,8
|
21
|
-
maps4fs-1.2.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|