Index: ps/trunk/source/ps/XML/Xeromyces.cpp =================================================================== --- ps/trunk/source/ps/XML/Xeromyces.cpp (revision 11099) +++ ps/trunk/source/ps/XML/Xeromyces.cpp (revision 11100) @@ -1,371 +1,372 @@ -/* Copyright (C) 2010 Wildfire Games. +/* Copyright (C) 2012 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 #include #include #include #include #include "maths/MD5.h" #include "ps/CacheLoader.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "Xeromyces.h" #include static void errorHandler(void* UNUSED(userData), xmlErrorPtr error) { // Strip a trailing newline std::string message = error->message; if (message.length() > 0 && message[message.length()-1] == '\n') message.erase(message.length()-1); LOGERROR(L"CXeromyces: Parse %ls: %hs:%d: %hs", error->level == XML_ERR_WARNING ? L"warning" : L"error", error->file, error->line, message.c_str()); // TODO: The (non-fatal) warnings and errors don't get stored in the XMB, // so the caching is less transparent than it should be } static bool g_XeromycesStarted = false; void CXeromyces::Startup() { ENSURE(!g_XeromycesStarted); xmlInitParser(); xmlSetStructuredErrorFunc(NULL, &errorHandler); g_XeromycesStarted = true; } void CXeromyces::Terminate() { ENSURE(g_XeromycesStarted); xmlCleanupParser(); xmlSetStructuredErrorFunc(NULL, NULL); g_XeromycesStarted = false; } void CXeromyces::PrepareCacheKey(MD5& hash, u32& version) { // We don't have anything special to add into the hash UNUSED2(hash); // Arbitrary version number - change this if we update the code and // need to invalidate old users' caches version = 1; } PSRETURN CXeromyces::Load(const PIVFS& vfs, const VfsPath& filename) { ENSURE(g_XeromycesStarted); CCacheLoader cacheLoader(vfs, L".xmb"); MD5 hash; u32 version; PrepareCacheKey(hash, version); VfsPath xmbPath; Status ret = cacheLoader.TryLoadingCached(filename, MD5(), version, xmbPath); if (ret == INFO::OK) { // Found a cached XMB - load it if (ReadXMBFile(vfs, xmbPath)) return PSRETURN_OK; // If this fails then we'll continue and (re)create the loose cache - // this failure legitimately happens due to partially-written XMB files. } else if (ret == INFO::SKIPPED) { // No cached version was found - we'll need to create it } else { ENSURE(ret < 0); // No source file or archive cache was found, so we can't load the // XML file at all + LOGERROR(L"CCacheLoader failed to find archived or source file for: \"%ls\"", filename.string().c_str()); return PSRETURN_Xeromyces_XMLOpenFailed; } // XMB isn't up to date with the XML, so rebuild it return ConvertFile(vfs, filename, xmbPath); } bool CXeromyces::GenerateCachedXMB(const PIVFS& vfs, const VfsPath& sourcePath, VfsPath& archiveCachePath) { CCacheLoader cacheLoader(vfs, L".xmb"); MD5 hash; u32 version; PrepareCacheKey(hash, version); archiveCachePath = cacheLoader.ArchiveCachePath(sourcePath); return (ConvertFile(vfs, sourcePath, VfsPath("cache") / archiveCachePath) == PSRETURN_OK); } PSRETURN CXeromyces::ConvertFile(const PIVFS& vfs, const VfsPath& filename, const VfsPath& xmbPath) { CVFSFile input; if (input.Load(vfs, filename)) { LOGERROR(L"CXeromyces: Failed to open XML file %ls", filename.string().c_str()); return PSRETURN_Xeromyces_XMLOpenFailed; } CStr8 filename8(CStrW(filename.string()).ToUTF8()); xmlDocPtr doc = xmlReadMemory((const char*)input.GetBuffer(), (int)input.GetBufferSize(), filename8.c_str(), NULL, XML_PARSE_NONET|XML_PARSE_NOCDATA); if (! doc) { LOGERROR(L"CXeromyces: Failed to parse XML file %ls", filename.string().c_str()); return PSRETURN_Xeromyces_XMLParseError; } WriteBuffer writeBuffer; CreateXMB(doc, writeBuffer); xmlFreeDoc(doc); // Save the file to disk, so it can be loaded quickly next time vfs->CreateFile(xmbPath, writeBuffer.Data(), writeBuffer.Size()); m_XMBBuffer = writeBuffer.Data(); // add a reference // Set up the XMBFile const bool ok = Initialise((const char*)m_XMBBuffer.get()); ENSURE(ok); return PSRETURN_OK; } bool CXeromyces::ReadXMBFile(const PIVFS& vfs, const VfsPath& filename) { size_t size; if(vfs->LoadFile(filename, m_XMBBuffer, size) < 0) return false; // if the game crashes during loading, (e.g. due to driver bugs), // it sometimes leaves empty XMB files in the cache. // reporting failure will cause our caller to re-generate the XMB. if(size == 0) return false; ENSURE(size >= 4); // make sure it's at least got the initial header // Set up the XMBFile if(!Initialise((const char*)m_XMBBuffer.get())) return false; return true; } PSRETURN CXeromyces::LoadString(const char* xml) { ENSURE(g_XeromycesStarted); xmlDocPtr doc = xmlReadMemory(xml, (int)strlen(xml), "", NULL, XML_PARSE_NONET|XML_PARSE_NOCDATA); if (! doc) { LOGERROR(L"CXeromyces: Failed to parse XML string"); return PSRETURN_Xeromyces_XMLParseError; } WriteBuffer writeBuffer; CreateXMB(doc, writeBuffer); xmlFreeDoc(doc); m_XMBBuffer = writeBuffer.Data(); // add a reference // Set up the XMBFile const bool ok = Initialise((const char*)m_XMBBuffer.get()); ENSURE(ok); return PSRETURN_OK; } static void FindNames(const xmlNodePtr node, std::set& elementNames, std::set& attributeNames) { elementNames.insert((const char*)node->name); for (xmlAttrPtr attr = node->properties; attr; attr = attr->next) attributeNames.insert((const char*)attr->name); for (xmlNodePtr child = node->children; child; child = child->next) if (child->type == XML_ELEMENT_NODE) FindNames(child, elementNames, attributeNames); } static void OutputElement(const xmlNodePtr node, WriteBuffer& writeBuffer, std::map& elementIDs, std::map& attributeIDs ) { // Filled in later with the length of the element size_t posLength = writeBuffer.Size(); writeBuffer.Append("????", 4); writeBuffer.Append(&elementIDs[(const char*)node->name], 4); u32 attrCount = 0; for (xmlAttrPtr attr = node->properties; attr; attr = attr->next) ++attrCount; writeBuffer.Append(&attrCount, 4); u32 childCount = 0; for (xmlNodePtr child = node->children; child; child = child->next) if (child->type == XML_ELEMENT_NODE) ++childCount; writeBuffer.Append(&childCount, 4); // Filled in later with the offset to the list of child elements size_t posChildrenOffset = writeBuffer.Size(); writeBuffer.Append("????", 4); // Trim excess whitespace in the entity's text, while counting // the number of newlines trimmed (so that JS error reporting // can give the correct line number within the script) std::string whitespace = " \t\r\n"; std::string text; for (xmlNodePtr child = node->children; child; child = child->next) { if (child->type == XML_TEXT_NODE) { xmlChar* content = xmlNodeGetContent(child); text += std::string((const char*)content); xmlFree(content); } } u32 linenum = xmlGetLineNo(node); // Find the start of the non-whitespace section size_t first = text.find_first_not_of(whitespace); if (first == text.npos) // Entirely whitespace - easy to handle text = ""; else { // Count the number of \n being cut off, // and add them to the line number std::string trimmed (text.begin(), text.begin()+first); linenum += std::count(trimmed.begin(), trimmed.end(), '\n'); // Find the end of the non-whitespace section, // and trim off everything else size_t last = text.find_last_not_of(whitespace); text = text.substr(first, 1+last-first); } // Output text, prefixed by length in bytes if (text.length() == 0) { // No text; don't write much writeBuffer.Append("\0\0\0\0", 4); } else { // Write length and line number and null-terminated text utf16string textW = CStr8(text).FromUTF8().utf16(); u32 nodeLen = u32(4 + 2*(textW.length()+1)); writeBuffer.Append(&nodeLen, 4); writeBuffer.Append(&linenum, 4); writeBuffer.Append((void*)textW.c_str(), nodeLen-4); } // Output attributes for (xmlAttrPtr attr = node->properties; attr; attr = attr->next) { writeBuffer.Append(&attributeIDs[(const char*)attr->name], 4); xmlChar* value = xmlNodeGetContent(attr->children); utf16string textW = CStr8((const char*)value).FromUTF8().utf16(); xmlFree(value); u32 attrLen = u32(2*(textW.length()+1)); writeBuffer.Append(&attrLen, 4); writeBuffer.Append((void*)textW.c_str(), attrLen); } // Go back and fill in the child-element offset u32 childrenOffset = (u32)(writeBuffer.Size() - (posChildrenOffset+4)); writeBuffer.Overwrite(&childrenOffset, 4, posChildrenOffset); // Output all child elements for (xmlNodePtr child = node->children; child; child = child->next) if (child->type == XML_ELEMENT_NODE) OutputElement(child, writeBuffer, elementIDs, attributeIDs); // Go back and fill in the length u32 length = (u32)(writeBuffer.Size() - posLength); writeBuffer.Overwrite(&length, 4, posLength); } PSRETURN CXeromyces::CreateXMB(const xmlDocPtr doc, WriteBuffer& writeBuffer) { // Header writeBuffer.Append(UnfinishedHeaderMagicStr, 4); std::set::iterator it; u32 i; // Find the unique element/attribute names std::set elementNames; std::set attributeNames; FindNames(xmlDocGetRootElement(doc), elementNames, attributeNames); std::map elementIDs; std::map attributeIDs; // Output element names i = 0; u32 elementCount = (u32)elementNames.size(); writeBuffer.Append(&elementCount, 4); for (it = elementNames.begin(); it != elementNames.end(); ++it) { u32 textLen = (u32)it->length()+1; writeBuffer.Append(&textLen, 4); writeBuffer.Append((void*)it->c_str(), textLen); elementIDs[*it] = i++; } // Output attribute names i = 0; u32 attributeCount = (u32)attributeNames.size(); writeBuffer.Append(&attributeCount, 4); for (it = attributeNames.begin(); it != attributeNames.end(); ++it) { u32 textLen = (u32)it->length()+1; writeBuffer.Append(&textLen, 4); writeBuffer.Append((void*)it->c_str(), textLen); attributeIDs[*it] = i++; } OutputElement(xmlDocGetRootElement(doc), writeBuffer, elementIDs, attributeIDs); // file is now valid, so insert correct magic string writeBuffer.Overwrite(HeaderMagicStr, 4, 0); return PSRETURN_OK; } Index: ps/trunk/source/ps/CacheLoader.cpp =================================================================== --- ps/trunk/source/ps/CacheLoader.cpp (revision 11099) +++ ps/trunk/source/ps/CacheLoader.cpp (revision 11100) @@ -1,147 +1,150 @@ -/* Copyright (C) 2010 Wildfire Games. +/* Copyright (C) 2012 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 "CacheLoader.h" #include "ps/CLogger.h" #include "maths/MD5.h" #include CCacheLoader::CCacheLoader(PIVFS vfs, const std::wstring& fileExtension) : m_VFS(vfs), m_FileExtension(fileExtension) { } Status CCacheLoader::TryLoadingCached(const VfsPath& sourcePath, const MD5& initialHash, u32 version, VfsPath& loadPath) { VfsPath archiveCachePath = ArchiveCachePath(sourcePath); // Try the archive cache file first if (CanUseArchiveCache(sourcePath, archiveCachePath)) { loadPath = archiveCachePath; return INFO::OK; } // Fail if no source or archive cache + // Note: this is not always an error case, because for instance there + // are some uncached .pmd/psa files in the game with no source .dae. + // This test fails (correctly) in that valid situation, so it seems + // best to leave the error handling to the caller. Status err = m_VFS->GetFileInfo(sourcePath, NULL); if (err < 0) { - LOGERROR(L"Failed to find file: \"%ls\"", sourcePath.string().c_str()); return err; } // Look for loose cache of source file VfsPath looseCachePath = LooseCachePath(sourcePath, initialHash, version); // If the loose cache file exists, use it if (m_VFS->GetFileInfo(looseCachePath, NULL) >= 0) { loadPath = looseCachePath; return INFO::OK; } // No cache - we'll need to regenerate it loadPath = looseCachePath; return INFO::SKIPPED; } bool CCacheLoader::CanUseArchiveCache(const VfsPath& sourcePath, const VfsPath& archiveCachePath) { // We want to use the archive cache whenever possible, // unless it's superseded by a source file that the user has edited size_t archiveCachePriority = 0; size_t sourcePriority = 0; bool archiveCacheExists = (m_VFS->GetFilePriority(archiveCachePath, &archiveCachePriority) >= 0); // Can't use it if there's no cache if (!archiveCacheExists) return false; bool sourceExists = (m_VFS->GetFilePriority(sourcePath, &sourcePriority) >= 0); // Must use the cache if there's no source if (!sourceExists) return true; // If source file is from a higher-priority mod than archive cache, // don't use the old cache if (archiveCachePriority < sourcePriority) return false; // If source file is more recent than the archive cache (i.e. the user has edited it), // don't use the old cache FileInfo sourceInfo, archiveCacheInfo; if (m_VFS->GetFileInfo(sourcePath, &sourceInfo) >= 0 && m_VFS->GetFileInfo(archiveCachePath, &archiveCacheInfo) >= 0) { const double howMuchNewer = difftime(sourceInfo.MTime(), archiveCacheInfo.MTime()); const double threshold = 2.0; // FAT timestamp resolution [seconds] if (howMuchNewer > threshold) return false; } // Otherwise we can use the cache return true; } VfsPath CCacheLoader::ArchiveCachePath(const VfsPath& sourcePath) { return sourcePath.ChangeExtension(sourcePath.Extension().string() + L".cached" + m_FileExtension); } VfsPath CCacheLoader::LooseCachePath(const VfsPath& sourcePath, const MD5& initialHash, u32 version) { FileInfo fileInfo; if (m_VFS->GetFileInfo(sourcePath, &fileInfo) < 0) { debug_warn(L"source file disappeared"); // this should never happen return VfsPath(); } u64 mtime = (u64)fileInfo.MTime() & ~1; // skip lowest bit, since zip and FAT don't preserve it u64 size = (u64)fileInfo.Size(); // Construct a hash of the file data and settings. MD5 hash = initialHash; hash.Update((const u8*)&mtime, sizeof(mtime)); hash.Update((const u8*)&size, sizeof(size)); hash.Update((const u8*)&version, sizeof(version)); // these are local cached files, so we don't care about endianness etc // Use a short prefix of the full hash (we don't need high collision-resistance), // converted to hex u8 digest[MD5::DIGESTSIZE]; hash.Final(digest); std::wstringstream digestPrefix; digestPrefix << std::hex; for (size_t i = 0; i < 8; ++i) digestPrefix << std::setfill(L'0') << std::setw(2) << (int)digest[i]; // Construct the final path return VfsPath("cache") / sourcePath.ChangeExtension(sourcePath.Extension().string() + L"." + digestPrefix.str() + m_FileExtension); // TODO: we should probably include the mod name, once that's possible (http://trac.wildfiregames.com/ticket/564) } Index: ps/trunk/source/ps/CacheLoader.h =================================================================== --- ps/trunk/source/ps/CacheLoader.h (revision 11099) +++ ps/trunk/source/ps/CacheLoader.h (revision 11100) @@ -1,74 +1,74 @@ -/* Copyright (C) 2010 Wildfire Games. +/* Copyright (C) 2012 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 . */ #ifndef INCLUDED_CACHELOADER #define INCLUDED_CACHELOADER #include "lib/file/vfs/vfs.h" class MD5; /** * Helper class for systems that have an expensive cacheable conversion process * when loading files. * * Conversion output can be automatically cached as loose files, indexed by a hash * of the file's timestamp and size plus any other data the caller provides. * This allows developers and modders to easily produce new files, with the conversion * happening transparently. * * For release packages, files can be precached by appending ".cached.{extension}" * to their name, which will be used instead of doing runtime conversion. * These cache files will typically be packed into an archive for faster loading; * if no archive cache is available then the source file will be converted and stored * as a loose cache file instead. */ class CCacheLoader { public: CCacheLoader(PIVFS vfs, const std::wstring& fileExtension); /** * Attempts to find a valid cached which can be loaded. * Returns INFO::OK and sets loadPath to the cached file if there is one. * Returns INFO::SKIPPED and sets loadPath to the desire loose cache name if there isn't one. - * Returns a value < 0 on error (e.g. the source file doesn't exist). + * Returns a value < 0 on error (e.g. the source file doesn't exist). No error is logged or thrown. */ Status TryLoadingCached(const VfsPath& sourcePath, const MD5& initialHash, u32 version, VfsPath& loadPath); /** * Determines whether we can safely use the archived cache file, or need to * re-convert the source file. */ bool CanUseArchiveCache(const VfsPath& sourcePath, const VfsPath& archiveCachePath); /** * Return the path of the archive cache for the given source file. */ VfsPath ArchiveCachePath(const VfsPath& sourcePath); /** * Return the path of the loose cache for the given source file. */ VfsPath LooseCachePath(const VfsPath& sourcePath, const MD5& initialHash, u32 version); private: PIVFS m_VFS; std::wstring m_FileExtension; }; #endif // INCLUDED_CACHELOADER Index: ps/trunk/source/ps/ArchiveBuilder.cpp =================================================================== --- ps/trunk/source/ps/ArchiveBuilder.cpp (revision 11099) +++ ps/trunk/source/ps/ArchiveBuilder.cpp (revision 11100) @@ -1,144 +1,182 @@ /* Copyright (C) 2012 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 "ArchiveBuilder.h" #include "graphics/TextureManager.h" +#include "graphics/ColladaManager.h" #include "lib/tex/tex_codec.h" #include "lib/file/archive/archive_zip.h" #include "lib/file/vfs/vfs_util.h" #include "ps/XML/Xeromyces.h" // Disable "'boost::algorithm::detail::is_classifiedF' : assignment operator could not be generated" #if MSC_VERSION #pragma warning(disable:4512) #endif #include CArchiveBuilder::CArchiveBuilder(const OsPath& mod, const OsPath& tempdir) : m_TempDir(tempdir) { tex_codec_register_all(); m_VFS = CreateVfs(20*MiB); DeleteDirectory(m_TempDir/"_archivecache"); // clean up in case the last run failed - m_VFS->Mount(L"cache/", m_TempDir/"_archivecache/"); + m_VFS->Mount(L"cache/", m_TempDir/"_archivecache"/""); m_VFS->Mount(L"", mod/"", VFS_MOUNT_MUST_EXIST | VFS_MOUNT_KEEP_DELETED); // Collect the list of files before loading any base mods vfs::ForEachFile(m_VFS, L"", &CollectFileCB, (uintptr_t)static_cast(this), 0, vfs::DIR_RECURSIVE); } CArchiveBuilder::~CArchiveBuilder() { m_VFS.reset(); DeleteDirectory(m_TempDir/"_archivecache"); tex_codec_unregister_all(); } void CArchiveBuilder::AddBaseMod(const OsPath& mod) { m_VFS->Mount(L"", mod/"", VFS_MOUNT_MUST_EXIST); } void CArchiveBuilder::Build(const OsPath& archive) { // Disable zip compression because it significantly hurts download size // for releases (which re-compress all files with better compression // algorithms) - it's probably most important currently to optimise for // download size rather than install size or startup performance. // (See http://trac.wildfiregames.com/ticket/671) const bool noDeflate = true; PIArchiveWriter writer = CreateArchiveWriter_Zip(archive, noDeflate); // Use CTextureManager instead of CTextureConverter directly, // so it can deal with all the loading of settings.xml files - CTextureManager texman(m_VFS, true, true); + CTextureManager textureManager(m_VFS, true, true); + + CColladaManager colladaManager(m_VFS); CXeromyces xero; for (size_t i = 0; i < m_Files.size(); ++i) { Status ret; const VfsPath path = m_Files[i]; OsPath realPath; ret = m_VFS->GetRealPath(path, realPath); ENSURE(ret == INFO::OK); // Compress textures and store the new cached version instead of the original if (boost::algorithm::starts_with(path.string(), L"art/textures/") && tex_is_known_extension(path) && // Skip some subdirectories where the engine doesn't use CTextureManager yet: !boost::algorithm::starts_with(path.string(), L"art/textures/cursors/") && !boost::algorithm::starts_with(path.string(), L"art/textures/terrain/alphamaps/") ) { VfsPath cachedPath; debug_printf(L"Converting texture %ls\n", realPath.string().c_str()); - bool ok = texman.GenerateCachedTexture(path, cachedPath); + bool ok = textureManager.GenerateCachedTexture(path, cachedPath); ENSURE(ok); OsPath cachedRealPath; ret = m_VFS->GetRealPath(VfsPath("cache")/cachedPath, cachedRealPath); ENSURE(ret == INFO::OK); writer->AddFile(cachedRealPath, cachedPath); // We don't want to store the original file too (since it's a // large waste of space), so skip to the next file continue; } - // TODO: should cache DAE->PMD and DAE->PSA conversions too + // Convert DAE models and store the new cached version instead of the original + if (path.Extension() == L".dae") + { + CColladaManager::FileType type; + + if (boost::algorithm::starts_with(path.string(), L"art/meshes/")) + type = CColladaManager::PMD; + else if (boost::algorithm::starts_with(path.string(), L"art/animation/")) + type = CColladaManager::PSA; + else + { + // Unknown type of DAE, just add to archive and continue + writer->AddFile(realPath, path); + continue; + } + + VfsPath cachedPath; + debug_printf(L"Converting model %ls\n", realPath.string().c_str()); + bool ok = colladaManager.GenerateCachedFile(path, type, cachedPath); + + // The DAE might fail to convert for whatever reason, and in that case + // it can't be used in the game, so we just exclude it + // (alternatively we could throw release blocking errors on useless files) + if (ok) + { + OsPath cachedRealPath; + ret = m_VFS->GetRealPath(VfsPath("cache")/cachedPath, cachedRealPath); + ENSURE(ret == INFO::OK); + + writer->AddFile(cachedRealPath, cachedPath); + } + + // We don't want to store the original file too (since it's a + // large waste of space), so skip to the next file + continue; + } debug_printf(L"Adding %ls\n", realPath.string().c_str()); writer->AddFile(realPath, path); // Also cache XMB versions of all XML files if (path.Extension() == L".xml") { VfsPath cachedPath; debug_printf(L"Converting XML file %ls\n", realPath.string().c_str()); bool ok = xero.GenerateCachedXMB(m_VFS, path, cachedPath); ENSURE(ok); OsPath cachedRealPath; ret = m_VFS->GetRealPath(VfsPath("cache")/cachedPath, cachedRealPath); ENSURE(ret == INFO::OK); writer->AddFile(cachedRealPath, cachedPath); } } } Status CArchiveBuilder::CollectFileCB(const VfsPath& pathname, const FileInfo& UNUSED(fileInfo), const uintptr_t cbData) { CArchiveBuilder* self = static_cast((void*)cbData); self->m_Files.push_back(pathname); return INFO::OK; } Index: ps/trunk/source/tools/atlas/GameInterface/ActorViewer.cpp =================================================================== --- ps/trunk/source/tools/atlas/GameInterface/ActorViewer.cpp (revision 11099) +++ ps/trunk/source/tools/atlas/GameInterface/ActorViewer.cpp (revision 11100) @@ -1,526 +1,527 @@ /* Copyright (C) 2012 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 "ActorViewer.h" #include "View.h" #include "graphics/ColladaManager.h" #include "graphics/LOSTexture.h" #include "graphics/Unit.h" #include "graphics/Model.h" #include "graphics/ModelDef.h" #include "graphics/ObjectManager.h" #include "graphics/ParticleManager.h" #include "graphics/Patch.h" #include "graphics/SkeletonAnimManager.h" #include "graphics/Terrain.h" #include "graphics/TerrainTextureEntry.h" #include "graphics/TerrainTextureManager.h" #include "graphics/TerritoryTexture.h" #include "graphics/UnitManager.h" #include "graphics/Overlay.h" #include "maths/MathUtil.h" +#include "ps/Filesystem.h" #include "ps/Font.h" #include "ps/CLogger.h" #include "ps/GameSetup/Config.h" #include "ps/ProfileViewer.h" #include "renderer/Renderer.h" #include "renderer/Scene.h" #include "renderer/SkyManager.h" #include "renderer/WaterManager.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/components/ICmpTerrain.h" #include "simulation2/components/ICmpUnitMotion.h" #include "simulation2/components/ICmpVisual.h" #include "simulation2/helpers/Render.h" struct ActorViewerImpl : public Scene { NONCOPYABLE(ActorViewerImpl); public: ActorViewerImpl() : Entity(INVALID_ENTITY), Terrain(), - ColladaManager(), + ColladaManager(g_VFS), MeshManager(ColladaManager), SkeletonAnimManager(ColladaManager), UnitManager(), Simulation2(&UnitManager, &Terrain), ObjectManager(MeshManager, SkeletonAnimManager, Simulation2), LOSTexture(Simulation2), TerritoryTexture(Simulation2) { UnitManager.SetObjectManager(ObjectManager); } entity_id_t Entity; CStrW CurrentUnitID; CStrW CurrentUnitAnim; float CurrentSpeed; bool WalkEnabled; bool GroundEnabled; bool ShadowsEnabled; bool SelectionBoxEnabled; bool AxesMarkerEnabled; int PropPointsMode; // 0 disabled, 1 for point markers, 2 for point markers + axes SColor4ub Background; CTerrain Terrain; CColladaManager ColladaManager; CMeshManager MeshManager; CSkeletonAnimManager SkeletonAnimManager; CObjectManager ObjectManager; CUnitManager UnitManager; CSimulation2 Simulation2; CLOSTexture LOSTexture; CTerritoryTexture TerritoryTexture; SOverlayLine SelectionBoxOverlay; SOverlayLine AxesMarkerOverlays[3]; std::vector Props; std::vector PropPointOverlays; // Simplistic implementation of the Scene interface virtual void EnumerateObjects(const CFrustum& frustum, SceneCollector* c) { if (GroundEnabled) { for (ssize_t pj = 0; pj < Terrain.GetPatchesPerSide(); ++pj) for (ssize_t pi = 0; pi < Terrain.GetPatchesPerSide(); ++pi) c->Submit(Terrain.GetPatch(pi, pj)); } CmpPtr cmpVisual(Simulation2, Entity); if (cmpVisual) { // add selection box outlines manually if (SelectionBoxEnabled) { SelectionBoxOverlay.m_Color = CColor(35/255.f, 86/255.f, 188/255.f, .75f); // pretty blue SelectionBoxOverlay.m_Thickness = 2; SimRender::ConstructBoxOutline(cmpVisual->GetSelectionBox(), SelectionBoxOverlay); c->Submit(&SelectionBoxOverlay); } // add origin axis thingy if (AxesMarkerEnabled) { CMatrix3D worldSpaceAxes; // offset from the ground a little bit to prevent fighting with the floor texture (also note: SetTranslation // sets the identity 3x3 transformation matrix, which are the world axes) worldSpaceAxes.SetTranslation(cmpVisual->GetPosition() + CVector3D(0, 0.02f, 0)); SimRender::ConstructAxesMarker(worldSpaceAxes, AxesMarkerOverlays[0], AxesMarkerOverlays[1], AxesMarkerOverlays[2]); c->Submit(&AxesMarkerOverlays[0]); c->Submit(&AxesMarkerOverlays[1]); c->Submit(&AxesMarkerOverlays[2]); } // add prop point overlays if (PropPointsMode > 0 && Props.size() > 0) { PropPointOverlays.clear(); // doesn't clear capacity, but should be ok since the number of prop points is usually pretty limited for (size_t i = 0; i < Props.size(); ++i) { CModel::Prop& prop = Props[i]; if (prop.m_Model) // should always be the case { // prop point positions are automatically updated during animations etc. by CModel::ValidatePosition const CMatrix3D& propCoordSystem = prop.m_Model->GetTransform(); SOverlayLine pointGimbal; pointGimbal.m_Color = CColor(1.f, 0.f, 1.f, 1.f); SimRender::ConstructGimbal(propCoordSystem.GetTranslation(), 0.05f, pointGimbal); PropPointOverlays.push_back(pointGimbal); if (PropPointsMode > 1) { // scale the prop axes coord system down a bit to distinguish them from the main world-space axes markers CMatrix3D displayCoordSystem = propCoordSystem; displayCoordSystem.Scale(0.5f, 0.5f, 0.5f); // revert translation scaling displayCoordSystem._14 = propCoordSystem._14; displayCoordSystem._24 = propCoordSystem._24; displayCoordSystem._34 = propCoordSystem._34; // construct an XYZ axes marker for the prop's coordinate system SOverlayLine xAxis, yAxis, zAxis; SimRender::ConstructAxesMarker(displayCoordSystem, xAxis, yAxis, zAxis); PropPointOverlays.push_back(xAxis); PropPointOverlays.push_back(yAxis); PropPointOverlays.push_back(zAxis); } } } for (size_t i = 0; i < PropPointOverlays.size(); ++i) { c->Submit(&PropPointOverlays[i]); } } } // send a RenderSubmit message so the components can submit their visuals to the renderer Simulation2.RenderSubmit(*c, frustum, false); } virtual CLOSTexture& GetLOSTexture() { return LOSTexture; } virtual CTerritoryTexture& GetTerritoryTexture() { return TerritoryTexture; } /** * Recursively fetches the props of the currently displayed entity model and its submodels, and stores them for rendering. */ void UpdatePropList(); void UpdatePropListRecursive(CModelAbstract* model); }; void ActorViewerImpl::UpdatePropList() { Props.clear(); CmpPtr cmpVisual(Simulation2, Entity); if (cmpVisual) { CUnit* unit = cmpVisual->GetUnit(); if (unit) { CModelAbstract& modelAbstract = unit->GetModel(); UpdatePropListRecursive(&modelAbstract); } } } void ActorViewerImpl::UpdatePropListRecursive(CModelAbstract* modelAbstract) { ENSURE(modelAbstract); CModel* model = modelAbstract->ToCModel(); if (model) { std::vector& modelProps = model->GetProps(); for (size_t i=0; i < modelProps.size(); i++) { CModel::Prop& modelProp = modelProps[i]; Props.push_back(modelProp); if (modelProp.m_Model) UpdatePropListRecursive(modelProp.m_Model); } } } ActorViewer::ActorViewer() : m(*new ActorViewerImpl()) { m.WalkEnabled = false; m.GroundEnabled = true; m.ShadowsEnabled = g_Renderer.GetOptionBool(CRenderer::OPT_SHADOWS); m.SelectionBoxEnabled = false; m.AxesMarkerEnabled = false; m.PropPointsMode = 0; m.Background = SColor4ub(0, 0, 0, 255); // Create a tiny empty piece of terrain, just so we can put shadows // on it without having to think too hard m.Terrain.Initialize(2, NULL); CTerrainTextureEntry* tex = g_TexMan.FindTexture("whiteness"); if (tex) { for (ssize_t pi = 0; pi < m.Terrain.GetPatchesPerSide(); ++pi) { for (ssize_t pj = 0; pj < m.Terrain.GetPatchesPerSide(); ++pj) { CPatch* patch = m.Terrain.GetPatch(pi, pj); for (ssize_t i = 0; i < PATCH_SIZE; ++i) { for (ssize_t j = 0; j < PATCH_SIZE; ++j) { CMiniPatch& mp = patch->m_MiniPatches[i][j]; mp.Tex = tex; mp.Priority = 0; } } } } } else { debug_warn(L"Failed to load whiteness texture"); } // Start the simulation m.Simulation2.LoadDefaultScripts(); m.Simulation2.ResetState(); // Tell the simulation we've already loaded the terrain CmpPtr cmpTerrain(m.Simulation2, SYSTEM_ENTITY); if (cmpTerrain) cmpTerrain->ReloadTerrain(); CmpPtr cmpRangeManager(m.Simulation2, SYSTEM_ENTITY); if (cmpRangeManager) cmpRangeManager->SetLosRevealAll(-1, true); } ActorViewer::~ActorViewer() { delete &m; } CSimulation2* ActorViewer::GetSimulation2() { return &m.Simulation2; } entity_id_t ActorViewer::GetEntity() { return m.Entity; } void ActorViewer::UnloadObjects() { m.ObjectManager.UnloadObjects(); } void ActorViewer::SetActor(const CStrW& name, const CStrW& animation) { bool needsAnimReload = false; CStrW id = name; // Recreate the entity, if we don't have one or if the new one is different if (m.Entity == INVALID_ENTITY || id != m.CurrentUnitID) { // Delete the old entity (if any) if (m.Entity != INVALID_ENTITY) { m.Simulation2.DestroyEntity(m.Entity); m.Simulation2.FlushDestroyedEntities(); m.Entity = INVALID_ENTITY; } // Clear particles associated with deleted entity g_Renderer.GetParticleManager().ClearUnattachedEmitters(); // If there's no actor to display, return with nothing loaded if (id.empty()) return; m.Entity = m.Simulation2.AddEntity(L"preview|" + id); if (m.Entity == INVALID_ENTITY) return; CmpPtr cmpPosition(m.Simulation2, m.Entity); if (cmpPosition) { ssize_t c = TERRAIN_TILE_SIZE * m.Terrain.GetPatchesPerSide()*PATCH_SIZE/2; cmpPosition->JumpTo(entity_pos_t::FromInt(c), entity_pos_t::FromInt(c)); cmpPosition->SetYRotation(entity_angle_t::Pi()); } needsAnimReload = true; } if (animation != m.CurrentUnitAnim) needsAnimReload = true; if (needsAnimReload) { CStr anim = animation.ToUTF8().LowerCase(); // Emulate the typical simulation animation behaviour float speed; float repeattime = 0.f; if (anim == "walk") { CmpPtr cmpUnitMotion(m.Simulation2, m.Entity); if (cmpUnitMotion) speed = cmpUnitMotion->GetWalkSpeed().ToFloat(); else speed = 7.f; // typical unit speed m.CurrentSpeed = speed; } else if (anim == "run") { CmpPtr cmpUnitMotion(m.Simulation2, m.Entity); if (cmpUnitMotion) speed = cmpUnitMotion->GetRunSpeed().ToFloat(); else speed = 12.f; // typical unit speed m.CurrentSpeed = speed; } else if (anim == "melee") { speed = 1.f; // speed will be ignored if we have a repeattime m.CurrentSpeed = 0.f; CStr code = "var cmp = Engine.QueryInterface("+CStr::FromUInt(m.Entity)+", IID_Attack); " + "if (cmp) cmp.GetTimers(cmp.GetBestAttack()).repeat; else 0;"; m.Simulation2.GetScriptInterface().Eval(code.c_str(), repeattime); } else { // Play the animation at normal speed, but movement speed is zero speed = 1.f; m.CurrentSpeed = 0.f; } CStr sound; if (anim == "melee") sound = "attack"; else if (anim == "build") sound = "build"; else if (anim.Find("gather_") == 0) sound = anim; std::wstring soundgroup; if (!sound.empty()) { CStr code = "var cmp = Engine.QueryInterface("+CStr::FromUInt(m.Entity)+", IID_Sound); " + "if (cmp) cmp.GetSoundGroup('"+sound+"'); else '';"; m.Simulation2.GetScriptInterface().Eval(code.c_str(), soundgroup); } CmpPtr cmpVisual(m.Simulation2, m.Entity); if (cmpVisual) { // TODO: SetEntitySelection(anim) cmpVisual->SelectAnimation(anim, false, fixed::FromFloat(speed), soundgroup); if (repeattime) cmpVisual->SetAnimationSyncRepeat(fixed::FromFloat(repeattime)); } // update prop list for new entity/animation (relies on needsAnimReload also getting called for entire entity changes) m.UpdatePropList(); } m.CurrentUnitID = id; m.CurrentUnitAnim = animation; } void ActorViewer::SetBackgroundColour(const SColor4ub& colour) { m.Background = colour; m.Terrain.SetBaseColour(colour); } void ActorViewer::SetWalkEnabled(bool enabled) { m.WalkEnabled = enabled; } void ActorViewer::SetGroundEnabled(bool enabled) { m.GroundEnabled = enabled; } void ActorViewer::SetShadowsEnabled(bool enabled) { m.ShadowsEnabled = enabled; } void ActorViewer::SetBoundingBoxesEnabled(bool enabled) { m.SelectionBoxEnabled = enabled; } void ActorViewer::SetAxesMarkerEnabled(bool enabled) { m.AxesMarkerEnabled = enabled; } void ActorViewer::SetPropPointsMode(int mode) { m.PropPointsMode = mode; } void ActorViewer::SetStatsEnabled(bool enabled) { if (enabled) g_ProfileViewer.ShowTable("renderer"); else g_ProfileViewer.ShowTable(""); } void ActorViewer::Render() { m.Terrain.MakeDirty(RENDERDATA_UPDATE_COLOR); g_Renderer.SetClearColor(m.Background); // Disable shadows locally (avoid clobbering global state) bool oldShadows = g_Renderer.GetOptionBool(CRenderer::OPT_SHADOWS); g_Renderer.SetOptionBool(CRenderer::OPT_SHADOWS, m.ShadowsEnabled); bool oldSky = g_Renderer.GetSkyManager()->m_RenderSky; g_Renderer.GetSkyManager()->m_RenderSky = false; bool oldWater = g_Renderer.GetWaterManager()->m_RenderWater; g_Renderer.GetWaterManager()->m_RenderWater = false; g_Renderer.BeginFrame(); // Find the centre of the interesting region, in the middle of the patch // and half way up the model (assuming there is one) CVector3D centre; CmpPtr cmpVisual(m.Simulation2, m.Entity); if (cmpVisual) cmpVisual->GetBounds().GetCentre(centre); else centre.Y = 0.f; centre.X = centre.Z = TERRAIN_TILE_SIZE * m.Terrain.GetPatchesPerSide()*PATCH_SIZE/2; CCamera camera = View::GetView_Actor()->GetCamera(); camera.m_Orientation.Translate(centre.X, centre.Y, centre.Z); camera.UpdateFrustum(); g_Renderer.SetSceneCamera(camera, camera); g_Renderer.RenderScene(m); glDisable(GL_DEPTH_TEST); g_ProfileViewer.RenderProfile(); glEnable(GL_DEPTH_TEST); g_Renderer.EndFrame(); // Restore the old renderer state g_Renderer.SetOptionBool(CRenderer::OPT_SHADOWS, oldShadows); g_Renderer.GetSkyManager()->m_RenderSky = oldSky; g_Renderer.GetWaterManager()->m_RenderWater = oldWater; ogl_WarnIfError(); } void ActorViewer::Update(float dt) { m.Simulation2.Update((int)(dt*1000)); m.Simulation2.Interpolate(dt, 0); g_Renderer.GetParticleManager().Interpolate(dt); if (m.WalkEnabled && m.CurrentSpeed) { CmpPtr cmpPosition(m.Simulation2, m.Entity); if (cmpPosition) { // Move the model by speed*dt forwards float z = cmpPosition->GetPosition().Z.ToFloat(); z -= m.CurrentSpeed*dt; // Wrap at the edges, so it doesn't run off into the horizon ssize_t c = TERRAIN_TILE_SIZE * m.Terrain.GetPatchesPerSide()*PATCH_SIZE/2; if (z < c - TERRAIN_TILE_SIZE*PATCH_SIZE * 0.1f) z = c + TERRAIN_TILE_SIZE*PATCH_SIZE * 0.1f; cmpPosition->JumpTo(cmpPosition->GetPosition().X, entity_pos_t::FromFloat(z)); } } } Index: ps/trunk/source/graphics/ColladaManager.h =================================================================== --- ps/trunk/source/graphics/ColladaManager.h (revision 11099) +++ ps/trunk/source/graphics/ColladaManager.h (revision 11100) @@ -1,51 +1,76 @@ -/* Copyright (C) 2009 Wildfire Games. +/* Copyright (C) 2012 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 . */ #ifndef INCLUDED_COLLADAMANAGER #define INCLUDED_COLLADAMANAGER -#include "lib/file/vfs/vfs_path.h" +#include "lib/file/vfs/vfs.h" class CStr8; class CColladaManagerImpl; +class MD5; class CColladaManager { public: enum FileType { PMD, PSA }; - CColladaManager(); + CColladaManager(const PIVFS& vfs); ~CColladaManager(); /** * Returns the VFS path to a PMD/PSA file for the given source file. * Performs a (cached) conversion from COLLADA if necessary. * * @param pathnameNoExtension path and name, minus extension, of file to load. * One of either "sourceName.pmd" or "sourceName.dae" should exist. * @param type FileType, .pmd or .psa * * @return full VFS path (including extension) of file to load; or empty - * string if there was a problem and it could not be loaded. + * string if there was a problem and it could not be loaded. Doesn't knowingly + * return an invalid path. */ - VfsPath GetLoadableFilename(const VfsPath& pathnameNoExtension, FileType type); + VfsPath GetLoadablePath(const VfsPath& pathnameNoExtension, FileType type); + + /** + * Converts DAE to archive cached .pmd/psa and outputs the resulting path + * (used by archive builder) + * + * @param sourcePath[in] path of the .dae to load + * @param type[in] FileType, .pmd or .psa + * @param archiveCachePath[out] output path of the cached file + * + * @return true if COLLADA converter completed successfully; or false if it failed + */ + bool GenerateCachedFile(const VfsPath& sourcePath, FileType type, VfsPath& archiveCachePath); private: + /** + * Creates MD5 hash key from skeletons.xml info and COLLADA converter version, + * used to invalidate cached .pmd/psas + * + * @param hash[out] resulting MD5 hash + * @param version[out] version passed to CCacheLoader, used if code change should force + * cache invalidation + */ + void PrepareCacheKey(MD5& hash, u32& version); + CColladaManagerImpl* m; + PIVFS m_VFS; }; #endif // INCLUDED_COLLADAMANAGER Index: ps/trunk/source/graphics/SkeletonAnimManager.cpp =================================================================== --- ps/trunk/source/graphics/SkeletonAnimManager.cpp (revision 11099) +++ ps/trunk/source/graphics/SkeletonAnimManager.cpp (revision 11100) @@ -1,91 +1,91 @@ -/* Copyright (C) 2009 Wildfire Games. +/* Copyright (C) 2012 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 . */ /* * Owner of all skeleton animations */ #include "precompiled.h" #include "SkeletonAnimManager.h" #include "graphics/ColladaManager.h" #include "graphics/Model.h" #include "graphics/SkeletonAnimDef.h" #include "ps/CLogger.h" #include "ps/FileIo.h" /////////////////////////////////////////////////////////////////////////////// // CSkeletonAnimManager constructor CSkeletonAnimManager::CSkeletonAnimManager(CColladaManager& colladaManager) : m_ColladaManager(colladaManager) { } /////////////////////////////////////////////////////////////////////////////// // CSkeletonAnimManager destructor CSkeletonAnimManager::~CSkeletonAnimManager() { typedef boost::unordered_map::iterator Iter; for (Iter i = m_Animations.begin(); i != m_Animations.end(); ++i) delete i->second; } /////////////////////////////////////////////////////////////////////////////// // GetAnimation: return a given animation by filename; return null if filename // doesn't refer to valid animation file CSkeletonAnimDef* CSkeletonAnimManager::GetAnimation(const VfsPath& pathname) { VfsPath name = pathname.ChangeExtension(L""); // Find if it's already been loaded boost::unordered_map::iterator iter = m_Animations.find(name); if (iter != m_Animations.end()) return iter->second; CSkeletonAnimDef* def = NULL; // Find the file to load - VfsPath psaFilename = m_ColladaManager.GetLoadableFilename(name, CColladaManager::PSA); + VfsPath psaFilename = m_ColladaManager.GetLoadablePath(name, CColladaManager::PSA); if (psaFilename.empty()) { LOGERROR(L"Could not load animation '%ls'", pathname.string().c_str()); def = NULL; } else { try { def = CSkeletonAnimDef::Load(psaFilename); } catch (PSERROR_File&) { // ignore errors (they'll be logged elsewhere) } } if (def) LOGMESSAGE(L"CSkeletonAnimManager::GetAnimation(%ls): Loaded successfully", pathname.string().c_str()); else LOGERROR(L"CSkeletonAnimManager::GetAnimation(%ls): Failed loading, marked file as bad", pathname.string().c_str()); // Add to map m_Animations[name] = def; // NULL if failed to load - we won't try loading it again return def; } Index: ps/trunk/source/graphics/GameView.cpp =================================================================== --- ps/trunk/source/graphics/GameView.cpp (revision 11099) +++ ps/trunk/source/graphics/GameView.cpp (revision 11100) @@ -1,1071 +1,1072 @@ /* Copyright (C) 2012 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 "GameView.h" #include "graphics/Camera.h" #include "graphics/CinemaTrack.h" #include "graphics/ColladaManager.h" #include "graphics/HFTracer.h" #include "graphics/LightEnv.h" #include "graphics/LOSTexture.h" #include "graphics/Model.h" #include "graphics/ObjectManager.h" #include "graphics/Patch.h" #include "graphics/SkeletonAnimManager.h" #include "graphics/Terrain.h" #include "graphics/TerrainTextureManager.h" #include "graphics/TerritoryTexture.h" #include "graphics/Unit.h" #include "graphics/UnitManager.h" #include "lib/input.h" #include "lib/timer.h" #include "maths/BoundingBoxAligned.h" #include "maths/MathUtil.h" #include "maths/Matrix3D.h" #include "maths/Quaternion.h" #include "ps/ConfigDB.h" +#include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/Globals.h" #include "ps/Hotkey.h" #include "ps/Joystick.h" #include "ps/Loader.h" #include "ps/LoaderThunks.h" #include "ps/Profile.h" #include "ps/Pyrogenesis.h" #include "ps/World.h" #include "renderer/Renderer.h" #include "renderer/WaterManager.h" #include "scripting/ScriptableObject.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpRangeManager.h" extern int g_xres, g_yres; // Maximum distance outside the edge of the map that the camera's // focus point can be moved static const float CAMERA_EDGE_MARGIN = 2.0f*TERRAIN_TILE_SIZE; /** * A value with exponential decay towards the target value. */ class CSmoothedValue { public: CSmoothedValue(float value, float smoothness, float minDelta) : m_Target(value), m_Current(value), m_Smoothness(smoothness), m_MinDelta(minDelta) { } float GetSmoothedValue() { return m_Current; } void SetValueSmoothly(float value) { m_Target = value; } void AddSmoothly(float value) { m_Target += value; } void Add(float value) { m_Target += value; m_Current += value; } float GetValue() { return m_Target; } void SetValue(float value) { m_Target = value; m_Current = value; } float Update(float time) { if (fabs(m_Target - m_Current) < m_MinDelta) return 0.0f; double p = pow((double)m_Smoothness, 10.0 * (double)time); // (add the factor of 10 so that smoothnesses don't have to be tiny numbers) double delta = (m_Target - m_Current) * (1.0 - p); m_Current += delta; return (float)delta; } void ClampSmoothly(float min, float max) { m_Target = Clamp(m_Target, (double)min, (double)max); } // Wrap so 'target' is in the range [min, max] void Wrap(float min, float max) { double t = fmod(m_Target - min, (double)(max - min)); if (t < 0) t += max - min; t += min; m_Current += t - m_Target; m_Target = t; } private: double m_Target; // the value which m_Current is tending towards double m_Current; // (We use double because the extra precision is worthwhile here) float m_MinDelta; // cutoff where we stop moving (to avoid ugly shimmering effects) public: float m_Smoothness; }; class CGameViewImpl : public CJSObject { NONCOPYABLE(CGameViewImpl); public: CGameViewImpl(CGame* game) : Game(game), - ColladaManager(), MeshManager(ColladaManager), SkeletonAnimManager(ColladaManager), + ColladaManager(g_VFS), MeshManager(ColladaManager), SkeletonAnimManager(ColladaManager), ObjectManager(MeshManager, SkeletonAnimManager, *game->GetSimulation2()), LOSTexture(*game->GetSimulation2()), TerritoryTexture(*game->GetSimulation2()), ViewCamera(), CullCamera(), LockCullCamera(false), ConstrainCamera(true), Culling(true), FollowEntity(INVALID_ENTITY), FollowFirstPerson(false), // Dummy values (these will be filled in by the config file) ViewScrollSpeed(0), ViewRotateXSpeed(0), ViewRotateXMin(0), ViewRotateXMax(0), ViewRotateXDefault(0), ViewRotateYSpeed(0), ViewRotateYSpeedWheel(0), ViewRotateYDefault(0), ViewDragSpeed(0), ViewZoomSpeed(0), ViewZoomSpeedWheel(0), ViewZoomMin(0), ViewZoomMax(0), ViewZoomDefault(0), ViewFOV(DEGTORAD(45.f)), ViewNear(2.f), ViewFar(4096.f), JoystickPanX(-1), JoystickPanY(-1), JoystickRotateX(-1), JoystickRotateY(-1), JoystickZoomIn(-1), JoystickZoomOut(-1), PosX(0, 0, 0.01f), PosY(0, 0, 0.01f), PosZ(0, 0, 0.01f), Zoom(0, 0, 0.1f), RotateX(0, 0, 0.001f), RotateY(0, 0, 0.001f) { } CGame* Game; CColladaManager ColladaManager; CMeshManager MeshManager; CSkeletonAnimManager SkeletonAnimManager; CObjectManager ObjectManager; CLOSTexture LOSTexture; CTerritoryTexture TerritoryTexture; /** * this camera controls the eye position when rendering */ CCamera ViewCamera; /** * this camera controls the frustum that is used for culling * and shadow calculations * * Note that all code that works with camera movements should only change * m_ViewCamera. The render functions automatically sync the cull camera to * the view camera depending on the value of m_LockCullCamera. */ CCamera CullCamera; /** * When @c true, the cull camera is locked in place. * When @c false, the cull camera follows the view camera. * * Exposed to JS as gameView.lockCullCamera */ bool LockCullCamera; /** * When @c true, culling is enabled so that only models that have a chance of * being visible are sent to the renderer. * Otherwise, the entire world is sent to the renderer. * * Exposed to JS as gameView.culling */ bool Culling; /** * Whether the camera movement should be constrained by min/max limits * and terrain avoidance. */ bool ConstrainCamera; /** * Cache global lighting environment. This is used to check whether the * environment has changed during the last frame, so that vertex data can be updated etc. */ CLightEnv CachedLightEnv; CCinemaManager TrackManager; /** * Entity for the camera to follow, or INVALID_ENTITY if none. */ entity_id_t FollowEntity; /** * Whether to follow FollowEntity in first-person mode. */ bool FollowFirstPerson; //////////////////////////////////////// // Settings float ViewScrollSpeed; float ViewRotateXSpeed; float ViewRotateXMin; float ViewRotateXMax; float ViewRotateXDefault; float ViewRotateYSpeed; float ViewRotateYSpeedWheel; float ViewRotateYDefault; float ViewDragSpeed; float ViewZoomSpeed; float ViewZoomSpeedWheel; float ViewZoomMin; float ViewZoomMax; float ViewZoomDefault; float ViewFOV; float ViewNear; float ViewFar; int JoystickPanX; int JoystickPanY; int JoystickRotateX; int JoystickRotateY; int JoystickZoomIn; int JoystickZoomOut; //////////////////////////////////////// // Camera Controls State CSmoothedValue PosX; CSmoothedValue PosY; CSmoothedValue PosZ; CSmoothedValue Zoom; CSmoothedValue RotateX; // inclination around x axis (relative to camera) CSmoothedValue RotateY; // rotation around y (vertical) axis static void ScriptingInit(); }; static void SetupCameraMatrixSmooth(CGameViewImpl* m, CMatrix3D* orientation) { orientation->SetIdentity(); orientation->RotateX(m->RotateX.GetSmoothedValue()); orientation->RotateY(m->RotateY.GetSmoothedValue()); orientation->Translate(m->PosX.GetSmoothedValue(), m->PosY.GetSmoothedValue(), m->PosZ.GetSmoothedValue()); } static void SetupCameraMatrixSmoothRot(CGameViewImpl* m, CMatrix3D* orientation) { orientation->SetIdentity(); orientation->RotateX(m->RotateX.GetSmoothedValue()); orientation->RotateY(m->RotateY.GetSmoothedValue()); orientation->Translate(m->PosX.GetValue(), m->PosY.GetValue(), m->PosZ.GetValue()); } static void SetupCameraMatrixNonSmooth(CGameViewImpl* m, CMatrix3D* orientation) { orientation->SetIdentity(); orientation->RotateX(m->RotateX.GetValue()); orientation->RotateY(m->RotateY.GetValue()); orientation->Translate(m->PosX.GetValue(), m->PosY.GetValue(), m->PosZ.GetValue()); } CGameView::CGameView(CGame *pGame): m(new CGameViewImpl(pGame)) { SViewPort vp; vp.m_X=0; vp.m_Y=0; vp.m_Width=g_xres; vp.m_Height=g_yres; m->ViewCamera.SetViewPort(vp); m->ViewCamera.SetProjection(m->ViewNear, m->ViewFar, m->ViewFOV); SetupCameraMatrixSmooth(m, &m->ViewCamera.m_Orientation); m->ViewCamera.UpdateFrustum(); m->CullCamera = m->ViewCamera; g_Renderer.SetSceneCamera(m->ViewCamera, m->CullCamera); } CGameView::~CGameView() { UnloadResources(); delete m; } void CGameView::SetViewport(const SViewPort& vp) { m->ViewCamera.SetViewPort(vp); m->ViewCamera.SetProjection(m->ViewNear, m->ViewFar, m->ViewFOV); } CObjectManager& CGameView::GetObjectManager() const { return m->ObjectManager; } JSObject* CGameView::GetScript() { return m->GetScript(); } /*static*/ void CGameView::ScriptingInit() { return CGameViewImpl::ScriptingInit(); } CCamera* CGameView::GetCamera() { return &m->ViewCamera; } CCinemaManager* CGameView::GetCinema() { return &m->TrackManager; }; CLOSTexture& CGameView::GetLOSTexture() { return m->LOSTexture; } CTerritoryTexture& CGameView::GetTerritoryTexture() { return m->TerritoryTexture; } void CGameViewImpl::ScriptingInit() { AddProperty(L"culling", &CGameViewImpl::Culling); AddProperty(L"lockCullCamera", &CGameViewImpl::LockCullCamera); AddProperty(L"constrainCamera", &CGameViewImpl::ConstrainCamera); CJSObject::ScriptingInit("GameView"); } int CGameView::Initialize() { CFG_GET_SYS_VAL("view.scroll.speed", Float, m->ViewScrollSpeed); CFG_GET_SYS_VAL("view.rotate.x.speed", Float, m->ViewRotateXSpeed); CFG_GET_SYS_VAL("view.rotate.x.min", Float, m->ViewRotateXMin); CFG_GET_SYS_VAL("view.rotate.x.max", Float, m->ViewRotateXMax); CFG_GET_SYS_VAL("view.rotate.x.default", Float, m->ViewRotateXDefault); CFG_GET_SYS_VAL("view.rotate.y.speed", Float, m->ViewRotateYSpeed); CFG_GET_SYS_VAL("view.rotate.y.speed.wheel", Float, m->ViewRotateYSpeedWheel); CFG_GET_SYS_VAL("view.rotate.y.default", Float, m->ViewRotateYDefault); CFG_GET_SYS_VAL("view.drag.speed", Float, m->ViewDragSpeed); CFG_GET_SYS_VAL("view.zoom.speed", Float, m->ViewZoomSpeed); CFG_GET_SYS_VAL("view.zoom.speed.wheel", Float, m->ViewZoomSpeedWheel); CFG_GET_SYS_VAL("view.zoom.min", Float, m->ViewZoomMin); CFG_GET_SYS_VAL("view.zoom.max", Float, m->ViewZoomMax); CFG_GET_SYS_VAL("view.zoom.default", Float, m->ViewZoomDefault); CFG_GET_SYS_VAL("joystick.camera.pan.x", Int, m->JoystickPanX); CFG_GET_SYS_VAL("joystick.camera.pan.y", Int, m->JoystickPanY); CFG_GET_SYS_VAL("joystick.camera.rotate.x", Int, m->JoystickRotateX); CFG_GET_SYS_VAL("joystick.camera.rotate.y", Int, m->JoystickRotateY); CFG_GET_SYS_VAL("joystick.camera.zoom.in", Int, m->JoystickZoomIn); CFG_GET_SYS_VAL("joystick.camera.zoom.out", Int, m->JoystickZoomOut); CFG_GET_SYS_VAL("view.pos.smoothness", Float, m->PosX.m_Smoothness); CFG_GET_SYS_VAL("view.pos.smoothness", Float, m->PosY.m_Smoothness); CFG_GET_SYS_VAL("view.pos.smoothness", Float, m->PosZ.m_Smoothness); CFG_GET_SYS_VAL("view.zoom.smoothness", Float, m->Zoom.m_Smoothness); CFG_GET_SYS_VAL("view.rotate.x.smoothness", Float, m->RotateX.m_Smoothness); CFG_GET_SYS_VAL("view.rotate.y.smoothness", Float, m->RotateY.m_Smoothness); CFG_GET_SYS_VAL("view.near", Float, m->ViewNear); CFG_GET_SYS_VAL("view.far", Float, m->ViewFar); CFG_GET_SYS_VAL("view.fov", Float, m->ViewFOV); // Convert to radians m->RotateX.SetValue(DEGTORAD(m->ViewRotateXDefault)); m->RotateY.SetValue(DEGTORAD(m->ViewRotateYDefault)); m->ViewFOV = DEGTORAD(m->ViewFOV); return 0; } void CGameView::RegisterInit() { // CGameView init RegMemFun(this, &CGameView::Initialize, L"CGameView init", 1); // previously done by CGameView::InitResources RegMemFun(g_TexMan.GetSingletonPtr(), &CTerrainTextureManager::LoadTerrainTextures, L"LoadTerrainTextures", 60); RegMemFun(g_Renderer.GetSingletonPtr(), &CRenderer::LoadAlphaMaps, L"LoadAlphaMaps", 5); RegMemFun(g_Renderer.GetSingletonPtr()->GetWaterManager(), &WaterManager::LoadWaterTextures, L"LoadWaterTextures", 80); } void CGameView::BeginFrame() { if (m->LockCullCamera == false) { // Set up cull camera m->CullCamera = m->ViewCamera; // One way to fix shadows popping in at the edge of the screen is to widen the culling frustum so that // objects aren't culled as early. The downside is that objects will get rendered even though they appear // off screen, which is somewhat inefficient. A better solution would be to decouple shadow map rendering // from model rendering; as it is now, a shadow map is only rendered if its associated model is to be // rendered. // (See http://trac.wildfiregames.com/ticket/504) m->CullCamera.SetProjection(m->ViewNear, m->ViewFar, GetCullFOV()); m->CullCamera.UpdateFrustum(); } g_Renderer.SetSceneCamera(m->ViewCamera, m->CullCamera); CheckLightEnv(); m->Game->CachePlayerColours(); } void CGameView::Render() { g_Renderer.RenderScene(*this); } /////////////////////////////////////////////////////////// // This callback is part of the Scene interface // Submit all objects visible in the given frustum void CGameView::EnumerateObjects(const CFrustum& frustum, SceneCollector* c) { { PROFILE3("submit terrain"); CTerrain* pTerrain = m->Game->GetWorld()->GetTerrain(); const ssize_t patchesPerSide = pTerrain->GetPatchesPerSide(); // find out which patches will be drawn for (ssize_t j=0; jGetPatch(i,j); // can't fail // If the patch is underwater, calculate a bounding box that also contains the water plane CBoundingBoxAligned bounds = patch->GetWorldBounds(); float waterHeight = g_Renderer.GetWaterManager()->m_WaterHeight + 0.001f; if(bounds[1].Y < waterHeight) { bounds[1].Y = waterHeight; } if (!m->Culling || frustum.IsBoxVisible (CVector3D(0,0,0), bounds)) { //c->Submit(patch); // set the renderstate for this patch patch->setDrawState(true); // set the renderstate for the neighbors CPatch *nPatch; nPatch = pTerrain->GetPatch(i-1,j-1); if(nPatch) nPatch->setDrawState(true); nPatch = pTerrain->GetPatch(i,j-1); if(nPatch) nPatch->setDrawState(true); nPatch = pTerrain->GetPatch(i+1,j-1); if(nPatch) nPatch->setDrawState(true); nPatch = pTerrain->GetPatch(i-1,j); if(nPatch) nPatch->setDrawState(true); nPatch = pTerrain->GetPatch(i+1,j); if(nPatch) nPatch->setDrawState(true); nPatch = pTerrain->GetPatch(i-1,j+1); if(nPatch) nPatch->setDrawState(true); nPatch = pTerrain->GetPatch(i,j+1); if(nPatch) nPatch->setDrawState(true); nPatch = pTerrain->GetPatch(i+1,j+1); if(nPatch) nPatch->setDrawState(true); } } } // draw the patches for (ssize_t j=0; jGetPatch(i,j); // can't fail if(patch->getDrawState() == true) { c->Submit(patch); patch->setDrawState(false); } } } } m->Game->GetSimulation2()->RenderSubmit(*c, frustum, m->Culling); } void CGameView::CheckLightEnv() { if (m->CachedLightEnv == g_LightEnv) return; if (m->CachedLightEnv.GetLightingModel() != g_LightEnv.GetLightingModel()) g_Renderer.MakeShadersDirty(); m->CachedLightEnv = g_LightEnv; CTerrain* pTerrain = m->Game->GetWorld()->GetTerrain(); if (!pTerrain) return; PROFILE("update light env"); pTerrain->MakeDirty(RENDERDATA_UPDATE_COLOR); const std::vector& units = m->Game->GetWorld()->GetUnitManager().GetUnits(); for (size_t i = 0; i < units.size(); ++i) units[i]->GetModel().SetDirtyRec(RENDERDATA_UPDATE_COLOR); } void CGameView::UnloadResources() { g_TexMan.UnloadTerrainTextures(); g_Renderer.UnloadAlphaMaps(); g_Renderer.GetWaterManager()->UnloadWaterTextures(); } static void ClampDistance(CGameViewImpl* m, bool smooth) { if (!m->ConstrainCamera) return; CCamera targetCam = m->ViewCamera; SetupCameraMatrixSmoothRot(m, &targetCam.m_Orientation); CVector3D forwards = targetCam.m_Orientation.GetIn(); CVector3D delta = targetCam.GetFocus() - targetCam.m_Orientation.GetTranslation(); float dist = delta.Dot(forwards); float clampedDist = Clamp(dist, m->ViewZoomMin, m->ViewZoomMax); float diff = clampedDist - dist; if (!diff) return; if (smooth) { m->PosX.AddSmoothly(forwards.X * -diff); m->PosY.AddSmoothly(forwards.Y * -diff); m->PosZ.AddSmoothly(forwards.Z * -diff); } else { m->PosX.Add(forwards.X * -diff); m->PosY.Add(forwards.Y * -diff); m->PosZ.Add(forwards.Z * -diff); } } void CGameView::Update(float DeltaTime) { if (!g_app_has_focus) return; // TODO: this is probably not an ideal place for this, it should probably go // in a CCmpWaterManager or some such thing (once such a thing exists) if (!m->Game->m_Paused) g_Renderer.GetWaterManager()->m_WaterTexTimer += DeltaTime; if (m->TrackManager.IsActive() && m->TrackManager.IsPlaying()) { if (! m->TrackManager.Update(DeltaTime)) { // ResetCamera(); } return; } // Calculate mouse movement static int mouse_last_x = 0; static int mouse_last_y = 0; int mouse_dx = g_mouse_x - mouse_last_x; int mouse_dy = g_mouse_y - mouse_last_y; mouse_last_x = g_mouse_x; mouse_last_y = g_mouse_y; if (HotkeyIsPressed("camera.rotate.cw")) m->RotateY.AddSmoothly(m->ViewRotateYSpeed * DeltaTime); if (HotkeyIsPressed("camera.rotate.ccw")) m->RotateY.AddSmoothly(-m->ViewRotateYSpeed * DeltaTime); if (HotkeyIsPressed("camera.rotate.up")) m->RotateX.AddSmoothly(-m->ViewRotateXSpeed * DeltaTime); if (HotkeyIsPressed("camera.rotate.down")) m->RotateX.AddSmoothly(m->ViewRotateXSpeed * DeltaTime); float moveRightward = 0.f; float moveForward = 0.f; if (HotkeyIsPressed("camera.pan")) { moveRightward += m->ViewDragSpeed * mouse_dx; moveForward += m->ViewDragSpeed * -mouse_dy; } if (g_mouse_active) { if (g_mouse_x >= g_xres - 2 && g_mouse_x < g_xres) moveRightward += m->ViewScrollSpeed * DeltaTime; else if (g_mouse_x <= 3 && g_mouse_x >= 0) moveRightward -= m->ViewScrollSpeed * DeltaTime; if (g_mouse_y >= g_yres - 2 && g_mouse_y < g_yres) moveForward -= m->ViewScrollSpeed * DeltaTime; else if (g_mouse_y <= 3 && g_mouse_y >= 0) moveForward += m->ViewScrollSpeed * DeltaTime; } if (HotkeyIsPressed("camera.right")) moveRightward += m->ViewScrollSpeed * DeltaTime; if (HotkeyIsPressed("camera.left")) moveRightward -= m->ViewScrollSpeed * DeltaTime; if (HotkeyIsPressed("camera.up")) moveForward += m->ViewScrollSpeed * DeltaTime; if (HotkeyIsPressed("camera.down")) moveForward -= m->ViewScrollSpeed * DeltaTime; if (g_Joystick.IsEnabled()) { // This could all be improved with extra speed and sensitivity settings // (maybe use pow to allow finer control?), and inversion settings moveRightward += g_Joystick.GetAxisValue(m->JoystickPanX) * m->ViewScrollSpeed * DeltaTime; moveForward -= g_Joystick.GetAxisValue(m->JoystickPanY) * m->ViewScrollSpeed * DeltaTime; m->RotateX.AddSmoothly(g_Joystick.GetAxisValue(m->JoystickRotateX) * m->ViewRotateXSpeed * DeltaTime); m->RotateY.AddSmoothly(-g_Joystick.GetAxisValue(m->JoystickRotateY) * m->ViewRotateYSpeed * DeltaTime); // Use a +1 bias for zoom because I want this to work with trigger buttons that default to -1 m->Zoom.AddSmoothly((g_Joystick.GetAxisValue(m->JoystickZoomIn) + 1.0f) / 2.0f * m->ViewZoomSpeed * DeltaTime); m->Zoom.AddSmoothly(-(g_Joystick.GetAxisValue(m->JoystickZoomOut) + 1.0f) / 2.0f * m->ViewZoomSpeed * DeltaTime); } if (moveRightward || moveForward) { // Break out of following mode when the user starts scrolling m->FollowEntity = INVALID_ENTITY; float s = sin(m->RotateY.GetSmoothedValue()); float c = cos(m->RotateY.GetSmoothedValue()); m->PosX.AddSmoothly(c * moveRightward); m->PosZ.AddSmoothly(-s * moveRightward); m->PosX.AddSmoothly(s * moveForward); m->PosZ.AddSmoothly(c * moveForward); } if (m->FollowEntity) { CmpPtr cmpPosition(*(m->Game->GetSimulation2()), m->FollowEntity); if (cmpPosition && cmpPosition->IsInWorld()) { // Get the most recent interpolated position float frameOffset = m->Game->GetSimulation2()->GetLastFrameOffset(); CMatrix3D transform = cmpPosition->GetInterpolatedTransform(frameOffset, false); CVector3D pos = transform.GetTranslation(); if (m->FollowFirstPerson) { float x, z, angle; cmpPosition->GetInterpolatedPosition2D(frameOffset, x, z, angle); float height = 4.f; m->ViewCamera.m_Orientation.SetIdentity(); m->ViewCamera.m_Orientation.RotateX((float)M_PI/24.f); m->ViewCamera.m_Orientation.RotateY(angle); m->ViewCamera.m_Orientation.Translate(pos.X, pos.Y + height, pos.Z); m->ViewCamera.UpdateFrustum(); return; } else { // Move the camera to match the unit CCamera targetCam = m->ViewCamera; SetupCameraMatrixSmoothRot(m, &targetCam.m_Orientation); CVector3D pivot = targetCam.GetFocus(); CVector3D delta = pos - pivot; m->PosX.AddSmoothly(delta.X); m->PosY.AddSmoothly(delta.Y); m->PosZ.AddSmoothly(delta.Z); } } else { // The unit disappeared (died or garrisoned etc), so stop following it m->FollowEntity = INVALID_ENTITY; } } if (HotkeyIsPressed("camera.zoom.in")) m->Zoom.AddSmoothly(m->ViewZoomSpeed * DeltaTime); if (HotkeyIsPressed("camera.zoom.out")) m->Zoom.AddSmoothly(-m->ViewZoomSpeed * DeltaTime); float zoomDelta = m->Zoom.Update(DeltaTime); if (zoomDelta) { CVector3D forwards = m->ViewCamera.m_Orientation.GetIn(); m->PosX.AddSmoothly(forwards.X * zoomDelta); m->PosY.AddSmoothly(forwards.Y * zoomDelta); m->PosZ.AddSmoothly(forwards.Z * zoomDelta); } if (m->ConstrainCamera) m->RotateX.ClampSmoothly(DEGTORAD(m->ViewRotateXMin), DEGTORAD(m->ViewRotateXMax)); ClampDistance(m, true); // Ensure the ViewCamera focus is inside the map with the chosen margins // if not so - apply margins to the camera if (m->ConstrainCamera) { CCamera targetCam = m->ViewCamera; SetupCameraMatrixSmoothRot(m, &targetCam.m_Orientation); CTerrain* pTerrain = m->Game->GetWorld()->GetTerrain(); CVector3D pivot = targetCam.GetFocus(); CVector3D delta = targetCam.m_Orientation.GetTranslation() - pivot; CVector3D desiredPivot = pivot; CmpPtr cmpRangeManager(*m->Game->GetSimulation2(), SYSTEM_ENTITY); if (cmpRangeManager && cmpRangeManager->GetLosCircular()) { // Clamp to a circular region around the center of the map float r = pTerrain->GetMaxX() / 2; CVector3D center(r, desiredPivot.Y, r); float dist = (desiredPivot - center).Length(); if (dist > r - CAMERA_EDGE_MARGIN) desiredPivot = center + (desiredPivot - center).Normalized() * (r - CAMERA_EDGE_MARGIN); } else { // Clamp to the square edges of the map desiredPivot.X = Clamp(desiredPivot.X, pTerrain->GetMinX() + CAMERA_EDGE_MARGIN, pTerrain->GetMaxX() - CAMERA_EDGE_MARGIN); desiredPivot.Z = Clamp(desiredPivot.Z, pTerrain->GetMinZ() + CAMERA_EDGE_MARGIN, pTerrain->GetMaxZ() - CAMERA_EDGE_MARGIN); } // Update the position so that pivot is within the margin m->PosX.SetValueSmoothly(desiredPivot.X + delta.X); m->PosZ.SetValueSmoothly(desiredPivot.Z + delta.Z); } m->PosX.Update(DeltaTime); m->PosY.Update(DeltaTime); m->PosZ.Update(DeltaTime); // Handle rotation around the Y (vertical) axis { CCamera targetCam = m->ViewCamera; SetupCameraMatrixSmooth(m, &targetCam.m_Orientation); float rotateYDelta = m->RotateY.Update(DeltaTime); if (rotateYDelta) { // We've updated RotateY, and need to adjust Pos so that it's still // facing towards the original focus point (the terrain in the center // of the screen). CVector3D upwards(0.0f, 1.0f, 0.0f); CVector3D pivot = targetCam.GetFocus(); CVector3D delta = targetCam.m_Orientation.GetTranslation() - pivot; CQuaternion q; q.FromAxisAngle(upwards, rotateYDelta); CVector3D d = q.Rotate(delta) - delta; m->PosX.Add(d.X); m->PosY.Add(d.Y); m->PosZ.Add(d.Z); } } // Handle rotation around the X (sideways, relative to camera) axis { CCamera targetCam = m->ViewCamera; SetupCameraMatrixSmooth(m, &targetCam.m_Orientation); float rotateXDelta = m->RotateX.Update(DeltaTime); if (rotateXDelta) { CVector3D rightwards = targetCam.m_Orientation.GetLeft() * -1.0f; CVector3D pivot = m->ViewCamera.GetFocus(); CVector3D delta = targetCam.m_Orientation.GetTranslation() - pivot; CQuaternion q; q.FromAxisAngle(rightwards, rotateXDelta); CVector3D d = q.Rotate(delta) - delta; m->PosX.Add(d.X); m->PosY.Add(d.Y); m->PosZ.Add(d.Z); } } /* This is disabled since it doesn't seem necessary: // Ensure the camera's near point is never inside the terrain if (m->ConstrainCamera) { CMatrix3D target; target.SetIdentity(); target.RotateX(m->RotateX.GetValue()); target.RotateY(m->RotateY.GetValue()); target.Translate(m->PosX.GetValue(), m->PosY.GetValue(), m->PosZ.GetValue()); CVector3D nearPoint = target.GetTranslation() + target.GetIn() * defaultNear; float ground = m->Game->GetWorld()->GetTerrain()->GetExactGroundLevel(nearPoint.X, nearPoint.Z); float limit = ground + 16.f; if (nearPoint.Y < limit) m->PosY.AddSmoothly(limit - nearPoint.Y); } */ m->RotateY.Wrap(-(float)M_PI, (float)M_PI); // Update the camera matrix m->ViewCamera.SetProjection(m->ViewNear, m->ViewFar, m->ViewFOV); SetupCameraMatrixSmooth(m, &m->ViewCamera.m_Orientation); m->ViewCamera.UpdateFrustum(); } void CGameView::MoveCameraTarget(const CVector3D& target) { // Maintain the same orientation and level of zoom, if we can // (do this by working out the point the camera is looking at, saving // the difference between that position and the camera point, and restoring // that difference to our new target) CCamera targetCam = m->ViewCamera; SetupCameraMatrixNonSmooth(m, &targetCam.m_Orientation); CVector3D pivot = targetCam.GetFocus(); CVector3D delta = target - pivot; m->PosX.SetValueSmoothly(delta.X + m->PosX.GetValue()); m->PosZ.SetValueSmoothly(delta.Z + m->PosZ.GetValue()); ClampDistance(m, false); // Break out of following mode so the camera really moves to the target m->FollowEntity = INVALID_ENTITY; } void CGameView::ResetCameraTarget(const CVector3D& target) { CMatrix3D orientation; orientation.SetIdentity(); orientation.RotateX(DEGTORAD(m->ViewRotateXDefault)); orientation.RotateY(DEGTORAD(m->ViewRotateYDefault)); CVector3D delta = orientation.GetIn() * m->ViewZoomDefault; m->PosX.SetValue(target.X - delta.X); m->PosY.SetValue(target.Y - delta.Y); m->PosZ.SetValue(target.Z - delta.Z); m->RotateX.SetValue(DEGTORAD(m->ViewRotateXDefault)); m->RotateY.SetValue(DEGTORAD(m->ViewRotateYDefault)); ClampDistance(m, false); SetupCameraMatrixSmooth(m, &m->ViewCamera.m_Orientation); m->ViewCamera.UpdateFrustum(); // Break out of following mode so the camera really moves to the target m->FollowEntity = INVALID_ENTITY; } void CGameView::ResetCameraAngleZoom() { CCamera targetCam = m->ViewCamera; SetupCameraMatrixNonSmooth(m, &targetCam.m_Orientation); // Compute the zoom adjustment to get us back to the default CVector3D forwards = targetCam.m_Orientation.GetIn(); CVector3D delta = targetCam.GetFocus() - targetCam.m_Orientation.GetTranslation(); float dist = delta.Dot(forwards); m->Zoom.AddSmoothly(dist - m->ViewZoomDefault); // Reset orientations to default m->RotateX.SetValueSmoothly(DEGTORAD(m->ViewRotateXDefault)); m->RotateY.SetValueSmoothly(DEGTORAD(m->ViewRotateYDefault)); } void CGameView::CameraFollow(entity_id_t entity, bool firstPerson) { m->FollowEntity = entity; m->FollowFirstPerson = firstPerson; } entity_id_t CGameView::GetFollowedEntity() { return m->FollowEntity; } float CGameView::GetNear() const { return m->ViewNear; } float CGameView::GetFar() const { return m->ViewFar; } float CGameView::GetFOV() const { return m->ViewFOV; } float CGameView::GetCullFOV() const { return m->ViewFOV + DEGTORAD(6.0f); //add 6 degrees to the default FOV for use with the culling frustum; } void CGameView::SetCameraProjection() { m->ViewCamera.SetProjection(m->ViewNear, m->ViewFar, m->ViewFOV); } InReaction game_view_handler(const SDL_Event_* ev) { // put any events that must be processed even if inactive here if(!g_app_has_focus || !g_Game || !g_Game->IsGameStarted()) return IN_PASS; CGameView *pView=g_Game->GetView(); return pView->HandleEvent(ev); } InReaction CGameView::HandleEvent(const SDL_Event_* ev) { switch(ev->ev.type) { case SDL_HOTKEYDOWN: std::string hotkey = static_cast(ev->ev.user.data1); if (hotkey == "wireframe") { if (g_Renderer.GetModelRenderMode() == SOLID) { g_Renderer.SetTerrainRenderMode(EDGED_FACES); g_Renderer.SetModelRenderMode(EDGED_FACES); } else if (g_Renderer.GetModelRenderMode() == EDGED_FACES) { g_Renderer.SetTerrainRenderMode(WIREFRAME); g_Renderer.SetModelRenderMode(WIREFRAME); } else { g_Renderer.SetTerrainRenderMode(SOLID); g_Renderer.SetModelRenderMode(SOLID); } return IN_HANDLED; } // Mouse wheel must be treated using events instead of polling, // because SDL auto-generates a sequence of mousedown/mouseup events // and we never get to see the "down" state inside Update(). else if (hotkey == "camera.zoom.wheel.in") { m->Zoom.AddSmoothly(m->ViewZoomSpeedWheel); return IN_HANDLED; } else if (hotkey == "camera.zoom.wheel.out") { m->Zoom.AddSmoothly(-m->ViewZoomSpeedWheel); return IN_HANDLED; } else if (hotkey == "camera.rotate.wheel.cw") { m->RotateY.AddSmoothly(m->ViewRotateYSpeedWheel); return IN_HANDLED; } else if (hotkey == "camera.rotate.wheel.ccw") { m->RotateY.AddSmoothly(-m->ViewRotateYSpeedWheel); return IN_HANDLED; } else if (hotkey == "camera.reset") { ResetCameraAngleZoom(); return IN_HANDLED; } } return IN_PASS; } Index: ps/trunk/source/graphics/MeshManager.cpp =================================================================== --- ps/trunk/source/graphics/MeshManager.cpp (revision 11099) +++ ps/trunk/source/graphics/MeshManager.cpp (revision 11100) @@ -1,71 +1,71 @@ -/* Copyright (C) 2010 Wildfire Games. +/* Copyright (C) 2012 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 "MeshManager.h" #include "graphics/ColladaManager.h" #include "graphics/ModelDef.h" #include "ps/CLogger.h" #include "ps/FileIo.h" // to get access to its CError #include "ps/Profile.h" // TODO: should this cache models while they're not actively in the game? // (Currently they'll probably be deleted when the reference count drops to 0, // even if it's quite possible that they'll get reloaded very soon.) CMeshManager::CMeshManager(CColladaManager& colladaManager) : m_ColladaManager(colladaManager) { } CMeshManager::~CMeshManager() { } CModelDefPtr CMeshManager::GetMesh(const VfsPath& pathname) { const VfsPath name = pathname.ChangeExtension(L""); // Find the mesh if it's already been loaded and cached mesh_map::iterator iter = m_MeshMap.find(name); if (iter != m_MeshMap.end() && !iter->second.expired()) return CModelDefPtr(iter->second); PROFILE("load mesh"); - VfsPath pmdFilename = m_ColladaManager.GetLoadableFilename(name, CColladaManager::PMD); + VfsPath pmdFilename = m_ColladaManager.GetLoadablePath(name, CColladaManager::PMD); if (pmdFilename.empty()) { LOGERROR(L"Could not load mesh '%ls'", pathname.string().c_str()); return CModelDefPtr(); } try { CModelDefPtr model (CModelDef::Load(pmdFilename, name)); m_MeshMap[name] = model; return model; } catch (PSERROR_File&) { LOGERROR(L"Could not load mesh '%ls'", pathname.string().c_str()); return CModelDefPtr(); } } Index: ps/trunk/source/graphics/TextureManager.cpp =================================================================== --- ps/trunk/source/graphics/TextureManager.cpp (revision 11099) +++ ps/trunk/source/graphics/TextureManager.cpp (revision 11100) @@ -1,642 +1,643 @@ -/* Copyright (C) 2010 Wildfire Games. +/* Copyright (C) 2012 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 "TextureManager.h" #include "graphics/TextureConverter.h" #include "lib/allocators/shared_ptr.h" #include "lib/res/h_mgr.h" #include "lib/file/vfs/vfs_tree.h" #include "lib/res/graphics/ogl_tex.h" #include "lib/timer.h" #include "maths/MD5.h" #include "ps/CacheLoader.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Profile.h" #include #include #include #include struct TPhash : std::unary_function, std::unary_function { std::size_t operator()(CTextureProperties const& a) const { std::size_t seed = 0; boost::hash_combine(seed, a.m_Path); boost::hash_combine(seed, a.m_Filter); boost::hash_combine(seed, a.m_WrapS); boost::hash_combine(seed, a.m_WrapT); boost::hash_combine(seed, a.m_Aniso); return seed; } std::size_t operator()(CTexturePtr const& a) const { return (*this)(a->m_Properties); } }; struct TPequal_to : std::binary_function, std::binary_function { bool operator()(CTextureProperties const& a, CTextureProperties const& b) const { return a.m_Path == b.m_Path && a.m_Filter == b.m_Filter && a.m_WrapS == b.m_WrapS && a.m_WrapT == b.m_WrapT && a.m_Aniso == b.m_Aniso; } bool operator()(CTexturePtr const& a, CTexturePtr const& b) const { return (*this)(a->m_Properties, b->m_Properties); } }; class CTextureManagerImpl { friend class CTexture; public: CTextureManagerImpl(PIVFS vfs, bool highQuality, bool disableGL) : m_VFS(vfs), m_CacheLoader(vfs, L".dds"), m_DisableGL(disableGL), m_TextureConverter(vfs, highQuality), m_DefaultHandle(0), m_ErrorHandle(0) { // Initialise some textures that will always be available, // without needing to load any files // Default placeholder texture (grey) if (!m_DisableGL) { // Construct 1x1 24-bit texture shared_ptr data(new u8[3], ArrayDeleter()); data.get()[0] = 64; data.get()[1] = 64; data.get()[2] = 64; Tex t; (void)tex_wrap(1, 1, 24, 0, data, 0, &t); m_DefaultHandle = ogl_tex_wrap(&t, m_VFS, L"(default texture)"); (void)ogl_tex_set_filter(m_DefaultHandle, GL_LINEAR); if (!m_DisableGL) (void)ogl_tex_upload(m_DefaultHandle); } // Error texture (magenta) if (!m_DisableGL) { // Construct 1x1 24-bit texture shared_ptr data(new u8[3], ArrayDeleter()); data.get()[0] = 255; data.get()[1] = 0; data.get()[2] = 255; Tex t; (void)tex_wrap(1, 1, 24, 0, data, 0, &t); m_ErrorHandle = ogl_tex_wrap(&t, m_VFS, L"(error texture)"); (void)ogl_tex_set_filter(m_ErrorHandle, GL_LINEAR); if (!m_DisableGL) (void)ogl_tex_upload(m_ErrorHandle); // Construct a CTexture to return to callers who want an error texture CTextureProperties props(L"(error texture)"); m_ErrorTexture = CTexturePtr(new CTexture(m_ErrorHandle, props, this)); m_ErrorTexture->m_State = CTexture::LOADED; m_ErrorTexture->m_Self = m_ErrorTexture; } // Allow hotloading of textures RegisterFileReloadFunc(ReloadChangedFileCB, this); } ~CTextureManagerImpl() { UnregisterFileReloadFunc(ReloadChangedFileCB, this); (void)ogl_tex_free(m_DefaultHandle); (void)ogl_tex_free(m_ErrorHandle); } CTexturePtr GetErrorTexture() { return m_ErrorTexture; } /** * See CTextureManager::CreateTexture */ CTexturePtr CreateTexture(const CTextureProperties& props) { // Construct a new default texture with the given properties to use as the search key CTexturePtr texture(new CTexture(m_DefaultHandle, props, this)); // Try to find an existing texture with the given properties TextureCache::iterator it = m_TextureCache.find(texture); if (it != m_TextureCache.end()) return *it; // Can't find an existing texture - finish setting up this new texture texture->m_Self = texture; m_TextureCache.insert(texture); m_HotloadFiles[props.m_Path].insert(texture); return texture; } /** * Load the given file into the texture object and upload it to OpenGL. * Assumes the file already exists. */ void LoadTexture(const CTexturePtr& texture, const VfsPath& path) { if (m_DisableGL) return; PROFILE2("load texture"); PROFILE2_ATTR("name: %ls", path.string().c_str()); Handle h = ogl_tex_load(m_VFS, path, RES_UNIQUE); if (h <= 0) { LOGERROR(L"Texture failed to load; \"%ls\"", texture->m_Properties.m_Path.string().c_str()); // Replace with error texture to make it obvious texture->SetHandle(m_ErrorHandle); return; } // Get some flags for later use size_t flags = 0; (void)ogl_tex_get_format(h, &flags, NULL); // Initialise base colour from the texture (void)ogl_tex_get_average_colour(h, &texture->m_BaseColour); // Set GL upload properties (void)ogl_tex_set_wrap(h, texture->m_Properties.m_WrapS, texture->m_Properties.m_WrapT); (void)ogl_tex_set_anisotropy(h, texture->m_Properties.m_Aniso); // Prevent ogl_tex automatically generating mipmaps (which is slow and unwanted), // by avoiding mipmapped filters unless the source texture already has mipmaps GLint filter = texture->m_Properties.m_Filter; if (!(flags & TEX_MIPMAPS)) { switch (filter) { case GL_NEAREST_MIPMAP_NEAREST: case GL_NEAREST_MIPMAP_LINEAR: filter = GL_NEAREST; break; case GL_LINEAR_MIPMAP_NEAREST: case GL_LINEAR_MIPMAP_LINEAR: filter = GL_LINEAR; break; } } (void)ogl_tex_set_filter(h, filter); // Upload to GL if (!m_DisableGL && ogl_tex_upload(h) < 0) { LOGERROR(L"Texture failed to upload: \"%ls\"", texture->m_Properties.m_Path.string().c_str()); ogl_tex_free(h); // Replace with error texture to make it obvious texture->SetHandle(m_ErrorHandle); return; } // Let the texture object take ownership of this handle texture->SetHandle(h, true); } /** * Set up some parameters for the loose cache filename code. */ void PrepareCacheKey(const CTexturePtr& texture, MD5& hash, u32& version) { // Hash the settings, so we won't use an old loose cache file if the // settings have changed CTextureConverter::Settings settings = GetConverterSettings(texture); settings.Hash(hash); // Arbitrary version number - change this if we update the code and // need to invalidate old users' caches version = 1; } /** * Attempts to load a cached version of a texture. * If the texture is loaded (or there was an error), returns true. * Otherwise, returns false to indicate the caller should generate the cached version. */ bool TryLoadingCached(const CTexturePtr& texture) { MD5 hash; u32 version; PrepareCacheKey(texture, hash, version); VfsPath loadPath; Status ret = m_CacheLoader.TryLoadingCached(texture->m_Properties.m_Path, hash, version, loadPath); if (ret == INFO::OK) { // Found a cached texture - load it LoadTexture(texture, loadPath); return true; } else if (ret == INFO::SKIPPED) { // No cached version was found - we'll need to create it return false; } else { ENSURE(ret < 0); // No source file or archive cache was found, so we can't load the // real texture at all - return the error texture instead + LOGERROR(L"CCacheLoader failed to find archived or source file for: \"%ls\"", texture->m_Properties.m_Path.string().c_str()); texture->SetHandle(m_ErrorHandle); return true; } } /** * Initiates an asynchronous conversion process, from the texture's * source file to the corresponding loose cache file. */ void ConvertTexture(const CTexturePtr& texture) { VfsPath sourcePath = texture->m_Properties.m_Path; PROFILE2("convert texture"); PROFILE2_ATTR("name: %ls", sourcePath.string().c_str()); MD5 hash; u32 version; PrepareCacheKey(texture, hash, version); VfsPath looseCachePath = m_CacheLoader.LooseCachePath(sourcePath, hash, version); // LOGWARNING(L"Converting texture \"%ls\"", srcPath.c_str()); CTextureConverter::Settings settings = GetConverterSettings(texture); m_TextureConverter.ConvertTexture(texture, sourcePath, looseCachePath, settings); } bool GenerateCachedTexture(const VfsPath& sourcePath, VfsPath& archiveCachePath) { archiveCachePath = m_CacheLoader.ArchiveCachePath(sourcePath); CTextureProperties textureProps(sourcePath); CTexturePtr texture = CreateTexture(textureProps); CTextureConverter::Settings settings = GetConverterSettings(texture); if (!m_TextureConverter.ConvertTexture(texture, sourcePath, VfsPath("cache") / archiveCachePath, settings)) return false; while (true) { CTexturePtr textureOut; VfsPath dest; bool ok; if (m_TextureConverter.Poll(textureOut, dest, ok)) return ok; // Spin-loop is dumb but it works okay for now SDL_Delay(0); } } bool MakeProgress() { // Process any completed conversion tasks { CTexturePtr texture; VfsPath dest; bool ok; if (m_TextureConverter.Poll(texture, dest, ok)) { if (ok) { LoadTexture(texture, dest); } else { LOGERROR(L"Texture failed to convert: \"%ls\"", texture->m_Properties.m_Path.string().c_str()); texture->SetHandle(m_ErrorHandle); } texture->m_State = CTexture::LOADED; return true; } } // We'll only push new conversion requests if it's not already busy bool converterBusy = m_TextureConverter.IsBusy(); if (!converterBusy) { // Look for all high-priority textures needing conversion. // (Iterating over all textures isn't optimally efficient, but it // doesn't seem to be a problem yet and it's simpler than maintaining // multiple queues.) for (TextureCache::iterator it = m_TextureCache.begin(); it != m_TextureCache.end(); ++it) { if ((*it)->m_State == CTexture::HIGH_NEEDS_CONVERTING) { // Start converting this texture (*it)->m_State = CTexture::HIGH_IS_CONVERTING; ConvertTexture(*it); return true; } } } // Try loading prefetched textures from their cache for (TextureCache::iterator it = m_TextureCache.begin(); it != m_TextureCache.end(); ++it) { if ((*it)->m_State == CTexture::PREFETCH_NEEDS_LOADING) { if (TryLoadingCached(*it)) { (*it)->m_State = CTexture::LOADED; } else { (*it)->m_State = CTexture::PREFETCH_NEEDS_CONVERTING; } return true; } } // If we've got nothing better to do, then start converting prefetched textures. if (!converterBusy) { for (TextureCache::iterator it = m_TextureCache.begin(); it != m_TextureCache.end(); ++it) { if ((*it)->m_State == CTexture::PREFETCH_NEEDS_CONVERTING) { (*it)->m_State = CTexture::PREFETCH_IS_CONVERTING; ConvertTexture(*it); return true; } } } return false; } /** * Compute the conversion settings that apply to a given texture, by combining * the textures.xml files from its directory and all parent directories * (up to the VFS root). */ CTextureConverter::Settings GetConverterSettings(const CTexturePtr& texture) { fs::wpath srcPath = texture->m_Properties.m_Path.string(); std::vector files; VfsPath p; for (fs::wpath::iterator it = srcPath.begin(); it != srcPath.end(); ++it) { VfsPath settingsPath = p / "textures.xml"; m_HotloadFiles[settingsPath].insert(texture); CTextureConverter::SettingsFile* f = GetSettingsFile(settingsPath); if (f) files.push_back(f); p = p / *it; } return m_TextureConverter.ComputeSettings(srcPath.leaf(), files); } /** * Return the (cached) settings file with the given filename, * or NULL if it doesn't exist. */ CTextureConverter::SettingsFile* GetSettingsFile(const VfsPath& path) { SettingsFilesMap::iterator it = m_SettingsFiles.find(path); if (it != m_SettingsFiles.end()) return it->second.get(); if (m_VFS->GetFileInfo(path, NULL) >= 0) { shared_ptr settings(m_TextureConverter.LoadSettings(path)); m_SettingsFiles.insert(std::make_pair(path, settings)); return settings.get(); } else { m_SettingsFiles.insert(std::make_pair(path, shared_ptr())); return NULL; } } static Status ReloadChangedFileCB(void* param, const VfsPath& path) { return static_cast(param)->ReloadChangedFile(path); } Status ReloadChangedFile(const VfsPath& path) { // Uncache settings file, if this is one m_SettingsFiles.erase(path); // Find all textures using this file HotloadFilesMap::iterator files = m_HotloadFiles.find(path); if (files != m_HotloadFiles.end()) { // Flag all textures using this file as needing reloading for (std::set >::iterator it = files->second.begin(); it != files->second.end(); ++it) { if (shared_ptr texture = it->lock()) { texture->m_State = CTexture::UNLOADED; texture->SetHandle(m_DefaultHandle); } } } return INFO::OK; } private: PIVFS m_VFS; CCacheLoader m_CacheLoader; bool m_DisableGL; CTextureConverter m_TextureConverter; Handle m_DefaultHandle; Handle m_ErrorHandle; CTexturePtr m_ErrorTexture; // Cache of all loaded textures typedef boost::unordered_set TextureCache; TextureCache m_TextureCache; // TODO: we ought to expire unused textures from the cache eventually // Store the set of textures that need to be reloaded when the given file // (a source file or settings.xml) is modified typedef boost::unordered_map > > HotloadFilesMap; HotloadFilesMap m_HotloadFiles; // Cache for the conversion settings files typedef boost::unordered_map > SettingsFilesMap; SettingsFilesMap m_SettingsFiles; }; CTexture::CTexture(Handle handle, const CTextureProperties& props, CTextureManagerImpl* textureManager) : m_Handle(handle), m_BaseColour(0), m_State(UNLOADED), m_Properties(props), m_TextureManager(textureManager) { // Add a reference to the handle (it might be shared by multiple CTextures // so we can't take ownership of it) if (m_Handle) h_add_ref(m_Handle); } CTexture::~CTexture() { if (m_Handle) ogl_tex_free(m_Handle); } void CTexture::Bind(size_t unit) { ogl_tex_bind(GetHandle(), unit); } Handle CTexture::GetHandle() { // TODO: TryLoad might call ogl_tex_upload which enables GL_TEXTURE_2D // on texture unit 0, regardless of 'unit', which callers might // not be expecting. Ideally that wouldn't happen. TryLoad(); return m_Handle; } bool CTexture::TryLoad() { // If we haven't started loading, then try loading, and if that fails then request conversion. // If we have already tried prefetch loading, and it failed, bump the conversion request to HIGH priority. if (m_State == UNLOADED || m_State == PREFETCH_NEEDS_LOADING || m_State == PREFETCH_NEEDS_CONVERTING) { if (shared_ptr self = m_Self.lock()) { if (m_State != PREFETCH_NEEDS_CONVERTING && m_TextureManager->TryLoadingCached(self)) m_State = LOADED; else m_State = HIGH_NEEDS_CONVERTING; } } return (m_State == LOADED); } void CTexture::Prefetch() { if (m_State == UNLOADED) { if (shared_ptr self = m_Self.lock()) { m_State = PREFETCH_NEEDS_LOADING; } } } bool CTexture::IsLoaded() { return (m_State == LOADED); } void CTexture::SetHandle(Handle handle, bool takeOwnership) { if (handle == m_Handle) return; if (!takeOwnership) h_add_ref(handle); ogl_tex_free(m_Handle); m_Handle = handle; } size_t CTexture::GetWidth() const { size_t w = 0; (void)ogl_tex_get_size(m_Handle, &w, 0, 0); return w; } size_t CTexture::GetHeight() const { size_t h = 0; (void)ogl_tex_get_size(m_Handle, 0, &h, 0); return h; } bool CTexture::HasAlpha() const { size_t flags = 0; (void)ogl_tex_get_format(m_Handle, &flags, 0); return (flags & TEX_ALPHA) != 0; } u32 CTexture::GetBaseColour() const { return m_BaseColour; } // CTextureManager: forward all calls to impl: CTextureManager::CTextureManager(PIVFS vfs, bool highQuality, bool disableGL) : m(new CTextureManagerImpl(vfs, highQuality, disableGL)) { } CTextureManager::~CTextureManager() { delete m; } CTexturePtr CTextureManager::CreateTexture(const CTextureProperties& props) { return m->CreateTexture(props); } CTexturePtr CTextureManager::GetErrorTexture() { return m->GetErrorTexture(); } bool CTextureManager::MakeProgress() { return m->MakeProgress(); } bool CTextureManager::GenerateCachedTexture(const VfsPath& path, VfsPath& outputPath) { return m->GenerateCachedTexture(path, outputPath); } Index: ps/trunk/source/graphics/tests/test_MeshManager.h =================================================================== --- ps/trunk/source/graphics/tests/test_MeshManager.h (revision 11099) +++ ps/trunk/source/graphics/tests/test_MeshManager.h (revision 11100) @@ -1,256 +1,256 @@ -/* Copyright (C) 2009 Wildfire Games. +/* Copyright (C) 2012 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 "lib/file/file_system.h" #include "lib/file/vfs/vfs.h" #include "lib/file/io/io.h" #include "lib/allocators/shared_ptr.h" #include "graphics/ColladaManager.h" #include "graphics/MeshManager.h" #include "graphics/ModelDef.h" #include "ps/CLogger.h" #include "ps/XML/RelaxNG.h" static OsPath MOD_PATH(DataDir()/"mods"/"_test.mesh"); static OsPath CACHE_PATH(DataDir()/"_testcache"); const OsPath srcDAE(L"collada/sphere.dae"); const OsPath srcPMD(L"collada/sphere.pmd"); const OsPath testDAE(L"art/skeletons/test.dae"); const OsPath testPMD(L"art/skeletons/test.pmd"); const OsPath testBase(L"art/skeletons/test"); const OsPath srcSkeletonDefs(L"collada/skeletons.xml"); const OsPath testSkeletonDefs(L"art/skeletons/skeletons.xml"); extern PIVFS g_VFS; class TestMeshManager : public CxxTest::TestSuite { void initVfs() { // Initialise VFS: // Set up a mod directory to work in: // Make sure the required directories doesn't exist when we start, // in case the previous test aborted and left them full of junk if(DirectoryExists(MOD_PATH)) DeleteDirectory(MOD_PATH); if(DirectoryExists(CACHE_PATH)) DeleteDirectory(CACHE_PATH); g_VFS = CreateVfs(20*MiB); TS_ASSERT_OK(g_VFS->Mount(L"", MOD_PATH)); TS_ASSERT_OK(g_VFS->Mount(L"collada/", DataDir()/"tests"/"collada", VFS_MOUNT_MUST_EXIST)); // Mount _testcache onto virtual /cache - don't use the normal cache // directory because that's full of loads of cached files from the // proper game and takes a long time to load. TS_ASSERT_OK(g_VFS->Mount(L"cache/", CACHE_PATH)); } void deinitVfs() { g_VFS.reset(); DeleteDirectory(MOD_PATH); DeleteDirectory(CACHE_PATH); } void copyFile(const VfsPath& src, const VfsPath& dst) { // Copy a file into the mod directory, so we can work on it: shared_ptr data; size_t size = 0; TS_ASSERT_OK(g_VFS->LoadFile(src, data, size)); TS_ASSERT_OK(g_VFS->CreateFile(dst, data, size)); } void buildArchive() { // Create a junk trace file first, because vfs_opt_auto_build requires one // std::string trace = "000.000000: L \"-\" 0 0000\n"; // vfs_store("trace.txt", (const u8*)trace.c_str(), trace.size(), FILE_NO_AIO); // then make the archive // TS_ASSERT_OK(vfs_opt_rebuild_main_archive(MOD_PATH"/trace.txt", MOD_PATH"/test%02d.zip")); } CColladaManager* colladaManager; CMeshManager* meshManager; public: void setUp() { initVfs(); - colladaManager = new CColladaManager(); + colladaManager = new CColladaManager(g_VFS); meshManager = new CMeshManager(*colladaManager); } void tearDown() { delete meshManager; delete colladaManager; deinitVfs(); } void IRRELEVANT_test_archived() { copyFile(srcDAE, testDAE); //buildArchive(); shared_ptr buf; AllocateAligned(buf, 100, maxSectorSize); strcpy_s((char*)buf.get(), 5, "Test"); g_VFS->CreateFile(testDAE, buf, 4); } void test_load_pmd_with_extension() { copyFile(srcPMD, testPMD); CModelDefPtr modeldef = meshManager->GetMesh(testPMD); TS_ASSERT(modeldef); if (modeldef) TS_ASSERT_PATH_EQUALS(modeldef->GetName(), testBase); } void test_load_pmd_without_extension() { copyFile(srcPMD, testPMD); CModelDefPtr modeldef = meshManager->GetMesh(testBase); TS_ASSERT(modeldef); if (modeldef) TS_ASSERT_PATH_EQUALS(modeldef->GetName(), testBase); } void test_caching() { copyFile(srcPMD, testPMD); CModelDefPtr modeldef1 = meshManager->GetMesh(testPMD); CModelDefPtr modeldef2 = meshManager->GetMesh(testPMD); TS_ASSERT(modeldef1 && modeldef2); if (modeldef1 && modeldef2) TS_ASSERT_EQUALS(modeldef1.get(), modeldef2.get()); } void test_load_dae() { copyFile(srcDAE, testDAE); copyFile(srcSkeletonDefs, testSkeletonDefs); CModelDefPtr modeldef = meshManager->GetMesh(testDAE); TS_ASSERT(modeldef); if (modeldef) TS_ASSERT_PATH_EQUALS(modeldef->GetName(), testBase); } void test_load_dae_caching() { copyFile(srcDAE, testDAE); copyFile(srcSkeletonDefs, testSkeletonDefs); - VfsPath daeName1 = colladaManager->GetLoadableFilename(testBase, CColladaManager::PMD); - VfsPath daeName2 = colladaManager->GetLoadableFilename(testBase, CColladaManager::PMD); + VfsPath daeName1 = colladaManager->GetLoadablePath(testBase, CColladaManager::PMD); + VfsPath daeName2 = colladaManager->GetLoadablePath(testBase, CColladaManager::PMD); TS_ASSERT(!daeName1.empty()); TS_ASSERT_PATH_EQUALS(daeName1, daeName2); // TODO: it'd be nice to test that it really isn't doing the DAE->PMD // conversion a second time, but there doesn't seem to be an easy way // to check that } void test_invalid_skeletons() { TestLogger logger; copyFile(srcDAE, testDAE); shared_ptr buf; AllocateAligned(buf, 100, maxSectorSize); strcpy_s((char*)buf.get(), 100, "Not valid XML"); g_VFS->CreateFile(testSkeletonDefs, buf, 13); CModelDefPtr modeldef = meshManager->GetMesh(testDAE); TS_ASSERT(! modeldef); TS_ASSERT_WSTR_CONTAINS(logger.GetOutput(), L"parser error"); } void test_invalid_dae() { TestLogger logger; copyFile(srcSkeletonDefs, testSkeletonDefs); shared_ptr buf; AllocateAligned(buf, 100, maxSectorSize); strcpy_s((char*)buf.get(), 100, "Not valid XML"); g_VFS->CreateFile(testDAE, buf, 13); CModelDefPtr modeldef = meshManager->GetMesh(testDAE); TS_ASSERT(! modeldef); TS_ASSERT_WSTR_CONTAINS(logger.GetOutput(), L"parser error"); } void test_load_nonexistent_pmd() { TestLogger logger; CModelDefPtr modeldef = meshManager->GetMesh(testPMD); TS_ASSERT(! modeldef); } void test_load_nonexistent_dae() { TestLogger logger; CModelDefPtr modeldef = meshManager->GetMesh(testDAE); TS_ASSERT(! modeldef); } void test_load_across_relaxng() { // Verify that loading meshes doesn't invalidate other users of libxml2 by calling xmlCleanupParser // (Run this in Valgrind and check for use-of-freed-memory errors) RelaxNGValidator v; TS_ASSERT(v.LoadGrammar("")); TS_ASSERT(v.Validate(L"doc", L"2.0")); copyFile(srcDAE, testDAE); copyFile(srcSkeletonDefs, testSkeletonDefs); CModelDefPtr modeldef = meshManager->GetMesh(testDAE); TS_ASSERT(modeldef); if (modeldef) TS_ASSERT_PATH_EQUALS(modeldef->GetName(), testBase); TS_ASSERT(v.Validate(L"doc", L"2.0")); } ////////////////////////////////////////////////////////////////////////// // Tests based on real DAE files: void test_load_dae_bogus_material_target() { copyFile(L"collada/bogus_material_target.dae", testDAE); copyFile(srcSkeletonDefs, testSkeletonDefs); CModelDefPtr modeldef = meshManager->GetMesh(testDAE); TS_ASSERT(modeldef); } }; Index: ps/trunk/source/graphics/ColladaManager.cpp =================================================================== --- ps/trunk/source/graphics/ColladaManager.cpp (revision 11099) +++ ps/trunk/source/graphics/ColladaManager.cpp (revision 11100) @@ -1,271 +1,320 @@ -/* Copyright (C) 2011 Wildfire Games. +/* Copyright (C) 2012 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 "ColladaManager.h" #include "graphics/ModelDef.h" #include "lib/fnv_hash.h" +#include "maths/MD5.h" +#include "ps/CacheLoader.h" #include "ps/CLogger.h" #include "ps/CStr.h" #include "ps/DllLoader.h" #include "ps/Filesystem.h" namespace Collada { #include "collada/DLL.h" } namespace { void ColladaLog(void* cb_data, int severity, const char* text) { VfsPath* path = static_cast(cb_data); if (severity == LOG_INFO) LOGMESSAGE(L"%ls: %hs", path->string().c_str(), text); else if (severity == LOG_WARNING) LOGWARNING(L"%ls: %hs", path->string().c_str(), text); else LOGERROR(L"%ls: %hs", path->string().c_str(), text); } void ColladaOutput(void* cb_data, const char* data, unsigned int length) { WriteBuffer* writeBuffer = static_cast(cb_data); writeBuffer->Append(data, (size_t)length); } } class CColladaManagerImpl { DllLoader dll; void (*set_logger)(Collada::LogFn logger, void* cb_data); int (*set_skeleton_definitions)(const char* xml, int length); int (*convert_dae_to_pmd)(const char* dae, Collada::OutputFn pmd_writer, void* cb_data); int (*convert_dae_to_psa)(const char* dae, Collada::OutputFn psa_writer, void* cb_data); public: - CColladaManagerImpl() - : dll("Collada") + CColladaManagerImpl(const PIVFS& vfs) + : dll("Collada"), m_VFS(vfs) { } ~CColladaManagerImpl() { if (dll.IsLoaded()) set_logger(NULL, NULL); // unregister the log handler } bool Convert(const VfsPath& daeFilename, const VfsPath& pmdFilename, CColladaManager::FileType type) { // To avoid always loading the DLL when it's usually not going to be // used (and to do the same on Linux where delay-loading won't help), // and to avoid compile-time dependencies (because it's a minor pain // to get all the right libraries to build the COLLADA DLL), we load // it dynamically when it is required, instead of using the exported // functions and binding at link-time. if (! dll.IsLoaded()) { if (! dll.LoadDLL()) { LOGERROR(L"Failed to load COLLADA conversion DLL"); return false; } try { dll.LoadSymbol("set_logger", set_logger); dll.LoadSymbol("set_skeleton_definitions", set_skeleton_definitions); dll.LoadSymbol("convert_dae_to_pmd", convert_dae_to_pmd); dll.LoadSymbol("convert_dae_to_psa", convert_dae_to_psa); } catch (PSERROR_DllLoader&) { LOGERROR(L"Failed to load symbols from COLLADA conversion DLL"); dll.Unload(); return false; } VfsPath skeletonPath("art/skeletons/skeletons.xml"); // Set the filename for the logger to report set_logger(ColladaLog, static_cast(&skeletonPath)); CVFSFile skeletonFile; - if (skeletonFile.Load(g_VFS, skeletonPath) != PSRETURN_OK) + if (skeletonFile.Load(m_VFS, skeletonPath) != PSRETURN_OK) { LOGERROR(L"Failed to read skeleton definitions"); dll.Unload(); return false; } int ok = set_skeleton_definitions((const char*)skeletonFile.GetBuffer(), (int)skeletonFile.GetBufferSize()); if (ok < 0) { LOGERROR(L"Failed to load skeleton definitions"); dll.Unload(); return false; } - - // TODO: the cached PMD/PSA files should probably be invalidated when - // the skeleton definition file is changed, else people will get confused - // as to why it's not picking up their changes } // Set the filename for the logger to report set_logger(ColladaLog, const_cast(static_cast(&daeFilename))); // We need to null-terminate the buffer, so do it (possibly inefficiently) // by converting to a CStr CStr daeData; { CVFSFile daeFile; - if (daeFile.Load(g_VFS, daeFilename) != PSRETURN_OK) + if (daeFile.Load(m_VFS, daeFilename) != PSRETURN_OK) return false; daeData = daeFile.GetAsString(); } // Do the conversion into a memory buffer + // We need to check the result, as archive builder needs to know if the source dae + // was sucessfully converted to .pmd/psa + int result = -1; WriteBuffer writeBuffer; switch (type) { - case CColladaManager::PMD: convert_dae_to_pmd(daeData.c_str(), ColladaOutput, &writeBuffer); break; - case CColladaManager::PSA: convert_dae_to_psa(daeData.c_str(), ColladaOutput, &writeBuffer); break; + case CColladaManager::PMD: + result = convert_dae_to_pmd(daeData.c_str(), ColladaOutput, &writeBuffer); + break; + case CColladaManager::PSA: + result = convert_dae_to_psa(daeData.c_str(), ColladaOutput, &writeBuffer); + break; } // don't create zero-length files (as happens in test_invalid_dae when // we deliberately pass invalid XML data) because the VFS caching // logic warns when asked to load such. if (writeBuffer.Size()) { - Status ret = g_VFS->CreateFile(pmdFilename, writeBuffer.Data(), writeBuffer.Size()); + Status ret = m_VFS->CreateFile(pmdFilename, writeBuffer.Data(), writeBuffer.Size()); ENSURE(ret == INFO::OK); } - return true; + return (result == 0); } + +private: + PIVFS m_VFS; }; -CColladaManager::CColladaManager() -: m(new CColladaManagerImpl()) +CColladaManager::CColladaManager(const PIVFS& vfs) +: m(new CColladaManagerImpl(vfs)), m_VFS(vfs) { } CColladaManager::~CColladaManager() { delete m; } -VfsPath CColladaManager::GetLoadableFilename(const VfsPath& pathnameNoExtension, FileType type) +void CColladaManager::PrepareCacheKey(MD5& hash, u32& version) +{ + // Include skeletons.xml file info in the hash + VfsPath skeletonPath("art/skeletons/skeletons.xml"); + FileInfo fileInfo; + + // This will cause an assertion failure if skeletons.xml doesn't exist, + // because fileinfo is not a NULL pointer, which is annoying but that + // should never happen, unless there really is a problem + if (m_VFS->GetFileInfo(skeletonPath, &fileInfo) != INFO::OK) + { + LOGERROR(L"Failed to stat '%ls' for DAE caching", skeletonPath.string().c_str()); + // We can continue, something else will break if we try loading a skeletal model + } + else + { + u64 skeletonsModifyTime = (u64)fileInfo.MTime() & ~1; // skip lowest bit, since zip and FAT don't preserve it + u64 skeletonsSize = (u64)fileInfo.Size(); + hash.Update((const u8*)&skeletonsModifyTime, sizeof(skeletonsModifyTime)); + hash.Update((const u8*)&skeletonsSize, sizeof(skeletonsSize)); + } + + // Add converter version to the hash + version = COLLADA_CONVERTER_VERSION; +} + +VfsPath CColladaManager::GetLoadablePath(const VfsPath& pathnameNoExtension, FileType type) { std::wstring extn; switch (type) { case PMD: extn = L".pmd"; break; case PSA: extn = L".psa"; break; // no other alternatives } /* - If there is a .dae file: - * Calculate a hash to identify it. - * Look for a cached .pmd file matching that hash. - * If it exists, load it. Else, convert the .dae into .pmd and load it. - Otherwise, if there is a (non-cache) .pmd file: - * Load it. - Else, fail. - - The hash calculation ought to be fast, since normally (during development) - the .dae file will exist but won't have changed recently and so the cache - would be used. Hence, just hash the file's size, mtime, and the converter - version number (so updates of the converter can cause regeneration of .pmds) - instead of the file's actual contents. + Algorithm: + * Calculate hash of skeletons.xml and converter version. + * Use CCacheLoader to check for archived or loose cached .pmd/psa. + * If cached version exists: + * Return pathname of cached .pmd/psa. + * Else, if source .dae for this model exists: + * Convert it to cached .pmd/psa. + * If converter succeeded: + * Return pathname of cached .pmd/psa. + * Else, fail (return empty path). + * Else, if uncached .pmd/psa exists: + * Return pathname of uncached .pmd/psa. + * Else, fail (return empty path). + + Since we use CCacheLoader which automatically hashes file size and mtime, + and handles archived files and loose cache, when preparing the cache key + we add converter version number (so updates of the converter cause + regeneration of the .pmd/psa) and the global skeletons.xml file size and + mtime, as modelers frequently change the contents of skeletons.xml and get + perplexed if the in-game models haven't updated as expected (we don't know + which models were affected by the skeletons.xml change, if any, so we just + regenerate all of them) - TODO (maybe): The .dae -> .pmd conversion may fail (e.g. if the .dae is + TODO (maybe): The .dae -> .pmd/psa conversion may fail (e.g. if the .dae is invalid or unsupported), but it may take a long time to start the conversion then realise it's not going to work. That will delay the loading of the game every time, which is annoying, so maybe it should cache the error message until the .dae is updated and fixed. (Alternatively, avoid having that many broken .daes in the game.) */ - // (TODO: the comments and variable names say "pmd" but actually they can - // be "psa" too.) - - VfsPath dae(pathnameNoExtension.ChangeExtension(L".dae")); - if (! VfsFileExists(dae)) + // Now we're looking for cached files + CCacheLoader cacheLoader(m_VFS, extn); + MD5 hash; + u32 version; + PrepareCacheKey(hash, version); + + VfsPath cachePath; + VfsPath sourcePath = pathnameNoExtension.ChangeExtension(L".dae"); + Status ret = cacheLoader.TryLoadingCached(sourcePath, hash, version, cachePath); + if (ret == INFO::OK) { - // No .dae - got to use the .pmd, assuming there is one - return pathnameNoExtension.ChangeExtension(extn); + // Found a valid cached version + return cachePath; + } + else if (ret == INFO::SKIPPED) + { + // No valid cached version was found - but source .dae exists + // We'll try converting it + } + else + { + // No valid cached version was found, and no source .dae exists + ENSURE(ret < 0); + + // Check if source (uncached) .pmd/psa exists + sourcePath = pathnameNoExtension.ChangeExtension(extn); + if (m_VFS->GetFileInfo(sourcePath, NULL) != INFO::OK) + { + // Broken reference, the caller will need to handle this + return L""; + } + else + { + return sourcePath; + } } - // There is a .dae - see if there's an up-to-date cached copy - - FileInfo fileInfo; - if (g_VFS->GetFileInfo(dae, &fileInfo) < 0) + // We have a source .dae and invalid cached version, so regenerate cached version + if (! m->Convert(sourcePath, cachePath, type)) { - // This shouldn't occur for any sensible reasons - LOGERROR(L"Failed to stat DAE file '%ls'", dae.string().c_str()); - return VfsPath(); - } - - // Build a struct of all the data we want to hash. - // (Use ints and not time_t/off_t because we don't care about overflow - // but do care about the fields not being 64-bit aligned) - // (Remove the lowest bit of mtime because some things round it to a - // resolution of 2 seconds) -#pragma pack(push, 1) - struct { int version; int mtime; int size; } hashSource - = { COLLADA_CONVERTER_VERSION, (int)fileInfo.MTime() & ~1, (int)fileInfo.Size() }; - cassert(sizeof(hashSource) == sizeof(int) * 3); // no padding, because that would be bad -#pragma pack(pop) - - // Calculate the hash, convert to hex - u32 hash = fnv_hash(static_cast(&hashSource), sizeof(hashSource)); - wchar_t hashString[9]; - swprintf_s(hashString, ARRAY_SIZE(hashString), L"%08x", hash); - std::wstring extension(L"_"); - extension += hashString; - extension += extn; - - // realDaePath_ is "[..]/mods/whatever/art/meshes/whatever.dae" - OsPath realDaePath_; - Status ret = g_VFS->GetRealPath(dae, realDaePath_); - ENSURE(ret == INFO::OK); - wchar_t realDaeBuf[PATH_MAX]; - wcscpy_s(realDaeBuf, ARRAY_SIZE(realDaeBuf), realDaePath_.string().c_str()); - std::replace(realDaeBuf, realDaeBuf+ARRAY_SIZE(realDaeBuf), '\\', '/'); - const wchar_t* realDaePath = wcsstr(realDaeBuf, L"mods/"); - - // cachedPmdVfsPath is "cache/mods/whatever/art/meshes/whatever_{hash}.pmd" - VfsPath cachedPmdVfsPath = VfsPath("cache") / realDaePath; - cachedPmdVfsPath = cachedPmdVfsPath.ChangeExtension(extension); + // The COLLADA converter failed for some reason, this will need to be handled + // by the caller + return L""; + } + + return cachePath; +} - // If it's not in the cache, we'll have to create it first - if (! VfsFileExists(cachedPmdVfsPath)) +bool CColladaManager::GenerateCachedFile(const VfsPath& sourcePath, FileType type, VfsPath& archiveCachePath) +{ + std::wstring extn; + switch (type) { - if (! m->Convert(dae, cachedPmdVfsPath, type)) - return L""; // failed to convert + case PMD: extn = L".pmd"; break; + case PSA: extn = L".psa"; break; + // no other alternatives } - return cachedPmdVfsPath; + CCacheLoader cacheLoader(m_VFS, extn); + MD5 hash; + u32 version; + PrepareCacheKey(hash, version); + + archiveCachePath = cacheLoader.ArchiveCachePath(sourcePath); + + return m->Convert(sourcePath, VfsPath("cache") / archiveCachePath, type); }