525 lines
22 KiB
C#
525 lines
22 KiB
C#
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<CECMatter> 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<Collider>() == null)
|
|
{
|
|
var collider = matterObject.AddComponent<BoxCollider>();
|
|
//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<Renderer>();
|
|
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<MatterCubeClickHandler>();
|
|
// clickHandler.Initialize(Info.mid, this);
|
|
CECMatter matterScript = matterObject.AddComponent<CECMatter>();
|
|
// 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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 未刷新的问题。
|
|
/// </summary>
|
|
private static bool TryGetCombinedRendererBounds(Transform matterRoot, Transform excludeSubtree, out Bounds combinedBounds)
|
|
{
|
|
combinedBounds = default;
|
|
if (matterRoot == null)
|
|
return false;
|
|
|
|
var renderers = matterRoot.GetComponentsInChildren<Renderer>(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<MeshFilter>();
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Name anchor at top of tallest mesh: combinedBounds.max.y in world, converted to local.
|
|
/// 名牌锚点位于最高 mesh 顶部:世界坐标 combinedBounds.max.y,再转为本地坐标。
|
|
/// </summary>
|
|
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<TextMeshPro>();
|
|
|
|
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<Billboard>();
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|