Index: ps/trunk/source/graphics/CameraController.cpp =================================================================== --- ps/trunk/source/graphics/CameraController.cpp (revision 23768) +++ ps/trunk/source/graphics/CameraController.cpp (revision 23769) @@ -1,718 +1,718 @@ /* Copyright (C) 2020 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 "CameraController.h" #include "graphics/HFTracer.h" #include "graphics/Terrain.h" #include "graphics/scripting/JSInterface_GameView.h" #include "lib/input.h" #include "lib/timer.h" #include "maths/MathUtil.h" #include "maths/Matrix3D.h" #include "maths/Quaternion.h" #include "ps/ConfigDB.h" #include "ps/Game.h" #include "ps/Globals.h" #include "ps/Hotkey.h" #include "ps/Joystick.h" #include "ps/Pyrogenesis.h" #include "ps/TouchInput.h" #include "ps/World.h" #include "renderer/Renderer.h" #include "renderer/WaterManager.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpRangeManager.h" extern int g_xres, g_yres; // Maximum distance outside the edge of the map that the camera's // focus point can be moved static const float CAMERA_EDGE_MARGIN = 2.0f * TERRAIN_TILE_SIZE; CCameraController::CCameraController(CCamera& camera) : ICameraController(camera), m_ConstrainCamera(true), m_FollowEntity(INVALID_ENTITY), m_FollowFirstPerson(false), // Dummy values (these will be filled in by the config file) m_ViewScrollSpeed(0), m_ViewScrollSpeedModifier(1), m_ViewRotateXSpeed(0), m_ViewRotateXMin(0), m_ViewRotateXMax(0), m_ViewRotateXDefault(0), m_ViewRotateYSpeed(0), m_ViewRotateYSpeedWheel(0), m_ViewRotateYDefault(0), m_ViewRotateSpeedModifier(1), m_ViewDragSpeed(0), m_ViewZoomSpeed(0), m_ViewZoomSpeedWheel(0), m_ViewZoomMin(0), m_ViewZoomMax(0), m_ViewZoomDefault(0), m_ViewZoomSpeedModifier(1), m_ViewFOV(DEGTORAD(45.f)), m_ViewNear(2.f), m_ViewFar(4096.f), m_JoystickPanX(-1), m_JoystickPanY(-1), m_JoystickRotateX(-1), m_JoystickRotateY(-1), m_JoystickZoomIn(-1), m_JoystickZoomOut(-1), m_HeightSmoothness(0.5f), m_HeightMin(16.f), m_PosX(0, 0, 0.01f), m_PosY(0, 0, 0.01f), m_PosZ(0, 0, 0.01f), m_Zoom(0, 0, 0.1f), m_RotateX(0, 0, 0.001f), m_RotateY(0, 0, 0.001f) { SViewPort vp; vp.m_X = 0; vp.m_Y = 0; vp.m_Width = g_xres; vp.m_Height = g_yres; m_Camera.SetViewPort(vp); SetCameraProjection(); SetupCameraMatrixSmooth(&m_Camera.m_Orientation); m_Camera.UpdateFrustum(); } CCameraController::~CCameraController() = default; void CCameraController::LoadConfig() { CFG_GET_VAL("view.scroll.speed", m_ViewScrollSpeed); CFG_GET_VAL("view.scroll.speed.modifier", m_ViewScrollSpeedModifier); CFG_GET_VAL("view.rotate.x.speed", m_ViewRotateXSpeed); CFG_GET_VAL("view.rotate.x.min", m_ViewRotateXMin); CFG_GET_VAL("view.rotate.x.max", m_ViewRotateXMax); CFG_GET_VAL("view.rotate.x.default", m_ViewRotateXDefault); CFG_GET_VAL("view.rotate.y.speed", m_ViewRotateYSpeed); CFG_GET_VAL("view.rotate.y.speed.wheel", m_ViewRotateYSpeedWheel); CFG_GET_VAL("view.rotate.y.default", m_ViewRotateYDefault); CFG_GET_VAL("view.rotate.speed.modifier", m_ViewRotateSpeedModifier); CFG_GET_VAL("view.drag.speed", m_ViewDragSpeed); CFG_GET_VAL("view.zoom.speed", m_ViewZoomSpeed); CFG_GET_VAL("view.zoom.speed.wheel", m_ViewZoomSpeedWheel); CFG_GET_VAL("view.zoom.min", m_ViewZoomMin); CFG_GET_VAL("view.zoom.max", m_ViewZoomMax); CFG_GET_VAL("view.zoom.default", m_ViewZoomDefault); CFG_GET_VAL("view.zoom.speed.modifier", m_ViewZoomSpeedModifier); CFG_GET_VAL("joystick.camera.pan.x", m_JoystickPanX); CFG_GET_VAL("joystick.camera.pan.y", m_JoystickPanY); CFG_GET_VAL("joystick.camera.rotate.x", m_JoystickRotateX); CFG_GET_VAL("joystick.camera.rotate.y", m_JoystickRotateY); CFG_GET_VAL("joystick.camera.zoom.in", m_JoystickZoomIn); CFG_GET_VAL("joystick.camera.zoom.out", m_JoystickZoomOut); CFG_GET_VAL("view.height.smoothness", m_HeightSmoothness); CFG_GET_VAL("view.height.min", m_HeightMin); #define SETUP_SMOOTHNESS(CFG_PREFIX, SMOOTHED_VALUE) \ { \ float smoothness = SMOOTHED_VALUE.GetSmoothness(); \ CFG_GET_VAL(CFG_PREFIX ".smoothness", smoothness); \ SMOOTHED_VALUE.SetSmoothness(smoothness); \ } SETUP_SMOOTHNESS("view.pos", m_PosX); SETUP_SMOOTHNESS("view.pos", m_PosY); SETUP_SMOOTHNESS("view.pos", m_PosZ); SETUP_SMOOTHNESS("view.zoom", m_Zoom); SETUP_SMOOTHNESS("view.rotate.x", m_RotateX); SETUP_SMOOTHNESS("view.rotate.y", m_RotateY); #undef SETUP_SMOOTHNESS CFG_GET_VAL("view.near", m_ViewNear); CFG_GET_VAL("view.far", m_ViewFar); CFG_GET_VAL("view.fov", m_ViewFOV); // Convert to radians m_RotateX.SetValue(DEGTORAD(m_ViewRotateXDefault)); m_RotateY.SetValue(DEGTORAD(m_ViewRotateYDefault)); m_ViewFOV = DEGTORAD(m_ViewFOV); } void CCameraController::SetViewport(const SViewPort& vp) { m_Camera.SetViewPort(vp); SetCameraProjection(); } void CCameraController::Update(const float deltaRealTime) { // Calculate mouse movement static int mouse_last_x = 0; static int mouse_last_y = 0; int mouse_dx = g_mouse_x - mouse_last_x; int mouse_dy = g_mouse_y - mouse_last_y; mouse_last_x = g_mouse_x; mouse_last_y = g_mouse_y; if (HotkeyIsPressed("camera.rotate.cw")) m_RotateY.AddSmoothly(m_ViewRotateYSpeed * deltaRealTime); if (HotkeyIsPressed("camera.rotate.ccw")) m_RotateY.AddSmoothly(-m_ViewRotateYSpeed * deltaRealTime); if (HotkeyIsPressed("camera.rotate.up")) m_RotateX.AddSmoothly(-m_ViewRotateXSpeed * deltaRealTime); if (HotkeyIsPressed("camera.rotate.down")) m_RotateX.AddSmoothly(m_ViewRotateXSpeed * deltaRealTime); float moveRightward = 0.f; float moveForward = 0.f; if (HotkeyIsPressed("camera.pan")) { moveRightward += m_ViewDragSpeed * mouse_dx; moveForward += m_ViewDragSpeed * -mouse_dy; } if (g_mouse_active) { if (g_mouse_x >= g_xres - 2 && g_mouse_x < g_xres) moveRightward += m_ViewScrollSpeed * deltaRealTime; else if (g_mouse_x <= 3 && g_mouse_x >= 0) moveRightward -= m_ViewScrollSpeed * deltaRealTime; if (g_mouse_y >= g_yres - 2 && g_mouse_y < g_yres) moveForward -= m_ViewScrollSpeed * deltaRealTime; else if (g_mouse_y <= 3 && g_mouse_y >= 0) moveForward += m_ViewScrollSpeed * deltaRealTime; } if (HotkeyIsPressed("camera.right")) moveRightward += m_ViewScrollSpeed * deltaRealTime; if (HotkeyIsPressed("camera.left")) moveRightward -= m_ViewScrollSpeed * deltaRealTime; if (HotkeyIsPressed("camera.up")) moveForward += m_ViewScrollSpeed * deltaRealTime; if (HotkeyIsPressed("camera.down")) moveForward -= m_ViewScrollSpeed * deltaRealTime; if (g_Joystick.IsEnabled()) { // This could all be improved with extra speed and sensitivity settings // (maybe use pow to allow finer control?), and inversion settings moveRightward += g_Joystick.GetAxisValue(m_JoystickPanX) * m_ViewScrollSpeed * deltaRealTime; moveForward -= g_Joystick.GetAxisValue(m_JoystickPanY) * m_ViewScrollSpeed * deltaRealTime; m_RotateX.AddSmoothly(g_Joystick.GetAxisValue(m_JoystickRotateX) * m_ViewRotateXSpeed * deltaRealTime); m_RotateY.AddSmoothly(-g_Joystick.GetAxisValue(m_JoystickRotateY) * m_ViewRotateYSpeed * deltaRealTime); // Use a +1 bias for zoom because I want this to work with trigger buttons that default to -1 m_Zoom.AddSmoothly((g_Joystick.GetAxisValue(m_JoystickZoomIn) + 1.0f) / 2.0f * m_ViewZoomSpeed * deltaRealTime); m_Zoom.AddSmoothly(-(g_Joystick.GetAxisValue(m_JoystickZoomOut) + 1.0f) / 2.0f * m_ViewZoomSpeed * deltaRealTime); } if (moveRightward || moveForward) { // Break out of following mode when the user starts scrolling m_FollowEntity = INVALID_ENTITY; float s = sin(m_RotateY.GetSmoothedValue()); float c = cos(m_RotateY.GetSmoothedValue()); m_PosX.AddSmoothly(c * moveRightward); m_PosZ.AddSmoothly(-s * moveRightward); m_PosX.AddSmoothly(s * moveForward); m_PosZ.AddSmoothly(c * moveForward); } if (m_FollowEntity) { CmpPtr cmpPosition(*(g_Game->GetSimulation2()), m_FollowEntity); CmpPtr cmpRangeManager(*(g_Game->GetSimulation2()), SYSTEM_ENTITY); if (cmpPosition && cmpPosition->IsInWorld() && - cmpRangeManager && cmpRangeManager->GetLosVisibility(m_FollowEntity, g_Game->GetViewedPlayerID()) == ICmpRangeManager::VIS_VISIBLE) + cmpRangeManager && cmpRangeManager->GetLosVisibility(m_FollowEntity, g_Game->GetViewedPlayerID()) == LosVisibility::VISIBLE) { // Get the most recent interpolated position float frameOffset = g_Game->GetSimulation2()->GetLastFrameOffset(); CMatrix3D transform = cmpPosition->GetInterpolatedTransform(frameOffset); CVector3D pos = transform.GetTranslation(); if (m_FollowFirstPerson) { float x, z, angle; cmpPosition->GetInterpolatedPosition2D(frameOffset, x, z, angle); float height = 4.f; m_Camera.m_Orientation.SetIdentity(); m_Camera.m_Orientation.RotateX(static_cast(M_PI) / 24.f); m_Camera.m_Orientation.RotateY(angle); m_Camera.m_Orientation.Translate(pos.X, pos.Y + height, pos.Z); m_Camera.UpdateFrustum(); return; } else { // Move the camera to match the unit CCamera targetCam = m_Camera; SetupCameraMatrixSmoothRot(&targetCam.m_Orientation); CVector3D pivot = GetSmoothPivot(targetCam); CVector3D delta = pos - pivot; m_PosX.AddSmoothly(delta.X); m_PosY.AddSmoothly(delta.Y); m_PosZ.AddSmoothly(delta.Z); } } else { // The unit disappeared (died or garrisoned etc), so stop following it m_FollowEntity = INVALID_ENTITY; } } if (HotkeyIsPressed("camera.zoom.in")) m_Zoom.AddSmoothly(-m_ViewZoomSpeed * deltaRealTime); if (HotkeyIsPressed("camera.zoom.out")) m_Zoom.AddSmoothly(m_ViewZoomSpeed * deltaRealTime); if (m_ConstrainCamera) m_Zoom.ClampSmoothly(m_ViewZoomMin, m_ViewZoomMax); float zoomDelta = -m_Zoom.Update(deltaRealTime); if (zoomDelta) { CVector3D forwards = m_Camera.GetOrientation().GetIn(); m_PosX.AddSmoothly(forwards.X * zoomDelta); m_PosY.AddSmoothly(forwards.Y * zoomDelta); m_PosZ.AddSmoothly(forwards.Z * zoomDelta); } if (m_ConstrainCamera) m_RotateX.ClampSmoothly(DEGTORAD(m_ViewRotateXMin), DEGTORAD(m_ViewRotateXMax)); FocusHeight(true); // Ensure the ViewCamera focus is inside the map with the chosen margins // if not so - apply margins to the camera if (m_ConstrainCamera) { CCamera targetCam = m_Camera; SetupCameraMatrixSmoothRot(&targetCam.m_Orientation); CTerrain* pTerrain = g_Game->GetWorld()->GetTerrain(); CVector3D pivot = GetSmoothPivot(targetCam); CVector3D delta = targetCam.GetOrientation().GetTranslation() - pivot; CVector3D desiredPivot = pivot; CmpPtr cmpRangeManager(*(g_Game->GetSimulation2()), SYSTEM_ENTITY); if (cmpRangeManager && cmpRangeManager->GetLosCircular()) { // Clamp to a circular region around the center of the map float r = pTerrain->GetMaxX() / 2; CVector3D center(r, desiredPivot.Y, r); float dist = (desiredPivot - center).Length(); if (dist > r - CAMERA_EDGE_MARGIN) desiredPivot = center + (desiredPivot - center).Normalized() * (r - CAMERA_EDGE_MARGIN); } else { // Clamp to the square edges of the map desiredPivot.X = Clamp(desiredPivot.X, pTerrain->GetMinX() + CAMERA_EDGE_MARGIN, pTerrain->GetMaxX() - CAMERA_EDGE_MARGIN); desiredPivot.Z = Clamp(desiredPivot.Z, pTerrain->GetMinZ() + CAMERA_EDGE_MARGIN, pTerrain->GetMaxZ() - CAMERA_EDGE_MARGIN); } // Update the position so that pivot is within the margin m_PosX.SetValueSmoothly(desiredPivot.X + delta.X); m_PosZ.SetValueSmoothly(desiredPivot.Z + delta.Z); } m_PosX.Update(deltaRealTime); m_PosY.Update(deltaRealTime); m_PosZ.Update(deltaRealTime); // Handle rotation around the Y (vertical) axis { CCamera targetCam = m_Camera; SetupCameraMatrixSmooth(&targetCam.m_Orientation); float rotateYDelta = m_RotateY.Update(deltaRealTime); if (rotateYDelta) { // We've updated RotateY, and need to adjust Pos so that it's still // facing towards the original focus point (the terrain in the center // of the screen). CVector3D upwards(0.0f, 1.0f, 0.0f); CVector3D pivot = GetSmoothPivot(targetCam); CVector3D delta = targetCam.GetOrientation().GetTranslation() - pivot; CQuaternion q; q.FromAxisAngle(upwards, rotateYDelta); CVector3D d = q.Rotate(delta) - delta; m_PosX.Add(d.X); m_PosY.Add(d.Y); m_PosZ.Add(d.Z); } } // Handle rotation around the X (sideways, relative to camera) axis { CCamera targetCam = m_Camera; SetupCameraMatrixSmooth(&targetCam.m_Orientation); float rotateXDelta = m_RotateX.Update(deltaRealTime); if (rotateXDelta) { CVector3D rightwards = targetCam.GetOrientation().GetLeft() * -1.0f; CVector3D pivot = GetSmoothPivot(targetCam); CVector3D delta = targetCam.GetOrientation().GetTranslation() - pivot; CQuaternion q; q.FromAxisAngle(rightwards, rotateXDelta); CVector3D d = q.Rotate(delta) - delta; m_PosX.Add(d.X); m_PosY.Add(d.Y); m_PosZ.Add(d.Z); } } /* This is disabled since it doesn't seem necessary: // Ensure the camera's near point is never inside the terrain if (m_ConstrainCamera) { CMatrix3D target; target.SetIdentity(); target.RotateX(m_RotateX.GetValue()); target.RotateY(m_RotateY.GetValue()); target.Translate(m_PosX.GetValue(), m_PosY.GetValue(), m_PosZ.GetValue()); CVector3D nearPoint = target.GetTranslation() + target.GetIn() * defaultNear; float ground = g_Game->GetWorld()->GetTerrain()->GetExactGroundLevel(nearPoint.X, nearPoint.Z); float limit = ground + 16.f; if (nearPoint.Y < limit) m_PosY.AddSmoothly(limit - nearPoint.Y); } */ m_RotateY.Wrap(-static_cast(M_PI), static_cast(M_PI)); // Update the camera matrix SetCameraProjection(); SetupCameraMatrixSmooth(&m_Camera.m_Orientation); m_Camera.UpdateFrustum(); } CVector3D CCameraController::GetSmoothPivot(CCamera& camera) const { return camera.GetOrientation().GetTranslation() + camera.GetOrientation().GetIn() * m_Zoom.GetSmoothedValue(); } CVector3D CCameraController::GetCameraPivot() const { return GetSmoothPivot(m_Camera); } CVector3D CCameraController::GetCameraPosition() const { return CVector3D(m_PosX.GetValue(), m_PosY.GetValue(), m_PosZ.GetValue()); } CVector3D CCameraController::GetCameraRotation() const { // The angle of rotation around the Z axis is not used. return CVector3D(m_RotateX.GetValue(), m_RotateY.GetValue(), 0.0f); } float CCameraController::GetCameraZoom() const { return m_Zoom.GetValue(); } void CCameraController::SetCamera(const CVector3D& pos, float rotX, float rotY, float zoom) { m_PosX.SetValue(pos.X); m_PosY.SetValue(pos.Y); m_PosZ.SetValue(pos.Z); m_RotateX.SetValue(rotX); m_RotateY.SetValue(rotY); m_Zoom.SetValue(zoom); FocusHeight(false); SetupCameraMatrixNonSmooth(&m_Camera.m_Orientation); m_Camera.UpdateFrustum(); // Break out of following mode so the camera really moves to the target m_FollowEntity = INVALID_ENTITY; } void CCameraController::MoveCameraTarget(const CVector3D& target) { // Maintain the same orientation and level of zoom, if we can // (do this by working out the point the camera is looking at, saving // the difference between that position and the camera point, and restoring // that difference to our new target) CCamera targetCam = m_Camera; SetupCameraMatrixNonSmooth(&targetCam.m_Orientation); CVector3D pivot = GetSmoothPivot(targetCam); CVector3D delta = target - pivot; m_PosX.SetValueSmoothly(delta.X + m_PosX.GetValue()); m_PosZ.SetValueSmoothly(delta.Z + m_PosZ.GetValue()); FocusHeight(false); // Break out of following mode so the camera really moves to the target m_FollowEntity = INVALID_ENTITY; } void CCameraController::ResetCameraTarget(const CVector3D& target) { CMatrix3D orientation; orientation.SetIdentity(); orientation.RotateX(DEGTORAD(m_ViewRotateXDefault)); orientation.RotateY(DEGTORAD(m_ViewRotateYDefault)); CVector3D delta = orientation.GetIn() * m_ViewZoomDefault; m_PosX.SetValue(target.X - delta.X); m_PosY.SetValue(target.Y - delta.Y); m_PosZ.SetValue(target.Z - delta.Z); m_RotateX.SetValue(DEGTORAD(m_ViewRotateXDefault)); m_RotateY.SetValue(DEGTORAD(m_ViewRotateYDefault)); m_Zoom.SetValue(m_ViewZoomDefault); FocusHeight(false); SetupCameraMatrixSmooth(&m_Camera.m_Orientation); m_Camera.UpdateFrustum(); // Break out of following mode so the camera really moves to the target m_FollowEntity = INVALID_ENTITY; } void CCameraController::FollowEntity(entity_id_t entity, bool firstPerson) { m_FollowEntity = entity; m_FollowFirstPerson = firstPerson; } entity_id_t CCameraController::GetFollowedEntity() { return m_FollowEntity; } void CCameraController::SetCameraProjection() { m_Camera.SetPerspectiveProjection(m_ViewNear, m_ViewFar, m_ViewFOV); } void CCameraController::ResetCameraAngleZoom() { CCamera targetCam = m_Camera; SetupCameraMatrixNonSmooth(&targetCam.m_Orientation); // Compute the zoom adjustment to get us back to the default CVector3D forwards = targetCam.GetOrientation().GetIn(); CVector3D pivot = GetSmoothPivot(targetCam); CVector3D delta = pivot - targetCam.GetOrientation().GetTranslation(); float dist = delta.Dot(forwards); m_Zoom.AddSmoothly(m_ViewZoomDefault - dist); // Reset orientations to default m_RotateX.SetValueSmoothly(DEGTORAD(m_ViewRotateXDefault)); m_RotateY.SetValueSmoothly(DEGTORAD(m_ViewRotateYDefault)); } void CCameraController::SetupCameraMatrixSmooth(CMatrix3D* orientation) { orientation->SetIdentity(); orientation->RotateX(m_RotateX.GetSmoothedValue()); orientation->RotateY(m_RotateY.GetSmoothedValue()); orientation->Translate(m_PosX.GetSmoothedValue(), m_PosY.GetSmoothedValue(), m_PosZ.GetSmoothedValue()); } void CCameraController::SetupCameraMatrixSmoothRot(CMatrix3D* orientation) { orientation->SetIdentity(); orientation->RotateX(m_RotateX.GetSmoothedValue()); orientation->RotateY(m_RotateY.GetSmoothedValue()); orientation->Translate(m_PosX.GetValue(), m_PosY.GetValue(), m_PosZ.GetValue()); } void CCameraController::SetupCameraMatrixNonSmooth(CMatrix3D* orientation) { orientation->SetIdentity(); orientation->RotateX(m_RotateX.GetValue()); orientation->RotateY(m_RotateY.GetValue()); orientation->Translate(m_PosX.GetValue(), m_PosY.GetValue(), m_PosZ.GetValue()); } void CCameraController::FocusHeight(bool smooth) { /* The camera pivot height is moved towards ground level. To prevent excessive zoom when looking over a cliff, the target ground level is the maximum of the ground level at the camera's near and pivot points. The ground levels are filtered to achieve smooth camera movement. The filter radius is proportional to the zoom level. The camera height is clamped to prevent map penetration. */ if (!m_ConstrainCamera) return; CCamera targetCam = m_Camera; SetupCameraMatrixSmoothRot(&targetCam.m_Orientation); const CVector3D position = targetCam.GetOrientation().GetTranslation(); const CVector3D forwards = targetCam.GetOrientation().GetIn(); // horizontal view radius const float radius = sqrtf(forwards.X * forwards.X + forwards.Z * forwards.Z) * m_Zoom.GetSmoothedValue(); const float near_radius = radius * m_HeightSmoothness; const float pivot_radius = radius * m_HeightSmoothness; const CVector3D nearPoint = position + forwards * m_ViewNear; const CVector3D pivotPoint = position + forwards * m_Zoom.GetSmoothedValue(); const float ground = std::max( g_Game->GetWorld()->GetTerrain()->GetExactGroundLevel(nearPoint.X, nearPoint.Z), g_Renderer.GetWaterManager()->m_WaterHeight); // filter ground levels for smooth camera movement const float filtered_near_ground = g_Game->GetWorld()->GetTerrain()->GetFilteredGroundLevel(nearPoint.X, nearPoint.Z, near_radius); const float filtered_pivot_ground = g_Game->GetWorld()->GetTerrain()->GetFilteredGroundLevel(pivotPoint.X, pivotPoint.Z, pivot_radius); // filtered maximum visible ground level in view const float filtered_ground = std::max( std::max(filtered_near_ground, filtered_pivot_ground), g_Renderer.GetWaterManager()->m_WaterHeight); // target camera height above pivot point const float pivot_height = -forwards.Y * (m_Zoom.GetSmoothedValue() - m_ViewNear); // minimum camera height above filtered ground level const float min_height = (m_HeightMin + ground - filtered_ground); const float target_height = std::max(pivot_height, min_height); const float height = (nearPoint.Y - filtered_ground); const float diff = target_height - height; if (fabsf(diff) < 0.0001f) return; if (smooth) m_PosY.AddSmoothly(diff); else m_PosY.Add(diff); } InReaction CCameraController::HandleEvent(const SDL_Event_* ev) { switch (ev->ev.type) { case SDL_HOTKEYPRESS: { std::string hotkey = static_cast(ev->ev.user.data1); if (hotkey == "camera.reset") { ResetCameraAngleZoom(); return IN_HANDLED; } return IN_PASS; } case SDL_HOTKEYDOWN: { std::string hotkey = static_cast(ev->ev.user.data1); // Mouse wheel must be treated using events instead of polling, // because SDL auto-generates a sequence of mousedown/mouseup events // and we never get to see the "down" state inside Update(). if (hotkey == "camera.zoom.wheel.in") { m_Zoom.AddSmoothly(-m_ViewZoomSpeedWheel); return IN_HANDLED; } else if (hotkey == "camera.zoom.wheel.out") { m_Zoom.AddSmoothly(m_ViewZoomSpeedWheel); return IN_HANDLED; } else if (hotkey == "camera.rotate.wheel.cw") { m_RotateY.AddSmoothly(m_ViewRotateYSpeedWheel); return IN_HANDLED; } else if (hotkey == "camera.rotate.wheel.ccw") { m_RotateY.AddSmoothly(-m_ViewRotateYSpeedWheel); return IN_HANDLED; } else if (hotkey == "camera.scroll.speed.increase") { m_ViewScrollSpeed *= m_ViewScrollSpeedModifier; return IN_HANDLED; } else if (hotkey == "camera.scroll.speed.decrease") { m_ViewScrollSpeed /= m_ViewScrollSpeedModifier; return IN_HANDLED; } else if (hotkey == "camera.rotate.speed.increase") { m_ViewRotateXSpeed *= m_ViewRotateSpeedModifier; m_ViewRotateYSpeed *= m_ViewRotateSpeedModifier; return IN_HANDLED; } else if (hotkey == "camera.rotate.speed.decrease") { m_ViewRotateXSpeed /= m_ViewRotateSpeedModifier; m_ViewRotateYSpeed /= m_ViewRotateSpeedModifier; return IN_HANDLED; } else if (hotkey == "camera.zoom.speed.increase") { m_ViewZoomSpeed *= m_ViewZoomSpeedModifier; return IN_HANDLED; } else if (hotkey == "camera.zoom.speed.decrease") { m_ViewZoomSpeed /= m_ViewZoomSpeedModifier; return IN_HANDLED; } return IN_PASS; } } return IN_PASS; } Index: ps/trunk/source/graphics/tests/test_LOSTexture.h =================================================================== --- ps/trunk/source/graphics/tests/test_LOSTexture.h (revision 23768) +++ ps/trunk/source/graphics/tests/test_LOSTexture.h (revision 23769) @@ -1,90 +1,93 @@ -/* Copyright (C) 2012 Wildfire Games. +/* Copyright (C) 2020 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" #include "graphics/LOSTexture.h" #include "lib/timer.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" class TestLOSTexture : public CxxTest::TestSuite { public: void test_basic() { CSimulation2 sim(NULL, g_ScriptRuntime, NULL); CLOSTexture tex(sim); const ssize_t size = 8; u32 inputData[size*size] = { 2, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2 }; - std::vector inputDataVec(inputData, inputData+size*size); + Grid inputDataVec(size, size); - // LOS_MASK should be cmpRanageManager->GetSharedLosMask(1), + for (u8 i = 0; i < size; ++i) + for (u8 j = 0; j < size; ++j) + inputDataVec.set(i, j, inputData[i + j * size]); + + // LosState::MASK should be cmpRanageManager->GetSharedLosMask(1), // but that would mean adding a huge mock component for this and it - // should always be LOS_MASK for player 1 (as the other players are bit-shifted). - ICmpRangeManager::CLosQuerier los(ICmpRangeManager::LOS_MASK, inputDataVec, size); + // should always be LosState::MASK for player 1 (as the other players are bit-shifted). + ICmpRangeManager::CLosQuerier los((u32)LosState::MASK, inputDataVec, size); std::vector losData; size_t pitch; losData.resize(tex.GetBitmapSize(size, size, &pitch)); tex.GenerateBitmap(los, &losData[0], size, size, pitch); // for (size_t i = 0; i < losData.size(); ++i) // printf("%s %3d", i % (size_t)sqrt(losData.size()) ? "" : "\n", losData[i]); TS_ASSERT_EQUALS(losData[0], 104); } void test_perf_DISABLED() { CSimulation2 sim(NULL, g_ScriptRuntime, NULL); CLOSTexture tex(sim); const ssize_t size = 257; - std::vector inputDataVec; - inputDataVec.resize(size*size); + Grid inputDataVec(size, size); - // LOS_MASK should be cmpRanageManager->GetSharedLosMask(1), + // LosState::MASK should be cmpRanageManager->GetSharedLosMask(1), // but that would mean adding a huge mock component for this and it - // should always be LOS_MASK for player 1 (as the other players are bit-shifted). - ICmpRangeManager::CLosQuerier los(ICmpRangeManager::LOS_MASK, inputDataVec, size); + // should always be LosState::MASK for player 1 (as the other players are bit-shifted). + ICmpRangeManager::CLosQuerier los((u32)LosState::MASK, inputDataVec, size); size_t reps = 128; double t = timer_Time(); for (size_t i = 0; i < reps; ++i) { std::vector losData; size_t pitch; losData.resize(tex.GetBitmapSize(size, size, &pitch)); tex.GenerateBitmap(los, &losData[0], size, size, pitch); } double dt = timer_Time() - t; printf("\n# %f secs\n", dt/reps); } }; Index: ps/trunk/source/gui/ObjectTypes/CMiniMap.cpp =================================================================== --- ps/trunk/source/gui/ObjectTypes/CMiniMap.cpp (revision 23768) +++ ps/trunk/source/gui/ObjectTypes/CMiniMap.cpp (revision 23769) @@ -1,720 +1,720 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2020 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/GameView.h" #include "graphics/LOSTexture.h" #include "graphics/MiniPatch.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/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/GameSetup/Config.h" #include "ps/Profile.h" #include "ps/World.h" #include "ps/XML/Xeromyces.h" #include "renderer/Renderer.h" #include "renderer/RenderingOptions.h" #include "renderer/WaterManager.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/components/ICmpMinimap.h" #include "simulation2/Simulation2.h" #include "simulation2/system/ParamNode.h" #include extern bool g_GameRestarted; // 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; static unsigned int ScaleColor(unsigned int color, float x) { unsigned int r = unsigned(float(color & 0xff) * x); unsigned int g = unsigned(float((color>>8) & 0xff) * x); unsigned int b = unsigned(float((color>>16) & 0xff) * x); return (0xff000000 | b | g<<8 | r<<16); } const CStr CMiniMap::EventNameWorldClick = "WorldClick"; CMiniMap::CMiniMap(CGUI& pGUI) : IGUIObject(pGUI), m_TerrainTexture(0), m_TerrainData(0), m_MapSize(0), m_Terrain(0), m_TerrainDirty(true), m_MapScale(1.f), m_EntitiesDrawn(0), m_IndexArray(GL_STATIC_DRAW), m_VertexArray(GL_DYNAMIC_DRAW), m_NextBlinkTime(0.0), m_PingDuration(25.0), m_BlinkState(false), m_WaterHeight(0.0) { m_Clicking = false; m_MouseHovering = false; // Register Relax NG validator CXeromyces::AddValidator(g_VFS, "pathfinder", "simulation/data/pathfinder.rng"); // Get the maximum height for unit passage in water. CParamNode externalParamNode; CParamNode::LoadXML(externalParamNode, L"simulation/data/pathfinder.xml", "pathfinder"); const CParamNode pathingSettings = externalParamNode.GetChild("Pathfinder").GetChild("PassabilityClasses"); if (pathingSettings.GetChild("default").IsOk() && pathingSettings.GetChild("default").GetChild("MaxWaterDepth").IsOk()) m_ShallowPassageHeight = pathingSettings.GetChild("default").GetChild("MaxWaterDepth").ToFloat(); else m_ShallowPassageHeight = 0.0f; 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() { Destroy(); } void CMiniMap::HandleMessage(SGUIMessage& Message) { IGUIObject::HandleMessage(Message); switch (Message.type) { case GUIM_MOUSE_PRESS_LEFT: if (m_MouseHovering) { if (!CMiniMap::FireWorldClickEvent(SDL_BUTTON_LEFT, 1)) { SetCameraPos(); m_Clicking = true; } } break; case GUIM_MOUSE_RELEASE_LEFT: if (m_MouseHovering && m_Clicking) SetCameraPos(); m_Clicking = false; break; case GUIM_MOUSE_DBLCLICK_LEFT: if (m_MouseHovering && m_Clicking) SetCameraPos(); 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) SetCameraPos(); break; case GUIM_MOUSE_WHEEL_DOWN: case GUIM_MOUSE_WHEEL_UP: Message.Skip(); break; default: break; } } bool CMiniMap::IsMouseOver() const { // Get the mouse position. const CPos& mousePos = m_pGUI.GetMousePos(); // Get the position of the center of the minimap. CPos minimapCenter = CPos(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 CPos& 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::SetCameraPos() { 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 = m_Camera->GetOrientation().GetIn(); return -atan2(cameraIn.X, cameraIn.Z); } bool CMiniMap::FireWorldClickEvent(int button, int UNUSED(clicks)) { JSContext* cx = g_GUI->GetActiveGUI()->GetScriptInterface()->GetContext(); JSAutoRequest rq(cx); float x, z; GetMouseWorldCoordinates(x, z); JS::RootedValue coords(cx); ScriptInterface::CreateObject(cx, &coords, "x", x, "z", z); JS::RootedValue buttonJs(cx); ScriptInterface::ToJSVal(cx, &buttonJs, button); JS::AutoValueVector paramData(cx); paramData.append(coords); 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(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); CVector3D hitPt[4]; hitPt[0] = m_Camera->GetWorldCoordinates(0, g_Renderer.GetHeight(), h); hitPt[1] = m_Camera->GetWorldCoordinates(g_Renderer.GetWidth(), g_Renderer.GetHeight(), h); hitPt[2] = m_Camera->GetWorldCoordinates(g_Renderer.GetWidth(), 0, h); hitPt[3] = m_Camera->GetWorldCoordinates(0, 0, h); float ViewRect[4][2]; for (int i = 0; i < 4; ++i) { // convert to minimap space ViewRect[i][0] = (width * hitPt[i].X * invTileMapSize); ViewRect[i][1] = (height * hitPt[i].Z * invTileMapSize); } float viewVerts[] = { ViewRect[0][0], -ViewRect[0][1], ViewRect[1][0], -ViewRect[1][1], ViewRect[2][0], -ViewRect[2][1], ViewRect[3][0], -ViewRect[3][1] }; // Enable Scissoring to restrict the rectangle to only the minimap. glScissor( m_CachedActualSize.left * g_GuiScale, g_Renderer.GetHeight() - m_CachedActualSize.bottom * g_GuiScale, width * g_GuiScale, height * g_GuiScale); glEnable(GL_SCISSOR_TEST); 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, viewVerts); shader->AssertPointersBound(); if (!g_Renderer.m_SkipSubmit) glDrawArrays(GL_LINE_LOOP, 0, 4); tech->EndPass(); glLineWidth(1.0f); glDisable(GL_SCISSOR_TEST); } 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; } void CMiniMap::DrawTexture(CShaderProgramPtr shader, float coordMax, float angle, float x, float y, float x2, float y2, float z) const { // 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) * m_MapScale; const float c = cos(angle) * m_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, z, x2, y, z, x2, y2, z, x2, y2, z, x, y2, z, x, y, z }; shader->TexCoordPointer(GL_TEXTURE0, 2, GL_FLOAT, 0, quadTex); shader->VertexPointer(3, GL_FLOAT, 0, quadVerts); shader->AssertPointersBound(); if (!g_Renderer.m_SkipSubmit) glDrawArrays(GL_TRIANGLES, 0, 6); } // 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() { 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; CSimulation2* sim = g_Game->GetSimulation2(); CmpPtr cmpRangeManager(*sim, SYSTEM_ENTITY); ENSURE(cmpRangeManager); // Set our globals in case they hadn't been set before m_Camera = g_Game->GetView()->GetCamera(); m_Terrain = g_Game->GetWorld()->GetTerrain(); m_Width = (u32)(m_CachedActualSize.right - m_CachedActualSize.left); m_Height = (u32)(m_CachedActualSize.bottom - m_CachedActualSize.top); m_MapSize = m_Terrain->GetVerticesPerSide(); m_TextureSize = (GLsizei)round_up_to_pow2((size_t)m_MapSize); m_MapScale = (cmpRangeManager->GetLosCircular() ? 1.f : 1.414f); if (!m_TerrainTexture || g_GameRestarted) CreateTextures(); // 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; if (m_TerrainDirty || m_WaterHeight != g_Renderer.GetWaterManager()->m_WaterHeight) RebuildTerrainTexture(); } const float x = m_CachedActualSize.left, y = m_CachedActualSize.bottom; const float x2 = m_CachedActualSize.right, y2 = m_CachedActualSize.top; const float z = GetBufferedZ(); const float texCoordMax = (float)(m_MapSize - 1) / (float)m_TextureSize; const float angle = GetAngle(); const float unitScale = (cmpRangeManager->GetLosCircular() ? 1.f : m_MapScale/2.f); // Disable depth updates to prevent apparent z-fighting-related issues // with some drivers causing units to get drawn behind the texture. glDepthMask(0); CShaderProgramPtr shader; CShaderTechniquePtr tech; CShaderDefines baseDefines; baseDefines.Add(str_MINIMAP_BASE, str_1); tech = g_Renderer.GetShaderManager().LoadEffect(str_minimap, g_Renderer.GetSystemShaderDefines(), baseDefines); tech->BeginPass(); shader = tech->GetShader(); // Draw the main textured quad shader->BindTexture(str_baseTex, m_TerrainTexture); const CMatrix3D baseTransform = GetDefaultGuiMatrix(); CMatrix3D baseTextureTransform; baseTextureTransform.SetIdentity(); shader->Uniform(str_transform, baseTransform); shader->Uniform(str_textureTransform, baseTextureTransform); DrawTexture(shader, texCoordMax, angle, x, y, x2, y2, z); // Draw territory boundaries glEnable(GL_BLEND); CTerritoryTexture& territoryTexture = g_Game->GetView()->GetTerritoryTexture(); shader->BindTexture(str_baseTex, territoryTexture.GetTexture()); const CMatrix3D* territoryTransform = territoryTexture.GetMinimapTextureMatrix(); shader->Uniform(str_transform, baseTransform); shader->Uniform(str_textureTransform, *territoryTransform); DrawTexture(shader, 1.0f, angle, x, y, x2, y2, z); tech->EndPass(); // Draw the LOS quad in black, using alpha values from the LOS texture CLOSTexture& losTexture = g_Game->GetView()->GetLOSTexture(); 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()); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); const CMatrix3D* losTransform = losTexture.GetMinimapTextureMatrix(); shader->Uniform(str_transform, baseTransform); shader->Uniform(str_textureTransform, *losTransform); DrawTexture(shader, 1.0f, angle, x, y, x2, y2, z); tech->EndPass(); glDisable(GL_BLEND); PROFILE_START("minimap units"); 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, z); // Apply the gui matrix. unitMatrix *= GetDefaultGuiMatrix(); // Load the transform into the shader. shader->Uniform(str_transform, unitMatrix); const float sx = (float)m_Width / ((m_MapSize - 1) * TERRAIN_TILE_SIZE); const float sy = (float)m_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)) { - ICmpRangeManager::ELosVisibility vis = cmpRangeManager->GetLosVisibility(it->first, g_Game->GetSimulation2()->GetSimContext().GetCurrentDisplayedPlayer()); - if (vis != ICmpRangeManager::VIS_HIDDEN) + 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 (size_t v = 0; v < pingingVertices.size(); ++v) { addVertex(pingingVertices[v], attrColor, attrPos); ++m_EntitiesDrawn; } ENSURE(m_EntitiesDrawn < MAX_ENTITIES_DRAWN); m_VertexArray.Upload(); } m_VertexArray.PrepareForRendering(); if (m_EntitiesDrawn > 0) { #if !CONFIG2_GLES if (g_RenderingOptions.GetRenderPath() == RenderPath::SHADER) 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.m_SkipSubmit) glDrawElements(GL_POINTS, (GLsizei)(m_EntitiesDrawn), GL_UNSIGNED_SHORT, indexBase); g_Renderer.GetStats().m_DrawCalls++; CVertexBuffer::Unbind(); #if !CONFIG2_GLES if (g_RenderingOptions.GetRenderPath() == RenderPath::SHADER) glDisable(GL_VERTEX_PROGRAM_POINT_SIZE); #endif } tech->EndPass(); DrawViewRect(unitMatrix); PROFILE_END("minimap units"); // Reset depth mask glDepthMask(1); } void CMiniMap::CreateTextures() { Destroy(); // Create terrain texture glGenTextures(1, &m_TerrainTexture); g_Renderer.BindTexture(0, m_TerrainTexture); // Initialise texture with solid black, for the areas we don't // overwrite with glTexSubImage2D later u32* texData = new u32[m_TextureSize * m_TextureSize]; for (ssize_t i = 0; i < m_TextureSize * m_TextureSize; ++i) texData[i] = 0xFF000000; glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_TextureSize, m_TextureSize, 0, GL_RGBA, GL_UNSIGNED_BYTE, texData); delete[] texData; m_TerrainData = new u32[(m_MapSize - 1) * (m_MapSize - 1)]; glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // Rebuild and upload both of them RebuildTerrainTexture(); } void CMiniMap::RebuildTerrainTexture() { u32 x = 0; u32 y = 0; u32 w = m_MapSize - 1; u32 h = m_MapSize - 1; m_WaterHeight = g_Renderer.GetWaterManager()->m_WaterHeight; m_TerrainDirty = false; for (u32 j = 0; j < h; ++j) { u32* dataPtr = m_TerrainData + ((y + j) * (m_MapSize - 1)) + x; for (u32 i = 0; i < w; ++i) { float avgHeight = ( m_Terrain->GetVertexGroundLevel((int)i, (int)j) + m_Terrain->GetVertexGroundLevel((int)i+1, (int)j) + m_Terrain->GetVertexGroundLevel((int)i, (int)j+1) + m_Terrain->GetVertexGroundLevel((int)i+1, (int)j+1) ) / 4.0f; if (avgHeight < m_WaterHeight && avgHeight > m_WaterHeight - m_ShallowPassageHeight) { // shallow water *dataPtr++ = 0xffc09870; } else if (avgHeight < m_WaterHeight) { // Set water as constant color for consistency on different maps *dataPtr++ = 0xffa07850; } else { int hmap = ((int)m_Terrain->GetHeightMap()[(y + j) * m_MapSize + x + i]) >> 8; int val = (hmap / 3) + 170; u32 color = 0xFFFFFFFF; CMiniPatch* mp = m_Terrain->GetTile(x + i, y + j); if (mp) { CTerrainTextureEntry* tex = mp->GetTextureEntry(); if (tex) { // If the texture can't be loaded yet, set the dirty flags // so we'll try regenerating the terrain texture again soon if(!tex->GetTexture()->TryLoad()) m_TerrainDirty = true; color = tex->GetBaseColor(); } } *dataPtr++ = ScaleColor(color, float(val) / 255.0f); } } } // Upload the texture g_Renderer.BindTexture(0, m_TerrainTexture); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m_MapSize - 1, m_MapSize - 1, GL_RGBA, GL_UNSIGNED_BYTE, m_TerrainData); } void CMiniMap::Destroy() { if (m_TerrainTexture) { glDeleteTextures(1, &m_TerrainTexture); m_TerrainTexture = 0; } SAFE_ARRAY_DELETE(m_TerrainData); } Index: ps/trunk/source/simulation2/components/CCmpRangeManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpRangeManager.cpp (revision 23768) +++ ps/trunk/source/simulation2/components/CCmpRangeManager.cpp (revision 23769) @@ -1,2465 +1,2461 @@ /* Copyright (C) 2020 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 "simulation2/system/Component.h" #include "ICmpRangeManager.h" #include "ICmpTerrain.h" #include "simulation2/system/EntityMap.h" #include "simulation2/MessageTypes.h" #include "simulation2/components/ICmpFogging.h" #include "simulation2/components/ICmpMirage.h" #include "simulation2/components/ICmpOwnership.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpObstructionManager.h" #include "simulation2/components/ICmpTerritoryManager.h" #include "simulation2/components/ICmpVisibility.h" #include "simulation2/components/ICmpVision.h" #include "simulation2/components/ICmpWaterManager.h" #include "simulation2/helpers/MapEdgeTiles.h" #include "simulation2/helpers/Render.h" #include "simulation2/helpers/Spatial.h" #include "graphics/Overlay.h" #include "graphics/Terrain.h" #include "lib/timer.h" #include "ps/CLogger.h" #include "ps/Profile.h" #include "renderer/Scene.h" #define LOS_TILES_RATIO 8 #define DEBUG_RANGE_MANAGER_BOUNDS 0 -/** - * Representation of a range query. - */ -struct Query -{ - bool enabled; - bool parabolic; - CEntityHandle source; // TODO: this could crash if an entity is destroyed while a Query is still referencing it - entity_pos_t minRange; - entity_pos_t maxRange; - entity_pos_t elevationBonus; - u32 ownersMask; - i32 interface; - std::vector lastMatch; - u8 flagsMask; -}; /** * Convert an owner ID (-1 = unowned, 0 = gaia, 1..30 = players) * into a 32-bit mask for quick set-membership tests. */ static inline u32 CalcOwnerMask(player_id_t owner) { if (owner >= -1 && owner < 31) return 1 << (1+owner); else return 0; // owner was invalid } /** * Returns LOS mask for given player. */ static inline u32 CalcPlayerLosMask(player_id_t player) { if (player > 0 && player <= 16) - return ICmpRangeManager::LOS_MASK << (2*(player-1)); + return (u32)LosState::MASK << (2*(player-1)); return 0; } /** * Returns shared LOS mask for given list of players. */ static u32 CalcSharedLosMask(std::vector players) { u32 playerMask = 0; for (size_t i = 0; i < players.size(); i++) playerMask |= CalcPlayerLosMask(players[i]); return playerMask; } /** * Add/remove a player to/from mask, which is a 1-bit mask representing a list of players. * Returns true if the mask is modified. */ static bool SetPlayerSharedDirtyVisibilityBit(u16& mask, player_id_t player, bool enable) { if (player <= 0 || player > 16) return false; u16 oldMask = mask; if (enable) mask |= (0x1 << (player - 1)); else mask &= ~(0x1 << (player - 1)); return oldMask != mask; } /** * Computes the 2-bit visibility for one player, given the total 32-bit visibilities */ -static inline u8 GetPlayerVisibility(u32 visibilities, player_id_t player) +static inline LosVisibility GetPlayerVisibility(u32 visibilities, player_id_t player) { if (player > 0 && player <= 16) - return (visibilities >> (2 *(player-1))) & 0x3; - return 0; + return static_cast( (visibilities >> (2 *(player-1))) & 0x3 ); + return LosVisibility::HIDDEN; } /** * Test whether the visibility is dirty for a given LoS tile and a given player */ static inline bool IsVisibilityDirty(u16 dirty, player_id_t player) { if (player > 0 && player <= 16) return (dirty >> (player - 1)) & 0x1; return false; } /** * Test whether a player share this vision */ static inline bool HasVisionSharing(u16 visionSharing, player_id_t player) { return visionSharing & 1 << (player-1); } /** * Computes the shared vision mask for the player */ static inline u16 CalcVisionSharingMask(player_id_t player) { return 1 << (player-1); } /** + * Representation of a range query. + */ +struct Query +{ + bool enabled; + bool parabolic; + CEntityHandle source; // TODO: this could crash if an entity is destroyed while a Query is still referencing it + entity_pos_t minRange; + entity_pos_t maxRange; + entity_pos_t elevationBonus; + u32 ownersMask; + i32 interface; + std::vector lastMatch; + u8 flagsMask; +}; + +/** * Checks whether v is in a parabolic range of (0,0,0) * The highest point of the paraboloid is (0,range/2,0) * and the circle of distance 'range' around (0,0,0) on height y=0 is part of the paraboloid * * Avoids sqrting and overflowing. */ static bool InParabolicRange(CFixedVector3D v, fixed range) { u64 xx = SQUARE_U64_FIXED(v.X); // xx <= 2^62 u64 zz = SQUARE_U64_FIXED(v.Z); i64 d2 = (xx + zz) >> 1; // d2 <= 2^62 (no overflow) i32 y = v.Y.GetInternalValue(); i32 c = range.GetInternalValue(); i32 c_2 = c >> 1; i64 c2 = MUL_I64_I32_I32(c_2 - y, c); if (d2 <= c2) return true; return false; } struct EntityParabolicRangeOutline { entity_id_t source; CFixedVector3D position; entity_pos_t range; std::vector outline; }; static std::map ParabolicRangesOutlines; /** * Representation of an entity, with the data needed for queries. */ enum FlagMasks { // flags used for queries None = 0x00, Normal = 0x01, Injured = 0x02, AllQuery = Normal | Injured, // 0x04 reserved for future use // general flags InWorld = 0x08, RetainInFog = 0x10, RevealShore = 0x20, ScriptedVisibility = 0x40, SharedVision = 0x80 }; struct EntityData { EntityData() : visibilities(0), size(0), visionSharing(0), owner(-1), flags(FlagMasks::Normal) { } entity_pos_t x, z; entity_pos_t visionRange; u32 visibilities; // 2-bit visibility, per player u32 size; u16 visionSharing; // 1-bit per player i8 owner; u8 flags; // See the FlagMasks enum template inline bool HasFlag() const { return (flags & mask) != 0; } template inline void SetFlag(bool val) { flags = val ? (flags | mask) : (flags & ~mask); } inline void SetFlag(u8 mask, bool val) { flags = val ? (flags | mask) : (flags & ~mask); } }; cassert(sizeof(EntityData) == 24); /** * Serialization helper template for Query */ struct SerializeQuery { template void Common(S& serialize, const char* UNUSED(name), Query& value) { serialize.Bool("enabled", value.enabled); serialize.Bool("parabolic",value.parabolic); serialize.NumberFixed_Unbounded("min range", value.minRange); serialize.NumberFixed_Unbounded("max range", value.maxRange); serialize.NumberFixed_Unbounded("elevation bonus", value.elevationBonus); serialize.NumberU32_Unbounded("owners mask", value.ownersMask); serialize.NumberI32_Unbounded("interface", value.interface); SerializeVector()(serialize, "last match", value.lastMatch); serialize.NumberU8_Unbounded("flagsMask", value.flagsMask); } void operator()(ISerializer& serialize, const char* name, Query& value, const CSimContext& UNUSED(context)) { Common(serialize, name, value); uint32_t id = value.source.GetId(); serialize.NumberU32_Unbounded("source", id); } void operator()(IDeserializer& deserialize, const char* name, Query& value, const CSimContext& context) { Common(deserialize, name, value); uint32_t id; deserialize.NumberU32_Unbounded("source", id); value.source = context.GetComponentManager().LookupEntityHandle(id, true); // the referenced entity might not have been deserialized yet, // so tell LookupEntityHandle to allocate the handle if necessary } }; /** * Serialization helper template for EntityData */ struct SerializeEntityData { template void operator()(S& serialize, const char* UNUSED(name), EntityData& value) { serialize.NumberFixed_Unbounded("x", value.x); serialize.NumberFixed_Unbounded("z", value.z); serialize.NumberFixed_Unbounded("vision", value.visionRange); serialize.NumberU32_Unbounded("visibilities", value.visibilities); serialize.NumberU32_Unbounded("size", value.size); serialize.NumberU16_Unbounded("vision sharing", value.visionSharing); serialize.NumberI8_Unbounded("owner", value.owner); serialize.NumberU8_Unbounded("flags", value.flags); } }; /** * Functor for sorting entities by distance from a source point. * It must only be passed entities that are in 'entities' * and are currently in the world. */ struct EntityDistanceOrdering { EntityDistanceOrdering(const EntityMap& entities, const CFixedVector2D& source) : m_EntityData(entities), m_Source(source) { } bool operator()(entity_id_t a, entity_id_t b) const { const EntityData& da = m_EntityData.find(a)->second; const EntityData& db = m_EntityData.find(b)->second; CFixedVector2D vecA = CFixedVector2D(da.x, da.z) - m_Source; CFixedVector2D vecB = CFixedVector2D(db.x, db.z) - m_Source; return (vecA.CompareLength(vecB) < 0); } const EntityMap& m_EntityData; CFixedVector2D m_Source; private: EntityDistanceOrdering& operator=(const EntityDistanceOrdering&); }; /** * Range manager implementation. * Maintains a list of all entities (and their positions and owners), which is used for * queries. * * LOS implementation is based on the model described in GPG2. * (TODO: would be nice to make it cleverer, so e.g. mountains and walls * can block vision) */ class CCmpRangeManager : public ICmpRangeManager { public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeGloballyToMessageType(MT_Create); componentManager.SubscribeGloballyToMessageType(MT_PositionChanged); componentManager.SubscribeGloballyToMessageType(MT_OwnershipChanged); componentManager.SubscribeGloballyToMessageType(MT_Destroy); componentManager.SubscribeGloballyToMessageType(MT_VisionRangeChanged); componentManager.SubscribeGloballyToMessageType(MT_VisionSharingChanged); componentManager.SubscribeToMessageType(MT_Deserialized); componentManager.SubscribeToMessageType(MT_Update); componentManager.SubscribeToMessageType(MT_RenderSubmit); // for debug overlays } DEFAULT_COMPONENT_ALLOCATOR(RangeManager) bool m_DebugOverlayEnabled; bool m_DebugOverlayDirty; std::vector m_DebugOverlayLines; // Deserialization flag. A lot of different functions are called by Deserialize() // and we don't want to pass isDeserializing bool arguments to all of them... bool m_Deserializing; // World bounds (entities are expected to be within this range) entity_pos_t m_WorldX0; entity_pos_t m_WorldZ0; entity_pos_t m_WorldX1; entity_pos_t m_WorldZ1; // Range query state: tag_t m_QueryNext; // next allocated id std::map m_Queries; EntityMap m_EntityData; FastSpatialSubdivision m_Subdivision; // spatial index of m_EntityData std::vector m_SubdivisionResults; // LOS state: static const player_id_t MAX_LOS_PLAYER_ID = 16; - std::vector m_LosRevealAll; + using LosTile = std::pair; + + std::array m_LosRevealAll; bool m_LosCircular; i32 m_TerrainVerticesPerSide; // Cache for visibility tracking i32 m_LosTilesPerSide; bool m_GlobalVisibilityUpdate; - std::vector m_GlobalPlayerVisibilityUpdate; - std::vector m_DirtyVisibility; - std::vector > m_LosTiles; + std::array m_GlobalPlayerVisibilityUpdate; + Grid m_DirtyVisibility; + Grid> m_LosTiles; // List of entities that must be updated, regardless of the status of their tile std::vector m_ModifiedEntities; // Counts of units seeing vertex, per vertex, per player (starting with player 0). // Use u16 to avoid overflows when we have very large (but not infeasibly large) numbers // of units in a very small area. // (Note we use vertexes, not tiles, to better match the renderer.) // Lazily constructed when it's needed, to save memory in smaller games. - std::vector > m_LosPlayerCounts; + std::array, MAX_LOS_PLAYER_ID> m_LosPlayerCounts; - // 2-bit ELosState per player, starting with player 1 (not 0!) up to player MAX_LOS_PLAYER_ID (inclusive) - std::vector m_LosState; + // 2-bit LosState per player, starting with player 1 (not 0!) up to player MAX_LOS_PLAYER_ID (inclusive) + Grid m_LosState; // Special static visibility data for the "reveal whole map" mode // (TODO: this is usually a waste of memory) - std::vector m_LosStateRevealed; + Grid m_LosStateRevealed; // Shared LOS masks, one per player. - std::vector m_SharedLosMasks; + std::array m_SharedLosMasks; // Shared dirty visibility masks, one per player. - std::vector m_SharedDirtyVisibilityMasks; + std::array m_SharedDirtyVisibilityMasks; // Cache explored vertices per player (not serialized) u32 m_TotalInworldVertices; std::vector m_ExploredVertices; static std::string GetSchema() { return ""; } virtual void Init(const CParamNode& UNUSED(paramNode)) { m_QueryNext = 1; m_DebugOverlayEnabled = false; m_DebugOverlayDirty = true; m_Deserializing = false; m_WorldX0 = m_WorldZ0 = m_WorldX1 = m_WorldZ1 = entity_pos_t::Zero(); // Initialise with bogus values (these will get replaced when // SetBounds is called) ResetSubdivisions(entity_pos_t::FromInt(1024), entity_pos_t::FromInt(1024)); m_SubdivisionResults.reserve(4096); // The whole map should be visible to Gaia by default, else e.g. animals // will get confused when trying to run from enemies - m_LosRevealAll.resize(MAX_LOS_PLAYER_ID+2,false); m_LosRevealAll[0] = true; - m_SharedLosMasks.resize(MAX_LOS_PLAYER_ID+2,0); - m_SharedDirtyVisibilityMasks.resize(MAX_LOS_PLAYER_ID + 2, 0); m_GlobalVisibilityUpdate = true; - m_GlobalPlayerVisibilityUpdate.resize(MAX_LOS_PLAYER_ID); m_LosCircular = false; m_TerrainVerticesPerSide = 0; } virtual void Deinit() { } template void SerializeCommon(S& serialize) { serialize.NumberFixed_Unbounded("world x0", m_WorldX0); serialize.NumberFixed_Unbounded("world z0", m_WorldZ0); serialize.NumberFixed_Unbounded("world x1", m_WorldX1); serialize.NumberFixed_Unbounded("world z1", m_WorldZ1); serialize.NumberU32_Unbounded("query next", m_QueryNext); SerializeMap()(serialize, "queries", m_Queries, GetSimContext()); SerializeEntityMap()(serialize, "entity data", m_EntityData); - SerializeVector()(serialize, "los reveal all", m_LosRevealAll); + SerializeArray()(serialize, "los reveal all", m_LosRevealAll); serialize.Bool("los circular", m_LosCircular); serialize.NumberI32_Unbounded("terrain verts per side", m_TerrainVerticesPerSide); serialize.Bool("global visibility update", m_GlobalVisibilityUpdate); - SerializeVector()(serialize, "global player visibility update", m_GlobalPlayerVisibilityUpdate); - SerializeRepetitiveVector()(serialize, "dirty visibility", m_DirtyVisibility); + SerializeArray()(serialize, "global player visibility update", m_GlobalPlayerVisibilityUpdate); + SerializedGridCompressed()(serialize, "dirty visibility", m_DirtyVisibility); SerializeVector()(serialize, "modified entities", m_ModifiedEntities); // We don't serialize m_Subdivision, m_LosPlayerCounts or m_LosTiles // since they can be recomputed from the entity data when deserializing; // m_LosState must be serialized since it depends on the history of exploration - SerializeRepetitiveVector()(serialize, "los state", m_LosState); - SerializeVector()(serialize, "shared los masks", m_SharedLosMasks); - SerializeVector()(serialize, "shared dirty visibility masks", m_SharedDirtyVisibilityMasks); + SerializedGridCompressed()(serialize, "los state", m_LosState); + SerializeArray()(serialize, "shared los masks", m_SharedLosMasks); + SerializeArray()(serialize, "shared dirty visibility masks", m_SharedDirtyVisibilityMasks); } virtual void Serialize(ISerializer& serialize) { SerializeCommon(serialize); } virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) { Init(paramNode); SerializeCommon(deserialize); } virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_Deserialized: { // Reinitialize subdivisions and LOS data after all // other components have been deserialized. m_Deserializing = true; ResetDerivedData(); m_Deserializing = false; break; } case MT_Create: { const CMessageCreate& msgData = static_cast (msg); entity_id_t ent = msgData.entity; // Ignore local entities - we shouldn't let them influence anything if (ENTITY_IS_LOCAL(ent)) break; // Ignore non-positional entities CmpPtr cmpPosition(GetSimContext(), ent); if (!cmpPosition) break; // The newly-created entity will have owner -1 and position out-of-world // (any initialisation of those values will happen later), so we can just // use the default-constructed EntityData here EntityData entdata; // Store the LOS data, if any CmpPtr cmpVision(GetSimContext(), ent); if (cmpVision) { entdata.visionRange = cmpVision->GetRange(); entdata.SetFlag(cmpVision->GetRevealShore()); } CmpPtr cmpVisibility(GetSimContext(), ent); if (cmpVisibility) entdata.SetFlag(cmpVisibility->GetRetainInFog()); // Store the size CmpPtr cmpObstruction(GetSimContext(), ent); if (cmpObstruction) entdata.size = cmpObstruction->GetSize().ToInt_RoundToInfinity(); // Remember this entity m_EntityData.insert(ent, entdata); break; } case MT_PositionChanged: { const CMessagePositionChanged& msgData = static_cast (msg); entity_id_t ent = msgData.entity; EntityMap::iterator it = m_EntityData.find(ent); // Ignore if we're not already tracking this entity if (it == m_EntityData.end()) break; if (msgData.inWorld) { if (it->second.HasFlag()) { CFixedVector2D from(it->second.x, it->second.z); CFixedVector2D to(msgData.x, msgData.z); m_Subdivision.Move(ent, from, to, it->second.size); if (it->second.HasFlag()) SharingLosMove(it->second.visionSharing, it->second.visionRange, from, to); else LosMove(it->second.owner, it->second.visionRange, from, to); - i32 oldLosTile = PosToLosTilesHelper(it->second.x, it->second.z); - i32 newLosTile = PosToLosTilesHelper(msgData.x, msgData.z); + LosTile oldLosTile = PosToLosTilesHelper(it->second.x, it->second.z); + LosTile newLosTile = PosToLosTilesHelper(msgData.x, msgData.z); if (oldLosTile != newLosTile) { RemoveFromTile(oldLosTile, ent); AddToTile(newLosTile, ent); } } else { CFixedVector2D to(msgData.x, msgData.z); m_Subdivision.Add(ent, to, it->second.size); if (it->second.HasFlag()) SharingLosAdd(it->second.visionSharing, it->second.visionRange, to); else LosAdd(it->second.owner, it->second.visionRange, to); AddToTile(PosToLosTilesHelper(msgData.x, msgData.z), ent); } it->second.SetFlag(true); it->second.x = msgData.x; it->second.z = msgData.z; } else { if (it->second.HasFlag()) { CFixedVector2D from(it->second.x, it->second.z); m_Subdivision.Remove(ent, from, it->second.size); if (it->second.HasFlag()) SharingLosRemove(it->second.visionSharing, it->second.visionRange, from); else LosRemove(it->second.owner, it->second.visionRange, from); RemoveFromTile(PosToLosTilesHelper(it->second.x, it->second.z), ent); } it->second.SetFlag(false); it->second.x = entity_pos_t::Zero(); it->second.z = entity_pos_t::Zero(); } RequestVisibilityUpdate(ent); break; } case MT_OwnershipChanged: { const CMessageOwnershipChanged& msgData = static_cast (msg); entity_id_t ent = msgData.entity; EntityMap::iterator it = m_EntityData.find(ent); // Ignore if we're not already tracking this entity if (it == m_EntityData.end()) break; if (it->second.HasFlag()) { // Entity vision is taken into account in VisionSharingChanged // when sharing component activated if (!it->second.HasFlag()) { CFixedVector2D pos(it->second.x, it->second.z); LosRemove(it->second.owner, it->second.visionRange, pos); LosAdd(msgData.to, it->second.visionRange, pos); } if (it->second.HasFlag()) { RevealShore(it->second.owner, false); RevealShore(msgData.to, true); } } ENSURE(-128 <= msgData.to && msgData.to <= 127); it->second.owner = (i8)msgData.to; break; } case MT_Destroy: { const CMessageDestroy& msgData = static_cast (msg); entity_id_t ent = msgData.entity; EntityMap::iterator it = m_EntityData.find(ent); // Ignore if we're not already tracking this entity if (it == m_EntityData.end()) break; if (it->second.HasFlag()) { m_Subdivision.Remove(ent, CFixedVector2D(it->second.x, it->second.z), it->second.size); RemoveFromTile(PosToLosTilesHelper(it->second.x, it->second.z), ent); } // This will be called after Ownership's OnDestroy, so ownership will be set // to -1 already and we don't have to do a LosRemove here ENSURE(it->second.owner == -1); m_EntityData.erase(it); break; } case MT_VisionRangeChanged: { const CMessageVisionRangeChanged& msgData = static_cast (msg); entity_id_t ent = msgData.entity; EntityMap::iterator it = m_EntityData.find(ent); // Ignore if we're not already tracking this entity if (it == m_EntityData.end()) break; CmpPtr cmpVision(GetSimContext(), ent); if (!cmpVision) break; entity_pos_t oldRange = it->second.visionRange; entity_pos_t newRange = msgData.newRange; // If the range changed and the entity's in-world, we need to manually adjust it // but if it's not in-world, we only need to set the new vision range it->second.visionRange = newRange; if (it->second.HasFlag()) { CFixedVector2D pos(it->second.x, it->second.z); if (it->second.HasFlag()) { SharingLosRemove(it->second.visionSharing, oldRange, pos); SharingLosAdd(it->second.visionSharing, newRange, pos); } else { LosRemove(it->second.owner, oldRange, pos); LosAdd(it->second.owner, newRange, pos); } } break; } case MT_VisionSharingChanged: { const CMessageVisionSharingChanged& msgData = static_cast (msg); entity_id_t ent = msgData.entity; EntityMap::iterator it = m_EntityData.find(ent); // Ignore if we're not already tracking this entity if (it == m_EntityData.end()) break; ENSURE(msgData.player > 0 && msgData.player < MAX_LOS_PLAYER_ID+1); u16 visionChanged = CalcVisionSharingMask(msgData.player); if (!it->second.HasFlag()) { // Activation of the Vision Sharing ENSURE(it->second.owner == (i8)msgData.player); it->second.visionSharing = visionChanged; it->second.SetFlag(true); break; } if (it->second.HasFlag()) { entity_pos_t range = it->second.visionRange; CFixedVector2D pos(it->second.x, it->second.z); if (msgData.add) LosAdd(msgData.player, range, pos); else LosRemove(msgData.player, range, pos); } if (msgData.add) it->second.visionSharing |= visionChanged; else it->second.visionSharing &= ~visionChanged; break; } case MT_Update: { m_DebugOverlayDirty = true; ExecuteActiveQueries(); UpdateVisibilityData(); break; } case MT_RenderSubmit: { const CMessageRenderSubmit& msgData = static_cast (msg); RenderSubmit(msgData.collector); break; } } } virtual void SetBounds(entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, ssize_t vertices) { m_WorldX0 = x0; m_WorldZ0 = z0; m_WorldX1 = x1; m_WorldZ1 = z1; m_TerrainVerticesPerSide = (i32)vertices; ResetDerivedData(); } virtual void Verify() { // Ignore if map not initialised yet if (m_WorldX1.IsZero()) return; // Check that calling ResetDerivedData (i.e. recomputing all the state from scratch) // does not affect the incrementally-computed state - std::vector > oldPlayerCounts = m_LosPlayerCounts; - std::vector oldStateRevealed = m_LosStateRevealed; + std::array, MAX_LOS_PLAYER_ID> oldPlayerCounts = m_LosPlayerCounts; + Grid oldStateRevealed = m_LosStateRevealed; FastSpatialSubdivision oldSubdivision = m_Subdivision; - std::vector > oldLosTiles = m_LosTiles; + Grid > oldLosTiles = m_LosTiles; m_Deserializing = true; ResetDerivedData(); m_Deserializing = false; if (oldPlayerCounts != m_LosPlayerCounts) { - for (size_t i = 0; i < oldPlayerCounts.size(); ++i) + for (size_t id = 0; id < m_LosPlayerCounts.size(); ++id) { - debug_printf("%d: ", (int)i); - for (size_t j = 0; j < oldPlayerCounts[i].size(); ++j) - debug_printf("%d ", oldPlayerCounts[i][j]); - debug_printf("\n"); - } - for (size_t i = 0; i < m_LosPlayerCounts.size(); ++i) - { - debug_printf("%d: ", (int)i); - for (size_t j = 0; j < m_LosPlayerCounts[i].size(); ++j) - debug_printf("%d ", m_LosPlayerCounts[i][j]); - debug_printf("\n"); + debug_printf("player %li\n", id); + for (size_t i = 0; i < oldPlayerCounts[id].width(); ++i) + { + for (size_t j = 0; j < oldPlayerCounts[id].height(); ++j) + debug_printf("%i ", oldPlayerCounts[id].get(i,j)); + debug_printf("\n"); + } + } + for (size_t id = 0; id < m_LosPlayerCounts.size(); ++id) + { + debug_printf("player %li\n", id); + for (size_t i = 0; i < m_LosPlayerCounts[id].width(); ++i) + { + for (size_t j = 0; j < m_LosPlayerCounts[id].height(); ++j) + debug_printf("%i ", m_LosPlayerCounts[id].get(i,j)); + debug_printf("\n"); + } } debug_warn(L"inconsistent player counts"); } if (oldStateRevealed != m_LosStateRevealed) debug_warn(L"inconsistent revealed"); if (oldSubdivision != m_Subdivision) debug_warn(L"inconsistent subdivs"); if (oldLosTiles != m_LosTiles) debug_warn(L"inconsistent los tiles"); } FastSpatialSubdivision* GetSubdivision() { return &m_Subdivision; } // Reinitialise subdivisions and LOS data, based on entity data void ResetDerivedData() { ENSURE(m_WorldX0.IsZero() && m_WorldZ0.IsZero()); // don't bother implementing non-zero offsets yet ResetSubdivisions(m_WorldX1, m_WorldZ1); m_LosTilesPerSide = (m_TerrainVerticesPerSide - 1)/LOS_TILES_RATIO; - m_LosPlayerCounts.clear(); - m_LosPlayerCounts.resize(MAX_LOS_PLAYER_ID+1); + for (size_t player_id = 0; player_id < m_LosPlayerCounts.size(); ++player_id) + m_LosPlayerCounts[player_id].reset(); + m_ExploredVertices.clear(); m_ExploredVertices.resize(MAX_LOS_PLAYER_ID+1, 0); + if (m_Deserializing) { // recalc current exploration stats. for (i32 j = 0; j < m_TerrainVerticesPerSide; j++) for (i32 i = 0; i < m_TerrainVerticesPerSide; i++) if (!LosIsOffWorld(i, j)) for (u8 k = 1; k < MAX_LOS_PLAYER_ID+1; ++k) - m_ExploredVertices.at(k) += ((m_LosState[j*m_TerrainVerticesPerSide + i] & (LOS_EXPLORED << (2*(k-1)))) > 0); - } - else - { - m_LosState.clear(); - m_LosState.resize(m_TerrainVerticesPerSide*m_TerrainVerticesPerSide); - } - m_LosStateRevealed.clear(); - m_LosStateRevealed.resize(m_TerrainVerticesPerSide*m_TerrainVerticesPerSide); + m_ExploredVertices.at(k) += ((m_LosState.get(i, j) & ((u32)LosState::EXPLORED << (2*(k-1)))) > 0); + } else + m_LosState.resize(m_TerrainVerticesPerSide, m_TerrainVerticesPerSide); + + m_LosStateRevealed.resize(m_TerrainVerticesPerSide, m_TerrainVerticesPerSide); if (!m_Deserializing) { - m_DirtyVisibility.clear(); - m_DirtyVisibility.resize(m_LosTilesPerSide*m_LosTilesPerSide); + m_DirtyVisibility.resize(m_LosTilesPerSide, m_LosTilesPerSide); } - ENSURE(m_DirtyVisibility.size() == (size_t)(m_LosTilesPerSide*m_LosTilesPerSide)); + ENSURE(m_DirtyVisibility.width() == m_LosTilesPerSide); + ENSURE(m_DirtyVisibility.height() == m_LosTilesPerSide); - m_LosTiles.clear(); - m_LosTiles.resize(m_LosTilesPerSide*m_LosTilesPerSide); + m_LosTiles.resize(m_LosTilesPerSide, m_LosTilesPerSide); for (EntityMap::const_iterator it = m_EntityData.begin(); it != m_EntityData.end(); ++it) if (it->second.HasFlag()) { if (it->second.HasFlag()) SharingLosAdd(it->second.visionSharing, it->second.visionRange, CFixedVector2D(it->second.x, it->second.z)); else LosAdd(it->second.owner, it->second.visionRange, CFixedVector2D(it->second.x, it->second.z)); AddToTile(PosToLosTilesHelper(it->second.x, it->second.z), it->first); if (it->second.HasFlag()) RevealShore(it->second.owner, true); } m_TotalInworldVertices = 0; for (ssize_t j = 0; j < m_TerrainVerticesPerSide; ++j) for (ssize_t i = 0; i < m_TerrainVerticesPerSide; ++i) { if (LosIsOffWorld(i,j)) - m_LosStateRevealed[i + j*m_TerrainVerticesPerSide] = 0; + m_LosStateRevealed.get(i, j) = 0; else { - m_LosStateRevealed[i + j*m_TerrainVerticesPerSide] = 0xFFFFFFFFu; + m_LosStateRevealed.get(i, j) = 0xFFFFFFFFu; m_TotalInworldVertices++; } } } void ResetSubdivisions(entity_pos_t x1, entity_pos_t z1) { m_Subdivision.Reset(x1, z1); for (EntityMap::const_iterator it = m_EntityData.begin(); it != m_EntityData.end(); ++it) if (it->second.HasFlag()) m_Subdivision.Add(it->first, CFixedVector2D(it->second.x, it->second.z), it->second.size); } virtual tag_t CreateActiveQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, const std::vector& owners, int requiredInterface, u8 flags) { tag_t id = m_QueryNext++; m_Queries[id] = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, flags); return id; } virtual tag_t CreateActiveParabolicQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t elevationBonus, const std::vector& owners, int requiredInterface, u8 flags) { tag_t id = m_QueryNext++; m_Queries[id] = ConstructParabolicQuery(source, minRange, maxRange, elevationBonus, owners, requiredInterface, flags); return id; } virtual void DestroyActiveQuery(tag_t tag) { if (m_Queries.find(tag) == m_Queries.end()) { LOGERROR("CCmpRangeManager: DestroyActiveQuery called with invalid tag %u", tag); return; } m_Queries.erase(tag); } virtual void EnableActiveQuery(tag_t tag) { std::map::iterator it = m_Queries.find(tag); if (it == m_Queries.end()) { LOGERROR("CCmpRangeManager: EnableActiveQuery called with invalid tag %u", tag); return; } Query& q = it->second; q.enabled = true; } virtual void DisableActiveQuery(tag_t tag) { std::map::iterator it = m_Queries.find(tag); if (it == m_Queries.end()) { LOGERROR("CCmpRangeManager: DisableActiveQuery called with invalid tag %u", tag); return; } Query& q = it->second; q.enabled = false; } virtual bool IsActiveQueryEnabled(tag_t tag) const { std::map::const_iterator it = m_Queries.find(tag); if (it == m_Queries.end()) { LOGERROR("CCmpRangeManager: IsActiveQueryEnabled called with invalid tag %u", tag); return false; } const Query& q = it->second; return q.enabled; } virtual std::vector ExecuteQueryAroundPos(const CFixedVector2D& pos, entity_pos_t minRange, entity_pos_t maxRange, const std::vector& owners, int requiredInterface) { Query q = ConstructQuery(INVALID_ENTITY, minRange, maxRange, owners, requiredInterface, GetEntityFlagMask("normal")); std::vector r; PerformQuery(q, r, pos); // Return the list sorted by distance from the entity std::stable_sort(r.begin(), r.end(), EntityDistanceOrdering(m_EntityData, pos)); return r; } virtual std::vector ExecuteQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, const std::vector& owners, int requiredInterface) { PROFILE("ExecuteQuery"); Query q = ConstructQuery(source, minRange, maxRange, owners, requiredInterface, GetEntityFlagMask("normal")); std::vector r; CmpPtr cmpSourcePosition(q.source); if (!cmpSourcePosition || !cmpSourcePosition->IsInWorld()) { // If the source doesn't have a position, then the result is just the empty list return r; } CFixedVector2D pos = cmpSourcePosition->GetPosition2D(); PerformQuery(q, r, pos); // Return the list sorted by distance from the entity std::stable_sort(r.begin(), r.end(), EntityDistanceOrdering(m_EntityData, pos)); return r; } virtual std::vector ResetActiveQuery(tag_t tag) { PROFILE("ResetActiveQuery"); std::vector r; std::map::iterator it = m_Queries.find(tag); if (it == m_Queries.end()) { LOGERROR("CCmpRangeManager: ResetActiveQuery called with invalid tag %u", tag); return r; } Query& q = it->second; q.enabled = true; CmpPtr cmpSourcePosition(q.source); if (!cmpSourcePosition || !cmpSourcePosition->IsInWorld()) { // If the source doesn't have a position, then the result is just the empty list q.lastMatch = r; return r; } CFixedVector2D pos = cmpSourcePosition->GetPosition2D(); PerformQuery(q, r, pos); q.lastMatch = r; // Return the list sorted by distance from the entity std::stable_sort(r.begin(), r.end(), EntityDistanceOrdering(m_EntityData, pos)); return r; } virtual std::vector GetEntitiesByPlayer(player_id_t player) const { return GetEntitiesByMask(CalcOwnerMask(player)); } virtual std::vector GetNonGaiaEntities() const { return GetEntitiesByMask(~3); // bit 0 for owner=-1 and bit 1 for gaia } virtual std::vector GetGaiaAndNonGaiaEntities() const { return GetEntitiesByMask(~1); // bit 0 for owner=-1 } std::vector GetEntitiesByMask(u32 ownerMask) const { std::vector entities; for (EntityMap::const_iterator it = m_EntityData.begin(); it != m_EntityData.end(); ++it) { // Check owner and add to list if it matches if (CalcOwnerMask(it->second.owner) & ownerMask) entities.push_back(it->first); } return entities; } virtual void SetDebugOverlay(bool enabled) { m_DebugOverlayEnabled = enabled; m_DebugOverlayDirty = true; if (!enabled) m_DebugOverlayLines.clear(); } /** * Update all currently-enabled active queries. */ void ExecuteActiveQueries() { PROFILE3("ExecuteActiveQueries"); // Store a queue of all messages before sending any, so we can assume // no entities will move until we've finished checking all the ranges std::vector > messages; std::vector results; std::vector added; std::vector removed; for (std::map::iterator it = m_Queries.begin(); it != m_Queries.end(); ++it) { Query& query = it->second; if (!query.enabled) continue; results.clear(); CmpPtr cmpSourcePosition(query.source); if (cmpSourcePosition && cmpSourcePosition->IsInWorld()) { results.reserve(query.lastMatch.size()); PerformQuery(query, results, cmpSourcePosition->GetPosition2D()); } // Compute the changes vs the last match added.clear(); removed.clear(); // Return the 'added' list sorted by distance from the entity // (Don't bother sorting 'removed' because they might not even have positions or exist any more) std::set_difference(results.begin(), results.end(), query.lastMatch.begin(), query.lastMatch.end(), std::back_inserter(added)); std::set_difference(query.lastMatch.begin(), query.lastMatch.end(), results.begin(), results.end(), std::back_inserter(removed)); if (added.empty() && removed.empty()) continue; if (cmpSourcePosition && cmpSourcePosition->IsInWorld()) std::stable_sort(added.begin(), added.end(), EntityDistanceOrdering(m_EntityData, cmpSourcePosition->GetPosition2D())); messages.resize(messages.size() + 1); std::pair& back = messages.back(); back.first = query.source.GetId(); back.second.tag = it->first; back.second.added.swap(added); back.second.removed.swap(removed); query.lastMatch.swap(results); } CComponentManager& cmpMgr = GetSimContext().GetComponentManager(); for (size_t i = 0; i < messages.size(); ++i) cmpMgr.PostMessage(messages[i].first, messages[i].second); } /** * Returns whether the given entity matches the given query (ignoring maxRange) */ bool TestEntityQuery(const Query& q, entity_id_t id, const EntityData& entity) const { // Quick filter to ignore entities with the wrong owner if (!(CalcOwnerMask(entity.owner) & q.ownersMask)) return false; // Ignore entities not present in the world if (!entity.HasFlag()) return false; // Ignore entities that don't match the current flags if (!((entity.flags & FlagMasks::AllQuery) & q.flagsMask)) return false; // Ignore self if (id == q.source.GetId()) return false; // Ignore if it's missing the required interface if (q.interface && !GetSimContext().GetComponentManager().QueryInterface(id, q.interface)) return false; return true; } /** * Returns a list of distinct entity IDs that match the given query, sorted by ID. */ void PerformQuery(const Query& q, std::vector& r, CFixedVector2D pos) { // Special case: range -1.0 means check all entities ignoring distance if (q.maxRange == entity_pos_t::FromInt(-1)) { for (EntityMap::const_iterator it = m_EntityData.begin(); it != m_EntityData.end(); ++it) { if (!TestEntityQuery(q, it->first, it->second)) continue; r.push_back(it->first); } } // Not the entire world, so check a parabolic range, or a regular range else if (q.parabolic) { // elevationBonus is part of the 3D position, as the source is really that much heigher CmpPtr cmpSourcePosition(q.source); CFixedVector3D pos3d = cmpSourcePosition->GetPosition()+ CFixedVector3D(entity_pos_t::Zero(), q.elevationBonus, entity_pos_t::Zero()) ; // Get a quick list of entities that are potentially in range, with a cutoff of 2*maxRange m_SubdivisionResults.clear(); m_Subdivision.GetNear(m_SubdivisionResults, pos, q.maxRange * 2); for (size_t i = 0; i < m_SubdivisionResults.size(); ++i) { EntityMap::const_iterator it = m_EntityData.find(m_SubdivisionResults[i]); ENSURE(it != m_EntityData.end()); if (!TestEntityQuery(q, it->first, it->second)) continue; CmpPtr cmpSecondPosition(GetSimContext(), m_SubdivisionResults[i]); if (!cmpSecondPosition || !cmpSecondPosition->IsInWorld()) continue; CFixedVector3D secondPosition = cmpSecondPosition->GetPosition(); // Restrict based on precise distance if (!InParabolicRange( CFixedVector3D(it->second.x, secondPosition.Y, it->second.z) - pos3d, q.maxRange)) continue; if (!q.minRange.IsZero()) { int distVsMin = (CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.minRange); if (distVsMin < 0) continue; } r.push_back(it->first); } std::sort(r.begin(), r.end()); } // check a regular range (i.e. not the entire world, and not parabolic) else { // Get a quick list of entities that are potentially in range m_SubdivisionResults.clear(); m_Subdivision.GetNear(m_SubdivisionResults, pos, q.maxRange); for (size_t i = 0; i < m_SubdivisionResults.size(); ++i) { EntityMap::const_iterator it = m_EntityData.find(m_SubdivisionResults[i]); ENSURE(it != m_EntityData.end()); if (!TestEntityQuery(q, it->first, it->second)) continue; // Restrict based on precise distance int distVsMax = (CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.maxRange); if (distVsMax > 0) continue; if (!q.minRange.IsZero()) { int distVsMin = (CFixedVector2D(it->second.x, it->second.z) - pos).CompareLength(q.minRange); if (distVsMin < 0) continue; } r.push_back(it->first); } std::sort(r.begin(), r.end()); } } virtual entity_pos_t GetElevationAdaptedRange(const CFixedVector3D& pos1, const CFixedVector3D& rot, entity_pos_t range, entity_pos_t elevationBonus, entity_pos_t angle) const { entity_pos_t r = entity_pos_t::Zero(); CFixedVector3D pos(pos1); pos.Y += elevationBonus; entity_pos_t orientation = rot.Y; entity_pos_t maxAngle = orientation + angle/2; entity_pos_t minAngle = orientation - angle/2; int numberOfSteps = 16; if (angle == entity_pos_t::Zero()) numberOfSteps = 1; std::vector coords = getParabolicRangeForm(pos, range, range*2, minAngle, maxAngle, numberOfSteps); entity_pos_t part = entity_pos_t::FromInt(numberOfSteps); for (int i = 0; i < numberOfSteps; ++i) r = r + CFixedVector2D(coords[2*i],coords[2*i+1]).Length() / part; return r; } virtual std::vector getParabolicRangeForm(CFixedVector3D pos, entity_pos_t maxRange, entity_pos_t cutoff, entity_pos_t minAngle, entity_pos_t maxAngle, int numberOfSteps) const { std::vector r; CmpPtr cmpTerrain(GetSystemEntity()); if (!cmpTerrain) return r; // angle = 0 goes in the positive Z direction u64 precisionSquared = SQUARE_U64_FIXED(entity_pos_t::FromInt(static_cast(TERRAIN_TILE_SIZE)) / 8); CmpPtr cmpWaterManager(GetSystemEntity()); entity_pos_t waterLevel = cmpWaterManager ? cmpWaterManager->GetWaterLevel(pos.X, pos.Z) : entity_pos_t::Zero(); entity_pos_t thisHeight = pos.Y > waterLevel ? pos.Y : waterLevel; for (int i = 0; i < numberOfSteps; ++i) { entity_pos_t angle = minAngle + (maxAngle - minAngle) / numberOfSteps * i; entity_pos_t sin; entity_pos_t cos; entity_pos_t minDistance = entity_pos_t::Zero(); entity_pos_t maxDistance = cutoff; sincos_approx(angle, sin, cos); CFixedVector2D minVector = CFixedVector2D(entity_pos_t::Zero(), entity_pos_t::Zero()); CFixedVector2D maxVector = CFixedVector2D(sin, cos).Multiply(cutoff); entity_pos_t targetHeight = cmpTerrain->GetGroundLevel(pos.X+maxVector.X, pos.Z+maxVector.Y); // use water level to display range on water targetHeight = targetHeight > waterLevel ? targetHeight : waterLevel; if (InParabolicRange(CFixedVector3D(maxVector.X, targetHeight-thisHeight, maxVector.Y), maxRange)) { r.push_back(maxVector.X); r.push_back(maxVector.Y); continue; } // Loop until vectors come close enough while ((maxVector - minVector).CompareLengthSquared(precisionSquared) > 0) { // difference still bigger than precision, bisect to get smaller difference entity_pos_t newDistance = (minDistance+maxDistance)/entity_pos_t::FromInt(2); CFixedVector2D newVector = CFixedVector2D(sin, cos).Multiply(newDistance); // get the height of the ground targetHeight = cmpTerrain->GetGroundLevel(pos.X+newVector.X, pos.Z+newVector.Y); targetHeight = targetHeight > waterLevel ? targetHeight : waterLevel; if (InParabolicRange(CFixedVector3D(newVector.X, targetHeight-thisHeight, newVector.Y), maxRange)) { // new vector is in parabolic range, so this is a new minVector minVector = newVector; minDistance = newDistance; } else { // new vector is out parabolic range, so this is a new maxVector maxVector = newVector; maxDistance = newDistance; } } r.push_back(maxVector.X); r.push_back(maxVector.Y); } r.push_back(r[0]); r.push_back(r[1]); return r; } Query ConstructQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, const std::vector& owners, int requiredInterface, u8 flagsMask) const { // Min range must be non-negative if (minRange < entity_pos_t::Zero()) LOGWARNING("CCmpRangeManager: Invalid min range %f in query for entity %u", minRange.ToDouble(), source); // Max range must be non-negative, or else -1 if (maxRange < entity_pos_t::Zero() && maxRange != entity_pos_t::FromInt(-1)) LOGWARNING("CCmpRangeManager: Invalid max range %f in query for entity %u", maxRange.ToDouble(), source); Query q; q.enabled = false; q.parabolic = false; q.source = GetSimContext().GetComponentManager().LookupEntityHandle(source); q.minRange = minRange; q.maxRange = maxRange; q.elevationBonus = entity_pos_t::Zero(); q.ownersMask = 0; for (size_t i = 0; i < owners.size(); ++i) q.ownersMask |= CalcOwnerMask(owners[i]); if (q.ownersMask == 0) LOGWARNING("CCmpRangeManager: No owners in query for entity %u", source); q.interface = requiredInterface; q.flagsMask = flagsMask; return q; } Query ConstructParabolicQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t elevationBonus, const std::vector& owners, int requiredInterface, u8 flagsMask) const { Query q = ConstructQuery(source,minRange,maxRange,owners,requiredInterface,flagsMask); q.parabolic = true; q.elevationBonus = elevationBonus; return q; } void RenderSubmit(SceneCollector& collector) { if (!m_DebugOverlayEnabled) return; static CColor disabledRingColor(1, 0, 0, 1); // red static CColor enabledRingColor(0, 1, 0, 1); // green static CColor subdivColor(0, 0, 1, 1); // blue static CColor rayColor(1, 1, 0, 0.2f); if (m_DebugOverlayDirty) { m_DebugOverlayLines.clear(); for (std::map::iterator it = m_Queries.begin(); it != m_Queries.end(); ++it) { Query& q = it->second; CmpPtr cmpSourcePosition(q.source); if (!cmpSourcePosition || !cmpSourcePosition->IsInWorld()) continue; CFixedVector2D pos = cmpSourcePosition->GetPosition2D(); // Draw the max range circle if (!q.parabolic) { m_DebugOverlayLines.push_back(SOverlayLine()); m_DebugOverlayLines.back().m_Color = (q.enabled ? enabledRingColor : disabledRingColor); SimRender::ConstructCircleOnGround(GetSimContext(), pos.X.ToFloat(), pos.Y.ToFloat(), q.maxRange.ToFloat(), m_DebugOverlayLines.back(), true); } else { // elevation bonus is part of the 3D position. As if the unit is really that much higher CFixedVector3D pos = cmpSourcePosition->GetPosition(); pos.Y += q.elevationBonus; std::vector coords; // Get the outline from cache if possible if (ParabolicRangesOutlines.find(q.source.GetId()) != ParabolicRangesOutlines.end()) { EntityParabolicRangeOutline e = ParabolicRangesOutlines[q.source.GetId()]; if (e.position == pos && e.range == q.maxRange) { // outline is cached correctly, use it coords = e.outline; } else { // outline was cached, but important parameters changed // (position, elevation, range) // update it coords = getParabolicRangeForm(pos,q.maxRange,q.maxRange*2, entity_pos_t::Zero(), entity_pos_t::FromFloat(2.0f*3.14f),70); e.outline = coords; e.range = q.maxRange; e.position = pos; ParabolicRangesOutlines[q.source.GetId()] = e; } } else { // outline wasn't cached (first time you enable the range overlay // or you created a new entiy) // cache a new outline coords = getParabolicRangeForm(pos,q.maxRange,q.maxRange*2, entity_pos_t::Zero(), entity_pos_t::FromFloat(2.0f*3.14f),70); EntityParabolicRangeOutline e; e.source = q.source.GetId(); e.range = q.maxRange; e.position = pos; e.outline = coords; ParabolicRangesOutlines[q.source.GetId()] = e; } CColor thiscolor = q.enabled ? enabledRingColor : disabledRingColor; // draw the outline (piece by piece) for (size_t i = 3; i < coords.size(); i += 2) { std::vector c; c.push_back((coords[i-3]+pos.X).ToFloat()); c.push_back((coords[i-2]+pos.Z).ToFloat()); c.push_back((coords[i-1]+pos.X).ToFloat()); c.push_back((coords[i]+pos.Z).ToFloat()); m_DebugOverlayLines.push_back(SOverlayLine()); m_DebugOverlayLines.back().m_Color = thiscolor; SimRender::ConstructLineOnGround(GetSimContext(), c, m_DebugOverlayLines.back(), true); } } // Draw the min range circle if (!q.minRange.IsZero()) SimRender::ConstructCircleOnGround(GetSimContext(), pos.X.ToFloat(), pos.Y.ToFloat(), q.minRange.ToFloat(), m_DebugOverlayLines.back(), true); // Draw a ray from the source to each matched entity for (size_t i = 0; i < q.lastMatch.size(); ++i) { CmpPtr cmpTargetPosition(GetSimContext(), q.lastMatch[i]); if (!cmpTargetPosition || !cmpTargetPosition->IsInWorld()) continue; CFixedVector2D targetPos = cmpTargetPosition->GetPosition2D(); std::vector coords; coords.push_back(pos.X.ToFloat()); coords.push_back(pos.Y.ToFloat()); coords.push_back(targetPos.X.ToFloat()); coords.push_back(targetPos.Y.ToFloat()); m_DebugOverlayLines.push_back(SOverlayLine()); m_DebugOverlayLines.back().m_Color = rayColor; SimRender::ConstructLineOnGround(GetSimContext(), coords, m_DebugOverlayLines.back(), true); } } // render subdivision grid float divSize = m_Subdivision.GetDivisionSize(); int size = m_Subdivision.GetWidth(); for (int x = 0; x < size; ++x) { for (int y = 0; y < size; ++y) { m_DebugOverlayLines.push_back(SOverlayLine()); m_DebugOverlayLines.back().m_Color = subdivColor; float xpos = x*divSize + divSize/2; float zpos = y*divSize + divSize/2; SimRender::ConstructSquareOnGround(GetSimContext(), xpos, zpos, divSize, divSize, 0.0f, m_DebugOverlayLines.back(), false, 1.0f); } } m_DebugOverlayDirty = false; } for (size_t i = 0; i < m_DebugOverlayLines.size(); ++i) collector.Submit(&m_DebugOverlayLines[i]); } virtual u8 GetEntityFlagMask(const std::string& identifier) const { if (identifier == "normal") return FlagMasks::Normal; if (identifier == "injured") return FlagMasks::Injured; LOGWARNING("CCmpRangeManager: Invalid flag identifier %s", identifier.c_str()); return FlagMasks::None; } virtual void SetEntityFlag(entity_id_t ent, const std::string& identifier, bool value) { EntityMap::iterator it = m_EntityData.find(ent); // We don't have this entity if (it == m_EntityData.end()) return; u8 flag = GetEntityFlagMask(identifier); if (flag == FlagMasks::None) LOGWARNING("CCmpRangeManager: Invalid flag identifier %s for entity %u", identifier.c_str(), ent); else it->second.SetFlag(flag, value); } // **************************************************************** // LOS implementation: virtual CLosQuerier GetLosQuerier(player_id_t player) const { if (GetLosRevealAll(player)) return CLosQuerier(0xFFFFFFFFu, m_LosStateRevealed, m_TerrainVerticesPerSide); else return CLosQuerier(GetSharedLosMask(player), m_LosState, m_TerrainVerticesPerSide); } virtual void ActivateScriptedVisibility(entity_id_t ent, bool status) { EntityMap::iterator it = m_EntityData.find(ent); if (it != m_EntityData.end()) it->second.SetFlag(status); } - ELosVisibility ComputeLosVisibility(CEntityHandle ent, player_id_t player) const + LosVisibility ComputeLosVisibility(CEntityHandle ent, player_id_t player) const { // Entities not with positions in the world are never visible if (ent.GetId() == INVALID_ENTITY) - return VIS_HIDDEN; + return LosVisibility::HIDDEN; CmpPtr cmpPosition(ent); if (!cmpPosition || !cmpPosition->IsInWorld()) - return VIS_HIDDEN; + return LosVisibility::HIDDEN; // Mirage entities, whatever the situation, are visible for one specific player CmpPtr cmpMirage(ent); if (cmpMirage && cmpMirage->GetPlayer() != player) - return VIS_HIDDEN; + return LosVisibility::HIDDEN; CFixedVector2D pos = cmpPosition->GetPosition2D(); int i = (pos.X / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); int j = (pos.Y / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); // Reveal flag makes all positioned entities visible and all mirages useless if (GetLosRevealAll(player)) { if (LosIsOffWorld(i, j) || cmpMirage) - return VIS_HIDDEN; - else - return VIS_VISIBLE; + return LosVisibility::HIDDEN; + return LosVisibility::VISIBLE; } // Get visible regions CLosQuerier los(GetSharedLosMask(player), m_LosState, m_TerrainVerticesPerSide); CmpPtr cmpVisibility(ent); // Possibly ask the scripted Visibility component EntityMap::const_iterator it = m_EntityData.find(ent.GetId()); if (it != m_EntityData.end()) { if (it->second.HasFlag() && cmpVisibility) return cmpVisibility->GetVisibility(player, los.IsVisible(i, j), los.IsExplored(i, j)); } else { if (cmpVisibility && cmpVisibility->IsActivated()) return cmpVisibility->GetVisibility(player, los.IsVisible(i, j), los.IsExplored(i, j)); } // Else, default behavior if (los.IsVisible(i, j)) { if (cmpMirage) - return VIS_HIDDEN; + return LosVisibility::HIDDEN; - return VIS_VISIBLE; + return LosVisibility::VISIBLE; } if (!los.IsExplored(i, j)) - return VIS_HIDDEN; + return LosVisibility::HIDDEN; // Invisible if the 'retain in fog' flag is not set, and in a non-visible explored region // Try using the 'retainInFog' flag in m_EntityData to save a script call if (it != m_EntityData.end()) { if (!it->second.HasFlag()) - return VIS_HIDDEN; + return LosVisibility::HIDDEN; } else { if (!(cmpVisibility && cmpVisibility->GetRetainInFog())) - return VIS_HIDDEN; + return LosVisibility::HIDDEN; } if (cmpMirage) - return VIS_FOGGED; + return LosVisibility::FOGGED; CmpPtr cmpOwnership(ent); if (!cmpOwnership) - return VIS_FOGGED; + return LosVisibility::FOGGED; if (cmpOwnership->GetOwner() == player) { CmpPtr cmpFogging(ent); if (!(cmpFogging && cmpFogging->IsMiraged(player))) - return VIS_FOGGED; + return LosVisibility::FOGGED; - return VIS_HIDDEN; + return LosVisibility::HIDDEN; } // Fogged entities are hidden in two cases: // - They were not scouted // - A mirage replaces them CmpPtr cmpFogging(ent); if (cmpFogging && cmpFogging->IsActivated() && (!cmpFogging->WasSeen(player) || cmpFogging->IsMiraged(player))) - return VIS_HIDDEN; + return LosVisibility::HIDDEN; - return VIS_FOGGED; + return LosVisibility::FOGGED; } - ELosVisibility ComputeLosVisibility(entity_id_t ent, player_id_t player) const + LosVisibility ComputeLosVisibility(entity_id_t ent, player_id_t player) const { CEntityHandle handle = GetSimContext().GetComponentManager().LookupEntityHandle(ent); return ComputeLosVisibility(handle, player); } - virtual ELosVisibility GetLosVisibility(CEntityHandle ent, player_id_t player) const + virtual LosVisibility GetLosVisibility(CEntityHandle ent, player_id_t player) const { entity_id_t entId = ent.GetId(); // Entities not with positions in the world are never visible if (entId == INVALID_ENTITY) - return VIS_HIDDEN; + return LosVisibility::HIDDEN; CmpPtr cmpPosition(ent); if (!cmpPosition || !cmpPosition->IsInWorld()) - return VIS_HIDDEN; + return LosVisibility::HIDDEN; // Gaia and observers do not have a visibility cache if (player <= 0) return ComputeLosVisibility(ent, player); CFixedVector2D pos = cmpPosition->GetPosition2D(); - i32 n = PosToLosTilesHelper(pos.X, pos.Y); - if (IsVisibilityDirty(m_DirtyVisibility[n], player)) + if (IsVisibilityDirty(m_DirtyVisibility[PosToLosTilesHelper(pos.X, pos.Y)], player)) return ComputeLosVisibility(ent, player); if (std::find(m_ModifiedEntities.begin(), m_ModifiedEntities.end(), entId) != m_ModifiedEntities.end()) return ComputeLosVisibility(ent, player); EntityMap::const_iterator it = m_EntityData.find(entId); if (it == m_EntityData.end()) return ComputeLosVisibility(ent, player); - return static_cast(GetPlayerVisibility(it->second.visibilities, player)); + return static_cast(GetPlayerVisibility(it->second.visibilities, player)); } - virtual ELosVisibility GetLosVisibility(entity_id_t ent, player_id_t player) const + virtual LosVisibility GetLosVisibility(entity_id_t ent, player_id_t player) const { CEntityHandle handle = GetSimContext().GetComponentManager().LookupEntityHandle(ent); return GetLosVisibility(handle, player); } - virtual ELosVisibility GetLosVisibilityPosition(entity_pos_t x, entity_pos_t z, player_id_t player) const + virtual LosVisibility GetLosVisibilityPosition(entity_pos_t x, entity_pos_t z, player_id_t player) const { int i = (x / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); int j = (z / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); // Reveal flag makes all positioned entities visible and all mirages useless if (GetLosRevealAll(player)) { if (LosIsOffWorld(i, j)) - return VIS_HIDDEN; + return LosVisibility::HIDDEN; else - return VIS_VISIBLE; + return LosVisibility::VISIBLE; } // Get visible regions CLosQuerier los(GetSharedLosMask(player), m_LosState, m_TerrainVerticesPerSide); if (los.IsVisible(i,j)) - return VIS_VISIBLE; + return LosVisibility::VISIBLE; if (los.IsExplored(i,j)) - return VIS_FOGGED; - return VIS_HIDDEN; + return LosVisibility::FOGGED; + return LosVisibility::HIDDEN; + } + + LosTile PosToLosTilesHelper(u16 x, u16 z) const + { + return LosTile{ Clamp(x/LOS_TILES_RATIO, 0, m_LosTilesPerSide - 1), Clamp(z/LOS_TILES_RATIO, 0, m_LosTilesPerSide - 1) }; } - i32 PosToLosTilesHelper(entity_pos_t x, entity_pos_t z) const + LosTile PosToLosTilesHelper(entity_pos_t x, entity_pos_t z) const { i32 i = Clamp( (x/(entity_pos_t::FromInt(TERRAIN_TILE_SIZE * LOS_TILES_RATIO))).ToInt_RoundToZero(), 0, m_LosTilesPerSide - 1); i32 j = Clamp( (z/(entity_pos_t::FromInt(TERRAIN_TILE_SIZE * LOS_TILES_RATIO))).ToInt_RoundToZero(), 0, m_LosTilesPerSide - 1); - return j*m_LosTilesPerSide + i; + return std::make_pair(i, j); } - void AddToTile(i32 tile, entity_id_t ent) + void AddToTile(LosTile tile, entity_id_t ent) { m_LosTiles[tile].insert(ent); } - void RemoveFromTile(i32 tile, entity_id_t ent) + void RemoveFromTile(LosTile tile, entity_id_t ent) { std::set::const_iterator tileIt = m_LosTiles[tile].find(ent); if (tileIt != m_LosTiles[tile].end()) m_LosTiles[tile].erase(tileIt); } void UpdateVisibilityData() { PROFILE("UpdateVisibilityData"); - for (i32 n = 0; n < m_LosTilesPerSide * m_LosTilesPerSide; ++n) - { - for (player_id_t player = 1; player < MAX_LOS_PLAYER_ID + 1; ++player) - if (IsVisibilityDirty(m_DirtyVisibility[n], player) || m_GlobalPlayerVisibilityUpdate[player-1] == 1 || m_GlobalVisibilityUpdate) - for (const entity_id_t& ent : m_LosTiles[n]) - UpdateVisibility(ent, player); + for (u16 i = 0; i < m_LosTilesPerSide; ++i) + for (u16 j = 0; j < m_LosTilesPerSide; ++j) + { + LosTile pos{i, j}; + for (player_id_t player = 1; player < MAX_LOS_PLAYER_ID + 1; ++player) + if (IsVisibilityDirty(m_DirtyVisibility[pos], player) || m_GlobalPlayerVisibilityUpdate[player-1] == 1 || m_GlobalVisibilityUpdate) + for (const entity_id_t& ent : m_LosTiles[pos]) + UpdateVisibility(ent, player); - m_DirtyVisibility[n] = 0; - } + m_DirtyVisibility[pos] = 0; + } - std::fill(m_GlobalPlayerVisibilityUpdate.begin(), m_GlobalPlayerVisibilityUpdate.end(), 0); + std::fill(m_GlobalPlayerVisibilityUpdate.begin(), m_GlobalPlayerVisibilityUpdate.end(), false); m_GlobalVisibilityUpdate = false; // Calling UpdateVisibility can modify m_ModifiedEntities, so be careful: // infinite loops could be triggered by feedback between entities and their mirages. std::map attempts; while (!m_ModifiedEntities.empty()) { entity_id_t ent = m_ModifiedEntities.back(); m_ModifiedEntities.pop_back(); ++attempts[ent]; ENSURE(attempts[ent] < 100 && "Infinite loop in UpdateVisibilityData"); UpdateVisibility(ent); } } virtual void RequestVisibilityUpdate(entity_id_t ent) { if (std::find(m_ModifiedEntities.begin(), m_ModifiedEntities.end(), ent) == m_ModifiedEntities.end()) m_ModifiedEntities.push_back(ent); } void UpdateVisibility(entity_id_t ent, player_id_t player) { EntityMap::iterator itEnts = m_EntityData.find(ent); if (itEnts == m_EntityData.end()) return; - u8 oldVis = GetPlayerVisibility(itEnts->second.visibilities, player); - u8 newVis = ComputeLosVisibility(itEnts->first, player); + LosVisibility oldVis = GetPlayerVisibility(itEnts->second.visibilities, player); + LosVisibility newVis = ComputeLosVisibility(itEnts->first, player); if (oldVis == newVis) return; - itEnts->second.visibilities = (itEnts->second.visibilities & ~(0x3 << 2 * (player - 1))) | (newVis << 2 * (player - 1)); + itEnts->second.visibilities = (itEnts->second.visibilities & ~(0x3 << 2 * (player - 1))) | ((u8)newVis << 2 * (player - 1)); - CMessageVisibilityChanged msg(player, ent, oldVis, newVis); + CMessageVisibilityChanged msg(player, ent, static_cast(oldVis), static_cast(newVis)); GetSimContext().GetComponentManager().PostMessage(ent, msg); } void UpdateVisibility(entity_id_t ent) { for (player_id_t player = 1; player < MAX_LOS_PLAYER_ID + 1; ++player) UpdateVisibility(ent, player); } virtual void SetLosRevealAll(player_id_t player, bool enabled) { if (player == -1) m_LosRevealAll[MAX_LOS_PLAYER_ID+1] = enabled; else { ENSURE(player >= 0 && player <= MAX_LOS_PLAYER_ID); m_LosRevealAll[player] = enabled; } // On next update, update the visibility of every entity in the world m_GlobalVisibilityUpdate = true; } virtual bool GetLosRevealAll(player_id_t player) const { // Special player value can force reveal-all for every player if (m_LosRevealAll[MAX_LOS_PLAYER_ID+1] || player == -1) return true; ENSURE(player >= 0 && player <= MAX_LOS_PLAYER_ID+1); // Otherwise check the player-specific flag if (m_LosRevealAll[player]) return true; return false; } virtual void SetLosCircular(bool enabled) { m_LosCircular = enabled; ResetDerivedData(); } virtual bool GetLosCircular() const { return m_LosCircular; } virtual void SetSharedLos(player_id_t player, const std::vector& players) { m_SharedLosMasks[player] = CalcSharedLosMask(players); // Units belonging to any of 'players' can now trigger visibility updates for 'player'. // If shared LOS partners have been removed, we disable visibility updates from them // in order to improve performance. That also allows us to properly determine whether // 'player' needs a global visibility update for this turn. bool modified = false; for (player_id_t p = 1; p < MAX_LOS_PLAYER_ID+1; ++p) { bool inList = std::find(players.begin(), players.end(), p) != players.end(); if (SetPlayerSharedDirtyVisibilityBit(m_SharedDirtyVisibilityMasks[p], player, inList)) modified = true; } if (modified && (size_t)player <= m_GlobalPlayerVisibilityUpdate.size()) m_GlobalPlayerVisibilityUpdate[player-1] = 1; } virtual u32 GetSharedLosMask(player_id_t player) const { return m_SharedLosMasks[player]; } void ExploreAllTiles(player_id_t p) { for (u16 j = 0; j < m_TerrainVerticesPerSide; ++j) for (u16 i = 0; i < m_TerrainVerticesPerSide; ++i) { if (LosIsOffWorld(i,j)) continue; u32 &explored = m_ExploredVertices.at(p); - explored += !(m_LosState[i + j*m_TerrainVerticesPerSide] & (LOS_EXPLORED << (2*(p-1)))); - m_LosState[i + j*m_TerrainVerticesPerSide] |= (LOS_EXPLORED << (2*(p-1))); + explored += !(m_LosState.get(i, j) & ((u32)LosState::EXPLORED << (2*(p-1)))); + m_LosState.get(i, j) |= ((u32)LosState::EXPLORED << (2*(p-1))); } SeeExploredEntities(p); } virtual void ExploreTerritories() { PROFILE3("ExploreTerritories"); CmpPtr cmpTerritoryManager(GetSystemEntity()); const Grid& grid = cmpTerritoryManager->GetTerritoryGrid(); // Territory data is stored per territory-tile (typically a multiple of terrain-tiles). // LOS data is stored per terrain-tile vertex. // For each territory-tile, if it is owned by a valid player then update the LOS // for every vertex inside/around that tile, to mark them as explored. // Currently this code doesn't support territory-tiles smaller than terrain-tiles // (it will get scale==0 and break), or a non-integer multiple, so check that first cassert(ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE >= Pathfinding::NAVCELLS_PER_TILE); cassert(ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE % Pathfinding::NAVCELLS_PER_TILE == 0); int scale = ICmpTerritoryManager::NAVCELLS_PER_TERRITORY_TILE / Pathfinding::NAVCELLS_PER_TILE; ENSURE(grid.m_W*scale == m_TerrainVerticesPerSide-1 && grid.m_H*scale == m_TerrainVerticesPerSide-1); for (u16 j = 0; j < grid.m_H; ++j) for (u16 i = 0; i < grid.m_W; ++i) { u8 p = grid.get(i, j) & ICmpTerritoryManager::TERRITORY_PLAYER_MASK; if (p > 0 && p <= MAX_LOS_PLAYER_ID) { u32& explored = m_ExploredVertices.at(p); for (int tj = j * scale; tj <= (j+1) * scale; ++tj) for (int ti = i * scale; ti <= (i+1) * scale; ++ti) { if (LosIsOffWorld(ti, tj)) continue; - u32& losState = m_LosState[ti + tj * m_TerrainVerticesPerSide]; - if (!(losState & (LOS_EXPLORED << (2*(p-1))))) + u32& losState = m_LosState.get(ti, tj); + if (!(losState & ((u32)LosState::EXPLORED << (2*(p-1))))) { ++explored; - losState |= (LOS_EXPLORED << (2*(p-1))); + losState |= ((u32)LosState::EXPLORED << (2*(p-1))); } } } } for (player_id_t p = 1; p < MAX_LOS_PLAYER_ID+1; ++p) SeeExploredEntities(p); } /** * Force any entity in explored territory to appear for player p. * This is useful for miraging entities inside the territory borders at the beginning of a game, * or if the "Explore Map" option has been set. */ void SeeExploredEntities(player_id_t p) const { // Warning: Code related to fogging (like ForceMiraging) shouldn't be // invoked while iterating through m_EntityData. // Otherwise, by deleting mirage entities and so on, that code will // change the indexes in the map, leading to segfaults. // So we just remember what entities to mirage and do that later. std::vector miragableEntities; for (EntityMap::const_iterator it = m_EntityData.begin(); it != m_EntityData.end(); ++it) { CmpPtr cmpPosition(GetSimContext(), it->first); if (!cmpPosition || !cmpPosition->IsInWorld()) continue; CFixedVector2D pos = cmpPosition->GetPosition2D(); int i = (pos.X / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); int j = (pos.Y / (int)TERRAIN_TILE_SIZE).ToInt_RoundToNearest(); CLosQuerier los(GetSharedLosMask(p), m_LosState, m_TerrainVerticesPerSide); if (!los.IsExplored(i,j) || los.IsVisible(i,j)) continue; CmpPtr cmpFogging(GetSimContext(), it->first); if (cmpFogging) miragableEntities.push_back(it->first); } for (std::vector::iterator it = miragableEntities.begin(); it != miragableEntities.end(); ++it) { CmpPtr cmpFogging(GetSimContext(), *it); ENSURE(cmpFogging && "Impossible to retrieve Fogging component, previously achieved"); cmpFogging->ForceMiraging(p); } } virtual void RevealShore(player_id_t p, bool enable) { if (p <= 0 || p > MAX_LOS_PLAYER_ID) return; // Maximum distance to the shore const u16 maxdist = 10; CmpPtr cmpPathfinder(GetSystemEntity()); const Grid& shoreGrid = cmpPathfinder->ComputeShoreGrid(true); ENSURE(shoreGrid.m_W == m_TerrainVerticesPerSide-1 && shoreGrid.m_H == m_TerrainVerticesPerSide-1); - std::vector& counts = m_LosPlayerCounts.at(p); - ENSURE(!counts.empty()); - u16* countsData = &counts[0]; + Grid& counts = m_LosPlayerCounts.at(p); + ENSURE(!counts.blank()); for (u16 j = 0; j < shoreGrid.m_H; ++j) for (u16 i = 0; i < shoreGrid.m_W; ++i) { u16 shoredist = shoreGrid.get(i, j); if (shoredist > maxdist) continue; // Maybe we could be more clever and don't add dummy strips of one tile if (enable) - LosAddStripHelper(p, i, i, j, countsData); + LosAddStripHelper(p, i, i, j, counts); else - LosRemoveStripHelper(p, i, i, j, countsData); + LosRemoveStripHelper(p, i, i, j, counts); } } /** * Returns whether the given vertex is outside the normal bounds of the world * (i.e. outside the range of a circular map) */ inline bool LosIsOffWorld(ssize_t i, ssize_t j) const { if (m_LosCircular) { // With a circular map, vertex is off-world if hypot(i - size/2, j - size/2) >= size/2: ssize_t dist2 = (i - m_TerrainVerticesPerSide/2)*(i - m_TerrainVerticesPerSide/2) + (j - m_TerrainVerticesPerSide/2)*(j - m_TerrainVerticesPerSide/2); ssize_t r = m_TerrainVerticesPerSide / 2 - MAP_EDGE_TILES + 1; // subtract a bit from the radius to ensure nice // SoD blurring around the edges of the map return (dist2 >= r*r); } else { // With a square map, the outermost edge of the map should be off-world, // so the SoD texture blends out nicely return i < MAP_EDGE_TILES || j < MAP_EDGE_TILES || i >= m_TerrainVerticesPerSide - MAP_EDGE_TILES || j >= m_TerrainVerticesPerSide - MAP_EDGE_TILES; } } /** * Update the LOS state of tiles within a given horizontal strip (i0,j) to (i1,j) (inclusive). */ - inline void LosAddStripHelper(u8 owner, i32 i0, i32 i1, i32 j, u16* counts) + inline void LosAddStripHelper(u8 owner, i32 i0, i32 i1, i32 j, Grid& counts) { if (i1 < i0) return; - i32 idx0 = j*m_TerrainVerticesPerSide + i0; - i32 idx1 = j*m_TerrainVerticesPerSide + i1; u32 &explored = m_ExploredVertices.at(owner); - for (i32 idx = idx0; idx <= idx1; ++idx) + for (i32 i = i0; i <= i1; ++i) { // Increasing from zero to non-zero - move from unexplored/explored to visible+explored - if (counts[idx] == 0) + if (counts.get(i, j) == 0) { - i32 i = i0 + idx - idx0; if (!LosIsOffWorld(i, j)) { - explored += !(m_LosState[idx] & (LOS_EXPLORED << (2*(owner-1)))); - m_LosState[idx] |= ((LOS_VISIBLE | LOS_EXPLORED) << (2*(owner-1))); + explored += !(m_LosState.get(i, j) & ((u32)LosState::EXPLORED << (2*(owner-1)))); + m_LosState.get(i, j) |= (((int)LosState::VISIBLE | (u32)LosState::EXPLORED) << (2*(owner-1))); } MarkVisibilityDirtyAroundTile(owner, i, j); } - ASSERT(counts[idx] < 65535); - counts[idx] = (u16)(counts[idx] + 1); // ignore overflow; the player should never have 64K units + ENSURE(counts.get(i, j) < std::numeric_limits::max()); + counts.get(i, j) = (u16)(counts.get(i, j) + 1); // ignore overflow; the player should never have 64K units } } /** * Update the LOS state of tiles within a given horizontal strip (i0,j) to (i1,j) (inclusive). */ - inline void LosRemoveStripHelper(u8 owner, i32 i0, i32 i1, i32 j, u16* counts) + inline void LosRemoveStripHelper(u8 owner, i32 i0, i32 i1, i32 j, Grid& counts) { if (i1 < i0) return; - i32 idx0 = j*m_TerrainVerticesPerSide + i0; - i32 idx1 = j*m_TerrainVerticesPerSide + i1; - for (i32 idx = idx0; idx <= idx1; ++idx) + for (i32 i = i0; i <= i1; ++i) { - ASSERT(counts[idx] > 0); - counts[idx] = (u16)(counts[idx] - 1); + ASSERT(counts.get(i, j) > 0); + counts.get(i, j) = (u16)(counts.get(i, j) - 1); // Decreasing from non-zero to zero - move from visible+explored to explored - if (counts[idx] == 0) + if (counts.get(i, j) == 0) { // (If LosIsOffWorld then this is a no-op, so don't bother doing the check) - m_LosState[idx] &= ~(LOS_VISIBLE << (2*(owner-1))); + m_LosState.get(i, j) &= ~((int)LosState::VISIBLE << (2*(owner-1))); - i32 i = i0 + idx - idx0; MarkVisibilityDirtyAroundTile(owner, i, j); } } } inline void MarkVisibilityDirtyAroundTile(u8 owner, i32 i, i32 j) { // If we're still in the deserializing process, we must not modify m_DirtyVisibility if (m_Deserializing) return; // Mark the LoS tiles around the updated vertex // 1: left-up, 2: right-up, 3: left-down, 4: right-down - int n1 = ((j-1)/LOS_TILES_RATIO)*m_LosTilesPerSide + (i-1)/LOS_TILES_RATIO; - int n2 = ((j-1)/LOS_TILES_RATIO)*m_LosTilesPerSide + i/LOS_TILES_RATIO; - int n3 = (j/LOS_TILES_RATIO)*m_LosTilesPerSide + (i-1)/LOS_TILES_RATIO; - int n4 = (j/LOS_TILES_RATIO)*m_LosTilesPerSide + i/LOS_TILES_RATIO; + LosTile n1 = PosToLosTilesHelper(i-1, j-1); + LosTile n2 = PosToLosTilesHelper(i-1, j); + LosTile n3 = PosToLosTilesHelper(i, j-1); + LosTile n4 = PosToLosTilesHelper(i, j); u16 sharedDirtyVisibilityMask = m_SharedDirtyVisibilityMasks[owner]; if (j > 0 && i > 0) m_DirtyVisibility[n1] |= sharedDirtyVisibilityMask; if (n2 != n1 && j > 0 && i < m_TerrainVerticesPerSide) m_DirtyVisibility[n2] |= sharedDirtyVisibilityMask; if (n3 != n1 && j < m_TerrainVerticesPerSide && i > 0) m_DirtyVisibility[n3] |= sharedDirtyVisibilityMask; if (n4 != n1 && j < m_TerrainVerticesPerSide && i < m_TerrainVerticesPerSide) m_DirtyVisibility[n4] |= sharedDirtyVisibilityMask; } /** * Update the LOS state of tiles within a given circular range, * either adding or removing visibility depending on the template parameter. * Assumes owner is in the valid range. */ template void LosUpdateHelper(u8 owner, entity_pos_t visionRange, CFixedVector2D pos) { if (m_TerrainVerticesPerSide == 0) // do nothing if not initialised yet return; PROFILE("LosUpdateHelper"); - std::vector& counts = m_LosPlayerCounts.at(owner); + Grid& counts = m_LosPlayerCounts.at(owner); // Lazy initialisation of counts: - if (counts.empty()) - counts.resize(m_TerrainVerticesPerSide*m_TerrainVerticesPerSide); - - u16* countsData = &counts[0]; + if (counts.blank()) + counts.resize(m_TerrainVerticesPerSide, m_TerrainVerticesPerSide); // Compute the circular region as a series of strips. // Rather than quantise pos to vertexes, we do more precise sub-tile computations // to get smoother behaviour as a unit moves rather than jumping a whole tile // at once. // To avoid the cost of sqrt when computing the outline of the circle, // we loop from the bottom to the top and estimate the width of the current // strip based on the previous strip, then adjust each end of the strip // inwards or outwards until it's the widest that still falls within the circle. // Compute top/bottom coordinates, and clamp to exclude the 1-tile border around the map // (so that we never render the sharp edge of the map) i32 j0 = ((pos.Y - visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToInfinity(); i32 j1 = ((pos.Y + visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToNegInfinity(); i32 j0clamp = std::max(j0, 1); i32 j1clamp = std::min(j1, m_TerrainVerticesPerSide-2); // Translate world coordinates into fractional tile-space coordinates entity_pos_t x = pos.X / (int)TERRAIN_TILE_SIZE; entity_pos_t y = pos.Y / (int)TERRAIN_TILE_SIZE; entity_pos_t r = visionRange / (int)TERRAIN_TILE_SIZE; entity_pos_t r2 = r.Square(); // Compute the integers on either side of x i32 xfloor = (x - entity_pos_t::Epsilon()).ToInt_RoundToNegInfinity(); i32 xceil = (x + entity_pos_t::Epsilon()).ToInt_RoundToInfinity(); // Initialise the strip (i0, i1) to a rough guess i32 i0 = xfloor; i32 i1 = xceil; for (i32 j = j0clamp; j <= j1clamp; ++j) { // Adjust i0 and i1 to be the outermost values that don't exceed // the circle's radius (i.e. require dy^2 + dx^2 <= r^2). // When moving the points inwards, clamp them to xceil+1 or xfloor-1 // so they don't accidentally shoot off in the wrong direction forever. entity_pos_t dy = entity_pos_t::FromInt(j) - y; entity_pos_t dy2 = dy.Square(); while (dy2 + (entity_pos_t::FromInt(i0-1) - x).Square() <= r2) --i0; while (i0 < xceil && dy2 + (entity_pos_t::FromInt(i0) - x).Square() > r2) ++i0; while (dy2 + (entity_pos_t::FromInt(i1+1) - x).Square() <= r2) ++i1; while (i1 > xfloor && dy2 + (entity_pos_t::FromInt(i1) - x).Square() > r2) --i1; #if DEBUG_RANGE_MANAGER_BOUNDS if (i0 <= i1) { ENSURE(dy2 + (entity_pos_t::FromInt(i0) - x).Square() <= r2); ENSURE(dy2 + (entity_pos_t::FromInt(i1) - x).Square() <= r2); } ENSURE(dy2 + (entity_pos_t::FromInt(i0 - 1) - x).Square() > r2); ENSURE(dy2 + (entity_pos_t::FromInt(i1 + 1) - x).Square() > r2); #endif // Clamp the strip to exclude the 1-tile border, // then add or remove the strip as requested i32 i0clamp = std::max(i0, 1); i32 i1clamp = std::min(i1, m_TerrainVerticesPerSide-2); if (adding) - LosAddStripHelper(owner, i0clamp, i1clamp, j, countsData); + LosAddStripHelper(owner, i0clamp, i1clamp, j, counts); else - LosRemoveStripHelper(owner, i0clamp, i1clamp, j, countsData); + LosRemoveStripHelper(owner, i0clamp, i1clamp, j, counts); } } /** * Update the LOS state of tiles within a given circular range, * by removing visibility around the 'from' position * and then adding visibility around the 'to' position. */ void LosUpdateHelperIncremental(u8 owner, entity_pos_t visionRange, CFixedVector2D from, CFixedVector2D to) { if (m_TerrainVerticesPerSide == 0) // do nothing if not initialised yet return; PROFILE("LosUpdateHelperIncremental"); - std::vector& counts = m_LosPlayerCounts.at(owner); + Grid& counts = m_LosPlayerCounts.at(owner); // Lazy initialisation of counts: - if (counts.empty()) - counts.resize(m_TerrainVerticesPerSide*m_TerrainVerticesPerSide); - - u16* countsData = &counts[0]; + if (counts.blank()) + counts.resize(m_TerrainVerticesPerSide, m_TerrainVerticesPerSide); // See comments in LosUpdateHelper. // This does exactly the same, except computing the strips for // both circles simultaneously. // (The idea is that the circles will be heavily overlapping, // so we can compute the difference between the removed/added strips // and only have to touch tiles that have a net change.) i32 j0_from = ((from.Y - visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToInfinity(); i32 j1_from = ((from.Y + visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToNegInfinity(); i32 j0_to = ((to.Y - visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToInfinity(); i32 j1_to = ((to.Y + visionRange)/(int)TERRAIN_TILE_SIZE).ToInt_RoundToNegInfinity(); i32 j0clamp = std::max(std::min(j0_from, j0_to), 1); i32 j1clamp = std::min(std::max(j1_from, j1_to), m_TerrainVerticesPerSide-2); entity_pos_t x_from = from.X / (int)TERRAIN_TILE_SIZE; entity_pos_t y_from = from.Y / (int)TERRAIN_TILE_SIZE; entity_pos_t x_to = to.X / (int)TERRAIN_TILE_SIZE; entity_pos_t y_to = to.Y / (int)TERRAIN_TILE_SIZE; entity_pos_t r = visionRange / (int)TERRAIN_TILE_SIZE; entity_pos_t r2 = r.Square(); i32 xfloor_from = (x_from - entity_pos_t::Epsilon()).ToInt_RoundToNegInfinity(); i32 xceil_from = (x_from + entity_pos_t::Epsilon()).ToInt_RoundToInfinity(); i32 xfloor_to = (x_to - entity_pos_t::Epsilon()).ToInt_RoundToNegInfinity(); i32 xceil_to = (x_to + entity_pos_t::Epsilon()).ToInt_RoundToInfinity(); i32 i0_from = xfloor_from; i32 i1_from = xceil_from; i32 i0_to = xfloor_to; i32 i1_to = xceil_to; for (i32 j = j0clamp; j <= j1clamp; ++j) { entity_pos_t dy_from = entity_pos_t::FromInt(j) - y_from; entity_pos_t dy2_from = dy_from.Square(); while (dy2_from + (entity_pos_t::FromInt(i0_from-1) - x_from).Square() <= r2) --i0_from; while (i0_from < xceil_from && dy2_from + (entity_pos_t::FromInt(i0_from) - x_from).Square() > r2) ++i0_from; while (dy2_from + (entity_pos_t::FromInt(i1_from+1) - x_from).Square() <= r2) ++i1_from; while (i1_from > xfloor_from && dy2_from + (entity_pos_t::FromInt(i1_from) - x_from).Square() > r2) --i1_from; entity_pos_t dy_to = entity_pos_t::FromInt(j) - y_to; entity_pos_t dy2_to = dy_to.Square(); while (dy2_to + (entity_pos_t::FromInt(i0_to-1) - x_to).Square() <= r2) --i0_to; while (i0_to < xceil_to && dy2_to + (entity_pos_t::FromInt(i0_to) - x_to).Square() > r2) ++i0_to; while (dy2_to + (entity_pos_t::FromInt(i1_to+1) - x_to).Square() <= r2) ++i1_to; while (i1_to > xfloor_to && dy2_to + (entity_pos_t::FromInt(i1_to) - x_to).Square() > r2) --i1_to; #if DEBUG_RANGE_MANAGER_BOUNDS if (i0_from <= i1_from) { ENSURE(dy2_from + (entity_pos_t::FromInt(i0_from) - x_from).Square() <= r2); ENSURE(dy2_from + (entity_pos_t::FromInt(i1_from) - x_from).Square() <= r2); } ENSURE(dy2_from + (entity_pos_t::FromInt(i0_from - 1) - x_from).Square() > r2); ENSURE(dy2_from + (entity_pos_t::FromInt(i1_from + 1) - x_from).Square() > r2); if (i0_to <= i1_to) { ENSURE(dy2_to + (entity_pos_t::FromInt(i0_to) - x_to).Square() <= r2); ENSURE(dy2_to + (entity_pos_t::FromInt(i1_to) - x_to).Square() <= r2); } ENSURE(dy2_to + (entity_pos_t::FromInt(i0_to - 1) - x_to).Square() > r2); ENSURE(dy2_to + (entity_pos_t::FromInt(i1_to + 1) - x_to).Square() > r2); #endif // Check whether this strip moved at all if (!(i0_to == i0_from && i1_to == i1_from)) { i32 i0clamp_from = std::max(i0_from, 1); i32 i1clamp_from = std::min(i1_from, m_TerrainVerticesPerSide-2); i32 i0clamp_to = std::max(i0_to, 1); i32 i1clamp_to = std::min(i1_to, m_TerrainVerticesPerSide-2); // Check whether one strip is negative width, // and we can just add/remove the entire other strip if (i1clamp_from < i0clamp_from) { - LosAddStripHelper(owner, i0clamp_to, i1clamp_to, j, countsData); + LosAddStripHelper(owner, i0clamp_to, i1clamp_to, j, counts); } else if (i1clamp_to < i0clamp_to) { - LosRemoveStripHelper(owner, i0clamp_from, i1clamp_from, j, countsData); + LosRemoveStripHelper(owner, i0clamp_from, i1clamp_from, j, counts); } else { // There are four possible regions of overlap between the two strips // (remove before add, remove after add, add before remove, add after remove). // Process each of the regions as its own strip. // (If this produces negative-width strips then they'll just get ignored // which is fine.) // (If the strips don't actually overlap (which is very rare with normal unit // movement speeds), the region between them will be both added and removed, // so we have to do the add first to avoid overflowing to -1 and triggering // assertion failures.) - LosAddStripHelper(owner, i0clamp_to, i0clamp_from-1, j, countsData); - LosAddStripHelper(owner, i1clamp_from+1, i1clamp_to, j, countsData); - LosRemoveStripHelper(owner, i0clamp_from, i0clamp_to-1, j, countsData); - LosRemoveStripHelper(owner, i1clamp_to+1, i1clamp_from, j, countsData); + LosAddStripHelper(owner, i0clamp_to, i0clamp_from-1, j, counts); + LosAddStripHelper(owner, i1clamp_from+1, i1clamp_to, j, counts); + LosRemoveStripHelper(owner, i0clamp_from, i0clamp_to-1, j, counts); + LosRemoveStripHelper(owner, i1clamp_to+1, i1clamp_from, j, counts); } } } } void LosAdd(player_id_t owner, entity_pos_t visionRange, CFixedVector2D pos) { if (visionRange.IsZero() || owner <= 0 || owner > MAX_LOS_PLAYER_ID) return; LosUpdateHelper((u8)owner, visionRange, pos); } void SharingLosAdd(u16 visionSharing, entity_pos_t visionRange, CFixedVector2D pos) { if (visionRange.IsZero()) return; for (player_id_t i = 1; i < MAX_LOS_PLAYER_ID+1; ++i) if (HasVisionSharing(visionSharing, i)) LosAdd(i, visionRange, pos); } void LosRemove(player_id_t owner, entity_pos_t visionRange, CFixedVector2D pos) { if (visionRange.IsZero() || owner <= 0 || owner > MAX_LOS_PLAYER_ID) return; LosUpdateHelper((u8)owner, visionRange, pos); } void SharingLosRemove(u16 visionSharing, entity_pos_t visionRange, CFixedVector2D pos) { if (visionRange.IsZero()) return; for (player_id_t i = 1; i < MAX_LOS_PLAYER_ID+1; ++i) if (HasVisionSharing(visionSharing, i)) LosRemove(i, visionRange, pos); } void LosMove(player_id_t owner, entity_pos_t visionRange, CFixedVector2D from, CFixedVector2D to) { if (visionRange.IsZero() || owner <= 0 || owner > MAX_LOS_PLAYER_ID) return; if ((from - to).CompareLength(visionRange) > 0) { // If it's a very large move, then simply remove and add to the new position LosUpdateHelper((u8)owner, visionRange, from); LosUpdateHelper((u8)owner, visionRange, to); } else // Otherwise use the version optimised for mostly-overlapping circles LosUpdateHelperIncremental((u8)owner, visionRange, from, to); } void SharingLosMove(u16 visionSharing, entity_pos_t visionRange, CFixedVector2D from, CFixedVector2D to) { if (visionRange.IsZero()) return; for (player_id_t i = 1; i < MAX_LOS_PLAYER_ID+1; ++i) if (HasVisionSharing(visionSharing, i)) LosMove(i, visionRange, from, to); } virtual u8 GetPercentMapExplored(player_id_t player) const { return m_ExploredVertices.at((u8)player) * 100 / m_TotalInworldVertices; } virtual u8 GetUnionPercentMapExplored(const std::vector& players) const { u32 exploredVertices = 0; std::vector::const_iterator playerIt; for (i32 j = 0; j < m_TerrainVerticesPerSide; j++) for (i32 i = 0; i < m_TerrainVerticesPerSide; i++) { if (LosIsOffWorld(i, j)) continue; for (playerIt = players.begin(); playerIt != players.end(); ++playerIt) - if (m_LosState[j*m_TerrainVerticesPerSide + i] & (LOS_EXPLORED << (2*((*playerIt)-1)))) + if (m_LosState.get(i, j) & ((u32)LosState::EXPLORED << (2*((*playerIt)-1)))) { exploredVertices += 1; break; } } return exploredVertices * 100 / m_TotalInworldVertices; } }; REGISTER_COMPONENT_TYPE(RangeManager) #undef LOS_TILES_RATIO #undef DEBUG_RANGE_MANAGER_BOUNDS Index: ps/trunk/source/simulation2/components/CCmpSoundManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpSoundManager.cpp (revision 23768) +++ ps/trunk/source/simulation2/components/CCmpSoundManager.cpp (revision 23769) @@ -1,105 +1,105 @@ -/* Copyright (C) 2018 Wildfire Games. +/* Copyright (C) 2020 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 "simulation2/system/Component.h" #include "ICmpSoundManager.h" #include "simulation2/MessageTypes.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/components/ICmpOwnership.h" #include "soundmanager/ISoundManager.h" class CCmpSoundManager : public ICmpSoundManager { public: static void ClassInit(CComponentManager& UNUSED(componentManager) ) { } DEFAULT_COMPONENT_ALLOCATOR(SoundManager) static std::string GetSchema() { return ""; } virtual void Init(const CParamNode& UNUSED(paramNode)) { } virtual void Deinit() { } virtual void Serialize(ISerializer& UNUSED(serialize)) { // Do nothing here - sounds are purely local, and don't need to be preserved across saved games etc // (If we add music support in here then we might want to save the music state, though) } virtual void Deserialize(const CParamNode& paramNode, IDeserializer& UNUSED(deserialize)) { Init(paramNode); } virtual void PlaySoundGroup(const std::wstring& name, entity_id_t source) { if (!g_SoundManager || (source == INVALID_ENTITY)) return; int currentPlayer = GetSimContext().GetCurrentDisplayedPlayer(); CmpPtr cmpRangeManager(GetSystemEntity()); - if (!cmpRangeManager || (cmpRangeManager->GetLosVisibility(source, currentPlayer) != ICmpRangeManager::VIS_VISIBLE)) + if (!cmpRangeManager || (cmpRangeManager->GetLosVisibility(source, currentPlayer) != LosVisibility::VISIBLE)) return; CmpPtr cmpPosition(GetSimContext(), source); if (!cmpPosition || !cmpPosition->IsInWorld()) return; bool playerOwned = false; CmpPtr cmpOwnership(GetSimContext(), source); if (cmpOwnership) playerOwned = cmpOwnership->GetOwner() == currentPlayer; CVector3D sourcePos = CVector3D(cmpPosition->GetPosition()); g_SoundManager->PlayAsGroup(name, sourcePos, source, playerOwned); } virtual void PlaySoundGroupAtPosition(const std::wstring& name, const CFixedVector3D& sourcePos) { if (!g_SoundManager) return; g_SoundManager->PlayAsGroup(name, CVector3D(sourcePos), INVALID_ENTITY, false); } virtual void StopMusic() { if (!g_SoundManager) return; g_SoundManager->Pause(true); } }; REGISTER_COMPONENT_TYPE(SoundManager) Index: ps/trunk/source/simulation2/components/CCmpUnitRenderer.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpUnitRenderer.cpp (revision 23768) +++ ps/trunk/source/simulation2/components/CCmpUnitRenderer.cpp (revision 23769) @@ -1,473 +1,473 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2020 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 "simulation2/system/Component.h" #include "ICmpUnitRenderer.h" #include "simulation2/MessageTypes.h" #include "ICmpPosition.h" #include "ICmpRangeManager.h" #include "ICmpSelectable.h" #include "ICmpVisibility.h" #include "ICmpVisual.h" #include "graphics/Frustum.h" #include "graphics/ModelAbstract.h" #include "graphics/ObjectEntry.h" #include "graphics/Overlay.h" #include "graphics/Unit.h" #include "maths/BoundingSphere.h" #include "maths/Matrix3D.h" #include "ps/GameSetup/Config.h" #include "ps/Profile.h" #include "renderer/RenderingOptions.h" #include "renderer/Scene.h" #include "tools/atlas/GameInterface/GameLoop.h" /** * Efficiently(ish) renders all the units in the world. * * The class maintains a list of all units that currently exist, and the data * needed for frustum-culling them. To minimise the amount of work done per * frame (despite a unit's interpolated position changing every frame), the * culling data is only updated once per turn: we store the position at the * start of the turn, and the position at the end of the turn, and assume the * unit might be anywhere between those two points (linearly). * * (Note this is a slightly invalid assumption: units don't always move linearly, * since their interpolated position depends on terrain and water. But over a * single turn it's probably going to be a good enough approximation, and will * only break for units that both start and end the turn off-screen.) * * We want to ignore rotation entirely, since it's a complex function of * interpolated position and terrain. So we store a bounding sphere, which * is rotation-independent, instead of a bounding box. */ class CCmpUnitRenderer : public ICmpUnitRenderer { public: struct SUnit { CEntityHandle entity; CUnit* actor; int flags; /** * m_FrameNumber from when the model's transform was last updated. * This is used to avoid recomputing it multiple times per frame * if a model is visible in multiple cull groups. */ int lastTransformFrame; /** * Worst-case bounding shape, relative to position. Needs to account * for all possible animations, orientations, etc. */ CBoundingSphere boundsApprox; /** * Cached LOS visibility status. */ - ICmpRangeManager::ELosVisibility visibility; + LosVisibility visibility; bool visibilityDirty; /** * Whether the unit has a valid position. If false, pos0 and pos1 * are meaningless. */ bool inWorld; /** * World-space positions to interpolate between. */ CVector3D pos0; CVector3D pos1; /** * Bounds encompassing the unit's bounds when it is anywhere between * pos0 and pos1. */ CBoundingSphere sweptBounds; /** * For debug overlay. */ bool culled; }; std::vector m_Units; std::vector m_UnitTagsFree; int m_FrameNumber; float m_FrameOffset; bool m_EnableDebugOverlays; std::vector m_DebugSpheres; static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeToMessageType(MT_TurnStart); componentManager.SubscribeToMessageType(MT_Interpolate); componentManager.SubscribeToMessageType(MT_RenderSubmit); } DEFAULT_COMPONENT_ALLOCATOR(UnitRenderer) static std::string GetSchema() { return ""; } virtual void Init(const CParamNode& UNUSED(paramNode)) { m_FrameNumber = 0; m_FrameOffset = 0.0f; m_EnableDebugOverlays = false; } virtual void Deinit() { } virtual void Serialize(ISerializer& UNUSED(serialize)) { } virtual void Deserialize(const CParamNode& paramNode, IDeserializer& UNUSED(deserialize)) { Init(paramNode); } virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_TurnStart: { TurnStart(); break; } case MT_Interpolate: { const CMessageInterpolate& msgData = static_cast (msg); Interpolate(msgData.deltaSimTime, msgData.offset); break; } case MT_RenderSubmit: { const CMessageRenderSubmit& msgData = static_cast (msg); RenderSubmit(msgData.collector, msgData.frustum, msgData.culling); break; } } } SUnit* LookupUnit(tag_t tag) { if (tag.n < 1 || tag.n - 1 >= m_Units.size()) return NULL; return &m_Units[tag.n - 1]; } virtual tag_t AddUnit(CEntityHandle entity, CUnit* actor, const CBoundingSphere& boundsApprox, int flags) { ENSURE(actor != NULL); tag_t tag; if (!m_UnitTagsFree.empty()) { tag = m_UnitTagsFree.back(); m_UnitTagsFree.pop_back(); } else { m_Units.push_back(SUnit()); tag.n = m_Units.size(); } SUnit* unit = LookupUnit(tag); unit->entity = entity; unit->actor = actor; unit->lastTransformFrame = -1; unit->flags = flags; unit->boundsApprox = boundsApprox; unit->inWorld = false; unit->visibilityDirty = true; unit->pos0 = unit->pos1 = CVector3D(); return tag; } virtual void RemoveUnit(tag_t tag) { SUnit* unit = LookupUnit(tag); unit->actor = NULL; unit->inWorld = false; m_UnitTagsFree.push_back(tag); } void RecomputeSweptBounds(SUnit* unit) { // Compute the bounding sphere of the capsule formed by // sweeping boundsApprox from pos0 to pos1 CVector3D mid = (unit->pos0 + unit->pos1) * 0.5f + unit->boundsApprox.GetCenter(); float radius = (unit->pos1 - unit->pos0).Length() * 0.5f + unit->boundsApprox.GetRadius(); unit->sweptBounds = CBoundingSphere(mid, radius); } virtual void UpdateUnit(tag_t tag, CUnit* actor, const CBoundingSphere& boundsApprox) { SUnit* unit = LookupUnit(tag); unit->actor = actor; unit->boundsApprox = boundsApprox; RecomputeSweptBounds(unit); } virtual void UpdateUnitPos(tag_t tag, bool inWorld, const CVector3D& pos0, const CVector3D& pos1) { SUnit* unit = LookupUnit(tag); unit->inWorld = inWorld; unit->pos0 = pos0; unit->pos1 = pos1; unit->visibilityDirty = true; RecomputeSweptBounds(unit); } void TurnStart(); void Interpolate(float frameTime, float frameOffset); void RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling); void UpdateVisibility(SUnit& unit) const; virtual float GetFrameOffset() const { return m_FrameOffset; } virtual void SetDebugOverlay(bool enabled) { m_EnableDebugOverlays = enabled; } virtual void PickAllEntitiesAtPoint(std::vector >& outEntities, const CVector3D& origin, const CVector3D& dir, bool allowEditorSelectables) const { // First, make a rough test with the worst-case bounding boxes to pick all // entities/models that could possibly be hit by the ray. std::vector candidates; for (const SUnit& unit : m_Units) { if (!unit.actor || !unit.inWorld) continue; if (unit.sweptBounds.RayIntersect(origin, dir)) candidates.push_back(&unit); } // Now make a more precise test to get rid of the remaining false positives float tmin, tmax; CVector3D center; for (size_t i = 0; i< candidates.size(); ++i) { const SUnit& unit = *candidates[i]; CmpPtr cmpVisual(unit.entity); if (!cmpVisual) continue; CBoundingBoxOriented selectionBox = cmpVisual->GetSelectionBox(); if (selectionBox.IsEmpty()) { if (!allowEditorSelectables) continue; // Fall back to using old AABB selection method for decals // see: http://trac.wildfiregames.com/ticket/1032 // Decals are flat objects without a selectionShape defined, // but they should still be selectable in the editor to move them // around or delete them after they are placed. // Check campaigns/labels/ in the Actors tab of atlas for examples. CBoundingBoxAligned aABBox = cmpVisual->GetBounds(); if (aABBox.IsEmpty()) continue; if (!aABBox.RayIntersect(origin, dir, tmin, tmax)) continue; aABBox.GetCenter(center); } else { if (!selectionBox.RayIntersect(origin, dir, tmin, tmax)) continue; center = selectionBox.m_Center; } outEntities.emplace_back(unit.entity, center); } } }; void CCmpUnitRenderer::TurnStart() { PROFILE3("UnitRenderer::TurnStart"); // Assume units have stopped moving after the previous turn. If that assumption is not // correct, we will get a UpdateUnitPos to tell us about its movement in the new turn. for (size_t i = 0; i < m_Units.size(); i++) { SUnit& unit = m_Units[i]; unit.pos0 = unit.pos1; unit.sweptBounds = CBoundingSphere(unit.pos1, unit.boundsApprox.GetRadius()); // Visibility must be recomputed on the first frame during this turn unit.visibilityDirty = true; } } void CCmpUnitRenderer::Interpolate(float frameTime, float frameOffset) { PROFILE3("UnitRenderer::Interpolate"); ++m_FrameNumber; m_FrameOffset = frameOffset; // TODO: we shouldn't update all the animations etc for units that are off-screen // (but need to be careful about e.g. sounds triggered by animations of off-screen // units) for (size_t i = 0; i < m_Units.size(); i++) { SUnit& unit = m_Units[i]; if (unit.actor) unit.actor->UpdateModel(frameTime); } m_DebugSpheres.clear(); if (m_EnableDebugOverlays) { for (size_t i = 0; i < m_Units.size(); i++) { SUnit& unit = m_Units[i]; if (!(unit.actor && unit.inWorld)) continue; SOverlaySphere sphere; sphere.m_Center = unit.sweptBounds.GetCenter(); sphere.m_Radius = unit.sweptBounds.GetRadius(); if (unit.culled) sphere.m_Color = CColor(1.0f, 0.5f, 0.5f, 0.5f); else sphere.m_Color = CColor(0.5f, 0.5f, 1.0f, 0.5f); m_DebugSpheres.push_back(sphere); } } } void CCmpUnitRenderer::RenderSubmit(SceneCollector& collector, const CFrustum& frustum, bool culling) { // TODO: need a coarse culling pass based on some kind of spatial data // structure - that's the main point of this design. Once we've got a // rough list of possibly-visible units, then we can do the more precise // culling. (And once it's cheap enough, we can do multiple culling passes // per frame - one for shadow generation, one for water reflections, etc.) PROFILE3("UnitRenderer::RenderSubmit"); for (size_t i = 0; i < m_Units.size(); ++i) { SUnit& unit = m_Units[i]; unit.culled = true; if (!unit.actor) continue; if (unit.visibilityDirty) UpdateVisibility(unit); - if (unit.visibility == ICmpRangeManager::VIS_HIDDEN) + if (unit.visibility == LosVisibility::HIDDEN) continue; if (!g_AtlasGameLoop->running && !g_RenderingOptions.GetRenderActors() && (unit.flags & ACTOR_ONLY)) continue; if (!g_AtlasGameLoop->running && (unit.flags & VISIBLE_IN_ATLAS_ONLY)) continue; if (culling && !frustum.IsSphereVisible(unit.sweptBounds.GetCenter(), unit.sweptBounds.GetRadius())) continue; unit.culled = false; CModelAbstract& unitModel = unit.actor->GetModel(); if (unit.lastTransformFrame != m_FrameNumber) { CmpPtr cmpPosition(unit.entity); if (!cmpPosition) continue; CMatrix3D transform(cmpPosition->GetInterpolatedTransform(m_FrameOffset)); unitModel.SetTransform(transform); unit.lastTransformFrame = m_FrameNumber; } if (culling && !frustum.IsBoxVisible(unitModel.GetWorldBoundsRec())) continue; collector.SubmitRecursive(&unitModel); } for (size_t i = 0; i < m_DebugSpheres.size(); ++i) collector.Submit(&m_DebugSpheres[i]); } void CCmpUnitRenderer::UpdateVisibility(SUnit& unit) const { if (unit.inWorld) { // The 'always visible' flag means we should always render the unit // (regardless of whether the LOS system thinks it's visible) CmpPtr cmpVisibility(unit.entity); if (cmpVisibility && cmpVisibility->GetAlwaysVisible()) - unit.visibility = ICmpRangeManager::VIS_VISIBLE; + unit.visibility = LosVisibility::VISIBLE; else { CmpPtr cmpRangeManager(GetSystemEntity()); unit.visibility = cmpRangeManager->GetLosVisibility(unit.entity, GetSimContext().GetCurrentDisplayedPlayer()); } } else - unit.visibility = ICmpRangeManager::VIS_HIDDEN; + unit.visibility = LosVisibility::HIDDEN; // Change the visibility of the visual actor's selectable if it has one. CmpPtr cmpSelectable(unit.entity); if (cmpSelectable) - cmpSelectable->SetVisibility(unit.visibility != ICmpRangeManager::VIS_HIDDEN); + cmpSelectable->SetVisibility(unit.visibility != LosVisibility::HIDDEN); unit.visibilityDirty = false; } REGISTER_COMPONENT_TYPE(UnitRenderer) Index: ps/trunk/source/simulation2/components/ICmpRangeManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/ICmpRangeManager.cpp (revision 23768) +++ ps/trunk/source/simulation2/components/ICmpRangeManager.cpp (revision 23769) @@ -1,77 +1,77 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2020 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 "ICmpRangeManager.h" #include "simulation2/system/InterfaceScripted.h" namespace { - std::string VisibilityToString(ICmpRangeManager::ELosVisibility visibility) + std::string VisibilityToString(LosVisibility visibility) { switch (visibility) { - case ICmpRangeManager::VIS_HIDDEN: return "hidden"; - case ICmpRangeManager::VIS_FOGGED: return "fogged"; - case ICmpRangeManager::VIS_VISIBLE: return "visible"; + case LosVisibility::HIDDEN: return "hidden"; + case LosVisibility::FOGGED: return "fogged"; + case LosVisibility::VISIBLE: return "visible"; default: return "error"; // should never happen } } } std::string ICmpRangeManager::GetLosVisibility_wrapper(entity_id_t ent, int player) const { return VisibilityToString(GetLosVisibility(ent, player)); } std::string ICmpRangeManager::GetLosVisibilityPosition_wrapper(entity_pos_t x, entity_pos_t z, int player) const { return VisibilityToString(GetLosVisibilityPosition(x, z, player)); } BEGIN_INTERFACE_WRAPPER(RangeManager) DEFINE_INTERFACE_METHOD_5("ExecuteQuery", std::vector, ICmpRangeManager, ExecuteQuery, entity_id_t, entity_pos_t, entity_pos_t, std::vector, int) DEFINE_INTERFACE_METHOD_5("ExecuteQueryAroundPos", std::vector, ICmpRangeManager, ExecuteQueryAroundPos, CFixedVector2D, entity_pos_t, entity_pos_t, std::vector, int) DEFINE_INTERFACE_METHOD_6("CreateActiveQuery", ICmpRangeManager::tag_t, ICmpRangeManager, CreateActiveQuery, entity_id_t, entity_pos_t, entity_pos_t, std::vector, int, u8) DEFINE_INTERFACE_METHOD_7("CreateActiveParabolicQuery", ICmpRangeManager::tag_t, ICmpRangeManager, CreateActiveParabolicQuery, entity_id_t, entity_pos_t, entity_pos_t, entity_pos_t, std::vector, int, u8) DEFINE_INTERFACE_METHOD_1("DestroyActiveQuery", void, ICmpRangeManager, DestroyActiveQuery, ICmpRangeManager::tag_t) DEFINE_INTERFACE_METHOD_1("EnableActiveQuery", void, ICmpRangeManager, EnableActiveQuery, ICmpRangeManager::tag_t) DEFINE_INTERFACE_METHOD_1("DisableActiveQuery", void, ICmpRangeManager, DisableActiveQuery, ICmpRangeManager::tag_t) DEFINE_INTERFACE_METHOD_CONST_1("IsActiveQueryEnabled", bool, ICmpRangeManager, IsActiveQueryEnabled, ICmpRangeManager::tag_t) DEFINE_INTERFACE_METHOD_1("ResetActiveQuery", std::vector, ICmpRangeManager, ResetActiveQuery, ICmpRangeManager::tag_t) DEFINE_INTERFACE_METHOD_3("SetEntityFlag", void, ICmpRangeManager, SetEntityFlag, entity_id_t, std::string, bool) DEFINE_INTERFACE_METHOD_CONST_1("GetEntityFlagMask", u8, ICmpRangeManager, GetEntityFlagMask, std::string) DEFINE_INTERFACE_METHOD_CONST_1("GetEntitiesByPlayer", std::vector, ICmpRangeManager, GetEntitiesByPlayer, player_id_t) DEFINE_INTERFACE_METHOD_CONST_0("GetNonGaiaEntities", std::vector, ICmpRangeManager, GetNonGaiaEntities) DEFINE_INTERFACE_METHOD_CONST_0("GetGaiaAndNonGaiaEntities", std::vector, ICmpRangeManager, GetGaiaAndNonGaiaEntities) DEFINE_INTERFACE_METHOD_1("SetDebugOverlay", void, ICmpRangeManager, SetDebugOverlay, bool) DEFINE_INTERFACE_METHOD_1("ExploreAllTiles", void, ICmpRangeManager, ExploreAllTiles, player_id_t) DEFINE_INTERFACE_METHOD_0("ExploreTerritories", void, ICmpRangeManager, ExploreTerritories) DEFINE_INTERFACE_METHOD_2("SetLosRevealAll", void, ICmpRangeManager, SetLosRevealAll, player_id_t, bool) DEFINE_INTERFACE_METHOD_CONST_1("GetLosRevealAll", bool, ICmpRangeManager, GetLosRevealAll, player_id_t) DEFINE_INTERFACE_METHOD_CONST_5("GetElevationAdaptedRange", entity_pos_t, ICmpRangeManager, GetElevationAdaptedRange, CFixedVector3D, CFixedVector3D, entity_pos_t, entity_pos_t, entity_pos_t) DEFINE_INTERFACE_METHOD_2("ActivateScriptedVisibility", void, ICmpRangeManager, ActivateScriptedVisibility, entity_id_t, bool) DEFINE_INTERFACE_METHOD_CONST_2("GetLosVisibility", std::string, ICmpRangeManager, GetLosVisibility_wrapper, entity_id_t, player_id_t) DEFINE_INTERFACE_METHOD_CONST_3("GetLosVisibilityPosition", std::string, ICmpRangeManager, GetLosVisibilityPosition_wrapper, entity_pos_t, entity_pos_t, player_id_t) DEFINE_INTERFACE_METHOD_1("RequestVisibilityUpdate", void, ICmpRangeManager, RequestVisibilityUpdate, entity_id_t) DEFINE_INTERFACE_METHOD_1("SetLosCircular", void, ICmpRangeManager, SetLosCircular, bool) DEFINE_INTERFACE_METHOD_CONST_0("GetLosCircular", bool, ICmpRangeManager, GetLosCircular) DEFINE_INTERFACE_METHOD_2("SetSharedLos", void, ICmpRangeManager, SetSharedLos, player_id_t, std::vector) DEFINE_INTERFACE_METHOD_CONST_1("GetPercentMapExplored", u8, ICmpRangeManager, GetPercentMapExplored, player_id_t) DEFINE_INTERFACE_METHOD_CONST_1("GetUnionPercentMapExplored", u8, ICmpRangeManager, GetUnionPercentMapExplored, std::vector) END_INTERFACE_WRAPPER(RangeManager) Index: ps/trunk/source/simulation2/components/ICmpRangeManager.h =================================================================== --- ps/trunk/source/simulation2/components/ICmpRangeManager.h (revision 23768) +++ ps/trunk/source/simulation2/components/ICmpRangeManager.h (revision 23769) @@ -1,443 +1,452 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2020 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 . */ #ifndef INCLUDED_ICMPRANGEMANAGER #define INCLUDED_ICMPRANGEMANAGER #include "maths/FixedVector3D.h" #include "maths/FixedVector2D.h" #include "simulation2/system/Interface.h" +#include "simulation2/helpers/Grid.h" #include "simulation2/helpers/Position.h" #include "simulation2/helpers/Player.h" #include "graphics/Terrain.h" // for TERRAIN_TILE_SIZE class FastSpatialSubdivision; /** + * Since GetVisibility queries are run by the range manager + * other code using these must include ICmpRangeManager.h anyways, + * so define this enum here (Ideally, it'd be in its own header file, + * but adding header file does incur its own compilation time increase). + */ +enum class LosVisibility : u8 +{ + HIDDEN = 0, + FOGGED = 1, + VISIBLE = 2 +}; + +enum class LosState : u8 +{ + UNEXPLORED = 0, + EXPLORED = 1, + VISIBLE = 2, + MASK = 3 +}; + +/** * Provides efficient range-based queries of the game world, * and also LOS-based effects (fog of war). * * (These are somewhat distinct concepts but they share a lot of the implementation, * so for efficiency they're combined into this class.) * * Possible use cases: * - combat units need to detect targetable enemies entering LOS, so they can choose * to auto-attack. * - auras let a unit have some effect on all units (or those of the same player, or of enemies) * within a certain range. * - capturable animals need to detect when a player-owned unit is nearby and no units of other * players are in range. * - scenario triggers may want to detect when units enter a given area. * - units gathering from a resource that is exhausted need to find a new resource of the * same type, near the old one and reachable. * - projectile weapons with splash damage need to find all units within some distance * of the target point. * - ... * * In most cases the users are event-based and want notifications when something * has entered or left the range, and the query can be set up once and rarely changed. * These queries have to be fast. It's fine to approximate an entity as a point. * * Current design: * * This class handles just the most common parts of range queries: * distance, target interface, and player ownership. * The caller can then apply any more complex filtering that it needs. * * There are two types of query: * Passive queries are performed by ExecuteQuery and immediately return the matching entities. * Active queries are set up by CreateActiveQuery, and then a CMessageRangeUpdate message will be * sent to the entity once per turn if anybody has entered or left the range since the last RangeUpdate. * Queries can be disabled, in which case no message will be sent. */ class ICmpRangeManager : public IComponent { public: /** * External identifiers for active queries. */ typedef u32 tag_t; /** * Access the spatial subdivision kept by the range manager. * @return pointer to spatial subdivision structure. */ virtual FastSpatialSubdivision* GetSubdivision() = 0; /** * Set the bounds of the world. * Entities should not be outside the bounds (else efficiency will suffer). * @param x0,z0,x1,z1 Coordinates of the corners of the world * @param vertices Number of terrain vertices per side */ virtual void SetBounds(entity_pos_t x0, entity_pos_t z0, entity_pos_t x1, entity_pos_t z1, ssize_t vertices) = 0; /** * Execute a passive query. * @param source the entity around which the range will be computed. * @param minRange non-negative minimum distance in metres (inclusive). * @param maxRange non-negative maximum distance in metres (inclusive); or -1.0 to ignore distance. * @param owners list of player IDs that matching entities may have; -1 matches entities with no owner. * @param requiredInterface if non-zero, an interface ID that matching entities must implement. * @return list of entities matching the query, ordered by increasing distance from the source entity. */ virtual std::vector ExecuteQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, const std::vector& owners, int requiredInterface) = 0; /** * Execute a passive query. * @param pos the position around which the range will be computed. * @param minRange non-negative minimum distance in metres (inclusive). * @param maxRange non-negative maximum distance in metres (inclusive); or -1.0 to ignore distance. * @param owners list of player IDs that matching entities may have; -1 matches entities with no owner. * @param requiredInterface if non-zero, an interface ID that matching entities must implement. * @return list of entities matching the query, ordered by increasing distance from the source entity. */ virtual std::vector ExecuteQueryAroundPos(const CFixedVector2D& pos, entity_pos_t minRange, entity_pos_t maxRange, const std::vector& owners, int requiredInterface) = 0; /** * Construct an active query. The query will be disabled by default. * @param source the entity around which the range will be computed. * @param minRange non-negative minimum distance in metres (inclusive). * @param maxRange non-negative maximum distance in metres (inclusive); or -1.0 to ignore distance. * @param owners list of player IDs that matching entities may have; -1 matches entities with no owner. * @param requiredInterface if non-zero, an interface ID that matching entities must implement. * @param flags if a entity in range has one of the flags set it will show up. * @return unique non-zero identifier of query. */ virtual tag_t CreateActiveQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, const std::vector& owners, int requiredInterface, u8 flags) = 0; /** * Construct an active query of a paraboloic form around the unit. * The query will be disabled by default. * @param source the entity around which the range will be computed. * @param minRange non-negative minimum horizontal distance in metres (inclusive). MinRange doesn't do parabolic checks. * @param maxRange non-negative maximum distance in metres (inclusive) for units on the same elevation; * or -1.0 to ignore distance. * For units on a different elevation, a physical correct paraboloid with height=maxRange/2 above the unit is used to query them * @param elevationBonus extra bonus so the source can be placed higher and shoot further * @param owners list of player IDs that matching entities may have; -1 matches entities with no owner. * @param requiredInterface if non-zero, an interface ID that matching entities must implement. * @param flags if a entity in range has one of the flags set it will show up. * @return unique non-zero identifier of query. */ virtual tag_t CreateActiveParabolicQuery(entity_id_t source, entity_pos_t minRange, entity_pos_t maxRange, entity_pos_t elevationBonus, const std::vector& owners, int requiredInterface, u8 flags) = 0; /** * Get the average elevation over 8 points on distance range around the entity * @param id the entity id to look around * @param range the distance to compare terrain height with * @return a fixed number representing the average difference. It's positive when the entity is on average higher than the terrain surrounding it. */ virtual entity_pos_t GetElevationAdaptedRange(const CFixedVector3D& pos, const CFixedVector3D& rot, entity_pos_t range, entity_pos_t elevationBonus, entity_pos_t angle) const = 0; /** * Destroy a query and clean up resources. This must be called when an entity no longer needs its * query (e.g. when the entity is destroyed). * @param tag identifier of query. */ virtual void DestroyActiveQuery(tag_t tag) = 0; /** * Re-enable the processing of a query. * @param tag identifier of query. */ virtual void EnableActiveQuery(tag_t tag) = 0; /** * Disable the processing of a query (no RangeUpdate messages will be sent). * @param tag identifier of query. */ virtual void DisableActiveQuery(tag_t tag) = 0; /** * Check if the processing of a query is enabled. * @param tag identifier of a query. */ virtual bool IsActiveQueryEnabled(tag_t tag) const = 0; /** * Immediately execute a query, and re-enable it if disabled. * The next RangeUpdate message will say who has entered/left since this call, * so you won't miss any notifications. * @param tag identifier of query. * @return list of entities matching the query, ordered by increasing distance from the source entity. */ virtual std::vector ResetActiveQuery(tag_t tag) = 0; /** * Returns a list of all entities for a specific player. * (This is on this interface because it shares a lot of the implementation. * Maybe it should be extended to be more like ExecuteQuery without * the range parameter.) */ virtual std::vector GetEntitiesByPlayer(player_id_t player) const = 0; /** * Returns a list of all entities of all players except gaia. */ virtual std::vector GetNonGaiaEntities() const = 0; /** * Returns a list of all entities owned by a player or gaia. */ virtual std::vector GetGaiaAndNonGaiaEntities() const = 0; /** * Toggle the rendering of debug info. */ virtual void SetDebugOverlay(bool enabled) = 0; /** * Returns the mask for the specified identifier. */ virtual u8 GetEntityFlagMask(const std::string& identifier) const = 0; /** * Set the flag specified by the identifier to the supplied value for the entity * @param ent the entity whose flags will be modified. * @param identifier the flag to be modified. * @param value to which the flag will be set. */ virtual void SetEntityFlag(entity_id_t ent, const std::string& identifier, bool value) = 0; - // LOS interface: - - enum ELosState - { - LOS_UNEXPLORED = 0, - LOS_EXPLORED = 1, - LOS_VISIBLE = 2, - LOS_MASK = 3 - }; - - enum ELosVisibility - { - VIS_HIDDEN = 0, - VIS_FOGGED = 1, - VIS_VISIBLE = 2 - }; /** * Object providing efficient abstracted access to the LOS state. * This depends on some implementation details of CCmpRangeManager. * * This *ignores* the GetLosRevealAll flag - callers should check that explicitly. */ class CLosQuerier { private: friend class CCmpRangeManager; friend class TestLOSTexture; - CLosQuerier(u32 playerMask, const std::vector& data, ssize_t verticesPerSide) : - m_Data(&data[0]), m_PlayerMask(playerMask), m_VerticesPerSide(verticesPerSide) + CLosQuerier(u32 playerMask, const Grid& data, ssize_t verticesPerSide) : + m_Data(data), m_PlayerMask(playerMask), m_VerticesPerSide(verticesPerSide) { } const CLosQuerier& operator=(const CLosQuerier&); // not implemented public: /** * Returns whether the given vertex is visible (i.e. is within a unit's LOS). */ inline bool IsVisible(ssize_t i, ssize_t j) const { if (!(i >= 0 && j >= 0 && i < m_VerticesPerSide && j < m_VerticesPerSide)) return false; // Check high bit of each bit-pair - if ((m_Data[j*m_VerticesPerSide + i] & m_PlayerMask) & 0xAAAAAAAAu) + if ((m_Data.get(i, j) & m_PlayerMask) & 0xAAAAAAAAu) return true; else return false; } /** * Returns whether the given vertex is explored (i.e. was (or still is) within a unit's LOS). */ inline bool IsExplored(ssize_t i, ssize_t j) const { if (!(i >= 0 && j >= 0 && i < m_VerticesPerSide && j < m_VerticesPerSide)) return false; // Check low bit of each bit-pair - if ((m_Data[j*m_VerticesPerSide + i] & m_PlayerMask) & 0x55555555u) + if ((m_Data.get(i, j) & m_PlayerMask) & 0x55555555u) return true; else return false; } /** * Returns whether the given vertex is visible (i.e. is within a unit's LOS). * i and j must be in the range [0, verticesPerSide), else behaviour is undefined. */ inline bool IsVisible_UncheckedRange(ssize_t i, ssize_t j) const { #ifndef NDEBUG ENSURE(i >= 0 && j >= 0 && i < m_VerticesPerSide && j < m_VerticesPerSide); #endif // Check high bit of each bit-pair - if ((m_Data[j*m_VerticesPerSide + i] & m_PlayerMask) & 0xAAAAAAAAu) + if ((m_Data.get(i, j) & m_PlayerMask) & 0xAAAAAAAAu) return true; else return false; } /** * Returns whether the given vertex is explored (i.e. was (or still is) within a unit's LOS). * i and j must be in the range [0, verticesPerSide), else behaviour is undefined. */ inline bool IsExplored_UncheckedRange(ssize_t i, ssize_t j) const { #ifndef NDEBUG ENSURE(i >= 0 && j >= 0 && i < m_VerticesPerSide && j < m_VerticesPerSide); #endif // Check low bit of each bit-pair - if ((m_Data[j*m_VerticesPerSide + i] & m_PlayerMask) & 0x55555555u) + if ((m_Data.get(i, j) & m_PlayerMask) & 0x55555555u) return true; else return false; } private: u32 m_PlayerMask; - const u32* m_Data; + const Grid& m_Data; ssize_t m_VerticesPerSide; }; + ////////////////////////////////////////////////////////////////// + //// LOS interface below this line //// + ////////////////////////////////////////////////////////////////// /** * Returns a CLosQuerier for checking whether vertex positions are visible to the given player * (or other players it shares LOS with). */ virtual CLosQuerier GetLosQuerier(player_id_t player) const = 0; /** * Toggle the scripted Visibility component activation for entity ent. */ virtual void ActivateScriptedVisibility(entity_id_t ent, bool status) = 0; /** * Returns the visibility status of the given entity, with respect to the given player. - * Returns VIS_HIDDEN if the entity doesn't exist or is not in the world. + * Returns LosVisibility::HIDDEN if the entity doesn't exist or is not in the world. * This respects the GetLosRevealAll flag. */ - virtual ELosVisibility GetLosVisibility(CEntityHandle ent, player_id_t player) const = 0; - virtual ELosVisibility GetLosVisibility(entity_id_t ent, player_id_t player) const = 0; + virtual LosVisibility GetLosVisibility(CEntityHandle ent, player_id_t player) const = 0; + virtual LosVisibility GetLosVisibility(entity_id_t ent, player_id_t player) const = 0; /** * Returns the visibility status of the given position, with respect to the given player. * This respects the GetLosRevealAll flag. */ - virtual ELosVisibility GetLosVisibilityPosition(entity_pos_t x, entity_pos_t z, player_id_t player) const = 0; + virtual LosVisibility GetLosVisibilityPosition(entity_pos_t x, entity_pos_t z, player_id_t player) const = 0; /** * Request the update of the visibility cache of ent at next turn. * Typically used for fogging. */ virtual void RequestVisibilityUpdate(entity_id_t ent) = 0; /** * GetLosVisibility wrapped for script calls. * Returns "hidden", "fogged" or "visible". */ std::string GetLosVisibility_wrapper(entity_id_t ent, player_id_t player) const; /** * GetLosVisibilityPosition wrapped for script calls. * Returns "hidden", "fogged" or "visible". */ std::string GetLosVisibilityPosition_wrapper(entity_pos_t x, entity_pos_t z, player_id_t player) const; /** * Explore all tiles (but leave them in the FoW) for player p */ virtual void ExploreAllTiles(player_id_t p) = 0; /** * Explore the tiles inside each player's territory. * This is done only at the beginning of the game. */ virtual void ExploreTerritories() = 0; /** * Reveal the shore for specified player p. * This works like for entities: if RevealShore is called multiple times with enabled, it * will be necessary to call it the same number of times with !enabled to make the shore * fall back into the FoW. */ virtual void RevealShore(player_id_t p, bool enable) = 0; /** * Set whether the whole map should be made visible to the given player. * If player is -1, the map will be made visible to all players. */ virtual void SetLosRevealAll(player_id_t player, bool enabled) = 0; /** * Returns whether the whole map has been made visible to the given player. */ virtual bool GetLosRevealAll(player_id_t player) const = 0; /** * Set the LOS to be restricted to a circular map. */ virtual void SetLosCircular(bool enabled) = 0; /** * Returns whether the LOS is restricted to a circular map. */ virtual bool GetLosCircular() const = 0; /** * Sets shared LOS data for player to the given list of players. */ virtual void SetSharedLos(player_id_t player, const std::vector& players) = 0; /** * Returns shared LOS mask for player. */ virtual u32 GetSharedLosMask(player_id_t player) const = 0; /** * Get percent map explored statistics for specified player. */ virtual u8 GetPercentMapExplored(player_id_t player) const = 0; /** * Get percent map explored statistics for specified set of players. * Note: this function computes statistics from scratch and should not be called too often. */ virtual u8 GetUnionPercentMapExplored(const std::vector& players) const = 0; /** * Perform some internal consistency checks for testing/debugging. */ virtual void Verify() = 0; DECLARE_INTERFACE_TYPE(RangeManager) }; #endif // INCLUDED_ICMPRANGEMANAGER Index: ps/trunk/source/simulation2/components/ICmpVisibility.cpp =================================================================== --- ps/trunk/source/simulation2/components/ICmpVisibility.cpp (revision 23768) +++ ps/trunk/source/simulation2/components/ICmpVisibility.cpp (revision 23769) @@ -1,67 +1,67 @@ -/* Copyright (C) 2015 Wildfire Games. +/* Copyright (C) 2020 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 "ICmpVisibility.h" #include "simulation2/scripting/ScriptComponent.h" #include "simulation2/system/InterfaceScripted.h" BEGIN_INTERFACE_WRAPPER(Visibility) END_INTERFACE_WRAPPER(Visibility) class CCmpVisibilityScripted : public ICmpVisibility { public: DEFAULT_SCRIPT_WRAPPER(VisibilityScripted) virtual bool IsActivated() { return m_Script.Call("IsActivated"); } - virtual ICmpRangeManager::ELosVisibility GetVisibility(player_id_t player, bool isVisible, bool isExplored) + virtual LosVisibility GetVisibility(player_id_t player, bool isVisible, bool isExplored) { int visibility = m_Script.Call("GetVisibility", player, isVisible, isExplored); switch (visibility) { - case ICmpRangeManager::VIS_HIDDEN: - return ICmpRangeManager::VIS_HIDDEN; - case ICmpRangeManager::VIS_FOGGED: - return ICmpRangeManager::VIS_FOGGED; - case ICmpRangeManager::VIS_VISIBLE: - return ICmpRangeManager::VIS_VISIBLE; + case static_cast(LosVisibility::HIDDEN): + return LosVisibility::HIDDEN; + case static_cast(LosVisibility::FOGGED): + return LosVisibility::FOGGED; + case static_cast(LosVisibility::VISIBLE): + return LosVisibility::VISIBLE; default: LOGERROR("Received the invalid visibility value %d from the Visibility scripted component!", visibility); - return ICmpRangeManager::VIS_HIDDEN; + return LosVisibility::HIDDEN; } } virtual bool GetRetainInFog() { return m_Script.Call("GetRetainInFog"); } virtual bool GetAlwaysVisible() { return m_Script.Call("GetAlwaysVisible"); } }; REGISTER_COMPONENT_SCRIPT_WRAPPER(VisibilityScripted) Index: ps/trunk/source/simulation2/components/ICmpVisibility.h =================================================================== --- ps/trunk/source/simulation2/components/ICmpVisibility.h (revision 23768) +++ ps/trunk/source/simulation2/components/ICmpVisibility.h (revision 23769) @@ -1,54 +1,54 @@ -/* Copyright (C) 2015 Wildfire Games. +/* Copyright (C) 2020 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 . */ #ifndef INCLUDED_ICMPVISIBILITY #define INCLUDED_ICMPVISIBILITY #include "simulation2/system/Interface.h" #include "simulation2/components/ICmpRangeManager.h" /** * The Visibility component is a scripted component that allows any part of the simulation to * influence the visibility of an entity. * * This component: * - Holds the template values RetainInFog and AlwaysVisible, used by the range manager to compute * the visibility of the entity; * - Can supersede the range manager if it is "activated". This is to avoid useless calls to the scripts. */ class ICmpVisibility : public IComponent { public: /** * This function is a fallback for some entities whose visibility status * cannot be cached by the range manager (especially local entities like previews). * Calling the scripts is expensive, so only call it if really needed. */ virtual bool IsActivated() = 0; - virtual ICmpRangeManager::ELosVisibility GetVisibility(player_id_t player, bool isVisible, bool isExplored) = 0; + virtual LosVisibility GetVisibility(player_id_t player, bool isVisible, bool isExplored) = 0; virtual bool GetRetainInFog() = 0; virtual bool GetAlwaysVisible() = 0; DECLARE_INTERFACE_TYPE(Visibility) }; #endif // INCLUDED_ICMPVISIBILITY Index: ps/trunk/source/simulation2/helpers/Grid.h =================================================================== --- ps/trunk/source/simulation2/helpers/Grid.h (revision 23768) +++ ps/trunk/source/simulation2/helpers/Grid.h (revision 23769) @@ -1,290 +1,418 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2020 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 . */ #ifndef INCLUDED_GRID #define INCLUDED_GRID #include +#include "simulation2/serialization/IDeserializer.h" +#include "simulation2/serialization/ISerializer.h" + + #ifdef NDEBUG #define GRID_BOUNDS_DEBUG 0 #else #define GRID_BOUNDS_DEBUG 1 #endif +template +struct SerializedGridCompressed; + /** * Basic 2D array, intended for storing tile data, plus support for lazy updates * by ICmpObstructionManager. * @c T must be a POD type that can be initialised with 0s. */ template class Grid { + friend struct SerializedGridCompressed; +protected: + // Tag-dispatching internal utilities for convenience. + struct default_type{}; + struct is_pod { operator default_type() { return default_type{}; }}; + struct is_container { operator default_type() { return default_type{}; }}; + + // helper to detect value_type + template struct has_value_type : std::false_type { }; + template struct has_value_type (), 0)> : std::true_type { }; + + template using if_ = typename std::conditional::type; + + template + using dispatch = if_< std::is_pod, is_pod, + if_, is_container, + default_type>>; + public: - Grid() : m_W(0), m_H(0), m_Data(NULL), m_DirtyID(0) + Grid() : m_W(0), m_H(0), m_Data(NULL) { } - Grid(u16 w, u16 h) : m_W(w), m_H(h), m_Data(NULL), m_DirtyID(0) + Grid(u16 w, u16 h) : m_W(w), m_H(h), m_Data(NULL) { - if (m_W || m_H) - m_Data = new T[m_W * m_H]; - reset(); + resize(w, h); } - Grid(const Grid& g) : m_W(0), m_H(0), m_Data(NULL), m_DirtyID(0) + Grid(const Grid& g) : m_W(0), m_H(0), m_Data(NULL) { *this = g; } + using value_type = T; +public: + + // Ensure that o and this are the same size before calling. + void copy_data(T* o, default_type) { std::copy(o, o + m_H*m_W, &m_Data[0]); } + void copy_data(T* o, is_pod) { memcpy(m_Data, o, m_W*m_H*sizeof(T)); } + Grid& operator=(const Grid& g) { if (this == &g) return *this; - m_DirtyID = g.m_DirtyID; if (m_W == g.m_W && m_H == g.m_H) { - memcpy(m_Data, g.m_Data, m_W*m_H*sizeof(T)); + copy_data(g.m_Data, dispatch{}); return *this; } m_W = g.m_W; m_H = g.m_H; delete[] m_Data; if (g.m_Data) { m_Data = new T[m_W * m_H]; - memcpy(m_Data, g.m_Data, m_W*m_H*sizeof(T)); + copy_data(g.m_Data, dispatch{}); } else m_Data = NULL; return *this; } void swap(Grid& g) { - std::swap(m_DirtyID, g.m_DirtyID); std::swap(m_Data, g.m_Data); std::swap(m_H, g.m_H); std::swap(m_W, g.m_W); } ~Grid() { delete[] m_Data; } + // Ensure that o and this are the same size before calling. + bool compare_data(T* o, default_type) const { return std::equal(&m_Data[0], &m_Data[m_W*m_H], o); } + bool compare_data(T* o, is_pod) const { return memcmp(m_Data, o, m_W*m_H*sizeof(T)) == 0; } + bool operator==(const Grid& g) const { - if (!compare_sizes(&g) || m_DirtyID != g.m_DirtyID) + if (!compare_sizes(&g)) return false; - return memcmp(m_Data, g.m_Data, m_W*m_H*sizeof(T)) == 0; + return compare_data(g.m_Data, dispatch{}); } + bool operator!=(const Grid& g) const { return !(*this==g); } bool blank() const { return m_W == 0 && m_H == 0; } - bool any_set_in_square(int i0, int j0, int i1, int j1) const + u16 width() const { return m_W; }; + u16 height() const { return m_H; }; + + + bool _any_set_in_square(int, int, int, int, default_type) const + { + static_assert(!std::is_same::value, "Not implemented."); + return false; // Fix warnings. + } + bool _any_set_in_square(int i0, int j0, int i1, int j1, is_pod) const { - #if GRID_BOUNDS_DEBUG +#if GRID_BOUNDS_DEBUG ENSURE(i0 >= 0 && j0 >= 0 && i1 <= m_W && j1 <= m_H); - #endif +#endif for (int j = j0; j < j1; ++j) { int sum = 0; for (int i = i0; i < i1; ++i) sum += m_Data[j*m_W + i]; if (sum > 0) return true; } return false; } + bool any_set_in_square(int i0, int j0, int i1, int j1) const + { + return _any_set_in_square(i0, j0, i1, j1, dispatch{}); + } + + void reset_data(default_type) { std::fill(&m_Data[0], &m_Data[m_H*m_W], T{}); } + void reset_data(is_pod) { memset(m_Data, 0, m_W*m_H*sizeof(T)); } + void reset() { if (m_Data) - memset(m_Data, 0, m_W*m_H*sizeof(T)); + reset_data(dispatch{}); + } + + void resize(u16 w, u16 h) + { + if (m_Data) + delete[] m_Data; + m_W = w; + m_H = h; + if (m_W || m_H) + m_Data = new T[m_W * m_H]; + ENSURE(m_Data); + reset(); } // Add two grids of the same size void add(const Grid& g) { #if GRID_BOUNDS_DEBUG ENSURE(g.m_W == m_W && g.m_H == m_H); #endif for (int i=0; i < m_H*m_W; ++i) m_Data[i] += g.m_Data[i]; } void bitwise_or(const Grid& g) { if (this == &g) return; #if GRID_BOUNDS_DEBUG ENSURE(g.m_W == m_W && g.m_H == m_H); #endif for (int i = 0; i < m_H*m_W; ++i) m_Data[i] |= g.m_Data[i]; } void set(int i, int j, const T& value) { #if GRID_BOUNDS_DEBUG ENSURE(0 <= i && i < m_W && 0 <= j && j < m_H); #endif m_Data[j*m_W + i] = value; } + T& operator[](std::pair coords) { return get(coords.first, coords.second); } + T& get(std::pair coords) { return get(coords.first, coords.second); } + + T& operator[](std::pair coords) const { return get(coords.first, coords.second); } + T& get(std::pair coords) const { return get(coords.first, coords.second); } + + T& get(int i, int j) + { +#if GRID_BOUNDS_DEBUG + ENSURE(0 <= i && i < m_W && 0 <= j && j < m_H); +#endif + return m_Data[j*m_W + i]; + } + T& get(int i, int j) const { #if GRID_BOUNDS_DEBUG ENSURE(0 <= i && i < m_W && 0 <= j && j < m_H); #endif return m_Data[j*m_W + i]; } template bool compare_sizes(const Grid* g) const { return g && m_W == g->m_W && m_H == g->m_H; } u16 m_W, m_H; T* m_Data; +}; - size_t m_DirtyID; // if this is < the id maintained by ICmpObstructionManager then it needs to be updated + +/** + * Serialize a grid, applying a simple RLE compression that is assumed efficient. + */ +template +struct SerializedGridCompressed +{ + template + void operator()(ISerializer& serialize, const char* name, Grid& value) + { + size_t len = value.m_H * value.m_W; + serialize.NumberU16_Unbounded("width", value.m_W); + serialize.NumberU16_Unbounded("height", value.m_H); + if (len == 0) + return; + u32 count = 1; + T prevVal = value.m_Data[0]; + for (size_t i = 1; i < len; ++i) + { + if (prevVal == value.m_Data[i]) + { + count++; + continue; + } + serialize.NumberU32_Unbounded("#", count); + ELEM()(serialize, name, prevVal); + count = 1; + prevVal = value.m_Data[i]; + } + serialize.NumberU32_Unbounded("#", count); + ELEM()(serialize, name, prevVal); + } + + template + void operator()(IDeserializer& deserialize, const char* name, Grid& value) + { + u16 w, h; + deserialize.NumberU16_Unbounded("width", w); + deserialize.NumberU16_Unbounded("height", h); + u32 len = h * w; + value.resize(w, h); + for (size_t i = 0; i < len;) + { + u32 count; + deserialize.NumberU32_Unbounded("#", count); + T el; + ELEM()(deserialize, name, el); + std::fill(&value.m_Data[i], &value.m_Data[i+count], el); + i += count; + } + } }; + /** * Similar to Grid, except optimised for sparse usage (the grid is subdivided into * buckets whose contents are only initialised on demand, to save on memset cost). */ template class SparseGrid { NONCOPYABLE(SparseGrid); enum { BucketBits = 4, BucketSize = 1 << BucketBits }; T* GetBucket(int i, int j) { size_t b = (j >> BucketBits) * m_BW + (i >> BucketBits); if (!m_Data[b]) { m_Data[b] = new T[BucketSize*BucketSize](); } return m_Data[b]; } public: SparseGrid(u16 w, u16 h) : m_W(w), m_H(h), m_DirtyID(0) { ENSURE(m_W && m_H); m_BW = (u16)((m_W + BucketSize-1) >> BucketBits); m_BH = (u16)((m_H + BucketSize-1) >> BucketBits); m_Data = new T*[m_BW*m_BH](); } ~SparseGrid() { reset(); delete[] m_Data; } void reset() { for (size_t i = 0; i < (size_t)(m_BW*m_BH); ++i) delete[] m_Data[i]; // Reset m_Data by value-constructing in place with placement new. m_Data = new (m_Data) T*[m_BW*m_BH](); } void set(int i, int j, const T& value) { #if GRID_BOUNDS_DEBUG ENSURE(0 <= i && i < m_W && 0 <= j && j < m_H); #endif GetBucket(i, j)[(j % BucketSize)*BucketSize + (i % BucketSize)] = value; } T& get(int i, int j) { #if GRID_BOUNDS_DEBUG ENSURE(0 <= i && i < m_W && 0 <= j && j < m_H); #endif return GetBucket(i, j)[(j % BucketSize)*BucketSize + (i % BucketSize)]; } u16 m_W, m_H; u16 m_BW, m_BH; T** m_Data; size_t m_DirtyID; // if this is < the id maintained by ICmpObstructionManager then it needs to be updated }; /** * Structure holding grid dirtiness informations, for clever updates. */ struct GridUpdateInformation { bool dirty; bool globallyDirty; Grid dirtinessGrid; /** * Update the information with additionnal needed updates, then erase the source of additions. * This can usually be optimized through a careful memory management. */ void MergeAndClear(GridUpdateInformation& b) { ENSURE(dirtinessGrid.compare_sizes(&b.dirtinessGrid)); bool wasDirty = dirty; dirty |= b.dirty; globallyDirty |= b.globallyDirty; // If the current grid is useless, swap it if (!wasDirty) dirtinessGrid.swap(b.dirtinessGrid); // If the new grid isn't used, don't bother updating it else if (dirty && !globallyDirty) dirtinessGrid.bitwise_or(b.dirtinessGrid); b.Clean(); } /** * Mark everything as clean */ void Clean() { dirty = false; globallyDirty = false; dirtinessGrid.reset(); } }; #endif // INCLUDED_GRID Index: ps/trunk/source/simulation2/helpers/Selection.cpp =================================================================== --- ps/trunk/source/simulation2/helpers/Selection.cpp (revision 23768) +++ ps/trunk/source/simulation2/helpers/Selection.cpp (revision 23769) @@ -1,262 +1,262 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2020 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 "Selection.h" #include "graphics/Camera.h" #include "ps/CLogger.h" #include "ps/Profiler2.h" #include "simulation2/components/ICmpIdentity.h" #include "simulation2/components/ICmpOwnership.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/components/ICmpTemplateManager.h" #include "simulation2/components/ICmpSelectable.h" #include "simulation2/components/ICmpVisual.h" #include "simulation2/components/ICmpUnitRenderer.h" #include "simulation2/system/ComponentManager.h" entity_id_t EntitySelection::PickEntityAtPoint(CSimulation2& simulation, const CCamera& camera, int screenX, int screenY, player_id_t player, bool allowEditorSelectables) { PROFILE2("PickEntityAtPoint"); CVector3D origin, dir; camera.BuildCameraRay(screenX, screenY, origin, dir); CmpPtr cmpUnitRenderer(simulation.GetSimContext().GetSystemEntity()); ENSURE(cmpUnitRenderer); std::vector > entities; cmpUnitRenderer->PickAllEntitiesAtPoint(entities, origin, dir, allowEditorSelectables); if (entities.empty()) return INVALID_ENTITY; // Filter for relevent entities in the list of candidates (all entities below the mouse) std::vector > hits; // (dist^2, entity) pairs for (size_t i = 0; i < entities.size(); ++i) { // Find the perpendicular distance from the object's centre to the picker ray float dist2; const CVector3D center = entities[i].second; CVector3D closest = origin + dir * (center - origin).Dot(dir); dist2 = (closest - center).LengthSquared(); hits.emplace_back(dist2, entities[i].first); } // Sort hits by distance std::sort(hits.begin(), hits.end(), [](const std::pair& a, const std::pair& b) { return a.first < b.first; }); CmpPtr cmpRangeManager(simulation, SYSTEM_ENTITY); ENSURE(cmpRangeManager); for (size_t i = 0; i < hits.size(); ++i) { const CEntityHandle& handle = hits[i].second; CmpPtr cmpSelectable(handle); if (!cmpSelectable) continue; // Check if this entity is only selectable in Atlas if (!allowEditorSelectables && cmpSelectable->IsEditorOnly()) continue; // Ignore entities hidden by LOS (or otherwise hidden, e.g. when not IsInWorld) - if (cmpRangeManager->GetLosVisibility(handle, player) == ICmpRangeManager::VIS_HIDDEN) + if (cmpRangeManager->GetLosVisibility(handle, player) == LosVisibility::HIDDEN) continue; return handle.GetId(); } return INVALID_ENTITY; } /** * Returns true if the given entity is visible in the given screen area. * If the entity is a decorative, the function will only return true if allowEditorSelectables. */ bool CheckEntityInRect(CEntityHandle handle, const CCamera& camera, int sx0, int sy0, int sx1, int sy1, bool allowEditorSelectables) { // Check if this entity is only selectable in Atlas CmpPtr cmpSelectable(handle); if (!cmpSelectable || (!allowEditorSelectables && cmpSelectable->IsEditorOnly())) return false; // Find the current interpolated model position. // (We just use the centre position and not the whole bounding box, because maybe // that's better for users trying to select objects in busy areas) CmpPtr cmpVisual(handle); if (!cmpVisual) return false; CVector3D position = cmpVisual->GetPosition(); // Reject if it's not on-screen (e.g. it's behind the camera) if (!camera.GetFrustum().IsPointVisible(position)) return false; // Compare screen-space coordinates float x, y; camera.GetScreenCoordinates(position, x, y); int ix = (int)x; int iy = (int)y; return sx0 <= ix && ix <= sx1 && sy0 <= iy && iy <= sy1; } /** * Returns true if the given entity is visible to the given player and visible in the given screen area. */ static bool CheckEntityVisibleAndInRect(CEntityHandle handle, CmpPtr cmpRangeManager, const CCamera& camera, int sx0, int sy0, int sx1, int sy1, player_id_t player, bool allowEditorSelectables) { // Ignore entities hidden by LOS (or otherwise hidden, e.g. when not IsInWorld) - if (cmpRangeManager->GetLosVisibility(handle, player) == ICmpRangeManager::VIS_HIDDEN) + if (cmpRangeManager->GetLosVisibility(handle, player) == LosVisibility::HIDDEN) return false; return CheckEntityInRect(handle, camera, sx0, sy0, sx1, sy1, allowEditorSelectables); } std::vector EntitySelection::PickEntitiesInRect(CSimulation2& simulation, const CCamera& camera, int sx0, int sy0, int sx1, int sy1, player_id_t owner, bool allowEditorSelectables) { PROFILE2("PickEntitiesInRect"); // Make sure sx0 <= sx1, and sy0 <= sy1 if (sx0 > sx1) std::swap(sx0, sx1); if (sy0 > sy1) std::swap(sy0, sy1); CmpPtr cmpRangeManager(simulation, SYSTEM_ENTITY); ENSURE(cmpRangeManager); std::vector hitEnts; if (owner != INVALID_PLAYER) { CComponentManager& componentManager = simulation.GetSimContext().GetComponentManager(); std::vector ents = cmpRangeManager->GetEntitiesByPlayer(owner); for (std::vector::iterator it = ents.begin(); it != ents.end(); ++it) { if (CheckEntityVisibleAndInRect(componentManager.LookupEntityHandle(*it), cmpRangeManager, camera, sx0, sy0, sx1, sy1, owner, allowEditorSelectables)) hitEnts.push_back(*it); } } else // owner == INVALID_PLAYER; Used when selecting units in Atlas or other mods that allow all kinds of selectables to be selected. { const CSimulation2::InterfaceListUnordered& selectableEnts = simulation.GetEntitiesWithInterfaceUnordered(IID_Selectable); for (CSimulation2::InterfaceListUnordered::const_iterator it = selectableEnts.begin(); it != selectableEnts.end(); ++it) { if (CheckEntityVisibleAndInRect(it->second->GetEntityHandle(), cmpRangeManager, camera, sx0, sy0, sx1, sy1, owner, allowEditorSelectables)) hitEnts.push_back(it->first); } } return hitEnts; } std::vector EntitySelection::PickNonGaiaEntitiesInRect(CSimulation2& simulation, const CCamera& camera, int sx0, int sy0, int sx1, int sy1, bool allowEditorSelectables) { PROFILE2("PickNonGaiaEntitiesInRect"); // Make sure sx0 <= sx1, and sy0 <= sy1 if (sx0 > sx1) std::swap(sx0, sx1); if (sy0 > sy1) std::swap(sy0, sy1); CmpPtr cmpRangeManager(simulation, SYSTEM_ENTITY); ENSURE(cmpRangeManager); std::vector hitEnts; CComponentManager& componentManager = simulation.GetSimContext().GetComponentManager(); for (entity_id_t ent : cmpRangeManager->GetNonGaiaEntities()) if (CheckEntityInRect(componentManager.LookupEntityHandle(ent), camera, sx0, sy0, sx1, sy1, allowEditorSelectables)) hitEnts.push_back(ent); return hitEnts; } std::vector EntitySelection::PickSimilarEntities(CSimulation2& simulation, const CCamera& camera, const std::string& templateName, player_id_t owner, bool includeOffScreen, bool matchRank, bool allowEditorSelectables, bool allowFoundations) { PROFILE2("PickSimilarEntities"); CmpPtr cmpTemplateManager(simulation, SYSTEM_ENTITY); CmpPtr cmpRangeManager(simulation, SYSTEM_ENTITY); std::vector hitEnts; const CSimulation2::InterfaceListUnordered& ents = simulation.GetEntitiesWithInterfaceUnordered(IID_Selectable); for (CSimulation2::InterfaceListUnordered::const_iterator it = ents.begin(); it != ents.end(); ++it) { entity_id_t ent = it->first; CEntityHandle handle = it->second->GetEntityHandle(); // Check if this entity is only selectable in Atlas if (static_cast(it->second)->IsEditorOnly() && !allowEditorSelectables) continue; if (matchRank) { // Exact template name matching, optionally also allowing foundations std::string curTemplateName = cmpTemplateManager->GetCurrentTemplateName(ent); bool matches = (curTemplateName == templateName || (allowFoundations && curTemplateName.substr(0, 11) == "foundation|" && curTemplateName.substr(11) == templateName)); if (!matches) continue; } // Ignore entities hidden by LOS (or otherwise hidden, e.g. when not IsInWorld) // In this case, the checking is done to avoid selecting garrisoned units - if (cmpRangeManager->GetLosVisibility(handle, owner) == ICmpRangeManager::VIS_HIDDEN) + if (cmpRangeManager->GetLosVisibility(handle, owner) == LosVisibility::HIDDEN) continue; // Ignore entities not owned by 'owner' CmpPtr cmpOwnership(simulation.GetSimContext(), ent); if (owner != INVALID_PLAYER && (!cmpOwnership || cmpOwnership->GetOwner() != owner)) continue; // Ignore off screen entities if (!includeOffScreen) { // Find the current interpolated model position. CmpPtr cmpVisual(simulation.GetSimContext(), ent); if (!cmpVisual) continue; CVector3D position = cmpVisual->GetPosition(); // Reject if it's not on-screen (e.g. it's behind the camera) if (!camera.GetFrustum().IsPointVisible(position)) continue; } if (!matchRank) { // Match by selection group name // (This is relatively expensive since it involves script calls, so do it after all other tests) CmpPtr cmpIdentity(simulation.GetSimContext(), ent); if (!cmpIdentity || cmpIdentity->GetSelectionGroupName() != templateName) continue; } hitEnts.push_back(ent); } return hitEnts; } Index: ps/trunk/source/simulation2/serialization/SerializeTemplates.h =================================================================== --- ps/trunk/source/simulation2/serialization/SerializeTemplates.h (revision 23768) +++ ps/trunk/source/simulation2/serialization/SerializeTemplates.h (revision 23769) @@ -1,343 +1,314 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2020 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 . */ #ifndef INCLUDED_SERIALIZETEMPLATES #define INCLUDED_SERIALIZETEMPLATES /** * @file * Helper templates for serializing/deserializing common objects. */ #include "simulation2/components/ICmpPathfinder.h" #include "simulation2/serialization/IDeserializer.h" #include "simulation2/serialization/ISerializer.h" #include #include template -struct SerializeVector +struct SerializeArray { - template - void operator()(ISerializer& serialize, const char* name, std::vector& value) + template + void operator()(ISerializer& serialize, const char* name, std::array& value) { - size_t len = value.size(); - serialize.NumberU32_Unbounded("length", (u32)len); - for (size_t i = 0; i < len; ++i) + for (size_t i = 0; i < N; ++i) ELEM()(serialize, name, value[i]); } - template - void operator()(IDeserializer& deserialize, const char* name, std::vector& value) + template + void operator()(IDeserializer& deserialize, const char* name, std::array& value) { - value.clear(); - u32 len; - deserialize.NumberU32_Unbounded("length", len); - value.reserve(len); // TODO: watch out for out-of-memory - for (size_t i = 0; i < len; ++i) - { - T el; - ELEM()(deserialize, name, el); - value.push_back(el); - } + for (size_t i = 0; i < N; ++i) + ELEM()(deserialize, name, value[i]); } }; template -struct SerializeRepetitiveVector +struct SerializeVector { template void operator()(ISerializer& serialize, const char* name, std::vector& value) { size_t len = value.size(); serialize.NumberU32_Unbounded("length", (u32)len); - if (len == 0) - return; - u32 count = 1; - T prevVal = value[0]; - for (size_t i = 1; i < len; ++i) - { - if (prevVal == value[i]) - { - count++; - continue; - } - serialize.NumberU32_Unbounded("#", count); - ELEM()(serialize, name, prevVal); - count = 1; - prevVal = value[i]; - } - serialize.NumberU32_Unbounded("#", count); - ELEM()(serialize, name, prevVal); + for (size_t i = 0; i < len; ++i) + ELEM()(serialize, name, value[i]); } template void operator()(IDeserializer& deserialize, const char* name, std::vector& value) { value.clear(); u32 len; deserialize.NumberU32_Unbounded("length", len); value.reserve(len); // TODO: watch out for out-of-memory - for (size_t i = 0; i < len;) + for (size_t i = 0; i < len; ++i) { - u32 count; - deserialize.NumberU32_Unbounded("#", count); T el; ELEM()(deserialize, name, el); - i += count; - value.insert(value.end(), count, el); + value.emplace_back(el); } } }; template struct SerializeSet { template void operator()(ISerializer& serialize, const char* name, const std::set& value) { serialize.NumberU32_Unbounded("size", static_cast(value.size())); for (const T& elem : value) ELEM()(serialize, name, elem); } template void operator()(IDeserializer& deserialize, const char* name, std::set& value) { value.clear(); u32 size; deserialize.NumberU32_Unbounded("size", size); for (size_t i = 0; i < size; ++i) { T el; ELEM()(deserialize, name, el); value.emplace(std::move(el)); } } }; template struct SerializeMap { template void operator()(ISerializer& serialize, const char* UNUSED(name), std::map& value) { size_t len = value.size(); serialize.NumberU32_Unbounded("length", (u32)len); for (typename std::map::iterator it = value.begin(); it != value.end(); ++it) { KS()(serialize, "key", it->first); VS()(serialize, "value", it->second); } } template void operator()(ISerializer& serialize, const char* UNUSED(name), std::map& value, C& context) { size_t len = value.size(); serialize.NumberU32_Unbounded("length", (u32)len); for (typename std::map::iterator it = value.begin(); it != value.end(); ++it) { KS()(serialize, "key", it->first); VS()(serialize, "value", it->second, context); } } template void operator()(IDeserializer& deserialize, const char* UNUSED(name), M& value) { typedef typename M::key_type K; typedef typename M::value_type::second_type V; // M::data_type gives errors with gcc value.clear(); u32 len; deserialize.NumberU32_Unbounded("length", len); for (size_t i = 0; i < len; ++i) { K k; V v; KS()(deserialize, "key", k); VS()(deserialize, "value", v); value.emplace(std::move(k), std::move(v)); } } template void operator()(IDeserializer& deserialize, const char* UNUSED(name), M& value, C& context) { typedef typename M::key_type K; typedef typename M::value_type::second_type V; // M::data_type gives errors with gcc value.clear(); u32 len; deserialize.NumberU32_Unbounded("length", len); for (size_t i = 0; i < len; ++i) { K k; V v; KS()(deserialize, "key", k); VS()(deserialize, "value", v, context); value.emplace(std::move(k), std::move(v)); } } }; // We have to order the map before serializing to make things consistent template struct SerializeUnorderedMap { template void operator()(ISerializer& serialize, const char* name, std::unordered_map& value) { std::map ordered_value(value.begin(), value.end()); SerializeMap()(serialize, name, ordered_value); } template void operator()(IDeserializer& deserialize, const char* name, std::unordered_map& value) { SerializeMap()(deserialize, name, value); } }; template struct SerializeU8_Enum { void operator()(ISerializer& serialize, const char* name, T value) { serialize.NumberU8(name, value, 0, max); } void operator()(IDeserializer& deserialize, const char* name, T& value) { u8 val; deserialize.NumberU8(name, val, 0, max); value = static_cast(val); } }; struct SerializeU8_Unbounded { void operator()(ISerializer& serialize, const char* name, u8 value) { serialize.NumberU8_Unbounded(name, value); } void operator()(IDeserializer& deserialize, const char* name, u8& value) { deserialize.NumberU8_Unbounded(name, value); } }; struct SerializeU16_Unbounded { void operator()(ISerializer& serialize, const char* name, u16 value) { serialize.NumberU16_Unbounded(name, value); } void operator()(IDeserializer& deserialize, const char* name, u16& value) { deserialize.NumberU16_Unbounded(name, value); } }; struct SerializeU32_Unbounded { void operator()(ISerializer& serialize, const char* name, u32 value) { serialize.NumberU32_Unbounded(name, value); } void operator()(IDeserializer& deserialize, const char* name, u32& value) { deserialize.NumberU32_Unbounded(name, value); } }; struct SerializeI32_Unbounded { void operator()(ISerializer& serialize, const char* name, i32 value) { serialize.NumberI32_Unbounded(name, value); } void operator()(IDeserializer& deserialize, const char* name, i32& value) { deserialize.NumberI32_Unbounded(name, value); } }; struct SerializeBool { void operator()(ISerializer& serialize, const char* name, bool value) { serialize.Bool(name, value); } void operator()(IDeserializer& deserialize, const char* name, bool& value) { deserialize.Bool(name, value); } }; struct SerializeString { void operator()(ISerializer& serialize, const char* name, const std::string& value) { serialize.StringASCII(name, value, 0, UINT32_MAX); } void operator()(IDeserializer& deserialize, const char* name, std::string& value) { deserialize.StringASCII(name, value, 0, UINT32_MAX); } }; struct SerializeWaypoint { void operator()(ISerializer& serialize, const char* UNUSED(name), const Waypoint& value) { serialize.NumberFixed_Unbounded("waypoint x", value.x); serialize.NumberFixed_Unbounded("waypoint z", value.z); } void operator()(IDeserializer& deserialize, const char* UNUSED(name), Waypoint& value) { deserialize.NumberFixed_Unbounded("waypoint x", value.x); deserialize.NumberFixed_Unbounded("waypoint z", value.z); } }; struct SerializeGoal { template void operator()(S& serialize, const char* UNUSED(name), PathGoal& value) { SerializeU8_Enum()(serialize, "type", value.type); serialize.NumberFixed_Unbounded("goal x", value.x); serialize.NumberFixed_Unbounded("goal z", value.z); serialize.NumberFixed_Unbounded("goal u x", value.u.X); serialize.NumberFixed_Unbounded("goal u z", value.u.Y); serialize.NumberFixed_Unbounded("goal v x", value.v.X); serialize.NumberFixed_Unbounded("goal v z", value.v.Y); serialize.NumberFixed_Unbounded("goal hw", value.hw); serialize.NumberFixed_Unbounded("goal hh", value.hh); serialize.NumberFixed_Unbounded("maxdist", value.maxdist); } }; #endif // INCLUDED_SERIALIZETEMPLATES Index: ps/trunk/source/simulation2/tests/test_SerializeTemplates.h =================================================================== --- ps/trunk/source/simulation2/tests/test_SerializeTemplates.h (revision 23768) +++ ps/trunk/source/simulation2/tests/test_SerializeTemplates.h (revision 23769) @@ -1,56 +1,89 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2020 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" #include "scriptinterface/ScriptInterface.h" +#include "simulation2/helpers/Grid.h" #include "simulation2/serialization/DebugSerializer.h" #include "simulation2/serialization/SerializeTemplates.h" #include #include #include class TestSerializeTemplates : public CxxTest::TestSuite { public: + void test_Debug_array() + { + ScriptInterface script("Test", "Test", g_ScriptRuntime); + std::stringstream stream; + + CDebugSerializer serialize(script, stream); + std::array value = { + 3, 0, 1, 4, 1, 5 + }; + SerializeArray()(serialize, "E", value); + TS_ASSERT_STR_EQUALS(stream.str(), "E: 3\nE: 0\nE: 1\nE: 4\nE: 1\nE: 5\n"); + } + void test_Debug_vector() { ScriptInterface script("Test", "Test", g_ScriptRuntime); std::stringstream stream; CDebugSerializer serialize(script, stream); std::vector value = { 3, 0, 1, 4, 1, 5 }; SerializeVector()(serialize, "E", value); TS_ASSERT_STR_EQUALS(stream.str(), "length: 6\nE: 3\nE: 0\nE: 1\nE: 4\nE: 1\nE: 5\n"); } void test_Debug_set() { ScriptInterface script("Test", "Test", g_ScriptRuntime); std::stringstream stream; CDebugSerializer serialize(script, stream); std::set value = { 3, 0, 1, 4, 1, 5 }; SerializeSet()(serialize, "E", value); TS_ASSERT_STR_EQUALS(stream.str(), "size: 5\nE: 0\nE: 1\nE: 3\nE: 4\nE: 5\n"); } + + void test_Debug_grid() + { + ScriptInterface script("Test", "Test", g_ScriptRuntime); + std::stringstream stream; + + CDebugSerializer serialize(script, stream); + Grid value; + value.resize(3,2); + // Checkerboard pattern. + for (u8 j = 0; j < value.height(); ++j) + for (u8 i = 0; i < value.width(); ++i) + value.set(i, j, ((i % 2) + (j % 2)) % 2); + + SerializedGridCompressed()(serialize, "E", value); + TS_ASSERT_STR_EQUALS(stream.str(), "width: 3\nheight: 2\n" + "#: 1\nE: 0\n#: 1\nE: 1\n#: 1\nE: 0\n" + "#: 1\nE: 1\n#: 1\nE: 0\n#: 1\nE: 1\n"); + } };