cog/Frameworks/OpenMPT.old/OpenMPT/soundlib/RowVisitor.cpp

266 lines
7.3 KiB
C++

/*
* RowVisitor.cpp
* --------------
* Purpose: Class for managing which rows of a song has already been visited. Useful for detecting backwards jumps, loops, etc.
* Notes : The class keeps track of rows that have been visited by the player before.
* This way, we can tell when the module starts to loop, i.e. we can determine the song length,
* or find out that a given point of the module can never be reached.
*
* Specific implementations:
*
* Length detection code:
* As the ModPlug engine already deals with pattern loops sufficiently (though not always correctly),
* there's no problem with (infinite) pattern loops in this code.
*
* Normal player code:
* Bear in mind that rows inside pattern loops should only be evaluated once, or else the algorithm will cancel too early!
* So in that case, the pattern loop rows have to be reset when looping back.
* Authors: OpenMPT Devs
* The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
*/
#include "stdafx.h"
#include "Sndfile.h"
#include "RowVisitor.h"
OPENMPT_NAMESPACE_BEGIN
RowVisitor::RowVisitor(const CSoundFile &sf, SEQUENCEINDEX sequence)
: m_sndFile(sf)
, m_currentOrder(0)
, m_sequence(sequence)
{
Initialize(true);
}
void RowVisitor::MoveVisitedRowsFrom(RowVisitor &other)
{
m_visitedRows = std::move(other.m_visitedRows);
}
const ModSequence &RowVisitor::Order() const
{
if(m_sequence >= m_sndFile.Order.GetNumSequences())
return m_sndFile.Order();
else
return m_sndFile.Order(m_sequence);
}
// Resize / Clear the row vector.
// If reset is true, the vector is not only resized to the required dimensions, but also completely cleared (i.e. all visited rows are unset).
void RowVisitor::Initialize(bool reset)
{
auto &order = Order();
const ORDERINDEX endOrder = order.GetLengthTailTrimmed();
m_visitedRows.resize(endOrder);
if(reset)
{
m_visitOrder.clear();
// Pre-allocate maximum amount of memory most likely needed for keeping track of visited rows in a pattern
if(m_visitOrder.capacity() < MAX_PATTERN_ROWS)
{
ROWINDEX maxRows = 0;
for(const auto &pat :m_sndFile.Patterns)
{
maxRows = std::max(maxRows, pat.GetNumRows());
}
m_visitOrder.reserve(maxRows);
}
}
for(ORDERINDEX ord = 0; ord < endOrder; ord++)
{
auto &row = m_visitedRows[ord];
const size_t size = GetVisitedRowsVectorSize(order[ord]);
if(reset)
{
// If we want to reset the vectors completely, we overwrite existing items with false.
row.assign(size, false);
} else
{
row.resize(size, false);
}
}
}
// (Un)sets a given row as visited.
// order, row - which row should be (un)set
// If visited is true, the row will be set as visited.
void RowVisitor::SetVisited(ORDERINDEX ord, ROWINDEX row, bool visited)
{
auto &order = Order();
if(ord >= order.size() || row >= GetVisitedRowsVectorSize(order[ord]))
{
return;
}
// The module might have been edited in the meantime - so we have to extend this a bit.
if(ord >= m_visitedRows.size() || row >= m_visitedRows[ord].size())
{
Initialize(false);
// If it's still past the end of the vector, this means that ord >= order.GetLengthTailTrimmed(), i.e. we are trying to play an empty order.
if(ord >= m_visitedRows.size())
{
return;
}
}
m_visitedRows[ord][row] = visited;
if(visited)
{
AddVisitedRow(ord, row);
}
}
// Returns whether a given row has been visited yet.
// If autoSet is true, the queried row will automatically be marked as visited.
// Use this parameter instead of consecutive IsRowVisited / SetRowVisited calls.
bool RowVisitor::IsVisited(ORDERINDEX ord, ROWINDEX row, bool autoSet)
{
if(ord >= Order().size())
{
return false;
}
// The row slot for this row has not been assigned yet - Just return false, as this means that the program has not played the row yet.
if(ord >= m_visitedRows.size() || row >= m_visitedRows[ord].size())
{
if(autoSet)
{
SetVisited(ord, row, true);
}
return false;
}
if(m_visitedRows[ord][row])
{
// We visited this row already - this module must be looping.
return true;
} else if(autoSet)
{
m_visitedRows[ord][row] = true;
AddVisitedRow(ord, row);
}
return false;
}
// Get the needed vector size for pattern nPat.
size_t RowVisitor::GetVisitedRowsVectorSize(PATTERNINDEX pattern) const
{
if(m_sndFile.Patterns.IsValidPat(pattern))
{
return static_cast<size_t>(m_sndFile.Patterns[pattern].GetNumRows());
} else
{
// Invalid patterns consist of a "fake" row.
return 1;
}
}
// Find the first row that has not been played yet.
// The order and row is stored in the order and row variables on success, on failure they contain invalid values.
// If onlyUnplayedPatterns is true (default), only completely unplayed patterns are considered, otherwise a song can start anywhere.
// Function returns true on success.
bool RowVisitor::GetFirstUnvisitedRow(ORDERINDEX &ord, ROWINDEX &row, bool onlyUnplayedPatterns) const
{
const auto &order = Order();
const ORDERINDEX endOrder = order.GetLengthTailTrimmed();
for(ord = 0; ord < endOrder; ord++)
{
const PATTERNINDEX pattern = order[ord];
if(!m_sndFile.Patterns.IsValidPat(pattern))
{
continue;
}
if(ord >= m_visitedRows.size())
{
// Not yet initialized => unvisited
row = 0;
return true;
}
const auto &visitedRows = m_visitedRows[ord];
auto foundRow = std::find(visitedRows.begin(), visitedRows.end(), onlyUnplayedPatterns);
if(onlyUnplayedPatterns && foundRow == visitedRows.end())
{
// No row of this pattern has been played yet.
row = 0;
return true;
} else if(!onlyUnplayedPatterns)
{
// Return the first unplayed row in this pattern
if(foundRow != visitedRows.end())
{
row = static_cast<ROWINDEX>(std::distance(visitedRows.begin(), foundRow));
return true;
}
if(visitedRows.size() < m_sndFile.Patterns[pattern].GetNumRows())
{
// History is not fully initialized
row = static_cast<ROWINDEX>(visitedRows.size());
return true;
}
}
}
// Didn't find anything :(
ord = ORDERINDEX_INVALID;
row = ROWINDEX_INVALID;
return false;
}
// Set all rows of a previous pattern loop as unvisited.
void RowVisitor::ResetPatternLoop(ORDERINDEX ord, ROWINDEX startRow)
{
MPT_ASSERT(ord == m_currentOrder); // Shouldn't trigger, unless we're jumping around in the GUI during a pattern loop.
// Unvisit all rows that are in the visited row buffer, until we hit the start row for this pattern loop.
ROWINDEX row = ROWINDEX_INVALID;
for(auto iter = m_visitOrder.crbegin(); iter != m_visitOrder.crend() && row != startRow; iter++)
{
row = *iter;
Unvisit(ord, row);
}
}
// Add a row to the visited row memory for this pattern.
void RowVisitor::AddVisitedRow(ORDERINDEX ord, ROWINDEX row)
{
if(ord != m_currentOrder)
{
// We're in a new pattern! Forget about which rows we previously visited...
m_visitOrder.clear();
m_currentOrder = ord;
}
if(m_visitOrder.empty())
{
m_visitOrder.reserve(GetVisitedRowsVectorSize(Order()[ord]));
}
// And now add the played row to our memory.
m_visitOrder.push_back(row);
}
// Returns the last visited row index of the current pattern, or ROWINDEX_INVALID if there is none.
ROWINDEX RowVisitor::GetLastVisitedRow() const
{
if(m_visitOrder.empty())
return ROWINDEX_INVALID;
return m_visitOrder.back();
}
OPENMPT_NAMESPACE_END