using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Bansonic; using GameServer.Client; using UnityEngine; using UnityEngine.UI; public class friendsManager : MonoBehaviour { private const float RequestCooldownSeconds = 3f; private static string FriendServiceUnavailable => LocalizationService.Get("friends.notice.service_uninitialized", "好友服务未初始化"); private static string AlreadyFriendNotice => LocalizationService.Get("friends.search.already_friend", "你们已经是好友"); private static string NetworkNotInitialized => LocalizationService.Get("friends.search.network_not_initialized", "网络服务尚未初始化"); public InputField searchFriend_iptfd; public Button searchButton; [Header("FriendSearchObjects")] public GameObject friendSearchPrefab; public Transform friendSearchParent; [Header("FriendRequestObjects")] public GameObject friendRequestPrefab; public GameObject friendRequestParent; public Button quickAcceptAll; public Button quickRejectAll; [Header("Default Texts")] public Text defaultFriendSearchText; public Text defaultFriendRequestText; private readonly Dictionary _spawnedRequestCards = new Dictionary(); private readonly Dictionary _actionCooldownUntil = new Dictionary(); private readonly HashSet _pendingActions = new HashSet(); private GameObject _spawnedSearchCard; private bool _isSearching; private bool _isBatchProcessing; private Transform RequestParentTransform => friendRequestParent != null ? friendRequestParent.transform : null; private void OnEnable() { ClearSearchParentChildren(); ClearRequestParentChildren(); SetSearchDefaultVisible(false); SetRequestDefaultVisible(true); BindUiEvents(); if (ArenaRoomService.Instance != null) { ArenaRoomService.Instance.OnFriendRequestsChanged += HandleFriendRequestsChanged; } _ = InitializeAsync(); } private void OnDisable() { if (ArenaRoomService.Instance != null) { ArenaRoomService.Instance.OnFriendRequestsChanged -= HandleFriendRequestsChanged; } UnbindUiEvents(); } private async Task InitializeAsync() { ArenaRoomService arena = ArenaRoomService.Instance; if (arena == null) { SetRequestDefaultVisible(true); return; } try { IReadOnlyList requests = await arena.GetFriendRequestsAsync(true); RenderFriendRequests(requests); } catch (Exception ex) { gNotice.error.display(ex.Message); SetRequestDefaultVisible(true); } } private void BindUiEvents() { if (searchButton != null) { searchButton.onClick.RemoveListener(HandleSearchClicked); searchButton.onClick.AddListener(HandleSearchClicked); } if (searchFriend_iptfd != null) { searchFriend_iptfd.onEndEdit.RemoveListener(HandleSearchEndEdit); searchFriend_iptfd.onEndEdit.AddListener(HandleSearchEndEdit); } if (quickAcceptAll != null) { quickAcceptAll.onClick.RemoveListener(HandleQuickAcceptAll); quickAcceptAll.onClick.AddListener(HandleQuickAcceptAll); } if (quickRejectAll != null) { quickRejectAll.onClick.RemoveListener(HandleQuickRejectAll); quickRejectAll.onClick.AddListener(HandleQuickRejectAll); } } private void UnbindUiEvents() { if (searchButton != null) { searchButton.onClick.RemoveListener(HandleSearchClicked); } if (searchFriend_iptfd != null) { searchFriend_iptfd.onEndEdit.RemoveListener(HandleSearchEndEdit); } if (quickAcceptAll != null) { quickAcceptAll.onClick.RemoveListener(HandleQuickAcceptAll); } if (quickRejectAll != null) { quickRejectAll.onClick.RemoveListener(HandleQuickRejectAll); } } private void HandleFriendRequestsChanged(IReadOnlyList requests) { RenderFriendRequests(requests); } private void HandleSearchClicked() { string actionSignature = BuildSearchSignature(); if (!TryBeginAction(actionSignature)) { return; } BeginButtonCooldown(searchButton); _ = SearchExactPlayerAsync(actionSignature); } private void HandleSearchEndEdit(string value) { if (!isActiveAndEnabled) { return; } if (Input.GetKeyDown(KeyCode.Return) || Input.GetKeyDown(KeyCode.KeypadEnter)) { string actionSignature = BuildSearchSignature(); if (!TryBeginAction(actionSignature)) { return; } BeginButtonCooldown(searchButton); _ = SearchExactPlayerAsync(actionSignature); } } private async Task SearchExactPlayerAsync(string actionSignature) { if (_isSearching || string.IsNullOrWhiteSpace(actionSignature)) { EndAction(actionSignature); return; } _isSearching = true; SetSearchDefaultVisible(false); ClearSearchParentChildren(); try { string query = searchFriend_iptfd != null ? searchFriend_iptfd.text?.Trim() : string.Empty; ProfileData profile = await LookupProfileExactAsync(query); if (!IsValidProfile(profile)) { SetSearchDefaultVisible(true); return; } if (friendSearchPrefab == null || friendSearchParent == null) { return; } _spawnedSearchCard = Instantiate(friendSearchPrefab, friendSearchParent, false); UI_OrderedEntryAnimator.PlayFadeOnly(_spawnedSearchCard, 0, 0.16f, 0f, true); friendSearchPrefab prefab = _spawnedSearchCard.GetComponent(); if (prefab != null) { bool alreadyFriend = IsAlreadyFriend(profile.steam_id); bool canSendRequest = CanSendFriendRequest(profile.steam_id); prefab.Bind( profile, canSendRequest, alreadyFriend ? AlreadyFriendNotice : string.Empty, () => SendFriendRequestAsync(profile.steam_id), RequestCooldownSeconds); } } catch (Exception ex) { gNotice.error.display(ex.Message); SetSearchDefaultVisible(true); } finally { _isSearching = false; EndAction(actionSignature); } } private async Task LookupProfileExactAsync(string query) { if (string.IsNullOrWhiteSpace(query)) { return null; } NetworkManager network = NetworkManager.Instance; if (network == null) { throw new Exception(NetworkNotInitialized); } string trimmed = query.Trim(); if (trimmed.All(char.IsDigit)) { if (trimmed.Length >= 16) { return await SafeGetProfileBySteamId(network, trimmed); } if (int.TryParse(trimmed, out int uid) && uid > 0) { return await SafeGetProfileByUid(network, uid); } } return null; } private static async Task SafeGetProfileBySteamId(NetworkManager network, string steamId) { try { return await network.GetProfile(steamId); } catch { return null; } } private static async Task SafeGetProfileByUid(NetworkManager network, int uid) { try { return await network.GetPlayerByUid(uid); } catch { return null; } } private static bool IsValidProfile(ProfileData profile) { return profile != null && !string.IsNullOrWhiteSpace(profile.steam_id); } private bool CanSendFriendRequest(string targetSteamId) { if (string.IsNullOrWhiteSpace(targetSteamId)) { return false; } NetworkManager network = NetworkManager.Instance; if (network != null && string.Equals(network.SteamId, targetSteamId, StringComparison.Ordinal)) { return false; } return !IsAlreadyFriend(targetSteamId); } private bool IsAlreadyFriend(string targetSteamId) { if (string.IsNullOrWhiteSpace(targetSteamId)) { return false; } ArenaRoomService arena = ArenaRoomService.Instance; return arena != null && arena.GetFriendsSnapshot().Any(friend => friend != null && string.Equals(friend.friend_steam_id, targetSteamId, StringComparison.Ordinal)); } private async Task SendFriendRequestAsync(string targetSteamId) { string actionSignature = $"send:{targetSteamId}"; if (!TryBeginAction(actionSignature)) { return; } ArenaRoomService arena = ArenaRoomService.Instance; if (arena == null) { gNotice.error.display(FriendServiceUnavailable); EndAction(actionSignature); return; } try { await arena.SendFriendRequestAsync(targetSteamId); ClearSearchParentChildren(); SetSearchDefaultVisible(false); } catch (Exception ex) { gNotice.error.display(ex.Message); } finally { EndAction(actionSignature); } } private void RenderFriendRequests(IReadOnlyList requests) { Transform parent = RequestParentTransform; if (parent == null || friendRequestPrefab == null) { SetRequestDefaultVisible(true); return; } ClearRequestParentChildren(); List normalized = (requests ?? Array.Empty()) .Where(request => request != null && request.id > 0 && !string.IsNullOrWhiteSpace(request.from_steam_id)) .OrderByDescending(request => request.created_at ?? string.Empty, StringComparer.Ordinal) .ThenByDescending(request => request.id) .ToList(); for (int index = 0; index < normalized.Count; index++) { SocialFriendRequestEntry request = normalized[index]; GameObject cardObject = Instantiate(friendRequestPrefab, parent, false); _spawnedRequestCards[request.id] = cardObject; cardObject.transform.SetSiblingIndex(index); UI_OrderedEntryAnimator.PlayFadeOnly(cardObject, index, 0.16f, 0.012f, true); friendRequestPrefab prefab = cardObject.GetComponent(); if (prefab != null) { prefab.Bind( request, () => AcceptRequestAsync(request.id), () => RejectRequestAsync(request.id), RequestCooldownSeconds); } } SetRequestDefaultVisible(normalized.Count == 0); } private async Task AcceptRequestAsync(long requestId) { string actionSignature = $"accept:{requestId}"; if (!TryBeginAction(actionSignature)) { return; } ArenaRoomService arena = ArenaRoomService.Instance; if (arena == null) { gNotice.error.display(FriendServiceUnavailable); EndAction(actionSignature); return; } try { await arena.AcceptFriendRequestAsync(requestId); } catch (Exception ex) { gNotice.error.display(ex.Message); } finally { EndAction(actionSignature); } } private async Task RejectRequestAsync(long requestId) { string actionSignature = $"reject:{requestId}"; if (!TryBeginAction(actionSignature)) { return; } ArenaRoomService arena = ArenaRoomService.Instance; if (arena == null) { gNotice.error.display(FriendServiceUnavailable); EndAction(actionSignature); return; } try { await arena.RejectFriendRequestAsync(requestId); } catch (Exception ex) { gNotice.error.display(ex.Message); } finally { EndAction(actionSignature); } } private void HandleQuickAcceptAll() { const string actionSignature = "batch:accept"; if (_isBatchProcessing || !TryBeginAction(actionSignature)) { return; } BeginButtonCooldown(quickAcceptAll); _ = ProcessAllRequestsAsync(accept: true, actionSignature); } private void HandleQuickRejectAll() { const string actionSignature = "batch:reject"; if (_isBatchProcessing || !TryBeginAction(actionSignature)) { return; } BeginButtonCooldown(quickRejectAll); _ = ProcessAllRequestsAsync(accept: false, actionSignature); } private async Task ProcessAllRequestsAsync(bool accept, string actionSignature) { ArenaRoomService arena = ArenaRoomService.Instance; if (arena == null) { gNotice.error.display(FriendServiceUnavailable); EndAction(actionSignature); return; } _isBatchProcessing = true; SetBatchButtonsInteractable(false); try { List requestIds = arena.GetFriendRequestsSnapshot() .Where(request => request != null && request.id > 0) .Select(request => request.id) .ToList(); foreach (long requestId in requestIds) { if (accept) { await arena.AcceptFriendRequestAsync(requestId); } else { await arena.RejectFriendRequestAsync(requestId); } } } catch (Exception ex) { gNotice.error.display(ex.Message); } finally { _isBatchProcessing = false; EndAction(actionSignature); StartCoroutine(ReenableBatchButtonsAfterCooldown()); } } private string BuildSearchSignature() { string query = searchFriend_iptfd != null ? searchFriend_iptfd.text?.Trim() : string.Empty; return string.IsNullOrWhiteSpace(query) ? string.Empty : $"search:{query}"; } private bool TryBeginAction(string actionSignature) { if (string.IsNullOrWhiteSpace(actionSignature)) { return false; } float now = Time.unscaledTime; CleanupExpiredActions(now); if (_pendingActions.Contains(actionSignature)) { return false; } if (_actionCooldownUntil.TryGetValue(actionSignature, out float cooldownUntil) && cooldownUntil > now) { return false; } _pendingActions.Add(actionSignature); _actionCooldownUntil[actionSignature] = now + RequestCooldownSeconds; return true; } private void EndAction(string actionSignature) { if (string.IsNullOrWhiteSpace(actionSignature)) { return; } _pendingActions.Remove(actionSignature); } private void CleanupExpiredActions(float now) { List expiredKeys = null; foreach (KeyValuePair entry in _actionCooldownUntil) { if (_pendingActions.Contains(entry.Key) || entry.Value > now) { continue; } expiredKeys ??= new List(); expiredKeys.Add(entry.Key); } if (expiredKeys == null) { return; } foreach (string key in expiredKeys) { _actionCooldownUntil.Remove(key); } } private void BeginButtonCooldown(Button button) { if (button == null) { return; } StartCoroutine(ButtonCooldownRoutine(button)); } private IEnumerator ButtonCooldownRoutine(Button button) { button.interactable = false; yield return new WaitForSecondsRealtime(RequestCooldownSeconds); if (button != null) { button.interactable = true; } } private IEnumerator ReenableBatchButtonsAfterCooldown() { yield return new WaitForSecondsRealtime(RequestCooldownSeconds); SetBatchButtonsInteractable(true); } private void SetBatchButtonsInteractable(bool interactable) { if (quickAcceptAll != null) { quickAcceptAll.interactable = interactable; } if (quickRejectAll != null) { quickRejectAll.interactable = interactable; } } private void ClearSearchParentChildren() { Transform parent = friendSearchParent; if (parent == null) { return; } for (int index = parent.childCount - 1; index >= 0; index--) { Transform child = parent.GetChild(index); if (child != null) { Destroy(child.gameObject); } } _spawnedSearchCard = null; } private void ClearRequestParentChildren() { Transform parent = RequestParentTransform; if (parent != null) { for (int index = parent.childCount - 1; index >= 0; index--) { Transform child = parent.GetChild(index); if (child != null) { Destroy(child.gameObject); } } } _spawnedRequestCards.Clear(); } private void SetSearchDefaultVisible(bool visible) { if (defaultFriendSearchText != null) { defaultFriendSearchText.gameObject.SetActive(visible); } } private void SetRequestDefaultVisible(bool visible) { if (defaultFriendRequestText != null) { defaultFriendRequestText.gameObject.SetActive(visible); } } }