mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-09 04:49:39 -10:00
1625 lines
58 KiB
C++
1625 lines
58 KiB
C++
#include "model/modelanimation.h"
|
|
#include "model/modelanimation_segments.h"
|
|
#include "model/modelanimation_moveables.h"
|
|
#include "network/multi.h"
|
|
#include "network/multimsgs.h"
|
|
#include "parse/encrypt.h"
|
|
#include "ship/ship.h"
|
|
#include "utils/Random.h"
|
|
|
|
|
|
extern float flFrametime;
|
|
|
|
namespace animation {
|
|
|
|
flag_def_list_new<animation::Animation_Flags> Animation_flags[] = {
|
|
{ "auto reverse", animation::Animation_Flags::Auto_Reverse, true, false },
|
|
{ "reset at completion", animation::Animation_Flags::Reset_at_completion, true, false },
|
|
{ "loop", animation::Animation_Flags::Loop, true, false },
|
|
{ "random starting phase", animation::Animation_Flags::Random_starting_phase, true, false },
|
|
{ "pause on reverse", animation::Animation_Flags::Pause_on_reverse, true, false },
|
|
};
|
|
|
|
const size_t Num_animation_flags = sizeof(Animation_flags) / sizeof(flag_def_list_new<animation::Animation_Flags>);
|
|
|
|
std::map<int, ModelAnimationSet::RunningAnimationList> ModelAnimationSet::s_runningAnimations;
|
|
std::map<unsigned int, std::shared_ptr<ModelAnimation>> ModelAnimationSet::s_animationById;
|
|
|
|
ModelAnimation::ModelAnimation(bool isInitialType, bool isMultiCompatible, bool canStateChange, const ModelAnimationSet* defaultSet)
|
|
: m_set(defaultSet), m_isInitialType(isInitialType), m_isMultiCompatible(isMultiCompatible), m_canChangeState(canStateChange)
|
|
{ }
|
|
|
|
void ModelAnimation::setAnimation(std::shared_ptr<ModelAnimationSegment> animation) {
|
|
m_animation = std::move(animation);
|
|
}
|
|
|
|
void ModelAnimation::forceRecalculate(polymodel_instance* pmi) {
|
|
Assertion(m_canChangeState, "Attempted to force recalculation of an animation that is not allowed to state-change!");
|
|
if(!m_canChangeState)
|
|
return;
|
|
|
|
instance_data& instanceData = m_instances[pmi->id];
|
|
|
|
instanceData.state = ModelAnimationState::NEED_RECALC;
|
|
instanceData.time = 0;
|
|
}
|
|
|
|
ModelAnimationState ModelAnimation::play(float frametime, polymodel_instance* pmi, ModelAnimationSubmodelBuffer& applyBuffer, bool applyOnly) {
|
|
instance_data& instanceData = m_instances[pmi->id];
|
|
|
|
if (applyOnly) {
|
|
m_animation->calculateAnimation(applyBuffer, instanceData.time, pmi->id);
|
|
|
|
return instanceData.state;
|
|
}
|
|
|
|
float prevTime = instanceData.time;
|
|
|
|
switch (instanceData.state) {
|
|
case ModelAnimationState::UNTRIGGERED:
|
|
//We have a new animation starting up in this phase. Put it in the list of running animations to track and step it later
|
|
if (!m_isInitialType) {
|
|
auto& animEntry = ModelAnimationSet::s_runningAnimations[pmi->id];
|
|
animEntry.parentSet = m_set;
|
|
|
|
auto thisPtr = shared_from_this();
|
|
|
|
animEntry.animationList.push_back(thisPtr);
|
|
}
|
|
|
|
/* fall-thru */
|
|
case ModelAnimationState::NEED_RECALC:
|
|
//Store the submodels current data as the base for this animation and calculate this animations parameters
|
|
m_animation->recalculate(applyBuffer, pmi);
|
|
|
|
instanceData.duration = m_animation->getDuration(pmi->id);
|
|
instanceData.state = ModelAnimationState::RUNNING_FWD;
|
|
|
|
if (m_flags[Animation_Flags::Random_starting_phase]) {
|
|
instanceData.time = util::Random::next() * util::Random::INV_F_MAX_VALUE * instanceData.duration;
|
|
//Check if we might be on the way backwards
|
|
if (m_flags[Animation_Flags::Loop, Animation_Flags::Auto_Reverse] && !m_flags[Animation_Flags::Reset_at_completion] && util::Random::flip_coin()) {
|
|
instanceData.state = ModelAnimationState::RUNNING_RWD;
|
|
break; //In this case, we must delay for a frame
|
|
}
|
|
}
|
|
|
|
/* fall-thru */
|
|
case ModelAnimationState::RUNNING_FWD:
|
|
instanceData.time += frametime;
|
|
|
|
//Cap time if needed
|
|
if (instanceData.time > instanceData.duration) {
|
|
instanceData.time = instanceData.duration;
|
|
if (m_flags[Animation_Flags::Auto_Reverse])
|
|
//Reverse
|
|
instanceData.state = ModelAnimationState::RUNNING_RWD;
|
|
else if (m_flags[Animation_Flags::Loop]) {
|
|
if (m_flags[Animation_Flags::Reset_at_completion]) {
|
|
//Loop from start
|
|
instanceData.time = 0;
|
|
}
|
|
else {
|
|
//Loop back
|
|
instanceData.state = ModelAnimationState::RUNNING_RWD;
|
|
}
|
|
}
|
|
else if (m_flags[Animation_Flags::Reset_at_completion])
|
|
//Stop animation at start, not end
|
|
stop(pmi, false);
|
|
else
|
|
//Normal execution, stop aniamtion at end
|
|
instanceData.state = ModelAnimationState::COMPLETED;
|
|
}
|
|
|
|
m_animation->calculateAnimation(applyBuffer, instanceData.time, pmi->id);
|
|
m_animation->executeAnimation(applyBuffer, prevTime, instanceData.time, ModelAnimationDirection::FWD, pmi->id);
|
|
break;
|
|
|
|
case ModelAnimationState::COMPLETED:
|
|
//This means someone requested to start once we were complete, so start moving backwards.
|
|
instanceData.state = ModelAnimationState::RUNNING_RWD;
|
|
/* fall-thru */
|
|
case ModelAnimationState::RUNNING_RWD:
|
|
instanceData.time -= frametime;
|
|
|
|
//Cap time at 0, but don't clean up the animations here since this function is called in a loop over the running animations, and cleaning now would invalidate the iterator
|
|
if (instanceData.time < 0) {
|
|
if (m_flags[Animation_Flags::Loop]) {
|
|
if (m_flags[Animation_Flags::Reset_at_completion]) {
|
|
//Loop from end. This happens when a Loop + Reset at completion animation is started in reverse.
|
|
instanceData.time = instanceData.duration;
|
|
}
|
|
else {
|
|
//Loop back
|
|
instanceData.time = 0;
|
|
instanceData.state = ModelAnimationState::RUNNING_FWD;
|
|
}
|
|
}
|
|
else
|
|
stop(pmi, false);
|
|
}
|
|
|
|
m_animation->calculateAnimation(applyBuffer, instanceData.time, pmi->id);
|
|
m_animation->executeAnimation(applyBuffer, instanceData.time, prevTime, ModelAnimationDirection::RWD, pmi->id);
|
|
break;
|
|
case ModelAnimationState::PAUSED:
|
|
//Shouldn't happen. Paused Animations are only allowed to be apply only.
|
|
UNREACHABLE("Tried to play a paused animation without starting it. Get a coder.");
|
|
break;
|
|
}
|
|
|
|
return instanceData.state;
|
|
}
|
|
|
|
void ModelAnimation::start(polymodel_instance* pmi, ModelAnimationDirection direction, bool force, bool instant, bool pause, const float* multiOverrideTime) {
|
|
if (pmi == nullptr)
|
|
return;
|
|
|
|
instance_data& instanceData = m_instances[pmi->id];
|
|
|
|
if (multiOverrideTime == nullptr && m_isMultiCompatible && (Game_mode & GM_MULTIPLAYER) && id != 0) {
|
|
//We are in multiplayer. Send animation to server to start. Server starts animation online, and sends start request back (which'll have multiOverride == true).
|
|
//If we _are_ the server, also just start the animation
|
|
|
|
object* objp = pmi->objnum > -1 ? &Objects[pmi->objnum] : nullptr;
|
|
|
|
if(objp != nullptr)
|
|
send_animation_triggered_packet(id, objp, 0, direction, force, instant, pause);
|
|
else {
|
|
//Find special mode based on id and send
|
|
}
|
|
|
|
if(MULTIPLAYER_CLIENT)
|
|
return;
|
|
}
|
|
|
|
if (pause || (direction == ModelAnimationDirection::RWD && m_flags[Animation_Flags::Pause_on_reverse])) {
|
|
if(instanceData.state != ModelAnimationState::UNTRIGGERED && instanceData.state != ModelAnimationState::NEED_RECALC && instanceData.state != ModelAnimationState::COMPLETED)
|
|
instanceData.state = ModelAnimationState::PAUSED;
|
|
return;
|
|
}
|
|
|
|
float timeOffset = multiOverrideTime != nullptr ? *multiOverrideTime : 0.0f;
|
|
|
|
//Make sure to recalculate the animation here, as otherwise we cannot inquire about things like length after starting.
|
|
//Don't apply just yet if it's a non-initial type, as there might be other animations this'd need to depend upon
|
|
ModelAnimationSubmodelBuffer applyBuffer;
|
|
m_set->initializeSubmodelBuffer(pmi, applyBuffer);
|
|
play(0, pmi, applyBuffer);
|
|
|
|
if (direction == ModelAnimationDirection::RWD) {
|
|
instanceData.state = ModelAnimationState::RUNNING_RWD;
|
|
instanceData.time -= timeOffset;
|
|
if (force)
|
|
instanceData.time = instant ? 0 : instanceData.duration - timeOffset;
|
|
}
|
|
else {
|
|
instanceData.state = ModelAnimationState::RUNNING_FWD;
|
|
instanceData.time += timeOffset;
|
|
if (force)
|
|
instanceData.time = instant ? instanceData.duration : 0 + timeOffset;
|
|
}
|
|
|
|
//Since initial types never get stepped, they need to be manually applied here once.
|
|
if (m_isInitialType) {
|
|
m_set->initializeSubmodelBuffer(pmi, applyBuffer);
|
|
play(0, pmi, applyBuffer);
|
|
ModelAnimationSet::apply(pmi, applyBuffer);
|
|
|
|
//Save the things modified by initial animations as actual baseline
|
|
for (const auto& initialModified : applyBuffer) {
|
|
if (initialModified.second.modified)
|
|
initialModified.first->saveCurrentAsBase(pmi, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ModelAnimation::stop(polymodel_instance* pmi, bool cleanup) {
|
|
//If inaccuracies occur, this needs to reset again, but only if no other animation is running on this submodel
|
|
/*for (const auto& animation : m_submodelAnimation) {
|
|
animation->reset(pmi);
|
|
}*/
|
|
|
|
if (!pmi)
|
|
return;
|
|
|
|
instance_data& instanceData = m_instances[pmi->id];
|
|
instanceData.time = 0.0f;
|
|
instanceData.state = ModelAnimationState::UNTRIGGERED;
|
|
|
|
if (cleanup)
|
|
ModelAnimationSet::cleanRunning();
|
|
}
|
|
|
|
float ModelAnimation::getTime(int pmi_id) const{
|
|
auto instance = m_instances.find(pmi_id);
|
|
if (instance != m_instances.end())
|
|
return instance->second.time;
|
|
return 0.0f;
|
|
}
|
|
|
|
void ModelAnimation::stepAnimations(float frametime, polymodel_instance* pmi) {
|
|
auto animListIt = ModelAnimationSet::s_runningAnimations.find(pmi->id);
|
|
|
|
if (animListIt == ModelAnimationSet::s_runningAnimations.end())
|
|
return;
|
|
|
|
const auto& animList = animListIt->second;
|
|
|
|
ModelAnimationSubmodelBuffer applyBuffer;
|
|
animList.parentSet->initializeSubmodelBuffer(pmi, applyBuffer);
|
|
|
|
for (const auto& anim : animList.animationList) {
|
|
switch (anim->m_instances[pmi->id].state) {
|
|
case ModelAnimationState::RUNNING_FWD:
|
|
case ModelAnimationState::RUNNING_RWD:
|
|
case ModelAnimationState::NEED_RECALC:
|
|
anim->play(frametime, pmi, applyBuffer);
|
|
break;
|
|
case ModelAnimationState::COMPLETED:
|
|
case ModelAnimationState::PAUSED:
|
|
anim->play(frametime, pmi, applyBuffer, true);
|
|
//Currently not moving. Keep in buffer in case some other animation starts on that submodel, but don't play without manual starting
|
|
break;
|
|
case ModelAnimationState::UNTRIGGERED:
|
|
UNREACHABLE("An untriggered animation should not be in the runningAnimations buffer");
|
|
break;
|
|
}
|
|
|
|
}
|
|
|
|
ModelAnimationSet::apply(pmi, applyBuffer);
|
|
|
|
//Clear Animations that might have completed this frame.
|
|
ModelAnimationSet::cleanRunning();
|
|
}
|
|
|
|
ModelAnimationData<> ModelAnimationSubmodel::identity(ZERO_VECTOR, IDENTITY_MATRIX);
|
|
|
|
ModelAnimationSubmodel::ModelAnimationSubmodel(SCP_string submodelName) {
|
|
SCP_tolower(submodelName);
|
|
m_name = std::move(submodelName);
|
|
}
|
|
|
|
ModelAnimationSubmodel* ModelAnimationSubmodel::copy() const {
|
|
return new ModelAnimationSubmodel(*this);
|
|
}
|
|
|
|
const ModelAnimationData<>& ModelAnimationSubmodel::getInitialData(polymodel_instance* pmi) {
|
|
auto dataIt = m_initialData.find({ pmi->id });
|
|
if (dataIt == m_initialData.end()) {
|
|
if (!saveCurrentAsBase(pmi)) {
|
|
return identity;
|
|
}
|
|
}
|
|
|
|
return m_initialData.at(pmi->id);
|
|
}
|
|
|
|
void ModelAnimationSubmodel::reset(polymodel_instance* pmi) {
|
|
if(!m_submodel.has())
|
|
findSubmodel(pmi);
|
|
|
|
auto dataIt = m_initialData.find({ pmi->id });
|
|
if (dataIt == m_initialData.end())
|
|
return;
|
|
|
|
//Since resetting the submodel animation also has to happen for the submodel itself, copy its state at the animation's start back to the submodel
|
|
copyToSubmodel(dataIt->second, pmi);
|
|
}
|
|
|
|
void ModelAnimationSubmodel::copyToSubmodel(const ModelAnimationData<>& data, polymodel_instance* pmi) {
|
|
submodel_instance* submodel = findSubmodel(pmi).first;
|
|
if (!submodel)
|
|
return;
|
|
|
|
submodel->canonical_prev_orient = submodel->canonical_orient;
|
|
submodel->canonical_orient = data.orientation;
|
|
|
|
matrix delta;
|
|
vm_copy_transpose(&delta, &submodel->canonical_prev_orient);
|
|
vm_matrix_x_matrix(&delta, &submodel->canonical_orient, &delta);
|
|
|
|
float deltaAngle;
|
|
vm_matrix_to_rot_axis_and_angle(&delta, &deltaAngle, &submodel->rotation_axis);
|
|
submodel->current_turn_rate = deltaAngle / flFrametime;
|
|
|
|
//TODO: Once translation is a thing
|
|
//m_subsys->submodel_instance_1->offset = data.position;
|
|
}
|
|
|
|
void ModelAnimationSubmodel::resetPhysicsData(polymodel_instance* pmi) {
|
|
submodel_instance* submodel = findSubmodel(pmi).first;
|
|
if (!submodel)
|
|
return;
|
|
|
|
submodel->canonical_prev_orient = submodel->canonical_orient;
|
|
submodel->current_turn_rate = 0.0f;
|
|
}
|
|
|
|
bool ModelAnimationSubmodel::saveCurrentAsBase(polymodel_instance* pmi, bool isInitialType) {
|
|
auto submodel = findSubmodel(pmi);
|
|
|
|
if (!submodel.first || !submodel.second)
|
|
return false;
|
|
|
|
ModelAnimationData<>& data = m_initialData[{ pmi->id }];
|
|
|
|
if(!submodel.second->flags[Model::Submodel_flags::Can_move]){
|
|
mprintf(("Submodel %s of model %s is animated and has movement enabled.\n", submodel.second->name, model_get(pmi->model_num)->filename));
|
|
submodel.second->flags.set(Model::Submodel_flags::Can_move);
|
|
|
|
if (submodel.second->rotation_type == MOVEMENT_TYPE_NONE) {
|
|
submodel.second->rotation_type = MOVEMENT_TYPE_TRIGGERED;
|
|
}
|
|
}
|
|
|
|
data.orientation = submodel.first->canonical_orient;
|
|
//TODO: Once translation is a thing
|
|
//data.position = m_subsys->submodel_instance_1->offset;
|
|
|
|
//In this case, we just initial-type initialized the submodel. Properly set its last frame data as well
|
|
if(isInitialType)
|
|
submodel.first->canonical_prev_orient = submodel.first->canonical_orient;
|
|
|
|
return true;
|
|
}
|
|
|
|
std::pair<submodel_instance*, bsp_info*> ModelAnimationSubmodel::findSubmodel(polymodel_instance* pmi) {
|
|
//Ship was deleted
|
|
if(pmi->id < 0)
|
|
return { nullptr, nullptr };
|
|
|
|
int submodelNumber = -1;
|
|
|
|
polymodel* pm = model_get(pmi->model_num);
|
|
|
|
//Do we have a submodel number already cached?
|
|
if (m_submodel.has())
|
|
submodelNumber = m_submodel;
|
|
//We seem to have a submodel name
|
|
else {
|
|
for (int i = 0; i < pm->n_models; i++) {
|
|
if (!subsystem_stricmp(pm->submodel[i].name, m_name.c_str())) {
|
|
submodelNumber = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
m_submodel = submodelNumber;
|
|
}
|
|
|
|
//If the model does not exist, return null. The system is expected to just silently tolerate this,
|
|
//as this needs to be the case to be able to handle turrets whose barrels may or may not exist, depending on the pof which is not loaded at table parse time
|
|
if (submodelNumber < 0 || submodelNumber >= pm->n_models)
|
|
return { nullptr, nullptr };
|
|
|
|
return { &pmi->submodel[submodelNumber], &pm->submodel[submodelNumber] };
|
|
}
|
|
|
|
ModelAnimationSubmodelTurret::ModelAnimationSubmodelTurret(SCP_string subsystemName, bool findBarrel, SCP_string SIPname) : ModelAnimationSubmodel(std::move(subsystemName)), m_SIPname(std::move(SIPname)), m_findBarrel(findBarrel) {
|
|
is_turret = true;
|
|
}
|
|
|
|
ModelAnimationSubmodel* ModelAnimationSubmodelTurret::copy() const {
|
|
return new ModelAnimationSubmodelTurret(*this);
|
|
}
|
|
|
|
void ModelAnimationSubmodelTurret::renameSIP(const SCP_string& newSIPname) {
|
|
m_SIPname = newSIPname;
|
|
}
|
|
|
|
std::pair<submodel_instance*, bsp_info*> ModelAnimationSubmodelTurret::findSubmodel(polymodel_instance* pmi) {
|
|
//Ship was deleted
|
|
if (pmi->id < 0)
|
|
return { nullptr, nullptr };
|
|
|
|
int submodelNumber = -1;
|
|
|
|
polymodel* pm = model_get(pmi->model_num);
|
|
|
|
//Do we have a submodel number already cached?
|
|
if (m_submodel.has())
|
|
submodelNumber = m_submodel;
|
|
//Do we know if we were told to find the barrel submodel or not? This implies we have a subsystem name, not a submodel name
|
|
else {
|
|
ship_info* sip = nullptr;
|
|
if (pmi->objnum >= 0) {
|
|
sip = &Ship_info[Ships[Objects[pmi->objnum].instance].ship_info_index];
|
|
}
|
|
else {
|
|
//No objnum present. Check by SIP name we have saved (slow, so don't do if not absolutely necessary)
|
|
int sip_index = ship_info_lookup(m_SIPname.c_str());
|
|
|
|
if(sip_index < 0) //Give up
|
|
return { nullptr, nullptr };
|
|
|
|
sip = &Ship_info[sip_index];
|
|
}
|
|
|
|
for (int i = 0; i < sip->n_subsystems; i++) {
|
|
if (!subsystem_stricmp(sip->subsystems[i].subobj_name, m_name.c_str())) {
|
|
if ((bool)m_findBarrel) {
|
|
//Check if the barrel subobj is a dedicated existing subobj or just the base turret.
|
|
if (sip->subsystems[i].turret_gun_sobj != sip->subsystems[i].subobj_num)
|
|
submodelNumber = sip->subsystems[i].turret_gun_sobj;
|
|
else
|
|
submodelNumber = -1;
|
|
}
|
|
else
|
|
submodelNumber = sip->subsystems[i].subobj_num;
|
|
break;
|
|
}
|
|
}
|
|
|
|
m_submodel = submodelNumber;
|
|
}
|
|
|
|
//If the model does not exist, return null. The system is expected to just silently tolerate this,
|
|
//as this needs to be the case to be able to handle turrets whose barrels may or may not exist, depending on the pof which is not loaded at table parse time
|
|
if (submodelNumber < 0 || submodelNumber >= pm->n_models)
|
|
return { nullptr, nullptr };
|
|
|
|
return { &pmi->submodel[submodelNumber], &pm->submodel[submodelNumber] };
|
|
}
|
|
|
|
//To handle Multipart-turrets properly, they need a modified version that sets the actual angle, not just the orientation matrix.
|
|
void ModelAnimationSubmodelTurret::copyToSubmodel(const ModelAnimationData<>& data, polymodel_instance* pmi) {
|
|
const auto& submodels = findSubmodel(pmi);
|
|
submodel_instance* submodel = submodels.first;
|
|
bsp_info* sm = submodels.second;
|
|
|
|
if (!submodel)
|
|
return;
|
|
|
|
submodel->canonical_prev_orient = submodel->canonical_orient;
|
|
submodel->canonical_orient = data.orientation;
|
|
|
|
float angle = 0.0f;
|
|
vm_closest_angle_to_matrix(&submodel->canonical_orient, &sm->rotation_axis, &angle);
|
|
submodel->rotation_axis = sm->rotation_axis;
|
|
|
|
submodel->cur_angle = angle;
|
|
submodel->turret_idle_angle = angle;
|
|
}
|
|
|
|
|
|
float ModelAnimationSegment::getDuration(int pmi_id) const {
|
|
return m_duration.at(pmi_id);
|
|
}
|
|
|
|
|
|
int ModelAnimationSet::SUBTYPE_DEFAULT = ANIMATION_SUBTYPE_ALL;
|
|
|
|
ModelAnimationSet::ModelAnimationSet(SCP_string SIPname) : m_SIPname(std::move(SIPname)) { };
|
|
|
|
ModelAnimationSet::ModelAnimationSet(const ModelAnimationSet& other) {
|
|
operator=(other);
|
|
}
|
|
|
|
ModelAnimationSet& ModelAnimationSet::operator=(ModelAnimationSet&& other) noexcept {
|
|
std::swap(m_submodels, other.m_submodels);
|
|
std::swap(m_animationSet, other.m_animationSet);
|
|
std::swap(m_moveableSet, other.m_moveableSet);
|
|
std::swap(m_SIPname, other.m_SIPname);
|
|
|
|
for (const auto& animationTypes : m_animationSet) {
|
|
for (const auto& animations : animationTypes.second) {
|
|
for (const auto& animation : animations.second) {
|
|
animation->m_animation->exchangeSubmodelPointers(*this);
|
|
animation->m_set = this;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return *this;
|
|
}
|
|
|
|
ModelAnimationSet& ModelAnimationSet::operator=(const ModelAnimationSet& other) {
|
|
m_SIPname = other.m_SIPname;
|
|
|
|
for (const auto& submodel : other.m_submodels) {
|
|
auto newSubmodel = std::shared_ptr<ModelAnimationSubmodel>(submodel->copy());
|
|
newSubmodel->m_submodel = optional<int>();
|
|
m_submodels.push_back(newSubmodel);
|
|
}
|
|
|
|
for (const auto& animationTypes : other.m_animationSet) {
|
|
auto& newAnimations = m_animationSet[animationTypes.first];
|
|
for (const auto& oldAnimations : animationTypes.second) {
|
|
for (const auto& oldAnimation : oldAnimations.second) {
|
|
std::shared_ptr<ModelAnimation> newAnimation = std::shared_ptr<ModelAnimation>(new ModelAnimation(*oldAnimation));
|
|
newAnimation->m_animation = std::shared_ptr<ModelAnimationSegment>(newAnimation->m_animation->copy());
|
|
|
|
newAnimation->m_animation->exchangeSubmodelPointers(*this);
|
|
newAnimation->m_set = this;
|
|
|
|
newAnimations[oldAnimations.first].push_back(newAnimation);
|
|
}
|
|
}
|
|
}
|
|
|
|
m_moveableSet = other.m_moveableSet;
|
|
|
|
return *this;
|
|
}
|
|
|
|
void ModelAnimationSet::emplace(const std::shared_ptr<ModelAnimation>& animation, const SCP_string& name, ModelAnimationTriggerType type, int subtype, unsigned int uniqueId) {
|
|
auto newAnim = std::shared_ptr<ModelAnimation>(new ModelAnimation(*animation));
|
|
newAnim->m_set = this;
|
|
newAnim->m_animation = std::shared_ptr<ModelAnimationSegment>(animation->m_animation->copy());
|
|
newAnim->m_animation->exchangeSubmodelPointers(*this);
|
|
newAnim->id = uniqueId;
|
|
ModelAnimationSet::s_animationById[uniqueId] = newAnim;
|
|
m_animationSet[{type, subtype}][name].push_back(newAnim);
|
|
}
|
|
|
|
void ModelAnimationSet::changeShipName(const SCP_string& name) {
|
|
unsigned int hash_change = hash_fnv1a(m_SIPname) ^ hash_fnv1a(name);
|
|
|
|
m_SIPname = name;
|
|
for (const auto& submodel : m_submodels) {
|
|
submodel->renameSIP(name);
|
|
}
|
|
|
|
for (const auto& animationTypes : m_animationSet) {
|
|
for (const auto& animations : animationTypes.second) {
|
|
for (const auto& animation : animations.second) {
|
|
animation->id ^= hash_change;
|
|
ModelAnimationSet::s_animationById[animation->id] = animation;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ModelAnimationSet::cleanRunning() {
|
|
auto removeIt = s_runningAnimations.begin();
|
|
while (removeIt != s_runningAnimations.end()) {
|
|
auto animIt = removeIt->second.animationList.cbegin();
|
|
while (animIt != removeIt->second.animationList.cend()) {
|
|
if ((*animIt)->m_instances[removeIt->first].state == ModelAnimationState::UNTRIGGERED) {
|
|
animIt = removeIt->second.animationList.erase(animIt);
|
|
}
|
|
else
|
|
animIt++;
|
|
}
|
|
|
|
if (removeIt->second.animationList.empty()) {
|
|
removeIt = s_runningAnimations.erase(removeIt);
|
|
}
|
|
else
|
|
removeIt++;
|
|
}
|
|
}
|
|
|
|
void ModelAnimationSet::stopAnimations(polymodel_instance* pmi) {
|
|
if (pmi != nullptr) {
|
|
for (const auto& anim : s_runningAnimations[pmi->id].animationList) {
|
|
anim->stop(pmi, false);
|
|
}
|
|
s_runningAnimations.erase(pmi->id);
|
|
}
|
|
else {
|
|
for (const auto& animList : s_runningAnimations) {
|
|
for (const auto& anim : animList.second.animationList) {
|
|
anim->stop(model_get_instance(animList.first), false);
|
|
}
|
|
}
|
|
|
|
s_runningAnimations.clear();
|
|
}
|
|
}
|
|
|
|
void ModelAnimationSet::clearShipData(polymodel_instance* pmi) {
|
|
for (const auto& submodel : m_submodels) {
|
|
submodel->m_initialData.erase(pmi->id);
|
|
}
|
|
for (const auto& animations : m_animationSet) {
|
|
for (const auto& namedAnimations : animations.second) {
|
|
for (const auto& namedAnimation : namedAnimations.second) {
|
|
namedAnimation->m_instances.erase(pmi->id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ModelAnimationSet::apply(polymodel_instance* pmi, const ModelAnimationSubmodelBuffer& applyBuffer) {
|
|
for (const auto& toApply : applyBuffer) {
|
|
if (toApply.second.modified)
|
|
toApply.first->copyToSubmodel(toApply.second.data, pmi);
|
|
else
|
|
toApply.first->resetPhysicsData(pmi);
|
|
}
|
|
}
|
|
|
|
void ModelAnimationSet::initializeSubmodelBuffer(polymodel_instance* pmi, ModelAnimationSubmodelBuffer& applyBuffer) const {
|
|
for (const auto& submodel : m_submodels) {
|
|
ModelAnimationData<> base = submodel->getInitialData(pmi);
|
|
applyBuffer[submodel].data = base;
|
|
applyBuffer[submodel].modified = false;
|
|
}
|
|
}
|
|
|
|
bool ModelAnimationSet::start(polymodel_instance* pmi, ModelAnimationTriggerType type, const SCP_string& name, ModelAnimationDirection direction, bool forced, bool instant, bool pause, int subtype) const {
|
|
if (pmi == nullptr)
|
|
return false;
|
|
|
|
bool started = false;
|
|
auto animations = m_animationSet.find({ type, subtype });
|
|
if (animations != m_animationSet.end()) {
|
|
auto namedAnimations = animations->second.find(name);
|
|
if (namedAnimations != animations->second.end()) {
|
|
for (const auto& namedAnimation : namedAnimations->second) {
|
|
namedAnimation->start(pmi, direction, forced, instant, pause);
|
|
started = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
//Only search for default anims again if these weren't looked for in the first place
|
|
if (subtype == SUBTYPE_DEFAULT)
|
|
return started;
|
|
|
|
animations = m_animationSet.find({ type, SUBTYPE_DEFAULT });
|
|
if (animations != m_animationSet.end()) {
|
|
auto namedAnimations = animations->second.find(name);
|
|
if (namedAnimations != animations->second.end()) {
|
|
for (const auto& namedAnimation : namedAnimations->second) {
|
|
namedAnimation->start(pmi, direction, forced, instant, pause);
|
|
started = true;
|
|
}
|
|
}
|
|
}
|
|
return started;
|
|
}
|
|
|
|
bool ModelAnimationSet::startAll(polymodel_instance* pmi, ModelAnimationTriggerType type, ModelAnimationDirection direction, bool forced, bool instant, bool pause, int subtype, bool strict) const {
|
|
if (pmi == nullptr)
|
|
return false;
|
|
|
|
bool started = false;
|
|
auto animations = m_animationSet.find({ type, subtype });
|
|
if (animations != m_animationSet.end()) {
|
|
for (auto& namedAnimations : animations->second) {
|
|
for (const auto& namedAnimation : namedAnimations.second) {
|
|
namedAnimation->start(pmi, direction, forced, instant, pause);
|
|
started = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
//Only search for default anims again if these weren't looked for in the first place
|
|
if (strict || subtype == SUBTYPE_DEFAULT)
|
|
return started;
|
|
|
|
animations = m_animationSet.find({ type, SUBTYPE_DEFAULT });
|
|
if (animations != m_animationSet.end()) {
|
|
for (auto& namedAnimations : animations->second) {
|
|
for (const auto& namedAnimation : namedAnimations.second) {
|
|
namedAnimation->start(pmi, direction, forced, instant, pause);
|
|
started = true;
|
|
}
|
|
}
|
|
}
|
|
return started;
|
|
}
|
|
|
|
bool ModelAnimationSet::startBlanket(polymodel_instance* pmi, ModelAnimationTriggerType type, ModelAnimationDirection direction, bool forced, bool instant, bool pause) const {
|
|
if (pmi == nullptr)
|
|
return false;
|
|
|
|
bool started = false;
|
|
|
|
for(const auto& animations : m_animationSet) {
|
|
if(animations.first.type != type)
|
|
continue;
|
|
|
|
for (const auto& namedAnimation : animations.second){
|
|
for(const auto& animation : namedAnimation.second){
|
|
animation->start(pmi, direction, forced, instant, pause);
|
|
started = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return started;
|
|
}
|
|
|
|
//Yes why of course does this need special handling...
|
|
bool ModelAnimationSet::startDockBayDoors(polymodel_instance* pmi, ModelAnimationDirection direction, bool forced, bool instant, bool pause, int subtype) const {
|
|
if (pmi == nullptr)
|
|
return false;
|
|
|
|
bool started = false;
|
|
subtype++;
|
|
|
|
for (const auto& animList : m_animationSet) {
|
|
if (animList.first.type != ModelAnimationTriggerType::DockBayDoor)
|
|
continue;
|
|
|
|
if (animList.first.subtype != ModelAnimationSet::SUBTYPE_DEFAULT) {
|
|
//Trigger on all but x type
|
|
if (animList.first.subtype < 0 && animList.first.subtype == subtype)
|
|
continue;
|
|
|
|
//Trigger only on x type. For the record, animsubtype 0 cannot happen here.
|
|
if (animList.first.subtype > 0 && animList.first.subtype != subtype)
|
|
continue;
|
|
}
|
|
for (auto& namedAnimations : animList.second) {
|
|
for (const auto& namedAnimation : namedAnimations.second) {
|
|
namedAnimation->start(pmi, direction, forced, instant, pause);
|
|
started = true;
|
|
}
|
|
}
|
|
}
|
|
return started;
|
|
}
|
|
|
|
int ModelAnimationSet::getTimeDockBayDoors(polymodel_instance* pmi, int subtype) const {
|
|
float duration = 0.0f;
|
|
subtype++;
|
|
|
|
for (const auto& animList : m_animationSet) {
|
|
if (animList.first.type != ModelAnimationTriggerType::DockBayDoor)
|
|
continue;
|
|
|
|
if (animList.first.subtype != ModelAnimationSet::SUBTYPE_DEFAULT) {
|
|
//Trigger on all but x type
|
|
if (animList.first.subtype < 0 && animList.first.subtype == subtype)
|
|
continue;
|
|
|
|
//Trigger only on x type. For the record, animsubtype 0 cannot happen here.
|
|
if (animList.first.subtype > 0 && animList.first.subtype != subtype)
|
|
continue;
|
|
}
|
|
for (auto& namedAnimations : animList.second) {
|
|
for (const auto& namedAnimation : namedAnimations.second) {
|
|
if (namedAnimation->m_instances[pmi->id].state == ModelAnimationState::UNTRIGGERED)
|
|
continue;
|
|
|
|
float localDur = namedAnimation->m_instances[pmi->id].duration;
|
|
duration = duration < localDur ? localDur : duration;
|
|
}
|
|
}
|
|
}
|
|
|
|
return (int) (duration * 1000);
|
|
}
|
|
|
|
int ModelAnimationSet::getTime(polymodel_instance* pmi, ModelAnimationTriggerType type, const SCP_string& name, int subtype) const {
|
|
float duration = 0.0f;
|
|
|
|
auto animations = m_animationSet.find({ type, subtype });
|
|
if (animations != m_animationSet.end()) {
|
|
auto namedAnimations = animations->second.find(name);
|
|
if (namedAnimations != animations->second.end()){
|
|
for (const auto& namedAnimation : namedAnimations->second) {
|
|
if (namedAnimation->m_instances[pmi->id].state != ModelAnimationState::UNTRIGGERED) {
|
|
float localDur = namedAnimation->m_instances[pmi->id].duration;
|
|
duration = duration < localDur ? localDur : duration;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//Only search for default anims again if these weren't looked for in the first place
|
|
if (subtype == SUBTYPE_DEFAULT)
|
|
return (int) (duration * 1000);
|
|
|
|
animations = m_animationSet.find({ type, SUBTYPE_DEFAULT });
|
|
if (animations != m_animationSet.end()) {
|
|
auto namedAnimations = animations->second.find(name);
|
|
if (namedAnimations != animations->second.end()) {
|
|
for (const auto& namedAnimation : namedAnimations->second) {
|
|
if (namedAnimation->m_instances[pmi->id].state != ModelAnimationState::UNTRIGGERED) {
|
|
float localDur = namedAnimation->m_instances[pmi->id].duration;
|
|
duration = duration < localDur ? localDur : duration;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return (int) (duration * 1000);
|
|
}
|
|
|
|
int ModelAnimationSet::getTimeAll(polymodel_instance* pmi, ModelAnimationTriggerType type, int subtype, bool strict) const {
|
|
float duration = 0.0f;
|
|
auto animations = m_animationSet.find({ type, subtype });
|
|
if (animations != m_animationSet.end()) {
|
|
for (const auto& namedAnimations : animations->second) {
|
|
for (const auto& namedAnimation : namedAnimations.second) {
|
|
if (namedAnimation->m_instances[pmi->id].state == ModelAnimationState::UNTRIGGERED)
|
|
continue;
|
|
float localDur = namedAnimation->m_instances[pmi->id].duration;
|
|
duration = duration < localDur ? localDur : duration;
|
|
}
|
|
}
|
|
}
|
|
|
|
//Only search for default anims again if these weren't looked for in the first place
|
|
if (strict || subtype == SUBTYPE_DEFAULT)
|
|
return (int) (duration * 1000);
|
|
|
|
animations = m_animationSet.find({ type, SUBTYPE_DEFAULT });
|
|
if (animations != m_animationSet.end()) {
|
|
for (const auto& namedAnimations : animations->second) {
|
|
for (const auto& namedAnimation : namedAnimations.second) {
|
|
if (namedAnimation->m_instances[pmi->id].state == ModelAnimationState::UNTRIGGERED)
|
|
continue;
|
|
float localDur = namedAnimation->m_instances[pmi->id].duration;
|
|
duration = duration < localDur ? localDur : duration;
|
|
}
|
|
}
|
|
}
|
|
|
|
return (int) (duration * 1000);
|
|
}
|
|
|
|
std::vector<ModelAnimationSet::RegisteredTrigger> ModelAnimationSet::getRegisteredTriggers() const {
|
|
std::vector<RegisteredTrigger> ret;
|
|
|
|
for (const auto& animList : m_animationSet) {
|
|
for (const auto& animation : animList.second) {
|
|
ret.push_back({ animList.first.type, animList.first.subtype, animation.first });
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
};
|
|
|
|
bool ModelAnimationSet::updateMoveable(polymodel_instance* pmi, const SCP_string& name, const std::vector<linb::any>& args) const {
|
|
SCP_string lowername = name;
|
|
SCP_tolower(lowername);
|
|
auto moveable = m_moveableSet.find(lowername);
|
|
if (moveable == m_moveableSet.end())
|
|
return false;
|
|
|
|
moveable->second->update(pmi, args);
|
|
return true;
|
|
}
|
|
|
|
void ModelAnimationSet::initializeMoveables(polymodel_instance* pmi) {
|
|
for(const auto& moveable : m_moveableSet){
|
|
moveable.second->initialize(this, pmi);
|
|
}
|
|
}
|
|
|
|
std::vector<SCP_string> ModelAnimationSet::getRegisteredMoveables() const {
|
|
std::vector<SCP_string> ret;
|
|
|
|
for (const auto& moveable : m_moveableSet) {
|
|
ret.push_back(moveable.first);
|
|
}
|
|
|
|
return ret;
|
|
};
|
|
|
|
bool ModelAnimationSet::isEmpty() const {
|
|
for (const auto& animSet : m_animationSet) {
|
|
if (!animSet.second.empty())
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
|
|
std::shared_ptr<ModelAnimationSubmodel> ModelAnimationSet::getSubmodel(SCP_string submodelName) {
|
|
SCP_tolower(submodelName);
|
|
|
|
for (const auto& submodel : m_submodels) {
|
|
if (!submodel->is_turret && submodel->m_name == submodelName)
|
|
return submodel;
|
|
}
|
|
|
|
auto submodel = std::shared_ptr<ModelAnimationSubmodel>(new ModelAnimationSubmodel(submodelName));
|
|
m_submodels.push_back(submodel);
|
|
return submodel;
|
|
}
|
|
|
|
std::shared_ptr<ModelAnimationSubmodel> ModelAnimationSet::getSubmodel(SCP_string submodelName, const SCP_string& SIP_name, bool findBarrel) {
|
|
SCP_tolower(submodelName);
|
|
|
|
for (const auto& submodel : m_submodels) {
|
|
if (submodel->is_turret && submodel->m_name == submodelName) {
|
|
auto submodelTurret = ((ModelAnimationSubmodelTurret*)submodel.get());
|
|
if (submodelTurret->m_SIPname == SIP_name && submodelTurret->m_findBarrel == findBarrel)
|
|
return submodel;
|
|
}
|
|
}
|
|
|
|
auto submodel = std::shared_ptr<ModelAnimationSubmodelTurret>(new ModelAnimationSubmodelTurret(submodelName, findBarrel, SIP_name));
|
|
m_submodels.push_back(submodel);
|
|
return submodel;
|
|
}
|
|
|
|
std::shared_ptr<ModelAnimationSubmodel> ModelAnimationSet::getSubmodel(const std::shared_ptr<ModelAnimationSubmodel>& other) {
|
|
if (other->is_turret) {
|
|
auto const turret = (const ModelAnimationSubmodelTurret*)other.get();
|
|
return getSubmodel(turret->m_name, m_SIPname, turret->m_findBarrel);
|
|
}
|
|
else
|
|
return getSubmodel(other->m_name);
|
|
}
|
|
|
|
|
|
void anim_set_initial_states(ship* shipp) {
|
|
ship_info* sip = &Ship_info[shipp->ship_info_index];
|
|
|
|
polymodel_instance* pmi = model_get_instance(shipp->model_instance_num);
|
|
|
|
sip->animations.clearShipData(pmi);
|
|
sip->animations.startAll(pmi, animation::ModelAnimationTriggerType::Initial, ModelAnimationDirection::FWD, true, true);
|
|
sip->animations.initializeMoveables(pmi);
|
|
sip->animations.startAll(pmi, animation::ModelAnimationTriggerType::OnSpawn, ModelAnimationDirection::FWD);
|
|
}
|
|
|
|
const std::map<ModelAnimationTriggerType, std::pair<const char*, bool>> Animation_types = {
|
|
{ModelAnimationTriggerType::Initial, {"initial", false}}, //Atypical case. Will only ever be run in fully triggered state and then applied as base transformation
|
|
{ModelAnimationTriggerType::OnSpawn, {"on-spawn", false}}, //Atypical case. While no reverse trigger occurs, it is also guaranteed to not trigger more than once per model, hence non-resetting animations are fine here.
|
|
{ModelAnimationTriggerType::Docking_Stage1, {"docking-stage-1", false}},
|
|
{ModelAnimationTriggerType::Docking_Stage2, {"docking-stage-2", false}},
|
|
{ModelAnimationTriggerType::Docking_Stage3, {"docking-stage-3", false}},
|
|
{ModelAnimationTriggerType::Docked, {"docked", false}},
|
|
{ModelAnimationTriggerType::PrimaryBank, {"primary-bank", false}},
|
|
{ModelAnimationTriggerType::SecondaryBank, {"secondary-bank", false}},
|
|
{ModelAnimationTriggerType::DockBayDoor, {"fighterbay", false}},
|
|
{ModelAnimationTriggerType::Afterburner, {"afterburner", false}},
|
|
{ModelAnimationTriggerType::TurretFiring, {"turret-firing", false}},
|
|
{ModelAnimationTriggerType::Scripted, {"scripted", false}},
|
|
{ModelAnimationTriggerType::TurretFired, {"turret-fired", true}},
|
|
{ModelAnimationTriggerType::PrimaryFired, {"primary-fired", true}},
|
|
{ModelAnimationTriggerType::SecondaryFired, {"secondary-fired", true}}
|
|
};
|
|
|
|
ModelAnimationTriggerType anim_match_type(const char* p)
|
|
{
|
|
// standard match
|
|
for (const auto& entry : Animation_types) {
|
|
if (!strnicmp(p, entry.second.first, strlen(entry.second.first)))
|
|
return entry.first;
|
|
}
|
|
|
|
// Goober5000 - misspelling
|
|
if (!strnicmp(p, "inital", 6) || !strnicmp(p, "\"inital\"", 8)) {
|
|
Warning(LOCATION, "Spelling error in table file. Please change \"inital\" to \"initial\".");
|
|
return ModelAnimationTriggerType::Initial;
|
|
}
|
|
|
|
// Goober5000 - deprecation
|
|
if (!strnicmp(p, "docking", 7) || !strnicmp(p, "\"docking\"", 9)) {
|
|
auto docking_string = Animation_types.find(ModelAnimationTriggerType::Docking_Stage2)->second.first;
|
|
mprintf(("The \"docking\" animation type name is deprecated. Specify \"%s\" instead.\n", docking_string));
|
|
return ModelAnimationTriggerType::Docking_Stage2;
|
|
}
|
|
else if (!strnicmp(p, "primary_bank", 12) || !strnicmp(p, "\"primary_bank\"", 14)) {
|
|
auto pbank_string = Animation_types.find(ModelAnimationTriggerType::PrimaryBank)->second.first;
|
|
mprintf(("The \"primary_bank\" animation type name is deprecated. Specify \"%s\" instead.\n", pbank_string));
|
|
return ModelAnimationTriggerType::PrimaryBank;
|
|
}
|
|
else if (!strnicmp(p, "secondary_bank", 14) || !strnicmp(p, "\"secondary_bank\"", 16)) {
|
|
auto sbank_string = Animation_types.find(ModelAnimationTriggerType::SecondaryBank)->second.first;
|
|
mprintf(("The \"secondary_bank\" animation type name is deprecated. Specify \"%s\" instead.\n", sbank_string));
|
|
return ModelAnimationTriggerType::SecondaryBank;
|
|
}
|
|
else if (!strnicmp(p, "door", 4) || !strnicmp(p, "\"door\"", 6)) {
|
|
auto docking_string = Animation_types.find(ModelAnimationTriggerType::DockBayDoor)->second.first;
|
|
mprintf(("The \"door\" animation type name is deprecated. Specify \"%s\" instead.\n", docking_string));
|
|
return ModelAnimationTriggerType::DockBayDoor;
|
|
}
|
|
else if (!strnicmp(p, "turret firing", 13) || !strnicmp(p, "\"turret firing\"", 15)) {
|
|
auto turret_string = Animation_types.find(ModelAnimationTriggerType::TurretFiring)->second.first;
|
|
mprintf(("The \"turret firing\" animation type name is deprecated. Specify \"%s\" instead.\n", turret_string));
|
|
return ModelAnimationTriggerType::TurretFiring;
|
|
}
|
|
|
|
// Goober5000 - with quotes
|
|
for (const auto& entry : Animation_types) {
|
|
char quoted_name[NAME_LENGTH + 2];
|
|
strcpy(quoted_name, "\"");
|
|
strcat(quoted_name, entry.second.first);
|
|
strcat(quoted_name, "\"");
|
|
|
|
if (!strnicmp(p, quoted_name, strlen(quoted_name))) {
|
|
mprintf(("Old usage warning: Please remove quotes from animation type %s.\n", quoted_name));
|
|
return entry.first;
|
|
}
|
|
}
|
|
|
|
return ModelAnimationTriggerType::None;
|
|
}
|
|
|
|
SCP_string anim_name_from_subsys(model_subsystem* ss) {
|
|
char namelower[MAX_NAME_LEN];
|
|
strncpy(namelower, ss->subobj_name, MAX_NAME_LEN);
|
|
strlwr(namelower);
|
|
return namelower;
|
|
}
|
|
|
|
std::pair<std::function<bool(ModelAnimationDirection, bool, bool, bool)>, std::function<int()>> anim_parse_scripted_start(const ModelAnimationSet& set, polymodel_instance* pmi, ModelAnimationTriggerType type, const SCP_string& triggeredBy) {
|
|
int subtype = ModelAnimationSet::SUBTYPE_DEFAULT;
|
|
|
|
switch (type) {
|
|
case ModelAnimationTriggerType::Docking_Stage1:
|
|
case ModelAnimationTriggerType::Docking_Stage2:
|
|
case ModelAnimationTriggerType::Docking_Stage3:
|
|
case ModelAnimationTriggerType::Docked: {
|
|
//The index of the dock port or name of the dock port.
|
|
if (can_construe_as_integer(triggeredBy.c_str()))
|
|
subtype = atoi(triggeredBy.c_str());
|
|
else
|
|
subtype = model_find_dock_name_index(pmi->model_num, triggeredBy.c_str());
|
|
|
|
SCP_string dock_name = model_get_dock_name(pmi->model_num, subtype);
|
|
|
|
return {
|
|
[&set, pmi, type, dock_name, subtype](ModelAnimationDirection direction, bool forced, bool instant, bool pause) -> bool {
|
|
bool started = false;
|
|
started |= set.start(pmi, type, dock_name, direction, forced, instant, pause);
|
|
started |= set.start(pmi, type, "", direction, forced, instant, pause);
|
|
started |= set.startAll(pmi, type, direction, forced, instant, pause, subtype, true);
|
|
return started;
|
|
},
|
|
[&set, pmi, type, dock_name, subtype]() -> int {
|
|
int time1 = set.getTime(pmi, type, dock_name);
|
|
int time2 = set.getTime(pmi, type, "");
|
|
int time3 = set.getTimeAll(pmi, type, subtype, true);
|
|
return std::max({ time1, time2, time3 });
|
|
}
|
|
};
|
|
}
|
|
case ModelAnimationTriggerType::PrimaryBank:
|
|
case ModelAnimationTriggerType::SecondaryBank:
|
|
case ModelAnimationTriggerType::PrimaryFired:
|
|
case ModelAnimationTriggerType::SecondaryFired:
|
|
//The index of the bank
|
|
subtype = atoi(triggeredBy.c_str());
|
|
|
|
return {
|
|
[&set, pmi, type, subtype](ModelAnimationDirection direction, bool forced, bool instant, bool pause) -> bool {
|
|
return set.startAll(pmi, type, direction, forced, instant, pause, subtype);
|
|
},
|
|
[&set, pmi, type, subtype]() -> int {
|
|
return set.getTimeAll(pmi, type, subtype);
|
|
}
|
|
};
|
|
|
|
case ModelAnimationTriggerType::DockBayDoor:
|
|
//Index of the dock bay door
|
|
subtype = atoi(triggeredBy.c_str());
|
|
|
|
return {
|
|
[&set, pmi, subtype](ModelAnimationDirection direction, bool forced, bool instant, bool pause) -> bool {
|
|
return set.startDockBayDoors(pmi, direction, forced, instant, pause, subtype);
|
|
},
|
|
[&set, pmi, subtype]() -> int {
|
|
return set.getTimeDockBayDoors(pmi, subtype);
|
|
}
|
|
};
|
|
|
|
case ModelAnimationTriggerType::Scripted:
|
|
//More accurate name of scripted animation
|
|
case ModelAnimationTriggerType::TurretFired:
|
|
case ModelAnimationTriggerType::TurretFiring: {
|
|
//Name of the turret subsys that needs to be firing
|
|
std::string name(triggeredBy);
|
|
SCP_tolower(name);
|
|
|
|
return {
|
|
[&set, pmi, type, name](ModelAnimationDirection direction, bool forced, bool instant, bool pause) -> bool {
|
|
return set.start(pmi, type, name, direction, forced, instant, pause);
|
|
},
|
|
[&set, pmi, type, name]() -> int {
|
|
return set.getTime(pmi, type, name);
|
|
}
|
|
};
|
|
}
|
|
|
|
case ModelAnimationTriggerType::Afterburner:
|
|
case ModelAnimationTriggerType::OnSpawn:
|
|
//No triggered by specialization
|
|
return {
|
|
[&set, type, pmi](ModelAnimationDirection direction, bool forced, bool instant, bool pause) -> bool {
|
|
return set.startAll(pmi, type, direction, forced, instant, pause);
|
|
},
|
|
[&set, type, pmi]() -> int {
|
|
return set.getTimeAll(pmi, type);
|
|
}
|
|
};
|
|
|
|
case ModelAnimationTriggerType::Initial:
|
|
default:
|
|
// Can't trigger by script
|
|
Warning(LOCATION, "Initial-type animations cannot be triggered by script/SEXP!");
|
|
return {
|
|
[](ModelAnimationDirection /*direction*/, bool /*forced*/, bool /*instant*/, bool /*pause*/) -> bool { return false; },
|
|
[]() -> int { return 0; }
|
|
};
|
|
}
|
|
}
|
|
|
|
//Parsing functions
|
|
std::map<SCP_string, ModelAnimationParseHelper::ParsedModelAnimation> ModelAnimationParseHelper::s_animationsById;
|
|
std::map<SCP_string, std::shared_ptr<ModelAnimationMoveable>> ModelAnimationParseHelper::s_moveablesById;
|
|
|
|
std::shared_ptr<ModelAnimationSegment> ModelAnimationParseHelper::parseSegment() {
|
|
ignore_white_space();
|
|
char segment_type[NAME_LENGTH];
|
|
stuff_string(segment_type, F_NAME, NAME_LENGTH);
|
|
|
|
auto segment_parser = s_segmentParsers.find(segment_type);
|
|
if (segment_parser != s_segmentParsers.end())
|
|
return segment_parser->second(this);
|
|
else {
|
|
error_display(1, "Unknown segment type %s in animation with ID %s!", segment_type, m_animationName.c_str());
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
std::shared_ptr<ModelAnimationSubmodel> ModelAnimationParseHelper::parseSubmodel() {
|
|
int turretStatus = optional_string_one_of(3, "+Submodel:", "+Turret Base:", "+Turret Arm:");
|
|
|
|
if (turretStatus == -1)
|
|
return nullptr;
|
|
|
|
char name[NAME_LENGTH];
|
|
stuff_string(name, F_NAME, NAME_LENGTH);
|
|
|
|
switch (turretStatus) {
|
|
case 0: //Submodel
|
|
return std::shared_ptr<ModelAnimationSubmodel>(new ModelAnimationSubmodel(name));
|
|
case 1: //Turret Base
|
|
return std::shared_ptr<ModelAnimationSubmodel>(new ModelAnimationSubmodelTurret(name, false, ""));
|
|
case 2: //Turret Arm
|
|
return std::shared_ptr<ModelAnimationSubmodel>(new ModelAnimationSubmodelTurret(name, true, ""));
|
|
default: //Not specified
|
|
return nullptr;
|
|
}
|
|
|
|
}
|
|
|
|
void ModelAnimationParseHelper::parseSingleAnimation() {
|
|
|
|
ModelAnimationParseHelper helper;
|
|
required_string("$Name:");
|
|
char animID[NAME_LENGTH];
|
|
stuff_string(animID, F_NAME, NAME_LENGTH);
|
|
helper.m_animationName = animID;
|
|
|
|
if (s_animationsById.count(helper.m_animationName))
|
|
error_display(1, "Animation with name %s already exists!", animID);
|
|
|
|
required_string("$Type:");
|
|
char atype[NAME_LENGTH];
|
|
stuff_string(atype, F_NAME, NAME_LENGTH);
|
|
ModelAnimationTriggerType type = anim_match_type(atype);
|
|
|
|
if (type == ModelAnimationTriggerType::None) {
|
|
error_display(1, "Animation with unknown trigger type %s!", atype);
|
|
return;
|
|
}
|
|
|
|
auto animation = std::shared_ptr<ModelAnimation>(new ModelAnimation(type == ModelAnimationTriggerType::Initial));
|
|
|
|
int subtype = ModelAnimationSet::SUBTYPE_DEFAULT;
|
|
SCP_string name;
|
|
|
|
if (optional_string("+Triggered By:")) {
|
|
switch (type) {
|
|
case ModelAnimationTriggerType::Docking_Stage1:
|
|
case ModelAnimationTriggerType::Docking_Stage2:
|
|
case ModelAnimationTriggerType::Docking_Stage3:
|
|
case ModelAnimationTriggerType::Docked: {
|
|
//The index of the dock port or name of the dock port.
|
|
char parsedname[NAME_LENGTH];
|
|
stuff_string(parsedname, F_NAME, NAME_LENGTH);
|
|
|
|
if (can_construe_as_integer(parsedname))
|
|
subtype = atoi(parsedname);
|
|
else {
|
|
strlwr(parsedname);
|
|
name = parsedname;
|
|
}
|
|
|
|
break;
|
|
}
|
|
case ModelAnimationTriggerType::PrimaryBank:
|
|
case ModelAnimationTriggerType::SecondaryBank:
|
|
case ModelAnimationTriggerType::PrimaryFired:
|
|
case ModelAnimationTriggerType::SecondaryFired:
|
|
//The index of the bank
|
|
stuff_int(&subtype);
|
|
|
|
break;
|
|
case ModelAnimationTriggerType::DockBayDoor: {
|
|
//Index of the dock bay door, if NOT before, then all but this door
|
|
bool flip = optional_string("NOT");
|
|
stuff_int(&subtype);
|
|
subtype++;
|
|
if (flip)
|
|
subtype *= -1;
|
|
|
|
break;
|
|
}
|
|
case ModelAnimationTriggerType::Scripted:
|
|
//More accurate name of scripted animation
|
|
case ModelAnimationTriggerType::TurretFired:
|
|
case ModelAnimationTriggerType::TurretFiring: {
|
|
//Name of the turret subsys that needs to be firing
|
|
char parsedname[NAME_LENGTH];
|
|
stuff_string(parsedname, F_NAME, NAME_LENGTH);
|
|
strlwr(parsedname);
|
|
name = parsedname;
|
|
break;
|
|
}
|
|
|
|
case ModelAnimationTriggerType::Initial:
|
|
case ModelAnimationTriggerType::Afterburner:
|
|
case ModelAnimationTriggerType::OnSpawn:
|
|
default:
|
|
error_display(0, "Animation trigger type %s does not use any trigger specification!", atype);
|
|
//These shouldn't have further specifications. Ignore
|
|
}
|
|
}
|
|
|
|
if (optional_string("$Flags:")) {
|
|
SCP_vector<SCP_string> unparsed;
|
|
parse_string_flag_list(animation->m_flags, Animation_flags, Num_animation_flags, &unparsed);
|
|
}
|
|
|
|
if (Animation_types.find(type)->second.second) {
|
|
//This is a type where the code does not reset the animation. The animation is expected to auto-reset in one way or another.
|
|
//If it doesn't, nothing will crash, but the result is most likely unintended.
|
|
if (!animation->m_flags[Animation_Flags::Auto_Reverse, Animation_Flags::Reset_at_completion]) {
|
|
error_display(0, "Animation trigger type %s expects an auto-reset flag to be set.", atype);
|
|
}
|
|
|
|
if (animation->m_flags[Animation_Flags::Loop]) {
|
|
//Looping animations for these trigger types are probably unintended as well, but rare cases could exist, hence no explicit warning.
|
|
mprintf(("Animation %s with trigger type %s has an unexpected loop flag.\n", helper.m_animationName.c_str(), atype));
|
|
}
|
|
}
|
|
|
|
animation->setAnimation(helper.parseSegment());
|
|
|
|
s_animationsById.emplace(helper.m_animationName, ParsedModelAnimation { animation, type, name, subtype });
|
|
}
|
|
|
|
void ModelAnimationParseHelper::parseSingleMoveable() {
|
|
volatile ModelAnimationMoveableOrientation o = ModelAnimationMoveableOrientation(nullptr, angles{ 0,0,0 });
|
|
volatile ModelAnimationMoveableRotation r = ModelAnimationMoveableRotation(nullptr, angles{ 0,0,0 }, angles{0,0,0}, optional<angles>());
|
|
|
|
required_string("$Name:");
|
|
char animID[NAME_LENGTH];
|
|
stuff_string(animID, F_NAME, NAME_LENGTH);
|
|
|
|
SCP_string name = animID;
|
|
SCP_tolower(name);
|
|
|
|
if (s_moveablesById.count(name))
|
|
error_display(1, "Moveable with name %s already exists!", animID);
|
|
|
|
required_string("$Type:");
|
|
|
|
char segment_type[NAME_LENGTH];
|
|
stuff_string(segment_type, F_NAME, NAME_LENGTH);
|
|
|
|
auto segment_parser = s_moveableParsers.find(segment_type);
|
|
if (segment_parser != s_moveableParsers.end()){
|
|
s_moveablesById.emplace(name, segment_parser->second());
|
|
}
|
|
else {
|
|
error_display(1, "Could not create moveable %s: Unknown moveable type %s!", animID, segment_type);
|
|
return;
|
|
}
|
|
|
|
}
|
|
|
|
void ModelAnimationParseHelper::parseTableFile(const char* filename) {
|
|
try {
|
|
read_file_text(filename, CF_TYPE_TABLES);
|
|
reset_parse();
|
|
|
|
required_string("#Animations");
|
|
ignore_white_space();
|
|
|
|
while (!optional_string("#End")) {
|
|
parseSingleAnimation();
|
|
ignore_white_space();
|
|
}
|
|
|
|
if(optional_string("#Moveables")){
|
|
while (!optional_string("#End")) {
|
|
parseSingleMoveable();
|
|
ignore_white_space();
|
|
}
|
|
}
|
|
}
|
|
catch (const parse::ParseException& e) {
|
|
mprintf(("TABLES: Unable to parse '%s'! Error message = %s.\n", filename, e.what()));
|
|
return;
|
|
}
|
|
|
|
}
|
|
|
|
void ModelAnimationParseHelper::parseTables() {
|
|
s_animationsById.clear();
|
|
s_moveablesById.clear();
|
|
parse_modular_table("*-anim.tbm", parseTableFile, CF_TYPE_TABLES);
|
|
}
|
|
|
|
unsigned int ModelAnimationParseHelper::getUniqueAnimationID(const SCP_string& animName, char uniquePrefix, const SCP_string& parentName) {
|
|
return hash_fnv1a(animName + uniquePrefix) ^ hash_fnv1a(parentName);
|
|
}
|
|
|
|
void ModelAnimationParseHelper::parseAnimsetInfo(ModelAnimationSet& set, char uniqueTypePrefix, const SCP_string& uniqueParentName) {
|
|
SCP_vector<SCP_string> requestedAnimations;
|
|
stuff_string_list(requestedAnimations);
|
|
|
|
set.m_animationSet.clear();
|
|
|
|
for (const SCP_string& request : requestedAnimations) {
|
|
auto animIt = s_animationsById.find(request);
|
|
if (animIt != s_animationsById.end()) {
|
|
const ParsedModelAnimation& foundAnim = animIt->second;
|
|
set.emplace(foundAnim.anim, foundAnim.name, foundAnim.type, foundAnim.subtype, ModelAnimationParseHelper::getUniqueAnimationID(animIt->first, uniqueTypePrefix, uniqueParentName));
|
|
}
|
|
else {
|
|
error_display(0, "Animation with name %s not found!", request.c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
void ModelAnimationParseHelper::parseAnimsetInfo(ModelAnimationSet& set, ship_info* sip) {
|
|
Assert(sip != nullptr);
|
|
|
|
set.changeShipName(sip->name);
|
|
|
|
ModelAnimationParseHelper::parseAnimsetInfo(set, 's', sip->name);
|
|
}
|
|
|
|
void ModelAnimationParseHelper::parseMoveablesetInfo(ModelAnimationSet& set) {
|
|
SCP_vector<SCP_string> requestedAnimations;
|
|
stuff_string_list(requestedAnimations);
|
|
|
|
set.m_moveableSet.clear();
|
|
|
|
for (SCP_string request : requestedAnimations) {
|
|
SCP_tolower(request);
|
|
auto animIt = s_moveablesById.find(request);
|
|
if (animIt != s_moveablesById.end()) {
|
|
set.m_moveableSet.emplace(animIt->first, animIt->second);
|
|
}
|
|
else {
|
|
error_display(0, "Moveable with name %s not found!", request.c_str());
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void ModelAnimationParseHelper::parseLegacyAnimationTable(model_subsystem* sp, ship_info* sip) {
|
|
//the only thing initial animation type needs is the angle,
|
|
//so to save space lets just make everything optional in this case
|
|
|
|
//Most of these properties were read and allowed, but never actually used for anything.
|
|
//Hence, still allow them in tables, but now just skip reading them
|
|
|
|
required_string("$type:");
|
|
char atype[NAME_LENGTH];
|
|
stuff_string(atype, F_NAME, NAME_LENGTH);
|
|
animation::ModelAnimationTriggerType type = anim_match_type(atype);
|
|
int subtype = ModelAnimationSet::SUBTYPE_DEFAULT;
|
|
char sub_name[NAME_LENGTH];
|
|
SCP_string name = anim_name_from_subsys(sp);
|
|
|
|
if (optional_string("+sub_type:")) {
|
|
stuff_int(&subtype);
|
|
|
|
if (type == ModelAnimationTriggerType::DockBayDoor) {
|
|
if (subtype == 0) {
|
|
//Apparently the legacy code treated a 0 here like subtype default (this is incorrect in a lot of places).
|
|
subtype = ModelAnimationSet::SUBTYPE_DEFAULT;
|
|
}
|
|
else {
|
|
//Increase by 1, so that -x != x is true for all possible values
|
|
subtype += subtype < 0 ? -1 : 1;
|
|
}
|
|
|
|
//Also set name to empty for compatibility with naming and subtyping in the new table while being compatible with legacy subtypes
|
|
name = "";
|
|
}
|
|
}
|
|
|
|
if (optional_string("+sub_name:")) {
|
|
stuff_string(sub_name, F_NAME, NAME_LENGTH);
|
|
}
|
|
else {
|
|
strcpy_s(sub_name, "<none>");
|
|
}
|
|
|
|
if (type == animation::ModelAnimationTriggerType::Initial) {
|
|
angles angle;
|
|
bool isRelative;
|
|
|
|
if (optional_string("+delay:"))
|
|
skip_token();
|
|
|
|
if (optional_string("+reverse_delay:"))
|
|
skip_token();
|
|
|
|
if (optional_string("+absolute_angle:")) {
|
|
stuff_angles_deg_phb(&angle);
|
|
|
|
isRelative = false;
|
|
}
|
|
else {
|
|
required_string("+relative_angle:");
|
|
|
|
stuff_angles_deg_phb(&angle);
|
|
|
|
isRelative = true;
|
|
}
|
|
|
|
if (optional_string("+velocity:"))
|
|
skip_token();
|
|
|
|
if (optional_string("+acceleration:"))
|
|
skip_token();
|
|
|
|
if (optional_string("+time:"))
|
|
skip_token();
|
|
|
|
auto anim = std::shared_ptr<ModelAnimation>(new ModelAnimation(true));
|
|
|
|
char namelower[MAX_NAME_LEN];
|
|
strncpy(namelower, sp->subobj_name, MAX_NAME_LEN);
|
|
strlwr(namelower);
|
|
//since sp->type is not set without reading the pof, we need to infer it by subsystem name (which works, since the same name is used to match the submodels name, which is used to match the type in pof parsing)
|
|
//sadly, we also need to check for engine and radar, since these take precedent (as in, an engineturret is an engine before a turret type)
|
|
if (!strstr(namelower, "engine") && !strstr(namelower, "radar") && strstr(namelower, "turret")) {
|
|
auto subsysBase = sip->animations.getSubmodel(sp->subobj_name, sip->name, false);
|
|
auto rotBase = std::shared_ptr<ModelAnimationSegmentSetAngle>(new ModelAnimationSegmentSetAngle(subsysBase, angle.h));
|
|
|
|
auto subsysBarrel = sip->animations.getSubmodel(sp->subobj_name, sip->name, true);
|
|
auto rotBarrel = std::shared_ptr<ModelAnimationSegmentSetAngle>(new ModelAnimationSegmentSetAngle(subsysBarrel, angle.p));
|
|
|
|
auto rot = std::shared_ptr<ModelAnimationSegmentParallel>(new ModelAnimationSegmentParallel());
|
|
rot->addSegment(std::move(rotBase));
|
|
rot->addSegment(std::move(rotBarrel));
|
|
|
|
anim->setAnimation(std::move(rot));
|
|
}
|
|
else {
|
|
auto subsys = sip->animations.getSubmodel(sp->subobj_name);
|
|
auto rot = std::shared_ptr<ModelAnimationSegmentSetOrientation>(new ModelAnimationSegmentSetOrientation(subsys, angle, isRelative));
|
|
anim->setAnimation(std::move(rot));
|
|
}
|
|
|
|
//Initial Animations in legacy style will continue to be fully supported and allowed, given the frequency of these (especially for turrets) and the fact that these are more intuitive to be directly in the subsystem section of the ship table, as these are closer to representing a property of the subsystem rather than an animation.
|
|
//Hence, there will not be any warning displayed if the legacy table is used for these. -Lafiel
|
|
sip->animations.emplace(anim, name, ModelAnimationTriggerType::Initial, ModelAnimationSet::SUBTYPE_DEFAULT, ModelAnimationParseHelper::getUniqueAnimationID(name + Animation_types.at(ModelAnimationTriggerType::Initial).first + std::to_string(subtype), 'b', sip->name));
|
|
}
|
|
else {
|
|
auto anim = std::shared_ptr<ModelAnimation>(new ModelAnimation());
|
|
auto subsys = sip->animations.getSubmodel(sp->subobj_name);
|
|
|
|
if (type == ModelAnimationTriggerType::TurretFired) {
|
|
//Turret fireds won't get reset by code, so make them auto-resetting
|
|
anim->m_flags += Animation_Flags::Reset_at_completion;
|
|
}
|
|
|
|
auto mainSegment = std::shared_ptr<ModelAnimationSegmentSerial>(new ModelAnimationSegmentSerial());
|
|
|
|
if (optional_string("+delay:")) {
|
|
int delayByMs;
|
|
stuff_int(&delayByMs);
|
|
auto delay = std::shared_ptr<ModelAnimationSegmentWait>(new ModelAnimationSegmentWait(((float)delayByMs) * 0.001f));
|
|
mainSegment->addSegment(delay);
|
|
}
|
|
|
|
int delayByMsReverse = -1;
|
|
if (optional_string("+reverse_delay:")) {
|
|
stuff_int(&delayByMsReverse);
|
|
}
|
|
|
|
angles target{ 0,0,0 };
|
|
bool absolute = false;
|
|
|
|
if (optional_string("+absolute_angle:")) {
|
|
absolute = true;
|
|
|
|
stuff_angles_deg_phb(&target);
|
|
}
|
|
else {
|
|
absolute = false;
|
|
|
|
required_string("+relative_angle:");
|
|
stuff_angles_deg_phb(&target);
|
|
}
|
|
|
|
angles velocity{ 0,0,0 };
|
|
required_string("+velocity:");
|
|
stuff_angles_deg_phb(&velocity);
|
|
|
|
optional<angles> acceleration;
|
|
|
|
if (optional_string("+acceleration:")) {
|
|
angles accel{ 0,0,0 };
|
|
stuff_angles_deg_phb(&accel);
|
|
|
|
bool allZero = accel.p == 0 && accel.b == 0 && accel.h == 0;
|
|
|
|
if (!allZero) {
|
|
acceleration = accel;
|
|
}
|
|
}
|
|
|
|
if (optional_string("+time:")) {
|
|
skip_token();
|
|
|
|
//Time is ignored if acceleration is set in legacy animations. Even if it isn't set, time seems to only affect metadata and not the actual animation.
|
|
//Hence, throw time away, and let the segment handle calculating how long it actually takes
|
|
}
|
|
|
|
auto rotation = std::shared_ptr<ModelAnimationSegmentRotation>(new ModelAnimationSegmentRotation(subsys, target, velocity, optional<float>(), acceleration, absolute));
|
|
|
|
if (optional_string("$Sound:")) {
|
|
gamesnd_id start_sound;
|
|
gamesnd_id loop_sound;
|
|
gamesnd_id end_sound;
|
|
float snd_rad;
|
|
|
|
parse_game_sound("+Start:", &start_sound);
|
|
|
|
parse_game_sound("+Loop:", &loop_sound);
|
|
|
|
parse_game_sound("+End:", &end_sound);
|
|
|
|
required_string("+Radius:");
|
|
stuff_float(&snd_rad);
|
|
|
|
auto sound = std::shared_ptr<ModelAnimationSegmentSoundDuring>(new ModelAnimationSegmentSoundDuring(rotation, start_sound, end_sound, loop_sound, true));
|
|
mainSegment->addSegment(sound);
|
|
}
|
|
else
|
|
mainSegment->addSegment(rotation);
|
|
|
|
if (delayByMsReverse != -1) {
|
|
auto delay = std::shared_ptr<ModelAnimationSegmentWait>(new ModelAnimationSegmentWait(((float)delayByMsReverse) * 0.001f));
|
|
mainSegment->addSegment(delay);
|
|
}
|
|
|
|
anim->setAnimation(mainSegment);
|
|
|
|
//TODO maybe handle sub_name? Not documented in Wiki, maybe no one actually uses it...
|
|
sip->animations.emplace(anim, name, type, subtype, ModelAnimationParseHelper::getUniqueAnimationID(name + Animation_types.at(type).first + std::to_string(subtype), 'b', sip->name));
|
|
|
|
mprintf(("Specified deprecated non-initial type animation on subsystem %s of ship class %s. Consider using *-anim.tbm's instead.\n", sp->subobj_name, sip->name));
|
|
}
|
|
}
|
|
|
|
std::map<SCP_string, ModelAnimationParseHelper::ModelAnimationSegmentParser> ModelAnimationParseHelper::s_segmentParsers = {
|
|
{"$Segment Sequential:", ModelAnimationSegmentSerial::parser},
|
|
{"$Segment Parallel:", ModelAnimationSegmentParallel::parser},
|
|
{"$Wait:", ModelAnimationSegmentWait::parser},
|
|
{"$Set Orientation:", ModelAnimationSegmentSetOrientation::parser},
|
|
{"$Set Angle:", ModelAnimationSegmentSetAngle::parser},
|
|
{"$Rotation:", ModelAnimationSegmentRotation::parser},
|
|
{"$Axis Rotation:", ModelAnimationSegmentAxisRotation::parser},
|
|
// {"$Translation:", ModelAnimationSegmentTranslation::parser},
|
|
{"$Sound During:", ModelAnimationSegmentSoundDuring::parser},
|
|
{"$Inverse Kinematics:", ModelAnimationSegmentIK::parser}
|
|
};
|
|
|
|
std::map<SCP_string, ModelAnimationParseHelper::ModelAnimationMoveableParser> ModelAnimationParseHelper::s_moveableParsers = {
|
|
{"Orientation", ModelAnimationMoveableOrientation::parser},
|
|
{"Rotation", ModelAnimationMoveableRotation::parser},
|
|
{"Axis Rotation", ModelAnimationMoveableAxisRotation::parser},
|
|
{"Inverse Kinematics", ModelAnimationMoveableIK::parser}
|
|
};
|
|
|
|
|
|
} |