using System; using System.Collections.Generic; using BrewMonster.Scripts.Managers; using CSNetwork.Protocols.RPCData; using Cysharp.Threading.Tasks; using UnityEngine; namespace BrewMonster.Scripts { public class PlayerModelPreview : MonoSingleton { [SerializeField] private Transform modelRoot; [SerializeField] private bool enablePreviewCameraWithInventory = true; public List playerModels = new(); public List playerModelIds = new(); /// /// Fired (with the roleId) the first time a model becomes active after loading completes. /// Subscribe from HostPlayerPortraitCapture so it can attach the portrait camera at the /// right moment even when the async load finishes after AttachToPlayerModelPreview is called. /// public event Action OnModelReady; private int _loadVersion; private bool _hasRequestedPlayerModelId; private int _requestedPlayerModelId; private bool _inventoryPreviewActive; private bool _suppressHierarchyWatch; private bool _refreshQueued; private RoleInfo _lastPreviewRoleInfo; private Transform _lastOriginModelRoot; private int _lastOriginModelChildCount; private Camera _previewCamera; private void Awake() { if (modelRoot == null) modelRoot = transform; _previewCamera = GetComponent(); } private void OnDestroy() { ClearModelsImmediate(); } private void LateUpdate() { if (!_inventoryPreviewActive || _suppressHierarchyWatch) return; Transform activeRoot = GetActivePreviewModelTransform(); if (activeRoot == null) { if (_refreshQueued) { _refreshQueued = false; TryRefreshFromCachedRole(); } return; } int currentChildCount = CountAllChildren(activeRoot); if (_lastOriginModelRoot != activeRoot || _lastOriginModelChildCount != currentChildCount) { bool hierarchyChanged = _lastOriginModelRoot != null; _lastOriginModelRoot = activeRoot; _lastOriginModelChildCount = currentChildCount; if (hierarchyChanged && _lastPreviewRoleInfo != null) _refreshQueued = true; } if (_refreshQueued) { _refreshQueued = false; TryRefreshFromCachedRole(); } } /// Called when Inventory UI opens — shows the host preview model on shared Bootstrap camera. public void BeginInventoryPreview(int roleId) { _inventoryPreviewActive = true; ResetHierarchyWatch(); if (enablePreviewCameraWithInventory && _previewCamera != null) _previewCamera.enabled = true; ShowPlayerModel(roleId); } /// Called when Inventory UI closes — hides preview models. Camera stays enabled for login/character-select reuse. public void EndInventoryPreview() { _inventoryPreviewActive = false; _refreshQueued = false; _suppressHierarchyWatch = false; ResetHierarchyWatch(); HideAllPlayerModels(); } /// /// Loads one preview instance per role (via ). /// All instances stay hidden until is called. /// public async UniTask ShowAllPlayerModels(List roleInfos) { BMLogger.Log($"ShowAllPlayerModels: {roleInfos.Count}"); _loadVersion++; int version = _loadVersion; if (roleInfos == null || roleInfos.Count == 0 || NPCManager.Instance == null) { ClearModels(); _suppressHierarchyWatch = false; return false; } List oldModels = new List(playerModels); playerModels.Clear(); playerModelIds.Clear(); for (int i = 0; i < roleInfos.Count; i++) { RoleInfo role = roleInfos[i]; GameObject model = await LoadPlayerModel(role); if (model == null) continue; if (version != _loadVersion) { Destroy(model); return false; } model.transform.SetParent(modelRoot, false); model.transform.localPosition = Vector3.zero; model.transform.localRotation = Quaternion.identity; model.SetActive(false); playerModels.Add(model); playerModelIds.Add(role.roleid); ApplyRequestedModelVisibility(); } for (int i = 0; i < oldModels.Count; i++) { if (oldModels[i] != null) { Destroy(oldModels[i]); } } _suppressHierarchyWatch = false; if (_inventoryPreviewActive) SyncHierarchyBaseline(); return true; } /// /// Shows the preview for the given role id (matches ). Hides all others. /// public void ShowPlayerModel(int playerModelId) { _hasRequestedPlayerModelId = true; _requestedPlayerModelId = playerModelId; ApplyRequestedModelVisibility(); } /// /// Applies the visibility of player models based on the requested player model id. /// Fires the first time a model transitions to active so that /// portrait capture systems can attach at the correct moment after async loading. /// private void ApplyRequestedModelVisibility() { if (!_hasRequestedPlayerModelId) return; int n = Mathf.Min(playerModels.Count, playerModelIds.Count); for (int i = 0; i < n; i++) { GameObject go = playerModels[i]; if (go == null) continue; bool shouldBeActive = playerModelIds[i] == _requestedPlayerModelId; bool wasActive = go.activeSelf; go.SetActive(shouldBeActive); if (shouldBeActive && !wasActive) OnModelReady?.Invoke(playerModelIds[i]); } if (_inventoryPreviewActive) SyncHierarchyBaseline(); } public void HideAllPlayerModels() { _hasRequestedPlayerModelId = false; for (int i = 0; i < playerModels.Count; i++) { if (playerModels[i] != null) { playerModels[i].SetActive(false); } } } public void ClearModels() { for (int i = 0; i < playerModels.Count; i++) { if (playerModels[i] != null) { Destroy(playerModels[i]); } } playerModels.Clear(); playerModelIds.Clear(); } private void ClearModelsImmediate() { for (int i = 0; i < playerModels.Count; i++) { if (modelRoot != null) { if (playerModels[i] != null) { if (Application.isPlaying) Destroy(playerModels[i]); else DestroyImmediate(playerModels[i]); } } } playerModels.Clear(); playerModelIds.Clear(); } private async UniTask LoadPlayerModel(RoleInfo role) { var elemendataman = ElementDataManProvider.GetElementDataMan(); GameObject model = await NPCManager.Instance.GetModelPlayer(role.occupation, role.gender); if (model == null) return null; model.SetActive(false); if (modelRoot != null) { model.transform.SetParent(modelRoot, false); } var playerDefaultEquipments = model.GetComponentInChildren(); if (playerDefaultEquipments == null) { return null; } DATA_TYPE DataType = default; bool useDefaultUpper = true; bool useDefaultLower = true; bool useDefaultWrist = true; bool useDefaultFoot = true; GRoleInventory equipment; for (int i = 0; i < role.equipment.Count; i++) { equipment = role.equipment[i]; var equipData = elemendataman.get_data_ptr((uint)equipment.id, ID_SPACE.ID_SPACE_ESSENCE, ref DataType); switch (DataType) { case DATA_TYPE.DT_WEAPON_ESSENCE: if (equipData == null) break; var weaponData = (WEAPON_ESSENCE)equipData; string fileModelRight = AFile.NormalizePath(weaponData.FileModelRight, true).ToLower(); string fileModelLeft = AFile.NormalizePath(weaponData.FileModelLeft, true).ToLower(); GameObject weaponPrefab = null; if (!string.IsNullOrEmpty(fileModelRight)) { weaponPrefab = await AddressableManager.Instance.LoadPrefabAsync(fileModelRight); var weaponObject = Instantiate(weaponPrefab); if (weaponObject != null) { weaponObject.transform.SetParent(FindChildObjectRecursive(model.transform, CECPlayer._hh_right_hand_weapon).transform); weaponObject.transform.localPosition = weaponPrefab.transform.localPosition; weaponObject.transform.localRotation = weaponPrefab.transform.localRotation; weaponObject.transform.localScale = Vector3.one; weaponObject.SetActive(true); } } if (!string.IsNullOrEmpty(fileModelLeft)) { weaponPrefab = await AddressableManager.Instance.LoadPrefabAsync(fileModelLeft); var weaponObject = Instantiate(weaponPrefab); if (weaponObject != null) { weaponObject.transform.SetParent(FindChildObjectRecursive(model.transform, CECPlayer._hh_left_hand_weapon).transform); weaponObject.transform.localPosition = weaponPrefab.transform.localPosition; weaponObject.transform.localRotation = weaponPrefab.transform.localRotation; weaponObject.transform.localScale = Vector3.one; weaponObject.SetActive(true); } } break; case DATA_TYPE.DT_ARMOR_ESSENCE: if (equipData == null) break; var pArmor = (ARMOR_ESSENCE)equipData; var nLocation = pArmor.equip_location; var armorSkinPath = CECPlayer._GenEquipmentSkinPath(role.occupation, role.gender, pArmor.RealName); if (!armorSkinPath.EndsWith(".ecm")) { armorSkinPath += ".ecm"; } var armorPrefab = await AddressableManager.Instance.LoadPrefabAsync(armorSkinPath); if (armorPrefab != null) { var armorObject = Instantiate(armorPrefab); armorObject.transform.SetParent(GetSkeletonBuilder(model)?.transform); armorObject.transform.localPosition = Vector3.zero; armorObject.transform.localRotation = Quaternion.identity; armorObject.transform.localScale = Vector3.one; var skinnedMeshRenderereFromDataList = armorObject.GetComponentsInChildren(); foreach (var skinnedMeshRenderereFromData in skinnedMeshRenderereFromDataList) { if (skinnedMeshRenderereFromData != null) { skinnedMeshRenderereFromData._skinnedMeshRenderer.bones = GetSkeletonBuilder(model).GetBones(skinnedMeshRenderereFromData.BoneNames); skinnedMeshRenderereFromData._skinnedMeshRenderer.rootBone = skinnedMeshRenderereFromData._skinnedMeshRenderer.bones[^1]; } } switch (nLocation) { case (uint)CECPlayer.SkinIndex.SKIN_UPPER_BODY_INDEX: useDefaultUpper = false; break; case (uint)CECPlayer.SkinIndex.SKIN_LOWER_INDEX: useDefaultLower = false; break; case (uint)CECPlayer.SkinIndex.SKIN_WRIST_INDEX: useDefaultWrist = false; break; case (uint)CECPlayer.SkinIndex.SKIN_FOOT_INDEX: useDefaultFoot = false; break; } } break; } switch (equipment.pos) { case InventoryConst.EQUIPIVTR_BODY: useDefaultUpper = false; break; case InventoryConst.EQUIPIVTR_LEG: useDefaultLower = false; break; case InventoryConst.EQUIPIVTR_WRIST: useDefaultWrist = false; break; case InventoryConst.EQUIPIVTR_FOOT: useDefaultFoot = false; break; } } playerDefaultEquipments.DefaultUpper.SetActive(useDefaultUpper); playerDefaultEquipments.DefaultLower.SetActive(useDefaultLower); playerDefaultEquipments.DefaultWirst.SetActive(useDefaultWrist); playerDefaultEquipments.DefaultFoot.SetActive(useDefaultFoot); return model; } private GameObject FindChildObjectRecursive(Transform parent, string name) { foreach (Transform child in parent) { if (child.name == name) { return child.gameObject; } var childObject = FindChildObjectRecursive(child, name); if (childObject != null) { return childObject; } } return null; } private SkeletonBuilder GetSkeletonBuilder(GameObject characterModel) { if (characterModel == null) { return null; } return characterModel.GetComponentInChildren(); } public void ReloadRoleModel(RoleInfo roleInfo) { if (roleInfo == null) return; _lastPreviewRoleInfo = roleInfo; _suppressHierarchyWatch = true; ShowAllPlayerModels(new List { roleInfo }).Forget(); ShowPlayerModel(roleInfo.roleid); } private Transform GetActivePreviewModelTransform() { if (!_hasRequestedPlayerModelId) return null; int n = Mathf.Min(playerModels.Count, playerModelIds.Count); for (int i = 0; i < n; i++) { GameObject model = playerModels[i]; if (model != null && model.activeSelf && playerModelIds[i] == _requestedPlayerModelId) return model.transform; } for (int i = 0; i < n; i++) { GameObject model = playerModels[i]; if (model != null && playerModelIds[i] == _requestedPlayerModelId) return model.transform; } return null; } private void TryRefreshFromCachedRole() { if (_lastPreviewRoleInfo == null || !_inventoryPreviewActive || _suppressHierarchyWatch) return; ReloadRoleModel(_lastPreviewRoleInfo); } private void SyncHierarchyBaseline() { Transform activeRoot = GetActivePreviewModelTransform(); if (activeRoot == null) return; _lastOriginModelRoot = activeRoot; _lastOriginModelChildCount = CountAllChildren(activeRoot); } private void ResetHierarchyWatch() { _lastOriginModelRoot = null; _lastOriginModelChildCount = 0; } private static int CountAllChildren(Transform root) { if (root == null) return 0; int count = root.childCount; for (int i = 0; i < root.childCount; i++) { count += CountAllChildren(root.GetChild(i)); } return count; } } }