206 lines
6.9 KiB
C#
206 lines
6.9 KiB
C#
![]() |
#if UNITY_EDITOR
|
|||
|
using System;
|
|||
|
using System.Collections.Generic;
|
|||
|
using System.Threading.Tasks;
|
|||
|
using System.Text.Json;
|
|||
|
using UnityEditor;
|
|||
|
using UnityEngine;
|
|||
|
using Supabase;
|
|||
|
using Postgrest;
|
|||
|
using Postgrest.Attributes;
|
|||
|
using Postgrest.Models;
|
|||
|
|
|||
|
#region DB Model
|
|||
|
|
|||
|
[Table("players")]
|
|||
|
public class PlayerRow : BaseModel
|
|||
|
{
|
|||
|
[PrimaryKey("wallet_address", false)]
|
|||
|
[Column("wallet_address")] public string WalletAddress { get; set; }
|
|||
|
|
|||
|
[Column("total_kills")] public int TotalKills { get; set; }
|
|||
|
[Column("average_placement")] public decimal? AveragePlacement { get; set; }
|
|||
|
[Column("win_percentage")] public decimal? WinPercentage { get; set; }
|
|||
|
[Column("in_game_currency")] public long InGameCurrency { get; set; }
|
|||
|
|
|||
|
// jsonb – we'll send/receive as a plain object map
|
|||
|
[Column("purchased_items")] public Dictionary<string, object> PurchasedItems { get; set; } = new();
|
|||
|
|
|||
|
[Column("updated_at")] public DateTimeOffset? UpdatedAt { get; set; }
|
|||
|
}
|
|||
|
|
|||
|
#endregion
|
|||
|
|
|||
|
public class PlayersDevTools : EditorWindow
|
|||
|
{
|
|||
|
// Inputs
|
|||
|
string wallet = "dev_wallet_test_1";
|
|||
|
int totalKills = 3;
|
|||
|
double avgPlacement = 2.5;
|
|||
|
double winPct = 55.25;
|
|||
|
long currency = 1000;
|
|||
|
string purchasedJson = "{\"starter_pack\":true,\"sword\":\"iron\"}";
|
|||
|
|
|||
|
Supabase.Client client;
|
|||
|
|
|||
|
[MenuItem("Tools/Supabase/Players Tester")]
|
|||
|
public static void Open() => GetWindow<PlayersDevTools>("Players Tester");
|
|||
|
|
|||
|
async void OnEnable() => await EnsureClient();
|
|||
|
|
|||
|
async Task EnsureClient()
|
|||
|
{
|
|||
|
if (client != null) return;
|
|||
|
var s = await SupabaseSecretsLoader.LoadAsync(); // editor dev json or Resources
|
|||
|
var opts = new SupabaseOptions { AutoRefreshToken = true, AutoConnectRealtime = false, Schema = "public" };
|
|||
|
client = new Supabase.Client(s.url, s.anonKey, opts);
|
|||
|
await client.InitializeAsync();
|
|||
|
}
|
|||
|
|
|||
|
void OnGUI()
|
|||
|
{
|
|||
|
EditorGUILayout.LabelField("Upsert / Read / Delete: public.players", EditorStyles.boldLabel);
|
|||
|
EditorGUILayout.Space();
|
|||
|
|
|||
|
wallet = EditorGUILayout.TextField("wallet_address", wallet);
|
|||
|
totalKills = EditorGUILayout.IntField("total_kills", totalKills);
|
|||
|
avgPlacement = EditorGUILayout.DoubleField("average_placement", avgPlacement);
|
|||
|
winPct = EditorGUILayout.DoubleField("win_percentage", winPct);
|
|||
|
currency = EditorGUILayout.LongField("in_game_currency", currency);
|
|||
|
purchasedJson = EditorGUILayout.TextField("purchased_items (json)", purchasedJson);
|
|||
|
|
|||
|
EditorGUILayout.Space();
|
|||
|
using (new EditorGUILayout.HorizontalScope())
|
|||
|
{
|
|||
|
if (GUILayout.Button("UPSERT")) _ = UpsertAsync();
|
|||
|
if (GUILayout.Button("READ")) _ = ReadAsync();
|
|||
|
if (GUILayout.Button("DELETE")) _ = DeleteAsync();
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
async Task UpsertAsync()
|
|||
|
{
|
|||
|
try
|
|||
|
{
|
|||
|
await EnsureClient();
|
|||
|
|
|||
|
var row = new PlayerRow
|
|||
|
{
|
|||
|
WalletAddress = wallet,
|
|||
|
TotalKills = totalKills,
|
|||
|
AveragePlacement = (decimal)avgPlacement,
|
|||
|
WinPercentage = (decimal)winPct,
|
|||
|
InGameCurrency = currency,
|
|||
|
PurchasedItems = ParseDict(purchasedJson),
|
|||
|
UpdatedAt = DateTimeOffset.UtcNow
|
|||
|
};
|
|||
|
|
|||
|
// postgrest-csharp 3.5.x – Upsert by PK, no onConflict param
|
|||
|
var resp = await client.From<PlayerRow>().Upsert(row);
|
|||
|
var saved = resp.Models.Count > 0 ? resp.Models[0] : row;
|
|||
|
|
|||
|
Debug.Log($"✅ UPSERT OK -> wallet={saved.WalletAddress} kills={saved.TotalKills} currency={saved.InGameCurrency}");
|
|||
|
ShowNotification(new GUIContent("UPSERT OK"));
|
|||
|
}
|
|||
|
catch (Exception e)
|
|||
|
{
|
|||
|
Debug.LogError($"UPSERT FAILED: {e.Message}");
|
|||
|
ShowNotification(new GUIContent("UPSERT FAILED"));
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
async Task ReadAsync()
|
|||
|
{
|
|||
|
try
|
|||
|
{
|
|||
|
await EnsureClient();
|
|||
|
|
|||
|
var resp = await client
|
|||
|
.From<PlayerRow>()
|
|||
|
.Select("*") // 3.5.x needs a string
|
|||
|
.Filter("wallet_address", Postgrest.Constants.Operator.Equals, wallet)
|
|||
|
.Get();
|
|||
|
|
|||
|
if (resp.Models.Count == 0)
|
|||
|
{
|
|||
|
Debug.LogWarning("No row found.");
|
|||
|
ShowNotification(new GUIContent("NOT FOUND"));
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
var p = resp.Models[0];
|
|||
|
Debug.Log($"✅ READ -> wallet={p.WalletAddress}, kills={p.TotalKills}, avg={p.AveragePlacement}, win%={p.WinPercentage}, cur={p.InGameCurrency}, items={JsonSerializer.Serialize(p.PurchasedItems)}");
|
|||
|
ShowNotification(new GUIContent("READ OK"));
|
|||
|
}
|
|||
|
catch (Exception e)
|
|||
|
{
|
|||
|
Debug.LogError($"READ FAILED: {e.Message}");
|
|||
|
ShowNotification(new GUIContent("READ FAILED"));
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
async Task DeleteAsync()
|
|||
|
{
|
|||
|
try
|
|||
|
{
|
|||
|
await EnsureClient();
|
|||
|
|
|||
|
await client
|
|||
|
.From<PlayerRow>()
|
|||
|
.Filter("wallet_address", Postgrest.Constants.Operator.Equals, wallet)
|
|||
|
.Delete();
|
|||
|
|
|||
|
Debug.Log("✅ DELETE OK");
|
|||
|
ShowNotification(new GUIContent("DELETE OK"));
|
|||
|
}
|
|||
|
catch (Exception e)
|
|||
|
{
|
|||
|
Debug.LogError($"DELETE FAILED: {e.Message}");
|
|||
|
ShowNotification(new GUIContent("DELETE FAILED"));
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// -------- JSON helpers (System.Text.Json) --------
|
|||
|
static Dictionary<string, object> ParseDict(string json)
|
|||
|
{
|
|||
|
var result = new Dictionary<string, object>();
|
|||
|
if (string.IsNullOrWhiteSpace(json)) return result;
|
|||
|
|
|||
|
try
|
|||
|
{
|
|||
|
using var doc = JsonDocument.Parse(json);
|
|||
|
if (doc.RootElement.ValueKind != JsonValueKind.Object) return result;
|
|||
|
|
|||
|
foreach (var p in doc.RootElement.EnumerateObject())
|
|||
|
result[p.Name] = ToPlain(p.Value);
|
|||
|
}
|
|||
|
catch { /* ignore -> empty dict */ }
|
|||
|
return result;
|
|||
|
}
|
|||
|
|
|||
|
static object ToPlain(JsonElement e)
|
|||
|
{
|
|||
|
switch (e.ValueKind)
|
|||
|
{
|
|||
|
case JsonValueKind.String: return e.GetString();
|
|||
|
case JsonValueKind.Number:
|
|||
|
if (e.TryGetInt64(out var l)) return l;
|
|||
|
if (e.TryGetDouble(out var d)) return d;
|
|||
|
return e.GetRawText();
|
|||
|
case JsonValueKind.True: return true;
|
|||
|
case JsonValueKind.False: return false;
|
|||
|
case JsonValueKind.Null: return null;
|
|||
|
case JsonValueKind.Array:
|
|||
|
var list = new List<object>();
|
|||
|
foreach (var it in e.EnumerateArray()) list.Add(ToPlain(it));
|
|||
|
return list;
|
|||
|
case JsonValueKind.Object:
|
|||
|
var dict = new Dictionary<string, object>();
|
|||
|
foreach (var p in e.EnumerateObject()) dict[p.Name] = ToPlain(p.Value);
|
|||
|
return dict;
|
|||
|
default: return e.GetRawText();
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
#endif
|