Compare commits

..

No commits in common. "master" and "20170606" have entirely different histories.

72 changed files with 2795 additions and 4713 deletions

View File

@ -1,16 +0,0 @@
.git
.github
*~
minetestmapper
minetestmapper.exe
CMakeCache.txt
CMakeFiles/
CPack*.cmake
_CPack_Packages/
install_manifest.txt
Makefile
cmake_install.cmake
cmake_config.h

View File

@ -1,85 +0,0 @@
name: build
# build on source or workflow changes
on:
push:
paths:
- '**.[ch]'
- '**.cpp'
- '**/CMakeLists.txt'
- 'util/ci/**'
- '.github/workflows/**.yml'
pull_request:
paths:
- '**.[ch]'
- '**.cpp'
- '**/CMakeLists.txt'
- 'util/ci/**'
- '.github/workflows/**.yml'
jobs:
gcc:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install deps
run: |
source util/ci/script.sh
install_linux_deps
- name: Build
run: |
source util/ci/script.sh
run_build
env:
CC: gcc
CXX: g++
- name: Test
run: |
./util/ci/test.sh
- name: Test Install
run: |
make DESTDIR=/tmp/install install
clang:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Install deps
run: |
source util/ci/script.sh
install_linux_deps
- name: Build
run: |
source util/ci/script.sh
run_build
env:
CC: clang
CXX: clang++
- name: Test
run: |
./util/ci/test.sh
gcc_fedora:
runs-on: ubuntu-latest
container:
image: fedora:latest
steps:
- uses: actions/checkout@v4
- name: Install deps
run: |
source util/ci/script.sh
install_linux_deps
- name: Build
run: |
source util/ci/script.sh
run_build
- name: Test
run: |
./util/ci/test.sh

View File

@ -1,87 +0,0 @@
---
name: docker_image
# https://docs.github.com/en/actions/publishing-packages/publishing-docker-images
# https://docs.docker.com/build/ci/github-actions/multi-platform
# https://github.com/opencontainers/image-spec/blob/main/annotations.md
on:
push:
branches: [ "master" ]
# Publish semver tags as releases.
tags: [ "*" ]
pull_request:
# Build docker image on pull requests. (but do not publish)
paths:
- '**/**.[ch]'
- '**/**.cpp'
workflow_dispatch:
inputs:
use_cache:
description: "Use build cache"
required: true
type: boolean
default: true
env:
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v3.0.0
# Login against the Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@v3.0.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5.5.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
labels: |
org.opencontainers.image.title=Minetestmapper
org.opencontainers.image.vendor=Luanti
org.opencontainers.image.licenses=BSD 2-Clause
# Build and push Docker image
# https://github.com/docker/build-push-action
# No arm support for now. Require cross-compilation support in Dockerfile to not use QEMU.
- name: Build and push Docker image
uses: docker/build-push-action@v5.1.0
with:
context: .
platforms: linux/amd64
push: ${{ github.event_name != 'pull_request' }}
load: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
no-cache: ${{ (github.event_name == 'workflow_dispatch' && !inputs.use_cache) || startsWith(github.ref, 'refs/tags/') }}
- name: Test Docker Image
run: |
docker run --rm $(cut -d, -f1 <<<"$DOCKER_METADATA_OUTPUT_TAGS") minetestmapper --help
shell: bash

16
.gitignore vendored
View File

@ -1,16 +1,10 @@
*~ colors.txt
minetestmapper
/minetestmapper minetestmapper.exe
/minetestmapper.exe
/colors.txt
CMakeCache.txt CMakeCache.txt
CMakeFiles/ CMakeFiles/
CPack*.cmake CPack*
_CPack_Packages/
install_manifest.txt
Makefile Makefile
cmake_install.cmake cmake_install.cmake
cmake_config.h cmake_config.h
compile_commands.json *~
.vscode/

26
.travis.yml Normal file
View File

@ -0,0 +1,26 @@
language: cpp
compiler:
- gcc
- clang
sudo: false
addons:
apt:
sources:
- llvm-toolchain-precise-3.8
- ubuntu-toolchain-r-test
packages:
- cmake
- libgd2-noxpm-dev
- libsqlite3-dev
- p7zip
- g++-6
- clang-3.8
before_install:
- # Nothing ever works correctly with precise, use a custom libleveldb build
- wget http://minetest.kitsunemimi.pw/libleveldb-1.18-ubuntu12.04.7z
- 7zr x -olibleveldb libleveldb-1.18-ubuntu12.04.7z
script: ./util/travis/script.sh
notifications:
email: false
matrix:
fast_finish: true

View File

@ -1,9 +1,11 @@
cmake_minimum_required(VERSION 3.5) project(minetestmapper CXX)
cmake_minimum_required(VERSION 2.6)
cmake_policy(SET CMP0003 NEW)
set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
project(minetestmapper set(VERSION_MAJOR 1)
VERSION 1.0 set(VERSION_MINOR 0)
LANGUAGES CXX set(VERSION_STRING "${VERSION_MAJOR}.${VERSION_MINOR}")
)
# Stuff & Paths # Stuff & Paths
@ -11,18 +13,18 @@ if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build." FORCE) set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build." FORCE)
endif() endif()
set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_FLAGS_RELEASE "-O3 -Wall")
set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g2 -Wall")
if(WIN32) if(WIN32)
set(SHAREDIR ".") set(SHAREDIR ".")
set(BINDIR ".") set(BINDIR ".")
set(DOCDIR ".") set(DOCDIR ".")
else() else()
set(SHAREDIR "share/luanti") # reuse engine share dir set(SHAREDIR "${CMAKE_INSTALL_PREFIX}/share/minetest") # reuse Minetest share dir
set(BINDIR "bin") set(BINDIR "${CMAKE_INSTALL_PREFIX}/bin")
set(DOCDIR "share/doc/${PROJECT_NAME}") set(DOCDIR "${CMAKE_INSTALL_PREFIX}/share/doc/${PROJECT_NAME}")
set(MANDIR "share/man") set(MANDIR "${CMAKE_INSTALL_PREFIX}/share/man")
endif() endif()
set(CUSTOM_SHAREDIR "" CACHE STRING "Directory to install data files into") set(CUSTOM_SHAREDIR "" CACHE STRING "Directory to install data files into")
@ -43,7 +45,6 @@ if(NOT CUSTOM_DOCDIR STREQUAL "")
message(STATUS "Using DOCDIR=${DOCDIR}") message(STATUS "Using DOCDIR=${DOCDIR}")
endif() endif()
list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake")
# Libraries: gd # Libraries: gd
@ -57,26 +58,21 @@ endif(NOT LIBGD_LIBRARY OR NOT LIBGD_INCLUDE_DIR)
# Libraries: zlib # Libraries: zlib
find_package(zlib-ng QUIET) find_library(ZLIB_LIBRARY z)
if(zlib-ng_FOUND) find_path(ZLIB_INCLUDE_DIR zlib.h)
set(ZLIB_INCLUDE_DIR zlib-ng::zlib) message (STATUS "zlib library: ${ZLIB_LIBRARY}")
set(ZLIB_LIBRARY zlib-ng::zlib) message (STATUS "zlib headers: ${ZLIB_INCLUDE_DIR}")
set(USE_ZLIB_NG TRUE) if(NOT ZLIB_LIBRARY OR NOT ZLIB_INCLUDE_DIR)
message(STATUS "Found zlib-ng, using it instead of zlib.") message(FATAL_ERROR "zlib not found!")
else() endif(NOT ZLIB_LIBRARY OR NOT ZLIB_INCLUDE_DIR)
message(STATUS "zlib-ng not found, falling back to zlib.")
find_package(ZLIB REQUIRED)
set(USE_ZLIB_NG FALSE)
endif()
# Libraries: zstd find_package(PkgConfig)
include(FindPackageHandleStandardArgs)
find_package(Zstd REQUIRED)
# Libraries: sqlite3 # Libraries: sqlite3
find_library(SQLITE3_LIBRARY sqlite3) find_library(SQLITE3_LIBRARY sqlite3)
find_path(SQLITE3_INCLUDE_DIR sqlite3.h) find_path(SQLITE3_INCLUDE_DIR zlib.h)
message (STATUS "sqlite3 library: ${SQLITE3_LIBRARY}") message (STATUS "sqlite3 library: ${SQLITE3_LIBRARY}")
message (STATUS "sqlite3 headers: ${SQLITE3_INCLUDE_DIR}") message (STATUS "sqlite3 headers: ${SQLITE3_INCLUDE_DIR}")
if(NOT SQLITE3_LIBRARY OR NOT SQLITE3_INCLUDE_DIR) if(NOT SQLITE3_LIBRARY OR NOT SQLITE3_INCLUDE_DIR)
@ -86,133 +82,131 @@ endif(NOT SQLITE3_LIBRARY OR NOT SQLITE3_INCLUDE_DIR)
# Libraries: postgresql # Libraries: postgresql
option(ENABLE_POSTGRESQL "Enable PostgreSQL backend" TRUE) option(ENABLE_POSTGRESQL "Enable PostgreSQL backend" TRUE)
set(USE_POSTGRESQL FALSE) set(USE_POSTGRESQL 0)
if(ENABLE_POSTGRESQL) if(ENABLE_POSTGRESQL)
if(CMAKE_VERSION VERSION_LESS "3.20") find_program(POSTGRESQL_CONFIG_EXECUTABLE pg_config DOC "pg_config")
find_package(PostgreSQL QUIET) find_library(POSTGRESQL_LIBRARY pq)
# Before CMake 3.20 FindPostgreSQL.cmake always looked for server includes if(POSTGRESQL_CONFIG_EXECUTABLE)
# but we don't need them, so continue anyway if only those are missing. execute_process(COMMAND ${POSTGRESQL_CONFIG_EXECUTABLE} --includedir-server
if(PostgreSQL_INCLUDE_DIR AND PostgreSQL_LIBRARY) OUTPUT_VARIABLE POSTGRESQL_SERVER_INCLUDE_DIRS
set(PostgreSQL_FOUND TRUE) OUTPUT_STRIP_TRAILING_WHITESPACE)
set(PostgreSQL_INCLUDE_DIRS ${PostgreSQL_INCLUDE_DIR}) execute_process(COMMAND ${POSTGRESQL_CONFIG_EXECUTABLE}
set(PostgreSQL_LIBRARIES ${PostgreSQL_LIBRARY}) OUTPUT_VARIABLE POSTGRESQL_CLIENT_INCLUDE_DIRS
endif() OUTPUT_STRIP_TRAILING_WHITESPACE)
else() # This variable is case sensitive for the cmake PostgreSQL module
find_package(PostgreSQL) set(PostgreSQL_ADDITIONAL_SEARCH_PATHS ${POSTGRESQL_SERVER_INCLUDE_DIRS} ${POSTGRESQL_CLIENT_INCLUDE_DIRS})
endif() endif()
if(PostgreSQL_FOUND) find_package("PostgreSQL")
set(USE_POSTGRESQL TRUE)
if(POSTGRESQL_FOUND)
set(USE_POSTGRESQL 1)
message(STATUS "PostgreSQL backend enabled") message(STATUS "PostgreSQL backend enabled")
# This variable is case sensitive, don't try to change it to POSTGRESQL_INCLUDE_DIR # This variable is case sensitive, don't try to change it to POSTGRESQL_INCLUDE_DIR
message(STATUS "PostgreSQL includes: ${PostgreSQL_INCLUDE_DIRS}") message(STATUS "PostgreSQL includes: ${PostgreSQL_INCLUDE_DIR}")
include_directories(${PostgreSQL_INCLUDE_DIRS}) include_directories(${PostgreSQL_INCLUDE_DIR})
set(POSTGRESQL_LIBRARY ${PostgreSQL_LIBRARIES})
else() else()
message(STATUS "PostgreSQL not found!") message(STATUS "PostgreSQL not found.")
set(PostgreSQL_LIBRARIES "") set(POSTGRESQL_LIBRARY "")
endif() endif()
endif(ENABLE_POSTGRESQL) endif(ENABLE_POSTGRESQL)
# Libraries: leveldb # Libraries: leveldb
OPTION(ENABLE_LEVELDB "Enable LevelDB backend" TRUE) set(USE_LEVELDB 0)
set(USE_LEVELDB FALSE)
OPTION(ENABLE_LEVELDB "Enable LevelDB backend")
if(ENABLE_LEVELDB) if(ENABLE_LEVELDB)
find_library(LEVELDB_LIBRARY leveldb) find_library(LEVELDB_LIBRARY leveldb)
find_path(LEVELDB_INCLUDE_DIR leveldb/db.h) find_path(LEVELDB_INCLUDE_DIR db.h PATH_SUFFIXES leveldb)
message (STATUS "LevelDB library: ${LEVELDB_LIBRARY}") message (STATUS "LevelDB library: ${LEVELDB_LIBRARY}")
message (STATUS "LevelDB headers: ${LEVELDB_INCLUDE_DIR}") message (STATUS "LevelDB headers: ${LEVELDB_INCLUDE_DIR}")
if(LEVELDB_LIBRARY AND LEVELDB_INCLUDE_DIR) if(LEVELDB_LIBRARY AND LEVELDB_INCLUDE_DIR)
set(USE_LEVELDB TRUE) set(USE_LEVELDB 1)
message(STATUS "LevelDB backend enabled") message(STATUS "LevelDB backend enabled")
include_directories(${LEVELDB_INCLUDE_DIR}) include_directories(${LEVELDB_INCLUDE_DIR})
else() else(LEVELDB_LIBRARY AND LEVELDB_INCLUDE_DIR)
set(USE_LEVELDB 0)
message(STATUS "LevelDB not found!") message(STATUS "LevelDB not found!")
set(LEVELDB_LIBRARY "") endif(LEVELDB_LIBRARY AND LEVELDB_INCLUDE_DIR)
endif()
endif(ENABLE_LEVELDB) endif(ENABLE_LEVELDB)
# Libraries: redis # Libraries: redis
OPTION(ENABLE_REDIS "Enable redis backend" TRUE) set(USE_REDIS 0)
set(USE_REDIS FALSE)
OPTION(ENABLE_REDIS "Enable redis backend")
if(ENABLE_REDIS) if(ENABLE_REDIS)
find_library(REDIS_LIBRARY hiredis) find_library(REDIS_LIBRARY hiredis)
find_path(REDIS_INCLUDE_DIR hiredis/hiredis.h) find_path(REDIS_INCLUDE_DIR hiredis.h PATH_SUFFIXES hiredis)
message (STATUS "redis library: ${REDIS_LIBRARY}") message (STATUS "redis library: ${REDIS_LIBRARY}")
message (STATUS "redis headers: ${REDIS_INCLUDE_DIR}") message (STATUS "redis headers: ${REDIS_INCLUDE_DIR}")
if(REDIS_LIBRARY AND REDIS_INCLUDE_DIR) if(REDIS_LIBRARY AND REDIS_INCLUDE_DIR)
set(USE_REDIS TRUE) set(USE_REDIS 1)
message(STATUS "redis backend enabled") message(STATUS "redis backend enabled")
include_directories(${REDIS_INCLUDE_DIR}) include_directories(${REDIS_INCLUDE_DIR})
else() else(REDIS_LIBRARY AND REDIS_INCLUDE_DIR)
set(USE_REDIS 0)
message(STATUS "redis not found!") message(STATUS "redis not found!")
set(REDIS_LIBRARY "") endif(REDIS_LIBRARY AND REDIS_INCLUDE_DIR)
endif()
endif(ENABLE_REDIS) endif(ENABLE_REDIS)
# Compiling & Linking # Compiling & Linking
configure_file( include_directories(
"${CMAKE_CURRENT_SOURCE_DIR}/src/cmake_config.h.in" "${PROJECT_BINARY_DIR}"
"${CMAKE_CURRENT_BINARY_DIR}/cmake_config.h"
)
if(CMAKE_CXX_COMPILER_ID MATCHES "^(GNU|Clang|AppleClang)$")
set(CMAKE_CXX_FLAGS_RELEASE "-O2")
set(CMAKE_CXX_FLAGS_DEBUG "-Og -g2")
add_compile_options(-Wall -pipe)
elseif(MSVC)
add_compile_options(/GR- /Zl)
endif()
if(CMAKE_BUILD_TYPE STREQUAL "Release")
add_definitions(-DNDEBUG)
endif()
add_executable(minetestmapper)
target_include_directories(minetestmapper PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}"
"${CMAKE_CURRENT_BINARY_DIR}" "${CMAKE_CURRENT_BINARY_DIR}"
)
target_sources(minetestmapper PRIVATE
src/BlockDecoder.cpp
src/PixelAttributes.cpp
src/PlayerAttributes.cpp
src/TileGenerator.cpp
src/ZlibDecompressor.cpp
src/ZstdDecompressor.cpp
src/Image.cpp
src/mapper.cpp
src/util.cpp
src/log.cpp
src/db-sqlite3.cpp
$<$<BOOL:${USE_POSTGRESQL}>:src/db-postgresql.cpp>
$<$<BOOL:${USE_LEVELDB}>:src/db-leveldb.cpp>
$<$<BOOL:${USE_REDIS}>:src/db-redis.cpp>
)
target_include_directories(minetestmapper PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/src"
"${CMAKE_CURRENT_BINARY_DIR}"
${SQLITE3_INCLUDE_DIR} ${SQLITE3_INCLUDE_DIR}
${LIBGD_INCLUDE_DIR} ${LIBGD_INCLUDE_DIR}
${ZLIB_INCLUDE_DIR} ${ZLIB_INCLUDE_DIR}
${ZSTD_INCLUDE_DIR}
) )
target_link_libraries(minetestmapper configure_file(
"${PROJECT_SOURCE_DIR}/cmake_config.h.in"
"${PROJECT_BINARY_DIR}/cmake_config.h"
)
add_definitions ( -DUSE_CMAKE_CONFIG_H )
set(mapper_SRCS
PixelAttributes.cpp
PlayerAttributes.cpp
TileGenerator.cpp
ZlibDecompressor.cpp
Image.cpp
mapper.cpp
util.cpp
db-sqlite3.cpp
)
if(USE_POSTGRESQL)
set(mapper_SRCS ${mapper_SRCS} db-postgresql.cpp)
endif(USE_POSTGRESQL)
if(USE_LEVELDB)
set(mapper_SRCS ${mapper_SRCS} db-leveldb.cpp)
endif(USE_LEVELDB)
if(USE_REDIS)
set(mapper_SRCS ${mapper_SRCS} db-redis.cpp)
endif(USE_REDIS)
add_executable(minetestmapper
${mapper_SRCS}
)
target_link_libraries(
minetestmapper
${SQLITE3_LIBRARY} ${SQLITE3_LIBRARY}
${PostgreSQL_LIBRARIES} ${POSTGRESQL_LIBRARY}
${LEVELDB_LIBRARY} ${LEVELDB_LIBRARY}
${REDIS_LIBRARY} ${REDIS_LIBRARY}
${LIBGD_LIBRARY} ${LIBGD_LIBRARY}
${ZLIB_LIBRARY} ${ZLIB_LIBRARY}
${ZSTD_LIBRARY}
) )
# Installing & Packaging # Installing & Packaging
@ -226,15 +220,17 @@ if(UNIX)
install(FILES "minetestmapper.6" DESTINATION "${MANDIR}/man6") install(FILES "minetestmapper.6" DESTINATION "${MANDIR}/man6")
endif() endif()
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Overview mapper for Luanti") set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Overview mapper for Minetest")
set(CPACK_PACKAGE_VERSION_MAJOR ${VERSION_MAJOR})
set(CPACK_PACKAGE_VERSION_MINOR ${VERSION_MINOR})
set(CPACK_PACKAGE_VENDOR "celeron55") set(CPACK_PACKAGE_VENDOR "celeron55")
set(CPACK_PACKAGE_CONTACT "Perttu Ahola <celeron55@gmail.com>") set(CPACK_PACKAGE_CONTACT "Perttu Ahola <celeron55@gmail.com>")
if(WIN32) if(WIN32)
set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}-win32") set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${VERSION_STRING}-win32")
set(CPACK_GENERATOR ZIP) set(CPACK_GENERATOR ZIP)
else() else()
set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}-linux") set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${VERSION_STRING}-linux")
set(CPACK_GENERATOR TGZ) set(CPACK_GENERATOR TGZ)
set(CPACK_SOURCE_GENERATOR TGZ) set(CPACK_SOURCE_GENERATOR TGZ)
endif() endif()

View File

@ -1,24 +0,0 @@
ARG DOCKER_IMAGE=alpine:3.20
FROM $DOCKER_IMAGE AS builder
RUN apk add --no-cache build-base cmake \
gd-dev sqlite-dev postgresql-dev hiredis-dev leveldb-dev \
ninja
COPY . /usr/src/minetestmapper
WORKDIR /usr/src/minetestmapper
RUN cmake -B build -G Ninja && \
cmake --build build --parallel $(nproc) && \
cmake --install build
FROM $DOCKER_IMAGE AS runtime
RUN apk add --no-cache libstdc++ libgcc libpq \
gd sqlite-libs postgresql hiredis leveldb
COPY --from=builder /usr/local/share/luanti /usr/local/share/luanti
COPY --from=builder /usr/local/bin/minetestmapper /usr/local/bin/minetestmapper
COPY COPYING /usr/local/share/minetest/minetestmapper.COPYING
ENTRYPOINT ["/usr/local/bin/minetestmapper"]

View File

@ -10,53 +10,40 @@
#include "Image.h" #include "Image.h"
#ifndef NDEBUG #ifndef NDEBUG
#define SIZECHECK(x, y) check_bounds((x), (y), m_width, m_height) #define SIZECHECK(x, y) do { \
if((x) < 0 || (x) >= m_width) \
throw std::out_of_range("sizecheck x"); \
if((y) < 0 || (y) >= m_height) \
throw std::out_of_range("sizecheck y"); \
} while(0)
#else #else
#define SIZECHECK(x, y) do {} while(0) #define SIZECHECK(x, y) do {} while(0)
#endif #endif
// ARGB but with inverted alpha // ARGB but with inverted alpha
static inline int color2int(const Color &c) static inline int color2int(Color c)
{ {
u8 a = (255 - c.a) * gdAlphaMax / 255; u8 a = 255 - c.a;
return (a << 24) | (c.r << 16) | (c.g << 8) | c.b; return (a << 24) | (c.r << 16) | (c.g << 8) | c.b;
} }
static inline Color int2color(int c) static inline Color int2color(int c)
{ {
Color c2; Color c2;
u8 a;
c2.b = c & 0xff; c2.b = c & 0xff;
c2.g = (c >> 8) & 0xff; c2.g = (c >> 8) & 0xff;
c2.r = (c >> 16) & 0xff; c2.r = (c >> 16) & 0xff;
u8 a = (c >> 24) & 0xff; a = (c >> 24) & 0xff;
c2.a = 255 - (a*255 / gdAlphaMax); c2.a = 255 - a;
return c2; return c2;
} }
#ifndef NDEBUG
static inline void check_bounds(int x, int y, int width, int height)
{
if(x < 0 || x >= width) {
std::ostringstream oss;
oss << "Access outside image bounds (x), 0 < "
<< x << " < " << width << " is false.";
throw std::out_of_range(oss.str());
}
if(y < 0 || y >= height) {
std::ostringstream oss;
oss << "Access outside image bounds (y), 0 < "
<< y << " < " << height << " is false.";
throw std::out_of_range(oss.str());
}
}
#endif
Image::Image(int width, int height) : Image::Image(int width, int height) :
m_width(width), m_height(height), m_image(nullptr) m_width(width), m_height(height), m_image(NULL)
{ {
SIZECHECK(0, 0);
m_image = gdImageCreateTrueColor(m_width, m_height); m_image = gdImageCreateTrueColor(m_width, m_height);
} }
@ -73,7 +60,10 @@ void Image::setPixel(int x, int y, const Color &c)
Color Image::getPixel(int x, int y) Color Image::getPixel(int x, int y)
{ {
SIZECHECK(x, y); #ifndef NDEBUG
if(x < 0 || x > m_width || y < 0 || y > m_height)
throw std::out_of_range("sizecheck");
#endif
return int2color(m_image->tpixels[y][x]); return int2color(m_image->tpixels[y][x]);
} }
@ -105,22 +95,12 @@ void Image::drawCircle(int x, int y, int diameter, const Color &c)
void Image::save(const std::string &filename) void Image::save(const std::string &filename)
{ {
#if (GD_MAJOR_VERSION == 2 && GD_MINOR_VERSION == 1 && GD_RELEASE_VERSION >= 1) || (GD_MAJOR_VERSION == 2 && GD_MINOR_VERSION > 1) || GD_MAJOR_VERSION > 2
const char *f = filename.c_str();
if (gdSupportsFileType(f, 1) == GD_FALSE)
throw std::runtime_error("Image format not supported by gd");
if (gdImageFile(m_image, f) == GD_FALSE)
throw std::runtime_error("Error saving image");
#else
if (filename.compare(filename.length() - 4, 4, ".png") != 0)
throw std::runtime_error("Only PNG is supported");
FILE *f = fopen(filename.c_str(), "wb"); FILE *f = fopen(filename.c_str(), "wb");
if(!f) { if(!f) {
std::ostringstream oss; std::ostringstream oss;
oss << "Error opening image file: " << std::strerror(errno); oss << "Error writing image file: " << std::strerror(errno);
throw std::runtime_error(oss.str()); throw std::runtime_error(oss.str());
} }
gdImagePng(m_image, f); gdImagePng(m_image, f); // other formats?
fclose(f); fclose(f);
#endif
} }

View File

@ -1,4 +1,5 @@
#pragma once #ifndef IMAGE_HEADER
#define IMAGE_HEADER
#include "types.h" #include "types.h"
#include <string> #include <string>
@ -8,6 +9,7 @@ struct Color {
Color() : r(0), g(0), b(0), a(0) {}; Color() : r(0), g(0), b(0), a(0) {};
Color(u8 r, u8 g, u8 b) : r(r), g(g), b(b), a(255) {}; Color(u8 r, u8 g, u8 b) : r(r), g(g), b(b), a(255) {};
Color(u8 r, u8 g, u8 b, u8 a) : r(r), g(g), b(b), a(a) {}; Color(u8 r, u8 g, u8 b, u8 a) : r(r), g(g), b(b), a(a) {};
inline Color noAlpha() const { return Color(r, g, b); }
u8 r, g, b, a; u8 r, g, b, a;
}; };
@ -17,9 +19,6 @@ public:
Image(int width, int height); Image(int width, int height);
~Image(); ~Image();
Image(const Image&) = delete;
Image& operator=(const Image&) = delete;
void setPixel(int x, int y, const Color &c); void setPixel(int x, int y, const Color &c);
Color getPixel(int x, int y); Color getPixel(int x, int y);
void drawLine(int x1, int y1, int x2, int y2, const Color &c); void drawLine(int x1, int y1, int x2, int y2, const Color &c);
@ -29,6 +28,10 @@ public:
void save(const std::string &filename); void save(const std::string &filename);
private: private:
Image(const Image&);
int m_width, m_height; int m_width, m_height;
gdImagePtr m_image; gdImagePtr m_image;
}; };
#endif // IMAGE_HEADER

View File

@ -1,12 +1,23 @@
#include <cstring> /*
* =====================================================================
* Version: 1.0
* Created: 25.08.2012 10:55:27
* Author: Miroslav Bendík
* Company: LinuxOS.sk
* =====================================================================
*/
#include <cstdlib>
#include <cstring>
#include "PixelAttributes.h" #include "PixelAttributes.h"
using namespace std;
PixelAttributes::PixelAttributes(): PixelAttributes::PixelAttributes():
m_width(0) m_width(0)
{ {
for (size_t i = 0; i < LineCount; ++i) { for (size_t i = 0; i < LineCount; ++i) {
m_pixelAttributes[i] = nullptr; m_pixelAttributes[i] = 0;
} }
} }
@ -36,9 +47,9 @@ void PixelAttributes::scroll()
void PixelAttributes::freeAttributes() void PixelAttributes::freeAttributes()
{ {
for (size_t i = 0; i < LineCount; ++i) { for (size_t i = 0; i < LineCount; ++i) {
if (m_pixelAttributes[i] != nullptr) { if (m_pixelAttributes[i] != 0) {
delete[] m_pixelAttributes[i]; delete[] m_pixelAttributes[i];
m_pixelAttributes[i] = nullptr; m_pixelAttributes[i] = 0;
} }
} }
} }

50
PixelAttributes.h Normal file
View File

@ -0,0 +1,50 @@
/*
* =====================================================================
* Version: 1.0
* Created: 25.08.2012 10:55:29
* Author: Miroslav Bendík
* Company: LinuxOS.sk
* =====================================================================
*/
#ifndef PIXELATTRIBUTES_H_ADZ35GYF
#define PIXELATTRIBUTES_H_ADZ35GYF
#include <limits>
#include <stdint.h>
#include "config.h"
struct PixelAttribute {
PixelAttribute(): height(std::numeric_limits<int>::min()), thickness(0) {};
int height;
uint8_t thickness;
inline bool valid_height() {
return height != std::numeric_limits<int>::min();
}
};
class PixelAttributes
{
public:
PixelAttributes();
virtual ~PixelAttributes();
void setWidth(int width);
void scroll();
inline PixelAttribute &attribute(int z, int x) { return m_pixelAttributes[z + 1][x + 1]; };
private:
void freeAttributes();
private:
enum Line {
FirstLine = 0,
LastLine = BLOCK_SIZE,
EmptyLine = BLOCK_SIZE + 1,
LineCount = BLOCK_SIZE + 2
};
PixelAttribute *m_pixelAttributes[BLOCK_SIZE + 2]; // 1px gradient + empty
int m_width;
};
#endif /* end of include guard: PIXELATTRIBUTES_H_ADZ35GYF */

73
PlayerAttributes.cpp Normal file
View File

@ -0,0 +1,73 @@
/*
* =====================================================================
* Version: 1.0
* Created: 01.09.2012 14:38:05
* Author: Miroslav Bendík
* Company: LinuxOS.sk
* =====================================================================
*/
#include <dirent.h>
#include <fstream>
#include <sstream>
#include "config.h"
#include "PlayerAttributes.h"
using namespace std;
PlayerAttributes::PlayerAttributes(const std::string &sourceDirectory)
{
string playersPath = sourceDirectory + "players";
DIR *dir;
dir = opendir (playersPath.c_str());
if (dir == NULL) {
return;
}
struct dirent *ent;
while ((ent = readdir (dir)) != NULL) {
if (ent->d_name[0] == '.') {
continue;
}
string path = playersPath + PATH_SEPARATOR + ent->d_name;
ifstream in;
in.open(path.c_str(), ifstream::in);
string buffer;
string name;
string position;
while (getline(in, buffer)) {
if (buffer.find("name = ") == 0) {
name = buffer.substr(7);
}
else if (buffer.find("position = ") == 0) {
position = buffer.substr(12, buffer.length() - 13);
}
}
char comma;
Player player;
istringstream positionStream(position, istringstream::in);
positionStream >> player.x;
positionStream >> comma;
positionStream >> player.y;
positionStream >> comma;
positionStream >> player.z;
player.name = name;
m_players.push_back(player);
}
closedir(dir);
}
PlayerAttributes::Players::iterator PlayerAttributes::begin()
{
return m_players.begin();
}
PlayerAttributes::Players::iterator PlayerAttributes::end()
{
return m_players.end();
}

38
PlayerAttributes.h Normal file
View File

@ -0,0 +1,38 @@
/*
* =====================================================================
* Version: 1.0
* Created: 01.09.2012 14:38:08
* Author: Miroslav Bendík
* Company: LinuxOS.sk
* =====================================================================
*/
#ifndef PLAYERATTRIBUTES_H_D7THWFVV
#define PLAYERATTRIBUTES_H_D7THWFVV
#include <list>
#include <string>
struct Player
{
std::string name;
double x;
double y;
double z;
}; /* ----- end of struct Player ----- */
class PlayerAttributes
{
public:
typedef std::list<Player> Players;
PlayerAttributes(const std::string &sourceDirectory);
Players::iterator begin();
Players::iterator end();
private:
Players m_players;
}; /* ----- end of class PlayerAttributes ----- */
#endif /* end of include guard: PLAYERATTRIBUTES_H_D7THWFVV */

View File

@ -1,50 +1,32 @@
Minetest Mapper C++ Minetest Mapper C++
=================== ===================
.. image:: https://github.com/minetest/minetestmapper/workflows/build/badge.svg .. image:: https://travis-ci.org/minetest/minetestmapper.svg?branch=master
:target: https://github.com/minetest/minetestmapper/actions/workflows/build.yml :target: https://travis-ci.org/minetest/minetestmapper
Minetestmapper generates a top-down overview image from a Luanti map. A port of minetestmapper.py to C++ from https://github.com/minetest/minetest/tree/master/util.
This version is both faster and provides more features than the now deprecated Python script.
A port of minetestmapper.py to C++ from `the obsolete Python script
<https://github.com/minetest/minetest/tree/0.4.17/util>`_.
This version is both faster and provides more features.
Minetestmapper ships with a colors.txt file suitable for Minetest Game,
if you use a different game or have mods installed you should generate a
matching colors.txt for better results (colors will be missing otherwise).
The `generate_colorstxt.py script
<./util/generate_colorstxt.py>`_ in the util folder exists for this purpose,
detailed instructions can be found within.
Requirements Requirements
------------ ------------
* C++ compiler, zlib, zstd
* libgd * libgd
* sqlite3 * sqlite3
* LevelDB (optional) * leveldb (optional, set ENABLE_LEVELDB=1 in CMake to enable)
* hiredis (optional) * hiredis (optional, set ENABLE_REDIS=1 in CMake to enable)
* Postgres libraries (optional) * Postgres libraries (optional, set ENABLE_POSTGRES=1 in CMake to enable)
on Debian/Ubuntu: e.g. on Debian:
^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^
``sudo apt install cmake libgd-dev libhiredis-dev libleveldb-dev libpq-dev libsqlite3-dev zlib1g-dev libzstd-dev`` sudo apt-get install libgd-dev libsqlite3-dev libleveldb-dev libhiredis-dev libpq-dev
on openSUSE: Windows
^^^^^^^^^^^^ ^^^^^^^
Minetestmapper for Windows can be downloaded here: https://github.com/minetest/minetestmapper/releases
``sudo zypper install gd-devel hiredis-devel leveldb-devel postgresql-devel sqlite3-devel zlib-devel libzstd-devel`` After extracting the archive somewhere minetestmapper will be available from cmd.exe:
::
for Windows:
^^^^^^^^^^^^
Minetestmapper for Windows can be downloaded `from the Releases section
<https://github.com/minetest/minetestmapper/releases>`_.
After extracting the archive, it can be invoked from cmd.exe or PowerShell:
.. code-block:: dos
cd C:\Users\yourname\Desktop\example\path cd C:\Users\yourname\Desktop\example\path
minetestmapper.exe --help minetestmapper.exe --help
@ -52,16 +34,16 @@ After extracting the archive, it can be invoked from cmd.exe or PowerShell:
Compilation Compilation
----------- -----------
.. code-block:: bash ::
cmake . -DENABLE_LEVELDB=1 cmake . -DENABLE_LEVELDB=1
make -j$(nproc) make -j2
Usage Usage
----- -----
``minetestmapper`` has two mandatory paremeters, ``-i`` (input world path) `minetestmapper` has two mandatory paremeters, `-i` (input world path)
and ``-o`` (output image path). and `-o` (output image path).
:: ::
@ -72,68 +54,49 @@ Parameters
^^^^^^^^^^ ^^^^^^^^^^
bgcolor: bgcolor:
Background color of image, e.g. ``--bgcolor '#ffffff'`` Background color of image, e.g. ``--bgcolor #ffffff``
scalecolor: scalecolor:
Color of scale marks and text, e.g. ``--scalecolor '#000000'`` Color of scale, e.g. ``--scalecolor #000000``
playercolor: playercolor:
Color of player indicators, e.g. ``--playercolor '#ff0000'`` Color of player indicators, e.g. ``--playercolor #ff0000``
origincolor: origincolor:
Color of origin indicator, e.g. ``--origincolor '#ff0000'`` Color of origin indicator, e.g. ``--origincolor #ff0000``
drawscale: drawscale:
Draw scale(s) with tick marks and numbers, ``--drawscale`` Draw tick marks, ``--drawscale``
drawplayers: drawplayers:
Draw player indicators with name, ``--drawplayers`` Draw player indicators, ``--drawplayers``
draworigin: draworigin:
Draw origin indicator, ``--draworigin`` Draw origin indicator, ``--draworigin``
drawalpha: drawalpha:
Allow nodes to be drawn with transparency (such as water), ``--drawalpha`` Allow nodes to be drawn with transparency, ``--drawalpha``
extent:
Don't output any imagery, just print the extent of the full map, ``--extent``
noshading: noshading:
Don't draw shading on nodes, ``--noshading`` Don't draw shading on nodes, ``--noshading``
noemptyimage:
Don't output anything when the image would be empty, ``--noemptyimage``
verbose:
Enable verbose log putput, ``--verbose``
min-y: min-y:
Don't draw nodes below this Y value, e.g. ``--min-y -25`` Don't draw nodes below this y value, e.g. ``--min-y -25``
max-y: max-y:
Don't draw nodes above this Y value, e.g. ``--max-y 75`` Don't draw nodes above this y value, e.g. ``--max-y 75``
backend: backend:
Override auto-detected map backend; supported: *sqlite3*, *leveldb*, *redis*, *postgresql*, e.g. ``--backend leveldb`` Use specific map backend; supported: *sqlite3*, *leveldb*, *redis*, *postgresql*, e.g. ``--backend leveldb``
geometry: geometry:
Limit area to specific geometry (*x:z+w+h* where x and z specify the lower left corner), e.g. ``--geometry -800:-800+1600+1600`` Limit area to specific geometry (*x:y+w+h* where x and y specify the lower left corner), e.g. ``--geometry -800:-800+1600+1600``
The coordinates are specified with the same axes as in-game. The Z axis becomes Y when projected on the image.
zoom: zoom:
Zoom the image by using more than one pixel per node, e.g. ``--zoom 4`` "Zoom" the image by using more than one pixel per node, e.g. ``--zoom 4``
colors: colors:
Override auto-detected path to colors.txt, e.g. ``--colors ../world/mycolors.txt`` Forcefully set path to colors.txt file (it's autodetected otherwise), e.g. ``--colors ../minetest/mycolors.txt``
scales: scales:
Draw scales on specified image edges (letters *t b l r* meaning top, bottom, left and right), e.g. ``--scales tbr`` Draw scales on specified image edges (letters *t b l r* meaning top, bottom, left and right), e.g. ``--scales tbr``
exhaustive:
Select if database should be traversed exhaustively or using range queries, available: *never*, *y*, *full*, *auto*
Defaults to *auto*. You shouldn't need to change this, as minetestmapper tries to automatically picks the best option.
dumpblock:
Instead of rendering anything try to load the block at the given position (*x,y,z*) and print its raw data as hexadecimal.

723
TileGenerator.cpp Normal file
View File

@ -0,0 +1,723 @@
#include <cstdio>
#include <cstdlib>
#include <climits>
#include <fstream>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <cstring>
#include <vector>
#include "config.h"
#include "PlayerAttributes.h"
#include "TileGenerator.h"
#include "ZlibDecompressor.h"
#include "util.h"
#include "db-sqlite3.h"
#if USE_POSTGRESQL
#include "db-postgresql.h"
#endif
#if USE_LEVELDB
#include "db-leveldb.h"
#endif
#if USE_REDIS
#include "db-redis.h"
#endif
using namespace std;
static inline uint16_t readU16(const unsigned char *data)
{
return data[0] << 8 | data[1];
}
// rounds n (away from 0) to a multiple of f while preserving the sign of n
static int round_multiple_nosign(int n, int f)
{
int abs_n, sign;
abs_n = (n >= 0) ? n : -n;
sign = (n >= 0) ? 1 : -1;
if (abs_n % f == 0)
return n; // n == abs_n * sign
else
return sign * (abs_n + f - (abs_n % f));
}
static int readBlockContent(const unsigned char *mapData, int version, int datapos)
{
if (version >= 24) {
size_t index = datapos << 1;
return (mapData[index] << 8) | mapData[index + 1];
} else if (version >= 20) {
if (mapData[datapos] <= 0x80)
return mapData[datapos];
else
return (int(mapData[datapos]) << 4) | (int(mapData[datapos + 0x2000]) >> 4);
}
std::ostringstream oss;
oss << "Unsupported map version " << version;
throw std::runtime_error(oss.str());
}
static inline int colorSafeBounds(int color)
{
color = (color > 255) ? 255 : color;
color = (color < 0) ? 0 : color;
return color;
}
static Color mixColors(Color a, Color b)
{
Color result;
double a1 = a.a / 255.0;
double a2 = b.a / 255.0;
result.r = (int) (a1 * a.r + a2 * (1 - a1) * b.r);
result.g = (int) (a1 * a.g + a2 * (1 - a1) * b.g);
result.b = (int) (a1 * a.b + a2 * (1 - a1) * b.b);
result.a = (int) (255 * (a1 + a2 * (1 - a1)));
return result;
}
TileGenerator::TileGenerator():
m_bgColor(255, 255, 255),
m_scaleColor(0, 0, 0),
m_originColor(255, 0, 0),
m_playerColor(255, 0, 0),
m_drawOrigin(false),
m_drawPlayers(false),
m_drawScale(false),
m_drawAlpha(false),
m_shading(true),
m_backend(""),
m_xBorder(0),
m_yBorder(0),
m_db(NULL),
m_image(NULL),
m_xMin(INT_MAX),
m_xMax(INT_MIN),
m_zMin(INT_MAX),
m_zMax(INT_MIN),
m_yMin(-30000),
m_yMax(30000),
m_geomX(-2048),
m_geomY(-2048),
m_geomX2(2048),
m_geomY2(2048),
m_zoom(1),
m_scales(SCALE_LEFT | SCALE_TOP)
{
}
TileGenerator::~TileGenerator()
{
closeDatabase();
}
void TileGenerator::setBgColor(const std::string &bgColor)
{
m_bgColor = parseColor(bgColor);
}
void TileGenerator::setScaleColor(const std::string &scaleColor)
{
m_scaleColor = parseColor(scaleColor);
}
void TileGenerator::setOriginColor(const std::string &originColor)
{
m_originColor = parseColor(originColor);
}
void TileGenerator::setPlayerColor(const std::string &playerColor)
{
m_playerColor = parseColor(playerColor);
}
void TileGenerator::setZoom(int zoom)
{
if (zoom < 1)
throw std::runtime_error("Zoom level needs to be a number: 1 or higher");
m_zoom = zoom;
}
void TileGenerator::setScales(uint flags)
{
m_scales = flags;
}
Color TileGenerator::parseColor(const std::string &color)
{
Color parsed;
if (color.length() != 7)
throw std::runtime_error("Color needs to be 7 characters long");
if (color[0] != '#')
throw std::runtime_error("Color needs to begin with #");
unsigned long col = strtoul(color.c_str() + 1, NULL, 16);
parsed.b = col & 0xff;
parsed.g = (col >> 8) & 0xff;
parsed.r = (col >> 16) & 0xff;
parsed.a = 255;
return parsed;
}
void TileGenerator::setDrawOrigin(bool drawOrigin)
{
m_drawOrigin = drawOrigin;
}
void TileGenerator::setDrawPlayers(bool drawPlayers)
{
m_drawPlayers = drawPlayers;
}
void TileGenerator::setDrawScale(bool drawScale)
{
m_drawScale = drawScale;
}
void TileGenerator::setDrawAlpha(bool drawAlpha)
{
m_drawAlpha = drawAlpha;
}
void TileGenerator::setShading(bool shading)
{
m_shading = shading;
}
void TileGenerator::setBackend(std::string backend)
{
m_backend = backend;
}
void TileGenerator::setGeometry(int x, int y, int w, int h)
{
m_geomX = round_multiple_nosign(x, 16) / 16;
m_geomY = round_multiple_nosign(y, 16) / 16;
m_geomX2 = round_multiple_nosign(x + w, 16) / 16;
m_geomY2 = round_multiple_nosign(y + h, 16) / 16;
}
void TileGenerator::setMinY(int y)
{
m_yMin = y;
}
void TileGenerator::setMaxY(int y)
{
m_yMax = y;
}
void TileGenerator::parseColorsFile(const std::string &fileName)
{
ifstream in;
in.open(fileName.c_str(), ifstream::in);
if (!in.is_open())
throw std::runtime_error("Specified colors file could not be found");
parseColorsStream(in);
}
void TileGenerator::generate(const std::string &input, const std::string &output)
{
string input_path = input;
if (input_path[input.length() - 1] != PATH_SEPARATOR) {
input_path += PATH_SEPARATOR;
}
openDb(input_path);
loadBlocks();
createImage();
renderMap();
closeDatabase();
if (m_drawScale) {
renderScale();
}
if (m_drawOrigin) {
renderOrigin();
}
if (m_drawPlayers) {
renderPlayers(input_path);
}
writeImage(output);
printUnknown();
}
void TileGenerator::parseColorsStream(std::istream &in)
{
char line[128];
while (in.good()) {
in.getline(line, 128);
for(char *p = line; *p; p++) {
if(*p != '#')
continue;
*p = '\0'; // Cut off at the first #
break;
}
if(strlen(line) == 0)
continue;
char name[64];
unsigned int r, g, b, a, t;
a = 255;
t = 0;
int items = sscanf(line, "%64s %u %u %u %u %u", name, &r, &g, &b, &a, &t);
if(items < 4) {
std::cerr << "Failed to parse color entry '" << line << "'" << std::endl;
continue;
}
ColorEntry color = ColorEntry(r, g, b, a, t);
m_colorMap[name] = color;
}
}
void TileGenerator::openDb(const std::string &input)
{
std::string backend = m_backend;
if(backend == "") {
std::ifstream ifs((input + "/world.mt").c_str());
if(!ifs.good())
throw std::runtime_error("Failed to read world.mt");
backend = get_setting("backend", ifs);
ifs.close();
}
if(backend == "sqlite3")
m_db = new DBSQLite3(input);
#if USE_POSTGRESQL
else if(backend == "postgresql")
m_db = new DBPostgreSQL(input);
#endif
#if USE_LEVELDB
else if(backend == "leveldb")
m_db = new DBLevelDB(input);
#endif
#if USE_REDIS
else if(backend == "redis")
m_db = new DBRedis(input);
#endif
else
throw std::runtime_error(((std::string) "Unknown map backend: ") + backend);
}
void TileGenerator::closeDatabase()
{
delete m_db;
m_db = NULL;
}
void TileGenerator::loadBlocks()
{
std::vector<BlockPos> vec = m_db->getBlockPos();
for (std::vector<BlockPos>::iterator it = vec.begin(); it != vec.end(); ++it) {
BlockPos pos = *it;
// Check that it's in geometry (from --geometry option)
if (pos.x < m_geomX || pos.x >= m_geomX2 || pos.z < m_geomY || pos.z >= m_geomY2)
continue;
// Check that it's between --min-y and --max-y
if (pos.y * 16 < m_yMin || pos.y * 16 > m_yMax)
continue;
// Adjust minimum and maximum positions to the nearest block
if (pos.x < m_xMin)
m_xMin = pos.x;
if (pos.x > m_xMax)
m_xMax = pos.x;
if (pos.z < m_zMin)
m_zMin = pos.z;
if (pos.z > m_zMax)
m_zMax = pos.z;
m_positions.push_back(std::pair<int, int>(pos.x, pos.z));
}
m_positions.sort();
m_positions.unique();
}
void TileGenerator::createImage()
{
m_mapWidth = (m_xMax - m_xMin + 1) * 16;
m_mapHeight = (m_zMax - m_zMin + 1) * 16;
if(m_drawScale) {
m_xBorder = (m_scales & SCALE_LEFT) ? 40 : 0;
m_yBorder = (m_scales & SCALE_TOP) ? 40 : 0;
}
m_blockPixelAttributes.setWidth(m_mapWidth);
int image_width, image_height;
image_width = (m_mapWidth * m_zoom) + m_xBorder;
image_width += m_drawScale && (m_scales & SCALE_RIGHT) ? 40 : 0;
image_height = (m_mapHeight * m_zoom) + m_yBorder;
image_height += m_drawScale && (m_scales & SCALE_BOTTOM) ? 40 : 0;
if(image_width > 4096 || image_height > 4096)
std::cerr << "Warning: The width or height of the image to be created exceeds 4096 pixels!"
<< " (Dimensions: " << image_width << "x" << image_height << ")"
<< std::endl;
m_image = new Image(image_width, image_height);
m_image->drawFilledRect(0, 0, image_width, image_height, m_bgColor); // Background
}
void TileGenerator::renderMap()
{
std::list<int> zlist = getZValueList();
for (std::list<int>::iterator zPosition = zlist.begin(); zPosition != zlist.end(); ++zPosition) {
int zPos = *zPosition;
std::map<int16_t, BlockList> blocks;
m_db->getBlocksOnZ(blocks, zPos);
for (std::list<std::pair<int, int> >::const_iterator position = m_positions.begin(); position != m_positions.end(); ++position) {
if (position->second != zPos)
continue;
for (int i = 0; i < 16; ++i) {
m_readPixels[i] = 0;
m_readInfo[i] = 0;
}
for (int i = 0; i < 16; i++) {
for (int j = 0; j < 16; j++) {
m_color[i][j] = m_bgColor; // This will be drawn by renderMapBlockBottom() for y-rows with only 'air', 'ignore' or unknown nodes if --drawalpha is used
m_color[i][j].a = 0; // ..but set alpha to 0 to tell renderMapBlock() not to use this color to mix a shade
m_thickness[i][j] = 0;
}
}
int xPos = position->first;
blocks[xPos].sort();
const BlockList &blockStack = blocks[xPos];
for (BlockList::const_iterator it = blockStack.begin(); it != blockStack.end(); ++it) {
const BlockPos &pos = it->first;
const unsigned char *data = it->second.c_str();
size_t length = it->second.length();
uint8_t version = data[0];
//uint8_t flags = data[1];
size_t dataOffset = 0;
if (version >= 27)
dataOffset = 6;
else if (version >= 22)
dataOffset = 4;
else
dataOffset = 2;
ZlibDecompressor decompressor(data, length);
decompressor.setSeekPos(dataOffset);
ustring mapData = decompressor.decompress();
ustring mapMetadata = decompressor.decompress();
dataOffset = decompressor.seekPos();
// Skip unused data
if (version <= 21)
dataOffset += 2;
if (version == 23)
dataOffset += 1;
if (version == 24) {
uint8_t ver = data[dataOffset++];
if (ver == 1) {
uint16_t num = readU16(data + dataOffset);
dataOffset += 2;
dataOffset += 10 * num;
}
}
// Skip unused static objects
dataOffset++; // Skip static object version
int staticObjectCount = readU16(data + dataOffset);
dataOffset += 2;
for (int i = 0; i < staticObjectCount; ++i) {
dataOffset += 13;
uint16_t dataSize = readU16(data + dataOffset);
dataOffset += dataSize + 2;
}
dataOffset += 4; // Skip timestamp
m_blockAirId = -1;
m_blockIgnoreId = -1;
m_nameMap.clear();
// Read mapping
if (version >= 22) {
dataOffset++; // mapping version
uint16_t numMappings = readU16(data + dataOffset);
dataOffset += 2;
for (int i = 0; i < numMappings; ++i) {
uint16_t nodeId = readU16(data + dataOffset);
dataOffset += 2;
uint16_t nameLen = readU16(data + dataOffset);
dataOffset += 2;
string name = string(reinterpret_cast<const char *>(data) + dataOffset, nameLen);
if (name == "air")
m_blockAirId = nodeId;
else if (name == "ignore")
m_blockIgnoreId = nodeId;
else
m_nameMap[nodeId] = name;
dataOffset += nameLen;
}
// Skip block if made of only air or ignore nodes
if (m_nameMap.empty())
continue;
}
// Node timers
if (version >= 25) {
dataOffset++;
uint16_t numTimers = readU16(data + dataOffset);
dataOffset += 2;
dataOffset += numTimers * 10;
}
renderMapBlock(mapData, pos, version);
bool allRead = true;
for (int i = 0; i < 16; ++i) {
if (m_readPixels[i] != 0xffff)
allRead = false;
}
if (allRead)
break;
}
bool allRead = true;
for (int i = 0; i < 16; ++i) {
if (m_readPixels[i] != 0xffff)
allRead = false;
}
if (!allRead)
renderMapBlockBottom(blockStack.begin()->first);
}
if(m_shading)
renderShading(zPos);
}
}
void TileGenerator::renderMapBlock(const ustring &mapBlock, const BlockPos &pos, int version)
{
int xBegin = (pos.x - m_xMin) * 16;
int zBegin = (m_zMax - pos.z) * 16;
const unsigned char *mapData = mapBlock.c_str();
int minY = (pos.y * 16 > m_yMin) ? 0 : m_yMin - pos.y * 16;
int maxY = (pos.y * 16 < m_yMax) ? 15 : m_yMax - pos.y * 16;
for (int z = 0; z < 16; ++z) {
int imageY = zBegin + 15 - z;
for (int x = 0; x < 16; ++x) {
if (m_readPixels[z] & (1 << x))
continue;
int imageX = xBegin + x;
for (int y = maxY; y >= minY; --y) {
int position = x + (y << 4) + (z << 8);
int content = readBlockContent(mapData, version, position);
if (content == m_blockAirId || content == m_blockIgnoreId)
continue;
NameMap::iterator blockName = m_nameMap.find(content);
if (blockName == m_nameMap.end()) {
std::cerr << "Skipping node with invalid ID." << std::endl;
continue;
}
const string &name = blockName->second;
ColorMap::const_iterator color = m_colorMap.find(name);
if (color != m_colorMap.end()) {
const Color c = color->second.to_color();
if (m_drawAlpha) {
// mix with previous color (unless first visible time)
if (m_color[z][x].a == 0)
m_color[z][x] = c;
else
m_color[z][x] = mixColors(m_color[z][x], c);
if(m_color[z][x].a == 0xff) {
// color is opaque at this depth (no point continuing)
setZoomed(imageX, imageY, m_color[z][x]);
m_readPixels[z] |= (1 << x);
m_blockPixelAttributes.attribute(15 - z, xBegin + x).thickness = m_thickness[z][x];
} else {
// near thickness value to thickness of current node
m_thickness[z][x] = (m_thickness[z][x] + color->second.t) / 2.0;
continue;
}
} else {
setZoomed(imageX, imageY, c.noAlpha());
m_readPixels[z] |= (1 << x);
}
if(!(m_readInfo[z] & (1 << x))) {
m_blockPixelAttributes.attribute(15 - z, xBegin + x).height = pos.y * 16 + y;
m_readInfo[z] |= (1 << x);
}
} else {
m_unknownNodes.insert(name);
continue;
}
break;
}
}
}
}
void TileGenerator::renderMapBlockBottom(const BlockPos &pos)
{
int xBegin = (pos.x - m_xMin) * 16;
int zBegin = (m_zMax - pos.z) * 16;
for (int z = 0; z < 16; ++z) {
int imageY = zBegin + 15 - z;
for (int x = 0; x < 16; ++x) {
if (m_readPixels[z] & (1 << x))
continue;
int imageX = xBegin + x;
if (m_drawAlpha) {
// set color in case it wasn't done in renderMapBlock()
setZoomed(imageX, imageY, m_color[z][x]);
m_readPixels[z] |= (1 << x);
m_blockPixelAttributes.attribute(15 - z, xBegin + x).thickness = m_thickness[z][x];
}
}
}
}
void TileGenerator::renderShading(int zPos)
{
int zBegin = (m_zMax - zPos) * 16;
for (int z = 0; z < 16; ++z) {
int imageY = zBegin + z;
if (imageY >= m_mapHeight)
continue;
for (int x = 0; x < m_mapWidth; ++x) {
if(
!m_blockPixelAttributes.attribute(z, x).valid_height() ||
!m_blockPixelAttributes.attribute(z, x - 1).valid_height() ||
!m_blockPixelAttributes.attribute(z - 1, x).valid_height()
)
continue;
// calculate shadow to apply
int y = m_blockPixelAttributes.attribute(z, x).height;
int y1 = m_blockPixelAttributes.attribute(z, x - 1).height;
int y2 = m_blockPixelAttributes.attribute(z - 1, x).height;
int d = ((y - y1) + (y - y2)) * 12;
if (d > 36)
d = 36;
// more thickness -> less visible shadows: t=0 -> 100% visible, t=255 -> 0% visible
if (m_drawAlpha)
d = d * ((0xFF - m_blockPixelAttributes.attribute(z, x).thickness) / 255.0);
Color c = m_image->getPixel(getImageX(x), getImageY(imageY));
c.r = colorSafeBounds(c.r + d);
c.g = colorSafeBounds(c.g + d);
c.b = colorSafeBounds(c.b + d);
setZoomed(x, imageY, c);
}
}
m_blockPixelAttributes.scroll();
}
void TileGenerator::renderScale()
{
if (m_scales & SCALE_TOP) {
m_image->drawText(24, 0, "X", m_scaleColor);
for (int i = (m_xMin / 4) * 4; i <= m_xMax; i += 4) {
stringstream buf;
buf << i * 16;
int xPos = (m_xMin * -16 + i * 16)*m_zoom + m_xBorder;
m_image->drawText(xPos + 2, 0, buf.str(), m_scaleColor);
m_image->drawLine(xPos, 0, xPos, m_yBorder - 1, m_scaleColor);
}
}
if (m_scales & SCALE_LEFT) {
m_image->drawText(2, 24, "Z", m_scaleColor);
for (int i = (m_zMax / 4) * 4; i >= m_zMin; i -= 4) {
stringstream buf;
buf << i * 16;
int yPos = (m_mapHeight - 1 - (i * 16 - m_zMin * 16))*m_zoom + m_yBorder;
m_image->drawText(2, yPos, buf.str(), m_scaleColor);
m_image->drawLine(0, yPos, m_xBorder - 1, yPos, m_scaleColor);
}
}
if (m_scales & SCALE_BOTTOM) {
for (int i = (m_xMin / 4) * 4; i <= m_xMax; i += 4) {
stringstream buf;
buf << i * 16;
int xPos = (m_xMin * -16 + i * 16)*m_zoom + m_xBorder;
int yPos = m_yBorder + m_mapHeight*m_zoom;
m_image->drawText(xPos + 2, yPos, buf.str(), m_scaleColor);
m_image->drawLine(xPos, yPos, xPos, yPos + 39, m_scaleColor);
}
}
if (m_scales & SCALE_RIGHT) {
for (int i = (m_zMax / 4) * 4; i >= m_zMin; i -= 4) {
stringstream buf;
buf << i * 16;
int xPos = m_xBorder + m_mapWidth*m_zoom;
int yPos = (m_mapHeight - 1 - (i * 16 - m_zMin * 16))*m_zoom + m_yBorder;
m_image->drawText(xPos + 2, yPos, buf.str(), m_scaleColor);
m_image->drawLine(xPos, yPos, xPos + 39, yPos, m_scaleColor);
}
}
}
void TileGenerator::renderOrigin()
{
int imageX = (-m_xMin * 16)*m_zoom + m_xBorder;
int imageY = (m_mapHeight - m_zMin * -16)*m_zoom + m_yBorder;
m_image->drawCircle(imageX, imageY, 12, m_originColor);
}
void TileGenerator::renderPlayers(const std::string &inputPath)
{
PlayerAttributes players(inputPath);
for (PlayerAttributes::Players::iterator player = players.begin(); player != players.end(); ++player) {
int imageX = (player->x / 10 - m_xMin * 16)*m_zoom + m_xBorder;
int imageY = (m_mapHeight - (player->z / 10 - m_zMin * 16))*m_zoom + m_yBorder;
m_image->drawCircle(imageX, imageY, 5, m_playerColor);
m_image->drawText(imageX + 2, imageY + 2, player->name, m_playerColor);
}
}
inline std::list<int> TileGenerator::getZValueList() const
{
std::list<int> zlist;
for (std::list<std::pair<int, int> >::const_iterator position = m_positions.begin(); position != m_positions.end(); ++position)
zlist.push_back(position->second);
zlist.sort();
zlist.unique();
zlist.reverse();
return zlist;
}
void TileGenerator::writeImage(const std::string &output)
{
m_image->save(output);
delete m_image;
m_image = NULL;
}
void TileGenerator::printUnknown()
{
if (m_unknownNodes.size() > 0) {
std::cerr << "Unknown nodes:" << std::endl;
for (NameSet::iterator node = m_unknownNodes.begin(); node != m_unknownNodes.end(); ++node) {
std::cerr << *node << std::endl;
}
}
}
inline int TileGenerator::getImageX(int val) const
{
return (m_zoom*val) + m_xBorder;
}
inline int TileGenerator::getImageY(int val) const
{
return (m_zoom*val) + m_yBorder;
}
inline void TileGenerator::setZoomed(int x, int y, Color color)
{
m_image->drawFilledRect(getImageX(x), getImageY(y), m_zoom, m_zoom, color);
}

138
TileGenerator.h Normal file
View File

@ -0,0 +1,138 @@
#ifndef TILEGENERATOR_HEADER
#define TILEGENERATOR_HEADER
#include <gd.h>
#include <iosfwd>
#include <list>
#include <config.h>
#if __cplusplus >= 201103L
#include <unordered_map>
#include <unordered_set>
#else
#include <map>
#include <set>
#endif
#include <stdint.h>
#include <string>
#include "PixelAttributes.h"
#include "Image.h"
#include "db.h"
#include "types.h"
enum {
SCALE_TOP = (1 << 0),
SCALE_BOTTOM = (1 << 1),
SCALE_LEFT = (1 << 2),
SCALE_RIGHT = (1 << 3),
};
struct ColorEntry {
ColorEntry(): r(0), g(0), b(0), a(0), t(0) {};
ColorEntry(uint8_t r, uint8_t g, uint8_t b, uint8_t a, uint8_t t): r(r), g(g), b(b), a(a), t(t) {};
inline Color to_color() const { return Color(r, g, b, a); }
uint8_t r;
uint8_t g;
uint8_t b;
uint8_t a;
uint8_t t;
};
class TileGenerator
{
private:
#if __cplusplus >= 201103L
typedef std::unordered_map<std::string, ColorEntry> ColorMap;
typedef std::unordered_map<int, std::string> NameMap;
typedef std::unordered_set<std::string> NameSet;
#else
typedef std::map<std::string, ColorEntry> ColorMap;
typedef std::map<int, std::string> NameMap;
typedef std::set<std::string> NameSet;
#endif
public:
TileGenerator();
~TileGenerator();
void setBgColor(const std::string &bgColor);
void setScaleColor(const std::string &scaleColor);
void setOriginColor(const std::string &originColor);
void setPlayerColor(const std::string &playerColor); Color parseColor(const std::string &color);
void setDrawOrigin(bool drawOrigin);
void setDrawPlayers(bool drawPlayers);
void setDrawScale(bool drawScale);
void setDrawAlpha(bool drawAlpha);
void setShading(bool shading);
void setGeometry(int x, int y, int w, int h);
void setMinY(int y);
void setMaxY(int y);
void parseColorsFile(const std::string &fileName);
void setBackend(std::string backend);
void generate(const std::string &input, const std::string &output);
void setZoom(int zoom);
void setScales(uint flags);
private:
void parseColorsStream(std::istream &in);
void openDb(const std::string &input);
void closeDatabase();
void loadBlocks();
void createImage();
void renderMap();
std::list<int> getZValueList() const;
void renderMapBlock(const ustring &mapBlock, const BlockPos &pos, int version);
void renderMapBlockBottom(const BlockPos &pos);
void renderShading(int zPos);
void renderScale();
void renderOrigin();
void renderPlayers(const std::string &inputPath);
void writeImage(const std::string &output);
void printUnknown();
int getImageX(int val) const;
int getImageY(int val) const;
void setZoomed(int x, int y, Color color);
private:
Color m_bgColor;
Color m_scaleColor;
Color m_originColor;
Color m_playerColor;
bool m_drawOrigin;
bool m_drawPlayers;
bool m_drawScale;
bool m_drawAlpha;
bool m_shading;
std::string m_backend;
int m_xBorder, m_yBorder;
DB *m_db;
Image *m_image;
PixelAttributes m_blockPixelAttributes;
int m_xMin;
int m_xMax;
int m_zMin;
int m_zMax;
int m_yMin;
int m_yMax;
int m_geomX;
int m_geomY;
int m_geomX2;
int m_geomY2;
int m_mapWidth;
int m_mapHeight;
std::list<std::pair<int, int> > m_positions;
NameMap m_nameMap;
ColorMap m_colorMap;
uint16_t m_readPixels[16];
uint16_t m_readInfo[16];
NameSet m_unknownNodes;
Color m_color[16][16];
uint8_t m_thickness[16][16];
int m_blockAirId;
int m_blockIgnoreId;
int m_zoom;
uint m_scales;
}; // class TileGenerator
#endif // TILEGENERATOR_HEADER

71
ZlibDecompressor.cpp Normal file
View File

@ -0,0 +1,71 @@
/*
* =====================================================================
* Version: 1.0
* Created: 18.09.2012 10:20:47
* Author: Miroslav Bendík
* Company: LinuxOS.sk
* =====================================================================
*/
#include <zlib.h>
#include <stdint.h>
#include "ZlibDecompressor.h"
ZlibDecompressor::ZlibDecompressor(const unsigned char *data, std::size_t size):
m_data(data),
m_seekPos(0),
m_size(size)
{
}
ZlibDecompressor::~ZlibDecompressor()
{
}
void ZlibDecompressor::setSeekPos(std::size_t seekPos)
{
m_seekPos = seekPos;
}
std::size_t ZlibDecompressor::seekPos() const
{
return m_seekPos;
}
ustring ZlibDecompressor::decompress()
{
const unsigned char *data = m_data + m_seekPos;
const std::size_t size = m_size - m_seekPos;
ustring buffer;
const size_t BUFSIZE = 128 * 1024;
uint8_t temp_buffer[BUFSIZE];
z_stream strm;
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
strm.next_in = Z_NULL;
strm.avail_in = size;
if (inflateInit(&strm) != Z_OK) {
throw DecompressError();
}
strm.next_in = const_cast<unsigned char *>(data);
int ret = 0;
do {
strm.avail_out = BUFSIZE;
strm.next_out = temp_buffer;
ret = inflate(&strm, Z_NO_FLUSH);
buffer += ustring(reinterpret_cast<unsigned char *>(temp_buffer), BUFSIZE - strm.avail_out);
} while (ret == Z_OK);
if (ret != Z_STREAM_END) {
throw DecompressError();
}
m_seekPos += strm.next_in - data;
(void)inflateEnd(&strm);
return buffer;
}

37
ZlibDecompressor.h Normal file
View File

@ -0,0 +1,37 @@
/*
* =====================================================================
* Version: 1.0
* Created: 18.09.2012 10:20:51
* Author: Miroslav Bendík
* Company: LinuxOS.sk
* =====================================================================
*/
#ifndef ZLIBDECOMPRESSOR_H_ZQL1PN8Q
#define ZLIBDECOMPRESSOR_H_ZQL1PN8Q
#include <cstdlib>
#include <string>
#include "types.h"
class ZlibDecompressor
{
public:
class DecompressError {
};
ZlibDecompressor(const unsigned char *data, std::size_t size);
~ZlibDecompressor();
void setSeekPos(std::size_t seekPos);
std::size_t seekPos() const;
ustring decompress();
private:
const unsigned char *m_data;
std::size_t m_seekPos;
std::size_t m_size;
}; /* ----- end of class ZlibDecompressor ----- */
#endif /* end of include guard: ZLIBDECOMPRESSOR_H_ZQL1PN8Q */

131
autogenerating-colors.txt Normal file
View File

@ -0,0 +1,131 @@
==FILE== mods/dumpnodes/init.lua
local function nd_get_tiles(nd)
return nd.tiles or nd.tile_images
end
local function nd_get_tile(nd, n)
local tile = nd_get_tiles(nd)[n]
if type(tile) == 'table' then
tile = tile.name
end
return tile
end
local function pairs_s(dict)
local keys = {}
for k in pairs(dict) do
table.insert(keys, k)
end
table.sort(keys)
return ipairs(keys)
end
minetest.register_chatcommand("dumpnodes", {
params = "",
description = "",
func = function(player, param)
local n = 0
local ntbl = {}
for _, nn in pairs_s(minetest.registered_nodes) do
local nd = minetest.registered_nodes[nn]
local prefix, name = nn:match('(.*):(.*)')
if prefix == nil or name == nil then
print("ignored(1): " .. nn)
else
if ntbl[prefix] == nil then
ntbl[prefix] = {}
end
ntbl[prefix][name] = true
end
end
local out, err = io.open('nodes.txt', 'wb')
if not out then
return true, "io.open(): " .. err
end
for _, prefix in pairs_s(ntbl) do
out:write('# ' .. prefix .. '\n')
for _, name in pairs_s(ntbl[prefix]) do
local nn = prefix .. ":" .. name
local nd = minetest.registered_nodes[nn]
if nd.drawtype == 'airlike' or nd_get_tiles(nd) == nil then
print("ignored(2): " .. nn)
else
local tl = nd_get_tile(nd, 1)
tl = (tl .. '^'):match('(.-)^') -- strip modifiers
out:write(nn .. ' ' .. tl .. '\n')
n = n + 1
end
end
out:write('\n')
end
out:close()
return true, n .. " nodes dumped."
end,
})
==FILE== avgcolor.py
#!/usr/bin/env python
import sys
from PIL import Image
def tadd(a, b):
return tuple(sum(e) for e in zip(a, b))
if len(sys.argv) < 2:
print("Prints average color (RGB) of input image")
print("Usage: %s <input>" % sys.argv[0])
exit(1)
inp = Image.open(sys.argv[1]).convert('RGBA')
ind = inp.load()
cl = (0, 0, 0)
counted = 0
for x in range(inp.size[0]):
for y in range(inp.size[1]):
px = ind[x, y]
if px[3] < 128: continue # alpha
cl = tadd(cl, px[:3])
counted += 1
if counted == 0:
sys.stderr.write("did not find avg color for %s\n" % sys.argv[1])
print("0 0 0")
else:
cl = tuple(int(n / counted) for n in cl)
print("%d %d %d" % cl)
==SCRIPT==
#!/bin/bash -e
AVGCOLOR_PATH=/path/to/avgcolor.py
GAME_PATH=/path/to/minetest_game
MODS_PATH= # path to "mods" folder, only set if you have loaded mods
NODESTXT_PATH=./nodes.txt
COLORSTXT_PATH=./colors.txt
while read -r line; do
set -- junk $line; shift
if [[ -z "$1" || $1 == "#" ]]; then
echo "$line"; continue
fi
tex=$(find $GAME_PATH -type f -name "$2")
[[ -z "$tex" && -n "$MODS_PATH" ]] && tex=$(find $MODS_PATH -type f -name "$2")
if [ -z "$tex" ]; then
echo "skip $1: texture not found" >&2
continue
fi
echo "$1" $(python $AVGCOLOR_PATH "$tex")
echo "ok $1" >&2
done < $NODESTXT_PATH > $COLORSTXT_PATH
# Use nicer colors for water and lava:
sed -re 's/^default:((river_)?water_(flowing|source)) [0-9 ]+$/default:\1 39 66 106 128 224/g' $COLORSTXT_PATH -i
sed -re 's/^default:(lava_(flowing|source)) [0-9 ]+$/default:\1 255 100 0/g' $COLORSTXT_PATH -i
# Add transparency to glass nodes:
sed -re 's/^default:(.*glass) ([0-9 ]+)$/default:\1 \2 64 16/g' $COLORSTXT_PATH -i
sed -re 's/^doors:(.*glass[^ ]*) ([0-9 ]+)$/doors:\1 \2 64 16/g' $COLORSTXT_PATH -i
# Fix xpanes color:
sed -re 's/^xpanes:((pane|bar)(_flat)?) [0-9 ]+$/xpanes:\1 194 194 227 64 16/g' $COLORSTXT_PATH -i
==INSTRUCTIONS==
1) Make sure avgcolors.py works (outputs the usage instructions when run)
2) Add the dumpnodes mod to Minetest
3) Create a world and load dumpnodes & all mods you want to generate colors for
4) Execute /dumpnodes ingame
5) Run the script to generate colors.txt (make sure to adjust the PATH variables at the top)

View File

@ -1,24 +0,0 @@
mark_as_advanced(ZSTD_LIBRARY ZSTD_INCLUDE_DIR)
find_path(ZSTD_INCLUDE_DIR NAMES zstd.h)
find_library(ZSTD_LIBRARY NAMES zstd)
if(ZSTD_INCLUDE_DIR AND ZSTD_LIBRARY)
# Check that the API we use exists
include(CheckSymbolExists)
unset(HAVE_ZSTD_INITDSTREAM CACHE)
set(CMAKE_REQUIRED_INCLUDES ${ZSTD_INCLUDE_DIR})
set(CMAKE_REQUIRED_LIBRARIES ${ZSTD_LIBRARY})
check_symbol_exists(ZSTD_initDStream zstd.h HAVE_ZSTD_INITDSTREAM)
unset(CMAKE_REQUIRED_INCLUDES)
unset(CMAKE_REQUIRED_LIBRARIES)
if(NOT HAVE_ZSTD_INITDSTREAM)
unset(ZSTD_INCLUDE_DIR CACHE)
unset(ZSTD_LIBRARY CACHE)
endif()
endif()
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Zstd DEFAULT_MSG ZSTD_LIBRARY ZSTD_INCLUDE_DIR)

View File

@ -3,10 +3,9 @@
#ifndef CMAKE_CONFIG_H #ifndef CMAKE_CONFIG_H
#define CMAKE_CONFIG_H #define CMAKE_CONFIG_H
#cmakedefine01 USE_POSTGRESQL #define USE_POSTGRESQL @USE_POSTGRESQL@
#cmakedefine01 USE_LEVELDB #define USE_LEVELDB @USE_LEVELDB@
#cmakedefine01 USE_REDIS #define USE_REDIS @USE_REDIS@
#cmakedefine01 USE_ZLIB_NG
#define SHAREDIR "@SHAREDIR@" #define SHAREDIR "@SHAREDIR@"

View File

@ -1,163 +1,127 @@
# beds # beds
beds:bed_bottom 130 3 3 beds:bed_bottom 130 3 3
beds:bed_top 185 162 163 beds:bed_top 178 116 116
beds:fancy_bed_bottom 136 49 28 beds:fancy_bed_bottom 135 23 14
beds:fancy_bed_top 179 153 148 beds:fancy_bed_top 172 112 103
# bones # bones
bones:bones 117 117 117 bones:bones 86 86 86
# butterflies
# carts # carts
carts:brakerail 150 121 102 carts:brakerail 129 110 81
carts:powerrail 160 145 102 carts:powerrail 154 136 81
carts:rail 146 128 108 carts:rail 143 123 90
# default # default
default:acacia_bush_leaves 109 133 87 default:acacia_bush_leaves 90 124 55
default:acacia_bush_sapling 85 121 61 default:acacia_bush_sapling 85 118 58
default:acacia_bush_stem 84 77 70 default:acacia_bush_stem 84 76 69
default:acacia_leaves 126 153 101 default:acacia_leaves 108 147 67
default:acacia_sapling 87 120 64 default:acacia_sapling 87 116 61
default:acacia_tree 195 119 97 default:acacia_tree 188 109 90
default:acacia_wood 150 61 39 default:acacia_wood 146 60 37
default:apple 161 34 19 default:apple 145 20 9
default:aspen_leaves 72 105 29 default:aspen_leaves 70 104 27
default:aspen_sapling 85 123 45 default:aspen_sapling 82 110 43
default:aspen_tree 218 198 168 default:aspen_tree 218 197 166
default:aspen_wood 210 199 170 default:aspen_wood 209 198 169
default:blueberry_bush_leaves 63 99 22 default:bookshelf 128 99 55
default:blueberry_bush_leaves_with_berries 63 99 22 default:brick 117 71 69
default:blueberry_bush_sapling 81 112 33 default:bronzeblock 185 110 15
default:bookshelf 131 102 57 default:bush_leaves 34 52 29
default:brick 123 99 95 default:bush_sapling 65 60 40
default:bronzeblock 186 111 15 default:bush_stem 45 33 23
default:bush_leaves 35 55 29 default:cactus 52 116 15
default:bush_sapling 66 64 40 default:chest 140 108 65
default:bush_stem 46 34 24 default:chest_locked 140 108 65
default:cactus 70 119 52 default:chest_locked_open 140 108 65
default:cave_ice 168 206 247 default:chest_open 140 108 65
default:chest 149 115 69 default:clay 182 182 182
default:chest_locked 149 115 69
default:chest_locked_open 149 115 69
default:chest_open 149 115 69
default:clay 183 183 183
default:cloud 255 255 255 default:cloud 255 255 255
default:coalblock 58 58 58 default:coalblock 57 57 57
default:cobble 89 86 84 default:cobble 88 84 82
default:copperblock 193 126 65 default:copperblock 192 126 63
default:coral_brown 146 113 77 default:coral_brown 139 104 72
default:coral_cyan 235 230 215 default:coral_orange 191 62 12
default:coral_green 235 230 215 default:coral_skeleton 235 230 214
default:coral_orange 197 68 17 default:desert_cobble 146 95 76
default:coral_pink 235 230 215
default:coral_skeleton 235 230 215
default:desert_cobble 110 67 50
default:desert_sand 206 165 98 default:desert_sand 206 165 98
default:desert_sandstone 195 152 92 default:desert_sandstone 195 152 92
default:desert_sandstone_block 193 152 94 default:desert_sandstone_block 192 151 94
default:desert_sandstone_brick 191 151 95 default:desert_sandstone_brick 191 151 94
default:desert_stone 130 79 61 default:desert_stone 129 79 60
default:desert_stone_block 131 80 61 default:desert_stone_block 130 79 60
default:desert_stonebrick 131 80 61 default:desert_stonebrick 129 79 60
default:diamondblock 140 218 223 default:diamondblock 135 217 223
default:dirt 97 67 43 default:dirt 95 64 39
default:dirt_with_coniferous_litter 109 90 71 default:dirt_with_dry_grass 187 148 77
default:dirt_with_dry_grass 187 148 78 default:dirt_with_grass 66 112 31
default:dirt_with_grass 64 111 26 default:dirt_with_grass_footsteps 66 112 31
default:dirt_with_grass_footsteps 64 111 26 default:dirt_with_rainforest_litter 74 38 8
default:dirt_with_rainforest_litter 76 39 10 default:dirt_with_snow 224 225 238
default:dirt_with_snow 225 225 238
default:dry_dirt 178 136 90
default:dry_dirt_with_dry_grass 187 148 78
default:dry_grass_1 208 172 87 default:dry_grass_1 208 172 87
default:dry_grass_2 210 174 87 default:dry_grass_2 210 174 87
default:dry_grass_3 210 174 87 default:dry_grass_3 210 174 87
default:dry_grass_4 211 175 88 default:dry_grass_4 211 175 88
default:dry_grass_5 214 178 92 default:dry_grass_5 213 178 92
default:dry_shrub 103 67 18 default:dry_shrub 101 66 17
default:emergent_jungle_sapling 51 40 16 default:fence_acacia_wood 147 60 38
default:fence_acacia_wood 151 62 39
default:fence_aspen_wood 210 199 170 default:fence_aspen_wood 210 199 170
default:fence_junglewood 57 39 14 default:fence_junglewood 54 37 11
default:fence_pine_wood 221 185 131 default:fence_pine_wood 221 184 129
default:fence_rail_acacia_wood 150 61 39 default:fence_wood 129 100 55
default:fence_rail_aspen_wood 209 198 170 default:furnace 100 96 94
default:fence_rail_junglewood 56 39 14 default:furnace_active 100 96 94
default:fence_rail_pine_wood 221 184 130 default:glass 194 194 227 64 16
default:fence_rail_wood 131 102 57 default:goldblock 230 201 29
default:fence_wood 132 103 57 default:grass_1 97 138 53
default:fern_1 85 118 51 default:grass_2 95 136 54
default:fern_2 90 123 53 default:grass_3 91 133 52
default:fern_3 91 125 54 default:grass_4 86 130 45
default:furnace 101 98 96 default:grass_5 83 124 47
default:furnace_active 101 98 96 default:gravel 131 131 131
default:glass 247 247 247 64 16 default:ice 167 206 247
default:goldblock 231 203 35 default:junglegrass 63 105 25
default:grass_1 100 140 54 default:jungleleaves 20 28 15
default:grass_2 98 139 55 default:junglesapling 48 38 14
default:grass_3 94 136 53 default:jungletree 105 76 41
default:grass_4 89 133 48 default:junglewood 54 37 11
default:grass_5 86 126 48 default:ladder_steel 126 126 126
default:gravel 132 132 132 default:ladder_wood 123 90 34
default:ice 168 206 247
default:junglegrass 67 110 28
default:jungleleaves 22 31 16
default:junglesapling 51 39 15
default:jungletree 121 97 62
default:junglewood 56 39 14
default:ladder_steel 132 132 132
default:ladder_wood 125 93 43
default:large_cactus_seedling 67 107 52
default:lava_flowing 255 100 0 default:lava_flowing 255 100 0
default:lava_source 255 100 0 default:lava_source 255 100 0
default:leaves 36 55 29 default:leaves 34 52 29
default:marram_grass_1 113 139 96 default:mese 220 220 0
default:marram_grass_2 102 131 90 default:mese_post_light 131 102 57
default:marram_grass_3 99 130 88 default:meselamp 211 213 139
default:mese 222 222 0 default:mossycobble 86 90 68
default:mese_post_light 132 103 57 default:obsidian 19 21 24
default:mese_post_light_acacia_wood 151 62 39 default:obsidian_block 20 22 25
default:mese_post_light_aspen_wood 210 199 170 default:obsidian_glass 19 21 23 64 16
default:mese_post_light_junglewood 57 39 14 default:obsidianbrick 20 22 24
default:mese_post_light_pine_wood 221 185 131 default:papyrus 94 132 33
default:meselamp 213 215 143 default:pine_needles 13 36 21
default:mossycobble 88 91 73 default:pine_sapling 27 48 25
default:obsidian 21 24 29 default:pine_tree 182 155 124
default:obsidian_block 23 25 30 default:pine_wood 221 184 128
default:obsidian_glass 20 23 27 64 16
default:obsidianbrick 23 25 29
default:papyrus 97 134 38
default:permafrost 71 66 61
default:permafrost_with_moss 108 150 51
default:permafrost_with_stones 71 66 61
default:pine_bush_needles 16 50 19
default:pine_bush_sapling 58 51 40
default:pine_bush_stem 73 62 53
default:pine_needles 16 50 19
default:pine_sapling 41 48 26
default:pine_tree 191 165 132
default:pine_wood 221 185 130
default:river_water_flowing 39 66 106 128 224 default:river_water_flowing 39 66 106 128 224
default:river_water_source 39 66 106 128 224 default:river_water_source 39 66 106 128 224
default:sand 214 207 158 default:sand 214 207 158
default:sand_with_kelp 214 207 158 default:sandstone 197 193 143
default:sandstone 198 193 143 default:sandstone_block 195 190 141
default:sandstone_block 195 191 142 default:sandstonebrick 193 189 140
default:sandstonebrick 194 190 141 default:sapling 65 59 40
default:sapling 67 63 41 default:sign_wall_steel 144 144 144
default:sign_wall_steel 147 147 147 default:sign_wall_wood 145 101 64
default:sign_wall_wood 148 103 66
default:silver_sand 193 191 179 default:silver_sand 193 191 179
default:silver_sandstone 195 192 181 default:silver_sandstone 195 192 181
default:silver_sandstone_block 192 190 180 default:silver_sandstone_block 192 190 179
default:silver_sandstone_brick 191 189 179 default:silver_sandstone_brick 190 188 178
default:snow 225 225 238 default:snow 224 225 238
default:snowblock 225 225 238 default:snowblock 224 225 238
default:steelblock 195 195 195 default:steelblock 194 194 194
default:stone 97 94 93 default:stone 97 94 93
default:stone_block 100 97 96 default:stone_block 99 96 95
default:stone_with_coal 97 94 93 default:stone_with_coal 97 94 93
default:stone_with_copper 97 94 93 default:stone_with_copper 97 94 93
default:stone_with_diamond 97 94 93 default:stone_with_diamond 97 94 93
@ -165,292 +129,188 @@ default:stone_with_gold 97 94 93
default:stone_with_iron 97 94 93 default:stone_with_iron 97 94 93
default:stone_with_mese 97 94 93 default:stone_with_mese 97 94 93
default:stone_with_tin 97 94 93 default:stone_with_tin 97 94 93
default:stonebrick 102 99 98 default:stonebrick 99 96 95
default:tinblock 150 150 150 default:tinblock 149 149 149
default:torch 141 123 93 default:torch 120 98 67
default:torch_ceiling 141 123 93 default:torch_ceiling 120 98 67
default:torch_wall 141 123 93 default:torch_wall 120 98 67
default:tree 179 145 99 default:tree 164 131 88
default:water_flowing 39 66 106 128 224 default:water_flowing 39 66 106 128 224
default:water_source 39 66 106 128 224 default:water_source 39 66 106 128 224
default:wood 131 102 57 default:wood 128 99 55
# doors # doors
doors:door_glass_a 245 245 245 64 16 doors:door_glass_a 184 184 216 64 16
doors:door_glass_b 245 245 245 64 16 doors:door_glass_b 184 184 216 64 16
doors:door_glass_c 245 245 245 64 16 doors:door_obsidian_glass_a 27 28 29 64 16
doors:door_glass_d 245 245 245 64 16 doors:door_obsidian_glass_b 27 28 29 64 16
doors:door_obsidian_glass_a 48 49 50 64 16 doors:door_steel_a 201 201 201
doors:door_obsidian_glass_b 48 49 50 64 16 doors:door_steel_b 201 201 201
doors:door_obsidian_glass_c 48 49 50 64 16 doors:door_wood_a 87 67 35
doors:door_obsidian_glass_d 48 49 50 64 16 doors:door_wood_b 87 67 35
doors:door_steel_a 203 203 203 doors:gate_acacia_wood_closed 146 60 37
doors:door_steel_b 203 203 203 doors:gate_acacia_wood_open 146 60 37
doors:door_steel_c 203 203 203 doors:gate_aspen_wood_closed 209 198 169
doors:door_steel_d 203 203 203 doors:gate_aspen_wood_open 209 198 169
doors:door_wood_a 89 68 37 doors:gate_junglewood_closed 54 37 11
doors:door_wood_b 89 68 37 doors:gate_junglewood_open 54 37 11
doors:door_wood_c 89 68 37 doors:gate_pine_wood_closed 221 184 128
doors:door_wood_d 89 68 37 doors:gate_pine_wood_open 221 184 128
doors:gate_acacia_wood_closed 150 61 39 doors:gate_wood_closed 128 99 55
doors:gate_acacia_wood_open 150 61 39 doors:gate_wood_open 128 99 55
doors:gate_aspen_wood_closed 210 199 170 doors:hidden 0 0 0
doors:gate_aspen_wood_open 210 199 170 doors:trapdoor 128 99 50
doors:gate_junglewood_closed 56 39 14 doors:trapdoor_open 225 217 206
doors:gate_junglewood_open 56 39 14 doors:trapdoor_steel 199 199 199
doors:gate_pine_wood_closed 221 185 130 doors:trapdoor_steel_open 206 206 206
doors:gate_pine_wood_open 221 185 130
doors:gate_wood_closed 131 102 57
doors:gate_wood_open 131 102 57
doors:trapdoor 130 100 51
doors:trapdoor_open 68 53 30
doors:trapdoor_steel 200 200 200
doors:trapdoor_steel_open 97 97 97
# farming # farming
farming:cotton_1 89 117 39 farming:cotton_1 88 116 39
farming:cotton_2 89 116 38 farming:cotton_2 87 116 38
farming:cotton_3 99 121 41 farming:cotton_3 97 120 41
farming:cotton_4 108 114 47 farming:cotton_4 106 113 47
farming:cotton_5 116 105 53 farming:cotton_5 114 104 53
farming:cotton_6 121 95 59 farming:cotton_6 119 94 59
farming:cotton_7 94 70 37 farming:cotton_7 92 69 37
farming:cotton_8 122 108 93 farming:cotton_8 110 91 61
farming:cotton_wild 111 111 101 farming:desert_sand_soil 159 131 70
farming:desert_sand_soil 161 132 72 farming:desert_sand_soil_wet 119 98 52
farming:desert_sand_soil_wet 120 99 53 farming:seed_cotton 90 85 57
farming:dry_soil 178 136 90 farming:seed_wheat 175 159 93
farming:dry_soil_wet 178 136 90 farming:soil 95 64 39
farming:seed_cotton 92 87 60 farming:soil_wet 95 64 39
farming:seed_wheat 177 161 96 farming:straw 211 182 67
farming:soil 97 67 43 farming:wheat_1 107 174 32
farming:soil_wet 97 67 43 farming:wheat_2 133 175 50
farming:straw 212 184 68 farming:wheat_3 162 182 82
farming:wheat_1 110 175 36 farming:wheat_4 169 187 93
farming:wheat_2 136 177 53 farming:wheat_5 169 178 94
farming:wheat_3 163 182 84 farming:wheat_6 172 176 85
farming:wheat_4 170 188 95 farming:wheat_7 192 181 81
farming:wheat_5 171 179 97 farming:wheat_8 186 161 35
farming:wheat_6 173 177 87
farming:wheat_7 193 181 83
farming:wheat_8 187 162 40
# fire # fire
fire:basic_flame 223 136 44 fire:basic_flame 218 120 36
fire:permanent_flame 223 136 44 fire:permanent_flame 218 120 36
# fireflies
fireflies:firefly_bottle 191 194 202
# flowers # flowers
flowers:chrysanthemum_green 118 152 44 flowers:dandelion_white 178 176 140
flowers:dandelion_white 199 191 176 flowers:dandelion_yellow 178 148 24
flowers:dandelion_yellow 212 167 31 flowers:geranium 72 87 155
flowers:geranium 77 91 168 flowers:mushroom_brown 106 79 72
flowers:mushroom_brown 109 84 78 flowers:mushroom_red 192 85 85
flowers:mushroom_red 195 102 102 flowers:rose 118 48 24
flowers:rose 130 68 33 flowers:tulip 130 99 36
flowers:tulip 156 101 44 flowers:viola 106 60 159
flowers:tulip_black 78 120 72 flowers:waterlily 102 158 61
flowers:viola 115 69 184
flowers:waterlily 107 160 68
flowers:waterlily_waving 107 160 68
# stairs # stairs
stairs:slab_acacia_wood 150 61 39 stairs:slab_acacia_wood 146 60 37
stairs:slab_aspen_wood 210 199 170 stairs:slab_aspen_wood 209 198 169
stairs:slab_brick 123 99 95 stairs:slab_brick 117 71 69
stairs:slab_bronzeblock 186 111 15 stairs:slab_bronzeblock 185 110 15
stairs:slab_cobble 89 86 84 stairs:slab_cobble 88 84 82
stairs:slab_copperblock 193 126 65 stairs:slab_copperblock 192 126 63
stairs:slab_desert_cobble 110 67 50 stairs:slab_desert_cobble 146 95 76
stairs:slab_desert_sandstone 195 152 92 stairs:slab_desert_sandstone 195 152 92
stairs:slab_desert_sandstone_block 193 152 94 stairs:slab_desert_sandstone_block 192 151 94
stairs:slab_desert_sandstone_brick 191 151 95 stairs:slab_desert_sandstone_brick 191 151 94
stairs:slab_desert_stone 130 79 61 stairs:slab_desert_stone 129 79 60
stairs:slab_desert_stone_block 131 80 61 stairs:slab_desert_stone_block 130 79 60
stairs:slab_desert_stonebrick 131 80 61 stairs:slab_desert_stonebrick 129 79 60
stairs:slab_glass 247 247 247 stairs:slab_goldblock 230 201 29
stairs:slab_goldblock 231 203 35 stairs:slab_ice 167 206 247
stairs:slab_ice 168 206 247 stairs:slab_junglewood 54 37 11
stairs:slab_junglewood 56 39 14 stairs:slab_mossycobble 86 90 68
stairs:slab_mossycobble 88 91 73 stairs:slab_obsidian 19 21 24
stairs:slab_obsidian 21 24 29 stairs:slab_obsidian_block 20 22 25
stairs:slab_obsidian_block 23 25 30 stairs:slab_obsidianbrick 20 22 24
stairs:slab_obsidian_glass 20 23 27 stairs:slab_pine_wood 221 184 128
stairs:slab_obsidianbrick 23 25 29 stairs:slab_sandstone 197 193 143
stairs:slab_pine_wood 221 185 130 stairs:slab_sandstone_block 195 190 141
stairs:slab_sandstone 198 193 143 stairs:slab_sandstonebrick 193 189 140
stairs:slab_sandstone_block 195 191 142
stairs:slab_sandstonebrick 194 190 141
stairs:slab_silver_sandstone 195 192 181 stairs:slab_silver_sandstone 195 192 181
stairs:slab_silver_sandstone_block 192 190 180 stairs:slab_silver_sandstone_block 192 190 179
stairs:slab_silver_sandstone_brick 191 189 179 stairs:slab_silver_sandstone_brick 190 188 178
stairs:slab_snowblock 225 225 238 stairs:slab_snowblock 224 225 238
stairs:slab_steelblock 195 195 195 stairs:slab_steelblock 194 194 194
stairs:slab_stone 97 94 93 stairs:slab_stone 97 94 93
stairs:slab_stone_block 100 97 96 stairs:slab_stone_block 99 96 95
stairs:slab_stonebrick 102 99 98 stairs:slab_stonebrick 99 96 95
stairs:slab_straw 212 184 68 stairs:slab_straw 211 182 67
stairs:slab_tinblock 150 150 150 stairs:slab_wood 128 99 55
stairs:slab_wood 131 102 57 stairs:stair_acacia_wood 146 60 37
stairs:stair_acacia_wood 150 61 39 stairs:stair_aspen_wood 209 198 169
stairs:stair_aspen_wood 210 199 170 stairs:stair_brick 117 71 69
stairs:stair_brick 123 99 95 stairs:stair_bronzeblock 185 110 15
stairs:stair_bronzeblock 186 111 15 stairs:stair_cobble 88 84 82
stairs:stair_cobble 89 86 84 stairs:stair_copperblock 192 126 63
stairs:stair_copperblock 193 126 65 stairs:stair_desert_cobble 146 95 76
stairs:stair_desert_cobble 110 67 50
stairs:stair_desert_sandstone 195 152 92 stairs:stair_desert_sandstone 195 152 92
stairs:stair_desert_sandstone_block 193 152 94 stairs:stair_desert_sandstone_block 192 151 94
stairs:stair_desert_sandstone_brick 191 151 95 stairs:stair_desert_sandstone_brick 191 151 94
stairs:stair_desert_stone 130 79 61 stairs:stair_desert_stone 129 79 60
stairs:stair_desert_stone_block 131 80 61 stairs:stair_desert_stone_block 130 79 60
stairs:stair_desert_stonebrick 131 80 61 stairs:stair_desert_stonebrick 129 79 60
stairs:stair_glass 249 249 249 stairs:stair_goldblock 230 201 29
stairs:stair_goldblock 231 203 35 stairs:stair_ice 167 206 247
stairs:stair_ice 168 206 247 stairs:stair_junglewood 54 37 11
stairs:stair_inner_acacia_wood 150 61 39 stairs:stair_mossycobble 86 90 68
stairs:stair_inner_aspen_wood 210 199 170 stairs:stair_obsidian 19 21 24
stairs:stair_inner_brick 123 99 95 stairs:stair_obsidian_block 20 22 25
stairs:stair_inner_bronzeblock 186 111 15 stairs:stair_obsidianbrick 20 22 24
stairs:stair_inner_cobble 89 86 84 stairs:stair_pine_wood 221 184 128
stairs:stair_inner_copperblock 193 126 65 stairs:stair_sandstone 197 193 143
stairs:stair_inner_desert_cobble 110 67 50 stairs:stair_sandstone_block 195 190 141
stairs:stair_inner_desert_sandstone 195 152 92 stairs:stair_sandstonebrick 193 189 140
stairs:stair_inner_desert_sandstone_block 193 152 94
stairs:stair_inner_desert_sandstone_brick 191 151 95
stairs:stair_inner_desert_stone 130 79 61
stairs:stair_inner_desert_stone_block 131 80 61
stairs:stair_inner_desert_stonebrick 131 80 61
stairs:stair_inner_glass 250 250 250
stairs:stair_inner_goldblock 231 203 35
stairs:stair_inner_ice 168 206 247
stairs:stair_inner_junglewood 56 39 14
stairs:stair_inner_mossycobble 88 91 73
stairs:stair_inner_obsidian 21 24 29
stairs:stair_inner_obsidian_block 23 25 30
stairs:stair_inner_obsidian_glass 20 22 27
stairs:stair_inner_obsidianbrick 23 25 29
stairs:stair_inner_pine_wood 221 185 130
stairs:stair_inner_sandstone 198 193 143
stairs:stair_inner_sandstone_block 195 191 142
stairs:stair_inner_sandstonebrick 194 190 141
stairs:stair_inner_silver_sandstone 195 192 181
stairs:stair_inner_silver_sandstone_block 192 190 180
stairs:stair_inner_silver_sandstone_brick 191 189 179
stairs:stair_inner_snowblock 225 225 238
stairs:stair_inner_steelblock 195 195 195
stairs:stair_inner_stone 97 94 93
stairs:stair_inner_stone_block 100 97 96
stairs:stair_inner_stonebrick 102 99 98
stairs:stair_inner_straw 212 184 68
stairs:stair_inner_tinblock 150 150 150
stairs:stair_inner_wood 131 102 57
stairs:stair_junglewood 56 39 14
stairs:stair_mossycobble 88 91 73
stairs:stair_obsidian 21 24 29
stairs:stair_obsidian_block 23 25 30
stairs:stair_obsidian_glass 20 22 27
stairs:stair_obsidianbrick 23 25 29
stairs:stair_outer_acacia_wood 150 61 39
stairs:stair_outer_aspen_wood 210 199 170
stairs:stair_outer_brick 123 99 95
stairs:stair_outer_bronzeblock 186 111 15
stairs:stair_outer_cobble 89 86 84
stairs:stair_outer_copperblock 193 126 65
stairs:stair_outer_desert_cobble 110 67 50
stairs:stair_outer_desert_sandstone 195 152 92
stairs:stair_outer_desert_sandstone_block 193 152 94
stairs:stair_outer_desert_sandstone_brick 191 151 95
stairs:stair_outer_desert_stone 130 79 61
stairs:stair_outer_desert_stone_block 131 80 61
stairs:stair_outer_desert_stonebrick 131 80 61
stairs:stair_outer_glass 250 250 250
stairs:stair_outer_goldblock 231 203 35
stairs:stair_outer_ice 168 206 247
stairs:stair_outer_junglewood 56 39 14
stairs:stair_outer_mossycobble 88 91 73
stairs:stair_outer_obsidian 21 24 29
stairs:stair_outer_obsidian_block 23 25 30
stairs:stair_outer_obsidian_glass 20 22 27
stairs:stair_outer_obsidianbrick 23 25 29
stairs:stair_outer_pine_wood 221 185 130
stairs:stair_outer_sandstone 198 193 143
stairs:stair_outer_sandstone_block 195 191 142
stairs:stair_outer_sandstonebrick 194 190 141
stairs:stair_outer_silver_sandstone 195 192 181
stairs:stair_outer_silver_sandstone_block 192 190 180
stairs:stair_outer_silver_sandstone_brick 191 189 179
stairs:stair_outer_snowblock 225 225 238
stairs:stair_outer_steelblock 195 195 195
stairs:stair_outer_stone 97 94 93
stairs:stair_outer_stone_block 100 97 96
stairs:stair_outer_stonebrick 102 99 98
stairs:stair_outer_straw 212 184 68
stairs:stair_outer_tinblock 150 150 150
stairs:stair_outer_wood 131 102 57
stairs:stair_pine_wood 221 185 130
stairs:stair_sandstone 198 193 143
stairs:stair_sandstone_block 195 191 142
stairs:stair_sandstonebrick 194 190 141
stairs:stair_silver_sandstone 195 192 181 stairs:stair_silver_sandstone 195 192 181
stairs:stair_silver_sandstone_block 192 190 180 stairs:stair_silver_sandstone_block 192 190 179
stairs:stair_silver_sandstone_brick 191 189 179 stairs:stair_silver_sandstone_brick 190 188 178
stairs:stair_snowblock 225 225 238 stairs:stair_snowblock 224 225 238
stairs:stair_steelblock 195 195 195 stairs:stair_steelblock 194 194 194
stairs:stair_stone 97 94 93 stairs:stair_stone 97 94 93
stairs:stair_stone_block 100 97 96 stairs:stair_stone_block 99 96 95
stairs:stair_stonebrick 102 99 98 stairs:stair_stonebrick 99 96 95
stairs:stair_straw 212 184 68 stairs:stair_straw 211 182 67
stairs:stair_tinblock 150 150 150 stairs:stair_wood 128 99 55
stairs:stair_wood 131 102 57
# tnt # tnt
tnt:gunpowder 12 12 12 tnt:gunpowder 6 6 6
tnt:gunpowder_burning 156 143 7 tnt:gunpowder_burning 112 103 2
tnt:tnt 196 0 0 tnt:tnt 181 0 0
tnt:tnt_burning 201 41 0 tnt:tnt_burning 190 8 0
# vessels # vessels
vessels:drinking_glass 207 214 228 vessels:drinking_glass 206 214 228
vessels:glass_bottle 189 192 204 vessels:glass_bottle 188 190 201
vessels:shelf 131 102 57 vessels:shelf 128 99 55
vessels:steel_bottle 194 193 193 vessels:steel_bottle 193 192 191
# walls # walls
walls:cobble 89 86 84 walls:cobble 88 84 82
walls:desertcobble 110 67 50 walls:desertcobble 146 95 76
walls:mossycobble 88 91 73 walls:mossycobble 86 90 68
# wool # wool
wool:black 30 30 30 wool:black 29 29 29
wool:blue 0 73 146 wool:blue 0 72 145
wool:brown 88 44 0 wool:brown 86 42 0
wool:cyan 0 132 140 wool:cyan 0 130 139
wool:dark_green 33 103 0 wool:dark_green 32 102 0
wool:dark_grey 60 60 60 wool:dark_grey 59 59 59
wool:green 93 218 28 wool:green 92 216 28
wool:grey 133 133 133 wool:grey 131 131 131
wool:magenta 201 3 112 wool:magenta 200 3 109
wool:orange 214 83 22 wool:orange 213 81 21
wool:pink 255 133 133 wool:pink 255 131 131
wool:red 170 18 18 wool:red 168 17 17
wool:violet 93 5 169 wool:violet 92 2 169
wool:white 220 220 220 wool:white 220 220 220
wool:yellow 254 226 16 wool:yellow 254 225 15
# xpanes # xpanes
xpanes:bar 114 114 114 64 16 xpanes:bar 194 194 227 64 16
xpanes:bar_flat 114 114 114 64 16 xpanes:bar_flat 194 194 227 64 16
xpanes:door_steel_bar_a 133 133 133 64 16 xpanes:pane 194 194 227 64 16
xpanes:door_steel_bar_b 133 133 133 64 16 xpanes:pane_flat 194 194 227 64 16
xpanes:door_steel_bar_c 133 133 133 64 16
xpanes:door_steel_bar_d 133 133 133 64 16
xpanes:obsidian_pane 16 17 18 64 16
xpanes:obsidian_pane_flat 16 17 18 64 16
xpanes:pane 249 249 249 64 16
xpanes:pane_flat 249 249 249 64 16
xpanes:trapdoor_steel_bar 127 127 127 64 16
xpanes:trapdoor_steel_bar_open 77 77 77 64 16

17
config.h Normal file
View File

@ -0,0 +1,17 @@
#if MSDOS || __OS2__ || __NT__ || _WIN32
#define PATH_SEPARATOR '\\'
#else
#define PATH_SEPARATOR '/'
#endif
#define BLOCK_SIZE 16
#ifdef USE_CMAKE_CONFIG_H
#include "cmake_config.h"
#else
#define USE_POSTGRESQL 0
#define USE_LEVELDB 0
#define USE_REDIS 0
#define SHAREDIR "/usr/share/minetest"
#endif

74
db-leveldb.cpp Normal file
View File

@ -0,0 +1,74 @@
#include <stdexcept>
#include <sstream>
#include "db-leveldb.h"
#include "types.h"
static inline int64_t stoi64(const std::string &s)
{
std::stringstream tmp(s);
int64_t t;
tmp >> t;
return t;
}
static inline std::string i64tos(int64_t i)
{
std::ostringstream os;
os << i;
return os.str();
}
DBLevelDB::DBLevelDB(const std::string &mapdir)
{
leveldb::Options options;
options.create_if_missing = false;
leveldb::Status status = leveldb::DB::Open(options, mapdir + "map.db", &db);
if (!status.ok()) {
throw std::runtime_error(std::string("Failed to open Database: ") + status.ToString());
}
loadPosCache();
}
DBLevelDB::~DBLevelDB()
{
delete db;
}
std::vector<BlockPos> DBLevelDB::getBlockPos()
{
return posCache;
}
void DBLevelDB::loadPosCache()
{
leveldb::Iterator * it = db->NewIterator(leveldb::ReadOptions());
for (it->SeekToFirst(); it->Valid(); it->Next()) {
int64_t posHash = stoi64(it->key().ToString());
posCache.push_back(decodeBlockPos(posHash));
}
delete it;
}
void DBLevelDB::getBlocksOnZ(std::map<int16_t, BlockList> &blocks, int16_t zPos)
{
std::string datastr;
leveldb::Status status;
for (std::vector<BlockPos>::iterator it = posCache.begin(); it != posCache.end(); ++it) {
if (it->z != zPos) {
continue;
}
status = db->Get(leveldb::ReadOptions(), i64tos(encodeBlockPos(*it)), &datastr);
if (status.ok()) {
Block b(*it, ustring((const unsigned char *) datastr.data(), datastr.size()));
blocks[b.first.x].push_back(b);
}
}
}

21
db-leveldb.h Normal file
View File

@ -0,0 +1,21 @@
#ifndef DB_LEVELDB_HEADER
#define DB_LEVELDB_HEADER
#include "db.h"
#include <leveldb/db.h>
class DBLevelDB : public DB {
public:
DBLevelDB(const std::string &mapdir);
virtual std::vector<BlockPos> getBlockPos();
virtual void getBlocksOnZ(std::map<int16_t, BlockList> &blocks, int16_t zPos);
virtual ~DBLevelDB();
private:
void loadPosCache();
std::vector<BlockPos> posCache;
leveldb::DB *db;
};
#endif // DB_LEVELDB_HEADER

168
db-postgresql.cpp Normal file
View File

@ -0,0 +1,168 @@
#include <stdexcept>
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <arpa/inet.h>
#include "db-postgresql.h"
#include "util.h"
#include "types.h"
#define ARRLEN(x) (sizeof(x) / sizeof((x)[0]))
DBPostgreSQL::DBPostgreSQL(const std::string &mapdir)
{
std::ifstream ifs((mapdir + "/world.mt").c_str());
if(!ifs.good())
throw std::runtime_error("Failed to read world.mt");
std::string const connect_string = get_setting("pgsql_connection", ifs);
ifs.close();
db = PQconnectdb(connect_string.c_str());
if (PQstatus(db) != CONNECTION_OK) {
throw std::runtime_error(std::string(
"PostgreSQL database error: ") +
PQerrorMessage(db)
);
}
prepareStatement(
"get_block_pos",
"SELECT posX, posY, posZ FROM blocks"
);
prepareStatement(
"get_blocks_z",
"SELECT posX, posY, data FROM blocks WHERE posZ = $1::int4"
);
checkResults(PQexec(db, "START TRANSACTION;"));
checkResults(PQexec(db, "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;"));
}
DBPostgreSQL::~DBPostgreSQL()
{
try {
checkResults(PQexec(db, "COMMIT;"));
} catch (std::exception& caught) {
std::cerr << "could not finalize: " << caught.what() << std::endl;
}
PQfinish(db);
}
std::vector<BlockPos> DBPostgreSQL::getBlockPos()
{
std::vector<BlockPos> positions;
PGresult *results = execPrepared(
"get_block_pos", 0,
NULL, NULL, NULL, false, false
);
int numrows = PQntuples(results);
for (int row = 0; row < numrows; ++row)
positions.push_back(pg_to_blockpos(results, row, 0));
PQclear(results);
return positions;
}
void DBPostgreSQL::getBlocksOnZ(std::map<int16_t, BlockList> &blocks, int16_t zPos)
{
int32_t const z = htonl(zPos);
const void *args[] = { &z };
const int argLen[] = { sizeof(z) };
const int argFmt[] = { 1 };
PGresult *results = execPrepared(
"get_blocks_z", ARRLEN(args), args,
argLen, argFmt, false
);
int numrows = PQntuples(results);
for (int row = 0; row < numrows; ++row) {
BlockPos position;
position.x = pg_binary_to_int(results, row, 0);
position.y = pg_binary_to_int(results, row, 1);
position.z = zPos;
Block const b(
position,
ustring(
reinterpret_cast<unsigned char*>(
PQgetvalue(results, row, 2)
),
PQgetlength(results, row, 2)
)
);
blocks[position.x].push_back(b);
}
PQclear(results);
}
PGresult *DBPostgreSQL::checkResults(PGresult *res, bool clear)
{
ExecStatusType statusType = PQresultStatus(res);
switch (statusType) {
case PGRES_COMMAND_OK:
case PGRES_TUPLES_OK:
break;
case PGRES_FATAL_ERROR:
throw std::runtime_error(
std::string("PostgreSQL database error: ") +
PQresultErrorMessage(res)
);
default:
throw std::runtime_error(
"Unhandled PostgreSQL result code"
);
}
if (clear)
PQclear(res);
return res;
}
void DBPostgreSQL::prepareStatement(const std::string &name, const std::string &sql)
{
checkResults(PQprepare(db, name.c_str(), sql.c_str(), 0, NULL));
}
PGresult *DBPostgreSQL::execPrepared(
const char *stmtName, const int paramsNumber,
const void **params,
const int *paramsLengths, const int *paramsFormats,
bool clear, bool nobinary
)
{
return checkResults(PQexecPrepared(db, stmtName, paramsNumber,
(const char* const*) params, paramsLengths, paramsFormats,
nobinary ? 1 : 0), clear
);
}
int DBPostgreSQL::pg_to_int(PGresult *res, int row, int col)
{
return atoi(PQgetvalue(res, row, col));
}
int DBPostgreSQL::pg_binary_to_int(PGresult *res, int row, int col)
{
int32_t* raw = reinterpret_cast<int32_t*>(PQgetvalue(res, row, col));
return ntohl(*raw);
}
BlockPos DBPostgreSQL::pg_to_blockpos(PGresult *res, int row, int col)
{
BlockPos result;
result.x = pg_to_int(res, row, col);
result.y = pg_to_int(res, row, col + 1);
result.z = pg_to_int(res, row, col + 2);
return result;
}

29
db-postgresql.h Normal file
View File

@ -0,0 +1,29 @@
#ifndef _DB_POSTGRESQL_H
#define _DB_POSTGRESQL_H
#include "db.h"
#include <libpq-fe.h>
class DBPostgreSQL : public DB {
public:
DBPostgreSQL(const std::string &mapdir);
virtual std::vector<BlockPos> getBlockPos();
virtual void getBlocksOnZ(std::map<int16_t, BlockList> &blocks, int16_t zPos);
virtual ~DBPostgreSQL();
protected:
PGresult *checkResults(PGresult *res, bool clear = true);
void prepareStatement(const std::string &name, const std::string &sql);
PGresult *execPrepared(
const char *stmtName, const int paramsNumber,
const void **params,
const int *paramsLengths = NULL, const int *paramsFormats = NULL,
bool clear = true, bool nobinary = true
);
int pg_to_int(PGresult *res, int row, int col);
int pg_binary_to_int(PGresult *res, int row, int col);
BlockPos pg_to_blockpos(PGresult *res, int row, int col);
private:
PGconn *db;
};
#endif // _DB_POSTGRESQL_H

190
db-redis.cpp Normal file
View File

@ -0,0 +1,190 @@
#include <stdexcept>
#include <sstream>
#include <fstream>
#include "db-redis.h"
#include "types.h"
#include "util.h"
#define DB_REDIS_HMGET_NUMFIELDS 30
#define REPLY_TYPE_ERR(reply, desc) do { \
throw std::runtime_error(std::string("Unexpected type for " desc ": ") \
+ replyTypeStr((reply)->type)); \
} while(0)
static inline int64_t stoi64(const std::string &s)
{
std::stringstream tmp(s);
int64_t t;
tmp >> t;
return t;
}
static inline std::string i64tos(int64_t i)
{
std::ostringstream os;
os << i;
return os.str();
}
std::string get_setting_default(std::string name, std::istream &is, const std::string def)
{
try {
return get_setting(name, is);
} catch(std::runtime_error e) {
return def;
}
}
DBRedis::DBRedis(const std::string &mapdir)
{
std::ifstream ifs((mapdir + "/world.mt").c_str());
if(!ifs.good())
throw std::runtime_error("Failed to read world.mt");
std::string tmp;
try {
tmp = get_setting("redis_address", ifs);
ifs.seekg(0);
hash = get_setting("redis_hash", ifs);
ifs.seekg(0);
} catch(std::runtime_error e) {
throw std::runtime_error("Set redis_address and redis_hash in world.mt to use the redis backend");
}
const char *addr = tmp.c_str();
int port = stoi64(get_setting_default("redis_port", ifs, "6379"));
ctx = redisConnect(addr, port);
if(!ctx)
throw std::runtime_error("Cannot allocate redis context");
else if(ctx->err) {
std::string err = std::string("Connection error: ") + ctx->errstr;
redisFree(ctx);
throw std::runtime_error(err);
}
loadPosCache();
}
DBRedis::~DBRedis()
{
redisFree(ctx);
}
std::vector<BlockPos> DBRedis::getBlockPos()
{
return posCache;
}
std::string DBRedis::replyTypeStr(int type) {
switch(type) {
case REDIS_REPLY_STATUS:
return "REDIS_REPLY_STATUS";
case REDIS_REPLY_ERROR:
return "REDIS_REPLY_ERROR";
case REDIS_REPLY_INTEGER:
return "REDIS_REPLY_INTEGER";
case REDIS_REPLY_NIL:
return "REDIS_REPLY_NIL";
case REDIS_REPLY_STRING:
return "REDIS_REPLY_STRING";
case REDIS_REPLY_ARRAY:
return "REDIS_REPLY_ARRAY";
default:
return "unknown";
}
}
void DBRedis::loadPosCache()
{
redisReply *reply;
reply = (redisReply*) redisCommand(ctx, "HKEYS %s", hash.c_str());
if(!reply)
throw std::runtime_error("Redis command HKEYS failed");
if(reply->type != REDIS_REPLY_ARRAY)
REPLY_TYPE_ERR(reply, "HKEYS reply");
for(size_t i = 0; i < reply->elements; i++) {
if(reply->element[i]->type != REDIS_REPLY_STRING)
REPLY_TYPE_ERR(reply->element[i], "HKEYS subreply");
posCache.push_back(decodeBlockPos(stoi64(reply->element[i]->str)));
}
freeReplyObject(reply);
}
void DBRedis::HMGET(const std::vector<BlockPos> &positions, std::vector<ustring> *result)
{
const char *argv[DB_REDIS_HMGET_NUMFIELDS + 2];
argv[0] = "HMGET";
argv[1] = hash.c_str();
std::vector<BlockPos>::const_iterator position = positions.begin();
std::size_t remaining = positions.size();
while (remaining > 0) {
const std::size_t batch_size =
(remaining > DB_REDIS_HMGET_NUMFIELDS) ? DB_REDIS_HMGET_NUMFIELDS : remaining;
redisReply *reply;
{
// storage to preserve validity of .c_str()
std::string keys[batch_size];
for (std::size_t i = 0; i < batch_size; ++i) {
keys[i] = i64tos(encodeBlockPos(*position++));
argv[i+2] = keys[i].c_str();
}
reply = (redisReply*) redisCommandArgv(ctx, batch_size + 2, argv, NULL);
}
if(!reply)
throw std::runtime_error("Redis command HMGET failed");
if (reply->type != REDIS_REPLY_ARRAY) {
freeReplyObject(reply);
REPLY_TYPE_ERR(reply, "HKEYS subreply");
}
if (reply->elements != batch_size) {
freeReplyObject(reply);
throw std::runtime_error("HMGET wrong number of elements");
}
for (std::size_t i = 0; i < batch_size; ++i) {
redisReply *subreply = reply->element[i];
if(!subreply)
throw std::runtime_error("Redis command HMGET failed");
if (subreply->type != REDIS_REPLY_STRING) {
freeReplyObject(reply);
REPLY_TYPE_ERR(reply, "HKEYS subreply");
}
if (subreply->len == 0) {
freeReplyObject(reply);
throw std::runtime_error("HMGET empty string");
}
result->push_back(ustring((const unsigned char *) subreply->str, subreply->len));
}
freeReplyObject(reply);
remaining -= batch_size;
}
}
void DBRedis::getBlocksOnZ(std::map<int16_t, BlockList> &blocks, int16_t zPos)
{
std::vector<BlockPos> z_positions;
for (std::vector<BlockPos>::const_iterator it = posCache.begin(); it != posCache.end(); ++it) {
if (it->z != zPos) {
continue;
}
z_positions.push_back(*it);
}
std::vector<ustring> z_blocks;
HMGET(z_positions, &z_blocks);
std::vector<ustring>::const_iterator z_block = z_blocks.begin();
for (std::vector<BlockPos>::const_iterator pos = z_positions.begin();
pos != z_positions.end();
++pos, ++z_block) {
blocks[pos->x].push_back(Block(*pos, *z_block));
}
}

25
db-redis.h Normal file
View File

@ -0,0 +1,25 @@
#ifndef DB_REDIS_HEADER
#define DB_REDIS_HEADER
#include "db.h"
#include <hiredis.h>
class DBRedis : public DB {
public:
DBRedis(const std::string &mapdir);
virtual std::vector<BlockPos> getBlockPos();
virtual void getBlocksOnZ(std::map<int16_t, BlockList> &blocks, int16_t zPos);
virtual ~DBRedis();
private:
static std::string replyTypeStr(int type);
void loadPosCache();
void HMGET(const std::vector<BlockPos> &positions, std::vector<ustring> *result);
std::vector<BlockPos> posCache;
redisContext *ctx;
std::string hash;
};
#endif // DB_REDIS_HEADER

92
db-sqlite3.cpp Normal file
View File

@ -0,0 +1,92 @@
#include <stdexcept>
#include <unistd.h> // for usleep
#include <iostream>
#include "db-sqlite3.h"
#include "types.h"
#define SQLRES(f, good) \
result = (sqlite3_##f);\
if (result != good) {\
throw std::runtime_error(sqlite3_errmsg(db));\
}
#define SQLOK(f) SQLRES(f, SQLITE_OK)
DBSQLite3::DBSQLite3(const std::string &mapdir)
{
int result;
std::string db_name = mapdir + "map.sqlite";
SQLOK(open_v2(db_name.c_str(), &db, SQLITE_OPEN_READONLY |
SQLITE_OPEN_PRIVATECACHE, 0))
SQLOK(prepare_v2(db,
"SELECT pos, data FROM blocks WHERE pos BETWEEN ? AND ?",
-1, &stmt_get_blocks_z, NULL))
SQLOK(prepare_v2(db,
"SELECT pos FROM blocks",
-1, &stmt_get_block_pos, NULL))
}
DBSQLite3::~DBSQLite3()
{
sqlite3_finalize(stmt_get_blocks_z);
sqlite3_finalize(stmt_get_block_pos);
if (sqlite3_close(db) != SQLITE_OK) {
std::cerr << "Error closing SQLite database." << std::endl;
};
}
std::vector<BlockPos> DBSQLite3::getBlockPos()
{
int result;
std::vector<BlockPos> positions;
while ((result = sqlite3_step(stmt_get_block_pos)) != SQLITE_DONE) {
if (result == SQLITE_ROW) {
int64_t posHash = sqlite3_column_int64(stmt_get_block_pos, 0);
positions.push_back(decodeBlockPos(posHash));
} else if (result == SQLITE_BUSY) { // Wait some time and try again
usleep(10000);
} else {
throw std::runtime_error(sqlite3_errmsg(db));
}
}
SQLOK(reset(stmt_get_block_pos));
return positions;
}
void DBSQLite3::getBlocksOnZ(std::map<int16_t, BlockList> &blocks, int16_t zPos)
{
int result;
// Magic numbers!
int64_t minPos = encodeBlockPos(BlockPos(0, -2048, zPos));
int64_t maxPos = encodeBlockPos(BlockPos(0, 2048, zPos)) - 1;
SQLOK(bind_int64(stmt_get_blocks_z, 1, minPos));
SQLOK(bind_int64(stmt_get_blocks_z, 2, maxPos));
while ((result = sqlite3_step(stmt_get_blocks_z)) != SQLITE_DONE) {
if (result == SQLITE_ROW) {
int64_t posHash = sqlite3_column_int64(stmt_get_blocks_z, 0);
const unsigned char *data = reinterpret_cast<const unsigned char *>(
sqlite3_column_blob(stmt_get_blocks_z, 1));
size_t size = sqlite3_column_bytes(stmt_get_blocks_z, 1);
Block b(decodeBlockPos(posHash), ustring(data, size));
blocks[b.first.x].push_back(b);
} else if (result == SQLITE_BUSY) { // Wait some time and try again
usleep(10000);
} else {
throw std::runtime_error(sqlite3_errmsg(db));
}
}
SQLOK(reset(stmt_get_blocks_z));
}
#undef SQLRES
#undef SQLOK

20
db-sqlite3.h Normal file
View File

@ -0,0 +1,20 @@
#ifndef _DB_SQLITE3_H
#define _DB_SQLITE3_H
#include "db.h"
#include <sqlite3.h>
class DBSQLite3 : public DB {
public:
DBSQLite3(const std::string &mapdir);
virtual std::vector<BlockPos> getBlockPos();
virtual void getBlocksOnZ(std::map<int16_t, BlockList> &blocks, int16_t zPos);
virtual ~DBSQLite3();
private:
sqlite3 *db;
sqlite3_stmt *stmt_get_block_pos;
sqlite3_stmt *stmt_get_blocks_z;
};
#endif // _DB_SQLITE3_H

113
db.h Normal file
View File

@ -0,0 +1,113 @@
#ifndef DB_HEADER
#define DB_HEADER
#include <stdint.h>
#include <map>
#include <list>
#include <vector>
#include <string>
#include <utility>
#include "types.h"
class BlockPos {
public:
int16_t x;
int16_t y;
int16_t z;
BlockPos() : x(0), y(0), z(0) {}
BlockPos(int16_t x, int16_t y, int16_t z) : x(x), y(y), z(z) {}
bool operator < (const BlockPos &p) const
{
if (z > p.z) {
return true;
}
if (z < p.z) {
return false;
}
if (y > p.y) {
return true;
}
if (y < p.y) {
return false;
}
if (x > p.x) {
return true;
}
if (x < p.x) {
return false;
}
return false;
}
};
typedef std::pair<BlockPos, ustring> Block;
typedef std::list<Block> BlockList;
class DB {
protected:
inline int64_t encodeBlockPos(const BlockPos pos) const;
inline BlockPos decodeBlockPos(int64_t hash) const;
public:
virtual std::vector<BlockPos> getBlockPos() = 0;
virtual void getBlocksOnZ(std::map<int16_t, BlockList> &blocks, int16_t zPos) = 0;
virtual ~DB() {};
};
/****************
* Black magic! *
****************
* The position hashing is seriously messed up,
* and is a lot more complicated than it looks.
*/
static inline int16_t unsigned_to_signed(uint16_t i, uint16_t max_positive)
{
if (i < max_positive) {
return i;
} else {
return i - (max_positive * 2);
}
}
// Modulo of a negative number does not work consistently in C
static inline int64_t pythonmodulo(int64_t i, int64_t mod)
{
if (i >= 0) {
return i % mod;
}
return mod - ((-i) % mod);
}
inline int64_t DB::encodeBlockPos(const BlockPos pos) const
{
return (uint64_t) pos.z * 0x1000000 +
(uint64_t) pos.y * 0x1000 +
(uint64_t) pos.x;
}
inline BlockPos DB::decodeBlockPos(int64_t hash) const
{
BlockPos pos;
pos.x = unsigned_to_signed(pythonmodulo(hash, 4096), 2048);
hash = (hash - pos.x) / 4096;
pos.y = unsigned_to_signed(pythonmodulo(hash, 4096), 2048);
hash = (hash - pos.y) / 4096;
pos.z = unsigned_to_signed(pythonmodulo(hash, 4096), 2048);
return pos;
}
/*******************
* End black magic *
*******************/
#endif // DB_HEADER

215
mapper.cpp Normal file
View File

@ -0,0 +1,215 @@
#include <cstdlib>
#include <cstring>
#include <getopt.h>
#include <fstream>
#include <iostream>
#include <map>
#include <string>
#include <sstream>
#include <stdexcept>
#include "cmake_config.h"
#include "TileGenerator.h"
void usage()
{
const char *usage_text = "minetestmapper [options]\n"
" -i/--input <world_path>\n"
" -o/--output <output_image.png>\n"
" --bgcolor <color>\n"
" --scalecolor <color>\n"
" --playercolor <color>\n"
" --origincolor <color>\n"
" --drawscale\n"
" --drawplayers\n"
" --draworigin\n"
" --drawalpha\n"
" --noshading\n"
" --min-y <y>\n"
" --max-y <y>\n"
" --backend <backend>\n"
" --geometry x:y+w+h\n"
" --zoom <zoomlevel>\n"
" --colors <colors.txt>\n"
" --scales [t][b][l][r]\n"
"Color format: '#000000'\n";
std::cout << usage_text;
}
bool file_exists(const std::string &path)
{
std::ifstream ifs(path.c_str());
return ifs.is_open();
}
std::string search_colors(const std::string &worldpath)
{
if(file_exists(worldpath + "/colors.txt"))
return worldpath + "/colors.txt";
#ifndef _WIN32
char *home = std::getenv("HOME");
if(home) {
std::string check = ((std::string) home) + "/.minetest/colors.txt";
if(file_exists(check))
return check;
}
#endif
if(!(SHAREDIR[0] == '.' || SHAREDIR[0] == '\0') && file_exists(SHAREDIR "/colors.txt"))
return SHAREDIR "/colors.txt";
std::cerr << "Warning: Falling back to using colors.txt from current directory." << std::endl;
return "colors.txt";
}
int main(int argc, char *argv[])
{
static struct option long_options[] =
{
{"help", no_argument, 0, 'h'},
{"input", required_argument, 0, 'i'},
{"output", required_argument, 0, 'o'},
{"bgcolor", required_argument, 0, 'b'},
{"scalecolor", required_argument, 0, 's'},
{"origincolor", required_argument, 0, 'r'},
{"playercolor", required_argument, 0, 'p'},
{"draworigin", no_argument, 0, 'R'},
{"drawplayers", no_argument, 0, 'P'},
{"drawscale", no_argument, 0, 'S'},
{"drawalpha", no_argument, 0, 'e'},
{"noshading", no_argument, 0, 'H'},
{"backend", required_argument, 0, 'd'},
{"geometry", required_argument, 0, 'g'},
{"min-y", required_argument, 0, 'a'},
{"max-y", required_argument, 0, 'c'},
{"zoom", required_argument, 0, 'z'},
{"colors", required_argument, 0, 'C'},
{"scales", required_argument, 0, 'f'},
{0, 0, 0, 0}
};
std::string input;
std::string output;
std::string colors = "";
TileGenerator generator;
int option_index = 0;
int c = 0;
while (1) {
c = getopt_long(argc, argv, "hi:o:", long_options, &option_index);
if (c == -1) {
if (input.empty() || output.empty()) {
usage();
return 0;
}
break;
}
switch (c) {
case 'h':
usage();
return 0;
break;
case 'i':
input = optarg;
break;
case 'o':
output = optarg;
break;
case 'b':
generator.setBgColor(optarg);
break;
case 's':
generator.setScaleColor(optarg);
break;
case 'r':
generator.setOriginColor(optarg);
break;
case 'p':
generator.setPlayerColor(optarg);
break;
case 'R':
generator.setDrawOrigin(true);
break;
case 'P':
generator.setDrawPlayers(true);
break;
case 'S':
generator.setDrawScale(true);
break;
case 'e':
generator.setDrawAlpha(true);
break;
case 'H':
generator.setShading(false);
break;
case 'd':
generator.setBackend(optarg);
break;
case 'a': {
std::istringstream iss;
iss.str(optarg);
int miny;
iss >> miny;
generator.setMinY(miny);
}
break;
case 'c': {
std::istringstream iss;
iss.str(optarg);
int maxy;
iss >> maxy;
generator.setMaxY(maxy);
}
break;
case 'g': {
std::istringstream geometry;
geometry.str(optarg);
int x, y, w, h;
char c;
geometry >> x >> c >> y >> w >> h;
if (geometry.fail() || c != ':' || w < 1 || h < 1) {
usage();
exit(1);
}
generator.setGeometry(x, y, w, h);
}
break;
case 'f': {
uint flags = 0;
if(strchr(optarg, 't') != NULL)
flags |= SCALE_TOP;
if(strchr(optarg, 'b') != NULL)
flags |= SCALE_BOTTOM;
if(strchr(optarg, 'l') != NULL)
flags |= SCALE_LEFT;
if(strchr(optarg, 'r') != NULL)
flags |= SCALE_RIGHT;
generator.setScales(flags);
}
break;
case 'z': {
std::istringstream iss;
iss.str(optarg);
int zoom;
iss >> zoom;
generator.setZoom(zoom);
}
break;
case 'C':
colors = optarg;
break;
default:
exit(1);
}
}
if(colors == "")
colors = search_colors(input);
try {
generator.parseColorsFile(colors);
generator.generate(input, output);
} catch(std::runtime_error e) {
std::cerr << "Exception: " << e.what() << std::endl;
return 1;
}
return 0;
}

View File

@ -1,6 +1,6 @@
.TH MINETESTMAPPER 6 .TH MINETESTMAPPER 6
.SH NAME .SH NAME
minetestmapper \- generate an overview image of a Luanti map minetestmapper \- generate an overview image of a Minetest map
.SH SYNOPSIS .SH SYNOPSIS
.B minetestmapper .B minetestmapper
\fB\-i\fR \fIworld_path\fR \fB\-i\fR \fIworld_path\fR
@ -9,22 +9,16 @@ minetestmapper \- generate an overview image of a Luanti map
See additional optional parameters below. See additional optional parameters below.
.SH DESCRIPTION .SH DESCRIPTION
.B minetestmapper .B minetestmapper
generates a top-down overview image of a Luanti map. generates an overview image of a minetest map. This is a port of
This is a port of the obsolete minetestmapper.py script to C++, the original minetestmapper.py to C++, that is both faster and
that is both faster and provides more features. provides more funtionality than the deprecated Python script.
Minetestmapper ships with a colors.txt file suitable for Minetest Game,
if you use a different game or have mods installed you should generate a
matching colors.txt for better results (colors will be missing otherwise).
.SH MANDATORY PARAMETERS .SH MANDATORY PARAMETERS
.TP .TP
.BR \-i " " \fIworld_path\fR .BR \-i " " \fIworld_path\fR
Input world path Input world path.
.TP .TP
.BR \-o " " \fIoutput_image\fR .BR \-o " " \fIoutput_image\fR
Path to output image Path to output image. (only PNG supported currently)
.SH OPTIONAL PARAMETERS .SH OPTIONAL PARAMETERS
.TP .TP
.BR \-\-bgcolor " " \fIcolor\fR .BR \-\-bgcolor " " \fIcolor\fR
@ -32,7 +26,7 @@ Background color of image, e.g. "--bgcolor #ffffff"
.TP .TP
.BR \-\-scalecolor " " \fIcolor\fR .BR \-\-scalecolor " " \fIcolor\fR
Color of scale marks and text, e.g. "--scalecolor #000000" Color of scale, e.g. "--scalecolor #000000"
.TP .TP
.BR \-\-playercolor " " \fIcolor\fR .BR \-\-playercolor " " \fIcolor\fR
@ -44,11 +38,11 @@ Color of origin indicator, e.g. "--origincolor #ff0000"
.TP .TP
.BR \-\-drawscale .BR \-\-drawscale
Draw scale(s) with tick marks and numbers Draw tick marks
.TP .TP
.BR \-\-drawplayers .BR \-\-drawplayers
Draw player indicators with name Draw player indicators
.TP .TP
.BR \-\-draworigin .BR \-\-draworigin
@ -56,41 +50,27 @@ Draw origin indicator
.TP .TP
.BR \-\-drawalpha .BR \-\-drawalpha
Allow nodes to be drawn with transparency (such as water) Allow nodes to be drawn with transparency
.TP .TP
.BR \-\-noshading .BR \-\-noshading
Don't draw shading on nodes Don't draw shading on nodes
.TP
.BR \-\-noemptyimage
Don't output anything when the image would be empty
.TP
.BR \-\-verbose
Enable verbose log output.
.TP .TP
.BR \-\-min-y " " \fInumber\fR .BR \-\-min-y " " \fInumber\fR
Don't draw nodes below this Y value, e.g. "--min-y -25" Don't draw nodes below this y value, e.g. "--min-y -25"
.TP .TP
.BR \-\-max-y " " \fInumber\fR .BR \-\-max-y " " \fInumber\fR
Don't draw nodes above this Y value, e.g. "--max-y 75" Don't draw nodes above this y value, e.g. "--max-y 75"
.TP .TP
.BR \-\-backend " " \fIbackend\fR .BR \-\-backend " " \fIbackend\fR
Override auto-detected map backend; supported: \fIsqlite3\fP, \fIleveldb\fP, \fIredis\fP, \fIpostgresql\fP, e.g. "--backend leveldb" Use specific map backend; supported: *sqlite3*, *leveldb*, *redis*, *postgresql*, e.g. "--backend leveldb"
.TP .TP
.BR \-\-geometry " " \fIgeometry\fR .BR \-\-geometry " " \fIgeometry\fR
Limit area to specific geometry (\fIx:z+w+h\fP where x and z specify the lower left corner), e.g. "--geometry -800:-800+1600+1600" Limit area to specific geometry (*x:y+w+h* where x and y specify the lower left corner), e.g. "--geometry -800:-800+1600+1600"
The coordinates are specified with the same axes as in-game. The Z axis becomes Y when projected on the image.
.TP
.BR \-\-extent
Don't render the image, just print the extent of the map that would be generated, in the same format as the geometry above.
.TP .TP
.BR \-\-zoom " " \fIfactor\fR .BR \-\-zoom " " \fIfactor\fR
@ -98,24 +78,14 @@ Zoom the image by using more than one pixel per node, e.g. "--zoom 4"
.TP .TP
.BR \-\-colors " " \fIpath\fR .BR \-\-colors " " \fIpath\fR
Override auto-detected path to colors.txt, e.g. "--colors ../world/mycolors.txt" Forcefully set path to colors.txt file (it's autodetected otherwise), e.g. "--colors ../minetest/mycolors.txt"
.TP .TP
.BR \-\-scales " " \fIedges\fR .BR \-\-scales " " \fIedges\fR
Draw scales on specified image edges (letters \fIt b l r\fP meaning top, bottom, left and right), e.g. "--scales tbr" Draw scales on specified image edges (letters *t b l r* meaning top, bottom, left and right), e.g. "--scales tbr"
.TP
.BR \-\-exhaustive " " \fImode\fR
Select if database should be traversed exhaustively or using range queries, available: \fInever\fP, \fIy\fP, \fIfull\fP, \fIauto\fP
Defaults to \fIauto\fP. You shouldn't need to change this, as minetestmapper tries to automatically picks the best option.
.TP
.BR \-\-dumpblock " " \fIpos\fR
Instead of rendering anything try to load the block at the given position (\fIx,y,z\fR) and print its raw data as hexadecimal.
.SH MORE INFORMATION .SH MORE INFORMATION
Website: https://github.com/luanti-org/minetestmapper Website: https://github.com/minetest/minetestmapper
.SH MAN PAGE AUTHOR .SH MAN PAGE AUTHOR
Daniel Moerner Daniel Moerner

View File

@ -1,168 +0,0 @@
#include <string>
#include <iostream>
#include <sstream>
#include "BlockDecoder.h"
#include "ZlibDecompressor.h"
#include "log.h"
static inline uint16_t readU16(const unsigned char *data)
{
return data[0] << 8 | data[1];
}
static inline uint16_t readBlockContent(const unsigned char *mapData,
u8 contentWidth, unsigned int datapos)
{
if (contentWidth == 2) {
size_t index = datapos << 1;
return (mapData[index] << 8) | mapData[index + 1];
} else {
u8 param = mapData[datapos];
if (param <= 0x7f)
return param;
else
return (param << 4) | (mapData[datapos + 0x2000] >> 4);
}
}
BlockDecoder::BlockDecoder()
{
reset();
}
void BlockDecoder::reset()
{
m_blockAirId = -1;
m_blockIgnoreId = -1;
m_nameMap.clear();
m_version = 0;
m_contentWidth = 0;
m_mapData.clear();
}
void BlockDecoder::decode(const ustring &datastr)
{
const unsigned char *data = datastr.c_str();
size_t length = datastr.length();
// TODO: bounds checks
uint8_t version = data[0];
if (version < 22) {
std::ostringstream oss;
oss << "Unsupported map version " << (int)version;
throw std::runtime_error(oss.str());
}
m_version = version;
if (version >= 29) {
// decompress whole block at once
m_zstd_decompressor.setData(data, length, 1);
m_zstd_decompressor.decompress(m_scratch);
data = m_scratch.c_str();
length = m_scratch.size();
}
size_t dataOffset = 0;
if (version >= 29)
dataOffset = 7;
else if (version >= 27)
dataOffset = 4;
else
dataOffset = 2;
auto decode_mapping = [&] () {
dataOffset++; // mapping version
uint16_t numMappings = readU16(data + dataOffset);
dataOffset += 2;
for (int i = 0; i < numMappings; ++i) {
uint16_t nodeId = readU16(data + dataOffset);
dataOffset += 2;
uint16_t nameLen = readU16(data + dataOffset);
dataOffset += 2;
std::string name(reinterpret_cast<const char *>(data) + dataOffset, nameLen);
if (name == "air")
m_blockAirId = nodeId;
else if (name == "ignore")
m_blockIgnoreId = nodeId;
else
m_nameMap[nodeId] = name;
dataOffset += nameLen;
}
};
if (version >= 29)
decode_mapping();
uint8_t contentWidth = data[dataOffset];
dataOffset++;
uint8_t paramsWidth = data[dataOffset];
dataOffset++;
if (contentWidth != 1 && contentWidth != 2)
throw std::runtime_error("unsupported map version (contentWidth)");
if (paramsWidth != 2)
throw std::runtime_error("unsupported map version (paramsWidth)");
m_contentWidth = contentWidth;
if (version >= 29) {
size_t mapDataSize = (contentWidth + paramsWidth) * 4096;
m_mapData.assign(data + dataOffset, mapDataSize);
return; // we have read everything we need and can return early
}
// version < 29
ZlibDecompressor decompressor(data, length);
decompressor.setSeekPos(dataOffset);
decompressor.decompress(m_mapData);
decompressor.decompress(m_scratch); // unused metadata
dataOffset = decompressor.seekPos();
// Skip unused node timers
if (version == 23)
dataOffset += 1;
if (version == 24) {
uint8_t ver = data[dataOffset++];
if (ver == 1) {
uint16_t num = readU16(data + dataOffset);
dataOffset += 2;
dataOffset += 10 * num;
}
}
// Skip unused static objects
dataOffset++; // Skip static object version
int staticObjectCount = readU16(data + dataOffset);
dataOffset += 2;
for (int i = 0; i < staticObjectCount; ++i) {
dataOffset += 13;
uint16_t dataSize = readU16(data + dataOffset);
dataOffset += dataSize + 2;
}
dataOffset += 4; // Skip timestamp
// Read mapping
decode_mapping();
}
bool BlockDecoder::isEmpty() const
{
// only contains ignore and air nodes?
return m_nameMap.empty();
}
const static std::string empty;
const std::string &BlockDecoder::getNode(u8 x, u8 y, u8 z) const
{
unsigned int position = x + (y << 4) + (z << 8);
uint16_t content = readBlockContent(m_mapData.c_str(), m_contentWidth, position);
if (content == m_blockAirId || content == m_blockIgnoreId)
return empty;
NameMap::const_iterator it = m_nameMap.find(content);
if (it == m_nameMap.end()) {
errorstream << "Skipping node with invalid ID." << std::endl;
return empty;
}
return it->second;
}

View File

@ -1,29 +0,0 @@
#pragma once
#include <cstdint>
#include <unordered_map>
#include "types.h"
#include <ZstdDecompressor.h>
class BlockDecoder {
public:
BlockDecoder();
void reset();
void decode(const ustring &data);
bool isEmpty() const;
// returns "" for air, ignore and invalid nodes
const std::string &getNode(u8 x, u8 y, u8 z) const;
private:
typedef std::unordered_map<uint16_t, std::string> NameMap;
NameMap m_nameMap;
uint16_t m_blockAirId, m_blockIgnoreId;
u8 m_version, m_contentWidth;
ustring m_mapData;
// cached allocations/instances for performance
ZstdDecompressor m_zstd_decompressor;
ustring m_scratch;
};

View File

@ -1,44 +0,0 @@
#pragma once
#include <climits>
#include <cstdint>
#define BLOCK_SIZE 16
struct PixelAttribute {
PixelAttribute() : height(INT16_MIN), thickness(0) {};
int16_t height;
uint8_t thickness;
inline bool valid_height() const {
return height != INT16_MIN;
}
};
class PixelAttributes
{
public:
PixelAttributes();
virtual ~PixelAttributes();
void setWidth(int width);
void scroll();
inline PixelAttribute &attribute(int z, int x) {
return m_pixelAttributes[z + 1][x + 1];
};
private:
void freeAttributes();
private:
enum Line {
FirstLine = 0,
LastLine = BLOCK_SIZE,
EmptyLine = BLOCK_SIZE + 1,
LineCount = BLOCK_SIZE + 2
};
PixelAttribute *m_pixelAttributes[BLOCK_SIZE + 2]; // 1px gradient + empty
int m_width;
};

View File

@ -1,159 +0,0 @@
#include <fstream>
#include <sstream>
#include <stdexcept>
#include <dirent.h>
#include <unistd.h> // usleep
#include "config.h"
#include "PlayerAttributes.h"
#include "util.h"
#include "log.h"
#include "db-sqlite3.h" // SQLite3Base
namespace {
bool parse_pos(std::string position, Player &dst)
{
if (position.empty())
return false;
if (position.front() == '(' && position.back() == ')')
position = position.substr(1, position.size() - 2);
std::istringstream iss(position);
if (!(iss >> dst.x))
return false;
if (iss.get() != ',')
return false;
if (!(iss >> dst.y))
return false;
if (iss.get() != ',')
return false;
if (!(iss >> dst.z))
return false;
return iss.eof();
}
// Helper classes per backend
class FilesReader {
std::string path;
DIR *dir = nullptr;
public:
FilesReader(const std::string &path) : path(path) {
dir = opendir(path.c_str());
}
~FilesReader() {
if (dir)
closedir(dir);
}
void read(PlayerAttributes::Players &dest);
};
class SQLiteReader : SQLite3Base {
sqlite3_stmt *stmt_get_player_pos = NULL;
public:
SQLiteReader(const std::string &database) {
openDatabase(database.c_str());
}
~SQLiteReader() {
sqlite3_finalize(stmt_get_player_pos);
}
void read(PlayerAttributes::Players &dest);
};
}
void FilesReader::read(PlayerAttributes::Players &dest)
{
if (!dir)
return;
struct dirent *ent;
std::string name, position;
while ((ent = readdir(dir)) != NULL) {
if (ent->d_name[0] == '.')
continue;
std::ifstream in(path + PATH_SEPARATOR + ent->d_name);
if (!in.good())
continue;
name = read_setting("name", in);
position = read_setting("position", in);
Player player;
player.name = name;
if (!parse_pos(position, player)) {
errorstream << "Failed to parse position '" << position << "' in "
<< ent->d_name << std::endl;
continue;
}
player.x /= 10.0f;
player.y /= 10.0f;
player.z /= 10.0f;
dest.push_back(std::move(player));
}
}
#define SQLRES(r, good) check_result(r, good)
#define SQLOK(r) SQLRES(r, SQLITE_OK)
void SQLiteReader::read(PlayerAttributes::Players &dest)
{
SQLOK(prepare(stmt_get_player_pos,
"SELECT name, posX, posY, posZ FROM player"));
int result;
while ((result = sqlite3_step(stmt_get_player_pos)) != SQLITE_DONE) {
if (result == SQLITE_BUSY) { // Wait some time and try again
usleep(10000);
} else if (result != SQLITE_ROW) {
throw std::runtime_error(sqlite3_errmsg(db));
}
Player player;
player.name = read_str(stmt_get_player_pos, 0);
player.x = sqlite3_column_double(stmt_get_player_pos, 1);
player.y = sqlite3_column_double(stmt_get_player_pos, 2);
player.z = sqlite3_column_double(stmt_get_player_pos, 3);
player.x /= 10.0f;
player.y /= 10.0f;
player.z /= 10.0f;
dest.push_back(std::move(player));
}
}
/**********/
PlayerAttributes::PlayerAttributes(const std::string &worldDir)
{
std::ifstream ifs(worldDir + "world.mt");
if (!ifs.good())
throw std::runtime_error("Failed to read world.mt");
std::string backend = read_setting_default("player_backend", ifs, "files");
ifs.close();
verbosestream << "Player backend: " << backend << std::endl;
if (backend == "files")
FilesReader(worldDir + "players").read(m_players);
else if (backend == "sqlite3")
SQLiteReader(worldDir + "players.sqlite").read(m_players);
else
throw std::runtime_error(std::string("Unknown player backend: ") + backend);
verbosestream << "Loaded " << m_players.size() << " players" << std::endl;
}
PlayerAttributes::Players::const_iterator PlayerAttributes::begin() const
{
return m_players.cbegin();
}
PlayerAttributes::Players::const_iterator PlayerAttributes::end() const
{
return m_players.cend();
}

View File

@ -1,23 +0,0 @@
#pragma once
#include <list>
#include <string>
struct Player
{
std::string name;
float x, y, z;
};
class PlayerAttributes
{
public:
typedef std::list<Player> Players;
PlayerAttributes(const std::string &worldDir);
Players::const_iterator begin() const;
Players::const_iterator end() const;
private:
Players m_players;
};

View File

@ -1,930 +0,0 @@
#include <cstdio>
#include <cstdlib>
#include <climits>
#include <cassert>
#include <fstream>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <cstring>
#include <vector>
#include <type_traits>
#include <limits>
#include "TileGenerator.h"
#include "config.h"
#include "PlayerAttributes.h"
#include "BlockDecoder.h"
#include "Image.h"
#include "util.h"
#include "log.h"
#include "db-sqlite3.h"
#if USE_POSTGRESQL
#include "db-postgresql.h"
#endif
#if USE_LEVELDB
#include "db-leveldb.h"
#endif
#if USE_REDIS
#include "db-redis.h"
#endif
#ifndef __has_builtin
#define __has_builtin(x) 0
#endif
// saturating multiplication
template<typename T, class = typename std::enable_if<std::is_unsigned<T>::value>::type>
inline T sat_mul(T a, T b)
{
#if __has_builtin(__builtin_mul_overflow)
T res;
if (__builtin_mul_overflow(a, b, &res))
return std::numeric_limits<T>::max();
return res;
#else
// WARNING: the fallback implementation is incorrect since we compute ceil(log(x)) not log(x)
// but that's good enough for our usecase...
const int bits = sizeof(T) * 8;
int hb_a = 0, hb_b = 0;
for (int i = bits - 1; i >= 0; i--) {
if (a & (static_cast<T>(1) << i)) {
hb_a = i; break;
}
}
for (int i = bits - 1; i >= 0; i--) {
if (b & (static_cast<T>(1) << i)) {
hb_b = i; break;
}
}
// log2(a) + log2(b) >= log2(MAX) <=> calculation will overflow
if (hb_a + hb_b >= bits)
return std::numeric_limits<T>::max();
return a * b;
#endif
}
template<typename T>
inline T sat_mul(T a, T b, T c)
{
return sat_mul(sat_mul(a, b), c);
}
// rounds n (away from 0) to a multiple of f while preserving the sign of n
static int round_multiple_nosign(int n, int f)
{
int abs_n, sign;
abs_n = (n >= 0) ? n : -n;
sign = (n >= 0) ? 1 : -1;
if (abs_n % f == 0)
return n; // n == abs_n * sign
else
return sign * (abs_n + f - (abs_n % f));
}
static inline unsigned int colorSafeBounds(int channel)
{
return mymin(mymax(channel, 0), 255);
}
static Color parseColor(const std::string &color)
{
if (color.length() != 7)
throw std::runtime_error("Color needs to be 7 characters long");
if (color[0] != '#')
throw std::runtime_error("Color needs to begin with #");
unsigned long col = strtoul(color.c_str() + 1, NULL, 16);
u8 b, g, r;
b = col & 0xff;
g = (col >> 8) & 0xff;
r = (col >> 16) & 0xff;
return Color(r, g, b);
}
static Color mixColors(Color a, Color b)
{
Color result;
float a1 = a.a / 255.0f;
float a2 = b.a / 255.0f;
result.r = (int) (a1 * a.r + a2 * (1 - a1) * b.r);
result.g = (int) (a1 * a.g + a2 * (1 - a1) * b.g);
result.b = (int) (a1 * a.b + a2 * (1 - a1) * b.b);
result.a = (int) (255 * (a1 + a2 * (1 - a1)));
return result;
}
TileGenerator::TileGenerator():
m_bgColor(255, 255, 255),
m_scaleColor(0, 0, 0),
m_originColor(255, 0, 0),
m_playerColor(255, 0, 0),
m_drawOrigin(false),
m_drawPlayers(false),
m_drawScale(false),
m_drawAlpha(false),
m_shading(true),
m_dontWriteEmpty(false),
m_backend(""),
m_xBorder(0),
m_yBorder(0),
m_db(NULL),
m_image(NULL),
m_xMin(INT_MAX),
m_xMax(INT_MIN),
m_zMin(INT_MAX),
m_zMax(INT_MIN),
m_yMin(INT16_MIN),
m_yMax(INT16_MAX),
m_geomX(-2048),
m_geomY(-2048),
m_geomX2(2048),
m_geomY2(2048),
m_exhaustiveSearch(EXH_AUTO),
m_renderedAny(false),
m_zoom(1),
m_scales(SCALE_LEFT | SCALE_TOP),
m_progressMax(0),
m_progressLast(-1)
{
}
TileGenerator::~TileGenerator()
{
closeDatabase();
delete m_image;
m_image = nullptr;
}
void TileGenerator::setBgColor(const std::string &bgColor)
{
m_bgColor = parseColor(bgColor);
}
void TileGenerator::setScaleColor(const std::string &scaleColor)
{
m_scaleColor = parseColor(scaleColor);
}
void TileGenerator::setOriginColor(const std::string &originColor)
{
m_originColor = parseColor(originColor);
}
void TileGenerator::setPlayerColor(const std::string &playerColor)
{
m_playerColor = parseColor(playerColor);
}
void TileGenerator::setZoom(int zoom)
{
if (zoom < 1)
throw std::runtime_error("Zoom level needs to be a number: 1 or higher");
m_zoom = zoom;
}
void TileGenerator::setScales(uint flags)
{
m_scales = flags;
}
void TileGenerator::setDrawOrigin(bool drawOrigin)
{
m_drawOrigin = drawOrigin;
}
void TileGenerator::setDrawPlayers(bool drawPlayers)
{
m_drawPlayers = drawPlayers;
}
void TileGenerator::setDrawScale(bool drawScale)
{
m_drawScale = drawScale;
}
void TileGenerator::setDrawAlpha(bool drawAlpha)
{
m_drawAlpha = drawAlpha;
}
void TileGenerator::setShading(bool shading)
{
m_shading = shading;
}
void TileGenerator::setBackend(std::string backend)
{
m_backend = backend;
}
void TileGenerator::setGeometry(int x, int y, int w, int h)
{
assert(w > 0 && h > 0);
m_geomX = round_multiple_nosign(x, 16) / 16;
m_geomY = round_multiple_nosign(y, 16) / 16;
m_geomX2 = round_multiple_nosign(x + w, 16) / 16;
m_geomY2 = round_multiple_nosign(y + h, 16) / 16;
}
void TileGenerator::setMinY(int y)
{
m_yMin = y;
if (m_yMin > m_yMax)
std::swap(m_yMin, m_yMax);
}
void TileGenerator::setMaxY(int y)
{
m_yMax = y;
if (m_yMin > m_yMax)
std::swap(m_yMin, m_yMax);
}
void TileGenerator::setExhaustiveSearch(int mode)
{
m_exhaustiveSearch = mode;
}
void TileGenerator::setDontWriteEmpty(bool f)
{
m_dontWriteEmpty = f;
}
void TileGenerator::parseColorsFile(const std::string &fileName)
{
std::ifstream in(fileName);
if (!in.good())
throw std::runtime_error("Specified colors file could not be found");
verbosestream << "Parsing colors.txt: " << fileName << std::endl;
parseColorsStream(in);
}
void TileGenerator::printGeometry(const std::string &input_path)
{
setExhaustiveSearch(EXH_NEVER);
openDb(input_path);
loadBlocks();
std::cout << "Map extent: "
<< m_xMin*16 << ":" << m_zMin*16
<< "+" << (m_xMax - m_xMin+1)*16
<< "+" << (m_zMax - m_zMin+1)*16
<< std::endl;
closeDatabase();
}
void TileGenerator::dumpBlock(const std::string &input_path, BlockPos pos)
{
openDb(input_path);
BlockList list;
std::vector<BlockPos> positions;
positions.emplace_back(pos);
m_db->getBlocksByPos(list, positions);
if (!list.empty()) {
const ustring &data = list.begin()->second;
for (u8 c : data)
printf("%02x", static_cast<int>(c));
printf("\n");
}
closeDatabase();
}
void TileGenerator::generate(const std::string &input_path, const std::string &output)
{
openDb(input_path);
loadBlocks();
// If we needed to load positions and there are none, that means the
// result will be empty.
if (m_dontWriteEmpty && (m_exhaustiveSearch == EXH_NEVER ||
m_exhaustiveSearch == EXH_Y) && m_positions.empty()) {
verbosestream << "Result is empty (no positions)" << std::endl;
return;
}
createImage();
renderMap();
if (m_dontWriteEmpty && !m_renderedAny) {
verbosestream << "Result is empty (no pixels)" << std::endl;
printUnknown();
return;
}
closeDatabase();
if (m_drawScale) {
renderScale();
}
if (m_drawOrigin) {
renderOrigin();
}
if (m_drawPlayers) {
renderPlayers(input_path);
}
writeImage(output);
printUnknown();
}
void TileGenerator::parseColorsStream(std::istream &in)
{
char line[512];
while (in.good()) {
in.getline(line, sizeof(line));
for (char *p = line; *p; p++) {
if (*p != '#')
continue;
*p = '\0'; // Cut off at the first #
break;
}
if(!line[0])
continue;
char name[200 + 1] = {0};
unsigned int r, g, b, a = 255, t = 0;
int items = sscanf(line, "%200s %u %u %u %u %u", name, &r, &g, &b, &a, &t);
if (items < 4) {
errorstream << "Failed to parse color entry '" << line << "'" << std::endl;
continue;
}
m_colorMap[name] = ColorEntry(r, g, b, a, t);
}
}
std::set<std::string> TileGenerator::getSupportedBackends()
{
std::set<std::string> r;
r.insert("sqlite3");
#if USE_POSTGRESQL
r.insert("postgresql");
#endif
#if USE_LEVELDB
r.insert("leveldb");
#endif
#if USE_REDIS
r.insert("redis");
#endif
return r;
}
void TileGenerator::openDb(const std::string &input_path)
{
if (dir_exists(input_path.c_str())) {
// ok
} else if (file_exists(input_path.c_str())) {
throw std::runtime_error("Input path is a file, it should point to the world folder instead");
} else {
throw std::runtime_error("Input path does not exist");
}
std::string input = input_path;
if (input.back() != PATH_SEPARATOR)
input += PATH_SEPARATOR;
std::ifstream ifs(input + "world.mt");
std::string backend = m_backend;
if (backend.empty() && !ifs.good()) {
throw std::runtime_error("Failed to open world.mt");
} else if (backend.empty()) {
backend = read_setting_default("backend", ifs, "sqlite3");
}
if (backend == "dummy") {
throw std::runtime_error("This map uses the dummy backend and contains no data");
} else if (backend == "sqlite3") {
m_db = new DBSQLite3(input);
#if USE_POSTGRESQL
} else if (backend == "postgresql") {
m_db = new DBPostgreSQL(input);
#endif
#if USE_LEVELDB
} else if (backend == "leveldb") {
m_db = new DBLevelDB(input);
#endif
#if USE_REDIS
} else if (backend == "redis") {
m_db = new DBRedis(input);
#endif
} else {
throw std::runtime_error(std::string("Unknown map backend: ") + backend);
}
if (!read_setting_default("readonly_backend", ifs, "").empty()) {
errorstream << "Warning: Map with readonly_backend is not supported. "
"The result may be incomplete." << std::endl;
}
// Determine how we're going to traverse the database (heuristic)
if (m_exhaustiveSearch == EXH_AUTO) {
size_t y_range = (m_yMax / 16 + 1) - (m_yMin / 16);
size_t blocks = sat_mul<size_t>(m_geomX2 - m_geomX, y_range, m_geomY2 - m_geomY);
verbosestream << "Heuristic parameters:"
<< " preferRangeQueries()=" << m_db->preferRangeQueries()
<< " y_range=" << y_range << " blocks=" << blocks << std::endl;
if (m_db->preferRangeQueries())
m_exhaustiveSearch = EXH_NEVER;
else if (blocks < 200000)
m_exhaustiveSearch = EXH_FULL;
else if (y_range < 42)
m_exhaustiveSearch = EXH_Y;
else
m_exhaustiveSearch = EXH_NEVER;
} else if (m_exhaustiveSearch == EXH_FULL || m_exhaustiveSearch == EXH_Y) {
if (m_db->preferRangeQueries()) {
errorstream << "Note: The current database backend supports efficient "
"range queries, forcing exhaustive search will generally result "
"in worse performance." << std::endl;
}
}
assert(m_exhaustiveSearch != EXH_AUTO);
}
void TileGenerator::closeDatabase()
{
delete m_db;
m_db = NULL;
}
static inline int16_t mod16(int16_t y)
{
if (y < 0)
return (y - 15) / 16;
return y / 16;
}
void TileGenerator::loadBlocks()
{
const int16_t yMax = mod16(m_yMax) + 1;
const int16_t yMin = mod16(m_yMin);
if (m_exhaustiveSearch == EXH_NEVER || m_exhaustiveSearch == EXH_Y) {
std::vector<BlockPos> vec = m_db->getBlockPosXZ(
BlockPos(m_geomX, yMin, m_geomY),
BlockPos(m_geomX2, yMax, m_geomY2)
);
for (auto pos : vec) {
assert(pos.x >= m_geomX && pos.x < m_geomX2);
assert(pos.z >= m_geomY && pos.z < m_geomY2);
// Adjust minimum and maximum positions to the nearest block
m_xMin = mymin<int>(m_xMin, pos.x);
m_xMax = mymax<int>(m_xMax, pos.x);
m_zMin = mymin<int>(m_zMin, pos.z);
m_zMax = mymax<int>(m_zMax, pos.z);
m_positions[pos.z].emplace(pos.x);
}
size_t count = 0;
for (const auto &it : m_positions)
count += it.second.size();
m_progressMax = count;
verbosestream << "Loaded " << count
<< " positions (across Z: " << m_positions.size() << ") for rendering" << std::endl;
}
}
void TileGenerator::createImage()
{
const int scale_d = 40; // pixels reserved for a scale
if(!m_drawScale)
m_scales = 0;
// If a geometry is explicitly set, set the bounding box to the requested geometry
// instead of cropping to the content. This way we will always output a full tile
// of the correct size.
if (m_geomX > -2048 && m_geomX2 < 2048)
{
m_xMin = m_geomX;
m_xMax = m_geomX2-1;
}
if (m_geomY > -2048 && m_geomY2 < 2048)
{
m_zMin = m_geomY;
m_zMax = m_geomY2-1;
}
m_mapWidth = (m_xMax - m_xMin + 1) * 16;
m_mapHeight = (m_zMax - m_zMin + 1) * 16;
m_xBorder = (m_scales & SCALE_LEFT) ? scale_d : 0;
m_yBorder = (m_scales & SCALE_TOP) ? scale_d : 0;
m_blockPixelAttributes.setWidth(m_mapWidth);
int image_width, image_height;
image_width = (m_mapWidth * m_zoom) + m_xBorder;
image_width += (m_scales & SCALE_RIGHT) ? scale_d : 0;
image_height = (m_mapHeight * m_zoom) + m_yBorder;
image_height += (m_scales & SCALE_BOTTOM) ? scale_d : 0;
if(image_width > 4096 || image_height > 4096) {
errorstream << "Warning: The side length of the image to be created exceeds 4096 pixels!"
<< " (dimensions: " << image_width << "x" << image_height << ")"
<< std::endl;
} else {
verbosestream << "Creating image with size " << image_width << "x" << image_height
<< std::endl;
}
m_image = new Image(image_width, image_height);
m_image->drawFilledRect(0, 0, image_width, image_height, m_bgColor); // Background
}
void TileGenerator::renderMap()
{
BlockDecoder blk;
const int16_t yMax = mod16(m_yMax) + 1;
const int16_t yMin = mod16(m_yMin);
size_t count = 0;
auto renderSingle = [&] (int16_t xPos, int16_t zPos, BlockList &blockStack) {
m_readPixels.reset();
m_readInfo.reset();
for (int i = 0; i < 16; i++) {
for (int j = 0; j < 16; j++) {
m_color[i][j] = m_bgColor; // This will be drawn by renderMapBlockBottom() for y-rows with only 'air', 'ignore' or unknown nodes if --drawalpha is used
m_color[i][j].a = 0; // ..but set alpha to 0 to tell renderMapBlock() not to use this color to mix a shade
m_thickness[i][j] = 0;
}
}
for (const auto &it : blockStack) {
const BlockPos pos = it.first;
assert(pos.x == xPos && pos.z == zPos);
assert(pos.y >= yMin && pos.y < yMax);
blk.reset();
blk.decode(it.second);
if (blk.isEmpty())
continue;
renderMapBlock(blk, pos);
// Exit out if all pixels for this MapBlock are covered
if (m_readPixels.full())
break;
}
if (!m_readPixels.full())
renderMapBlockBottom(blockStack.begin()->first);
m_renderedAny |= m_readInfo.any();
};
auto postRenderRow = [&] (int16_t zPos) {
if (m_shading)
renderShading(zPos);
};
if (m_exhaustiveSearch == EXH_NEVER) {
for (auto it = m_positions.rbegin(); it != m_positions.rend(); ++it) {
int16_t zPos = it->first;
for (auto it2 = it->second.rbegin(); it2 != it->second.rend(); ++it2) {
int16_t xPos = *it2;
BlockList blockStack;
m_db->getBlocksOnXZ(blockStack, xPos, zPos, yMin, yMax);
blockStack.sort();
renderSingle(xPos, zPos, blockStack);
reportProgress(count++);
}
postRenderRow(zPos);
}
} else if (m_exhaustiveSearch == EXH_Y) {
verbosestream << "Exhaustively searching height of "
<< (yMax - yMin) << " blocks" << std::endl;
std::vector<BlockPos> positions;
positions.reserve(yMax - yMin);
for (auto it = m_positions.rbegin(); it != m_positions.rend(); ++it) {
int16_t zPos = it->first;
for (auto it2 = it->second.rbegin(); it2 != it->second.rend(); ++it2) {
int16_t xPos = *it2;
positions.clear();
for (int16_t yPos = yMin; yPos < yMax; yPos++)
positions.emplace_back(xPos, yPos, zPos);
BlockList blockStack;
m_db->getBlocksByPos(blockStack, positions);
blockStack.sort();
renderSingle(xPos, zPos, blockStack);
reportProgress(count++);
}
postRenderRow(zPos);
}
} else if (m_exhaustiveSearch == EXH_FULL) {
const size_t span_y = yMax - yMin;
m_progressMax = (m_geomX2 - m_geomX) * span_y * (m_geomY2 - m_geomY);
verbosestream << "Exhaustively searching "
<< (m_geomX2 - m_geomX) << "x" << span_y << "x"
<< (m_geomY2 - m_geomY) << " blocks" << std::endl;
std::vector<BlockPos> positions;
positions.reserve(span_y);
for (int16_t zPos = m_geomY2 - 1; zPos >= m_geomY; zPos--) {
for (int16_t xPos = m_geomX2 - 1; xPos >= m_geomX; xPos--) {
positions.clear();
for (int16_t yPos = yMin; yPos < yMax; yPos++)
positions.emplace_back(xPos, yPos, zPos);
BlockList blockStack;
m_db->getBlocksByPos(blockStack, positions);
blockStack.sort();
renderSingle(xPos, zPos, blockStack);
reportProgress(count++);
}
postRenderRow(zPos);
}
}
reportProgress(m_progressMax);
}
void TileGenerator::renderMapBlock(const BlockDecoder &blk, const BlockPos &pos)
{
int xBegin = (pos.x - m_xMin) * 16;
int zBegin = (m_zMax - pos.z) * 16;
int minY = (pos.y * 16 > m_yMin) ? 0 : m_yMin - pos.y * 16;
int maxY = (pos.y * 16 + 15 < m_yMax) ? 15 : m_yMax - pos.y * 16;
for (int z = 0; z < 16; ++z) {
int imageY = zBegin + 15 - z;
for (int x = 0; x < 16; ++x) {
if (m_readPixels.get(x, z))
continue;
int imageX = xBegin + x;
auto &attr = m_blockPixelAttributes.attribute(15 - z, xBegin + x);
for (int y = maxY; y >= minY; --y) {
const std::string &name = blk.getNode(x, y, z);
if (name.empty())
continue;
ColorMap::const_iterator it = m_colorMap.find(name);
if (it == m_colorMap.end()) {
m_unknownNodes.insert(name);
continue;
}
Color c = it->second.toColor();
if (c.a == 0)
continue; // node is fully invisible
if (m_drawAlpha) {
if (m_color[z][x].a != 0)
c = mixColors(m_color[z][x], c);
if (c.a < 255) {
// remember color and near thickness value
m_color[z][x] = c;
m_thickness[z][x] = (m_thickness[z][x] + it->second.t) / 2;
continue;
}
// color became opaque, draw it
setZoomed(imageX, imageY, c);
attr.thickness = m_thickness[z][x];
} else {
c.a = 255;
setZoomed(imageX, imageY, c);
}
m_readPixels.set(x, z);
// do this afterwards so we can record height values
// inside transparent nodes (water) too
if (!m_readInfo.get(x, z)) {
attr.height = pos.y * 16 + y;
m_readInfo.set(x, z);
}
break;
}
}
}
}
void TileGenerator::renderMapBlockBottom(const BlockPos &pos)
{
if (!m_drawAlpha)
return; // "missing" pixels can only happen with --drawalpha
int xBegin = (pos.x - m_xMin) * 16;
int zBegin = (m_zMax - pos.z) * 16;
for (int z = 0; z < 16; ++z) {
int imageY = zBegin + 15 - z;
for (int x = 0; x < 16; ++x) {
if (m_readPixels.get(x, z))
continue;
int imageX = xBegin + x;
auto &attr = m_blockPixelAttributes.attribute(15 - z, xBegin + x);
// set color since it wasn't done in renderMapBlock()
setZoomed(imageX, imageY, m_color[z][x]);
m_readPixels.set(x, z);
attr.thickness = m_thickness[z][x];
}
}
}
void TileGenerator::renderShading(int zPos)
{
auto &a = m_blockPixelAttributes;
int zBegin = (m_zMax - zPos) * 16;
for (int z = 0; z < 16; ++z) {
int imageY = zBegin + z;
if (imageY >= m_mapHeight)
continue;
for (int x = 0; x < m_mapWidth; ++x) {
if(
!a.attribute(z, x).valid_height() ||
!a.attribute(z, x - 1).valid_height() ||
!a.attribute(z - 1, x).valid_height()
)
continue;
// calculate shadow to apply
int y = a.attribute(z, x).height;
int y1 = a.attribute(z, x - 1).height;
int y2 = a.attribute(z - 1, x).height;
int d = ((y - y1) + (y - y2)) * 12;
if (m_drawAlpha) { // less visible shadow with increasing "thickness"
float t = a.attribute(z, x).thickness * 1.2f;
t = mymin(t, 255.0f);
d *= 1.0f - t / 255.0f;
}
d = mymin(d, 36);
// apply shadow/light by just adding to it pixel values
Color c = m_image->getPixel(getImageX(x), getImageY(imageY));
c.r = colorSafeBounds(c.r + d);
c.g = colorSafeBounds(c.g + d);
c.b = colorSafeBounds(c.b + d);
setZoomed(x, imageY, c);
}
}
a.scroll();
}
void TileGenerator::renderScale()
{
const int scale_d = 40; // see createImage()
if (m_scales & SCALE_TOP) {
m_image->drawText(24, 0, "X", m_scaleColor);
for (int i = (m_xMin / 4) * 4; i <= m_xMax; i += 4) {
std::ostringstream buf;
buf << i * 16;
int xPos = getImageX(i * 16, true);
if (xPos >= 0) {
m_image->drawText(xPos + 2, 0, buf.str(), m_scaleColor);
m_image->drawLine(xPos, 0, xPos, m_yBorder - 1, m_scaleColor);
}
}
}
if (m_scales & SCALE_LEFT) {
m_image->drawText(2, 24, "Z", m_scaleColor);
for (int i = (m_zMax / 4) * 4; i >= m_zMin; i -= 4) {
std::ostringstream buf;
buf << i * 16;
int yPos = getImageY(i * 16 + 1, true);
if (yPos >= 0) {
m_image->drawText(2, yPos, buf.str(), m_scaleColor);
m_image->drawLine(0, yPos, m_xBorder - 1, yPos, m_scaleColor);
}
}
}
if (m_scales & SCALE_BOTTOM) {
int xPos = m_xBorder + m_mapWidth*m_zoom - 24 - 8,
yPos = m_yBorder + m_mapHeight*m_zoom + scale_d - 12;
m_image->drawText(xPos, yPos, "X", m_scaleColor);
for (int i = (m_xMin / 4) * 4; i <= m_xMax; i += 4) {
std::ostringstream buf;
buf << i * 16;
xPos = getImageX(i * 16, true);
yPos = m_yBorder + m_mapHeight*m_zoom;
if (xPos >= 0) {
m_image->drawText(xPos + 2, yPos, buf.str(), m_scaleColor);
m_image->drawLine(xPos, yPos, xPos, yPos + 39, m_scaleColor);
}
}
}
if (m_scales & SCALE_RIGHT) {
int xPos = m_xBorder + m_mapWidth*m_zoom + scale_d - 2 - 8,
yPos = m_yBorder + m_mapHeight*m_zoom - 24 - 12;
m_image->drawText(xPos, yPos, "Z", m_scaleColor);
for (int i = (m_zMax / 4) * 4; i >= m_zMin; i -= 4) {
std::ostringstream buf;
buf << i * 16;
xPos = m_xBorder + m_mapWidth*m_zoom;
yPos = getImageY(i * 16 + 1, true);
if (yPos >= 0) {
m_image->drawText(xPos + 2, yPos, buf.str(), m_scaleColor);
m_image->drawLine(xPos, yPos, xPos + 39, yPos, m_scaleColor);
}
}
}
}
void TileGenerator::renderOrigin()
{
if (m_xMin > 0 || m_xMax < 0 ||
m_zMin > 0 || m_zMax < 0)
return;
m_image->drawCircle(getImageX(0, true), getImageY(0, true), 12, m_originColor);
}
void TileGenerator::renderPlayers(const std::string &input_path)
{
std::string input = input_path;
if (input.back() != PATH_SEPARATOR)
input += PATH_SEPARATOR;
PlayerAttributes players(input);
for (auto &player : players) {
if (player.x < m_xMin * 16 || player.x >= (m_xMax+1) * 16 ||
player.z < m_zMin * 16 || player.z >= (m_zMax+1) * 16)
continue;
if (player.y < m_yMin || player.y > m_yMax)
continue;
int imageX = getImageX(player.x, true),
imageY = getImageY(player.z, true);
m_image->drawFilledRect(imageX - 1, imageY, 3, 1, m_playerColor);
m_image->drawFilledRect(imageX, imageY - 1, 1, 3, m_playerColor);
assert(!player.name.empty());
m_image->drawText(imageX + 2, imageY, player.name, m_playerColor);
}
}
void TileGenerator::writeImage(const std::string &output)
{
m_image->save(output);
delete m_image;
m_image = nullptr;
}
void TileGenerator::printUnknown()
{
if (m_unknownNodes.empty())
return;
errorstream << "Unknown nodes:\n";
for (const auto &node : m_unknownNodes)
errorstream << "\t" << node << '\n';
if (!m_renderedAny) {
errorstream << "The map was read successfully and not empty, but none of the "
"encountered nodes had a color associated.\nCheck that you're using "
"the right colors.txt. It should match the game you have installed.\n";
}
errorstream << std::flush;
}
void TileGenerator::reportProgress(size_t count)
{
if (!m_progressMax)
return;
int percent = count / static_cast<float>(m_progressMax) * 100;
if (percent == m_progressLast)
return;
m_progressLast = percent;
// Print a nice-looking ASCII progress bar
char bar[51] = {0};
memset(bar, ' ', 50);
int i = 0, j = percent;
for (; j >= 2; j -= 2)
bar[i++] = '=';
if (j)
bar[i++] = '-';
std::cout << "[" << bar << "] " << percent << "% " << (percent == 100 ? "\n" : "\r");
std::cout.flush();
}
inline int TileGenerator::getImageX(int val, bool absolute) const
{
if (absolute)
val = (val - m_xMin * 16);
return (m_zoom*val) + m_xBorder;
}
inline int TileGenerator::getImageY(int val, bool absolute) const
{
if (absolute)
val = m_mapHeight - (val - m_zMin * 16); // Z axis is flipped on image
return (m_zoom*val) + m_yBorder;
}
inline void TileGenerator::setZoomed(int x, int y, Color color)
{
m_image->drawFilledRect(getImageX(x), getImageY(y), m_zoom, m_zoom, color);
}

View File

@ -1,167 +0,0 @@
#pragma once
#include <iostream>
#include <map>
#include <set>
#include <unordered_map>
#include <cstdint>
#include <string>
#include "PixelAttributes.h"
#include "Image.h"
#include "db.h"
#include "types.h"
class BlockDecoder;
class Image;
enum {
SCALE_TOP = (1 << 0),
SCALE_BOTTOM = (1 << 1),
SCALE_LEFT = (1 << 2),
SCALE_RIGHT = (1 << 3),
};
enum {
EXH_NEVER, // Always use range queries
EXH_Y, // Exhaustively search Y space, range queries for X/Z
EXH_FULL, // Exhaustively search entire requested geometry
EXH_AUTO, // Automatically pick one of the previous modes
};
struct ColorEntry {
ColorEntry() : r(0), g(0), b(0), a(0), t(0) {};
ColorEntry(uint8_t r, uint8_t g, uint8_t b, uint8_t a, uint8_t t) :
r(r), g(g), b(b), a(a), t(t) {};
inline Color toColor() const { return Color(r, g, b, a); }
uint8_t r, g, b, a; // Red, Green, Blue, Alpha
uint8_t t; // "thickness" value
};
struct BitmapThing { // 16x16 bitmap
inline void reset() {
for (int i = 0; i < 16; ++i)
val[i] = 0;
}
inline bool any_neq(uint16_t v) const {
for (int i = 0; i < 16; ++i) {
if (val[i] != v)
return true;
}
return false;
}
inline bool any() const { return any_neq(0); }
inline bool full() const { return !any_neq(0xffff); }
inline void set(unsigned int x, unsigned int z) {
val[z] |= (1 << x);
}
inline bool get(unsigned int x, unsigned int z) const {
return !!(val[z] & (1 << x));
}
uint16_t val[16];
};
class TileGenerator
{
private:
typedef std::unordered_map<std::string, ColorEntry> ColorMap;
public:
TileGenerator();
~TileGenerator();
void setBgColor(const std::string &bgColor);
void setScaleColor(const std::string &scaleColor);
void setOriginColor(const std::string &originColor);
void setPlayerColor(const std::string &playerColor);
void setDrawOrigin(bool drawOrigin);
void setDrawPlayers(bool drawPlayers);
void setDrawScale(bool drawScale);
void setDrawAlpha(bool drawAlpha);
void setShading(bool shading);
void setGeometry(int x, int y, int w, int h);
void setMinY(int y);
void setMaxY(int y);
void setExhaustiveSearch(int mode);
void parseColorsFile(const std::string &fileName);
void setBackend(std::string backend);
void setZoom(int zoom);
void setScales(uint flags);
void setDontWriteEmpty(bool f);
void generate(const std::string &input, const std::string &output);
void printGeometry(const std::string &input);
void dumpBlock(const std::string &input, BlockPos pos);
static std::set<std::string> getSupportedBackends();
private:
void parseColorsStream(std::istream &in);
void openDb(const std::string &input);
void closeDatabase();
void loadBlocks();
void createImage();
void renderMap();
void renderMapBlock(const BlockDecoder &blk, const BlockPos &pos);
void renderMapBlockBottom(const BlockPos &pos);
void renderShading(int zPos);
void renderScale();
void renderOrigin();
void renderPlayers(const std::string &inputPath);
void writeImage(const std::string &output);
void printUnknown();
void reportProgress(size_t count);
int getImageX(int val, bool absolute=false) const;
int getImageY(int val, bool absolute=false) const;
void setZoomed(int x, int y, Color color);
private:
Color m_bgColor;
Color m_scaleColor;
Color m_originColor;
Color m_playerColor;
bool m_drawOrigin;
bool m_drawPlayers;
bool m_drawScale;
bool m_drawAlpha;
bool m_shading;
bool m_dontWriteEmpty;
std::string m_backend;
int m_xBorder, m_yBorder;
DB *m_db;
Image *m_image;
PixelAttributes m_blockPixelAttributes;
/* smallest/largest seen X or Z block coordinate */
int m_xMin;
int m_xMax;
int m_zMin;
int m_zMax;
/* Y limits for rendered area (node units) */
int m_yMin;
int m_yMax;
/* limits for rendered area (block units) */
int16_t m_geomX;
int16_t m_geomY; /* Y in terms of rendered image, Z in the world */
int16_t m_geomX2;
int16_t m_geomY2;
int m_mapWidth;
int m_mapHeight;
int m_exhaustiveSearch;
std::set<std::string> m_unknownNodes;
bool m_renderedAny;
std::map<int16_t, std::set<int16_t>> m_positions; /* indexed by Z, contains X coords */
ColorMap m_colorMap;
BitmapThing m_readPixels;
BitmapThing m_readInfo;
Color m_color[16][16];
uint8_t m_thickness[16][16];
int m_zoom;
uint m_scales;
size_t m_progressMax;
int m_progressLast; // percentage
}; // class TileGenerator

View File

@ -1,73 +0,0 @@
#include <cstdint>
#include "ZlibDecompressor.h"
#include "config.h"
// for convenient usage of both
#if USE_ZLIB_NG
#include <zlib-ng.h>
#define z_stream zng_stream
#define Z(x) zng_ ## x
#else
#include <zlib.h>
#define Z(x) x
#endif
ZlibDecompressor::ZlibDecompressor(const u8 *data, size_t size):
m_data(data),
m_seekPos(0),
m_size(size)
{
}
ZlibDecompressor::~ZlibDecompressor()
{
}
void ZlibDecompressor::setSeekPos(size_t seekPos)
{
m_seekPos = seekPos;
}
void ZlibDecompressor::decompress(ustring &buffer)
{
const unsigned char *data = m_data + m_seekPos;
const size_t size = m_size - m_seekPos;
// output space is extended in chunks of this size
constexpr size_t BUFSIZE = 8 * 1024;
z_stream strm;
strm.zalloc = Z_NULL;
strm.zfree = Z_NULL;
strm.opaque = Z_NULL;
strm.next_in = Z_NULL;
strm.avail_in = 0;
if (Z(inflateInit)(&strm) != Z_OK)
throw DecompressError();
strm.next_in = const_cast<unsigned char *>(data);
strm.avail_in = size;
if (buffer.empty())
buffer.resize(BUFSIZE);
strm.next_out = &buffer[0];
strm.avail_out = buffer.size();
int ret = 0;
do {
ret = Z(inflate)(&strm, Z_NO_FLUSH);
if (strm.avail_out == 0) {
const auto off = buffer.size();
buffer.resize(off + BUFSIZE);
strm.next_out = &buffer[off];
strm.avail_out = BUFSIZE;
}
} while (ret == Z_OK);
if (ret != Z_STREAM_END)
throw DecompressError();
m_seekPos += strm.next_in - data;
buffer.resize(buffer.size() - strm.avail_out);
(void) Z(inflateEnd)(&strm);
}

View File

@ -1,22 +0,0 @@
#pragma once
#include <exception>
#include "types.h"
class ZlibDecompressor
{
public:
class DecompressError : std::exception {};
ZlibDecompressor(const u8 *data, size_t size);
~ZlibDecompressor();
void setSeekPos(size_t seekPos);
size_t seekPos() const { return m_seekPos; }
// Decompress and return one zlib stream from the buffer
// Advances seekPos as appropriate.
void decompress(ustring &dst);
private:
const u8 *m_data;
size_t m_seekPos, m_size;
};

View File

@ -1,52 +0,0 @@
#include <zstd.h>
#include "ZstdDecompressor.h"
ZstdDecompressor::ZstdDecompressor():
m_data(nullptr),
m_seekPos(0),
m_size(0)
{
m_stream = ZSTD_createDStream();
}
ZstdDecompressor::~ZstdDecompressor()
{
ZSTD_freeDStream(reinterpret_cast<ZSTD_DStream*>(m_stream));
}
void ZstdDecompressor::setData(const u8 *data, size_t size, size_t seekPos)
{
m_data = data;
m_seekPos = seekPos;
m_size = size;
}
void ZstdDecompressor::decompress(ustring &buffer)
{
ZSTD_DStream *stream = reinterpret_cast<ZSTD_DStream*>(m_stream);
ZSTD_inBuffer inbuf = { m_data, m_size, m_seekPos };
// output space is extended in chunks of this size
constexpr size_t BUFSIZE = 8 * 1024;
if (buffer.empty())
buffer.resize(BUFSIZE);
ZSTD_outBuffer outbuf = { &buffer[0], buffer.size(), 0 };
ZSTD_initDStream(stream);
size_t ret;
do {
ret = ZSTD_decompressStream(stream, &outbuf, &inbuf);
if (ret && ZSTD_isError(ret))
throw DecompressError();
if (outbuf.size == outbuf.pos) {
outbuf.size += BUFSIZE;
buffer.resize(outbuf.size);
outbuf.dst = &buffer[0];
}
} while (ret != 0);
m_seekPos = inbuf.pos;
buffer.resize(outbuf.pos);
}

View File

@ -1,23 +0,0 @@
#pragma once
#include <exception>
#include "types.h"
class ZstdDecompressor
{
public:
class DecompressError : std::exception {};
ZstdDecompressor();
~ZstdDecompressor();
void setData(const u8 *data, size_t size, size_t seekPos);
size_t seekPos() const { return m_seekPos; }
// Decompress and return one zstd stream from the buffer
// Advances seekPos as appropriate.
void decompress(ustring &dst);
private:
void *m_stream; // ZSTD_DStream
const u8 *m_data;
size_t m_seekPos, m_size;
};

View File

@ -1,7 +0,0 @@
#if defined(MSDOS) || defined(__OS2__) || defined(__NT__) || defined(_WIN32)
#define PATH_SEPARATOR '\\'
#else
#define PATH_SEPARATOR '/'
#endif
#include "cmake_config.h"

View File

@ -1,135 +0,0 @@
#include <stdexcept>
#include <sstream>
#include <algorithm>
#include "db-leveldb.h"
#include "types.h"
static inline int64_t stoi64(const std::string &s)
{
std::istringstream tmp(s);
int64_t t;
tmp >> t;
return t;
}
static inline std::string i64tos(int64_t i)
{
std::ostringstream os;
os << i;
return os.str();
}
// finds the first position in the list where it.x >= x
#define lower_bound_x(container, find_x) \
std::lower_bound((container).begin(), (container).end(), (find_x), \
[] (const vec2 &left, int16_t right) { \
return left.x < right; \
})
DBLevelDB::DBLevelDB(const std::string &mapdir)
{
leveldb::Options options;
options.create_if_missing = false;
leveldb::Status status = leveldb::DB::Open(options, mapdir + "map.db", &db);
if (!status.ok()) {
throw std::runtime_error(std::string("Failed to open database: ") + status.ToString());
}
/* LevelDB is a dumb key-value store, so the only optimization we can do
* is to cache the block positions that exist in the db.
*/
loadPosCache();
}
DBLevelDB::~DBLevelDB()
{
delete db;
}
std::vector<BlockPos> DBLevelDB::getBlockPosXZ(BlockPos min, BlockPos max)
{
std::vector<BlockPos> res;
for (const auto &it : posCache) {
const int16_t zpos = it.first;
if (zpos < min.z || zpos >= max.z)
continue;
auto it2 = lower_bound_x(it.second, min.x);
for (; it2 != it.second.end(); it2++) {
const auto &pos2 = *it2;
if (pos2.x >= max.x)
break; // went past
if (pos2.y < min.y || pos2.y >= max.y)
continue;
// skip duplicates
if (!res.empty() && res.back().x == pos2.x && res.back().z == zpos)
continue;
res.emplace_back(pos2.x, pos2.y, zpos);
}
}
return res;
}
void DBLevelDB::loadPosCache()
{
leveldb::Iterator *it = db->NewIterator(leveldb::ReadOptions());
for (it->SeekToFirst(); it->Valid(); it->Next()) {
int64_t posHash = stoi64(it->key().ToString());
BlockPos pos = decodeBlockPos(posHash);
posCache[pos.z].emplace_back(pos.x, pos.y);
}
delete it;
for (auto &it : posCache)
std::sort(it.second.begin(), it.second.end());
}
void DBLevelDB::getBlocksOnXZ(BlockList &blocks, int16_t x, int16_t z,
int16_t min_y, int16_t max_y)
{
std::string datastr;
leveldb::Status status;
auto it = posCache.find(z);
if (it == posCache.cend())
return;
auto it2 = lower_bound_x(it->second, x);
if (it2 == it->second.end() || it2->x != x)
return;
// it2 is now pointing to a contigous part where it2->x == x
for (; it2 != it->second.end(); it2++) {
const auto &pos2 = *it2;
if (pos2.x != x)
break; // went past
if (pos2.y < min_y || pos2.y >= max_y)
continue;
BlockPos pos(x, pos2.y, z);
status = db->Get(leveldb::ReadOptions(), i64tos(encodeBlockPos(pos)), &datastr);
if (status.ok()) {
blocks.emplace_back(
pos, ustring((unsigned char *) datastr.data(), datastr.size())
);
}
}
}
void DBLevelDB::getBlocksByPos(BlockList &blocks,
const std::vector<BlockPos> &positions)
{
std::string datastr;
leveldb::Status status;
for (auto pos : positions) {
status = db->Get(leveldb::ReadOptions(), i64tos(encodeBlockPos(pos)), &datastr);
if (status.ok()) {
blocks.emplace_back(
pos, ustring((unsigned char *) datastr.data(), datastr.size())
);
}
}
}

View File

@ -1,41 +0,0 @@
#pragma once
#include "db.h"
#include <unordered_map>
#include <utility>
#include <leveldb/db.h>
class DBLevelDB : public DB {
public:
DBLevelDB(const std::string &mapdir);
std::vector<BlockPos> getBlockPosXZ(BlockPos min, BlockPos max) override;
void getBlocksOnXZ(BlockList &blocks, int16_t x, int16_t z,
int16_t min_y, int16_t max_y) override;
void getBlocksByPos(BlockList &blocks,
const std::vector<BlockPos> &positions) override;
~DBLevelDB() override;
bool preferRangeQueries() const override { return false; }
private:
struct vec2 {
int16_t x, y;
constexpr vec2() : x(0), y(0) {}
constexpr vec2(int16_t x, int16_t y) : x(x), y(y) {}
inline bool operator<(const vec2 &p) const
{
if (x < p.x)
return true;
if (x > p.x)
return false;
return y < p.y;
}
};
void loadPosCache();
// indexed by Z, contains all (x,y) position pairs
std::unordered_map<int16_t, std::vector<vec2>> posCache;
leveldb::DB *db = NULL;
};

View File

@ -1,229 +0,0 @@
#include <stdexcept>
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <arpa/inet.h>
#include "db-postgresql.h"
#include "util.h"
#include "log.h"
#include "types.h"
/* PostgreSQLBase */
PostgreSQLBase::~PostgreSQLBase()
{
if (db)
PQfinish(db);
}
void PostgreSQLBase::openDatabase(const char *connect_string)
{
if (db)
throw std::logic_error("Database already open");
db = PQconnectdb(connect_string);
if (PQstatus(db) != CONNECTION_OK) {
throw std::runtime_error(std::string("PostgreSQL database error: ") +
PQerrorMessage(db)
);
}
}
PGresult *PostgreSQLBase::checkResults(PGresult *res, bool clear)
{
ExecStatusType statusType = PQresultStatus(res);
switch (statusType) {
case PGRES_COMMAND_OK:
case PGRES_TUPLES_OK:
break;
case PGRES_FATAL_ERROR:
throw std::runtime_error(
std::string("PostgreSQL database error: ") +
PQresultErrorMessage(res)
);
default:
throw std::runtime_error(
std::string("Unhandled PostgreSQL result code ") +
std::to_string(statusType)
);
}
if (clear)
PQclear(res);
return res;
}
PGresult *PostgreSQLBase::execPrepared(
const char *stmtName, const int paramsNumber,
const void **params,
const int *paramsLengths, const int *paramsFormats,
bool clear)
{
return checkResults(PQexecPrepared(db, stmtName, paramsNumber,
(const char* const*) params, paramsLengths, paramsFormats,
1 /* binary output */), clear
);
}
/* DBPostgreSQL */
DBPostgreSQL::DBPostgreSQL(const std::string &mapdir)
{
std::ifstream ifs(mapdir + "world.mt");
if (!ifs.good())
throw std::runtime_error("Failed to read world.mt");
std::string connect_string = read_setting("pgsql_connection", ifs);
ifs.close();
openDatabase(connect_string.c_str());
prepareStatement(
"get_block_pos",
"SELECT posX::int4, posZ::int4 FROM blocks WHERE"
" (posX BETWEEN $1::int4 AND $2::int4) AND"
" (posY BETWEEN $3::int4 AND $4::int4) AND"
" (posZ BETWEEN $5::int4 AND $6::int4) GROUP BY posX, posZ"
);
prepareStatement(
"get_blocks",
"SELECT posY::int4, data FROM blocks WHERE"
" posX = $1::int4 AND posZ = $2::int4"
" AND (posY BETWEEN $3::int4 AND $4::int4)"
);
prepareStatement(
"get_block_exact",
"SELECT data FROM blocks WHERE"
" posX = $1::int4 AND posY = $2::int4 AND posZ = $3::int4"
);
checkResults(PQexec(db, "START TRANSACTION;"));
checkResults(PQexec(db, "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;"));
}
DBPostgreSQL::~DBPostgreSQL()
{
try {
checkResults(PQexec(db, "COMMIT;"));
} catch (const std::exception& caught) {
errorstream << "could not finalize: " << caught.what() << std::endl;
}
}
std::vector<BlockPos> DBPostgreSQL::getBlockPosXZ(BlockPos min, BlockPos max)
{
int32_t const x1 = htonl(min.x);
int32_t const x2 = htonl(max.x - 1);
int32_t const y1 = htonl(min.y);
int32_t const y2 = htonl(max.y - 1);
int32_t const z1 = htonl(min.z);
int32_t const z2 = htonl(max.z - 1);
const void *args[] = { &x1, &x2, &y1, &y2, &z1, &z2 };
const int argLen[] = { 4, 4, 4, 4, 4, 4 };
const int argFmt[] = { 1, 1, 1, 1, 1, 1 };
PGresult *results = execPrepared(
"get_block_pos", ARRLEN(args), args,
argLen, argFmt, false
);
int numrows = PQntuples(results);
std::vector<BlockPos> positions;
positions.reserve(numrows);
BlockPos pos;
for (int row = 0; row < numrows; ++row) {
pos.x = pg_binary_to_int(results, row, 0);
pos.z = pg_binary_to_int(results, row, 1);
positions.push_back(pos);
}
PQclear(results);
return positions;
}
void DBPostgreSQL::getBlocksOnXZ(BlockList &blocks, int16_t xPos, int16_t zPos,
int16_t min_y, int16_t max_y)
{
int32_t const x = htonl(xPos);
int32_t const z = htonl(zPos);
int32_t const y1 = htonl(min_y);
int32_t const y2 = htonl(max_y - 1);
const void *args[] = { &x, &z, &y1, &y2 };
const int argLen[] = { 4, 4, 4, 4 };
const int argFmt[] = { 1, 1, 1, 1 };
PGresult *results = execPrepared(
"get_blocks", ARRLEN(args), args,
argLen, argFmt, false
);
int numrows = PQntuples(results);
for (int row = 0; row < numrows; ++row) {
BlockPos position;
position.x = xPos;
position.y = pg_binary_to_int(results, row, 0);
position.z = zPos;
blocks.emplace_back(
position,
ustring(
reinterpret_cast<unsigned char*>(
PQgetvalue(results, row, 1)
),
PQgetlength(results, row, 1)
)
);
}
PQclear(results);
}
void DBPostgreSQL::getBlocksByPos(BlockList &blocks,
const std::vector<BlockPos> &positions)
{
int32_t x, y, z;
const void *args[] = { &x, &y, &z };
const int argLen[] = { 4, 4, 4 };
const int argFmt[] = { 1, 1, 1 };
for (auto pos : positions) {
x = htonl(pos.x);
y = htonl(pos.y);
z = htonl(pos.z);
PGresult *results = execPrepared(
"get_block_exact", ARRLEN(args), args,
argLen, argFmt, false
);
if (PQntuples(results) > 0) {
blocks.emplace_back(
pos,
ustring(
reinterpret_cast<unsigned char*>(
PQgetvalue(results, 0, 0)
),
PQgetlength(results, 0, 0)
)
);
}
PQclear(results);
}
}
int DBPostgreSQL::pg_binary_to_int(PGresult *res, int row, int col)
{
int32_t* raw = reinterpret_cast<int32_t*>(PQgetvalue(res, row, col));
return ntohl(*raw);
}

View File

@ -1,41 +0,0 @@
#pragma once
#include "db.h"
#include <libpq-fe.h>
class PostgreSQLBase {
public:
~PostgreSQLBase();
protected:
void openDatabase(const char *connect_string);
PGresult *checkResults(PGresult *res, bool clear = true);
void prepareStatement(const std::string &name, const std::string &sql) {
checkResults(PQprepare(db, name.c_str(), sql.c_str(), 0, NULL));
}
PGresult *execPrepared(
const char *stmtName, const int paramsNumber,
const void **params,
const int *paramsLengths = nullptr, const int *paramsFormats = nullptr,
bool clear = true
);
PGconn *db = NULL;
};
class DBPostgreSQL : public DB, PostgreSQLBase {
public:
DBPostgreSQL(const std::string &mapdir);
std::vector<BlockPos> getBlockPosXZ(BlockPos min, BlockPos max) override;
void getBlocksOnXZ(BlockList &blocks, int16_t x, int16_t z,
int16_t min_y, int16_t max_y) override;
void getBlocksByPos(BlockList &blocks,
const std::vector<BlockPos> &positions) override;
~DBPostgreSQL() override;
bool preferRangeQueries() const override { return true; }
private:
int pg_binary_to_int(PGresult *res, int row, int col);
};

View File

@ -1,206 +0,0 @@
#include <stdexcept>
#include <sstream>
#include <fstream>
#include "db-redis.h"
#include "types.h"
#include "util.h"
#define DB_REDIS_HMGET_NUMFIELDS 30
#define REPLY_TYPE_ERR(reply, desc) do { \
throw std::runtime_error(std::string("Unexpected type for " desc ": ") \
+ replyTypeStr((reply)->type)); \
} while(0)
static inline int64_t stoi64(const std::string &s)
{
std::stringstream tmp(s);
int64_t t;
tmp >> t;
return t;
}
static inline std::string i64tos(int64_t i)
{
std::ostringstream os;
os << i;
return os.str();
}
DBRedis::DBRedis(const std::string &mapdir)
{
std::ifstream ifs(mapdir + "world.mt");
if (!ifs.good())
throw std::runtime_error("Failed to read world.mt");
std::string tmp;
tmp = read_setting("redis_address", ifs);
ifs.seekg(0);
hash = read_setting("redis_hash", ifs);
ifs.seekg(0);
if (tmp.find('/') != std::string::npos) {
ctx = redisConnectUnix(tmp.c_str());
} else {
int port = stoi64(read_setting_default("redis_port", ifs, "6379"));
ctx = redisConnect(tmp.c_str(), port);
}
if (!ctx) {
throw std::runtime_error("Cannot allocate redis context");
} else if (ctx->err) {
std::string err = std::string("Connection error: ") + ctx->errstr;
redisFree(ctx);
throw std::runtime_error(err);
}
/* Redis is just a key-value store, so the only optimization we can do
* is to cache the block positions that exist in the db.
*/
loadPosCache();
}
DBRedis::~DBRedis()
{
redisFree(ctx);
}
std::vector<BlockPos> DBRedis::getBlockPosXZ(BlockPos min, BlockPos max)
{
std::vector<BlockPos> res;
for (const auto &it : posCache) {
if (it.first < min.z || it.first >= max.z)
continue;
for (auto pos2 : it.second) {
if (pos2.first < min.x || pos2.first >= max.x)
continue;
if (pos2.second < min.y || pos2.second >= max.y)
continue;
res.emplace_back(pos2.first, pos2.second, it.first);
}
}
return res;
}
const char *DBRedis::replyTypeStr(int type)
{
switch (type) {
case REDIS_REPLY_STATUS:
return "REDIS_REPLY_STATUS";
case REDIS_REPLY_ERROR:
return "REDIS_REPLY_ERROR";
case REDIS_REPLY_INTEGER:
return "REDIS_REPLY_INTEGER";
case REDIS_REPLY_NIL:
return "REDIS_REPLY_NIL";
case REDIS_REPLY_STRING:
return "REDIS_REPLY_STRING";
case REDIS_REPLY_ARRAY:
return "REDIS_REPLY_ARRAY";
default:
return "(unknown)";
}
}
void DBRedis::loadPosCache()
{
redisReply *reply;
reply = (redisReply*) redisCommand(ctx, "HKEYS %s", hash.c_str());
if (!reply)
throw std::runtime_error("Redis command HKEYS failed");
if (reply->type != REDIS_REPLY_ARRAY)
REPLY_TYPE_ERR(reply, "HKEYS reply");
for (size_t i = 0; i < reply->elements; i++) {
if (reply->element[i]->type != REDIS_REPLY_STRING)
REPLY_TYPE_ERR(reply->element[i], "HKEYS subreply");
BlockPos pos = decodeBlockPos(stoi64(reply->element[i]->str));
posCache[pos.z].emplace_back(pos.x, pos.y);
}
freeReplyObject(reply);
}
void DBRedis::HMGET(const std::vector<BlockPos> &positions,
std::function<void(std::size_t, ustring)> result)
{
const char *argv[DB_REDIS_HMGET_NUMFIELDS + 2];
argv[0] = "HMGET";
argv[1] = hash.c_str();
auto position = positions.begin();
size_t remaining = positions.size();
size_t abs_i = 0;
while (remaining > 0) {
const size_t batch_size = mymin<size_t>(DB_REDIS_HMGET_NUMFIELDS, remaining);
redisReply *reply;
{
// storage to preserve validity of .c_str()
std::string keys[batch_size];
for (size_t i = 0; i < batch_size; ++i) {
keys[i] = i64tos(encodeBlockPos(*position++));
argv[i+2] = keys[i].c_str();
}
reply = (redisReply*) redisCommandArgv(ctx, batch_size + 2, argv, NULL);
}
if (!reply)
throw std::runtime_error("Redis command HMGET failed");
if (reply->type != REDIS_REPLY_ARRAY)
REPLY_TYPE_ERR(reply, "HMGET reply");
if (reply->elements != batch_size) {
freeReplyObject(reply);
throw std::runtime_error("HMGET wrong number of elements");
}
for (size_t i = 0; i < reply->elements; ++i) {
redisReply *subreply = reply->element[i];
if (subreply->type == REDIS_REPLY_NIL)
continue;
else if (subreply->type != REDIS_REPLY_STRING)
REPLY_TYPE_ERR(subreply, "HMGET subreply");
if (subreply->len == 0)
throw std::runtime_error("HMGET empty string");
result(abs_i + i, ustring(
reinterpret_cast<const unsigned char*>(subreply->str),
subreply->len
));
}
freeReplyObject(reply);
abs_i += batch_size;
remaining -= batch_size;
}
}
void DBRedis::getBlocksOnXZ(BlockList &blocks, int16_t x, int16_t z,
int16_t min_y, int16_t max_y)
{
auto it = posCache.find(z);
if (it == posCache.cend())
return;
std::vector<BlockPos> positions;
for (auto pos2 : it->second) {
if (pos2.first == x && pos2.second >= min_y && pos2.second < max_y)
positions.emplace_back(x, pos2.second, z);
}
getBlocksByPos(blocks, positions);
}
void DBRedis::getBlocksByPos(BlockList &blocks,
const std::vector<BlockPos> &positions)
{
auto result = [&] (std::size_t i, ustring data) {
blocks.emplace_back(positions[i], std::move(data));
};
HMGET(positions, result);
}

View File

@ -1,34 +0,0 @@
#pragma once
#include "db.h"
#include <unordered_map>
#include <utility>
#include <functional>
#include <hiredis/hiredis.h>
class DBRedis : public DB {
public:
DBRedis(const std::string &mapdir);
std::vector<BlockPos> getBlockPosXZ(BlockPos min, BlockPos max) override;
void getBlocksOnXZ(BlockList &blocks, int16_t x, int16_t z,
int16_t min_y, int16_t max_y) override;
void getBlocksByPos(BlockList &blocks,
const std::vector<BlockPos> &positions) override;
~DBRedis() override;
bool preferRangeQueries() const override { return false; }
private:
using pos2d = std::pair<int16_t, int16_t>;
static const char *replyTypeStr(int type);
void loadPosCache();
void HMGET(const std::vector<BlockPos> &positions,
std::function<void(std::size_t, ustring)> result);
// indexed by Z, contains all (x,y) position pairs
std::unordered_map<int16_t, std::vector<pos2d>> posCache;
redisContext *ctx;
std::string hash;
};

View File

@ -1,261 +0,0 @@
#include <stdexcept>
#include <unistd.h> // for usleep
#include <iostream>
#include <algorithm>
#include <cassert>
#include "db-sqlite3.h"
#include "log.h"
#include "types.h"
/* SQLite3Base */
#define SQLRES(r, good) check_result(r, good)
#define SQLOK(r) SQLRES(r, SQLITE_OK)
SQLite3Base::~SQLite3Base()
{
if (db && sqlite3_close(db) != SQLITE_OK) {
errorstream << "Error closing SQLite database: "
<< sqlite3_errmsg(db) << std::endl;
}
}
void SQLite3Base::openDatabase(const char *path, bool readonly)
{
if (db)
throw std::logic_error("Database already open");
int flags = 0;
if (readonly)
flags |= SQLITE_OPEN_READONLY | SQLITE_OPEN_PRIVATECACHE;
else
flags |= SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE;
#ifdef SQLITE_OPEN_EXRESCODE
flags |= SQLITE_OPEN_EXRESCODE;
#endif
SQLOK(sqlite3_open_v2(path, &db, flags, 0));
}
/* DBSQLite3 */
// make sure a row is available. intended to be used outside a loop.
// compare result to SQLITE_ROW afterwards.
#define SQLROW1(stmt) \
while ((result = sqlite3_step(stmt)) == SQLITE_BUSY) \
usleep(10000); /* wait some time and try again */ \
if (result != SQLITE_ROW && result != SQLITE_DONE) { \
throw std::runtime_error(sqlite3_errmsg(db)); \
}
// make sure next row is available. intended to be used in a while(sqlite3_step) loop
#define SQLROW2() \
if (result == SQLITE_BUSY) { \
usleep(10000); /* wait some time and try again */ \
continue; \
} else if (result != SQLITE_ROW) { \
throw std::runtime_error(sqlite3_errmsg(db)); \
}
DBSQLite3::DBSQLite3(const std::string &mapdir)
{
std::string db_name = mapdir + "map.sqlite";
openDatabase(db_name.c_str());
// There's a simple, dumb way to check if we have a new or old database schema.
// If we prepare a statement that references columns that don't exist, it will
// error right there.
int result = prepare(stmt_get_block_pos, "SELECT x, y, z FROM blocks");
newFormat = result == SQLITE_OK;
verbosestream << "Detected " << (newFormat ? "new" : "old") << " SQLite schema" << std::endl;
if (newFormat) {
SQLOK(prepare(stmt_get_blocks_xz_range,
"SELECT y, data FROM blocks WHERE "
"x = ? AND z = ? AND y BETWEEN ? AND ?"));
SQLOK(prepare(stmt_get_block_exact,
"SELECT data FROM blocks WHERE x = ? AND y = ? AND z = ?"));
SQLOK(prepare(stmt_get_block_pos_range,
"SELECT x, z FROM blocks WHERE "
"x >= ? AND y >= ? AND z >= ? AND "
"x < ? AND y < ? AND z < ? GROUP BY x, z"));
} else {
SQLOK(prepare(stmt_get_blocks_z,
"SELECT pos, data FROM blocks WHERE pos BETWEEN ? AND ?"));
SQLOK(prepare(stmt_get_block_exact,
"SELECT data FROM blocks WHERE pos = ?"));
SQLOK(prepare(stmt_get_block_pos,
"SELECT pos FROM blocks"));
SQLOK(prepare(stmt_get_block_pos_range,
"SELECT pos FROM blocks WHERE pos BETWEEN ? AND ?"));
}
#undef RANGE
}
DBSQLite3::~DBSQLite3()
{
sqlite3_finalize(stmt_get_blocks_z);
sqlite3_finalize(stmt_get_blocks_xz_range);
sqlite3_finalize(stmt_get_block_pos);
sqlite3_finalize(stmt_get_block_pos_range);
sqlite3_finalize(stmt_get_block_exact);
}
inline void DBSQLite3::getPosRange(int64_t &min, int64_t &max,
int16_t zPos, int16_t zPos2)
{
// Magic numbers!
min = encodeBlockPos(BlockPos(0, -2048, zPos));
max = encodeBlockPos(BlockPos(0, 2048, zPos2)) - 1;
}
std::vector<BlockPos> DBSQLite3::getBlockPosXZ(BlockPos min, BlockPos max)
{
int result;
sqlite3_stmt *stmt;
if (newFormat) {
stmt = stmt_get_block_pos_range;
int col = bind_pos(stmt, 1, min);
bind_pos(stmt, col, max);
} else {
// can handle range query on Z axis via SQL
if (min.z <= -2048 && max.z >= 2048) {
stmt = stmt_get_block_pos;
} else {
stmt = stmt_get_block_pos_range;
int64_t minPos, maxPos;
if (min.z < -2048)
min.z = -2048;
if (max.z > 2048)
max.z = 2048;
getPosRange(minPos, maxPos, min.z, max.z - 1);
SQLOK(sqlite3_bind_int64(stmt, 1, minPos));
SQLOK(sqlite3_bind_int64(stmt, 2, maxPos));
}
}
std::vector<BlockPos> positions;
BlockPos pos;
while ((result = sqlite3_step(stmt)) != SQLITE_DONE) {
SQLROW2()
if (newFormat) {
pos.x = sqlite3_column_int(stmt, 0);
pos.z = sqlite3_column_int(stmt, 1);
} else {
pos = decodeBlockPos(sqlite3_column_int64(stmt, 0));
if (pos.x < min.x || pos.x >= max.x || pos.y < min.y || pos.y >= max.y)
continue;
// note that we can't try to deduplicate these because the order
// of the encoded pos (if sorted) is ZYX.
}
positions.emplace_back(pos);
}
SQLOK(sqlite3_reset(stmt));
return positions;
}
void DBSQLite3::loadBlockCache(int16_t zPos)
{
int result;
blockCache.clear();
assert(!newFormat);
int64_t minPos, maxPos;
getPosRange(minPos, maxPos, zPos, zPos);
SQLOK(sqlite3_bind_int64(stmt_get_blocks_z, 1, minPos));
SQLOK(sqlite3_bind_int64(stmt_get_blocks_z, 2, maxPos));
while ((result = sqlite3_step(stmt_get_blocks_z)) != SQLITE_DONE) {
SQLROW2()
int64_t posHash = sqlite3_column_int64(stmt_get_blocks_z, 0);
BlockPos pos = decodeBlockPos(posHash);
blockCache[pos.x].emplace_back(pos, read_blob(stmt_get_blocks_z, 1));
}
SQLOK(sqlite3_reset(stmt_get_blocks_z));
}
void DBSQLite3::getBlocksOnXZ(BlockList &blocks, int16_t x, int16_t z,
int16_t min_y, int16_t max_y)
{
// New format: use a real range query
if (newFormat) {
auto *stmt = stmt_get_blocks_xz_range;
SQLOK(sqlite3_bind_int(stmt, 1, x));
SQLOK(sqlite3_bind_int(stmt, 2, z));
SQLOK(sqlite3_bind_int(stmt, 3, min_y));
SQLOK(sqlite3_bind_int(stmt, 4, max_y - 1)); // BETWEEN is inclusive
int result;
while ((result = sqlite3_step(stmt)) != SQLITE_DONE) {
SQLROW2()
BlockPos pos(x, sqlite3_column_int(stmt, 0), z);
blocks.emplace_back(pos, read_blob(stmt, 1));
}
SQLOK(sqlite3_reset(stmt));
return;
}
/* Cache the blocks on the given Z coordinate between calls, this only
* works due to order in which the TileGenerator asks for blocks. */
if (z != blockCachedZ) {
loadBlockCache(z);
blockCachedZ = z;
}
auto it = blockCache.find(x);
if (it == blockCache.end())
return;
if (it->second.empty()) {
/* We have swapped this list before, this is not supposed to happen
* because it's bad for performance. But rather than silently breaking
* do the right thing and load the blocks again. */
verbosestream << "suboptimal access pattern for sqlite3 backend?!" << std::endl;
loadBlockCache(z);
}
// Swap lists to avoid copying contents
blocks.clear();
std::swap(blocks, it->second);
for (auto it = blocks.begin(); it != blocks.end(); ) {
if (it->first.y < min_y || it->first.y >= max_y)
it = blocks.erase(it);
else
it++;
}
}
void DBSQLite3::getBlocksByPos(BlockList &blocks,
const std::vector<BlockPos> &positions)
{
int result;
for (auto pos : positions) {
bind_pos(stmt_get_block_exact, 1, pos);
SQLROW1(stmt_get_block_exact)
if (result == SQLITE_ROW)
blocks.emplace_back(pos, read_blob(stmt_get_block_exact, 0));
SQLOK(sqlite3_reset(stmt_get_block_exact));
}
}

View File

@ -1,87 +0,0 @@
#pragma once
#include "db.h"
#include <unordered_map>
#include <sqlite3.h>
class SQLite3Base {
public:
~SQLite3Base();
protected:
void openDatabase(const char *path, bool readonly = true);
// check function result or throw error
inline void check_result(int result, int good = SQLITE_OK)
{
if (result != good)
throw std::runtime_error(sqlite3_errmsg(db));
}
// prepare a statement
inline int prepare(sqlite3_stmt *&stmt, const char *sql)
{
return sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
}
// read string from statement
static inline std::string read_str(sqlite3_stmt *stmt, int iCol)
{
auto *data = reinterpret_cast<const char*>(
sqlite3_column_text(stmt, iCol));
return std::string(data);
}
// read blob from statement
static inline ustring read_blob(sqlite3_stmt *stmt, int iCol)
{
auto *data = reinterpret_cast<const unsigned char *>(
sqlite3_column_blob(stmt, iCol));
size_t size = sqlite3_column_bytes(stmt, iCol);
return ustring(data, size);
}
sqlite3 *db = NULL;
};
class DBSQLite3 : public DB, SQLite3Base {
public:
DBSQLite3(const std::string &mapdir);
std::vector<BlockPos> getBlockPosXZ(BlockPos min, BlockPos max) override;
void getBlocksOnXZ(BlockList &blocks, int16_t x, int16_t z,
int16_t min_y, int16_t max_y) override;
void getBlocksByPos(BlockList &blocks,
const std::vector<BlockPos> &positions) override;
~DBSQLite3() override;
bool preferRangeQueries() const override { return newFormat; }
private:
static inline void getPosRange(int64_t &min, int64_t &max, int16_t zPos,
int16_t zPos2);
void loadBlockCache(int16_t zPos);
// bind pos to statement. returns index of next column.
inline int bind_pos(sqlite3_stmt *stmt, int iCol, BlockPos pos)
{
if (newFormat) {
sqlite3_bind_int(stmt, iCol, pos.x);
sqlite3_bind_int(stmt, iCol + 1, pos.y);
sqlite3_bind_int(stmt, iCol + 2, pos.z);
return iCol + 3;
} else {
sqlite3_bind_int64(stmt, iCol, encodeBlockPos(pos));
return iCol + 1;
}
}
sqlite3_stmt *stmt_get_block_pos = NULL;
sqlite3_stmt *stmt_get_block_pos_range = NULL;
sqlite3_stmt *stmt_get_blocks_z = NULL;
sqlite3_stmt *stmt_get_blocks_xz_range = NULL;
sqlite3_stmt *stmt_get_block_exact = NULL;
bool newFormat = false;
int16_t blockCachedZ = -10000;
std::unordered_map<int16_t, BlockList> blockCache; // indexed by X
};

119
src/db.h
View File

@ -1,119 +0,0 @@
#pragma once
#include <cstdint>
#include <list>
#include <vector>
#include <utility>
#include "types.h"
struct BlockPos {
int16_t x, y, z;
constexpr BlockPos() : x(0), y(0), z(0) {}
explicit constexpr BlockPos(int16_t v) : x(v), y(v), z(v) {}
constexpr BlockPos(int16_t x, int16_t y, int16_t z) : x(x), y(y), z(z) {}
// Implements the inverse ordering so that (2,2,2) < (1,1,1)
inline bool operator<(const BlockPos &p) const
{
if (z > p.z)
return true;
if (z < p.z)
return false;
if (y > p.y)
return true;
if (y < p.y)
return false;
return x > p.x;
}
};
typedef std::pair<BlockPos, ustring> Block;
typedef std::list<Block> BlockList;
class DB {
protected:
// Helpers that implement the hashed positions used by most backends
static inline int64_t encodeBlockPos(const BlockPos pos);
static inline BlockPos decodeBlockPos(int64_t hash);
public:
/* Return all unique (X, Z) position pairs inside area given by min and max,
* so that min.x <= x < max.x && min.z <= z < max.z
* Note: duplicates are allowed, but results in wasted time.
*/
virtual std::vector<BlockPos> getBlockPosXZ(BlockPos min, BlockPos max) = 0;
/* Read all blocks in column given by x and z
* and inside the given Y range (min_y <= y < max_y) into list
*/
virtual void getBlocksOnXZ(BlockList &blocks, int16_t x, int16_t z,
int16_t min_y, int16_t max_y) = 0;
/* Read blocks at given positions into list
*/
virtual void getBlocksByPos(BlockList &blocks,
const std::vector<BlockPos> &positions) = 0;
/* Can this database efficiently do range queries?
* (for large data sets, more efficient that brute force)
*/
virtual bool preferRangeQueries() const = 0;
virtual ~DB() {}
};
/****************
* Black magic! *
****************
* The position hashing is seriously messed up,
* and is a lot more complicated than it looks.
*/
static inline int16_t unsigned_to_signed(uint16_t i, uint16_t max_positive)
{
if (i < max_positive) {
return i;
} else {
return i - (max_positive * 2);
}
}
// Modulo of a negative number does not work consistently in C
static inline int64_t pythonmodulo(int64_t i, int64_t mod)
{
if (i >= 0) {
return i % mod;
}
return mod - ((-i) % mod);
}
inline int64_t DB::encodeBlockPos(const BlockPos pos)
{
return (uint64_t) pos.z * 0x1000000 +
(uint64_t) pos.y * 0x1000 +
(uint64_t) pos.x;
}
inline BlockPos DB::decodeBlockPos(int64_t hash)
{
BlockPos pos;
pos.x = unsigned_to_signed(pythonmodulo(hash, 4096), 2048);
hash = (hash - pos.x) / 4096;
pos.y = unsigned_to_signed(pythonmodulo(hash, 4096), 2048);
hash = (hash - pos.y) / 4096;
pos.z = unsigned_to_signed(pythonmodulo(hash, 4096), 2048);
return pos;
}
/*******************
* End black magic *
*******************/

View File

@ -1,16 +0,0 @@
#include <iostream>
#include "log.h"
StreamProxy errorstream(nullptr);
StreamProxy verbosestream(nullptr);
void configure_log_streams(bool verbose)
{
errorstream << std::flush;
verbosestream << std::flush;
errorstream = std::cerr.good() ? &std::cerr : nullptr;
// std::clog does not automatically flush
verbosestream = (verbose && std::clog.good()) ? &std::clog : nullptr;
}

View File

@ -1,40 +0,0 @@
#pragma once
#include <iostream>
#include <utility>
// Forwards to an ostream, optionally
class StreamProxy {
public:
StreamProxy(std::ostream *os) : m_os(os) {}
template<typename T>
StreamProxy &operator<<(T &&arg)
{
if (m_os)
*m_os << std::forward<T>(arg);
return *this;
}
StreamProxy &operator<<(std::ostream &(*func)(std::ostream&))
{
if (m_os)
*m_os << func;
return *this;
}
private:
std::ostream *m_os;
};
/// Error and warning output, forwards to std::cerr
extern StreamProxy errorstream;
/// Verbose output, might forward to std::cerr
extern StreamProxy verbosestream;
/**
* Configure log streams defined in this file.
* @param verbose enable verbose output
* @note not thread-safe!
*/
void configure_log_streams(bool verbose);

View File

@ -1,287 +0,0 @@
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <getopt.h>
#include <fstream>
#include <iostream>
#include <utility>
#include <string>
#include <sstream>
#include <stdexcept>
#include "config.h"
#include "TileGenerator.h"
#include "util.h"
#include "log.h"
static void usage()
{
static const std::pair<const char*, const char*> options[] = {
{"-i/--input", "<path>"},
{"-o/--output", "<path>"},
{"--bgcolor", "<color>"},
{"--scalecolor", "<color>"},
{"--playercolor", "<color>"},
{"--origincolor", "<color>"},
{"--drawscale", ""},
{"--drawplayers", ""},
{"--draworigin", ""},
{"--drawalpha", ""},
{"--noshading", ""},
{"--noemptyimage", ""},
{"-v/--verbose", ""},
{"--min-y", "<y>"},
{"--max-y", "<y>"},
{"--backend", "<backend>"},
{"--geometry", "x:z+w+h"},
{"--extent", ""},
{"--zoom", "<factor>"},
{"--colors", "<path>"},
{"--scales", "[t][b][l][r]"},
{"--exhaustive", "never|y|full|auto"},
{"--dumpblock", "x,y,z"},
};
const char *top_text =
"minetestmapper -i <world_path> -o <output_image> [options]\n"
"Generate an overview image of a Luanti map.\n"
"\n"
"Options:\n";
const char *bottom_text =
"\n"
"Color format: hexadecimal '#RRGGBB', e.g. '#FF0000' = red\n";
printf("%s", top_text);
for (const auto &p : options)
printf(" %-18s%s\n", p.first, p.second);
printf("%s", bottom_text);
auto backends = TileGenerator::getSupportedBackends();
printf("Supported backends: ");
for (auto s : backends)
printf("%s ", s.c_str());
printf("\n");
#ifdef _WIN32
printf("See also the full documentation in README.rst\n");
#else
printf("See also the full documentation in minetestmapper(6) or README.rst\n");
#endif
}
static inline bool file_exists(const std::string &path)
{
return file_exists(path.c_str());
}
static inline int stoi(const char *s)
{
std::istringstream iss(s);
int ret;
iss >> ret;
return ret;
}
static std::string search_colors(const std::string &worldpath)
{
if (file_exists(worldpath + "/colors.txt"))
return worldpath + "/colors.txt";
#ifndef _WIN32
char *home = std::getenv("HOME");
if (home && home[0]) {
std::string check = std::string(home) + "/.minetest/colors.txt";
if (file_exists(check))
return check;
}
#endif
constexpr bool sharedir_valid = !(SHAREDIR[0] == '.' || !SHAREDIR[0]);
if (sharedir_valid && file_exists(SHAREDIR "/colors.txt"))
return SHAREDIR "/colors.txt";
errorstream << "Warning: Falling back to using colors.txt from current directory." << std::endl;
return "./colors.txt";
}
int main(int argc, char *argv[])
{
const char *short_options = "hi:o:v";
const static struct option long_options[] =
{
{"help", no_argument, 0, 'h'},
{"input", required_argument, 0, 'i'},
{"output", required_argument, 0, 'o'},
{"bgcolor", required_argument, 0, 'b'},
{"scalecolor", required_argument, 0, 's'},
{"origincolor", required_argument, 0, 'r'},
{"playercolor", required_argument, 0, 'p'},
{"draworigin", no_argument, 0, 'R'},
{"drawplayers", no_argument, 0, 'P'},
{"drawscale", no_argument, 0, 'S'},
{"drawalpha", no_argument, 0, 'e'},
{"noshading", no_argument, 0, 'H'},
{"backend", required_argument, 0, 'd'},
{"geometry", required_argument, 0, 'g'},
{"extent", no_argument, 0, 'E'},
{"min-y", required_argument, 0, 'a'},
{"max-y", required_argument, 0, 'c'},
{"zoom", required_argument, 0, 'z'},
{"colors", required_argument, 0, 'C'},
{"scales", required_argument, 0, 'f'},
{"noemptyimage", no_argument, 0, 'n'},
{"exhaustive", required_argument, 0, 'j'},
{"dumpblock", required_argument, 0, 'k'},
{"verbose", no_argument, 0, 'v'},
{0, 0, 0, 0}
};
configure_log_streams(false);
std::string input;
std::string output;
std::string colors;
bool onlyPrintExtent = false;
BlockPos dumpblock(INT16_MIN);
TileGenerator generator;
while (1) {
int option_index;
int c = getopt_long(argc, argv, short_options, long_options, &option_index);
if (c == -1)
break; // done
switch (c) {
case 'h':
usage();
return 0;
case 'i':
input = optarg;
break;
case 'o':
output = optarg;
break;
case 'b':
generator.setBgColor(optarg);
break;
case 's':
generator.setScaleColor(optarg);
break;
case 'r':
generator.setOriginColor(optarg);
break;
case 'p':
generator.setPlayerColor(optarg);
break;
case 'R':
generator.setDrawOrigin(true);
break;
case 'P':
generator.setDrawPlayers(true);
break;
case 'S':
generator.setDrawScale(true);
break;
case 'e':
generator.setDrawAlpha(true);
break;
case 'E':
onlyPrintExtent = true;
break;
case 'H':
generator.setShading(false);
break;
case 'd':
generator.setBackend(optarg);
break;
case 'a':
generator.setMinY(stoi(optarg));
break;
case 'c':
generator.setMaxY(stoi(optarg));
break;
case 'g': {
std::istringstream geometry(optarg);
int x, y, w, h;
char c;
geometry >> x >> c >> y >> w >> h;
if (geometry.fail() || c != ':' || w < 1 || h < 1) {
usage();
return 1;
}
generator.setGeometry(x, y, w, h);
}
break;
case 'f': {
uint flags = 0;
if (strchr(optarg, 't'))
flags |= SCALE_TOP;
if (strchr(optarg, 'b'))
flags |= SCALE_BOTTOM;
if (strchr(optarg, 'l'))
flags |= SCALE_LEFT;
if (strchr(optarg, 'r'))
flags |= SCALE_RIGHT;
generator.setScales(flags);
}
break;
case 'z':
generator.setZoom(stoi(optarg));
break;
case 'C':
colors = optarg;
break;
case 'n':
generator.setDontWriteEmpty(true);
break;
case 'j': {
int mode = EXH_AUTO;
if (!strcmp(optarg, "never"))
mode = EXH_NEVER;
else if (!strcmp(optarg, "y"))
mode = EXH_Y;
else if (!strcmp(optarg, "full"))
mode = EXH_FULL;
generator.setExhaustiveSearch(mode);
}
break;
case 'k': {
std::istringstream iss(optarg);
char c, c2;
iss >> dumpblock.x >> c >> dumpblock.y >> c2 >> dumpblock.z;
if (iss.fail() || c != ',' || c2 != ',') {
usage();
return 1;
}
break;
}
case 'v':
configure_log_streams(true);
break;
default:
return 1;
}
}
const bool need_output = !onlyPrintExtent && dumpblock.x == INT16_MIN;
if (input.empty() || (need_output && output.empty())) {
usage();
return 0;
}
try {
if (onlyPrintExtent) {
generator.printGeometry(input);
return 0;
} else if (dumpblock.x != INT16_MIN) {
generator.dumpBlock(input, dumpblock);
return 0;
}
if(colors.empty())
colors = search_colors(input);
generator.parseColorsFile(colors);
generator.generate(input, output);
} catch (const std::exception &e) {
errorstream << "Exception: " << e.what() << std::endl;
return 1;
}
return 0;
}

View File

@ -1,40 +0,0 @@
#pragma once
#include <string>
// Define custom char traits since std::char_traits<unsigend char> is not part of C++ standard
struct uchar_traits : std::char_traits<char>
{
using super = std::char_traits<char>;
using char_type = unsigned char;
static void assign(char_type& c1, const char_type& c2) noexcept {
c1 = c2;
}
static char_type* assign(char_type* ptr, std::size_t count, char_type c2) {
return reinterpret_cast<char_type*>(
super::assign(reinterpret_cast<char*>(ptr), count, static_cast<char>(c2)));
}
static char_type* move(char_type* dest, const char_type* src, std::size_t count) {
return reinterpret_cast<char_type*>(
super::move(reinterpret_cast<char*>(dest), reinterpret_cast<const char*>(src), count));
}
static char_type* copy(char_type* dest, const char_type* src, std::size_t count) {
return reinterpret_cast<char_type*>(
super::copy(reinterpret_cast<char*>(dest), reinterpret_cast<const char*>(src), count));
}
static int compare(const char_type* s1, const char_type* s2, std::size_t count) {
return super::compare(reinterpret_cast<const char*>(s1), reinterpret_cast<const char*>(s2), count);
}
static char_type to_char_type(int_type c) noexcept {
return static_cast<char_type>(c);
}
};
typedef std::basic_string<unsigned char, uchar_traits> ustring;
typedef unsigned int uint;
typedef unsigned char u8;

View File

@ -1,76 +0,0 @@
#include <stdexcept>
#include <iostream>
#include <sys/types.h>
#include <sys/stat.h>
#include "util.h"
static std::string trim(const std::string &s)
{
auto isspace = [] (char c) {
return c == ' ' || c == '\t' || c == '\r' || c == '\n';
};
size_t front = 0;
while (isspace(s[front]))
++front;
size_t back = s.size() - 1;
while (back > front && isspace(s[back]))
--back;
return s.substr(front, back - front + 1);
}
static bool read_setting(const std::string &name, std::istream &is, std::string &out)
{
char linebuf[512];
is.seekg(0);
while (is.good()) {
is.getline(linebuf, sizeof(linebuf));
std::string line(linebuf);
auto pos = line.find('#');
if (pos != std::string::npos)
line.erase(pos); // remove comments
pos = line.find('=');
if (pos == std::string::npos)
continue;
auto key = trim(line.substr(0, pos));
if (key != name)
continue;
out = trim(line.substr(pos+1));
return true;
}
return false;
}
std::string read_setting(const std::string &name, std::istream &is)
{
std::string ret;
if (!read_setting(name, is, ret))
throw std::runtime_error(std::string("Setting not found: ") + name);
return ret;
}
std::string read_setting_default(const std::string &name, std::istream &is,
const std::string &def)
{
std::string ret;
if (!read_setting(name, is, ret))
return def;
return ret;
}
bool file_exists(const char *path)
{
struct stat s{};
// check for !dir to allow symlinks or such
return stat(path, &s) == 0 && (s.st_mode & S_IFDIR) != S_IFDIR;
}
bool dir_exists(const char *path)
{
struct stat s{};
return stat(path, &s) == 0 && (s.st_mode & S_IFDIR) == S_IFDIR;
}

View File

@ -1,27 +0,0 @@
#pragma once
#include <string>
#include <iostream>
#define ARRLEN(x) (sizeof(x) / sizeof((x)[0]))
template<typename T>
static inline T mymax(T a, T b)
{
return (a > b) ? a : b;
}
template<typename T>
static inline T mymin(T a, T b)
{
return (a > b) ? b : a;
}
std::string read_setting(const std::string &name, std::istream &is);
std::string read_setting_default(const std::string &name, std::istream &is,
const std::string &def);
bool file_exists(const char *path);
bool dir_exists(const char *path);

5
types.h Normal file
View File

@ -0,0 +1,5 @@
#include <string>
typedef std::basic_string<unsigned char> ustring;
typedef unsigned int uint;
typedef unsigned char u8;

56
util.cpp Normal file
View File

@ -0,0 +1,56 @@
#include "util.h"
inline std::string trim(const std::string &s)
{
size_t front = 0;
while(s[front] == ' ' ||
s[front] == '\t' ||
s[front] == '\r' ||
s[front] == '\n'
)
++front;
size_t back = s.size();
while(back > front &&
(s[back-1] == ' ' ||
s[back-1] == '\t' ||
s[back-1] == '\r' ||
s[back-1] == '\n'
)
)
--back;
return s.substr(front, back - front);
}
#define EOFCHECK() \
if(is.eof()) \
throw std::runtime_error(((std::string) "Setting '") + name + "' not found");
std::string get_setting(std::string name, std::istream &is)
{
char c;
char s[256];
std::string nm, value;
next:
while((c = is.get()) == ' ' || c == '\t' || c == '\r' || c == '\n')
;
EOFCHECK();
if(c == '#') // Ignore comments
is.ignore(0xffff, '\n');
EOFCHECK();
s[0] = c; // The current char belongs to the name too
is.get(&s[1], 255, '=');
is.ignore(1); // Jump over the =
EOFCHECK();
nm = trim(std::string(s));
is.get(s, 256, '\n');
value = trim(std::string(s));
if(name == nm)
return value;
else
goto next;
}
#undef EOFCHECK

10
util.h Normal file
View File

@ -0,0 +1,10 @@
#ifndef _UTIL_H
#define _UTIL_H
#include <string>
#include <stdexcept>
#include <fstream>
std::string get_setting(std::string name, std::istream &is);
#endif // _UTIL_H

View File

@ -1,57 +0,0 @@
#!/bin/bash -e
#######
# this expects unpacked libraries and a toolchain file like Luanti's buildbot uses
# $extradlls will typically contain the compiler-specific DLLs and libpng
toolchain_file=
libgd_dir=
zlib_dir=
zstd_dir=
sqlite_dir=
leveldb_dir=
extradlls=(
)
#######
[ -f "$toolchain_file" ] || exit 1
variant=win32
grep -q 'CX?X?_COMPILER.*x86_64-' $toolchain_file && variant=win64
echo "Detected target $variant"
[ -f ./CMakeLists.txt ] || { echo "run from root folder" >&2; exit 1; }
cmake -S . -B build \
-DCMAKE_TOOLCHAIN_FILE="$toolchain_file" \
-DCMAKE_EXE_LINKER_FLAGS="-s" \
\
-DENABLE_LEVELDB=1 \
\
-DLEVELDB_INCLUDE_DIR=$leveldb_dir/include \
-DLEVELDB_LIBRARY=$leveldb_dir/lib/libleveldb.dll.a \
-DLIBGD_INCLUDE_DIR=$libgd_dir/include \
-DLIBGD_LIBRARY=$libgd_dir/lib/libgd.dll.a \
-DSQLITE3_INCLUDE_DIR=$sqlite_dir/include \
-DSQLITE3_LIBRARY=$sqlite_dir/lib/libsqlite3.dll.a \
-DZLIB_INCLUDE_DIR=$zlib_dir/include \
-DZLIB_LIBRARY=$zlib_dir/lib/libz.dll.a \
-DZSTD_INCLUDE_DIR=$zstd_dir/include \
-DZSTD_LIBRARY=$zstd_dir/lib/libzstd.dll.a
make -C build -j4
mkdir pack
cp -p \
AUTHORS colors.txt COPYING README.rst \
build/minetestmapper.exe \
$leveldb_dir/bin/libleveldb.dll \
$libgd_dir/bin/libgd*.dll \
$sqlite_dir/bin/libsqlite*.dll \
$zlib_dir/bin/zlib1.dll \
$zstd_dir/bin/libzstd.dll \
"${extradlls[@]}" \
pack/
zipfile=$PWD/minetestmapper-$variant.zip
(cd pack; zip -9r "$zipfile" *)
rm -rf build pack
echo "Done."

View File

@ -1,29 +0,0 @@
#!/bin/bash -e
install_linux_deps() {
local upkgs=(
cmake libgd-dev libsqlite3-dev libleveldb-dev libpq-dev
libhiredis-dev libzstd-dev
)
local fpkgs=(
cmake gcc-g++ gd-devel sqlite-devel libzstd-devel zlib-ng-devel
)
if command -v dnf; then
sudo dnf install --setopt=install_weak_deps=False -y "${fpkgs[@]}"
else
sudo apt-get update
sudo apt-get install -y --no-install-recommends "${upkgs[@]}"
fi
}
run_build() {
local args=(
-DCMAKE_BUILD_TYPE=Debug
-DENABLE_LEVELDB=ON -DENABLE_POSTGRESQL=ON -DENABLE_REDIS=ON
)
[[ "$CXX" == clang* ]] && args+=(-DCMAKE_CXX_FLAGS="-fsanitize=address")
cmake . "${args[@]}"
make -j2
}

View File

@ -1,116 +0,0 @@
#!/bin/bash
set -eo pipefail
mapdir=./testmap
msg () {
echo
echo "==== $1"
echo
}
# encodes a block position by X, Y, Z (positive numbers only!)
encodepos () {
echo "$(($1 + 0x1000 * $2 + 0x1000000 * $3))"
}
# create map file with sql statements
writemap () {
rm -rf $mapdir
mkdir $mapdir
echo "backend = sqlite3" >$mapdir/world.mt
echo "default:stone 10 10 10" >$mapdir/colors.txt
printf '%s\n' \
"CREATE TABLE d(d BLOB);" \
"INSERT INTO d VALUES (x'$(cat util/ci/test_block)');" \
"$1" \
"DROP TABLE d;" | sqlite3 $mapdir/map.sqlite
}
# check that a non-empty ($1=1) or empty map ($1=0) was written with the args ($2 ...)
checkmap () {
local c=$1
shift
rm -f map.png
./minetestmapper --noemptyimage -v -i ./testmap -o map.png "$@"
if [[ $c -eq 1 && ! -f map.png ]]; then
echo "Output not generated!"
exit 1
elif [[ $c -eq 0 && -f map.png ]]; then
echo "Output was generated, none expected!"
exit 1
fi
echo "Passed."
}
# this is missing the indices and primary keys but that doesn't matter
schema_old="CREATE TABLE blocks(pos INT, data BLOB);"
schema_new="CREATE TABLE blocks(x INT, y INT, z INT, data BLOB);"
msg "old schema"
writemap "
$schema_old
INSERT INTO blocks SELECT $(encodepos 0 1 0), d FROM d;
"
checkmap 1
msg "old schema: Y limit"
# Note: test data contains a plane at y = 17 an a single node at y = 18
checkmap 1 --max-y 17
checkmap 0 --max-y 16
checkmap 1 --min-y 18
checkmap 0 --min-y 19
# do this for every strategy
for exh in never y full; do
msg "old schema: all limits ($exh)"
# fill the map with more blocks and then request just a single one to be rendered
# this will run through internal consistency asserts.
writemap "
$schema_old
INSERT INTO blocks SELECT $(encodepos 2 2 2), d FROM d;
INSERT INTO blocks SELECT $(encodepos 1 2 2), d FROM d;
INSERT INTO blocks SELECT $(encodepos 2 1 2), d FROM d;
INSERT INTO blocks SELECT $(encodepos 2 2 1), d FROM d;
INSERT INTO blocks SELECT $(encodepos 3 2 2), d FROM d;
INSERT INTO blocks SELECT $(encodepos 2 3 2), d FROM d;
INSERT INTO blocks SELECT $(encodepos 2 2 3), d FROM d;
"
checkmap 1 --geometry 32:32+16+16 --min-y 32 --max-y $((32+16-1)) --exhaustive $exh
done
msg "new schema"
writemap "
$schema_new
INSERT INTO blocks SELECT 0, 1, 0, d FROM d;
"
checkmap 1
# same as above
for exh in never y full; do
msg "new schema: all limits ($exh)"
writemap "
$schema_new
INSERT INTO blocks SELECT 2, 2, 2, d FROM d;
INSERT INTO blocks SELECT 1, 2, 2, d FROM d;
INSERT INTO blocks SELECT 2, 1, 2, d FROM d;
INSERT INTO blocks SELECT 2, 2, 1, d FROM d;
INSERT INTO blocks SELECT 3, 2, 2, d FROM d;
INSERT INTO blocks SELECT 2, 3, 2, d FROM d;
INSERT INTO blocks SELECT 2, 2, 3, d FROM d;
"
checkmap 1 --geometry 32:32+16+16 --min-y 32 --max-y $((32+16-1)) --exhaustive $exh
done
msg "new schema: empty map"
writemap "$schema_new"
checkmap 0
msg "drawplayers"
writemap "
$schema_new
INSERT INTO blocks SELECT 0, 0, 0, d FROM d;
"
mkdir $mapdir/players
printf '%s\n' "name = cat" "position = (80,0,80)" >$mapdir/players/cat
# we can't check that it actually worked, however
checkmap 1 --drawplayers --zoom 4

View File

@ -1 +0,0 @@
1b00ffff020278daedd4c1090000080331dd7f691710faf12589235cb12ae870fca6bffefaebafbffefaebafbffefaebbff7b708fdf1ffd11ffdd11ffdd11ffd01000000000000003836d59f010578da63000000010001000000ffffffff000002000000036169720001000d64656661756c743a73746f6e650a0000

View File

@ -1,73 +0,0 @@
local function get_tile(tiles, n)
local tile = tiles[n]
if type(tile) == 'table' then
return tile.name or tile.image
elseif type(tile) == 'string' then
return tile
end
end
local function strip_texture(tex)
tex = (tex .. '^'):match('%(*(.-)%)*^') -- strip modifiers
if tex:find("[combine", 1, true) then
tex = tex:match('.-=([^:]-)') -- extract first texture
elseif tex:find("[png", 1, true) then
return nil -- can't
end
return tex
end
local function pairs_s(dict)
local keys = {}
for k in pairs(dict) do
keys[#keys+1] = k
end
table.sort(keys)
return ipairs(keys)
end
core.register_chatcommand("dumpnodes", {
description = "Dump node and texture list for use with minetestmapper",
func = function()
local ntbl = {}
for _, nn in pairs_s(minetest.registered_nodes) do
local prefix, name = nn:match('(.-):(.*)')
if prefix == nil or name == nil then
print("ignored(1): " .. nn)
else
if ntbl[prefix] == nil then
ntbl[prefix] = {}
end
ntbl[prefix][name] = true
end
end
local out, err = io.open(core.get_worldpath() .. "/nodes.txt", 'wb')
if not out then
return true, err
end
local n = 0
for _, prefix in pairs_s(ntbl) do
out:write('# ' .. prefix .. '\n')
for _, name in pairs_s(ntbl[prefix]) do
local nn = prefix .. ":" .. name
local nd = core.registered_nodes[nn]
local tiles = nd.tiles or nd.tile_images
if tiles == nil or nd.drawtype == 'airlike' then
print("ignored(2): " .. nn)
else
local tex = get_tile(tiles, 1)
tex = tex and strip_texture(tex)
if not tex then
print("ignored(3): " .. nn)
else
out:write(nn .. ' ' .. tex .. '\n')
n = n + 1
end
end
end
out:write('\n')
end
out:close()
return true, n .. " nodes dumped."
end,
})

View File

@ -1,2 +0,0 @@
name = dumpnodes
description = minetestmapper development mod (node dumper)

View File

@ -1,183 +0,0 @@
#!/usr/bin/env python3
import sys
import os.path
import getopt
import re
from math import sqrt
try:
from PIL import Image
except:
print("Could not load image routines, install PIL ('pillow' on pypi)!", file=sys.stderr)
exit(1)
############
############
# Instructions for generating a colors.txt file for custom games and/or mods:
# 1) Add the dumpnodes mod to a Luanti world with the chosen game and mods enabled.
# 2) Join ingame and run the /dumpnodes chat command.
# 3) Run this script and poin it to the installation path of the game using -g,
# the path(s) where mods are stored using -m and the nodes.txt in your world folder.
# Example command line:
# ./util/generate_colorstxt.py --game /usr/share/luanti/games/minetest_game \
# -m ~/.minetest/mods ~/.minetest/worlds/my_world/nodes.txt
# 4) Copy the resulting colors.txt file to your world folder or to any other place
# and use it with minetestmapper's --colors option.
###########
###########
# minimal sed syntax, s|match|replace| and /match/d supported
REPLACEMENTS = [
# Delete some nodes that are usually hidden
r'/^fireflies:firefly /d',
r'/^butterflies:butterfly_/d',
# Nicer colors for water and lava
r's/^(default:(river_)?water_(flowing|source)) [0-9 ]+$/\1 39 66 106 128 224/',
r's/^(default:lava_(flowing|source)) [0-9 ]+$/\1 255 100 0/',
# Transparency for glass nodes and panes
r's/^(default:.*glass) ([0-9 ]+)$/\1 \2 64 16/',
r's/^(doors:.*glass[^ ]*) ([0-9 ]+)$/\1 \2 64 16/',
r's/^(xpanes:.*(pane|bar)[^ ]*) ([0-9 ]+)$/\1 \3 64 16/',
]
def usage():
print("Usage: generate_colorstxt.py [options] [input file] [output file]")
print("If not specified the input file defaults to ./nodes.txt and the output file to ./colors.txt")
print(" -g / --game <folder>\t\tSet path to the game (for textures), required")
print(" -m / --mods <folder>\t\tAdd search path for mod textures")
print(" --replace <file>\t\tLoad replacements from file (ADVANCED)")
def collect_files(path):
dirs = []
with os.scandir(path) as it:
for entry in it:
if entry.name[0] == '.': continue
if entry.is_dir():
dirs.append(entry.path)
continue
if entry.is_file() and '.' in entry.name:
if entry.name not in textures.keys():
textures[entry.name] = entry.path
for path2 in dirs:
collect_files(path2)
def average_color(filename):
inp = Image.open(filename).convert('RGBA')
data = inp.load()
c0, c1, c2 = [], [], []
for x in range(inp.size[0]):
for y in range(inp.size[1]):
px = data[x, y]
if px[3] < 128: continue # alpha
c0.append(px[0]**2)
c1.append(px[1]**2)
c2.append(px[2]**2)
if len(c0) == 0:
print(f"didn't find color for '{os.path.basename(filename)}'", file=sys.stderr)
return "0 0 0"
c0 = sqrt(sum(c0) / len(c0))
c1 = sqrt(sum(c1) / len(c1))
c2 = sqrt(sum(c2) / len(c2))
return "%d %d %d" % (c0, c1, c2)
def apply_sed(line, exprs):
for expr in exprs:
if expr[0] == '/':
if not expr.endswith("/d"): raise ValueError()
if re.search(expr[1:-2], line):
return ''
elif expr[0] == 's':
expr = expr.split(expr[1])
if len(expr) != 4 or expr[3] != '': raise ValueError()
line = re.sub(expr[1], expr[2], line)
else:
raise ValueError()
return line
#
try:
opts, args = getopt.getopt(sys.argv[1:], "hg:m:", ["help", "game=", "mods=", "replace="])
except getopt.GetoptError as e:
print(str(e))
exit(1)
if ('-h', '') in opts or ('--help', '') in opts:
usage()
exit(0)
input_file = "./nodes.txt"
output_file = "./colors.txt"
texturepaths = []
try:
gamepath = next(o[1] for o in opts if o[0] in ('-g', '--game'))
if not os.path.isdir(os.path.join(gamepath, "mods")):
print(f"'{gamepath}' doesn't exist or does not contain a game.", file=sys.stderr)
exit(1)
texturepaths.append(os.path.join(gamepath, "mods"))
except StopIteration:
print("No game path set but one is required. (see --help)", file=sys.stderr)
exit(1)
try:
tmp = next(o[1] for o in opts if o[0] == "--replace")
REPLACEMENTS.clear()
with open(tmp, 'r') as f:
for line in f:
if not line or line[0] == '#': continue
REPLACEMENTS.append(line.strip())
except StopIteration:
pass
for o in opts:
if o[0] not in ('-m', '--mods'): continue
if not os.path.isdir(o[1]):
print(f"Given path '{o[1]}' does not exist.'", file=sys.stderr)
exit(1)
texturepaths.append(o[1])
if len(args) > 2:
print("Too many arguments.", file=sys.stderr)
exit(1)
if len(args) > 1:
output_file = args[1]
if len(args) > 0:
input_file = args[0]
if not os.path.exists(input_file) or os.path.isdir(input_file):
print(f"Input file '{input_file}' does not exist.", file=sys.stderr)
exit(1)
#
print(f"Collecting textures from {len(texturepaths)} path(s)... ", end="", flush=True)
textures = {}
for path in texturepaths:
collect_files(path)
print("done")
print("Processing nodes...")
fin = open(input_file, 'r')
fout = open(output_file, 'w')
n = 0
for line in fin:
line = line.rstrip('\r\n')
if not line or line[0] == '#':
fout.write(line + '\n')
continue
node, tex = line.split(" ")
if not tex or tex == "blank.png":
continue
elif tex not in textures.keys():
print(f"skip {node} texture not found")
continue
color = average_color(textures[tex])
line = f"{node} {color}"
#print(f"ok {node}")
line = apply_sed(line, REPLACEMENTS)
if line:
fout.write(line + '\n')
n += 1
fin.close()
fout.close()
print(f"Done, {n} entries written.")

17
util/travis/script.sh Executable file
View File

@ -0,0 +1,17 @@
#!/bin/bash -e
CXX=g++-6
[ $CC == "clang" ] && CXX=clang-3.8
export CXX
mkdir -p travisbuild
cd travisbuild
cmake \
-DENABLE_LEVELDB=1 \
-DLEVELDB_LIBRARY=../libleveldb/lib/libleveldb.so \
-DLEVELDB_INCLUDE_DIR=../libleveldb/include \
..
make -j2