#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 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("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().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() .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() .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 ParseDict(string json) { var result = new Dictionary(); 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(); foreach (var it in e.EnumerateArray()) list.Add(ToPlain(it)); return list; case JsonValueKind.Object: var dict = new Dictionary(); foreach (var p in e.EnumerateObject()) dict[p.Name] = ToPlain(p.Value); return dict; default: return e.GetRawText(); } } } #endif