Index: ps/trunk/source/ps/SavedGame.h =================================================================== --- ps/trunk/source/ps/SavedGame.h (nonexistent) +++ ps/trunk/source/ps/SavedGame.h (revision 10454) @@ -0,0 +1,37 @@ +/* Copyright (C) 2011 Wildfire Games. + * This file is part of 0 A.D. + * + * 0 A.D. is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * 0 A.D. is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with 0 A.D. If not, see . + */ + +#ifndef INCLUDED_SAVEDGAME +#define INCLUDED_SAVEDGAME + +class CSimulation2; +class ScriptInterface; +class CScriptValRooted; +class CGUIManager; + +namespace SavedGames +{ + +Status Save(const std::wstring& prefix, CSimulation2& simulation, CGUIManager* gui, int playerIDo); + +Status Load(const std::wstring& name, ScriptInterface& scriptInterface, CScriptValRooted& metadata, std::string& savedState); + +std::vector GetSavedGames(ScriptInterface& scriptInterface); + +} + +#endif // INCLUDED_SAVEDGAME \ No newline at end of file Property changes on: ps/trunk/source/ps/SavedGame.h ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/source/ps/Replay.cpp =================================================================== --- ps/trunk/source/ps/Replay.cpp (revision 10453) +++ ps/trunk/source/ps/Replay.cpp (revision 10454) @@ -1,243 +1,243 @@ /* Copyright (C) 2011 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 "Replay.h" #include "graphics/TerrainTextureManager.h" #include "lib/timer.h" #include "lib/file/file_system.h" #include "lib/res/h_mgr.h" #include "lib/tex/tex.h" #include "ps/Game.h" #include "ps/Loader.h" #include "ps/Profile.h" #include "ps/ProfileViewer.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/ScriptStats.h" #include "simulation2/Simulation2.h" #include "simulation2/helpers/SimulationCommand.h" #include #include #include #if MSC_VERSION #include #define getpid _getpid // use the non-deprecated function name #endif static std::string Hexify(const std::string& s) { std::stringstream str; str << std::hex; for (size_t i = 0; i < s.size(); ++i) str << std::setfill('0') << std::setw(2) << (int)(unsigned char)s[i]; return str.str(); } CReplayLogger::CReplayLogger(ScriptInterface& scriptInterface) : m_ScriptInterface(scriptInterface) { // Construct the directory name based on the PID, to be relatively unique. // Append "-1", "-2" etc if we run multiple matches in a single session, // to avoid accidentally overwriting earlier logs. std::wstringstream name; name << getpid(); static int run = -1; if (++run) name << "-" << run; OsPath path = psLogDir() / L"sim_log" / name.str() / L"commands.txt"; CreateDirectories(path.Parent(), 0700); m_Stream = new std::ofstream(OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc); } CReplayLogger::~CReplayLogger() { delete m_Stream; } void CReplayLogger::StartGame(const CScriptValRooted& attribs) { *m_Stream << "start " << m_ScriptInterface.StringifyJSON(attribs.get(), false) << "\n"; } void CReplayLogger::Turn(u32 n, u32 turnLength, const std::vector& commands) { *m_Stream << "turn " << n << " " << turnLength << "\n"; for (size_t i = 0; i < commands.size(); ++i) { *m_Stream << "cmd " << commands[i].player << " " << m_ScriptInterface.StringifyJSON(commands[i].data.get(), false) << "\n"; } *m_Stream << "end\n"; m_Stream->flush(); } void CReplayLogger::Hash(const std::string& hash, bool quick) { if (quick) *m_Stream << "hash-quick " << Hexify(hash) << "\n"; else *m_Stream << "hash " << Hexify(hash) << "\n"; } //////////////////////////////////////////////////////////////// CReplayPlayer::CReplayPlayer() : m_Stream(NULL) { } CReplayPlayer::~CReplayPlayer() { delete m_Stream; } void CReplayPlayer::Load(const std::string& path) { ENSURE(!m_Stream); m_Stream = new std::ifstream(path.c_str()); ENSURE(m_Stream->good()); } void CReplayPlayer::Replay() { ENSURE(m_Stream); new CProfileViewer; new CProfileManager; g_ScriptStatsTable = new CScriptStatsTable; g_ProfileViewer.AddRootTable(g_ScriptStatsTable); CGame game(true); g_Game = &game; // Need some stuff for terrain movement costs: // (TODO: this ought to be independent of any graphics code) tex_codec_register_all(); new CTerrainTextureManager; g_TexMan.LoadTerrainTextures(); // Initialise h_mgr so it doesn't crash when emitting sounds h_mgr_init(); std::vector commands; u32 turn = 0; u32 turnLength = 0; std::string type; while ((*m_Stream >> type).good()) { // if (turn >= 1400) break; if (type == "start") { std::string line; std::getline(*m_Stream, line); CScriptValRooted attribs = game.GetSimulation2()->GetScriptInterface().ParseJSON(line); - game.StartGame(attribs); + game.StartGame(attribs, ""); // TODO: Non progressive load can fail - need a decent way to handle this LDR_NonprogressiveLoad(); PSRETURN ret = game.ReallyStartGame(); ENSURE(ret == PSRETURN_OK); } else if (type == "turn") { *m_Stream >> turn >> turnLength; debug_printf(L"Turn %d (%d)... ", turn, turnLength); } else if (type == "cmd") { u32 player; *m_Stream >> player; std::string line; std::getline(*m_Stream, line); CScriptValRooted data = game.GetSimulation2()->GetScriptInterface().ParseJSON(line); SimulationCommand cmd = { player, data }; commands.push_back(cmd); } else if (type == "hash" || type == "hash-quick") { std::string replayHash; *m_Stream >> replayHash; bool quick = (type == "hash-quick"); // if (turn >= 1300) // if (turn >= 0) if (turn % 100 == 0) { std::string hash; bool ok = game.GetSimulation2()->ComputeStateHash(hash, quick); ENSURE(ok); std::string hexHash = Hexify(hash); if (hexHash == replayHash) debug_printf(L"hash ok (%hs)", hexHash.c_str()); else debug_printf(L"HASH MISMATCH (%hs != %hs)", hexHash.c_str(), replayHash.c_str()); } } else if (type == "end") { game.GetSimulation2()->Update(turnLength, commands); commands.clear(); // std::string hash; // bool ok = game.GetSimulation2()->ComputeStateHash(hash, true); // ENSURE(ok); // debug_printf(L"%hs", Hexify(hash).c_str()); debug_printf(L"\n"); g_Profiler.Frame(); // if (turn % 1000 == 0) // JS_GC(game.GetSimulation2()->GetScriptInterface().GetContext()); if (turn % 20 == 0) g_ProfileViewer.SaveToFile(); } else { debug_printf(L"Unrecognised replay token %hs\n", type.c_str()); } } std::string hash; bool ok = game.GetSimulation2()->ComputeStateHash(hash, false); ENSURE(ok); debug_printf(L"# Final state: %hs\n", Hexify(hash).c_str()); timer_DisplayClientTotals(); // Clean up delete &g_TexMan; tex_codec_unregister_all(); delete &g_Profiler; delete &g_ProfileViewer; } Index: ps/trunk/source/ps/GameSetup/GameSetup.cpp =================================================================== --- ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 10453) +++ ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 10454) @@ -1,1180 +1,1181 @@ /* Copyright (C) 2011 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/input.h" #include "lib/ogl.h" #include "lib/timer.h" #include "lib/external_libraries/sdl.h" #include "lib/file/common/file_stats.h" #include "lib/res/h_mgr.h" #include "lib/res/graphics/cursor.h" #include "lib/res/sound/snd_mgr.h" #include "lib/sysdep/cursor.h" #include "lib/sysdep/cpu.h" #include "lib/sysdep/gfx.h" #include "lib/tex/tex.h" #if OS_WIN #include "lib/sysdep/os/win/wversion.h" #endif #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/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/UserReport.h" #include "ps/Util.h" #include "ps/VideoMode.h" #include "ps/World.h" #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 "renderer/Renderer.h" #include "renderer/VertexBufferManager.h" #include "maths/MathUtil.h" #include "simulation2/Simulation2.h" #include "scripting/ScriptingHost.h" #include "scripting/ScriptGlue.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/ScriptStats.h" #include "maths/scripting/JSInterface_Vector3D.h" #include "ps/scripting/JSInterface_Console.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 "sound/JSI_Sound.h" #include "network/NetServer.h" #include "network/NetClient.h" #include "ps/Pyrogenesis.h" // psSetLogDir #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" #if !(OS_WIN || OS_MACOSX) // 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() { ogl_WarnIfError(); CStr skystring = "255 0 255"; CFG_GET_USER_VAL("skycolor", String, skystring); CColor skycol; GUI::ParseString(skystring.FromUTF8(), skycol); g_Renderer.SetClearColor(skycol.AsSColor4ub()); // prepare before starting the renderer frame if (g_Game && g_Game->IsGameStarted()) g_Game->GetView()->BeginFrame(); // start new frame g_Renderer.BeginFrame(); ogl_WarnIfError(); if (g_Game && g_Game->IsGameStarted()) g_Game->GetView()->Render(); ogl_WarnIfError(); // set up overlay mode glPushAttrib(GL_ENABLE_BIT); glEnable(GL_TEXTURE_2D); glDisable(GL_CULL_FACE); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glEnable(GL_BLEND); glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); glOrtho(0.f, (float)g_xres, 0.f, (float)g_yres, -1.f, 1000.f); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glLoadIdentity(); ogl_WarnIfError(); g_Renderer.RenderTextOverlays(); // Temp GUI message GeeTODO PROFILE_START("render gui"); if(g_DoRenderGui) g_GUI->Draw(); PROFILE_END("render gui"); ogl_WarnIfError(); // Text: // Use the GL_ALPHA texture as the alpha channel with a flat colouring glDisable(GL_ALPHA_TEST); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); // Added -- glEnable(GL_TEXTURE_2D); // -- GL glLoadIdentity(); PROFILE_START("render console"); g_Console->Render(); PROFILE_END("render console"); ogl_WarnIfError(); PROFILE_START("render logger"); if(g_DoRenderLogger) g_Logger->Render(); PROFILE_END("render logger"); ogl_WarnIfError(); // Profile information PROFILE_START("render profiling"); g_ProfileViewer.RenderProfile(); PROFILE_END("render profiling"); ogl_WarnIfError(); // Draw the cursor (or set the Windows cursor, on Windows) if (g_DoRenderCursor) { PROFILE("render cursor"); CStrW cursorName = g_CursorName; if (cursorName.empty()) { cursor_draw(g_VFS, NULL, g_mouse_x, g_yres-g_mouse_y); } else { if (cursor_draw(g_VFS, cursorName.c_str(), g_mouse_x, g_yres-g_mouse_y) < 0) LOGWARNING(L"Failed to draw cursor '%ls'", cursorName.c_str()); } } // restore glMatrixMode(GL_PROJECTION); glPopMatrix(); glMatrixMode(GL_MODELVIEW); glPopMatrix(); glPopAttrib(); g_Renderer.EndFrame(); ogl_WarnIfError(); } static void RegisterJavascriptInterfaces() { // maths JSI_Vector3D::init(); // graphics CGameView::ScriptingInit(); // renderer CRenderer::ScriptingInit(); // sound JSI_Sound::ScriptingInit(); // ps JSI_Console::init(); // GUI CGUI::ScriptingInit(); GuiScriptingInit(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 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); g_VFS->Mount(L"screenshots/", paths.Data()/"screenshots"/""); + g_VFS->Mount(L"saves/", paths.Data()/"saves"/""); const OsPath readonlyConfig = paths.RData()/"config"/""; g_VFS->Mount(L"config/", readonlyConfig); if(readonlyConfig != paths.Config()) g_VFS->Mount(L"config/", paths.Config()); g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE); // (adding XMBs to archive speeds up subsequent reads) std::vector mods = args.GetMultiple("mod"); mods.push_back("public"); if(!args.Has("onlyPublicFiles")) mods.push_back("internal"); OsPath modArchivePath = paths.Cache()/"mods"; OsPath modLoosePath = paths.RData()/"mods"; for (size_t i = 0; i < mods.size(); ++i) { size_t priority = i; size_t flags = VFS_MOUNT_WATCH|VFS_MOUNT_ARCHIVABLE|VFS_MOUNT_MUST_EXIST; OsPath modName(mods[i]); g_VFS->Mount(L"", modLoosePath / modName/"", flags, priority); g_VFS->Mount(L"", modArchivePath / modName/"", flags, priority); } // 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; } // 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() { SDL_EnableKeyRepeat(SDL_DEFAULT_REPEAT_DELAY, SDL_DEFAULT_REPEAT_INTERVAL); 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); // 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); } 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_FANCYWATER,g_FancyWater); g_Renderer.SetRenderPath(CRenderer::GetRenderPathByName(g_RenderPath)); g_Renderer.SetOptionFloat(CRenderer::OPT_LODBIAS, g_LodBias); g_Renderer.SetOptionBool(CRenderer::OPT_SHADOWPCF, g_ShadowPCF); // create terrain related stuff new CTerrainTextureManager; // create the material manager new CMaterialManager; 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(); } 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); SDL_EnableUNICODE(1); } 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(); // destroy actor related stuff TIMER_BEGIN(L"shutdown actor stuff"); delete &g_MaterialManager; TIMER_END(L"shutdown actor stuff"); // destroy terrain related stuff 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(); 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; 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"); snd_shutdown(); 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 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(); 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); InitScripting(); // before GUI // g_ConfigDB, command line args, globals CONFIG_Init(args); if (!g_Quickstart) g_UserReporter.Initialize(); // after config } 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 SDL_WM_SetCaption("0 A.D.", "0 A.D."); } RunHardwareDetection(); tex_codec_register_all(); const int quality = SANE_TEX_QUALITY_DEFAULT; // TODO: set value from config file SetTextureQuality(quality); ogl_WarnIfError(); 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. snd_disable(true); } g_GUI = new CGUIManager(g_ScriptingHost.GetScriptInterface()); // (must come after SetVideoMode, since it calls ogl_Init) 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); InitPs(setup_gui, L"page_pregame.xml", JSVAL_VOID); } } 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 (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 scriptPath = L"maps/random/" + 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(), "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); scriptInterface.SetProperty(player.get(), "Civ", std::string("hele")); 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.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); + 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)) ok = JS_CallFunctionValue(cx, g_GUI->GetScriptObject(), fval, 1, &msgval, &rval); } } Index: ps/trunk/source/ps/Game.h =================================================================== --- ps/trunk/source/ps/Game.h (revision 10453) +++ ps/trunk/source/ps/Game.h (revision 10454) @@ -1,165 +1,168 @@ /* Copyright (C) 2011 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_GAME #define INCLUDED_GAME #include "ps/Errors.h" #include #include "scriptinterface/ScriptVal.h" class CWorld; class CSimulation2; class CGameView; class CNetTurnManager; class IReplayLogger; struct CColor; /** * The container that holds the rules, resources and attributes of the game. * The CGame object is responsible for creating a game that is defined by * a set of attributes provided. The CGame object is also responsible for * maintaining the relations between CPlayer and CWorld, CSimulation and CWorld. **/ class CGame { NONCOPYABLE(CGame); /** * pointer to the CWorld object representing the game world. **/ CWorld *m_World; /** * pointer to the CSimulation2 object operating on the game world. **/ CSimulation2 *m_Simulation2; /** * pointer to the CGameView object representing the view into the game world. **/ CGameView *m_GameView; /** * the game has been initialized and ready for use if true. **/ bool m_GameStarted; /** * scale multiplier for simulation rate. **/ float m_SimRate; int m_PlayerID; CNetTurnManager* m_TurnManager; public: CGame(bool disableGraphics = false); ~CGame(); /** * the game is paused and no updates will be performed if true. **/ bool m_Paused; - void StartGame(const CScriptValRooted& attribs); + void StartGame(const CScriptValRooted& attribs, const std::string& savedState); PSRETURN ReallyStartGame(); /* Perform all per-frame updates */ bool Update(double deltaTime, bool doInterpolate = true); void Interpolate(float frameLength); int GetPlayerID(); void SetPlayerID(int playerID); /** * Retrieving player colours from scripts is slow, so this updates an * internal cache of all players' colours. * Call this just before rendering, so it will always have the latest * colours. */ void CachePlayerColours(); CColor GetPlayerColour(int player) const; /** * Get m_GameStarted. * * @return bool the value of m_GameStarted. **/ inline bool IsGameStarted() const { return m_GameStarted; } /** * Get the pointer to the game world object. * * @return CWorld * the value of m_World. **/ inline CWorld *GetWorld() { return m_World; } /** * Get the pointer to the game view object. * * @return CGameView * the value of m_GameView. **/ inline CGameView *GetView() { return m_GameView; } /** * Get the pointer to the simulation2 object. * * @return CSimulation2 * the value of m_Simulation2. **/ inline CSimulation2 *GetSimulation2() { return m_Simulation2; } /** * Set the simulation scale multiplier. * * @param simRate Float value to set m_SimRate to. * Because m_SimRate is also used to * scale TimeSinceLastFrame it must be * clamped to 0.0f. **/ inline void SetSimRate(float simRate) { m_SimRate = std::max(simRate, 0.0f); } /** * Replace the current turn manager. * This class will take ownership of the pointer. */ void SetTurnManager(CNetTurnManager* turnManager); CNetTurnManager* GetTurnManager() const { return m_TurnManager; } IReplayLogger& GetReplayLogger() const { return *m_ReplayLogger; } private: - void RegisterInit(const CScriptValRooted& attribs); + void RegisterInit(const CScriptValRooted& attribs, const std::string& savedState); IReplayLogger* m_ReplayLogger; std::vector m_PlayerColours; + + int LoadInitialState(); + std::string m_InitialSavedState; // valid between RegisterInit and LoadInitialState }; extern CGame *g_Game; #endif Index: ps/trunk/source/ps/SavedGame.cpp =================================================================== --- ps/trunk/source/ps/SavedGame.cpp (nonexistent) +++ ps/trunk/source/ps/SavedGame.cpp (revision 10454) @@ -0,0 +1,216 @@ +/* Copyright (C) 2011 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 "SavedGame.h" + +#include "gui/GUIManager.h" +#include "lib/allocators/shared_ptr.h" +#include "lib/file/archive/archive_zip.h" +#include "ps/CLogger.h" +#include "ps/Filesystem.h" +#include "scriptinterface/ScriptInterface.h" +#include "simulation2/Simulation2.h" + +static const int SAVED_GAME_VERSION_MAJOR = 1; // increment on incompatible changes to the format +static const int SAVED_GAME_VERSION_MINOR = 0; // increment on compatible changes to the format +// TODO: we ought to check version numbers when loading files + +Status SavedGames::Save(const std::wstring& prefix, CSimulation2& simulation, CGUIManager* gui, int playerID) +{ + // Determine the filename to save under + const VfsPath basenameFormat(L"saves/" + prefix + L"-%04d"); + const VfsPath filenameFormat = basenameFormat.ChangeExtension(L".0adsave"); + VfsPath filename; + + // Don't make this a static global like NextNumberedFilename expects, because + // that wouldn't work when 'prefix' changes, and because it's not thread-safe + size_t nextSaveNumber = 0; + vfs::NextNumberedFilename(g_VFS, filenameFormat, nextSaveNumber, filename); + + // ArchiveWriter_Zip can only write to OsPaths, not VfsPaths, + // but we'd like to handle saved games via VFS. + // To avoid potential confusion from writing with non-VFS then + // reading the same file with VFS, we'll just write to a temporary + // non-VFS path and then load and save again via VFS, + // which is kind of a hack. + + OsPath tempSaveFileRealPath; + WARN_RETURN_STATUS_IF_ERR(g_VFS->GetDirectoryRealPath("cache/", tempSaveFileRealPath)); + tempSaveFileRealPath = tempSaveFileRealPath / "temp.0adsave"; + + time_t now = time(NULL); + + // Construct the serialized state to be saved + + std::stringstream simStateStream; + if (!simulation.SerializeState(simStateStream)) + WARN_RETURN(ERR::FAIL); + + CScriptValRooted metadata; + simulation.GetScriptInterface().Eval("({})", metadata); + simulation.GetScriptInterface().SetProperty(metadata.get(), "version_major", SAVED_GAME_VERSION_MAJOR); + simulation.GetScriptInterface().SetProperty(metadata.get(), "version_minor", SAVED_GAME_VERSION_MINOR); + simulation.GetScriptInterface().SetProperty(metadata.get(), "time", (double)now); + simulation.GetScriptInterface().SetProperty(metadata.get(), "player", playerID); + simulation.GetScriptInterface().SetProperty(metadata.get(), "initAttributes", simulation.GetInitAttributes()); + if (gui) + simulation.GetScriptInterface().SetProperty(metadata.get(), "gui", gui->GetSavedGameData()); + + std::string metadataString = simulation.GetScriptInterface().StringifyJSON(metadata.get(), true); + + // Write the saved game as zip file containing the various components + PIArchiveWriter archiveWriter; + try + { + archiveWriter = CreateArchiveWriter_Zip(tempSaveFileRealPath, false); + } + catch (Status err) + { + WARN_RETURN(err); + } + + WARN_RETURN_STATUS_IF_ERR(archiveWriter->AddMemory((const u8*)metadataString.c_str(), metadataString.length(), now, "metadata.json")); + WARN_RETURN_STATUS_IF_ERR(archiveWriter->AddMemory((const u8*)simStateStream.str().c_str(), simStateStream.str().length(), now, "simulation.dat")); + archiveWriter.reset(); // close the file + + WriteBuffer buffer; + FileInfo tempSaveFile; + WARN_RETURN_STATUS_IF_ERR(GetFileInfo(tempSaveFileRealPath, &tempSaveFile)); + buffer.Reserve(tempSaveFile.Size()); + WARN_RETURN_STATUS_IF_ERR(io::Load(tempSaveFileRealPath, buffer.Data().get(), buffer.Size())); + WARN_RETURN_STATUS_IF_ERR(g_VFS->CreateFile(filename, buffer.Data(), buffer.Size())); + + OsPath realPath; + WARN_RETURN_STATUS_IF_ERR(g_VFS->GetRealPath(filename, realPath)); + LOGMESSAGERENDER(L"Saved game to %ls\n", realPath.string().c_str()); + + return INFO::OK; +} + +class CGameLoader +{ + NONCOPYABLE(CGameLoader); +public: + CGameLoader(ScriptInterface& scriptInterface, CScriptValRooted* metadata, std::string* savedState) : + m_ScriptInterface(scriptInterface), m_Metadata(metadata), m_SavedState(savedState) + { + } + + static void ReadEntryCallback(const VfsPath& pathname, const FileInfo& fileInfo, PIArchiveFile archiveFile, uintptr_t cbData) + { + ((CGameLoader*)cbData)->ReadEntry(pathname, fileInfo, archiveFile); + } + + void ReadEntry(const VfsPath& pathname, const FileInfo& fileInfo, PIArchiveFile archiveFile) + { + if (pathname == L"metadata.json" && m_Metadata) + { + std::string buffer; + buffer.resize(fileInfo.Size()); + WARN_IF_ERR(archiveFile->Load("", DummySharedPtr((u8*)buffer.data()), buffer.size())); + *m_Metadata = m_ScriptInterface.ParseJSON(buffer); + } + else if (pathname == L"simulation.dat" && m_SavedState) + { + m_SavedState->resize(fileInfo.Size()); + WARN_IF_ERR(archiveFile->Load("", DummySharedPtr((u8*)m_SavedState->data()), m_SavedState->size())); + } + } + + ScriptInterface& m_ScriptInterface; + CScriptValRooted* m_Metadata; + std::string* m_SavedState; +}; + +Status SavedGames::Load(const std::wstring& name, ScriptInterface& scriptInterface, CScriptValRooted& metadata, std::string& savedState) +{ + // Determine the filename to load + const VfsPath basename(L"saves/" + name); + const VfsPath filename = basename.ChangeExtension(L".0adsave"); + + OsPath realPath; + WARN_RETURN_STATUS_IF_ERR(g_VFS->GetRealPath(filename, realPath)); + + PIArchiveReader archiveReader; + try + { + archiveReader = CreateArchiveReader_Zip(realPath); + } + catch (Status err) + { + WARN_RETURN(err); + } + + CGameLoader loader(scriptInterface, &metadata, &savedState); + WARN_RETURN_STATUS_IF_ERR(archiveReader->ReadEntries(CGameLoader::ReadEntryCallback, (uintptr_t)&loader)); + + return INFO::OK; +} + +std::vector SavedGames::GetSavedGames(ScriptInterface& scriptInterface) +{ + TIMER(L"GetSavedGames"); + + std::vector games; + + Status err; + + VfsPaths pathnames; + err = vfs::GetPathnames(g_VFS, "saves/", L"*.0adsave", pathnames); + WARN_IF_ERR(err); + + for (size_t i = 0; i < pathnames.size(); ++i) + { + OsPath realPath; + err = g_VFS->GetRealPath(pathnames[i], realPath); + if (err < 0) + { + DEBUG_WARN_ERR(err); + continue; // skip this file + } + + PIArchiveReader archiveReader; + try + { + archiveReader = CreateArchiveReader_Zip(realPath); + } + catch (Status err) + { + DEBUG_WARN_ERR(err); + continue; // skip this file + } + + CScriptValRooted metadata; + CGameLoader loader(scriptInterface, &metadata, NULL); + err = archiveReader->ReadEntries(CGameLoader::ReadEntryCallback, (uintptr_t)&loader); + if (err < 0) + { + DEBUG_WARN_ERR(err); + continue; // skip this file + } + + CScriptValRooted game; + scriptInterface.Eval("({})", game); + scriptInterface.SetProperty(game.get(), "id", pathnames[i].Basename()); + scriptInterface.SetProperty(game.get(), "metadata", metadata); + games.push_back(game); + } + + return games; +} \ No newline at end of file Property changes on: ps/trunk/source/ps/SavedGame.cpp ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/source/ps/Game.cpp =================================================================== --- ps/trunk/source/ps/Game.cpp (revision 10453) +++ ps/trunk/source/ps/Game.cpp (revision 10454) @@ -1,320 +1,340 @@ /* Copyright (C) 2011 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 "Game.h" #include "graphics/GameView.h" #include "graphics/LOSTexture.h" #include "graphics/ParticleManager.h" #include "graphics/UnitManager.h" #include "lib/timer.h" #include "network/NetClient.h" #include "network/NetServer.h" #include "network/NetTurnManager.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/CStr.h" #include "ps/Loader.h" #include "ps/LoaderThunks.h" #include "ps/Overlay.h" #include "ps/Profile.h" #include "ps/Replay.h" #include "ps/World.h" #include "renderer/Renderer.h" #include "scripting/ScriptingHost.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpPlayer.h" #include "simulation2/components/ICmpPlayerManager.h" #include "gui/GUIManager.h" extern bool g_GameRestarted; /** * Globally accessible pointer to the CGame object. **/ CGame *g_Game=NULL; /** * Constructor * **/ CGame::CGame(bool disableGraphics): m_World(new CWorld(this)), m_Simulation2(new CSimulation2(&m_World->GetUnitManager(), m_World->GetTerrain())), m_GameView(disableGraphics ? NULL : new CGameView(this)), m_GameStarted(false), m_Paused(false), m_SimRate(1.0f), m_PlayerID(-1) { m_ReplayLogger = new CReplayLogger(m_Simulation2->GetScriptInterface()); // TODO: should use CDummyReplayLogger unless activated by cmd-line arg, perhaps? // Need to set the CObjectManager references after various objects have // been initialised, so do it here rather than via the initialisers above. if (m_GameView) m_World->GetUnitManager().SetObjectManager(m_GameView->GetObjectManager()); m_TurnManager = new CNetLocalTurnManager(*m_Simulation2, GetReplayLogger()); // this will get replaced if we're a net server/client m_Simulation2->LoadDefaultScripts(); } /** * Destructor * **/ CGame::~CGame() { // Again, the in-game call tree is going to be different to the main menu one. if (CProfileManager::IsInitialised()) g_Profiler.StructuralReset(); delete m_TurnManager; delete m_GameView; delete m_Simulation2; delete m_World; delete m_ReplayLogger; } void CGame::SetTurnManager(CNetTurnManager* turnManager) { if (m_TurnManager) delete m_TurnManager; m_TurnManager = turnManager; if (m_TurnManager) m_TurnManager->SetPlayerID(m_PlayerID); } /** * Initializes the game with the set of attributes provided. * Makes calls to initialize the game view, world, and simulation objects. * Calls are made to facilitate progress reporting of the initialization. **/ -void CGame::RegisterInit(const CScriptValRooted& attribs) +void CGame::RegisterInit(const CScriptValRooted& attribs, const std::string& savedState) { + m_InitialSavedState = savedState; + m_Simulation2->SetInitAttributes(attribs); std::string mapType; m_Simulation2->GetScriptInterface().GetProperty(attribs.get(), "mapType", mapType); LDR_BeginRegistering(); RegMemFun(m_Simulation2, &CSimulation2::ProgressiveLoad, L"Simulation init", 1000); // RC, 040804 - GameView needs to be initialized before World, otherwise GameView initialization // overwrites anything stored in the map file that gets loaded by CWorld::Initialize with default // values. At the minute, it's just lighting settings, but could be extended to store camera position. // Storing lighting settings in the game view seems a little odd, but it's no big deal; maybe move it at // some point to be stored in the world object? if (m_GameView) m_GameView->RegisterInit(); if (mapType == "scenario") { // Load scenario attributes std::wstring mapFile; m_Simulation2->GetScriptInterface().GetProperty(attribs.get(), "map", mapFile); m_World->RegisterInit(mapFile, m_PlayerID); } else if (mapType == "random") { // Load random map attributes std::wstring scriptFile; CScriptValRooted settings; m_Simulation2->GetScriptInterface().GetProperty(attribs.get(), "script", scriptFile); m_Simulation2->GetScriptInterface().GetProperty(attribs.get(), "settings", settings); m_World->RegisterInitRMS(scriptFile, settings, m_PlayerID); } + if (!m_InitialSavedState.empty()) + RegMemFun(this, &CGame::LoadInitialState, L"Loading game", 1000); + LDR_EndRegistering(); } +int CGame::LoadInitialState() +{ + ENSURE(!m_InitialSavedState.empty()); + + std::string state; + m_InitialSavedState.swap(state); // deletes the original to save a bit of memory + + std::stringstream stream(state); + + bool ok = m_Simulation2->DeserializeState(stream); + ENSURE(ok); + + return 0; +} + /** * Game initialization has been completed. Set game started flag and start the session. * * @return PSRETURN 0 **/ PSRETURN CGame::ReallyStartGame() { CScriptVal settings; m_Simulation2->GetScriptInterface().GetProperty(m_Simulation2->GetInitAttributes().get(), "settings", settings); m_Simulation2->InitGame(settings); // Call the reallyStartGame GUI function, but only if it exists if (g_GUI && g_GUI->HasPages()) { jsval fval, rval; JSBool ok = JS_GetProperty(g_ScriptingHost.getContext(), g_GUI->GetScriptObject(), "reallyStartGame", &fval); ENSURE(ok); if (ok && !JSVAL_IS_VOID(fval)) ok = JS_CallFunctionValue(g_ScriptingHost.getContext(), g_GUI->GetScriptObject(), fval, 0, NULL, &rval); } if (g_NetClient) g_NetClient->LoadFinished(); // We need to do an initial Interpolate call to set up all the models etc, // because Update might never interpolate (e.g. if the game starts paused) // and we could end up rendering before having set up any models (so they'd // all be invisible) Interpolate(0); debug_printf(L"GAME STARTED, ALL INIT COMPLETE\n"); m_GameStarted=true; // The call tree we've built for pregame probably isn't useful in-game. if (CProfileManager::IsInitialised()) g_Profiler.StructuralReset(); // Mark terrain as modified so the minimap can repaint (is there a cleaner way of handling this?) g_GameRestarted = true; return 0; } int CGame::GetPlayerID() { return m_PlayerID; } void CGame::SetPlayerID(int playerID) { m_PlayerID = playerID; if (m_TurnManager) m_TurnManager->SetPlayerID(m_PlayerID); } -void CGame::StartGame(const CScriptValRooted& attribs) +void CGame::StartGame(const CScriptValRooted& attribs, const std::string& savedState) { m_ReplayLogger->StartGame(attribs); - RegisterInit(attribs); + RegisterInit(attribs, savedState); } // TODO: doInterpolate is optional because Atlas interpolates explicitly, // so that it has more control over the update rate. The game might want to // do the same, and then doInterpolate should be redundant and removed. /** * Periodic heartbeat that controls the process. * Simulation update is called and game status update is called. * * @param deltaTime Double. Elapsed time since last beat in seconds. * @param doInterpolate Bool. Perform interpolation if true. * @return bool false if it can't keep up with the desired simulation rate * indicating that you might want to render less frequently. **/ bool CGame::Update(double deltaTime, bool doInterpolate) { if (m_Paused) return true; if (!m_TurnManager) return true; deltaTime *= m_SimRate; bool ok = true; if (deltaTime) { // To avoid confusing the profiler, we need to trigger the new turn // while we're not nested inside any PROFILE blocks if (m_TurnManager->WillUpdate(deltaTime)) g_Profiler.Turn(); // At the normal sim rate, we currently want to render at least one // frame per simulation turn, so let maxTurns be 1. But for fast-forward // sim rates we want to allow more, so it's not bounded by framerate, // so just use the sim rate itself as the number of turns per frame. size_t maxTurns = (size_t)m_SimRate; PROFILE("simulation update"); if (m_TurnManager->Update(deltaTime, maxTurns)) { g_GUI->SendEventToAll("SimulationUpdate"); GetView()->GetLOSTexture().MakeDirty(); } } if (doInterpolate) { PROFILE("interpolate"); m_TurnManager->Interpolate(deltaTime); } // TODO: maybe we should add a CCmpParticleInterface that passes the interpolation commands // etc to CParticleManager. But in the meantime just handle it explicitly here. if (doInterpolate && CRenderer::IsInitialised()) g_Renderer.GetParticleManager().Interpolate(deltaTime); return ok; } void CGame::Interpolate(float frameLength) { if (!m_TurnManager) return; m_TurnManager->Interpolate(frameLength); if (CRenderer::IsInitialised()) g_Renderer.GetParticleManager().Interpolate(frameLength); } static CColor BrokenColor(0.3f, 0.3f, 0.3f, 1.0f); void CGame::CachePlayerColours() { m_PlayerColours.clear(); CmpPtr cmpPlayerManager(*m_Simulation2, SYSTEM_ENTITY); if (cmpPlayerManager.null()) return; int numPlayers = cmpPlayerManager->GetNumPlayers(); m_PlayerColours.resize(numPlayers); for (int i = 0; i < numPlayers; ++i) { CmpPtr cmpPlayer(*m_Simulation2, cmpPlayerManager->GetPlayerByID(i)); if (cmpPlayer.null()) m_PlayerColours[i] = BrokenColor; else m_PlayerColours[i] = cmpPlayer->GetColour(); } } CColor CGame::GetPlayerColour(int player) const { if (player < 0 || player >= (int)m_PlayerColours.size()) return BrokenColor; return m_PlayerColours[player]; } Index: ps/trunk/source/gui/scripting/ScriptFunctions.cpp =================================================================== --- ps/trunk/source/gui/scripting/ScriptFunctions.cpp (revision 10453) +++ ps/trunk/source/gui/scripting/ScriptFunctions.cpp (revision 10454) @@ -1,535 +1,593 @@ /* Copyright (C) 2011 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 "scriptinterface/ScriptInterface.h" #include "graphics/Camera.h" #include "graphics/GameView.h" #include "graphics/MapReader.h" #include "gui/GUIManager.h" #include "lib/timer.h" #include "lib/utf8.h" #include "lib/sysdep/sysdep.h" #include "maths/FixedVector3D.h" #include "network/NetClient.h" #include "network/NetServer.h" #include "network/NetTurnManager.h" #include "ps/CLogger.h" #include "ps/CConsole.h" #include "ps/Errors.h" #include "ps/Game.h" #include "ps/Hotkey.h" #include "ps/Overlay.h" #include "ps/ProfileViewer.h" #include "ps/Pyrogenesis.h" +#include "ps/SavedGame.h" #include "ps/UserReport.h" #include "ps/GameSetup/Atlas.h" #include "ps/GameSetup/Config.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpAIManager.h" #include "simulation2/components/ICmpCommandQueue.h" #include "simulation2/components/ICmpGuiInterface.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/components/ICmpTemplateManager.h" #include "simulation2/helpers/Selection.h" #include "js/jsapi.h" /* * This file defines a set of functions that are available to GUI scripts, to allow * interaction with the rest of the engine. * Functions are exposed to scripts within the global object 'Engine', so * scripts should call "Engine.FunctionName(...)" etc. */ extern void restart_mainloop_in_atlas(); // from main.cpp namespace { CScriptVal GetActiveGui(void* UNUSED(cbdata)) { return OBJECT_TO_JSVAL(g_GUI->GetScriptObject()); } void PushGuiPage(void* UNUSED(cbdata), std::wstring name, CScriptVal initData) { g_GUI->PushPage(name, initData); } void SwitchGuiPage(void* UNUSED(cbdata), std::wstring name, CScriptVal initData) { g_GUI->SwitchPage(name, initData); } void PopGuiPage(void* UNUSED(cbdata)) { g_GUI->PopPage(); } CScriptVal GuiInterfaceCall(void* cbdata, std::wstring name, CScriptVal data) { CGUIManager* guiManager = static_cast (cbdata); if (!g_Game) return JSVAL_VOID; CSimulation2* sim = g_Game->GetSimulation2(); ENSURE(sim); CmpPtr gui(*sim, SYSTEM_ENTITY); if (gui.null()) return JSVAL_VOID; int player = -1; if (g_Game) player = g_Game->GetPlayerID(); CScriptValRooted arg (sim->GetScriptInterface().GetContext(), sim->GetScriptInterface().CloneValueFromOtherContext(guiManager->GetScriptInterface(), data.get())); CScriptVal ret (gui->ScriptCall(player, name, arg.get())); return guiManager->GetScriptInterface().CloneValueFromOtherContext(sim->GetScriptInterface(), ret.get()); } void PostNetworkCommand(void* cbdata, CScriptVal cmd) { CGUIManager* guiManager = static_cast (cbdata); if (!g_Game) return; CSimulation2* sim = g_Game->GetSimulation2(); ENSURE(sim); CmpPtr queue(*sim, SYSTEM_ENTITY); if (queue.null()) return; jsval cmd2 = sim->GetScriptInterface().CloneValueFromOtherContext(guiManager->GetScriptInterface(), cmd.get()); queue->PostNetworkCommand(cmd2); } std::vector PickEntitiesAtPoint(void* UNUSED(cbdata), int x, int y) { return EntitySelection::PickEntitiesAtPoint(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), x, y, g_Game->GetPlayerID()); } std::vector PickFriendlyEntitiesInRect(void* UNUSED(cbdata), int x0, int y0, int x1, int y1, int player) { return EntitySelection::PickEntitiesInRect(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), x0, y0, x1, y1, player); } std::vector PickSimilarFriendlyEntities(void* UNUSED(cbdata), std::string templateName, bool includeOffScreen, bool matchRank) { return EntitySelection::PickSimilarEntities(*g_Game->GetSimulation2(), *g_Game->GetView()->GetCamera(), templateName, g_Game->GetPlayerID(), includeOffScreen, matchRank); } CFixedVector3D GetTerrainAtPoint(void* UNUSED(cbdata), int x, int y) { CVector3D pos = g_Game->GetView()->GetCamera()->GetWorldCoordinates(x, y, true); return CFixedVector3D(fixed::FromFloat(pos.X), fixed::FromFloat(pos.Y), fixed::FromFloat(pos.Z)); } std::wstring SetCursor(void* UNUSED(cbdata), std::wstring name) { std::wstring old = g_CursorName; g_CursorName = name; return old; } int GetPlayerID(void* UNUSED(cbdata)) { if (g_Game) return g_Game->GetPlayerID(); return -1; } std::wstring GetDefaultPlayerName(void* UNUSED(cbdata)) { // TODO: this should come from a config file or something std::wstring name = sys_get_user_name(); if (name.empty()) name = L"anonymous"; return name; } void StartNetworkGame(void* UNUSED(cbdata)) { ENSURE(g_NetServer); g_NetServer->StartGame(); } void StartGame(void* cbdata, CScriptVal attribs, int playerID) { CGUIManager* guiManager = static_cast (cbdata); ENSURE(!g_NetServer); ENSURE(!g_NetClient); ENSURE(!g_Game); g_Game = new CGame(); // Convert from GUI script context to sim script context CSimulation2* sim = g_Game->GetSimulation2(); CScriptValRooted gameAttribs (sim->GetScriptInterface().GetContext(), sim->GetScriptInterface().CloneValueFromOtherContext(guiManager->GetScriptInterface(), attribs.get())); g_Game->SetPlayerID(playerID); - g_Game->StartGame(gameAttribs); + g_Game->StartGame(gameAttribs, ""); +} + +CScriptVal StartSavedGame(void* cbdata, std::wstring name) +{ + CGUIManager* guiManager = static_cast (cbdata); + + ENSURE(!g_NetServer); + ENSURE(!g_NetClient); + + ENSURE(!g_Game); + + // Load the saved game data from disk + CScriptValRooted metadata; + std::string savedState; + Status err = SavedGames::Load(name, guiManager->GetScriptInterface(), metadata, savedState); + WARN_IF_ERR(err); + if (err < 0) + return CScriptVal(); + + g_Game = new CGame(); + + // Convert from GUI script context to sim script context + CSimulation2* sim = g_Game->GetSimulation2(); + CScriptValRooted gameMetadata (sim->GetScriptInterface().GetContext(), + sim->GetScriptInterface().CloneValueFromOtherContext(guiManager->GetScriptInterface(), metadata.get())); + + CScriptValRooted gameInitAttributes; + sim->GetScriptInterface().GetProperty(gameMetadata.get(), "initAttributes", gameInitAttributes); + + int playerID; + sim->GetScriptInterface().GetProperty(gameMetadata.get(), "player", playerID); + + // Start the game + g_Game->SetPlayerID(playerID); + g_Game->StartGame(gameInitAttributes, savedState); + + return metadata.get(); +} + +void SaveGame(void* cbdata) +{ + CGUIManager* guiManager = static_cast (cbdata); + + if (SavedGames::Save(L"quicksave", *g_Game->GetSimulation2(), guiManager, g_Game->GetPlayerID()) < 0) + LOGERROR(L"Failed to save game"); } void SetNetworkGameAttributes(void* cbdata, CScriptVal attribs) { CGUIManager* guiManager = static_cast (cbdata); ENSURE(g_NetServer); g_NetServer->UpdateGameAttributes(attribs, guiManager->GetScriptInterface()); } void StartNetworkHost(void* cbdata, std::wstring playerName) { CGUIManager* guiManager = static_cast (cbdata); ENSURE(!g_NetClient); ENSURE(!g_NetServer); ENSURE(!g_Game); g_NetServer = new CNetServer(); if (!g_NetServer->SetupConnection()) { guiManager->GetScriptInterface().ReportError("Failed to start server"); SAFE_DELETE(g_NetServer); return; } g_Game = new CGame(); g_NetClient = new CNetClient(g_Game); g_NetClient->SetUserName(playerName); if (!g_NetClient->SetupConnection("127.0.0.1")) { guiManager->GetScriptInterface().ReportError("Failed to connect to server"); SAFE_DELETE(g_NetClient); SAFE_DELETE(g_Game); } } void StartNetworkJoin(void* cbdata, std::wstring playerName, std::string serverAddress) { CGUIManager* guiManager = static_cast (cbdata); ENSURE(!g_NetClient); ENSURE(!g_NetServer); ENSURE(!g_Game); g_Game = new CGame(); g_NetClient = new CNetClient(g_Game); g_NetClient->SetUserName(playerName); if (!g_NetClient->SetupConnection(serverAddress)) { guiManager->GetScriptInterface().ReportError("Failed to connect to server"); SAFE_DELETE(g_NetClient); SAFE_DELETE(g_Game); } } void DisconnectNetworkGame(void* UNUSED(cbdata)) { // TODO: we ought to do async reliable disconnections SAFE_DELETE(g_NetServer); SAFE_DELETE(g_NetClient); SAFE_DELETE(g_Game); } CScriptVal PollNetworkClient(void* cbdata) { CGUIManager* guiManager = static_cast (cbdata); if (!g_NetClient) return CScriptVal(); CScriptValRooted poll = g_NetClient->GuiPoll(); // Convert from net client context to GUI script context return guiManager->GetScriptInterface().CloneValueFromOtherContext(g_NetClient->GetScriptInterface(), poll.get()); } void AssignNetworkPlayer(void* UNUSED(cbdata), int playerID, std::string guid) { ENSURE(g_NetServer); g_NetServer->AssignPlayer(playerID, guid); } void SendNetworkChat(void* UNUSED(cbdata), std::wstring message) { ENSURE(g_NetClient); g_NetClient->SendChatMessage(message); } std::vector GetAIs(void* cbdata) { CGUIManager* guiManager = static_cast (cbdata); return ICmpAIManager::GetAIs(guiManager->GetScriptInterface()); } +std::vector GetSavedGames(void* cbdata) +{ + CGUIManager* guiManager = static_cast (cbdata); + + return SavedGames::GetSavedGames(guiManager->GetScriptInterface()); +} + void OpenURL(void* UNUSED(cbdata), std::string url) { sys_open_url(url); } void RestartInAtlas(void* UNUSED(cbdata)) { restart_mainloop_in_atlas(); } bool AtlasIsAvailable(void* UNUSED(cbdata)) { return ATLAS_IsAvailable(); } CScriptVal LoadMapSettings(void* cbdata, VfsPath pathname) { CGUIManager* guiManager = static_cast (cbdata); CMapSummaryReader reader; if (reader.LoadMap(pathname.ChangeExtension(L".xml")) != PSRETURN_OK) return CScriptVal(); return reader.GetMapSettings(guiManager->GetScriptInterface()).get(); } CScriptVal GetMapSettings(void* cbdata) { CGUIManager* guiManager = static_cast (cbdata); if (!g_Game) return CScriptVal(); return guiManager->GetScriptInterface().CloneValueFromOtherContext( g_Game->GetSimulation2()->GetScriptInterface(), g_Game->GetSimulation2()->GetMapSettings().get()); } /** * Start / stop camera following mode * @param entityid unit id to follow. If zero, stop following mode */ void CameraFollow(void* UNUSED(cbdata), entity_id_t entityid) { if (g_Game && g_Game->GetView()) g_Game->GetView()->CameraFollow(entityid, false); } void CameraFollowFPS(void* UNUSED(cbdata), entity_id_t entityid) { if (g_Game && g_Game->GetView()) g_Game->GetView()->CameraFollow(entityid, true); } entity_id_t GetFollowedEntity(void* UNUSED(cbdata)) { if (g_Game && g_Game->GetView()) return g_Game->GetView()->GetFollowedEntity(); return INVALID_ENTITY; } bool HotkeyIsPressed_(void* UNUSED(cbdata), std::string hotkeyName) { return HotkeyIsPressed(hotkeyName); } void DisplayErrorDialog(void* UNUSED(cbdata), std::wstring msg) { debug_DisplayError(msg.c_str(), DE_NO_DEBUG_INFO, NULL, NULL, NULL, 0, NULL, NULL); } CScriptVal GetProfilerState(void* cbdata) { CGUIManager* guiManager = static_cast (cbdata); return g_ProfileViewer.SaveToJS(guiManager->GetScriptInterface()); } bool IsUserReportEnabled(void* UNUSED(cbdata)) { return g_UserReporter.IsReportingEnabled(); } void SetUserReportEnabled(void* UNUSED(cbdata), bool enabled) { g_UserReporter.SetReportingEnabled(enabled); } std::string GetUserReportStatus(void* UNUSED(cbdata)) { return g_UserReporter.GetStatus(); } void SubmitUserReport(void* UNUSED(cbdata), std::string type, int version, std::wstring data) { g_UserReporter.SubmitReport(type.c_str(), version, utf8_from_wstring(data)); } void SetSimRate(void* UNUSED(cbdata), float rate) { g_Game->SetSimRate(rate); } void SetTurnLength(void* UNUSED(cbdata), int length) { if (g_NetServer) g_NetServer->SetTurnLength(length); else LOGERROR(L"Only network host can change turn length"); } // Focus the game camera on a given position. void SetCameraTarget(void* UNUSED(cbdata), float x, float y, float z) { g_Game->GetView()->ResetCameraTarget(CVector3D(x, y, z)); } // Deliberately cause the game to crash. // Currently implemented via access violation (read of address 0). // Useful for testing the crashlog/stack trace code. int Crash(void* UNUSED(cbdata)) { debug_printf(L"Crashing at user's request.\n"); return *(int*)0; } void DebugWarn(void* UNUSED(cbdata)) { debug_warn(L"Warning at user's request."); } // Force a JS garbage collection cycle to take place immediately. // Writes an indication of how long this took to the console. void ForceGC(void* cbdata) { CGUIManager* guiManager = static_cast (cbdata); double time = timer_Time(); JS_GC(guiManager->GetScriptInterface().GetContext()); time = timer_Time() - time; g_Console->InsertMessage(L"Garbage collection completed in: %f", time); } void DumpSimState(void* UNUSED(cbdata)) { OsPath path = psLogDir()/"sim_dump.txt"; std::ofstream file (OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc); g_Game->GetSimulation2()->DumpDebugState(file); } void EnableTimeWarpRecording(void* UNUSED(cbdata), unsigned int numTurns) { g_Game->GetTurnManager()->EnableTimeWarpRecording(numTurns); } void RewindTimeWarp(void* UNUSED(cbdata)) { g_Game->GetTurnManager()->RewindTimeWarp(); } void QuickSave(void* UNUSED(cbdata)) { g_Game->GetTurnManager()->QuickSave(); } void QuickLoad(void* UNUSED(cbdata)) { g_Game->GetTurnManager()->QuickLoad(); } } // namespace void GuiScriptingInit(ScriptInterface& scriptInterface) { // GUI manager functions: scriptInterface.RegisterFunction("GetActiveGui"); scriptInterface.RegisterFunction("PushGuiPage"); scriptInterface.RegisterFunction("SwitchGuiPage"); scriptInterface.RegisterFunction("PopGuiPage"); // Simulation<->GUI interface functions: scriptInterface.RegisterFunction("GuiInterfaceCall"); scriptInterface.RegisterFunction("PostNetworkCommand"); // Entity picking scriptInterface.RegisterFunction, int, int, &PickEntitiesAtPoint>("PickEntitiesAtPoint"); scriptInterface.RegisterFunction, int, int, int, int, int, &PickFriendlyEntitiesInRect>("PickFriendlyEntitiesInRect"); scriptInterface.RegisterFunction, std::string, bool, bool, &PickSimilarFriendlyEntities>("PickSimilarFriendlyEntities"); scriptInterface.RegisterFunction("GetTerrainAtPoint"); // Network / game setup functions scriptInterface.RegisterFunction("StartNetworkGame"); scriptInterface.RegisterFunction("StartGame"); scriptInterface.RegisterFunction("StartNetworkHost"); scriptInterface.RegisterFunction("StartNetworkJoin"); scriptInterface.RegisterFunction("DisconnectNetworkGame"); scriptInterface.RegisterFunction("PollNetworkClient"); scriptInterface.RegisterFunction("SetNetworkGameAttributes"); scriptInterface.RegisterFunction("AssignNetworkPlayer"); scriptInterface.RegisterFunction("SendNetworkChat"); scriptInterface.RegisterFunction, &GetAIs>("GetAIs"); + // Saved games + scriptInterface.RegisterFunction("StartSavedGame"); + scriptInterface.RegisterFunction, &GetSavedGames>("GetSavedGames"); + scriptInterface.RegisterFunction("SaveGame"); + scriptInterface.RegisterFunction("QuickSave"); + scriptInterface.RegisterFunction("QuickLoad"); + // Misc functions scriptInterface.RegisterFunction("SetCursor"); scriptInterface.RegisterFunction("GetPlayerID"); scriptInterface.RegisterFunction("GetDefaultPlayerName"); scriptInterface.RegisterFunction("OpenURL"); scriptInterface.RegisterFunction("RestartInAtlas"); scriptInterface.RegisterFunction("AtlasIsAvailable"); scriptInterface.RegisterFunction("LoadMapSettings"); scriptInterface.RegisterFunction("GetMapSettings"); scriptInterface.RegisterFunction("CameraFollow"); scriptInterface.RegisterFunction("CameraFollowFPS"); scriptInterface.RegisterFunction("GetFollowedEntity"); scriptInterface.RegisterFunction("HotkeyIsPressed"); scriptInterface.RegisterFunction("DisplayErrorDialog"); scriptInterface.RegisterFunction("GetProfilerState"); // User report functions scriptInterface.RegisterFunction("IsUserReportEnabled"); scriptInterface.RegisterFunction("SetUserReportEnabled"); scriptInterface.RegisterFunction("GetUserReportStatus"); scriptInterface.RegisterFunction("SubmitUserReport"); // Development/debugging functions scriptInterface.RegisterFunction("SetSimRate"); scriptInterface.RegisterFunction("SetTurnLength"); scriptInterface.RegisterFunction("SetCameraTarget"); scriptInterface.RegisterFunction("Crash"); scriptInterface.RegisterFunction("DebugWarn"); scriptInterface.RegisterFunction("ForceGC"); scriptInterface.RegisterFunction("DumpSimState"); scriptInterface.RegisterFunction("EnableTimeWarpRecording"); scriptInterface.RegisterFunction("RewindTimeWarp"); - scriptInterface.RegisterFunction("QuickSave"); - scriptInterface.RegisterFunction("QuickLoad"); } Index: ps/trunk/source/gui/GUIManager.h =================================================================== --- ps/trunk/source/gui/GUIManager.h (revision 10453) +++ ps/trunk/source/gui/GUIManager.h (revision 10454) @@ -1,165 +1,170 @@ /* Copyright (C) 2010 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_GUIMANAGER #define INCLUDED_GUIMANAGER #include #include "lib/input.h" #include "lib/file/vfs/vfs_path.h" #include "ps/CStr.h" #include "scriptinterface/ScriptVal.h" #include class CGUI; struct JSObject; class IGUIObject; struct CColor; struct SGUIIcon; /** * External interface to the GUI system. * * The GUI consists of a set of pages. Each page is constructed from a * series of XML files, and is independent from any other page. * Only one page is active at a time. All events and render requests etc * will go to the active page. This lets the GUI switch between pre-game menu * and in-game UI. */ class CGUIManager { NONCOPYABLE(CGUIManager); public: CGUIManager(ScriptInterface& scriptInterface); ~CGUIManager(); ScriptInterface& GetScriptInterface() { return m_ScriptInterface; } /** * Returns whether there are any current pages. */ bool HasPages(); /** * Load a new GUI page and make it active. All current pages will be destroyed. */ void SwitchPage(const CStrW& name, CScriptVal initData); /** * Load a new GUI page and make it active. All current pages will be retained, * and will still be drawn and receive tick events, but will not receive * user inputs. */ void PushPage(const CStrW& name, CScriptVal initData); /** * Unload the currently active GUI page, and make the previous page active. * (There must be at least two pages when you call this.) */ void PopPage(); /** * Display a modal message box with an "OK" button. */ void DisplayMessageBox(int width, int height, const CStrW& title, const CStrW& message); /** * Call when a file has bee modified, to hotload pages if their .xml files changed. */ Status ReloadChangedFiles(const VfsPath& path); /** * Pass input events to the currently active GUI page. */ InReaction HandleEvent(const SDL_Event_* ev); /** * See CGUI::GetPreDefinedColor; applies to the currently active page. */ bool GetPreDefinedColor(const CStr& name, CColor& output); /** * See CGUI::IconExists; applies to the currently active page. */ bool IconExists(const CStr& str) const; /** * See CGUI::GetIcon; applies to the currently active page. */ SGUIIcon GetIcon(const CStr& str) const; /** * See CGUI::FindObjectByName; applies to the currently active page. */ IGUIObject* FindObjectByName(const CStr& name) const; /** * See CGUI::SendEventToAll; applies to the currently active page. */ void SendEventToAll(const CStr& eventName); /** * See CGUI::GetScriptObject; applies to the currently active page. */ JSObject* GetScriptObject(); /** * See CGUI::TickObjects; applies to @em all loaded pages. */ void TickObjects(); /** * See CGUI::Draw; applies to @em all loaded pages. */ void Draw(); /** * See CGUI::UpdateResolution; applies to @em all loaded pages. */ void UpdateResolution(); + /** + * Calls the current page's script function getSavedGameData() and returns the result. + */ + CScriptVal GetSavedGameData(); + private: struct SGUIPage { CStrW name; boost::unordered_set inputs; // for hotloading JSContext* cx; CScriptValRooted initData; // data to be passed to the init() function shared_ptr gui; // the actual GUI page }; void LoadPage(SGUIPage& page); shared_ptr top() const; typedef std::vector PageStackType; PageStackType m_PageStack; shared_ptr m_CurrentGUI; // used to latch state during TickObjects/LoadPage (this is kind of ugly) ScriptInterface& m_ScriptInterface; }; extern CGUIManager* g_GUI; extern InReaction gui_handler(const SDL_Event_* ev); #endif // INCLUDED_GUIMANAGER Index: ps/trunk/source/gui/GUIManager.cpp =================================================================== --- ps/trunk/source/gui/GUIManager.cpp (revision 10453) +++ ps/trunk/source/gui/GUIManager.cpp (revision 10454) @@ -1,285 +1,291 @@ /* Copyright (C) 2010 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 "GUIManager.h" #include "CGUI.h" #include "lib/timer.h" #include "ps/Filesystem.h" #include "ps/CLogger.h" #include "ps/Profile.h" #include "ps/XML/Xeromyces.h" #include "scripting/ScriptingHost.h" #include "scriptinterface/ScriptInterface.h" CGUIManager* g_GUI = NULL; // General TODOs: // // A lot of the CGUI data could (and should) be shared between // multiple pages, instead of treating them as completely independent, to save // memory and loading time. // called from main loop when (input) events are received. // event is passed to other handlers if false is returned. // trampoline: we don't want to make the HandleEvent implementation static InReaction gui_handler(const SDL_Event_* ev) { PROFILE("GUI event handler"); return g_GUI->HandleEvent(ev); } CGUIManager::CGUIManager(ScriptInterface& scriptInterface) : m_ScriptInterface(scriptInterface) { ENSURE(ScriptInterface::GetCallbackData(scriptInterface.GetContext()) == NULL); scriptInterface.SetCallbackData(this); } CGUIManager::~CGUIManager() { } bool CGUIManager::HasPages() { return !m_PageStack.empty(); } void CGUIManager::SwitchPage(const CStrW& pageName, CScriptVal initData) { m_PageStack.clear(); PushPage(pageName, initData); } void CGUIManager::PushPage(const CStrW& pageName, CScriptVal initData) { m_PageStack.push_back(SGUIPage()); m_PageStack.back().name = pageName; m_PageStack.back().initData = CScriptValRooted(m_ScriptInterface.GetContext(), initData); LoadPage(m_PageStack.back()); } void CGUIManager::PopPage() { if (m_PageStack.size() < 2) { debug_warn(L"Tried to pop GUI page when there's < 2 in the stack"); return; } m_PageStack.pop_back(); } void CGUIManager::DisplayMessageBox(int width, int height, const CStrW& title, const CStrW& message) { // Set up scripted init data for the standard message box window CScriptValRooted data; m_ScriptInterface.Eval("({})", data); m_ScriptInterface.SetProperty(data.get(), "width", width, false); m_ScriptInterface.SetProperty(data.get(), "height", height, false); m_ScriptInterface.SetProperty(data.get(), "mode", 2, false); m_ScriptInterface.SetProperty(data.get(), "title", std::wstring(title), false); m_ScriptInterface.SetProperty(data.get(), "message", std::wstring(message), false); // Display the message box PushPage(L"page_msgbox.xml", data.get()); } void CGUIManager::LoadPage(SGUIPage& page) { // If we're hotloading then try to grab some data from the previous page CScriptValRooted hotloadData; if (page.gui) m_ScriptInterface.CallFunction(OBJECT_TO_JSVAL(page.gui->GetScriptObject()), "getHotloadData", hotloadData); page.inputs.clear(); page.gui.reset(new CGUI()); page.gui->Initialize(); VfsPath path = VfsPath("gui") / page.name; page.inputs.insert(path); CXeromyces xero; if (xero.Load(g_VFS, path) != PSRETURN_OK) // Fail silently (Xeromyces reported the error) return; int elmt_page = xero.GetElementID("page"); int elmt_include = xero.GetElementID("include"); XMBElement root = xero.GetRoot(); if (root.GetNodeName() != elmt_page) { LOGERROR(L"GUI page '%ls' must have root element ", page.name.c_str()); return; } XERO_ITER_EL(root, node) { if (node.GetNodeName() != elmt_include) { LOGERROR(L"GUI page '%ls' must only have elements inside ", page.name.c_str()); continue; } CStrW name (node.GetText().FromUTF8()); TIMER(name.c_str()); VfsPath path = VfsPath("gui") / name; page.gui->LoadXmlFile(path, page.inputs); } // Remember this GUI page, in case the scripts call FindObjectByName shared_ptr oldGUI = m_CurrentGUI; m_CurrentGUI = page.gui; page.gui->SendEventToAll("load"); // Call the init() function if (!m_ScriptInterface.CallFunctionVoid(OBJECT_TO_JSVAL(page.gui->GetScriptObject()), "init", page.initData, hotloadData)) { LOGERROR(L"GUI page '%ls': Failed to call init() function", page.name.c_str()); } m_CurrentGUI = oldGUI; } Status CGUIManager::ReloadChangedFiles(const VfsPath& path) { for (PageStackType::iterator it = m_PageStack.begin(); it != m_PageStack.end(); ++it) { if (it->inputs.count(path)) { LOGMESSAGE(L"GUI file '%ls' changed - reloading page '%ls'", path.string().c_str(), it->name.c_str()); LoadPage(*it); // TODO: this can crash if LoadPage runs an init script which modifies the page stack and breaks our iterators } } return INFO::OK; } +CScriptVal CGUIManager::GetSavedGameData() +{ + CScriptVal data; + m_ScriptInterface.CallFunction(OBJECT_TO_JSVAL(top()->GetScriptObject()), "getSavedGameData", data); + return data; +} InReaction CGUIManager::HandleEvent(const SDL_Event_* ev) { // We want scripts to have access to the raw input events, so they can do complex // processing when necessary (e.g. for unit selection and camera movement). // Sometimes they'll want to be layered behind the GUI widgets (e.g. to detect mousedowns on the // visible game area), sometimes they'll want to intercepts events before the GUI (e.g. // to capture all mouse events until a mouseup after dragging). // So we call two separate handler functions: bool handled; { PROFILE("handleInputBeforeGui"); if (m_ScriptInterface.CallFunction(OBJECT_TO_JSVAL(top()->GetScriptObject()), "handleInputBeforeGui", *ev, top()->FindObjectUnderMouse(), handled)) if (handled) return IN_HANDLED; } { PROFILE("handle event in native GUI"); InReaction r = top()->HandleEvent(ev); if (r != IN_PASS) return r; } { PROFILE("handleInputAfterGui"); if (m_ScriptInterface.CallFunction(OBJECT_TO_JSVAL(top()->GetScriptObject()), "handleInputAfterGui", *ev, handled)) if (handled) return IN_HANDLED; } return IN_PASS; } bool CGUIManager::GetPreDefinedColor(const CStr& name, CColor& output) { return top()->GetPreDefinedColor(name, output); } bool CGUIManager::IconExists(const CStr& str) const { return top()->IconExists(str); } SGUIIcon CGUIManager::GetIcon(const CStr& str) const { return top()->GetIcon(str); } IGUIObject* CGUIManager::FindObjectByName(const CStr& name) const { // This can be called from scripts run by TickObjects, // and we want to return it the same GUI page as is being ticked if (m_CurrentGUI) return m_CurrentGUI->FindObjectByName(name); else return top()->FindObjectByName(name); } void CGUIManager::SendEventToAll(const CStr& eventName) { top()->SendEventToAll(eventName); } void CGUIManager::TickObjects() { // Save an immutable copy so iterators aren't invalidated by tick handlers PageStackType pageStack = m_PageStack; for (PageStackType::iterator it = pageStack.begin(); it != pageStack.end(); ++it) { m_CurrentGUI = it->gui; it->gui->TickObjects(); } m_CurrentGUI.reset(); } void CGUIManager::Draw() { for (PageStackType::iterator it = m_PageStack.begin(); it != m_PageStack.end(); ++it) it->gui->Draw(); } void CGUIManager::UpdateResolution() { for (PageStackType::iterator it = m_PageStack.begin(); it != m_PageStack.end(); ++it) it->gui->UpdateResolution(); } JSObject* CGUIManager::GetScriptObject() { return top()->GetScriptObject(); } // This returns a shared_ptr to make sure the CGUI doesn't get deallocated // while we're in the middle of calling a function on it (e.g. if a GUI script // calls SwitchPage) shared_ptr CGUIManager::top() const { ENSURE(m_PageStack.size()); return m_PageStack.back().gui; } Index: ps/trunk/source/network/NetClient.cpp =================================================================== --- ps/trunk/source/network/NetClient.cpp (revision 10453) +++ ps/trunk/source/network/NetClient.cpp (revision 10454) @@ -1,579 +1,579 @@ /* Copyright (C) 2011 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 "NetClient.h" #include "NetMessage.h" #include "NetSession.h" #include "NetTurnManager.h" #include "lib/byte_order.h" #include "lib/sysdep/sysdep.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/Compress.h" #include "ps/CStr.h" #include "ps/Game.h" #include "ps/Loader.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" CNetClient *g_NetClient = NULL; /** * Async task for receiving the initial game state when rejoining an * in-progress network game. */ class CNetFileReceiveTask_ClientRejoin : public CNetFileReceiveTask { NONCOPYABLE(CNetFileReceiveTask_ClientRejoin); public: CNetFileReceiveTask_ClientRejoin(CNetClient& client) : m_Client(client) { } virtual void OnComplete() { // We've received the game state from the server // Save it so we can use it after the map has finished loading m_Client.m_JoinSyncBuffer = m_Buffer; // Pretend the server told us to start the game CGameStartMessage start; m_Client.HandleMessage(&start); } private: CNetClient& m_Client; }; CNetClient::CNetClient(CGame* game) : m_Session(NULL), m_UserName(L"anonymous"), m_GUID(GenerateGUID()), m_HostID((u32)-1), m_ClientTurnManager(NULL), m_Game(game) { m_Game->SetTurnManager(NULL); // delete the old local turn manager so we don't accidentally use it void* context = this; // Set up transitions for session AddTransition(NCS_UNCONNECTED, (uint)NMT_CONNECT_COMPLETE, NCS_CONNECT, (void*)&OnConnect, context); AddTransition(NCS_CONNECT, (uint)NMT_SERVER_HANDSHAKE, NCS_HANDSHAKE, (void*)&OnHandshake, context); AddTransition(NCS_HANDSHAKE, (uint)NMT_SERVER_HANDSHAKE_RESPONSE, NCS_AUTHENTICATE, (void*)&OnHandshakeResponse, context); AddTransition(NCS_AUTHENTICATE, (uint)NMT_AUTHENTICATE_RESULT, NCS_INITIAL_GAMESETUP, (void*)&OnAuthenticate, context); AddTransition(NCS_INITIAL_GAMESETUP, (uint)NMT_GAME_SETUP, NCS_PREGAME, (void*)&OnGameSetup, context); AddTransition(NCS_PREGAME, (uint)NMT_CHAT, NCS_PREGAME, (void*)&OnChat, context); AddTransition(NCS_PREGAME, (uint)NMT_GAME_SETUP, NCS_PREGAME, (void*)&OnGameSetup, context); AddTransition(NCS_PREGAME, (uint)NMT_PLAYER_ASSIGNMENT, NCS_PREGAME, (void*)&OnPlayerAssignment, context); AddTransition(NCS_PREGAME, (uint)NMT_GAME_START, NCS_LOADING, (void*)&OnGameStart, context); AddTransition(NCS_PREGAME, (uint)NMT_JOIN_SYNC_START, NCS_JOIN_SYNCING, (void*)&OnJoinSyncStart, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_CHAT, NCS_JOIN_SYNCING, (void*)&OnChat, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_GAME_SETUP, NCS_JOIN_SYNCING, (void*)&OnGameSetup, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_PLAYER_ASSIGNMENT, NCS_JOIN_SYNCING, (void*)&OnPlayerAssignment, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_GAME_START, NCS_JOIN_SYNCING, (void*)&OnGameStart, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_SIMULATION_COMMAND, NCS_JOIN_SYNCING, (void*)&OnInGame, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_END_COMMAND_BATCH, NCS_JOIN_SYNCING, (void*)&OnJoinSyncEndCommandBatch, context); AddTransition(NCS_JOIN_SYNCING, (uint)NMT_LOADED_GAME, NCS_INGAME, (void*)&OnLoadedGame, context); AddTransition(NCS_LOADING, (uint)NMT_CHAT, NCS_LOADING, (void*)&OnChat, context); AddTransition(NCS_LOADING, (uint)NMT_GAME_SETUP, NCS_LOADING, (void*)&OnGameSetup, context); AddTransition(NCS_LOADING, (uint)NMT_PLAYER_ASSIGNMENT, NCS_LOADING, (void*)&OnPlayerAssignment, context); AddTransition(NCS_LOADING, (uint)NMT_LOADED_GAME, NCS_INGAME, (void*)&OnLoadedGame, context); AddTransition(NCS_INGAME, (uint)NMT_CHAT, NCS_INGAME, (void*)&OnChat, context); AddTransition(NCS_INGAME, (uint)NMT_GAME_SETUP, NCS_INGAME, (void*)&OnGameSetup, context); AddTransition(NCS_INGAME, (uint)NMT_PLAYER_ASSIGNMENT, NCS_INGAME, (void*)&OnPlayerAssignment, context); AddTransition(NCS_INGAME, (uint)NMT_SIMULATION_COMMAND, NCS_INGAME, (void*)&OnInGame, context); AddTransition(NCS_INGAME, (uint)NMT_SYNC_ERROR, NCS_INGAME, (void*)&OnInGame, context); AddTransition(NCS_INGAME, (uint)NMT_END_COMMAND_BATCH, NCS_INGAME, (void*)&OnInGame, context); // Set first state SetFirstState(NCS_UNCONNECTED); } CNetClient::~CNetClient() { DestroyConnection(); } void CNetClient::SetUserName(const CStrW& username) { ENSURE(!m_Session); // must be called before we start the connection m_UserName = username; } bool CNetClient::SetupConnection(const CStr& server) { CNetClientSession* session = new CNetClientSession(*this); bool ok = session->Connect(PS_DEFAULT_PORT, server); SetAndOwnSession(session); return ok; } void CNetClient::SetAndOwnSession(CNetClientSession* session) { delete m_Session; m_Session = session; } void CNetClient::DestroyConnection() { SAFE_DELETE(m_Session); } void CNetClient::Poll() { if (m_Session) m_Session->Poll(); } void CNetClient::Flush() { if (m_Session) m_Session->Flush(); } CScriptValRooted CNetClient::GuiPoll() { if (m_GuiMessageQueue.empty()) return CScriptValRooted(); CScriptValRooted r = m_GuiMessageQueue.front(); m_GuiMessageQueue.pop_front(); return r; } void CNetClient::PushGuiMessage(const CScriptValRooted& message) { ENSURE(!message.undefined()); m_GuiMessageQueue.push_back(message); } std::wstring CNetClient::TestReadGuiMessages() { std::wstring r; while (true) { CScriptValRooted msg = GuiPoll(); if (msg.undefined()) break; r += GetScriptInterface().ToString(msg.get()) + L"\n"; } return r; } ScriptInterface& CNetClient::GetScriptInterface() { return m_Game->GetSimulation2()->GetScriptInterface(); } void CNetClient::PostPlayerAssignmentsToScript() { CScriptValRooted msg; GetScriptInterface().Eval("({'type':'players', 'hosts':{}})", msg); CScriptValRooted hosts; GetScriptInterface().GetProperty(msg.get(), "hosts", hosts); for (PlayerAssignmentMap::iterator it = m_PlayerAssignments.begin(); it != m_PlayerAssignments.end(); ++it) { CScriptValRooted host; GetScriptInterface().Eval("({})", host); GetScriptInterface().SetProperty(host.get(), "name", std::wstring(it->second.m_Name), false); GetScriptInterface().SetProperty(host.get(), "player", it->second.m_PlayerID, false); GetScriptInterface().SetProperty(hosts.get(), it->first.c_str(), host, false); } PushGuiMessage(msg); } bool CNetClient::SendMessage(const CNetMessage* message) { if (!m_Session) return false; return m_Session->SendMessage(message); } void CNetClient::HandleConnect() { Update((uint)NMT_CONNECT_COMPLETE, NULL); } void CNetClient::HandleDisconnect(u32 reason) { CScriptValRooted msg; GetScriptInterface().Eval("({'type':'netstatus','status':'disconnected'})", msg); GetScriptInterface().SetProperty(msg.get(), "reason", (int)reason, false); PushGuiMessage(msg); SAFE_DELETE(m_Session); // Update the state immediately to UNCONNECTED (don't bother with FSM transitions since // we'd need one for every single state, and we don't need to use per-state actions) SetCurrState(NCS_UNCONNECTED); } void CNetClient::SendChatMessage(const std::wstring& text) { CChatMessage chat; chat.m_Message = text; SendMessage(&chat); } bool CNetClient::HandleMessage(CNetMessage* message) { // Handle non-FSM messages first Status status = m_Session->GetFileTransferer().HandleMessageReceive(message); if (status == INFO::OK) return true; if (status != INFO::SKIPPED) return false; if (message->GetType() == NMT_FILE_TRANSFER_REQUEST) { CFileTransferRequestMessage* reqMessage = (CFileTransferRequestMessage*)message; // TODO: we should support different transfer request types, instead of assuming // it's always requesting the simulation state std::stringstream stream; LOGMESSAGERENDER(L"Serializing game at turn %d for rejoining player", m_ClientTurnManager->GetCurrentTurn()); u32 turn = to_le32(m_ClientTurnManager->GetCurrentTurn()); stream.write((char*)&turn, sizeof(turn)); bool ok = m_Game->GetSimulation2()->SerializeState(stream); ENSURE(ok); // Compress the content with zlib to save bandwidth // (TODO: if this is still too large, compressing with e.g. LZMA works much better) std::string compressed; CompressZLib(stream.str(), compressed, true); m_Session->GetFileTransferer().StartResponse(reqMessage->m_RequestID, compressed); return true; } // Update FSM bool ok = Update(message->GetType(), message); if (!ok) LOGERROR(L"Net client: Error running FSM update (type=%d state=%d)", (int)message->GetType(), (int)GetCurrState()); return ok; } void CNetClient::LoadFinished() { if (!m_JoinSyncBuffer.empty()) { // We're rejoining a game, and just finished loading the initial map, // so deserialize the saved game state now std::string state; DecompressZLib(m_JoinSyncBuffer, state, true); std::stringstream stream(state); u32 turn; stream.read((char*)&turn, sizeof(turn)); turn = to_le32(turn); LOGMESSAGE(L"Rejoining client deserializing state at turn %d\n", turn); bool ok = m_Game->GetSimulation2()->DeserializeState(stream); ENSURE(ok); m_ClientTurnManager->ResetState(turn, turn); CScriptValRooted msg; GetScriptInterface().Eval("({'type':'netstatus','status':'join_syncing'})", msg); PushGuiMessage(msg); } else { // Connecting at the start of a game, so we'll wait for other players to finish loading CScriptValRooted msg; GetScriptInterface().Eval("({'type':'netstatus','status':'waiting_for_players'})", msg); PushGuiMessage(msg); } CLoadedGameMessage loaded; loaded.m_CurrentTurn = m_ClientTurnManager->GetCurrentTurn(); SendMessage(&loaded); } bool CNetClient::OnConnect(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_CONNECT_COMPLETE); CNetClient* client = (CNetClient*)context; CScriptValRooted msg; client->GetScriptInterface().Eval("({'type':'netstatus','status':'connected'})", msg); client->PushGuiMessage(msg); return true; } bool CNetClient::OnHandshake(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_SERVER_HANDSHAKE); CNetClient* client = (CNetClient*)context; CCliHandshakeMessage handshake; handshake.m_MagicResponse = PS_PROTOCOL_MAGIC_RESPONSE; handshake.m_ProtocolVersion = PS_PROTOCOL_VERSION; handshake.m_SoftwareVersion = PS_PROTOCOL_VERSION; client->SendMessage(&handshake); return true; } bool CNetClient::OnHandshakeResponse(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_SERVER_HANDSHAKE_RESPONSE); CNetClient* client = (CNetClient*)context; CAuthenticateMessage authenticate; authenticate.m_GUID = client->m_GUID; authenticate.m_Name = client->m_UserName; authenticate.m_Password = L""; // TODO client->SendMessage(&authenticate); return true; } bool CNetClient::OnAuthenticate(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_AUTHENTICATE_RESULT); CNetClient* client = (CNetClient*)context; CAuthenticateResultMessage* message = (CAuthenticateResultMessage*)event->GetParamRef(); LOGMESSAGE(L"Net: Authentication result: host=%d, %ls", message->m_HostID, message->m_Message.c_str()); bool isRejoining = (message->m_Code == ARC_OK_REJOINING); client->m_HostID = message->m_HostID; CScriptValRooted msg; client->GetScriptInterface().Eval("({'type':'netstatus','status':'authenticated'})", msg); client->GetScriptInterface().SetProperty(msg.get(), "rejoining", isRejoining); client->PushGuiMessage(msg); return true; } bool CNetClient::OnChat(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_CHAT); CNetClient* client = (CNetClient*)context; CChatMessage* message = (CChatMessage*)event->GetParamRef(); CScriptValRooted msg; client->GetScriptInterface().Eval("({'type':'chat'})", msg); client->GetScriptInterface().SetProperty(msg.get(), "guid", std::string(message->m_GUID), false); client->GetScriptInterface().SetProperty(msg.get(), "text", std::wstring(message->m_Message), false); client->PushGuiMessage(msg); return true; } bool CNetClient::OnGameSetup(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_GAME_SETUP); CNetClient* client = (CNetClient*)context; CGameSetupMessage* message = (CGameSetupMessage*)event->GetParamRef(); client->m_GameAttributes = message->m_Data; CScriptValRooted msg; client->GetScriptInterface().Eval("({'type':'gamesetup'})", msg); client->GetScriptInterface().SetProperty(msg.get(), "data", message->m_Data, false); client->PushGuiMessage(msg); return true; } bool CNetClient::OnPlayerAssignment(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_PLAYER_ASSIGNMENT); CNetClient* client = (CNetClient*)context; CPlayerAssignmentMessage* message = (CPlayerAssignmentMessage*)event->GetParamRef(); // Unpack the message PlayerAssignmentMap newPlayerAssignments; for (size_t i = 0; i < message->m_Hosts.size(); ++i) { PlayerAssignment assignment; assignment.m_Enabled = true; assignment.m_Name = message->m_Hosts[i].m_Name; assignment.m_PlayerID = message->m_Hosts[i].m_PlayerID; newPlayerAssignments[message->m_Hosts[i].m_GUID] = assignment; } client->m_PlayerAssignments.swap(newPlayerAssignments); client->PostPlayerAssignmentsToScript(); return true; } bool CNetClient::OnGameStart(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_GAME_START); CNetClient* client = (CNetClient*)context; // Find the player assigned to our GUID int player = -1; if (client->m_PlayerAssignments.find(client->m_GUID) != client->m_PlayerAssignments.end()) player = client->m_PlayerAssignments[client->m_GUID].m_PlayerID; client->m_ClientTurnManager = new CNetClientTurnManager( *client->m_Game->GetSimulation2(), *client, client->m_HostID, client->m_Game->GetReplayLogger()); client->m_Game->SetPlayerID(player); - client->m_Game->StartGame(client->m_GameAttributes); + client->m_Game->StartGame(client->m_GameAttributes, ""); CScriptValRooted msg; client->GetScriptInterface().Eval("({'type':'start'})", msg); client->PushGuiMessage(msg); return true; } bool CNetClient::OnJoinSyncStart(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_JOIN_SYNC_START); CNetClient* client = (CNetClient*)context; // The server wants us to start downloading the game state from it, so do so client->m_Session->GetFileTransferer().StartTask( shared_ptr(new CNetFileReceiveTask_ClientRejoin(*client)) ); return true; } bool CNetClient::OnJoinSyncEndCommandBatch(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_END_COMMAND_BATCH); CNetClient* client = (CNetClient*)context; CEndCommandBatchMessage* endMessage = (CEndCommandBatchMessage*)event->GetParamRef(); client->m_ClientTurnManager->FinishedAllCommands(endMessage->m_Turn, endMessage->m_TurnLength); // Execute all the received commands for the latest turn client->m_ClientTurnManager->UpdateFastForward(); return true; } bool CNetClient::OnLoadedGame(void* context, CFsmEvent* event) { ENSURE(event->GetType() == (uint)NMT_LOADED_GAME); CNetClient* client = (CNetClient*)context; // All players have loaded the game - start running the turn manager // so that the game begins client->m_Game->SetTurnManager(client->m_ClientTurnManager); CScriptValRooted msg; client->GetScriptInterface().Eval("({'type':'netstatus','status':'active'})", msg); client->PushGuiMessage(msg); return true; } bool CNetClient::OnInGame(void *context, CFsmEvent* event) { // TODO: should split each of these cases into a separate method CNetClient* client = (CNetClient*)context; CNetMessage* message = (CNetMessage*)event->GetParamRef(); if (message) { if (message->GetType() == NMT_SIMULATION_COMMAND) { CSimulationMessage* simMessage = static_cast (message); client->m_ClientTurnManager->OnSimulationMessage(simMessage); } else if (message->GetType() == NMT_SYNC_ERROR) { CSyncErrorMessage* syncMessage = static_cast (message); client->m_ClientTurnManager->OnSyncError(syncMessage->m_Turn, syncMessage->m_HashExpected); } else if (message->GetType() == NMT_END_COMMAND_BATCH) { CEndCommandBatchMessage* endMessage = static_cast (message); client->m_ClientTurnManager->FinishedAllCommands(endMessage->m_Turn, endMessage->m_TurnLength); } } return true; } CStr CNetClient::GenerateGUID() { // TODO: Ideally this will be guaranteed unique (and verified // cryptographically) since we'll rely on it to identify hosts // and associate them with player controls (e.g. to support // leaving/rejoining in-progress games), and we don't want // a host to masquerade as someone else. // For now, just try to pick a very random number. CStr guid; for (size_t i = 0; i < 2; ++i) { u32 r = 0; sys_generate_random_bytes((u8*)&r, sizeof(r)); char buf[32]; sprintf_s(buf, ARRAY_SIZE(buf), "%08X", r); guid += buf; } return guid; } Index: ps/trunk/source/network/NetTurnManager.cpp =================================================================== --- ps/trunk/source/network/NetTurnManager.cpp (revision 10453) +++ ps/trunk/source/network/NetTurnManager.cpp (revision 10454) @@ -1,581 +1,582 @@ /* Copyright (C) 2011 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 "NetTurnManager.h" #include "network/NetServer.h" #include "network/NetClient.h" #include "network/NetMessage.h" #include "gui/GUIManager.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/Profile.h" #include "ps/Pyrogenesis.h" #include "ps/Replay.h" +#include "ps/SavedGame.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" #include #include #include static const int DEFAULT_TURN_LENGTH_MP = 500; static const int DEFAULT_TURN_LENGTH_SP = 200; static const int COMMAND_DELAY = 2; #if 0 #define NETTURN_LOG(args) debug_printf args #else #define NETTURN_LOG(args) #endif static std::wstring Hexify(const std::string& s) { std::wstringstream str; str << std::hex; for (size_t i = 0; i < s.size(); ++i) str << std::setfill(L'0') << std::setw(2) << (int)(unsigned char)s[i]; return str.str(); } CNetTurnManager::CNetTurnManager(CSimulation2& simulation, u32 defaultTurnLength, int clientId, IReplayLogger& replay) : m_Simulation2(simulation), m_CurrentTurn(0), m_ReadyTurn(1), m_TurnLength(defaultTurnLength), m_DeltaTime(0), m_PlayerId(-1), m_ClientId(clientId), m_HasSyncError(false), m_Replay(replay), m_TimeWarpNumTurns(0) { // When we are on turn n, we schedule new commands for n+2. // We know that all other clients have finished scheduling commands for n (else we couldn't have got here). // We know we have not yet finished scheduling commands for n+2. // Hence other clients can be on turn n-1, n, n+1, and no other. // So they can be sending us commands scheduled for n+1, n+2, n+3. // So we need a 3-element buffer: m_QueuedCommands.resize(COMMAND_DELAY + 1); } void CNetTurnManager::ResetState(u32 newCurrentTurn, u32 newReadyTurn) { m_CurrentTurn = newCurrentTurn; m_ReadyTurn = newReadyTurn; m_DeltaTime = 0; size_t queuedCommandsSize = m_QueuedCommands.size(); m_QueuedCommands.clear(); m_QueuedCommands.resize(queuedCommandsSize); } void CNetTurnManager::SetPlayerID(int playerId) { m_PlayerId = playerId; } bool CNetTurnManager::WillUpdate(float frameLength) { // Keep this in sync with the return value of Update() if (m_DeltaTime + frameLength < 0) return false; if (m_ReadyTurn <= m_CurrentTurn) return false; return true; } bool CNetTurnManager::Update(float frameLength, size_t maxTurns) { m_DeltaTime += frameLength; // If we haven't reached the next turn yet, do nothing if (m_DeltaTime < 0) return false; NETTURN_LOG((L"Update current=%d ready=%d\n", m_CurrentTurn, m_ReadyTurn)); // Check that the next turn is ready for execution if (m_ReadyTurn <= m_CurrentTurn) { // Oops, we wanted to start the next turn but it's not ready yet - // there must be too much network lag. // TODO: complain to the user. // TODO: send feedback to the server to increase the turn length. // Reset the next-turn timer to 0 so we try again next update but // so we don't rush to catch up in subsequent turns. // TODO: we should do clever rate adjustment instead of just pausing like this. m_DeltaTime = 0; return false; } maxTurns = std::max((size_t)1, maxTurns); // always do at least one turn for (size_t i = 0; i < maxTurns; ++i) { // Check that we've reached the i'th next turn if (m_DeltaTime < 0) break; // Check that the i'th next turn is still ready if (m_ReadyTurn <= m_CurrentTurn) break; NotifyFinishedOwnCommands(m_CurrentTurn + COMMAND_DELAY); m_CurrentTurn += 1; // increase the turn number now, so Update can send new commands for a subsequent turn // Clean up any destroyed entities since the last turn (e.g. placement previews // or rally point flags generated by the GUI). (Must do this before the time warp // serialization.) m_Simulation2.FlushDestroyedEntities(); // Save the current state for rewinding, if enabled if (m_TimeWarpNumTurns && (m_CurrentTurn % m_TimeWarpNumTurns) == 0) { PROFILE("time warp serialization"); std::stringstream stream; m_Simulation2.SerializeState(stream); m_TimeWarpStates.push_back(stream.str()); } // Put all the client commands into a single list, in a globally consistent order std::vector commands; for (std::map >::iterator it = m_QueuedCommands[0].begin(); it != m_QueuedCommands[0].end(); ++it) { commands.insert(commands.end(), it->second.begin(), it->second.end()); } m_QueuedCommands.pop_front(); m_QueuedCommands.resize(m_QueuedCommands.size() + 1); m_Replay.Turn(m_CurrentTurn-1, m_TurnLength, commands); NETTURN_LOG((L"Running %d cmds\n", commands.size())); m_Simulation2.Update(m_TurnLength, commands); NotifyFinishedUpdate(m_CurrentTurn); // Set the time for the next turn update m_DeltaTime -= m_TurnLength / 1000.f; } return true; } bool CNetTurnManager::UpdateFastForward() { m_DeltaTime = 0; NETTURN_LOG((L"UpdateFastForward current=%d ready=%d\n", m_CurrentTurn, m_ReadyTurn)); // Check that the next turn is ready for execution if (m_ReadyTurn <= m_CurrentTurn) return false; while (m_ReadyTurn > m_CurrentTurn) { // TODO: It would be nice to remove some of the duplication with Update() // (This is similar but doesn't call any Notify functions or update DeltaTime, // it just updates the simulation state) m_CurrentTurn += 1; m_Simulation2.FlushDestroyedEntities(); // Put all the client commands into a single list, in a globally consistent order std::vector commands; for (std::map >::iterator it = m_QueuedCommands[0].begin(); it != m_QueuedCommands[0].end(); ++it) { commands.insert(commands.end(), it->second.begin(), it->second.end()); } m_QueuedCommands.pop_front(); m_QueuedCommands.resize(m_QueuedCommands.size() + 1); m_Replay.Turn(m_CurrentTurn-1, m_TurnLength, commands); NETTURN_LOG((L"Running %d cmds\n", commands.size())); m_Simulation2.Update(m_TurnLength, commands); } return true; } void CNetTurnManager::OnSyncError(u32 turn, const std::string& expectedHash) { NETTURN_LOG((L"OnSyncError(%d, %ls)\n", turn, Hexify(expectedHash).c_str())); // Only complain the first time if (m_HasSyncError) return; m_HasSyncError = true; bool quick = !TurnNeedsFullHash(turn); std::string hash; bool ok = m_Simulation2.ComputeStateHash(hash, quick); ENSURE(ok); OsPath path = psLogDir()/"oos_dump.txt"; std::ofstream file (OsString(path).c_str(), std::ofstream::out | std::ofstream::trunc); m_Simulation2.DumpDebugState(file); file.close(); std::wstringstream msg; msg << L"Out of sync on turn " << turn << L": expected hash " << Hexify(expectedHash) << L"\n\n"; msg << L"Current state: turn " << m_CurrentTurn << L", hash " << Hexify(hash) << L"\n\n"; msg << L"Dumping current state to " << path; if (g_GUI) g_GUI->DisplayMessageBox(600, 350, L"Sync error", msg.str()); else LOGERROR(L"%ls", msg.str().c_str()); } void CNetTurnManager::Interpolate(float frameLength) { // TODO: using m_TurnLength might be a bit dodgy when length changes - maybe // we need to save the previous turn length? float offset = clamp(m_DeltaTime / (m_TurnLength / 1000.f) + 1.0, 0.0, 1.0); m_Simulation2.Interpolate(frameLength, offset); } void CNetTurnManager::AddCommand(int client, int player, CScriptValRooted data, u32 turn) { NETTURN_LOG((L"AddCommand(client=%d player=%d turn=%d)\n", client, player, turn)); if (!(m_CurrentTurn < turn && turn <= m_CurrentTurn + COMMAND_DELAY + 1)) { debug_warn(L"Received command for invalid turn"); return; } SimulationCommand cmd; cmd.player = player; cmd.data = data; m_QueuedCommands[turn - (m_CurrentTurn+1)][client].push_back(cmd); } void CNetTurnManager::FinishedAllCommands(u32 turn, u32 turnLength) { NETTURN_LOG((L"FinishedAllCommands(%d, %d)\n", turn, turnLength)); ENSURE(turn == m_ReadyTurn + 1); m_ReadyTurn = turn; m_TurnLength = turnLength; } bool CNetTurnManager::TurnNeedsFullHash(u32 turn) { // Check immediately for errors caused by e.g. inconsistent game versions // (The hash is computed after the first sim update, so we start at turn == 1) if (turn == 1) return true; // Otherwise check the full state every ~10 seconds in multiplayer games // (TODO: should probably remove this when we're reasonably sure the game // isn't too buggy, since the full hash is still pretty slow) if (turn % 20 == 0) return true; return false; } void CNetTurnManager::EnableTimeWarpRecording(size_t numTurns) { m_TimeWarpStates.clear(); m_TimeWarpNumTurns = numTurns; } void CNetTurnManager::RewindTimeWarp() { if (m_TimeWarpStates.empty()) return; std::stringstream stream(m_TimeWarpStates.back()); m_Simulation2.DeserializeState(stream); m_TimeWarpStates.pop_back(); // Reset the turn manager state, so we won't execute stray commands and // won't do the next snapshot until the appropriate time. // (Ideally we ought to serialise the turn manager state and restore it // here, but this is simpler for now.) ResetState(0, 1); } void CNetTurnManager::QuickSave() { std::stringstream stream; bool ok = m_Simulation2.SerializeState(stream); if (!ok) { LOGERROR(L"Failed to quicksave game"); return; } LOGMESSAGERENDER(L"Quicksaved game"); m_QuickSaveState = stream.str(); } void CNetTurnManager::QuickLoad() { if (m_QuickSaveState.empty()) { LOGERROR(L"Cannot quickload game - no game was quicksaved"); return; } std::stringstream stream(m_QuickSaveState); bool ok = m_Simulation2.DeserializeState(stream); if (!ok) { LOGERROR(L"Failed to quickload game"); return; } LOGMESSAGERENDER(L"Quickloaded game"); // See RewindTimeWarp ResetState(0, 1); } CNetClientTurnManager::CNetClientTurnManager(CSimulation2& simulation, CNetClient& client, int clientId, IReplayLogger& replay) : CNetTurnManager(simulation, DEFAULT_TURN_LENGTH_MP, clientId, replay), m_NetClient(client) { } void CNetClientTurnManager::PostCommand(CScriptValRooted data) { NETTURN_LOG((L"PostCommand()\n")); // Transmit command to server CSimulationMessage msg(m_Simulation2.GetScriptInterface(), m_ClientId, m_PlayerId, m_CurrentTurn + COMMAND_DELAY, data.get()); m_NetClient.SendMessage(&msg); // Add to our local queue //AddCommand(m_ClientId, m_PlayerId, data, m_CurrentTurn + COMMAND_DELAY); // TODO: we should do this when the server stops sending our commands back to us } void CNetClientTurnManager::NotifyFinishedOwnCommands(u32 turn) { NETTURN_LOG((L"NotifyFinishedOwnCommands(%d)\n", turn)); // Send message to the server CEndCommandBatchMessage msg; msg.m_TurnLength = DEFAULT_TURN_LENGTH_MP; // TODO: why do we send this? msg.m_Turn = turn; m_NetClient.SendMessage(&msg); } void CNetClientTurnManager::NotifyFinishedUpdate(u32 turn) { bool quick = !TurnNeedsFullHash(turn); std::string hash; { PROFILE("state hash check"); bool ok = m_Simulation2.ComputeStateHash(hash, quick); ENSURE(ok); } NETTURN_LOG((L"NotifyFinishedUpdate(%d, %ls)\n", turn, Hexify(hash).c_str())); m_Replay.Hash(hash, quick); // Send message to the server CSyncCheckMessage msg; msg.m_Turn = turn; msg.m_Hash = hash; m_NetClient.SendMessage(&msg); } void CNetClientTurnManager::OnSimulationMessage(CSimulationMessage* msg) { // Command received from the server - store it for later execution AddCommand(msg->m_Client, msg->m_Player, msg->m_Data, msg->m_Turn); } CNetLocalTurnManager::CNetLocalTurnManager(CSimulation2& simulation, IReplayLogger& replay) : CNetTurnManager(simulation, DEFAULT_TURN_LENGTH_SP, 0, replay) { } void CNetLocalTurnManager::PostCommand(CScriptValRooted data) { // Add directly to the next turn, ignoring COMMAND_DELAY, // because we don't need to compensate for network latency AddCommand(m_ClientId, m_PlayerId, data, m_CurrentTurn + 1); } void CNetLocalTurnManager::NotifyFinishedOwnCommands(u32 turn) { FinishedAllCommands(turn, m_TurnLength); } void CNetLocalTurnManager::NotifyFinishedUpdate(u32 UNUSED(turn)) { #if 0 // this hurts performance and is only useful for verifying log replays std::string hash; { PROFILE("state hash check"); bool ok = m_Simulation2.ComputeStateHash(hash); ENSURE(ok); } m_Replay.Hash(hash); #endif } void CNetLocalTurnManager::OnSimulationMessage(CSimulationMessage* UNUSED(msg)) { debug_warn(L"This should never be called"); } CNetServerTurnManager::CNetServerTurnManager(CNetServerWorker& server) : m_NetServer(server), m_ReadyTurn(1), m_TurnLength(DEFAULT_TURN_LENGTH_MP) { // The first turn we will actually execute is number 2, // so store dummy values into the saved lengths list m_SavedTurnLengths.push_back(0); m_SavedTurnLengths.push_back(0); } void CNetServerTurnManager::NotifyFinishedClientCommands(int client, u32 turn) { NETTURN_LOG((L"NotifyFinishedClientCommands(client=%d, turn=%d)\n", client, turn)); // Must be a client we've already heard of ENSURE(m_ClientsReady.find(client) != m_ClientsReady.end()); // Clients must advance one turn at a time ENSURE(turn == m_ClientsReady[client] + 1); m_ClientsReady[client] = turn; // Check whether this was the final client to become ready CheckClientsReady(); } void CNetServerTurnManager::CheckClientsReady() { // See if all clients (including self) are ready for a new turn for (std::map::iterator it = m_ClientsReady.begin(); it != m_ClientsReady.end(); ++it) { NETTURN_LOG((L" %d: %d <=? %d\n", it->first, it->second, m_ReadyTurn)); if (it->second <= m_ReadyTurn) return; // wasn't ready for m_ReadyTurn+1 } // Advance the turn ++m_ReadyTurn; NETTURN_LOG((L"CheckClientsReady: ready for turn %d\n", m_ReadyTurn)); // Tell all clients that the next turn is ready CEndCommandBatchMessage msg; msg.m_TurnLength = m_TurnLength; msg.m_Turn = m_ReadyTurn; m_NetServer.Broadcast(&msg); // Save the turn length in case it's needed later ENSURE(m_SavedTurnLengths.size() == m_ReadyTurn); m_SavedTurnLengths.push_back(m_TurnLength); } void CNetServerTurnManager::NotifyFinishedClientUpdate(int client, u32 turn, const std::string& hash) { // Clients must advance one turn at a time ENSURE(turn == m_ClientsSimulated[client] + 1); m_ClientsSimulated[client] = turn; m_ClientStateHashes[turn][client] = hash; // Find the newest turn which we know all clients have simulated u32 newest = std::numeric_limits::max(); for (std::map::iterator it = m_ClientsSimulated.begin(); it != m_ClientsSimulated.end(); ++it) { if (it->second < newest) newest = it->second; } // For every set of state hashes that all clients have simulated, check for OOS for (std::map >::iterator it = m_ClientStateHashes.begin(); it != m_ClientStateHashes.end(); ++it) { if (it->first > newest) break; // Assume the host is correct (maybe we should choose the most common instead to help debugging) std::string expected = it->second.begin()->second; for (std::map::iterator cit = it->second.begin(); cit != it->second.end(); ++cit) { NETTURN_LOG((L"sync check %d: %d = %ls\n", it->first, cit->first, Hexify(cit->second).c_str())); if (cit->second != expected) { // Oh no, out of sync // Tell everyone about it CSyncErrorMessage msg; msg.m_Turn = it->first; msg.m_HashExpected = expected; m_NetServer.Broadcast(&msg); break; } } } // Delete the saved hashes for all turns that we've already verified m_ClientStateHashes.erase(m_ClientStateHashes.begin(), m_ClientStateHashes.lower_bound(newest+1)); } void CNetServerTurnManager::InitialiseClient(int client, u32 turn) { NETTURN_LOG((L"InitialiseClient(client=%d, turn=%d)\n", client, turn)); ENSURE(m_ClientsReady.find(client) == m_ClientsReady.end()); m_ClientsReady[client] = turn + 1; m_ClientsSimulated[client] = turn; } void CNetServerTurnManager::UninitialiseClient(int client) { NETTURN_LOG((L"UninitialiseClient(client=%d)\n", client)); ENSURE(m_ClientsReady.find(client) != m_ClientsReady.end()); m_ClientsReady.erase(client); m_ClientsSimulated.erase(client); // Check whether we're ready for the next turn now that we're not // waiting for this client any more CheckClientsReady(); } void CNetServerTurnManager::SetTurnLength(u32 msecs) { m_TurnLength = msecs; } u32 CNetServerTurnManager::GetSavedTurnLength(u32 turn) { ENSURE(turn <= m_ReadyTurn); return m_SavedTurnLengths.at(turn); } Index: ps/trunk/source/tools/atlas/GameInterface/Handlers/MapHandlers.cpp =================================================================== --- ps/trunk/source/tools/atlas/GameInterface/Handlers/MapHandlers.cpp (revision 10453) +++ ps/trunk/source/tools/atlas/GameInterface/Handlers/MapHandlers.cpp (revision 10454) @@ -1,254 +1,254 @@ /* Copyright (C) 2011 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 "MessageHandler.h" #include "../GameLoop.h" #include "../CommandProc.h" #include "graphics/GameView.h" #include "graphics/LOSTexture.h" #include "graphics/MapWriter.h" #include "graphics/Patch.h" #include "graphics/Terrain.h" #include "graphics/TerrainTextureEntry.h" #include "graphics/TerrainTextureManager.h" #include "ps/Game.h" #include "ps/Loader.h" #include "ps/World.h" #include "renderer/Renderer.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" #include "simulation2/components/ICmpPlayer.h" #include "simulation2/components/ICmpPlayerManager.h" #include "simulation2/components/ICmpPosition.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/components/ICmpTerrain.h" namespace { void InitGame() { if (g_Game) { delete g_Game; g_Game = NULL; } g_Game = new CGame(); // Default to player 1 for playtesting g_Game->SetPlayerID(1); } void StartGame(const CScriptValRooted& attrs) { - g_Game->StartGame(attrs); + g_Game->StartGame(attrs, ""); // TODO: Non progressive load can fail - need a decent way to handle this LDR_NonprogressiveLoad(); PSRETURN ret = g_Game->ReallyStartGame(); ENSURE(ret == PSRETURN_OK); // Disable fog-of-war CmpPtr cmpRangeManager(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (!cmpRangeManager.null()) cmpRangeManager->SetLosRevealAll(-1, true); } } namespace AtlasMessage { QUERYHANDLER(GenerateMap) { InitGame(); // Random map ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); CScriptValRooted settings = scriptInterface.ParseJSON(*msg->settings); CScriptValRooted attrs; scriptInterface.Eval("({})", attrs); scriptInterface.SetProperty(attrs.get(), "mapType", std::string("random")); scriptInterface.SetProperty(attrs.get(), "script", std::wstring(*msg->filename)); scriptInterface.SetProperty(attrs.get(), "settings", settings, false); try { StartGame(attrs); msg->status = 0; } catch (PSERROR_Game_World_MapLoadFailed e) { // Cancel loading LDR_Cancel(); msg->status = -1; } } MESSAGEHANDLER(LoadMap) { InitGame(); // Scenario CStrW map = *msg->filename; CStrW mapBase = map.BeforeLast(L".pmp"); // strip the file extension, if any ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); CScriptValRooted attrs; scriptInterface.Eval("({})", attrs); scriptInterface.SetProperty(attrs.get(), "mapType", std::string("scenario")); scriptInterface.SetProperty(attrs.get(), "map", std::wstring(mapBase)); StartGame(attrs); } MESSAGEHANDLER(SaveMap) { CMapWriter writer; const VfsPath pathname = VfsPath("maps/scenarios") / *msg->filename; writer.SaveMap(pathname, g_Game->GetWorld()->GetTerrain(), g_Renderer.GetWaterManager(), g_Renderer.GetSkyManager(), &g_LightEnv, g_Game->GetView()->GetCamera(), g_Game->GetView()->GetCinema(), g_Game->GetSimulation2()); } QUERYHANDLER(GetMapSettings) { msg->settings = g_Game->GetSimulation2()->GetMapSettingsString(); } BEGIN_COMMAND(SetMapSettings) { std::string m_OldSettings, m_NewSettings; void SetSettings(const std::string& settings) { g_Game->GetSimulation2()->SetMapSettings(settings); } void Do() { m_OldSettings = g_Game->GetSimulation2()->GetMapSettingsString(); m_NewSettings = *msg->settings; SetSettings(m_NewSettings); } // TODO: we need some way to notify the Atlas UI when the settings are changed // externally, otherwise this will have no visible effect void Undo() { // SetSettings(m_OldSettings); } void Redo() { // SetSettings(m_NewSettings); } void MergeIntoPrevious(cSetMapSettings* prev) { prev->m_NewSettings = m_NewSettings; } }; END_COMMAND(SetMapSettings) MESSAGEHANDLER(LoadPlayerSettings) { g_Game->GetSimulation2()->LoadPlayerSettings(msg->newplayers); } QUERYHANDLER(GetMapSizes) { msg->sizes = g_Game->GetSimulation2()->GetMapSizes(); } QUERYHANDLER(GetRMSData) { msg->data = g_Game->GetSimulation2()->GetRMSData(); } BEGIN_COMMAND(ResizeMap) { int m_OldTiles, m_NewTiles; cResizeMap() { } void MakeDirty() { CmpPtr cmpTerrain(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (!cmpTerrain.null()) cmpTerrain->ReloadTerrain(); // The LOS texture won't normally get updated when running Atlas // (since there's no simulation updates), so explicitly dirty it g_Game->GetView()->GetLOSTexture().MakeDirty(); } void ResizeTerrain(int tiles) { CTerrain* terrain = g_Game->GetWorld()->GetTerrain(); terrain->Resize(tiles / PATCH_SIZE); MakeDirty(); } void Do() { CmpPtr cmpTerrain(*g_Game->GetSimulation2(), SYSTEM_ENTITY); if (cmpTerrain.null()) { m_OldTiles = m_NewTiles = 0; } else { m_OldTiles = (int)cmpTerrain->GetTilesPerSide(); m_NewTiles = msg->tiles; } ResizeTerrain(m_NewTiles); } void Undo() { ResizeTerrain(m_OldTiles); } void Redo() { ResizeTerrain(m_NewTiles); } }; END_COMMAND(ResizeMap) QUERYHANDLER(VFSFileExists) { msg->exists = VfsFileExists(*msg->path); } } Index: ps/trunk/source/lib/file/archive/archive_zip.cpp =================================================================== --- ps/trunk/source/lib/file/archive/archive_zip.cpp (revision 10453) +++ ps/trunk/source/lib/file/archive/archive_zip.cpp (revision 10454) @@ -1,719 +1,741 @@ /* 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. */ /* * archive backend for Zip files. */ #include "precompiled.h" #include "lib/file/archive/archive_zip.h" #include #include #include "lib/utf8.h" #include "lib/bits.h" #include "lib/byte_order.h" #include "lib/allocators/pool.h" #include "lib/sysdep/filesystem.h" #include "lib/file/archive/archive.h" #include "lib/file/archive/codec_zlib.h" #include "lib/file/archive/stream.h" #include "lib/file/file.h" #include "lib/file/io/io.h" //----------------------------------------------------------------------------- // timestamp conversion: DOS FAT <-> Unix time_t //----------------------------------------------------------------------------- static time_t time_t_from_FAT(u32 fat_timedate) { const u32 fat_time = bits(fat_timedate, 0, 15); const u32 fat_date = bits(fat_timedate, 16, 31); struct tm t; // struct tm format: t.tm_sec = bits(fat_time, 0,4) * 2; // [0,59] t.tm_min = bits(fat_time, 5,10); // [0,59] t.tm_hour = bits(fat_time, 11,15); // [0,23] t.tm_mday = bits(fat_date, 0,4); // [1,31] t.tm_mon = bits(fat_date, 5,8) - 1; // [0,11] t.tm_year = bits(fat_date, 9,15) + 80; // since 1900 t.tm_isdst = -1; // unknown - let libc determine // otherwise: totally bogus, and at the limit of 32-bit time_t ENSURE(t.tm_year < 138); time_t ret = mktime(&t); ENSURE(ret != (time_t)-1); // mktime shouldn't fail return ret; } static u32 FAT_from_time_t(time_t time) { // (values are adjusted for DST) struct tm* t = localtime(&time); const u16 fat_time = u16( (t->tm_sec/2) | // 5 (u16(t->tm_min) << 5) | // 6 (u16(t->tm_hour) << 11) // 5 ); const u16 fat_date = u16( (t->tm_mday) | // 5 (u16(t->tm_mon+1) << 5) | // 4 (u16(t->tm_year-80) << 9) // 7 ); u32 fat_timedate = u32_from_u16(fat_date, fat_time); return fat_timedate; } //----------------------------------------------------------------------------- // Zip archive definitions //----------------------------------------------------------------------------- static const u32 cdfh_magic = FOURCC_LE('P','K','\1','\2'); static const u32 lfh_magic = FOURCC_LE('P','K','\3','\4'); static const u32 ecdr_magic = FOURCC_LE('P','K','\5','\6'); enum ZipMethod { ZIP_METHOD_NONE = 0, ZIP_METHOD_DEFLATE = 8 }; #pragma pack(push, 1) class LFH { public: void Init(const FileInfo& fileInfo, off_t csize, ZipMethod method, u32 checksum, const Path& pathname) { const std::string pathnameUTF8 = utf8_from_wstring(pathname.string()); const size_t pathnameSize = pathnameUTF8.length(); m_magic = lfh_magic; m_x1 = to_le16(0); m_flags = to_le16(0); m_method = to_le16(u16_from_larger(method)); m_fat_mtime = to_le32(FAT_from_time_t(fileInfo.MTime())); m_crc = to_le32(checksum); m_csize = to_le32(u32_from_larger(csize)); m_usize = to_le32(u32_from_larger(fileInfo.Size())); m_fn_len = to_le16(u16_from_larger(pathnameSize)); m_e_len = to_le16(0); memcpy((char*)this + sizeof(LFH), pathnameUTF8.c_str(), pathnameSize); } size_t Size() const { ENSURE(m_magic == lfh_magic); size_t size = sizeof(LFH); size += read_le16(&m_fn_len); size += read_le16(&m_e_len); // note: LFH doesn't have a comment field! return size; } private: u32 m_magic; u16 m_x1; // version needed u16 m_flags; u16 m_method; u32 m_fat_mtime; // last modified time (DOS FAT format) u32 m_crc; u32 m_csize; u32 m_usize; u16 m_fn_len; u16 m_e_len; }; cassert(sizeof(LFH) == 30); class CDFH { public: void Init(const FileInfo& fileInfo, off_t ofs, off_t csize, ZipMethod method, u32 checksum, const Path& pathname, size_t slack) { const std::string pathnameUTF8 = utf8_from_wstring(pathname.string()); const size_t pathnameLength = pathnameUTF8.length(); m_magic = cdfh_magic; m_x1 = to_le32(0); m_flags = to_le16(0); m_method = to_le16(u16_from_larger(method)); m_fat_mtime = to_le32(FAT_from_time_t(fileInfo.MTime())); m_crc = to_le32(checksum); m_csize = to_le32(u32_from_larger(csize)); m_usize = to_le32(u32_from_larger(fileInfo.Size())); m_fn_len = to_le16(u16_from_larger(pathnameLength)); m_e_len = to_le16(0); m_c_len = to_le16(u16_from_larger((size_t)slack)); m_x2 = to_le32(0); m_x3 = to_le32(0); m_lfh_ofs = to_le32(u32_from_larger(ofs)); memcpy((char*)this + sizeof(CDFH), pathnameUTF8.c_str(), pathnameLength); } Path Pathname() const { const size_t length = (size_t)read_le16(&m_fn_len); const char* pathname = (const char*)this + sizeof(CDFH); // not 0-terminated! return Path(std::string(pathname, length)); } off_t HeaderOffset() const { return read_le32(&m_lfh_ofs); } off_t USize() const { return (off_t)read_le32(&m_usize); } off_t CSize() const { return (off_t)read_le32(&m_csize); } ZipMethod Method() const { return (ZipMethod)read_le16(&m_method); } u32 Checksum() const { return read_le32(&m_crc); } time_t MTime() const { const u32 fat_mtime = read_le32(&m_fat_mtime); return time_t_from_FAT(fat_mtime); } size_t Size() const { size_t size = sizeof(CDFH); size += read_le16(&m_fn_len); size += read_le16(&m_e_len); size += read_le16(&m_c_len); return size; } private: u32 m_magic; u32 m_x1; // versions u16 m_flags; u16 m_method; u32 m_fat_mtime; // last modified time (DOS FAT format) u32 m_crc; u32 m_csize; u32 m_usize; u16 m_fn_len; u16 m_e_len; u16 m_c_len; u32 m_x2; // spanning u32 m_x3; // attributes u32 m_lfh_ofs; }; cassert(sizeof(CDFH) == 46); class ECDR { public: void Init(size_t cd_numEntries, off_t cd_ofs, size_t cd_size) { m_magic = ecdr_magic; m_diskNum = to_le16(0); m_cd_diskNum = to_le16(0); m_cd_numEntriesOnDisk = to_le16(u16_from_larger(cd_numEntries)); m_cd_numEntries = m_cd_numEntriesOnDisk; m_cd_size = to_le32(u32_from_larger(cd_size)); m_cd_ofs = to_le32(u32_from_larger(cd_ofs)); m_comment_len = to_le16(0); } void Decompose(size_t& cd_numEntries, off_t& cd_ofs, size_t& cd_size) const { cd_numEntries = (size_t)read_le16(&m_cd_numEntries); cd_ofs = (off_t)read_le32(&m_cd_ofs); cd_size = (size_t)read_le32(&m_cd_size); } private: u32 m_magic; u16 m_diskNum; u16 m_cd_diskNum; u16 m_cd_numEntriesOnDisk; u16 m_cd_numEntries; u32 m_cd_size; u32 m_cd_ofs; u16 m_comment_len; }; cassert(sizeof(ECDR) == 22); #pragma pack(pop) //----------------------------------------------------------------------------- // ArchiveFile_Zip //----------------------------------------------------------------------------- class ArchiveFile_Zip : public IArchiveFile { public: ArchiveFile_Zip(const PFile& file, off_t ofs, off_t csize, u32 checksum, ZipMethod method) : m_file(file), m_ofs(ofs) , m_csize(csize), m_checksum(checksum), m_method((u16)method) , m_flags(NeedsFixup) { } virtual size_t Precedence() const { return 2u; } virtual wchar_t LocationCode() const { return 'A'; } virtual OsPath Path() const { return m_file->Pathname(); } virtual Status Load(const OsPath& UNUSED(name), const shared_ptr& buf, size_t size) const { AdjustOffset(); PICodec codec; switch(m_method) { case ZIP_METHOD_NONE: codec = CreateCodec_ZLibNone(); break; case ZIP_METHOD_DEFLATE: codec = CreateDecompressor_ZLibDeflate(); break; default: WARN_RETURN(ERR::ARCHIVE_UNKNOWN_METHOD); } Stream stream(codec); stream.SetOutputBuffer(buf.get(), size); io::Operation op(*m_file.get(), 0, m_csize, m_ofs); StreamFeeder streamFeeder(stream); RETURN_STATUS_IF_ERR(io::Run(op, io::Parameters(), streamFeeder)); RETURN_STATUS_IF_ERR(stream.Finish()); #if CODEC_COMPUTE_CHECKSUM ENSURE(m_checksum == stream.Checksum()); #endif return INFO::OK; } private: enum Flags { // indicates m_ofs points to a "local file header" instead of // the file data. a fixup routine is called when reading the file; // it skips past the LFH and clears this flag. // this is somewhat of a hack, but vital to archive open performance. // without it, we'd have to scan through the entire archive file, // which can take *seconds*. // (we cannot use the information in CDFH, because its 'extra' field // has been observed to differ from that of the LFH) // since we read the LFH right before the rest of the file, the block // cache will absorb the IO cost. NeedsFixup = 1 }; struct LFH_Copier { LFH_Copier(u8* lfh_dst, size_t lfh_bytes_remaining) : lfh_dst(lfh_dst), lfh_bytes_remaining(lfh_bytes_remaining) { } // this code grabs an LFH struct from file block(s) that are // passed to the callback. usually, one call copies the whole thing, // but the LFH may straddle a block boundary. // // rationale: this allows using temp buffers for zip_fixup_lfh, // which avoids involving the file buffer manager and thus // avoids cluttering the trace and cache contents. Status operator()(const u8* block, size_t size) const { ENSURE(size <= lfh_bytes_remaining); memcpy(lfh_dst, block, size); lfh_dst += size; lfh_bytes_remaining -= size; return INFO::OK; } mutable u8* lfh_dst; mutable size_t lfh_bytes_remaining; }; /** * fix up m_ofs (adjust it to point to cdata instead of the LFH). * * note: we cannot use CDFH filename and extra field lengths to skip * past LFH since that may not mirror CDFH (has happened). * * this is called at file-open time instead of while mounting to * reduce seeks: since reading the file will typically follow, the * block cache entirely absorbs the IO cost. **/ void AdjustOffset() const { if(!(m_flags & NeedsFixup)) return; m_flags &= ~NeedsFixup; // performance note: this ends up reading one file block, which is // only in the block cache if the file starts in the same block as a // previously read file (i.e. both are small). LFH lfh; io::Operation op(*m_file.get(), 0, sizeof(LFH), m_ofs); if(io::Run(op, io::Parameters(), LFH_Copier((u8*)&lfh, sizeof(LFH))) == INFO::OK) m_ofs += (off_t)lfh.Size(); } PFile m_file; // all relevant LFH/CDFH fields not covered by FileInfo mutable off_t m_ofs; off_t m_csize; u32 m_checksum; u16 m_method; mutable u16 m_flags; }; //----------------------------------------------------------------------------- // ArchiveReader_Zip //----------------------------------------------------------------------------- class ArchiveReader_Zip : public IArchiveReader { public: ArchiveReader_Zip(const OsPath& pathname) : m_file(new File(pathname, O_RDONLY)) { FileInfo fileInfo; GetFileInfo(pathname, &fileInfo); m_fileSize = fileInfo.Size(); const size_t minFileSize = sizeof(LFH)+sizeof(CDFH)+sizeof(ECDR); ENSURE(m_fileSize >= off_t(minFileSize)); } virtual Status ReadEntries(ArchiveEntryCallback cb, uintptr_t cbData) { // locate and read Central Directory off_t cd_ofs = 0; size_t cd_numEntries = 0; size_t cd_size = 0; RETURN_STATUS_IF_ERR(LocateCentralDirectory(m_file, m_fileSize, cd_ofs, cd_numEntries, cd_size)); UniqueRange buf(RVALUE(io::Allocate(cd_size))); io::Operation op(*m_file.get(), buf.get(), cd_size, cd_ofs); RETURN_STATUS_IF_ERR(io::Run(op)); // iterate over Central Directory const u8* pos = (const u8*)buf.get(); for(size_t i = 0; i < cd_numEntries; i++) { // scan for next CDFH CDFH* cdfh = (CDFH*)FindRecord((const u8*)buf.get(), cd_size, pos, cdfh_magic, sizeof(CDFH)); if(!cdfh) WARN_RETURN(ERR::CORRUPTED); const Path relativePathname(cdfh->Pathname()); if(!relativePathname.IsDirectory()) { const OsPath name = relativePathname.Filename(); FileInfo fileInfo(name, cdfh->USize(), cdfh->MTime()); shared_ptr archiveFile(new ArchiveFile_Zip(m_file, cdfh->HeaderOffset(), cdfh->CSize(), cdfh->Checksum(), cdfh->Method())); cb(relativePathname, fileInfo, archiveFile, cbData); } pos += cdfh->Size(); } return INFO::OK; } private: /** * Scan buffer for a Zip file record. * * @param buf * @param size * @param start position within buffer * @param magic signature of record * @param recordSize size of record (including signature) * @return pointer to record within buffer or 0 if not found. **/ static const u8* FindRecord(const u8* buf, size_t size, const u8* start, u32 magic, size_t recordSize) { // (don't use as the counter - otherwise we can't tell if // scanning within the buffer was necessary.) for(const u8* p = start; p <= buf+size-recordSize; p++) { // found it if(*(u32*)p == magic) { ENSURE(p == start); // otherwise, the archive is a bit broken return p; } } // passed EOF, didn't find it. // note: do not warn - this happens in the initial ECDR search at // EOF if the archive contains a comment field. return 0; } // search for ECDR in the last bytes of the file. // if found, fill with a copy of the (little-endian) ECDR and // return INFO::OK, otherwise IO error or ERR::CORRUPTED. static Status ScanForEcdr(const PFile& file, off_t fileSize, u8* buf, size_t maxScanSize, size_t& cd_numEntries, off_t& cd_ofs, size_t& cd_size) { // don't scan more than the entire file const size_t scanSize = std::min(maxScanSize, size_t(fileSize)); // read desired chunk of file into memory const off_t ofs = fileSize - off_t(scanSize); io::Operation op(*file.get(), buf, scanSize, ofs); RETURN_STATUS_IF_ERR(io::Run(op)); // look for ECDR in buffer const ECDR* ecdr = (const ECDR*)FindRecord(buf, scanSize, buf, ecdr_magic, sizeof(ECDR)); if(!ecdr) return INFO::CANNOT_HANDLE; ecdr->Decompose(cd_numEntries, cd_ofs, cd_size); return INFO::OK; } static Status LocateCentralDirectory(const PFile& file, off_t fileSize, off_t& cd_ofs, size_t& cd_numEntries, size_t& cd_size) { const size_t maxScanSize = 66000u; // see below UniqueRange buf(RVALUE(io::Allocate(maxScanSize))); // expected case: ECDR at EOF; no file comment Status ret = ScanForEcdr(file, fileSize, (u8*)buf.get(), sizeof(ECDR), cd_numEntries, cd_ofs, cd_size); if(ret == INFO::OK) return INFO::OK; // worst case: ECDR precedes 64 KiB of file comment ret = ScanForEcdr(file, fileSize, (u8*)buf.get(), maxScanSize, cd_numEntries, cd_ofs, cd_size); if(ret == INFO::OK) return INFO::OK; // both ECDR scans failed - this is not a valid Zip file. io::Operation op(*file.get(), buf.get(), sizeof(LFH)); RETURN_STATUS_IF_ERR(io::Run(op)); // the Zip file has an LFH but lacks an ECDR. this can happen if // the user hard-exits while an archive is being written. // notes: // - return ERR::CORRUPTED so VFS will not include this file. // - we could work around this by scanning all LFHs, but won't bother // because it'd be slow. // - do not warn - the corrupt archive will be deleted on next // successful archive builder run anyway. if(FindRecord((const u8*)buf.get(), sizeof(LFH), (const u8*)buf.get(), lfh_magic, sizeof(LFH))) return ERR::CORRUPTED; // NOWARN // totally bogus else WARN_RETURN(ERR::ARCHIVE_UNKNOWN_FORMAT); } PFile m_file; off_t m_fileSize; }; PIArchiveReader CreateArchiveReader_Zip(const OsPath& archivePathname) { return PIArchiveReader(new ArchiveReader_Zip(archivePathname)); } //----------------------------------------------------------------------------- // ArchiveWriter_Zip //----------------------------------------------------------------------------- class ArchiveWriter_Zip : public IArchiveWriter { public: ArchiveWriter_Zip(const OsPath& archivePathname, bool noDeflate) : m_file(new File(archivePathname, O_WRONLY)), m_fileSize(0) , m_numEntries(0), m_noDeflate(noDeflate) { THROW_STATUS_IF_ERR(pool_create(&m_cdfhPool, 10*MiB, 0)); } ~ArchiveWriter_Zip() { // append an ECDR to the CDFH list (this allows us to // write out both to the archive file in one burst) const size_t cd_size = m_cdfhPool.da.pos; ECDR* ecdr = (ECDR*)pool_alloc(&m_cdfhPool, sizeof(ECDR)); if(!ecdr) throw std::bad_alloc(); const off_t cd_ofs = m_fileSize; ecdr->Init(m_numEntries, cd_ofs, cd_size); if(write(m_file->Descriptor(), m_cdfhPool.da.base, cd_size+sizeof(ECDR)) < 0) DEBUG_WARN_ERR(ERR::IO); // no way to return error code (void)pool_destroy(&m_cdfhPool); } Status AddFile(const OsPath& pathname, const OsPath& pathnameInArchive) { FileInfo fileInfo; RETURN_STATUS_IF_ERR(GetFileInfo(pathname, &fileInfo)); + + PFile file(new File); + RETURN_STATUS_IF_ERR(file->Open(pathname, O_RDONLY)); + + return AddFileOrMemory(fileInfo, pathnameInArchive, file, NULL); + } + + Status AddMemory(const u8* data, size_t size, time_t mtime, const OsPath& pathnameInArchive) + { + FileInfo fileInfo(pathnameInArchive, size, mtime); + + return AddFileOrMemory(fileInfo, pathnameInArchive, PFile(), data); + } + + Status AddFileOrMemory(const FileInfo& fileInfo, const OsPath& pathnameInArchive, const PFile& file, const u8* data) + { + ENSURE((file && !data) || (data && !file)); + const off_t usize = fileInfo.Size(); // skip 0-length files. // rationale: zip.cpp needs to determine whether a CDFH entry is // a file or directory (the latter are written by some programs but // not needed - they'd only pollute the file table). // it looks like checking for usize=csize=0 is the safest way - // relying on file attributes (which are system-dependent!) is // even less safe. // we thus skip 0-length files to avoid confusing them with directories. if(!usize) return INFO::SKIPPED; - PFile file(new File); - RETURN_STATUS_IF_ERR(file->Open(pathname, O_RDONLY)); - const size_t pathnameLength = pathnameInArchive.string().length(); // choose method and the corresponding codec ZipMethod method; PICodec codec; if(m_noDeflate || IsFileTypeIncompressible(pathnameInArchive)) { method = ZIP_METHOD_NONE; codec = CreateCodec_ZLibNone(); } else { method = ZIP_METHOD_DEFLATE; codec = CreateCompressor_ZLibDeflate(); } // allocate memory const size_t csizeMax = codec->MaxOutputSize(size_t(usize)); UniqueRange buf(RVALUE(io::Allocate(sizeof(LFH) + pathnameLength + csizeMax))); // read and compress file contents size_t csize; u32 checksum; { u8* cdata = (u8*)buf.get() + sizeof(LFH) + pathnameLength; Stream stream(codec); stream.SetOutputBuffer(cdata, csizeMax); - io::Operation op(*file.get(), 0, usize); StreamFeeder streamFeeder(stream); - RETURN_STATUS_IF_ERR(io::Run(op, io::Parameters(), streamFeeder)); + if(file) + { + io::Operation op(*file.get(), 0, usize); + RETURN_STATUS_IF_ERR(io::Run(op, io::Parameters(), streamFeeder)); + } + else + { + RETURN_STATUS_IF_ERR(streamFeeder(data, usize)); + } RETURN_STATUS_IF_ERR(stream.Finish()); csize = stream.OutSize(); checksum = stream.Checksum(); } // build LFH { LFH* lfh = (LFH*)buf.get(); lfh->Init(fileInfo, (off_t)csize, method, checksum, pathnameInArchive); } // append a CDFH to the central directory (in memory) const off_t ofs = m_fileSize; const size_t prev_pos = m_cdfhPool.da.pos; // (required to determine padding size) const size_t cdfhSize = sizeof(CDFH) + pathnameLength; CDFH* cdfh = (CDFH*)pool_alloc(&m_cdfhPool, cdfhSize); if(!cdfh) WARN_RETURN(ERR::NO_MEM); const size_t slack = m_cdfhPool.da.pos - prev_pos - cdfhSize; cdfh->Init(fileInfo, ofs, (off_t)csize, method, checksum, pathnameInArchive, slack); m_numEntries++; // write LFH, pathname and cdata to file const size_t packageSize = sizeof(LFH) + pathnameLength + csize; if(write(m_file->Descriptor(), buf.get(), packageSize) < 0) WARN_RETURN(ERR::IO); m_fileSize += (off_t)packageSize; return INFO::OK; } private: static bool IsFileTypeIncompressible(const OsPath& pathname) { const OsPath extension = pathname.Extension(); // file extensions that we don't want to compress static const wchar_t* incompressibleExtensions[] = { L".zip", L".rar", L".jpg", L".jpeg", L".png", L".ogg", L".mp3" }; for(size_t i = 0; i < ARRAY_SIZE(incompressibleExtensions); i++) { if(extension == incompressibleExtensions[i]) return true; } return false; } PFile m_file; off_t m_fileSize; Pool m_cdfhPool; size_t m_numEntries; bool m_noDeflate; }; PIArchiveWriter CreateArchiveWriter_Zip(const OsPath& archivePathname, bool noDeflate) { return PIArchiveWriter(new ArchiveWriter_Zip(archivePathname, noDeflate)); } Index: ps/trunk/source/lib/file/archive/archive.h =================================================================== --- ps/trunk/source/lib/file/archive/archive.h (revision 10453) +++ ps/trunk/source/lib/file/archive/archive.h (revision 10454) @@ -1,99 +1,109 @@ /* 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. */ /* * interface for reading from and creating archives. */ #ifndef INCLUDED_ARCHIVE #define INCLUDED_ARCHIVE #include "lib/file/file_system.h" // FileInfo #include "lib/file/common/file_loader.h" #include "lib/file/vfs/vfs_path.h" // rationale: this module doesn't build a directory tree of the entries // within an archive. that task is left to the VFS; here, we are only // concerned with enumerating all archive entries. namespace ERR { const Status ARCHIVE_UNKNOWN_FORMAT = -110400; const Status ARCHIVE_UNKNOWN_METHOD = -110401; } struct IArchiveFile : public IFileLoader { }; typedef shared_ptr PIArchiveFile; struct IArchiveReader { virtual ~IArchiveReader(); /** * called for each archive entry. * @param pathname full pathname of entry; only valid during the callback. **/ typedef void (*ArchiveEntryCallback)(const VfsPath& pathname, const FileInfo& fileInfo, PIArchiveFile archiveFile, uintptr_t cbData); virtual Status ReadEntries(ArchiveEntryCallback cb, uintptr_t cbData) = 0; }; typedef shared_ptr PIArchiveReader; // note: when creating an archive, any existing file with the given pathname // will be overwritten. // rationale: don't support partial adding, i.e. updating archive with // only one file. this would require overwriting parts of the Zip archive, // which is annoying and slow. also, archives are usually built in // seek-optimal order, which would break if we start inserting files. // while testing, loose files can be used, so there's no loss. struct IArchiveWriter { /** * write out the archive to disk; only hereafter is it valid. **/ virtual ~IArchiveWriter(); /** * add a file to the archive. * * rationale: passing in a filename instead of the compressed file * contents makes for better encapsulation because callers don't need * to know about the codec. one disadvantage is that loading the file * contents can no longer take advantage of the VFS cache nor previously * archived versions. however, the archive builder usually adds files * precisely because they aren't in archives, and the cache would * thrash anyway, so this is deemed acceptable. * * @param pathname the actual file to add * @param pathnameInArchive the name to store in the archive **/ virtual Status AddFile(const OsPath& pathname, const Path& pathameInArchive) = 0; + + /** + * add a file to the archive, when it is already in memory and not on disk. + * + * @param data the uncompressed file contents to add + * @param size the length of data + * @param mtime the last-modified-time to be stored in the archive + * @param pathnameInArchive the name to store in the archive + **/ + virtual Status AddMemory(const u8* data, size_t size, time_t mtime, const OsPath& pathnameInArchive) = 0; }; typedef shared_ptr PIArchiveWriter; #endif // #ifndef INCLUDED_ARCHIVE Index: ps/trunk/source/lib/file/io/write_buffer.cpp =================================================================== --- ps/trunk/source/lib/file/io/write_buffer.cpp (revision 10453) +++ ps/trunk/source/lib/file/io/write_buffer.cpp (revision 10454) @@ -1,134 +1,147 @@ /* 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 "precompiled.h" #include "lib/file/io/write_buffer.h" #include "lib/bits.h" // IsAligned #include "lib/sysdep/cpu.h" #include "lib/allocators/shared_ptr.h" #include "lib/file/io/io.h" static const size_t BLOCK_SIZE = 512*KiB; WriteBuffer::WriteBuffer() : m_capacity(pageSize), m_data((u8*)rtl_AllocateAligned(m_capacity, maxSectorSize), AlignedDeleter()), m_size(0) { } -void WriteBuffer::Append(const void* data, size_t size) +void WriteBuffer::EnsureSufficientCapacity(size_t size) { if(m_size + size > m_capacity) { m_capacity = round_up_to_pow2(m_size + size); shared_ptr newData; AllocateAligned(newData, m_capacity, maxSectorSize); memcpy(newData.get(), m_data.get(), m_size); m_data = newData; } +} + +void WriteBuffer::Append(const void* data, size_t size) +{ + EnsureSufficientCapacity(size); memcpy(m_data.get() + m_size, data, size); m_size += size; } +void WriteBuffer::Reserve(size_t size) +{ + EnsureSufficientCapacity(size); + memset(m_data.get() + m_size, 0, size); + m_size += size; +} + + void WriteBuffer::Overwrite(const void* data, size_t size, size_t offset) { ENSURE(offset+size < m_size); memcpy(m_data.get()+offset, data, size); } //----------------------------------------------------------------------------- // UnalignedWriter //----------------------------------------------------------------------------- UnalignedWriter::UnalignedWriter(const PFile& file, off_t ofs) : m_file(file), m_alignedBuf((u8*)rtl_AllocateAligned(BLOCK_SIZE, maxSectorSize), AlignedDeleter()) { m_alignedOfs = round_down(ofs, (off_t)BLOCK_SIZE); const size_t misalignment = (size_t)(ofs - m_alignedOfs); if(misalignment) { io::Operation op(*m_file.get(), m_alignedBuf.get(), BLOCK_SIZE, m_alignedOfs); THROW_STATUS_IF_ERR(io::Run(op)); } m_bytesUsed = misalignment; } UnalignedWriter::~UnalignedWriter() { Flush(); } Status UnalignedWriter::Append(const u8* data, size_t size) const { while(size != 0) { // optimization: write directly from the input buffer, if possible const size_t alignedSize = (size / BLOCK_SIZE) * BLOCK_SIZE; if(m_bytesUsed == 0 && IsAligned(data, maxSectorSize) && alignedSize != 0) { io::Operation op(*m_file.get(), (void*)data, alignedSize, m_alignedOfs); RETURN_STATUS_IF_ERR(io::Run(op)); m_alignedOfs += (off_t)alignedSize; data += alignedSize; size -= alignedSize; } const size_t chunkSize = std::min(size, BLOCK_SIZE-m_bytesUsed); memcpy(m_alignedBuf.get()+m_bytesUsed, data, chunkSize); m_bytesUsed += chunkSize; data += chunkSize; size -= chunkSize; if(m_bytesUsed == BLOCK_SIZE) RETURN_STATUS_IF_ERR(WriteBlock()); } return INFO::OK; } void UnalignedWriter::Flush() const { if(m_bytesUsed) { memset(m_alignedBuf.get()+m_bytesUsed, 0, BLOCK_SIZE-m_bytesUsed); (void)WriteBlock(); } } Status UnalignedWriter::WriteBlock() const { io::Operation op(*m_file.get(), m_alignedBuf.get(), BLOCK_SIZE, m_alignedOfs); RETURN_STATUS_IF_ERR(io::Run(op)); m_alignedOfs += BLOCK_SIZE; m_bytesUsed = 0; return INFO::OK; } Index: ps/trunk/source/lib/file/io/write_buffer.h =================================================================== --- ps/trunk/source/lib/file/io/write_buffer.h (revision 10453) +++ ps/trunk/source/lib/file/io/write_buffer.h (revision 10454) @@ -1,83 +1,86 @@ /* 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. */ #ifndef INCLUDED_WRITE_BUFFER #define INCLUDED_WRITE_BUFFER #include "lib/file/file.h" class WriteBuffer { public: WriteBuffer(); void Append(const void* data, size_t size); + void Reserve(size_t size); void Overwrite(const void* data, size_t size, size_t offset); shared_ptr Data() const { return m_data; } size_t Size() const { return m_size; } private: + void EnsureSufficientCapacity(size_t size); + size_t m_capacity; // must come first (init order) shared_ptr m_data; size_t m_size; }; class UnalignedWriter { NONCOPYABLE(UnalignedWriter); public: UnalignedWriter(const PFile& file, off_t ofs); ~UnalignedWriter(); /** * add data to the align buffer, writing it out to disk if full. **/ Status Append(const u8* data, size_t size) const; /** * zero-initialize any remaining space in the align buffer and write * it to the file. this is called by the destructor. **/ void Flush() const; private: Status WriteBlock() const; PFile m_file; shared_ptr m_alignedBuf; mutable off_t m_alignedOfs; mutable size_t m_bytesUsed; }; typedef shared_ptr PUnalignedWriter; #endif // #ifndef INCLUDED_WRITE_BUFFER Index: ps/trunk/source/lib/file/vfs/vfs.cpp =================================================================== --- ps/trunk/source/lib/file/vfs/vfs.cpp (revision 10453) +++ ps/trunk/source/lib/file/vfs/vfs.cpp (revision 10454) @@ -1,271 +1,280 @@ /* 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 "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(CreateTrace(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)); 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 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 Invalidate(const VfsPath& pathname) { ScopedLock s; m_fileCache.Remove(pathname); VfsDirectory* directory; RETURN_STATUS_IF_ERR(vfs_Lookup(pathname, &m_rootDirectory, directory, 0)); const OsPath name = pathname.Filename(); directory->Invalidate(name); 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 10453) +++ ps/trunk/source/lib/file/vfs/vfs.h (revision 10454) @@ -1,198 +1,205 @@ /* 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. */ /* * 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 }; // (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; /** * 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; /** * indicate that a file has changed; remove its data from the cache and * arrange for its directory to be updated. **/ virtual Status Invalidate(const VfsPath& pathname) = 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/binaries/data/mods/public/gui/savedgames/load.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/savedgames/load.js (nonexistent) +++ ps/trunk/binaries/data/mods/public/gui/savedgames/load.js (revision 10454) @@ -0,0 +1,47 @@ +function sortDecreasingDate(a, b) +{ + return b.metadata.time - a.metadata.time; +} + +function twoDigits(n) +{ + return n < 10 ? "0" + n : n; +} + +function generateLabel(metadata) +{ + var t = new Date(metadata.time*1000); + // TODO: timezones + var date = t.getUTCFullYear()+"-"+twoDigits(1+t.getUTCMonth())+"-"+twoDigits(t.getUTCDate()); + var time = twoDigits(t.getUTCHours())+":"+twoDigits(t.getUTCMinutes())+":"+twoDigits(t.getUTCSeconds()); + return "["+date+" "+time+"] "+metadata.initAttributes.map; +} + +function init() +{ + var savedGames = Engine.GetSavedGames(); + + savedGames.sort(sortDecreasingDate); + + var gameListIDs = [ game.id for each (game in savedGames) ]; + var gameListLabels = [ generateLabel(game.metadata) for each (game in savedGames) ]; + + var gameSelection = getGUIObjectByName("gameSelection"); + gameSelection.list = gameListLabels; + gameSelection.list_data = gameListIDs; + gameSelection.selected = 0; +} + +function loadGame() +{ + var gameSelection = getGUIObjectByName("gameSelection"); + var gameID = gameSelection.list_data[gameSelection.selected]; + + var metadata = Engine.StartSavedGame(gameID); + + Engine.SwitchGuiPage("page_loading.xml", { + "attribs": metadata.initAttributes, + "isNetworked" : false, + "playerAssignments": metadata.gui.playerAssignments + }); +} \ No newline at end of file Property changes on: ps/trunk/binaries/data/mods/public/gui/savedgames/load.js ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/binaries/data/mods/public/gui/savedgames/load.xml =================================================================== --- ps/trunk/binaries/data/mods/public/gui/savedgames/load.xml (nonexistent) +++ ps/trunk/binaries/data/mods/public/gui/savedgames/load.xml (revision 10454) @@ -0,0 +1,34 @@ + + + + +