using System.Reflection; using System.Threading.Tasks; using BrewMonster; using BrewMonster.Network; using BrewMonster.Scripts; using CSNetwork.GPDataType; using ModelRenderer.Scripts.Common; using PerfectWorld.Scripts.Managers; using TMPro; using UnityEngine; namespace PerfectWorld.Scripts { public class CECMatter : CECObject { private static Mesh s_itemNameQuadMesh; private static Material s_itemNameBgMaterial; // Matter information got from server public struct INFO { public int mid; // Matter id public int tid; // Template id } protected EC_ManMatter m_pMatterMan; protected INFO m_MatterInfo; protected int m_iLevelReq; protected float m_fGatherDist; protected uint m_dwMatterType; // Matter type flags / Matter type flags private bool m_registeredToManMatter = false; // Matter type constants / Matter type constants public const uint MATTER_UNKNOWN = 0; public const uint MATTER_ITEM = 1; public const uint MATTER_MINE = 2; public const uint MATTER_MONEY = 3; public const uint MATTER_TYPEMASK = 0xff; private const float MatterNameExtraWorldYOffset = 0.05f; private const float MatterNameFallbackLocalY = 0.6f; private const string ItemNameTextChildName = "ItemNameText"; // Constructor / Constructor public CECMatter() { m_iCID = Class_ID.OCID_MATTER; } // Awake is called when the component is initialized / Awake is called when the component is initialized private void Awake() { m_iCID = Class_ID.OCID_MATTER; // Ensure CID is set after Unity initialization / Ensure CID is set after Unity initialization TryRegisterToManMatter(); } private void OnEnable() { TryRegisterToManMatter(); } private void TryRegisterToManMatter() { if (m_registeredToManMatter) return; // Only register once we have a valid mid. int mid = GetMatterID(); if (mid == 0) return; var mono = BrewMonster.Managers.EC_ManMessageMono.Instance; if (mono == null || mono.EC_ManMatter == null) return; mono.EC_ManMatter.RegisterExistingMatter(this); m_registeredToManMatter = true; } // Override SetUpCECObject to set class ID / Override SetUpCECObject to set class ID public override void SetUpCECObject() { base.SetUpCECObject(); m_iCID = Class_ID.OCID_MATTER; // Set after base call to override the reset / Set after base call to override the reset } public void SetMatterInfo(INFO matterInfo) { m_MatterInfo = matterInfo; } public int GetTemplateID() { return m_MatterInfo.tid; } public int GetLevelReq() { return m_iLevelReq; } public float GetGatherDist() { // 采集/拾取距离:某些情况下(例如场景预制体未走 Init 流程)m_fGatherDist 可能为 0。 // Gather/pickup distance: in some cases (e.g. scene prefab not created via Init) m_fGatherDist may be 0. // Provide a safe default consistent with native behavior. return m_fGatherDist > 0.01f ? m_fGatherDist : 3.0f; } // Is this matter a mine? / Is this matter a mine? public bool IsMine() { return (m_dwMatterType & MATTER_TYPEMASK) == MATTER_MINE; } // Is this matter an item? / Is this matter an item? public bool IsItem() { return (m_dwMatterType & MATTER_TYPEMASK) == MATTER_ITEM; } // Is this matter money? / Is this matter money? public bool IsMoney() { return (m_dwMatterType & MATTER_TYPEMASK) == MATTER_MONEY; } public static async Task Init(info_matter Info) { INFO matterInfo = new INFO(); matterInfo.mid = Info.mid; matterInfo.tid = Info.tid & 0x0000ffff; // get the matter template from elementdataman DATA_TYPE DataType = DATA_TYPE.DT_INVALID; var matterData = ElementDataManProvider.GetElementDataMan().get_data_ptr((uint)matterInfo.tid, ID_SPACE.ID_SPACE_ESSENCE, ref DataType); // Determine matter type based on DataType / Determine matter type based on DataType uint dwMatterType = MATTER_UNKNOWN; if (DataType == DATA_TYPE.DT_MINE_ESSENCE) { dwMatterType = MATTER_MINE; } else { // Default to ITEM for other essence types / Default to ITEM for other essence types dwMatterType = MATTER_ITEM; } // 钱币掉落 / Money drop // 服务器用特殊 tid 表示金币/银币拾取物;不应占用背包格子。 // The server uses a special tid to represent money matters; they should not consume inventory slots. if (GPDataTypeHelper.ISMONEYTID(matterInfo.tid)) { dwMatterType = MATTER_MONEY; } if (matterData != null) { var matterDataType = matterData.GetType(); var fileMatterField = matterDataType.GetField("file_matter", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (fileMatterField == null) { fileMatterField = matterDataType.GetField("file_model", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); } if (fileMatterField != null) { var fileMatterValue = fileMatterField.GetValue(matterData); string filePath = ByteToStringUtils.ByteArrayToCP936String((byte[])fileMatterValue); //var matterPrefab = await AddressableManager.Instance.LoadPrefabAsync(AFile.NormalizePath(filePath.ToLower(), true)); var matterObject = await PoolManager.Instance.SpawnAsync(AFile.NormalizePath(filePath.ToLower(), true), Vector3.zero, Quaternion.identity, memoryReleaseTTL: 0f); if (matterObject != null) { //var matterObject = Instantiate(matterPrefab); matterObject.transform.SetParent(ObjectSpawner.Instance.transform); matterObject.name = $"Matter {matterObject.name} {matterInfo.tid} {matterInfo.mid}"; matterObject.transform.position = new Vector3(Info.pos.x, Info.pos.y, Info.pos.z); matterObject.transform.localScale = new Vector3(1f, 1f, 1f); // use same rotation as Prefab // matterObject.transform.localRotation = Quaternion.identity; matterObject.SetActive(true); // Add a collider if it doesn't have one if (matterObject.GetComponent() == null) { var collider = matterObject.AddComponent(); //this is a workaround to fix the collider size issue when load prefab go wrong at some point //TODO: remove this workaround after the prefab load issue is fixed if (TryGetCombinedRendererBounds(matterObject.transform, null, out var combinedBounds)) { Vector3 size = combinedBounds.size; if (size.x < 0.5f) size.x = 0.5f; if (size.y < 0.5f) size.y = 0.5f; if (size.z < 0.5f) size.z = 0.5f; collider.size = size; collider.center = matterObject.transform.InverseTransformPoint(combinedBounds.center); } else { var firstRenderer = matterObject.GetComponentInChildren(); Vector3 size = firstRenderer != null ? firstRenderer.bounds.size : Vector3.one; if (size.x < 0.5f) size.x = 0.5f; if (size.y < 0.5f) size.y = 0.5f; if (size.z < 0.5f) size.z = 0.5f; collider.size = size; } } // Create text object to display item name above the cube CreateItemNameText(matterObject, Info.tid); // Add a script to handle click events // MatterCubeClickHandler clickHandler = matterObject.AddComponent(); // clickHandler.Initialize(Info.mid, this); CECMatter matterScript = matterObject.AddComponent(); // Set CID immediately after AddComponent (before SetUpCECObject resets it) / Set CID immediately after AddComponent (before SetUpCECObject resets it) matterScript.m_iCID = Class_ID.OCID_MATTER; matterScript.SetMatterInfo(matterInfo); matterScript.m_dwMatterType = dwMatterType; // Set matter type / Set matter type // Set level requirement and gather distance for mines / Set level requirement and gather distance for mines if (dwMatterType == MATTER_MINE && DataType == DATA_TYPE.DT_MINE_ESSENCE) { var levelReqField = matterData.GetType().GetField("level_required", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (levelReqField != null) { matterScript.m_iLevelReq = (int)levelReqField.GetValue(matterData); } var gatherDistField = matterData.GetType().GetField("gather_dist", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (gatherDistField != null) { float gatherDist = (float)gatherDistField.GetValue(matterData); matterScript.m_fGatherDist = gatherDist > 3.0f ? gatherDist - 1.0f : 3.0f; } else { matterScript.m_fGatherDist = 3.0f; } } else { // For non-mine items, set a default pickup distance / For non-mine items, set a default pickup distance matterScript.m_fGatherDist = 3.0f; // Default pickup distance / Default pickup distance } matterScript.SetUpCECObject(); // This will reset m_iCID, so we set it again after / This will reset m_iCID, so we set it again after // Force set CID again after SetUpCECObject (which resets it to OCID_OBJECT) / Force set CID again after SetUpCECObject (which resets it to OCID_OBJECT) matterScript.m_iCID = Class_ID.OCID_MATTER; // Store reference to the cube // matterGameObjects[info.mid] = matterObject; return matterScript; } else { Debug.LogWarning($"Failed to load matter prefab from path: {filePath}"); } } else { Debug.LogWarning($"file_matter field not found on matter data type {matterDataType.FullName}"); } } return null; } /// /// Merge world-space bounds of all child Renderers (MeshRenderer + SkinnedMeshRenderer). /// Reads sharedMesh.bounds (mesh local space) and manually converts to world space — /// same approach as PlayerVisual/NPCVisual — to avoid stale renderer.bounds after SetActive. /// 合并所有子 Renderer 的世界包围盒(MeshRenderer + SkinnedMeshRenderer)。 /// 直接读 sharedMesh.bounds(网格本地空间)再手动转为世界坐标,避免 SetActive 后同帧 renderer.bounds 未刷新的问题。 /// private static bool TryGetCombinedRendererBounds(Transform matterRoot, Transform excludeSubtree, out Bounds combinedBounds) { combinedBounds = default; if (matterRoot == null) return false; var renderers = matterRoot.GetComponentsInChildren(true); bool hasAny = false; for (int i = 0; i < renderers.Length; i++) { var renderer = renderers[i]; if (renderer == null) continue; if (excludeSubtree != null && renderer.transform.IsChildOf(excludeSubtree)) continue; Mesh mesh = null; if (renderer is SkinnedMeshRenderer smr) { mesh = smr.sharedMesh; } else if (renderer is MeshRenderer) { var mf = renderer.GetComponent(); if (mf != null) mesh = mf.sharedMesh; } if (mesh == null) continue; // Manually build world-space bounds from mesh-local bounds + transform, // identical to PlayerVisual/NPCVisual — reliable even right after SetActive(true). // 与 PlayerVisual/NPCVisual 相同:从网格本地包围盒手动计算世界包围盒,SetActive 后同帧可靠。 var meshBounds = mesh.bounds; var scale = renderer.transform.lossyScale; var worldCenter = renderer.transform.TransformPoint(meshBounds.center); var worldSize = new Vector3( Mathf.Abs(meshBounds.size.x * scale.x), Mathf.Abs(meshBounds.size.y * scale.y), Mathf.Abs(meshBounds.size.z * scale.z)); var currentBounds = new Bounds(worldCenter, worldSize); BMLogger.Log($"[Cuong] [CECMatter] renderer={renderer.name} meshLocalCenter={meshBounds.center} meshLocalSize={meshBounds.size} worldCenter={worldCenter} worldSize={worldSize} worldMaxY={currentBounds.max.y}"); if (!hasAny) { combinedBounds = currentBounds; hasAny = true; } else { combinedBounds.Encapsulate(currentBounds); } } return hasAny; } /// /// Name anchor at top of tallest mesh: combinedBounds.max.y in world, converted to local. /// 名牌锚点位于最高 mesh 顶部:世界坐标 combinedBounds.max.y,再转为本地坐标。 /// private static bool TryGetItemNameAnchorLocal(Transform matterRoot, out Vector3 localAnchor) { if (!TryGetCombinedRendererBounds(matterRoot, null, out var combinedBounds)) { localAnchor = new Vector3(0f, MatterNameFallbackLocalY, 0f); return false; } var worldAnchor = new Vector3( combinedBounds.center.x, combinedBounds.max.y + MatterNameExtraWorldYOffset, combinedBounds.center.z); localAnchor = matterRoot.InverseTransformPoint(worldAnchor); return true; } private static void CreateItemNameText(GameObject matterObject, int tid) { if (matterObject == null) return; // Avoid duplicating if prefab already contains it (or Init called twice). if (matterObject.transform.Find(ItemNameTextChildName) != null) return; var textObject = new GameObject(ItemNameTextChildName); textObject.transform.SetParent(matterObject.transform, false); if (!TryGetItemNameAnchorLocal(matterObject.transform, out var localAnchor)) { Debug.LogWarning( $"[Cuong] [CECMatter] No renderer bounds for '{matterObject.name}'; using fallback Y={MatterNameFallbackLocalY}"); } textObject.transform.localPosition = localAnchor; var textMesh = textObject.AddComponent(); string itemName = null; if (EC_IvtrItemUtils.Instance != null) itemName = EC_IvtrItemUtils.Instance.ResolveItemName(tid); // Some name resolvers return the localization key when the translation is missing // (e.g. "BrewMonster.MINE_ESSENCE"). In that case we prefer showing nothing. static bool LooksLikeUntranslatedKey(string s) { if (string.IsNullOrWhiteSpace(s)) return false; if (s.IndexOf(' ') >= 0 || s.IndexOf('\t') >= 0 || s.IndexOf('\n') >= 0 || s.IndexOf('\r') >= 0) return false; // Heuristic: "Namespace.KEY_NAME" (has '.' and '_' and mostly identifier chars). if (s.IndexOf('.') < 0 || s.IndexOf('_') < 0) return false; for (int i = 0; i < s.Length; i++) { char c = s[i]; if (char.IsLetterOrDigit(c) || c == '_' || c == '.') continue; return false; } return true; } if (LooksLikeUntranslatedKey(itemName)) { itemName = string.Empty; } else if (string.IsNullOrEmpty(itemName)) { itemName = $"Item {tid}"; } textMesh.text = itemName; textMesh.fontSize = 2f; textMesh.color = Color.white; textMesh.alignment = TextAlignmentOptions.Center; textMesh.textWrappingMode = TextWrappingModes.NoWrap; textMesh.overflowMode = TextOverflowModes.Overflow; textObject.AddComponent(); } private static Mesh GetOrCreateItemNameQuadMesh() { if (s_itemNameQuadMesh != null) return s_itemNameQuadMesh; var quadMesh = new Mesh { name = "CECMatter_ItemNameQuad" }; quadMesh.vertices = new[] { new Vector3(-0.5f, -0.5f, 0), new Vector3(0.5f, -0.5f, 0), new Vector3(0.5f, 0.5f, 0), new Vector3(-0.5f, 0.5f, 0) }; quadMesh.triangles = new[] { 0, 1, 2, 0, 2, 3 }; quadMesh.uv = new[] { new Vector2(0, 0), new Vector2(1, 0), new Vector2(1, 1), new Vector2(0, 1) }; quadMesh.RecalculateNormals(); quadMesh.RecalculateBounds(); s_itemNameQuadMesh = quadMesh; return s_itemNameQuadMesh; } private static Material GetOrCreateItemNameBgMaterial() { if (s_itemNameBgMaterial != null) return s_itemNameBgMaterial; var shader = Shader.Find("Unlit/Color"); if (shader == null) shader = Shader.Find("Sprites/Default"); if (shader == null) shader = Shader.Find("Standard"); if (shader == null) return null; var mat = new Material(shader) { name = "CECMatter_ItemNameBg" }; mat.color = new Color(0f, 0f, 0f, 0.7f); // Best-effort: make it transparent if the shader supports it. if (mat.HasProperty("_Mode")) mat.SetFloat("_Mode", 3f); if (mat.HasProperty("_Surface")) mat.SetFloat("_Surface", 1f); s_itemNameBgMaterial = mat; return s_itemNameBgMaterial; } private new void Update() { base.Update(); // Recovery: after Unity domain reload, manager dictionaries reset but scene objects persist. // Keep trying until we successfully register. if (!m_registeredToManMatter) TryRegisterToManMatter(); // Check for touch input (mobile) if (Input.touchCount > 0) { Touch touch = Input.GetTouch(0); if (touch.phase == TouchPhase.Began) { // Touch handling intentionally disabled for now. / Touch handling intentionally disabled for now. } } // Check for mouse input (desktop) // else if (Input.GetMouseButtonDown(0)) // { // inputPressed = true; // screenPosition = Input.mousePosition; // } // // if (inputPressed) // { // Camera mainCamera = Camera.main; // if (mainCamera == null) // return; // // Ray ray = mainCamera.ScreenPointToRay(screenPosition); // // RaycastHit[] hits = Physics.RaycastAll(ray); // // foreach (RaycastHit hit in hits) // { // if (hit.collider.gameObject == this.gameObject || // hit.collider.transform.IsChildOf(this.transform)) // { // Debug.Log($"CECMatter::RaycastHit():: mid: {m_MatterInfo.mid}"); // UnityGameSession.RequestPickupItem(m_MatterInfo.mid, m_MatterInfo.tid); // break; // } // } // } } public int GetMatterID() { return m_MatterInfo.mid; } } }