288 lines
11 KiB
C#
Raw Normal View History

2025-10-15 20:28:33 +04:00
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Threading.Tasks;
using Supabase;
using Postgrest;
using Game.Data;
using UnityEngine; // for PlayerPrefs
using System.Text.RegularExpressions;
public static class GameDb
{
private static Supabase.Client _client;
private const string PrefsWalletKey = "WALLET_ADDRESS";
private const string PrefsName = "DISPLAY_NAME";
public static async Task InitAsync()
{
if (_client != null) return;
var s = await SupabaseSecretsLoader.LoadAsync();
var opts = new SupabaseOptions { AutoConnectRealtime = false, AutoRefreshToken = true, Schema = "public" };
_client = new Supabase.Client(s.url, s.anonKey, opts);
await _client.InitializeAsync();
}
// ------------------------------------------------------------
// WalletConnected: check if exists, create if missing, store in PlayerPrefs
// ------------------------------------------------------------
//public static async Task<PlayerRecord> WalletConnectedAsync(string walletAddress)
//{
// if (string.IsNullOrWhiteSpace(walletAddress))
// throw new ArgumentException("walletAddress cannot be empty.");
// await InitAsync();
// // exists?
// var existing = await _client.From<PlayerRecord>()
// .Select("*")
// .Filter("wallet_address", Postgrest.Constants.Operator.Equals, walletAddress)
// .Get();
// PlayerRecord result;
// if (existing.Models.Count > 0)
// {
// result = existing.Models[0];
// }
// else
// {
// // insert minimal row (DB supplies defaults & computed columns)
// var insert = await _client.From<PlayerRecord>().Insert(new PlayerRecord { WalletAddress = walletAddress });
// result = insert.Models.Count > 0 ? insert.Models[0] : new PlayerRecord { WalletAddress = walletAddress };
// }
// // "login success": save wallet locally
// PlayerPrefs.SetString(PrefsWalletKey, walletAddress);
// PlayerPrefs.Save();
// return result;
//}
public static async Task<PlayerRecord> WalletConnectedAsync(string walletAddress)
{
if (string.IsNullOrWhiteSpace(walletAddress))
throw new ArgumentException("walletAddress cannot be empty.");
await InitAsync();
// Atomic, safe, and wont try to write generated columns.
var rows = await _client.Rpc<List<PlayerRecord>>("ensure_player", new { p_wallet = walletAddress });
var player = rows != null && rows.Count > 0 ? rows[0] : null;
if (player == null)
throw new Exception("ensure_player RPC returned no row.");
PlayerPrefs.SetString("WALLET_ADDRESS", walletAddress);
PlayerPrefs.Save();
return player;
}
// ------------------------------------------------------------
// AddKill (+1 by default) -- uses RPC for atomic increment
// ------------------------------------------------------------
public static async Task<PlayerRecord> AddKillAsync(string walletAddress, int delta = 1)
{
await InitAsync();
var payload = new { p_wallet = walletAddress, p_delta = delta };
// SQL returns SETOF players → deserialize to a list/array and take first
var rows = await _client.Rpc<List<PlayerRecord>>("add_kills", payload);
return rows != null && rows.Count > 0 ? rows[0] : null;
}
// ------------------------------------------------------------
// Call Supabase-side method to add currency (RPC)
// ------------------------------------------------------------
public static async Task<PlayerRecord> AddCurrencyAsync(string walletAddress, long delta)
{
await InitAsync();
var payload = new { p_wallet = walletAddress, p_delta = delta };
var rows = await _client.Rpc<List<PlayerRecord>>("add_currency", payload);
return rows != null && rows.Count > 0 ? rows[0] : null;
}
// ------------------------------------------------------------
// Get currency for player
// ------------------------------------------------------------
public static async Task<long> GetCurrencyAsync(string walletAddress)
{
await InitAsync();
var resp = await _client.From<PlayerRecord>()
.Select("in_game_currency")
.Filter("wallet_address", Postgrest.Constants.Operator.Equals, walletAddress)
.Get();
return resp.Models.Count > 0 ? resp.Models[0].InGameCurrency : 0L;
}
// ------------------------------------------------------------
// Get JSON for bought items
// ------------------------------------------------------------
public static async Task<string> GetPurchasedItemsJsonAsync(string walletAddress)
{
await InitAsync();
var resp = await _client.From<PlayerRecord>()
.Select("purchased_items")
.Filter("wallet_address", Postgrest.Constants.Operator.Equals, walletAddress)
.Get();
if (resp.Models.Count == 0) return "{}";
var dict = resp.Models[0].PurchasedItems ?? new Dictionary<string, object>();
return JsonSerializer.Serialize(dict);
}
// ------------------------------------------------------------
// Edit JSON for bought items (replace with provided JSON)
// ------------------------------------------------------------
public static async Task<PlayerRecord> SetPurchasedItemsJsonAsync(string walletAddress, string json)
{
await InitAsync();
var rows = await _client.Rpc<List<PlayerRecord>>(
"set_purchased_items",
new { p_wallet = walletAddress, p_items = System.Text.Json.JsonDocument.Parse(json).RootElement }
);
return rows != null && rows.Count > 0 ? rows[0] : null;
}
// ------------------------------------------------------------
// +1 games played / +1 games won (RPCs)
// ------------------------------------------------------------
public static async Task<PlayerRecord> IncGamesPlayedAsync(string walletAddress)
{
await InitAsync();
var rows = await _client.Rpc<List<PlayerRecord>>("inc_games_played", new { p_wallet = walletAddress });
return rows != null && rows.Count > 0 ? rows[0] : null;
}
public static async Task<PlayerRecord> IncGamesWonAsync(string walletAddress)
{
await InitAsync();
var rows = await _client.Rpc<List<PlayerRecord>>("inc_games_won", new { p_wallet = walletAddress });
return rows != null && rows.Count > 0 ? rows[0] : null;
}
// ------------------------------------------------------------
// Get Player data (full record)
// ------------------------------------------------------------
public static async Task<PlayerRecord> GetPlayerAsync(string walletAddress)
{
await InitAsync();
var resp = await _client.From<PlayerRecord>()
.Select("*")
.Filter("wallet_address", Postgrest.Constants.Operator.Equals, walletAddress)
.Get();
return resp.Models.Count > 0 ? resp.Models[0] : null;
}
public static async Task<PlayerRecord> SendGameWinAsync(
string walletAddress, int placement)
{
await InitAsync();
var payload = new
{
p_wallet = walletAddress,
p_placement = placement
};
var rows = await _client.Rpc<List<PlayerRecord>>("record_game_win", payload);
return rows != null && rows.Count > 0 ? rows[0] : null;
}
/// <summary>
/// Add/overwrite one entry in purchased_items: key -> value (value default true).
/// </summary>
public static async Task<PlayerRecord> AddPurchasedItemAsync(string walletAddress, string key, object value = null)
{
await InitAsync();
// Fallback: serialize -> parse -> take RootElement
JsonElement jsonb = value == null
? JsonDocument.Parse("true").RootElement
: JsonDocument.Parse(JsonSerializer.Serialize(value)).RootElement;
var payload = new { p_wallet = walletAddress, p_key = key, p_value = jsonb };
var rows = await _client.Rpc<List<PlayerRecord>>("add_purchased_item", payload);
return rows != null && rows.Count > 0 ? rows[0] : null;
}
static string SanitizeName(string name)
{
if (string.IsNullOrWhiteSpace(name)) return null;
name = name.Trim();
// allow letters, numbers, space, underscore, dash; clamp length 3..24
name = Regex.Replace(name, @"[^A-Za-z0-9 _\-]", "");
if (name.Length < 3) return null;
if (name.Length > 24) name = name.Substring(0, 24);
return name;
}
// -------- GETTER --------
public static async Task<string> GetPlayerNameAsync(string walletAddress)
{
await InitAsync();
var resp = await _client.From<PlayerRecord>()
.Select("display_name")
.Filter("wallet_address", Postgrest.Constants.Operator.Equals, walletAddress)
.Get();
var name = resp.Models.Count > 0 ? resp.Models[0].DisplayName ?? "" : "";
// cache (optional)
PlayerPrefs.SetString(PrefsName, name);
PlayerPrefs.Save();
return name;
}
// -------- CHECK EMPTY (DB) --------
public static async Task<bool> IsNameEmptyAsync(string walletAddress)
{
var name = await GetPlayerNameAsync(walletAddress);
return string.IsNullOrEmpty(name);
}
// -------- SETTER: only if empty (safe path) --------
// Calls your SQL function: set_name_if_empty(p_wallet, p_name)
public static async Task<PlayerRecord> SetNameIfEmptyAsync(string walletAddress, string proposedName)
{
await InitAsync();
var clean = SanitizeName(proposedName);
if (string.IsNullOrEmpty(clean))
throw new ArgumentException("Invalid name. Use 324 chars: letters, numbers, space, _ or -.");
// RPC returns the player row (whether it updated or not)
var rows = await _client.Rpc<List<PlayerRecord>>(
"set_name_if_empty",
new { p_wallet = walletAddress, p_name = clean }
);
var player = rows != null && rows.Count > 0 ? rows[0] : null;
if (player == null) throw new Exception("set_name_if_empty returned no row.");
PlayerPrefs.SetString(PrefsName, player.DisplayName ?? "");
PlayerPrefs.Save();
return player;
}
// -------- SETTER: force update (optional, for rename UI) --------
public static async Task<PlayerRecord> SetNameAsync(string walletAddress, string newName)
{
await InitAsync();
var clean = SanitizeName(newName);
if (string.IsNullOrEmpty(clean))
throw new ArgumentException("Invalid name. Use 324 chars: letters, numbers, space, _ or -.");
// RPC returns the row; no SDK .Set() nonsense, no risk of overwriting other fields
var rows = await _client.Rpc<List<PlayerRecord>>(
"set_name",
new { p_wallet = walletAddress, p_name = clean }
);
var rec = rows != null && rows.Count > 0 ? rows[0] : null;
if (rec == null) throw new Exception("Name update failed.");
PlayerPrefs.SetString("DISPLAY_NAME", rec.DisplayName ?? "");
PlayerPrefs.Save();
return rec;
}
}