669 lines
18 KiB
C#
669 lines
18 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;
|
|
|
|
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);
|
|
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 ? "你们已经是好友" : 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("NetworkManager is not initialized");
|
|
}
|
|
|
|
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("好友服务未初始化");
|
|
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);
|
|
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("好友服务未初始化");
|
|
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("好友服务未初始化");
|
|
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("好友服务未初始化");
|
|
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);
|
|
}
|
|
}
|
|
}
|