Changeset View
Changeset View
Standalone View
Standalone View
ps/trunk/source/ps/TemplateLoader.cpp
/* Copyright (C) 2020 Wildfire Games. | /* Copyright (C) 2021 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 | ||||
* it under the terms of the GNU General Public License as published by | * it under the terms of the GNU General Public License as published by | ||||
* the Free Software Foundation, either version 2 of the License, or | * the Free Software Foundation, either version 2 of the License, or | ||||
* (at your option) any later version. | * (at your option) any later version. | ||||
* | * | ||||
* 0 A.D. is distributed in the hope that it will be useful, | * 0 A.D. is distributed in the hope that it will be useful, | ||||
Show All 14 Lines | |||||
#include "ps/Filesystem.h" | #include "ps/Filesystem.h" | ||||
#include "ps/XML/Xeromyces.h" | #include "ps/XML/Xeromyces.h" | ||||
static const wchar_t TEMPLATE_ROOT[] = L"simulation/templates/"; | static const wchar_t TEMPLATE_ROOT[] = L"simulation/templates/"; | ||||
static const wchar_t ACTOR_ROOT[] = L"art/actors/"; | static const wchar_t ACTOR_ROOT[] = L"art/actors/"; | ||||
static CParamNode NULL_NODE(false); | static CParamNode NULL_NODE(false); | ||||
bool CTemplateLoader::LoadTemplateFile(const std::string& templateName, int depth) | bool CTemplateLoader::LoadTemplateFile(CParamNode& node, std::string_view templateName, bool compositing, int depth) | ||||
{ | { | ||||
// If this file was already loaded, we don't need to do anything | // Handle special case "actor|foo", which does not load 'foo' at all, just uses the name. | ||||
if (m_TemplateFileData.find(templateName) != m_TemplateFileData.end()) | if (templateName.compare(0, 6, "actor|") == 0) | ||||
{ | |||||
ConstructTemplateActor(templateName.substr(6), node); | |||||
return true; | return true; | ||||
} | |||||
// Handle infinite loops more gracefully than running out of stack space and crashing | // Handle infinite loops more gracefully than running out of stack space and crashing | ||||
if (depth > 100) | if (depth > 100) | ||||
{ | { | ||||
LOGERROR("Probable infinite inheritance loop in entity template '%s'", templateName.c_str()); | LOGERROR("Probable infinite inheritance loop in entity template '%s'", std::string(templateName)); | ||||
return false; | return false; | ||||
} | } | ||||
// Handle special case "actor|foo" | |||||
if (templateName.find("actor|") == 0) | |||||
{ | |||||
ConstructTemplateActor(templateName.substr(6), m_TemplateFileData[templateName]); | |||||
return true; | |||||
} | |||||
// Handle special case "bar|foo" | |||||
size_t pos = templateName.find_first_of('|'); | size_t pos = templateName.find_first_of('|'); | ||||
if (pos != std::string::npos) | if (pos != std::string::npos) | ||||
{ | { | ||||
std::string prefix = templateName.substr(0, pos); | // 'foo|bar' pattern: 'bar' is treated as the parent of 'foo'. | ||||
std::string baseName = templateName.substr(pos+1); | if (!LoadTemplateFile(node, templateName.substr(pos + 1), false, depth + 1)) | ||||
if (!LoadTemplateFile(baseName, depth+1)) | |||||
{ | |||||
LOGERROR("Failed to load entity template '%s'", baseName.c_str()); | |||||
return false; | return false; | ||||
} | if (!LoadTemplateFile(node, templateName.substr(0, pos), true, depth + 1)) | ||||
VfsPath path = VfsPath(TEMPLATE_ROOT) / L"special" / L"filter" / wstring_from_utf8(prefix + ".xml"); | |||||
if (!VfsFileExists(path)) | |||||
{ | |||||
LOGERROR("Invalid subset '%s'", prefix.c_str()); | |||||
return false; | return false; | ||||
} | |||||
CXeromyces xero; | |||||
PSRETURN ok = xero.Load(g_VFS, path); | |||||
if (ok != PSRETURN_OK) | |||||
return false; // (Xeromyces already logged an error with the full filename) | |||||
m_TemplateFileData[templateName] = m_TemplateFileData[baseName]; | |||||
CParamNode::LoadXML(m_TemplateFileData[templateName], xero, path.string().c_str()); | |||||
return true; | return true; | ||||
} | } | ||||
// Normal case: templateName is an XML file: | // Load the data we need to apply on the node. This data may contain special modifiers, | ||||
// such as filters, merges, multiplying the parent values, etc. Applying it to paramnode is destructive. | |||||
// Find the XML file to load - by default, this assumes the files reside in 'special/filter'. | |||||
// If not found there, it will be searched for in 'mixins/', then from the root. | |||||
// The reason for this order is that filters are used at runtime, mixins at load time. | |||||
std::wstring wtempName = wstring_from_utf8(std::string(templateName) + ".xml"); | |||||
VfsPath path = VfsPath(TEMPLATE_ROOT) / L"special" / L"filter" / wtempName; | |||||
if (!VfsFileExists(path)) | |||||
path = VfsPath(TEMPLATE_ROOT) / L"mixins" / wtempName; | |||||
if (!VfsFileExists(path)) | |||||
path = VfsPath(TEMPLATE_ROOT) / wtempName; | |||||
VfsPath path = VfsPath(TEMPLATE_ROOT) / wstring_from_utf8(templateName + ".xml"); | |||||
CXeromyces xero; | CXeromyces xero; | ||||
PSRETURN ok = xero.Load(g_VFS, path); | PSRETURN ok = xero.Load(g_VFS, path); | ||||
if (ok != PSRETURN_OK) | if (ok != PSRETURN_OK) | ||||
return false; // (Xeromyces already logged an error with the full filename) | return false; // (Xeromyces already logged an error with the full filename) | ||||
// If the layer defines an explicit parent, we must load that and apply it before ourselves. | |||||
int attr_parent = xero.GetAttributeID("parent"); | int attr_parent = xero.GetAttributeID("parent"); | ||||
CStr parentName = xero.GetRoot().GetAttributes().GetNamedItem(attr_parent); | CStr parentName = xero.GetRoot().GetAttributes().GetNamedItem(attr_parent); | ||||
if (!parentName.empty()) | if (!parentName.empty() && !LoadTemplateFile(node, parentName, compositing, depth + 1)) | ||||
{ | |||||
// To prevent needless complexity in template design, we don't allow |-separated strings as parents | |||||
if (parentName.find('|') != parentName.npos) | |||||
{ | |||||
LOGERROR("Invalid parent '%s' in entity template '%s'", parentName.c_str(), templateName.c_str()); | |||||
return false; | |||||
} | |||||
// Ensure the parent is loaded | |||||
if (!LoadTemplateFile(parentName, depth+1)) | |||||
{ | |||||
LOGERROR("Failed to load parent '%s' of entity template '%s'", parentName.c_str(), templateName.c_str()); | |||||
return false; | return false; | ||||
} | |||||
CParamNode& parentData = m_TemplateFileData[parentName]; | |||||
// Initialise this template with its parent | |||||
m_TemplateFileData[templateName] = parentData; | |||||
} | |||||
// Load the new file into the template data (overriding parent values) | |||||
CParamNode::LoadXML(m_TemplateFileData[templateName], xero, wstring_from_utf8(templateName).c_str()); | |||||
// Load the new file into the template data (overriding parent values). | |||||
// TODO: error handling. | |||||
CParamNode::LoadXML(node, xero); | |||||
return true; | return true; | ||||
} | } | ||||
static Status AddToTemplates(const VfsPath& pathname, const CFileInfo& UNUSED(fileInfo), const uintptr_t cbData) | static Status AddToTemplates(const VfsPath& pathname, const CFileInfo& UNUSED(fileInfo), const uintptr_t cbData) | ||||
{ | { | ||||
std::vector<std::string>& templates = *(std::vector<std::string>*)cbData; | std::vector<std::string>& templates = *(std::vector<std::string>*)cbData; | ||||
// Strip the .xml extension | // Strip the .xml extension | ||||
VfsPath pathstem = pathname.ChangeExtension(L""); | VfsPath pathstem = pathname.ChangeExtension(L""); | ||||
// Strip the root from the path | // Strip the root from the path | ||||
std::wstring name = pathstem.string().substr(ARRAY_SIZE(TEMPLATE_ROOT)-1); | std::wstring_view name = pathstem.string().substr(ARRAY_SIZE(TEMPLATE_ROOT)-1); | ||||
// We want to ignore template_*.xml templates, since they should never be built in the editor | // We want to ignore template_*.xml templates, since they should never be built in the editor | ||||
if (name.substr(0, 9) == L"template_") | if (name.substr(0, 9) == L"template_") | ||||
return INFO::OK; | return INFO::OK; | ||||
if (name.substr(0, 8) == L"special/") | // Also ignore some subfolders. | ||||
if (name.substr(0, 8) == L"special/" || name.substr(0, 7) == L"mixins/") | |||||
return INFO::OK; | return INFO::OK; | ||||
templates.push_back(std::string(name.begin(), name.end())); | templates.push_back(std::string(name.begin(), name.end())); | ||||
return INFO::OK; | return INFO::OK; | ||||
} | } | ||||
static Status AddActorToTemplates(const VfsPath& pathname, const CFileInfo& UNUSED(fileInfo), const uintptr_t cbData) | static Status AddActorToTemplates(const VfsPath& pathname, const CFileInfo& UNUSED(fileInfo), const uintptr_t cbData) | ||||
{ | { | ||||
Show All 31 Lines | std::vector<std::string> CTemplateLoader::FindTemplates(const std::string& path, bool includeSubdirectories, ETemplatesType templatesType) const | ||||
if (templatesType == ACTOR_TEMPLATES || templatesType == ALL_TEMPLATES) | if (templatesType == ACTOR_TEMPLATES || templatesType == ALL_TEMPLATES) | ||||
WARN_IF_ERR(vfs::ForEachFile(g_VFS, VfsPath(ACTOR_ROOT) / path, AddActorToTemplates, (uintptr_t)&templates, L"*.xml", flags)); | WARN_IF_ERR(vfs::ForEachFile(g_VFS, VfsPath(ACTOR_ROOT) / path, AddActorToTemplates, (uintptr_t)&templates, L"*.xml", flags)); | ||||
return templates; | return templates; | ||||
} | } | ||||
const CParamNode& CTemplateLoader::GetTemplateFileData(const std::string& templateName) | const CParamNode& CTemplateLoader::GetTemplateFileData(const std::string& templateName) | ||||
{ | { | ||||
// Load the template if necessary | if (std::unordered_map<std::string, CParamNode>::const_iterator it = m_TemplateFileData.find(templateName); it != m_TemplateFileData.end()) | ||||
if (!LoadTemplateFile(templateName, 0)) | return it->second; | ||||
CParamNode ret; | |||||
if (!LoadTemplateFile(ret, templateName, false, 0)) | |||||
{ | { | ||||
LOGERROR("Failed to load entity template '%s'", templateName.c_str()); | LOGERROR("Failed to load entity template '%s'", templateName.c_str()); | ||||
return NULL_NODE; | return NULL_NODE; | ||||
} | } | ||||
return m_TemplateFileData.insert_or_assign(templateName, ret).first->second; | |||||
return m_TemplateFileData[templateName]; | |||||
} | } | ||||
void CTemplateLoader::ConstructTemplateActor(const std::string& actorName, CParamNode& out) | void CTemplateLoader::ConstructTemplateActor(std::string_view actorName, CParamNode& out) | ||||
{ | { | ||||
// Copy the actor template | // Copy the actor template | ||||
out = GetTemplateFileData("special/actor"); | out = GetTemplateFileData("special/actor"); | ||||
// Initialize the actor's name and make it an Atlas selectable entity. | // Initialize the actor's name and make it an Atlas selectable entity. | ||||
std::wstring actorNameW = wstring_from_utf8(actorName); | std::string source(actorName); | ||||
std::string name = utf8_from_wstring(CParamNode::EscapeXMLString(actorNameW)); | std::wstring actorNameW = wstring_from_utf8(source); | ||||
std::string xml = "<Entity>" | source = "<Entity>" | ||||
"<VisualActor><Actor>" + name + "</Actor><ActorOnly/></VisualActor>" | "<VisualActor><Actor>" + source + "</Actor><ActorOnly/></VisualActor>" | ||||
// Arbitrary-sized Footprint definition to make actors' selection outlines show up in Atlas. | // Arbitrary-sized Footprint definition to make actors' selection outlines show up in Atlas. | ||||
"<Footprint><Circle radius='2.0'/><Height>1.0</Height></Footprint>" | "<Footprint><Circle radius='2.0'/><Height>1.0</Height></Footprint>" | ||||
"<Selectable>" | "<Selectable>" | ||||
"<EditorOnly/>" | "<EditorOnly/>" | ||||
"<Overlay><Texture><MainTexture>128x128/ellipse.png</MainTexture><MainTextureMask>128x128/ellipse_mask.png</MainTextureMask></Texture></Overlay>" | "<Overlay><Texture><MainTexture>128x128/ellipse.png</MainTexture><MainTextureMask>128x128/ellipse_mask.png</MainTextureMask></Texture></Overlay>" | ||||
"</Selectable>" | "</Selectable>" | ||||
"</Entity>"; | "</Entity>"; | ||||
// We'll assume that actorName is valid XML, otherwise this will fail and report the error anyways. | |||||
CParamNode::LoadXMLString(out, xml.c_str(), actorNameW.c_str()); | CParamNode::LoadXMLString(out, source.c_str(), actorNameW.c_str()); | ||||
} | } |
Wildfire Games · Phabricator