using System; using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using UnityEngine; using UnityEngine.Networking; using UnityEngine.UI; #if UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX using Steamworks; #endif public class playerMessagePrefab : MonoBehaviour { [SerializeField] Image playerProfile; [SerializeField] Image playerTitleImage; [SerializeField] Text playerTitle; [SerializeField] Text playerName; [SerializeField] Text messageText; [SerializeField] Image messageBtm; private static readonly Dictionary AvatarCache = new Dictionary(StringComparer.Ordinal); private static readonly Dictionary> AvatarDownloads = new Dictionary>(StringComparer.Ordinal); private string _boundSteamId = string.Empty; private string _boundAvatarUrl = string.Empty; private static string AvatarCacheFolderPath { get { return Path.Combine(Application.persistentDataPath, "chat_avatar_cache"); } } [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void ResetAvatarRuntimeCaches() { AvatarCache.Clear(); AvatarDownloads.Clear(); try { if (Directory.Exists(AvatarCacheFolderPath)) { Directory.Delete(AvatarCacheFolderPath, true); } } catch (Exception ex) { Debug.LogWarning($"[playerMessagePrefab] Failed to clear avatar cache folder: {ex.Message}"); } } public void Bind(string steamId, string avatarUrl, string displayName, string title, string content, Color bubbleColor) { Bind(steamId, avatarUrl, displayName, string.Empty, title, content, bubbleColor); } public void Bind(string steamId, string avatarUrl, string displayName, string titleId, string title, string content, Color bubbleColor) { _boundSteamId = steamId ?? string.Empty; _boundAvatarUrl = avatarUrl ?? string.Empty; BindPlayerTitle(titleId, title); if (playerName != null) { playerName.text = displayName ?? string.Empty; } if (messageText != null) { messageText.text = content ?? string.Empty; } if (messageBtm != null) { messageBtm.color = bubbleColor; } if (playerProfile != null) { playerProfile.sprite = null; _ = RefreshAvatarAsync(_boundSteamId, _boundAvatarUrl); } } private void BindPlayerTitle(string titleId, string title) { EnsureTitleImageReference(); PlayerTitleResolver.ResolvedTitle resolvedTitle = PlayerTitleResolver.Resolve(titleId, title); bool hasTitleSprite = resolvedTitle.HasSprite; bool hasTitleText = resolvedTitle.HasText; if (playerTitleImage != null) { playerTitleImage.sprite = resolvedTitle.titleSprite; playerTitleImage.enabled = hasTitleSprite; playerTitleImage.gameObject.SetActive(hasTitleSprite); if (!hasTitleSprite) { playerTitleImage.sprite = null; } } if (playerTitle != null) { playerTitle.text = !hasTitleSprite && hasTitleText ? resolvedTitle.titleText : string.Empty; playerTitle.gameObject.SetActive(!hasTitleSprite && hasTitleText); } } private void EnsureTitleImageReference() { if (playerTitleImage != null) { return; } Transform[] children = GetComponentsInChildren(true); for (int i = 0; i < children.Length; i++) { Transform child = children[i]; if (child != null && child.name == "玩家称号image") { playerTitleImage = child.GetComponent(); return; } } } public static async Task WarmAvatarCacheAsync(string steamId, string avatarUrl) { if (string.IsNullOrWhiteSpace(steamId) && string.IsNullOrWhiteSpace(avatarUrl)) { return; } string normalizedAvatarUrl = GameServer.Client.NetworkManager.NormalizeAvatarUrlForClient(avatarUrl); await TryGetAvatarSpriteAsync(steamId, normalizedAvatarUrl); } public static bool TryGetCachedAvatarSprite(string steamId, string avatarUrl, out Sprite sprite) { sprite = null; string normalizedAvatarUrl = GameServer.Client.NetworkManager.NormalizeAvatarUrlForClient(avatarUrl); if (!string.IsNullOrWhiteSpace(normalizedAvatarUrl) && AvatarCache.TryGetValue(normalizedAvatarUrl, out Sprite cachedByUrl) && cachedByUrl != null) { sprite = cachedByUrl; return true; } if (!string.IsNullOrWhiteSpace(steamId) && AvatarCache.TryGetValue(steamId, out Sprite cachedBySteamId) && cachedBySteamId != null) { sprite = cachedBySteamId; return true; } Sprite diskSprite = TryLoadAvatarSpriteFromDisk(steamId, normalizedAvatarUrl); if (diskSprite != null) { CacheAvatarSpriteInMemory(steamId, normalizedAvatarUrl, diskSprite); sprite = diskSprite; return true; } return false; } private async Task RefreshAvatarAsync(string steamId, string avatarUrl) { const int maxAttempts = 20; for (int attempt = 0; attempt < maxAttempts; attempt++) { Sprite sprite = await TryGetAvatarSpriteAsync(steamId, avatarUrl); if (playerProfile == null) { return; } if (!string.Equals(_boundSteamId, steamId, StringComparison.Ordinal) || !string.Equals(_boundAvatarUrl, avatarUrl ?? string.Empty, StringComparison.Ordinal)) { return; } if (sprite != null) { playerProfile.sprite = sprite; return; } await Task.Delay(250); } } private static async Task TryGetAvatarSpriteAsync(string steamId, string avatarUrl) { Sprite diskSprite = TryLoadAvatarSpriteFromDisk(steamId, avatarUrl); if (diskSprite != null) { CacheAvatarSpriteInMemory(steamId, avatarUrl, diskSprite); return diskSprite; } string avatarSteamId = GameServer.Client.NetworkManager.ParseSteamAvatarUrl(avatarUrl); if (!string.IsNullOrWhiteSpace(avatarSteamId)) { return await TryGetSteamAvatarSpriteAsync(avatarSteamId); } if (!string.IsNullOrWhiteSpace(avatarUrl)) { Sprite remoteSprite = await TryGetRemoteAvatarSprite(avatarUrl); if (remoteSprite != null) { SaveAvatarSpriteToDisk(steamId, avatarUrl, remoteSprite); CacheAvatarSpriteInMemory(steamId, avatarUrl, remoteSprite); return remoteSprite; } } return await TryGetSteamAvatarSpriteAsync(steamId); } private static async Task TryGetRemoteAvatarSprite(string avatarUrl) { if (string.IsNullOrWhiteSpace(avatarUrl)) { return null; } if (AvatarCache.TryGetValue(avatarUrl, out Sprite cachedSprite) && cachedSprite != null) { return cachedSprite; } Task downloadTask; lock (AvatarDownloads) { if (!AvatarDownloads.TryGetValue(avatarUrl, out downloadTask)) { downloadTask = DownloadAvatarSpriteAsync(avatarUrl); AvatarDownloads[avatarUrl] = downloadTask; } } try { Sprite sprite = await downloadTask; if (sprite != null) { CacheAvatarSpriteInMemory(string.Empty, avatarUrl, sprite); } return sprite; } finally { lock (AvatarDownloads) { AvatarDownloads.Remove(avatarUrl); } } } private static async Task DownloadAvatarSpriteAsync(string avatarUrl) { using (UnityWebRequest request = UnityWebRequestTexture.GetTexture(avatarUrl)) { request.timeout = 15; await AwaitUnityOperationAsync(request.SendWebRequest()); if (request.result != UnityWebRequest.Result.Success) { Debug.LogWarning($"[playerMessagePrefab] Avatar download failed: url={avatarUrl} error={request.error}"); return null; } Texture2D texture = DownloadHandlerTexture.GetContent(request); if (texture == null) { return null; } return Sprite.Create(texture, new Rect(0f, 0f, texture.width, texture.height), new Vector2(0.5f, 0.5f)); } } private static Task AwaitUnityOperationAsync(AsyncOperation operation) { if (operation == null) { throw new ArgumentNullException(nameof(operation)); } if (operation.isDone) { return Task.CompletedTask; } var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); void Complete(AsyncOperation _) { operation.completed -= Complete; tcs.TrySetResult(true); } operation.completed += Complete; return tcs.Task; } private static async Task TryGetSteamAvatarSpriteAsync(string steamId) { #if !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX) return null; #else if (string.IsNullOrWhiteSpace(steamId) || !SteamManager.Initialized) { return null; } if (AvatarCache.TryGetValue(steamId, out Sprite cached) && cached != null) { return cached; } if (!ulong.TryParse(steamId, out ulong parsedSteamId)) { return null; } CSteamID targetSteamId = new CSteamID(parsedSteamId); SteamFriends.RequestUserInformation(targetSteamId, false); int imageId = -1; const int maxPollFrames = 60; for (int attempt = 0; attempt < maxPollFrames; attempt++) { imageId = SteamFriends.GetLargeFriendAvatar(targetSteamId); if (imageId <= 0) { imageId = SteamFriends.GetMediumFriendAvatar(targetSteamId); } if (imageId > 0) { break; } if (imageId == 0) { return null; } await Task.Yield(); } if (imageId <= 0) { return null; } if (!SteamUtils.GetImageSize(imageId, out uint width, out uint height) || width == 0 || height == 0) { return null; } byte[] imageBuffer = new byte[width * height * 4]; if (!SteamUtils.GetImageRGBA(imageId, imageBuffer, imageBuffer.Length)) { return null; } Texture2D texture = new Texture2D((int)width, (int)height, TextureFormat.RGBA32, false); texture.LoadRawTextureData(imageBuffer); texture.Apply(); Texture2D flipped = new Texture2D((int)width, (int)height, TextureFormat.RGBA32, false); for (int y = 0; y < (int)height; y++) { Color[] pixels = texture.GetPixels(0, y, (int)width, 1); flipped.SetPixels(0, (int)height - 1 - y, (int)width, 1, pixels); } flipped.Apply(); Sprite sprite = Sprite.Create(flipped, new Rect(0, 0, width, height), new Vector2(0.5f, 0.5f)); CacheAvatarSpriteInMemory(steamId, null, sprite); SaveAvatarSpriteToDisk(steamId, null, sprite); return sprite; #endif } private static void CacheAvatarSpriteInMemory(string steamId, string avatarUrl, Sprite sprite) { if (sprite == null) { return; } if (!string.IsNullOrWhiteSpace(avatarUrl)) { AvatarCache[avatarUrl] = sprite; } if (!string.IsNullOrWhiteSpace(steamId)) { AvatarCache[steamId] = sprite; } } private static Sprite TryLoadAvatarSpriteFromDisk(string steamId, string avatarUrl) { string diskPath = GetAvatarDiskCachePath(steamId, avatarUrl); if (string.IsNullOrWhiteSpace(diskPath) || !File.Exists(diskPath)) { return null; } try { byte[] bytes = File.ReadAllBytes(diskPath); if (bytes == null || bytes.Length == 0) { return null; } Texture2D texture = new Texture2D(2, 2, TextureFormat.RGBA32, false); if (!texture.LoadImage(bytes, false)) { UnityEngine.Object.Destroy(texture); return null; } return Sprite.Create(texture, new Rect(0f, 0f, texture.width, texture.height), new Vector2(0.5f, 0.5f)); } catch (Exception ex) { Debug.LogWarning($"[playerMessagePrefab] Failed to load cached avatar: {ex.Message}"); return null; } } private static void SaveAvatarSpriteToDisk(string steamId, string avatarUrl, Sprite sprite) { if (sprite == null) { return; } string diskPath = GetAvatarDiskCachePath(steamId, avatarUrl); if (string.IsNullOrWhiteSpace(diskPath)) { return; } try { Directory.CreateDirectory(AvatarCacheFolderPath); Texture2D readableTexture = ExtractTextureFromSprite(sprite); if (readableTexture == null) { return; } byte[] bytes = readableTexture.EncodeToPNG(); if (bytes == null || bytes.Length == 0) { return; } File.WriteAllBytes(diskPath, bytes); } catch (Exception ex) { Debug.LogWarning($"[playerMessagePrefab] Failed to save avatar cache: {ex.Message}"); } } private static Texture2D ExtractTextureFromSprite(Sprite sprite) { if (sprite == null) { return null; } Texture2D source = sprite.texture; if (source == null) { return null; } Rect rect = sprite.textureRect; Texture2D texture = new Texture2D((int)rect.width, (int)rect.height, TextureFormat.RGBA32, false); Color[] pixels = source.GetPixels((int)rect.x, (int)rect.y, (int)rect.width, (int)rect.height); texture.SetPixels(pixels); texture.Apply(); return texture; } private static string GetAvatarDiskCachePath(string steamId, string avatarUrl) { if (!string.IsNullOrWhiteSpace(steamId)) { return Path.Combine(AvatarCacheFolderPath, $"{steamId}.png"); } if (string.IsNullOrWhiteSpace(avatarUrl)) { return string.Empty; } string sanitized = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(avatarUrl)) .Replace('/', '_') .Replace('+', '-') .TrimEnd('='); return Path.Combine(AvatarCacheFolderPath, $"{sanitized}.png"); } }