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

#include "hsConfig.h"
#include "hsWindows.h"

#include "hsTypes.h"
#include "plSceneInputInterface.h"

#include "plInputInterfaceMgr.h"
#include "plInputManager.h"
#include "plInputDevice.h"

#include "plPhysical/plPickingDetector.h"
#include "plMessage/plInputEventMsg.h"
#include "plMessage/plLOSRequestMsg.h"
#include "plMessage/plLOSHitMsg.h"
#include "plMessage/plPickedMsg.h"
#include "plMessage/plRenderMsg.h"
#include "plMessage/plInputIfaceMgrMsg.h"
#include "plMessage/plVaultNotifyMsg.h"
#include "pnMessage/plFakeOutMsg.h"
#include "pnMessage/plNotifyMsg.h"
#include "pnMessage/plRemoteAvatarInfoMsg.h"
#include "pnMessage/plCursorChangeMsg.h"
#include "pnMessage/plCameraMsg.h"
#include "pnMessage/plPlayerPageMsg.h"
#include "pnMessage/plCmdIfaceModMsg.h"
#include "plAvatar/plArmatureMod.h"
#include "plAvatar/plAvBrain.h"
#include "plAvatar/plAvatarMgr.h"
#include "plAvatar/plAvCallbackAction.h"
#include "plModifier/plInterfaceInfoModifier.h"
#include "pnModifier/plLogicModBase.h"
#include "plVault/plVault.h"
#include "plNetClient/plNetClientMgr.h"
#include "plNetClient/plNetLinkingMgr.h"
#include "plNetCommon/plNetServerSessionInfo.h"
#include "plNetTransport/plNetTransport.h"
#include "plNetTransport/plNetTransportMember.h"
#include "pnSceneObject/plSceneObject.h"
#include "pnSceneObject/plCoordinateInterface.h"
#include "pnKeyedObject/plKey.h"
#include "pnKeyedObject/plFixedKey.h"
#include "pnInputCore/plKeyMap.h"
#include "plPhysical.h"

#include "plgDispatch.h"
#include "plPipeline.h"

#include "plModifier/plDetectorLog.h"


#define ID_FIND_CLICKABLE 2
#define ID_FIND_LOCALPLAYER 3
#define ID_FIND_WALKABLE_GROUND 4

#define SHARE_FACING_TOLERANCE -0.70f       // 45 degrees

plSceneInputInterface *plSceneInputInterface::fInstance = nil;

hsBool plSceneInputInterface::fShowLOS = false;


//// Constructor/Destructor //////////////////////////////////////////////////

plSceneInputInterface::plSceneInputInterface()
{
    fPipe = nil;
    fSpawnPoint = nil;
    GuidClear(&fAgeInstanceGuid);
    fInstance = this;
    SetEnabled( true );         // Always enabled
}

plSceneInputInterface::~plSceneInputInterface()
{
    ClearClickableMap();
    fIgnoredAvatars.Reset();
    fLocalIgnoredAvatars.Reset();
    fGUIIgnoredAvatars.Reset();
    fInstance = nil;
}


//// Init/Shutdown ///////////////////////////////////////////////////////////

void    plSceneInputInterface::Init( plInputInterfaceMgr *manager )
{
    plInputInterface::Init( manager );

    // To get the pipeline
    fPipe = nil;
    plgDispatch::Dispatch()->RegisterForExactType( plRenderMsg::Index(), fManager->GetKey() );
    plgDispatch::Dispatch()->RegisterForExactType( plInputIfaceMgrMsg::Index(), fManager->GetKey() );
    plgDispatch::Dispatch()->RegisterForExactType( plPlayerPageMsg::Index(), fManager->GetKey() );

    fCurrentClickable = nil;
    fCurrentClickableLogicMod = nil;
    fLastClicked = nil;
    fButtonState = 0;
    fClickability = 1;      // Hack for clickable avatars, we need to always do the LOS check
    fLastClickIsAvatar = false;
    fCurrClickIsAvatar = false;
    fFadedLocalAvatar = false;
    fBookMode = kNotOffering;
    fOffereeKey = nil;
    fPendingLink = false;

    // register for control messages
    plCmdIfaceModMsg* pModMsg = TRACKED_NEW plCmdIfaceModMsg;
    pModMsg->SetBCastFlag(plMessage::kBCastByExactType);
    pModMsg->SetSender(fManager->GetKey());
    pModMsg->SetCmd(plCmdIfaceModMsg::kAdd);
    plgDispatch::MsgSend(pModMsg);

}

void    plSceneInputInterface::Shutdown( void )
{
    if( fPipe == nil )
        plgDispatch::Dispatch()->UnRegisterForExactType( plRenderMsg::Index(), fManager->GetKey() );
    else
        fPipe = nil;
}

void plSceneInputInterface::ClearClickableMap()
{
    for (int i = 0; i < fClickableMap.Count(); i++)
    {
        clickableTest* pTest = fClickableMap[i];
        delete(pTest);
    }
    fClickableMap.SetCountAndZero(0);
}

//// IHalfFadeAvatar /////////////////////////////////////////////////////////

void plSceneInputInterface::IHalfFadeAvatar(hsBool out)
{
    plIfaceFadeAvatarMsg* pMsg = TRACKED_NEW plIfaceFadeAvatarMsg();
    pMsg->SetSubjectKey(plNetClientMgr::GetInstance()->GetLocalPlayerKey());
    pMsg->SetBCastFlag(plMessage::kBCastByExactType);
    pMsg->SetBCastFlag(plMessage::kNetPropagate, FALSE);
    pMsg->SetFadeOut(out);
    pMsg->Send();
    fFadedLocalAvatar = out;
    
}


void plSceneInputInterface::ResetClickableState()
{
    if( fLastClicked != nil )
        ISetLastClicked( nil, hsPoint3(0,0,0) );
    
    ClearClickableMap();
    fCurrentClickable = nil;
    fCurrentClickableLogicMod = nil;
    fCurrentCursor = SetCurrentCursorID(kNullCursor);
    fCurrClickIsAvatar = false;
    
}
//// IEval ///////////////////////////////////////////////////////////////////

hsBool plSceneInputInterface::IEval( double secs, hsScalar del, UInt32 dirty )
{
    // this needs to always go no matter what...
    // ...unless we have cliclability disabled (as in the case of certain multistage behaviors)
    if (plMouseDevice::Instance()->GetCursorOpacity() > 0.f && !plMouseDevice::Instance()->GetHideCursor())
    {
        IRequestLOSCheck( plMouseDevice::Instance()->GetCursorX(), plMouseDevice::Instance()->GetCursorY(), ID_FIND_LOCALPLAYER );
        if (fClickability)
            IRequestLOSCheck( plMouseDevice::Instance()->GetCursorX(), plMouseDevice::Instance()->GetCursorY(), ID_FIND_CLICKABLE );
    }
    else
    if (fFadedLocalAvatar)
        IHalfFadeAvatar(false);
    
//  if (!fCurrClickIsAvatar)
//      fCurrentCursor = SetCurrentCursorID(kNullCursor);
    // ping for possible cursor changes
    int i;
    for (i=0; i < fClickableMap.Count(); i++)
    {
        plFakeOutMsg *pMsg = TRACKED_NEW plFakeOutMsg;
        pMsg->SetSender( fManager->GetKey() );
        pMsg->AddReceiver( fClickableMap[i]->key );
        plgDispatch::MsgSend( pMsg );
    }
    // then see if we have any
    hsBool change = false;
    for (i=0; i < fClickableMap.Count(); i++)
    {
        if( fClickableMap[i]->val )
        {
            change = true;
            break;
        }
    }
    if (change)
    {
        if( fLastClicked != nil )
            fCurrentCursor = SetCurrentCursorID(kCursorClicked);
        else
            fCurrentCursor = SetCurrentCursorID(kCursorPoised);
    }
    return true;
}

//// MsgReceive //////////////////////////////////////////////////////////////

hsBool  plSceneInputInterface::MsgReceive( plMessage *msg )
{
    plLOSHitMsg *pLOSMsg = plLOSHitMsg::ConvertNoRef( msg );
    if( pLOSMsg )
    {
        if( pLOSMsg->fRequestID == ID_FIND_CLICKABLE )
        {
            hsBool clearCursor = false;
            if (!fClickability)
                return true;
            if( pLOSMsg->fObj )
            {
                // is this object clickable?
                plSceneObject *pObj = plSceneObject::ConvertNoRef( pLOSMsg->fObj->ObjectIsLoaded() );
                if( pObj )
                {
                    if (fShowLOS)
                    {
                        if (pLOSMsg->fNoHit)
                            DetectorLogSpecial("%s: LOS miss", pObj->GetKeyName());
                        else
                            DetectorLogSpecial("%s: LOS hit", pObj->GetKeyName());
                    }
                    int i;
                    const plInterfaceInfoModifier* pMod = 0;
                    for( i = 0; i < pObj->GetNumModifiers(); i++ )
                    {
                        if (fBookMode == kNotOffering) // when sharing a book we don't care about other clickables
                        {
                            pMod = plInterfaceInfoModifier::ConvertNoRef( pObj->GetModifier(i) );
                            if (pMod) // we found our list, stop here
                            {
                                plLogicModBase* pLogicMod = (plLogicModBase*)pObj->GetModifierByType(plLogicModBase::Index());
                                if (!pLogicMod)
                                    return true;

                                if (fCurrentClickable != pObj->GetKey())
                                {   // is it the current clickable already?
                                    ClearClickableMap();
                                    fCurrentCursor = SetCurrentCursorID(kNullCursor);
                                    fCurrentClickable = pObj->GetKey();
                                    fCurrentClickableLogicMod = pLogicMod->GetKey();
                                    fCurrentClickPoint = pLOSMsg->fHitPoint;
                                    for (int x = 0; x < pMod->GetNumReferencedKeys(); x++)
                                        fClickableMap.Append( TRACKED_NEW clickableTest(pMod->GetReferencedKey(x)));
                                }
                                else
                                {
                                    // even if this is still the same clickable object, the cursor could be
                                    // ...at a different spot on the clickable, so save that
                                    fCurrentClickPoint = pLOSMsg->fHitPoint;
                                }
                                fCurrClickIsAvatar = false;
                                return true;
                            }
                        }
                        
                        // see if it is an avatar 
                        plArmatureMod* armMod = (plArmatureMod*)plArmatureMod::ConvertNoRef( pObj->GetModifier(i));
                        if (armMod)
                        {
                            if (armMod->IsMidLink())
                                return true;

                            // okay, are we a CCR?
                            hsBool amCCR = plNetClientMgr::GetInstance()->GetCCRLevel();
                            
                            // is this person a NPC or CCR?
                            int mbrIdx=plNetClientMgr::GetInstance()->TransportMgr().FindMember(pObj->GetKey());
                            plNetTransportMember* pMbr = plNetClientMgr::GetInstance()->TransportMgr().GetMember(mbrIdx);
                            if (!pMbr) // whoops - it's a freakin' NPC !
                                return true;
                            
                            if (pMbr->IsCCR())
                            {
                                if (amCCR)
                                {
                                    // we can click on them 
                                    plMouseDevice::AddCCRToCursor();
                                }
                                else
                                {
                                    // nope
                                    return true;
                                }
                                
                            }
                            // now, if I am a CCR, let me click on anyone at any time
                            if (amCCR)
                            {
                                ClearClickableMap();
                                fCurrentClickable = pObj->GetKey();
                                fCurrentClickableLogicMod = nil;
                                fCurrClickIsAvatar = true;
                                fCurrentCursor = SetCurrentCursorID(kCursorPoised);
                                // not sure why we need to point on the avatar...
                                // ...but maybe something in the future will need this
                                fCurrentClickPoint = pLOSMsg->fHitPoint;
                                plMouseDevice::AddNameToCursor(plNetClientMgr::GetInstance()->GetPlayerName(fCurrentClickable));
                                // also add their player ID to the cursor
                                plMouseDevice::AddIDNumToCursor(pMbr->GetPlayerID());
                                return true;
                            }
                            // otherwise, cull people as necessary
                            // also make sure that they are not in our ignore list
                            else if (VaultAmIgnoringPlayer( pMbr->GetPlayerID()))
                                return true;
                            // further, if we are offering a book, only allow clicks on the person
                            // whom we've already offered it to (to cancel it)
                            else if (fBookMode == kBookOffered && pObj->GetKey() != fOffereeKey)
                                return true;
                            // within distance
                            // also... make sure they aren't off climbing a ladder or looking at their KI
                            else if (fBookMode == kOfferBook)
                            {
                                plArmatureBrain* curBrain = armMod->GetCurrentBrain();
                                if (curBrain)
                                {
                                    if (curBrain->IsRunningTask())
                                    {
                                        fCurrentCursor = SetCurrentCursorID(kCursorClickDisabled);
                                        plMouseDevice::AddNameToCursor(plNetClientMgr::GetInstance()->GetPlayerName(pObj->GetKey()));
                                        return true;
                                    }
                                }
                                plAvatarMgr* aMgr = plAvatarMgr::GetInstance();
                                if (aMgr)
                                {
                                    if (aMgr->IsACoopRunning())
                                    {
                                        fCurrentCursor = SetCurrentCursorID(kCursorClickDisabled);
                                        plMouseDevice::AddNameToCursor(plNetClientMgr::GetInstance()->GetPlayerName(pObj->GetKey()));
                                        return true;
                                    }
                                }
                                plSceneObject* locPlayer = (plSceneObject*)plNetClientMgr::GetInstance()->GetLocalPlayer();
                                // make sure that they are facing each other
                                if ( locPlayer )
                                {
                                    hsVector3 ourView = locPlayer->GetCoordinateInterface()->GetLocalToWorld().GetAxis(hsMatrix44::kView);
                                    hsVector3 theirView = pObj->GetCoordinateInterface()->GetLocalToWorld().GetAxis(hsMatrix44::kView);
                                    hsScalar viewdot = ourView * theirView;
                                    hsVector3 towards(locPlayer->GetCoordinateInterface()->GetLocalToWorld().GetTranslate() - pObj->GetCoordinateInterface()->GetLocalToWorld().GetTranslate());
                                    towards.Normalize();
                                    hsScalar towardsdot = ourView * towards;
                                    if (viewdot > SHARE_FACING_TOLERANCE || towardsdot > SHARE_FACING_TOLERANCE )
                                    {
                                        ResetClickableState();
                                        return true;    // not facing enough... reject
                                    }
                                }
                                //otherwise make sure that they are close enough to click on
                                if (locPlayer)
                                {
                                    hsPoint3 avPt = locPlayer->GetCoordinateInterface()->GetLocalToWorld().GetTranslate();
                                    hsPoint3 objPt = pObj->GetCoordinateInterface()->GetLocalToWorld().GetTranslate();
                                    hsVector3 dist(avPt - objPt);
                                    if ( dist.MagnitudeSquared() >= 16.0f ) // you are too far away
                                    {
                                        ResetClickableState();
                                        return true;
                                    }
                                    
                                    if (hsABS(avPt.fZ - objPt.fZ) > 1.0f) // you need to also be in the same plane (some books are on top of rocks you need to jump onto)
                                    {
                                        ResetClickableState();
                                        return true;
                                    }
                                }
                            }
                            // finally - make sure this guy is not in our ignore lists.
                            int x;
                            for (x = 0; x < fIgnoredAvatars.Count(); x++)
                            {
                                if (fIgnoredAvatars[x] == pObj->GetKey())
                                {   
                                    fCurrentCursor = SetCurrentCursorID(kCursorClickDisabled);
                                    plMouseDevice::AddNameToCursor(plNetClientMgr::GetInstance()->GetPlayerName(pObj->GetKey()));
                                    return true;
                                }
                            }
                            for (x = 0; x < fGUIIgnoredAvatars.Count(); x++)
                            {
                                if (fGUIIgnoredAvatars[x] == pObj->GetKey())
                                {   
                                    fCurrentCursor = SetCurrentCursorID(kCursorClickDisabled);
                                    plMouseDevice::AddNameToCursor(plNetClientMgr::GetInstance()->GetPlayerName(pObj->GetKey()));
                                    return true;
                                }
                            }
                            
                            ClearClickableMap();
                            fCurrentClickable = pObj->GetKey();
                            fCurrentClickableLogicMod = nil;
                            fCurrClickIsAvatar = true;
                            fCurrentCursor = SetCurrentCursorID(kCursorPoised);
                            // not sure why we need to point on the avatar...
                            // ...but maybe something in the future will need this
                            fCurrentClickPoint = pLOSMsg->fHitPoint;
                            plMouseDevice::AddNameToCursor(plNetClientMgr::GetInstance()->GetPlayerName(fCurrentClickable));
                            return true;
                        }
                    }
                    // here! it's an object which is not clickable
                    // no object, or not clickable or avatar
                    
                    fCurrentClickPoint = pLOSMsg->fHitPoint;
                    ResetClickableState();
                    return false;
                }
            }
            // no object, or not clickable or avatar
            ResetClickableState();
        }
        else
        if( pLOSMsg->fRequestID == ID_FIND_LOCALPLAYER )
        {
            bool result = false;
            if( pLOSMsg->fObj )
            {
                // is this object clickable?
                plSceneObject *pObj = plSceneObject::ConvertNoRef( pLOSMsg->fObj->ObjectIsLoaded() );
                if( pObj )
                {   
                    if (pObj == plNetClientMgr::GetInstance()->GetLocalPlayer())
                        result = true;
                }   
            }
            if (result && !fFadedLocalAvatar)
            {   
                IHalfFadeAvatar(true);
                return true;
            }
            else
            if (!result && fFadedLocalAvatar)
            {
                IHalfFadeAvatar(false);
                return true;
            }
        }
        if( pLOSMsg->fRequestID == ID_FIND_WALKABLE_GROUND )
        {
            if (!pLOSMsg->fNoHit)
            {
                plAvatarMgr::GetInstance()->GetLocalAvatar()->TurnToPoint(pLOSMsg->fHitPoint);
            }
            return true;
        }
            
        return true;
    }

    plCursorChangeMsg   *fakeReplyMsg = plCursorChangeMsg::ConvertNoRef( msg );
    if( fakeReplyMsg != nil )
    {
        hsBool deniedCurrent = false;
        plKey key = fakeReplyMsg->GetSender();
        for (int i = 0; i < fClickableMap.Count(); i++)
        {
            if (fClickableMap[i]->key == key)
            {
                if( fakeReplyMsg->fType == plCursorChangeMsg::kNullCursor )
                {
                    // Means not clickable--gotta fix this someday
                    fClickableMap[i]->val = false;
                    if (fClickableMap[i]->key == fCurrentClickableLogicMod)
                    {
                        deniedCurrent = true;
                        break;
                    }
                }
                else
                {
                    // And fix this...
                    fClickableMap[i]->val = true;
                }
            }
        }
        if (deniedCurrent)
            ResetClickableState();
        return true;
    }

    plRenderMsg *rMsg = plRenderMsg::ConvertNoRef( msg );
    if( rMsg != nil )
    {
        fPipe = rMsg->Pipeline();
        plgDispatch::Dispatch()->UnRegisterForExactType( plRenderMsg::Index(), fManager->GetKey() );
        return true;
    }
    // reply from coop share book multistage
    plNotifyMsg* pNMsg = plNotifyMsg::ConvertNoRef(msg);
    if (pNMsg)
    {
        for(int x=0; x < pNMsg->GetEventCount();x++)
        {
            proEventData* pED = pNMsg->GetEventRecord(0);
            if ( pED->fEventType == proEventData::kMultiStage )
            {
                proMultiStageEventData* pMS = (proMultiStageEventData*)pED;
                if (pMS->fAvatar == fOffereeKey) // mojo has linked
                {
                    // do something - they linked out but we are still in the multistage
                    fOffereeKey = nil;
                }
                else
                if (pMS->fAvatar == plNetClientMgr::GetInstance()->GetLocalPlayerKey())
                {
                    // do something else
                    if (fBookMode = kNotOffering && fPendingLink == false) // we just linked out
                    {
                        // make me clickable again
                        ISendAvatarDisabledNotification(true);
                    }
                    else // we put the book back after our target linked out
                    {
                        fBookMode = kNotOffering;
                        fPendingLink = false;
                        // make ME clickable again
                        ISendAvatarDisabledNotification(true);
                    }
                }   
                return true;
            }
        }
        return false;
                
    }
    // if someone pages out / in, remove them from our ignore list or notify them to ignore us
    plPlayerPageMsg* pPlayerMsg = plPlayerPageMsg::ConvertNoRef(msg);
    if (pPlayerMsg)
    {
        if (pPlayerMsg->fUnload)
        {
            int x;
            // first, remove this avatar from my list of avatars I ingore for clickable griefing (when the 'ignore avatars' key is pressed)
            for(x = 0; x < fLocalIgnoredAvatars.Count(); x++)
            {
                if (fLocalIgnoredAvatars[x] == pPlayerMsg->fPlayer)
                    fLocalIgnoredAvatars.RemoveItem(pPlayerMsg->fPlayer);
            }
            // now deal with avatars we are always ignoring because of their current activity
            for(x = 0; x < fIgnoredAvatars.Count(); x++)
            {
                if (fIgnoredAvatars[x] == pPlayerMsg->fPlayer)
                    fIgnoredAvatars.RemoveItem(pPlayerMsg->fPlayer);
            }
            for(x = 0; x < fGUIIgnoredAvatars.Count(); x++)
            {
                if (fGUIIgnoredAvatars[x] == pPlayerMsg->fPlayer)
                    fGUIIgnoredAvatars.RemoveItem(pPlayerMsg->fPlayer);
            }
            if (fOffereeKey == pPlayerMsg->fPlayer)
            {
                if (fBookMode == kBookOffered)
                {
                    // and put our own dialog back up...
                    ISendOfferNotification(plNetClientMgr::GetInstance()->GetLocalPlayerKey(), 0, false);
                    //IManageIgnoredAvatars(fOffereeKey, false);
                    fOffereeKey = nil;
                    fBookMode = kNotOffering;
                    ISendAvatarDisabledNotification(true);
                }
            }
        }
        else
        {   
            // add them to the list we keep of everyone here:
            // but DO NOT add the local avatar
            if (pPlayerMsg->fPlayer != plNetClientMgr::GetInstance()->GetLocalPlayerKey())
                fLocalIgnoredAvatars.Append(pPlayerMsg->fPlayer);
            if (fBookMode != kNotOffering)
            {
                // tell them to ignore us
                
                plInputIfaceMgrMsg* pMsg = TRACKED_NEW plInputIfaceMgrMsg(plInputIfaceMgrMsg::kDisableAvatarClickable);
                pMsg->SetAvKey(plNetClientMgr::GetInstance()->GetLocalPlayerKey());
                pMsg->SetBCastFlag(plMessage::kNetPropagate);
                pMsg->SetBCastFlag(plMessage::kNetForce);
                pMsg->SetBCastFlag(plMessage::kLocalPropagate, false);
                pMsg->AddNetReceiver( pPlayerMsg->fClientID );
                pMsg->Send();
                
                // and tell them to ignore our victim

                //plInputIfaceMgrMsg* pMsg2 = TRACKED_NEW plInputIfaceMgrMsg(plInputIfaceMgrMsg::kDisableAvatarClickable);
                //pMsg2->SetAvKey(fOffereeKey);
                //pMsg2->SetBCastFlag(plMessage::kNetPropagate);
                //pMsg2->SetBCastFlag(plMessage::kNetForce);
                //pMsg2->SetBCastFlag(plMessage::kLocalPropagate, false);
                //pMsg2->AddNetReceiver( pPlayerMsg->fClientID );
                //pMsg2->Send();
                            
            }
            // tell them to ingore us if we are looking at a GUI
            for(int x = 0; x < fGUIIgnoredAvatars.Count(); x++)
            {
                if (fGUIIgnoredAvatars[x] == plNetClientMgr::GetInstance()->GetLocalPlayerKey())
                {
                    plInputIfaceMgrMsg* pMsg3 = TRACKED_NEW plInputIfaceMgrMsg(plInputIfaceMgrMsg::kGUIDisableAvatarClickable);
                    pMsg3->SetAvKey(fGUIIgnoredAvatars[x]);
                    pMsg3->SetBCastFlag(plMessage::kNetPropagate);
                    pMsg3->SetBCastFlag(plMessage::kNetForce);
                    pMsg3->SetBCastFlag(plMessage::kLocalPropagate, false);
                    pMsg3->AddNetReceiver( pPlayerMsg->fClientID );
                    pMsg3->Send();
                    return true;
                }
            }
        }
    }
    
    plInputIfaceMgrMsg *mgrMsg = plInputIfaceMgrMsg::ConvertNoRef( msg );
    if( mgrMsg != nil )
    {
        if ( mgrMsg->GetCommand() == plInputIfaceMgrMsg::kDisableAvatarClickable )
        {
            // ignore if already in list or this is who WE are offering the book to...
            if (mgrMsg->GetAvKey() == fOffereeKey)
                return true;
            for(int x = 0; x < fIgnoredAvatars.Count(); x++)
            {
                if (fIgnoredAvatars[x] == mgrMsg->GetAvKey())
                    return true;
            }
            fIgnoredAvatars.Append(mgrMsg->GetAvKey());
        }
        else
        if ( mgrMsg->GetCommand() == plInputIfaceMgrMsg::kEnableAvatarClickable )
        {
            for(int x = 0; x < fIgnoredAvatars.Count(); x++)
            {
                if (fIgnoredAvatars[x] == mgrMsg->GetAvKey())
                    fIgnoredAvatars.RemoveItem(mgrMsg->GetAvKey());
            }
        }
        else
        if ( mgrMsg->GetCommand() == plInputIfaceMgrMsg::kGUIDisableAvatarClickable )
        {
            // ignore if already in list or this is who WE are offering the book to...
            if (mgrMsg->GetAvKey() == fOffereeKey)
                return true;
            for(int x = 0; x < fGUIIgnoredAvatars.Count(); x++)
            {
                if (fGUIIgnoredAvatars[x] == mgrMsg->GetAvKey())
                    return true;
            }
            fGUIIgnoredAvatars.Append(mgrMsg->GetAvKey());
        }
        else
        if ( mgrMsg->GetCommand() == plInputIfaceMgrMsg::kGUIEnableAvatarClickable )
        {
            for(int x = 0; x < fGUIIgnoredAvatars.Count(); x++)
            {
                if (fGUIIgnoredAvatars[x] == mgrMsg->GetAvKey())
                    fGUIIgnoredAvatars.RemoveItem(mgrMsg->GetAvKey());
            }
        }
        else
        if( mgrMsg->GetCommand() == plInputIfaceMgrMsg::kEnableClickables )
        {
            fClickability = true;
            return true;
        }
        else if( mgrMsg->GetCommand() == plInputIfaceMgrMsg::kDisableClickables )
        {
            fClickability = false;
            ResetClickableState();
            return true;
        }
        else if ( mgrMsg->GetCommand() == plInputIfaceMgrMsg::kSetOfferBookMode )
        {
            fBookMode = kOfferBook;
            fOffereeKey = nil;
            fBookKey = mgrMsg->GetSender();
            fOfferedAgeInstance = mgrMsg->GetAgeName();
            fOfferedAgeFile = mgrMsg->GetAgeFileName();
            ISendAvatarDisabledNotification(false);

        }
        else if ( mgrMsg->GetCommand() == plInputIfaceMgrMsg::kClearOfferBookMode )
        {
            if (fBookMode == kOfferAccepted || fBookMode == kOfferLinkPending)
            {
                fPendingLink = true;
            }
            else
            if (fOffereeKey != nil)
            {
                // notify any offeree that the offer is rescinded
                ISendOfferNotification(fOffereeKey, -999, true);
                //IManageIgnoredAvatars(fOffereeKey, false);
                fOffereeKey = nil;
            }
            // shut down offer book mode
            fBookMode = kNotOffering;
            ISendAvatarDisabledNotification(true);
        }
        else if ( mgrMsg->GetCommand() == plInputIfaceMgrMsg::kNotifyOfferRejected)
        {
            if (fBookMode == kBookOffered)
            {
                // and put our own dialog back up...
                ISendOfferNotification(plNetClientMgr::GetInstance()->GetLocalPlayerKey(), 0, false);
                //IManageIgnoredAvatars(fOffereeKey, false);
                fBookMode = kOfferBook;
                fOffereeKey = nil;
            }
            else
            if (mgrMsg->GetSender() == plNetClientMgr::GetInstance()->GetLocalPlayerKey())
            {
                fBookMode = kNotOffering;
                ISendAvatarDisabledNotification(true);
            }
        }
        else if ( mgrMsg->GetCommand() == plInputIfaceMgrMsg::kNotifyOfferAccepted && fBookMode == kBookOffered)
        {
            fBookMode = kOfferAccepted;
        }
        else if ( mgrMsg->GetCommand() == plInputIfaceMgrMsg::kNotifyOfferCompleted )
        {
            // must have actually offered the book...
            if (!fPendingLink)
            {
                if (fBookMode == kOfferBook || fBookMode == kBookOffered)
                return true;
            }
            if (!plNetClientMgr::GetInstance())
                return true;

            fOffereeID = mgrMsg->GetPageID();
            ILinkOffereeToAge();
        }
        else if ( mgrMsg->GetCommand() == plInputIfaceMgrMsg::kSetShareSpawnPoint )
        {
            fSpawnPoint = mgrMsg->GetSpawnPoint();
        }
        else if ( mgrMsg->GetCommand() == plInputIfaceMgrMsg::kSetShareAgeInstanceGuid )
        {
            fAgeInstanceGuid = mgrMsg->GetAgeInstanceGuid();
        }
    }
    plVaultNotifyMsg* pVaultMsg = plVaultNotifyMsg::ConvertNoRef(msg);
    if (pVaultMsg && pVaultMsg->GetType()==plNetCommon::VaultTasks::kRegisterOwnedAge )
    {
        //sanity check -
        if (fBookMode != kOfferLinkPending && fPendingLink == false)
            return true;
        // stop looking for this message and reset interface to 'offer book' mode again
        plgDispatch::Dispatch()->UnRegisterForExactType(plVaultNotifyMsg::Index(), fManager->GetKey());
        ILinkOffereeToAge();
        return true;
    }
    return false;
}


///// ILinkOffereeToAge

void plSceneInputInterface::ILinkOffereeToAge()
{
    // check vault to see if we've got an instance of the offered age now, if not create one and wait until we get a reply...
    plAgeInfoStruct info;
    info.SetAgeFilename(fOfferedAgeFile);
    info.SetAgeInstanceName(fOfferedAgeInstance);

    bool isAgeInstanceGuidSet = !GuidIsNil(fAgeInstanceGuid);
    
    plAgeLinkStruct link;
    
    if (isAgeInstanceGuidSet) {
        info.SetAgeInstanceGuid(&plUUID(fAgeInstanceGuid));
        link.GetAgeInfo()->CopyFrom(&info);

        GuidClear(&fAgeInstanceGuid);
    }
    else if (!VaultGetOwnedAgeLink(&info, &link)) {
    
        // We must have an owned copy of the age before we can offer it, so make one now
        info.SetAgeInstanceGuid(&plUUID(GuidGenerate()));
        std::string title;
        std::string desc;
        
        unsigned nameLen = StrLen(plNetClientMgr::GetInstance()->GetPlayerName());
        if (plNetClientMgr::GetInstance()->GetPlayerName()[nameLen - 1] == 's' || plNetClientMgr::GetInstance()->GetPlayerName()[nameLen - 1] == 'S') {
            xtl::format( title, "%s'", plNetClientMgr::GetInstance()->GetPlayerName() );
            xtl::format( desc, "%s' %s", plNetClientMgr::GetInstance()->GetPlayerName(), link.GetAgeInfo()->GetAgeInstanceName() );
        }
        else {
            xtl::format( title, "%s's", plNetClientMgr::GetInstance()->GetPlayerName() );
            xtl::format( desc, "%s's %s", plNetClientMgr::GetInstance()->GetPlayerName(), link.GetAgeInfo()->GetAgeInstanceName() );
        }
        
        info.SetAgeUserDefinedName( title.c_str() );
        info.SetAgeDescription( desc.c_str() );

        link.GetAgeInfo()->CopyFrom(&info);
        if (!VaultRegisterOwnedAgeAndWait(&link)) {
            // failed to become an owner of the age for some reason, offer cannot continue
            return;
        }
    }
    else if (RelVaultNode * linkNode = VaultGetOwnedAgeLinkIncRef(&info)) {
        // We have the age in our AgesIOwnFolder. If its volatile, dump it for the new one.
        VaultAgeLinkNode linkAcc(linkNode);
        if (linkAcc.volat) {
            if (VaultUnregisterOwnedAgeAndWait(link.GetAgeInfo())) {
                link.GetAgeInfo()->SetAgeInstanceGuid(&plUUID(GuidGenerate()));
                VaultRegisterOwnedAgeAndWait(&link);
            }
        }
        linkNode->DecRef();
    }

    if (fSpawnPoint) {
        plSpawnPointInfo spawnPoint;
        spawnPoint.SetName(fSpawnPoint);
        link.SetSpawnPoint(spawnPoint);
    }
    
            
    // We now own the age, offer it

    if (0 == stricmp(fOfferedAgeFile, kPersonalAgeFilename))
        plNetLinkingMgr::GetInstance()->OfferLinkToPlayer(&link, fOffereeID, fManager->GetKey());
    else
        plNetLinkingMgr::GetInstance()->LinkPlayerToAge(&link, fOffereeID);
        
    if (!fPendingLink && stricmp(fOfferedAgeFile, kPersonalAgeFilename))
    {   
        // tell our local dialog to pop up again...
        ISendOfferNotification(plNetClientMgr::GetInstance()->GetLocalPlayerKey(), 0, false);
        // make them clickable again(in case they come back?)
        //IManageIgnoredAvatars(fOffereeKey, false);
        
        fBookMode = kNotOffering;
        fOffereeKey = nil;
        fPendingLink = false;
    }
    else // this is a yeesha book link, must wait for multistage callbacks
    {
        // commented out until after 0.9
        fBookMode = kOfferLinkPending;
        fPendingLink = true;
//          fBookMode = kNotOffering;
//          fOffereeKey = nil;
//          fPendingLink = false;
//          ISendAvatarDisabledNotification(true);
    }
}

//// ISetLastClicked /////////////////////////////////////////////////////////

#define MATT_WAS_HERE

void    plSceneInputInterface::ISetLastClicked( plKey obj, hsPoint3 hitPoint )
{
    if (fBookMode != kNotOffering)
        return;
    
    if( fLastClicked != nil )
    {
        // Send an "un-picked" message to it
        if( !fLastClickIsAvatar )
        {
            plPickedMsg *pPickedMsg = TRACKED_NEW plPickedMsg;
            pPickedMsg->AddReceiver( fLastClicked );
            pPickedMsg->fPicked = false;
            plgDispatch::MsgSend( pPickedMsg );
        }
        else
        {
            plRemoteAvatarInfoMsg *pMsg = TRACKED_NEW plRemoteAvatarInfoMsg;
            pMsg->SetAvatarKey( nil );
            plgDispatch::MsgSend( pMsg );
        }
    }

    fLastClicked = obj;
    fLastClickIsAvatar = ( obj == nil ) ? false : fCurrClickIsAvatar;

    if( fLastClicked != nil )
    {
#ifdef MATT_WAS_HERE
    // now we send pick messages to avatars as well...
    plPickedMsg *pPickedMsg = TRACKED_NEW plPickedMsg;
    pPickedMsg->AddReceiver( fLastClicked );
    pPickedMsg->fHitPoint = hitPoint;
    plgDispatch::MsgSend( pPickedMsg );

    // if it's an avatar, we also send this thing
    if(fLastClickIsAvatar)
    {                   
        plRemoteAvatarInfoMsg *pMsg = TRACKED_NEW plRemoteAvatarInfoMsg;
        pMsg->SetAvatarKey( fLastClicked );
        plgDispatch::MsgSend( pMsg );
    }
#else
    // Send a "picked" message to it
    if( !fLastClickIsAvatar )
    {
        plPickedMsg *pPickedMsg = TRACKED_NEW plPickedMsg;
        pPickedMsg->AddReceiver( fLastClicked );
        pPickedMsg->fHitPoint = hitPoint;
        plgDispatch::MsgSend( pPickedMsg );
    }
    else
    {                   
        plRemoteAvatarInfoMsg *pMsg = TRACKED_NEW plRemoteAvatarInfoMsg;
        pMsg->SetAvatarKey( fLastClicked );
        plgDispatch::MsgSend( pMsg );
    }
#endif
    }
}

//// InterpretInputEvent /////////////////////////////////////////////////////

hsBool plSceneInputInterface::InterpretInputEvent( plInputEventMsg *pMsg )
{
    plControlEventMsg* pControlEvent = plControlEventMsg::ConvertNoRef(pMsg);
    if (pControlEvent)
    {
        if (pControlEvent->GetControlCode() == B_CONTROL_IGNORE_AVATARS)
        {
            for (int i = 0; i < fLocalIgnoredAvatars.Count(); i++)
            {
                plSceneObject* pObj = plSceneObject::ConvertNoRef(fLocalIgnoredAvatars[i]->ObjectIsLoaded());
                if (!pObj)
                    continue;

                const plArmatureMod* pArm = (const plArmatureMod*)pObj->GetModifierByType(plArmatureMod::Index());
                if (!pArm)
                    continue;

                plPhysicalControllerCore* controller = pArm->GetController();
                if (controller)
                {
                    if (pControlEvent->ControlActivated())
                        controller->SetLOSDB(plSimDefs::kLOSDBNone);
                    else
                        controller->SetLOSDB(plSimDefs::kLOSDBUIItems);
                }
            }
            return true;
        }
        return false;
    }

    plMouseEventMsg *mouseMsg = plMouseEventMsg::ConvertNoRef( pMsg );
    if( mouseMsg != nil )
    {
        // you're suspended when in this mode...
        if (fBookMode == kOfferLinkPending || fBookMode == kOfferAccepted)
            return true;

        if( mouseMsg->GetButton() == kLeftButtonDown )
        {
            if( fCurrentClickable != nil && fLastClicked == nil && fCurrentCursor != kNullCursor )
            {
                fButtonState |= kLeftButtonDown;
                ISetLastClicked( fCurrentClickable, fCurrentClickPoint );
                fCurrentCursor = SetCurrentCursorID(kCursorClicked);
                return true;
            }
            // right here
            if (fBookMode == kOfferBook)
            {
                fBookMode = kNotOffering;
                fOffereeKey = nil;
                ISendAvatarDisabledNotification(true);
            }
        }
        else if( mouseMsg->GetButton() == kLeftButtonUp )
        {
            if (fBookMode != kNotOffering)
            {
                if (fBookMode == kOfferBook && fCurrClickIsAvatar)
                {
                    // send the avatar a message to put up his appropriate book
                    ISendOfferNotification(fCurrentClickable, 999, true);
                    //IManageIgnoredAvatars(fCurrentClickable, true);
                    fBookMode = kBookOffered;
                    fOffereeKey = fCurrentClickable;
                }
                else
                if (fBookMode == kBookOffered && fCurrClickIsAvatar)
                {
                    // and put our own dialog back up...
                    ISendOfferNotification(fOffereeKey, -999, true);
                    ISendOfferNotification(plNetClientMgr::GetInstance()->GetLocalPlayerKey(), 0, false);
                    //IManageIgnoredAvatars(fOffereeKey, false);
                    fBookMode = kOfferBook;
                    fOffereeKey = nil;
                }   
                else
                if (fBookMode == kOfferBook)
                {
                    fBookMode = kNotOffering;
                    fOffereeKey = nil;
                    ISendAvatarDisabledNotification(true);
                }
            }
            if( fLastClicked != nil )
            {
                fButtonState &= ~kLeftButtonDown;
                ISetLastClicked( nil, hsPoint3(0,0,0) );
                
                return true;
            }
        }
    }

    return false;
}


//// ISendOfferNotification ////////////////////////////////////////////////////////
void plSceneInputInterface::IManageIgnoredAvatars(plKey& offeree, hsBool add)
{
    // tell everyone else to be able to / not to be able to select this avatar
    plInputIfaceMgrMsg* pMsg = 0;
    if (!add)
        pMsg = TRACKED_NEW plInputIfaceMgrMsg(plInputIfaceMgrMsg::kEnableAvatarClickable);
    else
        pMsg = TRACKED_NEW plInputIfaceMgrMsg(plInputIfaceMgrMsg::kDisableAvatarClickable);
    pMsg->SetAvKey(offeree);
    pMsg->SetBCastFlag(plMessage::kNetPropagate);
    pMsg->SetBCastFlag(plMessage::kNetForce);
    pMsg->SetBCastFlag(plMessage::kLocalPropagate, false);
    pMsg->Send();
}   

void plSceneInputInterface::ISendOfferNotification(plKey& offeree, int ID, hsBool net)
{
    int offereeID = -1;
    if (offeree == plNetClientMgr::GetInstance()->GetLocalPlayerKey())
    {
        offereeID = plNetClientMgr::GetInstance()->GetPlayerID();
    }
    else
    {
        plNetTransportMember **members = nil;
        plNetClientMgr::GetInstance()->TransportMgr().GetMemberListDistSorted( members );
        if( members != nil)
        {
            for(int i = 0; i < plNetClientMgr::GetInstance()->TransportMgr().GetNumMembers(); i++ )
            {
                plNetTransportMember *mbr = members[ i ];

                if( mbr != nil && mbr->GetAvatarKey() == offeree)
                {   
                    offereeID = mbr->GetPlayerID();
                    break;
                }
            }
        }

        delete [] members;

    }
    plNotifyMsg* pMsg = TRACKED_NEW plNotifyMsg;
    pMsg->AddOfferBookEvent(plNetClientMgr::GetInstance()->GetLocalPlayerKey(), ID, offereeID);
    pMsg->AddReceiver(fBookKey);
    if (net)
    {   
        pMsg->SetBCastFlag(plMessage::kNetPropagate);
        pMsg->SetBCastFlag(plMessage::kNetForce);
        pMsg->SetBCastFlag(plMessage::kLocalPropagate,false);
        pMsg->AddNetReceiver( offereeID );
        pMsg->Send();
    }
    else
    {
        pMsg->SetBCastFlag(plMessage::kNetPropagate, false); // don't deliver networked!
        pMsg->Send();
    }

}

void plSceneInputInterface::ISendAvatarDisabledNotification(hsBool enabled)
{
    plInputIfaceMgrMsg* pMsg = 0;
    if (enabled)
        pMsg = TRACKED_NEW plInputIfaceMgrMsg(plInputIfaceMgrMsg::kEnableAvatarClickable);
    else
        pMsg = TRACKED_NEW plInputIfaceMgrMsg(plInputIfaceMgrMsg::kDisableAvatarClickable);
    pMsg->SetAvKey(plNetClientMgr::GetInstance()->GetLocalPlayerKey());
    pMsg->SetBCastFlag(plMessage::kNetPropagate);
    pMsg->SetBCastFlag(plMessage::kNetForce);
    pMsg->SetBCastFlag(plMessage::kLocalPropagate, false);
    pMsg->Send();
}


//// IRequestLOSCheck ////////////////////////////////////////////////////////

void    plSceneInputInterface::IRequestLOSCheck( hsScalar xPos, hsScalar yPos, int ID )
{
    if( fPipe == nil )
        return;
    

    Int32 x=(Int32) ( xPos * fPipe->Width() );
    Int32 y=(Int32) ( yPos * fPipe->Height() );

    hsPoint3 endPos, startPos;
    
    fPipe->ScreenToWorldPoint( 1,0, &x, &y, 10000, 0, &endPos );
    startPos = fPipe->GetViewPositionWorld();

    // move the start pos out a little to avoid backing up against physical objects...
    hsVector3 view(endPos - startPos);
    view.Normalize();
    startPos = startPos + (view * 0.3f);

    plLOSRequestMsg* pMsg;
    
    if(ID == ID_FIND_CLICKABLE) {
        pMsg = TRACKED_NEW plLOSRequestMsg( fManager->GetKey(), startPos, endPos, plSimDefs::kLOSDBUIItems, plLOSRequestMsg::kTestClosest );
        pMsg->SetCullDB(plSimDefs::kLOSDBUIBlockers);
    } else if(ID == ID_FIND_WALKABLE_GROUND) {
        pMsg = TRACKED_NEW plLOSRequestMsg( fManager->GetKey(), startPos, endPos, plSimDefs::kLOSDBAvatarWalkable, plLOSRequestMsg::kTestClosest);
    } else
        pMsg = TRACKED_NEW plLOSRequestMsg( fManager->GetKey(), startPos, endPos, plSimDefs::kLOSDBLocalAvatar, plLOSRequestMsg::kTestClosest);
    
    pMsg->SetReportType( plLOSRequestMsg::kReportHitOrMiss );

    pMsg->SetRequestID( ID );

    plgDispatch::MsgSend( pMsg );

    fLastStartPt = startPos;
    fLastEndPt = endPos;
}
        
//// IWorldPosMovedSinceLastLOSCheck /////////////////////////////////////////

hsBool  plSceneInputInterface::IWorldPosMovedSinceLastLOSCheck( void )
{
    if( fPipe == nil )
        return false;

    Int32 x=(Int32) ( plMouseDevice::Instance()->GetCursorX() * fPipe->Width() );
    Int32 y=(Int32) ( plMouseDevice::Instance()->GetCursorY() * fPipe->Height() );

    hsPoint3 endPos, startPos;
    
    startPos = fPipe->GetViewPositionWorld();
    if( !( startPos == fLastStartPt ) )
        return true;

    fPipe->ScreenToWorldPoint( 1,0, &x, &y, 10000, 0, &endPos );
    if( !( endPos == fLastEndPt ) )
        return true;

    return false;
}

//// GetCurrentCursorID ///////////////////////////////////////////////////////

UInt32 plSceneInputInterface::SetCurrentCursorID(UInt32 id)
{ 
    if (fBookMode == kOfferBook || fBookMode == kBookOffered)
    {
        switch(id)
        {
        case kCursorPoised:
            return kCursorOfferBookHilite;
        case kNullCursor:
            return kCursorOfferBook;
        case kCursorClicked:
            return kCursorOfferBookClicked;
        }
    }
    else
    if (fBookMode == kOfferAccepted || fBookMode == kOfferLinkPending)
        return kCursorOfferBook;
    
    return id; 
}

void plSceneInputInterface::RequestAvatarTurnToPointLOS()
{
    IRequestLOSCheck( plMouseDevice::Instance()->GetCursorX(), plMouseDevice::Instance()->GetCursorY(), ID_FIND_WALKABLE_GROUND );  
}