From b820c2d6be263fa5e086a49d7da83fbbf673e9ea Mon Sep 17 00:00:00 2001
From: Zack Middleton
Date: Sat, 30 Dec 2023 02:44:57 -0600
Subject: [PATCH] iqefilter: Add Quake 3 player export for IQE
---
doc/html/olh_iqeprompt.htm | 23 ++
doc/html/olh_quakemd3prompt.htm | 4 +-
src/implui/iqeprompt.cc | 6 +
src/libmm3d/iqefilter.cc | 521 +++++++++++++++++++++++++++++++-
src/libmm3d/iqefilter.h | 39 +++
src/libmm3d/md3filter.cc | 2 +-
src/qtui/iqeprompt.ui | 22 ++
src/qtui/md3prompt.ui | 4 +-
8 files changed, 615 insertions(+), 6 deletions(-)
diff --git a/doc/html/olh_iqeprompt.htm b/doc/html/olh_iqeprompt.htm
index 41241fcc..3a361beb 100644
--- a/doc/html/olh_iqeprompt.htm
+++ b/doc/html/olh_iqeprompt.htm
@@ -7,6 +7,29 @@ IQE Export Options
which animations to save.
+
+ The Save as a Quake 3 Player checkbox allows you indicate
+ if you want to export as a Quake 3 composite player model
+ (head.iqe, upper.iqe, and lower.iqe models).
+ If it is grayed out, see the list of requirements for export on the MD3 page.
+
+
+
+ For exporting as a Quake 3 player, there must be a root joint with a desendent torso joint and desendent (of torso joint) head joint. There must be a "tag_torso" point at the exact location of the torso joint and attached to the torso joint. Same for "tag_head" at exact location of head joint and attached to it. The point names must be "tag_torso" and "tag_head", the joint names can be anything. Do not make the root joint be the torso joint; everything will be a desendent of it and there cannot be leg joints.
+
+
+
+ Joints/points that are not a desendents of torso joint are exported in lower.iqe. Torso joint and desendent joints/points to (and including) head joint and "tag_head" point are exported to upper.iqe. Head joint and desendent joints/points are exported to head.iqe.
+
+
+
+ The Save Quake 3 animation.cfg checkbox allows you to indicate
+ if you want to write "animation.cfg" file along side the model.
+ This file contains a list of animations with looping and frames
+ per-second information and can contain other arbitry data from
+ MD3_CFG_* meta data. See the MD3 page for details.
+
+
The Save Meshes checkbox allows you indicate if you want
the meshes saved in the file or not.
diff --git a/doc/html/olh_quakemd3prompt.htm b/doc/html/olh_quakemd3prompt.htm
index aedd3728..769c0cd5 100644
--- a/doc/html/olh_quakemd3prompt.htm
+++ b/doc/html/olh_quakemd3prompt.htm
@@ -7,14 +7,14 @@
MD3 Export Options
- The Save as a Player Model checkbox allows you indicate
+ The Save as a Quake 3 Player checkbox allows you indicate
if you want to export as a Quake 3 composite player model
(head.md3, upper.md3, and lower.md3 models).
If it is grayed out, see the list of requirements for export on the MD3 page.
- The Save animation.cfg checkbox allows you to indicate
+ The Save Quake 3 animation.cfg checkbox allows you to indicate
if you want to write "animation.cfg" file along side the model.
This file contains a list of animations with looping and frames
per-second information and can contain other arbitry data from
diff --git a/src/implui/iqeprompt.cc b/src/implui/iqeprompt.cc
index d92a3885..8f3b6dda 100644
--- a/src/implui/iqeprompt.cc
+++ b/src/implui/iqeprompt.cc
@@ -47,6 +47,9 @@ IqePrompt::~IqePrompt()
void IqePrompt::setOptions( IqeFilter::IqeOptions * opts, Model * model )
{
+ m_saveAsPlayer->setEnabled( opts->m_playerSupported );
+ m_saveAsPlayer->setChecked( opts->m_saveAsPlayer );
+ m_saveAnimationCfg->setChecked( opts->m_saveAnimationCfg );
m_saveMeshes->setChecked( opts->m_saveMeshes );
m_savePointsJoint->setChecked( opts->m_savePointsJoint );
m_saveSkeleton->setChecked( opts->m_saveSkeleton );
@@ -63,6 +66,8 @@ void IqePrompt::setOptions( IqeFilter::IqeOptions * opts, Model * model )
void IqePrompt::getOptions( IqeFilter::IqeOptions * opts )
{
+ opts->m_saveAsPlayer = m_saveAsPlayer->isChecked();
+ opts->m_saveAnimationCfg = m_saveAnimationCfg->isChecked();
opts->m_saveMeshes = m_saveMeshes->isChecked();
opts->m_savePointsJoint= m_savePointsJoint->isChecked();
opts->m_saveSkeleton = m_saveSkeleton->isChecked();
@@ -93,6 +98,7 @@ bool iqeprompt_show( Model * model, const char * const filename, ModelFilter::Op
IqeFilter::IqeOptions * opts = dynamic_cast< IqeFilter::IqeOptions * >( o );
if ( opts )
{
+ opts->setOptionsFromModel( model, filename );
p.setOptions( opts, model );
if ( p.exec() )
diff --git a/src/libmm3d/iqefilter.cc b/src/libmm3d/iqefilter.cc
index 9d3edc05..08e1de8d 100644
--- a/src/libmm3d/iqefilter.cc
+++ b/src/libmm3d/iqefilter.cc
@@ -47,10 +47,22 @@
using std::list;
using std::string;
+static const char *s_animSyncWarning[] =
+{
+ "torso_attack",
+ "torso_attack2",
+ "torso_drop",
+ "torso_raise",
+ NULL
+};
+
static const char *IQE_HEADER = "# Inter-Quake Export";
IqeFilter::IqeOptions::IqeOptions()
- : m_saveMeshes( true ),
+ : m_playerSupported( false ),
+ m_saveAsPlayer( false ),
+ m_saveAnimationCfg( false ),
+ m_saveMeshes( true ),
m_savePointsJoint( true ),
m_saveSkeleton( true ),
m_saveAnimations( true ),
@@ -62,6 +74,77 @@ IqeFilter::IqeOptions::~IqeOptions()
{
}
+void IqeFilter::IqeOptions::setOptionsFromModel( Model * model, const char * const filename )
+{
+ m_playerSupported = false;
+
+ string modelPath = "";
+ string modelBaseName = "";
+ string modelFullName = "";
+
+ normalizePath( filename, modelFullName, modelPath, modelBaseName );
+
+ if ( strncasecmp( modelBaseName.c_str(), "lower.", 6 ) == 0
+ || strncasecmp( modelBaseName.c_str(), "upper.", 6 ) == 0
+ || strncasecmp( modelBaseName.c_str(), "head.", 5 ) == 0 )
+ {
+ bool haveUpper = false;
+ bool haveLower = false;
+ bool haveUnknown = false;
+
+ unsigned gcount = model->getGroupCount();
+ for ( unsigned g = 0; g < gcount; g++ )
+ {
+ std::string name = model->getGroupName( g );
+ if ( name[0] != '\0' && name[1] == '_' )
+ {
+ switch ( toupper( name[0] ) )
+ {
+ case 'U':
+ haveUpper = true;
+ break;
+ case 'L':
+ haveLower = true;
+ break;
+ case 'H':
+ break;
+ default:
+ haveUnknown = true;
+ break;
+ }
+ } else {
+ haveUnknown = true;
+ }
+ }
+
+ if ( !haveUnknown && haveUpper && haveLower
+ && model->getPointByName( "tag_torso" ) >= 0
+ && model->getPointByName( "tag_head" ) >= 0 )
+ {
+ // have filename, groups, and tags required for player model
+ m_playerSupported = true;
+ }
+ }
+
+ m_saveAsPlayer = m_playerSupported;
+ m_saveAnimationCfg = m_playerSupported;
+
+ char value[20];
+ if ( model->getMetaData( "MD3_composite", value, sizeof( value ) ) )
+ {
+ if ( atoi( value ) == 0 )
+ {
+ m_saveAsPlayer = false;
+ m_saveAnimationCfg = false;
+ }
+ }
+
+ if ( model->getMetaData( "MD3_animationcfg", value, sizeof( value ) ) )
+ {
+ m_saveAnimationCfg = !!atoi( value );
+ }
+}
+
IqeFilter::IqeFilter()
{
}
@@ -123,6 +206,101 @@ Model::ModelErrorE IqeFilter::writeFile( Model * model, const char * const filen
}
}
+ m_model = model;
+
+ string modelPath = "";
+ string modelBaseName = "";
+ string modelFullName = "";
+
+ normalizePath( filename, modelFullName, modelPath, modelBaseName );
+
+ m_modelPath = modelPath;
+
+ if ( m_options->m_saveAsPlayer )
+ {
+ log_debug( "saving as a player model\n" );
+
+ Model * section;
+ std::string playerFile;
+ std::string path = modelPath + "/";
+
+ section = m_model->copyQuake3PlayerSection( Model::MS_Lower, Model::ANIMMODE_SKELETAL );
+ if ( section )
+ {
+ playerFile = path + fixFileCase( m_modelPath.c_str(), "lower.iqe" );
+ writeSectionFile( section, playerFile.c_str() );
+ delete section;
+ }
+
+ section = m_model->copyQuake3PlayerSection( Model::MS_Upper, Model::ANIMMODE_SKELETAL );
+ if ( section )
+ {
+ playerFile = path + fixFileCase( m_modelPath.c_str(), "upper.iqe" );
+ writeSectionFile( section, playerFile.c_str() );
+ delete section;
+ }
+
+ section = m_model->copyQuake3PlayerSection( Model::MS_Head, Model::ANIMMODE_SKELETAL );
+ if ( section )
+ {
+ playerFile = path + fixFileCase( m_modelPath.c_str(), "head.iqe" );
+ writeSectionFile( section, playerFile.c_str() );
+ delete section;
+ }
+
+ if ( m_options->m_saveAnimationCfg )
+ {
+ writeAnimations( true, NULL );
+ }
+ else
+ {
+ model->addMetaData( "MD3_animationcfg", "0" );
+ }
+
+ model->addMetaData( "MD3_composite", "1" );
+ model->operationComplete( transll( QT_TRANSLATE_NOOP( "LowLevel", "Set meta data for IQE export" ) ).c_str() );
+
+ return Model::ERROR_NONE;
+ }
+ else
+ {
+ log_debug( "saving as a single model\n" );
+
+ writeSectionFile( model, filename );
+
+ if ( m_options->m_saveAnimationCfg )
+ {
+ writeAnimations( false, filename );
+ }
+
+ if ( m_options->m_saveAnimationCfg || m_options->m_playerSupported )
+ {
+ if ( m_options->m_playerSupported )
+ {
+ model->addMetaData( "MD3_composite", "0" );
+ }
+ if ( m_options->m_saveAnimationCfg )
+ {
+ model->addMetaData( "MD3_animationcfg", "1" );
+ }
+
+ model->operationComplete( transll( QT_TRANSLATE_NOOP( "LowLevel", "Set meta data for IQE export" ) ).c_str() );
+ }
+
+ return Model::ERROR_NONE;
+ }
+ }
+ else
+ {
+ log_error( "no filename supplied for model filter\n" );
+ return Model::ERROR_NO_FILE;
+ }
+}
+
+Model::ModelErrorE IqeFilter::writeSectionFile( Model *model, const char * filename )
+{
+ if ( model && filename && filename[0] )
+ {
Model::ModelErrorE err = Model::ERROR_NONE;
DataDest * dst = openOutput( filename, err );
DestCloser fc( dst );
@@ -256,7 +434,12 @@ Model::ModelErrorE IqeFilter::writeFile( Model * model, const char * const filen
// coordinate or normal. MM3D does. The mesh_create_list function will
// break the model up into meshes where vertices meet the MD3 criteria.
// See mesh.h for details.
+#if 0
+ // TODO: Split up meshes to meet Quake 3 per-mesh triangle and vertex limits. Need an export option dialog and/or meta data.
+ mesh_create_list( meshes, model, Mesh::MO_All, 2000 - 1, 1000 - 1 );
+#else
mesh_create_list( meshes, model );
+#endif
vector & modelMaterials = getMaterialList( model );
@@ -525,6 +708,342 @@ bool IqeFilter::writeLine( DataDest *dst, const char * line, ... )
return true;
}
+// writeAnimations() and it's util functions are copied from Md3Filter.
+// Changed to use ANIMMODE_SKELETAL and m_animations.
+bool IqeFilter::writeAnimations( bool playerModel, const char * modelName )
+{
+ string animFile = m_modelPath + "/animation.cfg";
+ bool eliteLoop = false;
+ bool animKeyword = false;
+
+ Model::ModelErrorE err = Model::ERROR_NONE;
+ DataDest *dst = openOutput( animFile.c_str(), err );
+ DestCloser fc( dst );
+
+ if ( err != Model::ERROR_NONE )
+ {
+ return false;
+ }
+ else
+ {
+ log_debug( "writing animation.cfg\n" );
+
+ if ( modelName )
+ {
+ dst->writePrintf( "// animation config file for %s\r\n", PORT_basename( modelName ) );
+ }
+ else
+ {
+ dst->writeString( "// animation config file\r\n" );
+ }
+
+ bool hadKeyword = false;
+ char keyword[1024], value[1024];
+
+ for (unsigned int i = 0; i < m_model->getMetaDataCount(); i++)
+ {
+ if (!m_model->getMetaData(i, keyword, sizeof (keyword), value, sizeof (value)))
+ continue;
+
+ if (strncasecmp(keyword, "MD3_CFG_", 8) == 0)
+ {
+ if (!hadKeyword)
+ {
+ hadKeyword = true;
+ dst->writeString( "\r\n" );
+ }
+ if (strlen(value) > 0)
+ dst->writePrintf( "%s %s\r\n", &keyword[8], value );
+ else
+ dst->writePrintf( "%s\r\n", &keyword[8] );
+ }
+ // Support old keywords
+ else if (strncasecmp(keyword, "MD3_sex", 7) == 0
+ || strncasecmp(keyword, "MD3_footsteps", 13) == 0
+ || strncasecmp(keyword, "MD3_headoffset", 14) == 0
+ || strncasecmp(keyword, "MD3_fixedtorso", 14) == 0
+ || strncasecmp(keyword, "MD3_fixedlegs", 13) == 0)
+ {
+ if (!hadKeyword)
+ {
+ hadKeyword = true;
+ dst->writeString( "\r\n" );
+ }
+ if (strlen(value) > 0)
+ dst->writePrintf( "%s %s\r\n", &keyword[4], value );
+ else
+ dst->writePrintf( "%s\r\n", &keyword[4] );
+ }
+ // animations.cfg format settings
+ else if (strncasecmp(keyword, "MD3_EliteLoop", 13) == 0)
+ {
+ eliteLoop = (atoi(value) > 0);
+ }
+ else if (strncasecmp(keyword, "MD3_AnimKeyword", 15) == 0)
+ {
+ animKeyword = (atoi(value) > 0);
+ }
+ }
+
+ dst->writeString( "\r\n" );
+
+ dst->writeString( "// frame data:\r\n" );
+ dst->writeString( "// first count looping fps\r\n\r\n" );
+
+ char warning[] = " (MUST NOT CHANGE -- hand animation is synced to this)";
+
+ size_t longestName = 16; // minimum name length for spacing
+ std::vector::iterator it;
+ for (it = m_options->m_animations.begin(); it != m_options->m_animations.end(); it++)
+ {
+ unsigned anim = *it;
+ std::string name = getSafeName( anim );
+ size_t len = name.length();
+ if (len > longestName) {
+ longestName = len;
+ }
+ }
+
+ for (it = m_options->m_animations.begin(); it != m_options->m_animations.end(); it++)
+ {
+ unsigned anim = *it;
+ int animFrame = 0;
+ int count = 1;
+ int fps = 15;
+ if (!getExportAnimData( playerModel, (int)anim, animFrame, count, fps ))
+ {
+ continue;
+ }
+
+ int loop = count; // loop by default
+
+ std::string name = getSafeName( anim );
+ size_t len = name.length();
+ for ( size_t n = 0; n < len; n++ )
+ {
+ name[n] = std::toupper(name[n]);
+ }
+
+ // disable looping on non-looping anims
+ if ( !m_model->getAnimLooping( Model::ANIMMODE_SKELETAL, anim ) )
+ {
+ loop = 0;
+ }
+
+ // Convert to Elite Force Single Player Style
+ if (eliteLoop)
+ {
+ if (loop == 0)
+ loop = -1; // No loop
+ else
+ loop = 0; // Loop
+ }
+
+ if (animKeyword)
+ {
+ // Align animFrame
+ const size_t MAX_SPACES = 40;
+ char spaces[MAX_SPACES+2] = {' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',
+ ' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','\0'};
+ size_t maxSpaces = (longestName+6 > MAX_SPACES) ? MAX_SPACES : longestName+6;
+
+ spaces[((name.length() < maxSpaces) ? (maxSpaces - name.length()) : 0)] = '\0';
+
+ if (animSyncWarning(name))
+ dst->writePrintf( "%s%s\t%d\t%d\t%d\t%d\t\t// %s\r\n",
+ name.c_str(), spaces, animFrame, count, loop, fps, warning );
+ else
+ dst->writePrintf( "%s%s\t%d\t%d\t%d\t%d\r\n",
+ name.c_str(), spaces, animFrame, count, loop, fps );
+ }
+ else
+ {
+ dst->writePrintf( "%d\t%d\t%d\t%d\t\t// %s%s\r\n",
+ animFrame, count, loop, fps, name.c_str(),
+ (animSyncWarning(name) ? warning : "") );
+ }
+ }
+ return true;
+ }
+}
+
+std::string IqeFilter::getSafeName( unsigned int anim )
+{
+ std::string animName = "none";
+
+ if ( anim < m_model->getAnimCount( Model::ANIMMODE_SKELETAL ) )
+ {
+ animName = m_model->getAnimName( Model::ANIMMODE_SKELETAL, anim );
+ }
+
+ return animName;
+}
+
+bool IqeFilter::animSyncWarning(std::string name)
+{
+ char value[20];
+
+ if ( m_model->getMetaData( "MD3_NoSyncWarning", value, sizeof(value) ) )
+ {
+ if ( atoi( value ) == 1 )
+ return false;
+ }
+
+ for (unsigned i = 0; s_animSyncWarning[i] != NULL; i++)
+ {
+ if (strncasecmp(s_animSyncWarning[i], name.c_str(), name.length()) == 0)
+ {
+ return true;
+ }
+ }
+ return false;
+}
+
+IqeFilter::MeshAnimationTypeE IqeFilter::getAnimationType( bool playerModel, const std::string & animName )
+{
+ MeshAnimationTypeE animType = MA_All;
+
+ if ( !playerModel )
+ {
+ return MA_All;
+ }
+
+ if (strncasecmp(animName.c_str(), "both_", 5) == 0)
+ {
+ animType = MA_Both;
+ }
+ else if (strncasecmp(animName.c_str(), "torso_", 6) == 0)
+ {
+ animType = MA_Torso;
+ }
+ else if (strncasecmp(animName.c_str(), "legs_", 5) == 0)
+ {
+ animType = MA_Legs;
+ }
+ else if (strncasecmp(animName.c_str(), "head_", 5) == 0)
+ {
+ animType = MA_Head;
+ }
+
+ return animType;
+}
+
+bool IqeFilter::animInSection( std::string animName, MeshSectionE section )
+{
+ if ( strncasecmp( animName.c_str(), "torso_", 6 ) == 0 )
+ {
+ if ( section == MS_Upper )
+ return true;
+ else
+ return false;
+ }
+ if ( strncasecmp( animName.c_str(), "legs_", 5 ) == 0 )
+ {
+ if ( section == MS_Lower )
+ return true;
+ else
+ return false;
+ }
+
+ if ( strncasecmp( animName.c_str(), "head_", 5 ) == 0 )
+ {
+ if ( section == MS_Head )
+ return true;
+ else
+ return false;
+ }
+
+ // It's a "both_" animation, or something weird
+ if ( strncasecmp( animName.c_str(), "both_", 5 ) == 0 )
+ {
+ if ( section == MS_Lower || section == MS_Upper )
+ return true;
+ else
+ return false;
+ }
+
+ if ( strncasecmp( animName.c_str(), "all_", 4 ) == 0 )
+ {
+ // Animation for torso, legs, and head!
+ return true;
+ }
+
+ return false;
+}
+
+bool IqeFilter::getExportAnimData( bool playerModel, int modelAnim,
+ int & fileFrame, int & frameCount, int & fps )
+{
+ fileFrame = 0;
+ frameCount = 0;
+
+ std::string animName = getSafeName( modelAnim );
+ MeshAnimationTypeE animType = getAnimationType( playerModel, animName );
+
+ // If this is a "dead" animation and its after a "death" animation
+ // and it has 0 frames, use the last frame of the death animation.
+ if (modelAnim > 0 && ((strncasecmp(animName.c_str(), "all_dead", 8) == 0
+ && strncasecmp(getSafeName( modelAnim - 1 ).c_str(), "all_death", 9) == 0)
+ || (strncasecmp(animName.c_str(), "both_dead", 9) == 0
+ && strncasecmp(getSafeName( modelAnim - 1 ).c_str(), "both_death", 10) == 0))
+ && m_model->getAnimFrameCount( Model::ANIMMODE_SKELETAL, modelAnim ) == 0 )
+ {
+ if ( getExportAnimData( playerModel, modelAnim - 1, fileFrame, frameCount, fps ) )
+ {
+ fileFrame += frameCount - 1;
+ frameCount = 1;
+ return true;
+ }
+ }
+
+ std::vector::iterator it;
+ for (it = m_options->m_animations.begin(); it != m_options->m_animations.end(); it++)
+ {
+ size_t a = *it;
+ std::string name = getSafeName( a );
+ if ( !playerModel
+ || animInSection( name, MS_Upper )
+ || animInSection( name, MS_Lower )
+ || animInSection( name, MS_Head ) )
+ {
+ MeshAnimationTypeE type = getAnimationType( playerModel, name );
+ if ( (int)a == modelAnim )
+ {
+ frameCount = m_model->getAnimFrameCount( Model::ANIMMODE_SKELETAL, a );
+ fps = (int) m_model->getAnimFPS( Model::ANIMMODE_SKELETAL, a );
+
+ if ( fps <= 0 ) // just being paranoid
+ {
+ fps = 15;
+ }
+
+ if (animType > MA_Torso)
+ {
+ // Must still count torso animations after this for fileFrame
+ // and in the case of 'head' we must also count legs animations
+ }
+ else
+ {
+ return true;
+ }
+ }
+ // All torso frames go before leg frames, all legs go after torso
+ else if (((int)a < modelAnim && animType >= type) || ((int)a > modelAnim && animType > type))
+ {
+ fileFrame += m_model->getAnimFrameCount( Model::ANIMMODE_SKELETAL, a );
+ }
+ }
+ }
+
+ if (animType > MA_Torso && frameCount)
+ {
+ // Finished adding up fileFrame for legs or head animations
+ return true;
+ }
+
+ return false;
+}
+
bool IqeFilter::canRead( const char * filename )
{
return false;
diff --git a/src/libmm3d/iqefilter.h b/src/libmm3d/iqefilter.h
index bac7b4e9..85107bda 100644
--- a/src/libmm3d/iqefilter.h
+++ b/src/libmm3d/iqefilter.h
@@ -40,12 +40,19 @@ class IqeFilter : public ModelFilter
virtual void release() { delete this; };
+ bool m_playerSupported;
+
+ bool m_saveAsPlayer;
+ bool m_saveAnimationCfg;
+
bool m_saveMeshes;
bool m_savePointsJoint;
bool m_saveSkeleton;
bool m_saveAnimations;
std::vector m_animations;
+ void setOptionsFromModel( Model * m, const char * const filename );
+
protected:
virtual ~IqeOptions(); // Use release() instead
};
@@ -69,9 +76,41 @@ class IqeFilter : public ModelFilter
protected:
+ typedef enum _MeshSection_e
+ {
+ MS_Lower = 0,
+ MS_Upper,
+ MS_Head,
+ MS_MAX
+ } MeshSectionE;
+
+ // the order is important, used for writing continue frames by type
+ typedef enum _MeshAnimationType_e
+ {
+ MA_All,
+ MA_Both,
+ MA_Torso,
+ MA_Legs,
+ // NOTE: Team Arena has extra torso animations after legs
+ MA_Head,
+ MA_MAX
+ } MeshAnimationTypeE;
+
IqeOptions * m_options;
+ Model * m_model;
+ std::string m_modelPath;
bool writeLine( DataDest *dst, const char * line, ... ) __attribute__ ((format (printf, 3, 4)));
+
+ Model::ModelErrorE writeSectionFile( Model *model, const char * filename );
+
+ bool writeAnimations( bool playerModel, const char * filename );
+ std::string getSafeName( unsigned int anim );
+ bool animSyncWarning(std::string name);
+ MeshAnimationTypeE getAnimationType( bool playerModel, const std::string & animName );
+ bool animInSection( std::string animName, MeshSectionE section );
+ bool getExportAnimData( bool playerModel, int modelAnim,
+ int & fileFrame, int & frameCount, int & fps );
};
#endif // __IQEFILTER_H
diff --git a/src/libmm3d/md3filter.cc b/src/libmm3d/md3filter.cc
index 09222b35..775c37e3 100644
--- a/src/libmm3d/md3filter.cc
+++ b/src/libmm3d/md3filter.cc
@@ -100,7 +100,7 @@ const char s_animNames[ MD3_ANIMATIONS ][16] =
"legs_turn",
};
-const char *s_animSyncWarning[] =
+static const char *s_animSyncWarning[] =
{
"torso_attack",
"torso_attack2",
diff --git a/src/qtui/iqeprompt.ui b/src/qtui/iqeprompt.ui
index 30404f1a..c02b3bec 100644
--- a/src/qtui/iqeprompt.ui
+++ b/src/qtui/iqeprompt.ui
@@ -14,6 +14,26 @@
IQE Filter Options
+ -
+
+
+ Save as a Quake 3 Player (head.iqe, upper.iqe, lower.iqe)
+
+
+ true
+
+
+
+ -
+
+
+ Save Quake 3 animation.cfg
+
+
+ true
+
+
+
-
@@ -124,6 +144,8 @@
+ m_saveAsPlayer
+ m_saveAnimationCfg
m_saveMeshes
m_savePointsJoint
m_saveSkeleton
diff --git a/src/qtui/md3prompt.ui b/src/qtui/md3prompt.ui
index 181b43bc..48cb7983 100644
--- a/src/qtui/md3prompt.ui
+++ b/src/qtui/md3prompt.ui
@@ -17,7 +17,7 @@
-
- Save as a Player Model (head.md3, upper.md3, lower.md3)
+ Save as a Quake 3 Player (head.md3, upper.md3, lower.md3)
true
@@ -27,7 +27,7 @@
-
- Save animation.cfg
+ Save Quake 3 animation.cfg
true