├── util ├── dumpnodes │ ├── mod.conf │ └── init.lua ├── ci │ ├── test_block │ ├── script.sh │ └── test.sh ├── build-mingw.sh └── generate_colorstxt.py ├── AUTHORS ├── src ├── config.h ├── cmake_config.h.in ├── PlayerAttributes.h ├── log.cpp ├── util.h ├── ZlibDecompressor.h ├── ZstdDecompressor.h ├── BlockDecoder.h ├── log.h ├── PixelAttributes.h ├── Image.h ├── db-redis.h ├── db-leveldb.h ├── PixelAttributes.cpp ├── db-postgresql.h ├── ZstdDecompressor.cpp ├── types.h ├── ZlibDecompressor.cpp ├── util.cpp ├── db-sqlite3.h ├── db.h ├── Image.cpp ├── db-leveldb.cpp ├── PlayerAttributes.cpp ├── TileGenerator.h ├── BlockDecoder.cpp ├── db-redis.cpp ├── db-postgresql.cpp ├── db-sqlite3.cpp ├── mapper.cpp └── TileGenerator.cpp ├── .dockerignore ├── .gitignore ├── Dockerfile ├── cmake └── FindZstd.cmake ├── COPYING ├── .github └── workflows │ ├── build.yml │ └── docker_image.yml ├── minetestmapper.6 ├── README.rst ├── CMakeLists.txt └── colors.txt /util/dumpnodes/mod.conf: -------------------------------------------------------------------------------- 1 | name = dumpnodes 2 | description = minetestmapper development mod (node dumper) 3 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Miroslav Bendík 2 | ShadowNinja 3 | sfan5 4 | -------------------------------------------------------------------------------- /src/config.h: -------------------------------------------------------------------------------- 1 | #if defined(MSDOS) || defined(__OS2__) || defined(__NT__) || defined(_WIN32) 2 | #define PATH_SEPARATOR '\\' 3 | #else 4 | #define PATH_SEPARATOR '/' 5 | #endif 6 | 7 | #include "cmake_config.h" 8 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .github 3 | 4 | *~ 5 | 6 | minetestmapper 7 | minetestmapper.exe 8 | 9 | CMakeCache.txt 10 | CMakeFiles/ 11 | CPack*.cmake 12 | _CPack_Packages/ 13 | install_manifest.txt 14 | Makefile 15 | cmake_install.cmake 16 | cmake_config.h 17 | -------------------------------------------------------------------------------- /util/ci/test_block: -------------------------------------------------------------------------------- 1 | 1b00ffff020278daedd4c1090000080331dd7f691710faf12589235cb12ae870fca6bffefaebafbffefaebafbffefaebbff7b708fdf1ffd11ffdd11ffdd11ffd01000000000000003836d59f010578da63000000010001000000ffffffff000002000000036169720001000d64656661756c743a73746f6e650a0000 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | 3 | /minetestmapper 4 | /minetestmapper.exe 5 | /colors.txt 6 | 7 | CMakeCache.txt 8 | CMakeFiles/ 9 | CPack*.cmake 10 | _CPack_Packages/ 11 | install_manifest.txt 12 | Makefile 13 | cmake_install.cmake 14 | cmake_config.h 15 | compile_commands.json 16 | .vscode/ 17 | -------------------------------------------------------------------------------- /src/cmake_config.h.in: -------------------------------------------------------------------------------- 1 | // Filled in by the build system 2 | 3 | #ifndef CMAKE_CONFIG_H 4 | #define CMAKE_CONFIG_H 5 | 6 | #cmakedefine01 USE_POSTGRESQL 7 | #cmakedefine01 USE_LEVELDB 8 | #cmakedefine01 USE_REDIS 9 | #cmakedefine01 USE_ZLIB_NG 10 | 11 | #define SHAREDIR "@SHAREDIR@" 12 | 13 | #endif 14 | 15 | -------------------------------------------------------------------------------- /src/PlayerAttributes.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | struct Player 7 | { 8 | std::string name; 9 | float x, y, z; 10 | }; 11 | 12 | class PlayerAttributes 13 | { 14 | public: 15 | typedef std::list Players; 16 | 17 | PlayerAttributes(const std::string &worldDir); 18 | Players::const_iterator begin() const; 19 | Players::const_iterator end() const; 20 | 21 | private: 22 | Players m_players; 23 | }; 24 | -------------------------------------------------------------------------------- /src/log.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "log.h" 4 | 5 | StreamProxy errorstream(nullptr); 6 | StreamProxy verbosestream(nullptr); 7 | 8 | void configure_log_streams(bool verbose) 9 | { 10 | errorstream << std::flush; 11 | verbosestream << std::flush; 12 | 13 | errorstream = std::cerr.good() ? &std::cerr : nullptr; 14 | // std::clog does not automatically flush 15 | verbosestream = (verbose && std::clog.good()) ? &std::clog : nullptr; 16 | } 17 | -------------------------------------------------------------------------------- /src/util.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #define ARRLEN(x) (sizeof(x) / sizeof((x)[0])) 7 | 8 | template 9 | static inline T mymax(T a, T b) 10 | { 11 | return (a > b) ? a : b; 12 | } 13 | 14 | template 15 | static inline T mymin(T a, T b) 16 | { 17 | return (a > b) ? b : a; 18 | } 19 | 20 | std::string read_setting(const std::string &name, std::istream &is); 21 | 22 | std::string read_setting_default(const std::string &name, std::istream &is, 23 | const std::string &def); 24 | 25 | bool file_exists(const char *path); 26 | 27 | bool dir_exists(const char *path); 28 | -------------------------------------------------------------------------------- /src/ZlibDecompressor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "types.h" 5 | 6 | class ZlibDecompressor 7 | { 8 | public: 9 | class DecompressError : public std::exception { 10 | public: 11 | const char* what() const noexcept override { 12 | return "ZlibDecompressor::DecompressError"; 13 | } 14 | }; 15 | 16 | ZlibDecompressor(const u8 *data, size_t size); 17 | ~ZlibDecompressor(); 18 | void setSeekPos(size_t seekPos); 19 | size_t seekPos() const { return m_seekPos; } 20 | // Decompress and return one zlib stream from the buffer 21 | // Advances seekPos as appropriate. 22 | void decompress(ustring &dst); 23 | 24 | private: 25 | const u8 *m_data; 26 | size_t m_seekPos, m_size; 27 | }; 28 | -------------------------------------------------------------------------------- /src/ZstdDecompressor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include "types.h" 5 | 6 | class ZstdDecompressor 7 | { 8 | public: 9 | class DecompressError : public std::exception { 10 | public: 11 | const char* what() const noexcept override { 12 | return "ZstdDecompressor::DecompressError"; 13 | } 14 | }; 15 | 16 | ZstdDecompressor(); 17 | ~ZstdDecompressor(); 18 | void setData(const u8 *data, size_t size, size_t seekPos); 19 | size_t seekPos() const { return m_seekPos; } 20 | // Decompress and return one zstd stream from the buffer 21 | // Advances seekPos as appropriate. 22 | void decompress(ustring &dst); 23 | 24 | private: 25 | void *m_stream; // ZSTD_DStream 26 | const u8 *m_data; 27 | size_t m_seekPos, m_size; 28 | }; 29 | -------------------------------------------------------------------------------- /src/BlockDecoder.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include "types.h" 6 | #include 7 | 8 | class BlockDecoder { 9 | public: 10 | BlockDecoder(); 11 | 12 | void reset(); 13 | void decode(const ustring &data); 14 | bool isEmpty() const; 15 | // returns "" for air, ignore and invalid nodes 16 | const std::string &getNode(u8 x, u8 y, u8 z) const; 17 | 18 | private: 19 | typedef std::unordered_map NameMap; 20 | NameMap m_nameMap; 21 | uint16_t m_blockAirId, m_blockIgnoreId; 22 | 23 | u8 m_version, m_contentWidth; 24 | ustring m_mapData; 25 | 26 | // cached allocations/instances for performance 27 | ZstdDecompressor m_zstd_decompressor; 28 | ustring m_scratch; 29 | }; 30 | -------------------------------------------------------------------------------- /util/ci/script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | install_linux_deps() { 4 | local upkgs=( 5 | cmake libgd-dev libsqlite3-dev libleveldb-dev libpq-dev 6 | libhiredis-dev libzstd-dev 7 | ) 8 | local fpkgs=( 9 | cmake gcc-g++ gd-devel sqlite-devel libzstd-devel zlib-ng-devel 10 | ) 11 | 12 | if command -v dnf; then 13 | sudo dnf install --setopt=install_weak_deps=False -y "${fpkgs[@]}" 14 | else 15 | sudo apt-get update 16 | sudo apt-get install -y --no-install-recommends "${upkgs[@]}" 17 | fi 18 | } 19 | 20 | run_build() { 21 | local args=( 22 | -DCMAKE_BUILD_TYPE=Debug 23 | -DENABLE_LEVELDB=ON -DENABLE_POSTGRESQL=ON -DENABLE_REDIS=ON 24 | ) 25 | [[ "$CXX" == clang* ]] && args+=(-DCMAKE_CXX_FLAGS="-fsanitize=address") 26 | cmake . "${args[@]}" 27 | 28 | make -j2 29 | } 30 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG DOCKER_IMAGE=alpine:3.20 2 | FROM $DOCKER_IMAGE AS builder 3 | 4 | RUN apk add --no-cache build-base cmake \ 5 | gd-dev sqlite-dev postgresql-dev hiredis-dev leveldb-dev \ 6 | ninja 7 | 8 | COPY . /usr/src/minetestmapper 9 | WORKDIR /usr/src/minetestmapper 10 | 11 | RUN cmake -B build -G Ninja && \ 12 | cmake --build build --parallel $(nproc) && \ 13 | cmake --install build 14 | 15 | FROM $DOCKER_IMAGE AS runtime 16 | 17 | RUN apk add --no-cache libstdc++ libgcc libpq \ 18 | gd sqlite-libs postgresql hiredis leveldb 19 | 20 | COPY --from=builder /usr/local/share/luanti /usr/local/share/luanti 21 | COPY --from=builder /usr/local/bin/minetestmapper /usr/local/bin/minetestmapper 22 | COPY COPYING /usr/local/share/minetest/minetestmapper.COPYING 23 | 24 | ENTRYPOINT ["/usr/local/bin/minetestmapper"] 25 | -------------------------------------------------------------------------------- /cmake/FindZstd.cmake: -------------------------------------------------------------------------------- 1 | mark_as_advanced(ZSTD_LIBRARY ZSTD_INCLUDE_DIR) 2 | 3 | find_path(ZSTD_INCLUDE_DIR NAMES zstd.h) 4 | 5 | find_library(ZSTD_LIBRARY NAMES zstd) 6 | 7 | if(ZSTD_INCLUDE_DIR AND ZSTD_LIBRARY) 8 | # Check that the API we use exists 9 | include(CheckSymbolExists) 10 | unset(HAVE_ZSTD_INITDSTREAM CACHE) 11 | set(CMAKE_REQUIRED_INCLUDES ${ZSTD_INCLUDE_DIR}) 12 | set(CMAKE_REQUIRED_LIBRARIES ${ZSTD_LIBRARY}) 13 | check_symbol_exists(ZSTD_initDStream zstd.h HAVE_ZSTD_INITDSTREAM) 14 | unset(CMAKE_REQUIRED_INCLUDES) 15 | unset(CMAKE_REQUIRED_LIBRARIES) 16 | 17 | if(NOT HAVE_ZSTD_INITDSTREAM) 18 | unset(ZSTD_INCLUDE_DIR CACHE) 19 | unset(ZSTD_LIBRARY CACHE) 20 | endif() 21 | endif() 22 | 23 | include(FindPackageHandleStandardArgs) 24 | find_package_handle_standard_args(Zstd DEFAULT_MSG ZSTD_LIBRARY ZSTD_INCLUDE_DIR) 25 | -------------------------------------------------------------------------------- /src/log.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | // Forwards to an ostream, optionally 7 | class StreamProxy { 8 | public: 9 | StreamProxy(std::ostream *os) : m_os(os) {} 10 | 11 | template 12 | StreamProxy &operator<<(T &&arg) 13 | { 14 | if (m_os) 15 | *m_os << std::forward(arg); 16 | return *this; 17 | } 18 | 19 | StreamProxy &operator<<(std::ostream &(*func)(std::ostream&)) 20 | { 21 | if (m_os) 22 | *m_os << func; 23 | return *this; 24 | } 25 | 26 | private: 27 | std::ostream *m_os; 28 | }; 29 | 30 | /// Error and warning output, forwards to std::cerr 31 | extern StreamProxy errorstream; 32 | /// Verbose output, might forward to std::cerr 33 | extern StreamProxy verbosestream; 34 | 35 | /** 36 | * Configure log streams defined in this file. 37 | * @param verbose enable verbose output 38 | * @note not thread-safe! 39 | */ 40 | void configure_log_streams(bool verbose); 41 | -------------------------------------------------------------------------------- /src/PixelAttributes.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #define BLOCK_SIZE 16 7 | 8 | struct PixelAttribute { 9 | PixelAttribute() : height(INT16_MIN), thickness(0) {}; 10 | 11 | int16_t height; 12 | uint8_t thickness; 13 | 14 | inline bool valid_height() const { 15 | return height != INT16_MIN; 16 | } 17 | }; 18 | 19 | class PixelAttributes 20 | { 21 | public: 22 | PixelAttributes(); 23 | virtual ~PixelAttributes(); 24 | 25 | void setWidth(int width); 26 | void scroll(); 27 | 28 | inline PixelAttribute &attribute(int z, int x) { 29 | return m_pixelAttributes[z + 1][x + 1]; 30 | }; 31 | 32 | private: 33 | void freeAttributes(); 34 | 35 | private: 36 | enum Line { 37 | FirstLine = 0, 38 | LastLine = BLOCK_SIZE, 39 | EmptyLine = BLOCK_SIZE + 1, 40 | LineCount = BLOCK_SIZE + 2 41 | }; 42 | PixelAttribute *m_pixelAttributes[BLOCK_SIZE + 2]; // 1px gradient + empty 43 | int m_width; 44 | }; 45 | -------------------------------------------------------------------------------- /src/Image.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "types.h" 4 | #include 5 | #include 6 | 7 | struct Color { 8 | Color() : r(0), g(0), b(0), a(0) {}; 9 | Color(u8 r, u8 g, u8 b) : r(r), g(g), b(b), a(255) {}; 10 | Color(u8 r, u8 g, u8 b, u8 a) : r(r), g(g), b(b), a(a) {}; 11 | 12 | u8 r, g, b, a; 13 | }; 14 | 15 | class Image { 16 | public: 17 | Image(int width, int height); 18 | ~Image(); 19 | 20 | Image(const Image&) = delete; 21 | Image& operator=(const Image&) = delete; 22 | 23 | void setPixel(int x, int y, const Color &c); 24 | Color getPixel(int x, int y); 25 | void drawLine(int x1, int y1, int x2, int y2, const Color &c); 26 | void drawText(int x, int y, const std::string &s, const Color &c); 27 | void drawFilledRect(int x, int y, int w, int h, const Color &c); 28 | void drawCircle(int x, int y, int diameter, const Color &c); 29 | void save(const std::string &filename); 30 | 31 | private: 32 | int m_width, m_height; 33 | gdImagePtr m_image; 34 | }; 35 | -------------------------------------------------------------------------------- /src/db-redis.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "db.h" 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | class DBRedis : public DB { 10 | public: 11 | DBRedis(const std::string &mapdir); 12 | std::vector getBlockPosXZ(BlockPos min, BlockPos max) override; 13 | void getBlocksOnXZ(BlockList &blocks, int16_t x, int16_t z, 14 | int16_t min_y, int16_t max_y) override; 15 | void getBlocksByPos(BlockList &blocks, 16 | const std::vector &positions) override; 17 | ~DBRedis() override; 18 | 19 | bool preferRangeQueries() const override { return false; } 20 | 21 | private: 22 | using pos2d = std::pair; 23 | static const char *replyTypeStr(int type); 24 | 25 | void loadPosCache(); 26 | void HMGET(const std::vector &positions, 27 | std::function result); 28 | 29 | // indexed by Z, contains all (x,y) position pairs 30 | std::unordered_map> posCache; 31 | 32 | redisContext *ctx; 33 | std::string hash; 34 | }; 35 | -------------------------------------------------------------------------------- /src/db-leveldb.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "db.h" 4 | #include 5 | #include 6 | #include 7 | 8 | class DBLevelDB : public DB { 9 | public: 10 | DBLevelDB(const std::string &mapdir); 11 | std::vector getBlockPosXZ(BlockPos min, BlockPos max) override; 12 | void getBlocksOnXZ(BlockList &blocks, int16_t x, int16_t z, 13 | int16_t min_y, int16_t max_y) override; 14 | void getBlocksByPos(BlockList &blocks, 15 | const std::vector &positions) override; 16 | ~DBLevelDB() override; 17 | 18 | bool preferRangeQueries() const override { return false; } 19 | 20 | private: 21 | struct vec2 { 22 | int16_t x, y; 23 | constexpr vec2() : x(0), y(0) {} 24 | constexpr vec2(int16_t x, int16_t y) : x(x), y(y) {} 25 | 26 | inline bool operator<(const vec2 &p) const 27 | { 28 | if (x < p.x) 29 | return true; 30 | if (x > p.x) 31 | return false; 32 | return y < p.y; 33 | } 34 | }; 35 | 36 | void loadPosCache(); 37 | 38 | // indexed by Z, contains all (x,y) position pairs 39 | std::unordered_map> posCache; 40 | leveldb::DB *db = NULL; 41 | }; 42 | -------------------------------------------------------------------------------- /src/PixelAttributes.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "PixelAttributes.h" 4 | 5 | PixelAttributes::PixelAttributes(): 6 | m_width(0) 7 | { 8 | for (size_t i = 0; i < LineCount; ++i) { 9 | m_pixelAttributes[i] = nullptr; 10 | } 11 | } 12 | 13 | PixelAttributes::~PixelAttributes() 14 | { 15 | freeAttributes(); 16 | } 17 | 18 | void PixelAttributes::setWidth(int width) 19 | { 20 | freeAttributes(); 21 | m_width = width + 1; // 1px gradient calculation 22 | for (size_t i = 0; i < LineCount; ++i) { 23 | m_pixelAttributes[i] = new PixelAttribute[m_width]; 24 | } 25 | } 26 | 27 | void PixelAttributes::scroll() 28 | { 29 | size_t lineLength = m_width * sizeof(PixelAttribute); 30 | memcpy(m_pixelAttributes[FirstLine], m_pixelAttributes[LastLine], lineLength); 31 | for (size_t i = 1; i < LineCount - 1; ++i) { 32 | memcpy(m_pixelAttributes[i], m_pixelAttributes[EmptyLine], lineLength); 33 | } 34 | } 35 | 36 | void PixelAttributes::freeAttributes() 37 | { 38 | for (size_t i = 0; i < LineCount; ++i) { 39 | if (m_pixelAttributes[i] != nullptr) { 40 | delete[] m_pixelAttributes[i]; 41 | m_pixelAttributes[i] = nullptr; 42 | } 43 | } 44 | } 45 | 46 | -------------------------------------------------------------------------------- /src/db-postgresql.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "db.h" 4 | #include 5 | 6 | class PostgreSQLBase { 7 | public: 8 | ~PostgreSQLBase(); 9 | 10 | protected: 11 | void openDatabase(const char *connect_string); 12 | 13 | PGresult *checkResults(PGresult *res, bool clear = true); 14 | void prepareStatement(const std::string &name, const std::string &sql) { 15 | checkResults(PQprepare(db, name.c_str(), sql.c_str(), 0, NULL)); 16 | } 17 | PGresult *execPrepared( 18 | const char *stmtName, const int paramsNumber, 19 | const void **params, 20 | const int *paramsLengths = nullptr, const int *paramsFormats = nullptr, 21 | bool clear = true 22 | ); 23 | 24 | PGconn *db = NULL; 25 | }; 26 | 27 | class DBPostgreSQL : public DB, PostgreSQLBase { 28 | public: 29 | DBPostgreSQL(const std::string &mapdir); 30 | std::vector getBlockPosXZ(BlockPos min, BlockPos max) override; 31 | void getBlocksOnXZ(BlockList &blocks, int16_t x, int16_t z, 32 | int16_t min_y, int16_t max_y) override; 33 | void getBlocksByPos(BlockList &blocks, 34 | const std::vector &positions) override; 35 | ~DBPostgreSQL() override; 36 | 37 | bool preferRangeQueries() const override { return true; } 38 | 39 | private: 40 | int pg_binary_to_int(PGresult *res, int row, int col); 41 | }; 42 | -------------------------------------------------------------------------------- /src/ZstdDecompressor.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "ZstdDecompressor.h" 3 | 4 | ZstdDecompressor::ZstdDecompressor(): 5 | m_data(nullptr), 6 | m_seekPos(0), 7 | m_size(0) 8 | { 9 | m_stream = ZSTD_createDStream(); 10 | } 11 | 12 | ZstdDecompressor::~ZstdDecompressor() 13 | { 14 | ZSTD_freeDStream(reinterpret_cast(m_stream)); 15 | } 16 | 17 | void ZstdDecompressor::setData(const u8 *data, size_t size, size_t seekPos) 18 | { 19 | m_data = data; 20 | m_seekPos = seekPos; 21 | m_size = size; 22 | } 23 | 24 | void ZstdDecompressor::decompress(ustring &buffer) 25 | { 26 | ZSTD_DStream *stream = reinterpret_cast(m_stream); 27 | ZSTD_inBuffer inbuf = { m_data, m_size, m_seekPos }; 28 | 29 | // output space is extended in chunks of this size 30 | constexpr size_t BUFSIZE = 8 * 1024; 31 | 32 | if (buffer.empty()) 33 | buffer.resize(BUFSIZE); 34 | ZSTD_outBuffer outbuf = { &buffer[0], buffer.size(), 0 }; 35 | 36 | ZSTD_initDStream(stream); 37 | 38 | size_t ret; 39 | do { 40 | ret = ZSTD_decompressStream(stream, &outbuf, &inbuf); 41 | if (ret && ZSTD_isError(ret)) 42 | throw DecompressError(); 43 | if (outbuf.size == outbuf.pos) { 44 | outbuf.size += BUFSIZE; 45 | buffer.resize(outbuf.size); 46 | outbuf.dst = &buffer[0]; 47 | } 48 | } while (ret != 0); 49 | 50 | m_seekPos = inbuf.pos; 51 | buffer.resize(outbuf.pos); 52 | } 53 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2014, Miroslav Bendík and various contributors (see AUTHORS) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /src/types.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | // Define custom char traits since std::char_traits is not part of C++ standard 6 | struct uchar_traits : std::char_traits 7 | { 8 | using super = std::char_traits; 9 | using char_type = unsigned char; 10 | 11 | static void assign(char_type& c1, const char_type& c2) noexcept { 12 | c1 = c2; 13 | } 14 | static char_type* assign(char_type* ptr, std::size_t count, char_type c2) { 15 | return reinterpret_cast( 16 | super::assign(reinterpret_cast(ptr), count, static_cast(c2))); 17 | } 18 | 19 | static char_type* move(char_type* dest, const char_type* src, std::size_t count) { 20 | return reinterpret_cast( 21 | super::move(reinterpret_cast(dest), reinterpret_cast(src), count)); 22 | } 23 | 24 | static char_type* copy(char_type* dest, const char_type* src, std::size_t count) { 25 | return reinterpret_cast( 26 | super::copy(reinterpret_cast(dest), reinterpret_cast(src), count)); 27 | } 28 | 29 | static int compare(const char_type* s1, const char_type* s2, std::size_t count) { 30 | return super::compare(reinterpret_cast(s1), reinterpret_cast(s2), count); 31 | } 32 | 33 | static char_type to_char_type(int_type c) noexcept { 34 | return static_cast(c); 35 | } 36 | }; 37 | 38 | typedef std::basic_string ustring; 39 | typedef unsigned int uint; 40 | typedef unsigned char u8; 41 | -------------------------------------------------------------------------------- /util/build-mingw.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | ####### 4 | # this expects unpacked libraries and a toolchain file like Luanti's buildbot uses 5 | # $extradlls will typically contain the compiler-specific DLLs and libpng 6 | toolchain_file= 7 | libgd_dir= 8 | zlib_dir= 9 | zstd_dir= 10 | sqlite_dir= 11 | leveldb_dir= 12 | extradlls=( 13 | ) 14 | ####### 15 | 16 | [ -f "$toolchain_file" ] || exit 1 17 | variant=win32 18 | grep -q 'CX?X?_COMPILER.*x86_64-' $toolchain_file && variant=win64 19 | echo "Detected target $variant" 20 | 21 | [ -f ./CMakeLists.txt ] || { echo "run from root folder" >&2; exit 1; } 22 | 23 | cmake -S . -B build \ 24 | -DCMAKE_TOOLCHAIN_FILE="$toolchain_file" \ 25 | -DCMAKE_EXE_LINKER_FLAGS="-s" \ 26 | \ 27 | -DENABLE_LEVELDB=1 \ 28 | \ 29 | -DLEVELDB_INCLUDE_DIR=$leveldb_dir/include \ 30 | -DLEVELDB_LIBRARY=$leveldb_dir/lib/libleveldb.dll.a \ 31 | -DLIBGD_INCLUDE_DIR=$libgd_dir/include \ 32 | -DLIBGD_LIBRARY=$libgd_dir/lib/libgd.dll.a \ 33 | -DSQLITE3_INCLUDE_DIR=$sqlite_dir/include \ 34 | -DSQLITE3_LIBRARY=$sqlite_dir/lib/libsqlite3.dll.a \ 35 | -DZLIB_INCLUDE_DIR=$zlib_dir/include \ 36 | -DZLIB_LIBRARY=$zlib_dir/lib/libz.dll.a \ 37 | -DZSTD_INCLUDE_DIR=$zstd_dir/include \ 38 | -DZSTD_LIBRARY=$zstd_dir/lib/libzstd.dll.a 39 | 40 | make -C build -j4 41 | 42 | mkdir pack 43 | cp -p \ 44 | AUTHORS colors.txt COPYING README.rst \ 45 | build/minetestmapper.exe \ 46 | $leveldb_dir/bin/libleveldb.dll \ 47 | $libgd_dir/bin/libgd*.dll \ 48 | $sqlite_dir/bin/libsqlite*.dll \ 49 | $zlib_dir/bin/zlib1.dll \ 50 | $zstd_dir/bin/libzstd.dll \ 51 | "${extradlls[@]}" \ 52 | pack/ 53 | zipfile=$PWD/minetestmapper-$variant.zip 54 | (cd pack; zip -9r "$zipfile" *) 55 | 56 | rm -rf build pack 57 | echo "Done." 58 | -------------------------------------------------------------------------------- /src/ZlibDecompressor.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "ZlibDecompressor.h" 3 | #include "config.h" 4 | 5 | // for convenient usage of both 6 | #if USE_ZLIB_NG 7 | #include 8 | #define z_stream zng_stream 9 | #define Z(x) zng_ ## x 10 | #else 11 | #include 12 | #define Z(x) x 13 | #endif 14 | 15 | ZlibDecompressor::ZlibDecompressor(const u8 *data, size_t size): 16 | m_data(data), 17 | m_seekPos(0), 18 | m_size(size) 19 | { 20 | } 21 | 22 | ZlibDecompressor::~ZlibDecompressor() 23 | { 24 | } 25 | 26 | void ZlibDecompressor::setSeekPos(size_t seekPos) 27 | { 28 | m_seekPos = seekPos; 29 | } 30 | 31 | void ZlibDecompressor::decompress(ustring &buffer) 32 | { 33 | const unsigned char *data = m_data + m_seekPos; 34 | const size_t size = m_size - m_seekPos; 35 | 36 | // output space is extended in chunks of this size 37 | constexpr size_t BUFSIZE = 8 * 1024; 38 | 39 | z_stream strm; 40 | strm.zalloc = Z_NULL; 41 | strm.zfree = Z_NULL; 42 | strm.opaque = Z_NULL; 43 | strm.next_in = Z_NULL; 44 | strm.avail_in = 0; 45 | 46 | if (Z(inflateInit)(&strm) != Z_OK) 47 | throw DecompressError(); 48 | 49 | strm.next_in = const_cast(data); 50 | strm.avail_in = size; 51 | if (buffer.empty()) 52 | buffer.resize(BUFSIZE); 53 | strm.next_out = &buffer[0]; 54 | strm.avail_out = buffer.size(); 55 | 56 | int ret = 0; 57 | do { 58 | ret = Z(inflate)(&strm, Z_NO_FLUSH); 59 | if (strm.avail_out == 0) { 60 | const auto off = buffer.size(); 61 | buffer.resize(off + BUFSIZE); 62 | strm.next_out = &buffer[off]; 63 | strm.avail_out = BUFSIZE; 64 | } 65 | } while (ret == Z_OK); 66 | if (ret != Z_STREAM_END) 67 | throw DecompressError(); 68 | 69 | m_seekPos += strm.next_in - data; 70 | buffer.resize(buffer.size() - strm.avail_out); 71 | (void) Z(inflateEnd)(&strm); 72 | } 73 | 74 | -------------------------------------------------------------------------------- /src/util.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "util.h" 7 | 8 | static std::string trim(const std::string &s) 9 | { 10 | auto isspace = [] (char c) { 11 | return c == ' ' || c == '\t' || c == '\r' || c == '\n'; 12 | }; 13 | 14 | size_t front = 0; 15 | while (isspace(s[front])) 16 | ++front; 17 | size_t back = s.size() - 1; 18 | while (back > front && isspace(s[back])) 19 | --back; 20 | 21 | return s.substr(front, back - front + 1); 22 | } 23 | 24 | static bool read_setting(const std::string &name, std::istream &is, std::string &out) 25 | { 26 | char linebuf[512]; 27 | is.seekg(0); 28 | while (is.good()) { 29 | is.getline(linebuf, sizeof(linebuf)); 30 | std::string line(linebuf); 31 | 32 | auto pos = line.find('#'); 33 | if (pos != std::string::npos) 34 | line.erase(pos); // remove comments 35 | 36 | pos = line.find('='); 37 | if (pos == std::string::npos) 38 | continue; 39 | auto key = trim(line.substr(0, pos)); 40 | if (key != name) 41 | continue; 42 | out = trim(line.substr(pos+1)); 43 | return true; 44 | } 45 | return false; 46 | } 47 | 48 | std::string read_setting(const std::string &name, std::istream &is) 49 | { 50 | std::string ret; 51 | if (!read_setting(name, is, ret)) 52 | throw std::runtime_error(std::string("Setting not found: ") + name); 53 | return ret; 54 | } 55 | 56 | std::string read_setting_default(const std::string &name, std::istream &is, 57 | const std::string &def) 58 | { 59 | std::string ret; 60 | if (!read_setting(name, is, ret)) 61 | return def; 62 | return ret; 63 | } 64 | 65 | bool file_exists(const char *path) 66 | { 67 | struct stat s{}; 68 | // check for !dir to allow symlinks or such 69 | return stat(path, &s) == 0 && (s.st_mode & S_IFDIR) != S_IFDIR; 70 | } 71 | 72 | bool dir_exists(const char *path) 73 | { 74 | struct stat s{}; 75 | return stat(path, &s) == 0 && (s.st_mode & S_IFDIR) == S_IFDIR; 76 | } 77 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | # build on source or workflow changes 4 | on: 5 | push: 6 | paths: 7 | - '**.[ch]' 8 | - '**.cpp' 9 | - '**/CMakeLists.txt' 10 | - 'util/ci/**' 11 | - '.github/workflows/**.yml' 12 | pull_request: 13 | paths: 14 | - '**.[ch]' 15 | - '**.cpp' 16 | - '**/CMakeLists.txt' 17 | - 'util/ci/**' 18 | - '.github/workflows/**.yml' 19 | 20 | jobs: 21 | gcc: 22 | runs-on: ubuntu-22.04 23 | steps: 24 | - uses: actions/checkout@v4 25 | - name: Install deps 26 | run: | 27 | source util/ci/script.sh 28 | install_linux_deps 29 | 30 | - name: Build 31 | run: | 32 | source util/ci/script.sh 33 | run_build 34 | env: 35 | CC: gcc 36 | CXX: g++ 37 | 38 | - name: Test 39 | run: | 40 | ./util/ci/test.sh 41 | 42 | - name: Test Install 43 | run: | 44 | make DESTDIR=/tmp/install install 45 | 46 | clang: 47 | runs-on: ubuntu-22.04 48 | steps: 49 | - uses: actions/checkout@v4 50 | - name: Install deps 51 | run: | 52 | source util/ci/script.sh 53 | install_linux_deps 54 | 55 | - name: Build 56 | run: | 57 | source util/ci/script.sh 58 | run_build 59 | env: 60 | CC: clang 61 | CXX: clang++ 62 | 63 | - name: Test 64 | run: | 65 | ./util/ci/test.sh 66 | 67 | gcc_fedora: 68 | runs-on: ubuntu-latest 69 | container: 70 | image: fedora:latest 71 | steps: 72 | - uses: actions/checkout@v4 73 | - name: Install deps 74 | run: | 75 | source util/ci/script.sh 76 | install_linux_deps 77 | 78 | - name: Build 79 | run: | 80 | source util/ci/script.sh 81 | run_build 82 | 83 | - name: Test 84 | run: | 85 | ./util/ci/test.sh 86 | -------------------------------------------------------------------------------- /util/dumpnodes/init.lua: -------------------------------------------------------------------------------- 1 | local function get_tile(tiles, n) 2 | local tile = tiles[n] 3 | if type(tile) == 'table' then 4 | return tile.name or tile.image 5 | elseif type(tile) == 'string' then 6 | return tile 7 | end 8 | end 9 | 10 | local function strip_texture(tex) 11 | tex = (tex .. '^'):match('%(*(.-)%)*^') -- strip modifiers 12 | if tex:find("[combine", 1, true) then 13 | tex = tex:match('.-=([^:]-)') -- extract first texture 14 | elseif tex:find("[png", 1, true) then 15 | return nil -- can't 16 | end 17 | return tex 18 | end 19 | 20 | local function pairs_s(dict) 21 | local keys = {} 22 | for k in pairs(dict) do 23 | keys[#keys+1] = k 24 | end 25 | table.sort(keys) 26 | return ipairs(keys) 27 | end 28 | 29 | core.register_chatcommand("dumpnodes", { 30 | description = "Dump node and texture list for use with minetestmapper", 31 | func = function() 32 | local ntbl = {} 33 | for _, nn in pairs_s(minetest.registered_nodes) do 34 | local prefix, name = nn:match('(.-):(.*)') 35 | if prefix == nil or name == nil then 36 | print("ignored(1): " .. nn) 37 | else 38 | if ntbl[prefix] == nil then 39 | ntbl[prefix] = {} 40 | end 41 | ntbl[prefix][name] = true 42 | end 43 | end 44 | local out, err = io.open(core.get_worldpath() .. "/nodes.txt", 'wb') 45 | if not out then 46 | return true, err 47 | end 48 | local n = 0 49 | for _, prefix in pairs_s(ntbl) do 50 | out:write('# ' .. prefix .. '\n') 51 | for _, name in pairs_s(ntbl[prefix]) do 52 | local nn = prefix .. ":" .. name 53 | local nd = core.registered_nodes[nn] 54 | local tiles = nd.tiles or nd.tile_images 55 | if tiles == nil or nd.drawtype == 'airlike' then 56 | print("ignored(2): " .. nn) 57 | else 58 | local tex = get_tile(tiles, 1) 59 | tex = tex and strip_texture(tex) 60 | if not tex then 61 | print("ignored(3): " .. nn) 62 | else 63 | out:write(nn .. ' ' .. tex .. '\n') 64 | n = n + 1 65 | end 66 | end 67 | end 68 | out:write('\n') 69 | end 70 | out:close() 71 | return true, n .. " nodes dumped." 72 | end, 73 | }) 74 | -------------------------------------------------------------------------------- /src/db-sqlite3.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "db.h" 4 | #include 5 | #include 6 | 7 | class SQLite3Base { 8 | public: 9 | ~SQLite3Base(); 10 | 11 | protected: 12 | void openDatabase(const char *path, bool readonly = true); 13 | 14 | // check function result or throw error 15 | inline void check_result(int result, int good = SQLITE_OK) 16 | { 17 | if (result != good) 18 | throw std::runtime_error(sqlite3_errmsg(db)); 19 | } 20 | 21 | // prepare a statement 22 | inline int prepare(sqlite3_stmt *&stmt, const char *sql) 23 | { 24 | return sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); 25 | } 26 | 27 | // read string from statement 28 | static inline std::string read_str(sqlite3_stmt *stmt, int iCol) 29 | { 30 | auto *data = reinterpret_cast( 31 | sqlite3_column_text(stmt, iCol)); 32 | return std::string(data); 33 | } 34 | 35 | // read blob from statement 36 | static inline ustring read_blob(sqlite3_stmt *stmt, int iCol) 37 | { 38 | auto *data = reinterpret_cast( 39 | sqlite3_column_blob(stmt, iCol)); 40 | size_t size = sqlite3_column_bytes(stmt, iCol); 41 | return ustring(data, size); 42 | } 43 | 44 | sqlite3 *db = NULL; 45 | }; 46 | 47 | class DBSQLite3 : public DB, SQLite3Base { 48 | public: 49 | DBSQLite3(const std::string &mapdir); 50 | std::vector getBlockPosXZ(BlockPos min, BlockPos max) override; 51 | void getBlocksOnXZ(BlockList &blocks, int16_t x, int16_t z, 52 | int16_t min_y, int16_t max_y) override; 53 | void getBlocksByPos(BlockList &blocks, 54 | const std::vector &positions) override; 55 | ~DBSQLite3() override; 56 | 57 | bool preferRangeQueries() const override { return newFormat; } 58 | 59 | private: 60 | static inline void getPosRange(int64_t &min, int64_t &max, int16_t zPos, 61 | int16_t zPos2); 62 | void loadBlockCache(int16_t zPos); 63 | 64 | // bind pos to statement. returns index of next column. 65 | inline int bind_pos(sqlite3_stmt *stmt, int iCol, BlockPos pos) 66 | { 67 | if (newFormat) { 68 | sqlite3_bind_int(stmt, iCol, pos.x); 69 | sqlite3_bind_int(stmt, iCol + 1, pos.y); 70 | sqlite3_bind_int(stmt, iCol + 2, pos.z); 71 | return iCol + 3; 72 | } else { 73 | sqlite3_bind_int64(stmt, iCol, encodeBlockPos(pos)); 74 | return iCol + 1; 75 | } 76 | } 77 | 78 | sqlite3_stmt *stmt_get_block_pos = NULL; 79 | sqlite3_stmt *stmt_get_block_pos_range = NULL; 80 | sqlite3_stmt *stmt_get_blocks_z = NULL; 81 | sqlite3_stmt *stmt_get_blocks_xz_range = NULL; 82 | sqlite3_stmt *stmt_get_block_exact = NULL; 83 | 84 | bool newFormat = false; 85 | int16_t blockCachedZ = -10000; 86 | std::unordered_map blockCache; // indexed by X 87 | }; 88 | -------------------------------------------------------------------------------- /.github/workflows/docker_image.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: docker_image 3 | 4 | # https://docs.github.com/en/actions/publishing-packages/publishing-docker-images 5 | # https://docs.docker.com/build/ci/github-actions/multi-platform 6 | # https://github.com/opencontainers/image-spec/blob/main/annotations.md 7 | 8 | on: 9 | push: 10 | branches: [ "master" ] 11 | # Publish semver tags as releases. 12 | tags: [ "*" ] 13 | pull_request: 14 | # Build docker image on pull requests. (but do not publish) 15 | paths: 16 | - '**/**.[ch]' 17 | - '**/**.cpp' 18 | workflow_dispatch: 19 | inputs: 20 | use_cache: 21 | description: "Use build cache" 22 | required: true 23 | type: boolean 24 | default: true 25 | 26 | env: 27 | REGISTRY: ghcr.io 28 | # github.repository as / 29 | IMAGE_NAME: ${{ github.repository }} 30 | 31 | jobs: 32 | publish: 33 | runs-on: ubuntu-latest 34 | 35 | permissions: 36 | contents: read 37 | packages: write 38 | 39 | steps: 40 | - name: Check out repository 41 | uses: actions/checkout@v4 42 | 43 | - name: Setup Docker buildx 44 | uses: docker/setup-buildx-action@v3.0.0 45 | 46 | # Login against the Docker registry except on PR 47 | # https://github.com/docker/login-action 48 | - name: Log into registry ${{ env.REGISTRY }} 49 | if: github.event_name != 'pull_request' 50 | uses: docker/login-action@v3.0.0 51 | with: 52 | registry: ${{ env.REGISTRY }} 53 | username: ${{ github.actor }} 54 | password: ${{ secrets.GITHUB_TOKEN }} 55 | 56 | # Extract metadata (tags, labels) for Docker 57 | # https://github.com/docker/metadata-action 58 | - name: Extract Docker metadata 59 | id: meta 60 | uses: docker/metadata-action@v5.5.0 61 | with: 62 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 63 | labels: | 64 | org.opencontainers.image.title=Minetestmapper 65 | org.opencontainers.image.vendor=Luanti 66 | org.opencontainers.image.licenses=BSD 2-Clause 67 | 68 | # Build and push Docker image 69 | # https://github.com/docker/build-push-action 70 | # No arm support for now. Require cross-compilation support in Dockerfile to not use QEMU. 71 | - name: Build and push Docker image 72 | uses: docker/build-push-action@v5.1.0 73 | with: 74 | context: . 75 | platforms: linux/amd64 76 | push: ${{ github.event_name != 'pull_request' }} 77 | load: true 78 | tags: ${{ steps.meta.outputs.tags }} 79 | labels: ${{ steps.meta.outputs.labels }} 80 | cache-from: type=gha 81 | cache-to: type=gha,mode=max 82 | no-cache: ${{ (github.event_name == 'workflow_dispatch' && !inputs.use_cache) || startsWith(github.ref, 'refs/tags/') }} 83 | 84 | - name: Test Docker Image 85 | run: | 86 | docker run --rm $(cut -d, -f1 <<<"$DOCKER_METADATA_OUTPUT_TAGS") minetestmapper --help 87 | shell: bash 88 | -------------------------------------------------------------------------------- /src/db.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include "types.h" 8 | 9 | struct BlockPos { 10 | int16_t x, y, z; 11 | 12 | constexpr BlockPos() : x(0), y(0), z(0) {} 13 | explicit constexpr BlockPos(int16_t v) : x(v), y(v), z(v) {} 14 | constexpr BlockPos(int16_t x, int16_t y, int16_t z) : x(x), y(y), z(z) {} 15 | 16 | // Implements the inverse ordering so that (2,2,2) < (1,1,1) 17 | inline bool operator<(const BlockPos &p) const 18 | { 19 | if (z > p.z) 20 | return true; 21 | if (z < p.z) 22 | return false; 23 | if (y > p.y) 24 | return true; 25 | if (y < p.y) 26 | return false; 27 | return x > p.x; 28 | } 29 | }; 30 | 31 | 32 | typedef std::pair Block; 33 | typedef std::list BlockList; 34 | 35 | 36 | class DB { 37 | protected: 38 | // Helpers that implement the hashed positions used by most backends 39 | static inline int64_t encodeBlockPos(const BlockPos pos); 40 | static inline BlockPos decodeBlockPos(int64_t hash); 41 | 42 | public: 43 | /* Return all unique (X, Z) position pairs inside area given by min and max, 44 | * so that min.x <= x < max.x && min.z <= z < max.z 45 | * Note: duplicates are allowed, but results in wasted time. 46 | */ 47 | virtual std::vector getBlockPosXZ(BlockPos min, BlockPos max) = 0; 48 | 49 | /* Read all blocks in column given by x and z 50 | * and inside the given Y range (min_y <= y < max_y) into list 51 | */ 52 | virtual void getBlocksOnXZ(BlockList &blocks, int16_t x, int16_t z, 53 | int16_t min_y, int16_t max_y) = 0; 54 | 55 | /* Read blocks at given positions into list 56 | */ 57 | virtual void getBlocksByPos(BlockList &blocks, 58 | const std::vector &positions) = 0; 59 | 60 | /* Can this database efficiently do range queries? 61 | * (for large data sets, more efficient that brute force) 62 | */ 63 | virtual bool preferRangeQueries() const = 0; 64 | 65 | virtual ~DB() {} 66 | }; 67 | 68 | 69 | 70 | /**************** 71 | * Black magic! * 72 | **************** 73 | * The position hashing is seriously messed up, 74 | * and is a lot more complicated than it looks. 75 | */ 76 | 77 | static inline int16_t unsigned_to_signed(uint16_t i, uint16_t max_positive) 78 | { 79 | if (i < max_positive) { 80 | return i; 81 | } else { 82 | return i - (max_positive * 2); 83 | } 84 | } 85 | 86 | 87 | // Modulo of a negative number does not work consistently in C 88 | static inline int64_t pythonmodulo(int64_t i, int64_t mod) 89 | { 90 | if (i >= 0) { 91 | return i % mod; 92 | } 93 | return mod - ((-i) % mod); 94 | } 95 | 96 | 97 | inline int64_t DB::encodeBlockPos(const BlockPos pos) 98 | { 99 | return (uint64_t) pos.z * 0x1000000 + 100 | (uint64_t) pos.y * 0x1000 + 101 | (uint64_t) pos.x; 102 | } 103 | 104 | 105 | inline BlockPos DB::decodeBlockPos(int64_t hash) 106 | { 107 | BlockPos pos; 108 | pos.x = unsigned_to_signed(pythonmodulo(hash, 4096), 2048); 109 | hash = (hash - pos.x) / 4096; 110 | pos.y = unsigned_to_signed(pythonmodulo(hash, 4096), 2048); 111 | hash = (hash - pos.y) / 4096; 112 | pos.z = unsigned_to_signed(pythonmodulo(hash, 4096), 2048); 113 | return pos; 114 | } 115 | 116 | /******************* 117 | * End black magic * 118 | *******************/ 119 | 120 | -------------------------------------------------------------------------------- /src/Image.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "Image.h" 11 | 12 | #ifndef NDEBUG 13 | #define SIZECHECK(x, y) check_bounds((x), (y), m_width, m_height) 14 | #else 15 | #define SIZECHECK(x, y) do {} while(0) 16 | #endif 17 | 18 | // ARGB but with inverted alpha 19 | 20 | static inline int color2int(const Color &c) 21 | { 22 | u8 a = (255 - c.a) * gdAlphaMax / 255; 23 | return (a << 24) | (c.r << 16) | (c.g << 8) | c.b; 24 | } 25 | 26 | static inline Color int2color(int c) 27 | { 28 | Color c2; 29 | c2.b = c & 0xff; 30 | c2.g = (c >> 8) & 0xff; 31 | c2.r = (c >> 16) & 0xff; 32 | u8 a = (c >> 24) & 0xff; 33 | c2.a = 255 - (a*255 / gdAlphaMax); 34 | return c2; 35 | } 36 | 37 | #ifndef NDEBUG 38 | static inline void check_bounds(int x, int y, int width, int height) 39 | { 40 | if(x < 0 || x >= width) { 41 | std::ostringstream oss; 42 | oss << "Access outside image bounds (x), 0 < " 43 | << x << " < " << width << " is false."; 44 | throw std::out_of_range(oss.str()); 45 | } 46 | if(y < 0 || y >= height) { 47 | std::ostringstream oss; 48 | oss << "Access outside image bounds (y), 0 < " 49 | << y << " < " << height << " is false."; 50 | throw std::out_of_range(oss.str()); 51 | } 52 | } 53 | #endif 54 | 55 | 56 | Image::Image(int width, int height) : 57 | m_width(width), m_height(height), m_image(nullptr) 58 | { 59 | SIZECHECK(0, 0); 60 | m_image = gdImageCreateTrueColor(m_width, m_height); 61 | } 62 | 63 | Image::~Image() 64 | { 65 | gdImageDestroy(m_image); 66 | } 67 | 68 | void Image::setPixel(int x, int y, const Color &c) 69 | { 70 | SIZECHECK(x, y); 71 | m_image->tpixels[y][x] = color2int(c); 72 | } 73 | 74 | Color Image::getPixel(int x, int y) 75 | { 76 | SIZECHECK(x, y); 77 | return int2color(m_image->tpixels[y][x]); 78 | } 79 | 80 | void Image::drawLine(int x1, int y1, int x2, int y2, const Color &c) 81 | { 82 | SIZECHECK(x1, y1); 83 | SIZECHECK(x2, y2); 84 | gdImageLine(m_image, x1, y1, x2, y2, color2int(c)); 85 | } 86 | 87 | void Image::drawText(int x, int y, const std::string &s, const Color &c) 88 | { 89 | SIZECHECK(x, y); 90 | gdImageString(m_image, gdFontGetMediumBold(), x, y, (unsigned char*) s.c_str(), color2int(c)); 91 | } 92 | 93 | void Image::drawFilledRect(int x, int y, int w, int h, const Color &c) 94 | { 95 | SIZECHECK(x, y); 96 | SIZECHECK(x + w - 1, y + h - 1); 97 | gdImageFilledRectangle(m_image, x, y, x + w - 1, y + h - 1, color2int(c)); 98 | } 99 | 100 | void Image::drawCircle(int x, int y, int diameter, const Color &c) 101 | { 102 | SIZECHECK(x, y); 103 | gdImageArc(m_image, x, y, diameter, diameter, 0, 360, color2int(c)); 104 | } 105 | 106 | void Image::save(const std::string &filename) 107 | { 108 | #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 109 | const char *f = filename.c_str(); 110 | if (gdSupportsFileType(f, 1) == GD_FALSE) 111 | throw std::runtime_error("Image format not supported by gd"); 112 | if (gdImageFile(m_image, f) == GD_FALSE) 113 | throw std::runtime_error("Error saving image"); 114 | #else 115 | if (filename.compare(filename.length() - 4, 4, ".png") != 0) 116 | throw std::runtime_error("Only PNG is supported"); 117 | FILE *f = fopen(filename.c_str(), "wb"); 118 | if (!f) { 119 | std::ostringstream oss; 120 | oss << "Error opening image file: " << std::strerror(errno); 121 | throw std::runtime_error(oss.str()); 122 | } 123 | gdImagePng(m_image, f); 124 | fclose(f); 125 | #endif 126 | } 127 | -------------------------------------------------------------------------------- /src/db-leveldb.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "db-leveldb.h" 5 | #include "types.h" 6 | 7 | static inline int64_t stoi64(const std::string &s) 8 | { 9 | std::istringstream tmp(s); 10 | int64_t t; 11 | tmp >> t; 12 | return t; 13 | } 14 | 15 | static inline std::string i64tos(int64_t i) 16 | { 17 | std::ostringstream os; 18 | os << i; 19 | return os.str(); 20 | } 21 | 22 | // finds the first position in the list where it.x >= x 23 | #define lower_bound_x(container, find_x) \ 24 | std::lower_bound((container).begin(), (container).end(), (find_x), \ 25 | [] (const vec2 &left, int16_t right) { \ 26 | return left.x < right; \ 27 | }) 28 | 29 | DBLevelDB::DBLevelDB(const std::string &mapdir) 30 | { 31 | leveldb::Options options; 32 | options.create_if_missing = false; 33 | leveldb::Status status = leveldb::DB::Open(options, mapdir + "map.db", &db); 34 | if (!status.ok()) { 35 | throw std::runtime_error(std::string("Failed to open database: ") + status.ToString()); 36 | } 37 | 38 | /* LevelDB is a dumb key-value store, so the only optimization we can do 39 | * is to cache the block positions that exist in the db. 40 | */ 41 | loadPosCache(); 42 | } 43 | 44 | 45 | DBLevelDB::~DBLevelDB() 46 | { 47 | delete db; 48 | } 49 | 50 | 51 | std::vector DBLevelDB::getBlockPosXZ(BlockPos min, BlockPos max) 52 | { 53 | std::vector res; 54 | for (const auto &it : posCache) { 55 | const int16_t zpos = it.first; 56 | if (zpos < min.z || zpos >= max.z) 57 | continue; 58 | auto it2 = lower_bound_x(it.second, min.x); 59 | for (; it2 != it.second.end(); it2++) { 60 | const auto &pos2 = *it2; 61 | if (pos2.x >= max.x) 62 | break; // went past 63 | if (pos2.y < min.y || pos2.y >= max.y) 64 | continue; 65 | // skip duplicates 66 | if (!res.empty() && res.back().x == pos2.x && res.back().z == zpos) 67 | continue; 68 | res.emplace_back(pos2.x, pos2.y, zpos); 69 | } 70 | } 71 | return res; 72 | } 73 | 74 | 75 | void DBLevelDB::loadPosCache() 76 | { 77 | leveldb::Iterator *it = db->NewIterator(leveldb::ReadOptions()); 78 | for (it->SeekToFirst(); it->Valid(); it->Next()) { 79 | int64_t posHash = stoi64(it->key().ToString()); 80 | BlockPos pos = decodeBlockPos(posHash); 81 | 82 | posCache[pos.z].emplace_back(pos.x, pos.y); 83 | } 84 | delete it; 85 | 86 | for (auto &it : posCache) 87 | std::sort(it.second.begin(), it.second.end()); 88 | } 89 | 90 | 91 | void DBLevelDB::getBlocksOnXZ(BlockList &blocks, int16_t x, int16_t z, 92 | int16_t min_y, int16_t max_y) 93 | { 94 | std::string datastr; 95 | leveldb::Status status; 96 | 97 | auto it = posCache.find(z); 98 | if (it == posCache.cend()) 99 | return; 100 | auto it2 = lower_bound_x(it->second, x); 101 | if (it2 == it->second.end() || it2->x != x) 102 | return; 103 | // it2 is now pointing to a contigous part where it2->x == x 104 | for (; it2 != it->second.end(); it2++) { 105 | const auto &pos2 = *it2; 106 | if (pos2.x != x) 107 | break; // went past 108 | if (pos2.y < min_y || pos2.y >= max_y) 109 | continue; 110 | 111 | BlockPos pos(x, pos2.y, z); 112 | status = db->Get(leveldb::ReadOptions(), i64tos(encodeBlockPos(pos)), &datastr); 113 | if (status.ok()) { 114 | blocks.emplace_back( 115 | pos, ustring((unsigned char *) datastr.data(), datastr.size()) 116 | ); 117 | } 118 | } 119 | } 120 | 121 | void DBLevelDB::getBlocksByPos(BlockList &blocks, 122 | const std::vector &positions) 123 | { 124 | std::string datastr; 125 | leveldb::Status status; 126 | 127 | for (auto pos : positions) { 128 | status = db->Get(leveldb::ReadOptions(), i64tos(encodeBlockPos(pos)), &datastr); 129 | if (status.ok()) { 130 | blocks.emplace_back( 131 | pos, ustring((unsigned char *) datastr.data(), datastr.size()) 132 | ); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /minetestmapper.6: -------------------------------------------------------------------------------- 1 | .TH MINETESTMAPPER 6 2 | .SH NAME 3 | minetestmapper \- generate an overview image of a Luanti map 4 | .SH SYNOPSIS 5 | .B minetestmapper 6 | \fB\-i\fR \fIworld_path\fR 7 | \fB\-o\fR \fIoutput_image\fR 8 | .PP 9 | See additional optional parameters below. 10 | .SH DESCRIPTION 11 | .B minetestmapper 12 | generates a top-down overview image of a Luanti map. 13 | This is a port of the obsolete minetestmapper.py script to C++, 14 | that is both faster and provides more features. 15 | 16 | Minetestmapper ships with a colors.txt file suitable for Minetest Game, 17 | if you use a different game or have mods installed you should generate a 18 | matching colors.txt for better results (colors will be missing otherwise). 19 | 20 | .SH MANDATORY PARAMETERS 21 | .TP 22 | .BR \-i " " \fIworld_path\fR 23 | Input world path 24 | .TP 25 | .BR \-o " " \fIoutput_image\fR 26 | Path to output image 27 | 28 | .SH OPTIONAL PARAMETERS 29 | .TP 30 | .BR \-\-bgcolor " " \fIcolor\fR 31 | Background color of image, e.g. "--bgcolor #ffffff" 32 | 33 | .TP 34 | .BR \-\-scalecolor " " \fIcolor\fR 35 | Color of scale marks and text, e.g. "--scalecolor #000000" 36 | 37 | .TP 38 | .BR \-\-playercolor " " \fIcolor\fR 39 | Color of player indicators, e.g. "--playercolor #ff0000" 40 | 41 | .TP 42 | .BR \-\-origincolor " " \fIcolor\fR 43 | Color of origin indicator, e.g. "--origincolor #ff0000" 44 | 45 | .TP 46 | .BR \-\-drawscale 47 | Draw scale(s) with tick marks and numbers 48 | 49 | .TP 50 | .BR \-\-drawplayers 51 | Draw player indicators with name 52 | 53 | .TP 54 | .BR \-\-draworigin 55 | Draw origin indicator 56 | 57 | .TP 58 | .BR \-\-drawalpha 59 | Allow nodes to be drawn with transparency (such as water) 60 | 61 | .TP 62 | .BR \-\-noshading 63 | Don't draw shading on nodes 64 | 65 | .TP 66 | .BR \-\-noemptyimage 67 | Don't output anything when the image would be empty 68 | 69 | .TP 70 | .BR \-\-verbose 71 | Enable verbose log output. 72 | 73 | .TP 74 | .BR \-\-min-y " " \fInumber\fR 75 | Don't draw nodes below this Y value, e.g. "--min-y -25" 76 | 77 | .TP 78 | .BR \-\-max-y " " \fInumber\fR 79 | Don't draw nodes above this Y value, e.g. "--max-y 75" 80 | 81 | .TP 82 | .BR \-\-backend " " \fIbackend\fR 83 | Override auto-detected map backend; supported: \fIsqlite3\fP, \fIleveldb\fP, \fIredis\fP, \fIpostgresql\fP, e.g. "--backend leveldb" 84 | 85 | .TP 86 | .BR \-\-geometry " " \fIgeometry\fR 87 | 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" 88 | 89 | The coordinates are specified with the same axes as in-game. The Z axis becomes Y when projected on the image. 90 | 91 | .TP 92 | .BR \-\-extent 93 | Don't render the image, just print the extent of the map that would be generated, in the same format as the geometry above. 94 | 95 | .TP 96 | .BR \-\-zoom " " \fIfactor\fR 97 | Zoom the image by using more than one pixel per node, e.g. "--zoom 4" 98 | 99 | .TP 100 | .BR \-\-colors " " \fIpath\fR 101 | Override auto-detected path to colors.txt, e.g. "--colors ../world/mycolors.txt" 102 | 103 | .TP 104 | .BR \-\-scales " " \fIedges\fR 105 | Draw scales on specified image edges (letters \fIt b l r\fP meaning top, bottom, left and right), e.g. "--scales tbr" 106 | 107 | .TP 108 | .BR \-\-exhaustive " " \fImode\fR 109 | Select if database should be traversed exhaustively or using range queries, available: \fInever\fP, \fIy\fP, \fIfull\fP, \fIauto\fP 110 | 111 | Defaults to \fIauto\fP. You shouldn't need to change this, as minetestmapper tries to automatically picks the best option. 112 | 113 | .TP 114 | .BR \-\-dumpblock " " \fIpos\fR 115 | Instead of rendering anything try to load the block at the given position (\fIx,y,z\fR) and print its raw data as hexadecimal. 116 | 117 | .SH MORE INFORMATION 118 | Website: https://github.com/luanti-org/minetestmapper 119 | 120 | .SH MAN PAGE AUTHOR 121 | Daniel Moerner 122 | -------------------------------------------------------------------------------- /util/ci/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eo pipefail 3 | mapdir=./testmap 4 | 5 | msg () { 6 | echo 7 | echo "==== $1" 8 | echo 9 | } 10 | 11 | # encodes a block position by X, Y, Z (positive numbers only!) 12 | encodepos () { 13 | echo "$(($1 + 0x1000 * $2 + 0x1000000 * $3))" 14 | } 15 | 16 | # create map file using SQL statements 17 | writemap () { 18 | rm -rf $mapdir 19 | mkdir $mapdir 20 | echo "backend = sqlite3" >$mapdir/world.mt 21 | echo "default:stone 10 10 10" >$mapdir/colors.txt 22 | printf '%s\n' \ 23 | "CREATE TABLE d(d BLOB);" \ 24 | "INSERT INTO d VALUES (x'$(cat util/ci/test_block)');" \ 25 | "$1" \ 26 | "DROP TABLE d;" | sqlite3 $mapdir/map.sqlite 27 | } 28 | 29 | # check that a non-empty ($1=1) or empty map ($1=0) was written with the args ($2 ...) 30 | checkmap () { 31 | local c=$1 32 | shift 33 | rm -f map.png 34 | ./minetestmapper --noemptyimage -v -i ./testmap -o map.png "$@" 35 | if [[ $c -eq 1 && ! -f map.png ]]; then 36 | echo "Output not generated!" 37 | exit 1 38 | elif [[ $c -eq 0 && -f map.png ]]; then 39 | echo "Output was generated, none expected!" 40 | exit 1 41 | fi 42 | echo "Passed." 43 | } 44 | 45 | # check that invocation returned an error 46 | checkerr () { 47 | local r=0 48 | ./minetestmapper --noemptyimage -v -i ./testmap -o map.png "$@" || r=1 49 | if [ $r -eq 0 ]; then 50 | echo "Did not return error!" 51 | exit 1 52 | fi 53 | echo "Passed." 54 | } 55 | 56 | # this is missing the indices and primary keys but that doesn't matter 57 | schema_old="CREATE TABLE blocks(pos INT, data BLOB);" 58 | schema_new="CREATE TABLE blocks(x INT, y INT, z INT, data BLOB);" 59 | 60 | msg "old schema" 61 | writemap " 62 | $schema_old 63 | INSERT INTO blocks SELECT $(encodepos 0 1 0), d FROM d; 64 | " 65 | checkmap 1 66 | 67 | msg "old schema: Y limit" 68 | # Note: test data contains a plane at y = 17 an a single node at y = 18 69 | checkmap 1 --max-y 17 70 | checkmap 0 --max-y 16 71 | checkmap 1 --min-y 18 72 | checkmap 0 --min-y 19 73 | 74 | # do this for every strategy 75 | for exh in never y full; do 76 | msg "old schema: all limits ($exh)" 77 | # fill the map with more blocks and then request just a single one to be rendered 78 | # this will run through internal consistency asserts. 79 | writemap " 80 | $schema_old 81 | INSERT INTO blocks SELECT $(encodepos 2 2 2), d FROM d; 82 | INSERT INTO blocks SELECT $(encodepos 1 2 2), d FROM d; 83 | INSERT INTO blocks SELECT $(encodepos 2 1 2), d FROM d; 84 | INSERT INTO blocks SELECT $(encodepos 2 2 1), d FROM d; 85 | INSERT INTO blocks SELECT $(encodepos 3 2 2), d FROM d; 86 | INSERT INTO blocks SELECT $(encodepos 2 3 2), d FROM d; 87 | INSERT INTO blocks SELECT $(encodepos 2 2 3), d FROM d; 88 | " 89 | checkmap 1 --geometry 32:32+16+16 --min-y 32 --max-y $((32+16-1)) --exhaustive $exh 90 | done 91 | 92 | msg "new schema" 93 | writemap " 94 | $schema_new 95 | INSERT INTO blocks SELECT 0, 1, 0, d FROM d; 96 | " 97 | checkmap 1 98 | 99 | # same as above 100 | for exh in never y full; do 101 | msg "new schema: all limits ($exh)" 102 | writemap " 103 | $schema_new 104 | INSERT INTO blocks SELECT 2, 2, 2, d FROM d; 105 | INSERT INTO blocks SELECT 1, 2, 2, d FROM d; 106 | INSERT INTO blocks SELECT 2, 1, 2, d FROM d; 107 | INSERT INTO blocks SELECT 2, 2, 1, d FROM d; 108 | INSERT INTO blocks SELECT 3, 2, 2, d FROM d; 109 | INSERT INTO blocks SELECT 2, 3, 2, d FROM d; 110 | INSERT INTO blocks SELECT 2, 2, 3, d FROM d; 111 | " 112 | checkmap 1 --geometry 32:32+16+16 --min-y 32 --max-y $((32+16-1)) --exhaustive $exh 113 | done 114 | 115 | msg "new schema: empty map" 116 | writemap "$schema_new" 117 | checkmap 0 118 | 119 | msg "drawplayers" 120 | writemap " 121 | $schema_new 122 | INSERT INTO blocks SELECT 0, 0, 0, d FROM d; 123 | " 124 | mkdir $mapdir/players 125 | printf '%s\n' "name = cat" "position = (80,0,80)" >$mapdir/players/cat 126 | # we can't check that it actually worked, however 127 | checkmap 1 --drawplayers --zoom 4 128 | 129 | msg "block error (wrong version)" 130 | writemap " 131 | $schema_new 132 | INSERT INTO blocks VALUES (0, 0, 0, x'150000'); 133 | " 134 | checkerr 135 | 136 | msg "block error (invalid zstd)" 137 | writemap " 138 | $schema_new 139 | INSERT INTO blocks VALUES (0, 0, 0, x'1d28b52ffd2001090000'); 140 | " 141 | checkerr 142 | -------------------------------------------------------------------------------- /src/PlayerAttributes.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include // usleep 6 | 7 | #include "config.h" 8 | #include "PlayerAttributes.h" 9 | #include "util.h" 10 | #include "log.h" 11 | #include "db-sqlite3.h" // SQLite3Base 12 | 13 | namespace { 14 | bool parse_pos(std::string position, Player &dst) 15 | { 16 | if (position.empty()) 17 | return false; 18 | if (position.front() == '(' && position.back() == ')') 19 | position = position.substr(1, position.size() - 2); 20 | std::istringstream iss(position); 21 | if (!(iss >> dst.x)) 22 | return false; 23 | if (iss.get() != ',') 24 | return false; 25 | if (!(iss >> dst.y)) 26 | return false; 27 | if (iss.get() != ',') 28 | return false; 29 | if (!(iss >> dst.z)) 30 | return false; 31 | return iss.eof(); 32 | } 33 | 34 | // Helper classes per backend 35 | 36 | class FilesReader { 37 | std::string path; 38 | DIR *dir = nullptr; 39 | public: 40 | FilesReader(const std::string &path) : path(path) { 41 | dir = opendir(path.c_str()); 42 | } 43 | ~FilesReader() { 44 | if (dir) 45 | closedir(dir); 46 | } 47 | 48 | void read(PlayerAttributes::Players &dest); 49 | }; 50 | 51 | class SQLiteReader : SQLite3Base { 52 | sqlite3_stmt *stmt_get_player_pos = NULL; 53 | public: 54 | SQLiteReader(const std::string &database) { 55 | openDatabase(database.c_str()); 56 | } 57 | ~SQLiteReader() { 58 | sqlite3_finalize(stmt_get_player_pos); 59 | } 60 | 61 | void read(PlayerAttributes::Players &dest); 62 | }; 63 | } 64 | 65 | void FilesReader::read(PlayerAttributes::Players &dest) 66 | { 67 | if (!dir) 68 | return; 69 | 70 | struct dirent *ent; 71 | std::string name, position; 72 | while ((ent = readdir(dir)) != NULL) { 73 | if (ent->d_name[0] == '.') 74 | continue; 75 | 76 | std::ifstream in(path + PATH_SEPARATOR + ent->d_name); 77 | if (!in.good()) 78 | continue; 79 | 80 | name = read_setting("name", in); 81 | position = read_setting("position", in); 82 | 83 | Player player; 84 | player.name = name; 85 | if (!parse_pos(position, player)) { 86 | errorstream << "Failed to parse position '" << position << "' in " 87 | << ent->d_name << std::endl; 88 | continue; 89 | } 90 | 91 | player.x /= 10.0f; 92 | player.y /= 10.0f; 93 | player.z /= 10.0f; 94 | 95 | dest.push_back(std::move(player)); 96 | } 97 | } 98 | 99 | #define SQLRES(r, good) check_result(r, good) 100 | #define SQLOK(r) SQLRES(r, SQLITE_OK) 101 | 102 | void SQLiteReader::read(PlayerAttributes::Players &dest) 103 | { 104 | SQLOK(prepare(stmt_get_player_pos, 105 | "SELECT name, posX, posY, posZ FROM player")); 106 | 107 | int result; 108 | while ((result = sqlite3_step(stmt_get_player_pos)) != SQLITE_DONE) { 109 | if (result == SQLITE_BUSY) { // Wait some time and try again 110 | usleep(10000); 111 | } else if (result != SQLITE_ROW) { 112 | throw std::runtime_error(sqlite3_errmsg(db)); 113 | } 114 | 115 | Player player; 116 | player.name = read_str(stmt_get_player_pos, 0); 117 | player.x = sqlite3_column_double(stmt_get_player_pos, 1); 118 | player.y = sqlite3_column_double(stmt_get_player_pos, 2); 119 | player.z = sqlite3_column_double(stmt_get_player_pos, 3); 120 | 121 | player.x /= 10.0f; 122 | player.y /= 10.0f; 123 | player.z /= 10.0f; 124 | 125 | dest.push_back(std::move(player)); 126 | } 127 | } 128 | 129 | /**********/ 130 | 131 | PlayerAttributes::PlayerAttributes(const std::string &worldDir) 132 | { 133 | std::ifstream ifs(worldDir + "world.mt"); 134 | if (!ifs.good()) 135 | throw std::runtime_error("Failed to read world.mt"); 136 | std::string backend = read_setting_default("player_backend", ifs, "files"); 137 | ifs.close(); 138 | 139 | verbosestream << "Player backend: " << backend << std::endl; 140 | if (backend == "files") 141 | FilesReader(worldDir + "players").read(m_players); 142 | else if (backend == "sqlite3") 143 | SQLiteReader(worldDir + "players.sqlite").read(m_players); 144 | else 145 | throw std::runtime_error(std::string("Unknown player backend: ") + backend); 146 | 147 | verbosestream << "Loaded " << m_players.size() << " players" << std::endl; 148 | } 149 | 150 | PlayerAttributes::Players::const_iterator PlayerAttributes::begin() const 151 | { 152 | return m_players.cbegin(); 153 | } 154 | 155 | PlayerAttributes::Players::const_iterator PlayerAttributes::end() const 156 | { 157 | return m_players.cend(); 158 | } 159 | 160 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Minetest Mapper C++ 2 | =================== 3 | 4 | .. image:: https://github.com/minetest/minetestmapper/workflows/build/badge.svg 5 | :target: https://github.com/minetest/minetestmapper/actions/workflows/build.yml 6 | 7 | Minetestmapper generates a top-down overview image from a Luanti map. 8 | 9 | A port of minetestmapper.py to C++ from `the obsolete Python script 10 | `_. 11 | This version is both faster and provides more features. 12 | 13 | Minetestmapper ships with a colors.txt file suitable for Minetest Game, 14 | if you use a different game or have mods installed you should generate a 15 | matching colors.txt for better results (colors will be missing otherwise). 16 | The `generate_colorstxt.py script 17 | <./util/generate_colorstxt.py>`_ in the util folder exists for this purpose, 18 | detailed instructions can be found within. 19 | 20 | Requirements 21 | ------------ 22 | 23 | * C++ compiler, zlib, zstd 24 | * libgd 25 | * sqlite3 26 | * LevelDB (optional) 27 | * hiredis (optional) 28 | * Postgres libraries (optional) 29 | 30 | on Debian/Ubuntu: 31 | ^^^^^^^^^^^^^^^^^ 32 | 33 | ``sudo apt install cmake libgd-dev libhiredis-dev libleveldb-dev libpq-dev libsqlite3-dev zlib1g-dev libzstd-dev`` 34 | 35 | on openSUSE: 36 | ^^^^^^^^^^^^ 37 | 38 | ``sudo zypper install gd-devel hiredis-devel leveldb-devel postgresql-devel sqlite3-devel zlib-devel libzstd-devel`` 39 | 40 | for Windows: 41 | ^^^^^^^^^^^^ 42 | Minetestmapper for Windows can be downloaded `from the Releases section 43 | `_. 44 | 45 | After extracting the archive, it can be invoked from cmd.exe or PowerShell: 46 | 47 | .. code-block:: dos 48 | 49 | cd C:\Users\yourname\Desktop\example\path 50 | minetestmapper.exe --help 51 | 52 | Compilation 53 | ----------- 54 | 55 | .. code-block:: bash 56 | 57 | cmake . -DENABLE_LEVELDB=1 58 | make -j$(nproc) 59 | 60 | Usage 61 | ----- 62 | 63 | ``minetestmapper`` has two mandatory paremeters, ``-i`` (input world path) 64 | and ``-o`` (output image path). 65 | 66 | :: 67 | 68 | ./minetestmapper -i ~/.minetest/worlds/my_world/ -o map.png 69 | 70 | 71 | Parameters 72 | ^^^^^^^^^^ 73 | 74 | bgcolor: 75 | Background color of image, e.g. ``--bgcolor '#ffffff'`` 76 | 77 | scalecolor: 78 | Color of scale marks and text, e.g. ``--scalecolor '#000000'`` 79 | 80 | playercolor: 81 | Color of player indicators, e.g. ``--playercolor '#ff0000'`` 82 | 83 | origincolor: 84 | Color of origin indicator, e.g. ``--origincolor '#ff0000'`` 85 | 86 | drawscale: 87 | Draw scale(s) with tick marks and numbers, ``--drawscale`` 88 | 89 | drawplayers: 90 | Draw player indicators with name, ``--drawplayers`` 91 | 92 | draworigin: 93 | Draw origin indicator, ``--draworigin`` 94 | 95 | drawalpha: 96 | Allow nodes to be drawn with transparency (such as water), ``--drawalpha`` 97 | 98 | extent: 99 | Don't output any imagery, just print the extent of the full map, ``--extent`` 100 | 101 | noshading: 102 | Don't draw shading on nodes, ``--noshading`` 103 | 104 | noemptyimage: 105 | Don't output anything when the image would be empty, ``--noemptyimage`` 106 | 107 | verbose: 108 | Enable verbose log putput, ``--verbose`` 109 | 110 | min-y: 111 | Don't draw nodes below this Y value, e.g. ``--min-y -25`` 112 | 113 | max-y: 114 | Don't draw nodes above this Y value, e.g. ``--max-y 75`` 115 | 116 | backend: 117 | Override auto-detected map backend; supported: *sqlite3*, *leveldb*, *redis*, *postgresql*, e.g. ``--backend leveldb`` 118 | 119 | geometry: 120 | 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`` 121 | 122 | The coordinates are specified with the same axes as in-game. The Z axis becomes Y when projected on the image. 123 | 124 | zoom: 125 | Zoom the image by using more than one pixel per node, e.g. ``--zoom 4`` 126 | 127 | colors: 128 | Override auto-detected path to colors.txt, e.g. ``--colors ../world/mycolors.txt`` 129 | 130 | scales: 131 | Draw scales on specified image edges (letters *t b l r* meaning top, bottom, left and right), e.g. ``--scales tbr`` 132 | 133 | exhaustive: 134 | Select if database should be traversed exhaustively or using range queries, available: *never*, *y*, *full*, *auto* 135 | 136 | Defaults to *auto*. You shouldn't need to change this, as minetestmapper tries to automatically picks the best option. 137 | 138 | dumpblock: 139 | Instead of rendering anything try to load the block at the given position (*x,y,z*) and print its raw data as hexadecimal. 140 | -------------------------------------------------------------------------------- /src/TileGenerator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "PixelAttributes.h" 11 | #include "Image.h" 12 | #include "db.h" 13 | #include "types.h" 14 | 15 | class BlockDecoder; 16 | class Image; 17 | 18 | enum { 19 | SCALE_TOP = (1 << 0), 20 | SCALE_BOTTOM = (1 << 1), 21 | SCALE_LEFT = (1 << 2), 22 | SCALE_RIGHT = (1 << 3), 23 | }; 24 | 25 | enum { 26 | EXH_NEVER, // Always use range queries 27 | EXH_Y, // Exhaustively search Y space, range queries for X/Z 28 | EXH_FULL, // Exhaustively search entire requested geometry 29 | EXH_AUTO, // Automatically pick one of the previous modes 30 | }; 31 | 32 | struct ColorEntry { 33 | ColorEntry() : r(0), g(0), b(0), a(0), t(0) {}; 34 | ColorEntry(uint8_t r, uint8_t g, uint8_t b, uint8_t a, uint8_t t) : 35 | r(r), g(g), b(b), a(a), t(t) {}; 36 | inline Color toColor() const { return Color(r, g, b, a); } 37 | uint8_t r, g, b, a; // Red, Green, Blue, Alpha 38 | uint8_t t; // "thickness" value 39 | }; 40 | 41 | struct BitmapThing { // 16x16 bitmap 42 | inline void reset() { 43 | for (int i = 0; i < 16; ++i) 44 | val[i] = 0; 45 | } 46 | inline bool any_neq(uint16_t v) const { 47 | for (int i = 0; i < 16; ++i) { 48 | if (val[i] != v) 49 | return true; 50 | } 51 | return false; 52 | } 53 | inline bool any() const { return any_neq(0); } 54 | inline bool full() const { return !any_neq(0xffff); } 55 | inline void set(unsigned int x, unsigned int z) { 56 | val[z] |= (1 << x); 57 | } 58 | inline bool get(unsigned int x, unsigned int z) const { 59 | return !!(val[z] & (1 << x)); 60 | } 61 | 62 | uint16_t val[16]; 63 | }; 64 | 65 | 66 | class TileGenerator 67 | { 68 | private: 69 | typedef std::unordered_map ColorMap; 70 | 71 | public: 72 | TileGenerator(); 73 | ~TileGenerator(); 74 | void setBgColor(const std::string &bgColor); 75 | void setScaleColor(const std::string &scaleColor); 76 | void setOriginColor(const std::string &originColor); 77 | void setPlayerColor(const std::string &playerColor); 78 | void setDrawOrigin(bool drawOrigin); 79 | void setDrawPlayers(bool drawPlayers); 80 | void setDrawScale(bool drawScale); 81 | void setDrawAlpha(bool drawAlpha); 82 | void setShading(bool shading); 83 | void setGeometry(int x, int y, int w, int h); 84 | void setMinY(int y); 85 | void setMaxY(int y); 86 | void setExhaustiveSearch(int mode); 87 | void parseColorsFile(const std::string &fileName); 88 | void setBackend(std::string backend); 89 | void setZoom(int zoom); 90 | void setScales(uint flags); 91 | void setDontWriteEmpty(bool f); 92 | 93 | void generate(const std::string &input, const std::string &output); 94 | void printGeometry(const std::string &input); 95 | void dumpBlock(const std::string &input, BlockPos pos); 96 | 97 | static std::set getSupportedBackends(); 98 | 99 | private: 100 | void parseColorsStream(std::istream &in); 101 | void openDb(const std::string &input); 102 | void closeDatabase(); 103 | void loadBlocks(); 104 | void createImage(); 105 | void renderMap(); 106 | void renderMapBlock(const BlockDecoder &blk, const BlockPos &pos); 107 | void renderMapBlockBottom(const BlockPos &pos); 108 | void renderShading(int zPos); 109 | void renderScale(); 110 | void renderOrigin(); 111 | void renderPlayers(const std::string &inputPath); 112 | void writeImage(const std::string &output); 113 | void printUnknown(); 114 | void reportProgress(size_t count); 115 | int getImageX(int val, bool absolute=false) const; 116 | int getImageY(int val, bool absolute=false) const; 117 | void setZoomed(int x, int y, Color color); 118 | 119 | private: 120 | Color m_bgColor; 121 | Color m_scaleColor; 122 | Color m_originColor; 123 | Color m_playerColor; 124 | bool m_drawOrigin; 125 | bool m_drawPlayers; 126 | bool m_drawScale; 127 | bool m_drawAlpha; 128 | bool m_shading; 129 | bool m_dontWriteEmpty; 130 | std::string m_backend; 131 | int m_xBorder, m_yBorder; 132 | 133 | DB *m_db; 134 | Image *m_image; 135 | PixelAttributes m_blockPixelAttributes; 136 | /* smallest/largest seen X or Z block coordinate */ 137 | int m_xMin; 138 | int m_xMax; 139 | int m_zMin; 140 | int m_zMax; 141 | /* Y limits for rendered area (node units) */ 142 | int m_yMin; 143 | int m_yMax; 144 | /* limits for rendered area (block units) */ 145 | int16_t m_geomX; 146 | int16_t m_geomY; /* Y in terms of rendered image, Z in the world */ 147 | int16_t m_geomX2; 148 | int16_t m_geomY2; 149 | 150 | int m_mapWidth; 151 | int m_mapHeight; 152 | int m_exhaustiveSearch; 153 | std::set m_unknownNodes; 154 | bool m_renderedAny; 155 | std::map> m_positions; /* indexed by Z, contains X coords */ 156 | ColorMap m_colorMap; 157 | BitmapThing m_readPixels; 158 | BitmapThing m_readInfo; 159 | Color m_color[16][16]; 160 | uint8_t m_thickness[16][16]; 161 | 162 | int m_zoom; 163 | uint m_scales; 164 | 165 | size_t m_progressMax; 166 | int m_progressLast; // percentage 167 | }; // class TileGenerator 168 | -------------------------------------------------------------------------------- /src/BlockDecoder.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "BlockDecoder.h" 5 | #include "ZlibDecompressor.h" 6 | #include "log.h" 7 | 8 | static inline uint16_t readU16(const unsigned char *data) 9 | { 10 | return data[0] << 8 | data[1]; 11 | } 12 | 13 | static inline uint16_t readBlockContent(const unsigned char *mapData, 14 | u8 contentWidth, unsigned int datapos) 15 | { 16 | if (contentWidth == 2) { 17 | size_t index = datapos << 1; 18 | return (mapData[index] << 8) | mapData[index + 1]; 19 | } else { 20 | u8 param = mapData[datapos]; 21 | if (param <= 0x7f) 22 | return param; 23 | else 24 | return (param << 4) | (mapData[datapos + 0x2000] >> 4); 25 | } 26 | } 27 | 28 | BlockDecoder::BlockDecoder() 29 | { 30 | reset(); 31 | } 32 | 33 | void BlockDecoder::reset() 34 | { 35 | m_blockAirId = -1; 36 | m_blockIgnoreId = -1; 37 | m_nameMap.clear(); 38 | 39 | m_version = 0; 40 | m_contentWidth = 0; 41 | m_mapData.clear(); 42 | } 43 | 44 | void BlockDecoder::decode(const ustring &datastr) 45 | { 46 | const unsigned char *data = datastr.c_str(); 47 | size_t length = datastr.length(); 48 | // TODO: Add strict bounds checks everywhere 49 | 50 | uint8_t version = data[0]; 51 | if (version < 22) { 52 | auto err = "Unsupported map version " + std::to_string(version); 53 | throw std::runtime_error(err); 54 | } 55 | m_version = version; 56 | 57 | if (version >= 29) { 58 | // decompress whole block at once 59 | m_zstd_decompressor.setData(data, length, 1); 60 | m_zstd_decompressor.decompress(m_scratch); 61 | data = m_scratch.c_str(); 62 | length = m_scratch.size(); 63 | } 64 | 65 | size_t dataOffset = 0; 66 | if (version >= 29) 67 | dataOffset = 7; 68 | else if (version >= 27) 69 | dataOffset = 4; 70 | else 71 | dataOffset = 2; 72 | 73 | auto decode_mapping = [&] () { 74 | dataOffset++; // mapping version 75 | uint16_t numMappings = readU16(data + dataOffset); 76 | dataOffset += 2; 77 | for (int i = 0; i < numMappings; ++i) { 78 | uint16_t nodeId = readU16(data + dataOffset); 79 | dataOffset += 2; 80 | uint16_t nameLen = readU16(data + dataOffset); 81 | dataOffset += 2; 82 | std::string name(reinterpret_cast(data) + dataOffset, nameLen); 83 | if (name == "air") 84 | m_blockAirId = nodeId; 85 | else if (name == "ignore") 86 | m_blockIgnoreId = nodeId; 87 | else 88 | m_nameMap[nodeId] = std::move(name); 89 | dataOffset += nameLen; 90 | } 91 | }; 92 | 93 | if (version >= 29) 94 | decode_mapping(); 95 | 96 | uint8_t contentWidth = data[dataOffset]; 97 | dataOffset++; 98 | uint8_t paramsWidth = data[dataOffset]; 99 | dataOffset++; 100 | if (contentWidth != 1 && contentWidth != 2) { 101 | auto err = "Unsupported map version contentWidth=" + std::to_string(contentWidth); 102 | throw std::runtime_error(err); 103 | } 104 | if (paramsWidth != 2) { 105 | auto err = "Unsupported map version paramsWidth=" + std::to_string(paramsWidth); 106 | throw std::runtime_error(err); 107 | } 108 | m_contentWidth = contentWidth; 109 | const size_t mapDataSize = (contentWidth + paramsWidth) * 4096; 110 | 111 | if (version >= 29) { 112 | if (length < dataOffset + mapDataSize) 113 | throw std::runtime_error("Map data buffer truncated"); 114 | m_mapData.assign(data + dataOffset, mapDataSize); 115 | return; // we have read everything we need and can return early 116 | } 117 | 118 | // version < 29 119 | ZlibDecompressor decompressor(data, length); 120 | decompressor.setSeekPos(dataOffset); 121 | decompressor.decompress(m_mapData); 122 | decompressor.decompress(m_scratch); // unused metadata 123 | dataOffset = decompressor.seekPos(); 124 | 125 | if (m_mapData.size() < mapDataSize) 126 | throw std::runtime_error("Map data buffer truncated"); 127 | 128 | // Skip unused node timers 129 | if (version == 23) 130 | dataOffset += 1; 131 | if (version == 24) { 132 | uint8_t ver = data[dataOffset++]; 133 | if (ver == 1) { 134 | uint16_t num = readU16(data + dataOffset); 135 | dataOffset += 2; 136 | dataOffset += 10 * num; 137 | } 138 | } 139 | 140 | // Skip unused static objects 141 | dataOffset++; // Skip static object version 142 | uint16_t staticObjectCount = readU16(data + dataOffset); 143 | dataOffset += 2; 144 | for (int i = 0; i < staticObjectCount; ++i) { 145 | dataOffset += 13; 146 | uint16_t dataSize = readU16(data + dataOffset); 147 | dataOffset += dataSize + 2; 148 | } 149 | dataOffset += 4; // Skip timestamp 150 | 151 | // Read mapping 152 | decode_mapping(); 153 | } 154 | 155 | bool BlockDecoder::isEmpty() const 156 | { 157 | // only contains ignore and air nodes? 158 | return m_nameMap.empty(); 159 | } 160 | 161 | const static std::string empty; 162 | 163 | const std::string &BlockDecoder::getNode(u8 x, u8 y, u8 z) const 164 | { 165 | unsigned int position = x + (y << 4) + (z << 8); 166 | uint16_t content = readBlockContent(m_mapData.c_str(), m_contentWidth, position); 167 | if (content == m_blockAirId || content == m_blockIgnoreId) 168 | return empty; 169 | NameMap::const_iterator it = m_nameMap.find(content); 170 | if (it == m_nameMap.end()) { 171 | errorstream << "Skipping node with invalid ID " << (int)content << std::endl; 172 | return empty; 173 | } 174 | return it->second; 175 | } 176 | -------------------------------------------------------------------------------- /src/db-redis.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "db-redis.h" 5 | #include "types.h" 6 | #include "util.h" 7 | 8 | #define DB_REDIS_HMGET_NUMFIELDS 30 9 | 10 | #define REPLY_TYPE_ERR(reply, desc) do { \ 11 | throw std::runtime_error(std::string("Unexpected type for " desc ": ") \ 12 | + replyTypeStr((reply)->type)); \ 13 | } while(0) 14 | 15 | static inline int64_t stoi64(const std::string &s) 16 | { 17 | std::stringstream tmp(s); 18 | int64_t t; 19 | tmp >> t; 20 | return t; 21 | } 22 | 23 | static inline std::string i64tos(int64_t i) 24 | { 25 | std::ostringstream os; 26 | os << i; 27 | return os.str(); 28 | } 29 | 30 | 31 | DBRedis::DBRedis(const std::string &mapdir) 32 | { 33 | std::ifstream ifs(mapdir + "world.mt"); 34 | if (!ifs.good()) 35 | throw std::runtime_error("Failed to read world.mt"); 36 | std::string tmp; 37 | 38 | tmp = read_setting("redis_address", ifs); 39 | ifs.seekg(0); 40 | hash = read_setting("redis_hash", ifs); 41 | ifs.seekg(0); 42 | 43 | if (tmp.find('/') != std::string::npos) { 44 | ctx = redisConnectUnix(tmp.c_str()); 45 | } else { 46 | int port = stoi64(read_setting_default("redis_port", ifs, "6379")); 47 | ctx = redisConnect(tmp.c_str(), port); 48 | } 49 | 50 | if (!ctx) { 51 | throw std::runtime_error("Cannot allocate redis context"); 52 | } else if (ctx->err) { 53 | std::string err = std::string("Connection error: ") + ctx->errstr; 54 | redisFree(ctx); 55 | throw std::runtime_error(err); 56 | } 57 | 58 | /* Redis is just a key-value store, so the only optimization we can do 59 | * is to cache the block positions that exist in the db. 60 | */ 61 | loadPosCache(); 62 | } 63 | 64 | 65 | DBRedis::~DBRedis() 66 | { 67 | redisFree(ctx); 68 | } 69 | 70 | 71 | std::vector DBRedis::getBlockPosXZ(BlockPos min, BlockPos max) 72 | { 73 | std::vector res; 74 | for (const auto &it : posCache) { 75 | if (it.first < min.z || it.first >= max.z) 76 | continue; 77 | for (auto pos2 : it.second) { 78 | if (pos2.first < min.x || pos2.first >= max.x) 79 | continue; 80 | if (pos2.second < min.y || pos2.second >= max.y) 81 | continue; 82 | res.emplace_back(pos2.first, pos2.second, it.first); 83 | } 84 | } 85 | return res; 86 | } 87 | 88 | 89 | const char *DBRedis::replyTypeStr(int type) 90 | { 91 | switch (type) { 92 | case REDIS_REPLY_STATUS: 93 | return "REDIS_REPLY_STATUS"; 94 | case REDIS_REPLY_ERROR: 95 | return "REDIS_REPLY_ERROR"; 96 | case REDIS_REPLY_INTEGER: 97 | return "REDIS_REPLY_INTEGER"; 98 | case REDIS_REPLY_NIL: 99 | return "REDIS_REPLY_NIL"; 100 | case REDIS_REPLY_STRING: 101 | return "REDIS_REPLY_STRING"; 102 | case REDIS_REPLY_ARRAY: 103 | return "REDIS_REPLY_ARRAY"; 104 | default: 105 | return "(unknown)"; 106 | } 107 | } 108 | 109 | 110 | void DBRedis::loadPosCache() 111 | { 112 | redisReply *reply; 113 | reply = (redisReply*) redisCommand(ctx, "HKEYS %s", hash.c_str()); 114 | if (!reply) 115 | throw std::runtime_error("Redis command HKEYS failed"); 116 | if (reply->type != REDIS_REPLY_ARRAY) 117 | REPLY_TYPE_ERR(reply, "HKEYS reply"); 118 | for (size_t i = 0; i < reply->elements; i++) { 119 | if (reply->element[i]->type != REDIS_REPLY_STRING) 120 | REPLY_TYPE_ERR(reply->element[i], "HKEYS subreply"); 121 | BlockPos pos = decodeBlockPos(stoi64(reply->element[i]->str)); 122 | posCache[pos.z].emplace_back(pos.x, pos.y); 123 | } 124 | 125 | freeReplyObject(reply); 126 | } 127 | 128 | 129 | void DBRedis::HMGET(const std::vector &positions, 130 | std::function result) 131 | { 132 | const char *argv[DB_REDIS_HMGET_NUMFIELDS + 2]; 133 | argv[0] = "HMGET"; 134 | argv[1] = hash.c_str(); 135 | 136 | auto position = positions.begin(); 137 | size_t remaining = positions.size(); 138 | size_t abs_i = 0; 139 | while (remaining > 0) { 140 | const size_t batch_size = mymin(DB_REDIS_HMGET_NUMFIELDS, remaining); 141 | 142 | redisReply *reply; 143 | { 144 | // storage to preserve validity of .c_str() 145 | std::string keys[batch_size]; 146 | for (size_t i = 0; i < batch_size; ++i) { 147 | keys[i] = i64tos(encodeBlockPos(*position++)); 148 | argv[i+2] = keys[i].c_str(); 149 | } 150 | reply = (redisReply*) redisCommandArgv(ctx, batch_size + 2, argv, NULL); 151 | } 152 | 153 | if (!reply) 154 | throw std::runtime_error("Redis command HMGET failed"); 155 | if (reply->type != REDIS_REPLY_ARRAY) 156 | REPLY_TYPE_ERR(reply, "HMGET reply"); 157 | if (reply->elements != batch_size) { 158 | freeReplyObject(reply); 159 | throw std::runtime_error("HMGET wrong number of elements"); 160 | } 161 | for (size_t i = 0; i < reply->elements; ++i) { 162 | redisReply *subreply = reply->element[i]; 163 | if (subreply->type == REDIS_REPLY_NIL) 164 | continue; 165 | else if (subreply->type != REDIS_REPLY_STRING) 166 | REPLY_TYPE_ERR(subreply, "HMGET subreply"); 167 | if (subreply->len == 0) 168 | throw std::runtime_error("HMGET empty string"); 169 | result(abs_i + i, ustring( 170 | reinterpret_cast(subreply->str), 171 | subreply->len 172 | )); 173 | } 174 | freeReplyObject(reply); 175 | 176 | abs_i += batch_size; 177 | remaining -= batch_size; 178 | } 179 | } 180 | 181 | 182 | void DBRedis::getBlocksOnXZ(BlockList &blocks, int16_t x, int16_t z, 183 | int16_t min_y, int16_t max_y) 184 | { 185 | auto it = posCache.find(z); 186 | if (it == posCache.cend()) 187 | return; 188 | 189 | std::vector positions; 190 | for (auto pos2 : it->second) { 191 | if (pos2.first == x && pos2.second >= min_y && pos2.second < max_y) 192 | positions.emplace_back(x, pos2.second, z); 193 | } 194 | 195 | getBlocksByPos(blocks, positions); 196 | } 197 | 198 | 199 | void DBRedis::getBlocksByPos(BlockList &blocks, 200 | const std::vector &positions) 201 | { 202 | auto result = [&] (std::size_t i, ustring data) { 203 | blocks.emplace_back(positions[i], std::move(data)); 204 | }; 205 | HMGET(positions, result); 206 | } 207 | -------------------------------------------------------------------------------- /util/generate_colorstxt.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | import os.path 4 | import getopt 5 | import re 6 | from math import sqrt 7 | try: 8 | from PIL import Image 9 | except: 10 | print("Could not load image routines, install PIL ('pillow' on pypi)!", file=sys.stderr) 11 | exit(1) 12 | 13 | ############ 14 | ############ 15 | # Instructions for generating a colors.txt file for custom games and/or mods: 16 | # 1) Add the dumpnodes mod to a Luanti world with the chosen game and mods enabled. 17 | # 2) Join ingame and run the /dumpnodes chat command. 18 | # 3) Run this script and poin it to the installation path of the game using -g, 19 | # the path(s) where mods are stored using -m and the nodes.txt in your world folder. 20 | # Example command line: 21 | # ./util/generate_colorstxt.py --game /usr/share/luanti/games/minetest_game \ 22 | # -m ~/.minetest/mods ~/.minetest/worlds/my_world/nodes.txt 23 | # 4) Copy the resulting colors.txt file to your world folder or to any other place 24 | # and use it with minetestmapper's --colors option. 25 | ########### 26 | ########### 27 | 28 | # minimal sed syntax, s|match|replace| and /match/d supported 29 | REPLACEMENTS = [ 30 | # Delete some nodes that are usually hidden 31 | r'/^fireflies:firefly /d', 32 | r'/^butterflies:butterfly_/d', 33 | # Nicer colors for water and lava 34 | r's/^(default:(river_)?water_(flowing|source)) [0-9 ]+$/\1 39 66 106 128 224/', 35 | r's/^(default:lava_(flowing|source)) [0-9 ]+$/\1 255 100 0/', 36 | # Transparency for glass nodes and panes 37 | r's/^(default:.*glass) ([0-9 ]+)$/\1 \2 64 16/', 38 | r's/^(doors:.*glass[^ ]*) ([0-9 ]+)$/\1 \2 64 16/', 39 | r's/^(xpanes:.*(pane|bar)[^ ]*) ([0-9 ]+)$/\1 \3 64 16/', 40 | ] 41 | 42 | def usage(): 43 | print("Usage: generate_colorstxt.py [options] [input file] [output file]") 44 | print("If not specified the input file defaults to ./nodes.txt and the output file to ./colors.txt") 45 | print(" -g / --game \t\tSet path to the game (for textures), required") 46 | print(" -m / --mods \t\tAdd search path for mod textures") 47 | print(" --replace \t\tLoad replacements from file (ADVANCED)") 48 | 49 | def collect_files(path): 50 | dirs = [] 51 | with os.scandir(path) as it: 52 | for entry in it: 53 | if entry.name[0] == '.': continue 54 | if entry.is_dir(): 55 | dirs.append(entry.path) 56 | continue 57 | if entry.is_file() and '.' in entry.name: 58 | if entry.name not in textures.keys(): 59 | textures[entry.name] = entry.path 60 | for path2 in dirs: 61 | collect_files(path2) 62 | 63 | def average_color(filename): 64 | inp = Image.open(filename).convert('RGBA') 65 | data = inp.load() 66 | 67 | c0, c1, c2 = [], [], [] 68 | for x in range(inp.size[0]): 69 | for y in range(inp.size[1]): 70 | px = data[x, y] 71 | if px[3] < 128: continue # alpha 72 | c0.append(px[0]**2) 73 | c1.append(px[1]**2) 74 | c2.append(px[2]**2) 75 | 76 | if len(c0) == 0: 77 | print(f"didn't find color for '{os.path.basename(filename)}'", file=sys.stderr) 78 | return "0 0 0" 79 | c0 = sqrt(sum(c0) / len(c0)) 80 | c1 = sqrt(sum(c1) / len(c1)) 81 | c2 = sqrt(sum(c2) / len(c2)) 82 | return "%d %d %d" % (c0, c1, c2) 83 | 84 | def apply_sed(line, exprs): 85 | for expr in exprs: 86 | if expr[0] == '/': 87 | if not expr.endswith("/d"): raise ValueError() 88 | if re.search(expr[1:-2], line): 89 | return '' 90 | elif expr[0] == 's': 91 | expr = expr.split(expr[1]) 92 | if len(expr) != 4 or expr[3] != '': raise ValueError() 93 | line = re.sub(expr[1], expr[2], line) 94 | else: 95 | raise ValueError() 96 | return line 97 | # 98 | 99 | try: 100 | opts, args = getopt.getopt(sys.argv[1:], "hg:m:", ["help", "game=", "mods=", "replace="]) 101 | except getopt.GetoptError as e: 102 | print(str(e)) 103 | exit(1) 104 | if ('-h', '') in opts or ('--help', '') in opts: 105 | usage() 106 | exit(0) 107 | 108 | input_file = "./nodes.txt" 109 | output_file = "./colors.txt" 110 | texturepaths = [] 111 | 112 | try: 113 | gamepath = next(o[1] for o in opts if o[0] in ('-g', '--game')) 114 | if not os.path.isdir(os.path.join(gamepath, "mods")): 115 | print(f"'{gamepath}' doesn't exist or does not contain a game.", file=sys.stderr) 116 | exit(1) 117 | texturepaths.append(os.path.join(gamepath, "mods")) 118 | except StopIteration: 119 | print("No game path set but one is required. (see --help)", file=sys.stderr) 120 | exit(1) 121 | 122 | try: 123 | tmp = next(o[1] for o in opts if o[0] == "--replace") 124 | REPLACEMENTS.clear() 125 | with open(tmp, 'r') as f: 126 | for line in f: 127 | if not line or line[0] == '#': continue 128 | REPLACEMENTS.append(line.strip()) 129 | except StopIteration: 130 | pass 131 | 132 | for o in opts: 133 | if o[0] not in ('-m', '--mods'): continue 134 | if not os.path.isdir(o[1]): 135 | print(f"Given path '{o[1]}' does not exist.'", file=sys.stderr) 136 | exit(1) 137 | texturepaths.append(o[1]) 138 | 139 | if len(args) > 2: 140 | print("Too many arguments.", file=sys.stderr) 141 | exit(1) 142 | if len(args) > 1: 143 | output_file = args[1] 144 | if len(args) > 0: 145 | input_file = args[0] 146 | 147 | if not os.path.exists(input_file) or os.path.isdir(input_file): 148 | print(f"Input file '{input_file}' does not exist.", file=sys.stderr) 149 | exit(1) 150 | 151 | # 152 | 153 | print(f"Collecting textures from {len(texturepaths)} path(s)... ", end="", flush=True) 154 | textures = {} 155 | for path in texturepaths: 156 | collect_files(path) 157 | print("done") 158 | 159 | print("Processing nodes...") 160 | fin = open(input_file, 'r') 161 | fout = open(output_file, 'w') 162 | n = 0 163 | for line in fin: 164 | line = line.rstrip('\r\n') 165 | if not line or line[0] == '#': 166 | fout.write(line + '\n') 167 | continue 168 | node, tex = line.split(" ") 169 | if not tex or tex == "blank.png": 170 | continue 171 | elif tex not in textures.keys(): 172 | print(f"skip {node} texture not found") 173 | continue 174 | color = average_color(textures[tex]) 175 | line = f"{node} {color}" 176 | #print(f"ok {node}") 177 | line = apply_sed(line, REPLACEMENTS) 178 | if line: 179 | fout.write(line + '\n') 180 | n += 1 181 | fin.close() 182 | fout.close() 183 | print(f"Done, {n} entries written.") 184 | -------------------------------------------------------------------------------- /src/db-postgresql.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "db-postgresql.h" 8 | #include "util.h" 9 | #include "log.h" 10 | #include "types.h" 11 | 12 | /* PostgreSQLBase */ 13 | 14 | PostgreSQLBase::~PostgreSQLBase() 15 | { 16 | if (db) 17 | PQfinish(db); 18 | } 19 | 20 | void PostgreSQLBase::openDatabase(const char *connect_string) 21 | { 22 | if (db) 23 | throw std::logic_error("Database already open"); 24 | 25 | db = PQconnectdb(connect_string); 26 | if (PQstatus(db) != CONNECTION_OK) { 27 | throw std::runtime_error(std::string("PostgreSQL database error: ") + 28 | PQerrorMessage(db) 29 | ); 30 | } 31 | } 32 | 33 | PGresult *PostgreSQLBase::checkResults(PGresult *res, bool clear) 34 | { 35 | ExecStatusType statusType = PQresultStatus(res); 36 | 37 | switch (statusType) { 38 | case PGRES_COMMAND_OK: 39 | case PGRES_TUPLES_OK: 40 | break; 41 | case PGRES_FATAL_ERROR: 42 | throw std::runtime_error( 43 | std::string("PostgreSQL database error: ") + 44 | PQresultErrorMessage(res) 45 | ); 46 | default: 47 | throw std::runtime_error( 48 | std::string("Unhandled PostgreSQL result code ") + 49 | std::to_string(statusType) 50 | ); 51 | } 52 | 53 | if (clear) 54 | PQclear(res); 55 | return res; 56 | } 57 | 58 | PGresult *PostgreSQLBase::execPrepared( 59 | const char *stmtName, const int paramsNumber, 60 | const void **params, 61 | const int *paramsLengths, const int *paramsFormats, 62 | bool clear) 63 | { 64 | return checkResults(PQexecPrepared(db, stmtName, paramsNumber, 65 | (const char* const*) params, paramsLengths, paramsFormats, 66 | 1 /* binary output */), clear 67 | ); 68 | } 69 | 70 | /* DBPostgreSQL */ 71 | 72 | DBPostgreSQL::DBPostgreSQL(const std::string &mapdir) 73 | { 74 | std::ifstream ifs(mapdir + "world.mt"); 75 | if (!ifs.good()) 76 | throw std::runtime_error("Failed to read world.mt"); 77 | std::string connect_string = read_setting("pgsql_connection", ifs); 78 | ifs.close(); 79 | 80 | openDatabase(connect_string.c_str()); 81 | 82 | prepareStatement( 83 | "get_block_pos", 84 | "SELECT posX::int4, posZ::int4 FROM blocks WHERE" 85 | " (posX BETWEEN $1::int4 AND $2::int4) AND" 86 | " (posY BETWEEN $3::int4 AND $4::int4) AND" 87 | " (posZ BETWEEN $5::int4 AND $6::int4) GROUP BY posX, posZ" 88 | ); 89 | prepareStatement( 90 | "get_blocks", 91 | "SELECT posY::int4, data FROM blocks WHERE" 92 | " posX = $1::int4 AND posZ = $2::int4" 93 | " AND (posY BETWEEN $3::int4 AND $4::int4)" 94 | ); 95 | prepareStatement( 96 | "get_block_exact", 97 | "SELECT data FROM blocks WHERE" 98 | " posX = $1::int4 AND posY = $2::int4 AND posZ = $3::int4" 99 | ); 100 | 101 | checkResults(PQexec(db, "START TRANSACTION;")); 102 | checkResults(PQexec(db, "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;")); 103 | } 104 | 105 | 106 | DBPostgreSQL::~DBPostgreSQL() 107 | { 108 | try { 109 | checkResults(PQexec(db, "COMMIT;")); 110 | } catch (const std::exception& caught) { 111 | errorstream << "could not finalize: " << caught.what() << std::endl; 112 | } 113 | } 114 | 115 | 116 | std::vector DBPostgreSQL::getBlockPosXZ(BlockPos min, BlockPos max) 117 | { 118 | int32_t const x1 = htonl(min.x); 119 | int32_t const x2 = htonl(max.x - 1); 120 | int32_t const y1 = htonl(min.y); 121 | int32_t const y2 = htonl(max.y - 1); 122 | int32_t const z1 = htonl(min.z); 123 | int32_t const z2 = htonl(max.z - 1); 124 | 125 | const void *args[] = { &x1, &x2, &y1, &y2, &z1, &z2 }; 126 | const int argLen[] = { 4, 4, 4, 4, 4, 4 }; 127 | const int argFmt[] = { 1, 1, 1, 1, 1, 1 }; 128 | 129 | PGresult *results = execPrepared( 130 | "get_block_pos", ARRLEN(args), args, 131 | argLen, argFmt, false 132 | ); 133 | 134 | int numrows = PQntuples(results); 135 | 136 | std::vector positions; 137 | positions.reserve(numrows); 138 | 139 | BlockPos pos; 140 | for (int row = 0; row < numrows; ++row) { 141 | pos.x = pg_binary_to_int(results, row, 0); 142 | pos.z = pg_binary_to_int(results, row, 1); 143 | positions.push_back(pos); 144 | } 145 | 146 | PQclear(results); 147 | return positions; 148 | } 149 | 150 | 151 | void DBPostgreSQL::getBlocksOnXZ(BlockList &blocks, int16_t xPos, int16_t zPos, 152 | int16_t min_y, int16_t max_y) 153 | { 154 | int32_t const x = htonl(xPos); 155 | int32_t const z = htonl(zPos); 156 | int32_t const y1 = htonl(min_y); 157 | int32_t const y2 = htonl(max_y - 1); 158 | 159 | const void *args[] = { &x, &z, &y1, &y2 }; 160 | const int argLen[] = { 4, 4, 4, 4 }; 161 | const int argFmt[] = { 1, 1, 1, 1 }; 162 | 163 | PGresult *results = execPrepared( 164 | "get_blocks", ARRLEN(args), args, 165 | argLen, argFmt, false 166 | ); 167 | 168 | int numrows = PQntuples(results); 169 | 170 | for (int row = 0; row < numrows; ++row) { 171 | BlockPos position; 172 | position.x = xPos; 173 | position.y = pg_binary_to_int(results, row, 0); 174 | position.z = zPos; 175 | blocks.emplace_back( 176 | position, 177 | ustring( 178 | reinterpret_cast( 179 | PQgetvalue(results, row, 1) 180 | ), 181 | PQgetlength(results, row, 1) 182 | ) 183 | ); 184 | } 185 | 186 | PQclear(results); 187 | } 188 | 189 | 190 | void DBPostgreSQL::getBlocksByPos(BlockList &blocks, 191 | const std::vector &positions) 192 | { 193 | int32_t x, y, z; 194 | 195 | const void *args[] = { &x, &y, &z }; 196 | const int argLen[] = { 4, 4, 4 }; 197 | const int argFmt[] = { 1, 1, 1 }; 198 | 199 | for (auto pos : positions) { 200 | x = htonl(pos.x); 201 | y = htonl(pos.y); 202 | z = htonl(pos.z); 203 | 204 | PGresult *results = execPrepared( 205 | "get_block_exact", ARRLEN(args), args, 206 | argLen, argFmt, false 207 | ); 208 | 209 | if (PQntuples(results) > 0) { 210 | blocks.emplace_back( 211 | pos, 212 | ustring( 213 | reinterpret_cast( 214 | PQgetvalue(results, 0, 0) 215 | ), 216 | PQgetlength(results, 0, 0) 217 | ) 218 | ); 219 | } 220 | 221 | PQclear(results); 222 | } 223 | } 224 | 225 | int DBPostgreSQL::pg_binary_to_int(PGresult *res, int row, int col) 226 | { 227 | int32_t* raw = reinterpret_cast(PQgetvalue(res, row, col)); 228 | return ntohl(*raw); 229 | } 230 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.5) 2 | 3 | project(minetestmapper 4 | VERSION 1.0 5 | LANGUAGES CXX 6 | ) 7 | 8 | # Stuff & Paths 9 | 10 | if(NOT CMAKE_BUILD_TYPE) 11 | set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build." FORCE) 12 | endif() 13 | 14 | set(CMAKE_CXX_STANDARD 11) 15 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 16 | 17 | if(WIN32) 18 | set(SHAREDIR ".") 19 | set(BINDIR ".") 20 | set(DOCDIR ".") 21 | else() 22 | set(SHAREDIR "share/luanti") # reuse engine share dir 23 | set(BINDIR "bin") 24 | set(DOCDIR "share/doc/${PROJECT_NAME}") 25 | set(MANDIR "share/man") 26 | endif() 27 | 28 | set(CUSTOM_SHAREDIR "" CACHE STRING "Directory to install data files into") 29 | if(NOT CUSTOM_SHAREDIR STREQUAL "") 30 | set(SHAREDIR "${CUSTOM_SHAREDIR}") 31 | message(STATUS "Using SHAREDIR=${SHAREDIR}") 32 | endif() 33 | 34 | set(CUSTOM_BINDIR "" CACHE STRING "Directory to install binaries into") 35 | if(NOT CUSTOM_BINDIR STREQUAL "") 36 | set(BINDIR "${CUSTOM_BINDIR}") 37 | message(STATUS "Using BINDIR=${BINDIR}") 38 | endif() 39 | 40 | set(CUSTOM_DOCDIR "" CACHE STRING "Directory to install documentation into") 41 | if(NOT CUSTOM_DOCDIR STREQUAL "") 42 | set(DOCDIR "${CUSTOM_DOCDIR}") 43 | message(STATUS "Using DOCDIR=${DOCDIR}") 44 | endif() 45 | 46 | list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") 47 | 48 | # Libraries: gd 49 | 50 | find_library(LIBGD_LIBRARY gd) 51 | find_path(LIBGD_INCLUDE_DIR gd.h) 52 | message (STATUS "libgd library: ${LIBGD_LIBRARY}") 53 | message (STATUS "libgd headers: ${LIBGD_INCLUDE_DIR}") 54 | if(NOT LIBGD_LIBRARY OR NOT LIBGD_INCLUDE_DIR) 55 | message(FATAL_ERROR "libgd not found!") 56 | endif(NOT LIBGD_LIBRARY OR NOT LIBGD_INCLUDE_DIR) 57 | 58 | # Libraries: zlib 59 | 60 | find_package(zlib-ng QUIET) 61 | if(zlib-ng_FOUND) 62 | set(ZLIB_INCLUDE_DIR zlib-ng::zlib) 63 | set(ZLIB_LIBRARY zlib-ng::zlib) 64 | set(USE_ZLIB_NG TRUE) 65 | message(STATUS "Found zlib-ng, using it instead of zlib.") 66 | else() 67 | message(STATUS "zlib-ng not found, falling back to zlib.") 68 | find_package(ZLIB REQUIRED) 69 | set(USE_ZLIB_NG FALSE) 70 | endif() 71 | 72 | # Libraries: zstd 73 | 74 | find_package(Zstd REQUIRED) 75 | 76 | # Libraries: sqlite3 77 | 78 | find_library(SQLITE3_LIBRARY sqlite3) 79 | find_path(SQLITE3_INCLUDE_DIR sqlite3.h) 80 | message (STATUS "sqlite3 library: ${SQLITE3_LIBRARY}") 81 | message (STATUS "sqlite3 headers: ${SQLITE3_INCLUDE_DIR}") 82 | if(NOT SQLITE3_LIBRARY OR NOT SQLITE3_INCLUDE_DIR) 83 | message(FATAL_ERROR "sqlite3 not found!") 84 | endif(NOT SQLITE3_LIBRARY OR NOT SQLITE3_INCLUDE_DIR) 85 | 86 | # Libraries: postgresql 87 | 88 | option(ENABLE_POSTGRESQL "Enable PostgreSQL backend" TRUE) 89 | set(USE_POSTGRESQL FALSE) 90 | 91 | if(ENABLE_POSTGRESQL) 92 | if(CMAKE_VERSION VERSION_LESS "3.20") 93 | find_package(PostgreSQL QUIET) 94 | # Before CMake 3.20 FindPostgreSQL.cmake always looked for server includes 95 | # but we don't need them, so continue anyway if only those are missing. 96 | if(PostgreSQL_INCLUDE_DIR AND PostgreSQL_LIBRARY) 97 | set(PostgreSQL_FOUND TRUE) 98 | set(PostgreSQL_INCLUDE_DIRS ${PostgreSQL_INCLUDE_DIR}) 99 | set(PostgreSQL_LIBRARIES ${PostgreSQL_LIBRARY}) 100 | endif() 101 | else() 102 | find_package(PostgreSQL) 103 | endif() 104 | 105 | if(PostgreSQL_FOUND) 106 | set(USE_POSTGRESQL TRUE) 107 | message(STATUS "PostgreSQL backend enabled") 108 | # This variable is case sensitive, don't try to change it to POSTGRESQL_INCLUDE_DIR 109 | message(STATUS "PostgreSQL includes: ${PostgreSQL_INCLUDE_DIRS}") 110 | include_directories(${PostgreSQL_INCLUDE_DIRS}) 111 | else() 112 | message(STATUS "PostgreSQL not found!") 113 | set(PostgreSQL_LIBRARIES "") 114 | endif() 115 | endif(ENABLE_POSTGRESQL) 116 | 117 | # Libraries: leveldb 118 | 119 | OPTION(ENABLE_LEVELDB "Enable LevelDB backend" TRUE) 120 | set(USE_LEVELDB FALSE) 121 | 122 | if(ENABLE_LEVELDB) 123 | find_library(LEVELDB_LIBRARY leveldb) 124 | find_path(LEVELDB_INCLUDE_DIR leveldb/db.h) 125 | message (STATUS "LevelDB library: ${LEVELDB_LIBRARY}") 126 | message (STATUS "LevelDB headers: ${LEVELDB_INCLUDE_DIR}") 127 | if(LEVELDB_LIBRARY AND LEVELDB_INCLUDE_DIR) 128 | set(USE_LEVELDB TRUE) 129 | message(STATUS "LevelDB backend enabled") 130 | include_directories(${LEVELDB_INCLUDE_DIR}) 131 | else() 132 | message(STATUS "LevelDB not found!") 133 | set(LEVELDB_LIBRARY "") 134 | endif() 135 | endif(ENABLE_LEVELDB) 136 | 137 | # Libraries: redis 138 | 139 | OPTION(ENABLE_REDIS "Enable redis backend" TRUE) 140 | set(USE_REDIS FALSE) 141 | 142 | if(ENABLE_REDIS) 143 | find_library(REDIS_LIBRARY hiredis) 144 | find_path(REDIS_INCLUDE_DIR hiredis/hiredis.h) 145 | message (STATUS "redis library: ${REDIS_LIBRARY}") 146 | message (STATUS "redis headers: ${REDIS_INCLUDE_DIR}") 147 | if(REDIS_LIBRARY AND REDIS_INCLUDE_DIR) 148 | set(USE_REDIS TRUE) 149 | message(STATUS "redis backend enabled") 150 | include_directories(${REDIS_INCLUDE_DIR}) 151 | else() 152 | message(STATUS "redis not found!") 153 | set(REDIS_LIBRARY "") 154 | endif() 155 | endif(ENABLE_REDIS) 156 | 157 | # Compiling & Linking 158 | 159 | configure_file( 160 | "${CMAKE_CURRENT_SOURCE_DIR}/src/cmake_config.h.in" 161 | "${CMAKE_CURRENT_BINARY_DIR}/cmake_config.h" 162 | ) 163 | 164 | if(CMAKE_CXX_COMPILER_ID MATCHES "^(GNU|Clang|AppleClang)$") 165 | set(CMAKE_CXX_FLAGS_RELEASE "-O2") 166 | set(CMAKE_CXX_FLAGS_DEBUG "-Og -g2") 167 | add_compile_options(-Wall -pipe) 168 | elseif(MSVC) 169 | add_compile_options(/GR- /Zl) 170 | endif() 171 | if(CMAKE_BUILD_TYPE STREQUAL "Release") 172 | add_definitions(-DNDEBUG) 173 | endif() 174 | 175 | add_executable(minetestmapper) 176 | 177 | target_include_directories(minetestmapper PRIVATE 178 | "${CMAKE_CURRENT_SOURCE_DIR}" 179 | "${CMAKE_CURRENT_BINARY_DIR}" 180 | ) 181 | 182 | target_sources(minetestmapper PRIVATE 183 | src/BlockDecoder.cpp 184 | src/PixelAttributes.cpp 185 | src/PlayerAttributes.cpp 186 | src/TileGenerator.cpp 187 | src/ZlibDecompressor.cpp 188 | src/ZstdDecompressor.cpp 189 | src/Image.cpp 190 | src/mapper.cpp 191 | src/util.cpp 192 | src/log.cpp 193 | src/db-sqlite3.cpp 194 | $<$:src/db-postgresql.cpp> 195 | $<$:src/db-leveldb.cpp> 196 | $<$:src/db-redis.cpp> 197 | ) 198 | 199 | target_include_directories(minetestmapper PRIVATE 200 | "${CMAKE_CURRENT_SOURCE_DIR}/src" 201 | "${CMAKE_CURRENT_BINARY_DIR}" 202 | ${SQLITE3_INCLUDE_DIR} 203 | ${LIBGD_INCLUDE_DIR} 204 | ${ZLIB_INCLUDE_DIR} 205 | ${ZSTD_INCLUDE_DIR} 206 | ) 207 | 208 | target_link_libraries(minetestmapper 209 | ${SQLITE3_LIBRARY} 210 | ${PostgreSQL_LIBRARIES} 211 | ${LEVELDB_LIBRARY} 212 | ${REDIS_LIBRARY} 213 | ${LIBGD_LIBRARY} 214 | ${ZLIB_LIBRARY} 215 | ${ZSTD_LIBRARY} 216 | ) 217 | 218 | # Installing & Packaging 219 | 220 | install(TARGETS "${PROJECT_NAME}" DESTINATION "${BINDIR}") 221 | install(FILES "AUTHORS" DESTINATION "${DOCDIR}") 222 | install(FILES "COPYING" DESTINATION "${DOCDIR}") 223 | install(FILES "README.rst" DESTINATION "${DOCDIR}") 224 | install(FILES "colors.txt" DESTINATION "${SHAREDIR}") 225 | if(UNIX) 226 | install(FILES "minetestmapper.6" DESTINATION "${MANDIR}/man6") 227 | endif() 228 | 229 | set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Overview mapper for Luanti") 230 | set(CPACK_PACKAGE_VENDOR "celeron55") 231 | set(CPACK_PACKAGE_CONTACT "Perttu Ahola ") 232 | 233 | if(WIN32) 234 | set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}-win32") 235 | set(CPACK_GENERATOR ZIP) 236 | else() 237 | set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}-linux") 238 | set(CPACK_GENERATOR TGZ) 239 | set(CPACK_SOURCE_GENERATOR TGZ) 240 | endif() 241 | 242 | include(CPack) 243 | -------------------------------------------------------------------------------- /src/db-sqlite3.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include // for usleep 3 | #include 4 | #include 5 | #include 6 | 7 | #include "db-sqlite3.h" 8 | #include "log.h" 9 | #include "types.h" 10 | 11 | /* SQLite3Base */ 12 | 13 | #define SQLRES(r, good) check_result(r, good) 14 | #define SQLOK(r) SQLRES(r, SQLITE_OK) 15 | 16 | SQLite3Base::~SQLite3Base() 17 | { 18 | if (db && sqlite3_close(db) != SQLITE_OK) { 19 | errorstream << "Error closing SQLite database: " 20 | << sqlite3_errmsg(db) << std::endl; 21 | } 22 | } 23 | 24 | void SQLite3Base::openDatabase(const char *path, bool readonly) 25 | { 26 | if (db) 27 | throw std::logic_error("Database already open"); 28 | 29 | int flags = 0; 30 | if (readonly) 31 | flags |= SQLITE_OPEN_READONLY | SQLITE_OPEN_PRIVATECACHE; 32 | else 33 | flags |= SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE; 34 | #ifdef SQLITE_OPEN_EXRESCODE 35 | flags |= SQLITE_OPEN_EXRESCODE; 36 | #endif 37 | SQLOK(sqlite3_open_v2(path, &db, flags, 0)); 38 | } 39 | 40 | /* DBSQLite3 */ 41 | 42 | // make sure a row is available. intended to be used outside a loop. 43 | // compare result to SQLITE_ROW afterwards. 44 | #define SQLROW1(stmt) \ 45 | while ((result = sqlite3_step(stmt)) == SQLITE_BUSY) \ 46 | usleep(10000); /* wait some time and try again */ \ 47 | if (result != SQLITE_ROW && result != SQLITE_DONE) { \ 48 | throw std::runtime_error(sqlite3_errmsg(db)); \ 49 | } 50 | 51 | // make sure next row is available. intended to be used in a while(sqlite3_step) loop 52 | #define SQLROW2() \ 53 | if (result == SQLITE_BUSY) { \ 54 | usleep(10000); /* wait some time and try again */ \ 55 | continue; \ 56 | } else if (result != SQLITE_ROW) { \ 57 | throw std::runtime_error(sqlite3_errmsg(db)); \ 58 | } 59 | 60 | DBSQLite3::DBSQLite3(const std::string &mapdir) 61 | { 62 | std::string db_name = mapdir + "map.sqlite"; 63 | 64 | openDatabase(db_name.c_str()); 65 | 66 | // There's a simple, dumb way to check if we have a new or old database schema. 67 | // If we prepare a statement that references columns that don't exist, it will 68 | // error right there. 69 | int result = prepare(stmt_get_block_pos, "SELECT x, y, z FROM blocks"); 70 | newFormat = result == SQLITE_OK; 71 | verbosestream << "Detected " << (newFormat ? "new" : "old") << " SQLite schema" << std::endl; 72 | 73 | if (newFormat) { 74 | SQLOK(prepare(stmt_get_blocks_xz_range, 75 | "SELECT y, data FROM blocks WHERE " 76 | "x = ? AND z = ? AND y BETWEEN ? AND ?")); 77 | 78 | SQLOK(prepare(stmt_get_block_exact, 79 | "SELECT data FROM blocks WHERE x = ? AND y = ? AND z = ?")); 80 | 81 | SQLOK(prepare(stmt_get_block_pos_range, 82 | "SELECT x, z FROM blocks WHERE " 83 | "x >= ? AND y >= ? AND z >= ? AND " 84 | "x < ? AND y < ? AND z < ? GROUP BY x, z")); 85 | } else { 86 | SQLOK(prepare(stmt_get_blocks_z, 87 | "SELECT pos, data FROM blocks WHERE pos BETWEEN ? AND ?")); 88 | 89 | SQLOK(prepare(stmt_get_block_exact, 90 | "SELECT data FROM blocks WHERE pos = ?")); 91 | 92 | SQLOK(prepare(stmt_get_block_pos, 93 | "SELECT pos FROM blocks")); 94 | 95 | SQLOK(prepare(stmt_get_block_pos_range, 96 | "SELECT pos FROM blocks WHERE pos BETWEEN ? AND ?")); 97 | } 98 | 99 | #undef RANGE 100 | } 101 | 102 | 103 | DBSQLite3::~DBSQLite3() 104 | { 105 | sqlite3_finalize(stmt_get_blocks_z); 106 | sqlite3_finalize(stmt_get_blocks_xz_range); 107 | sqlite3_finalize(stmt_get_block_pos); 108 | sqlite3_finalize(stmt_get_block_pos_range); 109 | sqlite3_finalize(stmt_get_block_exact); 110 | } 111 | 112 | 113 | inline void DBSQLite3::getPosRange(int64_t &min, int64_t &max, 114 | int16_t zPos, int16_t zPos2) 115 | { 116 | // Magic numbers! 117 | min = encodeBlockPos(BlockPos(0, -2048, zPos)); 118 | max = encodeBlockPos(BlockPos(0, 2048, zPos2)) - 1; 119 | } 120 | 121 | 122 | std::vector DBSQLite3::getBlockPosXZ(BlockPos min, BlockPos max) 123 | { 124 | int result; 125 | sqlite3_stmt *stmt; 126 | 127 | if (newFormat) { 128 | stmt = stmt_get_block_pos_range; 129 | int col = bind_pos(stmt, 1, min); 130 | bind_pos(stmt, col, max); 131 | } else { 132 | // can handle range query on Z axis via SQL 133 | if (min.z <= -2048 && max.z >= 2048) { 134 | stmt = stmt_get_block_pos; 135 | } else { 136 | stmt = stmt_get_block_pos_range; 137 | int64_t minPos, maxPos; 138 | if (min.z < -2048) 139 | min.z = -2048; 140 | if (max.z > 2048) 141 | max.z = 2048; 142 | getPosRange(minPos, maxPos, min.z, max.z - 1); 143 | SQLOK(sqlite3_bind_int64(stmt, 1, minPos)); 144 | SQLOK(sqlite3_bind_int64(stmt, 2, maxPos)); 145 | } 146 | } 147 | 148 | std::vector positions; 149 | BlockPos pos; 150 | while ((result = sqlite3_step(stmt)) != SQLITE_DONE) { 151 | SQLROW2() 152 | 153 | if (newFormat) { 154 | pos.x = sqlite3_column_int(stmt, 0); 155 | pos.z = sqlite3_column_int(stmt, 1); 156 | } else { 157 | pos = decodeBlockPos(sqlite3_column_int64(stmt, 0)); 158 | if (pos.x < min.x || pos.x >= max.x || pos.y < min.y || pos.y >= max.y) 159 | continue; 160 | // note that we can't try to deduplicate these because the order 161 | // of the encoded pos (if sorted) is ZYX. 162 | } 163 | positions.emplace_back(pos); 164 | } 165 | SQLOK(sqlite3_reset(stmt)); 166 | return positions; 167 | } 168 | 169 | 170 | void DBSQLite3::loadBlockCache(int16_t zPos) 171 | { 172 | int result; 173 | blockCache.clear(); 174 | 175 | assert(!newFormat); 176 | 177 | int64_t minPos, maxPos; 178 | getPosRange(minPos, maxPos, zPos, zPos); 179 | 180 | SQLOK(sqlite3_bind_int64(stmt_get_blocks_z, 1, minPos)); 181 | SQLOK(sqlite3_bind_int64(stmt_get_blocks_z, 2, maxPos)); 182 | 183 | while ((result = sqlite3_step(stmt_get_blocks_z)) != SQLITE_DONE) { 184 | SQLROW2() 185 | 186 | int64_t posHash = sqlite3_column_int64(stmt_get_blocks_z, 0); 187 | BlockPos pos = decodeBlockPos(posHash); 188 | blockCache[pos.x].emplace_back(pos, read_blob(stmt_get_blocks_z, 1)); 189 | } 190 | SQLOK(sqlite3_reset(stmt_get_blocks_z)); 191 | } 192 | 193 | 194 | void DBSQLite3::getBlocksOnXZ(BlockList &blocks, int16_t x, int16_t z, 195 | int16_t min_y, int16_t max_y) 196 | { 197 | // New format: use a real range query 198 | if (newFormat) { 199 | auto *stmt = stmt_get_blocks_xz_range; 200 | SQLOK(sqlite3_bind_int(stmt, 1, x)); 201 | SQLOK(sqlite3_bind_int(stmt, 2, z)); 202 | SQLOK(sqlite3_bind_int(stmt, 3, min_y)); 203 | SQLOK(sqlite3_bind_int(stmt, 4, max_y - 1)); // BETWEEN is inclusive 204 | 205 | int result; 206 | while ((result = sqlite3_step(stmt)) != SQLITE_DONE) { 207 | SQLROW2() 208 | 209 | BlockPos pos(x, sqlite3_column_int(stmt, 0), z); 210 | blocks.emplace_back(pos, read_blob(stmt, 1)); 211 | } 212 | SQLOK(sqlite3_reset(stmt)); 213 | return; 214 | } 215 | 216 | /* Cache the blocks on the given Z coordinate between calls, this only 217 | * works due to order in which the TileGenerator asks for blocks. */ 218 | if (z != blockCachedZ) { 219 | loadBlockCache(z); 220 | blockCachedZ = z; 221 | } 222 | 223 | auto it = blockCache.find(x); 224 | if (it == blockCache.end()) 225 | return; 226 | 227 | if (it->second.empty()) { 228 | /* We have swapped this list before, this is not supposed to happen 229 | * because it's bad for performance. But rather than silently breaking 230 | * do the right thing and load the blocks again. */ 231 | verbosestream << "suboptimal access pattern for sqlite3 backend?!" << std::endl; 232 | loadBlockCache(z); 233 | } 234 | // Swap lists to avoid copying contents 235 | blocks.clear(); 236 | std::swap(blocks, it->second); 237 | 238 | for (auto it = blocks.begin(); it != blocks.end(); ) { 239 | if (it->first.y < min_y || it->first.y >= max_y) 240 | it = blocks.erase(it); 241 | else 242 | it++; 243 | } 244 | } 245 | 246 | 247 | void DBSQLite3::getBlocksByPos(BlockList &blocks, 248 | const std::vector &positions) 249 | { 250 | int result; 251 | 252 | for (auto pos : positions) { 253 | bind_pos(stmt_get_block_exact, 1, pos); 254 | 255 | SQLROW1(stmt_get_block_exact) 256 | if (result == SQLITE_ROW) 257 | blocks.emplace_back(pos, read_blob(stmt_get_block_exact, 0)); 258 | 259 | SQLOK(sqlite3_reset(stmt_get_block_exact)); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/mapper.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include "config.h" 12 | #include "TileGenerator.h" 13 | #include "util.h" 14 | #include "log.h" 15 | 16 | static void usage() 17 | { 18 | static const std::pair options[] = { 19 | {"-i/--input", ""}, 20 | {"-o/--output", ""}, 21 | {"--bgcolor", ""}, 22 | {"--scalecolor", ""}, 23 | {"--playercolor", ""}, 24 | {"--origincolor", ""}, 25 | {"--drawscale", ""}, 26 | {"--drawplayers", ""}, 27 | {"--draworigin", ""}, 28 | {"--drawalpha", ""}, 29 | {"--noshading", ""}, 30 | {"--noemptyimage", ""}, 31 | {"-v/--verbose", ""}, 32 | {"--min-y", ""}, 33 | {"--max-y", ""}, 34 | {"--backend", ""}, 35 | {"--geometry", "x:z+w+h"}, 36 | {"--extent", ""}, 37 | {"--zoom", ""}, 38 | {"--colors", ""}, 39 | {"--scales", "[t][b][l][r]"}, 40 | {"--exhaustive", "never|y|full|auto"}, 41 | {"--dumpblock", "x,y,z"}, 42 | }; 43 | const char *top_text = 44 | "minetestmapper -i -o [options]\n" 45 | "Generate an overview image of a Luanti map.\n" 46 | "\n" 47 | "Options:\n"; 48 | const char *bottom_text = 49 | "\n" 50 | "Color format: hexadecimal '#RRGGBB', e.g. '#FF0000' = red\n"; 51 | 52 | printf("%s", top_text); 53 | for (const auto &p : options) 54 | printf(" %-18s%s\n", p.first, p.second); 55 | printf("%s", bottom_text); 56 | auto backends = TileGenerator::getSupportedBackends(); 57 | printf("Supported backends: "); 58 | for (auto s : backends) 59 | printf("%s ", s.c_str()); 60 | printf("\n"); 61 | #ifdef _WIN32 62 | printf("See also the full documentation in README.rst\n"); 63 | #else 64 | printf("See also the full documentation in minetestmapper(6) or README.rst\n"); 65 | #endif 66 | } 67 | 68 | static inline bool file_exists(const std::string &path) 69 | { 70 | return file_exists(path.c_str()); 71 | } 72 | 73 | static inline int stoi(const char *s) 74 | { 75 | std::istringstream iss(s); 76 | int ret; 77 | iss >> ret; 78 | return ret; 79 | } 80 | 81 | static std::string search_colors(const std::string &worldpath) 82 | { 83 | if (file_exists(worldpath + "/colors.txt")) 84 | return worldpath + "/colors.txt"; 85 | 86 | #ifndef _WIN32 87 | char *home = std::getenv("HOME"); 88 | if (home && home[0]) { 89 | std::string check = std::string(home) + "/.minetest/colors.txt"; 90 | if (file_exists(check)) 91 | return check; 92 | } 93 | #endif 94 | 95 | constexpr bool sharedir_valid = !(SHAREDIR[0] == '.' || !SHAREDIR[0]); 96 | if (sharedir_valid && file_exists(SHAREDIR "/colors.txt")) 97 | return SHAREDIR "/colors.txt"; 98 | 99 | errorstream << "Warning: Falling back to using colors.txt from current directory." << std::endl; 100 | return "./colors.txt"; 101 | } 102 | 103 | int main(int argc, char *argv[]) 104 | { 105 | const char *short_options = "hi:o:v"; 106 | const static struct option long_options[] = 107 | { 108 | {"help", no_argument, 0, 'h'}, 109 | {"input", required_argument, 0, 'i'}, 110 | {"output", required_argument, 0, 'o'}, 111 | {"bgcolor", required_argument, 0, 'b'}, 112 | {"scalecolor", required_argument, 0, 's'}, 113 | {"origincolor", required_argument, 0, 'r'}, 114 | {"playercolor", required_argument, 0, 'p'}, 115 | {"draworigin", no_argument, 0, 'R'}, 116 | {"drawplayers", no_argument, 0, 'P'}, 117 | {"drawscale", no_argument, 0, 'S'}, 118 | {"drawalpha", no_argument, 0, 'e'}, 119 | {"noshading", no_argument, 0, 'H'}, 120 | {"backend", required_argument, 0, 'd'}, 121 | {"geometry", required_argument, 0, 'g'}, 122 | {"extent", no_argument, 0, 'E'}, 123 | {"min-y", required_argument, 0, 'a'}, 124 | {"max-y", required_argument, 0, 'c'}, 125 | {"zoom", required_argument, 0, 'z'}, 126 | {"colors", required_argument, 0, 'C'}, 127 | {"scales", required_argument, 0, 'f'}, 128 | {"noemptyimage", no_argument, 0, 'n'}, 129 | {"exhaustive", required_argument, 0, 'j'}, 130 | {"dumpblock", required_argument, 0, 'k'}, 131 | {"verbose", no_argument, 0, 'v'}, 132 | {0, 0, 0, 0} 133 | }; 134 | 135 | configure_log_streams(false); 136 | 137 | std::string input; 138 | std::string output; 139 | std::string colors; 140 | bool onlyPrintExtent = false; 141 | BlockPos dumpblock(INT16_MIN); 142 | 143 | TileGenerator generator; 144 | while (1) { 145 | int option_index; 146 | int c = getopt_long(argc, argv, short_options, long_options, &option_index); 147 | if (c == -1) 148 | break; // done 149 | 150 | switch (c) { 151 | case 'h': 152 | usage(); 153 | return 0; 154 | case 'i': 155 | input = optarg; 156 | break; 157 | case 'o': 158 | output = optarg; 159 | break; 160 | case 'b': 161 | generator.setBgColor(optarg); 162 | break; 163 | case 's': 164 | generator.setScaleColor(optarg); 165 | break; 166 | case 'r': 167 | generator.setOriginColor(optarg); 168 | break; 169 | case 'p': 170 | generator.setPlayerColor(optarg); 171 | break; 172 | case 'R': 173 | generator.setDrawOrigin(true); 174 | break; 175 | case 'P': 176 | generator.setDrawPlayers(true); 177 | break; 178 | case 'S': 179 | generator.setDrawScale(true); 180 | break; 181 | case 'e': 182 | generator.setDrawAlpha(true); 183 | break; 184 | case 'E': 185 | onlyPrintExtent = true; 186 | break; 187 | case 'H': 188 | generator.setShading(false); 189 | break; 190 | case 'd': 191 | generator.setBackend(optarg); 192 | break; 193 | case 'a': 194 | generator.setMinY(stoi(optarg)); 195 | break; 196 | case 'c': 197 | generator.setMaxY(stoi(optarg)); 198 | break; 199 | case 'g': { 200 | std::istringstream geometry(optarg); 201 | int x, y, w, h; 202 | char c; 203 | geometry >> x >> c >> y >> w >> h; 204 | if (geometry.fail() || c != ':' || w < 1 || h < 1) { 205 | usage(); 206 | return 1; 207 | } 208 | generator.setGeometry(x, y, w, h); 209 | } 210 | break; 211 | case 'f': { 212 | uint flags = 0; 213 | if (strchr(optarg, 't')) 214 | flags |= SCALE_TOP; 215 | if (strchr(optarg, 'b')) 216 | flags |= SCALE_BOTTOM; 217 | if (strchr(optarg, 'l')) 218 | flags |= SCALE_LEFT; 219 | if (strchr(optarg, 'r')) 220 | flags |= SCALE_RIGHT; 221 | generator.setScales(flags); 222 | } 223 | break; 224 | case 'z': 225 | generator.setZoom(stoi(optarg)); 226 | break; 227 | case 'C': 228 | colors = optarg; 229 | break; 230 | case 'n': 231 | generator.setDontWriteEmpty(true); 232 | break; 233 | case 'j': { 234 | int mode = EXH_AUTO; 235 | if (!strcmp(optarg, "never")) 236 | mode = EXH_NEVER; 237 | else if (!strcmp(optarg, "y")) 238 | mode = EXH_Y; 239 | else if (!strcmp(optarg, "full")) 240 | mode = EXH_FULL; 241 | generator.setExhaustiveSearch(mode); 242 | } 243 | break; 244 | case 'k': { 245 | std::istringstream iss(optarg); 246 | char c, c2; 247 | iss >> dumpblock.x >> c >> dumpblock.y >> c2 >> dumpblock.z; 248 | if (iss.fail() || c != ',' || c2 != ',') { 249 | usage(); 250 | return 1; 251 | } 252 | break; 253 | } 254 | case 'v': 255 | configure_log_streams(true); 256 | break; 257 | default: 258 | return 1; 259 | } 260 | } 261 | 262 | const bool need_output = !onlyPrintExtent && dumpblock.x == INT16_MIN; 263 | if (input.empty() || (need_output && output.empty())) { 264 | usage(); 265 | return 0; 266 | } 267 | 268 | try { 269 | if (onlyPrintExtent) { 270 | generator.printGeometry(input); 271 | return 0; 272 | } else if (dumpblock.x != INT16_MIN) { 273 | generator.dumpBlock(input, dumpblock); 274 | return 0; 275 | } 276 | 277 | if (colors.empty()) 278 | colors = search_colors(input); 279 | generator.parseColorsFile(colors); 280 | generator.generate(input, output); 281 | 282 | } catch (std::exception &e) { 283 | errorstream << "Exception: " << e.what() << std::endl; 284 | return 1; 285 | } 286 | return 0; 287 | } 288 | -------------------------------------------------------------------------------- /colors.txt: -------------------------------------------------------------------------------- 1 | # beds 2 | beds:bed_bottom 130 3 3 3 | beds:bed_top 185 162 163 4 | beds:fancy_bed_bottom 136 49 28 5 | beds:fancy_bed_top 179 153 148 6 | 7 | # bones 8 | bones:bones 117 117 117 9 | 10 | # butterflies 11 | 12 | # carts 13 | carts:brakerail 150 121 102 14 | carts:powerrail 160 145 102 15 | carts:rail 146 128 108 16 | 17 | # default 18 | default:acacia_bush_leaves 109 133 87 19 | default:acacia_bush_sapling 85 121 61 20 | default:acacia_bush_stem 84 77 70 21 | default:acacia_leaves 126 153 101 22 | default:acacia_sapling 87 120 64 23 | default:acacia_tree 195 119 97 24 | default:acacia_wood 150 61 39 25 | default:apple 161 34 19 26 | default:aspen_leaves 72 105 29 27 | default:aspen_sapling 85 123 45 28 | default:aspen_tree 218 198 168 29 | default:aspen_wood 210 199 170 30 | default:blueberry_bush_leaves 63 99 22 31 | default:blueberry_bush_leaves_with_berries 63 99 22 32 | default:blueberry_bush_sapling 81 112 33 33 | default:bookshelf 131 102 57 34 | default:brick 123 99 95 35 | default:bronzeblock 186 111 15 36 | default:bush_leaves 35 55 29 37 | default:bush_sapling 66 64 40 38 | default:bush_stem 46 34 24 39 | default:cactus 70 119 52 40 | default:cave_ice 168 206 247 41 | default:chest 149 115 69 42 | default:chest_locked 149 115 69 43 | default:chest_locked_open 149 115 69 44 | default:chest_open 149 115 69 45 | default:clay 183 183 183 46 | default:cloud 255 255 255 47 | default:coalblock 58 58 58 48 | default:cobble 89 86 84 49 | default:copperblock 193 126 65 50 | default:coral_brown 146 113 77 51 | default:coral_cyan 235 230 215 52 | default:coral_green 235 230 215 53 | default:coral_orange 197 68 17 54 | default:coral_pink 235 230 215 55 | default:coral_skeleton 235 230 215 56 | default:desert_cobble 110 67 50 57 | default:desert_sand 206 165 98 58 | default:desert_sandstone 195 152 92 59 | default:desert_sandstone_block 193 152 94 60 | default:desert_sandstone_brick 191 151 95 61 | default:desert_stone 130 79 61 62 | default:desert_stone_block 131 80 61 63 | default:desert_stonebrick 131 80 61 64 | default:diamondblock 140 218 223 65 | default:dirt 97 67 43 66 | default:dirt_with_coniferous_litter 109 90 71 67 | default:dirt_with_dry_grass 187 148 78 68 | default:dirt_with_grass 64 111 26 69 | default:dirt_with_grass_footsteps 64 111 26 70 | default:dirt_with_rainforest_litter 76 39 10 71 | default:dirt_with_snow 225 225 238 72 | default:dry_dirt 178 136 90 73 | default:dry_dirt_with_dry_grass 187 148 78 74 | default:dry_grass_1 208 172 87 75 | default:dry_grass_2 210 174 87 76 | default:dry_grass_3 210 174 87 77 | default:dry_grass_4 211 175 88 78 | default:dry_grass_5 214 178 92 79 | default:dry_shrub 103 67 18 80 | default:emergent_jungle_sapling 51 40 16 81 | default:fence_acacia_wood 151 62 39 82 | default:fence_aspen_wood 210 199 170 83 | default:fence_junglewood 57 39 14 84 | default:fence_pine_wood 221 185 131 85 | default:fence_rail_acacia_wood 150 61 39 86 | default:fence_rail_aspen_wood 209 198 170 87 | default:fence_rail_junglewood 56 39 14 88 | default:fence_rail_pine_wood 221 184 130 89 | default:fence_rail_wood 131 102 57 90 | default:fence_wood 132 103 57 91 | default:fern_1 85 118 51 92 | default:fern_2 90 123 53 93 | default:fern_3 91 125 54 94 | default:furnace 101 98 96 95 | default:furnace_active 101 98 96 96 | default:glass 247 247 247 64 16 97 | default:goldblock 231 203 35 98 | default:grass_1 100 140 54 99 | default:grass_2 98 139 55 100 | default:grass_3 94 136 53 101 | default:grass_4 89 133 48 102 | default:grass_5 86 126 48 103 | default:gravel 132 132 132 104 | default:ice 168 206 247 105 | default:junglegrass 67 110 28 106 | default:jungleleaves 22 31 16 107 | default:junglesapling 51 39 15 108 | default:jungletree 121 97 62 109 | default:junglewood 56 39 14 110 | default:ladder_steel 132 132 132 111 | default:ladder_wood 125 93 43 112 | default:large_cactus_seedling 67 107 52 113 | default:lava_flowing 255 100 0 114 | default:lava_source 255 100 0 115 | default:leaves 36 55 29 116 | default:marram_grass_1 113 139 96 117 | default:marram_grass_2 102 131 90 118 | default:marram_grass_3 99 130 88 119 | default:mese 222 222 0 120 | default:mese_post_light 132 103 57 121 | default:mese_post_light_acacia_wood 151 62 39 122 | default:mese_post_light_aspen_wood 210 199 170 123 | default:mese_post_light_junglewood 57 39 14 124 | default:mese_post_light_pine_wood 221 185 131 125 | default:meselamp 213 215 143 126 | default:mossycobble 88 91 73 127 | default:obsidian 21 24 29 128 | default:obsidian_block 23 25 30 129 | default:obsidian_glass 20 23 27 64 16 130 | default:obsidianbrick 23 25 29 131 | default:papyrus 97 134 38 132 | default:permafrost 71 66 61 133 | default:permafrost_with_moss 108 150 51 134 | default:permafrost_with_stones 71 66 61 135 | default:pine_bush_needles 16 50 19 136 | default:pine_bush_sapling 58 51 40 137 | default:pine_bush_stem 73 62 53 138 | default:pine_needles 16 50 19 139 | default:pine_sapling 41 48 26 140 | default:pine_tree 191 165 132 141 | default:pine_wood 221 185 130 142 | default:river_water_flowing 39 66 106 128 224 143 | default:river_water_source 39 66 106 128 224 144 | default:sand 214 207 158 145 | default:sand_with_kelp 214 207 158 146 | default:sandstone 198 193 143 147 | default:sandstone_block 195 191 142 148 | default:sandstonebrick 194 190 141 149 | default:sapling 67 63 41 150 | default:sign_wall_steel 147 147 147 151 | default:sign_wall_wood 148 103 66 152 | default:silver_sand 193 191 179 153 | default:silver_sandstone 195 192 181 154 | default:silver_sandstone_block 192 190 180 155 | default:silver_sandstone_brick 191 189 179 156 | default:snow 225 225 238 157 | default:snowblock 225 225 238 158 | default:steelblock 195 195 195 159 | default:stone 97 94 93 160 | default:stone_block 100 97 96 161 | default:stone_with_coal 97 94 93 162 | default:stone_with_copper 97 94 93 163 | default:stone_with_diamond 97 94 93 164 | default:stone_with_gold 97 94 93 165 | default:stone_with_iron 97 94 93 166 | default:stone_with_mese 97 94 93 167 | default:stone_with_tin 97 94 93 168 | default:stonebrick 102 99 98 169 | default:tinblock 150 150 150 170 | default:torch 141 123 93 171 | default:torch_ceiling 141 123 93 172 | default:torch_wall 141 123 93 173 | default:tree 179 145 99 174 | default:water_flowing 39 66 106 128 224 175 | default:water_source 39 66 106 128 224 176 | default:wood 131 102 57 177 | 178 | # doors 179 | doors:door_glass_a 245 245 245 64 16 180 | doors:door_glass_b 245 245 245 64 16 181 | doors:door_glass_c 245 245 245 64 16 182 | doors:door_glass_d 245 245 245 64 16 183 | doors:door_obsidian_glass_a 48 49 50 64 16 184 | doors:door_obsidian_glass_b 48 49 50 64 16 185 | doors:door_obsidian_glass_c 48 49 50 64 16 186 | doors:door_obsidian_glass_d 48 49 50 64 16 187 | doors:door_steel_a 203 203 203 188 | doors:door_steel_b 203 203 203 189 | doors:door_steel_c 203 203 203 190 | doors:door_steel_d 203 203 203 191 | doors:door_wood_a 89 68 37 192 | doors:door_wood_b 89 68 37 193 | doors:door_wood_c 89 68 37 194 | doors:door_wood_d 89 68 37 195 | doors:gate_acacia_wood_closed 150 61 39 196 | doors:gate_acacia_wood_open 150 61 39 197 | doors:gate_aspen_wood_closed 210 199 170 198 | doors:gate_aspen_wood_open 210 199 170 199 | doors:gate_junglewood_closed 56 39 14 200 | doors:gate_junglewood_open 56 39 14 201 | doors:gate_pine_wood_closed 221 185 130 202 | doors:gate_pine_wood_open 221 185 130 203 | doors:gate_wood_closed 131 102 57 204 | doors:gate_wood_open 131 102 57 205 | doors:trapdoor 130 100 51 206 | doors:trapdoor_open 68 53 30 207 | doors:trapdoor_steel 200 200 200 208 | doors:trapdoor_steel_open 97 97 97 209 | 210 | # farming 211 | farming:cotton_1 89 117 39 212 | farming:cotton_2 89 116 38 213 | farming:cotton_3 99 121 41 214 | farming:cotton_4 108 114 47 215 | farming:cotton_5 116 105 53 216 | farming:cotton_6 121 95 59 217 | farming:cotton_7 94 70 37 218 | farming:cotton_8 122 108 93 219 | farming:cotton_wild 111 111 101 220 | farming:desert_sand_soil 161 132 72 221 | farming:desert_sand_soil_wet 120 99 53 222 | farming:dry_soil 178 136 90 223 | farming:dry_soil_wet 178 136 90 224 | farming:seed_cotton 92 87 60 225 | farming:seed_wheat 177 161 96 226 | farming:soil 97 67 43 227 | farming:soil_wet 97 67 43 228 | farming:straw 212 184 68 229 | farming:wheat_1 110 175 36 230 | farming:wheat_2 136 177 53 231 | farming:wheat_3 163 182 84 232 | farming:wheat_4 170 188 95 233 | farming:wheat_5 171 179 97 234 | farming:wheat_6 173 177 87 235 | farming:wheat_7 193 181 83 236 | farming:wheat_8 187 162 40 237 | 238 | # fire 239 | fire:basic_flame 223 136 44 240 | fire:permanent_flame 223 136 44 241 | 242 | # fireflies 243 | fireflies:firefly_bottle 191 194 202 244 | 245 | # flowers 246 | flowers:chrysanthemum_green 118 152 44 247 | flowers:dandelion_white 199 191 176 248 | flowers:dandelion_yellow 212 167 31 249 | flowers:geranium 77 91 168 250 | flowers:mushroom_brown 109 84 78 251 | flowers:mushroom_red 195 102 102 252 | flowers:rose 130 68 33 253 | flowers:tulip 156 101 44 254 | flowers:tulip_black 78 120 72 255 | flowers:viola 115 69 184 256 | flowers:waterlily 107 160 68 257 | flowers:waterlily_waving 107 160 68 258 | 259 | # stairs 260 | stairs:slab_acacia_wood 150 61 39 261 | stairs:slab_aspen_wood 210 199 170 262 | stairs:slab_brick 123 99 95 263 | stairs:slab_bronzeblock 186 111 15 264 | stairs:slab_cobble 89 86 84 265 | stairs:slab_copperblock 193 126 65 266 | stairs:slab_desert_cobble 110 67 50 267 | stairs:slab_desert_sandstone 195 152 92 268 | stairs:slab_desert_sandstone_block 193 152 94 269 | stairs:slab_desert_sandstone_brick 191 151 95 270 | stairs:slab_desert_stone 130 79 61 271 | stairs:slab_desert_stone_block 131 80 61 272 | stairs:slab_desert_stonebrick 131 80 61 273 | stairs:slab_glass 247 247 247 274 | stairs:slab_goldblock 231 203 35 275 | stairs:slab_ice 168 206 247 276 | stairs:slab_junglewood 56 39 14 277 | stairs:slab_mossycobble 88 91 73 278 | stairs:slab_obsidian 21 24 29 279 | stairs:slab_obsidian_block 23 25 30 280 | stairs:slab_obsidian_glass 20 23 27 281 | stairs:slab_obsidianbrick 23 25 29 282 | stairs:slab_pine_wood 221 185 130 283 | stairs:slab_sandstone 198 193 143 284 | stairs:slab_sandstone_block 195 191 142 285 | stairs:slab_sandstonebrick 194 190 141 286 | stairs:slab_silver_sandstone 195 192 181 287 | stairs:slab_silver_sandstone_block 192 190 180 288 | stairs:slab_silver_sandstone_brick 191 189 179 289 | stairs:slab_snowblock 225 225 238 290 | stairs:slab_steelblock 195 195 195 291 | stairs:slab_stone 97 94 93 292 | stairs:slab_stone_block 100 97 96 293 | stairs:slab_stonebrick 102 99 98 294 | stairs:slab_straw 212 184 68 295 | stairs:slab_tinblock 150 150 150 296 | stairs:slab_wood 131 102 57 297 | stairs:stair_acacia_wood 150 61 39 298 | stairs:stair_aspen_wood 210 199 170 299 | stairs:stair_brick 123 99 95 300 | stairs:stair_bronzeblock 186 111 15 301 | stairs:stair_cobble 89 86 84 302 | stairs:stair_copperblock 193 126 65 303 | stairs:stair_desert_cobble 110 67 50 304 | stairs:stair_desert_sandstone 195 152 92 305 | stairs:stair_desert_sandstone_block 193 152 94 306 | stairs:stair_desert_sandstone_brick 191 151 95 307 | stairs:stair_desert_stone 130 79 61 308 | stairs:stair_desert_stone_block 131 80 61 309 | stairs:stair_desert_stonebrick 131 80 61 310 | stairs:stair_glass 249 249 249 311 | stairs:stair_goldblock 231 203 35 312 | stairs:stair_ice 168 206 247 313 | stairs:stair_inner_acacia_wood 150 61 39 314 | stairs:stair_inner_aspen_wood 210 199 170 315 | stairs:stair_inner_brick 123 99 95 316 | stairs:stair_inner_bronzeblock 186 111 15 317 | stairs:stair_inner_cobble 89 86 84 318 | stairs:stair_inner_copperblock 193 126 65 319 | stairs:stair_inner_desert_cobble 110 67 50 320 | stairs:stair_inner_desert_sandstone 195 152 92 321 | stairs:stair_inner_desert_sandstone_block 193 152 94 322 | stairs:stair_inner_desert_sandstone_brick 191 151 95 323 | stairs:stair_inner_desert_stone 130 79 61 324 | stairs:stair_inner_desert_stone_block 131 80 61 325 | stairs:stair_inner_desert_stonebrick 131 80 61 326 | stairs:stair_inner_glass 250 250 250 327 | stairs:stair_inner_goldblock 231 203 35 328 | stairs:stair_inner_ice 168 206 247 329 | stairs:stair_inner_junglewood 56 39 14 330 | stairs:stair_inner_mossycobble 88 91 73 331 | stairs:stair_inner_obsidian 21 24 29 332 | stairs:stair_inner_obsidian_block 23 25 30 333 | stairs:stair_inner_obsidian_glass 20 22 27 334 | stairs:stair_inner_obsidianbrick 23 25 29 335 | stairs:stair_inner_pine_wood 221 185 130 336 | stairs:stair_inner_sandstone 198 193 143 337 | stairs:stair_inner_sandstone_block 195 191 142 338 | stairs:stair_inner_sandstonebrick 194 190 141 339 | stairs:stair_inner_silver_sandstone 195 192 181 340 | stairs:stair_inner_silver_sandstone_block 192 190 180 341 | stairs:stair_inner_silver_sandstone_brick 191 189 179 342 | stairs:stair_inner_snowblock 225 225 238 343 | stairs:stair_inner_steelblock 195 195 195 344 | stairs:stair_inner_stone 97 94 93 345 | stairs:stair_inner_stone_block 100 97 96 346 | stairs:stair_inner_stonebrick 102 99 98 347 | stairs:stair_inner_straw 212 184 68 348 | stairs:stair_inner_tinblock 150 150 150 349 | stairs:stair_inner_wood 131 102 57 350 | stairs:stair_junglewood 56 39 14 351 | stairs:stair_mossycobble 88 91 73 352 | stairs:stair_obsidian 21 24 29 353 | stairs:stair_obsidian_block 23 25 30 354 | stairs:stair_obsidian_glass 20 22 27 355 | stairs:stair_obsidianbrick 23 25 29 356 | stairs:stair_outer_acacia_wood 150 61 39 357 | stairs:stair_outer_aspen_wood 210 199 170 358 | stairs:stair_outer_brick 123 99 95 359 | stairs:stair_outer_bronzeblock 186 111 15 360 | stairs:stair_outer_cobble 89 86 84 361 | stairs:stair_outer_copperblock 193 126 65 362 | stairs:stair_outer_desert_cobble 110 67 50 363 | stairs:stair_outer_desert_sandstone 195 152 92 364 | stairs:stair_outer_desert_sandstone_block 193 152 94 365 | stairs:stair_outer_desert_sandstone_brick 191 151 95 366 | stairs:stair_outer_desert_stone 130 79 61 367 | stairs:stair_outer_desert_stone_block 131 80 61 368 | stairs:stair_outer_desert_stonebrick 131 80 61 369 | stairs:stair_outer_glass 250 250 250 370 | stairs:stair_outer_goldblock 231 203 35 371 | stairs:stair_outer_ice 168 206 247 372 | stairs:stair_outer_junglewood 56 39 14 373 | stairs:stair_outer_mossycobble 88 91 73 374 | stairs:stair_outer_obsidian 21 24 29 375 | stairs:stair_outer_obsidian_block 23 25 30 376 | stairs:stair_outer_obsidian_glass 20 22 27 377 | stairs:stair_outer_obsidianbrick 23 25 29 378 | stairs:stair_outer_pine_wood 221 185 130 379 | stairs:stair_outer_sandstone 198 193 143 380 | stairs:stair_outer_sandstone_block 195 191 142 381 | stairs:stair_outer_sandstonebrick 194 190 141 382 | stairs:stair_outer_silver_sandstone 195 192 181 383 | stairs:stair_outer_silver_sandstone_block 192 190 180 384 | stairs:stair_outer_silver_sandstone_brick 191 189 179 385 | stairs:stair_outer_snowblock 225 225 238 386 | stairs:stair_outer_steelblock 195 195 195 387 | stairs:stair_outer_stone 97 94 93 388 | stairs:stair_outer_stone_block 100 97 96 389 | stairs:stair_outer_stonebrick 102 99 98 390 | stairs:stair_outer_straw 212 184 68 391 | stairs:stair_outer_tinblock 150 150 150 392 | stairs:stair_outer_wood 131 102 57 393 | stairs:stair_pine_wood 221 185 130 394 | stairs:stair_sandstone 198 193 143 395 | stairs:stair_sandstone_block 195 191 142 396 | stairs:stair_sandstonebrick 194 190 141 397 | stairs:stair_silver_sandstone 195 192 181 398 | stairs:stair_silver_sandstone_block 192 190 180 399 | stairs:stair_silver_sandstone_brick 191 189 179 400 | stairs:stair_snowblock 225 225 238 401 | stairs:stair_steelblock 195 195 195 402 | stairs:stair_stone 97 94 93 403 | stairs:stair_stone_block 100 97 96 404 | stairs:stair_stonebrick 102 99 98 405 | stairs:stair_straw 212 184 68 406 | stairs:stair_tinblock 150 150 150 407 | stairs:stair_wood 131 102 57 408 | 409 | # tnt 410 | tnt:gunpowder 12 12 12 411 | tnt:gunpowder_burning 156 143 7 412 | tnt:tnt 196 0 0 413 | tnt:tnt_burning 201 41 0 414 | 415 | # vessels 416 | vessels:drinking_glass 207 214 228 417 | vessels:glass_bottle 189 192 204 418 | vessels:shelf 131 102 57 419 | vessels:steel_bottle 194 193 193 420 | 421 | # walls 422 | walls:cobble 89 86 84 423 | walls:desertcobble 110 67 50 424 | walls:mossycobble 88 91 73 425 | 426 | # wool 427 | wool:black 30 30 30 428 | wool:blue 0 73 146 429 | wool:brown 88 44 0 430 | wool:cyan 0 132 140 431 | wool:dark_green 33 103 0 432 | wool:dark_grey 60 60 60 433 | wool:green 93 218 28 434 | wool:grey 133 133 133 435 | wool:magenta 201 3 112 436 | wool:orange 214 83 22 437 | wool:pink 255 133 133 438 | wool:red 170 18 18 439 | wool:violet 93 5 169 440 | wool:white 220 220 220 441 | wool:yellow 254 226 16 442 | 443 | # xpanes 444 | xpanes:bar 114 114 114 64 16 445 | xpanes:bar_flat 114 114 114 64 16 446 | xpanes:door_steel_bar_a 133 133 133 64 16 447 | xpanes:door_steel_bar_b 133 133 133 64 16 448 | xpanes:door_steel_bar_c 133 133 133 64 16 449 | xpanes:door_steel_bar_d 133 133 133 64 16 450 | xpanes:obsidian_pane 16 17 18 64 16 451 | xpanes:obsidian_pane_flat 16 17 18 64 16 452 | xpanes:pane 249 249 249 64 16 453 | xpanes:pane_flat 249 249 249 64 16 454 | xpanes:trapdoor_steel_bar 127 127 127 64 16 455 | xpanes:trapdoor_steel_bar_open 77 77 77 64 16 456 | 457 | -------------------------------------------------------------------------------- /src/TileGenerator.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "TileGenerator.h" 15 | #include "config.h" 16 | #include "PlayerAttributes.h" 17 | #include "BlockDecoder.h" 18 | #include "Image.h" 19 | #include "util.h" 20 | #include "log.h" 21 | 22 | #include "db-sqlite3.h" 23 | #if USE_POSTGRESQL 24 | #include "db-postgresql.h" 25 | #endif 26 | #if USE_LEVELDB 27 | #include "db-leveldb.h" 28 | #endif 29 | #if USE_REDIS 30 | #include "db-redis.h" 31 | #endif 32 | 33 | #ifndef __has_builtin 34 | #define __has_builtin(x) 0 35 | #endif 36 | 37 | // saturating multiplication 38 | template::value>::type> 39 | inline T sat_mul(T a, T b) 40 | { 41 | #if __has_builtin(__builtin_mul_overflow) 42 | T res; 43 | if (__builtin_mul_overflow(a, b, &res)) 44 | return std::numeric_limits::max(); 45 | return res; 46 | #else 47 | // WARNING: the fallback implementation is incorrect since we compute ceil(log(x)) not log(x) 48 | // but that's good enough for our usecase... 49 | const int bits = sizeof(T) * 8; 50 | int hb_a = 0, hb_b = 0; 51 | for (int i = bits - 1; i >= 0; i--) { 52 | if (a & (static_cast(1) << i)) { 53 | hb_a = i; break; 54 | } 55 | } 56 | for (int i = bits - 1; i >= 0; i--) { 57 | if (b & (static_cast(1) << i)) { 58 | hb_b = i; break; 59 | } 60 | } 61 | // log2(a) + log2(b) >= log2(MAX) <=> calculation will overflow 62 | if (hb_a + hb_b >= bits) 63 | return std::numeric_limits::max(); 64 | return a * b; 65 | #endif 66 | } 67 | 68 | template 69 | inline T sat_mul(T a, T b, T c) 70 | { 71 | return sat_mul(sat_mul(a, b), c); 72 | } 73 | 74 | // rounds n (away from 0) to a multiple of f while preserving the sign of n 75 | static int round_multiple_nosign(int n, int f) 76 | { 77 | int abs_n, sign; 78 | abs_n = (n >= 0) ? n : -n; 79 | sign = (n >= 0) ? 1 : -1; 80 | if (abs_n % f == 0) 81 | return n; // n == abs_n * sign 82 | else 83 | return sign * (abs_n + f - (abs_n % f)); 84 | } 85 | 86 | static inline unsigned int colorSafeBounds(int channel) 87 | { 88 | return mymin(mymax(channel, 0), 255); 89 | } 90 | 91 | static Color parseColor(const std::string &color) 92 | { 93 | if (color.length() != 7) 94 | throw std::runtime_error("Color needs to be 7 characters long"); 95 | if (color[0] != '#') 96 | throw std::runtime_error("Color needs to begin with #"); 97 | unsigned long col = strtoul(color.c_str() + 1, NULL, 16); 98 | u8 b, g, r; 99 | b = col & 0xff; 100 | g = (col >> 8) & 0xff; 101 | r = (col >> 16) & 0xff; 102 | return Color(r, g, b); 103 | } 104 | 105 | static Color mixColors(Color a, Color b) 106 | { 107 | Color result; 108 | float a1 = a.a / 255.0f; 109 | float a2 = b.a / 255.0f; 110 | 111 | result.r = (int) (a1 * a.r + a2 * (1 - a1) * b.r); 112 | result.g = (int) (a1 * a.g + a2 * (1 - a1) * b.g); 113 | result.b = (int) (a1 * a.b + a2 * (1 - a1) * b.b); 114 | result.a = (int) (255 * (a1 + a2 * (1 - a1))); 115 | 116 | return result; 117 | } 118 | 119 | TileGenerator::TileGenerator(): 120 | m_bgColor(255, 255, 255), 121 | m_scaleColor(0, 0, 0), 122 | m_originColor(255, 0, 0), 123 | m_playerColor(255, 0, 0), 124 | m_drawOrigin(false), 125 | m_drawPlayers(false), 126 | m_drawScale(false), 127 | m_drawAlpha(false), 128 | m_shading(true), 129 | m_dontWriteEmpty(false), 130 | m_backend(""), 131 | m_xBorder(0), 132 | m_yBorder(0), 133 | m_db(NULL), 134 | m_image(NULL), 135 | m_xMin(INT_MAX), 136 | m_xMax(INT_MIN), 137 | m_zMin(INT_MAX), 138 | m_zMax(INT_MIN), 139 | m_yMin(INT16_MIN), 140 | m_yMax(INT16_MAX), 141 | m_geomX(-2048), 142 | m_geomY(-2048), 143 | m_geomX2(2048), 144 | m_geomY2(2048), 145 | m_exhaustiveSearch(EXH_AUTO), 146 | m_renderedAny(false), 147 | m_zoom(1), 148 | m_scales(SCALE_LEFT | SCALE_TOP), 149 | m_progressMax(0), 150 | m_progressLast(-1) 151 | { 152 | } 153 | 154 | TileGenerator::~TileGenerator() 155 | { 156 | closeDatabase(); 157 | delete m_image; 158 | m_image = nullptr; 159 | } 160 | 161 | void TileGenerator::setBgColor(const std::string &bgColor) 162 | { 163 | m_bgColor = parseColor(bgColor); 164 | } 165 | 166 | void TileGenerator::setScaleColor(const std::string &scaleColor) 167 | { 168 | m_scaleColor = parseColor(scaleColor); 169 | } 170 | 171 | void TileGenerator::setOriginColor(const std::string &originColor) 172 | { 173 | m_originColor = parseColor(originColor); 174 | } 175 | 176 | void TileGenerator::setPlayerColor(const std::string &playerColor) 177 | { 178 | m_playerColor = parseColor(playerColor); 179 | } 180 | 181 | void TileGenerator::setZoom(int zoom) 182 | { 183 | if (zoom < 1) 184 | throw std::runtime_error("Zoom level needs to be a number: 1 or higher"); 185 | m_zoom = zoom; 186 | } 187 | 188 | void TileGenerator::setScales(uint flags) 189 | { 190 | m_scales = flags; 191 | } 192 | 193 | void TileGenerator::setDrawOrigin(bool drawOrigin) 194 | { 195 | m_drawOrigin = drawOrigin; 196 | } 197 | 198 | void TileGenerator::setDrawPlayers(bool drawPlayers) 199 | { 200 | m_drawPlayers = drawPlayers; 201 | } 202 | 203 | void TileGenerator::setDrawScale(bool drawScale) 204 | { 205 | m_drawScale = drawScale; 206 | } 207 | 208 | void TileGenerator::setDrawAlpha(bool drawAlpha) 209 | { 210 | m_drawAlpha = drawAlpha; 211 | } 212 | 213 | void TileGenerator::setShading(bool shading) 214 | { 215 | m_shading = shading; 216 | } 217 | 218 | void TileGenerator::setBackend(std::string backend) 219 | { 220 | m_backend = backend; 221 | } 222 | 223 | void TileGenerator::setGeometry(int x, int y, int w, int h) 224 | { 225 | assert(w > 0 && h > 0); 226 | m_geomX = round_multiple_nosign(x, 16) / 16; 227 | m_geomY = round_multiple_nosign(y, 16) / 16; 228 | m_geomX2 = round_multiple_nosign(x + w, 16) / 16; 229 | m_geomY2 = round_multiple_nosign(y + h, 16) / 16; 230 | } 231 | 232 | void TileGenerator::setMinY(int y) 233 | { 234 | m_yMin = y; 235 | if (m_yMin > m_yMax) 236 | std::swap(m_yMin, m_yMax); 237 | } 238 | 239 | void TileGenerator::setMaxY(int y) 240 | { 241 | m_yMax = y; 242 | if (m_yMin > m_yMax) 243 | std::swap(m_yMin, m_yMax); 244 | } 245 | 246 | void TileGenerator::setExhaustiveSearch(int mode) 247 | { 248 | m_exhaustiveSearch = mode; 249 | } 250 | 251 | void TileGenerator::setDontWriteEmpty(bool f) 252 | { 253 | m_dontWriteEmpty = f; 254 | } 255 | 256 | void TileGenerator::parseColorsFile(const std::string &fileName) 257 | { 258 | std::ifstream in(fileName); 259 | if (!in.good()) 260 | throw std::runtime_error("Specified colors file could not be found"); 261 | verbosestream << "Parsing colors.txt: " << fileName << std::endl; 262 | parseColorsStream(in); 263 | } 264 | 265 | void TileGenerator::printGeometry(const std::string &input_path) 266 | { 267 | setExhaustiveSearch(EXH_NEVER); 268 | openDb(input_path); 269 | loadBlocks(); 270 | 271 | std::cout << "Map extent: " 272 | << m_xMin*16 << ":" << m_zMin*16 273 | << "+" << (m_xMax - m_xMin+1)*16 274 | << "+" << (m_zMax - m_zMin+1)*16 275 | << std::endl; 276 | 277 | closeDatabase(); 278 | } 279 | 280 | void TileGenerator::dumpBlock(const std::string &input_path, BlockPos pos) 281 | { 282 | openDb(input_path); 283 | 284 | BlockList list; 285 | std::vector positions; 286 | positions.emplace_back(pos); 287 | m_db->getBlocksByPos(list, positions); 288 | if (!list.empty()) { 289 | const ustring &data = list.begin()->second; 290 | for (u8 c : data) 291 | printf("%02x", static_cast(c)); 292 | printf("\n"); 293 | } 294 | 295 | closeDatabase(); 296 | } 297 | 298 | void TileGenerator::generate(const std::string &input_path, const std::string &output) 299 | { 300 | openDb(input_path); 301 | loadBlocks(); 302 | 303 | // If we needed to load positions and there are none, that means the 304 | // result will be empty. 305 | if (m_dontWriteEmpty && (m_exhaustiveSearch == EXH_NEVER || 306 | m_exhaustiveSearch == EXH_Y) && m_positions.empty()) { 307 | verbosestream << "Result is empty (no positions)" << std::endl; 308 | return; 309 | } 310 | 311 | createImage(); 312 | renderMap(); 313 | 314 | if (m_dontWriteEmpty && !m_renderedAny) { 315 | verbosestream << "Result is empty (no pixels)" << std::endl; 316 | printUnknown(); 317 | return; 318 | } 319 | 320 | closeDatabase(); 321 | if (m_drawScale) { 322 | renderScale(); 323 | } 324 | if (m_drawOrigin) { 325 | renderOrigin(); 326 | } 327 | if (m_drawPlayers) { 328 | renderPlayers(input_path); 329 | } 330 | writeImage(output); 331 | printUnknown(); 332 | } 333 | 334 | void TileGenerator::parseColorsStream(std::istream &in) 335 | { 336 | char line[512]; 337 | while (in.good()) { 338 | in.getline(line, sizeof(line)); 339 | 340 | for (char *p = line; *p; p++) { 341 | if (*p != '#') 342 | continue; 343 | *p = '\0'; // Cut off at the first # 344 | break; 345 | } 346 | if(!line[0]) 347 | continue; 348 | 349 | char name[200 + 1] = {0}; 350 | unsigned int r, g, b, a = 255, t = 0; 351 | int items = sscanf(line, "%200s %u %u %u %u %u", name, &r, &g, &b, &a, &t); 352 | if (items < 4) { 353 | errorstream << "Failed to parse color entry '" << line << "'" << std::endl; 354 | continue; 355 | } 356 | 357 | m_colorMap[name] = ColorEntry(r, g, b, a, t); 358 | } 359 | } 360 | 361 | std::set TileGenerator::getSupportedBackends() 362 | { 363 | std::set r; 364 | r.insert("sqlite3"); 365 | #if USE_POSTGRESQL 366 | r.insert("postgresql"); 367 | #endif 368 | #if USE_LEVELDB 369 | r.insert("leveldb"); 370 | #endif 371 | #if USE_REDIS 372 | r.insert("redis"); 373 | #endif 374 | return r; 375 | } 376 | 377 | void TileGenerator::openDb(const std::string &input_path) 378 | { 379 | if (dir_exists(input_path.c_str())) { 380 | // ok 381 | } else if (file_exists(input_path.c_str())) { 382 | throw std::runtime_error("Input path is a file, it should point to the world folder instead"); 383 | } else { 384 | throw std::runtime_error("Input path does not exist"); 385 | } 386 | 387 | std::string input = input_path; 388 | if (input.back() != PATH_SEPARATOR) 389 | input += PATH_SEPARATOR; 390 | 391 | std::ifstream ifs(input + "world.mt"); 392 | 393 | std::string backend = m_backend; 394 | if (backend.empty() && !ifs.good()) { 395 | throw std::runtime_error("Failed to open world.mt"); 396 | } else if (backend.empty()) { 397 | backend = read_setting_default("backend", ifs, "sqlite3"); 398 | } 399 | 400 | if (backend == "dummy") { 401 | throw std::runtime_error("This map uses the dummy backend and contains no data"); 402 | } else if (backend == "sqlite3") { 403 | m_db = new DBSQLite3(input); 404 | #if USE_POSTGRESQL 405 | } else if (backend == "postgresql") { 406 | m_db = new DBPostgreSQL(input); 407 | #endif 408 | #if USE_LEVELDB 409 | } else if (backend == "leveldb") { 410 | m_db = new DBLevelDB(input); 411 | #endif 412 | #if USE_REDIS 413 | } else if (backend == "redis") { 414 | m_db = new DBRedis(input); 415 | #endif 416 | } else { 417 | throw std::runtime_error(std::string("Unknown map backend: ") + backend); 418 | } 419 | 420 | if (!read_setting_default("readonly_backend", ifs, "").empty()) { 421 | errorstream << "Warning: Map with readonly_backend is not supported. " 422 | "The result may be incomplete." << std::endl; 423 | } 424 | 425 | // Determine how we're going to traverse the database (heuristic) 426 | if (m_exhaustiveSearch == EXH_AUTO) { 427 | size_t y_range = (m_yMax / 16 + 1) - (m_yMin / 16); 428 | size_t blocks = sat_mul(m_geomX2 - m_geomX, y_range, m_geomY2 - m_geomY); 429 | verbosestream << "Heuristic parameters:" 430 | << " preferRangeQueries()=" << m_db->preferRangeQueries() 431 | << " y_range=" << y_range << " blocks=" << blocks << std::endl; 432 | if (m_db->preferRangeQueries()) 433 | m_exhaustiveSearch = EXH_NEVER; 434 | else if (blocks < 200000) 435 | m_exhaustiveSearch = EXH_FULL; 436 | else if (y_range < 42) 437 | m_exhaustiveSearch = EXH_Y; 438 | else 439 | m_exhaustiveSearch = EXH_NEVER; 440 | } else if (m_exhaustiveSearch == EXH_FULL || m_exhaustiveSearch == EXH_Y) { 441 | if (m_db->preferRangeQueries()) { 442 | errorstream << "Note: The current database backend supports efficient " 443 | "range queries, forcing exhaustive search will generally result " 444 | "in worse performance." << std::endl; 445 | } 446 | } 447 | assert(m_exhaustiveSearch != EXH_AUTO); 448 | } 449 | 450 | void TileGenerator::closeDatabase() 451 | { 452 | delete m_db; 453 | m_db = NULL; 454 | } 455 | 456 | static inline int16_t mod16(int16_t y) 457 | { 458 | if (y < 0) 459 | return (y - 15) / 16; 460 | return y / 16; 461 | } 462 | 463 | void TileGenerator::loadBlocks() 464 | { 465 | const int16_t yMax = mod16(m_yMax) + 1; 466 | const int16_t yMin = mod16(m_yMin); 467 | 468 | if (m_exhaustiveSearch == EXH_NEVER || m_exhaustiveSearch == EXH_Y) { 469 | std::vector vec = m_db->getBlockPosXZ( 470 | BlockPos(m_geomX, yMin, m_geomY), 471 | BlockPos(m_geomX2, yMax, m_geomY2) 472 | ); 473 | 474 | for (auto pos : vec) { 475 | assert(pos.x >= m_geomX && pos.x < m_geomX2); 476 | assert(pos.z >= m_geomY && pos.z < m_geomY2); 477 | 478 | // Adjust minimum and maximum positions to the nearest block 479 | m_xMin = mymin(m_xMin, pos.x); 480 | m_xMax = mymax(m_xMax, pos.x); 481 | m_zMin = mymin(m_zMin, pos.z); 482 | m_zMax = mymax(m_zMax, pos.z); 483 | 484 | m_positions[pos.z].emplace(pos.x); 485 | } 486 | 487 | size_t count = 0; 488 | for (const auto &it : m_positions) 489 | count += it.second.size(); 490 | m_progressMax = count; 491 | verbosestream << "Loaded " << count 492 | << " positions (across Z: " << m_positions.size() << ") for rendering" << std::endl; 493 | } 494 | } 495 | 496 | void TileGenerator::createImage() 497 | { 498 | const int scale_d = 40; // pixels reserved for a scale 499 | if(!m_drawScale) 500 | m_scales = 0; 501 | 502 | // If a geometry is explicitly set, set the bounding box to the requested geometry 503 | // instead of cropping to the content. This way we will always output a full tile 504 | // of the correct size. 505 | if (m_geomX > -2048 && m_geomX2 < 2048) 506 | { 507 | m_xMin = m_geomX; 508 | m_xMax = m_geomX2-1; 509 | } 510 | 511 | if (m_geomY > -2048 && m_geomY2 < 2048) 512 | { 513 | m_zMin = m_geomY; 514 | m_zMax = m_geomY2-1; 515 | } 516 | 517 | m_mapWidth = (m_xMax - m_xMin + 1) * 16; 518 | m_mapHeight = (m_zMax - m_zMin + 1) * 16; 519 | 520 | m_xBorder = (m_scales & SCALE_LEFT) ? scale_d : 0; 521 | m_yBorder = (m_scales & SCALE_TOP) ? scale_d : 0; 522 | m_blockPixelAttributes.setWidth(m_mapWidth); 523 | 524 | int image_width, image_height; 525 | image_width = (m_mapWidth * m_zoom) + m_xBorder; 526 | image_width += (m_scales & SCALE_RIGHT) ? scale_d : 0; 527 | image_height = (m_mapHeight * m_zoom) + m_yBorder; 528 | image_height += (m_scales & SCALE_BOTTOM) ? scale_d : 0; 529 | 530 | if(image_width > 4096 || image_height > 4096) { 531 | errorstream << "Warning: The side length of the image to be created exceeds 4096 pixels!" 532 | << " (dimensions: " << image_width << "x" << image_height << ")" 533 | << std::endl; 534 | } else { 535 | verbosestream << "Creating image with size " << image_width << "x" << image_height 536 | << std::endl; 537 | } 538 | m_image = new Image(image_width, image_height); 539 | m_image->drawFilledRect(0, 0, image_width, image_height, m_bgColor); // Background 540 | } 541 | 542 | void TileGenerator::renderMap() 543 | { 544 | BlockDecoder blk; 545 | const int16_t yMax = mod16(m_yMax) + 1; 546 | const int16_t yMin = mod16(m_yMin); 547 | size_t bTotal = 0, bRender = 0, bEmpty = 0; 548 | 549 | // returns true to skip 550 | auto decode = [&] (BlockPos pos, const ustring &buf) -> bool { 551 | blk.reset(); 552 | try { 553 | blk.decode(buf); 554 | } catch (std::exception &e) { 555 | errorstream << "While decoding block " << pos.x << ',' << pos.y << ',' << pos.z 556 | << ':' << std::endl; 557 | throw; 558 | }; 559 | return blk.isEmpty(); 560 | }; 561 | 562 | auto renderSingle = [&] (int16_t xPos, int16_t zPos, BlockList &blockStack) { 563 | if (blockStack.empty()) 564 | return; 565 | 566 | m_readPixels.reset(); 567 | m_readInfo.reset(); 568 | for (int i = 0; i < 16; i++) { 569 | for (int j = 0; j < 16; j++) { 570 | 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 571 | m_color[i][j].a = 0; // ..but set alpha to 0 to tell renderMapBlock() not to use this color to mix a shade 572 | m_thickness[i][j] = 0; 573 | } 574 | } 575 | 576 | bTotal += blockStack.size(); 577 | for (const auto &it : blockStack) { 578 | const BlockPos pos = it.first; 579 | assert(pos.x == xPos && pos.z == zPos); 580 | assert(pos.y >= yMin && pos.y < yMax); 581 | 582 | if (decode(pos, it.second)) { 583 | bEmpty++; 584 | continue; 585 | } 586 | bRender++; 587 | renderMapBlock(blk, pos); 588 | 589 | // Exit out if all pixels for this MapBlock are covered 590 | if (m_readPixels.full()) 591 | break; 592 | } 593 | if (!m_readPixels.full()) 594 | renderMapBlockBottom(blockStack.begin()->first); 595 | m_renderedAny |= m_readInfo.any(); 596 | }; 597 | auto postRenderRow = [&] (int16_t zPos) { 598 | if (m_shading) 599 | renderShading(zPos); 600 | }; 601 | 602 | size_t count = 0; // fraction of m_progressMax 603 | if (m_exhaustiveSearch == EXH_NEVER) { 604 | for (auto it = m_positions.rbegin(); it != m_positions.rend(); ++it) { 605 | int16_t zPos = it->first; 606 | for (auto it2 = it->second.rbegin(); it2 != it->second.rend(); ++it2) { 607 | int16_t xPos = *it2; 608 | 609 | BlockList blockStack; 610 | m_db->getBlocksOnXZ(blockStack, xPos, zPos, yMin, yMax); 611 | blockStack.sort(); 612 | 613 | renderSingle(xPos, zPos, blockStack); 614 | reportProgress(count++); 615 | } 616 | postRenderRow(zPos); 617 | } 618 | } else if (m_exhaustiveSearch == EXH_Y) { 619 | verbosestream << "Exhaustively searching height of " 620 | << (yMax - yMin) << " blocks" << std::endl; 621 | std::vector positions; 622 | positions.reserve(yMax - yMin); 623 | for (auto it = m_positions.rbegin(); it != m_positions.rend(); ++it) { 624 | int16_t zPos = it->first; 625 | for (auto it2 = it->second.rbegin(); it2 != it->second.rend(); ++it2) { 626 | int16_t xPos = *it2; 627 | 628 | positions.clear(); 629 | for (int16_t yPos = yMin; yPos < yMax; yPos++) 630 | positions.emplace_back(xPos, yPos, zPos); 631 | 632 | BlockList blockStack; 633 | m_db->getBlocksByPos(blockStack, positions); 634 | blockStack.sort(); 635 | 636 | renderSingle(xPos, zPos, blockStack); 637 | reportProgress(count++); 638 | } 639 | postRenderRow(zPos); 640 | } 641 | } else if (m_exhaustiveSearch == EXH_FULL) { 642 | const size_t span_y = yMax - yMin; 643 | m_progressMax = (m_geomX2 - m_geomX) * span_y * (m_geomY2 - m_geomY); 644 | verbosestream << "Exhaustively searching " 645 | << (m_geomX2 - m_geomX) << "x" << span_y << "x" 646 | << (m_geomY2 - m_geomY) << " blocks" << std::endl; 647 | 648 | std::vector positions; 649 | positions.reserve(span_y); 650 | for (int16_t zPos = m_geomY2 - 1; zPos >= m_geomY; zPos--) { 651 | for (int16_t xPos = m_geomX2 - 1; xPos >= m_geomX; xPos--) { 652 | positions.clear(); 653 | for (int16_t yPos = yMin; yPos < yMax; yPos++) 654 | positions.emplace_back(xPos, yPos, zPos); 655 | 656 | BlockList blockStack; 657 | m_db->getBlocksByPos(blockStack, positions); 658 | blockStack.sort(); 659 | 660 | renderSingle(xPos, zPos, blockStack); 661 | reportProgress(count++); 662 | } 663 | postRenderRow(zPos); 664 | } 665 | } 666 | 667 | reportProgress(m_progressMax); 668 | verbosestream << "Block stats: " << bTotal << " total, " << bRender 669 | << " rendered, " << bEmpty << " empty" << std::endl; 670 | } 671 | 672 | void TileGenerator::renderMapBlock(const BlockDecoder &blk, const BlockPos &pos) 673 | { 674 | int xBegin = (pos.x - m_xMin) * 16; 675 | int zBegin = (m_zMax - pos.z) * 16; 676 | int minY = (pos.y * 16 > m_yMin) ? 0 : m_yMin - pos.y * 16; 677 | int maxY = (pos.y * 16 + 15 < m_yMax) ? 15 : m_yMax - pos.y * 16; 678 | for (int z = 0; z < 16; ++z) { 679 | int imageY = zBegin + 15 - z; 680 | for (int x = 0; x < 16; ++x) { 681 | if (m_readPixels.get(x, z)) 682 | continue; 683 | int imageX = xBegin + x; 684 | auto &attr = m_blockPixelAttributes.attribute(15 - z, xBegin + x); 685 | 686 | for (int y = maxY; y >= minY; --y) { 687 | const std::string &name = blk.getNode(x, y, z); 688 | if (name.empty()) 689 | continue; 690 | ColorMap::const_iterator it = m_colorMap.find(name); 691 | if (it == m_colorMap.end()) { 692 | m_unknownNodes.insert(name); 693 | continue; 694 | } 695 | 696 | Color c = it->second.toColor(); 697 | if (c.a == 0) 698 | continue; // node is fully invisible 699 | if (m_drawAlpha) { 700 | if (m_color[z][x].a != 0) 701 | c = mixColors(m_color[z][x], c); 702 | if (c.a < 255) { 703 | // remember color and near thickness value 704 | m_color[z][x] = c; 705 | m_thickness[z][x] = (m_thickness[z][x] + it->second.t) / 2; 706 | continue; 707 | } 708 | // color became opaque, draw it 709 | setZoomed(imageX, imageY, c); 710 | attr.thickness = m_thickness[z][x]; 711 | } else { 712 | c.a = 255; 713 | setZoomed(imageX, imageY, c); 714 | } 715 | m_readPixels.set(x, z); 716 | 717 | // do this afterwards so we can record height values 718 | // inside transparent nodes (water) too 719 | if (!m_readInfo.get(x, z)) { 720 | attr.height = pos.y * 16 + y; 721 | m_readInfo.set(x, z); 722 | } 723 | break; 724 | } 725 | } 726 | } 727 | } 728 | 729 | void TileGenerator::renderMapBlockBottom(const BlockPos &pos) 730 | { 731 | if (!m_drawAlpha) 732 | return; // "missing" pixels can only happen with --drawalpha 733 | 734 | int xBegin = (pos.x - m_xMin) * 16; 735 | int zBegin = (m_zMax - pos.z) * 16; 736 | for (int z = 0; z < 16; ++z) { 737 | int imageY = zBegin + 15 - z; 738 | for (int x = 0; x < 16; ++x) { 739 | if (m_readPixels.get(x, z)) 740 | continue; 741 | int imageX = xBegin + x; 742 | auto &attr = m_blockPixelAttributes.attribute(15 - z, xBegin + x); 743 | 744 | // set color since it wasn't done in renderMapBlock() 745 | setZoomed(imageX, imageY, m_color[z][x]); 746 | m_readPixels.set(x, z); 747 | attr.thickness = m_thickness[z][x]; 748 | } 749 | } 750 | } 751 | 752 | void TileGenerator::renderShading(int zPos) 753 | { 754 | auto &a = m_blockPixelAttributes; 755 | int zBegin = (m_zMax - zPos) * 16; 756 | for (int z = 0; z < 16; ++z) { 757 | int imageY = zBegin + z; 758 | if (imageY >= m_mapHeight) 759 | continue; 760 | for (int x = 0; x < m_mapWidth; ++x) { 761 | if( 762 | !a.attribute(z, x).valid_height() || 763 | !a.attribute(z, x - 1).valid_height() || 764 | !a.attribute(z - 1, x).valid_height() 765 | ) 766 | continue; 767 | 768 | // calculate shadow to apply 769 | int y = a.attribute(z, x).height; 770 | int y1 = a.attribute(z, x - 1).height; 771 | int y2 = a.attribute(z - 1, x).height; 772 | int d = ((y - y1) + (y - y2)) * 12; 773 | 774 | if (m_drawAlpha) { // less visible shadow with increasing "thickness" 775 | float t = a.attribute(z, x).thickness * 1.2f; 776 | t = mymin(t, 255.0f); 777 | d *= 1.0f - t / 255.0f; 778 | } 779 | 780 | d = mymin(d, 36); 781 | 782 | // apply shadow/light by just adding to it pixel values 783 | Color c = m_image->getPixel(getImageX(x), getImageY(imageY)); 784 | c.r = colorSafeBounds(c.r + d); 785 | c.g = colorSafeBounds(c.g + d); 786 | c.b = colorSafeBounds(c.b + d); 787 | setZoomed(x, imageY, c); 788 | } 789 | } 790 | a.scroll(); 791 | } 792 | 793 | void TileGenerator::renderScale() 794 | { 795 | const int scale_d = 40; // see createImage() 796 | 797 | if (m_scales & SCALE_TOP) { 798 | m_image->drawText(24, 0, "X", m_scaleColor); 799 | for (int i = (m_xMin / 4) * 4; i <= m_xMax; i += 4) { 800 | std::ostringstream buf; 801 | buf << i * 16; 802 | 803 | int xPos = getImageX(i * 16, true); 804 | if (xPos >= 0) { 805 | m_image->drawText(xPos + 2, 0, buf.str(), m_scaleColor); 806 | m_image->drawLine(xPos, 0, xPos, m_yBorder - 1, m_scaleColor); 807 | } 808 | } 809 | } 810 | 811 | if (m_scales & SCALE_LEFT) { 812 | m_image->drawText(2, 24, "Z", m_scaleColor); 813 | for (int i = (m_zMax / 4) * 4; i >= m_zMin; i -= 4) { 814 | std::ostringstream buf; 815 | buf << i * 16; 816 | 817 | int yPos = getImageY(i * 16 + 1, true); 818 | if (yPos >= 0) { 819 | m_image->drawText(2, yPos, buf.str(), m_scaleColor); 820 | m_image->drawLine(0, yPos, m_xBorder - 1, yPos, m_scaleColor); 821 | } 822 | } 823 | } 824 | 825 | if (m_scales & SCALE_BOTTOM) { 826 | int xPos = m_xBorder + m_mapWidth*m_zoom - 24 - 8, 827 | yPos = m_yBorder + m_mapHeight*m_zoom + scale_d - 12; 828 | m_image->drawText(xPos, yPos, "X", m_scaleColor); 829 | for (int i = (m_xMin / 4) * 4; i <= m_xMax; i += 4) { 830 | std::ostringstream buf; 831 | buf << i * 16; 832 | 833 | xPos = getImageX(i * 16, true); 834 | yPos = m_yBorder + m_mapHeight*m_zoom; 835 | if (xPos >= 0) { 836 | m_image->drawText(xPos + 2, yPos, buf.str(), m_scaleColor); 837 | m_image->drawLine(xPos, yPos, xPos, yPos + 39, m_scaleColor); 838 | } 839 | } 840 | } 841 | 842 | if (m_scales & SCALE_RIGHT) { 843 | int xPos = m_xBorder + m_mapWidth*m_zoom + scale_d - 2 - 8, 844 | yPos = m_yBorder + m_mapHeight*m_zoom - 24 - 12; 845 | m_image->drawText(xPos, yPos, "Z", m_scaleColor); 846 | for (int i = (m_zMax / 4) * 4; i >= m_zMin; i -= 4) { 847 | std::ostringstream buf; 848 | buf << i * 16; 849 | 850 | xPos = m_xBorder + m_mapWidth*m_zoom; 851 | yPos = getImageY(i * 16 + 1, true); 852 | if (yPos >= 0) { 853 | m_image->drawText(xPos + 2, yPos, buf.str(), m_scaleColor); 854 | m_image->drawLine(xPos, yPos, xPos + 39, yPos, m_scaleColor); 855 | } 856 | } 857 | } 858 | } 859 | 860 | void TileGenerator::renderOrigin() 861 | { 862 | if (m_xMin > 0 || m_xMax < 0 || 863 | m_zMin > 0 || m_zMax < 0) 864 | return; 865 | m_image->drawCircle(getImageX(0, true), getImageY(0, true), 12, m_originColor); 866 | } 867 | 868 | void TileGenerator::renderPlayers(const std::string &input_path) 869 | { 870 | std::string input = input_path; 871 | if (input.back() != PATH_SEPARATOR) 872 | input += PATH_SEPARATOR; 873 | 874 | PlayerAttributes players(input); 875 | for (auto &player : players) { 876 | if (player.x < m_xMin * 16 || player.x >= (m_xMax+1) * 16 || 877 | player.z < m_zMin * 16 || player.z >= (m_zMax+1) * 16) 878 | continue; 879 | if (player.y < m_yMin || player.y > m_yMax) 880 | continue; 881 | int imageX = getImageX(player.x, true), 882 | imageY = getImageY(player.z, true); 883 | 884 | m_image->drawFilledRect(imageX - 1, imageY, 3, 1, m_playerColor); 885 | m_image->drawFilledRect(imageX, imageY - 1, 1, 3, m_playerColor); 886 | assert(!player.name.empty()); 887 | m_image->drawText(imageX + 2, imageY, player.name, m_playerColor); 888 | } 889 | } 890 | 891 | void TileGenerator::writeImage(const std::string &output) 892 | { 893 | m_image->save(output); 894 | delete m_image; 895 | m_image = nullptr; 896 | } 897 | 898 | void TileGenerator::printUnknown() 899 | { 900 | if (m_unknownNodes.empty()) 901 | return; 902 | errorstream << "Unknown nodes:\n"; 903 | for (const auto &node : m_unknownNodes) 904 | errorstream << "\t" << node << '\n'; 905 | if (!m_renderedAny) { 906 | errorstream << "The map was read successfully and not empty, but none of the " 907 | "encountered nodes had a color associated.\nCheck that you're using " 908 | "the right colors.txt. It should match the game you have installed.\n"; 909 | } 910 | errorstream << std::flush; 911 | } 912 | 913 | void TileGenerator::reportProgress(size_t count) 914 | { 915 | if (!m_progressMax) 916 | return; 917 | int percent = count / static_cast(m_progressMax) * 100; 918 | if (percent == m_progressLast) 919 | return; 920 | m_progressLast = percent; 921 | 922 | // Print a nice-looking ASCII progress bar 923 | char bar[51] = {0}; 924 | memset(bar, ' ', 50); 925 | int i = 0, j = percent; 926 | for (; j >= 2; j -= 2) 927 | bar[i++] = '='; 928 | if (j) 929 | bar[i++] = '-'; 930 | std::cout << "[" << bar << "] " << percent << "% " << (percent == 100 ? "\n" : "\r"); 931 | std::cout.flush(); 932 | } 933 | 934 | inline int TileGenerator::getImageX(int val, bool absolute) const 935 | { 936 | if (absolute) 937 | val = (val - m_xMin * 16); 938 | return (m_zoom*val) + m_xBorder; 939 | } 940 | 941 | inline int TileGenerator::getImageY(int val, bool absolute) const 942 | { 943 | if (absolute) 944 | val = m_mapHeight - (val - m_zMin * 16); // Z axis is flipped on image 945 | return (m_zoom*val) + m_yBorder; 946 | } 947 | 948 | inline void TileGenerator::setZoomed(int x, int y, Color color) 949 | { 950 | m_image->drawFilledRect(getImageX(x), getImageY(y), m_zoom, m_zoom, color); 951 | } 952 | --------------------------------------------------------------------------------