LCOV - code coverage report
Current view: top level - source/gui/ObjectTypes - CMiniMap.cpp (source / functions) Hit Total Coverage
Test: 0 A.D. test coverage report Lines: 2 217 0.9 %
Date: 2023-01-19 00:18:29 Functions: 2 17 11.8 %

          Line data    Source code
       1             : /* Copyright (C) 2023 Wildfire Games.
       2             :  * This file is part of 0 A.D.
       3             :  *
       4             :  * 0 A.D. is free software: you can redistribute it and/or modify
       5             :  * it under the terms of the GNU General Public License as published by
       6             :  * the Free Software Foundation, either version 2 of the License, or
       7             :  * (at your option) any later version.
       8             :  *
       9             :  * 0 A.D. is distributed in the hope that it will be useful,
      10             :  * but WITHOUT ANY WARRANTY; without even the implied warranty of
      11             :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      12             :  * GNU General Public License for more details.
      13             :  *
      14             :  * You should have received a copy of the GNU General Public License
      15             :  * along with 0 A.D.  If not, see <http://www.gnu.org/licenses/>.
      16             :  */
      17             : 
      18             : #include "precompiled.h"
      19             : 
      20             : #include "CMiniMap.h"
      21             : 
      22             : #include "graphics/Canvas2D.h"
      23             : #include "graphics/GameView.h"
      24             : #include "graphics/MiniMapTexture.h"
      25             : #include "graphics/MiniPatch.h"
      26             : #include "graphics/Terrain.h"
      27             : #include "graphics/TerrainTextureEntry.h"
      28             : #include "graphics/TerrainTextureManager.h"
      29             : #include "graphics/TextureManager.h"
      30             : #include "gui/CGUI.h"
      31             : #include "gui/GUIManager.h"
      32             : #include "lib/bits.h"
      33             : #include "lib/external_libraries/libsdl.h"
      34             : #include "lib/timer.h"
      35             : #include "maths/MathUtil.h"
      36             : #include "ps/CLogger.h"
      37             : #include "ps/ConfigDB.h"
      38             : #include "ps/Filesystem.h"
      39             : #include "ps/Game.h"
      40             : #include "ps/GameSetup/Config.h"
      41             : #include "ps/Profile.h"
      42             : #include "ps/World.h"
      43             : #include "renderer/Renderer.h"
      44             : #include "renderer/SceneRenderer.h"
      45             : #include "renderer/WaterManager.h"
      46             : #include "scriptinterface/Object.h"
      47             : #include "simulation2/Simulation2.h"
      48             : #include "simulation2/components/ICmpMinimap.h"
      49             : #include "simulation2/components/ICmpRangeManager.h"
      50             : #include "simulation2/helpers/Los.h"
      51             : #include "simulation2/system/ParamNode.h"
      52             : 
      53             : #include <array>
      54             : #include <cmath>
      55             : #include <vector>
      56             : 
      57             : namespace
      58             : {
      59             : 
      60             : // Adds segments pieces lying inside the circle to lines.
      61           0 : void CropPointsByCircle(const std::array<CVector3D, 4>& points, const CVector3D& center, const float radius, std::vector<CVector3D>* lines)
      62             : {
      63           0 :     constexpr float EPS = 1e-3f;
      64           0 :     lines->reserve(points.size() * 2);
      65           0 :     for (size_t idx = 0; idx < points.size(); ++idx)
      66             :     {
      67           0 :         const CVector3D& currentPoint = points[idx];
      68           0 :         const CVector3D& nextPoint = points[(idx + 1) % points.size()];
      69           0 :         const CVector3D direction = (nextPoint - currentPoint).Normalized();
      70           0 :         const CVector3D normal(direction.Z, 0.0f, -direction.X);
      71           0 :         const float offset = normal.Dot(currentPoint) - normal.Dot(center);
      72             :         // We need to have lines only inside the circle.
      73           0 :         if (std::abs(offset) + EPS >= radius)
      74           0 :             continue;
      75           0 :         const CVector3D closestPoint = center + normal * offset;
      76           0 :         const float halfChordLength = sqrt(radius * radius - offset * offset);
      77           0 :         const CVector3D intersectionA = closestPoint - direction * halfChordLength;
      78           0 :         const CVector3D intersectionB = closestPoint + direction * halfChordLength;
      79             :         // We have no intersection if the segment is lying outside of the circle.
      80           0 :         if (direction.Dot(currentPoint) + EPS > direction.Dot(intersectionB) ||
      81           0 :             direction.Dot(nextPoint) - EPS < direction.Dot(intersectionA))
      82           0 :             continue;
      83             : 
      84           0 :         lines->emplace_back(
      85           0 :             direction.Dot(currentPoint) > direction.Dot(intersectionA) ? currentPoint : intersectionA);
      86           0 :         lines->emplace_back(
      87           0 :             direction.Dot(nextPoint) < direction.Dot(intersectionB) ? nextPoint : intersectionB);
      88             :     }
      89           0 : }
      90             : 
      91             : } // anonymous namespace
      92             : 
      93           1 : const CStr CMiniMap::EventNameWorldClick = "WorldClick";
      94             : 
      95           0 : CMiniMap::CMiniMap(CGUI& pGUI) :
      96             :     IGUIObject(pGUI),
      97             :     m_MapSize(0), m_MapScale(1.f), m_Mask(this, "mask", false),
      98             :     m_FlareTextureCount(this, "flare_texture_count", 0), m_FlareRenderSize(this, "flare_render_size", 0),
      99             :     m_FlareInterleave(this, "flare_interleave", false), m_FlareAnimationSpeed(this, "flare_animation_speed", 0.0f),
     100             :     m_FlareLifetimeSeconds(this, "flare_lifetime_seconds", 0.0f),
     101             :     m_FlareStartFadeSeconds(this, "flare_start_fade_seconds", 0.0f),
     102           0 :     m_FlareStopFadeSeconds(this, "flare_stop_fade_seconds", 0.0f)
     103             : {
     104           0 :     m_Clicking = false;
     105           0 :     m_MouseHovering = false;
     106           0 : }
     107             : 
     108             : CMiniMap::~CMiniMap() = default;
     109             : 
     110           0 : void CMiniMap::HandleMessage(SGUIMessage& Message)
     111             : {
     112           0 :     IGUIObject::HandleMessage(Message);
     113           0 :     switch (Message.type)
     114             :     {
     115           0 :     case GUIM_LOAD:
     116           0 :         RecreateFlareTextures();
     117           0 :         break;
     118           0 :     case GUIM_SETTINGS_UPDATED:
     119           0 :         if (Message.value == "flare_texture_count")
     120           0 :             RecreateFlareTextures();
     121           0 :         break;
     122           0 :     case GUIM_MOUSE_PRESS_LEFT:
     123           0 :         if (m_MouseHovering)
     124             :         {
     125           0 :             if (!CMiniMap::FireWorldClickEvent(SDL_BUTTON_LEFT, 1))
     126             :             {
     127           0 :                 SetCameraPositionFromMousePosition();
     128           0 :                 m_Clicking = true;
     129             :             }
     130             :         }
     131           0 :         break;
     132           0 :     case GUIM_MOUSE_RELEASE_LEFT:
     133           0 :         if (m_MouseHovering && m_Clicking)
     134           0 :             SetCameraPositionFromMousePosition();
     135           0 :         m_Clicking = false;
     136           0 :         break;
     137           0 :     case GUIM_MOUSE_DBLCLICK_LEFT:
     138           0 :         if (m_MouseHovering && m_Clicking)
     139           0 :             SetCameraPositionFromMousePosition();
     140           0 :         m_Clicking = false;
     141           0 :         break;
     142           0 :     case GUIM_MOUSE_ENTER:
     143           0 :         m_MouseHovering = true;
     144           0 :         break;
     145           0 :     case GUIM_MOUSE_LEAVE:
     146           0 :         m_Clicking = false;
     147           0 :         m_MouseHovering = false;
     148           0 :         break;
     149           0 :     case GUIM_MOUSE_RELEASE_RIGHT:
     150           0 :         CMiniMap::FireWorldClickEvent(SDL_BUTTON_RIGHT, 1);
     151           0 :         break;
     152           0 :     case GUIM_MOUSE_DBLCLICK_RIGHT:
     153           0 :         CMiniMap::FireWorldClickEvent(SDL_BUTTON_RIGHT, 2);
     154           0 :         break;
     155           0 :     case GUIM_MOUSE_MOTION:
     156           0 :         if (m_MouseHovering && m_Clicking)
     157           0 :             SetCameraPositionFromMousePosition();
     158           0 :         break;
     159           0 :     case GUIM_MOUSE_WHEEL_DOWN:
     160             :     case GUIM_MOUSE_WHEEL_UP:
     161           0 :         Message.Skip();
     162           0 :         break;
     163             : 
     164           0 :     default:
     165           0 :         break;
     166             :     }
     167           0 : }
     168             : 
     169           0 : void CMiniMap::RecreateFlareTextures()
     170             : {
     171             :     // Catch invalid values.
     172           0 :     if (m_FlareTextureCount > 99)
     173             :     {
     174           0 :         LOGERROR("Invalid value for flare texture count. Valid range is 0-99.");
     175           0 :         return;
     176             :     }
     177           0 :     const CStr textureNumberingFormat = "art/textures/animated/minimap-flare/frame%02u.png";
     178           0 :     m_FlareTextures.clear();
     179           0 :     m_FlareTextures.reserve(m_FlareTextureCount);
     180           0 :     for (u32 i = 0; i < m_FlareTextureCount; ++i)
     181             :     {
     182           0 :         CTextureProperties textureProps(fmt::sprintf(textureNumberingFormat, i).c_str());
     183           0 :         textureProps.SetIgnoreQuality(true);
     184           0 :         m_FlareTextures.emplace_back(g_Renderer.GetTextureManager().CreateTexture(textureProps));
     185             :     }
     186             : }
     187             : 
     188           0 : bool CMiniMap::IsMouseOver() const
     189             : {
     190           0 :     const CVector2D& mousePos = m_pGUI.GetMousePos();
     191             :     // Take the magnitude of the difference of the mouse position and minimap center.
     192           0 :     const float distanceFromCenter = (mousePos - m_CachedActualSize.CenterPoint()).Length();
     193             :     // If the distance is less then the radius of the minimap (half the width) the mouse is over the minimap.
     194           0 :     return distanceFromCenter < m_CachedActualSize.GetWidth() / 2.0;
     195             : }
     196             : 
     197           0 : void CMiniMap::GetMouseWorldCoordinates(float& x, float& z) const
     198             : {
     199             :     // Determine X and Z according to proportion of mouse position and minimap.
     200           0 :     const CVector2D& mousePos = m_pGUI.GetMousePos();
     201             : 
     202           0 :     float px = (mousePos.X - m_CachedActualSize.left) / m_CachedActualSize.GetWidth();
     203           0 :     float py = (m_CachedActualSize.bottom - mousePos.Y) / m_CachedActualSize.GetHeight();
     204             : 
     205           0 :     float angle = GetAngle();
     206             : 
     207             :     // Scale world coordinates for shrunken square map
     208           0 :     x = TERRAIN_TILE_SIZE * m_MapSize * (m_MapScale * (cos(angle)*(px-0.5) - sin(angle)*(py-0.5)) + 0.5);
     209           0 :     z = TERRAIN_TILE_SIZE * m_MapSize * (m_MapScale * (cos(angle)*(py-0.5) + sin(angle)*(px-0.5)) + 0.5);
     210           0 : }
     211             : 
     212           0 : void CMiniMap::SetCameraPositionFromMousePosition()
     213             : {
     214           0 :     CTerrain* terrain = g_Game->GetWorld()->GetTerrain();
     215             : 
     216           0 :     CVector3D target;
     217           0 :     GetMouseWorldCoordinates(target.X, target.Z);
     218           0 :     target.Y = terrain->GetExactGroundLevel(target.X, target.Z);
     219           0 :     g_Game->GetView()->MoveCameraTarget(target);
     220           0 : }
     221             : 
     222           0 : float CMiniMap::GetAngle() const
     223             : {
     224           0 :     CVector3D cameraIn = g_Game->GetView()->GetCamera()->GetOrientation().GetIn();
     225           0 :     return -atan2(cameraIn.X, cameraIn.Z);
     226             : }
     227             : 
     228           0 : CVector2D CMiniMap::WorldSpaceToMiniMapSpace(const CVector3D& worldPosition) const
     229             : {
     230             :     // Coordinates with 0,0 in the middle of the minimap and +-0.5 as max.
     231           0 :     const float invTileMapSize = 1.0f / static_cast<float>(TERRAIN_TILE_SIZE * m_MapSize);
     232           0 :     const float relativeX = (worldPosition.X * invTileMapSize - 0.5) / m_MapScale;
     233           0 :     const float relativeY = (worldPosition.Z * invTileMapSize - 0.5) / m_MapScale;
     234             : 
     235             :     // Rotate coordinates.
     236           0 :     const float angle = GetAngle();
     237           0 :     const float rotatedX = cos(angle) * relativeX + sin(angle) * relativeY;
     238           0 :     const float rotatedY = -sin(angle) * relativeX + cos(angle) * relativeY;
     239             : 
     240             :     // Calculate coordinates in GUI space.
     241           0 :     return CVector2D(
     242           0 :         m_CachedActualSize.left + (0.5f + rotatedX) * m_CachedActualSize.GetWidth(),
     243           0 :         m_CachedActualSize.bottom - (0.5f + rotatedY) * m_CachedActualSize.GetHeight());
     244             : }
     245             : 
     246           0 : bool CMiniMap::FireWorldClickEvent(int button, int UNUSED(clicks))
     247             : {
     248           0 :     ScriptRequest rq(g_GUI->GetActiveGUI()->GetScriptInterface());
     249             : 
     250             :     float x, z;
     251           0 :     GetMouseWorldCoordinates(x, z);
     252             : 
     253           0 :     JS::RootedValue coords(rq.cx);
     254           0 :     Script::CreateObject(rq, &coords, "x", x, "z", z);
     255             : 
     256           0 :     JS::RootedValue buttonJs(rq.cx);
     257           0 :     Script::ToJSVal(rq, &buttonJs, button);
     258             : 
     259           0 :     JS::RootedValueVector paramData(rq.cx);
     260           0 :     ignore_result(paramData.append(coords));
     261           0 :     ignore_result(paramData.append(buttonJs));
     262             : 
     263           0 :     return ScriptEventWithReturn(EventNameWorldClick, paramData);
     264             : }
     265             : 
     266             : // This sets up and draws the rectangle on the minimap
     267             : //  which represents the view of the camera in the world.
     268           0 : void CMiniMap::DrawViewRect(CCanvas2D& canvas) const
     269             : {
     270             :     // Compute the camera frustum intersected with a fixed-height plane.
     271             :     // Use the water height as a fixed base height, which should be the lowest we can go
     272           0 :     const float sampleHeight = g_Renderer.GetSceneRenderer().GetWaterManager().m_WaterHeight;
     273             : 
     274           0 :     const CCamera* camera = g_Game->GetView()->GetCamera();
     275             :     const std::array<CVector3D, 4> hitPoints = {
     276           0 :         camera->GetWorldCoordinates(0, g_Renderer.GetHeight(), sampleHeight),
     277           0 :         camera->GetWorldCoordinates(g_Renderer.GetWidth(), g_Renderer.GetHeight(), sampleHeight),
     278           0 :         camera->GetWorldCoordinates(g_Renderer.GetWidth(), 0, sampleHeight),
     279             :         camera->GetWorldCoordinates(0, 0, sampleHeight)
     280           0 :     };
     281             : 
     282           0 :     std::vector<CVector3D> worldSpaceLines;
     283             :     // We need to prevent drawing view bounds out of the map.
     284           0 :     const float halfMapSize = static_cast<float>((m_MapSize - 1) * TERRAIN_TILE_SIZE) * 0.5f;
     285           0 :     CropPointsByCircle(hitPoints, CVector3D(halfMapSize, 0.0f, halfMapSize), halfMapSize * m_MapScale, &worldSpaceLines);
     286           0 :     if (worldSpaceLines.empty())
     287           0 :         return;
     288             : 
     289           0 :     for (size_t index = 0; index < worldSpaceLines.size() && index + 1 < worldSpaceLines.size(); index += 2)
     290             :     {
     291           0 :         const CVector2D from = WorldSpaceToMiniMapSpace(worldSpaceLines[index]);
     292           0 :         const CVector2D to = WorldSpaceToMiniMapSpace(worldSpaceLines[index + 1]);
     293           0 :         canvas.DrawLine({from, to}, 2.0f, CColor(1.0f, 0.3f, 0.3f, 1.0f));
     294             :     }
     295             : }
     296             : 
     297           0 : void CMiniMap::DrawFlare(CCanvas2D& canvas, const MapFlare& flare, double currentTime) const
     298             : {
     299           0 :     if (m_FlareTextures.empty())
     300           0 :         return;
     301             : 
     302           0 :     const CVector2D flareCenter = WorldSpaceToMiniMapSpace(CVector3D(flare.pos.X, 0.0f, flare.pos.Y));
     303             : 
     304             :     const CRect destination(
     305           0 :         flareCenter.X - m_FlareRenderSize, flareCenter.Y - m_FlareRenderSize,
     306           0 :         flareCenter.X + m_FlareRenderSize, flareCenter.Y + m_FlareRenderSize);
     307             : 
     308           0 :     const double deltaTime = currentTime - flare.time;
     309           0 :     const double remainingTime = m_FlareLifetimeSeconds - deltaTime;
     310           0 :     const u32 flooredStep = floor(deltaTime * m_FlareAnimationSpeed);
     311             : 
     312           0 :     const float startFadeAlpha = m_FlareStartFadeSeconds > 0.0f ? deltaTime / m_FlareStartFadeSeconds : 1.0f;
     313           0 :     const float stopFadeAlpha = m_FlareStopFadeSeconds > 0.0f ? remainingTime / m_FlareStopFadeSeconds : 1.0f;
     314           0 :     const float alpha = Clamp(std::min(
     315           0 :         SmoothStep(0.0f, 1.0f, startFadeAlpha), SmoothStep(0.0f, 1.0f, stopFadeAlpha)),
     316           0 :         0.0f, 1.0f);
     317             : 
     318           0 :     DrawFlareFrame(canvas, flooredStep % m_FlareTextures.size(), destination, flare.color, alpha);
     319             : 
     320             :     // Draw a second circle if the first has reached half of the animation.
     321           0 :     if (m_FlareInterleave && flooredStep >= m_FlareTextures.size() / 2)
     322             :     {
     323           0 :         DrawFlareFrame(canvas, (flooredStep - m_FlareTextures.size() / 2) % m_FlareTextures.size(),
     324             :             destination, flare.color, alpha);
     325             :     }
     326             : }
     327             : 
     328           0 : void CMiniMap::DrawFlareFrame(CCanvas2D& canvas, const u32 frameIndex,
     329             :     const CRect& destination, const CColor& color, float alpha) const
     330             : {
     331             :     // TODO: Only draw inside the minimap circle.
     332           0 :     CTexturePtr texture = m_FlareTextures[frameIndex % m_FlareTextures.size()];
     333           0 :     CColor finalColor = color;
     334           0 :     finalColor.a *= alpha;
     335           0 :     canvas.DrawTexture(texture, destination,
     336           0 :         CRect(0, 0, texture->GetWidth(), texture->GetHeight()), finalColor,
     337           0 :         CColor(0.0f, 0.0f, 0.0f, 0.0f), 0.0f);
     338           0 : }
     339             : 
     340           0 : void CMiniMap::Draw(CCanvas2D& canvas)
     341             : {
     342           0 :     PROFILE3("render minimap");
     343             : 
     344             :     // The terrain isn't actually initialized until the map is loaded, which
     345             :     // happens when the game is started, so abort until then.
     346           0 :     if (!g_Game || !g_Game->IsGameStarted())
     347           0 :         return;
     348             : 
     349           0 :     if (!m_Mask)
     350           0 :         canvas.DrawRect(m_CachedActualSize, CColor(0.0f, 0.0f, 0.0f, 1.0f));
     351             : 
     352           0 :     CSimulation2* sim = g_Game->GetSimulation2();
     353           0 :     CmpPtr<ICmpRangeManager> cmpRangeManager(*sim, SYSTEM_ENTITY);
     354           0 :     ENSURE(cmpRangeManager);
     355             : 
     356             :     // Set our globals in case they hadn't been set before
     357           0 :     const CTerrain* terrain = g_Game->GetWorld()->GetTerrain();
     358           0 :     m_MapSize = terrain->GetVerticesPerSide();
     359           0 :     m_MapScale = (cmpRangeManager->GetLosCircular() ? 1.f : 1.414f);
     360             : 
     361             :     // Draw the main textured quad
     362           0 :     CMiniMapTexture& miniMapTexture = g_Game->GetView()->GetMiniMapTexture();
     363           0 :     if (miniMapTexture.GetTexture())
     364             :     {
     365           0 :         const CVector2D center = m_CachedActualSize.CenterPoint();
     366             :         const CRect source(
     367             :             0,
     368           0 :             miniMapTexture.IsFlipped() ? 0 : miniMapTexture.GetTexture()->GetHeight(),
     369           0 :             miniMapTexture.GetTexture()->GetWidth(),
     370           0 :             miniMapTexture.IsFlipped() ? miniMapTexture.GetTexture()->GetHeight() : 0);
     371           0 :         const CSize2D size(m_CachedActualSize.GetSize() / m_MapScale);
     372           0 :         const CRect destination(center - size / 2.0f, size);
     373           0 :         canvas.DrawRotatedTexture(
     374             :             miniMapTexture.GetTexture(), destination, source,
     375           0 :             CColor(1.0f, 1.0f, 1.0f, 1.0f), CColor(0.0f, 0.0f, 0.0f, 0.0f), 0.0f,
     376             :             center, GetAngle());
     377             :     }
     378             : 
     379           0 :     for (const CMiniMapTexture::Icon& icon : miniMapTexture.GetIcons())
     380             :     {
     381             :         const CVector2D center = WorldSpaceToMiniMapSpace(
     382           0 :             CVector3D(icon.worldPosition.X, 0.0f, icon.worldPosition.Y));
     383             :         const CRect destination(
     384           0 :             center.X - icon.halfSize, center.Y - icon.halfSize,
     385           0 :             center.X + icon.halfSize, center.Y + icon.halfSize);
     386           0 :         const CRect source(0, 0, icon.texture->GetWidth(), icon.texture->GetHeight());
     387           0 :         canvas.DrawTexture(
     388             :             icon.texture, destination, source,
     389           0 :             icon.color, CColor(0.0f, 0.0f, 0.0f, 0.0f), 0.0f);
     390             :     }
     391             : 
     392           0 :     PROFILE_START("minimap flares");
     393             : 
     394           0 :     DrawViewRect(canvas);
     395             : 
     396           0 :     const double currentTime = timer_Time();
     397           0 :     while (!m_MapFlares.empty() && m_FlareLifetimeSeconds + m_MapFlares.front().time < currentTime)
     398           0 :         m_MapFlares.pop_front();
     399             : 
     400           0 :     for (const MapFlare& flare : m_MapFlares)
     401           0 :         DrawFlare(canvas, flare, currentTime);
     402             : 
     403             :     PROFILE_END("minimap flares");
     404             : }
     405             : 
     406           0 : bool CMiniMap::Flare(const CVector2D& pos, const CStr& colorStr)
     407             : {
     408           0 :     CColor color;
     409           0 :     if (!color.ParseString(colorStr))
     410             :     {
     411           0 :         LOGERROR("CMiniMap::Flare: Couldn't parse color string");
     412           0 :         return false;
     413             :     }
     414           0 :     m_MapFlares.push_back({ pos, color, timer_Time() });
     415           0 :     return true;
     416           3 : }

Generated by: LCOV version 1.13