Index: ps/trunk/source/gui/ObjectTypes/CMiniMap.cpp =================================================================== --- ps/trunk/source/gui/ObjectTypes/CMiniMap.cpp (revision 25884) +++ ps/trunk/source/gui/ObjectTypes/CMiniMap.cpp (revision 25885) @@ -1,727 +1,725 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. 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 2 of the License, or * (at your option) any later version. * * 0 A.D. 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 0 A.D. If not, see . */ #include "precompiled.h" #include "CMiniMap.h" #include "graphics/Canvas2D.h" #include "graphics/GameView.h" #include "graphics/LOSTexture.h" #include "graphics/MiniMapTexture.h" #include "graphics/MiniPatch.h" #include "graphics/ShaderManager.h" #include "graphics/ShaderProgramPtr.h" #include "graphics/Terrain.h" #include "graphics/TerrainTextureEntry.h" #include "graphics/TerrainTextureManager.h" #include "graphics/TerritoryTexture.h" #include "gui/CGUI.h" #include "gui/GUIManager.h" #include "gui/GUIMatrix.h" #include "lib/bits.h" #include "lib/external_libraries/libsdl.h" #include "lib/ogl.h" #include "lib/timer.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/CStrInternStatic.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/GameSetup/Config.h" #include "ps/Profile.h" #include "ps/World.h" #include "renderer/Renderer.h" #include "renderer/RenderingOptions.h" #include "renderer/WaterManager.h" #include "scriptinterface/Object.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpMinimap.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/helpers/Los.h" #include "simulation2/system/ParamNode.h" #include #include #include -extern bool g_GameRestarted; - namespace { // Set max drawn entities to UINT16_MAX for now, which is more than enough // TODO: we should be cleverer about drawing them to reduce clutter const u16 MAX_ENTITIES_DRAWN = 65535; // Adds segments pieces lying inside the circle to lines. void CropPointsByCircle(const std::array& points, const CVector3D& center, const float radius, std::vector* lines) { constexpr float EPS = 1e-3f; lines->reserve(points.size() * 2); for (size_t idx = 0; idx < points.size(); ++idx) { const CVector3D& currentPoint = points[idx]; const CVector3D& nextPoint = points[(idx + 1) % points.size()]; const CVector3D direction = (nextPoint - currentPoint).Normalized(); const CVector3D normal(direction.Z, 0.0f, -direction.X); const float offset = normal.Dot(currentPoint) - normal.Dot(center); // We need to have lines only inside the circle. if (std::abs(offset) + EPS >= radius) continue; const CVector3D closestPoint = center + normal * offset; const float halfChordLength = sqrt(radius * radius - offset * offset); const CVector3D intersectionA = closestPoint - direction * halfChordLength; const CVector3D intersectionB = closestPoint + direction * halfChordLength; // We have no intersection if the segment is lying outside of the circle. if (direction.Dot(currentPoint) + EPS > direction.Dot(intersectionB) || direction.Dot(nextPoint) - EPS < direction.Dot(intersectionA)) continue; lines->emplace_back( direction.Dot(currentPoint) > direction.Dot(intersectionA) ? currentPoint : intersectionA); lines->emplace_back( direction.Dot(nextPoint) < direction.Dot(intersectionB) ? nextPoint : intersectionB); } } void DrawTexture(CShaderProgramPtr shader, float coordMax, float angle, float x, float y, float x2, float y2, float mapScale) { // Rotate the texture coordinates (0,0)-(coordMax,coordMax) around their center point (m,m) // Scale square maps to fit in circular minimap area const float s = sin(angle) * mapScale; const float c = cos(angle) * mapScale; const float m = coordMax / 2.f; float quadTex[] = { m*(-c + s + 1.f), m*(-c + -s + 1.f), m*(c + s + 1.f), m*(-c + s + 1.f), m*(c + -s + 1.f), m*(c + s + 1.f), m*(c + -s + 1.f), m*(c + s + 1.f), m*(-c + -s + 1.f), m*(c + -s + 1.f), m*(-c + s + 1.f), m*(-c + -s + 1.f) }; float quadVerts[] = { x, y, 0.0f, x2, y, 0.0f, x2, y2, 0.0f, x2, y2, 0.0f, x, y2, 0.0f, x, y, 0.0f }; shader->TexCoordPointer(GL_TEXTURE0, 2, GL_FLOAT, 0, quadTex); shader->VertexPointer(3, GL_FLOAT, 0, quadVerts); shader->AssertPointersBound(); if (!g_Renderer.DoSkipSubmit()) glDrawArrays(GL_TRIANGLES, 0, 6); } } // anonymous namespace const CStr CMiniMap::EventNameWorldClick = "WorldClick"; CMiniMap::CMiniMap(CGUI& pGUI) : IGUIObject(pGUI), m_MapSize(0), m_MapScale(1.f), m_EntitiesDrawn(0), m_IndexArray(GL_STATIC_DRAW), m_VertexArray(GL_DYNAMIC_DRAW), m_Mask(this, "mask", false), m_NextBlinkTime(0.0), m_PingDuration(25.0), m_BlinkState(false), m_FlareTextureCount(this, "flare_texture_count", 0), m_FlareRenderSize(this, "flare_render_size", 0), m_FlareInterleave(this, "flare_interleave", false), m_FlareAnimationSpeed(this, "flare_animation_speed", 0.0f), m_FlareLifetimeSeconds(this, "flare_lifetime_seconds", 0.0f) { m_Clicking = false; m_MouseHovering = false; m_AttributePos.type = GL_FLOAT; m_AttributePos.elems = 2; m_VertexArray.AddAttribute(&m_AttributePos); m_AttributeColor.type = GL_UNSIGNED_BYTE; m_AttributeColor.elems = 4; m_VertexArray.AddAttribute(&m_AttributeColor); m_VertexArray.SetNumVertices(MAX_ENTITIES_DRAWN); m_VertexArray.Layout(); m_IndexArray.SetNumVertices(MAX_ENTITIES_DRAWN); m_IndexArray.Layout(); VertexArrayIterator index = m_IndexArray.GetIterator(); for (u16 i = 0; i < MAX_ENTITIES_DRAWN; ++i) *index++ = i; m_IndexArray.Upload(); m_IndexArray.FreeBackingStore(); VertexArrayIterator attrPos = m_AttributePos.GetIterator(); VertexArrayIterator attrColor = m_AttributeColor.GetIterator(); for (u16 i = 0; i < MAX_ENTITIES_DRAWN; ++i) { (*attrColor)[0] = 0; (*attrColor)[1] = 0; (*attrColor)[2] = 0; (*attrColor)[3] = 0; ++attrColor; (*attrPos)[0] = -10000.0f; (*attrPos)[1] = -10000.0f; ++attrPos; } m_VertexArray.Upload(); double blinkDuration = 1.0; // Tests won't have config initialised if (CConfigDB::IsInitialised()) { CFG_GET_VAL("gui.session.minimap.pingduration", m_PingDuration); CFG_GET_VAL("gui.session.minimap.blinkduration", blinkDuration); } m_HalfBlinkDuration = blinkDuration/2; } CMiniMap::~CMiniMap() = default; void CMiniMap::HandleMessage(SGUIMessage& Message) { IGUIObject::HandleMessage(Message); switch (Message.type) { case GUIM_LOAD: RecreateFlareTextures(); break; case GUIM_SETTINGS_UPDATED: if (Message.value == "flare_texture_count") RecreateFlareTextures(); break; case GUIM_MOUSE_PRESS_LEFT: if (m_MouseHovering) { if (!CMiniMap::FireWorldClickEvent(SDL_BUTTON_LEFT, 1)) { SetCameraPositionFromMousePosition(); m_Clicking = true; } } break; case GUIM_MOUSE_RELEASE_LEFT: if (m_MouseHovering && m_Clicking) SetCameraPositionFromMousePosition(); m_Clicking = false; break; case GUIM_MOUSE_DBLCLICK_LEFT: if (m_MouseHovering && m_Clicking) SetCameraPositionFromMousePosition(); m_Clicking = false; break; case GUIM_MOUSE_ENTER: m_MouseHovering = true; break; case GUIM_MOUSE_LEAVE: m_Clicking = false; m_MouseHovering = false; break; case GUIM_MOUSE_RELEASE_RIGHT: CMiniMap::FireWorldClickEvent(SDL_BUTTON_RIGHT, 1); break; case GUIM_MOUSE_DBLCLICK_RIGHT: CMiniMap::FireWorldClickEvent(SDL_BUTTON_RIGHT, 2); break; case GUIM_MOUSE_MOTION: if (m_MouseHovering && m_Clicking) SetCameraPositionFromMousePosition(); break; case GUIM_MOUSE_WHEEL_DOWN: case GUIM_MOUSE_WHEEL_UP: Message.Skip(); break; default: break; } } void CMiniMap::RecreateFlareTextures() { // Catch invalid values. if (m_FlareTextureCount > 99) { LOGERROR("Invalid value for flare texture count. Valid range is 0-99."); return; } const CStr numberingFormat = "%02u"; m_FlareTextures.clear(); m_FlareTextures.reserve(m_FlareTextureCount); for (u32 i = 0; i < m_FlareTextureCount; ++i) { CTextureProperties textureProps(L"art/textures/animated/minimap-flare/frame" + CStr(fmt::sprintf(numberingFormat, i)).FromUTF8() + L".png"); m_FlareTextures.emplace_back(g_Renderer.GetTextureManager().CreateTexture(textureProps)); } } bool CMiniMap::IsMouseOver() const { // Get the mouse position. const CVector2D& mousePos = m_pGUI.GetMousePos(); // Get the position of the center of the minimap. CVector2D minimapCenter = CVector2D(m_CachedActualSize.left + m_CachedActualSize.GetWidth() / 2.0, m_CachedActualSize.bottom - m_CachedActualSize.GetHeight() / 2.0); // Take the magnitude of the difference of the mouse position and minimap center. double distFromCenter = sqrt(pow((mousePos.X - minimapCenter.X), 2) + pow((mousePos.Y - minimapCenter.Y), 2)); // If the distance is less then the radius of the minimap (half the width) the mouse is over the minimap. if (distFromCenter < m_CachedActualSize.GetWidth() / 2.0) return true; else return false; } void CMiniMap::GetMouseWorldCoordinates(float& x, float& z) const { // Determine X and Z according to proportion of mouse position and minimap. const CVector2D& mousePos = m_pGUI.GetMousePos(); float px = (mousePos.X - m_CachedActualSize.left) / m_CachedActualSize.GetWidth(); float py = (m_CachedActualSize.bottom - mousePos.Y) / m_CachedActualSize.GetHeight(); float angle = GetAngle(); // Scale world coordinates for shrunken square map x = TERRAIN_TILE_SIZE * m_MapSize * (m_MapScale * (cos(angle)*(px-0.5) - sin(angle)*(py-0.5)) + 0.5); z = TERRAIN_TILE_SIZE * m_MapSize * (m_MapScale * (cos(angle)*(py-0.5) + sin(angle)*(px-0.5)) + 0.5); } void CMiniMap::SetCameraPositionFromMousePosition() { CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); CVector3D target; GetMouseWorldCoordinates(target.X, target.Z); target.Y = terrain->GetExactGroundLevel(target.X, target.Z); g_Game->GetView()->MoveCameraTarget(target); } float CMiniMap::GetAngle() const { CVector3D cameraIn = g_Game->GetView()->GetCamera()->GetOrientation().GetIn(); return -atan2(cameraIn.X, cameraIn.Z); } bool CMiniMap::FireWorldClickEvent(int button, int UNUSED(clicks)) { ScriptRequest rq(g_GUI->GetActiveGUI()->GetScriptInterface()); float x, z; GetMouseWorldCoordinates(x, z); JS::RootedValue coords(rq.cx); Script::CreateObject(rq, &coords, "x", x, "z", z); JS::RootedValue buttonJs(rq.cx); Script::ToJSVal(rq, &buttonJs, button); JS::RootedValueVector paramData(rq.cx); ignore_result(paramData.append(coords)); ignore_result(paramData.append(buttonJs)); return ScriptEventWithReturn(EventNameWorldClick, paramData); } // This sets up and draws the rectangle on the minimap // which represents the view of the camera in the world. void CMiniMap::DrawViewRect(const CMatrix3D& transform) const { // Compute the camera frustum intersected with a fixed-height plane. // Use the water height as a fixed base height, which should be the lowest we can go float h = g_Renderer.GetWaterManager()->m_WaterHeight; const float width = m_CachedActualSize.GetWidth(); const float height = m_CachedActualSize.GetHeight(); const float invTileMapSize = 1.0f / float(TERRAIN_TILE_SIZE * m_MapSize); const CCamera* camera = g_Game->GetView()->GetCamera(); const std::array hitPoints = { camera->GetWorldCoordinates(0, g_Renderer.GetHeight(), h), camera->GetWorldCoordinates(g_Renderer.GetWidth(), g_Renderer.GetHeight(), h), camera->GetWorldCoordinates(g_Renderer.GetWidth(), 0, h), camera->GetWorldCoordinates(0, 0, h) }; std::vector lines; // We need to prevent drawing view bounds out of the map. const float halfMapSize = static_cast((m_MapSize - 1) * TERRAIN_TILE_SIZE) * 0.5f; CropPointsByCircle(hitPoints, CVector3D(halfMapSize, 0.0f, halfMapSize), halfMapSize * m_MapScale, &lines); if (lines.empty()) return; std::vector vertices; vertices.reserve(lines.size() * 2); for (const CVector3D& point : lines) { // Convert to minimap space. vertices.emplace_back(width * point.X * invTileMapSize); vertices.emplace_back(-(height * point.Z * invTileMapSize)); } glLineWidth(2.0f); CShaderDefines lineDefines; lineDefines.Add(str_MINIMAP_LINE, str_1); CShaderTechniquePtr tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap, g_Renderer.GetSystemShaderDefines(), lineDefines); tech->BeginPass(); CShaderProgramPtr shader = tech->GetShader(); shader->Uniform(str_transform, transform); shader->Uniform(str_color, 1.0f, 0.3f, 0.3f, 1.0f); shader->VertexPointer(2, GL_FLOAT, 0, vertices.data()); shader->AssertPointersBound(); if (!g_Renderer.DoSkipSubmit()) glDrawArrays(GL_LINES, 0, vertices.size() / 2); tech->EndPass(); glLineWidth(1.0f); } void CMiniMap::DrawFlare(CCanvas2D& canvas, const MapFlare& flare, double currentTime) const { if (!m_FlareTextures.size()) return; // Coordinates with 0,0 in the middle of the minimap and +-0.5 as max. const float invTileMapSize = 1.0f / static_cast(TERRAIN_TILE_SIZE * m_MapSize); const float relativeX = (flare.pos.X * invTileMapSize - 0.5) / m_MapScale; const float relativeY = (flare.pos.Y * invTileMapSize - 0.5) / m_MapScale; // Rotate coordinates. const float angle = GetAngle(); const float rotatedX = cos(angle) * relativeX + sin(angle) * relativeY; const float rotatedY = -sin(angle) * relativeX + cos(angle) * relativeY; // Calculate coordinates in gui space. const float cx = m_CachedActualSize.left + (0.5f + rotatedX) * m_CachedActualSize.GetWidth(); const float cy = m_CachedActualSize.bottom - (0.5f + rotatedY) * m_CachedActualSize.GetHeight(); const CRect destination(cx-m_FlareRenderSize, cy-m_FlareRenderSize, cx+m_FlareRenderSize, cy+m_FlareRenderSize); const u32 flooredStep = floor((currentTime - flare.time) * m_FlareAnimationSpeed); CTexturePtr texture = m_FlareTextures[flooredStep % m_FlareTextures.size()]; // TODO: Only draw inside the minimap circle. canvas.DrawTexture(texture, destination, CRect(0, 0, texture->GetWidth(), texture->GetHeight()), flare.color, CColor(0.0f, 0.0f, 0.0f, 0.0f), 0.0f); // Draw a second circle if the first has reached half of the animation if (m_FlareInterleave && flooredStep >= m_FlareTextures.size() / 2) { texture = m_FlareTextures[(flooredStep - m_FlareTextures.size() / 2) % m_FlareTextures.size()]; // TODO: Only draw inside the minimap circle. canvas.DrawTexture(texture, destination, CRect(0, 0, texture->GetWidth(), texture->GetHeight()), flare.color, CColor(0.0f, 0.0f, 0.0f, 0.0f), 0.0f); } } struct MinimapUnitVertex { // This struct is copyable for convenience and because to move is to copy for primitives. u8 r, g, b, a; float x, y; }; // Adds a vertex to the passed VertexArray static void inline addVertex(const MinimapUnitVertex& v, VertexArrayIterator& attrColor, VertexArrayIterator& attrPos) { (*attrColor)[0] = v.r; (*attrColor)[1] = v.g; (*attrColor)[2] = v.b; (*attrColor)[3] = v.a; ++attrColor; (*attrPos)[0] = v.x; (*attrPos)[1] = v.y; ++attrPos; } // TODO: render the minimap in a framebuffer and just draw the frambuffer texture // most of the time, updating the framebuffer twice a frame. // Here it updates as ping-pong either texture or vertex array each sec to lower gpu stalling // (those operations cause a gpu sync, which slows down the way gpu works) void CMiniMap::Draw(CCanvas2D& canvas) { PROFILE3("render minimap"); // The terrain isn't actually initialized until the map is loaded, which // happens when the game is started, so abort until then. if (!g_Game || !g_Game->IsGameStarted()) return; canvas.Flush(); CSimulation2* sim = g_Game->GetSimulation2(); CmpPtr cmpRangeManager(*sim, SYSTEM_ENTITY); ENSURE(cmpRangeManager); CLOSTexture& losTexture = g_Game->GetView()->GetLOSTexture(); CMiniMapTexture& miniMapTexture = g_Game->GetView()->GetMiniMapTexture(); // Set our globals in case they hadn't been set before const CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); ssize_t width = (u32)(m_CachedActualSize.right - m_CachedActualSize.left); ssize_t height = (u32)(m_CachedActualSize.bottom - m_CachedActualSize.top); m_MapSize = terrain->GetVerticesPerSide(); GLsizei textureSize = miniMapTexture.GetTerrainTextureSize(); m_MapScale = (cmpRangeManager->GetLosCircular() ? 1.f : 1.414f); // only update 2x / second // (note: since units only move a few pixels per second on the minimap, // we can get away with infrequent updates; this is slow) // TODO: Update all but camera at same speed as simulation static double last_time; const double cur_time = timer_Time(); const bool doUpdate = cur_time - last_time > 0.5; if (doUpdate) last_time = cur_time; const float x = m_CachedActualSize.left, y = m_CachedActualSize.bottom; const float x2 = m_CachedActualSize.right, y2 = m_CachedActualSize.top; const float texCoordMax = (float)(m_MapSize - 1) / (float)textureSize; const float angle = GetAngle(); const float unitScale = (cmpRangeManager->GetLosCircular() ? 1.f : m_MapScale/2.f); CShaderProgramPtr shader; CShaderTechniquePtr tech; CShaderDefines baseDefines; baseDefines.Add(str_MINIMAP_BASE, str_1); if (m_Mask) baseDefines.Add(str_MINIMAP_MASK, str_1); tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap, g_Renderer.GetSystemShaderDefines(), baseDefines); tech->BeginPass(); shader = tech->GetShader(); // Draw the main textured quad if (miniMapTexture.GetTerrainTexture()) shader->BindTexture(str_baseTex, miniMapTexture.GetTerrainTexture()); if (m_Mask) { shader->BindTexture(str_maskTex, losTexture.GetTexture()); CMatrix3D maskTextureTransform = *losTexture.GetMinimapTextureMatrix(); // We need to have texture coordinates in the same coordinate space. const float scale = 1.0f / texCoordMax; maskTextureTransform.Scale(scale, scale, 1.0f); shader->Uniform(str_maskTextureTransform, maskTextureTransform); } const CMatrix3D baseTransform = GetDefaultGuiMatrix(); CMatrix3D baseTextureTransform; baseTextureTransform.SetIdentity(); shader->Uniform(str_transform, baseTransform); shader->Uniform(str_textureTransform, baseTextureTransform); if (m_Mask) { glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); } if (miniMapTexture.GetTerrainTexture()) DrawTexture(shader, texCoordMax, angle, x, y, x2, y2, m_MapScale); if (!m_Mask) { glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); } // Draw territory boundaries CTerritoryTexture& territoryTexture = g_Game->GetView()->GetTerritoryTexture(); shader->BindTexture(str_baseTex, territoryTexture.GetTexture()); if (m_Mask) { shader->BindTexture(str_maskTex, losTexture.GetTexture()); shader->Uniform(str_maskTextureTransform, *losTexture.GetMinimapTextureMatrix()); } const CMatrix3D* territoryTransform = territoryTexture.GetMinimapTextureMatrix(); shader->Uniform(str_transform, baseTransform); shader->Uniform(str_textureTransform, *territoryTransform); DrawTexture(shader, 1.0f, angle, x, y, x2, y2, m_MapScale); tech->EndPass(); // Draw the LOS quad in black, using alpha values from the LOS texture if (!m_Mask) { CShaderDefines losDefines; losDefines.Add(str_MINIMAP_LOS, str_1); tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap, g_Renderer.GetSystemShaderDefines(), losDefines); tech->BeginPass(); shader = tech->GetShader(); shader->BindTexture(str_baseTex, losTexture.GetTexture()); const CMatrix3D* losTransform = losTexture.GetMinimapTextureMatrix(); shader->Uniform(str_transform, baseTransform); shader->Uniform(str_textureTransform, *losTransform); DrawTexture(shader, 1.0f, angle, x, y, x2, y2, m_MapScale); tech->EndPass(); } glDisable(GL_BLEND); PROFILE_START("minimap units and flares"); CShaderDefines pointDefines; pointDefines.Add(str_MINIMAP_POINT, str_1); tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap, g_Renderer.GetSystemShaderDefines(), pointDefines); tech->BeginPass(); shader = tech->GetShader(); shader->Uniform(str_transform, baseTransform); shader->Uniform(str_pointSize, 3.f); CMatrix3D unitMatrix; unitMatrix.SetIdentity(); // Center the minimap on the origin of the axis of rotation. unitMatrix.Translate(-(x2 - x) / 2.f, -(y2 - y) / 2.f, 0.f); // Rotate the map. unitMatrix.RotateZ(angle); // Scale square maps to fit. unitMatrix.Scale(unitScale, unitScale, 1.f); // Move the minimap back to it's starting position. unitMatrix.Translate((x2 - x) / 2.f, (y2 - y) / 2.f, 0.f); // Move the minimap to it's final location. unitMatrix.Translate(x, y, 0.0f); // Apply the gui matrix. unitMatrix *= GetDefaultGuiMatrix(); // Load the transform into the shader. shader->Uniform(str_transform, unitMatrix); const float sx = (float)width / ((m_MapSize - 1) * TERRAIN_TILE_SIZE); const float sy = (float)height / ((m_MapSize - 1) * TERRAIN_TILE_SIZE); CSimulation2::InterfaceList ents = sim->GetEntitiesWithInterface(IID_Minimap); if (doUpdate) { VertexArrayIterator attrPos = m_AttributePos.GetIterator(); VertexArrayIterator attrColor = m_AttributeColor.GetIterator(); m_EntitiesDrawn = 0; MinimapUnitVertex v; std::vector pingingVertices; pingingVertices.reserve(MAX_ENTITIES_DRAWN / 2); if (cur_time > m_NextBlinkTime) { m_BlinkState = !m_BlinkState; m_NextBlinkTime = cur_time + m_HalfBlinkDuration; } entity_pos_t posX, posZ; for (CSimulation2::InterfaceList::const_iterator it = ents.begin(); it != ents.end(); ++it) { ICmpMinimap* cmpMinimap = static_cast(it->second); if (cmpMinimap->GetRenderData(v.r, v.g, v.b, posX, posZ)) { LosVisibility vis = cmpRangeManager->GetLosVisibility(it->first, g_Game->GetSimulation2()->GetSimContext().GetCurrentDisplayedPlayer()); if (vis != LosVisibility::HIDDEN) { v.a = 255; v.x = posX.ToFloat() * sx; v.y = -posZ.ToFloat() * sy; // Check minimap pinging to indicate something if (m_BlinkState && cmpMinimap->CheckPing(cur_time, m_PingDuration)) { v.r = 255; // ping color is white v.g = 255; v.b = 255; pingingVertices.push_back(v); } else { addVertex(v, attrColor, attrPos); ++m_EntitiesDrawn; } } } } // Add the pinged vertices at the end, so they are drawn on top for (const MinimapUnitVertex& vertex : pingingVertices) { addVertex(vertex, attrColor, attrPos); ++m_EntitiesDrawn; } ENSURE(m_EntitiesDrawn < MAX_ENTITIES_DRAWN); m_VertexArray.Upload(); } m_VertexArray.PrepareForRendering(); if (m_EntitiesDrawn > 0) { #if !CONFIG2_GLES glEnable(GL_VERTEX_PROGRAM_POINT_SIZE); #endif u8* indexBase = m_IndexArray.Bind(); u8* base = m_VertexArray.Bind(); const GLsizei stride = (GLsizei)m_VertexArray.GetStride(); shader->VertexPointer(2, GL_FLOAT, stride, base + m_AttributePos.offset); shader->ColorPointer(4, GL_UNSIGNED_BYTE, stride, base + m_AttributeColor.offset); shader->AssertPointersBound(); if (!g_Renderer.DoSkipSubmit()) glDrawElements(GL_POINTS, (GLsizei)(m_EntitiesDrawn), GL_UNSIGNED_SHORT, indexBase); g_Renderer.GetStats().m_DrawCalls++; CVertexBuffer::Unbind(); #if !CONFIG2_GLES glDisable(GL_VERTEX_PROGRAM_POINT_SIZE); #endif } tech->EndPass(); DrawViewRect(unitMatrix); while (!m_MapFlares.empty() && m_FlareLifetimeSeconds + m_MapFlares.front().time < cur_time) m_MapFlares.pop_front(); for (const MapFlare& flare : m_MapFlares) DrawFlare(canvas, flare, cur_time); PROFILE_END("minimap units and flares"); } bool CMiniMap::Flare(const CVector2D& pos, const CStr& colorStr) { CColor color; if (!color.ParseString(colorStr)) { LOGERROR("CMiniMap::Flare: Couldn't parse color string"); return false; } m_MapFlares.push_back({ pos, color, timer_Time() }); return true; } Index: ps/trunk/source/main.cpp =================================================================== --- ps/trunk/source/main.cpp (revision 25884) +++ ps/trunk/source/main.cpp (revision 25885) @@ -1,759 +1,754 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. 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 2 of the License, or * (at your option) any later version. * * 0 A.D. 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 0 A.D. If not, see . */ /* This module drives the game when running without Atlas (our integrated map editor). It receives input and OS messages via SDL and feeds them into the input dispatcher, where they are passed on to the game GUI and simulation. It also contains main(), which either runs the above controller or that of Atlas depending on commandline parameters. */ // not for any PCH effort, but instead for the (common) definitions // included there. #define MINIMAL_PCH 2 #include "lib/precompiled.h" #include "lib/debug.h" #include "lib/status.h" #include "lib/secure_crt.h" #include "lib/frequency_filter.h" #include "lib/input.h" #include "lib/ogl.h" #include "lib/timer.h" #include "lib/external_libraries/libsdl.h" #include "ps/ArchiveBuilder.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/Globals.h" #include "ps/Hotkey.h" #include "ps/Loader.h" #include "ps/Mod.h" #include "ps/ModInstaller.h" #include "ps/Profile.h" #include "ps/Profiler2.h" #include "ps/Pyrogenesis.h" #include "ps/Replay.h" #include "ps/TouchInput.h" #include "ps/UserReport.h" #include "ps/Util.h" #include "ps/VideoMode.h" #include "ps/TaskManager.h" #include "ps/World.h" #include "ps/GameSetup/GameSetup.h" #include "ps/GameSetup/Atlas.h" #include "ps/GameSetup/Config.h" #include "ps/GameSetup/CmdLineArgs.h" #include "ps/GameSetup/Paths.h" #include "ps/XML/Xeromyces.h" #include "network/NetClient.h" #include "network/NetServer.h" #include "network/NetSession.h" #include "lobby/IXmppClient.h" #include "graphics/Camera.h" #include "graphics/GameView.h" #include "graphics/TextureManager.h" #include "gui/GUIManager.h" #include "renderer/Renderer.h" #include "rlinterface/RLInterface.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptEngine.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/JSON.h" #include "simulation2/Simulation2.h" #include "simulation2/system/TurnManager.h" #include "soundmanager/ISoundManager.h" #if OS_UNIX #include #include // geteuid #endif // OS_UNIX #if OS_MACOSX #include "lib/sysdep/os/osx/osx_atlas.h" #endif #if MSC_VERSION #include #define getpid _getpid // Use the non-deprecated function name #endif #include extern CStrW g_UniqueLogPostfix; -// Marks terrain as modified so the minimap can repaint (is there a cleaner way of handling this?) -bool g_GameRestarted = false; - // Determines the lifetime of the mainloop enum ShutdownType { // The application shall continue the main loop. None, // The process shall terminate as soon as possible. Quit, // The engine should be restarted in the same process, for instance to activate different mods. Restart, // Atlas should be started in the same process. RestartAsAtlas }; static ShutdownType g_Shutdown = ShutdownType::None; // to avoid redundant and/or recursive resizing, we save the new // size after VIDEORESIZE messages and only update the video mode // once per frame. // these values are the latest resize message, and reset to 0 once we've // updated the video mode static int g_ResizedW; static int g_ResizedH; static std::chrono::high_resolution_clock::time_point lastFrameTime; bool IsQuitRequested() { return g_Shutdown == ShutdownType::Quit; } void QuitEngine() { g_Shutdown = ShutdownType::Quit; } void RestartEngine() { g_Shutdown = ShutdownType::Restart; } void StartAtlas() { g_Shutdown = ShutdownType::RestartAsAtlas; } // main app message handler static InReaction MainInputHandler(const SDL_Event_* ev) { switch(ev->ev.type) { case SDL_WINDOWEVENT: switch(ev->ev.window.event) { case SDL_WINDOWEVENT_ENTER: RenderCursor(true); break; case SDL_WINDOWEVENT_LEAVE: RenderCursor(false); break; case SDL_WINDOWEVENT_RESIZED: g_ResizedW = ev->ev.window.data1; g_ResizedH = ev->ev.window.data2; break; case SDL_WINDOWEVENT_MOVED: g_VideoMode.UpdatePosition(ev->ev.window.data1, ev->ev.window.data2); } break; case SDL_QUIT: QuitEngine(); break; case SDL_DROPFILE: { char* dropped_filedir = ev->ev.drop.file; const Paths paths(g_CmdLineArgs); CModInstaller installer(paths.UserData() / "mods", paths.Cache()); installer.Install(std::string(dropped_filedir), g_ScriptContext, true); SDL_free(dropped_filedir); if (installer.GetInstalledMods().empty()) LOGERROR("Failed to install mod %s", dropped_filedir); else { LOGMESSAGE("Installed mod %s", installer.GetInstalledMods().front()); ScriptInterface modInterface("Engine", "Mod", g_ScriptContext); g_Mods.UpdateAvailableMods(modInterface); RestartEngine(); } break; } case SDL_HOTKEYPRESS: std::string hotkey = static_cast(ev->ev.user.data1); if (hotkey == "exit") { QuitEngine(); return IN_HANDLED; } else if (hotkey == "screenshot") { WriteScreenshot(L".png"); return IN_HANDLED; } else if (hotkey == "bigscreenshot") { int tiles = 4, tileWidth = 256, tileHeight = 256; CFG_GET_VAL("screenshot.tiles", tiles); CFG_GET_VAL("screenshot.tilewidth", tileWidth); CFG_GET_VAL("screenshot.tileheight", tileHeight); if (tiles > 0 && tileWidth > 0 && tileHeight > 0) WriteBigScreenshot(L".bmp", tiles, tileWidth, tileHeight); else LOGWARNING("Invalid big screenshot size: tiles=%d tileWidth=%d tileHeight=%d", tiles, tileWidth, tileHeight); return IN_HANDLED; } else if (hotkey == "togglefullscreen") { g_VideoMode.ToggleFullscreen(); return IN_HANDLED; } else if (hotkey == "profile2.toggle") { g_Profiler2.Toggle(); return IN_HANDLED; } break; } return IN_PASS; } // dispatch all pending events to the various receivers. static void PumpEvents() { ScriptRequest rq(g_GUI->GetScriptInterface()); PROFILE3("dispatch events"); SDL_Event_ ev; while (in_poll_event(&ev)) { PROFILE2("event"); if (g_GUI) { JS::RootedValue tmpVal(rq.cx); Script::ToJSVal(rq, &tmpVal, ev); std::string data = Script::StringifyJSON(rq, &tmpVal); PROFILE2_ATTR("%s", data.c_str()); } in_dispatch_event(&ev); } g_TouchInput.Frame(); } /** * Optionally throttle the render frequency in order to * prevent 100% workload of the currently used CPU core. */ inline static void LimitFPS() { if (g_VideoMode.IsVSyncEnabled()) return; double fpsLimit = 0.0; CFG_GET_VAL(g_Game && g_Game->IsGameStarted() ? "adaptivefps.session" : "adaptivefps.menu", fpsLimit); // Keep in sync with options.json if (fpsLimit < 20.0 || fpsLimit >= 100.0) return; double wait = 1000.0 / fpsLimit - std::chrono::duration_cast( std::chrono::high_resolution_clock::now() - lastFrameTime).count() / 1000.0; if (wait > 0.0) SDL_Delay(wait); lastFrameTime = std::chrono::high_resolution_clock::now(); } static int ProgressiveLoad() { PROFILE3("progressive load"); wchar_t description[100]; int progress_percent; try { Status ret = LDR_ProgressiveLoad(10e-3, description, ARRAY_SIZE(description), &progress_percent); switch(ret) { // no load active => no-op (skip code below) case INFO::OK: return 0; // current task didn't complete. we only care about this insofar as the // load process is therefore not yet finished. case ERR::TIMED_OUT: break; // just finished loading case INFO::ALL_COMPLETE: g_Game->ReallyStartGame(); wcscpy_s(description, ARRAY_SIZE(description), L"Game is starting.."); // LDR_ProgressiveLoad returns L""; set to valid text to // avoid problems in converting to JSString break; // error! default: WARN_RETURN_STATUS_IF_ERR(ret); // can't do this above due to legit ERR::TIMED_OUT break; } } catch (PSERROR_Game_World_MapLoadFailed& e) { // Map loading failed // Call script function to do the actual work // (delete game data, switch GUI page, show error, etc.) CancelLoad(CStr(e.what()).FromUTF8()); } GUI_DisplayLoadProgress(progress_percent, description); return 0; } static void RendererIncrementalLoad() { PROFILE3("renderer incremental load"); const double maxTime = 0.1f; double startTime = timer_Time(); bool more; do { more = g_Renderer.GetTextureManager().MakeProgress(); } while (more && timer_Time() - startTime < maxTime); } static void Frame() { g_Profiler2.RecordFrameStart(); PROFILE2("frame"); g_Profiler2.IncrementFrameNumber(); PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber()); ogl_WarnIfError(); // get elapsed time const double time = timer_Time(); g_frequencyFilter->Update(time); // .. old method - "exact" but contains jumps #if 0 static double last_time; const double time = timer_Time(); const float TimeSinceLastFrame = (float)(time-last_time); last_time = time; ONCE(return); // first call: set last_time and return // .. new method - filtered and more smooth, but errors may accumulate #else const float realTimeSinceLastFrame = 1.0 / g_frequencyFilter->SmoothedFrequency(); #endif ENSURE(realTimeSinceLastFrame > 0.0f); // Decide if update is necessary bool need_update = true; // If we are not running a multiplayer game, disable updates when the game is // minimized or out of focus and relinquish the CPU a bit, in order to make // debugging easier. if (g_PauseOnFocusLoss && !g_NetClient && !g_app_has_focus) { PROFILE3("non-focus delay"); need_update = false; // don't use SDL_WaitEvent: don't want the main loop to freeze until app focus is restored SDL_Delay(10); } // this scans for changed files/directories and reloads them, thus // allowing hotloading (changes are immediately assimilated in-game). ReloadChangedFiles(); ProgressiveLoad(); RendererIncrementalLoad(); PumpEvents(); // if the user quit by closing the window, the GL context will be broken and // may crash when we call Render() on some drivers, so leave this loop // before rendering if (g_Shutdown != ShutdownType::None) return; // respond to pumped resize events if (g_ResizedW || g_ResizedH) { g_VideoMode.ResizeWindow(g_ResizedW, g_ResizedH); g_ResizedW = g_ResizedH = 0; } if (g_NetClient) g_NetClient->Poll(); ogl_WarnIfError(); g_GUI->TickObjects(); ogl_WarnIfError(); if (g_RLInterface) g_RLInterface->TryApplyMessage(); if (g_Game && g_Game->IsGameStarted() && need_update) { if (!g_RLInterface) g_Game->Update(realTimeSinceLastFrame); g_Game->GetView()->Update(float(realTimeSinceLastFrame)); } // Keep us connected to any XMPP servers if (g_XmppClient) g_XmppClient->recv(); g_UserReporter.Update(); g_Console->Update(realTimeSinceLastFrame); ogl_WarnIfError(); if (g_SoundManager) g_SoundManager->IdleTask(); if (ShouldRender()) { Render(); { PROFILE3("swap buffers"); SDL_GL_SwapWindow(g_VideoMode.GetWindow()); ogl_WarnIfError(); } g_Renderer.OnSwapBuffers(); } g_Profiler.Frame(); - g_GameRestarted = false; - LimitFPS(); } static void NonVisualFrame() { g_Profiler2.RecordFrameStart(); PROFILE2("frame"); g_Profiler2.IncrementFrameNumber(); PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber()); static u32 turn = 0; debug_printf("Turn %u (%u)...\n", turn++, DEFAULT_TURN_LENGTH); g_Game->GetSimulation2()->Update(DEFAULT_TURN_LENGTH); g_Profiler.Frame(); if (g_Game->IsGameFinished()) QuitEngine(); } static void MainControllerInit() { // add additional input handlers only needed by this controller: // must be registered after gui_handler. Should mayhap even be last. in_add_handler(MainInputHandler); } static void MainControllerShutdown() { in_reset_handlers(); } static void StartRLInterface(CmdLineArgs args) { std::string server_address; CFG_GET_VAL("rlinterface.address", server_address); if (!args.Get("rl-interface").empty()) server_address = args.Get("rl-interface"); g_RLInterface = std::make_unique(server_address.c_str()); debug_printf("RL interface listening on %s\n", server_address.c_str()); } // moved into a helper function to ensure args is destroyed before // exit(), which may result in a memory leak. static void RunGameOrAtlas(int argc, const char* argv[]) { CmdLineArgs args(argc, argv); g_CmdLineArgs = args; if (args.Has("version")) { debug_printf("Pyrogenesis %s\n", engine_version); return; } if (args.Has("autostart-nonvisual") && args.Get("autostart").empty() && !args.Has("rl-interface")) { LOGERROR("-autostart-nonvisual cant be used alone. A map with -autostart=\"TYPEDIR/MAPNAME\" is needed."); return; } if (args.Has("unique-logs")) g_UniqueLogPostfix = L"_" + std::to_wstring(std::time(nullptr)) + L"_" + std::to_wstring(getpid()); const bool isVisualReplay = args.Has("replay-visual"); const bool isNonVisualReplay = args.Has("replay"); const bool isNonVisual = args.Has("autostart-nonvisual"); const bool isUsingRLInterface = args.Has("rl-interface"); const OsPath replayFile( isVisualReplay ? args.Get("replay-visual") : isNonVisualReplay ? args.Get("replay") : ""); if (isVisualReplay || isNonVisualReplay) { if (!FileExists(replayFile)) { debug_printf("ERROR: The requested replay file '%s' does not exist!\n", replayFile.string8().c_str()); return; } if (DirectoryExists(replayFile)) { debug_printf("ERROR: The requested replay file '%s' is a directory!\n", replayFile.string8().c_str()); return; } } std::vector modsToInstall; for (const CStr& arg : args.GetArgsWithoutName()) { const OsPath modPath(arg); if (!CModInstaller::IsDefaultModExtension(modPath.Extension())) { debug_printf("Skipping file '%s' which does not have a mod file extension.\n", modPath.string8().c_str()); continue; } if (!FileExists(modPath)) { debug_printf("ERROR: The mod file '%s' does not exist!\n", modPath.string8().c_str()); continue; } if (DirectoryExists(modPath)) { debug_printf("ERROR: The mod file '%s' is a directory!\n", modPath.string8().c_str()); continue; } modsToInstall.emplace_back(std::move(modPath)); } // We need to initialize SpiderMonkey and libxml2 in the main thread before // any thread uses them. So initialize them here before we might run Atlas. ScriptEngine scriptEngine; CXeromyces::Startup(); // Initialise the global task manager at this point (JS & Profiler2 are set up). Threading::TaskManager::Initialise(); if (ATLAS_RunIfOnCmdLine(args, false)) { CXeromyces::Terminate(); return; } if (isNonVisualReplay) { Paths paths(args); g_VFS = CreateVfs(); // Mount with highest priority, we don't want mods overwriting this. g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE, VFS_MAX_PRIORITY); { CReplayPlayer replay; replay.Load(replayFile); replay.Replay( args.Has("serializationtest"), args.Has("rejointest") ? args.Get("rejointest").ToInt() : -1, args.Has("ooslog"), !args.Has("hashtest-full") || args.Get("hashtest-full") == "true", args.Has("hashtest-quick") && args.Get("hashtest-quick") == "true"); } g_VFS.reset(); CXeromyces::Terminate(); return; } // run in archive-building mode if requested if (args.Has("archivebuild")) { Paths paths(args); OsPath mod(args.Get("archivebuild")); OsPath zip; if (args.Has("archivebuild-output")) zip = args.Get("archivebuild-output"); else zip = mod.Filename().ChangeExtension(L".zip"); CArchiveBuilder builder(mod, paths.Cache()); // Add mods provided on the command line // NOTE: We do not handle mods in the user mod path here std::vector mods = args.GetMultiple("mod"); for (size_t i = 0; i < mods.size(); ++i) builder.AddBaseMod(paths.RData()/"mods"/mods[i]); builder.Build(zip, args.Has("archivebuild-compress")); CXeromyces::Terminate(); return; } const double res = timer_Resolution(); g_frequencyFilter = CreateFrequencyFilter(res, 30.0); // run the game int flags = INIT_MODS; do { g_Shutdown = ShutdownType::None; if (!Init(args, flags)) { flags &= ~INIT_MODS; Shutdown(SHUTDOWN_FROM_CONFIG); continue; } std::vector installedMods; if (!modsToInstall.empty()) { Paths paths(args); CModInstaller installer(paths.UserData() / "mods", paths.Cache()); // Install the mods without deleting the pyromod files for (const OsPath& modPath : modsToInstall) { CModInstaller::ModInstallationResult result = installer.Install(modPath, g_ScriptContext, true); if (result != CModInstaller::ModInstallationResult::SUCCESS) LOGERROR("Failed to install '%s'", modPath.string8().c_str()); } installedMods = installer.GetInstalledMods(); ScriptInterface modInterface("Engine", "Mod", g_ScriptContext); g_Mods.UpdateAvailableMods(modInterface); } if (isNonVisual) { InitNonVisual(args); if (isUsingRLInterface) StartRLInterface(args); while (g_Shutdown == ShutdownType::None) { if (isUsingRLInterface) g_RLInterface->TryApplyMessage(); else NonVisualFrame(); } } else { InitGraphics(args, 0, installedMods); MainControllerInit(); if (isUsingRLInterface) StartRLInterface(args); while (g_Shutdown == ShutdownType::None) Frame(); } // Do not install mods again in case of restart (typically from the mod selector) modsToInstall.clear(); Shutdown(0); MainControllerShutdown(); flags &= ~INIT_MODS; } while (g_Shutdown == ShutdownType::Restart); #if OS_MACOSX if (g_Shutdown == ShutdownType::RestartAsAtlas) startNewAtlasProcess(g_Mods.GetEnabledMods()); #else if (g_Shutdown == ShutdownType::RestartAsAtlas) ATLAS_RunIfOnCmdLine(args, true); #endif Threading::TaskManager::Instance().ClearQueue(); CXeromyces::Terminate(); } #if OS_ANDROID // In Android we compile the engine as a shared library, not an executable, // so rename main() to a different symbol that the wrapper library can load #undef main #define main pyrogenesis_main extern "C" __attribute__((visibility ("default"))) int main(int argc, char* argv[]); #endif extern "C" int main(int argc, char* argv[]) { #if OS_UNIX // Don't allow people to run the game with root permissions, // because bad things can happen, check before we do anything if (geteuid() == 0) { std::cerr << "********************************************************\n" << "WARNING: Attempted to run the game with root permission!\n" << "This is not allowed because it can alter home directory \n" << "permissions and opens your system to vulnerabilities. \n" << "(You received this message because you were either \n" <<" logged in as root or used e.g. the 'sudo' command.) \n" << "********************************************************\n\n"; return EXIT_FAILURE; } #endif // OS_UNIX EarlyInit(); // must come at beginning of main RunGameOrAtlas(argc, const_cast(argv)); // Shut down profiler initialised by EarlyInit g_Profiler2.Shutdown(); return EXIT_SUCCESS; } Index: ps/trunk/source/ps/Game.cpp =================================================================== --- ps/trunk/source/ps/Game.cpp (revision 25884) +++ ps/trunk/source/ps/Game.cpp (revision 25885) @@ -1,472 +1,469 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. 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 2 of the License, or * (at your option) any later version. * * 0 A.D. 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 0 A.D. If not, see . */ #include "precompiled.h" #include "Game.h" #include "graphics/GameView.h" #include "graphics/LOSTexture.h" #include "graphics/ParticleManager.h" #include "graphics/UnitManager.h" #include "gui/GUIManager.h" #include "gui/CGUI.h" #include "lib/config2.h" #include "lib/timer.h" #include "network/NetClient.h" #include "network/NetServer.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/CStr.h" #include "ps/Loader.h" #include "ps/LoaderThunks.h" #include "ps/Profile.h" #include "ps/Replay.h" #include "ps/World.h" #include "ps/GameSetup/GameSetup.h" #include "renderer/Renderer.h" #include "renderer/TimeManager.h" #include "renderer/WaterManager.h" #include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/JSON.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpPlayer.h" #include "simulation2/components/ICmpPlayerManager.h" #include "simulation2/system/ReplayTurnManager.h" #include "soundmanager/ISoundManager.h" #include "tools/atlas/GameInterface/GameLoop.h" #include -extern bool g_GameRestarted; extern GameLoopState* g_AtlasGameLoop; /** * Globally accessible pointer to the CGame object. **/ CGame *g_Game=NULL; const CStr CGame::EventNameSimulationUpdate = "SimulationUpdate"; /** * Constructor * **/ CGame::CGame(bool replayLog): m_World(new CWorld(this)), m_Simulation2(new CSimulation2(&m_World->GetUnitManager(), g_ScriptContext, m_World->GetTerrain())), m_GameView(CRenderer::IsInitialised() ? new CGameView(this) : nullptr), m_GameStarted(false), m_Paused(false), m_SimRate(1.0f), m_PlayerID(-1), m_ViewedPlayerID(-1), m_IsSavedGame(false), m_IsVisualReplay(false), m_ReplayStream(NULL) { // TODO: should use CDummyReplayLogger unless activated by cmd-line arg, perhaps? if (replayLog) m_ReplayLogger = new CReplayLogger(m_Simulation2->GetScriptInterface()); else m_ReplayLogger = new CDummyReplayLogger(); // Need to set the CObjectManager references after various objects have // been initialised, so do it here rather than via the initialisers above. if (m_GameView) m_World->GetUnitManager().SetObjectManager(m_GameView->GetObjectManager()); m_TurnManager = new CLocalTurnManager(*m_Simulation2, GetReplayLogger()); // this will get replaced if we're a net server/client m_Simulation2->LoadDefaultScripts(); } /** * Destructor * **/ CGame::~CGame() { // Again, the in-game call tree is going to be different to the main menu one. if (CProfileManager::IsInitialised()) g_Profiler.StructuralReset(); if (m_ReplayLogger && m_GameStarted) m_ReplayLogger->SaveMetadata(*m_Simulation2); delete m_TurnManager; delete m_GameView; delete m_Simulation2; delete m_World; delete m_ReplayLogger; delete m_ReplayStream; } void CGame::SetTurnManager(CTurnManager* turnManager) { if (m_TurnManager) delete m_TurnManager; m_TurnManager = turnManager; if (m_TurnManager) m_TurnManager->SetPlayerID(m_PlayerID); } int CGame::LoadVisualReplayData() { ENSURE(m_IsVisualReplay); ENSURE(!m_ReplayPath.empty()); ENSURE(m_ReplayStream); CReplayTurnManager* replayTurnMgr = static_cast(GetTurnManager()); u32 currentTurn = 0; std::string type; while ((*m_ReplayStream >> type).good()) { if (type == "turn") { u32 turn = 0; u32 turnLength = 0; *m_ReplayStream >> turn >> turnLength; ENSURE(turn == currentTurn && "You tried to replay a commands.txt file of a rejoined client. Please use the host's file."); replayTurnMgr->StoreReplayTurnLength(currentTurn, turnLength); } else if (type == "cmd") { player_id_t player; *m_ReplayStream >> player; std::string line; std::getline(*m_ReplayStream, line); replayTurnMgr->StoreReplayCommand(currentTurn, player, line); } else if (type == "hash" || type == "hash-quick") { bool quick = (type == "hash-quick"); std::string replayHash; *m_ReplayStream >> replayHash; replayTurnMgr->StoreReplayHash(currentTurn, replayHash, quick); } else if (type == "end") ++currentTurn; else CancelLoad(L"Failed to load replay data (unrecognized content)"); } SAFE_DELETE(m_ReplayStream); m_FinalReplayTurn = currentTurn > 0 ? currentTurn - 1 : 0; replayTurnMgr->StoreFinalReplayTurn(m_FinalReplayTurn); return 0; } bool CGame::StartVisualReplay(const OsPath& replayPath) { debug_printf("Starting to replay %s\n", replayPath.string8().c_str()); m_IsVisualReplay = true; SetTurnManager(new CReplayTurnManager(*m_Simulation2, GetReplayLogger())); m_ReplayPath = replayPath; m_ReplayStream = new std::ifstream(OsString(replayPath).c_str()); std::string type; ENSURE((*m_ReplayStream >> type).good() && type == "start"); std::string line; std::getline(*m_ReplayStream, line); const ScriptInterface& scriptInterface = m_Simulation2->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue attribs(rq.cx); Script::ParseJSON(rq, line, &attribs); StartGame(&attribs, ""); return true; } /** * Initializes the game with the set of attributes provided. * Makes calls to initialize the game view, world, and simulation objects. * Calls are made to facilitate progress reporting of the initialization. **/ void CGame::RegisterInit(const JS::HandleValue attribs, const std::string& savedState) { const ScriptInterface& scriptInterface = m_Simulation2->GetScriptInterface(); ScriptRequest rq(scriptInterface); m_InitialSavedState = savedState; m_IsSavedGame = !savedState.empty(); m_Simulation2->SetInitAttributes(attribs); std::string mapType; Script::GetProperty(rq, attribs, "mapType", mapType); float speed; if (Script::HasProperty(rq, attribs, "gameSpeed")) { if (Script::GetProperty(rq, attribs, "gameSpeed", speed)) SetSimRate(speed); else LOGERROR("GameSpeed could not be parsed."); } LDR_BeginRegistering(); RegMemFun(m_Simulation2, &CSimulation2::ProgressiveLoad, L"Simulation init", 1000); // RC, 040804 - GameView needs to be initialized before World, otherwise GameView initialization // overwrites anything stored in the map file that gets loaded by CWorld::Initialize with default // values. At the minute, it's just lighting settings, but could be extended to store camera position. // Storing lighting settings in the game view seems a little odd, but it's no big deal; maybe move it at // some point to be stored in the world object? if (m_GameView) m_GameView->RegisterInit(); if (mapType == "random") { // Load random map attributes std::wstring scriptFile; JS::RootedValue settings(rq.cx); Script::GetProperty(rq, attribs, "script", scriptFile); Script::GetProperty(rq, attribs, "settings", &settings); m_World->RegisterInitRMS(scriptFile, *scriptInterface.GetContext(), settings, m_PlayerID); } else { std::wstring mapFile; JS::RootedValue settings(rq.cx); Script::GetProperty(rq, attribs, "map", mapFile); Script::GetProperty(rq, attribs, "settings", &settings); m_World->RegisterInit(mapFile, *scriptInterface.GetContext(), settings, m_PlayerID); } if (m_GameView) RegMemFun(g_Renderer.GetSingletonPtr()->GetWaterManager(), &WaterManager::LoadWaterTextures, L"LoadWaterTextures", 80); if (m_IsSavedGame) RegMemFun(this, &CGame::LoadInitialState, L"Loading game", 1000); if (m_IsVisualReplay) RegMemFun(this, &CGame::LoadVisualReplayData, L"Loading visual replay data", 1000); LDR_EndRegistering(); } int CGame::LoadInitialState() { ENSURE(m_IsSavedGame); ENSURE(!m_InitialSavedState.empty()); std::string state; m_InitialSavedState.swap(state); // deletes the original to save a bit of memory std::stringstream stream(state); bool ok = m_Simulation2->DeserializeState(stream); if (!ok) { CancelLoad(L"Failed to load saved game state. It might have been\nsaved with an incompatible version of the game."); return 0; } return 0; } /** * Game initialization has been completed. Set game started flag and start the session. * * @return PSRETURN 0 **/ PSRETURN CGame::ReallyStartGame() { // Call the script function InitGame only for new games, not saved games if (!m_IsSavedGame) { // Perform some simulation initializations (replace skirmish entities, explore territories, etc.) // that needs to be done before setting up the AI and shouldn't be done in Atlas if (!g_AtlasGameLoop->running) m_Simulation2->PreInitGame(); m_Simulation2->InitGame(); } // We need to do an initial Interpolate call to set up all the models etc, // because Update might never interpolate (e.g. if the game starts paused) // and we could end up rendering before having set up any models (so they'd // all be invisible) Interpolate(0, 0); m_GameStarted=true; // Render a frame to begin loading assets if (CRenderer::IsInitialised()) Render(); if (g_NetClient) g_NetClient->LoadFinished(); // Call the reallyStartGame GUI function, but only if it exists if (g_GUI && g_GUI->GetPageCount()) { std::shared_ptr scriptInterface = g_GUI->GetActiveGUI()->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue global(rq.cx, rq.globalValue()); if (Script::HasProperty(rq, global, "reallyStartGame")) ScriptFunction::CallVoid(rq, global, "reallyStartGame"); } debug_printf("GAME STARTED, ALL INIT COMPLETE\n"); // The call tree we've built for pregame probably isn't useful in-game. if (CProfileManager::IsInitialised()) g_Profiler.StructuralReset(); - g_GameRestarted = true; - return 0; } int CGame::GetPlayerID() { return m_PlayerID; } void CGame::SetPlayerID(player_id_t playerID) { m_PlayerID = playerID; m_ViewedPlayerID = playerID; if (m_TurnManager) m_TurnManager->SetPlayerID(m_PlayerID); } int CGame::GetViewedPlayerID() { return m_ViewedPlayerID; } void CGame::SetViewedPlayerID(player_id_t playerID) { m_ViewedPlayerID = playerID; } void CGame::StartGame(JS::MutableHandleValue attribs, const std::string& savedState) { if (m_ReplayLogger) m_ReplayLogger->StartGame(attribs); RegisterInit(attribs, savedState); } // TODO: doInterpolate is optional because Atlas interpolates explicitly, // so that it has more control over the update rate. The game might want to // do the same, and then doInterpolate should be redundant and removed. void CGame::Update(const double deltaRealTime, bool doInterpolate) { if (m_Paused || !m_TurnManager) return; const double deltaSimTime = deltaRealTime * m_SimRate; if (deltaSimTime) { // At the normal sim rate, we currently want to render at least one // frame per simulation turn, so let maxTurns be 1. But for fast-forward // sim rates we want to allow more, so it's not bounded by framerate, // so just use the sim rate itself as the number of turns per frame. size_t maxTurns = (size_t)m_SimRate; if (m_TurnManager->Update(deltaSimTime, maxTurns)) { { PROFILE3("gui sim update"); g_GUI->SendEventToAll(EventNameSimulationUpdate); } GetView()->GetLOSTexture().MakeDirty(); } if (CRenderer::IsInitialised()) g_Renderer.GetTimeManager().Update(deltaSimTime); } if (doInterpolate) m_TurnManager->Interpolate(deltaSimTime, deltaRealTime); } void CGame::Interpolate(float simFrameLength, float realFrameLength) { if (!m_TurnManager) return; m_TurnManager->Interpolate(simFrameLength, realFrameLength); } static CColor BrokenColor(0.3f, 0.3f, 0.3f, 1.0f); void CGame::CachePlayerColors() { m_PlayerColors.clear(); CmpPtr cmpPlayerManager(*m_Simulation2, SYSTEM_ENTITY); if (!cmpPlayerManager) return; int numPlayers = cmpPlayerManager->GetNumPlayers(); m_PlayerColors.resize(numPlayers); for (int i = 0; i < numPlayers; ++i) { CmpPtr cmpPlayer(*m_Simulation2, cmpPlayerManager->GetPlayerByID(i)); if (!cmpPlayer) m_PlayerColors[i] = BrokenColor; else m_PlayerColors[i] = cmpPlayer->GetDisplayedColor(); } } CColor CGame::GetPlayerColor(player_id_t player) const { if (player < 0 || player >= (int)m_PlayerColors.size()) return BrokenColor; return m_PlayerColors[player]; } bool CGame::IsGameFinished() const { for (const std::pair& p : m_Simulation2->GetEntitiesWithInterface(IID_Player)) { CmpPtr cmpPlayer(*m_Simulation2, p.first); if (cmpPlayer && cmpPlayer->GetState() == "won") return true; } return false; } Index: ps/trunk/source/ps/tests/stub_impl_hack.h =================================================================== --- ps/trunk/source/ps/tests/stub_impl_hack.h (revision 25884) +++ ps/trunk/source/ps/tests/stub_impl_hack.h (revision 25885) @@ -1,49 +1,47 @@ -/* Copyright (C) 2018 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. 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 2 of the License, or * (at your option) any later version. * * 0 A.D. 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 0 A.D. If not, see . */ #include "lib/self_test.h" // usually defined by main.cpp, used by engine's scripting/ScriptFunctions.cpp, // must be included here to placate linker. -bool g_GameRestarted; - bool IsQuitRequested() { return false; } void QuitEngine() { } void StartAtlas() { } void RestartEngine() { } // just so that cxxtestgen doesn't complain "No tests defined" class TestDummy : public CxxTest::TestSuite { public: void test_dummy() { } };