Index: ps/trunk/binaries/system/readme.txt =================================================================== --- ps/trunk/binaries/system/readme.txt (revision 25106) +++ ps/trunk/binaries/system/readme.txt (revision 25107) @@ -1,92 +1,91 @@ COMMAND LINE OPTIONS Basic gameplay: -autostart=... load a map instead of showing main menu (see below) -editor launch the Atlas scenario editor -mod=NAME start the game using NAME mod -quickstart load faster (disables audio and some system info logging) Autostart: -autostart="TYPEDIR/MAPNAME" enables autostart and sets MAPNAME; TYPEDIR is skirmishes, scenarios, or random -autostart-seed=SEED sets randomization seed value (default 0, use -1 for random) -autostart-ai=PLAYER:AI sets the AI for PLAYER (e.g. 2:petra) -autostart-aidiff=PLAYER:DIFF sets the DIFFiculty of PLAYER's AI (0: sandbox, 5: very hard) -autostart-aiseed=AISEED sets the seed used for the AI random generator (default 0, use -1 for random) -autostart-player=NUMBER sets the playerID in non-networked games (default 1, use -1 for observer) -autostart-civ=PLAYER:CIV sets PLAYER's civilisation to CIV (skirmish and random maps only) -autostart-team=PLAYER:TEAM sets the team for PLAYER (e.g. 2:2). -autostart-ceasefire=NUM sets a ceasefire duration NUM (default 0 minutes) -autostart-nonvisual disable any graphics and sounds -autostart-victory=SCRIPTNAME sets the victory conditions with SCRIPTNAME located in simulation/data/settings/victory_conditions/ (default conquest). When the first given SCRIPTNAME is "endless", no victory conditions will apply. -autostart-wonderduration=NUM sets the victory duration NUM for wonder victory condition (default 10 minutes) -autostart-relicduration=NUM sets the victory duration NUM for relic victory condition (default 10 minutes) -autostart-reliccount=NUM sets the number of relics for relic victory condition (default 2 relics) -autostart-disable-replay disable saving of replays Multiplayer: -autostart-playername=NAME sets local player NAME (default 'anonymous') -autostart-host sets multiplayer host mode -autostart-host-players=NUMBER sets NUMBER of human players for multiplayer game (default 2) -autostart-client=IP sets multiplayer client to join host at given IP address Random maps only: -autostart-size=TILES sets random map size in TILES (default 192) -autostart-players=NUMBER sets NUMBER of players on random map (default 2) Examples: 1) "Bob" will host a 2 player game on the Arcadia map: -autostart="scenarios/Arcadia" -autostart-host -autostart-host-players=2 -autostart-playername="Bob" "Alice" joins the match as player 2: -autostart="scenarios/Arcadia" -autostart-client=127.0.0.1 -autostart-playername="Alice" The players use the developer overlay to control players. 2) Load Alpine Lakes random map with random seed, 2 players (Athens and Britons), and player 2 is PetraBot: -autostart="random/alpine_lakes" -autostart-seed=-1 -autostart-players=2 -autostart-civ=1:athen -autostart-civ=2:brit -autostart-ai=2:petra 3) Observe the PetraBot on a triggerscript map: -autostart="random/jebel_barkal" -autostart-seed=-1 -autostart-players=2 -autostart-civ=1:athen -autostart-civ=2:brit -autostart-ai=1:petra -autostart-ai=2:petra -autostart-player=-1 RL client: -rl-interface Run the RL interface (see source/tools/rlclient) Configuration: -conf=KEY:VALUE set a config value -nosound disable audio --noUserMod disable loading of the user mod -shadows enable shadows -vsync enable VSync, i.e. lock FPS to monitor refresh rate -xres=N set screen X resolution to 'N' -yres=N set screen Y resolution to 'N' Installing mods: PATHS install mods located at PATHS. For instance: "./pyrogenesis mod1.pyromod mod2.zip" Advanced / diagnostic: -version print the version of the engine and exit -dumpSchema creates a file entity.rng in the working directory, containing complete entity XML schema, used by various analysis tools -entgraph (disabled) -listfiles (disabled) -profile=NAME (disabled) -replay=PATH non-visual replay of a previous game, used for analysis purposes PATH is system path to commands.txt containing simulation log -replay-visual=PATH visual replay of a previous game, used for analysis purposes PATH is system path to commands.txt containing simulation log -writableRoot store runtime game data in root data directory (only use if you have write permissions on that directory) -ooslog dumps simulation state in binary and ASCII representations each turn, files created in sim_log within the game's log folder. NOTE: game will run much slower with this option! -serializationtest checks simulation state each turn for serialization errors; on test failure, error is displayed and logs created in oos_log within the game's log folder. NOTE: game will run much slower with this option! -rejointest=N simulates a rejoin and checks simulation state each turn for serialization errors; this is similar to a serialization test but much faster and less complete. It should be enough for debugging most rejoin OOSes. -unique-logs adds unix timestamp and process id to the filename of mainlog.html, interestinglog.html and oos_dump.txt to prevent these files from becoming overwritten by another pyrogenesis process. -hashtest-full=X whether to enable computation of full hashes in replaymode (default true). Can be disabled to improve performance. -hashtest-quick=X whether to enable computation of quick hashes in replaymode (default false). Can be enabled for debugging purposes. Archive builder: -archivebuild=PATH system PATH of the base directory containing mod data to be archived/precached specify all mods it depends on with -mod=NAME -archivebuild-output=PATH system PATH to output of the resulting .zip archive (use with archivebuild) -archivebuild-compress enable deflate compression in the .zip (no zip compression by default since it hurts compression of release packages) Index: ps/trunk/source/simulation2/components/tests/test_scripts.h =================================================================== --- ps/trunk/source/simulation2/components/tests/test_scripts.h (revision 25106) +++ ps/trunk/source/simulation2/components/tests/test_scripts.h (revision 25107) @@ -1,134 +1,134 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "simulation2/system/ComponentTest.h" #include "simulation2/serialization/StdDeserializer.h" #include "simulation2/serialization/StdSerializer.h" #include "ps/Filesystem.h" #include "scriptinterface/FunctionWrapper.h" #include "scriptinterface/ScriptContext.h" class TestComponentScripts : public CxxTest::TestSuite { public: void setUp() { g_VFS = CreateVfs(); - g_VFS->Mount(L"", DataDir()/"mods"/"mod", VFS_MOUNT_MUST_EXIST); - g_VFS->Mount(L"", DataDir()/"mods"/"public", VFS_MOUNT_MUST_EXIST, 1); // ignore directory-not-found errors + g_VFS->Mount(L"", DataDir() / "mods" / "mod" / "", VFS_MOUNT_MUST_EXIST); + g_VFS->Mount(L"", DataDir() / "mods" / "public" / "", VFS_MOUNT_MUST_EXIST, 1); // ignore directory-not-found errors CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); g_VFS.reset(); } static void load_script(const ScriptInterface& scriptInterface, const VfsPath& pathname) { CVFSFile file; TS_ASSERT_EQUALS(file.Load(g_VFS, pathname), PSRETURN_OK); CStr content = file.DecodeUTF8(); // assume it's UTF-8 TSM_ASSERT(L"Running script "+pathname.string(), scriptInterface.LoadScript(pathname, content)); } static void Script_LoadComponentScript(ScriptInterface::CmptPrivate* pCmptPrivate, const VfsPath& pathname) { CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); TS_ASSERT(componentManager->LoadScript(VfsPath(L"simulation/components") / pathname)); } static void Script_LoadHelperScript(ScriptInterface::CmptPrivate* pCmptPrivate, const VfsPath& pathname) { CComponentManager* componentManager = static_cast (pCmptPrivate->pCBData); TS_ASSERT(componentManager->LoadScript(VfsPath(L"simulation/helpers") / pathname)); } static JS::Value Script_SerializationRoundTrip(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue value) { ScriptInterface& scriptInterface = *(pCmptPrivate->pScriptInterface); ScriptRequest rq(scriptInterface); JS::RootedValue val(rq.cx); val = value; std::stringstream stream; CStdSerializer serializer(scriptInterface, stream); serializer.ScriptVal("", &val); CStdDeserializer deserializer(scriptInterface, stream); deserializer.ScriptVal("", &val); return val; } void test_global_scripts() { if (!VfsDirectoryExists(L"globalscripts/tests/")) { debug_printf("Skipping globalscripts tests (can't find binaries/data/mods/public/globalscripts/tests/)\n"); return; } VfsPaths paths; TS_ASSERT_OK(vfs::GetPathnames(g_VFS, L"globalscripts/tests/", L"test_*.js", paths)); for (const VfsPath& path : paths) { CSimContext context; CComponentManager componentManager(context, g_ScriptContext, true); ScriptTestSetup(componentManager.GetScriptInterface()); ScriptRequest rq(componentManager.GetScriptInterface()); ScriptFunction::Register(rq, "SerializationRoundTrip"); load_script(componentManager.GetScriptInterface(), path); } } void test_scripts() { if (!VfsFileExists(L"simulation/components/tests/setup.js")) { debug_printf("Skipping component scripts tests (can't find binaries/data/mods/public/simulation/components/tests/setup.js)\n"); return; } VfsPaths paths; TS_ASSERT_OK(vfs::GetPathnames(g_VFS, L"simulation/components/tests/", L"test_*.js", paths)); TS_ASSERT_OK(vfs::GetPathnames(g_VFS, L"simulation/helpers/tests/", L"test_*.js", paths)); paths.push_back(VfsPath(L"simulation/components/tests/setup_test.js")); for (const VfsPath& path : paths) { // Clean up previous scripts. g_ScriptContext->ShrinkingGC(); CSimContext context; CComponentManager componentManager(context, g_ScriptContext, true); ScriptTestSetup(componentManager.GetScriptInterface()); ScriptRequest rq(componentManager.GetScriptInterface()); ScriptFunction::Register(rq, "LoadComponentScript"); ScriptFunction::Register(rq, "LoadHelperScript"); ScriptFunction::Register(rq, "SerializationRoundTrip"); componentManager.LoadComponentTypes(); load_script(componentManager.GetScriptInterface(), L"simulation/components/tests/setup.js"); load_script(componentManager.GetScriptInterface(), path); } } }; Index: ps/trunk/source/simulation2/tests/test_ComponentManager.h =================================================================== --- ps/trunk/source/simulation2/tests/test_ComponentManager.h (revision 25106) +++ ps/trunk/source/simulation2/tests/test_ComponentManager.h (revision 25107) @@ -1,902 +1,902 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "lib/self_test.h" #include "simulation2/system/ComponentManager.h" #include "simulation2/MessageTypes.h" #include "simulation2/system/ParamNode.h" #include "simulation2/system/SimContext.h" #include "simulation2/serialization/ISerializer.h" #include "simulation2/components/ICmpTest.h" #include "simulation2/components/ICmpTemplateManager.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/XML/Xeromyces.h" #define TS_ASSERT_STREAM(stream, len, buffer) \ TS_ASSERT_EQUALS(stream.str().length(), (size_t)len); \ TS_ASSERT_SAME_DATA(stream.str().data(), buffer, len) #define TS_ASSERT_THROWS_PSERROR(e, t, s) \ TS_ASSERT_THROWS_EQUALS(e, const t& ex, std::string(ex.what()), s) class TestComponentManager : public CxxTest::TestSuite { public: void setUp() { g_VFS = CreateVfs(); - TS_ASSERT_OK(g_VFS->Mount(L"", DataDir()/"mods"/"_test.sim", VFS_MOUNT_MUST_EXIST)); - TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir()/"_testcache")); + TS_ASSERT_OK(g_VFS->Mount(L"", DataDir() / "mods" / "_test.sim" / "", VFS_MOUNT_MUST_EXIST)); + TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir() / "_testcache" / "", 0, VFS_MAX_PRIORITY)); CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); g_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); } void test_Load() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); } void test_LookupCID() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT_EQUALS(man.LookupCID("Test1A"), (int)CID_Test1A); TS_ASSERT_EQUALS(man.LookupCID("Test1B"), (int)CID_Test1B); } void test_AllocateNewEntity() { CSimContext context; CComponentManager man(context, g_ScriptContext); TS_ASSERT_EQUALS(man.AllocateNewEntity(), (u32)2); TS_ASSERT_EQUALS(man.AllocateNewEntity(), (u32)3); TS_ASSERT_EQUALS(man.AllocateNewEntity(), (u32)4); TS_ASSERT_EQUALS(man.AllocateNewEntity(100), (u32)100); TS_ASSERT_EQUALS(man.AllocateNewEntity(), (u32)101); // TODO: // TS_ASSERT_EQUALS(man.AllocateNewEntity(3), (u32)102); TS_ASSERT_EQUALS(man.AllocateNewLocalEntity(), (u32)FIRST_LOCAL_ENTITY); TS_ASSERT_EQUALS(man.AllocateNewLocalEntity(), (u32)FIRST_LOCAL_ENTITY+1); man.ResetState(); TS_ASSERT_EQUALS(man.AllocateNewEntity(), (u32)2); TS_ASSERT_EQUALS(man.AllocateNewEntity(3), (u32)3); TS_ASSERT_EQUALS(man.AllocateNewLocalEntity(), (u32)FIRST_LOCAL_ENTITY); } void test_rng() { // Ensure we get the same random number with the same seed double first; { CSimContext context; CComponentManager man(context, g_ScriptContext); man.SetRNGSeed(123); if (!man.m_ScriptInterface.MathRandom(first)) TS_FAIL("Couldn't get random number!"); } double second; { CSimContext context; CComponentManager man(context, g_ScriptContext); man.SetRNGSeed(123); if (!man.m_ScriptInterface.MathRandom(second)) TS_FAIL("Couldn't get random number!"); } TS_ASSERT_EQUALS(first, second); } void test_AddComponent_errors() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); CEntityHandle hnd1 = man.AllocateEntityHandle(1); CParamNode noParam; TS_ASSERT(man.AddComponent(hnd1, CID_Test1A, noParam)); { TestLogger log; TS_ASSERT(! man.AddComponent(hnd1, 12345, noParam)); TS_ASSERT_STR_CONTAINS(log.GetOutput(), "ERROR: Invalid component id 12345"); } { TestLogger log; TS_ASSERT(! man.AddComponent(hnd1, CID_Test1B, noParam)); TS_ASSERT_STR_CONTAINS(log.GetOutput(), "ERROR: Multiple components for interface "); } } void test_QueryInterface() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CParamNode noParam; man.AddComponent(hnd1, CID_Test1A, noParam); TS_ASSERT(man.QueryInterface(ent1, IID_Test1) != NULL); TS_ASSERT(man.QueryInterface(ent1, IID_Test2) == NULL); TS_ASSERT(man.QueryInterface(ent2, IID_Test1) == NULL); TS_ASSERT(man.QueryInterface(ent2, IID_Test2) == NULL); man.AddComponent(hnd2, CID_Test1B, noParam); TS_ASSERT(man.QueryInterface(ent2, IID_Test1) != NULL); TS_ASSERT(man.QueryInterface(ent2, IID_Test2) == NULL); man.AddComponent(hnd2, CID_Test2A, noParam); TS_ASSERT(man.QueryInterface(ent2, IID_Test1) != NULL); TS_ASSERT(man.QueryInterface(ent2, IID_Test2) != NULL); } void test_SendMessage() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); entity_id_t ent1 = 1, ent2 = 2, ent3 = 3, ent4 = 4; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CEntityHandle hnd3 = man.AllocateEntityHandle(ent3); CEntityHandle hnd4 = man.AllocateEntityHandle(ent4); CParamNode noParam; man.AddComponent(hnd1, CID_Test1A, noParam); man.AddComponent(hnd2, CID_Test1B, noParam); man.AddComponent(hnd3, CID_Test2A, noParam); man.AddComponent(hnd4, CID_Test1A, noParam); man.AddComponent(hnd4, CID_Test2A, noParam); CMessageTurnStart msg1; CMessageUpdate msg2(fixed::FromInt(100)); CMessageInterpolate msg3(0, 0, 0); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 12000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 21000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test1))->GetX(), 11000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test2))->GetX(), 21000); // Test_1A subscribed locally to msg1, nothing subscribed globally man.PostMessage(ent1, msg1); man.PostMessage(ent1, msg2); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 12000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 21000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test1))->GetX(), 11000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test2))->GetX(), 21000); man.BroadcastMessage(msg1); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11002); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 12000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 21050); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test1))->GetX(), 11001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test2))->GetX(), 21050); // Test_1B, Test_2A subscribed locally to msg2, nothing subscribed globally man.BroadcastMessage(msg2); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11002); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 12010); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 21150); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test1))->GetX(), 11001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test2))->GetX(), 21150); // Test_1A subscribed locally to msg3, Test_1B subscribed globally man.BroadcastMessage(msg3); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11004); // local TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 12030); // global TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 21150); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test1))->GetX(), 11003); // local TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test2))->GetX(), 21150); man.PostMessage(ent1, msg3); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11006); // local TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 12050); // global TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 21150); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test1))->GetX(), 11003); // local - skipped TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test2))->GetX(), 21150); } void test_ParamNode() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CParamNode noParam; CParamNode testParam; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(testParam, "1234"), PSRETURN_OK); man.AddComponent(hnd1, CID_Test1A, noParam); man.AddComponent(hnd2, CID_Test1A, testParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 1234); } void test_script_basic() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test.js")); TS_ASSERT_EQUALS(man.LookupCID("TestScript1A"), (int)CID__LastNative); TS_ASSERT_EQUALS(man.LookupCID("TestScript1B"), (int)CID__LastNative+1); entity_id_t ent1 = 1, ent2 = 2, ent3 = 3; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CEntityHandle hnd3 = man.AllocateEntityHandle(ent3); CParamNode noParam; man.AddComponent(hnd1, CID_Test1A, noParam); man.AddComponent(hnd2, man.LookupCID("TestScript1A"), noParam); man.AddComponent(hnd3, man.LookupCID("TestScript1B"), noParam); man.AddComponent(hnd3, man.LookupCID("TestScript2A"), noParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 101000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test1))->GetX(), 102000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 201000); CMessageTurnStart msg1; CMessageUpdate msg2(fixed::FromInt(25)); man.BroadcastMessage(msg1); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 101001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test1))->GetX(), 102001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 201000); man.BroadcastMessage(msg2); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 101001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test1))->GetX(), 102001); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 201025); } void test_script_helper_basic() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-helper.js")); TS_ASSERT(man.LoadScript(L"simulation/helpers/test-helper.js")); entity_id_t ent1 = 1; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CParamNode noParam; man.AddComponent(hnd1, man.LookupCID("TestScript1_Helper"), noParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 3); } void test_script_global_helper() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-global-helper.js")); entity_id_t ent1 = 1; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CParamNode noParam; man.AddComponent(hnd1, man.LookupCID("TestScript1_GlobalHelper"), noParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 2); } void test_script_interface() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/interfaces/test-interface.js")); TS_ASSERT(man.LoadScript(L"simulation/components/test-interface.js")); entity_id_t ent1 = 1; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CParamNode noParam; man.AddComponent(hnd1, man.LookupCID("TestScript1_Interface"), noParam); man.AddComponent(hnd1, man.LookupCID("TestScript2_Interface"), noParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 1000 + IID__LastNative); } void test_script_errors() { CSimContext context; CComponentManager man(context, g_ScriptContext); ScriptTestSetup(man.m_ScriptInterface); man.LoadComponentTypes(); TestLogger log; TS_ASSERT(man.LoadScript(L"simulation/components/error.js")); // The following exception is caught and dropped by the JS script, and should not appear in the logs. TS_ASSERT_STR_NOT_CONTAINS(log.GetOutput(), "ERROR: JavaScript error: simulation/components/error.js line 4\nInvalid interface id"); // The following exception is not caught by the JS script. TS_ASSERT_STR_CONTAINS(log.GetOutput(), "ERROR: No script wrapper found for interface id 12345"); } void test_script_entityID() { CSimContext context; CComponentManager man(context, g_ScriptContext); ScriptTestSetup(man.m_ScriptInterface); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-entityid.js")); entity_id_t ent1 = 1, ent2 = 234; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CParamNode noParam; man.AddComponent(hnd1, man.LookupCID("TestScript1A"), noParam); man.AddComponent(hnd2, man.LookupCID("TestScript1A"), noParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), (int)ent1); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), (int)ent2); } void test_script_QueryInterface() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-query.js")); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CParamNode noParam; man.AddComponent(hnd1, man.LookupCID("TestScript1A"), noParam); man.AddComponent(hnd1, man.LookupCID("TestScript2A"), noParam); man.AddComponent(hnd2, man.LookupCID("TestScript1A"), noParam); man.AddComponent(hnd2, CID_Test2A, noParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 400); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 21000); } void test_script_AddEntity() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-addentity.js")); TS_ASSERT(man.LoadScript(L"simulation/components/addentity/test-addentity.js")); man.InitSystemEntity(); entity_id_t ent1 = man.AllocateNewEntity(); entity_id_t ent2 = ent1 + 2; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CParamNode noParam; TS_ASSERT(man.AddComponent(man.GetSystemEntity(), CID_TemplateManager, noParam)); TS_ASSERT(man.AddComponent(hnd1, man.LookupCID("TestScript1_AddEntity"), noParam)); TS_ASSERT(man.QueryInterface(ent2, IID_Test1) == NULL); TS_ASSERT(man.QueryInterface(ent2, IID_Test2) == NULL); { TestLogger logger; // ignore bogus-template warnings TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), (int)ent2); } TS_ASSERT(man.QueryInterface(ent2, IID_Test1) != NULL); TS_ASSERT(man.QueryInterface(ent2, IID_Test2) != NULL); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 999); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test2))->GetX(), 12345); } void test_script_AddLocalEntity() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-addentity.js")); TS_ASSERT(man.LoadScript(L"simulation/components/addentity/test-addentity.js")); man.InitSystemEntity(); entity_id_t ent1 = man.AllocateNewEntity(); entity_id_t ent2 = man.AllocateNewLocalEntity() + 2; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CParamNode noParam; TS_ASSERT(man.AddComponent(man.GetSystemEntity(), CID_TemplateManager, noParam)); TS_ASSERT(man.AddComponent(hnd1, man.LookupCID("TestScript1_AddLocalEntity"), noParam)); TS_ASSERT(man.QueryInterface(ent2, IID_Test1) == NULL); TS_ASSERT(man.QueryInterface(ent2, IID_Test2) == NULL); { TestLogger logger; // ignore bogus-template warnings TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), (int)ent2); } TS_ASSERT(man.QueryInterface(ent2, IID_Test1) != NULL); TS_ASSERT(man.QueryInterface(ent2, IID_Test2) != NULL); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 999); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test2))->GetX(), 12345); } void test_script_DestroyEntity() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-destroyentity.js")); entity_id_t ent1 = 10; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CParamNode noParam; TS_ASSERT(man.AddComponent(hnd1, man.LookupCID("TestScript1_DestroyEntity"), noParam)); TS_ASSERT(man.QueryInterface(ent1, IID_Test1) != NULL); static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(); TS_ASSERT(man.QueryInterface(ent1, IID_Test1) != NULL); man.FlushDestroyedComponents(); TS_ASSERT(man.QueryInterface(ent1, IID_Test1) == NULL); } void test_script_messages() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-msg.js")); entity_id_t ent1 = 1, ent2 = 2, ent3 = 3; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CEntityHandle hnd3 = man.AllocateEntityHandle(ent3); CParamNode noParam; man.AddComponent(hnd1, man.LookupCID("TestScript1A"), noParam); man.AddComponent(hnd1, man.LookupCID("TestScript2A"), noParam); man.AddComponent(hnd2, man.LookupCID("TestScript1A"), noParam); man.AddComponent(hnd2, CID_Test2A, noParam); man.AddComponent(hnd3, man.LookupCID("TestScript1B"), noParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 100); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 100); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test2))->GetX(), 21000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test1))->GetX(), 100); // This GetX broadcasts messages TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test2))->GetX(), 200); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 650); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 5150); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test2))->GetX(), 26050); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test1))->GetX(), 5650); } void test_script_template() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-param.js")); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CParamNode noParam; CParamNode testParam; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(testParam, "1110000"), PSRETURN_OK); man.AddComponent(hnd1, man.LookupCID("TestScript1_Init"), noParam); man.AddComponent(hnd2, man.LookupCID("TestScript1_Init"), testParam.GetChild("node")); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 100); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 1+10+100+1000); } void test_script_template_readonly() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-param.js")); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CParamNode noParam; CParamNode testParam; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(testParam, "100"), PSRETURN_OK); man.AddComponent(hnd1, man.LookupCID("TestScript1_readonly"), testParam); man.AddComponent(hnd2, man.LookupCID("TestScript1_readonly"), testParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 102); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 102); } void test_script_hotload() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-hotload1.js")); entity_id_t ent1 = 1, ent2 = 2, ent3 = 3, ent4 = 4; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CEntityHandle hnd3 = man.AllocateEntityHandle(ent3); CEntityHandle hnd4 = man.AllocateEntityHandle(ent4); CParamNode testParam; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(testParam, "100"), PSRETURN_OK); man.AddComponent(hnd1, man.LookupCID("HotloadA"), testParam); man.AddComponent(hnd2, man.LookupCID("HotloadB"), testParam); man.AddComponent(hnd2, man.LookupCID("HotloadC"), testParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 100); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 200); TS_ASSERT(man.LoadScript(L"simulation/components/test-hotload2.js", true)); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 1000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 200); man.AddComponent(hnd3, man.LookupCID("HotloadA"), testParam); man.AddComponent(hnd4, man.LookupCID("HotloadB"), testParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test1))->GetX(), 1000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent4, IID_Test1))->GetX(), 200); } void test_script_modding() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); CParamNode testParam; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(testParam, "100"), PSRETURN_OK); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); TS_ASSERT(man.LoadScript(L"simulation/components/test-modding1.js")); man.AddComponent(hnd1, man.LookupCID("Modding"), testParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 100); TS_ASSERT(man.LoadScript(L"simulation/components/test-modding2.js")); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 1000); man.AddComponent(hnd2, man.LookupCID("Modding"), testParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 1000); } void test_serialization() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); entity_id_t ent1 = 10, ent2 = 20, ent3 = FIRST_LOCAL_ENTITY; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CEntityHandle hnd3 = man.AllocateEntityHandle(ent3); CParamNode noParam; CParamNode testParam; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(testParam, "1234"), PSRETURN_OK); man.AddComponent(hnd1, CID_Test1A, noParam); man.AddComponent(hnd1, CID_Test2A, noParam); man.AddComponent(hnd2, CID_Test1A, testParam); man.AddComponent(hnd3, CID_Test2A, noParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 11000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test2))->GetX(), 21000); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 1234); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test2))->GetX(), 21000); std::stringstream debugStream; TS_ASSERT(man.DumpDebugState(debugStream, true)); TS_ASSERT_STR_EQUALS(debugStream.str(), "rng: \"78606\"\n" "entities:\n" "- id: 10\n" " Test1A:\n" " x: 11000\n" " Test2A:\n" " x: 21000\n" "\n" "- id: 20\n" " Test1A:\n" " x: 1234\n" "\n" "- id: 536870912\n" " type: local\n" " Test2A:\n" " x: 21000\n" "\n" ); std::string hash; TS_ASSERT(man.ComputeStateHash(hash, false)); TS_ASSERT_EQUALS(hash.length(), (size_t)16); TS_ASSERT_SAME_DATA(hash.data(), "\x3c\x25\x6e\x22\x58\x23\x09\x58\x38\xca\xb2\x1e\x0b\x8c\xac\xcf", 16); // echo -en "\x05\x00\x00\x0078606\x02\0\0\0\x01\0\0\0\x0a\0\0\0\xf8\x2a\0\0\x14\0\0\0\xd2\x04\0\0\x04\0\0\0\x0a\0\0\0\x08\x52\0\0" | md5sum | perl -pe 's/([0-9a-f]{2})/\\x$1/g' // ^^^^^^^^ rng ^^^^^^^^ ^^next^^ ^^Test1A^^ ^^^ent1^^ ^^^11000^^^ ^^^ent2^^ ^^^1234^^^ ^^Test2A^^ ^^ent1^^ ^^^21000^^^ std::stringstream stateStream; TS_ASSERT(man.SerializeState(stateStream)); TS_ASSERT_STREAM(stateStream, 73, "\x05\x00\x00\x00\x37\x38\x36\x30\x36" // RNG "\x02\x00\x00\x00" // next entity ID "\x00\x00\x00\x00" // num system component types "\x02\x00\x00\x00" // num component types "\x06\x00\x00\x00Test1A" "\x02\x00\x00\x00" // num ents "\x0a\x00\x00\x00" // ent1 "\xf8\x2a\x00\x00" // 11000 "\x14\x00\x00\x00" // ent2 "\xd2\x04\x00\x00" // 1234 "\x06\x00\x00\x00Test2A" "\x01\x00\x00\x00" // num ents "\x0a\x00\x00\x00" // ent1 "\x08\x52\x00\x00" // 21000 ); CSimContext context2; CComponentManager man2(context2, g_ScriptContext); man2.LoadComponentTypes(); TS_ASSERT(man2.QueryInterface(ent1, IID_Test1) == NULL); TS_ASSERT(man2.QueryInterface(ent1, IID_Test2) == NULL); TS_ASSERT(man2.QueryInterface(ent2, IID_Test1) == NULL); TS_ASSERT(man2.QueryInterface(ent3, IID_Test2) == NULL); TS_ASSERT(man2.DeserializeState(stateStream)); TS_ASSERT_EQUALS(static_cast (man2.QueryInterface(ent1, IID_Test1))->GetX(), 11000); TS_ASSERT_EQUALS(static_cast (man2.QueryInterface(ent1, IID_Test2))->GetX(), 21000); TS_ASSERT_EQUALS(static_cast (man2.QueryInterface(ent2, IID_Test1))->GetX(), 1234); TS_ASSERT(man2.QueryInterface(ent3, IID_Test2) == NULL); } void test_script_serialization() { CSimContext context; CComponentManager man(context, g_ScriptContext); ScriptTestSetup(man.m_ScriptInterface); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-serialize.js")); entity_id_t ent1 = 1, ent2 = 2, ent3 = 3, ent4 = 4; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CEntityHandle hnd3 = man.AllocateEntityHandle(ent3); CEntityHandle hnd4 = man.AllocateEntityHandle(ent4); CParamNode noParam; CParamNode testParam; TS_ASSERT_EQUALS(CParamNode::LoadXMLString(testParam, "1234"), PSRETURN_OK); man.AddComponent(hnd1, man.LookupCID("TestScript1_values"), testParam); man.AddComponent(hnd2, man.LookupCID("TestScript1_entity"), testParam); // TODO: Since the upgrade to SpiderMonkey v24 this test won't be able to correctly represent // non-tree structures because sharp variables were removed (bug 566700). // This also affects the debug serializer and it could make sense to implement correct serialization again. man.AddComponent(hnd3, man.LookupCID("TestScript1_nontree"), testParam); man.AddComponent(hnd4, man.LookupCID("TestScript1_custom"), testParam); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent1, IID_Test1))->GetX(), 1234); { TestLogger log; // swallow warnings about this.entity being read-only TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), (int)ent2); } TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent3, IID_Test1))->GetX(), 8); std::stringstream debugStream; TS_ASSERT(man.DumpDebugState(debugStream, true)); TS_ASSERT_STR_EQUALS(debugStream.str(), "rng: \"78606\"\n\ entities:\n\ - id: 1\n\ TestScript1_values:\n\ comp: {\n\ \"x\": 1234,\n\ \"str\": \"this is a string\",\n\ \"things\": {\n\ \"a\": 1,\n\ \"b\": \"2\",\n\ \"c\": [\n\ 3,\n\ \"4\",\n\ [\n\ 5,\n\ []\n\ ]\n\ ]\n\ }\n\ }\n\ \n\ - id: 2\n\ TestScript1_entity:\n\ comp: {}\n\ \n\ - id: 3\n\ TestScript1_nontree:\n\ comp: ({x:[[2], [2], [], {y:[2]}]})\n\ \n\ - id: 4\n\ TestScript1_custom:\n\ comp: {\n\ \"c\": 1\n\ }\n\ \n" ); std::stringstream stateStream; TS_ASSERT(man.SerializeState(stateStream)); CSimContext context2; CComponentManager man2(context2, g_ScriptContext); man2.LoadComponentTypes(); TS_ASSERT(man2.LoadScript(L"simulation/components/test-serialize.js")); TS_ASSERT(man2.QueryInterface(ent1, IID_Test1) == NULL); TS_ASSERT(man2.DeserializeState(stateStream)); TS_ASSERT_EQUALS(static_cast (man2.QueryInterface(ent1, IID_Test1))->GetX(), 1234); { TestLogger log; // swallow warnings about this.entity being read-only TS_ASSERT_EQUALS(static_cast (man2.QueryInterface(ent2, IID_Test1))->GetX(), (int)ent2); } TS_ASSERT_EQUALS(static_cast (man2.QueryInterface(ent3, IID_Test1))->GetX(), 12); } void test_script_serialization_errors() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-serialize.js")); entity_id_t ent1 = 1; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CParamNode noParam; man.AddComponent(hnd1, man.LookupCID("TestScript1_getter"), noParam); std::stringstream stateStream; TS_ASSERT_THROWS_PSERROR(man.SerializeState(stateStream), PSERROR_Serialize_ScriptError, "Cannot serialize property getters"); // (The script will die if the getter is executed) } void test_script_serialization_template() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); TS_ASSERT(man.LoadScript(L"simulation/components/test-serialize.js")); man.InitSystemEntity(); entity_id_t ent2 = 2; CEntityHandle hnd2 = man.AllocateEntityHandle(ent2); CParamNode noParam; // The template manager takes care of reloading templates on deserialization, // so we need to use it here TS_ASSERT(man.AddComponent(man.GetSystemEntity(), CID_TemplateManager, noParam)); ICmpTemplateManager* tempMan = static_cast (man.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager)); const CParamNode* testParam = tempMan->LoadTemplate(ent2, "template-serialize"); man.AddComponent(hnd2, man.LookupCID("TestScript1_consts"), testParam->GetChild("TestScript1_consts")); TS_ASSERT_EQUALS(static_cast (man.QueryInterface(ent2, IID_Test1))->GetX(), 12347); std::stringstream stateStream; TS_ASSERT(man.SerializeState(stateStream)); CSimContext context2; CComponentManager man2(context2, g_ScriptContext); man2.LoadComponentTypes(); TS_ASSERT(man2.LoadScript(L"simulation/components/test-serialize.js")); TS_ASSERT(man2.DeserializeState(stateStream)); TS_ASSERT_EQUALS(static_cast (man2.QueryInterface(ent2, IID_Test1))->GetX(), 12347); } void test_dynamic_subscription() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); entity_id_t ent1 = 1; CEntityHandle hnd1 = man.AllocateEntityHandle(ent1); CParamNode noParam; man.AddComponent(hnd1, CID_Test1A, noParam); man.AddComponent(hnd1, CID_Test2A, noParam); man.DynamicSubscriptionNonsync(MT_RenderSubmit, man.QueryInterface(ent1, IID_Test1), true); man.DynamicSubscriptionNonsync(MT_RenderSubmit, man.QueryInterface(ent1, IID_Test2), true); man.DestroyComponentsSoon(ent1); man.FlushDestroyedComponents(); } }; Index: ps/trunk/source/simulation2/tests/test_Simulation2.h =================================================================== --- ps/trunk/source/simulation2/tests/test_Simulation2.h (revision 25106) +++ ps/trunk/source/simulation2/tests/test_Simulation2.h (revision 25107) @@ -1,171 +1,171 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "lib/self_test.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" #include "simulation2/MessageTypes.h" #include "simulation2/components/ICmpTest.h" #include "graphics/Terrain.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/XML/Xeromyces.h" class TestSimulation2 : public CxxTest::TestSuite { void copyFile(const VfsPath& src, const VfsPath& dst) { shared_ptr data; size_t size = 0; TS_ASSERT_OK(g_VFS->LoadFile(src, data, size)); TS_ASSERT_OK(g_VFS->CreateFile(dst, data, size)); } CTerrain m_Terrain; public: void setUp() { g_VFS = CreateVfs(); - TS_ASSERT_OK(g_VFS->Mount(L"", DataDir()/"mods"/"_test.sim", VFS_MOUNT_MUST_EXIST)); - TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir()/"_testcache")); + TS_ASSERT_OK(g_VFS->Mount(L"", DataDir() / "mods" / "_test.sim" / "", VFS_MOUNT_MUST_EXIST)); + TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir() / "_testcache" / "", 0, VFS_MAX_PRIORITY)); CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); g_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); } void test_AddEntity() { CSimulation2 sim(NULL, g_ScriptContext, &m_Terrain); TS_ASSERT(sim.LoadScripts(L"simulation/components/addentity/")); sim.ResetState(true, true); entity_id_t ent1 = sim.AddEntity(L"test1"); TS_ASSERT_EQUALS(ent1, (u32)2); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent1, IID_Test1))->GetX(), 999); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent1, IID_Test2))->GetX(), 12345); entity_id_t ent2 = sim.AddEntity(L"test1-inherit"); TS_ASSERT_EQUALS(ent2, (u32)3); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent2, IID_Test1))->GetX(), 1234); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent2, IID_Test2))->GetX(), 12345); } void test_DestroyEntity() { CSimulation2 sim(NULL, g_ScriptContext, &m_Terrain); TS_ASSERT(sim.LoadScripts(L"simulation/components/addentity/")); sim.ResetState(true, true); entity_id_t ent1 = sim.AddEntity(L"test1"); entity_id_t ent2 = sim.AddEntity(L"test1"); entity_id_t ent3 = sim.AddEntity(L"test1"); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent1, IID_Test1))->GetX(), 999); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent1, IID_Test2))->GetX(), 12345); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent2, IID_Test1))->GetX(), 999); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent2, IID_Test2))->GetX(), 12345); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent3, IID_Test1))->GetX(), 999); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent3, IID_Test2))->GetX(), 12345); sim.DestroyEntity(ent2); // mark it for deletion TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent2, IID_Test1))->GetX(), 999); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent2, IID_Test2))->GetX(), 12345); sim.FlushDestroyedEntities(); // actually delete it TS_ASSERT(sim.QueryInterface(ent2, IID_Test1) == NULL); TS_ASSERT(sim.QueryInterface(ent2, IID_Test2) == NULL); sim.FlushDestroyedEntities(); // nothing in the queue // Allow destroying INVALID_ENTITY which should do nothing. sim.DestroyEntity(INVALID_ENTITY); sim.FlushDestroyedEntities(); sim.DestroyEntity(ent2); sim.FlushDestroyedEntities(); // already deleted // Other entities weren't affected TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent1, IID_Test1))->GetX(), 999); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent1, IID_Test2))->GetX(), 12345); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent3, IID_Test1))->GetX(), 999); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent3, IID_Test2))->GetX(), 12345); sim.DestroyEntity(ent3); // mark it for deletion twice sim.DestroyEntity(ent3); sim.FlushDestroyedEntities(); TS_ASSERT(sim.QueryInterface(ent3, IID_Test1) == NULL); TS_ASSERT(sim.QueryInterface(ent3, IID_Test2) == NULL); // Messages mustn't get sent to the destroyed components (else we'll crash) CMessageTurnStart msg; sim.BroadcastMessage(msg); } void test_hotload_scripts() { CSimulation2 sim(NULL, g_ScriptContext, &m_Terrain); TS_ASSERT_OK(CreateDirectories(DataDir()/"mods"/"_test.sim"/"simulation"/"components"/"hotload"/"", 0700)); copyFile(L"simulation/components/test-hotload1.js", L"simulation/components/hotload/hotload.js"); TS_ASSERT_OK(g_VFS->RemoveFile(L"simulation/components/hotload/hotload.js")); TS_ASSERT_OK(g_VFS->RepopulateDirectory(L"simulation/components/hotload/")); TS_ASSERT(sim.LoadScripts(L"simulation/components/hotload/")); sim.ResetState(true, true); entity_id_t ent = sim.AddEntity(L"hotload"); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent, IID_Test1))->GetX(), 100); copyFile(L"simulation/components/test-hotload2.js", L"simulation/components/hotload/hotload.js"); TS_ASSERT_OK(g_VFS->RemoveFile(L"simulation/components/hotload/hotload.js")); TS_ASSERT_OK(g_VFS->RepopulateDirectory(L"simulation/components/hotload/")); TS_ASSERT_OK(sim.ReloadChangedFile(L"art/irrelevant.xml")); TS_ASSERT_OK(sim.ReloadChangedFile(L"simulation/components/irrelevant.js")); TS_ASSERT_OK(sim.ReloadChangedFile(L"simulation/components/hotload/nonexistent.js")); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent, IID_Test1))->GetX(), 100); TS_ASSERT_OK(sim.ReloadChangedFile(L"simulation/components/hotload/hotload.js")); TS_ASSERT_EQUALS(static_cast (sim.QueryInterface(ent, IID_Test1))->GetX(), 1000); TS_ASSERT_OK(DeleteDirectory(DataDir()/"mods"/"_test.sim"/"simulation"/"components"/"hotload"/"")); TS_ASSERT_OK(g_VFS->RemoveFile(L"simulation/components/hotload/hotload.js")); TS_ASSERT_OK(g_VFS->RepopulateDirectory(L"simulation/components/hotload/")); TS_ASSERT_OK(sim.ReloadChangedFile(L"simulation/components/hotload/hotload.js")); } }; Index: ps/trunk/source/graphics/tests/test_MapGenerator.h =================================================================== --- ps/trunk/source/graphics/tests/test_MapGenerator.h (revision 25106) +++ ps/trunk/source/graphics/tests/test_MapGenerator.h (revision 25107) @@ -1,60 +1,60 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 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 "graphics/MapGenerator.h" #include "ps/Filesystem.h" #include "simulation2/system/ComponentTest.h" class TestMapGenerator : public CxxTest::TestSuite { public: void setUp() { g_VFS = CreateVfs(); - g_VFS->Mount(L"", DataDir() / "mods" / "mod", VFS_MOUNT_MUST_EXIST); - g_VFS->Mount(L"", DataDir() / "mods" / "public", VFS_MOUNT_MUST_EXIST, 1); // ignore directory-not-found errors + g_VFS->Mount(L"", DataDir() / "mods" / "mod" / "", VFS_MOUNT_MUST_EXIST); + g_VFS->Mount(L"", DataDir() / "mods" / "public" / "", VFS_MOUNT_MUST_EXIST, 1); // ignore directory-not-found errors CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); g_VFS.reset(); } void test_mapgen_scripts() { if (!VfsDirectoryExists(L"maps/random/tests/")) { debug_printf("Skipping map generator tests (can't find binaries/data/mods/public/maps/random/tests/)\n"); return; } VfsPaths paths; TS_ASSERT_OK(vfs::GetPathnames(g_VFS, L"maps/random/tests/", L"test_*.js", paths)); for (const VfsPath& path : paths) { ScriptInterface scriptInterface("Engine", "MapGenerator", g_ScriptContext); ScriptTestSetup(scriptInterface); CMapGeneratorWorker worker(&scriptInterface); worker.InitScriptInterface(0); scriptInterface.LoadGlobalScriptFile(path); } } }; Index: ps/trunk/source/graphics/tests/test_MeshManager.h =================================================================== --- ps/trunk/source/graphics/tests/test_MeshManager.h (revision 25106) +++ ps/trunk/source/graphics/tests/test_MeshManager.h (revision 25107) @@ -1,261 +1,257 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "lib/self_test.h" #include "lib/file/file_system.h" #include "lib/file/vfs/vfs.h" #include "lib/file/io/io.h" #include "lib/allocators/shared_ptr.h" #include "graphics/ColladaManager.h" #include "graphics/MeshManager.h" #include "graphics/ModelDef.h" #include "ps/CLogger.h" #include "ps/XML/RelaxNG.h" -static OsPath MOD_PATH(DataDir()/"mods"/"_test.mesh"); -static OsPath CACHE_PATH(DataDir()/"_testcache"); +static OsPath MOD_PATH(DataDir() / "mods" / "_test.mesh" / ""); +static OsPath CACHE_PATH(DataDir() / "_testcache" / ""); const OsPath srcDAE(L"collada/sphere.dae"); const OsPath srcPMD(L"collada/sphere.pmd"); const OsPath testDAE(L"art/skeletons/test.dae"); const OsPath testPMD(L"art/skeletons/test.pmd"); const OsPath testBase(L"art/skeletons/test"); const OsPath srcSkeletonDefs(L"collada/skeletons.xml"); const OsPath testSkeletonDefs(L"art/skeletons/skeletons.xml"); extern PIVFS g_VFS; class TestMeshManager : public CxxTest::TestSuite { void initVfs() { // Initialise VFS: // Set up a mod directory to work in: // Make sure the required directories doesn't exist when we start, // in case the previous test aborted and left them full of junk if(DirectoryExists(MOD_PATH)) DeleteDirectory(MOD_PATH); if(DirectoryExists(CACHE_PATH)) DeleteDirectory(CACHE_PATH); g_VFS = CreateVfs(); TS_ASSERT_OK(g_VFS->Mount(L"", MOD_PATH)); - TS_ASSERT_OK(g_VFS->Mount(L"collada/", DataDir()/"tests"/"collada", VFS_MOUNT_MUST_EXIST)); - - // Mount _testcache onto virtual /cache - don't use the normal cache - // directory because that's full of loads of cached files from the - // proper game and takes a long time to load. - TS_ASSERT_OK(g_VFS->Mount(L"cache/", CACHE_PATH)); + TS_ASSERT_OK(g_VFS->Mount(L"collada/", DataDir() / "tests" / "collada" / "", VFS_MOUNT_MUST_EXIST)); + TS_ASSERT_OK(g_VFS->Mount(L"cache/", CACHE_PATH, 0, VFS_MAX_PRIORITY)); } void deinitVfs() { g_VFS.reset(); DeleteDirectory(MOD_PATH); DeleteDirectory(CACHE_PATH); } void copyFile(const VfsPath& src, const VfsPath& dst) { // Copy a file into the mod directory, so we can work on it: shared_ptr data; size_t size = 0; TS_ASSERT_OK(g_VFS->LoadFile(src, data, size)); TS_ASSERT_OK(g_VFS->CreateFile(dst, data, size)); } void buildArchive() { // Create a junk trace file first, because vfs_opt_auto_build requires one // std::string trace = "000.000000: L \"-\" 0 0000\n"; // vfs_store("trace.txt", (const u8*)trace.c_str(), trace.size(), FILE_NO_AIO); // then make the archive // TS_ASSERT_OK(vfs_opt_rebuild_main_archive(MOD_PATH"/trace.txt", MOD_PATH"/test%02d.zip")); } CColladaManager* colladaManager; CMeshManager* meshManager; public: void setUp() { initVfs(); colladaManager = new CColladaManager(g_VFS); meshManager = new CMeshManager(*colladaManager); } void tearDown() { delete meshManager; delete colladaManager; deinitVfs(); } void IRRELEVANT_test_archived() { copyFile(srcDAE, testDAE); //buildArchive(); shared_ptr buf; AllocateAligned(buf, 100, maxSectorSize); strcpy_s((char*)buf.get(), 5, "Test"); g_VFS->CreateFile(testDAE, buf, 4); } void test_load_pmd_with_extension() { copyFile(srcPMD, testPMD); copyFile(srcSkeletonDefs, testSkeletonDefs); CModelDefPtr modeldef = meshManager->GetMesh(testPMD); TS_ASSERT(modeldef); if (modeldef) TS_ASSERT_PATH_EQUALS(modeldef->GetName(), testBase); } void test_load_pmd_without_extension() { copyFile(srcPMD, testPMD); copyFile(srcSkeletonDefs, testSkeletonDefs); CModelDefPtr modeldef = meshManager->GetMesh(testBase); TS_ASSERT(modeldef); if (modeldef) TS_ASSERT_PATH_EQUALS(modeldef->GetName(), testBase); } void test_caching() { copyFile(srcPMD, testPMD); copyFile(srcSkeletonDefs, testSkeletonDefs); CModelDefPtr modeldef1 = meshManager->GetMesh(testPMD); CModelDefPtr modeldef2 = meshManager->GetMesh(testPMD); TS_ASSERT(modeldef1 && modeldef2); if (modeldef1 && modeldef2) TS_ASSERT_EQUALS(modeldef1.get(), modeldef2.get()); } void test_load_dae() { copyFile(srcDAE, testDAE); copyFile(srcSkeletonDefs, testSkeletonDefs); CModelDefPtr modeldef = meshManager->GetMesh(testDAE); TS_ASSERT(modeldef); if (modeldef) TS_ASSERT_PATH_EQUALS(modeldef->GetName(), testBase); } void test_load_dae_caching() { copyFile(srcDAE, testDAE); copyFile(srcSkeletonDefs, testSkeletonDefs); VfsPath daeName1 = colladaManager->GetLoadablePath(testBase, CColladaManager::PMD); VfsPath daeName2 = colladaManager->GetLoadablePath(testBase, CColladaManager::PMD); TS_ASSERT(!daeName1.empty()); TS_ASSERT_PATH_EQUALS(daeName1, daeName2); // TODO: it'd be nice to test that it really isn't doing the DAE->PMD // conversion a second time, but there doesn't seem to be an easy way // to check that } void test_invalid_skeletons() { TestLogger logger; copyFile(srcDAE, testDAE); shared_ptr buf; AllocateAligned(buf, 100, maxSectorSize); strcpy_s((char*)buf.get(), 100, "Not valid XML"); g_VFS->CreateFile(testSkeletonDefs, buf, 13); CModelDefPtr modeldef = meshManager->GetMesh(testDAE); TS_ASSERT(! modeldef); TS_ASSERT_STR_CONTAINS(logger.GetOutput(), "parser error"); } void test_invalid_dae() { TestLogger logger; copyFile(srcSkeletonDefs, testSkeletonDefs); shared_ptr buf; AllocateAligned(buf, 100, maxSectorSize); strcpy_s((char*)buf.get(), 100, "Not valid XML"); g_VFS->CreateFile(testDAE, buf, 13); CModelDefPtr modeldef = meshManager->GetMesh(testDAE); TS_ASSERT(! modeldef); TS_ASSERT_STR_CONTAINS(logger.GetOutput(), "parser error"); } void test_load_nonexistent_pmd() { TestLogger logger; copyFile(srcSkeletonDefs, testSkeletonDefs); CModelDefPtr modeldef = meshManager->GetMesh(testPMD); TS_ASSERT(! modeldef); } void test_load_nonexistent_dae() { TestLogger logger; copyFile(srcSkeletonDefs, testSkeletonDefs); CModelDefPtr modeldef = meshManager->GetMesh(testDAE); TS_ASSERT(! modeldef); } void test_load_across_relaxng() { // Verify that loading meshes doesn't invalidate other users of libxml2 by calling xmlCleanupParser // (Run this in Valgrind and check for use-of-freed-memory errors) RelaxNGValidator v; TS_ASSERT(v.LoadGrammar("")); TS_ASSERT(v.Validate(L"doc", L"2.0")); copyFile(srcDAE, testDAE); copyFile(srcSkeletonDefs, testSkeletonDefs); CModelDefPtr modeldef = meshManager->GetMesh(testDAE); TS_ASSERT(modeldef); if (modeldef) TS_ASSERT_PATH_EQUALS(modeldef->GetName(), testBase); TS_ASSERT(v.Validate(L"doc", L"2.0")); } ////////////////////////////////////////////////////////////////////////// // Tests based on real DAE files: void test_load_dae_bogus_material_target() { copyFile(L"collada/bogus_material_target.dae", testDAE); copyFile(srcSkeletonDefs, testSkeletonDefs); CModelDefPtr modeldef = meshManager->GetMesh(testDAE); TS_ASSERT(modeldef); } }; Index: ps/trunk/source/graphics/tests/test_TextureConverter.h =================================================================== --- ps/trunk/source/graphics/tests/test_TextureConverter.h (revision 25106) +++ ps/trunk/source/graphics/tests/test_TextureConverter.h (revision 25107) @@ -1,96 +1,96 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "lib/self_test.h" #include "graphics/TextureConverter.h" #include "lib/file/vfs/vfs.h" #include "lib/res/h_mgr.h" #include "lib/tex/tex.h" #include "ps/XML/Xeromyces.h" class TestTextureConverter : public CxxTest::TestSuite { PIVFS m_VFS; public: void setUp() { DeleteDirectory(DataDir()/"_testcache"); // clean up in case the last test run failed m_VFS = CreateVfs(); - TS_ASSERT_OK(m_VFS->Mount(L"", DataDir()/"mods"/"_test.tex", VFS_MOUNT_MUST_EXIST)); - TS_ASSERT_OK(m_VFS->Mount(L"cache/", DataDir()/"_testcache")); + TS_ASSERT_OK(m_VFS->Mount(L"", DataDir() / "mods" / "_test.tex" / "", VFS_MOUNT_MUST_EXIST)); + TS_ASSERT_OK(m_VFS->Mount(L"cache/", DataDir() / "_testcache" / "", 0, VFS_MAX_PRIORITY)); } void tearDown() { m_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); } void test_convert_quality() { // Test for the bug in http://code.google.com/p/nvidia-texture-tools/issues/detail?id=139 VfsPath src = L"art/textures/b/test.png"; CTextureConverter converter(m_VFS, false); CTextureConverter::Settings settings = converter.ComputeSettings(L"", std::vector()); TS_ASSERT(converter.ConvertTexture(CTexturePtr(), src, L"cache/test.png", settings)); VfsPath dest; for (size_t i = 0; i < 100; ++i) { CTexturePtr texture; bool ok; if (converter.Poll(texture, dest, ok)) { TS_ASSERT(ok); break; } SDL_Delay(10); } shared_ptr file; size_t fileSize = 0; TS_ASSERT_OK(m_VFS->LoadFile(dest, file, fileSize)); Tex tex; TS_ASSERT_OK(tex.decode(file, fileSize)); TS_ASSERT_OK(tex.transform_to((tex.m_Flags | TEX_BGR | TEX_ALPHA) & ~(TEX_DXT | TEX_MIPMAPS))); u8* texdata = tex.get_data(); // The source texture is repeated after 4 pixels, so the compressed texture // should be identical after 4 pixels TS_ASSERT_EQUALS(texdata[0*4], texdata[4*4]); // 1st, 2nd and 4th rows should be unequal TS_ASSERT_DIFFERS(texdata[0*4], texdata[8*4]); TS_ASSERT_EQUALS(texdata[8*4], texdata[16*4]); TS_ASSERT_DIFFERS(texdata[16*4], texdata[24*4]); // for (size_t i = 0; i < tex.dataSize; ++i) // { // if (i % 4 == 0) printf("\n"); // printf("%02x ", texdata[i]); // } } }; Index: ps/trunk/source/graphics/tests/test_TextureManager.h =================================================================== --- ps/trunk/source/graphics/tests/test_TextureManager.h (revision 25106) +++ ps/trunk/source/graphics/tests/test_TextureManager.h (revision 25107) @@ -1,128 +1,128 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "lib/self_test.h" #include "graphics/TextureManager.h" #include "lib/external_libraries/libsdl.h" #include "lib/file/vfs/vfs.h" #include "lib/res/h_mgr.h" #include "lib/tex/tex.h" #include "lib/ogl.h" #include "ps/XML/Xeromyces.h" class TestTextureManager : public CxxTest::TestSuite { PIVFS m_VFS; public: void setUp() { DeleteDirectory(DataDir()/"_testcache"); // clean up in case the last test run failed m_VFS = CreateVfs(); - TS_ASSERT_OK(m_VFS->Mount(L"", DataDir()/"mods"/"_test.tex", VFS_MOUNT_MUST_EXIST)); - TS_ASSERT_OK(m_VFS->Mount(L"cache/", DataDir()/"_testcache")); + TS_ASSERT_OK(m_VFS->Mount(L"", DataDir() / "mods" / "_test.tex" / "", VFS_MOUNT_MUST_EXIST)); + TS_ASSERT_OK(m_VFS->Mount(L"cache/", DataDir() / "_testcache" / "", 0, VFS_MAX_PRIORITY)); h_mgr_init(); CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); h_mgr_shutdown(); m_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); } void test_load_basic() { { CTextureManager texman(m_VFS, false, true); CTexturePtr t1 = texman.CreateTexture(CTextureProperties(L"art/textures/a/demo.png")); TS_ASSERT(!t1->IsLoaded()); TS_ASSERT(!t1->TryLoad()); TS_ASSERT(!t1->IsLoaded()); TS_ASSERT(texman.MakeProgress()); for (size_t i = 0; i < 100; ++i) { if (texman.MakeProgress()) break; SDL_Delay(10); } TS_ASSERT(t1->IsLoaded()); // We can't test sizes because we had to disable GL function calls // and therefore couldn't load the texture. Maybe we should try loading // the texture file directly, to make sure it's actually worked. // TS_ASSERT_EQUALS(t1->GetWidth(), (size_t)64); // TS_ASSERT_EQUALS(t1->GetHeight(), (size_t)64); // CreateTexture should return the same object CTexturePtr t2 = texman.CreateTexture(CTextureProperties(L"art/textures/a/demo.png")); TS_ASSERT(t1 == t2); } // New texture manager - should use the cached file { CTextureManager texman(m_VFS, false, true); CTexturePtr t1 = texman.CreateTexture(CTextureProperties(L"art/textures/a/demo.png")); TS_ASSERT(!t1->IsLoaded()); TS_ASSERT(t1->TryLoad()); TS_ASSERT(t1->IsLoaded()); } } void test_load_formats() { CTextureManager texman(m_VFS, false, true); CTexturePtr t1 = texman.CreateTexture(CTextureProperties(L"art/textures/a/demo.tga")); CTexturePtr t2 = texman.CreateTexture(CTextureProperties(L"art/textures/a/demo-abgr.dds")); CTexturePtr t3 = texman.CreateTexture(CTextureProperties(L"art/textures/a/demo-dxt1.dds")); CTexturePtr t4 = texman.CreateTexture(CTextureProperties(L"art/textures/a/demo-dxt5.dds")); t1->TryLoad(); t2->TryLoad(); t3->TryLoad(); t4->TryLoad(); size_t done = 0; for (size_t i = 0; i < 100; ++i) { if (texman.MakeProgress()) ++done; if (done == 8) // 4 loads, 4 conversions break; SDL_Delay(10); } TS_ASSERT(t1->IsLoaded()); TS_ASSERT(t2->IsLoaded()); TS_ASSERT(t3->IsLoaded()); TS_ASSERT(t4->IsLoaded()); } }; Index: ps/trunk/source/gui/tests/test_GuiManager.h =================================================================== --- ps/trunk/source/gui/tests/test_GuiManager.h (revision 25106) +++ ps/trunk/source/gui/tests/test_GuiManager.h (revision 25107) @@ -1,194 +1,194 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "lib/self_test.h" #include "gui/GUIManager.h" #include "gui/CGUI.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/GameSetup/GameSetup.h" #include "ps/Hotkey.h" #include "ps/XML/Xeromyces.h" class TestGuiManager : public CxxTest::TestSuite { CConfigDB* configDB; public: void setUp() { g_VFS = CreateVfs(); - TS_ASSERT_OK(g_VFS->Mount(L"", DataDir()/"mods"/"_test.gui", VFS_MOUNT_MUST_EXIST)); - TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir()/"_testcache")); + TS_ASSERT_OK(g_VFS->Mount(L"", DataDir() / "mods" / "_test.gui" / "", VFS_MOUNT_MUST_EXIST)); + TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir() / "_testcache" / "", 0, VFS_MAX_PRIORITY)); configDB = new CConfigDB; CXeromyces::Startup(); g_GUI = new CGUIManager(); } void tearDown() { delete g_GUI; CXeromyces::Terminate(); delete configDB; g_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); } void test_EventObject() { // Load up a test page. const ScriptInterface& scriptInterface = *(g_GUI->GetScriptInterface()); ScriptRequest rq(scriptInterface); JS::RootedValue val(rq.cx); scriptInterface.CreateObject(rq, &val); ScriptInterface::StructuredClone data = scriptInterface.WriteStructuredClone(JS::NullHandleValue); g_GUI->PushPage(L"event/page_event.xml", data, JS::UndefinedHandleValue); const ScriptInterface& pageScriptInterface = *(g_GUI->GetActiveGUI()->GetScriptInterface()); ScriptRequest prq(pageScriptInterface); JS::RootedValue global(prq.cx, prq.globalValue()); int called_value = 0; JS::RootedValue js_called_value(prq.cx); // Ticking will call the onTick handlers of all object. The second // onTick is configured to disable the onTick handlers of the first and // third and enable the fourth. So ticking once will only call the // first and second object. We don't want the fourth object to be // called, to avoid infinite additions of objects. g_GUI->TickObjects(); pageScriptInterface.GetProperty(global, "called1", &js_called_value); ScriptInterface::FromJSVal(prq, js_called_value, called_value); TS_ASSERT_EQUALS(called_value, 1); pageScriptInterface.GetProperty(global, "called2", &js_called_value); ScriptInterface::FromJSVal(prq, js_called_value, called_value); TS_ASSERT_EQUALS(called_value, 1); pageScriptInterface.GetProperty(global, "called3", &js_called_value); ScriptInterface::FromJSVal(prq, js_called_value, called_value); TS_ASSERT_EQUALS(called_value, 0); pageScriptInterface.GetProperty(global, "called4", &js_called_value); ScriptInterface::FromJSVal(prq, js_called_value, called_value); TS_ASSERT_EQUALS(called_value, 0); // Ticking again will still call the second object, but also the fourth. g_GUI->TickObjects(); pageScriptInterface.GetProperty(global, "called1", &js_called_value); ScriptInterface::FromJSVal(prq, js_called_value, called_value); TS_ASSERT_EQUALS(called_value, 1); pageScriptInterface.GetProperty(global, "called2", &js_called_value); ScriptInterface::FromJSVal(prq, js_called_value, called_value); TS_ASSERT_EQUALS(called_value, 2); pageScriptInterface.GetProperty(global, "called3", &js_called_value); ScriptInterface::FromJSVal(prq, js_called_value, called_value); TS_ASSERT_EQUALS(called_value, 0); pageScriptInterface.GetProperty(global, "called4", &js_called_value); ScriptInterface::FromJSVal(prq, js_called_value, called_value); TS_ASSERT_EQUALS(called_value, 1); } void test_hotkeysState() { // Load up a fake test hotkey when pressing 'a'. const char* test_hotkey_name = "hotkey.test"; configDB->SetValueString(CFG_USER, test_hotkey_name, "A"); LoadHotkeys(*configDB); // Load up a test page. const ScriptInterface& scriptInterface = *(g_GUI->GetScriptInterface()); ScriptRequest rq(scriptInterface); JS::RootedValue val(rq.cx); scriptInterface.CreateObject(rq, &val); ScriptInterface::StructuredClone data = scriptInterface.WriteStructuredClone(JS::NullHandleValue); g_GUI->PushPage(L"hotkey/page_hotkey.xml", data, JS::UndefinedHandleValue); // Press 'a'. SDL_Event_ hotkeyNotification; hotkeyNotification.ev.type = SDL_KEYDOWN; hotkeyNotification.ev.key.keysym.scancode = SDL_SCANCODE_A; hotkeyNotification.ev.key.repeat = 0; // Init input and poll the event. InitInput(); in_push_priority_event(&hotkeyNotification); SDL_Event_ ev; while (in_poll_event(&ev)) in_dispatch_event(&ev); const ScriptInterface& pageScriptInterface = *(g_GUI->GetActiveGUI()->GetScriptInterface()); ScriptRequest prq(pageScriptInterface); JS::RootedValue global(prq.cx, prq.globalValue()); // Ensure that our hotkey state was synchronised with the event itself. bool hotkey_pressed_value = false; JS::RootedValue js_hotkey_pressed_value(prq.cx); pageScriptInterface.GetProperty(global, "state_before", &js_hotkey_pressed_value); ScriptInterface::FromJSVal(prq, js_hotkey_pressed_value, hotkey_pressed_value); TS_ASSERT_EQUALS(hotkey_pressed_value, true); hotkey_pressed_value = false; pageScriptInterface.GetProperty(global, "state_after", &js_hotkey_pressed_value); ScriptInterface::FromJSVal(prq, js_hotkey_pressed_value, hotkey_pressed_value); TS_ASSERT_EQUALS(hotkey_pressed_value, true); // We are listening to KeyDown events, so repeat shouldn't matter. hotkeyNotification.ev.key.repeat = 1; in_push_priority_event(&hotkeyNotification); while (in_poll_event(&ev)) in_dispatch_event(&ev); hotkey_pressed_value = false; pageScriptInterface.GetProperty(global, "state_before", &js_hotkey_pressed_value); ScriptInterface::FromJSVal(prq, js_hotkey_pressed_value, hotkey_pressed_value); TS_ASSERT_EQUALS(hotkey_pressed_value, true); hotkey_pressed_value = false; pageScriptInterface.GetProperty(global, "state_after", &js_hotkey_pressed_value); ScriptInterface::FromJSVal(prq, js_hotkey_pressed_value, hotkey_pressed_value); TS_ASSERT_EQUALS(hotkey_pressed_value, true); hotkeyNotification.ev.type = SDL_KEYUP; in_push_priority_event(&hotkeyNotification); while (in_poll_event(&ev)) in_dispatch_event(&ev); hotkey_pressed_value = true; pageScriptInterface.GetProperty(global, "state_before", &js_hotkey_pressed_value); ScriptInterface::FromJSVal(prq, js_hotkey_pressed_value, hotkey_pressed_value); TS_ASSERT_EQUALS(hotkey_pressed_value, false); hotkey_pressed_value = true; pageScriptInterface.GetProperty(global, "state_after", &js_hotkey_pressed_value); ScriptInterface::FromJSVal(prq, js_hotkey_pressed_value, hotkey_pressed_value); TS_ASSERT_EQUALS(hotkey_pressed_value, false); UnloadHotkeys(); } }; Index: ps/trunk/source/lib/file/common/real_directory.cpp =================================================================== --- ps/trunk/source/lib/file/common/real_directory.cpp (revision 25106) +++ ps/trunk/source/lib/file/common/real_directory.cpp (revision 25107) @@ -1,72 +1,73 @@ -/* Copyright (C) 2010 Wildfire Games. +/* Copyright (C) 2021 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/common/real_directory.h" #include "lib/sysdep/filesystem.h" #include "lib/file/file.h" #include "lib/file/io/io.h" RealDirectory::RealDirectory(const OsPath& path, size_t priority, size_t flags) : m_path(path), m_priority(priority), m_flags(flags) { + ENSURE(path.IsDirectory()); } /*virtual*/ size_t RealDirectory::Precedence() const { return 1u; } /*virtual*/ wchar_t RealDirectory::LocationCode() const { return 'F'; } /*virtual*/ Status RealDirectory::Load(const OsPath& name, const shared_ptr& buf, size_t size) const { return io::Load(m_path / name, buf.get(), size); } Status RealDirectory::Store(const OsPath& name, const shared_ptr& fileContents, size_t size) { return io::Store(m_path / name, fileContents.get(), size); } void RealDirectory::Watch() { if(!m_watch) (void)dir_watch_Add(m_path, m_watch); } PRealDirectory CreateRealSubdirectory(const PRealDirectory& realDirectory, const OsPath& subdirectoryName) { const OsPath path = realDirectory->Path() / subdirectoryName/""; return PRealDirectory(new RealDirectory(path, realDirectory->Priority(), realDirectory->Flags())); } Index: ps/trunk/source/lib/file/vfs/tests/test_vfs_real_path.h =================================================================== --- ps/trunk/source/lib/file/vfs/tests/test_vfs_real_path.h (nonexistent) +++ ps/trunk/source/lib/file/vfs/tests/test_vfs_real_path.h (revision 25107) @@ -0,0 +1,190 @@ +/* Copyright (C) 2021 Wildfire Games. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#include "lib/self_test.h" + +#include "lib/file/vfs/vfs_populate.h" +#include "lib/os_path.h" + +static OsPath TEST_FOLDER(DataDir()/"_test.temp"); + +extern PIVFS g_VFS; + +class TestVfsPopulate : public CxxTest::TestSuite +{ + void initVfs() + { + g_VFS = CreateVfs(); + } + + void deinitVfs() + { + if(DirectoryExists(TEST_FOLDER)) + DeleteDirectory(TEST_FOLDER); + + g_VFS.reset(); + } + + void createRealDir(const OsPath& path) + { + if(DirectoryExists(path)) + DeleteDirectory(path); + CreateDirectories(path, 0700, false); + } + +public: + void setUp() + { + initVfs(); + } + + void tearDown() + { + deinitVfs(); + } + + /** + * This is a regression test. Before priorities were used for real directories, things were... Rather undefined. + * The new spec is that the path is relative to the highest priority subdirectory in the path. + * The order remains undefined in case of equal priority. + * (see below for tests on that). + */ + void test_write_path_hijacking() + { + createRealDir(TEST_FOLDER / "cache" / "some_folder"); + createRealDir(TEST_FOLDER / "some_mod" / "cache" / "some_mod"); + + shared_ptr buf(new u8(1)); + + g_VFS->Mount(L"", TEST_FOLDER / "some_mod" / "", 0, 0); + + // Access the subfolder, creating subdirectories in the VFS. + g_VFS->CreateFile(L"cache/some_mod/peek.txt", buf, 0); + + g_VFS->Mount(L"cache/", TEST_FOLDER / "cache" / "", 0, 1); + + OsPath realPath; + g_VFS->GetDirectoryRealPath(L"cache/", realPath); + TS_ASSERT_EQUALS(realPath, TEST_FOLDER / "cache" / ""); + g_VFS->CreateFile(L"cache/test.txt", buf, 0); + g_VFS->GetRealPath(L"cache/test.txt", realPath); + TS_ASSERT_EQUALS(realPath, TEST_FOLDER / "cache" / "test.txt"); + + g_VFS->GetDirectoryRealPath(L"cache/some_mod/", realPath); + TS_ASSERT_EQUALS(realPath, TEST_FOLDER / "cache" / "some_mod" / ""); + g_VFS->CreateFile(L"cache/some_mod/test.txt", buf, 0); + g_VFS->GetRealPath(L"cache/some_mod/test.txt", realPath); + TS_ASSERT_EQUALS(realPath, TEST_FOLDER / "cache" / "some_mod" / "test.txt"); + }; + + void test_priority() + { + createRealDir(TEST_FOLDER / "mod_a"); + createRealDir(TEST_FOLDER / "mod_b"); + + // Check that the real directory points to the highest priority mod. + g_VFS->Mount(L"mod", TEST_FOLDER / "mod_a" / "", 0, 1); + g_VFS->Mount(L"mod", TEST_FOLDER / "mod_b" / "", 0, 0); + + // For consistency, populate everything. + g_VFS->TextRepresentation().c_str(); + + OsPath realPath; + g_VFS->GetDirectoryRealPath(L"mod", realPath); + TS_ASSERT_EQUALS(realPath, TEST_FOLDER / "mod_a" / ""); + }; + + void test_real_path_0() + { + createRealDir(TEST_FOLDER / "mod_0" / "folder_a"); + createRealDir(TEST_FOLDER / "mod_0" / "folder_a" / "subfolder"); + createRealDir(TEST_FOLDER / "mod_0" / "folder_b"); + createRealDir(TEST_FOLDER / "mod_1" / "folder_a"); + createRealDir(TEST_FOLDER / "folder_a"); + + g_VFS->Mount(L"folder_a/", TEST_FOLDER / "folder_a" / "", 0, 2); + g_VFS->Mount(L"", TEST_FOLDER / "mod_1" / "", 0, 1); + g_VFS->Mount(L"", TEST_FOLDER / "mod_0" / "", 0, 0); + + OsPath realPath; + g_VFS->GetDirectoryRealPath(L"folder_a/", realPath); + TS_ASSERT_EQUALS(realPath, TEST_FOLDER / "folder_a" / ""); + // Despite being lower priority, we still load non-conflicting files of mod_0 + g_VFS->GetDirectoryRealPath(L"folder_b/", realPath); + // However their real path is rewritten to mod_1. + TS_ASSERT_EQUALS(realPath, TEST_FOLDER / "mod_1" / "folder_b" / ""); + + // (including sub-subfolders) + g_VFS->GetDirectoryRealPath(L"folder_a/subfolder/", realPath); + // However their real path is rewritten. + TS_ASSERT_EQUALS(realPath, TEST_FOLDER / "folder_a" / "subfolder" / ""); + }; + + void test_real_path_1() + { + createRealDir(TEST_FOLDER / "mod_0" / "folder_a"); + createRealDir(TEST_FOLDER / "mod_1" / "folder_a"); + createRealDir(TEST_FOLDER / "folder_a"); + + // Equal priority, the order is undetermined. + OsPath realPath; + g_VFS->Mount(L"", TEST_FOLDER / "mod_0" / "", 0, 1); + g_VFS->Mount(L"folder_a/", TEST_FOLDER / "folder_a" / "", 0, 1); + g_VFS->Mount(L"", TEST_FOLDER / "mod_1" / "", 0, 0); + + g_VFS->GetDirectoryRealPath(L"folder_a/", realPath); + TS_ASSERT_EQUALS(realPath, TEST_FOLDER / "folder_a" / ""); + }; + + // Same as test_real_path_1, but invert the mounting order + // (may or may not result in different behaviour). + void test_real_path_2() + { + createRealDir(TEST_FOLDER / "mod_0" / "folder_a"); + createRealDir(TEST_FOLDER / "mod_1" / "folder_a"); + createRealDir(TEST_FOLDER / "folder_a"); + + // Equal priority, the order is undetermined. + OsPath realPath; + g_VFS->Mount(L"folder_a/", TEST_FOLDER / "folder_a" / "", 0, 1); + g_VFS->Mount(L"", TEST_FOLDER / "mod_1" / "", 0, 0); + g_VFS->Mount(L"", TEST_FOLDER / "mod_0" / "", 0, 1); + + g_VFS->GetDirectoryRealPath(L"folder_a/", realPath); + TS_ASSERT_EQUALS(realPath, TEST_FOLDER / "folder_a" / ""); + }; + + void test_real_path_subpath() + { + createRealDir(TEST_FOLDER / "mod_0" / "folder" / "subfolder"); + createRealDir(TEST_FOLDER / "mod_1" / "folder" / "subfolder"); + createRealDir(TEST_FOLDER / "other_folder"); + + g_VFS->Mount(L"", TEST_FOLDER / "mod_0" / "", 0, 0); + g_VFS->Mount(L"", TEST_FOLDER / "mod_1" / "", 0, 1); + g_VFS->Mount(L"folder/subfolder/", TEST_FOLDER / "other_folder" / "", 0, 2); + + OsPath realPath; + g_VFS->GetDirectoryRealPath(L"folder/subfolder/", realPath); + TS_ASSERT_EQUALS(realPath, TEST_FOLDER / "other_folder" / ""); + }; +}; Property changes on: ps/trunk/source/lib/file/vfs/tests/test_vfs_real_path.h ___________________________________________________________________ Added: svn:eol-style ## -0,0 +1 ## +native \ No newline at end of property Index: ps/trunk/source/lib/file/vfs/tests/test_vfs_util.h =================================================================== --- ps/trunk/source/lib/file/vfs/tests/test_vfs_util.h (revision 25106) +++ ps/trunk/source/lib/file/vfs/tests/test_vfs_util.h (revision 25107) @@ -1,95 +1,91 @@ -/* Copyright (C) 2019 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "lib/self_test.h" #include "lib/file/vfs/vfs_util.h" #include "lib/file/file_system.h" #include "lib/os_path.h" -static OsPath MOD_PATH(DataDir()/"mods"/"_test.vfs"); -static OsPath CACHE_PATH(DataDir()/"_testcache"); +static OsPath MOD_PATH(DataDir() / "mods" / "_test.vfs" / ""); +static OsPath CACHE_PATH(DataDir() / "_testcache" / ""); extern PIVFS g_VFS; class TestVfsUtil : public CxxTest::TestSuite { void initVfs() { // Initialise VFS: // Make sure the required directories doesn't exist when we start, // in case the previous test aborted and left them full of junk if(DirectoryExists(MOD_PATH)) DeleteDirectory(MOD_PATH); if(DirectoryExists(CACHE_PATH)) DeleteDirectory(CACHE_PATH); g_VFS = CreateVfs(); TS_ASSERT_OK(g_VFS->Mount(L"", MOD_PATH)); - - // Mount _testcache onto virtual /cache - don't use the normal cache - // directory because that's full of loads of cached files from the - // proper game and takes a long time to load. - TS_ASSERT_OK(g_VFS->Mount(L"cache/", CACHE_PATH)); + TS_ASSERT_OK(g_VFS->Mount(L"cache/", CACHE_PATH, 0, VFS_MAX_PRIORITY)); } void deinitVfs() { g_VFS.reset(); DeleteDirectory(MOD_PATH); DeleteDirectory(CACHE_PATH); } public: void setUp() { initVfs(); } void tearDown() { deinitVfs(); } void test_getPathnames() { std::shared_ptr nodata(new u8); g_VFS->CreateFile("test_file.txt", nodata, 0); g_VFS->CreateFile("test_file2.txt", nodata, 0); g_VFS->CreateFile("test_file3.txt", nodata, 0); g_VFS->CreateFile("test_file2.not_txt", nodata, 0); g_VFS->CreateFile("sub_folder_a/sub_test_file1.txt", nodata, 0); g_VFS->CreateFile("sub_folder_a/sub_test_file2.txt", nodata, 0); g_VFS->CreateFile("sub_folder_b/sub_test_file1.txt", nodata, 0); g_VFS->CreateFile("sub_folder_b/another_file.not_txt", nodata, 0); VfsPaths pathNames; vfs::GetPathnames(g_VFS, "", L"*.txt", pathNames); TS_ASSERT_EQUALS(pathNames.size(), 3); vfs::GetPathnames(g_VFS, "sub_folder_a/", L"*.txt", pathNames); TS_ASSERT_EQUALS(pathNames.size(), 5); vfs::GetPathnames(g_VFS, "sub_folder_b/", L"*.txt", pathNames); TS_ASSERT_EQUALS(pathNames.size(), 6); }; }; Index: ps/trunk/source/lib/file/vfs/vfs.cpp =================================================================== --- ps/trunk/source/lib/file/vfs/vfs.cpp (revision 25106) +++ ps/trunk/source/lib/file/vfs/vfs.cpp (revision 25107) @@ -1,294 +1,283 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2021 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/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 #include 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 std::mutex vfs_mutex; class VFS : public IVFS { public: VFS() : m_trace(CreateDummyTrace(8*MiB)) { } virtual Status Mount(const VfsPath& mountPoint, const OsPath& path, size_t flags /* = 0 */, size_t priority /* = 0 */) { + ENSURE(path.IsDirectory()); + std::lock_guard lock(vfs_mutex); if(!DirectoryExists(path)) { if(flags & VFS_MOUNT_MUST_EXIST) return ERR::VFS_DIR_NOT_FOUND; // NOWARN else RETURN_STATUS_IF_ERR(CreateDirectories(path, 0700)); } VfsDirectory* directory; WARN_RETURN_STATUS_IF_ERR(vfs_Lookup(mountPoint, &m_rootDirectory, directory, 0, VFS_LOOKUP_ADD|VFS_LOOKUP_SKIP_POPULATE)); PRealDirectory realDirectory(new RealDirectory(path, priority, flags)); RETURN_STATUS_IF_ERR(vfs_Attach(directory, realDirectory)); return INFO::OK; } virtual Status GetFileInfo(const VfsPath& pathname, CFileInfo* pfileInfo) const { std::lock_guard lock(vfs_mutex); VfsDirectory* directory; VfsFile* file; Status ret = vfs_Lookup(pathname, &m_rootDirectory, directory, &file); if(!pfileInfo) // just indicate if the file exists without raising warnings. return ret; WARN_RETURN_STATUS_IF_ERR(ret); *pfileInfo = CFileInfo(file->Name(), file->Size(), file->MTime()); return INFO::OK; } virtual Status GetFilePriority(const VfsPath& pathname, size_t* ppriority) const { std::lock_guard lock(vfs_mutex); VfsDirectory* directory; VfsFile* file; RETURN_STATUS_IF_ERR(vfs_Lookup(pathname, &m_rootDirectory, directory, &file)); *ppriority = file->Priority(); return INFO::OK; } virtual Status GetDirectoryEntries(const VfsPath& path, CFileInfos* fileInfos, DirectoryNames* subdirectoryNames) const { std::lock_guard lock(vfs_mutex); VfsDirectory* directory; RETURN_STATUS_IF_ERR(vfs_Lookup(path, &m_rootDirectory, directory, 0)); if(fileInfos) { const VfsDirectory::VfsFiles& files = directory->Files(); fileInfos->clear(); fileInfos->reserve(files.size()); for(VfsDirectory::VfsFiles::const_iterator it = files.begin(); it != files.end(); ++it) { const VfsFile& file = it->second; fileInfos->push_back(CFileInfo(file.Name(), file.Size(), file.MTime())); } } if(subdirectoryNames) { const VfsDirectory::VfsSubdirectories& subdirectories = directory->Subdirectories(); subdirectoryNames->clear(); subdirectoryNames->reserve(subdirectories.size()); for(VfsDirectory::VfsSubdirectories::const_iterator it = subdirectories.begin(); it != subdirectories.end(); ++it) subdirectoryNames->push_back(it->first); } return INFO::OK; } virtual Status CreateFile(const VfsPath& pathname, const shared_ptr& fileContents, size_t size) { std::lock_guard lock(vfs_mutex); VfsDirectory* directory; Status st; - st = vfs_Lookup(pathname, &m_rootDirectory, directory, 0, VFS_LOOKUP_ADD|VFS_LOOKUP_CREATE|VFS_LOOKUP_CREATE_ALWAYS); + st = vfs_Lookup(pathname, &m_rootDirectory, directory, 0, VFS_LOOKUP_ADD|VFS_LOOKUP_REAL_PATH); if (st == ERR::FILE_ACCESS) return ERR::FILE_ACCESS; WARN_RETURN_STATUS_IF_ERR(st); const PRealDirectory& realDirectory = directory->AssociatedDirectory(); const OsPath name = pathname.Filename(); RETURN_STATUS_IF_ERR(realDirectory->Store(name, fileContents, size)); const VfsFile file(name, size, time(0), realDirectory->Priority(), realDirectory); directory->AddFile(file); m_trace->NotifyStore(pathname, size); return INFO::OK; } - virtual Status ReplaceFile(const VfsPath& pathname, const shared_ptr& fileContents, size_t size) - { - std::unique_lock lock(vfs_mutex); - VfsDirectory* directory; - VfsFile* file; - Status st; - st = vfs_Lookup(pathname, &m_rootDirectory, directory, &file, VFS_LOOKUP_ADD|VFS_LOOKUP_CREATE); - - // There is no such file, create it. - if (st == ERR::VFS_FILE_NOT_FOUND) - { - lock.unlock(); - return CreateFile(pathname, fileContents, size); - } - - WARN_RETURN_STATUS_IF_ERR(st); - - RealDirectory realDirectory(file->Loader()->Path(), file->Priority(), directory->AssociatedDirectory()->Flags()); - RETURN_STATUS_IF_ERR(realDirectory.Store(pathname.Filename(), fileContents, size)); - - directory->AddFile(*file); - - m_trace->NotifyStore(pathname, size); - return INFO::OK; - } - virtual Status LoadFile(const VfsPath& pathname, shared_ptr& fileContents, size_t& size) { std::lock_guard lock(vfs_mutex); VfsDirectory* directory; VfsFile* file; // per 2010-05-01 meeting, this shouldn't raise 'scary error // dialogs', which might fail to display the culprit pathname // instead, callers should log the error, including pathname. RETURN_STATUS_IF_ERR(vfs_Lookup(pathname, &m_rootDirectory, directory, &file)); fileContents = DummySharedPtr((u8*)0); size = file->Size(); RETURN_STATUS_IF_ERR(AllocateAligned(fileContents, size, maxSectorSize)); RETURN_STATUS_IF_ERR(file->Loader()->Load(file->Name(), fileContents, file->Size())); stats_io_user_request(size); m_trace->NotifyLoad(pathname, size); return INFO::OK; } virtual std::wstring TextRepresentation() const { std::lock_guard lock(vfs_mutex); std::wstring textRepresentation; textRepresentation.reserve(100*KiB); DirectoryDescriptionR(textRepresentation, m_rootDirectory, 0); return textRepresentation; } - virtual Status GetRealPath(const VfsPath& pathname, OsPath& realPathname) + virtual Status GetOriginalPath(const VfsPath& pathname, OsPath& realPathname) { std::lock_guard lock(vfs_mutex); 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) + virtual Status GetRealPath(const VfsPath& pathname, OsPath& realPathname, bool createMissingDirectories) + { + std::lock_guard lock(vfs_mutex); + VfsDirectory* directory; VfsFile* file; + size_t flags = VFS_LOOKUP_REAL_PATH | (createMissingDirectories ? VFS_LOOKUP_ADD : 0); + WARN_RETURN_STATUS_IF_ERR(vfs_Lookup(pathname, &m_rootDirectory, directory, &file, flags)); + ENSURE(directory->AssociatedDirectory()); + realPathname = directory->AssociatedDirectory()->Path() / pathname.Filename(); + return INFO::OK; + } + + virtual Status GetDirectoryRealPath(const VfsPath& pathname, OsPath& realPathname, bool createMissingDirectories) { std::lock_guard lock(vfs_mutex); VfsDirectory* directory; - WARN_RETURN_STATUS_IF_ERR(vfs_Lookup(pathname, &m_rootDirectory, directory, NULL)); + size_t flags = VFS_LOOKUP_REAL_PATH | (createMissingDirectories ? VFS_LOOKUP_ADD : 0); + WARN_RETURN_STATUS_IF_ERR(vfs_Lookup(pathname, &m_rootDirectory, directory, NULL, flags)); + ENSURE(directory->AssociatedDirectory()); realPathname = directory->AssociatedDirectory()->Path(); return INFO::OK; } virtual Status GetVirtualPath(const OsPath& realPathname, VfsPath& pathname) { std::lock_guard lock(vfs_mutex); const OsPath realPath = realPathname.Parent()/""; VfsPath path; RETURN_STATUS_IF_ERR(FindRealPathR(realPath, m_rootDirectory, L"", path)); pathname = path / realPathname.Filename(); return INFO::OK; } virtual Status RemoveFile(const VfsPath& pathname) { std::lock_guard lock(vfs_mutex); VfsDirectory* directory; VfsFile* file; RETURN_STATUS_IF_ERR(vfs_Lookup(pathname, &m_rootDirectory, directory, &file)); directory->RemoveFile(file->Name()); return INFO::OK; } virtual Status RepopulateDirectory(const VfsPath& path) { std::lock_guard lock(vfs_mutex); VfsDirectory* directory; RETURN_STATUS_IF_ERR(vfs_Lookup(path, &m_rootDirectory, directory, 0)); directory->RequestRepopulate(); return INFO::OK; } virtual void Clear() { std::lock_guard lock(vfs_mutex); 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 } PITrace m_trace; mutable VfsDirectory m_rootDirectory; }; //----------------------------------------------------------------------------- PIVFS CreateVfs() { return PIVFS(new VFS()); } Index: ps/trunk/source/lib/file/vfs/vfs.h =================================================================== --- ps/trunk/source/lib/file/vfs/vfs.h (revision 25106) +++ ps/trunk/source/lib/file/vfs/vfs.h (revision 25107) @@ -1,228 +1,230 @@ -/* Copyright (C) 2017 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* * Virtual File System API - allows transparent access to files in * archives, modding via multiple mount points and hotloading. */ #ifndef INCLUDED_VFS #define INCLUDED_VFS #include "lib/file/file_system.h" // CFileInfo #include "lib/file/vfs/vfs_path.h" +constexpr size_t VFS_MIN_PRIORITY = 0; +constexpr size_t VFS_MAX_PRIORITY = std::numeric_limits::max(); + namespace ERR { const Status VFS_DIR_NOT_FOUND = -110100; const Status VFS_FILE_NOT_FOUND = -110101; const Status VFS_ALREADY_MOUNTED = -110102; } // (recursive mounting and mounting archives are no longer optional since they don't hurt) enum VfsMountFlags { /** * all real directories mounted during this operation will be watched * for changes. this flag is provided to avoid watches in output-only * directories, e.g. screenshots/ (only causes unnecessary overhead). **/ VFS_MOUNT_WATCH = 1, /** * anything mounted from here should be included when building archives. **/ VFS_MOUNT_ARCHIVABLE = 2, /** * return ERR::VFS_DIR_NOT_FOUND if the given real path doesn't exist. * (the default behavior is to create all real directories in the path) **/ VFS_MOUNT_MUST_EXIST = 4, /** * keep the files named "*.DELETED" visible in the VFS directories. * the standard behavior of hiding the file with the same name minus the * ".DELETED" suffix will still apply. * (the default behavior is to hide both the suffixed and unsuffixed files) **/ - VFS_MOUNT_KEEP_DELETED = 8, - - /** - * mark a directory replaceable, so that when writing a file to this path - * new real directories will be created instead of reusing already existing - * ones mounted at a subpath of the VFS path. - * (the default behaviour is to write to the real directory associated - * with the VFS directory that was last mounted to this path (or subpath)) - **/ - VFS_MOUNT_REPLACEABLE = 16 + VFS_MOUNT_KEEP_DELETED = 8 }; // (member functions are thread-safe after the instance has been // constructed - each acquires a 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. * + * Note that the 'real directory' associated with a VFS Path + * will be relative to the highest priority subdirectory in the path, + * and that in case of equal priority, the order is _undefined_, + * and will depend on the exact order of populate() calls. + * * if files with archive extensions are seen, their contents are added * as well. **/ virtual Status Mount(const VfsPath& mountPoint, const OsPath& path, size_t flags = 0, size_t priority = 0) = 0; /** * Retrieve information about a file (similar to POSIX stat). * * @param pathname * @param pfileInfo receives information about the file. Passing NULL * suppresses warnings if the file doesn't exist. * * @return Status. **/ virtual Status GetFileInfo(const VfsPath& pathname, CFileInfo* pfileInfo) const = 0; /** * Retrieve mount priority for a file. * * @param pathname * @param ppriority receives priority value, if the file can be found. * * @return Status. **/ virtual Status GetFilePriority(const VfsPath& pathname, size_t* ppriority) const = 0; /** * Retrieve lists of all files and subdirectories in a directory. * * @return Status. * * Rationale: * - this interface avoids having to lock the directory while an * iterator is extant. * - we cannot efficiently provide routines for returning files and * subdirectories separately due to the underlying POSIX interface. **/ virtual Status GetDirectoryEntries(const VfsPath& path, CFileInfos* fileInfos, DirectoryNames* subdirectoryNames) const = 0; /** * Create a file with the given contents. * @param pathname * @param fileContents * @param size [bytes] of the contents, will match that of the file. * @return Status. **/ virtual Status CreateFile(const VfsPath& pathname, const shared_ptr& fileContents, size_t size) = 0; /** - * Replace a file with the given contents. - * - * @see CreateFile - * - * Used to replace a file if it is already present (even if the file is not - * in the attached vfs directory). Calls CreateFile if the file doesn't yet - * exist. - **/ - virtual Status ReplaceFile(const VfsPath& pathname, const shared_ptr& fileContents, size_t size) = 0; - - /** * Read an entire file into memory. * * @param pathname * @param fileContents receives a smart pointer to the contents. * @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. + * Retrieve the POSIX pathname a VFS file was loaded from. + * This is distinct from the current 'real' path, since that depends on the parent directory's real path, + * which may have been overwritten by a mod or another call to Mount(). * - * this is useful for passing paths to external libraries. + * This is used by the caching to split by mod, and you also ought to call this to delete a file. + * (note that deleting has other issues, see below). **/ - virtual Status GetRealPath(const VfsPath& pathname, OsPath& realPathname) = 0; + virtual Status GetOriginalPath(const VfsPath& filename, OsPath& loadedPathname) = 0; /** - * retrieve the real (POSIX) pathname underlying a VFS directory. - * - * this is useful for passing paths to external libraries. + * Retrieve the real (POSIX) pathname underlying a VFS file. + * This is useful for passing paths to external libraries. + * Note that this returns the real path relative to the highest priority VFS subpath. + * @param createMissingDirectories - if true, create subdirectories on the file system as required. + * (this defaults to true because, in general, this function is then used to create new files). + **/ + virtual Status GetRealPath(const VfsPath& pathname, OsPath& realPathname, bool createMissingDirectories = true) = 0; + + /** + * Retrieve the real (POSIX) pathname underlying a VFS directory. + * This is useful for passing paths to external libraries. + * Note that this returns the real path relative to the highest priority VFS subpath. + * @param createMissingDirectories - if true, create subdirectories on the file system as required. + * (this defaults to true because, in general, this function is then used to create new files). **/ - virtual Status GetDirectoryRealPath(const VfsPath& pathname, OsPath& realPathname) = 0; + virtual Status GetDirectoryRealPath(const VfsPath& pathname, OsPath& realPathname, bool createMissingDirectories = true) = 0; /** * retrieve the VFS pathname that corresponds to a real file. * * this is useful for reacting to file change notifications. * * the current implementation requires time proportional to the * number of directories; this could be accelerated by only checking * directories below a mount point with a matching real path. **/ virtual Status GetVirtualPath(const OsPath& realPathname, VfsPath& pathname) = 0; /** * remove file from the virtual directory listing. **/ virtual Status RemoveFile(const VfsPath& pathname) = 0; /** * request the directory be re-populated when it is next accessed. * useful for synchronizing with the underlying filesystem after * files have been created or their metadata changed. **/ virtual Status RepopulateDirectory(const VfsPath& path) = 0; /** * empty the contents of the filesystem. * this is typically only necessary when changing the set of * mounted directories, e.g. when switching mods. * NB: open files are not affected. **/ virtual void Clear() = 0; }; typedef shared_ptr PIVFS; /** * create an instance of a Virtual File System. * * 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(); #endif // #ifndef INCLUDED_VFS Index: ps/trunk/source/lib/file/vfs/vfs_lookup.cpp =================================================================== --- ps/trunk/source/lib/file/vfs/vfs_lookup.cpp (revision 25106) +++ ps/trunk/source/lib/file/vfs/vfs_lookup.cpp (revision 25107) @@ -1,149 +1,162 @@ -/* Copyright (C) 2013 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* * look up directories/files by traversing path components. */ #include "precompiled.h" #include "lib/file/vfs/vfs_lookup.h" #include "lib/external_libraries/suppress_boost_warnings.h" #include "lib/sysdep/filesystem.h" #include "lib/file/file.h" #include "lib/file/vfs/vfs.h" // error codes #include "lib/file/vfs/vfs_tree.h" #include "lib/file/vfs/vfs_populate.h" #include "lib/timer.h" static Status CreateDirectory(const OsPath& path) { { const mode_t mode = S_IRWXU; // 0700 as prescribed by XDG basedir const int ret = wmkdir(path, mode); if(ret == 0) // success return INFO::OK; } - // failed because the directory already exists. this can happen - // when the first vfs_Lookup has addMissingDirectories && - // !createMissingDirectories, and the directory is subsequently - // created. return 'success' to attach the existing directory.. + // Failed because the directory already exists. + // Return 'success' to attach the existing directory. if(errno == EEXIST) { - // but first ensure it's really a directory (otherwise, a - // file is "in the way" and needs to be deleted) + // But first ensure it's really a directory + // (otherwise, a file is "in the way" and needs to be deleted). struct stat s; const int ret = wstat(path, &s); ENSURE(ret == 0); // (wmkdir said it existed) ENSURE(S_ISDIR(s.st_mode)); return INFO::OK; } if (errno == EACCES) return ERR::FILE_ACCESS; // unexpected failure debug_printf("wmkdir failed with errno=%d\n", errno); DEBUG_WARN_ERR(ERR::LOGIC); WARN_RETURN(StatusFromErrno()); } Status vfs_Lookup(const VfsPath& pathname, VfsDirectory* startDirectory, VfsDirectory*& directory, VfsFile** pfile, size_t flags) { // extract and validate flags (ensure no unknown bits are set) const bool addMissingDirectories = (flags & VFS_LOOKUP_ADD) != 0; - const bool createMissingDirectories = (flags & VFS_LOOKUP_CREATE) != 0; const bool skipPopulate = (flags & VFS_LOOKUP_SKIP_POPULATE) != 0; - const bool createAlways = (flags & VFS_LOOKUP_CREATE_ALWAYS) != 0; - ENSURE((flags & ~(VFS_LOOKUP_ADD|VFS_LOOKUP_CREATE|VFS_LOOKUP_SKIP_POPULATE|VFS_LOOKUP_CREATE_ALWAYS)) == 0); + const bool realPath = (flags & VFS_LOOKUP_REAL_PATH) != 0; + ENSURE((flags & ~(VFS_LOOKUP_ADD|VFS_LOOKUP_SKIP_POPULATE|VFS_LOOKUP_REAL_PATH)) == 0); directory = startDirectory; - if(pfile) + if (pfile) *pfile = 0; - if(!skipPopulate) + if (!skipPopulate) RETURN_STATUS_IF_ERR(vfs_Populate(directory)); // early-out for pathname == "" when mounting into VFS root - if(pathname.empty()) // (prevent iterator error in loop end condition) + if (pathname.empty()) // (prevent iterator error in loop end condition) { - if(pfile) // preserve a guarantee that if pfile then we either return an error or set *pfile + // Preserve a guarantee that if pfile then we either return an error or set *pfile, + // and if looking for a real path ensure an associated directory. + if (pfile || (realPath && !directory->AssociatedDirectory())) return ERR::VFS_FILE_NOT_FOUND; else return INFO::OK; } // for each directory component: size_t pos = 0; // (needed outside of loop) for(;;) { const size_t nextSlash = pathname.string().find_first_of('/', pos); - if(nextSlash == VfsPath::String::npos) + if (nextSlash == VfsPath::String::npos) break; const VfsPath subdirectoryName = pathname.string().substr(pos, nextSlash-pos); pos = nextSlash+1; VfsDirectory* subdirectory = directory->GetSubdirectory(subdirectoryName); - if(!subdirectory) + if (!subdirectory) { - if(addMissingDirectories) + if (addMissingDirectories) subdirectory = directory->AddSubdirectory(subdirectoryName); else return ERR::VFS_DIR_NOT_FOUND; // NOWARN } - - if(createMissingDirectories && (!subdirectory->AssociatedDirectory() - || (createAlways && (subdirectory->AssociatedDirectory()->Flags() & VFS_MOUNT_REPLACEABLE) != 0))) + // When looking for a real path, we need to keep the path of the highest priority subdirectory. + // If the current directory has an associated directory, and the subdir does not / is lower priority, + // we will overwrite it. + PRealDirectory realDir = directory->AssociatedDirectory(); + if (realPath && realDir && + (!subdirectory->AssociatedDirectory() || + realDir->Priority() > subdirectory->AssociatedDirectory()->Priority())) { - OsPath currentPath; - if(directory->AssociatedDirectory()) // (is NULL when mounting into root) - currentPath = directory->AssociatedDirectory()->Path(); - currentPath = currentPath / subdirectoryName; - - RETURN_STATUS_IF_ERR(CreateDirectory(currentPath)); + OsPath currentPath = directory->AssociatedDirectory()->Path(); + currentPath = currentPath / subdirectoryName / ""; - PRealDirectory realDirectory(new RealDirectory(currentPath, 0, 0)); + // Only actually create the directory if we're in LOOKUP_ADD mode. + if (addMissingDirectories) + RETURN_STATUS_IF_ERR(CreateDirectory(currentPath)); + else if (!DirectoryExists(currentPath)) + return ERR::VFS_DIR_NOT_FOUND; + + // Propagate priority and flags to the subdirectory. + // If it already existed, it will be replaced & the memory freed. + PRealDirectory realDirectory(new RealDirectory(currentPath, + realDir ? realDir->Priority() : 0, + realDir ? realDir->Flags() : 0) + ); RETURN_STATUS_IF_ERR(vfs_Attach(subdirectory, realDirectory)); } - if(!skipPopulate) + if (!skipPopulate) RETURN_STATUS_IF_ERR(vfs_Populate(subdirectory)); directory = subdirectory; } - if(pfile) + if (realPath && !directory->AssociatedDirectory()) + return ERR::VFS_DIR_NOT_FOUND; + + if (pfile) { ENSURE(!pathname.IsDirectory()); const VfsPath filename = pathname.string().substr(pos); *pfile = directory->GetFile(filename); - if(!*pfile) + if (!*pfile) return ERR::VFS_FILE_NOT_FOUND; // NOWARN } return INFO::OK; } Index: ps/trunk/source/lib/file/vfs/vfs_lookup.h =================================================================== --- ps/trunk/source/lib/file/vfs/vfs_lookup.h (revision 25106) +++ ps/trunk/source/lib/file/vfs/vfs_lookup.h (revision 25107) @@ -1,75 +1,75 @@ -/* Copyright (C) 2013 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* * look up directories/files by traversing path components. */ #ifndef INCLUDED_VFS_LOOKUP #define INCLUDED_VFS_LOOKUP #include "lib/file/vfs/vfs_path.h" class VfsFile; class VfsDirectory; // note: VfsDirectory pointers are non-const because they may be // populated during the lookup. enum VfsLookupFlags { - // add (if they do not already exist) subdirectory components + // Add (if they do not already exist) subdirectory components // encountered in the path[name]. + // If subdirectores do not exist on disk, they will be created. VFS_LOOKUP_ADD = 1, - // if VFS directories encountered are not already associated - // with a real directory, do so (creating the directories - // if they do not already exist). - VFS_LOOKUP_CREATE = 2, - - // don't populate the directories encountered. this makes sense + // Don't populate the directories encountered. This makes sense // when adding files from an archive, which would otherwise // cause nearly every directory to be populated. - VFS_LOOKUP_SKIP_POPULATE = 4, + VFS_LOOKUP_SKIP_POPULATE = 2, - // even create directories if they are already present, this is - // useful to write new files to the directory that was attached - // last, if the directory wasn't mounted with VFS_MOUNT_REPLACEABLE - VFS_LOOKUP_CREATE_ALWAYS = 8 + // Perform a 'real path' lookup. + // Because the VFS maps multiple 'disk paths' to a single tree of paths, + // the 'real directory' of a VFS directory at any given time may be almost anything, + // in particular not its real parent directory on disk. + // To make writing predictable, we'll return a path relative to the 'disk path' of the + // highest priority subdirectory found in the lookup path. + // See test_vfs_real_paths.h for examples of this behaviour. + VFS_LOOKUP_REAL_PATH = 4 }; /** * Resolve a pathname. * * @param pathname * @param startDirectory VfsStartDirectory. * @param directory is set to the last directory component that is encountered. * @param pfile File is set to 0 if there is no name component, otherwise the * corresponding file. * @param flags @see VfsLookupFlags. * @return Status (INFO::OK if all components in pathname exist). * * to allow noiseless file-existence queries, this does not raise warnings. **/ extern Status vfs_Lookup(const VfsPath& pathname, VfsDirectory* startDirectory, VfsDirectory*& directory, VfsFile** pfile, size_t flags = 0); #endif // #ifndef INCLUDED_VFS_LOOKUP Index: ps/trunk/source/lib/file/vfs/vfs_populate.cpp =================================================================== --- ps/trunk/source/lib/file/vfs/vfs_populate.cpp (revision 25106) +++ ps/trunk/source/lib/file/vfs/vfs_populate.cpp (revision 25107) @@ -1,174 +1,196 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 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. */ /* * populate VFS directories with files */ #include "precompiled.h" #include "lib/file/vfs/vfs_populate.h" #include "lib/file/archive/archive_zip.h" #include "lib/file/vfs/vfs_tree.h" #include "lib/file/vfs/vfs_lookup.h" #include "lib/file/vfs/vfs.h" // error codes struct CompareFileInfoByName { bool operator()(const CFileInfo& a, const CFileInfo& b) { return a.Name() < b.Name(); } }; // helper class that allows breaking up the logic into sub-functions without // always having to pass directory/realDirectory as parameters. class PopulateHelper { NONCOPYABLE(PopulateHelper); public: PopulateHelper(VfsDirectory* directory, const PRealDirectory& realDirectory) : m_directory(directory), m_realDirectory(realDirectory) { } Status AddEntries() const { CFileInfos files; files.reserve(500); DirectoryNames subdirectoryNames; subdirectoryNames.reserve(50); RETURN_STATUS_IF_ERR(GetDirectoryEntries(m_realDirectory->Path(), &files, &subdirectoryNames)); // Since .DELETED files only remove files in lower priority mods // loose files and archive files have no conflicts so we do not need // to sort them. // We add directories after they might have been removed by .DELETED // files (as they did not contain any files at that point). The order // of GetDirectoryEntries is undefined, but that does not really matter (TODO really?) // so we do not need to sort its output. RETURN_STATUS_IF_ERR(AddFiles(files)); AddSubdirectories(subdirectoryNames); return INFO::OK; } private: void AddFile(const CFileInfo& fileInfo) const { const VfsPath name = fileInfo.Name(); const VfsFile file(name, (size_t)fileInfo.Size(), fileInfo.MTime(), m_realDirectory->Priority(), m_realDirectory); if(name.Extension() == L".DELETED") { m_directory->DeleteSubtree(file); if(!(m_realDirectory->Flags() & VFS_MOUNT_KEEP_DELETED)) return; } m_directory->AddFile(file); } static void AddArchiveFile(const VfsPath& pathname, const CFileInfo& fileInfo, PIArchiveFile archiveFile, uintptr_t cbData) { PopulateHelper* this_ = (PopulateHelper*)cbData; // (we have to create missing subdirectoryNames because archivers // don't always place directory entries before their files) const size_t flags = VFS_LOOKUP_ADD|VFS_LOOKUP_SKIP_POPULATE; VfsDirectory* directory; WARN_IF_ERR(vfs_Lookup(pathname, this_->m_directory, directory, 0, flags)); const VfsPath name = fileInfo.Name(); const VfsFile file(name, (size_t)fileInfo.Size(), fileInfo.MTime(), this_->m_realDirectory->Priority(), archiveFile); if(name.Extension() == L".DELETED") { directory->DeleteSubtree(file); if(!(this_->m_realDirectory->Flags() & VFS_MOUNT_KEEP_DELETED)) return; } directory->AddFile(file); } Status AddFiles(const CFileInfos& files) const { const OsPath path(m_realDirectory->Path()); for(size_t i = 0; i < files.size(); i++) { const OsPath pathname = path / files[i].Name(); if(pathname.Extension() == L".zip") { PIArchiveReader archiveReader = CreateArchiveReader_Zip(pathname); // archiveReader == nullptr if file could not be opened (e.g. because // archive is currently open in another program) if(archiveReader) RETURN_STATUS_IF_ERR(archiveReader->ReadEntries(AddArchiveFile, (uintptr_t)this)); } else // regular (non-archive) file AddFile(files[i]); } return INFO::OK; } void AddSubdirectories(const DirectoryNames& subdirectoryNames) const { for(size_t i = 0; i < subdirectoryNames.size(); i++) { // skip version control directories - this avoids cluttering the // VFS with hundreds of irrelevant files. if(subdirectoryNames[i] == L".svn" || subdirectoryNames[i] == L".git") continue; VfsDirectory* subdirectory = m_directory->AddSubdirectory(subdirectoryNames[i]); PRealDirectory realDirectory = CreateRealSubdirectory(m_realDirectory, subdirectoryNames[i]); vfs_Attach(subdirectory, realDirectory); } } VfsDirectory* const m_directory; PRealDirectory m_realDirectory; }; Status vfs_Populate(VfsDirectory* directory) { if(!directory->ShouldPopulate()) return INFO::OK; const PRealDirectory& realDirectory = directory->AssociatedDirectory(); if(realDirectory->Flags() & VFS_MOUNT_WATCH) realDirectory->Watch(); PopulateHelper helper(directory, realDirectory); RETURN_STATUS_IF_ERR(helper.AddEntries()); return INFO::OK; } Status vfs_Attach(VfsDirectory* directory, const PRealDirectory& realDirectory) { - RETURN_STATUS_IF_ERR(vfs_Populate(directory)); + PRealDirectory existingRealDir = directory->AssociatedDirectory(); + + // Don't allow replacing the real directory by a lower-priority one. + if (!existingRealDir || existingRealDir->Priority() < realDirectory->Priority()) + { + // This ordering is peculiar but useful, as it "defers" the population call. + // If there is already a real directory, we will replace it (and lose track of it), + // so we'll populate it right away, but the 'new' real directory can wait until we access it. + RETURN_STATUS_IF_ERR(vfs_Populate(directory)); + directory->SetAssociatedDirectory(realDirectory); + return INFO::OK; + } + // We are attaching a lower-priority real directory. + // Because of deferred population, we need to immediately populate this new directory. + bool shouldPop = directory->ShouldPopulate(); + // This sets "should populate" to true, so the vfs_Populate call below immediately populates. directory->SetAssociatedDirectory(realDirectory); + RETURN_STATUS_IF_ERR(vfs_Populate(directory)); + // Reset to the higher priority realDirectory, which resets ShouldPopulate to true. + directory->SetAssociatedDirectory(existingRealDir); + // Avoid un-necessary repopulation by clearing the flag. + if (!shouldPop) + directory->ShouldPopulate(); + return INFO::OK; } Index: ps/trunk/source/main.cpp =================================================================== --- ps/trunk/source/main.cpp (revision 25106) +++ ps/trunk/source/main.cpp (revision 25107) @@ -1,749 +1,750 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ /* This module drives the game when running without Atlas (our integrated map editor). It receives input and OS messages via SDL and feeds them into the input dispatcher, where they are passed on to the game GUI and simulation. It also contains main(), which either runs the above controller or that of Atlas depending on commandline parameters. */ // not for any PCH effort, but instead for the (common) definitions // included there. #define MINIMAL_PCH 2 #include "lib/precompiled.h" #include "lib/debug.h" #include "lib/status.h" #include "lib/secure_crt.h" #include "lib/frequency_filter.h" #include "lib/input.h" #include "lib/ogl.h" #include "lib/timer.h" #include "lib/external_libraries/libsdl.h" #include "ps/ArchiveBuilder.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/Globals.h" #include "ps/Hotkey.h" #include "ps/Loader.h" #include "ps/ModInstaller.h" #include "ps/Profile.h" #include "ps/Profiler2.h" #include "ps/Pyrogenesis.h" #include "ps/Replay.h" #include "ps/TouchInput.h" #include "ps/UserReport.h" #include "ps/Util.h" #include "ps/VideoMode.h" #include "ps/World.h" #include "ps/GameSetup/GameSetup.h" #include "ps/GameSetup/Atlas.h" #include "ps/GameSetup/Config.h" #include "ps/GameSetup/CmdLineArgs.h" #include "ps/GameSetup/Paths.h" #include "ps/XML/Xeromyces.h" #include "network/NetClient.h" #include "network/NetServer.h" #include "network/NetSession.h" #include "lobby/IXmppClient.h" #include "graphics/Camera.h" #include "graphics/GameView.h" #include "graphics/TextureManager.h" #include "gui/GUIManager.h" #include "renderer/Renderer.h" #include "rlinterface/RLInterface.h" #include "scriptinterface/ScriptEngine.h" #include "simulation2/Simulation2.h" #include "simulation2/system/TurnManager.h" #include "soundmanager/ISoundManager.h" #if OS_UNIX #include #include // geteuid #endif // OS_UNIX #if OS_MACOSX #include "lib/sysdep/os/osx/osx_atlas.h" #endif #if MSC_VERSION #include #define getpid _getpid // Use the non-deprecated function name #endif #include extern CmdLineArgs g_args; extern CStrW g_UniqueLogPostfix; // Marks terrain as modified so the minimap can repaint (is there a cleaner way of handling this?) bool g_GameRestarted = false; // Determines the lifetime of the mainloop enum ShutdownType { // The application shall continue the main loop. None, // The process shall terminate as soon as possible. Quit, // The engine should be restarted in the same process, for instance to activate different mods. Restart, // Atlas should be started in the same process. RestartAsAtlas }; static ShutdownType g_Shutdown = ShutdownType::None; // to avoid redundant and/or recursive resizing, we save the new // size after VIDEORESIZE messages and only update the video mode // once per frame. // these values are the latest resize message, and reset to 0 once we've // updated the video mode static int g_ResizedW; static int g_ResizedH; static std::chrono::high_resolution_clock::time_point lastFrameTime; bool IsQuitRequested() { return g_Shutdown == ShutdownType::Quit; } void QuitEngine() { g_Shutdown = ShutdownType::Quit; } void RestartEngine() { g_Shutdown = ShutdownType::Restart; } void StartAtlas() { g_Shutdown = ShutdownType::RestartAsAtlas; } // main app message handler static InReaction MainInputHandler(const SDL_Event_* ev) { switch(ev->ev.type) { case SDL_WINDOWEVENT: switch(ev->ev.window.event) { case SDL_WINDOWEVENT_ENTER: RenderCursor(true); break; case SDL_WINDOWEVENT_LEAVE: RenderCursor(false); break; case SDL_WINDOWEVENT_RESIZED: g_ResizedW = ev->ev.window.data1; g_ResizedH = ev->ev.window.data2; break; case SDL_WINDOWEVENT_MOVED: g_VideoMode.UpdatePosition(ev->ev.window.data1, ev->ev.window.data2); } break; case SDL_QUIT: QuitEngine(); break; case SDL_DROPFILE: { char* dropped_filedir = ev->ev.drop.file; const Paths paths(g_args); CModInstaller installer(paths.UserData() / "mods", paths.Cache()); installer.Install(std::string(dropped_filedir), g_ScriptContext, true); SDL_free(dropped_filedir); if (installer.GetInstalledMods().empty()) LOGERROR("Failed to install mod %s", dropped_filedir); else { LOGMESSAGE("Installed mod %s", installer.GetInstalledMods().front()); RestartEngine(); } break; } case SDL_HOTKEYPRESS: std::string hotkey = static_cast(ev->ev.user.data1); if (hotkey == "exit") { QuitEngine(); return IN_HANDLED; } else if (hotkey == "screenshot") { WriteScreenshot(L".png"); return IN_HANDLED; } else if (hotkey == "bigscreenshot") { int tiles = 4, tileWidth = 256, tileHeight = 256; CFG_GET_VAL("screenshot.tiles", tiles); CFG_GET_VAL("screenshot.tilewidth", tileWidth); CFG_GET_VAL("screenshot.tileheight", tileHeight); if (tiles > 0 && tileWidth > 0 && tileHeight > 0) WriteBigScreenshot(L".bmp", tiles, tileWidth, tileHeight); else LOGWARNING("Invalid big screenshot size: tiles=%d tileWidth=%d tileHeight=%d", tiles, tileWidth, tileHeight); return IN_HANDLED; } else if (hotkey == "togglefullscreen") { g_VideoMode.ToggleFullscreen(); return IN_HANDLED; } else if (hotkey == "profile2.toggle") { g_Profiler2.Toggle(); return IN_HANDLED; } break; } return IN_PASS; } // dispatch all pending events to the various receivers. static void PumpEvents() { ScriptRequest rq(g_GUI->GetScriptInterface()); PROFILE3("dispatch events"); SDL_Event_ ev; while (in_poll_event(&ev)) { PROFILE2("event"); if (g_GUI) { JS::RootedValue tmpVal(rq.cx); ScriptInterface::ToJSVal(rq, &tmpVal, ev); std::string data = g_GUI->GetScriptInterface()->StringifyJSON(&tmpVal); PROFILE2_ATTR("%s", data.c_str()); } in_dispatch_event(&ev); } g_TouchInput.Frame(); } /** * Optionally throttle the render frequency in order to * prevent 100% workload of the currently used CPU core. */ inline static void LimitFPS() { if (g_VSync) return; double fpsLimit = 0.0; CFG_GET_VAL(g_Game && g_Game->IsGameStarted() ? "adaptivefps.session" : "adaptivefps.menu", fpsLimit); // Keep in sync with options.json if (fpsLimit < 20.0 || fpsLimit >= 100.0) return; double wait = 1000.0 / fpsLimit - std::chrono::duration_cast( std::chrono::high_resolution_clock::now() - lastFrameTime).count() / 1000.0; if (wait > 0.0) SDL_Delay(wait); lastFrameTime = std::chrono::high_resolution_clock::now(); } static int ProgressiveLoad() { PROFILE3("progressive load"); wchar_t description[100]; int progress_percent; try { Status ret = LDR_ProgressiveLoad(10e-3, description, ARRAY_SIZE(description), &progress_percent); switch(ret) { // no load active => no-op (skip code below) case INFO::OK: return 0; // current task didn't complete. we only care about this insofar as the // load process is therefore not yet finished. case ERR::TIMED_OUT: break; // just finished loading case INFO::ALL_COMPLETE: g_Game->ReallyStartGame(); wcscpy_s(description, ARRAY_SIZE(description), L"Game is starting.."); // LDR_ProgressiveLoad returns L""; set to valid text to // avoid problems in converting to JSString break; // error! default: WARN_RETURN_STATUS_IF_ERR(ret); // can't do this above due to legit ERR::TIMED_OUT break; } } catch (PSERROR_Game_World_MapLoadFailed& e) { // Map loading failed // Call script function to do the actual work // (delete game data, switch GUI page, show error, etc.) CancelLoad(CStr(e.what()).FromUTF8()); } GUI_DisplayLoadProgress(progress_percent, description); return 0; } static void RendererIncrementalLoad() { PROFILE3("renderer incremental load"); const double maxTime = 0.1f; double startTime = timer_Time(); bool more; do { more = g_Renderer.GetTextureManager().MakeProgress(); } while (more && timer_Time() - startTime < maxTime); } static void Frame() { g_Profiler2.RecordFrameStart(); PROFILE2("frame"); g_Profiler2.IncrementFrameNumber(); PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber()); ogl_WarnIfError(); // get elapsed time const double time = timer_Time(); g_frequencyFilter->Update(time); // .. old method - "exact" but contains jumps #if 0 static double last_time; const double time = timer_Time(); const float TimeSinceLastFrame = (float)(time-last_time); last_time = time; ONCE(return); // first call: set last_time and return // .. new method - filtered and more smooth, but errors may accumulate #else const float realTimeSinceLastFrame = 1.0 / g_frequencyFilter->SmoothedFrequency(); #endif ENSURE(realTimeSinceLastFrame > 0.0f); // Decide if update is necessary bool need_update = true; // If we are not running a multiplayer game, disable updates when the game is // minimized or out of focus and relinquish the CPU a bit, in order to make // debugging easier. if (g_PauseOnFocusLoss && !g_NetClient && !g_app_has_focus) { PROFILE3("non-focus delay"); need_update = false; // don't use SDL_WaitEvent: don't want the main loop to freeze until app focus is restored SDL_Delay(10); } // this scans for changed files/directories and reloads them, thus // allowing hotloading (changes are immediately assimilated in-game). ReloadChangedFiles(); ProgressiveLoad(); RendererIncrementalLoad(); PumpEvents(); // if the user quit by closing the window, the GL context will be broken and // may crash when we call Render() on some drivers, so leave this loop // before rendering if (g_Shutdown != ShutdownType::None) return; // respond to pumped resize events if (g_ResizedW || g_ResizedH) { g_VideoMode.ResizeWindow(g_ResizedW, g_ResizedH); g_ResizedW = g_ResizedH = 0; } if (g_NetClient) g_NetClient->Poll(); ogl_WarnIfError(); g_GUI->TickObjects(); ogl_WarnIfError(); if (g_RLInterface) g_RLInterface->TryApplyMessage(); if (g_Game && g_Game->IsGameStarted() && need_update) { if (!g_RLInterface) g_Game->Update(realTimeSinceLastFrame); g_Game->GetView()->Update(float(realTimeSinceLastFrame)); } // Keep us connected to any XMPP servers if (g_XmppClient) g_XmppClient->recv(); g_UserReporter.Update(); g_Console->Update(realTimeSinceLastFrame); ogl_WarnIfError(); if (g_SoundManager) g_SoundManager->IdleTask(); if (ShouldRender()) { Render(); { PROFILE3("swap buffers"); SDL_GL_SwapWindow(g_VideoMode.GetWindow()); ogl_WarnIfError(); } g_Renderer.OnSwapBuffers(); } g_Profiler.Frame(); g_GameRestarted = false; LimitFPS(); } static void NonVisualFrame() { g_Profiler2.RecordFrameStart(); PROFILE2("frame"); g_Profiler2.IncrementFrameNumber(); PROFILE2_ATTR("%d", g_Profiler2.GetFrameNumber()); static u32 turn = 0; debug_printf("Turn %u (%u)...\n", turn++, DEFAULT_TURN_LENGTH); g_Game->GetSimulation2()->Update(DEFAULT_TURN_LENGTH); g_Profiler.Frame(); if (g_Game->IsGameFinished()) QuitEngine(); } static void MainControllerInit() { // add additional input handlers only needed by this controller: // must be registered after gui_handler. Should mayhap even be last. in_add_handler(MainInputHandler); } static void MainControllerShutdown() { in_reset_handlers(); } static void StartRLInterface(CmdLineArgs args) { std::string server_address; CFG_GET_VAL("rlinterface.address", server_address); if (!args.Get("rl-interface").empty()) server_address = args.Get("rl-interface"); g_RLInterface = std::make_unique(server_address.c_str()); debug_printf("RL interface listening on %s\n", server_address.c_str()); } // moved into a helper function to ensure args is destroyed before // exit(), which may result in a memory leak. static void RunGameOrAtlas(int argc, const char* argv[]) { CmdLineArgs args(argc, argv); g_args = args; if (args.Has("version")) { debug_printf("Pyrogenesis %s\n", engine_version); return; } if (args.Has("autostart-nonvisual") && args.Get("autostart").empty() && !args.Has("rl-interface")) { LOGERROR("-autostart-nonvisual cant be used alone. A map with -autostart=\"TYPEDIR/MAPNAME\" is needed."); return; } if (args.Has("unique-logs")) g_UniqueLogPostfix = L"_" + std::to_wstring(std::time(nullptr)) + L"_" + std::to_wstring(getpid()); const bool isVisualReplay = args.Has("replay-visual"); const bool isNonVisualReplay = args.Has("replay"); const bool isNonVisual = args.Has("autostart-nonvisual"); const bool isUsingRLInterface = args.Has("rl-interface"); const OsPath replayFile( isVisualReplay ? args.Get("replay-visual") : isNonVisualReplay ? args.Get("replay") : ""); if (isVisualReplay || isNonVisualReplay) { if (!FileExists(replayFile)) { debug_printf("ERROR: The requested replay file '%s' does not exist!\n", replayFile.string8().c_str()); return; } if (DirectoryExists(replayFile)) { debug_printf("ERROR: The requested replay file '%s' is a directory!\n", replayFile.string8().c_str()); return; } } std::vector modsToInstall; for (const CStr& arg : args.GetArgsWithoutName()) { const OsPath modPath(arg); if (!CModInstaller::IsDefaultModExtension(modPath.Extension())) { debug_printf("Skipping file '%s' which does not have a mod file extension.\n", modPath.string8().c_str()); continue; } if (!FileExists(modPath)) { debug_printf("ERROR: The mod file '%s' does not exist!\n", modPath.string8().c_str()); continue; } if (DirectoryExists(modPath)) { debug_printf("ERROR: The mod file '%s' is a directory!\n", modPath.string8().c_str()); continue; } modsToInstall.emplace_back(std::move(modPath)); } // We need to initialize SpiderMonkey and libxml2 in the main thread before // any thread uses them. So initialize them here before we might run Atlas. ScriptEngine scriptEngine; CXeromyces::Startup(); if (ATLAS_RunIfOnCmdLine(args, false)) { CXeromyces::Terminate(); return; } if (isNonVisualReplay) { if (!args.Has("mod")) { LOGERROR("At least one mod should be specified! Did you mean to add the argument '-mod=public'?"); CXeromyces::Terminate(); return; } Paths paths(args); g_VFS = CreateVfs(); - g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE); + // Mount with highest priority, we don't want mods overwriting this. + g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE, VFS_MAX_PRIORITY); MountMods(paths, GetMods(args, INIT_MODS)); { CReplayPlayer replay; replay.Load(replayFile); replay.Replay( args.Has("serializationtest"), args.Has("rejointest") ? args.Get("rejointest").ToInt() : -1, args.Has("ooslog"), !args.Has("hashtest-full") || args.Get("hashtest-full") == "true", args.Has("hashtest-quick") && args.Get("hashtest-quick") == "true"); } g_VFS.reset(); CXeromyces::Terminate(); return; } // run in archive-building mode if requested if (args.Has("archivebuild")) { Paths paths(args); OsPath mod(args.Get("archivebuild")); OsPath zip; if (args.Has("archivebuild-output")) zip = args.Get("archivebuild-output"); else zip = mod.Filename().ChangeExtension(L".zip"); CArchiveBuilder builder(mod, paths.Cache()); // Add mods provided on the command line // NOTE: We do not handle mods in the user mod path here std::vector mods = args.GetMultiple("mod"); for (size_t i = 0; i < mods.size(); ++i) builder.AddBaseMod(paths.RData()/"mods"/mods[i]); builder.Build(zip, args.Has("archivebuild-compress")); CXeromyces::Terminate(); return; } const double res = timer_Resolution(); g_frequencyFilter = CreateFrequencyFilter(res, 30.0); // run the game int flags = INIT_MODS; do { g_Shutdown = ShutdownType::None; if (!Init(args, flags)) { flags &= ~INIT_MODS; Shutdown(SHUTDOWN_FROM_CONFIG); continue; } std::vector installedMods; if (!modsToInstall.empty()) { Paths paths(args); CModInstaller installer(paths.UserData() / "mods", paths.Cache()); // Install the mods without deleting the pyromod files for (const OsPath& modPath : modsToInstall) installer.Install(modPath, g_ScriptContext, true); installedMods = installer.GetInstalledMods(); } if (isNonVisual) { InitNonVisual(args); if (isUsingRLInterface) StartRLInterface(args); while (g_Shutdown == ShutdownType::None) { if (isUsingRLInterface) g_RLInterface->TryApplyMessage(); else NonVisualFrame(); } } else { InitGraphics(args, 0, installedMods); MainControllerInit(); if (isUsingRLInterface) StartRLInterface(args); while (g_Shutdown == ShutdownType::None) Frame(); } // Do not install mods again in case of restart (typically from the mod selector) modsToInstall.clear(); Shutdown(0); MainControllerShutdown(); flags &= ~INIT_MODS; } while (g_Shutdown == ShutdownType::Restart); #if OS_MACOSX if (g_Shutdown == ShutdownType::RestartAsAtlas) startNewAtlasProcess(); #else if (g_Shutdown == ShutdownType::RestartAsAtlas) ATLAS_RunIfOnCmdLine(args, true); #endif CXeromyces::Terminate(); } #if OS_ANDROID // In Android we compile the engine as a shared library, not an executable, // so rename main() to a different symbol that the wrapper library can load #undef main #define main pyrogenesis_main extern "C" __attribute__((visibility ("default"))) int main(int argc, char* argv[]); #endif extern "C" int main(int argc, char* argv[]) { #if OS_UNIX // Don't allow people to run the game with root permissions, // because bad things can happen, check before we do anything if (geteuid() == 0) { std::cerr << "********************************************************\n" << "WARNING: Attempted to run the game with root permission!\n" << "This is not allowed because it can alter home directory \n" << "permissions and opens your system to vulnerabilities. \n" << "(You received this message because you were either \n" <<" logged in as root or used e.g. the 'sudo' command.) \n" << "********************************************************\n\n"; return EXIT_FAILURE; } #endif // OS_UNIX EarlyInit(); // must come at beginning of main RunGameOrAtlas(argc, const_cast(argv)); // Shut down profiler initialised by EarlyInit g_Profiler2.Shutdown(); return EXIT_SUCCESS; } Index: ps/trunk/source/network/tests/test_Net.h =================================================================== --- ps/trunk/source/network/tests/test_Net.h (revision 25106) +++ ps/trunk/source/network/tests/test_Net.h (revision 25107) @@ -1,384 +1,384 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "lib/self_test.h" #include "graphics/TerrainTextureManager.h" #include "lib/external_libraries/enet.h" #include "lib/external_libraries/libsdl.h" #include "lib/tex/tex.h" #include "network/NetServer.h" #include "network/NetClient.h" #include "network/NetMessage.h" #include "network/NetMessages.h" #include "ps/CLogger.h" #include "ps/Game.h" #include "ps/Filesystem.h" #include "ps/Loader.h" #include "ps/XML/Xeromyces.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" #include "simulation2/system/TurnManager.h" class TestNetComms : public CxxTest::TestSuite { public: void setUp() { g_VFS = CreateVfs(); - TS_ASSERT_OK(g_VFS->Mount(L"", DataDir()/"mods"/"public", VFS_MOUNT_MUST_EXIST)); - TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir()/"_testcache")); + TS_ASSERT_OK(g_VFS->Mount(L"", DataDir() / "mods" / "public" / "", VFS_MOUNT_MUST_EXIST)); + TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir() / "_testcache" / "", 0, VFS_MAX_PRIORITY)); CXeromyces::Startup(); // Need some stuff for terrain movement costs: // (TODO: this ought to be independent of any graphics code) new CTerrainTextureManager; g_TexMan.LoadTerrainTextures(); enet_initialize(); } void tearDown() { enet_deinitialize(); delete &g_TexMan; CXeromyces::Terminate(); g_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); } bool clients_are_all(const std::vector& clients, uint state) { for (size_t j = 0; j < clients.size(); ++j) if (clients[j]->GetCurrState() != state) return false; return true; } void connect(CNetServer& server, const std::vector& clients) { TS_ASSERT(server.SetupConnection(PS_DEFAULT_PORT)); for (CNetClient* client: clients) { client->SetupServerData("127.0.0.1", PS_DEFAULT_PORT, false); TS_ASSERT(client->SetupConnection(nullptr)); } for (size_t i = 0; ; ++i) { // debug_printf("."); for (size_t j = 0; j < clients.size(); ++j) clients[j]->Poll(); if (clients_are_all(clients, NCS_PREGAME)) break; if (i > 20) { TS_FAIL("connection timeout"); break; } SDL_Delay(100); } } #if 0 void disconnect(CNetServer& server, const std::vector& clients) { for (size_t i = 0; ; ++i) { // debug_printf("."); server.Poll(); for (size_t j = 0; j < clients.size(); ++j) clients[j]->Poll(); if (server.GetState() == SERVER_STATE_UNCONNECTED && clients_are_all(clients, NCS_UNCONNECTED)) break; if (i > 20) { TS_FAIL("disconnection timeout"); break; } SDL_Delay(100); } } #endif void wait(const std::vector& clients, size_t msecs) { for (size_t i = 0; i < msecs/10; ++i) { for (size_t j = 0; j < clients.size(); ++j) clients[j]->Poll(); SDL_Delay(10); } } void test_basic_DISABLED() { // This doesn't actually test much, it just runs a very quick multiplayer game // and prints a load of debug output so you can see if anything funny's going on ScriptInterface scriptInterface("Engine", "Test", g_ScriptContext); ScriptRequest rq(scriptInterface); TestStdoutLogger logger; std::vector clients; CGame client1Game(false); CGame client2Game(false); CGame client3Game(false); CNetServer server("no_secret"); JS::RootedValue attrs(rq.cx); ScriptInterface::CreateObject( rq, &attrs, "mapType", "scenario", "map", "maps/scenarios/Saharan Oases", "mapPath", "maps/scenarios/", "thing", "example"); server.UpdateInitAttributes(&attrs, scriptInterface); CNetClient client1(&client1Game); CNetClient client2(&client2Game); CNetClient client3(&client3Game); clients.push_back(&client1); clients.push_back(&client2); clients.push_back(&client3); connect(server, clients); debug_printf("%s", client1.TestReadGuiMessages().c_str()); server.StartGame(); SDL_Delay(100); for (size_t j = 0; j < clients.size(); ++j) { clients[j]->Poll(); TS_ASSERT_OK(LDR_NonprogressiveLoad()); clients[j]->LoadFinished(); } wait(clients, 100); { JS::RootedValue cmd(rq.cx); ScriptInterface::CreateObject( rq, &cmd, "type", "debug-print", "message", "[>>> client1 test sim command]\\n"); client1Game.GetTurnManager()->PostCommand(cmd); } { JS::RootedValue cmd(rq.cx); ScriptInterface::CreateObject( rq, &cmd, "type", "debug-print", "message", "[>>> client2 test sim command]\\n"); client2Game.GetTurnManager()->PostCommand(cmd); } wait(clients, 100); client1Game.GetTurnManager()->Update(1.0f, 1); client2Game.GetTurnManager()->Update(1.0f, 1); client3Game.GetTurnManager()->Update(1.0f, 1); wait(clients, 100); client1Game.GetTurnManager()->Update(1.0f, 1); client2Game.GetTurnManager()->Update(1.0f, 1); client3Game.GetTurnManager()->Update(1.0f, 1); wait(clients, 100); } void test_rejoin_DISABLED() { ScriptInterface scriptInterface("Engine", "Test", g_ScriptContext); ScriptRequest rq(scriptInterface); TestStdoutLogger logger; std::vector clients; CGame client1Game(false); CGame client2Game(false); CGame client3Game(false); CNetServer server("no_secret"); JS::RootedValue attrs(rq.cx); ScriptInterface::CreateObject( rq, &attrs, "mapType", "scenario", "map", "maps/scenarios/Saharan Oases", "mapPath", "maps/scenarios/", "thing", "example"); server.UpdateInitAttributes(&attrs, scriptInterface); CNetClient client1(&client1Game); CNetClient client2(&client2Game); CNetClient client3(&client3Game); client1.SetUserName(L"alice"); client2.SetUserName(L"bob"); client3.SetUserName(L"charlie"); clients.push_back(&client1); clients.push_back(&client2); clients.push_back(&client3); connect(server, clients); debug_printf("%s", client1.TestReadGuiMessages().c_str()); server.StartGame(); SDL_Delay(100); for (size_t j = 0; j < clients.size(); ++j) { clients[j]->Poll(); TS_ASSERT_OK(LDR_NonprogressiveLoad()); clients[j]->LoadFinished(); } wait(clients, 100); { JS::RootedValue cmd(rq.cx); ScriptInterface::CreateObject( rq, &cmd, "type", "debug-print", "message", "[>>> client1 test sim command 1]\\n"); client1Game.GetTurnManager()->PostCommand(cmd); } wait(clients, 100); client1Game.GetTurnManager()->Update(1.0f, 1); client2Game.GetTurnManager()->Update(1.0f, 1); client3Game.GetTurnManager()->Update(1.0f, 1); wait(clients, 100); { JS::RootedValue cmd(rq.cx); ScriptInterface::CreateObject( rq, &cmd, "type", "debug-print", "message", "[>>> client1 test sim command 2]\\n"); client1Game.GetTurnManager()->PostCommand(cmd); } debug_printf("==== Disconnecting client 2\n"); client2.DestroyConnection(); clients.erase(clients.begin()+1); debug_printf("==== Connecting client 2B\n"); CGame client2BGame(false); CNetClient client2B(&client2BGame); client2B.SetUserName(L"bob"); clients.push_back(&client2B); client2B.SetupServerData("127.0.0.1", PS_DEFAULT_PORT, false); TS_ASSERT(client2B.SetupConnection(nullptr)); for (size_t i = 0; ; ++i) { debug_printf("[%u]\n", client2B.GetCurrState()); client2B.Poll(); if (client2B.GetCurrState() == NCS_PREGAME) break; if (client2B.GetCurrState() == NCS_UNCONNECTED) { TS_FAIL("connection rejected"); return; } if (i > 20) { TS_FAIL("connection timeout"); return; } SDL_Delay(100); } wait(clients, 100); client1Game.GetTurnManager()->Update(1.0f, 1); client3Game.GetTurnManager()->Update(1.0f, 1); wait(clients, 100); server.SetTurnLength(100); client1Game.GetTurnManager()->Update(1.0f, 1); client3Game.GetTurnManager()->Update(1.0f, 1); wait(clients, 100); // (This SetTurnLength thing doesn't actually detect errors unless you change // CTurnManager::TurnNeedsFullHash to always return true) { JS::RootedValue cmd(rq.cx); ScriptInterface::CreateObject( rq, &cmd, "type", "debug-print", "message", "[>>> client1 test sim command 3]\\n"); client1Game.GetTurnManager()->PostCommand(cmd); } clients[2]->Poll(); TS_ASSERT_OK(LDR_NonprogressiveLoad()); clients[2]->LoadFinished(); wait(clients, 100); { JS::RootedValue cmd(rq.cx); ScriptInterface::CreateObject( rq, &cmd, "type", "debug-print", "message", "[>>> client1 test sim command 4]\\n"); client1Game.GetTurnManager()->PostCommand(cmd); } for (size_t i = 0; i < 3; ++i) { client1Game.GetTurnManager()->Update(1.0f, 1); client2BGame.GetTurnManager()->Update(1.0f, 1); client3Game.GetTurnManager()->Update(1.0f, 1); wait(clients, 100); } } }; Index: ps/trunk/source/ps/ArchiveBuilder.cpp =================================================================== --- ps/trunk/source/ps/ArchiveBuilder.cpp (revision 25106) +++ ps/trunk/source/ps/ArchiveBuilder.cpp (revision 25107) @@ -1,183 +1,183 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ArchiveBuilder.h" #include "graphics/TextureManager.h" #include "graphics/ColladaManager.h" #include "lib/tex/tex_codec.h" #include "lib/file/archive/archive_zip.h" #include "lib/file/vfs/vfs_util.h" #include "ps/XML/Xeromyces.h" #include CArchiveBuilder::CArchiveBuilder(const OsPath& mod, const OsPath& tempdir) : m_TempDir(tempdir), m_NumBaseMods(0) { m_VFS = CreateVfs(); DeleteDirectory(m_TempDir/"_archivecache"); // clean up in case the last run failed - m_VFS->Mount(L"cache/", m_TempDir/"_archivecache"/""); + m_VFS->Mount(L"cache/", m_TempDir/"_archivecache"/"", 0, VFS_MAX_PRIORITY); // Mount with highest priority so base mods do not overwrite files in this mod - m_VFS->Mount(L"", mod/"", VFS_MOUNT_MUST_EXIST | VFS_MOUNT_KEEP_DELETED, (size_t)-1); + m_VFS->Mount(L"", mod/"", VFS_MOUNT_MUST_EXIST | VFS_MOUNT_KEEP_DELETED, VFS_MAX_PRIORITY); // Collect the list of files before loading any base mods vfs::ForEachFile(m_VFS, L"", &CollectFileCB, (uintptr_t)static_cast(this), 0, vfs::DIR_RECURSIVE); } CArchiveBuilder::~CArchiveBuilder() { m_VFS.reset(); DeleteDirectory(m_TempDir/"_archivecache"); } void CArchiveBuilder::AddBaseMod(const OsPath& mod) { // Increase priority for each additional base mod, so that the // mods are mounted in the same way as when starting the game. m_VFS->Mount(L"", mod/"", VFS_MOUNT_MUST_EXIST, ++m_NumBaseMods); } void CArchiveBuilder::Build(const OsPath& archive, bool compress) { // By default we disable zip compression because it significantly hurts download // size for releases (which re-compress all files with better compression // algorithms) - it's probably most important currently to optimise for // download size rather than install size or startup performance. // (See http://trac.wildfiregames.com/ticket/671) const bool noDeflate = !compress; PIArchiveWriter writer = CreateArchiveWriter_Zip(archive, noDeflate); if (!writer) { debug_printf("Failed to create the archive \"%s\".", archive.string8().c_str()); return; } // Use CTextureManager instead of CTextureConverter directly, // so it can deal with all the loading of settings.xml files CTextureManager textureManager(m_VFS, true, true); CColladaManager colladaManager(m_VFS); CXeromyces xero; for (const VfsPath& path : m_Files) { Status ret; OsPath realPath; ret = m_VFS->GetRealPath(path, realPath); ENSURE(ret == INFO::OK); // Compress textures and store the new cached version instead of the original if ((boost::algorithm::starts_with(path.string(), L"art/textures/") || boost::algorithm::starts_with(path.string(), L"fonts/") ) && tex_is_known_extension(path) && // Skip some subdirectories where the engine doesn't use CTextureManager yet: !boost::algorithm::starts_with(path.string(), L"art/textures/cursors/") && !boost::algorithm::starts_with(path.string(), L"art/textures/terrain/alphamaps/") ) { VfsPath cachedPath; debug_printf("Converting texture \"%s\"\n", realPath.string8().c_str()); bool ok = textureManager.GenerateCachedTexture(path, cachedPath); ENSURE(ok); OsPath cachedRealPath; ret = m_VFS->GetRealPath(VfsPath("cache")/cachedPath, cachedRealPath); ENSURE(ret == INFO::OK); writer->AddFile(cachedRealPath, cachedPath); // We don't want to store the original file too (since it's a // large waste of space), so skip to the next file continue; } // Convert DAE models and store the new cached version instead of the original if (path.Extension() == L".dae") { CColladaManager::FileType type; if (boost::algorithm::starts_with(path.string(), L"art/meshes/")) type = CColladaManager::PMD; else if (boost::algorithm::starts_with(path.string(), L"art/animation/")) type = CColladaManager::PSA; else { // Unknown type of DAE, just add to archive and continue writer->AddFile(realPath, path); continue; } VfsPath cachedPath; debug_printf("Converting model %s\n", realPath.string8().c_str()); bool ok = colladaManager.GenerateCachedFile(path, type, cachedPath); // The DAE might fail to convert for whatever reason, and in that case // it can't be used in the game, so we just exclude it // (alternatively we could throw release blocking errors on useless files) if (ok) { OsPath cachedRealPath; ret = m_VFS->GetRealPath(VfsPath("cache")/cachedPath, cachedRealPath); ENSURE(ret == INFO::OK); writer->AddFile(cachedRealPath, cachedPath); } // We don't want to store the original file too (since it's a // large waste of space), so skip to the next file continue; } debug_printf("Adding %s\n", realPath.string8().c_str()); writer->AddFile(realPath, path); // Also cache XMB versions of all XML files if (path.Extension() == L".xml") { VfsPath cachedPath; debug_printf("Converting XML file \"%s\"\n", realPath.string8().c_str()); bool ok = xero.GenerateCachedXMB(m_VFS, path, cachedPath); ENSURE(ok); OsPath cachedRealPath; ret = m_VFS->GetRealPath(VfsPath("cache")/cachedPath, cachedRealPath); ENSURE(ret == INFO::OK); writer->AddFile(cachedRealPath, cachedPath); } } debug_printf("Finished packaging \"%s\".", archive.string8().c_str()); } Status CArchiveBuilder::CollectFileCB(const VfsPath& pathname, const CFileInfo& UNUSED(fileInfo), const uintptr_t cbData) { CArchiveBuilder* self = static_cast((void*)cbData); self->m_Files.push_back(pathname); return INFO::OK; } Index: ps/trunk/source/ps/CacheLoader.cpp =================================================================== --- ps/trunk/source/ps/CacheLoader.cpp (revision 25106) +++ ps/trunk/source/ps/CacheLoader.cpp (revision 25107) @@ -1,152 +1,152 @@ -/* Copyright (C) 2018 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "CacheLoader.h" #include "maths/MD5.h" #include "ps/CLogger.h" #include "ps/Util.h" #include CCacheLoader::CCacheLoader(PIVFS vfs, const std::wstring& fileExtension) : m_VFS(vfs), m_FileExtension(fileExtension) { } Status CCacheLoader::TryLoadingCached(const VfsPath& sourcePath, const MD5& initialHash, u32 version, VfsPath& loadPath) { VfsPath archiveCachePath = ArchiveCachePath(sourcePath); // Try the archive cache file first if (CanUseArchiveCache(sourcePath, archiveCachePath)) { loadPath = archiveCachePath; return INFO::OK; } // Fail if no source or archive cache // Note: this is not always an error case, because for instance there // are some uncached .pmd/psa files in the game with no source .dae. // This test fails (correctly) in that valid situation, so it seems // best to leave the error handling to the caller. Status err = m_VFS->GetFileInfo(sourcePath, NULL); if (err < 0) { return err; } // Look for loose cache of source file VfsPath looseCachePath = LooseCachePath(sourcePath, initialHash, version); // If the loose cache file exists, use it if (m_VFS->GetFileInfo(looseCachePath, NULL) >= 0) { loadPath = looseCachePath; return INFO::OK; } // No cache - we'll need to regenerate it loadPath = looseCachePath; return INFO::SKIPPED; } bool CCacheLoader::CanUseArchiveCache(const VfsPath& sourcePath, const VfsPath& archiveCachePath) { // We want to use the archive cache whenever possible, // unless it's superseded by a source file that the user has edited size_t archiveCachePriority = 0; size_t sourcePriority = 0; bool archiveCacheExists = (m_VFS->GetFilePriority(archiveCachePath, &archiveCachePriority) >= 0); // Can't use it if there's no cache if (!archiveCacheExists) return false; bool sourceExists = (m_VFS->GetFilePriority(sourcePath, &sourcePriority) >= 0); // Must use the cache if there's no source if (!sourceExists) return true; // If source file is from a higher-priority mod than archive cache, // don't use the old cache if (archiveCachePriority < sourcePriority) return false; // If source file is more recent than the archive cache (i.e. the user has edited it), // don't use the old cache CFileInfo sourceInfo, archiveCacheInfo; if (m_VFS->GetFileInfo(sourcePath, &sourceInfo) >= 0 && m_VFS->GetFileInfo(archiveCachePath, &archiveCacheInfo) >= 0) { const double howMuchNewer = difftime(sourceInfo.MTime(), archiveCacheInfo.MTime()); const double threshold = 2.0; // FAT timestamp resolution [seconds] if (howMuchNewer > threshold) return false; } // Otherwise we can use the cache return true; } VfsPath CCacheLoader::ArchiveCachePath(const VfsPath& sourcePath) const { return sourcePath.ChangeExtension(sourcePath.Extension().string() + L".cached" + m_FileExtension); } VfsPath CCacheLoader::LooseCachePath(const VfsPath& sourcePath, const MD5& initialHash, u32 version) { CFileInfo fileInfo; if (m_VFS->GetFileInfo(sourcePath, &fileInfo) < 0) { debug_warn(L"source file disappeared"); // this should never happen return VfsPath(); } u64 mtime = (u64)fileInfo.MTime() & ~1; // skip lowest bit, since zip and FAT don't preserve it u64 size = (u64)fileInfo.Size(); // Construct a hash of the file data and settings. MD5 hash = initialHash; hash.Update((const u8*)&mtime, sizeof(mtime)); hash.Update((const u8*)&size, sizeof(size)); hash.Update((const u8*)&version, sizeof(version)); // these are local cached files, so we don't care about endianness etc u8 digest[MD5::DIGESTSIZE]; hash.Final(digest); // Get the mod path OsPath path; - m_VFS->GetRealPath(sourcePath, path); + m_VFS->GetOriginalPath(sourcePath, path); return VfsPath("cache") / path_name_only(path.BeforeCommon(sourcePath).Parent().string().c_str()) / sourcePath.ChangeExtension(sourcePath.Extension().string() + L"." + // Use a short prefix of the full hash (we don't need high collision-resistance) wstring_from_utf8(Hexify(digest, 8)) + m_FileExtension); } Index: ps/trunk/source/ps/GameSetup/GameSetup.cpp =================================================================== --- ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 25106) +++ ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 25107) @@ -1,1653 +1,1633 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "lib/app_hooks.h" #include "lib/config2.h" #include "lib/input.h" #include "lib/ogl.h" #include "lib/timer.h" #include "lib/external_libraries/libsdl.h" #include "lib/file/common/file_stats.h" #include "lib/res/h_mgr.h" #include "lib/res/graphics/cursor.h" #include "graphics/CinemaManager.h" #include "graphics/Color.h" #include "graphics/FontMetrics.h" #include "graphics/GameView.h" #include "graphics/LightEnv.h" #include "graphics/MapReader.h" #include "graphics/ModelDef.h" #include "graphics/MaterialManager.h" #include "graphics/TerrainTextureManager.h" #include "gui/CGUI.h" #include "gui/GUIManager.h" #include "i18n/L10n.h" #include "maths/MathUtil.h" #include "network/NetServer.h" #include "network/NetClient.h" #include "network/NetMessage.h" #include "network/NetMessages.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/GameSetup/Atlas.h" #include "ps/GameSetup/GameSetup.h" #include "ps/GameSetup/Paths.h" #include "ps/GameSetup/Config.h" #include "ps/GameSetup/CmdLineArgs.h" #include "ps/GameSetup/HWDetect.h" #include "ps/Globals.h" #include "ps/GUID.h" #include "ps/Hotkey.h" #include "ps/Joystick.h" #include "ps/Loader.h" #include "ps/Mod.h" #include "ps/ModIo.h" #include "ps/Profile.h" #include "ps/ProfileViewer.h" #include "ps/Profiler2.h" #include "ps/Pyrogenesis.h" // psSetLogDir #include "ps/scripting/JSInterface_Console.h" #include "ps/TouchInput.h" #include "ps/UserReport.h" #include "ps/Util.h" #include "ps/VideoMode.h" #include "ps/VisualReplay.h" #include "ps/World.h" #include "renderer/Renderer.h" #include "renderer/VertexBufferManager.h" #include "renderer/ModelRenderer.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/ScriptStats.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptConversions.h" #include "simulation2/Simulation2.h" #include "lobby/IXmppClient.h" #include "soundmanager/scripting/JSInterface_Sound.h" #include "soundmanager/ISoundManager.h" #include "tools/atlas/GameInterface/GameLoop.h" #include "tools/atlas/GameInterface/View.h" #if !(OS_WIN || OS_MACOSX || OS_ANDROID) // assume all other platforms use X11 for wxWidgets #define MUST_INIT_X11 1 #include #else #define MUST_INIT_X11 0 #endif extern void RestartEngine(); #include #include #include #include ERROR_GROUP(System); ERROR_TYPE(System, SDLInitFailed); ERROR_TYPE(System, VmodeFailed); ERROR_TYPE(System, RequiredExtensionsMissing); bool g_DoRenderGui = true; bool g_DoRenderLogger = true; bool g_DoRenderCursor = true; thread_local shared_ptr g_ScriptContext; static const int SANE_TEX_QUALITY_DEFAULT = 5; // keep in sync with code static const CStr g_EventNameGameLoadProgress = "GameLoadProgress"; bool g_InDevelopmentCopy; bool g_CheckedIfInDevelopmentCopy = false; static void SetTextureQuality(int quality) { int q_flags; GLint filter; retry: // keep this in sync with SANE_TEX_QUALITY_DEFAULT switch(quality) { // worst quality case 0: q_flags = OGL_TEX_HALF_RES|OGL_TEX_HALF_BPP; filter = GL_NEAREST; break; // [perf] add bilinear filtering case 1: q_flags = OGL_TEX_HALF_RES|OGL_TEX_HALF_BPP; filter = GL_LINEAR; break; // [vmem] no longer reduce resolution case 2: q_flags = OGL_TEX_HALF_BPP; filter = GL_LINEAR; break; // [vmem] add mipmaps case 3: q_flags = OGL_TEX_HALF_BPP; filter = GL_NEAREST_MIPMAP_LINEAR; break; // [perf] better filtering case 4: q_flags = OGL_TEX_HALF_BPP; filter = GL_LINEAR_MIPMAP_LINEAR; break; // [vmem] no longer reduce bpp case SANE_TEX_QUALITY_DEFAULT: q_flags = OGL_TEX_FULL_QUALITY; filter = GL_LINEAR_MIPMAP_LINEAR; break; // [perf] add anisotropy case 6: // TODO: add anisotropic filtering q_flags = OGL_TEX_FULL_QUALITY; filter = GL_LINEAR_MIPMAP_LINEAR; break; // invalid default: debug_warn(L"SetTextureQuality: invalid quality"); quality = SANE_TEX_QUALITY_DEFAULT; // careful: recursion doesn't work and we don't want to duplicate // the "sane" default values. goto retry; } ogl_tex_set_defaults(q_flags, filter); } //---------------------------------------------------------------------------- // GUI integration //---------------------------------------------------------------------------- // display progress / description in loading screen void GUI_DisplayLoadProgress(int percent, const wchar_t* pending_task) { const ScriptInterface& scriptInterface = *(g_GUI->GetActiveGUI()->GetScriptInterface()); ScriptRequest rq(scriptInterface); JS::RootedValueVector paramData(rq.cx); ignore_result(paramData.append(JS::NumberValue(percent))); JS::RootedValue valPendingTask(rq.cx); scriptInterface.ToJSVal(rq, &valPendingTask, pending_task); ignore_result(paramData.append(valPendingTask)); g_GUI->SendEventToAll(g_EventNameGameLoadProgress, paramData); } bool ShouldRender() { return !g_app_minimized && (g_app_has_focus || !g_VideoMode.IsInFullscreen()); } void Render() { // Do not render if not focused while in fullscreen or minimised, // as that triggers a difficult-to-reproduce crash on some graphic cards. if (!ShouldRender()) return; PROFILE3("render"); ogl_WarnIfError(); g_Profiler2.RecordGPUFrameStart(); ogl_WarnIfError(); // prepare before starting the renderer frame if (g_Game && g_Game->IsGameStarted()) g_Game->GetView()->BeginFrame(); if (g_Game) g_Renderer.SetSimulation(g_Game->GetSimulation2()); // start new frame g_Renderer.BeginFrame(); ogl_WarnIfError(); if (g_Game && g_Game->IsGameStarted()) g_Game->GetView()->Render(); ogl_WarnIfError(); g_Renderer.RenderTextOverlays(); // If we're in Atlas game view, render special tools if (g_AtlasGameLoop && g_AtlasGameLoop->view) { g_AtlasGameLoop->view->DrawCinemaPathTool(); ogl_WarnIfError(); } if (g_Game && g_Game->IsGameStarted()) g_Game->GetView()->GetCinema()->Render(); ogl_WarnIfError(); if (g_DoRenderGui) g_GUI->Draw(); ogl_WarnIfError(); // If we're in Atlas game view, render special overlays (e.g. editor bandbox) if (g_AtlasGameLoop && g_AtlasGameLoop->view) { g_AtlasGameLoop->view->DrawOverlays(); ogl_WarnIfError(); } // Text: glDisable(GL_DEPTH_TEST); g_Console->Render(); ogl_WarnIfError(); if (g_DoRenderLogger) g_Logger->Render(); ogl_WarnIfError(); // Profile information g_ProfileViewer.RenderProfile(); ogl_WarnIfError(); // Draw the cursor (or set the Windows cursor, on Windows) if (g_DoRenderCursor) { PROFILE3_GPU("cursor"); CStrW cursorName = g_CursorName; if (cursorName.empty()) { cursor_draw(g_VFS, NULL, g_mouse_x, g_yres-g_mouse_y, g_GuiScale, false); } else { bool forceGL = false; CFG_GET_VAL("nohwcursor", forceGL); #if CONFIG2_GLES #warning TODO: implement cursors for GLES #else // set up transform for GL cursor glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glLoadIdentity(); CMatrix3D transform; transform.SetOrtho(0.f, (float)g_xres, 0.f, (float)g_yres, -1.f, 1000.f); glLoadMatrixf(&transform._11); #endif #if OS_ANDROID #warning TODO: cursors for Android #else if (cursor_draw(g_VFS, cursorName.c_str(), g_mouse_x, g_yres-g_mouse_y, g_GuiScale, forceGL) < 0) LOGWARNING("Failed to draw cursor '%s'", utf8_from_wstring(cursorName)); #endif #if CONFIG2_GLES #warning TODO: implement cursors for GLES #else // restore transform glMatrixMode(GL_PROJECTION); glPopMatrix(); glMatrixMode(GL_MODELVIEW); glPopMatrix(); #endif } } glEnable(GL_DEPTH_TEST); g_Renderer.EndFrame(); PROFILE2_ATTR("draw calls: %d", (int)g_Renderer.GetStats().m_DrawCalls); PROFILE2_ATTR("terrain tris: %d", (int)g_Renderer.GetStats().m_TerrainTris); PROFILE2_ATTR("water tris: %d", (int)g_Renderer.GetStats().m_WaterTris); PROFILE2_ATTR("model tris: %d", (int)g_Renderer.GetStats().m_ModelTris); PROFILE2_ATTR("overlay tris: %d", (int)g_Renderer.GetStats().m_OverlayTris); PROFILE2_ATTR("blend splats: %d", (int)g_Renderer.GetStats().m_BlendSplats); PROFILE2_ATTR("particles: %d", (int)g_Renderer.GetStats().m_Particles); ogl_WarnIfError(); g_Profiler2.RecordGPUFrameEnd(); ogl_WarnIfError(); } 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 (Threading::IsMainThread()) { static bool reentering = false; if (!reentering) { reentering = true; g_VideoMode.SetFullscreen(false); reentering = false; } } // We don't actually implement the error display here, so return appropriately return ERI_NOT_IMPLEMENTED; } const std::vector& GetMods(const CmdLineArgs& args, int flags) { const bool init_mods = (flags & INIT_MODS) == INIT_MODS; - const bool add_user = !InDevelopmentCopy() && !args.Has("noUserMod"); const bool add_public = (flags & INIT_MODS_PUBLIC) == INIT_MODS_PUBLIC; if (!init_mods) - { - // Add the user mod if it should be present - if (add_user && (g_modsLoaded.empty() || g_modsLoaded.back() != "user")) - g_modsLoaded.push_back("user"); - return g_modsLoaded; - } g_modsLoaded = args.GetMultiple("mod"); if (add_public) g_modsLoaded.insert(g_modsLoaded.begin(), "public"); g_modsLoaded.insert(g_modsLoaded.begin(), "mod"); - // Add the user mod if not explicitly disabled or we have a dev copy so - // that saved files end up in version control and not in the user mod. - if (add_user) - g_modsLoaded.push_back("user"); - return g_modsLoaded; } void MountMods(const Paths& paths, const std::vector& mods) { OsPath modPath = paths.RData()/"mods"; OsPath modUserPath = paths.UserData()/"mods"; + + size_t userFlags = VFS_MOUNT_WATCH|VFS_MOUNT_ARCHIVABLE; + size_t baseFlags = userFlags|VFS_MOUNT_MUST_EXIST; + size_t priority; for (size_t i = 0; i < mods.size(); ++i) { - size_t priority = (i+1)*2; // mods are higher priority than regular mountings, which default to priority 0 - size_t userFlags = VFS_MOUNT_WATCH|VFS_MOUNT_ARCHIVABLE|VFS_MOUNT_REPLACEABLE; - size_t baseFlags = userFlags|VFS_MOUNT_MUST_EXIST; + priority = i + 1; // Mods are higher priority than regular mountings, which default to priority 0 OsPath modName(mods[i]); - if (InDevelopmentCopy()) - { - // We are running a dev copy, so only mount mods in the user mod path - // if the mod does not exist in the data path. - if (DirectoryExists(modPath / modName/"")) - g_VFS->Mount(L"", modPath / modName/"", baseFlags, priority); - else - g_VFS->Mount(L"", modUserPath / modName/"", userFlags, priority); - } + // Only mount mods from the user path if they don't exist in the 'rdata' path. + if (DirectoryExists(modPath / modName / "")) + g_VFS->Mount(L"", modPath / modName / "", baseFlags, priority); else - { - g_VFS->Mount(L"", modPath / modName/"", baseFlags, priority); - // Ensure that user modified files are loaded, if they are present - g_VFS->Mount(L"", modUserPath / modName/"", userFlags, priority+1); - } + g_VFS->Mount(L"", modUserPath / modName / "", userFlags, priority); } + + // Mount the user mod last. In dev copy, mount it with a low priority. Otherwise, make it writable. + g_VFS->Mount(L"", modUserPath / "user" / "", userFlags, InDevelopmentCopy() ? 0 : priority + 1); } static void InitVfs(const CmdLineArgs& args, int flags) { TIMER(L"InitVfs"); const bool setup_error = (flags & INIT_HAVE_DISPLAY_ERROR) == 0; const Paths paths(args); OsPath logs(paths.Logs()); CreateDirectories(logs, 0700); psSetLogDir(logs); // desired location for crashlog is now known. update AppHooks ASAP // (particularly before the following error-prone operations): AppHooks hooks = {0}; hooks.bundle_logs = psBundleLogs; hooks.get_log_dir = psLogDir; if (setup_error) hooks.display_error = psDisplayError; app_hooks_update(&hooks); g_VFS = CreateVfs(); const OsPath readonlyConfig = paths.RData()/"config"/""; - g_VFS->Mount(L"config/", readonlyConfig); - // Engine localization files. + // Mount these dirs with highest priority so that mods can't overwrite them. + g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE, VFS_MAX_PRIORITY); // (adding XMBs to archive speeds up subsequent reads) + if (readonlyConfig != paths.Config()) + g_VFS->Mount(L"config/", readonlyConfig, 0, VFS_MAX_PRIORITY-1); + g_VFS->Mount(L"config/", paths.Config(), 0, VFS_MAX_PRIORITY); + g_VFS->Mount(L"screenshots/", paths.UserData()/"screenshots"/"", 0, VFS_MAX_PRIORITY); + g_VFS->Mount(L"saves/", paths.UserData()/"saves"/"", VFS_MOUNT_WATCH, VFS_MAX_PRIORITY); + + // Engine localization files (regular priority, these can be overwritten). g_VFS->Mount(L"l10n/", paths.RData()/"l10n"/""); MountMods(paths, GetMods(args, flags)); - // We mount these dirs last as otherwise writing could result in files being placed in a mod's dir. - g_VFS->Mount(L"screenshots/", paths.UserData()/"screenshots"/""); - g_VFS->Mount(L"saves/", paths.UserData()/"saves"/"", VFS_MOUNT_WATCH); - // Mounting with highest priority, so that a mod supplied user.cfg is harmless - g_VFS->Mount(L"config/", readonlyConfig, 0, (size_t)-1); - if(readonlyConfig != paths.Config()) - g_VFS->Mount(L"config/", paths.Config(), 0, (size_t)-1); - - g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE); // (adding XMBs to archive speeds up subsequent reads) - // note: don't bother with g_VFS->TextRepresentation - directories // haven't yet been populated and are empty. } static void InitPs(bool setup_gui, const CStrW& gui_page, ScriptInterface* srcScriptInterface, JS::HandleValue initData) { { // console TIMER(L"ps_console"); g_Console->UpdateScreenSize(g_xres, g_yres); // Calculate and store the line spacing CFontMetrics font(CStrIntern(CONSOLE_FONT)); g_Console->m_iFontHeight = font.GetLineSpacing(); g_Console->m_iFontWidth = font.GetCharacterWidth(L'C'); g_Console->m_charsPerPage = (size_t)(g_xres / g_Console->m_iFontWidth); // Offset by an arbitrary amount, to make it fit more nicely g_Console->m_iFontOffset = 7; double blinkRate = 0.5; CFG_GET_VAL("gui.cursorblinkrate", blinkRate); g_Console->SetCursorBlinkRate(blinkRate); } // hotkeys { TIMER(L"ps_lang_hotkeys"); LoadHotkeys(g_ConfigDB); } if (!setup_gui) { // We do actually need *some* kind of GUI loaded, so use the // (currently empty) Atlas one g_GUI->SwitchPage(L"page_atlas.xml", srcScriptInterface, initData); return; } // GUI uses VFS, so this must come after VFS init. g_GUI->SwitchPage(gui_page, srcScriptInterface, initData); } void InitPsAutostart(bool networked, JS::HandleValue attrs) { // The GUI has not been initialized yet, so use the simulation scriptinterface for this variable ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue playerAssignments(rq.cx); ScriptInterface::CreateObject(rq, &playerAssignments); if (!networked) { JS::RootedValue localPlayer(rq.cx); ScriptInterface::CreateObject(rq, &localPlayer, "player", g_Game->GetPlayerID()); scriptInterface.SetProperty(playerAssignments, "local", localPlayer); } JS::RootedValue sessionInitData(rq.cx); ScriptInterface::CreateObject( rq, &sessionInitData, "attribs", attrs, "playerAssignments", playerAssignments); InitPs(true, L"page_loading.xml", &scriptInterface, sessionInitData); } void InitInput() { g_Joystick.Initialise(); // register input handlers // This stack is constructed so the first added, will be the last // one called. This is important, because each of the handlers // has the potential to block events to go further down // in the chain. I.e. the last one in the list added, is the // only handler that can block all messages before they are // processed. in_add_handler(game_view_handler); in_add_handler(CProfileViewer::InputThunk); in_add_handler(conInputHandler); in_add_handler(HotkeyInputHandler); // gui_handler needs to be registered after (i.e. called before!) the // hotkey handler so that input boxes can be typed in without // setting off hotkeys. in_add_handler(gui_handler); in_add_handler(touch_input_handler); // must be registered after (called before) the GUI which relies on these globals in_add_handler(GlobalsInputHandler); // Should be called first, this updates our hotkey press state // so that js calls to HotkeyIsPressed are synched with events. in_add_handler(HotkeyStateChange); } static void ShutdownPs() { SAFE_DELETE(g_GUI); UnloadHotkeys(); // disable the special Windows cursor, or free textures for OGL cursors cursor_draw(g_VFS, 0, g_mouse_x, g_yres-g_mouse_y, 1.0, false); } static void InitRenderer() { TIMER(L"InitRenderer"); // create renderer new CRenderer; // create terrain related stuff new CTerrainTextureManager; g_Renderer.Open(g_xres, g_yres); // Setup lighting environment. Since the Renderer accesses the // lighting environment through a pointer, this has to be done before // the first Frame. g_Renderer.SetLightEnv(&g_LightEnv); // I haven't seen the camera affecting GUI rendering and such, but the // viewport has to be updated according to the video mode SViewPort vp; vp.m_X = 0; vp.m_Y = 0; vp.m_Width = g_xres; vp.m_Height = g_yres; g_Renderer.SetViewport(vp); ModelDefActivateFastImpl(); ColorActivateFastImpl(); ModelRenderer::Init(); } static void InitSDL() { #if OS_LINUX // In fullscreen mode when SDL is compiled with DGA support, the mouse // sensitivity often appears to be unusably wrong (typically too low). // (This seems to be reported almost exclusively on Ubuntu, but can be // reproduced on Gentoo after explicitly enabling DGA.) // Disabling the DGA mouse appears to fix that problem, and doesn't // have any obvious negative effects. setenv("SDL_VIDEO_X11_DGAMOUSE", "0", 0); #endif if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER|SDL_INIT_NOPARACHUTE) < 0) { LOGERROR("SDL library initialization failed: %s", SDL_GetError()); throw PSERROR_System_SDLInitFailed(); } atexit(SDL_Quit); // Text input is active by default, disable it until it is actually needed. SDL_StopTextInput(); #if SDL_VERSION_ATLEAST(2, 0, 9) // SDL2 >= 2.0.9 defaults to 32 pixels (to support touch screens) but that can break our double-clicking. SDL_SetHint(SDL_HINT_MOUSE_DOUBLE_CLICK_RADIUS, "1"); #endif #if OS_MACOSX // Some Mac mice only have one button, so they can't right-click // but SDL2 can emulate that with Ctrl+Click bool macMouse = false; CFG_GET_VAL("macmouse", macMouse); SDL_SetHint(SDL_HINT_MAC_CTRL_CLICK_EMULATE_RIGHT_CLICK, macMouse ? "1" : "0"); #endif } static void ShutdownSDL() { SDL_Quit(); } void EndGame() { SAFE_DELETE(g_NetClient); SAFE_DELETE(g_NetServer); SAFE_DELETE(g_Game); if (CRenderer::IsInitialised()) { ISoundManager::CloseGame(); g_Renderer.ResetState(); } } void Shutdown(int flags) { const bool hasRenderer = CRenderer::IsInitialised(); if ((flags & SHUTDOWN_FROM_CONFIG)) goto from_config; EndGame(); SAFE_DELETE(g_XmppClient); SAFE_DELETE(g_ModIo); ShutdownPs(); TIMER_BEGIN(L"shutdown TexMan"); delete &g_TexMan; TIMER_END(L"shutdown TexMan"); if (hasRenderer) { TIMER_BEGIN(L"shutdown Renderer"); g_Renderer.~CRenderer(); g_VBMan.Shutdown(); TIMER_END(L"shutdown Renderer"); } g_RenderingOptions.ClearHooks(); g_Profiler2.ShutdownGPU(); // Free cursors before shutting down SDL, as they may depend on SDL. cursor_shutdown(); TIMER_BEGIN(L"shutdown SDL"); ShutdownSDL(); TIMER_END(L"shutdown SDL"); if (hasRenderer) g_VideoMode.Shutdown(); TIMER_BEGIN(L"shutdown UserReporter"); g_UserReporter.Deinitialize(); TIMER_END(L"shutdown UserReporter"); // Cleanup curl now that g_ModIo and g_UserReporter have been shutdown. curl_global_cleanup(); delete &g_L10n; from_config: TIMER_BEGIN(L"shutdown ConfigDB"); delete &g_ConfigDB; TIMER_END(L"shutdown ConfigDB"); SAFE_DELETE(g_Console); // This is needed to ensure that no callbacks from the JSAPI try to use // the profiler when it's already destructed g_ScriptContext.reset(); // resource // first shut down all resource owners, and then the handle manager. TIMER_BEGIN(L"resource modules"); ISoundManager::SetEnabled(false); g_VFS.reset(); // this forcibly frees all open handles (thus preventing real leaks), // and makes further access to h_mgr impossible. h_mgr_shutdown(); file_stats_dump(); TIMER_END(L"resource modules"); TIMER_BEGIN(L"shutdown misc"); timer_DisplayClientTotals(); CNetHost::Deinitialize(); // should be last, since the above use them SAFE_DELETE(g_Logger); delete &g_Profiler; delete &g_ProfileViewer; SAFE_DELETE(g_ScriptStatsTable); TIMER_END(L"shutdown misc"); } #if OS_UNIX static void FixLocales() { #if OS_MACOSX || OS_BSD // OS X requires a UTF-8 locale in LC_CTYPE so that *wprintf can handle // wide characters. Peculiarly the string "UTF-8" seems to be acceptable // despite not being a real locale, and it's conveniently language-agnostic, // so use that. setlocale(LC_CTYPE, "UTF-8"); #endif // On misconfigured systems with incorrect locale settings, we'll die // with a C++ exception when some code (e.g. Boost) tries to use locales. // To avoid death, we'll detect the problem here and warn the user and // reset to the default C locale. // For informing the user of the problem, use the list of env vars that // glibc setlocale looks at. (LC_ALL is checked first, and LANG last.) const char* const LocaleEnvVars[] = { "LC_ALL", "LC_COLLATE", "LC_CTYPE", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", "LC_MESSAGES", "LANG" }; try { // this constructor is similar to setlocale(LC_ALL, ""), // but instead of returning NULL, it throws runtime_error // when the first locale env variable found contains an invalid value std::locale(""); } catch (std::runtime_error&) { LOGWARNING("Invalid locale settings"); for (size_t i = 0; i < ARRAY_SIZE(LocaleEnvVars); i++) { if (char* envval = getenv(LocaleEnvVars[i])) LOGWARNING(" %s=\"%s\"", LocaleEnvVars[i], envval); else LOGWARNING(" %s=\"(unset)\"", LocaleEnvVars[i]); } // We should set LC_ALL since it overrides LANG if (setenv("LC_ALL", std::locale::classic().name().c_str(), 1)) debug_warn(L"Invalid locale settings, and unable to set LC_ALL env variable."); else LOGWARNING("Setting LC_ALL env variable to: %s", getenv("LC_ALL")); } } #else static void FixLocales() { // Do nothing on Windows } #endif void EarlyInit() { // If you ever want to catch a particular allocation: //_CrtSetBreakAlloc(232647); Threading::SetMainThread(); debug_SetThreadName("main"); // add all debug_printf "tags" that we are interested in: debug_filter_add("TIMER"); timer_Init(); // initialise profiler early so it can profile startup, // but only after LatchStartTime g_Profiler2.Initialise(); FixLocales(); // Because we do GL calls from a secondary thread, Xlib needs to // be told to support multiple threads safely. // This is needed for Atlas, but we have to call it before any other // Xlib functions (e.g. the ones used when drawing the main menu // before launching Atlas) #if MUST_INIT_X11 int status = XInitThreads(); if (status == 0) debug_printf("Error enabling thread-safety via XInitThreads\n"); #endif // Initialise the low-quality rand function srand(time(NULL)); // NOTE: this rand should *not* be used for simulation! } bool Autostart(const CmdLineArgs& args); /** * Returns true if the user has intended to start a visual replay from command line. */ bool AutostartVisualReplay(const std::string& replayFile); bool Init(const CmdLineArgs& args, int flags) { h_mgr_init(); // Do this as soon as possible, because it chdirs // and will mess up the error reporting if anything // crashes before the working directory is set. InitVfs(args, flags); // This must come after VFS init, which sets the current directory // (required for finding our output log files). g_Logger = new CLogger; new CProfileViewer; new CProfileManager; // before any script code g_ScriptStatsTable = new CScriptStatsTable; g_ProfileViewer.AddRootTable(g_ScriptStatsTable); // Set up the console early, so that debugging // messages can be logged to it. (The console's size // and fonts are set later in InitPs()) g_Console = new CConsole(); // g_ConfigDB, command line args, globals CONFIG_Init(args); // Using a global object for the context is a workaround until Simulation and AI use // their own threads and also their own contexts. const int contextSize = 384 * 1024 * 1024; const int heapGrowthBytesGCTrigger = 20 * 1024 * 1024; g_ScriptContext = ScriptContext::CreateContext(contextSize, heapGrowthBytesGCTrigger); Mod::CacheEnabledModVersions(g_ScriptContext); // Special command-line mode to dump the entity schemas instead of running the game. // (This must be done after loading VFS etc, but should be done before wasting time // on anything else.) if (args.Has("dumpSchema")) { CSimulation2 sim(NULL, g_ScriptContext, NULL); sim.LoadDefaultScripts(); std::ofstream f("entity.rng", std::ios_base::out | std::ios_base::trunc); f << sim.GenerateSchema(); std::cout << "Generated entity.rng\n"; exit(0); } CNetHost::Initialize(); #if CONFIG2_AUDIO if (!args.Has("autostart-nonvisual") && !g_DisableAudio) ISoundManager::CreateSoundManager(); #endif // Check if there are mods specified on the command line, // or if we already set the mods (~INIT_MODS), // else check if there are mods that should be loaded specified // in the config and load those (by aborting init and restarting // the engine). if (!args.Has("mod") && (flags & INIT_MODS) == INIT_MODS) { CStr modstring; CFG_GET_VAL("mod.enabledmods", modstring); if (!modstring.empty()) { std::vector mods; boost::split(mods, modstring, boost::is_any_of(" "), boost::token_compress_on); std::swap(g_modsLoaded, mods); // Abort init and restart RestartEngine(); return false; } } new L10n; // Optionally start profiler HTTP output automatically // (By default it's only enabled by a hotkey, for security/performance) bool profilerHTTPEnable = false; CFG_GET_VAL("profiler2.autoenable", profilerHTTPEnable); if (profilerHTTPEnable) g_Profiler2.EnableHTTP(); // Initialise everything except Win32 sockets (because our networking // system already inits those) curl_global_init(CURL_GLOBAL_ALL & ~CURL_GLOBAL_WIN32); if (!g_Quickstart) g_UserReporter.Initialize(); // after config PROFILE2_EVENT("Init finished"); return true; } void InitGraphics(const CmdLineArgs& args, int flags, const std::vector& installedMods) { const bool setup_vmode = (flags & INIT_HAVE_VMODE) == 0; if(setup_vmode) { InitSDL(); if (!g_VideoMode.InitSDL()) throw PSERROR_System_VmodeFailed(); // abort startup } RunHardwareDetection(); const int quality = SANE_TEX_QUALITY_DEFAULT; // TODO: set value from config file SetTextureQuality(quality); ogl_WarnIfError(); // Optionally start profiler GPU timings automatically // (By default it's only enabled by a hotkey, for performance/compatibility) bool profilerGPUEnable = false; CFG_GET_VAL("profiler2.autoenable", profilerGPUEnable); if (profilerGPUEnable) g_Profiler2.EnableGPU(); if(!g_Quickstart) { WriteSystemInfo(); // note: no longer vfs_display here. it's dog-slow due to unbuffered // file output and very rarely needed. } if(g_DisableAudio) ISoundManager::SetEnabled(false); g_GUI = new CGUIManager(); // (must come after SetVideoMode, since it calls ogl_Init) CStr8 renderPath = "default"; CFG_GET_VAL("renderpath", renderPath); if ((ogl_HaveExtensions(0, "GL_ARB_vertex_program", "GL_ARB_fragment_program", NULL) != 0 // ARB && ogl_HaveExtensions(0, "GL_ARB_vertex_shader", "GL_ARB_fragment_shader", NULL) != 0) // GLSL || RenderPathEnum::FromString(renderPath) == FIXED) { // It doesn't make sense to continue working here, because we're not // able to display anything. DEBUG_DISPLAY_FATAL_ERROR( L"Your graphics card doesn't appear to be fully compatible with OpenGL shaders." L" The game does not support pre-shader graphics cards." L" You are advised to try installing newer drivers and/or upgrade your graphics card." L" For more information, please see http://www.wildfiregames.com/forum/index.php?showtopic=16734" ); } 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_ConfigDB.SetValueBool(CFG_HWDETECT, "shadows", false); } ogl_WarnIfError(); g_RenderingOptions.ReadConfigAndSetupHooks(); InitRenderer(); InitInput(); ogl_WarnIfError(); // TODO: Is this the best place for this? if (VfsDirectoryExists(L"maps/")) CXeromyces::AddValidator(g_VFS, "map", "maps/scenario.rng"); try { if (!AutostartVisualReplay(args.Get("replay-visual")) && !Autostart(args)) { const bool setup_gui = ((flags & INIT_NO_GUI) == 0); // We only want to display the splash screen at startup shared_ptr scriptInterface = g_GUI->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue data(rq.cx); if (g_GUI) { ScriptInterface::CreateObject(rq, &data, "isStartup", true); if (!installedMods.empty()) scriptInterface->SetProperty(data, "installedMods", installedMods); } InitPs(setup_gui, installedMods.empty() ? L"page_pregame.xml" : L"page_modmod.xml", g_GUI->GetScriptInterface().get(), data); } } catch (PSERROR_Game_World_MapLoadFailed& e) { // Map Loading failed // Start the engine so we have a GUI InitPs(true, L"page_pregame.xml", NULL, JS::UndefinedHandleValue); // Call script function to do the actual work // (delete game data, switch GUI page, show error, etc.) CancelLoad(CStr(e.what()).FromUTF8()); } } void InitNonVisual(const CmdLineArgs& args) { // Need some stuff for terrain movement costs: // (TODO: this ought to be independent of any graphics code) new CTerrainTextureManager; g_TexMan.LoadTerrainTextures(); Autostart(args); } void RenderGui(bool RenderingState) { g_DoRenderGui = RenderingState; } void RenderLogger(bool RenderingState) { g_DoRenderLogger = RenderingState; } void RenderCursor(bool RenderingState) { g_DoRenderCursor = RenderingState; } /** * Temporarily loads a scenario map and retrieves the "ScriptSettings" JSON * data from it. * The scenario map format is used for scenario and skirmish map types (random * games do not use a "map" (format) but a small JavaScript program which * creates a map on the fly). It contains a section to initialize the game * setup screen. * @param mapPath Absolute path (from VFS root) to the map file to peek in. * @return ScriptSettings in JSON format extracted from the map. */ CStr8 LoadSettingsOfScenarioMap(const VfsPath &mapPath) { CXeromyces mapFile; const char *pathToSettings[] = { "Scenario", "ScriptSettings", "" // Path to JSON data in map }; Status loadResult = mapFile.Load(g_VFS, mapPath); if (INFO::OK != loadResult) { LOGERROR("LoadSettingsOfScenarioMap: Unable to load map file '%s'", mapPath.string8()); throw PSERROR_Game_World_MapLoadFailed("Unable to load map file, check the path for typos."); } XMBElement mapElement = mapFile.GetRoot(); // Select the ScriptSettings node in the map file... for (int i = 0; pathToSettings[i][0]; ++i) { int childId = mapFile.GetElementID(pathToSettings[i]); XMBElementList nodes = mapElement.GetChildNodes(); auto it = std::find_if(nodes.begin(), nodes.end(), [&childId](const XMBElement& child) { return child.GetNodeName() == childId; }); if (it != nodes.end()) mapElement = *it; } // ... they contain a JSON document to initialize the game setup // screen return mapElement.GetText(); } /* * Command line options for autostart * (keep synchronized with binaries/system/readme.txt): * * -autostart="TYPEDIR/MAPNAME" enables autostart and sets MAPNAME; * TYPEDIR is skirmishes, scenarios, or random * -autostart-seed=SEED sets randomization seed value (default 0, use -1 for random) * -autostart-ai=PLAYER:AI sets the AI for PLAYER (e.g. 2:petra) * -autostart-aidiff=PLAYER:DIFF sets the DIFFiculty of PLAYER's AI * (0: sandbox, 5: very hard) * -autostart-aiseed=AISEED sets the seed used for the AI random * generator (default 0, use -1 for random) * -autostart-player=NUMBER sets the playerID in non-networked games (default 1, use -1 for observer) * -autostart-civ=PLAYER:CIV sets PLAYER's civilisation to CIV * (skirmish and random maps only) * -autostart-team=PLAYER:TEAM sets the team for PLAYER (e.g. 2:2). * -autostart-ceasefire=NUM sets a ceasefire duration NUM * (default 0 minutes) * -autostart-nonvisual disable any graphics and sounds * -autostart-victory=SCRIPTNAME sets the victory conditions with SCRIPTNAME * located in simulation/data/settings/victory_conditions/ * (default conquest). When the first given SCRIPTNAME is * "endless", no victory conditions will apply. * -autostart-wonderduration=NUM sets the victory duration NUM for wonder victory condition * (default 10 minutes) * -autostart-relicduration=NUM sets the victory duration NUM for relic victory condition * (default 10 minutes) * -autostart-reliccount=NUM sets the number of relics for relic victory condition * (default 2 relics) * -autostart-disable-replay disable saving of replays * * Multiplayer: * -autostart-playername=NAME sets local player NAME (default 'anonymous') * -autostart-host sets multiplayer host mode * -autostart-host-players=NUMBER sets NUMBER of human players for multiplayer * game (default 2) * -autostart-client=IP sets multiplayer client to join host at * given IP address * Random maps only: * -autostart-size=TILES sets random map size in TILES (default 192) * -autostart-players=NUMBER sets NUMBER of players on random map * (default 2) * * Examples: * 1) "Bob" will host a 2 player game on the Arcadia map: * -autostart="scenarios/Arcadia" -autostart-host -autostart-host-players=2 -autostart-playername="Bob" * "Alice" joins the match as player 2: * -autostart="scenarios/Arcadia" -autostart-client=127.0.0.1 -autostart-playername="Alice" * The players use the developer overlay to control players. * * 2) Load Alpine Lakes random map with random seed, 2 players (Athens and Britons), and player 2 is PetraBot: * -autostart="random/alpine_lakes" -autostart-seed=-1 -autostart-players=2 -autostart-civ=1:athen -autostart-civ=2:brit -autostart-ai=2:petra * * 3) Observe the PetraBot on a triggerscript map: * -autostart="random/jebel_barkal" -autostart-seed=-1 -autostart-players=2 -autostart-civ=1:athen -autostart-civ=2:brit -autostart-ai=1:petra -autostart-ai=2:petra -autostart-player=-1 */ bool Autostart(const CmdLineArgs& args) { CStr autoStartName = args.Get("autostart"); if (autoStartName.empty()) return false; g_Game = new CGame(!args.Has("autostart-disable-replay")); ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue attrs(rq.cx); JS::RootedValue settings(rq.cx); JS::RootedValue playerData(rq.cx); ScriptInterface::CreateObject(rq, &attrs); ScriptInterface::CreateObject(rq, &settings); ScriptInterface::CreateArray(rq, &playerData); // The directory in front of the actual map name indicates which type // of map is being loaded. Drawback of this approach is the association // of map types and folders is hard-coded, but benefits are: // - No need to pass the map type via command line separately // - Prevents mixing up of scenarios and skirmish maps to some degree Path mapPath = Path(autoStartName); std::wstring mapDirectory = mapPath.Parent().Filename().string(); std::string mapType; if (mapDirectory == L"random") { // Random map definition will be loaded from JSON file, so we need to parse it std::wstring scriptPath = L"maps/" + autoStartName.FromUTF8() + L".json"; JS::RootedValue scriptData(rq.cx); scriptInterface.ReadJSONFile(scriptPath, &scriptData); if (!scriptData.isUndefined() && scriptInterface.GetProperty(scriptData, "settings", &settings)) { // JSON loaded ok - copy script name over to game attributes std::wstring scriptFile; scriptInterface.GetProperty(settings, "Script", scriptFile); scriptInterface.SetProperty(attrs, "script", scriptFile); // RMS filename } else { // Problem with JSON file LOGERROR("Autostart: Error reading random map script '%s'", utf8_from_wstring(scriptPath)); throw PSERROR_Game_World_MapLoadFailed("Error reading random map script.\nCheck application log for details."); } // Get optional map size argument (default 192) uint mapSize = 192; if (args.Has("autostart-size")) { CStr size = args.Get("autostart-size"); mapSize = size.ToUInt(); } scriptInterface.SetProperty(settings, "Size", mapSize); // Random map size (in patches) // Get optional number of players (default 2) size_t numPlayers = 2; if (args.Has("autostart-players")) { CStr num = args.Get("autostart-players"); numPlayers = num.ToUInt(); } // Set up player data for (size_t i = 0; i < numPlayers; ++i) { JS::RootedValue player(rq.cx); // We could load player_defaults.json here, but that would complicate the logic // even more and autostart is only intended for developers anyway ScriptInterface::CreateObject(rq, &player, "Civ", "athen"); scriptInterface.SetPropertyInt(playerData, i, player); } mapType = "random"; } else if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes") { // Initialize general settings from the map data so some values // (e.g. name of map) are always present, even when autostart is // partially configured CStr8 mapSettingsJSON = LoadSettingsOfScenarioMap("maps/" + autoStartName + ".xml"); scriptInterface.ParseJSON(mapSettingsJSON, &settings); // Initialize the playerData array being modified by autostart // with the real map data, so sensible values are present: scriptInterface.GetProperty(settings, "PlayerData", &playerData); if (mapDirectory == L"scenarios") mapType = "scenario"; else mapType = "skirmish"; } else { LOGERROR("Autostart: Unrecognized map type '%s'", utf8_from_wstring(mapDirectory)); throw PSERROR_Game_World_MapLoadFailed("Unrecognized map type.\nConsult readme.txt for the currently supported types."); } scriptInterface.SetProperty(attrs, "mapType", mapType); scriptInterface.SetProperty(attrs, "map", "maps/" + autoStartName); scriptInterface.SetProperty(settings, "mapType", mapType); scriptInterface.SetProperty(settings, "CheatsEnabled", true); // The seed is used for both random map generation and simulation u32 seed = 0; if (args.Has("autostart-seed")) { CStr seedArg = args.Get("autostart-seed"); if (seedArg == "-1") seed = rand(); else seed = seedArg.ToULong(); } scriptInterface.SetProperty(settings, "Seed", seed); // Set seed for AIs u32 aiseed = 0; if (args.Has("autostart-aiseed")) { CStr seedArg = args.Get("autostart-aiseed"); if (seedArg == "-1") aiseed = rand(); else aiseed = seedArg.ToULong(); } scriptInterface.SetProperty(settings, "AISeed", aiseed); // Set player data for AIs // attrs.settings = { PlayerData: [ { AI: ... }, ... ] } // or = { PlayerData: [ null, { AI: ... }, ... ] } when gaia set int offset = 1; JS::RootedValue player(rq.cx); if (scriptInterface.GetPropertyInt(playerData, 0, &player) && player.isNull()) offset = 0; // Set teams if (args.Has("autostart-team")) { std::vector civArgs = args.GetMultiple("autostart-team"); for (size_t i = 0; i < civArgs.size(); ++i) { int playerID = civArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue currentPlayer(rq.cx); if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, ¤tPlayer) || currentPlayer.isUndefined()) { if (mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING("Autostart: Invalid player %d in autostart-team option", playerID); continue; } ScriptInterface::CreateObject(rq, ¤tPlayer); } int teamID = civArgs[i].AfterFirst(":").ToInt() - 1; scriptInterface.SetProperty(currentPlayer, "Team", teamID); scriptInterface.SetPropertyInt(playerData, playerID-offset, currentPlayer); } } int ceasefire = 0; if (args.Has("autostart-ceasefire")) ceasefire = args.Get("autostart-ceasefire").ToInt(); scriptInterface.SetProperty(settings, "Ceasefire", ceasefire); if (args.Has("autostart-ai")) { std::vector aiArgs = args.GetMultiple("autostart-ai"); for (size_t i = 0; i < aiArgs.size(); ++i) { int playerID = aiArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue currentPlayer(rq.cx); if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, ¤tPlayer) || currentPlayer.isUndefined()) { if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING("Autostart: Invalid player %d in autostart-ai option", playerID); continue; } ScriptInterface::CreateObject(rq, ¤tPlayer); } scriptInterface.SetProperty(currentPlayer, "AI", aiArgs[i].AfterFirst(":")); scriptInterface.SetProperty(currentPlayer, "AIDiff", 3); scriptInterface.SetProperty(currentPlayer, "AIBehavior", "balanced"); scriptInterface.SetPropertyInt(playerData, playerID-offset, currentPlayer); } } // Set AI difficulty if (args.Has("autostart-aidiff")) { std::vector civArgs = args.GetMultiple("autostart-aidiff"); for (size_t i = 0; i < civArgs.size(); ++i) { int playerID = civArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue currentPlayer(rq.cx); if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, ¤tPlayer) || currentPlayer.isUndefined()) { if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING("Autostart: Invalid player %d in autostart-aidiff option", playerID); continue; } ScriptInterface::CreateObject(rq, ¤tPlayer); } scriptInterface.SetProperty(currentPlayer, "AIDiff", civArgs[i].AfterFirst(":").ToInt()); scriptInterface.SetPropertyInt(playerData, playerID-offset, currentPlayer); } } // Set player data for Civs if (args.Has("autostart-civ")) { if (mapDirectory != L"scenarios") { std::vector civArgs = args.GetMultiple("autostart-civ"); for (size_t i = 0; i < civArgs.size(); ++i) { int playerID = civArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue currentPlayer(rq.cx); if (!scriptInterface.GetPropertyInt(playerData, playerID-offset, ¤tPlayer) || currentPlayer.isUndefined()) { if (mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING("Autostart: Invalid player %d in autostart-civ option", playerID); continue; } ScriptInterface::CreateObject(rq, ¤tPlayer); } scriptInterface.SetProperty(currentPlayer, "Civ", civArgs[i].AfterFirst(":")); scriptInterface.SetPropertyInt(playerData, playerID-offset, currentPlayer); } } else LOGWARNING("Autostart: Option 'autostart-civ' is invalid for scenarios"); } // Add player data to map settings scriptInterface.SetProperty(settings, "PlayerData", playerData); // Add map settings to game attributes scriptInterface.SetProperty(attrs, "settings", settings); // Get optional playername CStrW userName = L"anonymous"; if (args.Has("autostart-playername")) userName = args.Get("autostart-playername").FromUTF8(); // Add additional scripts to the TriggerScripts property std::vector triggerScriptsVector; JS::RootedValue triggerScripts(rq.cx); if (scriptInterface.HasProperty(settings, "TriggerScripts")) { scriptInterface.GetProperty(settings, "TriggerScripts", &triggerScripts); FromJSVal_vector(rq, triggerScripts, triggerScriptsVector); } if (!CRenderer::IsInitialised()) { CStr nonVisualScript = "scripts/NonVisualTrigger.js"; triggerScriptsVector.push_back(nonVisualScript.FromUTF8()); } std::vector victoryConditions(1, "conquest"); if (args.Has("autostart-victory")) victoryConditions = args.GetMultiple("autostart-victory"); if (victoryConditions.size() == 1 && victoryConditions[0] == "endless") victoryConditions.clear(); scriptInterface.SetProperty(settings, "VictoryConditions", victoryConditions); for (const CStr& victory : victoryConditions) { JS::RootedValue scriptData(rq.cx); JS::RootedValue data(rq.cx); JS::RootedValue victoryScripts(rq.cx); CStrW scriptPath = L"simulation/data/settings/victory_conditions/" + victory.FromUTF8() + L".json"; scriptInterface.ReadJSONFile(scriptPath, &scriptData); if (!scriptData.isUndefined() && scriptInterface.GetProperty(scriptData, "Data", &data) && !data.isUndefined() && scriptInterface.GetProperty(data, "Scripts", &victoryScripts) && !victoryScripts.isUndefined()) { std::vector victoryScriptsVector; FromJSVal_vector(rq, victoryScripts, victoryScriptsVector); triggerScriptsVector.insert(triggerScriptsVector.end(), victoryScriptsVector.begin(), victoryScriptsVector.end()); } else { LOGERROR("Autostart: Error reading victory script '%s'", utf8_from_wstring(scriptPath)); throw PSERROR_Game_World_MapLoadFailed("Error reading victory script.\nCheck application log for details."); } } ToJSVal_vector(rq, &triggerScripts, triggerScriptsVector); scriptInterface.SetProperty(settings, "TriggerScripts", triggerScripts); int wonderDuration = 10; if (args.Has("autostart-wonderduration")) wonderDuration = args.Get("autostart-wonderduration").ToInt(); scriptInterface.SetProperty(settings, "WonderDuration", wonderDuration); int relicDuration = 10; if (args.Has("autostart-relicduration")) relicDuration = args.Get("autostart-relicduration").ToInt(); scriptInterface.SetProperty(settings, "RelicDuration", relicDuration); int relicCount = 2; if (args.Has("autostart-reliccount")) relicCount = args.Get("autostart-reliccount").ToInt(); scriptInterface.SetProperty(settings, "RelicCount", relicCount); if (args.Has("autostart-host")) { InitPsAutostart(true, attrs); size_t maxPlayers = 2; if (args.Has("autostart-host-players")) maxPlayers = args.Get("autostart-host-players").ToUInt(); // Generate a secret to identify the host client. std::string secret = ps_generate_guid(); g_NetServer = new CNetServer(false, maxPlayers); g_NetServer->SetControllerSecret(secret); g_NetServer->UpdateInitAttributes(&attrs, scriptInterface); bool ok = g_NetServer->SetupConnection(PS_DEFAULT_PORT); ENSURE(ok); g_NetClient = new CNetClient(g_Game); g_NetClient->SetUserName(userName); g_NetClient->SetupServerData("127.0.0.1", PS_DEFAULT_PORT, false); g_NetClient->SetControllerSecret(secret); g_NetClient->SetupConnection(nullptr); } else if (args.Has("autostart-client")) { InitPsAutostart(true, attrs); g_NetClient = new CNetClient(g_Game); g_NetClient->SetUserName(userName); CStr ip = args.Get("autostart-client"); if (ip.empty()) ip = "127.0.0.1"; g_NetClient->SetupServerData(ip, PS_DEFAULT_PORT, false); ENSURE(g_NetClient->SetupConnection(nullptr)); } else { g_Game->SetPlayerID(args.Has("autostart-player") ? args.Get("autostart-player").ToInt() : 1); g_Game->StartGame(&attrs, ""); if (CRenderer::IsInitialised()) { InitPsAutostart(false, attrs); } else { // TODO: Non progressive load can fail - need a decent way to handle this LDR_NonprogressiveLoad(); ENSURE(g_Game->ReallyStartGame() == PSRETURN_OK); } } return true; } bool AutostartVisualReplay(const std::string& replayFile) { if (!FileExists(OsPath(replayFile))) return false; g_Game = new CGame(false); g_Game->SetPlayerID(-1); g_Game->StartVisualReplay(replayFile); ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); ScriptRequest rq(scriptInterface); JS::RootedValue attrs(rq.cx, g_Game->GetSimulation2()->GetInitAttributes()); InitPsAutostart(false, attrs); return true; } void CancelLoad(const CStrW& message) { shared_ptr pScriptInterface = g_GUI->GetActiveGUI()->GetScriptInterface(); ScriptRequest rq(pScriptInterface); JS::RootedValue global(rq.cx, rq.globalValue()); LDR_Cancel(); if (g_GUI && g_GUI->GetPageCount() && pScriptInterface->HasProperty(global, "cancelOnLoadGameError")) pScriptInterface->CallFunctionVoid(global, "cancelOnLoadGameError", message); } bool InDevelopmentCopy() { if (!g_CheckedIfInDevelopmentCopy) { g_InDevelopmentCopy = (g_VFS->GetFileInfo(L"config/dev.cfg", NULL) == INFO::OK); g_CheckedIfInDevelopmentCopy = true; } return g_InDevelopmentCopy; } Index: ps/trunk/source/ps/Mod.cpp =================================================================== --- ps/trunk/source/ps/Mod.cpp (revision 25106) +++ ps/trunk/source/ps/Mod.cpp (revision 25107) @@ -1,154 +1,156 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ps/Mod.h" #include #include "lib/file/file_system.h" #include "lib/file/vfs/vfs.h" #include "lib/utf8.h" #include "ps/Filesystem.h" #include "ps/GameSetup/GameSetup.h" #include "ps/GameSetup/Paths.h" #include "ps/Pyrogenesis.h" #include "scriptinterface/ScriptInterface.h" std::vector g_modsLoaded; std::vector> g_LoadedModVersions; CmdLineArgs g_args; JS::Value Mod::GetAvailableMods(const ScriptInterface& scriptInterface) { ScriptRequest rq(scriptInterface); JS::RootedObject obj(rq.cx, JS_NewPlainObject(rq.cx)); const Paths paths(g_args); // loop over all possible paths OsPath modPath = paths.RData()/"mods"; OsPath modUserPath = paths.UserData()/"mods"; DirectoryNames modDirs; DirectoryNames modDirsUser; GetDirectoryEntries(modPath, NULL, &modDirs); // Sort modDirs so that we can do a fast lookup below std::sort(modDirs.begin(), modDirs.end()); PIVFS vfs = CreateVfs(); for (DirectoryNames::iterator iter = modDirs.begin(); iter != modDirs.end(); ++iter) { vfs->Clear(); - if (vfs->Mount(L"", modPath / *iter, VFS_MOUNT_MUST_EXIST) < 0) + // Mount with lowest priority, we don't want to overwrite anything + if (vfs->Mount(L"", modPath / *iter / "", VFS_MOUNT_MUST_EXIST, VFS_MIN_PRIORITY) < 0) continue; CVFSFile modinfo; if (modinfo.Load(vfs, L"mod.json", false) != PSRETURN_OK) continue; JS::RootedValue json(rq.cx); if (!scriptInterface.ParseJSON(modinfo.GetAsString(), &json)) continue; // Valid mod, add it to our structure JS_SetProperty(rq.cx, obj, utf8_from_wstring(iter->string()).c_str(), json); } GetDirectoryEntries(modUserPath, NULL, &modDirsUser); bool dev = InDevelopmentCopy(); for (DirectoryNames::iterator iter = modDirsUser.begin(); iter != modDirsUser.end(); ++iter) { // If we are in a dev copy we do not mount mods in the user mod folder that // are already present in the mod folder, thus we skip those here. if (dev && std::binary_search(modDirs.begin(), modDirs.end(), *iter)) continue; vfs->Clear(); - if (vfs->Mount(L"", modUserPath / *iter, VFS_MOUNT_MUST_EXIST) < 0) + // Mount with lowest priority, we don't want to overwrite anything + if (vfs->Mount(L"", modUserPath / *iter / "", VFS_MOUNT_MUST_EXIST, VFS_MIN_PRIORITY) < 0) continue; CVFSFile modinfo; if (modinfo.Load(vfs, L"mod.json", false) != PSRETURN_OK) continue; JS::RootedValue json(rq.cx); if (!scriptInterface.ParseJSON(modinfo.GetAsString(), &json)) continue; // Valid mod, add it to our structure JS_SetProperty(rq.cx, obj, utf8_from_wstring(iter->string()).c_str(), json); } return JS::ObjectValue(*obj); } void Mod::CacheEnabledModVersions(const shared_ptr& scriptContext) { ScriptInterface scriptInterface("Engine", "CacheEnabledModVersions", scriptContext); ScriptRequest rq(scriptInterface); JS::RootedValue availableMods(rq.cx, GetAvailableMods(scriptInterface)); g_LoadedModVersions.clear(); for (const CStr& mod : g_modsLoaded) { - // Ignore user and mod mod as they are irrelevant for compatibility checks - if (mod == "mod" || mod == "user") + // Ignore mod mod as it is irrelevant for compatibility checks + if (mod == "mod") continue; CStr version; JS::RootedValue modData(rq.cx); if (scriptInterface.GetProperty(availableMods, mod.c_str(), &modData)) scriptInterface.GetProperty(modData, "version", version); g_LoadedModVersions.push_back({mod, version}); } } JS::Value Mod::GetLoadedModsWithVersions(const ScriptInterface& scriptInterface) { ScriptRequest rq(scriptInterface); JS::RootedValue returnValue(rq.cx); scriptInterface.ToJSVal(rq, &returnValue, g_LoadedModVersions); return returnValue; } JS::Value Mod::GetEngineInfo(const ScriptInterface& scriptInterface) { ScriptRequest rq(scriptInterface); JS::RootedValue mods(rq.cx, Mod::GetLoadedModsWithVersions(scriptInterface)); JS::RootedValue metainfo(rq.cx); ScriptInterface::CreateObject( rq, &metainfo, "engine_version", engine_version, "mods", mods); scriptInterface.FreezeObject(metainfo, true); return metainfo; } Index: ps/trunk/source/ps/SavedGame.cpp =================================================================== --- ps/trunk/source/ps/SavedGame.cpp (revision 25106) +++ ps/trunk/source/ps/SavedGame.cpp (revision 25107) @@ -1,301 +1,301 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 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 "graphics/GameView.h" #include "i18n/L10n.h" #include "lib/allocators/shared_ptr.h" #include "lib/file/archive/archive_zip.h" #include "lib/file/io/io.h" #include "lib/utf8.h" #include "maths/Vector3D.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/Mod.h" #include "ps/Pyrogenesis.h" #include "scriptinterface/ScriptInterface.h" #include "simulation2/Simulation2.h" // TODO: we ought to check version numbers when loading files Status SavedGames::SavePrefix(const CStrW& prefix, const CStrW& description, CSimulation2& simulation, const ScriptInterface::StructuredClone& guiMetadataClone) { // 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); return Save(filename.Filename().string(), description, simulation, guiMetadataClone); } Status SavedGames::Save(const CStrW& name, const CStrW& description, CSimulation2& simulation, const ScriptInterface::StructuredClone& guiMetadataClone) { ScriptRequest rq(simulation.GetScriptInterface()); // Determine the filename to save under const VfsPath basenameFormat(L"saves/" + name); const VfsPath filename = basenameFormat.ChangeExtension(L".0adsave"); // 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); JS::RootedValue initAttributes(rq.cx, simulation.GetInitAttributes()); JS::RootedValue mods(rq.cx, Mod::GetLoadedModsWithVersions(simulation.GetScriptInterface())); JS::RootedValue metadata(rq.cx); ScriptInterface::CreateObject( rq, &metadata, "engine_version", engine_version, "time", static_cast(now), "playerID", g_Game->GetPlayerID(), "mods", mods, "initAttributes", initAttributes); JS::RootedValue guiMetadata(rq.cx); simulation.GetScriptInterface().ReadStructuredClone(guiMetadataClone, &guiMetadata); // get some camera data const CVector3D cameraPosition = g_Game->GetView()->GetCameraPosition(); const CVector3D cameraRotation = g_Game->GetView()->GetCameraRotation(); JS::RootedValue cameraMetadata(rq.cx); ScriptInterface::CreateObject( rq, &cameraMetadata, "PosX", cameraPosition.X, "PosY", cameraPosition.Y, "PosZ", cameraPosition.Z, "RotX", cameraRotation.X, "RotY", cameraRotation.Y, "Zoom", g_Game->GetView()->GetCameraZoom()); simulation.GetScriptInterface().SetProperty(guiMetadata, "camera", cameraMetadata); simulation.GetScriptInterface().SetProperty(metadata, "gui", guiMetadata); simulation.GetScriptInterface().SetProperty(metadata, "description", description); std::string metadataString = simulation.GetScriptInterface().StringifyJSON(&metadata, true); // Write the saved game as zip file containing the various components PIArchiveWriter archiveWriter = CreateArchiveWriter_Zip(tempSaveFileRealPath, false); if (!archiveWriter) WARN_RETURN(ERR::FAIL); 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; CFileInfo 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(g_L10n.Translate("Saved game to '%s'"), realPath.string8()); debug_printf("Saved game to '%s'\n", realPath.string8().c_str()); return INFO::OK; } /** * Helper class for retrieving data from saved game archives */ class CGameLoader { NONCOPYABLE(CGameLoader); public: /** * @param scriptInterface the ScriptInterface used for loading metadata. * @param[out] savedState serialized simulation state stored as string of bytes, * loaded from simulation.dat inside the archive. * * Note: We use a different approach for returning the string and the metadata JS::Value. * We use a pointer for the string to avoid copies (efficiency). We don't use this approach * for the metadata because it would be error prone with rooting and the stack-based rooting * types and confusing (a chain of pointers pointing to other pointers). */ CGameLoader(const ScriptInterface& scriptInterface, std::string* savedState) : m_ScriptInterface(scriptInterface), m_Metadata(scriptInterface.GetGeneralJSContext()), m_SavedState(savedState) { } static void ReadEntryCallback(const VfsPath& pathname, const CFileInfo& fileInfo, PIArchiveFile archiveFile, uintptr_t cbData) { ((CGameLoader*)cbData)->ReadEntry(pathname, fileInfo, archiveFile); } void ReadEntry(const VfsPath& pathname, const CFileInfo& fileInfo, PIArchiveFile archiveFile) { if (pathname == L"metadata.json") { std::string buffer; buffer.resize(fileInfo.Size()); WARN_IF_ERR(archiveFile->Load("", DummySharedPtr((u8*)buffer.data()), buffer.size())); m_ScriptInterface.ParseJSON(buffer, &m_Metadata); } 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())); } } JS::Value GetMetadata() { return m_Metadata.get(); } private: const ScriptInterface& m_ScriptInterface; JS::PersistentRooted m_Metadata; std::string* m_SavedState; }; Status SavedGames::Load(const std::wstring& name, const ScriptInterface& scriptInterface, JS::MutableHandleValue metadata, std::string& savedState) { // Determine the filename to load const VfsPath basename(L"saves/" + name); const VfsPath filename = basename.ChangeExtension(L".0adsave"); // Don't crash just because file isn't found, this can happen if the file is deleted from the OS if (!VfsFileExists(filename)) return ERR::FILE_NOT_FOUND; OsPath realPath; WARN_RETURN_STATUS_IF_ERR(g_VFS->GetRealPath(filename, realPath)); PIArchiveReader archiveReader = CreateArchiveReader_Zip(realPath); if (!archiveReader) WARN_RETURN(ERR::FAIL); CGameLoader loader(scriptInterface, &savedState); WARN_RETURN_STATUS_IF_ERR(archiveReader->ReadEntries(CGameLoader::ReadEntryCallback, (uintptr_t)&loader)); metadata.set(loader.GetMetadata()); return INFO::OK; } JS::Value SavedGames::GetSavedGames(const ScriptInterface& scriptInterface) { TIMER(L"GetSavedGames"); ScriptRequest rq(scriptInterface); JS::RootedValue games(rq.cx); ScriptInterface::CreateArray(rq, &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 = CreateArchiveReader_Zip(realPath); if (!archiveReader) { // Triggered by e.g. the file being open in another program LOGWARNING("Failed to read saved game '%s'", realPath.string8()); continue; // skip this file } CGameLoader loader(scriptInterface, NULL); err = archiveReader->ReadEntries(CGameLoader::ReadEntryCallback, (uintptr_t)&loader); if (err < 0) { DEBUG_WARN_ERR(err); continue; // skip this file } JS::RootedValue metadata(rq.cx, loader.GetMetadata()); JS::RootedValue game(rq.cx); ScriptInterface::CreateObject( rq, &game, "id", pathnames[i].Basename(), "metadata", metadata); scriptInterface.SetPropertyInt(games, i, game); } return games; } bool SavedGames::DeleteSavedGame(const std::wstring& name) { const VfsPath basename(L"saves/" + name); const VfsPath filename = basename.ChangeExtension(L".0adsave"); OsPath realpath; - // Make sure it exists in VFS and find its real path - if (!VfsFileExists(filename) || g_VFS->GetRealPath(filename, realpath) != INFO::OK) + // Make sure it exists in VFS and find its path + if (!VfsFileExists(filename) || g_VFS->GetOriginalPath(filename, realpath) != INFO::OK) return false; // Error // Remove from VFS if (g_VFS->RemoveFile(filename) != INFO::OK) return false; // Error // Delete actual file if (wunlink(realpath) != 0) return false; // Error // Successfully deleted file return true; } Index: ps/trunk/source/ps/tests/test_ConfigDB.h =================================================================== --- ps/trunk/source/ps/tests/test_ConfigDB.h (revision 25106) +++ ps/trunk/source/ps/tests/test_ConfigDB.h (revision 25107) @@ -1,87 +1,87 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "lib/self_test.h" #include "lib/file/vfs/vfs.h" #include "ps/ConfigDB.h" extern PIVFS g_VFS; class TestConfigDB : public CxxTest::TestSuite { CConfigDB* configDB; public: void setUp() { g_VFS = CreateVfs(); - TS_ASSERT_OK(g_VFS->Mount(L"config", DataDir()/"_testconfig")); + TS_ASSERT_OK(g_VFS->Mount(L"config", DataDir() / "_testconfig" / "")); configDB = new CConfigDB; } void tearDown() { DeleteDirectory(DataDir()/"_testconfig"); g_VFS.reset(); delete configDB; } void test_setting_int() { configDB->SetConfigFile(CFG_SYSTEM, "config/file.cfg"); configDB->WriteFile(CFG_SYSTEM); configDB->Reload(CFG_SYSTEM); configDB->SetValueString(CFG_SYSTEM, "test_setting", "5"); configDB->WriteFile(CFG_SYSTEM); configDB->Reload(CFG_SYSTEM); { std::string res; configDB->GetValue(CFG_SYSTEM, "test_setting", res); TS_ASSERT_EQUALS(res, "5"); } { int res; configDB->GetValue(CFG_SYSTEM, "test_setting", res); TS_ASSERT_EQUALS(res, 5); } } void test_setting_empty() { configDB->SetConfigFile(CFG_SYSTEM, "config/file.cfg"); configDB->WriteFile(CFG_SYSTEM); configDB->Reload(CFG_SYSTEM); configDB->SetValueList(CFG_SYSTEM, "test_setting", {}); configDB->WriteFile(CFG_SYSTEM); configDB->Reload(CFG_SYSTEM); { std::string res = "toto"; configDB->GetValue(CFG_SYSTEM, "test_setting", res); // Empty config values don't overwrite TS_ASSERT_EQUALS(res, "toto"); } { int res = 3; configDB->GetValue(CFG_SYSTEM, "test_setting", res); // Empty config values don't overwrite TS_ASSERT_EQUALS(res, 3); } } }; Index: ps/trunk/source/ps/tests/test_Hotkeys.h =================================================================== --- ps/trunk/source/ps/tests/test_Hotkeys.h (revision 25106) +++ ps/trunk/source/ps/tests/test_Hotkeys.h (revision 25107) @@ -1,214 +1,214 @@ /* Copyright (C) 2021 Wildfire Games. * * Permission is hereby granted, free of charge, to any person obtaining * a copy of this software and associated documentation files (the * "Software"), to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to * the following conditions: * * The above copyright notice and this permission notice shall be included * in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "lib/self_test.h" #include "lib/external_libraries/libsdl.h" #include "ps/Hotkey.h" #include "ps/ConfigDB.h" #include "ps/Globals.h" #include "ps/Filesystem.h" class TestHotkey : public CxxTest::TestSuite { CConfigDB* configDB; private: void fakeInput(const char* key, bool keyDown) { SDL_Event_ ev; ev.ev.type = keyDown ? SDL_KEYDOWN : SDL_KEYUP; ev.ev.key.repeat = 0; ev.ev.key.keysym.scancode = SDL_GetScancodeFromName(key); GlobalsInputHandler(&ev); HotkeyInputHandler(&ev); while(in_poll_priority_event(&ev)) HotkeyStateChange(&ev); } public: void setUp() { g_VFS = CreateVfs(); - TS_ASSERT_OK(g_VFS->Mount(L"config", DataDir()/"_testconfig")); - TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir()/"_testcache")); + TS_ASSERT_OK(g_VFS->Mount(L"config", DataDir() / "_testconfig" / "")); + TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir() / "_testcache" / "")); configDB = new CConfigDB; g_scancodes = {}; } void tearDown() { delete configDB; g_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); DeleteDirectory(DataDir()/"_testconfig"); } void test_Hotkeys() { configDB->SetValueString(CFG_SYSTEM, "hotkey.A", "A"); configDB->SetValueString(CFG_SYSTEM, "hotkey.AB", "A+B"); configDB->SetValueString(CFG_SYSTEM, "hotkey.ABC", "A+B+C"); configDB->SetValueList(CFG_SYSTEM, "hotkey.D", { "D", "D+E" }); configDB->WriteFile(CFG_SYSTEM, "config/conf.cfg"); configDB->Reload(CFG_SYSTEM); UnloadHotkeys(); LoadHotkeys(*configDB); TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false); /** * Simple check. */ fakeInput("A", true); TS_ASSERT_EQUALS(HotkeyIsPressed("A"), true); TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false); fakeInput("A", false); TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false); /** * Hotkey combinations: * - The most precise match only is selected * - Order does not matter. */ fakeInput("A", true); fakeInput("B", true); TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), true); TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false); fakeInput("A", false); fakeInput("B", false); TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false); fakeInput("B", true); fakeInput("A", true); // Activating the more precise hotkey AB untriggers "A" TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), true); TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false); fakeInput("A", false); fakeInput("B", false); TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false); fakeInput("A", true); fakeInput("B", true); fakeInput("B", false); TS_ASSERT_EQUALS(HotkeyIsPressed("A"), true); TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false); fakeInput("A", false); fakeInput("D", true); TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true); fakeInput("E", true); // Changing from one hotkey to another more specific combination of the same hotkey keeps it active TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true); fakeInput("E", false); // Likewise going the other way. TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true); } void test_quirk() { configDB->SetValueString(CFG_SYSTEM, "hotkey.A", "A"); configDB->SetValueString(CFG_SYSTEM, "hotkey.AB", "A+B"); configDB->SetValueString(CFG_SYSTEM, "hotkey.ABC", "A+B+C"); configDB->SetValueList(CFG_SYSTEM, "hotkey.D", { "D", "D+E" }); configDB->WriteFile(CFG_SYSTEM, "config/conf.cfg"); configDB->Reload(CFG_SYSTEM); UnloadHotkeys(); LoadHotkeys(*configDB); /** * Quirk of the implementation: hotkeys are allowed to fire with too many keys. * Further, hotkeys of the same specificity (i.e. same # of required keys) * are allowed to fire at the same time if they don't conflict. * This is required so that e.g. up+left scrolls both up and left at the same time. */ fakeInput("A", true); fakeInput("D", true); // A+D isn't a hotkey; both A and D are active. TS_ASSERT_EQUALS(HotkeyIsPressed("A"), true); TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true); fakeInput("C", true); // A+D+C likewise. TS_ASSERT_EQUALS(HotkeyIsPressed("A"), true); TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true); fakeInput("B", true); // Here D is inactivated because it's lower-specificity than A+B+C (with D being ignored). TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), true); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false); fakeInput("A", false); fakeInput("B", false); fakeInput("C", false); fakeInput("D", false); fakeInput("B", true); fakeInput("D", true); fakeInput("A", true); TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), true); TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false); TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false); UnloadHotkeys(); } }; Index: ps/trunk/source/scriptinterface/tests/test_ScriptConversions.h =================================================================== --- ps/trunk/source/scriptinterface/tests/test_ScriptConversions.h (revision 25106) +++ ps/trunk/source/scriptinterface/tests/test_ScriptConversions.h (revision 25107) @@ -1,272 +1,272 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "lib/self_test.h" #include "scriptinterface/ScriptInterface.h" #include "maths/Fixed.h" #include "maths/FixedVector2D.h" #include "maths/FixedVector3D.h" #include "maths/MathUtil.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "jsapi.h" class TestScriptConversions : public CxxTest::TestSuite { template void convert_to(const T& value, const std::string& expected) { ScriptInterface script("Test", "Test", g_ScriptContext); TS_ASSERT(script.LoadGlobalScripts()); ScriptRequest rq(script); JS::RootedValue v1(rq.cx); ScriptInterface::ToJSVal(rq, &v1, value); // We want to convert values to strings, but can't just call toSource() on them // since they might not be objects. So just use uneval. std::string source; JS::RootedValue global(rq.cx, rq.globalValue()); TS_ASSERT(script.CallFunction(global, "uneval", source, v1)); TS_ASSERT_STR_EQUALS(source, expected); } template void roundtrip(const T& value, const char* expected) { ScriptInterface script("Test", "Test", g_ScriptContext); TS_ASSERT(script.LoadGlobalScripts()); ScriptRequest rq(script); JS::RootedValue v1(rq.cx); ScriptInterface::ToJSVal(rq, &v1, value); std::string source; JS::RootedValue global(rq.cx, rq.globalValue()); TS_ASSERT(script.CallFunction(global, "uneval", source, v1)); if (expected) TS_ASSERT_STR_EQUALS(source, expected); T v2 = T(); TS_ASSERT(ScriptInterface::FromJSVal(rq, v1, v2)); TS_ASSERT_EQUALS(value, v2); } template void call_prototype_function(const T& u, const T& v, const std::string& func, const std::string& expected) { ScriptInterface script("Test", "Test", g_ScriptContext); TS_ASSERT(script.LoadGlobalScripts()); ScriptRequest rq(script); JS::RootedValue v1(rq.cx); ScriptInterface::ToJSVal(rq, &v1, v); JS::RootedValue u1(rq.cx); ScriptInterface::ToJSVal(rq, &u1, u); T r; JS::RootedValue r1(rq.cx); TS_ASSERT(script.CallFunction(u1, func.c_str(), r, v1)); ScriptInterface::ToJSVal(rq, &r1, r); std::string source; JS::RootedValue global(rq.cx, rq.globalValue()); TS_ASSERT(script.CallFunction(global, "uneval", source, r1)); TS_ASSERT_STR_EQUALS(source, expected); } public: void setUp() { g_VFS = CreateVfs(); - TS_ASSERT_OK(g_VFS->Mount(L"", DataDir()/"mods"/"_test.sim", VFS_MOUNT_MUST_EXIST)); + TS_ASSERT_OK(g_VFS->Mount(L"", DataDir() / "mods" / "_test.sim" / "", VFS_MOUNT_MUST_EXIST)); } void tearDown() { g_VFS.reset(); } void test_roundtrip() { roundtrip(true, "true"); roundtrip(false, "false"); roundtrip(0, "0"); roundtrip(0.5, "0.5"); roundtrip(1e9f, "1000000000"); roundtrip(1e30f, "1.0000000150474662e+30"); roundtrip(0, "0"); roundtrip(123, "123"); roundtrip(-123, "-123"); roundtrip(JSVAL_INT_MAX - 1, "2147483646"); roundtrip(JSVAL_INT_MAX, "2147483647"); roundtrip(JSVAL_INT_MIN + 1, "-2147483647"); roundtrip(JSVAL_INT_MIN, "-2147483648"); roundtrip(0, "0"); roundtrip(123, "123"); roundtrip(JSVAL_INT_MAX - 1, "2147483646"); roundtrip(JSVAL_INT_MAX, "2147483647"); roundtrip(static_cast(JSVAL_INT_MAX) + 1, "2147483648"); std::string s1 = "test"; s1[1] = '\0'; std::string s2 = "тест"; s2[2] = s2[3] = '\0'; std::wstring w1 = L"test"; w1[1] = '\0'; std::wstring w2 = L"тест"; w2[1] = '\0'; roundtrip("", "\"\""); roundtrip("test", "\"test\""); roundtrip("тест", "\"\\u0442\\u0435\\u0441\\u0442\""); roundtrip(s1, "\"t\\x00st\""); roundtrip(s2, "\"\\u0442\\x00\\x00\\u0441\\u0442\""); roundtrip(L"", "\"\""); roundtrip(L"test", "\"test\""); roundtrip(L"тест", "\"\\u0442\\u0435\\u0441\\u0442\""); roundtrip(w1, "\"t\\x00st\""); roundtrip(w2, "\"\\u0442\\x00\\u0441\\u0442\""); convert_to("", "\"\""); convert_to("test", "\"test\""); convert_to(s1.c_str(), "\"t\""); convert_to(s2.c_str(), "\"\\xD1\\x82\""); roundtrip(fixed::FromInt(0), "0"); roundtrip(fixed::FromInt(123), "123"); roundtrip(fixed::FromInt(-123), "-123"); roundtrip(fixed::FromDouble(123.25), "123.25"); } void test_integers() { ScriptInterface script("Test", "Test", g_ScriptContext); ScriptRequest rq(script); // using new uninitialized variables each time to be sure the test doesn't succeeed if ToJSVal doesn't touch the value at all. JS::RootedValue val0(rq.cx), val1(rq.cx), val2(rq.cx), val3(rq.cx), val4(rq.cx), val5(rq.cx), val6(rq.cx), val7(rq.cx), val8(rq.cx); ScriptInterface::ToJSVal(rq, &val0, 0); ScriptInterface::ToJSVal(rq, &val1, JSVAL_INT_MAX - 1); ScriptInterface::ToJSVal(rq, &val2, JSVAL_INT_MAX); ScriptInterface::ToJSVal(rq, &val3, JSVAL_INT_MIN + 1); ScriptInterface::ToJSVal(rq, &val4, -(i64)2147483648u); // JSVAL_INT_MIN TS_ASSERT(val0.isInt32()); TS_ASSERT(val1.isInt32()); TS_ASSERT(val2.isInt32()); TS_ASSERT(val3.isInt32()); TS_ASSERT(val4.isInt32()); ScriptInterface::ToJSVal(rq, &val5, 0); ScriptInterface::ToJSVal(rq, &val6, 2147483646u); // JSVAL_INT_MAX-1 ScriptInterface::ToJSVal(rq, &val7, 2147483647u); // JSVAL_INT_MAX ScriptInterface::ToJSVal(rq, &val8, 2147483648u); // JSVAL_INT_MAX+1 TS_ASSERT(val5.isInt32()); TS_ASSERT(val6.isInt32()); TS_ASSERT(val7.isInt32()); TS_ASSERT(val8.isDouble()); } void test_nonfinite() { roundtrip(std::numeric_limits::infinity(), "Infinity"); roundtrip(-std::numeric_limits::infinity(), "-Infinity"); convert_to(std::numeric_limits::quiet_NaN(), "NaN"); // can't use roundtrip since nan != nan ScriptInterface script("Test", "Test", g_ScriptContext); ScriptRequest rq(script); float f = 0; JS::RootedValue testNANVal(rq.cx); ScriptInterface::ToJSVal(rq, &testNANVal, NAN); TS_ASSERT(ScriptInterface::FromJSVal(rq, testNANVal, f)); TS_ASSERT(isnan(f)); } // NOTE: fixed and vector conversions are defined in simulation2/scripting/EngineScriptConversions.cpp void test_fixed() { fixed f; f.SetInternalValue(10590283); roundtrip(f, "161.5948944091797"); f.SetInternalValue(-10590283); roundtrip(f, "-161.5948944091797"); f.SetInternalValue(2000000000); roundtrip(f, "30517.578125"); f.SetInternalValue(2000000001); roundtrip(f, "30517.57814025879"); } void test_vector2d() { CFixedVector2D v(fixed::Zero(), fixed::Pi()); roundtrip(v, "({x:0, y:3.1415863037109375})"); CFixedVector2D u(fixed::FromInt(1), fixed::Zero()); call_prototype_function(u, v, "add", "({x:1, y:3.1415863037109375})"); } void test_vector3d() { CFixedVector3D v(fixed::Zero(), fixed::Pi(), fixed::FromInt(1)); roundtrip(v, "({x:0, y:3.1415863037109375, z:1})"); CFixedVector3D u(fixed::Pi(), fixed::Zero(), fixed::FromInt(2)); call_prototype_function(u, v, "add", "({x:3.1415863037109375, y:3.1415863037109375, z:3})"); } void test_utf8utf16_conversion() { // Fancier conversion: we store UTF8 and get UTF16 and vice-versa ScriptInterface script("Test", "Test", g_ScriptContext); TS_ASSERT(script.LoadGlobalScripts()); ScriptRequest rq(script); std::string in_utf8("éè!§$-aezi134900°°©©¢¢ÇÇ'{¶«¡Ç'[å»ÛÁØ"); std::wstring in_utf16(L"éè!§$-aezi134900°°©©¢¢ÇÇ'{¶«¡Ç'[å»ÛÁØ"); JS::RootedValue v1(rq.cx); ScriptInterface::ToJSVal(rq, &v1, in_utf8); std::wstring test_out_utf16; TS_ASSERT(ScriptInterface::FromJSVal(rq, v1, test_out_utf16)); TS_ASSERT_EQUALS(test_out_utf16, in_utf16); JS::RootedValue v2(rq.cx); ScriptInterface::ToJSVal(rq, &v2, in_utf16); std::string test_out_utf8; TS_ASSERT(ScriptInterface::FromJSVal(rq, v2, test_out_utf8)); TS_ASSERT_EQUALS(test_out_utf8, in_utf8); } }; Index: ps/trunk/source/simulation2/components/tests/test_Pathfinder.h =================================================================== --- ps/trunk/source/simulation2/components/tests/test_Pathfinder.h (revision 25106) +++ ps/trunk/source/simulation2/components/tests/test_Pathfinder.h (revision 25107) @@ -1,431 +1,431 @@ /* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "simulation2/system/ComponentTest.h" #include "simulation2/components/ICmpObstructionManager.h" #include "simulation2/components/ICmpPathfinder.h" #include "simulation2/helpers/Grid.h" #include "graphics/MapReader.h" #include "graphics/Terrain.h" #include "graphics/TerrainTextureManager.h" #include "lib/timer.h" #include "lib/tex/tex.h" #include "ps/Loader.h" #include "ps/Pyrogenesis.h" #include "scriptinterface/ScriptContext.h" #include "simulation2/Simulation2.h" #include class TestCmpPathfinder : public CxxTest::TestSuite { public: void setUp() { g_VFS = CreateVfs(); - g_VFS->Mount(L"", DataDir()/"mods"/"mod", VFS_MOUNT_MUST_EXIST); - g_VFS->Mount(L"", DataDir()/"mods"/"public", VFS_MOUNT_MUST_EXIST, 1); // ignore directory-not-found errors - TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir()/"_testcache")); + g_VFS->Mount(L"", DataDir() / "mods" / "mod" / "", VFS_MOUNT_MUST_EXIST); + g_VFS->Mount(L"", DataDir() / "mods" / "public" / "", VFS_MOUNT_MUST_EXIST, 1); // ignore directory-not-found errors + TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir() / "_testcache" / "", 0, VFS_MAX_PRIORITY)); CXeromyces::Startup(); // Need some stuff for terrain movement costs: // (TODO: this ought to be independent of any graphics code) new CTerrainTextureManager; g_TexMan.LoadTerrainTextures(); } void tearDown() { delete &g_TexMan; CXeromyces::Terminate(); g_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); } void test_namespace() { // Check that Pathfinding::NAVCELL_SIZE is actually an integer and that the definitions // of Pathfinding::NAVCELL_SIZE_INT and Pathfinding::NAVCELL_SIZE_LOG2 match TS_ASSERT_EQUALS(Pathfinding::NAVCELL_SIZE.ToInt_RoundToNegInfinity(), Pathfinding::NAVCELL_SIZE.ToInt_RoundToInfinity()); TS_ASSERT_EQUALS(Pathfinding::NAVCELL_SIZE.ToInt_RoundToNearest(), Pathfinding::NAVCELL_SIZE_INT); TS_ASSERT_EQUALS((Pathfinding::NAVCELL_SIZE >> 1).ToInt_RoundToZero(), Pathfinding::NAVCELL_SIZE_LOG2); } void test_pathgoal_nearest_distance() { entity_pos_t i = Pathfinding::NAVCELL_SIZE; CFixedVector2D u(i*1, i*0); CFixedVector2D v(i*0, i*1); { PathGoal goal = { PathGoal::POINT, i*8, i*6 }; TS_ASSERT_EQUALS(goal.NearestPointOnGoal(u*8 + v*4), u*8 + v*6); TS_ASSERT_EQUALS(goal.DistanceToPoint(u*8 + v*4), i*2); TS_ASSERT_EQUALS(goal.NearestPointOnGoal(u*0 + v*0), u*8 + v*6); TS_ASSERT_EQUALS(goal.DistanceToPoint(u*0 + v*0), i*10); TS_ASSERT(goal.RectContainsGoal(i*4, i*3, i*12, i*9)); TS_ASSERT(goal.RectContainsGoal(i*4, i*3, i*8, i*6)); TS_ASSERT(goal.RectContainsGoal(i*8, i*6, i*12, i*9)); TS_ASSERT(!goal.RectContainsGoal(i*4, i*3, i*7, i*5)); TS_ASSERT(!goal.RectContainsGoal(i*9, i*7, i*13, i*15)); } { PathGoal goal = { PathGoal::CIRCLE, i*8, i*6, i*5 }; TS_ASSERT_EQUALS(goal.NearestPointOnGoal(u*8 + v*4), u*8 + v*4); TS_ASSERT_EQUALS(goal.DistanceToPoint(u*8 + v*4), i*0); TS_ASSERT_EQUALS(goal.NearestPointOnGoal(u*0 + v*0), u*4 + v*3); TS_ASSERT_EQUALS(goal.DistanceToPoint(u*0 + v*0), i*5); TS_ASSERT(goal.RectContainsGoal(i*7, i*5, i*9, i*7)); // fully inside TS_ASSERT(goal.RectContainsGoal(i*3, i*1, i*13, i*11)); // fully outside TS_ASSERT(goal.RectContainsGoal(i*4, i*3, i*8, i*6)); // partially inside TS_ASSERT(goal.RectContainsGoal(i*4, i*0, i*12, i*1)); // touching the edge } { PathGoal goal = { PathGoal::INVERTED_CIRCLE, i*8, i*6, i*5 }; TS_ASSERT_EQUALS(goal.NearestPointOnGoal(u*8 + v*4), u*8 + v*1); TS_ASSERT_EQUALS(goal.DistanceToPoint(u*8 + v*4), i*3); TS_ASSERT_EQUALS(goal.NearestPointOnGoal(u*0 + v*0), u*0 + v*0); TS_ASSERT_EQUALS(goal.DistanceToPoint(u*0 + v*0), i*0); TS_ASSERT(!goal.RectContainsGoal(i*7, i*5, i*9, i*7)); // fully inside TS_ASSERT(goal.RectContainsGoal(i*3, i*1, i*13, i*11)); // fully outside TS_ASSERT(goal.RectContainsGoal(i*4, i*3, i*8, i*6)); // partially inside TS_ASSERT(goal.RectContainsGoal(i*4, i*0, i*12, i*1)); // touching the edge } { PathGoal goal = { PathGoal::SQUARE, i*8, i*6, i*4, i*3, u, v }; TS_ASSERT_EQUALS(goal.NearestPointOnGoal(u*8 + v*4), u*8 + v*4); TS_ASSERT_EQUALS(goal.DistanceToPoint(u*8 + v*4), i*0); TS_ASSERT_EQUALS(goal.NearestPointOnGoal(u*0 + v*0), u*4 + v*3); TS_ASSERT_EQUALS(goal.DistanceToPoint(u*0 + v*0), i*5); TS_ASSERT(goal.RectContainsGoal(i*7, i*5, i*9, i*7)); // fully inside TS_ASSERT(goal.RectContainsGoal(i*3, i*1, i*13, i*11)); // fully outside TS_ASSERT(goal.RectContainsGoal(i*4, i*3, i*8, i*6)); // partially inside TS_ASSERT(goal.RectContainsGoal(i*4, i*2, i*12, i*3)); // touching the edge TS_ASSERT(goal.RectContainsGoal(i*3, i*0, i*4, i*10)); // touching the edge } { PathGoal goal = { PathGoal::INVERTED_SQUARE, i*8, i*6, i*4, i*3, u, v }; TS_ASSERT_EQUALS(goal.NearestPointOnGoal(u*8 + v*4), u*8 + v*3); TS_ASSERT_EQUALS(goal.DistanceToPoint(u*8 + v*4), i*1); TS_ASSERT_EQUALS(goal.NearestPointOnGoal(u*0 + v*0), u*0 + v*0); TS_ASSERT_EQUALS(goal.DistanceToPoint(u*0 + v*0), i*0); TS_ASSERT(!goal.RectContainsGoal(i*7, i*5, i*9, i*7)); // fully inside TS_ASSERT(goal.RectContainsGoal(i*3, i*1, i*13, i*11)); // fully outside TS_ASSERT(!goal.RectContainsGoal(i*4, i*3, i*8, i*6)); // inside, touching (should fail) TS_ASSERT(goal.RectContainsGoal(i*4, i*2, i*12, i*3)); // touching the edge TS_ASSERT(goal.RectContainsGoal(i*3, i*0, i*4, i*10)); // touching the edge } } void test_performance_DISABLED() { CTerrain terrain; CSimulation2 sim2(NULL, g_ScriptContext, &terrain); sim2.LoadDefaultScripts(); sim2.ResetState(); std::unique_ptr mapReader = std::make_unique(); LDR_BeginRegistering(); mapReader->LoadMap(L"maps/skirmishes/Median Oasis (2).pmp", *sim2.GetScriptInterface().GetContext(), JS::UndefinedHandleValue, &terrain, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &sim2, &sim2.GetSimContext(), -1, false); LDR_EndRegistering(); TS_ASSERT_OK(LDR_NonprogressiveLoad()); sim2.PreInitGame(); sim2.InitGame(); sim2.Update(0); CmpPtr cmp(sim2, SYSTEM_ENTITY); #if 0 entity_pos_t x0 = entity_pos_t::FromInt(10); entity_pos_t z0 = entity_pos_t::FromInt(495); entity_pos_t x1 = entity_pos_t::FromInt(500); entity_pos_t z1 = entity_pos_t::FromInt(495); ICmpPathfinder::Goal goal = { ICmpPathfinder::Goal::POINT, x1, z1 }; WaypointPath path; cmp->ComputePath(x0, z0, goal, cmp->GetPassabilityClass("default"), path); for (size_t i = 0; i < path.m_Waypoints.size(); ++i) printf("%d: %f %f\n", (int)i, path.m_Waypoints[i].x.ToDouble(), path.m_Waypoints[i].z.ToDouble()); #endif double t = timer_Time(); srand(1234); for (size_t j = 0; j < 1024*2; ++j) { entity_pos_t x0 = entity_pos_t::FromInt(rand() % 512); entity_pos_t z0 = entity_pos_t::FromInt(rand() % 512); entity_pos_t x1 = x0 + entity_pos_t::FromInt(rand() % 64); entity_pos_t z1 = z0 + entity_pos_t::FromInt(rand() % 64); PathGoal goal = { PathGoal::POINT, x1, z1 }; WaypointPath path; cmp->ComputePathImmediate(x0, z0, goal, cmp->GetPassabilityClass("default"), path); } t = timer_Time() - t; printf("[%f]", t); } void test_performance_short_DISABLED() { CTerrain terrain; terrain.Initialize(5, NULL); CSimulation2 sim2(NULL, g_ScriptContext, &terrain); sim2.LoadDefaultScripts(); sim2.ResetState(); const entity_pos_t range = entity_pos_t::FromInt(TERRAIN_TILE_SIZE*12); CmpPtr cmpObstructionMan(sim2, SYSTEM_ENTITY); CmpPtr cmpPathfinder(sim2, SYSTEM_ENTITY); srand(0); for (size_t i = 0; i < 200; ++i) { fixed x = fixed::FromFloat(1.5f*range.ToFloat() * rand()/(float)RAND_MAX); fixed z = fixed::FromFloat(1.5f*range.ToFloat() * rand()/(float)RAND_MAX); // printf("# %f %f\n", x.ToFloat(), z.ToFloat()); cmpObstructionMan->AddUnitShape(INVALID_ENTITY, x, z, fixed::FromInt(2), 0, INVALID_ENTITY); } PathGoal goal = { PathGoal::POINT, range, range }; WaypointPath path = cmpPathfinder->ComputeShortPathImmediate(ShortPathRequest{ 0, range/3, range/3, fixed::FromInt(2), range, goal, 0, false, 0, 0 }); for (size_t i = 0; i < path.m_Waypoints.size(); ++i) printf("# %d: %f %f\n", (int)i, path.m_Waypoints[i].x.ToFloat(), path.m_Waypoints[i].z.ToFloat()); } template void DumpGrid(std::ostream& stream, const Grid& grid, int mask) { for (u16 j = 0; j < grid.m_H; ++j) { for (u16 i = 0; i < grid.m_W; ) { if (!(grid.get(i, j) & mask)) { i++; continue; } u16 i0 = i; for (i = i0+1; ; ++i) { if (i >= grid.m_W || !(grid.get(i, j) & mask)) { stream << " \n"; break; } } } } } void test_perf2_DISABLED() { CTerrain terrain; CSimulation2 sim2(NULL, g_ScriptContext, &terrain); sim2.LoadDefaultScripts(); sim2.ResetState(); std::unique_ptr mapReader = std::make_unique(); LDR_BeginRegistering(); mapReader->LoadMap(L"maps/scenarios/Peloponnese.pmp", *sim2.GetScriptInterface().GetContext(), JS::UndefinedHandleValue, &terrain, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &sim2, &sim2.GetSimContext(), -1, false); LDR_EndRegistering(); TS_ASSERT_OK(LDR_NonprogressiveLoad()); sim2.PreInitGame(); sim2.InitGame(); sim2.Update(0); std::ofstream stream(OsString("perf2.html").c_str(), std::ofstream::out | std::ofstream::trunc); CmpPtr cmpObstructionManager(sim2, SYSTEM_ENTITY); CmpPtr cmpPathfinder(sim2, SYSTEM_ENTITY); pass_class_t obstructionsMask = cmpPathfinder->GetPassabilityClass("default"); const Grid& obstructions = cmpPathfinder->GetPassabilityGrid(); int scale = 1; stream << "\n"; stream << "\n"; stream << "\n"; stream << "\n"; stream << " \n"; DumpGrid(stream, obstructions, obstructionsMask); stream << " \n"; DumpPath(stream, 128*4, 256*4, 128*4, 384*4, cmpPathfinder); // RepeatPath(500, 128*4, 256*4, 128*4, 384*4, cmpPathfinder); // // DumpPath(stream, 128*4, 204*4, 192*4, 204*4, cmpPathfinder); // // DumpPath(stream, 128*4, 230*4, 32*4, 230*4, cmpPathfinder); stream << "\n"; stream << "\n"; } void test_perf3_DISABLED() { CTerrain terrain; CSimulation2 sim2(NULL, g_ScriptContext, &terrain); sim2.LoadDefaultScripts(); sim2.ResetState(); std::unique_ptr mapReader = std::make_unique(); LDR_BeginRegistering(); mapReader->LoadMap(L"maps/scenarios/Peloponnese.pmp", *sim2.GetScriptInterface().GetContext(), JS::UndefinedHandleValue, &terrain, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &sim2, &sim2.GetSimContext(), -1, false); LDR_EndRegistering(); TS_ASSERT_OK(LDR_NonprogressiveLoad()); sim2.PreInitGame(); sim2.InitGame(); sim2.Update(0); std::ofstream stream(OsString("perf3.html").c_str(), std::ofstream::out | std::ofstream::trunc); CmpPtr cmpObstructionManager(sim2, SYSTEM_ENTITY); CmpPtr cmpPathfinder(sim2, SYSTEM_ENTITY); pass_class_t obstructionsMask = cmpPathfinder->GetPassabilityClass("default"); const Grid& obstructions = cmpPathfinder->GetPassabilityGrid(); int scale = 31; stream << "\n"; stream << "\n"; stream << "\n"; stream << "\n"; stream << "\n"; stream << "\n"; stream << "\n"; stream << " \n"; DumpGrid(stream, obstructions, obstructionsMask); stream << " \n"; for (int j = 160; j < 190; ++j) for (int i = 220; i < 290; ++i) DumpPath(stream, 230, 178, i, j, cmpPathfinder); stream << "\n"; stream << "\n"; stream << "\n"; } void DumpPath(std::ostream& stream, int i0, int j0, int i1, int j1, CmpPtr& cmpPathfinder) { entity_pos_t x0 = entity_pos_t::FromInt(i0); entity_pos_t z0 = entity_pos_t::FromInt(j0); entity_pos_t x1 = entity_pos_t::FromInt(i1); entity_pos_t z1 = entity_pos_t::FromInt(j1); PathGoal goal = { PathGoal::POINT, x1, z1 }; WaypointPath path; cmpPathfinder->ComputePathImmediate(x0, z0, goal, cmpPathfinder->GetPassabilityClass("default"), path); u32 debugSteps; double debugTime; Grid debugGrid; cmpPathfinder->GetDebugData(debugSteps, debugTime, debugGrid); // stream << " \n"; stream << " \n"; // stream << " \n"; // DumpGrid(stream, debugGrid, 1); // stream << " \n"; // stream << " \n"; // DumpGrid(stream, debugGrid, 2); // stream << " \n"; // stream << " \n"; // DumpGrid(stream, debugGrid, 3); // stream << " \n"; stream << " \n"; stream << " \n"; } void RepeatPath(int n, int i0, int j0, int i1, int j1, CmpPtr& cmpPathfinder) { entity_pos_t x0 = entity_pos_t::FromInt(i0); entity_pos_t z0 = entity_pos_t::FromInt(j0); entity_pos_t x1 = entity_pos_t::FromInt(i1); entity_pos_t z1 = entity_pos_t::FromInt(j1); PathGoal goal = { PathGoal::POINT, x1, z1 }; double t = timer_Time(); for (int i = 0; i < n; ++i) { WaypointPath path; cmpPathfinder->ComputePathImmediate(x0, z0, goal, cmpPathfinder->GetPassabilityClass("default"), path); } t = timer_Time() - t; debug_printf("### RepeatPath %fms each (%fs total)\n", 1000*t / n, t); } }; Index: ps/trunk/source/simulation2/tests/test_CmpTemplateManager.h =================================================================== --- ps/trunk/source/simulation2/tests/test_CmpTemplateManager.h (revision 25106) +++ ps/trunk/source/simulation2/tests/test_CmpTemplateManager.h (revision 25107) @@ -1,257 +1,257 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "lib/self_test.h" #include "simulation2/system/ComponentManager.h" #include "simulation2/components/ICmpTemplateManager.h" #include "simulation2/components/ICmpTest.h" #include "simulation2/MessageTypes.h" #include "simulation2/system/ParamNode.h" #include "simulation2/system/SimContext.h" #include "simulation2/Simulation2.h" #include "graphics/Terrain.h" #include "ps/Filesystem.h" #include "ps/CLogger.h" #include "ps/XML/Xeromyces.h" class TestCmpTemplateManager : public CxxTest::TestSuite { public: void setUp() { g_VFS = CreateVfs(); - TS_ASSERT_OK(g_VFS->Mount(L"", DataDir()/"mods"/"_test.sim", VFS_MOUNT_MUST_EXIST)); - TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir()/"_testcache")); + TS_ASSERT_OK(g_VFS->Mount(L"", DataDir() / "mods" / "_test.sim" / "", VFS_MOUNT_MUST_EXIST)); + TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir() / "_testcache" / "", 0, VFS_MAX_PRIORITY)); CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); g_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); } void test_LoadTemplate() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.LookupEntityHandle(ent1, true); CParamNode noParam; TS_ASSERT(man.AddComponent(hnd1, CID_TemplateManager, noParam)); ICmpTemplateManager* tempMan = static_cast (man.QueryInterface(ent1, IID_TemplateManager)); TS_ASSERT(tempMan != NULL); const CParamNode* basic = tempMan->LoadTemplate(ent2, "basic"); TS_ASSERT(basic != NULL); TS_ASSERT_WSTR_EQUALS(basic->ToXML(), L"12345"); const CParamNode* inherit2 = tempMan->LoadTemplate(ent2, "inherit2"); TS_ASSERT(inherit2 != NULL); TS_ASSERT_WSTR_EQUALS(inherit2->ToXML(), L"d2e1f1g2"); const CParamNode* inherit1 = tempMan->LoadTemplate(ent2, "inherit1"); TS_ASSERT(inherit1 != NULL); TS_ASSERT_WSTR_EQUALS(inherit1->ToXML(), L"d1e1f1"); const CParamNode* actor = tempMan->LoadTemplate(ent2, "actor|example1"); TS_ASSERT(actor != NULL); TS_ASSERT_WSTR_EQUALS(actor->ToXML(), L"1.0128x128/ellipse.png128x128/ellipse_mask.png" L"example1falsefalsefalse"); } void test_LoadTemplate_scriptcache() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.LookupEntityHandle(ent1, true); CParamNode noParam; TS_ASSERT(man.AddComponent(hnd1, CID_TemplateManager, noParam)); ICmpTemplateManager* tempMan = static_cast (man.QueryInterface(ent1, IID_TemplateManager)); TS_ASSERT(tempMan != NULL); tempMan->DisableValidation(); ScriptRequest rq(man.GetScriptInterface()); // This is testing some bugs in the template JS object caching const CParamNode* inherit1 = tempMan->LoadTemplate(ent2, "inherit1"); JS::RootedValue val(rq.cx); ScriptInterface::ToJSVal(rq, &val, inherit1); TS_ASSERT_STR_EQUALS(man.GetScriptInterface().ToString(&val), "({Test1A:{'@a':\"a1\", '@b':\"b1\", '@c':\"c1\", d:\"d1\", e:\"e1\", f:\"f1\"}})"); const CParamNode* inherit2 = tempMan->LoadTemplate(ent2, "inherit2"); ScriptInterface::ToJSVal(rq, &val, inherit2); TS_ASSERT_STR_EQUALS(man.GetScriptInterface().ToString(&val), "({'@parent':\"inherit1\", Test1A:{'@a':\"a2\", '@b':\"b1\", '@c':\"c1\", d:\"d2\", e:\"e1\", f:\"f1\", g:\"g2\"}})"); const CParamNode* actor = tempMan->LoadTemplate(ent2, "actor|example1"); ScriptInterface::ToJSVal(rq, &val, &actor->GetChild("VisualActor")); TS_ASSERT_STR_EQUALS(man.GetScriptInterface().ToString(&val), "({Actor:\"example1\", ActorOnly:(void 0), SilhouetteDisplay:\"false\", SilhouetteOccluder:\"false\", VisibleInAtlasOnly:\"false\"})"); const CParamNode* foundation = tempMan->LoadTemplate(ent2, "foundation|actor|example1"); ScriptInterface::ToJSVal(rq, &val, &foundation->GetChild("VisualActor")); TS_ASSERT_STR_EQUALS(man.GetScriptInterface().ToString(&val), "({Actor:\"example1\", ActorOnly:(void 0), Foundation:(void 0), SilhouetteDisplay:\"false\", SilhouetteOccluder:\"false\", VisibleInAtlasOnly:\"false\"})"); #define GET_FIRST_ELEMENT(n, templateName) \ const CParamNode* n = tempMan->LoadTemplate(ent2, templateName); \ for (CParamNode::ChildrenMap::const_iterator it = n->GetChildren().begin(); it != n->GetChildren().end(); ++it) \ { \ if (it->first[0] == '@') \ continue; \ ScriptInterface::ToJSVal(rq, &val, it->second); \ break; \ } GET_FIRST_ELEMENT(n1, "inherit_a"); TS_ASSERT_STR_EQUALS(man.GetScriptInterface().ToString(&val), "({'@datatype':\"tokens\", _string:\"a b c\"})"); GET_FIRST_ELEMENT(n2, "inherit_b"); TS_ASSERT_STR_EQUALS(man.GetScriptInterface().ToString(&val), "({'@datatype':\"tokens\", _string:\"a b c d\"})"); GET_FIRST_ELEMENT(n3, "inherit_c"); TS_ASSERT_STR_EQUALS(man.GetScriptInterface().ToString(&val), "({'@a':\"b\", _string:\"a b c\"})"); GET_FIRST_ELEMENT(n4, "inherit_d"); TS_ASSERT_STR_EQUALS(man.GetScriptInterface().ToString(&val), "({'@a':\"b\", '@c':\"d\"})"); #undef GET_FIRST_ELEMENT } void test_LoadTemplate_errors() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.LookupEntityHandle(ent1, true); CParamNode noParam; TS_ASSERT(man.AddComponent(hnd1, CID_TemplateManager, noParam)); ICmpTemplateManager* tempMan = static_cast (man.QueryInterface(ent1, IID_TemplateManager)); TS_ASSERT(tempMan != NULL); TestLogger logger; TS_ASSERT(tempMan->LoadTemplate(ent2, "illformed") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "invalid") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "nonexistent") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "inherit-loop") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "inherit-broken") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "inherit-special") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "preview|nonexistent") == NULL); } void test_LoadTemplate_multiple() { CSimContext context; CComponentManager man(context, g_ScriptContext); man.LoadComponentTypes(); entity_id_t ent1 = 1, ent2 = 2; CEntityHandle hnd1 = man.LookupEntityHandle(ent1, true); CParamNode noParam; TS_ASSERT(man.AddComponent(hnd1, CID_TemplateManager, noParam)); ICmpTemplateManager* tempMan = static_cast (man.QueryInterface(ent1, IID_TemplateManager)); TS_ASSERT(tempMan != NULL); const CParamNode* basicA = tempMan->LoadTemplate(ent2, "basic"); TS_ASSERT(basicA != NULL); const CParamNode* basicB = tempMan->LoadTemplate(ent2, "basic"); TS_ASSERT(basicA == basicB); const CParamNode* inherit2A = tempMan->LoadTemplate(ent2, "inherit2"); TS_ASSERT(inherit2A != NULL); const CParamNode* inherit2B = tempMan->LoadTemplate(ent2, "inherit2"); TS_ASSERT(inherit2A == inherit2B); TestLogger logger; TS_ASSERT(tempMan->LoadTemplate(ent2, "nonexistent") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "nonexistent") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "inherit-loop") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "inherit-loop") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "inherit-broken") == NULL); TS_ASSERT(tempMan->LoadTemplate(ent2, "inherit-broken") == NULL); } }; class TestCmpTemplateManager_2 : public CxxTest::TestSuite { public: void setUp() { g_VFS = CreateVfs(); - TS_ASSERT_OK(g_VFS->Mount(L"", DataDir()/"mods"/"mod", VFS_MOUNT_MUST_EXIST)); - TS_ASSERT_OK(g_VFS->Mount(L"", DataDir()/"mods"/"public", VFS_MOUNT_MUST_EXIST)); - TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir()/"_testcache")); + TS_ASSERT_OK(g_VFS->Mount(L"", DataDir() / "mods" / "mod" / "", VFS_MOUNT_MUST_EXIST)); + TS_ASSERT_OK(g_VFS->Mount(L"", DataDir() / "mods" / "public" / "", VFS_MOUNT_MUST_EXIST)); + TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir()/"_testcache" / "", 0, VFS_MAX_PRIORITY)); CXeromyces::Startup(); } void tearDown() { CXeromyces::Terminate(); g_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); } // This just attempts loading every public entity, to check there's no validation errors void test_load_all_DISABLED() // disabled since it's a bit slow and noisy { CTerrain dummy; CSimulation2 sim(NULL, g_ScriptContext, &dummy); sim.LoadDefaultScripts(); sim.ResetState(); CmpPtr cmpTemplateManager(sim, SYSTEM_ENTITY); TS_ASSERT(cmpTemplateManager); std::vector templates = cmpTemplateManager->FindAllTemplates(true); for (size_t i = 0; i < templates.size(); ++i) { std::string name = templates[i]; printf("# %s\n", name.c_str()); const CParamNode* p = cmpTemplateManager->GetTemplate(name); TS_ASSERT(p != NULL); } } }; Index: ps/trunk/source/simulation2/tests/test_Serializer.h =================================================================== --- ps/trunk/source/simulation2/tests/test_Serializer.h (revision 25106) +++ ps/trunk/source/simulation2/tests/test_Serializer.h (revision 25107) @@ -1,936 +1,936 @@ -/* Copyright (C) 2020 Wildfire Games. +/* Copyright (C) 2021 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "lib/self_test.h" #include "simulation2/serialization/DebugSerializer.h" #include "simulation2/serialization/HashSerializer.h" #include "simulation2/serialization/StdSerializer.h" #include "simulation2/serialization/StdDeserializer.h" #include "scriptinterface/ScriptContext.h" #include "scriptinterface/ScriptInterface.h" #include "graphics/MapReader.h" #include "graphics/Terrain.h" #include "graphics/TerrainTextureManager.h" #include "lib/timer.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Loader.h" #include "ps/XML/Xeromyces.h" #include "simulation2/Simulation2.h" #include "callgrind.h" #include #define TS_ASSERT_STREAM(stream, len, buffer) \ TS_ASSERT_EQUALS(stream.str().length(), (size_t)len); \ TS_ASSERT_SAME_DATA(stream.str().data(), buffer, len) #define TSM_ASSERT_STREAM(m, stream, len, buffer) \ TSM_ASSERT_EQUALS(m, stream.str().length(), (size_t)len); \ TSM_ASSERT_SAME_DATA(m, stream.str().data(), buffer, len) class TestSerializer : public CxxTest::TestSuite { public: void serialize_types(ISerializer& serialize) { serialize.NumberI8_Unbounded("i8", (signed char)-123); serialize.NumberU8_Unbounded("u8", (unsigned char)255); serialize.NumberI16_Unbounded("i16", -12345); serialize.NumberU16_Unbounded("u16", 56789); serialize.NumberI32_Unbounded("i32", -123); serialize.NumberU32_Unbounded("u32", (unsigned)-123); serialize.NumberFloat_Unbounded("float", 1e+30f); serialize.NumberDouble_Unbounded("double", 1e+300); serialize.NumberFixed_Unbounded("fixed", fixed::FromFloat(1234.5f)); serialize.Bool("t", true); serialize.Bool("f", false); serialize.StringASCII("string", "example", 0, 255); serialize.StringASCII("string 2", "example\"\\\"", 0, 255); serialize.StringASCII("string 3", "example\n\ntest", 0, 255); wchar_t testw[] = { 't', 0xEA, 's', 't', 0 }; serialize.String("string 4", testw, 0, 255); serialize.RawBytes("raw bytes", (const u8*)"\0\1\2\3\x0f\x10", 6); } void test_Debug_basic() { ScriptInterface script("Test", "Test", g_ScriptContext); std::stringstream stream; CDebugSerializer serialize(script, stream); serialize.NumberI32_Unbounded("x", -123); serialize.NumberU32_Unbounded("y", 1234); serialize.NumberI32("z", 12345, 0, 65535); TS_ASSERT_STR_EQUALS(stream.str(), "x: -123\ny: 1234\nz: 12345\n"); } void test_Debug_floats() { ScriptInterface script("Test", "Test", g_ScriptContext); std::stringstream stream; CDebugSerializer serialize(script, stream); serialize.NumberFloat_Unbounded("x", 1e4f); serialize.NumberFloat_Unbounded("x", 1e-4f); serialize.NumberFloat_Unbounded("x", 1e5f); serialize.NumberFloat_Unbounded("x", 1e-5f); serialize.NumberFloat_Unbounded("x", 1e6f); serialize.NumberFloat_Unbounded("x", 1e-6f); serialize.NumberFloat_Unbounded("x", 1e10f); serialize.NumberFloat_Unbounded("x", 1e-10f); serialize.NumberDouble_Unbounded("x", 1e4); serialize.NumberDouble_Unbounded("x", 1e-4); serialize.NumberDouble_Unbounded("x", 1e5); serialize.NumberDouble_Unbounded("x", 1e-5); serialize.NumberDouble_Unbounded("x", 1e6); serialize.NumberDouble_Unbounded("x", 1e-6); serialize.NumberDouble_Unbounded("x", 1e10); serialize.NumberDouble_Unbounded("x", 1e-10); serialize.NumberDouble_Unbounded("x", 1e100); serialize.NumberDouble_Unbounded("x", 1e-100); serialize.NumberFixed_Unbounded("x", fixed::FromDouble(1e4)); TS_ASSERT_STR_EQUALS(stream.str(), "x: 10000\nx: 9.9999997e-05\nx: 100000\nx: 9.9999997e-06\nx: 1000000\nx: 1e-06\nx: 1e+10\nx: 1e-10\n" "x: 10000\nx: 0.0001\nx: 100000\nx: 1.0000000000000001e-05\nx: 1000000\nx: 9.9999999999999995e-07\nx: 10000000000\nx: 1e-10\nx: 1e+100\nx: 1e-100\n" "x: 10000\n" ); } void test_Debug_types() { ScriptInterface script("Test", "Test", g_ScriptContext); std::stringstream stream; CDebugSerializer serialize(script, stream); serialize.Comment("comment"); serialize_types(serialize); TS_ASSERT_STR_EQUALS(stream.str(), "# comment\n" "i8: -123\n" "u8: 255\n" "i16: -12345\n" "u16: 56789\n" "i32: -123\n" "u32: 4294967173\n" "float: 1e+30\n" "double: 1.0000000000000001e+300\n" "fixed: 1234.5\n" "t: true\n" "f: false\n" "string: \"example\"\n" "string 2: \"example\\\"\\\\\\\"\"\n" // C-escaped form of: "example\"\\\"" "string 3: \"example\\n\\ntest\"\n" "string 4: \"t\xC3\xAAst\"\n" "raw bytes: (6 bytes) 00 01 02 03 0f 10\n" ); } void test_Std_basic() { ScriptInterface script("Test", "Test", g_ScriptContext); std::stringstream stream; CStdSerializer serialize(script, stream); serialize.NumberI32_Unbounded("x", -123); serialize.NumberU32_Unbounded("y", 1234); serialize.NumberI32("z", 12345, 0, 65535); TS_ASSERT_STREAM(stream, 12, "\x85\xff\xff\xff" "\xd2\x04\x00\x00" "\x39\x30\x00\x00"); CStdDeserializer deserialize(script, stream); int32_t n; deserialize.NumberI32_Unbounded("x", n); TS_ASSERT_EQUALS(n, -123); deserialize.NumberI32_Unbounded("y", n); TS_ASSERT_EQUALS(n, 1234); deserialize.NumberI32("z", n, 0, 65535); TS_ASSERT_EQUALS(n, 12345); // NOTE: Don't use good() here - it fails due to a bug in older libc++ versions TS_ASSERT(!stream.bad() && !stream.fail()); TS_ASSERT_EQUALS(stream.peek(), EOF); } void test_Std_types() { ScriptInterface script("Test", "Test", g_ScriptContext); std::stringstream stream; CStdSerializer serialize(script, stream); serialize_types(serialize); CStdDeserializer deserialize(script, stream); int8_t i8v; uint8_t u8v; int16_t i16v; uint16_t u16v; int32_t i32v; uint32_t u32v; float flt; double dbl; fixed fxd; bool bl; std::string str; std::wstring wstr; u8 cbuf[256]; deserialize.NumberI8_Unbounded("i8", i8v); TS_ASSERT_EQUALS(i8v, -123); deserialize.NumberU8_Unbounded("u8", u8v); TS_ASSERT_EQUALS(u8v, 255); deserialize.NumberI16_Unbounded("i16", i16v); TS_ASSERT_EQUALS(i16v, -12345); deserialize.NumberU16_Unbounded("u16", u16v); TS_ASSERT_EQUALS(u16v, 56789); deserialize.NumberI32_Unbounded("i32", i32v); TS_ASSERT_EQUALS(i32v, -123); deserialize.NumberU32_Unbounded("u32", u32v); TS_ASSERT_EQUALS(u32v, 4294967173u); deserialize.NumberFloat_Unbounded("float", flt); TS_ASSERT_EQUALS(flt, 1e+30f); deserialize.NumberDouble_Unbounded("double", dbl); TS_ASSERT_EQUALS(dbl, 1e+300); deserialize.NumberFixed_Unbounded("fixed", fxd); TS_ASSERT_EQUALS(fxd.ToDouble(), 1234.5); deserialize.Bool("t", bl); TS_ASSERT_EQUALS(bl, true); deserialize.Bool("f", bl); TS_ASSERT_EQUALS(bl, false); deserialize.StringASCII("string", str, 0, 255); TS_ASSERT_STR_EQUALS(str, "example"); deserialize.StringASCII("string 2", str, 0, 255); TS_ASSERT_STR_EQUALS(str, "example\"\\\""); deserialize.StringASCII("string 3", str, 0, 255); TS_ASSERT_STR_EQUALS(str, "example\n\ntest"); wchar_t testw[] = { 't', 0xEA, 's', 't', 0 }; deserialize.String("string 4", wstr, 0, 255); TS_ASSERT_WSTR_EQUALS(wstr, testw); cbuf[6] = 0x42; // sentinel deserialize.RawBytes("raw bytes", cbuf, 6); TS_ASSERT_SAME_DATA(cbuf, (const u8*)"\0\1\2\3\x0f\x10\x42", 7); // NOTE: Don't use good() here - it fails due to a bug in older libc++ versions TS_ASSERT(!stream.bad() && !stream.fail()); TS_ASSERT_EQUALS(stream.peek(), EOF); } void test_Hash_basic() { ScriptInterface script("Test", "Test", g_ScriptContext); CHashSerializer serialize(script); serialize.NumberI32_Unbounded("x", -123); serialize.NumberU32_Unbounded("y", 1234); serialize.NumberI32("z", 12345, 0, 65535); TS_ASSERT_EQUALS(serialize.GetHashLength(), (size_t)16); TS_ASSERT_SAME_DATA(serialize.ComputeHash(), "\xa0\x3a\xe5\x3e\x9b\xd7\xfb\x11\x88\x35\xc6\xfb\xb9\x94\xa9\x72", 16); // echo -en "\x85\xff\xff\xff\xd2\x04\x00\x00\x39\x30\x00\x00" | openssl md5 -binary | xxd -p | perl -pe 's/(..)/\\x$1/g' } void test_Hash_stream() { ScriptInterface script("Test", "Test", g_ScriptContext); CHashSerializer hashSerialize(script); hashSerialize.NumberI32_Unbounded("x", -123); hashSerialize.NumberU32_Unbounded("y", 1234); hashSerialize.NumberI32("z", 12345, 0, 65535); ISerializer& serialize = hashSerialize; { CStdSerializer streamSerialize(script, serialize.GetStream()); streamSerialize.NumberI32_Unbounded("x2", -456); streamSerialize.NumberU32_Unbounded("y2", 5678); streamSerialize.NumberI32("z2", 45678, 0, 65535); } TS_ASSERT_EQUALS(hashSerialize.GetHashLength(), (size_t)16); TS_ASSERT_SAME_DATA(hashSerialize.ComputeHash(), "\x5c\xff\x33\xd1\x72\xdd\x6d\x77\xa8\xd4\xa1\xf6\x84\xcc\xaa\x10", 16); // echo -en "\x85\xff\xff\xff\xd2\x04\x00\x00\x39\x30\x00\x00\x38\xfe\xff\xff\x2e\x16\x00\x00\x6e\xb2\x00\x00" | openssl md5 -binary | xxd -p | perl -pe 's/(..)/\\x$1/g' } void test_bounds() { ScriptInterface script("Test", "Test", g_ScriptContext); std::stringstream stream; CDebugSerializer serialize(script, stream); serialize.NumberI32("x", 16, -16, 16); serialize.NumberI32("x", -16, -16, 16); TS_ASSERT_THROWS(serialize.NumberI32("x", 99, -16, 16), const PSERROR_Serialize_OutOfBounds&); TS_ASSERT_THROWS(serialize.NumberI32("x", -17, -16, 16), const PSERROR_Serialize_OutOfBounds&); } // TODO: test exceptions more thoroughly void helper_script_roundtrip(const char* msg, const char* input, const char* expected, size_t expstreamlen = 0, const char* expstream = NULL, const char* debug = NULL) { ScriptInterface script("Test", "Test", g_ScriptContext); ScriptRequest rq(script); JS::RootedValue obj(rq.cx); TSM_ASSERT(msg, script.Eval(input, &obj)); if (debug) { std::stringstream dbgstream; CDebugSerializer serialize(script, dbgstream); serialize.ScriptVal("script", &obj); TS_ASSERT_STR_EQUALS(dbgstream.str(), debug); } std::stringstream stream; CStdSerializer serialize(script, stream); serialize.ScriptVal("script", &obj); if (expstream) { TSM_ASSERT_STREAM(msg, stream, expstreamlen, expstream); } CStdDeserializer deserialize(script, stream); JS::RootedValue newobj(rq.cx); deserialize.ScriptVal("script", &newobj); // NOTE: Don't use good() here - it fails due to a bug in older libc++ versions TSM_ASSERT(msg, !stream.bad() && !stream.fail()); TSM_ASSERT_EQUALS(msg, stream.peek(), EOF); std::stringstream stream2; CStdSerializer serialize2(script, stream2); CStdDeserializer deserialize2(script, stream2); // Round-trip the deserialized value again. This helps ensure prototypes are correctly deserialized. serialize2.ScriptVal("script2", &newobj); deserialize2.ScriptVal("script2", &newobj); std::string source; TSM_ASSERT(msg, script.CallFunction(newobj, "toSource", source)); TS_ASSERT_STR_EQUALS(source, expected); } void test_script_basic() { helper_script_roundtrip("Object", "({'x': 123, 'y': [1, 1.5, '2', 'test', undefined, null, true, false]})", /* expected: */ "({x:123, y:[1, 1.5, \"2\", \"test\", (void 0), null, true, false]})", /* expected stream: */ 116, "\x03" // SCRIPT_TYPE_OBJECT "\x02\0\0\0" // num props "\x01\x01\0\0\0" "x" // "x" "\x05" // SCRIPT_TYPE_INT "\x7b\0\0\0" // 123 "\x01\x01\0\0\0" "y" // "y" "\x02" // SCRIPT_TYPE_ARRAY "\x08\0\0\0" // array length "\x08\0\0\0" // num props "\x01\x01\0\0\0" "0" // "0" "\x05" "\x01\0\0\0" // SCRIPT_TYPE_INT 1 "\x01\x01\0\0\0" "1" // "1" "\x06" "\0\0\0\0\0\0\xf8\x3f" // SCRIPT_TYPE_DOUBLE 1.5 "\x01\x01\0\0\0" "2" // "2" "\x04" "\x01\x01\0\0\0" "2" // SCRIPT_TYPE_STRING "2" "\x01\x01\0\0\0" "3" // "3" "\x04" "\x01\x04\0\0\0" "test" // SCRIPT_TYPE_STRING "test" "\x01\x01\0\0\0" "4" // "4" "\x00" // SCRIPT_TYPE_VOID "\x01\x01\0\0\0" "5" // "5" "\x01" // SCRIPT_TYPE_NULL "\x01\x01\0\0\0" "6" // "6" "\x07" "\x01" // SCRIPT_TYPE_BOOLEAN true "\x01\x01\0\0\0" "7" // "7" "\x07" "\x00", // SCRIPT_TYPE_BOOLEAN false /* expected debug: */ "script: {\n" " \"x\": 123,\n" " \"y\": [\n" " 1,\n" " 1.5,\n" " \"2\",\n" " \"test\",\n" " null,\n" " null,\n" " true,\n" " false\n" " ]\n" "}\n" ); } void test_script_unicode() { helper_script_roundtrip("unicode", "({" "'x': \"\\x01\\x80\\xff\\u0100\\ud7ff\", " "'y': \"\\ue000\\ufffd\"" "})", /* expected: */ "({" "x:\"\\x01\\x80\\xFF\\u0100\\uD7FF\", " "y:\"\\uE000\\uFFFD\"" "})"); // Disabled since we no longer do the UTF-8 conversion that rejects invalid characters // TS_ASSERT_THROWS(helper_script_roundtrip("invalid chars 1", "(\"\\ud7ff\\ud800\")", "..."), PSERROR_Serialize_InvalidCharInString); // TS_ASSERT_THROWS(helper_script_roundtrip("invalid chars 2", "(\"\\udfff\")", "..."), PSERROR_Serialize_InvalidCharInString); // TS_ASSERT_THROWS(helper_script_roundtrip("invalid chars 3", "(\"\\uffff\")", "..."), PSERROR_Serialize_InvalidCharInString); // TS_ASSERT_THROWS(helper_script_roundtrip("invalid chars 4", "(\"\\ud800\\udc00\")" /* U+10000 */, "..."), PSERROR_Serialize_InvalidCharInString); helper_script_roundtrip("unicode", "\"\\ud800\\uffff\"", "(new String(\"\\uD800\\uFFFF\"))"); } void test_script_objects() { helper_script_roundtrip("Number", "[1, new Number('2.0'), 3]", "[1, (new Number(2)), 3]"); helper_script_roundtrip("Number with props", "var n=new Number('2.0'); n.foo='bar'; n", "(new Number(2))"); helper_script_roundtrip("String", "['test1', new String('test2'), 'test3']", "[\"test1\", (new String(\"test2\")), \"test3\"]"); helper_script_roundtrip("String with props", "var s=new String('test'); s.foo='bar'; s", "(new String(\"test\"))"); helper_script_roundtrip("Boolean", "[new Boolean('true'), false]", "[(new Boolean(true)), false]"); helper_script_roundtrip("Boolean with props", "var b=new Boolean('true'); b.foo='bar'; b", "(new Boolean(true))"); } void test_script_fancy_objects() { // This asserts that objects are deserialized with their correct prototypes. helper_script_roundtrip("Custom Object", "" "function customObj() { this.a = this.customFunc.name; };" "customObj.prototype.customFunc = function customFunc(){};" "new customObj();", "({a:\"customFunc\"})"); helper_script_roundtrip("Custom Class", "" "class customObj {" " constructor() { this.a = this.customFunc.name; }" " customFunc(){};" "}; new customObj();", "({a:\"customFunc\"})"); helper_script_roundtrip("Custom Class with Serialize/Deserialize()", "" "class customObj {" " constructor() { this.a = this.customFunc.name; }" " Serialize() { return { 'foo': 'bar' }; }" " Deserialize(data) { this.foo = data.foo; }" " customFunc(){};" "}; new customObj();", "({a:\"customFunc\", foo:\"bar\"})"); helper_script_roundtrip("Custom Class with null serialize & deserialize()", "" "class customObj {" " constructor() { this.a = this.customFunc.name; }" " Deserialize(data) { this.test = 'test'; };" " customFunc(){};" "}; customObj.prototype.Serialize=null;" "new customObj();", "({a:\"customFunc\", test:\"test\"})"); helper_script_roundtrip("Custom Class with arguments but still works", "" "class customObj {" " constructor(test) { this.a = test; }" " Serialize() { return { 'data': this.a }; };" " Deserialize(data) { this.a = data.data; };" "}; new customObj(4);", "({a:4})"); } void test_script_objects_properties() { helper_script_roundtrip("Object with null in prop name", "({\"foo\\0bar\":1})", "({\'foo\\x00bar\':1})"); } void test_script_typed_arrays_simple() { helper_script_roundtrip("Int8Array", "var arr=new Int8Array(8);" "for(var i=0; iMount(L"", DataDir()/"mods"/"public", VFS_MOUNT_MUST_EXIST)); - TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir()/"_testcache")); + TS_ASSERT_OK(g_VFS->Mount(L"", DataDir() / "mods" / "public" / "", VFS_MOUNT_MUST_EXIST)); + TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir() / "_testcache" / "", 0, VFS_MAX_PRIORITY)); // Need some stuff for terrain movement costs: // (TODO: this ought to be independent of any graphics code) new CTerrainTextureManager; g_TexMan.LoadTerrainTextures(); CTerrain terrain; CSimulation2 sim2(NULL, g_ScriptContext, &terrain); sim2.LoadDefaultScripts(); sim2.ResetState(); std::unique_ptr mapReader = std::make_unique(); LDR_BeginRegistering(); mapReader->LoadMap(L"maps/skirmishes/Greek Acropolis (2).pmp", *sim2.GetScriptInterface().GetContext(), JS::UndefinedHandleValue, &terrain, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &sim2, &sim2.GetSimContext(), -1, false); LDR_EndRegistering(); TS_ASSERT_OK(LDR_NonprogressiveLoad()); sim2.Update(0); { std::stringstream str; std::string hash; sim2.SerializeState(str); sim2.ComputeStateHash(hash, false); debug_printf("\n"); debug_printf("# size = %d\n", (int)str.str().length()); debug_printf("# hash = "); for (size_t i = 0; i < hash.size(); ++i) debug_printf("%02x", (unsigned int)(u8)hash[i]); debug_printf("\n"); } double t = timer_Time(); CALLGRIND_START_INSTRUMENTATION; size_t reps = 128; for (size_t i = 0; i < reps; ++i) { std::string hash; sim2.ComputeStateHash(hash, false); } CALLGRIND_STOP_INSTRUMENTATION; t = timer_Time() - t; debug_printf("# time = %f (%f/%d)\n", t/reps, t, (int)reps); // Shut down the world delete &g_TexMan; g_VFS.reset(); DeleteDirectory(DataDir()/"_testcache"); CXeromyces::Terminate(); } };