diff --git a/Core/GameEngine/Include/Common/OptionPreferences.h b/Core/GameEngine/Include/Common/OptionPreferences.h index 85aba4228be..6d7f664d8ba 100644 --- a/Core/GameEngine/Include/Common/OptionPreferences.h +++ b/Core/GameEngine/Include/Common/OptionPreferences.h @@ -76,6 +76,7 @@ class OptionPreferences : public UserPreferences Real getScrollFactor(); Bool getDrawScrollAnchor(); Bool getMoveScrollAnchor(); + Bool getSelectionCircleEnabled() const; Bool getCursorCaptureEnabledInWindowedGame() const; Bool getCursorCaptureEnabledInWindowedMenu() const; Bool getCursorCaptureEnabledInFullscreenGame() const; diff --git a/Core/GameEngine/Source/Common/OptionPreferences.cpp b/Core/GameEngine/Source/Common/OptionPreferences.cpp index e681ef8b192..ec2ba7ffd20 100644 --- a/Core/GameEngine/Source/Common/OptionPreferences.cpp +++ b/Core/GameEngine/Source/Common/OptionPreferences.cpp @@ -216,6 +216,20 @@ Bool OptionPreferences::getRightMouseScrollWithAlternateMouseEnabled() const return FALSE; } +// TheSuperHackers @feature Options.ini: SelectionCircle = Yes draws a green ring on the ground +// under every selected object, so the current selection reads at a glance. +Bool OptionPreferences::getSelectionCircleEnabled() const +{ + OptionPreferences::const_iterator it = find("SelectionCircle"); + if (it == end()) + return FALSE; + + if (stricmp(it->second.str(), "yes") == 0) { + return TRUE; + } + return FALSE; +} + Bool OptionPreferences::getRetaliationModeEnabled() { OptionPreferences::const_iterator it = find("Retaliation"); diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/Module/W3DModelDraw.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/Module/W3DModelDraw.h index 9defb85d715..5e3f0241f44 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/Module/W3DModelDraw.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/Module/W3DModelDraw.h @@ -363,6 +363,9 @@ class W3DModelDraw : public DrawModule, public ObjectDrawInterface virtual void setFullyObscuredByShroud(Bool fullyObscured) override; virtual void setTerrainDecal(TerrainDecalType type) override; + // TheSuperHackers @feature Selection ring, kept in its own slot so it does not evict the + // horde or chem suit decal while a unit is selected. + virtual void setSelectionDecal(Bool enable, Real radius) override; virtual Bool isVisible() const override; virtual void reactToTransformChange(const Matrix3D* oldMtx, const Coord3D* oldPos, Real oldAngle) override; @@ -501,6 +504,12 @@ class W3DModelDraw : public DrawModule, public ObjectDrawInterface RenderObjClass* m_renderObject; ///< W3D Render object for this drawable Shadow* m_shadow; ///< Updates/Renders shadows of this object Shadow* m_terrainDecal; + // TheSuperHackers @feature Selection ring decal, independent of m_terrainDecal. The wanted + // state is remembered separately so the ring survives render object rebuilds - a damage + // state or upgrade swaps the model while the object stays selected. + Shadow* m_selectionDecal; + Bool m_selectionDecalWanted; + Real m_selectionDecalRadius; TerrainTracksRenderObjClass* m_trackRenderObject; ///< This is rendered under object ParticleSystemIDVec m_particleSystemIDs; ///< The ID numbers of the particle systems currently running. std::vector m_subObjectVec; diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp index 6b9d2bd3095..5c1ad4cd725 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DModelDraw.cpp @@ -1732,6 +1732,10 @@ W3DModelDraw::W3DModelDraw(Thing *thing, const ModuleData* moduleData) : DrawMod m_shadow = nullptr; m_shadowEnabled = TRUE; m_terrainDecal = nullptr; + // TheSuperHackers @feature no selection ring until one is asked for + m_selectionDecal = nullptr; + m_selectionDecalWanted = FALSE; + m_selectionDecalRadius = 0.0f; m_trackRenderObject = nullptr; m_whichAnimInCurState = -1; m_nextState = nullptr; @@ -1836,6 +1840,10 @@ void W3DModelDraw::setHidden(Bool hidden) if (m_terrainDecal) m_terrainDecal->enableShadowRender(!hidden); + // TheSuperHackers @fix the ring must not keep rendering under a hidden drawable + if (m_selectionDecal) + m_selectionDecal->enableShadowRender(!hidden); + if (m_trackRenderObject && hidden) { const Coord3D* pos = getDrawable()->getPosition(); m_trackRenderObject->addCapEdgeToTrack(pos->x,pos->y); @@ -1949,6 +1957,9 @@ void W3DModelDraw::setFullyObscuredByShroud(Bool fullyObscured) m_shadow->enableShadowInvisible(m_fullyObscuredByShroud); if (m_terrainDecal) m_terrainDecal->enableShadowInvisible(m_fullyObscuredByShroud); + // TheSuperHackers @fix the ring must not expose a fully shrouded unit + if (m_selectionDecal) + m_selectionDecal->enableShadowInvisible(m_fullyObscuredByShroud); doStartOrStopParticleSys(); } @@ -2719,6 +2730,53 @@ Bool W3DModelDraw::updateBonesForClientParticleSystems() +//------------------------------------------------------------------------------------------------- +// TheSuperHackers @feature Selection ring decal. +//------------------------------------------------------------------------------------------------- +/** Put a green ring on the ground under this object, or take it away. + * + * Uses the projected shadow system rather than screen space lines, so the ring is genuinely + * projected onto the terrain and the model draws over it. It lives in its own slot rather than + * sharing m_terrainDecal, so selecting a horde unit does not evict its horde ring. + * + * Expects a PlainRingSelection.tga in the mod's assets. The engine appends the extension, and + * the art is tinted green at runtime, so a plain white or greyscale ring works. */ +//------------------------------------------------------------------------------------------------- +void W3DModelDraw::setSelectionDecal(Bool enable, Real radius) +{ + // remembered so the ring can be recreated after a model swap tears the render object down + m_selectionDecalWanted = enable; + m_selectionDecalRadius = radius; + + if (m_selectionDecal) + { + m_selectionDecal->release(); + m_selectionDecal = nullptr; + } + + if (!enable || m_renderObject == nullptr || TheProjectedShadowManager == nullptr) + return; + + Shadow::ShadowTypeInfo decalInfo; + decalInfo.allowUpdates = FALSE; //the ring never needs regenerating + decalInfo.allowWorldAlign = TRUE; //wrap it around terrain and world objects + decalInfo.m_type = SHADOW_ALPHA_DECAL; + strlcpy(decalInfo.m_ShadowName, "PlainRingSelection", ARRAY_SIZE(decalInfo.m_ShadowName)); + decalInfo.m_sizeX = radius * 2.0f; + decalInfo.m_sizeY = radius * 2.0f; + decalInfo.m_offsetX = 0.0f; + decalInfo.m_offsetY = 0.0f; + + m_selectionDecal = TheProjectedShadowManager->addDecal(m_renderObject, &decalInfo); + if (m_selectionDecal) + { + m_selectionDecal->enableShadowInvisible(m_fullyObscuredByShroud); + m_selectionDecal->enableShadowRender(TRUE); + //the art is a plain ring, so tint it to the selection green + m_selectionDecal->setColor(GameMakeColor(0, 255, 0, 255)); + } +} + //------------------------------------------------------------------------------------------------- void W3DModelDraw::setTerrainDecal(TerrainDecalType type) { @@ -2790,6 +2848,14 @@ void W3DModelDraw::nukeCurrentRender(Matrix3D* xform) m_terrainDecal->release(); m_terrainDecal = nullptr; + // TheSuperHackers @fix The selection ring is bound to the render object about to be torn down + // here, exactly like the shadow and the terrain decal above, so it has to go with them. Left + // behind it stayed registered with the projected shadow manager while the render object it + // points at was freed, and the next renderShadows walked that dangling entry into the driver. + if (m_selectionDecal) + m_selectionDecal->release(); + m_selectionDecal = nullptr; + // remove existing render object from the scene if (m_renderObject) { @@ -3078,6 +3144,11 @@ void W3DModelDraw::setModelState(const ModelConditionInfo* newState) } } + // TheSuperHackers @fix The selection ring was bound to the render object that was just + // torn down; the object is still selected, so put the ring back on the new one. + if (m_selectionDecalWanted) + setSelectionDecal(TRUE, m_selectionDecalRadius); + if( m_renderObject ) { // set collision type for render object. Used by WW3D2 collision code. diff --git a/Generals/Code/GameEngine/Include/Common/DrawModule.h b/Generals/Code/GameEngine/Include/Common/DrawModule.h index 1867db65564..e920b609d24 100644 --- a/Generals/Code/GameEngine/Include/Common/DrawModule.h +++ b/Generals/Code/GameEngine/Include/Common/DrawModule.h @@ -82,6 +82,9 @@ class DrawModule : public DrawableModule virtual void setTerrainDecal(TerrainDecalType type) {}; virtual void setTerrainDecalSize(Real x, Real y) {}; virtual void setTerrainDecalOpacity(Real o) {}; + // TheSuperHackers @feature Selection ring decal, in its own slot so it does not evict the + // horde or chem suit decal while a unit is selected. + virtual void setSelectionDecal(Bool enable, Real radius) {}; virtual void setFullyObscuredByShroud(Bool fullyObscured) = 0; diff --git a/GeneralsMD/Code/GameEngine/Include/Common/DrawModule.h b/GeneralsMD/Code/GameEngine/Include/Common/DrawModule.h index d2595fc4954..00ccc8a25e9 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/DrawModule.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/DrawModule.h @@ -82,6 +82,9 @@ class DrawModule : public DrawableModule virtual void setTerrainDecal(TerrainDecalType type) {}; virtual void setTerrainDecalSize(Real x, Real y) {}; virtual void setTerrainDecalOpacity(Real o) {}; + // TheSuperHackers @feature Selection ring decal, in its own slot so it does not evict the + // horde or chem suit decal while a unit is selected. + virtual void setSelectionDecal(Bool enable, Real radius) {}; virtual void setFullyObscuredByShroud(Bool fullyObscured) = 0; diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 89a5fa08f9d..f8ad4424d16 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -142,6 +142,8 @@ class GlobalData : public SubsystemInterface Bool m_useAlternateMouse; Bool m_useRightMouseScrollWithAlternateMouse; // TheSuperHackers @feature User option for RMB scroll in Alternate Mouse mode. Bool m_clientRetaliationModeEnabled; + // TheSuperHackers @feature Draw a green hexagon ring under selected objects. + Bool m_selectionCircleEnabled; Bool m_doubleClickAttackMove; Bool m_rightMouseAlwaysScrolls; Int m_jpegQuality; // TheSuperHackers @feature Quality for JPEG screenshots. diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h b/GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h index 7169b8cb51f..487bf1acb79 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/Drawable.h @@ -396,6 +396,9 @@ class Drawable : public Thing, /// Return true if drawable has been marked as "selected" Bool isSelected() const { return m_selected; } + // TheSuperHackers @feature Green selection ring decal (Options.ini: SelectionCircle). + // Client only -- never xfer'd, never read by game logic. + void updateSelectionDecal( void ); void onSelected(); ///< Work unrelated to selection that must happen at time of selection void onUnselected(); ///< Work unrelated to selection that must happen at time of unselection diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index e862cd149d5..d8e9f4d7cd1 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -1062,6 +1062,7 @@ GlobalData::GlobalData() m_useRightMouseScrollWithAlternateMouse = TRUE; #endif m_clientRetaliationModeEnabled = TRUE; //On by default. + m_selectionCircleEnabled = FALSE; m_doubleClickAttackMove = FALSE; } @@ -1206,6 +1207,7 @@ void GlobalData::parseGameDataDefinition( INI* ini ) TheWritableGlobalData->m_useRightMouseScrollWithAlternateMouse = optionPref.getRightMouseScrollWithAlternateMouseEnabled(); TheWritableGlobalData->m_clientRetaliationModeEnabled = optionPref.getRetaliationModeEnabled(); TheWritableGlobalData->m_doubleClickAttackMove = optionPref.getDoubleClickAttackMoveEnabled(); + TheWritableGlobalData->m_selectionCircleEnabled = optionPref.getSelectionCircleEnabled(); TheWritableGlobalData->m_jpegQuality = optionPref.getJpegQuality(); TheWritableGlobalData->m_keyboardScrollFactor = optionPref.getScrollFactor(); TheWritableGlobalData->m_drawScrollAnchor = optionPref.getDrawScrollAnchor(); diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp index 54dff3325f2..a28d5bb9743 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -979,8 +979,50 @@ void Drawable::colorTint( const RGBColor* color ) //------------------------------------------------------------------------------------------------- /** Gathering point for all things besides actual selection that must happen on selection */ //------------------------------------------------------------------------------------------------- +// TheSuperHackers @feature Selection ring decal. +//------------------------------------------------------------------------------------------------- +/** Show or hide the green selection ring, following the SelectionCircle option. + * + * Only the first draw module gets one, matching how terrain decals avoid stacking. */ +//------------------------------------------------------------------------------------------------- +void Drawable::updateSelectionDecal( void ) +{ + Bool wanted = TheGlobalData && TheGlobalData->m_selectionCircleEnabled && isSelected(); + + const Object *obj = getObject(); + if( obj == nullptr || obj->isEffectivelyDead() || obj->isKindOf( KINDOF_IGNORED_IN_GUI ) ) + wanted = FALSE; + + Real radius = 0.0f; + if( wanted ) + { + // The bounding circle encloses the whole model, so it reads as too big drawn at full + // size. How much too big depends on the shape: a building fills its circle, a soldier + // barely occupies the middle of one, so scale per kind. + Real scale; + if( obj->isKindOf( KINDOF_STRUCTURE ) ) + scale = 1.0f; + else if( obj->isKindOf( KINDOF_INFANTRY ) ) + scale = 0.7f; + else + scale = 0.85f; // vehicles, aircraft and everything else + + radius = getDrawableGeometryInfo().getBoundingCircleRadius() * scale; + if( radius < 1.0f ) + radius = 1.0f; + } + + for( DrawModule **dm = getDrawModules(); *dm; ++dm ) + { + (*dm)->setSelectionDecal( wanted, radius ); + break; // first draw module only, so rings do not stack + } +} + void Drawable::onSelected() { + // TheSuperHackers @feature put the selection ring up straight away rather than waiting a frame + updateSelectionDecal(); flashAsSelected();//much simpler @@ -1001,7 +1043,8 @@ void Drawable::onSelected() //------------------------------------------------------------------------------------------------- void Drawable::onUnselected() { - // nothing + // TheSuperHackers @feature take the selection ring down + updateSelectionDecal(); } //------------------------------------------------------------------------------------------------- @@ -4161,6 +4204,9 @@ void Drawable::reactToTransformChange(const Matrix3D* oldMtx, const Coord3D* old //------------------------------------------------------------------------------------------------- void Drawable::reactToGeometryChange() { + // TheSuperHackers @fix resize the selection ring along with the geometry it is derived from + updateSelectionDecal(); + for (DrawModule** dm = getDrawModules(); *dm; ++dm) { (*dm)->reactToGeometryChange();