Merge pull request #5820 from ynput/enhancement/OP-7072_Validate-Loaded-Plugins

Max: Validate loaded plugins tweaks
This commit is contained in:
Kayla Man 2023-11-17 22:11:49 +08:00 committed by GitHub
commit ccdda3d817
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 217 additions and 49 deletions

View file

@ -0,0 +1,133 @@
# -*- coding: utf-8 -*-
"""Validator for Loaded Plugin."""
import os
import pyblish.api
from pymxs import runtime as rt
from openpype.pipeline.publish import (
RepairAction,
OptionalPyblishPluginMixin,
PublishValidationError
)
from openpype.hosts.max.api.lib import get_plugins
class ValidateLoadedPlugin(OptionalPyblishPluginMixin,
pyblish.api.InstancePlugin):
"""Validates if the specific plugin is loaded in 3ds max.
Studio Admin(s) can add the plugins they want to check in validation
via studio defined project settings
"""
order = pyblish.api.ValidatorOrder
hosts = ["max"]
label = "Validate Loaded Plugins"
optional = True
actions = [RepairAction]
family_plugins_mapping = {}
@classmethod
def get_invalid(cls, instance):
"""Plugin entry point."""
family_plugins_mapping = cls.family_plugins_mapping
if not family_plugins_mapping:
return
invalid = []
# Find all plug-in requirements for current instance
instance_families = {instance.data["family"]}
instance_families.update(instance.data.get("families", []))
cls.log.debug("Checking plug-in validation "
f"for instance families: {instance_families}")
all_required_plugins = set()
for mapping in family_plugins_mapping:
# Check for matching families
if not mapping:
return
match_families = {fam.strip() for fam in mapping["families"]}
has_match = "*" in match_families or match_families.intersection(
instance_families)
if not has_match:
continue
cls.log.debug(
f"Found plug-in family requirements: {match_families}")
required_plugins = [
# match lowercase and format with os.environ to allow
# plugin names defined by max version, e.g. {3DSMAX_VERSION}
plugin.format(**os.environ).lower()
for plugin in mapping["plugins"]
# ignore empty fields in settings
if plugin.strip()
]
all_required_plugins.update(required_plugins)
if not all_required_plugins:
# Instance has no plug-in requirements
return
# get all DLL loaded plugins in Max and their plugin index
available_plugins = {
plugin_name.lower(): index for index, plugin_name in enumerate(
get_plugins())
}
# validate the required plug-ins
for plugin in sorted(all_required_plugins):
plugin_index = available_plugins.get(plugin)
if plugin_index is None:
debug_msg = (
f"Plugin {plugin} does not exist"
" in 3dsMax Plugin List."
)
invalid.append((plugin, debug_msg))
continue
if not rt.pluginManager.isPluginDllLoaded(plugin_index):
debug_msg = f"Plugin {plugin} not loaded."
invalid.append((plugin, debug_msg))
return invalid
def process(self, instance):
if not self.is_active(instance.data):
self.log.debug("Skipping Validate Loaded Plugin...")
return
invalid = self.get_invalid(instance)
if invalid:
bullet_point_invalid_statement = "\n".join(
"- {}".format(message) for _, message in invalid
)
report = (
"Required plugins are not loaded.\n\n"
f"{bullet_point_invalid_statement}\n\n"
"You can use repair action to load the plugin."
)
raise PublishValidationError(
report, title="Missing Required Plugins")
@classmethod
def repair(cls, instance):
# get all DLL loaded plugins in Max and their plugin index
invalid = cls.get_invalid(instance)
if not invalid:
return
# get all DLL loaded plugins in Max and their plugin index
available_plugins = {
plugin_name.lower(): index for index, plugin_name in enumerate(
get_plugins())
}
for invalid_plugin, _ in invalid:
plugin_index = available_plugins.get(invalid_plugin)
if plugin_index is None:
cls.log.warning(
f"Can't enable missing plugin: {invalid_plugin}")
continue
if not rt.pluginManager.isPluginDllLoaded(plugin_index):
rt.pluginManager.loadPluginDll(plugin_index)

View file

@ -1,49 +0,0 @@
# -*- coding: utf-8 -*-
"""Validator for USD plugin."""
from pyblish.api import InstancePlugin, ValidatorOrder
from pymxs import runtime as rt
from openpype.pipeline import (
OptionalPyblishPluginMixin,
PublishValidationError
)
def get_plugins() -> list:
"""Get plugin list from 3ds max."""
manager = rt.PluginManager
count = manager.pluginDllCount
plugin_info_list = []
for p in range(1, count + 1):
plugin_info = manager.pluginDllName(p)
plugin_info_list.append(plugin_info)
return plugin_info_list
class ValidateUSDPlugin(OptionalPyblishPluginMixin,
InstancePlugin):
"""Validates if USD plugin is installed or loaded in 3ds max."""
order = ValidatorOrder - 0.01
families = ["model"]
hosts = ["max"]
label = "Validate USD Plugin loaded"
optional = True
def process(self, instance):
"""Plugin entry point."""
for sc in ValidateUSDPlugin.__subclasses__():
self.log.info(sc)
if not self.is_active(instance.data):
return
plugin_info = get_plugins()
usd_import = "usdimport.dli"
if usd_import not in plugin_info:
raise PublishValidationError(f"USD Plugin {usd_import} not found")
usd_export = "usdexport.dle"
if usd_export not in plugin_info:
raise PublishValidationError(f"USD Plugin {usd_export} not found")

View file

@ -651,6 +651,13 @@ def _convert_3dsmax_project_settings(ayon_settings, output):
attributes = {}
ayon_publish["ValidateAttributes"]["attributes"] = attributes
if "ValidateLoadedPlugin" in ayon_publish:
loaded_plugin = (
ayon_publish["ValidateLoadedPlugin"]["family_plugins_mapping"]
)
for item in loaded_plugin:
item["families"] = item.pop("product_types")
output["max"] = ayon_max

View file

@ -51,6 +51,11 @@
"ValidateAttributes": {
"enabled": false,
"attributes": {}
},
"ValidateLoadedPlugin": {
"enabled": false,
"optional": true,
"family_plugins_mapping": []
}
}
}

View file

@ -47,6 +47,49 @@
"label": "Attributes"
}
]
},
{
"type": "dict",
"collapsible": true,
"key": "ValidateLoadedPlugin",
"label": "Validate Loaded Plugin",
"checkbox_key": "enabled",
"children": [
{
"type": "boolean",
"key": "enabled",
"label": "Enabled"
},
{
"type": "boolean",
"key": "optional",
"label": "Optional"
},
{
"type": "list",
"collapsible": true,
"key": "family_plugins_mapping",
"label": "Family Plugins Mapping",
"use_label_wrap": true,
"object_type": {
"type": "dict",
"children": [
{
"key": "families",
"label": "Famiies",
"type": "list",
"object_type": "text"
},
{
"key": "plugins",
"label": "Plugins",
"type": "list",
"object_type": "text"
}
]
}
}
]
}
]
}

View file

@ -27,6 +27,26 @@ class ValidateAttributesModel(BaseSettingsModel):
return value
class FamilyMappingItemModel(BaseSettingsModel):
product_types: list[str] = Field(
default_factory=list,
title="Product Types"
)
plugins: list[str] = Field(
default_factory=list,
title="Plugins"
)
class ValidateLoadedPluginModel(BaseSettingsModel):
enabled: bool = Field(title="Enabled")
optional: bool = Field(title="Optional")
family_plugins_mapping: list[FamilyMappingItemModel] = Field(
default_factory=list,
title="Family Plugins Mapping"
)
class BasicValidateModel(BaseSettingsModel):
enabled: bool = Field(title="Enabled")
optional: bool = Field(title="Optional")
@ -44,6 +64,10 @@ class PublishersModel(BaseSettingsModel):
title="Validate Attributes"
)
ValidateLoadedPlugin: ValidateLoadedPluginModel = Field(
default_factory=ValidateLoadedPluginModel,
title="Validate Loaded Plugin"
)
DEFAULT_PUBLISH_SETTINGS = {
"ValidateFrameRange": {
@ -55,4 +79,9 @@ DEFAULT_PUBLISH_SETTINGS = {
"enabled": False,
"attributes": "{}"
},
"ValidateLoadedPlugin": {
"enabled": False,
"optional": True,
"family_plugins_mapping": []
}
}