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.

742 lines
22 KiB

/*==LICENSE==*
CyanWorlds.com Engine - MMOG client, server and tools
Copyright (C) 2011 Cyan Worlds, Inc.
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
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
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.
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==*/
//////////////////////////////////////////////////////////////////////////////
//
// plResManagerHelper - The wonderful helper class that can receive messages
// for the resManager.
//
//// History /////////////////////////////////////////////////////////////////
//
// 6.7.2002 mcn - Created
//
//////////////////////////////////////////////////////////////////////////////
#include "hsTypes.h"
#include "plResManagerHelper.h"
#include "plResManager.h"
#include "plRegistryNode.h"
#include "plRegistryHelpers.h"
//#include "plRegistry.h"
#include "plResMgrSettings.h"
#include "../pnKeyedObject/plFixedKey.h"
#include "../plMessage/plResMgrHelperMsg.h"
#include "../plStatusLog/plStatusLog.h"
#include "hsTimer.h"
#ifdef MCN_RESMGR_DEBUGGING
static const int kLogSize = 40;
static const float kUpdateDelay = 0.5f;
#include "../plInputCore/plInputInterface.h"
#include "../plInputCore/plInputDevice.h"
#include "../plInputCore/plInputInterfaceMgr.h"
#include "../pnInputCore/plInputMap.h"
#include "../plMessage/plInputEventMsg.h"
#include "../plMessage/plInputIfaceMgrMsg.h"
#include "../pnKeyedObject/plKeyImp.h"
#endif
/// Logging #define for easier use
#define kResMgrLog( level ) if( plResMgrSettings::Get().GetLoggingLevel() >= level ) plStatusLog::AddLineS( "resources.log",
//// Constructor/Destructor //////////////////////////////////////////////////
plResManagerHelper *plResManagerHelper::fInstance = nil;
plResManagerHelper::plResManagerHelper( plResManager *resMgr )
{
fResManager = resMgr;
fInstance = this;
fInShutdown = false;
#ifdef MCN_RESMGR_DEBUGGING
fDebugScreen = nil;
fCurrAge = -1;
fCurrAgeExpanded = false;
fRefreshing = false;
fDebugDisplayType = 0;
#endif
}
plResManagerHelper::~plResManagerHelper()
{
fInstance = nil;
}
//// Shutdown ////////////////////////////////////////////////////////////////
void plResManagerHelper::Shutdown( void )
{
EnableDebugScreen( false );
UnRegisterAs( kResManagerHelper_KEY );
}
//// Init ////////////////////////////////////////////////////////////////////
void plResManagerHelper::Init( void )
{
RegisterAs( kResManagerHelper_KEY );
}
//// MsgReceive //////////////////////////////////////////////////////////////
hsBool plResManagerHelper::MsgReceive( plMessage *msg )
{
plResMgrHelperMsg *refferMsg = plResMgrHelperMsg::ConvertNoRef( msg );
if( refferMsg != nil )
{
if( refferMsg->GetCommand() == plResMgrHelperMsg::kKeyRefList )
{
// Message to let go of these keys. So unref the key list, destroy it and we're done!
kResMgrLog( 2 ) 0xff80ff80, "Dropping page keys after timed delay" );
hsStatusMessage( "*** Dropping page keys after timed delay ***" );
delete refferMsg->fKeyList;
refferMsg->fKeyList = nil;
}
else if( refferMsg->GetCommand() == plResMgrHelperMsg::kUpdateDebugScreen )
{
IUpdateDebugScreen();
}
else if( refferMsg->GetCommand() == plResMgrHelperMsg::kEnableDebugScreen )
EnableDebugScreen( true );
else if( refferMsg->GetCommand() == plResMgrHelperMsg::kDisableDebugScreen )
EnableDebugScreen( false );
return true;
}
return hsKeyedObject::MsgReceive( msg );
}
//// Read/Write //////////////////////////////////////////////////////////////
void plResManagerHelper::Read( hsStream *s, hsResMgr *mgr )
{
hsAssert( false, "You should never read me in!" );
}
void plResManagerHelper::Write( hsStream *s, hsResMgr *mgr )
{
hsAssert( false, "You should never write me out!" );
}
//// LoadAndHoldPageKeys /////////////////////////////////////////////////////
// Loads and refs the keys for the given page, then sends the ref list as
// a list to ourself, time delayed 1 second, so that we can unref them one
// second later.
void plResManagerHelper::LoadAndHoldPageKeys( plRegistryPageNode *page )
{
hsAssert( GetKey() != nil, "Can't load and hold keys when we don't have a key for the helper" );
// Create our msg
plResMgrHelperMsg *refferMsg = TRACKED_NEW plResMgrHelperMsg( plResMgrHelperMsg::kKeyRefList );
refferMsg->fKeyList = TRACKED_NEW plResPageKeyRefList;
fResManager->LoadPageKeys(page);
page->IterateKeys( refferMsg->fKeyList );
// Load and ref the keys
#ifdef HS_DEBUGGING
char msg[ 256 ];
sprintf( msg, "*** Temporarily loading keys for room %s>%s based on FindKey() query, will drop in 1 sec ***", page->GetPageInfo().GetAge(), page->GetPageInfo().GetPage() );
hsStatusMessage( msg );
#endif
kResMgrLog( 2 ) 0xff80ff80, "Temporarily loading keys for room %s>%s, will drop in 1 sec", page->GetPageInfo().GetAge(), page->GetPageInfo().GetPage() );
// Deliver the message to ourselves!
refferMsg->SetTimeStamp( hsTimer::GetSysSeconds() + 1.f );
refferMsg->Send( GetKey() );
}
#ifdef MCN_RESMGR_DEBUGGING
//////////////////////////////////////////////////////////////////////////////
//// plResMgrDebugInterface Definition ///////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
class plResMgrDebugInterface : public plInputInterface
{
protected:
plResManagerHelper * const fParent;
virtual ControlEventCode *IGetOwnedCodeList( void ) const
{
static ControlEventCode codes[] = { END_CONTROLS };
return codes;
}
public:
plResMgrDebugInterface( plResManagerHelper * const mgr ) : fParent( mgr ) { SetEnabled( true ); }
virtual UInt32 GetPriorityLevel( void ) const { return kGUISystemPriority + 10; }
virtual hsBool InterpretInputEvent( plInputEventMsg *pMsg )
{
plKeyEventMsg *pKeyMsg = plKeyEventMsg::ConvertNoRef( pMsg );
if( pKeyMsg != nil && pKeyMsg->GetKeyDown() )
{
if( pKeyMsg->GetKeyCode() == KEY_UP && fParent->fCurrAge >= 0 )
{
fParent->fCurrAge--;
fParent->IUpdateDebugScreen( true );
return true;
}
else if( pKeyMsg->GetKeyCode() == KEY_DOWN )
{
fParent->fCurrAge++;
fParent->IUpdateDebugScreen( true );
return true;
}
else if( pKeyMsg->GetKeyCode() == KEY_ENTER )
{
fParent->fCurrAgeExpanded = !fParent->fCurrAgeExpanded;
fParent->IUpdateDebugScreen( true );
return true;
}
else if( pKeyMsg->GetKeyCode() == KEY_ESCAPE )
{
plResMgrHelperMsg *msg = TRACKED_NEW plResMgrHelperMsg( plResMgrHelperMsg::kDisableDebugScreen );
msg->Send( fParent->GetKey() );
return true;
}
else if( pKeyMsg->GetKeyCode() == KEY_RIGHT )
{
if( !fParent->fCurrAgeExpanded )
fParent->fCurrAgeExpanded = true;
else
{
fParent->fDebugDisplayType++;
if( fParent->fDebugDisplayType == plResManagerHelper::kMaxDisplayType )
fParent->fDebugDisplayType = 0;
}
fParent->IUpdateDebugScreen( true );
return true;
}
else if( pKeyMsg->GetKeyCode() == KEY_LEFT )
{
fParent->fCurrAgeExpanded = false;
fParent->IUpdateDebugScreen( true );
return true;
}
}
return false;
}
virtual UInt32 GetCurrentCursorID( void ) const { return 0; }
virtual hsBool HasInterestingCursorID( void ) const { return false; }
};
#endif
//// EnableDebugScreen ///////////////////////////////////////////////////////
void plResManagerHelper::EnableDebugScreen( hsBool enable )
{
#ifdef MCN_RESMGR_DEBUGGING
if( enable )
{
if( fDebugScreen == nil )
{
fDebugScreen = plStatusLogMgr::GetInstance().CreateStatusLog( kLogSize, "ResManager Status", plStatusLog::kFilledBackground | plStatusLog::kDontWriteFile );
fRefreshing = true;
plResMgrHelperMsg *msg = TRACKED_NEW plResMgrHelperMsg( plResMgrHelperMsg::kUpdateDebugScreen );
// msg->SetTimeStamp( hsTimer::GetSysSeconds() + kUpdateDelay );
msg->Send( GetKey() );
fDebugInput = TRACKED_NEW plResMgrDebugInterface( this );
plInputIfaceMgrMsg *imsg = TRACKED_NEW plInputIfaceMgrMsg( plInputIfaceMgrMsg::kAddInterface );
imsg->SetIFace( fDebugInput );
imsg->Send();
}
}
else
{
fRefreshing = false;
if( fDebugScreen != nil )
{
delete fDebugScreen;
fDebugScreen = nil;
plInputIfaceMgrMsg *imsg = TRACKED_NEW plInputIfaceMgrMsg( plInputIfaceMgrMsg::kRemoveInterface );
imsg->SetIFace( fDebugInput );
imsg->Send();
hsRefCnt_SafeUnRef( fDebugInput );
fDebugInput = nil;
}
}
#endif
}
//// IUpdateDebugScreen /////////////////////////////////////////////////////
#ifdef MCN_RESMGR_DEBUGGING
class plDebugPrintIterator : public plRegistryPageIterator, plRegistryKeyIterator
{
public:
plStatusLog *fLog;
UInt8 fStep, fLines;
UInt32 &fLoadedCount, &fHoldingCount, fPageCount, fAgeIndex;
char fCurrAge[ 128 ];
UInt32 fLoadedKeys, fTotalKeys, fTotalSize, fLoadedSize;
plResManagerHelper *fParent;
plDebugPrintIterator( plResManagerHelper *parent, plStatusLog *log, UInt32 &loadedCount, UInt32 &holdingCount )
: fParent( parent ), fLog( log ), fStep( 0 ), fLines( 0 ), fLoadedCount( loadedCount ), fHoldingCount( holdingCount )
{
fLoadedCount = fHoldingCount = 0;
fCurrAge[ 0 ] = 0;
fPageCount = 0;
fAgeIndex = 0;
}
virtual hsBool EatPage( plRegistryPageNode *page )
{
if( fStep == 0 )
{
fLog->AddLineF( 0xff80ff80, "Loaded Pages" );
fStep = 1;
fLines++;
}
else if( fStep == 1 && page != nil && !page->IsLoaded() )
{
fStep = 2;
fLog->AddLineF( 0xff80ff80, "Holding Pages" );
fLines++;
}
if( page != nil && page->IsLoaded() )
fLoadedCount++;
else if( page != nil )
fHoldingCount++;
// Changed ages?
if( page == nil || strcmp( fCurrAge, page->GetPageInfo().GetAge() ) != 0 )
{
// Print some info for the last age we were on
if( fCurrAge[ 0 ] != 0 )
{
if( fParent->fCurrAge != fAgeIndex || !fParent->fCurrAgeExpanded )
{
if( fLines < kLogSize - 4 )
{
UInt32 color = plStatusLog::kWhite;
if( fParent->fCurrAge == fAgeIndex )
color = plStatusLog::kYellow;
fLog->AddLineF( color, " %s (%d pages)", fCurrAge, fPageCount );
fLines++;
}
else if( fLines == kLogSize - 4 )
{
fLog->AddLineF( plStatusLog::kWhite, " ..." );
fLines++;
}
}
fAgeIndex++;
}
fPageCount = 0;
if( page != nil )
strncpy( fCurrAge, page->GetPageInfo().GetAge(), sizeof( fCurrAge ) - 1 );
else
fCurrAge[ 0 ] = 0;
if( fParent->fCurrAge == fAgeIndex && fParent->fCurrAgeExpanded )
{
// Print header now, since we won't be printing a footer
if( fLines < kLogSize - 4 )
{
fLog->AddLineF( plStatusLog::kYellow, " %s>", fCurrAge );
fLines++;
}
else if( fLines == kLogSize - 4 )
{
fLog->AddLineF( plStatusLog::kWhite, " ..." );
fLines++;
}
}
}
fPageCount++;
if( fParent->fCurrAge == fAgeIndex && fParent->fCurrAgeExpanded && page != nil )
{
// Count keys for this page
fTotalKeys = fLoadedKeys = fTotalSize = fLoadedSize = 0;
page->IterateKeys( this );
// Print page for this expanded age view
if( fLines < kLogSize - 4 )
{
if( fParent->fDebugDisplayType == plResManagerHelper::kSizes )
fLog->AddLineF( plStatusLog::kWhite, " %s (%d keys @ %4.1fk, %d loaded @ %4.1fk)", page->GetPageInfo().GetPage(), fTotalKeys, fTotalSize / 1024.f, fLoadedKeys, fLoadedSize / 1024.f );
else if( fParent->fDebugDisplayType == plResManagerHelper::kPercents )
fLog->AddLineF( plStatusLog::kWhite, " %s (%d%% loaded of %d keys @ %4.1fk)", page->GetPageInfo().GetPage(), fLoadedSize * 100 / ( fTotalSize > 0 ? fTotalSize : -1 ), fTotalKeys, fTotalSize / 1024.f );
else //if( fParent->fDebugDisplayType == plResManagerHelper::kBars )
{
const int startPos = 20, length = 32;
char line[ 128 ];
memset( line, ' ', sizeof( line ) - 1 );
line[ 127 ] = 0;
if( strlen( page->GetPageInfo().GetPage() ) < startPos - 2 )
memcpy( line + 2, page->GetPageInfo().GetPage(), strlen( page->GetPageInfo().GetPage() ) );
else
memcpy( line + 2, page->GetPageInfo().GetPage(), startPos - 2 );
line[ startPos ] = '|';
if( fTotalSize == 0 )
{
line[ startPos + 1 ] = '|';
line[ startPos + 2 ] = 0;
}
else
{
char temp[ 12 ];
sprintf( temp, "%d%%", fLoadedSize * 100 / fTotalSize );
line[ startPos + length + 1 ] = '|';
int i, sum = 0;
for( i = startPos + 1; i < startPos + length + 1 && sum < fLoadedSize; i++ )
{
line[ i ] = '=';
sum += fTotalSize / length;
}
line[ startPos + length + 2 ] = 0;
memcpy( line + startPos + 1, temp, strlen( temp ) );
}
fLog->AddLine( line, plStatusLog::kWhite );
}
fLines++;
}
else if( fLines == kLogSize - 4 )
{
fLog->AddLineF( plStatusLog::kWhite, " ..." );
fLines++;
}
}
return true;
}
virtual hsBool EatKey( const plKey& key )
{
if( key->ObjectIsLoaded() )
{
fLoadedKeys++;
fLoadedSize += ((plKeyImp *)key)->GetDataLen();
}
fTotalKeys++;
fTotalSize += ((plKeyImp *)key)->GetDataLen();
return true;
}
};
#endif
void plResManagerHelper::IUpdateDebugScreen( hsBool force )
{
#ifdef MCN_RESMGR_DEBUGGING
if( !fRefreshing )
return;
plRegistry *reg = fResManager->IGetRegistry();
UInt32 loadedCnt, holdingCnt;
fDebugScreen->Clear();
plDebugPrintIterator iter( this, fDebugScreen, loadedCnt, holdingCnt );
reg->IterateAllPages( &iter );
iter.EatPage( nil ); // Force a final update
fDebugScreen->AddLineF( plStatusLog::kGreen, "%d pages loaded, %d holding", loadedCnt, holdingCnt );
if( fCurrAge >= iter.fAgeIndex )
fCurrAge = -1;
// Repump our update
if( !force )
{
plResMgrHelperMsg *msg = TRACKED_NEW plResMgrHelperMsg( plResMgrHelperMsg::kUpdateDebugScreen );
msg->SetTimeStamp( hsTimer::GetSysSeconds() + kUpdateDelay );
msg->Send( GetKey() );
}
#endif
}
#if 0
// FIXME
hsBool VerifyKeyUnloaded(const char* logFile, const plKey& key);
// Verifies that a key which shouldn't be loaded isn't, and if it is tries to figure out why.
void VerifyAgeUnloaded(const char* logFile, const char* age);
// Helper for VerifyKeyUnloaded
hsBool IVerifyKeyUnloadedRecur(const char* logFile, const plKey& baseKey, const plKey& upKey, const char* baseAge);
bool ILookForCyclesRecur(const char* logFile, const plKey& key, hsTArray<plKey>& tree, int& cycleStart);
bool plResManager::ILookForCyclesRecur(const char* logFile, const plKey& key, hsTArray<plKey>& tree, int& cycleStart)
{
int idx = tree.Find(key);
tree.Append(key);
if (tree.kMissingIndex != idx)
{
cycleStart = idx;
// Found a cycle.
return true;
}
// Now recurse up the active reference tree.
for (int i = 0; i < key->GetNumNotifyCreated(); i++)
{
if (key->GetActiveBits().IsBitSet(i))
{
for (int j = 0; j < key->GetNotifyCreated(i)->GetNumReceivers(); j++)
{
plKey reffer = key->GetNotifyCreated(i)->GetReceiver(j);
if (ILookForCyclesRecur(logFile, reffer, tree, cycleStart))
return true;
}
}
}
tree.Pop();
return false;
}
bool plResManager::IVerifyKeyUnloadedRecur(const char* logFile, const plKey& baseKey, const plKey& upKey, const char* baseAge)
{
const plPageInfo& pageInfo = FindPage(upKey->GetUoid().GetLocation())->GetPageInfo();
const char* upAge = pageInfo.GetAge();
const char* upPage = pageInfo.GetPage();
if( !upKey->GetActiveRefs() )
{
// We've hit a key active reffing us that should be inactive.
// If it's object is loaded, then it somehow missed getting unloaded.
// Else it must have missed letting go of us when it got unloaded.
if( upKey->ObjectIsLoaded() )
{
plStatusLog::AddLineS(logFile, "\tHeld by %s [%s] page %s which is loaded but nothing is reffing",
upKey->GetName(),
plFactory::GetNameOfClass(upKey->GetUoid().GetClassType()),
upPage);
return true;
}
else
{
plStatusLog::AddLineS(logFile, "\tHeld by %s [%s] page %s which isn't even loaded",
upKey->GetName(),
plFactory::GetNameOfClass(upKey->GetUoid().GetClassType()),
upPage);
return true;
}
}
// if the age of this key is different from the age on the baseKey,
// we've got a cross age active ref, which is illegal.
if( stricmp(upAge, baseAge) )
{
plStatusLog::AddLineS(logFile, "\tHeld by %s [%s] which is in a different age %s-%s",
upKey->GetName(),
plFactory::GetNameOfClass(upKey->GetUoid().GetClassType()),
upAge,
upPage);
return true;
}
int numActive = 0;
int i;
for( i = 0; i < upKey->GetNumNotifyCreated(); i++ )
{
if( upKey->GetActiveBits().IsBitSet(i) )
{
numActive++;
}
}
if( numActive < upKey->GetActiveRefs() )
{
// Someone has AddRef'd us
plStatusLog::AddLineS(logFile, "\tHeld by %s [%s] page %s which is loaded due to %d AddRef(s)",
upKey->GetName(),
plFactory::GetNameOfClass(upKey->GetUoid().GetClassType()),
upPage,
upKey->GetActiveRefs()-numActive);
return true;
}
// Now recurse up the active reference tree.
for( i = 0; i < upKey->GetNumNotifyCreated(); i++ )
{
if( upKey->GetActiveBits().IsBitSet(i) )
{
int j;
for( j = 0; j < upKey->GetNotifyCreated(i)->GetNumReceivers(); j++ )
{
plKey reffer = upKey->GetNotifyCreated(i)->GetReceiver(j);
if( IVerifyKeyUnloadedRecur(logFile, baseKey, reffer, baseAge) )
{
return true;
}
}
}
}
return false;
}
hsBool plResManager::VerifyKeyUnloaded(const char* logFile, const plKey& key)
{
if( key->ObjectIsLoaded() )
{
const plPageInfo& pageInfo = FindPage(key->GetUoid().GetLocation())->GetPageInfo();
const char* age = pageInfo.GetAge();
const char* page = pageInfo.GetPage();
plStatusLog::AddLineS(logFile, "==================================");
plStatusLog::AddLineS(logFile, "Object %s [%s] page %s is loaded", key->GetName(), plFactory::GetNameOfClass(key->GetUoid().GetClassType()), page);
hsTArray<plKey> tree;
int cycleStart;
hsBool hasCycle = ILookForCyclesRecur(logFile, key, tree, cycleStart);
if( hasCycle )
{
plStatusLog::AddLineS(logFile, "\t%s [%s] held by dependency cycle", key->GetName(), plFactory::GetNameOfClass(key->GetUoid().GetClassType()));
int i;
for( i = cycleStart; i < tree.GetCount(); i++ )
{
plStatusLog::AddLineS(logFile, "\t%s [%s]", tree[i]->GetName(), plFactory::GetNameOfClass(tree[i]->GetUoid().GetClassType()));
}
plStatusLog::AddLineS(logFile, "\tEnd Cycle");
return true;
}
else
{
return IVerifyKeyUnloadedRecur(logFile, key, key, age);
}
}
return false;
}
class plValidateKeyIterator : public plRegistryKeyIterator
{
protected:
plRegistry* fRegistry;
const char* fLogFile;
public:
plValidateKeyIterator(const char* logFile, plRegistry* reg)
{
fRegistry = reg;
fLogFile = logFile;
}
virtual hsBool EatKey(const plKey& key)
{
fRegistry->VerifyKeyUnloaded(fLogFile, key);
return true;
}
};
class plValidatePageIterator : public plRegistryPageIterator
{
protected:
const char* fAge;
plRegistryKeyIterator* fIter;
public:
plValidatePageIterator(const char* age, plRegistryKeyIterator* iter) : fAge(age), fIter(iter) {}
virtual hsBool EatPage( plRegistryPageNode *keyNode )
{
if( !stricmp(fAge, keyNode->GetPageInfo().GetAge()) )
return keyNode->IterateKeys( fIter );
return true;
}
};
void plResManager::VerifyAgeUnloaded(const char* logFile, const char* age)
{
hsBool autoLog = false;
char buff[256];
if( !logFile || !*logFile )
{
sprintf(buff, "%s.log", age);
logFile = buff;
autoLog = true;
}
if( !autoLog )
{
plStatusLog::AddLineS(logFile, "///////////////////////////////////");
plStatusLog::AddLineS(logFile, "Begin Verification of age %s", age);
}
plValidateKeyIterator keyIter(logFile, this);
plValidatePageIterator pageIter(age, &keyIter);
IterateAllPages(&pageIter);
if( !autoLog )
{
plStatusLog::AddLineS(logFile, "End Verification of age %s", age);
plStatusLog::AddLineS(logFile, "///////////////////////////////////");
}
}
#endif