diff --git a/Example Worlds/Arnis Example City Arnis.zip b/Example Worlds/Arnis Example City Arnis.zip deleted file mode 100644 index eb58156..0000000 Binary files a/Example Worlds/Arnis Example City Arnis.zip and /dev/null differ diff --git a/Example Worlds/Arnis Example City Krempe.zip b/Example Worlds/Arnis Example City Krempe.zip deleted file mode 100644 index e264bfd..0000000 Binary files a/Example Worlds/Arnis Example City Krempe.zip and /dev/null differ diff --git a/Example Worlds/Arnis Example City Wilster.zip b/Example Worlds/Arnis Example City Wilster.zip deleted file mode 100644 index 3c77c53..0000000 Binary files a/Example Worlds/Arnis Example City Wilster.zip and /dev/null differ diff --git a/README.md b/README.md index e905913..8f986b6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ +

+ +

+ # Arnis [![Testing](https://github.com/louis-e/arnis/actions/workflows/python-app.yml/badge.svg)](https://github.com/louis-e/arnis/actions/workflows/python-app.yml) -*Arnis - Generate cities from real life in Minecraft using Python*
-This open source project generates cities in Minecraft based on real-life locations, allowing users to explore and build in a virtual world that mirrors the real one. +This open source project generates any chosen location from the real world in Minecraft, allowing users to explore and build in a virtual world that mirrors the real one.
## :desktop_computer: Example @@ -10,44 +13,45 @@ This open source project generates cities in Minecraft based on real-life locati ## :floppy_disk: How it works ![CLI Generation](https://github.com/louis-e/arnis/blob/main/gitassets/cli-generation.gif?raw=true) -The raw data which we receive from the API *[(see FAQ)](#question-faq)* contains each element (buildings, walls, fountains, farmlands,...) with its respective corner coordinates as well as tags which describe the element. - -When you run the script, the following steps are performed automatically: -1. Scraping data from API -2. Find biggest and lowest latitude and longitude coordinate value -3. Make the length of each coordinate the same and remove the coordinate separator dot -4. Normalize data to start from 0 by subtracting the beforehand determined lowest value from each coordinate -5. Parsing data into uniform structure -6. Iterate through each element and set the corresponding ID *[(see FAQ)](#question-faq)* at coordinate in array -7. Reduce array size by focusing on the outer buildings -8. Add up landuse layer and the other generated data -9. Iterate through array to generate Minecraft world +The raw data obtained from the API *[(see FAQ)](#question-faq)* includes each element (buildings, walls, fountains, farmlands, etc.) with its respective corner coordinates (nodes) and descriptive tags. When you run the script, the following steps are performed automatically to generate a Minecraft world: -The last step is responsible for generating three dimensional structures like forests, houses or cemetery graves. +1. Scraping Data from API: The script fetches geospatial data from the Overpass Turbo API. +2. Determine Coordinate Extremes: Identifies the highest and lowest latitude and longitude values from the dataset. +3. Standardize Coordinate Lengths: Ensures all coordinates are of uniform length and removes the decimal separator. +4. Normalize Data: Adjusts all coordinates to start from zero by subtracting the previously determined lowest values. +5. Parse Data: Transforms the raw data into a standardized structure. +6. Assign IDs to Elements: Iterates through each element and assigns a corresponding ID *[(see FAQ)](#question-faq)* at each coordinate in an array. +7. Optimize Array Size: Focuses on the outermost buildings to reduce array size. +8. Integrate Landuse and Additional Data: Combines the landuse layer with other generated data. +9. Generate Minecraft World: Iterates through the array to create the Minecraft world, including 3D structures like forests, houses, and cemetery graves. ## :keyboard: Usage -```python3 arnis.py --city "Arnis" --state "Schleswig-Holstein" --country "Deutschland" --path "C:/Users/username/AppData/Roaming/.minecraft/saves/worldname"``` +```python3 arnis.py --bbox="min_lng,min_lat,max_lng,max_lat" --path="C:/Users/username/AppData/Roaming/.minecraft/saves/worldname"``` + +Use http://bboxfinder.com/ to draw a rectangle of your wanted area. Then copy the four box coordinates as shown below and use them as the input for the --bbox parameter. +![How to find area](https://github.com/louis-e/arnis/blob/main/gitassets/bbox-finder.png?raw=true) + +Manually generate a new Minecraft world (preferably a flat world) before running the script. +The --bbox parameter specifies the bounding box coordinates in the format: min_lng,min_lat,max_lng,max_lat. +Use --path to specify the location of the Minecraft world. +You can optionally use the parameter --debug to see processed value outputs during runtime. -Optional: ```--debug``` -Notes: -- Manually generate a Minecraft world, preferably a flat world, before running the script. -- The city, state and country name should be in the local language of the respective country. Otherwise the city might not be found. -- In some cases you need a dash instead of a space in the parameters. I will look into this problem and try to find an uniform fix for it. -- You can optionally use the parameter ```--debug``` in order to see the processed values as a text output during runtime. +#### Experimental City/State/Country Input Method +The following method is experimental and may not perform as expected: -### Docker image -If you want to run this project in a container, you can use the Dockerfile provided in this repository. It will automatically scrape the latest source code. After running the container, you have to manually copy the generated region files from the container to the host machine in order to use them. When running the Docker image, set the ```--path``` parameter to ```/home```. An image on Dockerhub will follow soon. +```python3 arnis.py --city="CityName" --state="StateName" --country="CountryName" --path="C:/Users/username/AppData/Roaming/.minecraft/saves/worldname"``` + +### Docker image (experimental) +If you want to run this project containerized, you can use the Dockerfile provided in this repository. It will automatically scrape the latest source code from the repository. After running the container, you have to manually copy the generated region files from the container to the host machine in order to use them. When running the Docker image, set the ```--path``` parameter to ```/home```. ``` docker build -t arnis . -docker run arnis --city "Arnis" --state "Schleswig Holstein" --country "Deutschland" --path "/home" +docker run arnis --city="Arnis" --state="Schleswig Holstein" --country="Deutschland" --path="/home" docker cp CONTAINER_ID:/home/region DESTINATION_PATH ``` ## :cd: Requirements - Python 3 -- At least 8 Gigabyte RAM memory - -```pip install -r requirements.txt``` +- ```pip install -r requirements.txt``` - To conform with style guide please format any changes ```black .``` @@ -57,17 +61,15 @@ docker cp CONTAINER_ID:/home/region DESTINATION_PATH ## :question: FAQ - *Why do some cities take so long to generate?*
-There is a known problem with the floodfill algorithm, where big element outlines (e.g. farmlands,...) slow down the entire script for several seconds. When the geo data contains hundreds or even thousands of those big elements, it results in a long delay which can take up to multiple hours. Start with some small cities or towns before you generate bigger cities with the script in order to get a good feeling for how long it takes. I'm already thinking about a way to rewrite the floodfill algorithm as multithreaded to split up the task on multiple CPU cores which should reduce the delay drastically. +The script's performance can be significantly affected by large elements, such as extensive farmlands. The current floodfill algorithm can slow down considerably when dealing with such elements, leading to long processing times that can stretch to hours for large cities. Thus there is also a timeout restriction in place. It is recommended to start with smaller towns to get a sense of the script's performance. Future improvements will focus on making the floodfill algorithm multi-threaded to utilize multiple CPU cores and reduce processing times drastically. - *Where does the data come from?*
-In order to get the raw geo data, Arnis contacts a random Overpass Turbo API server. This API gets its data from the free collaborative geographic project OpenStreetMap (OSM)[^1]. OSM is an online community databse founded in 2004 which basically provides an open source Google Maps alternative. -- *Why can't my city be found?*
-Due to it being limited by the amount of user contribution, some areas might not be covered yet. See question above. +The geographic data is sourced from OpenStreetMap (OSM)[^1], a free, collaborative mapping project that serves as an open-source alternative to commercial mapping services. The data is accessed via the Overpass API, which queries OSM's database. - *How does the Minecraft world generation work?*
-For this purpose I am using the [anvil-parser](https://github.com/matcool/anvil-parser) package. +The script uses the [anvil-parser](https://github.com/matcool/anvil-parser) library to interact with Minecraft's world format. This library allows the script to create and manipulate Minecraft region files, enabling the generation of real-world locations within the game. - *Where does the name come from?*
-Arnis is the name of the smallest city in Germany[^2] and due to its size, I used it during development to debug the algorithm fast and efficiently. :shipit: +The project is named after Arnis[^2], the smallest city in Germany. The city's small size made it an ideal test case for developing and debugging the script efficiently. - *What are the corresponding IDs?*
-In step 6 *[(see How it works)](#floppy_disk-how-it-works)*, the script assigns an ID to each array element which is later translated and processed into a Minecraft world. These seperate steps are required to implement a layer system (e.g. farmlands should never overwrite buildings). +During the processing stage (*[(see How it works)](#floppy_disk-how-it-works)*), each element is assigned an ID that determines how it will be represented in Minecraft. This system ensures that different layers (e.g., buildings, roads, landuse) are rendered correctly without conflicts. ID | Name | Note | --- | --- | --- | @@ -96,8 +98,8 @@ ID | Name | Note | 70-79 | House interior | The last digit refers to the building height | ## :memo: ToDo -Pick an item from the ToDo / Known Bugs list or implement an own idea. Everyone is welcome to contribute to this project. -- [ ] Alternative reliable city input options[^3] +Feel free to choose an item from the To-Do or Known Bugs list, or bring your own idea to the table. Contributions from everyone are welcome and encouraged to help improve this project. +- [x] Alternative reliable city input options[^3] - [ ] Split up processData array into several smaller ones for big cities[^4] - [ ] Floodfill timeout parameters - [ ] Implement multiprocessing in floodfill algorithm in order to boost CPU bound calculation performance @@ -117,12 +119,21 @@ Pick an item from the ToDo / Known Bugs list or implement an own idea. Everyone ## :bug: Known Bugs - [ ] 'Noer' bug (occurs when several different digits appear in coordinates before the decimal point) -- [ ] 'Nortorf' bug (occurs when there are several elements with a big distance to each other, e.g. the API returns several different cities with the exact same name) +- [x] 'Nortorf' bug (occurs when there are several elements with a big distance to each other, e.g. the API returns several different cities with the exact same name) - [ ] Saving step memory overflow - [ ] Docker image size -- [ ] Non uniform OSM naming standards (dashes) (See name tags at https://overpass-turbo.eu/s/1mMj) +- [x] Non uniform OSM naming standards (dashes) (See name tags at https://overpass-turbo.eu/s/1mMj) + +## :trophy: Hall of Fame Contributors +This section is dedicated to recognizing and celebrating the outstanding contributions of individuals who have significantly enhanced this project. Your work and dedication are deeply appreciated! + +#### Contributors: +- callumfrance +- amir16yp +- EdwardWeir13579 +- daniil2327 -## Star History +## :star: Star History [![Star History Chart](https://api.star-history.com/svg?repos=louis-e/arnis&type=Date)](https://star-history.com/#louis-e/arnis&Date) diff --git a/gitassets/demo-comp.png b/gitassets/demo-comp.png index 8b6a94e..4115c42 100644 Binary files a/gitassets/demo-comp.png and b/gitassets/demo-comp.png differ diff --git a/requirements.txt b/requirements.txt index 789f80a..4493813 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ python-polylabel==0.6 requests==2.32.2 argparse==1.4.0 black==24.4.2 -flake8==7.0.0 \ No newline at end of file +flake8==7.0.0 +tqdm==4.66.5 \ No newline at end of file diff --git a/src/floodFill.py b/src/floodFill.py index 5eedb4f..f130f99 100644 --- a/src/floodFill.py +++ b/src/floodFill.py @@ -95,7 +95,11 @@ def floodFill( ): nxt.append((x, y + 1)) - # Timeout ei dirst + # Timeout (known issue, see Github readme) + if time() - startTimeFloodfill > 7 or ( + elementType == "tree_row" and time() - startTimeFloodfill > 0.2 + ): + return img queue = nxt if len(nxt) > queueLen: diff --git a/src/getData.py b/src/getData.py index 54b47bb..8c70a00 100644 --- a/src/getData.py +++ b/src/getData.py @@ -1,8 +1,8 @@ +from random import choice +import json import os -import subprocess import requests -import json -from random import choice +import subprocess def download_with_requests(url, params, filename): @@ -47,6 +47,7 @@ def getData(city, state, country, bbox, file, debug, download_method="requests") "https://lz4.overpass-api.de/api/interpreter", "https://z.overpass-api.de/api/interpreter", "https://overpass.kumi.systems/api/interpreter", + "https://overpass.private.coffee/api/interpreter" ] url = choice(api_servers) @@ -74,10 +75,11 @@ def getData(city, state, country, bbox, file, debug, download_method="requests") elif bbox: bbox = bbox.split(",") bbox = [float(i) for i in bbox] - print(bbox) + if debug: + print(f"Bbox input: {bbox}") query1 = f""" - [out:json][bbox:{bbox[0]},{bbox[1]},{bbox[2]},{bbox[3]}]; + [out:json][bbox:{bbox[1]},{bbox[0]},{bbox[3]},{bbox[2]}]; ( nwr["building"]; nwr["highway"]; @@ -89,9 +91,12 @@ def getData(city, state, country, bbox, file, debug, download_method="requests") nwr["bridge"]; nwr["railway"]; nwr["barrier"]; - ); - (._;>;); - out; + )->.waysinbbox; + ( + node(w.waysinbbox); + )->.nodesinbbox; + .waysinbbox out body; + .nodesinbbox out skel qt; """ elif file: print("Loading data from file") @@ -115,12 +120,17 @@ def getData(city, state, country, bbox, file, debug, download_method="requests") (._;>;); out; """ + + if debug: + print(f"OSM Query: {query1}") + try: if file: - with open("data.json") as dataset: + with open("data.json", encoding="utf8") as dataset: data = json.load(dataset) else: - print(f"Chosen server: {url}") + if debug: + print(f"Chosen server: {url}") filename = "arnis-debug-raw_data.json" if download_method == "requests": file_path = download_with_requests(url, {"data": query1}, filename) @@ -146,7 +156,7 @@ def getData(city, state, country, bbox, file, debug, download_method="requests") if "The server is probably too busy to handle your request." in str(e): print("Error! OSM server overloaded") elif "Dispatcher_Client::request_read_and_idx::rate_limited" in str(e): - print("Error! IP rate limited") + print("Error! IP rate limited, wait before trying again") else: print(f"Error! {e}") os._exit(1) diff --git a/src/main.py b/src/main.py index f992037..272c7d6 100644 --- a/src/main.py +++ b/src/main.py @@ -4,28 +4,29 @@ # MIT License # Please see the LICENSE file that should have been included as part of this package. +from math import floor +from random import randint +from tqdm import tqdm +import anvil +import argparse +import gc +import numpy as np import os import sys import time -import gc -import argparse -import anvil -from random import randint -from math import floor -import numpy as np from .getData import getData from .processData import processData parser = argparse.ArgumentParser( - description="Arnis - Generate cities from real life in Minecraft using Python" + description="Arnis - Generate cities from real life in Minecraft" ) -parser.add_argument("--city", dest="city", help="Name of the city") -parser.add_argument("--state", dest="state", help="Name of the state") -parser.add_argument("--country", dest="country", help="Name of the country") -parser.add_argument("--bbox", dest="bbox", help="Bounding box of the city") +parser.add_argument("--bbox", dest="bbox", help="Bounding box of the area") +parser.add_argument("--city", dest="city", help="Name of the city (Experimental)") +parser.add_argument("--state", dest="state", help="Name of the state (Experimental)") +parser.add_argument("--country", dest="country", help="Name of the country (Experimental)") parser.add_argument("--file", dest="file", help="JSON file containing OSM data") -parser.add_argument("--path", dest="path", help="Path to the minecraft world") +parser.add_argument("--path", dest="path", required=True, help="Path to the minecraft world") parser.add_argument( "--downloader", dest="downloader", @@ -41,10 +42,16 @@ help="Enable debug mode", ) args = parser.parse_args() -if args.city is None or args.state is None or args.country is None or args.path is None: - if args.bbox is None and args.file is None: - print("Error! Missing arguments") - os._exit(1) + +# Ensure either bbox or city/state/country is provided +if not args.bbox and not (args.city and args.state and args.country): + print("Error! You must provide either a bounding box (bbox) or city/state/country (experimental) information.") + os._exit(1) + +# Ensure file argument is handled correctly +if args.file and args.bbox: + print("Error! You cannot provide both a bounding box (bbox) and a file.") + os._exit(1) gc.collect() np.seterr(all="raise") @@ -64,23 +71,24 @@ grass = anvil.Block.from_numeric_id(175, 2) farmland = anvil.Block("minecraft", "farmland") water = anvil.Block("minecraft", "water") -wheat = anvil.Block("minecraft", "wheat") -carrots = anvil.Block("minecraft", "carrots") -potatoes = anvil.Block("minecraft", "potatoes") +wheat = anvil.Block("minecraft", "wheat", {"age": 7}) +carrots = anvil.Block("minecraft", "carrots", {"age": 7}) +potatoes = anvil.Block("minecraft", "potatoes", {"age": 7}) cobblestone = anvil.Block("minecraft", "cobblestone") iron_block = anvil.Block("minecraft", "iron_block") oak_log = anvil.Block.from_numeric_id(17) -oak_leaves = anvil.Block.from_numeric_id(18) +oak_leaves = anvil.Block("minecraft", "oak_leaves") birch_log = anvil.Block("minecraft", "birch_log") white_stained_glass = anvil.Block("minecraft", "white_stained_glass") dark_oak_door_lower = anvil.Block( - "minecraft", "dark_oak_door", properties={"half": "lower"} + "minecraft", "dark_oak_door", {"half": "lower"} ) dark_oak_door_upper = anvil.Block( - "minecraft", "dark_oak_door", properties={"half": "upper"} + "minecraft", "dark_oak_door", {"half": "upper"} ) cobblestone_wall = anvil.Block("minecraft", "cobblestone_wall") stone_brick_slab = anvil.Block.from_numeric_id(44, 5) +rail = anvil.Block("minecraft", "rail") red_flower = anvil.Block.from_numeric_id(38) yellow_flower = anvil.Block("minecraft","dandelion") @@ -95,6 +103,7 @@ dirt = anvil.Block("minecraft", "dirt") glowstone = anvil.Block("minecraft", "glowstone") sponge = anvil.Block("minecraft", "sponge") +hay_bale = anvil.Block("minecraft", "hay_block") regions = {} for x in range(0, 3): @@ -126,15 +135,29 @@ def fillBlocks(block, x1, y1, z1, x2, y2, z2): def saveRegion(region="all"): if region == "all": - for key in regions: + region_keys = list(regions.keys()) + for key in tqdm(region_keys, desc="Saving minecraft world", unit="region", bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt}'): regions[key].save(mcWorldPath + "/region/" + key + ".mca") - print(f"Saved {key}") else: regions[region].save(mcWorldPath + "/region/" + region + ".mca") print(f"Saved {region}") from .tree import createTree def run(): + print("""\n + ▄████████ ▄████████ ███▄▄▄▄ ▄█ ▄████████ + ███ ███ ███ ███ ███▀▀▀██▄ ███ ███ ███ + ███ ███ ███ ███ ███ ███ ███▌ ███ █▀ + ███ ███ ▄███▄▄▄▄██▀ ███ ███ ███▌ ███ + ▀███████████ ▀▀███▀▀▀▀▀ ███ ███ ███▌ ▀███████████ + ███ ███ ▀███████████ ███ ███ ███ ███ + ███ ███ ███ ███ ███ ███ ███ ▄█ ███ + ███ █▀ ███ ███ ▀█ █▀ █▀ ▄████████▀ + ███ ███ + + https://github.com/louis-e/arnis + """) + if not (os.path.exists(mcWorldPath + "/region")): print("Error! No Minecraft world found at given path") os._exit(1) @@ -142,80 +165,78 @@ def run(): rawdata = getData(args.city, args.state, args.country, args.bbox, args.file, args.debug, args.downloader) imgarray = processData(rawdata, args) - print("Generating minecraft world...") - + # Generate Minecraft world x = 0 z = 0 doorIncrement = 0 ElementIncr = 0 ElementsLen = len(imgarray) - lastProgressPercentage = 0 - for i in imgarray: - progressPercentage = round(100 * (ElementIncr + 1) / ElementsLen) - if ( - progressPercentage % 10 == 0 - and progressPercentage != lastProgressPercentage - ): - print(f"Pixel {ElementIncr + 1}/{ElementsLen} ({progressPercentage}%)") - lastProgressPercentage = progressPercentage - + for i in tqdm(imgarray, desc="Generating pixels", unit=" pixels", total=ElementsLen, bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}]'): z = 0 for j in i: setBlock(dirt, x, 0, z) - if j == 0: # Ground + if j == 0: # Ground setBlock(grass_block, x, 1, z) - elif j == 10: # Street + elif j == 10: # Street setBlock(black_concrete, x, 1, z) setBlock(air, x, 2, z) - elif j == 11: # Footway + elif j == 11: # Footway setBlock(gray_concrete, x, 1, z) setBlock(air, x, 2, z) - elif j == 12: # Natural path + elif j == 12: # Natural path setBlock(cobblestone, x, 1, z) - elif j == 13: # Bridge + elif j == 13: # Bridge setBlock(light_gray_concrete, x, 2, z) setBlock(light_gray_concrete, x - 1, 2, z - 1) setBlock(light_gray_concrete, x + 1, 2, z - 1) setBlock(light_gray_concrete, x + 1, 2, z + 1) setBlock(light_gray_concrete, x - 1, 2, z + 1) - elif j == 14: # Railway - setBlock(iron_block, x, 2, z) - elif j == 20: # Parking + elif j == 14: # Railway + setBlock(iron_block, x, 1, z) + setBlock(rail, x, 2, z) + elif j == 20: # Parking setBlock(gray_concrete, x, 1, z) - elif j == 21: # Fountain border + elif j == 21: # Fountain border setBlock(light_gray_concrete, x, 2, z) setBlock(white_concrete, x, 1, z) - elif j >= 22 and j <= 24: # Fence + elif j >= 22 and j <= 24: # Fence if str(j)[-1] == "2" or int(str(j[0])[-1]) == 2: setBlock(cobblestone_wall, x, 2, z) else: fillBlocks(cobblestone, x, 2, z, x, int(str(j[0])[-1]), z) setBlock(grass_block, x, 1, z) - elif j == 30: # Meadow + elif j == 30: # Meadow setBlock(grass_block, x, 1, z) randomChoice = randint(0, 2) if randomChoice == 0 or randomChoice == 1: setBlock(grass, x, 2, z) - elif j == 31: # Farmland - setBlock(grass_block, x, 1, z) - randomChoice = randint(0, 20) - randomTree = randint(1, 3) - randomFlower = randint(1, 4) - if randomChoice == 20: - createTree(x, z, randomTree) - elif randomChoice == 2: - if randomFlower == 1: - setBlock(red_flower, x, 2, z) - elif randomFlower == 2: - setBlock(blue_flower, x, 2, z) - elif randomFlower == 3: - setBlock(yellow_flower, x, 2, z) + elif j == 31: # Farmland + if x % 15 == 0 or z % 15 == 0: # Place water every 8 blocks + setBlock(water, x, 1, z) + else: + setBlock(farmland, x, 1, z) + if randint(0, 75) == 0: # Rarely place trees, hay bales, or leaf blocks + special_choice = randint(1, 10) + if special_choice <= 1: # 20% chance + createTree(x, z, randint(1, 3)) + elif special_choice <= 6: # 40% chance + setBlock(hay_bale, x, 2, z) + if randint(0, 2) == 0: + setBlock(hay_bale, x, 3, z) + setBlock(hay_bale, x - 1, 2, z) + setBlock(hay_bale, x, 2, z - 1) + else: + setBlock(hay_bale, x, 3, z) + setBlock(hay_bale, x + 1, 2, z) + setBlock(hay_bale, x, 2, z + 1) + else: # Remaining 40% chance + setBlock(oak_leaves, x, 2, z) else: - setBlock(white_flower, x, 2, z) - elif randomChoice == 0 or randomChoice == 1: - setBlock(grass, x, 2, z) - elif j == 32: # Forest + crop_choice = randint(0, 2) + crops = [wheat, carrots, potatoes] + setBlock(crops[crop_choice], x, 2, z) + elif j == 32: # Forest setBlock(grass_block, x, 1, z) randomChoice = randint(0, 20) randomTree = randint(1, 3) @@ -233,39 +254,48 @@ def run(): setBlock(white_flower, x, 2, z) elif randomChoice == 0 or randomChoice == 1: setBlock(grass, x, 2, z) - elif j == 33: # Cemetery + elif j == 33: # Cemetery + # Spawn chances + grave_chance = 25 + flower_chance = 15 + tree_chance = 5 + setBlock(podzol, x, 1, z) - randomChoice = randint(0, 100) - if randomChoice == 0: - setBlock(cobblestone, x - 1, 2, z) - setBlock(stone_brick_slab, x - 1, 3, z) - setBlock(stone_brick_slab, x, 2, z) - setBlock(stone_brick_slab, x + 1, 2, z) - elif randomChoice == 1: - setBlock(cobblestone, x, 2, z - 1) - setBlock(stone_brick_slab, x, 3, z - 1) - setBlock(stone_brick_slab, x, 2, z) - setBlock(stone_brick_slab, x, 2, z + 1) - elif randomChoice == 2 or randomChoice == 3: - setBlock(red_flower, x, 2, z) - elif j == 34: # Beach + if (x % 3 == 0) and (z % 3 == 0): + randomChoice = randint(0, 100) + if randomChoice < grave_chance: + if randint(0, 1) == 0: + setBlock(cobblestone, x - 1, 2, z) + setBlock(stone_brick_slab, x - 1, 3, z) + setBlock(stone_brick_slab, x, 2, z) + setBlock(stone_brick_slab, x + 1, 2, z) + else: + setBlock(cobblestone, x, 2, z - 1) + setBlock(stone_brick_slab, x, 3, z - 1) + setBlock(stone_brick_slab, x, 2, z) + setBlock(stone_brick_slab, x, 2, z + 1) + elif randomChoice < grave_chance + flower_chance: + setBlock(red_flower, x, 2, z) + elif randomChoice < grave_chance + flower_chance + tree_chance: + createTree(x, z, randint(1, 3)) + elif j == 34: # Beach setBlock(sand, x, 1, z) - elif j == 35: # Wetland + elif j == 35: # Wetland randomChoice = randint(0, 2) if randomChoice == 0: setBlock(grass_block, x, 1, z) else: setBlock(water, x, 1, z) - elif j == 36: # Pitch + elif j == 36: # Pitch setBlock(green_stained_hardened_clay, x, 1, z) - elif j == 37: # Swimming pool + elif j == 37: # Swimming pool setBlock(water, x, 1, z) setBlock(white_concrete, x, 0, z) - elif j == 38: # Water + elif j == 38: # Water setBlock(water, x, 1, z) - elif j == 39: # Raw grass + elif j == 39: # Raw grass setBlock(grass_block, x, 1, z) - elif j >= 50 and j <= 59: # House corner + elif j >= 50 and j <= 59: # House corner building_height = 5 if j == 51: building_height = 8 @@ -287,7 +317,7 @@ def run(): building_height = 32 fillBlocks(white_concrete, x, 1, z, x, building_height, z) - elif j >= 60 and j <= 69: # House wall + elif j >= 60 and j <= 69: # House wall building_height = 4 if j == 61: building_height = 7 @@ -319,7 +349,7 @@ def run(): fillBlocks(white_stained_glass, x, 3, z, x, building_height, z) doorIncrement += 1 setBlock(white_concrete, x, building_height + 1, z) - elif j >= 70 and j <= 79: # House interior + elif j >= 70 and j <= 79: # House interior if j >= 70: setBlock(white_concrete, x, 5, z) if j >= 71: @@ -349,7 +379,11 @@ def run(): x += 1 ElementIncr += 1 - print("Saving minecraft world...") + print( + f"Generation finished in {(time.time() - processStartTime):.2f} " + + f"seconds ({((time.time() - processStartTime) / 60):.2f} minutes)" + ) + # Saving minecraft world saveRegion() print( f"Done! Finished in {(time.time() - processStartTime):.2f} " diff --git a/src/processData.py b/src/processData.py index 6712fdb..71309fd 100644 --- a/src/processData.py +++ b/src/processData.py @@ -1,11 +1,14 @@ -from time import time from cv2 import imwrite -import numpy as np +from time import time +from tqdm import tqdm import mmap +import numpy as np from .bresenham import bresenham from .floodFill import floodFill +OFFSET = 1000000000 # Fixed offset to ensure all coordinates are positive +SCALE_FACTOR = 1000000 # Consistent scaling factor def create_memory_mapped_array(filename, shape, dtype): # Open a file in binary read-write mode @@ -22,52 +25,38 @@ def create_memory_mapped_array(filename, shape, dtype): # Create a memory-mapped array from the memory-mapped file return np.ndarray(shape=shape, dtype=dtype, buffer=mmapped_array) - +# Parsing data def processData(data, args): - print("Parsing data...") - resDownScaler = 100 + resDownScaler = 10 processingStartTime = time() - greatestElementX = 0 - greatestElementY = 0 + greatestElementX = -OFFSET + greatestElementY = -OFFSET + lowestElementX = OFFSET + lowestElementY = OFFSET + + # Convert all coordinates and determine bounds for element in data["elements"]: if element["type"] == "node": - element["lat"] = int(str(element["lat"]).replace(".", "")) - element["lon"] = int(str(element["lon"]).replace(".", "")) + element["lat"] = int((element["lat"] + OFFSET) * SCALE_FACTOR) + element["lon"] = int((element["lon"] + OFFSET) * SCALE_FACTOR) if element["lat"] > greatestElementX: greatestElementX = element["lat"] if element["lon"] > greatestElementY: greatestElementY = element["lon"] - - for element in data["elements"]: - if element["type"] == "node": - if len(str(element["lat"])) != len(str(greatestElementX)): - for i in range( - 0, len(str(greatestElementX)) - len(str(element["lat"])) - ): - element["lat"] *= 10 - - if len(str(element["lon"])) != len(str(greatestElementY)): - for i in range( - 0, len(str(greatestElementY)) - len(str(element["lon"])) - ): - element["lon"] *= 10 - - lowestElementX = greatestElementX - lowestElementY = greatestElementY - for element in data["elements"]: - if element["type"] == "node": if element["lat"] < lowestElementX: lowestElementX = element["lat"] if element["lon"] < lowestElementY: lowestElementY = element["lon"] + if args.debug: + print(f"greatestElementX: {greatestElementX}, greatestElementY: {greatestElementY}") + print(f"lowestElementX: {lowestElementX}, lowestElementY: {lowestElementY}") + nodesDict = {} for element in data["elements"]: if element["type"] == "node": - element["lat"] -= lowestElementX - element["lon"] -= lowestElementY nodesDict[element["id"]] = [element["lat"], element["lon"]] orig_posDeterminationCoordX = 0 @@ -172,20 +161,10 @@ def processData(data, args): img.fill(0) imgLanduse = img.copy() - print("Processing data...") - + # Processing data ElementIncr = 0 ElementsLen = len(data["elements"]) - lastProgressPercentage = 0 - for element in reversed(data["elements"]): - progressPercentage = round(100 * (ElementIncr + 1) / ElementsLen) - if ( - progressPercentage % 10 == 0 - and progressPercentage != lastProgressPercentage - ): - print(f"Element {ElementIncr + 1}/{ElementsLen} ({progressPercentage}%)") - lastProgressPercentage = progressPercentage - + for element in tqdm(reversed(data["elements"]), desc="Processing elements", unit=" elements", total=ElementsLen, bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}, {rate_fmt}]'): if element["type"] == "way" and "tags" in element: if "building" in element["tags"]: previousElement = (0, 0) @@ -216,6 +195,12 @@ def processData(data, args): buildingHeight = str( int(float(element["tags"]["building:levels"])) - 1 ) + + if ( + "building" in element["tags"] + and element["tags"]["building"] == "garage" + ): + buildingHeight = 0 for i in bresenham( coordinate[0], @@ -281,6 +266,7 @@ def processData(data, args): and previousElement != (0, 0) and element["tags"]["highway"] != "steps" and element["tags"]["highway"] != "bridge" + and element["tags"]["highway"] != "proposed" ): blockRange = 2 highwayType = 10 @@ -461,7 +447,7 @@ def processData(data, args): or element["tags"]["leisure"] == "playground" or element["tags"]["leisure"] == "garden" ): - leisureType = 30 + leisureType = 32 elif element["tags"]["leisure"] == "pitch": leisureType = 36 elif element["tags"]["leisure"] == "swimming_pool": @@ -505,7 +491,7 @@ def processData(data, args): and ( element["tags"]["layer"] == "-1" or element["tags"]["layer"] == "-2" - or element["tags"]["layer"] != "-3" + or element["tags"]["layer"] == "-3" ) ): waterwayWidth = 4 @@ -603,20 +589,13 @@ def processData(data, args): if ( previousElement != (0, 0) and element["tags"]["railway"] != "proposed" + and element["tags"]["railway"] != "abandoned" ): for i in bresenham( - coordinate[0] - 2, - coordinate[1] - 2, - previousElement[0] - 2, - previousElement[1] - 2, - ): - if i[0] < minMaxDistX and i[1] < minMaxDistY: - img[i[1]][i[0]] = 14 - for i in bresenham( - coordinate[0] + 1, - coordinate[1] + 1, - previousElement[0] + 1, - previousElement[1] + 1, + coordinate[0], + coordinate[1], + previousElement[0], + previousElement[1], ): if i[0] < minMaxDistX and i[1] < minMaxDistY: img[i[1]][i[0]] = 14 @@ -653,22 +632,13 @@ def processData(data, args): ElementIncr += 1 - print("Calculating layers...") - total_pixels = img.shape[0] * img.shape[1] - processed_pixels = 0 - - for x in range(0, img.shape[0]): - for y in range(0, img.shape[1]): - if imgLanduse[x][y] != 0 and img[x][y] == 0: - img[x][y] = imgLanduse[x][y] - processed_pixels += 1 - percentage = (processed_pixels / total_pixels) * 100 - print(f"Progress: {percentage:.2f}% completed", end="\r") - print("Progress: 100.00% completed") + # Calculating layers + mask = (imgLanduse != 0) & (img == 0) + img[mask] = imgLanduse[mask] print( f"Processing finished in {(time() - processingStartTime):.2f} seconds" - + f"({((time() - processingStartTime) / 60):.2f} minutes)" + + f" ({((time() - processingStartTime) / 60):.2f} minutes)" ) if args.debug: imwrite("arnis-debug-map.png", img) diff --git a/src/tree.py b/src/tree.py index edf8f9d..360e0ad 100644 --- a/src/tree.py +++ b/src/tree.py @@ -39,8 +39,8 @@ def round3(material, x, y, z): setBlock(material, x-1, y, z-3) -def createTree(x, z, typetree=1): #3 hours - if typetree == 1: #oak +def createTree(x, z, typetree=1): + if typetree == 1: # Oak tree fillBlocks(oak_log, x, 2, z, x, 10, z) fillBlocks(oak_leaves, x-1, 5, z, x-1, 11, z) fillBlocks(oak_leaves, x+1, 5, z, x+1, 11, z) @@ -60,7 +60,7 @@ def createTree(x, z, typetree=1): #3 hours round3(oak_leaves, x, 8, z) round3(oak_leaves, x, 7, z) - elif typetree == 2: #spruce + elif typetree == 2: # Spruce tree fillBlocks(spruce_log, x, 2, z, x, 11, z) fillBlocks(birch_leaves, x-1, 5, z, x-1, 12, z) fillBlocks(birch_leaves, x+1, 5, z, x+1, 12, z) @@ -75,7 +75,7 @@ def createTree(x, z, typetree=1): #3 hours round2(birch_leaves, x, 8, z) round2(birch_leaves, x, 5, z) - elif typetree == 3: #birch + elif typetree == 3: # Birch tree fillBlocks(birch_log, x, 2, z, x, 8, z) fillBlocks(birch_leaves, x-1, 4, z, x-1, 9, z) fillBlocks(birch_leaves, x+1, 4, z, x+1, 9, z)