#include "server/database.hh" namespace server { namespace database { // because of all the things not to work, bit cast doesn't template static T bit_cast(const U& src) noexcept { static_assert(sizeof(T) == sizeof(U)); T dest; std::memcpy(&dest, &src, sizeof(U)); return dest; } sqlite3* get_database() noexcept { static sqlite3* const database = []() -> sqlite3* { std::filesystem::create_directory(server::state.directory); const std::string path = server::state.directory + "world.dat"; if (!std::filesystem::exists(path)) { shared::print::warn << shared::print::time << "server: regenerating non-existent world data\n"; } sqlite3* database = nullptr; if (const int status = sqlite3_open(path.c_str(), &database); status != SQLITE_OK) { throw std::runtime_error("failed to open chunk database at " + path); } return database; }(); return database; } static void prepare(sqlite3_stmt*& statement, const std::string& sql) noexcept { sqlite3* const db = get_database(); if (const int status = sqlite3_prepare_v2( db, sql.c_str(), static_cast(std::size(sql) + 1), &statement, nullptr); status != SQLITE_OK) { throw std::runtime_error(std::string{"sqlite prepare error \""} + sqlite3_errmsg(db) + '\"'); } } static auto step(sqlite3_stmt*& statement) noexcept { const int status = sqlite3_step(statement); if (status == SQLITE_ERROR) { sqlite3* const db = get_database(); throw std::runtime_error(std::string{"sqlite step error \""} + sqlite3_errmsg(db) + '\"'); } return status; } static void finalise(sqlite3_stmt*& statement) noexcept { if (const int status = sqlite3_finalize(statement); status != SQLITE_OK) { throw std::runtime_error(std::string{"sqlite bind blob error \""} + sqlite3_errstr(status) + '\"'); } } static void bind_int64(sqlite3_stmt*& statement, const int index, const std::uint64_t& key) noexcept { if (const int status = sqlite3_bind_int64(statement, index, bit_cast(key)); status != SQLITE_OK) { throw std::runtime_error(std::string{"sqlite bind int error \""} + sqlite3_errstr(status) + '\"'); } } static void bind_text(sqlite3_stmt*& statement, const int index, const std::string& text) noexcept { if (const int status = sqlite3_bind_text(statement, index, std::data(text), static_cast(std::size(text)), SQLITE_STATIC); status != SQLITE_OK) { throw std::runtime_error(std::string{"sqlite bind text error \""} + sqlite3_errstr(status) + '\"'); } } template static void bind_blob(sqlite3_stmt*& statement, const int index, const T& blob) noexcept { if (const int status = sqlite3_bind_blob(statement, index, std::data(blob), static_cast(std::size(blob)), SQLITE_STATIC); status != SQLITE_OK) { throw std::runtime_error(std::string{"sqlite bind blob error \""} + sqlite3_errstr(status) + '\"'); } } static void exec_void_stmt(const std::string& sql) noexcept { sqlite3_stmt* statement = nullptr; prepare(statement, sql); step(statement); finalise(statement); } void init() noexcept { exec_void_stmt("CREATE TABLE IF NOT EXISTS Chunks (" " coords BIGINT PRIMARY KEY," " blocks BLOB NOT NULL" ");"); exec_void_stmt("CREATE TABLE IF NOT EXISTS Players (" " username TEXT PRIMARY KEY," " password TEXT NOT NULL," " player BLOB NOT NULL" ");"); } void quit() noexcept { sqlite3* database = get_database(); if (const int status = sqlite3_close(database); status != SQLITE_OK) { throw std::runtime_error(std::string{"failed to close chunk database"} + sqlite3_errmsg(database)); }; } // We have to store our key in network byte order! static std::uint64_t make_key(const shared::math::coords& coords) noexcept { const auto to_64 = [](const std::int32_t& val, const int lshift) { return static_cast(htonl(bit_cast(val))) << lshift; }; return to_64(coords.x, 32) + to_64(coords.z, 0); } std::optional maybe_read_chunk(const shared::math::coords& pos) noexcept { sqlite3_stmt* statement = nullptr; prepare(statement, "SELECT blocks FROM Chunks WHERE Chunks.coords = ?;"); bind_int64(statement, 1, make_key(pos)); if (step(statement) != SQLITE_ROW) { finalise(statement); return std::nullopt; } const char* addr = static_cast(sqlite3_column_blob(statement, 0)); std::string blocks{addr, addr + sqlite3_column_bytes(statement, 0)}; finalise(statement); std::optional chunk = [&]() -> std::optional { const auto decompress = shared::maybe_decompress_string(blocks); if (!decompress.has_value()) { return std::nullopt; } proto::chunk ret; ret.ParseFromString(*decompress); return ret; }(); if (!chunk.has_value()) { shared::print::fault << shared::print::time << "server: chunk [" << pos.x << ", " << pos.z << "] failed to parse and was evicted\n"; statement = nullptr; prepare(statement, "DELETE FROM Chunks WHERE Chunks.coords = ?;"); bind_int64(statement, 1, make_key(pos)); step(statement); finalise(statement); return std::nullopt; } return *chunk; } void write_chunk(const shared::math::coords& pos, const proto::chunk& chunk) noexcept { std::string blocks; chunk.SerializeToString(&blocks); blocks = shared::compress_string(blocks); sqlite3_stmt* statement = nullptr; prepare(statement, "INSERT OR REPLACE INTO Chunks VALUES(?, ?);"); bind_int64(statement, 1, make_key(pos)); bind_blob(statement, 2, blocks); step(statement); finalise(statement); } std::optional> maybe_read_player(const std::string& username) noexcept { sqlite3_stmt* statement = nullptr; prepare(statement, "SELECT player, password " "FROM Players " "WHERE Players.username = ?"); bind_text(statement, 1, username); if (step(statement) != SQLITE_ROW) { finalise(statement); return std::nullopt; } const char* plr_adr = static_cast(sqlite3_column_blob(statement, 0)); std::string player_bytes{plr_adr, plr_adr + sqlite3_column_bytes(statement, 0)}; const unsigned char* pass_adr = sqlite3_column_text(statement, 1); std::string pass_bytes{pass_adr, pass_adr + sqlite3_column_bytes(statement, 1)}; finalise(statement); const std::optional player = [&]() -> std::optional { const auto decompress = shared::maybe_decompress_string(player_bytes); if (!decompress.has_value()) { return std::nullopt; } proto::player ret; ret.ParseFromString(*decompress); return ret; }(); if (!player.has_value()) { shared::print::fault << shared::print::time << "server: player \"" << username << "\" failed to parse and was evicted\n"; statement = nullptr; prepare(statement, "DELETE FROM Players WHERE Players.username = ?;"); bind_text(statement, 1, username); step(statement); finalise(statement); return std::nullopt; } return std::make_pair(*player, pass_bytes); } void write_player(const std::string& username, const std::string& password, const proto::player& player) noexcept { std::string player_bytes; player.SerializeToString(&player_bytes); player_bytes = shared::compress_string(player_bytes); sqlite3_stmt* statement = nullptr; prepare(statement, "INSERT OR REPLACE INTO Players VALUES(?, ?, ?);"); bind_text(statement, 1, username); bind_text(statement, 2, password); bind_blob(statement, 3, player_bytes); step(statement); finalise(statement); } } // namespace database } // namespace server