├── .gitignore ├── .vscode └── settings.json ├── Content └── Python │ ├── Examples │ ├── __init__.py │ └── post_import_texture2D_settings.py │ ├── ImporterRules │ ├── Actions.py │ ├── Manager.py │ ├── Queries.py │ ├── Rules.py │ └── __init__.py │ └── init_unreal.py ├── Documentation └── editor_properties.png ├── ImporterRulesPython.code-workspace ├── ImporterRulesPython.uplugin ├── LICENSE ├── README.md └── Resources └── Icon128.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Visual Studio 2015 user specific files 2 | .vs/ 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | 22 | # Compiled Static libraries 23 | *.lai 24 | *.la 25 | *.a 26 | *.lib 27 | 28 | # Executables 29 | *.exe 30 | *.out 31 | *.app 32 | *.ipa 33 | 34 | # These project files can be generated by the engine 35 | *.xcodeproj 36 | *.xcworkspace 37 | *.sln 38 | *.suo 39 | *.opensdf 40 | *.sdf 41 | *.VC.db 42 | *.VC.opendb 43 | 44 | # Precompiled Assets 45 | SourceArt/**/*.png 46 | SourceArt/**/*.tga 47 | 48 | # Binary Files 49 | Binaries/* 50 | Plugins/*/Binaries/* 51 | 52 | # Builds 53 | Build/* 54 | 55 | # Whitelist PakBlacklist-.txt files 56 | !Build/*/ 57 | Build/*/** 58 | !Build/*/PakBlacklist*.txt 59 | 60 | # Don't ignore icon files in Build 61 | !Build/**/*.ico 62 | 63 | # Built data for maps 64 | *_BuiltData.uasset 65 | 66 | # Configuration files generated by the Editor 67 | Saved/* 68 | 69 | # Compiled source files for the engine to use 70 | Intermediate/* 71 | Plugins/*/Intermediate/* 72 | 73 | # Cache files for the editor to use 74 | DerivedDataCache/* 75 | 76 | *.pyc 77 | __pycache__/* -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.analysis.typeCheckingMode": "basic", 3 | "python.analysis.extraPaths": [ 4 | "..\\..\\Intermediate\\PythonStub" 5 | ], 6 | "python.defaultInterpreterPath": "E:\\epic\\UE_5.1\\Engine\\Binaries\\ThirdParty\\Python3\\Win64\\python.exe" 7 | } -------------------------------------------------------------------------------- /Content/Python/Examples/__init__.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Ryan DowlingSoka 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. -------------------------------------------------------------------------------- /Content/Python/Examples/post_import_texture2D_settings.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Ryan DowlingSoka 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | ''' 24 | This example file shows a simple set of rules applying to imported assets of the type: "Texture2D" 25 | By importing this module in an init_unreal these rules will get applied to any newly imported assets. 26 | ''' 27 | 28 | from ImporterRules import * 29 | import unreal 30 | 31 | importer_rules_manager.register_rules( 32 | class_type = unreal.Texture2D, 33 | rules = [ 34 | # The first rule is simple, it takes any textures ending with _n and applies the flip_green_channel property as false. 35 | # You might do something like this if you want to switch from DirectX to OpenGL normals. 36 | # There is only one rule, so the requires_all parameter is irrelevant. 37 | Rule( 38 | queries=[ 39 | SourcePath(file_name_ends_with="_n"), 40 | ], 41 | actions=[SetEditorProperties(flip_green_channel=False)], 42 | ), 43 | 44 | # This second rule shows how you can put several queries together. Because the requires_all parameter is 'False' 45 | # this rule will fire if ANY of the source path queries are true. So if the texture ends with _n, _o, _h, _r, _m 46 | # then this rule will remove the sRGB property from those textures. 47 | Rule( 48 | queries=[ 49 | SourcePath(file_name_ends_with="_n"), 50 | SourcePath(file_name_ends_with="_o"), 51 | SourcePath(file_name_ends_with="_h"), 52 | SourcePath(file_name_ends_with="_r"), 53 | SourcePath(file_name_ends_with="_m"), 54 | ], 55 | actions=[SetEditorProperties(srgb=False)], 56 | requires_all=False 57 | ), 58 | 59 | # The third rule is targeting more specifically. While the previous rules have been requires_all = False, this rule is True 60 | # so now both the SourcePath and DestinationPath queries must come back true for the action to be applied. 61 | # In this example the texture must have the suffix _d and be in a folder named /TestFolder/ somewhere in its path hierarchy 62 | # to pass. 63 | # You can see that the SetEditorProperties takes two property names as well. 64 | Rule( 65 | queries=[ 66 | SourcePath(file_name_ends_with="_d"), 67 | DestinationPath(path_contains="/TestFolder/"), 68 | ], 69 | actions=[SetEditorProperties(srgb=False, lod_bias=5)], 70 | requires_all=True 71 | ), 72 | 73 | # This rule is similar to the previous, but the SetEditorProperties has been broken up into two actions, just like queries 74 | # you aren't limited to one action at a time. 75 | # In addition, this rule has the apply_on_reimport parameter set to True: which will cause this rule to run each time the 76 | # asset is reimported, and not just the first time. 77 | Rule( 78 | queries=[ 79 | SourcePath(file_name_ends_with="_test"), 80 | DestinationPath(path_contains="/TestFolder/"), 81 | ], 82 | actions=[SetEditorProperties(srgb=False), SetAssetTags({"obsolete":True})], 83 | requires_all=True, 84 | apply_on_reimport=True, 85 | ), 86 | ], 87 | ) 88 | 89 | unreal.log("Registered Texture Post Import Rules!") -------------------------------------------------------------------------------- /Content/Python/ImporterRules/Actions.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Ryan DowlingSoka 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from abc import ABC 24 | import unreal 25 | from traceback import format_exc 26 | from typing import Dict, Any 27 | 28 | 29 | class ImportActionBase(ABC): 30 | """Base import action class stub.""" 31 | 32 | def apply(self, factory: unreal.Factory, created_object: unreal.Object) -> bool: 33 | raise NotImplemented 34 | 35 | 36 | class SetEditorProperties(ImportActionBase): 37 | """Generic import action to set editor properties with the set_editor_properties(dict) function.""" 38 | 39 | def __init__(self, **kwargs) -> None: 40 | self.editor_properties = kwargs 41 | 42 | def apply(self, factory: unreal.Factory, created_object: unreal.Object) -> bool: 43 | if created_object is None: 44 | return False 45 | created_object.set_editor_properties(self.editor_properties) 46 | unreal.log( f"Applied the following editor properties to {created_object.get_full_name()}") 47 | unreal.log( self.editor_properties ) 48 | return True 49 | 50 | 51 | class SetAssetTags(ImportActionBase): 52 | """Generic import action to set asset tag data on the asset on import.""" 53 | 54 | def __init__(self, asset_tags:Dict[str,Any]) -> None: 55 | self.asset_tags = asset_tags 56 | 57 | def apply(self, factory: unreal.Factory, created_object: unreal.Object) -> bool: 58 | if created_object is None: 59 | return False 60 | 61 | eas = unreal.get_editor_subsystem(unreal.EditorAssetSubsystem) 62 | if eas: 63 | for key,value in self.asset_tags.items(): 64 | eas.set_metadata_tag(created_object, key, str(value)) 65 | 66 | return True 67 | -------------------------------------------------------------------------------- /Content/Python/ImporterRules/Manager.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Ryan DowlingSoka 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import unreal 24 | from typing import Dict, List 25 | from ImporterRules.Rules import ImportRuleBase 26 | from ImporterRules.Actions import SetAssetTags 27 | from ImporterRules.Queries import CheckAssetTag 28 | 29 | 30 | class ImporterRulesManager(object): 31 | """Manager class to make it easier to add importer rules registrations.""" 32 | 33 | def __init__(self) -> None: 34 | self.import_subsystem = unreal.get_editor_subsystem(unreal.ImportSubsystem) 35 | if self.import_subsystem: 36 | self.import_subsystem.on_asset_post_import.add_callable( 37 | self.on_asset_post_import 38 | ) 39 | self._rules: Dict[type, List[ImportRuleBase]] = {} 40 | self._set_imported_asset_tag_action = SetAssetTags({"importer_rules_applied":"True"}) 41 | self._check_imported_asset_tag_action = CheckAssetTag("importer_rules_applied","True") 42 | 43 | def on_asset_post_import( 44 | self, factory: unreal.Factory, created_object: unreal.Object 45 | ): 46 | # previously had rules run on this asset: 47 | is_reimport = self._check_imported_asset_tag_action.test(factory, created_object) 48 | 49 | for supported_class in self._rules.keys(): 50 | if isinstance(created_object, supported_class): 51 | for rule in self._rules[supported_class]: 52 | if not is_reimport or rule.apply_on_reimport: 53 | rule.apply(factory, created_object) 54 | self._set_imported_asset_tag_action.apply(factory, created_object) 55 | 56 | 57 | def register_rule(self, class_type: type, rule: ImportRuleBase): 58 | if class_type in self._rules: 59 | self._rules[class_type].append(rule) 60 | else: 61 | self._rules[class_type] = [rule] 62 | 63 | def register_rules(self, class_type: type, rules: List[ImportRuleBase]): 64 | if class_type in self._rules: 65 | self._rules[class_type] += rules 66 | else: 67 | self._rules[class_type] = rules 68 | 69 | 70 | importer_rules_manager = ImporterRulesManager() 71 | -------------------------------------------------------------------------------- /Content/Python/ImporterRules/Queries.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Ryan DowlingSoka 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from abc import ABC 24 | import unreal 25 | from typing import List, cast, Any 26 | import os.path 27 | 28 | 29 | class QueryBase(ABC): 30 | """Base class for import queries.""" 31 | 32 | def test(self, factory: unreal.Factory, created_object: unreal.Object) -> bool: 33 | """Test the created object and factory against this query.""" 34 | raise NotImplemented 35 | 36 | 37 | class SourcePath(QueryBase): 38 | """Query an imported factory and file path""" 39 | 40 | def __init__( 41 | self, 42 | file_name_starts_with: str = "", 43 | file_name_ends_with: str = "", 44 | file_name_contains: str = "", 45 | full_path_contains: str = "", 46 | extensions: List[str] = [], 47 | requires_all: bool = False, 48 | case_sensitive: bool = False, 49 | ) -> None: 50 | self.case_sensitive = case_sensitive 51 | 52 | # if case sensitive we use the substring as is, if not we lowercase it for future comparisons. 53 | self.file_name_starts_with = ( 54 | file_name_starts_with 55 | if self.case_sensitive 56 | else file_name_starts_with.lower() 57 | ) 58 | self.file_name_ends_with = ( 59 | file_name_ends_with if self.case_sensitive else file_name_ends_with.lower() 60 | ) 61 | self.file_name_contains = ( 62 | file_name_contains if self.case_sensitive else file_name_contains.lower() 63 | ) 64 | self.full_path_contains = ( 65 | full_path_contains if self.case_sensitive else full_path_contains.lower() 66 | ) 67 | self.extensions = [ext.lower() for ext in extensions] 68 | self.requires_all = requires_all 69 | 70 | def test(self, factory: unreal.Factory, created_object: unreal.Object) -> bool: 71 | if created_object is None: 72 | return False 73 | 74 | # if the created_object doesn't implement asset_import_data we early out. 75 | if not has_editor_property(created_object, "asset_import_data"): 76 | return False 77 | 78 | asset_import_data = cast( 79 | unreal.AssetImportData, created_object.get_editor_property("asset_import_data") 80 | ) 81 | file_path = asset_import_data.get_first_filename() 82 | 83 | # if case sensitive we use the filename raw, if not we lowercase for the future comparisons. 84 | file_path = file_path if self.case_sensitive else file_path.lower() 85 | file_name, extension = os.path.splitext(os.path.basename(file_path)) 86 | 87 | results = [] 88 | 89 | if self.file_name_starts_with: 90 | results.append(file_name.startswith(self.file_name_starts_with)) 91 | 92 | if self.file_name_ends_with: 93 | results.append(file_name.endswith(self.file_name_ends_with)) 94 | 95 | if self.file_name_contains: 96 | results.append(self.file_name_contains in file_name) 97 | 98 | if self.full_path_contains: 99 | results.append(self.full_path_contains in file_name) 100 | 101 | if self.extensions: 102 | results.append(extension in self.extensions) 103 | 104 | if self.requires_all: 105 | return all(results) 106 | 107 | return any(results) 108 | 109 | 110 | class DestinationPath(QueryBase): 111 | 112 | """Query based on the path the created object ends up in.""" 113 | 114 | def __init__( 115 | self, 116 | path_contains: str = "", 117 | case_sensitive: bool = False, 118 | ) -> None: 119 | self.case_sensitive = case_sensitive 120 | self.destination_path_contains = ( 121 | path_contains if case_sensitive else path_contains.lower() 122 | ) 123 | 124 | def test(self, factory: unreal.Factory, created_object: unreal.Object) -> bool: 125 | if created_object is None: # Early out, can't do destination path comparisons. 126 | return False 127 | 128 | destination_path = ( 129 | created_object.get_path_name() 130 | if self.case_sensitive 131 | else created_object.get_path_name().lower() 132 | ) 133 | 134 | if self.destination_path_contains: 135 | return self.destination_path_contains in destination_path 136 | 137 | return False 138 | 139 | class CheckAssetTag(QueryBase): 140 | 141 | """Query based on the asset tags of the created object. Optional asset_tag_value parameter will do a string equality compare. 142 | If left empty, then the test will only look to see if the tag exists.""" 143 | 144 | def __init__( 145 | self, 146 | asset_tag_key:str, 147 | asset_tag_value:Any = None, 148 | ) -> None: 149 | self.asset_tag_key = asset_tag_key 150 | self.asset_tag_value = asset_tag_value 151 | 152 | def test(self, factory: unreal.Factory, created_object: unreal.Object) -> bool: 153 | if created_object is None: 154 | return False 155 | 156 | eas = unreal.get_editor_subsystem(unreal.EditorAssetSubsystem) 157 | if eas: 158 | value = eas.get_metadata_tag(created_object, self.asset_tag_key) 159 | if value == "": 160 | return False 161 | return self.asset_tag_value is None or str(self.asset_tag_value) == value 162 | return False 163 | 164 | 165 | 166 | def has_editor_property(created_object: unreal.Object, editor_property: str) -> bool: 167 | try: 168 | obj = created_object.get_editor_property(editor_property) 169 | return True 170 | except: 171 | return False 172 | -------------------------------------------------------------------------------- /Content/Python/ImporterRules/Rules.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Ryan DowlingSoka 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from abc import ABC 24 | from ImporterRules.Actions import ImportActionBase 25 | from ImporterRules.Queries import QueryBase 26 | import unreal 27 | from typing import List 28 | from traceback import format_exc 29 | 30 | class ImportRuleBase(ABC): 31 | """Base class for rules, to be applied """ 32 | 33 | def __init__(self, apply_on_reimport:bool=False) -> None: 34 | self.apply_on_reimport = apply_on_reimport 35 | 36 | def apply(self, factory: unreal.Factory, created_object: unreal.Object) -> bool: 37 | raise NotImplemented 38 | 39 | class Rule(ImportRuleBase): 40 | """Import Rule class to apply actions if designated queries are true.""" 41 | 42 | def __init__(self, queries:List[QueryBase], actions:List[ImportActionBase], requires_all:bool=False, apply_on_reimport:bool=False) -> None: 43 | super().__init__(apply_on_reimport) 44 | self.queries = queries 45 | self.actions = actions 46 | self.requires_all = requires_all 47 | 48 | def apply(self, factory: unreal.Factory, created_object: unreal.Object) -> bool: 49 | results = [query.test(factory, created_object) for query in self.queries] 50 | 51 | if len(results) > 0: 52 | if self.requires_all: 53 | if not all(results): 54 | return False 55 | else: 56 | if not any(results): 57 | return False 58 | 59 | try: 60 | action_results = [action.apply(factory, created_object) for action in self.actions] 61 | except Exception as err: 62 | unreal.log_error(f"Failed to run actions on {created_object.get_name()}") 63 | error_message = format_exc() 64 | unreal.log_error(error_message) 65 | unreal.EditorDialog.show_message(title="Import Action Error", message=f"{error_message}", message_type=unreal.AppMsgType.OK, default_value=unreal.AppReturnType.OK) 66 | return False 67 | return all(action_results) -------------------------------------------------------------------------------- /Content/Python/ImporterRules/__init__.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Ryan DowlingSoka 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | """ 24 | Importer Rules is a simple, bare bones, python frame work for handling data transformations on imported assets on import. 25 | A user can define a "Rule" by ascribing "Queries" that the incoming asset must match before subsequent "Actions" are applied. 26 | You can write your own rules, queries, and actions or use the existing generic ones to modify assets that need updating. 27 | """ 28 | 29 | from ImporterRules.Manager import importer_rules_manager 30 | from ImporterRules.Actions import SetEditorProperties, SetAssetTags 31 | from ImporterRules.Queries import SourcePath, DestinationPath 32 | from ImporterRules.Rules import Rule -------------------------------------------------------------------------------- /Content/Python/init_unreal.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) 2023 Ryan DowlingSoka 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | from unreal import log_error 24 | from traceback import print_exc 25 | 26 | # @NOTE: A good practice is to wrap your individual (and stand alone) 27 | # and modules in following try clause, this allows your other modules 28 | # to still run even if one of the modules has an error. 29 | 30 | try: 31 | # Uncomment the next line to use the test file. 32 | # import Examples.post_import_texture2D_settings 33 | pass 34 | except Exception as err: 35 | log_error("Plugin Importer Rules failed to initialize.") 36 | print_exc() 37 | -------------------------------------------------------------------------------- /Documentation/editor_properties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ryan-DowlingSoka/UnrealImporterRules-Python/6d0087ff24d96573d9731c05b1f61527eacc437b/Documentation/editor_properties.png -------------------------------------------------------------------------------- /ImporterRulesPython.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": {} 8 | } -------------------------------------------------------------------------------- /ImporterRulesPython.uplugin: -------------------------------------------------------------------------------- 1 | { 2 | "FileVersion": 3, 3 | "Version": 1, 4 | "VersionName": "1.0", 5 | "FriendlyName": "ImporterRules (Python)", 6 | "Description": "Example project showing how to do rules based import settings to assets from python.", 7 | "Category": "Other", 8 | "CreatedBy": "Ryan DowlingSoka", 9 | "CreatedByURL": "https://ryandowlingsoka.com", 10 | "DocsURL": "", 11 | "MarketplaceURL": "", 12 | "SupportURL": "", 13 | "CanContainContent": true, 14 | "IsBetaVersion": false, 15 | "IsExperimentalVersion": false, 16 | "Installed": false, 17 | "EnabledByDefault": false, 18 | "Plugins": [ 19 | { 20 | "Name": "PythonScriptPlugin", 21 | "Enabled": true 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ryan DowlingSoka 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Asset Importer Rules - Python Edition 2 | 3 | This repo is an example of how you can use python to do rules based modifications to assets post import. 4 | 5 | ## How to use 6 | 7 | First, add a new python module in a plugin or project `/Python/`. The name doesn't matter, you can use the `Content/Python/Examples/post_import_texture2D_settings.py` as an example file. 8 | 9 | in this file you'll need to `import unreal` and `from ImporterRules import *` which will give you access to the example framework. 10 | 11 | You'll then add your first set of rules to the system. 12 | 13 | The format of the rules is as follows: 14 | 15 | ```python 16 | importer_rules_manager.register_rules( 17 | unreal.{{ClassName}}, 18 | [ 19 | Rule( 20 | queries=[ 21 | {{QueryType}}({{QueryArgs}}), 22 | ... 23 | ], 24 | actions=[ 25 | {{ActionType}}({{ActionArgs}}), 26 | ... 27 | ], 28 | requires_all={{True|False}} 29 | ), 30 | ... 31 | ] 32 | ) 33 | ``` 34 | 35 | Then in an `init_unreal.py` a /python/ folder import the module and the rules will get registered. 36 | 37 | > If you wish to try out the example file, you can uncomment the include in the plugin's init_unreal.py 38 | 39 | ### Example File 40 | 41 | This example file shows a simple set of rules applying to imported assets of the type: "Texture2D" 42 | By importing this module in an init_unreal these rules will get applied to any newly imported assets. 43 | 44 | First we need to import the importer rules related classes and unreal for the class type. 45 | 46 | ```python 47 | from ImporterRules import * 48 | import unreal 49 | ``` 50 | 51 | The importer_rules_manager handles the post_import delegate. So we register_rules through that. The first argument is the type of classes that these rules should be applied to. 52 | 53 | ```python 54 | importer_rules_manager.register_rules( 55 | class_type = unreal.Texture2D, 56 | ``` 57 | 58 | Next we have an array of rules. 59 | 60 | The first rule is simple, it takes any textures ending with _n and applies the flip_green_channel property as false. 61 | You might do something like this if you want to switch from DirectX to OpenGL normals. 62 | There is only one rule, so the requires_all parameter is irrelevant. 63 | 64 | ```python 65 | rules = [ 66 | Rule( 67 | queries=[ 68 | SourcePath(file_name_ends_with="_n"), 69 | ], 70 | actions=[SetEditorProperties(flip_green_channel=False)], 71 | ), 72 | ``` 73 | 74 | This second rule shows how you can put several queries together. Because the requires_all parameter is 'False' this rule will fire if ANY of the source path queries are true. So if the texture ends with `_n, _o, _h, _r, _m` then this rule will remove the sRGB property from those textures. 75 | 76 | ```python 77 | Rule( 78 | queries=[ 79 | SourcePath(file_name_ends_with="_n"), 80 | SourcePath(file_name_ends_with="_o"), 81 | SourcePath(file_name_ends_with="_h"), 82 | SourcePath(file_name_ends_with="_r"), 83 | SourcePath(file_name_ends_with="_m"), 84 | ], 85 | actions=[SetEditorProperties(srgb=False)], 86 | requires_all=False 87 | ), 88 | ``` 89 | 90 | The third rule is targeting more specifically. While the previous rules have been requires_all = False, this rule is True so now both the SourcePath and DestinationPath queries must come back true for the action to be applied. In this example the texture must have the suffix `_d` and be in a folder named /TestFolder/ somewhere in its path hierarchy to pass. You can see that the SetEditorProperties takes two property names as well. 91 | 92 | ```python 93 | Rule( 94 | queries=[ 95 | SourcePath(file_name_ends_with="_d"), 96 | DestinationPath(path_contains="/TestFolder/"), 97 | ], 98 | actions=[SetEditorProperties(srgb=False, lod_bias=5)], 99 | requires_all=True 100 | ), 101 | ``` 102 | 103 | This rule is similar to the previous, but the SetEditorProperties has been broken up into two actions, just like queries you aren't limited to one action at a time. 104 | 105 | ```python 106 | Rule( 107 | queries=[ 108 | SourcePath(file_name_ends_with="_test"), 109 | DestinationPath(path_contains="/TestFolder/"), 110 | ], 111 | actions=[SetEditorProperties(srgb=False), SetAssetTags({"obsolete":True})], 112 | requires_all=True 113 | ), 114 | ], 115 | ) 116 | ``` 117 | 118 | ## Framework breakdown 119 | 120 | `Rules` are made out of `Queries` and `Actions` 121 | 122 | ### Queries 123 | 124 | A query is a test that can be run on the recently imported `Factory` or `Created Object` to determine if the subsequent actions should run. 125 | 126 | For example the `Queries.SourcePath` class is a query that lets you do a series of string tests against the source path of the asset. 127 | 128 | To create a query type of your own, you simply need to inherit from the `Queries.QueryBase` and implement the `def test(self, factory: unreal.Factory, created_object: unreal.Object) -> bool` function. 129 | 130 | Use the `__init__` function of your new class to allow the user to create user parameters. 131 | 132 | Most queries, if they contain multiple different types of tests, should also implement a `self.requires_all` boolean class member this signifies whether or not all the tests in the query must be past or if only one must pass. 133 | 134 | For example in the `Queries.SourcePath` there are several tests: 135 | 136 | ```python 137 | file_name_starts_with: str = "", 138 | file_name_ends_with: str = "", 139 | file_name_contains: str = "", 140 | full_path_contains: str = "", 141 | extensions: List[str] = [], 142 | requires_all: bool = False, 143 | case_sensitive: bool = False, 144 | ``` 145 | 146 | The first 4 parameters are tests on the file name or path. The fifth is a list of valid extensions. The final two are modifiers on the tests. `case_sensitive` is used to have all string comparisons be done in `lower()` case. `requires_all` is the modifier mentioned above, if true `all` of the tests above must pass, if false `any` of the tests must pass. 147 | 148 | These queries are then instantiated in the `queries` parameter of a `Rule` 149 | 150 | Other types of queries you could choose to complete might be tests on specific data in the asset. For example, `.fbx` files can have `MetadataTags` that can get created by software like Maya and saved into the files. You can use the `CheckAssetTag` query looking for particular metadata tags that are created at import time, and do specific actions based on whether or not that tag exists. 151 | 152 | Complex boolean groupings like `query1 and query2 but not query3` aren't supported by the framework, buy could be done so easily by creating a `AND` or `OR` set of composite queries that could *wrap* other queries. 153 | 154 | ### Actions 155 | 156 | Once an imported asset passes any associated queries then `Actions` are run on it. Actions are any arbitrary amount of work that you want to run on the Created Object (or anything else) when a query passes. 157 | 158 | For example the `Actions.SetEditorProperties` can apply any amount of EditorProperties through the `.set_editor_properties(dict)` python function. It uses **kwargs so follows the pattern: 159 | 160 | `SetEditorProperties(srgb=False, lod_bias=2)` 161 | 162 | To create your own actions you simply need to inherit from `ImportActionBase` and implement the `def apply(self, factory: unreal.Factory, created_object: unreal.Object) -> bool:` function. This function should return `True` if successful, but note that the entire `apply` action is wrapped in a `try: except:` block. 163 | 164 | As before you should use the `__init__` definition to create member variables for action configuration in most cases. 165 | 166 | #### Actions.SetEditorProperties(**kwargs) 167 | 168 | This is the most generic included action. Pass in any editor properties as parameter values. For example, for a texture you might pass in: 169 | 170 | ```SetEditorProperties(srgb=True)``` 171 | 172 | The naming of the editor properties that are available for a given class can by found in the stub definition of the unreal class. For example: 173 | 174 | ```text 175 | Texture 2D 176 | 177 | **C++ Source:** 178 | 179 | - **Module**: Engine 180 | - **File**: Texture2D.h 181 | 182 | **Editor Properties:** (see get_editor_property/set_editor_property) 183 | 184 | - ``address_x`` (TextureAddress): [Read-Write] Address X: 185 | The addressing mode to use for the X axis. 186 | - ``address_y`` (TextureAddress): [Read-Write] Address Y: 187 | The addressing mode to use for the Y axis. 188 | - ``adjust_brightness`` (float): [Read-Write] Adjust Brightness: 189 | Static texture brightness adjustment (scales HSV value.) (Non-destructive; Requires texture source art to be available.) 190 | ``` 191 | 192 | This action will fail if the target does not have the given property name or if the value is the wrong type. 193 | 194 | #### Actions.SetAssetTags({TagKey:TagValue}) 195 | 196 | This action is for setting asset tags on import. It takes a dictionary of `str:Any` but be aware it runs `str()` on the value, so only types that can be cast to `str()` will be valid. 197 | 198 | ### Rules 199 | 200 | Rules are simple enough, they are just a set of `Queries` and a set of `Actions`. If any/all `Queries` pass then *all* `Actions` are run. If `requires_all` is True, then *all* `Queries` must pass, if False, then a *single* `Query` is enough to cause the actions to run. 201 | 202 | Rules are registered through the `Manager.importer_rules_manager` using `register_rule`. 203 | 204 | Internally the `importer_rules_manager` wraps adding `on_asset_post_import` a delegate in `unreal.ImportSubsystem` 205 | 206 | ```python 207 | self.import_subsystem = unreal.get_editor_subsystem(unreal.ImportSubsystem) 208 | if self.import_subsystem: 209 | self.import_subsystem.on_asset_post_import.add_callable( 210 | self.on_asset_post_import 211 | ) 212 | ``` 213 | 214 | If you'd like to build your own system you could either bind your rules directly to this delegate. There are some other useful delegates in the `ImportSubsystem` so take a look at these other delegates if you are interested in learning more. 215 | 216 | ## Notes 217 | 218 | * There is a native C++ and Blueprints version of this pattern available at [https://github.com/Ryan-DowlingSoka/UnrealImporterRules-CPP](https://github.com/Ryan-DowlingSoka/UnrealImporterRules-CPP) 219 | 220 | * The Unreal Python Path is set at each `/Python/` folder in each plugin and the project. As such, modules inside of modules such as `/Python/ImporterRules/.../` folders need to reference their siblings with `import ImporterRules.{{ModuleName}}` 221 | 222 | * The `ImporterRules` imports all the queries and action classes, so you can use `from ImporterRules import *` as a handy shorthand to get the classes you need. If you make additional queries or actions, you should either put them in this file or just remember to import them manually. 223 | 224 | * (Re)importing. All of the delegates related to importing in Unreal happen for reimporting too. You most likely don't want your rules to apply to assets that have already had the rules applied to them, so we need to detect if the current import is a reimport. There are two possible patterns for this: The first is to bind to pre-import, calculate what objects will be generated and if those packages already exist. This is a bummer because factories can do some pretty intense logic to determine what the names of the newly imported objects will be. The second (what this example project does) is to assign a MetadataTag to the asset if it has had rules applied to it already, and then rules can opt in to running despite if that tag exists. This second way is easier to implement, *but* does mean that reimporting existing assets from before when the plugin was created will have those rules applied. This might be beneficial in some cases, but do be careful. If you wanted to prevent that, you could run the `import_rules_manager._set_imported_asset_tag_action.apply()` function on all the assets in your content library as part of the installation process. 225 | 226 | * This tutorial was made for `#notGDC 2023`! Check out some other great entries at [https://notgdc.io](https://notgdc.io) 227 | -------------------------------------------------------------------------------- /Resources/Icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ryan-DowlingSoka/UnrealImporterRules-Python/6d0087ff24d96573d9731c05b1f61527eacc437b/Resources/Icon128.png --------------------------------------------------------------------------------