Index: ps/trunk/source/lib/file/disabled_tests/test_file_cache.h =================================================================== --- ps/trunk/source/lib/file/disabled_tests/test_file_cache.h (revision 20638) +++ ps/trunk/source/lib/file/disabled_tests/test_file_cache.h (nonexistent) @@ -1,73 +0,0 @@ -/* Copyright (C) 2010 Wildfire Games. - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -#include "lib/self_test.h" - -#include "lib/res/file/file_cache.h" -#include "lib/rand.h" - -class TestFileCache : public CxxTest::TestSuite -{ - enum { TEST_ALLOC_TOTAL = 100*1000*1000 }; -public: - void test_cache_allocator() - { - // allocated address -> its size - typedef std::map AllocMap; - AllocMap allocations; - - // put allocator through its paces by allocating several times - // its capacity (this ensures memory is reused) - srand(1); - size_t total_size_used = 0; - while(total_size_used < TEST_ALLOC_TOTAL) - { - size_t size = rand(1, TEST_ALLOC_TOTAL/16); - total_size_used += size; - void* p; - // until successful alloc: - for(;;) - { - p = file_cache_allocator_alloc(size); - if(p) - break; - // out of room - remove a previous allocation - // .. choose one at random - size_t chosen_idx = (size_t)rand(0, (size_t)allocations.size()); - AllocMap::iterator it = allocations.begin(); - for(; chosen_idx != 0; chosen_idx--) - ++it; - file_cache_allocator_free(it->first, it->second); - allocations.erase(it); - } - - // must not already have been allocated - TS_ASSERT_EQUALS(allocations.find(p), allocations.end()); - allocations[p] = size; - } - - // reset to virginal state - // note: even though everything has now been freed, this is - // necessary since the freelists may be a bit scattered already. - file_cache_allocator_reset(); - } -}; Property changes on: ps/trunk/source/lib/file/disabled_tests/test_file_cache.h ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Index: ps/trunk/source/lib/file/vfs/file_cache.cpp =================================================================== --- ps/trunk/source/lib/file/vfs/file_cache.cpp (revision 20638) +++ ps/trunk/source/lib/file/vfs/file_cache.cpp (nonexistent) @@ -1,253 +0,0 @@ -/* Copyright (C) 2010 Wildfire Games. - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -/* - * cache of file contents (supports zero-copy IO) - */ - -#include "precompiled.h" -#include "lib/file/vfs/file_cache.h" - -#include "lib/external_libraries/suppress_boost_warnings.h" - -#include "lib/file/common/file_stats.h" -#include "lib/adts/cache_adt.h" -#include "lib/bits.h" // round_up -#include "lib/allocators/allocator_checker.h" -#include "lib/allocators/shared_ptr.h" -#include "lib/allocators/headerless.h" -#include "lib/sysdep/os_cpu.h" // os_cpu_PageSize -#include "lib/posix/posix_mman.h" // mprotect - - -//----------------------------------------------------------------------------- -// allocator - -/* -the biggest worry of a file cache is external fragmentation. there are two -basic ways to combat this: -1) 'defragment' periodically - move blocks around to increase -size of available 'holes'. -2) prevent fragmentation from occurring at all via -deliberate alloc/free policy. - -file contents are returned directly to the user (zero-copy IO), so only -currently unreferenced blocks can be moved. it is believed that this would -severely hamper defragmentation; we therefore go with the latter approach. - -the basic insight is: fragmentation occurs when a block is freed whose -neighbors are not free (thus preventing coalescing). this can be prevented by -allocating objects of similar lifetimes together. typical workloads -(uniform access frequency) already show such behavior: the Landlord cache -manager evicts files in an LRU manner, which matches the allocation policy. - -references: -"The Memory Fragmentation Problem - Solved?" (Johnstone and Wilson) -"Dynamic Storage Allocation - A Survey and Critical Review" (Johnstone and Wilson) -*/ - -// shared_ptrs must own a reference to their allocator to ensure it's extant when -// they are freed. it is stored in the shared_ptr deleter. -class Allocator; -typedef shared_ptr PAllocator; - -class FileCacheDeleter -{ -public: - FileCacheDeleter(size_t size, const PAllocator& allocator) - : m_size(size), m_allocator(allocator) - { - } - - // (this uses Allocator and must come after its definition) - void operator()(u8* mem) const; - -private: - size_t m_size; - PAllocator m_allocator; -}; - - -// adds statistics and AllocatorChecker to a HeaderlessAllocator -class Allocator -{ -public: - Allocator(size_t maxSize) - : m_allocator(maxSize) - { - } - - shared_ptr Allocate(size_t size, const PAllocator& pthis) - { - const size_t alignedSize = Align(size); - - u8* mem = (u8*)m_allocator.Allocate(alignedSize); - if(!mem) - return DummySharedPtr(0); // (prevent FileCacheDeleter from seeing a null pointer) - -#ifndef NDEBUG - m_checker.OnAllocate(mem, alignedSize); -#endif - - stats_buf_alloc(size, alignedSize); - return shared_ptr(mem, FileCacheDeleter(size, pthis)); - } - - void Deallocate(u8* mem, size_t size) - { - const size_t alignedSize = Align(size); - - // (re)allow writes in case the buffer was made read-only. it would - // be nice to unmap the buffer, but this is not possible because - // HeaderlessAllocator needs to affix boundary tags. - (void)mprotect(mem, size, PROT_READ|PROT_WRITE); - -#ifndef NDEBUG - m_checker.OnDeallocate(mem, alignedSize); -#endif - m_allocator.Deallocate(mem, alignedSize); - - stats_buf_free(); - } - -private: - HeaderlessAllocator m_allocator; - -#ifndef NDEBUG - AllocatorChecker m_checker; -#endif -}; - - -void FileCacheDeleter::operator()(u8* mem) const -{ - m_allocator->Deallocate(mem, m_size); -} - - -//----------------------------------------------------------------------------- -// FileCache::Impl -//----------------------------------------------------------------------------- - -// since users are strongly encouraged to only load/process one file at a -// time, there won't be many active references to cache entries. we could -// take advantage of this with a separate extant list, but the cache's -// hash map should be fast enough and this way is less work than maintaining -// (possibly disjunct) cached and extant lists. - -class FileCache::Impl -{ -public: - Impl(size_t maxSize) - : m_allocator(new Allocator(maxSize)) - { - } - - shared_ptr Reserve(size_t size) - { - // (should never happen because the VFS ensures size != 0.) - ENSURE(size != 0); - - // (300 iterations have been observed when reserving several MB - // of space in a full cache) - for(;;) - { - { - shared_ptr data = m_allocator->Allocate(size, m_allocator); - if(data) - return data; - } - - // remove least valuable entry from cache (if users are holding - // references, the contents won't actually be deallocated) - { - shared_ptr discardedData; size_t discardedSize; - bool removed = m_cache.remove_least_valuable(&discardedData, &discardedSize); - // the cache is empty, and allocation still failed. - // apparently the cache is full of data that's still - // referenced, so we can't reserve any more space. - if(!removed) - return shared_ptr(); - } - } - } - - void Add(const VfsPath& pathname, const shared_ptr& data, size_t size, size_t cost) - { - // zero-copy cache => all users share the contents => must not - // allow changes. this will be reverted when deallocating. - (void)mprotect((void*)data.get(), size, PROT_READ); - - m_cache.add(pathname, data, size, cost); - } - - bool Retrieve(const VfsPath& pathname, shared_ptr& data, size_t& size) - { - // (note: don't call stats_cache because we don't know the file size - // in case of a cache miss; doing so is left to the caller.) - stats_buf_ref(); - - return m_cache.retrieve(pathname, data, &size); - } - - void Remove(const VfsPath& pathname) - { - m_cache.remove(pathname); - - // note: we could check if someone is still holding a reference - // to the contents, but that currently doesn't matter. - } - -private: - typedef Cache > CacheType; - CacheType m_cache; - - PAllocator m_allocator; -}; - - -//----------------------------------------------------------------------------- - -FileCache::FileCache(size_t size) - : impl(new Impl(size)) -{ -} - -shared_ptr FileCache::Reserve(size_t size) -{ - return impl->Reserve(size); -} - -void FileCache::Add(const VfsPath& pathname, const shared_ptr& data, size_t size, size_t cost) -{ - impl->Add(pathname, data, size, cost); -} - -void FileCache::Remove(const VfsPath& pathname) -{ - impl->Remove(pathname); -} - -bool FileCache::Retrieve(const VfsPath& pathname, shared_ptr& data, size_t& size) -{ - return impl->Retrieve(pathname, data, size); -} Property changes on: ps/trunk/source/lib/file/vfs/file_cache.cpp ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Index: ps/trunk/source/lib/file/vfs/file_cache.h =================================================================== --- ps/trunk/source/lib/file/vfs/file_cache.h (revision 20638) +++ ps/trunk/source/lib/file/vfs/file_cache.h (nonexistent) @@ -1,108 +0,0 @@ -/* Copyright (C) 2010 Wildfire Games. - * - * Permission is hereby granted, free of charge, to any person obtaining - * a copy of this software and associated documentation files (the - * "Software"), to deal in the Software without restriction, including - * without limitation the rights to use, copy, modify, merge, publish, - * distribute, sublicense, and/or sell copies of the Software, and to - * permit persons to whom the Software is furnished to do so, subject to - * the following conditions: - * - * The above copyright notice and this permission notice shall be included - * in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, - * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF - * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. - * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY - * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, - * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE - * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ - -/* - * cache of file contents (supports zero-copy IO) - */ - -#ifndef INCLUDED_FILE_CACHE -#define INCLUDED_FILE_CACHE - -#include "lib/file/vfs/vfs_path.h" - -/** - * cache of file contents with support for zero-copy IO. - * this works by reserving a region of the cache, using it as the IO buffer, - * and returning the memory directly to users. optional write-protection - * via MMU ensures that the shared contents aren't inadvertently changed. - * - * (unique copies of) VFS pathnames are used as lookup key and owner tag. - * - * to ensure efficient operation and prevent fragmentation, only one - * reference should be active at a time. in other words, read a file, - * process it, and only then start reading the next file. - * - * rationale: this is rather similar to BlockCache; however, the differences - * (Reserve's size parameter, eviction policies) are enough to warrant - * separate implementations. - **/ -class FileCache -{ -public: - /** - * @param size maximum amount [bytes] of memory to use for the cache. - * (managed as a virtual memory region that's committed on-demand) - **/ - FileCache(size_t size); - - /** - * Reserve a chunk of the cache's memory region. - * - * @param size required number of bytes (more may be allocated due to - * alignment and/or internal fragmentation) - * @return memory suitably aligned for IO; never fails. - * - * it is expected that this data will be Add()-ed once its IO completes. - **/ - shared_ptr Reserve(size_t size); - - /** - * Add a file's contents to the cache. - * - * The cache will be able to satisfy subsequent Retrieve() calls by - * returning this data; if CONFIG2_CACHE_READ_ONLY, the buffer is made - * read-only. If need be and no references are currently attached to it, - * the memory can also be commandeered by Reserve(). - * - * @param data - * @param size - * @param pathname key that will be used to Retrieve file contents. - * @param cost is the expected cost of retrieving the file again and - * influences how/when it is evicted from the cache. - **/ - void Add(const VfsPath& pathname, const shared_ptr& data, size_t size, size_t cost = 1); - - /** - * Remove a file's contents from the cache (if it exists). - * - * this ensures subsequent reads of the files see the current, presumably - * recently changed, contents of the file. - * - * this would typically be called in response to a notification that a - * file has changed. - **/ - void Remove(const VfsPath& pathname); - - /** - * Attempt to retrieve a file's contents from the file cache. - * - * @return whether the contents were successfully retrieved; if so, - * data references the read-only file contents. - **/ - bool Retrieve(const VfsPath& pathname, shared_ptr& data, size_t& size); - -private: - class Impl; - shared_ptr impl; -}; - -#endif // #ifndef INCLUDED_FILE_CACHE Property changes on: ps/trunk/source/lib/file/vfs/file_cache.h ___________________________________________________________________ Deleted: svn:eol-style ## -1 +0,0 ## -native \ No newline at end of property Index: ps/trunk/source/graphics/tests/test_MeshManager.h =================================================================== --- ps/trunk/source/graphics/tests/test_MeshManager.h (revision 20638) +++ ps/trunk/source/graphics/tests/test_MeshManager.h (revision 20639) @@ -1,261 +1,261 @@ -/* Copyright (C) 2012 Wildfire Games. +/* Copyright (C) 2017 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); + g_VFS = CreateVfs(); 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(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); copyFile(srcSkeletonDefs, testSkeletonDefs); 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); copyFile(srcSkeletonDefs, testSkeletonDefs); CModelDefPtr modeldef = meshManager->GetMesh(testBase); TS_ASSERT(modeldef); if (modeldef) TS_ASSERT_PATH_EQUALS(modeldef->GetName(), testBase); } void test_caching() { copyFile(srcPMD, testPMD); copyFile(srcSkeletonDefs, testSkeletonDefs); 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->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_STR_CONTAINS(logger.GetOutput(), "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_STR_CONTAINS(logger.GetOutput(), "parser error"); } void test_load_nonexistent_pmd() { TestLogger logger; copyFile(srcSkeletonDefs, testSkeletonDefs); CModelDefPtr modeldef = meshManager->GetMesh(testPMD); TS_ASSERT(! modeldef); } void test_load_nonexistent_dae() { TestLogger logger; copyFile(srcSkeletonDefs, testSkeletonDefs); 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/tests/test_TextureConverter.h =================================================================== --- ps/trunk/source/graphics/tests/test_TextureConverter.h (revision 20638) +++ ps/trunk/source/graphics/tests/test_TextureConverter.h (revision 20639) @@ -1,97 +1,96 @@ -/* Copyright (C) 2010 Wildfire Games. +/* Copyright (C) 2017 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 "graphics/TextureConverter.h" -#include "lib/alignment.h" #include "lib/file/vfs/vfs.h" #include "lib/res/h_mgr.h" #include "lib/tex/tex.h" #include "ps/XML/Xeromyces.h" class TestTextureConverter : public CxxTest::TestSuite { PIVFS m_VFS; public: void setUp() { DeleteDirectory(DataDir()/"_testcache"); // clean up in case the last test run failed - m_VFS = CreateVfs(20*MiB); + m_VFS = CreateVfs(); TS_ASSERT_OK(m_VFS->Mount(L"", DataDir()/"mods"/"_test.tex", VFS_MOUNT_MUST_EXIST)); TS_ASSERT_OK(m_VFS->Mount(L"cache/", DataDir()/"_testcache")); } void tearDown() { m_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); } void test_convert_quality() { // Test for the bug in http://code.google.com/p/nvidia-texture-tools/issues/detail?id=139 VfsPath src = L"art/textures/b/test.png"; CTextureConverter converter(m_VFS, false); CTextureConverter::Settings settings = converter.ComputeSettings(L"", std::vector()); TS_ASSERT(converter.ConvertTexture(CTexturePtr(), src, L"cache/test.png", settings)); VfsPath dest; for (size_t i = 0; i < 100; ++i) { CTexturePtr texture; bool ok; if (converter.Poll(texture, dest, ok)) { TS_ASSERT(ok); break; } SDL_Delay(10); } shared_ptr file; size_t fileSize = 0; TS_ASSERT_OK(m_VFS->LoadFile(dest, file, fileSize)); Tex tex; TS_ASSERT_OK(tex.decode(file, fileSize)); TS_ASSERT_OK(tex.transform_to((tex.m_Flags | TEX_BGR | TEX_ALPHA) & ~(TEX_DXT | TEX_MIPMAPS))); u8* texdata = tex.get_data(); // The source texture is repeated after 4 pixels, so the compressed texture // should be identical after 4 pixels TS_ASSERT_EQUALS(texdata[0*4], texdata[4*4]); // 1st, 2nd and 4th rows should be unequal TS_ASSERT_DIFFERS(texdata[0*4], texdata[8*4]); TS_ASSERT_EQUALS(texdata[8*4], texdata[16*4]); TS_ASSERT_DIFFERS(texdata[16*4], texdata[24*4]); // for (size_t i = 0; i < tex.dataSize; ++i) // { // if (i % 4 == 0) printf("\n"); // printf("%02x ", texdata[i]); // } } }; Index: ps/trunk/source/graphics/tests/test_TextureManager.h =================================================================== --- ps/trunk/source/graphics/tests/test_TextureManager.h (revision 20638) +++ ps/trunk/source/graphics/tests/test_TextureManager.h (revision 20639) @@ -1,129 +1,128 @@ -/* Copyright (C) 2012 Wildfire Games. +/* Copyright (C) 2017 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 "graphics/TextureManager.h" -#include "lib/alignment.h" #include "lib/external_libraries/libsdl.h" #include "lib/file/vfs/vfs.h" #include "lib/res/h_mgr.h" #include "lib/tex/tex.h" #include "lib/ogl.h" #include "ps/XML/Xeromyces.h" class TestTextureManager : public CxxTest::TestSuite { PIVFS m_VFS; public: void setUp() { DeleteDirectory(DataDir()/"_testcache"); // clean up in case the last test run failed - m_VFS = CreateVfs(20*MiB); + m_VFS = CreateVfs(); TS_ASSERT_OK(m_VFS->Mount(L"", DataDir()/"mods"/"_test.tex", VFS_MOUNT_MUST_EXIST)); TS_ASSERT_OK(m_VFS->Mount(L"cache/", DataDir()/"_testcache")); h_mgr_init(); CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); h_mgr_shutdown(); m_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); } void test_load_basic() { { CTextureManager texman(m_VFS, false, true); CTexturePtr t1 = texman.CreateTexture(CTextureProperties(L"art/textures/a/demo.png")); TS_ASSERT(!t1->IsLoaded()); TS_ASSERT(!t1->TryLoad()); TS_ASSERT(!t1->IsLoaded()); TS_ASSERT(texman.MakeProgress()); for (size_t i = 0; i < 100; ++i) { if (texman.MakeProgress()) break; SDL_Delay(10); } TS_ASSERT(t1->IsLoaded()); // We can't test sizes because we had to disable GL function calls // and therefore couldn't load the texture. Maybe we should try loading // the texture file directly, to make sure it's actually worked. // TS_ASSERT_EQUALS(t1->GetWidth(), (size_t)64); // TS_ASSERT_EQUALS(t1->GetHeight(), (size_t)64); // CreateTexture should return the same object CTexturePtr t2 = texman.CreateTexture(CTextureProperties(L"art/textures/a/demo.png")); TS_ASSERT(t1 == t2); } // New texture manager - should use the cached file { CTextureManager texman(m_VFS, false, true); CTexturePtr t1 = texman.CreateTexture(CTextureProperties(L"art/textures/a/demo.png")); TS_ASSERT(!t1->IsLoaded()); TS_ASSERT(t1->TryLoad()); TS_ASSERT(t1->IsLoaded()); } } void test_load_formats() { CTextureManager texman(m_VFS, false, true); CTexturePtr t1 = texman.CreateTexture(CTextureProperties(L"art/textures/a/demo.tga")); CTexturePtr t2 = texman.CreateTexture(CTextureProperties(L"art/textures/a/demo-abgr.dds")); CTexturePtr t3 = texman.CreateTexture(CTextureProperties(L"art/textures/a/demo-dxt1.dds")); CTexturePtr t4 = texman.CreateTexture(CTextureProperties(L"art/textures/a/demo-dxt5.dds")); t1->TryLoad(); t2->TryLoad(); t3->TryLoad(); t4->TryLoad(); size_t done = 0; for (size_t i = 0; i < 100; ++i) { if (texman.MakeProgress()) ++done; if (done == 8) // 4 loads, 4 conversions break; SDL_Delay(10); } TS_ASSERT(t1->IsLoaded()); TS_ASSERT(t2->IsLoaded()); TS_ASSERT(t3->IsLoaded()); TS_ASSERT(t4->IsLoaded()); } }; Index: ps/trunk/source/lib/file/common/file_stats.cpp =================================================================== --- ps/trunk/source/lib/file/common/file_stats.cpp (revision 20638) +++ ps/trunk/source/lib/file/common/file_stats.cpp (revision 20639) @@ -1,337 +1,313 @@ /* Copyright (C) 2010 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* * gathers statistics from all file modules. */ #include "precompiled.h" #include "lib/file/common/file_stats.h" #include #include "lib/timer.h" #if FILE_STATS_ENABLED // vfs static size_t vfs_files; static double vfs_size_total; static double vfs_init_elapsed_time; // file static size_t unique_names; static size_t unique_name_len_total; static size_t open_files_cur, open_files_max; // total = opened_files.size() // file_buf static size_t extant_bufs_cur, extant_bufs_max, extant_bufs_total; static double buf_size_total, buf_aligned_size_total; // file_io static size_t user_ios; static double user_io_size_total; static double io_actual_size_total[FI_MAX_IDX][2]; static double io_elapsed_time[FI_MAX_IDX][2]; static double io_process_time_total; static size_t io_seeks; // file_cache static size_t cache_count[2]; static double cache_size_total[2]; static size_t conflict_misses; //static double conflict_miss_size_total; // JW: currently not used nor computed static size_t block_cache_count[2]; // archive builder static size_t ab_connection_attempts; // total number of trace entries static size_t ab_repeated_connections; // how many of these were not unique // convenience functions for measuring elapsed time in an interval. // by exposing start/finish calls, we avoid callers from querying // timestamps when stats are disabled. static double start_time; static void timer_start(double* start_time_storage = &start_time) { // make sure no measurement is currently active // (since start_time is shared static storage) ENSURE(*start_time_storage == 0.0); *start_time_storage = timer_Time(); } static double timer_reset(double* start_time_storage = &start_time) { double elapsed = timer_Time() - *start_time_storage; *start_time_storage = 0.0; return elapsed; } //----------------------------------------------------------------------------- // // vfs // void stats_vfs_file_add(size_t file_size) { vfs_files++; vfs_size_total += file_size; } void stats_vfs_file_remove(size_t file_size) { vfs_files--; vfs_size_total -= file_size; } // stats_vfs_init_* are currently unused void stats_vfs_init_start() { timer_start(); } void stats_vfs_init_finish() { vfs_init_elapsed_time += timer_reset(); } // // file // void stats_unique_name(size_t name_len) { unique_names++; unique_name_len_total += name_len; } void stats_open() { open_files_cur++; open_files_max = std::max(open_files_max, open_files_cur); // could also use a set to determine unique files that have been opened } void stats_close() { ENSURE(open_files_cur > 0); open_files_cur--; } // // file_buf // void stats_buf_alloc(size_t size, size_t alignedSize) { extant_bufs_cur++; extant_bufs_max = std::max(extant_bufs_max, extant_bufs_cur); extant_bufs_total++; buf_size_total += size; buf_aligned_size_total += alignedSize; } void stats_buf_free() { ENSURE(extant_bufs_cur > 0); extant_bufs_cur--; } void stats_buf_ref() { extant_bufs_cur++; } // // file_io // void stats_io_user_request(size_t user_size) { user_ios++; user_io_size_total += user_size; } ScopedIoMonitor::ScopedIoMonitor() { m_startTime = 0.0; timer_start(&m_startTime); } ScopedIoMonitor::~ScopedIoMonitor() { // note: we can only bill IOs that have succeeded :S timer_reset(&m_startTime); } void ScopedIoMonitor::NotifyOfSuccess(FileIOImplentation fi, int opcode, off_t size) { ENSURE(fi < FI_MAX_IDX); ENSURE(opcode == LIO_READ || opcode == LIO_WRITE); io_actual_size_total[fi][opcode == LIO_WRITE] += size; io_elapsed_time[fi][opcode == LIO_WRITE] += timer_reset(&m_startTime); } void stats_cb_start() { timer_start(); } void stats_cb_finish() { io_process_time_total += timer_reset(); } -// -// file_cache -// - -void stats_cache(CacheRet cr, size_t size) -{ - ENSURE(cr == CR_HIT || cr == CR_MISS); - -#if 0 - if(cr == CR_MISS) - { - PairIB ret = ever_cached_files.insert(atom_fn); - if(!ret.second) // was already cached once - { - conflict_miss_size_total += size; - conflict_misses++; - } - } -#endif - - cache_count[cr]++; - cache_size_total[cr] += size; -} - void stats_block_cache(CacheRet cr) { ENSURE(cr == CR_HIT || cr == CR_MISS); block_cache_count[cr]++; } // // archive builder // void stats_ab_connection(bool already_exists) { ab_connection_attempts++; if(already_exists) ab_repeated_connections++; } //----------------------------------------------------------------------------- template int percent(T num, T divisor) { if(!divisor) return 0; return (int)(100*num / divisor); } void file_stats_dump() { if(!debug_filter_allows("FILE_STATS|")) return; const double KB = 1e3; const double MB = 1e6; const double ms = 1e-3; debug_printf("--------------------------------------------------------------------------------\n"); debug_printf("File statistics:\n"); // note: we split the reports into several debug_printfs for clarity; // this is necessary anyway due to fixed-size buffer. debug_printf( L"\nvfs:\n" L"Total files: %lu (%g MB)\n" L"Init/mount time: %g ms\n", (unsigned long)vfs_files, vfs_size_total/MB, vfs_init_elapsed_time/ms ); debug_printf( L"\nfile:\n" L"Total names: %lu (%lu KB)\n" L"Max. concurrent: %lu; leaked: %lu.\n", (unsigned long)unique_names, (unsigned long)(unique_name_len_total/1000), (unsigned long)open_files_max, (unsigned long)open_files_cur ); debug_printf( L"\nfile_buf:\n" L"Total buffers used: %lu (%g MB)\n" L"Max concurrent: %lu; leaked: %lu\n" L"Internal fragmentation: %d%%\n", (unsigned long)extant_bufs_total, buf_size_total/MB, (unsigned long)extant_bufs_max, (unsigned long)extant_bufs_cur, percent(buf_aligned_size_total-buf_size_total, buf_size_total) ); debug_printf( L"\nfile_io:\n" L"Total user load requests: %lu (%g MB)\n" L"IO thoughput [MB/s; 0=never happened]:\n" L" lowio: R=%.3g, W=%.3g\n" L" aio: R=%.3g, W=%.3g\n" L"Average size = %g KB; seeks: %lu; total callback time: %g ms\n" L"Total data actually read from disk = %g MB\n", (unsigned long)user_ios, user_io_size_total/MB, #define THROUGHPUT(impl, opcode) (io_elapsed_time[impl][opcode == LIO_WRITE] == 0.0)? 0.0 : (io_actual_size_total[impl][opcode == LIO_WRITE] / io_elapsed_time[impl][opcode == LIO_WRITE] / MB) THROUGHPUT(FI_LOWIO, LIO_READ), THROUGHPUT(FI_LOWIO, LIO_WRITE), THROUGHPUT(FI_AIO , LIO_READ), THROUGHPUT(FI_AIO , LIO_WRITE), user_io_size_total/user_ios/KB, (unsigned long)io_seeks, io_process_time_total/ms, (io_actual_size_total[FI_LOWIO][0]+io_actual_size_total[FI_AIO][0])/MB ); debug_printf( L"\nfile_cache:\n" L"Hits: %lu (%g MB); misses %lu (%g MB); ratio: %u%%\n" L"Percent of requested bytes satisfied by cache: %u%%; non-compulsory misses: %lu (%u%% of misses)\n" L"Block hits: %lu; misses: %lu; ratio: %u%%\n", (unsigned long)cache_count[CR_HIT], cache_size_total[CR_HIT]/MB, (unsigned long)cache_count[CR_MISS], cache_size_total[CR_MISS]/MB, percent(cache_count[CR_HIT], cache_count[CR_HIT]+cache_count[CR_MISS]), percent(cache_size_total[CR_HIT], cache_size_total[CR_HIT]+cache_size_total[CR_MISS]), (unsigned long)conflict_misses, percent(conflict_misses, cache_count[CR_MISS]), (unsigned long)block_cache_count[CR_HIT], (unsigned long)block_cache_count[CR_MISS], percent(block_cache_count[CR_HIT], block_cache_count[CR_HIT]+block_cache_count[CR_MISS]) ); debug_printf( L"\nvfs_optimizer:\n" L"Total trace entries: %lu; repeated connections: %lu; unique files: %lu\n", (unsigned long)ab_connection_attempts, (unsigned long)ab_repeated_connections, (unsigned long)(ab_connection_attempts-ab_repeated_connections) ); } #endif // FILE_STATS_ENABLED Index: ps/trunk/source/lib/file/common/file_stats.h =================================================================== --- ps/trunk/source/lib/file/common/file_stats.h (revision 20638) +++ ps/trunk/source/lib/file/common/file_stats.h (revision 20639) @@ -1,118 +1,116 @@ /* Copyright (C) 2010 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* * gathers statistics from all file modules. */ #ifndef INCLUDED_FILE_STATS #define INCLUDED_FILE_STATS #include "lib/posix/posix_aio.h" // LIO_READ, LIO_WRITE #define FILE_STATS_ENABLED 0 enum FileIOImplentation { FI_LOWIO, FI_AIO, FI_BCACHE, FI_MAX_IDX }; enum CacheRet { CR_HIT, CR_MISS }; #if FILE_STATS_ENABLED // vfs extern void stats_vfs_file_add(size_t file_size); extern void stats_vfs_file_remove(size_t file_size); extern void stats_vfs_init_start(); extern void stats_vfs_init_finish(); // file // currently not called because string_pool is now in lib/allocators extern void stats_unique_name(size_t name_len); extern void stats_open(); extern void stats_close(); // file_buf extern void stats_buf_alloc(size_t size, size_t alignedSize); extern void stats_buf_free(); extern void stats_buf_ref(); // file_io extern void stats_io_user_request(size_t user_size); // this is used to measure effective throughput for the two // synchronous IO variants. // note: improved measurements of the actual aio throughput by instrumenting // issue/wait doesn't work because IOManager's decompression may cause us to // miss the exact end of IO, thus throwing off measurements. class ScopedIoMonitor { public: ScopedIoMonitor(); ~ScopedIoMonitor(); void NotifyOfSuccess(FileIOImplentation fi, int opcode, off_t size); private: double m_startTime; }; extern void stats_cb_start(); extern void stats_cb_finish(); // file_cache -extern void stats_cache(CacheRet cr, size_t size); extern void stats_block_cache(CacheRet cr); // archive builder extern void stats_ab_connection(bool already_exists); extern void file_stats_dump(); #else #define stats_vfs_file_add(file_size) #define stats_vfs_file_remove(file_size) #define stats_vfs_init_start() #define stats_vfs_init_finish() #define stats_unique_name(name_len) #define stats_open() #define stats_close() #define stats_buf_alloc(size, alignedSize) #define stats_buf_free() #define stats_buf_ref() #define stats_io_user_request(user_size) class ScopedIoMonitor { public: ScopedIoMonitor() {} ~ScopedIoMonitor() {} void NotifyOfSuccess(FileIOImplentation UNUSED(fi), int UNUSED(opcode), off_t UNUSED(size)) {} }; #define stats_cb_start() #define stats_cb_finish() -#define stats_cache(cr, size) #define stats_block_cache(cr) #define stats_ab_connection(already_exists) #define file_stats_dump() #endif #endif // #ifndef INCLUDED_FILE_STATS Index: ps/trunk/source/lib/file/vfs/vfs.cpp =================================================================== --- ps/trunk/source/lib/file/vfs/vfs.cpp (revision 20638) +++ ps/trunk/source/lib/file/vfs/vfs.cpp (revision 20639) @@ -1,326 +1,299 @@ -/* Copyright (C) 2014 Wildfire Games. +/* Copyright (C) 2017 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "precompiled.h" #include "lib/file/vfs/vfs.h" #include "lib/allocators/shared_ptr.h" #include "lib/posix/posix_pthread.h" #include "lib/file/file_system.h" #include "lib/file/common/file_stats.h" #include "lib/file/common/trace.h" #include "lib/file/archive/archive.h" #include "lib/file/io/io.h" #include "lib/file/vfs/vfs_tree.h" #include "lib/file/vfs/vfs_lookup.h" #include "lib/file/vfs/vfs_populate.h" -#include "lib/file/vfs/file_cache.h" static const StatusDefinition vfsStatusDefinitions[] = { { ERR::VFS_DIR_NOT_FOUND, L"VFS directory not found" }, { ERR::VFS_FILE_NOT_FOUND, L"VFS file not found" }, { ERR::VFS_ALREADY_MOUNTED, L"VFS path already mounted" } }; STATUS_ADD_DEFINITIONS(vfsStatusDefinitions); static pthread_mutex_t vfs_mutex = PTHREAD_MUTEX_INITIALIZER; namespace { struct ScopedLock { ScopedLock() { pthread_mutex_lock(&vfs_mutex); } ~ScopedLock() { pthread_mutex_unlock(&vfs_mutex); } }; } // namespace class VFS : public IVFS { public: - VFS(size_t cacheSize) - : m_cacheSize(cacheSize), m_fileCache(m_cacheSize) - , m_trace(CreateDummyTrace(8*MiB)) + VFS() : m_trace(CreateDummyTrace(8*MiB)) { } virtual Status Mount(const VfsPath& mountPoint, const OsPath& path, size_t flags /* = 0 */, size_t priority /* = 0 */) { ScopedLock s; if(!DirectoryExists(path)) { if(flags & VFS_MOUNT_MUST_EXIST) return ERR::VFS_DIR_NOT_FOUND; // NOWARN else RETURN_STATUS_IF_ERR(CreateDirectories(path, 0700)); } VfsDirectory* directory; WARN_RETURN_STATUS_IF_ERR(vfs_Lookup(mountPoint, &m_rootDirectory, directory, 0, VFS_LOOKUP_ADD|VFS_LOOKUP_SKIP_POPULATE)); PRealDirectory realDirectory(new RealDirectory(path, priority, flags)); RETURN_STATUS_IF_ERR(vfs_Attach(directory, realDirectory)); return INFO::OK; } virtual Status GetFileInfo(const VfsPath& pathname, CFileInfo* pfileInfo) const { ScopedLock s; - VfsDirectory* directory; VfsFile* file; + VfsDirectory* directory; + VfsFile* file; + Status ret = vfs_Lookup(pathname, &m_rootDirectory, directory, &file); if(!pfileInfo) // just indicate if the file exists without raising warnings. return ret; WARN_RETURN_STATUS_IF_ERR(ret); *pfileInfo = CFileInfo(file->Name(), file->Size(), file->MTime()); return INFO::OK; } virtual Status GetFilePriority(const VfsPath& pathname, size_t* ppriority) const { ScopedLock s; VfsDirectory* directory; VfsFile* file; RETURN_STATUS_IF_ERR(vfs_Lookup(pathname, &m_rootDirectory, directory, &file)); *ppriority = file->Priority(); return INFO::OK; } virtual Status GetDirectoryEntries(const VfsPath& path, CFileInfos* fileInfos, DirectoryNames* subdirectoryNames) const { ScopedLock s; VfsDirectory* directory; RETURN_STATUS_IF_ERR(vfs_Lookup(path, &m_rootDirectory, directory, 0)); if(fileInfos) { const VfsDirectory::VfsFiles& files = directory->Files(); fileInfos->clear(); fileInfos->reserve(files.size()); for(VfsDirectory::VfsFiles::const_iterator it = files.begin(); it != files.end(); ++it) { const VfsFile& file = it->second; fileInfos->push_back(CFileInfo(file.Name(), file.Size(), file.MTime())); } } if(subdirectoryNames) { const VfsDirectory::VfsSubdirectories& subdirectories = directory->Subdirectories(); subdirectoryNames->clear(); subdirectoryNames->reserve(subdirectories.size()); for(VfsDirectory::VfsSubdirectories::const_iterator it = subdirectories.begin(); it != subdirectories.end(); ++it) subdirectoryNames->push_back(it->first); } return INFO::OK; } virtual Status CreateFile(const VfsPath& pathname, const shared_ptr& fileContents, size_t size) { ScopedLock s; VfsDirectory* directory; Status st; st = vfs_Lookup(pathname, &m_rootDirectory, directory, 0, VFS_LOOKUP_ADD|VFS_LOOKUP_CREATE|VFS_LOOKUP_CREATE_ALWAYS); if (st == ERR::FILE_ACCESS) return ERR::FILE_ACCESS; WARN_RETURN_STATUS_IF_ERR(st); const PRealDirectory& realDirectory = directory->AssociatedDirectory(); const OsPath name = pathname.Filename(); RETURN_STATUS_IF_ERR(realDirectory->Store(name, fileContents, size)); - // wipe out any cached blocks. this is necessary to cover the (rare) case - // of file cache contents predating the file write. - m_fileCache.Remove(pathname); - const VfsFile file(name, size, time(0), realDirectory->Priority(), realDirectory); directory->AddFile(file); m_trace->NotifyStore(pathname, size); return INFO::OK; } virtual Status ReplaceFile(const VfsPath& pathname, const shared_ptr& fileContents, size_t size) { ScopedLock s; VfsDirectory* directory; VfsFile* file; Status st; st = vfs_Lookup(pathname, &m_rootDirectory, directory, &file, VFS_LOOKUP_ADD|VFS_LOOKUP_CREATE); // There is no such file, create it. if (st == ERR::VFS_FILE_NOT_FOUND) { s.~ScopedLock(); return CreateFile(pathname, fileContents, size); } WARN_RETURN_STATUS_IF_ERR(st); RealDirectory realDirectory(file->Loader()->Path(), file->Priority(), directory->AssociatedDirectory()->Flags()); RETURN_STATUS_IF_ERR(realDirectory.Store(pathname.Filename(), fileContents, size)); - // See comment in CreateFile - m_fileCache.Remove(pathname); - directory->AddFile(*file); m_trace->NotifyStore(pathname, size); return INFO::OK; } virtual Status LoadFile(const VfsPath& pathname, shared_ptr& fileContents, size_t& size) { ScopedLock s; - const bool isCacheHit = m_fileCache.Retrieve(pathname, fileContents, size); - if(!isCacheHit) - { - VfsDirectory* directory; VfsFile* file; - // per 2010-05-01 meeting, this shouldn't raise 'scary error - // dialogs', which might fail to display the culprit pathname - // instead, callers should log the error, including pathname. - RETURN_STATUS_IF_ERR(vfs_Lookup(pathname, &m_rootDirectory, directory, &file)); - - fileContents = DummySharedPtr((u8*)0); - size = file->Size(); - if(size != 0) // (the file cache can't handle zero-length allocations) - { - if(size < m_cacheSize/2) // (avoid evicting lots of previous data) - fileContents = m_fileCache.Reserve(size); - if(fileContents) - { - RETURN_STATUS_IF_ERR(file->Loader()->Load(file->Name(), fileContents, file->Size())); - m_fileCache.Add(pathname, fileContents, size); - } - else - { - RETURN_STATUS_IF_ERR(AllocateAligned(fileContents, size, maxSectorSize)); - RETURN_STATUS_IF_ERR(file->Loader()->Load(file->Name(), fileContents, file->Size())); - } - } - } + + VfsDirectory* directory; VfsFile* file; + // per 2010-05-01 meeting, this shouldn't raise 'scary error + // dialogs', which might fail to display the culprit pathname + // instead, callers should log the error, including pathname. + RETURN_STATUS_IF_ERR(vfs_Lookup(pathname, &m_rootDirectory, directory, &file)); + + fileContents = DummySharedPtr((u8*)0); + size = file->Size(); + + RETURN_STATUS_IF_ERR(AllocateAligned(fileContents, size, maxSectorSize)); + RETURN_STATUS_IF_ERR(file->Loader()->Load(file->Name(), fileContents, file->Size())); stats_io_user_request(size); - stats_cache(isCacheHit? CR_HIT : CR_MISS, size); m_trace->NotifyLoad(pathname, size); return INFO::OK; } virtual std::wstring TextRepresentation() const { ScopedLock s; std::wstring textRepresentation; textRepresentation.reserve(100*KiB); DirectoryDescriptionR(textRepresentation, m_rootDirectory, 0); return textRepresentation; } virtual Status GetRealPath(const VfsPath& pathname, OsPath& realPathname) { ScopedLock s; VfsDirectory* directory; VfsFile* file; WARN_RETURN_STATUS_IF_ERR(vfs_Lookup(pathname, &m_rootDirectory, directory, &file)); realPathname = file->Loader()->Path() / pathname.Filename(); return INFO::OK; } virtual Status GetDirectoryRealPath(const VfsPath& pathname, OsPath& realPathname) { ScopedLock s; VfsDirectory* directory; WARN_RETURN_STATUS_IF_ERR(vfs_Lookup(pathname, &m_rootDirectory, directory, NULL)); realPathname = directory->AssociatedDirectory()->Path(); return INFO::OK; } virtual Status GetVirtualPath(const OsPath& realPathname, VfsPath& pathname) { ScopedLock s; const OsPath realPath = realPathname.Parent()/""; VfsPath path; RETURN_STATUS_IF_ERR(FindRealPathR(realPath, m_rootDirectory, L"", path)); pathname = path / realPathname.Filename(); return INFO::OK; } virtual Status RemoveFile(const VfsPath& pathname) { ScopedLock s; - m_fileCache.Remove(pathname); VfsDirectory* directory; VfsFile* file; RETURN_STATUS_IF_ERR(vfs_Lookup(pathname, &m_rootDirectory, directory, &file)); directory->RemoveFile(file->Name()); return INFO::OK; } virtual Status RepopulateDirectory(const VfsPath& path) { ScopedLock s; VfsDirectory* directory; RETURN_STATUS_IF_ERR(vfs_Lookup(path, &m_rootDirectory, directory, 0)); directory->RequestRepopulate(); return INFO::OK; } virtual void Clear() { ScopedLock s; m_rootDirectory.Clear(); } private: Status FindRealPathR(const OsPath& realPath, const VfsDirectory& directory, const VfsPath& curPath, VfsPath& path) { PRealDirectory realDirectory = directory.AssociatedDirectory(); if(realDirectory && realDirectory->Path() == realPath) { path = curPath; return INFO::OK; } const VfsDirectory::VfsSubdirectories& subdirectories = directory.Subdirectories(); for(VfsDirectory::VfsSubdirectories::const_iterator it = subdirectories.begin(); it != subdirectories.end(); ++it) { const OsPath& subdirectoryName = it->first; const VfsDirectory& subdirectory = it->second; Status ret = FindRealPathR(realPath, subdirectory, curPath / subdirectoryName/"", path); if(ret == INFO::OK) return INFO::OK; } return ERR::PATH_NOT_FOUND; // NOWARN } - size_t m_cacheSize; - FileCache m_fileCache; PITrace m_trace; mutable VfsDirectory m_rootDirectory; }; //----------------------------------------------------------------------------- -PIVFS CreateVfs(size_t cacheSize) +PIVFS CreateVfs() { - return PIVFS(new VFS(cacheSize)); + return PIVFS(new VFS()); } Index: ps/trunk/source/lib/file/vfs/vfs.h =================================================================== --- ps/trunk/source/lib/file/vfs/vfs.h (revision 20638) +++ ps/trunk/source/lib/file/vfs/vfs.h (revision 20639) @@ -1,240 +1,228 @@ -/* Copyright (C) 2013 Wildfire Games. +/* Copyright (C) 2017 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* * Virtual File System API - allows transparent access to files in * archives, modding via multiple mount points and hotloading. */ #ifndef INCLUDED_VFS #define INCLUDED_VFS #include "lib/file/file_system.h" // CFileInfo #include "lib/file/vfs/vfs_path.h" namespace ERR { const Status VFS_DIR_NOT_FOUND = -110100; const Status VFS_FILE_NOT_FOUND = -110101; const Status VFS_ALREADY_MOUNTED = -110102; } // (recursive mounting and mounting archives are no longer optional since they don't hurt) enum VfsMountFlags { /** * all real directories mounted during this operation will be watched * for changes. this flag is provided to avoid watches in output-only * directories, e.g. screenshots/ (only causes unnecessary overhead). **/ VFS_MOUNT_WATCH = 1, /** * anything mounted from here should be included when building archives. **/ VFS_MOUNT_ARCHIVABLE = 2, /** * return ERR::VFS_DIR_NOT_FOUND if the given real path doesn't exist. * (the default behavior is to create all real directories in the path) **/ VFS_MOUNT_MUST_EXIST = 4, /** * keep the files named "*.DELETED" visible in the VFS directories. * the standard behavior of hiding the file with the same name minus the * ".DELETED" suffix will still apply. * (the default behavior is to hide both the suffixed and unsuffixed files) **/ VFS_MOUNT_KEEP_DELETED = 8, /** * mark a directory replaceable, so that when writing a file to this path * new real directories will be created instead of reusing already existing * ones mounted at a subpath of the VFS path. * (the default behaviour is to write to the real directory associated * with the VFS directory that was last mounted to this path (or subpath)) **/ VFS_MOUNT_REPLACEABLE = 16 }; // (member functions are thread-safe after the instance has been // constructed - each acquires a pthread mutex.) struct IVFS { virtual ~IVFS() {} /** * mount a directory into the VFS. * * @param mountPoint (will be created if it does not already exist) * @param path real directory path * @param flags * @param priority * @return Status. * * if files are encountered that already exist in the VFS (sub)directories, * the most recent / highest priority/precedence version is preferred. * * if files with archive extensions are seen, their contents are added * as well. **/ virtual Status Mount(const VfsPath& mountPoint, const OsPath& path, size_t flags = 0, size_t priority = 0) = 0; /** * Retrieve information about a file (similar to POSIX stat). * * @param pathname * @param pfileInfo receives information about the file. Passing NULL * suppresses warnings if the file doesn't exist. * * @return Status. **/ virtual Status GetFileInfo(const VfsPath& pathname, CFileInfo* pfileInfo) const = 0; /** * Retrieve mount priority for a file. * * @param pathname * @param ppriority receives priority value, if the file can be found. * * @return Status. **/ virtual Status GetFilePriority(const VfsPath& pathname, size_t* ppriority) const = 0; /** * Retrieve lists of all files and subdirectories in a directory. * * @return Status. * * Rationale: * - this interface avoids having to lock the directory while an * iterator is extant. * - we cannot efficiently provide routines for returning files and * subdirectories separately due to the underlying POSIX interface. **/ virtual Status GetDirectoryEntries(const VfsPath& path, CFileInfos* fileInfos, DirectoryNames* subdirectoryNames) const = 0; /** * Create a file with the given contents. * @param pathname * @param fileContents * @param size [bytes] of the contents, will match that of the file. * @return Status. - * - * rationale: disallowing partial writes simplifies file cache coherency - * (we need only invalidate cached data when closing a newly written file). **/ virtual Status CreateFile(const VfsPath& pathname, const shared_ptr& fileContents, size_t size) = 0; /** * Replace a file with the given contents. * * @see CreateFile * * Used to replace a file if it is already present (even if the file is not * in the attached vfs directory). Calls CreateFile if the file doesn't yet * exist. **/ virtual Status ReplaceFile(const VfsPath& pathname, const shared_ptr& fileContents, size_t size) = 0; /** * Read an entire file into memory. * * @param pathname * @param fileContents receives a smart pointer to the contents. - * CAVEAT: this will be taken from the file cache if the VFS was - * created with cacheSize != 0 and size < cacheSize. There is no - * provision for Copy-on-Write, which means that such buffers - * must not be modified (this is enforced via mprotect). * @param size receives the size [bytes] of the file contents. * @return Status. **/ virtual Status LoadFile(const VfsPath& pathname, shared_ptr& fileContents, size_t& size) = 0; /** * @return a string representation of all files and directories. **/ virtual std::wstring TextRepresentation() const = 0; /** * retrieve the real (POSIX) pathname underlying a VFS file. * * this is useful for passing paths to external libraries. **/ virtual Status GetRealPath(const VfsPath& pathname, OsPath& realPathname) = 0; /** * retrieve the real (POSIX) pathname underlying a VFS directory. * * this is useful for passing paths to external libraries. **/ virtual Status GetDirectoryRealPath(const VfsPath& pathname, OsPath& realPathname) = 0; /** * retrieve the VFS pathname that corresponds to a real file. * * this is useful for reacting to file change notifications. * * the current implementation requires time proportional to the * number of directories; this could be accelerated by only checking * directories below a mount point with a matching real path. **/ virtual Status GetVirtualPath(const OsPath& realPathname, VfsPath& pathname) = 0; /** - * remove file from the virtual directory listing and evict its - * data from the cache. + * remove file from the virtual directory listing. **/ virtual Status RemoveFile(const VfsPath& pathname) = 0; /** * request the directory be re-populated when it is next accessed. * useful for synchronizing with the underlying filesystem after * files have been created or their metadata changed. **/ virtual Status RepopulateDirectory(const VfsPath& path) = 0; /** * empty the contents of the filesystem. * this is typically only necessary when changing the set of * mounted directories, e.g. when switching mods. * NB: open files are not affected. **/ virtual void Clear() = 0; }; typedef shared_ptr PIVFS; /** * create an instance of a Virtual File System. * - * @param cacheSize size [bytes] of memory to reserve for a file cache, - * or zero to disable it. if small enough to fit, file contents are - * stored here until no references remain and they are evicted. - * * note: there is no limitation to a single instance, it may make sense * to create and destroy VFS instances during each unit test. **/ -LIB_API PIVFS CreateVfs(size_t cacheSize); +LIB_API PIVFS CreateVfs(); #endif // #ifndef INCLUDED_VFS Index: ps/trunk/source/main.cpp =================================================================== --- ps/trunk/source/main.cpp (revision 20638) +++ ps/trunk/source/main.cpp (revision 20639) @@ -1,638 +1,638 @@ /* Copyright (C) 2017 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 . */ /* This module drives the game when running without Atlas (our integrated map editor). It receives input and OS messages via SDL and feeds them into the input dispatcher, where they are passed on to the game GUI and simulation. It also contains main(), which either runs the above controller or that of Atlas depending on commandline parameters. */ // not for any PCH effort, but instead for the (common) definitions // included there. #define MINIMAL_PCH 2 #include "lib/precompiled.h" #include #include "lib/debug.h" #include "lib/status.h" #include "lib/secure_crt.h" #include "lib/frequency_filter.h" #include "lib/input.h" #include "lib/ogl.h" #include "lib/timer.h" #include "lib/external_libraries/libsdl.h" #include "ps/ArchiveBuilder.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/Globals.h" #include "ps/Hotkey.h" #include "ps/Loader.h" #include "ps/Profile.h" #include "ps/Profiler2.h" #include "ps/Pyrogenesis.h" #include "ps/Replay.h" #include "ps/TouchInput.h" #include "ps/UserReport.h" #include "ps/Util.h" #include "ps/VideoMode.h" #include "ps/World.h" #include "ps/GameSetup/GameSetup.h" #include "ps/GameSetup/Atlas.h" #include "ps/GameSetup/Config.h" #include "ps/GameSetup/CmdLineArgs.h" #include "ps/GameSetup/Paths.h" #include "ps/XML/Xeromyces.h" #include "network/NetClient.h" #include "network/NetServer.h" #include "network/NetSession.h" #include "lobby/IXmppClient.h" #include "graphics/Camera.h" #include "graphics/GameView.h" #include "graphics/TextureManager.h" #include "gui/GUIManager.h" #include "renderer/Renderer.h" #include "scriptinterface/ScriptEngine.h" #include "simulation2/Simulation2.h" #include "simulation2/system/TurnManager.h" #if OS_UNIX #include // geteuid #endif // OS_UNIX #if MSC_VERSION #include #define getpid _getpid // Use the non-deprecated function name #endif extern bool g_GameRestarted; extern CStrW g_UniqueLogPostfix; void kill_mainloop(); // to avoid redundant and/or recursive resizing, we save the new // size after VIDEORESIZE messages and only update the video mode // once per frame. // these values are the latest resize message, and reset to 0 once we've // updated the video mode static int g_ResizedW; static int g_ResizedH; static std::chrono::high_resolution_clock::time_point lastFrameTime; // main app message handler static InReaction MainInputHandler(const SDL_Event_* ev) { switch(ev->ev.type) { case SDL_WINDOWEVENT: switch(ev->ev.window.event) { case SDL_WINDOWEVENT_ENTER: RenderCursor(true); break; case SDL_WINDOWEVENT_LEAVE: RenderCursor(false); break; case SDL_WINDOWEVENT_RESIZED: g_ResizedW = ev->ev.window.data1; g_ResizedH = ev->ev.window.data2; break; case SDL_WINDOWEVENT_MOVED: g_VideoMode.UpdatePosition(ev->ev.window.data1, ev->ev.window.data2); } break; case SDL_QUIT: kill_mainloop(); break; case SDL_HOTKEYDOWN: std::string hotkey = static_cast(ev->ev.user.data1); if (hotkey == "exit") { kill_mainloop(); return IN_HANDLED; } else if (hotkey == "screenshot") { WriteScreenshot(L".png"); return IN_HANDLED; } else if (hotkey == "bigscreenshot") { WriteBigScreenshot(L".bmp", 10); return IN_HANDLED; } else if (hotkey == "togglefullscreen") { g_VideoMode.ToggleFullscreen(); return IN_HANDLED; } else if (hotkey == "profile2.toggle") { g_Profiler2.Toggle(); return IN_HANDLED; } break; } return IN_PASS; } // dispatch all pending events to the various receivers. static void PumpEvents() { JSContext* cx = g_GUI->GetScriptInterface()->GetContext(); JSAutoRequest rq(cx); PROFILE3("dispatch events"); SDL_Event_ ev; while (in_poll_event(&ev)) { PROFILE2("event"); if (g_GUI) { JS::RootedValue tmpVal(cx); ScriptInterface::ToJSVal(cx, &tmpVal, ev); std::string data = g_GUI->GetScriptInterface()->StringifyJSON(&tmpVal); PROFILE2_ATTR("%s", data.c_str()); } in_dispatch_event(&ev); } g_TouchInput.Frame(); } /** * Optionally throttle the render frequency in order to * prevent 100% workload of the currently used CPU core. */ inline static void LimitFPS() { if (g_VSync) return; double fpsLimit = 0.0; CFG_GET_VAL(g_Game && g_Game->IsGameStarted() ? "adaptivefps.session" : "adaptivefps.menu", fpsLimit); // Keep in sync with options.json if (fpsLimit < 20.0 || fpsLimit >= 100.0) return; double wait = 1000.0 / fpsLimit - std::chrono::duration_cast( std::chrono::high_resolution_clock::now() - lastFrameTime).count() / 1000.0; if (wait > 0.0) SDL_Delay(wait); lastFrameTime = std::chrono::high_resolution_clock::now(); } static int ProgressiveLoad() { PROFILE3("progressive load"); wchar_t description[100]; int progress_percent; try { Status ret = LDR_ProgressiveLoad(10e-3, description, ARRAY_SIZE(description), &progress_percent); switch(ret) { // no load active => no-op (skip code below) case INFO::OK: return 0; // current task didn't complete. we only care about this insofar as the // load process is therefore not yet finished. case ERR::TIMED_OUT: break; // just finished loading case INFO::ALL_COMPLETE: g_Game->ReallyStartGame(); wcscpy_s(description, ARRAY_SIZE(description), L"Game is starting.."); // LDR_ProgressiveLoad returns L""; set to valid text to // avoid problems in converting to JSString break; // error! default: WARN_RETURN_STATUS_IF_ERR(ret); // can't do this above due to legit ERR::TIMED_OUT break; } } catch (PSERROR_Game_World_MapLoadFailed& e) { // Map loading failed // Call script function to do the actual work // (delete game data, switch GUI page, show error, etc.) CancelLoad(CStr(e.what()).FromUTF8()); } GUI_DisplayLoadProgress(progress_percent, description); return 0; } static void RendererIncrementalLoad() { PROFILE3("renderer incremental load"); const double maxTime = 0.1f; double startTime = timer_Time(); bool more; do { more = g_Renderer.GetTextureManager().MakeProgress(); } while (more && timer_Time() - startTime < maxTime); } static bool quit = false; // break out of main loop static void Frame() { g_Profiler2.RecordFrameStart(); PROFILE2("frame"); g_Profiler2.IncrementFrameNumber(); PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber()); ogl_WarnIfError(); // get elapsed time const double time = timer_Time(); g_frequencyFilter->Update(time); // .. old method - "exact" but contains jumps #if 0 static double last_time; const double time = timer_Time(); const float TimeSinceLastFrame = (float)(time-last_time); last_time = time; ONCE(return); // first call: set last_time and return // .. new method - filtered and more smooth, but errors may accumulate #else const float realTimeSinceLastFrame = 1.0 / g_frequencyFilter->SmoothedFrequency(); #endif ENSURE(realTimeSinceLastFrame > 0.0f); // decide if update/render is necessary bool need_render = !g_app_minimized; bool need_update = true; // If we are not running a multiplayer game, disable updates when the game is // minimized or out of focus and relinquish the CPU a bit, in order to make // debugging easier. if (g_PauseOnFocusLoss && !g_NetClient && !g_app_has_focus) { PROFILE3("non-focus delay"); need_update = false; // don't use SDL_WaitEvent: don't want the main loop to freeze until app focus is restored SDL_Delay(10); } // this scans for changed files/directories and reloads them, thus // allowing hotloading (changes are immediately assimilated in-game). ReloadChangedFiles(); ProgressiveLoad(); RendererIncrementalLoad(); PumpEvents(); // if the user quit by closing the window, the GL context will be broken and // may crash when we call Render() on some drivers, so leave this loop // before rendering if (quit) return; // respond to pumped resize events if (g_ResizedW || g_ResizedH) { g_VideoMode.ResizeWindow(g_ResizedW, g_ResizedH); g_ResizedW = g_ResizedH = 0; } if (g_NetClient) g_NetClient->Poll(); ogl_WarnIfError(); g_GUI->TickObjects(); ogl_WarnIfError(); if (g_Game && g_Game->IsGameStarted() && need_update) { g_Game->Update(realTimeSinceLastFrame); g_Game->GetView()->Update(float(realTimeSinceLastFrame)); } // Immediately flush any messages produced by simulation code if (g_NetClient) g_NetClient->Flush(); // Keep us connected to any XMPP servers if (g_XmppClient) g_XmppClient->recv(); g_UserReporter.Update(); g_Console->Update(realTimeSinceLastFrame); ogl_WarnIfError(); if (need_render) { Render(); PROFILE3("swap buffers"); SDL_GL_SwapWindow(g_VideoMode.GetWindow()); } ogl_WarnIfError(); g_Profiler.Frame(); g_GameRestarted = false; LimitFPS(); } static void NonVisualFrame() { g_Profiler2.RecordFrameStart(); PROFILE2("frame"); g_Profiler2.IncrementFrameNumber(); PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber()); static u32 turn = 0; debug_printf("Turn %u (%u)...\n", turn++, DEFAULT_TURN_LENGTH_SP); g_Game->GetSimulation2()->Update(DEFAULT_TURN_LENGTH_SP); g_Profiler.Frame(); if (g_Game->IsGameFinished()) kill_mainloop(); } static void MainControllerInit() { // add additional input handlers only needed by this controller: // must be registered after gui_handler. Should mayhap even be last. in_add_handler(MainInputHandler); } static void MainControllerShutdown() { in_reset_handlers(); } // stop the main loop and trigger orderly shutdown. called from several // places: the event handler (SDL_QUIT and hotkey) and JS exitProgram. void kill_mainloop() { quit = true; } static bool restart_in_atlas = false; // called by game code to indicate main() should restart in Atlas mode // instead of terminating void restart_mainloop_in_atlas() { quit = true; restart_in_atlas = true; } static bool restart = false; // trigger an orderly shutdown and restart the game. void restart_engine() { quit = true; restart = true; } extern CmdLineArgs g_args; // moved into a helper function to ensure args is destroyed before // exit(), which may result in a memory leak. static void RunGameOrAtlas(int argc, const char* argv[]) { CmdLineArgs args(argc, argv); g_args = args; if (args.Has("version")) { debug_printf("Pyrogenesis %s\n", engine_version); return; } if (args.Has("autostart-nonvisual") && args.Get("autostart").empty()) { LOGERROR("-autostart-nonvisual cant be used alone. A map with -autostart=\"TYPEDIR/MAPNAME\" is needed."); return; } if (args.Has("unique-logs")) g_UniqueLogPostfix = L"_" + std::to_wstring(std::time(nullptr)) + L"_" + std::to_wstring(getpid()); const bool isVisualReplay = args.Has("replay-visual"); const bool isNonVisualReplay = args.Has("replay"); const bool isNonVisual = args.Has("autostart-nonvisual"); const OsPath replayFile( isVisualReplay ? args.Get("replay-visual") : isNonVisualReplay ? args.Get("replay") : ""); if (isVisualReplay || isNonVisualReplay) { if (!FileExists(replayFile)) { debug_printf("ERROR: The requested replay file '%s' does not exist!\n", replayFile.string8().c_str()); return; } if (DirectoryExists(replayFile)) { debug_printf("ERROR: The requested replay file '%s' is a directory!\n", replayFile.string8().c_str()); return; } } // We need to initialize SpiderMonkey and libxml2 in the main thread before // any thread uses them. So initialize them here before we might run Atlas. ScriptEngine scriptEngine; CXeromyces::Startup(); if (ATLAS_RunIfOnCmdLine(args, false)) { CXeromyces::Terminate(); return; } if (isNonVisualReplay) { if (!args.Has("mod")) { LOGERROR("At least one mod should be specified! Did you mean to add the argument '-mod=public'?"); CXeromyces::Terminate(); return; } Paths paths(args); - g_VFS = CreateVfs(20 * MiB); + g_VFS = CreateVfs(); g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE); MountMods(paths, GetMods(args, INIT_MODS)); { CReplayPlayer replay; replay.Load(replayFile); replay.Replay( args.Has("serializationtest"), args.Has("rejointest") ? args.Get("rejointest").ToInt() : -1, args.Has("ooslog")); } g_VFS.reset(); CXeromyces::Terminate(); return; } // run in archive-building mode if requested if (args.Has("archivebuild")) { Paths paths(args); OsPath mod(args.Get("archivebuild")); OsPath zip; if (args.Has("archivebuild-output")) zip = args.Get("archivebuild-output"); else zip = mod.Filename().ChangeExtension(L".zip"); CArchiveBuilder builder(mod, paths.Cache()); // Add mods provided on the command line // NOTE: We do not handle mods in the user mod path here std::vector mods = args.GetMultiple("mod"); for (size_t i = 0; i < mods.size(); ++i) builder.AddBaseMod(paths.RData()/"mods"/mods[i]); builder.Build(zip, args.Has("archivebuild-compress")); CXeromyces::Terminate(); return; } const double res = timer_Resolution(); g_frequencyFilter = CreateFrequencyFilter(res, 30.0); // run the game int flags = INIT_MODS; do { restart = false; quit = false; if (!Init(args, flags)) { flags &= ~INIT_MODS; Shutdown(SHUTDOWN_FROM_CONFIG); continue; } if (isNonVisual) { InitNonVisual(args); while (!quit) NonVisualFrame(); } else { InitGraphics(args, 0); MainControllerInit(); while (!quit) Frame(); } Shutdown(0); MainControllerShutdown(); flags &= ~INIT_MODS; } while (restart); if (restart_in_atlas) ATLAS_RunIfOnCmdLine(args, true); CXeromyces::Terminate(); } #if OS_ANDROID // In Android we compile the engine as a shared library, not an executable, // so rename main() to a different symbol that the wrapper library can load #undef main #define main pyrogenesis_main extern "C" __attribute__((visibility ("default"))) int main(int argc, char* argv[]); #endif extern "C" int main(int argc, char* argv[]) { #if OS_UNIX // Don't allow people to run the game with root permissions, // because bad things can happen, check before we do anything if (geteuid() == 0) { std::cerr << "********************************************************\n" << "WARNING: Attempted to run the game with root permission!\n" << "This is not allowed because it can alter home directory \n" << "permissions and opens your system to vulnerabilities. \n" << "(You received this message because you were either \n" <<" logged in as root or used e.g. the 'sudo' command.) \n" << "********************************************************\n\n"; return EXIT_FAILURE; } #endif // OS_UNIX EarlyInit(); // must come at beginning of main RunGameOrAtlas(argc, const_cast(argv)); // Shut down profiler initialised by EarlyInit g_Profiler2.Shutdown(); return EXIT_SUCCESS; } Index: ps/trunk/source/network/tests/test_Net.h =================================================================== --- ps/trunk/source/network/tests/test_Net.h (revision 20638) +++ ps/trunk/source/network/tests/test_Net.h (revision 20639) @@ -1,342 +1,342 @@ /* Copyright (C) 2017 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 "graphics/TerrainTextureManager.h" #include "lib/external_libraries/enet.h" #include "lib/external_libraries/libsdl.h" #include "lib/tex/tex.h" #include "network/NetServer.h" #include "network/NetClient.h" #include "network/NetMessage.h" #include "network/NetMessages.h" #include "ps/CLogger.h" #include "ps/Game.h" #include "ps/Filesystem.h" #include "ps/Loader.h" #include "ps/XML/Xeromyces.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" #include "simulation2/system/TurnManager.h" class TestNetComms : public CxxTest::TestSuite { public: void setUp() { - g_VFS = CreateVfs(20 * MiB); + g_VFS = CreateVfs(); TS_ASSERT_OK(g_VFS->Mount(L"", DataDir()/"mods"/"public", VFS_MOUNT_MUST_EXIST)); TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir()/"_testcache")); CXeromyces::Startup(); // Need some stuff for terrain movement costs: // (TODO: this ought to be independent of any graphics code) new CTerrainTextureManager; g_TexMan.LoadTerrainTextures(); enet_initialize(); } void tearDown() { enet_deinitialize(); delete &g_TexMan; CXeromyces::Terminate(); g_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); } bool clients_are_all(const std::vector& clients, uint state) { for (size_t j = 0; j < clients.size(); ++j) if (clients[j]->GetCurrState() != state) return false; return true; } void connect(CNetServer& server, const std::vector& clients) { TS_ASSERT(server.SetupConnection(PS_DEFAULT_PORT)); for (size_t j = 0; j < clients.size(); ++j) TS_ASSERT(clients[j]->SetupConnection("127.0.0.1", PS_DEFAULT_PORT)); for (size_t i = 0; ; ++i) { // debug_printf("."); for (size_t j = 0; j < clients.size(); ++j) clients[j]->Poll(); if (clients_are_all(clients, NCS_PREGAME)) break; if (i > 20) { TS_FAIL("connection timeout"); break; } SDL_Delay(100); } } #if 0 void disconnect(CNetServer& server, const std::vector& clients) { for (size_t i = 0; ; ++i) { // debug_printf("."); server.Poll(); for (size_t j = 0; j < clients.size(); ++j) clients[j]->Poll(); if (server.GetState() == SERVER_STATE_UNCONNECTED && clients_are_all(clients, NCS_UNCONNECTED)) break; if (i > 20) { TS_FAIL("disconnection timeout"); break; } SDL_Delay(100); } } #endif void wait(const std::vector& clients, size_t msecs) { for (size_t i = 0; i < msecs/10; ++i) { for (size_t j = 0; j < clients.size(); ++j) clients[j]->Poll(); SDL_Delay(10); } } void test_basic_DISABLED() { // This doesn't actually test much, it just runs a very quick multiplayer game // and prints a load of debug output so you can see if anything funny's going on ScriptInterface scriptInterface("Engine", "Test", g_ScriptRuntime); JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); TestStdoutLogger logger; std::vector clients; CGame client1Game(true); CGame client2Game(true); CGame client3Game(true); CNetServer server; JS::RootedValue attrs(cx); scriptInterface.Eval("({mapType:'scenario',map:'maps/scenarios/Saharan Oases',mapPath:'maps/scenarios/',thing:'example'})", &attrs); server.UpdateGameAttributes(&attrs, scriptInterface); CNetClient client1(&client1Game, false); CNetClient client2(&client2Game, false); CNetClient client3(&client3Game, false); clients.push_back(&client1); clients.push_back(&client2); clients.push_back(&client3); connect(server, clients); debug_printf("%s", client1.TestReadGuiMessages().c_str()); server.StartGame(); SDL_Delay(100); for (size_t j = 0; j < clients.size(); ++j) { clients[j]->Poll(); TS_ASSERT_OK(LDR_NonprogressiveLoad()); clients[j]->LoadFinished(); } wait(clients, 100); { JS::RootedValue cmd(cx); client1.GetScriptInterface().Eval("({type:'debug-print', message:'[>>> client1 test sim command]\\n'})", &cmd); client1Game.GetTurnManager()->PostCommand(cmd); } { JS::RootedValue cmd(cx); client2.GetScriptInterface().Eval("({type:'debug-print', message:'[>>> client2 test sim command]\\n'})", &cmd); client2Game.GetTurnManager()->PostCommand(cmd); } wait(clients, 100); client1Game.GetTurnManager()->Update(1.0f, 1); client2Game.GetTurnManager()->Update(1.0f, 1); client3Game.GetTurnManager()->Update(1.0f, 1); wait(clients, 100); client1Game.GetTurnManager()->Update(1.0f, 1); client2Game.GetTurnManager()->Update(1.0f, 1); client3Game.GetTurnManager()->Update(1.0f, 1); wait(clients, 100); } void test_rejoin_DISABLED() { ScriptInterface scriptInterface("Engine", "Test", g_ScriptRuntime); JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); TestStdoutLogger logger; std::vector clients; CGame client1Game(true); CGame client2Game(true); CGame client3Game(true); CNetServer server; JS::RootedValue attrs(cx); scriptInterface.Eval("({mapType:'scenario',map:'maps/scenarios/Saharan Oases',mapPath:'maps/scenarios/',thing:'example'})", &attrs); server.UpdateGameAttributes(&attrs, scriptInterface); CNetClient client1(&client1Game, false); CNetClient client2(&client2Game, false); CNetClient client3(&client3Game, false); client1.SetUserName(L"alice"); client2.SetUserName(L"bob"); client3.SetUserName(L"charlie"); clients.push_back(&client1); clients.push_back(&client2); clients.push_back(&client3); connect(server, clients); debug_printf("%s", client1.TestReadGuiMessages().c_str()); server.StartGame(); SDL_Delay(100); for (size_t j = 0; j < clients.size(); ++j) { clients[j]->Poll(); TS_ASSERT_OK(LDR_NonprogressiveLoad()); clients[j]->LoadFinished(); } wait(clients, 100); { JS::RootedValue cmd(cx); client1.GetScriptInterface().Eval("({type:'debug-print', message:'[>>> client1 test sim command 1]\\n'})", &cmd); client1Game.GetTurnManager()->PostCommand(cmd); } wait(clients, 100); client1Game.GetTurnManager()->Update(1.0f, 1); client2Game.GetTurnManager()->Update(1.0f, 1); client3Game.GetTurnManager()->Update(1.0f, 1); wait(clients, 100); { JS::RootedValue cmd(cx); client1.GetScriptInterface().Eval("({type:'debug-print', message:'[>>> client1 test sim command 2]\\n'})", &cmd); client1Game.GetTurnManager()->PostCommand(cmd); } debug_printf("==== Disconnecting client 2\n"); client2.DestroyConnection(); clients.erase(clients.begin()+1); debug_printf("==== Connecting client 2B\n"); CGame client2BGame(true); CNetClient client2B(&client2BGame, false); client2B.SetUserName(L"bob"); clients.push_back(&client2B); TS_ASSERT(client2B.SetupConnection("127.0.0.1", PS_DEFAULT_PORT)); for (size_t i = 0; ; ++i) { debug_printf("[%u]\n", client2B.GetCurrState()); client2B.Poll(); if (client2B.GetCurrState() == NCS_PREGAME) break; if (client2B.GetCurrState() == NCS_UNCONNECTED) { TS_FAIL("connection rejected"); return; } if (i > 20) { TS_FAIL("connection timeout"); return; } SDL_Delay(100); } wait(clients, 100); client1Game.GetTurnManager()->Update(1.0f, 1); client3Game.GetTurnManager()->Update(1.0f, 1); wait(clients, 100); server.SetTurnLength(100); client1Game.GetTurnManager()->Update(1.0f, 1); client3Game.GetTurnManager()->Update(1.0f, 1); wait(clients, 100); // (This SetTurnLength thing doesn't actually detect errors unless you change // CTurnManager::TurnNeedsFullHash to always return true) { JS::RootedValue cmd(cx); client1.GetScriptInterface().Eval("({type:'debug-print', message:'[>>> client1 test sim command 3]\\n'})", &cmd); client1Game.GetTurnManager()->PostCommand(cmd); } clients[2]->Poll(); TS_ASSERT_OK(LDR_NonprogressiveLoad()); clients[2]->LoadFinished(); wait(clients, 100); { JS::RootedValue cmd(cx); client1.GetScriptInterface().Eval("({type:'debug-print', message:'[>>> client1 test sim command 4]\\n'})", &cmd); client1Game.GetTurnManager()->PostCommand(cmd); } for (size_t i = 0; i < 3; ++i) { client1Game.GetTurnManager()->Update(1.0f, 1); client2BGame.GetTurnManager()->Update(1.0f, 1); client3Game.GetTurnManager()->Update(1.0f, 1); wait(clients, 100); } } }; Index: ps/trunk/source/ps/ArchiveBuilder.cpp =================================================================== --- ps/trunk/source/ps/ArchiveBuilder.cpp (revision 20638) +++ ps/trunk/source/ps/ArchiveBuilder.cpp (revision 20639) @@ -1,176 +1,176 @@ -/* Copyright (C) 2016 Wildfire Games. +/* Copyright (C) 2017 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" #include CArchiveBuilder::CArchiveBuilder(const OsPath& mod, const OsPath& tempdir) : m_TempDir(tempdir), m_NumBaseMods(0) { - m_VFS = CreateVfs(20*MiB); + m_VFS = CreateVfs(); DeleteDirectory(m_TempDir/"_archivecache"); // clean up in case the last run failed m_VFS->Mount(L"cache/", m_TempDir/"_archivecache"/""); // Mount with highest priority so base mods do not overwrite files in this mod m_VFS->Mount(L"", mod/"", VFS_MOUNT_MUST_EXIST | VFS_MOUNT_KEEP_DELETED, (size_t)-1); // 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"); } void CArchiveBuilder::AddBaseMod(const OsPath& mod) { // Increase priority for each additional base mod, so that the // mods are mounted in the same way as when starting the game. m_VFS->Mount(L"", mod/"", VFS_MOUNT_MUST_EXIST, ++m_NumBaseMods); } void CArchiveBuilder::Build(const OsPath& archive, bool compress) { // By default we 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 = !compress; 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 textureManager(m_VFS, true, true); CColladaManager colladaManager(m_VFS); CXeromyces xero; for (const VfsPath& path : m_Files) { Status ret; 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/") || boost::algorithm::starts_with(path.string(), L"fonts/") ) && 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("Converting texture %s\n", realPath.string8().c_str()); 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; } // 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("Converting model %s\n", realPath.string8().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("Adding %s\n", realPath.string8().c_str()); writer->AddFile(realPath, path); // Also cache XMB versions of all XML files if (path.Extension() == L".xml") { VfsPath cachedPath; debug_printf("Converting XML file %s\n", realPath.string8().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 CFileInfo& 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/ps/GameSetup/GameSetup.cpp =================================================================== --- ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 20638) +++ ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 20639) @@ -1,1641 +1,1583 @@ /* Copyright (C) 2017 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 "lib/app_hooks.h" #include "lib/config2.h" #include "lib/input.h" #include "lib/ogl.h" #include "lib/timer.h" #include "lib/external_libraries/libsdl.h" #include "lib/file/common/file_stats.h" #include "lib/res/h_mgr.h" #include "lib/res/graphics/cursor.h" #include "lib/sysdep/cursor.h" -#include "lib/sysdep/cpu.h" -#include "lib/sysdep/gfx.h" -#include "lib/sysdep/os_cpu.h" -#include "lib/tex/tex.h" -#if OS_WIN -#include "lib/sysdep/os/win/wversion.h" -#endif #include "graphics/CinemaManager.h" #include "graphics/FontMetrics.h" #include "graphics/GameView.h" #include "graphics/LightEnv.h" #include "graphics/MapReader.h" #include "graphics/MaterialManager.h" #include "graphics/TerrainTextureManager.h" #include "gui/GUI.h" #include "gui/GUIManager.h" #include "gui/scripting/ScriptFunctions.h" #include "i18n/L10n.h" #include "maths/MathUtil.h" #include "network/NetServer.h" #include "network/NetClient.h" #include "network/NetMessage.h" #include "network/NetMessages.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/GameSetup/Atlas.h" #include "ps/GameSetup/GameSetup.h" #include "ps/GameSetup/Paths.h" #include "ps/GameSetup/Config.h" #include "ps/GameSetup/CmdLineArgs.h" #include "ps/GameSetup/HWDetect.h" #include "ps/Globals.h" #include "ps/Hotkey.h" #include "ps/Joystick.h" #include "ps/Loader.h" #include "ps/Mod.h" #include "ps/Profile.h" #include "ps/ProfileViewer.h" #include "ps/Profiler2.h" #include "ps/Pyrogenesis.h" // psSetLogDir #include "ps/scripting/JSInterface_Console.h" #include "ps/TouchInput.h" #include "ps/UserReport.h" #include "ps/Util.h" #include "ps/VideoMode.h" #include "ps/VisualReplay.h" #include "ps/World.h" #include "renderer/Renderer.h" #include "renderer/VertexBufferManager.h" #include "renderer/ModelRenderer.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/ScriptStats.h" #include "scriptinterface/ScriptConversions.h" #include "simulation2/Simulation2.h" #include "lobby/IXmppClient.h" #include "soundmanager/scripting/JSInterface_Sound.h" #include "soundmanager/ISoundManager.h" #include "tools/atlas/GameInterface/GameLoop.h" #include "tools/atlas/GameInterface/View.h" #if !(OS_WIN || OS_MACOSX || OS_ANDROID) // assume all other platforms use X11 for wxWidgets #define MUST_INIT_X11 1 #include #else #define MUST_INIT_X11 0 #endif extern void restart_engine(); #include #include #include ERROR_GROUP(System); ERROR_TYPE(System, SDLInitFailed); ERROR_TYPE(System, VmodeFailed); ERROR_TYPE(System, RequiredExtensionsMissing); bool g_DoRenderGui = true; bool g_DoRenderLogger = true; bool g_DoRenderCursor = true; shared_ptr g_ScriptRuntime; static const int SANE_TEX_QUALITY_DEFAULT = 5; // keep in sync with code bool g_InDevelopmentCopy; bool g_CheckedIfInDevelopmentCopy = false; static void SetTextureQuality(int quality) { int q_flags; GLint filter; retry: // keep this in sync with SANE_TEX_QUALITY_DEFAULT switch(quality) { // worst quality case 0: q_flags = OGL_TEX_HALF_RES|OGL_TEX_HALF_BPP; filter = GL_NEAREST; break; // [perf] add bilinear filtering case 1: q_flags = OGL_TEX_HALF_RES|OGL_TEX_HALF_BPP; filter = GL_LINEAR; break; // [vmem] no longer reduce resolution case 2: q_flags = OGL_TEX_HALF_BPP; filter = GL_LINEAR; break; // [vmem] add mipmaps case 3: q_flags = OGL_TEX_HALF_BPP; filter = GL_NEAREST_MIPMAP_LINEAR; break; // [perf] better filtering case 4: q_flags = OGL_TEX_HALF_BPP; filter = GL_LINEAR_MIPMAP_LINEAR; break; // [vmem] no longer reduce bpp case SANE_TEX_QUALITY_DEFAULT: q_flags = OGL_TEX_FULL_QUALITY; filter = GL_LINEAR_MIPMAP_LINEAR; break; // [perf] add anisotropy case 6: // TODO: add anisotropic filtering q_flags = OGL_TEX_FULL_QUALITY; filter = GL_LINEAR_MIPMAP_LINEAR; break; // invalid default: debug_warn(L"SetTextureQuality: invalid quality"); quality = SANE_TEX_QUALITY_DEFAULT; // careful: recursion doesn't work and we don't want to duplicate // the "sane" default values. goto retry; } ogl_tex_set_defaults(q_flags, filter); } //---------------------------------------------------------------------------- // GUI integration //---------------------------------------------------------------------------- // display progress / description in loading screen void GUI_DisplayLoadProgress(int percent, const wchar_t* pending_task) { g_GUI->GetActiveGUI()->GetScriptInterface()->SetGlobal("g_Progress", percent, true); g_GUI->GetActiveGUI()->GetScriptInterface()->SetGlobal("g_LoadDescription", pending_task, true); g_GUI->GetActiveGUI()->SendEventToAll("progress"); } void Render() { PROFILE3("render"); if (g_SoundManager) g_SoundManager->IdleTask(); ogl_WarnIfError(); g_Profiler2.RecordGPUFrameStart(); ogl_WarnIfError(); // prepare before starting the renderer frame if (g_Game && g_Game->IsGameStarted()) g_Game->GetView()->BeginFrame(); if (g_Game) g_Renderer.SetSimulation(g_Game->GetSimulation2()); // start new frame g_Renderer.BeginFrame(); ogl_WarnIfError(); if (g_Game && g_Game->IsGameStarted()) g_Game->GetView()->Render(); ogl_WarnIfError(); g_Renderer.RenderTextOverlays(); // If we're in Atlas game view, render special tools if (g_AtlasGameLoop && g_AtlasGameLoop->view) { g_AtlasGameLoop->view->DrawCinemaPathTool(); ogl_WarnIfError(); } if (g_Game && g_Game->IsGameStarted()) g_Game->GetView()->GetCinema()->Render(); ogl_WarnIfError(); if (g_DoRenderGui) g_GUI->Draw(); ogl_WarnIfError(); // If we're in Atlas game view, render special overlays (e.g. editor bandbox) if (g_AtlasGameLoop && g_AtlasGameLoop->view) { g_AtlasGameLoop->view->DrawOverlays(); ogl_WarnIfError(); } // Text: glDisable(GL_DEPTH_TEST); g_Console->Render(); ogl_WarnIfError(); if (g_DoRenderLogger) g_Logger->Render(); ogl_WarnIfError(); // Profile information g_ProfileViewer.RenderProfile(); ogl_WarnIfError(); // Draw the cursor (or set the Windows cursor, on Windows) if (g_DoRenderCursor) { PROFILE3_GPU("cursor"); CStrW cursorName = g_CursorName; if (cursorName.empty()) { cursor_draw(g_VFS, NULL, g_mouse_x, g_yres-g_mouse_y, g_GuiScale, false); } else { bool forceGL = false; CFG_GET_VAL("nohwcursor", forceGL); #if CONFIG2_GLES #warning TODO: implement cursors for GLES #else // set up transform for GL cursor glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glLoadIdentity(); CMatrix3D transform; transform.SetOrtho(0.f, (float)g_xres, 0.f, (float)g_yres, -1.f, 1000.f); glLoadMatrixf(&transform._11); #endif #if OS_ANDROID #warning TODO: cursors for Android #else if (cursor_draw(g_VFS, cursorName.c_str(), g_mouse_x, g_yres-g_mouse_y, g_GuiScale, forceGL) < 0) LOGWARNING("Failed to draw cursor '%s'", utf8_from_wstring(cursorName)); #endif #if CONFIG2_GLES #warning TODO: implement cursors for GLES #else // restore transform glMatrixMode(GL_PROJECTION); glPopMatrix(); glMatrixMode(GL_MODELVIEW); glPopMatrix(); #endif } } glEnable(GL_DEPTH_TEST); g_Renderer.EndFrame(); PROFILE2_ATTR("draw calls: %d", (int)g_Renderer.GetStats().m_DrawCalls); PROFILE2_ATTR("terrain tris: %d", (int)g_Renderer.GetStats().m_TerrainTris); PROFILE2_ATTR("water tris: %d", (int)g_Renderer.GetStats().m_WaterTris); PROFILE2_ATTR("model tris: %d", (int)g_Renderer.GetStats().m_ModelTris); PROFILE2_ATTR("overlay tris: %d", (int)g_Renderer.GetStats().m_OverlayTris); PROFILE2_ATTR("blend splats: %d", (int)g_Renderer.GetStats().m_BlendSplats); PROFILE2_ATTR("particles: %d", (int)g_Renderer.GetStats().m_Particles); ogl_WarnIfError(); g_Profiler2.RecordGPUFrameEnd(); ogl_WarnIfError(); } - -static size_t OperatingSystemFootprint() -{ -#if OS_WIN - switch(wversion_Number()) - { - case WVERSION_2K: - case WVERSION_XP: - return 150; - case WVERSION_XP64: - return 200; - default: // newer Windows version: assume the worst, and don't warn - case WVERSION_VISTA: - return 300; - case WVERSION_7: - return 250; - } -#else - return 200; -#endif -} - -static size_t ChooseCacheSize() -{ - // (all sizes in MiB and signed to allow temporarily negative computations) - - const ssize_t total = (ssize_t)os_cpu_MemorySize(); - // (NB: os_cpu_MemoryAvailable is useless on Linux because free memory - // is marked as "in use" by OS caches.) - const ssize_t os = (ssize_t)OperatingSystemFootprint(); - const ssize_t game = 300; // estimated working set - - ssize_t cache = 400; // upper bound: total size of our data - - // the cache reserves contiguous address space, which is a precious - // resource on 32-bit systems, so don't use too much: - if(ARCH_IA32 || sizeof(void*) == 4) - cache = std::min(cache, (ssize_t)200); - - // try to leave over enough memory for the OS and game - cache = std::min(cache, total-os-game); - - // always provide at least this much to ensure correct operation - cache = std::max(cache, (ssize_t)64); - - debug_printf("Cache: %d (total: %d) MiB\n", (int)cache, (int)total); - return size_t(cache)*MiB; -} - - ErrorReactionInternal psDisplayError(const wchar_t* UNUSED(text), size_t UNUSED(flags)) { // If we're fullscreen, then sometimes (at least on some particular drivers on Linux) // displaying the error dialog hangs the desktop since the dialog box is behind the // fullscreen window. So we just force the game to windowed mode before displaying the dialog. // (But only if we're in the main thread, and not if we're being reentrant.) if (ThreadUtil::IsMainThread()) { static bool reentering = false; if (!reentering) { reentering = true; g_VideoMode.SetFullscreen(false); reentering = false; } } // We don't actually implement the error display here, so return appropriately return ERI_NOT_IMPLEMENTED; } const std::vector& GetMods(const CmdLineArgs& args, int flags) { const bool init_mods = (flags & INIT_MODS) == INIT_MODS; const bool add_user = !InDevelopmentCopy() && !args.Has("noUserMod"); const bool add_public = (flags & INIT_MODS_PUBLIC) == INIT_MODS_PUBLIC; if (!init_mods) { // Add the user mod if it should be present if (add_user && (g_modsLoaded.empty() || g_modsLoaded.back() != "user")) g_modsLoaded.push_back("user"); return g_modsLoaded; } g_modsLoaded = args.GetMultiple("mod"); if (add_public) g_modsLoaded.insert(g_modsLoaded.begin(), "public"); g_modsLoaded.insert(g_modsLoaded.begin(), "mod"); // Add the user mod if not explicitly disabled or we have a dev copy so // that saved files end up in version control and not in the user mod. if (add_user) g_modsLoaded.push_back("user"); return g_modsLoaded; } void MountMods(const Paths& paths, const std::vector& mods) { OsPath modPath = paths.RData()/"mods"; OsPath modUserPath = paths.UserData()/"mods"; for (size_t i = 0; i < mods.size(); ++i) { size_t priority = (i+1)*2; // mods are higher priority than regular mountings, which default to priority 0 size_t userFlags = VFS_MOUNT_WATCH|VFS_MOUNT_ARCHIVABLE|VFS_MOUNT_REPLACEABLE; size_t baseFlags = userFlags|VFS_MOUNT_MUST_EXIST; OsPath modName(mods[i]); if (InDevelopmentCopy()) { // We are running a dev copy, so only mount mods in the user mod path // if the mod does not exist in the data path. if (DirectoryExists(modPath / modName/"")) g_VFS->Mount(L"", modPath / modName/"", baseFlags, priority); else g_VFS->Mount(L"", modUserPath / modName/"", userFlags, priority); } else { g_VFS->Mount(L"", modPath / modName/"", baseFlags, priority); // Ensure that user modified files are loaded, if they are present g_VFS->Mount(L"", modUserPath / modName/"", userFlags, priority+1); } } } static void InitVfs(const CmdLineArgs& args, int flags) { TIMER(L"InitVfs"); const bool setup_error = (flags & INIT_HAVE_DISPLAY_ERROR) == 0; const Paths paths(args); OsPath logs(paths.Logs()); CreateDirectories(logs, 0700); psSetLogDir(logs); // desired location for crashlog is now known. update AppHooks ASAP // (particularly before the following error-prone operations): AppHooks hooks = {0}; hooks.bundle_logs = psBundleLogs; hooks.get_log_dir = psLogDir; if (setup_error) hooks.display_error = psDisplayError; app_hooks_update(&hooks); - const size_t cacheSize = ChooseCacheSize(); - g_VFS = CreateVfs(cacheSize); + g_VFS = CreateVfs(); const OsPath readonlyConfig = paths.RData()/"config"/""; g_VFS->Mount(L"config/", readonlyConfig); // Engine localization files. g_VFS->Mount(L"l10n/", paths.RData()/"l10n"/""); MountMods(paths, GetMods(args, flags)); // We mount these dirs last as otherwise writing could result in files being placed in a mod's dir. g_VFS->Mount(L"screenshots/", paths.UserData()/"screenshots"/""); g_VFS->Mount(L"saves/", paths.UserData()/"saves"/"", VFS_MOUNT_WATCH); // Mounting with highest priority, so that a mod supplied user.cfg is harmless g_VFS->Mount(L"config/", readonlyConfig, 0, (size_t)-1); if(readonlyConfig != paths.Config()) g_VFS->Mount(L"config/", paths.Config(), 0, (size_t)-1); g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE); // (adding XMBs to archive speeds up subsequent reads) // note: don't bother with g_VFS->TextRepresentation - directories // haven't yet been populated and are empty. } static void InitPs(bool setup_gui, const CStrW& gui_page, ScriptInterface* srcScriptInterface, JS::HandleValue initData) { { // console TIMER(L"ps_console"); g_Console->UpdateScreenSize(g_xres, g_yres); // Calculate and store the line spacing CFontMetrics font(CStrIntern(CONSOLE_FONT)); g_Console->m_iFontHeight = font.GetLineSpacing(); g_Console->m_iFontWidth = font.GetCharacterWidth(L'C'); g_Console->m_charsPerPage = (size_t)(g_xres / g_Console->m_iFontWidth); // Offset by an arbitrary amount, to make it fit more nicely g_Console->m_iFontOffset = 7; double blinkRate = 0.5; CFG_GET_VAL("gui.cursorblinkrate", blinkRate); g_Console->SetCursorBlinkRate(blinkRate); } // hotkeys { TIMER(L"ps_lang_hotkeys"); LoadHotkeys(); } if (!setup_gui) { // We do actually need *some* kind of GUI loaded, so use the // (currently empty) Atlas one g_GUI->SwitchPage(L"page_atlas.xml", srcScriptInterface, initData); return; } // GUI uses VFS, so this must come after VFS init. g_GUI->SwitchPage(gui_page, srcScriptInterface, initData); } static void InitInput() { g_Joystick.Initialise(); // register input handlers // This stack is constructed so the first added, will be the last // one called. This is important, because each of the handlers // has the potential to block events to go further down // in the chain. I.e. the last one in the list added, is the // only handler that can block all messages before they are // processed. in_add_handler(game_view_handler); in_add_handler(CProfileViewer::InputThunk); in_add_handler(conInputHandler); in_add_handler(HotkeyInputHandler); // gui_handler needs to be registered after (i.e. called before!) the // hotkey handler so that input boxes can be typed in without // setting off hotkeys. in_add_handler(gui_handler); in_add_handler(touch_input_handler); // must be registered after (called before) the GUI which relies on these globals in_add_handler(GlobalsInputHandler); } static void ShutdownPs() { SAFE_DELETE(g_GUI); UnloadHotkeys(); // disable the special Windows cursor, or free textures for OGL cursors cursor_draw(g_VFS, 0, g_mouse_x, g_yres-g_mouse_y, 1.0, false); } static void InitRenderer() { TIMER(L"InitRenderer"); if(g_NoGLS3TC) ogl_tex_override(OGL_TEX_S3TC, OGL_TEX_DISABLE); if(g_NoGLAutoMipmap) ogl_tex_override(OGL_TEX_AUTO_MIPMAP_GEN, OGL_TEX_DISABLE); // create renderer new CRenderer; // set renderer options from command line options - NOVBO must be set before opening the renderer // and init them in the ConfigDB when needed g_Renderer.SetOptionBool(CRenderer::OPT_NOVBO, g_NoGLVBO); g_Renderer.SetOptionBool(CRenderer::OPT_SHADOWS, g_Shadows); g_ConfigDB.SetValueBool(CFG_SYSTEM, "shadows", g_Shadows); g_Renderer.SetOptionBool(CRenderer::OPT_WATEREFFECTS, g_WaterEffects); g_ConfigDB.SetValueBool(CFG_SYSTEM, "watereffects", g_WaterEffects); g_Renderer.SetOptionBool(CRenderer::OPT_WATERFANCYEFFECTS, g_WaterFancyEffects); g_ConfigDB.SetValueBool(CFG_SYSTEM, "waterfancyeffects", g_WaterFancyEffects); g_Renderer.SetOptionBool(CRenderer::OPT_WATERREALDEPTH, g_WaterRealDepth); g_ConfigDB.SetValueBool(CFG_SYSTEM, "waterrealdepth", g_WaterRealDepth); g_Renderer.SetOptionBool(CRenderer::OPT_WATERREFLECTION, g_WaterReflection); g_ConfigDB.SetValueBool(CFG_SYSTEM, "waterreflection", g_WaterReflection); g_Renderer.SetOptionBool(CRenderer::OPT_WATERREFRACTION, g_WaterRefraction); g_ConfigDB.SetValueBool(CFG_SYSTEM, "waterrefraction", g_WaterRefraction); g_Renderer.SetOptionBool(CRenderer::OPT_SHADOWSONWATER, g_WaterShadows); g_ConfigDB.SetValueBool(CFG_SYSTEM, "watershadows", g_WaterShadows); g_Renderer.SetRenderPath(CRenderer::GetRenderPathByName(g_RenderPath)); g_Renderer.SetOptionBool(CRenderer::OPT_SHADOWPCF, g_ShadowPCF); g_ConfigDB.SetValueBool(CFG_SYSTEM, "shadowpcf", g_ShadowPCF); g_Renderer.SetOptionBool(CRenderer::OPT_PARTICLES, g_Particles); g_ConfigDB.SetValueBool(CFG_SYSTEM, "particles", g_Particles); g_Renderer.SetOptionBool(CRenderer::OPT_FOG, g_Fog); g_ConfigDB.SetValueBool(CFG_SYSTEM, "fog", g_Fog); g_Renderer.SetOptionBool(CRenderer::OPT_SILHOUETTES, g_Silhouettes); g_ConfigDB.SetValueBool(CFG_SYSTEM, "silhouettes", g_Silhouettes); g_Renderer.SetOptionBool(CRenderer::OPT_SHOWSKY, g_ShowSky); g_ConfigDB.SetValueBool(CFG_SYSTEM, "showsky", g_ShowSky); g_Renderer.SetOptionBool(CRenderer::OPT_PREFERGLSL, g_PreferGLSL); g_ConfigDB.SetValueBool(CFG_SYSTEM, "preferglsl", g_PreferGLSL); g_Renderer.SetOptionBool(CRenderer::OPT_POSTPROC, g_PostProc); g_ConfigDB.SetValueBool(CFG_SYSTEM, "postproc", g_PostProc); g_Renderer.SetOptionBool(CRenderer::OPT_SMOOTHLOS, g_SmoothLOS); g_ConfigDB.SetValueBool(CFG_SYSTEM, "smoothlos", g_SmoothLOS); // create terrain related stuff new CTerrainTextureManager; g_Renderer.Open(g_xres, g_yres); // Setup lighting environment. Since the Renderer accesses the // lighting environment through a pointer, this has to be done before // the first Frame. g_Renderer.SetLightEnv(&g_LightEnv); // I haven't seen the camera affecting GUI rendering and such, but the // viewport has to be updated according to the video mode SViewPort vp; vp.m_X = 0; vp.m_Y = 0; vp.m_Width = g_xres; vp.m_Height = g_yres; g_Renderer.SetViewport(vp); ColorActivateFastImpl(); ModelRenderer::Init(); } static void InitSDL() { #if OS_LINUX // In fullscreen mode when SDL is compiled with DGA support, the mouse // sensitivity often appears to be unusably wrong (typically too low). // (This seems to be reported almost exclusively on Ubuntu, but can be // reproduced on Gentoo after explicitly enabling DGA.) // Disabling the DGA mouse appears to fix that problem, and doesn't // have any obvious negative effects. setenv("SDL_VIDEO_X11_DGAMOUSE", "0", 0); #endif if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER|SDL_INIT_NOPARACHUTE) < 0) { LOGERROR("SDL library initialization failed: %s", SDL_GetError()); throw PSERROR_System_SDLInitFailed(); } atexit(SDL_Quit); // Text input is active by default, disable it until it is actually needed. SDL_StopTextInput(); #if OS_MACOSX // Some Mac mice only have one button, so they can't right-click // but SDL2 can emulate that with Ctrl+Click bool macMouse = false; CFG_GET_VAL("macmouse", macMouse); SDL_SetHint(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, macMouse ? "1" : "0"); #endif } static void ShutdownSDL() { SDL_Quit(); sys_cursor_reset(); } void EndGame() { const bool nonVisual = g_Game && g_Game->IsGraphicsDisabled(); if (g_Game && g_Game->IsGameStarted() && !g_Game->IsVisualReplay() && g_AtlasGameLoop && !g_AtlasGameLoop->running && !nonVisual) VisualReplay::SaveReplayMetadata(g_GUI->GetActiveGUI()->GetScriptInterface().get()); SAFE_DELETE(g_NetClient); SAFE_DELETE(g_NetServer); SAFE_DELETE(g_Game); if (!nonVisual) { ISoundManager::CloseGame(); g_Renderer.ResetState(); } } void Shutdown(int flags) { const bool nonVisual = g_Game && g_Game->IsGraphicsDisabled(); if ((flags & SHUTDOWN_FROM_CONFIG)) goto from_config; EndGame(); SAFE_DELETE(g_XmppClient); ShutdownPs(); TIMER_BEGIN(L"shutdown TexMan"); delete &g_TexMan; TIMER_END(L"shutdown TexMan"); // destroy renderer if it was initialised if (!nonVisual) { TIMER_BEGIN(L"shutdown Renderer"); delete &g_Renderer; g_VBMan.Shutdown(); TIMER_END(L"shutdown Renderer"); } g_Profiler2.ShutdownGPU(); // Free cursors before shutting down SDL, as they may depend on SDL. cursor_shutdown(); TIMER_BEGIN(L"shutdown SDL"); ShutdownSDL(); TIMER_END(L"shutdown SDL"); if (!nonVisual) g_VideoMode.Shutdown(); TIMER_BEGIN(L"shutdown UserReporter"); g_UserReporter.Deinitialize(); TIMER_END(L"shutdown UserReporter"); delete &g_L10n; from_config: TIMER_BEGIN(L"shutdown ConfigDB"); delete &g_ConfigDB; TIMER_END(L"shutdown ConfigDB"); SAFE_DELETE(g_Console); // This is needed to ensure that no callbacks from the JSAPI try to use // the profiler when it's already destructed g_ScriptRuntime.reset(); // resource // first shut down all resource owners, and then the handle manager. TIMER_BEGIN(L"resource modules"); ISoundManager::SetEnabled(false); g_VFS.reset(); // this forcibly frees all open handles (thus preventing real leaks), // and makes further access to h_mgr impossible. h_mgr_shutdown(); file_stats_dump(); TIMER_END(L"resource modules"); TIMER_BEGIN(L"shutdown misc"); timer_DisplayClientTotals(); CNetHost::Deinitialize(); // should be last, since the above use them SAFE_DELETE(g_Logger); delete &g_Profiler; delete &g_ProfileViewer; SAFE_DELETE(g_ScriptStatsTable); TIMER_END(L"shutdown misc"); } #if OS_UNIX static void FixLocales() { #if OS_MACOSX || OS_BSD // OS X requires a UTF-8 locale in LC_CTYPE so that *wprintf can handle // wide characters. Peculiarly the string "UTF-8" seems to be acceptable // despite not being a real locale, and it's conveniently language-agnostic, // so use that. setlocale(LC_CTYPE, "UTF-8"); #endif // On misconfigured systems with incorrect locale settings, we'll die // with a C++ exception when some code (e.g. Boost) tries to use locales. // To avoid death, we'll detect the problem here and warn the user and // reset to the default C locale. // For informing the user of the problem, use the list of env vars that // glibc setlocale looks at. (LC_ALL is checked first, and LANG last.) const char* const LocaleEnvVars[] = { "LC_ALL", "LC_COLLATE", "LC_CTYPE", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", "LC_MESSAGES", "LANG" }; try { // this constructor is similar to setlocale(LC_ALL, ""), // but instead of returning NULL, it throws runtime_error // when the first locale env variable found contains an invalid value std::locale(""); } catch (std::runtime_error&) { LOGWARNING("Invalid locale settings"); for (size_t i = 0; i < ARRAY_SIZE(LocaleEnvVars); i++) { if (char* envval = getenv(LocaleEnvVars[i])) LOGWARNING(" %s=\"%s\"", LocaleEnvVars[i], envval); else LOGWARNING(" %s=\"(unset)\"", LocaleEnvVars[i]); } // We should set LC_ALL since it overrides LANG if (setenv("LC_ALL", std::locale::classic().name().c_str(), 1)) debug_warn(L"Invalid locale settings, and unable to set LC_ALL env variable."); else LOGWARNING("Setting LC_ALL env variable to: %s", getenv("LC_ALL")); } } #else static void FixLocales() { // Do nothing on Windows } #endif void EarlyInit() { // If you ever want to catch a particular allocation: //_CrtSetBreakAlloc(232647); ThreadUtil::SetMainThread(); debug_SetThreadName("main"); // add all debug_printf "tags" that we are interested in: debug_filter_add("TIMER"); timer_LatchStartTime(); // initialise profiler early so it can profile startup, // but only after LatchStartTime g_Profiler2.Initialise(); FixLocales(); // Because we do GL calls from a secondary thread, Xlib needs to // be told to support multiple threads safely. // This is needed for Atlas, but we have to call it before any other // Xlib functions (e.g. the ones used when drawing the main menu // before launching Atlas) #if MUST_INIT_X11 int status = XInitThreads(); if (status == 0) debug_printf("Error enabling thread-safety via XInitThreads\n"); #endif // Initialise the low-quality rand function srand(time(NULL)); // NOTE: this rand should *not* be used for simulation! } bool Autostart(const CmdLineArgs& args); /** * Returns true if the user has intended to start a visual replay from command line. */ bool AutostartVisualReplay(const std::string& replayFile); bool Init(const CmdLineArgs& args, int flags) { h_mgr_init(); // Do this as soon as possible, because it chdirs // and will mess up the error reporting if anything // crashes before the working directory is set. InitVfs(args, flags); // This must come after VFS init, which sets the current directory // (required for finding our output log files). g_Logger = new CLogger; new CProfileViewer; new CProfileManager; // before any script code g_ScriptStatsTable = new CScriptStatsTable; g_ProfileViewer.AddRootTable(g_ScriptStatsTable); // Set up the console early, so that debugging // messages can be logged to it. (The console's size // and fonts are set later in InitPs()) g_Console = new CConsole(); // g_ConfigDB, command line args, globals CONFIG_Init(args); // Using a global object for the runtime is a workaround until Simulation and AI use // their own threads and also their own runtimes. const int runtimeSize = 384 * 1024 * 1024; const int heapGrowthBytesGCTrigger = 20 * 1024 * 1024; g_ScriptRuntime = ScriptInterface::CreateRuntime(shared_ptr(), runtimeSize, heapGrowthBytesGCTrigger); // Special command-line mode to dump the entity schemas instead of running the game. // (This must be done after loading VFS etc, but should be done before wasting time // on anything else.) if (args.Has("dumpSchema")) { CSimulation2 sim(NULL, g_ScriptRuntime, NULL); sim.LoadDefaultScripts(); std::ofstream f("entity.rng", std::ios_base::out | std::ios_base::trunc); f << sim.GenerateSchema(); std::cout << "Generated entity.rng\n"; exit(0); } CNetHost::Initialize(); #if CONFIG2_AUDIO if (!args.Has("autostart-nonvisual")) ISoundManager::CreateSoundManager(); #endif // Check if there are mods specified on the command line, // or if we already set the mods (~INIT_MODS), // else check if there are mods that should be loaded specified // in the config and load those (by aborting init and restarting // the engine). if (!args.Has("mod") && (flags & INIT_MODS) == INIT_MODS) { CStr modstring; CFG_GET_VAL("mod.enabledmods", modstring); if (!modstring.empty()) { std::vector mods; boost::split(mods, modstring, boost::is_any_of(" "), boost::token_compress_on); std::swap(g_modsLoaded, mods); // Abort init and restart restart_engine(); return false; } } new L10n; // Optionally start profiler HTTP output automatically // (By default it's only enabled by a hotkey, for security/performance) bool profilerHTTPEnable = false; CFG_GET_VAL("profiler2.autoenable", profilerHTTPEnable); if (profilerHTTPEnable) g_Profiler2.EnableHTTP(); if (!g_Quickstart) g_UserReporter.Initialize(); // after config PROFILE2_EVENT("Init finished"); return true; } void InitGraphics(const CmdLineArgs& args, int flags) { const bool setup_vmode = (flags & INIT_HAVE_VMODE) == 0; if(setup_vmode) { InitSDL(); if (!g_VideoMode.InitSDL()) throw PSERROR_System_VmodeFailed(); // abort startup } RunHardwareDetection(); const int quality = SANE_TEX_QUALITY_DEFAULT; // TODO: set value from config file SetTextureQuality(quality); ogl_WarnIfError(); // Optionally start profiler GPU timings automatically // (By default it's only enabled by a hotkey, for performance/compatibility) bool profilerGPUEnable = false; CFG_GET_VAL("profiler2.autoenable", profilerGPUEnable); if (profilerGPUEnable) g_Profiler2.EnableGPU(); if(!g_Quickstart) { WriteSystemInfo(); // note: no longer vfs_display here. it's dog-slow due to unbuffered // file output and very rarely needed. } if(g_DisableAudio) ISoundManager::SetEnabled(false); g_GUI = new CGUIManager(); // (must come after SetVideoMode, since it calls ogl_Init) if (ogl_HaveExtensions(0, "GL_ARB_vertex_program", "GL_ARB_fragment_program", NULL) != 0 // ARB && ogl_HaveExtensions(0, "GL_ARB_vertex_shader", "GL_ARB_fragment_shader", NULL) != 0) // GLSL { DEBUG_DISPLAY_ERROR( L"Your graphics card doesn't appear to be fully compatible with OpenGL shaders." L" In the future, the game will not support pre-shader graphics cards." L" You are advised to try installing newer drivers and/or upgrade your graphics card." L" For more information, please see http://www.wildfiregames.com/forum/index.php?showtopic=16734" ); // TODO: actually quit once fixed function support is dropped } const char* missing = ogl_HaveExtensions(0, "GL_ARB_multitexture", "GL_EXT_draw_range_elements", "GL_ARB_texture_env_combine", "GL_ARB_texture_env_dot3", NULL); if(missing) { wchar_t buf[500]; swprintf_s(buf, ARRAY_SIZE(buf), L"The %hs extension doesn't appear to be available on your computer." L" The game may still work, though - you are welcome to try at your own risk." L" If not or it doesn't look right, upgrade your graphics card.", missing ); DEBUG_DISPLAY_ERROR(buf); // TODO: i18n } if (!ogl_HaveExtension("GL_ARB_texture_env_crossbar")) { DEBUG_DISPLAY_ERROR( L"The GL_ARB_texture_env_crossbar extension doesn't appear to be available on your computer." L" Shadows are not available and overall graphics quality might suffer." L" You are advised to try installing newer drivers and/or upgrade your graphics card."); g_Shadows = false; } ogl_WarnIfError(); InitRenderer(); InitInput(); ogl_WarnIfError(); // TODO: Is this the best place for this? if (VfsDirectoryExists(L"maps/")) CXeromyces::AddValidator(g_VFS, "map", "maps/scenario.rng"); try { if (!AutostartVisualReplay(args.Get("replay-visual")) && !Autostart(args)) { const bool setup_gui = ((flags & INIT_NO_GUI) == 0); // We only want to display the splash screen at startup shared_ptr scriptInterface = g_GUI->GetScriptInterface(); JSContext* cx = scriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedValue data(cx); if (g_GUI) { scriptInterface->Eval("({})", &data); scriptInterface->SetProperty(data, "isStartup", true); } InitPs(setup_gui, L"page_pregame.xml", g_GUI->GetScriptInterface().get(), data); } } catch (PSERROR_Game_World_MapLoadFailed& e) { // Map Loading failed // Start the engine so we have a GUI InitPs(true, L"page_pregame.xml", NULL, JS::UndefinedHandleValue); // Call script function to do the actual work // (delete game data, switch GUI page, show error, etc.) CancelLoad(CStr(e.what()).FromUTF8()); } } void InitNonVisual(const CmdLineArgs& args) { // Need some stuff for terrain movement costs: // (TODO: this ought to be independent of any graphics code) new CTerrainTextureManager; g_TexMan.LoadTerrainTextures(); Autostart(args); } void RenderGui(bool RenderingState) { g_DoRenderGui = RenderingState; } void RenderLogger(bool RenderingState) { g_DoRenderLogger = RenderingState; } void RenderCursor(bool RenderingState) { g_DoRenderCursor = RenderingState; } /** * Temporarily loads a scenario map and retrieves the "ScriptSettings" JSON * data from it. * The scenario map format is used for scenario and skirmish map types (random * games do not use a "map" (format) but a small JavaScript program which * creates a map on the fly). It contains a section to initialize the game * setup screen. * @param mapPath Absolute path (from VFS root) to the map file to peek in. * @return ScriptSettings in JSON format extracted from the map. */ CStr8 LoadSettingsOfScenarioMap(const VfsPath &mapPath) { CXeromyces mapFile; const char *pathToSettings[] = { "Scenario", "ScriptSettings", "" // Path to JSON data in map }; Status loadResult = mapFile.Load(g_VFS, mapPath); if (INFO::OK != loadResult) { LOGERROR("LoadSettingsOfScenarioMap: Unable to load map file '%s'", mapPath.string8()); throw PSERROR_Game_World_MapLoadFailed("Unable to load map file, check the path for typos."); } XMBElement mapElement = mapFile.GetRoot(); // Select the ScriptSettings node in the map file... for (int i = 0; pathToSettings[i][0]; ++i) { int childId = mapFile.GetElementID(pathToSettings[i]); XMBElementList nodes = mapElement.GetChildNodes(); auto it = std::find_if(nodes.begin(), nodes.end(), [&childId](const XMBElement& child) { return child.GetNodeName() == childId; }); if (it != nodes.end()) mapElement = *it; } // ... they contain a JSON document to initialize the game setup // screen return mapElement.GetText(); } /* * Command line options for autostart * (keep synchronized with binaries/system/readme.txt): * * -autostart="TYPEDIR/MAPNAME" enables autostart and sets MAPNAME; * TYPEDIR is skirmishes, scenarios, or random * -autostart-seed=SEED sets randomization seed value (default 0, use -1 for random) * -autostart-ai=PLAYER:AI sets the AI for PLAYER (e.g. 2:petra) * -autostart-aidiff=PLAYER:DIFF sets the DIFFiculty of PLAYER's AI * (0: sandbox, 5: very hard) * -autostart-aiseed=AISEED sets the seed used for the AI random * generator (default 0, use -1 for random) * -autostart-civ=PLAYER:CIV sets PLAYER's civilisation to CIV * (skirmish and random maps only) * -autostart-team=PLAYER:TEAM sets the team for PLAYER (e.g. 2:2). * -autostart-nonvisual disable any graphics and sounds * -autostart-victory=SCRIPTNAME sets the victory conditions with SCRIPTNAME * located in simulation/data/settings/victory_conditions/ * -autostart-victoryduration=NUM sets the victory duration NUM for specific victory conditions * * Multiplayer: * -autostart-playername=NAME sets local player NAME (default 'anonymous') * -autostart-host sets multiplayer host mode * -autostart-host-players=NUMBER sets NUMBER of human players for multiplayer * game (default 2) * -autostart-client=IP sets multiplayer client to join host at * given IP address * Random maps only: * -autostart-size=TILES sets random map size in TILES (default 192) * -autostart-players=NUMBER sets NUMBER of players on random map * (default 2) * * Examples: * 1) "Bob" will host a 2 player game on the Arcadia map: * -autostart="scenarios/Arcadia" -autostart-host -autostart-host-players=2 -autostart-playername="Bob" * * 2) Load Alpine Lakes random map with random seed, 2 players (Athens and Britons), and player 2 is PetraBot: * -autostart="random/alpine_lakes" -autostart-seed=-1 -autostart-players=2 -autostart-civ=1:athen -autostart-civ=2:brit -autostart-ai=2:petra */ bool Autostart(const CmdLineArgs& args) { CStr autoStartName = args.Get("autostart"); if (autoStartName.empty()) return false; const bool nonVisual = args.Has("autostart-nonvisual"); g_Game = new CGame(nonVisual, !nonVisual); ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); JS::RootedValue attrs(cx); scriptInterface.Eval("({})", &attrs); JS::RootedValue settings(cx); scriptInterface.Eval("({})", &settings); JS::RootedValue playerData(cx); scriptInterface.Eval("([])", &playerData); // The directory in front of the actual map name indicates which type // of map is being loaded. Drawback of this approach is the association // of map types and folders is hard-coded, but benefits are: // - No need to pass the map type via command line separately // - Prevents mixing up of scenarios and skirmish maps to some degree Path mapPath = Path(autoStartName); std::wstring mapDirectory = mapPath.Parent().Filename().string(); std::string mapType; if (mapDirectory == L"random") { // Random map definition will be loaded from JSON file, so we need to parse it std::wstring scriptPath = L"maps/" + autoStartName.FromUTF8() + L".json"; JS::RootedValue scriptData(cx); scriptInterface.ReadJSONFile(scriptPath, &scriptData); if (!scriptData.isUndefined() && scriptInterface.GetProperty(scriptData, "settings", &settings)) { // JSON loaded ok - copy script name over to game attributes std::wstring scriptFile; scriptInterface.GetProperty(settings, "Script", scriptFile); scriptInterface.SetProperty(attrs, "script", scriptFile); // RMS filename } else { // Problem with JSON file LOGERROR("Autostart: Error reading random map script '%s'", utf8_from_wstring(scriptPath)); throw PSERROR_Game_World_MapLoadFailed("Error reading random map script.\nCheck application log for details."); } // Get optional map size argument (default 192) uint mapSize = 192; if (args.Has("autostart-size")) { CStr size = args.Get("autostart-size"); mapSize = size.ToUInt(); } scriptInterface.SetProperty(settings, "Size", mapSize); // Random map size (in patches) // Get optional number of players (default 2) size_t numPlayers = 2; if (args.Has("autostart-players")) { CStr num = args.Get("autostart-players"); numPlayers = num.ToUInt(); } // Set up player data for (size_t i = 0; i < numPlayers; ++i) { JS::RootedValue player(cx); scriptInterface.Eval("({})", &player); // We could load player_defaults.json here, but that would complicate the logic // even more and autostart is only intended for developers anyway scriptInterface.SetProperty(player, "Civ", std::string("athen")); scriptInterface.SetPropertyInt(playerData, i, player); } mapType = "random"; } else if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes") { // Initialize general settings from the map data so some values // (e.g. name of map) are always present, even when autostart is // partially configured CStr8 mapSettingsJSON = LoadSettingsOfScenarioMap("maps/" + autoStartName + ".xml"); scriptInterface.ParseJSON(mapSettingsJSON, &settings); // Initialize the playerData array being modified by autostart // with the real map data, so sensible values are present: scriptInterface.GetProperty(settings, "PlayerData", &playerData); if (mapDirectory == L"scenarios") mapType = "scenario"; else mapType = "skirmish"; } else { LOGERROR("Autostart: Unrecognized map type '%s'", utf8_from_wstring(mapDirectory)); throw PSERROR_Game_World_MapLoadFailed("Unrecognized map type.\nConsult readme.txt for the currently supported types."); } scriptInterface.SetProperty(attrs, "mapType", mapType); scriptInterface.SetProperty(attrs, "map", std::string("maps/" + autoStartName)); scriptInterface.SetProperty(settings, "mapType", mapType); scriptInterface.SetProperty(settings, "CheatsEnabled", true); // The seed is used for both random map generation and simulation u32 seed = 0; if (args.Has("autostart-seed")) { CStr seedArg = args.Get("autostart-seed"); if (seedArg == "-1") seed = rand(); else seed = seedArg.ToULong(); } scriptInterface.SetProperty(settings, "Seed", seed); // Set seed for AIs u32 aiseed = 0; if (args.Has("autostart-aiseed")) { CStr seedArg = args.Get("autostart-aiseed"); if (seedArg == "-1") aiseed = rand(); else aiseed = seedArg.ToULong(); } scriptInterface.SetProperty(settings, "AISeed", aiseed); // Set player data for AIs // attrs.settings = { PlayerData: [ { AI: ... }, ... ] } // or = { PlayerData: [ null, { AI: ... }, ... ] } when gaia set int offset = 1; JS::RootedValue player(cx); if (scriptInterface.GetPropertyInt(playerData, 0, &player) && player.isNull()) offset = 0; // Set teams if (args.Has("autostart-team")) { std::vector civArgs = args.GetMultiple("autostart-team"); for (size_t i = 0; i < civArgs.size(); ++i) { int playerID = civArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue player(cx); if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, &player) || player.isUndefined()) { if (mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING("Autostart: Invalid player %d in autostart-team option", playerID); continue; } scriptInterface.Eval("({})", &player); } int teamID = civArgs[i].AfterFirst(":").ToInt() - 1; scriptInterface.SetProperty(player, "Team", teamID); scriptInterface.SetPropertyInt(playerData, playerID-offset, player); } } if (args.Has("autostart-ai")) { std::vector aiArgs = args.GetMultiple("autostart-ai"); for (size_t i = 0; i < aiArgs.size(); ++i) { int playerID = aiArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue player(cx); if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, &player) || player.isUndefined()) { if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING("Autostart: Invalid player %d in autostart-ai option", playerID); continue; } scriptInterface.Eval("({})", &player); } CStr name = aiArgs[i].AfterFirst(":"); scriptInterface.SetProperty(player, "AI", std::string(name)); scriptInterface.SetProperty(player, "AIDiff", 3); scriptInterface.SetPropertyInt(playerData, playerID-offset, player); } } // Set AI difficulty if (args.Has("autostart-aidiff")) { std::vector civArgs = args.GetMultiple("autostart-aidiff"); for (size_t i = 0; i < civArgs.size(); ++i) { int playerID = civArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue player(cx); if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, &player) || player.isUndefined()) { if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING("Autostart: Invalid player %d in autostart-aidiff option", playerID); continue; } scriptInterface.Eval("({})", &player); } int difficulty = civArgs[i].AfterFirst(":").ToInt(); scriptInterface.SetProperty(player, "AIDiff", difficulty); scriptInterface.SetPropertyInt(playerData, playerID-offset, player); } } // Set player data for Civs if (args.Has("autostart-civ")) { if (mapDirectory != L"scenarios") { std::vector civArgs = args.GetMultiple("autostart-civ"); for (size_t i = 0; i < civArgs.size(); ++i) { int playerID = civArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue player(cx); if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, &player) || player.isUndefined()) { if (mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING("Autostart: Invalid player %d in autostart-civ option", playerID); continue; } scriptInterface.Eval("({})", &player); } CStr name = civArgs[i].AfterFirst(":"); scriptInterface.SetProperty(player, "Civ", std::string(name)); scriptInterface.SetPropertyInt(playerData, playerID-offset, player); } } else LOGWARNING("Autostart: Option 'autostart-civ' is invalid for scenarios"); } // Add player data to map settings scriptInterface.SetProperty(settings, "PlayerData", playerData); // Add map settings to game attributes scriptInterface.SetProperty(attrs, "settings", settings); JS::RootedValue mpInitData(cx); scriptInterface.Eval("({isNetworked:true, playerAssignments:{}})", &mpInitData); scriptInterface.SetProperty(mpInitData, "attribs", attrs); // Get optional playername CStrW userName = L"anonymous"; if (args.Has("autostart-playername")) userName = args.Get("autostart-playername").FromUTF8(); // Add additional scripts to the TriggerScripts property std::vector triggerScriptsVector; JS::RootedValue triggerScripts(cx); if (scriptInterface.HasProperty(settings, "TriggerScripts")) { scriptInterface.GetProperty(settings, "TriggerScripts", &triggerScripts); FromJSVal_vector(cx, triggerScripts, triggerScriptsVector); } if (nonVisual) { CStr nonVisualScript = "scripts/NonVisualTrigger.js"; triggerScriptsVector.push_back(nonVisualScript.FromUTF8()); } if (args.Has("autostart-victory")) { CStrW scriptName = args.Get("autostart-victory").FromUTF8(); CStrW scriptPath = L"simulation/data/settings/victory_conditions/" + scriptName + L".json"; JS::RootedValue scriptData(cx); JS::RootedValue data(cx); JS::RootedValue victoryScripts(cx); scriptInterface.ReadJSONFile(scriptPath, &scriptData); if (!scriptData.isUndefined() && scriptInterface.GetProperty(scriptData, "Data", &data) && !data.isUndefined() && scriptInterface.GetProperty(data, "Scripts", &victoryScripts) && !victoryScripts.isUndefined()) { std::vector victoryScriptsVector; FromJSVal_vector(cx, victoryScripts, victoryScriptsVector); triggerScriptsVector.insert(triggerScriptsVector.end(), victoryScriptsVector.begin(), victoryScriptsVector.end()); } } ToJSVal_vector(cx, &triggerScripts, triggerScriptsVector); scriptInterface.SetProperty(settings, "TriggerScripts", triggerScripts); if (args.Has("autostart-victoryduration")) scriptInterface.SetProperty(settings, "VictoryDuration", args.Get("autostart-victoryduration").ToInt()); if (args.Has("autostart-host")) { InitPs(true, L"page_loading.xml", &scriptInterface, mpInitData); size_t maxPlayers = 2; if (args.Has("autostart-host-players")) maxPlayers = args.Get("autostart-host-players").ToUInt(); g_NetServer = new CNetServer(maxPlayers); g_NetServer->UpdateGameAttributes(&attrs, scriptInterface); bool ok = g_NetServer->SetupConnection(PS_DEFAULT_PORT); ENSURE(ok); g_NetClient = new CNetClient(g_Game, true); g_NetClient->SetUserName(userName); g_NetClient->SetupConnection("127.0.0.1", PS_DEFAULT_PORT); } else if (args.Has("autostart-client")) { InitPs(true, L"page_loading.xml", &scriptInterface, mpInitData); g_NetClient = new CNetClient(g_Game, false); g_NetClient->SetUserName(userName); CStr ip = args.Get("autostart-client"); if (ip.empty()) ip = "127.0.0.1"; bool ok = g_NetClient->SetupConnection(ip, PS_DEFAULT_PORT); ENSURE(ok); } else { g_Game->SetPlayerID(1); g_Game->StartGame(&attrs, ""); LDR_NonprogressiveLoad(); PSRETURN ret = g_Game->ReallyStartGame(); ENSURE(ret == PSRETURN_OK); if (nonVisual) return true; InitPs(true, L"page_session.xml", NULL, JS::UndefinedHandleValue); } return true; } bool AutostartVisualReplay(const std::string& replayFile) { if (!FileExists(OsPath(replayFile))) return false; g_Game = new CGame(false, false); g_Game->SetPlayerID(-1); g_Game->StartVisualReplay(replayFile); // TODO: Non progressive load can fail - need a decent way to handle this LDR_NonprogressiveLoad(); ENSURE(g_Game->ReallyStartGame() == PSRETURN_OK); ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); InitPs(true, L"page_session.xml", &scriptInterface, JS::UndefinedHandleValue); return true; } void CancelLoad(const CStrW& message) { shared_ptr pScriptInterface = g_GUI->GetActiveGUI()->GetScriptInterface(); JSContext* cx = pScriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedValue global(cx, pScriptInterface->GetGlobalObject()); LDR_Cancel(); if (g_GUI && g_GUI->HasPages() && pScriptInterface->HasProperty(global, "cancelOnLoadGameError")) pScriptInterface->CallFunctionVoid(global, "cancelOnLoadGameError", message); } bool InDevelopmentCopy() { if (!g_CheckedIfInDevelopmentCopy) { g_InDevelopmentCopy = (g_VFS->GetFileInfo(L"config/dev.cfg", NULL) == INFO::OK); g_CheckedIfInDevelopmentCopy = true; } return g_InDevelopmentCopy; } Index: ps/trunk/source/ps/Mod.cpp =================================================================== --- ps/trunk/source/ps/Mod.cpp (revision 20638) +++ ps/trunk/source/ps/Mod.cpp (revision 20639) @@ -1,101 +1,101 @@ /* Copyright (C) 2017 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 "ps/Mod.h" #include #include "lib/file/file_system.h" #include "lib/file/vfs/vfs.h" #include "lib/utf8.h" #include "ps/Filesystem.h" #include "ps/GameSetup/GameSetup.h" #include "ps/GameSetup/Paths.h" std::vector g_modsLoaded; CmdLineArgs g_args; JS::Value Mod::GetAvailableMods(const ScriptInterface& scriptInterface) { JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); JS::RootedObject obj(cx, JS_NewPlainObject(cx)); const Paths paths(g_args); // loop over all possible paths OsPath modPath = paths.RData()/"mods"; OsPath modUserPath = paths.UserData()/"mods"; DirectoryNames modDirs; DirectoryNames modDirsUser; GetDirectoryEntries(modPath, NULL, &modDirs); // Sort modDirs so that we can do a fast lookup below std::sort(modDirs.begin(), modDirs.end()); - PIVFS vfs = CreateVfs(1); // No cache needed; TODO but 0 crashes + PIVFS vfs = CreateVfs(); for (DirectoryNames::iterator iter = modDirs.begin(); iter != modDirs.end(); ++iter) { vfs->Clear(); if (vfs->Mount(L"", modPath / *iter, VFS_MOUNT_MUST_EXIST) < 0) continue; CVFSFile modinfo; if (modinfo.Load(vfs, L"mod.json", false) != PSRETURN_OK) continue; JS::RootedValue json(cx); if (!scriptInterface.ParseJSON(modinfo.GetAsString(), &json)) continue; // Valid mod, add it to our structure JS_SetProperty(cx, obj, utf8_from_wstring(iter->string()).c_str(), json); } GetDirectoryEntries(modUserPath, NULL, &modDirsUser); bool dev = InDevelopmentCopy(); for (DirectoryNames::iterator iter = modDirsUser.begin(); iter != modDirsUser.end(); ++iter) { // If we are in a dev copy we do not mount mods in the user mod folder that // are already present in the mod folder, thus we skip those here. if (dev && std::binary_search(modDirs.begin(), modDirs.end(), *iter)) continue; vfs->Clear(); if (vfs->Mount(L"", modUserPath / *iter, VFS_MOUNT_MUST_EXIST) < 0) continue; CVFSFile modinfo; if (modinfo.Load(vfs, L"mod.json", false) != PSRETURN_OK) continue; JS::RootedValue json(cx); if (!scriptInterface.ParseJSON(modinfo.GetAsString(), &json)) continue; // Valid mod, add it to our structure JS_SetProperty(cx, obj, utf8_from_wstring(iter->string()).c_str(), json); } return JS::ObjectValue(*obj); } Index: ps/trunk/source/scriptinterface/tests/test_ScriptConversions.h =================================================================== --- ps/trunk/source/scriptinterface/tests/test_ScriptConversions.h (revision 20638) +++ ps/trunk/source/scriptinterface/tests/test_ScriptConversions.h (revision 20639) @@ -1,260 +1,260 @@ /* Copyright (C) 2017 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 "scriptinterface/ScriptInterface.h" #include "maths/Fixed.h" #include "maths/FixedVector2D.h" #include "maths/FixedVector3D.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "jsapi.h" class TestScriptConversions : public CxxTest::TestSuite { template void convert_to(const T& value, const std::string& expected) { ScriptInterface script("Test", "Test", g_ScriptRuntime); TS_ASSERT(script.LoadGlobalScripts()); JSContext* cx = script.GetContext(); JSAutoRequest rq(cx); JS::RootedValue v1(cx); ScriptInterface::ToJSVal(cx, &v1, value); // We want to convert values to strings, but can't just call toSource() on them // since they might not be objects. So just use uneval. std::string source; JS::RootedValue global(cx, script.GetGlobalObject()); TS_ASSERT(script.CallFunction(global, "uneval", source, v1)); TS_ASSERT_STR_EQUALS(source, expected); } template void roundtrip(const T& value, const char* expected) { ScriptInterface script("Test", "Test", g_ScriptRuntime); TS_ASSERT(script.LoadGlobalScripts()); JSContext* cx = script.GetContext(); JSAutoRequest rq(cx); JS::RootedValue v1(cx); ScriptInterface::ToJSVal(cx, &v1, value); std::string source; JS::RootedValue global(cx, script.GetGlobalObject()); TS_ASSERT(script.CallFunction(global, "uneval", source, v1)); if (expected) TS_ASSERT_STR_EQUALS(source, expected); T v2 = T(); TS_ASSERT(ScriptInterface::FromJSVal(cx, v1, v2)); TS_ASSERT_EQUALS(value, v2); } template void call_prototype_function(const T& u, const T& v, const std::string& func, const std::string& expected) { ScriptInterface script("Test", "Test", g_ScriptRuntime); TS_ASSERT(script.LoadGlobalScripts()); JSContext* cx = script.GetContext(); JSAutoRequest rq(cx); JS::RootedValue v1(cx); ScriptInterface::ToJSVal(cx, &v1, v); JS::RootedValue u1(cx); ScriptInterface::ToJSVal(cx, &u1, u); T r; JS::RootedValue r1(cx); TS_ASSERT(script.CallFunction(u1, func.c_str(), r, v1)); ScriptInterface::ToJSVal(cx, &r1, r); std::string source; JS::RootedValue global(cx, script.GetGlobalObject()); TS_ASSERT(script.CallFunction(global, "uneval", source, r1)); TS_ASSERT_STR_EQUALS(source, expected); } public: void setUp() { - g_VFS = CreateVfs(20 * MiB); + g_VFS = CreateVfs(); TS_ASSERT_OK(g_VFS->Mount(L"", DataDir()/"mods"/"_test.sim", VFS_MOUNT_MUST_EXIST)); } void tearDown() { g_VFS.reset(); } void test_roundtrip() { roundtrip(true, "true"); roundtrip(false, "false"); roundtrip(0, "0"); roundtrip(0.5, "0.5"); roundtrip(1e9f, "1000000000"); roundtrip(1e30f, "1.0000000150474662e+30"); roundtrip(0, "0"); roundtrip(123, "123"); roundtrip(-123, "-123"); roundtrip(1073741822, "1073741822"); // JSVAL_INT_MAX-1 roundtrip(1073741823, "1073741823"); // JSVAL_INT_MAX roundtrip(-1073741823, "-1073741823"); // JSVAL_INT_MIN+1 roundtrip(-1073741824, "-1073741824"); // JSVAL_INT_MIN roundtrip(0, "0"); roundtrip(123, "123"); roundtrip(1073741822, "1073741822"); // JSVAL_INT_MAX-1 roundtrip(1073741823, "1073741823"); // JSVAL_INT_MAX { TestLogger log; // swallow warnings about values not being stored as integer JS::Values roundtrip(1073741824, "1073741824"); // JSVAL_INT_MAX+1 roundtrip(-1073741825, "-1073741825"); // JSVAL_INT_MIN-1 roundtrip(1073741824, "1073741824"); // JSVAL_INT_MAX+1 } std::string s1 = "test"; s1[1] = '\0'; std::string s2 = "тест"; s2[2] = s2[3] = '\0'; std::wstring w1 = L"test"; w1[1] = '\0'; std::wstring w2 = L"тест"; w2[1] = '\0'; roundtrip("", "\"\""); roundtrip("test", "\"test\""); roundtrip("тест", "\"\\xD1\\x82\\xD0\\xB5\\xD1\\x81\\xD1\\x82\""); roundtrip(s1, "\"t\\x00st\""); roundtrip(s2, "\"\\xD1\\x82\\x00\\x00\\xD1\\x81\\xD1\\x82\""); roundtrip(L"", "\"\""); roundtrip(L"test", "\"test\""); // Windows has two byte wchar_t. We test for this explicitly since we can catch more possible issues this way. roundtrip(L"тест", sizeof(wchar_t) == 2 ? "\"\\xD1\\u201A\\xD0\\xB5\\xD1\\x81\\xD1\\u201A\"" : "\"\\u0442\\u0435\\u0441\\u0442\""); roundtrip(w1, "\"t\\x00st\""); roundtrip(w2, sizeof(wchar_t) == 2 ? "\"\\xD1\\x00\\xD0\\xB5\\xD1\\x81\\xD1\\u201A\"" : "\"\\u0442\\x00\\u0441\\u0442\""); convert_to("", "\"\""); convert_to("test", "\"test\""); convert_to(s1.c_str(), "\"t\""); convert_to(s2.c_str(), "\"\\xD1\\x82\""); roundtrip(fixed::FromInt(0), "0"); roundtrip(fixed::FromInt(123), "123"); roundtrip(fixed::FromInt(-123), "-123"); roundtrip(fixed::FromDouble(123.25), "123.25"); } void test_integers() { ScriptInterface script("Test", "Test", g_ScriptRuntime); JSContext* cx = script.GetContext(); JSAutoRequest rq(cx); // using new uninitialized variables each time to be sure the test doesn't succeeed if ToJSVal doesn't touch the value at all. JS::RootedValue val0(cx), val1(cx), val2(cx), val3(cx), val4(cx), val5(cx), val6(cx), val7(cx), val8(cx); ScriptInterface::ToJSVal(cx, &val0, 0); ScriptInterface::ToJSVal(cx, &val1, 2147483646); // JSVAL_INT_MAX-1 ScriptInterface::ToJSVal(cx, &val2, 2147483647); // JSVAL_INT_MAX ScriptInterface::ToJSVal(cx, &val3, -2147483647); // JSVAL_INT_MIN+1 ScriptInterface::ToJSVal(cx, &val4, -(i64)2147483648u); // JSVAL_INT_MIN TS_ASSERT(val0.isInt32()); TS_ASSERT(val1.isInt32()); TS_ASSERT(val2.isInt32()); TS_ASSERT(val3.isInt32()); TS_ASSERT(val4.isInt32()); ScriptInterface::ToJSVal(cx, &val5, 0); ScriptInterface::ToJSVal(cx, &val6, 2147483646u); // JSVAL_INT_MAX-1 ScriptInterface::ToJSVal(cx, &val7, 2147483647u); // JSVAL_INT_MAX ScriptInterface::ToJSVal(cx, &val8, 2147483648u); // JSVAL_INT_MAX+1 TS_ASSERT(val5.isInt32()); TS_ASSERT(val6.isInt32()); TS_ASSERT(val7.isInt32()); TS_ASSERT(val8.isDouble()); } void test_nonfinite() { roundtrip(std::numeric_limits::infinity(), "Infinity"); roundtrip(-std::numeric_limits::infinity(), "-Infinity"); convert_to(std::numeric_limits::quiet_NaN(), "NaN"); // can't use roundtrip since nan != nan ScriptInterface script("Test", "Test", g_ScriptRuntime); JSContext* cx = script.GetContext(); JSAutoRequest rq(cx); float f = 0; JS::RootedValue testNANVal(cx); ScriptInterface::ToJSVal(cx, &testNANVal, NAN); TS_ASSERT(ScriptInterface::FromJSVal(cx, testNANVal, f)); TS_ASSERT(isnan(f)); } // NOTE: fixed and vector conversions are defined in simulation2/scripting/EngineScriptConversions.cpp void test_fixed() { fixed f; f.SetInternalValue(10590283); roundtrip(f, "161.5948944091797"); f.SetInternalValue(-10590283); roundtrip(f, "-161.5948944091797"); f.SetInternalValue(2000000000); roundtrip(f, "30517.578125"); f.SetInternalValue(2000000001); roundtrip(f, "30517.57814025879"); } void test_vector2d() { CFixedVector2D v(fixed::Zero(), fixed::Pi()); roundtrip(v, "({x:0, y:3.1415863037109375})"); CFixedVector2D u(fixed::FromInt(1), fixed::Zero()); call_prototype_function(u, v, "add", "({x:1, y:3.1415863037109375})"); } void test_vector3d() { CFixedVector3D v(fixed::Zero(), fixed::Pi(), fixed::FromInt(1)); roundtrip(v, "({x:0, y:3.1415863037109375, z:1})"); CFixedVector3D u(fixed::Pi(), fixed::Zero(), fixed::FromInt(2)); call_prototype_function(u, v, "add", "({x:3.1415863037109375, y:3.1415863037109375, z:3})"); } }; Index: ps/trunk/source/simulation2/components/tests/test_Pathfinder.h =================================================================== --- ps/trunk/source/simulation2/components/tests/test_Pathfinder.h (revision 20638) +++ ps/trunk/source/simulation2/components/tests/test_Pathfinder.h (revision 20639) @@ -1,353 +1,353 @@ /* Copyright (C) 2017 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 "simulation2/system/ComponentTest.h" #include "simulation2/components/ICmpObstructionManager.h" #include "simulation2/components/ICmpPathfinder.h" #include "graphics/MapReader.h" #include "graphics/Terrain.h" #include "graphics/TerrainTextureManager.h" #include "lib/timer.h" #include "lib/tex/tex.h" #include "ps/Loader.h" #include "ps/Pyrogenesis.h" #include "simulation2/Simulation2.h" class TestCmpPathfinder : public CxxTest::TestSuite { public: void setUp() { - g_VFS = CreateVfs(20 * MiB); + g_VFS = CreateVfs(); g_VFS->Mount(L"", DataDir()/"mods"/"mod", VFS_MOUNT_MUST_EXIST); g_VFS->Mount(L"", DataDir()/"mods"/"public", VFS_MOUNT_MUST_EXIST, 1); // ignore directory-not-found errors TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir()/"_testcache")); CXeromyces::Startup(); // Need some stuff for terrain movement costs: // (TODO: this ought to be independent of any graphics code) new CTerrainTextureManager; g_TexMan.LoadTerrainTextures(); } void tearDown() { delete &g_TexMan; CXeromyces::Terminate(); g_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); } void test_namespace() { // Check that Pathfinding::NAVCELL_SIZE is actually an integer and that the definitions // of Pathfinding::NAVCELL_SIZE_INT and Pathfinding::NAVCELL_SIZE_LOG2 match TS_ASSERT_EQUALS(Pathfinding::NAVCELL_SIZE.ToInt_RoundToNegInfinity(), Pathfinding::NAVCELL_SIZE.ToInt_RoundToInfinity()); TS_ASSERT_EQUALS(Pathfinding::NAVCELL_SIZE.ToInt_RoundToNearest(), Pathfinding::NAVCELL_SIZE_INT); TS_ASSERT_EQUALS((Pathfinding::NAVCELL_SIZE >> 1).ToInt_RoundToZero(), Pathfinding::NAVCELL_SIZE_LOG2); } void test_performance_DISABLED() { CTerrain terrain; CSimulation2 sim2(NULL, g_ScriptRuntime, &terrain); sim2.LoadDefaultScripts(); sim2.ResetState(); std::unique_ptr mapReader(new CMapReader()); LDR_BeginRegistering(); mapReader->LoadMap(L"maps/skirmishes/Median Oasis (2).pmp", sim2.GetScriptInterface().GetJSRuntime(), JS::UndefinedHandleValue, &terrain, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &sim2, &sim2.GetSimContext(), -1, false); LDR_EndRegistering(); TS_ASSERT_OK(LDR_NonprogressiveLoad()); sim2.Update(0); CmpPtr cmp(sim2, SYSTEM_ENTITY); #if 0 entity_pos_t x0 = entity_pos_t::FromInt(10); entity_pos_t z0 = entity_pos_t::FromInt(495); entity_pos_t x1 = entity_pos_t::FromInt(500); entity_pos_t z1 = entity_pos_t::FromInt(495); ICmpPathfinder::Goal goal = { ICmpPathfinder::Goal::POINT, x1, z1 }; WaypointPath path; cmp->ComputePath(x0, z0, goal, cmp->GetPassabilityClass("default"), path); for (size_t i = 0; i < path.m_Waypoints.size(); ++i) printf("%d: %f %f\n", (int)i, path.m_Waypoints[i].x.ToDouble(), path.m_Waypoints[i].z.ToDouble()); #endif double t = timer_Time(); srand(1234); for (size_t j = 0; j < 1024*2; ++j) { entity_pos_t x0 = entity_pos_t::FromInt(rand() % 512); entity_pos_t z0 = entity_pos_t::FromInt(rand() % 512); entity_pos_t x1 = x0 + entity_pos_t::FromInt(rand() % 64); entity_pos_t z1 = z0 + entity_pos_t::FromInt(rand() % 64); PathGoal goal = { PathGoal::POINT, x1, z1 }; WaypointPath path; cmp->ComputePath(x0, z0, goal, cmp->GetPassabilityClass("default"), path); } t = timer_Time() - t; printf("[%f]", t); } void test_performance_short_DISABLED() { CTerrain terrain; terrain.Initialize(5, NULL); CSimulation2 sim2(NULL, g_ScriptRuntime, &terrain); sim2.LoadDefaultScripts(); sim2.ResetState(); const entity_pos_t range = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*12); CmpPtr cmpObstructionMan(sim2, SYSTEM_ENTITY); CmpPtr cmpPathfinder(sim2, SYSTEM_ENTITY); srand(0); for (size_t i = 0; i < 200; ++i) { fixed x = fixed::FromFloat(1.5f*range.ToFloat() * rand()/(float)RAND_MAX); fixed z = fixed::FromFloat(1.5f*range.ToFloat() * rand()/(float)RAND_MAX); // printf("# %f %f\n", x.ToFloat(), z.ToFloat()); cmpObstructionMan->AddUnitShape(INVALID_ENTITY, x, z, fixed::FromInt(2), 0, INVALID_ENTITY); } NullObstructionFilter filter; PathGoal goal = { PathGoal::POINT, range, range }; WaypointPath path; cmpPathfinder->ComputeShortPath(filter, range/3, range/3, fixed::FromInt(2), range, goal, 0, path); for (size_t i = 0; i < path.m_Waypoints.size(); ++i) printf("# %d: %f %f\n", (int)i, path.m_Waypoints[i].x.ToFloat(), path.m_Waypoints[i].z.ToFloat()); } template void DumpGrid(std::ostream& stream, const Grid& grid, int mask) { for (u16 j = 0; j < grid.m_H; ++j) { for (u16 i = 0; i < grid.m_W; ) { if (!(grid.get(i, j) & mask)) { i++; continue; } u16 i0 = i; for (i = i0+1; ; ++i) { if (i >= grid.m_W || !(grid.get(i, j) & mask)) { stream << " \n"; break; } } } } } void test_perf2_DISABLED() { CTerrain terrain; CSimulation2 sim2(NULL, g_ScriptRuntime, &terrain); sim2.LoadDefaultScripts(); sim2.ResetState(); std::unique_ptr mapReader(new CMapReader()); LDR_BeginRegistering(); mapReader->LoadMap(L"maps/scenarios/Peloponnese.pmp", sim2.GetScriptInterface().GetJSRuntime(), JS::UndefinedHandleValue, &terrain, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &sim2, &sim2.GetSimContext(), -1, false); LDR_EndRegistering(); TS_ASSERT_OK(LDR_NonprogressiveLoad()); sim2.Update(0); std::ofstream stream(OsString("perf2.html").c_str(), std::ofstream::out | std::ofstream::trunc); CmpPtr cmpObstructionManager(sim2, SYSTEM_ENTITY); CmpPtr cmpPathfinder(sim2, SYSTEM_ENTITY); pass_class_t obstructionsMask = cmpPathfinder->GetPassabilityClass("default"); const Grid& obstructions = cmpPathfinder->GetPassabilityGrid(); int scale = 1; stream << "\n"; stream << "\n"; stream << "\n"; stream << "\n"; stream << " \n"; DumpGrid(stream, obstructions, obstructionsMask); stream << " \n"; DumpPath(stream, 128*4, 256*4, 128*4, 384*4, cmpPathfinder); // RepeatPath(500, 128*4, 256*4, 128*4, 384*4, cmpPathfinder); // // DumpPath(stream, 128*4, 204*4, 192*4, 204*4, cmpPathfinder); // // DumpPath(stream, 128*4, 230*4, 32*4, 230*4, cmpPathfinder); stream << "\n"; stream << "\n"; } void test_perf3_DISABLED() { CTerrain terrain; CSimulation2 sim2(NULL, g_ScriptRuntime, &terrain); sim2.LoadDefaultScripts(); sim2.ResetState(); std::unique_ptr mapReader(new CMapReader()); LDR_BeginRegistering(); mapReader->LoadMap(L"maps/scenarios/Peloponnese.pmp", sim2.GetScriptInterface().GetJSRuntime(), JS::UndefinedHandleValue, &terrain, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &sim2, &sim2.GetSimContext(), -1, false); LDR_EndRegistering(); TS_ASSERT_OK(LDR_NonprogressiveLoad()); sim2.Update(0); std::ofstream stream(OsString("perf3.html").c_str(), std::ofstream::out | std::ofstream::trunc); CmpPtr cmpObstructionManager(sim2, SYSTEM_ENTITY); CmpPtr cmpPathfinder(sim2, SYSTEM_ENTITY); pass_class_t obstructionsMask = cmpPathfinder->GetPassabilityClass("default"); const Grid& obstructions = cmpPathfinder->GetPassabilityGrid(); int scale = 31; stream << "\n"; stream << "\n"; stream << "\n"; stream << "\n"; stream << "\n"; stream << "\n"; stream << "\n"; stream << " \n"; DumpGrid(stream, obstructions, obstructionsMask); stream << " \n"; for (int j = 160; j < 190; ++j) for (int i = 220; i < 290; ++i) DumpPath(stream, 230, 178, i, j, cmpPathfinder); stream << "\n"; stream << "\n"; stream << "\n"; } void DumpPath(std::ostream& stream, int i0, int j0, int i1, int j1, CmpPtr& cmpPathfinder) { entity_pos_t x0 = entity_pos_t::FromInt(i0); entity_pos_t z0 = entity_pos_t::FromInt(j0); entity_pos_t x1 = entity_pos_t::FromInt(i1); entity_pos_t z1 = entity_pos_t::FromInt(j1); PathGoal goal = { PathGoal::POINT, x1, z1 }; WaypointPath path; cmpPathfinder->ComputePath(x0, z0, goal, cmpPathfinder->GetPassabilityClass("default"), path); u32 debugSteps; double debugTime; Grid debugGrid; cmpPathfinder->GetDebugData(debugSteps, debugTime, debugGrid); // stream << " \n"; stream << " \n"; // stream << " \n"; // DumpGrid(stream, debugGrid, 1); // stream << " \n"; // stream << " \n"; // DumpGrid(stream, debugGrid, 2); // stream << " \n"; // stream << " \n"; // DumpGrid(stream, debugGrid, 3); // stream << " \n"; stream << " \n"; stream << " \n"; } void RepeatPath(int n, int i0, int j0, int i1, int j1, CmpPtr& cmpPathfinder) { entity_pos_t x0 = entity_pos_t::FromInt(i0); entity_pos_t z0 = entity_pos_t::FromInt(j0); entity_pos_t x1 = entity_pos_t::FromInt(i1); entity_pos_t z1 = entity_pos_t::FromInt(j1); PathGoal goal = { PathGoal::POINT, x1, z1 }; double t = timer_Time(); for (int i = 0; i < n; ++i) { WaypointPath path; cmpPathfinder->ComputePath(x0, z0, goal, cmpPathfinder->GetPassabilityClass("default"), path); } t = timer_Time() - t; debug_printf("### RepeatPath %fms each (%fs total)\n", 1000*t / n, t); } }; Index: ps/trunk/source/simulation2/components/tests/test_scripts.h =================================================================== --- ps/trunk/source/simulation2/components/tests/test_scripts.h (revision 20638) +++ ps/trunk/source/simulation2/components/tests/test_scripts.h (revision 20639) @@ -1,86 +1,86 @@ /* Copyright (C) 2017 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 "simulation2/system/ComponentTest.h" #include "ps/Filesystem.h" class TestComponentScripts : public CxxTest::TestSuite { public: void setUp() { - g_VFS = CreateVfs(20 * MiB); + g_VFS = CreateVfs(); g_VFS->Mount(L"", DataDir()/"mods"/"mod", VFS_MOUNT_MUST_EXIST); g_VFS->Mount(L"", DataDir()/"mods"/"public", VFS_MOUNT_MUST_EXIST, 1); // ignore directory-not-found errors CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); g_VFS.reset(); } static void load_script(const ScriptInterface& scriptInterface, const VfsPath& pathname) { CVFSFile file; TS_ASSERT_EQUALS(file.Load(g_VFS, pathname), PSRETURN_OK); CStr content = file.DecodeUTF8(); // assume it's UTF-8 TSM_ASSERT(L"Running script "+pathname.string(), scriptInterface.LoadScript(pathname, content)); } static void Script_LoadComponentScript(ScriptInterface::CxPrivate* pCxPrivate, const VfsPath& pathname) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); TS_ASSERT(componentManager->LoadScript(VfsPath(L"simulation/components") / pathname)); } static void Script_LoadHelperScript(ScriptInterface::CxPrivate* pCxPrivate, const VfsPath& pathname) { CComponentManager* componentManager = static_cast (pCxPrivate->pCBData); TS_ASSERT(componentManager->LoadScript(VfsPath(L"simulation/helpers") / pathname)); } void test_scripts() { if (!VfsFileExists(L"simulation/components/tests/setup.js")) { debug_printf("WARNING: Skipping component scripts tests (can't find binaries/data/mods/public/simulation/components/tests/setup.js)\n"); return; } VfsPaths paths; TS_ASSERT_OK(vfs::GetPathnames(g_VFS, L"simulation/components/tests/", L"test_*.js", paths)); paths.push_back(VfsPath(L"simulation/components/tests/setup_test.js")); for (const VfsPath& path : paths) { CSimContext context; CComponentManager componentManager(context, g_ScriptRuntime, true); ScriptTestSetup(componentManager.GetScriptInterface()); componentManager.GetScriptInterface().RegisterFunction ("LoadComponentScript"); componentManager.GetScriptInterface().RegisterFunction ("LoadHelperScript"); componentManager.LoadComponentTypes(); load_script(componentManager.GetScriptInterface(), L"simulation/components/tests/setup.js"); load_script(componentManager.GetScriptInterface(), path); } } }; Index: ps/trunk/source/simulation2/tests/test_CmpTemplateManager.h =================================================================== --- ps/trunk/source/simulation2/tests/test_CmpTemplateManager.h (revision 20638) +++ ps/trunk/source/simulation2/tests/test_CmpTemplateManager.h (revision 20639) @@ -1,258 +1,258 @@ /* Copyright (C) 2017 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "lib/self_test.h" #include "simulation2/system/ComponentManager.h" #include "simulation2/components/ICmpTemplateManager.h" #include "simulation2/components/ICmpTest.h" #include "simulation2/MessageTypes.h" #include "simulation2/system/ParamNode.h" #include "simulation2/system/SimContext.h" #include "simulation2/Simulation2.h" #include "graphics/Terrain.h" #include "ps/Filesystem.h" #include "ps/CLogger.h" #include "ps/XML/Xeromyces.h" class TestCmpTemplateManager : public CxxTest::TestSuite { public: void setUp() { - g_VFS = CreateVfs(20 * MiB); + g_VFS = CreateVfs(); TS_ASSERT_OK(g_VFS->Mount(L"", DataDir()/"mods"/"_test.sim", VFS_MOUNT_MUST_EXIST)); TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir()/"_testcache")); CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); g_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); } void test_LoadTemplate() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); man.LoadComponentTypes(); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.LookupEntityHandle(ent1, true); CParamNode noParam; TS_ASSERT(man.AddComponent(hnd1, CID_TemplateManager, noParam)); ICmpTemplateManager* tempMan = static_cast (man.QueryInterface(ent1, IID_TemplateManager)); TS_ASSERT(tempMan != NULL); const CParamNode* basic = tempMan->LoadTemplate(ent2, "basic"); TS_ASSERT(basic != NULL); TS_ASSERT_WSTR_EQUALS(basic->ToXML(), L"12345"); const CParamNode* inherit2 = tempMan->LoadTemplate(ent2, "inherit2"); TS_ASSERT(inherit2 != NULL); TS_ASSERT_WSTR_EQUALS(inherit2->ToXML(), L"d2e1f1g2"); const CParamNode* inherit1 = tempMan->LoadTemplate(ent2, "inherit1"); TS_ASSERT(inherit1 != NULL); TS_ASSERT_WSTR_EQUALS(inherit1->ToXML(), L"d1e1f1"); const CParamNode* actor = tempMan->LoadTemplate(ent2, "actor|example1"); TS_ASSERT(actor != NULL); TS_ASSERT_WSTR_EQUALS(actor->ToXML(), L"1.0actor.pngactor_mask.png" L"example1falsefalsefalse"); } void test_LoadTemplate_scriptcache() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); man.LoadComponentTypes(); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.LookupEntityHandle(ent1, true); CParamNode noParam; TS_ASSERT(man.AddComponent(hnd1, CID_TemplateManager, noParam)); ICmpTemplateManager* tempMan = static_cast (man.QueryInterface(ent1, IID_TemplateManager)); TS_ASSERT(tempMan != NULL); tempMan->DisableValidation(); JSContext* cx = man.GetScriptInterface().GetContext(); JSAutoRequest rq(cx); // This is testing some bugs in the template JS object caching const CParamNode* inherit1 = tempMan->LoadTemplate(ent2, "inherit1"); JS::RootedValue val(cx); ScriptInterface::ToJSVal(cx, &val, inherit1); TS_ASSERT_STR_EQUALS(man.GetScriptInterface().ToString(&val), "({Test1A:{'@a':\"a1\", '@b':\"b1\", '@c':\"c1\", d:\"d1\", e:\"e1\", f:\"f1\"}})"); const CParamNode* inherit2 = tempMan->LoadTemplate(ent2, "inherit2"); ScriptInterface::ToJSVal(cx, &val, inherit2); TS_ASSERT_STR_EQUALS(man.GetScriptInterface().ToString(&val), "({'@parent':\"inherit1\", Test1A:{'@a':\"a2\", '@b':\"b1\", '@c':\"c1\", d:\"d2\", e:\"e1\", f:\"f1\", g:\"g2\"}})"); const CParamNode* actor = tempMan->LoadTemplate(ent2, "actor|example1"); ScriptInterface::ToJSVal(cx, &val, &actor->GetChild("VisualActor")); TS_ASSERT_STR_EQUALS(man.GetScriptInterface().ToString(&val), "({Actor:\"example1\", ActorOnly:(void 0), SilhouetteDisplay:\"false\", SilhouetteOccluder:\"false\", VisibleInAtlasOnly:\"false\"})"); const CParamNode* foundation = tempMan->LoadTemplate(ent2, "foundation|actor|example1"); ScriptInterface::ToJSVal(cx, &val, &foundation->GetChild("VisualActor")); TS_ASSERT_STR_EQUALS(man.GetScriptInterface().ToString(&val), "({Actor:\"example1\", ActorOnly:(void 0), Foundation:(void 0), SilhouetteDisplay:\"false\", SilhouetteOccluder:\"false\", VisibleInAtlasOnly:\"false\"})"); #define GET_FIRST_ELEMENT(n, templateName) \ const CParamNode* n = tempMan->LoadTemplate(ent2, templateName); \ for (CParamNode::ChildrenMap::const_iterator it = n->GetChildren().begin(); it != n->GetChildren().end(); ++it) \ { \ if (it->first[0] == '@') \ continue; \ ScriptInterface::ToJSVal(cx, &val, it->second); \ break; \ } GET_FIRST_ELEMENT(n1, "inherit_a"); TS_ASSERT_STR_EQUALS(man.GetScriptInterface().ToString(&val), "({'@datatype':\"tokens\", _string:\"a b c\"})"); GET_FIRST_ELEMENT(n2, "inherit_b"); TS_ASSERT_STR_EQUALS(man.GetScriptInterface().ToString(&val), "({'@datatype':\"tokens\", _string:\"a b c d\"})"); GET_FIRST_ELEMENT(n3, "inherit_c"); TS_ASSERT_STR_EQUALS(man.GetScriptInterface().ToString(&val), "({'@a':\"b\", _string:\"a b c\"})"); GET_FIRST_ELEMENT(n4, "inherit_d"); TS_ASSERT_STR_EQUALS(man.GetScriptInterface().ToString(&val), "({'@a':\"b\", '@c':\"d\"})"); #undef GET_FIRST_ELEMENT } void test_LoadTemplate_errors() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); man.LoadComponentTypes(); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.LookupEntityHandle(ent1, true); CParamNode noParam; TS_ASSERT(man.AddComponent(hnd1, CID_TemplateManager, noParam)); ICmpTemplateManager* tempMan = static_cast (man.QueryInterface(ent1, IID_TemplateManager)); TS_ASSERT(tempMan != NULL); TestLogger logger; TS_ASSERT(tempMan->LoadTemplate(ent2, "illformed") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "invalid") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "nonexistent") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "inherit-loop") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "inherit-broken") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "inherit-special") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "preview|nonexistent") == NULL); } void test_LoadTemplate_multiple() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); man.LoadComponentTypes(); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.LookupEntityHandle(ent1, true); CParamNode noParam; TS_ASSERT(man.AddComponent(hnd1, CID_TemplateManager, noParam)); ICmpTemplateManager* tempMan = static_cast (man.QueryInterface(ent1, IID_TemplateManager)); TS_ASSERT(tempMan != NULL); const CParamNode* basicA = tempMan->LoadTemplate(ent2, "basic"); TS_ASSERT(basicA != NULL); const CParamNode* basicB = tempMan->LoadTemplate(ent2, "basic"); TS_ASSERT(basicA == basicB); const CParamNode* inherit2A = tempMan->LoadTemplate(ent2, "inherit2"); TS_ASSERT(inherit2A != NULL); const CParamNode* inherit2B = tempMan->LoadTemplate(ent2, "inherit2"); TS_ASSERT(inherit2A == inherit2B); TestLogger logger; TS_ASSERT(tempMan->LoadTemplate(ent2, "nonexistent") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "nonexistent") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "inherit-loop") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "inherit-loop") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "inherit-broken") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "inherit-broken") == NULL); } }; class TestCmpTemplateManager_2 : public CxxTest::TestSuite { public: void setUp() { - g_VFS = CreateVfs(20 * MiB); + g_VFS = CreateVfs(); TS_ASSERT_OK(g_VFS->Mount(L"", DataDir()/"mods"/"mod", VFS_MOUNT_MUST_EXIST)); TS_ASSERT_OK(g_VFS->Mount(L"", DataDir()/"mods"/"public", VFS_MOUNT_MUST_EXIST)); TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir()/"_testcache")); CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); g_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); } // This just attempts loading every public entity, to check there's no validation errors void test_load_all_DISABLED() // disabled since it's a bit slow and noisy { CTerrain dummy; CSimulation2 sim(NULL, g_ScriptRuntime, &dummy); sim.LoadDefaultScripts(); sim.ResetState(); CmpPtr cmpTemplateManager(sim, SYSTEM_ENTITY); TS_ASSERT(cmpTemplateManager); std::vector templates = cmpTemplateManager->FindAllTemplates(true); for (size_t i = 0; i < templates.size(); ++i) { std::string name = templates[i]; printf("# %s\n", name.c_str()); const CParamNode* p = cmpTemplateManager->GetTemplate(name); TS_ASSERT(p != NULL); } } }; Index: ps/trunk/source/simulation2/tests/test_ComponentManager.h =================================================================== --- ps/trunk/source/simulation2/tests/test_ComponentManager.h (revision 20638) +++ ps/trunk/source/simulation2/tests/test_ComponentManager.h (revision 20639) @@ -1,879 +1,879 @@ /* Copyright (C) 2017 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "lib/self_test.h" #include "simulation2/system/ComponentManager.h" #include "simulation2/MessageTypes.h" #include "simulation2/system/ParamNode.h" #include "simulation2/system/SimContext.h" #include "simulation2/serialization/ISerializer.h" #include "simulation2/components/ICmpTest.h" #include "simulation2/components/ICmpTemplateManager.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/XML/Xeromyces.h" #define TS_ASSERT_STREAM(stream, len, buffer) \ TS_ASSERT_EQUALS(stream.str().length(), (size_t)len); \ TS_ASSERT_SAME_DATA(stream.str().data(), buffer, len) #define TS_ASSERT_THROWS_PSERROR(e, t, s) \ TS_ASSERT_THROWS_EQUALS(e, const t& ex, std::string(ex.what()), s) class TestComponentManager : public CxxTest::TestSuite { public: void setUp() { - g_VFS = CreateVfs(20 * MiB); + g_VFS = CreateVfs(); TS_ASSERT_OK(g_VFS->Mount(L"", DataDir()/"mods"/"_test.sim", VFS_MOUNT_MUST_EXIST)); TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir()/"_testcache")); CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); g_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); } void test_Load() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); man.LoadComponentTypes(); } void test_LookupCID() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); man.LoadComponentTypes(); TS_ASSERT_EQUALS(man.LookupCID("Test1A"), (int)CID_Test1A); TS_ASSERT_EQUALS(man.LookupCID("Test1B"), (int)CID_Test1B); } void test_AllocateNewEntity() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); TS_ASSERT_EQUALS(man.AllocateNewEntity(), (u32)2); TS_ASSERT_EQUALS(man.AllocateNewEntity(), (u32)3); TS_ASSERT_EQUALS(man.AllocateNewEntity(), (u32)4); TS_ASSERT_EQUALS(man.AllocateNewEntity(100), (u32)100); TS_ASSERT_EQUALS(man.AllocateNewEntity(), (u32)101); // TODO: // TS_ASSERT_EQUALS(man.AllocateNewEntity(3), (u32)102); TS_ASSERT_EQUALS(man.AllocateNewLocalEntity(), (u32)FIRST_LOCAL_ENTITY); TS_ASSERT_EQUALS(man.AllocateNewLocalEntity(), (u32)FIRST_LOCAL_ENTITY+1); man.ResetState(); TS_ASSERT_EQUALS(man.AllocateNewEntity(), (u32)2); TS_ASSERT_EQUALS(man.AllocateNewEntity(3), (u32)3); TS_ASSERT_EQUALS(man.AllocateNewLocalEntity(), (u32)FIRST_LOCAL_ENTITY); } void test_rng() { // Ensure we get the same random number with the same seed double first; { CSimContext context; CComponentManager man(context, g_ScriptRuntime); man.SetRNGSeed(123); if (!man.m_ScriptInterface.MathRandom(first)) TS_FAIL("Couldn't get random number!"); } double second; { CSimContext context; CComponentManager man(context, g_ScriptRuntime); man.SetRNGSeed(123); if (!man.m_ScriptInterface.MathRandom(second)) TS_FAIL("Couldn't get random number!"); } TS_ASSERT_EQUALS(first, second); } void test_AddComponent_errors() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); man.LoadComponentTypes(); CEntityHandle hnd1 = man.AllocateEntityHandle(1); CParamNode noParam; TS_ASSERT(man.AddComponent(hnd1, CID_Test1A, noParam)); { TestLogger log; TS_ASSERT(! man.AddComponent(hnd1, 12345, noParam)); TS_ASSERT_STR_CONTAINS(log.GetOutput(), "ERROR: Invalid component id 12345"); } { TestLogger log; TS_ASSERT(! man.AddComponent(hnd1, CID_Test1B, noParam)); TS_ASSERT_STR_CONTAINS(log.GetOutput(), "ERROR: Multiple components for interface "); } } void test_QueryInterface() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); man.LoadComponentTypes(); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CParamNode noParam; man.AddComponent(hnd1, CID_Test1A, noParam); TS_ASSERT(man.QueryInterface(ent1, IID_Test1) != NULL); TS_ASSERT(man.QueryInterface(ent1, IID_Test2) == NULL); TS_ASSERT(man.QueryInterface(ent2, IID_Test1) == NULL); TS_ASSERT(man.QueryInterface(ent2, IID_Test2) == NULL); man.AddComponent(hnd2, CID_Test1B, noParam); TS_ASSERT(man.QueryInterface(ent2, IID_Test1) != NULL); TS_ASSERT(man.QueryInterface(ent2, IID_Test2) == NULL); man.AddComponent(hnd2, CID_Test2A, noParam); TS_ASSERT(man.QueryInterface(ent2, IID_Test1) != NULL); TS_ASSERT(man.QueryInterface(ent2, IID_Test2) != NULL); } void test_SendMessage() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); man.LoadComponentTypes(); entity_id_t ent1 = 1, ent2 = 2, ent3 = 3, ent4 = 4; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CEntityHandle hnd3 = man.AllocateEntityHandle(ent3); CEntityHandle hnd4 = man.AllocateEntityHandle(ent4); CParamNode noParam; man.AddComponent(hnd1, CID_Test1A, noParam); man.AddComponent(hnd2, CID_Test1B, noParam); man.AddComponent(hnd3, CID_Test2A, noParam); man.AddComponent(hnd4, CID_Test1A, noParam); man.AddComponent(hnd4, CID_Test2A, noParam); CMessageTurnStart msg1; CMessageUpdate msg2(fixed::FromInt(100)); CMessageInterpolate msg3(0, 0, 0); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 12000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 21000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test1))->GetX(), 11000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test2))->GetX(), 21000); // Test_1A subscribed locally to msg1, nothing subscribed globally man.PostMessage(ent1, msg1); man.PostMessage(ent1, msg2); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 12000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 21000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test1))->GetX(), 11000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test2))->GetX(), 21000); man.BroadcastMessage(msg1); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11002); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 12000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 21050); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test1))->GetX(), 11001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test2))->GetX(), 21050); // Test_1B, Test_2A subscribed locally to msg2, nothing subscribed globally man.BroadcastMessage(msg2); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11002); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 12010); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 21150); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test1))->GetX(), 11001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test2))->GetX(), 21150); // Test_1A subscribed locally to msg3, Test_1B subscribed globally man.BroadcastMessage(msg3); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11004); // local TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 12030); // global TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 21150); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test1))->GetX(), 11003); // local TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test2))->GetX(), 21150); man.PostMessage(ent1, msg3); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11006); // local TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 12050); // global TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 21150); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test1))->GetX(), 11003); // local - skipped TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test2))->GetX(), 21150); } void test_ParamNode() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); man.LoadComponentTypes(); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CParamNode noParam; CParamNode testParam; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(testParam, "1234"), PSRETURN_OK); man.AddComponent(hnd1, CID_Test1A, noParam); man.AddComponent(hnd2, CID_Test1A, testParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 1234); } void test_script_basic() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test.js")); TS_ASSERT_EQUALS(man.LookupCID("TestScript1A"), (int)CID__LastNative); TS_ASSERT_EQUALS(man.LookupCID("TestScript1B"), (int)CID__LastNative+1); entity_id_t ent1 = 1, ent2 = 2, ent3 = 3; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CEntityHandle hnd3 = man.AllocateEntityHandle(ent3); CParamNode noParam; man.AddComponent(hnd1, CID_Test1A, noParam); man.AddComponent(hnd2, man.LookupCID("TestScript1A"), noParam); man.AddComponent(hnd3, man.LookupCID("TestScript1B"), noParam); man.AddComponent(hnd3, man.LookupCID("TestScript2A"), noParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 101000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test1))->GetX(), 102000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 201000); CMessageTurnStart msg1; CMessageUpdate msg2(fixed::FromInt(25)); man.BroadcastMessage(msg1); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 101001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test1))->GetX(), 102001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 201000); man.BroadcastMessage(msg2); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 101001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test1))->GetX(), 102001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 201025); } void test_script_helper_basic() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-helper.js")); TS_ASSERT(man.LoadScript(L"simulation/helpers/test-helper.js")); entity_id_t ent1 = 1; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CParamNode noParam; man.AddComponent(hnd1, man.LookupCID("TestScript1_Helper"), noParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 3); } void test_script_global_helper() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-global-helper.js")); entity_id_t ent1 = 1; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CParamNode noParam; man.AddComponent(hnd1, man.LookupCID("TestScript1_GlobalHelper"), noParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 2); } void test_script_interface() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/interfaces/test-interface.js")); TS_ASSERT(man.LoadScript(L"simulation/components/test-interface.js")); entity_id_t ent1 = 1; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CParamNode noParam; man.AddComponent(hnd1, man.LookupCID("TestScript1_Interface"), noParam); man.AddComponent(hnd1, man.LookupCID("TestScript2_Interface"), noParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 1000 + IID__LastNative); } void test_script_errors() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); ScriptTestSetup(man.m_ScriptInterface); man.LoadComponentTypes(); { TestLogger log; TS_ASSERT(man.LoadScript(L"simulation/components/error.js")); // In SpiderMonkey 1.6, JS_ReportError calls the error reporter even if it's inside // a try{} in the script; in recent versions (not sure when it changed) it doesn't // so the error here won't get reported. TS_ASSERT_STR_NOT_CONTAINS(log.GetOutput(), "ERROR: JavaScript error: simulation/components/error.js line 4\nInvalid interface id"); } } void test_script_entityID() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); ScriptTestSetup(man.m_ScriptInterface); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-entityid.js")); entity_id_t ent1 = 1, ent2 = 234; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CParamNode noParam; man.AddComponent(hnd1, man.LookupCID("TestScript1A"), noParam); man.AddComponent(hnd2, man.LookupCID("TestScript1A"), noParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), (int)ent1); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), (int)ent2); } void test_script_QueryInterface() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-query.js")); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CParamNode noParam; man.AddComponent(hnd1, man.LookupCID("TestScript1A"), noParam); man.AddComponent(hnd1, man.LookupCID("TestScript2A"), noParam); man.AddComponent(hnd2, man.LookupCID("TestScript1A"), noParam); man.AddComponent(hnd2, CID_Test2A, noParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 400); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 21000); } void test_script_AddEntity() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-addentity.js")); TS_ASSERT(man.LoadScript(L"simulation/components/addentity/test-addentity.js")); man.InitSystemEntity(); entity_id_t ent1 = man.AllocateNewEntity(); entity_id_t ent2 = ent1 + 2; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CParamNode noParam; TS_ASSERT(man.AddComponent(man.GetSystemEntity(), CID_TemplateManager, noParam)); TS_ASSERT(man.AddComponent(hnd1, man.LookupCID("TestScript1_AddEntity"), noParam)); TS_ASSERT(man.QueryInterface(ent2, IID_Test1) == NULL); TS_ASSERT(man.QueryInterface(ent2, IID_Test2) == NULL); { TestLogger logger; // ignore bogus-template warnings TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), (int)ent2); } TS_ASSERT(man.QueryInterface(ent2, IID_Test1) != NULL); TS_ASSERT(man.QueryInterface(ent2, IID_Test2) != NULL); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 999); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test2))->GetX(), 12345); } void test_script_AddLocalEntity() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-addentity.js")); TS_ASSERT(man.LoadScript(L"simulation/components/addentity/test-addentity.js")); man.InitSystemEntity(); entity_id_t ent1 = man.AllocateNewEntity(); entity_id_t ent2 = man.AllocateNewLocalEntity() + 2; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CParamNode noParam; TS_ASSERT(man.AddComponent(man.GetSystemEntity(), CID_TemplateManager, noParam)); TS_ASSERT(man.AddComponent(hnd1, man.LookupCID("TestScript1_AddLocalEntity"), noParam)); TS_ASSERT(man.QueryInterface(ent2, IID_Test1) == NULL); TS_ASSERT(man.QueryInterface(ent2, IID_Test2) == NULL); { TestLogger logger; // ignore bogus-template warnings TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), (int)ent2); } TS_ASSERT(man.QueryInterface(ent2, IID_Test1) != NULL); TS_ASSERT(man.QueryInterface(ent2, IID_Test2) != NULL); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 999); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test2))->GetX(), 12345); } void test_script_DestroyEntity() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-destroyentity.js")); entity_id_t ent1 = 10; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CParamNode noParam; TS_ASSERT(man.AddComponent(hnd1, man.LookupCID("TestScript1_DestroyEntity"), noParam)); TS_ASSERT(man.QueryInterface(ent1, IID_Test1) != NULL); static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(); TS_ASSERT(man.QueryInterface(ent1, IID_Test1) != NULL); man.FlushDestroyedComponents(); TS_ASSERT(man.QueryInterface(ent1, IID_Test1) == NULL); } void test_script_messages() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-msg.js")); entity_id_t ent1 = 1, ent2 = 2, ent3 = 3; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CEntityHandle hnd3 = man.AllocateEntityHandle(ent3); CParamNode noParam; man.AddComponent(hnd1, man.LookupCID("TestScript1A"), noParam); man.AddComponent(hnd1, man.LookupCID("TestScript2A"), noParam); man.AddComponent(hnd2, man.LookupCID("TestScript1A"), noParam); man.AddComponent(hnd2, CID_Test2A, noParam); man.AddComponent(hnd3, man.LookupCID("TestScript1B"), noParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 100); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 100); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test2))->GetX(), 21000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test1))->GetX(), 100); // This GetX broadcasts messages TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test2))->GetX(), 200); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 650); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 5150); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test2))->GetX(), 26050); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test1))->GetX(), 5650); } void test_script_template() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-param.js")); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CParamNode noParam; CParamNode testParam; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(testParam, "1110000"), PSRETURN_OK); man.AddComponent(hnd1, man.LookupCID("TestScript1_Init"), noParam); man.AddComponent(hnd2, man.LookupCID("TestScript1_Init"), testParam.GetChild("node")); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 100); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 1+10+100+1000); } void test_script_template_readonly() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-param.js")); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CParamNode noParam; CParamNode testParam; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(testParam, "100"), PSRETURN_OK); man.AddComponent(hnd1, man.LookupCID("TestScript1_readonly"), testParam); man.AddComponent(hnd2, man.LookupCID("TestScript1_readonly"), testParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 102); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 102); } void test_script_hotload() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-hotload1.js")); entity_id_t ent1 = 1, ent2 = 2, ent3 = 3, ent4 = 4; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CEntityHandle hnd3 = man.AllocateEntityHandle(ent3); CEntityHandle hnd4 = man.AllocateEntityHandle(ent4); CParamNode testParam; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(testParam, "100"), PSRETURN_OK); man.AddComponent(hnd1, man.LookupCID("HotloadA"), testParam); man.AddComponent(hnd2, man.LookupCID("HotloadB"), testParam); man.AddComponent(hnd2, man.LookupCID("HotloadC"), testParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 100); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 200); TS_ASSERT(man.LoadScript(L"simulation/components/test-hotload2.js", true)); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 1000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 200); man.AddComponent(hnd3, man.LookupCID("HotloadA"), testParam); man.AddComponent(hnd4, man.LookupCID("HotloadB"), testParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test1))->GetX(), 1000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test1))->GetX(), 200); } void test_serialization() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); man.LoadComponentTypes(); entity_id_t ent1 = 10, ent2 = 20, ent3 = FIRST_LOCAL_ENTITY; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CEntityHandle hnd3 = man.AllocateEntityHandle(ent3); CParamNode noParam; CParamNode testParam; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(testParam, "1234"), PSRETURN_OK); man.AddComponent(hnd1, CID_Test1A, noParam); man.AddComponent(hnd1, CID_Test2A, noParam); man.AddComponent(hnd2, CID_Test1A, testParam); man.AddComponent(hnd3, CID_Test2A, noParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test2))->GetX(), 21000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 1234); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 21000); std::stringstream debugStream; TS_ASSERT(man.DumpDebugState(debugStream, true)); TS_ASSERT_STR_EQUALS(debugStream.str(), "rng: \"78606\"\n" "entities:\n" "- id: 10\n" " Test1A:\n" " x: 11000\n" " Test2A:\n" " x: 21000\n" "\n" "- id: 20\n" " Test1A:\n" " x: 1234\n" "\n" "- id: 536870912\n" " type: local\n" " Test2A:\n" " x: 21000\n" "\n" ); std::string hash; TS_ASSERT(man.ComputeStateHash(hash, false)); TS_ASSERT_EQUALS(hash.length(), (size_t)16); TS_ASSERT_SAME_DATA(hash.data(), "\x3c\x25\x6e\x22\x58\x23\x09\x58\x38\xca\xb2\x1e\x0b\x8c\xac\xcf", 16); // echo -en "\x05\x00\x00\x0078606\x02\0\0\0\x01\0\0\0\x0a\0\0\0\xf8\x2a\0\0\x14\0\0\0\xd2\x04\0\0\x04\0\0\0\x0a\0\0\0\x08\x52\0\0" | md5sum | perl -pe 's/([0-9a-f]{2})/\\x$1/g' // ^^^^^^^^ rng ^^^^^^^^ ^^next^^ ^^Test1A^^ ^^^ent1^^ ^^^11000^^^ ^^^ent2^^ ^^^1234^^^ ^^Test2A^^ ^^ent1^^ ^^^21000^^^ std::stringstream stateStream; TS_ASSERT(man.SerializeState(stateStream)); TS_ASSERT_STREAM(stateStream, 73, "\x05\x00\x00\x00\x37\x38\x36\x30\x36" // RNG "\x02\x00\x00\x00" // next entity ID "\x00\x00\x00\x00" // num system component types "\x02\x00\x00\x00" // num component types "\x06\x00\x00\x00Test1A" "\x02\x00\x00\x00" // num ents "\x0a\x00\x00\x00" // ent1 "\xf8\x2a\x00\x00" // 11000 "\x14\x00\x00\x00" // ent2 "\xd2\x04\x00\x00" // 1234 "\x06\x00\x00\x00Test2A" "\x01\x00\x00\x00" // num ents "\x0a\x00\x00\x00" // ent1 "\x08\x52\x00\x00" // 21000 ); CSimContext context2; CComponentManager man2(context2, g_ScriptRuntime); man2.LoadComponentTypes(); TS_ASSERT(man2.QueryInterface(ent1, IID_Test1) == NULL); TS_ASSERT(man2.QueryInterface(ent1, IID_Test2) == NULL); TS_ASSERT(man2.QueryInterface(ent2, IID_Test1) == NULL); TS_ASSERT(man2.QueryInterface(ent3, IID_Test2) == NULL); TS_ASSERT(man2.DeserializeState(stateStream)); TS_ASSERT_EQUALS(static_cast (man2.QueryInterface(ent1, IID_Test1))->GetX(), 11000); TS_ASSERT_EQUALS(static_cast (man2.QueryInterface(ent1, IID_Test2))->GetX(), 21000); TS_ASSERT_EQUALS(static_cast (man2.QueryInterface(ent2, IID_Test1))->GetX(), 1234); TS_ASSERT(man2.QueryInterface(ent3, IID_Test2) == NULL); } void test_script_serialization() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); ScriptTestSetup(man.m_ScriptInterface); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-serialize.js")); entity_id_t ent1 = 1, ent2 = 2, ent3 = 3, ent4 = 4; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CEntityHandle hnd3 = man.AllocateEntityHandle(ent3); CEntityHandle hnd4 = man.AllocateEntityHandle(ent4); CParamNode noParam; CParamNode testParam; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(testParam, "1234"), PSRETURN_OK); man.AddComponent(hnd1, man.LookupCID("TestScript1_values"), testParam); man.AddComponent(hnd2, man.LookupCID("TestScript1_entity"), testParam); // TODO: Since the upgrade to SpiderMonkey v24 this test won't be able to correctly represent // non-tree structures because sharp variables were removed (bug 566700). // This also affects the debug serializer and it could make sense to implement correct serialization again. man.AddComponent(hnd3, man.LookupCID("TestScript1_nontree"), testParam); man.AddComponent(hnd4, man.LookupCID("TestScript1_custom"), testParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 1234); { TestLogger log; // swallow warnings about this.entity being read-only TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), (int)ent2); } TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test1))->GetX(), 8); std::stringstream debugStream; TS_ASSERT(man.DumpDebugState(debugStream, true)); TS_ASSERT_STR_EQUALS(debugStream.str(), "rng: \"78606\"\n\ entities:\n\ - id: 1\n\ TestScript1_values:\n\ object: {\n\ \"x\": 1234,\n\ \"str\": \"this is a string\",\n\ \"things\": {\n\ \"a\": 1,\n\ \"b\": \"2\",\n\ \"c\": [\n\ 3,\n\ \"4\",\n\ [\n\ 5,\n\ []\n\ ]\n\ ]\n\ }\n\ }\n\ \n\ - id: 2\n\ TestScript1_entity:\n\ object: {}\n\ \n\ - id: 3\n\ TestScript1_nontree:\n\ object: ({x:[[2], [2], [], {y:[2]}]})\n\ \n\ - id: 4\n\ TestScript1_custom:\n\ object: {\n\ \"c\": 1\n\ }\n\ \n" ); std::stringstream stateStream; TS_ASSERT(man.SerializeState(stateStream)); CSimContext context2; CComponentManager man2(context2, g_ScriptRuntime); man2.LoadComponentTypes(); TS_ASSERT(man2.LoadScript(L"simulation/components/test-serialize.js")); TS_ASSERT(man2.QueryInterface(ent1, IID_Test1) == NULL); TS_ASSERT(man2.DeserializeState(stateStream)); TS_ASSERT_EQUALS(static_cast (man2.QueryInterface(ent1, IID_Test1))->GetX(), 1234); { TestLogger log; // swallow warnings about this.entity being read-only TS_ASSERT_EQUALS(static_cast (man2.QueryInterface(ent2, IID_Test1))->GetX(), (int)ent2); } TS_ASSERT_EQUALS(static_cast (man2.QueryInterface(ent3, IID_Test1))->GetX(), 12); } void test_script_serialization_errors() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-serialize.js")); entity_id_t ent1 = 1; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CParamNode noParam; man.AddComponent(hnd1, man.LookupCID("TestScript1_getter"), noParam); std::stringstream stateStream; TS_ASSERT_THROWS_PSERROR(man.SerializeState(stateStream), PSERROR_Serialize_ScriptError, "Cannot serialize property getters"); // (The script will die if the getter is executed) } void test_script_serialization_template() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-serialize.js")); man.InitSystemEntity(); entity_id_t ent2 = 2; CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CParamNode noParam; // The template manager takes care of reloading templates on deserialization, // so we need to use it here TS_ASSERT(man.AddComponent(man.GetSystemEntity(), CID_TemplateManager, noParam)); ICmpTemplateManager* tempMan = static_cast (man.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager)); const CParamNode* testParam = tempMan->LoadTemplate(ent2, "template-serialize"); man.AddComponent(hnd2, man.LookupCID("TestScript1_consts"), testParam->GetChild("TestScript1_consts")); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 12347); std::stringstream stateStream; TS_ASSERT(man.SerializeState(stateStream)); CSimContext context2; CComponentManager man2(context2, g_ScriptRuntime); man2.LoadComponentTypes(); TS_ASSERT(man2.LoadScript(L"simulation/components/test-serialize.js")); TS_ASSERT(man2.DeserializeState(stateStream)); TS_ASSERT_EQUALS(static_cast (man2.QueryInterface(ent2, IID_Test1))->GetX(), 12347); } void test_dynamic_subscription() { CSimContext context; CComponentManager man(context, g_ScriptRuntime); man.LoadComponentTypes(); entity_id_t ent1 = 1; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CParamNode noParam; man.AddComponent(hnd1, CID_Test1A, noParam); man.AddComponent(hnd1, CID_Test2A, noParam); man.DynamicSubscriptionNonsync(MT_RenderSubmit, man.QueryInterface(ent1, IID_Test1), true); man.DynamicSubscriptionNonsync(MT_RenderSubmit, man.QueryInterface(ent1, IID_Test2), true); man.DestroyComponentsSoon(ent1); man.FlushDestroyedComponents(); } }; Index: ps/trunk/source/simulation2/tests/test_Serializer.h =================================================================== --- ps/trunk/source/simulation2/tests/test_Serializer.h (revision 20638) +++ ps/trunk/source/simulation2/tests/test_Serializer.h (revision 20639) @@ -1,886 +1,886 @@ /* Copyright (C) 2017 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "lib/self_test.h" #include "simulation2/serialization/DebugSerializer.h" #include "simulation2/serialization/HashSerializer.h" #include "simulation2/serialization/StdSerializer.h" #include "simulation2/serialization/StdDeserializer.h" #include "scriptinterface/ScriptInterface.h" #include "graphics/MapReader.h" #include "graphics/Terrain.h" #include "graphics/TerrainTextureManager.h" #include "lib/timer.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Loader.h" #include "ps/XML/Xeromyces.h" #include "simulation2/Simulation2.h" #include "callgrind.h" #include #define TS_ASSERT_STREAM(stream, len, buffer) \ TS_ASSERT_EQUALS(stream.str().length(), (size_t)len); \ TS_ASSERT_SAME_DATA(stream.str().data(), buffer, len) #define TSM_ASSERT_STREAM(m, stream, len, buffer) \ TSM_ASSERT_EQUALS(m, stream.str().length(), (size_t)len); \ TSM_ASSERT_SAME_DATA(m, stream.str().data(), buffer, len) class TestSerializer : public CxxTest::TestSuite { public: void serialize_types(ISerializer& serialize) { serialize.NumberI8_Unbounded("i8", (signed char)-123); serialize.NumberU8_Unbounded("u8", (unsigned char)255); serialize.NumberI16_Unbounded("i16", -12345); serialize.NumberU16_Unbounded("u16", 56789); serialize.NumberI32_Unbounded("i32", -123); serialize.NumberU32_Unbounded("u32", (unsigned)-123); serialize.NumberFloat_Unbounded("float", 1e+30f); serialize.NumberDouble_Unbounded("double", 1e+300); serialize.NumberFixed_Unbounded("fixed", fixed::FromFloat(1234.5f)); serialize.Bool("t", true); serialize.Bool("f", false); serialize.StringASCII("string", "example", 0, 255); serialize.StringASCII("string 2", "example\"\\\"", 0, 255); serialize.StringASCII("string 3", "example\n\ntest", 0, 255); wchar_t testw[] = { 't', 0xEA, 's', 't', 0 }; serialize.String("string 4", testw, 0, 255); serialize.RawBytes("raw bytes", (const u8*)"\0\1\2\3\x0f\x10", 6); } void test_Debug_basic() { ScriptInterface script("Test", "Test", g_ScriptRuntime); std::stringstream stream; CDebugSerializer serialize(script, stream); serialize.NumberI32_Unbounded("x", -123); serialize.NumberU32_Unbounded("y", 1234); serialize.NumberI32("z", 12345, 0, 65535); TS_ASSERT_STR_EQUALS(stream.str(), "x: -123\ny: 1234\nz: 12345\n"); } void test_Debug_floats() { ScriptInterface script("Test", "Test", g_ScriptRuntime); std::stringstream stream; CDebugSerializer serialize(script, stream); serialize.NumberFloat_Unbounded("x", 1e4f); serialize.NumberFloat_Unbounded("x", 1e-4f); serialize.NumberFloat_Unbounded("x", 1e5f); serialize.NumberFloat_Unbounded("x", 1e-5f); serialize.NumberFloat_Unbounded("x", 1e6f); serialize.NumberFloat_Unbounded("x", 1e-6f); serialize.NumberFloat_Unbounded("x", 1e10f); serialize.NumberFloat_Unbounded("x", 1e-10f); serialize.NumberDouble_Unbounded("x", 1e4); serialize.NumberDouble_Unbounded("x", 1e-4); serialize.NumberDouble_Unbounded("x", 1e5); serialize.NumberDouble_Unbounded("x", 1e-5); serialize.NumberDouble_Unbounded("x", 1e6); serialize.NumberDouble_Unbounded("x", 1e-6); serialize.NumberDouble_Unbounded("x", 1e10); serialize.NumberDouble_Unbounded("x", 1e-10); serialize.NumberDouble_Unbounded("x", 1e100); serialize.NumberDouble_Unbounded("x", 1e-100); serialize.NumberFixed_Unbounded("x", fixed::FromDouble(1e4)); TS_ASSERT_STR_EQUALS(stream.str(), "x: 10000\nx: 9.9999997e-05\nx: 100000\nx: 9.9999997e-06\nx: 1000000\nx: 1e-06\nx: 1e+10\nx: 1e-10\n" "x: 10000\nx: 0.0001\nx: 100000\nx: 1.0000000000000001e-05\nx: 1000000\nx: 9.9999999999999995e-07\nx: 10000000000\nx: 1e-10\nx: 1e+100\nx: 1e-100\n" "x: 10000\n" ); } void test_Debug_types() { ScriptInterface script("Test", "Test", g_ScriptRuntime); std::stringstream stream; CDebugSerializer serialize(script, stream); serialize.Comment("comment"); serialize_types(serialize); TS_ASSERT_STR_EQUALS(stream.str(), "# comment\n" "i8: -123\n" "u8: 255\n" "i16: -12345\n" "u16: 56789\n" "i32: -123\n" "u32: 4294967173\n" "float: 1e+30\n" "double: 1.0000000000000001e+300\n" "fixed: 1234.5\n" "t: true\n" "f: false\n" "string: \"example\"\n" "string 2: \"example\\\"\\\\\\\"\"\n" // C-escaped form of: "example\"\\\"" "string 3: \"example\\n\\ntest\"\n" "string 4: \"t\xC3\xAAst\"\n" "raw bytes: (6 bytes) 00 01 02 03 0f 10\n" ); } void test_Std_basic() { ScriptInterface script("Test", "Test", g_ScriptRuntime); std::stringstream stream; CStdSerializer serialize(script, stream); serialize.NumberI32_Unbounded("x", -123); serialize.NumberU32_Unbounded("y", 1234); serialize.NumberI32("z", 12345, 0, 65535); TS_ASSERT_STREAM(stream, 12, "\x85\xff\xff\xff" "\xd2\x04\x00\x00" "\x39\x30\x00\x00"); CStdDeserializer deserialize(script, stream); int32_t n; deserialize.NumberI32_Unbounded("x", n); TS_ASSERT_EQUALS(n, -123); deserialize.NumberI32_Unbounded("y", n); TS_ASSERT_EQUALS(n, 1234); deserialize.NumberI32("z", n, 0, 65535); TS_ASSERT_EQUALS(n, 12345); // NOTE: Don't use good() here - it fails due to a bug in older libc++ versions TS_ASSERT(!stream.bad() && !stream.fail()); TS_ASSERT_EQUALS(stream.peek(), EOF); } void test_Std_types() { ScriptInterface script("Test", "Test", g_ScriptRuntime); std::stringstream stream; CStdSerializer serialize(script, stream); serialize_types(serialize); CStdDeserializer deserialize(script, stream); int8_t i8v; uint8_t u8v; int16_t i16v; uint16_t u16v; int32_t i32v; uint32_t u32v; float flt; double dbl; fixed fxd; bool bl; std::string str; std::wstring wstr; u8 cbuf[256]; deserialize.NumberI8_Unbounded("i8", i8v); TS_ASSERT_EQUALS(i8v, -123); deserialize.NumberU8_Unbounded("u8", u8v); TS_ASSERT_EQUALS(u8v, 255); deserialize.NumberI16_Unbounded("i16", i16v); TS_ASSERT_EQUALS(i16v, -12345); deserialize.NumberU16_Unbounded("u16", u16v); TS_ASSERT_EQUALS(u16v, 56789); deserialize.NumberI32_Unbounded("i32", i32v); TS_ASSERT_EQUALS(i32v, -123); deserialize.NumberU32_Unbounded("u32", u32v); TS_ASSERT_EQUALS(u32v, 4294967173u); deserialize.NumberFloat_Unbounded("float", flt); TS_ASSERT_EQUALS(flt, 1e+30f); deserialize.NumberDouble_Unbounded("double", dbl); TS_ASSERT_EQUALS(dbl, 1e+300); deserialize.NumberFixed_Unbounded("fixed", fxd); TS_ASSERT_EQUALS(fxd.ToDouble(), 1234.5); deserialize.Bool("t", bl); TS_ASSERT_EQUALS(bl, true); deserialize.Bool("f", bl); TS_ASSERT_EQUALS(bl, false); deserialize.StringASCII("string", str, 0, 255); TS_ASSERT_STR_EQUALS(str, "example"); deserialize.StringASCII("string 2", str, 0, 255); TS_ASSERT_STR_EQUALS(str, "example\"\\\""); deserialize.StringASCII("string 3", str, 0, 255); TS_ASSERT_STR_EQUALS(str, "example\n\ntest"); wchar_t testw[] = { 't', 0xEA, 's', 't', 0 }; deserialize.String("string 4", wstr, 0, 255); TS_ASSERT_WSTR_EQUALS(wstr, testw); cbuf[6] = 0x42; // sentinel deserialize.RawBytes("raw bytes", cbuf, 6); TS_ASSERT_SAME_DATA(cbuf, (const u8*)"\0\1\2\3\x0f\x10\x42", 7); // NOTE: Don't use good() here - it fails due to a bug in older libc++ versions TS_ASSERT(!stream.bad() && !stream.fail()); TS_ASSERT_EQUALS(stream.peek(), EOF); } void test_Hash_basic() { ScriptInterface script("Test", "Test", g_ScriptRuntime); CHashSerializer serialize(script); serialize.NumberI32_Unbounded("x", -123); serialize.NumberU32_Unbounded("y", 1234); serialize.NumberI32("z", 12345, 0, 65535); TS_ASSERT_EQUALS(serialize.GetHashLength(), (size_t)16); TS_ASSERT_SAME_DATA(serialize.ComputeHash(), "\xa0\x3a\xe5\x3e\x9b\xd7\xfb\x11\x88\x35\xc6\xfb\xb9\x94\xa9\x72", 16); // echo -en "\x85\xff\xff\xff\xd2\x04\x00\x00\x39\x30\x00\x00" | openssl md5 -binary | xxd -p | perl -pe 's/(..)/\\x$1/g' } void test_Hash_stream() { ScriptInterface script("Test", "Test", g_ScriptRuntime); CHashSerializer hashSerialize(script); hashSerialize.NumberI32_Unbounded("x", -123); hashSerialize.NumberU32_Unbounded("y", 1234); hashSerialize.NumberI32("z", 12345, 0, 65535); ISerializer& serialize = hashSerialize; { CStdSerializer streamSerialize(script, serialize.GetStream()); streamSerialize.NumberI32_Unbounded("x2", -456); streamSerialize.NumberU32_Unbounded("y2", 5678); streamSerialize.NumberI32("z2", 45678, 0, 65535); } TS_ASSERT_EQUALS(hashSerialize.GetHashLength(), (size_t)16); TS_ASSERT_SAME_DATA(hashSerialize.ComputeHash(), "\x5c\xff\x33\xd1\x72\xdd\x6d\x77\xa8\xd4\xa1\xf6\x84\xcc\xaa\x10", 16); // echo -en "\x85\xff\xff\xff\xd2\x04\x00\x00\x39\x30\x00\x00\x38\xfe\xff\xff\x2e\x16\x00\x00\x6e\xb2\x00\x00" | openssl md5 -binary | xxd -p | perl -pe 's/(..)/\\x$1/g' } void test_bounds() { ScriptInterface script("Test", "Test", g_ScriptRuntime); std::stringstream stream; CDebugSerializer serialize(script, stream); serialize.NumberI32("x", 16, -16, 16); serialize.NumberI32("x", -16, -16, 16); TS_ASSERT_THROWS(serialize.NumberI32("x", 17, -16, 16), PSERROR_Serialize_OutOfBounds); TS_ASSERT_THROWS(serialize.NumberI32("x", -17, -16, 16), PSERROR_Serialize_OutOfBounds); } // TODO: test exceptions more thoroughly void helper_script_roundtrip(const char* msg, const char* input, const char* expected, size_t expstreamlen = 0, const char* expstream = NULL, const char* debug = NULL) { ScriptInterface script("Test", "Test", g_ScriptRuntime); JSContext* cx = script.GetContext(); JSAutoRequest rq(cx); JS::RootedValue obj(cx); TSM_ASSERT(msg, script.Eval(input, &obj)); if (debug) { std::stringstream dbgstream; CDebugSerializer serialize(script, dbgstream); serialize.ScriptVal("script", &obj); TS_ASSERT_STR_EQUALS(dbgstream.str(), debug); } std::stringstream stream; CStdSerializer serialize(script, stream); serialize.ScriptVal("script", &obj); if (expstream) { TSM_ASSERT_STREAM(msg, stream, expstreamlen, expstream); } CStdDeserializer deserialize(script, stream); JS::RootedValue newobj(cx); deserialize.ScriptVal("script", &newobj); // NOTE: Don't use good() here - it fails due to a bug in older libc++ versions TSM_ASSERT(msg, !stream.bad() && !stream.fail()); TSM_ASSERT_EQUALS(msg, stream.peek(), EOF); std::string source; TSM_ASSERT(msg, script.CallFunction(newobj, "toSource", source)); TS_ASSERT_STR_EQUALS(source, expected); } void test_script_basic() { helper_script_roundtrip("Object", "({'x': 123, 'y': [1, 1.5, '2', 'test', undefined, null, true, false]})", /* expected: */ "({x:123, y:[1, 1.5, \"2\", \"test\", (void 0), null, true, false]})", /* expected stream: */ 116, "\x03" // SCRIPT_TYPE_OBJECT "\x02\0\0\0" // num props "\x01\x01\0\0\0" "x" // "x" "\x05" // SCRIPT_TYPE_INT "\x7b\0\0\0" // 123 "\x01\x01\0\0\0" "y" // "y" "\x02" // SCRIPT_TYPE_ARRAY "\x08\0\0\0" // array length "\x08\0\0\0" // num props "\x01\x01\0\0\0" "0" // "0" "\x05" "\x01\0\0\0" // SCRIPT_TYPE_INT 1 "\x01\x01\0\0\0" "1" // "1" "\x06" "\0\0\0\0\0\0\xf8\x3f" // SCRIPT_TYPE_DOUBLE 1.5 "\x01\x01\0\0\0" "2" // "2" "\x04" "\x01\x01\0\0\0" "2" // SCRIPT_TYPE_STRING "2" "\x01\x01\0\0\0" "3" // "3" "\x04" "\x01\x04\0\0\0" "test" // SCRIPT_TYPE_STRING "test" "\x01\x01\0\0\0" "4" // "4" "\x00" // SCRIPT_TYPE_VOID "\x01\x01\0\0\0" "5" // "5" "\x01" // SCRIPT_TYPE_NULL "\x01\x01\0\0\0" "6" // "6" "\x07" "\x01" // SCRIPT_TYPE_BOOLEAN true "\x01\x01\0\0\0" "7" // "7" "\x07" "\x00", // SCRIPT_TYPE_BOOLEAN false /* expected debug: */ "script: {\n" " \"x\": 123,\n" " \"y\": [\n" " 1,\n" " 1.5,\n" " \"2\",\n" " \"test\",\n" " null,\n" " null,\n" " true,\n" " false\n" " ]\n" "}\n" ); } void test_script_unicode() { helper_script_roundtrip("unicode", "({" "'x': \"\\x01\\x80\\xff\\u0100\\ud7ff\", " "'y': \"\\ue000\\ufffd\"" "})", /* expected: */ "({" "x:\"\\x01\\x80\\xFF\\u0100\\uD7FF\", " "y:\"\\uE000\\uFFFD\"" "})"); // Disabled since we no longer do the UTF-8 conversion that rejects invalid characters // TS_ASSERT_THROWS(helper_script_roundtrip("invalid chars 1", "(\"\\ud7ff\\ud800\")", "..."), PSERROR_Serialize_InvalidCharInString); // TS_ASSERT_THROWS(helper_script_roundtrip("invalid chars 2", "(\"\\udfff\")", "..."), PSERROR_Serialize_InvalidCharInString); // TS_ASSERT_THROWS(helper_script_roundtrip("invalid chars 3", "(\"\\uffff\")", "..."), PSERROR_Serialize_InvalidCharInString); // TS_ASSERT_THROWS(helper_script_roundtrip("invalid chars 4", "(\"\\ud800\\udc00\")" /* U+10000 */, "..."), PSERROR_Serialize_InvalidCharInString); helper_script_roundtrip("unicode", "\"\\ud800\\uffff\"", "(new String(\"\\uD800\\uFFFF\"))"); } void test_script_objects() { helper_script_roundtrip("Number", "[1, new Number('2.0'), 3]", "[1, (new Number(2)), 3]"); helper_script_roundtrip("Number with props", "var n=new Number('2.0'); n.foo='bar'; n", "(new Number(2))"); helper_script_roundtrip("String", "['test1', new String('test2'), 'test3']", "[\"test1\", (new String(\"test2\")), \"test3\"]"); helper_script_roundtrip("String with props", "var s=new String('test'); s.foo='bar'; s", "(new String(\"test\"))"); helper_script_roundtrip("Boolean", "[new Boolean('true'), false]", "[(new Boolean(true)), false]"); helper_script_roundtrip("Boolean with props", "var b=new Boolean('true'); b.foo='bar'; b", "(new Boolean(true))"); } void test_script_objects_properties() { helper_script_roundtrip("Object with null in prop name", "({\"foo\\0bar\":1})", "({\'foo\\x00bar\':1})"); } void test_script_typed_arrays_simple() { helper_script_roundtrip("Int8Array", "var arr=new Int8Array(8);" "for(var i=0; iMount(L"", DataDir()/"mods"/"public", VFS_MOUNT_MUST_EXIST)); TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir()/"_testcache")); // Need some stuff for terrain movement costs: // (TODO: this ought to be independent of any graphics code) new CTerrainTextureManager; g_TexMan.LoadTerrainTextures(); CTerrain terrain; CSimulation2 sim2(NULL, g_ScriptRuntime, &terrain); sim2.LoadDefaultScripts(); sim2.ResetState(); std::unique_ptr mapReader(new CMapReader()); LDR_BeginRegistering(); mapReader->LoadMap(L"maps/skirmishes/Greek Acropolis (2).pmp", sim2.GetScriptInterface().GetJSRuntime(), JS::UndefinedHandleValue, &terrain, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &sim2, &sim2.GetSimContext(), -1, false); LDR_EndRegistering(); TS_ASSERT_OK(LDR_NonprogressiveLoad()); sim2.Update(0); { std::stringstream str; std::string hash; sim2.SerializeState(str); sim2.ComputeStateHash(hash, false); debug_printf("\n"); debug_printf("# size = %d\n", (int)str.str().length()); debug_printf("# hash = "); for (size_t i = 0; i < hash.size(); ++i) debug_printf("%02x", (unsigned int)(u8)hash[i]); debug_printf("\n"); } double t = timer_Time(); CALLGRIND_START_INSTRUMENTATION; size_t reps = 128; for (size_t i = 0; i < reps; ++i) { std::string hash; sim2.ComputeStateHash(hash, false); } CALLGRIND_STOP_INSTRUMENTATION; t = timer_Time() - t; debug_printf("# time = %f (%f/%d)\n", t/reps, t, (int)reps); // Shut down the world delete &g_TexMan; g_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); CXeromyces::Terminate(); } }; Index: ps/trunk/source/simulation2/tests/test_Simulation2.h =================================================================== --- ps/trunk/source/simulation2/tests/test_Simulation2.h (revision 20638) +++ ps/trunk/source/simulation2/tests/test_Simulation2.h (revision 20639) @@ -1,167 +1,167 @@ -/* Copyright (C) 2010 Wildfire Games. +/* Copyright (C) 2017 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 "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" #include "simulation2/MessageTypes.h" #include "simulation2/components/ICmpTest.h" #include "graphics/Terrain.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/XML/Xeromyces.h" class TestSimulation2 : public CxxTest::TestSuite { void copyFile(const VfsPath& src, const VfsPath& dst) { 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)); } CTerrain m_Terrain; public: void setUp() { - g_VFS = CreateVfs(20 * MiB); + g_VFS = CreateVfs(); TS_ASSERT_OK(g_VFS->Mount(L"", DataDir()/"mods"/"_test.sim", VFS_MOUNT_MUST_EXIST)); TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir()/"_testcache")); CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); g_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); } void test_AddEntity() { CSimulation2 sim(NULL, g_ScriptRuntime, &m_Terrain); TS_ASSERT(sim.LoadScripts(L"simulation/components/addentity/")); sim.ResetState(true, true); entity_id_t ent1 = sim.AddEntity(L"test1"); TS_ASSERT_EQUALS(ent1, (u32)2); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent1, IID_Test1))->GetX(), 999); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent1, IID_Test2))->GetX(), 12345); entity_id_t ent2 = sim.AddEntity(L"test1-inherit"); TS_ASSERT_EQUALS(ent2, (u32)3); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent2, IID_Test1))->GetX(), 1234); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent2, IID_Test2))->GetX(), 12345); } void test_DestroyEntity() { CSimulation2 sim(NULL, g_ScriptRuntime, &m_Terrain); TS_ASSERT(sim.LoadScripts(L"simulation/components/addentity/")); sim.ResetState(true, true); entity_id_t ent1 = sim.AddEntity(L"test1"); entity_id_t ent2 = sim.AddEntity(L"test1"); entity_id_t ent3 = sim.AddEntity(L"test1"); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent1, IID_Test1))->GetX(), 999); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent1, IID_Test2))->GetX(), 12345); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent2, IID_Test1))->GetX(), 999); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent2, IID_Test2))->GetX(), 12345); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent3, IID_Test1))->GetX(), 999); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent3, IID_Test2))->GetX(), 12345); sim.DestroyEntity(ent2); // mark it for deletion TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent2, IID_Test1))->GetX(), 999); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent2, IID_Test2))->GetX(), 12345); sim.FlushDestroyedEntities(); // actually delete it TS_ASSERT(sim.QueryInterface(ent2, IID_Test1) == NULL); TS_ASSERT(sim.QueryInterface(ent2, IID_Test2) == NULL); sim.FlushDestroyedEntities(); // nothing in the queue sim.DestroyEntity(ent2); sim.FlushDestroyedEntities(); // already deleted // Other entities weren't affected TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent1, IID_Test1))->GetX(), 999); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent1, IID_Test2))->GetX(), 12345); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent3, IID_Test1))->GetX(), 999); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent3, IID_Test2))->GetX(), 12345); sim.DestroyEntity(ent3); // mark it for deletion twice sim.DestroyEntity(ent3); sim.FlushDestroyedEntities(); TS_ASSERT(sim.QueryInterface(ent3, IID_Test1) == NULL); TS_ASSERT(sim.QueryInterface(ent3, IID_Test2) == NULL); // Messages mustn't get sent to the destroyed components (else we'll crash) CMessageTurnStart msg; sim.BroadcastMessage(msg); } void test_hotload_scripts() { CSimulation2 sim(NULL, g_ScriptRuntime, &m_Terrain); TS_ASSERT_OK(CreateDirectories(DataDir()/"mods"/"_test.sim"/"simulation"/"components"/"hotload"/"", 0700)); copyFile(L"simulation/components/test-hotload1.js", L"simulation/components/hotload/hotload.js"); TS_ASSERT_OK(g_VFS->RemoveFile(L"simulation/components/hotload/hotload.js")); TS_ASSERT_OK(g_VFS->RepopulateDirectory(L"simulation/components/hotload/")); TS_ASSERT(sim.LoadScripts(L"simulation/components/hotload/")); sim.ResetState(true, true); entity_id_t ent = sim.AddEntity(L"hotload"); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent, IID_Test1))->GetX(), 100); copyFile(L"simulation/components/test-hotload2.js", L"simulation/components/hotload/hotload.js"); TS_ASSERT_OK(g_VFS->RemoveFile(L"simulation/components/hotload/hotload.js")); TS_ASSERT_OK(g_VFS->RepopulateDirectory(L"simulation/components/hotload/")); TS_ASSERT_OK(sim.ReloadChangedFile(L"art/irrelevant.xml")); TS_ASSERT_OK(sim.ReloadChangedFile(L"simulation/components/irrelevant.js")); TS_ASSERT_OK(sim.ReloadChangedFile(L"simulation/components/hotload/nonexistent.js")); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent, IID_Test1))->GetX(), 100); TS_ASSERT_OK(sim.ReloadChangedFile(L"simulation/components/hotload/hotload.js")); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent, IID_Test1))->GetX(), 1000); TS_ASSERT_OK(DeleteDirectory(DataDir()/"mods"/"_test.sim"/"simulation"/"components"/"hotload"/"")); TS_ASSERT_OK(g_VFS->RemoveFile(L"simulation/components/hotload/hotload.js")); TS_ASSERT_OK(g_VFS->RepopulateDirectory(L"simulation/components/hotload/")); TS_ASSERT_OK(sim.ReloadChangedFile(L"simulation/components/hotload/hotload.js")); } };