Index: ps/trunk/binaries/data/config/dev.cfg =================================================================== --- ps/trunk/binaries/data/config/dev.cfg (nonexistent) +++ ps/trunk/binaries/data/config/dev.cfg (revision 13472) @@ -0,0 +1,11 @@ +; +; Developer configuration file +; +; NOTE: This is not read by the config system currently. +; +; This file is used by the engine to decide which mods are mounted, and in what +; order. The precense of this file suppresses loading of mods in the user mod +; path (see http://trac.wildfiregames.com/wiki/GameDataPaths) if the same mod is +; present in binaries/data/mods. It also implies -noUserMod. +; This is done to make saved maps end up in the right mod folder (and in the +; game-relative data path) to commit them later on. Index: ps/trunk/build/workspaces/build-osx-bundle.sh =================================================================== --- ps/trunk/build/workspaces/build-osx-bundle.sh (revision 13471) +++ ps/trunk/build/workspaces/build-osx-bundle.sh (revision 13472) @@ -1,192 +1,196 @@ #!/bin/sh # # This script will build an OSX app bundle for 0 A.D. # # App bundles are intended to be self-contained and portable. # An SDK is required, usually included with Xcode. The SDK ensures # that only those system libraries are used which are available on # the chosen target and compatible systems. # # Steps to build a 0 A.D. bundle are: # 1. confirm ARCH is set to desired target architecture # 2. confirm SYSROOT points to the correct target SDK # 3. confirm MIN_OSX_VERSION matches the target OS X version # 4. update BUNDLE_VERSION to match current 0 A.D. version # 5. if building 32-bit 10.5 bundle, read the accompanying documentation # 6. run this script # # Force build architecture, as sometimes environment is broken. # For a universal fat binary, the approach would be to build every # dependency with both archs and combine them with lipo, then do the # same thing with the game itself. # Choices are "x86_64" or "i386" (ppc and ppc64 not supported) export ARCH=${ARCH:="x86_64"} # Set SDK and mimimum required OSX version # (As of Xcode 4.3, the SDKs are located directly in Xcode.app, # but previously they were in /Developer/SDKs) # TODO: we could get this from xcode-select but the user must set that up #export SYSROOT=${SYSROOT="/Developer/SDKs/MacOSX10.5.sdk"} export SYSROOT=${SYSROOT="/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.7.sdk"} export MIN_OSX_VERSION=${MIN_OSX_VERSION="10.6"} # 0 A.D. release version, e.g. Alpha 12 is 0.0.12 BUNDLE_VERSION=${BUNDLE_VERSION:="0.0.0"} # Define compiler as "gcc" (in case anything expects e.g. gcc-4.2) # On newer OS X versions, this will be a symbolic link to LLVM GCC # TODO: don't rely on that export CC=${CC:="gcc"} CXX=${CXX:="g++"} die() { echo ERROR: $* exit 1 } # Check that we're actually on OS X if [ "`uname -s`" != "Darwin" ]; then die "This script is intended for OS X only" fi # Check SDK exists if [ ! -d "$SYSROOT" ]; then die "$SYSROOT does not exist! You probably need to install Xcode" fi cd "$(dirname $0)" # Now in build/workspaces/ (where we assume this script resides) BUNDLE_OUTPUT=${BUNDLE_OUTPUT:="$(pwd)/0ad.app"} BUNDLE_CONTENTS=$BUNDLE_OUTPUT/Contents BUNDLE_BIN=$BUNDLE_CONTENTS/MacOS BUNDLE_RESOURCES=$BUNDLE_CONTENTS/Resources BUNDLE_FRAMEWORKS=$BUNDLE_CONTENTS/Frameworks BUNDLE_PLUGINS=$BUNDLE_CONTENTS/PlugIns BUNDLE_SHAREDSUPPORT=$BUNDLE_CONTENTS/SharedSupport # Unique identifier string for this bundle (reverse-DNS style) BUNDLE_IDENTIFIER=${BUNDLE_IDENTIFIER:="com.wildfiregames.0ad"} # Minimum version of OSX on which the bundle will run BUNDLE_MIN_OSX_VERSION="${MIN_OSX_VERSION}.0" JOBS=${JOBS:="-j5"} # Parse command-line options: for i in "$@" do case $i in -j* ) JOBS=$i ;; esac done # TODO: Do we really want to regenerate everything? (consider if one task fails) # Build libraries against SDK echo "\nBuilding libaries\n" pushd ../../libraries/osx ./build-osx-libs.sh $JOBS --force-rebuild || die "Libraries build script failed" popd # Clean and update workspaces echo "\nGenerating workspaces\n" # Pass OSX options through to Premake (./clean-workspaces.sh && SYSROOT="$SYSROOT" MIN_OSX_VERSION="$MIN_OSX_VERSION" ./update-workspaces.sh --macosx-bundle="$BUNDLE_IDENTIFIER" --sysroot="$SYSROOT" --macosx-version-min="$MIN_OSX_VERSION") || die "update-workspaces.sh failed!" # Get SVN revision and update svn_revision.txt SVNREV=`svnversion -n .` echo L\"${SVNREV}-release\" > ../svn_revision/svn_revision.txt pushd gcc echo "\nBuilding game\n" (make clean && CC="$CC -arch $ARCH" CXX="$CXX -arch $ARCH" make ${JOBS}) || die "Game build failed!" popd # TODO: This is yucky - should we first export our working copy, like source\tools\dist\build.sh? svn revert ../svn_revision/svn_revision.txt # Create bundle structure echo "\nCreating bundle directories\n" rm -rf ${BUNDLE_OUTPUT} mkdir -p ${BUNDLE_BIN} mkdir -p ${BUNDLE_FRAMEWORKS} mkdir -p ${BUNDLE_PLUGINS} mkdir -p ${BUNDLE_RESOURCES} mkdir -p ${BUNDLE_SHAREDSUPPORT} pushd ../../binaries/system # Run test to confirm all is OK echo "\nRunning tests\n" ./test || die "Test(s) failed!" # Build archive(s) - don't archive the _test.* mods pushd ../data/mods archives="" for modname in [a-zA-Z0-9]* do archives="${archives} ${modname}" done popd for modname in $archives do echo "\nBuilding archive for '${modname}'\n" ARCHIVEBUILD_INPUT="$(pwd)/../data/mods/${modname}" ARCHIVEBUILD_OUTPUT="${BUNDLE_RESOURCES}/data/mods/${modname}" # For some reason the output directory has to exist? mkdir -p ${ARCHIVEBUILD_OUTPUT} (./pyrogenesis -archivebuild=${ARCHIVEBUILD_INPUT} -archivebuild-output=${ARCHIVEBUILD_OUTPUT}/${modname}.zip) || die "Archive build for '${modname}' failed!" done popd # Copy binaries echo "\nCopying binaries\n" # Only pyrogenesis for now, until we find a way to load # multiple binaries from one app bundle # TODO: Would be nicer if we could set this path in premake cp ../../binaries/system/pyrogenesis ${BUNDLE_BIN} # Copy libs echo "\nCopying libs\n" # TODO: Should probably make it so these libs get built in place cp -v ../../binaries/system/libAtlasUI.dylib ${BUNDLE_FRAMEWORKS} cp -v ../../binaries/system/libCollada.dylib ${BUNDLE_FRAMEWORKS} # Copy data echo "\nCopying non-archived game data\n" +# Removing it now and restoring it later, cp has no exclusion switch +# and using find is a bit over-the-top +rm ../../binaries/data/config/dev.cfg cp -v ../resources/0ad.icns ${BUNDLE_RESOURCES} cp -R -v ../../binaries/data/config ${BUNDLE_RESOURCES}/data/ cp -R -v ../../binaries/data/tools ${BUNDLE_RESOURCES}/data/ +svn revert ../../binaries/data/config/dev.cfg # Copy license/readmes # TODO: Also want copies in the DMG - decide on layout echo "\nCopying readmes\n" cp -v ../../*.txt ${BUNDLE_RESOURCES} # Create Info.plist echo "\nCreating Info.plist\n" alias PlistBuddy=/usr/libexec/PlistBuddy INFO_PLIST="${BUNDLE_CONTENTS}/Info.plist" PlistBuddy -c "Add :CFBundleName string 0 A.D." ${INFO_PLIST} PlistBuddy -c "Add :CFBundleIdentifier string ${BUNDLE_IDENTIFIER}" ${INFO_PLIST} PlistBuddy -c "Add :CFBundleVersion string ${BUNDLE_VERSION}" ${INFO_PLIST} PlistBuddy -c "Add :CFBundlePackageType string APPL" ${INFO_PLIST} PlistBuddy -c "Add :CFBundleSignature string none" ${INFO_PLIST} PlistBuddy -c "Add :CFBundleExecutable string pyrogenesis" ${INFO_PLIST} PlistBuddy -c "Add :CFBundleShortVersionString string ${BUNDLE_VERSION}" ${INFO_PLIST} PlistBuddy -c "Add :CFBundleDevelopmentRegion string English" ${INFO_PLIST} PlistBuddy -c "Add :CFBundleInfoDictionaryVersion string 6.0" ${INFO_PLIST} PlistBuddy -c "Add :CFBundleIconFile string 0ad" ${INFO_PLIST} PlistBuddy -c "Add :LSMinimumSystemVersion string ${BUNDLE_MIN_OSX_VERSION}" ${INFO_PLIST} PlistBuddy -c "Add :NSHumanReadableCopyright string Copyright © 2013 Wildfire Games" ${INFO_PLIST} # TODO: Automatically create compressed DMG with hditutil? # (this is a bit complicated so I do it manually for now) # (also we need to have good icon placement, background image, etc) echo "\nBundle complete! Located in ${BUNDLE_OUTPUT}" Index: ps/trunk/source/lib/file/vfs/vfs.cpp =================================================================== --- ps/trunk/source/lib/file/vfs/vfs.cpp (revision 13471) +++ ps/trunk/source/lib/file/vfs/vfs.cpp (revision 13472) @@ -1,319 +1,319 @@ /* Copyright (c) 2013 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; struct ScopedLock { ScopedLock() { pthread_mutex_lock(&vfs_mutex); } ~ScopedLock() { pthread_mutex_unlock(&vfs_mutex); } }; class VFS : public IVFS { public: VFS(size_t cacheSize) : m_cacheSize(cacheSize), m_fileCache(m_cacheSize) , 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, FileInfo* pfileInfo) const { ScopedLock s; 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 = FileInfo(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, FileInfos* fileInfos, DirectoryNames* subdirectoryNames) const { ScopedLock s; VfsDirectory* directory; WARN_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(FileInfo(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; - WARN_RETURN_STATUS_IF_ERR(vfs_Lookup(pathname, &m_rootDirectory, directory, 0, VFS_LOOKUP_ADD|VFS_LOOKUP_CREATE)); + WARN_RETURN_STATUS_IF_ERR(vfs_Lookup(pathname, &m_rootDirectory, directory, 0, VFS_LOOKUP_ADD|VFS_LOOKUP_CREATE|VFS_LOOKUP_CREATE_ALWAYS)); 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); } else 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())); } } } 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) { return PIVFS(new VFS(cacheSize)); } Index: ps/trunk/source/lib/file/vfs/vfs.h =================================================================== --- ps/trunk/source/lib/file/vfs/vfs.h (revision 13471) +++ ps/trunk/source/lib/file/vfs/vfs.h (revision 13472) @@ -1,231 +1,240 @@ /* Copyright (c) 2013 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" // FileInfo #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 + 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, FileInfo* 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, FileInfos* 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. **/ 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); #endif // #ifndef INCLUDED_VFS Index: ps/trunk/source/lib/file/vfs/vfs_lookup.cpp =================================================================== --- ps/trunk/source/lib/file/vfs/vfs_lookup.cpp (revision 13471) +++ ps/trunk/source/lib/file/vfs/vfs_lookup.cpp (revision 13472) @@ -1,143 +1,145 @@ -/* Copyright (c) 2010 Wildfire Games +/* Copyright (c) 2013 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. */ /* * look up directories/files by traversing path components. */ #include "precompiled.h" #include "lib/file/vfs/vfs_lookup.h" #include "lib/external_libraries/suppress_boost_warnings.h" #include "lib/sysdep/filesystem.h" #include "lib/file/vfs/vfs.h" // error codes #include "lib/file/vfs/vfs_tree.h" #include "lib/file/vfs/vfs_populate.h" #include "lib/timer.h" static Status CreateDirectory(const OsPath& path) { { const mode_t mode = S_IRWXU; // 0700 as prescribed by XDG basedir const int ret = wmkdir(path, mode); if(ret == 0) // success return INFO::OK; } // failed because the directory already exists. this can happen // when the first vfs_Lookup has addMissingDirectories && // !createMissingDirectories, and the directory is subsequently // created. return 'success' to attach the existing directory.. if(errno == EEXIST) { // but first ensure it's really a directory (otherwise, a // file is "in the way" and needs to be deleted) struct stat s; const int ret = wstat(path, &s); ENSURE(ret == 0); // (wmkdir said it existed) ENSURE(S_ISDIR(s.st_mode)); return INFO::OK; } // unexpected failure debug_printf(L"wmkdir failed with errno=%d\n", errno); DEBUG_WARN_ERR(ERR::LOGIC); WARN_RETURN(StatusFromErrno()); } Status vfs_Lookup(const VfsPath& pathname, VfsDirectory* startDirectory, VfsDirectory*& directory, VfsFile** pfile, size_t flags) { // extract and validate flags (ensure no unknown bits are set) const bool addMissingDirectories = (flags & VFS_LOOKUP_ADD) != 0; const bool createMissingDirectories = (flags & VFS_LOOKUP_CREATE) != 0; const bool skipPopulate = (flags & VFS_LOOKUP_SKIP_POPULATE) != 0; - ENSURE((flags & ~(VFS_LOOKUP_ADD|VFS_LOOKUP_CREATE|VFS_LOOKUP_SKIP_POPULATE)) == 0); + const bool createAlways = (flags & VFS_LOOKUP_CREATE_ALWAYS) != 0; + ENSURE((flags & ~(VFS_LOOKUP_ADD|VFS_LOOKUP_CREATE|VFS_LOOKUP_SKIP_POPULATE|VFS_LOOKUP_CREATE_ALWAYS)) == 0); directory = startDirectory; if(pfile) *pfile = 0; if(!skipPopulate) RETURN_STATUS_IF_ERR(vfs_Populate(directory)); // early-out for pathname == "" when mounting into VFS root if(pathname.empty()) // (prevent iterator error in loop end condition) { if(pfile) // preserve a guarantee that if pfile then we either return an error or set *pfile return ERR::VFS_FILE_NOT_FOUND; else return INFO::OK; } // for each directory component: size_t pos = 0; // (needed outside of loop) for(;;) { const size_t nextSlash = pathname.string().find_first_of('/', pos); if(nextSlash == VfsPath::String::npos) break; const VfsPath subdirectoryName = pathname.string().substr(pos, nextSlash-pos); pos = nextSlash+1; VfsDirectory* subdirectory = directory->GetSubdirectory(subdirectoryName); if(!subdirectory) { if(addMissingDirectories) subdirectory = directory->AddSubdirectory(subdirectoryName); else return ERR::VFS_DIR_NOT_FOUND; // NOWARN } - if(createMissingDirectories && !subdirectory->AssociatedDirectory()) + if(createMissingDirectories && (!subdirectory->AssociatedDirectory() + || (createAlways && (subdirectory->AssociatedDirectory()->Flags() & VFS_MOUNT_REPLACEABLE) != 0))) { OsPath currentPath; if(directory->AssociatedDirectory()) // (is NULL when mounting into root) currentPath = directory->AssociatedDirectory()->Path(); currentPath = currentPath / subdirectoryName; RETURN_STATUS_IF_ERR(CreateDirectory(currentPath)); PRealDirectory realDirectory(new RealDirectory(currentPath, 0, 0)); RETURN_STATUS_IF_ERR(vfs_Attach(subdirectory, realDirectory)); } if(!skipPopulate) RETURN_STATUS_IF_ERR(vfs_Populate(subdirectory)); directory = subdirectory; } if(pfile) { ENSURE(!pathname.IsDirectory()); const VfsPath filename = pathname.string().substr(pos); *pfile = directory->GetFile(filename); if(!*pfile) return ERR::VFS_FILE_NOT_FOUND; // NOWARN } return INFO::OK; } Index: ps/trunk/source/lib/file/vfs/vfs_lookup.h =================================================================== --- ps/trunk/source/lib/file/vfs/vfs_lookup.h (revision 13471) +++ ps/trunk/source/lib/file/vfs/vfs_lookup.h (revision 13472) @@ -1,70 +1,75 @@ -/* Copyright (c) 2010 Wildfire Games +/* Copyright (c) 2013 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. */ /* * look up directories/files by traversing path components. */ #ifndef INCLUDED_VFS_LOOKUP #define INCLUDED_VFS_LOOKUP #include "lib/file/vfs/vfs_path.h" class VfsFile; class VfsDirectory; // note: VfsDirectory pointers are non-const because they may be // populated during the lookup. enum VfsLookupFlags { // add (if they do not already exist) subdirectory components // encountered in the path[name]. VFS_LOOKUP_ADD = 1, // if VFS directories encountered are not already associated // with a real directory, do so (creating the directories // if they do not already exist). VFS_LOOKUP_CREATE = 2, // don't populate the directories encountered. this makes sense // when adding files from an archive, which would otherwise // cause nearly every directory to be populated. - VFS_LOOKUP_SKIP_POPULATE = 4 + VFS_LOOKUP_SKIP_POPULATE = 4, + + // even create directories if they are already present, this is + // useful to write new files to the directory that was attached + // last, if the directory wasn't mounted with VFS_MOUNT_REPLACEABLE + VFS_LOOKUP_CREATE_ALWAYS = 8 }; /** * Resolve a pathname. * * @param pathname * @param startDirectory VfsStartDirectory. * @param directory is set to the last directory component that is encountered. * @param pfile File is set to 0 if there is no name component, otherwise the * corresponding file. * @param flags @see VfsLookupFlags. * @return Status (INFO::OK if all components in pathname exist). * * to allow noiseless file-existence queries, this does not raise warnings. **/ extern Status vfs_Lookup(const VfsPath& pathname, VfsDirectory* startDirectory, VfsDirectory*& directory, VfsFile** pfile, size_t flags = 0); #endif // #ifndef INCLUDED_VFS_LOOKUP Index: ps/trunk/source/ps/GameSetup/GameSetup.cpp =================================================================== --- ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 13471) +++ ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 13472) @@ -1,1330 +1,1360 @@ /* Copyright (C) 2013 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/CinemaTrack.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/JSInterface_IGUIObject.h" #include "gui/scripting/JSInterface_GUITypes.h" #include "gui/scripting/ScriptFunctions.h" #include "maths/MathUtil.h" #include "maths/scripting/JSInterface_Vector3D.h" #include "network/NetServer.h" #include "network/NetClient.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Font.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/Overlay.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/World.h" #include "renderer/Renderer.h" #include "renderer/VertexBufferManager.h" #include "renderer/ModelRenderer.h" #include "scripting/ScriptingHost.h" #include "scripting/ScriptGlue.h" #include "scriptinterface/DebuggingServer.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/ScriptStats.h" #include "simulation2/Simulation2.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 #if OS_WIN extern void wmi_Shutdown(); #endif #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; static const int SANE_TEX_QUALITY_DEFAULT = 5; // keep in sync with code 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_ScriptingHost.GetScriptInterface().SetGlobal("g_Progress", percent, true); g_ScriptingHost.GetScriptInterface().SetGlobal("g_LoadDescription", pending_task, true); g_GUI->SendEventToAll("progress"); } void Render() { PROFILE3("render"); #if CONFIG2_AUDIO if (g_SoundManager) g_SoundManager->IdleTask(); #endif 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 (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, false); } else { bool forceGL = false; CFG_GET_VAL("nohwcursor", Bool, 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 (cursor_draw(g_VFS, cursorName.c_str(), g_mouse_x, g_yres-g_mouse_y, forceGL) < 0) LOGWARNING(L"Failed to draw cursor '%ls'", cursorName.c_str()); #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 void RegisterJavascriptInterfaces() { // maths JSI_Vector3D::init(); // graphics CGameView::ScriptingInit(); // renderer CRenderer::ScriptingInit(); // ps JSI_Console::init(); // GUI CGUI::ScriptingInit(); GuiScriptingInit(g_ScriptingHost.GetScriptInterface()); JSI_Sound::RegisterScriptFunctions(g_ScriptingHost.GetScriptInterface()); } static void InitScripting() { TIMER(L"InitScripting"); // Create the scripting host. This needs to be done before the GUI is created. // [7ms] new ScriptingHost; RegisterJavascriptInterfaces(); } 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 = 500; // 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(L"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; } +static std::vector GetMods(const CmdLineArgs& args, bool dev) +{ + std::vector mods = args.GetMultiple("mod"); + // TODO: It would be nice to remove this hard-coding + mods.insert(mods.begin(), "public"); + + // 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 (!dev && !args.Has("noUserMod")) + mods.push_back("user"); + + return mods; +} + static void InitVfs(const CmdLineArgs& args) { TIMER(L"InitVfs"); 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; hooks.display_error = psDisplayError; app_hooks_update(&hooks); const size_t cacheSize = ChooseCacheSize(); g_VFS = CreateVfs(cacheSize); + + // Work out whether we are a dev version to make sure saved files + // (maps, etc) end up in version control. + const OsPath readonlyConfig = paths.RData()/"config"/""; + g_VFS->Mount(L"config/", readonlyConfig); + bool dev = (g_VFS->GetFileInfo(L"config/dev.cfg", NULL) == INFO::OK); - std::vector mods = args.GetMultiple("mod"); - mods.insert(mods.begin(), "public"); - - if (!args.Has("noUserMod")) - mods.push_back("user"); + const std::vector mods = GetMods(args, dev); OsPath modPath = paths.RData()/"mods"; OsPath modUserPath = paths.UserData()/"mods"; for (size_t i = 0; i < mods.size(); ++i) { - size_t priority = i+1; // mods are higher priority than regular mountings, which default to priority 0 - size_t flags = VFS_MOUNT_WATCH|VFS_MOUNT_ARCHIVABLE|VFS_MOUNT_MUST_EXIST; + 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 flags = userFlags|VFS_MOUNT_MUST_EXIST; + OsPath modName(mods[i]); - g_VFS->Mount(L"", modPath / modName/"", flags, priority); - g_VFS->Mount(L"", modUserPath / modName/"", flags, priority); + if (dev) + { + // 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/"", flags, priority); + else + g_VFS->Mount(L"", modUserPath / modName/"", userFlags, priority); + } + else + { + g_VFS->Mount(L"", modPath / modName/"", flags, priority); + // Ensure that user modified files are loaded, if they are present + g_VFS->Mount(L"", modUserPath / modName/"", userFlags, priority+1); + } } // 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); - const OsPath readonlyConfig = paths.RData()/"config"/""; // 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, CScriptVal initData) { { // console TIMER(L"ps_console"); g_Console->UpdateScreenSize(g_xres, g_yres); // Calculate and store the line spacing CFont font(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", Double, 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", initData); return; } // GUI uses VFS, so this must come after VFS init. g_GUI->SwitchPage(gui_page, initData); } static void InitInput() { #if !SDL_VERSION_ATLEAST(2, 0, 0) SDL_EnableKeyRepeat(SDL_DEFAULT_REPEAT_DELAY, SDL_DEFAULT_REPEAT_INTERVAL); #endif 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); SAFE_DELETE(g_Console); // disable the special Windows cursor, or free textures for OGL cursors cursor_draw(g_VFS, 0, g_mouse_x, g_yres-g_mouse_y, 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 g_Renderer.SetOptionBool(CRenderer::OPT_NOVBO, g_NoGLVBO); g_Renderer.SetOptionBool(CRenderer::OPT_SHADOWS, g_Shadows); g_Renderer.SetOptionBool(CRenderer::OPT_WATERNORMAL, g_WaterNormal); g_Renderer.SetOptionBool(CRenderer::OPT_WATERREALDEPTH, g_WaterRealDepth); g_Renderer.SetOptionBool(CRenderer::OPT_WATERFOAM, g_WaterFoam); g_Renderer.SetOptionBool(CRenderer::OPT_WATERCOASTALWAVES, g_WaterCoastalWaves); g_Renderer.SetOptionBool(CRenderer::OPT_WATERREFLECTION, g_WaterRefraction); g_Renderer.SetOptionBool(CRenderer::OPT_WATERREFRACTION, g_WaterReflection); g_Renderer.SetOptionBool(CRenderer::OPT_WATERSHADOW, g_WaterShadows); g_Renderer.SetRenderPath(CRenderer::GetRenderPathByName(g_RenderPath)); g_Renderer.SetOptionBool(CRenderer::OPT_SHADOWPCF, g_ShadowPCF); g_Renderer.SetOptionBool(CRenderer::OPT_PARTICLES, g_Particles); g_Renderer.SetOptionBool(CRenderer::OPT_SILHOUETTES, g_Silhouettes); g_Renderer.SetOptionBool(CRenderer::OPT_SHOWSKY, g_ShowSky); // 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(L"SDL library initialization failed: %hs", SDL_GetError()); throw PSERROR_System_SDLInitFailed(); } atexit(SDL_Quit); #if SDL_VERSION_ATLEAST(2, 0, 0) SDL_StartTextInput(); #else SDL_EnableUNICODE(1); #endif } static void ShutdownSDL() { SDL_Quit(); sys_cursor_reset(); } void EndGame() { SAFE_DELETE(g_NetServer); SAFE_DELETE(g_NetClient); SAFE_DELETE(g_Game); } void Shutdown(int UNUSED(flags)) { EndGame(); ShutdownPs(); // Must delete g_GUI before g_ScriptingHost in_reset_handlers(); TIMER_BEGIN(L"shutdown TexMan"); delete &g_TexMan; TIMER_END(L"shutdown TexMan"); // destroy renderer TIMER_BEGIN(L"shutdown Renderer"); delete &g_Renderer; g_VBMan.Shutdown(); TIMER_END(L"shutdown Renderer"); tex_codec_unregister_all(); 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"); g_VideoMode.Shutdown(); TIMER_BEGIN(L"shutdown UserReporter"); g_UserReporter.Deinitialize(); TIMER_END(L"shutdown UserReporter"); TIMER_BEGIN(L"shutdown ScriptingHost"); delete &g_ScriptingHost; delete g_DebuggingServer; TIMER_END(L"shutdown ScriptingHost"); TIMER_BEGIN(L"shutdown ConfigDB"); delete &g_ConfigDB; TIMER_END(L"shutdown ConfigDB"); // resource // first shut down all resource owners, and then the handle manager. TIMER_BEGIN(L"resource modules"); #if CONFIG2_AUDIO if (g_SoundManager) delete g_SoundManager; #endif 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(); SAFE_DELETE(g_ScriptStatsTable); // should be last, since the above use them SAFE_DELETE(g_Logger); delete &g_Profiler; delete &g_ProfileViewer; TIMER_END(L"shutdown misc"); #if OS_WIN TIMER_BEGIN(L"shutdown wmi"); wmi_Shutdown(); TIMER_END(L"shutdown wmi"); #endif } #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(L"Invalid locale settings"); for (size_t i = 0; i < ARRAY_SIZE(LocaleEnvVars); i++) { if (char* envval = getenv(LocaleEnvVars[i])) LOGWARNING(L" %hs=\"%hs\"", LocaleEnvVars[i], envval); else LOGWARNING(L" %hs=\"(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(L"Setting LC_ALL env variable to: %hs", 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(L"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(L"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); void Init(const CmdLineArgs& args, int UNUSED(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); // This must come after VFS init, which sets the current directory // (required for finding our output log files). g_Logger = new CLogger; // 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, 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); } // override ah_translate with our i18n code. AppHooks hooks = {0}; hooks.translate = psTranslate; hooks.translate_free = psTranslateFree; app_hooks_update(&hooks); // 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(); CNetHost::Initialize(); new CProfileViewer; new CProfileManager; // before any script code g_ScriptStatsTable = new CScriptStatsTable; g_ProfileViewer.AddRootTable(g_ScriptStatsTable); #if CONFIG2_AUDIO ISoundManager::CreateSoundManager(); #endif // g_ConfigDB, command line args, globals CONFIG_Init(args); - + // before scripting if (g_JSDebuggerEnabled) g_DebuggingServer = new CDebuggingServer(); InitScripting(); // before GUI - + g_ConfigDB.RegisterJSConfigDB(); // after scripting // 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.http.autoenable", Bool, profilerHTTPEnable); if (profilerHTTPEnable) g_Profiler2.EnableHTTP(); if (!g_Quickstart) g_UserReporter.Initialize(); // after config PROFILE2_EVENT("Init finished"); } 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 #if !SDL_VERSION_ATLEAST(2, 0, 0) SDL_WM_SetCaption("0 A.D.", "0 A.D."); #endif } RunHardwareDetection(); tex_codec_register_all(); 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.gpu.autoenable", Bool, 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) { // speed up startup by disabling all sound // (OpenAL init will be skipped). // must be called before first snd_open. #if CONFIG2_AUDIO ISoundManager::SetEnabled(false); #endif } g_GUI = new CGUIManager(g_ScriptingHost.GetScriptInterface()); // (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(); try { if (!Autostart(args)) { const bool setup_gui = ((flags & INIT_NO_GUI) == 0); // We only want to display the splash screen at startup CScriptValRooted data; if (g_GUI) { ScriptInterface& scriptInterface = g_GUI->GetScriptInterface(); scriptInterface.Eval("({})", data); scriptInterface.SetProperty(data.get(), "isStartup", true); } InitPs(setup_gui, L"page_pregame.xml", data.get()); } } catch (PSERROR_Game_World_MapLoadFailed& e) { // Map Loading failed // Start the engine so we have a GUI InitPs(true, L"page_pregame.xml", JSVAL_VOID); // Call script function to do the actual work // (delete game data, switch GUI page, show error, etc.) CancelLoad(CStr(e.what()).FromUTF8()); } } void RenderGui(bool RenderingState) { g_DoRenderGui = RenderingState; } void RenderLogger(bool RenderingState) { g_DoRenderLogger = RenderingState; } void RenderCursor(bool RenderingState) { g_DoRenderCursor = RenderingState; } bool Autostart(const CmdLineArgs& args) { /* * Handle various command-line options, for quick testing of various features: * -autostart=name -- map name for scenario, or rms name for random map * -autostart-ai=1:dummybot -- adds the dummybot AI to player 1 * -autostart-playername=name -- multiplayer player name * -autostart-host -- multiplayer host mode * -autostart-players=2 -- number of players * -autostart-client -- multiplayer client mode * -autostart-ip=127.0.0.1 -- multiplayer connect to 127.0.0.1 * -autostart-random=104 -- random map, optional seed value = 104 (default is 0, random is -1) * -autostart-size=192 -- random map size in tiles = 192 (default is 192) * * Examples: * -autostart=Acropolis -autostart-host -autostart-players=2 -- Host game on Acropolis map, 2 players * -autostart=latium -autostart-random=-1 -- Start single player game on latium random map, random rng seed */ CStr autoStartName = args.Get("autostart"); #if OS_ANDROID // HACK: currently the most convenient way to test maps on Android; // should find a better solution autoStartName = "Oasis"; #endif if (autoStartName.empty()) { return false; } g_Game = new CGame(); ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); CScriptValRooted attrs; scriptInterface.Eval("({})", attrs); CScriptVal settings; scriptInterface.Eval("({})", settings); CScriptVal playerData; scriptInterface.Eval("([])", playerData); // Set different attributes for random or scenario game if (args.Has("autostart-random")) { CStr seedArg = args.Get("autostart-random"); // Default seed is 0 uint32 seed = 0; if (!seedArg.empty()) { if (seedArg.compare("-1") == 0) { // Random seed value seed = rand(); } else { seed = seedArg.ToULong(); } } // Random map definition will be loaded from JSON file, so we need to parse it std::wstring mapPath = L"maps/random/"; std::wstring scriptPath = mapPath + autoStartName.FromUTF8() + L".json"; CScriptValRooted scriptData = scriptInterface.ReadJSONFile(scriptPath); if (!scriptData.undefined() && scriptInterface.GetProperty(scriptData.get(), "settings", settings)) { // JSON loaded ok - copy script name over to game attributes std::wstring scriptFile; scriptInterface.GetProperty(settings.get(), "Script", scriptFile); scriptInterface.SetProperty(attrs.get(), "script", scriptFile); // RMS filename } else { // Problem with JSON file LOGERROR(L"Error reading random map script '%ls'", scriptPath.c_str()); 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(attrs.get(), "map", std::string(autoStartName)); scriptInterface.SetProperty(attrs.get(), "mapPath", mapPath); scriptInterface.SetProperty(attrs.get(), "mapType", std::string("random")); scriptInterface.SetProperty(settings.get(), "Seed", seed); // Random seed scriptInterface.SetProperty(settings.get(), "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) { CScriptVal player; 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.get(), "Civ", std::string("athen")); scriptInterface.SetPropertyInt(playerData.get(), i, player); } } else { scriptInterface.SetProperty(attrs.get(), "map", std::string(autoStartName)); scriptInterface.SetProperty(attrs.get(), "mapType", std::string("scenario")); } // Set player data for AIs // attrs.settings = { PlayerData: [ { AI: ... }, ... ] }: if (args.Has("autostart-ai")) { std::vector aiArgs = args.GetMultiple("autostart-ai"); for (size_t i = 0; i < aiArgs.size(); ++i) { // Instead of overwriting existing player data, modify the array CScriptVal player; if (!scriptInterface.GetPropertyInt(playerData.get(), i, player) || player.undefined()) { scriptInterface.Eval("({})", player); } int playerID = aiArgs[i].BeforeFirst(":").ToInt(); CStr name = aiArgs[i].AfterFirst(":"); scriptInterface.SetProperty(player.get(), "AI", std::string(name)); scriptInterface.SetProperty(player.get(), "AIDiff", 1); scriptInterface.SetPropertyInt(playerData.get(), playerID-1, 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) { // Instead of overwriting existing player data, modify the array CScriptVal player; if (!scriptInterface.GetPropertyInt(playerData.get(), i, player) || player.undefined()) { scriptInterface.Eval("({})", player); } int playerID = civArgs[i].BeforeFirst(":").ToInt(); int difficulty = civArgs[i].AfterFirst(":").ToInt(); scriptInterface.SetProperty(player.get(), "AIDiff", difficulty); scriptInterface.SetPropertyInt(playerData.get(), playerID-1, player); } } // Set player data for Civs if (args.Has("autostart-civ")) { std::vector civArgs = args.GetMultiple("autostart-civ"); for (size_t i = 0; i < civArgs.size(); ++i) { // Instead of overwriting existing player data, modify the array CScriptVal player; if (!scriptInterface.GetPropertyInt(playerData.get(), i, player) || player.undefined()) { scriptInterface.Eval("({})", player); } int playerID = civArgs[i].BeforeFirst(":").ToInt(); CStr name = civArgs[i].AfterFirst(":"); scriptInterface.SetProperty(player.get(), "Civ", std::string(name)); scriptInterface.SetPropertyInt(playerData.get(), playerID-1, player); } } // Add player data to map settings scriptInterface.SetProperty(settings.get(), "PlayerData", playerData); // Add map settings to game attributes scriptInterface.SetProperty(attrs.get(), "settings", settings); CScriptVal mpInitData; g_GUI->GetScriptInterface().Eval("({isNetworked:true, playerAssignments:{}})", mpInitData); g_GUI->GetScriptInterface().SetProperty(mpInitData.get(), "attribs", CScriptVal(g_GUI->GetScriptInterface().CloneValueFromOtherContext(scriptInterface, attrs.get()))); // Get optional playername CStrW userName = L"anonymous"; if (args.Has("autostart-playername")) { userName = args.Get("autostart-playername").FromUTF8(); } if (args.Has("autostart-host")) { InitPs(true, L"page_loading.xml", mpInitData.get()); size_t maxPlayers = 2; if (args.Has("autostart-players")) { maxPlayers = args.Get("autostart-players").ToUInt(); } g_NetServer = new CNetServer(maxPlayers); g_NetServer->UpdateGameAttributes(attrs.get(), scriptInterface); bool ok = g_NetServer->SetupConnection(); ENSURE(ok); g_NetClient = new CNetClient(g_Game); g_NetClient->SetUserName(userName); g_NetClient->SetupConnection("127.0.0.1"); } else if (args.Has("autostart-client")) { InitPs(true, L"page_loading.xml", mpInitData.get()); g_NetClient = new CNetClient(g_Game); g_NetClient->SetUserName(userName); CStr ip = "127.0.0.1"; if (args.Has("autostart-ip")) { ip = args.Get("autostart-ip"); } bool ok = g_NetClient->SetupConnection(ip); ENSURE(ok); } else { g_Game->SetPlayerID(1); g_Game->StartGame(attrs, ""); LDR_NonprogressiveLoad(); PSRETURN ret = g_Game->ReallyStartGame(); ENSURE(ret == PSRETURN_OK); InitPs(true, L"page_session.xml", JSVAL_VOID); } return true; } void CancelLoad(const CStrW& message) { // Cancel loader LDR_Cancel(); // Call the cancelOnError GUI function, defined in ..gui/common/functions_utility_error.js // So all GUI pages that load games should include this script if (g_GUI && g_GUI->HasPages()) { JSContext* cx = g_ScriptingHost.getContext(); jsval fval, rval; JSBool ok = JS_GetProperty(cx, g_GUI->GetScriptObject(), "cancelOnError", &fval); ENSURE(ok); jsval msgval = ToJSVal(message); if (ok && !JSVAL_IS_VOID(fval)) JS_CallFunctionValue(cx, g_GUI->GetScriptObject(), fval, 1, &msgval, &rval); } } Index: ps/trunk/source/tools/dist/build.sh =================================================================== --- ps/trunk/source/tools/dist/build.sh (revision 13471) +++ ps/trunk/source/tools/dist/build.sh (revision 13472) @@ -1,62 +1,64 @@ #!/bin/bash set -ev # Compiled executable for archive-builder tool EXE=~/0ad/hg/ps/binaries/system/pyrogenesis # Location of clean checkout SVNWC=~/0ad/public-trunk/ SVNREV=`svnversion -n ${SVNWC}` PREFIX=0ad-r${SVNREV}-alpha XZOPTS="-9 -e" BZ2OPTS="-9" GZIPOPTS="-9" GZIP7ZOPTS="-mx=9" # Export files with appropriate line-endings rm -rf export-unix rm -rf export-win32 svn export ${SVNWC} export-unix svn export --native-eol CRLF ${SVNWC} export-win32 # Update the svn_revision, so these builds can be identified echo L\"${SVNREV}-release\" > export-unix/build/svn_revision/svn_revision.txt echo L\"${SVNREV}-release\" > export-win32/build/svn_revision/svn_revision.txt # Package the mod data # (The platforms differ only in line endings, so just do the Unix one instead of # generating two needlessly inconsistent packages) ${EXE} -archivebuild=export-unix/binaries/data/mods/public -archivebuild-output=export-unix/binaries/data/mods/public/public.zip cp export-unix/binaries/data/mods/public/public.zip export-win32/binaries/data/mods/public/public.zip # Collect the relevant files ln -Tsf export-unix ${PREFIX} tar cf $PREFIX-unix-build.tar \ --exclude='*.bat' --exclude='*.dll' --exclude='*.exe' --exclude='*.lib' \ --exclude='libraries/source/fcollada/src/FCollada/FColladaTest' \ --exclude='libraries/source/spidermonkey/include-win32' \ ${PREFIX}/{source,build,libraries/source,binaries/system/readme.txt,binaries/data/tests,binaries/data/mods/_test.*,*.txt} -tar cf $PREFIX-unix-data.tar ${PREFIX}/binaries/data/{config,mods/public/public.zip,tools} +tar cf $PREFIX-unix-data.tar \ + --exclude='binaries/data/config/dev.cfg' \ + ${PREFIX}/binaries/data/{config,mods/public/public.zip,tools} # TODO: ought to include generated docs in here, perhaps? # Compress xz -kv ${XZOPTS} $PREFIX-unix-build.tar xz -kv ${XZOPTS} $PREFIX-unix-data.tar #bzip2 -kp ${BZ2OPTS} $PREFIX-unix-build.tar #bzip2 -kp ${BZ2OPTS} $PREFIX-unix-data.tar #gzip -cv ${GZIPOPTS} $PREFIX-unix-build.tar > $PREFIX-unix-build.tar.gz #gzip -cv ${GZIPOPTS} $PREFIX-unix-data.tar > $PREFIX-unix-data.tar.gz 7z a ${GZIP7ZOPTS} $PREFIX-unix-build.tar.gz $PREFIX-unix-build.tar 7z a ${GZIP7ZOPTS} $PREFIX-unix-data.tar.gz $PREFIX-unix-data.tar # Create Windows installer wine ~/.wine/drive_c/Program\ Files/NSIS/makensis.exe /nocd /dcheckoutpath=export-win32 /drevision=${SVNREV} export-win32/source/tools/dist/0ad.nsi # Fix permissions chmod -f 644 ${PREFIX}-{unix-{build,data}.tar.{xz,bz2,gz},win32.exe} # Print digests for copying into wiki page sha1sum ${PREFIX}-{unix-{build,data}.tar.{xz,bz2,gz},win32.exe}