/*
** 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: W3DMouse.cpp /////////////////////////////////////////////////////////////////////////////
// Author: Mark W.
// Desc: W3D Mouse cursor implementations
///////////////////////////////////////////////////////////////////////////////////////////////////
#include "Common/GameMemory.h"
#include "WW3D2/DX8Wrapper.h"
#include "WW3D2/RendObj.h"
#include "WW3D2/HAnim.h"
#include "WW3D2/Camera.h"
#include "assetmgr.h"
#include "W3DDevice/Common/W3DConvert.h"
#include "W3DDevice/GameClient/W3DMouse.h"
#include "W3DDevice/GameClient/W3DDisplay.h"
#include "W3DDevice/GameClient/W3DAssetManager.h"
#include "W3DDevice/GameClient/W3DScene.h"
#include "GameClient/Display.h"
#include "GameClient/Image.h"
#include "GameClient/InGameUI.h"
#include "mutex.h"
#include "thread.h"
#ifdef _INTERNAL
// for occasional debugging...
//#pragma optimize("", off)
//#pragma message("************************************** WARNING, optimization disabled for debugging purposes")
#endif
//Since there can't be more than 1 mouse, might as well keep these static.
static CriticalSectionClass mutex;
static Bool isThread;
static TextureClass *cursorTextures[Mouse::NUM_MOUSE_CURSORS][MAX_2D_CURSOR_ANIM_FRAMES]; ///draw();
isThread=FALSE;
Switch_Thread();
}
}
W3DMouse::W3DMouse( void )
{
// zero our event list
for (Int i=0; iShowCursor(FALSE); //kill DX8 cursor
Win32Mouse::setCursor(ARROW); //enable default windows cursor
}
freeD3DAssets();
freeW3DAssets();
thread.Stop();
} // end Win32Mouse
void W3DMouse::initPolygonAssets(void)
{
CriticalSectionClass::LockClass m(mutex);
//don't allow the mouse thread to initialize
//wait for main app to do initialization.
if (isThread)
return;
//Check if texture assets already loaded
if (m_currentRedrawMode == RM_POLYGON && cursorImages[1] == NULL)
{
for (Int i=0; ifindImageByName(m_cursorInfo[i].imageName);
}
}
}
void W3DMouse::freePolygonAssets(void)
{
for (Int i=0; i MAX_2D_CURSOR_ANIM_FRAMES)
animFrames = MAX_2D_CURSOR_ANIM_FRAMES;
m_currentFrames=0;
if (animFrames == 1)
{ //single animation frame without trailing numbers
sprintf(FrameName,"%s.tga",baseName);
cursorTextures[cursor][0]= am->Get_Texture(FrameName);
m_currentD3DSurface[0]=cursorTextures[cursor][0]->Get_Surface_Level();
m_currentFrames = 1;
}
else
for (Int i=0; iGet_Texture(FrameName)) != NULL)
{ m_currentD3DSurface[m_currentFrames]=cursorTextures[cursor][i]->Get_Surface_Level();
m_currentFrames++;
}
}
return TRUE;
}
void W3DMouse::initD3DAssets(void)
{
//Nothing to do here unless we want to preload all possible cursors which would
//probably not be practical for memory reasons.
CriticalSectionClass::LockClass m(mutex);
//don't allow the mouse thread to initialize
//wait for main app to do initialization.
if (isThread)
return;
WW3DAssetManager *am=WW3DAssetManager::Get_Instance();
//Check if texture assets already loaded
if (m_currentRedrawMode == RM_DX8 && cursorTextures[1] == NULL && am)
{
for (Int i=0; iGet_Texture(m_cursorInfo[i].textureName.str());
m_currentD3DSurface[i]=NULL;
}
}
}
}
void W3DMouse::freeD3DAssets(void)
{
//free pointers to texture surfaces.
for (Int i=0; iCreate_Render_Obj(m_cursorInfo[i].W3DModelName.str(), m_cursorInfo[i].W3DScale*m_orthoZoom, 0);
else
cursorModels[i] = W3DDisplay::m_assetManager->Create_Render_Obj(m_cursorInfo[i].W3DModelName.str(), m_cursorInfo[i].W3DScale, 0);
if (cursorModels[i])
{
cursorModels[i]->Set_Position(Vector3(0.0f, 0.0f, -1.0f));
//W3DDisplay::m_3DInterfaceScene->Add_Render_Object(cursorModels[i]);
}
}
}
}
if ((cursorAnims[1] == NULL && W3DDisplay::m_assetManager))
{
for (Int i=1; iGet_HAnim(m_cursorInfo[i].W3DAnimName.str());
if (cursorAnims[i] && cursorModels[i])
{
cursorModels[i]->Set_Animation(cursorAnims[i], 0, (m_cursorInfo[i].loop) ? RenderObjClass::ANIM_MODE_LOOP : RenderObjClass::ANIM_MODE_ONCE);
}
}
}
}
// create the camera
m_camera = NEW_REF( CameraClass, () );
m_camera->Set_Position( Vector3( 0, 1, 1 ) );
Vector2 min = Vector2( -1, -1 );
Vector2 max = Vector2( +1, +1 );
m_camera->Set_View_Plane( min, max );
m_camera->Set_Clip_Planes( 0.995f, 20.0f );
if (m_orthoCamera)
m_camera->Set_Projection_Type( CameraClass::ORTHO );
}
void W3DMouse::freeW3DAssets(void)
{
for (Int i=0; iRemove_Render_Object(cursorModels[i]);
}
REF_PTR_RELEASE(cursorModels[i]);
REF_PTR_RELEASE(cursorAnims[i]);
}
REF_PTR_RELEASE(m_camera);
}
//-------------------------------------------------------------------------------------------------
/** Initialize our device */
//-------------------------------------------------------------------------------------------------
void W3DMouse::init( void )
{
//check if system already initialized and texture assets loaded.
Win32Mouse::init();
setCursor(ARROW); //set default starting cursor image
WWASSERT(!thread.Is_Running());
isThread=FALSE;
if (m_currentRedrawMode == RM_DX8)
thread.Execute();
thread.Set_Priority(0);
} // end int
//-------------------------------------------------------------------------------------------------
/** Reset */
//-------------------------------------------------------------------------------------------------
void W3DMouse::reset( void )
{
// extend
Win32Mouse::reset();
} // end reset
//-------------------------------------------------------------------------------------------------
/** Super basic simplistic cursor */
//-------------------------------------------------------------------------------------------------
void W3DMouse::setCursor( MouseCursor cursor )
{
CriticalSectionClass::LockClass m(mutex);
m_directionFrame=0;
if (m_currentRedrawMode == RM_WINDOWS)
{ //Windows default cursor needs to refreshed whenever we get a WM_SETCURSOR
m_currentD3DCursor=NONE;
m_currentW3DCursor=NONE;
m_currentPolygonCursor=NONE;
setCursorDirection(cursor);
if (m_drawing) //only allow cursor to change when drawing the cursor (once per frame) to fix flickering.
Win32Mouse::setCursor(cursor);
m_currentCursor = cursor;
return;
}
// extend
Mouse::setCursor( cursor );
// if we're already on this cursor ignore the rest of code to stop cursor flickering.
if( m_currentCursor == cursor && m_currentD3DCursor == cursor)
return;
//make sure Windows didn't reset our cursor
if (m_currentRedrawMode == RM_DX8)
{
SetCursor(NULL); //Kill Windows Cursor
LPDIRECT3DDEVICE8 m_pDev=DX8Wrapper::_Get_D3D_Device8();
Bool doImageChange=FALSE;
if (m_pDev != NULL)
{
m_pDev->ShowCursor(FALSE); //disable DX8 cursor
if (cursor != m_currentD3DCursor)
{ if (!isThread)
{ releaseD3DCursorTextures(m_currentD3DCursor);
//Since this type of cursor is updated from a non-D3D thread, we need
//to preallocate all surfaces in main thread.
loadD3DCursorTextures(cursor);
}
}
if (m_currentD3DSurface[0])
doImageChange=TRUE;
}
//For DX8 Cursors, we continually set the image on every call even when
//it didn't change. This is needed to prevent the cursor from flickering.
if (doImageChange)
{
HRESULT res;
m_currentHotSpot = m_cursorInfo[cursor].hotSpotPosition;
m_currentFMS = m_cursorInfo[cursor].fps/1000.0f;
m_currentAnimFrame = 0; //reset animation when cursor changes
res = m_pDev->SetCursorProperties(m_currentHotSpot.x,m_currentHotSpot.y,m_currentD3DSurface[(Int)m_currentAnimFrame]->Peek_D3D_Surface());
m_pDev->ShowCursor(TRUE); //Enable DX8 cursor
m_currentD3DFrame=(Int)m_currentAnimFrame;
m_currentD3DCursor = cursor;
m_lastAnimTime=timeGetTime();
}
}
else if (m_currentRedrawMode == RM_POLYGON)
{
SetCursor(NULL); //Kill Windows Cursor
m_currentD3DCursor=NONE;
m_currentW3DCursor=NONE;
m_currentPolygonCursor = cursor;
m_currentHotSpot = m_cursorInfo[cursor].hotSpotPosition;
}
else if (m_currentRedrawMode == RM_W3D)
{
SetCursor(NULL); //Kill Windows Cursor
m_currentD3DCursor=NONE;
m_currentPolygonCursor=NONE;
if (cursor != m_currentW3DCursor)
{
// set the new model visible
if (!cursorModels[1])
initW3DAssets();
if (cursorModels[1])
{
if (cursorModels[m_currentW3DCursor])
{
W3DDisplay::m_3DInterfaceScene->Remove_Render_Object(cursorModels[m_currentW3DCursor]);
}
m_currentW3DCursor=cursor;
if (cursorModels[m_currentW3DCursor])
{
W3DDisplay::m_3DInterfaceScene->Add_Render_Object(cursorModels[m_currentW3DCursor]);
if (m_cursorInfo[m_currentW3DCursor].loop == FALSE && cursorAnims[m_currentW3DCursor])
{
cursorModels[m_currentW3DCursor]->Set_Animation(cursorAnims[m_currentW3DCursor], 0, RenderObjClass::ANIM_MODE_ONCE);
}
}
}
}
else
{
m_currentW3DCursor=cursor;
}
}
// save current cursor
m_currentCursor = cursor;
} // end setCursor
extern HWND ApplicationHWnd;
void W3DMouse::draw(void)
{
CriticalSectionClass::LockClass m(mutex);
m_drawing = TRUE;
//make sure the correct cursor image is selected
setCursor(m_currentCursor);
if (m_currentRedrawMode == RM_DX8 && m_currentD3DCursor != NONE)
{
//called from upate thread or rendering loop. Tells D3D where
//to draw the mouse cursor.
LPDIRECT3DDEVICE8 m_pDev=DX8Wrapper::_Get_D3D_Device8();
if (m_pDev)
{ m_pDev->ShowCursor(TRUE); //Enable DX8 cursor
if (TheDisplay && !TheDisplay->getWindowed())
{ //if we're full-screen, need to manually move cursor image
POINT ptCursor;
GetCursorPos( &ptCursor );
ScreenToClient( ApplicationHWnd, &ptCursor );
m_pDev->SetCursorPosition( ptCursor.x, ptCursor.y, D3DCURSOR_IMMEDIATE_UPDATE);
}
//Check if animated cursor and new frame
if (m_currentFrames > 1)
{
Int msTime=timeGetTime();
m_currentAnimFrame += (msTime-m_lastAnimTime) * m_currentFMS;
m_currentAnimFrame=fmod(m_currentAnimFrame,m_currentFrames);
m_lastAnimTime=msTime;
if ((Int)m_currentAnimFrame != m_currentD3DFrame)
{
m_currentD3DFrame=(Int)m_currentAnimFrame;
m_pDev->SetCursorProperties(m_currentHotSpot.x,m_currentHotSpot.y,m_currentD3DSurface[m_currentD3DFrame]->Peek_D3D_Surface());
}
}
}
}
else if (m_currentRedrawMode == RM_POLYGON)
{
const Image *image=cursorImages[m_currentPolygonCursor];
if (image)
{
TheDisplay->drawImage(image,m_currMouse.pos.x-m_currentHotSpot.x,m_currMouse.pos.y-m_currentHotSpot.y,
m_currMouse.pos.x+image->getImageWidth()-m_currentHotSpot.x, m_currMouse.pos.y+image->getImageHeight()-m_currentHotSpot.y);
}
}
else if (m_currentRedrawMode == RM_WINDOWS)
{
}
else if (m_currentRedrawMode == RM_W3D)
{
if ( W3DDisplay::m_3DInterfaceScene && m_camera && m_visible)
{
if (cursorModels[m_currentW3DCursor])
{
Real xPercent = (1.0f - (TheDisplay->getWidth() - m_currMouse.pos.x) / (Real)TheDisplay->getWidth());
Real yPercent = ((TheDisplay->getHeight() - m_currMouse.pos.y) / (Real)TheDisplay->getHeight());
Real x, y, z = -1.0f;
if (m_orthoCamera)
{
x = xPercent*2 - 1;
y = yPercent*2;
}
else
{
//W3D Screen coordinates are -1 to 1, so we need to do some conversion:
Real logX, logY;
PixelScreenToW3DLogicalScreen(m_currMouse.pos.x - 0, m_currMouse.pos.y - 0, &logX, &logY, TheDisplay->getWidth(), TheDisplay->getHeight());
Vector3 rayStart;
Vector3 rayEnd;
rayStart = m_camera->Get_Position(); //get camera location
m_camera->Un_Project(rayEnd,Vector2(logX,logY)); //get world space point
rayEnd -= rayStart; //vector camera to world space point
rayEnd.Normalize(); //make unit vector
rayEnd *= m_camera->Get_Depth(); //adjust length to reach far clip plane
rayEnd += rayStart; //get point on far clip plane along ray from camera.
x = Vector3::Find_X_At_Z(z, rayStart, rayEnd);
y = Vector3::Find_Y_At_Z(z, rayStart, rayEnd);
}
Matrix3D tm(1);
tm.Set_Translation(Vector3(x, y, z));
Coord2D offset = {0, 0};
if (TheInGameUI && TheInGameUI->isScrolling())
{
offset = TheInGameUI->getScrollAmount();
offset.normalize();
Real theta = atan2(-offset.y, offset.x);
theta -= (Real)M_PI/2;
tm.Rotate_Z(theta);
}
cursorModels[m_currentW3DCursor]->Set_Transform(tm);
WW3D::Render( W3DDisplay::m_3DInterfaceScene, m_camera );
}
}
}
//@todo: In DX8 mode the mouse is drawn in another thread which isn't allowed
//access to D3D so we can't do any drawing here.
// draw the cursor text
if (!isThread)
drawCursorText();
// draw tooltip text
if (m_visible && !isThread)
drawTooltip();
m_drawing = FALSE;
}
void W3DMouse::setRedrawMode(RedrawMode mode)
{
MouseCursor cursor = getMouseCursor();
//Turn off the previous cursor mode
setCursor(NONE);
m_currentRedrawMode=mode;
switch (mode)
{
case RM_WINDOWS:
{ //Windows mouse doesn't need an update thread.
if (thread.Is_Running())
thread.Stop();
freeD3DAssets(); //using Windows resources
freeW3DAssets();
freePolygonAssets();
m_currentD3DCursor = NONE;
m_currentW3DCursor = NONE;
m_currentPolygonCursor = NONE;
}
break;
case RM_W3D:
{ //Model mouse updated only at render time so doesn't
//require thread.
if (thread.Is_Running())
thread.Stop();
freeD3DAssets(); //using packed Image data, not textures.
freePolygonAssets();
m_currentD3DCursor = NONE;
m_currentPolygonCursor = NONE;
initW3DAssets();
}
break;
case RM_POLYGON:
{ //Polygon mouse updated only at render time so doesn't
//require thread.
if (thread.Is_Running())
thread.Stop();
freeD3DAssets(); //using packed Image data, not textures.
freeW3DAssets();
m_currentD3DCursor = NONE;
m_currentW3DCursor = NONE;
m_currentPolygonCursor = NONE;
initPolygonAssets();
}
break;
case RM_DX8:
{ //this cursor type is drawn by DX8 and can be refreshed
//independent of rendering rate. Uses another thread to do
//position updates.
initD3DAssets(); //make sure textures loaded.
freeW3DAssets();
freePolygonAssets();
if (!thread.Is_Running())
thread.Execute();
m_currentW3DCursor = NONE;
m_currentPolygonCursor = NONE;
break;
}
}
setCursor(NONE);
setCursor(cursor);
}
void W3DMouse::setCursorDirection(MouseCursor cursor)
{
Coord2D offset = {0, 0};
//Check if we have a directional cursor that needs different images for each direction
if (m_cursorInfo[cursor].numDirections > 1 && TheInGameUI && TheInGameUI->isScrolling())
{
offset = TheInGameUI->getScrollAmount();
if (offset.x || offset.y)
{
offset.normalize();
Real theta = atan2(offset.y, offset.x);
theta = fmod(theta+M_PI*2,M_PI*2);
Int numDirections=m_cursorInfo[m_currentCursor].numDirections;
//Figure out which of our predrawn cursor orientations best matches the
//actual cursor direction. Frame 0 is assumed to point right and continue
//clockwise.
m_directionFrame=(Int)(theta/(2.0f*M_PI/(Real)numDirections)+0.5f);
if (m_directionFrame >= numDirections)
m_directionFrame = 0;
}
else
{
m_directionFrame=0;
}
}
else
m_directionFrame = 0;
}