Move source files into folder

This commit is contained in:
sfan5
2025-02-18 15:45:14 +01:00
parent 1c16c40ccc
commit 527a56f22e
31 changed files with 40 additions and 32 deletions

168
src/BlockDecoder.cpp Normal file
View File

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

28
src/BlockDecoder.h Normal file
View File

@ -0,0 +1,28 @@
#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;
// one instance for performance
ZstdDecompressor m_zstd_decompressor;
};

126
src/Image.cpp Normal file
View File

@ -0,0 +1,126 @@
#include <cstdio>
#include <cerrno>
#include <cstring>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <gd.h>
#include <gdfontmb.h>
#include "Image.h"
#ifndef NDEBUG
#define SIZECHECK(x, y) check_bounds((x), (y), m_width, m_height)
#else
#define SIZECHECK(x, y) do {} while(0)
#endif
// ARGB but with inverted alpha
static inline int color2int(const Color &c)
{
u8 a = (255 - c.a) * gdAlphaMax / 255;
return (a << 24) | (c.r << 16) | (c.g << 8) | c.b;
}
static inline Color int2color(int c)
{
Color c2;
c2.b = c & 0xff;
c2.g = (c >> 8) & 0xff;
c2.r = (c >> 16) & 0xff;
u8 a = (c >> 24) & 0xff;
c2.a = 255 - (a*255 / gdAlphaMax);
return c2;
}
#ifndef NDEBUG
static inline void check_bounds(int x, int y, int width, int height)
{
if(x < 0 || x >= width) {
std::ostringstream oss;
oss << "Access outside image bounds (x), 0 < "
<< x << " < " << width << " is false.";
throw std::out_of_range(oss.str());
}
if(y < 0 || y >= height) {
std::ostringstream oss;
oss << "Access outside image bounds (y), 0 < "
<< y << " < " << height << " is false.";
throw std::out_of_range(oss.str());
}
}
#endif
Image::Image(int width, int height) :
m_width(width), m_height(height), m_image(nullptr)
{
SIZECHECK(0, 0);
m_image = gdImageCreateTrueColor(m_width, m_height);
}
Image::~Image()
{
gdImageDestroy(m_image);
}
void Image::setPixel(int x, int y, const Color &c)
{
SIZECHECK(x, y);
m_image->tpixels[y][x] = color2int(c);
}
Color Image::getPixel(int x, int y)
{
SIZECHECK(x, y);
return int2color(m_image->tpixels[y][x]);
}
void Image::drawLine(int x1, int y1, int x2, int y2, const Color &c)
{
SIZECHECK(x1, y1);
SIZECHECK(x2, y2);
gdImageLine(m_image, x1, y1, x2, y2, color2int(c));
}
void Image::drawText(int x, int y, const std::string &s, const Color &c)
{
SIZECHECK(x, y);
gdImageString(m_image, gdFontGetMediumBold(), x, y, (unsigned char*) s.c_str(), color2int(c));
}
void Image::drawFilledRect(int x, int y, int w, int h, const Color &c)
{
SIZECHECK(x, y);
SIZECHECK(x + w - 1, y + h - 1);
gdImageFilledRectangle(m_image, x, y, x + w - 1, y + h - 1, color2int(c));
}
void Image::drawCircle(int x, int y, int diameter, const Color &c)
{
SIZECHECK(x, y);
gdImageArc(m_image, x, y, diameter, diameter, 0, 360, color2int(c));
}
void Image::save(const std::string &filename)
{
#if (GD_MAJOR_VERSION == 2 && GD_MINOR_VERSION == 1 && GD_RELEASE_VERSION >= 1) || (GD_MAJOR_VERSION == 2 && GD_MINOR_VERSION > 1) || GD_MAJOR_VERSION > 2
const char *f = filename.c_str();
if (gdSupportsFileType(f, 1) == GD_FALSE)
throw std::runtime_error("Image format not supported by gd");
if (gdImageFile(m_image, f) == GD_FALSE)
throw std::runtime_error("Error saving image");
#else
if (filename.compare(filename.length() - 4, 4, ".png") != 0)
throw std::runtime_error("Only PNG is supported");
FILE *f = fopen(filename.c_str(), "wb");
if (!f) {
std::ostringstream oss;
oss << "Error opening image file: " << std::strerror(errno);
throw std::runtime_error(oss.str());
}
gdImagePng(m_image, f);
fclose(f);
#endif
}

34
src/Image.h Normal file
View File

@ -0,0 +1,34 @@
#pragma once
#include "types.h"
#include <string>
#include <gd.h>
struct Color {
Color() : r(0), g(0), b(0), a(0) {};
Color(u8 r, u8 g, u8 b) : r(r), g(g), b(b), a(255) {};
Color(u8 r, u8 g, u8 b, u8 a) : r(r), g(g), b(b), a(a) {};
u8 r, g, b, a;
};
class Image {
public:
Image(int width, int height);
~Image();
Image(const Image&) = delete;
Image& operator=(const Image&) = delete;
void setPixel(int x, int y, const Color &c);
Color getPixel(int x, int y);
void drawLine(int x1, int y1, int x2, int y2, const Color &c);
void drawText(int x, int y, const std::string &s, const Color &c);
void drawFilledRect(int x, int y, int w, int h, const Color &c);
void drawCircle(int x, int y, int diameter, const Color &c);
void save(const std::string &filename);
private:
int m_width, m_height;
gdImagePtr m_image;
};

45
src/PixelAttributes.cpp Normal file
View File

@ -0,0 +1,45 @@
#include <cstring>
#include "PixelAttributes.h"
PixelAttributes::PixelAttributes():
m_width(0)
{
for (size_t i = 0; i < LineCount; ++i) {
m_pixelAttributes[i] = nullptr;
}
}
PixelAttributes::~PixelAttributes()
{
freeAttributes();
}
void PixelAttributes::setWidth(int width)
{
freeAttributes();
m_width = width + 1; // 1px gradient calculation
for (size_t i = 0; i < LineCount; ++i) {
m_pixelAttributes[i] = new PixelAttribute[m_width];
}
}
void PixelAttributes::scroll()
{
size_t lineLength = m_width * sizeof(PixelAttribute);
memcpy(m_pixelAttributes[FirstLine], m_pixelAttributes[LastLine], lineLength);
for (size_t i = 1; i < LineCount - 1; ++i) {
memcpy(m_pixelAttributes[i], m_pixelAttributes[EmptyLine], lineLength);
}
}
void PixelAttributes::freeAttributes()
{
for (size_t i = 0; i < LineCount; ++i) {
if (m_pixelAttributes[i] != nullptr) {
delete[] m_pixelAttributes[i];
m_pixelAttributes[i] = nullptr;
}
}
}

44
src/PixelAttributes.h Normal file
View 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;
};

131
src/PlayerAttributes.cpp Normal file
View File

@ -0,0 +1,131 @@
#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"
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();
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)
return;
struct dirent *ent;
while ((ent = readdir (dir)) != NULL) {
if (ent->d_name[0] == '.')
continue;
std::ifstream in(playersPath + PATH_SEPARATOR + ent->d_name);
if (!in.good())
continue;
std::string name, position;
name = read_setting("name", in);
in.seekg(0);
position = read_setting("position", in);
Player player;
std::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.0f;
player.y /= 10.0f;
player.z /= 10.0f;
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 = 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.0f;
player.y /= 10.0f;
player.z /= 10.0f;
m_players.push_back(player);
}
sqlite3_finalize(stmt_get_player_pos);
sqlite3_close(db);
}
/**********/
PlayerAttributes::Players::const_iterator PlayerAttributes::begin() const
{
return m_players.cbegin();
}
PlayerAttributes::Players::const_iterator PlayerAttributes::end() const
{
return m_players.cend();
}

26
src/PlayerAttributes.h Normal file
View File

@ -0,0 +1,26 @@
#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:
void readFiles(const std::string &playersPath);
void readSqlite(const std::string &db_name);
Players m_players;
};

912
src/TileGenerator.cpp Normal file
View File

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

167
src/TileGenerator.h Normal file
View File

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

69
src/ZlibDecompressor.cpp Normal file
View File

@ -0,0 +1,69 @@
#include <zlib.h>
#include <stdint.h>
#include "ZlibDecompressor.h"
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;
}
size_t ZlibDecompressor::seekPos() const
{
return m_seekPos;
}
ustring ZlibDecompressor::decompress()
{
const unsigned char *data = m_data + m_seekPos;
const size_t size = m_size - m_seekPos;
ustring buffer;
constexpr size_t BUFSIZE = 32 * 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 (inflateInit(&strm) != Z_OK)
throw DecompressError();
strm.next_in = const_cast<unsigned char *>(data);
strm.avail_in = size;
buffer.resize(BUFSIZE);
strm.next_out = &buffer[0];
strm.avail_out = BUFSIZE;
int ret = 0;
do {
ret = inflate(&strm, Z_NO_FLUSH);
if (strm.avail_out == 0) {
const auto off = buffer.size();
buffer.reserve(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) inflateEnd(&strm);
return buffer;
}

20
src/ZlibDecompressor.h Normal file
View File

@ -0,0 +1,20 @@
#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;
ustring decompress();
private:
const u8 *m_data;
size_t m_seekPos, m_size;
};

58
src/ZstdDecompressor.cpp Normal file
View File

@ -0,0 +1,58 @@
#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;
}
std::size_t ZstdDecompressor::seekPos() const
{
return m_seekPos;
}
ustring ZstdDecompressor::decompress()
{
ZSTD_DStream *stream = reinterpret_cast<ZSTD_DStream*>(m_stream);
ZSTD_inBuffer inbuf = { m_data, m_size, m_seekPos };
ustring buffer;
constexpr size_t BUFSIZE = 32 * 1024;
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 (outbuf.size == outbuf.pos) {
outbuf.size += BUFSIZE;
buffer.resize(outbuf.size);
outbuf.dst = &buffer[0];
}
if (ret && ZSTD_isError(ret))
throw DecompressError();
} while (ret != 0);
m_seekPos = inbuf.pos;
buffer.resize(outbuf.pos);
return buffer;
}

21
src/ZstdDecompressor.h Normal file
View File

@ -0,0 +1,21 @@
#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;
ustring decompress();
private:
void *m_stream; // ZSTD_DStream
const u8 *m_data;
size_t m_seekPos, m_size;
};

13
src/cmake_config.h.in Normal file
View File

@ -0,0 +1,13 @@
// Filled in by the build system
#ifndef CMAKE_CONFIG_H
#define CMAKE_CONFIG_H
#cmakedefine01 USE_POSTGRESQL
#cmakedefine01 USE_LEVELDB
#cmakedefine01 USE_REDIS
#define SHAREDIR "@SHAREDIR@"
#endif

11
src/config.h Normal file
View File

@ -0,0 +1,11 @@
#if MSDOS || __OS2__ || __NT__ || _WIN32
#define PATH_SEPARATOR '\\'
#else
#define PATH_SEPARATOR '/'
#endif
#ifdef USE_CMAKE_CONFIG_H
#include "cmake_config.h"
#else
#error missing config
#endif

114
src/db-leveldb.cpp Normal file
View File

@ -0,0 +1,114 @@
#include <stdexcept>
#include <sstream>
#include "db-leveldb.h"
#include "types.h"
static inline int64_t stoi64(const std::string &s)
{
std::istringstream tmp(s);
int64_t t;
tmp >> t;
return t;
}
static inline std::string i64tos(int64_t i)
{
std::ostringstream os;
os << i;
return os.str();
}
DBLevelDB::DBLevelDB(const std::string &mapdir)
{
leveldb::Options options;
options.create_if_missing = false;
leveldb::Status status = leveldb::DB::Open(options, mapdir + "map.db", &db);
if (!status.ok()) {
throw std::runtime_error(std::string("Failed to open Database: ") + status.ToString());
}
/* LevelDB is a dumb key-value store, so the only optimization we can do
* is to cache the block positions that exist in the db.
*/
loadPosCache();
}
DBLevelDB::~DBLevelDB()
{
delete db;
}
std::vector<BlockPos> DBLevelDB::getBlockPos(BlockPos min, BlockPos max)
{
std::vector<BlockPos> res;
for (const auto &it : posCache) {
if (it.first < min.z || it.first >= max.z)
continue;
for (auto pos2 : it.second) {
if (pos2.first < min.x || pos2.first >= max.x)
continue;
if (pos2.second < min.y || pos2.second >= max.y)
continue;
res.emplace_back(pos2.first, pos2.second, it.first);
}
}
return res;
}
void DBLevelDB::loadPosCache()
{
leveldb::Iterator * it = db->NewIterator(leveldb::ReadOptions());
for (it->SeekToFirst(); it->Valid(); it->Next()) {
int64_t posHash = stoi64(it->key().ToString());
BlockPos pos = decodeBlockPos(posHash);
posCache[pos.z].emplace_back(pos.x, pos.y);
}
delete it;
}
void DBLevelDB::getBlocksOnXZ(BlockList &blocks, int16_t x, int16_t z,
int16_t min_y, int16_t max_y)
{
std::string datastr;
leveldb::Status status;
auto it = posCache.find(z);
if (it == posCache.cend())
return;
for (auto pos2 : it->second) {
if (pos2.first != x)
continue;
if (pos2.second < min_y || pos2.second >= max_y)
continue;
BlockPos pos(x, pos2.second, z);
status = db->Get(leveldb::ReadOptions(), i64tos(encodeBlockPos(pos)), &datastr);
if (status.ok()) {
blocks.emplace_back(
pos, ustring((unsigned char *) datastr.data(), datastr.size())
);
}
}
}
void DBLevelDB::getBlocksByPos(BlockList &blocks,
const std::vector<BlockPos> &positions)
{
std::string datastr;
leveldb::Status status;
for (auto pos : positions) {
status = db->Get(leveldb::ReadOptions(), i64tos(encodeBlockPos(pos)), &datastr);
if (status.ok()) {
blocks.emplace_back(
pos, ustring((unsigned char *) datastr.data(), datastr.size())
);
}
}
}

28
src/db-leveldb.h Normal file
View File

@ -0,0 +1,28 @@
#pragma once
#include "db.h"
#include <unordered_map>
#include <utility>
#include <leveldb/db.h>
class DBLevelDB : public DB {
public:
DBLevelDB(const std::string &mapdir);
std::vector<BlockPos> 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;
~DBLevelDB() override;
bool preferRangeQueries() const override { return false; }
private:
using pos2d = std::pair<int16_t, int16_t>;
void loadPosCache();
// indexed by Z, contains all (x,y) position pairs
std::unordered_map<int16_t, std::vector<pos2d>> posCache;
leveldb::DB *db;
};

226
src/db-postgresql.cpp Normal file
View File

@ -0,0 +1,226 @@
#include <stdexcept>
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <arpa/inet.h>
#include "db-postgresql.h"
#include "util.h"
#include "types.h"
#define ARRLEN(x) (sizeof(x) / sizeof((x)[0]))
DBPostgreSQL::DBPostgreSQL(const std::string &mapdir)
{
std::ifstream ifs(mapdir + "world.mt");
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 (PQstatus(db) != CONNECTION_OK) {
throw std::runtime_error(std::string(
"PostgreSQL database error: ") +
PQerrorMessage(db)
);
}
prepareStatement(
"get_block_pos",
"SELECT posX::int4, posY::int4, posZ::int4 FROM blocks WHERE"
" (posX BETWEEN $1::int4 AND $2::int4) AND"
" (posY BETWEEN $3::int4 AND $4::int4) AND"
" (posZ BETWEEN $5::int4 AND $6::int4)"
);
prepareStatement(
"get_blocks",
"SELECT posY::int4, data FROM blocks WHERE"
" posX = $1::int4 AND posZ = $2::int4"
" AND (posY BETWEEN $3::int4 AND $4::int4)"
);
prepareStatement(
"get_block_exact",
"SELECT data FROM blocks WHERE"
" posX = $1::int4 AND posY = $2::int4 AND posZ = $3::int4"
);
checkResults(PQexec(db, "START TRANSACTION;"));
checkResults(PQexec(db, "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;"));
}
DBPostgreSQL::~DBPostgreSQL()
{
try {
checkResults(PQexec(db, "COMMIT;"));
} catch (const std::exception& caught) {
std::cerr << "could not finalize: " << caught.what() << std::endl;
}
PQfinish(db);
}
std::vector<BlockPos> DBPostgreSQL::getBlockPos(BlockPos min, BlockPos max)
{
int32_t const x1 = htonl(min.x);
int32_t const x2 = htonl(max.x - 1);
int32_t const y1 = htonl(min.y);
int32_t const y2 = htonl(max.y - 1);
int32_t const z1 = htonl(min.z);
int32_t const z2 = htonl(max.z - 1);
const void *args[] = { &x1, &x2, &y1, &y2, &z1, &z2 };
const int argLen[] = { 4, 4, 4, 4, 4, 4 };
const int argFmt[] = { 1, 1, 1, 1, 1, 1 };
PGresult *results = execPrepared(
"get_block_pos", ARRLEN(args), args,
argLen, argFmt, false
);
int numrows = PQntuples(results);
std::vector<BlockPos> positions;
positions.reserve(numrows);
for (int row = 0; row < numrows; ++row)
positions.emplace_back(pg_to_blockpos(results, row, 0));
PQclear(results);
return positions;
}
void DBPostgreSQL::getBlocksOnXZ(BlockList &blocks, int16_t xPos, int16_t zPos,
int16_t min_y, int16_t max_y)
{
int32_t const x = htonl(xPos);
int32_t const z = htonl(zPos);
int32_t const y1 = htonl(min_y);
int32_t const y2 = htonl(max_y - 1);
const void *args[] = { &x, &z, &y1, &y2 };
const int argLen[] = { 4, 4, 4, 4 };
const int argFmt[] = { 1, 1, 1, 1 };
PGresult *results = execPrepared(
"get_blocks", ARRLEN(args), args,
argLen, argFmt, false
);
int numrows = PQntuples(results);
for (int row = 0; row < numrows; ++row) {
BlockPos position;
position.x = xPos;
position.y = pg_binary_to_int(results, row, 0);
position.z = zPos;
blocks.emplace_back(
position,
ustring(
reinterpret_cast<unsigned char*>(
PQgetvalue(results, row, 1)
),
PQgetlength(results, row, 1)
)
);
}
PQclear(results);
}
void DBPostgreSQL::getBlocksByPos(BlockList &blocks,
const std::vector<BlockPos> &positions)
{
int32_t x, y, z;
const void *args[] = { &x, &y, &z };
const int argLen[] = { 4, 4, 4 };
const int argFmt[] = { 1, 1, 1 };
for (auto pos : positions) {
x = htonl(pos.x);
y = htonl(pos.y);
z = htonl(pos.z);
PGresult *results = execPrepared(
"get_block_exact", ARRLEN(args), args,
argLen, argFmt, false
);
if (PQntuples(results) > 0) {
blocks.emplace_back(
pos,
ustring(
reinterpret_cast<unsigned char*>(
PQgetvalue(results, 0, 0)
),
PQgetlength(results, 0, 0)
)
);
}
PQclear(results);
}
}
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;
}

32
src/db-postgresql.h Normal file
View File

@ -0,0 +1,32 @@
#pragma once
#include "db.h"
#include <libpq-fe.h>
class DBPostgreSQL : public DB {
public:
DBPostgreSQL(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;
~DBPostgreSQL() override;
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 = nullptr, const int *paramsFormats = nullptr,
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;
};

206
src/db-redis.cpp Normal file
View File

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

34
src/db-redis.h Normal file
View File

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

196
src/db-sqlite3.cpp Normal file
View File

@ -0,0 +1,196 @@
#include <stdexcept>
#include <unistd.h> // for usleep
#include <iostream>
#include <algorithm>
#include <time.h>
#include "db-sqlite3.h"
#include "types.h"
#define SQLRES(f, good) \
result = (sqlite3_##f);\
if (result != good) {\
throw std::runtime_error(sqlite3_errmsg(db));\
}
#define SQLOK(f) SQLRES(f, SQLITE_OK)
DBSQLite3::DBSQLite3(const std::string &mapdir)
{
int result;
std::string db_name = mapdir + "map.sqlite";
SQLOK(open_v2(db_name.c_str(), &db, SQLITE_OPEN_READONLY |
SQLITE_OPEN_PRIVATECACHE, 0))
SQLOK(prepare_v2(db,
"SELECT pos, data FROM blocks WHERE pos BETWEEN ? AND ?",
-1, &stmt_get_blocks_z, NULL))
SQLOK(prepare_v2(db,
"SELECT data FROM blocks WHERE pos = ?",
-1, &stmt_get_block_exact, NULL))
SQLOK(prepare_v2(db,
"SELECT pos FROM blocks",
-1, &stmt_get_block_pos, NULL))
SQLOK(prepare_v2(db,
"SELECT pos FROM blocks WHERE pos BETWEEN ? AND ?",
-1, &stmt_get_block_pos_z, NULL))
}
DBSQLite3::~DBSQLite3()
{
sqlite3_finalize(stmt_get_blocks_z);
sqlite3_finalize(stmt_get_block_pos);
sqlite3_finalize(stmt_get_block_pos_z);
sqlite3_finalize(stmt_get_block_exact);
if (sqlite3_close(db) != SQLITE_OK) {
std::cerr << "Error closing SQLite database." << std::endl;
};
}
inline void DBSQLite3::getPosRange(int64_t &min, int64_t &max, int16_t zPos,
int16_t zPos2) const
{
/* The range of block positions is [-2048, 2047], which turns into [0, 4095]
* when casted to unsigned. This didn't actually help me understand the
* numbers below, but I wanted to write it down.
*/
// Magic numbers!
min = encodeBlockPos(BlockPos(0, -2048, zPos));
max = encodeBlockPos(BlockPos(0, 2048, zPos2)) - 1;
}
std::vector<BlockPos> DBSQLite3::getBlockPos(BlockPos min, BlockPos max)
{
int result;
sqlite3_stmt *stmt;
if(min.z <= -2048 && max.z >= 2048) {
stmt = stmt_get_block_pos;
} else {
stmt = stmt_get_block_pos_z;
int64_t minPos, maxPos;
if (min.z < -2048)
min.z = -2048;
if (max.z > 2048)
max.z = 2048;
getPosRange(minPos, maxPos, min.z, max.z - 1);
SQLOK(bind_int64(stmt, 1, minPos))
SQLOK(bind_int64(stmt, 2, maxPos))
}
std::vector<BlockPos> positions;
while ((result = sqlite3_step(stmt)) != SQLITE_DONE) {
if (result == SQLITE_BUSY) { // Wait some time and try again
usleep(10000);
} else if (result != SQLITE_ROW) {
throw std::runtime_error(sqlite3_errmsg(db));
}
int64_t posHash = sqlite3_column_int64(stmt, 0);
BlockPos pos = decodeBlockPos(posHash);
if(pos.x >= min.x && pos.x < max.x && pos.y >= min.y && pos.y < max.y)
positions.emplace_back(pos);
}
SQLOK(reset(stmt));
return positions;
}
void DBSQLite3::loadBlockCache(int16_t zPos)
{
int result;
blockCache.clear();
int64_t minPos, maxPos;
getPosRange(minPos, maxPos, zPos, zPos);
SQLOK(bind_int64(stmt_get_blocks_z, 1, minPos));
SQLOK(bind_int64(stmt_get_blocks_z, 2, maxPos));
while ((result = sqlite3_step(stmt_get_blocks_z)) != SQLITE_DONE) {
if (result == SQLITE_BUSY) { // Wait some time and try again
usleep(10000);
} else if (result != SQLITE_ROW) {
throw std::runtime_error(sqlite3_errmsg(db));
}
int64_t posHash = sqlite3_column_int64(stmt_get_blocks_z, 0);
BlockPos pos = decodeBlockPos(posHash);
const unsigned char *data = reinterpret_cast<const unsigned char *>(
sqlite3_column_blob(stmt_get_blocks_z, 1));
size_t size = sqlite3_column_bytes(stmt_get_blocks_z, 1);
blockCache[pos.x].emplace_back(pos, ustring(data, size));
}
SQLOK(reset(stmt_get_blocks_z))
}
void DBSQLite3::getBlocksOnXZ(BlockList &blocks, int16_t x, int16_t z,
int16_t min_y, int16_t max_y)
{
/* Cache the blocks on the given Z coordinate between calls, this only
* works due to order in which the TileGenerator asks for blocks. */
if (z != blockCachedZ) {
loadBlockCache(z);
blockCachedZ = z;
}
auto it = blockCache.find(x);
if (it == blockCache.end())
return;
if (it->second.empty()) {
/* We have swapped this list before, this is not supposed to happen
* because it's bad for performance. But rather than silently breaking
* do the right thing and load the blocks again. */
#ifndef NDEBUG
std::cerr << "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))
}
}

33
src/db-sqlite3.h Normal file
View File

@ -0,0 +1,33 @@
#pragma once
#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
};

123
src/db.h Normal file
View File

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

274
src/mapper.cpp Normal file
View File

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

5
src/types.h Normal file
View File

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

55
src/util.cpp Normal file
View File

@ -0,0 +1,55 @@
#include <stdexcept>
#include <sstream>
#include "util.h"
static 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());
}
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;
}
}

21
src/util.h Normal file
View File

@ -0,0 +1,21 @@
#pragma once
#include <string>
#include <iostream>
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);