/*==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/>.

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==*/
#include "plCreatableIndex.h"
#include "hsTimer.h"
#include "hsStream.h"
#include "plNetClientMgr.h"
#include "plgDispatch.h"
#include "plPhysical.h"
#include "plNetClientMsgHandler.h"
#include "plNetLinkingMgr.h"
#include "plNetObjectDebugger.h"

#include "pnUtils/pnUtils.h"
#include "pnProduct/pnProduct.h"
#include "pnNetCommon/plSynchedObject.h"
#include "pnNetCommon/plNetServers.h"
#include "pnNetCommon/plSDLTypes.h"
#include "pnKeyedObject/plKey.h"
#include "pnKeyedObject/plFixedKey.h"
#include "pnKeyedObject/hsKeyedObject.h"
#include "pnSceneObject/plSceneObject.h"
#include "pnSceneObject/plObjInterface.h"
#include "pnSceneObject/plCoordinateInterface.h"
#include "pnMessage/plClientMsg.h"
#include "pnMessage/plSDLModifierMsg.h"
#include "pnMessage/plPlayerPageMsg.h"
#include "pnMessage/plTimeMsg.h"
#include "pnModifier/plModifier.h"
#include "pnAsyncCore/pnAsyncCore.h"

#include "plAgeLoader/plAgeLoader.h"
#include "plAgeLoader/plResPatcher.h"
#include "plNetClientRecorder/plNetClientRecorder.h"
#include "plScene/plSceneNode.h"
#include "plNetCommon/plNetCommonConstants.h"
#include "plNetMessage/plNetMessage.h"
#include "plMessage/plLoadAvatarMsg.h"
#include "plMessage/plLoadCloneMsg.h"
#include "plMessage/plSynchEnableMsg.h"
#include "plMessage/plLinkToAgeMsg.h"
#include "plMessage/plLoadAgeMsg.h"
#include "plMessage/plAgeLoadedMsg.h"
#include "plMessage/plCCRMsg.h"
#include "plMessage/plAvatarMsg.h"
#include "plMessage/plNetVoiceListMsg.h"
#include "plMessage/plNetCommMsgs.h"
#include "plMessage/plNetClientMgrMsg.h"
#include "plResMgr/plKeyFinder.h"
#include "plResMgr/plPageInfo.h"
#include "plNetTransport/plNetTransportMember.h"
#include "plAgeDescription/plAgeDescription.h"
#include "plAvatar/plAvatarClothing.h"
#include "plAvatar/plArmatureMod.h"
#include "plAvatar/plAvatarMgr.h"
#include "plSurface/plLayerInterface.h"
#include "plStatusLog/plStatusLog.h"
#include "plSDL/plSDL.h"
#include "plUnifiedTime/plUnifiedTime.h"
#include "plFile/plEncryptedStream.h"
#include "plProgressMgr/plProgressMgr.h"
#include "plVault/plVault.h"

#include "pfMessage/pfKIMsg.h"	// Move this to PubUtil level

#if 1	// for debugging
#include "plCreatableIndex.h"	
#include "plModifier/plResponderModifier.h"
#include "plSurface/plLayerAnimation.h"
#endif

#include <algorithm>
#include <sstream>



////////////////////////////////////////////////////////////////////

plNetClientMgr::PendingLoad::~PendingLoad()
{
	delete fSDRec;
}

////////////////////////////////////////////////////////////////////

//
// CONSTRUCT
//
#pragma warning(disable:4355)	// this used in initializer list
plNetClientMgr::plNetClientMgr() : 
		fLocalPlayerKey(nil),
		fMsgHandler(this),
		fJoinOrder(0),
		// fProgressBar( nil ),
		fTaskProgBar( nil ),
		fMsgRecorder(nil),
		fServerTimeOffset(0),
		fTimeSamples(0),
		fLastTimeUpdate(0),
		fListenListMode(kListenList_Distance),
		fAgeSDLObjectKey(nil),
		fExperimentalLevel(0),
		fOverrideAgeTimeOfDayPercent(-1.f),
		fNumInitialSDLStates(0),
		fRequiredNumInitialSDLStates(0),
		fDisableMsg(nil),
		fIsOwner(true)
{	
#ifndef HS_DEBUGGING
	// release code will timeout inactive players on servers by default
	SetFlagsBit(kAllowTimeOut);
#endif
	SetFlagsBit(kAllowAuthTimeOut);

//	fPlayerVault.SetPlayerName("SinglePlayer"); // in a MP game, this will be replaced with a player name like 'Atrus'
	fTransport.SetNumChannels(kNetNumChannels);	
}
#pragma warning(default:4355)

//
// DESTRUCT
//
plNetClientMgr::~plNetClientMgr()
{
	plSynchedObject::PushSynchDisabled(true);		// disable dirty tracking

	IDeInitNetClientComm();	

	if (this==GetInstance())
		SetInstance(nil);		// we're going down boys
	
	IClearPendingLoads();
}

//
// App is exiting
//
void plNetClientMgr::Shutdown()
{
	ISendDirtyState(hsTimer::GetSysSeconds());

	plNetLinkingMgr::GetInstance()->LeaveAge(true);

	// release existing remote players
	int i;
	for (i=0;i<RemotePlayerKeys().size();i++)
	{
		plKey k=RemotePlayerKeys()[i];
		plAvatarMgr::GetInstance()->UnLoadRemotePlayer(k);
	}

	// Finally, pump the dispatch system so all the new refs get delivered.
	plgDispatch::Dispatch()->MsgQueueProcess();

	if (fMsgRecorder)
	{
		delete fMsgRecorder;
		fMsgRecorder = nil;
	}
	for (i = 0; i < fMsgPlayers.size(); i++)
		delete fMsgPlayers[i];
	fMsgPlayers.clear();

	IRemoveCloneRoom();

	// RATHER BAD DEBUG HACK: Clear the spawn override in armatureMod so there's no memory leak
	plArmatureMod::SetSpawnPointOverride( nil );

	VaultDestroy();
}

//
// Clear any pending state, probably joining a new age
//
void plNetClientMgr::IClearPendingLoads()
{
	std::for_each( fPendingLoads.begin(), fPendingLoads.end(), xtl::delete_ptr() );
	fPendingLoads.clear();
}

//
// manually add/remove the cloned object room in plClient
//
void plNetClientMgr::IAddCloneRoom()
{
	plSceneNode *cloneRoom = TRACKED_NEW plSceneNode();
	cloneRoom->RegisterAs(kNetClientCloneRoom_KEY);
	plKey clientKey=hsgResMgr::ResMgr()->FindKey(kClient_KEY);
	plClientRefMsg *pMsg1 = TRACKED_NEW plClientRefMsg(clientKey, plRefMsg::kOnCreate, -1, plClientRefMsg::kManualRoom);
	hsgResMgr::ResMgr()->AddViaNotify(cloneRoom->GetKey(), pMsg1, plRefFlags::kPassiveRef);
}

//
// manually add/remove the cloned object room in plClient
//
void plNetClientMgr::IRemoveCloneRoom()
{
	plKey cloneRoomKey = hsgResMgr::ResMgr()->FindKey(kNetClientCloneRoom_KEY);
	plSceneNode *cloneRoom = cloneRoomKey ? plSceneNode::ConvertNoRef(cloneRoomKey->ObjectIsLoaded()) : nil;
	if (cloneRoom)
	{
		cloneRoom->UnRegisterAs(kNetClientCloneRoom_KEY);
		cloneRoom = nil;
	}
}		

//
// turn null send on/off.  Null send does everything except actually send the msg out on the socket
//
void plNetClientMgr::SetNullSend(hsBool on)	
{
}

//
// returns server time in the form "[m/d/y h:m:s]"
//
const char* plNetClientMgr::GetServerLogTimeAsString(std::string& timestamp) const
{
	const plUnifiedTime st=GetServerTime();
	xtl::format(timestamp, "{%02d/%02d %02d:%02d:%02d}", 
		st.GetMonth(), st.GetDay(), st.GetHour(), st.GetMinute(), st.GetSecond());
	return timestamp.c_str();
}

//
// support for a single leading tab in statusLog strings
//
const char* ProcessTab(const char* fmt)
{
	static std::string s;
	if (fmt && *fmt=='\t')
	{
		s = xtl::format("  %s", fmt);
		return s.c_str();
	}
	return fmt;
}

//
// override for plLoggable
//
bool plNetClientMgr::Log(const char* str) const
{
	if (strlen(str)==nil)
		return true;

	// prepend raw time
	std::string buf2 = xtl::format("%.2f %s", hsTimer::GetSeconds(), ProcessTab(str));

	if ( GetConsoleOutput() )
		hsStatusMessage(buf2.c_str());

	// create status log if necessary
	if(fStatusLog==nil)
	{
		fStatusLog = plStatusLogMgr::GetInstance().CreateStatusLog(40, "network.log",
			plStatusLog::kTimestamp | plStatusLog::kFilledBackground | plStatusLog::kAlignToTop | 
			plStatusLog::kServerTimestamp);
		fWeCreatedLog = true;
	}

	plNetObjectDebugger::GetInstance()->LogMsgIfMatch(buf2.c_str());
	return fStatusLog->AddLine(buf2.c_str());
}

//
// Display OS version info for log
//
void plNetClientMgr::IDumpOSVersionInfo() const
{
	DebugMsg("*** OS Info");
	char** versionStrs = DisplaySystemVersion();
	int i=0;
	while(versionStrs && versionStrs[i])
	{
		DebugMsg(versionStrs[i]);
		delete [] versionStrs[i];
		i++;
	}
	delete [] versionStrs;
}

//
// initialize net client. returns hsFail on err.
//
int plNetClientMgr::Init()
{
	int ret=hsOK;
	hsLogEntry( DebugMsg("*** plNetClientMgr::Init GMT:%s", plUnifiedTime::GetCurrentTime().Print()) );
	
	IDumpOSVersionInfo();
	
	if (GetFlagsBit(kNullSend))
		SetNullSend(true);

	VaultInitialize();

	RegisterAs( kNetClientMgr_KEY );
	IAddCloneRoom();

	fNetGroups.Reset();

	plgDispatch::Dispatch()->RegisterForExactType(plNetClientMgrMsg::Index(), GetKey());	
	plgDispatch::Dispatch()->RegisterForExactType(plAgeLoadedMsg::Index(), GetKey());	
	plgDispatch::Dispatch()->RegisterForExactType(plAgeLoaded2Msg::Index(), GetKey());	
	plgDispatch::Dispatch()->RegisterForExactType(plCCRPetitionMsg::Index(), GetKey());	
	plgDispatch::Dispatch()->RegisterForExactType(plPlayerPageMsg::Index(), GetKey());
	plgDispatch::Dispatch()->RegisterForExactType(plInitialAgeStateLoadedMsg::Index(), GetKey());
	plgDispatch::Dispatch()->RegisterForExactType(plNetVoiceListMsg::Index(), GetKey());
	plgDispatch::Dispatch()->RegisterForExactType(plClientMsg::Index(), GetKey());
	plgDispatch::Dispatch()->RegisterForExactType(plEvalMsg::Index(), GetKey());

	plgDispatch::Dispatch()->RegisterForExactType(plNetCommAuthMsg::Index(), GetKey());
	plgDispatch::Dispatch()->RegisterForExactType(plNetCommActivePlayerMsg::Index(), GetKey());
	plgDispatch::Dispatch()->RegisterForExactType(plNetCommLinkToAgeMsg::Index(), GetKey());

    IInitNetClientComm();

	return ret;	
}

//
// Prepare to send.
// Update p2p transport groups and rcvrs list in some msgs
// Returns channel.
//
int plNetClientMgr::IPrepMsg(plNetMessage* msg)
{
	// pick channel, prepare msg
	int channel=kNetChanDefault;
	plNetMsgVoice* v=plNetMsgVoice::ConvertNoRef(msg);
	if (v)
	{		// VOICE MSG
		channel=kNetChanVoice;
		
		// compute new transport group (from talkList) if necessary
		GetTalkList()->UpdateTransportGroup(this);	
		
		// update receivers list in voice msg based on talk list
		v->Receivers()->Clear();
		int i;
		for(i=0;i<GetTalkList()->GetNumMembers(); i++)
		{
			v->Receivers()->AddReceiverPlayerID(GetTalkList()->GetMember(i)->GetPlayerID());
		}

		if (msg->IsBitSet(plNetMessage::kEchoBackToSender))
			v->Receivers()->AddReceiverPlayerID(GetPlayerID());
	}
	else if (plNetMsgListenListUpdate::ConvertNoRef(msg))
	{	// LISTEN LIST UPDATE MSG
		channel=kNetChanListenListUpdate;

		// update transport group from rcvrs list, add p2p mbrs to trasnport group
		fTransport.ClearChannelGrp(kNetChanListenListUpdate);
		if (IsPeerToPeer())
		{
			plNetMsgReceiversListHelper* rl = plNetMsgReceiversListHelper::ConvertNoRef(msg);
			hsAssert(rl, "error converting msg to rcvrs list?");
			int i;
			for(i=0;i<rl->GetNumReceivers();i++)
			{
				int idx=fTransport.FindMember(rl->GetReceiverPlayerID(i));
				hsAssert(idx!=-1, "error finding transport mbr");
				plNetTransportMember* tm = fTransport.GetMember(idx);
				if (tm->IsPeerToPeer())
					fTransport.SubscribeToChannelGrp(tm, kNetChanListenListUpdate);
			}
		}
	}
	else if( plNetMsgGameMessageDirected::ConvertNoRef( msg ) )
	{
		channel = kNetChanDirectedMsg;
	}
	return channel;
}

//
// unload other player since we're leaving the age
//
void plNetClientMgr::IUnloadRemotePlayers()
{
	for(int i=RemotePlayerKeys().size()-1;i>=0;i--)
		plAvatarMgr::GetInstance()->UnLoadRemotePlayer(RemotePlayerKeys()[i]);
	hsAssert(!RemotePlayerKeys().size(),"Still remote players left when linking out");
}

//
// begin linking out sounds and gfx
//
void plNetClientMgr::StartLinkOutFX()
{
	// send msg to trigger link out effect
	if (fLocalPlayerKey)
	{
		plNetLinkingMgr * lm = plNetLinkingMgr::GetInstance();

		plLinkEffectsTriggerMsg* lem = TRACKED_NEW plLinkEffectsTriggerMsg();
		lem->SetLeavingAge(true);
		lem->SetLinkKey(fLocalPlayerKey);
		lem->SetBCastFlag(plMessage::kNetPropagate);
		lem->SetBCastFlag(plMessage::kNetForce);	// Necessary?
		lem->AddReceiver(hsgResMgr::ResMgr()->FindKey(plUoid(kLinkEffectsMgr_KEY)));
		lem->Send();
	}
}

//
// beging linking in sounds snd gfx
//
void plNetClientMgr::StartLinkInFX()
{
	if (fLocalPlayerKey)
	{
		const plArmatureMod *avMod = plAvatarMgr::GetInstance()->GetLocalAvatar();

		plLinkEffectsTriggerMsg* lem = TRACKED_NEW plLinkEffectsTriggerMsg();
		lem->SetLeavingAge(false);	// linking in
		lem->SetLinkKey(fLocalPlayerKey);
		lem->SetLinkInAnimKey(avMod->GetLinkInAnimKey());
		
		// indicate if we are invisible 
		if (avMod && avMod->IsInStealthMode() && avMod->GetTarget(0))
		{
			lem->SetInvisLevel(avMod->GetStealthLevel());
		}

		lem->SetBCastFlag(plMessage::kNetPropagate);

		lem->AddReceiver(hsgResMgr::ResMgr()->FindKey(plUoid(kLinkEffectsMgr_KEY)));
		lem->AddReceiver(hsgResMgr::ResMgr()->FindKey(plUoid(kClient_KEY)));
		plgDispatch::MsgSend(lem);
	}
}

//
// compute the difference between our clock and the server's in unified time
//
void plNetClientMgr::UpdateServerTimeOffset(plNetMessage* msg)
{
	if ((hsTimer::GetSysSeconds() - fLastTimeUpdate) > 5)
	{
		fLastTimeUpdate = hsTimer::GetSysSeconds();

		const plUnifiedTime& msgSentUT = msg->GetTimeSent();
		if (!msgSentUT.AtEpoch())
		{
			double diff = plUnifiedTime::GetTimeDifference(msgSentUT, plClientUnifiedTime::GetCurrentTime());

			if (fServerTimeOffset == 0)
			{
				fServerTimeOffset = diff;
			}
			else
			{
				fServerTimeOffset = fServerTimeOffset + ((diff - fServerTimeOffset) / ++fTimeSamples);
			}

			DebugMsg("Setting server time offset to %f", fServerTimeOffset);
		}
	}
}

void plNetClientMgr::ResetServerTimeOffset()
{
	fServerTimeOffset = 0;
	fTimeSamples = 0;
	fLastTimeUpdate = 0;
}

//
// return the gameservers time
//
plUnifiedTime plNetClientMgr::GetServerTime() const 
{ 
	if ( fServerTimeOffset==0 )		// offline mode or before connecting/calibrating to a server
		return plUnifiedTime::GetCurrentTime();
	
	plUnifiedTime serverUT;
	if (fServerTimeOffset<0)
		return plUnifiedTime::GetCurrentTime() - plUnifiedTime(fabs(fServerTimeOffset)); 
	else
		return  plUnifiedTime::GetCurrentTime() + plUnifiedTime(fServerTimeOffset); 
}

//
// How old is the age
//
double plNetClientMgr::GetCurrentAgeElapsedSeconds() const
{
	return plAgeLoader::GetInstance()->GetCurrAgeDesc().GetAgeElapsedSeconds(GetServerTime());
}

//
// Get the time in the current age (0-1), taking into account the age's day length
//
float plNetClientMgr::GetCurrentAgeTimeOfDayPercent() const
{
	if (fOverrideAgeTimeOfDayPercent>=0)
		return fOverrideAgeTimeOfDayPercent;

	return plAgeLoader::GetInstance()->GetCurrAgeDesc().GetAgeTimeOfDayPercent(GetServerTime());
}

//
// main update fxn for net client code, return hsFail on err
//
int plNetClientMgr::Update(double secs)
{
	int ret=hsOK;	// ret code is unchecked, but what the hay
	
	if (GetFlagsBit(kDisableOnNextUpdate)) {
		SetFlagsBit(kDisableOnNextUpdate, false);
		IDisableNet();
	}
	
	// Pump net messages
	NetCommUpdate();

	static double lastUpdateTime=0;
	double curTime=hsTimer::GetSeconds();
	if (curTime-lastUpdateTime > 1.f)
	{
		DebugMsg("NetClient hasn't updated for %f secs", curTime-lastUpdateTime);
	}
	lastUpdateTime=curTime;

	if (!GetFlagsBit(kDisabled))
	{
		fNetClientStats.UpdateAgeStats();
	}

	if (GetFlagsBit(kPlayingGame) )
	{
		MaybeSendPendingPagingRoomMsgs();
		ICheckPendingStateLoad(secs);
		ISendDirtyState(secs);
		IUpdateListenList(secs);
		if (GetFlagsBit(plNetClientApp::kShowLists))
			IShowLists();
		if (GetFlagsBit(plNetClientApp::kShowRooms))
			IShowRooms();
		if (GetFlagsBit(plNetClientApp::kShowAvatars))
			IShowAvatars();
		if (GetFlagsBit(plNetClientApp::kShowRelevanceRegions))
			IShowRelevanceRegions();
	}

	// Send dirty nodes, deliver vault callbacks, etc.
	VaultUpdate();

	plNetLinkingMgr::GetInstance()->Update();

	return ret;
}

//
// See if there is state recvd from the network that needsto be delivered
//
void plNetClientMgr::ICheckPendingStateLoad(double secs)
{
	if (!fPendingLoads.empty() && GetFlagsBit( kPlayingGame ) || (GetFlagsBit(kLoadingInitialAgeState) && !GetFlagsBit(kNeedInitialAgeStateCount)))
	{
		PendingLoadsList::iterator it = fPendingLoads.begin();
		while ( it!=fPendingLoads.end() )
		{
			PendingLoad * pl = (*it);

			// cache rcvr key
			if (!pl->fKey)
			{
				// check for existence of key in dataset, excluding clone info.
				plUoid tmpUoid = pl->fUoid;
				tmpUoid.SetClone(0,0);
				if ( !hsgResMgr::ResMgr()->FindKey( tmpUoid ) )
				{
					// discard the state if object not found in dataset.
					char tmp[256];
					hsLogEntry( DebugMsg( "Failed to find object %s in dataset. Discarding pending state '%s'",
						tmpUoid.StringIze(tmp),
						pl->fSDRec->GetDescriptor()->GetName() ) );
					delete pl;
					it = fPendingLoads.erase(it);
					continue;
				}
				// find and cache the real key.
				pl->fKey = hsgResMgr::ResMgr()->FindKey(pl->fUoid);
			}
						
			// deliver state if possible
			plSynchedObject*so = pl->fKey ? plSynchedObject::ConvertNoRef(pl->fKey->ObjectIsLoaded()) : nil;
			if (so && so->IsFinal())
			{				
				plSDLModifierMsg* sdlMsg = TRACKED_NEW plSDLModifierMsg(pl->fSDRec->GetDescriptor()->GetName(), 
					plSDLModifierMsg::kRecv);
				sdlMsg->SetState( pl->fSDRec, true/*delete pl->fSDRec for us*/ );
				sdlMsg->SetPlayerID( pl->fPlayerID );

#ifdef HS_DEBUGGING
				if (plNetObjectDebugger::GetInstance()->IsDebugObject(so))
				{
					hsLogEntry( DebugMsg( "Delivering SDL state %s:%s", pl->fKey->GetName(), pl->fSDRec->GetDescriptor()->GetName() ) );
//					hsLogEntry(plNetObjectDebugger::GetInstance()->LogMsg(xtl::format("Dispatching SDL state, type %s to object:%s, locallyOwned=%d, st=%.3f rt=%.3f", 
//						pl->fSDRec->GetDescriptor()->GetName(), pl->fKey->GetName(), 
//						so->IsLocallyOwned()==plSynchedObject::kYes, secs, hsTimer::GetSeconds()).c_str()));
//					hsLogEntry( pl->fSDRec->DumpToObjectDebugger( "Delivering SDL state", false, 0 ) );
				}
#endif

				sdlMsg->Send(pl->fKey);

				pl->fSDRec = nil;	// so it won't be deleted in the PendingLoad dtor.
				delete pl;
				it = fPendingLoads.erase(it);
			}
			else
			{
				// report old pending state
				double rawSecs = hsTimer::GetSeconds();
				if ((rawSecs - pl->fQueuedTime) > 60.f /*secs*/)
				{
					char tmp[256], tmp2[256];
					if (pl->fQueueTimeResets >= 5)
					{
						// if this is our fifth time in here then we've been queued
						// for around 5 minutes and its time to go

						WarningMsg( "Pending state '%s' for object [uoid:%s,key:%s] has been queued for about %f secs. Removing...",
							pl->fSDRec && pl->fSDRec->GetDescriptor() ? pl->fSDRec->GetDescriptor()->GetName() : "?",
							pl->fUoid.StringIze(tmp2), pl->fKey ? pl->fKey->GetUoid().StringIze(tmp) : "?",
							( rawSecs - pl->fQueuedTime ) * pl->fQueueTimeResets);

						delete pl;
						it = fPendingLoads.erase(it);
						continue;
					}

					WarningMsg( "Pending state '%s' for object [uoid:%s,key:%s] has been queued for about %f secs. %s",
						pl->fSDRec && pl->fSDRec->GetDescriptor() ? pl->fSDRec->GetDescriptor()->GetName() : "?",
						pl->fUoid.StringIze(tmp2), pl->fKey ? pl->fKey->GetUoid().StringIze(tmp) : "?",
						( rawSecs - pl->fQueuedTime ) * pl->fQueueTimeResets,
						so ? "(not loaded)" : "(not final)" );
					// reset queue time so we don't spew too many log msgs.
					pl->fQueuedTime = rawSecs;
					pl->fQueueTimeResets += 1;
				}

				it++;
			}
		}
	}
}

//
// Determine the net group for a given object (based on paging unit)
//
plNetGroupId plNetClientMgr::SelectNetGroup(plSynchedObject* objIn, plKey roomKey)
{
	if( objIn->GetSynchFlags() & plSynchedObject::kHasConstantNetGroup )
		return objIn->GetNetGroup();

	return plNetGroupId(roomKey->GetUoid().GetLocation(), 0);
}

//
// Get the net group that an object belongs to, taking into acct
// that objects may derive their net group from another object they belong to.
//
plNetGroupId plNetClientMgr::GetEffectiveNetGroup(const plSynchedObject*& obj) const
{
   plNetGroupId netGroup = plNetGroup::kNetGroupUnknown;
	// the object is an interface, so consider the owner instead 
	const plObjInterface* oInt=plObjInterface::ConvertNoRef(obj);
	if (oInt)
	{
		// the object is an interface, so consider the owner instead 
		if (oInt->GetOwner())
			obj = oInt->GetOwner();
	}	
	else
	{
		const plModifier* mod=plModifier::ConvertNoRef(obj);
		if (mod)
		{
			// the object is a modifier, so consider the first target instead 
			if (mod->GetNumTargets() && mod->GetTarget(0))
				obj = mod->GetTarget(0);
		}
		else
		{
			const plLayerInterface* li = plLayerInterface::ConvertNoRef(obj);
			const plClothingOutfit* cl = plClothingOutfit::ConvertNoRef(obj);
			if (li || cl)
			{
				// the object is a layer interface or clothing object
				plUoid u = obj->GetKey()->GetUoid();
				if (u.GetLocation().IsReserved())
				{
					// layer interface is associated with an avatar, find which one
					int cloneID = u.GetCloneID();
					int clonePlayerID = u.GetClonePlayerID();
					for(int i = -1; i < RemotePlayerKeys().size(); i++)
					{
						plKey pKey = (i==-1) ? GetLocalPlayerKey() : RemotePlayerKeys()[i];
						if (pKey && pKey->GetUoid().GetCloneID()==cloneID && pKey->GetUoid().GetClonePlayerID()==clonePlayerID)
						{
							obj = plSynchedObject::ConvertNoRef(pKey->ObjectIsLoaded());
							break;
						}
					}
				}
				else
				{
					// layer interface is on a non-avatar object
					netGroup = u.GetLocation();
				}
			}
			else
			{
				const plPhysical* ph = plPhysical::ConvertNoRef(obj);
				if (ph)
				{
					// the object is a physical
					obj = plSynchedObject::ConvertNoRef(ph->GetObjectKey()->ObjectIsLoaded());
				}
			}
		}
	}
	
	if (obj)
		netGroup = obj->GetNetGroup();	
	
	return netGroup;
}

//
// If we don't know for sure if something is locallyOwned, check if it' related to the remote or 
// local avatar.  if not, fallback to join order.
//
int plNetClientMgr::IDeduceLocallyOwned(const plUoid& uoid) const
{
	// check against local/remote avatars
	if (fLocalPlayerKey)
		if (uoid.GetLocation() == fLocalPlayerKey->GetUoid().GetLocation() &&
			uoid.GetClonePlayerID() == fLocalPlayerKey->GetUoid().GetClonePlayerID())
			return plSynchedObject::kYes;	// this object's location is the same as the local player's

	if (!fRemotePlayerKeys.empty())
		if (uoid.GetLocation() == fRemotePlayerKeys.back()->GetUoid().GetLocation() &&
			uoid.GetClonePlayerID() == fRemotePlayerKeys.back()->GetUoid().GetClonePlayerID())
			return plSynchedObject::kNo;	// this object's location is the same as the currently loading remote player's

	return fIsOwner;
}


//
// returns yes/no
// for special cases, like sceneNodes.
//
int plNetClientMgr::IsLocallyOwned(const plUoid& uoid) const
{
	// we're non-networked
	if ( GetFlagsBit(kDisabled))
		return plSynchedObject::kYes;

	plNetGroupId netGroup = uoid.GetLocation();
	int answer=GetNetGroups()->IsGroupLocal(netGroup);
	
	if (answer==-1)
		answer = IDeduceLocallyOwned(uoid);

	hsAssert(answer==plSynchedObject::kYes || answer==plSynchedObject::kNo, "invalid answer to IsLocallyOwned()");
	return answer;
}


//
// check if this object is in a group which is owned (mastered) by this client.
// returns yes/no
//
int plNetClientMgr::IsLocallyOwned(const plSynchedObject* obj) const
{
	// we're non-networked
	if ( GetFlagsBit(kDisabled) )
		return plSynchedObject::kYes;

	if( !obj )
		return plSynchedObject::kNo;

	// object is flagged as local only
	if (!obj->IsNetSynched())
		return plSynchedObject::kYes;

	plNetGroupId netGroup = GetEffectiveNetGroup(obj);
	int answer=GetNetGroups()->IsGroupLocal(netGroup);

	if (answer==-1)		// not sure
		answer = IDeduceLocallyOwned(obj->GetKey()->GetUoid());

	hsAssert(answer==plSynchedObject::kYes || answer==plSynchedObject::kNo, "invalid answer to IsLocallyOwned()");
	return answer;
}

//
// return localPlayer ptr
//
plSynchedObject* plNetClientMgr::GetLocalPlayer(hsBool forceLoad) const
{ 
	if (forceLoad)
		return fLocalPlayerKey ? plSynchedObject::ConvertNoRef(fLocalPlayerKey->GetObjectPtr()) : nil; 
	else
		return fLocalPlayerKey ?
			plSynchedObject::ConvertNoRef(fLocalPlayerKey->ObjectIsLoaded()) : nil; 
}

//
// return a ptr to a remote player
//
plSynchedObject* plNetClientMgr::GetRemotePlayer(int i) const
{ 
	return fRemotePlayerKeys[i] ? plSynchedObject::ConvertNoRef(fRemotePlayerKeys[i]->ObjectIsLoaded()) : nil; 
}

//
// check if a key si a remote player
//
hsBool plNetClientMgr::IsRemotePlayerKey(const plKey pKey, int *idx)
{
	if (pKey)
	{
		plKeyVec::iterator result=std::find(fRemotePlayerKeys.begin(), fRemotePlayerKeys.end(), pKey);
		hsBool found = result!=fRemotePlayerKeys.end();
		if (idx)
			*idx = found ? result-fRemotePlayerKeys.begin() : -1;
		return found;
	}
	return false;
}

//
// add remote char if not already there
//
void plNetClientMgr::AddRemotePlayerKey(plKey pKey)
{
	hsAssert(pKey, "adding nil remote player key");
	if (!IsRemotePlayerKey(pKey))
	{
		fRemotePlayerKeys.push_back(pKey);
		hsAssert(pKey!=fLocalPlayerKey, "setting remote player to the local player?");
	}
}

//
// MsgReceive handler for plasma messages
//
hsBool plNetClientMgr::MsgReceive( plMessage* msg )
{
	if (plNetLinkingMgr::GetInstance()->MsgReceive( msg ))
		return true;

	plEvalMsg* evalMsg = plEvalMsg::ConvertNoRef(msg);
	if (evalMsg)
	{
		IPlaybackMsgs();

		if ( GetFlagsBit( kNeedToSendAgeLoadedMsg ) )
		{
			SetFlagsBit( kNeedToSendAgeLoadedMsg, false );
			plAgeLoader::GetInstance()->NotifyAgeLoaded( true );
		}

		if ( GetFlagsBit( kNeedToSendInitialAgeStateLoadedMsg ) )
		{
			SetFlagsBit(kNeedToSendInitialAgeStateLoadedMsg, false);
			plInitialAgeStateLoadedMsg* m = TRACKED_NEW plInitialAgeStateLoadedMsg;
			m->Send();
		}

		return true;
	}

	plGenRefMsg* ref = plGenRefMsg::ConvertNoRef(msg);
	if (ref)
	{
		if( ref->fType == kVaultImage )
		{
			// Ignore, we just use it for reffing, don't care about the actual pointer
			return true;
		}

		hsAssert(ref->fType==kAgeSDLHook, "unknown ref msg context");
		if (ref->GetContext()==plRefMsg::kOnCreate)
		{
			hsAssert(fAgeSDLObjectKey==nil, "already have a ref to age sdl hook");
			fAgeSDLObjectKey = ref->GetRef()->GetKey();
			char tmp[256];
			DebugMsg("Age SDL hook object created, uoid=%s", fAgeSDLObjectKey->GetUoid().StringIze(tmp));
		}
		else
		{
			fAgeSDLObjectKey=nil;
			DebugMsg("Age SDL hook object destroyed");
		}
		return true;
	}

	if (plNetClientMgrMsg * ncmMsg = plNetClientMgrMsg::ConvertNoRef(msg)) {
		if (ncmMsg->type == plNetClientMgrMsg::kCmdDisableNet) {
			SetFlagsBit(kDisableOnNextUpdate);
			hsRefCnt_SafeUnRef(fDisableMsg);
			fDisableMsg = ncmMsg;
			fDisableMsg->Ref();
		}
		return true;
	}
	
    if (plNetCommAuthMsg * authMsg = plNetCommAuthMsg::ConvertNoRef(msg)) {
        if (IS_NET_ERROR(authMsg->result)) {
			char str[256];
			StrPrintf(str, arrsize(str), "Authentication failed: %S", NetErrorToString(authMsg->result));
			QueueDisableNet(true, str);
            return false;	// @@@ TODO: Handle this failure better
        }

        return true;
    }

    if (plNetCommActivePlayerMsg * activePlrMsg = plNetCommActivePlayerMsg::ConvertNoRef(msg)) {
        if (IS_NET_ERROR(activePlrMsg->result)) {
			char str[256];
			StrPrintf(str, arrsize(str), "SetActivePlayer failed: %S", NetErrorToString(activePlrMsg->result));
			QueueDisableNet(true, str);
            return false;	// @@@ TODO: Handle this failure better.
        }
            
        return true;
    }

	plPlayerPageMsg *playerPageMsg = plPlayerPageMsg::ConvertNoRef(msg);
	if(playerPageMsg)
	{
		IHandlePlayerPageMsg(playerPageMsg);
		return true;	// handled
	}

	plLoadCloneMsg* pCloneMsg = plLoadCloneMsg::ConvertNoRef(msg);
	if(pCloneMsg)
	{
		ILoadClone(pCloneMsg);
		return true;	// handled
	}

	// player is petitioning a CCR
	plCCRPetitionMsg* petMsg=plCCRPetitionMsg::ConvertNoRef(msg);
	if (petMsg)
	{
		ISendCCRPetition(petMsg);
		return true;
	}

	// a remote CCR is turning invisible
	plCCRInvisibleMsg* invisMsg=plCCRInvisibleMsg::ConvertNoRef(msg);
	if (invisMsg)
	{
		LogMsg(kLogDebug, "plNetClientMgr::MsgReceive - Got plCCRInvisibleMsg");
		MakeCCRInvisible(invisMsg->fAvKey, invisMsg->fInvisLevel);
		return true;
	}
	
	plCCRBanLinkingMsg* banLinking = plCCRBanLinkingMsg::ConvertNoRef(msg);
	if (banLinking)
	{
		DebugMsg("Setting BanLinking to %d", banLinking->fBan);
		SetFlagsBit(kBanLinking, banLinking->fBan);
		return true;
	}

	plCCRSilencePlayerMsg* silence = plCCRSilencePlayerMsg::ConvertNoRef(msg);
	if (silence)
	{
		DebugMsg("Setting Silence to %d", silence->fSilence);
		SetFlagsBit(kSilencePlayer, silence->fSilence);
		return true;
	}


	plInitialAgeStateLoadedMsg *stateMsg = plInitialAgeStateLoadedMsg::ConvertNoRef( msg );
	if( stateMsg != nil )
	{
		// done receiving the initial state of the age from the server
		plNetObjectDebugger::GetInstance()->LogMsg("OnServerInitComplete");

		// delete fProgressBar;
		// fProgressBar=nil;

		SetFlagsBit(kLoadingInitialAgeState, false);
		StartLinkInFX();
	}

	plNetVoiceListMsg* voxList = plNetVoiceListMsg::ConvertNoRef(msg);
	if (voxList)
	{
		IHandleNetVoiceListMsg(voxList);
		return true;
	}

	plSynchEnableMsg* synchEnable = plSynchEnableMsg::ConvertNoRef(msg);
	if (synchEnable)
	{
		if (synchEnable->fPush)
		{
			plSynchedObject::PushSynchDisabled(!synchEnable->fEnable);
		}
		else
		{
			plSynchedObject::PopSynchDisabled();
		}
		return true;
	}

	plClientMsg* clientMsg = plClientMsg::ConvertNoRef(msg);
	if (clientMsg && clientMsg->GetClientMsgFlag()==plClientMsg::kInitComplete)
	{
		// add 1 debug object for age sdl
		if (plNetObjectDebugger::GetInstance())
		{
			plNetObjectDebugger::GetInstance()->RemoveDebugObject("AgeSDLHook");	
			plNetObjectDebugger::GetInstance()->AddDebugObject("AgeSDLHook");
		}

		// if we're linking to startup we don't need (or want) a player set
		char ageName[kMaxAgeNameLength];
		StrCopy(ageName, NetCommGetStartupAge()->ageDatasetName, arrsize(ageName));
		if (!StrLen(ageName))
			StrCopy(ageName, "StartUp", arrsize(ageName));
		if (0 == StrCmpI(ageName, "StartUp"))
			NetCommSetActivePlayer(0, nil);

		plAgeLinkStruct link;
		link.GetAgeInfo()->SetAgeFilename(NetCommGetStartupAge()->ageDatasetName);
		link.SetLinkingRules(plNetCommon::LinkingRules::kOriginalBook);
		plNetLinkingMgr::GetInstance()->LinkToAge(&link);

		return true;
	}
	
	return plNetClientApp::MsgReceive(msg);
}

void plNetClientMgr::IncNumInitialSDLStates()
{
	fNumInitialSDLStates++;
	DebugMsg( "Received %d initial SDL states", fNumInitialSDLStates );
	if ( GetFlagsBit( plNetClientApp::kNeedInitialAgeStateCount ) )
	{
		DebugMsg( "Need initial SDL state count" );
		return;
	}

	if ( GetNumInitialSDLStates()>=GetRequiredNumInitialSDLStates() )
	{
		ICheckPendingStateLoad(hsTimer::GetSysSeconds());
		NotifyRcvdAllSDLStates();
	}
}

void plNetClientMgr::NotifyRcvdAllSDLStates() {
	DebugMsg( "Got all initial SDL states" );
	plNetClientMgrMsg * msg = TRACKED_NEW plNetClientMgrMsg();
	msg->type = plNetClientMgrMsg::kNotifyRcvdAllSDLStates;
	msg->SetBCastFlag(plMessage::kBCastByType);
	msg->Send();
}

//
// return true if we should send this msg
//
bool plNetClientMgr::CanSendMsg(plNetMessage * msg)
{
	// TEMP
	if (GetFlagsBit(kSilencePlayer))
	{
		if (plNetMsgVoice::ConvertNoRef(msg))
			return false;

		plNetMsgGameMessage* gm=plNetMsgGameMessage::ConvertNoRef(msg);
		if (gm && gm->StreamInfo()->GetStreamType()==pfKIMsg::Index())
		{
			hsReadOnlyStream stream(gm->StreamInfo()->GetStreamLen(), gm->StreamInfo()->GetStreamBuf());
			pfKIMsg* kiMsg = pfKIMsg::ConvertNoRef(hsgResMgr::ResMgr()->ReadCreatable(&stream));
			if (kiMsg && kiMsg->GetCommand()==pfKIMsg::kHACKChatMsg)
			{
				delete kiMsg;
				return false;
			}
			delete kiMsg;
		}
	}

	return true;
}

//
// Return the net client (account) name of the player whose avatar
// key is provided.  If avKey is nil, returns local client name.
//
const char* plNetClientMgr::GetPlayerName(const plKey avKey) const
{
	// local case
	if (!avKey || avKey==GetLocalPlayerKey())
		return NetCommGetPlayer()->playerNameAnsi;
	
	plNetTransportMember* mbr=TransportMgr().GetMember(TransportMgr().FindMember(avKey));
	return mbr ? mbr->GetPlayerName() : nil;
}

const char * plNetClientMgr::GetPlayerNameById (unsigned playerId) const {
	// local case
	if (NetCommGetPlayer()->playerInt == playerId)
		return NetCommGetPlayer()->playerNameAnsi;
		
	plNetTransportMember * mbr = TransportMgr().GetMember(TransportMgr().FindMember(playerId));
	return mbr ? mbr->GetPlayerName() : nil;
}

unsigned plNetClientMgr::GetPlayerIdByName (const char name[]) const {
	// local case
	if (0 == StrCmp(name, NetCommGetPlayer()->playerNameAnsi, (unsigned)-1))
		NetCommGetPlayer()->playerInt;
	
	unsigned n = TransportMgr().GetNumMembers();
	for (unsigned i = 0; i < n; ++i)
		if (plNetTransportMember * member = TransportMgr().GetMember(i))
			if (0 == StrCmp(name, member->GetPlayerName(), (unsigned)-1))
				return member->GetPlayerID();
	return 0;
}

UInt32 plNetClientMgr::GetPlayerID() const
{
	return NetCommGetPlayer()->playerInt;
}

//
// return true if the age is the set of local ages
//
static const char* gLocalAges[]={"GUI", "AvatarCustomization", ""};
static bool IsLocalAge(const char* ageName)
{
	int i=0;
	while(strlen(gLocalAges[i]))
	{
		if (!stricmp(gLocalAges[i], ageName))
			return true;
		i++;
	}
	return false;
}

//
// is the object in a local age?
//
bool plNetClientMgr::ObjectInLocalAge(const plSynchedObject* obj) const
{
	plLocation loc = obj->GetKey()->GetUoid().GetLocation();
	const plPageInfo* pageInfo = plKeyFinder::Instance().GetLocationInfo(loc);
	bool ret= IsLocalAge(pageInfo->GetAge());
	return ret;
}

//
// the next age we are going to
//
const char* plNetClientMgr::GetNextAgeFilename() 
{ 
	// set when we start linking to an age.
	plNetLinkingMgr * lm = plNetLinkingMgr::GetInstance();
	return lm->GetAgeLink()->GetAgeInfo()->GetAgeFilename();
}

//
// a remote ccr is turning [in]visible
//
void plNetClientMgr::MakeCCRInvisible(plKey avKey, int level)
{
	if (!avKey)
	{
		ErrorMsg("Failed to make remote CCR Invisible, nil avatar key");
		return;
	}
		
	if (level<0)
	{
		ErrorMsg("Failed to make remote CCR Invisible, negative level");
		return;
	}
	
	if (!avKey->ObjectIsLoaded() || !avKey->ObjectIsLoaded()->IsFinal())
	{
		hsAssert(false, "avatar not loaded or final");
		return;
	}
	
	plAvatarStealthModeMsg *msg = TRACKED_NEW plAvatarStealthModeMsg();
	msg->SetSender(avKey);
	msg->fLevel = level;

	if (GetCCRLevel()<level)	// I'm a lower level than him, so he's invisible to me
		msg->fMode = plAvatarStealthModeMsg::kStealthCloaked;		
	else
	{
		// he's visible to me
		if (AmCCR() && level > 0)
			msg->fMode = plAvatarStealthModeMsg::kStealthCloakedButSeen;	// draw as semi-invis
		else
			msg->fMode = plAvatarStealthModeMsg::kStealthVisible;
	}

	DebugMsg("Handled MakeCCRInvisible - sending stealth msg");;

	// This fxn is called when avatar SDL state is received from the server,
	// so this msg will inherit the 'non-local' status of the parent sdl msg.
	// That means that the avatar linkSound won't receive it, since the linkSound
	// is set as localOnly.
	// So, terminate the remote cascade and start a new (local) cascade.
	msg->SetBCastFlag(plMessage::kNetStartCascade);	
	
	msg->Send();
}

void plNetClientMgr::QueueDisableNet (bool showDlg, const char str[]) {

    plNetClientMgrMsg * msg = NEWZERO(plNetClientMgrMsg);
    msg->type	= plNetClientMgrMsg::kCmdDisableNet;
    msg->yes	= showDlg;
    if (str)
		StrCopy(msg->str, str, arrsize(msg->str));

#if 0
	msg->Send(GetKey());
#else
	msg->Ref();
	fDisableMsg = msg;
	IDisableNet();
#endif;
}

void plNetClientMgr::IDisableNet () {
	ASSERT(fDisableMsg);
	
	if (!GetFlagsBit(kDisabled)) {
		SetFlagsBit(kDisabled);
		// cause subsequent net operations to fail immediately, but don't block
		// waiting for net subsystem to shutdown (we'll do that later)
		NetCommEnableNet(false, false);

		// display a msg to the player
		if ( fDisableMsg->yes )
		{
			if (!GetFlagsBit(plNetClientApp::kPlayingGame))
			{
				// KI may not be loaded
				char title[256];
				StrPrintf(title, arrsize(title), "%S Error", ProductCoreName());
				hsMessageBox(fDisableMsg->str, title, hsMessageBoxNormal, hsMessageBoxIconError );
				plClientMsg *quitMsg = NEW(plClientMsg)(plClientMsg::kQuit);
				quitMsg->Send(hsgResMgr::ResMgr()->FindKey(kClient_KEY));
			}
			else
			{
				pfKIMsg *msg = TRACKED_NEW pfKIMsg(pfKIMsg::kKIOKDialog);
				msg->SetString(fDisableMsg->str);
				msg->Send();
			}
		}
	}

	hsRefCnt_SafeUnRef(fDisableMsg);
	fDisableMsg = nil;
}

bool plNetClientMgr::IHandlePlayerPageMsg(plPlayerPageMsg *playerMsg)
{
	bool result = false;
	plKey playerKey = playerMsg->fPlayer;
	int idx;

	if(playerMsg->fUnload)
	{
		if (GetLocalPlayerKey() == playerKey)
		{
			fLocalPlayerKey = nil;
			DebugMsg("Net: Unloading local player %s", playerKey->GetName());
			
			// notify server - NOTE: he might not still be around to get this...
			plNetMsgPlayerPage npp (playerKey->GetUoid(), playerMsg->fUnload);
			npp.SetNetProtocol(kNetProtocolCli2Game);
			SendMsg(&npp);
		}
		else if (IsRemotePlayerKey(playerKey, &idx))
		{
			fRemotePlayerKeys.erase(fRemotePlayerKeys.begin()+idx);	// remove key from list
			DebugMsg("Net: Unloading remote player %s", playerKey->GetName());
		}
	}
	else
	{
		plSceneObject *playerSO = plSceneObject::ConvertNoRef(playerKey->ObjectIsLoaded());
		if (!playerSO)
		{
			hsStatusMessageF("Ignoring player page message for non-existant player.");
		}
		else
		if(playerMsg->fPlayer)
		{
			if (playerMsg->fLocallyOriginated)
			{
				hsAssert(!GetLocalPlayerKey() ||
					GetLocalPlayerKey() == playerKey,
					"Different local player already loaded");

				hsLogEntry(DebugMsg("Adding LOCAL player %s\n", playerKey->GetName()));
				playerSO->SetNetGroupConstant(plNetGroup::kNetGroupLocalPlayer);
				
				// don't save avatar state permanently on server
				playerSO->SetSynchFlagsBit(plSynchedObject::kAllStateIsVolatile);				
				const plCoordinateInterface* co = playerSO->GetCoordinateInterface();
				if (co)
				{
					int i;
					for(i=0;i<co->GetNumChildren();i++)
					{
						if (co->GetChild(i) && co->GetChild(i)->GetOwner())
								const_cast<plSceneObject*>(co->GetChild(i)->GetOwner())->SetSynchFlagsBit(plSynchedObject::kAllStateIsVolatile);
					}
				}

				// notify server
				plNetMsgPlayerPage npp (playerKey->GetUoid(), playerMsg->fUnload);
				npp.SetNetProtocol(kNetProtocolCli2Game);
				SendMsg(&npp);
			}
			else
			{
				hsLogEntry(DebugMsg("Adding REMOTE player %s\n", playerKey->GetName()));
				playerSO->SetNetGroupConstant(plNetGroup::kNetGroupRemotePlayer);
				idx=fTransport.FindMember(playerMsg->fClientID);
				if( idx != -1 )
				{
					hsAssert(playerKey, "NIL KEY?");
					hsAssert(playerKey->GetName(), "UNNAMED KEY");
					fTransport.GetMember(idx)->SetAvatarKey(playerKey);
				}
				else
				{
					hsLogEntry(DebugMsg("Ignoring player page msg (player not found in member list) : %s\n", playerKey->GetName()));
				}
			}

			hsAssert(IFindModifier(playerSO, CLASS_INDEX_SCOPED(plAvatarSDLModifier)), "avatar missing avatar SDL modifier");
			hsAssert(IFindModifier(playerSO, CLASS_INDEX_SCOPED(plClothingSDLModifier)), "avatar missing clothing SDL modifier");
			hsAssert(IFindModifier(playerSO, CLASS_INDEX_SCOPED(plAGMasterSDLModifier)), "avatar missing AGMaster SDL modifier");
			result = true;
		}
	}
	return result;
}

// for debugging purposes
bool plNetClientMgr::IFindModifier(plSynchedObject* obj, Int16 classIdx)
{
	plLayerAnimation* layer = plLayerAnimation::ConvertNoRef(obj);
	if (layer)
		return (layer->GetSDLModifier() != nil);

	plResponderModifier* resp = plResponderModifier::ConvertNoRef(obj);
	if (resp)
		return (resp->GetSDLModifier() != nil);

	int cnt=0;
	plSceneObject* sceneObj=plSceneObject::ConvertNoRef(obj);
	if (sceneObj)
	{
		int i;
		for(i=0; i<sceneObj->GetNumModifiers(); i++)
			if (sceneObj->GetModifier(i) && sceneObj->GetModifier(i)->ClassIndex()==classIdx)
				cnt++;
	}

	hsAssert(cnt<2, xtl::format("Object %s has multiple SDL modifiers of the same kind (%s)?", 
		obj->GetKeyName(), plFactory::GetNameOfClass(classIdx)).c_str());
	return cnt==0 ? false : true;
}

plUoid plNetClientMgr::GetAgeSDLObjectUoid(const char* ageName) const
{
	hsAssert(ageName, "nil ageName");

	// if age sdl hook is loaded
	if (fAgeSDLObjectKey)
		return fAgeSDLObjectKey->GetUoid();

	// if age is loaded
	plLocation loc = plKeyFinder::Instance().FindLocation(ageName,plAgeDescription::GetCommonPage(plAgeDescription::kGlobal));
	if (!loc.IsValid())
	{
		// check current age des
		if (plAgeLoader::GetInstance()->GetCurrAgeDesc().GetAgeName() &&
			!strcmp(plAgeLoader::GetInstance()->GetCurrAgeDesc().GetAgeName(), ageName))
			loc=plAgeLoader::GetInstance()->GetCurrAgeDesc().CalcPageLocation("BuiltIn");

		if (!loc.IsValid())
		{
			// try to load age desc
			hsStream* stream=plAgeLoader::GetAgeDescFileStream(ageName);
			if (stream)
			{
				plAgeDescription ad;
				ad.Read(stream);
				loc=ad.CalcPageLocation("BuiltIn");
				stream->Close();
			}			
			delete stream;
		}
	}

	return plUoid(loc, plSceneObject::Index(), plSDL::kAgeSDLObjectName);
}

//
// Add a state update to the pending queue
//
void plNetClientMgr::AddPendingLoad(PendingLoad *pl) 
{ 
	pl->fQueuedTime = hsTimer::GetSeconds();	// timestamp

	// find corresponding key
	pl->fKey = hsgResMgr::ResMgr()->FindKey(pl->fUoid);

	// check for age SDL state
	char tmp[256];
	if (pl->fUoid.GetObjectName() && !strcmp(pl->fUoid.GetObjectName(), plSDL::kAgeSDLObjectName))
	{
		DebugMsg("Recv SDL state for age hook object, uoid=%s", pl->fUoid.StringIze(tmp));
		if (!pl->fKey)
			WarningMsg("Can't find age hook object, nil key!");
		else
			DebugMsg("Found age hook object");
	}

	// check if object is ready
	if (pl->fKey)
	{
		if (!pl->fKey->ObjectIsLoaded())
		{
			WarningMsg("Object %s not loaded, withholding SDL state", 
				pl->fKey->GetUoid().StringIze(tmp));
		}
		else if (!pl->fKey->ObjectIsLoaded()->IsFinal())
		{
			WarningMsg("Object %s is not FINAL, withholding SDL state", 
				pl->fKey->GetUoid().StringIze(tmp));			
		}
	}

	// add entry
	fPendingLoads.push_back(pl);	
}

void plNetClientMgr::AddPendingPagingRoomMsg( plNetMsgPagingRoom * msg )
{
	fPendingPagingRoomMsgs.push_back( msg );
}

void plNetClientMgr::MaybeSendPendingPagingRoomMsgs()
{
	if ( GetFlagsBit( kPlayingGame ) )
		SendPendingPagingRoomMsgs();
}

void plNetClientMgr::SendPendingPagingRoomMsgs()
{
	for ( int i=0; i<fPendingPagingRoomMsgs.size(); i++ )
		SendMsg( fPendingPagingRoomMsgs[i] );
	ClearPendingPagingRoomMsgs();
}

void plNetClientMgr::ClearPendingPagingRoomMsgs()
{
	std::for_each( fPendingPagingRoomMsgs.begin(), fPendingPagingRoomMsgs.end(), xtl::delete_ptr() );
	fPendingPagingRoomMsgs.clear();
}


bool plNetClientMgr::DebugMsgV(const char* fmt, va_list args) const 
{
	LogMsgV(kLogDebug, fmt, args);
	return true;
}

bool plNetClientMgr::ErrorMsgV(const char* fmt, va_list args) const 
{
	LogMsgV(kLogError, fmt, args);
	return true;
}

bool plNetClientMgr::WarningMsgV(const char* fmt, va_list args) const 
{
	LogMsgV(kLogError, fmt, args);
	return true;
}

bool plNetClientMgr::AppMsgV(const char* fmt, va_list args) const 
{
	LogMsgV(kLogPerf, fmt, args);
	return true;
}

bool plNetClientMgr::IsObjectOwner()
{
	return fIsOwner;
}

void plNetClientMgr::SetObjectOwner(bool own)
{
	fIsOwner = own;
}