aboutsummaryrefslogtreecommitdiff
path: root/src/server/database.cc
diff options
context:
space:
mode:
authorNicolas James <Eele1Ephe7uZahRie@tutanota.com>2025-02-12 18:05:18 +1100
committerNicolas James <Eele1Ephe7uZahRie@tutanota.com>2025-02-12 18:05:18 +1100
commit1cc08c51eb4b0f95c30c0a98ad1fc5ad3459b2df (patch)
tree222dfcd07a1e40716127a347bbfd7119ce3d0984 /src/server/database.cc
initial commit
Diffstat (limited to 'src/server/database.cc')
-rw-r--r--src/server/database.cc238
1 files changed, 238 insertions, 0 deletions
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<int>(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<std::int64_t>(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<int>(std::size(text)), SQLITE_STATIC);
+ status != SQLITE_OK) {
+ throw std::runtime_error(std::string{"sqlite bind text error \""} +
+ sqlite3_errstr(status) + '\"');
+ }
+}
+
+template <typename T>
+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<int>(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<std::uint64_t>(
+ htonl(std::bit_cast<std::uint32_t>(val)))
+ << lshift;
+ };
+
+ return to_64(coords.x, 32) + to_64(coords.z, 0);
+}
+
+std::optional<proto::chunk>
+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<const char*>(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<std::pair<proto::player, std::string>>
+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<const char*>(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
+