#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 { #pragma region YYC Rule 1 constexpr char8_t YYC1[] = u8"YYC1"; YYCRule1::YYCRule1() : IRule() {} YYCRule1::~YYCRule1() {} std::u8string_view YYCRule1::GetRuleName() const { return YYC1; } void YYCRule1::Check(Reporter::Reporter& reporter, Map::Level& level) const { auto* ctx = level.GetCKContext(); // We get "Phys_FloorRails" group first. auto* phys_floorrails = Shared::FetchGroup(ctx, Shared::GroupNames::PHYS_FLOORRAILS); if (phys_floorrails == nullptr) return; // Create container holding smooth meshes std::set smooth_meshes; // We iterate all object grouped into it. auto group_3dobjects = Shared::Iter3dObjects(phys_floorrails); for (auto* group_3dobject : group_3dobjects) { // Then we iterate their current meshes auto* mesh = group_3dobject->GetCurrentMesh(); if (mesh == nullptr) continue; // Iterate all meshes auto mtls = Shared::IterMaterial(mesh); for (auto* mtl : mtls) { // Check whether all texture referred by this mesh are "Rail_Environment". auto texture = mtl->GetTexture(); if (texture == nullptr) continue; if (!Shared::CheckTextureFileName(texture, Shared::TextureNames::RAIL_ENVIRONMENT)) { // No, this is not rail texture, throw error. reporter.FormatError( YYC1, u8R"(Object %s is grouped into Phys_FloorRails, but its texture %s (referred by mesh %s and material %s) seems not the rail texture. This will cause some parts of this object be smooth unexpectly.)", Shared::QuoteObjectName(group_3dobject).c_str(), Shared::QuoteObjectName(texture).c_str(), Shared::QuoteObjectName(mesh).c_str(), Shared::QuoteObjectName(mtl).c_str()); } } // Record this mesh into set. smooth_meshes.emplace(mesh); } // Now we make sure that these smooth mesh is not referred by any other object. // We iterate all 3d object first auto all_3dobject = level.Get3dObjects(); for (auto* obj : all_3dobject) { // Then we get its current mesh auto* mesh = obj->GetCurrentMesh(); if (mesh == nullptr) continue; // Check whether its mesh is in smooth mesh, // and itself is not in "Phys_FloorRails" group if (!obj->IsInGroup(phys_floorrails) && smooth_meshes.contains(mesh)) { // Report error. reporter.FormatError( YYC1, u8R"(Object %s is not grouped into Phys_FloorRails, but some objects grouped into Phys_FloorRails refer its mesh %s. This will cause this object be smooth unexpectly.)", Shared::QuoteObjectName(obj).c_str(), Shared::QuoteObjectName(mesh).c_str()); } } } #pragma endregion #pragma region YYC Rule 2 constexpr char8_t YYC2[] = u8"YYC2"; YYCRule2::YYCRule2() : IRule() {} YYCRule2::~YYCRule2() {} std::u8string_view YYCRule2::GetRuleName() const { return YYC2; } void YYCRule2::Check(Reporter::Reporter& reporter, Map::Level& level) const { auto* ctx = level.GetCKContext(); auto physicalized_3dobjects = Shared::FetchPhysicalized3dObjects(ctx); // Iterate all physicalized 3dobject for (auto* physicalized_3dobject : physicalized_3dobjects) { // Get its mesh auto* mesh = physicalized_3dobject->GetCurrentMesh(); if (mesh == nullptr) continue; // Create a bool vector with vertex count and false init value. std::vector used_vertex(mesh->GetVertexCount(), false); // Iterate all face and set their vertex as used. auto* face_indices = mesh->GetFaceIndices(); for (L::CKDWORD face_idx = 0; face_idx < mesh->GetFaceCount(); ++face_idx) { used_vertex[face_indices[face_idx * 3]] = true; used_vertex[face_indices[face_idx * 3 + 1]] = true; used_vertex[face_indices[face_idx * 3 + 2]] = true; } // Check whether there is unused vertex auto has_unused_vertex = std::any_of(used_vertex.begin(), used_vertex.end(), [](bool v) { return v == false; }); // If there is unused vertex, report error if (has_unused_vertex) { reporter.FormatError( YYC2, u8R"(Object %s is grouped into physicalization groups, and its referred mesh %s has isolated vertex. This will cause it can not be physicalized.)", Shared::QuoteObjectName(physicalized_3dobject).c_str(), Shared::QuoteObjectName(mesh).c_str()); } } } #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