1 Commits

Author SHA1 Message Date
55e407c130 night rendering 2020-05-08 18:50:00 +02:00
64 changed files with 1523 additions and 2525 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 @@
*~
/minetestmapper
/minetestmapper.exe
/colors.txt
colors.txt
minetestmapper
minetestmapper.exe
CMakeCache.txt
CMakeFiles/
CPack*.cmake
_CPack_Packages/
install_manifest.txt
CPack*
Makefile
cmake_install.cmake
cmake_config.h
compile_commands.json
.vscode/
*~

19
.travis.yml Normal file
View File

@ -0,0 +1,19 @@
language: cpp
compiler:
- gcc
- clang
dist: bionic
addons:
apt:
packages:
- cmake
- libgd-dev
- libsqlite3-dev
- libleveldb-dev
- libpq-dev
- postgresql-server-dev-all
script: ./util/travis/script.sh
notifications:
email: false
matrix:
fast_finish: true

View File

@ -1,18 +1,17 @@
#include <stdint.h>
#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)
static int readBlockContent(const unsigned char *mapData, u8 contentWidth, unsigned int datapos)
{
if (contentWidth == 2) {
size_t index = datapos << 1;
@ -22,7 +21,7 @@ static inline uint16_t readBlockContent(const unsigned char *mapData,
if (param <= 0x7f)
return param;
else
return (param << 4) | (mapData[datapos + 0x2000] >> 4);
return (int(param) << 4) | (int(mapData[datapos + 0x2000]) >> 4);
}
}
@ -39,7 +38,7 @@ void BlockDecoder::reset()
m_version = 0;
m_contentWidth = 0;
m_mapData.clear();
m_mapData = ustring();
}
void BlockDecoder::decode(const ustring &datastr)
@ -49,6 +48,7 @@ void BlockDecoder::decode(const ustring &datastr)
// TODO: bounds checks
uint8_t version = data[0];
//uint8_t flags = data[1];
if (version < 22) {
std::ostringstream oss;
oss << "Unsupported map version " << (int)version;
@ -56,45 +56,12 @@ void BlockDecoder::decode(const ustring &datastr)
}
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)
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];
@ -105,20 +72,14 @@ void BlockDecoder::decode(const ustring &datastr)
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
m_mapData = decompressor.decompress();
decompressor.decompress(); // unused metadata
dataOffset = decompressor.seekPos();
// Skip unused node timers
// Skip unused data
if (version == 23)
dataOffset += 1;
if (version == 24) {
@ -142,7 +103,33 @@ void BlockDecoder::decode(const ustring &datastr)
dataOffset += 4; // Skip timestamp
// Read mapping
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;
}
}
// Node timers
if (version >= 25) {
dataOffset++;
uint16_t numTimers = readU16(data + dataOffset);
dataOffset += 2;
dataOffset += numTimers * 10;
}
}
bool BlockDecoder::isEmpty() const
@ -151,18 +138,23 @@ bool BlockDecoder::isEmpty() const
return m_nameMap.empty();
}
const static std::string empty;
const std::string &BlockDecoder::getNode(u8 x, u8 y, u8 z) 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);
int content = readBlockContent(m_mapData.c_str(), m_contentWidth, position);
if (content == m_blockAirId || content == m_blockIgnoreId)
return empty;
return "";
NameMap::const_iterator it = m_nameMap.find(content);
if (it == m_nameMap.end()) {
errorstream << "Skipping node with invalid ID." << std::endl;
return empty;
std::cerr << "Skipping node with invalid ID." << std::endl;
return "";
}
return it->second;
}
u8 BlockDecoder::getParam1(u8 x, u8 y, u8 z) const
{
unsigned int position = x + (y << 4) + (z << 8);
unsigned int offset = (m_contentWidth == 2) ? 0x2000 : 0x1000;
return m_mapData.c_str()[offset + position];
}

View File

@ -1,9 +1,10 @@
cmake_minimum_required(VERSION 3.5)
project(minetestmapper CXX)
cmake_minimum_required(VERSION 2.6)
cmake_policy(SET CMP0003 NEW)
project(minetestmapper
VERSION 1.0
LANGUAGES CXX
)
set(VERSION_MAJOR 1)
set(VERSION_MINOR 0)
set(VERSION_STRING "${VERSION_MAJOR}.${VERSION_MINOR}")
# Stuff & Paths
@ -12,17 +13,18 @@ if(NOT CMAKE_BUILD_TYPE)
endif()
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -Wall -DNDEBUG")
set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g2 -Wall")
if(WIN32)
set(SHAREDIR ".")
set(BINDIR ".")
set(DOCDIR ".")
else()
set(SHAREDIR "share/luanti") # reuse engine share dir
set(BINDIR "bin")
set(DOCDIR "share/doc/${PROJECT_NAME}")
set(MANDIR "share/man")
set(SHAREDIR "${CMAKE_INSTALL_PREFIX}/share/minetest") # reuse Minetest share dir
set(BINDIR "${CMAKE_INSTALL_PREFIX}/bin")
set(DOCDIR "${CMAKE_INSTALL_PREFIX}/share/doc/${PROJECT_NAME}")
set(MANDIR "${CMAKE_INSTALL_PREFIX}/share/man")
endif()
set(CUSTOM_SHAREDIR "" CACHE STRING "Directory to install data files into")
@ -43,7 +45,9 @@ if(NOT CUSTOM_DOCDIR STREQUAL "")
message(STATUS "Using DOCDIR=${DOCDIR}")
endif()
list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake")
#set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
find_package(PkgConfig)
include(FindPackageHandleStandardArgs)
# Libraries: gd
@ -57,26 +61,18 @@ endif(NOT LIBGD_LIBRARY OR NOT LIBGD_INCLUDE_DIR)
# Libraries: zlib
find_package(zlib-ng QUIET)
if(zlib-ng_FOUND)
set(ZLIB_INCLUDE_DIR zlib-ng::zlib)
set(ZLIB_LIBRARY zlib-ng::zlib)
set(USE_ZLIB_NG TRUE)
message(STATUS "Found zlib-ng, using it instead of zlib.")
else()
message(STATUS "zlib-ng not found, falling back to zlib.")
find_package(ZLIB REQUIRED)
set(USE_ZLIB_NG FALSE)
endif()
# Libraries: zstd
find_package(Zstd REQUIRED)
find_library(ZLIB_LIBRARY z)
find_path(ZLIB_INCLUDE_DIR zlib.h)
message (STATUS "zlib library: ${ZLIB_LIBRARY}")
message (STATUS "zlib headers: ${ZLIB_INCLUDE_DIR}")
if(NOT ZLIB_LIBRARY OR NOT ZLIB_INCLUDE_DIR)
message(FATAL_ERROR "zlib not found!")
endif(NOT ZLIB_LIBRARY OR NOT ZLIB_INCLUDE_DIR)
# Libraries: 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 headers: ${SQLITE3_INCLUDE_DIR}")
if(NOT SQLITE3_LIBRARY OR NOT SQLITE3_INCLUDE_DIR)
@ -89,18 +85,7 @@ option(ENABLE_POSTGRESQL "Enable PostgreSQL backend" TRUE)
set(USE_POSTGRESQL FALSE)
if(ENABLE_POSTGRESQL)
if(CMAKE_VERSION VERSION_LESS "3.20")
find_package(PostgreSQL QUIET)
# Before CMake 3.20 FindPostgreSQL.cmake always looked for server includes
# but we don't need them, so continue anyway if only those are missing.
if(PostgreSQL_INCLUDE_DIR AND PostgreSQL_LIBRARY)
set(PostgreSQL_FOUND TRUE)
set(PostgreSQL_INCLUDE_DIRS ${PostgreSQL_INCLUDE_DIR})
set(PostgreSQL_LIBRARIES ${PostgreSQL_LIBRARY})
endif()
else()
find_package(PostgreSQL)
endif()
find_package("PostgreSQL")
if(PostgreSQL_FOUND)
set(USE_POSTGRESQL TRUE)
@ -156,63 +141,57 @@ endif(ENABLE_REDIS)
# Compiling & Linking
configure_file(
"${CMAKE_CURRENT_SOURCE_DIR}/src/cmake_config.h.in"
"${CMAKE_CURRENT_BINARY_DIR}/cmake_config.h"
)
if(CMAKE_CXX_COMPILER_ID MATCHES "^(GNU|Clang|AppleClang)$")
set(CMAKE_CXX_FLAGS_RELEASE "-O2")
set(CMAKE_CXX_FLAGS_DEBUG "-Og -g2")
add_compile_options(-Wall -pipe)
elseif(MSVC)
add_compile_options(/GR- /Zl)
endif()
if(CMAKE_BUILD_TYPE STREQUAL "Release")
add_definitions(-DNDEBUG)
endif()
add_executable(minetestmapper)
target_include_directories(minetestmapper PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}"
"${CMAKE_CURRENT_BINARY_DIR}"
)
target_sources(minetestmapper PRIVATE
src/BlockDecoder.cpp
src/PixelAttributes.cpp
src/PlayerAttributes.cpp
src/TileGenerator.cpp
src/ZlibDecompressor.cpp
src/ZstdDecompressor.cpp
src/Image.cpp
src/mapper.cpp
src/util.cpp
src/log.cpp
src/db-sqlite3.cpp
$<$<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"
include_directories(
"${PROJECT_BINARY_DIR}"
"${CMAKE_CURRENT_SOURCE_DIR}/include"
"${CMAKE_CURRENT_BINARY_DIR}"
${SQLITE3_INCLUDE_DIR}
${LIBGD_INCLUDE_DIR}
${ZLIB_INCLUDE_DIR}
${ZSTD_INCLUDE_DIR}
)
target_link_libraries(minetestmapper
configure_file(
"${PROJECT_SOURCE_DIR}/include/cmake_config.h.in"
"${PROJECT_BINARY_DIR}/cmake_config.h"
)
add_definitions ( -DUSE_CMAKE_CONFIG_H )
set(mapper_SRCS
BlockDecoder.cpp
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}
${PostgreSQL_LIBRARIES}
${LEVELDB_LIBRARY}
${REDIS_LIBRARY}
${LIBGD_LIBRARY}
${ZLIB_LIBRARY}
${ZSTD_LIBRARY}
)
# Installing & Packaging
@ -226,15 +205,17 @@ if(UNIX)
install(FILES "minetestmapper.6" DESTINATION "${MANDIR}/man6")
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_CONTACT "Perttu Ahola <celeron55@gmail.com>")
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)
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_SOURCE_GENERATOR TGZ)
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

@ -17,7 +17,7 @@
// 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;
return (a << 24) | (c.r << 16) | (c.g << 8) | c.b;
@ -26,15 +26,15 @@ static inline int color2int(const Color &c)
static inline Color int2color(int c)
{
Color c2;
u8 a;
c2.b = c & 0xff;
c2.g = (c >> 8) & 0xff;
c2.r = (c >> 16) & 0xff;
u8 a = (c >> 24) & 0xff;
a = (c >> 24) & 0xff;
c2.a = 255 - (a*255 / gdAlphaMax);
return c2;
}
#ifndef NDEBUG
static inline void check_bounds(int x, int y, int width, int height)
{
if(x < 0 || x >= width) {
@ -50,13 +50,11 @@ static inline void check_bounds(int x, int y, int width, int height)
throw std::out_of_range(oss.str());
}
}
#endif
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);
}

View File

@ -1,6 +1,14 @@
#include <cstring>
/*
* =====================================================================
* Version: 1.0
* Created: 25.08.2012 10:55:27
* Author: Miroslav Bendík
* Company: LinuxOS.sk
* =====================================================================
*/
#include "PixelAttributes.h"
#include <cstring>
PixelAttributes::PixelAttributes():
m_width(0)

133
PlayerAttributes.cpp Normal file
View File

@ -0,0 +1,133 @@
#include <fstream>
#include <sstream>
#include <stdexcept>
#include <dirent.h>
#include <unistd.h> // for usleep
#include <sqlite3.h>
#include "config.h"
#include "PlayerAttributes.h"
#include "util.h"
using namespace std;
PlayerAttributes::PlayerAttributes(const std::string &worldDir)
{
std::ifstream ifs((worldDir + "world.mt").c_str());
if (!ifs.good())
throw std::runtime_error("Failed to read world.mt");
std::string backend = read_setting_default("player_backend", ifs, "files");
ifs.close();
if (backend == "files")
readFiles(worldDir + "players");
else if (backend == "sqlite3")
readSqlite(worldDir + "players.sqlite");
else
throw std::runtime_error(((std::string) "Unknown player backend: ") + backend);
}
void PlayerAttributes::readFiles(const std::string &playersPath)
{
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(path.c_str());
if(!in.good())
continue;
string name, position;
name = read_setting("name", in);
in.seekg(0);
position = read_setting("position", in);
Player player;
istringstream iss(position);
char tmp;
iss >> tmp; // '('
iss >> player.x;
iss >> tmp; // ','
iss >> player.y;
iss >> tmp; // ','
iss >> player.z;
iss >> tmp; // ')'
if(tmp != ')')
continue;
player.name = name;
player.x /= 10.0;
player.y /= 10.0;
player.z /= 10.0;
m_players.push_back(player);
}
closedir(dir);
}
/**********/
#define SQLRES(f, good) \
result = (sqlite3_##f); \
if (result != good) { \
throw std::runtime_error(sqlite3_errmsg(db));\
}
#define SQLOK(f) SQLRES(f, SQLITE_OK)
void PlayerAttributes::readSqlite(const std::string &db_name)
{
int result;
sqlite3 *db;
sqlite3_stmt *stmt_get_player_pos;
SQLOK(open_v2(db_name.c_str(), &db, SQLITE_OPEN_READONLY |
SQLITE_OPEN_PRIVATECACHE, 0))
SQLOK(prepare_v2(db,
"SELECT name, posX, posY, posZ FROM player",
-1, &stmt_get_player_pos, NULL))
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;
const unsigned char *name_ = sqlite3_column_text(stmt_get_player_pos, 0);
player.name = std::string(reinterpret_cast<const char*>(name_));
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.0;
player.y /= 10.0;
player.z /= 10.0;
m_players.push_back(player);
}
sqlite3_finalize(stmt_get_player_pos);
sqlite3_close(db);
}
/**********/
PlayerAttributes::Players::iterator PlayerAttributes::begin()
{
return m_players.begin();
}
PlayerAttributes::Players::iterator PlayerAttributes::end()
{
return m_players.end();
}

View File

@ -1,50 +1,32 @@
Minetest Mapper C++
===================
.. image:: https://github.com/minetest/minetestmapper/workflows/build/badge.svg
:target: https://github.com/minetest/minetestmapper/actions/workflows/build.yml
.. image:: https://travis-ci.org/minetest/minetestmapper.svg?branch=master
: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 `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.
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.
Requirements
------------
* C++ compiler, zlib, zstd
* libgd
* sqlite3
* LevelDB (optional)
* hiredis (optional)
* Postgres libraries (optional)
* LevelDB (optional, set ENABLE_LEVELDB=1 in CMake to enable)
* hiredis library (optional, set ENABLE_REDIS=1 in CMake to enable)
* 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``
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
After extracting the archive, minetestmapper can be invoked from cmd.exe:
::
cd C:\Users\yourname\Desktop\example\path
minetestmapper.exe --help
@ -52,16 +34,16 @@ After extracting the archive, it can be invoked from cmd.exe or PowerShell:
Compilation
-----------
.. code-block:: bash
::
cmake . -DENABLE_LEVELDB=1
make -j$(nproc)
make -j2
Usage
-----
``minetestmapper`` has two mandatory paremeters, ``-i`` (input world path)
and ``-o`` (output image path).
`minetestmapper` has two mandatory paremeters, `-i` (input world path)
and `-o` (output image path).
::
@ -93,7 +75,7 @@ draworigin:
Draw origin indicator, ``--draworigin``
drawalpha:
Allow nodes to be drawn with transparency (such as water), ``--drawalpha``
Allow nodes to be drawn with transparency (e.g. water), ``--drawalpha``
extent:
Don't output any imagery, just print the extent of the full map, ``--extent``
@ -104,14 +86,11 @@ noshading:
noemptyimage:
Don't output anything when the image would be empty, ``--noemptyimage``
verbose:
Enable verbose log putput, ``--verbose``
min-y:
Don't draw nodes below this Y value, e.g. ``--min-y -25``
Don't draw nodes below this y value, e.g. ``--min-y -25``
max-y:
Don't draw nodes above this Y value, e.g. ``--max-y 75``
Don't draw nodes above this y value, e.g. ``--max-y 75``
backend:
Override auto-detected map backend; supported: *sqlite3*, *leveldb*, *redis*, *postgresql*, e.g. ``--backend leveldb``
@ -119,21 +98,16 @@ backend:
geometry:
Limit area to specific geometry (*x:z+w+h* where x and z specify the lower left corner), e.g. ``--geometry -800:-800+1600+1600``
The coordinates are specified with the same axes as in-game. The Z axis becomes Y when projected on the image.
zoom:
Zoom the image by using more than one pixel per node, e.g. ``--zoom 4``
Apply zoom to drawn nodes by enlarging them to n*n squares, e.g. ``--zoom 4``
colors:
Override auto-detected path to colors.txt, e.g. ``--colors ../world/mycolors.txt``
Override auto-detected path to colors.txt, e.g. ``--colors ../minetest/mycolors.txt``
scales:
Draw scales on specified image edges (letters *t b l r* meaning top, bottom, left and right), e.g. ``--scales tbr``
exhaustive:
Select if database should be traversed exhaustively or using range queries, available: *never*, *y*, *full*, *auto*
Defaults to *auto*. You shouldn't need to change this, as minetestmapper tries to automatically picks the best option.
dumpblock:
Instead of rendering anything try to load the block at the given position (*x,y,z*) and print its raw data as hexadecimal.
| 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, but doing so can improve rendering times on large maps.
| For these optimizations to work it is important that you set ``min-y`` and ``max-y`` when you don't care about the world below e.g. -60 and above 1000 nodes.

View File

@ -8,17 +8,13 @@
#include <stdexcept>
#include <cstring>
#include <vector>
#include <type_traits>
#include <limits>
#include <cmath>
#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"
@ -30,45 +26,18 @@
#include "db-redis.h"
#endif
#ifndef __has_builtin
#define __has_builtin(x) 0
#endif
using namespace std;
// saturating multiplication
template<typename T, class = typename std::enable_if<std::is_unsigned<T>::value>::type>
inline T sat_mul(T a, T b)
template<typename T>
static inline T mymax(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
return (a > b) ? a : b;
}
template<typename T>
inline T sat_mul(T a, T b, T c)
static inline T mymin(T a, T b)
{
return sat_mul(sat_mul(a, b), c);
return (a > b) ? b : a;
}
// rounds n (away from 0) to a multiple of f while preserving the sign of n
@ -83,30 +52,16 @@ static int round_multiple_nosign(int n, int f)
return sign * (abs_n + f - (abs_n % f));
}
static inline unsigned int colorSafeBounds(int channel)
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;
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);
@ -143,19 +98,14 @@ TileGenerator::TileGenerator():
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)
m_scales(SCALE_LEFT | SCALE_TOP)
{
}
TileGenerator::~TileGenerator()
{
closeDatabase();
delete m_image;
m_image = nullptr;
}
void TileGenerator::setBgColor(const std::string &bgColor)
@ -190,6 +140,21 @@ 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;
@ -248,22 +213,22 @@ 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())
ifstream in;
in.open(fileName.c_str(), ifstream::in);
if (!in.is_open())
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)
void TileGenerator::printGeometry(const std::string &input)
{
string input_path = input;
if (input_path[input.length() - 1] != PATH_SEPARATOR) {
input_path += PATH_SEPARATOR;
}
setExhaustiveSearch(EXH_NEVER);
openDb(input_path);
loadBlocks();
@ -275,48 +240,34 @@ void TileGenerator::printGeometry(const std::string &input_path)
<< std::endl;
closeDatabase();
}
void TileGenerator::dumpBlock(const std::string &input_path, BlockPos pos)
void TileGenerator::setDontWriteEmpty(bool f)
{
openDb(input_path);
m_dontWriteEmpty = f;
}
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");
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;
}
closeDatabase();
}
void TileGenerator::generate(const std::string &input_path, const std::string &output)
{
if (m_dontWriteEmpty) // FIXME: possible too, just needs to be done differently
setExhaustiveSearch(EXH_NEVER);
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;
if (m_dontWriteEmpty && m_positions.empty())
{
closeDatabase();
return;
}
createImage();
renderMap();
if (m_dontWriteEmpty && !m_renderedAny) {
verbosestream << "Result is empty (no pixels)" << std::endl;
printUnknown();
return;
}
closeDatabase();
if (m_drawScale) {
renderScale();
@ -337,24 +288,27 @@ void TileGenerator::parseColorsStream(std::istream &in)
while (in.good()) {
in.getline(line, sizeof(line));
for (char *p = line; *p; p++) {
if (*p != '#')
for(char *p = line; *p; p++) {
if(*p != '#')
continue;
*p = '\0'; // Cut off at the first #
break;
}
if(!line[0])
if(strlen(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;
char name[128 + 1] = {0};
unsigned int r, g, b, a, t;
a = 255;
t = 0;
int items = sscanf(line, "%128s %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;
}
m_colorMap[name] = ColorEntry(r, g, b, a, t);
ColorEntry color(r, g, b, a, t);
m_colorMap[name] = color;
}
}
@ -374,61 +328,44 @@ std::set<std::string> TileGenerator::getSupportedBackends()
return r;
}
void TileGenerator::openDb(const std::string &input_path)
void TileGenerator::openDb(const std::string &input)
{
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()) {
if (backend == "") {
std::ifstream ifs(input + "/world.mt");
if(!ifs.good())
throw std::runtime_error("Failed to open world.mt");
backend = read_setting_default("backend", ifs, "sqlite3");
ifs.close();
}
if (backend == "dummy") {
throw std::runtime_error("This map uses the dummy backend and contains no data");
} else if (backend == "sqlite3") {
if(backend == "sqlite3")
m_db = new DBSQLite3(input);
#if USE_POSTGRESQL
} else if (backend == "postgresql") {
else if(backend == "postgresql")
m_db = new DBPostgreSQL(input);
#endif
#if USE_LEVELDB
} else if (backend == "leveldb") {
else if(backend == "leveldb")
m_db = new DBLevelDB(input);
#endif
#if USE_REDIS
} else if (backend == "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;
}
else
throw std::runtime_error(((std::string) "Unknown map backend: ") + backend);
// 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:"
using u64 = uint64_t;
u64 y_range = (m_yMax / 16 + 1) - (m_yMin / 16);
u64 blocks = (u64)(m_geomX2 - m_geomX) * y_range * (u64)(m_geomY2 - m_geomY);
#ifndef NDEBUG
std::cout << "Heuristic parameters:"
<< " preferRangeQueries()=" << m_db->preferRangeQueries()
<< " y_range=" << y_range << " blocks=" << blocks << std::endl;
#endif
if (m_db->preferRangeQueries())
m_exhaustiveSearch = EXH_NEVER;
else if (blocks < 200000)
@ -439,9 +376,9 @@ void TileGenerator::openDb(const std::string &input_path)
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;
std::cerr << "Note: The current database backend supports efficient "
"range queries, forcing exhaustive search should always result "
" in worse performance." << std::endl;
}
}
assert(m_exhaustiveSearch != EXH_AUTO);
@ -453,43 +390,42 @@ void TileGenerator::closeDatabase()
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);
const int16_t yMax = m_yMax / 16 + 1;
if (m_exhaustiveSearch == EXH_NEVER || m_exhaustiveSearch == EXH_Y) {
std::vector<BlockPos> vec = m_db->getBlockPosXZ(
BlockPos(m_geomX, yMin, m_geomY),
std::vector<BlockPos> vec = m_db->getBlockPos(
BlockPos(m_geomX, m_yMin / 16, m_geomY),
BlockPos(m_geomX2, yMax, m_geomY2)
);
for (auto pos : vec) {
assert(pos.x >= m_geomX && pos.x < m_geomX2);
assert(pos.y >= m_yMin / 16 && pos.y < yMax);
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);
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[pos.z].emplace(pos.x);
}
size_t count = 0;
#ifndef NDEBUG
int count = 0;
for (const auto &it : m_positions)
count += it.second.size();
m_progressMax = count;
verbosestream << "Loaded " << count
std::cout << "Loaded " << count
<< " positions (across Z: " << m_positions.size() << ") for rendering" << std::endl;
#endif
}
}
@ -499,6 +435,7 @@ void TileGenerator::createImage()
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.
@ -528,11 +465,8 @@ void TileGenerator::createImage()
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::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);
@ -542,9 +476,7 @@ void TileGenerator::createImage()
void TileGenerator::renderMap()
{
BlockDecoder blk;
const int16_t yMax = mod16(m_yMax) + 1;
const int16_t yMin = mod16(m_yMin);
size_t count = 0;
const int16_t yMax = m_yMax / 16 + 1;
auto renderSingle = [&] (int16_t xPos, int16_t zPos, BlockList &blockStack) {
m_readPixels.reset();
@ -560,12 +492,10 @@ void TileGenerator::renderMap()
for (const auto &it : blockStack) {
const BlockPos pos = it.first;
assert(pos.x == xPos && pos.z == zPos);
assert(pos.y >= yMin && pos.y < yMax);
assert(pos.y >= m_yMin / 16 && 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
@ -574,7 +504,6 @@ void TileGenerator::renderMap()
}
if (!m_readPixels.full())
renderMapBlockBottom(blockStack.begin()->first);
m_renderedAny |= m_readInfo.any();
};
auto postRenderRow = [&] (int16_t zPos) {
if (m_shading)
@ -588,26 +517,27 @@ void TileGenerator::renderMap()
int16_t xPos = *it2;
BlockList blockStack;
m_db->getBlocksOnXZ(blockStack, xPos, zPos, yMin, yMax);
m_db->getBlocksOnXZ(blockStack, xPos, zPos, m_yMin / 16, 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;
#ifndef NDEBUG
std::cerr << "Exhaustively searching height of "
<< (yMax - (m_yMin / 16)) << " blocks" << std::endl;
#endif
std::vector<BlockPos> positions;
positions.reserve(yMax - yMin);
positions.reserve(yMax - (m_yMin / 16));
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++)
for (int16_t yPos = m_yMin / 16; yPos < yMax; yPos++)
positions.emplace_back(xPos, yPos, zPos);
BlockList blockStack;
@ -615,23 +545,21 @@ void TileGenerator::renderMap()
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"
#ifndef NDEBUG
std::cerr << "Exhaustively searching "
<< (m_geomX2 - m_geomX) << "x" << (yMax - (m_yMin / 16)) << "x"
<< (m_geomY2 - m_geomY) << " blocks" << std::endl;
#endif
std::vector<BlockPos> positions;
positions.reserve(span_y);
positions.reserve(yMax - (m_yMin / 16));
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++)
for (int16_t yPos = m_yMin / 16; yPos < yMax; yPos++)
positions.emplace_back(xPos, yPos, zPos);
BlockList blockStack;
@ -639,17 +567,36 @@ void TileGenerator::renderMap()
blockStack.sort();
renderSingle(xPos, zPos, blockStack);
reportProgress(count++);
}
postRenderRow(zPos);
}
}
reportProgress(m_progressMax);
}
void TileGenerator::renderMapBlock(const BlockDecoder &blk, const BlockPos &pos)
{
/***/
static bool light_curve_init = false;
static float light_curve[16];
if (!light_curve_init) {
for (u8 i = 0; i < 16; i++)
light_curve[i] = expf((i - 15) / 12.0f);
light_curve_init = true;
}
/***/
auto light_at = [blk] (u8 x, u8 y, u8 z) -> u8 {
return blk.getParam1(x, y, z) >> 4; // night bank
};
static u8 m_light[16][16];
if (blk.isEmpty()) {
for (int z = 0; z < 16; ++z) {
for (int x = 0; x < 16; ++x) {
m_light[z][x] = light_at(x, 0, z);
}
}
return;
}
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;
@ -660,43 +607,50 @@ void TileGenerator::renderMapBlock(const BlockDecoder &blk, const BlockPos &pos)
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())
string name = blk.getNode(x, y, z);
if (name == "") {
if (y == 0) m_light[z][x] = light_at(x, 0, z);
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
Color c = it->second.to_color();
u8 light = (y == 15) ? m_light[z][x] : light_at(x, y+1, z);
if (light < 15) light = mymax(light, light_at(x, y, z));
if (1) {
float l2 = light_curve[light];
c.r = colorSafeBounds(c.r * l2);
c.g = colorSafeBounds(c.g * l2);
c.b = colorSafeBounds(c.b * l2);
} else
c = Color(light * 17, light * 17, light * 17);
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;
if (m_color[z][x].a == 0)
m_color[z][x] = c; // first visible time, no color mixing
else
m_color[z][x] = mixColors(m_color[z][x], c);
if(m_color[z][x].a < 0xff) {
// near thickness value to thickness of current node
m_thickness[z][x] = (m_thickness[z][x] + it->second.t) / 2.0;
continue;
}
// color became opaque, draw it
setZoomed(imageX, imageY, c);
attr.thickness = m_thickness[z][x];
setZoomed(imageX, imageY, m_color[z][x]);
m_blockPixelAttributes.attribute(15 - z, xBegin + x).thickness = m_thickness[z][x];
} else {
c.a = 255;
setZoomed(imageX, imageY, c);
setZoomed(imageX, imageY, c.noAlpha());
}
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_blockPixelAttributes.attribute(15 - z, xBegin + x).height = pos.y * 16 + y;
m_readInfo.set(x, z);
}
break;
@ -718,19 +672,17 @@ void TileGenerator::renderMapBlockBottom(const BlockPos &pos)
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];
m_blockPixelAttributes.attribute(15 - z, xBegin + x).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;
@ -738,27 +690,23 @@ void TileGenerator::renderShading(int zPos)
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()
!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 = a.attribute(z, x).height;
int y1 = a.attribute(z, x - 1).height;
int y2 = a.attribute(z - 1, x).height;
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 (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;
double t = m_blockPixelAttributes.attribute(z, x).thickness * 1.2;
d *= 1.0 - mymin(t, 255.0) / 255.0;
}
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);
@ -766,7 +714,7 @@ void TileGenerator::renderShading(int zPos)
setZoomed(x, imageY, c);
}
}
a.scroll();
m_blockPixelAttributes.scroll();
}
void TileGenerator::renderScale()
@ -844,16 +792,12 @@ void TileGenerator::renderOrigin()
m_image->drawCircle(getImageX(0, true), getImageY(0, true), 12, m_originColor);
}
void TileGenerator::renderPlayers(const std::string &input_path)
void TileGenerator::renderPlayers(const std::string &inputPath)
{
std::string input = input_path;
if (input.back() != PATH_SEPARATOR)
input += PATH_SEPARATOR;
PlayerAttributes players(input);
PlayerAttributes players(inputPath);
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)
if (player.x < m_xMin * 16 || player.x > m_xMax * 16 ||
player.z < m_zMin * 16 || player.z > m_zMax * 16)
continue;
if (player.y < m_yMin || player.y > m_yMax)
continue;
@ -862,7 +806,6 @@ void TileGenerator::renderPlayers(const std::string &input_path)
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);
}
}
@ -871,43 +814,16 @@ void TileGenerator::writeImage(const std::string &output)
{
m_image->save(output);
delete m_image;
m_image = nullptr;
m_image = NULL;
}
void TileGenerator::printUnknown()
{
if (m_unknownNodes.empty())
if (m_unknownNodes.size() == 0)
return;
errorstream << "Unknown nodes:\n";
std::cerr << "Unknown nodes:" << std::endl;
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();
std::cerr << "\t" << node << std::endl;
}
inline int TileGenerator::getImageX(int val, bool absolute) const

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;
}

132
autogenerating-colors.txt Normal file
View File

@ -0,0 +1,132 @@
==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 math import sqrt
from PIL import Image
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 = ([], [], [])
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[0].append(px[0]**2)
cl[1].append(px[1]**2)
cl[2].append(px[2]**2)
if len(cl[0]) == 0:
print("Didn't find average color for %s" % sys.argv[1], file=sys.stderr)
print("0 0 0")
else:
cl = tuple(sqrt(sum(x)/len(x)) for x 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 and xpanes:
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
sed -re 's/^xpanes:(.*(pane|bar)[^ ]*) ([0-9 ]+)$/xpanes:\1 \3 64 16/g' $COLORSTXT_PATH -i
# Delete some usually hidden nodes:
sed '/^doors:hidden /d' $COLORSTXT_PATH -i
sed '/^fireflies:firefly /d' $COLORSTXT_PATH -i
sed '/^butterflies:butterfly_/d' $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

@ -10,7 +10,7 @@ bones:bones 117 117 117
# butterflies
# carts
carts:brakerail 150 121 102
carts:brakerail 138 121 102
carts:powerrail 160 145 102
carts:rail 146 128 108
@ -117,11 +117,7 @@ default:marram_grass_1 113 139 96
default:marram_grass_2 102 131 90
default:marram_grass_3 99 130 88
default:mese 222 222 0
default:mese_post_light 132 103 57
default:mese_post_light_acacia_wood 151 62 39
default:mese_post_light_aspen_wood 210 199 170
default:mese_post_light_junglewood 57 39 14
default:mese_post_light_pine_wood 221 185 131
default:mese_post_light 134 105 59
default:meselamp 213 215 143
default:mossycobble 88 91 73
default:obsidian 21 24 29
@ -178,20 +174,12 @@ default:wood 131 102 57
# doors
doors:door_glass_a 245 245 245 64 16
doors:door_glass_b 245 245 245 64 16
doors:door_glass_c 245 245 245 64 16
doors:door_glass_d 245 245 245 64 16
doors:door_obsidian_glass_a 48 49 50 64 16
doors:door_obsidian_glass_b 48 49 50 64 16
doors:door_obsidian_glass_c 48 49 50 64 16
doors:door_obsidian_glass_d 48 49 50 64 16
doors:door_steel_a 203 203 203
doors:door_steel_b 203 203 203
doors:door_steel_c 203 203 203
doors:door_steel_d 203 203 203
doors:door_wood_a 89 68 37
doors:door_wood_b 89 68 37
doors:door_wood_c 89 68 37
doors:door_wood_d 89 68 37
doors:gate_acacia_wood_closed 150 61 39
doors:gate_acacia_wood_open 150 61 39
doors:gate_aspen_wood_closed 210 199 170
@ -216,7 +204,6 @@ farming:cotton_5 116 105 53
farming:cotton_6 121 95 59
farming:cotton_7 94 70 37
farming:cotton_8 122 108 93
farming:cotton_wild 111 111 101
farming:desert_sand_soil 161 132 72
farming:desert_sand_soil_wet 120 99 53
farming:dry_soil 178 136 90
@ -445,8 +432,6 @@ xpanes:bar 114 114 114 64 16
xpanes:bar_flat 114 114 114 64 16
xpanes:door_steel_bar_a 133 133 133 64 16
xpanes:door_steel_bar_b 133 133 133 64 16
xpanes:door_steel_bar_c 133 133 133 64 16
xpanes:door_steel_bar_d 133 133 133 64 16
xpanes:obsidian_pane 16 17 18 64 16
xpanes:obsidian_pane_flat 16 17 18 64 16
xpanes:pane 249 249 249 64 16

View File

@ -1,12 +1,11 @@
#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);
std::stringstream tmp(s);
int64_t t;
tmp >> t;
return t;
@ -19,12 +18,6 @@ static inline std::string i64tos(int64_t 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)
{
@ -32,7 +25,7 @@ DBLevelDB::DBLevelDB(const std::string &mapdir)
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());
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
@ -48,24 +41,18 @@ DBLevelDB::~DBLevelDB()
}
std::vector<BlockPos> DBLevelDB::getBlockPosXZ(BlockPos min, BlockPos max)
std::vector<BlockPos> DBLevelDB::getBlockPos(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)
if (it.first < min.z || it.first >= 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)
for (auto pos2 : it.second) {
if (pos2.first < min.x || pos2.first >= max.x)
continue;
// skip duplicates
if (!res.empty() && res.back().x == pos2.x && res.back().z == zpos)
if (pos2.second < min.y || pos2.second >= max.y)
continue;
res.emplace_back(pos2.x, pos2.y, zpos);
res.emplace_back(pos2.first, pos2.second, it.first);
}
}
return res;
@ -74,7 +61,7 @@ std::vector<BlockPos> DBLevelDB::getBlockPosXZ(BlockPos min, BlockPos max)
void DBLevelDB::loadPosCache()
{
leveldb::Iterator *it = db->NewIterator(leveldb::ReadOptions());
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);
@ -82,9 +69,6 @@ void DBLevelDB::loadPosCache()
posCache[pos.z].emplace_back(pos.x, pos.y);
}
delete it;
for (auto &it : posCache)
std::sort(it.second.begin(), it.second.end());
}
@ -97,18 +81,13 @@ void DBLevelDB::getBlocksOnXZ(BlockList &blocks, int16_t x, int16_t z,
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)
for (auto pos2 : it->second) {
if (pos2.first != x)
continue;
if (pos2.second < min_y || pos2.second >= max_y)
continue;
BlockPos pos(x, pos2.y, z);
BlockPos pos(x, pos2.second, z);
status = db->Get(leveldb::ReadOptions(), i64tos(encodeBlockPos(pos)), &datastr);
if (status.ok()) {
blocks.emplace_back(

View File

@ -3,88 +3,34 @@
#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 */
#define ARRLEN(x) (sizeof(x) / sizeof((x)[0]))
DBPostgreSQL::DBPostgreSQL(const std::string &mapdir)
{
std::ifstream ifs(mapdir + "world.mt");
if (!ifs.good())
std::ifstream ifs((mapdir + "/world.mt").c_str());
if(!ifs.good())
throw std::runtime_error("Failed to read world.mt");
std::string connect_string = read_setting("pgsql_connection", ifs);
ifs.close();
db = PQconnectdb(connect_string.c_str());
openDatabase(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::int4, posZ::int4 FROM blocks WHERE"
"SELECT posX::int4, posY::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"
" (posZ BETWEEN $5::int4 AND $6::int4)"
);
prepareStatement(
"get_blocks",
@ -108,12 +54,13 @@ DBPostgreSQL::~DBPostgreSQL()
try {
checkResults(PQexec(db, "COMMIT;"));
} catch (const std::exception& caught) {
errorstream << "could not finalize: " << caught.what() << std::endl;
std::cerr << "could not finalize: " << caught.what() << std::endl;
}
PQfinish(db);
}
std::vector<BlockPos> DBPostgreSQL::getBlockPosXZ(BlockPos min, BlockPos max)
std::vector<BlockPos> DBPostgreSQL::getBlockPos(BlockPos min, BlockPos max)
{
int32_t const x1 = htonl(min.x);
int32_t const x2 = htonl(max.x - 1);
@ -136,14 +83,11 @@ std::vector<BlockPos> DBPostgreSQL::getBlockPosXZ(BlockPos min, BlockPos max)
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);
}
for (int row = 0; row < numrows; ++row)
positions.emplace_back(pg_to_blockpos(results, row, 0));
PQclear(results);
return positions;
}
@ -222,8 +166,61 @@ void DBPostgreSQL::getBlocksByPos(BlockList &blocks,
}
}
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
)
{
return checkResults(PQexecPrepared(db, stmtName, paramsNumber,
(const char* const*) params, paramsLengths, paramsFormats,
1 /* binary output */), clear
);
}
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_binary_to_int(res, row, col);
result.y = pg_binary_to_int(res, row, col + 1);
result.z = pg_binary_to_int(res, row, col + 2);
return result;
}

View File

@ -20,6 +20,7 @@ static inline int64_t stoi64(const std::string &s)
return t;
}
static inline std::string i64tos(int64_t i)
{
std::ostringstream os;
@ -27,11 +28,10 @@ static inline std::string i64tos(int64_t i)
return os.str();
}
DBRedis::DBRedis(const std::string &mapdir)
{
std::ifstream ifs(mapdir + "world.mt");
if (!ifs.good())
std::ifstream ifs((mapdir + "/world.mt").c_str());
if(!ifs.good())
throw std::runtime_error("Failed to read world.mt");
std::string tmp;
@ -40,16 +40,12 @@ DBRedis::DBRedis(const std::string &mapdir)
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) {
const char *addr = tmp.c_str();
int port = stoi64(read_setting_default("redis_port", ifs, "6379"));
ctx = tmp.find('/') != std::string::npos ? redisConnectUnix(addr) : redisConnect(addr, port);
if(!ctx) {
throw std::runtime_error("Cannot allocate redis context");
} else if (ctx->err) {
} else if(ctx->err) {
std::string err = std::string("Connection error: ") + ctx->errstr;
redisFree(ctx);
throw std::runtime_error(err);
@ -68,7 +64,7 @@ DBRedis::~DBRedis()
}
std::vector<BlockPos> DBRedis::getBlockPosXZ(BlockPos min, BlockPos max)
std::vector<BlockPos> DBRedis::getBlockPos(BlockPos min, BlockPos max)
{
std::vector<BlockPos> res;
for (const auto &it : posCache) {
@ -86,9 +82,8 @@ std::vector<BlockPos> DBRedis::getBlockPosXZ(BlockPos min, BlockPos max)
}
const char *DBRedis::replyTypeStr(int type)
{
switch (type) {
const char *DBRedis::replyTypeStr(int type) {
switch(type) {
case REDIS_REPLY_STATUS:
return "REDIS_REPLY_STATUS";
case REDIS_REPLY_ERROR:
@ -102,7 +97,7 @@ const char *DBRedis::replyTypeStr(int type)
case REDIS_REPLY_ARRAY:
return "REDIS_REPLY_ARRAY";
default:
return "(unknown)";
return "unknown";
}
}
@ -111,12 +106,12 @@ void DBRedis::loadPosCache()
{
redisReply *reply;
reply = (redisReply*) redisCommand(ctx, "HKEYS %s", hash.c_str());
if (!reply)
if(!reply)
throw std::runtime_error("Redis command HKEYS failed");
if (reply->type != REDIS_REPLY_ARRAY)
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)
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);
@ -133,24 +128,25 @@ void DBRedis::HMGET(const std::vector<BlockPos> &positions,
argv[0] = "HMGET";
argv[1] = hash.c_str();
auto position = positions.begin();
size_t remaining = positions.size();
size_t abs_i = 0;
std::vector<BlockPos>::const_iterator position = positions.begin();
std::size_t remaining = positions.size();
std::size_t abs_i = 0;
while (remaining > 0) {
const size_t batch_size = mymin<size_t>(DB_REDIS_HMGET_NUMFIELDS, remaining);
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 (size_t i = 0; i < batch_size; ++i) {
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)
if(!reply)
throw std::runtime_error("Redis command HMGET failed");
if (reply->type != REDIS_REPLY_ARRAY)
REPLY_TYPE_ERR(reply, "HMGET reply");
@ -158,7 +154,7 @@ void DBRedis::HMGET(const std::vector<BlockPos> &positions,
freeReplyObject(reply);
throw std::runtime_error("HMGET wrong number of elements");
}
for (size_t i = 0; i < reply->elements; ++i) {
for (std::size_t i = 0; i < reply->elements; ++i) {
redisReply *subreply = reply->element[i];
if (subreply->type == REDIS_REPLY_NIL)
continue;
@ -166,14 +162,10 @@ void DBRedis::HMGET(const std::vector<BlockPos> &positions,
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
));
result(abs_i + i, ustring((const unsigned char *) subreply->str, subreply->len));
}
freeReplyObject(reply);
abs_i += batch_size;
abs_i += reply->elements;
remaining -= batch_size;
}
}

196
db-sqlite3.cpp Normal file
View File

@ -0,0 +1,196 @@
#include <stdexcept>
#include <unistd.h> // for usleep
#include <iostream>
#include <algorithm>
#include <time.h>
#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 data FROM blocks WHERE pos = ?",
-1, &stmt_get_block_exact, NULL))
SQLOK(prepare_v2(db,
"SELECT pos FROM blocks",
-1, &stmt_get_block_pos, NULL))
SQLOK(prepare_v2(db,
"SELECT pos FROM blocks WHERE pos BETWEEN ? AND ?",
-1, &stmt_get_block_pos_z, NULL))
}
DBSQLite3::~DBSQLite3()
{
sqlite3_finalize(stmt_get_blocks_z);
sqlite3_finalize(stmt_get_block_pos);
sqlite3_finalize(stmt_get_block_pos_z);
sqlite3_finalize(stmt_get_block_exact);
if (sqlite3_close(db) != SQLITE_OK) {
std::cerr << "Error closing SQLite database." << std::endl;
};
}
inline void DBSQLite3::getPosRange(int64_t &min, int64_t &max, int16_t zPos,
int16_t zPos2) const
{
/* The range of block positions is [-2048, 2047], which turns into [0, 4095]
* when casted to unsigned. This didn't actually help me understand the
* numbers below, but I wanted to write it down.
*/
// Magic numbers!
min = encodeBlockPos(BlockPos(0, -2048, zPos));
max = encodeBlockPos(BlockPos(0, 2048, zPos2)) - 1;
}
std::vector<BlockPos> DBSQLite3::getBlockPos(BlockPos min, BlockPos max)
{
int result;
sqlite3_stmt *stmt;
if(min.z <= -2048 && max.z >= 2048) {
stmt = stmt_get_block_pos;
} else {
stmt = stmt_get_block_pos_z;
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(bind_int64(stmt, 1, minPos))
SQLOK(bind_int64(stmt, 2, maxPos))
}
std::vector<BlockPos> positions;
while ((result = sqlite3_step(stmt)) != 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));
}
int64_t posHash = sqlite3_column_int64(stmt, 0);
BlockPos pos = decodeBlockPos(posHash);
if(pos.x >= min.x && pos.x < max.x && pos.y >= min.y && pos.y < max.y)
positions.emplace_back(pos);
}
SQLOK(reset(stmt));
return positions;
}
void DBSQLite3::loadBlockCache(int16_t zPos)
{
int result;
blockCache.clear();
int64_t minPos, maxPos;
getPosRange(minPos, maxPos, zPos, zPos);
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_BUSY) { // Wait some time and try again
usleep(10000);
} else if (result != SQLITE_ROW) {
throw std::runtime_error(sqlite3_errmsg(db));
}
int64_t posHash = sqlite3_column_int64(stmt_get_blocks_z, 0);
BlockPos pos = decodeBlockPos(posHash);
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);
blockCache[pos.x].emplace_back(pos, ustring(data, size));
}
SQLOK(reset(stmt_get_blocks_z))
}
void DBSQLite3::getBlocksOnXZ(BlockList &blocks, int16_t x, int16_t z,
int16_t min_y, int16_t max_y)
{
/* 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. */
#ifndef NDEBUG
std::cout << "Warning: suboptimal access pattern for sqlite3 backend" << std::endl;
#endif
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) {
int64_t dbPos = encodeBlockPos(pos);
SQLOK(bind_int64(stmt_get_block_exact, 1, dbPos));
while ((result = sqlite3_step(stmt_get_block_exact)) == SQLITE_BUSY) {
usleep(10000); // Wait some time and try again
}
if (result == SQLITE_DONE) {
// no data
} else if (result != SQLITE_ROW) {
throw std::runtime_error(sqlite3_errmsg(db));
} else {
const unsigned char *data = reinterpret_cast<const unsigned char *>(
sqlite3_column_blob(stmt_get_block_exact, 0));
size_t size = sqlite3_column_bytes(stmt_get_block_exact, 0);
blocks.emplace_back(pos, ustring(data, size));
}
SQLOK(reset(stmt_get_block_exact))
}
}

28
include/BlockDecoder.h Normal file
View File

@ -0,0 +1,28 @@
#ifndef BLOCKDECODER_H
#define BLOCKDECODER_H
#include <unordered_map>
#include "types.h"
class BlockDecoder {
public:
BlockDecoder();
void reset();
void decode(const ustring &data);
bool isEmpty() const;
std::string getNode(u8 x, u8 y, u8 z) const; // returns "" for air, ignore and invalid nodes
u8 getParam1(u8 x, u8 y, u8 z) const;
private:
typedef std::unordered_map<int, std::string> NameMap;
NameMap m_nameMap;
int m_blockAirId;
int m_blockIgnoreId;
u8 m_version, m_contentWidth;
ustring m_mapData;
};
#endif // BLOCKDECODER_H

View File

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

50
include/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 */

View File

@ -0,0 +1,30 @@
#ifndef PLAYERATTRIBUTES_H_D7THWFVV
#define PLAYERATTRIBUTES_H_D7THWFVV
#include <list>
#include <string>
struct Player
{
std::string name;
double x, y, z;
};
class PlayerAttributes
{
public:
typedef std::list<Player> Players;
PlayerAttributes(const std::string &worldDir);
Players::iterator begin();
Players::iterator end();
private:
void readFiles(const std::string &playersPath);
void readSqlite(const std::string &db_name);
Players m_players;
};
#endif /* end of include guard: PLAYERATTRIBUTES_H_D7THWFVV */

View File

@ -1,20 +1,21 @@
#pragma once
#ifndef TILEGENERATOR_HEADER
#define TILEGENERATOR_HEADER
#include <iostream>
#include <iosfwd>
#include <map>
#include <set>
#include <config.h>
#include <unordered_map>
#include <cstdint>
#include <unordered_set>
#include <stdint.h>
#include <string>
#include "PixelAttributes.h"
#include "BlockDecoder.h"
#include "Image.h"
#include "db.h"
#include "types.h"
class BlockDecoder;
class Image;
enum {
SCALE_TOP = (1 << 0),
SCALE_BOTTOM = (1 << 1),
@ -30,12 +31,10 @@ enum {
};
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
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, g, b, a, t;
};
struct BitmapThing { // 16x16 bitmap
@ -43,19 +42,17 @@ struct BitmapThing { // 16x16 bitmap
for (int i = 0; i < 16; ++i)
val[i] = 0;
}
inline bool any_neq(uint16_t v) const {
inline bool full() const {
for (int i = 0; i < 16; ++i) {
if (val[i] != v)
return true;
if (val[i] != 0xffff)
return false;
}
return false;
return true;
}
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 {
inline bool get(unsigned int x, unsigned int z) {
return !!(val[z] & (1 << x));
}
@ -67,6 +64,7 @@ class TileGenerator
{
private:
typedef std::unordered_map<std::string, ColorEntry> ColorMap;
typedef std::unordered_set<std::string> NameSet;
public:
TileGenerator();
@ -75,6 +73,7 @@ public:
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);
@ -92,8 +91,6 @@ public:
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:
@ -111,7 +108,6 @@ private:
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);
@ -146,22 +142,20 @@ private:
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;
NameSet m_unknownNodes;
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
#endif // TILEGENERATOR_HEADER

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 */

View File

@ -6,7 +6,6 @@
#cmakedefine01 USE_POSTGRESQL
#cmakedefine01 USE_LEVELDB
#cmakedefine01 USE_REDIS
#cmakedefine01 USE_ZLIB_NG
#define SHAREDIR "@SHAREDIR@"

17
include/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

View File

@ -1,4 +1,5 @@
#pragma once
#ifndef DB_LEVELDB_HEADER
#define DB_LEVELDB_HEADER
#include "db.h"
#include <unordered_map>
@ -8,7 +9,7 @@
class DBLevelDB : public DB {
public:
DBLevelDB(const std::string &mapdir);
std::vector<BlockPos> getBlockPosXZ(BlockPos min, BlockPos max) override;
std::vector<BlockPos> getBlockPos(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,
@ -18,24 +19,13 @@ public:
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;
}
};
using pos2d = std::pair<int16_t, int16_t>;
void loadPosCache();
// indexed by Z, contains all (x,y) position pairs
std::unordered_map<int16_t, std::vector<vec2>> posCache;
leveldb::DB *db = NULL;
std::unordered_map<int16_t, std::vector<pos2d>> posCache;
leveldb::DB *db;
};
#endif // DB_LEVELDB_HEADER

View File

@ -1,33 +1,13 @@
#pragma once
#ifndef _DB_POSTGRESQL_H
#define _DB_POSTGRESQL_H
#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 {
class DBPostgreSQL : public DB {
public:
DBPostgreSQL(const std::string &mapdir);
std::vector<BlockPos> getBlockPosXZ(BlockPos min, BlockPos max) override;
std::vector<BlockPos> getBlockPos(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,
@ -36,6 +16,20 @@ public:
bool preferRangeQueries() const override { return true; }
private:
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
);
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

View File

@ -1,4 +1,5 @@
#pragma once
#ifndef DB_REDIS_HEADER
#define DB_REDIS_HEADER
#include "db.h"
#include <unordered_map>
@ -9,7 +10,7 @@
class DBRedis : public DB {
public:
DBRedis(const std::string &mapdir);
std::vector<BlockPos> getBlockPosXZ(BlockPos min, BlockPos max) override;
std::vector<BlockPos> getBlockPos(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,
@ -32,3 +33,5 @@ private:
redisContext *ctx;
std::string hash;
};
#endif // DB_REDIS_HEADER

36
include/db-sqlite3.h Normal file
View File

@ -0,0 +1,36 @@
#ifndef _DB_SQLITE3_H
#define _DB_SQLITE3_H
#include "db.h"
#include <unordered_map>
#include <sqlite3.h>
class DBSQLite3 : public DB {
public:
DBSQLite3(const std::string &mapdir);
std::vector<BlockPos> getBlockPos(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 false; }
private:
inline void getPosRange(int64_t &min, int64_t &max, int16_t zPos,
int16_t zPos2) const;
void loadBlockCache(int16_t zPos);
sqlite3 *db;
sqlite3_stmt *stmt_get_block_pos;
sqlite3_stmt *stmt_get_block_pos_z;
sqlite3_stmt *stmt_get_blocks_z;
sqlite3_stmt *stmt_get_block_exact;
int16_t blockCachedZ = -10000;
std::unordered_map<int16_t, BlockList> blockCache; // indexed by X
};
#endif // _DB_SQLITE3_H

View File

@ -1,20 +1,24 @@
#pragma once
#ifndef DB_HEADER
#define DB_HEADER
#include <cstdint>
#include <stdint.h>
#include <map>
#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) {}
struct BlockPos {
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) {}
// Implements the inverse ordering so that (2,2,2) < (1,1,1)
inline bool operator<(const BlockPos &p) const
bool operator < (const BlockPos &p) const
{
if (z > p.z)
return true;
@ -24,7 +28,11 @@ struct BlockPos {
return true;
if (y < p.y)
return false;
return x > p.x;
if (x > p.x)
return true;
if (x < p.x)
return false;
return false;
}
};
@ -36,32 +44,29 @@ 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);
inline int64_t encodeBlockPos(const BlockPos pos) const;
inline BlockPos decodeBlockPos(int64_t hash) const;
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.
/* Return all block positions inside the range given by min and max,
* so that min.x <= x < max.x, ...
*/
virtual std::vector<BlockPos> getBlockPosXZ(BlockPos min, BlockPos max) = 0;
virtual std::vector<BlockPos> getBlockPos(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() {}
};
@ -94,7 +99,7 @@ static inline int64_t pythonmodulo(int64_t i, int64_t mod)
}
inline int64_t DB::encodeBlockPos(const BlockPos pos)
inline int64_t DB::encodeBlockPos(const BlockPos pos) const
{
return (uint64_t) pos.z * 0x1000000 +
(uint64_t) pos.y * 0x1000 +
@ -102,7 +107,7 @@ inline int64_t DB::encodeBlockPos(const BlockPos pos)
}
inline BlockPos DB::decodeBlockPos(int64_t hash)
inline BlockPos DB::decodeBlockPos(int64_t hash) const
{
BlockPos pos;
pos.x = unsigned_to_signed(pythonmodulo(hash, 4096), 2048);
@ -117,3 +122,4 @@ inline BlockPos DB::decodeBlockPos(int64_t hash)
* End black magic *
*******************/
#endif // DB_HEADER

5
include/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;

18
include/util.h Normal file
View File

@ -0,0 +1,18 @@
#ifndef UTIL_H
#define UTIL_H
#include <string>
#include <fstream>
std::string read_setting(const std::string &name, std::istream &is);
inline std::string read_setting_default(const std::string &name, std::istream &is, const std::string &def)
{
try {
return read_setting(name, is);
} catch(const std::runtime_error &e) {
return def;
}
}
#endif // UTIL_H

View File

@ -8,16 +8,14 @@
#include <string>
#include <sstream>
#include <stdexcept>
#include "config.h"
#include "cmake_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>"},
const std::pair<const char*, const char*> options[] = {
{"-i/--input", "<world_path>"},
{"-o/--output", "<output_image.png>"},
{"--bgcolor", "<color>"},
{"--scalecolor", "<color>"},
{"--playercolor", "<color>"},
@ -28,21 +26,19 @@ static void usage()
{"--drawalpha", ""},
{"--noshading", ""},
{"--noemptyimage", ""},
{"-v/--verbose", ""},
{"--min-y", "<y>"},
{"--max-y", "<y>"},
{"--backend", "<backend>"},
{"--geometry", "x:z+w+h"},
{"--geometry", "x:y+w+h"},
{"--extent", ""},
{"--zoom", "<factor>"},
{"--colors", "<path>"},
{"--zoom", "<zoomlevel>"},
{"--colors", "<colors.txt>"},
{"--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"
"minetestmapper -i <world_path> -o <output_image.png> [options]\n"
"Generate an overview image of a Minetest map.\n"
"\n"
"Options:\n";
const char *bottom_text =
@ -58,51 +54,38 @@ static void usage()
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)
static 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;
std::ifstream ifs(path.c_str());
return ifs.is_open();
}
static std::string search_colors(const std::string &worldpath)
{
if (file_exists(worldpath + "/colors.txt"))
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))
if(home) {
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"))
constexpr bool sharedir_valid = !(SHAREDIR[0] == '.' || SHAREDIR[0] == '\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";
std::cerr << "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'},
@ -127,23 +110,18 @@ int main(int argc, char *argv[])
{"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);
std::string colors = "";
TileGenerator generator;
bool onlyPrintExtent = false;
while (1) {
int option_index;
int c = getopt_long(argc, argv, short_options, long_options, &option_index);
int c = getopt_long(argc, argv, "hi:o:", long_options, &option_index);
if (c == -1)
break; // done
@ -151,6 +129,7 @@ int main(int argc, char *argv[])
case 'h':
usage();
return 0;
break;
case 'i':
input = optarg;
break;
@ -190,11 +169,19 @@ int main(int argc, char *argv[])
case 'd':
generator.setBackend(optarg);
break;
case 'a':
generator.setMinY(stoi(optarg));
case 'a': {
std::istringstream iss(optarg);
int miny;
iss >> miny;
generator.setMinY(miny);
}
break;
case 'c':
generator.setMaxY(stoi(optarg));
case 'c': {
std::istringstream iss(optarg);
int maxy;
iss >> maxy;
generator.setMaxY(maxy);
}
break;
case 'g': {
std::istringstream geometry(optarg);
@ -203,26 +190,30 @@ int main(int argc, char *argv[])
geometry >> x >> c >> y >> w >> h;
if (geometry.fail() || c != ':' || w < 1 || h < 1) {
usage();
return 1;
exit(1);
}
generator.setGeometry(x, y, w, h);
}
break;
case 'f': {
uint flags = 0;
if (strchr(optarg, 't'))
if(strchr(optarg, 't') != NULL)
flags |= SCALE_TOP;
if (strchr(optarg, 'b'))
if(strchr(optarg, 'b') != NULL)
flags |= SCALE_BOTTOM;
if (strchr(optarg, 'l'))
if(strchr(optarg, 'l') != NULL)
flags |= SCALE_LEFT;
if (strchr(optarg, 'r'))
if(strchr(optarg, 'r') != NULL)
flags |= SCALE_RIGHT;
generator.setScales(flags);
}
break;
case 'z':
generator.setZoom(stoi(optarg));
case 'z': {
std::istringstream iss(optarg);
int zoom;
iss >> zoom;
generator.setZoom(zoom);
}
break;
case 'C':
colors = optarg;
@ -231,56 +222,42 @@ int main(int argc, char *argv[])
generator.setDontWriteEmpty(true);
break;
case 'j': {
int mode = EXH_AUTO;
int mode;
if (!strcmp(optarg, "never"))
mode = EXH_NEVER;
else if (!strcmp(optarg, "y"))
mode = EXH_Y;
else if (!strcmp(optarg, "full"))
mode = EXH_FULL;
else
mode = EXH_AUTO;
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;
exit(1);
}
}
const bool need_output = !onlyPrintExtent && dumpblock.x == INT16_MIN;
if (input.empty() || (need_output && output.empty())) {
if (input.empty() || (!onlyPrintExtent && 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())
if(colors == "")
colors = search_colors(input);
generator.parseColorsFile(colors);
generator.generate(input, output);
} catch (const std::exception &e) {
errorstream << "Exception: " << e.what() << std::endl;
} 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
.SH NAME
minetestmapper \- generate an overview image of a Luanti map
minetestmapper \- generate an overview image of a Minetest map
.SH SYNOPSIS
.B minetestmapper
\fB\-i\fR \fIworld_path\fR
@ -9,22 +9,16 @@ minetestmapper \- generate an overview image of a Luanti map
See additional optional parameters below.
.SH DESCRIPTION
.B minetestmapper
generates a top-down overview image of a Luanti map.
This is a port of the obsolete minetestmapper.py script to C++,
that is both faster and provides more features.
Minetestmapper ships with a colors.txt file suitable for Minetest Game,
if you use a different game or have mods installed you should generate a
matching colors.txt for better results (colors will be missing otherwise).
generates an overview image of a minetest map. This is a port of
the original minetestmapper.py to C++, that is both faster and
provides more functionality than the deprecated Python script.
.SH MANDATORY PARAMETERS
.TP
.BR \-i " " \fIworld_path\fR
Input world path
Input world path.
.TP
.BR \-o " " \fIoutput_image\fR
Path to output image
Path to output image. (only PNG supported currently)
.SH OPTIONAL PARAMETERS
.TP
.BR \-\-bgcolor " " \fIcolor\fR
@ -32,7 +26,7 @@ Background color of image, e.g. "--bgcolor #ffffff"
.TP
.BR \-\-scalecolor " " \fIcolor\fR
Color of scale marks and text, e.g. "--scalecolor #000000"
Color of scale, e.g. "--scalecolor #000000"
.TP
.BR \-\-playercolor " " \fIcolor\fR
@ -44,11 +38,11 @@ Color of origin indicator, e.g. "--origincolor #ff0000"
.TP
.BR \-\-drawscale
Draw scale(s) with tick marks and numbers
Draw tick marks
.TP
.BR \-\-drawplayers
Draw player indicators with name
Draw player indicators
.TP
.BR \-\-draworigin
@ -56,7 +50,7 @@ Draw origin indicator
.TP
.BR \-\-drawalpha
Allow nodes to be drawn with transparency (such as water)
Allow nodes to be drawn with transparency
.TP
.BR \-\-noshading
@ -64,32 +58,26 @@ 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.
Don't output anything when the image would be empty.
.TP
.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
.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
.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: \fIsqlite3\fP, \fIleveldb\fP, \fIredis\fP, \fIpostgresql\fP, e.g. "--backend leveldb"
.TP
.BR \-\-geometry " " \fIgeometry\fR
Limit area to specific geometry (\fIx:z+w+h\fP where x and z specify the lower left corner), e.g. "--geometry -800:-800+1600+1600"
The coordinates are specified with the same axes as in-game. The Z axis becomes Y when projected on the image.
Limit area to specific geometry (\fIx:y+w+h\fP where x and y specify the lower left corner), e.g. "--geometry -800:-800+1600+1600"
.TP
.BR \-\-extent
.BR \-\-extent " " \fIextent\fR
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
@ -98,24 +86,25 @@ Zoom the image by using more than one pixel per node, e.g. "--zoom 4"
.TP
.BR \-\-colors " " \fIpath\fR
Override auto-detected path to colors.txt, e.g. "--colors ../world/mycolors.txt"
Forcefully set path to colors.txt file (it's autodetected otherwise), e.g. "--colors ../minetest/mycolors.txt"
.TP
.BR \-\-scales " " \fIedges\fR
Draw scales on specified image edges (letters \fIt b l r\fP meaning top, bottom, left and right), e.g. "--scales tbr"
.TP
.BR \-\-exhaustive " " \fImode\fR
.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.
Defaults to \fIauto\fP. You shouldn't need to change this, but doing so can improve rendering times on large maps.
For these optimizations to work it is important that you set
.B min-y
and
.B max-y
when you don't care about the world below e.g. -60 and above 1000 nodes.
.SH MORE INFORMATION
Website: https://github.com/luanti-org/minetestmapper
Website: https://github.com/minetest/minetestmapper
.SH MAN PAGE AUTHOR
Daniel Moerner

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,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,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
};

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,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);

45
util.cpp Normal file
View File

@ -0,0 +1,45 @@
#include <stdexcept>
#include <sstream>
#include "util.h"
static inline std::string trim(const std::string &s)
{
auto isspace = [] (char c) -> bool { 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);
}
std::string read_setting(const std::string &name, std::istream &is)
{
char linebuf[512];
while (is.good()) {
is.getline(linebuf, sizeof(linebuf));
for(char *p = linebuf; *p; p++) {
if(*p != '#')
continue;
*p = '\0'; // Cut off at the first #
break;
}
std::string line(linebuf);
auto pos = line.find('=');
if (pos == std::string::npos)
continue;
auto key = trim(line.substr(0, pos));
if (key != name)
continue;
return trim(line.substr(pos+1));
}
std::ostringstream oss;
oss << "Setting '" << name << "' not found";
throw std::runtime_error(oss.str());
}

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."

73
util/build_win.sh Executable file
View File

@ -0,0 +1,73 @@
#!/bin/bash -e
#######
# this expects an env similar to what minetest's buildbot uses
# extradll_path will typically contain libgcc, libstdc++ and libpng
toolchain_file=
toolchain_file64=
libgd_dir=
libgd_dir64=
zlib_dir=
zlib_dir64=
sqlite_dir=
sqlite_dir64=
leveldb_dir=
leveldb_dir64=
extradll_path=
extradll_path64=
#######
[ -f ./CMakeLists.txt ] || exit 1
if [ "$1" == "32" ]; then
:
elif [ "$1" == "64" ]; then
toolchain_file=$toolchain_file64
libgd_dir=$libgd_dir64
zlib_dir=$zlib_dir64
sqlite_dir=$sqlite_dir64
leveldb_dir=$leveldb_dir64
extradll_path=$extradll_path64
else
echo "Usage: $0 <32 / 64>"
exit 1
fi
cmake . \
-DCMAKE_INSTALL_PREFIX=/tmp \
-DCMAKE_TOOLCHAIN_FILE=$toolchain_file \
-DCMAKE_EXE_LINKER_FLAGS="-s" \
\
-DENABLE_LEVELDB=1 \
\
-DLIBGD_INCLUDE_DIR=$libgd_dir/include \
-DLIBGD_LIBRARY=$libgd_dir/lib/libgd.dll.a \
\
-DZLIB_INCLUDE_DIR=$zlib_dir/include \
-DZLIB_LIBRARY=$zlib_dir/lib/libz.dll.a \
\
-DSQLITE3_INCLUDE_DIR=$sqlite_dir/include \
-DSQLITE3_LIBRARY=$sqlite_dir/lib/libsqlite3.dll.a \
\
-DLEVELDB_INCLUDE_DIR=$leveldb_dir/include \
-DLEVELDB_LIBRARY=$leveldb_dir/lib/libleveldb.dll.a
make -j4
mkdir pack
cp -p \
AUTHORS colors.txt COPYING README.rst \
minetestmapper.exe \
$libgd_dir/bin/libgd-3.dll \
$zlib_dir/bin/zlib1.dll \
$sqlite_dir/bin/libsqlite3-0.dll \
$leveldb_dir/bin/libleveldb.dll \
$extradll_path/*.dll \
pack/
zipfile=minetestmapper-win$1.zip
(cd pack; zip -9r ../$zipfile *)
make clean
rm -r pack CMakeCache.txt
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,111 +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
msg "old schema: all limits"
# fill the map with more blocks and then request just a single one to be rendered
# this will run through internal consistency asserts.
writemap "
$schema_old
INSERT INTO blocks SELECT $(encodepos 2 2 2), d FROM d;
INSERT INTO blocks SELECT $(encodepos 1 2 2), d FROM d;
INSERT INTO blocks SELECT $(encodepos 2 1 2), d FROM d;
INSERT INTO blocks SELECT $(encodepos 2 2 1), d FROM d;
INSERT INTO blocks SELECT $(encodepos 3 2 2), d FROM d;
INSERT INTO blocks SELECT $(encodepos 2 3 2), d FROM d;
INSERT INTO blocks SELECT $(encodepos 2 2 3), d FROM d;
"
checkmap 1 --geometry 32:32+16+16 --min-y 32 --max-y $((32+16-1))
msg "new schema"
writemap "
$schema_new
INSERT INTO blocks SELECT 0, 1, 0, d FROM d;
"
checkmap 1
msg "new schema: all limits"
# same as above
writemap "
$schema_new
INSERT INTO blocks SELECT 2, 2, 2, d FROM d;
INSERT INTO blocks SELECT 1, 2, 2, d FROM d;
INSERT INTO blocks SELECT 2, 1, 2, d FROM d;
INSERT INTO blocks SELECT 2, 2, 1, d FROM d;
INSERT INTO blocks SELECT 3, 2, 2, d FROM d;
INSERT INTO blocks SELECT 2, 3, 2, d FROM d;
INSERT INTO blocks SELECT 2, 2, 3, d FROM d;
"
checkmap 1 --geometry 32:32+16+16 --min-y 32 --max-y $((32+16-1))
msg "new schema: empty map"
writemap "$schema_new"
checkmap 0
msg "drawplayers"
writemap "
$schema_new
INSERT INTO blocks SELECT 0, 0, 0, d FROM d;
"
mkdir $mapdir/players
printf '%s\n' "name = cat" "position = (80,0,80)" >$mapdir/players/cat
# we can't check that it actually worked, however
checkmap 1 --drawplayers --zoom 4

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.")

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

@ -0,0 +1,8 @@
#!/bin/bash -e
mkdir -p travisbuild
cd travisbuild
cmake .. \
-DENABLE_LEVELDB=1
make -j2