From 1a36a8b6d79c9aa793e01f998f3d75590ba9a9fc Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Mon, 2 Mar 2026 10:49:24 +0800 Subject: [PATCH] feat: add exact mesh/material/texture hash and equal_to --- Ballance/BMapInspector/Rule/YYCRules.cpp | 420 +++++++++++++++++++++++ Ballance/BMapInspector/Rule/YYCRules.hpp | 16 + LibCmo/LibCmo/CK2/ObjImpls/CKTexture.cpp | 4 + LibCmo/LibCmo/CK2/ObjImpls/CKTexture.hpp | 1 + 4 files changed, 441 insertions(+) diff --git a/Ballance/BMapInspector/Rule/YYCRules.cpp b/Ballance/BMapInspector/Rule/YYCRules.cpp index c021752..1bdbd4d 100644 --- a/Ballance/BMapInspector/Rule/YYCRules.cpp +++ b/Ballance/BMapInspector/Rule/YYCRules.cpp @@ -1,11 +1,17 @@ #include "YYCRules.hpp" #include "Shared.hpp" +#include +#include +#include #include #include #include +#include +#include namespace L = LibCmo; namespace C = LibCmo::CK2; +namespace V = LibCmo::VxMath; namespace O = LibCmo::CK2::ObjImpls; namespace BMapInspector::Rule { @@ -129,4 +135,418 @@ namespace BMapInspector::Rule { #pragma endregion +#pragma region YYC Rule 3 + + /** + * @brief FNV-1a Hash Combiner + */ + class Hasher { + public: + using ValueType = size_t; + + private: +#if defined(YYCC_PTRSIZE_32) + static constexpr ValueType FNV_OFFSET_BASIS = 2166136261U; + static constexpr ValueType FNV_PRIME = 16777619U; +#else + static constexpr ValueType FNV_OFFSET_BASIS = 14695981039346656037ULL; + static constexpr ValueType FNV_PRIME = 1099511628211ULL; +#endif + + public: + Hasher() : seed(FNV_OFFSET_BASIS) {} + ~Hasher() {} + + private: + /** + * @brief Update this hash combiner with new hash. + * @param h + */ + void combine(ValueType h) { + this->seed ^= h; + this->seed *= FNV_PRIME; + } + + public: + /** + * @brief Get final produced hash. + * @return + */ + [[nodiscard]] ValueType finish() const noexcept { return this->seed; } + template + void update(const T& v) { + std::hash hasher; + combine(hasher(v)); + } + template + void update_array(const T* addr, size_t cnt) { + std::hash hasher; + for (size_t i = 0; i < cnt; ++i) { + combine(hasher(addr[i])) + } + } + + private: + ValueType seed; + }; + +#pragma region Primitive CK Hasher +} // namespace BMapInspector::Rule + +namespace std { + using BMapInspector::Rule::Hasher; + + template<> + struct hash { + [[nodiscard]] size_t operator()(const V::VxColor& color) const noexcept { + Hasher combiner; + combiner.update(color.r); + combiner.update(color.g); + combiner.update(color.b); + combiner.update(color.a); + return combiner.finish(); + } + }; + + template<> + struct hash { + [[nodiscard]] size_t operator()(const V::VxVector2& vec) const noexcept { + Hasher combiner; + combiner.update(vec.x); + combiner.update(vec.y); + return combiner.finish(); + } + }; + + template<> + struct hash { + [[nodiscard]] size_t operator()(const V::VxVector3& vec) const noexcept { + Hasher combiner; + combiner.update(vec.x); + combiner.update(vec.y); + combiner.update(vec.z); + return combiner.finish(); + } + }; + +} // namespace std + +namespace BMapInspector::Rule { +#pragma endregion + +#pragma region CKObject Hash and Equal + + struct CKTextureHash { + [[nodiscard]] size_t operator()(const O::CKTexture* tex) const noexcept { + const auto& texdata = tex->GetUnderlyingData(); + Hasher combiner; + + auto filename = texdata.GetSlotFileName(0); + if (filename == nullptr) { + combiner.update(nullptr); + } else { + auto lower_filename = yycc::string::op::to_lower(filename); + combiner.update(lower_filename); + } + combiner.update(texdata.GetSaveOptions()); + combiner.update(tex->GetVideoFormat()); + + return combiner.finish(); + } + }; + + struct CKMaterialHash { + [[nodiscard]] size_t operator()(const O::CKMaterial* mtl) const noexcept { + Hasher combiner; + + combiner.update(mtl->GetDiffuse()); + combiner.update(mtl->GetAmbient()); + combiner.update(mtl->GetSpecular()); + combiner.update(mtl->GetEmissive()); + combiner.update(mtl->GetSpecularPower()); + + // TODO: + // Use raw pointer for hash is dangerous. + // But who cares? I simply assume that there is no memory reallocation. + combiner.update(mtl->GetTexture()); + combiner.update(mtl->GetTextureBorderColor()); + + combiner.update(mtl->GetTextureBlendMode()); + combiner.update(mtl->GetTextureMinMode()); + combiner.update(mtl->GetTextureMagMode()); + combiner.update(mtl->GetTextureAddressMode()); + + combiner.update(mtl->GetSourceBlend()); + combiner.update(mtl->GetDestBlend()); + combiner.update(mtl->GetFillMode()); + combiner.update(mtl->GetShadeMode()); + + // TODO: + // We also need use these "Enabled" variable to switch on/off + // for some field's hashing according to the Virtools layout. + // But I am lazy now. + // I guess there is the same default values for those fields + // controlled by some disable "Enabled" variable. + combiner.update(mtl->GetAlphaTestEnabled()); + combiner.update(mtl->GetAlphaBlendEnabled()); + combiner.update(mtl->GetPerspectiveCorrectionEnabled()); + combiner.update(mtl->GetZWriteEnabled()); + combiner.update(mtl->GetTwoSidedEnabled()); + + combiner.update(mtl->GetAlphaRef()); + combiner.update(mtl->GetAlphaFunc()); + combiner.update(mtl->GetZFunc()); + + return combiner.finish(); + } + }; + + struct CKMeshHash { + [[nodiscard]] size_t operator()(const O::CKMesh* _mesh) const noexcept { + O::CKMesh* mesh = const_cast(_mesh); + Hasher combiner; + + combiner.update(mesh->GetLitMode()); + + auto vertex_count = mesh->GetVertexCount(); + combiner.update(vertex_count); + combiner.update_array(mesh->GetVertexPositions(), vertex_count); + combiner.update_array(mesh->GetVertexNormals(), vertex_count); + combiner.update_array(mesh->GetVertexUVs(), vertex_count); + + // TODO: + // In theory, we need remap face material slot index to underlying material CKID, + // but its too complex. I give up. + auto face_count = mesh->GetFaceCount(); + combiner.update(face_count); + combiner.update_array(mesh->GetFaceIndices(), face_count * 3); + combiner.update_array(mesh->GetFaceMaterialSlotIndexs(), face_count); + + auto material_slot_count = mesh->GetMaterialSlotCount(); + combiner.update(material_slot_count); + // TODO: + // Same dangerous usage of raw pointer. + combiner.update_array(mesh->GetMaterialSlots(), material_slot_count); + + return combiner.finish(); + } + }; + + struct CKTextureEqualTo { + [[nodiscard]] bool operator()(const O::CKTexture* lhs, const O::CKTexture* rhs) const { + // Compare underlying data + const auto& lhs_data = lhs->GetUnderlyingData(); + const auto& rhs_data = rhs->GetUnderlyingData(); + + // Compare filename (case insensitive) + auto lhs_filename = lhs_data.GetSlotFileName(0); + auto rhs_filename = rhs_data.GetSlotFileName(0); + if (!C::CKStrEqualI(lhs_filename, rhs_filename)) return false; + + // Compare save options + if (lhs_data.GetSaveOptions() != rhs_data.GetSaveOptions()) return false; + // Compare video format + if (lhs->GetVideoFormat() != rhs->GetVideoFormat()) return false; + + return true; + } + }; + + struct CKMaterialEqualTo { + [[nodiscard]] bool operator()(const O::CKMaterial* lhs, const O::CKMaterial* rhs) const { + // Compare color properties + if (lhs->GetDiffuse() != rhs->GetDiffuse()) return false; + if (lhs->GetAmbient() != rhs->GetAmbient()) return false; + if (lhs->GetSpecular() != rhs->GetSpecular()) return false; + if (lhs->GetEmissive() != rhs->GetEmissive()) return false; + if (lhs->GetSpecularPower() != rhs->GetSpecularPower()) return false; + + // Compare texture properties + if (lhs->GetTexture() != rhs->GetTexture()) return false; + if (lhs->GetTextureBorderColor() != rhs->GetTextureBorderColor()) return false; + + // Compare texture modes + if (lhs->GetTextureBlendMode() != rhs->GetTextureBlendMode()) return false; + if (lhs->GetTextureMinMode() != rhs->GetTextureMinMode()) return false; + if (lhs->GetTextureMagMode() != rhs->GetTextureMagMode()) return false; + if (lhs->GetTextureAddressMode() != rhs->GetTextureAddressMode()) return false; + + // Compare blend modes + if (lhs->GetSourceBlend() != rhs->GetSourceBlend()) return false; + if (lhs->GetDestBlend() != rhs->GetDestBlend()) return false; + if (lhs->GetFillMode() != rhs->GetFillMode()) return false; + if (lhs->GetShadeMode() != rhs->GetShadeMode()) return false; + + // Compare enable flags + if (lhs->GetAlphaTestEnabled() != rhs->GetAlphaTestEnabled()) return false; + if (lhs->GetAlphaBlendEnabled() != rhs->GetAlphaBlendEnabled()) return false; + if (lhs->GetPerspectiveCorrectionEnabled() != rhs->GetPerspectiveCorrectionEnabled()) return false; + if (lhs->GetZWriteEnabled() != rhs->GetZWriteEnabled()) return false; + if (lhs->GetTwoSidedEnabled() != rhs->GetTwoSidedEnabled()) return false; + + // Compare alpha and z function properties + if (lhs->GetAlphaRef() != rhs->GetAlphaRef()) return false; + if (lhs->GetAlphaFunc() != rhs->GetAlphaFunc()) return false; + if (lhs->GetZFunc() != rhs->GetZFunc()) return false; + + return true; + } + }; + + struct CKMeshEqualTo { + [[nodiscard]] bool operator()(const O::CKMesh* _lhs, const O::CKMesh* _rhs) const { + O::CKMesh* lhs = const_cast(_lhs); + O::CKMesh* rhs = const_cast(_rhs); + + // Compare lit mode + if (lhs->GetLitMode() != rhs->GetLitMode()) return false; + + // Compare vertex count + auto vertex_count = lhs->GetVertexCount(); + if (vertex_count != rhs->GetVertexCount()) return false; + + // Compare vertex data arrays + if (!std::equal(lhs->GetVertexPositions(), lhs->GetVertexPositions() + vertex_count, rhs->GetVertexPositions())) return false; + if (!std::equal(lhs->GetVertexNormals(), lhs->GetVertexNormals() + vertex_count, rhs->GetVertexNormals())) return false; + if (!std::equal(lhs->GetVertexUVs(), lhs->GetVertexUVs() + vertex_count, rhs->GetVertexUVs())) return false; + + // Compare face count + auto face_count = lhs->GetFaceCount(); + if (face_count != rhs->GetFaceCount()) return false; + + // Compare face data arrays + if (!std::equal(lhs->GetFaceIndices(), lhs->GetFaceIndices() + face_count * 3, rhs->GetFaceIndices())) return false; + if (!std::equal(lhs->GetFaceMaterialSlotIndexs(), lhs->GetFaceMaterialSlotIndexs() + face_count, rhs->GetFaceMaterialSlotIndexs())) return false; + + // Compare material slot count + auto material_slot_count = lhs->GetMaterialSlotCount(); + if (material_slot_count != rhs->GetMaterialSlotCount()) return false; + + // Compare material slots array + if (!std::equal(lhs->GetMaterialSlots(), lhs->GetMaterialSlots() + material_slot_count, rhs->GetMaterialSlots())) return false; + + return true; + } + }; + +#pragma endregion + +#pragma region CKObject Wrapper + + class CKTextureWrapper { + public: + CKTextureWrapper(O::CKTexture* texture) : texture(texture), hasher(), hash(std::nullopt) {} + ~CKTextureWrapper() {} + YYCC_DEFAULT_COPY_MOVE(CKTextureWrapper) + + public: + O::CKTexture* GetTexture() const { return texture; } + size_t GetHash() const { + if (!hash.has_value()) hash = hasher(texture); + return hash.value(); + } + + private: + O::CKTexture* texture; + CKTextureHash hasher; + mutable std::optional hash; + }; + + class CKMaterialWrapper { + public: + CKMaterialWrapper(O::CKMaterial* material) : material(material), hasher(), hash(std::nullopt) {} + ~CKMaterialWrapper() {} + YYCC_DEFAULT_COPY_MOVE(CKMaterialWrapper) + + public: + O::CKMaterial* GetMaterial() const { return material; } + size_t GetHash() const { + if (!hash.has_value()) hash = hasher(material); + return hash.value(); + } + + private: + O::CKMaterial* material; + CKMaterialHash hasher; + mutable std::optional hash; + }; + + class CKMeshWrapper { + public: + CKMeshWrapper(O::CKMesh* mesh) : mesh(mesh), hasher(), hash(std::nullopt) {} + ~CKMeshWrapper() {} + YYCC_DEFAULT_COPY_MOVE(CKMeshWrapper) + + public: + O::CKMesh* GetMesh() const { return mesh; } + size_t GetHash() const { + if (!hash.has_value()) hash = hasher(mesh); + return hash.value(); + } + + private: + O::CKMesh* mesh; + CKMeshHash hasher; + mutable std::optional hash; + }; + +#pragma endregion + +#pragma region CKObject Wrapper Hash and Equal + + struct CKTextureWrapperHash { + [[nodiscard]] size_t operator()(const CKTextureWrapper& tex) const noexcept { + return tex.GetHash(); + } + }; + + struct CKMaterialWrapperHash { + [[nodiscard]] size_t operator()(const CKMaterialWrapper& mtl) const noexcept { + return mtl.GetHash(); + } + }; + + struct CKMeshWrapperHash { + [[nodiscard]] size_t operator()(const CKMeshWrapper& mesh) const noexcept { + return mesh.GetHash(); + } + }; + + struct CKTextureWrapperEqualTo { + CKTextureEqualTo equal_to; + [[nodiscard]] bool operator()(const CKTextureWrapper& lhs, const CKTextureWrapper& rhs) const { + return equal_to(lhs.GetTexture(), rhs.GetTexture()); + } + }; + + struct CKMaterialWrapperEqualTo { + CKMaterialEqualTo equal_to; + [[nodiscard]] bool operator()(const CKMaterialWrapper& lhs, const CKMaterialWrapper& rhs) const { + return equal_to(lhs.GetMaterial(), rhs.GetMaterial()); + } + }; + + struct CKMeshWrapperEqualTo { + CKMeshEqualTo equal_to; + [[nodiscard]] bool operator()(const CKMeshWrapper& lhs, const CKMeshWrapper& rhs) const { + return equal_to(lhs.GetMesh(), rhs.GetMesh()); + } + }; + +#pragma endregion + + constexpr char8_t YYC3[] = u8"YYC3"; + + YYCRule3::YYCRule3() : IRule() {} + + YYCRule3::~YYCRule3() {} + + std::u8string_view YYCRule3::GetRuleName() const { + return YYC3; + } + + void YYCRule3::Check(Reporter::Reporter& reporter, Map::Level& level) const {} + +#pragma endregion + } // namespace BMapInspector::Rule diff --git a/Ballance/BMapInspector/Rule/YYCRules.hpp b/Ballance/BMapInspector/Rule/YYCRules.hpp index b145f61..d043dbe 100644 --- a/Ballance/BMapInspector/Rule/YYCRules.hpp +++ b/Ballance/BMapInspector/Rule/YYCRules.hpp @@ -37,4 +37,20 @@ namespace BMapInspector::Rule { void Check(Reporter::Reporter& reporter, Map::Level& level) const override; }; + /** + * @brief YYC12345 Rule 3 + * @details + * Exactly same mesh, material and texture can be merged. + */ + class YYCRule3 : public IRule { + public: + YYCRule3(); + virtual ~YYCRule3(); + YYCC_DELETE_COPY_MOVE(YYCRule3) + + public: + std::u8string_view GetRuleName() const override; + void Check(Reporter::Reporter& reporter, Map::Level& level) const override; + }; + } diff --git a/LibCmo/LibCmo/CK2/ObjImpls/CKTexture.cpp b/LibCmo/LibCmo/CK2/ObjImpls/CKTexture.cpp index 966f40e..67e7830 100644 --- a/LibCmo/LibCmo/CK2/ObjImpls/CKTexture.cpp +++ b/LibCmo/LibCmo/CK2/ObjImpls/CKTexture.cpp @@ -343,6 +343,10 @@ namespace LibCmo::CK2::ObjImpls { return m_ImageHost; } + const CKBitmapData& CKTexture::GetUnderlyingData() const { + return m_ImageHost; + } + bool CKTexture::LoadImage(CKSTRING filename, CKDWORD slot) { // check file name if (filename == nullptr) return false; diff --git a/LibCmo/LibCmo/CK2/ObjImpls/CKTexture.hpp b/LibCmo/LibCmo/CK2/ObjImpls/CKTexture.hpp index aa3c955..4697d40 100644 --- a/LibCmo/LibCmo/CK2/ObjImpls/CKTexture.hpp +++ b/LibCmo/LibCmo/CK2/ObjImpls/CKTexture.hpp @@ -20,6 +20,7 @@ namespace LibCmo::CK2::ObjImpls { virtual bool Load(CKStateChunk* chunk, CKFileVisitor* file) override; CKBitmapData& GetUnderlyingData(); + const CKBitmapData& GetUnderlyingData() const; /** * @brief A wrapper of underlying CKBitmapData::LoadImage. Not only load image, but also set file name.