/*==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==*/ #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 "../plMessage/plVaultNotifyMsg.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 "../../FeatureLib/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 #include //////////////////////////////////////////////////////////////////// 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;iUnLoadRemotePlayer(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()); // We need plVaultNotifyMsgs for the NetLinkingMgr plgDispatch::Dispatch()->RegisterForType(plVaultNotifyMsg::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;iGetNumMembers(); 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;iGetNumReceivers();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()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;iGetNumChildren();i++) { if (co->GetChild(i) && co->GetChild(i)->GetOwner()) const_cast(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; iGetNumModifiers(); 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