You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
514 lines
15 KiB
514 lines
15 KiB
14 years ago
|
/*==LICENSE==*
|
||
|
|
||
|
CyanWorlds.com Engine - MMOG client, server and tools
|
||
13 years ago
|
Copyright (C) 2011 Cyan Worlds, Inc.
|
||
14 years ago
|
|
||
|
This program is free software: you can redistribute it and/or modify
|
||
|
it under the terms of the GNU General Public License as published by
|
||
|
the Free Software Foundation, either version 3 of the License, or
|
||
|
(at your option) any later version.
|
||
|
|
||
|
This program is distributed in the hope that it will be useful,
|
||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
13 years ago
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
14 years ago
|
GNU General Public License for more details.
|
||
|
|
||
|
You should have received a copy of the GNU General Public License
|
||
13 years ago
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||
|
|
||
|
Additional permissions under GNU GPL version 3 section 7
|
||
|
|
||
|
If you modify this Program, or any covered work, by linking or
|
||
|
combining it with any of RAD Game Tools Bink SDK, Autodesk 3ds Max SDK,
|
||
|
NVIDIA PhysX SDK, Microsoft DirectX SDK, OpenSSL library, Independent
|
||
|
JPEG Group JPEG library, Microsoft Windows Media SDK, or Apple QuickTime SDK
|
||
|
(or a modified version of those libraries),
|
||
|
containing parts covered by the terms of the Bink SDK EULA, 3ds Max EULA,
|
||
|
PhysX SDK EULA, DirectX SDK EULA, OpenSSL and SSLeay licenses, IJG
|
||
|
JPEG Library README, Windows Media SDK EULA, or QuickTime SDK EULA, the
|
||
|
licensors of this Program grant you additional
|
||
|
permission to convey the resulting work. Corresponding Source for a
|
||
|
non-source form of such a combination shall include the source code for
|
||
|
the parts of OpenSSL and IJG JPEG Library used as well as that of the covered
|
||
|
work.
|
||
14 years ago
|
|
||
|
You can contact Cyan Worlds, Inc. by email legal@cyan.com
|
||
|
or by snail mail at:
|
||
|
Cyan Worlds, Inc.
|
||
|
14617 N Newport Hwy
|
||
|
Mead, WA 99021
|
||
|
|
||
|
*==LICENSE==*/
|
||
|
|
||
|
//
|
||
|
// 3DSMax HeadSpin exporter
|
||
|
//
|
||
|
#include "hsTypes.h"
|
||
|
#include "Max.h"
|
||
|
#include "istdplug.h"
|
||
|
#include "Notify.h"
|
||
|
#include <commdlg.h>
|
||
|
#include "bmmlib.h"
|
||
|
#include "INode.h"
|
||
|
|
||
|
#include "plConvert.h"
|
||
|
#include "hsResMgr.h"
|
||
|
#include "hsTemplates.h"
|
||
|
|
||
|
#include "hsConverterUtils.h"
|
||
|
#include "hsControlConverter.h"
|
||
|
#include "plMeshConverter.h"
|
||
|
#include "hsMaterialConverter.h"
|
||
|
#include "plLayerConverter.h"
|
||
|
#include "UserPropMgr.h"
|
||
|
#include "hsStringTokenizer.h"
|
||
|
#include "../MaxExport/plErrorMsg.h"
|
||
|
#include "hsVertexShader.h"
|
||
|
#include "plLightMapGen.h"
|
||
|
#include "plBitmapCreator.h"
|
||
|
#include "plgDispatch.h"
|
||
|
|
||
|
#include "../pnMessage/plTimeMsg.h"
|
||
|
#include "../MaxComponent/plComponent.h"
|
||
|
#include "../MaxMain/plMaxNode.h"
|
||
|
#include "../plMessage/plNodeCleanupMsg.h"
|
||
|
#include "../pnSceneObject/plSceneObject.h"
|
||
|
#include "../MaxComponent/plClusterComponent.h"
|
||
|
|
||
|
#include "../plPhysX/plSimulationMgr.h"
|
||
|
#include "../MaxMain/plPhysXCooking.h"
|
||
|
#include "../MaxExport/plExportProgressBar.h"
|
||
|
#include "hsUtils.h"
|
||
|
|
||
|
#include "../MaxMain/plGetLocationDlg.h"
|
||
|
|
||
|
#ifdef HS_DEBUGGING
|
||
|
#define HS_NO_TRY
|
||
|
#endif
|
||
|
|
||
|
plConvert::plConvert() : fWarned(0)
|
||
|
{
|
||
|
}
|
||
|
|
||
|
plConvert& plConvert::Instance()
|
||
|
{
|
||
|
static plConvert theInstance;
|
||
|
return theInstance;
|
||
|
}
|
||
|
|
||
|
hsBool plConvert::IOK()
|
||
|
{
|
||
|
return (!fQuit && !fpErrorMsg->IsBogus() ) ? true: false;
|
||
|
}
|
||
|
|
||
|
hsBool plConvert::Convert()
|
||
|
{
|
||
|
#ifndef HS_NO_TRY
|
||
|
try
|
||
|
#endif
|
||
|
{
|
||
|
fSettings->fReconvert = false;
|
||
|
fWarned = 0;
|
||
|
|
||
|
fInterface->SetIncludeXRefsInHierarchy(TRUE);
|
||
|
|
||
|
plMaxNode *pNode = (plMaxNode *)fInterface->GetRootNode();
|
||
|
AddMessageToQueue(new plTransformMsg(nil, nil, nil, nil));
|
||
|
AddMessageToQueue(new plDelayedTransformMsg(nil, nil, nil, nil));
|
||
|
|
||
|
IFindDuplicateNames();
|
||
|
|
||
|
plExportProgressBar bar;
|
||
|
hsBool retVal = true; // sometime, we might look at this
|
||
|
|
||
|
if( !IAutoClusterRecur(pNode) )
|
||
|
{
|
||
|
fQuit = true;
|
||
|
}
|
||
|
|
||
|
if(IOK())
|
||
|
{
|
||
|
bar.Start("Clear Old Data");
|
||
|
retVal = pNode->DoAllRecur( plMaxNode::ClearData, fpErrorMsg, fSettings, &bar );
|
||
|
}
|
||
|
if(IOK())
|
||
|
{
|
||
|
bar.Start("Convert Validate");
|
||
|
retVal = pNode->DoRecur( plMaxNode::ConvertValidate, fpErrorMsg, fSettings, &bar );
|
||
|
}
|
||
|
if(IOK())
|
||
|
{
|
||
|
bar.Start("Components Initialize");
|
||
|
retVal = pNode->DoRecur( plMaxNode::SetupPropertiesPass, fpErrorMsg, fSettings, &bar );
|
||
|
}
|
||
|
if(IOK())
|
||
|
{
|
||
|
bar.Start("Prepare for skinning");
|
||
|
retVal = pNode->DoRecur( plMaxNode::PrepareSkin, fpErrorMsg, fSettings, &bar );
|
||
|
}
|
||
|
if(IOK())
|
||
|
{
|
||
|
bar.Start("Make Scene Object");
|
||
|
retVal = pNode->DoRecur( plMaxNode::MakeSceneObject, fpErrorMsg, fSettings, &bar );
|
||
|
}
|
||
|
if(IOK())
|
||
|
{
|
||
|
bar.Start("Make Physical");
|
||
|
plPhysXCooking::Init();
|
||
|
retVal = pNode->DoRecur( plMaxNode::MakePhysical, fpErrorMsg, fSettings, &bar );
|
||
|
plPhysXCooking::Shutdown();
|
||
|
}
|
||
|
if(IOK())
|
||
|
{
|
||
|
bar.Start("Component Preconvert");
|
||
|
retVal = pNode->DoRecur( plMaxNode::FirstComponentPass, fpErrorMsg, fSettings, &bar );
|
||
|
}
|
||
|
if(IOK())
|
||
|
{
|
||
|
bar.Start("Make Controller");
|
||
|
retVal = pNode->DoRecur( plMaxNode::MakeController, fpErrorMsg, fSettings, &bar );
|
||
|
}
|
||
|
if(IOK())
|
||
|
{ // must be before mesh
|
||
|
bar.Start("Make Coord Interface");
|
||
|
retVal = pNode->DoRecur( plMaxNode::MakeCoordinateInterface, fpErrorMsg, fSettings, &bar );
|
||
|
}
|
||
|
if(IOK())
|
||
|
{ // must be after coord interface but before pool data is created.
|
||
|
bar.Start("Make Connections");
|
||
|
retVal = pNode->DoRecur( plMaxNode::MakeParentOrRoomConnection, fpErrorMsg, fSettings, &bar );
|
||
|
}
|
||
|
|
||
|
if(IOK())
|
||
|
{ // must be before simulation
|
||
|
bar.Start("Make Mesh");
|
||
|
retVal = pNode->DoRecur( plMaxNode::MakeMesh, fpErrorMsg, fSettings, &bar );
|
||
|
}
|
||
|
|
||
|
if(IOK())
|
||
|
{ // doesn't matter when
|
||
|
bar.Start("Make Light");
|
||
|
retVal = pNode->DoRecur( plMaxNode::MakeLight, fpErrorMsg, fSettings, &bar );
|
||
|
}
|
||
|
if(IOK())
|
||
|
{ // doesn't matter when
|
||
|
bar.Start("Make Occluder");
|
||
|
retVal = pNode->DoRecur( plMaxNode::MakeOccluder, fpErrorMsg, fSettings, &bar );
|
||
|
}
|
||
|
if(IOK())
|
||
|
{ // must be after mesh
|
||
|
bar.Start("Make Modifiers");
|
||
|
retVal = pNode->DoRecur( plMaxNode::MakeModifiers, fpErrorMsg, fSettings, &bar );
|
||
|
}
|
||
|
if(IOK())
|
||
|
{
|
||
|
bar.Start("Convert Components");
|
||
|
retVal = pNode->DoRecur( plMaxNode::ConvertComponents, fpErrorMsg, fSettings, &bar );
|
||
|
}
|
||
|
if(IOK())
|
||
|
{
|
||
|
// do this after convert
|
||
|
bar.Start("Set Up Interface References");
|
||
|
retVal = pNode->DoRecur( plMaxNode::MakeIfaceReferences, fpErrorMsg, fSettings, &bar );
|
||
|
}
|
||
|
|
||
|
if(IOK() && fSettings->fDoPreshade)
|
||
|
{
|
||
|
// These need to be opened after the components have had a chance to flag the MaxNodes
|
||
|
plLightMapGen::Instance().Open(fInterface, fInterface->GetTime(), fSettings->fDoLightMap);
|
||
|
hsVertexShader::Instance().Open();
|
||
|
|
||
|
bar.Start("Preshade Geometry");
|
||
|
retVal = pNode->DoRecur( plMaxNode::ShadeMesh, fpErrorMsg, fSettings, &bar );
|
||
|
|
||
|
plLightMapGen::Instance().Close();
|
||
|
hsVertexShader::Instance().Close();
|
||
|
}
|
||
|
|
||
|
if(IOK())
|
||
|
{
|
||
|
// Do this next-to-last--allows all the components to free up any temp data they kept around
|
||
|
bar.Start("Component DeInit");
|
||
|
retVal = pNode->DoRecur( plMaxNode::DeInitComponents, fpErrorMsg, fSettings, &bar );
|
||
|
}
|
||
|
if(IOK())
|
||
|
{
|
||
|
// Do this very last--it de-inits and frees all the maxNodeDatas lying around
|
||
|
bar.Start("Clear MaxNodeDatas");
|
||
|
retVal = pNode->DoAllRecur( plMaxNode::ClearMaxNodeData, fpErrorMsg, fSettings, &bar );
|
||
|
}
|
||
|
// fpErrorMsg->Set();
|
||
|
|
||
|
DeInit();
|
||
|
|
||
|
fInterface->SetIncludeXRefsInHierarchy(FALSE);
|
||
|
|
||
|
return IOK();
|
||
|
}
|
||
|
#ifndef HS_NO_TRY
|
||
|
catch(plErrorMsg& err)
|
||
|
{
|
||
|
DeInit();
|
||
|
fInterface->SetIncludeXRefsInHierarchy(FALSE);
|
||
|
err.Show();
|
||
|
return false;
|
||
|
}
|
||
|
catch(...)
|
||
|
{
|
||
|
DeInit();
|
||
|
fInterface->SetIncludeXRefsInHierarchy(FALSE);
|
||
|
fpErrorMsg->Set(true, "plConvert", "Unknown error during convert\n");
|
||
|
fpErrorMsg->Show();
|
||
|
return false;
|
||
|
}
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
//#include "../MaxMain/plMaxNodeData.h"
|
||
|
//#include <set>
|
||
|
|
||
|
hsBool ConvertList(hsTArray<plMaxNode*>& nodes, PMaxNodeFunc p, plErrorMsg *errMsg, plConvertSettings *settings)
|
||
|
{
|
||
|
for (int i = 0; i < nodes.Count(); i++)
|
||
|
{
|
||
|
(nodes[i]->*p)(errMsg, settings);
|
||
|
|
||
|
if (errMsg && errMsg->IsBogus())
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
hsBool plConvert::Convert(hsTArray<plMaxNode*>& nodes)
|
||
|
{
|
||
|
#ifndef HS_NO_TRY
|
||
|
try
|
||
|
#endif
|
||
|
{
|
||
|
fSettings->fReconvert = true;
|
||
|
|
||
|
hsBool retVal = true;
|
||
|
|
||
|
if (IOK())
|
||
|
retVal = ConvertList(nodes, plMaxNode::ClearData, fpErrorMsg, fSettings);
|
||
|
|
||
|
if(IOK())
|
||
|
retVal = ConvertList(nodes, plMaxNode::ConvertValidate, fpErrorMsg, fSettings);
|
||
|
if(IOK())
|
||
|
retVal = ConvertList(nodes, plMaxNode::SetupPropertiesPass, fpErrorMsg, fSettings);
|
||
|
if(IOK())
|
||
|
retVal = ConvertList(nodes, plMaxNode::PrepareSkin, fpErrorMsg, fSettings);
|
||
|
if(IOK())
|
||
|
retVal = ConvertList(nodes, plMaxNode::MakeSceneObject, fpErrorMsg, fSettings);
|
||
|
if(IOK())
|
||
|
retVal = ConvertList(nodes, plMaxNode::FirstComponentPass, fpErrorMsg, fSettings);
|
||
|
if(IOK())
|
||
|
retVal = ConvertList(nodes, plMaxNode::MakeController, fpErrorMsg,fSettings);
|
||
|
if(IOK())
|
||
|
retVal = ConvertList(nodes, plMaxNode::MakeCoordinateInterface, fpErrorMsg, fSettings);// must be before mesh
|
||
|
if(IOK())
|
||
|
retVal = ConvertList(nodes, plMaxNode::MakeParentOrRoomConnection, fpErrorMsg, fSettings); // after coord, before mesh (or any other pool data).
|
||
|
|
||
|
// These shouldn't be opened until the components have had a chance to flag the MaxNodes
|
||
|
plLightMapGen::Instance().Open(fInterface, fInterface->GetTime(), fSettings->fDoLightMap);
|
||
|
hsVertexShader::Instance().Open();
|
||
|
|
||
|
if(IOK())
|
||
|
retVal = ConvertList(nodes, plMaxNode::MakeMesh, fpErrorMsg, fSettings); // must be before simulation
|
||
|
|
||
|
if(IOK()) // doesn't matter when
|
||
|
retVal = ConvertList(nodes, plMaxNode::MakeLight, fpErrorMsg, fSettings);
|
||
|
if(IOK()) // doesn't matter when
|
||
|
retVal = ConvertList(nodes, plMaxNode::MakeOccluder, fpErrorMsg, fSettings);
|
||
|
if(IOK()) // must be after mesh
|
||
|
retVal = ConvertList(nodes, plMaxNode::MakeModifiers, fpErrorMsg, fSettings);
|
||
|
if(IOK())
|
||
|
retVal = ConvertList(nodes, plMaxNode::ConvertComponents, fpErrorMsg, fSettings);
|
||
|
if(IOK())
|
||
|
retVal = ConvertList(nodes, plMaxNode::ShadeMesh, fpErrorMsg, fSettings);
|
||
|
|
||
|
// These may be used by components, so don't close them till the end.
|
||
|
plLightMapGen::Instance().Close();
|
||
|
hsVertexShader::Instance().Close();
|
||
|
|
||
|
plgDispatch::MsgSend(new plTransformMsg(nil, nil, nil, nil));
|
||
|
plgDispatch::MsgSend(new plDelayedTransformMsg(nil, nil, nil, nil));
|
||
|
DeInit();
|
||
|
|
||
|
return IOK();
|
||
|
}
|
||
|
#ifndef HS_NO_TRY
|
||
|
catch(plErrorMsg& err)
|
||
|
{
|
||
|
err.Show();
|
||
|
return false;
|
||
|
}
|
||
|
catch(...)
|
||
|
{
|
||
|
hsMessageBox("Unknown error during convert", "plConvert", hsMessageBoxNormal);
|
||
|
return false;
|
||
|
}
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
hsBool plConvert::Init(Interface *ip, plErrorMsg* msg, plConvertSettings *settings)
|
||
|
{
|
||
|
fInterface = ip;
|
||
|
fpErrorMsg = msg;
|
||
|
fSettings = settings;
|
||
|
|
||
|
// Move us to time 0, so that things like initial transforms are always consistent with the 0th frame.
|
||
|
// This saves our asses from things like the patch-generation process later
|
||
|
ip->SetTime( 0, false );
|
||
|
|
||
|
hsConverterUtils::Instance().Init(true, fpErrorMsg);
|
||
|
plBitmapCreator::Instance().Init(true, fpErrorMsg);
|
||
|
hsMaterialConverter::Instance().Init(true, fpErrorMsg);
|
||
|
hsControlConverter::Instance().Init(fpErrorMsg);
|
||
|
plMeshConverter::Instance().Init(true, fpErrorMsg);
|
||
|
plLayerConverter::Instance().Init(true, fpErrorMsg);
|
||
|
|
||
|
plGetLocationDlg::Instance().ResetDefaultLocation();
|
||
|
|
||
|
fQuit = false;
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
void plConvert::DeInit()
|
||
|
{
|
||
|
// Undo any autogenerated clusters.
|
||
|
IAutoUnClusterRecur(fInterface->GetRootNode());
|
||
|
|
||
|
// clear out the message queue
|
||
|
for (int i = 0; i < fMsgQueue.Count(); i++)
|
||
|
plgDispatch::MsgSend(fMsgQueue[i]);
|
||
|
|
||
|
fMsgQueue.Reset();
|
||
|
|
||
|
hsControlConverter::Instance().DeInit();
|
||
|
plMeshConverter::Instance().DeInit();
|
||
|
plLayerConverter::Instance().DeInit();
|
||
|
// Moving this to the end of writing the files out. Yes, this means that any unused mipmaps still get
|
||
|
// written to disk, including ones loaded on preload, but it's the only way to get shared texture pages
|
||
|
// to work without loading in the entire age worth of reffing objects. - 5.30.2002 mcn
|
||
|
// plBitmapCreator::Instance().DeInit();
|
||
|
|
||
|
plNodeCleanupMsg *clean = TRACKED_NEW plNodeCleanupMsg();
|
||
|
plgDispatch::MsgSend( clean );
|
||
|
}
|
||
|
|
||
|
void plConvert::AddMessageToQueue(plMessage* msg)
|
||
|
{
|
||
|
fMsgQueue.Append(msg);
|
||
|
}
|
||
|
|
||
|
void plConvert::SendEnvironmentMessage(plMaxNode* pNode, plMaxNode* efxRegion, plMessage* msg, hsBool ignorePhysicals )
|
||
|
{
|
||
|
for (int i = 0; i < pNode->NumberOfChildren(); i++)
|
||
|
SendEnvironmentMessage((plMaxNode *)pNode->GetChildNode(i), efxRegion, msg, ignorePhysicals );
|
||
|
|
||
|
// don't call ourself...
|
||
|
if (pNode == efxRegion)
|
||
|
return;
|
||
|
|
||
|
// send the scene object this message:
|
||
|
if (efxRegion->Contains( ((INode*)pNode)->GetNodeTM(hsConverterUtils::Instance().GetTime(pNode->GetInterface())).GetRow(3)) &&
|
||
|
pNode->GetSceneObject() && ( !ignorePhysicals || !pNode->IsPhysical() ) )
|
||
|
msg->AddReceiver( pNode->GetSceneObject()->GetKey() );
|
||
|
}
|
||
|
|
||
|
plMaxNode* plConvert::GetRootNode()
|
||
|
{
|
||
|
return (plMaxNode *)fInterface->GetRootNode();
|
||
|
}
|
||
|
|
||
|
BOOL plConvert::IAutoClusterRecur(INode* node)
|
||
|
{
|
||
|
plMaxNode* maxNode = (plMaxNode*)node;
|
||
|
plComponentBase* comp = maxNode->ConvertToComponent();
|
||
|
|
||
|
if( comp && (comp->ClassID() == CLUSTER_COMP_CID) )
|
||
|
{
|
||
|
plClusterComponent* clust = (plClusterComponent*)comp;
|
||
|
// Cluster decides if it needs autogen
|
||
|
if( clust->AutoGen(fpErrorMsg) )
|
||
|
return false;
|
||
|
}
|
||
|
int i;
|
||
|
for( i = 0; i < node->NumberOfChildren(); i++ )
|
||
|
{
|
||
|
if( !IAutoClusterRecur(node->GetChildNode(i)) )
|
||
|
return false;
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
BOOL plConvert::IAutoUnClusterRecur(INode* node)
|
||
|
{
|
||
|
plMaxNode* maxNode = (plMaxNode*)node;
|
||
|
plComponentBase* comp = maxNode->ConvertToComponent();
|
||
|
|
||
|
if( comp && (comp->ClassID() == CLUSTER_COMP_CID) )
|
||
|
{
|
||
|
plClusterComponent* clust = (plClusterComponent*)comp;
|
||
|
// Cluster remembers whether it was autogen'd.
|
||
|
clust->AutoClear(fpErrorMsg);
|
||
|
}
|
||
|
int i;
|
||
|
for( i = 0; i < node->NumberOfChildren(); i++ )
|
||
|
{
|
||
|
IAutoUnClusterRecur(node->GetChildNode(i));
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
bool plConvert::IFindDuplicateNames()
|
||
|
{
|
||
|
INode *node = fInterface->GetRootNode();
|
||
|
const char *name = ISearchNames(node, node);
|
||
|
|
||
|
if (!name)
|
||
|
return false;
|
||
|
|
||
|
fpErrorMsg->Set(true,
|
||
|
"Error in Conversion of Scene Objects",
|
||
|
"Two objects in the scene share the name '%s'.\nUnique names are necessary during the export process.\n",
|
||
|
name
|
||
|
);
|
||
|
fpErrorMsg->Show();
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
// Recursivly search nodes for duplicate names, and return when one is found
|
||
|
const char *plConvert::ISearchNames(INode *node, INode *root)
|
||
|
{
|
||
|
int count = ICountNameOccurances(root, node->GetName());
|
||
|
if (count > 1)
|
||
|
return node->GetName();
|
||
|
|
||
|
for (int i = 0; i < node->NumberOfChildren(); i++)
|
||
|
{
|
||
|
const char *name = ISearchNames(node->GetChildNode(i), root);
|
||
|
if (name)
|
||
|
return name;
|
||
|
}
|
||
|
|
||
|
return nil;
|
||
|
}
|
||
|
|
||
|
// Recursivly search nodes for this name, and return the number of times found
|
||
|
int plConvert::ICountNameOccurances(INode *node, const char *name)
|
||
|
{
|
||
|
int count = 0;
|
||
|
|
||
|
if (!stricmp(name, node->GetName()))
|
||
|
count++;
|
||
|
|
||
|
for (int i = 0; i < node->NumberOfChildren(); i++)
|
||
|
count += ICountNameOccurances(node->GetChildNode(i), name);
|
||
|
|
||
|
return count;
|
||
|
}
|