maps4fs 1.1.9__py3-none-any.whl → 1.2.0__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/game.py +15 -0
- maps4fs/generator/grle.py +129 -0
- maps4fs/generator/i3d.py +15 -0
- maps4fs/generator/texture.py +12 -0
- {maps4fs-1.1.9.dist-info → maps4fs-1.2.0.dist-info}/METADATA +4 -1
- {maps4fs-1.1.9.dist-info → maps4fs-1.2.0.dist-info}/RECORD +9 -9
- {maps4fs-1.1.9.dist-info → maps4fs-1.2.0.dist-info}/LICENSE.md +0 -0
- {maps4fs-1.1.9.dist-info → maps4fs-1.2.0.dist-info}/WHEEL +0 -0
- {maps4fs-1.1.9.dist-info → maps4fs-1.2.0.dist-info}/top_level.txt +0 -0
maps4fs/generator/game.py
CHANGED
@@ -36,6 +36,7 @@ class Game:
|
|
36
36
|
_map_template_path: str | None = None
|
37
37
|
_texture_schema: str | None = None
|
38
38
|
_grle_schema: str | None = None
|
39
|
+
_base_image: str | None = None
|
39
40
|
|
40
41
|
# Order matters! Some components depend on others.
|
41
42
|
components = [Texture, I3d, GRLE, Background, Config]
|
@@ -130,6 +131,19 @@ class Game:
|
|
130
131
|
str: The path to the weights directory."""
|
131
132
|
raise NotImplementedError
|
132
133
|
|
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
|
+
|
133
147
|
def i3d_file_path(self, map_directory: str) -> str:
|
134
148
|
"""Returns the path to the i3d file.
|
135
149
|
|
@@ -187,6 +201,7 @@ class FS25(Game):
|
|
187
201
|
_map_template_path = os.path.join(working_directory, "data", "fs25-map-template.zip")
|
188
202
|
_texture_schema = os.path.join(working_directory, "data", "fs25-texture-schema.json")
|
189
203
|
_grle_schema = os.path.join(working_directory, "data", "fs25-grle-schema.json")
|
204
|
+
_base_image = "base.png"
|
190
205
|
|
191
206
|
def dem_file_path(self, map_directory: str) -> str:
|
192
207
|
"""Returns the path to the DEM file.
|
maps4fs/generator/grle.py
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
import json
|
4
4
|
import os
|
5
|
+
from random import choice, randint
|
5
6
|
from xml.etree import ElementTree as ET
|
6
7
|
|
7
8
|
import cv2
|
@@ -9,6 +10,9 @@ import numpy as np
|
|
9
10
|
|
10
11
|
from maps4fs.generator.component import Component
|
11
12
|
|
13
|
+
ISLAND_SIZE_MIN = 10
|
14
|
+
ISLAND_SIZE_MAX = 200
|
15
|
+
|
12
16
|
|
13
17
|
# pylint: disable=W0223
|
14
18
|
class GRLE(Component):
|
@@ -76,6 +80,11 @@ class GRLE(Component):
|
|
76
80
|
self.logger.warning("Invalid InfoLayer schema: %s.", info_layer)
|
77
81
|
|
78
82
|
self._add_farmlands()
|
83
|
+
if self.game.code == "FS25":
|
84
|
+
self.logger.info("Game is %s, plants will be added.", self.game.code)
|
85
|
+
self._add_plants()
|
86
|
+
else:
|
87
|
+
self.logger.warning("Adding plants it's not supported for the %s.", self.game.code)
|
79
88
|
|
80
89
|
def previews(self) -> list[str]:
|
81
90
|
"""Returns a list of paths to the preview images (empty list).
|
@@ -184,3 +193,123 @@ class GRLE(Component):
|
|
184
193
|
self.logger.info(
|
185
194
|
"Farmlands added to the InfoLayer PNG file: %s.", info_layer_farmlands_path
|
186
195
|
)
|
196
|
+
|
197
|
+
def _add_plants(self) -> None:
|
198
|
+
"""Adds plants to the InfoLayer PNG file."""
|
199
|
+
# 1. Get the path to the densityMap_fruits.png.
|
200
|
+
# 2. Get the path to the base layer (grass).
|
201
|
+
# 3. Detect non-zero areas in the base layer (it's where the plants will be placed).
|
202
|
+
base_image_path = self.game.base_image_path(self.map_directory)
|
203
|
+
if not base_image_path or not os.path.isfile(base_image_path):
|
204
|
+
self.logger.warning("Base image not found in %s.", base_image_path)
|
205
|
+
return
|
206
|
+
|
207
|
+
density_map_fruit_path = os.path.join(
|
208
|
+
self.game.weights_dir_path(self.map_directory), "densityMap_fruits.png"
|
209
|
+
)
|
210
|
+
|
211
|
+
if not os.path.isfile(density_map_fruit_path):
|
212
|
+
self.logger.warning("Density map for fruits not found in %s.", density_map_fruit_path)
|
213
|
+
return
|
214
|
+
|
215
|
+
# Single channeled 8-bit image, where non-zero values (255) are where the grass is.
|
216
|
+
base_image = cv2.imread(base_image_path, cv2.IMREAD_UNCHANGED) # pylint: disable=no-member
|
217
|
+
|
218
|
+
# Density map of the fruits is 2X size of the base image, so we need to resize it.
|
219
|
+
# We'll resize the base image to make it bigger, so we can compare the values.
|
220
|
+
base_image = cv2.resize( # pylint: disable=no-member
|
221
|
+
base_image,
|
222
|
+
(base_image.shape[1] * 2, base_image.shape[0] * 2),
|
223
|
+
interpolation=cv2.INTER_NEAREST, # pylint: disable=no-member
|
224
|
+
)
|
225
|
+
|
226
|
+
# B and G channels remain the same (zeros), while we change the R channel.
|
227
|
+
possible_R_values = [33, 65, 97, 129, 161, 193, 225] # pylint: disable=C0103
|
228
|
+
|
229
|
+
# 1st approach: Change the non zero values in the base image to 33 (for debug).
|
230
|
+
# And use the base image as R channel in the density map.
|
231
|
+
|
232
|
+
# pylint: disable=no-member
|
233
|
+
def create_island_of_plants(image: np.ndarray, count: int) -> np.ndarray:
|
234
|
+
"""Create an island of plants in the image.
|
235
|
+
|
236
|
+
Arguments:
|
237
|
+
image (np.ndarray): The image where the island of plants will be created.
|
238
|
+
count (int): The number of islands of plants to create.
|
239
|
+
|
240
|
+
Returns:
|
241
|
+
np.ndarray: The image with the islands of plants.
|
242
|
+
"""
|
243
|
+
for _ in range(count):
|
244
|
+
# Randomly choose the value for the island.
|
245
|
+
plant_value = choice(possible_R_values)
|
246
|
+
# Randomly choose the size of the island.
|
247
|
+
island_size = randint(ISLAND_SIZE_MIN, ISLAND_SIZE_MAX)
|
248
|
+
# Randomly choose the position of the island.
|
249
|
+
# x = np.random.randint(0, image.shape[1] - island_size)
|
250
|
+
# y = np.random.randint(0, image.shape[0] - island_size)
|
251
|
+
x = randint(0, image.shape[1] - island_size)
|
252
|
+
y = randint(0, image.shape[0] - island_size)
|
253
|
+
|
254
|
+
# Randomly choose the shape of the island.
|
255
|
+
shapes = ["circle", "ellipse", "polygon"]
|
256
|
+
shape = choice(shapes)
|
257
|
+
|
258
|
+
try:
|
259
|
+
if shape == "circle":
|
260
|
+
center = (x + island_size // 2, y + island_size // 2)
|
261
|
+
radius = island_size // 2
|
262
|
+
cv2.circle(image, center, radius, plant_value, -1) # type: ignore
|
263
|
+
elif shape == "ellipse":
|
264
|
+
center = (x + island_size // 2, y + island_size // 2)
|
265
|
+
axes = (island_size // 2, island_size // 4)
|
266
|
+
angle = 0
|
267
|
+
cv2.ellipse( # type: ignore
|
268
|
+
image, center, axes, angle, 0, 360, plant_value, -1
|
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
|
278
|
+
except Exception: # pylint: disable=W0703
|
279
|
+
continue
|
280
|
+
|
281
|
+
return image
|
282
|
+
|
283
|
+
updated_base_image = base_image.copy()
|
284
|
+
# Set all the non-zero values to 33.
|
285
|
+
updated_base_image[base_image != 0] = 33
|
286
|
+
|
287
|
+
# Add islands of plants to the base image.
|
288
|
+
island_count = self.map_size
|
289
|
+
self.logger.info("Adding %s islands of plants to the base image.", island_count)
|
290
|
+
updated_base_image = create_island_of_plants(updated_base_image, island_count)
|
291
|
+
self.logger.debug("Islands of plants added to the base image.")
|
292
|
+
|
293
|
+
# Remove the values where the base image has zeros.
|
294
|
+
updated_base_image[base_image == 0] = 0
|
295
|
+
self.logger.debug("Removed the values where the base image has zeros.")
|
296
|
+
|
297
|
+
# Value of 33 represents the base grass plant.
|
298
|
+
# After painting it with base grass, we'll create multiple islands of different plants.
|
299
|
+
# On the final step, we'll remove all the values which in pixels
|
300
|
+
# where zerons in the original base image (so we don't paint grass where it should not be).
|
301
|
+
|
302
|
+
# Three channeled 8-bit image, where non-zero values are the
|
303
|
+
# different types of plants (only in the R channel).
|
304
|
+
density_map_fruits = cv2.imread(density_map_fruit_path, cv2.IMREAD_UNCHANGED)
|
305
|
+
self.logger.debug("Density map for fruits loaded, shape: %s.", density_map_fruits.shape)
|
306
|
+
|
307
|
+
# Put the updated base image as the B channel in the density map.
|
308
|
+
density_map_fruits[:, :, 0] = updated_base_image
|
309
|
+
self.logger.debug("Updated base image added as the B channel in the density map.")
|
310
|
+
|
311
|
+
# Save the updated density map.
|
312
|
+
# Ensure that order of channels is correct because CV2 uses BGR and we need RGB.
|
313
|
+
density_map_fruits = cv2.cvtColor(density_map_fruits, cv2.COLOR_BGR2RGB)
|
314
|
+
cv2.imwrite(density_map_fruit_path, density_map_fruits)
|
315
|
+
self.logger.info("Updated density map for fruits saved in %s.", density_map_fruit_path)
|
maps4fs/generator/i3d.py
CHANGED
@@ -85,6 +85,21 @@ class I3d(Component):
|
|
85
85
|
|
86
86
|
self.logger.debug("TerrainTransformGroup element updated in I3D file.")
|
87
87
|
|
88
|
+
sun_elem = map_elem.find(".//Light[@name='sun']")
|
89
|
+
|
90
|
+
if sun_elem is not None:
|
91
|
+
self.logger.debug("Sun element found in I3D file.")
|
92
|
+
|
93
|
+
distance = self.map_size // 2
|
94
|
+
|
95
|
+
sun_elem.set("lastShadowMapSplitBboxMin", f"-{distance},-128,-{distance}")
|
96
|
+
sun_elem.set("lastShadowMapSplitBboxMax", f"{distance},148,{distance}")
|
97
|
+
|
98
|
+
self.logger.debug(
|
99
|
+
"Sun BBOX updated with half of the map size: %s.",
|
100
|
+
distance,
|
101
|
+
)
|
102
|
+
|
88
103
|
if self.map_size > 4096:
|
89
104
|
displacement_layer = terrain_elem.find(".//DisplacementLayer") # pylint: disable=W0631
|
90
105
|
|
maps4fs/generator/texture.py
CHANGED
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
5
5
|
import json
|
6
6
|
import os
|
7
7
|
import re
|
8
|
+
import shutil
|
8
9
|
from collections import defaultdict
|
9
10
|
from typing import Any, Callable, Generator, Optional
|
10
11
|
|
@@ -242,6 +243,17 @@ class Texture(Component):
|
|
242
243
|
"Skipping rotation of layer %s because it has no tags.", layer.name
|
243
244
|
)
|
244
245
|
|
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
|
+
|
245
257
|
# pylint: disable=W0201
|
246
258
|
def _read_parameters(self) -> None:
|
247
259
|
"""Reads map parameters from OSM data, such as:
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: maps4fs
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.2.0
|
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
|
@@ -68,6 +68,7 @@ Requires-Dist: pympler
|
|
68
68
|
🔄 Support map rotation 🆕<br>
|
69
69
|
🌾 Automatically generates fields 🆕<br>
|
70
70
|
🌽 Automatically generates farmlands 🆕<br>
|
71
|
+
🌿 Automatically generates decorative foliage 🆕<br>
|
71
72
|
🌍 Based on real-world data from OpenStreetMap<br>
|
72
73
|
🏞️ Generates height map using SRTM dataset<br>
|
73
74
|
📦 Provides a ready-to-use map template for the Giants Editor<br>
|
@@ -84,6 +85,8 @@ Requires-Dist: pympler
|
|
84
85
|
🛰️ Realistic background terrain with satellite images.<br><br>
|
85
86
|
<img src="https://github.com/user-attachments/assets/6e3c0e99-2cce-46ac-82db-5cb60bba7a30"><br>
|
86
87
|
📐 Perfectly aligned background terrain.<br><br>
|
88
|
+
<img src="https://github.com/user-attachments/assets/5764b2ec-e626-426f-9f5d-beb12ba95133"><br>
|
89
|
+
🌿 Automatically generates decorative foliage.<br><br>
|
87
90
|
<img src="https://github.com/user-attachments/assets/80e5923c-22c7-4dc0-8906-680902511f3a"><br>
|
88
91
|
🗒️ True-to-life blueprints for fast and precise modding.<br><br>
|
89
92
|
<img width="480" src="https://github.com/user-attachments/assets/1a8802d2-6a3b-4bfa-af2b-7c09478e199b"><br>
|
@@ -5,17 +5,17 @@ maps4fs/generator/background.py,sha256=dnUkS1atEqqz_ryKkcfP_K9ZcwMHZVs97y-twZMpD
|
|
5
5
|
maps4fs/generator/component.py,sha256=sihh2S35q0o38leEU-dpi0is6kYCuxXiWUISAtiQErM,17351
|
6
6
|
maps4fs/generator/config.py,sha256=b7qY0luC-_WM_c72Ohtlf4FrB37X5cALInbestSdUsw,4382
|
7
7
|
maps4fs/generator/dem.py,sha256=rc7ADzjvlZzStOqagsWW0Vrm9-X86aPpoR1RhBF_-OE,16025
|
8
|
-
maps4fs/generator/game.py,sha256=
|
9
|
-
maps4fs/generator/grle.py,sha256=
|
10
|
-
maps4fs/generator/i3d.py,sha256=
|
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
11
|
maps4fs/generator/map.py,sha256=7UqLjDZgoY6M0ZxX5Q4Rjee2UGWZ64a3tGyr8B24UO0,4863
|
12
12
|
maps4fs/generator/qgis.py,sha256=Es8hLuqN_KH8lDfnJE6He2rWYbAKJ3RGPn-o87S6CPI,6116
|
13
|
-
maps4fs/generator/texture.py,sha256=
|
13
|
+
maps4fs/generator/texture.py,sha256=OaubSDheXlZOSfCFsROLDCXDNPeSrCja7ahldLBArWs,26550
|
14
14
|
maps4fs/toolbox/__init__.py,sha256=zZMLEkGzb4z0xql650gOtGSvcgX58DnJ2yN3vC2daRk,43
|
15
15
|
maps4fs/toolbox/background.py,sha256=9BXWNqs_n3HgqDiPztWylgYk_QM4YgBpe6_ZNQAWtSc,2154
|
16
16
|
maps4fs/toolbox/dem.py,sha256=z9IPFNmYbjiigb3t02ZenI3Mo8odd19c5MZbjDEovTo,3525
|
17
|
-
maps4fs-1.
|
18
|
-
maps4fs-1.
|
19
|
-
maps4fs-1.
|
20
|
-
maps4fs-1.
|
21
|
-
maps4fs-1.
|
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
|