5b148b1f40
Updates the Audio Controls Editor code to use rapidxml instead of legacy xml apis. Further makes improvements to path manipulations away from strings towards PathView apis and similar. Fixes some issues encountered with memory management when handling xml data that did not occur previously. Signed-off-by: amzn-phist <52085794+amzn-phist@users.noreply.github.com>
410 lines
15 KiB
C++
410 lines
15 KiB
C++
/*
|
|
* Copyright (c) Contributors to the Open 3D Engine Project.
|
|
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
*
|
|
*/
|
|
|
|
|
|
#include <AudioControlsWriter.h>
|
|
|
|
#include <AzCore/IO/ByteContainerStream.h>
|
|
#include <AzCore/IO/TextStreamWriters.h>
|
|
#include <AzCore/std/string/conversions.h>
|
|
#include <AzCore/StringFunc/StringFunc.h>
|
|
#include <AzCore/Utils/Utils.h>
|
|
#include <AzCore/XML/rapidxml_print.h>
|
|
|
|
#include <ACEEnums.h>
|
|
#include <ATLControlsModel.h>
|
|
#include <IAudioSystem.h>
|
|
#include <IAudioSystemControl.h>
|
|
#include <IAudioSystemEditor.h>
|
|
|
|
#include <IEditor.h>
|
|
#include <Include/IFileUtil.h>
|
|
#include <Include/ISourceControl.h>
|
|
|
|
#include <QModelIndex>
|
|
#include <QStandardItemModel>
|
|
#include <QFileInfo>
|
|
|
|
|
|
namespace AudioControls
|
|
{
|
|
namespace WriterStrings
|
|
{
|
|
static constexpr const char* LevelsSubFolder = "levels";
|
|
static constexpr const char* LibraryExtension = ".xml";
|
|
|
|
} // namespace WriterStrings
|
|
|
|
//-------------------------------------------------------------------------------------------//
|
|
AZStd::string_view TypeToTag(EACEControlType type)
|
|
{
|
|
switch (type)
|
|
{
|
|
case eACET_RTPC:
|
|
return Audio::ATLXmlTags::ATLRtpcTag;
|
|
case eACET_TRIGGER:
|
|
return Audio::ATLXmlTags::ATLTriggerTag;
|
|
case eACET_SWITCH:
|
|
return Audio::ATLXmlTags::ATLSwitchTag;
|
|
case eACET_SWITCH_STATE:
|
|
return Audio::ATLXmlTags::ATLSwitchStateTag;
|
|
case eACET_PRELOAD:
|
|
return Audio::ATLXmlTags::ATLPreloadRequestTag;
|
|
case eACET_ENVIRONMENT:
|
|
return Audio::ATLXmlTags::ATLEnvironmentTag;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------//
|
|
CAudioControlsWriter::CAudioControlsWriter(CATLControlsModel* atlModel, QStandardItemModel* layoutModel, IAudioSystemEditor* audioSystemImpl, FilepathSet& previousLibraryPaths)
|
|
: m_atlModel(atlModel)
|
|
, m_layoutModel(layoutModel)
|
|
, m_audioSystemImpl(audioSystemImpl)
|
|
{
|
|
if (m_atlModel && m_layoutModel && m_audioSystemImpl)
|
|
{
|
|
m_layoutModel->blockSignals(true);
|
|
|
|
int i = 0;
|
|
QModelIndex index = m_layoutModel->index(i, 0);
|
|
while (index.isValid())
|
|
{
|
|
WriteLibrary(index.data(Qt::DisplayRole).toString().toUtf8().data(), index);
|
|
index = index.sibling(++i, 0);
|
|
}
|
|
|
|
auto fileIO = AZ::IO::FileIOBase::GetInstance();
|
|
AZStd::for_each(
|
|
m_foundLibraryPaths.begin(), m_foundLibraryPaths.end(),
|
|
[fileIO](AZStd::string& libraryPath) -> void
|
|
{
|
|
AZStd::optional<AZ::u64> newLength = fileIO->ConvertToAlias(libraryPath.data(), libraryPath.size());
|
|
if (newLength)
|
|
{
|
|
libraryPath.resize_no_construct(*newLength);
|
|
}
|
|
AZStd::to_lower(libraryPath.begin(), libraryPath.end());
|
|
});
|
|
|
|
// Delete libraries that don't exist anymore from disk
|
|
FilepathSet librariesToDelete;
|
|
AZStd::set_difference(
|
|
previousLibraryPaths.begin(), previousLibraryPaths.end(),
|
|
m_foundLibraryPaths.begin(), m_foundLibraryPaths.end(),
|
|
AZStd::inserter(librariesToDelete, librariesToDelete.begin())
|
|
);
|
|
|
|
for (auto it = librariesToDelete.begin(); it != librariesToDelete.end(); ++it)
|
|
{
|
|
DeleteLibraryFile((*it).c_str());
|
|
}
|
|
|
|
previousLibraryPaths = m_foundLibraryPaths;
|
|
|
|
m_layoutModel->blockSignals(false);
|
|
}
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------//
|
|
void CAudioControlsWriter::WriteLibrary(const AZStd::string_view libraryName, QModelIndex root)
|
|
{
|
|
const char* controlsPath = nullptr;
|
|
Audio::AudioSystemRequestBus::BroadcastResult(controlsPath, &Audio::AudioSystemRequestBus::Events::GetControlsPath);
|
|
|
|
if (root.isValid() && controlsPath)
|
|
{
|
|
TLibraryStorage library;
|
|
int i = 0;
|
|
QModelIndex child = root.model()->index(i, 0, root);
|
|
while (child.isValid())
|
|
{
|
|
WriteItem(child, "", library, root.data(eDR_MODIFIED).toBool());
|
|
child = root.model()->index(++i, 0, root);
|
|
}
|
|
|
|
for (auto& libraryPair : library)
|
|
{
|
|
AZ::IO::FixedMaxPath libraryPath{ controlsPath };
|
|
const AZStd::string& scope = libraryPair.first;
|
|
if (scope.empty())
|
|
{
|
|
// no scope, file at the root level
|
|
libraryPath /= libraryName;
|
|
libraryPath.ReplaceExtension(WriterStrings::LibraryExtension);
|
|
}
|
|
else
|
|
{
|
|
// with scope, inside level folder
|
|
libraryPath /= AZ::IO::FixedMaxPath{ WriterStrings::LevelsSubFolder } / scope / libraryName;
|
|
libraryPath.ReplaceExtension(WriterStrings::LibraryExtension);
|
|
}
|
|
|
|
AZ::IO::FixedMaxPath fullFilePath = AZ::Utils::GetProjectPath();
|
|
fullFilePath /= libraryPath;
|
|
m_foundLibraryPaths.insert(fullFilePath.c_str());
|
|
|
|
const SLibraryScope& libScope = libraryPair.second;
|
|
if (libScope.m_isDirty)
|
|
{
|
|
XmlAllocator& xmlAlloc(AudioControls::s_xmlAllocator);
|
|
AZ::rapidxml::xml_node<char>* fileNode =
|
|
xmlAlloc.allocate_node(AZ::rapidxml::node_element, xmlAlloc.allocate_string(Audio::ATLXmlTags::RootNodeTag));
|
|
|
|
AZ::rapidxml::xml_attribute<char>* nameAttr = xmlAlloc.allocate_attribute(
|
|
xmlAlloc.allocate_string(Audio::ATLXmlTags::ATLNameAttribute), xmlAlloc.allocate_string(libraryName.data()));
|
|
|
|
fileNode->append_attribute(nameAttr);
|
|
|
|
for (int ii = 0; ii < eACET_NUM_TYPES; ++ii)
|
|
{
|
|
if (libScope.m_nodes[ii] && libScope.m_nodes[ii]->first_node() != nullptr)
|
|
{
|
|
fileNode->append_node(libScope.m_nodes[ii]);
|
|
}
|
|
}
|
|
|
|
if (auto fileInfo = QFileInfo(fullFilePath.c_str());
|
|
fileInfo.exists())
|
|
{
|
|
if (!fileInfo.isWritable())
|
|
{
|
|
// file exists and is read-only
|
|
CheckOutFile(fullFilePath.Native());
|
|
}
|
|
|
|
[[maybe_unused]] bool writeOk = WriteXmlToFile(fullFilePath.Native(), fileNode);
|
|
}
|
|
else
|
|
{
|
|
// since it's a new file, save the file first, CheckOutFile will add it
|
|
[[maybe_unused]] bool writeOk = WriteXmlToFile(fullFilePath.Native(), fileNode);
|
|
CheckOutFile(fullFilePath.Native());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------//
|
|
void CAudioControlsWriter::WriteItem(QModelIndex index, const AZStd::string& path, TLibraryStorage& library, bool isParentModified)
|
|
{
|
|
if (index.isValid())
|
|
{
|
|
if (index.data(eDR_TYPE) == eIT_FOLDER)
|
|
{
|
|
int i = 0;
|
|
QModelIndex child = index.model()->index(i, 0, index);
|
|
while (child.isValid())
|
|
{
|
|
AZStd::string newPath = path.empty() ? "" : path + "/";
|
|
newPath += index.data(Qt::DisplayRole).toString().toUtf8().data();
|
|
WriteItem(child, newPath, library, index.data(eDR_MODIFIED).toBool() || isParentModified);
|
|
child = index.model()->index(++i, 0, index);
|
|
}
|
|
|
|
QStandardItem* item = m_layoutModel->itemFromIndex(index);
|
|
if (item)
|
|
{
|
|
item->setData(false, eDR_MODIFIED);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CATLControl* control = m_atlModel->GetControlByID(index.data(eDR_ID).toUInt());
|
|
if (control)
|
|
{
|
|
SLibraryScope& scope = library[control->GetScope()];
|
|
if (IsItemModified(index) || isParentModified)
|
|
{
|
|
scope.m_isDirty = true;
|
|
QStandardItem* item = m_layoutModel->itemFromIndex(index);
|
|
if (item)
|
|
{
|
|
item->setData(false, eDR_MODIFIED);
|
|
}
|
|
}
|
|
|
|
WriteControlToXml(scope.m_nodes[control->GetType()], control, path);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------//
|
|
bool CAudioControlsWriter::IsItemModified(QModelIndex index)
|
|
{
|
|
if (index.data(eDR_MODIFIED).toBool() == true)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
int i = 0;
|
|
QModelIndex child = index.model()->index(i, 0, index);
|
|
while (child.isValid())
|
|
{
|
|
if (IsItemModified(child))
|
|
{
|
|
return true;
|
|
}
|
|
child = index.model()->index(++i, 0, index);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------//
|
|
bool CAudioControlsWriter::WriteXmlToFile(const AZStd::string_view filepath, AZ::rapidxml::xml_node<char>* rootNode)
|
|
{
|
|
if (!rootNode)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
using namespace AZ::IO;
|
|
AZStd::string docString;
|
|
ByteContainerStream stringStream(&docString);
|
|
|
|
AZ::rapidxml::xml_document<char> xmlDoc;
|
|
xmlDoc.append_node(rootNode);
|
|
|
|
RapidXMLStreamWriter streamWriter(&stringStream);
|
|
AZ::rapidxml::print(streamWriter.Iterator(), xmlDoc);
|
|
streamWriter.FlushCache();
|
|
|
|
constexpr int openMode =
|
|
(SystemFile::SF_OPEN_WRITE_ONLY | SystemFile::SF_OPEN_CREATE | SystemFile::SF_OPEN_CREATE_PATH);
|
|
|
|
if (SystemFile fileOut;
|
|
fileOut.Open(filepath.data(), openMode))
|
|
{
|
|
auto bytesWritten = fileOut.Write(docString.data(), docString.size());
|
|
return (bytesWritten == docString.size());
|
|
}
|
|
return false;
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------//
|
|
void CAudioControlsWriter::WriteControlToXml(AZ::rapidxml::xml_node<char>* node, CATLControl* control, const AZStd::string_view path)
|
|
{
|
|
if (!node || !control)
|
|
{
|
|
return;
|
|
}
|
|
|
|
XmlAllocator& xmlAlloc(AudioControls::s_xmlAllocator);
|
|
|
|
const EACEControlType type = control->GetType();
|
|
AZStd::string_view typeName = TypeToTag(type);
|
|
|
|
AZ::rapidxml::xml_node<char>* childNode =
|
|
xmlAlloc.allocate_node(AZ::rapidxml::node_element, xmlAlloc.allocate_string(typeName.data()));
|
|
|
|
AZ::rapidxml::xml_attribute<char>* nameAttr = xmlAlloc.allocate_attribute(
|
|
xmlAlloc.allocate_string(Audio::ATLXmlTags::ATLNameAttribute), xmlAlloc.allocate_string(control->GetName().c_str()));
|
|
|
|
childNode->append_attribute(nameAttr);
|
|
|
|
if (!path.empty())
|
|
{
|
|
AZ::rapidxml::xml_attribute<char>* pathAttr = xmlAlloc.allocate_attribute(
|
|
xmlAlloc.allocate_string(Audio::ATLXmlTags::ATLPathAttribute), xmlAlloc.allocate_string(path.data()));
|
|
|
|
childNode->append_attribute(pathAttr);
|
|
}
|
|
|
|
if (type == eACET_SWITCH)
|
|
{
|
|
const size_t size = control->ChildCount();
|
|
for (size_t i = 0; i < size; ++i)
|
|
{
|
|
WriteControlToXml(childNode, control->GetChild(i), "");
|
|
}
|
|
}
|
|
else if (type == eACET_PRELOAD)
|
|
{
|
|
if (control->IsAutoLoad())
|
|
{
|
|
AZ::rapidxml::xml_attribute<char>* loadAttr = xmlAlloc.allocate_attribute(
|
|
xmlAlloc.allocate_string(Audio::ATLXmlTags::ATLTypeAttribute),
|
|
xmlAlloc.allocate_string(Audio::ATLXmlTags::ATLDataLoadType));
|
|
|
|
childNode->append_attribute(loadAttr);
|
|
}
|
|
|
|
// New Preloads XML...
|
|
WriteConnectionsToXml(childNode, control);
|
|
}
|
|
else
|
|
{
|
|
WriteConnectionsToXml(childNode, control);
|
|
}
|
|
|
|
node->append_node(childNode);
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------//
|
|
void CAudioControlsWriter::WriteConnectionsToXml(AZ::rapidxml::xml_node<char>* node, CATLControl* control)
|
|
{
|
|
if (node && control && m_audioSystemImpl)
|
|
{
|
|
TXmlNodeList otherNodes = control->m_connectionNodes;
|
|
auto end = AZStd::remove_if(
|
|
otherNodes.begin(), otherNodes.end(),
|
|
[](const SRawConnectionData& connection)
|
|
{
|
|
return connection.m_isValid;
|
|
}
|
|
);
|
|
otherNodes.erase(end, otherNodes.end());
|
|
|
|
for (auto& connectionNode : otherNodes)
|
|
{
|
|
node->append_node(SRawConnectionData::DeepCopyNode(connectionNode.m_xmlNode));
|
|
}
|
|
|
|
const size_t size = control->ConnectionCount();
|
|
for (size_t i = 0; i < size; ++i)
|
|
{
|
|
if (TConnectionPtr connection = control->GetConnectionAt(i);
|
|
connection != nullptr)
|
|
{
|
|
if (auto childNode = m_audioSystemImpl->CreateXMLNodeFromConnection(connection, control->GetType());
|
|
childNode != nullptr)
|
|
{
|
|
node->append_node(childNode);
|
|
control->m_connectionNodes.push_back(SRawConnectionData(childNode, true));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------//
|
|
void CAudioControlsWriter::CheckOutFile(const AZStd::string_view filepath)
|
|
{
|
|
IEditor* editor = GetIEditor();
|
|
IFileUtil* fileUtil = editor ? editor->GetFileUtil() : nullptr;
|
|
if (fileUtil)
|
|
{
|
|
fileUtil->CheckoutFile(filepath.data(), nullptr);
|
|
}
|
|
}
|
|
|
|
//-------------------------------------------------------------------------------------------//
|
|
void CAudioControlsWriter::DeleteLibraryFile(const AZStd::string_view filepath)
|
|
{
|
|
IEditor* editor = GetIEditor();
|
|
IFileUtil* fileUtil = editor ? editor->GetFileUtil() : nullptr;
|
|
if (fileUtil)
|
|
{
|
|
fileUtil->DeleteFromSourceControl(filepath.data(), nullptr);
|
|
}
|
|
}
|
|
|
|
} // namespace AudioControls
|