Player data to Database (#5475)

* Player data to Database

Add player data into databases (SQLite3 & PG only)

PostgreSQL & SQLite: better POO Design for databases

Add --migrate-players argument to server + deprecation warning

* Remove players directory if empty
This commit is contained in:
Loïc Blot 2017-04-23 14:35:08 +02:00 committed by GitHub
parent dda171d292
commit 29ab20c272
31 changed files with 1555 additions and 378 deletions

@ -134,6 +134,7 @@ LOCAL_SRC_FILES := \
jni/src/convert_json.cpp \
jni/src/craftdef.cpp \
jni/src/database-dummy.cpp \
jni/src/database-files.cpp \
jni/src/database-sqlite3.cpp \
jni/src/database.cpp \
jni/src/debug.cpp \

@ -279,6 +279,31 @@ core.register_chatcommand("auth_reload", {
end,
})
core.register_chatcommand("remove_player", {
params = "<name>",
description = "Remove player data",
privs = {server=true},
func = function(name, param)
local toname = param
if toname == "" then
return false, "Name field required"
end
local rc = core.remove_player(toname)
if rc == 0 then
core.log("action", name .. " removed player data of " .. toname .. ".")
return true, "Player \"" .. toname .. "\" removed."
elseif rc == 1 then
return true, "No such player \"" .. toname .. "\" to remove."
elseif rc == 2 then
return true, "Player \"" .. toname .. "\" is connected, cannot remove."
end
return false, "Unhandled remove_player return code " .. rc .. ""
end,
})
core.register_chatcommand("teleport", {
params = "<X>,<Y>,<Z> | <to_name> | <name> <X>,<Y>,<Z> | <name> <to_name>",
description = "Teleport to player or position",

@ -2599,6 +2599,8 @@ These functions return the leftover itemstack.
* `minetest.cancel_shutdown_requests()`: cancel current delayed shutdown
* `minetest.get_server_status()`: returns server status string
* `minetest.get_server_uptime()`: returns the server uptime in seconds
* `minetest.remove_player(name)`: remove player from database (if he is not connected).
* Returns a code (0: successful, 1: no such player, 2: player is connected)
### Bans
* `minetest.get_ban_list()`: returns the ban list (same as `minetest.get_ban_description("")`)

@ -377,6 +377,7 @@ set(common_SRCS
convert_json.cpp
craftdef.cpp
database-dummy.cpp
database-files.cpp
database-leveldb.cpp
database-postgresql.cpp
database-redis.cpp

@ -770,7 +770,7 @@ void Client::initLocalMapSaving(const Address &address,
fs::CreateAllDirs(world_path);
m_localdb = new Database_SQLite3(world_path);
m_localdb = new MapDatabaseSQLite3(world_path);
m_localdb->beginSave();
actionstream << "Local map saving started, map will be saved at '" << world_path << "'" << std::endl;
}

@ -49,7 +49,7 @@ class ClientMediaDownloader;
struct MapDrawControl;
class MtEventManager;
struct PointedThing;
class Database;
class MapDatabase;
class Minimap;
struct MinimapMapblock;
class Camera;
@ -645,7 +645,7 @@ private:
LocalClientState m_state;
// Used for saving server map to disk client-side
Database *m_localdb;
MapDatabase *m_localdb;
IntervalLimiter m_localdb_save_interval;
u16 m_cache_save_interval;

@ -764,9 +764,10 @@ bool LuaEntitySAO::collideWithObjects() const
// No prototype, PlayerSAO does not need to be deserialized
PlayerSAO::PlayerSAO(ServerEnvironment *env_, u16 peer_id_, bool is_singleplayer):
PlayerSAO::PlayerSAO(ServerEnvironment *env_, RemotePlayer *player_, u16 peer_id_,
bool is_singleplayer):
UnitSAO(env_, v3f(0,0,0)),
m_player(NULL),
m_player(player_),
m_peer_id(peer_id_),
m_inventory(NULL),
m_damage(0),
@ -819,7 +820,7 @@ PlayerSAO::~PlayerSAO()
delete m_inventory;
}
void PlayerSAO::initialize(RemotePlayer *player, const std::set<std::string> &privs)
void PlayerSAO::finalize(RemotePlayer *player, const std::set<std::string> &privs)
{
assert(player);
m_player = player;

@ -194,7 +194,7 @@ class RemotePlayer;
class PlayerSAO : public UnitSAO
{
public:
PlayerSAO(ServerEnvironment *env_, u16 peer_id_, bool is_singleplayer);
PlayerSAO(ServerEnvironment *env_, RemotePlayer *player_, u16 peer_id_, bool is_singleplayer);
~PlayerSAO();
ActiveObjectType getType() const
{ return ACTIVEOBJECT_TYPE_PLAYER; }
@ -349,7 +349,7 @@ public:
bool getCollisionBox(aabb3f *toset) const;
bool collideWithObjects() const { return true; }
void initialize(RemotePlayer *player, const std::set<std::string> &privs);
void finalize(RemotePlayer *player, const std::set<std::string> &privs);
v3f getEyePosition() const { return m_base_position + getEyeOffset(); }
v3f getEyeOffset() const;

@ -25,7 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "database.h"
#include "irrlichttypes.h"
class Database_Dummy : public Database
class Database_Dummy : public MapDatabase, public PlayerDatabase
{
public:
bool saveBlock(const v3s16 &pos, const std::string &data);
@ -33,6 +33,13 @@ public:
bool deleteBlock(const v3s16 &pos);
void listAllLoadableBlocks(std::vector<v3s16> &dst);
void savePlayer(RemotePlayer *player) {}
bool loadPlayer(RemotePlayer *player, PlayerSAO *sao) { return true; }
bool removePlayer(const std::string &name) { return true; }
void listPlayers(std::vector<std::string> &) {}
void beginSave() {}
void endSave() {}
private:
std::map<s64, std::string> m_database;
};

179
src/database-files.cpp Normal file

@ -0,0 +1,179 @@
/*
Minetest
Copyright (C) 2017 nerzhul, Loic Blot <loic.blot@unix-experience.fr>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include <cassert>
#include <json/json.h>
#include "database-files.h"
#include "content_sao.h"
#include "remoteplayer.h"
#include "settings.h"
#include "porting.h"
#include "filesys.h"
// !!! WARNING !!!
// This backend is intended to be used on Minetest 0.4.16 only for the transition backend for
// player files
void PlayerDatabaseFiles::serialize(std::ostringstream &os, RemotePlayer *player)
{
// Utilize a Settings object for storing values
Settings args;
args.setS32("version", 1);
args.set("name", player->getName());
sanity_check(player->getPlayerSAO());
args.setS32("hp", player->getPlayerSAO()->getHP());
args.setV3F("position", player->getPlayerSAO()->getBasePosition());
args.setFloat("pitch", player->getPlayerSAO()->getPitch());
args.setFloat("yaw", player->getPlayerSAO()->getYaw());
args.setS32("breath", player->getPlayerSAO()->getBreath());
std::string extended_attrs = "";
player->serializeExtraAttributes(extended_attrs);
args.set("extended_attributes", extended_attrs);
args.writeLines(os);
os << "PlayerArgsEnd\n";
player->inventory.serialize(os);
}
void PlayerDatabaseFiles::savePlayer(RemotePlayer *player)
{
std::string savedir = m_savedir + DIR_DELIM;
std::string path = savedir + player->getName();
bool path_found = false;
RemotePlayer testplayer("", NULL);
for (u32 i = 0; i < PLAYER_FILE_ALTERNATE_TRIES && !path_found; i++) {
if (!fs::PathExists(path)) {
path_found = true;
continue;
}
// Open and deserialize file to check player name
std::ifstream is(path.c_str(), std::ios_base::binary);
if (!is.good()) {
errorstream << "Failed to open " << path << std::endl;
return;
}
testplayer.deSerialize(is, path, NULL);
is.close();
if (strcmp(testplayer.getName(), player->getName()) == 0) {
path_found = true;
continue;
}
path = savedir + player->getName() + itos(i);
}
if (!path_found) {
errorstream << "Didn't find free file for player " << player->getName()
<< std::endl;
return;
}
// Open and serialize file
std::ostringstream ss(std::ios_base::binary);
serialize(ss, player);
if (!fs::safeWriteToFile(path, ss.str())) {
infostream << "Failed to write " << path << std::endl;
}
player->setModified(false);
}
bool PlayerDatabaseFiles::removePlayer(const std::string &name)
{
std::string players_path = m_savedir + DIR_DELIM;
std::string path = players_path + name;
RemotePlayer temp_player("", NULL);
for (u32 i = 0; i < PLAYER_FILE_ALTERNATE_TRIES; i++) {
// Open file and deserialize
std::ifstream is(path.c_str(), std::ios_base::binary);
if (!is.good())
continue;
temp_player.deSerialize(is, path, NULL);
is.close();
if (temp_player.getName() == name) {
fs::DeleteSingleFileOrEmptyDirectory(path);
return true;
}
path = players_path + name + itos(i);
}
return false;
}
bool PlayerDatabaseFiles::loadPlayer(RemotePlayer *player, PlayerSAO *sao)
{
std::string players_path = m_savedir + DIR_DELIM;
std::string path = players_path + player->getName();
const std::string player_to_load = player->getName();
for (u32 i = 0; i < PLAYER_FILE_ALTERNATE_TRIES; i++) {
// Open file and deserialize
std::ifstream is(path.c_str(), std::ios_base::binary);
if (!is.good())
continue;
player->deSerialize(is, path, sao);
is.close();
if (player->getName() == player_to_load)
return true;
path = players_path + player_to_load + itos(i);
}
infostream << "Player file for player " << player_to_load << " not found" << std::endl;
return false;
}
void PlayerDatabaseFiles::listPlayers(std::vector<std::string> &res)
{
std::vector<fs::DirListNode> files = fs::GetDirListing(m_savedir);
// list files into players directory
for (std::vector<fs::DirListNode>::const_iterator it = files.begin(); it !=
files.end(); ++it) {
// Ignore directories
if (it->dir)
continue;
const std::string &filename = it->name;
std::string full_path = m_savedir + DIR_DELIM + filename;
std::ifstream is(full_path.c_str(), std::ios_base::binary);
if (!is.good())
continue;
RemotePlayer player(filename.c_str(), NULL);
// Null env & dummy peer_id
PlayerSAO playerSAO(NULL, &player, 15789, false);
player.deSerialize(is, "", &playerSAO);
is.close();
res.push_back(player.getName());
}
}

46
src/database-files.h Normal file

@ -0,0 +1,46 @@
/*
Minetest
Copyright (C) 2017 nerzhul, Loic Blot <loic.blot@unix-experience.fr>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#ifndef DATABASE_FILES_HEADER
#define DATABASE_FILES_HEADER
// !!! WARNING !!!
// This backend is intended to be used on Minetest 0.4.16 only for the transition backend for
// player files
#include "database.h"
class PlayerDatabaseFiles : public PlayerDatabase
{
public:
PlayerDatabaseFiles(const std::string &savedir) : m_savedir(savedir) {}
virtual ~PlayerDatabaseFiles() {}
void savePlayer(RemotePlayer *player);
bool loadPlayer(RemotePlayer *player, PlayerSAO *sao);
bool removePlayer(const std::string &name);
void listPlayers(std::vector<std::string> &res);
private:
void serialize(std::ostringstream &os, RemotePlayer *player);
std::string m_savedir;
};
#endif

@ -28,7 +28,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "database.h"
#include "leveldb/db.h"
class Database_LevelDB : public Database
class Database_LevelDB : public MapDatabase
{
public:
Database_LevelDB(const std::string &savedir);
@ -39,6 +39,8 @@ public:
bool deleteBlock(const v3s16 &pos);
void listAllLoadableBlocks(std::vector<v3s16> &dst);
void beginSave() {}
void endSave() {}
private:
leveldb::DB *m_database;
};

@ -39,13 +39,15 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "log.h"
#include "exceptions.h"
#include "settings.h"
#include "content_sao.h"
#include "remoteplayer.h"
Database_PostgreSQL::Database_PostgreSQL(const Settings &conf) :
m_connect_string(""),
Database_PostgreSQL::Database_PostgreSQL(const std::string &connect_string) :
m_connect_string(connect_string),
m_conn(NULL),
m_pgversion(0)
{
if (!conf.getNoEx("pgsql_connection", m_connect_string)) {
if (m_connect_string.empty()) {
throw SettingNotFoundException(
"Set pgsql_connection string in world.mt to "
"use the postgresql backend\n"
@ -57,8 +59,6 @@ Database_PostgreSQL::Database_PostgreSQL(const Settings &conf) :
"DELETE rights on the database.\n"
"Don't create mt_user as a SUPERUSER!");
}
connectToDatabase();
}
Database_PostgreSQL::~Database_PostgreSQL()
@ -118,40 +118,6 @@ bool Database_PostgreSQL::initialized() const
return (PQstatus(m_conn) == CONNECTION_OK);
}
void Database_PostgreSQL::initStatements()
{
prepareStatement("read_block",
"SELECT data FROM blocks "
"WHERE posX = $1::int4 AND posY = $2::int4 AND "
"posZ = $3::int4");
if (m_pgversion < 90500) {
prepareStatement("write_block_insert",
"INSERT INTO blocks (posX, posY, posZ, data) SELECT "
"$1::int4, $2::int4, $3::int4, $4::bytea "
"WHERE NOT EXISTS (SELECT true FROM blocks "
"WHERE posX = $1::int4 AND posY = $2::int4 AND "
"posZ = $3::int4)");
prepareStatement("write_block_update",
"UPDATE blocks SET data = $4::bytea "
"WHERE posX = $1::int4 AND posY = $2::int4 AND "
"posZ = $3::int4");
} else {
prepareStatement("write_block",
"INSERT INTO blocks (posX, posY, posZ, data) VALUES "
"($1::int4, $2::int4, $3::int4, $4::bytea) "
"ON CONFLICT ON CONSTRAINT blocks_pkey DO "
"UPDATE SET data = $4::bytea");
}
prepareStatement("delete_block", "DELETE FROM blocks WHERE "
"posX = $1::int4 AND posY = $2::int4 AND posZ = $3::int4");
prepareStatement("list_all_loadable_blocks",
"SELECT posX, posY, posZ FROM blocks");
}
PGresult *Database_PostgreSQL::checkResults(PGresult *result, bool clear)
{
ExecStatusType statusType = PQresultStatus(result);
@ -173,30 +139,21 @@ PGresult *Database_PostgreSQL::checkResults(PGresult *result, bool clear)
return result;
}
void Database_PostgreSQL::createDatabase()
void Database_PostgreSQL::createTableIfNotExists(const std::string &table_name,
const std::string &definition)
{
PGresult *result = checkResults(PQexec(m_conn,
"SELECT relname FROM pg_class WHERE relname='blocks';"),
false);
std::string sql_check_table = "SELECT relname FROM pg_class WHERE relname='" +
table_name + "';";
PGresult *result = checkResults(PQexec(m_conn, sql_check_table.c_str()), false);
// If table doesn't exist, create it
if (!PQntuples(result)) {
static const char* dbcreate_sql = "CREATE TABLE blocks ("
"posX INT NOT NULL,"
"posY INT NOT NULL,"
"posZ INT NOT NULL,"
"data BYTEA,"
"PRIMARY KEY (posX,posY,posZ)"
");";
checkResults(PQexec(m_conn, dbcreate_sql));
checkResults(PQexec(m_conn, definition.c_str()));
}
PQclear(result);
infostream << "PostgreSQL: Game Database was inited." << std::endl;
}
void Database_PostgreSQL::beginSave()
{
verifyDatabase();
@ -208,14 +165,70 @@ void Database_PostgreSQL::endSave()
checkResults(PQexec(m_conn, "COMMIT;"));
}
bool Database_PostgreSQL::saveBlock(const v3s16 &pos,
const std::string &data)
MapDatabasePostgreSQL::MapDatabasePostgreSQL(const std::string &connect_string):
Database_PostgreSQL(connect_string),
MapDatabase()
{
connectToDatabase();
}
void MapDatabasePostgreSQL::createDatabase()
{
createTableIfNotExists("blocks",
"CREATE TABLE blocks ("
"posX INT NOT NULL,"
"posY INT NOT NULL,"
"posZ INT NOT NULL,"
"data BYTEA,"
"PRIMARY KEY (posX,posY,posZ)"
");"
);
infostream << "PostgreSQL: Map Database was initialized." << std::endl;
}
void MapDatabasePostgreSQL::initStatements()
{
prepareStatement("read_block",
"SELECT data FROM blocks "
"WHERE posX = $1::int4 AND posY = $2::int4 AND "
"posZ = $3::int4");
if (getPGVersion() < 90500) {
prepareStatement("write_block_insert",
"INSERT INTO blocks (posX, posY, posZ, data) SELECT "
"$1::int4, $2::int4, $3::int4, $4::bytea "
"WHERE NOT EXISTS (SELECT true FROM blocks "
"WHERE posX = $1::int4 AND posY = $2::int4 AND "
"posZ = $3::int4)");
prepareStatement("write_block_update",
"UPDATE blocks SET data = $4::bytea "
"WHERE posX = $1::int4 AND posY = $2::int4 AND "
"posZ = $3::int4");
} else {
prepareStatement("write_block",
"INSERT INTO blocks (posX, posY, posZ, data) VALUES "
"($1::int4, $2::int4, $3::int4, $4::bytea) "
"ON CONFLICT ON CONSTRAINT blocks_pkey DO "
"UPDATE SET data = $4::bytea");
}
prepareStatement("delete_block", "DELETE FROM blocks WHERE "
"posX = $1::int4 AND posY = $2::int4 AND posZ = $3::int4");
prepareStatement("list_all_loadable_blocks",
"SELECT posX, posY, posZ FROM blocks");
}
bool MapDatabasePostgreSQL::saveBlock(const v3s16 &pos, const std::string &data)
{
// Verify if we don't overflow the platform integer with the mapblock size
if (data.size() > INT_MAX) {
errorstream << "Database_PostgreSQL::saveBlock: Data truncation! "
<< "data.size() over 0xFFFF (== " << data.size()
<< ")" << std::endl;
<< "data.size() over 0xFFFFFFFF (== " << data.size()
<< ")" << std::endl;
return false;
}
@ -232,7 +245,7 @@ bool Database_PostgreSQL::saveBlock(const v3s16 &pos,
};
const int argFmt[] = { 1, 1, 1, 1 };
if (m_pgversion < 90500) {
if (getPGVersion() < 90500) {
execPrepared("write_block_update", ARRLEN(args), args, argLen, argFmt);
execPrepared("write_block_insert", ARRLEN(args), args, argLen, argFmt);
} else {
@ -241,8 +254,7 @@ bool Database_PostgreSQL::saveBlock(const v3s16 &pos,
return true;
}
void Database_PostgreSQL::loadBlock(const v3s16 &pos,
std::string *block)
void MapDatabasePostgreSQL::loadBlock(const v3s16 &pos, std::string *block)
{
verifyDatabase();
@ -256,19 +268,17 @@ void Database_PostgreSQL::loadBlock(const v3s16 &pos,
const int argFmt[] = { 1, 1, 1 };
PGresult *results = execPrepared("read_block", ARRLEN(args), args,
argLen, argFmt, false);
argLen, argFmt, false);
*block = "";
if (PQntuples(results)) {
*block = std::string(PQgetvalue(results, 0, 0),
PQgetlength(results, 0, 0));
}
if (PQntuples(results))
*block = std::string(PQgetvalue(results, 0, 0), PQgetlength(results, 0, 0));
PQclear(results);
}
bool Database_PostgreSQL::deleteBlock(const v3s16 &pos)
bool MapDatabasePostgreSQL::deleteBlock(const v3s16 &pos)
{
verifyDatabase();
@ -286,19 +296,339 @@ bool Database_PostgreSQL::deleteBlock(const v3s16 &pos)
return true;
}
void Database_PostgreSQL::listAllLoadableBlocks(std::vector<v3s16> &dst)
void MapDatabasePostgreSQL::listAllLoadableBlocks(std::vector<v3s16> &dst)
{
verifyDatabase();
PGresult *results = execPrepared("list_all_loadable_blocks", 0,
NULL, NULL, NULL, false, false);
NULL, NULL, NULL, false, false);
int numrows = PQntuples(results);
for (int row = 0; row < numrows; ++row) {
for (int row = 0; row < numrows; ++row)
dst.push_back(pg_to_v3s16(results, 0, 0));
PQclear(results);
}
/*
* Player Database
*/
PlayerDatabasePostgreSQL::PlayerDatabasePostgreSQL(const std::string &connect_string):
Database_PostgreSQL(connect_string),
PlayerDatabase()
{
connectToDatabase();
}
void PlayerDatabasePostgreSQL::createDatabase()
{
createTableIfNotExists("player",
"CREATE TABLE player ("
"name VARCHAR(60) NOT NULL,"
"pitch NUMERIC(15, 7) NOT NULL,"
"yaw NUMERIC(15, 7) NOT NULL,"
"posX NUMERIC(15, 7) NOT NULL,"
"posY NUMERIC(15, 7) NOT NULL,"
"posZ NUMERIC(15, 7) NOT NULL,"
"hp INT NOT NULL,"
"breath INT NOT NULL,"
"creation_date TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),"
"modification_date TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT NOW(),"
"PRIMARY KEY (name)"
");"
);
createTableIfNotExists("player_inventories",
"CREATE TABLE player_inventories ("
"player VARCHAR(60) NOT NULL,"
"inv_id INT NOT NULL,"
"inv_width INT NOT NULL,"
"inv_name TEXT NOT NULL DEFAULT '',"
"inv_size INT NOT NULL,"
"PRIMARY KEY(player, inv_id),"
"CONSTRAINT player_inventories_fkey FOREIGN KEY (player) REFERENCES "
"player (name) ON DELETE CASCADE"
");"
);
createTableIfNotExists("player_inventory_items",
"CREATE TABLE player_inventory_items ("
"player VARCHAR(60) NOT NULL,"
"inv_id INT NOT NULL,"
"slot_id INT NOT NULL,"
"item TEXT NOT NULL DEFAULT '',"
"PRIMARY KEY(player, inv_id, slot_id),"
"CONSTRAINT player_inventory_items_fkey FOREIGN KEY (player) REFERENCES "
"player (name) ON DELETE CASCADE"
");"
);
createTableIfNotExists("player_metadata",
"CREATE TABLE player_metadata ("
"player VARCHAR(60) NOT NULL,"
"attr VARCHAR(256) NOT NULL,"
"value TEXT,"
"PRIMARY KEY(player, attr),"
"CONSTRAINT player_metadata_fkey FOREIGN KEY (player) REFERENCES "
"player (name) ON DELETE CASCADE"
");"
);
infostream << "PostgreSQL: Player Database was inited." << std::endl;
}
void PlayerDatabasePostgreSQL::initStatements()
{
if (getPGVersion() < 90500) {
prepareStatement("create_player",
"INSERT INTO player(name, pitch, yaw, posX, posY, posZ, hp, breath) VALUES "
"($1, $2, $3, $4, $5, $6, $7::int, $8::int)");
prepareStatement("update_player",
"UPDATE SET pitch = $2, yaw = $3, posX = $4, posY = $5, posZ = $6, hp = $7::int, "
"breath = $8::int, modification_date = NOW() WHERE name = $1");
} else {
prepareStatement("save_player",
"INSERT INTO player(name, pitch, yaw, posX, posY, posZ, hp, breath) VALUES "
"($1, $2, $3, $4, $5, $6, $7::int, $8::int)"
"ON CONFLICT ON CONSTRAINT player_pkey DO UPDATE SET pitch = $2, yaw = $3, "
"posX = $4, posY = $5, posZ = $6, hp = $7::int, breath = $8::int, "
"modification_date = NOW()");
}
prepareStatement("remove_player", "DELETE FROM player WHERE name = $1");
prepareStatement("load_player_list", "SELECT name FROM player");
prepareStatement("remove_player_inventories",
"DELETE FROM player_inventories WHERE player = $1");
prepareStatement("remove_player_inventory_items",
"DELETE FROM player_inventory_items WHERE player = $1");
prepareStatement("add_player_inventory",
"INSERT INTO player_inventories (player, inv_id, inv_width, inv_name, inv_size) VALUES "
"($1, $2::int, $3::int, $4, $5::int)");
prepareStatement("add_player_inventory_item",
"INSERT INTO player_inventory_items (player, inv_id, slot_id, item) VALUES "
"($1, $2::int, $3::int, $4)");
prepareStatement("load_player_inventories",
"SELECT inv_id, inv_width, inv_name, inv_size FROM player_inventories "
"WHERE player = $1 ORDER BY inv_id");
prepareStatement("load_player_inventory_items",
"SELECT slot_id, item FROM player_inventory_items WHERE "
"player = $1 AND inv_id = $2::int");
prepareStatement("load_player",
"SELECT pitch, yaw, posX, posY, posZ, hp, breath FROM player WHERE name = $1");
prepareStatement("remove_player_metadata",
"DELETE FROM player_metadata WHERE player = $1");
prepareStatement("save_player_metadata",
"INSERT INTO player_metadata (player, attr, value) VALUES ($1, $2, $3)");
prepareStatement("load_player_metadata",
"SELECT attr, value FROM player_metadata WHERE player = $1");
}
bool PlayerDatabasePostgreSQL::playerDataExists(const std::string &playername)
{
verifyDatabase();
const char *values[] = { playername.c_str() };
PGresult *results = execPrepared("load_player", 1, values, false);
bool res = (PQntuples(results) > 0);
PQclear(results);
return res;
}
void PlayerDatabasePostgreSQL::savePlayer(RemotePlayer *player)
{
PlayerSAO* sao = player->getPlayerSAO();
if (!sao)
return;
verifyDatabase();
v3f pos = sao->getBasePosition();
std::string pitch = ftos(sao->getPitch());
std::string yaw = ftos(sao->getYaw());
std::string posx = ftos(pos.X);
std::string posy = ftos(pos.Y);
std::string posz = ftos(pos.Z);
std::string hp = itos(sao->getHP());
std::string breath = itos(sao->getBreath());
const char *values[] = {
player->getName(),
pitch.c_str(),
yaw.c_str(),
posx.c_str(), posy.c_str(), posz.c_str(),
hp.c_str(),
breath.c_str()
};
const char* rmvalues[] = { player->getName() };
beginSave();
if (getPGVersion() < 90500) {
if (!playerDataExists(player->getName()))
execPrepared("create_player", 8, values, true, false);
else
execPrepared("update_player", 8, values, true, false);
}
else
execPrepared("save_player", 8, values, true, false);
// Write player inventories
execPrepared("remove_player_inventories", 1, rmvalues);
execPrepared("remove_player_inventory_items", 1, rmvalues);
std::vector<const InventoryList*> inventory_lists = sao->getInventory()->getLists();
for (u16 i = 0; i < inventory_lists.size(); i++) {
const InventoryList* list = inventory_lists[i];
std::string name = list->getName(), width = itos(list->getWidth()),
inv_id = itos(i), lsize = itos(list->getSize());
const char* inv_values[] = {
player->getName(),
inv_id.c_str(),
width.c_str(),
name.c_str(),
lsize.c_str()
};
execPrepared("add_player_inventory", 5, inv_values);
for (u32 j = 0; j < list->getSize(); j++) {
std::ostringstream os;
list->getItem(j).serialize(os);
std::string itemStr = os.str(), slotId = itos(j);
const char* invitem_values[] = {
player->getName(),
inv_id.c_str(),
slotId.c_str(),
itemStr.c_str()
};
execPrepared("add_player_inventory_item", 4, invitem_values);
}
}
execPrepared("remove_player_metadata", 1, rmvalues);
const PlayerAttributes &attrs = sao->getExtendedAttributes();
for (PlayerAttributes::const_iterator it = attrs.begin(); it != attrs.end(); ++it) {
const char *meta_values[] = {
player->getName(),
it->first.c_str(),
it->second.c_str()
};
execPrepared("save_player_metadata", 3, meta_values);
}
endSave();
}
bool PlayerDatabasePostgreSQL::loadPlayer(RemotePlayer *player, PlayerSAO *sao)
{
sanity_check(sao);
verifyDatabase();
const char *values[] = { player->getName() };
PGresult *results = execPrepared("load_player", 1, values, false, false);
// Player not found, return not found
if (!PQntuples(results)) {
PQclear(results);
return false;
}
sao->setPitch(pg_to_float(results, 0, 0));
sao->setYaw(pg_to_float(results, 0, 1));
sao->setBasePosition(v3f(
pg_to_float(results, 0, 2),
pg_to_float(results, 0, 3),
pg_to_float(results, 0, 4))
);
sao->setHPRaw((s16) pg_to_int(results, 0, 5));
sao->setBreath((u16) pg_to_int(results, 0, 6), false);
PQclear(results);
// Load inventory
results = execPrepared("load_player_inventories", 1, values, false, false);
int resultCount = PQntuples(results);
for (int row = 0; row < resultCount; ++row) {
InventoryList* invList = player->inventory.
addList(PQgetvalue(results, row, 2), pg_to_uint(results, row, 3));
invList->setWidth(pg_to_uint(results, row, 1));
u32 invId = pg_to_uint(results, row, 0);
std::string invIdStr = itos(invId);
const char* values2[] = {
player->getName(),
invIdStr.c_str()
};
PGresult *results2 = execPrepared("load_player_inventory_items", 2,
values2, false, false);
int resultCount2 = PQntuples(results2);
for (int row2 = 0; row2 < resultCount2; row2++) {
const std::string itemStr = PQgetvalue(results2, row2, 1);
if (itemStr.length() > 0) {
ItemStack stack;
stack.deSerialize(itemStr);
invList->addItem(pg_to_uint(results2, row2, 0), stack);
}
}
PQclear(results2);
}
PQclear(results);
results = execPrepared("load_player_metadata", 1, values, false);
int numrows = PQntuples(results);
for (int row = 0; row < numrows; row++) {
sao->setExtendedAttribute(PQgetvalue(results, row, 0),PQgetvalue(results, row, 1));
}
PQclear(results);
return true;
}
bool PlayerDatabasePostgreSQL::removePlayer(const std::string &name)
{
if (!playerDataExists(name))
return false;
verifyDatabase();
const char *values[] = { name.c_str() };
execPrepared("remove_player", 1, values);
return true;
}
void PlayerDatabasePostgreSQL::listPlayers(std::vector<std::string> &res)
{
verifyDatabase();
PGresult *results = execPrepared("load_player_list", 0, NULL, false);
int numrows = PQntuples(results);
for (int row = 0; row < numrows; row++)
res.push_back(PQgetvalue(results, row, 0));
PQclear(results);
}

@ -27,55 +27,35 @@ with this program; if not, write to the Free Software Foundation, Inc.,
class Settings;
class Database_PostgreSQL : public Database
class Database_PostgreSQL: public Database
{
public:
Database_PostgreSQL(const Settings &conf);
Database_PostgreSQL(const std::string &connect_string);
~Database_PostgreSQL();
void beginSave();
void endSave();
bool saveBlock(const v3s16 &pos, const std::string &data);
void loadBlock(const v3s16 &pos, std::string *block);
bool deleteBlock(const v3s16 &pos);
void listAllLoadableBlocks(std::vector<v3s16> &dst);
bool initialized() const;
private:
// Database initialization
void connectToDatabase();
void initStatements();
void createDatabase();
inline void prepareStatement(const std::string &name, const std::string &sql)
{
checkResults(PQprepare(m_conn, name.c_str(), sql.c_str(), 0, NULL));
}
// Database connectivity checks
void ping();
void verifyDatabase();
// Database usage
PGresult *checkResults(PGresult *res, bool clear = true);
inline PGresult *execPrepared(const char *stmtName, const int paramsNumber,
const void **params,
const int *paramsLengths = NULL, const int *paramsFormats = NULL,
bool clear = true, bool nobinary = true)
{
return checkResults(PQexecPrepared(m_conn, stmtName, paramsNumber,
(const char* const*) params, paramsLengths, paramsFormats,
nobinary ? 1 : 0), clear);
}
protected:
// Conversion helpers
inline int pg_to_int(PGresult *res, int row, int col)
{
return atoi(PQgetvalue(res, row, col));
}
inline u32 pg_to_uint(PGresult *res, int row, int col)
{
return (u32) atoi(PQgetvalue(res, row, col));
}
inline float pg_to_float(PGresult *res, int row, int col)
{
return (float) atof(PQgetvalue(res, row, col));
}
inline v3s16 pg_to_v3s16(PGresult *res, int row, int col)
{
return v3s16(
@ -85,11 +65,86 @@ private:
);
}
inline PGresult *execPrepared(const char *stmtName, const int paramsNumber,
const void **params,
const int *paramsLengths = NULL, const int *paramsFormats = NULL,
bool clear = true, bool nobinary = true)
{
return checkResults(PQexecPrepared(m_conn, stmtName, paramsNumber,
(const char* const*) params, paramsLengths, paramsFormats,
nobinary ? 1 : 0), clear);
}
inline PGresult *execPrepared(const char *stmtName, const int paramsNumber,
const char **params, bool clear = true, bool nobinary = true)
{
return execPrepared(stmtName, paramsNumber,
(const void **)params, NULL, NULL, clear, nobinary);
}
void createTableIfNotExists(const std::string &table_name, const std::string &definition);
void verifyDatabase();
// Database initialization
void connectToDatabase();
virtual void createDatabase() = 0;
virtual void initStatements() = 0;
inline void prepareStatement(const std::string &name, const std::string &sql)
{
checkResults(PQprepare(m_conn, name.c_str(), sql.c_str(), 0, NULL));
}
const int getPGVersion() const { return m_pgversion; }
private:
// Database connectivity checks
void ping();
// Database usage
PGresult *checkResults(PGresult *res, bool clear = true);
// Attributes
std::string m_connect_string;
PGconn *m_conn;
int m_pgversion;
};
class MapDatabasePostgreSQL : private Database_PostgreSQL, public MapDatabase
{
public:
MapDatabasePostgreSQL(const std::string &connect_string);
virtual ~MapDatabasePostgreSQL() {}
bool saveBlock(const v3s16 &pos, const std::string &data);
void loadBlock(const v3s16 &pos, std::string *block);
bool deleteBlock(const v3s16 &pos);
void listAllLoadableBlocks(std::vector<v3s16> &dst);
void beginSave() { Database_PostgreSQL::beginSave(); }
void endSave() { Database_PostgreSQL::endSave(); }
protected:
virtual void createDatabase();
virtual void initStatements();
};
class PlayerDatabasePostgreSQL : private Database_PostgreSQL, public PlayerDatabase
{
public:
PlayerDatabasePostgreSQL(const std::string &connect_string);
virtual ~PlayerDatabasePostgreSQL() {}
void savePlayer(RemotePlayer *player);
bool loadPlayer(RemotePlayer *player, PlayerSAO *sao);
bool removePlayer(const std::string &name);
void listPlayers(std::vector<std::string> &res);
protected:
virtual void createDatabase();
virtual void initStatements();
private:
bool playerDataExists(const std::string &playername);
};
#endif

@ -30,7 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
class Settings;
class Database_Redis : public Database
class Database_Redis : public MapDatabase
{
public:
Database_Redis(Settings &conf);

@ -33,6 +33,8 @@ SQLite format specification:
#include "settings.h"
#include "porting.h"
#include "util/string.h"
#include "content_sao.h"
#include "remoteplayer.h"
#include <cassert>
@ -111,27 +113,26 @@ int Database_SQLite3::busyHandler(void *data, int count)
}
Database_SQLite3::Database_SQLite3(const std::string &savedir) :
Database_SQLite3::Database_SQLite3(const std::string &savedir, const std::string &dbname) :
m_database(NULL),
m_initialized(false),
m_savedir(savedir),
m_database(NULL),
m_stmt_read(NULL),
m_stmt_write(NULL),
m_stmt_list(NULL),
m_stmt_delete(NULL),
m_dbname(dbname),
m_stmt_begin(NULL),
m_stmt_end(NULL)
{
}
void Database_SQLite3::beginSave() {
void Database_SQLite3::beginSave()
{
verifyDatabase();
SQLRES(sqlite3_step(m_stmt_begin), SQLITE_DONE,
"Failed to start SQLite3 transaction");
sqlite3_reset(m_stmt_begin);
}
void Database_SQLite3::endSave() {
void Database_SQLite3::endSave()
{
verifyDatabase();
SQLRES(sqlite3_step(m_stmt_end), SQLITE_DONE,
"Failed to commit SQLite3 transaction");
@ -142,7 +143,7 @@ void Database_SQLite3::openDatabase()
{
if (m_database) return;
std::string dbp = m_savedir + DIR_DELIM + "map.sqlite";
std::string dbp = m_savedir + DIR_DELIM + m_dbname + ".sqlite";
// Open the database connection
@ -170,6 +171,8 @@ void Database_SQLite3::openDatabase()
+ itos(g_settings->getU16("sqlite_synchronous"));
SQLOK(sqlite3_exec(m_database, query_str.c_str(), NULL, NULL, NULL),
"Failed to modify sqlite3 synchronous mode");
SQLOK(sqlite3_exec(m_database, "PRAGMA foreign_keys = ON", NULL, NULL, NULL),
"Failed to enable sqlite3 foreign key support");
}
void Database_SQLite3::verifyDatabase()
@ -178,8 +181,61 @@ void Database_SQLite3::verifyDatabase()
openDatabase();
PREPARE_STATEMENT(begin, "BEGIN");
PREPARE_STATEMENT(end, "COMMIT");
PREPARE_STATEMENT(begin, "BEGIN;");
PREPARE_STATEMENT(end, "COMMIT;");
initStatements();
m_initialized = true;
}
Database_SQLite3::~Database_SQLite3()
{
FINALIZE_STATEMENT(m_stmt_begin)
FINALIZE_STATEMENT(m_stmt_end)
SQLOK_ERRSTREAM(sqlite3_close(m_database), "Failed to close database");
}
/*
* Map database
*/
MapDatabaseSQLite3::MapDatabaseSQLite3(const std::string &savedir):
Database_SQLite3(savedir, "map"),
MapDatabase(),
m_stmt_read(NULL),
m_stmt_write(NULL),
m_stmt_list(NULL),
m_stmt_delete(NULL)
{
}
MapDatabaseSQLite3::~MapDatabaseSQLite3()
{
FINALIZE_STATEMENT(m_stmt_read)
FINALIZE_STATEMENT(m_stmt_write)
FINALIZE_STATEMENT(m_stmt_list)
FINALIZE_STATEMENT(m_stmt_delete)
}
void MapDatabaseSQLite3::createDatabase()
{
assert(m_database); // Pre-condition
SQLOK(sqlite3_exec(m_database,
"CREATE TABLE IF NOT EXISTS `blocks` (\n"
" `pos` INT PRIMARY KEY,\n"
" `data` BLOB\n"
");\n",
NULL, NULL, NULL),
"Failed to create database table");
}
void MapDatabaseSQLite3::initStatements()
{
PREPARE_STATEMENT(read, "SELECT `data` FROM `blocks` WHERE `pos` = ? LIMIT 1");
#ifdef __ANDROID__
PREPARE_STATEMENT(write, "INSERT INTO `blocks` (`pos`, `data`) VALUES (?, ?)");
@ -189,18 +245,16 @@ void Database_SQLite3::verifyDatabase()
PREPARE_STATEMENT(delete, "DELETE FROM `blocks` WHERE `pos` = ?");
PREPARE_STATEMENT(list, "SELECT `pos` FROM `blocks`");
m_initialized = true;
verbosestream << "ServerMap: SQLite3 database opened." << std::endl;
}
inline void Database_SQLite3::bindPos(sqlite3_stmt *stmt, const v3s16 &pos, int index)
inline void MapDatabaseSQLite3::bindPos(sqlite3_stmt *stmt, const v3s16 &pos, int index)
{
SQLOK(sqlite3_bind_int64(stmt, index, getBlockAsInteger(pos)),
"Internal error: failed to bind query at " __FILE__ ":" TOSTRING(__LINE__));
}
bool Database_SQLite3::deleteBlock(const v3s16 &pos)
bool MapDatabaseSQLite3::deleteBlock(const v3s16 &pos)
{
verifyDatabase();
@ -216,7 +270,7 @@ bool Database_SQLite3::deleteBlock(const v3s16 &pos)
return good;
}
bool Database_SQLite3::saveBlock(const v3s16 &pos, const std::string &data)
bool MapDatabaseSQLite3::saveBlock(const v3s16 &pos, const std::string &data)
{
verifyDatabase();
@ -243,7 +297,7 @@ bool Database_SQLite3::saveBlock(const v3s16 &pos, const std::string &data)
return true;
}
void Database_SQLite3::loadBlock(const v3s16 &pos, std::string *block)
void MapDatabaseSQLite3::loadBlock(const v3s16 &pos, std::string *block)
{
verifyDatabase();
@ -264,37 +318,312 @@ void Database_SQLite3::loadBlock(const v3s16 &pos, std::string *block)
sqlite3_reset(m_stmt_read);
}
void Database_SQLite3::createDatabase()
{
assert(m_database); // Pre-condition
SQLOK(sqlite3_exec(m_database,
"CREATE TABLE IF NOT EXISTS `blocks` (\n"
" `pos` INT PRIMARY KEY,\n"
" `data` BLOB\n"
");\n",
NULL, NULL, NULL),
"Failed to create database table");
}
void Database_SQLite3::listAllLoadableBlocks(std::vector<v3s16> &dst)
void MapDatabaseSQLite3::listAllLoadableBlocks(std::vector<v3s16> &dst)
{
verifyDatabase();
while (sqlite3_step(m_stmt_list) == SQLITE_ROW) {
while (sqlite3_step(m_stmt_list) == SQLITE_ROW)
dst.push_back(getIntegerAsBlock(sqlite3_column_int64(m_stmt_list, 0)));
}
sqlite3_reset(m_stmt_list);
}
Database_SQLite3::~Database_SQLite3()
{
FINALIZE_STATEMENT(m_stmt_read)
FINALIZE_STATEMENT(m_stmt_write)
FINALIZE_STATEMENT(m_stmt_list)
FINALIZE_STATEMENT(m_stmt_begin)
FINALIZE_STATEMENT(m_stmt_end)
FINALIZE_STATEMENT(m_stmt_delete)
/*
* Player Database
*/
SQLOK_ERRSTREAM(sqlite3_close(m_database), "Failed to close database");
PlayerDatabaseSQLite3::PlayerDatabaseSQLite3(const std::string &savedir):
Database_SQLite3(savedir, "players"),
PlayerDatabase(),
m_stmt_player_load(NULL),
m_stmt_player_add(NULL),
m_stmt_player_update(NULL),
m_stmt_player_remove(NULL),
m_stmt_player_list(NULL),
m_stmt_player_load_inventory(NULL),
m_stmt_player_load_inventory_items(NULL),
m_stmt_player_add_inventory(NULL),
m_stmt_player_add_inventory_items(NULL),
m_stmt_player_remove_inventory(NULL),
m_stmt_player_remove_inventory_items(NULL),
m_stmt_player_metadata_load(NULL),
m_stmt_player_metadata_remove(NULL),
m_stmt_player_metadata_add(NULL)
{
}
PlayerDatabaseSQLite3::~PlayerDatabaseSQLite3()
{
FINALIZE_STATEMENT(m_stmt_player_load)
FINALIZE_STATEMENT(m_stmt_player_add)
FINALIZE_STATEMENT(m_stmt_player_update)
FINALIZE_STATEMENT(m_stmt_player_remove)
FINALIZE_STATEMENT(m_stmt_player_list)
FINALIZE_STATEMENT(m_stmt_player_add_inventory)
FINALIZE_STATEMENT(m_stmt_player_add_inventory_items)
FINALIZE_STATEMENT(m_stmt_player_remove_inventory)
FINALIZE_STATEMENT(m_stmt_player_remove_inventory_items)
FINALIZE_STATEMENT(m_stmt_player_load_inventory)
FINALIZE_STATEMENT(m_stmt_player_load_inventory_items)
FINALIZE_STATEMENT(m_stmt_player_metadata_load)
FINALIZE_STATEMENT(m_stmt_player_metadata_add)
FINALIZE_STATEMENT(m_stmt_player_metadata_remove)
};
void PlayerDatabaseSQLite3::createDatabase()
{
assert(m_database); // Pre-condition
SQLOK(sqlite3_exec(m_database,
"CREATE TABLE IF NOT EXISTS `player` ("
"`name` VARCHAR(50) NOT NULL,"
"`pitch` NUMERIC(11, 4) NOT NULL,"
"`yaw` NUMERIC(11, 4) NOT NULL,"
"`posX` NUMERIC(11, 4) NOT NULL,"
"`posY` NUMERIC(11, 4) NOT NULL,"
"`posZ` NUMERIC(11, 4) NOT NULL,"
"`hp` INT NOT NULL,"
"`breath` INT NOT NULL,"
"`creation_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,"
"`modification_date` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,"
"PRIMARY KEY (`name`));",
NULL, NULL, NULL),
"Failed to create player table");
SQLOK(sqlite3_exec(m_database,
"CREATE TABLE IF NOT EXISTS `player_metadata` ("
" `player` VARCHAR(50) NOT NULL,"
" `metadata` VARCHAR(256) NOT NULL,"
" `value` TEXT,"
" PRIMARY KEY(`player`, `metadata`),"
" FOREIGN KEY (`player`) REFERENCES player (`name`) ON DELETE CASCADE );",
NULL, NULL, NULL),
"Failed to create player metadata table");
SQLOK(sqlite3_exec(m_database,
"CREATE TABLE IF NOT EXISTS `player_inventories` ("
" `player` VARCHAR(50) NOT NULL,"
" `inv_id` INT NOT NULL,"
" `inv_width` INT NOT NULL,"
" `inv_name` TEXT NOT NULL DEFAULT '',"
" `inv_size` INT NOT NULL,"
" PRIMARY KEY(player, inv_id),"
" FOREIGN KEY (`player`) REFERENCES player (`name`) ON DELETE CASCADE );",
NULL, NULL, NULL),
"Failed to create player inventory table");
SQLOK(sqlite3_exec(m_database,
"CREATE TABLE `player_inventory_items` ("
" `player` VARCHAR(50) NOT NULL,"
" `inv_id` INT NOT NULL,"
" `slot_id` INT NOT NULL,"
" `item` TEXT NOT NULL DEFAULT '',"
" PRIMARY KEY(player, inv_id, slot_id),"
" FOREIGN KEY (`player`) REFERENCES player (`name`) ON DELETE CASCADE );",
NULL, NULL, NULL),
"Failed to create player inventory items table");
}
void PlayerDatabaseSQLite3::initStatements()
{
PREPARE_STATEMENT(player_load, "SELECT `pitch`, `yaw`, `posX`, `posY`, `posZ`, `hp`, "
"`breath`"
"FROM `player` WHERE `name` = ?")
PREPARE_STATEMENT(player_add, "INSERT INTO `player` (`name`, `pitch`, `yaw`, `posX`, "
"`posY`, `posZ`, `hp`, `breath`) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
PREPARE_STATEMENT(player_update, "UPDATE `player` SET `pitch` = ?, `yaw` = ?, "
"`posX` = ?, `posY` = ?, `posZ` = ?, `hp` = ?, `breath` = ?, "
"`modification_date` = CURRENT_TIMESTAMP WHERE `name` = ?")
PREPARE_STATEMENT(player_remove, "DELETE FROM `player` WHERE `name` = ?")
PREPARE_STATEMENT(player_list, "SELECT `name` FROM `player`")
PREPARE_STATEMENT(player_add_inventory, "INSERT INTO `player_inventories` "
"(`player`, `inv_id`, `inv_width`, `inv_name`, `inv_size`) VALUES (?, ?, ?, ?, ?)")
PREPARE_STATEMENT(player_add_inventory_items, "INSERT INTO `player_inventory_items` "
"(`player`, `inv_id`, `slot_id`, `item`) VALUES (?, ?, ?, ?)")
PREPARE_STATEMENT(player_remove_inventory, "DELETE FROM `player_inventories` "
"WHERE `player` = ?")
PREPARE_STATEMENT(player_remove_inventory_items, "DELETE FROM `player_inventory_items` "
"WHERE `player` = ?")
PREPARE_STATEMENT(player_load_inventory, "SELECT `inv_id`, `inv_width`, `inv_name`, "
"`inv_size` FROM `player_inventories` WHERE `player` = ? ORDER BY inv_id")
PREPARE_STATEMENT(player_load_inventory_items, "SELECT `slot_id`, `item` "
"FROM `player_inventory_items` WHERE `player` = ? AND `inv_id` = ?")
PREPARE_STATEMENT(player_metadata_load, "SELECT `metadata`, `value` FROM "
"`player_metadata` WHERE `player` = ?")
PREPARE_STATEMENT(player_metadata_add, "INSERT INTO `player_metadata` "
"(`player`, `metadata`, `value`) VALUES (?, ?, ?)")
PREPARE_STATEMENT(player_metadata_remove, "DELETE FROM `player_metadata` "
"WHERE `player` = ?")
verbosestream << "ServerEnvironment: SQLite3 database opened (players)." << std::endl;
}
bool PlayerDatabaseSQLite3::playerDataExists(const std::string &name)
{
verifyDatabase();
str_to_sqlite(m_stmt_player_load, 1, name);
bool res = (sqlite3_step(m_stmt_player_load) == SQLITE_ROW);
sqlite3_reset(m_stmt_player_load);
return res;
}
void PlayerDatabaseSQLite3::savePlayer(RemotePlayer *player)
{
PlayerSAO* sao = player->getPlayerSAO();
sanity_check(sao);
const v3f &pos = sao->getBasePosition();
// Begin save in brace is mandatory
if (!playerDataExists(player->getName())) {
beginSave();
str_to_sqlite(m_stmt_player_add, 1, player->getName());
double_to_sqlite(m_stmt_player_add, 2, sao->getPitch());
double_to_sqlite(m_stmt_player_add, 3, sao->getYaw());
double_to_sqlite(m_stmt_player_add, 4, pos.X);
double_to_sqlite(m_stmt_player_add, 5, pos.Y);
double_to_sqlite(m_stmt_player_add, 6, pos.Z);
int64_to_sqlite(m_stmt_player_add, 7, sao->getHP());
int64_to_sqlite(m_stmt_player_add, 8, sao->getBreath());
sqlite3_vrfy(sqlite3_step(m_stmt_player_add), SQLITE_DONE);
sqlite3_reset(m_stmt_player_add);
} else {
beginSave();
double_to_sqlite(m_stmt_player_update, 1, sao->getPitch());
double_to_sqlite(m_stmt_player_update, 2, sao->getYaw());
double_to_sqlite(m_stmt_player_update, 3, pos.X);
double_to_sqlite(m_stmt_player_update, 4, pos.Y);
double_to_sqlite(m_stmt_player_update, 5, pos.Z);
int64_to_sqlite(m_stmt_player_update, 6, sao->getHP());
int64_to_sqlite(m_stmt_player_update, 7, sao->getBreath());
str_to_sqlite(m_stmt_player_update, 8, player->getName());
sqlite3_vrfy(sqlite3_step(m_stmt_player_update), SQLITE_DONE);
sqlite3_reset(m_stmt_player_update);
}
// Write player inventories
str_to_sqlite(m_stmt_player_remove_inventory, 1, player->getName());
sqlite3_vrfy(sqlite3_step(m_stmt_player_remove_inventory), SQLITE_DONE);
sqlite3_reset(m_stmt_player_remove_inventory);
str_to_sqlite(m_stmt_player_remove_inventory_items, 1, player->getName());
sqlite3_vrfy(sqlite3_step(m_stmt_player_remove_inventory_items), SQLITE_DONE);
sqlite3_reset(m_stmt_player_remove_inventory_items);
std::vector<const InventoryList*> inventory_lists = sao->getInventory()->getLists();
for (u16 i = 0; i < inventory_lists.size(); i++) {
const InventoryList* list = inventory_lists[i];
str_to_sqlite(m_stmt_player_add_inventory, 1, player->getName());
int_to_sqlite(m_stmt_player_add_inventory, 2, i);
int_to_sqlite(m_stmt_player_add_inventory, 3, list->getWidth());
str_to_sqlite(m_stmt_player_add_inventory, 4, list->getName());
int_to_sqlite(m_stmt_player_add_inventory, 5, list->getSize());
sqlite3_vrfy(sqlite3_step(m_stmt_player_add_inventory), SQLITE_DONE);
sqlite3_reset(m_stmt_player_add_inventory);
for (u32 j = 0; j < list->getSize(); j++) {
std::ostringstream os;
list->getItem(j).serialize(os);
std::string itemStr = os.str();
str_to_sqlite(m_stmt_player_add_inventory_items, 1, player->getName());
int_to_sqlite(m_stmt_player_add_inventory_items, 2, i);
int_to_sqlite(m_stmt_player_add_inventory_items, 3, j);
str_to_sqlite(m_stmt_player_add_inventory_items, 4, itemStr);
sqlite3_vrfy(sqlite3_step(m_stmt_player_add_inventory_items), SQLITE_DONE);
sqlite3_reset(m_stmt_player_add_inventory_items);
}
}
str_to_sqlite(m_stmt_player_metadata_remove, 1, player->getName());
sqlite3_vrfy(sqlite3_step(m_stmt_player_metadata_remove), SQLITE_DONE);
sqlite3_reset(m_stmt_player_metadata_remove);
const PlayerAttributes &attrs = sao->getExtendedAttributes();
for (PlayerAttributes::const_iterator it = attrs.begin(); it != attrs.end(); ++it) {
str_to_sqlite(m_stmt_player_metadata_add, 1, player->getName());
str_to_sqlite(m_stmt_player_metadata_add, 2, it->first);
str_to_sqlite(m_stmt_player_metadata_add, 3, it->second);
sqlite3_vrfy(sqlite3_step(m_stmt_player_metadata_add), SQLITE_DONE);
sqlite3_reset(m_stmt_player_metadata_add);
}
endSave();
}
bool PlayerDatabaseSQLite3::loadPlayer(RemotePlayer *player, PlayerSAO *sao)
{
verifyDatabase();
str_to_sqlite(m_stmt_player_load, 1, player->getName());
if (sqlite3_step(m_stmt_player_load) != SQLITE_ROW) {
sqlite3_reset(m_stmt_player_load);
return false;
}
sao->setPitch(sqlite_to_float(m_stmt_player_load, 0));
sao->setYaw(sqlite_to_float(m_stmt_player_load, 1));
sao->setBasePosition(sqlite_to_v3f(m_stmt_player_load, 2));
sao->setHPRaw((s16) MYMIN(sqlite_to_int(m_stmt_player_load, 5), S16_MAX));
sao->setBreath((u16) MYMIN(sqlite_to_int(m_stmt_player_load, 6), U16_MAX), false);
sqlite3_reset(m_stmt_player_load);
// Load inventory
str_to_sqlite(m_stmt_player_load_inventory, 1, player->getName());
while (sqlite3_step(m_stmt_player_load_inventory) == SQLITE_ROW) {
InventoryList *invList = player->inventory.addList(
sqlite_to_string(m_stmt_player_load_inventory, 2),
sqlite_to_uint(m_stmt_player_load_inventory, 3));
invList->setWidth(sqlite_to_uint(m_stmt_player_load_inventory, 1));
u32 invId = sqlite_to_uint(m_stmt_player_load_inventory, 0);
str_to_sqlite(m_stmt_player_load_inventory_items, 1, player->getName());
int_to_sqlite(m_stmt_player_load_inventory_items, 2, invId);
while (sqlite3_step(m_stmt_player_load_inventory_items) == SQLITE_ROW) {
const std::string itemStr = sqlite_to_string(m_stmt_player_load_inventory_items, 1);
if (itemStr.length() > 0) {
ItemStack stack;
stack.deSerialize(itemStr);
invList->addItem(sqlite_to_uint(m_stmt_player_load_inventory_items, 0), stack);
}
}
sqlite3_reset(m_stmt_player_load_inventory_items);
}
sqlite3_reset(m_stmt_player_load_inventory);
str_to_sqlite(m_stmt_player_metadata_load, 1, sao->getPlayer()->getName());
while (sqlite3_step(m_stmt_player_metadata_load) == SQLITE_ROW) {
std::string attr = sqlite_to_string(m_stmt_player_metadata_load, 0);
std::string value = sqlite_to_string(m_stmt_player_metadata_load, 1);
sao->setExtendedAttribute(attr, value);
}
sqlite3_reset(m_stmt_player_metadata_load);
return true;
}
bool PlayerDatabaseSQLite3::removePlayer(const std::string &name)
{
if (!playerDataExists(name))
return false;
str_to_sqlite(m_stmt_player_remove, 1, name);
sqlite3_vrfy(sqlite3_step(m_stmt_player_remove), SQLITE_DONE);
sqlite3_reset(m_stmt_player_remove);
return true;
}
void PlayerDatabaseSQLite3::listPlayers(std::vector<std::string> &res)
{
verifyDatabase();
while (sqlite3_step(m_stmt_player_list) == SQLITE_ROW)
res.push_back(sqlite_to_string(m_stmt_player_list, 0));
sqlite3_reset(m_stmt_player_list);
}

@ -20,8 +20,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#ifndef DATABASE_SQLITE3_HEADER
#define DATABASE_SQLITE3_HEADER
#include <cstring>
#include <string>
#include "database.h"
#include "exceptions.h"
extern "C" {
#include "sqlite3.h"
@ -30,37 +32,97 @@ extern "C" {
class Database_SQLite3 : public Database
{
public:
Database_SQLite3(const std::string &savedir);
~Database_SQLite3();
virtual ~Database_SQLite3();
void beginSave();
void endSave();
bool saveBlock(const v3s16 &pos, const std::string &data);
void loadBlock(const v3s16 &pos, std::string *block);
bool deleteBlock(const v3s16 &pos);
void listAllLoadableBlocks(std::vector<v3s16> &dst);
bool initialized() const { return m_initialized; }
protected:
Database_SQLite3(const std::string &savedir, const std::string &dbname);
private:
// Open the database
void openDatabase();
// Create the database structure
void createDatabase();
// Open and initialize the database if needed
void verifyDatabase();
void bindPos(sqlite3_stmt *stmt, const v3s16 &pos, int index = 1);
// Convertors
inline void str_to_sqlite(sqlite3_stmt *s, int iCol, const std::string &str) const
{
sqlite3_vrfy(sqlite3_bind_text(s, iCol, str.c_str(), str.size(), NULL));
}
inline void str_to_sqlite(sqlite3_stmt *s, int iCol, const char *str) const
{
sqlite3_vrfy(sqlite3_bind_text(s, iCol, str, strlen(str), NULL));
}
inline void int_to_sqlite(sqlite3_stmt *s, int iCol, int val) const
{
sqlite3_vrfy(sqlite3_bind_int(s, iCol, val));
}
inline void int64_to_sqlite(sqlite3_stmt *s, int iCol, s64 val) const
{
sqlite3_vrfy(sqlite3_bind_int64(s, iCol, (sqlite3_int64) val));
}
inline void double_to_sqlite(sqlite3_stmt *s, int iCol, double val) const
{
sqlite3_vrfy(sqlite3_bind_double(s, iCol, val));
}
inline std::string sqlite_to_string(sqlite3_stmt *s, int iCol)
{
const char* text = reinterpret_cast<const char*>(sqlite3_column_text(s, iCol));
return std::string(text ? text : "");
}
inline s32 sqlite_to_int(sqlite3_stmt *s, int iCol)
{
return sqlite3_column_int(s, iCol);
}
inline u32 sqlite_to_uint(sqlite3_stmt *s, int iCol)
{
return (u32) sqlite3_column_int(s, iCol);
}
inline float sqlite_to_float(sqlite3_stmt *s, int iCol)
{
return (float) sqlite3_column_double(s, iCol);
}
inline const v3f sqlite_to_v3f(sqlite3_stmt *s, int iCol)
{
return v3f(sqlite_to_float(s, iCol), sqlite_to_float(s, iCol + 1),
sqlite_to_float(s, iCol + 2));
}
// Query verifiers helpers
inline void sqlite3_vrfy(int s, const std::string &m = "", int r = SQLITE_OK) const
{
if (s != r)
throw DatabaseException(m + ": " + sqlite3_errmsg(m_database));
}
inline void sqlite3_vrfy(const int s, const int r, const std::string &m = "") const
{
sqlite3_vrfy(s, m, r);
}
// Create the database structure
virtual void createDatabase() = 0;
virtual void initStatements() = 0;
sqlite3 *m_database;
private:
// Open the database
void openDatabase();
bool m_initialized;
std::string m_savedir;
std::string m_dbname;
sqlite3 *m_database;
sqlite3_stmt *m_stmt_read;
sqlite3_stmt *m_stmt_write;
sqlite3_stmt *m_stmt_list;
sqlite3_stmt *m_stmt_delete;
sqlite3_stmt *m_stmt_begin;
sqlite3_stmt *m_stmt_end;
@ -69,4 +131,66 @@ private:
static int busyHandler(void *data, int count);
};
class MapDatabaseSQLite3 : private Database_SQLite3, public MapDatabase
{
public:
MapDatabaseSQLite3(const std::string &savedir);
virtual ~MapDatabaseSQLite3();
bool saveBlock(const v3s16 &pos, const std::string &data);
void loadBlock(const v3s16 &pos, std::string *block);
bool deleteBlock(const v3s16 &pos);
void listAllLoadableBlocks(std::vector<v3s16> &dst);
void beginSave() { Database_SQLite3::beginSave(); }
void endSave() { Database_SQLite3::endSave(); }
protected:
virtual void createDatabase();
virtual void initStatements();
private:
void bindPos(sqlite3_stmt *stmt, const v3s16 &pos, int index = 1);
// Map
sqlite3_stmt *m_stmt_read;
sqlite3_stmt *m_stmt_write;
sqlite3_stmt *m_stmt_list;
sqlite3_stmt *m_stmt_delete;
};
class PlayerDatabaseSQLite3 : private Database_SQLite3, public PlayerDatabase
{
public:
PlayerDatabaseSQLite3(const std::string &savedir);
virtual ~PlayerDatabaseSQLite3();
void savePlayer(RemotePlayer *player);
bool loadPlayer(RemotePlayer *player, PlayerSAO *sao);
bool removePlayer(const std::string &name);
void listPlayers(std::vector<std::string> &res);
protected:
virtual void createDatabase();
virtual void initStatements();
private:
bool playerDataExists(const std::string &name);
// Players
sqlite3_stmt *m_stmt_player_load;
sqlite3_stmt *m_stmt_player_add;
sqlite3_stmt *m_stmt_player_update;
sqlite3_stmt *m_stmt_player_remove;
sqlite3_stmt *m_stmt_player_list;
sqlite3_stmt *m_stmt_player_load_inventory;
sqlite3_stmt *m_stmt_player_load_inventory_items;
sqlite3_stmt *m_stmt_player_add_inventory;
sqlite3_stmt *m_stmt_player_add_inventory_items;
sqlite3_stmt *m_stmt_player_remove_inventory;
sqlite3_stmt *m_stmt_player_remove_inventory_items;
sqlite3_stmt *m_stmt_player_metadata_load;
sqlite3_stmt *m_stmt_player_metadata_remove;
sqlite3_stmt *m_stmt_player_metadata_add;
};
#endif

@ -48,7 +48,7 @@ static inline s64 pythonmodulo(s64 i, s16 mod)
}
s64 Database::getBlockAsInteger(const v3s16 &pos)
s64 MapDatabase::getBlockAsInteger(const v3s16 &pos)
{
return (u64) pos.Z * 0x1000000 +
(u64) pos.Y * 0x1000 +
@ -56,7 +56,7 @@ s64 Database::getBlockAsInteger(const v3s16 &pos)
}
v3s16 Database::getIntegerAsBlock(s64 i)
v3s16 MapDatabase::getIntegerAsBlock(s64 i)
{
v3s16 pos;
pos.X = unsigned_to_signed(pythonmodulo(i, 4096), 2048);

@ -29,10 +29,15 @@ with this program; if not, write to the Free Software Foundation, Inc.,
class Database
{
public:
virtual ~Database() {}
virtual void beginSave() = 0;
virtual void endSave() = 0;
virtual bool initialized() const { return true; }
};
virtual void beginSave() {}
virtual void endSave() {}
class MapDatabase : public Database
{
public:
virtual ~MapDatabase() {}
virtual bool saveBlock(const v3s16 &pos, const std::string &data) = 0;
virtual void loadBlock(const v3s16 &pos, std::string *block) = 0;
@ -42,8 +47,19 @@ public:
static v3s16 getIntegerAsBlock(s64 i);
virtual void listAllLoadableBlocks(std::vector<v3s16> &dst) = 0;
};
virtual bool initialized() const { return true; }
class PlayerSAO;
class RemotePlayer;
class PlayerDatabase
{
public:
virtual ~PlayerDatabase() {}
virtual void savePlayer(RemotePlayer *player) = 0;
virtual bool loadPlayer(RemotePlayer *player, PlayerSAO *sao) = 0;
virtual bool removePlayer(const std::string &name) = 0;
virtual void listPlayers(std::vector<std::string> &res) = 0;
};
#endif

@ -47,6 +47,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#endif
#ifndef SERVER
#include "client/clientlauncher.h"
#endif
#ifdef HAVE_TOUCHSCREENGUI
@ -102,7 +103,7 @@ static bool get_game_from_cmdline(GameParams *game_params, const Settings &cmd_a
static bool determine_subgame(GameParams *game_params);
static bool run_dedicated_server(const GameParams &game_params, const Settings &cmd_args);
static bool migrate_database(const GameParams &game_params, const Settings &cmd_args);
static bool migrate_map_database(const GameParams &game_params, const Settings &cmd_args);
/**********************************************************************/
@ -292,6 +293,8 @@ static void set_allowed_options(OptionList *allowed_options)
_("Set gameid (\"--gameid list\" prints available ones)"))));
allowed_options->insert(std::make_pair("migrate", ValueSpec(VALUETYPE_STRING,
_("Migrate from current map backend to another (Only works when using minetestserver or with --server)"))));
allowed_options->insert(std::make_pair("migrate-players", ValueSpec(VALUETYPE_STRING,
_("Migrate from current players backend to another (Only works when using minetestserver or with --server)"))));
allowed_options->insert(std::make_pair("terminal", ValueSpec(VALUETYPE_FLAG,
_("Feature an interactive terminal (Only works when using minetestserver or with --server)"))));
#ifndef SERVER
@ -332,7 +335,7 @@ static void print_allowed_options(const OptionList &allowed_options)
if (i->second.type != VALUETYPE_FLAG)
os1 << _(" <value>");
std::cout << padStringRight(os1.str(), 24);
std::cout << padStringRight(os1.str(), 30);
if (i->second.help != NULL)
std::cout << i->second.help;
@ -828,7 +831,9 @@ static bool run_dedicated_server(const GameParams &game_params, const Settings &
// Database migration
if (cmd_args.exists("migrate"))
return migrate_database(game_params, cmd_args);
return migrate_map_database(game_params, cmd_args);
else if (cmd_args.exists("migrate-players"))
return ServerEnvironment::migratePlayersDatabase(game_params, cmd_args);
if (cmd_args.exists("terminal")) {
#if USE_CURSES
@ -912,7 +917,7 @@ static bool run_dedicated_server(const GameParams &game_params, const Settings &
return true;
}
static bool migrate_database(const GameParams &game_params, const Settings &cmd_args)
static bool migrate_map_database(const GameParams &game_params, const Settings &cmd_args)
{
std::string migrate_to = cmd_args.get("migrate");
Settings world_mt;
@ -921,20 +926,23 @@ static bool migrate_database(const GameParams &game_params, const Settings &cmd_
errorstream << "Cannot read world.mt!" << std::endl;
return false;
}
if (!world_mt.exists("backend")) {
errorstream << "Please specify your current backend in world.mt:"
<< std::endl
<< " backend = {sqlite3|leveldb|redis|dummy}"
<< " backend = {sqlite3|leveldb|redis|dummy|postgresql}"
<< std::endl;
return false;
}
std::string backend = world_mt.get("backend");
if (backend == migrate_to) {
errorstream << "Cannot migrate: new backend is same"
<< " as the old one" << std::endl;
return false;
}
Database *old_db = ServerMap::createDatabase(backend, game_params.world_path, world_mt),
MapDatabase *old_db = ServerMap::createDatabase(backend, game_params.world_path, world_mt),
*new_db = ServerMap::createDatabase(migrate_to, game_params.world_path, world_mt);
u32 count = 0;
@ -976,4 +984,3 @@ static bool migrate_database(const GameParams &game_params, const Settings &cmd_
return true;
}

@ -2286,13 +2286,13 @@ bool ServerMap::loadSectorFull(v2s16 p2d)
}
#endif
Database *ServerMap::createDatabase(
MapDatabase *ServerMap::createDatabase(
const std::string &name,
const std::string &savedir,
Settings &conf)
{
if (name == "sqlite3")
return new Database_SQLite3(savedir);
return new MapDatabaseSQLite3(savedir);
if (name == "dummy")
return new Database_Dummy();
#if USE_LEVELDB
@ -2304,8 +2304,11 @@ Database *ServerMap::createDatabase(
return new Database_Redis(conf);
#endif
#if USE_POSTGRESQL
else if (name == "postgresql")
return new Database_PostgreSQL(conf);
else if (name == "postgresql") {
std::string connect_string = "";
conf.getNoEx("pgsql_connection", connect_string);
return new MapDatabasePostgreSQL(connect_string);
}
#endif
else
throw BaseException(std::string("Database backend ") + name + " not supported.");
@ -2326,7 +2329,7 @@ bool ServerMap::saveBlock(MapBlock *block)
return saveBlock(block, dbase);
}
bool ServerMap::saveBlock(MapBlock *block, Database *db)
bool ServerMap::saveBlock(MapBlock *block, MapDatabase *db)
{
v3s16 p3d = block->getPos();

@ -37,7 +37,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "map_settings_manager.h"
class Settings;
class Database;
class MapDatabase;
class ClientMap;
class MapSector;
class ServerMapSector;
@ -430,7 +430,7 @@ public:
/*
Database functions
*/
static Database *createDatabase(const std::string &name, const std::string &savedir, Settings &conf);
static MapDatabase *createDatabase(const std::string &name, const std::string &savedir, Settings &conf);
// Returns true if the database file does not exist
bool loadFromFolders();
@ -458,7 +458,7 @@ public:
bool loadSectorMeta(v2s16 p2d);
bool saveBlock(MapBlock *block);
static bool saveBlock(MapBlock *block, Database *db);
static bool saveBlock(MapBlock *block, MapDatabase *db);
// This will generate a sector with getSector if not found.
void loadBlock(const std::string &sectordir, const std::string &blockfile,
MapSector *sector, bool save_after_load=false);
@ -510,7 +510,7 @@ private:
This is reset to false when written on disk.
*/
bool m_map_metadata_changed;
Database *dbase;
MapDatabase *dbase;
};

@ -67,54 +67,6 @@ RemotePlayer::RemotePlayer(const char *name, IItemDefManager *idef):
movement_gravity = g_settings->getFloat("movement_gravity") * BS;
}
void RemotePlayer::save(std::string savedir, IGameDef *gamedef)
{
/*
* We have to open all possible player files in the players directory
* and check their player names because some file systems are not
* case-sensitive and player names are case-sensitive.
*/
// A player to deserialize files into to check their names
RemotePlayer testplayer("", gamedef->idef());
savedir += DIR_DELIM;
std::string path = savedir + m_name;
for (u32 i = 0; i < PLAYER_FILE_ALTERNATE_TRIES; i++) {
if (!fs::PathExists(path)) {
// Open file and serialize
std::ostringstream ss(std::ios_base::binary);
serialize(ss);
if (!fs::safeWriteToFile(path, ss.str())) {
infostream << "Failed to write " << path << std::endl;
}
setModified(false);
return;
}
// Open file and deserialize
std::ifstream is(path.c_str(), std::ios_base::binary);
if (!is.good()) {
infostream << "Failed to open " << path << std::endl;
return;
}
testplayer.deSerialize(is, path, NULL);
is.close();
if (strcmp(testplayer.getName(), m_name) == 0) {
// Open file and serialize
std::ostringstream ss(std::ios_base::binary);
serialize(ss);
if (!fs::safeWriteToFile(path, ss.str())) {
infostream << "Failed to write " << path << std::endl;
}
setModified(false);
return;
}
path = savedir + m_name + itos(i);
}
infostream << "Didn't find free file for player " << m_name << std::endl;
}
void RemotePlayer::serializeExtraAttributes(std::string &output)
{
assert(m_sao);

@ -37,11 +37,11 @@ enum RemotePlayerChatResult
*/
class RemotePlayer : public Player
{
friend class PlayerDatabaseFiles;
public:
RemotePlayer(const char *name, IItemDefManager *idef);
virtual ~RemotePlayer() {}
void save(std::string savedir, IGameDef *gamedef);
void deSerialize(std::istream &is, const std::string &playername, PlayerSAO *sao);
PlayerSAO *getPlayerSAO() { return m_sao; }

@ -334,6 +334,22 @@ int ModApiServer::l_kick_player(lua_State *L)
return 1;
}
int ModApiServer::l_remove_player(lua_State *L)
{
NO_MAP_LOCK_REQUIRED;
std::string name = luaL_checkstring(L, 1);
ServerEnvironment *s_env = dynamic_cast<ServerEnvironment *>(getEnv(L));
assert(s_env);
RemotePlayer *player = s_env->getPlayer(name.c_str());
if (!player)
lua_pushinteger(L, s_env->removePlayerFromDatabase(name) ? 0 : 1);
else
lua_pushinteger(L, 2);
return 1;
}
// unban_player_or_ip()
int ModApiServer::l_unban_player_or_ip(lua_State *L)
{
@ -510,6 +526,7 @@ void ModApiServer::Initialize(lua_State *L, int top)
API_FCT(get_ban_description);
API_FCT(ban_player);
API_FCT(kick_player);
API_FCT(remove_player);
API_FCT(unban_player_or_ip);
API_FCT(notify_authentication_modified);

@ -92,6 +92,9 @@ private:
// kick_player(name, [message]) -> success
static int l_kick_player(lua_State *L);
// remove_player(name)
static int l_remove_player(lua_State *L);
// notify_authentication_modified(name)
static int l_notify_authentication_modified(lua_State *L);

@ -60,6 +60,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "util/base64.h"
#include "util/sha1.h"
#include "util/hex.h"
#include "database.h"
class ClientNotFoundException : public BaseException
{
@ -2618,9 +2619,8 @@ void Server::RespawnPlayer(u16 peer_id)
bool repositioned = m_script->on_respawnplayer(playersao);
if (!repositioned) {
v3f pos = findSpawnPos();
// setPos will send the new position to client
playersao->setPos(pos);
playersao->setPos(findSpawnPos());
}
SendPlayerHP(peer_id);
@ -3442,8 +3442,8 @@ v3f Server::findSpawnPos()
s32 range = 1 + i;
// We're going to try to throw the player to this position
v2s16 nodepos2d = v2s16(
-range + (myrand() % (range * 2)),
-range + (myrand() % (range * 2)));
-range + (myrand() % (range * 2)),
-range + (myrand() % (range * 2)));
// Get spawn level at point
s16 spawn_level = m_emerge->getSpawnLevelAtPoint(nodepos2d);
@ -3516,8 +3516,6 @@ void Server::requestShutdown(const std::string &msg, bool reconnect, float delay
PlayerSAO* Server::emergePlayer(const char *name, u16 peer_id, u16 proto_version)
{
bool newplayer = false;
/*
Try to get an existing player
*/
@ -3538,45 +3536,19 @@ PlayerSAO* Server::emergePlayer(const char *name, u16 peer_id, u16 proto_version
return NULL;
}
// Create a new player active object
PlayerSAO *playersao = new PlayerSAO(m_env, peer_id, isSingleplayer());
player = m_env->loadPlayer(name, playersao);
// Create player if it doesn't exist
if (!player) {
newplayer = true;
player = new RemotePlayer(name, this->idef());
// Set player position
infostream<<"Server: Finding spawn place for player \""
<<name<<"\""<<std::endl;
playersao->setBasePosition(findSpawnPos());
// Make sure the player is saved
player->setModified(true);
// Add player to environment
m_env->addPlayer(player);
} else {
// If the player exists, ensure that they respawn inside legal bounds
// This fixes an assert crash when the player can't be added
// to the environment
if (objectpos_over_limit(playersao->getBasePosition())) {
actionstream << "Respawn position for player \""
<< name << "\" outside limits, resetting" << std::endl;
playersao->setBasePosition(findSpawnPos());
}
player = new RemotePlayer(name, idef());
}
playersao->initialize(player, getPlayerEffectivePrivs(player->getName()));
bool newplayer = false;
// Load player
PlayerSAO *playersao = m_env->loadPlayer(player, &newplayer, peer_id, isSingleplayer());
// Complete init with server parts
playersao->finalize(player, getPlayerEffectivePrivs(player->getName()));
player->protocol_version = proto_version;
/* Clean up old HUD elements from previous sessions */
player->clearHud();
/* Add object to environment */
m_env->addActiveObject(playersao);
/* Run scripts */
if (newplayer) {
m_script->on_newplayer(playersao);

@ -306,6 +306,7 @@ public:
bool showFormspec(const char *name, const std::string &formspec, const std::string &formname);
Map & getMap() { return m_env->getMap(); }
ServerEnvironment & getEnv() { return *m_env; }
v3f findSpawnPos();
u32 hudAdd(RemotePlayer *player, HudElement *element);
bool hudRemove(RemotePlayer *player, u32 id);
@ -472,8 +473,6 @@ private:
RemotePlayer *player = NULL);
void handleAdminChat(const ChatEventChat *evt);
v3f findSpawnPos();
// When called, connection mutex should be locked
RemoteClient* getClient(u16 peer_id,ClientState state_min=CS_Active);
RemoteClient* getClientNoEx(u16 peer_id,ClientState state_min=CS_Active);

@ -36,6 +36,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "util/pointedthing.h"
#include "threading/mutex_auto_lock.h"
#include "filesys.h"
#include "gameparams.h"
#include "database-dummy.h"
#include "database-files.h"
#include "database-sqlite3.h"
#if USE_POSTGRESQL
#include "database-postgresql.h"
#endif
#define LBM_NAME_ALLOWED_CHARS "abcdefghijklmnopqrstuvwxyz0123456789_:"
@ -365,8 +372,30 @@ ServerEnvironment::ServerEnvironment(ServerMap *map,
m_game_time_fraction_counter(0),
m_last_clear_objects_time(0),
m_recommended_send_interval(0.1),
m_max_lag_estimate(0.1)
m_max_lag_estimate(0.1),
m_player_database(NULL)
{
// Determine which database backend to use
std::string conf_path = path_world + DIR_DELIM + "world.mt";
Settings conf;
bool succeeded = conf.readConfigFile(conf_path.c_str());
if (!succeeded || !conf.exists("player_backend")) {
// fall back to files
conf.set("player_backend", "files");
warningstream << "/!\\ You are using old player file backend. "
<< "This backend is deprecated and will be removed in next release /!\\"
<< std::endl << "Switching to SQLite3 or PostgreSQL is advised, "
<< "please read http://wiki.minetest.net/Database_backends." << std::endl;
if (!conf.updateConfigFile(conf_path.c_str())) {
errorstream << "ServerEnvironment::ServerEnvironment(): "
<< "Failed to update world.mt!" << std::endl;
}
}
std::string name = "";
conf.getNoEx("player_backend", name);
m_player_database = openPlayerDatabase(name, path_world, conf);
}
ServerEnvironment::~ServerEnvironment()
@ -392,6 +421,8 @@ ServerEnvironment::~ServerEnvironment()
i != m_players.end(); ++i) {
delete (*i);
}
delete m_player_database;
}
Map & ServerEnvironment::getMap()
@ -455,6 +486,11 @@ void ServerEnvironment::removePlayer(RemotePlayer *player)
}
}
bool ServerEnvironment::removePlayerFromDatabase(const std::string &name)
{
return m_player_database->removePlayer(name);
}
bool ServerEnvironment::line_of_sight(v3f pos1, v3f pos2, float stepsize, v3s16 *p)
{
float distance = pos1.getDistanceFrom(pos2);
@ -495,7 +531,7 @@ void ServerEnvironment::kickAllPlayers(AccessDeniedCode reason,
void ServerEnvironment::saveLoadedPlayers()
{
std::string players_path = m_path_world + DIR_DELIM "players";
std::string players_path = m_path_world + DIR_DELIM + "players";
fs::CreateDir(players_path);
for (std::vector<RemotePlayer *>::iterator it = m_players.begin();
@ -503,63 +539,63 @@ void ServerEnvironment::saveLoadedPlayers()
++it) {
if ((*it)->checkModified() ||
((*it)->getPlayerSAO() && (*it)->getPlayerSAO()->extendedAttributesModified())) {
(*it)->save(players_path, m_server);
try {
m_player_database->savePlayer(*it);
} catch (DatabaseException &e) {
errorstream << "Failed to save player " << (*it)->getName() << " exception: "
<< e.what() << std::endl;
throw;
}
}
}
}
void ServerEnvironment::savePlayer(RemotePlayer *player)
{
std::string players_path = m_path_world + DIR_DELIM "players";
fs::CreateDir(players_path);
player->save(players_path, m_server);
try {
m_player_database->savePlayer(player);
} catch (DatabaseException &e) {
errorstream << "Failed to save player " << player->getName() << " exception: "
<< e.what() << std::endl;
throw;
}
}
RemotePlayer *ServerEnvironment::loadPlayer(const std::string &playername, PlayerSAO *sao)
PlayerSAO *ServerEnvironment::loadPlayer(RemotePlayer *player, bool *new_player,
u16 peer_id, bool is_singleplayer)
{
bool newplayer = false;
bool found = false;
std::string players_path = m_path_world + DIR_DELIM "players" DIR_DELIM;
std::string path = players_path + playername;
PlayerSAO *playersao = new PlayerSAO(this, player, peer_id, is_singleplayer);
// Create player if it doesn't exist
if (!m_player_database->loadPlayer(player, playersao)) {
*new_player = true;
// Set player position
infostream << "Server: Finding spawn place for player \""
<< player->getName() << "\"" << std::endl;
playersao->setBasePosition(m_server->findSpawnPos());
RemotePlayer *player = getPlayer(playername.c_str());
if (!player) {
player = new RemotePlayer("", m_server->idef());
newplayer = true;
}
for (u32 i = 0; i < PLAYER_FILE_ALTERNATE_TRIES; i++) {
//// Open file and deserialize
std::ifstream is(path.c_str(), std::ios_base::binary);
if (!is.good())
continue;
player->deSerialize(is, path, sao);
is.close();
if (player->getName() == playername) {
found = true;
break;
// Make sure the player is saved
player->setModified(true);
} else {
// If the player exists, ensure that they respawn inside legal bounds
// This fixes an assert crash when the player can't be added
// to the environment
if (objectpos_over_limit(playersao->getBasePosition())) {
actionstream << "Respawn position for player \""
<< player->getName() << "\" outside limits, resetting" << std::endl;
playersao->setBasePosition(m_server->findSpawnPos());
}
path = players_path + playername + itos(i);
}
if (!found) {
infostream << "Player file for player " << playername
<< " not found" << std::endl;
if (newplayer)
delete player;
// Add player to environment
addPlayer(player);
return NULL;
}
/* Clean up old HUD elements from previous sessions */
player->clearHud();
if (newplayer) {
addPlayer(player);
}
player->setModified(false);
return player;
/* Add object to environment */
addActiveObject(playersao);
return playersao;
}
void ServerEnvironment::saveMeta()
@ -2173,3 +2209,111 @@ void ServerEnvironment::deactivateFarObjects(bool _force_delete)
m_active_objects.erase(*i);
}
}
PlayerDatabase *ServerEnvironment::openPlayerDatabase(const std::string &name,
const std::string &savedir, const Settings &conf)
{
if (name == "sqlite3")
return new PlayerDatabaseSQLite3(savedir);
else if (name == "dummy")
return new Database_Dummy();
#if USE_POSTGRESQL
else if (name == "postgresql") {
std::string connect_string = "";
conf.getNoEx("pgsql_player_connection", connect_string);
return new PlayerDatabasePostgreSQL(connect_string);
}
#endif
else if (name == "files")
return new PlayerDatabaseFiles(savedir + DIR_DELIM + "players");
else
throw BaseException(std::string("Database backend ") + name + " not supported.");
}
bool ServerEnvironment::migratePlayersDatabase(const GameParams &game_params,
const Settings &cmd_args)
{
std::string migrate_to = cmd_args.get("migrate-players");
Settings world_mt;
std::string world_mt_path = game_params.world_path + DIR_DELIM + "world.mt";
if (!world_mt.readConfigFile(world_mt_path.c_str())) {
errorstream << "Cannot read world.mt!" << std::endl;
return false;
}
if (!world_mt.exists("player_backend")) {
errorstream << "Please specify your current backend in world.mt:"
<< std::endl
<< " player_backend = {files|sqlite3|postgresql}"
<< std::endl;
return false;
}
std::string backend = world_mt.get("player_backend");
if (backend == migrate_to) {
errorstream << "Cannot migrate: new backend is same"
<< " as the old one" << std::endl;
return false;
}
const std::string players_backup_path = game_params.world_path + DIR_DELIM
+ "players.bak";
if (backend == "files") {
// Create backup directory
fs::CreateDir(players_backup_path);
}
try {
PlayerDatabase *srcdb = ServerEnvironment::openPlayerDatabase(backend,
game_params.world_path, world_mt);
PlayerDatabase *dstdb = ServerEnvironment::openPlayerDatabase(migrate_to,
game_params.world_path, world_mt);
std::vector<std::string> player_list;
srcdb->listPlayers(player_list);
for (std::vector<std::string>::const_iterator it = player_list.begin();
it != player_list.end(); ++it) {
actionstream << "Migrating player " << it->c_str() << std::endl;
RemotePlayer player(it->c_str(), NULL);
PlayerSAO playerSAO(NULL, &player, 15000, false);
srcdb->loadPlayer(&player, &playerSAO);
playerSAO.finalize(&player, std::set<std::string>());
player.setPlayerSAO(&playerSAO);
dstdb->savePlayer(&player);
// For files source, move player files to backup dir
if (backend == "files") {
fs::Rename(
game_params.world_path + DIR_DELIM + "players" + DIR_DELIM + (*it),
players_backup_path + DIR_DELIM + (*it));
}
}
actionstream << "Successfully migrated " << player_list.size() << " players"
<< std::endl;
world_mt.set("player_backend", migrate_to);
if (!world_mt.updateConfigFile(world_mt_path.c_str()))
errorstream << "Failed to update world.mt!" << std::endl;
else
actionstream << "world.mt updated" << std::endl;
// When migration is finished from file backend, remove players directory if empty
if (backend == "files") {
fs::DeleteSingleFileOrEmptyDirectory(game_params.world_path + DIR_DELIM
+ "players");
}
delete srcdb;
delete dstdb;
} catch (BaseException &e) {
errorstream << "An error occured during migration: " << e.what() << std::endl;
return false;
}
return true;
}

@ -27,7 +27,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
class IGameDef;
class ServerMap;
struct GameParams;
class RemotePlayer;
class PlayerDatabase;
class PlayerSAO;
class ServerEnvironment;
class ActiveBlockModifier;
@ -217,9 +219,11 @@ public:
// Save players
void saveLoadedPlayers();
void savePlayer(RemotePlayer *player);
RemotePlayer *loadPlayer(const std::string &playername, PlayerSAO *sao);
PlayerSAO *loadPlayer(RemotePlayer *player, bool *new_player, u16 peer_id,
bool is_singleplayer);
void addPlayer(RemotePlayer *player);
void removePlayer(RemotePlayer *player);
bool removePlayerFromDatabase(const std::string &name);
/*
Save and load time of day and game timer
@ -334,8 +338,13 @@ public:
RemotePlayer *getPlayer(const u16 peer_id);
RemotePlayer *getPlayer(const char* name);
static bool migratePlayersDatabase(const GameParams &game_params,
const Settings &cmd_args);
private:
static PlayerDatabase *openPlayerDatabase(const std::string &name,
const std::string &savedir, const Settings &conf);
/*
Internal ActiveObject interface
-------------------------------------------
@ -419,6 +428,8 @@ private:
// peer_ids in here should be unique, except that there may be many 0s
std::vector<RemotePlayer*> m_players;
PlayerDatabase *m_player_database;
// Particles
IntervalLimiter m_particle_management_interval;
UNORDERED_MAP<u32, float> m_particle_spawners;

@ -31,59 +31,10 @@ public:
const char *getName() { return "TestPlayer"; }
void runTests(IGameDef *gamedef);
void testSave(IGameDef *gamedef);
void testLoad(IGameDef *gamedef);
};
static TestPlayer g_test_instance;
void TestPlayer::runTests(IGameDef *gamedef)
{
TEST(testSave, gamedef);
TEST(testLoad, gamedef);
}
void TestPlayer::testSave(IGameDef *gamedef)
{
RemotePlayer rplayer("testplayer_save", gamedef->idef());
PlayerSAO sao(NULL, 1, false);
sao.initialize(&rplayer, std::set<std::string>());
rplayer.setPlayerSAO(&sao);
sao.setBreath(10, false);
sao.setHPRaw(8);
sao.setYaw(0.1f);
sao.setPitch(0.6f);
sao.setBasePosition(v3f(450.2f, -15.7f, 68.1f));
rplayer.save(".", gamedef);
UASSERT(fs::PathExists("testplayer_save"));
}
void TestPlayer::testLoad(IGameDef *gamedef)
{
RemotePlayer rplayer("testplayer_load", gamedef->idef());
PlayerSAO sao(NULL, 1, false);
sao.initialize(&rplayer, std::set<std::string>());
rplayer.setPlayerSAO(&sao);
sao.setBreath(10, false);
sao.setHPRaw(8);
sao.setYaw(0.1f);
sao.setPitch(0.6f);
sao.setBasePosition(v3f(450.2f, -15.7f, 68.1f));
rplayer.save(".", gamedef);
UASSERT(fs::PathExists("testplayer_load"));
RemotePlayer rplayer_load("testplayer_load", gamedef->idef());
PlayerSAO sao_load(NULL, 2, false);
std::ifstream is("testplayer_load", std::ios_base::binary);
UASSERT(is.good());
rplayer_load.deSerialize(is, "testplayer_load", &sao_load);
is.close();
UASSERT(strcmp(rplayer_load.getName(), "testplayer_load") == 0);
UASSERT(sao_load.getBreath() == 10);
UASSERT(sao_load.getHP() == 8);
UASSERT(sao_load.getYaw() == 0.1f);
UASSERT(sao_load.getPitch() == 0.6f);
UASSERT(sao_load.getBasePosition() == v3f(450.2f, -15.7f, 68.1f));
}