|
2 | 2 | # This file has been modified by Epic Games, Inc. and is subject to the license
|
3 | 3 | # file included in this repository.
|
4 | 4 |
|
5 |
| -import sgtk |
| 5 | +from collections import namedtuple, defaultdict |
| 6 | +import copy |
6 | 7 | import os
|
7 | 8 |
|
| 9 | +import unreal |
| 10 | + |
| 11 | +import sgtk |
| 12 | + |
| 13 | +# A named tuple to store LevelSequence edits: the sequence/track/section |
| 14 | +# the edit is in. |
| 15 | +SequenceEdit = namedtuple("SequenceEdit", ["sequence", "track", "section"]) |
| 16 | + |
8 | 17 |
|
9 | 18 | HookBaseClass = sgtk.get_hook_baseclass()
|
10 | 19 |
|
@@ -129,26 +138,170 @@ def collect_current_session(self, settings, parent_item):
|
129 | 138 |
|
130 | 139 | return session_item
|
131 | 140 |
|
| 141 | + def create_asset_item(self, parent_item, asset_path, asset_type, asset_name, display_name=None): |
| 142 | + """ |
| 143 | + Create an unreal item under the given parent item. |
| 144 | +
|
| 145 | + :param asset_path: The unreal asset path, as a string. |
| 146 | + :param asset_type: The unreal asset type, as a string. |
| 147 | + :param asset_name: The unreal asset name, as a string. |
| 148 | + :param display_name: Optional display name for the item. |
| 149 | + :returns: The created item. |
| 150 | + """ |
| 151 | + item_type = "unreal.asset.%s" % asset_type |
| 152 | + asset_item = parent_item.create_item( |
| 153 | + item_type, # Include the asset type for the publish plugin to use |
| 154 | + asset_type, # Display type |
| 155 | + display_name or asset_name, # Display name of item instance |
| 156 | + ) |
| 157 | + |
| 158 | + # Set asset properties which can be used by publish plugins |
| 159 | + asset_item.properties["asset_path"] = asset_path |
| 160 | + asset_item.properties["asset_name"] = asset_name |
| 161 | + asset_item.properties["asset_type"] = asset_type |
| 162 | + return asset_item |
| 163 | + |
132 | 164 | def collect_selected_assets(self, parent_item):
|
133 | 165 | """
|
134 | 166 | Creates items for assets selected in Unreal.
|
135 | 167 |
|
136 | 168 | :param parent_item: Parent Item instance
|
137 | 169 | """
|
138 | 170 | unreal_sg = sgtk.platform.current_engine().unreal_sg_engine
|
| 171 | + sequence_edits = None |
139 | 172 | # Iterate through the selected assets and get their info and add them as items to be published
|
140 | 173 | for asset in unreal_sg.selected_assets:
|
141 |
| - asset_name = str(asset.asset_name) |
142 |
| - asset_type = str(asset.asset_class) |
143 |
| - |
144 |
| - item_type = "unreal.asset." + asset_type |
145 |
| - asset_item = parent_item.create_item( |
146 |
| - item_type, # Include the asset type for the publish plugin to use |
147 |
| - asset_type, # display type |
148 |
| - asset_name # display name of item instance |
| 174 | + if asset.asset_class == "LevelSequence": |
| 175 | + if sequence_edits is None: |
| 176 | + sequence_edits = self.retrieve_sequence_edits() |
| 177 | + self.collect_level_sequence(parent_item, asset, sequence_edits) |
| 178 | + else: |
| 179 | + self.create_asset_item( |
| 180 | + parent_item, |
| 181 | + # :class:`Name` instances, we cast them to strings otherwise |
| 182 | + # string operations fail down the line.. |
| 183 | + "%s" % asset.object_path, |
| 184 | + "%s" % asset.asset_class, |
| 185 | + "%s" % asset.asset_name, |
| 186 | + ) |
| 187 | + |
| 188 | + def get_all_paths_from_sequence(self, level_sequence, sequence_edits, visited=None): |
| 189 | + """ |
| 190 | + Retrieve all edit paths from the given Level Sequence to top Level Sequences. |
| 191 | +
|
| 192 | + Recursively explore the sequence edits, stop the recursion when a Level |
| 193 | + Sequence which is not a sub-sequence of another is reached. |
| 194 | +
|
| 195 | + Lists of Level Sequences are returned, where each list contains all the |
| 196 | + the Level Sequences to traverse to reach the top Level Sequence from the |
| 197 | + starting Level Sequence. |
| 198 | +
|
| 199 | + For example if a master Level Sequence contains some `Seq_<seq number>` |
| 200 | + sequences and each of them contains shots like `Shot_<seq number>_<shot number>`, |
| 201 | + a path for Shot_001_010 would be `[Shot_001_010, Seq_001, Master sequence]`. |
| 202 | +
|
| 203 | + If an alternate Cut is maintained with another master level Sequence, both |
| 204 | + paths would be detected and returned by this method, e.g. |
| 205 | + `[[Shot_001_010, Seq_001, Master sequence], [Shot_001_010, Seq_001, Master sequence 2]]` |
| 206 | +
|
| 207 | + Maintain a list of visited Level Sequences to detect cycles. |
| 208 | +
|
| 209 | + :param level_sequence: A :class:`unreal.LevelSequence` instance. |
| 210 | + :param sequence_edits: A dictionary with :class:`unreal.LevelSequence as keys and |
| 211 | + lists of :class:`SequenceEdit` as values. |
| 212 | + :param visited: A list of :class:`unreal.LevelSequence` instances, populated |
| 213 | + as nodes are visited. |
| 214 | + :returns: A list of lists of Level Sequences. |
| 215 | + """ |
| 216 | + if not visited: |
| 217 | + visited = [] |
| 218 | + visited.append(level_sequence) |
| 219 | + self.logger.info("Treating %s" % level_sequence.get_name()) |
| 220 | + if not sequence_edits[level_sequence]: |
| 221 | + # No parent, return a list with a single entry with the current |
| 222 | + # sequence |
| 223 | + return [[level_sequence]] |
| 224 | + |
| 225 | + all_paths = [] |
| 226 | + # Loop over parents get all paths starting from them |
| 227 | + for edit in sequence_edits[level_sequence]: |
| 228 | + if edit.sequence in visited: |
| 229 | + self.logger.warning( |
| 230 | + "Detected a cycle in edits path %s to %s" % ( |
| 231 | + "->".join(visited), edit.sequence |
| 232 | + ) |
| 233 | + ) |
| 234 | + else: |
| 235 | + # Get paths from the parent and prepend the current sequence |
| 236 | + # to them. |
| 237 | + for edit_path in self.get_all_paths_from_sequence( |
| 238 | + edit.sequence, |
| 239 | + sequence_edits, |
| 240 | + copy.copy(visited), # Each visit needs its own stack |
| 241 | + ): |
| 242 | + self.logger.info("Got %s from %s" % (edit_path, edit.sequence.get_name())) |
| 243 | + all_paths.append([level_sequence] + edit_path) |
| 244 | + return all_paths |
| 245 | + |
| 246 | + def collect_level_sequence(self, parent_item, asset, sequence_edits): |
| 247 | + """ |
| 248 | + Collect the items for the given Level Sequence asset. |
| 249 | +
|
| 250 | + Multiple items can be collected for a given Level Sequence if it appears |
| 251 | + in multiple edits. |
| 252 | +
|
| 253 | + :param parent_item: Parent Item instance. |
| 254 | + :param asset: An Unreal LevelSequence asset. |
| 255 | + :param sequence_edits: A dictionary with :class:`unreal.LevelSequence as keys and |
| 256 | + lists of :class:`SequenceEdit` as values. |
| 257 | + """ |
| 258 | + level_sequence = unreal.load_asset(asset.object_path) |
| 259 | + for edits_path in self.get_all_paths_from_sequence(level_sequence, sequence_edits): |
| 260 | + # Reverse the path to have it from top master sequence to the shot. |
| 261 | + edits_path.reverse() |
| 262 | + self.logger.info("Collected %s" % [x.get_name() for x in edits_path]) |
| 263 | + if len(edits_path) > 1: |
| 264 | + display_name = "%s (%s)" % (edits_path[0].get_name(), edits_path[-1].get_name()) |
| 265 | + else: |
| 266 | + display_name = edits_path[0].get_name() |
| 267 | + item = self.create_asset_item( |
| 268 | + parent_item, |
| 269 | + edits_path[0].get_path_name(), |
| 270 | + "LevelSequence", |
| 271 | + edits_path[0].get_name(), |
| 272 | + display_name, |
149 | 273 | )
|
| 274 | + # Store the edits on the item so we can leverage them later when |
| 275 | + # publishing. |
| 276 | + item.properties["edits_path"] = edits_path |
150 | 277 |
|
151 |
| - # Asset properties that can be used by publish plugins |
152 |
| - asset_item.properties["asset_path"] = asset.object_path |
153 |
| - asset_item.properties["asset_name"] = asset_name |
154 |
| - asset_item.properties["asset_type"] = asset_type |
| 278 | + def retrieve_sequence_edits(self): |
| 279 | + """ |
| 280 | + Build a dictionary for all Level Sequences where keys are Level Sequences |
| 281 | + and values the list of edits they are in. |
| 282 | +
|
| 283 | + :returns: A dictionary of :class:`unreal.LevelSequence` where values are |
| 284 | + lists of :class:`SequenceEdit`. |
| 285 | + """ |
| 286 | + sequence_edits = defaultdict(list) |
| 287 | + |
| 288 | + asset_helper = unreal.AssetRegistryHelpers.get_asset_registry() |
| 289 | + # Retrieve all Level Sequence assets |
| 290 | + all_level_sequences = asset_helper.get_assets_by_class("LevelSequence") |
| 291 | + for lvseq_asset in all_level_sequences: |
| 292 | + lvseq = unreal.load_asset(lvseq_asset.object_path, unreal.LevelSequence) |
| 293 | + # Check shots |
| 294 | + for track in lvseq.find_master_tracks_by_type(unreal.MovieSceneCinematicShotTrack): |
| 295 | + for section in track.get_sections(): |
| 296 | + # Not sure if you can have anything else than a MovieSceneSubSection |
| 297 | + # in a MovieSceneCinematicShotTrack, but let's be cautious here. |
| 298 | + try: |
| 299 | + # Get the Sequence attached to the section and check if |
| 300 | + # it is the one we're looking for. |
| 301 | + section_seq = section.get_sequence() |
| 302 | + sequence_edits[section_seq].append( |
| 303 | + SequenceEdit(lvseq, track, section) |
| 304 | + ) |
| 305 | + except AttributeError: |
| 306 | + pass |
| 307 | + return sequence_edits |
0 commit comments