From 1cc08c51eb4b0f95c30c0a98ad1fc5ad3459b2df Mon Sep 17 00:00:00 2001 From: Nicolas James Date: Wed, 12 Feb 2025 18:05:18 +1100 Subject: initial commit --- src/server/database.cc | 238 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 238 insertions(+) create mode 100644 src/server/database.cc (limited to 'src/server/database.cc') diff --git a/src/server/database.cc b/src/server/database.cc new file mode 100644 index 0000000..077c86c --- /dev/null +++ b/src/server/database.cc @@ -0,0 +1,238 @@ +#include "server/database.hh" + +namespace server { +namespace database { + +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( + "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, + std::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(std::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); + + shared::decompress_string(blocks); + proto::chunk chunk; + if (!chunk.ParseFromString(blocks)) { + + shared::print::fault("server: chunk [" + std::to_string(pos.x) + ", " + + std::to_string(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); + 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); + + shared::decompress_string(player_bytes); + proto::player player; + if (!player.ParseFromString(player_bytes)) { + shared::print::fault("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); + 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 + -- cgit v1.2.3