pax_global_header00006660000000000000000000000064147752460140014524gustar00rootroot0000000000000052 comment=637755d6f9e15215f2891eb1df13ae00040aa60d luanti-org-minetestmapper-637755d/000077500000000000000000000000001477524601400171265ustar00rootroot00000000000000luanti-org-minetestmapper-637755d/.dockerignore000066400000000000000000000002571477524601400216060ustar00rootroot00000000000000.git .github *~ minetestmapper minetestmapper.exe CMakeCache.txt CMakeFiles/ CPack*.cmake _CPack_Packages/ install_manifest.txt Makefile cmake_install.cmake cmake_config.h luanti-org-minetestmapper-637755d/.github/000077500000000000000000000000001477524601400204665ustar00rootroot00000000000000luanti-org-minetestmapper-637755d/.github/workflows/000077500000000000000000000000001477524601400225235ustar00rootroot00000000000000luanti-org-minetestmapper-637755d/.github/workflows/build.yml000066400000000000000000000031071477524601400243460ustar00rootroot00000000000000name: build # build on source or workflow changes on: push: paths: - '**.[ch]' - '**.cpp' - '**/CMakeLists.txt' - 'util/ci/**' - '.github/workflows/**.yml' pull_request: paths: - '**.[ch]' - '**.cpp' - '**/CMakeLists.txt' - 'util/ci/**' - '.github/workflows/**.yml' jobs: gcc: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - name: Install deps run: | source util/ci/script.sh install_linux_deps - name: Build run: | source util/ci/script.sh run_build env: CC: gcc CXX: g++ - name: Test run: | ./util/ci/test.sh - name: Test Install run: | make DESTDIR=/tmp/install install clang: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 - name: Install deps run: | source util/ci/script.sh install_linux_deps - name: Build run: | source util/ci/script.sh run_build env: CC: clang CXX: clang++ - name: Test run: | ./util/ci/test.sh gcc_fedora: runs-on: ubuntu-latest container: image: fedora:latest steps: - uses: actions/checkout@v4 - name: Install deps run: | source util/ci/script.sh install_linux_deps - name: Build run: | source util/ci/script.sh run_build - name: Test run: | ./util/ci/test.sh luanti-org-minetestmapper-637755d/.github/workflows/docker_image.yml000066400000000000000000000052631477524601400256650ustar00rootroot00000000000000--- name: docker_image # https://docs.github.com/en/actions/publishing-packages/publishing-docker-images # https://docs.docker.com/build/ci/github-actions/multi-platform # https://github.com/opencontainers/image-spec/blob/main/annotations.md on: push: branches: [ "master" ] # Publish semver tags as releases. tags: [ "*" ] pull_request: # Build docker image on pull requests. (but do not publish) paths: - '**/**.[ch]' - '**/**.cpp' workflow_dispatch: inputs: use_cache: description: "Use build cache" required: true type: boolean default: true env: REGISTRY: ghcr.io # github.repository as / IMAGE_NAME: ${{ github.repository }} jobs: publish: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - name: Check out repository uses: actions/checkout@v4 - name: Setup Docker buildx uses: docker/setup-buildx-action@v3.0.0 # Login against the Docker registry except on PR # https://github.com/docker/login-action - name: Log into registry ${{ env.REGISTRY }} if: github.event_name != 'pull_request' uses: docker/login-action@v3.0.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # Extract metadata (tags, labels) for Docker # https://github.com/docker/metadata-action - name: Extract Docker metadata id: meta uses: docker/metadata-action@v5.5.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} labels: | org.opencontainers.image.title=Minetestmapper org.opencontainers.image.vendor=Luanti org.opencontainers.image.licenses=BSD 2-Clause # Build and push Docker image # https://github.com/docker/build-push-action # No arm support for now. Require cross-compilation support in Dockerfile to not use QEMU. - name: Build and push Docker image uses: docker/build-push-action@v5.1.0 with: context: . platforms: linux/amd64 push: ${{ github.event_name != 'pull_request' }} load: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max no-cache: ${{ (github.event_name == 'workflow_dispatch' && !inputs.use_cache) || startsWith(github.ref, 'refs/tags/') }} - name: Test Docker Image run: | docker run --rm $(cut -d, -f1 <<<"$DOCKER_METADATA_OUTPUT_TAGS") minetestmapper --help shell: bash luanti-org-minetestmapper-637755d/.gitignore000066400000000000000000000003161477524601400211160ustar00rootroot00000000000000*~ /minetestmapper /minetestmapper.exe /colors.txt CMakeCache.txt CMakeFiles/ CPack*.cmake _CPack_Packages/ install_manifest.txt Makefile cmake_install.cmake cmake_config.h compile_commands.json .vscode/ luanti-org-minetestmapper-637755d/AUTHORS000066400000000000000000000001521477524601400201740ustar00rootroot00000000000000Miroslav Bendík ShadowNinja sfan5 luanti-org-minetestmapper-637755d/CMakeLists.txt000066400000000000000000000147321477524601400216750ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.5) project(minetestmapper VERSION 1.0 LANGUAGES CXX ) # Stuff & Paths if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build." FORCE) endif() set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) if(WIN32) set(SHAREDIR ".") set(BINDIR ".") set(DOCDIR ".") else() set(SHAREDIR "share/luanti") # reuse engine share dir set(BINDIR "bin") set(DOCDIR "share/doc/${PROJECT_NAME}") set(MANDIR "share/man") endif() set(CUSTOM_SHAREDIR "" CACHE STRING "Directory to install data files into") if(NOT CUSTOM_SHAREDIR STREQUAL "") set(SHAREDIR "${CUSTOM_SHAREDIR}") message(STATUS "Using SHAREDIR=${SHAREDIR}") endif() set(CUSTOM_BINDIR "" CACHE STRING "Directory to install binaries into") if(NOT CUSTOM_BINDIR STREQUAL "") set(BINDIR "${CUSTOM_BINDIR}") message(STATUS "Using BINDIR=${BINDIR}") endif() set(CUSTOM_DOCDIR "" CACHE STRING "Directory to install documentation into") if(NOT CUSTOM_DOCDIR STREQUAL "") set(DOCDIR "${CUSTOM_DOCDIR}") message(STATUS "Using DOCDIR=${DOCDIR}") endif() list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") # Libraries: gd find_library(LIBGD_LIBRARY gd) find_path(LIBGD_INCLUDE_DIR gd.h) message (STATUS "libgd library: ${LIBGD_LIBRARY}") message (STATUS "libgd headers: ${LIBGD_INCLUDE_DIR}") if(NOT LIBGD_LIBRARY OR NOT LIBGD_INCLUDE_DIR) message(FATAL_ERROR "libgd not found!") endif(NOT LIBGD_LIBRARY OR NOT LIBGD_INCLUDE_DIR) # Libraries: zlib find_package(zlib-ng QUIET) if(zlib-ng_FOUND) set(ZLIB_INCLUDE_DIR zlib-ng::zlib) set(ZLIB_LIBRARY zlib-ng::zlib) set(USE_ZLIB_NG TRUE) message(STATUS "Found zlib-ng, using it instead of zlib.") else() message(STATUS "zlib-ng not found, falling back to zlib.") find_package(ZLIB REQUIRED) set(USE_ZLIB_NG FALSE) endif() # Libraries: zstd find_package(Zstd REQUIRED) # Libraries: sqlite3 find_library(SQLITE3_LIBRARY sqlite3) find_path(SQLITE3_INCLUDE_DIR sqlite3.h) message (STATUS "sqlite3 library: ${SQLITE3_LIBRARY}") message (STATUS "sqlite3 headers: ${SQLITE3_INCLUDE_DIR}") if(NOT SQLITE3_LIBRARY OR NOT SQLITE3_INCLUDE_DIR) message(FATAL_ERROR "sqlite3 not found!") endif(NOT SQLITE3_LIBRARY OR NOT SQLITE3_INCLUDE_DIR) # Libraries: postgresql option(ENABLE_POSTGRESQL "Enable PostgreSQL backend" TRUE) set(USE_POSTGRESQL FALSE) if(ENABLE_POSTGRESQL) if(CMAKE_VERSION VERSION_LESS "3.20") find_package(PostgreSQL QUIET) # Before CMake 3.20 FindPostgreSQL.cmake always looked for server includes # but we don't need them, so continue anyway if only those are missing. if(PostgreSQL_INCLUDE_DIR AND PostgreSQL_LIBRARY) set(PostgreSQL_FOUND TRUE) set(PostgreSQL_INCLUDE_DIRS ${PostgreSQL_INCLUDE_DIR}) set(PostgreSQL_LIBRARIES ${PostgreSQL_LIBRARY}) endif() else() find_package(PostgreSQL) endif() if(PostgreSQL_FOUND) set(USE_POSTGRESQL TRUE) message(STATUS "PostgreSQL backend enabled") # This variable is case sensitive, don't try to change it to POSTGRESQL_INCLUDE_DIR message(STATUS "PostgreSQL includes: ${PostgreSQL_INCLUDE_DIRS}") include_directories(${PostgreSQL_INCLUDE_DIRS}) else() message(STATUS "PostgreSQL not found!") set(PostgreSQL_LIBRARIES "") endif() endif(ENABLE_POSTGRESQL) # Libraries: leveldb OPTION(ENABLE_LEVELDB "Enable LevelDB backend" TRUE) set(USE_LEVELDB FALSE) if(ENABLE_LEVELDB) find_library(LEVELDB_LIBRARY leveldb) find_path(LEVELDB_INCLUDE_DIR leveldb/db.h) message (STATUS "LevelDB library: ${LEVELDB_LIBRARY}") message (STATUS "LevelDB headers: ${LEVELDB_INCLUDE_DIR}") if(LEVELDB_LIBRARY AND LEVELDB_INCLUDE_DIR) set(USE_LEVELDB TRUE) message(STATUS "LevelDB backend enabled") include_directories(${LEVELDB_INCLUDE_DIR}) else() message(STATUS "LevelDB not found!") set(LEVELDB_LIBRARY "") endif() endif(ENABLE_LEVELDB) # Libraries: redis OPTION(ENABLE_REDIS "Enable redis backend" TRUE) set(USE_REDIS FALSE) if(ENABLE_REDIS) find_library(REDIS_LIBRARY hiredis) find_path(REDIS_INCLUDE_DIR hiredis/hiredis.h) message (STATUS "redis library: ${REDIS_LIBRARY}") message (STATUS "redis headers: ${REDIS_INCLUDE_DIR}") if(REDIS_LIBRARY AND REDIS_INCLUDE_DIR) set(USE_REDIS TRUE) message(STATUS "redis backend enabled") include_directories(${REDIS_INCLUDE_DIR}) else() message(STATUS "redis not found!") set(REDIS_LIBRARY "") endif() endif(ENABLE_REDIS) # Compiling & Linking configure_file( "${CMAKE_CURRENT_SOURCE_DIR}/src/cmake_config.h.in" "${CMAKE_CURRENT_BINARY_DIR}/cmake_config.h" ) if(CMAKE_CXX_COMPILER_ID MATCHES "^(GNU|Clang|AppleClang)$") set(CMAKE_CXX_FLAGS_RELEASE "-O2") set(CMAKE_CXX_FLAGS_DEBUG "-Og -g2") add_compile_options(-Wall -pipe) elseif(MSVC) add_compile_options(/GR- /Zl) endif() if(CMAKE_BUILD_TYPE STREQUAL "Release") add_definitions(-DNDEBUG) endif() add_executable(minetestmapper) target_include_directories(minetestmapper PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}" "${CMAKE_CURRENT_BINARY_DIR}" ) target_sources(minetestmapper PRIVATE src/BlockDecoder.cpp src/PixelAttributes.cpp src/PlayerAttributes.cpp src/TileGenerator.cpp src/ZlibDecompressor.cpp src/ZstdDecompressor.cpp src/Image.cpp src/mapper.cpp src/util.cpp src/log.cpp src/db-sqlite3.cpp $<$:src/db-postgresql.cpp> $<$:src/db-leveldb.cpp> $<$:src/db-redis.cpp> ) target_include_directories(minetestmapper PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src" "${CMAKE_CURRENT_BINARY_DIR}" ${SQLITE3_INCLUDE_DIR} ${LIBGD_INCLUDE_DIR} ${ZLIB_INCLUDE_DIR} ${ZSTD_INCLUDE_DIR} ) target_link_libraries(minetestmapper ${SQLITE3_LIBRARY} ${PostgreSQL_LIBRARIES} ${LEVELDB_LIBRARY} ${REDIS_LIBRARY} ${LIBGD_LIBRARY} ${ZLIB_LIBRARY} ${ZSTD_LIBRARY} ) # Installing & Packaging install(TARGETS "${PROJECT_NAME}" DESTINATION "${BINDIR}") install(FILES "AUTHORS" DESTINATION "${DOCDIR}") install(FILES "COPYING" DESTINATION "${DOCDIR}") install(FILES "README.rst" DESTINATION "${DOCDIR}") install(FILES "colors.txt" DESTINATION "${SHAREDIR}") if(UNIX) install(FILES "minetestmapper.6" DESTINATION "${MANDIR}/man6") endif() set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Overview mapper for Luanti") set(CPACK_PACKAGE_VENDOR "celeron55") set(CPACK_PACKAGE_CONTACT "Perttu Ahola ") if(WIN32) set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}-win32") set(CPACK_GENERATOR ZIP) else() set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}-linux") set(CPACK_GENERATOR TGZ) set(CPACK_SOURCE_GENERATOR TGZ) endif() include(CPack) luanti-org-minetestmapper-637755d/COPYING000066400000000000000000000025051477524601400201630ustar00rootroot00000000000000Copyright (c) 2013-2014, Miroslav Bendík and various contributors (see AUTHORS) All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. luanti-org-minetestmapper-637755d/Dockerfile000066400000000000000000000013331477524601400211200ustar00rootroot00000000000000ARG DOCKER_IMAGE=alpine:3.20 FROM $DOCKER_IMAGE AS builder RUN apk add --no-cache build-base cmake \ gd-dev sqlite-dev postgresql-dev hiredis-dev leveldb-dev \ ninja COPY . /usr/src/minetestmapper WORKDIR /usr/src/minetestmapper RUN cmake -B build -G Ninja && \ cmake --build build --parallel $(nproc) && \ cmake --install build FROM $DOCKER_IMAGE AS runtime RUN apk add --no-cache libstdc++ libgcc libpq \ gd sqlite-libs postgresql hiredis leveldb COPY --from=builder /usr/local/share/luanti /usr/local/share/luanti COPY --from=builder /usr/local/bin/minetestmapper /usr/local/bin/minetestmapper COPY COPYING /usr/local/share/minetest/minetestmapper.COPYING ENTRYPOINT ["/usr/local/bin/minetestmapper"] luanti-org-minetestmapper-637755d/README.rst000066400000000000000000000077251477524601400206300ustar00rootroot00000000000000Minetest Mapper C++ =================== .. image:: https://github.com/minetest/minetestmapper/workflows/build/badge.svg :target: https://github.com/minetest/minetestmapper/actions/workflows/build.yml Minetestmapper generates a top-down overview image from a Luanti map. A port of minetestmapper.py to C++ from `the obsolete Python script `_. This version is both faster and provides more features. Minetestmapper ships with a colors.txt file suitable for Minetest Game, if you use a different game or have mods installed you should generate a matching colors.txt for better results (colors will be missing otherwise). The `generate_colorstxt.py script <./util/generate_colorstxt.py>`_ in the util folder exists for this purpose, detailed instructions can be found within. Requirements ------------ * C++ compiler, zlib, zstd * libgd * sqlite3 * LevelDB (optional) * hiredis (optional) * Postgres libraries (optional) on Debian/Ubuntu: ^^^^^^^^^^^^^^^^^ ``sudo apt install cmake libgd-dev libhiredis-dev libleveldb-dev libpq-dev libsqlite3-dev zlib1g-dev libzstd-dev`` on openSUSE: ^^^^^^^^^^^^ ``sudo zypper install gd-devel hiredis-devel leveldb-devel postgresql-devel sqlite3-devel zlib-devel libzstd-devel`` for Windows: ^^^^^^^^^^^^ Minetestmapper for Windows can be downloaded `from the Releases section `_. After extracting the archive, it can be invoked from cmd.exe or PowerShell: .. code-block:: dos cd C:\Users\yourname\Desktop\example\path minetestmapper.exe --help Compilation ----------- .. code-block:: bash cmake . -DENABLE_LEVELDB=1 make -j$(nproc) Usage ----- ``minetestmapper`` has two mandatory paremeters, ``-i`` (input world path) and ``-o`` (output image path). :: ./minetestmapper -i ~/.minetest/worlds/my_world/ -o map.png Parameters ^^^^^^^^^^ bgcolor: Background color of image, e.g. ``--bgcolor '#ffffff'`` scalecolor: Color of scale marks and text, e.g. ``--scalecolor '#000000'`` playercolor: Color of player indicators, e.g. ``--playercolor '#ff0000'`` origincolor: Color of origin indicator, e.g. ``--origincolor '#ff0000'`` drawscale: Draw scale(s) with tick marks and numbers, ``--drawscale`` drawplayers: Draw player indicators with name, ``--drawplayers`` draworigin: Draw origin indicator, ``--draworigin`` drawalpha: Allow nodes to be drawn with transparency (such as water), ``--drawalpha`` extent: Don't output any imagery, just print the extent of the full map, ``--extent`` noshading: Don't draw shading on nodes, ``--noshading`` noemptyimage: Don't output anything when the image would be empty, ``--noemptyimage`` verbose: Enable verbose log putput, ``--verbose`` min-y: Don't draw nodes below this Y value, e.g. ``--min-y -25`` max-y: Don't draw nodes above this Y value, e.g. ``--max-y 75`` backend: Override auto-detected map backend; supported: *sqlite3*, *leveldb*, *redis*, *postgresql*, e.g. ``--backend leveldb`` geometry: Limit area to specific geometry (*x:z+w+h* where x and z specify the lower left corner), e.g. ``--geometry -800:-800+1600+1600`` The coordinates are specified with the same axes as in-game. The Z axis becomes Y when projected on the image. zoom: Zoom the image by using more than one pixel per node, e.g. ``--zoom 4`` colors: Override auto-detected path to colors.txt, e.g. ``--colors ../world/mycolors.txt`` scales: Draw scales on specified image edges (letters *t b l r* meaning top, bottom, left and right), e.g. ``--scales tbr`` exhaustive: Select if database should be traversed exhaustively or using range queries, available: *never*, *y*, *full*, *auto* Defaults to *auto*. You shouldn't need to change this, as minetestmapper tries to automatically picks the best option. dumpblock: Instead of rendering anything try to load the block at the given position (*x,y,z*) and print its raw data as hexadecimal. luanti-org-minetestmapper-637755d/cmake/000077500000000000000000000000001477524601400202065ustar00rootroot00000000000000luanti-org-minetestmapper-637755d/cmake/FindZstd.cmake000066400000000000000000000013311477524601400227330ustar00rootroot00000000000000mark_as_advanced(ZSTD_LIBRARY ZSTD_INCLUDE_DIR) find_path(ZSTD_INCLUDE_DIR NAMES zstd.h) find_library(ZSTD_LIBRARY NAMES zstd) if(ZSTD_INCLUDE_DIR AND ZSTD_LIBRARY) # Check that the API we use exists include(CheckSymbolExists) unset(HAVE_ZSTD_INITDSTREAM CACHE) set(CMAKE_REQUIRED_INCLUDES ${ZSTD_INCLUDE_DIR}) set(CMAKE_REQUIRED_LIBRARIES ${ZSTD_LIBRARY}) check_symbol_exists(ZSTD_initDStream zstd.h HAVE_ZSTD_INITDSTREAM) unset(CMAKE_REQUIRED_INCLUDES) unset(CMAKE_REQUIRED_LIBRARIES) if(NOT HAVE_ZSTD_INITDSTREAM) unset(ZSTD_INCLUDE_DIR CACHE) unset(ZSTD_LIBRARY CACHE) endif() endif() include(FindPackageHandleStandardArgs) find_package_handle_standard_args(Zstd DEFAULT_MSG ZSTD_LIBRARY ZSTD_INCLUDE_DIR) luanti-org-minetestmapper-637755d/colors.txt000066400000000000000000000345331477524601400212000ustar00rootroot00000000000000# beds beds:bed_bottom 130 3 3 beds:bed_top 185 162 163 beds:fancy_bed_bottom 136 49 28 beds:fancy_bed_top 179 153 148 # bones bones:bones 117 117 117 # butterflies # carts carts:brakerail 150 121 102 carts:powerrail 160 145 102 carts:rail 146 128 108 # default default:acacia_bush_leaves 109 133 87 default:acacia_bush_sapling 85 121 61 default:acacia_bush_stem 84 77 70 default:acacia_leaves 126 153 101 default:acacia_sapling 87 120 64 default:acacia_tree 195 119 97 default:acacia_wood 150 61 39 default:apple 161 34 19 default:aspen_leaves 72 105 29 default:aspen_sapling 85 123 45 default:aspen_tree 218 198 168 default:aspen_wood 210 199 170 default:blueberry_bush_leaves 63 99 22 default:blueberry_bush_leaves_with_berries 63 99 22 default:blueberry_bush_sapling 81 112 33 default:bookshelf 131 102 57 default:brick 123 99 95 default:bronzeblock 186 111 15 default:bush_leaves 35 55 29 default:bush_sapling 66 64 40 default:bush_stem 46 34 24 default:cactus 70 119 52 default:cave_ice 168 206 247 default:chest 149 115 69 default:chest_locked 149 115 69 default:chest_locked_open 149 115 69 default:chest_open 149 115 69 default:clay 183 183 183 default:cloud 255 255 255 default:coalblock 58 58 58 default:cobble 89 86 84 default:copperblock 193 126 65 default:coral_brown 146 113 77 default:coral_cyan 235 230 215 default:coral_green 235 230 215 default:coral_orange 197 68 17 default:coral_pink 235 230 215 default:coral_skeleton 235 230 215 default:desert_cobble 110 67 50 default:desert_sand 206 165 98 default:desert_sandstone 195 152 92 default:desert_sandstone_block 193 152 94 default:desert_sandstone_brick 191 151 95 default:desert_stone 130 79 61 default:desert_stone_block 131 80 61 default:desert_stonebrick 131 80 61 default:diamondblock 140 218 223 default:dirt 97 67 43 default:dirt_with_coniferous_litter 109 90 71 default:dirt_with_dry_grass 187 148 78 default:dirt_with_grass 64 111 26 default:dirt_with_grass_footsteps 64 111 26 default:dirt_with_rainforest_litter 76 39 10 default:dirt_with_snow 225 225 238 default:dry_dirt 178 136 90 default:dry_dirt_with_dry_grass 187 148 78 default:dry_grass_1 208 172 87 default:dry_grass_2 210 174 87 default:dry_grass_3 210 174 87 default:dry_grass_4 211 175 88 default:dry_grass_5 214 178 92 default:dry_shrub 103 67 18 default:emergent_jungle_sapling 51 40 16 default:fence_acacia_wood 151 62 39 default:fence_aspen_wood 210 199 170 default:fence_junglewood 57 39 14 default:fence_pine_wood 221 185 131 default:fence_rail_acacia_wood 150 61 39 default:fence_rail_aspen_wood 209 198 170 default:fence_rail_junglewood 56 39 14 default:fence_rail_pine_wood 221 184 130 default:fence_rail_wood 131 102 57 default:fence_wood 132 103 57 default:fern_1 85 118 51 default:fern_2 90 123 53 default:fern_3 91 125 54 default:furnace 101 98 96 default:furnace_active 101 98 96 default:glass 247 247 247 64 16 default:goldblock 231 203 35 default:grass_1 100 140 54 default:grass_2 98 139 55 default:grass_3 94 136 53 default:grass_4 89 133 48 default:grass_5 86 126 48 default:gravel 132 132 132 default:ice 168 206 247 default:junglegrass 67 110 28 default:jungleleaves 22 31 16 default:junglesapling 51 39 15 default:jungletree 121 97 62 default:junglewood 56 39 14 default:ladder_steel 132 132 132 default:ladder_wood 125 93 43 default:large_cactus_seedling 67 107 52 default:lava_flowing 255 100 0 default:lava_source 255 100 0 default:leaves 36 55 29 default:marram_grass_1 113 139 96 default:marram_grass_2 102 131 90 default:marram_grass_3 99 130 88 default:mese 222 222 0 default:mese_post_light 132 103 57 default:mese_post_light_acacia_wood 151 62 39 default:mese_post_light_aspen_wood 210 199 170 default:mese_post_light_junglewood 57 39 14 default:mese_post_light_pine_wood 221 185 131 default:meselamp 213 215 143 default:mossycobble 88 91 73 default:obsidian 21 24 29 default:obsidian_block 23 25 30 default:obsidian_glass 20 23 27 64 16 default:obsidianbrick 23 25 29 default:papyrus 97 134 38 default:permafrost 71 66 61 default:permafrost_with_moss 108 150 51 default:permafrost_with_stones 71 66 61 default:pine_bush_needles 16 50 19 default:pine_bush_sapling 58 51 40 default:pine_bush_stem 73 62 53 default:pine_needles 16 50 19 default:pine_sapling 41 48 26 default:pine_tree 191 165 132 default:pine_wood 221 185 130 default:river_water_flowing 39 66 106 128 224 default:river_water_source 39 66 106 128 224 default:sand 214 207 158 default:sand_with_kelp 214 207 158 default:sandstone 198 193 143 default:sandstone_block 195 191 142 default:sandstonebrick 194 190 141 default:sapling 67 63 41 default:sign_wall_steel 147 147 147 default:sign_wall_wood 148 103 66 default:silver_sand 193 191 179 default:silver_sandstone 195 192 181 default:silver_sandstone_block 192 190 180 default:silver_sandstone_brick 191 189 179 default:snow 225 225 238 default:snowblock 225 225 238 default:steelblock 195 195 195 default:stone 97 94 93 default:stone_block 100 97 96 default:stone_with_coal 97 94 93 default:stone_with_copper 97 94 93 default:stone_with_diamond 97 94 93 default:stone_with_gold 97 94 93 default:stone_with_iron 97 94 93 default:stone_with_mese 97 94 93 default:stone_with_tin 97 94 93 default:stonebrick 102 99 98 default:tinblock 150 150 150 default:torch 141 123 93 default:torch_ceiling 141 123 93 default:torch_wall 141 123 93 default:tree 179 145 99 default:water_flowing 39 66 106 128 224 default:water_source 39 66 106 128 224 default:wood 131 102 57 # doors doors:door_glass_a 245 245 245 64 16 doors:door_glass_b 245 245 245 64 16 doors:door_glass_c 245 245 245 64 16 doors:door_glass_d 245 245 245 64 16 doors:door_obsidian_glass_a 48 49 50 64 16 doors:door_obsidian_glass_b 48 49 50 64 16 doors:door_obsidian_glass_c 48 49 50 64 16 doors:door_obsidian_glass_d 48 49 50 64 16 doors:door_steel_a 203 203 203 doors:door_steel_b 203 203 203 doors:door_steel_c 203 203 203 doors:door_steel_d 203 203 203 doors:door_wood_a 89 68 37 doors:door_wood_b 89 68 37 doors:door_wood_c 89 68 37 doors:door_wood_d 89 68 37 doors:gate_acacia_wood_closed 150 61 39 doors:gate_acacia_wood_open 150 61 39 doors:gate_aspen_wood_closed 210 199 170 doors:gate_aspen_wood_open 210 199 170 doors:gate_junglewood_closed 56 39 14 doors:gate_junglewood_open 56 39 14 doors:gate_pine_wood_closed 221 185 130 doors:gate_pine_wood_open 221 185 130 doors:gate_wood_closed 131 102 57 doors:gate_wood_open 131 102 57 doors:trapdoor 130 100 51 doors:trapdoor_open 68 53 30 doors:trapdoor_steel 200 200 200 doors:trapdoor_steel_open 97 97 97 # farming farming:cotton_1 89 117 39 farming:cotton_2 89 116 38 farming:cotton_3 99 121 41 farming:cotton_4 108 114 47 farming:cotton_5 116 105 53 farming:cotton_6 121 95 59 farming:cotton_7 94 70 37 farming:cotton_8 122 108 93 farming:cotton_wild 111 111 101 farming:desert_sand_soil 161 132 72 farming:desert_sand_soil_wet 120 99 53 farming:dry_soil 178 136 90 farming:dry_soil_wet 178 136 90 farming:seed_cotton 92 87 60 farming:seed_wheat 177 161 96 farming:soil 97 67 43 farming:soil_wet 97 67 43 farming:straw 212 184 68 farming:wheat_1 110 175 36 farming:wheat_2 136 177 53 farming:wheat_3 163 182 84 farming:wheat_4 170 188 95 farming:wheat_5 171 179 97 farming:wheat_6 173 177 87 farming:wheat_7 193 181 83 farming:wheat_8 187 162 40 # fire fire:basic_flame 223 136 44 fire:permanent_flame 223 136 44 # fireflies fireflies:firefly_bottle 191 194 202 # flowers flowers:chrysanthemum_green 118 152 44 flowers:dandelion_white 199 191 176 flowers:dandelion_yellow 212 167 31 flowers:geranium 77 91 168 flowers:mushroom_brown 109 84 78 flowers:mushroom_red 195 102 102 flowers:rose 130 68 33 flowers:tulip 156 101 44 flowers:tulip_black 78 120 72 flowers:viola 115 69 184 flowers:waterlily 107 160 68 flowers:waterlily_waving 107 160 68 # stairs stairs:slab_acacia_wood 150 61 39 stairs:slab_aspen_wood 210 199 170 stairs:slab_brick 123 99 95 stairs:slab_bronzeblock 186 111 15 stairs:slab_cobble 89 86 84 stairs:slab_copperblock 193 126 65 stairs:slab_desert_cobble 110 67 50 stairs:slab_desert_sandstone 195 152 92 stairs:slab_desert_sandstone_block 193 152 94 stairs:slab_desert_sandstone_brick 191 151 95 stairs:slab_desert_stone 130 79 61 stairs:slab_desert_stone_block 131 80 61 stairs:slab_desert_stonebrick 131 80 61 stairs:slab_glass 247 247 247 stairs:slab_goldblock 231 203 35 stairs:slab_ice 168 206 247 stairs:slab_junglewood 56 39 14 stairs:slab_mossycobble 88 91 73 stairs:slab_obsidian 21 24 29 stairs:slab_obsidian_block 23 25 30 stairs:slab_obsidian_glass 20 23 27 stairs:slab_obsidianbrick 23 25 29 stairs:slab_pine_wood 221 185 130 stairs:slab_sandstone 198 193 143 stairs:slab_sandstone_block 195 191 142 stairs:slab_sandstonebrick 194 190 141 stairs:slab_silver_sandstone 195 192 181 stairs:slab_silver_sandstone_block 192 190 180 stairs:slab_silver_sandstone_brick 191 189 179 stairs:slab_snowblock 225 225 238 stairs:slab_steelblock 195 195 195 stairs:slab_stone 97 94 93 stairs:slab_stone_block 100 97 96 stairs:slab_stonebrick 102 99 98 stairs:slab_straw 212 184 68 stairs:slab_tinblock 150 150 150 stairs:slab_wood 131 102 57 stairs:stair_acacia_wood 150 61 39 stairs:stair_aspen_wood 210 199 170 stairs:stair_brick 123 99 95 stairs:stair_bronzeblock 186 111 15 stairs:stair_cobble 89 86 84 stairs:stair_copperblock 193 126 65 stairs:stair_desert_cobble 110 67 50 stairs:stair_desert_sandstone 195 152 92 stairs:stair_desert_sandstone_block 193 152 94 stairs:stair_desert_sandstone_brick 191 151 95 stairs:stair_desert_stone 130 79 61 stairs:stair_desert_stone_block 131 80 61 stairs:stair_desert_stonebrick 131 80 61 stairs:stair_glass 249 249 249 stairs:stair_goldblock 231 203 35 stairs:stair_ice 168 206 247 stairs:stair_inner_acacia_wood 150 61 39 stairs:stair_inner_aspen_wood 210 199 170 stairs:stair_inner_brick 123 99 95 stairs:stair_inner_bronzeblock 186 111 15 stairs:stair_inner_cobble 89 86 84 stairs:stair_inner_copperblock 193 126 65 stairs:stair_inner_desert_cobble 110 67 50 stairs:stair_inner_desert_sandstone 195 152 92 stairs:stair_inner_desert_sandstone_block 193 152 94 stairs:stair_inner_desert_sandstone_brick 191 151 95 stairs:stair_inner_desert_stone 130 79 61 stairs:stair_inner_desert_stone_block 131 80 61 stairs:stair_inner_desert_stonebrick 131 80 61 stairs:stair_inner_glass 250 250 250 stairs:stair_inner_goldblock 231 203 35 stairs:stair_inner_ice 168 206 247 stairs:stair_inner_junglewood 56 39 14 stairs:stair_inner_mossycobble 88 91 73 stairs:stair_inner_obsidian 21 24 29 stairs:stair_inner_obsidian_block 23 25 30 stairs:stair_inner_obsidian_glass 20 22 27 stairs:stair_inner_obsidianbrick 23 25 29 stairs:stair_inner_pine_wood 221 185 130 stairs:stair_inner_sandstone 198 193 143 stairs:stair_inner_sandstone_block 195 191 142 stairs:stair_inner_sandstonebrick 194 190 141 stairs:stair_inner_silver_sandstone 195 192 181 stairs:stair_inner_silver_sandstone_block 192 190 180 stairs:stair_inner_silver_sandstone_brick 191 189 179 stairs:stair_inner_snowblock 225 225 238 stairs:stair_inner_steelblock 195 195 195 stairs:stair_inner_stone 97 94 93 stairs:stair_inner_stone_block 100 97 96 stairs:stair_inner_stonebrick 102 99 98 stairs:stair_inner_straw 212 184 68 stairs:stair_inner_tinblock 150 150 150 stairs:stair_inner_wood 131 102 57 stairs:stair_junglewood 56 39 14 stairs:stair_mossycobble 88 91 73 stairs:stair_obsidian 21 24 29 stairs:stair_obsidian_block 23 25 30 stairs:stair_obsidian_glass 20 22 27 stairs:stair_obsidianbrick 23 25 29 stairs:stair_outer_acacia_wood 150 61 39 stairs:stair_outer_aspen_wood 210 199 170 stairs:stair_outer_brick 123 99 95 stairs:stair_outer_bronzeblock 186 111 15 stairs:stair_outer_cobble 89 86 84 stairs:stair_outer_copperblock 193 126 65 stairs:stair_outer_desert_cobble 110 67 50 stairs:stair_outer_desert_sandstone 195 152 92 stairs:stair_outer_desert_sandstone_block 193 152 94 stairs:stair_outer_desert_sandstone_brick 191 151 95 stairs:stair_outer_desert_stone 130 79 61 stairs:stair_outer_desert_stone_block 131 80 61 stairs:stair_outer_desert_stonebrick 131 80 61 stairs:stair_outer_glass 250 250 250 stairs:stair_outer_goldblock 231 203 35 stairs:stair_outer_ice 168 206 247 stairs:stair_outer_junglewood 56 39 14 stairs:stair_outer_mossycobble 88 91 73 stairs:stair_outer_obsidian 21 24 29 stairs:stair_outer_obsidian_block 23 25 30 stairs:stair_outer_obsidian_glass 20 22 27 stairs:stair_outer_obsidianbrick 23 25 29 stairs:stair_outer_pine_wood 221 185 130 stairs:stair_outer_sandstone 198 193 143 stairs:stair_outer_sandstone_block 195 191 142 stairs:stair_outer_sandstonebrick 194 190 141 stairs:stair_outer_silver_sandstone 195 192 181 stairs:stair_outer_silver_sandstone_block 192 190 180 stairs:stair_outer_silver_sandstone_brick 191 189 179 stairs:stair_outer_snowblock 225 225 238 stairs:stair_outer_steelblock 195 195 195 stairs:stair_outer_stone 97 94 93 stairs:stair_outer_stone_block 100 97 96 stairs:stair_outer_stonebrick 102 99 98 stairs:stair_outer_straw 212 184 68 stairs:stair_outer_tinblock 150 150 150 stairs:stair_outer_wood 131 102 57 stairs:stair_pine_wood 221 185 130 stairs:stair_sandstone 198 193 143 stairs:stair_sandstone_block 195 191 142 stairs:stair_sandstonebrick 194 190 141 stairs:stair_silver_sandstone 195 192 181 stairs:stair_silver_sandstone_block 192 190 180 stairs:stair_silver_sandstone_brick 191 189 179 stairs:stair_snowblock 225 225 238 stairs:stair_steelblock 195 195 195 stairs:stair_stone 97 94 93 stairs:stair_stone_block 100 97 96 stairs:stair_stonebrick 102 99 98 stairs:stair_straw 212 184 68 stairs:stair_tinblock 150 150 150 stairs:stair_wood 131 102 57 # tnt tnt:gunpowder 12 12 12 tnt:gunpowder_burning 156 143 7 tnt:tnt 196 0 0 tnt:tnt_burning 201 41 0 # vessels vessels:drinking_glass 207 214 228 vessels:glass_bottle 189 192 204 vessels:shelf 131 102 57 vessels:steel_bottle 194 193 193 # walls walls:cobble 89 86 84 walls:desertcobble 110 67 50 walls:mossycobble 88 91 73 # wool wool:black 30 30 30 wool:blue 0 73 146 wool:brown 88 44 0 wool:cyan 0 132 140 wool:dark_green 33 103 0 wool:dark_grey 60 60 60 wool:green 93 218 28 wool:grey 133 133 133 wool:magenta 201 3 112 wool:orange 214 83 22 wool:pink 255 133 133 wool:red 170 18 18 wool:violet 93 5 169 wool:white 220 220 220 wool:yellow 254 226 16 # xpanes xpanes:bar 114 114 114 64 16 xpanes:bar_flat 114 114 114 64 16 xpanes:door_steel_bar_a 133 133 133 64 16 xpanes:door_steel_bar_b 133 133 133 64 16 xpanes:door_steel_bar_c 133 133 133 64 16 xpanes:door_steel_bar_d 133 133 133 64 16 xpanes:obsidian_pane 16 17 18 64 16 xpanes:obsidian_pane_flat 16 17 18 64 16 xpanes:pane 249 249 249 64 16 xpanes:pane_flat 249 249 249 64 16 xpanes:trapdoor_steel_bar 127 127 127 64 16 xpanes:trapdoor_steel_bar_open 77 77 77 64 16 luanti-org-minetestmapper-637755d/minetestmapper.6000066400000000000000000000064131477524601400222560ustar00rootroot00000000000000.TH MINETESTMAPPER 6 .SH NAME minetestmapper \- generate an overview image of a Luanti map .SH SYNOPSIS .B minetestmapper \fB\-i\fR \fIworld_path\fR \fB\-o\fR \fIoutput_image\fR .PP See additional optional parameters below. .SH DESCRIPTION .B minetestmapper generates a top-down overview image of a Luanti map. This is a port of the obsolete minetestmapper.py script to C++, that is both faster and provides more features. Minetestmapper ships with a colors.txt file suitable for Minetest Game, if you use a different game or have mods installed you should generate a matching colors.txt for better results (colors will be missing otherwise). .SH MANDATORY PARAMETERS .TP .BR \-i " " \fIworld_path\fR Input world path .TP .BR \-o " " \fIoutput_image\fR Path to output image .SH OPTIONAL PARAMETERS .TP .BR \-\-bgcolor " " \fIcolor\fR Background color of image, e.g. "--bgcolor #ffffff" .TP .BR \-\-scalecolor " " \fIcolor\fR Color of scale marks and text, e.g. "--scalecolor #000000" .TP .BR \-\-playercolor " " \fIcolor\fR Color of player indicators, e.g. "--playercolor #ff0000" .TP .BR \-\-origincolor " " \fIcolor\fR Color of origin indicator, e.g. "--origincolor #ff0000" .TP .BR \-\-drawscale Draw scale(s) with tick marks and numbers .TP .BR \-\-drawplayers Draw player indicators with name .TP .BR \-\-draworigin Draw origin indicator .TP .BR \-\-drawalpha Allow nodes to be drawn with transparency (such as water) .TP .BR \-\-noshading Don't draw shading on nodes .TP .BR \-\-noemptyimage Don't output anything when the image would be empty .TP .BR \-\-verbose Enable verbose log output. .TP .BR \-\-min-y " " \fInumber\fR Don't draw nodes below this Y value, e.g. "--min-y -25" .TP .BR \-\-max-y " " \fInumber\fR Don't draw nodes above this Y value, e.g. "--max-y 75" .TP .BR \-\-backend " " \fIbackend\fR Override auto-detected map backend; supported: \fIsqlite3\fP, \fIleveldb\fP, \fIredis\fP, \fIpostgresql\fP, e.g. "--backend leveldb" .TP .BR \-\-geometry " " \fIgeometry\fR Limit area to specific geometry (\fIx:z+w+h\fP where x and z specify the lower left corner), e.g. "--geometry -800:-800+1600+1600" The coordinates are specified with the same axes as in-game. The Z axis becomes Y when projected on the image. .TP .BR \-\-extent Don't render the image, just print the extent of the map that would be generated, in the same format as the geometry above. .TP .BR \-\-zoom " " \fIfactor\fR Zoom the image by using more than one pixel per node, e.g. "--zoom 4" .TP .BR \-\-colors " " \fIpath\fR Override auto-detected path to colors.txt, e.g. "--colors ../world/mycolors.txt" .TP .BR \-\-scales " " \fIedges\fR Draw scales on specified image edges (letters \fIt b l r\fP meaning top, bottom, left and right), e.g. "--scales tbr" .TP .BR \-\-exhaustive " " \fImode\fR Select if database should be traversed exhaustively or using range queries, available: \fInever\fP, \fIy\fP, \fIfull\fP, \fIauto\fP Defaults to \fIauto\fP. You shouldn't need to change this, as minetestmapper tries to automatically picks the best option. .TP .BR \-\-dumpblock " " \fIpos\fR Instead of rendering anything try to load the block at the given position (\fIx,y,z\fR) and print its raw data as hexadecimal. .SH MORE INFORMATION Website: https://github.com/luanti-org/minetestmapper .SH MAN PAGE AUTHOR Daniel Moerner luanti-org-minetestmapper-637755d/src/000077500000000000000000000000001477524601400177155ustar00rootroot00000000000000luanti-org-minetestmapper-637755d/src/BlockDecoder.cpp000066400000000000000000000100371477524601400227420ustar00rootroot00000000000000#include #include #include #include "BlockDecoder.h" #include "ZlibDecompressor.h" #include "log.h" static inline uint16_t readU16(const unsigned char *data) { return data[0] << 8 | data[1]; } static inline uint16_t readBlockContent(const unsigned char *mapData, u8 contentWidth, unsigned int datapos) { if (contentWidth == 2) { size_t index = datapos << 1; return (mapData[index] << 8) | mapData[index + 1]; } else { u8 param = mapData[datapos]; if (param <= 0x7f) return param; else return (param << 4) | (mapData[datapos + 0x2000] >> 4); } } BlockDecoder::BlockDecoder() { reset(); } void BlockDecoder::reset() { m_blockAirId = -1; m_blockIgnoreId = -1; m_nameMap.clear(); m_version = 0; m_contentWidth = 0; m_mapData.clear(); } void BlockDecoder::decode(const ustring &datastr) { const unsigned char *data = datastr.c_str(); size_t length = datastr.length(); // TODO: bounds checks uint8_t version = data[0]; if (version < 22) { std::ostringstream oss; oss << "Unsupported map version " << (int)version; throw std::runtime_error(oss.str()); } m_version = version; if (version >= 29) { // decompress whole block at once m_zstd_decompressor.setData(data, length, 1); m_zstd_decompressor.decompress(m_scratch); data = m_scratch.c_str(); length = m_scratch.size(); } size_t dataOffset = 0; if (version >= 29) dataOffset = 7; else if (version >= 27) dataOffset = 4; else dataOffset = 2; auto decode_mapping = [&] () { dataOffset++; // mapping version uint16_t numMappings = readU16(data + dataOffset); dataOffset += 2; for (int i = 0; i < numMappings; ++i) { uint16_t nodeId = readU16(data + dataOffset); dataOffset += 2; uint16_t nameLen = readU16(data + dataOffset); dataOffset += 2; std::string name(reinterpret_cast(data) + dataOffset, nameLen); if (name == "air") m_blockAirId = nodeId; else if (name == "ignore") m_blockIgnoreId = nodeId; else m_nameMap[nodeId] = name; dataOffset += nameLen; } }; if (version >= 29) decode_mapping(); uint8_t contentWidth = data[dataOffset]; dataOffset++; uint8_t paramsWidth = data[dataOffset]; dataOffset++; if (contentWidth != 1 && contentWidth != 2) throw std::runtime_error("unsupported map version (contentWidth)"); if (paramsWidth != 2) throw std::runtime_error("unsupported map version (paramsWidth)"); m_contentWidth = contentWidth; if (version >= 29) { size_t mapDataSize = (contentWidth + paramsWidth) * 4096; m_mapData.assign(data + dataOffset, mapDataSize); return; // we have read everything we need and can return early } // version < 29 ZlibDecompressor decompressor(data, length); decompressor.setSeekPos(dataOffset); decompressor.decompress(m_mapData); decompressor.decompress(m_scratch); // unused metadata dataOffset = decompressor.seekPos(); // Skip unused node timers if (version == 23) dataOffset += 1; if (version == 24) { uint8_t ver = data[dataOffset++]; if (ver == 1) { uint16_t num = readU16(data + dataOffset); dataOffset += 2; dataOffset += 10 * num; } } // Skip unused static objects dataOffset++; // Skip static object version int staticObjectCount = readU16(data + dataOffset); dataOffset += 2; for (int i = 0; i < staticObjectCount; ++i) { dataOffset += 13; uint16_t dataSize = readU16(data + dataOffset); dataOffset += dataSize + 2; } dataOffset += 4; // Skip timestamp // Read mapping decode_mapping(); } bool BlockDecoder::isEmpty() const { // only contains ignore and air nodes? return m_nameMap.empty(); } const static std::string empty; const std::string &BlockDecoder::getNode(u8 x, u8 y, u8 z) const { unsigned int position = x + (y << 4) + (z << 8); uint16_t content = readBlockContent(m_mapData.c_str(), m_contentWidth, position); if (content == m_blockAirId || content == m_blockIgnoreId) return empty; NameMap::const_iterator it = m_nameMap.find(content); if (it == m_nameMap.end()) { errorstream << "Skipping node with invalid ID." << std::endl; return empty; } return it->second; } luanti-org-minetestmapper-637755d/src/BlockDecoder.h000066400000000000000000000011611477524601400224050ustar00rootroot00000000000000#pragma once #include #include #include "types.h" #include class BlockDecoder { public: BlockDecoder(); void reset(); void decode(const ustring &data); bool isEmpty() const; // returns "" for air, ignore and invalid nodes const std::string &getNode(u8 x, u8 y, u8 z) const; private: typedef std::unordered_map NameMap; NameMap m_nameMap; uint16_t m_blockAirId, m_blockIgnoreId; u8 m_version, m_contentWidth; ustring m_mapData; // cached allocations/instances for performance ZstdDecompressor m_zstd_decompressor; ustring m_scratch; }; luanti-org-minetestmapper-637755d/src/Image.cpp000066400000000000000000000060221477524601400214430ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include "Image.h" #ifndef NDEBUG #define SIZECHECK(x, y) check_bounds((x), (y), m_width, m_height) #else #define SIZECHECK(x, y) do {} while(0) #endif // ARGB but with inverted alpha static inline int color2int(const Color &c) { u8 a = (255 - c.a) * gdAlphaMax / 255; return (a << 24) | (c.r << 16) | (c.g << 8) | c.b; } static inline Color int2color(int c) { Color c2; c2.b = c & 0xff; c2.g = (c >> 8) & 0xff; c2.r = (c >> 16) & 0xff; u8 a = (c >> 24) & 0xff; c2.a = 255 - (a*255 / gdAlphaMax); return c2; } #ifndef NDEBUG static inline void check_bounds(int x, int y, int width, int height) { if(x < 0 || x >= width) { std::ostringstream oss; oss << "Access outside image bounds (x), 0 < " << x << " < " << width << " is false."; throw std::out_of_range(oss.str()); } if(y < 0 || y >= height) { std::ostringstream oss; oss << "Access outside image bounds (y), 0 < " << y << " < " << height << " is false."; throw std::out_of_range(oss.str()); } } #endif Image::Image(int width, int height) : m_width(width), m_height(height), m_image(nullptr) { SIZECHECK(0, 0); m_image = gdImageCreateTrueColor(m_width, m_height); } Image::~Image() { gdImageDestroy(m_image); } void Image::setPixel(int x, int y, const Color &c) { SIZECHECK(x, y); m_image->tpixels[y][x] = color2int(c); } Color Image::getPixel(int x, int y) { SIZECHECK(x, y); return int2color(m_image->tpixels[y][x]); } void Image::drawLine(int x1, int y1, int x2, int y2, const Color &c) { SIZECHECK(x1, y1); SIZECHECK(x2, y2); gdImageLine(m_image, x1, y1, x2, y2, color2int(c)); } void Image::drawText(int x, int y, const std::string &s, const Color &c) { SIZECHECK(x, y); gdImageString(m_image, gdFontGetMediumBold(), x, y, (unsigned char*) s.c_str(), color2int(c)); } void Image::drawFilledRect(int x, int y, int w, int h, const Color &c) { SIZECHECK(x, y); SIZECHECK(x + w - 1, y + h - 1); gdImageFilledRectangle(m_image, x, y, x + w - 1, y + h - 1, color2int(c)); } void Image::drawCircle(int x, int y, int diameter, const Color &c) { SIZECHECK(x, y); gdImageArc(m_image, x, y, diameter, diameter, 0, 360, color2int(c)); } void Image::save(const std::string &filename) { #if (GD_MAJOR_VERSION == 2 && GD_MINOR_VERSION == 1 && GD_RELEASE_VERSION >= 1) || (GD_MAJOR_VERSION == 2 && GD_MINOR_VERSION > 1) || GD_MAJOR_VERSION > 2 const char *f = filename.c_str(); if (gdSupportsFileType(f, 1) == GD_FALSE) throw std::runtime_error("Image format not supported by gd"); if (gdImageFile(m_image, f) == GD_FALSE) throw std::runtime_error("Error saving image"); #else if (filename.compare(filename.length() - 4, 4, ".png") != 0) throw std::runtime_error("Only PNG is supported"); FILE *f = fopen(filename.c_str(), "wb"); if (!f) { std::ostringstream oss; oss << "Error opening image file: " << std::strerror(errno); throw std::runtime_error(oss.str()); } gdImagePng(m_image, f); fclose(f); #endif } luanti-org-minetestmapper-637755d/src/Image.h000066400000000000000000000015011477524601400211050ustar00rootroot00000000000000#pragma once #include "types.h" #include #include struct Color { Color() : r(0), g(0), b(0), a(0) {}; Color(u8 r, u8 g, u8 b) : r(r), g(g), b(b), a(255) {}; Color(u8 r, u8 g, u8 b, u8 a) : r(r), g(g), b(b), a(a) {}; u8 r, g, b, a; }; class Image { public: Image(int width, int height); ~Image(); Image(const Image&) = delete; Image& operator=(const Image&) = delete; void setPixel(int x, int y, const Color &c); Color getPixel(int x, int y); void drawLine(int x1, int y1, int x2, int y2, const Color &c); void drawText(int x, int y, const std::string &s, const Color &c); void drawFilledRect(int x, int y, int w, int h, const Color &c); void drawCircle(int x, int y, int diameter, const Color &c); void save(const std::string &filename); private: int m_width, m_height; gdImagePtr m_image; }; luanti-org-minetestmapper-637755d/src/PixelAttributes.cpp000066400000000000000000000016721477524601400235570ustar00rootroot00000000000000#include #include "PixelAttributes.h" PixelAttributes::PixelAttributes(): m_width(0) { for (size_t i = 0; i < LineCount; ++i) { m_pixelAttributes[i] = nullptr; } } PixelAttributes::~PixelAttributes() { freeAttributes(); } void PixelAttributes::setWidth(int width) { freeAttributes(); m_width = width + 1; // 1px gradient calculation for (size_t i = 0; i < LineCount; ++i) { m_pixelAttributes[i] = new PixelAttribute[m_width]; } } void PixelAttributes::scroll() { size_t lineLength = m_width * sizeof(PixelAttribute); memcpy(m_pixelAttributes[FirstLine], m_pixelAttributes[LastLine], lineLength); for (size_t i = 1; i < LineCount - 1; ++i) { memcpy(m_pixelAttributes[i], m_pixelAttributes[EmptyLine], lineLength); } } void PixelAttributes::freeAttributes() { for (size_t i = 0; i < LineCount; ++i) { if (m_pixelAttributes[i] != nullptr) { delete[] m_pixelAttributes[i]; m_pixelAttributes[i] = nullptr; } } } luanti-org-minetestmapper-637755d/src/PixelAttributes.h000066400000000000000000000013511477524601400232160ustar00rootroot00000000000000#pragma once #include #include #define BLOCK_SIZE 16 struct PixelAttribute { PixelAttribute() : height(INT16_MIN), thickness(0) {}; int16_t height; uint8_t thickness; inline bool valid_height() const { return height != INT16_MIN; } }; class PixelAttributes { public: PixelAttributes(); virtual ~PixelAttributes(); void setWidth(int width); void scroll(); inline PixelAttribute &attribute(int z, int x) { return m_pixelAttributes[z + 1][x + 1]; }; private: void freeAttributes(); private: enum Line { FirstLine = 0, LastLine = BLOCK_SIZE, EmptyLine = BLOCK_SIZE + 1, LineCount = BLOCK_SIZE + 2 }; PixelAttribute *m_pixelAttributes[BLOCK_SIZE + 2]; // 1px gradient + empty int m_width; }; luanti-org-minetestmapper-637755d/src/PlayerAttributes.cpp000066400000000000000000000072261477524601400237330ustar00rootroot00000000000000#include #include #include #include #include // usleep #include "config.h" #include "PlayerAttributes.h" #include "util.h" #include "log.h" #include "db-sqlite3.h" // SQLite3Base namespace { bool parse_pos(std::string position, Player &dst) { if (position.empty()) return false; if (position.front() == '(' && position.back() == ')') position = position.substr(1, position.size() - 2); std::istringstream iss(position); if (!(iss >> dst.x)) return false; if (iss.get() != ',') return false; if (!(iss >> dst.y)) return false; if (iss.get() != ',') return false; if (!(iss >> dst.z)) return false; return iss.eof(); } // Helper classes per backend class FilesReader { std::string path; DIR *dir = nullptr; public: FilesReader(const std::string &path) : path(path) { dir = opendir(path.c_str()); } ~FilesReader() { if (dir) closedir(dir); } void read(PlayerAttributes::Players &dest); }; class SQLiteReader : SQLite3Base { sqlite3_stmt *stmt_get_player_pos = NULL; public: SQLiteReader(const std::string &database) { openDatabase(database.c_str()); } ~SQLiteReader() { sqlite3_finalize(stmt_get_player_pos); } void read(PlayerAttributes::Players &dest); }; } void FilesReader::read(PlayerAttributes::Players &dest) { if (!dir) return; struct dirent *ent; std::string name, position; while ((ent = readdir(dir)) != NULL) { if (ent->d_name[0] == '.') continue; std::ifstream in(path + PATH_SEPARATOR + ent->d_name); if (!in.good()) continue; name = read_setting("name", in); position = read_setting("position", in); Player player; player.name = name; if (!parse_pos(position, player)) { errorstream << "Failed to parse position '" << position << "' in " << ent->d_name << std::endl; continue; } player.x /= 10.0f; player.y /= 10.0f; player.z /= 10.0f; dest.push_back(std::move(player)); } } #define SQLRES(r, good) check_result(r, good) #define SQLOK(r) SQLRES(r, SQLITE_OK) void SQLiteReader::read(PlayerAttributes::Players &dest) { SQLOK(prepare(stmt_get_player_pos, "SELECT name, posX, posY, posZ FROM player")); int result; while ((result = sqlite3_step(stmt_get_player_pos)) != SQLITE_DONE) { if (result == SQLITE_BUSY) { // Wait some time and try again usleep(10000); } else if (result != SQLITE_ROW) { throw std::runtime_error(sqlite3_errmsg(db)); } Player player; player.name = read_str(stmt_get_player_pos, 0); player.x = sqlite3_column_double(stmt_get_player_pos, 1); player.y = sqlite3_column_double(stmt_get_player_pos, 2); player.z = sqlite3_column_double(stmt_get_player_pos, 3); player.x /= 10.0f; player.y /= 10.0f; player.z /= 10.0f; dest.push_back(std::move(player)); } } /**********/ PlayerAttributes::PlayerAttributes(const std::string &worldDir) { std::ifstream ifs(worldDir + "world.mt"); if (!ifs.good()) throw std::runtime_error("Failed to read world.mt"); std::string backend = read_setting_default("player_backend", ifs, "files"); ifs.close(); verbosestream << "Player backend: " << backend << std::endl; if (backend == "files") FilesReader(worldDir + "players").read(m_players); else if (backend == "sqlite3") SQLiteReader(worldDir + "players.sqlite").read(m_players); else throw std::runtime_error(std::string("Unknown player backend: ") + backend); verbosestream << "Loaded " << m_players.size() << " players" << std::endl; } PlayerAttributes::Players::const_iterator PlayerAttributes::begin() const { return m_players.cbegin(); } PlayerAttributes::Players::const_iterator PlayerAttributes::end() const { return m_players.cend(); } luanti-org-minetestmapper-637755d/src/PlayerAttributes.h000066400000000000000000000005151477524601400233720ustar00rootroot00000000000000#pragma once #include #include struct Player { std::string name; float x, y, z; }; class PlayerAttributes { public: typedef std::list Players; PlayerAttributes(const std::string &worldDir); Players::const_iterator begin() const; Players::const_iterator end() const; private: Players m_players; }; luanti-org-minetestmapper-637755d/src/TileGenerator.cpp000066400000000000000000000566101477524601400231750ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #include #include #include #include "TileGenerator.h" #include "config.h" #include "PlayerAttributes.h" #include "BlockDecoder.h" #include "Image.h" #include "util.h" #include "log.h" #include "db-sqlite3.h" #if USE_POSTGRESQL #include "db-postgresql.h" #endif #if USE_LEVELDB #include "db-leveldb.h" #endif #if USE_REDIS #include "db-redis.h" #endif #ifndef __has_builtin #define __has_builtin(x) 0 #endif // saturating multiplication template::value>::type> inline T sat_mul(T a, T b) { #if __has_builtin(__builtin_mul_overflow) T res; if (__builtin_mul_overflow(a, b, &res)) return std::numeric_limits::max(); return res; #else // WARNING: the fallback implementation is incorrect since we compute ceil(log(x)) not log(x) // but that's good enough for our usecase... const int bits = sizeof(T) * 8; int hb_a = 0, hb_b = 0; for (int i = bits - 1; i >= 0; i--) { if (a & (static_cast(1) << i)) { hb_a = i; break; } } for (int i = bits - 1; i >= 0; i--) { if (b & (static_cast(1) << i)) { hb_b = i; break; } } // log2(a) + log2(b) >= log2(MAX) <=> calculation will overflow if (hb_a + hb_b >= bits) return std::numeric_limits::max(); return a * b; #endif } template inline T sat_mul(T a, T b, T c) { return sat_mul(sat_mul(a, b), c); } // rounds n (away from 0) to a multiple of f while preserving the sign of n static int round_multiple_nosign(int n, int f) { int abs_n, sign; abs_n = (n >= 0) ? n : -n; sign = (n >= 0) ? 1 : -1; if (abs_n % f == 0) return n; // n == abs_n * sign else return sign * (abs_n + f - (abs_n % f)); } static inline unsigned int colorSafeBounds(int channel) { return mymin(mymax(channel, 0), 255); } static Color parseColor(const std::string &color) { if (color.length() != 7) throw std::runtime_error("Color needs to be 7 characters long"); if (color[0] != '#') throw std::runtime_error("Color needs to begin with #"); unsigned long col = strtoul(color.c_str() + 1, NULL, 16); u8 b, g, r; b = col & 0xff; g = (col >> 8) & 0xff; r = (col >> 16) & 0xff; return Color(r, g, b); } static Color mixColors(Color a, Color b) { Color result; float a1 = a.a / 255.0f; float a2 = b.a / 255.0f; result.r = (int) (a1 * a.r + a2 * (1 - a1) * b.r); result.g = (int) (a1 * a.g + a2 * (1 - a1) * b.g); result.b = (int) (a1 * a.b + a2 * (1 - a1) * b.b); result.a = (int) (255 * (a1 + a2 * (1 - a1))); return result; } TileGenerator::TileGenerator(): m_bgColor(255, 255, 255), m_scaleColor(0, 0, 0), m_originColor(255, 0, 0), m_playerColor(255, 0, 0), m_drawOrigin(false), m_drawPlayers(false), m_drawScale(false), m_drawAlpha(false), m_shading(true), m_dontWriteEmpty(false), m_backend(""), m_xBorder(0), m_yBorder(0), m_db(NULL), m_image(NULL), m_xMin(INT_MAX), m_xMax(INT_MIN), m_zMin(INT_MAX), m_zMax(INT_MIN), m_yMin(INT16_MIN), m_yMax(INT16_MAX), m_geomX(-2048), m_geomY(-2048), m_geomX2(2048), m_geomY2(2048), m_exhaustiveSearch(EXH_AUTO), m_renderedAny(false), m_zoom(1), m_scales(SCALE_LEFT | SCALE_TOP), m_progressMax(0), m_progressLast(-1) { } TileGenerator::~TileGenerator() { closeDatabase(); delete m_image; m_image = nullptr; } void TileGenerator::setBgColor(const std::string &bgColor) { m_bgColor = parseColor(bgColor); } void TileGenerator::setScaleColor(const std::string &scaleColor) { m_scaleColor = parseColor(scaleColor); } void TileGenerator::setOriginColor(const std::string &originColor) { m_originColor = parseColor(originColor); } void TileGenerator::setPlayerColor(const std::string &playerColor) { m_playerColor = parseColor(playerColor); } void TileGenerator::setZoom(int zoom) { if (zoom < 1) throw std::runtime_error("Zoom level needs to be a number: 1 or higher"); m_zoom = zoom; } void TileGenerator::setScales(uint flags) { m_scales = flags; } void TileGenerator::setDrawOrigin(bool drawOrigin) { m_drawOrigin = drawOrigin; } void TileGenerator::setDrawPlayers(bool drawPlayers) { m_drawPlayers = drawPlayers; } void TileGenerator::setDrawScale(bool drawScale) { m_drawScale = drawScale; } void TileGenerator::setDrawAlpha(bool drawAlpha) { m_drawAlpha = drawAlpha; } void TileGenerator::setShading(bool shading) { m_shading = shading; } void TileGenerator::setBackend(std::string backend) { m_backend = backend; } void TileGenerator::setGeometry(int x, int y, int w, int h) { assert(w > 0 && h > 0); m_geomX = round_multiple_nosign(x, 16) / 16; m_geomY = round_multiple_nosign(y, 16) / 16; m_geomX2 = round_multiple_nosign(x + w, 16) / 16; m_geomY2 = round_multiple_nosign(y + h, 16) / 16; } void TileGenerator::setMinY(int y) { m_yMin = y; if (m_yMin > m_yMax) std::swap(m_yMin, m_yMax); } void TileGenerator::setMaxY(int y) { m_yMax = y; if (m_yMin > m_yMax) std::swap(m_yMin, m_yMax); } void TileGenerator::setExhaustiveSearch(int mode) { m_exhaustiveSearch = mode; } void TileGenerator::setDontWriteEmpty(bool f) { m_dontWriteEmpty = f; } void TileGenerator::parseColorsFile(const std::string &fileName) { std::ifstream in(fileName); if (!in.good()) throw std::runtime_error("Specified colors file could not be found"); verbosestream << "Parsing colors.txt: " << fileName << std::endl; parseColorsStream(in); } void TileGenerator::printGeometry(const std::string &input_path) { setExhaustiveSearch(EXH_NEVER); openDb(input_path); loadBlocks(); std::cout << "Map extent: " << m_xMin*16 << ":" << m_zMin*16 << "+" << (m_xMax - m_xMin+1)*16 << "+" << (m_zMax - m_zMin+1)*16 << std::endl; closeDatabase(); } void TileGenerator::dumpBlock(const std::string &input_path, BlockPos pos) { openDb(input_path); BlockList list; std::vector positions; positions.emplace_back(pos); m_db->getBlocksByPos(list, positions); if (!list.empty()) { const ustring &data = list.begin()->second; for (u8 c : data) printf("%02x", static_cast(c)); printf("\n"); } closeDatabase(); } void TileGenerator::generate(const std::string &input_path, const std::string &output) { openDb(input_path); loadBlocks(); // If we needed to load positions and there are none, that means the // result will be empty. if (m_dontWriteEmpty && (m_exhaustiveSearch == EXH_NEVER || m_exhaustiveSearch == EXH_Y) && m_positions.empty()) { verbosestream << "Result is empty (no positions)" << std::endl; return; } createImage(); renderMap(); if (m_dontWriteEmpty && !m_renderedAny) { verbosestream << "Result is empty (no pixels)" << std::endl; printUnknown(); return; } closeDatabase(); if (m_drawScale) { renderScale(); } if (m_drawOrigin) { renderOrigin(); } if (m_drawPlayers) { renderPlayers(input_path); } writeImage(output); printUnknown(); } void TileGenerator::parseColorsStream(std::istream &in) { char line[512]; while (in.good()) { in.getline(line, sizeof(line)); for (char *p = line; *p; p++) { if (*p != '#') continue; *p = '\0'; // Cut off at the first # break; } if(!line[0]) continue; char name[200 + 1] = {0}; unsigned int r, g, b, a = 255, t = 0; int items = sscanf(line, "%200s %u %u %u %u %u", name, &r, &g, &b, &a, &t); if (items < 4) { errorstream << "Failed to parse color entry '" << line << "'" << std::endl; continue; } m_colorMap[name] = ColorEntry(r, g, b, a, t); } } std::set TileGenerator::getSupportedBackends() { std::set r; r.insert("sqlite3"); #if USE_POSTGRESQL r.insert("postgresql"); #endif #if USE_LEVELDB r.insert("leveldb"); #endif #if USE_REDIS r.insert("redis"); #endif return r; } void TileGenerator::openDb(const std::string &input_path) { if (dir_exists(input_path.c_str())) { // ok } else if (file_exists(input_path.c_str())) { throw std::runtime_error("Input path is a file, it should point to the world folder instead"); } else { throw std::runtime_error("Input path does not exist"); } std::string input = input_path; if (input.back() != PATH_SEPARATOR) input += PATH_SEPARATOR; std::ifstream ifs(input + "world.mt"); std::string backend = m_backend; if (backend.empty() && !ifs.good()) { throw std::runtime_error("Failed to open world.mt"); } else if (backend.empty()) { backend = read_setting_default("backend", ifs, "sqlite3"); } if (backend == "dummy") { throw std::runtime_error("This map uses the dummy backend and contains no data"); } else if (backend == "sqlite3") { m_db = new DBSQLite3(input); #if USE_POSTGRESQL } else if (backend == "postgresql") { m_db = new DBPostgreSQL(input); #endif #if USE_LEVELDB } else if (backend == "leveldb") { m_db = new DBLevelDB(input); #endif #if USE_REDIS } else if (backend == "redis") { m_db = new DBRedis(input); #endif } else { throw std::runtime_error(std::string("Unknown map backend: ") + backend); } if (!read_setting_default("readonly_backend", ifs, "").empty()) { errorstream << "Warning: Map with readonly_backend is not supported. " "The result may be incomplete." << std::endl; } // Determine how we're going to traverse the database (heuristic) if (m_exhaustiveSearch == EXH_AUTO) { size_t y_range = (m_yMax / 16 + 1) - (m_yMin / 16); size_t blocks = sat_mul(m_geomX2 - m_geomX, y_range, m_geomY2 - m_geomY); verbosestream << "Heuristic parameters:" << " preferRangeQueries()=" << m_db->preferRangeQueries() << " y_range=" << y_range << " blocks=" << blocks << std::endl; if (m_db->preferRangeQueries()) m_exhaustiveSearch = EXH_NEVER; else if (blocks < 200000) m_exhaustiveSearch = EXH_FULL; else if (y_range < 42) m_exhaustiveSearch = EXH_Y; else m_exhaustiveSearch = EXH_NEVER; } else if (m_exhaustiveSearch == EXH_FULL || m_exhaustiveSearch == EXH_Y) { if (m_db->preferRangeQueries()) { errorstream << "Note: The current database backend supports efficient " "range queries, forcing exhaustive search will generally result " "in worse performance." << std::endl; } } assert(m_exhaustiveSearch != EXH_AUTO); } void TileGenerator::closeDatabase() { delete m_db; m_db = NULL; } static inline int16_t mod16(int16_t y) { if (y < 0) return (y - 15) / 16; return y / 16; } void TileGenerator::loadBlocks() { const int16_t yMax = mod16(m_yMax) + 1; const int16_t yMin = mod16(m_yMin); if (m_exhaustiveSearch == EXH_NEVER || m_exhaustiveSearch == EXH_Y) { std::vector vec = m_db->getBlockPosXZ( BlockPos(m_geomX, yMin, m_geomY), BlockPos(m_geomX2, yMax, m_geomY2) ); for (auto pos : vec) { assert(pos.x >= m_geomX && pos.x < m_geomX2); assert(pos.z >= m_geomY && pos.z < m_geomY2); // Adjust minimum and maximum positions to the nearest block m_xMin = mymin(m_xMin, pos.x); m_xMax = mymax(m_xMax, pos.x); m_zMin = mymin(m_zMin, pos.z); m_zMax = mymax(m_zMax, pos.z); m_positions[pos.z].emplace(pos.x); } size_t count = 0; for (const auto &it : m_positions) count += it.second.size(); m_progressMax = count; verbosestream << "Loaded " << count << " positions (across Z: " << m_positions.size() << ") for rendering" << std::endl; } } void TileGenerator::createImage() { const int scale_d = 40; // pixels reserved for a scale if(!m_drawScale) m_scales = 0; // If a geometry is explicitly set, set the bounding box to the requested geometry // instead of cropping to the content. This way we will always output a full tile // of the correct size. if (m_geomX > -2048 && m_geomX2 < 2048) { m_xMin = m_geomX; m_xMax = m_geomX2-1; } if (m_geomY > -2048 && m_geomY2 < 2048) { m_zMin = m_geomY; m_zMax = m_geomY2-1; } m_mapWidth = (m_xMax - m_xMin + 1) * 16; m_mapHeight = (m_zMax - m_zMin + 1) * 16; m_xBorder = (m_scales & SCALE_LEFT) ? scale_d : 0; m_yBorder = (m_scales & SCALE_TOP) ? scale_d : 0; m_blockPixelAttributes.setWidth(m_mapWidth); int image_width, image_height; image_width = (m_mapWidth * m_zoom) + m_xBorder; image_width += (m_scales & SCALE_RIGHT) ? scale_d : 0; image_height = (m_mapHeight * m_zoom) + m_yBorder; image_height += (m_scales & SCALE_BOTTOM) ? scale_d : 0; if(image_width > 4096 || image_height > 4096) { errorstream << "Warning: The side length of the image to be created exceeds 4096 pixels!" << " (dimensions: " << image_width << "x" << image_height << ")" << std::endl; } else { verbosestream << "Creating image with size " << image_width << "x" << image_height << std::endl; } m_image = new Image(image_width, image_height); m_image->drawFilledRect(0, 0, image_width, image_height, m_bgColor); // Background } void TileGenerator::renderMap() { BlockDecoder blk; const int16_t yMax = mod16(m_yMax) + 1; const int16_t yMin = mod16(m_yMin); size_t count = 0; auto renderSingle = [&] (int16_t xPos, int16_t zPos, BlockList &blockStack) { m_readPixels.reset(); m_readInfo.reset(); for (int i = 0; i < 16; i++) { for (int j = 0; j < 16; j++) { m_color[i][j] = m_bgColor; // This will be drawn by renderMapBlockBottom() for y-rows with only 'air', 'ignore' or unknown nodes if --drawalpha is used m_color[i][j].a = 0; // ..but set alpha to 0 to tell renderMapBlock() not to use this color to mix a shade m_thickness[i][j] = 0; } } for (const auto &it : blockStack) { const BlockPos pos = it.first; assert(pos.x == xPos && pos.z == zPos); assert(pos.y >= yMin && pos.y < yMax); blk.reset(); blk.decode(it.second); if (blk.isEmpty()) continue; renderMapBlock(blk, pos); // Exit out if all pixels for this MapBlock are covered if (m_readPixels.full()) break; } if (!m_readPixels.full()) renderMapBlockBottom(blockStack.begin()->first); m_renderedAny |= m_readInfo.any(); }; auto postRenderRow = [&] (int16_t zPos) { if (m_shading) renderShading(zPos); }; if (m_exhaustiveSearch == EXH_NEVER) { for (auto it = m_positions.rbegin(); it != m_positions.rend(); ++it) { int16_t zPos = it->first; for (auto it2 = it->second.rbegin(); it2 != it->second.rend(); ++it2) { int16_t xPos = *it2; BlockList blockStack; m_db->getBlocksOnXZ(blockStack, xPos, zPos, yMin, yMax); blockStack.sort(); renderSingle(xPos, zPos, blockStack); reportProgress(count++); } postRenderRow(zPos); } } else if (m_exhaustiveSearch == EXH_Y) { verbosestream << "Exhaustively searching height of " << (yMax - yMin) << " blocks" << std::endl; std::vector positions; positions.reserve(yMax - yMin); for (auto it = m_positions.rbegin(); it != m_positions.rend(); ++it) { int16_t zPos = it->first; for (auto it2 = it->second.rbegin(); it2 != it->second.rend(); ++it2) { int16_t xPos = *it2; positions.clear(); for (int16_t yPos = yMin; yPos < yMax; yPos++) positions.emplace_back(xPos, yPos, zPos); BlockList blockStack; m_db->getBlocksByPos(blockStack, positions); blockStack.sort(); renderSingle(xPos, zPos, blockStack); reportProgress(count++); } postRenderRow(zPos); } } else if (m_exhaustiveSearch == EXH_FULL) { const size_t span_y = yMax - yMin; m_progressMax = (m_geomX2 - m_geomX) * span_y * (m_geomY2 - m_geomY); verbosestream << "Exhaustively searching " << (m_geomX2 - m_geomX) << "x" << span_y << "x" << (m_geomY2 - m_geomY) << " blocks" << std::endl; std::vector positions; positions.reserve(span_y); for (int16_t zPos = m_geomY2 - 1; zPos >= m_geomY; zPos--) { for (int16_t xPos = m_geomX2 - 1; xPos >= m_geomX; xPos--) { positions.clear(); for (int16_t yPos = yMin; yPos < yMax; yPos++) positions.emplace_back(xPos, yPos, zPos); BlockList blockStack; m_db->getBlocksByPos(blockStack, positions); blockStack.sort(); renderSingle(xPos, zPos, blockStack); reportProgress(count++); } postRenderRow(zPos); } } reportProgress(m_progressMax); } void TileGenerator::renderMapBlock(const BlockDecoder &blk, const BlockPos &pos) { int xBegin = (pos.x - m_xMin) * 16; int zBegin = (m_zMax - pos.z) * 16; int minY = (pos.y * 16 > m_yMin) ? 0 : m_yMin - pos.y * 16; int maxY = (pos.y * 16 + 15 < m_yMax) ? 15 : m_yMax - pos.y * 16; for (int z = 0; z < 16; ++z) { int imageY = zBegin + 15 - z; for (int x = 0; x < 16; ++x) { if (m_readPixels.get(x, z)) continue; int imageX = xBegin + x; auto &attr = m_blockPixelAttributes.attribute(15 - z, xBegin + x); for (int y = maxY; y >= minY; --y) { const std::string &name = blk.getNode(x, y, z); if (name.empty()) continue; ColorMap::const_iterator it = m_colorMap.find(name); if (it == m_colorMap.end()) { m_unknownNodes.insert(name); continue; } Color c = it->second.toColor(); if (c.a == 0) continue; // node is fully invisible if (m_drawAlpha) { if (m_color[z][x].a != 0) c = mixColors(m_color[z][x], c); if (c.a < 255) { // remember color and near thickness value m_color[z][x] = c; m_thickness[z][x] = (m_thickness[z][x] + it->second.t) / 2; continue; } // color became opaque, draw it setZoomed(imageX, imageY, c); attr.thickness = m_thickness[z][x]; } else { c.a = 255; setZoomed(imageX, imageY, c); } m_readPixels.set(x, z); // do this afterwards so we can record height values // inside transparent nodes (water) too if (!m_readInfo.get(x, z)) { attr.height = pos.y * 16 + y; m_readInfo.set(x, z); } break; } } } } void TileGenerator::renderMapBlockBottom(const BlockPos &pos) { if (!m_drawAlpha) return; // "missing" pixels can only happen with --drawalpha int xBegin = (pos.x - m_xMin) * 16; int zBegin = (m_zMax - pos.z) * 16; for (int z = 0; z < 16; ++z) { int imageY = zBegin + 15 - z; for (int x = 0; x < 16; ++x) { if (m_readPixels.get(x, z)) continue; int imageX = xBegin + x; auto &attr = m_blockPixelAttributes.attribute(15 - z, xBegin + x); // set color since it wasn't done in renderMapBlock() setZoomed(imageX, imageY, m_color[z][x]); m_readPixels.set(x, z); attr.thickness = m_thickness[z][x]; } } } void TileGenerator::renderShading(int zPos) { auto &a = m_blockPixelAttributes; int zBegin = (m_zMax - zPos) * 16; for (int z = 0; z < 16; ++z) { int imageY = zBegin + z; if (imageY >= m_mapHeight) continue; for (int x = 0; x < m_mapWidth; ++x) { if( !a.attribute(z, x).valid_height() || !a.attribute(z, x - 1).valid_height() || !a.attribute(z - 1, x).valid_height() ) continue; // calculate shadow to apply int y = a.attribute(z, x).height; int y1 = a.attribute(z, x - 1).height; int y2 = a.attribute(z - 1, x).height; int d = ((y - y1) + (y - y2)) * 12; if (m_drawAlpha) { // less visible shadow with increasing "thickness" float t = a.attribute(z, x).thickness * 1.2f; t = mymin(t, 255.0f); d *= 1.0f - t / 255.0f; } d = mymin(d, 36); // apply shadow/light by just adding to it pixel values Color c = m_image->getPixel(getImageX(x), getImageY(imageY)); c.r = colorSafeBounds(c.r + d); c.g = colorSafeBounds(c.g + d); c.b = colorSafeBounds(c.b + d); setZoomed(x, imageY, c); } } a.scroll(); } void TileGenerator::renderScale() { const int scale_d = 40; // see createImage() if (m_scales & SCALE_TOP) { m_image->drawText(24, 0, "X", m_scaleColor); for (int i = (m_xMin / 4) * 4; i <= m_xMax; i += 4) { std::ostringstream buf; buf << i * 16; int xPos = getImageX(i * 16, true); if (xPos >= 0) { m_image->drawText(xPos + 2, 0, buf.str(), m_scaleColor); m_image->drawLine(xPos, 0, xPos, m_yBorder - 1, m_scaleColor); } } } if (m_scales & SCALE_LEFT) { m_image->drawText(2, 24, "Z", m_scaleColor); for (int i = (m_zMax / 4) * 4; i >= m_zMin; i -= 4) { std::ostringstream buf; buf << i * 16; int yPos = getImageY(i * 16 + 1, true); if (yPos >= 0) { m_image->drawText(2, yPos, buf.str(), m_scaleColor); m_image->drawLine(0, yPos, m_xBorder - 1, yPos, m_scaleColor); } } } if (m_scales & SCALE_BOTTOM) { int xPos = m_xBorder + m_mapWidth*m_zoom - 24 - 8, yPos = m_yBorder + m_mapHeight*m_zoom + scale_d - 12; m_image->drawText(xPos, yPos, "X", m_scaleColor); for (int i = (m_xMin / 4) * 4; i <= m_xMax; i += 4) { std::ostringstream buf; buf << i * 16; xPos = getImageX(i * 16, true); yPos = m_yBorder + m_mapHeight*m_zoom; if (xPos >= 0) { m_image->drawText(xPos + 2, yPos, buf.str(), m_scaleColor); m_image->drawLine(xPos, yPos, xPos, yPos + 39, m_scaleColor); } } } if (m_scales & SCALE_RIGHT) { int xPos = m_xBorder + m_mapWidth*m_zoom + scale_d - 2 - 8, yPos = m_yBorder + m_mapHeight*m_zoom - 24 - 12; m_image->drawText(xPos, yPos, "Z", m_scaleColor); for (int i = (m_zMax / 4) * 4; i >= m_zMin; i -= 4) { std::ostringstream buf; buf << i * 16; xPos = m_xBorder + m_mapWidth*m_zoom; yPos = getImageY(i * 16 + 1, true); if (yPos >= 0) { m_image->drawText(xPos + 2, yPos, buf.str(), m_scaleColor); m_image->drawLine(xPos, yPos, xPos + 39, yPos, m_scaleColor); } } } } void TileGenerator::renderOrigin() { if (m_xMin > 0 || m_xMax < 0 || m_zMin > 0 || m_zMax < 0) return; m_image->drawCircle(getImageX(0, true), getImageY(0, true), 12, m_originColor); } void TileGenerator::renderPlayers(const std::string &input_path) { std::string input = input_path; if (input.back() != PATH_SEPARATOR) input += PATH_SEPARATOR; PlayerAttributes players(input); for (auto &player : players) { if (player.x < m_xMin * 16 || player.x >= (m_xMax+1) * 16 || player.z < m_zMin * 16 || player.z >= (m_zMax+1) * 16) continue; if (player.y < m_yMin || player.y > m_yMax) continue; int imageX = getImageX(player.x, true), imageY = getImageY(player.z, true); m_image->drawFilledRect(imageX - 1, imageY, 3, 1, m_playerColor); m_image->drawFilledRect(imageX, imageY - 1, 1, 3, m_playerColor); assert(!player.name.empty()); m_image->drawText(imageX + 2, imageY, player.name, m_playerColor); } } void TileGenerator::writeImage(const std::string &output) { m_image->save(output); delete m_image; m_image = nullptr; } void TileGenerator::printUnknown() { if (m_unknownNodes.empty()) return; errorstream << "Unknown nodes:\n"; for (const auto &node : m_unknownNodes) errorstream << "\t" << node << '\n'; if (!m_renderedAny) { errorstream << "The map was read successfully and not empty, but none of the " "encountered nodes had a color associated.\nCheck that you're using " "the right colors.txt. It should match the game you have installed.\n"; } errorstream << std::flush; } void TileGenerator::reportProgress(size_t count) { if (!m_progressMax) return; int percent = count / static_cast(m_progressMax) * 100; if (percent == m_progressLast) return; m_progressLast = percent; // Print a nice-looking ASCII progress bar char bar[51] = {0}; memset(bar, ' ', 50); int i = 0, j = percent; for (; j >= 2; j -= 2) bar[i++] = '='; if (j) bar[i++] = '-'; std::cout << "[" << bar << "] " << percent << "% " << (percent == 100 ? "\n" : "\r"); std::cout.flush(); } inline int TileGenerator::getImageX(int val, bool absolute) const { if (absolute) val = (val - m_xMin * 16); return (m_zoom*val) + m_xBorder; } inline int TileGenerator::getImageY(int val, bool absolute) const { if (absolute) val = m_mapHeight - (val - m_zMin * 16); // Z axis is flipped on image return (m_zoom*val) + m_yBorder; } inline void TileGenerator::setZoomed(int x, int y, Color color) { m_image->drawFilledRect(getImageX(x), getImageY(y), m_zoom, m_zoom, color); } luanti-org-minetestmapper-637755d/src/TileGenerator.h000066400000000000000000000103351477524601400226340ustar00rootroot00000000000000#pragma once #include #include #include #include #include #include #include "PixelAttributes.h" #include "Image.h" #include "db.h" #include "types.h" class BlockDecoder; class Image; enum { SCALE_TOP = (1 << 0), SCALE_BOTTOM = (1 << 1), SCALE_LEFT = (1 << 2), SCALE_RIGHT = (1 << 3), }; enum { EXH_NEVER, // Always use range queries EXH_Y, // Exhaustively search Y space, range queries for X/Z EXH_FULL, // Exhaustively search entire requested geometry EXH_AUTO, // Automatically pick one of the previous modes }; struct ColorEntry { ColorEntry() : r(0), g(0), b(0), a(0), t(0) {}; ColorEntry(uint8_t r, uint8_t g, uint8_t b, uint8_t a, uint8_t t) : r(r), g(g), b(b), a(a), t(t) {}; inline Color toColor() const { return Color(r, g, b, a); } uint8_t r, g, b, a; // Red, Green, Blue, Alpha uint8_t t; // "thickness" value }; struct BitmapThing { // 16x16 bitmap inline void reset() { for (int i = 0; i < 16; ++i) val[i] = 0; } inline bool any_neq(uint16_t v) const { for (int i = 0; i < 16; ++i) { if (val[i] != v) return true; } return false; } inline bool any() const { return any_neq(0); } inline bool full() const { return !any_neq(0xffff); } inline void set(unsigned int x, unsigned int z) { val[z] |= (1 << x); } inline bool get(unsigned int x, unsigned int z) const { return !!(val[z] & (1 << x)); } uint16_t val[16]; }; class TileGenerator { private: typedef std::unordered_map ColorMap; public: TileGenerator(); ~TileGenerator(); void setBgColor(const std::string &bgColor); void setScaleColor(const std::string &scaleColor); void setOriginColor(const std::string &originColor); void setPlayerColor(const std::string &playerColor); void setDrawOrigin(bool drawOrigin); void setDrawPlayers(bool drawPlayers); void setDrawScale(bool drawScale); void setDrawAlpha(bool drawAlpha); void setShading(bool shading); void setGeometry(int x, int y, int w, int h); void setMinY(int y); void setMaxY(int y); void setExhaustiveSearch(int mode); void parseColorsFile(const std::string &fileName); void setBackend(std::string backend); void setZoom(int zoom); void setScales(uint flags); void setDontWriteEmpty(bool f); void generate(const std::string &input, const std::string &output); void printGeometry(const std::string &input); void dumpBlock(const std::string &input, BlockPos pos); static std::set getSupportedBackends(); private: void parseColorsStream(std::istream &in); void openDb(const std::string &input); void closeDatabase(); void loadBlocks(); void createImage(); void renderMap(); void renderMapBlock(const BlockDecoder &blk, const BlockPos &pos); void renderMapBlockBottom(const BlockPos &pos); void renderShading(int zPos); void renderScale(); void renderOrigin(); void renderPlayers(const std::string &inputPath); void writeImage(const std::string &output); void printUnknown(); void reportProgress(size_t count); int getImageX(int val, bool absolute=false) const; int getImageY(int val, bool absolute=false) const; void setZoomed(int x, int y, Color color); private: Color m_bgColor; Color m_scaleColor; Color m_originColor; Color m_playerColor; bool m_drawOrigin; bool m_drawPlayers; bool m_drawScale; bool m_drawAlpha; bool m_shading; bool m_dontWriteEmpty; std::string m_backend; int m_xBorder, m_yBorder; DB *m_db; Image *m_image; PixelAttributes m_blockPixelAttributes; /* smallest/largest seen X or Z block coordinate */ int m_xMin; int m_xMax; int m_zMin; int m_zMax; /* Y limits for rendered area (node units) */ int m_yMin; int m_yMax; /* limits for rendered area (block units) */ int16_t m_geomX; int16_t m_geomY; /* Y in terms of rendered image, Z in the world */ int16_t m_geomX2; int16_t m_geomY2; int m_mapWidth; int m_mapHeight; int m_exhaustiveSearch; std::set m_unknownNodes; bool m_renderedAny; std::map> m_positions; /* indexed by Z, contains X coords */ ColorMap m_colorMap; BitmapThing m_readPixels; BitmapThing m_readInfo; Color m_color[16][16]; uint8_t m_thickness[16][16]; int m_zoom; uint m_scales; size_t m_progressMax; int m_progressLast; // percentage }; // class TileGenerator luanti-org-minetestmapper-637755d/src/ZlibDecompressor.cpp000066400000000000000000000027331477524601400237140ustar00rootroot00000000000000#include #include "ZlibDecompressor.h" #include "config.h" // for convenient usage of both #if USE_ZLIB_NG #include #define z_stream zng_stream #define Z(x) zng_ ## x #else #include #define Z(x) x #endif ZlibDecompressor::ZlibDecompressor(const u8 *data, size_t size): m_data(data), m_seekPos(0), m_size(size) { } ZlibDecompressor::~ZlibDecompressor() { } void ZlibDecompressor::setSeekPos(size_t seekPos) { m_seekPos = seekPos; } void ZlibDecompressor::decompress(ustring &buffer) { const unsigned char *data = m_data + m_seekPos; const size_t size = m_size - m_seekPos; // output space is extended in chunks of this size constexpr size_t BUFSIZE = 8 * 1024; z_stream strm; strm.zalloc = Z_NULL; strm.zfree = Z_NULL; strm.opaque = Z_NULL; strm.next_in = Z_NULL; strm.avail_in = 0; if (Z(inflateInit)(&strm) != Z_OK) throw DecompressError(); strm.next_in = const_cast(data); strm.avail_in = size; if (buffer.empty()) buffer.resize(BUFSIZE); strm.next_out = &buffer[0]; strm.avail_out = buffer.size(); int ret = 0; do { ret = Z(inflate)(&strm, Z_NO_FLUSH); if (strm.avail_out == 0) { const auto off = buffer.size(); buffer.resize(off + BUFSIZE); strm.next_out = &buffer[off]; strm.avail_out = BUFSIZE; } } while (ret == Z_OK); if (ret != Z_STREAM_END) throw DecompressError(); m_seekPos += strm.next_in - data; buffer.resize(buffer.size() - strm.avail_out); (void) Z(inflateEnd)(&strm); } luanti-org-minetestmapper-637755d/src/ZlibDecompressor.h000066400000000000000000000007251477524601400233600ustar00rootroot00000000000000#pragma once #include #include "types.h" class ZlibDecompressor { public: class DecompressError : std::exception {}; ZlibDecompressor(const u8 *data, size_t size); ~ZlibDecompressor(); void setSeekPos(size_t seekPos); size_t seekPos() const { return m_seekPos; } // Decompress and return one zlib stream from the buffer // Advances seekPos as appropriate. void decompress(ustring &dst); private: const u8 *m_data; size_t m_seekPos, m_size; }; luanti-org-minetestmapper-637755d/src/ZstdDecompressor.cpp000066400000000000000000000021731477524601400237360ustar00rootroot00000000000000#include #include "ZstdDecompressor.h" ZstdDecompressor::ZstdDecompressor(): m_data(nullptr), m_seekPos(0), m_size(0) { m_stream = ZSTD_createDStream(); } ZstdDecompressor::~ZstdDecompressor() { ZSTD_freeDStream(reinterpret_cast(m_stream)); } void ZstdDecompressor::setData(const u8 *data, size_t size, size_t seekPos) { m_data = data; m_seekPos = seekPos; m_size = size; } void ZstdDecompressor::decompress(ustring &buffer) { ZSTD_DStream *stream = reinterpret_cast(m_stream); ZSTD_inBuffer inbuf = { m_data, m_size, m_seekPos }; // output space is extended in chunks of this size constexpr size_t BUFSIZE = 8 * 1024; if (buffer.empty()) buffer.resize(BUFSIZE); ZSTD_outBuffer outbuf = { &buffer[0], buffer.size(), 0 }; ZSTD_initDStream(stream); size_t ret; do { ret = ZSTD_decompressStream(stream, &outbuf, &inbuf); if (ret && ZSTD_isError(ret)) throw DecompressError(); if (outbuf.size == outbuf.pos) { outbuf.size += BUFSIZE; buffer.resize(outbuf.size); outbuf.dst = &buffer[0]; } } while (ret != 0); m_seekPos = inbuf.pos; buffer.resize(outbuf.pos); } luanti-org-minetestmapper-637755d/src/ZstdDecompressor.h000066400000000000000000000007651477524601400234100ustar00rootroot00000000000000#pragma once #include #include "types.h" class ZstdDecompressor { public: class DecompressError : std::exception {}; ZstdDecompressor(); ~ZstdDecompressor(); void setData(const u8 *data, size_t size, size_t seekPos); size_t seekPos() const { return m_seekPos; } // Decompress and return one zstd stream from the buffer // Advances seekPos as appropriate. void decompress(ustring &dst); private: void *m_stream; // ZSTD_DStream const u8 *m_data; size_t m_seekPos, m_size; }; luanti-org-minetestmapper-637755d/src/cmake_config.h.in000066400000000000000000000003461477524601400231030ustar00rootroot00000000000000// Filled in by the build system #ifndef CMAKE_CONFIG_H #define CMAKE_CONFIG_H #cmakedefine01 USE_POSTGRESQL #cmakedefine01 USE_LEVELDB #cmakedefine01 USE_REDIS #cmakedefine01 USE_ZLIB_NG #define SHAREDIR "@SHAREDIR@" #endif luanti-org-minetestmapper-637755d/src/config.h000066400000000000000000000002541477524601400213340ustar00rootroot00000000000000#if defined(MSDOS) || defined(__OS2__) || defined(__NT__) || defined(_WIN32) #define PATH_SEPARATOR '\\' #else #define PATH_SEPARATOR '/' #endif #include "cmake_config.h" luanti-org-minetestmapper-637755d/src/db-leveldb.cpp000066400000000000000000000062571477524601400224330ustar00rootroot00000000000000#include #include #include #include "db-leveldb.h" #include "types.h" static inline int64_t stoi64(const std::string &s) { std::istringstream tmp(s); int64_t t; tmp >> t; return t; } static inline std::string i64tos(int64_t i) { std::ostringstream os; os << i; return os.str(); } // finds the first position in the list where it.x >= x #define lower_bound_x(container, find_x) \ std::lower_bound((container).begin(), (container).end(), (find_x), \ [] (const vec2 &left, int16_t right) { \ return left.x < right; \ }) DBLevelDB::DBLevelDB(const std::string &mapdir) { leveldb::Options options; options.create_if_missing = false; leveldb::Status status = leveldb::DB::Open(options, mapdir + "map.db", &db); if (!status.ok()) { throw std::runtime_error(std::string("Failed to open database: ") + status.ToString()); } /* LevelDB is a dumb key-value store, so the only optimization we can do * is to cache the block positions that exist in the db. */ loadPosCache(); } DBLevelDB::~DBLevelDB() { delete db; } std::vector DBLevelDB::getBlockPosXZ(BlockPos min, BlockPos max) { std::vector res; for (const auto &it : posCache) { const int16_t zpos = it.first; if (zpos < min.z || zpos >= max.z) continue; auto it2 = lower_bound_x(it.second, min.x); for (; it2 != it.second.end(); it2++) { const auto &pos2 = *it2; if (pos2.x >= max.x) break; // went past if (pos2.y < min.y || pos2.y >= max.y) continue; // skip duplicates if (!res.empty() && res.back().x == pos2.x && res.back().z == zpos) continue; res.emplace_back(pos2.x, pos2.y, zpos); } } return res; } void DBLevelDB::loadPosCache() { leveldb::Iterator *it = db->NewIterator(leveldb::ReadOptions()); for (it->SeekToFirst(); it->Valid(); it->Next()) { int64_t posHash = stoi64(it->key().ToString()); BlockPos pos = decodeBlockPos(posHash); posCache[pos.z].emplace_back(pos.x, pos.y); } delete it; for (auto &it : posCache) std::sort(it.second.begin(), it.second.end()); } void DBLevelDB::getBlocksOnXZ(BlockList &blocks, int16_t x, int16_t z, int16_t min_y, int16_t max_y) { std::string datastr; leveldb::Status status; auto it = posCache.find(z); if (it == posCache.cend()) return; auto it2 = lower_bound_x(it->second, x); if (it2 == it->second.end() || it2->x != x) return; // it2 is now pointing to a contigous part where it2->x == x for (; it2 != it->second.end(); it2++) { const auto &pos2 = *it2; if (pos2.x != x) break; // went past if (pos2.y < min_y || pos2.y >= max_y) continue; BlockPos pos(x, pos2.y, z); status = db->Get(leveldb::ReadOptions(), i64tos(encodeBlockPos(pos)), &datastr); if (status.ok()) { blocks.emplace_back( pos, ustring((unsigned char *) datastr.data(), datastr.size()) ); } } } void DBLevelDB::getBlocksByPos(BlockList &blocks, const std::vector &positions) { std::string datastr; leveldb::Status status; for (auto pos : positions) { status = db->Get(leveldb::ReadOptions(), i64tos(encodeBlockPos(pos)), &datastr); if (status.ok()) { blocks.emplace_back( pos, ustring((unsigned char *) datastr.data(), datastr.size()) ); } } } luanti-org-minetestmapper-637755d/src/db-leveldb.h000066400000000000000000000017121477524601400220670ustar00rootroot00000000000000#pragma once #include "db.h" #include #include #include class DBLevelDB : public DB { public: DBLevelDB(const std::string &mapdir); std::vector getBlockPosXZ(BlockPos min, BlockPos max) override; void getBlocksOnXZ(BlockList &blocks, int16_t x, int16_t z, int16_t min_y, int16_t max_y) override; void getBlocksByPos(BlockList &blocks, const std::vector &positions) override; ~DBLevelDB() override; bool preferRangeQueries() const override { return false; } private: struct vec2 { int16_t x, y; constexpr vec2() : x(0), y(0) {} constexpr vec2(int16_t x, int16_t y) : x(x), y(y) {} inline bool operator<(const vec2 &p) const { if (x < p.x) return true; if (x > p.x) return false; return y < p.y; } }; void loadPosCache(); // indexed by Z, contains all (x,y) position pairs std::unordered_map> posCache; leveldb::DB *db = NULL; }; luanti-org-minetestmapper-637755d/src/db-postgresql.cpp000066400000000000000000000120701477524601400232070ustar00rootroot00000000000000#include #include #include #include #include #include "db-postgresql.h" #include "util.h" #include "log.h" #include "types.h" /* PostgreSQLBase */ PostgreSQLBase::~PostgreSQLBase() { if (db) PQfinish(db); } void PostgreSQLBase::openDatabase(const char *connect_string) { if (db) throw std::logic_error("Database already open"); db = PQconnectdb(connect_string); if (PQstatus(db) != CONNECTION_OK) { throw std::runtime_error(std::string("PostgreSQL database error: ") + PQerrorMessage(db) ); } } PGresult *PostgreSQLBase::checkResults(PGresult *res, bool clear) { ExecStatusType statusType = PQresultStatus(res); switch (statusType) { case PGRES_COMMAND_OK: case PGRES_TUPLES_OK: break; case PGRES_FATAL_ERROR: throw std::runtime_error( std::string("PostgreSQL database error: ") + PQresultErrorMessage(res) ); default: throw std::runtime_error( std::string("Unhandled PostgreSQL result code ") + std::to_string(statusType) ); } if (clear) PQclear(res); return res; } PGresult *PostgreSQLBase::execPrepared( const char *stmtName, const int paramsNumber, const void **params, const int *paramsLengths, const int *paramsFormats, bool clear) { return checkResults(PQexecPrepared(db, stmtName, paramsNumber, (const char* const*) params, paramsLengths, paramsFormats, 1 /* binary output */), clear ); } /* DBPostgreSQL */ DBPostgreSQL::DBPostgreSQL(const std::string &mapdir) { std::ifstream ifs(mapdir + "world.mt"); if (!ifs.good()) throw std::runtime_error("Failed to read world.mt"); std::string connect_string = read_setting("pgsql_connection", ifs); ifs.close(); openDatabase(connect_string.c_str()); prepareStatement( "get_block_pos", "SELECT posX::int4, posZ::int4 FROM blocks WHERE" " (posX BETWEEN $1::int4 AND $2::int4) AND" " (posY BETWEEN $3::int4 AND $4::int4) AND" " (posZ BETWEEN $5::int4 AND $6::int4) GROUP BY posX, posZ" ); prepareStatement( "get_blocks", "SELECT posY::int4, data FROM blocks WHERE" " posX = $1::int4 AND posZ = $2::int4" " AND (posY BETWEEN $3::int4 AND $4::int4)" ); prepareStatement( "get_block_exact", "SELECT data FROM blocks WHERE" " posX = $1::int4 AND posY = $2::int4 AND posZ = $3::int4" ); checkResults(PQexec(db, "START TRANSACTION;")); checkResults(PQexec(db, "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;")); } DBPostgreSQL::~DBPostgreSQL() { try { checkResults(PQexec(db, "COMMIT;")); } catch (const std::exception& caught) { errorstream << "could not finalize: " << caught.what() << std::endl; } } std::vector DBPostgreSQL::getBlockPosXZ(BlockPos min, BlockPos max) { int32_t const x1 = htonl(min.x); int32_t const x2 = htonl(max.x - 1); int32_t const y1 = htonl(min.y); int32_t const y2 = htonl(max.y - 1); int32_t const z1 = htonl(min.z); int32_t const z2 = htonl(max.z - 1); const void *args[] = { &x1, &x2, &y1, &y2, &z1, &z2 }; const int argLen[] = { 4, 4, 4, 4, 4, 4 }; const int argFmt[] = { 1, 1, 1, 1, 1, 1 }; PGresult *results = execPrepared( "get_block_pos", ARRLEN(args), args, argLen, argFmt, false ); int numrows = PQntuples(results); std::vector positions; positions.reserve(numrows); BlockPos pos; for (int row = 0; row < numrows; ++row) { pos.x = pg_binary_to_int(results, row, 0); pos.z = pg_binary_to_int(results, row, 1); positions.push_back(pos); } PQclear(results); return positions; } void DBPostgreSQL::getBlocksOnXZ(BlockList &blocks, int16_t xPos, int16_t zPos, int16_t min_y, int16_t max_y) { int32_t const x = htonl(xPos); int32_t const z = htonl(zPos); int32_t const y1 = htonl(min_y); int32_t const y2 = htonl(max_y - 1); const void *args[] = { &x, &z, &y1, &y2 }; const int argLen[] = { 4, 4, 4, 4 }; const int argFmt[] = { 1, 1, 1, 1 }; PGresult *results = execPrepared( "get_blocks", ARRLEN(args), args, argLen, argFmt, false ); int numrows = PQntuples(results); for (int row = 0; row < numrows; ++row) { BlockPos position; position.x = xPos; position.y = pg_binary_to_int(results, row, 0); position.z = zPos; blocks.emplace_back( position, ustring( reinterpret_cast( PQgetvalue(results, row, 1) ), PQgetlength(results, row, 1) ) ); } PQclear(results); } void DBPostgreSQL::getBlocksByPos(BlockList &blocks, const std::vector &positions) { int32_t x, y, z; const void *args[] = { &x, &y, &z }; const int argLen[] = { 4, 4, 4 }; const int argFmt[] = { 1, 1, 1 }; for (auto pos : positions) { x = htonl(pos.x); y = htonl(pos.y); z = htonl(pos.z); PGresult *results = execPrepared( "get_block_exact", ARRLEN(args), args, argLen, argFmt, false ); if (PQntuples(results) > 0) { blocks.emplace_back( pos, ustring( reinterpret_cast( PQgetvalue(results, 0, 0) ), PQgetlength(results, 0, 0) ) ); } PQclear(results); } } int DBPostgreSQL::pg_binary_to_int(PGresult *res, int row, int col) { int32_t* raw = reinterpret_cast(PQgetvalue(res, row, col)); return ntohl(*raw); } luanti-org-minetestmapper-637755d/src/db-postgresql.h000066400000000000000000000021311477524601400226510ustar00rootroot00000000000000#pragma once #include "db.h" #include class PostgreSQLBase { public: ~PostgreSQLBase(); protected: void openDatabase(const char *connect_string); PGresult *checkResults(PGresult *res, bool clear = true); void prepareStatement(const std::string &name, const std::string &sql) { checkResults(PQprepare(db, name.c_str(), sql.c_str(), 0, NULL)); } PGresult *execPrepared( const char *stmtName, const int paramsNumber, const void **params, const int *paramsLengths = nullptr, const int *paramsFormats = nullptr, bool clear = true ); PGconn *db = NULL; }; class DBPostgreSQL : public DB, PostgreSQLBase { public: DBPostgreSQL(const std::string &mapdir); std::vector getBlockPosXZ(BlockPos min, BlockPos max) override; void getBlocksOnXZ(BlockList &blocks, int16_t x, int16_t z, int16_t min_y, int16_t max_y) override; void getBlocksByPos(BlockList &blocks, const std::vector &positions) override; ~DBPostgreSQL() override; bool preferRangeQueries() const override { return true; } private: int pg_binary_to_int(PGresult *res, int row, int col); }; luanti-org-minetestmapper-637755d/src/db-redis.cpp000066400000000000000000000117451477524601400221220ustar00rootroot00000000000000#include #include #include #include "db-redis.h" #include "types.h" #include "util.h" #define DB_REDIS_HMGET_NUMFIELDS 30 #define REPLY_TYPE_ERR(reply, desc) do { \ throw std::runtime_error(std::string("Unexpected type for " desc ": ") \ + replyTypeStr((reply)->type)); \ } while(0) static inline int64_t stoi64(const std::string &s) { std::stringstream tmp(s); int64_t t; tmp >> t; return t; } static inline std::string i64tos(int64_t i) { std::ostringstream os; os << i; return os.str(); } DBRedis::DBRedis(const std::string &mapdir) { std::ifstream ifs(mapdir + "world.mt"); if (!ifs.good()) throw std::runtime_error("Failed to read world.mt"); std::string tmp; tmp = read_setting("redis_address", ifs); ifs.seekg(0); hash = read_setting("redis_hash", ifs); ifs.seekg(0); if (tmp.find('/') != std::string::npos) { ctx = redisConnectUnix(tmp.c_str()); } else { int port = stoi64(read_setting_default("redis_port", ifs, "6379")); ctx = redisConnect(tmp.c_str(), port); } if (!ctx) { throw std::runtime_error("Cannot allocate redis context"); } else if (ctx->err) { std::string err = std::string("Connection error: ") + ctx->errstr; redisFree(ctx); throw std::runtime_error(err); } /* Redis is just a key-value store, so the only optimization we can do * is to cache the block positions that exist in the db. */ loadPosCache(); } DBRedis::~DBRedis() { redisFree(ctx); } std::vector DBRedis::getBlockPosXZ(BlockPos min, BlockPos max) { std::vector res; for (const auto &it : posCache) { if (it.first < min.z || it.first >= max.z) continue; for (auto pos2 : it.second) { if (pos2.first < min.x || pos2.first >= max.x) continue; if (pos2.second < min.y || pos2.second >= max.y) continue; res.emplace_back(pos2.first, pos2.second, it.first); } } return res; } const char *DBRedis::replyTypeStr(int type) { switch (type) { case REDIS_REPLY_STATUS: return "REDIS_REPLY_STATUS"; case REDIS_REPLY_ERROR: return "REDIS_REPLY_ERROR"; case REDIS_REPLY_INTEGER: return "REDIS_REPLY_INTEGER"; case REDIS_REPLY_NIL: return "REDIS_REPLY_NIL"; case REDIS_REPLY_STRING: return "REDIS_REPLY_STRING"; case REDIS_REPLY_ARRAY: return "REDIS_REPLY_ARRAY"; default: return "(unknown)"; } } void DBRedis::loadPosCache() { redisReply *reply; reply = (redisReply*) redisCommand(ctx, "HKEYS %s", hash.c_str()); if (!reply) throw std::runtime_error("Redis command HKEYS failed"); if (reply->type != REDIS_REPLY_ARRAY) REPLY_TYPE_ERR(reply, "HKEYS reply"); for (size_t i = 0; i < reply->elements; i++) { if (reply->element[i]->type != REDIS_REPLY_STRING) REPLY_TYPE_ERR(reply->element[i], "HKEYS subreply"); BlockPos pos = decodeBlockPos(stoi64(reply->element[i]->str)); posCache[pos.z].emplace_back(pos.x, pos.y); } freeReplyObject(reply); } void DBRedis::HMGET(const std::vector &positions, std::function result) { const char *argv[DB_REDIS_HMGET_NUMFIELDS + 2]; argv[0] = "HMGET"; argv[1] = hash.c_str(); auto position = positions.begin(); size_t remaining = positions.size(); size_t abs_i = 0; while (remaining > 0) { const size_t batch_size = mymin(DB_REDIS_HMGET_NUMFIELDS, remaining); redisReply *reply; { // storage to preserve validity of .c_str() std::string keys[batch_size]; for (size_t i = 0; i < batch_size; ++i) { keys[i] = i64tos(encodeBlockPos(*position++)); argv[i+2] = keys[i].c_str(); } reply = (redisReply*) redisCommandArgv(ctx, batch_size + 2, argv, NULL); } if (!reply) throw std::runtime_error("Redis command HMGET failed"); if (reply->type != REDIS_REPLY_ARRAY) REPLY_TYPE_ERR(reply, "HMGET reply"); if (reply->elements != batch_size) { freeReplyObject(reply); throw std::runtime_error("HMGET wrong number of elements"); } for (size_t i = 0; i < reply->elements; ++i) { redisReply *subreply = reply->element[i]; if (subreply->type == REDIS_REPLY_NIL) continue; else if (subreply->type != REDIS_REPLY_STRING) REPLY_TYPE_ERR(subreply, "HMGET subreply"); if (subreply->len == 0) throw std::runtime_error("HMGET empty string"); result(abs_i + i, ustring( reinterpret_cast(subreply->str), subreply->len )); } freeReplyObject(reply); abs_i += batch_size; remaining -= batch_size; } } void DBRedis::getBlocksOnXZ(BlockList &blocks, int16_t x, int16_t z, int16_t min_y, int16_t max_y) { auto it = posCache.find(z); if (it == posCache.cend()) return; std::vector positions; for (auto pos2 : it->second) { if (pos2.first == x && pos2.second >= min_y && pos2.second < max_y) positions.emplace_back(x, pos2.second, z); } getBlocksByPos(blocks, positions); } void DBRedis::getBlocksByPos(BlockList &blocks, const std::vector &positions) { auto result = [&] (std::size_t i, ustring data) { blocks.emplace_back(positions[i], std::move(data)); }; HMGET(positions, result); } luanti-org-minetestmapper-637755d/src/db-redis.h000066400000000000000000000016471477524601400215670ustar00rootroot00000000000000#pragma once #include "db.h" #include #include #include #include class DBRedis : public DB { public: DBRedis(const std::string &mapdir); std::vector getBlockPosXZ(BlockPos min, BlockPos max) override; void getBlocksOnXZ(BlockList &blocks, int16_t x, int16_t z, int16_t min_y, int16_t max_y) override; void getBlocksByPos(BlockList &blocks, const std::vector &positions) override; ~DBRedis() override; bool preferRangeQueries() const override { return false; } private: using pos2d = std::pair; static const char *replyTypeStr(int type); void loadPosCache(); void HMGET(const std::vector &positions, std::function result); // indexed by Z, contains all (x,y) position pairs std::unordered_map> posCache; redisContext *ctx; std::string hash; }; luanti-org-minetestmapper-637755d/src/db-sqlite3.cpp000066400000000000000000000155161477524601400224000ustar00rootroot00000000000000#include #include // for usleep #include #include #include #include "db-sqlite3.h" #include "log.h" #include "types.h" /* SQLite3Base */ #define SQLRES(r, good) check_result(r, good) #define SQLOK(r) SQLRES(r, SQLITE_OK) SQLite3Base::~SQLite3Base() { if (db && sqlite3_close(db) != SQLITE_OK) { errorstream << "Error closing SQLite database: " << sqlite3_errmsg(db) << std::endl; } } void SQLite3Base::openDatabase(const char *path, bool readonly) { if (db) throw std::logic_error("Database already open"); int flags = 0; if (readonly) flags |= SQLITE_OPEN_READONLY | SQLITE_OPEN_PRIVATECACHE; else flags |= SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE; #ifdef SQLITE_OPEN_EXRESCODE flags |= SQLITE_OPEN_EXRESCODE; #endif SQLOK(sqlite3_open_v2(path, &db, flags, 0)); } /* DBSQLite3 */ // make sure a row is available. intended to be used outside a loop. // compare result to SQLITE_ROW afterwards. #define SQLROW1(stmt) \ while ((result = sqlite3_step(stmt)) == SQLITE_BUSY) \ usleep(10000); /* wait some time and try again */ \ if (result != SQLITE_ROW && result != SQLITE_DONE) { \ throw std::runtime_error(sqlite3_errmsg(db)); \ } // make sure next row is available. intended to be used in a while(sqlite3_step) loop #define SQLROW2() \ if (result == SQLITE_BUSY) { \ usleep(10000); /* wait some time and try again */ \ continue; \ } else if (result != SQLITE_ROW) { \ throw std::runtime_error(sqlite3_errmsg(db)); \ } DBSQLite3::DBSQLite3(const std::string &mapdir) { std::string db_name = mapdir + "map.sqlite"; openDatabase(db_name.c_str()); // There's a simple, dumb way to check if we have a new or old database schema. // If we prepare a statement that references columns that don't exist, it will // error right there. int result = prepare(stmt_get_block_pos, "SELECT x, y, z FROM blocks"); newFormat = result == SQLITE_OK; verbosestream << "Detected " << (newFormat ? "new" : "old") << " SQLite schema" << std::endl; if (newFormat) { SQLOK(prepare(stmt_get_blocks_xz_range, "SELECT y, data FROM blocks WHERE " "x = ? AND z = ? AND y BETWEEN ? AND ?")); SQLOK(prepare(stmt_get_block_exact, "SELECT data FROM blocks WHERE x = ? AND y = ? AND z = ?")); SQLOK(prepare(stmt_get_block_pos_range, "SELECT x, z FROM blocks WHERE " "x >= ? AND y >= ? AND z >= ? AND " "x < ? AND y < ? AND z < ? GROUP BY x, z")); } else { SQLOK(prepare(stmt_get_blocks_z, "SELECT pos, data FROM blocks WHERE pos BETWEEN ? AND ?")); SQLOK(prepare(stmt_get_block_exact, "SELECT data FROM blocks WHERE pos = ?")); SQLOK(prepare(stmt_get_block_pos, "SELECT pos FROM blocks")); SQLOK(prepare(stmt_get_block_pos_range, "SELECT pos FROM blocks WHERE pos BETWEEN ? AND ?")); } #undef RANGE } DBSQLite3::~DBSQLite3() { sqlite3_finalize(stmt_get_blocks_z); sqlite3_finalize(stmt_get_blocks_xz_range); sqlite3_finalize(stmt_get_block_pos); sqlite3_finalize(stmt_get_block_pos_range); sqlite3_finalize(stmt_get_block_exact); } inline void DBSQLite3::getPosRange(int64_t &min, int64_t &max, int16_t zPos, int16_t zPos2) { // Magic numbers! min = encodeBlockPos(BlockPos(0, -2048, zPos)); max = encodeBlockPos(BlockPos(0, 2048, zPos2)) - 1; } std::vector DBSQLite3::getBlockPosXZ(BlockPos min, BlockPos max) { int result; sqlite3_stmt *stmt; if (newFormat) { stmt = stmt_get_block_pos_range; int col = bind_pos(stmt, 1, min); bind_pos(stmt, col, max); } else { // can handle range query on Z axis via SQL if (min.z <= -2048 && max.z >= 2048) { stmt = stmt_get_block_pos; } else { stmt = stmt_get_block_pos_range; int64_t minPos, maxPos; if (min.z < -2048) min.z = -2048; if (max.z > 2048) max.z = 2048; getPosRange(minPos, maxPos, min.z, max.z - 1); SQLOK(sqlite3_bind_int64(stmt, 1, minPos)); SQLOK(sqlite3_bind_int64(stmt, 2, maxPos)); } } std::vector positions; BlockPos pos; while ((result = sqlite3_step(stmt)) != SQLITE_DONE) { SQLROW2() if (newFormat) { pos.x = sqlite3_column_int(stmt, 0); pos.z = sqlite3_column_int(stmt, 1); } else { pos = decodeBlockPos(sqlite3_column_int64(stmt, 0)); if (pos.x < min.x || pos.x >= max.x || pos.y < min.y || pos.y >= max.y) continue; // note that we can't try to deduplicate these because the order // of the encoded pos (if sorted) is ZYX. } positions.emplace_back(pos); } SQLOK(sqlite3_reset(stmt)); return positions; } void DBSQLite3::loadBlockCache(int16_t zPos) { int result; blockCache.clear(); assert(!newFormat); int64_t minPos, maxPos; getPosRange(minPos, maxPos, zPos, zPos); SQLOK(sqlite3_bind_int64(stmt_get_blocks_z, 1, minPos)); SQLOK(sqlite3_bind_int64(stmt_get_blocks_z, 2, maxPos)); while ((result = sqlite3_step(stmt_get_blocks_z)) != SQLITE_DONE) { SQLROW2() int64_t posHash = sqlite3_column_int64(stmt_get_blocks_z, 0); BlockPos pos = decodeBlockPos(posHash); blockCache[pos.x].emplace_back(pos, read_blob(stmt_get_blocks_z, 1)); } SQLOK(sqlite3_reset(stmt_get_blocks_z)); } void DBSQLite3::getBlocksOnXZ(BlockList &blocks, int16_t x, int16_t z, int16_t min_y, int16_t max_y) { // New format: use a real range query if (newFormat) { auto *stmt = stmt_get_blocks_xz_range; SQLOK(sqlite3_bind_int(stmt, 1, x)); SQLOK(sqlite3_bind_int(stmt, 2, z)); SQLOK(sqlite3_bind_int(stmt, 3, min_y)); SQLOK(sqlite3_bind_int(stmt, 4, max_y - 1)); // BETWEEN is inclusive int result; while ((result = sqlite3_step(stmt)) != SQLITE_DONE) { SQLROW2() BlockPos pos(x, sqlite3_column_int(stmt, 0), z); blocks.emplace_back(pos, read_blob(stmt, 1)); } SQLOK(sqlite3_reset(stmt)); return; } /* Cache the blocks on the given Z coordinate between calls, this only * works due to order in which the TileGenerator asks for blocks. */ if (z != blockCachedZ) { loadBlockCache(z); blockCachedZ = z; } auto it = blockCache.find(x); if (it == blockCache.end()) return; if (it->second.empty()) { /* We have swapped this list before, this is not supposed to happen * because it's bad for performance. But rather than silently breaking * do the right thing and load the blocks again. */ verbosestream << "suboptimal access pattern for sqlite3 backend?!" << std::endl; loadBlockCache(z); } // Swap lists to avoid copying contents blocks.clear(); std::swap(blocks, it->second); for (auto it = blocks.begin(); it != blocks.end(); ) { if (it->first.y < min_y || it->first.y >= max_y) it = blocks.erase(it); else it++; } } void DBSQLite3::getBlocksByPos(BlockList &blocks, const std::vector &positions) { int result; for (auto pos : positions) { bind_pos(stmt_get_block_exact, 1, pos); SQLROW1(stmt_get_block_exact) if (result == SQLITE_ROW) blocks.emplace_back(pos, read_blob(stmt_get_block_exact, 0)); SQLOK(sqlite3_reset(stmt_get_block_exact)); } } luanti-org-minetestmapper-637755d/src/db-sqlite3.h000066400000000000000000000044701477524601400220420ustar00rootroot00000000000000#pragma once #include "db.h" #include #include class SQLite3Base { public: ~SQLite3Base(); protected: void openDatabase(const char *path, bool readonly = true); // check function result or throw error inline void check_result(int result, int good = SQLITE_OK) { if (result != good) throw std::runtime_error(sqlite3_errmsg(db)); } // prepare a statement inline int prepare(sqlite3_stmt *&stmt, const char *sql) { return sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); } // read string from statement static inline std::string read_str(sqlite3_stmt *stmt, int iCol) { auto *data = reinterpret_cast( sqlite3_column_text(stmt, iCol)); return std::string(data); } // read blob from statement static inline ustring read_blob(sqlite3_stmt *stmt, int iCol) { auto *data = reinterpret_cast( sqlite3_column_blob(stmt, iCol)); size_t size = sqlite3_column_bytes(stmt, iCol); return ustring(data, size); } sqlite3 *db = NULL; }; class DBSQLite3 : public DB, SQLite3Base { public: DBSQLite3(const std::string &mapdir); std::vector getBlockPosXZ(BlockPos min, BlockPos max) override; void getBlocksOnXZ(BlockList &blocks, int16_t x, int16_t z, int16_t min_y, int16_t max_y) override; void getBlocksByPos(BlockList &blocks, const std::vector &positions) override; ~DBSQLite3() override; bool preferRangeQueries() const override { return newFormat; } private: static inline void getPosRange(int64_t &min, int64_t &max, int16_t zPos, int16_t zPos2); void loadBlockCache(int16_t zPos); // bind pos to statement. returns index of next column. inline int bind_pos(sqlite3_stmt *stmt, int iCol, BlockPos pos) { if (newFormat) { sqlite3_bind_int(stmt, iCol, pos.x); sqlite3_bind_int(stmt, iCol + 1, pos.y); sqlite3_bind_int(stmt, iCol + 2, pos.z); return iCol + 3; } else { sqlite3_bind_int64(stmt, iCol, encodeBlockPos(pos)); return iCol + 1; } } sqlite3_stmt *stmt_get_block_pos = NULL; sqlite3_stmt *stmt_get_block_pos_range = NULL; sqlite3_stmt *stmt_get_blocks_z = NULL; sqlite3_stmt *stmt_get_blocks_xz_range = NULL; sqlite3_stmt *stmt_get_block_exact = NULL; bool newFormat = false; int16_t blockCachedZ = -10000; std::unordered_map blockCache; // indexed by X }; luanti-org-minetestmapper-637755d/src/db.h000066400000000000000000000053271477524601400204620ustar00rootroot00000000000000#pragma once #include #include #include #include #include "types.h" struct BlockPos { int16_t x, y, z; constexpr BlockPos() : x(0), y(0), z(0) {} explicit constexpr BlockPos(int16_t v) : x(v), y(v), z(v) {} constexpr BlockPos(int16_t x, int16_t y, int16_t z) : x(x), y(y), z(z) {} // Implements the inverse ordering so that (2,2,2) < (1,1,1) inline bool operator<(const BlockPos &p) const { if (z > p.z) return true; if (z < p.z) return false; if (y > p.y) return true; if (y < p.y) return false; return x > p.x; } }; typedef std::pair Block; typedef std::list BlockList; class DB { protected: // Helpers that implement the hashed positions used by most backends static inline int64_t encodeBlockPos(const BlockPos pos); static inline BlockPos decodeBlockPos(int64_t hash); public: /* Return all unique (X, Z) position pairs inside area given by min and max, * so that min.x <= x < max.x && min.z <= z < max.z * Note: duplicates are allowed, but results in wasted time. */ virtual std::vector getBlockPosXZ(BlockPos min, BlockPos max) = 0; /* Read all blocks in column given by x and z * and inside the given Y range (min_y <= y < max_y) into list */ virtual void getBlocksOnXZ(BlockList &blocks, int16_t x, int16_t z, int16_t min_y, int16_t max_y) = 0; /* Read blocks at given positions into list */ virtual void getBlocksByPos(BlockList &blocks, const std::vector &positions) = 0; /* Can this database efficiently do range queries? * (for large data sets, more efficient that brute force) */ virtual bool preferRangeQueries() const = 0; virtual ~DB() {} }; /**************** * Black magic! * **************** * The position hashing is seriously messed up, * and is a lot more complicated than it looks. */ static inline int16_t unsigned_to_signed(uint16_t i, uint16_t max_positive) { if (i < max_positive) { return i; } else { return i - (max_positive * 2); } } // Modulo of a negative number does not work consistently in C static inline int64_t pythonmodulo(int64_t i, int64_t mod) { if (i >= 0) { return i % mod; } return mod - ((-i) % mod); } inline int64_t DB::encodeBlockPos(const BlockPos pos) { return (uint64_t) pos.z * 0x1000000 + (uint64_t) pos.y * 0x1000 + (uint64_t) pos.x; } inline BlockPos DB::decodeBlockPos(int64_t hash) { BlockPos pos; pos.x = unsigned_to_signed(pythonmodulo(hash, 4096), 2048); hash = (hash - pos.x) / 4096; pos.y = unsigned_to_signed(pythonmodulo(hash, 4096), 2048); hash = (hash - pos.y) / 4096; pos.z = unsigned_to_signed(pythonmodulo(hash, 4096), 2048); return pos; } /******************* * End black magic * *******************/ luanti-org-minetestmapper-637755d/src/log.cpp000066400000000000000000000006001477524601400211760ustar00rootroot00000000000000#include #include "log.h" StreamProxy errorstream(nullptr); StreamProxy verbosestream(nullptr); void configure_log_streams(bool verbose) { errorstream << std::flush; verbosestream << std::flush; errorstream = std::cerr.good() ? &std::cerr : nullptr; // std::clog does not automatically flush verbosestream = (verbose && std::clog.good()) ? &std::clog : nullptr; } luanti-org-minetestmapper-637755d/src/log.h000066400000000000000000000014021477524601400206440ustar00rootroot00000000000000#pragma once #include #include // Forwards to an ostream, optionally class StreamProxy { public: StreamProxy(std::ostream *os) : m_os(os) {} template StreamProxy &operator<<(T &&arg) { if (m_os) *m_os << std::forward(arg); return *this; } StreamProxy &operator<<(std::ostream &(*func)(std::ostream&)) { if (m_os) *m_os << func; return *this; } private: std::ostream *m_os; }; /// Error and warning output, forwards to std::cerr extern StreamProxy errorstream; /// Verbose output, might forward to std::cerr extern StreamProxy verbosestream; /** * Configure log streams defined in this file. * @param verbose enable verbose output * @note not thread-safe! */ void configure_log_streams(bool verbose); luanti-org-minetestmapper-637755d/src/mapper.cpp000066400000000000000000000153071477524601400217130ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #include #include "config.h" #include "TileGenerator.h" #include "util.h" #include "log.h" static void usage() { static const std::pair options[] = { {"-i/--input", ""}, {"-o/--output", ""}, {"--bgcolor", ""}, {"--scalecolor", ""}, {"--playercolor", ""}, {"--origincolor", ""}, {"--drawscale", ""}, {"--drawplayers", ""}, {"--draworigin", ""}, {"--drawalpha", ""}, {"--noshading", ""}, {"--noemptyimage", ""}, {"-v/--verbose", ""}, {"--min-y", ""}, {"--max-y", ""}, {"--backend", ""}, {"--geometry", "x:z+w+h"}, {"--extent", ""}, {"--zoom", ""}, {"--colors", ""}, {"--scales", "[t][b][l][r]"}, {"--exhaustive", "never|y|full|auto"}, {"--dumpblock", "x,y,z"}, }; const char *top_text = "minetestmapper -i -o [options]\n" "Generate an overview image of a Luanti map.\n" "\n" "Options:\n"; const char *bottom_text = "\n" "Color format: hexadecimal '#RRGGBB', e.g. '#FF0000' = red\n"; printf("%s", top_text); for (const auto &p : options) printf(" %-18s%s\n", p.first, p.second); printf("%s", bottom_text); auto backends = TileGenerator::getSupportedBackends(); printf("Supported backends: "); for (auto s : backends) printf("%s ", s.c_str()); printf("\n"); #ifdef _WIN32 printf("See also the full documentation in README.rst\n"); #else printf("See also the full documentation in minetestmapper(6) or README.rst\n"); #endif } static inline bool file_exists(const std::string &path) { return file_exists(path.c_str()); } static inline int stoi(const char *s) { std::istringstream iss(s); int ret; iss >> ret; return ret; } static std::string search_colors(const std::string &worldpath) { if (file_exists(worldpath + "/colors.txt")) return worldpath + "/colors.txt"; #ifndef _WIN32 char *home = std::getenv("HOME"); if (home && home[0]) { std::string check = std::string(home) + "/.minetest/colors.txt"; if (file_exists(check)) return check; } #endif constexpr bool sharedir_valid = !(SHAREDIR[0] == '.' || !SHAREDIR[0]); if (sharedir_valid && file_exists(SHAREDIR "/colors.txt")) return SHAREDIR "/colors.txt"; errorstream << "Warning: Falling back to using colors.txt from current directory." << std::endl; return "./colors.txt"; } int main(int argc, char *argv[]) { const char *short_options = "hi:o:v"; const static struct option long_options[] = { {"help", no_argument, 0, 'h'}, {"input", required_argument, 0, 'i'}, {"output", required_argument, 0, 'o'}, {"bgcolor", required_argument, 0, 'b'}, {"scalecolor", required_argument, 0, 's'}, {"origincolor", required_argument, 0, 'r'}, {"playercolor", required_argument, 0, 'p'}, {"draworigin", no_argument, 0, 'R'}, {"drawplayers", no_argument, 0, 'P'}, {"drawscale", no_argument, 0, 'S'}, {"drawalpha", no_argument, 0, 'e'}, {"noshading", no_argument, 0, 'H'}, {"backend", required_argument, 0, 'd'}, {"geometry", required_argument, 0, 'g'}, {"extent", no_argument, 0, 'E'}, {"min-y", required_argument, 0, 'a'}, {"max-y", required_argument, 0, 'c'}, {"zoom", required_argument, 0, 'z'}, {"colors", required_argument, 0, 'C'}, {"scales", required_argument, 0, 'f'}, {"noemptyimage", no_argument, 0, 'n'}, {"exhaustive", required_argument, 0, 'j'}, {"dumpblock", required_argument, 0, 'k'}, {"verbose", no_argument, 0, 'v'}, {0, 0, 0, 0} }; configure_log_streams(false); std::string input; std::string output; std::string colors; bool onlyPrintExtent = false; BlockPos dumpblock(INT16_MIN); TileGenerator generator; while (1) { int option_index; int c = getopt_long(argc, argv, short_options, long_options, &option_index); if (c == -1) break; // done switch (c) { case 'h': usage(); return 0; case 'i': input = optarg; break; case 'o': output = optarg; break; case 'b': generator.setBgColor(optarg); break; case 's': generator.setScaleColor(optarg); break; case 'r': generator.setOriginColor(optarg); break; case 'p': generator.setPlayerColor(optarg); break; case 'R': generator.setDrawOrigin(true); break; case 'P': generator.setDrawPlayers(true); break; case 'S': generator.setDrawScale(true); break; case 'e': generator.setDrawAlpha(true); break; case 'E': onlyPrintExtent = true; break; case 'H': generator.setShading(false); break; case 'd': generator.setBackend(optarg); break; case 'a': generator.setMinY(stoi(optarg)); break; case 'c': generator.setMaxY(stoi(optarg)); break; case 'g': { std::istringstream geometry(optarg); int x, y, w, h; char c; geometry >> x >> c >> y >> w >> h; if (geometry.fail() || c != ':' || w < 1 || h < 1) { usage(); return 1; } generator.setGeometry(x, y, w, h); } break; case 'f': { uint flags = 0; if (strchr(optarg, 't')) flags |= SCALE_TOP; if (strchr(optarg, 'b')) flags |= SCALE_BOTTOM; if (strchr(optarg, 'l')) flags |= SCALE_LEFT; if (strchr(optarg, 'r')) flags |= SCALE_RIGHT; generator.setScales(flags); } break; case 'z': generator.setZoom(stoi(optarg)); break; case 'C': colors = optarg; break; case 'n': generator.setDontWriteEmpty(true); break; case 'j': { int mode = EXH_AUTO; if (!strcmp(optarg, "never")) mode = EXH_NEVER; else if (!strcmp(optarg, "y")) mode = EXH_Y; else if (!strcmp(optarg, "full")) mode = EXH_FULL; generator.setExhaustiveSearch(mode); } break; case 'k': { std::istringstream iss(optarg); char c, c2; iss >> dumpblock.x >> c >> dumpblock.y >> c2 >> dumpblock.z; if (iss.fail() || c != ',' || c2 != ',') { usage(); return 1; } break; } case 'v': configure_log_streams(true); break; default: return 1; } } const bool need_output = !onlyPrintExtent && dumpblock.x == INT16_MIN; if (input.empty() || (need_output && output.empty())) { usage(); return 0; } try { if (onlyPrintExtent) { generator.printGeometry(input); return 0; } else if (dumpblock.x != INT16_MIN) { generator.dumpBlock(input, dumpblock); return 0; } if(colors.empty()) colors = search_colors(input); generator.parseColorsFile(colors); generator.generate(input, output); } catch (const std::exception &e) { errorstream << "Exception: " << e.what() << std::endl; return 1; } return 0; } luanti-org-minetestmapper-637755d/src/types.h000066400000000000000000000025301477524601400212320ustar00rootroot00000000000000#pragma once #include // Define custom char traits since std::char_traits is not part of C++ standard struct uchar_traits : std::char_traits { using super = std::char_traits; using char_type = unsigned char; static void assign(char_type& c1, const char_type& c2) noexcept { c1 = c2; } static char_type* assign(char_type* ptr, std::size_t count, char_type c2) { return reinterpret_cast( super::assign(reinterpret_cast(ptr), count, static_cast(c2))); } static char_type* move(char_type* dest, const char_type* src, std::size_t count) { return reinterpret_cast( super::move(reinterpret_cast(dest), reinterpret_cast(src), count)); } static char_type* copy(char_type* dest, const char_type* src, std::size_t count) { return reinterpret_cast( super::copy(reinterpret_cast(dest), reinterpret_cast(src), count)); } static int compare(const char_type* s1, const char_type* s2, std::size_t count) { return super::compare(reinterpret_cast(s1), reinterpret_cast(s2), count); } static char_type to_char_type(int_type c) noexcept { return static_cast(c); } }; typedef std::basic_string ustring; typedef unsigned int uint; typedef unsigned char u8; luanti-org-minetestmapper-637755d/src/util.cpp000066400000000000000000000031521477524601400213770ustar00rootroot00000000000000#include #include #include #include #include "util.h" static std::string trim(const std::string &s) { auto isspace = [] (char c) { return c == ' ' || c == '\t' || c == '\r' || c == '\n'; }; size_t front = 0; while (isspace(s[front])) ++front; size_t back = s.size() - 1; while (back > front && isspace(s[back])) --back; return s.substr(front, back - front + 1); } static bool read_setting(const std::string &name, std::istream &is, std::string &out) { char linebuf[512]; is.seekg(0); while (is.good()) { is.getline(linebuf, sizeof(linebuf)); std::string line(linebuf); auto pos = line.find('#'); if (pos != std::string::npos) line.erase(pos); // remove comments pos = line.find('='); if (pos == std::string::npos) continue; auto key = trim(line.substr(0, pos)); if (key != name) continue; out = trim(line.substr(pos+1)); return true; } return false; } std::string read_setting(const std::string &name, std::istream &is) { std::string ret; if (!read_setting(name, is, ret)) throw std::runtime_error(std::string("Setting not found: ") + name); return ret; } std::string read_setting_default(const std::string &name, std::istream &is, const std::string &def) { std::string ret; if (!read_setting(name, is, ret)) return def; return ret; } bool file_exists(const char *path) { struct stat s{}; // check for !dir to allow symlinks or such return stat(path, &s) == 0 && (s.st_mode & S_IFDIR) != S_IFDIR; } bool dir_exists(const char *path) { struct stat s{}; return stat(path, &s) == 0 && (s.st_mode & S_IFDIR) == S_IFDIR; } luanti-org-minetestmapper-637755d/src/util.h000066400000000000000000000010001477524601400210320ustar00rootroot00000000000000#pragma once #include #include #define ARRLEN(x) (sizeof(x) / sizeof((x)[0])) template static inline T mymax(T a, T b) { return (a > b) ? a : b; } template static inline T mymin(T a, T b) { return (a > b) ? b : a; } std::string read_setting(const std::string &name, std::istream &is); std::string read_setting_default(const std::string &name, std::istream &is, const std::string &def); bool file_exists(const char *path); bool dir_exists(const char *path); luanti-org-minetestmapper-637755d/util/000077500000000000000000000000001477524601400201035ustar00rootroot00000000000000luanti-org-minetestmapper-637755d/util/build-mingw.sh000077500000000000000000000027031477524601400226620ustar00rootroot00000000000000#!/bin/bash -e ####### # this expects unpacked libraries and a toolchain file like Luanti's buildbot uses # $extradlls will typically contain the compiler-specific DLLs and libpng toolchain_file= libgd_dir= zlib_dir= zstd_dir= sqlite_dir= leveldb_dir= extradlls=( ) ####### [ -f "$toolchain_file" ] || exit 1 variant=win32 grep -q 'CX?X?_COMPILER.*x86_64-' $toolchain_file && variant=win64 echo "Detected target $variant" [ -f ./CMakeLists.txt ] || { echo "run from root folder" >&2; exit 1; } cmake -S . -B build \ -DCMAKE_TOOLCHAIN_FILE="$toolchain_file" \ -DCMAKE_EXE_LINKER_FLAGS="-s" \ \ -DENABLE_LEVELDB=1 \ \ -DLEVELDB_INCLUDE_DIR=$leveldb_dir/include \ -DLEVELDB_LIBRARY=$leveldb_dir/lib/libleveldb.dll.a \ -DLIBGD_INCLUDE_DIR=$libgd_dir/include \ -DLIBGD_LIBRARY=$libgd_dir/lib/libgd.dll.a \ -DSQLITE3_INCLUDE_DIR=$sqlite_dir/include \ -DSQLITE3_LIBRARY=$sqlite_dir/lib/libsqlite3.dll.a \ -DZLIB_INCLUDE_DIR=$zlib_dir/include \ -DZLIB_LIBRARY=$zlib_dir/lib/libz.dll.a \ -DZSTD_INCLUDE_DIR=$zstd_dir/include \ -DZSTD_LIBRARY=$zstd_dir/lib/libzstd.dll.a make -C build -j4 mkdir pack cp -p \ AUTHORS colors.txt COPYING README.rst \ build/minetestmapper.exe \ $leveldb_dir/bin/libleveldb.dll \ $libgd_dir/bin/libgd*.dll \ $sqlite_dir/bin/libsqlite*.dll \ $zlib_dir/bin/zlib1.dll \ $zstd_dir/bin/libzstd.dll \ "${extradlls[@]}" \ pack/ zipfile=$PWD/minetestmapper-$variant.zip (cd pack; zip -9r "$zipfile" *) rm -rf build pack echo "Done." luanti-org-minetestmapper-637755d/util/ci/000077500000000000000000000000001477524601400204765ustar00rootroot00000000000000luanti-org-minetestmapper-637755d/util/ci/script.sh000077500000000000000000000012141477524601400223370ustar00rootroot00000000000000#!/bin/bash -e install_linux_deps() { local upkgs=( cmake libgd-dev libsqlite3-dev libleveldb-dev libpq-dev libhiredis-dev libzstd-dev ) local fpkgs=( cmake gcc-g++ gd-devel sqlite-devel libzstd-devel zlib-ng-devel ) if command -v dnf; then sudo dnf install --setopt=install_weak_deps=False -y "${fpkgs[@]}" else sudo apt-get update sudo apt-get install -y --no-install-recommends "${upkgs[@]}" fi } run_build() { local args=( -DCMAKE_BUILD_TYPE=Debug -DENABLE_LEVELDB=ON -DENABLE_POSTGRESQL=ON -DENABLE_REDIS=ON ) [[ "$CXX" == clang* ]] && args+=(-DCMAKE_CXX_FLAGS="-fsanitize=address") cmake . "${args[@]}" make -j2 } luanti-org-minetestmapper-637755d/util/ci/test.sh000077500000000000000000000056371477524601400220270ustar00rootroot00000000000000#!/bin/bash set -eo pipefail mapdir=./testmap msg () { echo echo "==== $1" echo } # encodes a block position by X, Y, Z (positive numbers only!) encodepos () { echo "$(($1 + 0x1000 * $2 + 0x1000000 * $3))" } # create map file with sql statements writemap () { rm -rf $mapdir mkdir $mapdir echo "backend = sqlite3" >$mapdir/world.mt echo "default:stone 10 10 10" >$mapdir/colors.txt printf '%s\n' \ "CREATE TABLE d(d BLOB);" \ "INSERT INTO d VALUES (x'$(cat util/ci/test_block)');" \ "$1" \ "DROP TABLE d;" | sqlite3 $mapdir/map.sqlite } # check that a non-empty ($1=1) or empty map ($1=0) was written with the args ($2 ...) checkmap () { local c=$1 shift rm -f map.png ./minetestmapper --noemptyimage -v -i ./testmap -o map.png "$@" if [[ $c -eq 1 && ! -f map.png ]]; then echo "Output not generated!" exit 1 elif [[ $c -eq 0 && -f map.png ]]; then echo "Output was generated, none expected!" exit 1 fi echo "Passed." } # this is missing the indices and primary keys but that doesn't matter schema_old="CREATE TABLE blocks(pos INT, data BLOB);" schema_new="CREATE TABLE blocks(x INT, y INT, z INT, data BLOB);" msg "old schema" writemap " $schema_old INSERT INTO blocks SELECT $(encodepos 0 1 0), d FROM d; " checkmap 1 msg "old schema: Y limit" # Note: test data contains a plane at y = 17 an a single node at y = 18 checkmap 1 --max-y 17 checkmap 0 --max-y 16 checkmap 1 --min-y 18 checkmap 0 --min-y 19 msg "old schema: all limits" # fill the map with more blocks and then request just a single one to be rendered # this will run through internal consistency asserts. writemap " $schema_old INSERT INTO blocks SELECT $(encodepos 2 2 2), d FROM d; INSERT INTO blocks SELECT $(encodepos 1 2 2), d FROM d; INSERT INTO blocks SELECT $(encodepos 2 1 2), d FROM d; INSERT INTO blocks SELECT $(encodepos 2 2 1), d FROM d; INSERT INTO blocks SELECT $(encodepos 3 2 2), d FROM d; INSERT INTO blocks SELECT $(encodepos 2 3 2), d FROM d; INSERT INTO blocks SELECT $(encodepos 2 2 3), d FROM d; " checkmap 1 --geometry 32:32+16+16 --min-y 32 --max-y $((32+16-1)) msg "new schema" writemap " $schema_new INSERT INTO blocks SELECT 0, 1, 0, d FROM d; " checkmap 1 msg "new schema: all limits" # same as above writemap " $schema_new INSERT INTO blocks SELECT 2, 2, 2, d FROM d; INSERT INTO blocks SELECT 1, 2, 2, d FROM d; INSERT INTO blocks SELECT 2, 1, 2, d FROM d; INSERT INTO blocks SELECT 2, 2, 1, d FROM d; INSERT INTO blocks SELECT 3, 2, 2, d FROM d; INSERT INTO blocks SELECT 2, 3, 2, d FROM d; INSERT INTO blocks SELECT 2, 2, 3, d FROM d; " checkmap 1 --geometry 32:32+16+16 --min-y 32 --max-y $((32+16-1)) msg "new schema: empty map" writemap "$schema_new" checkmap 0 msg "drawplayers" writemap " $schema_new INSERT INTO blocks SELECT 0, 0, 0, d FROM d; " mkdir $mapdir/players printf '%s\n' "name = cat" "position = (80,0,80)" >$mapdir/players/cat # we can't check that it actually worked, however checkmap 1 --drawplayers --zoom 4 luanti-org-minetestmapper-637755d/util/ci/test_block000066400000000000000000000003711477524601400225530ustar00rootroot000000000000001b00ffff020278daedd4c1090000080331dd7f691710faf12589235cb12ae870fca6bffefaebafbffefaebafbffefaebbff7b708fdf1ffd11ffdd11ffdd11ffd01000000000000003836d59f010578da63000000010001000000ffffffff000002000000036169720001000d64656661756c743a73746f6e650a0000 luanti-org-minetestmapper-637755d/util/dumpnodes/000077500000000000000000000000001477524601400221015ustar00rootroot00000000000000luanti-org-minetestmapper-637755d/util/dumpnodes/init.lua000066400000000000000000000034071477524601400235530ustar00rootroot00000000000000local function get_tile(tiles, n) local tile = tiles[n] if type(tile) == 'table' then return tile.name or tile.image elseif type(tile) == 'string' then return tile end end local function strip_texture(tex) tex = (tex .. '^'):match('%(*(.-)%)*^') -- strip modifiers if tex:find("[combine", 1, true) then tex = tex:match('.-=([^:]-)') -- extract first texture elseif tex:find("[png", 1, true) then return nil -- can't end return tex end local function pairs_s(dict) local keys = {} for k in pairs(dict) do keys[#keys+1] = k end table.sort(keys) return ipairs(keys) end core.register_chatcommand("dumpnodes", { description = "Dump node and texture list for use with minetestmapper", func = function() local ntbl = {} for _, nn in pairs_s(minetest.registered_nodes) do local prefix, name = nn:match('(.-):(.*)') if prefix == nil or name == nil then print("ignored(1): " .. nn) else if ntbl[prefix] == nil then ntbl[prefix] = {} end ntbl[prefix][name] = true end end local out, err = io.open(core.get_worldpath() .. "/nodes.txt", 'wb') if not out then return true, err end local n = 0 for _, prefix in pairs_s(ntbl) do out:write('# ' .. prefix .. '\n') for _, name in pairs_s(ntbl[prefix]) do local nn = prefix .. ":" .. name local nd = core.registered_nodes[nn] local tiles = nd.tiles or nd.tile_images if tiles == nil or nd.drawtype == 'airlike' then print("ignored(2): " .. nn) else local tex = get_tile(tiles, 1) tex = tex and strip_texture(tex) if not tex then print("ignored(3): " .. nn) else out:write(nn .. ' ' .. tex .. '\n') n = n + 1 end end end out:write('\n') end out:close() return true, n .. " nodes dumped." end, }) luanti-org-minetestmapper-637755d/util/dumpnodes/mod.conf000066400000000000000000000001141477524601400235230ustar00rootroot00000000000000name = dumpnodes description = minetestmapper development mod (node dumper) luanti-org-minetestmapper-637755d/util/generate_colorstxt.py000077500000000000000000000124311477524601400243740ustar00rootroot00000000000000#!/usr/bin/env python3 import sys import os.path import getopt import re from math import sqrt try: from PIL import Image except: print("Could not load image routines, install PIL ('pillow' on pypi)!", file=sys.stderr) exit(1) ############ ############ # Instructions for generating a colors.txt file for custom games and/or mods: # 1) Add the dumpnodes mod to a Luanti world with the chosen game and mods enabled. # 2) Join ingame and run the /dumpnodes chat command. # 3) Run this script and poin it to the installation path of the game using -g, # the path(s) where mods are stored using -m and the nodes.txt in your world folder. # Example command line: # ./util/generate_colorstxt.py --game /usr/share/luanti/games/minetest_game \ # -m ~/.minetest/mods ~/.minetest/worlds/my_world/nodes.txt # 4) Copy the resulting colors.txt file to your world folder or to any other place # and use it with minetestmapper's --colors option. ########### ########### # minimal sed syntax, s|match|replace| and /match/d supported REPLACEMENTS = [ # Delete some nodes that are usually hidden r'/^fireflies:firefly /d', r'/^butterflies:butterfly_/d', # Nicer colors for water and lava r's/^(default:(river_)?water_(flowing|source)) [0-9 ]+$/\1 39 66 106 128 224/', r's/^(default:lava_(flowing|source)) [0-9 ]+$/\1 255 100 0/', # Transparency for glass nodes and panes r's/^(default:.*glass) ([0-9 ]+)$/\1 \2 64 16/', r's/^(doors:.*glass[^ ]*) ([0-9 ]+)$/\1 \2 64 16/', r's/^(xpanes:.*(pane|bar)[^ ]*) ([0-9 ]+)$/\1 \3 64 16/', ] def usage(): print("Usage: generate_colorstxt.py [options] [input file] [output file]") print("If not specified the input file defaults to ./nodes.txt and the output file to ./colors.txt") print(" -g / --game \t\tSet path to the game (for textures), required") print(" -m / --mods \t\tAdd search path for mod textures") print(" --replace \t\tLoad replacements from file (ADVANCED)") def collect_files(path): dirs = [] with os.scandir(path) as it: for entry in it: if entry.name[0] == '.': continue if entry.is_dir(): dirs.append(entry.path) continue if entry.is_file() and '.' in entry.name: if entry.name not in textures.keys(): textures[entry.name] = entry.path for path2 in dirs: collect_files(path2) def average_color(filename): inp = Image.open(filename).convert('RGBA') data = inp.load() c0, c1, c2 = [], [], [] for x in range(inp.size[0]): for y in range(inp.size[1]): px = data[x, y] if px[3] < 128: continue # alpha c0.append(px[0]**2) c1.append(px[1]**2) c2.append(px[2]**2) if len(c0) == 0: print(f"didn't find color for '{os.path.basename(filename)}'", file=sys.stderr) return "0 0 0" c0 = sqrt(sum(c0) / len(c0)) c1 = sqrt(sum(c1) / len(c1)) c2 = sqrt(sum(c2) / len(c2)) return "%d %d %d" % (c0, c1, c2) def apply_sed(line, exprs): for expr in exprs: if expr[0] == '/': if not expr.endswith("/d"): raise ValueError() if re.search(expr[1:-2], line): return '' elif expr[0] == 's': expr = expr.split(expr[1]) if len(expr) != 4 or expr[3] != '': raise ValueError() line = re.sub(expr[1], expr[2], line) else: raise ValueError() return line # try: opts, args = getopt.getopt(sys.argv[1:], "hg:m:", ["help", "game=", "mods=", "replace="]) except getopt.GetoptError as e: print(str(e)) exit(1) if ('-h', '') in opts or ('--help', '') in opts: usage() exit(0) input_file = "./nodes.txt" output_file = "./colors.txt" texturepaths = [] try: gamepath = next(o[1] for o in opts if o[0] in ('-g', '--game')) if not os.path.isdir(os.path.join(gamepath, "mods")): print(f"'{gamepath}' doesn't exist or does not contain a game.", file=sys.stderr) exit(1) texturepaths.append(os.path.join(gamepath, "mods")) except StopIteration: print("No game path set but one is required. (see --help)", file=sys.stderr) exit(1) try: tmp = next(o[1] for o in opts if o[0] == "--replace") REPLACEMENTS.clear() with open(tmp, 'r') as f: for line in f: if not line or line[0] == '#': continue REPLACEMENTS.append(line.strip()) except StopIteration: pass for o in opts: if o[0] not in ('-m', '--mods'): continue if not os.path.isdir(o[1]): print(f"Given path '{o[1]}' does not exist.'", file=sys.stderr) exit(1) texturepaths.append(o[1]) if len(args) > 2: print("Too many arguments.", file=sys.stderr) exit(1) if len(args) > 1: output_file = args[1] if len(args) > 0: input_file = args[0] if not os.path.exists(input_file) or os.path.isdir(input_file): print(f"Input file '{input_file}' does not exist.", file=sys.stderr) exit(1) # print(f"Collecting textures from {len(texturepaths)} path(s)... ", end="", flush=True) textures = {} for path in texturepaths: collect_files(path) print("done") print("Processing nodes...") fin = open(input_file, 'r') fout = open(output_file, 'w') n = 0 for line in fin: line = line.rstrip('\r\n') if not line or line[0] == '#': fout.write(line + '\n') continue node, tex = line.split(" ") if not tex or tex == "blank.png": continue elif tex not in textures.keys(): print(f"skip {node} texture not found") continue color = average_color(textures[tex]) line = f"{node} {color}" #print(f"ok {node}") line = apply_sed(line, REPLACEMENTS) if line: fout.write(line + '\n') n += 1 fin.close() fout.close() print(f"Done, {n} entries written.")