/*
** Command & Conquer Generals(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. //
// //
////////////////////////////////////////////////////////////////////////////////
// LookAtXlat.cpp
// Translate raw input events into camera movement commands
// Author: Michael S. Booth, April 2001
#include "PreRTS.h" // This must go first in EVERY cpp file int the GameEngine
#include "windows.h"
#include "Common/GameType.h"
#include "Common/MessageStream.h"
#include "Common/Player.h"
#include "Common/PlayerList.h"
#include "Common/Recorder.h"
#include "Common/StatsCollector.h"
#include "GameLogic/Object.h"
#include "GameLogic/PartitionManager.h"
#include "GameClient/Display.h"
#include "GameClient/GameText.h"
#include "GameClient/Mouse.h"
#include "GameClient/Shell.h"
#include "GameClient/GameClient.h"
#include "GameClient/KeyDefs.h"
#include "GameClient/View.h"
#include "GameClient/Drawable.h"
#include "GameClient/LookAtXlat.h"
#include "GameLogic/Module/UpdateModule.h"
#include "GameLogic/GameLogic.h"
#include "Common/GlobalData.h" // for camera pitch angle only
LookAtTranslator *TheLookAtTranslator = NULL;
static enum
{
DIR_UP = 0,
DIR_DOWN,
DIR_LEFT,
DIR_RIGHT
};
static Bool scrollDir[4] = { false, false, false, false };
Int SCROLL_AMT = 100;
static const Int edgeScrollSize = 3;
static Mouse::MouseCursor prevCursor = Mouse::ARROW;
//-----------------------------------------------------------------------------
void LookAtTranslator::setScrolling(Int x)
{
if (!TheInGameUI->getInputEnabled())
return;
prevCursor = TheMouse->getMouseCursor();
m_isScrolling = true;
TheInGameUI->setScrolling( TRUE );
TheTacticalView->setMouseLock( TRUE );
m_scrollType = x;
if(TheStatsCollector)
TheStatsCollector->startScrollTime();
}
//-----------------------------------------------------------------------------
void LookAtTranslator::stopScrolling( void )
{
m_isScrolling = false;
TheInGameUI->setScrolling( FALSE );
TheTacticalView->setMouseLock( FALSE );
TheMouse->setCursor(prevCursor);
m_scrollType = SCROLL_NONE;
// if we have a stats collectore increment the stats
if(TheStatsCollector)
TheStatsCollector->endScrollTime();
}
//-----------------------------------------------------------------------------
LookAtTranslator::LookAtTranslator() :
m_isScrolling(false),
m_isRotating(false),
m_isPitching(false),
m_isChangingFOV(false),
m_timestamp(0),
m_lastPlaneID(INVALID_DRAWABLE_ID),
m_lastMouseMoveFrame(0),
m_scrollType(SCROLL_NONE)
{
//Added By Sadullah Nader
//Initializations misssing and needed
m_anchor.x = m_anchor.y = 0;
m_currentPos.x = m_currentPos.y = 0;
m_originalAnchor.x = m_originalAnchor.y = 0;
//
DEBUG_ASSERTCRASH(!TheLookAtTranslator, ("Already have a LookAtTranslator - why do you need two?"));
TheLookAtTranslator = this;
}
//-----------------------------------------------------------------------------
LookAtTranslator::~LookAtTranslator()
{
if (TheLookAtTranslator == this)
TheLookAtTranslator = NULL;
}
const ICoord2D* LookAtTranslator::getRMBScrollAnchor(void)
{
if (m_isScrolling && m_scrollType == SCROLL_RMB)
{
return &m_anchor;
}
return NULL;
}
Bool LookAtTranslator::hasMouseMovedRecently( void )
{
if (m_lastMouseMoveFrame > TheGameLogic->getFrame())
m_lastMouseMoveFrame = 0; // reset for new game
if (m_lastMouseMoveFrame + LOGICFRAMES_PER_SECOND < TheGameLogic->getFrame())
return false;
return true;
}
void LookAtTranslator::setCurrentPos( const ICoord2D& pos )
{
m_currentPos = pos;
}
//-----------------------------------------------------------------------------
/**
* The LookAt Translator is responsible for camera movements. It is directly responsible for
* right mouse button scrolling, and CTRL- bookmarking. It also responds to certain
* LOOKAT message on the message stream.
*/
GameMessageDisposition LookAtTranslator::translateGameMessage(const GameMessage *msg)
{
GameMessageDisposition disp = KEEP_MESSAGE;
GameMessage::Type t = msg->getType();
switch (t)
{
//-----------------------------------------------------------------------------
case GameMessage::MSG_RAW_KEY_DOWN:
case GameMessage::MSG_RAW_KEY_UP:
{
// get key and state from args
UnsignedByte key = msg->getArgument( 0 )->integer;
UnsignedByte state = msg->getArgument( 1 )->integer;
Bool isPressed = !(BitTest( state, KEY_STATE_UP ));
if (TheShell && TheShell->isShellActive())
break;
switch (key)
{
case KEY_UP:
scrollDir[DIR_UP] = isPressed;
break;
case KEY_DOWN:
scrollDir[DIR_DOWN] = isPressed;
break;
case KEY_LEFT:
scrollDir[DIR_LEFT] = isPressed;
break;
case KEY_RIGHT:
scrollDir[DIR_RIGHT] = isPressed;
break;
}
if (TheInGameUI->isSelecting() || (m_isScrolling && m_scrollType != SCROLL_KEY))
break;
// see if we need to start/stop scrolling
Int numDirs = 0;
for (Int i=0; i<4; ++i)
{
if (scrollDir[i])
numDirs++;
}
if (numDirs && !m_isScrolling)
{
setScrolling( SCROLL_KEY );
}
else if (!numDirs && m_isScrolling)
{
stopScrolling();
}
break;
}
//-----------------------------------------------------------------------------
case GameMessage::MSG_RAW_MOUSE_RIGHT_BUTTON_DOWN:
{
m_lastMouseMoveFrame = TheGameLogic->getFrame();
m_anchor = msg->getArgument( 0 )->pixel;
m_currentPos = msg->getArgument( 0 )->pixel;
// disable mouse scrolling in alternate mouse mode, per Harvard 7/15/03
if (!TheGlobalData->m_useAlternateMouse && !TheInGameUI->isSelecting() && !m_isScrolling)
{
setScrolling(SCROLL_RMB);
}
break;
}
//-----------------------------------------------------------------------------
case GameMessage::MSG_RAW_MOUSE_RIGHT_BUTTON_UP:
{
m_lastMouseMoveFrame = TheGameLogic->getFrame();
if (m_scrollType == SCROLL_RMB)
{
stopScrolling();
}
break;
}
//-----------------------------------------------------------------------------
case GameMessage::MSG_RAW_MOUSE_MIDDLE_BUTTON_DOWN:
{
m_lastMouseMoveFrame = TheGameLogic->getFrame();
m_isRotating = true;
m_anchor = msg->getArgument( 0 )->pixel;
m_originalAnchor = msg->getArgument( 0 )->pixel;
m_currentPos = msg->getArgument( 0 )->pixel;
m_timestamp = TheGameClient->getFrame();
break;
}
//-----------------------------------------------------------------------------
case GameMessage::MSG_RAW_MOUSE_MIDDLE_BUTTON_UP:
{
m_lastMouseMoveFrame = TheGameLogic->getFrame();
const UnsignedInt CLICK_DURATION = 5;
const UnsignedInt PIXEL_OFFSET = 5;
m_isRotating = false;
Int dx = m_currentPos.x-m_originalAnchor.x;
if (dx<0) dx = -dx;
Int dy = m_currentPos.y-m_originalAnchor.y;
Bool didMove = dx>PIXEL_OFFSET || dy>PIXEL_OFFSET;
// if middle button is "clicked", reset to "home" orientation
if (!didMove && TheGameClient->getFrame() - m_timestamp < CLICK_DURATION)
{
TheTacticalView->setAngleAndPitchToDefault();
TheTacticalView->setZoomToDefault();
}
break;
}
//-----------------------------------------------------------------------------
case GameMessage::MSG_RAW_MOUSE_POSITION:
{
if (m_currentPos.x != msg->getArgument( 0 )->pixel.x || m_currentPos.y != msg->getArgument( 0 )->pixel.y)
m_lastMouseMoveFrame = TheGameLogic->getFrame();
m_currentPos = msg->getArgument( 0 )->pixel;
UnsignedInt height = TheDisplay->getHeight();
UnsignedInt width = TheDisplay->getWidth();
if (TheInGameUI->getInputEnabled() == FALSE) {
// We don't care how we're scrolling, just stop.
if (m_isScrolling)
stopScrolling();
break;
}
if (!TheGlobalData->m_windowed)
{
if (m_isScrolling)
{
if ( m_scrollType == SCROLL_SCREENEDGE && (m_currentPos.x >= edgeScrollSize && m_currentPos.y >= edgeScrollSize && m_currentPos.y < height-edgeScrollSize && m_currentPos.x < width-edgeScrollSize) )
{
stopScrolling();
}
}
else
{
if ( m_currentPos.x < edgeScrollSize || m_currentPos.y < edgeScrollSize || m_currentPos.y >= height-edgeScrollSize || m_currentPos.x >= width-edgeScrollSize )
{
setScrolling(SCROLL_SCREENEDGE);
}
}
}
// rotate the view
if (m_isRotating)
{
const Real FACTOR = 0.01f;
Real angle = FACTOR * (m_currentPos.x - m_anchor.x);
TheTacticalView->setAngle( TheTacticalView->getAngle() + angle );
m_anchor = msg->getArgument( 0 )->pixel;
}
// rotate the view up/down
if (m_isPitching)
{
const Real FACTOR = 0.01f;
Real angle = FACTOR * (m_currentPos.y - m_anchor.y);
TheTacticalView->setPitch( TheTacticalView->getPitch() + angle );
m_anchor = msg->getArgument( 0 )->pixel;
}
#if defined(_DEBUG) || defined(_INTERNAL)
// adjust the field of view
if (m_isChangingFOV)
{
const Real FACTOR = 0.01f;
Real angle = FACTOR * (m_currentPos.y - m_anchor.y);
TheTacticalView->setFieldOfView( TheTacticalView->getFieldOfView() + angle );
m_anchor = msg->getArgument( 0 )->pixel;
}
#endif
break;
}
//-----------------------------------------------------------------------------
case GameMessage::MSG_RAW_MOUSE_WHEEL:
{
m_lastMouseMoveFrame = TheGameLogic->getFrame();
Int spin = msg->getArgument( 1 )->integer;
if (spin > 0)
{
for ( ; spin > 0; spin--)
TheTacticalView->zoomIn();
}
else
{
for ( ;spin < 0; spin++ )
TheTacticalView->zoomOut();
}
}
//-----------------------------------------------------------------------------
case GameMessage::MSG_META_OPTIONS:
{
// stop the scrolling
stopScrolling();
// let the message drop through, cause we need to process this message for
// selection as well.
break;
}
//-----------------------------------------------------------------------------
case GameMessage::MSG_FRAME_TICK:
{
Coord2D offset = {0, 0};
// If we've been forced to stop scrolling (script action?) then stop
if (m_isScrolling && !TheInGameUI->isScrolling())
{
TheInGameUI->setScrollAmount(offset);
stopScrolling();
}
else
// scroll the view
if (m_isScrolling)
{
switch (m_scrollType)
{
case SCROLL_RMB:
{
if (TheInGameUI->shouldMoveRMBScrollAnchor())
{
Int maxX = TheDisplay->getWidth()/2;
Int maxY = TheDisplay->getHeight()/2;
if (m_currentPos.x + maxX < m_anchor.x)
m_anchor.x = m_currentPos.x + maxX;
else if (m_currentPos.x - maxX > m_anchor.x)
m_anchor.x = m_currentPos.x - maxX;
if (m_currentPos.y + maxY < m_anchor.y)
m_anchor.y = m_currentPos.y + maxY;
else if (m_currentPos.y - maxY > m_anchor.y)
m_anchor.y = m_currentPos.y - maxY;
}
offset.x = TheGlobalData->m_horizontalScrollSpeedFactor * (m_currentPos.x - m_anchor.x);
offset.y = TheGlobalData->m_verticalScrollSpeedFactor * (m_currentPos.y - m_anchor.y);
Coord2D vec;
vec.x = offset.x;
vec.y = offset.y;
vec.normalize();
// Add in the window scroll amount as the minimum.
offset.x += TheGlobalData->m_horizontalScrollSpeedFactor * vec.x * sqr(TheGlobalData->m_keyboardScrollFactor);
offset.y += TheGlobalData->m_verticalScrollSpeedFactor * vec.y * sqr(TheGlobalData->m_keyboardScrollFactor);
}
break;
case SCROLL_KEY:
{
if (scrollDir[DIR_UP])
{
offset.y -= TheGlobalData->m_verticalScrollSpeedFactor * SCROLL_AMT * TheGlobalData->m_keyboardScrollFactor;
}
if (scrollDir[DIR_DOWN])
{
offset.y += TheGlobalData->m_verticalScrollSpeedFactor * SCROLL_AMT * TheGlobalData->m_keyboardScrollFactor;
}
if (scrollDir[DIR_LEFT])
{
offset.x -= TheGlobalData->m_horizontalScrollSpeedFactor * SCROLL_AMT * TheGlobalData->m_keyboardScrollFactor;
}
if (scrollDir[DIR_RIGHT])
{
offset.x += TheGlobalData->m_horizontalScrollSpeedFactor * SCROLL_AMT * TheGlobalData->m_keyboardScrollFactor;
}
}
break;
case SCROLL_SCREENEDGE:
{
UnsignedInt height = TheDisplay->getHeight();
UnsignedInt width = TheDisplay->getWidth();
if (m_currentPos.y < edgeScrollSize)
{
offset.y -= TheGlobalData->m_verticalScrollSpeedFactor * SCROLL_AMT * TheGlobalData->m_keyboardScrollFactor;
}
if (m_currentPos.y >= height-edgeScrollSize)
{
offset.y += TheGlobalData->m_verticalScrollSpeedFactor * SCROLL_AMT * TheGlobalData->m_keyboardScrollFactor;
}
if (m_currentPos.x < edgeScrollSize)
{
offset.x -= TheGlobalData->m_horizontalScrollSpeedFactor * SCROLL_AMT * TheGlobalData->m_keyboardScrollFactor;
}
if (m_currentPos.x >= width-edgeScrollSize)
{
offset.x += TheGlobalData->m_horizontalScrollSpeedFactor * SCROLL_AMT * TheGlobalData->m_keyboardScrollFactor;
}
}
break;
}
TheInGameUI->setScrollAmount(offset);
TheTacticalView->scrollBy( &offset );
}
else //not scrolling so reset amount
TheInGameUI->setScrollAmount(offset);
#if !defined(_PLAYTEST)
//if (TheGlobalData->m_saveCameraInReplay /*&& TheRecorder->getMode() != RECORDERMODETYPE_PLAYBACK *//**/&& (TheGameLogic->isInSinglePlayerGame() || TheGameLogic->isInSkirmishGame())/**/)
//if (TheGlobalData->m_saveCameraInReplay && (TheGameLogic->isInMultiplayerGame() || TheGameLogic->isInSinglePlayerGame() || TheGameLogic->isInSkirmishGame()))
if (TheGlobalData->m_saveCameraInReplay && (TheGameLogic->isInSinglePlayerGame() || TheGameLogic->isInSkirmishGame()))
{
ViewLocation currentView;
TheTacticalView->getLocation(¤tView);
GameMessage *msg = TheMessageStream->appendMessage( GameMessage::MSG_SET_REPLAY_CAMERA );
msg->appendLocationArgument( currentView.m_pos );
msg->appendRealArgument( currentView.m_angle );
msg->appendRealArgument( currentView.m_pitch );
msg->appendRealArgument( currentView.m_zoom );
msg->appendIntegerArgument( (Int)TheMouse->getMouseCursor() );
msg->appendPixelArgument( m_currentPos );
}
#endif
break;
}
// ------------------------------------------------------------------------
#if defined(_DEBUG) || defined(_INTERNAL)
case GameMessage::MSG_META_DEMO_BEGIN_ADJUST_PITCH:
{
DEBUG_ASSERTCRASH(!m_isPitching, ("hmm, mismatched m_isPitching"));
m_isPitching = true;
disp = DESTROY_MESSAGE;
break;
}
#endif // #if defined(_DEBUG) || defined(_INTERNAL)
// ------------------------------------------------------------------------
#if defined(_DEBUG) || defined(_INTERNAL)
case GameMessage::MSG_META_DEMO_END_ADJUST_PITCH:
{
DEBUG_ASSERTCRASH(m_isPitching, ("hmm, mismatched m_isPitching"));
m_isPitching = false;
disp = DESTROY_MESSAGE;
break;
}
#endif // #if defined(_DEBUG) || defined(_INTERNAL)
// ------------------------------------------------------------------------
#if defined(_DEBUG) || defined(_INTERNAL)
case GameMessage::MSG_META_DEMO_DESHROUD:
{
ThePartitionManager->revealMapForPlayerPermanently( ThePlayerList->getLocalPlayer()->getPlayerIndex() );
break;
}
#endif // #if defined(_DEBUG) || defined(_INTERNAL)
// ------------------------------------------------------------------------
#if defined(_DEBUG) || defined(_INTERNAL)
case GameMessage::MSG_META_DEMO_ENSHROUD:
{
// Need to first undo the permanent Look laid down by DEMO_DESHROUD, then blast a shroud dollop.
ThePartitionManager->undoRevealMapForPlayerPermanently( ThePlayerList->getLocalPlayer()->getPlayerIndex() );
ThePartitionManager->shroudMapForPlayer( ThePlayerList->getLocalPlayer()->getPlayerIndex() );
break;
}
#endif // #if defined(_DEBUG) || defined(_INTERNAL)
// ------------------------------------------------------------------------
#if defined(_DEBUG) || defined(_INTERNAL)
case GameMessage::MSG_META_DEMO_BEGIN_ADJUST_FOV:
{
DEBUG_ASSERTCRASH(!m_isChangingFOV, ("hmm, mismatched m_isChangingFOV"));
m_isChangingFOV = true;
m_anchor = m_currentPos;
break;
}
#endif // #if defined(_DEBUG) || defined(_INTERNAL)
// ------------------------------------------------------------------------
#if defined(_DEBUG) || defined(_INTERNAL)
case GameMessage::MSG_META_DEMO_END_ADJUST_FOV:
{
DEBUG_ASSERTCRASH(m_isChangingFOV, ("hmm, mismatched m_isChangingFOV"));
m_isChangingFOV = false;
break;
}
#endif // #if defined(_DEBUG) || defined(_INTERNAL)
//-----------------------------------------------------------------------------------------
case GameMessage::MSG_META_SAVE_VIEW1:
case GameMessage::MSG_META_SAVE_VIEW2:
case GameMessage::MSG_META_SAVE_VIEW3:
case GameMessage::MSG_META_SAVE_VIEW4:
case GameMessage::MSG_META_SAVE_VIEW5:
case GameMessage::MSG_META_SAVE_VIEW6:
case GameMessage::MSG_META_SAVE_VIEW7:
case GameMessage::MSG_META_SAVE_VIEW8:
{
Int slot = t - GameMessage::MSG_META_SAVE_VIEW1 + 1;
if ( slot > 0 && slot <= MAX_VIEW_LOCS )
{
TheTacticalView->getLocation( &m_viewLocation[slot-1] );
UnicodeString msg;
msg.format( TheGameText->fetch( "GUI:BookmarkXSet" ), slot );
TheInGameUI->message( msg );
}
disp = DESTROY_MESSAGE;
break;
}
//-----------------------------------------------------------------------------------------
case GameMessage::MSG_META_VIEW_VIEW1:
case GameMessage::MSG_META_VIEW_VIEW2:
case GameMessage::MSG_META_VIEW_VIEW3:
case GameMessage::MSG_META_VIEW_VIEW4:
case GameMessage::MSG_META_VIEW_VIEW5:
case GameMessage::MSG_META_VIEW_VIEW6:
case GameMessage::MSG_META_VIEW_VIEW7:
case GameMessage::MSG_META_VIEW_VIEW8:
{
Int slot = t - GameMessage::MSG_META_VIEW_VIEW1 + 1;
if ( slot > 0 && slot <= MAX_VIEW_LOCS )
{
TheTacticalView->setLocation( &m_viewLocation[slot-1] );
}
disp = DESTROY_MESSAGE;
break;
}
//-----------------------------------------------------------------------------
#if defined(_DEBUG) || defined(_INTERNAL)
case GameMessage::MSG_META_DEMO_LOCK_CAMERA_TO_PLANES:
{
Drawable *first = NULL;
if (m_lastPlaneID)
first = TheGameClient->findDrawableByID( m_lastPlaneID );
if (first == NULL)
first = TheGameClient->firstDrawable();
if (first)
{
Drawable *d = first;
Bool done = false;
while(!done)
{
// get next Drawable, wrapping around to head of list if necessary
d = d->getNextDrawable();
if (d == NULL)
d = TheGameClient->firstDrawable();
// if we've found an airborne object, lock onto it
// "isAboveTerrain" only indicates that we are currently in the air, but that
// could be the case if we are a buggy jumping a hill, or a unit being paradropped.
// the right thing would be to look at the locomotors.
// so this isn't really right, but will suffice for demo purposes.
if (d->getObject() && d->getObject()->isAboveTerrain() )
{
Bool doLock = true;
// but don't lock onto projectiles
ProjectileUpdateInterface* pui = NULL;
for (BehaviorModule** u = d->getObject()->getBehaviorModules(); *u; ++u)
{
if ((pui = (*u)->getProjectileUpdateInterface()) != NULL)
{
doLock = false;
break;
}
}
if (doLock)
{
TheTacticalView->setCameraLock( d->getObject()->getID() );
m_lastPlaneID = d->getID();
done = true;
break;
}
} // if airborne found
// if we're back to the first, quit
if (d == first)
break;
} // while
} // end plane lock
disp = DESTROY_MESSAGE;
break;
}
#endif // #if defined(_DEBUG) || defined(_INTERNAL)
} // end switch
return disp;
} // end LookAtTranslator
void LookAtTranslator::resetModes()
{
m_isScrolling = FALSE;
m_isRotating = FALSE;
m_isPitching = FALSE;
m_isChangingFOV = FALSE;
}