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) #define FRIEND_DISABLE_STEAMWORKS #endif #if !FRIEND_DISABLE_STEAMWORKS 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 StatusText; public bool IsOnline; public bool IsStarred; public bool IsGameFriend; public bool IsSteamFriend; } public class friendSystem : MonoBehaviour { private const string LabelAllFriends = "全部好友"; private const string LabelOnlineFriends = "在线好友"; private const string LabelStarredFriends = "星标好友"; private const string LabelOfflineFriends = "离线好友"; private const string LabelGameFriends = "游戏好友"; private const string LabelSteamFriends = "Steam好友"; private const string StatusOnline = "在线"; private const string StatusOffline = "离线"; private const string StatusPlaying = "游戏中"; private const string StatusInRoom = "房间中"; private const string NoticeFriendDetailsNotReady = "好友详情面板尚未接入"; private const string NoticePrivateChatNotReady = "私聊面板尚未接入"; private const string DefaultStatusLoading = "正在拉取好友信息..."; private const string DefaultStatusEmpty = "你当前没有好友"; private const string DefaultStatusServiceUnavailable = "无法访问服务"; public GameObject friendCardPrefab; public Transform friendsDisplayContent; [Header("filter")] public Dropdown friendsDisplayDropdown; [Header("text")] public Text defaultText; [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<(FriendFilterType filter, string label)> _filters = new List<(FriendFilterType filter, string label)> { (FriendFilterType.All, LabelAllFriends), (FriendFilterType.Online, LabelOnlineFriends), (FriendFilterType.Starred, LabelStarredFriends), (FriendFilterType.Offline, LabelOfflineFriends), (FriendFilterType.GameFriends, LabelGameFriends), (FriendFilterType.SteamFriends, LabelSteamFriends) }; private bool _dropdownInitialized; private bool _isRefreshing; private bool _lastRefreshFailed; private Coroutine _resyncCoroutine; private static List _startupSteamFriendIds = new List(); [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void WarmSteamFriendIdsOnStartup() { _startupSteamFriendIds = ReadSteamFriendIdsOnly(); } private void OnEnable() { ClearDisplayedFriends(); SetDefaultText(DefaultStatusLoading); InitializeDropdown(); if (ArenaRoomService.Instance != null) { ArenaRoomService.Instance.OnFriendsListChanged += HandleFriendsListChanged; } StartCoroutine(InitializeFriendsRoutine()); if (_resyncCoroutine != null) { StopCoroutine(_resyncCoroutine); } _resyncCoroutine = StartCoroutine(SteamFriendResyncLoop()); } private void OnDisable() { 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) { 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); if (!_dropdownInitialized) { friendsDisplayDropdown.ClearOptions(); friendsDisplayDropdown.AddOptions(_filters.Select(item => item.label).ToList()); friendsDisplayDropdown.value = 0; _dropdownInitialized = true; } friendsDisplayDropdown.onValueChanged.AddListener(HandleFilterChanged); friendsDisplayDropdown.RefreshShownValue(); } private void HandleFilterChanged(int _) { Render(); } private void HandleFriendsListChanged(IReadOnlyList friends) { _lastRefreshFailed = false; 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), 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), StatusText = mergedStatus, IsOnline = mergedOnline, IsStarred = LoadStarredState(steamId), IsGameFriend = true, IsSteamFriend = true }; } } private void Render() { if (friendsDisplayContent == null || friendCardPrefab == null) { SetDefaultText(DefaultStatusServiceUnavailable); 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]; if (!_spawnedCards.TryGetValue(friend.SteamId, out GameObject cardObject) || cardObject == null) { cardObject = Instantiate(friendCardPrefab, friendsDisplayContent); _spawnedCards[friend.SteamId] = cardObject; } cardObject.transform.SetSiblingIndex(index); friendCardPrefab card = cardObject.GetComponent(); if (card != null) { card.Bind(friend, HandleStarChanged, HandleOpenDetails, HandleOpenPrivateConversation); } } UpdateDefaultTextState(target.Count); } 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 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].filter; } 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 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 FRIEND_DISABLE_STEAMWORKS 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 FRIEND_DISABLE_STEAMWORKS 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; } }