Files
2026-07-22 05:35:53 +08:00

522 lines
16 KiB
C#

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<string, Sprite> AvatarCache = new Dictionary<string, Sprite>(StringComparer.Ordinal);
private static readonly Dictionary<string, Task<Sprite>> AvatarDownloads = new Dictionary<string, Task<Sprite>>(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<Transform>(true);
for (int i = 0; i < children.Length; i++)
{
Transform child = children[i];
if (child != null && child.name == "玩家称号image")
{
playerTitleImage = child.GetComponent<Image>();
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<Sprite> 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<Sprite> TryGetRemoteAvatarSprite(string avatarUrl)
{
if (string.IsNullOrWhiteSpace(avatarUrl))
{
return null;
}
if (AvatarCache.TryGetValue(avatarUrl, out Sprite cachedSprite) && cachedSprite != null)
{
return cachedSprite;
}
Task<Sprite> 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<Sprite> 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<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
void Complete(AsyncOperation _)
{
operation.completed -= Complete;
tcs.TrySetResult(true);
}
operation.completed += Complete;
return tcs.Task;
}
private static async Task<Sprite> 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");
}
}