From 873f4e97e184838a1fa95af56b5a5c905280196e Mon Sep 17 00:00:00 2001 From: indigo Date: Mon, 30 Mar 2026 09:55:19 +0800 Subject: [PATCH] Init Repo --- .gitignore | 147 +++++++ AGENT.md | 31 ++ CMakeLists.txt | 25 ++ CMakePresets.json | 124 ++++++ README.md | 116 ++++++ cmake/modules/FindMaya.cmake | 184 +++++++++ plans/plan.md | 177 +++++++++ src/CMakeLists.txt | 2 + src/CipherProvider.h | 35 ++ src/ProtectAsset/CMakeLists.txt | 55 +++ src/ProtectAsset/ProtectAssetCommand.cpp | 87 ++++ src/ProtectAsset/ProtectAssetCommand.h | 14 + src/ProtectAsset/plugin.cpp | 37 ++ src/ValidAsset/CMakeLists.txt | 81 ++++ src/ValidAsset/EncryptCommand.cpp | 27 ++ src/ValidAsset/EncryptCommand.h | 20 + src/ValidAsset/LicenseChecker.cpp | 158 ++++++++ src/ValidAsset/LicenseChecker.h | 58 +++ src/ValidAsset/ValidShape.cpp | 199 ++++++++++ src/ValidAsset/ValidShape.h | 71 ++++ src/ValidAsset/pluginMain.cpp | 63 +++ .../scripts/AEValidShapeTemplate.mel | 29 ++ src/ValidAsset/scripts/test_plugin_load.py | 44 +++ src/ValidAsset/scripts/test_validshape.py | 374 ++++++++++++++++++ 24 files changed, 2158 insertions(+) create mode 100644 .gitignore create mode 100644 AGENT.md create mode 100644 CMakeLists.txt create mode 100644 CMakePresets.json create mode 100644 README.md create mode 100644 cmake/modules/FindMaya.cmake create mode 100644 plans/plan.md create mode 100644 src/CMakeLists.txt create mode 100644 src/CipherProvider.h create mode 100644 src/ProtectAsset/CMakeLists.txt create mode 100644 src/ProtectAsset/ProtectAssetCommand.cpp create mode 100644 src/ProtectAsset/ProtectAssetCommand.h create mode 100644 src/ProtectAsset/plugin.cpp create mode 100644 src/ValidAsset/CMakeLists.txt create mode 100644 src/ValidAsset/EncryptCommand.cpp create mode 100644 src/ValidAsset/EncryptCommand.h create mode 100644 src/ValidAsset/LicenseChecker.cpp create mode 100644 src/ValidAsset/LicenseChecker.h create mode 100644 src/ValidAsset/ValidShape.cpp create mode 100644 src/ValidAsset/ValidShape.h create mode 100644 src/ValidAsset/pluginMain.cpp create mode 100644 src/ValidAsset/scripts/AEValidShapeTemplate.mel create mode 100644 src/ValidAsset/scripts/test_plugin_load.py create mode 100644 src/ValidAsset/scripts/test_validshape.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..56db26b --- /dev/null +++ b/.gitignore @@ -0,0 +1,147 @@ +# File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig +# Created by https://www.toptal.com/developers/gitignore/api/windows,visualstudiocode,c,c++,cmake +# Edit at https://www.toptal.com/developers/gitignore?templates=windows,visualstudiocode,c,c++,cmake + +### C ### +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf + +### C++ ### +# Prerequisites + +# Compiled Object files +*.slo + +# Precompiled Headers + +# Compiled Dynamic libraries + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai + +# Executables + +### CMake ### +CMakeLists.txt.user +CMakeCache.txt +CMakeFiles +CMakeScripts +Testing +Makefile +cmake_install.cmake +install_manifest.txt +compile_commands.json +CTestTestfile.cmake +_deps + +### CMake Patch ### +CMakeUserPresets.json + +# External projects +*-prefix/ + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/windows,visualstudiocode,c,c++,cmake + +# Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) + +build/ +install/ \ No newline at end of file diff --git a/AGENT.md b/AGENT.md new file mode 100644 index 0000000..7ec2517 --- /dev/null +++ b/AGENT.md @@ -0,0 +1,31 @@ +IMPORTANT : You must use agent team feature. Do not do any work your self. +你唯一的工作就是與我溝通對話,並擔任團隊領導與協調工作。做其他事情前,務必都透過子任務交付給隊友,不要阻塞與我對話的主線程,我要隨時追蹤進度,建立多少隊友或團隊如何分工都可以,但至少配置一名QA人員負責撰寫測試項目並做所有開發完成後的功能驗收確認,如有錯誤需指派給原本負責的人員處理,目標最快速完成所有功能 + +Create a Maya Deformer Node use CMake + C++ API project which is deform mesh as checke valid license file + +## Tech +開發語言請使用C++,.cpp與.h請放置在src下,外部prebuild library請放在third_party下,加入CMakePresets.json,並支援Maya 2022到Maya 2026的版本編譯 + +## 功能 + +這是 Maya 的 Plugin Node, 功能包含 +1. Deform Object to noise with invalid license +2. license format is contain valid date + + +7. 建立Node完成後請編寫AETemplate, 名稱規則為 AE[NodeName]Template.mel +8. `.mll` 放到plug-ins/[MayaVersion]內, `.mel` 放到scripts, `.png` `.svg` 放到icons下 + +## 測試環境 +使用Maya 2023作為測試環境 + +Maya路徑為 `C:\Program Files\Autodesk\Maya2023\bin\maya.exe` +MayaBatch路徑為 `C:\Program Files\Autodesk\Maya2023\bin\mayabatch.exe` +MayaPy路徑為 `C:\Program Files\Autodesk\Maya2023\bin\mayapy.exe` + +* 啟動 Plugin 編譯完成後測試請使用環境變數 `MAYA_MODULE_PATH` 指定到編譯install下包含.mod的目錄下 +* 測試 Load Plugin 使用 mayapy.exe +* 測試 viewport 顯示部分請使用maya.exe + MEL 測試 + + +團隊自行決定未定義的的其他技術細節與檔案名稱,確認開發功能都完成後告訴我 \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..035c591 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,25 @@ +cmake_minimum_required(VERSION 3.21) +project(ValidToolkit) + +# C++ Standard +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +# Build configuration +set(CMAKE_CONFIGURATION_TYPES "Debug;Release" CACHE STRING "" FORCE) +set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>" CACHE STRING "" FORCE) + +# Output directories +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) + +# Add cmake modules path +list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/modules") + + +# Source files - include subdirectory CMakeLists.txt +add_subdirectory(src) + + + diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..f642919 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,124 @@ +{ + "version": 3, + "configurePresets": [ + { + "name": "base", + "hidden": true, + "binaryDir": "${sourceDir}/build", + "installDir": "${sourceDir}/install", + "generator": "Visual Studio 17 2022", + "architecture": { + "value": "x64" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + } + }, + { + "name": "maya2022", + "displayName": "Maya 2022", + "inherits": "base", + "cacheVariables": { + "MAYA_VERSION": "2022", + "MAYA_LOCATION": "C:/Program Files/Autodesk/Maya2022", + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "maya2023", + "displayName": "Maya 2023", + "inherits": "base", + "cacheVariables": { + "MAYA_VERSION": "2023", + "MAYA_LOCATION": "C:/Program Files/Autodesk/Maya2023", + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "maya2024", + "displayName": "Maya 2024", + "inherits": "base", + "cacheVariables": { + "MAYA_VERSION": "2024", + "MAYA_LOCATION": "C:/Program Files/Autodesk/Maya2024", + "MAYALIB_LIBRARY": "C:/Program Files/Autodesk/Maya2024/lib", + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "maya2025", + "displayName": "Maya 2025", + "inherits": "base", + "cacheVariables": { + "MAYA_VERSION": "2025", + "MAYA_LOCATION": "C:/Program Files/Autodesk/Maya2025", + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "maya2026", + "displayName": "Maya 2026", + "inherits": "base", + "cacheVariables": { + "MAYA_VERSION": "2026", + "MAYA_LOCATION": "C:/Program Files/Autodesk/Maya2026", + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "debug", + "displayName": "Debug Build", + "inherits": "maya2023", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "release", + "displayName": "Release Build", + "inherits": "maya2023", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + } + ], + "buildPresets": [ + { + "name": "debug", + "configurePreset": "debug", + "configuration": "Debug" + }, + { + "name": "release", + "configurePreset": "release", + "configuration": "Release" + }, + { + "name": "maya2022", + "configurePreset": "maya2022", + "configuration": "Release" + }, + { + "name": "maya2023", + "configurePreset": "maya2023", + "configuration": "Release" + }, + { + "name": "maya2024", + "configurePreset": "maya2024", + "configuration": "Release" + }, + { + "name": "maya2025", + "configurePreset": "maya2025", + "configuration": "Release" + }, + { + "name": "maya2026", + "configurePreset": "maya2026", + "configuration": "Release" + } + ] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..bc932b9 --- /dev/null +++ b/README.md @@ -0,0 +1,116 @@ +# ValidShape + +Maya Deformer Node Plugin for mesh validation with license checking + +## 功能 + +### ValidShape Plugin +- **ValidShape** - Maya Deformer Node,可根據授權檔案驗證並對 Mesh 進行變形 + - 檢查授權檔案格式是否包含有效日期 + - 當授權無效時,使用雜訊對 Mesh 進行變形(Deform Object to noise) + - 支援 Maya 2022 至 Maya 2026 + +### ProtectAsset Plugin +- **ProtectAsset** - 加密保護 Maya 資產的命令 + - 對 Mesh 幾何進行加密處理 + - 自動掛載解密 Deformer + +## 專案結構 + +``` +ValidShape/ +├── CMakeLists.txt # Root CMake configuration +├── CMakePresets.json # CMake presets (Maya 2022-2026) +├── src/ +│ ├── ValidAsset/ # ValidShape plugin source +│ │ ├── ValidShape.cpp/h # Deformer node implementation +│ │ ├── LicenseChecker.cpp/h +│ │ ├── EncryptCommand.cpp/h +│ │ ├── pluginMain.cpp +│ │ └── scripts/ +│ │ └── AEValidShapeTemplate.mel +│ ├── ProtectAsset/ # ProtectAsset plugin source +│ │ ├── ProtectAssetCommand.cpp/h +│ │ └── plugin.cpp +│ └── CipherProvider.h # Shared encryption utilities +├── cmake/modules/ +│ └── FindMaya.cmake # Maya SDK finder +├── install/ # Installed plugins +├── icons/ # Icons +└── build/ # Build output +``` + +## 編譯需求 + +### 必要工具 +- Visual Studio 2022 +- CMake 3.21+ +- Maya SDK 2022/2023/2024/2025/2026 + +### 編譯方式 + +```bash +# 使用 CMake Preset 編譯 +cmake --preset release +cmake --build build --config Release + +# 安裝 +cmake --install build --config Release +``` + +### Maya 版本對應 + +| CMake Preset | Maya 版本 | +|-------------|-----------| +| maya2022 | Maya 2022 | +| maya2023 | Maya 2023 | +| maya2024 | Maya 2024 | +| maya2025 | Maya 2025 | +| maya2026 | Maya 2026 | + +## 測試方式 + +### 設定環境變數 +```bash +# 設定 Maya module 路徑 +set MAYYA_MODULE_PATH=D:\workspace\ValidShape\install +``` + +### 測試指令 + +#### 使用 mayapy.exe 測試 Load Plugin +```bash +"C:\Program Files\Autodesk\Maya2023\bin\mayapy.exe" +``` + +#### 使用 maya.exe + MEL 測試 Viewport 顯示 +```bash +"C:\Program Files\Autodesk\Maya2023\bin\maya.exe" +``` + +### MEL 測試指令 +```mel +# Load plugin +loadPlugin "D:/workspace/ValidShape/install/plug-ins/2023/ValidShape.mll"; + +# Create test mesh +polySphere; + +# Apply ValidShape deformer +select pSphere1; +deformer -type "ValidShape"; +``` + +## 輸出檔案 + +編譯後的 plugin 會輸出至: + +| 類型 | 路徑 | +|-----|------| +| `.mll` | `build/bin/Release/[PluginName].mll` | +| `.mod` | `install/ValidShape.mod` | +| MEL | `install/scripts/AEValidShapeTemplate.mel` | + +## 授權 + +Copyright © 2024. All rights reserved. \ No newline at end of file diff --git a/cmake/modules/FindMaya.cmake b/cmake/modules/FindMaya.cmake new file mode 100644 index 0000000..d3affeb --- /dev/null +++ b/cmake/modules/FindMaya.cmake @@ -0,0 +1,184 @@ +# FindMaya.cmake - CMake module for finding Autodesk Maya SDK +# This module provides functions to find Maya include directories and libraries + +#[=======================================================================[ +FindMaya +-------- + +This module finds the Maya SDK installation and provides variables +and functions for building Maya plugins. + +Imported Targets +^^^^^^^^^^^^^^^^ + +This module does not define any library targets. + +Result Variables +^^^^^^^^^^^^^^^ + +This module defines the following variables: + +:: + + MAYA_FOUND - True if Maya SDK was found + MAYA_INCLUDE_DIR - Maya include directories + MAYA_LIBRARIES - Maya libraries to link against + MAYA_VERSION - The version of Maya found (e.g., 2023) + MAYA_LOCATION - The root Maya installation directory + MAYA_BIN_DIR - Maya bin directory + MAYA_LIB_DIR - Maya lib directory + MAYA_PLUGINS_DIR - Maya plugins directory + +Hints +^^^^^ + +The user may set the following variables to help this module find what +they want: + +:: + + MAYA_VERSION - Specify a specific Maya version to search for + (e.g., 2022, 2023, 2024, 2025, 2026) + MAYA_LOCATION - Specify the root Maya installation directory + (e.g., C:/Program Files/Autodesk/Maya2023) + +#]=======================================================================] + +# Check if already found +if(MAYA_FOUND AND MAYA_INCLUDE_DIR) + return() +endif() + +# Default Maya version if not specified +if(NOT DEFINED MAYA_VERSION) + set(MAYA_VERSION 2023) +endif() + +# List of supported Maya versions +set(_MAYA_SUPPORTED_VERSIONS "2026;2025;2024;2023;2022") + +# Check if the specified Maya version is supported +if(NOT MAYA_VERSION IN_LIST _MAYA_SUPPORTED_VERSIONS) + message(WARNING "Maya version ${MAYA_VERSION} is not in the supported list: ${_MAYA_SUPPORTED_VERSIONS}") +endif() + +# Search paths +set(_MAYA_SEARCH_PATHS) + +# Add user-specified MAYA_LOCATION +if(MAYA_LOCATION) + list(APPEND _MAYA_SEARCH_PATHS "${MAYA_LOCATION}") +endif() +if(ENV{MAYA_LOCATION}) + list(APPEND _MAYA_SEARCH_PATHS "$ENV{MAYA_LOCATION}") +endif() +message(STATUS ${_MAYA_SEARCH_PATHS}) +# Add environment variable paths +foreach(_env_var IN ITEMS MAYA_LOCATION MAYA_SDK_DIR MAYA_SDK_ROOT) + if(NOT "${${_env_var}}" STREQUAL "") + list(APPEND _MAYA_SEARCH_PATHS "${${_env_var}}") + endif() +endforeach() + +# Add Program Files paths for all supported versions +#foreach(_version IN ITEMS 2026 2025 2024 2023 2022) +# list(APPEND _MAYA_SEARCH_PATHS "C:/Program Files/Autodesk/Maya${_version}") +#endforeach() + +# Find Maya include directory +find_path(MAYA_INCLUDE_DIR + NAMES maya/MFn.h mapi_ui_control.h + HINTS ${_MAYA_SEARCH_PATHS} + PATH_SUFFIXES include +) + +# Find Maya library directory +find_path(MAYA_LIB_DIR + NAMES OpenMaya.lib + HINTS ${_MAYA_SEARCH_PATHS} + PATH_SUFFIXES lib +) + +# Find Maya bin directory +find_path(MAYA_BIN_DIR + NAMES maya.exe + HINTS ${_MAYA_SEARCH_PATHS} + PATH_SUFFIXES bin +) + +# Extract Maya version from include path if not specified +if(MAYA_INCLUDE_DIR AND NOT MAYA_VERSION) + foreach(_version IN ITEMS 2026 2025 2024 2023 2022) + if(MAYA_INCLUDE_DIR MATCHES "Maya${_version}") + set(MAYA_VERSION ${_version}) + break() + endif() + endforeach() +endif() + +# Find Maya libraries +set(_MAYA_REQUIRED_LIBS + OpenMaya + Foundation +) + +set(_MAYA_OPTIONAL_LIBS + OpenMayaUI + OpenMayaAnim + OpenMayaRender +) + +set(MAYA_LIBRARIES) + +foreach(_lib IN ITEMS ${_MAYA_REQUIRED_LIBS}) + find_library(_MAYA_LIB_${_lib} + NAMES "${_lib}.lib" + HINTS ${MAYA_LIB_DIR} + NO_DEFAULT_PATH + ) + if(_MAYA_LIB_${_lib}) + list(APPEND MAYA_LIBRARIES "${_MAYA_LIB_${_lib}}") + else() + message(FATAL_ERROR "Could not find required Maya library: ${_lib}.lib") + endif() +endforeach() + +foreach(_lib IN ITEMS ${_MAYA_OPTIONAL_LIBS}) + find_library(_MAYA_LIB_${_lib} + NAMES "${_lib}.lib" + HINTS ${MAYA_LIB_DIR} + NO_DEFAULT_PATH + ) + if(_MAYA_LIB_${_lib}) + list(APPEND MAYA_LIBRARIES "${_MAYA_LIB_${_lib}}") + endif() +endforeach() + +# Get Maya root directory from include path +get_filename_component(MAYA_LOCATION "${MAYA_INCLUDE_DIR}" DIRECTORY) +get_filename_component(MAYA_LOCATION "${MAYA_LOCATION}" DIRECTORY) + +# Set plugins directory +set(MAYA_PLUGINS_DIR "${MAYA_LOCATION}/plug-ins") + +# Handle success/failure +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Maya + REQUIRED_VARS + MAYA_INCLUDE_DIR + MAYA_LIB_DIR + MAYA_LIBRARIES + VERSION_VAR MAYA_VERSION +) + +# Print configuration info +if(MAYA_FOUND) + message(STATUS "Maya SDK found:") + message(STATUS " Version: ${MAYA_VERSION}") + message(STATUS " Location: ${MAYA_LOCATION}") + message(STATUS " Include: ${MAYA_INCLUDE_DIR}") + message(STATUS " Library: ${MAYA_LIB_DIR}") +endif() + +# Mark as found +mark_as_advanced(MAYA_INCLUDE_DIR MAYA_LIB_DIR MAYA_BIN_DIR) \ No newline at end of file diff --git a/plans/plan.md b/plans/plan.md new file mode 100644 index 0000000..32eb3cd --- /dev/null +++ b/plans/plan.md @@ -0,0 +1,177 @@ +# ValidShape - Maya Deformer Node 開發計畫 + +## 專案概述 +建立一個 Maya Deformer Node Plugin,透過 License 檔案驗證來控制網格變形效果。 + +## 技術規格 +- **開發語言**: C++ +- **建置系統**: CMake + CMakePresets.json +- **Maya 版本支援**: Maya 2022 - Maya 2026 +- **測試環境**: Maya 2023 + +## 功能需求 +1. **Deform Object**: 當 License 無效時,網格會被 Noise 變形 +2. **License 格式**: 包含有效日期 (格式: YYYY-MM-DD) +3. **License 檔案**: 透過節點屬性指定路徑 + +## 團隊分工 + +### 角色分配 +| 角色 | 姓名 | 職責 | +|------|------|------| +| Team Lead | - | 專案協調與進度追蹤 | +| C++ Developer | Dev1 | Maya Node 核心程式開發 | +| C++ Developer | Dev2 | License 檢查與解析模組 | +| QA Engineer | QA1 | 測試腳本撰寫與功能驗證 | +| Build Engineer | Build1 | CMake 建置系統與編譯環境 | + +### 工作流程 +1. 先由 Build Engineer 建立 CMake 專案結構 +2. C++ Developers 實作核心功能 +3. QA Engineer 撰寫測試腳本 +4. 團隊協作進行編譯與測試 + +## 檔案結構 +``` +ValidShape/ +├── CMakeLists.txt +├── CMakePresets.json +├── src/ +│ ├── ValidShapeNode.cpp +│ ├── ValidShapeNode.h +│ ├── LicenseChecker.cpp +│ └── LicenseChecker.h +├── third_party/ +│ └── (Maya SDK headers) +├── plug-ins/ +│ └── [MayaVersion]/ +├── scripts/ +├── icons/ +├── plans/ +│ └── todo.md +└── install/ + └── (編譯產出) +``` + +## 預期產出 +- `.mll` plugin 檔案 (置於 plug-ins/[Version]/) +- `.mel` AETemplate 檔案 (置於 scripts/) +- `.png` / `.svg` 圖示檔案 (置於 icons/) +- `.mod` module 檔案 (用於 MAYA_MODULE_PATH) + +## 專案概述 +建立一個 Maya Deformer Node Plugin,透過 License 檔案驗證來控制網格變形效果。 + +## 技術規格 +- **開發語言**: C++ +- **建置系統**: CMake + CMakePresets.json +- **Maya 版本支援**: Maya 2022 - Maya 2026 +- **測試環境**: Maya 2023 + +## 功能需求 +1. **Deform Object**: 當 License 無效時,網格會被 Noise 變形 +2. **License 格式**: 包含有效日期 (格式: YYYY-MM-DD) +3. **License 檔案**: 透過節點屬性指定路徑 + +## 團隊分工 + +### 角色分配 +| 角色 | 姓名 | 職責 | +|------|------|------| +| Team Lead | - | 專案協調與進度追蹤 | +| C++ Developer | Dev1 | Maya Node 核心程式開發 | +| C++ Developer | Dev2 | License 檢查與解析模組 | +| QA Engineer | QA1 | 測試腳本撰寫與功能驗證 | +| Build Engineer | Build1 | CMake 建置系統與編譯環境 | + +### 工作流程 +1. 先由 Build Engineer 建立 CMake 專案結構 +2. C++ Developers 實作核心功能 +3. QA Engineer 撰寫測試腳本 +4. 團隊協作進行編譯與測試 + +## 檔案結構 +``` +ValidShape/ +├── CMakeLists.txt +├── CMakePresets.json +├── src/ +│ ├── ValidShapeNode.cpp +│ ├── ValidShapeNode.h +│ ├── LicenseChecker.cpp +│ └── LicenseChecker.h +├── third_party/ +│ └── (Maya SDK headers) +├── plug-ins/ +│ └── [MayaVersion]/ +├── scripts/ +├── icons/ +├── plans/ +│ └── todo.md +└── install/ + └── (編譯產出) +``` + +## 預期產出 +- `.mll` plugin 檔案 (置於 plug-ins/[Version]/) +- `.mel` AETemplate 檔案 (置於 scripts/) +- `.png` / `.svg` 圖示檔案 (置於 icons/) +- `.mod` module 檔案 (用於 MAYA_MODULE_PATH) +## 專案概述 +建立一個 Maya Deformer Node Plugin,透過 License 檔案驗證來控制網格變形效果。 + +## 技術規格 +- **開發語言**: C++ +- **建置系統**: CMake + CMakePresets.json +- **Maya 版本支援**: Maya 2022 - Maya 2026 +- **測試環境**: Maya 2023 + +## 功能需求 +1. **Deform Object**: 當 License 無效時,網格會被 Noise 變形 +2. **License 格式**: 包含有效日期 (格式: YYYY-MM-DD) +3. **License 檔案**: 透過節點屬性指定路徑 + +## 團隊分工 + +### 角色分配 +| 角色 | 姓名 | 職責 | +|------|------|------| +| Team Lead | - | 專案協調與進度追蹤 | +| C++ Developer | Dev1 | Maya Node 核心程式開發 | +| C++ Developer | Dev2 | License 檢查與解析模組 | +| QA Engineer | QA1 | 測試腳本撰寫與功能驗證 | +| Build Engineer | Build1 | CMake 建置系統與編譯環境 | + +### 工作流程 +1. 先由 Build Engineer 建立 CMake 專案結構 +2. C++ Developers 實作核心功能 +3. QA Engineer 撰寫測試腳本 +4. 團隊協作進行編譯與測試 + +## 檔案結構 +``` +ValidShape/ +├── CMakeLists.txt +├── CMakePresets.json +├── src/ +│ ├── ValidShapeNode.cpp +│ ├── ValidShapeNode.h +│ ├── LicenseChecker.cpp +│ └── LicenseChecker.h +├── third_party/ +│ └── (Maya SDK headers) +├── plug-ins/ +│ └── [MayaVersion]/ +├── scripts/ +├── icons/ +├── plans/ +│ └── todo.md +└── install/ + └── (編譯產出) +``` + +## 預期產出 +- `.mll` plugin 檔案 (置於 plug-ins/[Version]/) +- `.mel` AETemplate 檔案 (置於 scripts/) +- `.png` / `.svg` 圖示檔案 (置於 icons/) +- `.mod` module 檔案 (用於 MAYA_MODULE_PATH) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..f327121 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(ProtectAsset) +add_subdirectory(ValidAsset) \ No newline at end of file diff --git a/src/CipherProvider.h b/src/CipherProvider.h new file mode 100644 index 0000000..dc11613 --- /dev/null +++ b/src/CipherProvider.h @@ -0,0 +1,35 @@ +#ifndef CIPHER_PROVIDER_H +#define CIPHER_PROVIDER_H + +#include +#include +#include + +class CipherProvider { +public: + // 加密與解密共用同一個邏輯 (異或偏移或對稱位移) + // mode: true 為加密 (加上位移), false 為解密 (減去位移) + static MPoint processVertex(int vtxId, const MPoint& inputPt, double seed, bool encrypt) { + // 使用頂點 ID 作為隨機生成器的種子偏移,確保每個點位移不同但可預測 + std::mt19937 gen(static_cast(vtxId + seed)); + std::uniform_real_distribution dist(-5.0, 5.0); // 位移範圍 5 單位 + + double offsetX = dist(gen); + double offsetY = dist(gen); + double offsetZ = dist(gen); + + MPoint result = inputPt; + if (encrypt) { + result.x += offsetX; + result.y += offsetY; + result.z += offsetZ; + } else { + result.x -= offsetX; + result.y -= offsetY; + result.z -= offsetZ; + } + return result; + } +}; + +#endif // CIPHER_PROVIDER_H \ No newline at end of file diff --git a/src/ProtectAsset/CMakeLists.txt b/src/ProtectAsset/CMakeLists.txt new file mode 100644 index 0000000..bf4ce10 --- /dev/null +++ b/src/ProtectAsset/CMakeLists.txt @@ -0,0 +1,55 @@ +project(ProtectAsset) + +set(SOURCE_FILES + "ProtectAssetCommand.cpp" + "ProtectAssetCommand.h" + "plugin.cpp" +) + +# Find Maya SDK using custom FindMaya module +find_package(Maya REQUIRED) + +# Include directories +include_directories( + ${MAYA_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/src +) + +# Create the plugin library +add_library(ProtectAsset SHARED + ${SOURCE_FILES} +) + +# Target libraries +target_link_libraries(ProtectAsset + ${MAYA_LIBRARIES} +) + +# Platform-specific settings +if(WIN32) + target_compile_definitions(ProtectAsset PRIVATE + _CRT_SECURE_NO_WARNINGS + _USRDLL + _WINDLL + NT_PLUGIN + ) + # Ignore code page 950 (Traditional Chinese) warnings for source files + target_compile_options(ProtectAsset PRIVATE "/wd4819") + set_target_properties(ProtectAsset PROPERTIES + OUTPUT_NAME "ProtectAsset" + PREFIX "" + SUFFIX ".mll" + ) +elseif(UNIX) + set_target_properties(ProtectAsset PROPERTIES + OUTPUT_NAME "ProtectAsset" + PREFIX "" + SUFFIX ".so" + ) +endif() + +# Install rules +install(TARGETS ProtectAsset + RUNTIME DESTINATION plug-ins/${MAYA_VERSION} + LIBRARY DESTINATION plug-ins/${MAYA_VERSION} +) \ No newline at end of file diff --git a/src/ProtectAsset/ProtectAssetCommand.cpp b/src/ProtectAsset/ProtectAssetCommand.cpp new file mode 100644 index 0000000..d5150ca --- /dev/null +++ b/src/ProtectAsset/ProtectAssetCommand.cpp @@ -0,0 +1,87 @@ +#include "ProtectAssetCommand.h" +#include "CipherProvider.h" + +#include +#include +#include +#include +#include + +MStatus ProtectAssetCommand::doIt(const MArgList& args) { + MSelectionList sel; + MGlobal::getActiveSelectionList(sel); + + if (sel.length() == 0) { + // MGlobal::displayError("請選取要加密的 Mesh (Transform)。"); + return MS::kFailure; + } + + double secretSeed = 98765.4321; // 與 Deformer 相同的金鑰 + + for (unsigned int i = 0; i < sel.length(); ++i) { + MDagPath transformPath; + sel.getDagPath(i, transformPath); + + // 1. 尋找該 Transform 下的原始數據層 (intermediateObject) + MObject targetShape = findOriginalShape(transformPath); + if (targetShape.isNull()) continue; + + MFnDagNode shapeFn(targetShape); + // MGlobal::displayInfo("正在處理原始節點: " + shapeFn.fullPathName()); + + // 2. 執行物理座標加密 (直接修改 Base Mesh) + encryptBaseMesh(targetShape, secretSeed); + + // 3. 自動掛載解密 Deformer (放在 SkinCluster 之後) + // 使用 MEL 以確保變形順序正確且自動處理連線 + MString meshName = transformPath.partialPathName(); + MString melCmd = "deformer -type \"SecureDeformer\" -after " + meshName; + MStringArray res; + if (MGlobal::executeCommand(melCmd, res) == MS::kSuccess) { + // MGlobal::displayInfo("已成功掛載解密節點: " + res[0]); + + // 4. 對解密節點執行額外保護 (隱藏與鎖定) + lockDeformerNode(res[0]); + } + } + + // MGlobal::displayInfo("資產加密保護程序已完成。"); + return MS::kSuccess; +} + +MObject ProtectAssetCommand::findOriginalShape(const MDagPath& transformPath) { + unsigned int childCount = transformPath.childCount(); + MObject visibleShape; + + for (unsigned int i = 0; i < childCount; ++i) { + MObject child = transformPath.child(i); + if (!child.hasFn(MFn::kMesh)) continue; + + MFnDagNode dagFn(child); + MPlug ioPlug = dagFn.findPlug("intermediateObject", false); + bool isIo = false; + ioPlug.getValue(isIo); + + if (isIo) return child; // 優先回傳原始層 + visibleShape = child; // 備份可見層 + } + return visibleShape; // 若無 Deform 歷史,則回傳唯一 Shape +} + +void ProtectAssetCommand::encryptBaseMesh(MObject& shape, double seed) { + MItGeometry iter(shape); + for (; !iter.isDone(); iter.next()) { + MPoint pt = iter.position(); + // 加密模式 (true) + MPoint encrypted = CipherProvider::processVertex(iter.index(), pt, seed, true); + iter.setPosition(encrypted); + } +} + +void ProtectAssetCommand::lockDeformerNode(MString nodeName) { + // 鎖定節點防止刪除 + MGlobal::executeCommand("lockNode -lock 1 " + nodeName); + // 隱藏在 UI 之外 + MGlobal::executeCommand("setAttr -keyable false -channelBox false " + nodeName + ".envelope"); +} + diff --git a/src/ProtectAsset/ProtectAssetCommand.h b/src/ProtectAsset/ProtectAssetCommand.h new file mode 100644 index 0000000..449264b --- /dev/null +++ b/src/ProtectAsset/ProtectAssetCommand.h @@ -0,0 +1,14 @@ +#include +#include +#include +#include + +class ProtectAssetCommand : public MPxCommand { +public: + MStatus doIt(const MArgList& args) override; + static void* creator() { return new ProtectAssetCommand(); } + + MObject findOriginalShape(const MDagPath& transformPath); + void encryptBaseMesh(MObject& shape, double seed); + void lockDeformerNode(MString nodeName); +}; \ No newline at end of file diff --git a/src/ProtectAsset/plugin.cpp b/src/ProtectAsset/plugin.cpp new file mode 100644 index 0000000..b7f15a5 --- /dev/null +++ b/src/ProtectAsset/plugin.cpp @@ -0,0 +1,37 @@ +#include +#include +#include +#include +#include + +#include "ProtectAssetCommand.h" + +MStatus initializePlugin(MObject obj) +{ + MStatus status; + MFnPlugin plugin(obj, "ProtectAsset", "1.0", "Any", &status); + if (status != MStatus::kSuccess) { + MGlobal::displayError("Failed to initialize ProtectAsset plugin"); + return status; + } + status = plugin.registerCommand("protectAsset", ProtectAssetCommand::creator); + if (status != MStatus::kSuccess) { + MGlobal::displayError("Failed to register protectAsset command"); + return status; + } + + return status; +} + +MStatus uninitializePlugin(MObject obj) +{ + MStatus status; + MFnPlugin plugin(obj); + status = plugin.deregisterCommand("protectAsset"); + if (!status) { + status.perror("Failed to deregister protectAsset command"); + return status; + } + MGlobal::displayInfo("ProtectAsset plugin uninitialized successfully."); + return status; +} \ No newline at end of file diff --git a/src/ValidAsset/CMakeLists.txt b/src/ValidAsset/CMakeLists.txt new file mode 100644 index 0000000..c511ec1 --- /dev/null +++ b/src/ValidAsset/CMakeLists.txt @@ -0,0 +1,81 @@ +# src/CMakeLists.txt - Source files for ValidShape plugin +# +# This file defines source files for the plugin. +# The root CMakeLists.txt includes this file via add_subdirectory(). +project(ValidAsset) + +# Full paths to source files (relative to project root) +set(SOURCE_FILES + ValidShape.cpp + ValidShape.h + LicenseChecker.cpp + LicenseChecker.h + EncryptCommand.cpp + EncryptCommand.h + pluginMain.cpp +) + +# Find Maya SDK using custom FindMaya module +find_package(Maya REQUIRED) + +# Include directories +include_directories( + ${MAYA_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/src/ +) + +# Create the plugin library +add_library(ValidAsset SHARED + ${SOURCE_FILES} +) + +# Target libraries +target_link_libraries(ValidAsset + ${MAYA_LIBRARIES} +) + +# Platform-specific settings +if(WIN32) + target_compile_definitions(ValidAsset PRIVATE + _CRT_SECURE_NO_WARNINGS + _USRDLL + _WINDLL + NT_PLUGIN + ) + # Ignore code page 950 (Traditional Chinese) warnings for source files + target_compile_options(ValidAsset PRIVATE "/wd4819") + set_target_properties(ValidAsset PROPERTIES + OUTPUT_NAME "ValidShape" + PREFIX "" + SUFFIX ".mll" + ) +elseif(UNIX) + set_target_properties(ValidAsset PROPERTIES + OUTPUT_NAME "ValidShape" + PREFIX "" + SUFFIX ".so" + ) +endif() + + +# Install rules +install(TARGETS ValidAsset + RUNTIME DESTINATION plug-ins/${MAYA_VERSION} + LIBRARY DESTINATION plug-ins/${MAYA_VERSION} +) + +install(FILES scripts/AEValidShapeTemplate.mel + DESTINATION scripts +) + +install(DIRECTORY icons/ + DESTINATION icons + FILES_MATCHING PATTERN "*.png" PATTERN "*.svg" +) +# Make these variables available to parent scope +set(VALIDSHAPE_SOURCES ${VALIDSHAPE_SOURCES} PARENT_SCOPE) +set(VALIDSHAPE_HEADERS ${VALIDSHAPE_HEADERS} PARENT_SCOPE) + +# Group source files in IDEs +source_group("Source Files" FILES ${VALIDSHAPE_SOURCES}) +source_group("Header Files" FILES ${VALIDSHAPE_HEADERS}) \ No newline at end of file diff --git a/src/ValidAsset/EncryptCommand.cpp b/src/ValidAsset/EncryptCommand.cpp new file mode 100644 index 0000000..0445ddc --- /dev/null +++ b/src/ValidAsset/EncryptCommand.cpp @@ -0,0 +1,27 @@ +#include "EncryptCommand.h" +#include "CipherProvider.h" + +#include +#include + +MStatus doIt(const MArgList& args){ + MSelectionList sel; + MGlobal::getActiveSelectionList(sel); + + MDagPath path; + sel.getDagPath(0, path); + MItGeometry iter(path); + + double secretSeed = 98765.4321; + + for (; !iter.isDone(); iter.next()) { + MPoint pt = iter.position(); + // 執行加密:把正確的點「炸開」 + pt = CipherProvider::processVertex(iter.index(), pt, secretSeed, true); + iter.setPosition(pt); + } + + // 提醒使用者凍結位移 (Freeze Transformations) + MGlobal::displayInfo("Encryption Complete. Please Freeze Transformations now."); + return MS::kSuccess; +} \ No newline at end of file diff --git a/src/ValidAsset/EncryptCommand.h b/src/ValidAsset/EncryptCommand.h new file mode 100644 index 0000000..1dcb317 --- /dev/null +++ b/src/ValidAsset/EncryptCommand.h @@ -0,0 +1,20 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "CipherProvider.h" + +class EncryptCommand : public MPxCommand { +public: + MStatus doIt(const MArgList& args); + static void* creator() { return new EncryptCommand(); } +}; + diff --git a/src/ValidAsset/LicenseChecker.cpp b/src/ValidAsset/LicenseChecker.cpp new file mode 100644 index 0000000..e1ea369 --- /dev/null +++ b/src/ValidAsset/LicenseChecker.cpp @@ -0,0 +1,158 @@ +#include "LicenseChecker.h" +#include +#include +#include +#include +#include + +LicenseChecker::LicenseChecker() + : m_expirationDate(-1) + , m_lastCheckTime(0) +{ +} + +LicenseChecker::~LicenseChecker() +{ +} + +bool LicenseChecker::isValidLicense(const std::string& licenseFilePath) +{ + // Reset state + m_expirationDate = -1; + + // Try to parse the license file + if (!parseLicenseFile(licenseFilePath)) + { + return false; + } + + // Update last check time + auto now = std::chrono::system_clock::now(); + m_lastCheckTime = std::chrono::system_clock::to_time_t(now); + + // Check if expired + return !isExpired(); +} + +time_t LicenseChecker::getExpirationDate() const +{ + return m_expirationDate; +} + +time_t LicenseChecker::getLastCheckTime() const +{ + return m_lastCheckTime; +} + +bool LicenseChecker::parseLicenseFile(const std::string& licenseFilePath) +{ + std::ifstream file(licenseFilePath); + if (!file.is_open()) + { + return false; + } + + std::string line; + while (std::getline(file, line)) + { + // Trim whitespace + line.erase(line.begin(), std::find_if(line.begin(), line.end(), [](int ch) { + return !std::isspace(ch); + })); + line.erase(std::find_if(line.rbegin(), line.rend(), [](int ch) { + return !std::isspace(ch); + }).base(), line.end()); + + // Skip empty lines and comments + if (line.empty() || line[0] == '#') + { + continue; + } + + // Look for date in format EXPIRE_DATE=YYYY-MM-DD or just YYYY-MM-DD + std::string dateValue; + + // Check for EXPIRE_DATE=YYYY-MM-DD format + const std::string expirePrefix = "EXPIRE_DATE="; + if (line.compare(0, expirePrefix.length(), expirePrefix) == 0) + { + dateValue = line.substr(expirePrefix.length()); + } + // Check for DATE=YYYY-MM-DD format + else if (line.compare(0, 5, "DATE=") == 0) + { + dateValue = line.substr(5); + } + // Check for EXPIRES=YYYY-MM-DD format + else if (line.compare(0, 8, "EXPIRES=") == 0) + { + dateValue = line.substr(8); + } + // Check if the line itself is a date in YYYY-MM-DD format + else if (line.length() == 10 && line[4] == '-' && line[7] == '-') + { + dateValue = line; + } + + // If we found a date value, parse it + if (!dateValue.empty()) + { + m_expirationDate = parseDateString(dateValue); + return true; + } + } + + // If we couldn't find any date, the license is invalid + return false; +} + +time_t LicenseChecker::parseDateString(const std::string& dateString) +{ + // Expected format: YYYY-MM-DD + if (dateString.length() != 10) + { + return -1; + } + + int year, month, day; + char dash1, dash2; + + std::istringstream iss(dateString); + iss >> year >> dash1 >> month >> dash2 >> day; + + if (dash1 != '-' || dash2 != '-') + { + return -1; + } + + if (month < 1 || month > 12 || day < 1 || day > 31) + { + return -1; + } + + // Create tm structure + std::tm tm = {}; + tm.tm_year = year - 1900; // Years since 1900 + tm.tm_mon = month - 1; // Months since January + tm.tm_mday = day; + tm.tm_hour = 23; + tm.tm_min = 59; + tm.tm_sec = 59; + + // Convert to time_t + return std::mktime(&tm); +} + +bool LicenseChecker::isExpired() const +{ + if (m_expirationDate == -1) + { + return true; // No expiration date means expired + } + + auto now = std::chrono::system_clock::now(); + time_t nowTime = std::chrono::system_clock::to_time_t(now); + + // Compare with expiration date + return nowTime > m_expirationDate; +} diff --git a/src/ValidAsset/LicenseChecker.h b/src/ValidAsset/LicenseChecker.h new file mode 100644 index 0000000..e1ddfb3 --- /dev/null +++ b/src/ValidAsset/LicenseChecker.h @@ -0,0 +1,58 @@ +#ifndef LICENSE_CHECKER_H +#define LICENSE_CHECKER_H + +#include +#include + +class LicenseChecker +{ +public: + LicenseChecker(); + ~LicenseChecker(); + + /** + * Check if the license file is valid + * @param licenseFilePath Path to the license file + * @return true if license is valid (not expired), false otherwise + */ + bool isValidLicense(const std::string& licenseFilePath); + + /** + * Get the expiration date from the license file + * @return Unix timestamp of expiration date, or -1 if not found + */ + time_t getExpirationDate() const; + + /** + * Get the last check timestamp + * @return Unix timestamp of last check + */ + time_t getLastCheckTime() const; + +private: + time_t m_expirationDate; + time_t m_lastCheckTime; + + /** + * Parse the license file and extract expiration date + * Expected format: YYYY-MM-DD + * @param licenseFilePath Path to the license file + * @return true if parsing successful, false otherwise + */ + bool parseLicenseFile(const std::string& licenseFilePath); + + /** + * Convert date string to Unix timestamp + * @param dateString Date in format YYYY-MM-DD + * @return Unix timestamp + */ + time_t parseDateString(const std::string& dateString); + + /** + * Check if the license is expired + * @return true if expired, false if still valid + */ + bool isExpired() const; +}; + +#endif // LICENSE_CHECKER_H diff --git a/src/ValidAsset/ValidShape.cpp b/src/ValidAsset/ValidShape.cpp new file mode 100644 index 0000000..cc94488 --- /dev/null +++ b/src/ValidAsset/ValidShape.cpp @@ -0,0 +1,199 @@ +#include "ValidShape.h" +#include "LicenseChecker.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "CipherProvider.h" + +// Static member initialization +MTypeId ValidShape::typeId(0x0013A2F0); // Reserved Maya ID +MString ValidShape::typeName("ValidShape"); +MObject ValidShape::aLicenseFile; +MObject ValidShape::aValidLicense; + +ValidShape::ValidShape() +{ + m_licenseChecker = new LicenseChecker(); + m_deleteCallbackId = 0; +} + +// PostConstructor - called after the node is fully constructed +// This is where we can register callbacks safely +void ValidShape::postConstructor() +{ + // Get the MObject for this node + MObject thisNode = this->thisMObject(); + MFnDependencyNode depNode(thisNode); + depNode.setLocked(true); + depNode.setDoNotWrite(false); + MGlobal::executeCommand("setAttr -keyable false -channelBox false " + depNode.name() + ".visibility"); + // Register the delete callback + registerDeleteCallback(thisNode); +} + +ValidShape::~ValidShape() +{ + // Remove delete callback + removeDeleteCallback(); + + delete m_licenseChecker; + m_licenseChecker = nullptr; +} + +// DeleteAware callback - called when Maya tries to delete the node +// Returns false to prevent deletion if CGCG env var is not set +bool ValidShape::deleteAware() +{ + // Check if CGCG environment variable is set + const char* cggcValue = std::getenv("CGCG"); + + // If CGCG is not set, prevent deletion + if (cggcValue == nullptr) + { + MGlobal::displayWarning("ValidShape cannot be deleted without CGCG environment variable set."); + return false; + } + + // If CGCG is set, allow deletion + return true; +} + +// Static callback function for node about to be deleted +void nodeAboutToDeleteCallback(MObject& node, MDGModifier& modifier, void* clientData) +{ + // Check if CGCG environment variable is set + const char* cggcValue = std::getenv("CGCG"); + + // If CGCG is not set, try to prevent deletion by modifying the modifier + if (cggcValue == nullptr) + { + MGlobal::displayWarning("ValidShape cannot be deleted."); + // Note: We cannot truly prevent deletion with this callback, + // but we can display a warning + } +} + +// Register the delete callback +MCallbackId ValidShape::registerDeleteCallback(MObject& node) +{ + MStatus status; + m_deleteCallbackId = MNodeMessage::addNodeAboutToDeleteCallback( + node, + nodeAboutToDeleteCallback, + this, + &status + ); + + if (!status) + { + MGlobal::displayError("Failed to register delete callback for ValidShape."); + } + + return m_deleteCallbackId; +} + +// Remove the delete callback +void ValidShape::removeDeleteCallback() +{ + if (m_deleteCallbackId != 0) + { + MMessage::removeCallback(m_deleteCallbackId); + m_deleteCallbackId = 0; + } +} + +void* ValidShape::creator() +{ + return new ValidShape(); +} + +MStatus ValidShape::initialize() +{ + return MS::kSuccess; +} + +MStatus ValidShape::deform(MDataBlock& dataBlock, MItGeometry& iter, + const MMatrix& mat, unsigned int multiIndex) +{ + bool isLicenseValid = checkLicense(); + // 獲取 Envelope (Maya 內建屬性,0 為不作用,1 為全作用) + float env = dataBlock.inputValue(envelope).asFloat(); + if (env <= 0.001f) return MS::kSuccess; + + double secretSeed = 98765.4321; // 必須與加密工具的 Seed 一致 + + for (; !iter.isDone(); iter.next()) { + MPoint pt = iter.position(); + int id = iter.index(); + + if (isLicenseValid) { + // 執行解密:將「炸開」的點拉回正確位置 + pt = CipherProvider::processVertex(id, pt, secretSeed, false); + } else { + // 授權失敗:可以故意讓它變更亂,或者乾脆不處理(維持炸開狀態) + MGlobal::displayError("INVALID LICENSE: Asset remains encrypted."); + } + + iter.setPosition(pt); + } + + return MS::kSuccess; +} +MStatus ValidShape::connectionBroken(const MPlug& plug, const MPlug& otherPlug, bool asSrc) +{ + // 如果有人試圖斷開 inputGeom 或 outputGeom 的連線 + if (plug.attribute() == inputGeom || plug.attribute() == outputGeom) { + MGlobal::displayError("Security Violation: Connections to this node are PERMANENT."); + return MS::kFailure; // 拒絕斷開 + } + return MS::kUnknownParameter; +} + +// Perlin-like noise implementation +double ValidShape::fade(double t) const +{ + return t * t * t * (t * (t * 6.0 - 15.0) + 10.0); +} + +double ValidShape::lerp(double a, double b, double t) const +{ + return a + t * (b - a); +} + +double ValidShape::noise3D(double x, double y, double z) const +{ + // Simple 3D noise using sine waves + double value = sin(x * 1.5 + y * 2.3 + z * 0.8) * 0.5; + value += sin(x * 3.1 - y * 1.7 + z * 2.2) * 0.3; + value += sin(x * 0.9 + y * 3.8 - z * 1.3) * 0.2; + return value; +} + +double ValidShape::smoothNoise3D(double x, double y, double z) const +{ + // For more complex noise, use smooth interpolation + int xi = (int)floor(x) & 255; + int yi = (int)floor(y) & 255; + int zi = (int)floor(z) & 255; + + double xf = x - floor(x); + double yf = y - floor(y); + double zf = z - floor(z); + + double u = fade(xf); + double v = fade(yf); + double w = fade(zf); + + // This is a simplified smooth noise + return noise3D(x, y, z); +} diff --git a/src/ValidAsset/ValidShape.h b/src/ValidAsset/ValidShape.h new file mode 100644 index 0000000..2603e41 --- /dev/null +++ b/src/ValidAsset/ValidShape.h @@ -0,0 +1,71 @@ +#ifndef VALID_SHAPE_H +#define VALID_SHAPE_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// License Checker forward declaration +class LicenseChecker; + +class ValidShape : public MPxDeformerNode +{ +public: + ValidShape(); + ~ValidShape() override; + + // Maya Deformer Node virtual methods + MStatus deform(MDataBlock& dataBlock, MItGeometry& iter, + const MMatrix& mat, unsigned int multiIndex) override; + + // DeleteAware - prevent deletion when CGCG env var is not set + // Now using MNodeMessage::addNodeAboutToDeleteCallback instead + bool deleteAware(); + + // Register delete callback - called after node is fully constructed + // Note: MPxNode::postConstructor() returns void, not MStatus + void postConstructor() override; + + // Register the delete callback + MCallbackId registerDeleteCallback(MObject& node); + void removeDeleteCallback(); + + static void* creator(); + static MStatus initialize(); + + // Attribute creators + static MObject aLicenseFile; + static MObject aValidLicense; + + // Node type name + static MString typeName; + static MTypeId typeId; + +private: + // Internal methods + double noise3D(double x, double y, double z) const; + double smoothNoise3D(double x, double y, double z) const; + double fade(double t) const; + double lerp(double a, double b, double t) const; + + // License checker instance + LicenseChecker* m_licenseChecker; + + // Delete callback ID + MCallbackId m_deleteCallbackId; + virtual MStatus connectionBroken(const MPlug& plug, const MPlug& otherPlug, bool asSrc) override; + + bool checkLicense() { + // 實作您的授權判斷 + return std::getenv("CGCG_LICENSE_VALID") != nullptr; + } +}; + +#endif // VALID_SHAPE_H diff --git a/src/ValidAsset/pluginMain.cpp b/src/ValidAsset/pluginMain.cpp new file mode 100644 index 0000000..5a1dd94 --- /dev/null +++ b/src/ValidAsset/pluginMain.cpp @@ -0,0 +1,63 @@ +#include +#include +#include +#include "ValidShape.h" + +#include "EncryptCommand.h" + +// Plugin initialization +MStatus initializePlugin(MObject obj) +{ + MStatus status; + MFnPlugin plugin(obj, "ValidShape", "1.0", "Any", &status); + + if (!status) + { + MGlobal::displayError("Failed to initialize plugin: " + status.errorString()); + return status; + } + + // Register the ValidShape deformer node + status = plugin.registerNode( + ValidShape::typeName, + ValidShape::typeId, + ValidShape::creator, + ValidShape::initialize, + MPxNode::kDeformerNode + ); + + if (!status) + { + MGlobal::displayError("Failed to register ValidShape node: " + status.errorString()); + return status; + } + + MGlobal::displayInfo("ValidShape plugin initialized successfully."); + return status; +} + +// Plugin uninitialization +MStatus uninitializePlugin(MObject obj) +{ + MStatus status; + MFnPlugin plugin(obj); + + // 卸載指令 + status = plugin.deregisterCommand("protectAsset"); + if (!status) { + status.perror("無法卸載 protectAsset 指令"); + return status; + } + + // Deregister the ValidShape node + status = plugin.deregisterNode(ValidShape::typeId); + + if (!status) + { + MGlobal::displayError("Failed to deregister ValidShape node: " + status.errorString()); + return status; + } + + MGlobal::displayInfo("ValidShape plugin uninitialized successfully."); + return status; +} diff --git a/src/ValidAsset/scripts/AEValidShapeTemplate.mel b/src/ValidAsset/scripts/AEValidShapeTemplate.mel new file mode 100644 index 0000000..b963696 --- /dev/null +++ b/src/ValidAsset/scripts/AEValidShapeTemplate.mel @@ -0,0 +1,29 @@ +// AEValidShapeTemplate.mel +// Custom Attribute Editor template for ValidShape node +// This file defines the UI for the ValidShape deformer node in Maya's Attribute Editor + +global proc AEValidShapeTemplate(string $nodeName) +{ + // Call the base template + AETemplateInit; + + // Define the UI layout + editorLayout -e -mt "ValidShape" -an "ValidShape" AETemplateContentLayout; + + // License Section + editorTemplate -beginScrollLayout; + editorTemplate -beginLayout "ValidShape" -collapse false; + + // License File + editorTemplate -addControl "licenseFile" -label "License File"; + + // Valid License Status (read-only) + editorTemplate -addControl "validLicense" -label "Valid License"; + + editorTemplate -endLayout; + + // Add standard deformer attributes + AEAddStandardDeformerAttributes $nodeName; + + editorTemplate -endScrollLayout; +} diff --git a/src/ValidAsset/scripts/test_plugin_load.py b/src/ValidAsset/scripts/test_plugin_load.py new file mode 100644 index 0000000..3067c46 --- /dev/null +++ b/src/ValidAsset/scripts/test_plugin_load.py @@ -0,0 +1,44 @@ +import os +import maya.standalone +maya.standalone.initialize() + +import maya.cmds as cmds + +# Load plugin with full path +plugin_path = "d:/workspace/ValidShape/install/plug-ins/2023/ValidShape.mll" +try: + result = cmds.loadPlugin(plugin_path) + print("Plugin loaded successfully:", result) + + # List all nodes of our type (use the exact type name) + all_valid = cmds.ls(type="ValidShape") + print("All ValidShape nodes in scene:", all_valid) + + # Try to create the node with exact type name + node = cmds.createNode("ValidShape") + print("Node created successfully:", node) + + # Get all nodes of our type + all_valid = cmds.ls(type="ValidShape") + print("All ValidShape nodes in scene:", all_valid) + + # Check node type + if node: + node_type = cmds.nodeType(node) + print("Node type:", node_type) + + # Delete the node before unloading + cmds.delete(node) + print("Node deleted") + + cmds.unloadPlugin("ValidShape") + print("Unload completed") + + print("\n=== TEST PASSED ===") + print("Plugin loads correctly") + print("ValidShape node can be created") + print("Node type is correctly registered as 'ValidShape'") +except Exception as e: + print("Error:", e) + import traceback + traceback.print_exc() \ No newline at end of file diff --git a/src/ValidAsset/scripts/test_validshape.py b/src/ValidAsset/scripts/test_validshape.py new file mode 100644 index 0000000..943e98b --- /dev/null +++ b/src/ValidAsset/scripts/test_validshape.py @@ -0,0 +1,374 @@ +""" +ValidShape Plugin Test Script for Maya 2023 +This script is used to test the ValidShape Maya Deformer Node plugin. + +Usage: + 1. Copy this script to Maya scripts directory or run directly + 2. Set MAYA_MODULE_PATH to the install directory + 3. Load the plugin and run tests + +Author: QA Team +""" + +import maya.cmds as cmds +import maya.mel as mel +import os +import sys + +class ValidShapeTest: + def __init__(self): + self.plugin_loaded = False + self.test_object = None + self.test_deformer = None + self.test_license_valid = "D:/workspace/ValidShape/test_license_valid.dat" + self.test_license_expired = "D:/workspace/ValidShape/test_license_expired.dat" + + def create_test_license_files(self): + """Create test license files""" + # Valid license (future date) + with open(self.test_license_valid, 'w') as f: + f.write("# Valid License File\n") + f.write("PRODUCT=ValidShape\n") + f.write("EXPIRE_DATE=2027-12-31\n") + + # Expired license (past date) + with open(self.test_license_expired, 'w') as f: + f.write("# Expired License File\n") + f.write("PRODUCT=ValidShape\n") + f.write("EXPIRE_DATE=2020-01-01\n") + + print("Test license files created successfully!") + + def load_plugin(self): + """Load the ValidShape plugin""" + plugin_path = os.path.join(os.environ.get('MAYA_MODULE_PATH', ''), + 'plug-ins', '2023', 'ValidShape.mll') + + if not cmds.pluginInfo('ValidShape.mll', q=True, loaded=True): + try: + cmds.loadPlugin('ValidShape.mll') + self.plugin_loaded = True + print("ValidShape plugin loaded successfully!") + return True + except RuntimeError as e: + print("Failed to load plugin: {}".format(e)) + return False + else: + print("Plugin already loaded") + self.plugin_loaded = True + return True + + def create_test_mesh(self): + """Create a test mesh for deformation""" + # Create a sphere as test object + self.test_object = cmds.polySphere(r=1, sx=20, sy=20, name='testSphere')[0] + print("Test mesh created: {}".format(self.test_object)) + return self.test_object + + def apply_deformer(self, mesh_name): + """Apply ValidShape deformer to mesh""" + if not self.plugin_loaded: + print("Plugin not loaded!") + return None + + # Create the deformer + self.test_deformer = cmds.deformableShape(mesh_name, + type='ValidShape', + name='ValidShapeDeform')[0] + print("ValidShape deformer applied: {}".format(self.test_deformer)) + return self.test_deformer + + def set_license_file(self, deformer_name, license_path): + """Set the license file for the deformer""" + if cmds.objExists(deformer_name + '.licenseFile'): + cmds.setAttr(deformer_name + '.licenseFile', license_path, type='string') + print("License file set to: {}".format(license_path)) + return True + return False + + def test_valid_license(self): + """Test with valid license""" + print("\n=== Testing Valid License ===") + + # Create test mesh if not exists + if not self.test_object: + self.create_test_mesh() + + # Apply deformer + if not self.test_deformer: + self.apply_deformer(self.test_object) + + # Set valid license + self.set_license_file(self.test_deformer, self.test_license_valid) + + # Get valid license status + is_valid = cmds.getAttr(self.test_deformer + '.validLicense') + print("Valid License Status: {}".format(is_valid)) + + # The mesh should NOT be deformed when license is valid + return is_valid == True + + def test_expired_license(self): + """Test with expired license""" + print("\n=== Testing Expired License ===") + + # Set expired license + self.set_license_file(self.test_deformer, self.test_license_expired) + + # Get valid license status + is_valid = cmds.getAttr(self.test_deformer + '.validLicense') + print("Valid License Status: {}".format(is_valid)) + + # The mesh SHOULD be deformed when license is expired + return is_valid == False + + def test_noise_parameters(self): + """Test noise parameter adjustments""" + print("\n=== Testing Noise Parameters ===") + + if not self.test_deformer: + print("No deformer to test!") + return False + + # Set noise amplitude + cmds.setAttr(self.test_deformer + '.noiseAmplitude', 1.0) + amp = cmds.getAttr(self.test_deformer + '.noiseAmplitude') + print("Noise Amplitude: {}".format(amp)) + + # Set noise frequency + cmds.setAttr(self.test_deformer + '.noiseFrequency', 2.0) + freq = cmds.getAttr(self.test_deformer + '.noiseFrequency') + print("Noise Frequency: {}".format(freq)) + + return True + + def run_all_tests(self): + """Run all tests""" + print("=" * 50) + print("Starting ValidShape Plugin Tests") + print("=" * 50) + + # Create test license files + self.create_test_license_files() + + # Load plugin + if not self.load_plugin(): + print("Plugin loading failed, aborting tests!") + return False + + # Run tests + test1 = self.test_valid_license() + test2 = self.test_expired_license() + test3 = self.test_noise_parameters() + + # Print results + print("\n" + "=" * 50) + print("Test Results:") + print(" Valid License Test: {}".format("PASS" if test1 else "FAIL")) + print(" Expired License Test: {}".format("PASS" if test2 else "FAIL")) + print(" Noise Parameters Test: {}".format("PASS" if test3 else "FAIL")) + print("=" * 50) + + return test1 and test2 and test3 + +def run_tests(): + """Main entry point to run tests""" + tester = ValidShapeTest() + result = tester.run_all_tests() + + if result: + print("\nAll tests PASSED!") + else: + print("\nSome tests FAILED!") + + return result + +if __name__ == "__main__": + run_tests() +ValidShape Plugin Test Script for Maya 2023 +This script is used to test the ValidShape Maya Deformer Node plugin. + +Usage: + 1. Copy this script to Maya scripts directory or run directly + 2. Set MAYA_MODULE_PATH to the install directory + 3. Load the plugin and run tests + +Author: QA Team +""" + +import maya.cmds as cmds +import maya.mel as mel +import os +import sys + +class ValidShapeTest: + def __init__(self): + self.plugin_loaded = False + self.test_object = None + self.test_deformer = None + self.test_license_valid = "D:/workspace/ValidShape/test_license_valid.dat" + self.test_license_expired = "D:/workspace/ValidShape/test_license_expired.dat" + + def create_test_license_files(self): + """Create test license files""" + # Valid license (future date) + with open(self.test_license_valid, 'w') as f: + f.write("# Valid License File\n") + f.write("PRODUCT=ValidShape\n") + f.write("EXPIRE_DATE=2027-12-31\n") + + # Expired license (past date) + with open(self.test_license_expired, 'w') as f: + f.write("# Expired License File\n") + f.write("PRODUCT=ValidShape\n") + f.write("EXPIRE_DATE=2020-01-01\n") + + print("Test license files created successfully!") + + def load_plugin(self): + """Load the ValidShape plugin""" + plugin_path = os.path.join(os.environ.get('MAYA_MODULE_PATH', ''), + 'plug-ins', '2023', 'ValidShape.mll') + + if not cmds.pluginInfo('ValidShape.mll', q=True, loaded=True): + try: + cmds.loadPlugin('ValidShape.mll') + self.plugin_loaded = True + print("ValidShape plugin loaded successfully!") + return True + except RuntimeError as e: + print("Failed to load plugin: {}".format(e)) + return False + else: + print("Plugin already loaded") + self.plugin_loaded = True + return True + + def create_test_mesh(self): + """Create a test mesh for deformation""" + # Create a sphere as test object + self.test_object = cmds.polySphere(r=1, sx=20, sy=20, name='testSphere')[0] + print("Test mesh created: {}".format(self.test_object)) + return self.test_object + + def apply_deformer(self, mesh_name): + """Apply ValidShape deformer to mesh""" + if not self.plugin_loaded: + print("Plugin not loaded!") + return None + + # Create the deformer + self.test_deformer = cmds.deformableShape(mesh_name, + type='ValidShape', + name='ValidShapeDeform')[0] + print("ValidShape deformer applied: {}".format(self.test_deformer)) + return self.test_deformer + + def set_license_file(self, deformer_name, license_path): + """Set the license file for the deformer""" + if cmds.objExists(deformer_name + '.licenseFile'): + cmds.setAttr(deformer_name + '.licenseFile', license_path, type='string') + print("License file set to: {}".format(license_path)) + return True + return False + + def test_valid_license(self): + """Test with valid license""" + print("\n=== Testing Valid License ===") + + # Create test mesh if not exists + if not self.test_object: + self.create_test_mesh() + + # Apply deformer + if not self.test_deformer: + self.apply_deformer(self.test_object) + + # Set valid license + self.set_license_file(self.test_deformer, self.test_license_valid) + + # Get valid license status + is_valid = cmds.getAttr(self.test_deformer + '.validLicense') + print("Valid License Status: {}".format(is_valid)) + + # The mesh should NOT be deformed when license is valid + return is_valid == True + + def test_expired_license(self): + """Test with expired license""" + print("\n=== Testing Expired License ===") + + # Set expired license + self.set_license_file(self.test_deformer, self.test_license_expired) + + # Get valid license status + is_valid = cmds.getAttr(self.test_deformer + '.validLicense') + print("Valid License Status: {}".format(is_valid)) + + # The mesh SHOULD be deformed when license is expired + return is_valid == False + + def test_noise_parameters(self): + """Test noise parameter adjustments""" + print("\n=== Testing Noise Parameters ===") + + if not self.test_deformer: + print("No deformer to test!") + return False + + # Set noise amplitude + cmds.setAttr(self.test_deformer + '.noiseAmplitude', 1.0) + amp = cmds.getAttr(self.test_deformer + '.noiseAmplitude') + print("Noise Amplitude: {}".format(amp)) + + # Set noise frequency + cmds.setAttr(self.test_deformer + '.noiseFrequency', 2.0) + freq = cmds.getAttr(self.test_deformer + '.noiseFrequency') + print("Noise Frequency: {}".format(freq)) + + return True + + def run_all_tests(self): + """Run all tests""" + print("=" * 50) + print("Starting ValidShape Plugin Tests") + print("=" * 50) + + # Create test license files + self.create_test_license_files() + + # Load plugin + if not self.load_plugin(): + print("Plugin loading failed, aborting tests!") + return False + + # Run tests + test1 = self.test_valid_license() + test2 = self.test_expired_license() + test3 = self.test_noise_parameters() + + # Print results + print("\n" + "=" * 50) + print("Test Results:") + print(" Valid License Test: {}".format("PASS" if test1 else "FAIL")) + print(" Expired License Test: {}".format("PASS" if test2 else "FAIL")) + print(" Noise Parameters Test: {}".format("PASS" if test3 else "FAIL")) + print("=" * 50) + + return test1 and test2 and test3 + +def run_tests(): + """Main entry point to run tests""" + tester = ValidShapeTest() + result = tester.run_all_tests() + + if result: + print("\nAll tests PASSED!") + else: + print("\nSome tests FAILED!") + + return result + +if __name__ == "__main__": + run_tests() +