288 lines
11 KiB
C#
288 lines
11 KiB
C#
![]() |
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 won’t 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 3–24 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 3–24 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;
|
|||
|
}
|
|||
|
|
|||
|
}
|