- Authoritative 30 Hz server (ENet, SQLite): accounts, characters, wild AI, tall grass / water / wandering spawns, fishing, captures, follower chains, clock + weather, LAN beacon - Client: title / join / character creator / play screens, prediction + reconciliation, interpolated players and parties, HD-2D UI (dialogue with Professor Henry, HUD, collection) - Shared deterministic zone generator, movement, protocol, roster, appearance - Paper-doll character art pipeline (PixelLab base walk + per-item animation edits) - Brandling roster from the company list (logo colors, jev types), Blender safari props - Headless test bot
321 lines
10 KiB
C++
321 lines
10 KiB
C++
#include "Database.h"
|
|
|
|
#include <nlohmann/json.hpp>
|
|
#include <sqlite3.h>
|
|
|
|
#include <cstdio>
|
|
#include <filesystem>
|
|
|
|
namespace bs {
|
|
|
|
namespace {
|
|
|
|
const char* kSchema = R"SQL(
|
|
CREATE TABLE IF NOT EXISTS players(
|
|
id INTEGER PRIMARY KEY,
|
|
token TEXT UNIQUE NOT NULL,
|
|
name TEXT NOT NULL,
|
|
appearance TEXT NOT NULL,
|
|
pos_x REAL NOT NULL,
|
|
pos_z REAL NOT NULL,
|
|
yaw REAL NOT NULL DEFAULT 0,
|
|
capsules INTEGER NOT NULL DEFAULT 0,
|
|
intro_done INTEGER NOT NULL DEFAULT 0,
|
|
created_at INTEGER NOT NULL,
|
|
last_seen INTEGER NOT NULL);
|
|
CREATE TABLE IF NOT EXISTS creatures(
|
|
uid INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
owner INTEGER NOT NULL REFERENCES players(id),
|
|
species INTEGER NOT NULL,
|
|
caught_at INTEGER NOT NULL,
|
|
in_party INTEGER NOT NULL DEFAULT 0);
|
|
CREATE INDEX IF NOT EXISTS creatures_owner ON creatures(owner);
|
|
CREATE TABLE IF NOT EXISTS dex(
|
|
owner INTEGER NOT NULL REFERENCES players(id),
|
|
species INTEGER NOT NULL,
|
|
seen INTEGER NOT NULL DEFAULT 0,
|
|
caught INTEGER NOT NULL DEFAULT 0,
|
|
PRIMARY KEY(owner, species));
|
|
CREATE TABLE IF NOT EXISTS world(
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL);
|
|
)SQL";
|
|
|
|
std::string text(sqlite3_stmt* s, int col) {
|
|
const unsigned char* t = sqlite3_column_text(s, col);
|
|
return t ? reinterpret_cast<const char*>(t) : "";
|
|
}
|
|
|
|
void bindText(sqlite3_stmt* s, int i, const std::string& v) {
|
|
sqlite3_bind_text(s, i, v.data(), int(v.size()), SQLITE_TRANSIENT);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
std::string appearanceToJson(const Appearance& a) {
|
|
nlohmann::json j;
|
|
j["body"] = a.body;
|
|
j["skin"] = a.skin;
|
|
nlohmann::json items = nlohmann::json::object(), colors = nlohmann::json::object();
|
|
for (int s = 0; s < kSlotCount; ++s) {
|
|
items[slotName(s)] = a.items[s];
|
|
colors[slotName(s)] = hexColor(a.colors[s]);
|
|
}
|
|
j["items"] = items;
|
|
j["colors"] = colors;
|
|
return j.dump();
|
|
}
|
|
|
|
Appearance appearanceFromJson(const std::string& s) {
|
|
Appearance a;
|
|
try {
|
|
nlohmann::json j = nlohmann::json::parse(s);
|
|
a.body = j.value("body", a.body);
|
|
a.skin = uint8_t(j.value("skin", 0));
|
|
for (int i = 0; i < kSlotCount; ++i) {
|
|
if (j.contains("items")) a.items[i] = j["items"].value(slotName(i), "");
|
|
if (j.contains("colors")) a.colors[i] = parseHexColor(j["colors"].value(slotName(i), "#ffffff"));
|
|
}
|
|
} catch (const std::exception&) {
|
|
}
|
|
return a;
|
|
}
|
|
|
|
Database::~Database() { close(); }
|
|
|
|
bool Database::open(const std::string& path, std::string* error) {
|
|
std::error_code ec;
|
|
std::filesystem::path p(path);
|
|
if (p.has_parent_path()) std::filesystem::create_directories(p.parent_path(), ec);
|
|
if (sqlite3_open(path.c_str(), &db_) != SQLITE_OK) {
|
|
if (error) *error = db_ ? sqlite3_errmsg(db_) : "sqlite3_open failed";
|
|
close();
|
|
return false;
|
|
}
|
|
sqlite3_busy_timeout(db_, 2000);
|
|
exec("PRAGMA journal_mode=WAL;");
|
|
exec("PRAGMA synchronous=NORMAL;");
|
|
exec("PRAGMA foreign_keys=ON;");
|
|
if (!exec(kSchema)) {
|
|
if (error) *error = sqlite3_errmsg(db_);
|
|
close();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void Database::close() {
|
|
for (auto& [sql, s] : stmts_) sqlite3_finalize(s);
|
|
stmts_.clear();
|
|
if (db_) sqlite3_close(db_);
|
|
db_ = nullptr;
|
|
}
|
|
|
|
bool Database::exec(const char* sql) {
|
|
char* err = nullptr;
|
|
if (sqlite3_exec(db_, sql, nullptr, nullptr, &err) != SQLITE_OK) {
|
|
std::fprintf(stderr, "[db] %s\n", err ? err : "error");
|
|
sqlite3_free(err);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
sqlite3_stmt* Database::prepare(const char* sql) {
|
|
if (!db_) return nullptr;
|
|
auto it = stmts_.find(sql);
|
|
if (it != stmts_.end()) {
|
|
sqlite3_reset(it->second);
|
|
sqlite3_clear_bindings(it->second);
|
|
return it->second;
|
|
}
|
|
sqlite3_stmt* s = nullptr;
|
|
if (sqlite3_prepare_v2(db_, sql, -1, &s, nullptr) != SQLITE_OK) {
|
|
std::fprintf(stderr, "[db] prepare failed: %s\n", sqlite3_errmsg(db_));
|
|
return nullptr;
|
|
}
|
|
stmts_[sql] = s;
|
|
return s;
|
|
}
|
|
|
|
void Database::begin() {
|
|
if (db_) exec("BEGIN;");
|
|
}
|
|
|
|
void Database::commit() {
|
|
if (db_) exec("COMMIT;");
|
|
}
|
|
|
|
bool Database::loadPlayer(const std::string& token, PlayerRow& out) {
|
|
sqlite3_stmt* s = prepare(
|
|
"SELECT id, name, appearance, pos_x, pos_z, yaw, capsules, intro_done, created_at, last_seen "
|
|
"FROM players WHERE token = ?");
|
|
if (!s) return false;
|
|
bindText(s, 1, token);
|
|
bool found = sqlite3_step(s) == SQLITE_ROW;
|
|
if (found) {
|
|
out.id = sqlite3_column_int64(s, 0);
|
|
out.token = token;
|
|
out.name = text(s, 1);
|
|
out.look = appearanceFromJson(text(s, 2));
|
|
out.x = float(sqlite3_column_double(s, 3));
|
|
out.z = float(sqlite3_column_double(s, 4));
|
|
out.yaw = float(sqlite3_column_double(s, 5));
|
|
out.capsules = sqlite3_column_int(s, 6);
|
|
out.introDone = sqlite3_column_int(s, 7) != 0;
|
|
out.createdAt = sqlite3_column_int64(s, 8);
|
|
out.lastSeen = sqlite3_column_int64(s, 9);
|
|
}
|
|
sqlite3_reset(s);
|
|
return found;
|
|
}
|
|
|
|
int64_t Database::createPlayer(const PlayerRow& r) {
|
|
sqlite3_stmt* s = prepare(
|
|
"INSERT INTO players(token, name, appearance, pos_x, pos_z, yaw, capsules, intro_done, created_at, last_seen) "
|
|
"VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
if (!s) return 0;
|
|
bindText(s, 1, r.token);
|
|
bindText(s, 2, r.name);
|
|
bindText(s, 3, appearanceToJson(r.look));
|
|
sqlite3_bind_double(s, 4, r.x);
|
|
sqlite3_bind_double(s, 5, r.z);
|
|
sqlite3_bind_double(s, 6, r.yaw);
|
|
sqlite3_bind_int(s, 7, r.capsules);
|
|
sqlite3_bind_int(s, 8, r.introDone);
|
|
sqlite3_bind_int64(s, 9, r.createdAt);
|
|
sqlite3_bind_int64(s, 10, r.lastSeen);
|
|
int64_t id = 0;
|
|
if (sqlite3_step(s) == SQLITE_DONE) id = sqlite3_last_insert_rowid(db_);
|
|
else std::fprintf(stderr, "[db] createPlayer: %s\n", sqlite3_errmsg(db_));
|
|
sqlite3_reset(s);
|
|
return id;
|
|
}
|
|
|
|
void Database::savePlayer(const PlayerRow& r) {
|
|
sqlite3_stmt* s = prepare(
|
|
"UPDATE players SET name = ?, appearance = ?, pos_x = ?, pos_z = ?, yaw = ?, capsules = ?, intro_done = ?, "
|
|
"last_seen = ? WHERE id = ?");
|
|
if (!s) return;
|
|
bindText(s, 1, r.name);
|
|
bindText(s, 2, appearanceToJson(r.look));
|
|
sqlite3_bind_double(s, 3, r.x);
|
|
sqlite3_bind_double(s, 4, r.z);
|
|
sqlite3_bind_double(s, 5, r.yaw);
|
|
sqlite3_bind_int(s, 6, r.capsules);
|
|
sqlite3_bind_int(s, 7, r.introDone);
|
|
sqlite3_bind_int64(s, 8, r.lastSeen);
|
|
sqlite3_bind_int64(s, 9, r.id);
|
|
if (sqlite3_step(s) != SQLITE_DONE) std::fprintf(stderr, "[db] savePlayer: %s\n", sqlite3_errmsg(db_));
|
|
sqlite3_reset(s);
|
|
}
|
|
|
|
void Database::savePosition(int64_t id, float x, float z, float yaw) {
|
|
sqlite3_stmt* s = prepare("UPDATE players SET pos_x = ?, pos_z = ?, yaw = ?, last_seen = strftime('%s','now') WHERE id = ?");
|
|
if (!s) return;
|
|
sqlite3_bind_double(s, 1, x);
|
|
sqlite3_bind_double(s, 2, z);
|
|
sqlite3_bind_double(s, 3, yaw);
|
|
sqlite3_bind_int64(s, 4, id);
|
|
sqlite3_step(s);
|
|
sqlite3_reset(s);
|
|
}
|
|
|
|
std::vector<OwnedCreature> Database::loadCreatures(int64_t owner) {
|
|
std::vector<OwnedCreature> out;
|
|
sqlite3_stmt* s = prepare("SELECT uid, species, in_party, caught_at FROM creatures WHERE owner = ? ORDER BY uid");
|
|
if (!s) return out;
|
|
sqlite3_bind_int64(s, 1, owner);
|
|
while (sqlite3_step(s) == SQLITE_ROW) {
|
|
OwnedCreature c;
|
|
c.uid = uint32_t(sqlite3_column_int64(s, 0));
|
|
c.species = uint16_t(sqlite3_column_int(s, 1));
|
|
c.inParty = sqlite3_column_int(s, 2) != 0;
|
|
c.caughtAt = sqlite3_column_int64(s, 3);
|
|
out.push_back(c);
|
|
}
|
|
sqlite3_reset(s);
|
|
return out;
|
|
}
|
|
|
|
uint32_t Database::insertCreature(int64_t owner, uint16_t species, int64_t caughtAt, bool inParty) {
|
|
sqlite3_stmt* s = prepare("INSERT INTO creatures(owner, species, caught_at, in_party) VALUES(?, ?, ?, ?)");
|
|
if (!s) return 0;
|
|
sqlite3_bind_int64(s, 1, owner);
|
|
sqlite3_bind_int(s, 2, species);
|
|
sqlite3_bind_int64(s, 3, caughtAt);
|
|
sqlite3_bind_int(s, 4, inParty);
|
|
uint32_t uid = 0;
|
|
if (sqlite3_step(s) == SQLITE_DONE) uid = uint32_t(sqlite3_last_insert_rowid(db_));
|
|
else std::fprintf(stderr, "[db] insertCreature: %s\n", sqlite3_errmsg(db_));
|
|
sqlite3_reset(s);
|
|
return uid;
|
|
}
|
|
|
|
void Database::setInParty(uint32_t uid, bool inParty) {
|
|
sqlite3_stmt* s = prepare("UPDATE creatures SET in_party = ? WHERE uid = ?");
|
|
if (!s) return;
|
|
sqlite3_bind_int(s, 1, inParty);
|
|
sqlite3_bind_int64(s, 2, uid);
|
|
sqlite3_step(s);
|
|
sqlite3_reset(s);
|
|
}
|
|
|
|
void Database::loadDex(int64_t owner, std::set<uint16_t>& seen, std::set<uint16_t>& caught) {
|
|
seen.clear();
|
|
caught.clear();
|
|
sqlite3_stmt* s = prepare("SELECT species, seen, caught FROM dex WHERE owner = ?");
|
|
if (!s) return;
|
|
sqlite3_bind_int64(s, 1, owner);
|
|
while (sqlite3_step(s) == SQLITE_ROW) {
|
|
uint16_t sp = uint16_t(sqlite3_column_int(s, 0));
|
|
if (sqlite3_column_int(s, 1)) seen.insert(sp);
|
|
if (sqlite3_column_int(s, 2)) caught.insert(sp);
|
|
}
|
|
sqlite3_reset(s);
|
|
}
|
|
|
|
void Database::markSeen(int64_t owner, uint16_t species) {
|
|
sqlite3_stmt* s = prepare(
|
|
"INSERT INTO dex(owner, species, seen, caught) VALUES(?, ?, 1, 0) "
|
|
"ON CONFLICT(owner, species) DO UPDATE SET seen = 1");
|
|
if (!s) return;
|
|
sqlite3_bind_int64(s, 1, owner);
|
|
sqlite3_bind_int(s, 2, species);
|
|
sqlite3_step(s);
|
|
sqlite3_reset(s);
|
|
}
|
|
|
|
void Database::markCaught(int64_t owner, uint16_t species) {
|
|
sqlite3_stmt* s = prepare(
|
|
"INSERT INTO dex(owner, species, seen, caught) VALUES(?, ?, 1, 1) "
|
|
"ON CONFLICT(owner, species) DO UPDATE SET seen = 1, caught = 1");
|
|
if (!s) return;
|
|
sqlite3_bind_int64(s, 1, owner);
|
|
sqlite3_bind_int(s, 2, species);
|
|
sqlite3_step(s);
|
|
sqlite3_reset(s);
|
|
}
|
|
|
|
bool Database::getWorld(const std::string& key, std::string& value) {
|
|
sqlite3_stmt* s = prepare("SELECT value FROM world WHERE key = ?");
|
|
if (!s) return false;
|
|
bindText(s, 1, key);
|
|
bool found = sqlite3_step(s) == SQLITE_ROW;
|
|
if (found) value = text(s, 0);
|
|
sqlite3_reset(s);
|
|
return found;
|
|
}
|
|
|
|
void Database::setWorld(const std::string& key, const std::string& value) {
|
|
sqlite3_stmt* s = prepare(
|
|
"INSERT INTO world(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value");
|
|
if (!s) return;
|
|
bindText(s, 1, key);
|
|
bindText(s, 2, value);
|
|
sqlite3_step(s);
|
|
sqlite3_reset(s);
|
|
}
|
|
|
|
} // namespace bs
|