From fd7a89701d7fb38cf7b82dae91a63aee3b00234d Mon Sep 17 00:00:00 2001
From: KINDNICK <68893236+KINDNICK@users.noreply.github.com>
Date: Tue, 4 Nov 2025 23:13:48 +0900
Subject: [PATCH] =?UTF-8?q?ADD=20:=20=ED=9B=84=EC=9B=90=20=EC=9D=B4?=
=?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=EC=B2=98=EB=A6=AC=20=EC=8A=A4=ED=81=AC?=
=?UTF-8?q?=EB=A6=BD=ED=8A=B8=20=EC=B6=94=EA=B0=80?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
Assets/Scripts/WefLab.meta | 8 +
.../Scripts/WefLab/WefLabWebSocketClient.cs | 729 ++++++++++++++++++
.../WefLab/WefLabWebSocketClient.cs.meta | 2 +
3 files changed, 739 insertions(+)
create mode 100644 Assets/Scripts/WefLab.meta
create mode 100644 Assets/Scripts/WefLab/WefLabWebSocketClient.cs
create mode 100644 Assets/Scripts/WefLab/WefLabWebSocketClient.cs.meta
diff --git a/Assets/Scripts/WefLab.meta b/Assets/Scripts/WefLab.meta
new file mode 100644
index 00000000..14ad623e
--- /dev/null
+++ b/Assets/Scripts/WefLab.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 8cc56f0c0c16686438285879ef2a7583
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/WefLab/WefLabWebSocketClient.cs b/Assets/Scripts/WefLab/WefLabWebSocketClient.cs
new file mode 100644
index 00000000..9f1a30c0
--- /dev/null
+++ b/Assets/Scripts/WefLab/WefLabWebSocketClient.cs
@@ -0,0 +1,729 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+using UnityEngine;
+using UnityEngine.Events;
+using UnityEngine.Networking;
+using WebSocketSharp;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace WefLab
+{
+ ///
+ /// Donation data structure
+ ///
+ [System.Serializable]
+ public class DonationData
+ {
+ public string platform;
+ public string donationType;
+ public int amount;
+ public string message;
+ public string donorName;
+ public long timestamp;
+ }
+
+ ///
+ /// Unity event with donation data parameter
+ ///
+ [System.Serializable]
+ public class DonationEvent : UnityEvent { }
+
+ ///
+ /// Donation event trigger based on amount range
+ ///
+ [System.Serializable]
+ public class DonationEventTrigger
+ {
+ [Tooltip("Name for this donation trigger (e.g., 'Small Donation', 'Large Donation')")]
+ public string triggerName = "New Trigger";
+
+ [Header("Amount Range")]
+ [Tooltip("Minimum donation amount (inclusive)")]
+ public int minAmount = 0;
+
+ [Tooltip("Maximum donation amount (inclusive, -1 for unlimited)")]
+ public int maxAmount = -1;
+
+ [Header("Event Settings")]
+ [Tooltip("Number of times to repeat the event (minimum 1)")]
+ [Min(1)]
+ public int repeatCount = 1;
+
+ [Tooltip("Delay between each repeat in seconds (0 for immediate)")]
+ [Min(0)]
+ public float repeatDelay = 0f;
+
+ [Header("Event")]
+ [Tooltip("Unity event to trigger when donation amount is in range")]
+ public DonationEvent onDonation;
+
+ ///
+ /// Check if donation amount is within range
+ ///
+ public bool IsInRange(int amount)
+ {
+ if (amount < minAmount)
+ return false;
+
+ if (maxAmount >= 0 && amount > maxAmount)
+ return false;
+
+ return true;
+ }
+ }
+
+ ///
+ /// WefLab WebSocket client for receiving donation events
+ /// Implements Engine.IO v4 and Socket.IO protocol
+ ///
+ public class WefLabWebSocketClient : MonoBehaviour
+ {
+ [Header("WefLab Settings")]
+ [Tooltip("WefLab page URL (e.g., https://weflab.com/page/guPK2ODAmmRvYG4)")]
+ public string pageUrl = "https://weflab.com/page/guPK2ODAmmRvYG4";
+
+ [Header("Donation Event Triggers")]
+ [Tooltip("Donation event triggers based on amount ranges")]
+ public DonationEventTrigger[] donationTriggers = Array.Empty();
+
+ // Hidden connection info
+ [HideInInspector] public bool isConnected = false;
+ [HideInInspector] public string currentSid = "";
+ [HideInInspector] public string extractedUserIdx = "";
+
+ // WebSocket connection
+ private WebSocket ws;
+ private string userIdx = ""; // Will be extracted from page
+ private string socketSid = "";
+ private bool isExtracting = false;
+
+ // Ping/Pong settings
+ private float pingInterval = 30f;
+ private float lastPingTime = 0f;
+
+ // Thread-safe action queue for main thread
+ private Queue mainThreadActions = new Queue();
+ private object actionLock = new object();
+
+ void Start()
+ {
+ // Extract user idx from page URL and then connect
+ StartCoroutine(ExtractUserIdxAndConnect());
+ }
+
+ void Update()
+ {
+ // Execute queued actions on main thread
+ lock (actionLock)
+ {
+ while (mainThreadActions.Count > 0)
+ {
+ mainThreadActions.Dequeue()?.Invoke();
+ }
+ }
+ }
+
+ void OnDestroy()
+ {
+ Disconnect();
+ }
+
+ ///
+ /// Extract user index from page URL and connect
+ ///
+ private IEnumerator ExtractUserIdxAndConnect()
+ {
+ if (string.IsNullOrEmpty(pageUrl))
+ {
+ Debug.LogError("[WefLab] Page URL is empty! Please set the page URL in Inspector.");
+ yield break;
+ }
+
+ isExtracting = true;
+ Debug.Log($"[WefLab] Fetching page URL: {pageUrl}");
+
+ // Fetch the page HTML
+ using (UnityWebRequest request = UnityWebRequest.Get(pageUrl))
+ {
+ yield return request.SendWebRequest();
+
+ if (request.result != UnityWebRequest.Result.Success)
+ {
+ Debug.LogError($"[WefLab] Failed to fetch page: {request.error}");
+ isExtracting = false;
+ yield break;
+ }
+
+ string htmlContent = request.downloadHandler.text;
+
+ // Extract userIdx from JavaScript in the page
+ // Look for pattern: loginData = {...}
+ Match loginDataMatch = Regex.Match(htmlContent, @"loginData\s*=\s*(\{[^;]+\});", RegexOptions.Singleline);
+
+ if (loginDataMatch.Success)
+ {
+ try
+ {
+ string loginDataJson = loginDataMatch.Groups[1].Value;
+ var loginData = JsonConvert.DeserializeObject(loginDataJson);
+
+ // Extract idx from loginData
+ userIdx = loginData["idx"]?.ToString() ?? "";
+
+ if (!string.IsNullOrEmpty(userIdx))
+ {
+ extractedUserIdx = userIdx;
+ Debug.Log($"[WefLab] Successfully extracted userIdx: {userIdx}");
+ }
+ else
+ {
+ Debug.LogError("[WefLab] userIdx not found in loginData");
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.LogError($"[WefLab] Error parsing loginData: {ex.Message}");
+ }
+ }
+ else
+ {
+ Debug.LogError("[WefLab] Could not find loginData in page HTML");
+ }
+ }
+
+ isExtracting = false;
+
+ // Connect if we successfully extracted the userIdx
+ if (!string.IsNullOrEmpty(userIdx))
+ {
+ Connect();
+ }
+ else
+ {
+ Debug.LogError("[WefLab] Cannot connect - userIdx extraction failed");
+ }
+ }
+
+ ///
+ /// Connect to WefLab WebSocket server
+ ///
+ public void Connect()
+ {
+ if (ws != null && ws.IsAlive)
+ {
+ Debug.LogWarning("[WefLab] Already connected");
+ return;
+ }
+
+ // Build WebSocket URL
+ string wsUrl = $"wss://ssmain.weflab.com/socket.io/?idx={userIdx}&type=page&page=alert&EIO=4&transport=websocket";
+
+ Debug.Log($"[WefLab] Connecting to: {wsUrl}");
+
+ ws = new WebSocket(wsUrl);
+
+ // Event handlers
+ ws.OnOpen += OnWebSocketOpen;
+ ws.OnMessage += OnWebSocketMessage;
+ ws.OnError += OnWebSocketError;
+ ws.OnClose += OnWebSocketClose;
+
+ // Connect
+ ws.ConnectAsync();
+ }
+
+ ///
+ /// Disconnect from WebSocket
+ ///
+ public void Disconnect()
+ {
+ if (ws != null)
+ {
+ ws.Close();
+ ws = null;
+ }
+
+ isConnected = false;
+ socketSid = "";
+ currentSid = "";
+ }
+
+ #region WebSocket Event Handlers
+
+ private void OnWebSocketOpen(object sender, EventArgs e)
+ {
+ EnqueueMainThreadAction(() =>
+ {
+ Debug.Log("[WefLab] WebSocket connection opened");
+ });
+ }
+
+ private void OnWebSocketMessage(object sender, MessageEventArgs e)
+ {
+ string data = e.Data;
+
+ EnqueueMainThreadAction(() =>
+ {
+ ProcessMessage(data);
+ });
+ }
+
+ private void OnWebSocketError(object sender, ErrorEventArgs e)
+ {
+ EnqueueMainThreadAction(() =>
+ {
+ Debug.LogError($"[WefLab] WebSocket error: {e.Message}");
+ if (e.Exception != null)
+ {
+ Debug.LogError($"[WefLab] Exception: {e.Exception}");
+ }
+ });
+ }
+
+ private void OnWebSocketClose(object sender, CloseEventArgs e)
+ {
+ EnqueueMainThreadAction(() =>
+ {
+ Debug.Log($"[WefLab] WebSocket closed. Code: {e.Code}, Reason: {e.Reason}");
+ isConnected = false;
+ socketSid = "";
+ currentSid = "";
+ });
+ }
+
+ #endregion
+
+ #region Message Processing
+
+ ///
+ /// Process incoming WebSocket message
+ ///
+ private void ProcessMessage(string data)
+ {
+ if (string.IsNullOrEmpty(data))
+ return;
+
+ // Get message type (first character or first two characters)
+ string messageType = data.Length >= 2 && char.IsDigit(data[1])
+ ? data.Substring(0, 2)
+ : data.Substring(0, 1);
+
+ string payload = data.Substring(messageType.Length);
+
+ switch (messageType)
+ {
+ case "0": // Engine.IO OPEN
+ HandleEngineOpen(payload);
+ break;
+
+ case "2": // Engine.IO PING
+ HandlePing();
+ break;
+
+ case "3": // Engine.IO PONG
+ Debug.Log("[WefLab] Received PONG");
+ break;
+
+ case "40": // Socket.IO CONNECT
+ HandleSocketConnect(payload);
+ break;
+
+ case "42": // Socket.IO EVENT
+ HandleSocketEvent(payload);
+ break;
+
+ default:
+ Debug.Log($"[WefLab] Unknown message type: {messageType} | Data: {data}");
+ break;
+ }
+ }
+
+ ///
+ /// Handle Engine.IO OPEN message (handshake)
+ ///
+ private void HandleEngineOpen(string payload)
+ {
+ try
+ {
+ var openData = JsonConvert.DeserializeObject(payload);
+ currentSid = openData["sid"]?.ToString() ?? "";
+
+ if (openData["pingInterval"] != null)
+ {
+ pingInterval = openData["pingInterval"].Value() / 1000f; // Convert to seconds
+ }
+
+ Debug.Log($"[WefLab] Engine.IO OPEN - SID: {currentSid}, PingInterval: {pingInterval}s");
+ Debug.Log($"[WefLab] Full handshake data: {payload}");
+
+ // Send Socket.IO CONNECT
+ SendSocketConnect();
+ }
+ catch (Exception ex)
+ {
+ Debug.LogError($"[WefLab] Error parsing OPEN message: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Handle Engine.IO PING
+ ///
+ private void HandlePing()
+ {
+ Debug.Log("[WefLab] Received PING, sending PONG");
+ SendMessage("3"); // Send PONG
+ }
+
+ ///
+ /// Handle Socket.IO CONNECT response
+ ///
+ private void HandleSocketConnect(string payload)
+ {
+ try
+ {
+ if (!string.IsNullOrEmpty(payload))
+ {
+ var connectData = JsonConvert.DeserializeObject(payload);
+ socketSid = connectData["sid"]?.ToString() ?? "";
+ Debug.Log($"[WefLab] Socket.IO CONNECT - SID: {socketSid}");
+ Debug.Log($"[WefLab] Full connect data: {payload}");
+ }
+
+ isConnected = true;
+
+ // Send join message
+ SendJoinMessage();
+ }
+ catch (Exception ex)
+ {
+ Debug.LogError($"[WefLab] Error parsing Socket.IO CONNECT: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Handle Socket.IO EVENT message (donation data)
+ ///
+ private void HandleSocketEvent(string payload)
+ {
+ try
+ {
+ var eventArray = JsonConvert.DeserializeObject(payload);
+
+ if (eventArray == null || eventArray.Count < 2)
+ {
+ Debug.LogWarning($"[WefLab] Invalid event format: {payload}");
+ return;
+ }
+
+ string eventName = eventArray[0].ToString();
+ var eventData = eventArray[1] as JObject;
+
+ Debug.Log($"[WefLab] ========== EVENT RECEIVED ==========");
+ Debug.Log($"[WefLab] Event Name: {eventName}");
+ Debug.Log($"[WefLab] Event Data: {eventData?.ToString(Formatting.Indented)}");
+ Debug.Log($"[WefLab] ===================================");
+
+ if (eventName == "msg")
+ {
+ HandleMessageEvent(eventData);
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.LogError($"[WefLab] Error parsing Socket.IO EVENT: {ex.Message}");
+ Debug.LogError($"[WefLab] Payload: {payload}");
+ }
+ }
+
+ ///
+ /// Handle "msg" event (donation notifications)
+ ///
+ private void HandleMessageEvent(JObject msgData)
+ {
+ if (msgData == null)
+ return;
+
+ string msgType = msgData["type"]?.ToString() ?? "";
+
+ switch (msgType)
+ {
+ case "test_donation":
+ HandleTestDonation(msgData);
+ break;
+
+ case "donation":
+ case "SENDBALLOON":
+ case "cheese":
+ case "superchat":
+ case "bits":
+ HandleDonation(msgData);
+ break;
+
+ default:
+ Debug.Log($"[WefLab] Unhandled message type: {msgType}");
+ Debug.Log($"[WefLab] Data: {msgData.ToString(Formatting.Indented)}");
+ break;
+ }
+ }
+
+ ///
+ /// Normalize donation amount to unified currency (Chzzk standard: 1 = 1 KRW)
+ /// SOOP (Afreeca): 1 balloon = 100 KRW → multiply by 100
+ /// Chzzk: 1 cheese = 1 KRW → no conversion
+ /// YouTube: Keep as-is (already in KRW equivalent)
+ /// Twitch: Keep as-is
+ ///
+ private static int NormalizeDonationAmount(string platform, int rawAmount)
+ {
+ platform = platform.ToLower();
+
+ if (platform == "afreeca" || platform == "soop")
+ {
+ // SOOP: 1 balloon = 100 KRW
+ return rawAmount * 100;
+ }
+
+ // Chzzk, YouTube, Twitch: no conversion needed
+ return rawAmount;
+ }
+
+ ///
+ /// Handle test donation event
+ ///
+ private void HandleTestDonation(JObject msgData)
+ {
+ if (msgData["data"] is not JObject donationDataJson)
+ return;
+
+ string platform = donationDataJson["platform"]?.ToString() ?? "unknown";
+ string type = donationDataJson["type"]?.ToString() ?? "unknown";
+ int rawAmount = 0;
+
+ // Parse amount
+ if (int.TryParse(donationDataJson["value"]?.ToString(), out int parsedAmount))
+ {
+ rawAmount = parsedAmount;
+ }
+
+ // Normalize amount based on platform
+ int amount = NormalizeDonationAmount(platform, rawAmount);
+
+ string message = donationDataJson["msg"]?.ToString() ?? "";
+ long timestamp = donationDataJson["time"]?.Value() ?? 0;
+
+ Debug.Log($"[WefLab] *** TEST DONATION ***");
+ Debug.Log($"[WefLab] Platform: {platform}");
+ Debug.Log($"[WefLab] Type: {type}");
+ Debug.Log($"[WefLab] Raw Amount: {rawAmount} → Normalized: {amount} KRW");
+ Debug.Log($"[WefLab] Message: {message}");
+ Debug.Log($"[WefLab] Timestamp: {timestamp}");
+
+ // Create donation data and trigger events
+ DonationData donation = new()
+ {
+ platform = platform,
+ donationType = type,
+ amount = amount,
+ message = message,
+ donorName = "Test Donor",
+ timestamp = timestamp
+ };
+
+ TriggerDonationEvents(donation);
+ }
+
+ ///
+ /// Handle real donation event
+ ///
+ private void HandleDonation(JObject msgData)
+ {
+ Debug.Log($"[WefLab] *** REAL DONATION ***");
+ Debug.Log($"[WefLab] Full donation data: {msgData.ToString(Formatting.Indented)}");
+
+ if (msgData["data"] is not JObject donationDataJson)
+ return;
+
+ string platform = msgData["platform"]?.ToString() ?? "unknown";
+ string type = msgData["type"]?.ToString() ?? "unknown";
+ int rawAmount = 0;
+
+ // Parse amount from different possible fields
+ if (donationDataJson["value"] != null && int.TryParse(donationDataJson["value"].ToString(), out int parsedValue))
+ {
+ rawAmount = parsedValue;
+ }
+ else if (donationDataJson["amount"] != null && int.TryParse(donationDataJson["amount"].ToString(), out int parsedAmount))
+ {
+ rawAmount = parsedAmount;
+ }
+
+ // Normalize amount based on platform
+ int amount = NormalizeDonationAmount(platform, rawAmount);
+
+ string message = donationDataJson["msg"]?.ToString() ?? donationDataJson["message"]?.ToString() ?? "";
+ string donorName = donationDataJson["nickname"]?.ToString() ?? donationDataJson["name"]?.ToString() ?? "Anonymous";
+ long timestamp = donationDataJson["time"]?.Value() ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
+
+ // Create donation data
+ DonationData donation = new()
+ {
+ platform = platform,
+ donationType = type,
+ amount = amount,
+ message = message,
+ donorName = donorName,
+ timestamp = timestamp
+ };
+
+ Debug.Log($"[WefLab] Donation: {donorName} - {rawAmount} → {amount} KRW ({platform})");
+
+ // Trigger events
+ TriggerDonationEvents(donation);
+ }
+
+ ///
+ /// Trigger donation events based on amount range
+ ///
+ private void TriggerDonationEvents(DonationData donation)
+ {
+ if (donationTriggers == null || donationTriggers.Length == 0)
+ {
+ Debug.LogWarning("[WefLab] No donation triggers configured");
+ return;
+ }
+
+ bool triggeredAny = false;
+
+ // Check each trigger
+ foreach (var trigger in donationTriggers)
+ {
+ if (trigger == null)
+ continue;
+
+ // Check if donation amount is in range
+ if (trigger.IsInRange(donation.amount))
+ {
+ Debug.Log($"[WefLab] Triggering: {trigger.triggerName} (Amount: {donation.amount}, Range: {trigger.minAmount}-{(trigger.maxAmount >= 0 ? trigger.maxAmount.ToString() : "unlimited")}, Repeat: {trigger.repeatCount}x)");
+
+ // Start coroutine to handle repeated invocation
+ StartCoroutine(InvokeRepeatedEvent(trigger, donation));
+ triggeredAny = true;
+ }
+ }
+
+ if (!triggeredAny)
+ {
+ Debug.LogWarning($"[WefLab] No triggers matched for donation amount: {donation.amount}");
+ }
+ }
+
+ ///
+ /// Coroutine to invoke event multiple times with delay
+ ///
+ private IEnumerator InvokeRepeatedEvent(DonationEventTrigger trigger, DonationData donation)
+ {
+ for (int i = 0; i < trigger.repeatCount; i++)
+ {
+ // Invoke the event
+ trigger.onDonation?.Invoke(donation);
+
+ // Wait for delay before next repetition (skip on last iteration)
+ if (i < trigger.repeatCount - 1 && trigger.repeatDelay > 0)
+ {
+ yield return new WaitForSeconds(trigger.repeatDelay);
+ }
+ }
+ }
+
+ #endregion
+
+ #region Send Messages
+
+ ///
+ /// Send Socket.IO CONNECT message (40)
+ ///
+ private void SendSocketConnect()
+ {
+ Debug.Log("[WefLab] Sending Socket.IO CONNECT (40)");
+ SendMessage("40");
+ }
+
+ ///
+ /// Send join message to subscribe to donation events
+ ///
+ private void SendJoinMessage()
+ {
+ var joinData = new
+ {
+ type = "join",
+ page = "page",
+ idx = userIdx,
+ pageid = "alert",
+ preset = "0"
+ };
+
+ var message = new object[] { "msg", joinData };
+ string json = JsonConvert.SerializeObject(message);
+ string fullMessage = "42" + json;
+
+ Debug.Log($"[WefLab] Sending JOIN message: {fullMessage}");
+ SendMessage(fullMessage);
+ }
+
+ ///
+ /// Send raw message through WebSocket
+ ///
+ private void SendMessage(string message)
+ {
+ if (ws != null && ws.IsAlive)
+ {
+ ws.Send(message);
+ }
+ else
+ {
+ Debug.LogWarning("[WefLab] Cannot send message - WebSocket not connected");
+ }
+ }
+
+ #endregion
+
+ #region Thread Safety
+
+ ///
+ /// Enqueue action to be executed on main thread
+ ///
+ private void EnqueueMainThreadAction(Action action)
+ {
+ lock (actionLock)
+ {
+ mainThreadActions.Enqueue(action);
+ }
+ }
+
+ #endregion
+
+ #region Public Methods
+
+ ///
+ /// Reconnect to WebSocket
+ ///
+ public void Reconnect()
+ {
+ Disconnect();
+ Connect();
+ }
+
+ ///
+ /// Check if connected
+ ///
+ public bool IsConnected()
+ {
+ return isConnected && ws != null && ws.IsAlive;
+ }
+
+ #endregion
+ }
+}
diff --git a/Assets/Scripts/WefLab/WefLabWebSocketClient.cs.meta b/Assets/Scripts/WefLab/WefLabWebSocketClient.cs.meta
new file mode 100644
index 00000000..0081856b
--- /dev/null
+++ b/Assets/Scripts/WefLab/WefLabWebSocketClient.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 5b7cb7cbeddf11148824325615980e1a
\ No newline at end of file