using System; using System.Collections; using System.Collections.Generic; #if UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX using Steamworks; #endif using UnityEngine; using UnityEngine.UI; public class UI_Player : MonoBehaviour { private const string FirstLaunchDatePrefKey = "player.first_launch_date"; private const string SongResourcesPath = "song_songIndex"; private const string VersionLabel = "\u5185\u90e8\u6d4b\u8bd5v207b35"; private const string ChannelLabel = "Steam"; private const float MaxDisplayedRksForSlider = 100f; private static readonly Color SteamOnlineColor = new Color32(20, 120, 45, 255); [Header("prefab")] public GameObject pInfoPrefab; public Transform pInfoParent; [Header("so")] [SerializeField] private Player_SO pSO; [Header("uData")] public Image userProfileImage; public Text userNameText; public Text userIDText; public Text user_registDate; [Header("uSource")] public Text userSource; [Header("uRecord")] public Text fcAmount; public Text firstFCsong; public Text totalPlayTime; [Header("uLevel")] [SerializeField] private userLevel_skills_SO userLevelConfig; public Slider userLevelSlider; public Text userCurrentLevelText; public Text userNextLevelText; public Text userCurrentExpText; [Header("uRks")] public Slider uRksSlider; public Text uRksText; [Header("buttons")] public Button quitButton; private readonly List spawnedInfoEntries = new List(); private Color defaultInfoColor = Color.black; private Coroutine forceRefreshUiRoutine; private void Start() { BindQuitButton(); PlayerRksService.OnRksChanged += HandleRksChanged; PlayerExperienceLedger.EnsureInstance().OnExperienceChanged += HandlePlayerExperienceChanged; StartCoroutine(InitializeAsync()); } private void OnEnable() { BindQuitButton(); DisableBlockingBackgroundRaycasts(); RefreshRksUi(); RefreshLevelUi(); RestartForceRefreshUiRoutine(); } private void OnDestroy() { PlayerRksService.OnRksChanged -= HandleRksChanged; if (PlayerExperienceLedger.Instance != null) { PlayerExperienceLedger.Instance.OnExperienceChanged -= HandlePlayerExperienceChanged; } if (forceRefreshUiRoutine != null) { StopCoroutine(forceRefreshUiRoutine); forceRefreshUiRoutine = null; } if (quitButton != null) { quitButton.onClick.RemoveListener(ClosePanel); } } private void BindQuitButton() { if (quitButton == null) { return; } quitButton.onClick.RemoveListener(ClosePanel); quitButton.onClick.AddListener(ClosePanel); } private void DisableBlockingBackgroundRaycasts() { Image[] images = GetComponentsInChildren(true); for (int i = 0; i < images.Length; i++) { Image image = images[i]; if (image == null) { continue; } string name = image.gameObject.name; if (string.Equals(name, "btmImg (1)", StringComparison.OrdinalIgnoreCase) || string.Equals(name, "btmImg (2)", StringComparison.OrdinalIgnoreCase) || string.Equals(name, "btmImg", StringComparison.OrdinalIgnoreCase)) { image.raycastTarget = false; } } } private void ClosePanel() { gameObject.SetActive(false); } private void RestartForceRefreshUiRoutine() { if (!isActiveAndEnabled) { return; } if (forceRefreshUiRoutine != null) { StopCoroutine(forceRefreshUiRoutine); } forceRefreshUiRoutine = StartCoroutine(ForceRefreshUiDeferred()); } private IEnumerator ForceRefreshUiDeferred() { for (int i = 0; i < 3; i++) { yield return null; ForceRefreshUiLayoutNow(); } forceRefreshUiRoutine = null; } private void ForceRefreshUiLayoutNow() { Canvas.ForceUpdateCanvases(); Transform current = transform; int safety = 0; while (current != null && safety++ < 12) { RectTransform rect = current as RectTransform; if (rect != null) { LayoutRebuilder.ForceRebuildLayoutImmediate(rect); } current = current.parent; } Canvas.ForceUpdateCanvases(); } private IEnumerator InitializeAsync() { StoreOwnershipLedger.EnsureInstance().InitializeIfNeeded(); StoreOwnershipLedger.EnsureInstance().ForceSyncMirrorFlags(); PlayerRksService.EnsureLoaded(pSO); PlayerExperienceLedger.EnsureInstance().AttachPlayerData(pSO); string registerDate = EnsureFirstLaunchDate(); UserInfoSnapshot snapshot = BuildUserInfoSnapshot(registerDate); ApplyLegacyTextMirrors(snapshot); RebuildInfoEntries(snapshot); RefreshRksUi(); RefreshLevelUi(); yield return null; yield return StartCoroutine(LoadSteamProfileAsync(snapshot.isSteamOnline)); } private void HandleRksChanged(float _) { RefreshRksUi(); } private void HandlePlayerExperienceChanged(int _) { RefreshLevelUi(); } private void RefreshRksUi() { float currentRks = PlayerRksService.RefreshAndPersist(pSO); if (uRksText != null) { uRksText.text = currentRks.ToString("F2"); } if (uRksSlider != null) { uRksSlider.minValue = 0f; uRksSlider.maxValue = MaxDisplayedRksForSlider; uRksSlider.value = Mathf.Clamp(currentRks, 0f, MaxDisplayedRksForSlider); } } private void RefreshLevelUi() { if (userLevelSlider == null && userCurrentLevelText == null && userNextLevelText == null && userCurrentExpText == null) { return; } userLevel_skills_SO levelConfig = ResolveUserLevelConfig(); LevelUiState state = BuildLevelUiState(levelConfig, GetCurrentPlayerExperience()); if (userCurrentLevelText != null) { userCurrentLevelText.text = state.currentLevelText; } if (userNextLevelText != null) { userNextLevelText.text = state.nextLevelText; } if (userCurrentExpText != null) { userCurrentExpText.text = state.expText; } if (userLevelSlider != null) { userLevelSlider.minValue = 0f; userLevelSlider.maxValue = 1f; userLevelSlider.value = state.progress01; } } private int GetCurrentPlayerExperience() { if (Application.isPlaying) { return Mathf.Max(0, PlayerExperienceLedger.EnsureInstance().GetExperience()); } return pSO != null ? Mathf.Max(0, pSO.player_currentEXP) : 0; } private userLevel_skills_SO ResolveUserLevelConfig() { if (userLevelConfig != null) { return userLevelConfig; } uLevel_skills[] skillPanels = Resources.FindObjectsOfTypeAll(); for (int i = 0; i < skillPanels.Length; i++) { if (skillPanels[i] != null && skillPanels[i].ulsSO != null) { userLevelConfig = skillPanels[i].ulsSO; return userLevelConfig; } } userLevel_skills_SO[] loadedConfigs = Resources.FindObjectsOfTypeAll(); for (int i = 0; i < loadedConfigs.Length; i++) { if (loadedConfigs[i] != null) { userLevelConfig = loadedConfigs[i]; return userLevelConfig; } } #if UNITY_EDITOR if (userLevelConfig == null) { userLevelConfig = UnityEditor.AssetDatabase.LoadAssetAtPath( "Assets/playerInfoDisplay/uls/ulsSO.asset"); } #endif return userLevelConfig; } private static LevelUiState BuildLevelUiState(userLevel_skills_SO config, int currentExp) { currentExp = Mathf.Max(0, currentExp); if (config == null || config.skills == null || config.skills.Count == 0) { return new LevelUiState { currentLevelText = "0", nextLevelText = string.Empty, expText = "MAX", progress01 = 1f }; } List validLevels = new List(); for (int i = 0; i < config.skills.Count; i++) { if (config.skills[i] != null) { validLevels.Add(config.skills[i]); } } if (validLevels.Count == 0) { return new LevelUiState { currentLevelText = "0", nextLevelText = string.Empty, expText = "MAX", progress01 = 1f }; } validLevels.Sort((left, right) => { int expCompare = Mathf.Max(0, left.thisLevelNeedExps).CompareTo(Mathf.Max(0, right.thisLevelNeedExps)); if (expCompare != 0) { return expCompare; } return left.userLevel.CompareTo(right.userLevel); }); int currentIndex = 0; for (int i = 0; i < validLevels.Count; i++) { if (currentExp >= Mathf.Max(0, validLevels[i].thisLevelNeedExps)) { currentIndex = i; } else { break; } } userLevel_skills_SO.UserLevelSkillEntry currentLevel = validLevels[currentIndex]; bool isMaxLevel = currentIndex >= validLevels.Count - 1; int nextRequiredExp = isMaxLevel ? Mathf.Max(0, currentLevel.thisLevelNeedExps) : Mathf.Max(0, validLevels[currentIndex + 1].thisLevelNeedExps); float progress = isMaxLevel || nextRequiredExp <= 0 ? 1f : Mathf.Clamp01((float)currentExp / nextRequiredExp); return new LevelUiState { currentLevelText = Mathf.Max(0, currentLevel.userLevel).ToString(), nextLevelText = isMaxLevel ? string.Empty : Mathf.Max(0, validLevels[currentIndex + 1].userLevel).ToString(), expText = isMaxLevel ? "MAX" : $"{currentExp}/{nextRequiredExp}", progress01 = progress }; } private string EnsureFirstLaunchDate() { string playerPrefsDate = PlayerPrefs.GetString(FirstLaunchDatePrefKey, string.Empty); string finalDate = !string.IsNullOrWhiteSpace(playerPrefsDate) ? playerPrefsDate : DateTime.Now.ToString("yyyy-MM-dd"); if (playerPrefsDate != finalDate) { PlayerPrefs.SetString(FirstLaunchDatePrefKey, finalDate); PlayerPrefs.Save(); } if (pSO != null && pSO.firstLaunchDate != finalDate) { pSO.firstLaunchDate = finalDate; #if UNITY_EDITOR if (!Application.isPlaying) { UnityEditor.EditorUtility.SetDirty(pSO); } #endif } return finalDate; } private UserInfoSnapshot BuildUserInfoSnapshot(string registerDate) { SongStats songStats = CollectSongStats(); return new UserInfoSnapshot { registerDate = registerDate, ownedHeroCount = CountOwnedHeroes(), ownedSongCount = songStats.ownedSongCount, totalGameEnterCount = songStats.totalGameEnterCount, totalPlaySeconds = songStats.totalPlaySeconds, isSteamOnline = IsSteamOnline() }; } private void ApplyLegacyTextMirrors(UserInfoSnapshot snapshot) { if (user_registDate != null) { user_registDate.text = snapshot.registerDate; } if (userSource != null) { userSource.text = ChannelLabel; userSource.color = snapshot.isSteamOnline ? SteamOnlineColor : Color.black; } if (totalPlayTime != null) { totalPlayTime.text = FormatPlayTime(snapshot.totalPlaySeconds); } if (fcAmount != null && string.IsNullOrWhiteSpace(fcAmount.text)) { fcAmount.text = "-"; } if (firstFCsong != null && string.IsNullOrWhiteSpace(firstFCsong.text)) { firstFCsong.text = "-"; } } private void RebuildInfoEntries(UserInfoSnapshot snapshot) { ClearInfoEntries(); CacheDefaultInfoColor(); SpawnInfoEntry("\u6ce8\u518c\u65e5\u671f", snapshot.registerDate); SpawnInfoEntry("\u62e5\u6709\u89d2\u8272\u6570\u91cf", snapshot.ownedHeroCount.ToString()); SpawnInfoEntry("\u62e5\u6709\u6b4c\u66f2\u6570\u91cf", snapshot.ownedSongCount.ToString()); SpawnInfoEntry("\u603b\u6e38\u620f\u6b21\u6570", snapshot.totalGameEnterCount.ToString()); SpawnInfoEntry("\u6e38\u73a9\u603b\u65f6\u957f", FormatPlayTime(snapshot.totalPlaySeconds)); SpawnInfoEntry("\u6e20\u9053", ChannelLabel, snapshot.isSteamOnline ? SteamOnlineColor : defaultInfoColor); SpawnInfoEntry("\u7248\u672c", VersionLabel); } private void ClearInfoEntries() { for (int i = 0; i < spawnedInfoEntries.Count; i++) { if (spawnedInfoEntries[i] != null) { Destroy(spawnedInfoEntries[i]); } } spawnedInfoEntries.Clear(); } private void CacheDefaultInfoColor() { if (pInfoPrefab == null) { return; } pInfoPrefab sample = pInfoPrefab.GetComponent(); if (sample != null && sample.this_iDetail != null) { defaultInfoColor = sample.this_iDetail.color; } } private void SpawnInfoEntry(string title, string detail, Color? detailColor = null) { if (pInfoPrefab == null || pInfoParent == null) { return; } GameObject entryObject = Instantiate(pInfoPrefab, pInfoParent); spawnedInfoEntries.Add(entryObject); pInfoPrefab entry = entryObject.GetComponent(); if (entry == null) { return; } if (entry.this_iTitle != null) { entry.this_iTitle.text = title; } if (entry.this_iDetail != null) { entry.this_iDetail.text = detail; entry.this_iDetail.color = detailColor ?? defaultInfoColor; } } private static int CountOwnedHeroes() { AllyHero_SO[] loadedHeroes = RuntimeResourcesCache.LoadAllAllyHeroes(); HashSet uniqueHeroIds = new HashSet(); int ownedCount = 0; AllyHeroDeployLedger ledger = AllyHeroDeployLedger.EnsureInstance(); ledger.InitializeIfNeeded(); for (int i = 0; i < loadedHeroes.Length; i++) { AllyHero_SO hero = loadedHeroes[i]; if (hero == null || !uniqueHeroIds.Add(hero.ally_heroID)) { continue; } hero.ally_battleDeployCount = ledger.GetDeployCount(hero.ally_heroID); if (hero.isUnlocked) { ownedCount++; } } return ownedCount; } private static SongStats CollectSongStats() { SongData[] songs = RuntimeResourcesCache.LoadSongsFromPath(SongResourcesPath); HashSet uniqueSongIds = new HashSet(); SongStats stats = new SongStats(); for (int i = 0; i < songs.Length; i++) { SongData song = songs[i]; if (song == null || !uniqueSongIds.Add(song.songID)) { continue; } song.EnsurePersistentDataLoaded(); if (song.isUnlocked) { stats.ownedSongCount++; } stats.totalGameEnterCount += Mathf.Max(0, song.game_enterTimes); stats.totalPlaySeconds += Math.Max(0f, song.time_totalPlayingTime); } return stats; } private static string FormatPlayTime(double totalSeconds) { TimeSpan span = TimeSpan.FromSeconds(Math.Max(0d, totalSeconds)); if (span.TotalHours >= 1d) { return string.Format("{0:D2}\u5c0f\u65f6{1:D2}\u5206\u949f", (int)span.TotalHours, span.Minutes); } if (span.TotalMinutes >= 1d) { return string.Format("{0:D2}\u5206\u949f{1:D2}\u79d2", (int)span.TotalMinutes, span.Seconds); } return string.Format("{0:D2}\u79d2", Math.Max(0, span.Seconds)); } private IEnumerator LoadSteamProfileAsync(bool isSteamOnline) { #if !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX) // 非 Steam 平台(如 Android):无 Steam 资料,直接显示不可用。 ApplySteamUnavailable(); yield break; #else int initRetry = 0; while (initRetry < 25 && !SteamManager.Initialized) { initRetry++; yield return new WaitForSecondsRealtime(0.2f); } if (!SteamManager.Initialized) { ApplySteamUnavailable(); yield break; } int retryCount = 0; while (retryCount < 20) { if (TryPopulateSteamProfile(isSteamOnline)) { yield break; } retryCount++; yield return new WaitForSecondsRealtime(0.25f); } ApplySteamUnavailable(); #endif } private void ApplySteamUnavailable() { if (userNameText != null) { userNameText.text = "Steam Unavailable"; } if (userIDText != null) { userIDText.text = "-"; } if (userSource != null) { userSource.text = ChannelLabel; userSource.color = Color.black; } } private static bool IsSteamOnline() { #if !(UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX) return false; #else if (!SteamManager.Initialized) { return false; } try { return SteamFriends.GetPersonaState() != EPersonaState.k_EPersonaStateOffline; } catch { return false; } #endif } #if UNITY_STANDALONE_WIN || UNITY_STANDALONE_LINUX || UNITY_STANDALONE_OSX || STEAMWORKS_WIN || STEAMWORKS_LIN_OSX private bool TryPopulateSteamProfile(bool isSteamOnline) { try { CSteamID steamId = SteamUser.GetSteamID(); string personaName = SteamFriends.GetPersonaName(); if (!steamId.IsValid() || string.IsNullOrWhiteSpace(personaName)) { return false; } if (userNameText != null) { userNameText.text = personaName; } if (userIDText != null) { userIDText.text = steamId.m_SteamID.ToString(); } if (userSource != null) { userSource.text = ChannelLabel; userSource.color = isSteamOnline ? SteamOnlineColor : Color.black; } return TryApplySteamAvatar(steamId) || userProfileImage == null; } catch { return false; } } private bool TryApplySteamAvatar(CSteamID steamId) { if (userProfileImage == null) { return true; } int imageId = SteamFriends.GetLargeFriendAvatar(steamId); if (imageId == -1) { return false; } if (imageId == 0) { imageId = SteamFriends.GetMediumFriendAvatar(steamId); } if (imageId < 0) { return false; } uint width; uint height; if (!SteamUtils.GetImageSize(imageId, out width, out height) || width == 0 || height == 0) { return false; } byte[] imageBuffer = new byte[width * height * 4]; if (!SteamUtils.GetImageRGBA(imageId, imageBuffer, imageBuffer.Length)) { return false; } Texture2D sourceTexture = new Texture2D((int)width, (int)height, TextureFormat.RGBA32, false); sourceTexture.LoadRawTextureData(imageBuffer); sourceTexture.Apply(); Texture2D flippedTexture = new Texture2D((int)width, (int)height, TextureFormat.RGBA32, false); for (int y = 0; y < (int)height; y++) { Color[] rowPixels = sourceTexture.GetPixels(0, y, (int)width, 1); flippedTexture.SetPixels(0, (int)height - 1 - y, (int)width, 1, rowPixels); } flippedTexture.Apply(); userProfileImage.sprite = Sprite.Create( flippedTexture, new Rect(0, 0, flippedTexture.width, flippedTexture.height), new Vector2(0.5f, 0.5f)); return true; } #endif private struct UserInfoSnapshot { public string registerDate; public int ownedHeroCount; public int ownedSongCount; public int totalGameEnterCount; public double totalPlaySeconds; public bool isSteamOnline; } private struct SongStats { public int ownedSongCount; public int totalGameEnterCount; public double totalPlaySeconds; } private struct LevelUiState { public string currentLevelText; public string nextLevelText; public string expText; public float progress01; } }