SupaBase entry done

dev-ali-supabase
Ali Sharoz 2 weeks ago
parent 8db95e6d40
commit ec8726cf94

@ -22176,7 +22176,7 @@ GameObject:
m_Icon: {fileID: 0} m_Icon: {fileID: 0}
m_NavMeshLayer: 0 m_NavMeshLayer: 0
m_StaticEditorFlags: 0 m_StaticEditorFlags: 0
m_IsActive: 1 m_IsActive: 0
--- !u!114 &292487262 --- !u!114 &292487262
MonoBehaviour: MonoBehaviour:
m_ObjectHideFlags: 0 m_ObjectHideFlags: 0

@ -47,14 +47,14 @@ public class BodyLinkHandler : MonoBehaviour, IPointerClickHandler, IPointerExit
EmailData email = emailPanel.Email; EmailData email = emailPanel.Email;
if (email != null) if (email != null)
{ {
SceneOutcomeManager.Instance?.Clicked(email);
//UserActionLogger.Instance?.Log($"Clicked link '{linkID}' in email from '{email.senderName}'"); //UserActionLogger.Instance?.Log($"Clicked link '{linkID}' in email from '{email.senderName}'");
string englishLog = $"Clicked link '{linkID}' in email from '{email.senderName}'"; string englishLog = $"Clicked link '{linkID}' in email from '{email.senderName}'";
string arabicLog = $"تم الضغط على الرابط '{linkID}' في البريد من '{email.senderName}'"; string arabicLog = $"تم الضغط على الرابط '{linkID}' في البريد من '{email.senderName}'";
UserActionLogger.Instance?.Log(englishLog, arabicLog); UserActionLogger.Instance?.Log(englishLog, arabicLog);
bool isOptimal = !email.isPhishing; bool isOptimal = !email.isPhishing;
SupabaseEventLogger.Instance?.LogDecisionEvent(isOptimal); SupabaseEventLogger.Instance?.LogDecisionEvent(isOptimal);
SceneOutcomeManager.Instance?.Clicked(email);
} }
else else

@ -65,21 +65,42 @@ public class UserActionLogger : MonoBehaviour
[ContextMenu("ShowSummary")] [ContextMenu("ShowSummary")]
public void ShowSummary() public void ShowSummary()
{ {
if (summaryText != null) if (summaryText == null)
{ return;
string logs = GetFullLog();
// Default to showing original logs
summaryText.text = logs;
bool isArabic = LanguageManager.Instance != null && // If Arabic language is active, fix and apply Arabic font
LanguageManager.Instance.currentLanguage == "Arabic"; if (LanguageManager.Instance != null &&
string logs = GetFullLog(); LanguageManager.Instance.currentLanguage == "Arabic")
if (isArabic) {
{ summaryText.text = ArabicFixer.Fix(logs);
summaryText.text = ArabicFixer.Fix(logs); summaryText.font = LanguageManager.Instance.fontArabic;
summaryText.font = LanguageManager.Instance.fontArabic;
summaryText.ForceMeshUpdate();
}
} }
summaryText.ForceMeshUpdate();
} }
//public void ShowSummary()
//{
// if (summaryText != null)
// {
// bool isArabic = LanguageManager.Instance != null &&
// LanguageManager.Instance.currentLanguage == "Arabic";
// string logs = GetFullLog();
// if (isArabic)
// {
// summaryText.text = ArabicFixer.Fix(logs);
// summaryText.font = LanguageManager.Instance.fontArabic;
// summaryText.ForceMeshUpdate();
// }
// }
//}
public void ClearLog() public void ClearLog()
{ {
logs.Clear(); logs.Clear();

@ -1,15 +1,17 @@
using UnityEngine; using UnityEngine;
using UnityEngine.Networking;
using System; using System;
using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks;
using Supabase; // Make sure SupabaseManager initializes this correctly
using Postgrest.Models;
using Postgrest.Attributes;
public class SupabaseEventLogger : MonoBehaviour public class SupabaseEventLogger : MonoBehaviour
{ {
public static SupabaseEventLogger Instance; public static SupabaseEventLogger Instance;
[Header("Supabase")]
public string supabaseUrl = "https://vihjspljbslozbjzxutl.supabase.co";
public string supabaseAnonKey = "YOUR_ANON_KEY_HERE";
private DateTime sessionStartTime; private DateTime sessionStartTime;
private void Awake() private void Awake()
@ -20,161 +22,539 @@ public class SupabaseEventLogger : MonoBehaviour
Destroy(gameObject); Destroy(gameObject);
} }
/// <summary> [Serializable]
/// Call this at the start of the game session. public class GameEventPayload
/// </summary> {
public async void StartSession() public string event_key;
public string timestamp;
public string user_id;
}
[Serializable]
public class GameAttemptPayload
{
public string game_id;
public string scenario_id;
public string user_id;
public int attempt_number;
public string start_timestamp;
public string end_timestamp;
public int duration_seconds;
public float final_score_percentage;
public bool pass_fail_status;
public int optimal_decisions_count;
public int suboptimal_decisions_count;
public string key_decisions_log;
}
[Serializable]
public class Decision
{
public string decisionId;
public string timestamp;
public bool optimal;
}
[Serializable]
public class DecisionLogWrapper
{
public List<Decision> decisions;
}
public void StartSession()
{ {
sessionStartTime = DateTime.UtcNow; sessionStartTime = DateTime.UtcNow;
var gameEvent = new GameEvent GameEventPayload payload = new GameEventPayload
{ {
Id = Guid.NewGuid(), // <== Ensure this is explicitly set event_key = "game_session_started",
EventKey = "game_session_started", timestamp = sessionStartTime.ToString("o"),
Timestamp = sessionStartTime, user_id = "user123"
UserId = "user123"
}; };
await Client.Instance.From<GameEvent>().Insert(gameEvent);
Debug.Log("✅ Supabase Event: game_session_started"); StartCoroutine(PostToSupabase("game_events", JsonUtility.ToJson(payload)));
} }
/// <summary> public void LogDecisionEvent(bool isOptimal)
/// Logs optimal/suboptimal decisions at runtime.
/// </summary>
public async void LogDecisionEvent(bool isOptimal)
{ {
string eventKey = isOptimal ? "game_optimal_decision_made" : "game_suboptimal_decision_made"; string eventKey = isOptimal ? "game_optimal_decision_made" : "game_suboptimal_decision_made";
var gameEvent = new GameEvent GameEventPayload payload = new GameEventPayload
{ {
Id = Guid.NewGuid(), event_key = eventKey,
EventKey = eventKey, timestamp = DateTime.UtcNow.ToString("o"),
Timestamp = DateTime.UtcNow, user_id = "user123"
UserId = "user123"
}; };
await Client.Instance.From<GameEvent>().Insert(gameEvent); StartCoroutine(PostToSupabase("game_events", JsonUtility.ToJson(payload)));
Debug.Log($"✅ Supabase Event: {eventKey}");
} }
/// <summary> public void CompleteSessionAndSubmitResult(string userId, bool passed, int optimal, int suboptimal, string scenarioId, List<Decision> decisionLog = null)
/// Completes the session and submits full results to phishing_game_attempts table.
/// </summary>
public async void CompleteSessionAndSubmitResult(string userId, bool passed, int optimal, int suboptimal, string scenarioId, List<Decision> decisionLog = null)
{ {
var endTime = DateTime.UtcNow; var endTime = DateTime.UtcNow;
int duration = (int)(endTime - sessionStartTime).TotalSeconds; int duration = (int)(endTime - sessionStartTime).TotalSeconds;
// Log completion events // Submit game_session_completed event
await Client.Instance.From<GameEvent>().Insert(new GameEvent GameEventPayload completedEvent = new GameEventPayload
{ {
Id = Guid.NewGuid(), event_key = "game_session_completed",
EventKey = "game_session_completed", timestamp = endTime.ToString("o"),
Timestamp = endTime, user_id = userId
UserId = userId };
}); StartCoroutine(PostToSupabase("game_events", JsonUtility.ToJson(completedEvent)));
await Client.Instance.From<GameEvent>().Insert(new GameEvent // Submit game_score_recorded event
{Id = Guid.NewGuid(), GameEventPayload scoreEvent = new GameEventPayload
EventKey = "game_score_recorded",
Timestamp = endTime,
UserId = userId
});
// Insert session result
var gameAttempt = new GameAttempt
{ {
GameId = "phishing-awareness-1", event_key = "game_score_recorded",
ScenarioId = scenarioId, timestamp = endTime.ToString("o"),
UserId = userId, user_id = userId
AttemptNumber = 1,
StartTime = sessionStartTime,
EndTime = endTime,
DurationSeconds = duration,
Score = passed ? 100 : 50,
Passed = passed,
Optimal = optimal,
Suboptimal = suboptimal,
KeyDecisionsLogJson = decisionLog != null ? JsonUtility.ToJson(new DecisionLogWrapper { decisions = decisionLog }) : "[]"
}; };
StartCoroutine(PostToSupabase("game_events", JsonUtility.ToJson(scoreEvent)));
await Client.Instance.From<GameAttempt>().Insert(gameAttempt); // Submit final game attempt data
Debug.Log("✅ Supabase Game Result Submitted"); GameAttemptPayload attempt = new GameAttemptPayload
} {
game_id = "phishing-awareness-1",
scenario_id = scenarioId,
user_id = userId,
attempt_number = 1,
start_timestamp = sessionStartTime.ToString("o"),
end_timestamp = endTime.ToString("o"),
duration_seconds = duration,
final_score_percentage = passed ? 100 : 50,
pass_fail_status = passed,
optimal_decisions_count = optimal,
suboptimal_decisions_count = suboptimal,
key_decisions_log = decisionLog != null ? JsonUtility.ToJson(new DecisionLogWrapper { decisions = decisionLog }) : "[]"
};
[Serializable] StartCoroutine(PostToSupabase("phishing_game_attempts", JsonUtility.ToJson(attempt)));
public class Decision
{
public string decisionId;
public string timestamp;
public bool optimal;
} }
[Serializable] private IEnumerator PostToSupabase(string table, string jsonBody)
public class DecisionLogWrapper
{ {
public List<Decision> decisions; string url = $"{supabaseUrl}/rest/v1/{table}";
} UnityWebRequest request = new UnityWebRequest(url, "POST");
} byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(jsonBody);
request.uploadHandler = new UploadHandlerRaw(bodyRaw);
// Existing SupabaseEventLogger class here... request.downloadHandler = new DownloadHandlerBuffer();
// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
[Table("game_events")]
public class GameEvent : BaseModel
{
[PrimaryKey("id", false)]
public Guid Id { get; set; }
[Column("event_key")] request.SetRequestHeader("Content-Type", "application/json");
public string EventKey { get; set; } request.SetRequestHeader("apikey", supabaseAnonKey);
request.SetRequestHeader("Authorization", "Bearer " + supabaseAnonKey);
request.SetRequestHeader("Prefer", "return=representation");
[Column("timestamp")] yield return request.SendWebRequest();
public DateTime Timestamp { get; set; }
[Column("user_id")] if (request.result == UnityWebRequest.Result.Success)
public string UserId { get; set; } {
Debug.Log($"✅ Supabase POST to {table}: " + request.downloadHandler.text);
}
else
{
Debug.LogError($"❌ Supabase POST Failed ({table}): {request.responseCode}\n{request.error}\n{request.downloadHandler.text}");
}
}
} }
[Table("phishing_game_attempts")]
public class GameAttempt : BaseModel
{
[PrimaryKey("id", false)]
public Guid Id { get; set; } = Guid.NewGuid();
[Column("game_id")]
public string GameId { get; set; }
[Column("scenario_id")]
public string ScenarioId { get; set; }
[Column("user_id")] //using UnityEngine;
public string UserId { get; set; } //using System;
//using System.Collections;
//using System.Collections.Generic;
//using System.Threading.Tasks;
//using Supabase;
//using Postgrest.Models;
//using Postgrest.Attributes;
//using static SupabaseTestInsert;
[Column("attempt_number")] //[Table("game_events")]
public int AttemptNumber { get; set; } //public class GameEvent : BaseModel
//{
// [PrimaryKey("id", false)]
// public Guid Id { get; set; }
[Column("start_timestamp")] // [Column("event_key")]
public DateTime StartTime { get; set; } // public string EventKey { get; set; }
[Column("end_timestamp")] // [Column("timestamp")]
public DateTime EndTime { get; set; } // public DateTime Timestamp { get; set; }
[Column("duration_seconds")] // [Column("user_id")]
public int DurationSeconds { get; set; } // public string UserId { get; set; }
//}
[Column("final_score_percentage")] //[Table("phishing_game_attempts")]
public float Score { get; set; } //public class GameAttempt : BaseModel
//{
// [PrimaryKey("id", false)]
// public Guid Id { get; set; } = Guid.NewGuid();
[Column("pass_fail_status")] // [Column("game_id")]
public bool Passed { get; set; } // public string GameId { get; set; }
[Column("optimal_decisions_count")] // [Column("scenario_id")]
public int Optimal { get; set; } // public string ScenarioId { get; set; }
[Column("suboptimal_decisions_count")] // [Column("user_id")]
public int Suboptimal { get; set; } // public string UserId { get; set; }
[Column("key_decisions_log")] // [Column("attempt_number")]
public string KeyDecisionsLogJson { get; set; } // public int AttemptNumber { get; set; }
}
// [Column("start_timestamp")]
// public DateTime StartTime { get; set; }
// [Column("end_timestamp")]
// public DateTime EndTime { get; set; }
// [Column("duration_seconds")]
// public int DurationSeconds { get; set; }
// [Column("final_score_percentage")]
// public float Score { get; set; }
// [Column("pass_fail_status")]
// public bool Passed { get; set; }
// [Column("optimal_decisions_count")]
// public int Optimal { get; set; }
// [Column("suboptimal_decisions_count")]
// public int Suboptimal { get; set; }
// [Column("key_decisions_log")]
// public string KeyDecisionsLogJson { get; set; }
//}
//public class SupabaseEventLogger : MonoBehaviour
//{
// public static SupabaseEventLogger Instance;
// private DateTime sessionStartTime;
// private void Awake()
// {
// if (Instance == null)
// Instance = this;
// else
// Destroy(gameObject);
// }
// public void StartSession()
// {
// StartCoroutine(StartSessionCoroutine());
// }
// private IEnumerator StartSessionCoroutine()
// {
// var task = StartSessionAsync();
// while (!task.IsCompleted)
// yield return null;
// if (task.Exception != null)
// Debug.LogError("❌ Supabase Error: " + task.Exception.InnerException?.Message);
// }
// private async Task StartSessionAsync()
// {
// sessionStartTime = DateTime.UtcNow;
// var gameEvent = new GameEvent
// {
// Id = Guid.NewGuid(),
// EventKey = "game_session_started",
// Timestamp = sessionStartTime,
// UserId = "user123"
// };
// await Client.Instance.From<GameEvent>().Insert(gameEvent);
// Debug.Log("✅ Supabase Event: game_session_started");
// }
// public void LogDecisionEvent(bool isOptimal)
// {
// StartCoroutine(LogDecisionCoroutine(isOptimal));
// }
// private IEnumerator LogDecisionCoroutine(bool isOptimal)
// {
// var task = LogDecisionAsync(isOptimal);
// while (!task.IsCompleted)
// yield return null;
// if (task.Exception != null)
// Debug.LogError("❌ Supabase Error: " + task.Exception.InnerException?.Message);
// }
// private async Task LogDecisionAsync(bool isOptimal)
// {
// string eventKey = isOptimal ? "game_optimal_decision_made" : "game_suboptimal_decision_made";
// var gameEvent = new GameEvent
// {
// Id = Guid.NewGuid(),
// EventKey = eventKey,
// Timestamp = DateTime.UtcNow,
// UserId = "user123"
// };
// await Client.Instance.From<GameEvent>().Insert(gameEvent);
// Debug.Log($"✅ Supabase Event: {eventKey}");
// }
// public void CompleteSessionAndSubmitResult(string userId, bool passed, int optimal, int suboptimal, string scenarioId, List<Decision> decisionLog = null)
// {
// StartCoroutine(CompleteSessionCoroutine(userId, passed, optimal, suboptimal, scenarioId, decisionLog));
// }
// private IEnumerator CompleteSessionCoroutine(string userId, bool passed, int optimal, int suboptimal, string scenarioId, List<Decision> decisionLog)
// {
// var task = CompleteSessionAsync(userId, passed, optimal, suboptimal, scenarioId, decisionLog);
// while (!task.IsCompleted)
// yield return null;
// if (task.Exception != null)
// Debug.LogError("❌ Supabase Error: " + task.Exception.InnerException?.Message);
// }
// private async Task CompleteSessionAsync(string userId, bool passed, int optimal, int suboptimal, string scenarioId, List<Decision> decisionLog)
// {
// var endTime = DateTime.UtcNow;
// int duration = (int)(endTime - sessionStartTime).TotalSeconds;
// await Client.Instance.From<GameEvent>().Insert(new GameEvent
// {
// Id = Guid.NewGuid(),
// EventKey = "game_session_completed",
// Timestamp = endTime,
// UserId = userId
// });
// await Client.Instance.From<GameEvent>().Insert(new GameEvent
// {
// Id = Guid.NewGuid(),
// EventKey = "game_score_recorded",
// Timestamp = endTime,
// UserId = userId
// });
// var gameAttempt = new GameAttempt
// {
// Id = Guid.NewGuid(),
// GameId = "phishing-awareness-1",
// ScenarioId = scenarioId,
// UserId = userId,
// AttemptNumber = 1,
// StartTime = sessionStartTime,
// EndTime = endTime,
// DurationSeconds = duration,
// Score = passed ? 100 : 50,
// Passed = passed,
// Optimal = optimal,
// Suboptimal = suboptimal,
// KeyDecisionsLogJson = decisionLog != null ? JsonUtility.ToJson(new DecisionLogWrapper { decisions = decisionLog }) : "[]"
// };
// await Client.Instance.From<GameAttempt>().Insert(gameAttempt);
// Debug.Log("✅ Supabase Game Result Submitted");
// }
// [Serializable]
// public class Decision
// {
// public string decisionId;
// public string timestamp;
// public bool optimal;
// }
// [Serializable]
// public class DecisionLogWrapper
// {
// public List<Decision> decisions;
// }
//}
////using UnityEngine;
////using System;
////using System.Collections.Generic;
////using System.Threading.Tasks;
////using Supabase; // Make sure SupabaseManager initializes this correctly
////using Postgrest.Models;
////using Postgrest.Attributes;
////public class SupabaseEventLogger : MonoBehaviour
////{
//// public static SupabaseEventLogger Instance;
//// private DateTime sessionStartTime;
//// private void Awake()
//// {
//// if (Instance == null)
//// Instance = this;
//// else
//// Destroy(gameObject);
//// }
//// /// <summary>
//// /// Call this at the start of the game session.
//// /// </summary>
//// public async void StartSession()
//// {
//// sessionStartTime = DateTime.UtcNow;
//// var gameEvent = new GameEvent
//// {
//// Id = Guid.NewGuid(), // <== Ensure this is explicitly set
//// EventKey = "game_session_started",
//// Timestamp = sessionStartTime,
//// UserId = "user123"
//// };
//// await Client.Instance.From<GameEvent>().Insert(gameEvent);
//// Debug.Log("✅ Supabase Event: game_session_started");
//// }
//// /// <summary>
//// /// Logs optimal/suboptimal decisions at runtime.
//// /// </summary>
//// public async void LogDecisionEvent(bool isOptimal)
//// {
//// string eventKey = isOptimal ? "game_optimal_decision_made" : "game_suboptimal_decision_made";
//// var gameEvent = new GameEvent
//// {
//// Id = Guid.NewGuid(),
//// EventKey = eventKey,
//// Timestamp = DateTime.UtcNow,
//// UserId = "user123"
//// };
//// await Client.Instance.From<GameEvent>().Insert(gameEvent);
//// Debug.Log($"✅ Supabase Event: {eventKey}");
//// }
//// /// <summary>
//// /// Completes the session and submits full results to phishing_game_attempts table.
//// /// </summary>
//// public async void CompleteSessionAndSubmitResult(string userId, bool passed, int optimal, int suboptimal, string scenarioId, List<Decision> decisionLog = null)
//// {
//// var endTime = DateTime.UtcNow;
//// int duration = (int)(endTime - sessionStartTime).TotalSeconds;
//// // Log completion events
//// await Client.Instance.From<GameEvent>().Insert(new GameEvent
//// {
//// Id = Guid.NewGuid(),
//// EventKey = "game_session_completed",
//// Timestamp = endTime,
//// UserId = userId
//// });
//// await Client.Instance.From<GameEvent>().Insert(new GameEvent
//// {Id = Guid.NewGuid(),
//// EventKey = "game_score_recorded",
//// Timestamp = endTime,
//// UserId = userId
//// });
//// // Insert session result
//// var gameAttempt = new GameAttempt
//// {
//// GameId = "phishing-awareness-1",
//// ScenarioId = scenarioId,
//// UserId = userId,
//// AttemptNumber = 1,
//// StartTime = sessionStartTime,
//// EndTime = endTime,
//// DurationSeconds = duration,
//// Score = passed ? 100 : 50,
//// Passed = passed,
//// Optimal = optimal,
//// Suboptimal = suboptimal,
//// KeyDecisionsLogJson = decisionLog != null ? JsonUtility.ToJson(new DecisionLogWrapper { decisions = decisionLog }) : "[]"
//// };
//// await Client.Instance.From<GameAttempt>().Insert(gameAttempt);
//// Debug.Log("✅ Supabase Game Result Submitted");
//// }
//// [Serializable]
//// public class Decision
//// {
//// public string decisionId;
//// public string timestamp;
//// public bool optimal;
//// }
//// [Serializable]
//// public class DecisionLogWrapper
//// {
//// public List<Decision> decisions;
//// }
////}
////// Existing SupabaseEventLogger class here...
////// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
////[Table("game_events")]
////public class GameEvent : BaseModel
////{
//// [PrimaryKey("id", false)]
//// public Guid Id { get; set; }
//// [Column("event_key")]
//// public string EventKey { get; set; }
//// [Column("timestamp")]
//// public DateTime Timestamp { get; set; }
//// [Column("user_id")]
//// public string UserId { get; set; }
////}
////[Table("phishing_game_attempts")]
////public class GameAttempt : BaseModel
////{
//// [PrimaryKey("id", false)]
//// public Guid Id { get; set; } = Guid.NewGuid();
//// [Column("game_id")]
//// public string GameId { get; set; }
//// [Column("scenario_id")]
//// public string ScenarioId { get; set; }
//// [Column("user_id")]
//// public string UserId { get; set; }
//// [Column("attempt_number")]
//// public int AttemptNumber { get; set; }
//// [Column("start_timestamp")]
//// public DateTime StartTime { get; set; }
//// [Column("end_timestamp")]
//// public DateTime EndTime { get; set; }
//// [Column("duration_seconds")]
//// public int DurationSeconds { get; set; }
//// [Column("final_score_percentage")]
//// public float Score { get; set; }
//// [Column("pass_fail_status")]
//// public bool Passed { get; set; }
//// [Column("optimal_decisions_count")]
//// public int Optimal { get; set; }
//// [Column("suboptimal_decisions_count")]
//// public int Suboptimal { get; set; }
//// [Column("key_decisions_log")]
//// public string KeyDecisionsLogJson { get; set; }
////}

Binary file not shown.

@ -816,14 +816,14 @@ PlayerSettings:
webGLExceptionSupport: 1 webGLExceptionSupport: 1
webGLNameFilesAsHashes: 0 webGLNameFilesAsHashes: 0
webGLShowDiagnostics: 0 webGLShowDiagnostics: 0
webGLDataCaching: 1 webGLDataCaching: 0
webGLDebugSymbols: 0 webGLDebugSymbols: 0
webGLEmscriptenArgs: webGLEmscriptenArgs:
webGLModulesDirectory: webGLModulesDirectory:
webGLTemplate: APPLICATION:Default webGLTemplate: APPLICATION:Default
webGLAnalyzeBuildSize: 0 webGLAnalyzeBuildSize: 0
webGLUseEmbeddedResources: 0 webGLUseEmbeddedResources: 0
webGLCompressionFormat: 1 webGLCompressionFormat: 2
webGLWasmArithmeticExceptions: 0 webGLWasmArithmeticExceptions: 0
webGLLinkerTarget: 1 webGLLinkerTarget: 1
webGLThreadsSupport: 0 webGLThreadsSupport: 0

Loading…
Cancel
Save