finland2mc 0.1.0__tar.gz

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.
Files changed (47) hide show
  1. finland2mc-0.1.0/.github/workflows/publish.yml +44 -0
  2. finland2mc-0.1.0/.github/workflows/test.yml +13 -0
  3. finland2mc-0.1.0/.gitignore +8 -0
  4. finland2mc-0.1.0/.python-version +1 -0
  5. finland2mc-0.1.0/LICENSE +21 -0
  6. finland2mc-0.1.0/PKG-INFO +169 -0
  7. finland2mc-0.1.0/README.md +149 -0
  8. finland2mc-0.1.0/docs/jyvaskyla.jpg +0 -0
  9. finland2mc-0.1.0/docs/keuruu.jpg +0 -0
  10. finland2mc-0.1.0/docs/naantali.jpg +0 -0
  11. finland2mc-0.1.0/finland2mc/__init__.py +4 -0
  12. finland2mc-0.1.0/finland2mc/__main__.py +3 -0
  13. finland2mc-0.1.0/finland2mc/area.py +45 -0
  14. finland2mc-0.1.0/finland2mc/cli.py +194 -0
  15. finland2mc-0.1.0/finland2mc/data/blocks_26.3.json +1 -0
  16. finland2mc-0.1.0/finland2mc/data/template/data/minecraft/custom_boss_events.dat +0 -0
  17. finland2mc-0.1.0/finland2mc/data/template/data/minecraft/game_rules.dat +0 -0
  18. finland2mc-0.1.0/finland2mc/data/template/data/minecraft/random_sequences.dat +0 -0
  19. finland2mc-0.1.0/finland2mc/data/template/data/minecraft/scheduled_events.dat +0 -0
  20. finland2mc-0.1.0/finland2mc/data/template/data/minecraft/scoreboard.dat +0 -0
  21. finland2mc-0.1.0/finland2mc/data/template/data/minecraft/stopwatches.dat +0 -0
  22. finland2mc-0.1.0/finland2mc/data/template/data/minecraft/weather.dat +0 -0
  23. finland2mc-0.1.0/finland2mc/data/template/data/minecraft/world_clocks.dat +0 -0
  24. finland2mc-0.1.0/finland2mc/data/template/data/minecraft/world_gen_settings.dat +0 -0
  25. finland2mc-0.1.0/finland2mc/data/template/dimensions/minecraft/overworld/data/minecraft/chunk_tickets.dat +0 -0
  26. finland2mc-0.1.0/finland2mc/data/template/dimensions/minecraft/overworld/data/minecraft/raids.dat +0 -0
  27. finland2mc-0.1.0/finland2mc/data/template/dimensions/minecraft/overworld/data/minecraft/world_border.dat +0 -0
  28. finland2mc-0.1.0/finland2mc/data/template/dimensions/minecraft/the_end/data/minecraft/chunk_tickets.dat +0 -0
  29. finland2mc-0.1.0/finland2mc/data/template/dimensions/minecraft/the_end/data/minecraft/ender_dragon_fight.dat +0 -0
  30. finland2mc-0.1.0/finland2mc/data/template/dimensions/minecraft/the_end/data/minecraft/raids.dat +0 -0
  31. finland2mc-0.1.0/finland2mc/data/template/dimensions/minecraft/the_end/data/minecraft/world_border.dat +0 -0
  32. finland2mc-0.1.0/finland2mc/data/template/dimensions/minecraft/the_nether/data/minecraft/chunk_tickets.dat +0 -0
  33. finland2mc-0.1.0/finland2mc/data/template/dimensions/minecraft/the_nether/data/minecraft/raids.dat +0 -0
  34. finland2mc-0.1.0/finland2mc/data/template/dimensions/minecraft/the_nether/data/minecraft/world_border.dat +0 -0
  35. finland2mc-0.1.0/finland2mc/data/template/level.dat +0 -0
  36. finland2mc-0.1.0/finland2mc/generate.py +422 -0
  37. finland2mc-0.1.0/finland2mc/geocode.py +47 -0
  38. finland2mc-0.1.0/finland2mc/mcformat.py +167 -0
  39. finland2mc-0.1.0/finland2mc/nls.py +161 -0
  40. finland2mc-0.1.0/finland2mc/prepare.py +395 -0
  41. finland2mc-0.1.0/finland2mc/prism.py +124 -0
  42. finland2mc-0.1.0/finland2mc/render.py +111 -0
  43. finland2mc-0.1.0/finland2mc/world.py +58 -0
  44. finland2mc-0.1.0/pyproject.toml +33 -0
  45. finland2mc-0.1.0/tests/test_mcformat.py +63 -0
  46. finland2mc-0.1.0/tests/test_nls.py +31 -0
  47. finland2mc-0.1.0/uv.lock +398 -0
@@ -0,0 +1,44 @@
1
+ name: publish
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ jobs:
12
+ build:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v7
16
+ - uses: astral-sh/setup-uv@v10.3.2
17
+ - name: Check that the tag matches the version
18
+ run: |
19
+ version="${GITHUB_REF_NAME#v}"
20
+ grep -qxF "version = \"$version\"" pyproject.toml \
21
+ || { echo "::error::pyproject.toml doesn't have version $version"; exit 1; }
22
+ grep -qxF "__version__ = \"$version\"" finland2mc/__init__.py \
23
+ || { echo "::error::finland2mc/__init__.py doesn't have version $version"; exit 1; }
24
+ - run: uv run --python 3.13 pytest -q
25
+ - run: uv build
26
+ - uses: actions/upload-artifact@v7
27
+ with:
28
+ name: dist
29
+ path: dist/
30
+
31
+ publish:
32
+ needs: build
33
+ runs-on: ubuntu-latest
34
+ environment:
35
+ name: pypi
36
+ url: https://pypi.org/p/finland2mc
37
+ permissions:
38
+ id-token: write
39
+ steps:
40
+ - uses: actions/download-artifact@v8
41
+ with:
42
+ name: dist
43
+ path: dist/
44
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,13 @@
1
+ name: test
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: astral-sh/setup-uv@v6
13
+ - run: uv run --python 3.13 pytest -q
@@ -0,0 +1,8 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .pytest_cache/
7
+ # generated worlds and their preview maps
8
+ worlds/
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Topi Autio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,169 @@
1
+ Metadata-Version: 2.5
2
+ Name: finland2mc
3
+ Version: 0.1.0
4
+ Summary: Turn any place in Finland into a 1:1 Minecraft Java Edition world from National Land Survey open data
5
+ Project-URL: Homepage, https://github.com/topiautio/finland2mc
6
+ Author: Topi Autio
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Python: <3.14,>=3.12
10
+ Requires-Dist: laspy[lazrs]
11
+ Requires-Dist: nbtlib
12
+ Requires-Dist: numpy
13
+ Requires-Dist: pillow
14
+ Requires-Dist: pyogrio
15
+ Requires-Dist: pyproj
16
+ Requires-Dist: rasterio
17
+ Requires-Dist: scipy
18
+ Requires-Dist: shapely>=2
19
+ Description-Content-Type: text/markdown
20
+
21
+ # finland2mc
22
+
23
+ Give it a place in Finland and it builds a 1:1 Minecraft Java Edition world of it: one block is one metre,
24
+ horizontally and vertically. Everything comes from the National Land Survey of Finland's open data: terrain,
25
+ lakes and sea, roads, bridges, buildings, fields, and trees placed where laser scans found them.
26
+
27
+ | Jyväskylä | Naantali | Keuruu |
28
+ |---|---|---|
29
+ | ![Jyväskylä](https://raw.githubusercontent.com/topiautio/finland2mc/main/docs/jyvaskyla.jpg) | ![Naantali](https://raw.githubusercontent.com/topiautio/finland2mc/main/docs/naantali.jpg) | ![Keuruu](https://raw.githubusercontent.com/topiautio/finland2mc/main/docs/keuruu.jpg) |
30
+
31
+ *Top-down maps drawn from the generated world files.*
32
+
33
+ ## Getting started
34
+
35
+ You need [uv](https://docs.astral.sh/uv/) and Minecraft Java Edition 26.3 or newer.
36
+
37
+ ```bash
38
+ uvx finland2mc "Jyväskylä"
39
+ ```
40
+
41
+ uvx downloads finland2mc and the Python packages it needs on the first run. After that it keeps using the
42
+ version it downloaded; `uvx finland2mc@latest` gets the newest one. To get a plain `finland2mc` command, run
43
+ `uv tool install finland2mc`.
44
+
45
+ Run like that, it asks a few questions, each with a default you can accept with Enter:
46
+
47
+ 1. Which match you meant, if the name fits several places.
48
+ 2. How big an area to build (default 4 × 4 km).
49
+ 3. Whether to use laser scans for real tree and building heights. This is usually most of the download.
50
+ 4. Whether to install the world into Prism Launcher.
51
+
52
+ Then it prints the download size and a time estimate, and asks before starting.
53
+
54
+ To skip the questions, give the answers as options:
55
+
56
+ ```bash
57
+ uvx finland2mc "Porvoo" --size 2 --yes # defaults, no questions
58
+ uvx finland2mc "Kallio, Helsinki" --size 3 --prism # also install into Prism Launcher
59
+ uvx finland2mc --at 61.4981,23.7608 --name "Tampere centre"
60
+ ```
61
+
62
+ The finished world goes to `worlds/<name> 1-1/`, with a top-down map next to it (`<name> 1-1.png`).
63
+ It needs Minecraft **26.3** or newer.
64
+
65
+ ## Options
66
+
67
+ | Option | What it does |
68
+ |---|---|
69
+ | `place` | Town, district or address in Finland. Looked up with OpenStreetMap's Nominatim. |
70
+ | `--at LAT,LON` | Use an exact centre point instead of a name. ETRS-TM35FIN also works: `--at E381000,N6905000`. |
71
+ | `--size KM` | Side of the square area, 0.5–10 km. Default 4. |
72
+ | `--name NAME` | World name shown in Minecraft. Default: `<place> 1:1`. |
73
+ | `--out DIR` | Where the world goes. Default `./worlds`. |
74
+ | `--no-lidar` | Skip laser scans. The download is much smaller, but trees are placed at random in forests and building heights are guessed from the building type. |
75
+ | `--prism [INSTANCE]` | Copy the world into a Prism Launcher instance. The default is `Finland 1:1`, which is created if it doesn't exist. It copies its Minecraft and Java setup from an instance you already have with 26.3, if there is one. |
76
+ | `--zip` | Also make a .zip of the world. |
77
+ | `--jobs N` | CPU cores to use. Default: all but one. |
78
+ | `-y`, `--yes` | Don't ask anything; use the defaults. |
79
+
80
+ ## How long it takes
81
+
82
+ Measured on 11 cores, with the data already downloaded:
83
+
84
+ | Place | Area | Time |
85
+ |---|---|---|
86
+ | Naantali (town and sea) | 2 × 2 km | 0.9 min |
87
+ | Jyväskylä (city centre) | 3 × 3 km | 1.3 min |
88
+ | Keuruu (mostly forest) | 6 × 6 km | about 25 min |
89
+
90
+ Dense forest is by far the slowest part. The program prints an estimate before it starts.
91
+
92
+ Downloads are cached in `~/.cache/finland2mc`, which you can change with the `FINLAND2MC_CACHE` environment
93
+ variable. Neighbouring or repeated areas reuse the cache. Laser scans are the big part of a download: older
94
+ scans (about 1 point/m²) are about 50 MB per 3 × 3 km tile, and newer ones (5 points/m²) can be several
95
+ hundred MB per tile.
96
+
97
+ ## In the world
98
+
99
+ - You start in creative mode on peaceful difficulty, on a street near the middle of the area.
100
+ - A world border keeps you inside the area. There is only void beyond it.
101
+ - x = 0, z = 0 is the centre point. +x is east and +z is south.
102
+ - The most common water level (the sea, or the biggest lake) is at y = 62. The program prints how y relates
103
+ to height above sea level.
104
+ - Far views look best with a level-of-detail mod such as Distant Horizons.
105
+
106
+ ## Where the data comes from
107
+
108
+ All map data is open data from the National Land Survey of Finland (Maanmittauslaitos), downloaded from the
109
+ [Funet mirror](https://www.nic.funet.fi/index/geodata/mml/):
110
+
111
+ | Data | Used for |
112
+ |---|---|
113
+ | Elevation model 2 m | Terrain height |
114
+ | Topographic database 2025 | Sea, lakes, streams, roads, railways, bridges, buildings, fields, bogs, rock, parks |
115
+ | Depth contours | Lake depths, interpolated between the contours |
116
+ | Laser scanning (newest available per tile) | Individual trees and their heights, and building heights |
117
+
118
+ Some things are guesses, because the data doesn't say:
119
+
120
+ - Tree species are chosen from the terrain: pine on ridges, rock and bogs; spruce in hollows; birch near open land.
121
+ - Building colours and roof materials are picked at random from typical Finnish choices.
122
+ - Roofs are hip roofs on houses and flat roofs on large buildings. Interiors are empty floors with no doors.
123
+ - Sea depth isn't in the open data, so the sea gets deeper with distance from the shore, down to 10 m.
124
+ - Laser scans can be several years old, so newer clear-cuts and buildings won't match.
125
+
126
+ The biome is birch forest everywhere. In a superflat world the game thinks sea level is y = −63, so colder
127
+ biomes such as taiga would freeze the lakes.
128
+
129
+ ## How it works
130
+
131
+ | File | What it does |
132
+ |---|---|
133
+ | `cli.py` | Asks the questions and runs the steps. |
134
+ | `geocode.py` | Turns a place name into coordinates. |
135
+ | `nls.py` | Works out the map sheet and tile names for the area (TM35 sheet division) and downloads them into the cache. |
136
+ | `prepare.py` | Turns the data into 1 m grids: ground and water heights, land cover, roads, bridge decks, building footprints and the tree list. |
137
+ | `generate.py` | Builds the chunks in parallel. |
138
+ | `mcformat.py` | Writes the 26.3 chunk and region format. Block names are checked against `data/blocks_26.3.json`, the official server's block report. |
139
+ | `world.py` | Adds `level.dat` and the other world files from `data/template/` (an empty world saved by the 26.3 server), then sets the spawn point and the world border. |
140
+ | `render.py` | Draws the preview map from the finished world files. |
141
+ | `prism.py` | Installs the world into Prism Launcher. |
142
+
143
+ The world is written directly in the 26.3 format. Newer Minecraft versions upgrade it when they open it.
144
+ `data/blocks_26.3.json` and `data/template/` were produced by the official 26.3 server: its data generator's
145
+ block report (trimmed to names, properties and default states), and an empty world it saved.
146
+
147
+ To work on the code, clone the repository and use `uv run finland2mc` in it. Run the tests with
148
+ `uv run pytest`.
149
+
150
+ ## Limits
151
+
152
+ - Finland only, because the data is Finnish.
153
+ - Areas up to 10 × 10 km. Memory use grows with area. 3 × 3 km measured about 1 GB, so expect roughly
154
+ 4 GB for 6 × 6 km and 11 GB for 10 × 10 km.
155
+ - Places with more than about 370 m height difference within the area (some Lapland fells) don't fit
156
+ Minecraft's 384-block world height. Use a smaller area there.
157
+
158
+ ## Credit and licence
159
+
160
+ The code is under the MIT licence (see `LICENSE`).
161
+
162
+ Worlds made with it contain data from the National Land Survey of Finland: Topographic Database, Elevation
163
+ model 2 m, and Laser scanning data. Licence: CC BY 4.0. If you share a world, keep this credit with it.
164
+
165
+ Place search uses OpenStreetMap Nominatim (© OpenStreetMap contributors). Please keep to its
166
+ [usage policy](https://operations.osmfoundation.org/policies/nominatim/); the program searches once per place
167
+ name you enter.
168
+
169
+ NOT AN OFFICIAL MINECRAFT PRODUCT. NOT APPROVED BY OR ASSOCIATED WITH MOJANG OR MICROSOFT.
@@ -0,0 +1,149 @@
1
+ # finland2mc
2
+
3
+ Give it a place in Finland and it builds a 1:1 Minecraft Java Edition world of it: one block is one metre,
4
+ horizontally and vertically. Everything comes from the National Land Survey of Finland's open data: terrain,
5
+ lakes and sea, roads, bridges, buildings, fields, and trees placed where laser scans found them.
6
+
7
+ | Jyväskylä | Naantali | Keuruu |
8
+ |---|---|---|
9
+ | ![Jyväskylä](https://raw.githubusercontent.com/topiautio/finland2mc/main/docs/jyvaskyla.jpg) | ![Naantali](https://raw.githubusercontent.com/topiautio/finland2mc/main/docs/naantali.jpg) | ![Keuruu](https://raw.githubusercontent.com/topiautio/finland2mc/main/docs/keuruu.jpg) |
10
+
11
+ *Top-down maps drawn from the generated world files.*
12
+
13
+ ## Getting started
14
+
15
+ You need [uv](https://docs.astral.sh/uv/) and Minecraft Java Edition 26.3 or newer.
16
+
17
+ ```bash
18
+ uvx finland2mc "Jyväskylä"
19
+ ```
20
+
21
+ uvx downloads finland2mc and the Python packages it needs on the first run. After that it keeps using the
22
+ version it downloaded; `uvx finland2mc@latest` gets the newest one. To get a plain `finland2mc` command, run
23
+ `uv tool install finland2mc`.
24
+
25
+ Run like that, it asks a few questions, each with a default you can accept with Enter:
26
+
27
+ 1. Which match you meant, if the name fits several places.
28
+ 2. How big an area to build (default 4 × 4 km).
29
+ 3. Whether to use laser scans for real tree and building heights. This is usually most of the download.
30
+ 4. Whether to install the world into Prism Launcher.
31
+
32
+ Then it prints the download size and a time estimate, and asks before starting.
33
+
34
+ To skip the questions, give the answers as options:
35
+
36
+ ```bash
37
+ uvx finland2mc "Porvoo" --size 2 --yes # defaults, no questions
38
+ uvx finland2mc "Kallio, Helsinki" --size 3 --prism # also install into Prism Launcher
39
+ uvx finland2mc --at 61.4981,23.7608 --name "Tampere centre"
40
+ ```
41
+
42
+ The finished world goes to `worlds/<name> 1-1/`, with a top-down map next to it (`<name> 1-1.png`).
43
+ It needs Minecraft **26.3** or newer.
44
+
45
+ ## Options
46
+
47
+ | Option | What it does |
48
+ |---|---|
49
+ | `place` | Town, district or address in Finland. Looked up with OpenStreetMap's Nominatim. |
50
+ | `--at LAT,LON` | Use an exact centre point instead of a name. ETRS-TM35FIN also works: `--at E381000,N6905000`. |
51
+ | `--size KM` | Side of the square area, 0.5–10 km. Default 4. |
52
+ | `--name NAME` | World name shown in Minecraft. Default: `<place> 1:1`. |
53
+ | `--out DIR` | Where the world goes. Default `./worlds`. |
54
+ | `--no-lidar` | Skip laser scans. The download is much smaller, but trees are placed at random in forests and building heights are guessed from the building type. |
55
+ | `--prism [INSTANCE]` | Copy the world into a Prism Launcher instance. The default is `Finland 1:1`, which is created if it doesn't exist. It copies its Minecraft and Java setup from an instance you already have with 26.3, if there is one. |
56
+ | `--zip` | Also make a .zip of the world. |
57
+ | `--jobs N` | CPU cores to use. Default: all but one. |
58
+ | `-y`, `--yes` | Don't ask anything; use the defaults. |
59
+
60
+ ## How long it takes
61
+
62
+ Measured on 11 cores, with the data already downloaded:
63
+
64
+ | Place | Area | Time |
65
+ |---|---|---|
66
+ | Naantali (town and sea) | 2 × 2 km | 0.9 min |
67
+ | Jyväskylä (city centre) | 3 × 3 km | 1.3 min |
68
+ | Keuruu (mostly forest) | 6 × 6 km | about 25 min |
69
+
70
+ Dense forest is by far the slowest part. The program prints an estimate before it starts.
71
+
72
+ Downloads are cached in `~/.cache/finland2mc`, which you can change with the `FINLAND2MC_CACHE` environment
73
+ variable. Neighbouring or repeated areas reuse the cache. Laser scans are the big part of a download: older
74
+ scans (about 1 point/m²) are about 50 MB per 3 × 3 km tile, and newer ones (5 points/m²) can be several
75
+ hundred MB per tile.
76
+
77
+ ## In the world
78
+
79
+ - You start in creative mode on peaceful difficulty, on a street near the middle of the area.
80
+ - A world border keeps you inside the area. There is only void beyond it.
81
+ - x = 0, z = 0 is the centre point. +x is east and +z is south.
82
+ - The most common water level (the sea, or the biggest lake) is at y = 62. The program prints how y relates
83
+ to height above sea level.
84
+ - Far views look best with a level-of-detail mod such as Distant Horizons.
85
+
86
+ ## Where the data comes from
87
+
88
+ All map data is open data from the National Land Survey of Finland (Maanmittauslaitos), downloaded from the
89
+ [Funet mirror](https://www.nic.funet.fi/index/geodata/mml/):
90
+
91
+ | Data | Used for |
92
+ |---|---|
93
+ | Elevation model 2 m | Terrain height |
94
+ | Topographic database 2025 | Sea, lakes, streams, roads, railways, bridges, buildings, fields, bogs, rock, parks |
95
+ | Depth contours | Lake depths, interpolated between the contours |
96
+ | Laser scanning (newest available per tile) | Individual trees and their heights, and building heights |
97
+
98
+ Some things are guesses, because the data doesn't say:
99
+
100
+ - Tree species are chosen from the terrain: pine on ridges, rock and bogs; spruce in hollows; birch near open land.
101
+ - Building colours and roof materials are picked at random from typical Finnish choices.
102
+ - Roofs are hip roofs on houses and flat roofs on large buildings. Interiors are empty floors with no doors.
103
+ - Sea depth isn't in the open data, so the sea gets deeper with distance from the shore, down to 10 m.
104
+ - Laser scans can be several years old, so newer clear-cuts and buildings won't match.
105
+
106
+ The biome is birch forest everywhere. In a superflat world the game thinks sea level is y = −63, so colder
107
+ biomes such as taiga would freeze the lakes.
108
+
109
+ ## How it works
110
+
111
+ | File | What it does |
112
+ |---|---|
113
+ | `cli.py` | Asks the questions and runs the steps. |
114
+ | `geocode.py` | Turns a place name into coordinates. |
115
+ | `nls.py` | Works out the map sheet and tile names for the area (TM35 sheet division) and downloads them into the cache. |
116
+ | `prepare.py` | Turns the data into 1 m grids: ground and water heights, land cover, roads, bridge decks, building footprints and the tree list. |
117
+ | `generate.py` | Builds the chunks in parallel. |
118
+ | `mcformat.py` | Writes the 26.3 chunk and region format. Block names are checked against `data/blocks_26.3.json`, the official server's block report. |
119
+ | `world.py` | Adds `level.dat` and the other world files from `data/template/` (an empty world saved by the 26.3 server), then sets the spawn point and the world border. |
120
+ | `render.py` | Draws the preview map from the finished world files. |
121
+ | `prism.py` | Installs the world into Prism Launcher. |
122
+
123
+ The world is written directly in the 26.3 format. Newer Minecraft versions upgrade it when they open it.
124
+ `data/blocks_26.3.json` and `data/template/` were produced by the official 26.3 server: its data generator's
125
+ block report (trimmed to names, properties and default states), and an empty world it saved.
126
+
127
+ To work on the code, clone the repository and use `uv run finland2mc` in it. Run the tests with
128
+ `uv run pytest`.
129
+
130
+ ## Limits
131
+
132
+ - Finland only, because the data is Finnish.
133
+ - Areas up to 10 × 10 km. Memory use grows with area. 3 × 3 km measured about 1 GB, so expect roughly
134
+ 4 GB for 6 × 6 km and 11 GB for 10 × 10 km.
135
+ - Places with more than about 370 m height difference within the area (some Lapland fells) don't fit
136
+ Minecraft's 384-block world height. Use a smaller area there.
137
+
138
+ ## Credit and licence
139
+
140
+ The code is under the MIT licence (see `LICENSE`).
141
+
142
+ Worlds made with it contain data from the National Land Survey of Finland: Topographic Database, Elevation
143
+ model 2 m, and Laser scanning data. Licence: CC BY 4.0. If you share a world, keep this credit with it.
144
+
145
+ Place search uses OpenStreetMap Nominatim (© OpenStreetMap contributors). Please keep to its
146
+ [usage policy](https://operations.osmfoundation.org/policies/nominatim/); the program searches once per place
147
+ name you enter.
148
+
149
+ NOT AN OFFICIAL MINECRAFT PRODUCT. NOT APPROVED BY OR ASSOCIATED WITH MOJANG OR MICROSOFT.
Binary file
Binary file
Binary file
@@ -0,0 +1,4 @@
1
+ """finland2mc: 1:1 Minecraft worlds of places in Finland, from National Land Survey open data."""
2
+
3
+ __version__ = "0.1.0"
4
+ MINECRAFT_VERSION = "26.3"
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
@@ -0,0 +1,45 @@
1
+ """The square of land being turned into a world, and the grid it is sampled on."""
2
+ from dataclasses import dataclass
3
+
4
+ from rasterio.transform import from_origin
5
+
6
+ MARGIN = 16 # extra metres sampled around the playable area
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class Area:
11
+ """A square centred on ETRS-TM35FIN point (ce, cn).
12
+
13
+ Minecraft x = x0 + col, z = x0 + row (1 block = 1 m). E = ce + x + 0.5, N = cn - z - 0.5.
14
+ """
15
+ ce: int
16
+ cn: int
17
+ half: int # half the side length in metres
18
+
19
+ @property
20
+ def x0(self):
21
+ return -(self.half + MARGIN)
22
+
23
+ @property
24
+ def size(self):
25
+ return 2 * (self.half + MARGIN)
26
+
27
+ @property
28
+ def west(self):
29
+ return self.ce + self.x0
30
+
31
+ @property
32
+ def north(self):
33
+ return self.cn - self.x0
34
+
35
+ @property
36
+ def bounds(self):
37
+ """(minE, minN, maxE, maxN) of the sampled grid."""
38
+ return (self.west, self.north - self.size, self.west + self.size, self.north)
39
+
40
+ @property
41
+ def transform(self):
42
+ return from_origin(self.west, self.north, 1, 1)
43
+
44
+ def to_dict(self):
45
+ return dict(ce=self.ce, cn=self.cn, half=self.half)
@@ -0,0 +1,194 @@
1
+ """finland2mc: give it a place in Finland, get a 1:1 Minecraft world of it.
2
+
3
+ finland2mc "Jyväskylä" # asks a few questions, everything has a default
4
+ finland2mc "Porvoo" --size 2 --yes # no questions
5
+ finland2mc --at 62.2416,25.7496 # exact centre point (lat,lon or E...,N... in ETRS-TM35FIN)
6
+ """
7
+ import argparse
8
+ import os
9
+ import re
10
+ import shutil
11
+ import sys
12
+ import time
13
+ from pathlib import Path
14
+
15
+ from . import MINECRAFT_VERSION, __version__, geocode, nls, prism
16
+ from .area import Area
17
+
18
+ SECONDS_PER_CHUNK = (0.025, 0.12) # per CPU core: town centre, dense forest
19
+ DEFAULT_INSTANCE = "Finland 1:1"
20
+
21
+
22
+ def say(*a):
23
+ print(*a, flush=True)
24
+
25
+
26
+ def ask(question, default, interactive):
27
+ if not interactive:
28
+ return default
29
+ ans = input(f"{question} [{default}]: ").strip()
30
+ return ans or default
31
+
32
+
33
+ def ask_yes(question, default, interactive):
34
+ if not interactive:
35
+ return default
36
+ while True:
37
+ ans = input(f"{question} [{'Y/n' if default else 'y/N'}]: ").strip().lower()
38
+ if not ans:
39
+ return default
40
+ if ans in ("y", "yes", "k", "kyllä"):
41
+ return True
42
+ if ans in ("n", "no", "e", "ei"):
43
+ return False
44
+ say("Please answer y or n.")
45
+
46
+
47
+ def choose_place(args, interactive):
48
+ if args.at:
49
+ return geocode.from_coordinates(args.at)
50
+ query = args.place
51
+ while True:
52
+ if not query:
53
+ if not interactive:
54
+ raise SystemExit("Give a place name, e.g. finland2mc \"Jyväskylä\"")
55
+ query = input("Place in Finland (town, district or address): ").strip()
56
+ continue
57
+ try:
58
+ found = geocode.search(query)
59
+ except OSError as e:
60
+ raise SystemExit(f"Place search failed ({e}). Use --at LAT,LON instead.")
61
+ if not found:
62
+ say(f"Nothing found in Finland for '{query}'.")
63
+ if not interactive:
64
+ raise SystemExit(1)
65
+ query = None
66
+ continue
67
+ if len(found) == 1 or not interactive:
68
+ return found[0]
69
+ for i, p in enumerate(found, 1):
70
+ say(f" {i}. {p.description}")
71
+ while True:
72
+ pick = ask("Which one", "1", interactive)
73
+ if pick.isdigit() and 1 <= int(pick) <= len(found):
74
+ return found[int(pick) - 1]
75
+ say(f"Pick a number from 1 to {len(found)}.")
76
+
77
+
78
+ def folder_name(name):
79
+ return re.sub(r'[<>"/\\|?*]', "", name.replace(":", "-")).strip() or "world"
80
+
81
+
82
+ def main(argv=None):
83
+ ap = argparse.ArgumentParser(prog="finland2mc", description="1:1 Minecraft worlds of places in Finland, "
84
+ "from National Land Survey of Finland open data.")
85
+ ap.add_argument("place", nargs="?", help="town, district or address in Finland")
86
+ ap.add_argument("--at", metavar="LAT,LON", help="centre point instead of a place name "
87
+ "(or E381000,N6905000 in ETRS-TM35FIN)")
88
+ ap.add_argument("--size", type=float, help="side of the square area in km (default 4, max 10)")
89
+ ap.add_argument("--name", help="world name shown in Minecraft (default: '<place> 1:1')")
90
+ ap.add_argument("--out", default="worlds", help="folder for the finished world (default: ./worlds)")
91
+ ap.add_argument("--no-lidar", action="store_true",
92
+ help="skip laser scans: smaller download, but made-up tree and building heights")
93
+ ap.add_argument("--prism", nargs="?", const=DEFAULT_INSTANCE, metavar="INSTANCE",
94
+ help=f"copy the world into a Prism Launcher instance (default '{DEFAULT_INSTANCE}', "
95
+ "created if missing)")
96
+ ap.add_argument("--zip", action="store_true", help="also make a .zip of the world")
97
+ ap.add_argument("--jobs", type=int, help="CPU cores to use (default: all but one)")
98
+ ap.add_argument("--keep-work", action="store_true", help="keep the intermediate grids")
99
+ ap.add_argument("-y", "--yes", action="store_true", help="don't ask anything, use defaults")
100
+ ap.add_argument("--version", action="version", version=f"finland2mc {__version__} (Minecraft {MINECRAFT_VERSION})")
101
+ args = ap.parse_args(argv)
102
+ interactive = sys.stdin.isatty() and not args.yes
103
+
104
+ place = choose_place(args, interactive)
105
+ e, n = place.etrs
106
+ say(f"Place: {place.description}\n ETRS-TM35FIN E {e} N {n}")
107
+ size = args.size
108
+ while size is None:
109
+ try:
110
+ size = float(ask("Area size in km (one side of the square)", "4", interactive).replace(",", "."))
111
+ except ValueError:
112
+ say("Please give a number, e.g. 3 or 2.5")
113
+ if not 0.5 <= size <= 10:
114
+ raise SystemExit("Area size must be between 0.5 and 10 km.")
115
+ name = args.name or f"{place.name} 1:1"
116
+ area = Area(e, n, int(round(size * 500)))
117
+ jobs = args.jobs or max(1, (os.cpu_count() or 2) - 1)
118
+
119
+ say("Checking which map data is needed...")
120
+ try:
121
+ files, todo = nls.plan(area.bounds, lidar=not args.no_lidar)
122
+ except ValueError as err:
123
+ raise SystemExit(f"That point is outside Finland's map sheets ({err}).")
124
+ if not files["dem"] and not files["mtk"]:
125
+ raise SystemExit("No map data here. Is the place in Finland?")
126
+ lidar_bytes = sum(nls.remote_size(p) or 0 for p in files["lidar"] if not nls.local(p).exists())
127
+ if files["lidar"] and lidar_bytes > 0 and interactive:
128
+ if not ask_yes(f"Use laser scans for real tree and building heights? ({lidar_bytes / 1e6:.0f} MB of the "
129
+ "download)", True, interactive):
130
+ todo -= lidar_bytes
131
+ files["lidar"] = []
132
+ chunks = ((area.half - 1) // 16 + (area.half + 15) // 16 + 1) ** 2
133
+ lo, hi = (chunks * s / jobs / 60 + size * size * 0.05 for s in SECONDS_PER_CHUNK)
134
+ lo, hi = max(1, round(lo)), max(1, round(hi))
135
+ say(f"World: '{name}', {size:g} x {size:g} km, {chunks} chunks, about {lo if lo == hi else f'{lo}-{hi}'} min "
136
+ f"on {jobs} cores (forest is slower than town)")
137
+ if not files["lidar"]:
138
+ say(" (no laser scans: tree positions and building heights will be guesses)")
139
+ say(f"Download: {todo / 1e6:.0f} MB (cached in {nls.CACHE}, reused next time)")
140
+
141
+ install_to = args.prism
142
+ root = prism.find_root()
143
+ if install_to is None and root and interactive:
144
+ if ask_yes(f"Install into Prism Launcher instance '{DEFAULT_INSTANCE}'?", True, interactive):
145
+ install_to = DEFAULT_INSTANCE
146
+ if not ask_yes("Start?", True, interactive):
147
+ return 0
148
+
149
+ t0 = time.time()
150
+ if todo:
151
+ say("Downloading National Land Survey data...")
152
+ local = nls.download(files, progress=lambda m: print(f"\r{m}", end="", flush=True))
153
+ print()
154
+
155
+ out = Path(args.out).resolve()
156
+ world = out / folder_name(name)
157
+ if world.exists():
158
+ raise SystemExit(f"{world} already exists. Remove it or use --name / --out.")
159
+ work = nls.CACHE / "work" / f"{e}_{n}_{area.half}"
160
+ from . import generate, prepare, render, world as world_mod # heavy imports after the questions
161
+
162
+ say("Preparing map data...")
163
+ meta = prepare.Prep(area, local, work, log=lambda m: say(" " + m)).run()
164
+ say("Building the world...")
165
+ generate.run(str(work), str(world), jobs=jobs, progress=lambda m: print(f"\r{m} ", end="", flush=True))
166
+ print()
167
+ spawn = world_mod.finalize(str(world), str(work), name)
168
+ preview = Path(str(world) + ".png")
169
+ render.render(str(world), str(preview), area.half)
170
+ if args.zip:
171
+ shutil.make_archive(str(world), "zip", out, world.name)
172
+ if not args.keep_work:
173
+ shutil.rmtree(work, ignore_errors=True)
174
+
175
+ say(f"\nDone in {(time.time() - t0) / 60:.1f} min.")
176
+ say(f" World: {world}")
177
+ say(f" Map: {preview}")
178
+ say(f" Spawn: {spawn[0]} {spawn[1]} {spawn[2]} (y = {prepare.height_rule(meta['offset'])})")
179
+ if install_to:
180
+ try:
181
+ inst, dest, created = prism.install(world, install_to)
182
+ say(f" Prism: {'created instance' if created else 'added to instance'} '{install_to}' ({inst})")
183
+ if created and prism.prism_running():
184
+ say(" Prism is running; restart it if the new instance doesn't show up.")
185
+ except (RuntimeError, OSError) as err:
186
+ say(f" Prism: not installed ({err})")
187
+ else:
188
+ say(f" Open it with Minecraft {MINECRAFT_VERSION} or newer: copy the world folder into your saves folder.")
189
+ say("Contains data from the National Land Survey of Finland (CC BY 4.0).")
190
+ return 0
191
+
192
+
193
+ if __name__ == "__main__":
194
+ sys.exit(main())