using System; using System.Collections; using System.Collections.Generic; using System.Linq; using Bansonic; using GameServer.Client; using UnityEngine; using UnityEngine.UI; #if UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX using Steamworks; #endif public enum FriendFilterType { All, Online, Starred, Offline, GameFriends, SteamFriends } [Serializable] public sealed class FriendCardViewData { public string SteamId; public string DisplayName; public string AvatarUrl; public string PlayerTitleId; public string PlayerTitle; public string StatusText; public bool IsOnline; public bool IsStarred; public bool IsGameFriend; public bool IsSteamFriend; } public class friendSystem : MonoBehaviour { private static string LabelAllFriends => LocalizationService.Get("friends.filter.all", "全部好友"); private static string LabelOnlineFriends => LocalizationService.Get("friends.filter.online", "在线好友"); private static string LabelStarredFriends => LocalizationService.Get("friends.filter.starred", "星标好友"); private static string LabelOfflineFriends => LocalizationService.Get("friends.filter.offline", "离线好友"); private static string LabelGameFriends => LocalizationService.Get("friends.filter.game", "游戏好友"); private static string LabelSteamFriends => LocalizationService.Get("friends.filter.steam", "Steam好友"); private static string StatusOnline => LocalizationService.Get("friends.status.online", "在线"); private static string StatusOffline => LocalizationService.Get("friends.status.offline", "离线"); private static string StatusPlaying => LocalizationService.Get("friends.status.playing", "游戏中"); private static string StatusInRoom => LocalizationService.Get("friends.status.in_room", "房间中"); private static string NoticeFriendDetailsNotReady => LocalizationService.Get("friends.notice.details_not_ready", "好友详情面板尚未接入"); private static string NoticePrivateChatNotReady => LocalizationService.Get("friends.notice.private_chat_not_ready", "私聊面板尚未接入"); private static string DefaultStatusLoading => LocalizationService.Get("friends.default.loading", "正在拉取好友信息..."); private static string DefaultStatusEmpty => LocalizationService.Get("friends.default.empty", "你当前没有好友"); private static string DefaultStatusServiceUnavailable => LocalizationService.Get("friends.default.service_unavailable", "无法访问服务"); public GameObject friendCardPrefab; public Transform friendsDisplayContent; [Header("filter")] public Dropdown friendsDisplayDropdown; [Header("text")] public Text defaultText; public Text onlineFriendsCountText; [Header("sync")] [SerializeField] private float steamFriendResyncIntervalSeconds = 8f; private readonly Dictionary _mergedFriends = new Dictionary(StringComparer.Ordinal); private readonly Dictionary _steamFriendCache = new Dictionary(StringComparer.Ordinal); private readonly Dictionary _spawnedCards = new Dictionary(StringComparer.Ordinal); private readonly List _filters = new List { FriendFilterType.All, FriendFilterType.Online, FriendFilterType.Starred, FriendFilterType.Offline, FriendFilterType.GameFriends, FriendFilterType.SteamFriends }; private bool _dropdownInitialized; private bool _isRefreshing; private bool _lastRefreshFailed; private bool _hasResolvedFriendCounts; private Coroutine _resyncCoroutine; private bool _languageSubscribed; private bool _chatStateSubscribed; private static List _startupSteamFriendIds = new List(); [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void WarmSteamFriendIdsOnStartup() { _startupSteamFriendIds = ReadSteamFriendIdsOnly(); } private void OnEnable() { LocalizationService.EnsureInitialized(); ClearDisplayedFriends(); _hasResolvedFriendCounts = false; SetDefaultText(DefaultStatusLoading); UpdateOnlineFriendsCountText(); InitializeDropdown(); SubscribeLanguageChanged(); SubscribeChatStateChanged(); if (ArenaRoomService.Instance != null) { ArenaRoomService.Instance.OnFriendsListChanged += HandleFriendsListChanged; } StartCoroutine(InitializeFriendsRoutine()); if (_resyncCoroutine != null) { StopCoroutine(_resyncCoroutine); } _resyncCoroutine = StartCoroutine(SteamFriendResyncLoop()); } private void OnDisable() { UnsubscribeLanguageChanged(); UnsubscribeChatStateChanged(); if (ArenaRoomService.Instance != null) { ArenaRoomService.Instance.OnFriendsListChanged -= HandleFriendsListChanged; } if (_resyncCoroutine != null) { StopCoroutine(_resyncCoroutine); _resyncCoroutine = null; } if (friendsDisplayDropdown != null) { friendsDisplayDropdown.onValueChanged.RemoveListener(HandleFilterChanged); } } private void ClearDisplayedFriends() { _spawnedCards.Clear(); if (friendsDisplayContent == null) { return; } for (int index = friendsDisplayContent.childCount - 1; index >= 0; index--) { Transform child = friendsDisplayContent.GetChild(index); if (child != null) { Destroy(child.gameObject); } } } private IEnumerator InitializeFriendsRoutine() { if (_isRefreshing) { yield break; } _isRefreshing = true; _lastRefreshFailed = false; SetDefaultText(DefaultStatusLoading); RefreshSteamFriendCache(); float waitDeadline = Time.realtimeSinceStartup + 3f; while (NetworkManager.Instance == null && Time.realtimeSinceStartup < waitDeadline) { yield return null; } NetworkManager network = NetworkManager.Instance; List steamFriendIds = _steamFriendCache.Keys .Where(id => !string.IsNullOrWhiteSpace(id)) .Distinct(StringComparer.Ordinal) .ToList(); if (network != null) { var friendsTask = network.GetFriendsList(steamFriendIds); float timeoutAt = Time.realtimeSinceStartup + 20f; while (!friendsTask.IsCompleted && Time.realtimeSinceStartup < timeoutAt) { yield return null; } if (!friendsTask.IsCompleted) { _lastRefreshFailed = true; Debug.LogWarning("[friendSystem] GetFriendsList timed out."); } else if (friendsTask.Status == System.Threading.Tasks.TaskStatus.RanToCompletion && friendsTask.Result != null && friendsTask.Result.success) { _hasResolvedFriendCounts = true; ReplaceGameFriends(friendsTask.Result.friends); } else { _lastRefreshFailed = true; string error = friendsTask.IsFaulted ? friendsTask.Exception?.GetBaseException().Message : "friend list request failed"; Debug.LogWarning($"[friendSystem] Initialize friends failed: {error}"); } } else { _lastRefreshFailed = true; Debug.LogWarning("[friendSystem] NetworkManager is unavailable when initializing friends."); } Render(); _isRefreshing = false; } private IEnumerator SteamFriendResyncLoop() { float interval = Mathf.Max(3f, steamFriendResyncIntervalSeconds); var wait = new WaitForSecondsRealtime(interval); while (isActiveAndEnabled) { yield return wait; if (_isRefreshing) { continue; } RefreshSteamFriendCache(); if (!ShouldRetrySteamImport()) { continue; } yield return InitializeFriendsRoutine(); } } private void InitializeDropdown() { if (friendsDisplayDropdown == null) { return; } friendsDisplayDropdown.onValueChanged.RemoveListener(HandleFilterChanged); int selectedValue = Mathf.Clamp(friendsDisplayDropdown.value, 0, Mathf.Max(0, _filters.Count - 1)); friendsDisplayDropdown.ClearOptions(); friendsDisplayDropdown.AddOptions(_filters.Select(GetFilterLabel).ToList()); friendsDisplayDropdown.value = selectedValue; _dropdownInitialized = true; friendsDisplayDropdown.onValueChanged.AddListener(HandleFilterChanged); friendsDisplayDropdown.RefreshShownValue(); } private void HandleFilterChanged(int _) { Render(); } private void HandleFriendsListChanged(IReadOnlyList friends) { _lastRefreshFailed = false; _hasResolvedFriendCounts = true; RefreshSteamFriendCache(); ReplaceGameFriends(friends); Render(); } private void RefreshSteamFriendCache() { CacheSteamFriends(ReadSteamFriends().Concat(BuildStartupSteamFriendCards())); } private bool ShouldRetrySteamImport() { if (_steamFriendCache.Count == 0) { return false; } foreach (KeyValuePair pair in _steamFriendCache) { FriendCardViewData steamFriend = pair.Value; if (steamFriend == null || string.IsNullOrWhiteSpace(steamFriend.SteamId)) { continue; } if (_mergedFriends.ContainsKey(steamFriend.SteamId)) { continue; } if (steamFriend.IsOnline || string.Equals(steamFriend.StatusText, StatusPlaying, StringComparison.Ordinal)) { return true; } } return false; } private void CacheSteamFriends(IEnumerable steamFriends) { _steamFriendCache.Clear(); if (steamFriends == null) { return; } foreach (FriendCardViewData steamFriend in steamFriends) { if (steamFriend == null || string.IsNullOrWhiteSpace(steamFriend.SteamId)) { continue; } steamFriend.IsStarred = LoadStarredState(steamFriend.SteamId); _steamFriendCache[steamFriend.SteamId] = steamFriend; } } private void ReplaceGameFriends(IEnumerable gameFriends) { _mergedFriends.Clear(); if (gameFriends == null) { return; } foreach (SocialFriendEntry gameFriend in gameFriends) { if (gameFriend == null || string.IsNullOrWhiteSpace(gameFriend.friend_steam_id)) { continue; } string steamId = gameFriend.friend_steam_id.Trim(); _steamFriendCache.TryGetValue(steamId, out FriendCardViewData steamFriend); bool steamSaysPlaying = steamFriend != null && string.Equals(steamFriend.StatusText, StatusPlaying, StringComparison.Ordinal); bool mergedOnline = gameFriend.online || (steamFriend != null && steamFriend.IsOnline) || steamSaysPlaying; string mergedStatus = MapServerPresenceToStatusText(gameFriend.status, steamFriend, mergedOnline); FriendCardViewData merged = new FriendCardViewData { SteamId = steamId, DisplayName = ResolvePreferredDisplayName( steamFriend != null ? steamFriend.DisplayName : string.Empty, NetworkManager.ResolveDisplayName(gameFriend.friend_name, steamId, false), steamId), AvatarUrl = ResolvePreferredAvatarUrl( steamFriend != null ? steamFriend.AvatarUrl : string.Empty, NetworkManager.ResolveAvatarUrl(gameFriend.friend_avatar, steamId), steamId), PlayerTitleId = !string.IsNullOrWhiteSpace(gameFriend.player_title_id) ? gameFriend.player_title_id : (steamFriend != null ? steamFriend.PlayerTitleId : string.Empty), PlayerTitle = !string.IsNullOrWhiteSpace(gameFriend.player_title) ? gameFriend.player_title : (steamFriend != null ? steamFriend.PlayerTitle : string.Empty), StatusText = mergedStatus, IsOnline = mergedOnline, IsStarred = LoadStarredState(steamId), IsGameFriend = true, IsSteamFriend = steamFriend != null }; _mergedFriends[steamId] = merged; } } private async System.Threading.Tasks.Task HydrateRegisteredSteamFriendsAsync(NetworkManager network) { if (network == null || _steamFriendCache.Count == 0) { return; } foreach (KeyValuePair pair in _steamFriendCache) { string steamId = pair.Key; FriendCardViewData steamFriend = pair.Value; if (string.IsNullOrWhiteSpace(steamId) || _mergedFriends.ContainsKey(steamId)) { continue; } ProfileData profile; try { profile = await network.GetProfile(steamId); } catch { continue; } if (profile == null || string.IsNullOrWhiteSpace(profile.steam_id)) { continue; } bool steamSaysPlaying = steamFriend != null && string.Equals(steamFriend.StatusText, StatusPlaying, StringComparison.Ordinal); bool mergedOnline = steamFriend != null && steamFriend.IsOnline; string mergedStatus = steamSaysPlaying ? StatusPlaying : (mergedOnline ? StatusOnline : StatusOffline); _mergedFriends[steamId] = new FriendCardViewData { SteamId = steamId, DisplayName = ResolvePreferredDisplayName( steamFriend != null ? steamFriend.DisplayName : string.Empty, NetworkManager.ResolveDisplayName(profile.display_name, steamId, false), steamId), AvatarUrl = ResolvePreferredAvatarUrl( steamFriend != null ? steamFriend.AvatarUrl : string.Empty, NetworkManager.ResolveAvatarUrl(profile.avatar_url, steamId), steamId), PlayerTitleId = !string.IsNullOrWhiteSpace(profile.player_title_id) ? profile.player_title_id : (!string.IsNullOrWhiteSpace(profile.primary_honor?.honor_key) ? profile.primary_honor.honor_key : string.Empty), PlayerTitle = !string.IsNullOrWhiteSpace(profile.player_title) ? profile.player_title : (!string.IsNullOrWhiteSpace(profile.primary_honor?.honor_name) ? profile.primary_honor.honor_name : string.Empty), StatusText = mergedStatus, IsOnline = mergedOnline, IsStarred = LoadStarredState(steamId), IsGameFriend = true, IsSteamFriend = true }; } } private void Render() { if (friendsDisplayContent == null || friendCardPrefab == null) { SetDefaultText(DefaultStatusServiceUnavailable); UpdateOnlineFriendsCountText(); return; } List target = ApplyFilter(_mergedFriends.Values) .OrderBy(friend => GetSortBucket(friend)) .ThenBy(friend => friend.DisplayName ?? string.Empty, StringComparer.OrdinalIgnoreCase) .ThenBy(friend => friend.SteamId ?? string.Empty, StringComparer.OrdinalIgnoreCase) .ToList(); HashSet targetIds = new HashSet(target.Select(friend => friend.SteamId), StringComparer.Ordinal); foreach (KeyValuePair pair in _spawnedCards.ToList()) { if (targetIds.Contains(pair.Key)) { continue; } Destroy(pair.Value); _spawnedCards.Remove(pair.Key); } for (int index = 0; index < target.Count; index++) { FriendCardViewData friend = target[index]; bool created = false; if (!_spawnedCards.TryGetValue(friend.SteamId, out GameObject cardObject) || cardObject == null) { cardObject = Instantiate(friendCardPrefab, friendsDisplayContent); _spawnedCards[friend.SteamId] = cardObject; created = true; } cardObject.transform.SetSiblingIndex(index); friendCardPrefab card = cardObject.GetComponent(); if (card != null) { card.Bind(friend, HandleStarChanged, HandleOpenDetails, HandleOpenPrivateConversation); } if (created) { UI_OrderedEntryAnimator.PlayFadeOnly(cardObject, index, 0.16f, 0.012f, true); } } UpdateDefaultTextState(target.Count); UpdateOnlineFriendsCountText(); } private void UpdateDefaultTextState(int visibleCount) { if (_lastRefreshFailed) { SetDefaultText(DefaultStatusServiceUnavailable); return; } if (visibleCount > 0) { SetDefaultText(string.Empty); return; } SetDefaultText(DefaultStatusEmpty); } private void SetDefaultText(string message) { if (defaultText == null) { return; } defaultText.text = message ?? string.Empty; } private void UpdateOnlineFriendsCountText() { if (onlineFriendsCountText == null) { return; } if (!_hasResolvedFriendCounts) { onlineFriendsCountText.text = "-/-"; return; } int totalCount = _mergedFriends.Count; int onlineCount = 0; foreach (FriendCardViewData friend in _mergedFriends.Values) { if (friend != null && friend.IsOnline) { onlineCount++; } } onlineFriendsCountText.text = $"{onlineCount}/{totalCount}"; } private IEnumerable ApplyFilter(IEnumerable source) { FriendFilterType filter = GetSelectedFilter(); switch (filter) { case FriendFilterType.All: return source; case FriendFilterType.Online: return source.Where(friend => friend.IsOnline); case FriendFilterType.Starred: return source.Where(friend => friend.IsStarred); case FriendFilterType.Offline: return source.Where(friend => !friend.IsOnline); case FriendFilterType.GameFriends: return source.Where(friend => friend.IsGameFriend); case FriendFilterType.SteamFriends: return source.Where(friend => friend.IsSteamFriend); default: return source; } } private FriendFilterType GetSelectedFilter() { if (friendsDisplayDropdown == null || friendsDisplayDropdown.value < 0 || friendsDisplayDropdown.value >= _filters.Count) { return FriendFilterType.All; } return _filters[friendsDisplayDropdown.value]; } private void HandleStarChanged(string steamId, bool isOn) { if (string.IsNullOrWhiteSpace(steamId)) { return; } PlayerPrefs.SetInt(BuildStarKey(steamId), isOn ? 1 : 0); PlayerPrefs.Save(); if (_mergedFriends.TryGetValue(steamId, out FriendCardViewData friend) && friend != null) { friend.IsStarred = isOn; } if (_steamFriendCache.TryGetValue(steamId, out FriendCardViewData steamFriend) && steamFriend != null) { steamFriend.IsStarred = isOn; } Render(); } private void HandleOpenDetails(string steamId) { if (string.IsNullOrWhiteSpace(steamId)) { return; } gNotice.warning.display(NoticeFriendDetailsNotReady); } private void HandleOpenPrivateConversation(string steamId) { if (string.IsNullOrWhiteSpace(steamId)) { return; } globalChatSystem.OpenPrivateConversation(steamId); } private void SubscribeChatStateChanged() { if (_chatStateSubscribed) { return; } globalChatSystem.ActivePrivateConversationChanged += HandleActivePrivateConversationChanged; _chatStateSubscribed = true; } private void UnsubscribeChatStateChanged() { if (!_chatStateSubscribed) { return; } globalChatSystem.ActivePrivateConversationChanged -= HandleActivePrivateConversationChanged; _chatStateSubscribed = false; } private void HandleActivePrivateConversationChanged(string _) { if (!isActiveAndEnabled) { return; } Render(); } private void SubscribeLanguageChanged() { if (_languageSubscribed) { return; } LocalizationService.LanguageChanged += HandleLanguageChanged; _languageSubscribed = true; } private void UnsubscribeLanguageChanged() { if (!_languageSubscribed) { return; } LocalizationService.LanguageChanged -= HandleLanguageChanged; _languageSubscribed = false; } private void HandleLanguageChanged(string _) { if (!isActiveAndEnabled) { return; } InitializeDropdown(); Render(); } private static string GetFilterLabel(FriendFilterType filter) { switch (filter) { case FriendFilterType.Online: return LabelOnlineFriends; case FriendFilterType.Starred: return LabelStarredFriends; case FriendFilterType.Offline: return LabelOfflineFriends; case FriendFilterType.GameFriends: return LabelGameFriends; case FriendFilterType.SteamFriends: return LabelSteamFriends; default: return LabelAllFriends; } } private static int GetSortBucket(FriendCardViewData friend) { if (friend == null) { return int.MaxValue; } if (friend.IsStarred && friend.IsOnline) { return 0; } if (friend.IsStarred && !friend.IsOnline) { return 1; } if (!friend.IsStarred && friend.IsOnline) { return 2; } return 3; } private static string ResolvePreferredDisplayName(string currentDisplayName, string newDisplayName, string steamId) { if (NetworkManager.IsUsableDisplayName(newDisplayName, steamId)) { return newDisplayName; } if (NetworkManager.IsUsableDisplayName(currentDisplayName, steamId)) { return currentDisplayName; } return string.Empty; } private static string ResolvePreferredAvatarUrl(string currentAvatarUrl, string newAvatarUrl, string steamId) { if (NetworkManager.IsUsableAvatarUrl(newAvatarUrl)) { return newAvatarUrl; } if (NetworkManager.IsUsableAvatarUrl(currentAvatarUrl)) { return currentAvatarUrl; } return NetworkManager.ResolveAvatarUrl(string.Empty, steamId); } private static string BuildStarKey(string steamId) { return $"friend_star_{steamId}"; } private static bool LoadStarredState(string steamId) { if (string.IsNullOrWhiteSpace(steamId)) { return false; } return PlayerPrefs.GetInt(BuildStarKey(steamId), 0) == 1; } private static List ReadSteamFriends() { var result = new List(); #if !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX) return result; #else if (!SteamManager.Initialized) { return result; } int count = SteamFriends.GetFriendCount(EFriendFlags.k_EFriendFlagImmediate); for (int index = 0; index < count; index++) { CSteamID friendSteamId = SteamFriends.GetFriendByIndex(index, EFriendFlags.k_EFriendFlagImmediate); string steamId = friendSteamId.m_SteamID.ToString(); SteamFriends.RequestUserInformation(friendSteamId, false); string displayName = SteamFriends.GetFriendPersonaName(friendSteamId); EPersonaState personaState = SteamFriends.GetFriendPersonaState(friendSteamId); FriendGameInfo_t gameInfo; bool isPlaying = SteamFriends.GetFriendGamePlayed(friendSteamId, out gameInfo); bool isOnline = personaState != EPersonaState.k_EPersonaStateOffline; string statusText = isPlaying ? StatusPlaying : (isOnline ? StatusOnline : StatusOffline); result.Add( new FriendCardViewData { SteamId = steamId, DisplayName = NetworkManager.ResolveDisplayName(displayName, steamId, false), AvatarUrl = NetworkManager.ResolveAvatarUrl(string.Empty, steamId), StatusText = statusText, IsOnline = isOnline, IsSteamFriend = true, IsGameFriend = false, IsStarred = LoadStarredState(steamId) } ); } return result; #endif } private static IEnumerable BuildStartupSteamFriendCards() { if (_startupSteamFriendIds == null || _startupSteamFriendIds.Count == 0) { yield break; } foreach (string steamId in _startupSteamFriendIds) { if (string.IsNullOrWhiteSpace(steamId)) { continue; } yield return new FriendCardViewData { SteamId = steamId, DisplayName = NetworkManager.ResolveDisplayName(string.Empty, steamId, false), AvatarUrl = NetworkManager.ResolveAvatarUrl(string.Empty, steamId), StatusText = string.Empty, IsOnline = false, IsSteamFriend = true, IsGameFriend = false, IsStarred = LoadStarredState(steamId) }; } } private static List ReadSteamFriendIdsOnly() { var result = new List(); #if !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX) return result; #else if (!SteamManager.Initialized) { return result; } int count = SteamFriends.GetFriendCount(EFriendFlags.k_EFriendFlagImmediate); for (int index = 0; index < count; index++) { CSteamID friendSteamId = SteamFriends.GetFriendByIndex(index, EFriendFlags.k_EFriendFlagImmediate); string steamId = friendSteamId.m_SteamID.ToString(); if (!string.IsNullOrWhiteSpace(steamId)) { result.Add(steamId); } } return result.Distinct(StringComparer.Ordinal).ToList(); #endif } private static string MapServerPresenceToStatusText(string status, FriendCardViewData steamFriend, bool mergedOnline) { if (string.Equals(status, "IN_GAME", StringComparison.OrdinalIgnoreCase)) { return StatusPlaying; } if (string.Equals(status, "IN_ROOM", StringComparison.OrdinalIgnoreCase)) { return StatusInRoom; } if (string.Equals(status, "ONLINE", StringComparison.OrdinalIgnoreCase)) { return StatusOnline; } if (string.Equals(status, "OFFLINE", StringComparison.OrdinalIgnoreCase)) { return StatusOffline; } if (steamFriend != null && string.Equals(steamFriend.StatusText, StatusPlaying, StringComparison.Ordinal)) { return StatusPlaying; } return mergedOnline ? StatusOnline : StatusOffline; } }