/* ** Command & Conquer Generals Zero Hour(tm) ** Copyright 2025 Electronic Arts 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 . */ //////////////////////////////////////////////////////////////////////////////// // // // (c) 2001-2003 Electronic Arts Inc. // // // //////////////////////////////////////////////////////////////////////////////// // FILE: BuddyThread.cpp ////////////////////////////////////////////////////// // GameSpy Presence & Messaging (Buddy) thread // This thread communicates with GameSpy's buddy server // and talks through a message queue with the rest of // the game. // Author: Matthew D. Campbell, June 2002 #include "PreRTS.h" // This must go first in EVERY cpp file int the GameEngine #include "GameNetwork/GameSpy/BuddyThread.h" #include "GameNetwork/GameSpy/PeerThread.h" #include "GameNetwork/GameSpy/PersistentStorageThread.h" #include "GameNetwork/GameSpy/ThreadUtils.h" #include "Common/StackDump.h" #include "mutex.h" #include "thread.h" #ifdef _INTERNAL // for occasional debugging... //#pragma optimize("", off) //#pragma MESSAGE("************************************** WARNING, optimization disabled for debugging purposes") #endif //------------------------------------------------------------------------- typedef std::queue RequestQueue; typedef std::queue ResponseQueue; class BuddyThreadClass; class GameSpyBuddyMessageQueue : public GameSpyBuddyMessageQueueInterface { public: virtual ~GameSpyBuddyMessageQueue(); GameSpyBuddyMessageQueue(); virtual void startThread( void ); virtual void endThread( void ); virtual Bool isThreadRunning( void ); virtual Bool isConnected( void ); virtual Bool isConnecting( void ); virtual void addRequest( const BuddyRequest& req ); virtual Bool getRequest( BuddyRequest& req ); virtual void addResponse( const BuddyResponse& resp ); virtual Bool getResponse( BuddyResponse& resp ); virtual GPProfile getLocalProfileID( void ); BuddyThreadClass* getThread( void ); private: MutexClass m_requestMutex; MutexClass m_responseMutex; RequestQueue m_requests; ResponseQueue m_responses; BuddyThreadClass *m_thread; }; GameSpyBuddyMessageQueueInterface* GameSpyBuddyMessageQueueInterface::createNewMessageQueue( void ) { return NEW GameSpyBuddyMessageQueue; } GameSpyBuddyMessageQueueInterface *TheGameSpyBuddyMessageQueue; #define MESSAGE_QUEUE ((GameSpyBuddyMessageQueue *)TheGameSpyBuddyMessageQueue) //------------------------------------------------------------------------- class BuddyThreadClass : public ThreadClass { public: BuddyThreadClass() : ThreadClass() { m_isNewAccount = m_isdeleting = m_isConnecting = m_isConnected = false; m_profileID = 0; m_lastErrorCode = 0; } void Thread_Function(); void errorCallback( GPConnection *con, GPErrorArg *arg ); void messageCallback( GPConnection *con, GPRecvBuddyMessageArg *arg ); void connectCallback( GPConnection *con, GPConnectResponseArg *arg ); void requestCallback( GPConnection *con, GPRecvBuddyRequestArg *arg ); void statusCallback( GPConnection *con, GPRecvBuddyStatusArg *arg ); Bool isConnecting( void ) { return m_isConnecting; } Bool isConnected( void ) { return m_isConnected; } GPProfile getLocalProfileID( void ) { return m_profileID; } private: Bool m_isNewAccount; Bool m_isConnecting; Bool m_isConnected; GPProfile m_profileID; Int m_lastErrorCode; Bool m_isdeleting; std::string m_nick, m_email, m_pass; }; static enum CallbackType { CALLBACK_CONNECT, CALLBACK_ERROR, CALLBACK_RECVMESSAGE, CALLBACK_RECVREQUEST, CALLBACK_RECVSTATUS, CALLBACK_MAX }; void callbackWrapper( GPConnection *con, void *arg, void *param ) { CallbackType info = (CallbackType)(Int)param; BuddyThreadClass *thread = MESSAGE_QUEUE->getThread() ? MESSAGE_QUEUE->getThread() : NULL /*(TheGameSpyBuddyMessageQueue)?TheGameSpyBuddyMessageQueue->getThread():NULL*/; if (!thread) return; switch (info) { case CALLBACK_CONNECT: thread->connectCallback( con, (GPConnectResponseArg *)arg ); break; case CALLBACK_ERROR: thread->errorCallback( con, (GPErrorArg *)arg ); break; case CALLBACK_RECVMESSAGE: thread->messageCallback( con, (GPRecvBuddyMessageArg *)arg ); break; case CALLBACK_RECVREQUEST: thread->requestCallback( con, (GPRecvBuddyRequestArg *)arg ); break; case CALLBACK_RECVSTATUS: thread->statusCallback( con, (GPRecvBuddyStatusArg *)arg ); break; } } //------------------------------------------------------------------------- GameSpyBuddyMessageQueue::GameSpyBuddyMessageQueue() { m_thread = NULL; } GameSpyBuddyMessageQueue::~GameSpyBuddyMessageQueue() { endThread(); } void GameSpyBuddyMessageQueue::startThread( void ) { if (!m_thread) { m_thread = NEW BuddyThreadClass; m_thread->Execute(); } else { if (!m_thread->Is_Running()) { m_thread->Execute(); } } } void GameSpyBuddyMessageQueue::endThread( void ) { if (m_thread) delete m_thread; m_thread = NULL; } Bool GameSpyBuddyMessageQueue::isThreadRunning( void ) { return (m_thread) ? m_thread->Is_Running() : false; } Bool GameSpyBuddyMessageQueue::isConnected( void ) { return (m_thread) ? m_thread->isConnected() : false; } Bool GameSpyBuddyMessageQueue::isConnecting( void ) { return (m_thread) ? m_thread->isConnecting() : false; } void GameSpyBuddyMessageQueue::addRequest( const BuddyRequest& req ) { MutexClass::LockClass m(m_requestMutex); if (m.Failed()) return; m_requests.push(req); } Bool GameSpyBuddyMessageQueue::getRequest( BuddyRequest& req ) { MutexClass::LockClass m(m_requestMutex, 0); if (m.Failed()) return false; if (m_requests.empty()) return false; req = m_requests.front(); m_requests.pop(); return true; } void GameSpyBuddyMessageQueue::addResponse( const BuddyResponse& resp ) { MutexClass::LockClass m(m_responseMutex); if (m.Failed()) return; m_responses.push(resp); } Bool GameSpyBuddyMessageQueue::getResponse( BuddyResponse& resp ) { MutexClass::LockClass m(m_responseMutex, 0); if (m.Failed()) return false; if (m_responses.empty()) return false; resp = m_responses.front(); m_responses.pop(); return true; } BuddyThreadClass* GameSpyBuddyMessageQueue::getThread( void ) { return m_thread; } GPProfile GameSpyBuddyMessageQueue::getLocalProfileID( void ) { return (m_thread) ? m_thread->getLocalProfileID() : 0; } //------------------------------------------------------------------------- void BuddyThreadClass::Thread_Function() { try { _set_se_translator( DumpExceptionInfo ); // Hook that allows stack trace. GPConnection gpCon; GPConnection *con = &gpCon; gpInitialize( con, 0 ); m_isConnected = m_isConnecting = false; gpSetCallback( con, GP_ERROR, callbackWrapper, (void *)CALLBACK_ERROR ); gpSetCallback( con, GP_RECV_BUDDY_MESSAGE, callbackWrapper, (void *)CALLBACK_RECVMESSAGE ); gpSetCallback( con, GP_RECV_BUDDY_REQUEST, callbackWrapper, (void *)CALLBACK_RECVREQUEST ); gpSetCallback( con, GP_RECV_BUDDY_STATUS, callbackWrapper, (void *)CALLBACK_RECVSTATUS ); GPEnum lastStatus = GP_OFFLINE; std::string lastStatusString; BuddyRequest incomingRequest; while ( running ) { // deal with requests if (TheGameSpyBuddyMessageQueue->getRequest(incomingRequest)) { switch (incomingRequest.buddyRequestType) { case BuddyRequest::BUDDYREQUEST_LOGIN: m_isConnecting = true; m_nick = incomingRequest.arg.login.nick; m_email = incomingRequest.arg.login.email; m_pass = incomingRequest.arg.login.password; m_isConnected = (gpConnect( con, incomingRequest.arg.login.nick, incomingRequest.arg.login.email, incomingRequest.arg.login.password, (incomingRequest.arg.login.hasFirewall)?GP_FIREWALL:GP_NO_FIREWALL, GP_BLOCKING, callbackWrapper, (void *)CALLBACK_CONNECT ) == GP_NO_ERROR); m_isConnecting = false; break; case BuddyRequest::BUDDYREQUEST_RELOGIN: m_isConnecting = true; m_isConnected = (gpConnect( con, m_nick.c_str(), m_email.c_str(), m_pass.c_str(), GP_FIREWALL, GP_BLOCKING, callbackWrapper, (void *)CALLBACK_CONNECT ) == GP_NO_ERROR); m_isConnecting = false; break; case BuddyRequest::BUDDYREQUEST_DELETEACCT: m_isdeleting = true; gpDeleteProfile( con ); break; case BuddyRequest::BUDDYREQUEST_LOGOUT: m_isConnecting = m_isConnected = false; gpDisconnect( con ); break; case BuddyRequest::BUDDYREQUEST_MESSAGE: { std::string s = WideCharStringToMultiByte( incomingRequest.arg.message.text ); DEBUG_LOG(("Sending a buddy message to %d [%s]\n", incomingRequest.arg.message.recipient, s.c_str())); gpSendBuddyMessage( con, incomingRequest.arg.message.recipient, s.c_str() ); } break; case BuddyRequest::BUDDYREQUEST_LOGINNEW: { m_isConnecting = true; m_nick = incomingRequest.arg.login.nick; m_email = incomingRequest.arg.login.email; m_pass = incomingRequest.arg.login.password; m_isNewAccount = TRUE; m_isConnected = (gpConnectNewUser( con, incomingRequest.arg.login.nick, incomingRequest.arg.login.email, incomingRequest.arg.login.password, (incomingRequest.arg.login.hasFirewall)?GP_FIREWALL:GP_NO_FIREWALL, GP_BLOCKING, callbackWrapper, (void *)CALLBACK_CONNECT ) == GP_NO_ERROR); if (m_isNewAccount) // if we didn't re-login { gpSetInfoMask( con, GP_MASK_NONE ); // don't share info } m_isConnecting = false; } break; case BuddyRequest::BUDDYREQUEST_ADDBUDDY: { std::string s = WideCharStringToMultiByte( incomingRequest.arg.addbuddy.text ); gpSendBuddyRequest( con, incomingRequest.arg.addbuddy.id, s.c_str() ); } break; case BuddyRequest::BUDDYREQUEST_DELBUDDY: { gpDeleteBuddy( con, incomingRequest.arg.profile.id ); } break; case BuddyRequest::BUDDYREQUEST_OKADD: { gpAuthBuddyRequest( con, incomingRequest.arg.profile.id ); } break; case BuddyRequest::BUDDYREQUEST_DENYADD: { gpDenyBuddyRequest( con, incomingRequest.arg.profile.id ); } case BuddyRequest::BUDDYREQUEST_SETSTATUS: { //don't blast our 'Loading' status with 'Online'. if (lastStatus == GP_PLAYING && lastStatusString == "Loading" && incomingRequest.arg.status.status == GP_ONLINE) break; DEBUG_LOG(("BUDDYREQUEST_SETSTATUS: status is now %d:%s/%s\n", incomingRequest.arg.status.status, incomingRequest.arg.status.statusString, incomingRequest.arg.status.locationString)); gpSetStatus( con, incomingRequest.arg.status.status, incomingRequest.arg.status.statusString, incomingRequest.arg.status.locationString ); lastStatus = incomingRequest.arg.status.status; lastStatusString = incomingRequest.arg.status.statusString; } break; } } // update the network GPEnum isConnected = GP_CONNECTED; GPResult res = GP_NO_ERROR; res = gpIsConnected( con, &isConnected ); if ( isConnected == GP_CONNECTED && res == GP_NO_ERROR ) gpProcess( con ); // end our timeslice Switch_Thread(); } gpDestroy( con ); } catch ( ... ) { DEBUG_CRASH(("Exception in buddy thread!")); } } void BuddyThreadClass::errorCallback( GPConnection *con, GPErrorArg *arg ) { // log the error DEBUG_LOG(("GPErrorCallback\n")); m_lastErrorCode = arg->errorCode; char errorCodeString[256]; char resultString[256]; #define RESULT(x) case x: strcpy(resultString, #x); break; switch(arg->result) { RESULT(GP_NO_ERROR) RESULT(GP_MEMORY_ERROR) RESULT(GP_PARAMETER_ERROR) RESULT(GP_NETWORK_ERROR) RESULT(GP_SERVER_ERROR) default: strcpy(resultString, "Unknown result!"); } #undef RESULT #define ERRORCODE(x) case x: strcpy(errorCodeString, #x); break; switch(arg->errorCode) { ERRORCODE(GP_GENERAL) ERRORCODE(GP_PARSE) ERRORCODE(GP_NOT_LOGGED_IN) ERRORCODE(GP_BAD_SESSKEY) ERRORCODE(GP_DATABASE) ERRORCODE(GP_NETWORK) ERRORCODE(GP_FORCED_DISCONNECT) ERRORCODE(GP_CONNECTION_CLOSED) ERRORCODE(GP_LOGIN) ERRORCODE(GP_LOGIN_TIMEOUT) ERRORCODE(GP_LOGIN_BAD_NICK) ERRORCODE(GP_LOGIN_BAD_EMAIL) ERRORCODE(GP_LOGIN_BAD_PASSWORD) ERRORCODE(GP_LOGIN_BAD_PROFILE) ERRORCODE(GP_LOGIN_PROFILE_DELETED) ERRORCODE(GP_LOGIN_CONNECTION_FAILED) ERRORCODE(GP_LOGIN_SERVER_AUTH_FAILED) ERRORCODE(GP_NEWUSER) ERRORCODE(GP_NEWUSER_BAD_NICK) ERRORCODE(GP_NEWUSER_BAD_PASSWORD) ERRORCODE(GP_UPDATEUI) ERRORCODE(GP_UPDATEUI_BAD_EMAIL) ERRORCODE(GP_NEWPROFILE) ERRORCODE(GP_NEWPROFILE_BAD_NICK) ERRORCODE(GP_NEWPROFILE_BAD_OLD_NICK) ERRORCODE(GP_UPDATEPRO) ERRORCODE(GP_UPDATEPRO_BAD_NICK) ERRORCODE(GP_ADDBUDDY) ERRORCODE(GP_ADDBUDDY_BAD_FROM) ERRORCODE(GP_ADDBUDDY_BAD_NEW) ERRORCODE(GP_ADDBUDDY_ALREADY_BUDDY) ERRORCODE(GP_AUTHADD) ERRORCODE(GP_AUTHADD_BAD_FROM) ERRORCODE(GP_AUTHADD_BAD_SIG) ERRORCODE(GP_STATUS) ERRORCODE(GP_BM) ERRORCODE(GP_BM_NOT_BUDDY) ERRORCODE(GP_GETPROFILE) ERRORCODE(GP_GETPROFILE_BAD_PROFILE) ERRORCODE(GP_DELBUDDY) ERRORCODE(GP_DELBUDDY_NOT_BUDDY) ERRORCODE(GP_DELPROFILE) ERRORCODE(GP_DELPROFILE_LAST_PROFILE) ERRORCODE(GP_SEARCH) ERRORCODE(GP_SEARCH_CONNECTION_FAILED) default: strcpy(errorCodeString, "Unknown error code!"); } #undef ERRORCODE if(arg->fatal) { DEBUG_LOG(( "-----------\n")); DEBUG_LOG(( "GP FATAL ERROR\n")); DEBUG_LOG(( "-----------\n")); } else { DEBUG_LOG(( "-----\n")); DEBUG_LOG(( "GP ERROR\n")); DEBUG_LOG(( "-----\n")); } DEBUG_LOG(( "RESULT: %s (%d)\n", resultString, arg->result)); DEBUG_LOG(( "ERROR CODE: %s (0x%X)\n", errorCodeString, arg->errorCode)); DEBUG_LOG(( "ERROR STRING: %s\n", arg->errorString)); if (arg->fatal == GP_FATAL) { BuddyResponse errorResponse; errorResponse.buddyResponseType = BuddyResponse::BUDDYRESPONSE_DISCONNECT; errorResponse.result = arg->result; errorResponse.arg.error.errorCode = arg->errorCode; errorResponse.arg.error.fatal = arg->fatal; strncpy(errorResponse.arg.error.errorString, arg->errorString, MAX_BUDDY_CHAT_LEN); errorResponse.arg.error.errorString[MAX_BUDDY_CHAT_LEN-1] = 0; m_isConnecting = m_isConnected = false; TheGameSpyBuddyMessageQueue->addResponse( errorResponse ); if (m_isdeleting) { PeerRequest req; req.peerRequestType = PeerRequest::PEERREQUEST_LOGOUT; TheGameSpyPeerMessageQueue->addRequest( req ); m_isdeleting = false; } } } static void getNickForMessage( GPConnection *con, GPGetInfoResponseArg *arg, void *param ) { BuddyResponse *resp = (BuddyResponse *)param; strcpy(resp->arg.message.nick, arg->nick); } void BuddyThreadClass::messageCallback( GPConnection *con, GPRecvBuddyMessageArg *arg ) { BuddyResponse messageResponse; messageResponse.buddyResponseType = BuddyResponse::BUDDYRESPONSE_MESSAGE; messageResponse.profile = arg->profile; // get info about the person asking to be our buddy gpGetInfo( con, arg->profile, GP_CHECK_CACHE, GP_BLOCKING, (GPCallback)getNickForMessage, &messageResponse); std::wstring s = MultiByteToWideCharSingleLine( arg->message ); wcsncpy(messageResponse.arg.message.text, s.c_str(), MAX_BUDDY_CHAT_LEN); messageResponse.arg.message.text[MAX_BUDDY_CHAT_LEN-1] = 0; messageResponse.arg.message.date = arg->date; DEBUG_LOG(("Got a buddy message from %d [%ls]\n", arg->profile, s.c_str())); TheGameSpyBuddyMessageQueue->addResponse( messageResponse ); } void BuddyThreadClass::connectCallback( GPConnection *con, GPConnectResponseArg *arg ) { BuddyResponse loginResponse; if (arg->result == GP_NO_ERROR) { loginResponse.buddyResponseType = BuddyResponse::BUDDYRESPONSE_LOGIN; loginResponse.result = arg->result; loginResponse.profile = arg->profile; TheGameSpyBuddyMessageQueue->addResponse( loginResponse ); m_profileID = arg->profile; if (!TheGameSpyPeerMessageQueue->isConnected() && !TheGameSpyPeerMessageQueue->isConnecting()) { DEBUG_LOG(("Buddy connect: trying chat connect\n")); PeerRequest req; req.peerRequestType = PeerRequest::PEERREQUEST_LOGIN; req.nick = m_nick.c_str(); req.password = m_pass; req.email = m_email; req.login.profileID = arg->profile; TheGameSpyPeerMessageQueue->addRequest(req); } } else { if (!TheGameSpyPeerMessageQueue->isConnected() && !TheGameSpyPeerMessageQueue->isConnecting()) { if (m_lastErrorCode == GP_NEWUSER_BAD_NICK) { m_isNewAccount = FALSE; // they just hit 'create account' instead of 'log in'. Fix them. DEBUG_LOG(("User Error: Create Account instead of Login. Fixing them...\n")); BuddyRequest req; req.buddyRequestType = BuddyRequest::BUDDYREQUEST_LOGIN; strcpy(req.arg.login.nick, m_nick.c_str()); strcpy(req.arg.login.email, m_email.c_str()); strcpy(req.arg.login.password, m_pass.c_str()); req.arg.login.hasFirewall = true; TheGameSpyBuddyMessageQueue->addRequest( req ); return; } DEBUG_LOG(("Buddy connect failed (%d/%d): posting a failed chat connect\n", arg->result, m_lastErrorCode)); PeerResponse resp; resp.peerResponseType = PeerResponse::PEERRESPONSE_DISCONNECT; resp.discon.reason = DISCONNECT_COULDNOTCONNECT; switch (m_lastErrorCode) { case GP_LOGIN_TIMEOUT: resp.discon.reason = DISCONNECT_GP_LOGIN_TIMEOUT; break; case GP_LOGIN_BAD_NICK: resp.discon.reason = DISCONNECT_GP_LOGIN_BAD_NICK; break; case GP_LOGIN_BAD_EMAIL: resp.discon.reason = DISCONNECT_GP_LOGIN_BAD_EMAIL; break; case GP_LOGIN_BAD_PASSWORD: resp.discon.reason = DISCONNECT_GP_LOGIN_BAD_PASSWORD; break; case GP_LOGIN_BAD_PROFILE: resp.discon.reason = DISCONNECT_GP_LOGIN_BAD_PROFILE; break; case GP_LOGIN_PROFILE_DELETED: resp.discon.reason = DISCONNECT_GP_LOGIN_PROFILE_DELETED; break; case GP_LOGIN_CONNECTION_FAILED: resp.discon.reason = DISCONNECT_GP_LOGIN_CONNECTION_FAILED; break; case GP_LOGIN_SERVER_AUTH_FAILED: resp.discon.reason = DISCONNECT_GP_LOGIN_SERVER_AUTH_FAILED; break; case GP_NEWUSER_BAD_NICK: resp.discon.reason = DISCONNECT_GP_NEWUSER_BAD_NICK; break; case GP_NEWUSER_BAD_PASSWORD: resp.discon.reason = DISCONNECT_GP_NEWUSER_BAD_PASSWORD; break; case GP_NEWPROFILE_BAD_NICK: resp.discon.reason = DISCONNECT_GP_NEWPROFILE_BAD_NICK; break; case GP_NEWPROFILE_BAD_OLD_NICK: resp.discon.reason = DISCONNECT_GP_NEWPROFILE_BAD_OLD_NICK; break; } TheGameSpyPeerMessageQueue->addResponse(resp); } } } // ----------------------- static void getInfoResponseForRequest( GPConnection *con, GPGetInfoResponseArg *arg, void *param ) { BuddyResponse *resp = (BuddyResponse *)param; resp->profile = arg->profile; strcpy(resp->arg.request.nick, arg->nick); strcpy(resp->arg.request.email, arg->email); strcpy(resp->arg.request.countrycode, arg->countrycode); } void BuddyThreadClass::requestCallback( GPConnection *con, GPRecvBuddyRequestArg *arg ) { BuddyResponse response; response.buddyResponseType = BuddyResponse::BUDDYRESPONSE_REQUEST; response.profile = arg->profile; // get info about the person asking to be our buddy gpGetInfo( con, arg->profile, GP_CHECK_CACHE, GP_BLOCKING, (GPCallback)getInfoResponseForRequest, &response); std::wstring s = MultiByteToWideCharSingleLine( arg->reason ); wcsncpy(response.arg.request.text, s.c_str(), GP_REASON_LEN); response.arg.request.text[GP_REASON_LEN-1] = 0; TheGameSpyBuddyMessageQueue->addResponse( response ); } // ----------------------- static void getInfoResponseForStatus(GPConnection * connection, GPGetInfoResponseArg * arg, void * param) { BuddyResponse *resp = (BuddyResponse *)param; resp->profile = arg->profile; strcpy(resp->arg.status.nick, arg->nick); strcpy(resp->arg.status.email, arg->email); strcpy(resp->arg.status.countrycode, arg->countrycode); } void BuddyThreadClass::statusCallback( GPConnection *con, GPRecvBuddyStatusArg *arg ) { BuddyResponse response; // get user's name response.buddyResponseType = BuddyResponse::BUDDYRESPONSE_STATUS; gpGetInfo( con, arg->profile, GP_CHECK_CACHE, GP_BLOCKING, (GPCallback)getInfoResponseForStatus, &response); // get user's status GPBuddyStatus status; gpGetBuddyStatus( con, arg->index, &status ); strcpy(response.arg.status.location, status.locationString); strcpy(response.arg.status.statusString, status.statusString); response.arg.status.status = status.status; DEBUG_LOG(("Got buddy status for %d(%s) - status %d\n", status.profile, response.arg.status.nick, status.status)); // relay to UI TheGameSpyBuddyMessageQueue->addResponse( response ); } //-------------------------------------------------------------------------