/*==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 .
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==*/
//////////////////////////////////////////////////////////////////////////////
// //
// plPassMtlBase - Base class for all Plasma MAX materials //
// //
//////////////////////////////////////////////////////////////////////////////
#include "HeadSpin.h"
#include "hsBitVector.h"
#include "Max.h"
#include "iparamb2.h"
#include "notify.h"
#include "notetrck.h"
#include "plPassMtlBase.h"
#include "plPassBaseParamIDs.h"
#include "plNoteTrackWatcher.h"
#include "plAnimStealthNode.h"
#include "MaxComponent/plMaxAnimUtils.h"
#include "MaxMain/plPlasmaRefMsgs.h"
// For converting from a MAX Mtl
#include "plPassMtl.h"
#include "plBumpMtl.h"
#include "plDecalMtl.h"
using namespace plPassBaseParamIDs;
IMtlParams *plPassMtlBase::fIMtlParams = nil;
//// plPostLoadHandler ///////////////////////////////////////////////////////
// Small class to keep track of all the materials to update after load
class plPostLoadHandler
{
static bool fLoading;
static hsTArray fPostLoads;
public:
static bool IsLoading() { return fLoading; }
static void PostLoadFixupFunction( void *param, NotifyInfo *info )
{
fLoading = false;
for( int i = 0; i < fPostLoads.GetCount(); i++ )
fPostLoads[ i ]->PostLoadAnimPBFixup();
fPostLoads.Reset();
UnRegisterNotification( PostLoadFixupFunction, param, NOTIFY_FILE_POST_OPEN );
UnRegisterNotification( PostLoadFixupFunction, param, NOTIFY_FILE_POST_MERGE );
}
static void AddPostLoad( plPassMtlBase *mtl )
{
fLoading = true;
if( fPostLoads.GetCount() == 0 )
{
RegisterNotification( PostLoadFixupFunction, mtl, NOTIFY_FILE_POST_OPEN );
RegisterNotification( PostLoadFixupFunction, mtl, NOTIFY_FILE_POST_MERGE );
}
mtl->SetLoadingFlag( true );
fPostLoads.Append( mtl );
}
static void RemovePostLoad( plPassMtlBase *mtl )
{
for( int i = 0; i < fPostLoads.GetCount(); i++ )
{
if( fPostLoads[ i ] == mtl )
{
fPostLoads.Remove( i );
break;
}
}
}
};
hsTArray plPostLoadHandler::fPostLoads;
bool plPostLoadHandler::fLoading = false;
plPassMtlBase::plPassMtlBase( BOOL loading ) : fNTWatcher( nil ), fBasicPB(NULL), fAdvPB(NULL), fLayersPB(NULL), fAnimPB(NULL),
fLoading( loading )
{
fNTWatcher = new plNoteTrackWatcher( this );
Reset();
}
plPassMtlBase::~plPassMtlBase()
{
if( fLoading )
plPostLoadHandler::RemovePostLoad( this );
// Force the watcher's parent pointer to nil, otherwise the de-ref will attempt to re-delete us
fNTWatcher->SetReference( plNoteTrackWatcher::kRefParentMtl, nil );
delete fNTWatcher;
fNTWatcher = nil;
// Manually delete our notetrack refs, otherwise there'll be hell to pay
for( int i = 0; i < fNotetracks.GetCount(); i++ )
{
if( fNotetracks[ i ] != nil )
DeleteReference( kRefNotetracks + i );
}
}
void plPassMtlBase::Reset( void )
{
fIValid.SetEmpty();
}
//// Stealth Accessors ///////////////////////////////////////////////////////
int plPassMtlBase::GetNumStealths( void )
{
return IGetNumStealths( true );
}
plAnimStealthNode *plPassMtlBase::GetStealth( int index )
{
return IGetStealth( index, false );
}
int plPassMtlBase::IGetNumStealths( bool update )
{
if( update )
IUpdateAnimNodes();
return fAnimPB->Count( (ParamID)kPBAnimStealthNodes );
}
plAnimStealthNode *plPassMtlBase::IGetStealth( int index, bool update )
{
if( update )
IUpdateAnimNodes();
return (plAnimStealthNode *)fAnimPB->GetReferenceTarget( (ParamID)kPBAnimStealthNodes, 0, index );
}
plAnimStealthNode *plPassMtlBase::IFindStealth( const plString &segmentName )
{
int i;
for( i = 0; i < fAnimPB->Count( (ParamID)kPBAnimStealthNodes ); i++ )
{
plAnimStealthNode *node = (plAnimStealthNode *)fAnimPB->GetReferenceTarget( (ParamID)kPBAnimStealthNodes, 0, i );
plString name = node->GetSegmentName();
if( node != nil && name.Compare( segmentName ) == 0 )
{
return node;
}
}
return nil;
}
//// IVerifyStealthPresent ///////////////////////////////////////////////////
// Ensures that we have a stealth for the given segment.
plAnimStealthNode *plPassMtlBase::IVerifyStealthPresent( const plString &animName )
{
// If we're in the middle of loading, don't check
if (plPostLoadHandler::IsLoading())
return nil;
plAnimStealthNode *stealth = IFindStealth( animName );
if( stealth == nil )
{
// New segment, add a new stealth node
stealth = (plAnimStealthNode *)GetCOREInterface()->CreateInstance( HELPER_CLASS_ID, ANIMSTEALTH_CLASSID );
INode *node = GetCOREInterface()->CreateObjectNode( stealth );
stealth->SetSegment( ( animName.Compare(ENTIRE_ANIMATION_NAME) != 0 ) ? _TEMP_CONVERT_TO_CONST_CHAR(animName) : nil );
stealth->SetNodeName( GetName() );
node->Freeze( true );
// Skip the attach, since we might not find a valid INode. This will leave the node attached to the scene
// root, which is fine. Since we just care about it being SOMEWHERE in the scene hierarchy
/*
if( fAnimPB->Count( (ParamID)kPBAnimStealthNodes ) > 0 )
{
plAnimStealthNode *first = (plAnimStealthNode *)fAnimPB->GetReferenceTarget( (ParamID)kPBAnimStealthNodes, 0, 0 );
first->GetINode()->AttachChild( node );
}
*/
fAnimPB->Append( (ParamID)kPBAnimStealthNodes, 1, (ReferenceTarget **)&stealth );
plString realName = stealth->GetSegmentName();
fStealthsChanged = true;
}
else
{
// Exists already, we're ok
stealth->SetParentMtl( this );
}
return stealth;
}
//// Change Callbacks ////////////////////////////////////////////////////////
void plPassMtlBase::RegisterChangeCallback( plMtlChangeCallback *callback )
{
if( fChangeCallbacks.Find( callback ) == fChangeCallbacks.kMissingIndex )
fChangeCallbacks.Append( callback );
}
void plPassMtlBase::UnregisterChangeCallback( plMtlChangeCallback *callback )
{
int idx = fChangeCallbacks.Find( callback );
if( idx != fChangeCallbacks.kMissingIndex )
fChangeCallbacks.Remove( idx );
}
//// IUpdateAnimNodes ////////////////////////////////////////////////////////
// Updates the list of stealth nodes in the anim paramBlock to match our
// list of anim segments.
void plPassMtlBase::IUpdateAnimNodes( void )
{
// Our beautiful hack, to make sure we don't update until we actually are loaded
if( fLoading )
return;
SegmentMap *segMap = GetAnimSegmentMap( this, nil );
hsTArray goodNodes;
// Keep track of whether we change anything
fStealthsChanged = false;
// Verify one for "entire animation"
plAnimStealthNode *stealth = IVerifyStealthPresent( _TEMP_CONVERT_FROM_LITERAL(ENTIRE_ANIMATION_NAME) );
goodNodes.Append( stealth );
// Verify segment nodes
if( segMap != nil )
{
for( SegmentMap::iterator i = segMap->begin(); i != segMap->end(); i++ )
{
SegmentSpec *spec = (*i).second;
if( spec->fType == SegmentSpec::kAnim )
{
plAnimStealthNode *stealth = IVerifyStealthPresent( spec->fName );
goodNodes.Append( stealth );
}
}
DeleteSegmentMap( segMap );
}
// Remove nodes that no longer have segments
int idx;
for( idx = 0; idx < fAnimPB->Count( (ParamID)kPBAnimStealthNodes ); )
{
plAnimStealthNode *node = (plAnimStealthNode *)fAnimPB->GetReferenceTarget( (ParamID)kPBAnimStealthNodes, 0, idx );
if( node != nil && goodNodes.Find( node ) == goodNodes.kMissingIndex )
{
fAnimPB->Delete( (ParamID)kPBAnimStealthNodes, idx, 1 );
// GetCOREInterface()->DeleteNode( node->GetINode() );
fStealthsChanged = true;
}
else
idx++;
}
if( fStealthsChanged )
{
// Yup, our list of stealths got updated. Notify everyone of such.
for( idx = 0; idx < fChangeCallbacks.GetCount(); idx++ )
fChangeCallbacks[ idx ]->SegmentListChanged();
}
}
//// NameChanged /////////////////////////////////////////////////////////////
// Notify from NTWatcher so we can update the names of our stealth nodes
void plPassMtlBase::NameChanged( void )
{
for( int idx = 0; idx < fAnimPB->Count( (ParamID)kPBAnimStealthNodes ); idx++ )
{
plAnimStealthNode *node = (plAnimStealthNode *)fAnimPB->GetReferenceTarget( (ParamID)kPBAnimStealthNodes, 0, idx );
if( node != nil )
node->SetNodeName( GetName() );
}
}
//// NoteTrackAdded/Removed //////////////////////////////////////////////////
// Notifies from NTWatcher so we can update our list of stealths
void plPassMtlBase::NoteTrackAdded( void )
{
int i;
// Make a ref to our new notetrack
for( i = 0; i < NumNoteTracks(); i++ )
{
NoteTrack *track = GetNoteTrack( i );
if( fNotetracks.Find( track ) == fNotetracks.kMissingIndex )
{
ReplaceReference(kRefNotetracks + fNotetracks.GetCount(), track);
break;
}
}
for( i = 0; i < fChangeCallbacks.GetCount(); i++ )
fChangeCallbacks[ i ]->NoteTrackListChanged();
IUpdateAnimNodes();
}
void plPassMtlBase::NoteTrackRemoved( void )
{
int i;
hsBitVector stillThere;
// Make a ref to our new notetrack
for( i = 0; i < NumNoteTracks(); i++ )
{
NoteTrack *track = GetNoteTrack( i );
int idx = fNotetracks.Find( track );
if( idx != fNotetracks.kMissingIndex )
stillThere.Set( idx );
}
for( i = 0; i < fNotetracks.GetCount(); i++ )
{
if( !stillThere.IsBitSet( i ) && fNotetracks[ i ] != nil )
{
// DeleteReference( kRefNotetracks + i );
SetReference( kRefNotetracks + i, nil );
}
}
for( i = 0; i < fChangeCallbacks.GetCount(); i++ )
fChangeCallbacks[ i ]->NoteTrackListChanged();
IUpdateAnimNodes();
}
//////////////////////////////////////////////////////////////////////////////
//// MAX Ref Stuff ///////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////
//// NumRefs /////////////////////////////////////////////////////////////////
int plPassMtlBase::NumRefs()
{
return 4 + fNotetracks.GetCount();
}
//// GetReference ////////////////////////////////////////////////////////////
RefTargetHandle plPassMtlBase::GetReference( int i )
{
if( i >= kRefNotetracks && i < kRefNotetracks + fNotetracks.GetCount() )
return fNotetracks[ i - kRefNotetracks ];
else
hsAssert(false, "shit");
return nil;
}
//// SetReference ////////////////////////////////////////////////////////////
void plPassMtlBase::SetReference(int i, RefTargetHandle rtarg)
{
if( i >= kRefNotetracks )
{
fNotetracks.ExpandAndZero(i - kRefNotetracks + 1);
fNotetracks[i - kRefNotetracks] = (NoteTrack*)rtarg;
}
}
//// NotifyRefChanged ////////////////////////////////////////////////////////
RefResult plPassMtlBase::NotifyRefChanged( Interval changeInt, RefTargetHandle hTarget,
PartID &partID, RefMessage message )
{
switch( message )
{
case REFMSG_CHANGE:
fIValid.SetEmpty();
// see if this message came from a changing parameter in the pblock,
// if so, limit rollout update to the changing item
if (hTarget == fBasicPB || hTarget == fAdvPB || hTarget == fLayersPB || hTarget == fAnimPB)
{
IParamBlock2 *pb = (IParamBlock2*)hTarget;
ParamID changingParam = pb->LastNotifyParamID();
pb->GetDesc()->InvalidateUI( changingParam );
// And let the SceneWatcher know that the material on some of it's
// referenced objects changed.
NotifyDependents( FOREVER, PART_ALL, REFMSG_USER_MAT );
}
else
{
// Was it a notetrack ref?
if( fNotetracks.Find( (NoteTrack *)hTarget ) != fNotetracks.kMissingIndex )
{
// Yup, so update our notetrack list
IUpdateAnimNodes();
}
}
break;
case REFMSG_TARGET_DELETED:
NoteTrackRemoved();
break;
}
return REF_SUCCEED;
}
//////////////////////////////////////////////////////////////////////////////
//// Standard IO (or non-standard, as the case may be) ///////////////////////
//////////////////////////////////////////////////////////////////////////////
//// PostLoadAnimPBFixup /////////////////////////////////////////////////////
// Takes the old version of the anim paramblock and translates it into the
// new version.
// Note that there's an interesting (?) side effect of this: for new materials,
// we'll incorrectly detect them as the "old" format and fix-up them as well.
// This means that we'll end up with the same defaults for (Entire Animation)
// that we had for the old materials. We can easily change the defaults by
// changing the defaults for the old paramblock though.
// Also, we go ahead and re-work the stealth parent pointers here, since it
// appears to be the ONLY time we can do it and have it reliably work. ARRGH!
//
// History of the options that we CAN'T do and why (and hence why this):
// - ParamBlock accessor doesn't work. For some reason, the accessor isn't
// always called on load, even with P_CALLSETS_ON_LOAD specified
// - Doing it on Load() doesn't work, because neither the ParamBlocks are
// guaranteed to be fully loaded (with their tabs filled) nor are the
// notetracks necessarily attached yet
// - Notetracks can also possibly be attached BEFORE load, which doesn't
// do us a damned bit of good, so we need to make sure we're fully
// loaded before we run this function. Unfortunately, the only time
// we're guaranteed THAT is by a FILE_POST_OPEN notify. (post-load
// callbacks don't work because they're called right after our object
// is loaded but not necessarily before the notetracks are attached)
void plPassMtlBase::PostLoadAnimPBFixup( void )
{
SetLoadingFlag( false );
#ifdef MCN_UPGRADE_OLD_ANIM_BLOCKS
if( fAnimPB->Count( (ParamID)kPBAnimStealthNodes ) == 0 )
{
// Yup, old style. So our update process looks like this:
// 1) Create stealths for all our segments as we are now
// 2) Set the parameters on all of them to our old defaults (no autostart,
// loop on entire, no ease).
// 3) Copy the old paramblock values to the single stealth indicated by
// the old PB
// Step 1...
IUpdateAnimNodes();
// Step 2...
for( int i = 0; i < fAnimPB->Count( (ParamID)kPBAnimStealthNodes ); i++ )
{
plAnimStealthNode *node = (plAnimStealthNode *)fAnimPB->GetReferenceTarget( (ParamID)kPBAnimStealthNodes, 0, i );
plString name = node->GetSegmentName();
node->SetAutoStart( false );
node->SetLoop( true, _TEMP_CONVERT_FROM_LITERAL( ENTIRE_ANIMATION_NAME ) );
node->SetEaseIn( plAnimEaseTypes::kNoEase, 1.f, 1.f, 1.f );
node->SetEaseOut( plAnimEaseTypes::kNoEase, 1.f, 1.f, 1.f );
}
// Step 3...
const char *oldSel = (const char *)fAnimPB->GetStr( (ParamID)kPBAnimName );
if( oldSel == nil )
oldSel = ENTIRE_ANIMATION_NAME;
plAnimStealthNode *myNew = IFindStealth( plString::FromUtf8( oldSel ) );
if( myNew != nil )
{
#pragma warning( push ) // Forcing value to bool true or false (go figure, i'm even explicitly casting)
#pragma warning( disable:4800 ) // Forcing value to bool true or false (go figure, i'm even explicitly casting)
myNew->SetAutoStart( (bool)fAnimPB->GetInt( (ParamID)kPBAnimAutoStart ) );
myNew->SetLoop( (bool)fAnimPB->GetInt( (ParamID)kPBAnimLoop ),
plString::FromUtf8( (char *)fAnimPB->GetStr( (ParamID)kPBAnimLoopName ) ) );
myNew->SetEaseIn( (uint8_t)fAnimPB->GetInt( (ParamID)kPBAnimEaseInType ),
(float)fAnimPB->GetFloat( (ParamID)kPBAnimEaseInLength ),
(float)fAnimPB->GetFloat( (ParamID)kPBAnimEaseInMin ),
(float)fAnimPB->GetFloat( (ParamID)kPBAnimEaseInMax ) );
myNew->SetEaseOut( (uint8_t)fAnimPB->GetInt( (ParamID)kPBAnimEaseOutType ),
(float)fAnimPB->GetFloat( (ParamID)kPBAnimEaseOutLength ),
(float)fAnimPB->GetFloat( (ParamID)kPBAnimEaseOutMin ),
(float)fAnimPB->GetFloat( (ParamID)kPBAnimEaseOutMax ) );
#pragma warning( pop )
}
}
#endif // MCN_UPGRADE_OLD_ANIM_BLOCKS
// Make sure the parent is set tho. Note: we have to do this because, for some *(#$&(* reason,
// when we're loading a file, MAX can somehow add the stealths to our tab list WITHOUT calling
// the accessor for it (and the tab is empty on the CallSetsOnLoad() pass, for some reason).
for( int i = 0; i < fAnimPB->Count( (ParamID)kPBAnimStealthNodes ); i++ )
{
plAnimStealthNode *node = (plAnimStealthNode *)fAnimPB->GetReferenceTarget( (ParamID)kPBAnimStealthNodes, 0, i );
node->SetParentMtl( this );
}
}
#define MTL_HDR_CHUNK 0x4000
//// Load ////////////////////////////////////////////////////////////////////
// Our actual MAX load function
IOResult plPassMtlBase::Load(ILoad *iload)
{
plPostLoadHandler::AddPostLoad( this );
IOResult res;
int id;
while (IO_OK==(res=iload->OpenChunk()))
{
switch(id = iload->CurChunkID())
{
case MTL_HDR_CHUNK:
res = MtlBase::Load(iload);
break;
}
iload->CloseChunk();
if (res!=IO_OK)
return res;
}
return IO_OK;
}
//// Save ////////////////////////////////////////////////////////////////////
// The MAX flip-side
IOResult plPassMtlBase::Save(ISave *isave)
{
IOResult res;
isave->BeginChunk(MTL_HDR_CHUNK);
res = MtlBase::Save(isave);
if (res!=IO_OK) return res;
isave->EndChunk();
return IO_OK;
}
//// ICloneBase //////////////////////////////////////////////////////////////
void plPassMtlBase::ICloneBase( plPassMtlBase *target, RemapDir &remap )
{
*((MtlBase*)target) = *((MtlBase*)this);
ICloneRefs( target, remap );
for( int idx = 0; idx < fAnimPB->Count( (ParamID)kPBAnimStealthNodes ); idx++ )
{
IParamBlock2 *pb = target->fAnimPB;
plAnimStealthNode *stealth = (plAnimStealthNode *)fAnimPB->GetReferenceTarget( (ParamID)kPBAnimStealthNodes, 0, idx );
pb->SetValue( (ParamID)kPBAnimStealthNodes, 0, remap.CloneRef( stealth ), idx );
stealth = (plAnimStealthNode *)pb->GetReferenceTarget( (ParamID)kPBAnimStealthNodes, 0, idx );
INode *node = GetCOREInterface()->CreateObjectNode( stealth );
stealth->SetNodeName( GetName() );
node->Freeze( true );
}
BaseClone(this, target, remap);
target->fIValid.SetEmpty();
}
//// ConvertToPassMtl ////////////////////////////////////////////////////////
// Static convert to our plPassMtlBase type, if possible
plPassMtlBase *plPassMtlBase::ConvertToPassMtl( Mtl *mtl )
{
if( mtl == nil )
return nil;
if( mtl->ClassID() == PASS_MTL_CLASS_ID
|| mtl->ClassID() == BUMP_MTL_CLASS_ID
|| mtl->ClassID() == DECAL_MTL_CLASS_ID )
{
return (plPassMtlBase *)mtl;
}
return nil;
}
//// SetupProperties /////////////////////////////////////////////////////////
bool plPassMtlBase::SetupProperties( plMaxNode *node, plErrorMsg *pErrMsg )
{
bool ret = true;
// Call SetupProperties on all our animStealths if we have any
int i, count = IGetNumStealths();
for( i = 0; i < count; i++ )
{
if( !IGetStealth( i, false )->SetupProperties( node, pErrMsg ) )
ret = false;
}
return ret;
}
//// ConvertDeInit ///////////////////////////////////////////////////////////
bool plPassMtlBase::ConvertDeInit( plMaxNode *node, plErrorMsg *pErrMsg )
{
bool ret = true;
// Call ConvertDeInit on all our animStealths if we have any
int i, count = IGetNumStealths();
for( i = 0; i < count; i++ )
{
if( !IGetStealth( i, false )->ConvertDeInit( node, pErrMsg ) )
ret = false;
}
return ret;
}