/* SPDX-FileCopyrightText: 2024 Blender Authors * * SPDX-License-Identifier: GPL-2.0-or-later */ /** \file * \ingroup animrig */ /* This is versioning code, so it's allowed to touch on deprecated DNA fields. */ #define DNA_DEPRECATED_ALLOW #include "ANIM_action.hh" #include "ANIM_action_iterators.hh" #include "ANIM_action_legacy.hh" #include "ANIM_versioning.hh" #include "DNA_action_types.h" #include "BKE_lib_id.hh" #include "BKE_main.hh" #include "BKE_nla.hh" #include "BKE_node.hh" #include "BKE_report.hh" #include "BLI_listbase.h" #include "BLI_string_utf8.h" #include "BLT_translation.hh" #include "BLO_readfile.hh" #include "RNA_access.hh" #include "RNA_prototypes.hh" namespace blender::animrig::versioning { bool action_is_layered(const bAction &dna_action) { /* NOTE: due to how forward-compatibility is handled when writing Actions to * blend files, it is important that this function does NOT check * `Action.idroot` as part of its determination of whether this is a layered * action or not. * * See: `action_blend_write()` and `action_blend_read_data()` */ const animrig::Action &action = dna_action.wrap(); const bool has_layered_data = action.layer_array_num > 0 || action.slot_array_num > 0; const bool has_animato_data = !(action.curves.is_empty() && action.groups.is_empty()); return has_layered_data || !has_animato_data; } void convert_legacy_animato_actions(Main &bmain) { for (bAction &dna_action : bmain.actions) { animrig::Action &action = dna_action.wrap(); if (action_is_layered(action) && !action.is_empty()) { /* This is just a safety net. Blender files that trigger this versioning code are not * expected to have any layered/slotted Actions. * * Empty Actions, even though they are valid "layered" Actions, should still get through * versioning, though, to ensure they have the default "Legacy Slot" and a zero idroot. */ continue; } convert_legacy_animato_action(action); } } void convert_legacy_animato_action(bAction &dna_action) { Action &action = dna_action.wrap(); /* Check that this is a legacy action. * Cannot use `!action_is_layered` because that would be false on empty actions. */ BLI_assert(action.layer_array_num == 0 && action.slot_array_num == 0); /* Store this ahead of time, because adding the slot sets the action's idroot * to 0. We also set the action's idroot to 0 manually, just to be defensive * so we don't depend on esoteric behavior in `slot_add()`. */ const int16_t idtype = action.idroot; action.idroot = 0; /* Initialize the Action's last_slot_handle field to its default value, before * we create a new slot. */ action.last_slot_handle = DNA_DEFAULT_ACTION_LAST_SLOT_HANDLE; Slot &slot = action.slot_add(); slot.idtype = idtype; const std::string slot_identifier{slot.idtype_string() + DATA_(legacy::DEFAULT_LEGACY_SLOT_NAME)}; action.slot_identifier_define(slot, slot_identifier); Layer &layer = action.layer_add(DATA_(legacy::DEFAULT_LEGACY_LAYER_NAME)); animrig::Strip &strip = layer.strip_add(action, animrig::Strip::Type::Keyframe); Channelbag &bag = strip.data(action).channelbag_for_slot_ensure(slot); const int fcu_count = action.curves.count(); const int group_count = action.groups.count(); bag.fcurve_array = MEM_new_array_zeroed(fcu_count, "Action versioning - fcurves"); bag.fcurve_array_num = fcu_count; bag.group_array = MEM_new_array_zeroed(group_count, "Action versioning - groups"); bag.group_array_num = group_count; int fcurve_index = 0; for (const auto [group_index, group] : action.groups.enumerate()) { bag.group_array[group_index] = &group; group.channelbag = &bag; group.fcurve_range_start = fcurve_index; for (FCurve &fcu : group.channels) { if (fcu.grp != &group) { break; } bag.fcurve_array[fcurve_index++] = &fcu; } group.fcurve_range_length = fcurve_index - group.fcurve_range_start; } for (FCurve &fcu : action.curves) { /* Any fcurves with groups have already been added to the fcurve array. */ if (fcu.grp) { continue; } bag.fcurve_array[fcurve_index++] = &fcu; } BLI_assert(fcurve_index == fcu_count); action.curves = {nullptr, nullptr}; action.groups = {nullptr, nullptr}; } void tag_action_user_for_slotted_actions_conversion(ID &animated_id) { animated_id.runtime->readfile_data->tags.action_assignment_needs_slot = true; } void tag_action_users_for_slotted_actions_conversion(Main &bmain) { /* This function is only called when the blend-file is old enough to NOT use * slotted Actions, so we can safely tag anything that uses an Action. */ auto flag_adt = [](ID &animated_id, bAction *& /*action_ptr_ref*/, slot_handle_t & /*slot_handle_ref*/, char * /*last_slot_identifier*/) -> bool { tag_action_user_for_slotted_actions_conversion(animated_id); /* Once tagged, the foreach loop can stop, because more tagging of the same * ID doesn't do anything. */ return false; }; ID *id; FOREACH_MAIN_ID_BEGIN (&bmain, id) { foreach_action_slot_use_with_references(*id, flag_adt); /* Process embedded IDs, as these are not listed in bmain, but still can * have their own Action+Slot. Unfortunately there is no generic looper * for embedded IDs. At this moment the only animatable embedded ID is a * node tree. */ bNodeTree *node_tree = bke::node_tree_from_id(id); if (node_tree) { foreach_action_slot_use_with_references(node_tree->id, flag_adt); } } FOREACH_MAIN_ID_END; } void convert_legacy_action_assignments(Main &bmain, ReportList *reports) { auto version_slot_assignment = [&](ID &animated_id, bAction *dna_action, PointerRNA &action_slot_owner_ptr, PropertyRNA &action_slot_prop, char *last_used_slot_identifier) { BLI_assert(dna_action); /* Ensured by the foreach loop. */ Action &action = dna_action->wrap(); if (action.slot_array_num == 0) { /* There's a few reasons why this Action doesn't have a slot. It could simply be a slotted * Action without slots, or a legacy-but-not-yet-versioned Action, or it could be it is a * _really_ old (pre-2.50) Action. The latter are upgraded in do_versions_after_setup(), but * this function can be called earlier than that. So better gracefully skip those. */ return true; } /* If there is already a slot assigned, there's nothing to do here. */ PointerRNA current_slot_ptr = RNA_property_pointer_get(&action_slot_owner_ptr, &action_slot_prop); if (current_slot_ptr.data) { return true; } /* Reset the "last used slot identifier" to the default "Legacy Slot". That way * generic_slot_for_autoassign() will pick up on legacy slots automatically. * * Note that this function should only run on legacy users of Actions, i.e. they are not * expected to have any last-used slot at all. The field in DNA can still be set, though, * because the 4.3 code already has the data model for slotted Actions. */ /* Ensure that the identifier has the correct ID type prefix. */ *reinterpret_cast(last_used_slot_identifier) = GS(animated_id.name); static_assert(Slot::identifier_length_max > 2); /* Because of the -2 below. */ BLI_strncpy_utf8(last_used_slot_identifier + 2, DATA_(legacy::DEFAULT_LEGACY_SLOT_NAME), Slot::identifier_length_max - 2); Slot *slot_to_assign = generic_slot_for_autoassign( animated_id, action, last_used_slot_identifier); if (!slot_to_assign) { /* This means that there is no slot that can be found by name, not even the "Legacy Slot" * name. Keep the ID unanimated, as this means that the referenced Action has changed * significantly since this file was opened. */ BKE_reportf(reports, RPT_WARNING, "\"%s\" is using Action \"%s\", which does not have a slot with identifier " "\"%s\" or \"%s\". Manually assign the right action slot to \"%s\".\n", animated_id.name, action.id.name + 2, last_used_slot_identifier, animated_id.name, animated_id.name + 2); return true; } PointerRNA slot_to_assign_ptr = RNA_pointer_create_discrete( &action.id, RNA_ActionSlot, slot_to_assign); RNA_property_pointer_set( &action_slot_owner_ptr, &action_slot_prop, slot_to_assign_ptr, reports); RNA_property_update_main(&bmain, nullptr, &action_slot_owner_ptr, &action_slot_prop); return true; }; /* Note that the code below does not remove the `action_assignment_needs_slot` tag. One ID can * use multiple Actions (via NLA, Action constraints, etc.); if one of those Action is a legacy * one from a linked datablock, this ID may needs to be re-visited after the library file was * versioned. Rather than trying to figure out if re-visiting is necessary, this function is safe * to call multiple times, and all that's lost is a little bit of CPU time. */ ID *id; FOREACH_MAIN_ID_BEGIN (&bmain, id) { /* Process the ID itself. */ if (BLO_readfile_id_runtime_tags(*id).action_assignment_needs_slot) { foreach_action_slot_use_with_rna(*id, version_slot_assignment); } /* Process embedded IDs, as these are not listed in bmain, but still can * have their own Action+Slot. Unfortunately there is no generic looper * for embedded IDs. At this moment the only animatable embedded ID is a * node tree. */ bNodeTree *node_tree = bke::node_tree_from_id(id); if (node_tree && BLO_readfile_id_runtime_tags(node_tree->id).action_assignment_needs_slot) { foreach_action_slot_use_with_rna(node_tree->id, version_slot_assignment); } } FOREACH_MAIN_ID_END; } void action_groups_reconstruct(bAction *act) { if (!act) { return; } /* Check that this is a legacy action. * Cannot use `!action_is_layered` because that would be false on empty actions. */ BLI_assert(act->layer_array_num == 0 && act->slot_array_num == 0); /* Clear out all group channels. Channels that are actually in use are * reconstructed below; this step is necessary to clear out unused groups. */ for (bActionGroup &group : act->groups) { group.channels.clear_no_delete(); } /* Sort the channels into the group lists, destroying the act->curves list. */ ListBaseT ungrouped = {nullptr, nullptr}; for (FCurve &fcurve : act->curves.items_mutable()) { if (fcurve.grp) { BLI_assert(BLI_findindex(&act->groups, fcurve.grp) >= 0); BLI_addtail(&fcurve.grp->channels, &fcurve); } else { BLI_addtail(&ungrouped, &fcurve); } } /* Recombine into the main list. */ act->curves.clear_no_delete(); for (bActionGroup &group : act->groups) { /* Copy the list header to preserve the pointers in the group. */ ListBase tmp = group.channels; BLI_movelisttolist(&act->curves, &tmp); } BLI_movelisttolist(&act->curves, &ungrouped); } using IDFCurveCallback = FunctionRef; /** * Iterates over FCurves until the callback returns false or all FCurves were visited. * * \returns true if all FCurves were visited. */ static bool fcurves_listbase_apply_cb(ID *id, ListBaseT *fcurves, const IDFCurveCallback func) { for (FCurve &fcu : *fcurves) { if (!func(id, &fcu)) { return false; } } return true; } /* Helper for adt_apply_all_fcurves_cb() - Recursively go through each NLA strip */ static bool nlastrips_apply_all_curves_cb(ID *id, ListBaseT *strips, const IDFCurveCallback func) { for (NlaStrip &strip : *strips) { if (strip.act) { if (!fcurves_listbase_apply_cb(id, &strip.act->curves, func)) { return false; } } if (!nlastrips_apply_all_curves_cb(id, &strip.strips, func)) { return false; } } return true; } static bool adt_apply_all_fcurves_cb(ID *id, AnimData *adt, const IDFCurveCallback func) { if (adt->action) { if (!fcurves_listbase_apply_cb(id, &adt->action->curves, func)) { return false; } } if (adt->tmpact) { if (!fcurves_listbase_apply_cb(id, &adt->tmpact->curves, func)) { return false; } } /* Drivers, stored as a list of F-Curves. */ if (!fcurves_listbase_apply_cb(id, &adt->drivers, func)) { return false; } /* NLA Data - Animation Data for Strips */ for (NlaTrack &nlt : adt->nla_tracks) { if (!nlastrips_apply_all_curves_cb(id, &nlt.strips, func)) { return false; } } return true; } void fcurves_id_cb(ID *id, const FunctionRef func) { AnimData *adt = BKE_animdata_from_id(id); if (adt != nullptr) { /* Use a little wrapper function to always return 'true' and thus keep the loop looping. */ const auto wrapper = [&func](ID *id, FCurve *fcurve) { func(id, fcurve); return true; }; adt_apply_all_fcurves_cb(id, adt, wrapper); } } void fcurves_main_cb(Main *bmain, const FunctionRef func) { /* Use a little wrapper function to always return 'true' and thus keep the loop looping. */ const auto wrapper = [&func](ID *id, FCurve *fcurve) { func(id, fcurve); return true; }; /* Use the AnimData-based function so that we don't have to reimplement all that stuff */ BKE_animdata_main_cb(bmain, [&](ID *id, AnimData *adt) { adt_apply_all_fcurves_cb(id, adt, wrapper); }); } Vector fcurves_for_legacy_action(bAction *action) { if (!action) { return {}; } Vector fcurves; for (FCurve &fcu : action->curves) { fcurves.append(&fcu); } return fcurves; } } // namespace blender::animrig::versioning