500 lines
19 KiB
C#
500 lines
19 KiB
C#
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<PlayerModelPreview>
|
|
{
|
|
[SerializeField] private Transform modelRoot;
|
|
[SerializeField] private bool enablePreviewCameraWithInventory = true;
|
|
|
|
public List<GameObject> playerModels = new();
|
|
public List<int> playerModelIds = new();
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public event Action<int> 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<Camera>();
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <summary>Called when Inventory UI opens — shows the host preview model on shared Bootstrap camera.</summary>
|
|
public void BeginInventoryPreview(int roleId)
|
|
{
|
|
_inventoryPreviewActive = true;
|
|
ResetHierarchyWatch();
|
|
|
|
if (enablePreviewCameraWithInventory && _previewCamera != null)
|
|
_previewCamera.enabled = true;
|
|
|
|
ShowPlayerModel(roleId);
|
|
}
|
|
|
|
/// <summary>Called when Inventory UI closes — hides preview models. Camera stays enabled for login/character-select reuse.</summary>
|
|
public void EndInventoryPreview()
|
|
{
|
|
_inventoryPreviewActive = false;
|
|
_refreshQueued = false;
|
|
_suppressHierarchyWatch = false;
|
|
ResetHierarchyWatch();
|
|
|
|
HideAllPlayerModels();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads one preview instance per role (via <see cref="NPCManager.GetModelPlayer"/>).
|
|
/// All instances stay hidden until <see cref="ShowPlayerModel"/> is called.
|
|
/// </summary>
|
|
public async void ShowAllPlayerModels(List<RoleInfo> roleInfos)
|
|
{
|
|
BMLogger.Log($"ShowAllPlayerModels: {roleInfos.Count}");
|
|
_loadVersion++;
|
|
int version = _loadVersion;
|
|
|
|
if (roleInfos == null || roleInfos.Count == 0 || NPCManager.Instance == null)
|
|
{
|
|
ClearModels();
|
|
_suppressHierarchyWatch = false;
|
|
return;
|
|
}
|
|
|
|
List<GameObject> oldModels = new List<GameObject>(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;
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shows the preview for the given role id (matches <see cref="RoleInfo.roleid"/>). Hides all others.
|
|
/// </summary>
|
|
public void ShowPlayerModel(int playerModelId)
|
|
{
|
|
_hasRequestedPlayerModelId = true;
|
|
_requestedPlayerModelId = playerModelId;
|
|
|
|
ApplyRequestedModelVisibility();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies the visibility of player models based on the requested player model id.
|
|
/// Fires <see cref="OnModelReady"/> the first time a model transitions to active so that
|
|
/// portrait capture systems can attach at the correct moment after async loading.
|
|
/// </summary>
|
|
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<GameObject> 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<PlayerDefaultEquipments>();
|
|
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<SkinnedMeshRenderFromData>();
|
|
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<SkeletonBuilder>();
|
|
}
|
|
|
|
public void ReloadRoleModel(RoleInfo roleInfo)
|
|
{
|
|
if (roleInfo == null)
|
|
return;
|
|
|
|
_lastPreviewRoleInfo = roleInfo;
|
|
_suppressHierarchyWatch = true;
|
|
ShowAllPlayerModels(new List<RoleInfo> { roleInfo });
|
|
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;
|
|
}
|
|
}
|
|
}
|