Merge branch 'develop' into bugfix/OP-3022-Look-publishing-and-srgb-colorspace-in-Maya-2022

This commit is contained in:
Kayla Man 2023-02-23 17:53:01 +08:00 committed by GitHub
commit db4f88d85b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
60 changed files with 720 additions and 409 deletions

View file

@ -1,7 +1,10 @@
import os
from openpype.pipeline import (
load
load,
get_representation_path
)
from openpype.hosts.max.api.pipeline import containerise
from openpype.hosts.max.api import lib
class FbxLoader(load.LoaderPlugin):
@ -36,14 +39,26 @@ importFile @"{filepath}" #noPrompt using:FBXIMP
container_name = f"{name}_CON"
asset = rt.getNodeByName(f"{name}")
# rename the container with "_CON"
container = rt.container(name=container_name)
asset.Parent = container
return container
return containerise(
name, [asset], context, loader=self.__class__.__name__)
def update(self, container, representation):
from pymxs import runtime as rt
path = get_representation_path(representation)
node = rt.getNodeByName(container["instance_node"])
fbx_objects = self.get_container_children(node)
for fbx_object in fbx_objects:
fbx_object.source = path
lib.imprint(container["instance_node"], {
"representation": str(representation["_id"])
})
def remove(self, container):
from pymxs import runtime as rt
node = container["node"]
node = rt.getNodeByName(container["instance_node"])
rt.delete(node)

View file

@ -1,7 +1,9 @@
import os
from openpype.pipeline import (
load
load, get_representation_path
)
from openpype.hosts.max.api.pipeline import containerise
from openpype.hosts.max.api import lib
class MaxSceneLoader(load.LoaderPlugin):
@ -35,16 +37,26 @@ class MaxSceneLoader(load.LoaderPlugin):
self.log.error("Something failed when loading.")
max_container = max_containers.pop()
container_name = f"{name}_CON"
# rename the container with "_CON"
# get the original container
container = rt.container(name=container_name)
max_container.Parent = container
return container
return containerise(
name, [max_container], context, loader=self.__class__.__name__)
def update(self, container, representation):
from pymxs import runtime as rt
path = get_representation_path(representation)
node = rt.getNodeByName(container["instance_node"])
max_objects = self.get_container_children(node)
for max_object in max_objects:
max_object.source = path
lib.imprint(container["instance_node"], {
"representation": str(representation["_id"])
})
def remove(self, container):
from pymxs import runtime as rt
node = container["node"]
node = rt.getNodeByName(container["instance_node"])
rt.delete(node)

View file

@ -80,7 +80,7 @@ importFile @"{file_path}" #noPrompt
def remove(self, container):
from pymxs import runtime as rt
node = container["node"]
node = rt.getNodeByName(container["instance_node"])
rt.delete(node)
@staticmethod

View file

@ -23,7 +23,7 @@ class ExtractReviewData(publish.Extractor):
representations = instance.data.get("representations", [])
# review can be removed since `ProcessSubmittedJobOnFarm` will create
# reviable representation if needed
# reviewable representation if needed
if (
"render.farm" in instance.data["families"]
and "review" in instance.data["families"]

View file

@ -1,7 +1,11 @@
# -*- coding: utf-8 -*-
"""Unreal Editor OpenPype host API."""
from .plugin import Loader
from .plugin import (
UnrealActorCreator,
UnrealAssetCreator,
Loader
)
from .pipeline import (
install,

View file

@ -1,9 +1,11 @@
# -*- coding: utf-8 -*-
import os
import json
import logging
from typing import List
from contextlib import contextmanager
import semver
import time
import pyblish.api
@ -16,13 +18,14 @@ from openpype.pipeline import (
)
from openpype.tools.utils import host_tools
import openpype.hosts.unreal
from openpype.host import HostBase, ILoadHost
from openpype.host import HostBase, ILoadHost, IPublishHost
import unreal # noqa
logger = logging.getLogger("openpype.hosts.unreal")
OPENPYPE_CONTAINERS = "OpenPypeContainers"
CONTEXT_CONTAINER = "OpenPype/context.json"
UNREAL_VERSION = semver.VersionInfo(
*os.getenv("OPENPYPE_UNREAL_VERSION").split(".")
)
@ -35,7 +38,7 @@ CREATE_PATH = os.path.join(PLUGINS_DIR, "create")
INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory")
class UnrealHost(HostBase, ILoadHost):
class UnrealHost(HostBase, ILoadHost, IPublishHost):
"""Unreal host implementation.
For some time this class will re-use functions from module based
@ -60,6 +63,32 @@ class UnrealHost(HostBase, ILoadHost):
show_tools_dialog()
def update_context_data(self, data, changes):
content_path = unreal.Paths.project_content_dir()
op_ctx = content_path + CONTEXT_CONTAINER
attempts = 3
for i in range(attempts):
try:
with open(op_ctx, "w+") as f:
json.dump(data, f)
break
except IOError:
if i == attempts - 1:
raise Exception("Failed to write context data. Aborting.")
unreal.log_warning("Failed to write context data. Retrying...")
i += 1
time.sleep(3)
continue
def get_context_data(self):
content_path = unreal.Paths.project_content_dir()
op_ctx = content_path + CONTEXT_CONTAINER
if not os.path.isfile(op_ctx):
return {}
with open(op_ctx, "r") as fp:
data = json.load(fp)
return data
def install():
"""Install Unreal configuration for OpenPype."""
@ -133,6 +162,31 @@ def ls():
yield data
def ls_inst():
ar = unreal.AssetRegistryHelpers.get_asset_registry()
# UE 5.1 changed how class name is specified
class_name = [
"/Script/OpenPype",
"OpenPypePublishInstance"
] if (
UNREAL_VERSION.major == 5
and UNREAL_VERSION.minor > 0
) else "OpenPypePublishInstance" # noqa
instances = ar.get_assets_by_class(class_name, True)
# get_asset_by_class returns AssetData. To get all metadata we need to
# load asset. get_tag_values() work only on metadata registered in
# Asset Registry Project settings (and there is no way to set it with
# python short of editing ini configuration file).
for asset_data in instances:
asset = asset_data.get_asset()
data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset)
data["objectName"] = asset_data.asset_name
data = cast_map_to_str_dict(data)
yield data
def parse_container(container):
"""To get data from container, AssetContainer must be loaded.

View file

@ -1,7 +1,245 @@
# -*- coding: utf-8 -*-
from abc import ABC
import ast
import collections
import sys
import six
from abc import (
ABC,
ABCMeta,
)
from openpype.pipeline import LoaderPlugin
import unreal
from .pipeline import (
create_publish_instance,
imprint,
ls_inst,
UNREAL_VERSION
)
from openpype.lib import (
BoolDef,
UILabelDef
)
from openpype.pipeline import (
Creator,
LoaderPlugin,
CreatorError,
CreatedInstance
)
@six.add_metaclass(ABCMeta)
class UnrealBaseCreator(Creator):
"""Base class for Unreal creator plugins."""
root = "/Game/OpenPype/PublishInstances"
suffix = "_INS"
@staticmethod
def cache_subsets(shared_data):
"""Cache instances for Creators to shared data.
Create `unreal_cached_subsets` key when needed in shared data and
fill it with all collected instances from the scene under its
respective creator identifiers.
If legacy instances are detected in the scene, create
`unreal_cached_legacy_subsets` there and fill it with
all legacy subsets under family as a key.
Args:
Dict[str, Any]: Shared data.
Return:
Dict[str, Any]: Shared data dictionary.
"""
if shared_data.get("unreal_cached_subsets") is None:
unreal_cached_subsets = collections.defaultdict(list)
unreal_cached_legacy_subsets = collections.defaultdict(list)
for instance in ls_inst():
creator_id = instance.get("creator_identifier")
if creator_id:
unreal_cached_subsets[creator_id].append(instance)
else:
family = instance.get("family")
unreal_cached_legacy_subsets[family].append(instance)
shared_data["unreal_cached_subsets"] = unreal_cached_subsets
shared_data["unreal_cached_legacy_subsets"] = (
unreal_cached_legacy_subsets
)
return shared_data
def create(self, subset_name, instance_data, pre_create_data):
try:
instance_name = f"{subset_name}{self.suffix}"
pub_instance = create_publish_instance(instance_name, self.root)
instance_data["subset"] = subset_name
instance_data["instance_path"] = f"{self.root}/{instance_name}"
instance = CreatedInstance(
self.family,
subset_name,
instance_data,
self)
self._add_instance_to_context(instance)
pub_instance.set_editor_property('add_external_assets', True)
assets = pub_instance.get_editor_property('asset_data_external')
ar = unreal.AssetRegistryHelpers.get_asset_registry()
for member in pre_create_data.get("members", []):
obj = ar.get_asset_by_object_path(member).get_asset()
assets.add(obj)
imprint(f"{self.root}/{instance_name}", instance.data_to_store())
return instance
except Exception as er:
six.reraise(
CreatorError,
CreatorError(f"Creator error: {er}"),
sys.exc_info()[2])
def collect_instances(self):
# cache instances if missing
self.cache_subsets(self.collection_shared_data)
for instance in self.collection_shared_data[
"unreal_cached_subsets"].get(self.identifier, []):
# Unreal saves metadata as string, so we need to convert it back
instance['creator_attributes'] = ast.literal_eval(
instance.get('creator_attributes', '{}'))
instance['publish_attributes'] = ast.literal_eval(
instance.get('publish_attributes', '{}'))
created_instance = CreatedInstance.from_existing(instance, self)
self._add_instance_to_context(created_instance)
def update_instances(self, update_list):
for created_inst, changes in update_list:
instance_node = created_inst.get("instance_path", "")
if not instance_node:
unreal.log_warning(
f"Instance node not found for {created_inst}")
continue
new_values = {
key: changes[key].new_value
for key in changes.changed_keys
}
imprint(
instance_node,
new_values
)
def remove_instances(self, instances):
for instance in instances:
instance_node = instance.data.get("instance_path", "")
if instance_node:
unreal.EditorAssetLibrary.delete_asset(instance_node)
self._remove_instance_from_context(instance)
@six.add_metaclass(ABCMeta)
class UnrealAssetCreator(UnrealBaseCreator):
"""Base class for Unreal creator plugins based on assets."""
def create(self, subset_name, instance_data, pre_create_data):
"""Create instance of the asset.
Args:
subset_name (str): Name of the subset.
instance_data (dict): Data for the instance.
pre_create_data (dict): Data for the instance.
Returns:
CreatedInstance: Created instance.
"""
try:
# Check if instance data has members, filled by the plugin.
# If not, use selection.
if not pre_create_data.get("members"):
pre_create_data["members"] = []
if pre_create_data.get("use_selection"):
utilib = unreal.EditorUtilityLibrary
sel_objects = utilib.get_selected_assets()
pre_create_data["members"] = [
a.get_path_name() for a in sel_objects]
super(UnrealAssetCreator, self).create(
subset_name,
instance_data,
pre_create_data)
except Exception as er:
six.reraise(
CreatorError,
CreatorError(f"Creator error: {er}"),
sys.exc_info()[2])
def get_pre_create_attr_defs(self):
return [
BoolDef("use_selection", label="Use selection", default=True)
]
@six.add_metaclass(ABCMeta)
class UnrealActorCreator(UnrealBaseCreator):
"""Base class for Unreal creator plugins based on actors."""
def create(self, subset_name, instance_data, pre_create_data):
"""Create instance of the asset.
Args:
subset_name (str): Name of the subset.
instance_data (dict): Data for the instance.
pre_create_data (dict): Data for the instance.
Returns:
CreatedInstance: Created instance.
"""
try:
if UNREAL_VERSION.major == 5:
world = unreal.UnrealEditorSubsystem().get_editor_world()
else:
world = unreal.EditorLevelLibrary.get_editor_world()
# Check if the level is saved
if world.get_path_name().startswith("/Temp/"):
raise CreatorError(
"Level must be saved before creating instances.")
# Check if instance data has members, filled by the plugin.
# If not, use selection.
if not instance_data.get("members"):
actor_subsystem = unreal.EditorActorSubsystem()
sel_actors = actor_subsystem.get_selected_level_actors()
selection = [a.get_path_name() for a in sel_actors]
instance_data["members"] = selection
instance_data["level"] = world.get_path_name()
super(UnrealActorCreator, self).create(
subset_name,
instance_data,
pre_create_data)
except Exception as er:
six.reraise(
CreatorError,
CreatorError(f"Creator error: {er}"),
sys.exc_info()[2])
def get_pre_create_attr_defs(self):
return [
UILabelDef("Select actors to create instance from them.")
]
class Loader(LoaderPlugin, ABC):

View file

@ -17,9 +17,8 @@ class ToolsBtnsWidget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(ToolsBtnsWidget, self).__init__(parent)
create_btn = QtWidgets.QPushButton("Create...", self)
load_btn = QtWidgets.QPushButton("Load...", self)
publish_btn = QtWidgets.QPushButton("Publish...", self)
publish_btn = QtWidgets.QPushButton("Publisher...", self)
manage_btn = QtWidgets.QPushButton("Manage...", self)
render_btn = QtWidgets.QPushButton("Render...", self)
experimental_tools_btn = QtWidgets.QPushButton(
@ -28,7 +27,6 @@ class ToolsBtnsWidget(QtWidgets.QWidget):
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(create_btn, 0)
layout.addWidget(load_btn, 0)
layout.addWidget(publish_btn, 0)
layout.addWidget(manage_btn, 0)
@ -36,7 +34,6 @@ class ToolsBtnsWidget(QtWidgets.QWidget):
layout.addWidget(experimental_tools_btn, 0)
layout.addStretch(1)
create_btn.clicked.connect(self._on_create)
load_btn.clicked.connect(self._on_load)
publish_btn.clicked.connect(self._on_publish)
manage_btn.clicked.connect(self._on_manage)
@ -50,7 +47,7 @@ class ToolsBtnsWidget(QtWidgets.QWidget):
self.tool_required.emit("loader")
def _on_publish(self):
self.tool_required.emit("publish")
self.tool_required.emit("publisher")
def _on_manage(self):
self.tool_required.emit("sceneinventory")

View file

@ -1,41 +1,38 @@
# -*- coding: utf-8 -*-
import unreal
from unreal import EditorAssetLibrary as eal
from unreal import EditorLevelLibrary as ell
from openpype.hosts.unreal.api.pipeline import instantiate
from openpype.pipeline import LegacyCreator
from openpype.pipeline import CreatorError
from openpype.hosts.unreal.api.pipeline import UNREAL_VERSION
from openpype.hosts.unreal.api.plugin import (
UnrealAssetCreator,
)
class CreateCamera(LegacyCreator):
"""Layout output for character rigs"""
class CreateCamera(UnrealAssetCreator):
"""Create Camera."""
name = "layoutMain"
identifier = "io.openpype.creators.unreal.camera"
label = "Camera"
family = "camera"
icon = "cubes"
icon = "fa.camera"
root = "/Game/OpenPype/Instances"
suffix = "_INS"
def create(self, subset_name, instance_data, pre_create_data):
if pre_create_data.get("use_selection"):
sel_objects = unreal.EditorUtilityLibrary.get_selected_assets()
selection = [a.get_path_name() for a in sel_objects]
def __init__(self, *args, **kwargs):
super(CreateCamera, self).__init__(*args, **kwargs)
if len(selection) != 1:
raise CreatorError("Please select only one object.")
def process(self):
data = self.data
# Add the current level path to the metadata
if UNREAL_VERSION.major == 5:
world = unreal.UnrealEditorSubsystem().get_editor_world()
else:
world = unreal.EditorLevelLibrary.get_editor_world()
name = data["subset"]
instance_data["level"] = world.get_path_name()
data["level"] = ell.get_editor_world().get_path_name()
if not eal.does_directory_exist(self.root):
eal.make_directory(self.root)
factory = unreal.LevelSequenceFactoryNew()
tools = unreal.AssetToolsHelpers().get_asset_tools()
tools.create_asset(name, f"{self.root}/{name}", None, factory)
asset_name = f"{self.root}/{name}/{name}.{name}"
data["members"] = [asset_name]
instantiate(f"{self.root}", name, data, None, self.suffix)
super(CreateCamera, self).create(
subset_name,
instance_data,
pre_create_data)

View file

@ -1,42 +1,13 @@
# -*- coding: utf-8 -*-
from unreal import EditorLevelLibrary
from openpype.pipeline import LegacyCreator
from openpype.hosts.unreal.api.pipeline import instantiate
from openpype.hosts.unreal.api.plugin import (
UnrealActorCreator,
)
class CreateLayout(LegacyCreator):
class CreateLayout(UnrealActorCreator):
"""Layout output for character rigs."""
name = "layoutMain"
identifier = "io.openpype.creators.unreal.layout"
label = "Layout"
family = "layout"
icon = "cubes"
root = "/Game"
suffix = "_INS"
def __init__(self, *args, **kwargs):
super(CreateLayout, self).__init__(*args, **kwargs)
def process(self):
data = self.data
name = data["subset"]
selection = []
# if (self.options or {}).get("useSelection"):
# sel_objects = unreal.EditorUtilityLibrary.get_selected_assets()
# selection = [a.get_path_name() for a in sel_objects]
data["level"] = EditorLevelLibrary.get_editor_world().get_path_name()
data["members"] = []
if (self.options or {}).get("useSelection"):
# Set as members the selected actors
for actor in EditorLevelLibrary.get_selected_level_actors():
data["members"].append("{}.{}".format(
actor.get_outer().get_name(), actor.get_name()))
instantiate(self.root, name, data, selection, self.suffix)

View file

@ -1,56 +1,57 @@
# -*- coding: utf-8 -*-
"""Create look in Unreal."""
import unreal # noqa
from openpype.hosts.unreal.api import pipeline, plugin
from openpype.pipeline import LegacyCreator
import unreal
from openpype.pipeline import CreatorError
from openpype.hosts.unreal.api.pipeline import (
create_folder
)
from openpype.hosts.unreal.api.plugin import (
UnrealAssetCreator
)
from openpype.lib import UILabelDef
class CreateLook(LegacyCreator):
class CreateLook(UnrealAssetCreator):
"""Shader connections defining shape look."""
name = "unrealLook"
label = "Unreal - Look"
identifier = "io.openpype.creators.unreal.look"
label = "Look"
family = "look"
icon = "paint-brush"
root = "/Game/Avalon/Assets"
suffix = "_INS"
def create(self, subset_name, instance_data, pre_create_data):
# We need to set this to True for the parent class to work
pre_create_data["use_selection"] = True
sel_objects = unreal.EditorUtilityLibrary.get_selected_assets()
selection = [a.get_path_name() for a in sel_objects]
def __init__(self, *args, **kwargs):
super(CreateLook, self).__init__(*args, **kwargs)
if len(selection) != 1:
raise CreatorError("Please select only one asset.")
def process(self):
name = self.data["subset"]
selected_asset = selection[0]
selection = []
if (self.options or {}).get("useSelection"):
sel_objects = unreal.EditorUtilityLibrary.get_selected_assets()
selection = [a.get_path_name() for a in sel_objects]
look_directory = "/Game/OpenPype/Looks"
# Create the folder
path = f"{self.root}/{self.data['asset']}"
new_name = pipeline.create_folder(path, name)
full_path = f"{path}/{new_name}"
folder_name = create_folder(look_directory, subset_name)
path = f"{look_directory}/{folder_name}"
instance_data["look"] = path
# Create a new cube static mesh
ar = unreal.AssetRegistryHelpers.get_asset_registry()
cube = ar.get_asset_by_object_path("/Engine/BasicShapes/Cube.Cube")
# Create the avalon publish instance object
container_name = f"{name}{self.suffix}"
pipeline.create_publish_instance(
instance=container_name, path=full_path)
# Get the mesh of the selected object
original_mesh = ar.get_asset_by_object_path(selection[0]).get_asset()
materials = original_mesh.get_editor_property('materials')
original_mesh = ar.get_asset_by_object_path(selected_asset).get_asset()
materials = original_mesh.get_editor_property('static_materials')
self.data["members"] = []
pre_create_data["members"] = []
# Add the materials to the cube
for material in materials:
name = material.get_editor_property('material_slot_name')
object_path = f"{full_path}/{name}.{name}"
mat_name = material.get_editor_property('material_slot_name')
object_path = f"{path}/{mat_name}.{mat_name}"
unreal_object = unreal.EditorAssetLibrary.duplicate_loaded_asset(
cube.get_asset(), object_path
)
@ -61,8 +62,16 @@ class CreateLook(LegacyCreator):
unreal_object.add_material(
material.get_editor_property('material_interface'))
self.data["members"].append(object_path)
pre_create_data["members"].append(object_path)
unreal.EditorAssetLibrary.save_asset(object_path)
pipeline.imprint(f"{full_path}/{container_name}", self.data)
super(CreateLook, self).create(
subset_name,
instance_data,
pre_create_data)
def get_pre_create_attr_defs(self):
return [
UILabelDef("Select the asset from which to create the look.")
]

View file

@ -1,117 +1,138 @@
# -*- coding: utf-8 -*-
import unreal
from openpype.hosts.unreal.api import pipeline
from openpype.pipeline import LegacyCreator
from openpype.pipeline import CreatorError
from openpype.hosts.unreal.api.pipeline import (
get_subsequences
)
from openpype.hosts.unreal.api.plugin import (
UnrealAssetCreator
)
from openpype.lib import UILabelDef
class CreateRender(LegacyCreator):
class CreateRender(UnrealAssetCreator):
"""Create instance for sequence for rendering"""
name = "unrealRender"
label = "Unreal - Render"
identifier = "io.openpype.creators.unreal.render"
label = "Render"
family = "render"
icon = "cube"
asset_types = ["LevelSequence"]
root = "/Game/OpenPype/PublishInstances"
suffix = "_INS"
def process(self):
subset = self.data["subset"]
icon = "eye"
def create(self, subset_name, instance_data, pre_create_data):
ar = unreal.AssetRegistryHelpers.get_asset_registry()
# The asset name is the the third element of the path which contains
# the map.
# The index of the split path is 3 because the first element is an
# empty string, as the path begins with "/Content".
a = unreal.EditorUtilityLibrary.get_selected_assets()[0]
asset_name = a.get_path_name().split("/")[3]
sel_objects = unreal.EditorUtilityLibrary.get_selected_assets()
selection = [
a.get_path_name() for a in sel_objects
if a.get_class().get_name() == "LevelSequence"]
# Get the master sequence and the master level.
# There should be only one sequence and one level in the directory.
filter = unreal.ARFilter(
class_names=["LevelSequence"],
package_paths=[f"/Game/OpenPype/{asset_name}"],
recursive_paths=False)
sequences = ar.get_assets(filter)
ms = sequences[0].get_editor_property('object_path')
filter = unreal.ARFilter(
class_names=["World"],
package_paths=[f"/Game/OpenPype/{asset_name}"],
recursive_paths=False)
levels = ar.get_assets(filter)
ml = levels[0].get_editor_property('object_path')
if not selection:
raise CreatorError("Please select at least one Level Sequence.")
selection = []
if (self.options or {}).get("useSelection"):
sel_objects = unreal.EditorUtilityLibrary.get_selected_assets()
selection = [
a.get_path_name() for a in sel_objects
if a.get_class().get_name() in self.asset_types]
else:
selection.append(self.data['sequence'])
seq_data = None
unreal.log(f"selection: {selection}")
for sel in selection:
selected_asset = ar.get_asset_by_object_path(sel).get_asset()
selected_asset_path = selected_asset.get_path_name()
path = f"{self.root}"
unreal.EditorAssetLibrary.make_directory(path)
# Check if the selected asset is a level sequence asset.
if selected_asset.get_class().get_name() != "LevelSequence":
unreal.log_warning(
f"Skipping {selected_asset.get_name()}. It isn't a Level "
"Sequence.")
ar = unreal.AssetRegistryHelpers.get_asset_registry()
# The asset name is the third element of the path which
# contains the map.
# To take the asset name, we remove from the path the prefix
# "/Game/OpenPype/" and then we split the path by "/".
sel_path = selected_asset_path
asset_name = sel_path.replace("/Game/OpenPype/", "").split("/")[0]
for a in selection:
ms_obj = ar.get_asset_by_object_path(ms).get_asset()
# Get the master sequence and the master level.
# There should be only one sequence and one level in the directory.
ar_filter = unreal.ARFilter(
class_names=["LevelSequence"],
package_paths=[f"/Game/OpenPype/{asset_name}"],
recursive_paths=False)
sequences = ar.get_assets(ar_filter)
master_seq = sequences[0].get_asset().get_path_name()
master_seq_obj = sequences[0].get_asset()
ar_filter = unreal.ARFilter(
class_names=["World"],
package_paths=[f"/Game/OpenPype/{asset_name}"],
recursive_paths=False)
levels = ar.get_assets(ar_filter)
master_lvl = levels[0].get_asset().get_path_name()
seq_data = None
# If the selected asset is the master sequence, we get its data
# and then we create the instance for the master sequence.
# Otherwise, we cycle from the master sequence to find the selected
# sequence and we get its data. This data will be used to create
# the instance for the selected sequence. In particular,
# we get the frame range of the selected sequence and its final
# output path.
master_seq_data = {
"sequence": master_seq_obj,
"output": f"{master_seq_obj.get_name()}",
"frame_range": (
master_seq_obj.get_playback_start(),
master_seq_obj.get_playback_end())}
if a == ms:
seq_data = {
"sequence": ms_obj,
"output": f"{ms_obj.get_name()}",
"frame_range": (
ms_obj.get_playback_start(), ms_obj.get_playback_end())
}
if selected_asset_path == master_seq:
seq_data = master_seq_data
else:
seq_data_list = [{
"sequence": ms_obj,
"output": f"{ms_obj.get_name()}",
"frame_range": (
ms_obj.get_playback_start(), ms_obj.get_playback_end())
}]
seq_data_list = [master_seq_data]
for s in seq_data_list:
subscenes = pipeline.get_subsequences(s.get('sequence'))
for seq in seq_data_list:
subscenes = get_subsequences(seq.get('sequence'))
for ss in subscenes:
for sub_seq in subscenes:
sub_seq_obj = sub_seq.get_sequence()
curr_data = {
"sequence": ss.get_sequence(),
"output": (f"{s.get('output')}/"
f"{ss.get_sequence().get_name()}"),
"sequence": sub_seq_obj,
"output": (f"{seq.get('output')}/"
f"{sub_seq_obj.get_name()}"),
"frame_range": (
ss.get_start_frame(), ss.get_end_frame() - 1)
}
sub_seq.get_start_frame(),
sub_seq.get_end_frame() - 1)}
if ss.get_sequence().get_path_name() == a:
# If the selected asset is the current sub-sequence,
# we get its data and we break the loop.
# Otherwise, we add the current sub-sequence data to
# the list of sequences to check.
if sub_seq_obj.get_path_name() == selected_asset_path:
seq_data = curr_data
break
seq_data_list.append(curr_data)
# If we found the selected asset, we break the loop.
if seq_data is not None:
break
# If we didn't find the selected asset, we don't create the
# instance.
if not seq_data:
unreal.log_warning(
f"Skipping {selected_asset.get_name()}. It isn't a "
"sub-sequence of the master sequence.")
continue
d = self.data.copy()
d["members"] = [a]
d["sequence"] = a
d["master_sequence"] = ms
d["master_level"] = ml
d["output"] = seq_data.get('output')
d["frameStart"] = seq_data.get('frame_range')[0]
d["frameEnd"] = seq_data.get('frame_range')[1]
instance_data["members"] = [selected_asset_path]
instance_data["sequence"] = selected_asset_path
instance_data["master_sequence"] = master_seq
instance_data["master_level"] = master_lvl
instance_data["output"] = seq_data.get('output')
instance_data["frameStart"] = seq_data.get('frame_range')[0]
instance_data["frameEnd"] = seq_data.get('frame_range')[1]
container_name = f"{subset}{self.suffix}"
pipeline.create_publish_instance(
instance=container_name, path=path)
pipeline.imprint(f"{path}/{container_name}", d)
super(CreateRender, self).create(
subset_name,
instance_data,
pre_create_data)
def get_pre_create_attr_defs(self):
return [
UILabelDef("Select the sequence to render.")
]

View file

@ -1,35 +1,13 @@
# -*- coding: utf-8 -*-
"""Create Static Meshes as FBX geometry."""
import unreal # noqa
from openpype.hosts.unreal.api.pipeline import (
instantiate,
from openpype.hosts.unreal.api.plugin import (
UnrealAssetCreator,
)
from openpype.pipeline import LegacyCreator
class CreateStaticMeshFBX(LegacyCreator):
"""Static FBX geometry."""
class CreateStaticMeshFBX(UnrealAssetCreator):
"""Create Static Meshes as FBX geometry."""
name = "unrealStaticMeshMain"
label = "Unreal - Static Mesh"
identifier = "io.openpype.creators.unreal.staticmeshfbx"
label = "Static Mesh (FBX)"
family = "unrealStaticMesh"
icon = "cube"
asset_types = ["StaticMesh"]
root = "/Game"
suffix = "_INS"
def __init__(self, *args, **kwargs):
super(CreateStaticMeshFBX, self).__init__(*args, **kwargs)
def process(self):
name = self.data["subset"]
selection = []
if (self.options or {}).get("useSelection"):
sel_objects = unreal.EditorUtilityLibrary.get_selected_assets()
selection = [a.get_path_name() for a in sel_objects]
unreal.log("selection: {}".format(selection))
instantiate(self.root, name, self.data, selection, self.suffix)

View file

@ -1,41 +1,31 @@
"""Create UAsset."""
# -*- coding: utf-8 -*-
from pathlib import Path
import unreal
from openpype.hosts.unreal.api import pipeline
from openpype.pipeline import LegacyCreator
from openpype.pipeline import CreatorError
from openpype.hosts.unreal.api.plugin import (
UnrealAssetCreator,
)
class CreateUAsset(LegacyCreator):
"""UAsset."""
class CreateUAsset(UnrealAssetCreator):
"""Create UAsset."""
name = "UAsset"
identifier = "io.openpype.creators.unreal.uasset"
label = "UAsset"
family = "uasset"
icon = "cube"
root = "/Game/OpenPype"
suffix = "_INS"
def create(self, subset_name, instance_data, pre_create_data):
if pre_create_data.get("use_selection"):
ar = unreal.AssetRegistryHelpers.get_asset_registry()
def __init__(self, *args, **kwargs):
super(CreateUAsset, self).__init__(*args, **kwargs)
def process(self):
ar = unreal.AssetRegistryHelpers.get_asset_registry()
subset = self.data["subset"]
path = f"{self.root}/PublishInstances/"
unreal.EditorAssetLibrary.make_directory(path)
selection = []
if (self.options or {}).get("useSelection"):
sel_objects = unreal.EditorUtilityLibrary.get_selected_assets()
selection = [a.get_path_name() for a in sel_objects]
if len(selection) != 1:
raise RuntimeError("Please select only one object.")
raise CreatorError("Please select only one object.")
obj = selection[0]
@ -43,19 +33,14 @@ class CreateUAsset(LegacyCreator):
sys_path = unreal.SystemLibrary.get_system_path(asset)
if not sys_path:
raise RuntimeError(
raise CreatorError(
f"{Path(obj).name} is not on the disk. Likely it needs to"
"be saved first.")
if Path(sys_path).suffix != ".uasset":
raise RuntimeError(f"{Path(sys_path).name} is not a UAsset.")
raise CreatorError(f"{Path(sys_path).name} is not a UAsset.")
unreal.log("selection: {}".format(selection))
container_name = f"{subset}{self.suffix}"
pipeline.create_publish_instance(
instance=container_name, path=path)
data = self.data.copy()
data["members"] = selection
pipeline.imprint(f"{path}/{container_name}", data)
super(CreateUAsset, self).create(
subset_name,
instance_data,
pre_create_data)

View file

@ -0,0 +1,46 @@
import unreal
import pyblish.api
class CollectInstanceMembers(pyblish.api.InstancePlugin):
"""
Collect members of instance.
This collector will collect the assets for the families that support to
have them included as External Data, and will add them to the instance
as members.
"""
order = pyblish.api.CollectorOrder + 0.1
hosts = ["unreal"]
families = ["camera", "look", "unrealStaticMesh", "uasset"]
label = "Collect Instance Members"
def process(self, instance):
"""Collect members of instance."""
self.log.info("Collecting instance members")
ar = unreal.AssetRegistryHelpers.get_asset_registry()
inst_path = instance.data.get('instance_path')
inst_name = instance.data.get('objectName')
pub_instance = ar.get_asset_by_object_path(
f"{inst_path}.{inst_name}").get_asset()
if not pub_instance:
self.log.error(f"{inst_path}.{inst_name}")
raise RuntimeError(f"Instance {instance} not found.")
if not pub_instance.get_editor_property("add_external_assets"):
# No external assets in the instance
return
assets = pub_instance.get_editor_property('asset_data_external')
members = [asset.get_path_name() for asset in assets]
self.log.debug(f"Members: {members}")
instance.data["members"] = members

View file

@ -1,67 +0,0 @@
# -*- coding: utf-8 -*-
"""Collect publishable instances in Unreal."""
import ast
import unreal # noqa
import pyblish.api
from openpype.hosts.unreal.api.pipeline import UNREAL_VERSION
from openpype.pipeline.publish import KnownPublishError
class CollectInstances(pyblish.api.ContextPlugin):
"""Gather instances by OpenPypePublishInstance class
This collector finds all paths containing `OpenPypePublishInstance` class
asset
Identifier:
id (str): "pyblish.avalon.instance"
"""
label = "Collect Instances"
order = pyblish.api.CollectorOrder - 0.1
hosts = ["unreal"]
def process(self, context):
ar = unreal.AssetRegistryHelpers.get_asset_registry()
class_name = [
"/Script/OpenPype",
"OpenPypePublishInstance"
] if (
UNREAL_VERSION.major == 5
and UNREAL_VERSION.minor > 0
) else "OpenPypePublishInstance" # noqa
instance_containers = ar.get_assets_by_class(class_name, True)
for container_data in instance_containers:
asset = container_data.get_asset()
data = unreal.EditorAssetLibrary.get_metadata_tag_values(asset)
data["objectName"] = container_data.asset_name
# convert to strings
data = {str(key): str(value) for (key, value) in data.items()}
if not data.get("family"):
raise KnownPublishError("instance has no family")
# content of container
members = ast.literal_eval(data.get("members"))
self.log.debug(members)
self.log.debug(asset.get_path_name())
# remove instance container
self.log.info("Creating instance for {}".format(asset.get_name()))
instance = context.create_instance(asset.get_name())
instance[:] = members
# Store the exact members of the object set
instance.data["setMembers"] = members
instance.data["families"] = [data.get("family")]
instance.data["level"] = data.get("level")
instance.data["parent"] = data.get("parent")
label = "{0} ({1})".format(asset.get_name()[:-4],
data["asset"])
instance.data["label"] = label
instance.data.update(data)

View file

@ -3,10 +3,9 @@
import os
import unreal
from unreal import EditorAssetLibrary as eal
from unreal import EditorLevelLibrary as ell
from openpype.pipeline import publish
from openpype.hosts.unreal.api.pipeline import UNREAL_VERSION
class ExtractCamera(publish.Extractor):
@ -18,6 +17,8 @@ class ExtractCamera(publish.Extractor):
optional = True
def process(self, instance):
ar = unreal.AssetRegistryHelpers.get_asset_registry()
# Define extract output file path
staging_dir = self.staging_dir(instance)
fbx_filename = "{}.fbx".format(instance.name)
@ -26,23 +27,54 @@ class ExtractCamera(publish.Extractor):
self.log.info("Performing extraction..")
# Check if the loaded level is the same of the instance
current_level = ell.get_editor_world().get_path_name()
if UNREAL_VERSION.major == 5:
world = unreal.UnrealEditorSubsystem().get_editor_world()
else:
world = unreal.EditorLevelLibrary.get_editor_world()
current_level = world.get_path_name()
assert current_level == instance.data.get("level"), \
"Wrong level loaded"
for member in instance[:]:
data = eal.find_asset_data(member)
if data.asset_class == "LevelSequence":
ar = unreal.AssetRegistryHelpers.get_asset_registry()
sequence = ar.get_asset_by_object_path(member).get_asset()
unreal.SequencerTools.export_fbx(
ell.get_editor_world(),
sequence,
sequence.get_bindings(),
unreal.FbxExportOption(),
os.path.join(staging_dir, fbx_filename)
)
break
for member in instance.data.get('members'):
data = ar.get_asset_by_object_path(member)
if UNREAL_VERSION.major == 5:
is_level_sequence = (
data.asset_class_path.asset_name == "LevelSequence")
else:
is_level_sequence = (data.asset_class == "LevelSequence")
if is_level_sequence:
sequence = data.get_asset()
if UNREAL_VERSION.major == 5 and UNREAL_VERSION.minor >= 1:
params = unreal.SequencerExportFBXParams(
world=world,
root_sequence=sequence,
sequence=sequence,
bindings=sequence.get_bindings(),
master_tracks=sequence.get_master_tracks(),
fbx_file_name=os.path.join(staging_dir, fbx_filename)
)
unreal.SequencerTools.export_level_sequence_fbx(params)
elif UNREAL_VERSION.major == 4 and UNREAL_VERSION.minor == 26:
unreal.SequencerTools.export_fbx(
world,
sequence,
sequence.get_bindings(),
unreal.FbxExportOption(),
os.path.join(staging_dir, fbx_filename)
)
else:
# Unreal 5.0 or 4.27
unreal.SequencerTools.export_level_sequence_fbx(
world,
sequence,
sequence.get_bindings(),
unreal.FbxExportOption(),
os.path.join(staging_dir, fbx_filename)
)
if not os.path.isfile(os.path.join(staging_dir, fbx_filename)):
raise RuntimeError("Failed to extract camera")
if "representations" not in instance.data:
instance.data["representations"] = []

View file

@ -29,13 +29,13 @@ class ExtractLook(publish.Extractor):
for member in instance:
asset = ar.get_asset_by_object_path(member)
object = asset.get_asset()
obj = asset.get_asset()
name = asset.get_editor_property('asset_name')
json_element = {'material': str(name)}
material_obj = object.get_editor_property('static_materials')[0]
material_obj = obj.get_editor_property('static_materials')[0]
material = material_obj.material_interface
base_color = mat_lib.get_material_property_input_node(

View file

@ -22,7 +22,13 @@ class ExtractUAsset(publish.Extractor):
staging_dir = self.staging_dir(instance)
filename = "{}.uasset".format(instance.name)
obj = instance[0]
members = instance.data.get("members", [])
if not members:
raise RuntimeError("No members found in instance.")
# UAsset publishing supports only one member
obj = members[0]
asset = ar.get_asset_by_object_path(obj).get_asset()
sys_path = unreal.SystemLibrary.get_system_path(asset)

View file

@ -28,6 +28,7 @@ def import_filepath(filepath, module_name=None):
# Prepare module object where content of file will be parsed
module = types.ModuleType(module_name)
module.__file__ = filepath
if six.PY3:
# Use loader so module has full specs
@ -41,7 +42,6 @@ def import_filepath(filepath, module_name=None):
# Execute content and store it to module object
six.exec_(_stream.read(), module.__dict__)
module.__file__ = filepath
return module

View file

@ -266,7 +266,8 @@ class NukeSubmitDeadline(pyblish.api.InstancePlugin):
"PYBLISHPLUGINPATH",
"NUKE_PATH",
"TOOL_ENV",
"FOUNDRY_LICENSE"
"FOUNDRY_LICENSE",
"OPENPYPE_SG_USER",
]
# Add OpenPype version if we are running from build.

View file

@ -139,7 +139,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
"FTRACK_API_KEY",
"FTRACK_SERVER",
"AVALON_APP_NAME",
"OPENPYPE_USERNAME"
"OPENPYPE_USERNAME",
"OPENPYPE_SG_USER",
]
# Add OpenPype version if we are running from build.
@ -194,7 +195,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
metadata_path = os.path.join(output_dir, metadata_filename)
# Convert output dir to `{root}/rest/of/path/...` with Anatomy
success, roothless_mtdt_p = self.anatomy.find_root_template_from_path(
success, rootless_mtdt_p = self.anatomy.find_root_template_from_path(
metadata_path)
if not success:
# `rootless_path` is not set to `output_dir` if none of roots match
@ -202,9 +203,9 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
"Could not find root path for remapping \"{}\"."
" This may cause issues on farm."
).format(output_dir))
roothless_mtdt_p = metadata_path
rootless_mtdt_p = metadata_path
return metadata_path, roothless_mtdt_p
return metadata_path, rootless_mtdt_p
def _submit_deadline_post_job(self, instance, job, instances):
"""Submit publish job to Deadline.
@ -237,7 +238,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
# Transfer the environment from the original job to this dependent
# job so they use the same environment
metadata_path, roothless_metadata_path = \
metadata_path, rootless_metadata_path = \
self._create_metadata_path(instance)
environment = {
@ -274,7 +275,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
args = [
"--headless",
'publish',
roothless_metadata_path,
rootless_metadata_path,
"--targets", "deadline",
"--targets", "farm"
]
@ -411,7 +412,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
assert fn is not None, "padding string wasn't found"
# list of tuples (source, destination)
staging = representation.get("stagingDir")
staging = self.anatomy.fill_roots(staging)
staging = self.anatomy.fill_root(staging)
resource_files.append(
(frame,
os.path.join(staging,
@ -588,7 +589,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
host_name = os.environ.get("AVALON_APP", "")
collections, remainders = clique.assemble(exp_files)
# create representation for every collected sequento ce
# create representation for every collected sequence
for collection in collections:
ext = collection.tail.lstrip(".")
preview = False
@ -656,7 +657,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
self._solve_families(instance, preview)
# add reminders as representations
# add remainders as representations
for remainder in remainders:
ext = remainder.split(".")[-1]
@ -676,7 +677,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
"name": ext,
"ext": ext,
"files": os.path.basename(remainder),
"stagingDir": os.path.dirname(remainder),
"stagingDir": staging,
}
preview = match_aov_pattern(
@ -1060,7 +1061,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
}
publish_job.update({"ftrack": ftrack})
metadata_path, roothless_metadata_path = self._create_metadata_path(
metadata_path, rootless_metadata_path = self._create_metadata_path(
instance)
self.log.info("Writing json file: {}".format(metadata_path))

View file

@ -91,7 +91,7 @@ class ValidateExpectedFiles(pyblish.api.InstancePlugin):
for job_id in render_job_ids:
job_info = self._get_job_info(job_id)
frame_list = job_info["Props"]["Frames"]
frame_list = job_info["Props"].get("Frames")
if frame_list:
all_frame_lists.extend(frame_list.split(','))

View file

@ -83,6 +83,11 @@ class CollectShotgridSession(pyblish.api.ContextPlugin):
"login to shotgrid withing openpype Tray"
)
# Set OPENPYPE_SG_USER with login so other deadline tasks can make
# use of it
self.log.info("Setting OPENPYPE_SG_USER to '%s'.", login)
os.environ["OPENPYPE_SG_USER"] = login
session = shotgun_api3.Shotgun(
base_url=shotgrid_url,
script_name=shotgrid_script_name,

View file

@ -7,7 +7,7 @@ from openpype.pipeline.publish import get_publish_repre_path
class IntegrateShotgridPublish(pyblish.api.InstancePlugin):
"""
Create published Files from representations and add it to version. If
representation is tagged add shotgrid review, it will add it in
representation is tagged as shotgrid review, it will add it in
path to movie for a movie file or path to frame for an image sequence.
"""
@ -27,11 +27,11 @@ class IntegrateShotgridPublish(pyblish.api.InstancePlugin):
local_path = get_publish_repre_path(
instance, representation, False
)
code = os.path.basename(local_path)
if representation.get("tags", []):
continue
code = os.path.basename(local_path)
published_file = self._find_existing_publish(
code, context, shotgrid_version
)

View file

@ -37,9 +37,9 @@ class IntegrateShotgridVersion(pyblish.api.InstancePlugin):
self.log.info("Use existing Shotgrid version: {}".format(version))
data_to_update = {}
status = context.data.get("intent", {}).get("value")
if status:
data_to_update["sg_status_list"] = status
intent = context.data.get("intent")
if intent:
data_to_update["sg_status_list"] = intent["value"]
for representation in instance.data.get("representations", []):
local_path = get_publish_repre_path(

View file

@ -12,6 +12,7 @@ import pyblish.api
from openpype.lib import (
Logger,
import_filepath,
filter_profiles
)
from openpype.settings import (
@ -301,12 +302,8 @@ def publish_plugins_discover(paths=None):
if not mod_ext == ".py":
continue
module = types.ModuleType(mod_name)
module.__file__ = abspath
try:
with open(abspath, "rb") as f:
six.exec_(f.read(), module.__dict__)
module = import_filepath(abspath, mod_name)
# Store reference to original module, to avoid
# garbage collection from collecting it's global
@ -683,6 +680,12 @@ def get_publish_repre_path(instance, repre, only_published=False):
staging_dir = repre.get("stagingDir")
if not staging_dir:
staging_dir = get_instance_staging_dir(instance)
# Expand the staging dir path in case it's been stored with the root
# template syntax
anatomy = instance.context.data["anatomy"]
staging_dir = anatomy.fill_root(staging_dir)
src_path = os.path.normpath(os.path.join(staging_dir, filename))
if os.path.exists(src_path):
return src_path

View file

@ -23,4 +23,4 @@
],
"tools_env": [],
"active": true
}
}

View file

@ -255,4 +255,4 @@
]
}
}
}
}

View file

@ -4,4 +4,4 @@
"darwin": "/Volumes/path",
"linux": "/mnt/share/projects"
}
}
}

View file

@ -41,4 +41,4 @@
"Compositing": {
"short_name": "comp"
}
}
}

View file

@ -66,4 +66,4 @@
"source": "source"
}
}
}
}

View file

@ -33,4 +33,4 @@
"create_first_version": false,
"custom_templates": []
}
}
}

View file

@ -82,4 +82,4 @@
"active": false
}
}
}
}

View file

@ -16,4 +16,4 @@
"anatomy_template_key_metadata": "render"
}
}
}
}

View file

@ -163,4 +163,4 @@
]
}
}
}
}

View file

@ -496,4 +496,4 @@
"farm_status_profiles": []
}
}
}
}

View file

@ -17,4 +17,4 @@
}
}
}
}
}

View file

@ -607,4 +607,4 @@
"linux": []
},
"project_environments": {}
}
}

View file

@ -50,4 +50,4 @@
"skip_timelines_check": []
}
}
}
}

View file

@ -97,4 +97,4 @@
}
]
}
}
}

View file

@ -76,4 +76,4 @@
"active": true
}
}
}
}

View file

@ -10,4 +10,4 @@
"note_status_shortname": "wfa"
}
}
}
}

View file

@ -5,4 +5,4 @@
"image_format": "exr",
"multipass": true
}
}
}

View file

@ -533,4 +533,4 @@
"profiles": []
},
"filters": {}
}
}

View file

@ -67,4 +67,4 @@
"create_first_version": false,
"custom_templates": []
}
}
}

View file

@ -27,4 +27,4 @@
"handleEnd": 10
}
}
}
}

View file

@ -4,4 +4,4 @@
"review": true
}
}
}
}

View file

@ -19,4 +19,4 @@
"step": "step"
}
}
}
}

View file

@ -17,4 +17,4 @@
]
}
}
}
}

View file

@ -321,4 +321,4 @@
"active": true
}
}
}
}

View file

@ -109,4 +109,4 @@
"custom_templates": []
},
"filters": {}
}
}

View file

@ -14,4 +14,4 @@
"project_setup": {
"dev_mode": true
}
}
}

View file

@ -141,4 +141,4 @@
"layer_name_regex": "(?P<layer>L[0-9]{3}_\\w+)_(?P<pass>.+)"
}
}
}
}

View file

@ -1302,7 +1302,9 @@
"variant_label": "Current",
"use_python_2": false,
"executables": {
"windows": ["C:/Program Files/CelAction/CelAction2D Studio/CelAction2D.exe"],
"windows": [
"C:/Program Files/CelAction/CelAction2D Studio/CelAction2D.exe"
],
"darwin": [],
"linux": []
},
@ -1365,4 +1367,4 @@
}
},
"additional_apps": {}
}
}

View file

@ -18,4 +18,4 @@
"production_version": "",
"staging_version": "",
"version_check_interval": 5
}
}

View file

@ -211,4 +211,4 @@
"linux": ""
}
}
}
}

View file

@ -87,4 +87,4 @@
"renderman": "Pixar Renderman"
}
}
}
}

View file

@ -440,8 +440,9 @@ class RootEntity(BaseItemEntity):
os.makedirs(dirpath)
self.log.debug("Saving data to: {}\n{}".format(subpath, value))
data = json.dumps(value, indent=4) + "\n"
with open(output_path, "w") as file_stream:
json.dump(value, file_stream, indent=4)
file_stream.write(data)
dynamic_values_item = self.collect_dynamic_schema_entities()
dynamic_values_item.save_values()