Index: ps/trunk/source/ps/TemplateLoader.cpp =================================================================== --- ps/trunk/source/ps/TemplateLoader.cpp (revision 23703) +++ ps/trunk/source/ps/TemplateLoader.cpp (revision 23704) @@ -1,210 +1,210 @@ -/* Copyright (C) 2017 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 "TemplateLoader.h" #include "lib/utf8.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/XML/Xeromyces.h" static const wchar_t TEMPLATE_ROOT[] = L"simulation/templates/"; static const wchar_t ACTOR_ROOT[] = L"art/actors/"; static CParamNode NULL_NODE(false); bool CTemplateLoader::LoadTemplateFile(const std::string& templateName, int depth) { // If this file was already loaded, we don't need to do anything if (m_TemplateFileData.find(templateName) != m_TemplateFileData.end()) return true; // Handle infinite loops more gracefully than running out of stack space and crashing if (depth > 100) { LOGERROR("Probable infinite inheritance loop in entity template '%s'", templateName.c_str()); 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('|'); if (pos != std::string::npos) { std::string prefix = templateName.substr(0, pos); std::string baseName = templateName.substr(pos+1); if (!LoadTemplateFile(baseName, depth+1)) { LOGERROR("Failed to load entity template '%s'", baseName.c_str()); return false; } 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; } 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; } // Normal case: templateName is an XML file: VfsPath path = VfsPath(TEMPLATE_ROOT) / wstring_from_utf8(templateName + ".xml"); CXeromyces xero; PSRETURN ok = xero.Load(g_VFS, path); if (ok != PSRETURN_OK) return false; // (Xeromyces already logged an error with the full filename) int attr_parent = xero.GetAttributeID("parent"); CStr parentName = xero.GetRoot().GetAttributes().GetNamedItem(attr_parent); if (!parentName.empty()) { // 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; } 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()); return true; } static Status AddToTemplates(const VfsPath& pathname, const CFileInfo& UNUSED(fileInfo), const uintptr_t cbData) { std::vector& templates = *(std::vector*)cbData; // Strip the .xml extension VfsPath pathstem = pathname.ChangeExtension(L""); // Strip the root from the path std::wstring 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 if (name.substr(0, 9) == L"template_") return INFO::OK; if (name.substr(0, 8) == L"special/") return INFO::OK; templates.push_back(std::string(name.begin(), name.end())); return INFO::OK; } static Status AddActorToTemplates(const VfsPath& pathname, const CFileInfo& UNUSED(fileInfo), const uintptr_t cbData) { std::vector& templates = *(std::vector*)cbData; // Strip the root from the path std::wstring name = pathname.string().substr(ARRAY_SIZE(ACTOR_ROOT)-1); templates.push_back("actor|" + std::string(name.begin(), name.end())); return INFO::OK; } bool CTemplateLoader::TemplateExists(const std::string& templateName) const { size_t pos = templateName.rfind('|'); std::string baseName(pos != std::string::npos ? templateName.substr(pos+1) : templateName); return VfsFileExists(VfsPath(TEMPLATE_ROOT) / wstring_from_utf8(baseName + ".xml")); } std::vector CTemplateLoader::FindTemplates(const std::string& path, bool includeSubdirectories, ETemplatesType templatesType) const { std::vector templates; if (templatesType != SIMULATION_TEMPLATES && templatesType != ACTOR_TEMPLATES && templatesType != ALL_TEMPLATES) { LOGERROR("Undefined template type (valid: all, simulation, actor)"); return templates; } size_t flags = includeSubdirectories ? vfs::DIR_RECURSIVE : 0; if (templatesType == SIMULATION_TEMPLATES || templatesType == ALL_TEMPLATES) WARN_IF_ERR(vfs::ForEachFile(g_VFS, VfsPath(TEMPLATE_ROOT) / path, AddToTemplates, (uintptr_t)&templates, L"*.xml", flags)); 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)); return templates; } const CParamNode& CTemplateLoader::GetTemplateFileData(const std::string& templateName) { // Load the template if necessary if (!LoadTemplateFile(templateName, 0)) { LOGERROR("Failed to load entity template '%s'", templateName.c_str()); return NULL_NODE; } return m_TemplateFileData[templateName]; } void CTemplateLoader::ConstructTemplateActor(const std::string& actorName, CParamNode& out) { // Copy the actor template out = GetTemplateFileData("special/actor"); - // Initialise 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 name = utf8_from_wstring(CParamNode::EscapeXMLString(actorNameW)); std::string xml = "" "" + name + "" - // 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. "1.0" "" "" - "actor.pngactor_mask.png" + "128x128/ellipse.png128x128/ellipse_mask.png" "" ""; CParamNode::LoadXMLString(out, xml.c_str(), actorNameW.c_str()); } Index: ps/trunk/source/simulation2/tests/test_CmpTemplateManager.h =================================================================== --- ps/trunk/source/simulation2/tests/test_CmpTemplateManager.h (revision 23703) +++ ps/trunk/source/simulation2/tests/test_CmpTemplateManager.h (revision 23704) @@ -1,258 +1,258 @@ -/* Copyright (C) 2017 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 "simulation2/system/ComponentManager.h" #include "simulation2/components/ICmpTemplateManager.h" #include "simulation2/components/ICmpTest.h" #include "simulation2/MessageTypes.h" #include "simulation2/system/ParamNode.h" #include "simulation2/system/SimContext.h" #include "simulation2/Simulation2.h" #include "graphics/Terrain.h" #include "ps/Filesystem.h" #include "ps/CLogger.h" #include "ps/XML/Xeromyces.h" class TestCmpTemplateManager : public CxxTest::TestSuite { public: void setUp() { g_VFS = CreateVfs(); TS_ASSERT_OK(g_VFS->Mount(L"", DataDir()/"mods"/"_test.sim", VFS_MOUNT_MUST_EXIST)); TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir()/"_testcache")); CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); g_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); } void test_LoadTemplate() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); man.LoadComponentTypes(); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.LookupEntityHandle(ent1, true); CParamNode noParam; TS_ASSERT(man.AddComponent(hnd1, CID_TemplateManager, noParam)); ICmpTemplateManager* tempMan = static_cast (man.QueryInterface(ent1, IID_TemplateManager)); TS_ASSERT(tempMan != NULL); const CParamNode* basic = tempMan->LoadTemplate(ent2, "basic"); TS_ASSERT(basic != NULL); TS_ASSERT_WSTR_EQUALS(basic->ToXML(), L"12345"); const CParamNode* inherit2 = tempMan->LoadTemplate(ent2, "inherit2"); TS_ASSERT(inherit2 != NULL); TS_ASSERT_WSTR_EQUALS(inherit2->ToXML(), L"d2e1f1g2"); const CParamNode* inherit1 = tempMan->LoadTemplate(ent2, "inherit1"); TS_ASSERT(inherit1 != NULL); TS_ASSERT_WSTR_EQUALS(inherit1->ToXML(), L"d1e1f1"); const CParamNode* actor = tempMan->LoadTemplate(ent2, "actor|example1"); TS_ASSERT(actor != NULL); TS_ASSERT_WSTR_EQUALS(actor->ToXML(), - L"1.0actor.pngactor_mask.png" + L"1.0128x128/ellipse.png128x128/ellipse_mask.png" L"example1falsefalsefalse"); } void test_LoadTemplate_scriptcache() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); man.LoadComponentTypes(); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.LookupEntityHandle(ent1, true); CParamNode noParam; TS_ASSERT(man.AddComponent(hnd1, CID_TemplateManager, noParam)); ICmpTemplateManager* tempMan = static_cast (man.QueryInterface(ent1, IID_TemplateManager)); TS_ASSERT(tempMan != NULL); tempMan->DisableValidation(); JSContext* cx = man.GetScriptInterface().GetContext(); JSAutoRequest rq(cx); // This is testing some bugs in the template JS object caching const CParamNode* inherit1 = tempMan->LoadTemplate(ent2, "inherit1"); JS::RootedValue val(cx); ScriptInterface::ToJSVal(cx, &val, inherit1); TS_ASSERT_STR_EQUALS(man.GetScriptInterface().ToString(&val), "({Test1A:{'@a':\"a1\", '@b':\"b1\", '@c':\"c1\", d:\"d1\", e:\"e1\", f:\"f1\"}})"); const CParamNode* inherit2 = tempMan->LoadTemplate(ent2, "inherit2"); ScriptInterface::ToJSVal(cx, &val, inherit2); TS_ASSERT_STR_EQUALS(man.GetScriptInterface().ToString(&val), "({'@parent':\"inherit1\", Test1A:{'@a':\"a2\", '@b':\"b1\", '@c':\"c1\", d:\"d2\", e:\"e1\", f:\"f1\", g:\"g2\"}})"); const CParamNode* actor = tempMan->LoadTemplate(ent2, "actor|example1"); ScriptInterface::ToJSVal(cx, &val, &actor->GetChild("VisualActor")); TS_ASSERT_STR_EQUALS(man.GetScriptInterface().ToString(&val), "({Actor:\"example1\", ActorOnly:(void 0), SilhouetteDisplay:\"false\", SilhouetteOccluder:\"false\", VisibleInAtlasOnly:\"false\"})"); const CParamNode* foundation = tempMan->LoadTemplate(ent2, "foundation|actor|example1"); ScriptInterface::ToJSVal(cx, &val, &foundation->GetChild("VisualActor")); TS_ASSERT_STR_EQUALS(man.GetScriptInterface().ToString(&val), "({Actor:\"example1\", ActorOnly:(void 0), Foundation:(void 0), SilhouetteDisplay:\"false\", SilhouetteOccluder:\"false\", VisibleInAtlasOnly:\"false\"})"); #define GET_FIRST_ELEMENT(n, templateName) \ const CParamNode* n = tempMan->LoadTemplate(ent2, templateName); \ for (CParamNode::ChildrenMap::const_iterator it = n->GetChildren().begin(); it != n->GetChildren().end(); ++it) \ { \ if (it->first[0] == '@') \ continue; \ ScriptInterface::ToJSVal(cx, &val, it->second); \ break; \ } GET_FIRST_ELEMENT(n1, "inherit_a"); TS_ASSERT_STR_EQUALS(man.GetScriptInterface().ToString(&val), "({'@datatype':\"tokens\", _string:\"a b c\"})"); GET_FIRST_ELEMENT(n2, "inherit_b"); TS_ASSERT_STR_EQUALS(man.GetScriptInterface().ToString(&val), "({'@datatype':\"tokens\", _string:\"a b c d\"})"); GET_FIRST_ELEMENT(n3, "inherit_c"); TS_ASSERT_STR_EQUALS(man.GetScriptInterface().ToString(&val), "({'@a':\"b\", _string:\"a b c\"})"); GET_FIRST_ELEMENT(n4, "inherit_d"); TS_ASSERT_STR_EQUALS(man.GetScriptInterface().ToString(&val), "({'@a':\"b\", '@c':\"d\"})"); #undef GET_FIRST_ELEMENT } void test_LoadTemplate_errors() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); man.LoadComponentTypes(); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.LookupEntityHandle(ent1, true); CParamNode noParam; TS_ASSERT(man.AddComponent(hnd1, CID_TemplateManager, noParam)); ICmpTemplateManager* tempMan = static_cast (man.QueryInterface(ent1, IID_TemplateManager)); TS_ASSERT(tempMan != NULL); TestLogger logger; TS_ASSERT(tempMan->LoadTemplate(ent2, "illformed") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "invalid") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "nonexistent") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "inherit-loop") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "inherit-broken") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "inherit-special") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "preview|nonexistent") == NULL); } void test_LoadTemplate_multiple() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); man.LoadComponentTypes(); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.LookupEntityHandle(ent1, true); CParamNode noParam; TS_ASSERT(man.AddComponent(hnd1, CID_TemplateManager, noParam)); ICmpTemplateManager* tempMan = static_cast (man.QueryInterface(ent1, IID_TemplateManager)); TS_ASSERT(tempMan != NULL); const CParamNode* basicA = tempMan->LoadTemplate(ent2, "basic"); TS_ASSERT(basicA != NULL); const CParamNode* basicB = tempMan->LoadTemplate(ent2, "basic"); TS_ASSERT(basicA == basicB); const CParamNode* inherit2A = tempMan->LoadTemplate(ent2, "inherit2"); TS_ASSERT(inherit2A != NULL); const CParamNode* inherit2B = tempMan->LoadTemplate(ent2, "inherit2"); TS_ASSERT(inherit2A == inherit2B); TestLogger logger; TS_ASSERT(tempMan->LoadTemplate(ent2, "nonexistent") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "nonexistent") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "inherit-loop") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "inherit-loop") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "inherit-broken") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "inherit-broken") == NULL); } }; class TestCmpTemplateManager_2 : public CxxTest::TestSuite { public: void setUp() { g_VFS = CreateVfs(); TS_ASSERT_OK(g_VFS->Mount(L"", DataDir()/"mods"/"mod", VFS_MOUNT_MUST_EXIST)); TS_ASSERT_OK(g_VFS->Mount(L"", DataDir()/"mods"/"public", VFS_MOUNT_MUST_EXIST)); TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir()/"_testcache")); CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); g_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); } // This just attempts loading every public entity, to check there's no validation errors void test_load_all_DISABLED() // disabled since it's a bit slow and noisy { CTerrain dummy; CSimulation2 sim(NULL, g_ScriptRuntime, &dummy); sim.LoadDefaultScripts(); sim.ResetState(); CmpPtr cmpTemplateManager(sim, SYSTEM_ENTITY); TS_ASSERT(cmpTemplateManager); std::vector templates = cmpTemplateManager->FindAllTemplates(true); for (size_t i = 0; i < templates.size(); ++i) { std::string name = templates[i]; printf("# %s\n", name.c_str()); const CParamNode* p = cmpTemplateManager->GetTemplate(name); TS_ASSERT(p != NULL); } } };