Files
2026-07-20 22:19:25 +08:00

674 lines
19 KiB
C#

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<long, GameObject> _spawnedRequestCards = new Dictionary<long, GameObject>();
private readonly Dictionary<string, float> _actionCooldownUntil = new Dictionary<string, float>();
private readonly HashSet<string> _pendingActions = new HashSet<string>();
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<SocialFriendRequestEntry> 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<SocialFriendRequestEntry> 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<friendSearchPrefab>();
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<ProfileData> 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<ProfileData> SafeGetProfileBySteamId(NetworkManager network, string steamId)
{
try
{
return await network.GetProfile(steamId);
}
catch
{
return null;
}
}
private static async Task<ProfileData> 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<SocialFriendRequestEntry> requests)
{
Transform parent = RequestParentTransform;
if (parent == null || friendRequestPrefab == null)
{
SetRequestDefaultVisible(true);
return;
}
ClearRequestParentChildren();
List<SocialFriendRequestEntry> normalized = (requests ?? Array.Empty<SocialFriendRequestEntry>())
.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<friendRequestPrefab>();
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<long> 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<string> expiredKeys = null;
foreach (KeyValuePair<string, float> entry in _actionCooldownUntil)
{
if (_pendingActions.Contains(entry.Key) || entry.Value > now)
{
continue;
}
expiredKeys ??= new List<string>();
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);
}
}
}