1
0
forked from mirrors/0ad

Revamp CinemaManager component

Clean up the the implementation, improve the naming, and
add some more documentation as well as more in-depth tests.
This commit is contained in:
Vantha
2026-02-24 10:31:19 +01:00
committed by Vantha
parent 1d3cdec48d
commit d882ab74a1
11 changed files with 285 additions and 183 deletions
@@ -1,22 +1,21 @@
Trigger.prototype.CounterMessage = function(data) Trigger.prototype.CounterMessage = function(data)
{ {
var cmpGUIInterface = Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface); Engine.QueryInterface(SYSTEM_ENTITY, IID_GuiInterface)?.PushNotification({
cmpGUIInterface.PushNotification({
"players": [1, 2], "players": [1, 2],
"message": markForTranslation("Cutscene starts after 5 seconds"), "message": markForTranslation("Cutscene starts after 5 seconds"),
"translateMessage": true "translateMessage": true
}); });
}; };
Trigger.prototype.StartCutscene = function(data) Trigger.prototype.StartCutscene = function()
{ {
var cmpCinemaManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CinemaManager); const cmpCinemaManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CinemaManager);
if (!cmpCinemaManager) if (!cmpCinemaManager)
return; return;
cmpCinemaManager.AddCinemaPathToQueue("test"); cmpCinemaManager.PushPathToQueue("test");
cmpCinemaManager.Play(); cmpCinemaManager.StartPlayingQueue();
}; };
var cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger); const cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
cmpTrigger.DoAfterDelay(1000, "CounterMessage", {}); cmpTrigger.DoAfterDelay(1000, "CounterMessage", {});
cmpTrigger.DoAfterDelay(6000, "StartCutscene", {}); cmpTrigger.DoAfterDelay(5000, "StartCutscene", {});
@@ -155,7 +155,7 @@ GuiInterface.prototype.GetSimulationState = function()
const cmpCinemaManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CinemaManager); const cmpCinemaManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CinemaManager);
if (cmpCinemaManager) if (cmpCinemaManager)
ret.cinemaPlaying = cmpCinemaManager.IsPlaying(); ret.cinemaPlaying = cmpCinemaManager.IsPlayingQueue();
const cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager); const cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
ret.victoryConditions = cmpEndGameManager.GetVictoryConditions(); ret.victoryConditions = cmpEndGameManager.GetVictoryConditions();
+4 -9
View File
@@ -51,12 +51,12 @@ void CCinemaManager::Update(const float deltaRealTime) const
return; return;
if (IsPlaying()) if (IsPlaying())
cmpCinemaManager->PlayQueue(deltaRealTime, g_Game->GetView()->GetCamera()); cmpCinemaManager->UpdateActivePath(deltaRealTime, g_Game->GetView()->GetCamera());
} }
void CCinemaManager::Render(Renderer::Backend::IDeviceCommandContext& deviceCommandContext) const void CCinemaManager::Render(Renderer::Backend::IDeviceCommandContext& deviceCommandContext) const
{ {
if (!IsEnabled() && m_DrawPaths) if (!IsPlaying() && m_DrawPaths)
DrawPaths(deviceCommandContext); DrawPaths(deviceCommandContext);
} }
@@ -121,15 +121,10 @@ void CCinemaManager::DrawNodes(Renderer::Backend::IDeviceCommandContext& deviceC
} }
} }
bool CCinemaManager::IsEnabled() const
{
CmpPtr<ICmpCinemaManager> cmpCinemaManager(g_Game->GetSimulation2()->GetSimContext().GetSystemEntity());
return cmpCinemaManager && cmpCinemaManager->IsEnabled();
}
bool CCinemaManager::IsPlaying() const bool CCinemaManager::IsPlaying() const
{ {
return IsEnabled() && g_Game && !g_Game->m_Paused; CmpPtr<ICmpCinemaManager> cmpCinemaManager(g_Game->GetSimulation2()->GetSimContext().GetSystemEntity());
return cmpCinemaManager && cmpCinemaManager->IsPlayingQueue();
} }
bool CCinemaManager::GetPathsDrawing() const bool CCinemaManager::GetPathsDrawing() const
-1
View File
@@ -39,7 +39,6 @@ public:
void Render(Renderer::Backend::IDeviceCommandContext& deviceCommandContext) const; void Render(Renderer::Backend::IDeviceCommandContext& deviceCommandContext) const;
bool IsPlaying() const; bool IsPlaying() const;
bool IsEnabled() const;
/** /**
* Updates CCinemManager and current path * Updates CCinemManager and current path
+5 -6
View File
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Wildfire Games. /* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D. * This file is part of 0 A.D.
* *
* 0 A.D. is free software: you can redistribute it and/or modify * 0 A.D. is free software: you can redistribute it and/or modify
@@ -291,6 +291,9 @@ void CGameView::Update(const float deltaRealTime)
{ {
m->MiniMapTexture.Update(deltaRealTime); m->MiniMapTexture.Update(deltaRealTime);
m->CinemaManager.Update(deltaRealTime);
if (m->CinemaManager.IsPlaying())
return;
// If camera movement is being handled by the touch-input system, // If camera movement is being handled by the touch-input system,
// then we should stop to avoid conflicting with it // then we should stop to avoid conflicting with it
if (g_TouchInput.IsEnabled()) if (g_TouchInput.IsEnabled())
@@ -299,10 +302,6 @@ void CGameView::Update(const float deltaRealTime)
if (!g_app_has_focus) if (!g_app_has_focus)
return; return;
m->CinemaManager.Update(deltaRealTime);
if (m->CinemaManager.IsEnabled())
return;
m->CameraController->Update(deltaRealTime); m->CameraController->Update(deltaRealTime);
} }
@@ -354,7 +353,7 @@ entity_id_t CGameView::GetFollowedEntity()
InReaction game_view_handler(const SDL_Event_* ev) InReaction game_view_handler(const SDL_Event_* ev)
{ {
// put any events that must be processed even if inactive here // put any events that must be processed even if inactive here
if (!g_app_has_focus || !g_Game || !g_Game->IsGameStarted() || g_Game->GetView()->GetCinema()->IsEnabled()) if (!g_app_has_focus || !g_Game || !g_Game->IsGameStarted() || g_Game->GetView()->GetCinema()->IsPlaying())
return IN_PASS; return IN_PASS;
CGameView *pView=g_Game->GetView(); CGameView *pView=g_Game->GetView();
@@ -26,11 +26,10 @@
#include "ps/CLogger.h" #include "ps/CLogger.h"
#include "ps/CStr.h" #include "ps/CStr.h"
#include "ps/Game.h" #include "ps/Game.h"
#include "renderer/Renderer.h"
#include "renderer/SceneRenderer.h"
#include "simulation2/MessageTypes.h" #include "simulation2/MessageTypes.h"
#include "simulation2/components/ICmpOverlayRenderer.h"
#include "simulation2/components/ICmpRangeManager.h" #include "simulation2/components/ICmpRangeManager.h"
#include "simulation2/components/ICmpSelectable.h"
#include "simulation2/components/ICmpTerritoryManager.h"
#include "simulation2/helpers/CinemaPath.h" #include "simulation2/helpers/CinemaPath.h"
#include "simulation2/system/Component.h" #include "simulation2/system/Component.h"
#include "simulation2/system/Entity.h" #include "simulation2/system/Entity.h"
@@ -50,6 +49,7 @@ public:
static void ClassInit(CComponentManager& componentManager) static void ClassInit(CComponentManager& componentManager)
{ {
componentManager.SubscribeToMessageType(MT_Update); componentManager.SubscribeToMessageType(MT_Update);
componentManager.SubscribeToMessageType(MT_Deserialized);
} }
DEFAULT_COMPONENT_ALLOCATOR(CinemaManager) DEFAULT_COMPONENT_ALLOCATOR(CinemaManager)
@@ -61,11 +61,11 @@ public:
void Init(const CParamNode&) override void Init(const CParamNode&) override
{ {
m_Enabled = false; m_IsPlayingPathQueue = false;
m_MapRevealed = false; m_QueuePlayingElapsedTime = fixed::Zero();
m_ElapsedTime = fixed::Zero(); m_PathQueueDuration = fixed::Zero();
m_TotalTime = fixed::Zero(); m_ActivePathElapsedTime = fixed::Zero();
m_CurrentPathElapsedTime = fixed::Zero(); m_WasMapRevealed = false;
} }
void Deinit() override void Deinit() override
@@ -74,10 +74,10 @@ public:
void Serialize(ISerializer& serializer) override void Serialize(ISerializer& serializer) override
{ {
serializer.Bool("Enabled", m_Enabled); serializer.Bool("IsPlayingPathQueue", m_IsPlayingPathQueue);
serializer.NumberFixed_Unbounded("ElapsedTime", m_ElapsedTime); serializer.NumberFixed_Unbounded("QueueElapsedTime", m_QueuePlayingElapsedTime);
serializer.NumberFixed_Unbounded("CurrentPathElapsedTime", m_CurrentPathElapsedTime); serializer.NumberFixed_Unbounded("CurrentPathElapsedTime", m_ActivePathElapsedTime);
serializer.Bool("MapRevealed", m_MapRevealed); serializer.Bool("WasMapRevealed", m_WasMapRevealed);
serializer.NumberU32_Unbounded("NumberOfPaths", m_Paths.size()); serializer.NumberU32_Unbounded("NumberOfPaths", m_Paths.size());
for (const std::pair<const CStrW, CCinemaPath>& it : m_Paths) for (const std::pair<const CStrW, CCinemaPath>& it : m_Paths)
@@ -85,15 +85,15 @@ public:
serializer.NumberU32_Unbounded("NumberOfQueuedPaths", m_PathQueue.size()); serializer.NumberU32_Unbounded("NumberOfQueuedPaths", m_PathQueue.size());
for (const CCinemaPath& path : m_PathQueue) for (const CCinemaPath& path : m_PathQueue)
serializer.String("PathName", path.GetName(), 1, 128); serializer.String("QueuedPathName", path.GetName(), 1, 128);
} }
void Deserialize(const CParamNode&, IDeserializer& deserializer) override void Deserialize(const CParamNode&, IDeserializer& deserializer) override
{ {
deserializer.Bool("Enabled", m_Enabled); deserializer.Bool("IsPlayingPathQueue", m_IsPlayingPathQueue);
deserializer.NumberFixed_Unbounded("ElapsedTime", m_ElapsedTime); deserializer.NumberFixed_Unbounded("QueueElapsedTime", m_QueuePlayingElapsedTime);
deserializer.NumberFixed_Unbounded("CurrentPathElapsedTime", m_CurrentPathElapsedTime); deserializer.NumberFixed_Unbounded("CurrentPathElapsedTime", m_ActivePathElapsedTime);
deserializer.Bool("MapRevealed", m_MapRevealed); deserializer.Bool("WasMapRevealed", m_WasMapRevealed);
uint32_t numberOfPaths = 0; uint32_t numberOfPaths = 0;
deserializer.NumberU32_Unbounded("NumberOfPaths", numberOfPaths); deserializer.NumberU32_Unbounded("NumberOfPaths", numberOfPaths);
@@ -108,55 +108,53 @@ public:
for (uint32_t i = 0; i < numberOfQueuedPaths; ++i) for (uint32_t i = 0; i < numberOfQueuedPaths; ++i)
{ {
CStrW pathName; CStrW pathName;
deserializer.String("PathName", pathName, 1, 128); deserializer.String("QueuedPathName", pathName, 1, 128);
ENSURE(HasPath(pathName)); ENSURE(HasPath(pathName));
AddCinemaPathToQueue(pathName); PushPathToQueue(pathName);
} }
if (!m_PathQueue.empty()) if (!m_PathQueue.empty())
{ {
m_PathQueue.front().m_TimeElapsed = m_CurrentPathElapsedTime.ToFloat(); m_PathQueue.front().m_TimeElapsed = m_ActivePathElapsedTime.ToFloat();
m_PathQueue.front().Validate(); m_PathQueue.front().Validate();
} }
SetEnabled(m_Enabled);
} }
void HandleMessage(const CMessage& msg, bool /*global*/) override void HandleMessage(const CMessage& msg, bool /*global*/) override
{ {
switch (msg.GetType()) switch (msg.GetType())
{ {
case MT_Deserialized:
if (!m_IsPlayingPathQueue)
break;
m_IsPlayingPathQueue = false;
StartPlayingQueue();
break;
case MT_Update: case MT_Update:
{ {
const CMessageUpdate &msgData = static_cast<const CMessageUpdate&>(msg); const CMessageUpdate &msgData = static_cast<const CMessageUpdate&>(msg);
if (!m_Enabled) if (!m_IsPlayingPathQueue)
break; break;
// The paths play at a fixed speed, no matter the sim rate. // The paths play at a fixed speed, no matter the sim rate.
// The turn length we have received here, however, is scaled by that rate. // The turn length we have received here, however, is scaled by that rate.
const fixed realTurnLength{msgData.turnLength / fixed::FromFloat(g_Game ? g_Game->GetSimRate() : 1.0f)}; const fixed realTurnLength{msgData.turnLength / fixed::FromFloat(g_Game ? g_Game->GetSimRate() : 1.0f)};
m_ElapsedTime += realTurnLength; m_QueuePlayingElapsedTime += realTurnLength;
m_CurrentPathElapsedTime += realTurnLength; m_ActivePathElapsedTime += realTurnLength;
if (m_CurrentPathElapsedTime >= m_PathQueue.front().GetDuration()) if (m_ActivePathElapsedTime >= m_PathQueue.front().GetDuration())
{ {
CMessageCinemaPathEnded msgCinemaPathEnded(m_PathQueue.front().GetName()); CMessageCinemaPathEnded msgCinemaPathEnded(m_PathQueue.front().GetName());
m_PathQueue.pop_front(); m_PathQueue.pop_front();
GetSimContext().GetComponentManager().PostMessage(SYSTEM_ENTITY, msgCinemaPathEnded); GetSimContext().GetComponentManager().PostMessage(SYSTEM_ENTITY, msgCinemaPathEnded);
m_CurrentPathElapsedTime = fixed::Zero(); m_ActivePathElapsedTime = fixed::Zero();
if (!m_PathQueue.empty()) if (!m_PathQueue.empty())
m_PathQueue.front().Reset(); m_PathQueue.front().Reset();
} }
if (m_ElapsedTime >= m_TotalTime) if (m_QueuePlayingElapsedTime >= m_PathQueueDuration)
{ StopPlayingQueue();
m_CurrentPathElapsedTime = fixed::Zero();
m_ElapsedTime = fixed::Zero();
m_TotalTime = fixed::Zero();
SetEnabled(false);
GetSimContext().GetComponentManager().PostMessage(SYSTEM_ENTITY, CMessageCinemaQueueEnded());
}
break; break;
} }
default: default:
@@ -168,57 +166,28 @@ public:
{ {
if (m_Paths.find(path.GetName()) != m_Paths.end()) if (m_Paths.find(path.GetName()) != m_Paths.end())
{ {
LOGWARNING("Path with name '%s' already exists", path.GetName().ToUTF8()); LOGWARNING("Cinema path with name '%s' already exists", path.GetName().ToUTF8());
return; return;
} }
m_Paths[path.GetName()] = path; m_Paths[path.GetName()] = path;
} }
void AddCinemaPathToQueue(const CStrW& name) override
{
if (!HasPath(name))
{
LOGWARNING("Path with name '%s' doesn't exist", name.ToUTF8());
return;
}
m_PathQueue.push_back(m_Paths[name]);
if (m_PathQueue.size() == 1)
m_PathQueue.front().Reset();
m_TotalTime += m_Paths[name].GetDuration();
}
void Play() override
{
SetEnabled(true);
}
void Stop() override
{
SetEnabled(false);
}
bool HasPath(const CStrW& name) const override
{
return m_Paths.find(name) != m_Paths.end();
}
void ClearQueue() override
{
m_PathQueue.clear();
}
void DeletePath(const CStrW& name) override void DeletePath(const CStrW& name) override
{ {
if (!HasPath(name)) if (!HasPath(name))
{ {
LOGWARNING("Path with name '%s' doesn't exist", name.ToUTF8()); LOGWARNING("Cinema path with name '%s' doesn't exist", name.ToUTF8());
return; return;
} }
m_PathQueue.remove_if([name](const CCinemaPath& path) { return path.GetName() == name; }); m_PathQueue.remove_if([name](const CCinemaPath& path) { return path.GetName() == name; });
m_Paths.erase(name); m_Paths.erase(name);
} }
bool HasPath(const CStrW& name) const override
{
return m_Paths.find(name) != m_Paths.end();
}
const std::map<CStrW, CCinemaPath>& GetPaths() const override const std::map<CStrW, CCinemaPath>& GetPaths() const override
{ {
return m_Paths; return m_Paths;
@@ -229,41 +198,85 @@ public:
m_Paths = newPaths; m_Paths = newPaths;
} }
const std::list<CCinemaPath>& GetQueue() const override void PushPathToQueue(const CStrW& name) override
{ {
return m_PathQueue; if (!HasPath(name))
{
LOGWARNING("Cinema path with name '%s' doesn't exist", name.ToUTF8());
return;
}
m_PathQueue.push_back(m_Paths[name]);
if (m_PathQueue.size() == 1)
m_PathQueue.front().Reset();
m_PathQueueDuration += m_Paths[name].GetDuration();
} }
bool IsEnabled() const override void ClearQueue() override
{ {
return m_Enabled; m_PathQueue.clear();
} }
void SetEnabled(bool enabled) override void StartPlayingQueue() override
{ {
if (m_PathQueue.empty() && enabled) if (m_IsPlayingPathQueue || m_PathQueue.empty())
enabled = false;
if (m_Enabled == enabled)
return; return;
CmpPtr<ICmpRangeManager> cmpRangeManager(GetSimContext().GetSystemEntity()); CmpPtr<ICmpRangeManager> cmpRangeManager(GetSimContext().GetSystemEntity());
if (cmpRangeManager) if (cmpRangeManager)
{ {
if (enabled) m_WasMapRevealed = cmpRangeManager->GetLosRevealWholeMapForAll();
m_MapRevealed = cmpRangeManager->GetLosRevealWholeMapForAll(); // Note: this results in all fogged entities seen during the cinema path being revealed/updated in FOW
// TODO: improve m_MapRevealed state and without fade in // after the queue has ended.
cmpRangeManager->SetLosRevealWholeMapForAll(enabled); cmpRangeManager->SetLosRevealWholeMapForAll(true);
} }
m_Enabled = enabled; m_IsPlayingPathQueue = true;
} }
void PlayQueue(const float deltaRealTime, CCamera* camera) override void StopPlayingQueue() override
{ {
if (m_PathQueue.empty()) if (!m_IsPlayingPathQueue)
return; return;
m_PathQueue.front().Play(deltaRealTime, camera);
CmpPtr<ICmpRangeManager> cmpRangeManager(GetSimContext().GetSystemEntity());
if (cmpRangeManager)
cmpRangeManager->SetLosRevealWholeMapForAll(m_WasMapRevealed);
GetSimContext().GetComponentManager().PostMessage(SYSTEM_ENTITY, CMessageCinemaQueueEnded());
m_ActivePathElapsedTime = fixed::Zero();
m_QueuePlayingElapsedTime = fixed::Zero();
m_PathQueueDuration = fixed::Zero();
for (const CCinemaPath& path : m_PathQueue)
m_PathQueueDuration += path.GetDuration();
m_IsPlayingPathQueue = false;
}
bool IsPlayingQueue() const override
{
return m_IsPlayingPathQueue;
}
void UpdateActivePath(const float deltaRealTime, CCamera* camera) override
{
if (m_IsPlayingPathQueue)
{
if (m_PathQueue.empty())
StopPlayingQueue();
else
m_PathQueue.front().Play(deltaRealTime, camera);
}
}
const CStrW GetActivePath() const override
{
return m_IsPlayingPathQueue ? m_PathQueue.front().GetName() : CStrW();
}
const fixed GetActivePathElapsedTime() const override
{
return m_ActivePathElapsedTime;
} }
private: private:
@@ -364,16 +377,22 @@ private:
return CCinemaPath(data, pathSpline, targetSpline); return CCinemaPath(data, pathSpline, targetSpline);
} }
bool m_Enabled; bool m_IsPlayingPathQueue;
std::map<CStrW, CCinemaPath> m_Paths; std::map<CStrW, CCinemaPath> m_Paths;
std::list<CCinemaPath> m_PathQueue; std::list<CCinemaPath> m_PathQueue;
fixed m_PathQueueDuration;
// States before playing // Total time elapsed since starting to play the queue.
bool m_MapRevealed; fixed m_QueuePlayingElapsedTime;
fixed m_ElapsedTime; // Time elapsed since the currently active path first started playing.
fixed m_TotalTime; fixed m_ActivePathElapsedTime;
fixed m_CurrentPathElapsedTime;
// Time elapsed since the
fixed m_QueueEndedElapsedTime;
// Whether the map was revealed before playing the queue.
bool m_WasMapRevealed;
}; };
REGISTER_COMPONENT_TYPE(CinemaManager) REGISTER_COMPONENT_TYPE(CinemaManager)
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Wildfire Games. /* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D. * This file is part of 0 A.D.
* *
* 0 A.D. is free software: you can redistribute it and/or modify * 0 A.D. is free software: you can redistribute it and/or modify
@@ -24,9 +24,13 @@
BEGIN_INTERFACE_WRAPPER(CinemaManager) BEGIN_INTERFACE_WRAPPER(CinemaManager)
DEFINE_INTERFACE_METHOD("AddPath", ICmpCinemaManager, AddPath) DEFINE_INTERFACE_METHOD("AddPath", ICmpCinemaManager, AddPath)
DEFINE_INTERFACE_METHOD("AddCinemaPathToQueue", ICmpCinemaManager, AddCinemaPathToQueue) DEFINE_INTERFACE_METHOD("HasPath", ICmpCinemaManager, HasPath)
DEFINE_INTERFACE_METHOD("DeletePath", ICmpCinemaManager, DeletePath) DEFINE_INTERFACE_METHOD("DeletePath", ICmpCinemaManager, DeletePath)
DEFINE_INTERFACE_METHOD("IsPlaying", ICmpCinemaManager, IsEnabled) DEFINE_INTERFACE_METHOD("PushPathToQueue", ICmpCinemaManager, PushPathToQueue)
DEFINE_INTERFACE_METHOD("Play", ICmpCinemaManager, Play) DEFINE_INTERFACE_METHOD("ClearQueue", ICmpCinemaManager, ClearQueue)
DEFINE_INTERFACE_METHOD("Stop", ICmpCinemaManager, Stop) DEFINE_INTERFACE_METHOD("StartPlayingQueue", ICmpCinemaManager, StartPlayingQueue)
DEFINE_INTERFACE_METHOD("IsPlayingQueue", ICmpCinemaManager, IsPlayingQueue)
DEFINE_INTERFACE_METHOD("GetActivePath", ICmpCinemaManager, GetActivePath)
DEFINE_INTERFACE_METHOD("GetActivePathElapsedTime", ICmpCinemaManager, GetActivePathElapsedTime)
DEFINE_INTERFACE_METHOD("StopPlayingQueue", ICmpCinemaManager, StopPlayingQueue)
END_INTERFACE_WRAPPER(CinemaManager) END_INTERFACE_WRAPPER(CinemaManager)
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Wildfire Games. /* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D. * This file is part of 0 A.D.
* *
* 0 A.D. is free software: you can redistribute it and/or modify * 0 A.D. is free software: you can redistribute it and/or modify
@@ -31,54 +31,83 @@ class CCinemaPath;
class CStrW; class CStrW;
/** /**
* Component for CCinemaManager class * Manages a dynamic list of cinema paths (predefined camera movements/cutscenes) as well as a queue of paths that
* TODO: write description * is played one by one on command to all players. Simulation messages are sent each time a path ends and also when
* the queue finishes as a whole.
*/ */
class ICmpCinemaManager : public IComponent class ICmpCinemaManager : public IComponent
{ {
public: public:
/** /**
* Adds the path to the path list * Register a new path.
* @param CCinemaPath path data * @param path path data
*/ */
virtual void AddPath(const CCinemaPath& path) = 0; virtual void AddPath(const CCinemaPath& path) = 0;
/** /**
* Adds the path to the playlist * Remove a path and its data entirely.
* @param name path name * @param name path name
*/ */
virtual void AddCinemaPathToQueue(const CStrW& name) = 0;
virtual void Play() = 0;
virtual void Stop() = 0;
virtual void PlayQueue(const float deltaRealTime, CCamera* camera) = 0;
/**
* Checks the path name in the path list
* @param name path name
* @return true if path with that name exists, else false
*/
virtual bool HasPath(const CStrW& name) const = 0;
virtual void DeletePath(const CStrW& name) = 0; virtual void DeletePath(const CStrW& name) = 0;
/** /**
* Clears the playlist * Check whether a path exists (is registered under the given name)..
* @param name path name
*/
virtual bool HasPath(const CStrW& name) const = 0;
/**
* Get all registered paths, keyed by their names.
*/
virtual const std::map<CStrW, CCinemaPath>& GetPaths() const = 0;
/**
* Override the entire list of existing paths.
* @param newPaths new list of paths
*/
virtual void SetPaths(const std::map<CStrW, CCinemaPath>& newPaths) = 0;
/**
* Push a path to the back of the queue.
* @param name path name
*/
virtual void PushPathToQueue(const CStrW& name) = 0;
/**
* Clear all paths from the queue.
*/ */
virtual void ClearQueue() = 0; virtual void ClearQueue() = 0;
virtual const std::map<CStrW, CCinemaPath>& GetPaths() const = 0; /**
virtual void SetPaths(const std::map<CStrW, CCinemaPath>& newPaths) = 0; * Start playing the paths in the queue one by one.
virtual const std::list<CCinemaPath>& GetQueue() const = 0; */
virtual void StartPlayingQueue() = 0;
virtual bool IsEnabled() const = 0;
/** /**
* Sets enable state of the cinema manager (shows/hide gui, show/hide rings, etc) * Stop playing the active path and the queue altogether.
* @param enable new state */
*/ virtual void StopPlayingQueue() = 0;
virtual void SetEnabled(bool enabled) = 0;
/**
* Whether the first path of the queue is being played at the moment.
*/
virtual bool IsPlayingQueue() const = 0;
/**
* Send an update to the path currently playing for it to determine the new camera position.
* Called every frame.
*/
virtual void UpdateActivePath(const float deltaRealTime, CCamera* camera) = 0;
/**
* Get the name of the path currently playing, if any.
*/
virtual const CStrW GetActivePath() const = 0;
/**
* Get the time elapsed since the currently active path started playing.
*/
virtual const fixed GetActivePathElapsedTime() const = 0;
DECLARE_INTERFACE_TYPE(CinemaManager) DECLARE_INTERFACE_TYPE(CinemaManager)
}; };
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Wildfire Games. /* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D. * This file is part of 0 A.D.
* *
* 0 A.D. is free software: you can redistribute it and/or modify * 0 A.D. is free software: you can redistribute it and/or modify
@@ -37,33 +37,91 @@
class TestCmpCinemaManager : public CxxTest::TestSuite class TestCmpCinemaManager : public CxxTest::TestSuite
{ {
public: public:
void test_basic() void test_managing_paths()
{ {
CXeromycesEngine xeromycesEngine; CXeromycesEngine xeromycesEngine;
ComponentTestHelper test(*g_ScriptContext); ComponentTestHelper test(*g_ScriptContext);
ICmpCinemaManager* cmp = test.Add<ICmpCinemaManager>(CID_CinemaManager, "", SYSTEM_ENTITY); ICmpCinemaManager* cmp = test.Add<ICmpCinemaManager>(CID_CinemaManager, "", SYSTEM_ENTITY);
TS_ASSERT_EQUALS(cmp->HasPath(L"test"), false); TS_ASSERT(!cmp->HasPath(L"test"));
cmp->AddPath(generatePath(L"test")); cmp->AddPath(generatePath(L"test"));
TS_ASSERT_EQUALS(cmp->HasPath(L"test"), true); TS_ASSERT(cmp->HasPath(L"test"));
cmp->DeletePath(L"test"); TS_ASSERT(!cmp->HasPath(L"test_2"));
TS_ASSERT_EQUALS(cmp->HasPath(L"test"), false); cmp->SetPaths(std::map<CStrW, CCinemaPath>{{L"test_2", generatePath(L"test_2")}});
TS_ASSERT(!cmp->HasPath(L"test"));
TS_ASSERT(cmp->HasPath(L"test_2"));
cmp->DeletePath(L"test_2");
TS_ASSERT(!cmp->HasPath(L"test_2"));
}
cmp->AddPath(generatePath(L"long_path", fixed::FromInt(3600))); void test_playing_queue()
TS_ASSERT_EQUALS(cmp->HasPath(L"long_path"), true); {
CXeromycesEngine xeromycesEngine;
ComponentTestHelper test(*g_ScriptContext);
TS_ASSERT_EQUALS(cmp->IsEnabled(), false); ICmpCinemaManager* cmp = test.Add<ICmpCinemaManager>(CID_CinemaManager, "", SYSTEM_ENTITY);
cmp->AddCinemaPathToQueue(L"long_path");
cmp->Play(); CMessageUpdate updateMsg(fixed::FromInt(200));
size_t number_of_turns = 0;
while (cmp->IsEnabled()) cmp->AddPath(generatePath(L"path_1", fixed::FromInt(10000)));
cmp->AddPath(generatePath(L"path_2", fixed::FromInt(5000)));
// Try getting the active path if there is none.
TS_ASSERT_WSTR_EQUALS(cmp->GetActivePath(), L"");
// Try to start playing the queue if it's empty.
cmp->StartPlayingQueue();
TS_ASSERT(!cmp->IsPlayingQueue());
// Try stopping playing the queue if it's not playing in the first place.
cmp->StartPlayingQueue();
TS_ASSERT(!cmp->IsPlayingQueue());
cmp->PushPathToQueue(L"path_1");
cmp->PushPathToQueue(L"path_2");
// Try getting the active path if there is none.
TS_ASSERT_WSTR_EQUALS(cmp->GetActivePath(), L"");
cmp->StartPlayingQueue();
TS_ASSERT(cmp->IsPlayingQueue());
TS_ASSERT_WSTR_EQUALS(cmp->GetActivePath(), L"path_1");
TS_ASSERT_EQUALS(cmp->GetActivePathElapsedTime(), fixed::FromInt(0));
for (int i = 0; i < 35; i++)
cmp->HandleMessage(updateMsg, true);
TS_ASSERT(cmp->IsPlayingQueue());
TS_ASSERT_WSTR_EQUALS(cmp->GetActivePath(), L"path_1");
TS_ASSERT_EQUALS(cmp->GetActivePathElapsedTime(), fixed::FromInt(7000));
// Finish path_1 and start with path_2
for (int i = 0; i < 20; i++)
cmp->HandleMessage(updateMsg, true);
TS_ASSERT_WSTR_EQUALS(cmp->GetActivePath(), L"path_2");
TS_ASSERT_EQUALS(cmp->GetActivePathElapsedTime(), fixed::FromInt(1000));
// Try restarting while a path is being played.
// This should result in the active path starting again from the beginning.
cmp->StopPlayingQueue();
cmp->StartPlayingQueue();
TS_ASSERT(cmp->IsPlayingQueue());
TS_ASSERT_WSTR_EQUALS(cmp->GetActivePath(), L"path_2");
size_t remainingTurns = 0;
while (cmp->IsPlayingQueue())
{ {
CMessageUpdate msg(fixed::FromInt(36)); cmp->HandleMessage(updateMsg, true);
cmp->HandleMessage(msg, true); remainingTurns++;
++number_of_turns;
} }
TS_ASSERT_EQUALS(number_of_turns, 100); TS_ASSERT_EQUALS(remainingTurns, 25);
TS_ASSERT(!cmp->IsPlayingQueue());
TS_ASSERT_WSTR_EQUALS(cmp->GetActivePath(), L"");
TS_ASSERT_EQUALS(cmp->GetActivePathElapsedTime(), fixed::FromInt(0));
// Make sure the queue is empty.
cmp->StartPlayingQueue();
TS_ASSERT(!cmp->IsPlayingQueue());
} }
private: private:
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Wildfire Games. /* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D. * This file is part of 0 A.D.
* *
* 0 A.D. is free software: you can redistribute it and/or modify * 0 A.D. is free software: you can redistribute it and/or modify
@@ -41,7 +41,7 @@ namespace AtlasMessage {
MESSAGEHANDLER(CameraReset) MESSAGEHANDLER(CameraReset)
{ {
if (!g_Game || g_Game->GetView()->GetCinema()->IsEnabled()) if (!g_Game || g_Game->GetView()->GetCinema()->IsPlaying())
return; return;
CVector3D focus = g_Game->GetView()->GetCamera()->GetFocus(); CVector3D focus = g_Game->GetView()->GetCamera()->GetFocus();
@@ -64,7 +64,7 @@ MESSAGEHANDLER(CameraReset)
MESSAGEHANDLER(ScrollConstant) MESSAGEHANDLER(ScrollConstant)
{ {
if (!g_Game || g_Game->GetView()->GetCinema()->IsEnabled()) if (!g_Game || g_Game->GetView()->GetCinema()->IsPlaying())
return; return;
if (msg->dir < 0 || msg->dir > 5) if (msg->dir < 0 || msg->dir > 5)
@@ -82,7 +82,7 @@ MESSAGEHANDLER(ScrollConstant)
MESSAGEHANDLER(Scroll) MESSAGEHANDLER(Scroll)
{ {
if (!g_Game || g_Game->GetView()->GetCinema()->IsEnabled()) // TODO: do this better (probably a separate AtlasView class for cinematics) if (!g_Game || g_Game->GetView()->GetCinema()->IsPlaying()) // TODO: do this better (probably a separate AtlasView class for cinematics)
return; return;
static CVector3D targetPos; static CVector3D targetPos;
@@ -131,7 +131,7 @@ MESSAGEHANDLER(Scroll)
MESSAGEHANDLER(SmoothZoom) MESSAGEHANDLER(SmoothZoom)
{ {
if (!g_Game || g_Game->GetView()->GetCinema()->IsEnabled()) if (!g_Game || g_Game->GetView()->GetCinema()->IsPlaying())
return; return;
g_AtlasGameLoop->input.zoomDelta += msg->amount; g_AtlasGameLoop->input.zoomDelta += msg->amount;
@@ -139,7 +139,7 @@ MESSAGEHANDLER(SmoothZoom)
MESSAGEHANDLER(RotateAround) MESSAGEHANDLER(RotateAround)
{ {
if (!g_Game || g_Game->GetView()->GetCinema()->IsEnabled()) if (!g_Game || g_Game->GetView()->GetCinema()->IsPlaying())
return; return;
static CVector3D focusPos; static CVector3D focusPos;
@@ -245,7 +245,7 @@ QUERYHANDLER(GetView)
MESSAGEHANDLER(SetView) MESSAGEHANDLER(SetView)
{ {
if (!g_Game || g_Game->GetView()->GetCinema()->IsEnabled()) if (!g_Game || g_Game->GetView()->GetCinema()->IsPlaying())
return; return;
CGameView* view = g_Game->GetView(); CGameView* view = g_Game->GetView();
@@ -1,4 +1,4 @@
/* Copyright (C) 2025 Wildfire Games. /* Copyright (C) 2026 Wildfire Games.
* This file is part of 0 A.D. * This file is part of 0 A.D.
* *
* 0 A.D. is free software: you can redistribute it and/or modify * 0 A.D. is free software: you can redistribute it and/or modify
@@ -189,7 +189,7 @@ MESSAGEHANDLER(CinemaEvent)
if (msg->mode == eCinemaEventMode::SMOOTH) if (msg->mode == eCinemaEventMode::SMOOTH)
{ {
cmpCinemaManager->AddCinemaPathToQueue(*msg->path); cmpCinemaManager->PushPathToQueue(*msg->path);
} }
else if ( msg->mode == eCinemaEventMode::RESET ) else if ( msg->mode == eCinemaEventMode::RESET )
{ {