mirror of
https://github.com/minetest/minetestmapper.git
synced 2025-07-04 17:40:22 +02:00
Compare commits
56 Commits
night_rend
...
20250408
Author | SHA1 | Date | |
---|---|---|---|
637755d6f9 | |||
9d7400a438 | |||
9367e45e66 | |||
314debe4fb | |||
458c3c30a0 | |||
0a56b18cfb | |||
3c08380d18 | |||
527db7fc34 | |||
8b1a143cda | |||
4ba09ec532 | |||
e982efe94e | |||
cd36f16775 | |||
7685e548f0 | |||
46cb386fef | |||
7a0bc15d21 | |||
d9c89bd6a2 | |||
5016bca232 | |||
c93948c200 | |||
ad403975fd | |||
0f51edcb1f | |||
b4d4632212 | |||
6947c5c4e4 | |||
bbe2f8f404 | |||
527a56f22e | |||
1c16c40ccc | |||
dd5c4e509d | |||
0982a03dbd | |||
dd1904a667 | |||
40a5e16e21 | |||
7e4caacb9e | |||
e14f27f412 | |||
7af222dd9d | |||
c81cda24d3 | |||
8a7333ef49 | |||
7fb3b9edd6 | |||
18f0615002 | |||
d75266eae1 | |||
31b0d09a19 | |||
e4bf375ac7 | |||
b491dd375a | |||
2e353312b5 | |||
8e9805c3ff | |||
9b26d9495c | |||
0198897306 | |||
2f3a548881 | |||
ccd5d14962 | |||
f471554294 | |||
b0ca3d7066 | |||
5c435f6459 | |||
8b563f409e | |||
f26070ef4f | |||
fd4c5dd232 | |||
fa5c63cfc8 | |||
e88fcf0dd8 | |||
6bb818ac2f | |||
8e83ce6464 |
16
.dockerignore
Normal file
16
.dockerignore
Normal file
@ -0,0 +1,16 @@
|
||||
.git
|
||||
.github
|
||||
|
||||
*~
|
||||
|
||||
minetestmapper
|
||||
minetestmapper.exe
|
||||
|
||||
CMakeCache.txt
|
||||
CMakeFiles/
|
||||
CPack*.cmake
|
||||
_CPack_Packages/
|
||||
install_manifest.txt
|
||||
Makefile
|
||||
cmake_install.cmake
|
||||
cmake_config.h
|
85
.github/workflows/build.yml
vendored
Normal file
85
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,85 @@
|
||||
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
|
87
.github/workflows/docker_image.yml
vendored
Normal file
87
.github/workflows/docker_image.yml
vendored
Normal file
@ -0,0 +1,87 @@
|
||||
---
|
||||
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
16
.gitignore
vendored
@ -1,10 +1,16 @@
|
||||
colors.txt
|
||||
minetestmapper
|
||||
minetestmapper.exe
|
||||
*~
|
||||
|
||||
/minetestmapper
|
||||
/minetestmapper.exe
|
||||
/colors.txt
|
||||
|
||||
CMakeCache.txt
|
||||
CMakeFiles/
|
||||
CPack*
|
||||
CPack*.cmake
|
||||
_CPack_Packages/
|
||||
install_manifest.txt
|
||||
Makefile
|
||||
cmake_install.cmake
|
||||
cmake_config.h
|
||||
*~
|
||||
compile_commands.json
|
||||
.vscode/
|
||||
|
19
.travis.yml
19
.travis.yml
@ -1,19 +0,0 @@
|
||||
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
|
155
CMakeLists.txt
155
CMakeLists.txt
@ -1,10 +1,9 @@
|
||||
project(minetestmapper CXX)
|
||||
cmake_minimum_required(VERSION 2.6)
|
||||
cmake_policy(SET CMP0003 NEW)
|
||||
cmake_minimum_required(VERSION 3.5)
|
||||
|
||||
set(VERSION_MAJOR 1)
|
||||
set(VERSION_MINOR 0)
|
||||
set(VERSION_STRING "${VERSION_MAJOR}.${VERSION_MINOR}")
|
||||
project(minetestmapper
|
||||
VERSION 1.0
|
||||
LANGUAGES CXX
|
||||
)
|
||||
|
||||
# Stuff & Paths
|
||||
|
||||
@ -13,18 +12,17 @@ if(NOT CMAKE_BUILD_TYPE)
|
||||
endif()
|
||||
|
||||
set(CMAKE_CXX_STANDARD 11)
|
||||
set(CMAKE_CXX_FLAGS_RELEASE "-O3 -Wall -DNDEBUG")
|
||||
set(CMAKE_CXX_FLAGS_DEBUG "-O0 -g2 -Wall")
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
if(WIN32)
|
||||
set(SHAREDIR ".")
|
||||
set(BINDIR ".")
|
||||
set(DOCDIR ".")
|
||||
else()
|
||||
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")
|
||||
set(SHAREDIR "share/luanti") # reuse engine share dir
|
||||
set(BINDIR "bin")
|
||||
set(DOCDIR "share/doc/${PROJECT_NAME}")
|
||||
set(MANDIR "share/man")
|
||||
endif()
|
||||
|
||||
set(CUSTOM_SHAREDIR "" CACHE STRING "Directory to install data files into")
|
||||
@ -45,9 +43,7 @@ if(NOT CUSTOM_DOCDIR STREQUAL "")
|
||||
message(STATUS "Using DOCDIR=${DOCDIR}")
|
||||
endif()
|
||||
|
||||
#set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)
|
||||
find_package(PkgConfig)
|
||||
include(FindPackageHandleStandardArgs)
|
||||
list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake")
|
||||
|
||||
# Libraries: gd
|
||||
|
||||
@ -61,18 +57,26 @@ endif(NOT LIBGD_LIBRARY OR NOT LIBGD_INCLUDE_DIR)
|
||||
|
||||
# Libraries: zlib
|
||||
|
||||
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)
|
||||
find_package(zlib-ng QUIET)
|
||||
if(zlib-ng_FOUND)
|
||||
set(ZLIB_INCLUDE_DIR zlib-ng::zlib)
|
||||
set(ZLIB_LIBRARY zlib-ng::zlib)
|
||||
set(USE_ZLIB_NG TRUE)
|
||||
message(STATUS "Found zlib-ng, using it instead of zlib.")
|
||||
else()
|
||||
message(STATUS "zlib-ng not found, falling back to zlib.")
|
||||
find_package(ZLIB REQUIRED)
|
||||
set(USE_ZLIB_NG FALSE)
|
||||
endif()
|
||||
|
||||
# Libraries: zstd
|
||||
|
||||
find_package(Zstd REQUIRED)
|
||||
|
||||
# Libraries: sqlite3
|
||||
|
||||
find_library(SQLITE3_LIBRARY sqlite3)
|
||||
find_path(SQLITE3_INCLUDE_DIR zlib.h)
|
||||
find_path(SQLITE3_INCLUDE_DIR sqlite3.h)
|
||||
message (STATUS "sqlite3 library: ${SQLITE3_LIBRARY}")
|
||||
message (STATUS "sqlite3 headers: ${SQLITE3_INCLUDE_DIR}")
|
||||
if(NOT SQLITE3_LIBRARY OR NOT SQLITE3_INCLUDE_DIR)
|
||||
@ -85,7 +89,18 @@ option(ENABLE_POSTGRESQL "Enable PostgreSQL backend" TRUE)
|
||||
set(USE_POSTGRESQL FALSE)
|
||||
|
||||
if(ENABLE_POSTGRESQL)
|
||||
find_package("PostgreSQL")
|
||||
if(CMAKE_VERSION VERSION_LESS "3.20")
|
||||
find_package(PostgreSQL QUIET)
|
||||
# Before CMake 3.20 FindPostgreSQL.cmake always looked for server includes
|
||||
# but we don't need them, so continue anyway if only those are missing.
|
||||
if(PostgreSQL_INCLUDE_DIR AND PostgreSQL_LIBRARY)
|
||||
set(PostgreSQL_FOUND TRUE)
|
||||
set(PostgreSQL_INCLUDE_DIRS ${PostgreSQL_INCLUDE_DIR})
|
||||
set(PostgreSQL_LIBRARIES ${PostgreSQL_LIBRARY})
|
||||
endif()
|
||||
else()
|
||||
find_package(PostgreSQL)
|
||||
endif()
|
||||
|
||||
if(PostgreSQL_FOUND)
|
||||
set(USE_POSTGRESQL TRUE)
|
||||
@ -141,57 +156,63 @@ endif(ENABLE_REDIS)
|
||||
|
||||
# Compiling & Linking
|
||||
|
||||
include_directories(
|
||||
"${PROJECT_BINARY_DIR}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/include"
|
||||
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"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}"
|
||||
${SQLITE3_INCLUDE_DIR}
|
||||
${LIBGD_INCLUDE_DIR}
|
||||
${ZLIB_INCLUDE_DIR}
|
||||
${ZSTD_INCLUDE_DIR}
|
||||
)
|
||||
|
||||
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
|
||||
target_link_libraries(minetestmapper
|
||||
${SQLITE3_LIBRARY}
|
||||
${PostgreSQL_LIBRARIES}
|
||||
${LEVELDB_LIBRARY}
|
||||
${REDIS_LIBRARY}
|
||||
${LIBGD_LIBRARY}
|
||||
${ZLIB_LIBRARY}
|
||||
${ZSTD_LIBRARY}
|
||||
)
|
||||
|
||||
# Installing & Packaging
|
||||
@ -205,17 +226,15 @@ if(UNIX)
|
||||
install(FILES "minetestmapper.6" DESTINATION "${MANDIR}/man6")
|
||||
endif()
|
||||
|
||||
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_DESCRIPTION_SUMMARY "Overview mapper for Luanti")
|
||||
set(CPACK_PACKAGE_VENDOR "celeron55")
|
||||
set(CPACK_PACKAGE_CONTACT "Perttu Ahola <celeron55@gmail.com>")
|
||||
|
||||
if(WIN32)
|
||||
set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${VERSION_STRING}-win32")
|
||||
set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}-win32")
|
||||
set(CPACK_GENERATOR ZIP)
|
||||
else()
|
||||
set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${VERSION_STRING}-linux")
|
||||
set(CPACK_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}-linux")
|
||||
set(CPACK_GENERATOR TGZ)
|
||||
set(CPACK_SOURCE_GENERATOR TGZ)
|
||||
endif()
|
||||
|
24
Dockerfile
Normal file
24
Dockerfile
Normal file
@ -0,0 +1,24 @@
|
||||
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"]
|
@ -1,133 +0,0 @@
|
||||
#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();
|
||||
}
|
||||
|
80
README.rst
80
README.rst
@ -1,32 +1,50 @@
|
||||
Minetest Mapper C++
|
||||
===================
|
||||
|
||||
.. image:: https://travis-ci.org/minetest/minetestmapper.svg?branch=master
|
||||
:target: https://travis-ci.org/minetest/minetestmapper
|
||||
.. image:: https://github.com/minetest/minetestmapper/workflows/build/badge.svg
|
||||
:target: https://github.com/minetest/minetestmapper/actions/workflows/build.yml
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
* C++ compiler, zlib, zstd
|
||||
* libgd
|
||||
* sqlite3
|
||||
* 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)
|
||||
* LevelDB (optional)
|
||||
* hiredis (optional)
|
||||
* Postgres libraries (optional)
|
||||
|
||||
e.g. on Debian:
|
||||
^^^^^^^^^^^^^^^
|
||||
on Debian/Ubuntu:
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
sudo apt-get install libgd-dev libsqlite3-dev libleveldb-dev libhiredis-dev libpq-dev
|
||||
``sudo apt install cmake libgd-dev libhiredis-dev libleveldb-dev libpq-dev libsqlite3-dev zlib1g-dev libzstd-dev``
|
||||
|
||||
Windows
|
||||
^^^^^^^
|
||||
Minetestmapper for Windows can be downloaded here: https://github.com/minetest/minetestmapper/releases
|
||||
on openSUSE:
|
||||
^^^^^^^^^^^^
|
||||
|
||||
After extracting the archive, minetestmapper can be invoked from cmd.exe:
|
||||
::
|
||||
``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
|
||||
|
||||
cd C:\Users\yourname\Desktop\example\path
|
||||
minetestmapper.exe --help
|
||||
@ -34,16 +52,16 @@ After extracting the archive, minetestmapper can be invoked from cmd.exe:
|
||||
Compilation
|
||||
-----------
|
||||
|
||||
::
|
||||
.. code-block:: bash
|
||||
|
||||
cmake . -DENABLE_LEVELDB=1
|
||||
make -j2
|
||||
make -j$(nproc)
|
||||
|
||||
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).
|
||||
|
||||
::
|
||||
|
||||
@ -75,7 +93,7 @@ draworigin:
|
||||
Draw origin indicator, ``--draworigin``
|
||||
|
||||
drawalpha:
|
||||
Allow nodes to be drawn with transparency (e.g. water), ``--drawalpha``
|
||||
Allow nodes to be drawn with transparency (such as water), ``--drawalpha``
|
||||
|
||||
extent:
|
||||
Don't output any imagery, just print the extent of the full map, ``--extent``
|
||||
@ -86,11 +104,14 @@ 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``
|
||||
@ -98,16 +119,21 @@ 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:
|
||||
Apply zoom to drawn nodes by enlarging them to n*n squares, e.g. ``--zoom 4``
|
||||
Zoom the image by using more than one pixel per node, e.g. ``--zoom 4``
|
||||
|
||||
colors:
|
||||
Override auto-detected path to colors.txt, e.g. ``--colors ../minetest/mycolors.txt``
|
||||
Override auto-detected path to colors.txt, e.g. ``--colors ../world/mycolors.txt``
|
||||
|
||||
scales:
|
||||
Draw scales on specified image edges (letters *t b l r* meaning top, bottom, left and right), e.g. ``--scales tbr``
|
||||
|
||||
exhaustive:
|
||||
| Select if database should be traversed exhaustively or using range queries, available: *never*, *y*, *full*, *auto*
|
||||
| Defaults to *auto*. You shouldn't need to change this, 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.
|
||||
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.
|
||||
|
@ -1,71 +0,0 @@
|
||||
/*
|
||||
* =====================================================================
|
||||
* 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;
|
||||
}
|
||||
|
@ -1,132 +0,0 @@
|
||||
==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)
|
24
cmake/FindZstd.cmake
Normal file
24
cmake/FindZstd.cmake
Normal file
@ -0,0 +1,24 @@
|
||||
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)
|
19
colors.txt
19
colors.txt
@ -10,7 +10,7 @@ bones:bones 117 117 117
|
||||
# butterflies
|
||||
|
||||
# carts
|
||||
carts:brakerail 138 121 102
|
||||
carts:brakerail 150 121 102
|
||||
carts:powerrail 160 145 102
|
||||
carts:rail 146 128 108
|
||||
|
||||
@ -117,7 +117,11 @@ 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 134 105 59
|
||||
default:mese_post_light 132 103 57
|
||||
default:mese_post_light_acacia_wood 151 62 39
|
||||
default:mese_post_light_aspen_wood 210 199 170
|
||||
default:mese_post_light_junglewood 57 39 14
|
||||
default:mese_post_light_pine_wood 221 185 131
|
||||
default:meselamp 213 215 143
|
||||
default:mossycobble 88 91 73
|
||||
default:obsidian 21 24 29
|
||||
@ -174,12 +178,20 @@ 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
|
||||
@ -204,6 +216,7 @@ 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
|
||||
@ -432,6 +445,8 @@ 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
|
||||
|
196
db-sqlite3.cpp
196
db-sqlite3.cpp
@ -1,196 +0,0 @@
|
||||
#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))
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
#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
|
@ -1,50 +0,0 @@
|
||||
/*
|
||||
* =====================================================================
|
||||
* 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 */
|
||||
|
@ -1,30 +0,0 @@
|
||||
#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 */
|
||||
|
@ -1,37 +0,0 @@
|
||||
/*
|
||||
* =====================================================================
|
||||
* 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 */
|
||||
|
@ -1,17 +0,0 @@
|
||||
#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
|
@ -1,36 +0,0 @@
|
||||
#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
|
@ -1,5 +0,0 @@
|
||||
#include <string>
|
||||
|
||||
typedef std::basic_string<unsigned char> ustring;
|
||||
typedef unsigned int uint;
|
||||
typedef unsigned char u8;
|
@ -1,18 +0,0 @@
|
||||
#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
|
@ -1,6 +1,6 @@
|
||||
.TH MINETESTMAPPER 6
|
||||
.SH NAME
|
||||
minetestmapper \- generate an overview image of a Minetest map
|
||||
minetestmapper \- generate an overview image of a Luanti map
|
||||
.SH SYNOPSIS
|
||||
.B minetestmapper
|
||||
\fB\-i\fR \fIworld_path\fR
|
||||
@ -9,16 +9,22 @@ minetestmapper \- generate an overview image of a Minetest map
|
||||
See additional optional parameters below.
|
||||
.SH DESCRIPTION
|
||||
.B minetestmapper
|
||||
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.
|
||||
generates a top-down overview image of a Luanti map.
|
||||
This is a port of the obsolete minetestmapper.py script to C++,
|
||||
that is both faster and provides more features.
|
||||
|
||||
Minetestmapper ships with a colors.txt file suitable for Minetest Game,
|
||||
if you use a different game or have mods installed you should generate a
|
||||
matching colors.txt for better results (colors will be missing otherwise).
|
||||
|
||||
.SH MANDATORY PARAMETERS
|
||||
.TP
|
||||
.BR \-i " " \fIworld_path\fR
|
||||
Input world path.
|
||||
Input world path
|
||||
.TP
|
||||
.BR \-o " " \fIoutput_image\fR
|
||||
Path to output image. (only PNG supported currently)
|
||||
Path to output image
|
||||
|
||||
.SH OPTIONAL PARAMETERS
|
||||
.TP
|
||||
.BR \-\-bgcolor " " \fIcolor\fR
|
||||
@ -26,7 +32,7 @@ Background color of image, e.g. "--bgcolor #ffffff"
|
||||
|
||||
.TP
|
||||
.BR \-\-scalecolor " " \fIcolor\fR
|
||||
Color of scale, e.g. "--scalecolor #000000"
|
||||
Color of scale marks and text, e.g. "--scalecolor #000000"
|
||||
|
||||
.TP
|
||||
.BR \-\-playercolor " " \fIcolor\fR
|
||||
@ -38,11 +44,11 @@ Color of origin indicator, e.g. "--origincolor #ff0000"
|
||||
|
||||
.TP
|
||||
.BR \-\-drawscale
|
||||
Draw tick marks
|
||||
Draw scale(s) with tick marks and numbers
|
||||
|
||||
.TP
|
||||
.BR \-\-drawplayers
|
||||
Draw player indicators
|
||||
Draw player indicators with name
|
||||
|
||||
.TP
|
||||
.BR \-\-draworigin
|
||||
@ -50,7 +56,7 @@ Draw origin indicator
|
||||
|
||||
.TP
|
||||
.BR \-\-drawalpha
|
||||
Allow nodes to be drawn with transparency
|
||||
Allow nodes to be drawn with transparency (such as water)
|
||||
|
||||
.TP
|
||||
.BR \-\-noshading
|
||||
@ -58,26 +64,32 @@ Don't draw shading on nodes
|
||||
|
||||
.TP
|
||||
.BR \-\-noemptyimage
|
||||
Don't output anything when the image would be empty.
|
||||
Don't output anything when the image would be empty
|
||||
|
||||
.TP
|
||||
.BR \-\-verbose
|
||||
Enable verbose log output.
|
||||
|
||||
.TP
|
||||
.BR \-\-min-y " " \fInumber\fR
|
||||
Don't draw nodes below this y value, e.g. "--min-y -25"
|
||||
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
|
||||
Use specific map backend; supported: \fIsqlite3\fP, \fIleveldb\fP, \fIredis\fP, \fIpostgresql\fP, e.g. "--backend leveldb"
|
||||
Override auto-detected map backend; supported: \fIsqlite3\fP, \fIleveldb\fP, \fIredis\fP, \fIpostgresql\fP, e.g. "--backend leveldb"
|
||||
|
||||
.TP
|
||||
.BR \-\-geometry " " \fIgeometry\fR
|
||||
Limit area to specific geometry (\fIx:y+w+h\fP where x and y specify the lower left corner), e.g. "--geometry -800:-800+1600+1600"
|
||||
Limit area to specific geometry (\fIx:z+w+h\fP where x and z specify the lower left corner), e.g. "--geometry -800:-800+1600+1600"
|
||||
|
||||
The coordinates are specified with the same axes as in-game. The Z axis becomes Y when projected on the image.
|
||||
|
||||
.TP
|
||||
.BR \-\-extent " " \fIextent\fR
|
||||
.BR \-\-extent
|
||||
Don't render the image, just print the extent of the map that would be generated, in the same format as the geometry above.
|
||||
|
||||
.TP
|
||||
@ -86,25 +98,24 @@ Zoom the image by using more than one pixel per node, e.g. "--zoom 4"
|
||||
|
||||
.TP
|
||||
.BR \-\-colors " " \fIpath\fR
|
||||
Forcefully set path to colors.txt file (it's autodetected otherwise), e.g. "--colors ../minetest/mycolors.txt"
|
||||
Override auto-detected path to colors.txt, e.g. "--colors ../world/mycolors.txt"
|
||||
|
||||
.TP
|
||||
.BR \-\-scales " " \fIedges\fR
|
||||
Draw scales on specified image edges (letters \fIt b l r\fP meaning top, bottom, left and right), e.g. "--scales tbr"
|
||||
|
||||
.TP
|
||||
.BR \-\-exhaustive " \fImode\fR
|
||||
.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, 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.
|
||||
Defaults to \fIauto\fP. You shouldn't need to change this, as minetestmapper tries to automatically picks the best option.
|
||||
|
||||
.TP
|
||||
.BR \-\-dumpblock " " \fIpos\fR
|
||||
Instead of rendering anything try to load the block at the given position (\fIx,y,z\fR) and print its raw data as hexadecimal.
|
||||
|
||||
.SH MORE INFORMATION
|
||||
Website: https://github.com/minetest/minetestmapper
|
||||
Website: https://github.com/luanti-org/minetestmapper
|
||||
|
||||
.SH MAN PAGE AUTHOR
|
||||
Daniel Moerner
|
||||
|
@ -1,17 +1,18 @@
|
||||
#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 int readBlockContent(const unsigned char *mapData, u8 contentWidth, unsigned int datapos)
|
||||
static inline uint16_t readBlockContent(const unsigned char *mapData,
|
||||
u8 contentWidth, unsigned int datapos)
|
||||
{
|
||||
if (contentWidth == 2) {
|
||||
size_t index = datapos << 1;
|
||||
@ -21,7 +22,7 @@ static int readBlockContent(const unsigned char *mapData, u8 contentWidth, unsig
|
||||
if (param <= 0x7f)
|
||||
return param;
|
||||
else
|
||||
return (int(param) << 4) | (int(mapData[datapos + 0x2000]) >> 4);
|
||||
return (param << 4) | (mapData[datapos + 0x2000] >> 4);
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,7 +39,7 @@ void BlockDecoder::reset()
|
||||
|
||||
m_version = 0;
|
||||
m_contentWidth = 0;
|
||||
m_mapData = ustring();
|
||||
m_mapData.clear();
|
||||
}
|
||||
|
||||
void BlockDecoder::decode(const ustring &datastr)
|
||||
@ -48,7 +49,6 @@ 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,12 +56,45 @@ 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 >= 27)
|
||||
if (version >= 29)
|
||||
dataOffset = 7;
|
||||
else if (version >= 27)
|
||||
dataOffset = 4;
|
||||
else
|
||||
dataOffset = 2;
|
||||
|
||||
auto decode_mapping = [&] () {
|
||||
dataOffset++; // mapping version
|
||||
uint16_t numMappings = readU16(data + dataOffset);
|
||||
dataOffset += 2;
|
||||
for (int i = 0; i < numMappings; ++i) {
|
||||
uint16_t nodeId = readU16(data + dataOffset);
|
||||
dataOffset += 2;
|
||||
uint16_t nameLen = readU16(data + dataOffset);
|
||||
dataOffset += 2;
|
||||
std::string name(reinterpret_cast<const char *>(data) + dataOffset, nameLen);
|
||||
if (name == "air")
|
||||
m_blockAirId = nodeId;
|
||||
else if (name == "ignore")
|
||||
m_blockIgnoreId = nodeId;
|
||||
else
|
||||
m_nameMap[nodeId] = name;
|
||||
dataOffset += nameLen;
|
||||
}
|
||||
};
|
||||
|
||||
if (version >= 29)
|
||||
decode_mapping();
|
||||
|
||||
uint8_t contentWidth = data[dataOffset];
|
||||
dataOffset++;
|
||||
uint8_t paramsWidth = data[dataOffset];
|
||||
@ -72,14 +105,20 @@ 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);
|
||||
m_mapData = decompressor.decompress();
|
||||
decompressor.decompress(); // unused metadata
|
||||
decompressor.decompress(m_mapData);
|
||||
decompressor.decompress(m_scratch); // unused metadata
|
||||
dataOffset = decompressor.seekPos();
|
||||
|
||||
// Skip unused data
|
||||
// Skip unused node timers
|
||||
if (version == 23)
|
||||
dataOffset += 1;
|
||||
if (version == 24) {
|
||||
@ -103,33 +142,7 @@ void BlockDecoder::decode(const ustring &datastr)
|
||||
dataOffset += 4; // Skip timestamp
|
||||
|
||||
// Read 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;
|
||||
}
|
||||
decode_mapping();
|
||||
}
|
||||
|
||||
bool BlockDecoder::isEmpty() const
|
||||
@ -138,23 +151,18 @@ bool BlockDecoder::isEmpty() const
|
||||
return m_nameMap.empty();
|
||||
}
|
||||
|
||||
std::string BlockDecoder::getNode(u8 x, u8 y, u8 z) const
|
||||
const static std::string empty;
|
||||
|
||||
const std::string &BlockDecoder::getNode(u8 x, u8 y, u8 z) const
|
||||
{
|
||||
unsigned int position = x + (y << 4) + (z << 8);
|
||||
int content = readBlockContent(m_mapData.c_str(), m_contentWidth, position);
|
||||
uint16_t content = readBlockContent(m_mapData.c_str(), m_contentWidth, position);
|
||||
if (content == m_blockAirId || content == m_blockIgnoreId)
|
||||
return "";
|
||||
return empty;
|
||||
NameMap::const_iterator it = m_nameMap.find(content);
|
||||
if (it == m_nameMap.end()) {
|
||||
std::cerr << "Skipping node with invalid ID." << std::endl;
|
||||
return "";
|
||||
errorstream << "Skipping node with invalid ID." << std::endl;
|
||||
return empty;
|
||||
}
|
||||
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];
|
||||
}
|
29
src/BlockDecoder.h
Normal file
29
src/BlockDecoder.h
Normal file
@ -0,0 +1,29 @@
|
||||
#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;
|
||||
};
|
@ -17,7 +17,7 @@
|
||||
|
||||
// ARGB but with inverted alpha
|
||||
|
||||
static inline int color2int(Color c)
|
||||
static inline int color2int(const Color &c)
|
||||
{
|
||||
u8 a = (255 - c.a) * gdAlphaMax / 255;
|
||||
return (a << 24) | (c.r << 16) | (c.g << 8) | c.b;
|
||||
@ -26,15 +26,15 @@ static inline int color2int(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;
|
||||
a = (c >> 24) & 0xff;
|
||||
u8 a = (c >> 24) & 0xff;
|
||||
c2.a = 255 - (a*255 / gdAlphaMax);
|
||||
return c2;
|
||||
}
|
||||
|
||||
#ifndef NDEBUG
|
||||
static inline void check_bounds(int x, int y, int width, int height)
|
||||
{
|
||||
if(x < 0 || x >= width) {
|
||||
@ -50,11 +50,13 @@ 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(NULL)
|
||||
m_width(width), m_height(height), m_image(nullptr)
|
||||
{
|
||||
SIZECHECK(0, 0);
|
||||
m_image = gdImageCreateTrueColor(m_width, m_height);
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
#ifndef IMAGE_HEADER
|
||||
#define IMAGE_HEADER
|
||||
#pragma once
|
||||
|
||||
#include "types.h"
|
||||
#include <string>
|
||||
@ -9,7 +8,6 @@ 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;
|
||||
};
|
||||
@ -19,6 +17,9 @@ 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);
|
||||
@ -28,10 +29,6 @@ public:
|
||||
void save(const std::string &filename);
|
||||
|
||||
private:
|
||||
Image(const Image&);
|
||||
|
||||
int m_width, m_height;
|
||||
gdImagePtr m_image;
|
||||
};
|
||||
|
||||
#endif // IMAGE_HEADER
|
@ -1,14 +1,6 @@
|
||||
/*
|
||||
* =====================================================================
|
||||
* Version: 1.0
|
||||
* Created: 25.08.2012 10:55:27
|
||||
* Author: Miroslav Bendík
|
||||
* Company: LinuxOS.sk
|
||||
* =====================================================================
|
||||
*/
|
||||
#include <cstring>
|
||||
|
||||
#include "PixelAttributes.h"
|
||||
#include <cstring>
|
||||
|
||||
PixelAttributes::PixelAttributes():
|
||||
m_width(0)
|
44
src/PixelAttributes.h
Normal file
44
src/PixelAttributes.h
Normal file
@ -0,0 +1,44 @@
|
||||
#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;
|
||||
};
|
159
src/PlayerAttributes.cpp
Normal file
159
src/PlayerAttributes.cpp
Normal file
@ -0,0 +1,159 @@
|
||||
#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();
|
||||
}
|
||||
|
23
src/PlayerAttributes.h
Normal file
23
src/PlayerAttributes.h
Normal file
@ -0,0 +1,23 @@
|
||||
#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;
|
||||
};
|
@ -8,13 +8,17 @@
|
||||
#include <stdexcept>
|
||||
#include <cstring>
|
||||
#include <vector>
|
||||
#include <cmath>
|
||||
#include <type_traits>
|
||||
#include <limits>
|
||||
|
||||
#include "TileGenerator.h"
|
||||
#include "config.h"
|
||||
#include "PlayerAttributes.h"
|
||||
#include "BlockDecoder.h"
|
||||
#include "Image.h"
|
||||
#include "util.h"
|
||||
#include "log.h"
|
||||
|
||||
#include "db-sqlite3.h"
|
||||
#if USE_POSTGRESQL
|
||||
#include "db-postgresql.h"
|
||||
@ -26,18 +30,45 @@
|
||||
#include "db-redis.h"
|
||||
#endif
|
||||
|
||||
using namespace std;
|
||||
#ifndef __has_builtin
|
||||
#define __has_builtin(x) 0
|
||||
#endif
|
||||
|
||||
template<typename T>
|
||||
static inline T mymax(T a, T b)
|
||||
// saturating multiplication
|
||||
template<typename T, class = typename std::enable_if<std::is_unsigned<T>::value>::type>
|
||||
inline T sat_mul(T a, T b)
|
||||
{
|
||||
return (a > b) ? a : b;
|
||||
#if __has_builtin(__builtin_mul_overflow)
|
||||
T res;
|
||||
if (__builtin_mul_overflow(a, b, &res))
|
||||
return std::numeric_limits<T>::max();
|
||||
return res;
|
||||
#else
|
||||
// WARNING: the fallback implementation is incorrect since we compute ceil(log(x)) not log(x)
|
||||
// but that's good enough for our usecase...
|
||||
const int bits = sizeof(T) * 8;
|
||||
int hb_a = 0, hb_b = 0;
|
||||
for (int i = bits - 1; i >= 0; i--) {
|
||||
if (a & (static_cast<T>(1) << i)) {
|
||||
hb_a = i; break;
|
||||
}
|
||||
}
|
||||
for (int i = bits - 1; i >= 0; i--) {
|
||||
if (b & (static_cast<T>(1) << i)) {
|
||||
hb_b = i; break;
|
||||
}
|
||||
}
|
||||
// log2(a) + log2(b) >= log2(MAX) <=> calculation will overflow
|
||||
if (hb_a + hb_b >= bits)
|
||||
return std::numeric_limits<T>::max();
|
||||
return a * b;
|
||||
#endif
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static inline T mymin(T a, T b)
|
||||
inline T sat_mul(T a, T b, T c)
|
||||
{
|
||||
return (a > b) ? b : a;
|
||||
return sat_mul(sat_mul(a, b), c);
|
||||
}
|
||||
|
||||
// rounds n (away from 0) to a multiple of f while preserving the sign of n
|
||||
@ -52,16 +83,30 @@ 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;
|
||||
double a1 = a.a / 255.0;
|
||||
double a2 = b.a / 255.0;
|
||||
float a1 = a.a / 255.0f;
|
||||
float a2 = b.a / 255.0f;
|
||||
|
||||
result.r = (int) (a1 * a.r + a2 * (1 - a1) * b.r);
|
||||
result.g = (int) (a1 * a.g + a2 * (1 - a1) * b.g);
|
||||
@ -98,14 +143,19 @@ 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_scales(SCALE_LEFT | SCALE_TOP),
|
||||
m_progressMax(0),
|
||||
m_progressLast(-1)
|
||||
{
|
||||
}
|
||||
|
||||
TileGenerator::~TileGenerator()
|
||||
{
|
||||
closeDatabase();
|
||||
delete m_image;
|
||||
m_image = nullptr;
|
||||
}
|
||||
|
||||
void TileGenerator::setBgColor(const std::string &bgColor)
|
||||
@ -140,21 +190,6 @@ 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;
|
||||
@ -213,22 +248,22 @@ void TileGenerator::setExhaustiveSearch(int mode)
|
||||
m_exhaustiveSearch = mode;
|
||||
}
|
||||
|
||||
void TileGenerator::setDontWriteEmpty(bool f)
|
||||
{
|
||||
m_dontWriteEmpty = f;
|
||||
}
|
||||
|
||||
void TileGenerator::parseColorsFile(const std::string &fileName)
|
||||
{
|
||||
ifstream in;
|
||||
in.open(fileName.c_str(), ifstream::in);
|
||||
if (!in.is_open())
|
||||
std::ifstream in(fileName);
|
||||
if (!in.good())
|
||||
throw std::runtime_error("Specified colors file could not be found");
|
||||
verbosestream << "Parsing colors.txt: " << fileName << std::endl;
|
||||
parseColorsStream(in);
|
||||
}
|
||||
|
||||
void TileGenerator::printGeometry(const std::string &input)
|
||||
void TileGenerator::printGeometry(const std::string &input_path)
|
||||
{
|
||||
string input_path = input;
|
||||
if (input_path[input.length() - 1] != PATH_SEPARATOR) {
|
||||
input_path += PATH_SEPARATOR;
|
||||
}
|
||||
|
||||
setExhaustiveSearch(EXH_NEVER);
|
||||
openDb(input_path);
|
||||
loadBlocks();
|
||||
@ -240,34 +275,48 @@ void TileGenerator::printGeometry(const std::string &input)
|
||||
<< std::endl;
|
||||
|
||||
closeDatabase();
|
||||
|
||||
}
|
||||
|
||||
void TileGenerator::setDontWriteEmpty(bool f)
|
||||
void TileGenerator::dumpBlock(const std::string &input_path, BlockPos pos)
|
||||
{
|
||||
m_dontWriteEmpty = f;
|
||||
}
|
||||
openDb(input_path);
|
||||
|
||||
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;
|
||||
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");
|
||||
}
|
||||
|
||||
if (m_dontWriteEmpty) // FIXME: possible too, just needs to be done differently
|
||||
setExhaustiveSearch(EXH_NEVER);
|
||||
closeDatabase();
|
||||
}
|
||||
|
||||
void TileGenerator::generate(const std::string &input_path, const std::string &output)
|
||||
{
|
||||
openDb(input_path);
|
||||
loadBlocks();
|
||||
|
||||
if (m_dontWriteEmpty && m_positions.empty())
|
||||
{
|
||||
closeDatabase();
|
||||
// If we needed to load positions and there are none, that means the
|
||||
// result will be empty.
|
||||
if (m_dontWriteEmpty && (m_exhaustiveSearch == EXH_NEVER ||
|
||||
m_exhaustiveSearch == EXH_Y) && m_positions.empty()) {
|
||||
verbosestream << "Result is empty (no positions)" << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
createImage();
|
||||
renderMap();
|
||||
|
||||
if (m_dontWriteEmpty && !m_renderedAny) {
|
||||
verbosestream << "Result is empty (no pixels)" << std::endl;
|
||||
printUnknown();
|
||||
return;
|
||||
}
|
||||
|
||||
closeDatabase();
|
||||
if (m_drawScale) {
|
||||
renderScale();
|
||||
@ -288,27 +337,24 @@ 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(strlen(line) == 0)
|
||||
if(!line[0])
|
||||
continue;
|
||||
|
||||
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;
|
||||
char name[200 + 1] = {0};
|
||||
unsigned int r, g, b, a = 255, t = 0;
|
||||
int items = sscanf(line, "%200s %u %u %u %u %u", name, &r, &g, &b, &a, &t);
|
||||
if (items < 4) {
|
||||
errorstream << "Failed to parse color entry '" << line << "'" << std::endl;
|
||||
continue;
|
||||
}
|
||||
|
||||
ColorEntry color(r, g, b, a, t);
|
||||
m_colorMap[name] = color;
|
||||
|
||||
m_colorMap[name] = ColorEntry(r, g, b, a, t);
|
||||
}
|
||||
}
|
||||
|
||||
@ -328,44 +374,61 @@ std::set<std::string> TileGenerator::getSupportedBackends()
|
||||
return r;
|
||||
}
|
||||
|
||||
void TileGenerator::openDb(const std::string &input)
|
||||
void TileGenerator::openDb(const std::string &input_path)
|
||||
{
|
||||
std::string backend = m_backend;
|
||||
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 (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");
|
||||
}
|
||||
|
||||
if(backend == "sqlite3")
|
||||
std::string input = input_path;
|
||||
if (input.back() != PATH_SEPARATOR)
|
||||
input += PATH_SEPARATOR;
|
||||
|
||||
std::ifstream ifs(input + "world.mt");
|
||||
|
||||
std::string backend = m_backend;
|
||||
if (backend.empty() && !ifs.good()) {
|
||||
throw std::runtime_error("Failed to open world.mt");
|
||||
} else if (backend.empty()) {
|
||||
backend = read_setting_default("backend", ifs, "sqlite3");
|
||||
}
|
||||
|
||||
if (backend == "dummy") {
|
||||
throw std::runtime_error("This map uses the dummy backend and contains no data");
|
||||
} else if (backend == "sqlite3") {
|
||||
m_db = new DBSQLite3(input);
|
||||
#if USE_POSTGRESQL
|
||||
else if(backend == "postgresql")
|
||||
} 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);
|
||||
} else {
|
||||
throw std::runtime_error(std::string("Unknown map backend: ") + backend);
|
||||
}
|
||||
|
||||
if (!read_setting_default("readonly_backend", ifs, "").empty()) {
|
||||
errorstream << "Warning: Map with readonly_backend is not supported. "
|
||||
"The result may be incomplete." << std::endl;
|
||||
}
|
||||
|
||||
// Determine how we're going to traverse the database (heuristic)
|
||||
if (m_exhaustiveSearch == EXH_AUTO) {
|
||||
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:"
|
||||
size_t y_range = (m_yMax / 16 + 1) - (m_yMin / 16);
|
||||
size_t blocks = sat_mul<size_t>(m_geomX2 - m_geomX, y_range, m_geomY2 - m_geomY);
|
||||
verbosestream << "Heuristic parameters:"
|
||||
<< " preferRangeQueries()=" << m_db->preferRangeQueries()
|
||||
<< " y_range=" << y_range << " blocks=" << blocks << std::endl;
|
||||
#endif
|
||||
if (m_db->preferRangeQueries())
|
||||
m_exhaustiveSearch = EXH_NEVER;
|
||||
else if (blocks < 200000)
|
||||
@ -376,9 +439,9 @@ void TileGenerator::openDb(const std::string &input)
|
||||
m_exhaustiveSearch = EXH_NEVER;
|
||||
} else if (m_exhaustiveSearch == EXH_FULL || m_exhaustiveSearch == EXH_Y) {
|
||||
if (m_db->preferRangeQueries()) {
|
||||
std::cerr << "Note: The current database backend supports efficient "
|
||||
"range queries, forcing exhaustive search should always result "
|
||||
" in worse performance." << std::endl;
|
||||
errorstream << "Note: The current database backend supports efficient "
|
||||
"range queries, forcing exhaustive search will generally result "
|
||||
"in worse performance." << std::endl;
|
||||
}
|
||||
}
|
||||
assert(m_exhaustiveSearch != EXH_AUTO);
|
||||
@ -390,42 +453,43 @@ 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 = m_yMax / 16 + 1;
|
||||
const int16_t yMax = mod16(m_yMax) + 1;
|
||||
const int16_t yMin = mod16(m_yMin);
|
||||
|
||||
if (m_exhaustiveSearch == EXH_NEVER || m_exhaustiveSearch == EXH_Y) {
|
||||
std::vector<BlockPos> vec = m_db->getBlockPos(
|
||||
BlockPos(m_geomX, m_yMin / 16, m_geomY),
|
||||
std::vector<BlockPos> vec = m_db->getBlockPosXZ(
|
||||
BlockPos(m_geomX, yMin, m_geomY),
|
||||
BlockPos(m_geomX2, yMax, m_geomY2)
|
||||
);
|
||||
|
||||
for (auto pos : vec) {
|
||||
assert(pos.x >= m_geomX && pos.x < m_geomX2);
|
||||
assert(pos.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
|
||||
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_xMin = mymin<int>(m_xMin, pos.x);
|
||||
m_xMax = mymax<int>(m_xMax, pos.x);
|
||||
m_zMin = mymin<int>(m_zMin, pos.z);
|
||||
m_zMax = mymax<int>(m_zMax, pos.z);
|
||||
|
||||
m_positions[pos.z].emplace(pos.x);
|
||||
}
|
||||
|
||||
#ifndef NDEBUG
|
||||
int count = 0;
|
||||
size_t count = 0;
|
||||
for (const auto &it : m_positions)
|
||||
count += it.second.size();
|
||||
std::cout << "Loaded " << count
|
||||
m_progressMax = count;
|
||||
verbosestream << "Loaded " << count
|
||||
<< " positions (across Z: " << m_positions.size() << ") for rendering" << std::endl;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@ -435,7 +499,6 @@ 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.
|
||||
@ -465,8 +528,11 @@ void TileGenerator::createImage()
|
||||
image_height += (m_scales & SCALE_BOTTOM) ? scale_d : 0;
|
||||
|
||||
if(image_width > 4096 || image_height > 4096) {
|
||||
std::cerr << "Warning: The width or height of the image to be created exceeds 4096 pixels!"
|
||||
<< " (Dimensions: " << image_width << "x" << image_height << ")"
|
||||
errorstream << "Warning: The side length of the image to be created exceeds 4096 pixels!"
|
||||
<< " (dimensions: " << image_width << "x" << image_height << ")"
|
||||
<< std::endl;
|
||||
} else {
|
||||
verbosestream << "Creating image with size " << image_width << "x" << image_height
|
||||
<< std::endl;
|
||||
}
|
||||
m_image = new Image(image_width, image_height);
|
||||
@ -476,7 +542,9 @@ void TileGenerator::createImage()
|
||||
void TileGenerator::renderMap()
|
||||
{
|
||||
BlockDecoder blk;
|
||||
const int16_t yMax = m_yMax / 16 + 1;
|
||||
const int16_t yMax = mod16(m_yMax) + 1;
|
||||
const int16_t yMin = mod16(m_yMin);
|
||||
size_t count = 0;
|
||||
|
||||
auto renderSingle = [&] (int16_t xPos, int16_t zPos, BlockList &blockStack) {
|
||||
m_readPixels.reset();
|
||||
@ -492,10 +560,12 @@ void TileGenerator::renderMap()
|
||||
for (const auto &it : blockStack) {
|
||||
const BlockPos pos = it.first;
|
||||
assert(pos.x == xPos && pos.z == zPos);
|
||||
assert(pos.y >= m_yMin / 16 && pos.y < yMax);
|
||||
assert(pos.y >= yMin && pos.y < yMax);
|
||||
|
||||
blk.reset();
|
||||
blk.decode(it.second);
|
||||
if (blk.isEmpty())
|
||||
continue;
|
||||
renderMapBlock(blk, pos);
|
||||
|
||||
// Exit out if all pixels for this MapBlock are covered
|
||||
@ -504,6 +574,7 @@ void TileGenerator::renderMap()
|
||||
}
|
||||
if (!m_readPixels.full())
|
||||
renderMapBlockBottom(blockStack.begin()->first);
|
||||
m_renderedAny |= m_readInfo.any();
|
||||
};
|
||||
auto postRenderRow = [&] (int16_t zPos) {
|
||||
if (m_shading)
|
||||
@ -517,27 +588,26 @@ void TileGenerator::renderMap()
|
||||
int16_t xPos = *it2;
|
||||
|
||||
BlockList blockStack;
|
||||
m_db->getBlocksOnXZ(blockStack, xPos, zPos, m_yMin / 16, yMax);
|
||||
m_db->getBlocksOnXZ(blockStack, xPos, zPos, yMin, yMax);
|
||||
blockStack.sort();
|
||||
|
||||
renderSingle(xPos, zPos, blockStack);
|
||||
reportProgress(count++);
|
||||
}
|
||||
postRenderRow(zPos);
|
||||
}
|
||||
} else if (m_exhaustiveSearch == EXH_Y) {
|
||||
#ifndef NDEBUG
|
||||
std::cerr << "Exhaustively searching height of "
|
||||
<< (yMax - (m_yMin / 16)) << " blocks" << std::endl;
|
||||
#endif
|
||||
verbosestream << "Exhaustively searching height of "
|
||||
<< (yMax - yMin) << " blocks" << std::endl;
|
||||
std::vector<BlockPos> positions;
|
||||
positions.reserve(yMax - (m_yMin / 16));
|
||||
positions.reserve(yMax - yMin);
|
||||
for (auto it = m_positions.rbegin(); it != m_positions.rend(); ++it) {
|
||||
int16_t zPos = it->first;
|
||||
for (auto it2 = it->second.rbegin(); it2 != it->second.rend(); ++it2) {
|
||||
int16_t xPos = *it2;
|
||||
|
||||
positions.clear();
|
||||
for (int16_t yPos = m_yMin / 16; yPos < yMax; yPos++)
|
||||
for (int16_t yPos = yMin; yPos < yMax; yPos++)
|
||||
positions.emplace_back(xPos, yPos, zPos);
|
||||
|
||||
BlockList blockStack;
|
||||
@ -545,21 +615,23 @@ void TileGenerator::renderMap()
|
||||
blockStack.sort();
|
||||
|
||||
renderSingle(xPos, zPos, blockStack);
|
||||
reportProgress(count++);
|
||||
}
|
||||
postRenderRow(zPos);
|
||||
}
|
||||
} else if (m_exhaustiveSearch == EXH_FULL) {
|
||||
#ifndef NDEBUG
|
||||
std::cerr << "Exhaustively searching "
|
||||
<< (m_geomX2 - m_geomX) << "x" << (yMax - (m_yMin / 16)) << "x"
|
||||
const size_t span_y = yMax - yMin;
|
||||
m_progressMax = (m_geomX2 - m_geomX) * span_y * (m_geomY2 - m_geomY);
|
||||
verbosestream << "Exhaustively searching "
|
||||
<< (m_geomX2 - m_geomX) << "x" << span_y << "x"
|
||||
<< (m_geomY2 - m_geomY) << " blocks" << std::endl;
|
||||
#endif
|
||||
|
||||
std::vector<BlockPos> positions;
|
||||
positions.reserve(yMax - (m_yMin / 16));
|
||||
positions.reserve(span_y);
|
||||
for (int16_t zPos = m_geomY2 - 1; zPos >= m_geomY; zPos--) {
|
||||
for (int16_t xPos = m_geomX2 - 1; xPos >= m_geomX; xPos--) {
|
||||
positions.clear();
|
||||
for (int16_t yPos = m_yMin / 16; yPos < yMax; yPos++)
|
||||
for (int16_t yPos = yMin; yPos < yMax; yPos++)
|
||||
positions.emplace_back(xPos, yPos, zPos);
|
||||
|
||||
BlockList blockStack;
|
||||
@ -567,36 +639,17 @@ 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;
|
||||
@ -607,50 +660,43 @@ 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) {
|
||||
string name = blk.getNode(x, y, z);
|
||||
if (name == "") {
|
||||
if (y == 0) m_light[z][x] = light_at(x, 0, z);
|
||||
const std::string &name = blk.getNode(x, y, z);
|
||||
if (name.empty())
|
||||
continue;
|
||||
}
|
||||
ColorMap::const_iterator it = m_colorMap.find(name);
|
||||
if (it == m_colorMap.end()) {
|
||||
m_unknownNodes.insert(name);
|
||||
continue;
|
||||
}
|
||||
Color c = it->second.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);
|
||||
|
||||
Color c = it->second.toColor();
|
||||
if (c.a == 0)
|
||||
continue; // node is fully invisible
|
||||
if (m_drawAlpha) {
|
||||
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;
|
||||
if (m_color[z][x].a != 0)
|
||||
c = mixColors(m_color[z][x], c);
|
||||
if (c.a < 255) {
|
||||
// remember color and near thickness value
|
||||
m_color[z][x] = c;
|
||||
m_thickness[z][x] = (m_thickness[z][x] + it->second.t) / 2;
|
||||
continue;
|
||||
}
|
||||
// color became opaque, draw it
|
||||
setZoomed(imageX, imageY, m_color[z][x]);
|
||||
m_blockPixelAttributes.attribute(15 - z, xBegin + x).thickness = m_thickness[z][x];
|
||||
setZoomed(imageX, imageY, c);
|
||||
attr.thickness = m_thickness[z][x];
|
||||
} else {
|
||||
setZoomed(imageX, imageY, c.noAlpha());
|
||||
c.a = 255;
|
||||
setZoomed(imageX, imageY, c);
|
||||
}
|
||||
m_readPixels.set(x, z);
|
||||
|
||||
// do this afterwards so we can record height values
|
||||
// inside transparent nodes (water) too
|
||||
if (!m_readInfo.get(x, z)) {
|
||||
m_blockPixelAttributes.attribute(15 - z, xBegin + x).height = pos.y * 16 + y;
|
||||
attr.height = pos.y * 16 + y;
|
||||
m_readInfo.set(x, z);
|
||||
}
|
||||
break;
|
||||
@ -672,17 +718,19 @@ 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);
|
||||
m_blockPixelAttributes.attribute(15 - z, xBegin + x).thickness = m_thickness[z][x];
|
||||
attr.thickness = m_thickness[z][x];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TileGenerator::renderShading(int zPos)
|
||||
{
|
||||
auto &a = m_blockPixelAttributes;
|
||||
int zBegin = (m_zMax - zPos) * 16;
|
||||
for (int z = 0; z < 16; ++z) {
|
||||
int imageY = zBegin + z;
|
||||
@ -690,23 +738,27 @@ void TileGenerator::renderShading(int zPos)
|
||||
continue;
|
||||
for (int x = 0; x < m_mapWidth; ++x) {
|
||||
if(
|
||||
!m_blockPixelAttributes.attribute(z, x).valid_height() ||
|
||||
!m_blockPixelAttributes.attribute(z, x - 1).valid_height() ||
|
||||
!m_blockPixelAttributes.attribute(z - 1, x).valid_height()
|
||||
!a.attribute(z, x).valid_height() ||
|
||||
!a.attribute(z, x - 1).valid_height() ||
|
||||
!a.attribute(z - 1, x).valid_height()
|
||||
)
|
||||
continue;
|
||||
|
||||
// calculate shadow to apply
|
||||
int y = 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 y = a.attribute(z, x).height;
|
||||
int y1 = a.attribute(z, x - 1).height;
|
||||
int y2 = a.attribute(z - 1, x).height;
|
||||
int d = ((y - y1) + (y - y2)) * 12;
|
||||
|
||||
if (m_drawAlpha) { // less visible shadow with increasing "thickness"
|
||||
double t = m_blockPixelAttributes.attribute(z, x).thickness * 1.2;
|
||||
d *= 1.0 - mymin(t, 255.0) / 255.0;
|
||||
float t = a.attribute(z, x).thickness * 1.2f;
|
||||
t = mymin(t, 255.0f);
|
||||
d *= 1.0f - t / 255.0f;
|
||||
}
|
||||
|
||||
d = mymin(d, 36);
|
||||
|
||||
// apply shadow/light by just adding to it pixel values
|
||||
Color c = m_image->getPixel(getImageX(x), getImageY(imageY));
|
||||
c.r = colorSafeBounds(c.r + d);
|
||||
c.g = colorSafeBounds(c.g + d);
|
||||
@ -714,7 +766,7 @@ void TileGenerator::renderShading(int zPos)
|
||||
setZoomed(x, imageY, c);
|
||||
}
|
||||
}
|
||||
m_blockPixelAttributes.scroll();
|
||||
a.scroll();
|
||||
}
|
||||
|
||||
void TileGenerator::renderScale()
|
||||
@ -792,12 +844,16 @@ void TileGenerator::renderOrigin()
|
||||
m_image->drawCircle(getImageX(0, true), getImageY(0, true), 12, m_originColor);
|
||||
}
|
||||
|
||||
void TileGenerator::renderPlayers(const std::string &inputPath)
|
||||
void TileGenerator::renderPlayers(const std::string &input_path)
|
||||
{
|
||||
PlayerAttributes players(inputPath);
|
||||
std::string input = input_path;
|
||||
if (input.back() != PATH_SEPARATOR)
|
||||
input += PATH_SEPARATOR;
|
||||
|
||||
PlayerAttributes players(input);
|
||||
for (auto &player : players) {
|
||||
if (player.x < m_xMin * 16 || player.x > m_xMax * 16 ||
|
||||
player.z < m_zMin * 16 || player.z > m_zMax * 16)
|
||||
if (player.x < m_xMin * 16 || player.x >= (m_xMax+1) * 16 ||
|
||||
player.z < m_zMin * 16 || player.z >= (m_zMax+1) * 16)
|
||||
continue;
|
||||
if (player.y < m_yMin || player.y > m_yMax)
|
||||
continue;
|
||||
@ -806,6 +862,7 @@ void TileGenerator::renderPlayers(const std::string &inputPath)
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -814,16 +871,43 @@ void TileGenerator::writeImage(const std::string &output)
|
||||
{
|
||||
m_image->save(output);
|
||||
delete m_image;
|
||||
m_image = NULL;
|
||||
m_image = nullptr;
|
||||
}
|
||||
|
||||
void TileGenerator::printUnknown()
|
||||
{
|
||||
if (m_unknownNodes.size() == 0)
|
||||
if (m_unknownNodes.empty())
|
||||
return;
|
||||
std::cerr << "Unknown nodes:" << std::endl;
|
||||
errorstream << "Unknown nodes:\n";
|
||||
for (const auto &node : m_unknownNodes)
|
||||
std::cerr << "\t" << node << std::endl;
|
||||
errorstream << "\t" << node << '\n';
|
||||
if (!m_renderedAny) {
|
||||
errorstream << "The map was read successfully and not empty, but none of the "
|
||||
"encountered nodes had a color associated.\nCheck that you're using "
|
||||
"the right colors.txt. It should match the game you have installed.\n";
|
||||
}
|
||||
errorstream << std::flush;
|
||||
}
|
||||
|
||||
void TileGenerator::reportProgress(size_t count)
|
||||
{
|
||||
if (!m_progressMax)
|
||||
return;
|
||||
int percent = count / static_cast<float>(m_progressMax) * 100;
|
||||
if (percent == m_progressLast)
|
||||
return;
|
||||
m_progressLast = percent;
|
||||
|
||||
// Print a nice-looking ASCII progress bar
|
||||
char bar[51] = {0};
|
||||
memset(bar, ' ', 50);
|
||||
int i = 0, j = percent;
|
||||
for (; j >= 2; j -= 2)
|
||||
bar[i++] = '=';
|
||||
if (j)
|
||||
bar[i++] = '-';
|
||||
std::cout << "[" << bar << "] " << percent << "% " << (percent == 100 ? "\n" : "\r");
|
||||
std::cout.flush();
|
||||
}
|
||||
|
||||
inline int TileGenerator::getImageX(int val, bool absolute) const
|
@ -1,21 +1,20 @@
|
||||
#ifndef TILEGENERATOR_HEADER
|
||||
#define TILEGENERATOR_HEADER
|
||||
#pragma once
|
||||
|
||||
#include <iosfwd>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <config.h>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <stdint.h>
|
||||
#include <cstdint>
|
||||
#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),
|
||||
@ -31,10 +30,12 @@ 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 to_color() const { return Color(r, g, b, a); }
|
||||
uint8_t r, g, b, a, t;
|
||||
ColorEntry() : r(0), g(0), b(0), a(0), t(0) {};
|
||||
ColorEntry(uint8_t r, uint8_t g, uint8_t b, uint8_t a, uint8_t t) :
|
||||
r(r), g(g), b(b), a(a), t(t) {};
|
||||
inline Color toColor() const { return Color(r, g, b, a); }
|
||||
uint8_t r, g, b, a; // Red, Green, Blue, Alpha
|
||||
uint8_t t; // "thickness" value
|
||||
};
|
||||
|
||||
struct BitmapThing { // 16x16 bitmap
|
||||
@ -42,17 +43,19 @@ struct BitmapThing { // 16x16 bitmap
|
||||
for (int i = 0; i < 16; ++i)
|
||||
val[i] = 0;
|
||||
}
|
||||
inline bool full() const {
|
||||
inline bool any_neq(uint16_t v) const {
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
if (val[i] != 0xffff)
|
||||
return false;
|
||||
if (val[i] != v)
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
inline bool any() const { return any_neq(0); }
|
||||
inline bool full() const { return !any_neq(0xffff); }
|
||||
inline void set(unsigned int x, unsigned int z) {
|
||||
val[z] |= (1 << x);
|
||||
}
|
||||
inline bool get(unsigned int x, unsigned int z) {
|
||||
inline bool get(unsigned int x, unsigned int z) const {
|
||||
return !!(val[z] & (1 << x));
|
||||
}
|
||||
|
||||
@ -64,7 +67,6 @@ class TileGenerator
|
||||
{
|
||||
private:
|
||||
typedef std::unordered_map<std::string, ColorEntry> ColorMap;
|
||||
typedef std::unordered_set<std::string> NameSet;
|
||||
|
||||
public:
|
||||
TileGenerator();
|
||||
@ -73,7 +75,6 @@ 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);
|
||||
@ -91,6 +92,8 @@ 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:
|
||||
@ -108,6 +111,7 @@ 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);
|
||||
@ -142,20 +146,22 @@ 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;
|
||||
}; // class TileGenerator
|
||||
|
||||
#endif // TILEGENERATOR_HEADER
|
||||
size_t m_progressMax;
|
||||
int m_progressLast; // percentage
|
||||
}; // class TileGenerator
|
73
src/ZlibDecompressor.cpp
Normal file
73
src/ZlibDecompressor.cpp
Normal file
@ -0,0 +1,73 @@
|
||||
#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);
|
||||
}
|
||||
|
22
src/ZlibDecompressor.h
Normal file
22
src/ZlibDecompressor.h
Normal file
@ -0,0 +1,22 @@
|
||||
#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;
|
||||
};
|
52
src/ZstdDecompressor.cpp
Normal file
52
src/ZstdDecompressor.cpp
Normal file
@ -0,0 +1,52 @@
|
||||
#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);
|
||||
}
|
23
src/ZstdDecompressor.h
Normal file
23
src/ZstdDecompressor.h
Normal file
@ -0,0 +1,23 @@
|
||||
#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;
|
||||
};
|
@ -6,6 +6,7 @@
|
||||
#cmakedefine01 USE_POSTGRESQL
|
||||
#cmakedefine01 USE_LEVELDB
|
||||
#cmakedefine01 USE_REDIS
|
||||
#cmakedefine01 USE_ZLIB_NG
|
||||
|
||||
#define SHAREDIR "@SHAREDIR@"
|
||||
|
7
src/config.h
Normal file
7
src/config.h
Normal file
@ -0,0 +1,7 @@
|
||||
#if defined(MSDOS) || defined(__OS2__) || defined(__NT__) || defined(_WIN32)
|
||||
#define PATH_SEPARATOR '\\'
|
||||
#else
|
||||
#define PATH_SEPARATOR '/'
|
||||
#endif
|
||||
|
||||
#include "cmake_config.h"
|
@ -1,11 +1,12 @@
|
||||
#include <stdexcept>
|
||||
#include <sstream>
|
||||
#include <algorithm>
|
||||
#include "db-leveldb.h"
|
||||
#include "types.h"
|
||||
|
||||
static inline int64_t stoi64(const std::string &s)
|
||||
{
|
||||
std::stringstream tmp(s);
|
||||
std::istringstream tmp(s);
|
||||
int64_t t;
|
||||
tmp >> t;
|
||||
return t;
|
||||
@ -18,6 +19,12 @@ 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)
|
||||
{
|
||||
@ -25,7 +32,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
|
||||
@ -41,18 +48,24 @@ DBLevelDB::~DBLevelDB()
|
||||
}
|
||||
|
||||
|
||||
std::vector<BlockPos> DBLevelDB::getBlockPos(BlockPos min, BlockPos max)
|
||||
std::vector<BlockPos> DBLevelDB::getBlockPosXZ(BlockPos min, BlockPos max)
|
||||
{
|
||||
std::vector<BlockPos> res;
|
||||
for (const auto &it : posCache) {
|
||||
if (it.first < min.z || it.first >= max.z)
|
||||
const int16_t zpos = it.first;
|
||||
if (zpos < min.z || zpos >= max.z)
|
||||
continue;
|
||||
for (auto pos2 : it.second) {
|
||||
if (pos2.first < min.x || pos2.first >= max.x)
|
||||
auto it2 = lower_bound_x(it.second, min.x);
|
||||
for (; it2 != it.second.end(); it2++) {
|
||||
const auto &pos2 = *it2;
|
||||
if (pos2.x >= max.x)
|
||||
break; // went past
|
||||
if (pos2.y < min.y || pos2.y >= max.y)
|
||||
continue;
|
||||
if (pos2.second < min.y || pos2.second >= max.y)
|
||||
// skip duplicates
|
||||
if (!res.empty() && res.back().x == pos2.x && res.back().z == zpos)
|
||||
continue;
|
||||
res.emplace_back(pos2.first, pos2.second, it.first);
|
||||
res.emplace_back(pos2.x, pos2.y, zpos);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
@ -61,7 +74,7 @@ std::vector<BlockPos> DBLevelDB::getBlockPos(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);
|
||||
@ -69,6 +82,9 @@ 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());
|
||||
}
|
||||
|
||||
|
||||
@ -81,13 +97,18 @@ void DBLevelDB::getBlocksOnXZ(BlockList &blocks, int16_t x, int16_t z,
|
||||
auto it = posCache.find(z);
|
||||
if (it == posCache.cend())
|
||||
return;
|
||||
for (auto pos2 : it->second) {
|
||||
if (pos2.first != x)
|
||||
continue;
|
||||
if (pos2.second < min_y || pos2.second >= max_y)
|
||||
auto it2 = lower_bound_x(it->second, x);
|
||||
if (it2 == it->second.end() || it2->x != x)
|
||||
return;
|
||||
// it2 is now pointing to a contigous part where it2->x == x
|
||||
for (; it2 != it->second.end(); it2++) {
|
||||
const auto &pos2 = *it2;
|
||||
if (pos2.x != x)
|
||||
break; // went past
|
||||
if (pos2.y < min_y || pos2.y >= max_y)
|
||||
continue;
|
||||
|
||||
BlockPos pos(x, pos2.second, z);
|
||||
BlockPos pos(x, pos2.y, z);
|
||||
status = db->Get(leveldb::ReadOptions(), i64tos(encodeBlockPos(pos)), &datastr);
|
||||
if (status.ok()) {
|
||||
blocks.emplace_back(
|
@ -1,5 +1,4 @@
|
||||
#ifndef DB_LEVELDB_HEADER
|
||||
#define DB_LEVELDB_HEADER
|
||||
#pragma once
|
||||
|
||||
#include "db.h"
|
||||
#include <unordered_map>
|
||||
@ -9,7 +8,7 @@
|
||||
class DBLevelDB : public DB {
|
||||
public:
|
||||
DBLevelDB(const std::string &mapdir);
|
||||
std::vector<BlockPos> getBlockPos(BlockPos min, BlockPos max) override;
|
||||
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,
|
||||
@ -19,13 +18,24 @@ public:
|
||||
bool preferRangeQueries() const override { return false; }
|
||||
|
||||
private:
|
||||
using pos2d = std::pair<int16_t, int16_t>;
|
||||
struct vec2 {
|
||||
int16_t x, y;
|
||||
constexpr vec2() : x(0), y(0) {}
|
||||
constexpr vec2(int16_t x, int16_t y) : x(x), y(y) {}
|
||||
|
||||
inline bool operator<(const vec2 &p) const
|
||||
{
|
||||
if (x < p.x)
|
||||
return true;
|
||||
if (x > p.x)
|
||||
return false;
|
||||
return y < p.y;
|
||||
}
|
||||
};
|
||||
|
||||
void loadPosCache();
|
||||
|
||||
// indexed by Z, contains all (x,y) position pairs
|
||||
std::unordered_map<int16_t, std::vector<pos2d>> posCache;
|
||||
leveldb::DB *db;
|
||||
std::unordered_map<int16_t, std::vector<vec2>> posCache;
|
||||
leveldb::DB *db = NULL;
|
||||
};
|
||||
|
||||
#endif // DB_LEVELDB_HEADER
|
@ -3,34 +3,88 @@
|
||||
#include <fstream>
|
||||
#include <cstdlib>
|
||||
#include <arpa/inet.h>
|
||||
|
||||
#include "db-postgresql.h"
|
||||
#include "util.h"
|
||||
#include "log.h"
|
||||
#include "types.h"
|
||||
|
||||
#define ARRLEN(x) (sizeof(x) / sizeof((x)[0]))
|
||||
/* PostgreSQLBase */
|
||||
|
||||
DBPostgreSQL::DBPostgreSQL(const std::string &mapdir)
|
||||
PostgreSQLBase::~PostgreSQLBase()
|
||||
{
|
||||
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());
|
||||
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: ") +
|
||||
throw std::runtime_error(std::string("PostgreSQL database error: ") +
|
||||
PQerrorMessage(db)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PGresult *PostgreSQLBase::checkResults(PGresult *res, bool clear)
|
||||
{
|
||||
ExecStatusType statusType = PQresultStatus(res);
|
||||
|
||||
switch (statusType) {
|
||||
case PGRES_COMMAND_OK:
|
||||
case PGRES_TUPLES_OK:
|
||||
break;
|
||||
case PGRES_FATAL_ERROR:
|
||||
throw std::runtime_error(
|
||||
std::string("PostgreSQL database error: ") +
|
||||
PQresultErrorMessage(res)
|
||||
);
|
||||
default:
|
||||
throw std::runtime_error(
|
||||
std::string("Unhandled PostgreSQL result code ") +
|
||||
std::to_string(statusType)
|
||||
);
|
||||
}
|
||||
|
||||
if (clear)
|
||||
PQclear(res);
|
||||
return res;
|
||||
}
|
||||
|
||||
PGresult *PostgreSQLBase::execPrepared(
|
||||
const char *stmtName, const int paramsNumber,
|
||||
const void **params,
|
||||
const int *paramsLengths, const int *paramsFormats,
|
||||
bool clear)
|
||||
{
|
||||
return checkResults(PQexecPrepared(db, stmtName, paramsNumber,
|
||||
(const char* const*) params, paramsLengths, paramsFormats,
|
||||
1 /* binary output */), clear
|
||||
);
|
||||
}
|
||||
|
||||
/* DBPostgreSQL */
|
||||
|
||||
DBPostgreSQL::DBPostgreSQL(const std::string &mapdir)
|
||||
{
|
||||
std::ifstream ifs(mapdir + "world.mt");
|
||||
if (!ifs.good())
|
||||
throw std::runtime_error("Failed to read world.mt");
|
||||
std::string connect_string = read_setting("pgsql_connection", ifs);
|
||||
ifs.close();
|
||||
|
||||
openDatabase(connect_string.c_str());
|
||||
|
||||
prepareStatement(
|
||||
"get_block_pos",
|
||||
"SELECT posX::int4, posY::int4, posZ::int4 FROM blocks WHERE"
|
||||
"SELECT posX::int4, posZ::int4 FROM blocks WHERE"
|
||||
" (posX BETWEEN $1::int4 AND $2::int4) AND"
|
||||
" (posY BETWEEN $3::int4 AND $4::int4) AND"
|
||||
" (posZ BETWEEN $5::int4 AND $6::int4)"
|
||||
" (posZ BETWEEN $5::int4 AND $6::int4) GROUP BY posX, posZ"
|
||||
);
|
||||
prepareStatement(
|
||||
"get_blocks",
|
||||
@ -54,13 +108,12 @@ DBPostgreSQL::~DBPostgreSQL()
|
||||
try {
|
||||
checkResults(PQexec(db, "COMMIT;"));
|
||||
} catch (const std::exception& caught) {
|
||||
std::cerr << "could not finalize: " << caught.what() << std::endl;
|
||||
errorstream << "could not finalize: " << caught.what() << std::endl;
|
||||
}
|
||||
PQfinish(db);
|
||||
}
|
||||
|
||||
|
||||
std::vector<BlockPos> DBPostgreSQL::getBlockPos(BlockPos min, BlockPos max)
|
||||
std::vector<BlockPos> DBPostgreSQL::getBlockPosXZ(BlockPos min, BlockPos max)
|
||||
{
|
||||
int32_t const x1 = htonl(min.x);
|
||||
int32_t const x2 = htonl(max.x - 1);
|
||||
@ -83,11 +136,14 @@ std::vector<BlockPos> DBPostgreSQL::getBlockPos(BlockPos min, BlockPos max)
|
||||
std::vector<BlockPos> positions;
|
||||
positions.reserve(numrows);
|
||||
|
||||
for (int row = 0; row < numrows; ++row)
|
||||
positions.emplace_back(pg_to_blockpos(results, row, 0));
|
||||
BlockPos pos;
|
||||
for (int row = 0; row < numrows; ++row) {
|
||||
pos.x = pg_binary_to_int(results, row, 0);
|
||||
pos.z = pg_binary_to_int(results, row, 1);
|
||||
positions.push_back(pos);
|
||||
}
|
||||
|
||||
PQclear(results);
|
||||
|
||||
return positions;
|
||||
}
|
||||
|
||||
@ -166,61 +222,8 @@ 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;
|
||||
}
|
@ -1,13 +1,33 @@
|
||||
#ifndef _DB_POSTGRESQL_H
|
||||
#define _DB_POSTGRESQL_H
|
||||
#pragma once
|
||||
|
||||
#include "db.h"
|
||||
#include <libpq-fe.h>
|
||||
|
||||
class DBPostgreSQL : public DB {
|
||||
class PostgreSQLBase {
|
||||
public:
|
||||
~PostgreSQLBase();
|
||||
|
||||
protected:
|
||||
void openDatabase(const char *connect_string);
|
||||
|
||||
PGresult *checkResults(PGresult *res, bool clear = true);
|
||||
void prepareStatement(const std::string &name, const std::string &sql) {
|
||||
checkResults(PQprepare(db, name.c_str(), sql.c_str(), 0, NULL));
|
||||
}
|
||||
PGresult *execPrepared(
|
||||
const char *stmtName, const int paramsNumber,
|
||||
const void **params,
|
||||
const int *paramsLengths = nullptr, const int *paramsFormats = nullptr,
|
||||
bool clear = true
|
||||
);
|
||||
|
||||
PGconn *db = NULL;
|
||||
};
|
||||
|
||||
class DBPostgreSQL : public DB, PostgreSQLBase {
|
||||
public:
|
||||
DBPostgreSQL(const std::string &mapdir);
|
||||
std::vector<BlockPos> getBlockPos(BlockPos min, BlockPos max) override;
|
||||
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,
|
||||
@ -16,20 +36,6 @@ public:
|
||||
|
||||
bool preferRangeQueries() const override { return true; }
|
||||
|
||||
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;
|
||||
int pg_binary_to_int(PGresult *res, int row, int col);
|
||||
};
|
||||
|
||||
#endif // _DB_POSTGRESQL_H
|
@ -20,7 +20,6 @@ static inline int64_t stoi64(const std::string &s)
|
||||
return t;
|
||||
}
|
||||
|
||||
|
||||
static inline std::string i64tos(int64_t i)
|
||||
{
|
||||
std::ostringstream os;
|
||||
@ -28,10 +27,11 @@ static inline std::string i64tos(int64_t i)
|
||||
return os.str();
|
||||
}
|
||||
|
||||
|
||||
DBRedis::DBRedis(const std::string &mapdir)
|
||||
{
|
||||
std::ifstream ifs((mapdir + "/world.mt").c_str());
|
||||
if(!ifs.good())
|
||||
std::ifstream ifs(mapdir + "world.mt");
|
||||
if (!ifs.good())
|
||||
throw std::runtime_error("Failed to read world.mt");
|
||||
std::string tmp;
|
||||
|
||||
@ -40,12 +40,16 @@ DBRedis::DBRedis(const std::string &mapdir)
|
||||
hash = read_setting("redis_hash", ifs);
|
||||
ifs.seekg(0);
|
||||
|
||||
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) {
|
||||
if (tmp.find('/') != std::string::npos) {
|
||||
ctx = redisConnectUnix(tmp.c_str());
|
||||
} else {
|
||||
int port = stoi64(read_setting_default("redis_port", ifs, "6379"));
|
||||
ctx = redisConnect(tmp.c_str(), port);
|
||||
}
|
||||
|
||||
if (!ctx) {
|
||||
throw std::runtime_error("Cannot allocate redis context");
|
||||
} else if(ctx->err) {
|
||||
} else if (ctx->err) {
|
||||
std::string err = std::string("Connection error: ") + ctx->errstr;
|
||||
redisFree(ctx);
|
||||
throw std::runtime_error(err);
|
||||
@ -64,7 +68,7 @@ DBRedis::~DBRedis()
|
||||
}
|
||||
|
||||
|
||||
std::vector<BlockPos> DBRedis::getBlockPos(BlockPos min, BlockPos max)
|
||||
std::vector<BlockPos> DBRedis::getBlockPosXZ(BlockPos min, BlockPos max)
|
||||
{
|
||||
std::vector<BlockPos> res;
|
||||
for (const auto &it : posCache) {
|
||||
@ -82,8 +86,9 @@ std::vector<BlockPos> DBRedis::getBlockPos(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:
|
||||
@ -97,7 +102,7 @@ const char *DBRedis::replyTypeStr(int type) {
|
||||
case REDIS_REPLY_ARRAY:
|
||||
return "REDIS_REPLY_ARRAY";
|
||||
default:
|
||||
return "unknown";
|
||||
return "(unknown)";
|
||||
}
|
||||
}
|
||||
|
||||
@ -106,12 +111,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);
|
||||
@ -128,25 +133,24 @@ void DBRedis::HMGET(const std::vector<BlockPos> &positions,
|
||||
argv[0] = "HMGET";
|
||||
argv[1] = hash.c_str();
|
||||
|
||||
std::vector<BlockPos>::const_iterator position = positions.begin();
|
||||
std::size_t remaining = positions.size();
|
||||
std::size_t abs_i = 0;
|
||||
auto position = positions.begin();
|
||||
size_t remaining = positions.size();
|
||||
size_t abs_i = 0;
|
||||
while (remaining > 0) {
|
||||
const std::size_t batch_size =
|
||||
(remaining > DB_REDIS_HMGET_NUMFIELDS) ? DB_REDIS_HMGET_NUMFIELDS : remaining;
|
||||
const size_t batch_size = mymin<size_t>(DB_REDIS_HMGET_NUMFIELDS, remaining);
|
||||
|
||||
redisReply *reply;
|
||||
{
|
||||
// storage to preserve validity of .c_str()
|
||||
std::string keys[batch_size];
|
||||
for (std::size_t i = 0; i < batch_size; ++i) {
|
||||
for (size_t i = 0; i < batch_size; ++i) {
|
||||
keys[i] = i64tos(encodeBlockPos(*position++));
|
||||
argv[i+2] = keys[i].c_str();
|
||||
}
|
||||
reply = (redisReply*) redisCommandArgv(ctx, batch_size + 2, argv, NULL);
|
||||
}
|
||||
|
||||
if(!reply)
|
||||
if (!reply)
|
||||
throw std::runtime_error("Redis command HMGET failed");
|
||||
if (reply->type != REDIS_REPLY_ARRAY)
|
||||
REPLY_TYPE_ERR(reply, "HMGET reply");
|
||||
@ -154,7 +158,7 @@ void DBRedis::HMGET(const std::vector<BlockPos> &positions,
|
||||
freeReplyObject(reply);
|
||||
throw std::runtime_error("HMGET wrong number of elements");
|
||||
}
|
||||
for (std::size_t i = 0; i < reply->elements; ++i) {
|
||||
for (size_t i = 0; i < reply->elements; ++i) {
|
||||
redisReply *subreply = reply->element[i];
|
||||
if (subreply->type == REDIS_REPLY_NIL)
|
||||
continue;
|
||||
@ -162,10 +166,14 @@ 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((const unsigned char *) subreply->str, subreply->len));
|
||||
result(abs_i + i, ustring(
|
||||
reinterpret_cast<const unsigned char*>(subreply->str),
|
||||
subreply->len
|
||||
));
|
||||
}
|
||||
freeReplyObject(reply);
|
||||
abs_i += reply->elements;
|
||||
|
||||
abs_i += batch_size;
|
||||
remaining -= batch_size;
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
#ifndef DB_REDIS_HEADER
|
||||
#define DB_REDIS_HEADER
|
||||
#pragma once
|
||||
|
||||
#include "db.h"
|
||||
#include <unordered_map>
|
||||
@ -10,7 +9,7 @@
|
||||
class DBRedis : public DB {
|
||||
public:
|
||||
DBRedis(const std::string &mapdir);
|
||||
std::vector<BlockPos> getBlockPos(BlockPos min, BlockPos max) override;
|
||||
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,
|
||||
@ -33,5 +32,3 @@ private:
|
||||
redisContext *ctx;
|
||||
std::string hash;
|
||||
};
|
||||
|
||||
#endif // DB_REDIS_HEADER
|
261
src/db-sqlite3.cpp
Normal file
261
src/db-sqlite3.cpp
Normal file
@ -0,0 +1,261 @@
|
||||
#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));
|
||||
}
|
||||
}
|
87
src/db-sqlite3.h
Normal file
87
src/db-sqlite3.h
Normal file
@ -0,0 +1,87 @@
|
||||
#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
|
||||
};
|
@ -1,24 +1,20 @@
|
||||
#ifndef DB_HEADER
|
||||
#define DB_HEADER
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <map>
|
||||
#include <cstdint>
|
||||
#include <list>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
#include "types.h"
|
||||
|
||||
|
||||
struct BlockPos {
|
||||
int16_t x;
|
||||
int16_t y;
|
||||
int16_t z;
|
||||
int16_t x, y, z;
|
||||
|
||||
BlockPos() : x(0), y(0), z(0) {}
|
||||
BlockPos(int16_t x, int16_t y, int16_t z) : x(x), y(y), z(z) {}
|
||||
constexpr BlockPos() : x(0), y(0), z(0) {}
|
||||
explicit constexpr BlockPos(int16_t v) : x(v), y(v), z(v) {}
|
||||
constexpr BlockPos(int16_t x, int16_t y, int16_t z) : x(x), y(y), z(z) {}
|
||||
|
||||
// Implements the inverse ordering so that (2,2,2) < (1,1,1)
|
||||
bool operator < (const BlockPos &p) const
|
||||
inline bool operator<(const BlockPos &p) const
|
||||
{
|
||||
if (z > p.z)
|
||||
return true;
|
||||
@ -28,11 +24,7 @@ struct BlockPos {
|
||||
return true;
|
||||
if (y < p.y)
|
||||
return false;
|
||||
if (x > p.x)
|
||||
return true;
|
||||
if (x < p.x)
|
||||
return false;
|
||||
return false;
|
||||
return x > p.x;
|
||||
}
|
||||
};
|
||||
|
||||
@ -44,29 +36,32 @@ typedef std::list<Block> BlockList;
|
||||
class DB {
|
||||
protected:
|
||||
// Helpers that implement the hashed positions used by most backends
|
||||
inline int64_t encodeBlockPos(const BlockPos pos) const;
|
||||
inline BlockPos decodeBlockPos(int64_t hash) const;
|
||||
static inline int64_t encodeBlockPos(const BlockPos pos);
|
||||
static inline BlockPos decodeBlockPos(int64_t hash);
|
||||
|
||||
public:
|
||||
/* Return all block positions inside the range given by min and max,
|
||||
* so that min.x <= x < max.x, ...
|
||||
/* Return all unique (X, Z) position pairs inside area given by min and max,
|
||||
* so that min.x <= x < max.x && min.z <= z < max.z
|
||||
* Note: duplicates are allowed, but results in wasted time.
|
||||
*/
|
||||
virtual std::vector<BlockPos> getBlockPos(BlockPos min, BlockPos max) = 0;
|
||||
virtual std::vector<BlockPos> getBlockPosXZ(BlockPos min, BlockPos max) = 0;
|
||||
|
||||
/* Read all blocks in column given by x and z
|
||||
* and inside the given Y range (min_y <= y < max_y) into list
|
||||
*/
|
||||
virtual void getBlocksOnXZ(BlockList &blocks, int16_t x, int16_t z,
|
||||
int16_t min_y, int16_t max_y) = 0;
|
||||
|
||||
/* Read blocks at given positions into list
|
||||
*/
|
||||
virtual void getBlocksByPos(BlockList &blocks,
|
||||
const std::vector<BlockPos> &positions) = 0;
|
||||
|
||||
/* Can this database efficiently do range queries?
|
||||
* (for large data sets, more efficient that brute force)
|
||||
*/
|
||||
virtual bool preferRangeQueries() const = 0;
|
||||
|
||||
|
||||
virtual ~DB() {}
|
||||
};
|
||||
|
||||
@ -99,7 +94,7 @@ static inline int64_t pythonmodulo(int64_t i, int64_t mod)
|
||||
}
|
||||
|
||||
|
||||
inline int64_t DB::encodeBlockPos(const BlockPos pos) const
|
||||
inline int64_t DB::encodeBlockPos(const BlockPos pos)
|
||||
{
|
||||
return (uint64_t) pos.z * 0x1000000 +
|
||||
(uint64_t) pos.y * 0x1000 +
|
||||
@ -107,7 +102,7 @@ inline int64_t DB::encodeBlockPos(const BlockPos pos) const
|
||||
}
|
||||
|
||||
|
||||
inline BlockPos DB::decodeBlockPos(int64_t hash) const
|
||||
inline BlockPos DB::decodeBlockPos(int64_t hash)
|
||||
{
|
||||
BlockPos pos;
|
||||
pos.x = unsigned_to_signed(pythonmodulo(hash, 4096), 2048);
|
||||
@ -122,4 +117,3 @@ inline BlockPos DB::decodeBlockPos(int64_t hash) const
|
||||
* End black magic *
|
||||
*******************/
|
||||
|
||||
#endif // DB_HEADER
|
16
src/log.cpp
Normal file
16
src/log.cpp
Normal file
@ -0,0 +1,16 @@
|
||||
#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;
|
||||
}
|
40
src/log.h
Normal file
40
src/log.h
Normal file
@ -0,0 +1,40 @@
|
||||
#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);
|
@ -8,14 +8,16 @@
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include "cmake_config.h"
|
||||
#include "config.h"
|
||||
#include "TileGenerator.h"
|
||||
#include "util.h"
|
||||
#include "log.h"
|
||||
|
||||
static void usage()
|
||||
{
|
||||
const std::pair<const char*, const char*> options[] = {
|
||||
{"-i/--input", "<world_path>"},
|
||||
{"-o/--output", "<output_image.png>"},
|
||||
static const std::pair<const char*, const char*> options[] = {
|
||||
{"-i/--input", "<path>"},
|
||||
{"-o/--output", "<path>"},
|
||||
{"--bgcolor", "<color>"},
|
||||
{"--scalecolor", "<color>"},
|
||||
{"--playercolor", "<color>"},
|
||||
@ -26,19 +28,21 @@ static void usage()
|
||||
{"--drawalpha", ""},
|
||||
{"--noshading", ""},
|
||||
{"--noemptyimage", ""},
|
||||
{"-v/--verbose", ""},
|
||||
{"--min-y", "<y>"},
|
||||
{"--max-y", "<y>"},
|
||||
{"--backend", "<backend>"},
|
||||
{"--geometry", "x:y+w+h"},
|
||||
{"--geometry", "x:z+w+h"},
|
||||
{"--extent", ""},
|
||||
{"--zoom", "<zoomlevel>"},
|
||||
{"--colors", "<colors.txt>"},
|
||||
{"--zoom", "<factor>"},
|
||||
{"--colors", "<path>"},
|
||||
{"--scales", "[t][b][l][r]"},
|
||||
{"--exhaustive", "never|y|full|auto"},
|
||||
{"--dumpblock", "x,y,z"},
|
||||
};
|
||||
const char *top_text =
|
||||
"minetestmapper -i <world_path> -o <output_image.png> [options]\n"
|
||||
"Generate an overview image of a Minetest map.\n"
|
||||
"minetestmapper -i <world_path> -o <output_image> [options]\n"
|
||||
"Generate an overview image of a Luanti map.\n"
|
||||
"\n"
|
||||
"Options:\n";
|
||||
const char *bottom_text =
|
||||
@ -54,38 +58,51 @@ 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 bool file_exists(const std::string &path)
|
||||
static inline bool file_exists(const std::string &path)
|
||||
{
|
||||
std::ifstream ifs(path.c_str());
|
||||
return ifs.is_open();
|
||||
return file_exists(path.c_str());
|
||||
}
|
||||
|
||||
static inline int stoi(const char *s)
|
||||
{
|
||||
std::istringstream iss(s);
|
||||
int ret;
|
||||
iss >> ret;
|
||||
return ret;
|
||||
}
|
||||
|
||||
static std::string search_colors(const std::string &worldpath)
|
||||
{
|
||||
if(file_exists(worldpath + "/colors.txt"))
|
||||
if (file_exists(worldpath + "/colors.txt"))
|
||||
return worldpath + "/colors.txt";
|
||||
|
||||
#ifndef _WIN32
|
||||
char *home = std::getenv("HOME");
|
||||
if(home) {
|
||||
std::string check = ((std::string) home) + "/.minetest/colors.txt";
|
||||
if(file_exists(check))
|
||||
if (home && home[0]) {
|
||||
std::string check = std::string(home) + "/.minetest/colors.txt";
|
||||
if (file_exists(check))
|
||||
return check;
|
||||
}
|
||||
#endif
|
||||
|
||||
constexpr bool sharedir_valid = !(SHAREDIR[0] == '.' || SHAREDIR[0] == '\0');
|
||||
if(sharedir_valid && file_exists(SHAREDIR "/colors.txt"))
|
||||
constexpr bool sharedir_valid = !(SHAREDIR[0] == '.' || !SHAREDIR[0]);
|
||||
if (sharedir_valid && file_exists(SHAREDIR "/colors.txt"))
|
||||
return SHAREDIR "/colors.txt";
|
||||
|
||||
std::cerr << "Warning: Falling back to using colors.txt from current directory." << std::endl;
|
||||
return "colors.txt";
|
||||
errorstream << "Warning: Falling back to using colors.txt from current directory." << std::endl;
|
||||
return "./colors.txt";
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
const char *short_options = "hi:o:v";
|
||||
const static struct option long_options[] =
|
||||
{
|
||||
{"help", no_argument, 0, 'h'},
|
||||
@ -110,18 +127,23 @@ 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 = "";
|
||||
std::string colors;
|
||||
bool onlyPrintExtent = false;
|
||||
BlockPos dumpblock(INT16_MIN);
|
||||
|
||||
TileGenerator generator;
|
||||
bool onlyPrintExtent = false;
|
||||
while (1) {
|
||||
int option_index;
|
||||
int c = getopt_long(argc, argv, "hi:o:", long_options, &option_index);
|
||||
int c = getopt_long(argc, argv, short_options, long_options, &option_index);
|
||||
if (c == -1)
|
||||
break; // done
|
||||
|
||||
@ -129,7 +151,6 @@ int main(int argc, char *argv[])
|
||||
case 'h':
|
||||
usage();
|
||||
return 0;
|
||||
break;
|
||||
case 'i':
|
||||
input = optarg;
|
||||
break;
|
||||
@ -169,19 +190,11 @@ int main(int argc, char *argv[])
|
||||
case 'd':
|
||||
generator.setBackend(optarg);
|
||||
break;
|
||||
case 'a': {
|
||||
std::istringstream iss(optarg);
|
||||
int miny;
|
||||
iss >> miny;
|
||||
generator.setMinY(miny);
|
||||
}
|
||||
case 'a':
|
||||
generator.setMinY(stoi(optarg));
|
||||
break;
|
||||
case 'c': {
|
||||
std::istringstream iss(optarg);
|
||||
int maxy;
|
||||
iss >> maxy;
|
||||
generator.setMaxY(maxy);
|
||||
}
|
||||
case 'c':
|
||||
generator.setMaxY(stoi(optarg));
|
||||
break;
|
||||
case 'g': {
|
||||
std::istringstream geometry(optarg);
|
||||
@ -190,30 +203,26 @@ int main(int argc, char *argv[])
|
||||
geometry >> x >> c >> y >> w >> h;
|
||||
if (geometry.fail() || c != ':' || w < 1 || h < 1) {
|
||||
usage();
|
||||
exit(1);
|
||||
return 1;
|
||||
}
|
||||
generator.setGeometry(x, y, w, h);
|
||||
}
|
||||
break;
|
||||
case 'f': {
|
||||
uint flags = 0;
|
||||
if(strchr(optarg, 't') != NULL)
|
||||
if (strchr(optarg, 't'))
|
||||
flags |= SCALE_TOP;
|
||||
if(strchr(optarg, 'b') != NULL)
|
||||
if (strchr(optarg, 'b'))
|
||||
flags |= SCALE_BOTTOM;
|
||||
if(strchr(optarg, 'l') != NULL)
|
||||
if (strchr(optarg, 'l'))
|
||||
flags |= SCALE_LEFT;
|
||||
if(strchr(optarg, 'r') != NULL)
|
||||
if (strchr(optarg, 'r'))
|
||||
flags |= SCALE_RIGHT;
|
||||
generator.setScales(flags);
|
||||
}
|
||||
break;
|
||||
case 'z': {
|
||||
std::istringstream iss(optarg);
|
||||
int zoom;
|
||||
iss >> zoom;
|
||||
generator.setZoom(zoom);
|
||||
}
|
||||
case 'z':
|
||||
generator.setZoom(stoi(optarg));
|
||||
break;
|
||||
case 'C':
|
||||
colors = optarg;
|
||||
@ -222,42 +231,56 @@ int main(int argc, char *argv[])
|
||||
generator.setDontWriteEmpty(true);
|
||||
break;
|
||||
case 'j': {
|
||||
int mode;
|
||||
int mode = EXH_AUTO;
|
||||
if (!strcmp(optarg, "never"))
|
||||
mode = EXH_NEVER;
|
||||
else if (!strcmp(optarg, "y"))
|
||||
mode = EXH_Y;
|
||||
else if (!strcmp(optarg, "full"))
|
||||
mode = EXH_FULL;
|
||||
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:
|
||||
exit(1);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (input.empty() || (!onlyPrintExtent && output.empty())) {
|
||||
const bool need_output = !onlyPrintExtent && dumpblock.x == INT16_MIN;
|
||||
if (input.empty() || (need_output && output.empty())) {
|
||||
usage();
|
||||
return 0;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
if (onlyPrintExtent) {
|
||||
generator.printGeometry(input);
|
||||
return 0;
|
||||
} else if (dumpblock.x != INT16_MIN) {
|
||||
generator.dumpBlock(input, dumpblock);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if(colors == "")
|
||||
if(colors.empty())
|
||||
colors = search_colors(input);
|
||||
generator.parseColorsFile(colors);
|
||||
generator.generate(input, output);
|
||||
|
||||
} catch(std::runtime_error &e) {
|
||||
std::cerr << "Exception: " << e.what() << std::endl;
|
||||
} catch (const std::exception &e) {
|
||||
errorstream << "Exception: " << e.what() << std::endl;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
40
src/types.h
Normal file
40
src/types.h
Normal file
@ -0,0 +1,40 @@
|
||||
#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;
|
76
src/util.cpp
Normal file
76
src/util.cpp
Normal file
@ -0,0 +1,76 @@
|
||||
#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;
|
||||
}
|
27
src/util.h
Normal file
27
src/util.h
Normal file
@ -0,0 +1,27 @@
|
||||
#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
45
util.cpp
@ -1,45 +0,0 @@
|
||||
#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());
|
||||
}
|
57
util/build-mingw.sh
Executable file
57
util/build-mingw.sh
Executable file
@ -0,0 +1,57 @@
|
||||
#!/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."
|
@ -1,73 +0,0 @@
|
||||
#!/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."
|
29
util/ci/script.sh
Executable file
29
util/ci/script.sh
Executable file
@ -0,0 +1,29 @@
|
||||
#!/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
|
||||
}
|
111
util/ci/test.sh
Executable file
111
util/ci/test.sh
Executable file
@ -0,0 +1,111 @@
|
||||
#!/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
|
1
util/ci/test_block
Normal file
1
util/ci/test_block
Normal file
@ -0,0 +1 @@
|
||||
1b00ffff020278daedd4c1090000080331dd7f691710faf12589235cb12ae870fca6bffefaebafbffefaebafbffefaebbff7b708fdf1ffd11ffdd11ffdd11ffd01000000000000003836d59f010578da63000000010001000000ffffffff000002000000036169720001000d64656661756c743a73746f6e650a0000
|
73
util/dumpnodes/init.lua
Normal file
73
util/dumpnodes/init.lua
Normal file
@ -0,0 +1,73 @@
|
||||
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,
|
||||
})
|
2
util/dumpnodes/mod.conf
Normal file
2
util/dumpnodes/mod.conf
Normal file
@ -0,0 +1,2 @@
|
||||
name = dumpnodes
|
||||
description = minetestmapper development mod (node dumper)
|
183
util/generate_colorstxt.py
Executable file
183
util/generate_colorstxt.py
Executable file
@ -0,0 +1,183 @@
|
||||
#!/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.")
|
@ -1,8 +0,0 @@
|
||||
#!/bin/bash -e
|
||||
mkdir -p travisbuild
|
||||
cd travisbuild
|
||||
|
||||
cmake .. \
|
||||
-DENABLE_LEVELDB=1
|
||||
|
||||
make -j2
|
Reference in New Issue
Block a user