Brandling Safari: LAN safari-zone creature catching on the HD-2D engine
- 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
This commit is contained in:
320
server/Database.cpp
Normal file
320
server/Database.cpp
Normal file
@@ -0,0 +1,320 @@
|
||||
#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
|
||||
Reference in New Issue
Block a user