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.
- finland2mc-0.1.0/.github/workflows/publish.yml +44 -0
- finland2mc-0.1.0/.github/workflows/test.yml +13 -0
- finland2mc-0.1.0/.gitignore +8 -0
- finland2mc-0.1.0/.python-version +1 -0
- finland2mc-0.1.0/LICENSE +21 -0
- finland2mc-0.1.0/PKG-INFO +169 -0
- finland2mc-0.1.0/README.md +149 -0
- finland2mc-0.1.0/docs/jyvaskyla.jpg +0 -0
- finland2mc-0.1.0/docs/keuruu.jpg +0 -0
- finland2mc-0.1.0/docs/naantali.jpg +0 -0
- finland2mc-0.1.0/finland2mc/__init__.py +4 -0
- finland2mc-0.1.0/finland2mc/__main__.py +3 -0
- finland2mc-0.1.0/finland2mc/area.py +45 -0
- finland2mc-0.1.0/finland2mc/cli.py +194 -0
- finland2mc-0.1.0/finland2mc/data/blocks_26.3.json +1 -0
- finland2mc-0.1.0/finland2mc/data/template/data/minecraft/custom_boss_events.dat +0 -0
- finland2mc-0.1.0/finland2mc/data/template/data/minecraft/game_rules.dat +0 -0
- finland2mc-0.1.0/finland2mc/data/template/data/minecraft/random_sequences.dat +0 -0
- finland2mc-0.1.0/finland2mc/data/template/data/minecraft/scheduled_events.dat +0 -0
- finland2mc-0.1.0/finland2mc/data/template/data/minecraft/scoreboard.dat +0 -0
- finland2mc-0.1.0/finland2mc/data/template/data/minecraft/stopwatches.dat +0 -0
- finland2mc-0.1.0/finland2mc/data/template/data/minecraft/weather.dat +0 -0
- finland2mc-0.1.0/finland2mc/data/template/data/minecraft/world_clocks.dat +0 -0
- finland2mc-0.1.0/finland2mc/data/template/data/minecraft/world_gen_settings.dat +0 -0
- finland2mc-0.1.0/finland2mc/data/template/dimensions/minecraft/overworld/data/minecraft/chunk_tickets.dat +0 -0
- finland2mc-0.1.0/finland2mc/data/template/dimensions/minecraft/overworld/data/minecraft/raids.dat +0 -0
- finland2mc-0.1.0/finland2mc/data/template/dimensions/minecraft/overworld/data/minecraft/world_border.dat +0 -0
- finland2mc-0.1.0/finland2mc/data/template/dimensions/minecraft/the_end/data/minecraft/chunk_tickets.dat +0 -0
- finland2mc-0.1.0/finland2mc/data/template/dimensions/minecraft/the_end/data/minecraft/ender_dragon_fight.dat +0 -0
- finland2mc-0.1.0/finland2mc/data/template/dimensions/minecraft/the_end/data/minecraft/raids.dat +0 -0
- finland2mc-0.1.0/finland2mc/data/template/dimensions/minecraft/the_end/data/minecraft/world_border.dat +0 -0
- finland2mc-0.1.0/finland2mc/data/template/dimensions/minecraft/the_nether/data/minecraft/chunk_tickets.dat +0 -0
- finland2mc-0.1.0/finland2mc/data/template/dimensions/minecraft/the_nether/data/minecraft/raids.dat +0 -0
- finland2mc-0.1.0/finland2mc/data/template/dimensions/minecraft/the_nether/data/minecraft/world_border.dat +0 -0
- finland2mc-0.1.0/finland2mc/data/template/level.dat +0 -0
- finland2mc-0.1.0/finland2mc/generate.py +422 -0
- finland2mc-0.1.0/finland2mc/geocode.py +47 -0
- finland2mc-0.1.0/finland2mc/mcformat.py +167 -0
- finland2mc-0.1.0/finland2mc/nls.py +161 -0
- finland2mc-0.1.0/finland2mc/prepare.py +395 -0
- finland2mc-0.1.0/finland2mc/prism.py +124 -0
- finland2mc-0.1.0/finland2mc/render.py +111 -0
- finland2mc-0.1.0/finland2mc/world.py +58 -0
- finland2mc-0.1.0/pyproject.toml +33 -0
- finland2mc-0.1.0/tests/test_mcformat.py +63 -0
- finland2mc-0.1.0/tests/test_nls.py +31 -0
- 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 @@
|
|
|
1
|
+
3.13
|
finland2mc-0.1.0/LICENSE
ADDED
|
@@ -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
|
+
|  |  |  |
|
|
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
|
+
|  |  |  |
|
|
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,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())
|