supabase integration instead firebase
This commit is contained in:
@ -44,6 +44,7 @@ set(TETRIS_SOURCES
|
|||||||
src/core/Settings.cpp
|
src/core/Settings.cpp
|
||||||
src/graphics/renderers/RenderManager.cpp
|
src/graphics/renderers/RenderManager.cpp
|
||||||
src/persistence/Scores.cpp
|
src/persistence/Scores.cpp
|
||||||
|
src/network/supabase_client.cpp
|
||||||
src/graphics/effects/Starfield.cpp
|
src/graphics/effects/Starfield.cpp
|
||||||
src/graphics/effects/Starfield3D.cpp
|
src/graphics/effects/Starfield3D.cpp
|
||||||
src/graphics/effects/SpaceWarp.cpp
|
src/graphics/effects/SpaceWarp.cpp
|
||||||
|
|||||||
168
src/network/supabase_client.cpp
Normal file
168
src/network/supabase_client.cpp
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
#include "supabase_client.h"
|
||||||
|
#include <curl/curl.h>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
#include <thread>
|
||||||
|
#include <iostream>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
// Supabase constants (publishable anon key)
|
||||||
|
const std::string SUPABASE_URL = "https://xzxpmvyamjvtxpwnjpad.supabase.co";
|
||||||
|
const std::string SUPABASE_ANON_KEY = "sb_publishable_GqQx844xYDizO9-ytlBXfA_MVT6N7yA";
|
||||||
|
|
||||||
|
std::string buildUrl(const std::string &path) {
|
||||||
|
std::string url = SUPABASE_URL;
|
||||||
|
if (!url.empty() && url.back() == '/') url.pop_back();
|
||||||
|
url += "/rest/v1/" + path;
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t curlWriteCallback(void* contents, size_t size, size_t nmemb, void* userp) {
|
||||||
|
size_t realSize = size * nmemb;
|
||||||
|
std::string *s = reinterpret_cast<std::string*>(userp);
|
||||||
|
s->append(reinterpret_cast<char*>(contents), realSize);
|
||||||
|
return realSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CurlInit {
|
||||||
|
CurlInit() { curl_global_init(CURL_GLOBAL_DEFAULT); }
|
||||||
|
~CurlInit() { curl_global_cleanup(); }
|
||||||
|
};
|
||||||
|
static CurlInit g_curl_init;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace supabase {
|
||||||
|
|
||||||
|
void SubmitHighscoreAsync(const ScoreEntry &entry) {
|
||||||
|
std::thread([entry]() {
|
||||||
|
try {
|
||||||
|
CURL* curl = curl_easy_init();
|
||||||
|
if (!curl) return;
|
||||||
|
|
||||||
|
std::string url = buildUrl("highscores");
|
||||||
|
|
||||||
|
json j;
|
||||||
|
j["score"] = entry.score;
|
||||||
|
j["lines"] = entry.lines;
|
||||||
|
j["level"] = entry.level;
|
||||||
|
j["time_sec"] = static_cast<int>(std::lround(entry.timeSec));
|
||||||
|
j["name"] = entry.name;
|
||||||
|
j["game_type"] = entry.gameType;
|
||||||
|
j["timestamp"] = static_cast<int>(std::time(nullptr));
|
||||||
|
|
||||||
|
std::string body = j.dump();
|
||||||
|
struct curl_slist *headers = nullptr;
|
||||||
|
std::string h1 = std::string("apikey: ") + SUPABASE_ANON_KEY;
|
||||||
|
std::string h2 = std::string("Authorization: Bearer ") + SUPABASE_ANON_KEY;
|
||||||
|
headers = curl_slist_append(headers, h1.c_str());
|
||||||
|
headers = curl_slist_append(headers, h2.c_str());
|
||||||
|
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||||
|
|
||||||
|
std::string resp;
|
||||||
|
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||||
|
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.c_str());
|
||||||
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curlWriteCallback);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resp);
|
||||||
|
|
||||||
|
// Debug: print outgoing request
|
||||||
|
std::cerr << "[Supabase] POST " << url << "\n";
|
||||||
|
std::cerr << "[Supabase] Body: " << body << "\n";
|
||||||
|
|
||||||
|
CURLcode res = curl_easy_perform(curl);
|
||||||
|
if (res != CURLE_OK) {
|
||||||
|
std::cerr << "[Supabase] POST error: " << curl_easy_strerror(res) << "\n";
|
||||||
|
} else {
|
||||||
|
long http_code = 0;
|
||||||
|
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
|
||||||
|
std::cerr << "[Supabase] POST response code: " << http_code << " body_len=" << resp.size() << "\n";
|
||||||
|
if (!resp.empty()) std::cerr << "[Supabase] POST response: " << resp << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_slist_free_all(headers);
|
||||||
|
curl_easy_cleanup(curl);
|
||||||
|
} catch (...) {
|
||||||
|
// swallow errors
|
||||||
|
}
|
||||||
|
}).detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ScoreEntry> FetchHighscores(const std::string &gameType, int limit) {
|
||||||
|
std::vector<ScoreEntry> out;
|
||||||
|
try {
|
||||||
|
CURL* curl = curl_easy_init();
|
||||||
|
if (!curl) return out;
|
||||||
|
|
||||||
|
std::string path = "highscores";
|
||||||
|
std::string query;
|
||||||
|
if (!gameType.empty()) {
|
||||||
|
if (gameType == "challenge") {
|
||||||
|
query = "?game_type=eq." + gameType + "&order=level.desc,time_sec.asc&limit=" + std::to_string(limit);
|
||||||
|
} else {
|
||||||
|
query = "?game_type=eq." + gameType + "&order=score.desc&limit=" + std::to_string(limit);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
query = "?order=score.desc&limit=" + std::to_string(limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string url = buildUrl(path) + query;
|
||||||
|
|
||||||
|
struct curl_slist *headers = nullptr;
|
||||||
|
headers = curl_slist_append(headers, ("apikey: " + SUPABASE_ANON_KEY).c_str());
|
||||||
|
headers = curl_slist_append(headers, ("Authorization: Bearer " + SUPABASE_ANON_KEY).c_str());
|
||||||
|
headers = curl_slist_append(headers, "Content-Type: application/json");
|
||||||
|
|
||||||
|
|
||||||
|
std::string resp;
|
||||||
|
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
|
||||||
|
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5L);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curlWriteCallback);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &resp);
|
||||||
|
|
||||||
|
// Debug: print outgoing GET
|
||||||
|
std::cerr << "[Supabase] GET " << url << "\n";
|
||||||
|
|
||||||
|
CURLcode res = curl_easy_perform(curl);
|
||||||
|
if (res == CURLE_OK) {
|
||||||
|
long http_code = 0;
|
||||||
|
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
|
||||||
|
std::cerr << "[Supabase] GET response code: " << http_code << " body_len=" << resp.size() << "\n";
|
||||||
|
if (!resp.empty()) std::cerr << "[Supabase] GET response: " << resp << "\n";
|
||||||
|
try {
|
||||||
|
auto j = json::parse(resp);
|
||||||
|
if (j.is_array()) {
|
||||||
|
for (auto &v : j) {
|
||||||
|
ScoreEntry e{};
|
||||||
|
if (v.contains("score")) e.score = v["score"].get<int>();
|
||||||
|
if (v.contains("lines")) e.lines = v["lines"].get<int>();
|
||||||
|
if (v.contains("level")) e.level = v["level"].get<int>();
|
||||||
|
if (v.contains("time_sec")) {
|
||||||
|
try { e.timeSec = v["time_sec"].get<double>(); } catch(...) { e.timeSec = v["time_sec"].get<int>(); }
|
||||||
|
} else if (v.contains("timestamp")) {
|
||||||
|
e.timeSec = v["timestamp"].get<int>();
|
||||||
|
}
|
||||||
|
if (v.contains("name")) e.name = v["name"].get<std::string>();
|
||||||
|
if (v.contains("game_type")) e.gameType = v["game_type"].get<std::string>();
|
||||||
|
out.push_back(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (...) {
|
||||||
|
std::cerr << "[Supabase] GET parse error" << std::endl;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
std::cerr << "[Supabase] GET error: " << curl_easy_strerror(res) << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
curl_slist_free_all(headers);
|
||||||
|
curl_easy_cleanup(curl);
|
||||||
|
} catch (...) {
|
||||||
|
// swallow
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace supabase
|
||||||
14
src/network/supabase_client.h
Normal file
14
src/network/supabase_client.h
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include "../persistence/Scores.h"
|
||||||
|
|
||||||
|
namespace supabase {
|
||||||
|
|
||||||
|
// Submit a highscore asynchronously (detached thread)
|
||||||
|
void SubmitHighscoreAsync(const ScoreEntry &entry);
|
||||||
|
|
||||||
|
// Fetch highscores for a game type. If gameType is empty, fetch all (limited).
|
||||||
|
std::vector<ScoreEntry> FetchHighscores(const std::string &gameType, int limit);
|
||||||
|
|
||||||
|
} // namespace supabase
|
||||||
@ -1,20 +1,18 @@
|
|||||||
// Scores.cpp - Implementation of ScoreManager with Firebase Sync
|
// Scores.cpp - Implementation of ScoreManager
|
||||||
#include "Scores.h"
|
#include "Scores.h"
|
||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cpr/cpr.h>
|
#include "../network/supabase_client.h"
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
||||||
// Firebase Realtime Database URL
|
|
||||||
const std::string FIREBASE_URL = "https://tetris-90139.firebaseio.com/scores.json";
|
|
||||||
|
|
||||||
ScoreManager::ScoreManager(size_t maxScores) : maxEntries(maxScores) {}
|
ScoreManager::ScoreManager(size_t maxScores) : maxEntries(maxScores) {}
|
||||||
|
|
||||||
std::string ScoreManager::filePath() const {
|
std::string ScoreManager::filePath() const {
|
||||||
@ -27,50 +25,18 @@ std::string ScoreManager::filePath() const {
|
|||||||
void ScoreManager::load() {
|
void ScoreManager::load() {
|
||||||
scores.clear();
|
scores.clear();
|
||||||
|
|
||||||
// Try to load from Firebase first
|
// Try to load from Supabase first
|
||||||
try {
|
try {
|
||||||
cpr::Response r = cpr::Get(cpr::Url{FIREBASE_URL}, cpr::Timeout{2000}); // 2s timeout
|
auto fetched = supabase::FetchHighscores("", static_cast<int>(maxEntries));
|
||||||
if (r.status_code == 200 && !r.text.empty() && r.text != "null") {
|
if (!fetched.empty()) {
|
||||||
auto j = json::parse(r.text);
|
scores = fetched;
|
||||||
|
|
||||||
// Firebase returns a map of auto-generated IDs to objects
|
|
||||||
if (j.is_object()) {
|
|
||||||
for (auto& [key, value] : j.items()) {
|
|
||||||
ScoreEntry e;
|
|
||||||
if (value.contains("score")) e.score = value["score"];
|
|
||||||
if (value.contains("lines")) e.lines = value["lines"];
|
|
||||||
if (value.contains("level")) e.level = value["level"];
|
|
||||||
if (value.contains("timeSec")) e.timeSec = value["timeSec"];
|
|
||||||
if (value.contains("name")) e.name = value["name"];
|
|
||||||
if (value.contains("game_type")) e.gameType = value["game_type"].get<std::string>();
|
|
||||||
scores.push_back(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Or it might be an array if keys are integers (unlikely for Firebase push)
|
|
||||||
else if (j.is_array()) {
|
|
||||||
for (auto& value : j) {
|
|
||||||
ScoreEntry e;
|
|
||||||
if (value.contains("score")) e.score = value["score"];
|
|
||||||
if (value.contains("lines")) e.lines = value["lines"];
|
|
||||||
if (value.contains("level")) e.level = value["level"];
|
|
||||||
if (value.contains("timeSec")) e.timeSec = value["timeSec"];
|
|
||||||
if (value.contains("name")) e.name = value["name"];
|
|
||||||
if (value.contains("game_type")) e.gameType = value["game_type"].get<std::string>();
|
|
||||||
scores.push_back(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort and keep top scores
|
|
||||||
std::sort(scores.begin(), scores.end(), [](auto&a,auto&b){return a.score>b.score;});
|
std::sort(scores.begin(), scores.end(), [](auto&a,auto&b){return a.score>b.score;});
|
||||||
if (scores.size() > maxEntries) scores.resize(maxEntries);
|
if (scores.size() > maxEntries) scores.resize(maxEntries);
|
||||||
|
|
||||||
// Save to local cache
|
|
||||||
save();
|
save();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
// Ignore network errors and fall back to local file
|
std::cerr << "Failed to load from Supabase, falling back to local file." << std::endl;
|
||||||
std::cerr << "Failed to load from Firebase, falling back to local file." << std::endl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to local file
|
// Fallback to local file
|
||||||
@ -119,37 +85,19 @@ void ScoreManager::save() const {
|
|||||||
|
|
||||||
void ScoreManager::submit(int score, int lines, int level, double timeSec, const std::string& name, const std::string& gameType) {
|
void ScoreManager::submit(int score, int lines, int level, double timeSec, const std::string& name, const std::string& gameType) {
|
||||||
// Add to local list
|
// Add to local list
|
||||||
scores.push_back(ScoreEntry{score,lines,level,timeSec, name});
|
ScoreEntry newEntry{};
|
||||||
|
newEntry.score = score;
|
||||||
|
newEntry.lines = lines;
|
||||||
|
newEntry.level = level;
|
||||||
|
newEntry.timeSec = timeSec;
|
||||||
|
newEntry.name = name;
|
||||||
|
scores.push_back(newEntry);
|
||||||
std::sort(scores.begin(), scores.end(), [](auto&a,auto&b){return a.score>b.score;});
|
std::sort(scores.begin(), scores.end(), [](auto&a,auto&b){return a.score>b.score;});
|
||||||
if (scores.size()>maxEntries) scores.resize(maxEntries);
|
if (scores.size()>maxEntries) scores.resize(maxEntries);
|
||||||
save();
|
save();
|
||||||
|
// Submit to Supabase asynchronously
|
||||||
// Submit to Firebase
|
ScoreEntry se{score, lines, level, timeSec, name, gameType};
|
||||||
// Run in a detached thread to avoid blocking the UI?
|
supabase::SubmitHighscoreAsync(se);
|
||||||
// For simplicity, we'll do it blocking for now, or rely on short timeout.
|
|
||||||
// Ideally this should be async.
|
|
||||||
|
|
||||||
json j;
|
|
||||||
j["score"] = score;
|
|
||||||
j["lines"] = lines;
|
|
||||||
j["level"] = level;
|
|
||||||
j["timeSec"] = timeSec;
|
|
||||||
j["name"] = name;
|
|
||||||
j["timestamp"] = std::time(nullptr); // Add timestamp
|
|
||||||
j["game_type"] = gameType;
|
|
||||||
|
|
||||||
// Fire and forget (async) would be better, but for now let's just try to send
|
|
||||||
// We can use std::thread to make it async
|
|
||||||
std::thread([j]() {
|
|
||||||
try {
|
|
||||||
cpr::Post(cpr::Url{FIREBASE_URL},
|
|
||||||
cpr::Body{j.dump()},
|
|
||||||
cpr::Header{{"Content-Type", "application/json"}},
|
|
||||||
cpr::Timeout{5000});
|
|
||||||
} catch (...) {
|
|
||||||
// Ignore errors
|
|
||||||
}
|
|
||||||
}).detach();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ScoreManager::isHighScore(int score) const {
|
bool ScoreManager::isHighScore(int score) const {
|
||||||
@ -159,17 +107,17 @@ bool ScoreManager::isHighScore(int score) const {
|
|||||||
|
|
||||||
void ScoreManager::createSampleScores() {
|
void ScoreManager::createSampleScores() {
|
||||||
scores = {
|
scores = {
|
||||||
{159840, 189, 14, 972, "GREGOR"},
|
{159840, 189, 14, 972.0, "GREGOR"},
|
||||||
{156340, 132, 12, 714, "GREGOR"},
|
{156340, 132, 12, 714.0, "GREGOR"},
|
||||||
{155219, 125, 12, 696, "GREGOR"},
|
{155219, 125, 12, 696.0, "GREGOR"},
|
||||||
{141823, 123, 10, 710, "GREGOR"},
|
{141823, 123, 10, 710.0, "GREGOR"},
|
||||||
{140079, 71, 11, 410, "GREGOR"},
|
{140079, 71, 11, 410.0, "GREGOR"},
|
||||||
{116012, 121, 10, 619, "GREGOR"},
|
{116012, 121, 10, 619.0, "GREGOR"},
|
||||||
{112643, 137, 13, 689, "GREGOR"},
|
{112643, 137, 13, 689.0, "GREGOR"},
|
||||||
{99190, 61, 10, 378, "GREGOR"},
|
{99190, 61, 10, 378.0, "GREGOR"},
|
||||||
{93648, 107, 10, 629, "GREGOR"},
|
{93648, 107, 10, 629.0, "GREGOR"},
|
||||||
{89041, 115, 10, 618, "GREGOR"},
|
{89041, 115, 10, 618.0, "GREGOR"},
|
||||||
{88600, 55, 9, 354, "GREGOR"},
|
{88600, 55, 9, 354.0, "GREGOR"},
|
||||||
{86346, 141, 13, 723, "GREGOR"}
|
{86346, 141, 13, 723.0, "GREGOR"}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ public:
|
|||||||
explicit ScoreManager(size_t maxScores = 12);
|
explicit ScoreManager(size_t maxScores = 12);
|
||||||
void load();
|
void load();
|
||||||
void save() const;
|
void save() const;
|
||||||
// New optional `gameType` parameter will be sent to Firebase as `game_type`.
|
// New optional `gameType` parameter will be sent as `game_type`.
|
||||||
// Allowed values: "classic", "versus", "cooperate", "challenge".
|
// Allowed values: "classic", "versus", "cooperate", "challenge".
|
||||||
void submit(int score, int lines, int level, double timeSec, const std::string& name = "PLAYER", const std::string& gameType = "classic");
|
void submit(int score, int lines, int level, double timeSec, const std::string& name = "PLAYER", const std::string& gameType = "classic");
|
||||||
bool isHighScore(int score) const;
|
bool isHighScore(int score) const;
|
||||||
|
|||||||
@ -813,11 +813,16 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
|
|||||||
}
|
}
|
||||||
static const std::vector<ScoreEntry> EMPTY_SCORES;
|
static const std::vector<ScoreEntry> EMPTY_SCORES;
|
||||||
const auto& hs = ctx.scores ? ctx.scores->all() : EMPTY_SCORES;
|
const auto& hs = ctx.scores ? ctx.scores->all() : EMPTY_SCORES;
|
||||||
// Filter highscores to show only classic gameplay entries on the main menu
|
// Choose which game_type to show based on current menu selection
|
||||||
|
std::string wantedType = "classic";
|
||||||
|
if (selectedButton == 0) wantedType = "classic"; // Play / Endless
|
||||||
|
else if (selectedButton == 1) wantedType = "cooperate"; // Coop
|
||||||
|
else if (selectedButton == 2) wantedType = "challenge"; // Challenge
|
||||||
|
// Filter highscores to the desired game type
|
||||||
std::vector<ScoreEntry> filtered;
|
std::vector<ScoreEntry> filtered;
|
||||||
filtered.reserve(hs.size());
|
filtered.reserve(hs.size());
|
||||||
for (const auto &e : hs) {
|
for (const auto &e : hs) {
|
||||||
if (e.gameType == "classic") filtered.push_back(e);
|
if (e.gameType == wantedType) filtered.push_back(e);
|
||||||
}
|
}
|
||||||
size_t maxDisplay = std::min(filtered.size(), size_t(10)); // display only top 10
|
size_t maxDisplay = std::min(filtered.size(), size_t(10)); // display only top 10
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user